diff --git a/finance/v1/cost_master_lookup.proto b/finance/v1/cost_master_lookup.proto index fdfd69e..5f574d2 100644 --- a/finance/v1/cost_master_lookup.proto +++ b/finance/v1/cost_master_lookup.proto @@ -131,6 +131,14 @@ message CostMasterRouteRm { string rm_group_code = 6; string route_rm_ratio = 7; // decimal-as-string string sub_type = 8; + // Resolved human-readable identity of the RM edge, so consumers never have to + // render a raw id. Both are derived server-side by rm_type: + // PRODUCT -> cost_product_master (product_code / product_name) + // ITEM -> cost_erp_item (item_code / item_name) + // GROUP -> cst_rm_group_head (group_code / group_name) + // Empty when the referenced master row is missing. + string rm_code = 9; + string rm_name = 10; } // CostMasterRouteStage is a lightweight route stage projection. diff --git a/ppc/v1/common.proto b/ppc/v1/common.proto index 966aa01..ca986c6 100644 --- a/ppc/v1/common.proto +++ b/ppc/v1/common.proto @@ -70,6 +70,20 @@ enum CarryAction { CARRY_ACTION_CANCEL = 5; // Close the demand. } +// PlanCarryAction is the per-plan-item decision in the start-new-month +// workflow. Deliberately NARROWER than CarryAction rather than a reuse of it: +// the plan-item lifecycle (DRAFT/CONFIRMED/IN_PROGRESS/COMPLETED/CLOSED) has no +// DEFERRED state, so CARRY_ACTION_DEFER has nothing to map onto, and SPLIT would +// need per-child machine groups and timelines that a demand split never carries. +// Stretching CarryAction to cover plan items would make two of its five values +// silently unimplementable. +enum PlanCarryAction { + PLAN_CARRY_ACTION_UNSPECIFIED = 0; // Default / none. + PLAN_CARRY_ACTION_CARRY_AS_IS = 1; // New plan item for the whole unplanned qty. + PLAN_CARRY_ACTION_PARTIAL_CARRY = 2; // Carry part; the rest is not carried. + PLAN_CARRY_ACTION_CANCEL = 3; // Close the plan item without carrying it. +} + // GradeReq expresses the grade requirement attached to a demand. enum GradeReq { GRADE_REQ_UNSPECIFIED = 0; // Default / no-filter. diff --git a/ppc/v1/plan_item.proto b/ppc/v1/plan_item.proto index cbe7d5f..7f9dc28 100644 --- a/ppc/v1/plan_item.proto +++ b/ppc/v1/plan_item.proto @@ -38,6 +38,81 @@ message PlanItem { // levels, which is what makes those items mergeable into one work order. string shade_code = 21; string shade_name = 22; + // Source plan item this one was carried forward from, 0 when it was not. + // A new row per carry (never a month reassignment) is what keeps the source + // month's plan an accurate record of what was committed there. + int64 carry_from_item_id = 23; + // The action that produced this item, when it came from a carry-forward. + PlanCarryAction carry_action = 24; +} + +// ── Carry-forward (month start) ────────────────────────────────────────────── +// +// Modelled on the demand pair (demand.proto) so the vocabulary stays the same, +// but with the narrower PlanCarryAction — see ppc/v1/common.proto for why. + +// PlanCarryCandidate is one plan item eligible to be carried into a new month, +// decorated with everything the planner needs to decide without seeing an id. +message PlanCarryCandidate { + PlanItem item = 1; + // Qty on this item not yet committed to any work order: + // qty_target - SUM(wo_plan_item_link.qty_contribution). This is what + // CARRY_AS_IS carries, so a plan item already half-covered by a work order + // does not get carried twice. + string qty_uncovered = 2; + // Qty already covered by work orders, for context alongside qty_uncovered. + string qty_covered = 3; + // How many work orders reference this plan item. Non-zero means the work is + // already in flight and the work-order carry scope may also apply to it. + int32 work_order_count = 4; + // True when a plan item in the target month already names this one as its + // carry source — a second run shows it as done rather than duplicating it. + bool already_carried = 5; + // Human label of the demand this item serves, for traceability. Empty for a + // cascade INTERMEDIATE item, which serves a parent item rather than a demand. + string demand_label = 6; +} + +message ListPlanCarryForwardCandidatesRequest { + string source_month = 1 [(buf.validate.field).string = { + len: 7 + pattern: "^[0-9]{4}-[0-9]{2}$" + }]; + // Target month the candidates are being carried into. Required: whether a + // candidate is `already_carried` can only be answered against a target. + string target_month = 2 [(buf.validate.field).string = { + len: 7 + pattern: "^[0-9]{4}-[0-9]{2}$" + }]; +} + +message ListPlanCarryForwardCandidatesResponse { + common.v1.BaseResponse base = 1; + repeated PlanCarryCandidate data = 2; +} + +message ProcessPlanCarryForwardRequest { + int64 source_plan_item_id = 1 [(buf.validate.field).int64.gt = 0]; + PlanCarryAction action = 2 [(buf.validate.field).enum = { + not_in: [0] + }]; + string target_month = 3 [(buf.validate.field).string = { + len: 7 + pattern: "^[0-9]{4}-[0-9]{2}$" + }]; + // New deadline for CARRY_AS_IS / PARTIAL_CARRY. Defaults to the source + // deadline when omitted. + optional string new_deadline = 4 [(buf.validate.field).string.max_len = 10]; + // Carried qty for PARTIAL_CARRY. Must not exceed the candidate's + // qty_uncovered. + optional string carry_qty = 5 [(buf.validate.field).string.max_len = 20]; +} + +message ProcessPlanCarryForwardResponse { + common.v1.BaseResponse base = 1; + // The plan item created in the target month. Absent for CANCEL, which + // creates nothing. + PlanItem data = 2; } message CreatePlanItemRequest { diff --git a/ppc/v1/ppc_service.proto b/ppc/v1/ppc_service.proto index dd215bd..ba98b0c 100644 --- a/ppc/v1/ppc_service.proto +++ b/ppc/v1/ppc_service.proto @@ -442,6 +442,31 @@ service PPCService { rpc GetGanttView(GetGanttViewRequest) returns (GetGanttViewResponse) { option (google.api.http) = {get: "/api/v1/ppc/plan-items/gantt"}; } + // ListPlanCarryForwardCandidates lists plan items eligible for carry-forward. + rpc ListPlanCarryForwardCandidates(ListPlanCarryForwardCandidatesRequest) returns (ListPlanCarryForwardCandidatesResponse) { + option (google.api.http) = {get: "/api/v1/ppc/plan-items/carry-forward-candidates"}; + } + // ProcessPlanCarryForward executes a per-plan-item carry-forward action. + rpc ProcessPlanCarryForward(ProcessPlanCarryForwardRequest) returns (ProcessPlanCarryForwardResponse) { + option (google.api.http) = { + post: "/api/v1/ppc/plan-items/{source_plan_item_id}/carry-forward" + body: "*" + }; + } + + // ── Work Order Carry-forward (month start) ────────────────────────────────── + // ListWorkOrderCarryForwardCandidates lists WOs eligible to carry into a new + // month. Ineligible WOs are included with their reason, never silently filtered. + rpc ListWorkOrderCarryForwardCandidates(ListWorkOrderCarryForwardCandidatesRequest) returns (ListWorkOrderCarryForwardCandidatesResponse) { + option (google.api.http) = {get: "/api/v1/ppc/work-orders/carry-forward-candidates"}; + } + // ProcessWorkOrderCarryForward carries one WO into a new month as a CONTINUATION. + rpc ProcessWorkOrderCarryForward(ProcessWorkOrderCarryForwardRequest) returns (ProcessWorkOrderCarryForwardResponse) { + option (google.api.http) = { + post: "/api/v1/ppc/work-orders/{source_wo_id}/carry-forward" + body: "*" + }; + } // ── Layer 3: Work Order ──────────────────────────────────────────────────── // CreateWorkOrder creates a work order. diff --git a/ppc/v1/work_order.proto b/ppc/v1/work_order.proto index a2eff7e..f09e409 100644 --- a/ppc/v1/work_order.proto +++ b/ppc/v1/work_order.proto @@ -76,6 +76,16 @@ message WORmAllocation { string shade_code = 8; string qty_allocated = 9; // decimal-as-string string notes = 10; + + // Presentation-only decoration resolved from the product's released route + // (finance CostMasterRouteRm). crm_rm_id stays the wire identity; these exist + // so no consumer ever has to render that raw id to a user. Empty when the + // route is unavailable or the edge is no longer part of it. + string rm_code = 11; // item / group / product code of the RM edge + string rm_name = 12; // display name of the RM edge + string route_stage_name = 13; // owning route stage (attribution) + int32 route_level = 14; // owning route stage level + string route_rm_ratio = 15; // decimal-as-string, ratio the qty suggestion came from } // WOProductionActual is the 1:N per date+shift production actual (two-axis @@ -661,3 +671,82 @@ message ListMergeCandidatesResponse { common.v1.BaseResponse base = 1; repeated PlanItem data = 2; } + +// ── Carry-forward (month start) ────────────────────────────────────────────── +// Modelled on the demand and plan-item carry pairs so the vocabulary stays the +// same. A carried WO is a CONTINUATION — the existing primitive, not a new +// concept — with its own lot and inherited demand/product/machine/parameters. +// Ineligible WOs are listed with a reason, never silently filtered (S-2.3). + +// WorkOrderCarryCandidate is one WO that may be carried into a new month, +// decorated with everything the planner needs to decide without seeing an id. +message WorkOrderCarryCandidate { + WorkOrder wo = 1; + // Qty not yet produced: QtyTarget − SUM(production actual, actual first then + // bobbin per row). Non-positive means fully produced — ineligible with reason. + string remaining_qty = 2; + // Human label for the machine this WO runs on. + string machine_label = 3; + // Human label for the product this WO produces. + string product_label = 4; + // Ineligibility reason, empty when the WO is eligible to carry. + string ineligibility_reason = 5; + // True when a WO in the target month already names this one as its carry + // source — a second run over the same source shows it as done. + bool already_carried = 6; +} + +// Which WOs can be carried into a target month out of a source month. +message ListWorkOrderCarryForwardCandidatesRequest { + string source_month = 1 [(buf.validate.field).string = { + len: 7 + pattern: "^[0-9]{4}-[0-9]{2}$" + }]; + string target_month = 2 [(buf.validate.field).string = { + len: 7 + pattern: "^[0-9]{4}-[0-9]{2}$" + }]; +} + +message ListWorkOrderCarryForwardCandidatesResponse { + common.v1.BaseResponse base = 1; + repeated WorkOrderCarryCandidate data = 2; +} + +// One WO carry action. The action vocabulary is narrower than demand carry +// because WOs are production instructions, not quantity balances: a carried WO +// is always a continuation, and rejecting it to close-reroute is a manual +// planner decision on the source WO's own detail page. +message ProcessWorkOrderCarryForwardRequest { + int64 source_wo_id = 1 [(buf.validate.field).int64.gt = 0]; + string target_month = 2 [(buf.validate.field).string = { + len: 7 + pattern: "^[0-9]{4}-[0-9]{2}$" + }]; + // Lot number for the continuation. Blank means auto-generate via the + // lot-provisioning path, exactly like creating a WO from scratch. An explicit + // value must already exist in lot_master (validateLot). + // + // Bound matches work_order.wo_lot_no VARCHAR(30), as every other lot_no field + // in this file does. At 50 a 31..50-char lot cleared validation and then failed + // at INSERT as a raw driver error, which reaches the planner as a 500 instead + // of a field-level complaint. Generated lots are 10 chars, so nothing real is + // excluded by the tighter bound. + string lot_no = 3 [(buf.validate.field).string.max_len = 30]; + // Qty to carry. When unset or zero, the whole remaining qty is carried. + // + // The pattern admits the empty string: full-remainder is the ordinary case and + // the BFF sends "" for it. The previous rule had no empty alternative, so the + // documented unset path was unreachable through the RPC — every carry that + // left the qty blank was refused at the validation boundary before the handler + // (which already checks `req.CarryQty != ""`) could apply the default. + string carry_qty = 4 [(buf.validate.field).string = { + max_len: 20 + pattern: "^([0-9]+(\\.[0-9]+)?)?$" + }]; +} + +message ProcessWorkOrderCarryForwardResponse { + common.v1.BaseResponse base = 1; + WorkOrder data = 2; +}