x
This commit is contained in:
@@ -22,6 +22,15 @@ from .utils import generate_order_no
|
|||||||
|
|
||||||
router = APIRouter(prefix="/stock-movements", tags=["库存变动"])
|
router = APIRouter(prefix="/stock-movements", tags=["库存变动"])
|
||||||
|
|
||||||
|
INBOUND_TYPES = {
|
||||||
|
"in", "purchase_in", "return_from_production", "outsource_return", "finish_in"
|
||||||
|
}
|
||||||
|
OUTBOUND_TYPES = {
|
||||||
|
"out", "issue_to_production", "outsource_send", "shipment_out", "scrap_out"
|
||||||
|
}
|
||||||
|
ADJUST_TYPES = {"adjust"}
|
||||||
|
SUPPORTED_MOVEMENT_TYPES = INBOUND_TYPES | OUTBOUND_TYPES | ADJUST_TYPES
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=StockMovementResponse, status_code=201)
|
@router.post("", response_model=StockMovementResponse, status_code=201)
|
||||||
async def create_stock_movement(
|
async def create_stock_movement(
|
||||||
@@ -29,7 +38,7 @@ async def create_stock_movement(
|
|||||||
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)
|
||||||
):
|
):
|
||||||
if movement_data.movement_type not in ["in", "out", "adjust"]:
|
if movement_data.movement_type not in SUPPORTED_MOVEMENT_TYPES:
|
||||||
raise HTTPException(status_code=400, detail="无效的变动类型")
|
raise HTTPException(status_code=400, detail="无效的变动类型")
|
||||||
if movement_data.quantity <= 0:
|
if movement_data.quantity <= 0:
|
||||||
raise HTTPException(status_code=400, detail="数量必须大于0")
|
raise HTTPException(status_code=400, detail="数量必须大于0")
|
||||||
@@ -70,7 +79,7 @@ async def create_stock_movement(
|
|||||||
inventory = result.scalar_one_or_none()
|
inventory = result.scalar_one_or_none()
|
||||||
|
|
||||||
if not inventory:
|
if not inventory:
|
||||||
if movement_data.movement_type == "out":
|
if movement_data.movement_type in OUTBOUND_TYPES:
|
||||||
raise HTTPException(status_code=400, detail="库存不足")
|
raise HTTPException(status_code=400, detail="库存不足")
|
||||||
inventory = Inventory(
|
inventory = Inventory(
|
||||||
product_id=resolved_product_id,
|
product_id=resolved_product_id,
|
||||||
@@ -82,9 +91,9 @@ async def create_stock_movement(
|
|||||||
|
|
||||||
before_qty = inventory.quantity
|
before_qty = inventory.quantity
|
||||||
|
|
||||||
if movement_data.movement_type == "in":
|
if movement_data.movement_type in INBOUND_TYPES:
|
||||||
inventory.quantity += movement_data.quantity
|
inventory.quantity += movement_data.quantity
|
||||||
elif movement_data.movement_type == "out":
|
elif movement_data.movement_type in OUTBOUND_TYPES:
|
||||||
if inventory.quantity < movement_data.quantity:
|
if inventory.quantity < movement_data.quantity:
|
||||||
raise HTTPException(status_code=400, detail="库存不足")
|
raise HTTPException(status_code=400, detail="库存不足")
|
||||||
inventory.quantity -= movement_data.quantity
|
inventory.quantity -= movement_data.quantity
|
||||||
|
|||||||
+68
-8
@@ -1498,6 +1498,49 @@ const InventoryView = {
|
|||||||
form: {}
|
form: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const inboundMovementOptions = [
|
||||||
|
{ value: 'purchase_in', label: '采购入库' },
|
||||||
|
{ value: 'return_from_production', label: '生产退料入库' },
|
||||||
|
{ value: 'outsource_return', label: '外协回库' },
|
||||||
|
{ value: 'finish_in', label: '完工入库' },
|
||||||
|
{ value: 'in', label: '其他入库' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const outboundMovementOptions = [
|
||||||
|
{ value: 'issue_to_production', label: '生产领料出库' },
|
||||||
|
{ value: 'outsource_send', label: '外协发料出库' },
|
||||||
|
{ value: 'shipment_out', label: '销售出库' },
|
||||||
|
{ value: 'scrap_out', label: '报废出库' },
|
||||||
|
{ value: 'out', label: '其他出库' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const getMovementTypeLabel = (movementType) => {
|
||||||
|
const movementLabelMap = {
|
||||||
|
in: '其他入库',
|
||||||
|
out: '其他出库',
|
||||||
|
adjust: '库存调整',
|
||||||
|
purchase_in: '采购入库',
|
||||||
|
return_from_production: '生产退料入库',
|
||||||
|
outsource_return: '外协回库',
|
||||||
|
finish_in: '完工入库',
|
||||||
|
issue_to_production: '生产领料出库',
|
||||||
|
outsource_send: '外协发料出库',
|
||||||
|
shipment_out: '销售出库',
|
||||||
|
scrap_out: '报废出库'
|
||||||
|
};
|
||||||
|
return movementLabelMap[movementType] || movementType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMovementBadgeClass = (movementType) => {
|
||||||
|
if (['purchase_in', 'return_from_production', 'outsource_return', 'finish_in', 'in'].includes(movementType)) {
|
||||||
|
return 'badge-success';
|
||||||
|
}
|
||||||
|
if (['issue_to_production', 'outsource_send', 'shipment_out', 'scrap_out', 'out'].includes(movementType)) {
|
||||||
|
return 'badge-error';
|
||||||
|
}
|
||||||
|
return 'badge-warning';
|
||||||
|
};
|
||||||
|
|
||||||
const loadDashboard = async () => {
|
const loadDashboard = async () => {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
try {
|
try {
|
||||||
@@ -1672,7 +1715,8 @@ const InventoryView = {
|
|||||||
state.form = {
|
state.form = {
|
||||||
product_id: state.products[0]?.id || null,
|
product_id: state.products[0]?.id || null,
|
||||||
warehouse_id: state.warehouses.find(w => w.is_default)?.id || state.warehouses[0]?.id || null,
|
warehouse_id: state.warehouses.find(w => w.is_default)?.id || state.warehouses[0]?.id || null,
|
||||||
quantity: 1
|
quantity: 1,
|
||||||
|
movement_type: type === 'stockIn' ? 'purchase_in' : 'issue_to_production'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1789,7 +1833,7 @@ const InventoryView = {
|
|||||||
try {
|
try {
|
||||||
await apiRequest('/api/stock-movements', {
|
await apiRequest('/api/stock-movements', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ ...state.form, movement_type: 'in' })
|
body: JSON.stringify({ ...state.form, movement_type: state.form.movement_type || 'purchase_in' })
|
||||||
});
|
});
|
||||||
addNotification('入库成功', 'success');
|
addNotification('入库成功', 'success');
|
||||||
closeModal();
|
closeModal();
|
||||||
@@ -1804,7 +1848,7 @@ const InventoryView = {
|
|||||||
try {
|
try {
|
||||||
await apiRequest('/api/stock-movements', {
|
await apiRequest('/api/stock-movements', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ ...state.form, movement_type: 'out' })
|
body: JSON.stringify({ ...state.form, movement_type: state.form.movement_type || 'issue_to_production' })
|
||||||
});
|
});
|
||||||
addNotification('出库成功', 'success');
|
addNotification('出库成功', 'success');
|
||||||
closeModal();
|
closeModal();
|
||||||
@@ -1839,7 +1883,11 @@ const InventoryView = {
|
|||||||
deleteCustomer,
|
deleteCustomer,
|
||||||
stockIn,
|
stockIn,
|
||||||
stockOut,
|
stockOut,
|
||||||
refreshFinanceByPeriod
|
refreshFinanceByPeriod,
|
||||||
|
inboundMovementOptions,
|
||||||
|
outboundMovementOptions,
|
||||||
|
getMovementTypeLabel,
|
||||||
|
getMovementBadgeClass
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
@@ -1949,8 +1997,8 @@ const InventoryView = {
|
|||||||
|
|
||||||
<div v-else-if="state.activeTab === 'inventory'">
|
<div v-else-if="state.activeTab === 'inventory'">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<button class="btn btn-primary" @click="openModal('stockIn')">📥 入库</button>
|
<button class="btn btn-primary" @click="openModal('stockIn')">📥 采购/完工入库</button>
|
||||||
<button class="btn btn-secondary" @click="openModal('stockOut')" style="margin-left: 8px;">📤 出库</button>
|
<button class="btn btn-secondary" @click="openModal('stockOut')" style="margin-left: 8px;">📤 领料/销售出库</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
@@ -2258,8 +2306,8 @@ const InventoryView = {
|
|||||||
<tr v-for="movement in state.movements" :key="movement.id">
|
<tr v-for="movement in state.movements" :key="movement.id">
|
||||||
<td>{{ movement.product_name }}{{ movement.product_sku ? ' (' + movement.product_sku + ')' : '' }}</td>
|
<td>{{ movement.product_name }}{{ movement.product_sku ? ' (' + movement.product_sku + ')' : '' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span :class="['badge', movement.movement_type === 'in' ? 'badge-success' : movement.movement_type === 'out' ? 'badge-error' : 'badge-warning']">
|
<span :class="['badge', getMovementBadgeClass(movement.movement_type)]">
|
||||||
{{ movement.movement_type === 'in' ? '入库' : movement.movement_type === 'out' ? '出库' : '调整' }}
|
{{ getMovementTypeLabel(movement.movement_type) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ movement.quantity }}</td>
|
<td>{{ movement.quantity }}</td>
|
||||||
@@ -2392,6 +2440,12 @@ const InventoryView = {
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">业务类型 *</label>
|
||||||
|
<select v-model="state.form.movement_type" class="form-input" required>
|
||||||
|
<option v-for="item in inboundMovementOptions" :key="'stockin-type-' + item.value" :value="item.value">{{ item.label }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">数量 *</label>
|
<label class="form-label">数量 *</label>
|
||||||
<input v-model.number="state.form.quantity" type="number" class="form-input" required placeholder="入库数量" />
|
<input v-model.number="state.form.quantity" type="number" class="form-input" required placeholder="入库数量" />
|
||||||
@@ -2430,6 +2484,12 @@ const InventoryView = {
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">业务类型 *</label>
|
||||||
|
<select v-model="state.form.movement_type" class="form-input" required>
|
||||||
|
<option v-for="item in outboundMovementOptions" :key="'stockout-type-' + item.value" :value="item.value">{{ item.label }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">数量 *</label>
|
<label class="form-label">数量 *</label>
|
||||||
<input v-model.number="state.form.quantity" type="number" class="form-input" required placeholder="出库数量" />
|
<input v-model.number="state.form.quantity" type="number" class="form-input" required placeholder="出库数量" />
|
||||||
|
|||||||
Reference in New Issue
Block a user