package schema import ( "entgo.io/ent" "entgo.io/ent/schema/field" "entgo.io/ent/schema/index" ) // Inventory 统一库存台账 // // 业务背景:原系统把库存拆成「结构件批次(inventory_batch)」与「电气件序列号(serial_number)」 // 两张表,导致 锁库存 / 导出 / 汇总 都要分别写两套逻辑,运维成本高且容易不一致。 // 现合并为一张表,用 manage_mode 区分管理粒度,对外提供统一的 锁/查询/汇总/盘点 能力。 // // 两种管理粒度的存储约定: // - manage_mode = 1 结构件(批次管理): // 一条记录 = 一个批次,quantity 表示该批次当前余量,locked_qty 表示已锁定件数。 // 可用量 = quantity - locked_qty。出库/盘点都按"数量"增减。 // - manage_mode = 2 电气件(SN 管理): // 一条记录 = 一个单件(SN),quantity 恒为 1,locked_qty 为 0 或 1(锁定时=1)。 // 生命周期用 status 字段跟踪:在库/锁定/出库/使用/报废。 // 可用量 = quantity - locked_qty(锁定时为 0)。 // // 库位四级贯通(U18):zone_code(区域) + shelf_no(货架) + layer_no(层) + position_no(位置),均手填、不做编号规则校验, // 贯穿入库/出库/库存明细/盘点/导出全链路;精确货位串由 locLabel 拼接(如 Z01/A架/3层/12位)。 // 统一收编(2026-09-07 定稿):半成品/成品也进本表(category 区分品类),废除独立 semi_finisheds 孤表; // 成品由 MES 完工发码后经内部接口自动回流入库,成品 SN 与实物码解耦(系统必发号,实物可不贴码,允许手填覆盖)。 type Inventory struct { ent.Schema } // 品类(category):1=原材料 2=半成品 3=成品。与 manage_mode 正交: // manage_mode 管"怎么管"(批次数量 vs SN 单件),category 管"是什么"(原料/半成品/成品)。 const ( InvCategoryRaw = 1 InvCategorySemi = 2 InvCategoryFinished = 3 ) func (Inventory) Fields() []ent.Field { return []ent.Field{ // 管理粒度:1=结构件(批次) 2=电气件(SN)。决定本行按"数量"还是按"单件"解释。 field.Int("manage_mode").Comment("管理粒度 1=结构件(批次) 2=电气件(SN)"), // 品类:1=原材料 2=半成品 3=成品(统一库存主表收编 semi_finished 后新增) field.Int("category").Default(InvCategoryRaw).Comment("品类 1=原材料 2=半成品 3=成品"), // 物料编码,关联 materials 主数据。 field.String("material_code").Comment("物料编码(关联 materials 表)"), // 物料名称冗余存储,查询时无需再 join 主数据。 field.String("material_name").Optional().Comment("物料名称(冗余,便于直接展示)"), // 批次号:结构件必填(系统生成);电气件可填所属批次用于分组追溯。 field.String("batch_no").Optional().Comment("批次号(系统生成);电气件可关联所属批次"), // 序列号:仅电气件填写,作为单件唯一追溯锚点。全局唯一(一物一码,P2-2): // 代码层跨 manage_mode 查重 + DB 部分唯一索引 ux_inventories_sn_code(非空值)双重保证;空值表示非 SN 行。 field.String("sn_code").Optional().Comment("序列号(SN,仅电气件);全局唯一,空值表示非 SN 行"), // 数量:结构件=批次余量;电气件恒为 1。 field.Int("quantity").Default(0).Comment("数量:结构件=批次余量;电气件=1"), // 已锁定数量:结构件=锁定件数;电气件=0或1。可用量 = quantity - locked_qty。 field.Int("locked_qty").Default(0).Comment("已锁定数量(库存占用):结构件=件数;电气件=0/1"), // 检验状态,由质量检验模块翻转。 field.String("quality_status").Default("未检").Comment("检验状态 未检/合格/不合格"), // 所在区域(大分区)。 field.String("zone_code").Optional().Comment("所在区域(Z01~Z04 大分区)"), // 库位四级贯通(客户诉求 问题记录 L32/L169 + D5):区域→货架号→第几层→位置号。 // 与 zones 表 shelf_no/layer_no/position_no 对齐,落位到具体货架/层/位置。 field.String("shelf_no").Optional().Comment("货架号(手填,如 A)"), field.String("layer_no").Optional().Comment("第几层(手填,如 3)"), field.String("position_no").Optional().Comment("位置号(手填,如 12)"), // 生命周期状态: // 结构件一律"在库"(锁定看 locked_qty,不靠本字段); // 电气件随业务流转:在库/锁定/出库/使用/报废。 field.String("status").Default("在库").Comment("生命周期:在库/锁定/出库/使用/报废(电气件用;结构件恒在库)"), // 生产日期(结构件批次用)。 field.String("production_date").Optional().Comment("生产日期"), // 供应商(结构件批次用)。 field.String("supplier").Optional().Comment("供应商"), // 关联入库单号,便于正向追溯。 field.String("inbound_no").Optional().Comment("关联入库单号"), // 关联工单号(半成品流转/成品完工回流时由 MES 传入)。 field.String("order_no").Default("").MaxLen(64).Comment("关联工单号(半成品/成品回流)"), // 已完成工序(半成品暂存回流用,如 135)。 field.String("completed_process").Default("").MaxLen(64).Comment("已完成工序(半成品用,如 135)"), // 半成品/成品入库扩展字段(客户诉求 问题记录 L218/L222/L230:归属/产品状态/相关标准/试验记录单号/记录员/记录日期) field.String("ownership_type").Optional().Comment("归属类型 workorder/project/other"), field.String("ownership_no").Optional().Comment("归属编号(工单号/科技项目号/其他)"), field.String("product_status").Optional().Comment("产品状态 已通过/部分通过/返修(成品用)"), field.String("related_standard").Optional().Comment("相关标准(试验方案号)"), field.String("test_record_no").Optional().Comment("试验记录单号"), field.String("recorded_by").Optional().Comment("记录员"), field.Int64("recorded_at").Optional().Comment("记录日期(unix 秒)"), // 半成品/成品出库原因(客户诉求 L224:包装/继续生产/检验试验/其他) field.String("outbound_reason").Optional().Comment("出库原因(半成品/成品出库用)"), // 盘点冻结标记(盘点15问 问题2 最严重):非空=本行正被该盘点单锁定,盘点期间禁止出库/入库/锁定, // 保证「库存量/锁定量/可用量」在盘点期间冻结,差异只反映盘盈盘亏而非业务变动。发起时置盘点单号,完成/取消时清空。 field.String("stocktake_no").Default("").MaxLen(64).Comment("盘点冻结标记(进行中盘点单号;空=未冻结)"), // 备注。 field.String("remark").Optional().Comment("备注"), field.Int64("created_at").DefaultFunc(nowUnix), field.Int64("updated_at").DefaultFunc(nowUnix), } } func (Inventory) Indexes() []ent.Index { return []ent.Index{ index.Fields("manage_mode"), index.Fields("category"), index.Fields("material_code"), index.Fields("quality_status"), index.Fields("zone_code"), index.Fields("batch_no"), index.Fields("sn_code"), } }