Skip to main content

FrancescoZanti.dev

[POC] Bom Compare: strumento di confronto tra due distinte base

Table of Contents

# Introduzione

Lo strumento BOM-Compare nasce dalla necessità di confrontare due Bill of Materials (BOM) per verificare le differenze: nello specifico il confronto avviene in base alla presenza o meno di un componente, alla differenza di quantità; non è stato preso in considerazione il confronto di altre proprietà come il livello della distinta base, la posizione, il tipo di componente, ecc.

Nello specifico, se non conosciamo come funzionino le BOM (o distinte base) ecco una breve spiegazione: una distinta base è una lista di materiali, parti, sottassiemi, componenti, ecc., necessari per la produzione di un prodotto. La distinta base è un documento tecnico che elenca tutti i componenti di un prodotto, insieme alle relative quantità e alle istruzioni per l’assemblaggio; quindi diventa chiaro come il confronto tra due distinte base possa essere un’operazione complessa e che richiede tempo: lo strumento BOM-Compare è stato realizzato per semplificare e velocizzare questo processo.

Le BOM possono essere molto complesse e possono includere centinaia o migliaia di componenti, risulterebbe difficile anche per una persona esperta confrontare due distinte basi manualmente; qui sotto è possibile vedere un esempio di distinta base:


Immagine: Datalog

Il concetto è estendibile in maniera ricorsiva, ovvero una distinta base può includere altre distinte base, che a loro volta possono includere altre distinte base: questo può rendere l’esplosione della distinta base e il successivo confronto esoso in termini di tempo e risorse.

# Tecnologie utilizzate

  • C#
  • MS SQL Server

# Logiche di funzionamento

Risiedendo i dati in un database SQL Server, il confronto avviene tramite una stored procedure che riceve in input i due BOM da confrontare e restituisce un risultato che indica le differenze: questo risulta essere un approccio più immediato per chi lavora quotidianamente sui database. A C# viene demandata invece la parte di interfaccia grafica e di gestione delle chiamate al database.

Tramite il Windows Form è possibile inserire i codici articolo delle due distinte base da confrontare ed eseguire il processo: nell’immediato nell’interfaccia grafica verrano indicate le differenze e le somiglianze, con colori differenti; sarà possibile inoltre esportare il risultato in un file Excel.


L'interfaccia grafica del tool BOM-Compare



Esempio di confronto tra due BOM

# Logiche di confronto

Il confronto tra i due BOM avviene tramite una stored procedure che riceve in input i due codici articolo da confrontare e restituisce un risultato che indica le differenze. La stored procedure è stata realizzata in modo tale da essere facilmente estendibile e modificabile, in modo da poter essere utilizzata in contesti diversi.

In questo esempio sono state prese in considerazione solo alcune proprietà della distinta base come il codice articolo e la quantità: infatti come è possibile vedere dal codice sorgente della stored procedure, il confronto avviene in base alla presenza o meno di un componente, alla differenza di quantità; non è stato preso in considerazione il confronto di altre proprietà come il livello della distinta base, la posizione, il tipo di componente. Questi confronti sono facilemnte estendibili e modificabili conoscendo la struttura del database.

USE [AdventureWorks]
GO

/****** Object:  StoredProcedure [dbo].[uspCompareBillOfMaterials]    Script Date: 20/08/2024 22:26:33 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

-- =============================================
-- Author:		<Francesco Zanti>
-- Create date: <2024-08-08>
-- Description:	<Compare BOMs>
-- =============================================

CREATE PROCEDURE [dbo].[uspCompareBillOfMaterials]
    @StartProductID_1 [int],
	@StartProductID_2 [int],
    @CheckDate [datetime] 
AS
BEGIN
    SET NOCOUNT ON;

	SET @CheckDate = GETDATE();


	-- EXEC [dbo].[uspCompareBillOfMaterials] 788, 789, '2024-01-01T00:00:00.000'

    -- Use recursive query to generate a multi-level Bill of Material (i.e. all level 1 
    -- components of a level 0 assembly, all level 2 components of a level 1 assembly)
    -- The CheckDate eliminates any components that are no longer used in the product on this date.
    WITH [BOM_cte_1]([ProductAssemblyID], [ComponentID], [ComponentDesc], [PerAssemblyQty], [StandardCost], [ListPrice], [BOMLevel], [RecursionLevel]) -- CTE name and columns
    AS (
        SELECT b.[ProductAssemblyID], b.[ComponentID], p.[Name], b.[PerAssemblyQty], p.[StandardCost], p.[ListPrice], b.[BOMLevel], 0 -- Get the initial list of components for the bike assembly
        FROM [Production].[BillOfMaterials] b
            INNER JOIN [Production].[Product] p 
            ON b.[ComponentID] = p.[ProductID] 
        WHERE b.[ProductAssemblyID] = @StartProductID_1 
            AND @CheckDate >= b.[StartDate] 
            AND @CheckDate <= ISNULL(b.[EndDate], @CheckDate)
        UNION ALL
        SELECT b.[ProductAssemblyID], b.[ComponentID], p.[Name], b.[PerAssemblyQty], p.[StandardCost], p.[ListPrice], b.[BOMLevel], [RecursionLevel] + 1 -- Join recursive member to anchor
        FROM [BOM_cte_1] cte
            INNER JOIN [Production].[BillOfMaterials] b 
            ON b.[ProductAssemblyID] = cte.[ComponentID]
            INNER JOIN [Production].[Product] p 
            ON b.[ComponentID] = p.[ProductID] 
        WHERE @CheckDate >= b.[StartDate] 
            AND @CheckDate <= ISNULL(b.[EndDate], @CheckDate)
        ),

		[BOM_cte_2]([ProductAssemblyID], [ComponentID], [ComponentDesc], [PerAssemblyQty], [StandardCost], [ListPrice], [BOMLevel], [RecursionLevel]) -- CTE name and columns
    AS (
        SELECT b.[ProductAssemblyID], b.[ComponentID], p.[Name], b.[PerAssemblyQty], p.[StandardCost], p.[ListPrice], b.[BOMLevel], 0 -- Get the initial list of components for the bike assembly
        FROM [Production].[BillOfMaterials] b
            INNER JOIN [Production].[Product] p 
            ON b.[ComponentID] = p.[ProductID] 
        WHERE b.[ProductAssemblyID] = @StartProductID_2 
            AND @CheckDate >= b.[StartDate] 
            AND @CheckDate <= ISNULL(b.[EndDate], @CheckDate)
        UNION ALL
        SELECT b.[ProductAssemblyID], b.[ComponentID], p.[Name], b.[PerAssemblyQty], p.[StandardCost], p.[ListPrice], b.[BOMLevel], [RecursionLevel] + 1 -- Join recursive member to anchor
        FROM [BOM_cte_2] cte
            INNER JOIN [Production].[BillOfMaterials] b 
            ON b.[ProductAssemblyID] = cte.[ComponentID]
            INNER JOIN [Production].[Product] p 
            ON b.[ComponentID] = p.[ProductID] 
        WHERE @CheckDate >= b.[StartDate] 
            AND @CheckDate <= ISNULL(b.[EndDate], @CheckDate)
        )

    -- Outer select from the CTE
	SELECT 


	CASE 

	WHEN b1.ComponentID IS NULL AND b2.ComponentID IS NOT NULL THEN '1. Added'
	WHEN b1.ComponentID IS NOT NULL AND b2.ComponentID IS NULL THEN '2. Deleted'
	WHEN b1.PerAssemblyQty <> b2.PerAssemblyQty THEN '3. Qty Modified'
	ELSE '0. Nothing change' 

	END AS Outcome,


	b1.ProductAssemblyID AS AssID_1,
	b1.ComponentID AS CompID_1,
	b1.ComponentDesc AS CompDesc_1,
	b1.PerAssemblyQty AS Qty_1,
	b1.BOMLevel AS Level_1,

	b2.ProductAssemblyID AS AssID_2,
	b2.ComponentID AS CompID_2,
	b2.ComponentDesc AS CompDesc_2,
	b2.PerAssemblyQty AS Qty_2,
	b2.BOMLevel AS Level_2


    FROM [BOM_cte_1] b1

	LEFT JOIN [BOM_cte_2] b2
	ON b1.ComponentID = b2.ComponentID


    -- ORDER BY b1.[BOMLevel], b1.[ProductAssemblyID], b1.[ComponentID]
    
END;
GO

La parte del codice SQL nella procedura soprastante è già implementato nel database di “AdventureWorks”.

# Conclusioni

Il progetto è stato realizzato in un tempo molto breve e con l’obiettivo di risolvere un problema concreto: il confronto tra due distinte base. Il tool è stato realizzato in maniera tale da essere facilmente estendibile e modificabile, in modo da poter essere utilizzato in contesti diversi.

Il codice sorgente è disponibile su GitHub e può essere utilizzato liberamente nei termini della licenza.