docs: add accessory module PRD and implementation plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
750
docs/plans/2026-05-24-accessory-impl.md
Normal file
750
docs/plans/2026-05-24-accessory-impl.md
Normal file
@@ -0,0 +1,750 @@
|
||||
# Accessory Module Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Add an independent accessory registration module to CargoTrace, allowing warehouse workers to manually register, shelve, and box order accessories that arrive without execution cards.
|
||||
|
||||
**Architecture:** New `finished_goods_accessory` table + `accessory_type` preset table in PostgreSQL. New FastAPI service layer (`accessory_service.py`) and API routes. Flutter gets a new `AccessoryPage` on the home screen. Existing boxing module is extended to show and box accessories via a new `item_type` column on `finished_goods_box_item`.
|
||||
|
||||
**Tech Stack:** FastAPI + SQLAlchemy 2.0 (sync) + Pydantic v2 / Flutter 3.x + Dart / PostgreSQL
|
||||
|
||||
**Branching:** Create a `dev` branch from `master` in both `services/fastapi` and `apps/pad_scanner` submodules.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Create dev branches in both submodules
|
||||
|
||||
**Step 1: Create dev branch in fastapi submodule**
|
||||
|
||||
```bash
|
||||
cd D:/FileLib/Projects/CargoTrace/services/fastapi
|
||||
git checkout -b dev
|
||||
git push -u origin dev
|
||||
```
|
||||
|
||||
**Step 2: Create dev branch in pad_scanner submodule**
|
||||
|
||||
```bash
|
||||
cd D:/FileLib/Projects/CargoTrace/apps/pad_scanner
|
||||
git checkout -b dev
|
||||
git push -u origin dev
|
||||
```
|
||||
|
||||
**Step 3: Commit plan document in fastapi**
|
||||
|
||||
The PRD `2026-05-24-accessory-module.md` is already in the working tree.
|
||||
|
||||
```bash
|
||||
cd D:/FileLib/Projects/CargoTrace/services/fastapi
|
||||
git add docs/plans/2026-05-24-accessory-module.md docs/plans/2026-05-24-accessory-impl.md
|
||||
git commit -m "docs: add accessory module PRD and implementation plan"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Database migration — new tables + ALTER existing table
|
||||
|
||||
**Files:**
|
||||
- Create: `services/fastapi/app/models/accessory.py`
|
||||
- Modify: `services/fastapi/app/models/finished_goods.py` (add `item_type` column to `FinishedGoodsBoxItem`)
|
||||
- Modify: `services/fastapi/app/models/__init__.py` (register new models for auto-import)
|
||||
|
||||
**Step 1: Create the accessory model file**
|
||||
|
||||
```python
|
||||
# app/models/accessory.py
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import BigInteger, DateTime, Index, Integer, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
SCHEMA = "CargoTrace"
|
||||
|
||||
|
||||
class FinishedGoodsAccessory(Base):
|
||||
__tablename__ = "finished_goods_accessory"
|
||||
__table_args__ = (
|
||||
Index("idx_fga_paichan_no", "paichan_no"),
|
||||
Index("idx_fga_zongpai_no", "zongpai_no"),
|
||||
Index("idx_fga_location_code", "location_code"),
|
||||
{"schema": SCHEMA},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
paichan_no: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
zongpai_no: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
accessory_type: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
quantity: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
location_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
nullable=False,
|
||||
default=func.current_timestamp(),
|
||||
server_default=func.current_timestamp(),
|
||||
)
|
||||
|
||||
|
||||
class AccessoryType(Base):
|
||||
__tablename__ = "accessory_type"
|
||||
__table_args__ = (
|
||||
Index("uk_accessory_type_name", "name", unique=True),
|
||||
{"schema": SCHEMA},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
```
|
||||
|
||||
**Step 2: Add `item_type` column to `FinishedGoodsBoxItem`**
|
||||
|
||||
In `app/models/finished_goods.py`, add to the `FinishedGoodsBoxItem` class:
|
||||
|
||||
```python
|
||||
item_type: Mapped[str] = mapped_column(
|
||||
String(16), nullable=False, default="product", server_default="product"
|
||||
)
|
||||
```
|
||||
|
||||
**Step 3: Run the SQL migration manually**
|
||||
|
||||
Execute against the PostgreSQL database:
|
||||
|
||||
```sql
|
||||
-- 1. 附件记录表
|
||||
CREATE TABLE IF NOT EXISTS "CargoTrace".finished_goods_accessory (
|
||||
id SERIAL PRIMARY KEY,
|
||||
paichan_no VARCHAR(64) NOT NULL,
|
||||
zongpai_no VARCHAR(64) NOT NULL,
|
||||
accessory_type VARCHAR(64) NOT NULL,
|
||||
quantity INT NOT NULL,
|
||||
location_code VARCHAR(64) NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_fga_paichan_no ON "CargoTrace".finished_goods_accessory (paichan_no);
|
||||
CREATE INDEX IF NOT EXISTS idx_fga_zongpai_no ON "CargoTrace".finished_goods_accessory (zongpai_no);
|
||||
CREATE INDEX IF NOT EXISTS idx_fga_location_code ON "CargoTrace".finished_goods_accessory (location_code);
|
||||
|
||||
-- 2. 附件类型预设表
|
||||
CREATE TABLE IF NOT EXISTS "CargoTrace".accessory_type (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
CONSTRAINT uk_accessory_type_name UNIQUE (name)
|
||||
);
|
||||
|
||||
-- 3. 装箱明细表新增 item_type 字段
|
||||
ALTER TABLE "CargoTrace".finished_goods_box_item
|
||||
ADD COLUMN IF NOT EXISTS item_type VARCHAR(16) NOT NULL DEFAULT 'product';
|
||||
```
|
||||
|
||||
**Step 4: Verify tables exist**
|
||||
|
||||
```bash
|
||||
cd D:/FileLib/Projects/CargoTrace/services/fastapi
|
||||
# Activate venv and run a quick check
|
||||
```
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add app/models/accessory.py app/models/finished_goods.py
|
||||
git commit -m "feat: add accessory models and box_item item_type column"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Pydantic schemas for accessory endpoints
|
||||
|
||||
**Files:**
|
||||
- Create: `services/fastapi/app/schemas/accessory.py`
|
||||
|
||||
**Step 1: Write the schema file**
|
||||
|
||||
```python
|
||||
# app/schemas/accessory.py
|
||||
import re
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
PAICHAN_PATTERN = re.compile(r"^[A-Z]{1,2}\d{5}(-J)?$")
|
||||
LOCATION_NORMAL_PATTERN = re.compile(r"^[A-Z]+\d+-\d+-\d+$")
|
||||
LOCATION_TRANS_PATTERN = re.compile(r"^TRANS-\d+")
|
||||
|
||||
|
||||
# ── Work order query ──
|
||||
|
||||
class WorkOrderGroup(BaseModel):
|
||||
work_order_no: str
|
||||
zongpai_nos: list[str]
|
||||
|
||||
|
||||
class WorkOrderQueryResponse(BaseModel):
|
||||
paichan_no: str
|
||||
work_orders: list[WorkOrderGroup]
|
||||
|
||||
|
||||
# ── Accessory CRUD ──
|
||||
|
||||
class AccessoryCreateRequest(BaseModel):
|
||||
paichan_no: str
|
||||
zongpai_no: str
|
||||
accessory_type: str
|
||||
quantity: int
|
||||
location_code: str | None = None
|
||||
|
||||
@field_validator("paichan_no")
|
||||
@classmethod
|
||||
def validate_paichan_no(cls, v: str) -> str:
|
||||
v = v.strip().upper()
|
||||
if not PAICHAN_PATTERN.match(v):
|
||||
raise ValueError("INVALID_PAICHAN")
|
||||
return v
|
||||
|
||||
@field_validator("zongpai_no")
|
||||
@classmethod
|
||||
def validate_zongpai_no(cls, v: str) -> str:
|
||||
v = v.strip().upper()
|
||||
if not re.match(r"^(\d{2}(B|C|T)\d+|\d{2}(BW|CW)\d{4})$", v):
|
||||
raise ValueError("INVALID_ZONGPAI")
|
||||
return v
|
||||
|
||||
@field_validator("quantity")
|
||||
@classmethod
|
||||
def validate_quantity(cls, v: int) -> int:
|
||||
if v <= 0:
|
||||
raise ValueError("INVALID_QUANTITY")
|
||||
return v
|
||||
|
||||
@field_validator("location_code")
|
||||
@classmethod
|
||||
def validate_location_code(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
v = v.strip().upper()
|
||||
if not LOCATION_NORMAL_PATTERN.match(v) and not LOCATION_TRANS_PATTERN.match(v):
|
||||
raise ValueError("INVALID_LOCATION")
|
||||
return v
|
||||
|
||||
|
||||
class AccessoryResponse(BaseModel):
|
||||
id: int
|
||||
paichan_no: str
|
||||
zongpai_no: str
|
||||
accessory_type: str
|
||||
quantity: int
|
||||
location_code: str | None = None
|
||||
is_boxed: bool = False
|
||||
created_at: str
|
||||
|
||||
|
||||
class AccessoryListResponse(BaseModel):
|
||||
paichan_no: str
|
||||
items: list[AccessoryResponse]
|
||||
|
||||
|
||||
class AccessoryUpdateRequest(BaseModel):
|
||||
accessory_type: str | None = None
|
||||
quantity: int | None = None
|
||||
location_code: str | None = None
|
||||
|
||||
@field_validator("quantity")
|
||||
@classmethod
|
||||
def validate_quantity(cls, v: int | None) -> int | None:
|
||||
if v is not None and v <= 0:
|
||||
raise ValueError("INVALID_QUANTITY")
|
||||
return v
|
||||
|
||||
@field_validator("location_code")
|
||||
@classmethod
|
||||
def validate_location_code(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
v = v.strip().upper()
|
||||
if not LOCATION_NORMAL_PATTERN.match(v) and not LOCATION_TRANS_PATTERN.match(v):
|
||||
raise ValueError("INVALID_LOCATION")
|
||||
return v
|
||||
|
||||
|
||||
# ── Accessory type management ──
|
||||
|
||||
class AccessoryTypeItem(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
sort_order: int
|
||||
|
||||
|
||||
class AccessoryTypeListResponse(BaseModel):
|
||||
types: list[AccessoryTypeItem]
|
||||
|
||||
|
||||
class AccessoryTypeCreateRequest(BaseModel):
|
||||
name: str
|
||||
sort_order: int = 0
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if not v:
|
||||
raise ValueError("INVALID_TYPE_NAME")
|
||||
return v
|
||||
|
||||
|
||||
class AccessoryTypeUpdateRequest(BaseModel):
|
||||
name: str | None = None
|
||||
sort_order: int | None = None
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add app/schemas/accessory.py
|
||||
git commit -m "feat: add accessory Pydantic schemas"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Accessory service layer — business logic
|
||||
|
||||
**Files:**
|
||||
- Create: `services/fastapi/app/services/accessory_service.py`
|
||||
|
||||
This service handles: work order query, accessory CRUD, shelving/off-shelf logic, and type management.
|
||||
|
||||
**Step 1: Write the service**
|
||||
|
||||
Key functions to implement:
|
||||
|
||||
1. `query_work_orders(db, paichan_no)` — Query ERP for all work orders under a paichan_no, group by work_order_no, return `WorkOrderQueryResponse`.
|
||||
2. `list_accessories(db, paichan_no)` — List all accessories for a paichan_no, check `is_boxed` via `finished_goods_box_item`.
|
||||
3. `create_accessory(db, req)` — Create accessory record with shelving logic (same off-shelf rules as location_service).
|
||||
4. `update_accessory(db, accessory_id, req)` — Partial update of accessory_type/quantity/location_code.
|
||||
5. `delete_accessory(db, accessory_id)` — Delete if not boxed.
|
||||
6. `list_accessory_types(db)` / `create_accessory_type(db, req)` / `update_accessory_type(db, type_id, req)` / `delete_accessory_type(db, type_id)`
|
||||
|
||||
**Error classes to define:**
|
||||
|
||||
```python
|
||||
class InvalidPaichanError(Exception):
|
||||
pass
|
||||
|
||||
class PaichanNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
class AccessoryNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
class AccessoryAlreadyBoxedError(Exception):
|
||||
pass
|
||||
|
||||
class AccessoryDuplicateLocationError(Exception):
|
||||
def __init__(self, location_code: str, registered_at: str):
|
||||
self.location_code = location_code
|
||||
self.registered_at = registered_at
|
||||
|
||||
class AccessoryAlreadyOffShelfError(Exception):
|
||||
def __init__(self, location_code: str, registered_at: str):
|
||||
self.location_code = location_code
|
||||
self.registered_at = registered_at
|
||||
```
|
||||
|
||||
**Shelving logic** (in `create_accessory` when `location_code` is provided):
|
||||
|
||||
- If the accessory row already has a `location_code`:
|
||||
- If existing starts with `TRANS-` → raise `AccessoryAlreadyOffShelfError`
|
||||
- If new target starts with `TRANS-` → update record (off-shelf / down-shelf)
|
||||
- If both are normal locations → raise `AccessoryDuplicateLocationError`
|
||||
|
||||
**is_boxed check**: Query `finished_goods_box_item` where `zongpai_no` matches and `item_type = 'accessory'` and there exists a `finished_goods_box` with `paichan_no` matching. This checks if the accessory has been packed into a box.
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add app/services/accessory_service.py
|
||||
git commit -m "feat: add accessory service layer with CRUD and shelving logic"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Accessory API routes
|
||||
|
||||
**Files:**
|
||||
- Create: `services/fastapi/app/api/v1/accessory.py`
|
||||
- Modify: `services/fastapi/app/main.py` — register new router + exception handlers
|
||||
|
||||
**Step 1: Create the accessory router**
|
||||
|
||||
```python
|
||||
# app/api/v1/accessory.py
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.schemas.accessory import (
|
||||
AccessoryCreateRequest,
|
||||
AccessoryListResponse,
|
||||
AccessoryResponse,
|
||||
AccessoryTypeCreateRequest,
|
||||
AccessoryTypeItem,
|
||||
AccessoryTypeListResponse,
|
||||
AccessoryTypeUpdateRequest,
|
||||
AccessoryUpdateRequest,
|
||||
WorkOrderQueryResponse,
|
||||
)
|
||||
from app.services.accessory_service import (
|
||||
create_accessory,
|
||||
create_accessory_type,
|
||||
delete_accessory,
|
||||
delete_accessory_type,
|
||||
list_accessories,
|
||||
list_accessory_types,
|
||||
update_accessory,
|
||||
update_accessory_type,
|
||||
query_work_orders,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["accessory"])
|
||||
```
|
||||
|
||||
Endpoints:
|
||||
|
||||
| Method | Path | Handler |
|
||||
|---|---|---|
|
||||
| GET | `/accessory/work-orders` | `query_work_orders` |
|
||||
| GET | `/accessory` | `list_accessories` |
|
||||
| POST | `/accessory` | `create_accessory` |
|
||||
| PATCH | `/accessory/{id}` | `update_accessory` |
|
||||
| DELETE | `/accessory/{id}` | `delete_accessory` |
|
||||
| GET | `/accessory-type` | `list_accessory_types` |
|
||||
| POST | `/accessory-type` | `create_accessory_type` |
|
||||
| PATCH | `/accessory-type/{id}` | `update_accessory_type` |
|
||||
| DELETE | `/accessory-type/{id}` | `delete_accessory_type` |
|
||||
|
||||
**Step 2: Register router + exception handlers in `main.py`**
|
||||
|
||||
Add to `app/main.py`:
|
||||
|
||||
```python
|
||||
from app.api.v1.accessory import router as accessory_router
|
||||
from app.services.accessory_service import (
|
||||
AccessoryAlreadyBoxedError,
|
||||
AccessoryAlreadyOffShelfError,
|
||||
AccessoryDuplicateLocationError,
|
||||
AccessoryNotFoundError,
|
||||
InvalidPaichanError,
|
||||
PaichanNotFoundError,
|
||||
)
|
||||
|
||||
app.include_router(accessory_router, prefix="/CargoTrace")
|
||||
```
|
||||
|
||||
Add exception handlers mirroring the existing pattern (400 for `InvalidPaichanError`, 404 for `PaichanNotFoundError`/`AccessoryNotFoundError`, 409 for duplicate/off-shelf/boxed errors).
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add app/api/v1/accessory.py app/main.py
|
||||
git commit -m "feat: add accessory API routes and exception handlers"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Extend boxing module to support accessories
|
||||
|
||||
**Files:**
|
||||
- Modify: `services/fastapi/app/services/box_service.py` — `get_box_info` returns `pending_accessories`; `save_box_record` handles `item_type=accessory`
|
||||
- Modify: `services/fastapi/app/schemas/box.py` — add `PendingAccessory`, `BoxSaveRequest.item_type`, `BoxSaveRequest.accessory_id`
|
||||
- Modify: `services/fastapi/app/api/v1/box.py` — no structural change, just forward new fields
|
||||
|
||||
**Step 1: Extend box schemas**
|
||||
|
||||
In `app/schemas/box.py`, add:
|
||||
|
||||
```python
|
||||
class PendingAccessory(BaseModel):
|
||||
accessory_id: int
|
||||
accessory_type: str
|
||||
quantity: int
|
||||
location_code: str | None = None
|
||||
```
|
||||
|
||||
Add to `BoxInfoResponse`:
|
||||
|
||||
```python
|
||||
pending_accessories: list[PendingAccessory] = Field(default_factory=list)
|
||||
```
|
||||
|
||||
Add to `BoxSaveRequest`:
|
||||
|
||||
```python
|
||||
item_type: str = "product"
|
||||
accessory_id: int | None = None
|
||||
```
|
||||
|
||||
**Step 2: Extend `get_box_info` in `box_service.py`**
|
||||
|
||||
After building the existing response dict, query `finished_goods_accessory` for the same `zongpai_no` where `location_code IS NOT NULL` (shelved but not yet boxed). Add as `pending_accessories`.
|
||||
|
||||
**Step 3: Extend `save_box_record` in `box_service.py`**
|
||||
|
||||
When `item_type == "accessory"`:
|
||||
- Skip the ERP quantity validation (accessories don't have ERP quantities)
|
||||
- Use the `zongpai_no` and `paichan_no` from the `accessory_id` record
|
||||
- Create `FinishedGoodsBoxItem` with `item_type="accessory"`
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add app/schemas/box.py app/services/box_service.py
|
||||
git commit -m "feat: extend boxing module to support accessory items"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: FastAPI — integration test
|
||||
|
||||
**Files:**
|
||||
- Create: `services/fastapi/tests/test_accessory.py`
|
||||
|
||||
**Step 1: Write integration tests**
|
||||
|
||||
Test the following scenarios against the running API:
|
||||
|
||||
1. `GET /CargoTrace/accessory/work-orders?paichan_no=W00009` — returns work order groups
|
||||
2. `POST /CargoTrace/accessory` — create accessory with location
|
||||
3. `GET /CargoTrace/accessory?paichan_no=W00009` — list accessories
|
||||
4. `PATCH /CargoTrace/accessory/{id}` — update quantity
|
||||
5. `DELETE /CargoTrace/accessory/{id}` — delete
|
||||
6. `GET /CargoTrace/accessory-type` — list types
|
||||
7. `POST /CargoTrace/accessory-type` — create type
|
||||
8. Off-shelf scenario: create with normal location, then update to TRANS- location
|
||||
9. Error scenario: duplicate location, already off-shelf, invalid paichan
|
||||
|
||||
**Step 2: Run tests**
|
||||
|
||||
```bash
|
||||
cd D:/FileLib/Projects/CargoTrace/services/fastapi
|
||||
source .venv/Scripts/activate
|
||||
pytest tests/test_accessory.py -v
|
||||
```
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/test_accessory.py
|
||||
git commit -m "test: add accessory module integration tests"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Flutter — API service extension
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/pad_scanner/lib/services/api_service.dart` — add accessory API methods
|
||||
|
||||
**Step 1: Add accessory API methods to `ApiService`**
|
||||
|
||||
Methods to add:
|
||||
|
||||
```dart
|
||||
// Work order query
|
||||
Future<WorkOrderQueryResult> queryWorkOrders(String paichanNo)
|
||||
|
||||
// Accessory CRUD
|
||||
Future<AccessoryResult> createAccessory(AccessoryCreateRequest req)
|
||||
Future<AccessoryListResult> listAccessories(String paichanNo)
|
||||
Future<AccessoryResult> updateAccessory(int id, AccessoryUpdateRequest req)
|
||||
Future<bool> deleteAccessory(int id)
|
||||
|
||||
// Accessory types
|
||||
Future<List<AccessoryType>> listAccessoryTypes()
|
||||
Future<AccessoryType> createAccessoryType(String name, {int sortOrder = 0})
|
||||
Future<bool> deleteAccessoryType(int id)
|
||||
```
|
||||
|
||||
Define corresponding result/model classes.
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add lib/services/api_service.dart
|
||||
git commit -m "feat: add accessory API methods to Flutter API service"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Flutter — Accessory registration page
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/pad_scanner/lib/pages/accessory_page.dart`
|
||||
- Create: `apps/pad_scanner/lib/pages/accessory/` directory with part files for separation
|
||||
|
||||
**Step 1: Create the main page structure**
|
||||
|
||||
`accessory_page.dart` as StatefulWidget, following the same pattern as `registration_page.dart`:
|
||||
|
||||
- Part files for scan handling, submit logic, status display, list management
|
||||
- State machine: `idle` → `paichanQueried` → `workOrderSelected` → `accessoryTypeSelected` → `readyToSubmit`
|
||||
|
||||
**Step 2: Implement the step-by-step form**
|
||||
|
||||
- Paichan_no input field + query button (hardware Enter key triggers query)
|
||||
- Work order dropdown (populated after query)
|
||||
- Zongpai_no auto-fill or dropdown (based on selected work order)
|
||||
- Accessory type selector (dropdown from presets + manual input option)
|
||||
- Quantity input field
|
||||
- Location code field (supports scanner input)
|
||||
- Submit button
|
||||
|
||||
**Step 3: Implement the registered accessories list**
|
||||
|
||||
Bottom half of the page, showing all accessories for the current paichan_no with inline edit/delete.
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add lib/pages/accessory_page.dart lib/pages/accessory/
|
||||
git commit -m "feat: add accessory registration page"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 10: Flutter — Update home page with accessory card
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/pad_scanner/lib/pages/home_page.dart`
|
||||
|
||||
**Step 1: Add accessory module card**
|
||||
|
||||
Add a third card between "装箱编号" and "货架查询":
|
||||
|
||||
```dart
|
||||
ModuleCard(
|
||||
icon: Icons.build,
|
||||
title: '附件登记',
|
||||
description: '手动登记订单附件上架与装箱',
|
||||
status: ModuleStatus.developing,
|
||||
onTap: () => Navigator.pushNamed(context, '/accessory'),
|
||||
)
|
||||
```
|
||||
|
||||
**Step 2: Register route in `main.dart`**
|
||||
|
||||
```dart
|
||||
'/accessory': (context) => const AccessoryPage(),
|
||||
```
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add lib/pages/home_page.dart lib/main.dart
|
||||
git commit -m "feat: add accessory module card to home page"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 11: Flutter — Extend boxing page for accessories
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/pad_scanner/lib/pages/boxing_page.dart` or relevant part files
|
||||
- Modify: `apps/pad_scanner/lib/pages/boxing/boxing_models.dart` — add `PendingAccessory` model
|
||||
|
||||
**Step 1: Parse `pending_accessories` from box info API response**
|
||||
|
||||
When `get_box_info` returns data, extract `pending_accessories` list.
|
||||
|
||||
**Step 2: Add "待装箱附件" section in boxing UI**
|
||||
|
||||
Below the existing assigned items list, show pending accessories with "加入本箱" buttons.
|
||||
|
||||
**Step 3: Handle accessory boxing submission**
|
||||
|
||||
When boxing an accessory, send `item_type: 'accessory'` and `accessory_id` in the request.
|
||||
|
||||
**Step 4: Show accessory items in boxing detail page**
|
||||
|
||||
In `boxing_detail_page.dart`, distinguish accessories from products (e.g., different row color or icon).
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add lib/pages/boxing_page.dart lib/pages/boxing/ lib/pages/boxing_detail_page.dart
|
||||
git commit -m "feat: extend boxing module to display and box accessories"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 12: Manual testing & polish
|
||||
|
||||
**Step 1: Start FastAPI dev server**
|
||||
|
||||
```bash
|
||||
cd D:/FileLib/Projects/CargoTrace/services/fastapi
|
||||
source .venv/Scripts/activate
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
**Step 2: Build and run Flutter app**
|
||||
|
||||
```bash
|
||||
cd D:/FileLib/Projects/CargoTrace/apps/pad_scanner
|
||||
flutter run
|
||||
```
|
||||
|
||||
**Step 3: Test the complete flow**
|
||||
|
||||
1. Home page → 附件登记 card visible, tap to enter
|
||||
2. Input paichan_no → work orders load in dropdown
|
||||
3. Select work order → zongpai_no auto-fills
|
||||
4. Select accessory type → input quantity
|
||||
5. Scan location code → submit
|
||||
6. Verify accessory appears in registered list
|
||||
7. Go to boxing page → scan a zongpai_no that has accessories
|
||||
8. Verify "待装箱附件" section shows accessories
|
||||
9. Box an accessory → verify in detail page
|
||||
|
||||
**Step 4: Commit any fixes**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "fix: address issues found during manual testing"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 13: Final commit and merge to master
|
||||
|
||||
**Step 1: In fastapi submodule**
|
||||
|
||||
```bash
|
||||
cd D:/FileLib/Projects/CargoTrace/services/fastapi
|
||||
# Ensure all changes committed on dev
|
||||
git log --oneline master..dev
|
||||
# Merge dev into master
|
||||
git checkout master
|
||||
git merge dev
|
||||
git push
|
||||
```
|
||||
|
||||
**Step 2: In pad_scanner submodule**
|
||||
|
||||
```bash
|
||||
cd D:/FileLib/Projects/CargoTrace/apps/pad_scanner
|
||||
git log --oneline master..dev
|
||||
git checkout master
|
||||
git merge dev
|
||||
git push
|
||||
```
|
||||
|
||||
**Step 3: Update parent repo submodules**
|
||||
|
||||
```bash
|
||||
cd D:/FileLib/Projects/CargoTrace
|
||||
git add apps/pad_scanner services/fastapi
|
||||
git commit -m "feat: add accessory registration module"
|
||||
git push
|
||||
```
|
||||
Reference in New Issue
Block a user