Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions finance/v1/cost_master_lookup.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions ppc/v1/common.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
75 changes: 75 additions & 0 deletions ppc/v1/plan_item.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
25 changes: 25 additions & 0 deletions ppc/v1/ppc_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
89 changes: 89 additions & 0 deletions ppc/v1/work_order.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Loading