This commit is contained in:
2026-06-23 09:38:31 +08:00
parent f6d8829071
commit 6b3188e693
11 changed files with 197 additions and 47 deletions
@@ -8,6 +8,7 @@ const { state, loadCustomers } = useInventory()
const showModal = ref(false) const showModal = ref(false)
const editingItem = ref<any>(null) const editingItem = ref<any>(null)
const saving = ref(false)
const form = reactive({ const form = reactive({
name: '', name: '',
contact_person: '', contact_person: '',
@@ -37,6 +38,12 @@ function editCustomer(item: any) {
} }
async function save() { async function save() {
if (saving.value) return
if (!form.name || !form.name.trim()) {
addNotification('请输入客户名称', 'warning')
return
}
saving.value = true
try { try {
const payload = { const payload = {
name: form.name, name: form.name,
@@ -62,6 +69,8 @@ async function save() {
loadCustomers() loadCustomers()
} catch (e) { } catch (e) {
handleApiError(e, '保存客户') handleApiError(e, '保存客户')
} finally {
saving.value = false
} }
} }
@@ -128,8 +137,8 @@ onMounted(() => { loadCustomers() })
</t-form-item> </t-form-item>
</t-form> </t-form>
<template #footer> <template #footer>
<t-button @click="showModal = false">取消</t-button> <t-button @click="showModal = false" :disabled="saving">取消</t-button>
<t-button type="primary" @click="save">保存</t-button> <t-button type="primary" :loading="saving" :disabled="saving" @click="save">保存</t-button>
</template> </template>
</t-dialog> </t-dialog>
</div> </div>
@@ -8,6 +8,7 @@ const { state, loadInventory, ensureStockBaseData } = useInventory()
const showModal = ref(false) const showModal = ref(false)
const editingItem = ref<any>(null) const editingItem = ref<any>(null)
const saving = ref(false)
const form = reactive({ const form = reactive({
product_id: null as number | null, product_id: null as number | null,
warehouse_id: null as number | null, warehouse_id: null as number | null,
@@ -42,6 +43,24 @@ function openEdit(item: any) {
} }
async function save() { async function save() {
if (saving.value) return
if (!form.product_id) {
addNotification('请选择物料', 'warning')
return
}
if (!form.warehouse_id) {
addNotification('请选择仓库', 'warning')
return
}
if (form.quantity < 0) {
addNotification('数量不能为负数', 'warning')
return
}
if (form.locked_quantity > form.quantity) {
addNotification('锁定数量不能大于库存数量', 'warning')
return
}
saving.value = true
try { try {
const payload = { const payload = {
product_id: form.product_id, product_id: form.product_id,
@@ -68,6 +87,8 @@ async function save() {
loadInventory() loadInventory()
} catch (e) { } catch (e) {
handleApiError(e, '保存物料库存') handleApiError(e, '保存物料库存')
} finally {
saving.value = false
} }
} }
@@ -139,8 +160,8 @@ onMounted(() => { loadInventory() })
</t-form-item> </t-form-item>
</t-form> </t-form>
<template #footer> <template #footer>
<t-button @click="showModal = false">取消</t-button> <t-button @click="showModal = false" :disabled="saving">取消</t-button>
<t-button type="primary" @click="save">保存</t-button> <t-button type="primary" :loading="saving" :disabled="saving" @click="save">保存</t-button>
</template> </template>
</t-dialog> </t-dialog>
</div> </div>
@@ -14,10 +14,12 @@ const {
const showModal = ref(false) const showModal = ref(false)
const editingItem = ref<any>(null) const editingItem = ref<any>(null)
const saving = ref(false)
const form = ref<any>({}) const form = ref<any>({})
const showRestockModal = ref(false) const showRestockModal = ref(false)
const restockItems = ref<any[]>([]) const restockItems = ref<any[]>([])
const restockSupplierId = ref<number | null>(null) const restockSupplierId = ref<number | null>(null)
const restocking = ref(false)
function openCreateMaterial() { function openCreateMaterial() {
editingItem.value = null editingItem.value = null
@@ -42,6 +44,7 @@ async function editMaterial(product: any) {
} }
async function saveProduct() { async function saveProduct() {
if (saving.value) return
if (!form.value.sku) { if (!form.value.sku) {
addNotification('请输入SKU', 'warning') addNotification('请输入SKU', 'warning')
return return
@@ -50,6 +53,7 @@ async function saveProduct() {
addNotification('请输入名称', 'warning') addNotification('请输入名称', 'warning')
return return
} }
saving.value = true
try { try {
if (editingItem.value) { if (editingItem.value) {
await apiRequest(`/api/products/${editingItem.value.id}`, { await apiRequest(`/api/products/${editingItem.value.id}`, {
@@ -70,6 +74,8 @@ async function saveProduct() {
loadMaterials() loadMaterials()
} catch (e) { } catch (e) {
handleApiError(e, '保存物料') handleApiError(e, '保存物料')
} finally {
saving.value = false
} }
} }
@@ -111,6 +117,11 @@ const restockTotalAmount = computed(() => {
}) })
async function saveRestock() { async function saveRestock() {
if (restocking.value) return
if (!restockSupplierId.value) {
addNotification('请选择供应商', 'warning')
return
}
if (!restockItems.value || restockItems.value.length === 0) { if (!restockItems.value || restockItems.value.length === 0) {
addNotification('请至少添加一个补货物料', 'warning') addNotification('请至少添加一个补货物料', 'warning')
return return
@@ -130,9 +141,10 @@ async function saveRestock() {
return return
} }
} }
restocking.value = true
try { try {
const payload = { const payload = {
supplier_id: restockSupplierId.value || state.suppliers[0]?.id || null, supplier_id: restockSupplierId.value,
expected_date: new Date().toISOString().split('T')[0], expected_date: new Date().toISOString().split('T')[0],
remark: '物料补货', remark: '物料补货',
items: restockItems.value.map((item: any) => ({ items: restockItems.value.map((item: any) => ({
@@ -142,10 +154,6 @@ async function saveRestock() {
remark: item.remark remark: item.remark
})) }))
} }
if (!payload.supplier_id) {
addNotification('请先添加供应商', 'warning')
return
}
await apiRequest('/api/purchase-orders', { await apiRequest('/api/purchase-orders', {
method: 'POST', method: 'POST',
body: JSON.stringify(payload) body: JSON.stringify(payload)
@@ -153,9 +161,12 @@ async function saveRestock() {
addNotification('采购订单创建成功', 'success') addNotification('采购订单创建成功', 'success')
showRestockModal.value = false showRestockModal.value = false
restockItems.value = [] restockItems.value = []
restockSupplierId.value = null
loadPurchaseOrders() loadPurchaseOrders()
} catch (e) { } catch (e) {
handleApiError(e, '保存补货订单') handleApiError(e, '保存补货订单')
} finally {
restocking.value = false
} }
} }
@@ -234,8 +245,8 @@ onMounted(() => { loadMaterials() })
</t-form-item> </t-form-item>
</t-form> </t-form>
<template #footer> <template #footer>
<t-button @click="showModal = false">取消</t-button> <t-button @click="showModal = false" :disabled="saving">取消</t-button>
<t-button type="primary" @click="saveProduct">保存</t-button> <t-button type="primary" :loading="saving" :disabled="saving" @click="saveProduct">保存</t-button>
</template> </template>
</t-dialog> </t-dialog>
@@ -246,7 +257,7 @@ onMounted(() => { loadMaterials() })
@closed="closeRestockModal" @closed="closeRestockModal"
> >
<t-form label-width="100px" style="margin-bottom: 16px;"> <t-form label-width="100px" style="margin-bottom: 16px;">
<t-form-item label="供应商"> <t-form-item label="供应商" required>
<t-select v-model="restockSupplierId" placeholder="请选择供应商" style="width:100%"> <t-select v-model="restockSupplierId" placeholder="请选择供应商" style="width:100%">
<t-option <t-option
v-for="supplier in state.suppliers" v-for="supplier in state.suppliers"
@@ -304,8 +315,8 @@ onMounted(() => { loadMaterials() })
补货总金额:{{ formatCurrency(restockTotalAmount) }} 补货总金额:{{ formatCurrency(restockTotalAmount) }}
</div> </div>
<template #footer> <template #footer>
<t-button @click="showRestockModal = false">取消</t-button> <t-button @click="showRestockModal = false" :disabled="restocking">取消</t-button>
<t-button type="primary" @click="saveRestock">创建采购订单</t-button> <t-button type="primary" :loading="restocking" :disabled="restocking" @click="saveRestock">创建采购订单</t-button>
</template> </template>
</t-dialog> </t-dialog>
</div> </div>
@@ -13,6 +13,7 @@ const {
const showModal = ref(false) const showModal = ref(false)
const editingItem = ref<any>(null) const editingItem = ref<any>(null)
const saving = ref(false)
const form = ref<any>({}) const form = ref<any>({})
const bomItems = ref<any[]>([]) const bomItems = ref<any[]>([])
@@ -61,6 +62,7 @@ function removeBomItem(index: number) {
} }
async function saveProduct() { async function saveProduct() {
if (saving.value) return
if (!form.value.sku) { if (!form.value.sku) {
addNotification('请输入SKU', 'warning') addNotification('请输入SKU', 'warning')
return return
@@ -69,6 +71,7 @@ async function saveProduct() {
addNotification('请输入名称', 'warning') addNotification('请输入名称', 'warning')
return return
} }
saving.value = true
try { try {
if (editingItem.value) { if (editingItem.value) {
await apiRequest(`/api/products/${editingItem.value.id}`, { await apiRequest(`/api/products/${editingItem.value.id}`, {
@@ -97,6 +100,8 @@ async function saveProduct() {
loadMaterials() loadMaterials()
} catch (e) { } catch (e) {
handleApiError(e, '保存成品') handleApiError(e, '保存成品')
} finally {
saving.value = false
} }
} }
@@ -216,8 +221,8 @@ onMounted(() => { loadFinishedProducts() })
</template> </template>
</t-form> </t-form>
<template #footer> <template #footer>
<t-button @click="showModal = false">取消</t-button> <t-button @click="showModal = false" :disabled="saving">取消</t-button>
<t-button type="primary" @click="saveProduct">保存</t-button> <t-button type="primary" :loading="saving" :disabled="saving" @click="saveProduct">保存</t-button>
</template> </template>
</t-dialog> </t-dialog>
</div> </div>
@@ -38,12 +38,14 @@ const editingItem = ref<any>(null)
const form = ref<any>({}) const form = ref<any>({})
const orderItems = ref<any[]>([]) const orderItems = ref<any[]>([])
const purchaseWarehouseId = ref<number | null>(null) const purchaseWarehouseId = ref<number | null>(null)
const saving = ref(false)
const showReceiveModal = ref(false) const showReceiveModal = ref(false)
const receivingOrder = ref<any>(null) const receivingOrder = ref<any>(null)
const receiveItems = ref<any[]>([]) const receiveItems = ref<any[]>([])
const receiveWarehouseId = ref<number | null>(null) const receiveWarehouseId = ref<number | null>(null)
const receiveRemark = ref('') const receiveRemark = ref('')
const receiving = ref(false)
function openCreateOrder() { function openCreateOrder() {
editingItem.value = null editingItem.value = null
@@ -99,6 +101,7 @@ async function editOrder(order: any) {
} }
async function savePurchaseOrder() { async function savePurchaseOrder() {
if (saving.value) return
if (!form.value.supplier_id) { if (!form.value.supplier_id) {
addNotification('请选择供应商', 'warning') addNotification('请选择供应商', 'warning')
return return
@@ -117,6 +120,7 @@ async function savePurchaseOrder() {
return return
} }
} }
saving.value = true
try { try {
const payload = { const payload = {
supplier_id: form.value.supplier_id, supplier_id: form.value.supplier_id,
@@ -148,6 +152,8 @@ async function savePurchaseOrder() {
loadPurchaseOrders() loadPurchaseOrders()
} catch (e) { } catch (e) {
handleApiError(e, '保存采购订单') handleApiError(e, '保存采购订单')
} finally {
saving.value = false
} }
} }
@@ -183,6 +189,7 @@ async function openReceiveDialog(order: any) {
} }
async function receivePurchaseOrder() { async function receivePurchaseOrder() {
if (receiving.value) return
if (!receiveWarehouseId.value) { if (!receiveWarehouseId.value) {
addNotification('请选择入库仓库', 'warning') addNotification('请选择入库仓库', 'warning')
return return
@@ -197,6 +204,7 @@ async function receivePurchaseOrder() {
addNotification('请输入本次入库数量', 'warning') addNotification('请输入本次入库数量', 'warning')
return return
} }
receiving.value = true
try { try {
await apiRequest(`/api/purchase-orders/${receivingOrder.value.id}/receive`, { await apiRequest(`/api/purchase-orders/${receivingOrder.value.id}/receive`, {
method: 'POST', method: 'POST',
@@ -215,6 +223,8 @@ async function receivePurchaseOrder() {
loadMovements() loadMovements()
} catch (e) { } catch (e) {
handleApiError(e, '采购入库') handleApiError(e, '采购入库')
} finally {
receiving.value = false
} }
} }
@@ -456,8 +466,8 @@ onMounted(() => {
订单总金额:{{ formatCurrency(orderTotalAmount) }} 订单总金额:{{ formatCurrency(orderTotalAmount) }}
</div> </div>
<template #footer> <template #footer>
<t-button @click="showModal = false">取消</t-button> <t-button @click="showModal = false" :disabled="saving">取消</t-button>
<t-button type="primary" @click="savePurchaseOrder">保存</t-button> <t-button type="primary" :loading="saving" :disabled="saving" @click="savePurchaseOrder">保存</t-button>
</template> </template>
</t-dialog> </t-dialog>
@@ -501,8 +511,8 @@ onMounted(() => {
</t-table-column> </t-table-column>
</t-table> </t-table>
<template #footer> <template #footer>
<t-button @click="showReceiveModal = false">取消</t-button> <t-button @click="showReceiveModal = false" :disabled="receiving">取消</t-button>
<t-button type="primary" @click="receivePurchaseOrder">确认入库</t-button> <t-button type="primary" :loading="receiving" :disabled="receiving" @click="receivePurchaseOrder">确认入库</t-button>
</template> </template>
</t-dialog> </t-dialog>
</div> </div>
@@ -38,10 +38,20 @@ const editingItem = ref<any>(null)
const form = ref<any>({}) const form = ref<any>({})
const moldItems = ref<any[]>([]) const moldItems = ref<any[]>([])
const productionWarehouseId = ref<number | null>(null) const productionWarehouseId = ref<number | null>(null)
const saving = ref(false)
const showConsumptionModal = ref(false) const showConsumptionModal = ref(false)
const consumptionItems = ref<any[]>([]) const consumptionItems = ref<any[]>([])
const consumedMaterials = ref<any[]>([]) const consumedMaterials = ref<any[]>([])
const consuming = ref(false)
// 删除守卫:已收款/已交付/已作废/有收款记录 的订单禁止删除(与后端一致)
function isSalesOrderDeleteLocked(row: any): boolean {
return row.payment_status === 'paid'
|| row.delivery_status === 'delivered'
|| row.delivery_status === 'cancelled'
|| (Number(row.received_amount || 0) > 0)
}
function openCreateOrder() { function openCreateOrder() {
editingItem.value = null editingItem.value = null
@@ -117,6 +127,7 @@ async function editOrder(order: any) {
} }
async function saveSalesOrder() { async function saveSalesOrder() {
if (saving.value) return
if (!form.value.customer_id) { if (!form.value.customer_id) {
addNotification('请选择客户', 'warning') addNotification('请选择客户', 'warning')
return return
@@ -125,6 +136,33 @@ async function saveSalesOrder() {
addNotification('请至少添加一个模具', 'warning') addNotification('请至少添加一个模具', 'warning')
return return
} }
for (const [i, item] of moldItems.value.entries()) {
const idx = i + 1
if (item.mold_mode === 'new') {
if (!item.mold_sku || !String(item.mold_sku).trim()) {
addNotification(`第 ${idx} 行:新模请填写模具SKU`, 'warning')
return
}
if (!item.mold_name || !String(item.mold_name).trim()) {
addNotification(`第 ${idx} 行:新模请填写模具名称`, 'warning')
return
}
} else {
if (!item.mold_id) {
addNotification(`第 ${idx} 行:改模请选择已有模具`, 'warning')
return
}
}
if (!item.quantity || item.quantity <= 0) {
addNotification(`第 ${idx} 行:数量必须大于 0`, 'warning')
return
}
if (!item.unit_price || item.unit_price < 0) {
addNotification(`第 ${idx} 行:单价不能为负`, 'warning')
return
}
}
saving.value = true
try { try {
const payload = { const payload = {
customer_id: form.value.customer_id, customer_id: form.value.customer_id,
@@ -164,13 +202,19 @@ async function saveSalesOrder() {
loadMovements() loadMovements()
} catch (e) { } catch (e) {
handleApiError(e, '保存销售订单') handleApiError(e, '保存销售订单')
} finally {
saving.value = false
} }
} }
async function deleteOrder(id: number) { async function deleteOrder(row: any) {
if (!await confirmDialog('删除确认', '确定要删除这个销售订单吗?', 'warning', '删除', '取消')) return if (isSalesOrderDeleteLocked(row)) {
addNotification('已收款/已交付/已作废或有收款记录的订单不可删除,请先作废相关收款单', 'warning')
return
}
if (!await confirmDialog('删除确认', '确定要删除这个销售订单吗?删除将自动回补已扣减的物料。', 'warning', '删除', '取消')) return
try { try {
await apiRequest(`/api/sales-orders/${id}`, { method: 'DELETE' }) await apiRequest(`/api/sales-orders/${row.id}`, { method: 'DELETE' })
addNotification('销售订单已删除', 'success') addNotification('销售订单已删除', 'success')
loadProductionOrders() loadProductionOrders()
loadInventory() loadInventory()
@@ -239,11 +283,24 @@ const consumptionTotalAmount = computed(() => {
}) })
async function saveConsumption() { async function saveConsumption() {
if (consuming.value) return
if (!editingItem.value) return if (!editingItem.value) return
if (!consumptionItems.value || consumptionItems.value.length === 0) { if (!consumptionItems.value || consumptionItems.value.length === 0) {
addNotification('请至少添加一个消耗物料', 'warning') addNotification('请至少添加一个消耗物料', 'warning')
return return
} }
for (const [i, item] of consumptionItems.value.entries()) {
const idx = i + 1
if (!item.material_id) {
addNotification(`第 ${idx} 行:请选择物料`, 'warning')
return
}
if (!item.quantity || item.quantity <= 0) {
addNotification(`第 ${idx} 行:数量必须大于 0`, 'warning')
return
}
}
consuming.value = true
try { try {
const payload = { const payload = {
items: consumptionItems.value.map((item: any) => ({ items: consumptionItems.value.map((item: any) => ({
@@ -275,6 +332,8 @@ async function saveConsumption() {
loadMovements() loadMovements()
} catch (e) { } catch (e) {
handleApiError(e, '保存物料消耗') handleApiError(e, '保存物料消耗')
} finally {
consuming.value = false
} }
} }
@@ -372,7 +431,7 @@ onMounted(() => {
:disabled="row.payment_status === 'paid' || row.delivery_status === 'cancelled'" :disabled="row.payment_status === 'paid' || row.delivery_status === 'cancelled'"
@click="editOrder(row)" @click="editOrder(row)"
>编辑</t-button> >编辑</t-button>
<t-button type="danger" size="small" @click="deleteOrder(row.id)">删除</t-button> <t-button type="danger" size="small" :disabled="isSalesOrderDeleteLocked(row)" @click="deleteOrder(row)">删除</t-button>
<t-button <t-button
v-if="row.payment_status !== 'paid' && row.delivery_status !== 'cancelled'" v-if="row.payment_status !== 'paid' && row.delivery_status !== 'cancelled'"
type="info" size="small" type="info" size="small"
@@ -500,8 +559,8 @@ onMounted(() => {
</template> </template>
<template #footer> <template #footer>
<t-button @click="showModal = false">取消</t-button> <t-button @click="showModal = false" :disabled="saving">取消</t-button>
<t-button type="primary" @click="saveSalesOrder">保存</t-button> <t-button type="primary" :loading="saving" :disabled="saving" @click="saveSalesOrder">保存</t-button>
</template> </template>
</t-dialog> </t-dialog>
@@ -563,8 +622,8 @@ onMounted(() => {
消耗总金额:{{ formatCurrency(consumptionTotalAmount) }} 消耗总金额:{{ formatCurrency(consumptionTotalAmount) }}
</div> </div>
<template #footer> <template #footer>
<t-button @click="showConsumptionModal = false">取消</t-button> <t-button @click="showConsumptionModal = false" :disabled="consuming">取消</t-button>
<t-button type="primary" @click="saveConsumption">保存消耗</t-button> <t-button type="primary" :loading="consuming" :disabled="consuming" @click="saveConsumption">保存消耗</t-button>
</template> </template>
</t-dialog> </t-dialog>
</div> </div>
@@ -8,6 +8,7 @@ const { state, loadSuppliers } = useInventory()
const showModal = ref(false) const showModal = ref(false)
const editingItem = ref<any>(null) const editingItem = ref<any>(null)
const saving = ref(false)
const form = reactive({ const form = reactive({
name: '', name: '',
contact_person: '', contact_person: '',
@@ -37,6 +38,12 @@ function editSupplier(item: any) {
} }
async function save() { async function save() {
if (saving.value) return
if (!form.name || !form.name.trim()) {
addNotification('请输入供应商名称', 'warning')
return
}
saving.value = true
try { try {
const payload = { const payload = {
name: form.name, name: form.name,
@@ -62,6 +69,8 @@ async function save() {
loadSuppliers() loadSuppliers()
} catch (e) { } catch (e) {
handleApiError(e, '保存供应商') handleApiError(e, '保存供应商')
} finally {
saving.value = false
} }
} }
@@ -128,8 +137,8 @@ onMounted(() => { loadSuppliers() })
</t-form-item> </t-form-item>
</t-form> </t-form>
<template #footer> <template #footer>
<t-button @click="showModal = false">取消</t-button> <t-button @click="showModal = false" :disabled="saving">取消</t-button>
<t-button type="primary" @click="save">保存</t-button> <t-button type="primary" :loading="saving" :disabled="saving" @click="save">保存</t-button>
</template> </template>
</t-dialog> </t-dialog>
</div> </div>
+7 -1
View File
@@ -10,6 +10,7 @@
from fastapi import APIRouter, Depends, Query, HTTPException from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update, func from sqlalchemy import select, update, func
from sqlalchemy.exc import IntegrityError
from typing import Optional, List from typing import Optional, List
from shared.database.database import get_db_session from shared.database.database import get_db_session
@@ -114,7 +115,12 @@ async def create_inventory(
location=payload.location location=payload.location
) )
db_session.add(inventory) db_session.add(inventory)
await db_session.commit() try:
await db_session.commit()
except IntegrityError:
# 并发创建命中 (product_id, warehouse_id) 唯一约束
await db_session.rollback()
raise HTTPException(status_code=400, detail="该仓库已存在该物料库存记录")
await db_session.refresh(inventory) await db_session.refresh(inventory)
return InventoryResponse( return InventoryResponse(
@@ -344,6 +344,10 @@ async def update_purchase_order_status(
valid_statuses = ["pending", "partial_received", "received", "paid", "cancelled"] valid_statuses = ["pending", "partial_received", "received", "paid", "cancelled"]
if new_status not in valid_statuses: if new_status not in valid_statuses:
raise HTTPException(status_code=400, detail=f"无效的状态值,有效值为: {valid_statuses}") raise HTTPException(status_code=400, detail=f"无效的状态值,有效值为: {valid_statuses}")
# 收货状态(partial_received/received)只能由收货端点驱动,禁止手动设置,避免与实际入库脱钩
if new_status in ("partial_received", "received"):
raise HTTPException(status_code=400, detail="收货状态只能通过收货入库操作自动变更,不能手动设置")
# 状态转换逻辑 # 状态转换逻辑
if order.status == "paid": if order.status == "paid":
@@ -380,6 +384,8 @@ async def receive_purchase_order(
current_user: User = Depends(get_current_active_user) current_user: User = Depends(get_current_active_user)
): ):
order, supplier = await _get_order_with_supplier(db_session, order_id) order, supplier = await _get_order_with_supplier(db_session, order_id)
if order.status in ("cancelled", "paid"):
raise HTTPException(status_code=400, detail=f"当前采购单状态为 {order.status},不允许收货")
warehouse = await _resolve_receive_warehouse(db_session, payload.warehouse_id) warehouse = await _resolve_receive_warehouse(db_session, payload.warehouse_id)
item_result = await db_session.execute( item_result = await db_session.execute(
+26 -15
View File
@@ -446,7 +446,7 @@ async def create_sales_order(
customer_id=order_data.customer_id, customer_id=order_data.customer_id,
order_date=now, order_date=now,
delivery_date=order_data.delivery_date, delivery_date=order_data.delivery_date,
manufacturing_date=now, manufacturing_date=now.date(),
created_at=now, created_at=now,
remark=order_data.remark, remark=order_data.remark,
operator_id=current_user.id, operator_id=current_user.id,
@@ -563,6 +563,10 @@ async def delete_sales_order(
order, customer = await _get_sales_order_with_customer(db_session, order_id) order, customer = await _get_sales_order_with_customer(db_session, order_id)
if order.status == "delivered": if order.status == "delivered":
raise HTTPException(status_code=400, detail="已交付的销售订单禁止删除") raise HTTPException(status_code=400, detail="已交付的销售订单禁止删除")
if order.status == "paid":
raise HTTPException(status_code=400, detail="已收款的销售订单禁止删除,请先作废相关收款单")
if (order.received_amount or 0) > 0:
raise HTTPException(status_code=400, detail="该销售订单已存在收款记录(received_amount>0),禁止删除,请先作废相关收款单")
await _rollback_issued_materials(db_session, order, current_user) await _rollback_issued_materials(db_session, order, current_user)
await db_session.delete(order) await db_session.delete(order)
await db_session.commit() await db_session.commit()
@@ -586,9 +590,14 @@ async def consume_materials(
db_session: AsyncSession = Depends(get_db_session), db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user) current_user: User = Depends(get_current_active_user)
): ):
"""记录销售订单的物料消耗""" """记录销售订单的物料消耗(在已发料基础上记录额外消耗,如报废/超耗)"""
order, customer = await _get_sales_order_with_customer(db_session, order_id) order, customer = await _get_sales_order_with_customer(db_session, order_id)
if order.status in ("delivered", "paid", "cancelled"):
raise HTTPException(status_code=400, detail="当前订单状态不允许记录物料消耗")
if order.production_status == "completed":
raise HTTPException(status_code=400, detail="该销售单已完成生产,不允许记录物料消耗")
default_warehouse = await _get_default_warehouse(db_session) default_warehouse = await _get_default_warehouse(db_session)
# 计算总物料成本 # 计算总物料成本
@@ -602,45 +611,47 @@ async def consume_materials(
raise HTTPException(status_code=404, detail=f"物料 ID {item.material_id} 不存在") raise HTTPException(status_code=404, detail=f"物料 ID {item.material_id} 不存在")
if material.item_type != "material": if material.item_type != "material":
raise HTTPException(status_code=400, detail=f"只能消耗物料类型的产品: {material.name}") raise HTTPException(status_code=400, detail=f"只能消耗物料类型的产品: {material.name}")
# 计算成本 # 统一转 Decimal,避免 Decimal * float 在 Postgres(Numeric) 上抛 TypeError
cost = Decimal(str(material.cost_price or 0)) * item.quantity consume_qty = Decimal(str(item.quantity))
unit_cost = Decimal(str(material.cost_price or 0))
cost = unit_cost * consume_qty
total_cost += cost total_cost += cost
# 更新物料库存(原子操作防并发) # 更新物料库存(原子操作防并发)
upd_result = await db_session.execute( upd_result = await db_session.execute(
update(Inventory) update(Inventory)
.where(Inventory.product_id == material.id) .where(Inventory.product_id == material.id)
.where(Inventory.warehouse_id == default_warehouse.id) .where(Inventory.warehouse_id == default_warehouse.id)
.where(Inventory.quantity >= item.quantity) .where(Inventory.quantity >= consume_qty)
.values(quantity=Inventory.quantity - item.quantity) .values(quantity=Inventory.quantity - consume_qty)
.returning(Inventory.quantity) .returning(Inventory.quantity)
) )
after_qty = upd_result.scalar_one_or_none() after_qty = upd_result.scalar_one_or_none()
if after_qty is None: if after_qty is None:
raise HTTPException(status_code=400, detail=f"物料 {material.name} 库存不足") raise HTTPException(status_code=400, detail=f"物料 {material.name} 库存不足")
after_qty = int(after_qty) after_qty = Decimal(str(after_qty))
before_qty = after_qty + int(item.quantity) before_qty = after_qty + consume_qty
# 记录物料消耗 # 记录物料消耗
movement = StockMovement( movement = StockMovement(
product_id=material.id, product_id=material.id,
warehouse_id=default_warehouse.id, warehouse_id=default_warehouse.id,
quantity=item.quantity, quantity=consume_qty,
before_quantity=before_qty, before_quantity=before_qty,
after_quantity=after_qty, after_quantity=after_qty,
movement_type="consumption", movement_type="consumption",
reference_type="sales_order", reference_type="sales_order",
reference_id=order.id, reference_id=order.id,
unit_price=material.cost_price, unit_price=unit_cost,
total_amount=cost, total_amount=cost,
operator_id=current_user.id, operator_id=current_user.id,
remark=item.remark remark=item.remark
) )
db_session.add(movement) db_session.add(movement)
# 更新订单的实际物料成本 # 累加实际物料成本(创建时已记录计划发料成本,此处为额外消耗,不应覆盖)
order.actual_material_cost = total_cost order.actual_material_cost = Decimal(str(order.actual_material_cost or 0)) + total_cost
await db_session.commit() await db_session.commit()
await db_session.refresh(order) await db_session.refresh(order)
+4 -1
View File
@@ -660,7 +660,10 @@ class Warehouse(Base):
class Inventory(Base): class Inventory(Base):
"""库存表""" """库存表"""
__tablename__ = "inventory" __tablename__ = "inventory"
__table_args__ = (
UniqueConstraint("product_id", "warehouse_id", name="uq_inventory_product_warehouse"),
)
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True) product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True)
warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=False, index=True) warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=False, index=True)