后端变更: - PurchaseRequest 新增 base_id 硬关联 MaterialBase,StockBuy 新增 request_id 关联采购单 - handle_inbound 支持 request_id 入参,入库后自动反写采购单状态为已完成 - 新增 get_approved_requests 接口,返回已审批未入库采购单列表(含物料信息+历史数据回退匹配) - create_purchase_request 新增 name+spec_model 自动匹配 MaterialBase - 新增 SQL 和 Alembic 数据库迁移脚本 前端变更: - 入库表单新增"从采购单导入"弹窗,支持搜索/选中已审批采购单并一键填充物料和商务信息 - 单价/总价交叉推算,缺项自动补全 - 选中行蓝色高亮+点击整行选中,行级交互优化
135 lines
5.5 KiB
Python
135 lines
5.5 KiB
Python
# inventory-backend/app/models/inbound/buy.py
|
|
from app.extensions import db
|
|
from pgvector.sqlalchemy import Vector
|
|
import json
|
|
# 显式导入 MaterialBase 以防 relationship 找不到引用
|
|
from app.models.base import MaterialBase
|
|
from app.models.purchase import PurchaseRequest
|
|
|
|
|
|
class StockBuy(db.Model):
|
|
"""
|
|
采购入库库存表
|
|
对应数据库表: stock_buy
|
|
"""
|
|
__tablename__ = 'stock_buy'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
base_id = db.Column(db.Integer, db.ForeignKey('material_base.id'), nullable=False, index=True) # ★ 批量 IN 查询高频列
|
|
# [新增] 关联采购申请单,打通"按单入库"链路
|
|
request_id = db.Column(db.Integer, db.ForeignKey('purchase_request.id'), index=True, comment='关联采购申请单ID')
|
|
|
|
# 身份标识
|
|
sku = db.Column(db.String(100), index=True) # ★ 条码/SKU 快速定位
|
|
in_date = db.Column(db.DateTime)
|
|
barcode = db.Column(db.String(100), index=True) # ★ 条码扫码查询高频列
|
|
serial_number = db.Column(db.String(100))
|
|
batch_number = db.Column(db.String(100))
|
|
|
|
# 状态
|
|
status = db.Column(db.String(50), index=True) # ★ 在库/锁定 过滤条件
|
|
inspection_status = db.Column(db.String(50))
|
|
warehouse_location = db.Column(db.String(100), index=True) # ★ 按库位分组/过滤
|
|
|
|
# 数量
|
|
in_quantity = db.Column(db.Numeric(19, 4), default=0)
|
|
stock_quantity = db.Column(db.Numeric(19, 4), default=0)
|
|
available_quantity = db.Column(db.Numeric(19, 4), default=0)
|
|
|
|
# 财务与商务
|
|
pre_tax_unit_price = db.Column(db.Numeric(19, 4), default=0) # 现意为:不含税单价
|
|
post_tax_unit_price = db.Column(db.Numeric(19, 4), default=0) # 税后单价
|
|
total_price = db.Column(db.Numeric(19, 4), default=0) # 总价
|
|
# [新增] 税率
|
|
tax_rate = db.Column(db.Numeric(5, 2), default=0)
|
|
|
|
currency = db.Column(db.String(20), default='CNY')
|
|
exchange_rate = db.Column(db.Numeric(15, 6), default=1.0)
|
|
|
|
supplier_name = db.Column(db.String(255))
|
|
buyer_name = db.Column(db.String(100))
|
|
buyer_email = db.Column(db.String(100))
|
|
original_link = db.Column(db.Text)
|
|
detail_link = db.Column(db.Text)
|
|
|
|
# 图片字段 (存储 JSON 字符串)
|
|
arrival_photo = db.Column(db.Text)
|
|
inspection_report = db.Column(db.Text)
|
|
|
|
# 全局打印流水号
|
|
global_print_id = db.Column(db.Integer)
|
|
|
|
# CLIP 视觉向量(用于以图搜图)
|
|
arrival_image_embedding = db.Column(Vector(512), nullable=True)
|
|
|
|
# 关系定义
|
|
base = db.relationship('MaterialBase', back_populates='stock_buys')
|
|
purchase_request = db.relationship('PurchaseRequest', back_populates='stock_buys')
|
|
|
|
def to_dict(self):
|
|
# 辅助解析函数
|
|
def parse_img_list(json_str):
|
|
if not json_str:
|
|
return []
|
|
try:
|
|
if not json_str.startswith('['):
|
|
return [json_str]
|
|
return json.loads(json_str)
|
|
except:
|
|
return []
|
|
|
|
return {
|
|
'id': self.id,
|
|
'base_id': self.base_id,
|
|
'request_id': self.request_id,
|
|
# [新增] 采购申请单号(便于前端展示)
|
|
'request_no': self.purchase_request.request_no if self.purchase_request else '',
|
|
|
|
# [修改] 增加公司名称
|
|
'company_name': self.base.company_name if self.base else '',
|
|
'material_name': self.base.name if self.base else '',
|
|
'spec_model': self.base.spec_model if self.base else '',
|
|
'category': self.base.category if self.base else '',
|
|
'unit': self.base.unit if self.base else '',
|
|
'material_type': self.base.material_type if self.base else '',
|
|
# 强制质检标记
|
|
'isInspectionRequired': bool(self.base.is_inspection_required) if self.base else False,
|
|
|
|
'sku': self.sku,
|
|
'inbound_date': self.in_date.strftime('%Y-%m-%d') if self.in_date else '',
|
|
'barcode': self.barcode,
|
|
'serial_number': self.serial_number,
|
|
'batch_number': self.batch_number,
|
|
'warehouse_loc': self.warehouse_location,
|
|
'status': self.status,
|
|
'inspection_status': self.inspection_status,
|
|
|
|
'in_quantity': float(self.in_quantity or 0),
|
|
'qty_inbound': float(self.in_quantity or 0),
|
|
'stock_quantity': float(self.stock_quantity or 0),
|
|
'qty_stock': float(self.stock_quantity or 0),
|
|
'available_quantity': float(self.available_quantity or 0),
|
|
'qty_available': float(self.available_quantity or 0),
|
|
|
|
'unit_price': float(self.pre_tax_unit_price or 0),
|
|
'post_tax_unit_price': float(self.post_tax_unit_price or 0),
|
|
'total_price': float(self.total_price or 0),
|
|
# [新增] 税率
|
|
'tax_rate': float(self.tax_rate or 0),
|
|
|
|
'currency': self.currency,
|
|
'exchange_rate': float(self.exchange_rate or 1.0),
|
|
|
|
'supplier_name': self.supplier_name,
|
|
'purchaser': self.buyer_name,
|
|
'purchaser_email': self.buyer_email,
|
|
'source_link': self.original_link,
|
|
'detail_link': self.detail_link,
|
|
|
|
'arrival_photo': parse_img_list(self.arrival_photo),
|
|
'inspection_report': parse_img_list(self.inspection_report),
|
|
|
|
'global_print_id': self.global_print_id,
|
|
'global_print_id_str': f"{self.global_print_id:010d}" if self.global_print_id else ""
|
|
}
|