x
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user