185 lines
6.8 KiB
Python
185 lines
6.8 KiB
Python
import uuid
|
||
from datetime import datetime
|
||
from app.extensions import db
|
||
from app.models.transaction import TransBorrow
|
||
from app.models.inbound.buy import StockBuy
|
||
from app.models.inbound.semi import StockSemi
|
||
from app.models.inbound.product import StockProduct
|
||
from sqlalchemy import desc, func
|
||
|
||
|
||
class TransService:
|
||
|
||
@staticmethod
|
||
def generate_borrow_no():
|
||
"""
|
||
生成借用单号: BOR-yyyyMMdd-0001 (按日流水)
|
||
逻辑:统计当天已存在的不同借用单号数量,+1 作为新序号
|
||
"""
|
||
now = datetime.now()
|
||
date_str = now.strftime('%Y%m%d')
|
||
prefix = f"BOR-{date_str}-"
|
||
|
||
# 使用 count distinct 来计算当天有多少个不同的借用单 (因为一单多货会占多行)
|
||
count = db.session.query(func.count(func.distinct(TransBorrow.borrow_no))) \
|
||
.filter(TransBorrow.borrow_no.like(f"{prefix}%")).scalar()
|
||
|
||
sequence = count + 1
|
||
return f"{prefix}{sequence:04d}"
|
||
|
||
@staticmethod
|
||
def create_borrow(data, operator_name='System'):
|
||
"""
|
||
借库逻辑:减少可用库存,不减总库存
|
||
"""
|
||
items = data.get('items', [])
|
||
borrower_name = data.get('borrower_name')
|
||
signature = data.get('signature_path') # 借用人签字
|
||
|
||
if not items: raise ValueError("物品列表为空")
|
||
if not borrower_name: raise ValueError("请输入借用人")
|
||
if not signature: raise ValueError("借用人必须签字")
|
||
|
||
borrow_no = TransService.generate_borrow_no()
|
||
model_map = {'stock_buy': StockBuy, 'stock_semi': StockSemi, 'stock_product': StockProduct}
|
||
|
||
try:
|
||
for item in items:
|
||
source_table = item.get('source_table')
|
||
stock_id = item.get('id')
|
||
qty = float(item.get('out_quantity', 0))
|
||
|
||
ModelClass = model_map.get(source_table)
|
||
if not ModelClass: continue
|
||
|
||
stock = ModelClass.query.with_for_update().get(stock_id)
|
||
if not stock: raise ValueError(f"库存不存在 ID:{stock_id}")
|
||
|
||
if float(stock.available_quantity) < qty:
|
||
raise ValueError(f"SKU {stock.sku} 可用库存不足")
|
||
|
||
# 1. 冻结库存 (只减可用)
|
||
stock.available_quantity = float(stock.available_quantity) - qty
|
||
|
||
# 2. 创建借用单
|
||
record = TransBorrow(
|
||
borrow_no=borrow_no,
|
||
sku=stock.sku,
|
||
source_table=source_table,
|
||
stock_id=stock.id,
|
||
barcode=stock.barcode,
|
||
quantity=qty,
|
||
borrower_name=borrower_name,
|
||
borrow_signature=signature,
|
||
remark=data.get('remark'),
|
||
expected_return_time=data.get('expected_return_time'),
|
||
status='borrowed',
|
||
is_returned=False
|
||
)
|
||
db.session.add(record)
|
||
|
||
db.session.commit()
|
||
return borrow_no
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
raise e
|
||
|
||
@staticmethod
|
||
def scan_for_return(barcode):
|
||
"""
|
||
扫码还库:查找未归还记录,并返回当前物品的库位
|
||
"""
|
||
records = TransBorrow.query.filter_by(barcode=barcode, is_returned=False).all()
|
||
if not records:
|
||
return None
|
||
|
||
# 取第一条未还记录
|
||
record = records[0]
|
||
|
||
# 获取当前库存表中的实时库位
|
||
current_location = ""
|
||
model_map = {'stock_buy': StockBuy, 'stock_semi': StockSemi, 'stock_product': StockProduct}
|
||
ModelClass = model_map.get(record.source_table)
|
||
|
||
if ModelClass:
|
||
stock = ModelClass.query.get(record.stock_id)
|
||
if stock:
|
||
current_location = stock.warehouse_location
|
||
|
||
res_dict = record.to_dict()
|
||
res_dict['current_location'] = current_location # 用于前端对比和预填
|
||
return res_dict
|
||
|
||
@staticmethod
|
||
def process_return(data, operator_name):
|
||
"""
|
||
还库逻辑:
|
||
1. 恢复可用库存
|
||
2. 更新库位 (如果有变动)
|
||
3. 记录库管签字
|
||
"""
|
||
items = data.get('items', [])
|
||
signature = data.get('signature_path') # 库管签字
|
||
|
||
if not items: raise ValueError("还库列表为空")
|
||
if not signature: raise ValueError("库管必须签字确认")
|
||
|
||
model_map = {'stock_buy': StockBuy, 'stock_semi': StockSemi, 'stock_product': StockProduct}
|
||
|
||
try:
|
||
for item in items:
|
||
borrow_id = item.get('id')
|
||
# 前端如果没有填 return_location,应该在提交前处理好,或者这里做 fallback
|
||
# 这里假设前端传来的 return_location 就是最终要保存的库位
|
||
final_location = item.get('return_location')
|
||
|
||
record = TransBorrow.query.with_for_update().get(borrow_id)
|
||
if not record or record.is_returned:
|
||
continue
|
||
|
||
ModelClass = model_map.get(record.source_table)
|
||
if ModelClass:
|
||
stock = ModelClass.query.with_for_update().get(record.stock_id)
|
||
if stock:
|
||
# 1. 恢复可用库存
|
||
stock.available_quantity = float(stock.available_quantity) + float(record.quantity)
|
||
|
||
# 2. 更新库位 (如果提供了有效值)
|
||
if final_location:
|
||
stock.warehouse_location = final_location
|
||
|
||
# 3. 更新借用单状态
|
||
record.is_returned = True
|
||
record.status = 'returned'
|
||
record.return_time = datetime.now()
|
||
record.return_operator = operator_name
|
||
record.return_signature = signature
|
||
record.return_location = final_location
|
||
|
||
db.session.commit()
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
raise e
|
||
|
||
@staticmethod
|
||
def get_records(page=1, limit=10, status='all', keyword=None):
|
||
q = TransBorrow.query
|
||
if status == 'borrowed':
|
||
q = q.filter(TransBorrow.is_returned == False)
|
||
elif status == 'returned':
|
||
q = q.filter(TransBorrow.is_returned == True)
|
||
|
||
if keyword:
|
||
q = q.filter(TransBorrow.borrower_name.ilike(f'%{keyword}%') |
|
||
TransBorrow.sku.ilike(f'%{keyword}%') |
|
||
TransBorrow.borrow_no.ilike(f'%{keyword}%'))
|
||
|
||
q = q.order_by(desc(TransBorrow.borrow_time))
|
||
pagination = q.paginate(page=page, per_page=limit, error_out=False)
|
||
|
||
return {
|
||
'items': [r.to_dict() for r in pagination.items],
|
||
'total': pagination.total,
|
||
'page': page,
|
||
'limit': limit
|
||
} |