This commit is contained in:
2026-04-19 23:41:35 +08:00
parent 7660cda647
commit ef257e7120
31 changed files with 6317 additions and 1332 deletions
+42 -41
View File
@@ -4,6 +4,7 @@ from sqlalchemy import select, func
from sqlalchemy.orm import selectinload
from typing import Optional, List, Dict, Tuple
from datetime import datetime
from decimal import Decimal
from database.database import get_db_session
from services.auth_service import get_current_active_user
@@ -414,15 +415,15 @@ async def get_finance_summary(
) or 0
return FinanceSummaryResponse(
receivable_total=round(float(receivable_total), 2),
payable_total=round(float(payable_total), 2),
monthly_receipt_total=round(float(monthly_receipt_total), 2),
monthly_payment_total=round(float(monthly_payment_total), 2),
receivable_total=Decimal(str(receivable_total)),
payable_total=Decimal(str(payable_total)),
monthly_receipt_total=Decimal(str(monthly_receipt_total)),
monthly_payment_total=Decimal(str(monthly_payment_total)),
selected_year=selected_year,
selected_quarter=selected_quarter,
period_label=period_label,
period_receipt_total=round(float(period_receipt_total), 2),
period_payment_total=round(float(period_payment_total), 2),
period_receipt_total=Decimal(str(period_receipt_total)),
period_payment_total=Decimal(str(period_payment_total)),
overdue_receivable_count=0,
overdue_payable_count=0,
)
@@ -464,9 +465,9 @@ async def get_partner_statement(
"outstanding_total": 0.0,
},
)
total_amount = float(order.total_amount or 0)
settled_amount = float(order.received_amount or 0)
outstanding = max(total_amount - settled_amount, 0.0)
total_amount = Decimal(str(order.total_amount or 0))
settled_amount = Decimal(str(order.received_amount or 0))
outstanding = max(total_amount - settled_amount, Decimal("0"))
partner_stat["order_count"] += 1
partner_stat["order_total"] += total_amount
partner_stat["settled_total"] += settled_amount
@@ -495,7 +496,7 @@ async def get_partner_statement(
},
)
partner_stat["transaction_count"] += 1
partner_stat["transaction_total"] += float(txn.amount or 0)
partner_stat["transaction_total"] += Decimal(str(txn.amount or 0))
else:
order_rows = await db_session.execute(
select(PurchaseOrder, Supplier)
@@ -518,9 +519,9 @@ async def get_partner_statement(
"outstanding_total": 0.0,
},
)
total_amount = float(order.total_amount or 0)
settled_amount = float(order.paid_amount or 0)
outstanding = max(total_amount - settled_amount, 0.0)
total_amount = Decimal(str(order.total_amount or 0))
settled_amount = Decimal(str(order.paid_amount or 0))
outstanding = max(total_amount - settled_amount, Decimal("0"))
partner_stat["order_count"] += 1
partner_stat["order_total"] += total_amount
partner_stat["settled_total"] += settled_amount
@@ -549,7 +550,7 @@ async def get_partner_statement(
},
)
partner_stat["transaction_count"] += 1
partner_stat["transaction_total"] += float(txn.amount or 0)
partner_stat["transaction_total"] += Decimal(str(txn.amount or 0))
missing_partner_ids = [pid for pid, item in stats_map.items() if "#" in item["partner_name"]]
if missing_partner_ids:
@@ -572,10 +573,10 @@ async def get_partner_statement(
partner_name=item["partner_name"],
order_count=item["order_count"],
transaction_count=item["transaction_count"],
order_total=round(float(item["order_total"]), 2),
settled_total=round(float(item["settled_total"]), 2),
transaction_total=round(float(item["transaction_total"]), 2),
outstanding_total=round(float(item["outstanding_total"]), 2),
order_total=Decimal(str(item["order_total"])),
settled_total=Decimal(str(item["settled_total"])),
transaction_total=Decimal(str(item["transaction_total"])),
outstanding_total=Decimal(str(item["outstanding_total"])),
period_year=selected_year,
period_quarter=selected_quarter,
)
@@ -587,10 +588,10 @@ async def get_partner_statement(
year=selected_year,
quarter=selected_quarter,
period_label=period_label,
order_total=round(float(sum(item.order_total for item in items)), 2),
settled_total=round(float(sum(item.settled_total for item in items)), 2),
transaction_total=round(float(sum(item.transaction_total for item in items)), 2),
outstanding_total=round(float(sum(item.outstanding_total for item in items)), 2),
order_total=Decimal(str(sum(item.order_total for item in items))),
settled_total=Decimal(str(sum(item.settled_total for item in items))),
transaction_total=Decimal(str(sum(item.transaction_total for item in items))),
outstanding_total=Decimal(str(sum(item.outstanding_total for item in items))),
items=items,
)
@@ -642,15 +643,15 @@ async def get_partner_product_statement(
},
)
item_amount = float(item.amount or 0)
order_total = float(order.total_amount or 0)
order_settled = max(float(order.received_amount or 0), 0.0)
ratio = (item_amount / order_total) if order_total > 1e-9 else 0.0
item_amount = Decimal(str(item.amount or 0))
order_total = Decimal(str(order.total_amount or 0))
order_settled = max(Decimal(str(order.received_amount or 0)), Decimal("0"))
ratio = (item_amount / order_total) if order_total > Decimal("1e-9") else Decimal("0")
item_settled = min(item_amount, order_settled * ratio)
item_outstanding = max(item_amount - item_settled, 0.0)
item_outstanding = max(item_amount - item_settled, Decimal("0"))
stat["order_ids"].add(order.id)
stat["order_quantity"] += float(item.quantity or 0)
stat["order_quantity"] += Decimal(str(item.quantity or 0))
stat["order_amount"] += item_amount
stat["settled_amount"] += item_settled
stat["outstanding_amount"] += item_outstanding
@@ -686,15 +687,15 @@ async def get_partner_product_statement(
},
)
item_amount = float(item.amount or 0)
order_total = float(order.total_amount or 0)
order_settled = max(float(order.paid_amount or 0), 0.0)
ratio = (item_amount / order_total) if order_total > 1e-9 else 0.0
item_amount = Decimal(str(item.amount or 0))
order_total = Decimal(str(order.total_amount or 0))
order_settled = max(Decimal(str(order.paid_amount or 0)), Decimal("0"))
ratio = (item_amount / order_total) if order_total > Decimal("1e-9") else Decimal("0")
item_settled = min(item_amount, order_settled * ratio)
item_outstanding = max(item_amount - item_settled, 0.0)
item_outstanding = max(item_amount - item_settled, Decimal("0"))
stat["order_ids"].add(order.id)
stat["order_quantity"] += float(item.quantity or 0)
stat["order_quantity"] += Decimal(str(item.quantity or 0))
stat["order_amount"] += item_amount
stat["settled_amount"] += item_settled
stat["outstanding_amount"] += item_outstanding
@@ -707,10 +708,10 @@ async def get_partner_product_statement(
product_sku=item["product_sku"],
product_name=item["product_name"],
order_count=len(item["order_ids"]),
order_quantity=round(float(item["order_quantity"]), 2),
order_amount=round(float(item["order_amount"]), 2),
settled_amount=round(float(item["settled_amount"]), 2),
outstanding_amount=round(float(item["outstanding_amount"]), 2),
order_quantity=Decimal(str(item["order_quantity"])),
order_amount=Decimal(str(item["order_amount"])),
settled_amount=Decimal(str(item["settled_amount"])),
outstanding_amount=Decimal(str(item["outstanding_amount"])),
period_year=selected_year,
period_quarter=selected_quarter,
)
@@ -727,9 +728,9 @@ async def get_partner_product_statement(
quarter=selected_quarter,
period_label=period_label,
partner_id=partner_id,
order_amount_total=round(float(sum(item.order_amount for item in items)), 2),
settled_amount_total=round(float(sum(item.settled_amount for item in items)), 2),
outstanding_amount_total=round(float(sum(item.outstanding_amount for item in items)), 2),
order_amount_total=Decimal(str(sum(item.order_amount for item in items))),
settled_amount_total=Decimal(str(sum(item.settled_amount for item in items))),
outstanding_amount_total=Decimal(str(sum(item.outstanding_amount for item in items))),
items=items,
)
+12 -11
View File
@@ -13,6 +13,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, or_, func, delete
from typing import Optional, List, Dict
from decimal import Decimal
from database.database import get_db_session
from services.auth_service import get_current_active_user, get_current_admin_user
@@ -45,10 +46,10 @@ async def _calculate_material_cost_map(db_session: AsyncSession, product_ids: Li
.where(ProductMaterial.finished_product_id.in_(product_ids))
.group_by(ProductMaterial.finished_product_id)
)
return {row[0]: float(row[1] or 0) for row in result.all()}
return {row[0]: Decimal(str(row[1] or 0)) for row in result.all()}
def _build_product_response(product: Product, material_cost: float = 0) -> ProductResponse:
def _build_product_response(product: Product, material_cost: Decimal = Decimal("0")) -> ProductResponse:
return ProductResponse(
id=product.id,
sku=product.sku,
@@ -57,11 +58,11 @@ def _build_product_response(product: Product, material_cost: float = 0) -> Produ
category=product.category,
unit=product.unit,
item_type=product.item_type,
cost_price=float(product.cost_price or 0),
sale_price=float(product.sale_price or 0),
cost_price=Decimal(str(product.cost_price or 0)),
sale_price=Decimal(str(product.sale_price or 0)),
min_stock=product.min_stock,
max_stock=product.max_stock,
material_cost=round(float(material_cost), 4),
material_cost=Decimal(str(material_cost)).quantize(Decimal("0.0001")),
is_active=product.is_active,
created_at=product.created_at,
)
@@ -184,25 +185,25 @@ async def get_product_bom(
)
items: List[ProductMaterialItemResponse] = []
total_material_cost = 0.0
total_material_cost = Decimal("0")
for bom, material in bom_result.all():
line_cost = float(material.cost_price or 0) * float(bom.quantity)
line_cost = Decimal(str(material.cost_price or 0)) * Decimal(str(bom.quantity))
total_material_cost += line_cost
items.append(
ProductMaterialItemResponse(
material_id=material.id,
material_sku=material.sku,
material_name=material.name,
quantity=round(float(bom.quantity), 4),
unit_cost=round(float(material.cost_price or 0), 4),
line_cost=round(float(line_cost), 4),
quantity=Decimal(str(bom.quantity)),
unit_cost=Decimal(str(material.cost_price or 0)),
line_cost=line_cost,
)
)
return ProductBOMResponse(
product_id=product.id,
product_name=product.name,
total_material_cost=round(float(total_material_cost), 4),
total_material_cost=total_material_cost,
items=items,
)
+4 -3
View File
@@ -12,6 +12,7 @@ from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from typing import Optional, List
from decimal import Decimal
from database.database import get_db_session
from services.auth_service import get_current_active_user
@@ -167,8 +168,8 @@ async def _apply_order_items(
order_id=order.id,
product_id=item_data.product_id,
quantity=int(item_data.quantity),
unit_price=float(unit_price),
amount=float(item_data.quantity) * float(unit_price),
unit_price=Decimal(str(unit_price)),
amount=Decimal(str(item_data.quantity)) * Decimal(str(unit_price)),
remark=item_data.remark or None
)
db_session.add(item)
@@ -408,7 +409,7 @@ async def receive_purchase_order(
reference_id=order.id,
reference_no=order.order_no,
unit_price=item.unit_price,
total_amount=round(float(item.unit_price * receive_item.receive_quantity), 4),
total_amount=Decimal(str(item.unit_price * receive_item.receive_quantity)),
remark=payload.remark or f"采购单{order.order_no}到货入库",
operator_id=current_user.id
)
+41 -38
View File
@@ -14,6 +14,7 @@ from sqlalchemy import select, func, delete, update
from typing import Optional, List
from math import ceil
from pydantic import BaseModel, Field
from decimal import Decimal
from database.database import get_db_session
from services.auth_service import get_current_active_user
@@ -58,10 +59,10 @@ def _build_sales_order_response(order: SalesOrder, customer_name: str) -> SalesO
status=order.status,
production_status=order.production_status or "not_started",
production_no=order.production_no,
planned_material_cost=round(float(order.planned_material_cost or 0), 4),
actual_material_cost=round(float(order.actual_material_cost or 0), 4),
total_amount=order.total_amount,
received_amount=order.received_amount,
planned_material_cost=Decimal(str(order.planned_material_cost or 0)),
actual_material_cost=Decimal(str(order.actual_material_cost or 0)),
total_amount=Decimal(str(order.total_amount or 0)),
received_amount=Decimal(str(order.received_amount or 0)),
remark=order.remark,
created_at=order.created_at
)
@@ -89,10 +90,10 @@ async def _build_sales_order_detail_response(
status=order.status,
production_status=order.production_status or "not_started",
production_no=order.production_no,
planned_material_cost=round(float(order.planned_material_cost or 0), 4),
actual_material_cost=round(float(order.actual_material_cost or 0), 4),
total_amount=order.total_amount,
received_amount=order.received_amount,
planned_material_cost=Decimal(str(order.planned_material_cost or 0)),
actual_material_cost=Decimal(str(order.actual_material_cost or 0)),
total_amount=Decimal(str(order.total_amount or 0)),
received_amount=Decimal(str(order.received_amount or 0)),
remark=order.remark,
created_at=order.created_at,
items=[
@@ -101,8 +102,8 @@ async def _build_sales_order_detail_response(
product_id=item.product_id,
quantity=item.quantity,
delivered_quantity=item.delivered_quantity,
unit_price=item.unit_price,
amount=item.amount,
unit_price=Decimal(str(item.unit_price or 0)),
amount=Decimal(str(item.amount or 0)),
remark=item.remark
) for item in items
]
@@ -152,12 +153,12 @@ async def _build_material_plan(db_session: AsyncSession, order: SalesOrder) -> t
for order_item in order_items:
bom_items = bom_by_finished_id.get(int(order_item.product_id)) or []
for bom, material in bom_items:
qty = float(order_item.quantity) * float(bom.quantity or 0) * (1 + float(bom.loss_rate or 0))
entry = required_qty_map.setdefault(material.id, {"material": material, "required_qty": 0.0})
qty = Decimal(str(order_item.quantity)) * Decimal(str(bom.quantity or 0)) * (1 + Decimal(str(bom.loss_rate or 0)))
entry = required_qty_map.setdefault(material.id, {"material": material, "required_qty": Decimal("0")})
entry["required_qty"] += qty
if not required_qty_map:
return [], 0
return [], Decimal("0")
material_ids = list(required_qty_map.keys())
stock_result = await db_session.execute(
@@ -165,28 +166,28 @@ async def _build_material_plan(db_session: AsyncSession, order: SalesOrder) -> t
.where(Inventory.product_id.in_(material_ids))
.group_by(Inventory.product_id)
)
stock_map = {row[0]: int(row[1] or 0) for row in stock_result.all()}
stock_map = {row[0]: Decimal(str(row[1] or 0)) for row in stock_result.all()}
plan_items = []
planned_material_cost = 0.0
planned_material_cost = Decimal("0")
for material_id, entry in required_qty_map.items():
material = entry["material"]
required_qty = int(ceil(entry["required_qty"]))
available_qty = stock_map.get(material_id, 0)
shortage_qty = max(required_qty - available_qty, 0)
unit_cost = float(material.cost_price or 0)
required_cost = required_qty * unit_cost
available_qty = stock_map.get(material_id, Decimal("0"))
shortage_qty = max(required_qty - int(available_qty), 0)
unit_cost = Decimal(str(material.cost_price or 0))
required_cost = Decimal(str(required_qty)) * unit_cost
planned_material_cost += required_cost
plan_items.append(
ProductionMaterialPlanItemResponse(
material_id=material.id,
material_sku=material.sku,
material_name=material.name,
required_quantity=required_qty,
required_quantity=Decimal(str(required_qty)),
available_quantity=available_qty,
shortage_quantity=shortage_qty,
unit_cost=round(unit_cost, 4),
required_cost=round(required_cost, 4),
shortage_quantity=Decimal(str(shortage_qty)),
unit_cost=unit_cost,
required_cost=required_cost,
)
)
@@ -255,7 +256,7 @@ async def _issue_materials_for_order_creation(
reference_id=order.id,
reference_no=production_no,
unit_price=item.unit_cost,
total_amount=round(float(total_amount), 4),
total_amount=Decimal(str(total_amount)),
remark=f"销售单{order.order_no}创建时自动扣减物料",
operator_id=current_user.id
)
@@ -264,8 +265,8 @@ async def _issue_materials_for_order_creation(
order.production_no = production_no
order.production_status = "material_issued"
order.planned_material_cost = round(float(planned_material_cost), 4)
order.actual_material_cost = round(float(actual_material_cost), 4)
order.planned_material_cost = planned_material_cost
order.actual_material_cost = actual_material_cost
order.status = "manufacturing"
return movement_count, planned_material_cost, actual_material_cost
@@ -543,8 +544,10 @@ async def consume_materials(
"""记录销售订单的物料消耗"""
order, customer = await _get_sales_order_with_customer(db_session, order_id)
default_warehouse = await _get_default_warehouse(db_session)
# 计算总物料成本
total_cost = 0
total_cost = Decimal("0")
# 处理每个物料消耗项
for item in request.items:
@@ -556,14 +559,14 @@ async def consume_materials(
raise HTTPException(status_code=400, detail=f"只能消耗物料类型的产品: {material.name}")
# 计算成本
cost = material.cost_price * item.quantity
cost = Decimal(str(material.cost_price or 0)) * item.quantity
total_cost += cost
# 更新物料库存
inventory_result = await db_session.execute(
select(Inventory)
.where(Inventory.product_id == material.id)
.where(Inventory.warehouse_id == 1)
.where(Inventory.warehouse_id == default_warehouse.id)
)
inventory = inventory_result.scalar()
if inventory:
@@ -578,7 +581,7 @@ async def consume_materials(
# 记录物料消耗
movement = StockMovement(
product_id=material.id,
warehouse_id=1, # 默认仓库
warehouse_id=default_warehouse.id,
quantity=-item.quantity,
before_quantity=before_qty,
after_quantity=after_qty,
@@ -620,7 +623,7 @@ async def get_sales_order_production_plan(
order_no=order.order_no,
customer_name=customer.name,
production_no=production_no,
planned_material_cost=round(float(planned_material_cost), 4),
planned_material_cost=planned_material_cost,
items=plan_items,
)
@@ -688,7 +691,7 @@ async def issue_sales_order_materials(
reference_id=order.id,
reference_no=production_no,
unit_price=item.unit_cost,
total_amount=round(float(total_amount), 4),
total_amount=Decimal(str(total_amount)),
remark=payload.remark or f"销售单{order.order_no}按单生产领料",
operator_id=current_user.id
)
@@ -697,22 +700,22 @@ async def issue_sales_order_materials(
order.production_no = production_no
order.production_status = "material_issued"
order.planned_material_cost = round(float(planned_material_cost), 4)
order.actual_material_cost = round(float(actual_material_cost), 4)
order.planned_material_cost = planned_material_cost
order.actual_material_cost = actual_material_cost
if order.status == "draft":
order.status = "manufacturing"
await db_session.commit()
cost_deviation = round(float(actual_material_cost - planned_material_cost), 4)
cost_deviation_rate = round((cost_deviation / planned_material_cost), 6) if planned_material_cost > 1e-9 else 0.0
cost_deviation = actual_material_cost - planned_material_cost
cost_deviation_rate = (cost_deviation / planned_material_cost) if planned_material_cost > Decimal("1e-9") else Decimal("0")
return SalesOrderIssueResponse(
sales_order_id=order.id,
order_no=order.order_no,
production_no=production_no,
movement_count=movement_count,
planned_material_cost=round(float(planned_material_cost), 4),
actual_material_cost=round(float(actual_material_cost), 4),
planned_material_cost=planned_material_cost,
actual_material_cost=actual_material_cost,
cost_deviation=cost_deviation,
cost_deviation_rate=cost_deviation_rate,
production_status=order.production_status,
+32 -33
View File
@@ -1,6 +1,7 @@
from pydantic import BaseModel, Field
from typing import Optional, List, Literal
from datetime import datetime
from decimal import Decimal
TxnType = Literal["receipt", "payment"]
@@ -12,11 +13,11 @@ TxnStatus = Literal["confirmed", "voided"]
class FinanceAllocationCreate(BaseModel):
order_type: OrderType
order_id: int
allocated_amount: float = Field(gt=0)
allocated_amount: Decimal = Field(gt=0)
class FinanceTransactionCreate(BaseModel):
amount: float = Field(gt=0)
amount: Decimal = Field(gt=0)
txn_date: Optional[datetime] = None
method: str = "bank"
account_name: Optional[str] = None
@@ -36,13 +37,11 @@ class FinanceAllocationResponse(BaseModel):
id: int
order_type: str
order_id: int
allocated_amount: float
allocated_amount: Decimal
class Config:
from_attributes = True
class FinanceTransactionResponse(BaseModel):
id: int
@@ -50,7 +49,7 @@ class FinanceTransactionResponse(BaseModel):
txn_type: TxnType
partner_type: PartnerType
partner_id: int
amount: float
amount: Decimal
txn_date: datetime
method: str
account_name: Optional[str]
@@ -64,15 +63,15 @@ class FinanceTransactionResponse(BaseModel):
class FinanceSummaryResponse(BaseModel):
receivable_total: float
payable_total: float
monthly_receipt_total: float
monthly_payment_total: float
receivable_total: Decimal
payable_total: Decimal
monthly_receipt_total: Decimal
monthly_payment_total: Decimal
selected_year: int
selected_quarter: Optional[int] = None
period_label: str
period_receipt_total: float = 0
period_payment_total: float = 0
period_receipt_total: Decimal = Decimal("0")
period_payment_total: Decimal = Decimal("0")
overdue_receivable_count: int = 0
overdue_payable_count: int = 0
@@ -83,9 +82,9 @@ class ReceivableItemResponse(BaseModel):
customer_id: int
customer_name: str
order_date: datetime
total_amount: float
received_amount: float
receivable_amount: float
total_amount: Decimal
received_amount: Decimal
receivable_amount: Decimal
status: str
@@ -95,9 +94,9 @@ class PayableItemResponse(BaseModel):
supplier_id: int
supplier_name: str
order_date: datetime
total_amount: float
paid_amount: float
payable_amount: float
total_amount: Decimal
paid_amount: Decimal
payable_amount: Decimal
status: str
@@ -106,10 +105,10 @@ class PartnerStatementItemResponse(BaseModel):
partner_name: str
order_count: int
transaction_count: int
order_total: float
settled_total: float
transaction_total: float
outstanding_total: float
order_total: Decimal
settled_total: Decimal
transaction_total: Decimal
outstanding_total: Decimal
period_year: int
period_quarter: Optional[int] = None
@@ -119,10 +118,10 @@ class FinancePartnerStatementResponse(BaseModel):
year: int
quarter: Optional[int] = None
period_label: str
order_total: float
settled_total: float
transaction_total: float
outstanding_total: float
order_total: Decimal
settled_total: Decimal
transaction_total: Decimal
outstanding_total: Decimal
items: List[PartnerStatementItemResponse] = []
@@ -133,10 +132,10 @@ class PartnerProductStatementItemResponse(BaseModel):
product_sku: Optional[str] = None
product_name: str
order_count: int
order_quantity: float
order_amount: float
settled_amount: float
outstanding_amount: float
order_quantity: Decimal
order_amount: Decimal
settled_amount: Decimal
outstanding_amount: Decimal
period_year: int
period_quarter: Optional[int] = None
@@ -147,7 +146,7 @@ class FinancePartnerProductStatementResponse(BaseModel):
quarter: Optional[int] = None
period_label: str
partner_id: Optional[int] = None
order_amount_total: float
settled_amount_total: float
outstanding_amount_total: float
order_amount_total: Decimal
settled_amount_total: Decimal
outstanding_amount_total: Decimal
items: List[PartnerProductStatementItemResponse] = []
@@ -1,5 +1,6 @@
from pydantic import BaseModel
from typing import Optional
from decimal import Decimal
class InventoryResponse(BaseModel):
@@ -9,9 +10,9 @@ class InventoryResponse(BaseModel):
product_sku: str
warehouse_id: int
warehouse_name: str
quantity: int
locked_quantity: int
available_quantity: int
quantity: Decimal
locked_quantity: Decimal
available_quantity: Decimal
class Config:
from_attributes = True
@@ -20,14 +21,14 @@ class InventoryResponse(BaseModel):
class InventoryCreate(BaseModel):
product_id: int
warehouse_id: int
quantity: int = 0
locked_quantity: int = 0
quantity: Decimal = Decimal("0")
locked_quantity: Decimal = Decimal("0")
batch_number: Optional[str] = None
location: Optional[str] = None
class InventoryUpdate(BaseModel):
quantity: Optional[int] = None
locked_quantity: Optional[int] = None
quantity: Optional[Decimal] = None
locked_quantity: Optional[Decimal] = None
batch_number: Optional[str] = None
location: Optional[str] = None
+11 -10
View File
@@ -1,6 +1,7 @@
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime
from decimal import Decimal
class ProductCreate(BaseModel):
@@ -10,8 +11,8 @@ class ProductCreate(BaseModel):
category: Optional[str] = None
unit: str = "件"
item_type: str = "finished"
cost_price: float = 0
sale_price: float = 0
cost_price: Decimal = Decimal("0")
sale_price: Decimal = Decimal("0")
min_stock: int = 0
max_stock: int = 1000
@@ -24,11 +25,11 @@ class ProductResponse(BaseModel):
category: Optional[str]
unit: str
item_type: str
cost_price: float
sale_price: float
cost_price: Decimal
sale_price: Decimal
min_stock: int
max_stock: int
material_cost: float = 0
material_cost: Decimal = Decimal("0")
is_active: bool
created_at: datetime
@@ -38,7 +39,7 @@ class ProductResponse(BaseModel):
class ProductMaterialItemUpdate(BaseModel):
material_id: int
quantity: float
quantity: Decimal
class ProductBOMUpdate(BaseModel):
@@ -49,13 +50,13 @@ class ProductMaterialItemResponse(BaseModel):
material_id: int
material_sku: str
material_name: str
quantity: float
unit_cost: float
line_cost: float
quantity: Decimal
unit_cost: Decimal
line_cost: Decimal
class ProductBOMResponse(BaseModel):
product_id: int
product_name: str
total_material_cost: float
total_material_cost: Decimal
items: List[ProductMaterialItemResponse]
@@ -1,12 +1,13 @@
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime, date
from decimal import Decimal
class PurchaseOrderItemCreate(BaseModel):
product_id: int
quantity: int = Field(..., gt=0, description="采购数量")
unit_price: Optional[float] = Field(None, description="单价(可选,后端自动使用物料成本价格)")
unit_price: Optional[Decimal] = Field(None, description="单价(可选,后端自动使用物料成本价格)")
remark: Optional[str] = None
@@ -24,8 +25,8 @@ class PurchaseOrderResponse(BaseModel):
order_date: datetime
expected_date: Optional[date]
status: str
total_amount: float
paid_amount: float
total_amount: Decimal
paid_amount: Decimal
remark: Optional[str]
created_at: datetime
received_date: Optional[datetime]
@@ -42,8 +43,8 @@ class PurchaseOrderItemResponse(BaseModel):
product_name: str
quantity: int
received_quantity: int
unit_price: float
amount: float
unit_price: Decimal
amount: Decimal
remark: Optional[str] = None
@@ -1,6 +1,7 @@
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime, date
from decimal import Decimal
class SalesOrderItemCreate(BaseModel):
@@ -10,7 +11,7 @@ class SalesOrderItemCreate(BaseModel):
product_category: Optional[str] = None
product_unit: Optional[str] = "件"
quantity: int = Field(gt=0)
unit_price: float = Field(ge=0)
unit_price: Decimal = Field(ge=0)
remark: Optional[str] = None
@@ -33,10 +34,10 @@ class SalesOrderResponse(BaseModel):
status: str
production_status: str
production_no: Optional[str]
planned_material_cost: float
actual_material_cost: float
total_amount: float
received_amount: float
planned_material_cost: Decimal
actual_material_cost: Decimal
total_amount: Decimal
received_amount: Decimal
remark: Optional[str]
created_at: datetime
@@ -49,8 +50,8 @@ class SalesOrderItemResponse(BaseModel):
product_id: int
quantity: int
delivered_quantity: int
unit_price: float
amount: float
unit_price: Decimal
amount: Decimal
remark: Optional[str] = None
@@ -63,11 +64,11 @@ class ProductionMaterialPlanItemResponse(BaseModel):
material_id: int
material_sku: str
material_name: str
required_quantity: int
available_quantity: int
shortage_quantity: int
unit_cost: float
required_cost: float
required_quantity: Decimal
available_quantity: Decimal
shortage_quantity: Decimal
unit_cost: Decimal
required_cost: Decimal
class SalesOrderProductionPlanResponse(BaseModel):
@@ -75,7 +76,7 @@ class SalesOrderProductionPlanResponse(BaseModel):
order_no: str
customer_name: str
production_no: str
planned_material_cost: float
planned_material_cost: Decimal
items: List[ProductionMaterialPlanItemResponse]
@@ -90,10 +91,10 @@ class SalesOrderIssueResponse(BaseModel):
order_no: str
production_no: str
movement_count: int
planned_material_cost: float
actual_material_cost: float
cost_deviation: float
cost_deviation_rate: float
planned_material_cost: Decimal
actual_material_cost: Decimal
cost_deviation: Decimal
cost_deviation_rate: Decimal
production_status: str
@@ -1,6 +1,7 @@
from pydantic import BaseModel
from typing import Optional
from datetime import datetime
from decimal import Decimal
class StockMovementCreate(BaseModel):
@@ -8,8 +9,8 @@ class StockMovementCreate(BaseModel):
product_sku: Optional[str] = None
warehouse_id: int
movement_type: str
quantity: int
unit_price: Optional[float] = None
quantity: Decimal
unit_price: Optional[Decimal] = None
remark: Optional[str] = None
@@ -19,9 +20,9 @@ class StockMovementResponse(BaseModel):
product_sku: Optional[str]
product_name: str
movement_type: str
quantity: int
before_quantity: int
after_quantity: int
quantity: Decimal
before_quantity: Decimal
after_quantity: Decimal
reference_no: Optional[str]
remark: Optional[str]
created_at: datetime
+353 -1
View File
@@ -3,6 +3,7 @@ from fastapi import APIRouter, UploadFile, File, HTTPException, BackgroundTasks,
from typing import Optional, Dict, Any, List
import uuid
from datetime import datetime
import os
from pathlib import Path
from models.schemas import ProcessingStatus, create_task_info
@@ -18,6 +19,12 @@ from core.mold_generator import MoldCavityGenerator
from core.aluminum_foam_mold import AluminumFoamMoldGenerator
from core.mold_quality_inspector import AluminumFoamMoldQualityInspector
from core.mesh_generator import MeshGenerator
from core.cavity_layout_optimizer import CavityLayoutOptimizer
from core.mold_system_designer import MoldSystemDesigner
from core.side_action_designer import SideActionDesigner
from core.mold_cam import MoldCAMDesigner
from core.mold_machining import CollisionDetector, ToolpathOptimizer, EDMElectrodeDesigner, MachiningSimulator
from core.cad_exporter import CADExporter
from services.auth_service import get_current_active_user
from models.database import User
@@ -35,6 +42,15 @@ aluminum_foam_generator = AluminumFoamMoldGenerator(shrinkage_rate=0.015, draft_
# 铝泡沫模具质量检测器
mold_quality_inspector = AluminumFoamMoldQualityInspector()
mesh_generator = MeshGenerator(quality="medium")
cavity_layout_optimizer = CavityLayoutOptimizer()
mold_system_designer = MoldSystemDesigner()
side_action_designer = SideActionDesigner()
mold_cam_designer = MoldCAMDesigner()
collision_detector = CollisionDetector()
toolpath_optimizer = ToolpathOptimizer()
edm_designer = EDMElectrodeDesigner()
machining_simulator = MachiningSimulator()
cad_exporter = CADExporter()
tasks = {}
@@ -348,6 +364,341 @@ async def process_file_with_storage(
tasks[task_id]["completed_at"] = str(datetime.now())
# ==================== P3 新增 API ====================
@router.post("/optimize-layout")
async def optimize_cavity_layout(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""多型腔布局优化"""
body = await request.json()
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
cavity_count = body.get("cavity_count", 1)
mold_base_size = body.get("mold_base_size")
layout_type = body.get("layout_type", "auto")
if cavity_count < 1 or cavity_count > 64:
raise HTTPException(400, "型腔数量必须在 1-64 之间")
result = cavity_layout_optimizer.optimize_layout(
product_bbox=product_bbox,
cavity_count=cavity_count,
mold_base_size=mold_base_size,
layout_type=layout_type,
)
return {"status": "success", "data": result}
@router.post("/design-cooling")
async def design_cooling_system(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""冷却系统设计"""
body = await request.json()
mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200})
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
material = body.get("material", "ABS")
cavity_count = body.get("cavity_count", 1)
cycle_time_target = body.get("cycle_time_target")
from core.mold_system_designer import CoolingSystemDesigner
designer = CoolingSystemDesigner()
result = designer.design_cooling_system(
mold_size=mold_size,
product_bbox=product_bbox,
material=material,
cavity_count=cavity_count,
cycle_time_target=cycle_time_target,
)
return {"status": "success", "data": result}
@router.post("/design-gating")
async def design_gating_system(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""浇注系统设计"""
body = await request.json()
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
material = body.get("material", "ABS")
cavity_count = body.get("cavity_count", 1)
gate_type = body.get("gate_type", "auto")
layout_positions = body.get("layout_positions")
from core.mold_system_designer import GatingSystemDesigner
designer = GatingSystemDesigner()
result = designer.design_gating_system(
product_bbox=product_bbox,
material=material,
cavity_count=cavity_count,
gate_type=gate_type,
layout_positions=layout_positions,
)
return {"status": "success", "data": result}
@router.post("/design-mold-system")
async def design_complete_mold_system(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""综合模具系统设计(冷却+浇注)"""
body = await request.json()
mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200})
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
material = body.get("material", "ABS")
cavity_count = body.get("cavity_count", 1)
gate_type = body.get("gate_type", "auto")
cycle_time_target = body.get("cycle_time_target")
layout_positions = body.get("layout_positions")
result = mold_system_designer.design_complete_system(
mold_size=mold_size,
product_bbox=product_bbox,
material=material,
cavity_count=cavity_count,
gate_type=gate_type,
cycle_time_target=cycle_time_target,
layout_positions=layout_positions,
)
return {"status": "success", "data": result}
@router.post("/ai-parting-detect")
async def ai_parting_surface_detect(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""AI 分型面检测"""
body = await request.json()
task_id = body.get("task_id")
if not task_id or task_id not in tasks:
raise HTTPException(404, "任务不存在")
task_data = tasks[task_id]
geometry_data = task_data.get("geometry_data")
if not geometry_data:
raise HTTPException(400, "该任务尚未完成几何分析")
from core.ai_parting_detector import AIPartingSurfaceDetectorV2
detector = AIPartingSurfaceDetectorV2(use_gnn=True)
result = detector._detect_with_geometry(None, geometry_data)
return {"status": "success", "data": result}
@router.post("/detect-undercuts")
async def detect_undercuts(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""倒扣区域检测与滑块/斜顶机构设计"""
body = await request.json()
task_id = body.get("task_id")
parting_direction = body.get("parting_direction", [0, 0, 1])
mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200})
if not task_id or task_id not in tasks:
raise HTTPException(404, "任务不存在")
task_data = tasks[task_id]
geometry_data = task_data.get("geometry_data")
if not geometry_data:
raise HTTPException(400, "该任务尚未完成几何分析")
result = side_action_designer.analyze_and_design(
shape=None, parting_direction=parting_direction, mold_size=mold_size
)
return {"status": "success", "data": result}
@router.post("/design-cam")
async def design_mold_cam(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""模具CAM刀路设计"""
body = await request.json()
cavity_bbox = body.get("cavity_bbox", {"dimensions": [100, 100, 50], "min": [-50, -50, -25], "max": [50, 50, 25]})
stock_bbox = body.get("stock_bbox", {"dimensions": [150, 150, 100], "min": [-75, -75, -50], "max": [75, 75, 50]})
mold_steel = body.get("mold_steel", "P20")
surface_quality = body.get("surface_quality", "standard")
controller = body.get("controller", "fanuc")
result = mold_cam_designer.design_mold_cam(
cavity_bbox=cavity_bbox,
stock_bbox=stock_bbox,
mold_steel=mold_steel,
surface_quality=surface_quality,
controller=controller,
)
return {"status": "success", "data": result}
@router.post("/check-collision")
async def check_toolpath_collision(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""刀路碰撞检测"""
body = await request.json()
toolpath_points = body.get("toolpath_points", [[0, 0, 50], [10, 10, -5], [20, 20, -10]])
tool = body.get("tool", {"diameter": 10, "flute_length": 30, "shank_diameter": 10})
stock_bbox = body.get("stock_bbox", {"min": [-50, -50, -25], "max": [50, 50, 25]})
clamp_positions = body.get("clamp_positions")
result = collision_detector.check_toolpath_safety(
toolpath_points, tool, stock_bbox, clamp_positions
)
return {"status": "success", "data": result}
@router.post("/optimize-toolpath")
async def optimize_toolpath(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""刀路优化"""
body = await request.json()
toolpath_points = body.get("toolpath_points", [[0, 0, 50], [10, 10, -5], [20, 20, -10]])
cutting_params = body.get("cutting_params", {"feed_rate_mm_min": 500})
stock_bbox = body.get("stock_bbox")
result = toolpath_optimizer.optimize_toolpath(
toolpath_points, cutting_params, stock_bbox
)
return {"status": "success", "data": result}
@router.post("/design-electrodes")
async def design_edm_electrodes(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""EDM电极设计"""
body = await request.json()
undercut_regions = body.get("undercut_regions", [{"center": [0, 0, 0], "area": 100, "type": "undercut"}])
cavity_bbox = body.get("cavity_bbox", {"dimensions": [100, 100, 50]})
material = body.get("material", "copper")
spark_gap = body.get("spark_gap", 0.05)
overburn = body.get("overburn", 0.1)
result = edm_designer.design_electrodes(
undercut_regions, cavity_bbox, material, spark_gap, overburn
)
return {"status": "success", "data": result}
@router.post("/simulate-machining")
async def simulate_machining(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""加工仿真"""
body = await request.json()
operations = body.get("operations", [{"strategy": "z_level_roughing", "levels": [{"z": -5}]}])
stock_bbox = body.get("stock_bbox", {"dimensions": [100, 100, 50], "min": [-50, -50, -25], "max": [50, 50, 25]})
resolution = body.get("resolution", 2.0)
result = machining_simulator.simulate_machining(
operations, stock_bbox, resolution
)
return {"status": "success", "data": result}
# ==================== CAD 导出 API ====================
@router.post("/export-mold")
async def export_mold_results(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""导出模具设计结果(STEP/IGES/STL/BRep)"""
body = await request.json()
task_id = body.get("task_id")
formats = body.get("formats", ["step", "stl"])
components = body.get("components", ["cavity", "core"])
if not task_id or task_id not in tasks:
raise HTTPException(404, "任务不存在")
task_data = tasks[task_id]
cavity_shapes = task_data.get("cavity_shapes")
if not cavity_shapes:
raise HTTPException(400, "该任务尚未完成模具生成或形状数据不可用")
base_filename = Path(task_data.get("filename", f"mold_{task_id}")).stem
result = cad_exporter.export_mold_results(
cavity_data=cavity_shapes,
base_filename=base_filename,
formats=formats,
components=components,
)
return {"status": "success", "data": result}
@router.get("/export-download/{filepath:path}")
async def download_export_file(
filepath: str,
current_user: User = Depends(get_current_active_user),
):
"""下载导出的CAD文件"""
from fastapi.responses import FileResponse
full_path = os.path.join(cad_exporter.output_dir, filepath)
if not os.path.exists(full_path):
raise HTTPException(404, "文件不存在")
if not os.path.abspath(full_path).startswith(os.path.abspath(cad_exporter.output_dir)):
raise HTTPException(403, "禁止访问")
media_types = {
".step": "application/step",
".stp": "application/step",
".iges": "application/iges",
".igs": "application/iges",
".stl": "model/stl",
".brep": "application/octet-stream",
}
ext = Path(full_path).suffix.lower()
media_type = media_types.get(ext, "application/octet-stream")
return FileResponse(
full_path,
media_type=media_type,
filename=os.path.basename(full_path),
)
@router.get("/export-recommendations")
async def get_export_recommendations(
target: str = "ug",
current_user: User = Depends(get_current_active_user),
):
"""获取导出格式建议(UG/FreeCAD/SolidWorks)"""
result = cad_exporter.get_export_recommendations(target)
return {"status": "success", "data": result}
async def _save_analysis_metrics(session, stp_file_id, analysis_result):
"""保存分析指标到数据库"""
from models.database import AnalysisMetrics
@@ -774,7 +1125,7 @@ async def process_file_core(
)
# 9. 分析模具设计
analysis_result = geometry_analyzer.analyze_mold_design(geometry_data)
analysis_result = geometry_analyzer.analyze_mold_design(geometry_data, shape=shape)
# 9.5 保存完整的分析结果到数据库
if analysis_result:
@@ -838,6 +1189,7 @@ async def process_file_core(
tasks[task_id]["geometry_data"] = geometry_data
tasks[task_id]["analysis_result"] = analysis_result
tasks[task_id]["cavity_data"] = detailed_cavity_json
tasks[task_id]["cavity_shapes"] = cavity_result
tasks[task_id]["key_info"] = detailed_cavity_json # 传递完整数据给前端
tasks[task_id]["html_file"] = f"/html/{Path(html_file_path).name}" # 只使用文件名
tasks[task_id]["verification"] = verification_result # 添加验证结果