Cost of Goods Sold (COGS) Calculation with Stateset API

This guide provides comprehensive instructions for calculating the Cost of Goods Sold (COGS) using the Stateset API. Designed for manufacturers and businesses managing inventory, this document details how to leverage Stateset to track inventory value, production costs, and ultimately determine COGS for improved profitability analysis and cost control. By following this guide, you will learn to integrate Stateset for managing inventory lifecycles, tracking costs associated with production, implementing standard COGS calculation methodologies, and recording COGS entries directly via the API. Note: This guide emphasizes a perpetual inventory system (real-time tracking of inventory and costs), which Stateset supports through inventory movements. For periodic systems, adjust reporting accordingly.

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Getting Started: SDK Setup
  4. Core Concepts: COGS, COGM, and Inventory Flow
  5. API Workflow: Tracking Costs for COGS Calculation
  6. Costing Methodologies
  7. Advanced COGS Considerations
  8. Error Handling Best Practices
  9. Troubleshooting Common Issues
  10. Support Resources
  11. Conclusion

Introduction

Cost of Goods Sold (COGS) represents the direct costs incurred in producing goods sold by a company. Accurate COGS calculation is fundamental for assessing gross profit, managing operational expenses, and making strategic business decisions. This guide demonstrates how the Stateset API facilitates precise COGS tracking by managing purchase orders, inventory movements, work orders, and associated costs. Key Distinction: COGM vs. COGS
  • Cost of Goods Manufactured (COGM): The total cost of producing finished goods during a period, including direct materials, direct labor, and manufacturing overhead (MOH), adjusted for changes in Work-in-Progress (WIP) inventory.
  • Cost of Goods Sold (COGS): The cost of finished goods that were actually sold during the period. In a manufacturing context, COGS = Beginning Finished Goods Inventory + COGM - Ending Finished Goods Inventory.
Stateset helps track these by recording costed inventory movements throughout the lifecycle. What You’ll Achieve:
  • Configure the Stateset Node.js SDK for API interaction.
  • Understand the relationship between COGS, COGM, inventory valuation, and production processes.
  • Utilize the Stateset API to record costs from purchasing through production and sales.
  • Implement logic to calculate COGM and COGS using tracked data.
  • Record COGS entries directly via the API.
  • Generate periodic COGS reports for analysis.
  • Apply best practices for error handling and data integrity.

Prerequisites

  • Node.js (version 16 or higher recommended).
  • An active Stateset account and API Key.
  • Basic understanding of Javascript (async/await) and REST APIs.
  • Familiarity with manufacturing concepts (Purchase Orders, Work Orders, BOMs, Inventory).
  • Understanding of accounting principles (e.g., GAAP/IFRS) for COGS methods.

Getting Started: SDK Setup

Begin by integrating the Stateset SDK into your application environment.

1. Install the Stateset Node.js SDK

Use npm or yarn to add the SDK package to your project.
npm install stateset-node
# or
# yarn add stateset-node

2. Configure Environment Variables

Securely store your Stateset API key using environment variables. Avoid hardcoding keys directly in your source code.
# Example for bash/zsh shells. Adapt for your environment (e.g., .env file).
export STATESET_API_KEY='your_api_key_here'

3. Initialize the Stateset Client

Instantiate the client in your application using your API key.
import { stateset } from 'stateset-node';

// Ensure the environment variable is loaded (e.g., using dotenv)
if (!process.env.STATESET_API_KEY) {
  throw new Error("STATESET_API_KEY environment variable is not set.");
}

// Initialize the client
const client = new stateset(process.env.STATESET_API_KEY);

// Optional: Verify API connection
async function verifyConnection() {
  try {
    const health = await client.system.healthCheck();
    console.log('Stateset API Connection Status:', health.status);
    if (health.status !== 'OK') {
        console.warn('API health check returned non-OK status:', health);
    }
  } catch (error) {
    console.error('Failed to connect to Stateset API:', error.message);
    // Depending on application needs, you might want to exit or retry
  }
}

verifyConnection();

Core Concepts: COGS, COGM, and Inventory Flow

Understanding these core concepts is essential for implementing COGS calculations effectively. What is COGS? COGS includes all direct costs associated with producing goods that a company sells. For manufacturers, this encompasses raw materials, direct labor, and manufacturing overhead directly tied to production. It excludes indirect costs like sales, marketing, or general administrative expenses. What is COGM? COGM calculates the cost of goods completed and transferred to finished goods inventory: Direct Materials Used + Direct Labor + MOH + Beginning WIP - Ending WIP. COGS Formula (Periodic System): COGS = Beginning Finished Goods Inventory Value + COGM - Ending Finished Goods Inventory Value. In a perpetual system (recommended with Stateset), COGS is the sum of costs assigned to inventory outflows (sales/shipments) during the period. The Role of Inventory: Inventory is central to COGS. The flow of costs through inventory directly impacts the final COGS value. Stateset helps track inventory quantity and value as it moves through different stages:
  • Purchase Order (PO): Captures the initial cost of raw materials, increasing raw material inventory value.
  • Work Order (WO) / Bill of Materials (BOM): Defines the components (raw materials, sub-assemblies) and potentially labor/overhead needed. Consuming components transfers their cost from Raw Materials to Work-in-Progress (WIP).
  • Work-in-Progress (WIP): Represents the value of partially completed goods. Track labor and overhead here if possible (e.g., via custom fields on WOs).
  • Finished Goods: Upon WO completion, the total cost accumulated in WIP transfers to Finished Goods inventory (this is COGM).
  • Sale: When a finished good is sold/shipped, its cost is removed from inventory and recognized as COGS on the income statement.
Costing Methods: The method used to value inventory withdrawn affects COGS:
  • Average Cost: Uses a simple average cost of all identical items in inventory.
  • Weighted Average Cost: Recalculates average after each purchase, weighting by quantity.
  • FIFO (First-In, First-Out): Assumes oldest items sold first.
  • LIFO (Last-In, First-Out): Assumes newest items sold first (permitted under US GAAP but not IFRS; check local regulations as of 2025).
Stateset provides the data foundation (tracking individual inventory receipts/movements with costs) to implement these methods. When creating negative inventory movements (consumptions/sales), assign costs based on your method and record them. Perpetual vs. Periodic Inventory:
  • Perpetual: Updates inventory and COGS in real-time with each transaction (Stateset supports this via movements).
  • Periodic: Calculates COGS at period-end using the formula above (use for reporting).

API Workflow: Tracking Costs for COGS Calculation

This section details using the Stateset API to track costs throughout the inventory lifecycle, enabling accurate COGS calculation.

Step 1: Recording Purchase Costs

Capture the cost of incoming raw materials via Purchase Orders and Inventory updates.

A. Create a Purchase Order

Record the details of a purchase, including items, quantities, and unit costs.
/**
 * Creates a Purchase Order in Stateset.
 * @param {object} poData - Data for the new Purchase Order.
 * @returns {Promise<object>} The created Purchase Order object.
 */
async function createPurchaseOrder(poData) {
    try {
        console.log('Creating Purchase Order with data:', poData);
        const newPO = await client.purchaseorder.create({
            number: poData.number,
            supplier: poData.supplier,
            order_date: poData.orderDate, // Expects ISO 8601 format string
            expected_delivery_date: poData.expectedDeliveryDate, // Expects ISO 8601 format string
            status: poData.status, // e.g., 'OPEN', 'SUBMITTED'
            total_amount: poData.totalAmount, // Ensure this matches line item totals
            currency: poData.currency, // e.g., 'USD'
            line_items: poData.lineItems // Array of { part_number, quantity, unit_cost }
        });
        console.log("Purchase Order created successfully:", newPO.id);
        return newPO;
    } catch (error) {
        console.error("Error creating purchase order:", error.response ? error.response.data : error.message);
        throw error; // Re-throw for upstream handling
    }
}

// Example Usage:
const poDetails = {
    number: `PO-${Date.now()}`, // Generate a unique PO number
    supplier: 'Supplier Inc.',
    orderDate: new Date().toISOString(),
    expectedDeliveryDate: new Date(Date.now() + 7 * 86400000).toISOString(), // 7 days from now
    status: 'OPEN',
    totalAmount: 150.00,
    currency: 'USD',
    lineItems: [
        { part_number: 'RAW_MAT_A', quantity: 10, unit_cost: 5.00 },
        { part_number: 'RAW_MAT_B', quantity: 5, unit_cost: 20.00 }
    ]
};
// const purchaseOrder = await createPurchaseOrder(poDetails);
Purpose: This API call logs the purchase intent and expected costs. The unit_cost on line items is crucial for later inventory valuation.

B. Receive Items and Update Inventory

When goods arrive, update the PO status and record the items received into inventory, capturing their actual cost.
/**
 * Records received items from a PO line into inventory.
 * Assumes each call represents a distinct receipt/movement.
 * @param {string} partNumber - The part number received.
 * @param {number} quantity - The quantity received.
 * @param {number} unitCost - The actual cost per unit from the PO/invoice.
 * @param {string} purchaseOrderId - Optional: Link to the source PO.
 * @returns {Promise<object>} The created Inventory movement record.
 */
async function recordInventoryReceipt(partNumber, quantity, unitCost, purchaseOrderId = null) {
    try {
        console.log(`Recording inventory receipt for ${partNumber}: Qty=${quantity}, Cost=${unitCost}`);
        const inventoryData = {
            part_number: partNumber,
            quantity: quantity, // Positive for receipts
            unit_cost: unitCost,
            source_document_id: purchaseOrderId,
            source_document_type: 'purchase_order',
        };
        const newInventoryEntry = await client.inventory.create(inventoryData);
        console.log(`Inventory updated for part ${partNumber}. Entry ID:`, newInventoryEntry.id);
        return newInventoryEntry;
    } catch (error) {
        console.error(`Error updating inventory for part ${partNumber}:`, error.response ? error.response.data : error.message);
        throw error;
    }
}

/**
 * Marks a Purchase Order as partially or fully received.
 * Updates inventory based on received line items.
 * @param {string} poId - The ID of the Purchase Order to receive against.
 * @param {Array<object>} receivedItems - Array of { part_number, quantity, unit_cost } received.
 * @param {string} finalStatus - The new status for the PO ('PARTIALLY_RECEIVED', 'RECEIVED').
 * @returns {Promise<object>} The updated Purchase Order object.
 */
async function receivePurchaseOrderItems(poId, receivedItems, finalStatus) {
    try {
        console.log(`Receiving items for PO ID: ${poId}`);
        // 1. Record each received item as an inventory movement
        for (const item of receivedItems) {
            await recordInventoryReceipt(item.part_number, item.quantity, item.unit_cost, poId);
        }

        // 2. Update the Purchase Order status
        const updatedPO = await client.purchaseorder.update(poId, {
            status: finalStatus,
            received_date: new Date().toISOString() // Record receipt date
        });
        console.log(`Purchase Order ${poId} status updated to ${finalStatus}`);
        return updatedPO;
    } catch (error) {
        console.error(`Error processing receipt for PO ${poId}:`, error.response ? error.response.data : error.message);
        throw error;
    }
}

// Example Usage (assuming 'purchaseOrder' object from previous step):
const itemsReceived = [
    { part_number: 'RAW_MAT_A', quantity: 10, unit_cost: 5.00 },
    { part_number: 'RAW_MAT_B', quantity: 5, unit_cost: 20.00 }
];
const receivedPO = await receivePurchaseOrderItems(purchaseOrder.id, itemsReceived, 'RECEIVED');

Purpose: This workflow creates distinct inventory records (movements) for each receipt. Each record stores the quantity and unit_cost at that point in time. This historical cost layering is the foundation for accurate COGS methods like FIFO or Weighted Average. Updating the PO status closes the loop on the purchasing process.

Step 2: Tracking Production Costs (Work Orders & BOMs)

Track the consumption of components, addition of labor/overhead, and creation of finished goods using Work Orders.

A. Retrieve Work Order and BOM Details

Fetch completed Work Orders within a period and their associated Bills of Materials (BOMs) to understand component usage.
/**
 * Fetches completed Work Orders within a given date range.
 * @param {string} startDateISO - ISO 8601 start date string.
 * @param {string} endDateISO - ISO 8601 end date string.
 * @returns {Promise<Array<object>>} List of completed Work Order objects.
 */
async function getCompletedWorkOrders(startDateISO, endDateISO) {
    try {
        console.log(`Fetching completed Work Orders from ${startDateISO} to ${endDateISO}`);
        const workOrders = await client.workorder.list({
            filter: {
                // Assuming status field exists and 'COMPLETED' is a valid value
                status: { eq: 'COMPLETED' },
                // Assuming completion_date field exists for filtering
                completion_date: { gte: startDateISO, lte: endDateISO }
            },
            limit: 1000 // Adjust limit as needed, handle pagination if necessary
        });
        console.log(`Found ${workOrders.length} completed Work Orders.`);
        return workOrders;
    } catch (error) {
        console.error("Error fetching work orders:", error.response ? error.response.data : error.message);
        throw error;
    }
}

/**
 * Retrieves the Bill of Materials (BOM) associated with a Work Order.
 * @param {object} workOrder - The Work Order object.
 * @returns {Promise<object>} The BOM object.
 */
async function getBOMForWorkOrder(workOrder) {
    try {
        if (!workOrder.bill_of_material_id) {
            throw new Error(`Work Order ${workOrder.id} does not have a linked BOM ID.`);
        }
        console.log(`Fetching BOM ID: ${workOrder.bill_of_material_id} for Work Order ${workOrder.id}`);
        const bom = await client.billofmaterials.get(workOrder.bill_of_material_id);
        if (!bom) {
             throw new Error(`BOM with ID ${workOrder.bill_of_material_id} not found.`);
        }
        console.log(`Retrieved BOM: ${bom.name}`);
        return bom;
    } catch (error) {
        console.error(`Error getting BOM for Work Order ${workOrder.id}:`, error.response ? error.response.data : error.message);
        throw error;
    }
}

// Example Usage:
const startDate = new Date(Date.UTC(2023, 11, 1)).toISOString(); // Dec 1, 2023
const endDate = new Date(Date.UTC(2023, 11, 31, 23, 59, 59, 999)).toISOString(); // Dec 31, 2023
// const completedWorkOrders = await getCompletedWorkOrders(startDate, endDate);
if (completedWorkOrders.length > 0) {
    const firstWO = completedWorkOrders[0];
    const bom = await getBOMForWorkOrder(firstWO);
    console.log("BOM Components:", bom.components);
}
Purpose: Identify the production activities (WOs) and the required components (BOMs) for finished goods produced in a specific period.

B. Record Component Consumption and Add to WIP

When starting or progressing a Work Order, create negative inventory movements for consumed components, assigning costs based on your method. Add labor/overhead via custom fields or separate entries.
/**
 * Records consumption of a component for a Work Order using specified costing method.
 * @param {string} partNumber - The part number consumed.
 * @param {number} quantity - The quantity consumed (will be negative in movement).
 * @param {string} workOrderId - The linked Work Order ID.
 * @param {string} costingMethod - 'FIFO', 'Average', or 'Simplified'.
 * @returns {Promise<object>} The created inventory movement.
 */
async function recordInventoryConsumption(partNumber, quantity, workOrderId, costingMethod) {
    let unitCost;
    switch (costingMethod) {
        case 'FIFO':
            unitCost = await calculateFIFOCost(partNumber, quantity) / quantity; // Total cost / qty for unit
            break;
        case 'Average':
            unitCost = await calculateAverageCost(partNumber);
            break;
        case 'Simplified':
        default:
            unitCost = await getComponentCost_Simplified(partNumber);
            break;
    }

    try {
        const inventoryData = {
            part_number: partNumber,
            quantity: -quantity, // Negative for consumption
            unit_cost: unitCost,
            source_document_id: workOrderId,
            source_document_type: 'work_order',
        };
        const newEntry = await client.inventory.create(inventoryData);
        console.log(`Consumption recorded for ${partNumber}. Entry ID: ${newEntry.id}`);
        return newEntry;
    } catch (error) {
        console.error(`Error recording consumption for ${partNumber}:`, error);
        throw error;
    }
}

/**
 * Consumes components for a Work Order based on its BOM.
 * @param {object} workOrder - The Work Order object.
 * @param {string} costingMethod - The costing method to use.
 * @returns {Promise<void>}
 */
async function consumeComponentsForWO(workOrder, costingMethod) {
    const bom = await getBOMForWorkOrder(workOrder);
    for (const component of bom.components) {
        await recordInventoryConsumption(component.item_id, component.quantity, workOrder.id, costingMethod);
    }
    // Add logic to update WO status or add to WIP value
}
Purpose: This records cost transfers from raw materials to WIP, using the chosen costing method to assign unit_cost.

C. Record Production Completion and Transfer to Finished Goods

Upon WO completion, calculate total cost (materials + labor + overhead) and create positive inventory for finished goods.
/**
 * Records completion of a Work Order, transferring costs to Finished Goods.
 * Assumes labor and overhead are tracked on the WO (e.g., custom fields).
 * @param {object} workOrder - The Work Order object.
 * @param {number} totalMaterialCost - Calculated material cost.
 * @param {string} finishedPartNumber - The finished good's part number.
 * @returns {Promise<object>} The inventory movement for FG.
 */
async function recordWOCompletion(workOrder, totalMaterialCost, finishedPartNumber) {
    const laborCost = workOrder.labor_cost || 0; // Assume field exists or fetch
    const overheadCost = workOrder.overhead_cost || 0;
    const totalCost = totalMaterialCost + laborCost + overheadCost;
    const quantityProduced = workOrder.quantity_produced || 0;
    const unitCost = quantityProduced > 0 ? totalCost / quantityProduced : 0;

    try {
        const inventoryData = {
            part_number: finishedPartNumber,
            quantity: quantityProduced,
            unit_cost: unitCost,
            source_document_id: workOrder.id,
            source_document_type: 'work_order',
        };
        const newEntry = await client.inventory.create(inventoryData);
        console.log(`FG recorded for ${finishedPartNumber}. Entry ID: ${newEntry.id}`);
        return newEntry;
    } catch (error) {
        console.error(`Error recording WO completion:`, error);
        throw error;
    }
}
Purpose: This transfers accumulated costs to finished goods inventory, representing COGM.

D. Determine Cost of Components Consumed (Simplified Example)

Calculate the cost of components used in a Work Order. Note: This example uses a simplified approach (fetching the latest cost). For accurate costing (FIFO/Average), you’d need logic to query and consume specific inventory cost layers based on your chosen method. Furthermore, remember to incorporate direct labor and overhead costs associated with the Work Order for a complete COGS picture.
/**
 * Retrieves the cost of a specific component part.
 * @param {string} partNumber - The component's part number.
 * @returns {Promise<number>} The unit cost of the component.
 */
async function getComponentCost_Simplified(partNumber) {
    try {
        // Fetch the most recent inventory entry (receipt) for this part
        const inventoryMovements = await client.inventory.list({
            filter: { part_number: { eq: partNumber }, quantity: { gt: 0 } }, // Look for receipts
            sort: '-created_at', // Get the most recent first
            limit: 1
        });

        if (inventoryMovements.length === 0) {
            console.warn(`No recent inventory receipt found for part number: ${partNumber}. Cost assumed 0.`);
            // Or throw an error depending on business rules
             return 0;
           // throw new Error(`No inventory cost found for part number: ${partNumber}`);
        }
        // Using optional chaining for safety
        const latestCost = inventoryMovements[0]?.unit_cost;
        if (latestCost === undefined || latestCost === null) {
             console.warn(`Latest inventory entry for ${partNumber} has no unit_cost. Cost assumed 0.`);
             return 0;
        }
        console.log(`Latest cost for ${partNumber}: ${latestCost}`);
        return latestCost;
    } catch (error) {
        console.error(`Error getting component cost for part ${partNumber}:`, error.response ? error.response.data : error.message);
        // Don't re-throw here if a default/warning is acceptable
        return 0; // Return 0 cost if lookup fails critically, or handle differently
    }
}

/**
 * Calculates the estimated COGS for a single Work Order based on its BOM components.
 * Uses the SIMPLIFIED component costing method (latest cost).
 * @param {object} workOrder - The completed Work Order object.
 * @returns {Promise<object>} Contains work order number, total cost, and quantity produced.
 */
async function calculateCOGSForWorkOrder_Simplified(workOrder) {
    try {
        const bom = await getBOMForWorkOrder(workOrder);
        let totalMaterialCost = 0;

        if (!bom.components || bom.components.length === 0) {
            console.warn(`BOM for Work Order ${workOrder.id} has no components.`);
            // Handle cases with no components (e.g., service WOs) if applicable
        } else {
             for (const component of bom.components) {
                 // Assumes component object has item_id (part number) and quantity fields
                 if (!component.item_id || component.quantity === undefined) {
                      console.warn(`Skipping invalid component in BOM ${bom.id}:`, component);
                      continue;
                 }
                 const componentCost = await getComponentCost_Simplified(component.item_id);
                 totalMaterialCost += componentCost * component.quantity;
             }
        }

        // Extract quantity produced from the work order (adapt field name as needed)
        // Example: Assuming quantity is on the WO itself or summed from line items
        const quantityProduced = workOrder.quantity_produced || workOrder.work_order_line_items?.reduce((sum, item) => sum + item.quantity, 0) || 0;

        if (quantityProduced === 0) {
             console.warn(`Work Order ${workOrder.id} has zero quantity produced. Average cost cannot be calculated.`);
        }

        console.log(`Calculated Material Cost for WO ${workOrder.number}: ${totalMaterialCost}`);

        return {
            workOrderNumber: workOrder.number,
            totalCost: totalMaterialCost, // Renaming for clarity - this is MATERIAL cost
            quantityProduced: quantityProduced
        };
    } catch (error) {
        console.error(`Error calculating COGS for Work Order ${workOrder.id}:`, error.message);
        // Return a structure indicating failure or partial data
        return {
             workOrderNumber: workOrder?.number || 'Unknown',
             totalCost: null,
             quantityProduced: null,
             error: error.message
        };
    }
}

// Example Usage:
if (completedWorkOrders.length > 0) {
    const cogsEstimate = await calculateCOGSForWorkOrder_Simplified(completedWorkOrders[0]);
    console.log("Simplified COGS Estimate for Work Order:", cogsEstimate);
}

Purpose: This step estimates the material cost of producing goods based on the BOM and component costs recorded in Stateset. Crucially, this simplified example uses the latest component cost. For accurate accounting, implement logic reflecting your chosen costing method (FIFO, Average) by querying specific inventory movement records. Furthermore, remember to incorporate direct labor and overhead costs associated with the Work Order for a complete COGS picture.

Variant: COGS Calculation Using FIFO

/**
 * Calculates the COGS for a single Work Order using FIFO for component costs.
 * @param {object} workOrder - The completed Work Order object.
 * @returns {Promise<object>} Contains work order number, total cost, and quantity produced.
 */
async function calculateCOGSForWorkOrder_FIFO(workOrder) {
    try {
        const bom = await getBOMForWorkOrder(workOrder);
        let totalMaterialCost = 0;

        if (!bom.components || bom.components.length === 0) {
            console.warn(`BOM for Work Order ${workOrder.id} has no components.`);
        } else {
            for (const component of bom.components) {
                if (!component.item_id || component.quantity === undefined) {
                    console.warn(`Skipping invalid component in BOM ${bom.id}:`, component);
                    continue;
                }
                const componentCost = await calculateFIFOCost(component.item_id, component.quantity);
                totalMaterialCost += componentCost;
            }
        }

        const quantityProduced = workOrder.quantity_produced || workOrder.work_order_line_items?.reduce((sum, item) => sum + item.quantity, 0) || 0;

        if (quantityProduced === 0) {
            console.warn(`Work Order ${workOrder.id} has zero quantity produced.`);
        }

        console.log(`Calculated Material Cost (FIFO) for WO ${workOrder.number}: ${totalMaterialCost}`);

        return {
            workOrderNumber: workOrder.number,
            totalCost: totalMaterialCost,
            quantityProduced: quantityProduced
        };
    } catch (error) {
        console.error(`Error calculating FIFO COGS for Work Order ${workOrder.id}:`, error.message);
        return {
            workOrderNumber: workOrder?.number || 'Unknown',
            totalCost: null,
            quantityProduced: null,
            error: error.message
        };
    }
}

Variant: COGS Calculation Using Average Cost

/**
 * Calculates the COGS for a single Work Order using Average Cost for components.
 * @param {object} workOrder - The completed Work Order object.
 * @returns {Promise<object>} Contains work order number, total cost, and quantity produced.
 */
async function calculateCOGSForWorkOrder_Average(workOrder) {
    try {
        const bom = await getBOMForWorkOrder(workOrder);
        let totalMaterialCost = 0;

        if (!bom.components || bom.components.length === 0) {
            console.warn(`BOM for Work Order ${workOrder.id} has no components.`);
        } else {
            for (const component of bom.components) {
                if (!component.item_id || component.quantity === undefined) {
                    console.warn(`Skipping invalid component in BOM ${bom.id}:`, component);
                    continue;
                }
                const unitCost = await calculateAverageCost(component.item_id);
                totalMaterialCost += unitCost * component.quantity;
            }
        }

        const quantityProduced = workOrder.quantity_produced || workOrder.work_order_line_items?.reduce((sum, item) => sum + item.quantity, 0) || 0;

        if (quantityProduced === 0) {
            console.warn(`Work Order ${workOrder.id} has zero quantity produced.`);
        }

        console.log(`Calculated Material Cost (Average) for WO ${workOrder.number}: ${totalMaterialCost}`);

        return {
            workOrderNumber: workOrder.number,
            totalCost: totalMaterialCost,
            quantityProduced: quantityProduced
        };
    } catch (error) {
        console.error(`Error calculating Average COGS for Work Order ${workOrder.id}:`, error.message);
        return {
            workOrderNumber: workOrder?.number || 'Unknown',
            totalCost: null,
            quantityProduced: null,
            error: error.message
        };
    }
}

Step 3: Calculating Cost of Goods Manufactured (COGM)

Aggregate costs from completed Work Orders to calculate COGM. Include adjustments for WIP changes.
/**
 * Calculates COGM for a period based on completed Work Orders and WIP changes.
 * @param {Array<object>} workOrders - List of completed work orders.
 * @param {string} costingMethod - 'Simplified', 'FIFO', or 'Average'.
 * @param {number} beginningWIP - Beginning WIP value.
 * @param {number} endingWIP - Ending WIP value.
 * @returns {Promise<number>} Total COGM.
 */
async function calculateCOGMForPeriod(workOrders, costingMethod, beginningWIP, endingWIP) {
    let totalDirectMaterials = 0;
    let totalLabor = 0;
    let totalOverhead = 0;

    for (const workOrder of workOrders) {
        const woCosts = await calculateCostsForWorkOrder(workOrder, costingMethod);
        totalDirectMaterials += woCosts.materials;
        totalLabor += woCosts.labor;
        totalOverhead += woCosts.overhead;
    }

    const cogm = totalDirectMaterials + totalLabor + totalOverhead + beginningWIP - endingWIP;
    console.log(`COGM Calculation: Materials=${totalDirectMaterials}, Labor=${totalLabor}, Overhead=${totalOverhead}, Beginning WIP=${beginningWIP}, Ending WIP=${endingWIP}`);
    console.log(`Total COGM for period: ${cogm}`);
    return cogm;
}

/**
 * Helper function to calculate all costs for a Work Order.
 * @param {object} workOrder - The Work Order object.
 * @param {string} costingMethod - The costing method to use.
 * @returns {Promise<object>} Object with materials, labor, and overhead costs.
 */
async function calculateCostsForWorkOrder(workOrder, costingMethod) {
    // Calculate material costs based on method
    let materialCost = 0;
    const bom = await getBOMForWorkOrder(workOrder);
    
    if (bom.components && bom.components.length > 0) {
        for (const component of bom.components) {
            if (!component.item_id || component.quantity === undefined) continue;
            
            let componentCost;
            switch (costingMethod) {
                case 'FIFO':
                    componentCost = await calculateFIFOCost(component.item_id, component.quantity);
                    break;
                case 'Average':
                    const unitCost = await calculateAverageCost(component.item_id);
                    componentCost = unitCost * component.quantity;
                    break;
                default:
                    const simpleCost = await getComponentCost_Simplified(component.item_id);
                    componentCost = simpleCost * component.quantity;
            }
            materialCost += componentCost;
        }
    }

    // Get labor and overhead from WO (assuming custom fields)
    const laborCost = workOrder.labor_cost || 0;
    const overheadCost = workOrder.overhead_cost || 0;

    return {
        materials: materialCost,
        labor: laborCost,
        overhead: overheadCost
    };
}
Purpose: This calculates the cost added to finished goods inventory during the period, accounting for WIP changes.

Step 4: Calculating COGS Using Inventory Data

For periodic COGS, use the formula with beginning/ending FG values. For perpetual, sum costs from sales movements.
/**
 * Calculates current inventory value for a part number using perpetual records.
 * @param {string} partNumber - The part number.
 * @returns {Promise<{quantity: number, value: number}>} Current stock and value.
 */
async function calculateCurrentInventoryValue(partNumber) {
    try {
        const movements = await client.inventory.list({
            filter: { part_number: { eq: partNumber } },
            limit: 10000 // Handle pagination if needed
        });

        let quantity = 0;
        let value = 0;
        movements.forEach(m => {
            quantity += m.quantity;
            value += m.quantity * m.unit_cost;
        });

        return { quantity, value: quantity > 0 ? value : 0 };
    } catch (error) {
        console.error(`Error calculating inventory value for ${partNumber}:`, error);
        throw error;
    }
}

/**
 * Calculates periodic COGS for finished goods.
 * @param {number} beginningFGValue - Beginning FG inventory value.
 * @param {number} cogm - COGM from Step 3.
 * @param {number} endingFGValue - Ending FG inventory value.
 * @returns {number} COGS.
 */
function calculatePeriodicCOGS(beginningFGValue, cogm, endingFGValue) {
    return beginningFGValue + cogm - endingFGValue;
}

/**
 * Calculates perpetual COGS by summing costs of sales movements.
 * @param {string} startDateISO - Start date.
 * @param {string} endDateISO - End date.
 * @param {string} fgPart - Finished good part number.
 * @returns {Promise<number>} Total COGS.
 */
async function calculatePerpetualCOGS(startDateISO, endDateISO, fgPart) {
    try {
        const salesMovements = await client.inventory.list({
            filter: {
                part_number: { eq: fgPart },
                quantity: { lt: 0 }, // Negative for sales/consumptions
                created_at: { gte: startDateISO, lte: endDateISO }
            }
        });

        return salesMovements.reduce((sum, m) => sum + Math.abs(m.quantity) * m.unit_cost, 0);
    } catch (error) {
        console.error(`Error calculating perpetual COGS:`, error);
        throw error;
    }
}

// Example: Calculate COGS for a specific finished good
async function calculateCOGSForProduct(fgPart, startDate, endDate, costingMethod) {
    // For periodic COGS
    const beginningFG = await calculateCurrentInventoryValue(fgPart); // Run at period start
    const endingFG = await calculateCurrentInventoryValue(fgPart); // Run at period end
    
    // Get completed WOs that produced this FG
    const workOrders = await getCompletedWorkOrders(startDate, endDate);
    const relevantWOs = workOrders.filter(wo => wo.finished_good_part === fgPart);
    
    // Calculate COGM for these WOs
    const cogm = await calculateCOGMForPeriod(relevantWOs, costingMethod, 0, 0); // Adjust WIP as needed
    
    // Calculate periodic COGS
    const periodicCOGS = calculatePeriodicCOGS(beginningFG.value, cogm, endingFG.value);
    
    // Also calculate perpetual for comparison
    const perpetualCOGS = await calculatePerpetualCOGS(startDate, endDate, fgPart);
    
    return {
        periodic: periodicCOGS,
        perpetual: perpetualCOGS,
        beginningInventory: beginningFG,
        endingInventory: endingFG,
        cogm: cogm
    };
}
Purpose: These methods provide accurate COGS based on inventory flows. Use perpetual for real-time insights and periodic for financial reporting.

Step 5: Recording COGS Entries

Once calculated, record COGS using Stateset’s dedicated COGS Entries API.
/**
 * Creates a COGS entry in Stateset.
 * @param {object} cogsData - Data for the COGS entry.
 * @returns {Promise<object>} The created COGS entry.
 */
async function createCOGSEntry(cogsData) {
    try {
        const entry = await client.cogs_entries.create(cogsData);
        console.log(`COGS Entry created: ${entry.id}`);
        return entry;
    } catch (error) {
        console.error(`Error creating COGS entry:`, error.response ? error.response.data : error.message);
        throw error;
    }
}

/**
 * Records COGS for a sale transaction.
 * @param {object} saleData - Information about the sale.
 * @param {number} calculatedCOGS - The calculated COGS amount.
 * @param {string} costingMethod - The method used for calculation.
 * @returns {Promise<object>} The created COGS entry.
 */
async function recordCOGSForSale(saleData, calculatedCOGS, costingMethod) {
    const cogsDetails = {
        period: saleData.period || new Date().toISOString().substring(0, 7), // YYYY-MM format
        product: saleData.product,
        quantity_sold: saleData.quantity,
        cogs: calculatedCOGS,
        average_cost: saleData.quantity > 0 ? calculatedCOGS / saleData.quantity : 0,
        ending_inventory_quantity: saleData.endingInventory?.quantity || 0,
        ending_inventory_value: saleData.endingInventory?.value || 0,
        currency: saleData.currency || 'USD',
        unit_selling_price: saleData.unitPrice,
        gross_sales: saleData.quantity * saleData.unitPrice,
        cogs_method: costingMethod,
        sale_date: saleData.saleDate || new Date().toISOString().split('T')[0]
    };
    
    return await createCOGSEntry(cogsDetails);
}

// Example: Complete workflow for recording a sale with COGS
async function processSaleWithCOGS(saleInfo, costingMethod = 'FIFO') {
    try {
        // 1. Record the inventory movement (negative for sale)
        const unitCost = costingMethod === 'FIFO' 
            ? await calculateFIFOCost(saleInfo.product, saleInfo.quantity) / saleInfo.quantity
            : await calculateAverageCost(saleInfo.product);
            
        const inventoryMovement = await client.inventory.create({
            part_number: saleInfo.product,
            quantity: -saleInfo.quantity,
            unit_cost: unitCost,
            source_document_id: saleInfo.orderId,
            source_document_type: 'sales_order'
        });
        
        // 2. Calculate COGS for this sale
        const cogs = unitCost * saleInfo.quantity;
        
        // 3. Get ending inventory for reporting
        const endingInventory = await calculateCurrentInventoryValue(saleInfo.product);
        
        // 4. Record COGS entry
        const cogsEntry = await recordCOGSForSale({
            ...saleInfo,
            endingInventory
        }, cogs, costingMethod);
        
        return {
            inventoryMovement,
            cogsEntry,
            cogs,
            grossProfit: (saleInfo.unitPrice * saleInfo.quantity) - cogs
        };
    } catch (error) {
        console.error('Error processing sale with COGS:', error);
        throw error;
    }
}
Purpose: This API records COGS for accounting and reporting, supporting methods like FIFO. The COGS Entries API requires calculated values as parameters.

Step 6: Periodic COGS Reporting

Generate reports summarizing COGS for analysis and financial reporting, incorporating both perpetual and periodic calculations.
/**
 * Generates a detailed COGS report for a specified period.
 * @param {string} startDateISO - ISO 8601 start date.
 * @param {string} endDateISO - ISO 8601 end date.
 * @param {string} costingMethod - 'Simplified', 'FIFO', or 'Average'.
 * @param {string} system - 'periodic' or 'perpetual'.
 * @returns {Promise<object>} A structured COGS report.
 */
async function generateCOGSReport(startDateISO, endDateISO, costingMethod = 'Simplified', system = 'periodic') {
    try {
        console.log(`Generating COGS Report from ${startDateISO} to ${endDateISO} using ${costingMethod} (${system} system)`);
        
        // Get all finished goods parts (you might want to filter this)
        const fgParts = ['FINISHED_GOOD_X', 'FINISHED_GOOD_Y']; // Example list
        const reportData = {
            period: `${startDateISO.split('T')[0]} to ${endDateISO.split('T')[0]}`,
            costingMethod: costingMethod,
            system: system,
            products: []
        };

        let totalCOGS = 0;
        let totalCOGM = 0;
        let totalSales = 0;

        for (const fgPart of fgParts) {
            // Calculate beginning and ending inventory
            const beginningFG = await calculateCurrentInventoryValue(fgPart);
            const endingFG = await calculateCurrentInventoryValue(fgPart);
            
            // Get WOs that produced this FG
            const workOrders = await getCompletedWorkOrders(startDateISO, endDateISO);
            const relevantWOs = workOrders.filter(wo => wo.finished_good_part === fgPart);
            
            // Calculate COGM for this product
            const cogm = await calculateCOGMForPeriod(relevantWOs, costingMethod, 0, 0); // Adjust WIP as needed
            totalCOGM += cogm;
            
            // Calculate COGS based on system type
            let productCOGS;
            if (system === 'periodic') {
                productCOGS = calculatePeriodicCOGS(beginningFG.value, cogm, endingFG.value);
            } else {
                productCOGS = await calculatePerpetualCOGS(startDateISO, endDateISO, fgPart);
            }
            totalCOGS += productCOGS;
            
            // Get sales data for gross profit calculation
            const salesMovements = await client.inventory.list({
                filter: {
                    part_number: { eq: fgPart },
                    quantity: { lt: 0 },
                    created_at: { gte: startDateISO, lte: endDateISO }
                }
            });
            
            const quantitySold = Math.abs(salesMovements.reduce((sum, m) => sum + m.quantity, 0));
            const averageCOGS = quantitySold > 0 ? productCOGS / quantitySold : 0;
            
            reportData.products.push({
                partNumber: fgPart,
                beginningInventory: beginningFG,
                endingInventory: endingFG,
                cogm: cogm,
                cogs: productCOGS,
                quantitySold: quantitySold,
                averageCOGSPerUnit: averageCOGS,
                workOrderCount: relevantWOs.length
            });
        }

        reportData.summary = {
            totalCOGM: totalCOGM,
            totalCOGS: totalCOGS,
            inventoryChange: reportData.products.reduce((sum, p) => 
                sum + (p.endingInventory.value - p.beginningInventory.value), 0)
        };

        console.log("COGS Report Generated Successfully.");
        return reportData;
    } catch (error) {
        console.error("Error generating COGS Report:", error.message);
        throw error;
    }
}

/**
 * Generates a summary COGS report with key metrics.
 * @param {string} period - The reporting period (e.g., '2025-Q1').
 * @param {object} reportData - The detailed report data.
 * @returns {object} Summary metrics.
 */
function generateCOGSSummary(period, reportData) {
    const summary = {
        period: period,
        keyMetrics: {
            totalCOGS: reportData.summary.totalCOGS,
            totalCOGM: reportData.summary.totalCOGM,
            inventoryTurnover: reportData.products.reduce((sum, p) => {
                const avgInventory = (p.beginningInventory.value + p.endingInventory.value) / 2;
                return sum + (avgInventory > 0 ? p.cogs / avgInventory : 0);
            }, 0) / reportData.products.length,
            grossMarginImpact: 'Calculate based on sales data'
        },
        recommendations: []
    };

    // Add recommendations based on metrics
    if (summary.keyMetrics.inventoryTurnover < 4) {
        summary.recommendations.push('Low inventory turnover detected. Consider reducing safety stock or improving demand forecasting.');
    }

    return summary;
}

// Example Usage:
const startDateReport = '2025-01-01T00:00:00.000Z';
const endDateReport = '2025-03-31T23:59:59.999Z';
const report = await generateCOGSReport(startDateReport, endDateReport, 'FIFO', 'periodic');
console.log("Generated COGS Report:", JSON.stringify(report, null, 2));

const summary = generateCOGSSummary('2025-Q1', report);
console.log("COGS Summary:", summary);
Purpose: Consolidate calculated COGS data into a structured format for review, analysis, and integration with financial systems. This enhanced report supports both periodic and perpetual systems and includes COGM calculations.

Costing Methodologies

As highlighted in the examples, the calculateCOGSForWorkOrder_Simplified function used the latest component cost. This is often insufficient for formal accounting. Stateset’s inventory resource, by tracking individual movements with unit_cost, provides the necessary data foundation to implement standard costing methods:
  • Average Cost: Requires calculating a running average cost for each part number based on all receipts and their costs over time. When components are consumed, this average cost is used.
  • Weighted Average Cost: Requires recalculating the average cost after each purchase. Query inventory movements, apply the weighted average formula, and use this cost for subsequent consumptions until the next purchase.
  • FIFO/LIFO: Requires querying inventory movements sorted by created_at (or another relevant date field). Consume the oldest (FIFO) or newest (LIFO) cost layers first, tracking remaining quantities in each layer.
Implementing these methods involves more complex query logic against the inventory resource data than shown in the simplified examples. You would typically fetch relevant inventory movements for a component, sort them appropriately, and apply the chosen costing logic to determine the value of consumed items.

Example: Implementing FIFO Costing

Here’s a simplified example of calculating the cost for consuming a certain quantity of a component using FIFO. This assumes that receipt quantities represent available layers and doesn’t account for prior partial consumptions. In a full system, you’d track remaining quantities per receipt.
/**
 * Calculates the FIFO cost for consuming a specified quantity of a part.
 * @param {string} partNumber - The part number.
 * @param {number} quantityToConsume - The quantity to consume.
 * @returns {Promise<number>} The total cost for the consumed quantity.
 */
async function calculateFIFOCost(partNumber, quantityToConsume) {
    try {
        // Fetch all positive inventory movements (receipts) sorted by created_at ascending (oldest first)
        const receipts = await client.inventory.list({
            filter: {
                part_number: { eq: partNumber },
                quantity: { gt: 0 } // Receipts have positive quantity
            },
            sort: 'created_at', // Ascending for FIFO
            limit: 1000 // Adjust as needed
        });

        let remaining = quantityToConsume;
        let totalCost = 0;

        for (const receipt of receipts) {
            const available = receipt.quantity; // Simplified; track remaining in real systems
            const consume = Math.min(available, remaining);
            totalCost += consume * receipt.unit_cost;
            remaining -= consume;
            if (remaining <= 0) break;
        }

        if (remaining > 0) {
            throw new Error(`Insufficient inventory for ${partNumber} using FIFO.`);
        }

        return totalCost;
    } catch (error) {
        console.error(`Error calculating FIFO cost for ${partNumber}:`, error.message);
        throw error;
    }
}

// Usage in COGS calculation:
// const componentCost = await calculateFIFOCost(component.item_id, component.quantity);
// totalMaterialCost += componentCost;

Example: Implementing Average Costing

Here’s an example for simple average cost based on all historical receipts.
/**
 * Calculates the simple average cost for a part based on all receipts.
 * @param {string} partNumber - The part number.
 * @returns {Promise<number>} The average unit cost.
 */
async function calculateAverageCost(partNumber) {
    try {
        const receipts = await client.inventory.list({
            filter: {
                part_number: { eq: partNumber },
                quantity: { gt: 0 }
            },
            limit: 1000
        });

        if (receipts.length === 0) return 0;

        const totalQuantity = receipts.reduce((sum, r) => sum + r.quantity, 0);
        const totalCost = receipts.reduce((sum, r) => sum + (r.quantity * r.unit_cost), 0);
        return totalCost / totalQuantity;
    } catch (error) {
        console.error(`Error calculating average cost for ${partNumber}:`, error.message);
        throw error;
    }
}

// Usage:
// const unitCost = await calculateAverageCost(component.item_id);
// totalMaterialCost += unitCost * component.quantity;
For Weighted Average, you would recalculate the average after each new receipt and use that until the next update.

Advanced COGS Considerations

  • Direct Labor and Overhead: Use custom fields on Work Orders or integrate with time-tracking systems to capture these. Allocate overhead based on machine hours, labor hours, or activity-based costing.
    // Example: Adding labor and overhead to Work Orders
    async function addLaborAndOverheadToWO(workOrderId, laborHours, hourlyRate, overheadRate) {
        const laborCost = laborHours * hourlyRate;
        const overheadCost = laborCost * overheadRate; // e.g., 150% of labor
        
        return await client.workorder.update(workOrderId, {
            labor_hours: laborHours,
            labor_cost: laborCost,
            overhead_cost: overheadCost,
            total_cost: laborCost + overheadCost // Plus materials from BOM
        });
    }
    
  • Inventory Adjustments: Record adjustments as inventory movements with costs (e.g., at average cost).
    async function recordInventoryAdjustment(partNumber, adjustmentQty, reason) {
        const avgCost = await calculateAverageCost(partNumber);
        return await client.inventory.create({
            part_number: partNumber,
            quantity: adjustmentQty, // Positive for found, negative for loss
            unit_cost: avgCost,
            source_document_type: 'adjustment',
            notes: reason
        });
    }
    
  • Standard Costing: Store standard costs on items; calculate variances by comparing to actuals from POs/WOs.
    async function calculateCostVariance(partNumber, actualCost, standardCost) {
        const variance = actualCost - standardCost;
        const variancePercentage = (variance / standardCost) * 100;
        
        // Record variance for analysis
        return {
            partNumber,
            standardCost,
            actualCost,
            variance,
            variancePercentage,
            favorable: variance < 0
        };
    }
    
  • Perpetual Inventory Enhancements: Use webhooks for real-time COGS updates on sales events.
    // Webhook handler for real-time COGS
    async function handleSaleWebhook(saleEvent) {
        const result = await processSaleWithCOGS({
            product: saleEvent.product,
            quantity: saleEvent.quantity,
            unitPrice: saleEvent.price,
            orderId: saleEvent.order_id,
            saleDate: saleEvent.date
        }, 'FIFO');
        
        // Update accounting system
        await updateAccountingSystem(result);
    }
    
  • Multi-Currency and Regulations: Handle exchange rates via API; ensure compliance (e.g., no LIFO under IFRS).
    async function convertCurrencyForCOGS(amount, fromCurrency, toCurrency, date) {
        // Fetch exchange rate from API or stored rates
        const rate = await getExchangeRate(fromCurrency, toCurrency, date);
        return amount * rate;
    }
    
  • Integrations: Sync COGS entries with accounting software (e.g., QuickBooks, NetSuite) via Stateset webhooks.
    async function syncCOGSToAccounting(cogsEntry) {
        // Example: Push to QuickBooks
        const journalEntry = {
            date: cogsEntry.sale_date,
            entries: [
                {
                    account: 'Cost of Goods Sold',
                    debit: cogsEntry.cogs,
                    description: `COGS for ${cogsEntry.product}`
                },
                {
                    account: 'Inventory',
                    credit: cogsEntry.cogs,
                    description: `Inventory reduction for ${cogsEntry.product}`
                }
            ]
        };
        
        // Send to accounting system API
        await accountingAPI.createJournalEntry(journalEntry);
    }
    

Error Handling Best Practices

Robust error handling is critical for accurate financial calculations.
  • Specific Error Catching: Use try...catch blocks around all API calls. Inspect the error object. Stateset SDK errors often have error.response.data containing details from the API.
  • Input Validation: Validate data before sending it to the API (e.g., check for required fields, correct data types, sensible values).
  • Retry Mechanisms: Implement exponential backoff retries for transient network errors or rate limiting (e.g., HTTP 429, 503). Avoid retrying non-transient errors (e.g., HTTP 400 Bad Request, 404 Not Found) without correcting the request.
  • Idempotency: When creating resources that shouldn’t be duplicated (like POs), consider using idempotency keys if the API supports them, or implement checks to prevent duplicate creation.
  • Centralized Logging: Log detailed error information (timestamp, operation, input data summary, error message, stack trace, API response) to a centralized logging system for easier debugging and monitoring.
  • Alerting: Set up alerts for critical failures in the COGS calculation process.
// Enhanced Error Handling Wrapper Example
async function safeApiOperation(operation, description) {
    try {
        console.log(`Attempting operation: ${description}`);
        const result = await operation();
        console.log(`Operation successful: ${description}`);
        return result;
    } catch (error) {
        const timestamp = new Date().toISOString();
        let errorDetails = {
            message: error.message,
            stack: error.stack,
            description: description,
            timestamp: timestamp
        };

        if (error.response) {
            // Capture API error details
            errorDetails.apiStatus = error.response.status;
            errorDetails.apiResponse = error.response.data;
        }

        console.error(`Error during operation: ${description}`, errorDetails);

        // Log to external system (replace console.error)
        // externalLogger.error('COGS Process Error', errorDetails);

        // Notify relevant personnel (placeholder)
        // notifyTeam(`Critical error in ${description}: ${error.message}`);

        // Re-throw or return an error indicator based on desired flow
        throw new Error(`Operation failed: ${description}. Details: ${errorDetails.message}`);
    }
}

// Usage Example:
const poDetails = { /* ... */ };
try {
    const purchaseOrder = await safeApiOperation(
        () => createPurchaseOrder(poDetails),
        `Create PO ${poDetails.number}`
    );
    // ... continue workflow
} catch (finalError) {
    console.error("Workflow halted due to critical error:", finalError.message);
    // Handle the halt appropriately
}

Troubleshooting Common Issues

  • Authentication Errors (401/403):
    • Solution: Verify STATESET_API_KEY is correct, loaded properly into the environment, and hasn’t expired or been revoked. Check API key permissions.
  • Validation Errors (400 Bad Request):
    • Solution: Check the error.response.data for specific field errors. Ensure all required fields are provided, data types are correct (e.g., numbers vs. strings, ISO dates), and values are valid (e.g., status codes).
  • Resource Not Found Errors (404):
    • Solution: Double-check the IDs being used in get, update, or delete calls (e.g., poId, bomId). Ensure the resource actually exists.
  • Incorrect Inventory Values:
    • Solution: Ensure all movements (positive/negative) are recorded consistently. Audit aggregations by running:
    async function auditInventoryValue(partNumber) {
        const movements = await client.inventory.list({
            filter: { part_number: { eq: partNumber } },
            sort: 'created_at',
            limit: 10000
        });
        
        let runningQty = 0;
        let runningValue = 0;
        
        movements.forEach(m => {
            console.log(`${m.created_at}: Qty=${m.quantity}, Cost=${m.unit_cost}, Running Qty=${runningQty + m.quantity}`);
            runningQty += m.quantity;
            runningValue += m.quantity * m.unit_cost;
        });
        
        return { finalQty: runningQty, finalValue: runningValue };
    }
    
  • COGS Mismatches:
    • Solution:
      • Verify costing method consistency across consumptions and sales. Check for unrecorded labor/overhead.
      • Ensure BOMs are accurate with correct components and quantities.
      • Validate Work Order quantities match actual production.
      • Cross-check perpetual vs periodic calculations:
      async function validateCOGS(product, period) {
          const perpetual = await calculatePerpetualCOGS(period.start, period.end, product);
          const periodic = await calculateCOGSForProduct(product, period.start, period.end, 'FIFO');
          
          const difference = Math.abs(perpetual - periodic.periodic);
          if (difference > 0.01) {
              console.warn(`COGS mismatch for ${product}: Perpetual=${perpetual}, Periodic=${periodic.periodic}`);
          }
      }
      
  • API Errors for COGS Entries:
    • Solution: Ensure all required parameters (e.g., cogs_method) are provided correctly. Valid methods include ‘FIFO’, ‘LIFO’, ‘AVERAGE’. Check that numeric fields are numbers, not strings.
  • Data Inconsistencies:
    • Solution: Implement regular data audits. Compare Stateset inventory levels/values with physical counts or accounting records. Ensure workflows correctly record all movements (receipts, consumptions, adjustments, shipments).
    async function performDataAudit() {
        const products = ['FINISHED_GOOD_X', 'RAW_MAT_A'];
        const auditResults = [];
        
        for (const product of products) {
            const statesetValue = await calculateCurrentInventoryValue(product);
            // Compare with external system or physical count
            auditResults.push({
                product,
                statesetQty: statesetValue.quantity,
                statesetValue: statesetValue.value,
                // externalQty: getFromExternalSystem(product),
                // variance: calculateVariance()
            });
        }
        
        return auditResults;
    }
    

Support Resources

For further assistance, refer to the official Stateset resources:

Conclusion

This enhanced guide demonstrates how to utilize the Stateset API for a complete, accurate COGS workflow—from tracking costs in production (COGM) to calculating and recording COGS based on sales and inventory changes. By distinguishing key concepts like COGM vs. COGS, incorporating dedicated API endpoints for COGS entries, and supporting both perpetual and periodic inventory systems, you can achieve precise financial insights. Key improvements covered in this guide include:
  • Clear distinction between Cost of Goods Manufactured (COGM) and Cost of Goods Sold (COGS)
  • Support for both perpetual and periodic inventory systems
  • Complete workflow from purchase orders through production to sales
  • Integration with Stateset’s COGS Entries API for proper accounting records
  • Implementation of multiple costing methods (FIFO, LIFO, Average Cost)
  • Tracking of direct labor and manufacturing overhead
  • Comprehensive error handling and troubleshooting guidance
  • Advanced considerations for multi-currency, standard costing, and system integrations
Remember to adapt the logic to your specific business needs and consult accounting professionals for compliance with relevant regulations (GAAP/IFRS). Leveraging Stateset effectively empowers data-driven decisions, enhances cost control, and ultimately contributes to improved business performance.