x
This commit is contained in:
+270
-15
@@ -1486,6 +1486,10 @@ const InventoryView = {
|
||||
customerProductStatement: [],
|
||||
supplierProductStatement: [],
|
||||
products: [],
|
||||
materials: [],
|
||||
productionOrders: [],
|
||||
productionPlan: null,
|
||||
productionWarehouseId: null,
|
||||
suppliers: [],
|
||||
customers: [],
|
||||
warehouses: [],
|
||||
@@ -1495,6 +1499,7 @@ const InventoryView = {
|
||||
showModal: false,
|
||||
modalType: '',
|
||||
editingItem: null,
|
||||
productBomItems: [],
|
||||
form: {}
|
||||
});
|
||||
|
||||
@@ -1555,7 +1560,7 @@ const InventoryView = {
|
||||
const loadProducts = async () => {
|
||||
state.loading = true;
|
||||
try {
|
||||
state.products = await apiRequest('/api/products');
|
||||
state.products = await apiRequest('/api/products?limit=200');
|
||||
} catch (e) {
|
||||
handleApiError(e, '加载产品');
|
||||
} finally {
|
||||
@@ -1563,6 +1568,17 @@ const InventoryView = {
|
||||
}
|
||||
};
|
||||
|
||||
const loadMaterials = async () => {
|
||||
state.loading = true;
|
||||
try {
|
||||
state.materials = await apiRequest('/api/products?item_type=material&limit=300');
|
||||
} catch (e) {
|
||||
handleApiError(e, '加载物料');
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadWarehouses = async () => {
|
||||
state.loading = true;
|
||||
try {
|
||||
@@ -1575,8 +1591,8 @@ const InventoryView = {
|
||||
};
|
||||
|
||||
const ensureStockBaseData = async () => {
|
||||
if (!state.products.length) {
|
||||
await loadProducts();
|
||||
if (!state.materials.length) {
|
||||
await loadMaterials();
|
||||
}
|
||||
if (!state.warehouses.length) {
|
||||
await loadWarehouses();
|
||||
@@ -1608,6 +1624,25 @@ const InventoryView = {
|
||||
}
|
||||
};
|
||||
|
||||
const loadProductionOrders = async () => {
|
||||
state.loading = true;
|
||||
try {
|
||||
const [orders, warehouses] = await Promise.all([
|
||||
apiRequest('/api/sales-orders?limit=100'),
|
||||
apiRequest('/api/warehouses')
|
||||
]);
|
||||
state.productionOrders = orders || [];
|
||||
state.warehouses = warehouses || [];
|
||||
if (!state.productionWarehouseId) {
|
||||
state.productionWarehouseId = state.warehouses.find(w => w.is_default)?.id || state.warehouses[0]?.id || null;
|
||||
}
|
||||
} catch (e) {
|
||||
handleApiError(e, '加载按单生产数据');
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadCustomers = async () => {
|
||||
state.loading = true;
|
||||
try {
|
||||
@@ -1699,10 +1734,41 @@ const InventoryView = {
|
||||
case 'customers': loadCustomers(); break;
|
||||
case 'inventory': loadInventory(); break;
|
||||
case 'movements': loadMovements(); break;
|
||||
case 'production': loadProductionOrders(); break;
|
||||
case 'finance': loadFinance(); break;
|
||||
}
|
||||
};
|
||||
|
||||
const loadOrderProductionPlan = async (orderId) => {
|
||||
try {
|
||||
state.productionPlan = await apiRequest(`/api/sales-orders/${orderId}/production-plan`);
|
||||
} catch (e) {
|
||||
handleApiError(e, '加载领料建议');
|
||||
}
|
||||
};
|
||||
|
||||
const issueOrderMaterials = async (order) => {
|
||||
if (!state.productionWarehouseId) {
|
||||
addNotification('请先选择领料仓库', 'warning');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await apiRequest(`/api/sales-orders/${order.id}/issue-materials`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
warehouse_id: state.productionWarehouseId,
|
||||
production_no: order.production_no || undefined
|
||||
})
|
||||
});
|
||||
addNotification(`领料成功,成本偏差率 ${(result.cost_deviation_rate * 100).toFixed(2)}%`, 'success');
|
||||
await loadProductionOrders();
|
||||
await loadMovements();
|
||||
state.productionPlan = await apiRequest(`/api/sales-orders/${order.id}/production-plan`);
|
||||
} catch (e) {
|
||||
handleApiError(e, '执行领料');
|
||||
}
|
||||
};
|
||||
|
||||
const openModal = async (type, item = null) => {
|
||||
state.modalType = type;
|
||||
state.editingItem = item;
|
||||
@@ -1713,12 +1779,22 @@ const InventoryView = {
|
||||
if (type === 'stockIn' || type === 'stockOut') {
|
||||
await ensureStockBaseData();
|
||||
state.form = {
|
||||
product_id: state.products[0]?.id || null,
|
||||
product_id: state.materials[0]?.id || null,
|
||||
warehouse_id: state.warehouses.find(w => w.is_default)?.id || state.warehouses[0]?.id || null,
|
||||
quantity: 1,
|
||||
movement_type: type === 'stockIn' ? 'purchase_in' : 'issue_to_production'
|
||||
};
|
||||
}
|
||||
if (type === 'product') {
|
||||
state.form = {
|
||||
item_type: 'finished',
|
||||
unit: '件',
|
||||
min_stock: 0,
|
||||
max_stock: 1000,
|
||||
cost_price: 0,
|
||||
sale_price: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
state.showModal = true;
|
||||
};
|
||||
@@ -1727,6 +1803,7 @@ const InventoryView = {
|
||||
state.showModal = false;
|
||||
state.modalType = '';
|
||||
state.editingItem = null;
|
||||
state.productBomItems = [];
|
||||
state.form = {};
|
||||
};
|
||||
|
||||
@@ -1747,6 +1824,7 @@ const InventoryView = {
|
||||
}
|
||||
closeModal();
|
||||
loadProducts();
|
||||
loadMaterials();
|
||||
} catch (e) {
|
||||
handleApiError(e, '保存产品');
|
||||
}
|
||||
@@ -1758,11 +1836,55 @@ const InventoryView = {
|
||||
await apiRequest(`/api/products/${id}`, { method: 'DELETE' });
|
||||
addNotification('产品已删除', 'success');
|
||||
loadProducts();
|
||||
loadMaterials();
|
||||
} catch (e) {
|
||||
handleApiError(e, '删除产品');
|
||||
}
|
||||
};
|
||||
|
||||
const editProductBom = async (product) => {
|
||||
try {
|
||||
await loadMaterials();
|
||||
const bom = await apiRequest(`/api/products/${product.id}/materials`);
|
||||
state.modalType = 'productBom';
|
||||
state.editingItem = product;
|
||||
state.productBomItems = (bom.items || []).map(item => ({
|
||||
material_id: item.material_id,
|
||||
quantity: item.quantity,
|
||||
loss_rate: item.loss_rate
|
||||
}));
|
||||
state.showModal = true;
|
||||
} catch (e) {
|
||||
handleApiError(e, '加载产品BOM');
|
||||
}
|
||||
};
|
||||
|
||||
const addBomItem = () => {
|
||||
state.productBomItems.push({
|
||||
material_id: state.materials[0]?.id || null,
|
||||
quantity: 1,
|
||||
loss_rate: 0
|
||||
});
|
||||
};
|
||||
|
||||
const removeBomItem = (idx) => {
|
||||
state.productBomItems.splice(idx, 1);
|
||||
};
|
||||
|
||||
const saveProductBom = async () => {
|
||||
try {
|
||||
await apiRequest(`/api/products/${state.editingItem.id}/materials`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ items: state.productBomItems })
|
||||
});
|
||||
addNotification('产品BOM保存成功', 'success');
|
||||
closeModal();
|
||||
loadProducts();
|
||||
} catch (e) {
|
||||
handleApiError(e, '保存产品BOM');
|
||||
}
|
||||
};
|
||||
|
||||
const saveSupplier = async () => {
|
||||
try {
|
||||
if (state.editingItem) {
|
||||
@@ -1877,12 +1999,19 @@ const InventoryView = {
|
||||
closeModal,
|
||||
saveProduct,
|
||||
deleteProduct,
|
||||
editProductBom,
|
||||
addBomItem,
|
||||
removeBomItem,
|
||||
saveProductBom,
|
||||
saveSupplier,
|
||||
deleteSupplier,
|
||||
saveCustomer,
|
||||
deleteCustomer,
|
||||
stockIn,
|
||||
stockOut,
|
||||
loadProductionOrders,
|
||||
loadOrderProductionPlan,
|
||||
issueOrderMaterials,
|
||||
refreshFinanceByPeriod,
|
||||
inboundMovementOptions,
|
||||
outboundMovementOptions,
|
||||
@@ -1904,6 +2033,7 @@ const InventoryView = {
|
||||
<button :class="['tab', { active: state.activeTab === 'suppliers' }]" @click="switchTab('suppliers')">供应商</button>
|
||||
<button :class="['tab', { active: state.activeTab === 'customers' }]" @click="switchTab('customers')">客户</button>
|
||||
<button :class="['tab', { active: state.activeTab === 'movements' }]" @click="switchTab('movements')">变动记录</button>
|
||||
<button :class="['tab', { active: state.activeTab === 'production' }]" @click="switchTab('production')">按单生产</button>
|
||||
<button :class="['tab', { active: state.activeTab === 'finance' }]" @click="switchTab('finance')">财务</button>
|
||||
</div>
|
||||
|
||||
@@ -1918,14 +2048,14 @@ const InventoryView = {
|
||||
<div class="stat-icon">📦</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ state.dashboard?.product_count || 0 }}</div>
|
||||
<div class="stat-label">产品数量</div>
|
||||
<div class="stat-label">成品数量</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📊</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ state.dashboard?.total_stock || 0 }}</div>
|
||||
<div class="stat-label">库存总量</div>
|
||||
<div class="stat-label">物料库存总量</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
@@ -1966,25 +2096,30 @@ const InventoryView = {
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>类型</th>
|
||||
<th>SKU</th>
|
||||
<th>名称</th>
|
||||
<th>分类</th>
|
||||
<th>单位</th>
|
||||
<th>成本价</th>
|
||||
<th>销售价</th>
|
||||
<th>基础物料成本</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="product in state.products" :key="product.id">
|
||||
<td>{{ product.item_type === 'material' ? '物料' : '成品' }}</td>
|
||||
<td>{{ product.sku }}</td>
|
||||
<td>{{ product.name }}</td>
|
||||
<td>{{ product.category || '-' }}</td>
|
||||
<td>{{ product.unit }}</td>
|
||||
<td>{{ formatCurrency(product.cost_price) }}</td>
|
||||
<td>{{ formatCurrency(product.sale_price) }}</td>
|
||||
<td>{{ product.item_type === 'finished' ? formatCurrency(product.material_cost || 0) : '-' }}</td>
|
||||
<td>
|
||||
<div class="action-btns">
|
||||
<button v-if="product.item_type === 'finished'" class="btn btn-sm btn-secondary" @click="editProductBom(product)">BOM</button>
|
||||
<button class="btn btn-sm btn-secondary" @click="openModal('product', product)">编辑</button>
|
||||
<button class="btn btn-sm btn-danger" @click="deleteProduct(product.id)">删除</button>
|
||||
</div>
|
||||
@@ -2094,6 +2229,79 @@ const InventoryView = {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="state.activeTab === 'production'">
|
||||
<div class="table-container" style="margin-bottom: 16px;">
|
||||
<div style="display:flex; gap:12px; align-items:center; flex-wrap:wrap;">
|
||||
<label>领料仓库</label>
|
||||
<select v-model.number="state.productionWarehouseId" class="form-input" style="width:260px;">
|
||||
<option v-for="warehouse in state.warehouses" :key="'production-warehouse-' + warehouse.id" :value="warehouse.id">
|
||||
{{ warehouse.name }}{{ warehouse.is_default ? ' [默认]' : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<button class="btn btn-secondary" @click="loadProductionOrders">刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>销售单</th>
|
||||
<th>客户</th>
|
||||
<th>生产单号</th>
|
||||
<th>生产状态</th>
|
||||
<th>计划物料成本</th>
|
||||
<th>实际领料成本</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="order in state.productionOrders" :key="'production-order-' + order.id">
|
||||
<td>{{ order.order_no }}</td>
|
||||
<td>{{ order.customer_name }}</td>
|
||||
<td>{{ order.production_no || '-' }}</td>
|
||||
<td>{{ order.production_status || '-' }}</td>
|
||||
<td>{{ formatCurrency(order.planned_material_cost || 0) }}</td>
|
||||
<td>{{ formatCurrency(order.actual_material_cost || 0) }}</td>
|
||||
<td>
|
||||
<div class="action-btns">
|
||||
<button class="btn btn-sm btn-secondary" @click="loadOrderProductionPlan(order.id)">领料建议</button>
|
||||
<button class="btn btn-sm btn-primary" @click="issueOrderMaterials(order)">执行领料</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-if="state.productionPlan" class="table-container" style="margin-top: 16px;">
|
||||
<h3 style="margin-bottom: 12px;">领料建议:{{ state.productionPlan.order_no }}({{ state.productionPlan.production_no }})</h3>
|
||||
<div style="margin-bottom: 12px; color: var(--text-secondary);">
|
||||
计划物料成本:{{ formatCurrency(state.productionPlan.planned_material_cost || 0) }}
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>物料</th>
|
||||
<th>需求</th>
|
||||
<th>可用</th>
|
||||
<th>缺口</th>
|
||||
<th>单位成本</th>
|
||||
<th>需求成本</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in state.productionPlan.items" :key="'plan-material-' + item.material_id">
|
||||
<td>{{ item.material_sku }} - {{ item.material_name }}</td>
|
||||
<td>{{ item.required_quantity }}</td>
|
||||
<td>{{ item.available_quantity }}</td>
|
||||
<td :class="{ 'text-warning': item.shortage_quantity > 0 }">{{ item.shortage_quantity }}</td>
|
||||
<td>{{ formatCurrency(item.unit_cost) }}</td>
|
||||
<td>{{ formatCurrency(item.required_cost) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="state.activeTab === 'finance'">
|
||||
<div class="table-container" style="margin-bottom: 16px;">
|
||||
<div style="display:flex; gap:12px; align-items:center; flex-wrap:wrap;">
|
||||
@@ -2294,7 +2502,7 @@ const InventoryView = {
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>产品</th>
|
||||
<th>物料</th>
|
||||
<th>类型</th>
|
||||
<th>数量</th>
|
||||
<th>变动前</th>
|
||||
@@ -2324,12 +2532,19 @@ const InventoryView = {
|
||||
<div v-if="state.showModal" class="modal-overlay" @click.self="closeModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>{{ state.editingItem ? '编辑' : '新增' }}{{ state.modalType === 'product' ? '产品' : state.modalType === 'supplier' ? '供应商' : state.modalType === 'customer' ? '客户' : state.modalType === 'stockIn' ? '入库' : '出库' }}</h3>
|
||||
<h3>{{ state.editingItem ? '编辑' : '新增' }}{{ state.modalType === 'product' ? '产品/物料' : state.modalType === 'productBom' ? '产品BOM' : state.modalType === 'supplier' ? '供应商' : state.modalType === 'customer' ? '客户' : state.modalType === 'stockIn' ? '入库' : '出库' }}</h3>
|
||||
<button class="modal-close" @click="closeModal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- 产品表单 -->
|
||||
<form v-if="state.modalType === 'product'" @submit.prevent="saveProduct">
|
||||
<div class="form-group">
|
||||
<label class="form-label">类型 *</label>
|
||||
<select v-model="state.form.item_type" class="form-input" required>
|
||||
<option value="finished">成品(按单生产,不做库存)</option>
|
||||
<option value="material">物料(纳入库存)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">SKU *</label>
|
||||
<input v-model="state.form.sku" class="form-input" required placeholder="产品编码" />
|
||||
@@ -2354,15 +2569,55 @@ const InventoryView = {
|
||||
<label class="form-label">销售价</label>
|
||||
<input v-model.number="state.form.sale_price" type="number" step="0.01" class="form-input" placeholder="0.00" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div v-if="state.form.item_type === 'material'" class="form-group">
|
||||
<label class="form-label">最低库存</label>
|
||||
<input v-model.number="state.form.min_stock" type="number" class="form-input" placeholder="0" />
|
||||
</div>
|
||||
<div v-if="state.form.item_type === 'finished'" class="form-group">
|
||||
<label class="form-label">说明</label>
|
||||
<input disabled value="成品不做库存,成本由下方BOM定义物料构成后自动计算" class="form-input" />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
||||
<button type="submit" class="btn btn-primary">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form v-else-if="state.modalType === 'productBom'" @submit.prevent="saveProductBom">
|
||||
<div class="table-header" style="margin-bottom: 12px;">
|
||||
<button type="button" class="btn btn-secondary" @click="addBomItem">+ 添加物料</button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>物料</th>
|
||||
<th>数量</th>
|
||||
<th>损耗率</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, idx) in state.productBomItems" :key="'bom-item-' + idx">
|
||||
<td>
|
||||
<select v-model.number="item.material_id" class="form-input" required>
|
||||
<option v-for="material in state.materials" :key="'bom-material-' + material.id" :value="material.id">
|
||||
{{ material.sku }} - {{ material.name }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input v-model.number="item.quantity" type="number" min="0.0001" step="0.0001" class="form-input" required /></td>
|
||||
<td><input v-model.number="item.loss_rate" type="number" min="0" step="0.0001" class="form-input" required /></td>
|
||||
<td><button type="button" class="btn btn-sm btn-danger" @click="removeBomItem(idx)">删除</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
||||
<button type="submit" class="btn btn-primary">保存BOM</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 供应商表单 -->
|
||||
<form v-else-if="state.modalType === 'supplier'" @submit.prevent="saveSupplier">
|
||||
@@ -2423,10 +2678,10 @@ const InventoryView = {
|
||||
<!-- 入库表单 -->
|
||||
<form v-else-if="state.modalType === 'stockIn'" @submit.prevent="stockIn">
|
||||
<div class="form-group">
|
||||
<label class="form-label">产品 *</label>
|
||||
<label class="form-label">物料 *</label>
|
||||
<select v-model.number="state.form.product_id" class="form-input" required>
|
||||
<option v-if="!state.products.length" :value="null" disabled>暂无产品,请先新增产品</option>
|
||||
<option v-for="product in state.products" :key="'stockin-product-' + product.id" :value="product.id">
|
||||
<option v-if="!state.materials.length" :value="null" disabled>暂无物料,请先新增物料</option>
|
||||
<option v-for="product in state.materials" :key="'stockin-product-' + product.id" :value="product.id">
|
||||
{{ product.sku }} - {{ product.name }}(ID: {{ product.id }})
|
||||
</option>
|
||||
</select>
|
||||
@@ -2467,10 +2722,10 @@ const InventoryView = {
|
||||
<!-- 出库表单 -->
|
||||
<form v-else-if="state.modalType === 'stockOut'" @submit.prevent="stockOut">
|
||||
<div class="form-group">
|
||||
<label class="form-label">产品 *</label>
|
||||
<label class="form-label">物料 *</label>
|
||||
<select v-model.number="state.form.product_id" class="form-input" required>
|
||||
<option v-if="!state.products.length" :value="null" disabled>暂无产品,请先新增产品</option>
|
||||
<option v-for="product in state.products" :key="'stockout-product-' + product.id" :value="product.id">
|
||||
<option v-if="!state.materials.length" :value="null" disabled>暂无物料,请先新增物料</option>
|
||||
<option v-for="product in state.materials" :key="'stockout-product-' + product.id" :value="product.id">
|
||||
{{ product.sku }} - {{ product.name }}(ID: {{ product.id }})
|
||||
</option>
|
||||
</select>
|
||||
|
||||
Reference in New Issue
Block a user