29 KiB
Delivery Entry Unification Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Make 嘉恒仓库 -> 成品库 -> 销售出库 the only place that creates delivery outbound records, while 发货与售后 -> 发货管理 becomes a read-only delivery ledger with traceability.
Architecture: Keep the existing /sales/deliveries API as the single write path for delivery creation. Extend the warehouse sales-out drawer to support both sales-order delivery and direct customer delivery, and remove the create path from the delivery management page. Preserve traceability through existing DeliveryItem.lot_id -> StockLot.source_material_lot_id/source_material_sub_batch_no/source_material_summary fields.
Tech Stack: FastAPI + SQLAlchemy backend, Vue 3 SFC frontend, Node static regression scripts, pytest backend tests, Vite build.
File Structure
- Modify:
backend/app/api/routes/sales.py- Enforce that delivery creation uses a
FINISHEDwarehouse. - Keep support for direct customer delivery with
sales_order_id = nullandsales_order_item_id = null.
- Enforce that delivery creation uses a
- Modify:
backend/tests/test_sales_order_delivery_trace.py- Add regression coverage for direct customer delivery and non-finished warehouse rejection.
- Create:
frontend/scripts/test-delivery-entry-unification.mjs- Static regression checks for the unified entry contract.
- Modify:
frontend/src/views/InventoryLedgerView.vue- Add delivery mode selection to the成品库销售出库 drawer.
- Allow direct customer delivery.
- Show source material trace fields in the pre-submit allocation table.
- Modify:
frontend/src/views/DeliveryManagementView.vue- Remove create delivery UI and write-side script.
- Rename page to发货台账 and keep query/detail trace UI only.
- No DB schema changes expected.
Task 1: Backend Delivery Contract
Files:
-
Modify:
backend/tests/test_sales_order_delivery_trace.py -
Modify:
backend/app/api/routes/sales.py -
Step 1: Add failing backend tests for direct delivery and finished-warehouse validation
Append the following two tests inside SalesOrderDeliveryTraceTest before the if __name__ == "__main__": block in backend/tests/test_sales_order_delivery_trace.py:
def test_direct_customer_delivery_without_sales_order_preserves_trace_and_inventory(self) -> None:
delivery_result = create_delivery(
DeliveryCreate(
customer_id=self.customer.id,
warehouse_id=self.warehouse.id,
consignee_name=self.customer.contact_name,
consignee_phone=self.customer.contact_phone,
delivery_address=self.customer.address,
waybill_no="YD-DIRECT-001",
freight_amount=18,
items=[
DeliveryItemCreate(
sales_order_item_id=None,
product_item_id=self.product.id,
lot_id=self.finished_lot.id,
delivery_qty=3,
delivery_weight_kg=6,
unit_price=0,
)
],
),
context=self._auth_context(),
db=self.db,
)
delivery = self.db.get(Delivery, delivery_result.delivery_id)
delivery_item = self.db.scalar(select(DeliveryItem).where(DeliveryItem.delivery_id == delivery.id))
trace_row = self.db.execute(get_delivery_items_query(limit=10, delivery_id=delivery.id)).mappings().first()
stock_balance = self.db.scalar(select(StockBalance).where(StockBalance.item_id == self.product.id))
sales_out_txn = self.db.scalar(select(InventoryTxn).where(InventoryTxn.txn_type == "SALES_OUT"))
self.assertIsNone(delivery.sales_order_id)
self.assertEqual(delivery.customer_id, self.customer.id)
self.assertIsNone(delivery_item.sales_order_item_id)
self.assertEqual(trace_row["source_material_lot_no"], "RML-001")
self.assertEqual(trace_row["source_material_code"], "MAT-001")
self.assertEqual(trace_row["source_material_name"], "冷轧钢板")
self.assertEqual(trace_row["source_material_sub_batch_no"], "RML-001")
self.assertEqual(trace_row["source_material_summary"], "RML-001 / 冷轧钢板 / 40kg / ¥200")
self.assertEqual(float(stock_balance.qty_on_hand), 17)
self.assertEqual(float(stock_balance.qty_available), 17)
self.assertEqual(sales_out_txn.source_doc_type, "DELIVERY")
self.assertEqual(sales_out_txn.source_doc_id, delivery.id)
def test_sales_delivery_rejects_non_finished_warehouse(self) -> None:
raw_warehouse = Warehouse(
id=2,
warehouse_code="WH-RAW",
warehouse_name="原材料库",
warehouse_type="RAW",
status="ACTIVE",
)
self.db.add(raw_warehouse)
self.db.commit()
with self.assertRaises(HTTPException) as exc:
create_delivery(
DeliveryCreate(
customer_id=self.customer.id,
warehouse_id=raw_warehouse.id,
waybill_no="YD-WRONG-WH",
freight_amount=12,
items=[
DeliveryItemCreate(
product_item_id=self.product.id,
lot_id=self.finished_lot.id,
delivery_qty=1,
delivery_weight_kg=2,
unit_price=0,
)
],
),
context=self._auth_context(),
db=self.db,
)
self.assertEqual(exc.exception.status_code, 400)
self.assertIn("销售出库必须选择成品库", exc.exception.detail)
- Step 2: Run backend test and verify the expected failure
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/backend
.venv/bin/python -m pytest tests/test_sales_order_delivery_trace.py -q
Expected before implementation:
FAILED backend/tests/test_sales_order_delivery_trace.py::SalesOrderDeliveryTraceTest::test_sales_delivery_rejects_non_finished_warehouse
The direct customer delivery test may already pass because the backend mostly supports it. The warehouse-type test must fail until validation is added.
- Step 3: Add explicit finished warehouse validation
In backend/app/api/routes/sales.py, inside create_delivery, immediately after:
warehouse = db.get(Warehouse, payload.warehouse_id)
if not warehouse:
raise HTTPException(status_code=404, detail="仓库不存在")
add:
if str(warehouse.warehouse_type or "").upper() != "FINISHED":
raise HTTPException(status_code=400, detail="销售出库必须选择成品库")
The surrounding code should become:
warehouse = db.get(Warehouse, payload.warehouse_id)
if not warehouse:
raise HTTPException(status_code=404, detail="仓库不存在")
if str(warehouse.warehouse_type or "").upper() != "FINISHED":
raise HTTPException(status_code=400, detail="销售出库必须选择成品库")
ensure_warehouses_unlocked(db, [payload.warehouse_id], "销售出库")
- Step 4: Run backend test and verify it passes
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/backend
.venv/bin/python -m pytest tests/test_sales_order_delivery_trace.py -q
Expected:
passed
- Step 5: Commit backend contract changes if git is available
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP
git rev-parse --is-inside-work-tree
If the command prints true, run:
git add backend/app/api/routes/sales.py backend/tests/test_sales_order_delivery_trace.py
git commit -m "test: protect unified delivery creation contract"
If it exits nonzero because this checkout is not a git repository, record that and continue without committing.
Task 2: Frontend Regression Guard
Files:
-
Create:
frontend/scripts/test-delivery-entry-unification.mjs -
Step 1: Write the failing static regression script
Create frontend/scripts/test-delivery-entry-unification.mjs with this content:
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import path from "node:path";
const root = process.cwd();
function readSource(relativePath) {
return readFileSync(path.join(root, relativePath), "utf8");
}
const inventory = readSource("src/views/InventoryLedgerView.vue");
const delivery = readSource("src/views/DeliveryManagementView.vue");
assert.match(inventory, /delivery_mode:\s*"SALES_ORDER"/, "sales-out form should default to sales-order delivery mode");
assert.match(inventory, /直接客户发货/, "warehouse sales-out drawer should support direct customer delivery");
assert.match(inventory, /销售订单发货/, "warehouse sales-out drawer should keep sales-order delivery mode");
assert.match(inventory, /isSalesOrderDelivery/, "sales-out drawer should centralize mode-dependent behavior");
assert.match(inventory, /setDeliveryMode\("DIRECT_CUSTOMER"\)/, "direct customer mode should be selectable");
assert.match(inventory, /deliveryForm\.delivery_mode === "SALES_ORDER"/, "sales order fields should be conditional");
assert.match(inventory, /source_material_summary/, "pre-submit allocation rows should expose source material summary");
assert.match(inventory, />来源材料摘要</, "pre-submit allocation table should show source material summary column");
assert.match(inventory, />成品库存批次号</, "pre-submit allocation table should show finished stock lot column");
assert.match(inventory, />来源原材料库存批次号</, "pre-submit allocation table should show source raw-material lot column");
assert.match(inventory, /sales_order_id:\s*isSalesOrderDelivery\.value \? Number\(deliveryForm\.sales_order_id\) : null/, "direct customer delivery should send null sales_order_id");
assert.match(inventory, /sales_order_item_id:\s*isSalesOrderDelivery\.value \? Number\(deliveryForm\.sales_order_item_id\) : null/, "direct customer delivery lines should send null sales_order_item_id");
assert.match(delivery, /发货台账/, "delivery management should be renamed to delivery ledger");
assert.match(delivery, /交付查询/, "delivery ledger should describe query/trace use");
assert.match(delivery, />销售订单号</, "delivery ledger should show sales order number");
assert.match(delivery, /source_material_summary/, "delivery ledger detail should keep material trace summary");
assert.doesNotMatch(delivery, /新增发货/, "delivery ledger must not expose create-delivery button");
assert.doesNotMatch(delivery, /openCreateDrawer/, "delivery ledger must not keep create drawer action");
assert.doesNotMatch(delivery, /submitDelivery/, "delivery ledger must not keep create submit handler");
assert.doesNotMatch(delivery, /postResource/, "delivery ledger must not import write API helpers");
assert.doesNotMatch(delivery, /uploadResource/, "delivery ledger must not upload logistics photos");
assert.doesNotMatch(delivery, /title="新增发货"/, "delivery ledger must not contain create drawer");
console.log("delivery entry unification checks passed");
- Step 2: Run static script and verify it fails
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
node scripts/test-delivery-entry-unification.mjs
Expected before implementation:
AssertionError
The failure should point to missing direct-customer mode in InventoryLedgerView.vue or create UI still existing in DeliveryManagementView.vue.
Task 3: Warehouse Sales-Out Drawer Supports Both Delivery Modes
Files:
-
Modify:
frontend/src/views/InventoryLedgerView.vue -
Test:
frontend/scripts/test-delivery-entry-unification.mjs -
Step 1: Add delivery mode state and mode helpers
In buildEmptyDeliveryForm() add delivery_mode:
function buildEmptyDeliveryForm() {
return {
delivery_mode: "SALES_ORDER",
sales_order_id: "",
sales_order_item_id: "",
product_item_id: "",
warehouse_id: "",
customer_id: "",
shipper_employee_id: "",
consignee_name: "",
consignee_phone: "",
delivery_address: "",
delivery_qty: 0,
waybill_no: "",
order_photo_url: "",
freight_amount: ""
};
}
Near the existing selectedSalesOrder computed, add:
const isSalesOrderDelivery = computed(() => deliveryForm.delivery_mode === "SALES_ORDER");
const isDirectCustomerDelivery = computed(() => deliveryForm.delivery_mode === "DIRECT_CUSTOMER");
Update selectableSalesOrderItems to return no rows in direct mode:
const selectableSalesOrderItems = computed(() => {
if (!isSalesOrderDelivery.value) {
return [];
}
return salesOrderItems.value.filter(
(item) => Number(item.sales_order_id) === Number(deliveryForm.sales_order_id) && orderItemPendingQty(item) > 0
);
});
Add this helper near resetDeliveryForm():
function setDeliveryMode(mode) {
deliveryForm.delivery_mode = mode === "DIRECT_CUSTOMER" ? "DIRECT_CUSTOMER" : "SALES_ORDER";
deliveryForm.sales_order_id = "";
deliveryForm.sales_order_item_id = "";
deliveryForm.product_item_id = "";
deliveryForm.customer_id = "";
deliveryForm.consignee_name = "";
deliveryForm.consignee_phone = "";
deliveryForm.delivery_address = "";
deliveryForm.delivery_qty = 0;
}
- Step 2: Make sales-order defaults no-op in direct mode
Replace applySalesOrderDefaults() with:
function applySalesOrderDefaults() {
deliveryForm.sales_order_item_id = "";
if (!isSalesOrderDelivery.value) {
return;
}
const order = selectedSalesOrder.value;
if (!order) {
return;
}
deliveryForm.customer_id = order.customer_id;
deliveryForm.delivery_address = order.delivery_address || "";
const customer = customers.value.find((item) => item.id === Number(order.customer_id));
deliveryForm.consignee_name = customer?.contact_name || "";
deliveryForm.consignee_phone = customer?.contact_phone || "";
deliveryForm.delivery_address = order.delivery_address || customer?.address || "";
const firstOpenItem = selectableSalesOrderItems.value[0];
if (firstOpenItem) {
deliveryForm.sales_order_item_id = firstOpenItem.sales_order_item_id;
applySalesOrderItemDefaults();
}
}
Replace applySalesOrderItemDefaults() with:
function applySalesOrderItemDefaults() {
const item = selectedSalesOrderItem.value;
if (!item || !isSalesOrderDelivery.value) {
return;
}
deliveryForm.product_item_id = item.product_item_id;
deliveryForm.delivery_qty = orderItemPendingQty(item);
}
Add a customer default helper if it does not already exist in this file:
function applyDeliveryCustomerDefaults() {
if (isSalesOrderDelivery.value) {
return;
}
const customer = customers.value.find((item) => item.id === Number(deliveryForm.customer_id));
deliveryForm.consignee_name = customer?.contact_name || "";
deliveryForm.consignee_phone = customer?.contact_phone || "";
deliveryForm.delivery_address = customer?.address || "";
}
- Step 3: Update sales-out drawer template
Inside the FormDrawer block whose open prop is deliveryDrawerOpen, replace the first sales-order field group with this mode section and conditional sales-order fields:
<div class="purchase-choice-grid">
<button
class="purchase-choice-card mode-choice"
:class="{ active: isSalesOrderDelivery }"
type="button"
title="选择已有销售订单,发货后回写订单已发数量。"
@click="setDeliveryMode('SALES_ORDER')"
>
<span class="choice-check"></span>
<span class="choice-main">
<strong>销售订单发货</strong>
</span>
</button>
<button
class="purchase-choice-card mode-choice"
:class="{ active: isDirectCustomerDelivery }"
type="button"
title="不绑定销售订单,直接按客户和产品从成品库出库。"
@click="setDeliveryMode('DIRECT_CUSTOMER')"
>
<span class="choice-check"></span>
<span class="choice-main">
<strong>直接客户发货</strong>
</span>
</button>
</div>
<div v-if="deliveryForm.delivery_mode === 'SALES_ORDER'" class="double-field">
<label class="form-field">
<span>关联销售订单</span>
<select v-model.number="deliveryForm.sales_order_id" required @change="applySalesOrderDefaults">
<option disabled value="">请选择销售订单</option>
<option v-for="order in openSalesOrders" :key="order.order_id" :value="order.order_id">
{{ order.order_no }} · {{ order.customer_name }} · 未发 {{ formatQty(orderPendingQty(order.order_id)) }}
</option>
</select>
</label>
<label class="form-field">
<span>销售订单明细</span>
<select v-model.number="deliveryForm.sales_order_item_id" required @change="applySalesOrderItemDefaults">
<option disabled value="">请选择订单明细</option>
<option v-for="item in selectableSalesOrderItems" :key="item.sales_order_item_id" :value="item.sales_order_item_id">
{{ item.product_code }} · {{ item.product_name }} · 未发 {{ formatQty(orderItemPendingQty(item)) }}
</option>
</select>
</label>
</div>
In the product/customer area:
- Change the product select to
:disabled="isSalesOrderDelivery && Boolean(deliveryForm.sales_order_item_id)". - Change the customer select to
:disabled="isSalesOrderDelivery"and add@change="applyDeliveryCustomerDefaults". - Change the quantity input max to
:max="isSalesOrderDelivery && selectedSalesOrderItem ? orderItemPendingQty(selectedSalesOrderItem) : undefined".
The relevant fields should look like:
<select v-model.number="deliveryForm.product_item_id" required :disabled="isSalesOrderDelivery && Boolean(deliveryForm.sales_order_item_id)">
<input
v-model.number="deliveryForm.delivery_qty"
type="number"
min="1"
step="1"
required
:max="isSalesOrderDelivery && selectedSalesOrderItem ? orderItemPendingQty(selectedSalesOrderItem) : undefined"
/>
<select v-model.number="deliveryForm.customer_id" required :disabled="isSalesOrderDelivery" @change="applyDeliveryCustomerDefaults">
- Step 4: Show trace fields before submit
In the automatic sales-out detail table, replace the headers and rows with:
<tr>
<th>成品库存批次号</th>
<th>来源原材料库存批次号</th>
<th>来源材料摘要</th>
<th>可发数量</th>
<th>本次发货</th>
<th>重量</th>
</tr>
<tr v-for="item in allocationRows" :key="item.lot_id">
<td>{{ item.lot_no }}</td>
<td>{{ item.source_material_sub_batch_no || item.source_material_lot_no || "-" }}</td>
<td :title="item.source_material_summary || ''">{{ item.source_material_summary || "-" }}</td>
<td>{{ formatQty(item.remaining_qty) }}</td>
<td>{{ formatQty(item.delivery_qty) }}</td>
<td>{{ formatWeight(item.delivery_weight_kg) }}</td>
</tr>
<tr v-if="!allocationRows.length">
<td colspan="6" class="empty-row">{{ isSalesOrderDelivery ? "选择销售订单和发货数量后显示拆分明细" : "选择客户、产品和发货数量后显示拆分明细" }}</td>
</tr>
- Step 5: Submit null sales-order fields for direct customer delivery
In submitSalesOut(), before calling postResource, add explicit validations:
if (isSalesOrderDelivery.value && !Number(deliveryForm.sales_order_id)) {
errorMessage.value = "请选择销售订单";
return;
}
if (isSalesOrderDelivery.value && !Number(deliveryForm.sales_order_item_id)) {
errorMessage.value = "请选择销售订单明细";
return;
}
if (!Number(deliveryForm.customer_id)) {
errorMessage.value = "请选择收货客户";
return;
}
if (!Number(deliveryForm.product_item_id)) {
errorMessage.value = "请选择发货产品";
return;
}
Inside the postResource("/sales/deliveries", payload) call, replace the payload sales-order lines with:
sales_order_id: isSalesOrderDelivery.value ? Number(deliveryForm.sales_order_id) : null,
customer_id: Number(deliveryForm.customer_id),
Replace the items mapping with:
items: allocationRows.value.map((item) => ({
sales_order_item_id: isSalesOrderDelivery.value ? Number(deliveryForm.sales_order_item_id) : null,
product_item_id: Number(deliveryForm.product_item_id),
lot_id: Number(item.lot_id),
delivery_qty: Number(item.delivery_qty || 0),
delivery_weight_kg: Number(item.delivery_weight_kg || 0),
unit_price: isSalesOrderDelivery.value ? Number(selectedSalesOrderItem.value?.unit_price || 0) : 0
}))
- Step 6: Run frontend static test
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
node scripts/test-delivery-entry-unification.mjs
Expected after Task 3 only:
AssertionError
The remaining failure should be about DeliveryManagementView.vue still exposing create delivery UI.
Task 4: Convert Delivery Management To Read-Only Ledger
Files:
-
Modify:
frontend/src/views/DeliveryManagementView.vue -
Test:
frontend/scripts/test-delivery-entry-unification.mjs -
Step 1: Remove create button and rename page
In frontend/src/views/DeliveryManagementView.vue, update the panel header from:
<p class="eyebrow">发货列表</p>
<h3>销售订单履约发货与直接发货</h3>
to:
<p class="eyebrow">发货台账</p>
<h3>交付查询、物流信息与批次追溯</h3>
Replace the right header actions:
<div class="workflow-inline-actions">
<span class="panel-tag">成品出库</span>
<button class="primary-button" type="button" @click="openCreateDrawer">新增发货</button>
</div>
with:
<div class="workflow-inline-actions">
<span class="panel-tag">只读台账</span>
</div>
Add a sales order number column after 发货单号:
<th>销售订单号</th>
and row cell:
<td>{{ delivery.order_no || "直接客户发货" }}</td>
Update the empty row colspan from 11 to 12.
- Step 2: Delete the create drawer template
Remove the entire create drawer block that starts with:
<FormDrawer
:open="drawerOpen"
eyebrow="发货管理"
title="新增发货"
through its matching:
</FormDrawer>
Do not remove the detail drawer that starts with:
<FormDrawer
:open="detailDrawerOpen"
eyebrow="发货明细"
The detail table must keep these cells:
<td>{{ item.product_name }}</td>
<td :title="item.source_material_summary || ''">{{ formatSourceMaterial(item) }}</td>
<td>{{ item.source_material_lot_no || "-" }}</td>
<td>{{ item.source_material_sub_batch_no || "-" }}</td>
<td>{{ item.lot_no || "-" }}</td>
- Step 3: Remove write-side imports, state, computed values, and functions
Change imports from:
import FormDrawer from "../components/FormDrawer.vue";
import { fetchResource, openResource, postResource, uploadResource } from "../services/api";
to:
import FormDrawer from "../components/FormDrawer.vue";
import { fetchResource, openResource } from "../services/api";
Remove these refs/state:
const salesOrders = ref([]);
const salesOrderItems = ref([]);
const warehouses = ref([]);
const employees = ref([]);
const lots = ref([]);
const submitting = ref(false);
const logisticsPhotoUploading = ref(false);
const drawerOpen = ref(false);
const form = reactive(buildEmptyForm());
Remove the computed declarations named:
selectedSalesOrder
selectedSalesOrderItem
openSalesOrders
shippingWarehouses
selectableSalesOrderItems
availableLots
totalAvailableQty
allocationRows
totalAllocatedQty
Remove these functions:
buildEmptyForm
isFreightBlank
validateLogisticsForm
handleLogisticsPhotoUpload
resetForm
normalizeWarehouseType
openCreateDrawer
applyCustomerDefaults
orderItemPendingQty
orderPendingQty
applySalesOrderDefaults
applySalesOrderItemDefaults
submitDelivery
Keep these functions:
joinUniqueNames
formatSourceMaterial
openLogisticsPhoto
formatFreight
openDetailDrawer
loadAll
Update loadAll() to fetch only read-side resources:
async function loadAll() {
const [productRows, customerRows, deliveryRows, deliveryItemRows] = await Promise.all([
fetchResource("/master-data/products?limit=200", []),
fetchResource("/sales/customers?limit=200", []),
fetchResource("/sales/deliveries?limit=500", []),
fetchResource("/sales/delivery-items?limit=500", [])
]);
products.value = productRows;
customers.value = customerRows;
deliveries.value = deliveryRows;
deliveryItems.value = deliveryItemRows;
}
- Step 4: Ensure delivery ledger search includes direct/customer and order number
Update deliveryControls search fields to:
const deliveryControls = useTableControls(deliveryRows, {
searchFields: [
"delivery_no",
"order_no",
"customer_name",
"product_names",
"consignee_name",
"consignee_phone",
"logistics_waybill_no",
"status",
(row) => row.order_no || "直接客户发货"
],
sortOptions: [
{ key: "delivery_no", label: "发货单号" },
{ key: "order_no", label: "销售订单号" },
{ key: "customer_name", label: "客户" },
{ key: "product_names", label: "产品" },
{ key: "delivery_date", label: "发货时间" },
{ key: "total_delivery_qty", label: "发货数量" },
{ key: "status", label: "状态" }
],
defaultSortKey: "delivery_date",
defaultSortDirection: "desc"
});
- Step 5: Run delivery static test and verify it passes
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
node scripts/test-delivery-entry-unification.mjs
Expected:
delivery entry unification checks passed
- Step 6: Run Vite build to catch SFC syntax errors
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
npm run build
Expected:
✓ built
The existing Vite chunk-size warning may appear and is acceptable.
Task 5: End-To-End Verification
Files:
-
No source edits expected.
-
Step 1: Run backend delivery tests
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/backend
.venv/bin/python -m pytest tests/test_sales_order_delivery_trace.py -q
Expected:
passed
- Step 2: Run new and existing frontend static checks
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
node scripts/test-delivery-entry-unification.mjs
node scripts/test-inventory-ledger-transaction-ledger.mjs
node scripts/test-inventory-ledger-return-warehouse.mjs
node scripts/test-inventory-ledger-options.mjs
node scripts/test-inventory-ledger-raw-return.mjs
node scripts/test-stocktake-workflow.mjs
Expected:
delivery entry unification checks passed
Warehouse transaction ledger UI checks passed
Return warehouse UI checks passed
inventory ledger option tests passed
Raw return UI checks passed
stocktake workflow tests passed
- Step 3: Run frontend production build
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
npm run build
Expected:
✓ built
- Step 4: Manual browser smoke test if local services are running
Open the ERP frontend and verify:
嘉恒仓库 -> 成品库 -> 销售出库
Expected UI behavior:
- Sales-out drawer shows
销售订单发货and直接客户发货. - In
销售订单发货, sales order and order item fields are visible and customer/product are derived. - In
直接客户发货, sales order fields are hidden and customer/product can be selected directly. - Allocation table shows
成品库存批次号,来源原材料库存批次号, and来源材料摘要.
Open:
发货与售后 -> 发货管理
Expected UI behavior:
-
Page title is
发货台账. -
No
新增发货button exists. -
Clicking a delivery number opens read-only details.
-
Details include source material and source raw-material stock lot fields.
-
Step 5: Commit frontend changes if git is available
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP
git rev-parse --is-inside-work-tree
If the command prints true, run:
git add \
frontend/scripts/test-delivery-entry-unification.mjs \
frontend/src/views/InventoryLedgerView.vue \
frontend/src/views/DeliveryManagementView.vue
git commit -m "feat: unify delivery creation entry"
If it exits nonzero because this checkout is not a git repository, record that and finish without committing.