diff --git a/inventory-backend/app/api/v1/inbound/product.py b/inventory-backend/app/api/v1/inbound/product.py index fd54ae1..21a09b5 100644 --- a/inventory-backend/app/api/v1/inbound/product.py +++ b/inventory-backend/app/api/v1/inbound/product.py @@ -1,11 +1,14 @@ +# inventory-backend/app/api/v1/inbound/product.py from flask import Blueprint, request, jsonify -# 引用更名后的服务 from app.services.inbound.product_service import ProductInboundService import traceback -# 蓝图命名改为 inbound_product_bp inbound_product_bp = Blueprint('inbound_product', __name__) + +# ------------------------------------------------------------------ +# 0. 基础物料搜索 +# ------------------------------------------------------------------ @inbound_product_bp.route('/search-base', methods=['GET']) def search_base(): try: @@ -14,6 +17,10 @@ def search_base(): except Exception as e: return jsonify({"code": 500, "msg": str(e)}), 500 + +# ------------------------------------------------------------------ +# 1. 获取列表 +# ------------------------------------------------------------------ @inbound_product_bp.route('/list', methods=['GET']) def get_list(): try: @@ -25,14 +32,30 @@ def get_list(): except Exception as e: return jsonify({"code": 500, "msg": str(e)}), 500 + +# ------------------------------------------------------------------ +# 2. 新增入库 (修改:返回创建的对象数据,用于打印) +# ------------------------------------------------------------------ @inbound_product_bp.route('/submit', methods=['POST']) def submit(): try: - ProductInboundService.handle_inbound(request.get_json()) - return jsonify({"code": 200, "msg": "入库成功"}) + # 调用 Service 处理入库,获取新创建的对象 + new_stock = ProductInboundService.handle_inbound(request.get_json()) + + # 返回成功信息以及新创建的数据(包含生成的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 + +# ------------------------------------------------------------------ +# 3. 更新入库 +# ------------------------------------------------------------------ @inbound_product_bp.route('/', methods=['PUT']) def update(id): try: @@ -41,6 +64,10 @@ def update(id): except Exception as e: return jsonify({"code": 500, "msg": str(e)}), 500 + +# ------------------------------------------------------------------ +# 4. 删除 +# ------------------------------------------------------------------ @inbound_product_bp.route('/', methods=['DELETE']) def delete(id): try: diff --git a/inventory-backend/app/models/inbound/product.py b/inventory-backend/app/models/inbound/product.py index a5d386d..cc380e7 100644 --- a/inventory-backend/app/models/inbound/product.py +++ b/inventory-backend/app/models/inbound/product.py @@ -12,11 +12,12 @@ class StockProduct(db.Model): id = db.Column(db.Integer, primary_key=True) base_id = db.Column(db.Integer, db.ForeignKey('material_base.id'), nullable=False) + # 身份标识 sku = db.Column(db.String(100)) production_date = db.Column(db.Date) barcode = db.Column(db.String(100)) serial_number = db.Column(db.String(100)) - # SQL 无 batch_number + # Note: 成品通常按SN管理,SQL定义无 batch_number # 数量 in_quantity = db.Column(db.Numeric(19, 4), default=0) @@ -48,6 +49,9 @@ class StockProduct(db.Model): inspection_report_link = db.Column(db.Text) order_id = db.Column(db.String(100)) + # [新增] 全局打印流水号 (用于跨表连续编号,对应 Sequence: global_print_seq) + global_print_id = db.Column(db.Integer) + # 关系定义 material = db.relationship('MaterialBase', back_populates='stock_products') @@ -98,5 +102,9 @@ class StockProduct(db.Model): 'sale_price': float(self.sale_price or 0), 'inspection_report_link': self.inspection_report_link, - 'order_id': self.order_id + 'order_id': self.order_id, + + # [新增] 返回全局打印ID及其格式化字符串 + '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/product_service.py b/inventory-backend/app/services/inbound/product_service.py index 9212660..bb0790b 100644 --- a/inventory-backend/app/services/inbound/product_service.py +++ b/inventory-backend/app/services/inbound/product_service.py @@ -1,9 +1,9 @@ +# app/services/inbound/product_service.py from app.extensions import db from app.models.base import MaterialBase -# 引用新的 product 路径 from app.models.inbound.product import StockProduct from datetime import datetime -from sqlalchemy import or_, func +from sqlalchemy import or_, func, text import traceback @@ -11,11 +11,16 @@ class ProductInboundService: @staticmethod def search_base_material(keyword): try: - if not keyword: return [] - query = MaterialBase.query.filter( - MaterialBase.is_enabled == True, - or_(MaterialBase.name.ilike(f'%{keyword}%'), MaterialBase.spec_model.ilike(f'%{keyword}%')) - ).limit(20) + if not keyword: + # 如果没有关键词,返回最新的20条 + query = MaterialBase.query.filter(MaterialBase.is_enabled == True).order_by( + MaterialBase.id.desc()).limit(20) + else: + query = MaterialBase.query.filter( + MaterialBase.is_enabled == True, + or_(MaterialBase.name.ilike(f'%{keyword}%'), MaterialBase.spec_model.ilike(f'%{keyword}%')) + ).limit(20) + results = [] for item in query.all(): results.append({ @@ -23,7 +28,8 @@ class ProductInboundService: 'category': item.category, 'unit': item.unit, 'type': item.material_type }) return results - except: + except Exception: + traceback.print_exc() return [] @staticmethod @@ -37,21 +43,47 @@ class ProductInboundService: in_date_val = datetime.utcnow().date() if data.get('in_date'): try: - in_date_val = datetime.strptime(str(data['in_date'])[:10], '%Y-%m-%d').date() + # 兼容字符串格式日期处理 + 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: pass in_qty = float(data.get('in_quantity') or 0) + # 处理生产时间范围 p_start = data.get('production_start_time', '') p_end = data.get('production_end_time', '') time_range = f"{p_start} ~ {p_end}" if p_start or p_end else None + # ------------------------------------------------------------------ + # 1. 获取全局打印流水号 (跨表唯一,用于打印逻辑) + # ------------------------------------------------------------------ + seq_sql = text("SELECT nextval('global_print_seq')") + result = db.session.execute(seq_sql) + next_global_id = result.scalar() + + # ------------------------------------------------------------------ + # 2. 自动生成 SKU (格式: 10位数字,补零) + # ------------------------------------------------------------------ + generated_sku = str(next_global_id).zfill(10) + + # ------------------------------------------------------------------ + # 3. 条码逻辑处理 + # 如果前端没传条码,则默认使用 SKU 作为条码 + # ------------------------------------------------------------------ + final_barcode = data.get('barcode') + if not final_barcode: + final_barcode = generated_sku + new_stock = StockProduct( base_id=material.id, - sku=data.get('sku'), + global_print_id=next_global_id, # 新增全局打印ID + sku=generated_sku, # 使用自动生成的SKU production_date=in_date_val, - barcode=data.get('barcode'), + barcode=final_barcode, serial_number=data.get('serial_number'), status='在库', @@ -81,6 +113,8 @@ class ProductInboundService: db.session.add(new_stock) db.session.commit() + + # 返回对象实例以便上层调用 to_dict() return new_stock except Exception as e: db.session.rollback() @@ -92,8 +126,9 @@ class ProductInboundService: stock = StockProduct.query.get(stock_id) if not stock: raise ValueError("记录不存在") + # 允许更新的字段列表 fields = [ - 'sku', 'barcode', 'serial_number', 'warehouse_location', + 'barcode', 'serial_number', 'warehouse_location', 'status', 'quality_status', 'bom_code', 'bom_version', 'work_order_code', 'production_manager', 'quality_report_link', 'detail_link', 'inspection_report_link', 'order_id' @@ -101,22 +136,31 @@ class ProductInboundService: for f in fields: if f in data: setattr(stock, f, data[f]) + # 数值类型处理 if 'sale_price' in data: stock.sale_price = float(data['sale_price']) if 'raw_material_cost' in data: stock.raw_material_cost = float(data['raw_material_cost']) if 'manual_cost' in data: stock.manual_cost = float(data['manual_cost']) + # 数量更新逻辑 (同步更新库存和可用量) if 'in_quantity' in data: new_qty = float(data['in_quantity']) - diff = new_qty - float(stock.in_quantity) - stock.in_quantity = new_qty - stock.stock_quantity = float(stock.stock_quantity) + diff - stock.available_quantity = float(stock.available_quantity) + diff + old_qty = float(stock.in_quantity) + if new_qty != old_qty: + diff = new_qty - old_qty + stock.in_quantity = new_qty + stock.stock_quantity = float(stock.stock_quantity) + diff + stock.available_quantity = float(stock.available_quantity) + diff + # 时间范围处理 if 'production_start_time' in data or 'production_end_time' in data: old_range = stock.production_time_range or " ~ " parts = old_range.split(' ~ ') - start = data.get('production_start_time', parts[0] if len(parts) > 0 else '') - end = data.get('production_end_time', parts[1] if len(parts) > 1 else '') + # 获取原值防止越界 + old_start = parts[0] if len(parts) > 0 else '' + old_end = parts[1] if len(parts) > 1 else '' + + start = data.get('production_start_time', old_start) + end = data.get('production_end_time', old_end) stock.production_time_range = f"{start} ~ {end}" db.session.commit() @@ -132,6 +176,7 @@ class ProductInboundService: if stock: db.session.delete(stock) db.session.commit() + return True except Exception as e: db.session.rollback() raise e @@ -139,17 +184,24 @@ class ProductInboundService: @staticmethod def get_list(page, limit, keyword=None): try: + # 联表查询 query = db.session.query(StockProduct).outerjoin(MaterialBase, StockProduct.base_id == MaterialBase.id) + if keyword: query = query.filter(or_( MaterialBase.name.ilike(f'%{keyword}%'), + MaterialBase.spec_model.ilike(f'%{keyword}%'), StockProduct.serial_number.ilike(f'%{keyword}%'), StockProduct.work_order_code.ilike(f'%{keyword}%'), - StockProduct.order_id.ilike(f'%{keyword}%') + StockProduct.order_id.ilike(f'%{keyword}%'), + StockProduct.sku.ilike(f'%{keyword}%') )) + pagination = query.order_by(StockProduct.id.desc()).paginate(page=page, per_page=limit, error_out=False) - base_ids = list(set([i.base_id for i in pagination.items])) + # 计算聚合库存 + current_items = pagination.items + base_ids = list(set([i.base_id for i in current_items])) stock_map = {} if base_ids: aggs = db.session.query( @@ -157,10 +209,11 @@ class ProductInboundService: func.sum(StockProduct.stock_quantity).label('s'), func.sum(StockProduct.available_quantity).label('a') ).filter(StockProduct.base_id.in_(base_ids)).group_by(StockProduct.base_id).all() - for a in aggs: stock_map[a.base_id] = {'s': float(a.s or 0), 'a': float(a.a or 0)} + for a in aggs: + stock_map[a.base_id] = {'s': float(a.s or 0), 'a': float(a.a or 0)} items = [] - for item in pagination.items: + for item in current_items: d = item.to_dict() stats = stock_map.get(item.base_id, {'s': 0, 'a': 0}) d['sum_stock'] = stats['s'] diff --git a/inventory-web/src/views/stock/inbound/product.vue b/inventory-web/src/views/stock/inbound/product.vue index 93a80b8..fe3016c 100644 --- a/inventory-web/src/views/stock/inbound/product.vue +++ b/inventory-web/src/views/stock/inbound/product.vue @@ -54,8 +54,11 @@ - + @@ -117,8 +120,8 @@
2. 入库详情
- - + + @@ -197,20 +200,51 @@ + + +
+
+ Label Preview +
正在生成预览...
+
+ +
+

打印机 IP: 192.168.9.205

+

尺寸: 40mm x 30mm

+
+
+ +
+