feat: initialize inventory profit and loss adjustment module
This commit is contained in:
@ -162,7 +162,17 @@ def create_app():
|
|||||||
print(f"⚠️ 审计日志菜单初始化跳过: {e}")
|
print(f"⚠️ 审计日志菜单初始化跳过: {e}")
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 2.10 注册库位管理模块 (Warehouse)
|
# 2.10 注册盘盈盘亏管理模块 (Stock Adjustment)
|
||||||
|
# -----------------------------------------------------
|
||||||
|
try:
|
||||||
|
from app.api.v1.stock.adjustment import adjustment_bp
|
||||||
|
app.register_blueprint(adjustment_bp, url_prefix='/api/v1/stock/adjustment')
|
||||||
|
print("✅ Stock Adjustment 模块注册成功")
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"❌ 错误: Stock Adjustment 模块导入失败: {e}")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 2.11 注册库位管理模块 (Warehouse)
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
try:
|
try:
|
||||||
from app.api.v1.warehouse import warehouse_bp
|
from app.api.v1.warehouse import warehouse_bp
|
||||||
|
|||||||
223
inventory-backend/app/api/v1/stock/adjustment.py
Normal file
223
inventory-backend/app/api/v1/stock/adjustment.py
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
# inventory-backend/app/api/v1/stock/adjustment.py
|
||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt
|
||||||
|
from app.utils.decorators import permission_required
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models.stock.adjustment import StockAdjustment
|
||||||
|
from app.models.base import MaterialBase
|
||||||
|
from app.models.inbound.buy import StockBuy
|
||||||
|
from app.models.inbound.semi import StockSemi
|
||||||
|
from app.models.inbound.product import StockProduct
|
||||||
|
from datetime import datetime
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
adjustment_bp = Blueprint('adjustment', __name__, url_prefix='/stock/adjustment')
|
||||||
|
|
||||||
|
|
||||||
|
def generate_order_no():
|
||||||
|
"""生成单号 ADJ-YYYYMMDD-XXXX"""
|
||||||
|
today = datetime.now().strftime('%Y%m%d')
|
||||||
|
suffix = ''.join(random.choices(string.digits, k=4))
|
||||||
|
return f'ADJ-{today}-{suffix}'
|
||||||
|
|
||||||
|
|
||||||
|
def get_stock_model(source_table):
|
||||||
|
"""根据source_table获取对应的库存模型"""
|
||||||
|
if source_table == 'stock_buy':
|
||||||
|
return StockBuy
|
||||||
|
elif source_table == 'stock_semi':
|
||||||
|
return StockSemi
|
||||||
|
elif source_table == 'stock_product':
|
||||||
|
return StockProduct
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# 1. 获取调整单列表
|
||||||
|
# GET /api/v1/stock/adjustment/list
|
||||||
|
# --------------------------------------------------------
|
||||||
|
@adjustment_bp.route('/list', methods=['GET'])
|
||||||
|
@jwt_required()
|
||||||
|
@permission_required('stock_adjustment:list')
|
||||||
|
def get_list():
|
||||||
|
page = int(request.args.get('page', 1))
|
||||||
|
limit = int(request.args.get('limit', 10))
|
||||||
|
keyword = request.args.get('keyword', '')
|
||||||
|
adjust_type = request.args.get('adjust_type', '')
|
||||||
|
status = request.args.get('status', '')
|
||||||
|
|
||||||
|
query = StockAdjustment.query
|
||||||
|
|
||||||
|
if keyword:
|
||||||
|
query = query.filter(
|
||||||
|
db.or_(
|
||||||
|
StockAdjustment.order_no.ilike(f'%{keyword}%'),
|
||||||
|
StockAdjustment.sku.ilike(f'%{keyword}%'),
|
||||||
|
StockAdjustment.material_name.ilike(f'%{keyword}%')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if adjust_type:
|
||||||
|
query = query.filter(StockAdjustment.adjust_type == adjust_type)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
query = query.filter(StockAdjustment.status == status)
|
||||||
|
|
||||||
|
# 按创建时间降序
|
||||||
|
query = query.order_by(StockAdjustment.create_time.desc())
|
||||||
|
|
||||||
|
pagination = query.paginate(page=page, per_page=limit, error_out=False)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'code': 200,
|
||||||
|
'data': {
|
||||||
|
'items': [item.to_dict() for item in pagination.items],
|
||||||
|
'total': pagination.total,
|
||||||
|
'page': page,
|
||||||
|
'limit': limit
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# 2. 创建调整单
|
||||||
|
# POST /api/v1/stock/adjustment/create
|
||||||
|
# --------------------------------------------------------
|
||||||
|
@adjustment_bp.route('/create', methods=['POST'])
|
||||||
|
@jwt_required()
|
||||||
|
@permission_required('stock_adjustment:operation')
|
||||||
|
def create():
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({'code': 400, 'msg': '请求参数不能为空'}), 400
|
||||||
|
|
||||||
|
# 必填字段验证
|
||||||
|
required_fields = ['source_table', 'stock_id', 'adjust_type', 'adjust_quantity', 'reason']
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in data or not data.get(field):
|
||||||
|
return jsonify({'code': 400, 'msg': f'{field} 为必填项'}), 400
|
||||||
|
|
||||||
|
source_table = data['source_table']
|
||||||
|
stock_id = int(data['stock_id'])
|
||||||
|
adjust_type = data['adjust_type'] # 'profit' or 'loss'
|
||||||
|
adjust_quantity = float(data['adjust_quantity'])
|
||||||
|
reason = data['reason']
|
||||||
|
operator = get_jwt_identity() or 'system'
|
||||||
|
|
||||||
|
# 获取库存记录
|
||||||
|
StockModel = get_stock_model(source_table)
|
||||||
|
if not StockModel:
|
||||||
|
return jsonify({'code': 400, 'msg': '无效的库存类型'}), 400
|
||||||
|
|
||||||
|
stock = StockModel.query.get(stock_id)
|
||||||
|
if not stock:
|
||||||
|
return jsonify({'code': 404, 'msg': '库存记录不存在'}), 404
|
||||||
|
|
||||||
|
# 获取物料信息
|
||||||
|
base_id = getattr(stock, 'base_id', None)
|
||||||
|
material = MaterialBase.query.get(base_id) if base_id else None
|
||||||
|
|
||||||
|
# 计算库存变动
|
||||||
|
if adjust_type == 'profit':
|
||||||
|
# 盘盈:增加库存
|
||||||
|
new_stock_qty = float(stock.stock_quantity or 0) + adjust_quantity
|
||||||
|
new_avail_qty = float(stock.available_quantity or 0) + adjust_quantity
|
||||||
|
elif adjust_type == 'loss':
|
||||||
|
# 盘亏:减少库存
|
||||||
|
new_stock_qty = float(stock.stock_quantity or 0) - adjust_quantity
|
||||||
|
new_avail_qty = float(stock.available_quantity or 0) - adjust_quantity
|
||||||
|
if new_stock_qty < 0 or new_avail_qty < 0:
|
||||||
|
return jsonify({'code': 400, 'msg': '库存不足,无法盘亏'}), 400
|
||||||
|
else:
|
||||||
|
return jsonify({'code': 400, 'msg': '无效的调整类型'}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 创建调整单
|
||||||
|
adjustment = StockAdjustment(
|
||||||
|
order_no=generate_order_no(),
|
||||||
|
base_id=base_id,
|
||||||
|
stock_id=stock_id,
|
||||||
|
source_table=source_table,
|
||||||
|
sku=getattr(stock, 'sku', None) or getattr(stock, 'SKU', None),
|
||||||
|
material_name=material.name if material else getattr(stock, 'sku', '未知'),
|
||||||
|
spec_model=getattr(material, 'spec_model', None) if material else None,
|
||||||
|
warehouse_location=getattr(stock, 'warehouse_location', None),
|
||||||
|
adjust_type=adjust_type,
|
||||||
|
adjust_quantity=adjust_quantity,
|
||||||
|
reason=reason,
|
||||||
|
status='completed',
|
||||||
|
operator=operator
|
||||||
|
)
|
||||||
|
db.session.add(adjustment)
|
||||||
|
|
||||||
|
# 更新库存
|
||||||
|
stock.stock_quantity = new_stock_qty
|
||||||
|
stock.available_quantity = new_avail_qty
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# 3. 获取库存列表(用于选择物料)
|
||||||
|
# GET /api/v1/stock/adjustment/stocks
|
||||||
|
# --------------------------------------------------------
|
||||||
|
@adjustment_bp.route('/stocks', methods=['GET'])
|
||||||
|
@jwt_required()
|
||||||
|
@permission_required('stock_adjustment:list')
|
||||||
|
def get_stocks():
|
||||||
|
"""获取可用于调整的库存列表"""
|
||||||
|
source_table = request.args.get('source_table', 'stock_buy')
|
||||||
|
keyword = request.args.get('keyword', '')
|
||||||
|
page = int(request.args.get('page', 1))
|
||||||
|
limit = int(request.args.get('limit', 20))
|
||||||
|
|
||||||
|
StockModel = get_stock_model(source_table)
|
||||||
|
if not StockModel:
|
||||||
|
return jsonify({'code': 400, 'msg': '无效的库存类型'}), 400
|
||||||
|
|
||||||
|
query = StockModel.query.filter(StockModel.stock_quantity > 0)
|
||||||
|
|
||||||
|
if keyword:
|
||||||
|
query = query.filter(
|
||||||
|
db.or_(
|
||||||
|
StockModel.sku.ilike(f'%{keyword}%'),
|
||||||
|
StockModel.barcode.ilike(f'%{keyword}%')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
pagination = query.paginate(page=page, per_page=limit, error_out=False)
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for stock in pagination.items:
|
||||||
|
base_id = getattr(stock, 'base_id', None)
|
||||||
|
material = MaterialBase.query.get(base_id) if base_id else None
|
||||||
|
items.append({
|
||||||
|
'stock_id': stock.id,
|
||||||
|
'source_table': source_table,
|
||||||
|
'sku': getattr(stock, 'sku', None) or getattr(stock, 'SKU', None),
|
||||||
|
'material_name': material.name if material else getattr(stock, 'sku', '未知'),
|
||||||
|
'spec_model': getattr(material, 'spec_model', None) if material else None,
|
||||||
|
'stock_quantity': float(stock.stock_quantity or 0),
|
||||||
|
'available_quantity': float(stock.available_quantity or 0),
|
||||||
|
'warehouse_location': getattr(stock, 'warehouse_location', None),
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'code': 200,
|
||||||
|
'data': {
|
||||||
|
'items': items,
|
||||||
|
'total': pagination.total,
|
||||||
|
'page': page,
|
||||||
|
'limit': limit
|
||||||
|
}
|
||||||
|
})
|
||||||
61
inventory-backend/app/models/stock/adjustment.py
Normal file
61
inventory-backend/app/models/stock/adjustment.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# app/models/stock/adjustment.py
|
||||||
|
from app.extensions import db, beijing_time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class StockAdjustment(db.Model):
|
||||||
|
"""
|
||||||
|
盘盈盘亏调整表
|
||||||
|
用于记录财务/主管手动发起的库存修正
|
||||||
|
"""
|
||||||
|
__tablename__ = 'stock_adjustment'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
# 单号,如 ADJ-YYYYMMDD-XXXX
|
||||||
|
order_no = db.Column(db.String(50), unique=True, nullable=False, index=True)
|
||||||
|
# 关联物料基础表
|
||||||
|
base_id = db.Column(db.Integer, db.ForeignKey('material_base.id'))
|
||||||
|
# 关联具体库存行ID
|
||||||
|
stock_id = db.Column(db.Integer)
|
||||||
|
# 库存类型 (stock_buy/stock_semi/stock_product)
|
||||||
|
source_table = db.Column(db.String(50))
|
||||||
|
# 物料冗余信息
|
||||||
|
sku = db.Column(db.String(100))
|
||||||
|
material_name = db.Column(db.String(255))
|
||||||
|
spec_model = db.Column(db.String(255))
|
||||||
|
# 库位
|
||||||
|
warehouse_location = db.Column(db.String(100))
|
||||||
|
# 调整类型:'profit' 盘盈 / 'loss' 盘亏
|
||||||
|
adjust_type = db.Column(db.String(20), nullable=False)
|
||||||
|
# 调整数量(绝对值)
|
||||||
|
adjust_quantity = db.Column(db.Numeric(19, 4), nullable=False)
|
||||||
|
# 原因说明(必填)
|
||||||
|
reason = db.Column(db.String(500), nullable=False)
|
||||||
|
# 状态:'pending' 待处理 / 'completed' 已完成 / 'cancelled' 已取消
|
||||||
|
status = db.Column(db.String(20), default='pending')
|
||||||
|
# 操作人/经办人
|
||||||
|
operator = db.Column(db.String(100))
|
||||||
|
# 创建时间
|
||||||
|
create_time = db.Column(db.DateTime, default=beijing_time)
|
||||||
|
# 更新时间
|
||||||
|
update_time = db.Column(db.DateTime, default=beijing_time, onupdate=beijing_time)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'order_no': self.order_no,
|
||||||
|
'base_id': self.base_id,
|
||||||
|
'stock_id': self.stock_id,
|
||||||
|
'source_table': self.source_table,
|
||||||
|
'sku': self.sku,
|
||||||
|
'material_name': self.material_name,
|
||||||
|
'spec_model': self.spec_model,
|
||||||
|
'warehouse_location': self.warehouse_location,
|
||||||
|
'adjust_type': self.adjust_type,
|
||||||
|
'adjust_quantity': float(self.adjust_quantity or 0),
|
||||||
|
'reason': self.reason,
|
||||||
|
'status': self.status,
|
||||||
|
'operator': self.operator,
|
||||||
|
'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,
|
||||||
|
}
|
||||||
@ -99,6 +99,13 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: 'InventoryStocktake',
|
name: 'InventoryStocktake',
|
||||||
component: () => import('@/views/stock/stocktake/index.vue'),
|
component: () => import('@/views/stock/stocktake/index.vue'),
|
||||||
meta: { title: '库存盘点' }
|
meta: { title: '库存盘点' }
|
||||||
|
},
|
||||||
|
// ★ [新增] 盘盈盘亏管理页面
|
||||||
|
{
|
||||||
|
path: 'adjustment',
|
||||||
|
name: 'StockAdjustment',
|
||||||
|
component: () => import('@/views/stock/adjustment/index.vue'),
|
||||||
|
meta: { title: '盘盈盘亏管理' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
307
inventory-web/src/views/stock/adjustment/index.vue
Normal file
307
inventory-web/src/views/stock/adjustment/index.vue
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- 筛选条件 -->
|
||||||
|
<div class="filter-container">
|
||||||
|
<el-input v-model="keyword" placeholder="搜索单号/SKU/物料名称" style="width: 220px" @keyup.enter="fetchData" clearable />
|
||||||
|
<el-select v-model="searchAdjustType" placeholder="调整类型" style="width: 120px" clearable>
|
||||||
|
<el-option label="盘盈" value="profit" />
|
||||||
|
<el-option label="盘亏" value="loss" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="searchStatus" placeholder="状态" style="width: 120px" clearable>
|
||||||
|
<el-option label="待处理" value="pending" />
|
||||||
|
<el-option label="已完成" value="completed" />
|
||||||
|
<el-option label="已取消" value="cancelled" />
|
||||||
|
</el-select>
|
||||||
|
<el-button type="primary" @click="fetchData">查询</el-button>
|
||||||
|
<el-button v-if="userStore.hasPermission('stock_adjustment:operation')" type="success" @click="showDialog = true">
|
||||||
|
新增调整单
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<el-table :data="list" border stripe v-loading="loading" style="margin-top: 20px">
|
||||||
|
<el-table-column prop="order_no" label="单号" width="180" />
|
||||||
|
<el-table-column prop="sku" label="SKU" width="140" />
|
||||||
|
<el-table-column prop="material_name" label="物料名称" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="spec_model" label="规格型号" width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="warehouse_location" label="库位" width="100" />
|
||||||
|
<el-table-column label="调整类型" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.adjust_type === 'profit' ? 'success' : 'danger'">
|
||||||
|
{{ row.adjust_type === 'profit' ? '盘盈' : '盘亏' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="adjust_quantity" label="调整数量" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :style="{ color: row.adjust_type === 'profit' ? '#67C23A' : '#F56C6C', fontWeight: 'bold' }">
|
||||||
|
{{ row.adjust_type === 'profit' ? '+' : '-' }}{{ row.adjust_quantity }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="reason" label="调整原因" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="operator" label="操作人" width="100" />
|
||||||
|
<el-table-column prop="status" label="状态" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.status === 'completed'" type="success" size="small">已完成</el-tag>
|
||||||
|
<el-tag v-else-if="row.status === 'pending'" type="warning" size="small">待处理</el-tag>
|
||||||
|
<el-tag v-else type="info" size="small">已取消</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="create_time" label="创建时间" width="160" />
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="prev, pager, next, total"
|
||||||
|
:total="total"
|
||||||
|
:page-size="limit"
|
||||||
|
v-model:current-page="page"
|
||||||
|
@current-change="fetchData"
|
||||||
|
style="margin-top: 20px; justify-content: center"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 新增调整单弹窗 -->
|
||||||
|
<el-dialog v-model="showDialog" title="新增盘盈盘亏调整单" width="700px" :close-on-click-modal="false">
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||||
|
<el-form-item label="选择物料" prop="stock_id">
|
||||||
|
<el-select
|
||||||
|
v-model="form.source_table"
|
||||||
|
placeholder="库存类型"
|
||||||
|
style="width: 150px; margin-right: 10px"
|
||||||
|
@change="handleSourceTableChange"
|
||||||
|
>
|
||||||
|
<el-option label="采购库存" value="stock_buy" />
|
||||||
|
<el-option label="半成品库存" value="stock_semi" />
|
||||||
|
<el-option label="成品库存" value="stock_product" />
|
||||||
|
</el-select>
|
||||||
|
<el-button @click="openStockSelector" type="primary" plain>选择物料</el-button>
|
||||||
|
<span v-if="selectedStock" style="margin-left: 10px">
|
||||||
|
{{ selectedStock.sku }} - {{ selectedStock.material_name }} (库存: {{ selectedStock.stock_quantity }})
|
||||||
|
</span>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="调整类型" prop="adjust_type">
|
||||||
|
<el-radio-group v-model="form.adjust_type">
|
||||||
|
<el-radio label="profit">盘盈 (加库存)</el-radio>
|
||||||
|
<el-radio label="loss">盘亏 (减库存)</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="调整数量" prop="adjust_quantity">
|
||||||
|
<el-input-number v-model="form.adjust_quantity" :min="1" :max="form.adjust_type === 'loss' ? (selectedStock?.stock_quantity || 9999) : 99999" style="width: 200px" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="调整原因" prop="reason">
|
||||||
|
<el-input v-model="form.reason" type="textarea" :rows="3" placeholder="请输入调整原因(必填)" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitForm" :loading="submitLoading">提交</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 物料选择弹窗 -->
|
||||||
|
<el-dialog v-model="showStockDialog" title="选择物料" width="800px">
|
||||||
|
<div class="filter-container" style="margin-bottom: 15px">
|
||||||
|
<el-input v-model="stockKeyword" placeholder="搜索SKU/条码" style="width: 200px" @keyup.enter="fetchStocks" clearable />
|
||||||
|
<el-button type="primary" @click="fetchStocks">搜索</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="stockList" border stripe v-loading="stockLoading" @row-click="selectStock" highlight-current-row style="cursor: pointer">
|
||||||
|
<el-table-column prop="sku" label="SKU" width="140" />
|
||||||
|
<el-table-column prop="material_name" label="物料名称" min-width="150" />
|
||||||
|
<el-table-column prop="spec_model" label="规格型号" width="120" />
|
||||||
|
<el-table-column prop="stock_quantity" label="当前库存" width="100" align="center" />
|
||||||
|
<el-table-column prop="warehouse_location" label="库位" width="100" />
|
||||||
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="prev, pager, next"
|
||||||
|
:total="stockTotal"
|
||||||
|
:page-size="stockLimit"
|
||||||
|
v-model:current-page="stockPage"
|
||||||
|
@current-change="fetchStocks"
|
||||||
|
style="margin-top: 15px; justify-content: center"
|
||||||
|
/>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 列表数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const list = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const limit = ref(10)
|
||||||
|
|
||||||
|
// 筛选条件
|
||||||
|
const keyword = ref('')
|
||||||
|
const searchAdjustType = ref('')
|
||||||
|
const searchStatus = ref('')
|
||||||
|
|
||||||
|
// 新增表单
|
||||||
|
const showDialog = ref(false)
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
const formRef = ref()
|
||||||
|
const form = ref({
|
||||||
|
source_table: 'stock_buy',
|
||||||
|
stock_id: null,
|
||||||
|
adjust_type: 'profit',
|
||||||
|
adjust_quantity: 1,
|
||||||
|
reason: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
stock_id: [{ required: true, message: '请选择物料', trigger: 'change' }],
|
||||||
|
adjust_type: [{ required: true, message: '请选择调整类型', trigger: 'change' }],
|
||||||
|
adjust_quantity: [{ required: true, message: '请输入调整数量', trigger: 'blur' }],
|
||||||
|
reason: [{ required: true, message: '请输入调整原因', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 物料选择
|
||||||
|
const showStockDialog = ref(false)
|
||||||
|
const stockLoading = ref(false)
|
||||||
|
const stockList = ref([])
|
||||||
|
const stockTotal = ref(0)
|
||||||
|
const stockPage = ref(1)
|
||||||
|
const stockLimit = ref(20)
|
||||||
|
const stockKeyword = ref('')
|
||||||
|
const selectedStock = ref<any>(null)
|
||||||
|
|
||||||
|
// 获取列表
|
||||||
|
async function fetchData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
page: page.value.toString(),
|
||||||
|
limit: limit.value.toString()
|
||||||
|
})
|
||||||
|
if (keyword.value) params.append('keyword', keyword.value)
|
||||||
|
if (searchAdjustType.value) params.append('adjust_type', searchAdjustType.value)
|
||||||
|
if (searchStatus.value) params.append('status', searchStatus.value)
|
||||||
|
|
||||||
|
const res = await fetch(`/api/v1/stock/adjustment/list?${params}`)
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code === 200) {
|
||||||
|
list.value = json.data.items
|
||||||
|
total.value = json.data.total
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('获取数据失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取库存列表
|
||||||
|
async function fetchStocks() {
|
||||||
|
stockLoading.value = true
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
source_table: form.value.source_table,
|
||||||
|
page: stockPage.value.toString(),
|
||||||
|
limit: stockLimit.value.toString()
|
||||||
|
})
|
||||||
|
if (stockKeyword.value) params.append('keyword', stockKeyword.value)
|
||||||
|
|
||||||
|
const res = await fetch(`/api/v1/stock/adjustment/stocks?${params}`)
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code === 200) {
|
||||||
|
stockList.value = json.data.items
|
||||||
|
stockTotal.value = json.data.total
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('获取库存失败')
|
||||||
|
} finally {
|
||||||
|
stockLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开物料选择
|
||||||
|
function openStockSelector() {
|
||||||
|
if (!form.value.source_table) {
|
||||||
|
ElMessage.warning('请先选择库存类型')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stockKeyword.value = ''
|
||||||
|
stockPage.value = 1
|
||||||
|
fetchStocks()
|
||||||
|
showStockDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择物料
|
||||||
|
function selectStock(row: any) {
|
||||||
|
selectedStock.value = row
|
||||||
|
form.value.stock_id = row.stock_id
|
||||||
|
showStockDialog.value = false
|
||||||
|
// 盘亏时自动限制最大数量
|
||||||
|
if (form.value.adjust_type === 'loss') {
|
||||||
|
form.value.adjust_quantity = Math.min(form.value.adjust_quantity, row.stock_quantity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 库存类型变化时清空选择
|
||||||
|
function handleSourceTableChange() {
|
||||||
|
selectedStock.value = null
|
||||||
|
form.value.stock_id = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
async function submitForm() {
|
||||||
|
if (!formRef.value) return
|
||||||
|
await formRef.value.validate()
|
||||||
|
|
||||||
|
submitLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/stock/adjustment/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(form.value)
|
||||||
|
})
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code === 200) {
|
||||||
|
ElMessage.success('提交成功')
|
||||||
|
showDialog.value = false
|
||||||
|
fetchData()
|
||||||
|
// 重置表单
|
||||||
|
form.value = {
|
||||||
|
source_table: 'stock_buy',
|
||||||
|
stock_id: null,
|
||||||
|
adjust_type: 'profit',
|
||||||
|
adjust_quantity: 1,
|
||||||
|
reason: ''
|
||||||
|
}
|
||||||
|
selectedStock.value = null
|
||||||
|
} else {
|
||||||
|
ElMessage.error(json.msg || '提交失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('提交失败')
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.filter-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
25
stock_adjustment.sql
Normal file
25
stock_adjustment.sql
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
-- 盘盈盘亏调整表
|
||||||
|
-- 用于记录财务/主管手动发起的库存修正
|
||||||
|
CREATE TABLE IF NOT EXISTS `stock_adjustment` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`order_no` VARCHAR(50) NOT NULL COMMENT '单号,如 ADJ-YYYYMMDD-XXXX',
|
||||||
|
`base_id` INT DEFAULT NULL COMMENT '关联物料基础表',
|
||||||
|
`stock_id` INT DEFAULT NULL COMMENT '关联具体库存行ID',
|
||||||
|
`source_table` VARCHAR(50) DEFAULT NULL COMMENT '库存类型 (stock_buy/stock_semi/stock_product)',
|
||||||
|
`sku` VARCHAR(100) DEFAULT NULL COMMENT '物料SKU',
|
||||||
|
`material_name` VARCHAR(255) DEFAULT NULL COMMENT '物料名称',
|
||||||
|
`spec_model` VARCHAR(255) DEFAULT NULL COMMENT '规格型号',
|
||||||
|
`warehouse_location` VARCHAR(100) DEFAULT NULL COMMENT '库位',
|
||||||
|
`adjust_type` VARCHAR(20) NOT NULL COMMENT '调整类型:profit 盘盈 / loss 盘亏',
|
||||||
|
`adjust_quantity` DECIMAL(19,4) NOT NULL COMMENT '调整数量(绝对值)',
|
||||||
|
`reason` VARCHAR(500) NOT NULL COMMENT '原因说明(必填)',
|
||||||
|
`status` VARCHAR(20) DEFAULT 'pending' COMMENT '状态:pending 待处理 / completed 已完成 / cancelled 已取消',
|
||||||
|
`operator` VARCHAR(100) DEFAULT NULL COMMENT '操作人/经办人',
|
||||||
|
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_order_no` (`order_no`),
|
||||||
|
KEY `idx_create_time` (`create_time`),
|
||||||
|
KEY `idx_adjust_type` (`adjust_type`),
|
||||||
|
KEY `idx_status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='盘盈盘亏调整表';
|
||||||
Reference in New Issue
Block a user