物料清单

物料清单


首页 » 资源 » 此处

物料清单 (BOM) 的存在让库存分析变得复杂化。在涉及到 BOM 时,所售或为之提供服务的项目与所购项目通常是不同的,因为其间实施了捆绑或制造操作。每个捆绑包或组件可以由部件列表生成,每个部件对应于捆绑包中的特定数量。为优化部件的库存水平,通常需要将按捆绑包或组件体现的原始需求转化为按部件体现的需求。Envision 中的函数 billOfMaterials() 便是专门针对该种使用案例而定制的。


物料清单表概述

物料清单 (BOM) 或产品结构即原材料、子组件、中间组件、分组件、部件以及制造最终产品(即捆绑包)所需数量的列表。最简单的 BOM 表包含三列:

  • Bundle 列用于标识捆绑包或最终产品。
  • Part 列用于标识捆绑包中的一个部件。
  • Quantity 列用于计算捆绑包中特定部件所需的件数。

实际上,BOM 表常常是递归性的:一个捆绑包可能由其他捆绑包组成。因此,为了识别特定捆绑包所需的最终部件,BOM 表需要予以递归处理。如果发现 BOM 表中存在循环依赖关系,则说明此表是不一致的。从本质上说,循环依赖关系意味着一个捆绑包由该捆绑包本身及其他一些部件组成,而这是没有意义的。

按照惯例,BOM 表中未出现的项目可以认定为是最终产品;因此,此项目无需任何特殊处理,应保留原样。

extend.billOfMaterials() 的语法

billOfMaterials() 函数的作用是将捆绑包级别中观察到的需求转化为部件级别体现的需求。这种转化通过物料清单表来控制,此表指定了各个捆绑包的组成。extend.billOfMaterials() 的语法如下:
table T = extend.billOfMaterials(
  Item: J.Id
  Part: B.PartId
  Quantity: B.Quantity
  DemandId: O.OrderId
  DemandValue: O.Quantity)

// illustrating how to get the date and how to export 'T'
T.DemandDate = same(O.Date) by O.OrderId at T.DemandId  
show table "Translated" with J.Id, T.DemandDate, T.Quantity
应当存在 JBO 这三个表。表 J 通常为项目表,但这并非硬性规定。表 B 应为物料清单;它是表 J 的扩展,即类型为 [Id, *],前提是 J 为项目表。表 O 应为需求历史记录,通常指订单表。表 O 也应为表 J 的扩展。

此函数的参数如下:
  • Item 参数用于识别初始需求表中的捆绑包或部件。
  • Part 参数用于识别 BOM 表中的部件列。
  • Quantity 参数用于识别 BOM 表中的数量列。它表示销售或服务 J.Id 标识的组件时所涉及的 B.PartId 的单位数量。
  • DemandId 参数用于保留初始表 O 与其扩展 T 之间的关系。
  • DemandValue 参数用于表示初始需求表中的单位数量。

最后得出的表就是表 J 的扩展,此扩展表具有适当的仿射性。表 T 包含两个已填充的字段:
  • T.DemandId 用于提供在表 O 中识别表 O 中存在的原始行的途径。
  • T.Quantity 通过滚动物料清单获取。

将日期注入表 T 往往很有用。这可以通过 by-at 语句来完成,上文中 billOfMaterials() 块下面的这一行说明了这一点。

extend.billOfMaterials() 函数支持递归捆绑包。这就是说,一个项目在表 B 中既可以作为组件出现,也可以作为捆绑包出现。如果物料清单表中发现循环依赖关系,此函数自然而然会失效并导致错误。

使用样本数据集进行说明

下面这段脚本可以直接使用样本数据集执行。这段脚本将初始表 Orders 转化为新表 T,新表表示了仅在部件级别重新体现的相同需求。
read "/sample/Lokad_Items.tsv" with "Ref" as Id
read "/sample/Lokad_Orders.tsv" as Orders with "Ref" as Id
read "/sample/Lokad_BOM.tsv" as BOM[Id, *] with "Bundle" as Id

Orders.Uid = concat(rank(Orders.1)) // 'concat()' to get a 'text' vector

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

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

// assigning a sell price to parts
T.NetAmount = same(Orders.NetAmount) by Orders.Uid at T.DemandId
T.NetAmount = T.NetAmount * (BuyPrice * T.Quantity / sum(BuyPrice * T.Quantity) by T.DemandId)

// assigning a buy price to bundles
Orders.Cost = sum(BuyPrice * T.Quantity) by T.DemandId at Orders.Uid

show table "Expanded" with Id, T.DemandDate, T.Quantity, T.NetAmount

样本数据集在表 Orders 中不含行标识符,因此我们在第 5 行创建了该标识符,来专门用于在后续连接表格。第 5 行的作用是只为 Orders 表的每行指定唯一的数字,然后通过 concat() 函数将此数字转换为文本。

在销售捆绑包时,各部件往往没有"真正"的售价。当然,我们可以为每个部件指定任意售价,如果在捆绑包级别进行了市场价格的基本定义,则可以指定市场价格。但从库存优化的角度来说,为部件指定售价是很重要的,否则,就不能计算部件的毛利润;而毛利润是第一时间进行每个部件存货的核心原因。

因此,在对部件指定售价时,我们可以确定每个部件的售价与捆绑包售价成正比,再乘以该部件在捆绑包中的"权重";权重定义为部件在捆绑包的成本与捆绑包总成本之比。第 17-18 行说明了计算过程:T.NetAmount 是捆绑包净额中分配给此部件的部分。因此,部件价格因交易的不同而不同,具体取决于所售的捆绑包。部件的最终售价通常这样获取:比如对最近 3 个月的交易取部件价格取平均值。

指定捆绑包的购买价格时,计算要简单得多,无需任何进一步的假设。唯一的要求就是将部件的购买价格相加,并针对各自的数量乘以倍数。第 21 行说明了如何执行此类计算。

压平物料清单

递归物料清单比非递归物料清单更难处理。但从数据管理的观点而言,递归物料清单通常更易于维护。因此,压平物料清单很有用,换言之,即消除表中的所有递归依赖关系。使用下面的脚本可以执行此类压平处理。
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)

where T.DemandId != Id // eliminating trivial entries
  show table "Flattened BOM" with T.DemandId as "Bundle", Id as "Part", T.Quantity
我们直接对项目表应用 billOfMaterials(),然后过滤捆绑包中仅包含单独一个部件(即捆绑包本身)的琐碎条目。过滤后的表就是压平后的物料清单。