From 98450d73f1317d005b1d0d5716eebb75ded2ed8b Mon Sep 17 00:00:00 2001 From: dxc Date: Tue, 3 Feb 2026 09:01:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E4=BA=8E=E5=8D=8A=E6=88=90=E5=93=81?= =?UTF-8?q?=E7=9A=84=E6=9D=A1=E5=BD=A2=E7=A0=81=E8=BF=9B=E8=A1=8C=E6=9B=B4?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- inventory-backend/app/api/v1/inbound/semi.py | 16 +- inventory-backend/app/models/inbound/semi.py | 33 ++-- .../app/services/inbound/semi_service.py | 166 +++++++++--------- .../src/views/stock/inbound/semi.vue | 132 +++++++++++++- 4 files changed, 238 insertions(+), 109 deletions(-) diff --git a/inventory-backend/app/api/v1/inbound/semi.py b/inventory-backend/app/api/v1/inbound/semi.py index 1950c99..d76f82a 100644 --- a/inventory-backend/app/api/v1/inbound/semi.py +++ b/inventory-backend/app/api/v1/inbound/semi.py @@ -1,8 +1,9 @@ +# inventory-backend/app/api/v1/inbound/semi.py from flask import Blueprint, request, jsonify from app.services.inbound.semi_service import SemiInboundService import traceback -# 定义蓝图,url_prefix 通常在注册蓝图时指定,例如 /api/v1/inbound/semi +# 定义蓝图 inbound_semi_bp = Blueprint('inbound_semi', __name__) @@ -48,7 +49,7 @@ def get_list(): # ------------------------------------------------------------------ -# 2. 新增半成品入库 +# 2. 新增半成品入库 (修改:返回创建的对象数据) # ------------------------------------------------------------------ @inbound_semi_bp.route('/submit', methods=['POST']) def submit(): @@ -57,8 +58,15 @@ def submit(): if not data: return jsonify({"code": 400, "msg": "No data"}), 400 - SemiInboundService.handle_inbound(data) - return jsonify({"code": 200, "msg": "入库成功"}) + # 修改:调用 Service 处理入库,获取新创建的对象 + new_stock = SemiInboundService.handle_inbound(data) + + # 修改:返回成功信息以及新创建的数据(包含生成的ID和SKU),供前端打印使用 + return jsonify({ + "code": 200, + "msg": "入库成功", + "data": new_stock.to_dict() + }) except Exception as e: traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 diff --git a/inventory-backend/app/models/inbound/semi.py b/inventory-backend/app/models/inbound/semi.py index 7dd138f..a2b9520 100644 --- a/inventory-backend/app/models/inbound/semi.py +++ b/inventory-backend/app/models/inbound/semi.py @@ -5,7 +5,6 @@ from app.extensions import db class StockSemi(db.Model): """ 半成品入库库存表 - 对应数据库表: stock_semi """ __tablename__ = 'stock_semi' @@ -16,7 +15,7 @@ class StockSemi(db.Model): production_date = db.Column(db.Date) barcode = db.Column(db.String(100)) serial_number = db.Column(db.String(100)) - # SQL 无 batch_number,此处移除 + batch_number = db.Column(db.String(100)) # 数量 in_quantity = db.Column(db.Numeric(19, 4), default=0) @@ -27,20 +26,27 @@ class StockSemi(db.Model): status = db.Column(db.String(50)) warehouse_location = db.Column(db.String(100)) - # 半成品特有字段 (SQL 字段映射) - bom_code = db.Column('bom_id', db.String(100)) # 映射 SQL: bom_id + # 半成品特有字段 + bom_code = db.Column('bom_id', db.String(100)) bom_version = db.Column(db.String(50)) - work_order_code = db.Column('work_order_id', db.String(100)) # 映射 SQL: work_order_id + work_order_code = db.Column('work_order_id', db.String(100)) raw_material_cost = db.Column(db.Numeric(19, 4), default=0) manual_cost = db.Column(db.Numeric(19, 4), default=0) + total_price = db.Column(db.Numeric(19, 4), default=0) - production_manager = db.Column('producer_name', db.String(100)) # 映射 SQL: producer_name + production_manager = db.Column('producer_name', db.String(100)) + production_start_time = db.Column(db.DateTime) + production_end_time = db.Column(db.DateTime) production_time_range = db.Column(db.String(255)) quality_status = db.Column(db.String(50)) quality_report_link = db.Column(db.Text) detail_link = db.Column(db.Text) + remark = db.Column(db.Text) + + # [新增] 全局打印流水号 (请务必确保数据库已添加此列) + global_print_id = db.Column(db.Integer) # 关系定义 material = db.relationship('MaterialBase', back_populates='stock_semis') @@ -63,6 +69,7 @@ class StockSemi(db.Model): 'inbound_date': self.production_date.strftime('%Y-%m-%d') if self.production_date else '', 'barcode': self.barcode, 'serial_number': self.serial_number, + 'batch_number': self.batch_number, 'warehouse_loc': self.warehouse_location, 'status': self.status, @@ -81,13 +88,15 @@ class StockSemi(db.Model): 'unit_total_cost': unit_total, 'production_manager': self.production_manager, 'production_time_range': self.production_time_range, - # 简单的时间拆分逻辑,防止 split 报错 - 'production_start_time': self.production_time_range.split(' ~ ')[ - 0] if self.production_time_range and ' ~ ' in self.production_time_range else '', - 'production_end_time': self.production_time_range.split(' ~ ')[ - 1] if self.production_time_range and ' ~ ' in self.production_time_range else '', + + 'production_start_time': str(self.production_start_time) if self.production_start_time else '', + 'production_end_time': str(self.production_end_time) if self.production_end_time else '', 'quality_status': self.quality_status, 'quality_report_link': self.quality_report_link, - 'detail_link': self.detail_link + 'detail_link': self.detail_link, + 'remark': self.remark, + + 'global_print_id': self.global_print_id, + 'global_print_id_str': f"{self.global_print_id:010d}" if self.global_print_id else "" } \ No newline at end of file diff --git a/inventory-backend/app/services/inbound/semi_service.py b/inventory-backend/app/services/inbound/semi_service.py index 3b6421b..72427c2 100644 --- a/inventory-backend/app/services/inbound/semi_service.py +++ b/inventory-backend/app/services/inbound/semi_service.py @@ -1,18 +1,17 @@ +# app/services/inbound/semi_service.py from app.extensions import db from app.models.base import MaterialBase -from app.models.inbound.semi import StockSemi +# --------------------------------------------------------------------- +# ❌ 头部禁止导入 StockSemi,防止 Circular Import +# --------------------------------------------------------------------- from datetime import datetime -from sqlalchemy import or_, func +from sqlalchemy import or_, func, text import traceback class SemiInboundService: @staticmethod def search_base_material(keyword): - """ - 搜索基础物料,逻辑与采购入库基本一致。 - 如果需要只搜索'半成品'类型的物料,可以在 filter 中增加条件。 - """ try: if not keyword: return [] @@ -43,6 +42,9 @@ class SemiInboundService: @staticmethod def handle_inbound(data): + # ✅ 【关键修复】局部导入 Model,解决循环引用 + from app.models.inbound.semi import StockSemi + try: base_id = data.get('base_id') if not base_id: @@ -56,12 +58,14 @@ class SemiInboundService: in_date_val = datetime.utcnow().date() if data.get('in_date'): try: - date_str = str(data['in_date'])[:10] + date_str = str(data['in_date']) + if len(date_str) > 10: + date_str = date_str[:10] in_date_val = datetime.strptime(date_str, '%Y-%m-%d').date() except ValueError: pass - # 2. 处理生产时间 (前端传来的是字符串 "YYYY-MM-DD HH:mm:ss") + # 2. 处理生产时间 p_start = None p_end = None if data.get('production_start_time'): @@ -75,50 +79,79 @@ class SemiInboundService: except: pass + # ✅ 优化:处理 time_range 字符串,防止前端传数组导致存入 None + time_range_str = None + raw_range = data.get('production_time_range') + if isinstance(raw_range, list): + time_range_str = " ~ ".join([str(x) for x in raw_range]) + elif isinstance(raw_range, str): + time_range_str = raw_range + # 3. 处理数值和成本 in_qty = float(data.get('in_quantity') or 0) raw_cost = float(data.get('raw_material_cost') or 0) manual_cost = float(data.get('manual_cost') or 0) - - # 单件总成本 = 原料 + 人工 unit_total_cost = raw_cost + manual_cost - # 总价值 = 单件总成本 * 数量 total_value = unit_total_cost * in_qty - # 4. 创建记录 + # ------------------------------------------------------------------ + # 4. 获取全局打印流水号 (跨表唯一) + # ------------------------------------------------------------------ + next_global_id = 0 + try: + seq_sql = text("SELECT nextval('global_print_seq')") + result = db.session.execute(seq_sql) + next_global_id = result.scalar() + except Exception as e: + print("❌ 数据库序列 global_print_seq 不存在,请执行SQL创建!") + raise e + + # ------------------------------------------------------------------ + # 5. 自动生成 SKU (格式: 00000001) + # ------------------------------------------------------------------ + generated_sku = str(next_global_id).zfill(10) + final_sku = data.get('sku') + if not final_sku: + final_sku = generated_sku + + # ------------------------------------------------------------------ + # 6. 条码逻辑处理 + # ------------------------------------------------------------------ + final_barcode = data.get('barcode') + if not final_barcode: + final_barcode = final_sku + + # 7. 创建记录 new_stock = StockSemi( base_id=material.id, - sku=data.get('sku'), - in_date=in_date_val, + global_print_id=next_global_id, + sku=final_sku, + production_date=in_date_val, - # 标识信息 serial_number=data.get('serial_number'), batch_number=data.get('batch_number'), - barcode=data.get('barcode'), + barcode=final_barcode, - # 状态与数量 status='在库', - quality_status=data.get('quality_status', '合格'), # 半成品使用质量状态 + quality_status=data.get('quality_status', '合格'), in_quantity=in_qty, stock_quantity=in_qty, available_quantity=in_qty, warehouse_location=data.get('warehouse_location'), - # 生产任务信息 (半成品特有) bom_code=data.get('bom_code'), bom_version=data.get('bom_version'), work_order_code=data.get('work_order_code'), production_manager=data.get('production_manager'), + production_start_time=p_start, production_end_time=p_end, + production_time_range=time_range_str, - # 成本信息 (半成品特有) raw_material_cost=raw_cost, manual_cost=manual_cost, - unit_total_cost=unit_total_cost, - total_price=total_value, # 数据库字段可能复用 total_price 或叫 total_value + total_price=total_value, - # 链接 quality_report_link=data.get('quality_report_link'), detail_link=data.get('detail_link'), remark=data.get('remark') @@ -130,10 +163,14 @@ class SemiInboundService: except Exception as e: db.session.rollback() + print("----- SemiInboundService Error -----") + traceback.print_exc() raise e @staticmethod def update_inbound(stock_id, data): + from app.models.inbound.semi import StockSemi + try: print(f"----- UPDATE SEMI DEBUG: ID={stock_id} -----") @@ -141,7 +178,6 @@ class SemiInboundService: if not stock: raise ValueError("记录不存在") - # 1. 简单字段映射 field_mapping = { 'sku': 'sku', 'barcode': 'barcode', @@ -149,22 +185,21 @@ class SemiInboundService: 'serial_number': 'serial_number', 'batch_number': 'batch_number', 'status': 'status', - 'quality_status': 'quality_status', # 质量状态 - - # 生产信息 + 'quality_status': 'quality_status', 'bom_code': 'bom_code', 'bom_version': 'bom_version', 'work_order_code': 'work_order_code', 'production_manager': 'production_manager', 'quality_report_link': 'quality_report_link', - 'detail_link': 'detail_link' + 'detail_link': 'detail_link', + 'remark': 'remark' } for frontend_key, db_attr in field_mapping.items(): if frontend_key in data: setattr(stock, db_attr, data[frontend_key]) - # 2. 处理时间更新 + # 时间处理 if 'production_start_time' in data: try: if data['production_start_time']: @@ -185,11 +220,17 @@ class SemiInboundService: except: pass - # 3. 处理数量和成本变更联动 + # 更新 production_time_range 字符串 + if 'production_time_range' in data: + raw_range = data['production_time_range'] + if isinstance(raw_range, list): + stock.production_time_range = " ~ ".join([str(x) for x in raw_range]) + else: + stock.production_time_range = raw_range + qty_changed = False cost_changed = False - # 更新数量 if 'in_quantity' in data: new_qty = float(data['in_quantity']) old_qty = float(stock.in_quantity) @@ -200,7 +241,6 @@ class SemiInboundService: stock.available_quantity = float(stock.available_quantity) + diff qty_changed = True - # 更新成本 (原材料 or 人工) if 'raw_material_cost' in data: stock.raw_material_cost = float(data['raw_material_cost']) cost_changed = True @@ -209,15 +249,11 @@ class SemiInboundService: stock.manual_cost = float(data['manual_cost']) cost_changed = True - # 如果成本或数量变了,重新计算单价和总价 - if cost_changed: - stock.unit_total_cost = float(stock.raw_material_cost) + float(stock.manual_cost) - if cost_changed or qty_changed: - stock.total_price = float(stock.in_quantity) * float(stock.unit_total_cost) + unit_total = float(stock.raw_material_cost) + float(stock.manual_cost) + stock.total_price = float(stock.in_quantity) * unit_total db.session.commit() - print("----- UPDATE SEMI SUCCESS -----") return stock except Exception as e: @@ -228,6 +264,7 @@ class SemiInboundService: @staticmethod def delete_inbound(stock_id): + from app.models.inbound.semi import StockSemi try: stock = StockSemi.query.get(stock_id) if not stock: @@ -241,8 +278,8 @@ class SemiInboundService: @staticmethod def get_list(page, limit, keyword=None): + from app.models.inbound.semi import StockSemi try: - # 1. 查询分页数据 query = db.session.query(StockSemi).outerjoin(MaterialBase, StockSemi.base_id == MaterialBase.id) if keyword: @@ -253,7 +290,6 @@ class SemiInboundService: StockSemi.batch_number.ilike(f'%{keyword}%'), StockSemi.serial_number.ilike(f'%{keyword}%'), StockSemi.sku.ilike(f'%{keyword}%'), - # 增加半成品特有的搜索字段 StockSemi.work_order_code.ilike(f'%{keyword}%'), StockSemi.bom_code.ilike(f'%{keyword}%') ) @@ -261,7 +297,6 @@ class SemiInboundService: pagination = query.order_by(StockSemi.id.desc()).paginate(page=page, per_page=limit, error_out=False) - # 2. 聚合统计 (计算该物料的总库存) current_items = pagination.items base_ids = list(set([item.base_id for item in current_items if item.base_id])) @@ -281,55 +316,12 @@ class SemiInboundService: items = [] for item in current_items: - mat_name = item.material.name if item.material else '未知物料' - mat_spec = item.material.spec_model if item.material else '' - mat_cat = item.material.category if item.material else '' - mat_unit = item.material.unit if item.material else '' - mat_type = item.material.material_type if item.material else '' - stats = stock_map.get(item.base_id, {'total_stock': 0, 'total_avail': 0}) - d = { - 'id': item.id, - 'base_id': item.base_id, - 'material_name': mat_name, - 'spec_model': mat_spec, - 'category': mat_cat, - 'unit': mat_unit, - 'material_type': mat_type, + d = item.to_dict() + d['sum_stock'] = stats['total_stock'] + d['sum_available'] = stats['total_avail'] - 'sku': item.sku, - 'inbound_date': str(item.in_date) if item.in_date else '', - 'barcode': item.barcode, - 'serial_number': item.serial_number, - 'batch_number': item.batch_number, - 'status': item.status, - 'quality_status': item.quality_status, # 半成品使用质量状态 - - 'qty_inbound': float(item.in_quantity or 0), - 'qty_stock': float(item.stock_quantity or 0), - 'qty_available': float(item.available_quantity or 0), - - 'sum_stock': stats['total_stock'], - 'sum_available': stats['total_avail'], - - 'warehouse_loc': item.warehouse_location, - - # 半成品特有字段返回 - 'bom_code': item.bom_code, - 'bom_version': item.bom_version, - 'work_order_code': item.work_order_code, - 'raw_material_cost': float(item.raw_material_cost or 0), - 'manual_cost': float(item.manual_cost or 0), - 'unit_total_cost': float(item.unit_total_cost or 0), - 'production_manager': item.production_manager, - 'production_start_time': str(item.production_start_time) if item.production_start_time else '', - 'production_end_time': str(item.production_end_time) if item.production_end_time else '', - - 'quality_report_link': item.quality_report_link, - 'detail_link': item.detail_link, - 'remark': item.remark - } items.append(d) return {"total": pagination.total, "items": items} diff --git a/inventory-web/src/views/stock/inbound/semi.vue b/inventory-web/src/views/stock/inbound/semi.vue index 29d8c7b..f9edf4d 100644 --- a/inventory-web/src/views/stock/inbound/semi.vue +++ b/inventory-web/src/views/stock/inbound/semi.vue @@ -97,8 +97,11 @@ - +