x
This commit is contained in:
@@ -35,11 +35,13 @@ from .schemas import (
|
|||||||
SalesOrderProductionPlanResponse,
|
SalesOrderProductionPlanResponse,
|
||||||
ProductionMaterialPlanItemResponse,
|
ProductionMaterialPlanItemResponse,
|
||||||
SalesOrderIssueRequest,
|
SalesOrderIssueRequest,
|
||||||
SalesOrderIssueResponse
|
SalesOrderIssueResponse,
|
||||||
|
SalesOrderStatusUpdate
|
||||||
)
|
)
|
||||||
from .utils import generate_order_no
|
from .utils import generate_order_no
|
||||||
|
|
||||||
router = APIRouter(prefix="/sales-orders", tags=["销售订单"])
|
router = APIRouter(prefix="/sales-orders", tags=["销售订单"])
|
||||||
|
VALID_ORDER_STATUSES = {"manufacturing", "delivered", "paid"}
|
||||||
|
|
||||||
|
|
||||||
def _build_sales_order_response(order: SalesOrder, customer_name: str) -> SalesOrderResponse:
|
def _build_sales_order_response(order: SalesOrder, customer_name: str) -> SalesOrderResponse:
|
||||||
@@ -317,17 +319,40 @@ async def _apply_order_items(
|
|||||||
) -> float:
|
) -> float:
|
||||||
total_amount = 0.0
|
total_amount = 0.0
|
||||||
for item_data in order_data.items:
|
for item_data in order_data.items:
|
||||||
product_result = await db_session.execute(
|
product = None
|
||||||
select(Product).where(Product.id == item_data.product_id, Product.is_active == True)
|
if item_data.product_id is not None:
|
||||||
)
|
product_result = await db_session.execute(
|
||||||
product = product_result.scalar_one_or_none()
|
select(Product).where(Product.id == item_data.product_id, Product.is_active == True)
|
||||||
if not product:
|
)
|
||||||
raise HTTPException(status_code=400, detail=f"产品不存在: {item_data.product_id}")
|
product = product_result.scalar_one_or_none()
|
||||||
|
if not product:
|
||||||
|
raise HTTPException(status_code=400, detail=f"产品不存在: {item_data.product_id}")
|
||||||
|
else:
|
||||||
|
if not item_data.product_sku or not item_data.product_name:
|
||||||
|
raise HTTPException(status_code=400, detail="请提供产品ID,或提供产品SKU与产品名称")
|
||||||
|
by_sku_result = await db_session.execute(
|
||||||
|
select(Product).where(Product.sku == item_data.product_sku, Product.is_active == True)
|
||||||
|
)
|
||||||
|
product = by_sku_result.scalar_one_or_none()
|
||||||
|
if not product:
|
||||||
|
product = Product(
|
||||||
|
sku=item_data.product_sku,
|
||||||
|
name=item_data.product_name,
|
||||||
|
category=item_data.product_category,
|
||||||
|
unit=item_data.product_unit or "件",
|
||||||
|
item_type="finished",
|
||||||
|
cost_price=0,
|
||||||
|
sale_price=item_data.unit_price or 0,
|
||||||
|
min_stock=0,
|
||||||
|
max_stock=0
|
||||||
|
)
|
||||||
|
db_session.add(product)
|
||||||
|
await db_session.flush()
|
||||||
if product.item_type != "finished":
|
if product.item_type != "finished":
|
||||||
raise HTTPException(status_code=400, detail=f"销售单仅允许成品: {product.name}")
|
raise HTTPException(status_code=400, detail=f"销售单仅允许成品: {product.name}")
|
||||||
item = SalesOrderItem(
|
item = SalesOrderItem(
|
||||||
order_id=order.id,
|
order_id=order.id,
|
||||||
product_id=item_data.product_id,
|
product_id=product.id,
|
||||||
quantity=item_data.quantity,
|
quantity=item_data.quantity,
|
||||||
unit_price=item_data.unit_price,
|
unit_price=item_data.unit_price,
|
||||||
amount=item_data.quantity * item_data.unit_price,
|
amount=item_data.quantity * item_data.unit_price,
|
||||||
@@ -377,7 +402,7 @@ async def create_sales_order(
|
|||||||
delivery_date=order_data.delivery_date,
|
delivery_date=order_data.delivery_date,
|
||||||
remark=order_data.remark,
|
remark=order_data.remark,
|
||||||
operator_id=current_user.id,
|
operator_id=current_user.id,
|
||||||
status="draft"
|
status="manufacturing"
|
||||||
)
|
)
|
||||||
db_session.add(order)
|
db_session.add(order)
|
||||||
await db_session.flush()
|
await db_session.flush()
|
||||||
@@ -421,6 +446,7 @@ async def update_sales_order(
|
|||||||
order.production_no = None
|
order.production_no = None
|
||||||
order.planned_material_cost = 0
|
order.planned_material_cost = 0
|
||||||
order.actual_material_cost = 0
|
order.actual_material_cost = 0
|
||||||
|
order.status = "manufacturing"
|
||||||
|
|
||||||
order.total_amount = await _apply_order_items(db_session, order, order_data)
|
order.total_amount = await _apply_order_items(db_session, order, order_data)
|
||||||
await _issue_materials_for_order_creation(db_session, order, current_user)
|
await _issue_materials_for_order_creation(db_session, order, current_user)
|
||||||
@@ -433,6 +459,23 @@ async def update_sales_order(
|
|||||||
return _build_sales_order_response(order, customer_name)
|
return _build_sales_order_response(order, customer_name)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{order_id}/status", response_model=SalesOrderResponse)
|
||||||
|
async def update_sales_order_status(
|
||||||
|
order_id: int,
|
||||||
|
payload: SalesOrderStatusUpdate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
if payload.status not in VALID_ORDER_STATUSES:
|
||||||
|
raise HTTPException(status_code=400, detail="订单状态必须为 manufacturing、delivered、paid")
|
||||||
|
|
||||||
|
order, customer = await _get_sales_order_with_customer(db_session, order_id)
|
||||||
|
order.status = payload.status
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(order)
|
||||||
|
return _build_sales_order_response(order, customer.name)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{order_id}")
|
@router.delete("/{order_id}")
|
||||||
async def delete_sales_order(
|
async def delete_sales_order(
|
||||||
order_id: int,
|
order_id: int,
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ from .sales_order_schemas import (
|
|||||||
ProductionMaterialPlanItemResponse,
|
ProductionMaterialPlanItemResponse,
|
||||||
SalesOrderProductionPlanResponse,
|
SalesOrderProductionPlanResponse,
|
||||||
SalesOrderIssueRequest,
|
SalesOrderIssueRequest,
|
||||||
SalesOrderIssueResponse
|
SalesOrderIssueResponse,
|
||||||
|
SalesOrderStatusUpdate
|
||||||
)
|
)
|
||||||
from .finance_schemas import (
|
from .finance_schemas import (
|
||||||
FinanceAllocationCreate,
|
FinanceAllocationCreate,
|
||||||
@@ -59,7 +60,7 @@ __all__ = [
|
|||||||
"PurchaseOrderItemResponse", "PurchaseOrderDetailResponse", "PurchaseOrderReceiveItem", "PurchaseOrderReceiveRequest",
|
"PurchaseOrderItemResponse", "PurchaseOrderDetailResponse", "PurchaseOrderReceiveItem", "PurchaseOrderReceiveRequest",
|
||||||
"SalesOrderCreate", "SalesOrderResponse", "SalesOrderItemCreate", "SalesOrderItemResponse", "SalesOrderDetailResponse",
|
"SalesOrderCreate", "SalesOrderResponse", "SalesOrderItemCreate", "SalesOrderItemResponse", "SalesOrderDetailResponse",
|
||||||
"ProductionMaterialPlanItemResponse", "SalesOrderProductionPlanResponse",
|
"ProductionMaterialPlanItemResponse", "SalesOrderProductionPlanResponse",
|
||||||
"SalesOrderIssueRequest", "SalesOrderIssueResponse",
|
"SalesOrderIssueRequest", "SalesOrderIssueResponse", "SalesOrderStatusUpdate",
|
||||||
"FinanceAllocationCreate", "FinanceTransactionCreate",
|
"FinanceAllocationCreate", "FinanceTransactionCreate",
|
||||||
"ReceiptCreate", "PaymentCreate",
|
"ReceiptCreate", "PaymentCreate",
|
||||||
"FinanceAllocationResponse", "FinanceTransactionResponse",
|
"FinanceAllocationResponse", "FinanceTransactionResponse",
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ from datetime import datetime
|
|||||||
|
|
||||||
|
|
||||||
class SalesOrderItemCreate(BaseModel):
|
class SalesOrderItemCreate(BaseModel):
|
||||||
product_id: int
|
product_id: Optional[int] = None
|
||||||
|
product_sku: Optional[str] = None
|
||||||
|
product_name: Optional[str] = None
|
||||||
|
product_category: Optional[str] = None
|
||||||
|
product_unit: Optional[str] = "件"
|
||||||
quantity: int
|
quantity: int
|
||||||
unit_price: float
|
unit_price: float
|
||||||
remark: Optional[str] = None
|
remark: Optional[str] = None
|
||||||
@@ -88,3 +92,7 @@ class SalesOrderIssueResponse(BaseModel):
|
|||||||
cost_deviation: float
|
cost_deviation: float
|
||||||
cost_deviation_rate: float
|
cost_deviation_rate: float
|
||||||
production_status: str
|
production_status: str
|
||||||
|
|
||||||
|
|
||||||
|
class SalesOrderStatusUpdate(BaseModel):
|
||||||
|
status: str
|
||||||
|
|||||||
+51
-15
@@ -1808,7 +1808,7 @@ const InventoryView = {
|
|||||||
if (item) {
|
if (item) {
|
||||||
if (type === 'salesOrder') {
|
if (type === 'salesOrder') {
|
||||||
await loadCustomers();
|
await loadCustomers();
|
||||||
await loadProducts();
|
await loadFinishedProducts();
|
||||||
const detail = await apiRequest(`/api/sales-orders/${item.id}`);
|
const detail = await apiRequest(`/api/sales-orders/${item.id}`);
|
||||||
state.form = {
|
state.form = {
|
||||||
customer_id: detail.customer_id,
|
customer_id: detail.customer_id,
|
||||||
@@ -1816,6 +1816,8 @@ const InventoryView = {
|
|||||||
remark: detail.remark || '',
|
remark: detail.remark || '',
|
||||||
items: (detail.items || []).map(line => ({
|
items: (detail.items || []).map(line => ({
|
||||||
product_id: line.product_id,
|
product_id: line.product_id,
|
||||||
|
product_sku: '',
|
||||||
|
product_name: '',
|
||||||
quantity: line.quantity,
|
quantity: line.quantity,
|
||||||
unit_price: line.unit_price,
|
unit_price: line.unit_price,
|
||||||
remark: line.remark || ''
|
remark: line.remark || ''
|
||||||
@@ -1879,13 +1881,15 @@ const InventoryView = {
|
|||||||
}
|
}
|
||||||
if (type === 'salesOrder') {
|
if (type === 'salesOrder') {
|
||||||
await loadCustomers();
|
await loadCustomers();
|
||||||
await loadProducts();
|
await loadFinishedProducts();
|
||||||
state.form = {
|
state.form = {
|
||||||
customer_id: state.customers[0]?.id || null,
|
customer_id: state.customers[0]?.id || null,
|
||||||
delivery_date: '',
|
delivery_date: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
items: [{
|
items: [{
|
||||||
product_id: state.products.find(p => p.item_type === 'finished')?.id || null,
|
product_id: state.finishedProducts[0]?.id || null,
|
||||||
|
product_sku: '',
|
||||||
|
product_name: '',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
unit_price: 0,
|
unit_price: 0,
|
||||||
remark: ''
|
remark: ''
|
||||||
@@ -2105,7 +2109,9 @@ const InventoryView = {
|
|||||||
const addSalesOrderItem = () => {
|
const addSalesOrderItem = () => {
|
||||||
state.form.items = state.form.items || [];
|
state.form.items = state.form.items || [];
|
||||||
state.form.items.push({
|
state.form.items.push({
|
||||||
product_id: state.products.find(p => p.item_type === 'finished')?.id || null,
|
product_id: state.finishedProducts[0]?.id || null,
|
||||||
|
product_sku: '',
|
||||||
|
product_name: '',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
unit_price: 0,
|
unit_price: 0,
|
||||||
remark: ''
|
remark: ''
|
||||||
@@ -2126,7 +2132,16 @@ const InventoryView = {
|
|||||||
customer_id: state.form.customer_id,
|
customer_id: state.form.customer_id,
|
||||||
delivery_date: state.form.delivery_date ? new Date(state.form.delivery_date).toISOString() : null,
|
delivery_date: state.form.delivery_date ? new Date(state.form.delivery_date).toISOString() : null,
|
||||||
remark: state.form.remark,
|
remark: state.form.remark,
|
||||||
items: state.form.items
|
items: state.form.items.map(item => ({
|
||||||
|
product_id: item.product_id || null,
|
||||||
|
product_sku: item.product_id ? null : (item.product_sku || null),
|
||||||
|
product_name: item.product_id ? null : (item.product_name || null),
|
||||||
|
product_category: null,
|
||||||
|
product_unit: '件',
|
||||||
|
quantity: item.quantity,
|
||||||
|
unit_price: item.unit_price,
|
||||||
|
remark: item.remark || ''
|
||||||
|
}))
|
||||||
};
|
};
|
||||||
if (state.editingItem) {
|
if (state.editingItem) {
|
||||||
await apiRequest(`/api/sales-orders/${state.editingItem.id}`, {
|
await apiRequest(`/api/sales-orders/${state.editingItem.id}`, {
|
||||||
@@ -2143,6 +2158,7 @@ const InventoryView = {
|
|||||||
}
|
}
|
||||||
closeModal();
|
closeModal();
|
||||||
loadProductionOrders();
|
loadProductionOrders();
|
||||||
|
loadFinishedProducts();
|
||||||
loadInventory();
|
loadInventory();
|
||||||
loadMovements();
|
loadMovements();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -2150,6 +2166,19 @@ const InventoryView = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateSalesOrderStatus = async (order, targetStatus) => {
|
||||||
|
try {
|
||||||
|
await apiRequest(`/api/sales-orders/${order.id}/status`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({ status: targetStatus })
|
||||||
|
});
|
||||||
|
addNotification('订单状态已更新', 'success');
|
||||||
|
loadProductionOrders();
|
||||||
|
} catch (e) {
|
||||||
|
handleApiError(e, '更新订单状态');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const deleteSalesOrder = async (orderId) => {
|
const deleteSalesOrder = async (orderId) => {
|
||||||
if (!confirm('确定删除这个销售订单吗?系统会自动回补已扣减物料。')) return;
|
if (!confirm('确定删除这个销售订单吗?系统会自动回补已扣减物料。')) return;
|
||||||
try {
|
try {
|
||||||
@@ -2288,6 +2317,7 @@ const InventoryView = {
|
|||||||
removeSalesOrderItem,
|
removeSalesOrderItem,
|
||||||
saveSalesOrder,
|
saveSalesOrder,
|
||||||
deleteSalesOrder,
|
deleteSalesOrder,
|
||||||
|
updateSalesOrderStatus,
|
||||||
addPurchaseOrderItem,
|
addPurchaseOrderItem,
|
||||||
removePurchaseOrderItem,
|
removePurchaseOrderItem,
|
||||||
savePurchaseOrder,
|
savePurchaseOrder,
|
||||||
@@ -2623,10 +2653,9 @@ const InventoryView = {
|
|||||||
<tr>
|
<tr>
|
||||||
<th>销售单</th>
|
<th>销售单</th>
|
||||||
<th>客户</th>
|
<th>客户</th>
|
||||||
<th>生产单号</th>
|
<th>订单金额</th>
|
||||||
<th>生产状态</th>
|
<th>预计交付</th>
|
||||||
<th>计划物料成本</th>
|
<th>订单状态</th>
|
||||||
<th>实际领料成本</th>
|
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -2634,12 +2663,14 @@ const InventoryView = {
|
|||||||
<tr v-for="order in state.productionOrders" :key="'production-order-' + order.id">
|
<tr v-for="order in state.productionOrders" :key="'production-order-' + order.id">
|
||||||
<td>{{ order.order_no }}</td>
|
<td>{{ order.order_no }}</td>
|
||||||
<td>{{ order.customer_name }}</td>
|
<td>{{ order.customer_name }}</td>
|
||||||
<td>{{ order.production_no || '-' }}</td>
|
<td>{{ formatCurrency(order.total_amount || 0) }}</td>
|
||||||
<td>{{ order.production_status || '-' }}</td>
|
<td>{{ order.delivery_date ? formatDateTime(order.delivery_date) : '-' }}</td>
|
||||||
<td>{{ formatCurrency(order.planned_material_cost || 0) }}</td>
|
<td>{{ order.status === 'manufacturing' ? '制造中' : order.status === 'delivered' ? '已交付' : order.status === 'paid' ? '已收款' : order.status }}</td>
|
||||||
<td>{{ formatCurrency(order.actual_material_cost || 0) }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<div class="action-btns">
|
<div class="action-btns">
|
||||||
|
<button class="btn btn-sm btn-secondary" @click="updateSalesOrderStatus(order, 'manufacturing')">制造中</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" @click="updateSalesOrderStatus(order, 'delivered')">已交付</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" @click="updateSalesOrderStatus(order, 'paid')">已收款</button>
|
||||||
<button class="btn btn-sm btn-secondary" @click="openModal('salesOrder', order)">编辑</button>
|
<button class="btn btn-sm btn-secondary" @click="openModal('salesOrder', order)">编辑</button>
|
||||||
<button class="btn btn-sm btn-danger" @click="deleteSalesOrder(order.id)">删除</button>
|
<button class="btn btn-sm btn-danger" @click="deleteSalesOrder(order.id)">删除</button>
|
||||||
<button class="btn btn-sm btn-secondary" @click="loadOrderProductionPlan(order.id)">领料建议</button>
|
<button class="btn btn-sm btn-secondary" @click="loadOrderProductionPlan(order.id)">领料建议</button>
|
||||||
@@ -2994,11 +3025,16 @@ const InventoryView = {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(line, index) in state.form.items" :key="'sales-order-line-' + index">
|
<tr v-for="(line, index) in state.form.items" :key="'sales-order-line-' + index">
|
||||||
<td>
|
<td>
|
||||||
<select v-model.number="line.product_id" class="form-input" required>
|
<select v-model.number="line.product_id" class="form-input">
|
||||||
<option v-for="product in state.products.filter(p => p.item_type === 'finished')" :key="'sales-order-product-' + product.id" :value="product.id">
|
<option :value="null">配置新产品</option>
|
||||||
|
<option v-for="product in state.finishedProducts" :key="'sales-order-product-' + product.id" :value="product.id">
|
||||||
{{ product.sku }} - {{ product.name }}
|
{{ product.sku }} - {{ product.name }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
<div v-if="!line.product_id" style="margin-top:8px; display:flex; gap:8px;">
|
||||||
|
<input v-model="line.product_sku" class="form-input" placeholder="新产品SKU" />
|
||||||
|
<input v-model="line.product_name" class="form-input" placeholder="新产品名称" />
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td><input v-model.number="line.quantity" type="number" min="1" class="form-input" required /></td>
|
<td><input v-model.number="line.quantity" type="number" min="1" class="form-input" required /></td>
|
||||||
<td><input v-model.number="line.unit_price" type="number" min="0" step="0.01" class="form-input" required /></td>
|
<td><input v-model.number="line.unit_price" type="number" min="0" step="0.01" class="form-input" required /></td>
|
||||||
|
|||||||
Reference in New Issue
Block a user