feat: upgrade adjustment workflow to require explicit inbound SKU or outbound tracking number and fix UTC timezone issue
This commit is contained in:
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user