Smart BOM Cloning in NetSuite Based on Material Specs

In jewelry manufacturing, BOM duplication for alternate materials—like different gold qualities or diamond grades—is a repetitive yet critical task. To streamline this, we’ve developed a NetSuite Suitelet that allows users to duplicate existing Bill of Materials (BOM) Revisions while automatically substituting components based on selected material specs such as Metal Type, Diamond Color, and Color Stone Attributes.

🛠️ What This Tool Does

  • Selects an Assembly Item and BOM Revision dynamically.
  • Auto-maps components like gold, diamonds, and stones using client-side logic (matching by quality, size, shape, etc.).
  • Highlights invalid substitutions and blocks submission if any component lacks a valid match.
  • Generates new BOMs and BOM Revisions with intelligent name handling to avoid conflicts.

✨ Key Features

  • Material-Aware Matching: Ensures only valid substitutes are used for gold and diamonds.
  • Serial History Support: Automatically loads BOM data based on build records.
  • Error Validation: Visually marks mismatched rows and prevents faulty submissions.
  • User-Friendly UI: Filtered dropdowns and dual tables to view original vs. modified components.

This Suitelet significantly reduces manual errors, speeds up BOM creation, and ensures consistency across product variants in high-precision industries like jewelry manufacturing.

jj_sl_bom_clone_spec
/**
 * @NApiVersion 2.1
 * @NScriptType Suitelet
 *
 * ***********************************************************************************************************************
 * DEWIN-331 
 * ***********************************************************************************************************************
 * 
 * Author: Jobin & Jismi IT Services LLP
 *
 * Date Created : 05 - June - 2025
 *  
 * Created By: Jobin and Jismi IT Services LLP
 *
 * Description: Suitelet to modify BOM components based on selected material and stone filters, and generate a new BOM revision.
 * 
 * REVISION HISTORY
 * @version 1.0 DEWIN-331 : 05 - June - 2025 : created initial build by JJ0312
 *
 * **************************************************************************************************************************/
define(['N/ui/serverWidget', 'N/record', 'N/runtime', 'N/search', 'N/redirect'],
    /**
     * @param{serverWidget} serverWidget
     * @param{record} record
     * @param{runtime} runtime
     * @param{search} search
     * @param{redirect} redirect
     */
    (serverWidget, record, runtime, search, redirect) => {


        // const COMPONENT_FIELDS = ['item', 'quantity', 'units', 'custrecord_bom_component_pieces', 'custrecord_component_type'];
        const GOLD_CLASS_IDS = [5, 22, 23, 24, 25];
        const DIAMOND_CLASSES_IDS = [6];
        const CS_CLASSES_IDS = [7];
        const METAL_ARRAY_GOLD = [4609, 8410, 8411]; // [G18, G22, G994]


        /**
         * Defines the Suitelet script trigger point.
         * @param {Object} scriptContext
         * @param {ServerRequest} scriptContext.request - Incoming request
         * @param {ServerResponse} scriptContext.response - Suitelet response
         * @since 2015.2
         */
        const onRequest = (scriptContext) => {
            try {
                if (scriptContext.request.method == 'GET') {
                    renderForm(scriptContext);
                } else {
                    handleSubmit(scriptContext);
                }
            } catch (e) {
                log.error({ title: 'Suitelet Error', details: e });
                scriptContext.response.write('An unexpected error occurred: ' + e.message);
            }
        };


        /**
         * Renders the Suitelet form for BOM cloning based on material and stone specifications.
         * 
         * Includes: 
         * - Input selection fields (assembly, BOM, revision, materials)
         * - Component mapping and merging
         * - Modified BOM preview logic
         * - Submission button logic with conditional display
         *
         * @function
         * @param {Object} context - The Suitelet context object
         * @param {ServerRequest} context.request - Incoming Suitelet request
         * @param {ServerResponse} context.response - Response handler to write the form
         * @returns {void}
         */
        function renderForm(context) {
            try {
                const form = serverWidget.createForm({ title: 'BOM Cloning with Material Specifications' });
                form.clientScriptModulePath = './jj_cs_bom_clone_spec.js'; // Include client script module path


                const { isRedirect, message: msgText } = context.request.parameters;


                if (isRedirect == true || isRedirect === 'true') {
                    const fieldMessage = form.addField({
                        id: 'custpage_message_text',
                        type: serverWidget.FieldType.TEXT,
                        label: 'Message Text'
                    });
                    hideFields({ fieldMessage });
                    fieldMessage.defaultValue = msgText || '';
                } else {
                    // Field Group: Selection
                    form.addFieldGroup({ id: 'selection_group', label: 'Select BOM Details' });


                    const serialField = form.addField({ id: 'custpage_serial_number', type: serverWidget.FieldType.SELECT, label: 'Enter/Scan Serial Number', source: 'inventorynumber', container: 'selection_group' });
                    setFieldHelp(serialField, 'Scan or enter a serial number. BOM and revision details will auto-load.');


                    const assemblyField = form.addField({ id: 'custpage_assembly', type: serverWidget.FieldType.SELECT, label: 'Assembly Item', source: 'assemblyitem', container: 'selection_group' });
                    setFieldHelp(assemblyField, 'Select the parent Assembly Item to which the BOM is associated.');
                    assemblyField.isMandatory = true;


                    const bomField = form.addField({ id: 'custpage_bom', type: serverWidget.FieldType.SELECT, label: 'BOM', container: 'selection_group' });
                    setFieldHelp(bomField, 'Select the Bill of Materials for the selected Assembly Item.');
                    bomField.isMandatory = true;


                    const revField = form.addField({ id: 'custpage_bomrev', type: serverWidget.FieldType.SELECT, label: 'BOM Revision', container: 'selection_group' });
                    setFieldHelp(revField, 'Select the active BOM Revision to view or modify components.');
                    revField.isMandatory = true;


                    const metalField = form.addField({ id: 'custpage_metal', type: serverWidget.FieldType.SELECT, label: 'Metal Type', container: 'selection_group' });
                    setFieldHelp(metalField, 'Choose a Metal type to be used for replacement in the modified BOM.');
                    metalField.isMandatory = true;


                    const metalsobj = getMetals();
                    populateSelectField(metalField, metalsobj);


                    const diamondField = form.addField({ id: 'custpage_diamond_color', type: serverWidget.FieldType.SELECT, label: 'Diamond Color', source: 'customrecord_jj_stone_color', container: 'selection_group' });
                    setFieldHelp(diamondField, 'Select the Diamond to be matched in the new BOM components.');


                    const csField = form.addField({ id: 'custpage_cs_color', type: serverWidget.FieldType.SELECT, label: 'Color Stone Color', source: 'customrecord_jj_colorstonecolour', container: 'selection_group' });
                    setFieldHelp(csField, 'Select the Color Stone to be matched in the new BOM components.');


                    const effectiveStartDateField = form.addField({ id: 'custpage_effective_start_date', type: serverWidget.FieldType.DATE, label: 'Effective Start Date', container: 'selection_group' });
                    setFieldHelp(effectiveStartDateField, 'This date defines when the new BOM Revision becomes effective.');
                    effectiveStartDateField.isMandatory = true;


                    const bomNameField = form.addField({ id: 'custpage_bom_name', type: serverWidget.FieldType.TEXT, label: 'BOM Name', container: 'selection_group' });
                    setFieldHelp(bomNameField, 'Enter a clear, unique name for the BOM.');
                    bomNameField.isMandatory = true;
                    hideFields({ bomNameField });


                    // Retrieve params        
                    let {
                        custpage_assembly: assemblyItemId,
                        custpage_assembly_name: assemblyItemName,
                        custpage_bom: bomId,
                        custpage_bom_name: bomName,
                        custpage_bomrev: bomRevisionId,
                        custpage_metal: metalId,
                        custpage_metal_name: metalName,
                        custpage_diamond_color: diamondColorId,
                        custpage_diamond_color_name: diamondColorName,
                        custpage_cs_color: colorStoneColorId,
                        custpage_cs_color_name: csColorName,
                        custpage_effective_start_date: effectiveStartDate,
                        custpage_serial_number: serialId
                    } = context.request.parameters;


                    let serialDetails, bomRevisionName;
                    if (serialId) {
                        serialField.defaultValue = serialId;
                        serialDetails = getSerialDetails(serialId);


                        assemblyItemId = serialDetails.assemblyItemId;
                        bomId = serialDetails.bomId;
                        bomRevisionId = serialDetails.bomRevisionId;
                        bomRevisionName = serialDetails.bomRevisionName;


                        const metalComponent = serialDetails?.modifiedComponents?.find(component =>
                            metalsobj.some(metal => String(metal.id) === String(component.itemId))
                        );
                        metalId = metalComponent?.itemId;


                        bomNameField.updateDisplayType({ displayType: serverWidget.FieldDisplayType.NORMAL });
                        bomField.isMandatory = false;
                        hideFields({ bomField });
                        disableFields({ assemblyField, bomField, revField, metalField, diamondField, csField });
                    }


                    if (assemblyItemId) {
                        assemblyField.defaultValue = assemblyItemId;
                        const bomOptions = getBOMsForAssembly(assemblyItemId);
                        if (bomOptions.length > 0) populateSelectField(bomField, bomOptions);
                        if (bomOptions.length == 1) {
                            bomId = bomOptions[0].id;
                        }
                        if (bomId) bomField.defaultValue = bomId;
                    } else {
                        bomField.addSelectOption({ value: '', text: '' });
                    }


                    if (bomId) {
                        const revOptions = getRevisionsForBOM(bomId);
                        if (revOptions?.length > 0) populateSelectField(revField, revOptions);
                        if (revOptions.length == 1) {
                            bomRevisionId = revOptions[0].id;
                        }
                        if (bomRevisionId) revField.defaultValue = bomRevisionId;
                    } else if (serialId) {
                        if (bomRevisionId) {
                            revField.addSelectOption({ value: bomRevisionId, text: bomRevisionName });
                            revField.defaultValue = bomRevisionId;
                        }
                    } else {
                        revField.addSelectOption({ value: '', text: '' });
                    }


                    if (metalId) metalField.defaultValue = metalId;
                    if (diamondColorId) diamondField.defaultValue = diamondColorId;
                    if (colorStoneColorId) csField.defaultValue = colorStoneColorId;
                    effectiveStartDate ? effectiveStartDateField.defaultValue = new Date(effectiveStartDate) : effectiveStartDateField.defaultValue = new Date();


                    // Tabs
                    form.addTab({ id: 'custpage_tab_original', label: 'Section One' });
                    form.addTab({ id: 'custpage_tab_modified', label: 'Section Two' });


                    // Subtabs
                    form.addSubtab({ id: 'custpage_subtab_original', label: 'Original Components', tab: 'custpage_tab_original' });
                    form.addSubtab({ id: 'custpage_subtab_modified', label: 'Modified Components', tab: 'custpage_tab_modified' });


                    if (serialId) {
                        if (serialDetails) {
                            assemblyItemId = serialDetails.assemblyItemId;
                            bomId = serialDetails.bomId;
                            bomRevisionId = serialDetails.bomRevisionId;


                            const originalComponents = serialDetails.originalComponents || [];
                            log.debug("originalComponents", originalComponents);
                            const modifiedComponents = serialDetails.modifiedComponents || [];
                            log.debug("modifiedComponents", modifiedComponents);
                            const revisionMetadata = serialDetails.revisionMetadata || {};


                            // Default values
                            if (assemblyItemId) assemblyField.defaultValue = assemblyItemId;
                            if (bomId) bomField.defaultValue = bomId;
                            if (bomRevisionId) revField.defaultValue = bomRevisionId;


                            // Render original components
                            renderComponentTable(form, originalComponents, 'original', 'Original BOM Components', 'custpage_subtab_original');


                            // Render modified components if available
                            if (modifiedComponents.length) {
                                renderComponentTable(form, modifiedComponents, 'modified', 'Modified Components', 'custpage_subtab_modified');


                                const sublistJSONField = form.addField({ id: 'custpage_modified_comp_json', type: serverWidget.FieldType.LONGTEXT, label: 'Modified Components JSON' });
                                sublistJSONField.defaultValue = JSON.stringify(modifiedComponents);


                                const metadataJSONField = form.addField({ id: 'custpage_modified_metadata_json', type: serverWidget.FieldType.LONGTEXT, label: 'Revision Metadata JSON' });
                                metadataJSONField.defaultValue = JSON.stringify([revisionMetadata]);


                                hideFields({ sublistJSONField, metadataJSONField })


                                form.addSubmitButton('Create New BOM & Revision');
                            }
                        }
                    }
                    // Only show 1st table if item, bom and revision selected
                    else if (bomRevisionId && assemblyItemId && bomId) {
                        const originalComponentsObj = getBOMRevisionComponents(bomRevisionId);
                        // log.debug("originalComponentsObj", originalComponentsObj);


                        const originalComponents = originalComponentsObj?.components;
                        const revisionMetadata = originalComponentsObj?.revisionMetadata;


                        // Render original component table
                        renderComponentTable(form, originalComponents, 'original', 'Original BOM Components', 'custpage_subtab_original');


                        // Show second table only if metal & diamond Color selected
                        if (metalId || diamondColorId || colorStoneColorId) {


                            // Create a unique list of itemIds from originalComponents (excluding null/undefined)
                            const itemIds = [...new Set(originalComponents.map(c => c.itemId).filter(Boolean))];


                            if (!itemIds.includes(metalId)) {
                                itemIds.push(metalId)
                            }


                            const componetItemDetails = getComponetItemDetails(itemIds);
                            const componentDetailMap = Object.fromEntries(
                                componetItemDetails.map(item => [String(item.id), item])
                            );


                            // Merge additional details into each original component
                            originalComponents.forEach(comp => {
                                const detail = componentDetailMap[String(comp.itemId)];
                                if (detail) {
                                    comp.class = detail.class;
                                    comp.diamondShape = detail.diamondShape;
                                    comp.qualityGroup = detail.qualityGroup;
                                    comp.quality = detail.quality;
                                    comp.diamondColor = detail.diamondColor;
                                    comp.sizeGroup = detail.sizeGroup;
                                    comp.size = detail.size;
                                    comp.sizeMM = detail.sizeMM;
                                    comp.category = detail.category;
                                    comp.categoryGroup = detail.categoryGroup;
                                    comp.subCategory = detail.subCategory;
                                    comp.metalColor = detail.metalColor;
                                    comp.metalQuality = detail.metalQuality;
                                    comp.metalPurityPercent = detail.metalPurityPercent;
                                    comp.parentId = detail.parentId;
                                    comp.stoneColor = detail.stoneColor;
                                    comp.stoneShape = detail.stoneShape;
                                    comp.stoneWeight = detail.stoneWeight;
                                    comp.stoneSize = detail.stoneSize;
                                }
                            });
                            log.debug("originalComponents", originalComponents);
                            // Get the metal item object from the result
                            const metalItemObj = componetItemDetails.find(item => String(item.id) == String(metalId));
                            // Get the metal color ID from the item
                            const metalColorId = metalItemObj?.metalColor;
                            const metalQualityId = metalItemObj?.metalQuality;


                            const replacements = findAllReplacementItems(componetItemDetails, metalColorId, diamondColorId, colorStoneColorId, metalQualityId);
                            log.debug("replacements", replacements);
                            const modifiedComponents = applyReplacements(originalComponents, replacements, metalId, metalName, diamondColorId, metalColorId, colorStoneColorId, metalsobj, metalQualityId);
                            log.debug("modifiedComponents", modifiedComponents);
                            const hasError = modifiedComponents.some(comp => comp.missing);


                            // Render new component table
                            renderComponentTable(form, modifiedComponents, 'modified', 'Modified Components', 'custpage_subtab_modified');


                            const sublistJSONField = form.addField({ id: 'custpage_modified_comp_json', type: serverWidget.FieldType.LONGTEXT, label: 'Modified Components JSON' });
                            sublistJSONField.defaultValue = JSON.stringify(modifiedComponents);


                            const metadataJSONField = form.addField({ id: 'custpage_modified_metadata_json', type: serverWidget.FieldType.LONGTEXT, label: 'Modified Components JSON' });
                            metadataJSONField.defaultValue = JSON.stringify([revisionMetadata]);


                            const metalItemNameField = form.addField({ id: 'custpage_metal_name', type: serverWidget.FieldType.TEXT, label: 'Metal Name' });
                            metalItemNameField.defaultValue = metalName;


                            const diamondColorNameField = form.addField({ id: 'custpage_diamond_color_name', type: serverWidget.FieldType.TEXT, label: 'Metal Color Name' });
                            diamondColorNameField.defaultValue = diamondColorName;


                            const csColorNameField = form.addField({ id: 'custpage_cs_color_name', type: serverWidget.FieldType.TEXT, label: 'Metal Color Name' });
                            csColorNameField.defaultValue = csColorName;


                            const assemblyItemNameField = form.addField({ id: 'custpage_assembly_name', type: serverWidget.FieldType.TEXT, label: 'Assembly Item Name' });
                            assemblyItemNameField.defaultValue = assemblyItemName;


                            bomNameField.defaultValue = bomName;


                            hideFields({ sublistJSONField, metadataJSONField, metalItemNameField, diamondColorNameField, csColorNameField, assemblyItemNameField })


                            if (!hasError) form.addSubmitButton('Create New BOM & Revision');
                            else {
                                const errorNote = form.addField({
                                    id: 'custpage_error_note',
                                    type: serverWidget.FieldType.INLINEHTML,
                                    label: 'Error Notice'
                                });
                                errorNote.defaultValue = `
                                    <div style="color:red; font-size: 13px; font-weight: normal; padding: 6px 0;">
                                        Some components could not be matched. Please adjust the filters or check your items and their specifications before proceeding.
                                    </div>
                                `;
                            }
                        }
                    }
                }


                context.response.writePage(form);
            } catch (e) {
                log.error({ title: 'Render Form Error', details: e });
                context.response.write('Failed to load form: ' + e.message);
            }
        }


        /**
         * Handles Suitelet form submission, creates BOM and BOM Revision based on input and modified components.
         *
         * @function
         * @param {Object} context - Suitelet context object containing request parameters
         * @returns {void}
         */
        function handleSubmit(context) {
            try {
                const {
                    custpage_assembly: assemblyId,
                    custpage_assembly_name: assemblyItemName,
                    custpage_modified_comp_json,
                    custpage_modified_metadata_json,
                    custpage_metal_name: metalName,
                    custpage_diamond_color_name: diamondColorName,
                    custpage_cs_color_name: csColorName,
                    custpage_effective_start_date: effectiveDateString,
                    custpage_bom_name: bomName,
                    custpage_serial_number: serialId
                } = context.request.parameters;


                if (!assemblyId || !custpage_modified_comp_json) {
                    context.response.write('Missing required data: assembly or component list.');
                    return;
                }
                let effectiveDate;
                if (effectiveDateString) {
                    effectiveDate = new Date(effectiveDateString);
                }


                const modifiedComponents = parseComponents(custpage_modified_comp_json);
                if (!modifiedComponents.length) {
                    context.response.write('No components available for BOM creation.');
                    return;
                }
                const revisionMetadata = parseComponents(custpage_modified_metadata_json);


                let formatedName = "";
                let bomId = "";
                formatedName = !serialId ? assemblyItemName.trim() + "/" + metalName?.trim() + "/" + diamondColorName.trim() + "/" + csColorName.trim() : bomName;
                bomId = createNewBOM(assemblyId, formatedName);


                const revisionId = createNewBOMRevision(bomId, assemblyId, modifiedComponents, revisionMetadata[0] || {}, formatedName, effectiveDate);


                addAssemblyToBOM(bomId, assemblyId);
                redirectSuitelet(true, `New BOM '${formatedName}' (#${bomId}) and Revision '${formatedName}' (#${revisionId}) created successfully.`);
            } catch (e) {
                log.error({ title: 'Submit Error', details: e });
                redirectSuitelet(true, e.message);
            }
        }


        /**
         * Sets contextual help text on a NetSuite field, if provided.
         *
         * @function
         * @param {Field} field - The NetSuite UI field object
         * @param {string} help - Help text to display to the user
         * @returns {void}
         */
        function setFieldHelp(field, help) {
            try {
                if (help) field.setHelpText({ help: help });
            } catch (e) {
                log.error("Error @setHelp", e);
            }
        }


        /**
         * Hides multiple NetSuite UI fields by setting their display type to HIDDEN.
         *
         * @param {Object} fieldsObj - Object containing NetSuite Field objects
         */
        function hideFields(fieldsObj) {
            try {
                Object.values(fieldsObj).forEach(field => {
                    field.updateDisplayType({
                        displayType: serverWidget.FieldDisplayType.HIDDEN
                    });
                });
            } catch (e) {
                log.error('Error in hideFields', e);
            }
        }


        /**
         * Disables multiple NetSuite UI fields by setting their display type to DISABLED.
         *
         * @param {Object} fieldsObj - Object containing NetSuite Field objects
         */
        function disableFields(fieldsObj) {
            try {
                Object.values(fieldsObj).forEach(field => {
                    field.updateDisplayType({
                        displayType: serverWidget.FieldDisplayType.DISABLED
                    });
                });
            } catch (e) {
                log.error('Error in disableFields', e);
            }
        }


        /**
         * Redirects user back to the same Suitelet with optional success or error message.
         *
         * @function
         * @param {boolean} isRedirect - Flag to indicate redirection intent
         * @param {string} message - Message to be passed to the Suitelet UI
         * @returns {void}
         */
        function redirectSuitelet(isRedirect, message) {
            try {
                const suiteletScriptId = runtime.getCurrentScript().id;
                const suiteletDeployId = runtime.getCurrentScript().deploymentId;
                redirect.toSuitelet({
                    scriptId: suiteletScriptId,
                    deploymentId: suiteletDeployId,
                    parameters: {
                        isRedirect: isRedirect,
                        message: message
                    }
                });
            } catch (e) {
                log.error("Error @redirectSuitelet", e);
                context.response.write('Failed to redirect: ' + e.message);
            }
        }


        /**
         * Parses a JSON string to an array of components. Returns an empty array if parsing fails.
         *
         * @function
         * @param {string} jsonString - JSON string of component list
         * @returns {Array<Object>} Parsed component list or empty array
         */
        function parseComponents(jsonString) {
            try {
                const parsed = JSON.parse(jsonString || '[]');
                return Array.isArray(parsed) ? parsed : [];
            } catch (e) {
                log.error('Component Parse Error', e);
                return [];
            }
        }


        /**
         * Adds select options to a NetSuite form field safely.
         *
         * @param {Field} field - NetSuite form field
         * @param {Array<{id: string, name: string}>} options - List of options to add
         */
        function populateSelectField(field, options) {
            try {
                field.addSelectOption({ value: '', text: '' }); // Always include a blank option first
                options.forEach(opt => {
                    field.addSelectOption({ value: opt.id, text: opt.name });
                });
            } catch (error) {
                log.error(`Error in populateSelectField: ${field.id}`, error.message || error);
            }
        }


        /**
         * Renders a sublist on the Suitelet form to display component data.
         *
         * @function
         * @param {Form} form - NetSuite UI form object
         * @param {Array<Object>} components - List of component objects to render
         * @param {string} prefix - Prefix for field IDs to differentiate sublists
         * @param {string} label - Label for the sublist UI section
         * @param {string} tabId - ID of the parent tab where the sublist will be displayed
         * @returns {void}
         */
        function renderComponentTable(form, components, prefix, label, tabId) {
            try {
                const sublist = form.addSublist({ id: `sublist_${prefix}`, label: label, type: serverWidget.SublistType.LIST, tab: tabId });


                sublist.addField({ id: `${prefix}_item`, type: serverWidget.FieldType.TEXT, label: 'Item' });
                sublist.addField({ id: `${prefix}_qty`, type: serverWidget.FieldType.FLOAT, label: 'Quantity' });
                sublist.addField({ id: `${prefix}_unit`, type: serverWidget.FieldType.TEXT, label: 'Units' });
                sublist.addField({ id: `${prefix}_pcs`, type: serverWidget.FieldType.INTEGER, label: 'Pieces' });
                sublist.addField({ id: `${prefix}_type`, type: serverWidget.FieldType.TEXT, label: 'Type' });


                components.forEach((comp, i) => {
                    if (comp.itemName) sublist.setSublistValue({ id: `${prefix}_item`, line: i, value: comp.itemName });
                    if (comp.quantity) sublist.setSublistValue({ id: `${prefix}_qty`, line: i, value: String(comp.quantity) });
                    if (comp.unit) sublist.setSublistValue({ id: `${prefix}_unit`, line: i, value: comp.unit });
                    if (comp.pieces) sublist.setSublistValue({ id: `${prefix}_pcs`, line: i, value: String(comp.pieces) });
                    if (comp.type || comp.class) sublist.setSublistValue({ id: `${prefix}_type`, line: i, value: comp.type || comp.class });
                });
            } catch (e) {
                log.error({ title: 'Render Table Error', details: e });
                throw e;
            }
        }


        /**
         * Applies replacement logic to a list of components based on type and material matching.
         *
         * @function
         * @param {Array<Object>} components - Original component list from BOM Revision
         * @param {Array<Object>} replacements - List of eligible replacement items
         * @param {string} metalId - Selected metal item ID
         * @param {string} metalName - Display name for the metal item
         * @param {string} diamondColorId - Selected diamond color ID
         * @param {string} metalColorId - Selected metal color ID
         * @param {string} colorStoneColorId - Selected color stone color ID
         * @param {Array<Object>} metalsobj - List of available metals
         * @param {string} metalQualityId - Selected metal quality ID
         * @returns {Array<Object>} Modified component list with replacements applied
         */
        function applyReplacements(components, replacements, metalId, metalName, diamondColorId, metalColorId, colorStoneColorId, metalsobj, metalQualityId) {
            try {
                return components.map(component => {
                    let matched = null;


                    const isGold = GOLD_CLASS_IDS.includes(Number(component.typeId));
                    const isDiamond = DIAMOND_CLASSES_IDS.includes(Number(component.typeId));
                    const isColorStone = CS_CLASSES_IDS.includes(Number(component.typeId));


                    if (isGold) {
                        const isMetalMatch = metalsobj.some(metal => String(metal.id) == String(component.itemId));
                        // log.debug("isMetalMatch, itemId metalsobj", { isMetalMatch, itemId: component.itemId, metalsobj });
                        if (isMetalMatch) {
                            matched = {
                                id: metalId,
                                name: metalName
                            };
                            // log.debug("matched Parent Gold", matched);
                        } else {
                            // for (const item of replacements) {
                            //     // log.debug(`Checking item:`, `${component.itemName} VS ${item.name}`);


                            //     const classMatch = item.class == component.typeId;
                            //     const metalColorMatch = item.metalColor == metalColorId;
                            //     const metalQualityMatch = item.metalQuality == metalQualityId;
                            //     const purityMatch = (!component.metalPurityPercent && !item.metalPurityPercent) || item.metalPurityPercent == component.metalPurityPercent;
                            //     const categoryMatch = (!component.category && !item.category) || item.category == component.category;
                            //     const categoryGroupMatch = (!component.categoryGroup && !item.categoryGroup) || item.categoryGroup == component.categoryGroup;
                            //     const subCategoryMatch = (!component.subCategory && !item.subCategory) || item.subCategory == component.subCategory;
                            //     const parentIdMatch = (!component.parentId && !item.parentId) || item.parentId == component.parentId;


                            //     log.debug("Match breakdown", { 
                            //         classMatch: classMatch, 
                            //         metalColorMatch:metalColorMatch, 
                            //         metalQualityMatch: metalQualityMatch, 
                            //         purityMatch: purityMatch, 
                            //         categoryMatch: categoryMatch, 
                            //         categoryGroupMatch: categoryGroupMatch, 
                            //         subCategoryMatch: subCategoryMatch, 
                            //         parentIdMatch: parentIdMatch
                            //     });


                            //     if (classMatch && metalColorMatch && metalQualityMatch && purityMatch && categoryMatch && categoryGroupMatch && subCategoryMatch && parentIdMatch) {
                            //         matched = item;
                            //         log.audit(`Match found: ${item.name}`, { id: item.id });
                            //         break;
                            //     } else {
                            //         log.debug(`No match for: ${item.name}`, { id: item.id });
                            //     }
                            // }
                            matched = replacements.find(item =>
                                item.class == component.typeId &&
                                item.metalColor == metalColorId &&
                                item.metalQuality == metalQualityId &&
                                ((!component.metalPurityPercent && !item.metalPurityPercent) || item.metalPurityPercent == component.metalPurityPercent) &&
                                ((!component.category && !item.category) || item.category == component.category) &&
                                ((!component.categoryGroup && !item.categoryGroup) || item.categoryGroup == component.categoryGroup) &&
                                ((!component.subCategory && !item.subCategory) || item.subCategory == component.subCategory) &&
                                ((!component.parentId && !item.parentId) || item.parentId == component.parentId)
                            );
                        }
                    }


                    if (isDiamond) {
                        matched = replacements.find(item =>
                            item.class == component.typeId &&
                            item.diamondColor == diamondColorId &&
                            ((!component.diamondShape && !item.diamondShape) || item.diamondShape == component.diamondShape) &&
                            ((!component.qualityGroup && !item.qualityGroup) || item.qualityGroup == component.qualityGroup) &&
                            ((!component.quality && !item.quality) || item.quality == component.quality) &&
                            ((!component.sizeGroup && !item.sizeGroup) || item.sizeGroup == component.sizeGroup) &&
                            ((!component.size && !item.size) || item.size == component.size) &&
                            ((!component.sizeMM && !item.sizeMM) || item.sizeMM == component.sizeMM)
                        );
                    }


                    if (isColorStone) {
                        matched = replacements.find(item =>
                            item.class == component.typeId &&
                            item.stoneColor == colorStoneColorId &&
                            ((!component.qualityGroup && !item.qualityGroup) || item.qualityGroup == component.qualityGroup) &&
                            ((!component.stoneShape && !item.stoneShape) || item.stoneShape == component.stoneShape) &&
                            ((!component.stoneWeight && !item.stoneWeight) || item.stoneWeight == component.stoneWeight) &&
                            ((!component.stoneSize && !item.stoneSize) || item.stoneSize == component.stoneSize)
                        );
                    }


                    return {
                        ...component,
                        itemId: matched?.id,
                        itemName: matched?.name,
                        missing: !matched
                    };
                });
            } catch (e) {
                log.error({ title: 'Apply Replacements Error', details: e });
                throw e;
            }
        }


        /**
         * Creates a new Bill of Materials (BOM) record for the specified assembly item.
         *
         * @function
         * @param {string|number} assemblyId - Internal ID of the assembly item
         * @param {string} formatedName - Name to be assigned to the new BOM
         * @returns {number} Internal ID of the newly created BOM
         */
        function createNewBOM(assemblyId, formatedName) {
            const bomRec = record.create({ type: record.Type.BOM, isDynamic: true });


            bomRec.setValue({ fieldId: 'name', value: formatedName });
            bomRec.setValue({ fieldId: 'usecomponentyield', value: true });
            bomRec.setValue({ fieldId: 'availableforallassemblies', value: false });
            bomRec.setValue({ fieldId: 'restricttoassemblies', value: assemblyId });
            bomRec.setValue({ fieldId: 'assembly', value: assemblyId });


            const bomId = bomRec.save({ enableSourcing: true, ignoreMandatoryFields: true });
            log.audit('Created BOM', bomId);
            return bomId;
        }


        /**
         * Creates a BOM Revision for a given BOM with specified components and metadata.
         *
         * @function
         * @param {number} bomId - Internal ID of the BOM
         * @param {number} assemblyId - Internal ID of the assembly item
         * @param {Array<Object>} components - Array of component objects to be added to the revision
         * @param {Object} revisionMetadata - Metadata including weights and stone details
         * @param {string} formatedName - Name of the BOM Revision
         * @param {Date} effectiveDate - Effective start and end date for the revision
         * @returns {number} Internal ID of the newly created BOM Revision
         */
        function createNewBOMRevision(bomId, assemblyId, components, revisionMetadata, formatedName, effectiveDate) {
            const revision = record.create({ type: record.Type.BOM_REVISION, isDynamic: true });


            revision.setValue({ fieldId: 'name', value: formatedName });
            revision.setValue({ fieldId: 'billofmaterials', value: bomId });
            revision.setValue({ fieldId: 'custrecord_jj_bom_rev_assm_item', value: assemblyId });
            revision.setValue({ fieldId: 'effectivestartdate', value: effectiveDate ? effectiveDate : new Date() });


            // Set BOM Revision-level body fields from metadata
            if (revisionMetadata) {
                if (revisionMetadata.csWeight)
                    revision.setValue({ fieldId: 'custrecord_jj_bom_rev_cs_weight', value: Number(revisionMetadata.csWeight) });


                if (revisionMetadata.diamondPieces)
                    revision.setValue({ fieldId: 'custrecord_jj_bom_rev_diam_pcs', value: Number(revisionMetadata.diamondPieces) });


                if (revisionMetadata.diamondWeight)
                    revision.setValue({ fieldId: 'custrecord_jj_bom_rev_diamond_weight', value: Number(revisionMetadata.diamondWeight) });


                if (revisionMetadata.grossWeight)
                    revision.setValue({ fieldId: 'custrecord_jj_bom_rev_gross_weight', value: Number(revisionMetadata.grossWeight) });


                if (revisionMetadata.netWeight)
                    revision.setValue({ fieldId: 'custrecord_jj_bom_rev_net_weight', value: Number(revisionMetadata.netWeight) });


                if (revisionMetadata.pureWeight)
                    revision.setValue({ fieldId: 'custrecord_jj_bom_rev_pure_weight', value: Number(revisionMetadata.pureWeight) });
            }


            // Add Components
            components.forEach(comp => {
                if (!comp.itemId) return;


                revision.selectNewLine({ sublistId: 'component' });
                revision.setCurrentSublistValue({ sublistId: 'component', fieldId: 'item', value: comp.itemId });
                revision.setCurrentSublistValue({ sublistId: 'component', fieldId: 'bomquantity', value: Number(comp.quantity) || 1 });
                if (comp.pieces) revision.setCurrentSublistValue({ sublistId: 'component', fieldId: 'custrecord_jj_bom_rev_pieces', value: comp.pieces });
                // if (comp.typeId) revision.setCurrentSublistValue({ sublistId: 'component', fieldId: 'custrecord_jj_bom_rev_purchase_type', value: comp.typeId });
                revision.commitLine({ sublistId: 'component' });
            });


            const revisionId = revision.save({ enableSourcing: true, ignoreMandatoryFields: true });
            log.audit('Created BOM Revision', revisionId);
            return revisionId;
        }


        /**
         * Adds an assembly line item to a Bill of Materials (BOM) record.
         *
         * Loads the specified BOM record, adds a new line with the given assembly ID
         * to the assembly sublist, and saves the updated BOM record.
         *
         * @param {string|number} bomId - The internal ID of the BOM record to update.
         * @param {string|number} assemblyId - The internal ID of the assembly to add to the BOM.
         *
         * @throws Will log an error if loading or saving the BOM fails.
         */
        function addAssemblyToBOM(bomId, assemblyId) {
            try {
                let bom = record.load({ type: record.Type.BOM, id: bomId, isDynamic: true });


                bom.selectNewLine({ sublistId: 'assembly' });
                bom.setCurrentSublistValue({ sublistId: 'assembly', fieldId: 'assembly', value: assemblyId });
                bom.commitLine({ sublistId: 'assembly' });


                let savedId = bom.save({ enableSourcing: true, ignoreMandatoryFields: true });
                log.audit('Updated BOM with assembly', `BOM ID: ${savedId}, Assembly ID: ${assemblyId}`);
            } catch (e) {
                log.error('Error adding assembly to BOM', e);
            }
        }




        // Saved Searches


        /**
         * Retrieves component-level and body-level data for a given BOM Revision.
         *
         * @function
         * @param {string|number} bomRevisionId - Internal ID of the BOM Revision
         * @returns {Object} An object with two keys:
         *  - `components`: List of line-level component data
         *  - `revisionMetadata`: Object containing BOM revision weights and attributes
         */
        function getBOMRevisionComponents(bomRevisionId) {
            try {
                const components = [];
                let revisionMetadata = {}; // For body-level fields


                const bomrevisionSearchObj = search.create({
                    type: "bomrevision",
                    filters: [
                        ["isinactive", "is", "F"],
                        "AND", ["internalid", "anyof", bomRevisionId]
                    ],
                    columns: [
                        search.createColumn({ name: "internalid", label: "Internal ID" }),
                        search.createColumn({ name: "name", label: "Name" }),
                        search.createColumn({ name: "billofmaterials", label: "Bill of Materials" }),
                        search.createColumn({ name: "item", join: "component", label: "Item" }),
                        search.createColumn({ name: "custrecord_jj_bom_rev_purchase_type", join: "component", label: "Purchase Type" }),
                        search.createColumn({ name: "custrecord_jj_bom_rev_pieces", join: "component", label: "Pieces" }),
                        search.createColumn({ name: "lineid", join: "component", label: "Line ID" }),
                        search.createColumn({ name: "itemtype", join: "component", label: "Item Type" }),
                        search.createColumn({ name: "quantity", join: "component", label: "Quantity" }),
                        search.createColumn({ name: "units", join: "component", label: "Units" }),


                        // Body-level BOM Revision fields (same for all rows)
                        search.createColumn({ name: "custrecord_jj_bom_rev_cs_weight", label: "Color Stone Weight (CSWT)" }),
                        search.createColumn({ name: "custrecord_jj_bom_rev_diam_pcs", label: "Diamond Pieces (DPCS)" }),
                        search.createColumn({ name: "custrecord_jj_bom_rev_diamond_weight", label: "Diamond Weight (DWT)" }),
                        search.createColumn({ name: "custrecord_jj_bom_rev_gross_weight", label: "Gross Weight (GWT)" }),
                        search.createColumn({ name: "custrecord_jj_bom_rev_net_weight", label: "Net Weight (NWT)" }),
                        search.createColumn({ name: "custrecord_jj_bom_rev_pure_weight", label: "Pure Weight (PWT)" })
                    ]
                });


                bomrevisionSearchObj.run().each(result => {
                    // Extract body fields only once (from first result row)
                    if (Object.keys(revisionMetadata).length === 0) {
                        revisionMetadata = {
                            csWeight: parseFloat(result.getValue({ name: 'custrecord_jj_bom_rev_cs_weight' }) || 0),
                            diamondPieces: parseInt(result.getValue({ name: 'custrecord_jj_bom_rev_diam_pcs' }) || 0),
                            diamondWeight: parseFloat(result.getValue({ name: 'custrecord_jj_bom_rev_diamond_weight' }) || 0),
                            grossWeight: parseFloat(result.getValue({ name: 'custrecord_jj_bom_rev_gross_weight' }) || 0),
                            netWeight: parseFloat(result.getValue({ name: 'custrecord_jj_bom_rev_net_weight' }) || 0),
                            pureWeight: parseFloat(result.getValue({ name: 'custrecord_jj_bom_rev_pure_weight' }) || 0)
                        };
                    }


                    const itemId = result.getValue({ name: 'item', join: 'component' });
                    const itemName = result.getText({ name: 'item', join: 'component' });
                    const itemType = result.getText({ name: 'itemtype', join: 'component' });
                    const quantity = Number(parseFloat(result.getValue({ name: 'quantity', join: 'component' }) || 0).toFixed(4));
                    const unit = result.getValue({ name: 'units', join: 'component' }) || '';
                    const pieces = parseInt(result.getValue({ name: 'custrecord_jj_bom_rev_pieces', join: 'component' }) || 0);
                    const purchaseType = result.getText({ name: 'custrecord_jj_bom_rev_purchase_type', join: 'component' }) || '';
                    const purchaseTypeId = result.getValue({ name: 'custrecord_jj_bom_rev_purchase_type', join: 'component' }) || '';


                    components.push({
                        itemId: itemId,
                        itemName: itemName,
                        itemType: itemType,
                        quantity: quantity,
                        unit: unit,
                        pieces: pieces,
                        type: purchaseType,
                        typeId: purchaseTypeId
                    });


                    return true;
                });


                return { components, revisionMetadata };


            } catch (e) {
                log.error({ title: 'Get BOM Revision Components Error', details: e });
                throw e;
            }
        }


        /**
         * Retrieves list of metal assembly items.
         *
         * @returns {Array<{id: string, name: string}>}
         */
        function getMetals() {
            try {
                const metalList = [];
                const filters = [
                    ['isinactive', 'is', 'F'],
                    'AND', ['type', 'anyof', 'Assembly'],
                    'AND', ['parent', 'anyof', METAL_ARRAY_GOLD]
                ];


                const metalSearch = search.create({
                    type: 'assemblyitem',
                    filters: filters,
                    columns: ['internalid', 'itemid', 'parent']
                });


                metalSearch.run().each(result => {
                    const itemName = result.getValue('itemid');
                    const parentName = result.getText('parent');
                    const formatted = itemName?.split(parentName + ' : ')?.pop();


                    metalList.push({
                        id: result.getValue('internalid'),
                        name: formatted || itemName
                    });


                    return true;
                });
                return metalList;
            } catch (error) {
                log.error('Error in getMetals', error);
                return [];
            }
        }


        /**
         * Retrieves detailed attribute information for a list of item IDs.
         *
         * @function
         * @param {Array<string|number>} itemIds - Array of internal IDs of items
         * @returns {Array<Object>} Array of item detail objects including metal, stone, and classification data
         */
        function getComponetItemDetails(itemIds) {
            try {
                let results = [];


                if (!itemIds || itemIds.length == 0) return results;


                const itemSearchObj = search.create({
                    type: "item",
                    filters: [
                        ["internalid", "anyof", itemIds],
                        "AND", ["isinactive", "is", "F"]
                    ],
                    columns: [
                        "internalid",
                        "itemid",
                        "class",
                        "custitem_jj_stone_shape",
                        "custitem_jj_stone_quality_group",
                        "custitem_jj_stone_quality",
                        "custitem_jj_stone_color",
                        "custitem_jj_stone_size_group",
                        "custitem_jj_stone_size",
                        "custitem_jj_stone_size_mm",
                        "custitem_jj_category",
                        "custitem_jj_category_group",
                        "custitem_jj_sub_category",
                        "custitem_jj_metal_color",
                        "custitem_jj_metal_quality",
                        "custitem_jj_metal_purity_percent",
                        "parent",
                        "custitem_jj_color_stone_shape",
                        "custitem_jj_colorstoneweightincarats",
                        "custitem_jj_colorstonecolour",
                        "custitem_jj_colorstonesize",
                    ]
                });


                itemSearchObj.run().each(result => {
                    results.push({
                        id: result.getValue({ name: 'internalid' }),
                        name: result.getValue({ name: 'itemid' }),
                        class: result.getValue({ name: 'class' }),
                        diamondShape: result.getValue({ name: 'custitem_jj_stone_shape' }),
                        qualityGroup: result.getValue({ name: 'custitem_jj_stone_quality_group' }),
                        quality: result.getValue({ name: 'custitem_jj_stone_quality' }),
                        diamondColor: result.getValue({ name: 'custitem_jj_stone_color' }),
                        sizeGroup: result.getValue({ name: 'custitem_jj_stone_size_group' }),
                        size: result.getValue({ name: 'custitem_jj_stone_size' }),
                        sizeMM: result.getValue({ name: 'custitem_jj_stone_size_mm' }),
                        category: result.getValue({ name: 'custitem_jj_category' }),
                        categoryGroup: result.getValue({ name: 'custitem_jj_category_group' }),
                        subCategory: result.getValue({ name: 'custitem_jj_sub_category' }),
                        metalColor: result.getValue({ name: 'custitem_jj_metal_color' }),
                        metalColorName: result.getText({ name: 'custitem_jj_metal_color' }),
                        metalQuality: result.getValue({ name: 'custitem_jj_metal_quality' }),
                        metalPurityPercent: result.getValue({ name: 'custitem_jj_metal_purity_percent' }),
                        parentId: result.getValue({ name: 'parent' }),
                        stoneShape: result.getValue("custitem_jj_color_stone_shape"),
                        stoneWeight: result.getValue("custitem_jj_colorstoneweightincarats"),
                        stoneColor: result.getValue("custitem_jj_colorstonecolour"),
                        stoneSize: result.getValue("custitem_jj_colorstonesize")
                    });
                    return true;
                });


                return results;


            } catch (error) {
                log.error('Error in getComponetItemDetails', error);
                return [];
            }
        }


        /**
         * Searches for valid replacement items matching provided filters based on gold, diamond, and color stone logic.
         *
         * @function
         * @param {Array<Object>} components - Source components to generate dynamic search filters
         * @param {string} metalColorId - Selected metal color ID
         * @param {string} diamondColorId - Selected diamond color ID
         * @param {string} colorStoneColorId - Selected color stone color ID
         * @param {Array<string|number>} itemIds - Original item IDs to avoid reusing
         * @param {string} metalQualityId - Selected metal quality ID
         * @returns {Array<Object>} List of replacement item objects satisfying match conditions
         */
        function findAllReplacementItems(components, metalColorId, diamondColorId, colorStoneColorId, metalQualityId) {
            try {
                const filters = [["isinactive", "is", "F"]];
                const seenGroups = new Set();
                const orGroups = [];


                components.forEach(comp => {
                    const isGold = GOLD_CLASS_IDS.includes(Number(comp.class));
                    const isDiamond = DIAMOND_CLASSES_IDS.includes(Number(comp.class));
                    const isColorStone = CS_CLASSES_IDS.includes(Number(comp.class));
                    const group = [];


                    if (isGold && metalColorId && metalQualityId) {
                        group.push(["custitem_jj_metal_color", "anyof", metalColorId]);
                        group.push(["custitem_jj_metal_quality", "anyof", metalQualityId]);


                        group.push(comp.parentId ? ["parent.internalid", "anyof", comp.parentId] : ["parent.internalid", "anyof", "@NONE@"]);
                        group.push(comp.metalPurityPercent ? ["custitem_jj_metal_purity_percent", "is", comp.metalPurityPercent] : ["custitem_jj_metal_purity_percent", "isempty", ""]);
                        group.push(comp.category ? ["custitem_jj_category", "anyof", comp.category] : ["custitem_jj_category", "anyof", "@NONE@"]);
                        group.push(comp.categoryGroup ? ["custitem_jj_category_group", "anyof", comp.categoryGroup] : ["custitem_jj_category_group", "anyof", "@NONE@"]);
                        group.push(comp.subCategory ? ["custitem_jj_sub_category", "anyof", comp.subCategory] : ["custitem_jj_sub_category", "anyof", "@NONE@"]);
                    }


                    if (isDiamond && diamondColorId) {
                        group.push(["custitem_jj_stone_color", "anyof", diamondColorId]);


                        group.push(comp.diamondShape ? ["custitem_jj_stone_shape", "anyof", comp.diamondShape] : ["custitem_jj_stone_shape", "anyof", "@NONE@"]);
                        group.push(comp.qualityGroup ? ["custitem_jj_stone_quality_group", "anyof", comp.qualityGroup] : ["custitem_jj_stone_quality_group", "anyof", "@NONE@"]);
                        group.push(comp.quality ? ["custitem_jj_stone_quality", "anyof", comp.quality] : ["custitem_jj_stone_quality", "anyof", "@NONE@"]);
                        group.push(comp.sizeGroup ? ["custitem_jj_stone_size_group", "anyof", comp.sizeGroup] : ["custitem_jj_stone_size_group", "anyof", "@NONE@"]);
                        group.push(comp.size ? ["custitem_jj_stone_size", "anyof", comp.size] : ["custitem_jj_stone_size", "anyof", "@NONE@"]);
                        group.push(comp.sizeMM ? ["custitem_jj_stone_size_mm", "is", comp.sizeMM] : ["custitem_jj_stone_size_mm", "isempty", ""]);
                    }


                    if (isColorStone && colorStoneColorId) {
                        group.push(["custitem_jj_colorstonecolour", "anyof", colorStoneColorId]);


                        group.push(comp.qualityGroup ? ["custitem_jj_stone_quality_group", "anyof", comp.qualityGroup] : ["custitem_jj_stone_quality_group", "anyof", "@NONE@"]);
                        group.push(comp.stoneShape ? ["custitem_jj_color_stone_shape", "anyof", comp.stoneShape] : ["custitem_jj_color_stone_shape", "anyof", "@NONE@"]);
                        group.push(comp.stoneWeight ? ["custitem_jj_colorstoneweightincarats", "is", comp.stoneWeight] : ["custitem_jj_colorstoneweightincarats", "isempty", ""]);
                        group.push(comp.stoneSize ? ["custitem_jj_colorstonesize", "anyof", comp.stoneSize] : ["custitem_jj_colorstonesize", "anyof", "@NONE@"]);
                    }


                    // De-duplication logic
                    if (group.length > 0) {
                        // Remove duplicates by creating a key from sorted conditions
                        const normalizedKey = group.map(JSON.stringify).sort().join('|');


                        if (!seenGroups.has(normalizedKey)) {
                            seenGroups.add(normalizedKey);


                            // Rebuild group with 'AND' in between
                            const andGroup = [];
                            group.forEach((condition, index) => {
                                if (index > 0) andGroup.push("AND");
                                andGroup.push(condition);
                            });


                            orGroups.push(andGroup);
                        }
                    }
                });


                // Append OR groups to main filter
                if (orGroups.length) {
                    filters.push("AND");
                    const orFilters = [];
                    orGroups.forEach((group, idx) => {
                        if (idx > 0) orFilters.push("OR");
                        orFilters.push(group);
                    });


                    filters.push(orFilters);
                }


                const columns = [
                    search.createColumn({ name: "internalid", sort: search.Sort.ASC }),
                    "itemid",
                    "class",
                    "custitem_jj_stone_shape",
                    "custitem_jj_stone_quality_group",
                    "custitem_jj_stone_quality",
                    "custitem_jj_stone_color",
                    "custitem_jj_stone_size_group",
                    "custitem_jj_stone_size",
                    "custitem_jj_stone_size_mm",
                    "custitem_jj_category",
                    "custitem_jj_category_group",
                    "custitem_jj_sub_category",
                    "custitem_jj_metal_color",
                    "custitem_jj_metal_quality",
                    "custitem_jj_metal_purity_percent",
                    "parent",
                    "custitem_jj_color_stone_shape",
                    "custitem_jj_colorstoneweightincarats",
                    "custitem_jj_colorstonecolour",
                    "custitem_jj_colorstonesize"
                ];


                const searchObj = search.create({
                    type: "item",
                    filters: filters,
                    columns: columns
                });


                const results = [];
                searchObj.run().each(result => {
                    results.push({
                        id: result.getValue("internalid"),
                        name: result.getValue("itemid"),
                        class: result.getValue("class"),
                        diamondShape: result.getValue("custitem_jj_stone_shape"),
                        qualityGroup: result.getValue("custitem_jj_stone_quality_group"),
                        quality: result.getValue("custitem_jj_stone_quality"),
                        diamondColor: result.getValue("custitem_jj_stone_color"),
                        sizeGroup: result.getValue("custitem_jj_stone_size_group"),
                        size: result.getValue("custitem_jj_stone_size"),
                        sizeMM: result.getValue("custitem_jj_stone_size_mm"),
                        category: result.getValue("custitem_jj_category"),
                        categoryGroup: result.getValue("custitem_jj_category_group"),
                        subCategory: result.getValue("custitem_jj_sub_category"),
                        metalColor: result.getValue("custitem_jj_metal_color"),
                        metalQuality: result.getValue("custitem_jj_metal_quality"),
                        metalPurityPercent: result.getValue("custitem_jj_metal_purity_percent"),
                        parentId: result.getValue("parent"),
                        stoneShape: result.getValue("custitem_jj_color_stone_shape"),
                        stoneWeight: result.getValue("custitem_jj_colorstoneweightincarats"),
                        stoneColor: result.getValue("custitem_jj_colorstonecolour"),
                        stoneSize: result.getValue("custitem_jj_colorstonesize"),
                    });
                    return true;
                });


                return results;
            } catch (e) {
                log.error("Error in findAllReplacementItems", e);
                return [];
            }
        }


        /**
         * Retrieves BOMs for a specific Assembly Item.
         *
         * @param {string} assemblyId
         * @returns {Array<{id: string, name: string}>}
         */
        function getBOMsForAssembly(assemblyId) {
            try {
                const boms = [];
                const bomSearch = search.create({
                    type: 'bom',
                    filters: [["isinactive", "is", "F"], "AND", ['restricttoassemblies', 'anyof', assemblyId]],
                    columns: ['internalid', 'name']
                });


                bomSearch.run().each(result => {
                    boms.push({
                        id: result.getValue('internalid'),
                        name: result.getValue('name')
                    });
                    return true;
                });


                return boms;
            } catch (error) {
                log.error('Error in getBOMsForAssembly', `Assembly ID: ${assemblyId} | ${error.message || error}`);
                return [];
            }
        }


        /**
         * Retrieves BOM Revisions for a given BOM.
         *
         * @param {string} bomId
         * @returns {Array<{id: string, name: string}>}
         */
        function getRevisionsForBOM(bomId) {
            try {
                const revs = [];


                const revSearch = search.create({
                    type: 'bomrevision',
                    filters: [["isinactive", "is", "F"], "AND", ['billofmaterials', 'anyof', bomId]],
                    columns: ['internalid', 'name']
                });


                revSearch.run().each(result => {
                    revs.push({
                        id: result.getValue('internalid'),
                        name: result.getValue('name')
                    });
                    return true;
                });


                return revs;
            } catch (error) {
                log.error('Error in getRevisionsForBOM', `BOM ID: ${bomId} | ${error.message || error}`);
                return [];
            }
        }


        /**
         * Extracts BOM, Revision, Assembly, and Build Components based on Serial number.
         *
         * @param {string} serialNumber - Entered serial number
         * @returns {Object} - BOM data
         */
        function getSerialDetails(serialNumber) {
            try {
                // Lookup the Build record by Serial Number
                const fgSerialComponentsSearchObj = search.create({
                    type: "customrecord_jj_fg_serial_components",
                    filters: [
                        ["isinactive", "is", "F"],
                        "AND", ["custrecord_jj_fsc_serial_number.custrecord_jj_fgs_serial", "anyof", serialNumber],
                        "AND", ["custrecord_jj_fsc_serial_number.custrecord_jj_fgs_assembly_build", "noneof", "@NONE@"]
                    ],
                    columns: [
                        search.createColumn({ name: "custrecord_jj_fgs_assembly_build", join: "CUSTRECORD_JJ_FSC_SERIAL_NUMBER", label: "Assembly Build" }),
                        search.createColumn({ name: "custrecord_jj_fgs_assembly_item", join: "CUSTRECORD_JJ_FSC_SERIAL_NUMBER", label: "Assembly Item" }),
                        search.createColumn({ name: "custrecord_jj_fgs_bom_revision", join: "CUSTRECORD_JJ_FSC_SERIAL_NUMBER", label: "BOM Revision" }),
                        search.createColumn({ name: "custrecord_jj_fgs_clr_stone_weight", join: "CUSTRECORD_JJ_FSC_SERIAL_NUMBER", label: "Color Stone Weight (CSWT)" }),
                        search.createColumn({ name: "custrecord_jj_fgs_diamond_weight", join: "CUSTRECORD_JJ_FSC_SERIAL_NUMBER", label: "Diamond Weight (DWT)" }),
                        search.createColumn({ name: "custrecord_jj_fgs_gold_weight", join: "CUSTRECORD_JJ_FSC_SERIAL_NUMBER", label: "Gold Weight (NWT)" }),
                        search.createColumn({ name: "custrecord_jj_fgs_gross_weight", join: "CUSTRECORD_JJ_FSC_SERIAL_NUMBER", label: "Gross Weight (GWT)" }),
                        search.createColumn({ name: "custrecord_jj_fgs_pure_weight", join: "CUSTRECORD_JJ_FSC_SERIAL_NUMBER", label: "Pure Weight (PWT)" }),
                        search.createColumn({ name: "custrecord_jj_fsc_item", label: "Item" }),
                        search.createColumn({ name: "custrecord_jj_fsc_quantity", label: "Quantity" }),
                        search.createColumn({ name: "custrecord_jj_fsc_pieces_value", label: "Pieces" }),
                        search.createColumn({ name: "custrecord_jj_fsc_item_units", label: "Units" }),
                        search.createColumn({ name: "class", join: "CUSTRECORD_JJ_FSC_ITEM", label: "Class" })
                    ]
                });


                let fgSerialComponentsResult = null;
                const modifiedComponents = [];
                let revisionMetadata = null;


                fgSerialComponentsSearchObj.run().each(result => {
                    if (!fgSerialComponentsResult) {
                        fgSerialComponentsResult = {
                            buildId: result.getValue({ name: "custrecord_jj_fgs_assembly_build", join: "CUSTRECORD_JJ_FSC_SERIAL_NUMBER" }),
                            assemblyId: result.getValue({ name: "custrecord_jj_fgs_assembly_item", join: "CUSTRECORD_JJ_FSC_SERIAL_NUMBER" }),
                            revisionId: result.getValue({ name: "custrecord_jj_fgs_bom_revision", join: "CUSTRECORD_JJ_FSC_SERIAL_NUMBER" }),
                            revisionName: result.getText({ name: "custrecord_jj_fgs_bom_revision", join: "CUSTRECORD_JJ_FSC_SERIAL_NUMBER" })
                        };


                        revisionMetadata = {
                            csWeight: parseFloat(result.getValue({ name: 'custrecord_jj_fgs_clr_stone_weight', join: 'CUSTRECORD_JJ_FSC_SERIAL_NUMBER' }) || 0),
                            diamondWeight: parseFloat(result.getValue({ name: 'custrecord_jj_fgs_diamond_weight', join: 'CUSTRECORD_JJ_FSC_SERIAL_NUMBER' }) || 0),
                            netWeight: parseFloat(result.getValue({ name: 'custrecord_jj_fgs_gold_weight', join: 'CUSTRECORD_JJ_FSC_SERIAL_NUMBER' }) || 0),
                            grossWeight: parseFloat(result.getValue({ name: 'custrecord_jj_fgs_gross_weight', join: 'CUSTRECORD_JJ_FSC_SERIAL_NUMBER' }) || 0),
                            pureWeight: parseFloat(result.getValue({ name: 'custrecord_jj_fgs_pure_weight', join: 'CUSTRECORD_JJ_FSC_SERIAL_NUMBER' }) || 0)
                        };
                    }


                    // Push all the component details
                    modifiedComponents.push({
                        itemId: result.getValue({ name: 'custrecord_jj_fsc_item' }),
                        itemName: result.getText({ name: 'custrecord_jj_fsc_item' }),
                        quantity: parseFloat(result.getValue({ name: 'custrecord_jj_fsc_quantity' }) || 0),
                        unit: result.getText({ name: 'custrecord_jj_fsc_item_units' }) || '',
                        pieces: parseInt(result.getValue({ name: 'custrecord_jj_fsc_pieces_value' }) || 0),
                        class: result.getText({ name: 'class', join: 'CUSTRECORD_JJ_FSC_ITEM' }) || ''
                    });


                    return true;
                });


                if (!fgSerialComponentsResult) throw new Error(`No Build found for Serial: ${serialNumber}`);


                // Load BOM Revision Components (original)
                const originalComponents = getBOMRevisionComponents(fgSerialComponentsResult.revisionId).components;


                return {
                    assemblyItemId: fgSerialComponentsResult.assemblyId,
                    bomRevisionId: fgSerialComponentsResult.revisionId,
                    bomRevisionName: fgSerialComponentsResult.revisionName,
                    originalComponents,
                    modifiedComponents,
                    revisionMetadata
                };


            } catch (err) {
                log.error('getBuildDetailsFromSerial', err);
                return {};
            }
        }


        return { onRequest };


    });

jj_cs_bom_clone_spec
/**
 * @NApiVersion 2.1
 * @NScriptType ClientScript
 * @NModuleScope SameAccount
 * 
 * ******************************************************************************
 * DEWIN-2022: BOM Duplication and Component Substitution Based on Material Specs
 * ******************************************************************************
 * 
 * Author: Jobin & Jismi IT Services LLP
 * 
 * Script Description: This client script is used to redirect to the suitelet based on the field updates and show message.
 * 
 * Date created : 05 - June - 2025
 * 
 * REVISION HISTORY
 * @version 1.0 DEWIN-2022 : 05 - June - 2025 : Initial build by JJ0312
 * 
 * *****************************************************************************/
define(['N/currentRecord', 'N/url', 'N/https', 'N/runtime', 'N/ui/dialog'],
    /**
     * @param{currentRecord} currentRecord
     * @param{url} url
     * @param{https} https
     * @param{runtime} runtime
     * @param{dialog} dialog
     */
    function (currentRecord, url, https, runtime, dialog) {
        const SCRIPT_ID = 'customscript_jj_sl_bom_clone_spec';
        const DEPLOYMENT_ID = 'customdeploy_jj_sl_bom_clone_spec';


        /**
         * Function to be executed after the page is initialized.
         *
         * @param {Object} scriptContext - Context object containing information about the page initialization event.
         * @param {Record} scriptContext.currentRecord - Current form record.
         *
         * @since 2015.2
         */
        function pageInit(scriptContext) {
            try {
                let rec = currentRecord.get();
                let message = rec.getValue({ fieldId: 'custpage_message_text' });
                if (message) {
                    dialog.alert({
                        title: 'Notification',
                        message: message
                    }).then(function () {
                        redirectToSuitelet(null);
                    }).catch(function (reason) {
                        redirectToSuitelet(null);
                    });
                }
            } catch (e) {
                console.error("Error @pageInit", e);
                log.error("Error @pageInit", e);
            }
        }


        /**
         * Function to be executed when field is changed.
         * Triggered on field change to dynamically reload Suitelet with dependent selections
         *
         * @param {Object} scriptContext
         * @param {Record} scriptContext.currentRecord - Current form record
         * @param {string} scriptContext.sublistId - Sublist name
         * @param {string} scriptContext.fieldId - Field name
         * @param {number} scriptContext.lineNum - Line number. Will be undefined if not a sublist or matrix field
         * @param {number} scriptContext.columnNum - Line number. Will be undefined if not a matrix field
         *
         * @since 2015.2
         */
        function fieldChanged(scriptContext) {
            try {
                const recordObj = currentRecord.get();
                const fieldId = scriptContext.fieldId;


                // Define fields that trigger reload
                const reloadTriggerFields = [
                    'custpage_assembly',
                    'custpage_bom',
                    'custpage_bomrev',
                    'custpage_metal',
                    'custpage_diamond_color',
                    'custpage_cs_color',
                    'custpage_serial_number'
                ];


                if (reloadTriggerFields.includes(fieldId)) {
                    redirectToSuitelet(recordObj, fieldId);
                }
            } catch (e) {
                console.error('Field Changed Error', e);
                log.error("Error @fieldChanged", e);
            }
        }


        /**
         * Redirects the user to the Suitelet with the provided record parameters.
         *
         * @param {Object} recordObj - The current record object to extract field values from.
         */
        function redirectToSuitelet(recordObj, fieldId) {
            try {
                let suiteletUrl;
                let assembly, bom, bomrev, metal, diamond, colorStone, metalName, effectiveDate, diamondColorName, csColorName, assemblyItemName, bomName, bomNameText, serialId;
                let params = {}; // will hold non-empty params
                if (recordObj) {
                    assembly = recordObj.getValue({ fieldId: 'custpage_assembly' });
                    assemblyItemName = recordObj.getText({ fieldId: 'custpage_assembly' });
                    bom = fieldId != 'custpage_assembly' ? recordObj.getValue({ fieldId: 'custpage_bom' }) : "";
                    bomName = fieldId != 'custpage_assembly' ? recordObj.getText({ fieldId: 'custpage_bom' }) : "";
                    bomrev = fieldId != 'custpage_bom' && fieldId != 'custpage_assembly' ? recordObj.getValue({ fieldId: 'custpage_bomrev' }) : "";
                    metal = recordObj.getValue({ fieldId: 'custpage_metal' });
                    metalName = recordObj.getText({ fieldId: 'custpage_metal' });
                    diamond = recordObj.getValue({ fieldId: 'custpage_diamond_color' });
                    diamondColorName = recordObj.getText({ fieldId: 'custpage_diamond_color' });
                    colorStone = recordObj.getValue({ fieldId: 'custpage_cs_color' });
                    csColorName = recordObj.getText({ fieldId: 'custpage_cs_color' });
                    effectiveDate = recordObj.getValue({ fieldId: 'custpage_effective_start_date' });
                    serialId = recordObj.getValue({ fieldId: 'custpage_serial_number' });
                    bomNameText = recordObj.getValue({ fieldId: 'custpage_bom_name' });


                    // Disable "leave site" warning
                    window.onbeforeunload = null;
                }


                if (assembly) params.custpage_assembly = assembly;
                if (assemblyItemName) params.custpage_assembly_name = assemblyItemName;
                if (bom) params.custpage_bom = bom;
                if (bomName) !serialId ? params.custpage_bom_name = bomName : params.custpage_bom_name = bomNameText;
                if (bomrev) params.custpage_bomrev = bomrev;
                if (metal) params.custpage_metal = metal;
                if (metalName) params.custpage_metal_name = metalName;
                if (diamond) params.custpage_diamond_color = diamond;
                if (diamondColorName) params.custpage_diamond_color_name = diamondColorName;
                if (colorStone) params.custpage_cs_color = colorStone;
                if (csColorName) params.custpage_cs_color_name = csColorName;
                if (effectiveDate) params.custpage_effective_start_date = effectiveDate;
                if (serialId) params.custpage_serial_number = serialId;


                // Build the Suitelet URL with parameters
                suiteletUrl = url.resolveScript({ scriptId: SCRIPT_ID, deploymentId: DEPLOYMENT_ID, params: params });


                // Redirect to Suitelet URL
                window.location.href = suiteletUrl;
            } catch (e) {
                console.error('Field redirectToSuitelet Error', e);
                log.error("Error @redirectToSuitelet", e);
            }
        }


        return {
            pageInit: pageInit,
            fieldChanged: fieldChanged
        }
    });


Leave a comment

Your email address will not be published. Required fields are marked *