采购件入库与出库相关联
This commit is contained in:
@ -1,7 +1,10 @@
|
|||||||
# app/services/inbound/buy_service.py
|
# 文件路径: inventory-backend/app/services/inbound/buy_service.py
|
||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.inbound.buy import StockBuy
|
from app.models.inbound.buy import StockBuy
|
||||||
from app.models.base import MaterialBase
|
from app.models.base import MaterialBase
|
||||||
|
# 引入出库记录模型,用于查询流转历史
|
||||||
|
from app.models.outbound import TransOutbound
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sqlalchemy import or_, func, text
|
from sqlalchemy import or_, func, text
|
||||||
import traceback
|
import traceback
|
||||||
@ -9,6 +12,10 @@ import json
|
|||||||
|
|
||||||
|
|
||||||
class BuyInboundService:
|
class BuyInboundService:
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 1. 基础物料搜索 (供下拉框使用)
|
||||||
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def search_base_material(keyword):
|
def search_base_material(keyword):
|
||||||
"""
|
"""
|
||||||
@ -45,6 +52,9 @@ class BuyInboundService:
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 2. 新增入库逻辑
|
||||||
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def handle_inbound(data):
|
def handle_inbound(data):
|
||||||
"""
|
"""
|
||||||
@ -73,32 +83,23 @@ class BuyInboundService:
|
|||||||
in_qty = float(data.get('in_quantity') or 0)
|
in_qty = float(data.get('in_quantity') or 0)
|
||||||
u_price = float(data.get('unit_price') or 0)
|
u_price = float(data.get('unit_price') or 0)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# 1. 获取全局打印流水号 (跨表唯一)
|
# 1. 获取全局打印流水号 (跨表唯一)
|
||||||
# ------------------------------------------------------------------
|
|
||||||
seq_sql = text("SELECT nextval('global_print_seq')")
|
seq_sql = text("SELECT nextval('global_print_seq')")
|
||||||
result = db.session.execute(seq_sql)
|
result = db.session.execute(seq_sql)
|
||||||
next_global_id = result.scalar()
|
next_global_id = result.scalar()
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# 2. 自动生成 SKU (格式: 00000001)
|
# 2. 自动生成 SKU (格式: 00000001)
|
||||||
# ------------------------------------------------------------------
|
|
||||||
generated_sku = str(next_global_id).zfill(10)
|
generated_sku = str(next_global_id).zfill(10)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# 3. 条码逻辑处理
|
# 3. 条码逻辑处理
|
||||||
# ------------------------------------------------------------------
|
|
||||||
final_barcode = data.get('barcode')
|
final_barcode = data.get('barcode')
|
||||||
if not final_barcode:
|
if not final_barcode:
|
||||||
final_barcode = generated_sku
|
final_barcode = generated_sku
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# 4. 图片列表转 JSON 字符串处理
|
# 4. 图片列表转 JSON 字符串处理
|
||||||
# ------------------------------------------------------------------
|
|
||||||
arrival_list = data.get('arrival_photo', [])
|
arrival_list = data.get('arrival_photo', [])
|
||||||
report_list = data.get('inspection_report', [])
|
report_list = data.get('inspection_report', [])
|
||||||
|
|
||||||
# 确保是列表类型,防止前端传错导致报错
|
|
||||||
if not isinstance(arrival_list, list): arrival_list = []
|
if not isinstance(arrival_list, list): arrival_list = []
|
||||||
if not isinstance(report_list, list): report_list = []
|
if not isinstance(report_list, list): report_list = []
|
||||||
|
|
||||||
@ -127,7 +128,7 @@ class BuyInboundService:
|
|||||||
original_link=data.get('source_link'),
|
original_link=data.get('source_link'),
|
||||||
detail_link=data.get('detail_link'),
|
detail_link=data.get('detail_link'),
|
||||||
|
|
||||||
# [核心修改] 将列表转为 JSON 字符串存储
|
# 将列表转为 JSON 字符串存储
|
||||||
arrival_photo=json.dumps(arrival_list),
|
arrival_photo=json.dumps(arrival_list),
|
||||||
inspection_report=json.dumps(report_list)
|
inspection_report=json.dumps(report_list)
|
||||||
)
|
)
|
||||||
@ -142,6 +143,9 @@ class BuyInboundService:
|
|||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 3. 更新入库逻辑
|
||||||
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_inbound(stock_id, data):
|
def update_inbound(stock_id, data):
|
||||||
"""
|
"""
|
||||||
@ -149,7 +153,6 @@ class BuyInboundService:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
print(f"----- UPDATE DEBUG: ID={stock_id} -----")
|
print(f"----- UPDATE DEBUG: ID={stock_id} -----")
|
||||||
|
|
||||||
stock = StockBuy.query.get(stock_id)
|
stock = StockBuy.query.get(stock_id)
|
||||||
if not stock:
|
if not stock:
|
||||||
raise ValueError("记录不存在")
|
raise ValueError("记录不存在")
|
||||||
@ -176,7 +179,6 @@ class BuyInboundService:
|
|||||||
if frontend_key in data:
|
if frontend_key in data:
|
||||||
setattr(stock, db_attr, data[frontend_key])
|
setattr(stock, db_attr, data[frontend_key])
|
||||||
|
|
||||||
# [核心修改] 图片字段更新 (List -> JSON String)
|
|
||||||
if 'arrival_photo' in data:
|
if 'arrival_photo' in data:
|
||||||
imgs = data['arrival_photo']
|
imgs = data['arrival_photo']
|
||||||
if isinstance(imgs, list):
|
if isinstance(imgs, list):
|
||||||
@ -197,6 +199,7 @@ class BuyInboundService:
|
|||||||
if new_qty != old_qty:
|
if new_qty != old_qty:
|
||||||
diff = new_qty - old_qty
|
diff = new_qty - old_qty
|
||||||
stock.in_quantity = new_qty
|
stock.in_quantity = new_qty
|
||||||
|
# 注意:手动修改入库量时,同步调整总库存和可用库存
|
||||||
stock.stock_quantity = float(stock.stock_quantity) + diff
|
stock.stock_quantity = float(stock.stock_quantity) + diff
|
||||||
stock.available_quantity = float(stock.available_quantity) + diff
|
stock.available_quantity = float(stock.available_quantity) + diff
|
||||||
qty_changed = True
|
qty_changed = True
|
||||||
@ -221,6 +224,9 @@ class BuyInboundService:
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 4. 删除逻辑
|
||||||
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_inbound(stock_id):
|
def delete_inbound(stock_id):
|
||||||
"""
|
"""
|
||||||
@ -237,13 +243,32 @@ class BuyInboundService:
|
|||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 5. [新增] 获取出库流转历史 (挂钩出库记录)
|
||||||
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_list(page, limit, keyword=None):
|
def get_outbound_history(stock_id):
|
||||||
"""
|
"""
|
||||||
获取分页列表
|
查询该入库单对应的所有出库记录
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 1. 查询分页数据
|
records = TransOutbound.query.filter_by(
|
||||||
|
source_table='stock_buy',
|
||||||
|
stock_id=stock_id
|
||||||
|
).order_by(TransOutbound.outbound_time.desc()).all()
|
||||||
|
|
||||||
|
return [r.to_dict() for r in records]
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
return []
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 6. 获取列表 (含动态状态计算)
|
||||||
|
# ============================================================
|
||||||
|
@staticmethod
|
||||||
|
def get_list(page, limit, keyword=None):
|
||||||
|
try:
|
||||||
|
# 1. 联表查询:StockBuy join MaterialBase
|
||||||
query = db.session.query(StockBuy).outerjoin(MaterialBase, StockBuy.base_id == MaterialBase.id)
|
query = db.session.query(StockBuy).outerjoin(MaterialBase, StockBuy.base_id == MaterialBase.id)
|
||||||
|
|
||||||
if keyword:
|
if keyword:
|
||||||
@ -253,18 +278,17 @@ class BuyInboundService:
|
|||||||
MaterialBase.spec_model.ilike(f'%{keyword}%'),
|
MaterialBase.spec_model.ilike(f'%{keyword}%'),
|
||||||
StockBuy.batch_number.ilike(f'%{keyword}%'),
|
StockBuy.batch_number.ilike(f'%{keyword}%'),
|
||||||
StockBuy.serial_number.ilike(f'%{keyword}%'),
|
StockBuy.serial_number.ilike(f'%{keyword}%'),
|
||||||
StockBuy.sku.ilike(f'%{keyword}%')
|
StockBuy.sku.ilike(f'%{keyword}%'),
|
||||||
|
StockBuy.supplier_name.ilike(f'%{keyword}%')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
pagination = query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit, error_out=False)
|
pagination = query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit, error_out=False)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# 计算总库存 (聚合)
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
current_items = pagination.items
|
current_items = pagination.items
|
||||||
base_ids = list(set([item.base_id for item in current_items if item.base_id]))
|
base_ids = list(set([item.base_id for item in current_items if item.base_id]))
|
||||||
|
|
||||||
|
# 2. 聚合统计 (计算该种物料的总库存)
|
||||||
stock_map = {}
|
stock_map = {}
|
||||||
if base_ids:
|
if base_ids:
|
||||||
aggregates = db.session.query(
|
aggregates = db.session.query(
|
||||||
@ -284,7 +308,6 @@ class BuyInboundService:
|
|||||||
if not json_str:
|
if not json_str:
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
# 兼容旧数据:如果不是 JSON 格式(比如是单个 URL),则包装成 list
|
|
||||||
if not json_str.startswith('['):
|
if not json_str.startswith('['):
|
||||||
return [json_str]
|
return [json_str]
|
||||||
return json.loads(json_str)
|
return json.loads(json_str)
|
||||||
@ -293,6 +316,20 @@ class BuyInboundService:
|
|||||||
|
|
||||||
items = []
|
items = []
|
||||||
for item in current_items:
|
for item in current_items:
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# [核心逻辑] 动态计算状态
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
qty_in = float(item.in_quantity or 0)
|
||||||
|
qty_avail = float(item.available_quantity or 0)
|
||||||
|
|
||||||
|
# 默认使用数据库字段
|
||||||
|
current_status = item.status
|
||||||
|
|
||||||
|
# 如果有入库量,但可用量为0,说明已经全部出库
|
||||||
|
if qty_in > 0 and qty_avail <= 0:
|
||||||
|
current_status = '出库'
|
||||||
|
|
||||||
|
# 获取聚合数据
|
||||||
mat_name = item.material.name if item.material else '未知物料'
|
mat_name = item.material.name if item.material else '未知物料'
|
||||||
mat_spec = item.material.spec_model if item.material else ''
|
mat_spec = item.material.spec_model if item.material else ''
|
||||||
mat_cat = item.material.category if item.material else ''
|
mat_cat = item.material.category if item.material else ''
|
||||||
@ -315,12 +352,15 @@ class BuyInboundService:
|
|||||||
'barcode': item.barcode,
|
'barcode': item.barcode,
|
||||||
'serial_number': item.serial_number,
|
'serial_number': item.serial_number,
|
||||||
'batch_number': item.batch_number,
|
'batch_number': item.batch_number,
|
||||||
'status': item.status,
|
|
||||||
|
# 使用动态计算的状态
|
||||||
|
'status': current_status,
|
||||||
|
|
||||||
'inspection_status': item.inspection_status,
|
'inspection_status': item.inspection_status,
|
||||||
|
|
||||||
'qty_inbound': float(item.in_quantity or 0),
|
'qty_inbound': qty_in,
|
||||||
'qty_stock': float(item.stock_quantity or 0),
|
'qty_stock': float(item.stock_quantity or 0),
|
||||||
'qty_available': float(item.available_quantity or 0),
|
'qty_available': qty_avail,
|
||||||
|
|
||||||
'sum_stock': stats['total_stock'],
|
'sum_stock': stats['total_stock'],
|
||||||
'sum_available': stats['total_avail'],
|
'sum_available': stats['total_avail'],
|
||||||
@ -336,7 +376,6 @@ class BuyInboundService:
|
|||||||
'source_link': item.original_link,
|
'source_link': item.original_link,
|
||||||
'detail_link': item.detail_link,
|
'detail_link': item.detail_link,
|
||||||
|
|
||||||
# [核心修改] 解析 JSON 字符串为数组返回给前端
|
|
||||||
'arrival_photo': parse_img_list(item.arrival_photo),
|
'arrival_photo': parse_img_list(item.arrival_photo),
|
||||||
'inspection_report': parse_img_list(item.inspection_report),
|
'inspection_report': parse_img_list(item.inspection_report),
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export default defineConfig({
|
|||||||
// 允许局域网访问前端页面
|
// 允许局域网访问前端页面
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 5173,
|
port: 5173,
|
||||||
https: false, // ★ [新增] 强制开启 HTTPS,否则浏览器会拦截摄像头
|
https: true, // ★ [新增] 强制开启 HTTPS,否则浏览器会拦截摄像头
|
||||||
proxy: {
|
proxy: {
|
||||||
// 拦截所有以 /api 开头的请求
|
// 拦截所有以 /api 开头的请求
|
||||||
'/api': {
|
'/api': {
|
||||||
|
|||||||
Reference in New Issue
Block a user