feat: 前后端全量开发 v0.2
后端: - 客户模块 CRUD+搜索 (api/services/repositories 分层) - 合同模块: FG单号规则/意向单转正式单/订单创建自动快照客户信息 - 优化下料: FFD首递减装箱算法(利用率统计/余料/未排入清单) 前端 (Vue3+TS+Element Plus): - 7页面: 登录/工作台/订单中心/合同/客户/样板库/优化下料 - 自研二维窗型画布渲染器: 解析drawing_data渲染玻璃区域/外框/ 扇开向标记/尺寸标注, 支持缩放平移复位 - 样板库与订单联动(用此样板下单自动带图纸) - axios统一envelope拦截/登录守卫/付费墙提示 工程: - SPA history 回退路由, 构建产物挂载后端 - 全接口回归 6/6 通过
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
"""合同路由:意向单/正式单管理。"""
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from huamenchuang.core.deps import CurrentUser, DbSession
|
||||
from huamenchuang.core.response import fail, ok, paginated
|
||||
from huamenchuang.repositories.contract_repo import status_label
|
||||
from huamenchuang.services.contract_service import ContractService
|
||||
|
||||
router = APIRouter(prefix="/contracts", tags=["合同"])
|
||||
|
||||
|
||||
class ContractBody(BaseModel):
|
||||
customer_id: int | None = None
|
||||
customer_name: str = ""
|
||||
phone: str = ""
|
||||
address: str = ""
|
||||
area: float = 0
|
||||
count: int = 0
|
||||
money: float = 0
|
||||
note: str = ""
|
||||
|
||||
|
||||
class ConvertBody(BaseModel):
|
||||
contract_id: int
|
||||
|
||||
|
||||
def _to_out(c) -> dict:
|
||||
return {
|
||||
"id": c.id, "contract_no": c.contract_no, "customer_id": c.customer_id,
|
||||
"customer_name": c.customer_name, "phone": c.phone, "address": c.address,
|
||||
"area": float(c.area), "count": c.count, "money": float(c.money),
|
||||
"status": c.status, "status_label": status_label(c.status),
|
||||
"sign_status": c.sign_status, "note": c.note,
|
||||
"created_at": str(c.created_at), "updated_at": str(c.updated_at),
|
||||
}
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_contract(body: ContractBody, user: CurrentUser, db: DbSession):
|
||||
"""创建合同(默认意向单)。"""
|
||||
contract = await ContractService(db).create(user, body.model_dump())
|
||||
return ok(data=_to_out(contract), message="创建成功")
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_contracts(user: CurrentUser, db: DbSession, page: int = 1,
|
||||
page_size: int = 15, keyword: str = "", status: int | None = None):
|
||||
"""合同分页列表(结构对齐目标站 {"contracts": {...}})。"""
|
||||
items, total = await ContractService(db).repo.search(
|
||||
user.company_id, page, page_size, keyword, status)
|
||||
return ok(data={"contracts": paginated(
|
||||
[_to_out(c) for c in items], page, page_size, total)})
|
||||
|
||||
|
||||
@router.put("/{contract_id}")
|
||||
async def update_contract(contract_id: int, body: ContractBody, user: CurrentUser, db: DbSession):
|
||||
"""编辑合同。"""
|
||||
try:
|
||||
c = await ContractService(db).update(user, contract_id, body.model_dump())
|
||||
except KeyError as e:
|
||||
return fail(str(e))
|
||||
return ok(data=_to_out(c), message="更新成功")
|
||||
|
||||
|
||||
@router.post("/convert")
|
||||
async def convert_contract(body: ConvertBody, user: CurrentUser, db: DbSession):
|
||||
"""意向单转正式单(对齐目标站 convert-contract/)。"""
|
||||
try:
|
||||
c = await ContractService(db).convert_to_formal(user, body.contract_id)
|
||||
except KeyError as e:
|
||||
return fail(str(e))
|
||||
except ValueError as e:
|
||||
return fail(str(e))
|
||||
return ok(data=_to_out(c), message="已转为正式单")
|
||||
|
||||
|
||||
@router.delete("/{contract_id}")
|
||||
async def delete_contract(contract_id: int, user: CurrentUser, db: DbSession):
|
||||
"""删除合同。"""
|
||||
try:
|
||||
await ContractService(db).delete(user, contract_id)
|
||||
except KeyError as e:
|
||||
return fail(str(e))
|
||||
return ok(message="删除成功")
|
||||
@@ -0,0 +1,68 @@
|
||||
"""客户路由。"""
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from huamenchuang.core.deps import CurrentUser, DbSession
|
||||
from huamenchuang.core.response import fail, ok, paginated
|
||||
from huamenchuang.services.customer_service import CustomerService
|
||||
|
||||
router = APIRouter(prefix="/customers", tags=["客户"])
|
||||
|
||||
|
||||
class CustomerBody(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=128)
|
||||
phone: str = ""
|
||||
address: str = ""
|
||||
note: str = ""
|
||||
salesmen: str | None = None
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_customer(body: CustomerBody, user: CurrentUser, db: DbSession):
|
||||
"""新增客户。"""
|
||||
customer = await CustomerService(db).create(user, body.model_dump())
|
||||
return ok(data={"id": customer.id, "name": customer.name}, message="创建成功")
|
||||
|
||||
|
||||
@router.post("/search")
|
||||
async def search_customers(user: CurrentUser, db: DbSession,
|
||||
page: int = 1, page_size: int = 15, keyword: str = ""):
|
||||
"""客户搜索。"""
|
||||
items, total = await CustomerService(db).repo.search(user.company_id, page, page_size, keyword)
|
||||
return ok(data=paginated([{
|
||||
"id": c.id, "name": c.name, "phone": c.phone, "address": c.address,
|
||||
"note": c.note, "salesmen": c.salesmen, "balance": float(c.balance),
|
||||
"created_at": str(c.created_at),
|
||||
} for c in items], page, page_size, total))
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_customers(user: CurrentUser, db: DbSession, page: int = 1,
|
||||
page_size: int = 15, keyword: str = ""):
|
||||
"""客户分页列表。"""
|
||||
items, total = await CustomerService(db).repo.search(user.company_id, page, page_size, keyword)
|
||||
return ok(data=paginated([{
|
||||
"id": c.id, "name": c.name, "phone": c.phone, "address": c.address,
|
||||
"note": c.note, "salesmen": c.salesmen, "balance": float(c.balance),
|
||||
"created_at": str(c.created_at),
|
||||
} for c in items], page, page_size, total))
|
||||
|
||||
|
||||
@router.put("/{customer_id}")
|
||||
async def update_customer(customer_id: int, body: CustomerBody, user: CurrentUser, db: DbSession):
|
||||
"""编辑客户。"""
|
||||
try:
|
||||
c = await CustomerService(db).update(user, customer_id, body.model_dump())
|
||||
except KeyError as e:
|
||||
return fail(str(e))
|
||||
return ok(data={"id": c.id}, message="更新成功")
|
||||
|
||||
|
||||
@router.delete("/{customer_id}")
|
||||
async def delete_customer(customer_id: int, user: CurrentUser, db: DbSession):
|
||||
"""删除客户。"""
|
||||
try:
|
||||
await CustomerService(db).delete(user, customer_id)
|
||||
except KeyError as e:
|
||||
return fail(str(e))
|
||||
return ok(message="删除成功")
|
||||
@@ -0,0 +1,28 @@
|
||||
"""优化下料路由:一维型材切割方案计算(无状态,纯计算)。"""
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from huamenchuang.core.deps import CurrentUser
|
||||
from huamenchuang.core.response import fail, ok
|
||||
from huamenchuang.services.optimize_service import BarDemand, StockBar, optimize_1d
|
||||
|
||||
router = APIRouter(prefix="/optimize1d", tags=["优化下料"])
|
||||
|
||||
|
||||
class OptimizeRequest(BaseModel):
|
||||
demands: list[BarDemand] = Field(description="型材需求 [{length,qty,label}]")
|
||||
stocks: list[StockBar] = Field(description="库存原料 [{length,qty,label}]")
|
||||
kerf: int = Field(default=5, ge=0, le=50, description="锯口损耗(mm)")
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def optimize(body: OptimizeRequest, user: CurrentUser):
|
||||
"""一维优化下料(FFD 首递减算法)。"""
|
||||
if not body.demands or not body.stocks:
|
||||
return fail("需求和库存均不能为空")
|
||||
total_demand = sum(d.length * d.qty for d in body.demands)
|
||||
total_stock = sum(s.length * s.qty for s in body.stocks)
|
||||
if total_demand > total_stock:
|
||||
return fail(f"库存不足: 需求 {total_demand}mm > 库存 {total_stock}mm")
|
||||
result = optimize_1d(body.demands, body.stocks, body.kerf)
|
||||
return ok(data=result, message="优化完成")
|
||||
@@ -1,9 +1,12 @@
|
||||
"""路由汇总。"""
|
||||
from fastapi import APIRouter
|
||||
|
||||
from huamenchuang.api import auth, orders, windoor
|
||||
from huamenchuang.api import auth, contracts, customers, optimize, orders, windoor
|
||||
|
||||
api_router = APIRouter(prefix="/api")
|
||||
api_router.include_router(auth.router)
|
||||
api_router.include_router(orders.router)
|
||||
api_router.include_router(customers.router)
|
||||
api_router.include_router(contracts.router)
|
||||
api_router.include_router(windoor.router)
|
||||
api_router.include_router(optimize.router)
|
||||
|
||||
@@ -53,15 +53,28 @@ async def health():
|
||||
|
||||
app.include_router(api_router)
|
||||
|
||||
from fastapi.responses import FileResponse # noqa: E402
|
||||
|
||||
# 静态资源:样板平面图(data/windoor-images)
|
||||
_imgdir = Path(__file__).resolve().parents[2] / "data" / "windoor-images"
|
||||
if _imgdir.exists():
|
||||
app.mount("/static/windoor-images", StaticFiles(directory=str(_imgdir)), name="windoor-images")
|
||||
|
||||
# 前端构建产物挂载(前后端规范:构建到后端目录)
|
||||
# 前端构建产物挂载(前后端规范:构建到后端目录)+ history 路由回退
|
||||
_dist = Path(__file__).resolve().parents[2] / "frontend" / "dist"
|
||||
if _dist.exists():
|
||||
app.mount("/", StaticFiles(directory=str(_dist), html=True), name="frontend")
|
||||
app.mount("/assets", StaticFiles(directory=str(_dist / "assets")), name="assets")
|
||||
|
||||
@app.get("/{full_path:path}", include_in_schema=False)
|
||||
async def spa_fallback(full_path: str):
|
||||
"""非 API/静态路径一律返回 index.html(前端 history 模式)。"""
|
||||
if full_path.startswith(("api/", "static/")):
|
||||
return JSONResponse(
|
||||
{"code": 0, "message": "接口不存在", "data": "", "other_data": "", "other_data2": ""})
|
||||
target = _dist / full_path
|
||||
if full_path and target.is_file():
|
||||
return FileResponse(target)
|
||||
return FileResponse(_dist / "index.html")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""合同数据访问层。"""
|
||||
import datetime
|
||||
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from huamenchuang.models.contract import Contract
|
||||
|
||||
STATUS_LABELS = {0: "意向单", 1: "正式单", 2: "已完成", 3: "已作废"}
|
||||
|
||||
|
||||
class ContractRepository:
|
||||
def __init__(self, db: AsyncSession) -> None:
|
||||
self.db = db
|
||||
|
||||
async def next_contract_no(self, company_id: int) -> str:
|
||||
"""合同号:FG+YYYYMMDD+3位流水(对齐目标站 FG20260818009 风格)。"""
|
||||
today = datetime.date.today().strftime("%Y%m%d")
|
||||
prefix = f"FG{today}"
|
||||
stmt = select(Contract.contract_no).where(
|
||||
Contract.company_id == company_id, Contract.contract_no.like(f"{prefix}%")
|
||||
).order_by(Contract.contract_no.desc()).limit(1)
|
||||
last = (await self.db.execute(stmt)).scalar_one_or_none()
|
||||
seq = int(last[-3:]) + 1 if last else 1
|
||||
return f"{prefix}{seq:03d}"
|
||||
|
||||
async def get(self, contract_id: int, company_id: int) -> Contract | None:
|
||||
stmt = select(Contract).where(Contract.id == contract_id, Contract.company_id == company_id)
|
||||
return (await self.db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
async def search(self, company_id: int, page: int, page_size: int, keyword: str = "",
|
||||
status: int | None = None) -> tuple[list[Contract], int]:
|
||||
conds = [Contract.company_id == company_id]
|
||||
if status is not None:
|
||||
conds.append(Contract.status == status)
|
||||
if kw := keyword.strip():
|
||||
like = f"%{kw}%"
|
||||
conds.append(or_(Contract.contract_no.like(like), Contract.customer_name.like(like), Contract.phone.like(like)))
|
||||
where = *conds,
|
||||
total = (await self.db.execute(select(func.count()).select_from(Contract).where(*where))).scalar_one()
|
||||
stmt = (select(Contract).where(*where).order_by(Contract.id.desc())
|
||||
.offset((page - 1) * page_size).limit(page_size))
|
||||
return list((await self.db.execute(stmt)).scalars()), total
|
||||
|
||||
async def create(self, contract: Contract) -> Contract:
|
||||
self.db.add(contract)
|
||||
await self.db.flush()
|
||||
return contract
|
||||
|
||||
async def delete(self, contract: Contract) -> None:
|
||||
await self.db.delete(contract)
|
||||
|
||||
|
||||
def status_label(status: int) -> str:
|
||||
return STATUS_LABELS.get(status, "未知")
|
||||
@@ -0,0 +1,33 @@
|
||||
"""客户数据访问层。"""
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from huamenchuang.models.contract import Customer
|
||||
|
||||
|
||||
class CustomerRepository:
|
||||
def __init__(self, db: AsyncSession) -> None:
|
||||
self.db = db
|
||||
|
||||
async def get(self, customer_id: int, company_id: int) -> Customer | None:
|
||||
stmt = select(Customer).where(Customer.id == customer_id, Customer.company_id == company_id)
|
||||
return (await self.db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
async def search(self, company_id: int, page: int, page_size: int, keyword: str = "") -> tuple[list[Customer], int]:
|
||||
conds = [Customer.company_id == company_id]
|
||||
if kw := keyword.strip():
|
||||
like = f"%{kw}%"
|
||||
conds.append(or_(Customer.name.like(like), Customer.phone.like(like), Customer.address.like(like)))
|
||||
where = *conds,
|
||||
total = (await self.db.execute(select(func.count()).select_from(Customer).where(*where))).scalar_one()
|
||||
stmt = (select(Customer).where(*where).order_by(Customer.id.desc())
|
||||
.offset((page - 1) * page_size).limit(page_size))
|
||||
return list((await self.db.execute(stmt)).scalars()), total
|
||||
|
||||
async def create(self, customer: Customer) -> Customer:
|
||||
self.db.add(customer)
|
||||
await self.db.flush()
|
||||
return customer
|
||||
|
||||
async def delete(self, customer: Customer) -> None:
|
||||
await self.db.delete(customer)
|
||||
@@ -0,0 +1,44 @@
|
||||
"""合同业务逻辑层:意向单→正式单流转。"""
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from huamenchuang.models.contract import Contract
|
||||
from huamenchuang.models.user import User
|
||||
from huamenchuang.repositories.contract_repo import ContractRepository
|
||||
|
||||
|
||||
class ContractService:
|
||||
def __init__(self, db: AsyncSession) -> None:
|
||||
self.repo = ContractRepository(db)
|
||||
|
||||
async def create(self, user: User, data: dict) -> Contract:
|
||||
contract = await self.repo.create(Contract(
|
||||
company_id=user.company_id,
|
||||
user_id=user.id,
|
||||
contract_no=await self.repo.next_contract_no(user.company_id),
|
||||
**data,
|
||||
))
|
||||
return contract
|
||||
|
||||
async def update(self, user: User, contract_id: int, data: dict) -> Contract:
|
||||
contract = await self.repo.get(contract_id, user.company_id)
|
||||
if contract is None:
|
||||
raise KeyError("合同不存在")
|
||||
for k, v in data.items():
|
||||
setattr(contract, k, v)
|
||||
return contract
|
||||
|
||||
async def convert_to_formal(self, user: User, contract_id: int) -> Contract:
|
||||
"""意向单转正式单(对齐目标站 convert-contract 语义)。"""
|
||||
contract = await self.repo.get(contract_id, user.company_id)
|
||||
if contract is None:
|
||||
raise KeyError("合同不存在")
|
||||
if contract.status != 0:
|
||||
raise ValueError("仅意向单可转正式单")
|
||||
contract.status = 1
|
||||
return contract
|
||||
|
||||
async def delete(self, user: User, contract_id: int) -> None:
|
||||
contract = await self.repo.get(contract_id, user.company_id)
|
||||
if contract is None:
|
||||
raise KeyError("合同不存在")
|
||||
await self.repo.delete(contract)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""客户业务逻辑层。"""
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from huamenchuang.models.contract import Customer
|
||||
from huamenchuang.models.user import User
|
||||
from huamenchuang.repositories.customer_repo import CustomerRepository
|
||||
|
||||
|
||||
class CustomerService:
|
||||
def __init__(self, db: AsyncSession) -> None:
|
||||
self.repo = CustomerRepository(db)
|
||||
|
||||
async def create(self, user: User, data: dict) -> Customer:
|
||||
return await self.repo.create(Customer(company_id=user.company_id, **data))
|
||||
|
||||
async def update(self, user: User, customer_id: int, data: dict) -> Customer:
|
||||
customer = await self.repo.get(customer_id, user.company_id)
|
||||
if customer is None:
|
||||
raise KeyError("客户不存在")
|
||||
for k, v in data.items():
|
||||
setattr(customer, k, v)
|
||||
return customer
|
||||
|
||||
async def delete(self, user: User, customer_id: int) -> None:
|
||||
customer = await self.repo.get(customer_id, user.company_id)
|
||||
if customer is None:
|
||||
raise KeyError("客户不存在")
|
||||
await self.repo.delete(customer)
|
||||
@@ -0,0 +1,104 @@
|
||||
"""一维优化下料服务:首递减(FFD)装箱算法,输出切割方案与利用率。
|
||||
|
||||
对齐目标站 optimize1d 语义:输入订单型材需求 + 库存原料,输出每根原料的切割组合。
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class BarDemand:
|
||||
"""一段型材需求。"""
|
||||
length: int # 需求长度(mm)
|
||||
qty: int # 数量
|
||||
label: str = "" # 用途标签
|
||||
|
||||
|
||||
@dataclass
|
||||
class StockBar:
|
||||
"""库存原料。"""
|
||||
length: int # 原料长度(mm)
|
||||
qty: int # 库存根数
|
||||
label: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class CutPiece:
|
||||
length: int
|
||||
label: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class CutPlan:
|
||||
"""一根原料的切割方案。"""
|
||||
stock_length: int
|
||||
stock_label: str
|
||||
pieces: list[CutPiece] = field(default_factory=list)
|
||||
waste: int = 0 # 余料浪费
|
||||
|
||||
|
||||
# 锯口损耗(mm),每次切割损耗一段
|
||||
KERF_DEFAULT = 5
|
||||
|
||||
|
||||
def optimize_1d(demands: list[BarDemand], stocks: list[StockBar],
|
||||
kerf: int = KERF_DEFAULT) -> dict:
|
||||
"""首递减装箱:长件优先装入可容纳的原料,输出方案与统计。"""
|
||||
# 展开需求并降序
|
||||
pieces: list[CutPiece] = []
|
||||
for d in demands:
|
||||
pieces.extend(CutPiece(length=d.length, label=d.label) for _ in range(max(0, d.qty)))
|
||||
pieces.sort(key=lambda p: p.length, reverse=True)
|
||||
|
||||
# 展开库存(可切割长度 = 原料长)
|
||||
avail: list[dict] = []
|
||||
for s in stocks:
|
||||
for _ in range(max(0, s.qty)):
|
||||
avail.append({"length": s.length, "label": s.label, "pieces": [], "used": 0})
|
||||
|
||||
unplaced: list[CutPiece] = []
|
||||
for piece in pieces:
|
||||
# 找第一根装得下的原料(剩余空间 >= 件长 + 锯口)
|
||||
placed = False
|
||||
for bar in avail:
|
||||
remain = bar["length"] - bar["used"]
|
||||
cost = piece.length + (kerf if bar["pieces"] else 0)
|
||||
if remain >= cost:
|
||||
bar["pieces"].append(piece)
|
||||
bar["used"] += cost
|
||||
placed = True
|
||||
break
|
||||
if not placed:
|
||||
unplaced.append(piece)
|
||||
|
||||
plans = [
|
||||
CutPlan(
|
||||
stock_length=b["length"],
|
||||
stock_label=b["label"],
|
||||
pieces=b["pieces"],
|
||||
waste=b["length"] - b["used"],
|
||||
)
|
||||
for b in avail if b["pieces"]
|
||||
]
|
||||
total_demand = sum(p.length for p in pieces)
|
||||
total_stock = sum(p.stock_length for p in plans)
|
||||
return {
|
||||
"plans": [
|
||||
{
|
||||
"stock_length": p.stock_length,
|
||||
"stock_label": p.stock_label,
|
||||
"pieces": [{"length": c.length, "label": c.label} for c in p.pieces],
|
||||
"waste": p.waste,
|
||||
}
|
||||
for p in plans
|
||||
],
|
||||
"unplaced": [{"length": c.length, "label": c.label} for c in unplaced],
|
||||
"stats": {
|
||||
"total_pieces": len(pieces),
|
||||
"placed_pieces": len(pieces) - len(unplaced),
|
||||
"stock_bars_used": len(plans),
|
||||
"total_demand_mm": total_demand,
|
||||
"total_stock_mm": total_stock,
|
||||
"utilization": round(total_demand / total_stock, 4) if total_stock else 0,
|
||||
"kerf_mm": kerf,
|
||||
},
|
||||
}
|
||||
@@ -14,13 +14,26 @@ class OrderService:
|
||||
self.repo = OrderRepository(db)
|
||||
|
||||
async def create_order(self, user: User, payload: OrderCreate) -> Order:
|
||||
"""创建订单:对齐目标站 {contract_id, drawing_data, pic_url} 契约。"""
|
||||
"""创建订单:对齐目标站 {contract_id, drawing_data, pic_url} 契约。
|
||||
指定 contract_id 时自动快照合同客户信息。"""
|
||||
customer_id = payload.customer_id
|
||||
customer_name = phone = address = ""
|
||||
if payload.contract_id:
|
||||
from huamenchuang.repositories.contract_repo import ContractRepository
|
||||
contract = await ContractRepository(self.db).get(payload.contract_id, user.company_id)
|
||||
if contract is None:
|
||||
raise KeyError("关联合同不存在")
|
||||
customer_id = contract.customer_id
|
||||
customer_name, phone, address = contract.customer_name, contract.phone, contract.address
|
||||
order = Order(
|
||||
order_no=await self.repo.next_order_no(user.company_id),
|
||||
contract_id=payload.contract_id,
|
||||
drawing_data=payload.drawing_data,
|
||||
pic_url=payload.pic_url,
|
||||
customer_id=payload.customer_id,
|
||||
customer_id=customer_id,
|
||||
customer_name=customer_name,
|
||||
phone=phone,
|
||||
address=address,
|
||||
note=payload.note,
|
||||
money=payload.money,
|
||||
count=payload.count,
|
||||
|
||||
Reference in New Issue
Block a user