feat: upgrade adjustment workflow to require explicit inbound SKU or outbound tracking number and fix UTC timezone issue

This commit is contained in:
DXC
2026-03-19 15:26:40 +08:00
parent ae63748060
commit 6cc3d1b6e0
4 changed files with 167 additions and 3 deletions

View File

@ -2,7 +2,7 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt
from app.utils.decorators import permission_required from app.utils.decorators import permission_required
from app.extensions import db from app.extensions import db, beijing_time
from app.models.stock.adjustment import StockAdjustment from app.models.stock.adjustment import StockAdjustment
from app.models.base import MaterialBase from app.models.base import MaterialBase
from app.models.inbound.buy import StockBuy from app.models.inbound.buy import StockBuy
@ -351,7 +351,7 @@ def import_from_stocktake():
# 生成调整单号 # 生成调整单号
order_no = generate_order_no() order_no = generate_order_no()
# 创建调整单 # 创建调整单(使用北京时间)
adjustment = StockAdjustment( adjustment = StockAdjustment(
order_no=order_no, order_no=order_no,
base_id=base_id, base_id=base_id,
@ -363,7 +363,8 @@ def import_from_stocktake():
adjust_quantity=adjust_quantity, adjust_quantity=adjust_quantity,
reason=reason, reason=reason,
status='pending', status='pending',
operator=operator operator=operator,
create_time=beijing_time()
) )
db.session.add(adjustment) db.session.add(adjustment)
count += 1 count += 1
@ -374,3 +375,53 @@ def import_from_stocktake():
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
return jsonify({'code': 500, 'msg': f'导入失败: {str(e)}'}), 500 return jsonify({'code': 500, 'msg': f'导入失败: {str(e)}'}), 500
# --------------------------------------------------------
# 7. 处理调整单关联入库SKU或出库单号
# POST /api/v1/stock/adjustment/<id>/process
# --------------------------------------------------------
@adjustment_bp.route('/<int:id>/process', methods=['POST'])
@jwt_required()
@permission_required('stock_adjustment:operation')
def process_adjustment(id):
"""处理待处理的调整单关联入库SKU或出库单号"""
identity = get_jwt_identity()
operator = identity.get('username', 'system') if isinstance(identity, dict) else str(identity)
if not operator or operator == '0':
operator = identity if identity else 'system'
data = request.get_json()
if not data:
return jsonify({'code': 400, 'msg': '缺少参数'}), 400
linked_sku = data.get('linked_sku', '')
linked_outbound_no = data.get('linked_outbound_no', '')
try:
adjustment = StockAdjustment.query.get(id)
if not adjustment:
return jsonify({'code': 404, 'msg': '调整单不存在'}), 404
if adjustment.status != 'pending':
return jsonify({'code': 400, 'msg': '只能处理待处理状态的调整单'}), 400
# 根据调整类型校验必填项
if adjustment.adjust_type == 'profit':
if not linked_sku:
return jsonify({'code': 400, 'msg': '盘盈调整必须关联入库SKU'}), 400
adjustment.linked_sku = linked_sku
elif adjustment.adjust_type == 'loss':
if not linked_outbound_no:
return jsonify({'code': 400, 'msg': '盘亏调整必须关联出库单号'}), 400
adjustment.linked_outbound_no = linked_outbound_no
adjustment.status = 'completed'
adjustment.operator = operator
db.session.commit()
return jsonify({'code': 200, 'msg': '处理成功', 'data': adjustment.to_dict()})
except Exception as e:
db.session.rollback()
return jsonify({'code': 500, 'msg': f'处理失败: {str(e)}'}), 500

View File

@ -34,6 +34,10 @@ class StockAdjustment(db.Model):
reason = db.Column(db.String(500), nullable=False) reason = db.Column(db.String(500), nullable=False)
# 状态:'pending' 待处理 / 'completed' 已完成 / 'cancelled' 已取消 # 状态:'pending' 待处理 / 'completed' 已完成 / 'cancelled' 已取消
status = db.Column(db.String(20), default='pending') status = db.Column(db.String(20), default='pending')
# 关联入库SKU盘盈时填写
linked_sku = db.Column(db.String(100), comment='关联入库SKU盘盈时填写')
# 关联出库单号(盘亏时填写)
linked_outbound_no = db.Column(db.String(100), comment='关联出库单号(盘亏时填写)')
# 操作人/经办人 # 操作人/经办人
operator = db.Column(db.String(100)) operator = db.Column(db.String(100))
# 创建时间 # 创建时间
@ -56,6 +60,8 @@ class StockAdjustment(db.Model):
'adjust_quantity': float(self.adjust_quantity or 0), 'adjust_quantity': float(self.adjust_quantity or 0),
'reason': self.reason, 'reason': self.reason,
'status': self.status, 'status': self.status,
'linked_sku': self.linked_sku,
'linked_outbound_no': self.linked_outbound_no,
'operator': self.operator, 'operator': self.operator,
'create_time': self.create_time.strftime('%Y-%m-%d %H:%M:%S') if self.create_time else None, 'create_time': self.create_time.strftime('%Y-%m-%d %H:%M:%S') if self.create_time else None,
'update_time': self.update_time.strftime('%Y-%m-%d %H:%M:%S') if self.update_time else None, 'update_time': self.update_time.strftime('%Y-%m-%d %H:%M:%S') if self.update_time else None,

View File

@ -95,6 +95,7 @@
<el-option label="销售出库" value="SALES" /> <el-option label="销售出库" value="SALES" />
<el-option label="内部领用" value="USE" /> <el-option label="内部领用" value="USE" />
<el-option label="调拨出库" value="TRANSFER" /> <el-option label="调拨出库" value="TRANSFER" />
<el-option label="盘亏出库" value="LOSS" />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>

View File

@ -43,6 +43,18 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="reason" label="调整原因" min-width="150" show-overflow-tooltip /> <el-table-column prop="reason" label="调整原因" min-width="150" show-overflow-tooltip />
<el-table-column prop="linked_sku" label="关联SKU" width="120" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.adjust_type === 'profit'">{{ row.linked_sku || '-' }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="linked_outbound_no" label="关联出库单" width="140" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.adjust_type === 'loss'">{{ row.linked_outbound_no || '-' }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="operator" label="操作人" width="100" /> <el-table-column prop="operator" label="操作人" width="100" />
<el-table-column prop="status" label="状态" width="90" align="center"> <el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }"> <template #default="{ row }">
@ -52,6 +64,14 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="create_time" label="创建时间" width="160" /> <el-table-column prop="create_time" label="创建时间" width="160" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button v-if="row.status === 'pending' && userStore.hasPermission('stock_adjustment:operation')" type="primary" link @click="openProcessDialog(row)">
处理
</el-button>
<span v-else>-</span>
</template>
</el-table-column>
</el-table> </el-table>
<!-- 分页 --> <!-- 分页 -->
@ -173,6 +193,30 @@
</el-button> </el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- 处理调整单弹窗 -->
<el-dialog v-model="showProcessDialog" title="处理调整单" width="500px" :close-on-click-modal="false">
<el-form label-width="120px">
<el-form-item label="调整单号">
<el-input v-model="processForm.order_no" disabled />
</el-form-item>
<el-form-item label="调整类型">
<el-tag :type="processForm.adjust_type === 'profit' ? 'success' : 'danger'">
{{ processForm.adjust_type === 'profit' ? '盘盈' : '盘亏' }}
</el-tag>
</el-form-item>
<el-form-item :label="processForm.adjust_type === 'profit' ? '关联入库SKU' : '关联出库单号'">
<el-input
v-model="processForm.linked_value"
:placeholder="processForm.adjust_type === 'profit' ? '请输入已入库的SKU' : '请输入对应的出库单号'"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showProcessDialog = false">取消</el-button>
<el-button type="primary" @click="handleProcessSubmit" :loading="processLoading">确认处理</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
@ -233,6 +277,68 @@ const selectedReviewRows = ref<any[]>([])
const reviewTableRef = ref() const reviewTableRef = ref()
const importLoading = ref(false) const importLoading = ref(false)
// 处理调整单
const showProcessDialog = ref(false)
const processLoading = ref(false)
const processForm = ref({
id: null as number | null,
order_no: '',
adjust_type: '',
linked_value: ''
})
// 打开处理弹窗
function openProcessDialog(row: any) {
processForm.value = {
id: row.id,
order_no: row.order_no,
adjust_type: row.adjust_type,
linked_value: ''
}
showProcessDialog.value = true
}
// 提交处理
async function handleProcessSubmit() {
if (!processForm.value.id) return
if (processForm.value.adjust_type === 'profit' && !processForm.value.linked_value) {
ElMessage.warning('请输入已入库的SKU')
return
}
if (processForm.value.adjust_type === 'loss' && !processForm.value.linked_value) {
ElMessage.warning('请输入对应的出库单号')
return
}
processLoading.value = true
try {
const data: any = {}
if (processForm.value.adjust_type === 'profit') {
data.linked_sku = processForm.value.linked_value
} else {
data.linked_outbound_no = processForm.value.linked_value
}
const res = await request({
url: `/v1/stock/adjustment/${processForm.value.id}/process`,
method: 'post',
data
})
if (res.code === 200) {
ElMessage.success('处理成功')
showProcessDialog.value = false
fetchData()
} else {
ElMessage.error(res.msg || '处理失败')
}
} catch (e) {
ElMessage.error('处理失败')
} finally {
processLoading.value = false
}
}
// 获取盘点差异列表 // 获取盘点差异列表
async function fetchReviewList() { async function fetchReviewList() {
reviewLoading.value = true reviewLoading.value = true