Elaborare una distinta base con Envision

La distinta base












Home » Risorse » Qui

L'analisi delle scorte può essere ulteriormente complicata dalla presenza di una distinta base (o Diba, o BOM). Se esiste una distinta base, infatti, vuol dire che gli articoli venduti o forniti come servizio non corrispondono a quelli acquistati, poiché si è verificato un passaggio intermedio di assemblaggio o produzione industriale. Ogni assemblaggio può essere prodotto a partire da una lista di componenti, ognuno dei quali è presente in una determinata quantità all'interno dell'assemblaggio. Per ottimizzare le scorte dei componenti utilizzati, è spesso necessario tradurre la domanda originale, espressa in assemblaggi, in domanda espressa in componenti. A tal fine abbiamo messo a punto la funzione billOfMaterials(), che in Envision serve proprio a questo scopo.


La tabella Bill Of Materials

Una distinta base, o Diba (in inglese Bill Of Materials o BOM), è un elenco di tutte le materie prime, i componenti, sottocomponenti, semilavorati e sottoassiemi necessari per realizzare un prodotto finale. Nella più semplice delle ipotesi, la tabella BOM si presenta come una tabella a tre colonne:

  • la colonna Bundle (assemblaggio) identifica l'assemblaggio o il prodotto finale;
  • la colonna Part (componente) identifica uno dei componenti dell'assemblaggio;
  • la colonna Quantity (quantità) quantifica le unità di ogni componente all'interno dell'assemblaggio.

A livello pratico, una tabella BOM è decisamente ricorsiva, perché ogni assemblaggio può essere costituito da altri assemblaggi. Quindi, per individuare i componenti finali necessari per un certo assemblaggio, occorre elaborare la tabella BOM in modo ricorsivo. Se all'interno della tabella BOM è presente una dipendenza circolare, allora esistono delle incongruenze (una dipendenza circolare significa, sostanzialmente, che un assemblaggio è composto dall'assemblaggio stesso, più altri componenti, il che, ovviamente, non ha senso).

Per convenzione, qualsiasi articolo non sia compreso nella tabella BOM è considerato come un prodotto finito: questi articoli vanno dunque lasciati così come sono, poiché non richiedono particolari elaborazioni.

Sintassi di extend.billOfMaterials()

Scopo della funzione billOfMaterials() è quello di tradurre la domanda osservata a livello di assemblaggio in domanda espressa per componenti. Il procedimento è controllato attraverso la tabella della distinta base, che specifica la composizione di ogni assemblaggio. La sintassi di extend.billOfMaterials() è la seguente:
table T = extend.billOfMaterials(
  Item: J.Id
  Part: B.PartId
  Quantity: B.Quantity
  DemandId: O.Oid // 'Oid' sta per OrderId
  DemandValue: O.Quantity)

// come ottenere la data  
// come esportare "T"
T.DemandDate = same(O.Date) by O.Oid at T.DemandId 
show table "Translated" with 
J.Id
T.DemandDate
T.Quantity
Sono previste tre tabelle: J, B e O. La tabella J corrisponde di solito, ma non necessariamente, alla tabella degli articoli. La tabella B è la distinta base vera e propria e deve essere un'estensione della tabella J, quindi del tipo [Id, *], dove J è la tabella degli articoli. La tabella O deve contenere i dati storici sulla domanda, di solito facendo riferimento alla tabella degli ordini; anche la tabella O deve essere un'estensione della tabella J.

Gli argomenti della funzione sono:
  • Item, utilizzato per identificare gli assemblaggi o i componenti presenti nella tabella della domanda originale;
  • Part, utilizzato per identificare la colonna dei componenti nella tabella BOM;
  • Quantity, utilizzato per identificare la colonna delle quantità nella tabella BOM. Rappresenta il numero di unità di B.PartId implicate nel vendere o servire un assemblaggio identificato da J.Id;
  • DemandId, utilizzato per mantenere un'affinità tra la tabella originale O e la sua estensione T;
  • DemandValue, che definisce il numero di unità presenti nella tabella della domanda originale.

La tabella che ne risulta è un'estensione della tabella J, con tutte le affinità del caso. La tabella T contiene due campi già popolati:
  • T.DemandId, che offre un modo per identificare, all'interno della tabella T, le righe originali presenti nella tabella O;
  • T.Quantity, ottenuto a partire dalla distinta base.

Spesso è utile inserire una data nella tabella T, aggiungendo un assegnamento by-at, come illustrato dalla riga sotto il blocco billOfMaterials() qui sopra.

La funzione extend.billOfMaterials() supporta assemblaggi ricorsivi: ciò significa che, all'interno della tabella B, uno stesso elemento può comparire sia come componente, sia come assemblaggio. Ovviamente, se nella tabella della distinta base sono rilevate dipendenze cicliche, la funzione dà come risultato un errore.

Un esempio con i nostri dati campione

Lo script qui di seguito può essere eseguito direttamente con i nostri dati campione. Lo script traspone la tabella originale degli ordini, Orders, in una nuova tabella T, che rappresenta la stessa domanda, ma espressa a livello di componenti.
read "/sample/Lokad_Items.tsv" with 
"Ref" as Id
read "/sample/Lokad_Orders.tsv" as O with 
"Ref" as Id
"NetAmount" as NetA : number
"OrderId" as Oid : number
read "/sample/Lokad_BOM.tsv" as BOM[Id, *] with 
"Bundle" as Id

// 'concat()' per ottenere un vettore 'text'
O.Uid = concat(rank() sort[0.1])

table T = extend.billOfMaterials(
  Item: Id
  Part: BOM.Part
  Quantity: BOM.Quantity
  DemandId: O.Uid
  DemandValue: O.Quantity)

T.DemandDate = same(O.Date) by O.Uid at T.DemandId

// assegnare un prezzo di vendita ai componenti
T.NetA = same(O.NetA) by O.Uid at T.DemandId
T.PartPrice = BuyPrice * T.Quantity
T.BundlePrice = sum(T.PartPrice) by T.DemandId
T.NetA = T.NetA * (T.PartPrice / T.BundlePrice)

// assegnare un prezzo di acquisto ai componenti
O.Cost = sum(T.PartPrice) by T.DemandId at O.Uid

show table "Expanded" with
  Id
  T.DemandDate
  T.Quantity
  T.NetA

Il set di dati campioni non contiene un identificatore di righe nella tabella O: alla riga 11, quindi, creiamo un identificatore al solo scopo di poter ricollegare le tabelle più tardi. Dunque, la riga 11 serve solo ad assegnare un numero univoco a ogni riga della tabella O e convertire questo numero in testo attraverso la funzione concat().

Spesso, quando si vendono assemblaggi, i componenti non hanno un vero e proprio prezzo di vendita: certo, è possibile assegnare un prezzo di vendita arbitrario ai vari componenti, ma il prezzo di mercato è comunque definito più che altro a livello di assemblaggio. In un'ottica di ottimizzazione delle scorte, però, è importante assegnare un prezzo di vendita ai componenti, altrimenti non sarebbe possibile calcolare il margine lordo relativo ai componenti, e il margine lordo è proprio il motivo per cui i componenti sono tenuti a magazzino.

Per assegnare un prezzo di vendita ai componenti, possiamo stabilire che ogni componente abbia un prezzo di vendita proporzionale al prezzo di vendita dell'assemblaggio, moltiplicato per il "peso" del componente all'interno dell'assemblaggio (per peso intendiamo il costo del componente all'interno dell'assemblaggio, confrontato con il costo totale dell'assemblaggio). Le righe da 23 a 26 illustrano come eseguire il calcolo: T.NetA è la frazione dell'importo netto dell'assemblaggio assegnato al componente. È probabile che il prezzo del componente vari da una transazione all'altra, a seconda dell'assemblaggio venduto. Solitamente, il prezzo finale di vendita del componente è ottenuto calcolando la media del prezzo del componente negli ultimi 3 (ipotesi) mesi.

Per assegnare un prezzo di acquisto agli assemblaggi, il calcolo è molto più semplice e non richiede ulteriori ipotesi. Basta, infatti, sommare il prezzo di acquisto dei componenti, applicando il corretto multiplo in base alla quantità di ciascuno di essi. Il calcolo è illustrato alla riga 29 dello script.

Livellare la distinta base

Una distinta base ricorsiva è più difficile da elaborare rispetto a una non ricorsiva. Dal punto di vista della gestione dei dati, però, una versione ricorsiva è più facile da portare avanti. Può essere quindi utile livellare la distinta base, ossia eliminare tutte le dipendenze ricorsive dalla tabella. Il procedimento è ottenuto con questo script:
read "/sample/Lokad_Items.tsv" with
  "Ref" as Id
read "/sample/Lokad_BOM.tsv" as BOM[Id, *] with
  "Bundle" as Id

table T = extend.billOfMaterials(
  Item: Id
  Part: BOM.Part
  Quantity: BOM.Quantity
  DemandId: Id
  DemandValue: 1)

// eliminating trivial entries
where T.DemandId != Id
  show table "Flattened BOM" with
    T.DemandId as "Bundle"
    Id as "Part"
    T.Quantity
Applichiamo billOfMaterials() direttamente alla tabella degli articoli e filtriamo le voci meno importanti, quelle cioè dove l'assemblaggio contiene un solo componente, ossia l'assemblaggio stesso. La tabella così filtrata è una distinta base livellata.