出库逻辑添加,扫码识别编码成功,后续对应逻辑没有完成
This commit is contained in:
153
inventory-backend/app/services/outbound_service.py
Normal file
153
inventory-backend/app/services/outbound_service.py
Normal file
@ -0,0 +1,153 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from sqlalchemy import or_
|
||||
from app.extensions import db
|
||||
from app.models.outbound import TransOutbound
|
||||
|
||||
# 导入所有库存实体模型,用于查找和扣减
|
||||
from app.models.inbound.buy import StockBuy
|
||||
from app.models.inbound.semi import StockSemi
|
||||
from app.models.inbound.product import StockProduct
|
||||
|
||||
|
||||
class OutboundService:
|
||||
|
||||
@staticmethod
|
||||
def generate_outbound_no():
|
||||
"""生成出库单号: OUT-yyyyMMdd-随机码"""
|
||||
date_str = datetime.now().strftime('%Y%m%d')
|
||||
short_uuid = uuid.uuid4().hex[:6].upper()
|
||||
return f"OUT-{date_str}-{short_uuid}"
|
||||
|
||||
@staticmethod
|
||||
def get_stock_by_barcode(barcode):
|
||||
"""
|
||||
根据条码在各个库存表中查找
|
||||
优先级: 成品 -> 半成品 -> 采购件
|
||||
"""
|
||||
if not barcode:
|
||||
return None
|
||||
|
||||
# 1. 查成品
|
||||
prod = StockProduct.query.filter_by(barcode=barcode).first()
|
||||
if prod:
|
||||
return OutboundService._format_scan_result(prod, 'stock_product', prod.sku)
|
||||
|
||||
# 2. 查半成品
|
||||
semi = StockSemi.query.filter_by(barcode=barcode).first()
|
||||
if semi:
|
||||
return OutboundService._format_scan_result(semi, 'stock_semi', semi.sku)
|
||||
|
||||
# 3. 查采购件
|
||||
buy = StockBuy.query.filter_by(barcode=barcode).first()
|
||||
if buy:
|
||||
# 采购件可能需要关联 material_base 获取名称,这里假设 base_id 关联已建立
|
||||
name = buy.base.name if buy.base else "未知采购件"
|
||||
spec = buy.base.spec_model if buy.base else ""
|
||||
return OutboundService._format_scan_result(buy, 'stock_buy', buy.sku, name, spec)
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _format_scan_result(item, table_name, sku, name=None, spec=None):
|
||||
"""格式化返回给前端的数据结构"""
|
||||
# 如果没有传 name (例如成品/半成品),尝试通过关联获取,或者直接用 SKU 代替
|
||||
item_name = name
|
||||
item_spec = spec
|
||||
|
||||
if not item_name and hasattr(item, 'base') and item.base:
|
||||
item_name = item.base.name
|
||||
item_spec = item.base.spec_model
|
||||
|
||||
return {
|
||||
'id': item.id,
|
||||
'sku': sku,
|
||||
'name': item_name or sku,
|
||||
'spec_model': item_spec or '',
|
||||
'source_table': table_name,
|
||||
'stock_quantity': float(item.stock_quantity),
|
||||
'available_quantity': float(item.available_quantity),
|
||||
'batch_number': getattr(item, 'batch_number', ''),
|
||||
'warehouse_location': getattr(item, 'warehouse_location', '')
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def create_outbound(data, operator_name='System'):
|
||||
"""执行出库逻辑:扣减库存 + 记录日志"""
|
||||
source_table = data.get('source_table')
|
||||
stock_id = data.get('stock_id')
|
||||
quantity = float(data.get('quantity', 0))
|
||||
|
||||
if quantity <= 0:
|
||||
raise ValueError("出库数量必须大于0")
|
||||
|
||||
# 1. 获取对应的库存记录模型
|
||||
model_map = {
|
||||
'stock_buy': StockBuy,
|
||||
'stock_semi': StockSemi,
|
||||
'stock_product': StockProduct
|
||||
}
|
||||
|
||||
ModelClass = model_map.get(source_table)
|
||||
if not ModelClass:
|
||||
raise ValueError(f"未知的库存来源表: {source_table}")
|
||||
|
||||
# 2. 锁定并查询库存 (使用 with_for_update 防止并发扣减)
|
||||
stock_item = ModelClass.query.with_for_update().get(stock_id)
|
||||
if not stock_item:
|
||||
raise ValueError("库存记录不存在")
|
||||
|
||||
if stock_item.available_quantity < quantity:
|
||||
raise ValueError(f"库存不足!当前可用: {stock_item.available_quantity}, 请求出库: {quantity}")
|
||||
|
||||
try:
|
||||
# 3. 扣减库存
|
||||
stock_item.stock_quantity -= quantity
|
||||
stock_item.available_quantity -= quantity
|
||||
|
||||
# 4. 创建出库记录
|
||||
new_outbound = TransOutbound(
|
||||
outbound_no=OutboundService.generate_outbound_no(),
|
||||
sku=data.get('sku'),
|
||||
source_table=source_table,
|
||||
stock_id=stock_id,
|
||||
barcode=data.get('barcode'),
|
||||
outbound_type=data.get('outbound_type', 'SALES'),
|
||||
quantity=quantity,
|
||||
consumer_name=data.get('consumer_name'),
|
||||
signature_path=data.get('signature_path'), # 存储签名的 URL
|
||||
operator_name=operator_name,
|
||||
remark=data.get('remark')
|
||||
)
|
||||
|
||||
db.session.add(new_outbound)
|
||||
db.session.commit()
|
||||
|
||||
return new_outbound
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def get_list(page=1, per_page=10, keyword=None, start_date=None, end_date=None):
|
||||
query = TransOutbound.query.order_by(TransOutbound.outbound_time.desc())
|
||||
|
||||
if keyword:
|
||||
query = query.filter(or_(
|
||||
TransOutbound.outbound_no.ilike(f'%{keyword}%'),
|
||||
TransOutbound.consumer_name.ilike(f'%{keyword}%'),
|
||||
TransOutbound.sku.ilike(f'%{keyword}%')
|
||||
))
|
||||
|
||||
if start_date and end_date:
|
||||
# 假设传入的是 'YYYY-MM-DD',需要处理时间范围
|
||||
query = query.filter(TransOutbound.outbound_time.between(start_date, end_date))
|
||||
|
||||
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
return {
|
||||
'items': [item.to_dict() for item in pagination.items],
|
||||
'total': pagination.total,
|
||||
'pages': pagination.pages,
|
||||
'current_page': page
|
||||
}
|
||||
Reference in New Issue
Block a user