diff --git a/sf_purchase_arrival_inform/__init__.py b/sf_purchase_arrival_inform/__init__.py new file mode 100644 index 00000000..9b429614 --- /dev/null +++ b/sf_purchase_arrival_inform/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/sf_purchase_arrival_inform/__manifest__.py b/sf_purchase_arrival_inform/__manifest__.py new file mode 100644 index 00000000..0c471355 --- /dev/null +++ b/sf_purchase_arrival_inform/__manifest__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +{ + 'name': '机企猫智能工厂 采购到货通知', + 'version': '1.1', + 'summary': '智能工厂计划管理', + 'sequence': 1, + 'description': """ +在本模块,支持采购到货通知 + """, + 'category': 'sf', + 'website': 'https://www.sf.jikimo.com', + 'depends': ['jikimo_purchase_request', 'quality_control', 'sf_manufacturing'], + 'data': [ + 'views/product_category.xml', + 'views/purchase_view.xml', + 'wizard/purchase_confirm_wizard_view.xml', + 'security/ir.model.access.csv', + ], + 'demo': [ + ], + 'assets': { + 'web.assets_qweb': [ + ], + 'web.assets_backend': [ + + ] + }, + 'license': 'LGPL-3', + 'installable': True, + 'application': False, + 'auto_install': False, +} diff --git a/sf_purchase_arrival_inform/models/__init__.py b/sf_purchase_arrival_inform/models/__init__.py new file mode 100644 index 00000000..c81fb314 --- /dev/null +++ b/sf_purchase_arrival_inform/models/__init__.py @@ -0,0 +1,5 @@ +from . import product_category +from . import purchase_order +from . import storage_list +from . import quality_check + diff --git a/sf_purchase_arrival_inform/models/product_category.py b/sf_purchase_arrival_inform/models/product_category.py new file mode 100644 index 00000000..61dd95a6 --- /dev/null +++ b/sf_purchase_arrival_inform/models/product_category.py @@ -0,0 +1,7 @@ +from odoo import models, fields + + +class SfProductCategory(models.Model): + _inherit = 'product.category' + arrival_inform = fields.Boolean('到货通知', default=False) + \ No newline at end of file diff --git a/sf_purchase_arrival_inform/models/purchase_order.py b/sf_purchase_arrival_inform/models/purchase_order.py new file mode 100644 index 00000000..a6624deb --- /dev/null +++ b/sf_purchase_arrival_inform/models/purchase_order.py @@ -0,0 +1,108 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + + +class SfPurchaseOrder(models.Model): + _inherit = 'purchase.order' + + # 收料入库明细 + storage_list_ids = fields.One2many('storage.list', 'purchase_order_id', string='收料入库明细') + + # 计算字段:总收料数量 + total_received_quantity = fields.Float(string='到货数量',compute='_compute_total_received_quantity',store=True) + + purchase_order_line_id = fields.Many2one('purchase.order.line', string='采购订单行') + + @api.depends('order_line.arrival_quantity','order_line.product_qty') + def _compute_total_received_quantity(self): + for order in self: + total_arrival_quantity = sum(line.arrival_quantity for line in order.order_line) + total_product_qty=sum(line.product_qty for line in order.order_line) + order.total_received_quantity = f'{total_arrival_quantity}/{total_product_qty}' + + def open_arrival_inform(self): + self.ensure_one() + # 方式一:把数据写入普通模型(如果要持久化关联关系等场景) + # 先清空旧数据(如果需要),避免重复 + self.env["storage.list"].search([ + ("purchase_order_id", "=", self.id) + ]).unlink() + pickings = self.env['stock.picking'].search([ + ('origin', '=', self.name), + # ('picking_type_code', '=', 'incoming'), + ('state', '!=', 'cancel') + ]) + relate_vals_list = [] + for picking in pickings: + for move_line in picking.move_ids_without_package: + relate_vals = { + "purchase_order_id": self.id, + "product_id": move_line.product_id.id, + "picking_id": picking.id, + "ordered_quantity": move_line.product_uom_qty, + # "current_arrival_quantity": move_line.product_uom_qty, # 默认到货数量等于需求数量 + "product_code": move_line.product_id.default_code, + "product_remark": move_line.name, + "part_number": move_line.part_number, + "state": picking.state, + } + relate_vals_list.append(relate_vals) + if relate_vals_list: + self.env["storage.list"].create(relate_vals_list) + action = { + 'name': _("选择到货通知"), + 'type':'ir.actions.act_window', + 'view_mode':'form', + 'views': [(self.env.ref('sf_purchase_arrival_inform.storage_list_wrapper_form').id, 'form')], + 'res_model':'purchase.order', + 'target':'new', + 'res_id': self.id, + 'context': { + 'dialog_size': 'large', + }, + } + return action + + def confirm_arrival_inform(self): + """确认到货通知""" + # 直接调用storage_list的批量确认方法 + if self.storage_list_ids: + self.storage_list_ids.button_confirm() + + # 关闭弹窗 + return {'type': 'ir.actions.act_window_close'} + + #询价单确认时加二次确认逻辑 + def button_confirm(self): + """采购订单确认,弹出确认向导""" + # 无订单行时直接确认 + if not self.order_line: + return super().button_confirm() + + # 直接创建向导记录并打开 + wizard = self.env['purchase.confirm.wizard'].create({ + 'purchase_order_id': self.id + }) + + return { + 'name': _('询价单订单确认'), + 'type': 'ir.actions.act_window', + 'res_model': 'purchase.confirm.wizard', + 'res_id': wizard.id, + 'view_mode': 'form', + 'target': 'new', + } + + def _execute_original_confirm(self): + """执行原始的确认逻辑""" + res = super().button_confirm() + return res + +class SfPurchaseOrderLine(models.Model): + _inherit = 'purchase.order.line' + + # 到货数量(实际存储字段) + arrival_quantity = fields.Float(string='到货数量', default=0.0, help='累计到货数量') + + + \ No newline at end of file diff --git a/sf_purchase_arrival_inform/models/quality_check.py b/sf_purchase_arrival_inform/models/quality_check.py new file mode 100644 index 00000000..964b2167 --- /dev/null +++ b/sf_purchase_arrival_inform/models/quality_check.py @@ -0,0 +1,61 @@ +from odoo import models, fields, api +import logging + +_logger = logging.getLogger(__name__) + +class QualityPoint(models.Model): + _inherit = 'quality.point' + + quality_status = fields.Selection([ + ('waiting', '等待'), + ('none', '待处理') + ], string='默认质检状态', + help='收料入库时质量检查单的默认状态。如果设置了此值,系统会在创建质检单时自动使用该状态。') + +class QualityCheck(models.Model): + _inherit = 'quality.check' + + @api.model_create_multi + def create(self, vals_list): + """ + 重写create方法,根据quality.point配置动态设置quality_state + 当picking_type为收料入库时,从质量控制点配置中获取默认状态 + """ + # 先调用父类方法创建记录 + records = super().create(vals_list) + + # 对创建的记录进行状态调整 + for record in records: + try: + # 只处理收料入库的质检单且未手动设置状态的记录 + if (record.picking_id and + record.picking_id.picking_type_id.code == 'incoming' and + record.quality_state in ['none', 'waiting']): # 只调整默认状态 + + # 查找匹配的质量控制点配置 + quality_point = record.point_id + if not quality_point and record.product_id: + # 如果没有直接关联的质量点,则根据产品和作业类型查找 + domain = self.env['quality.point']._get_domain( + record.product_id, + record.picking_id.picking_type_id, + measure_on='product' + ) + quality_point = self.env['quality.point'].search(domain, limit=1) + + # 如果找到配置且有默认状态值,则更新记录状态 + if quality_point and quality_point.quality_status: + original_state = record.quality_state + record.quality_state = quality_point.quality_status + + _logger.info( + f"质量检查单状态已更新: {record.name}, " + f"产品: {record.product_id.name}, " + f"收料单: {record.picking_id.name}, " + f"状态: {original_state} -> {quality_point.quality_status}" + ) + + except Exception as e: + _logger.warning(f"更新质量检查单状态时发生错误: {record.name}, 错误: {str(e)}") + + return records \ No newline at end of file diff --git a/sf_purchase_arrival_inform/models/storage_list.py b/sf_purchase_arrival_inform/models/storage_list.py new file mode 100644 index 00000000..fc7e68f5 --- /dev/null +++ b/sf_purchase_arrival_inform/models/storage_list.py @@ -0,0 +1,140 @@ +from odoo import models, fields, api +from odoo.exceptions import ValidationError + + +class StorageList(models.Model): + _name = 'storage.list' + _description = '收料入库明细' + _order = 'create_date desc' + + # 基础关联字段 + purchase_order_id = fields.Many2one('purchase.order', string='采购订单', required=True) + picking_id = fields.Many2one('stock.picking', string='收料入库单', readonly=True) + product_id = fields.Many2one('product.product', string='产品', required=True) + + # 产品信息(基于product_id的related字段) + product_code = fields.Char(related='product_id.default_code', string='产品料号', store=True) + product_remark = fields.Text(string='产品说明') + part_number = fields.Char(string='零件图号') + + # 数量字段 + ordered_quantity = fields.Float(string='需求数量', required=True) + # 到货数量,默认=需求数量 + current_arrival_quantity = fields.Float(string='到货数量', required=True) + + + @api.model + def create(self, vals): + # 如果没有设置current_arrival_quantity,则默认等于ordered_quantity + if 'current_arrival_quantity' not in vals and 'ordered_quantity' in vals: + vals['current_arrival_quantity'] = vals['ordered_quantity'] + return super().create(vals) + + @api.constrains('current_arrival_quantity') + def _check_current_arrival_quantity(self): + for line in self: + if line.current_arrival_quantity <= 0: + raise ValidationError('到货数量必须大于0') + if line.current_arrival_quantity > line.ordered_quantity: + raise ValidationError('当前到货数量不能超过需求数量') + + def button_confirm(self): + """更新采购订单行的收料数量""" + for record in self: + # 查找对应的采购订单行 + order_line = self.env['purchase.order.line'].search([ + ('order_id', '=', record.purchase_order_id.id), + ('product_id', '=', record.product_id.id) + ], limit=1) + + if order_line: + # 累加当前记录的到货数量到采购订单行 + order_line.arrival_quantity += record.current_arrival_quantity + + # 更新质量检查单状态为待处理 + if self: + self._update_quality_check_status(self[0].purchase_order_id) + + + def _update_quality_check_status(self, purchase_order): + """更新质量检查单状态为待处理 + Args: + purchase_order: 采购订单记录 + Returns: + int: 更新的质量检查单数量 + """ + try: + # 获取当前到货通知涉及的收料入库单 + storage_records = self.env['storage.list'].search([ + ('purchase_order_id', '=', purchase_order.id) + ]) + if not storage_records: + return 0 + + # 收集所有相关的picking_id和product_id(过滤空值) + picking_ids = [pid for pid in storage_records.mapped('picking_id.id') if pid] + product_ids = [pid for pid in storage_records.mapped('product_id.id') if pid] + + if not picking_ids or not product_ids: + return 0 + + # 构建质量检查单搜索域,包含等待和待处理状态 + base_domain = [ + ('quality_state', 'in', ['waiting', 'none']), + ('product_id', 'in', product_ids), + ] + + # 方法1:通过picking_id精确匹配质量检查单 + quality_checks_by_picking = self.env['quality.check'].search( + base_domain + [('picking_id', 'in', picking_ids)] + ) + + # 方法2:通过move_id匹配(更精确,因为一个picking可能有多个move) + stock_moves = self.env['stock.move'].search([ + ('picking_id', 'in', picking_ids), + ('product_id', 'in', product_ids), + ]) + + quality_checks_by_move = self.env['quality.check'] + if stock_moves: + quality_checks_by_move = self.env['quality.check'].search( + base_domain + [('move_id', 'in', stock_moves.ids)] + ) + + # 方法3:通过move_line_id匹配(最精确) + move_lines = self.env['stock.move.line'].search([ + ('picking_id', 'in', picking_ids), + ('product_id', 'in', product_ids), + ]) + + quality_checks_by_move_line = self.env['quality.check'] + if move_lines: + quality_checks_by_move_line = self.env['quality.check'].search( + base_domain + [('move_line_id', 'in', move_lines.ids)] + ) + + # 合并所有匹配的质量检查单(去重) + all_quality_checks = quality_checks_by_picking | quality_checks_by_move | quality_checks_by_move_line + + if not all_quality_checks: + return 0 + + # 批量更新状态为待处理 + all_quality_checks.write({'quality_state': 'none'}) + + # 记录日志 + product_names = storage_records.mapped('product_id.name') + purchase_order.message_post( + body=f"到货通知确认:已更新 {len(all_quality_checks)} 个质量检查单状态为待处理" + f"涉及产品:{', '.join(product_names)}" + f"涉及收料入库单:{', '.join([p.name or '' for p in storage_records.mapped('picking_id')])}" + ) + + return len(all_quality_checks) + + except Exception as e: + # 记录错误日志,但不阻断流程 + purchase_order.message_post( + body=f"更新质量检查单状态时发生错误:{str(e)}" + ) + return 0 \ No newline at end of file diff --git a/sf_purchase_arrival_inform/security/ir.model.access.csv b/sf_purchase_arrival_inform/security/ir.model.access.csv new file mode 100644 index 00000000..729f19fc --- /dev/null +++ b/sf_purchase_arrival_inform/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_storage_list,storage.list,model_storage_list,purchase.group_purchase_user,1,1,1,1 +access_purchase_confirm_wizard,purchase.confirm.wizard,model_purchase_confirm_wizard,purchase.group_purchase_user,1,1,1,1 +access_purchase_confirm_wizard_line,purchase.confirm.wizard.line,model_purchase_confirm_wizard_line,purchase.group_purchase_user,1,1,1,1 \ No newline at end of file diff --git a/sf_purchase_arrival_inform/views/product_category.xml b/sf_purchase_arrival_inform/views/product_category.xml new file mode 100644 index 00000000..a5036836 --- /dev/null +++ b/sf_purchase_arrival_inform/views/product_category.xml @@ -0,0 +1,27 @@ + + + + + + product.category.form.inherit + product.category + + + + + + + + + arrival.inform.quality.point.view.form + quality.point + + primary + + + + + + + + \ No newline at end of file diff --git a/sf_purchase_arrival_inform/views/purchase_view.xml b/sf_purchase_arrival_inform/views/purchase_view.xml new file mode 100644 index 00000000..da09546c --- /dev/null +++ b/sf_purchase_arrival_inform/views/purchase_view.xml @@ -0,0 +1,58 @@ + + + + + + purchase.order.tree.inherit + purchase.order + + + + + + + + + + + + purchase.order.form + purchase.order + + + + + + + + + + + storage.list.wrapper.form + purchase.order + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sf_purchase_arrival_inform/wizard/__init__.py b/sf_purchase_arrival_inform/wizard/__init__.py new file mode 100644 index 00000000..70899de9 --- /dev/null +++ b/sf_purchase_arrival_inform/wizard/__init__.py @@ -0,0 +1,2 @@ +from . import purchase_confirm_wizard + diff --git a/sf_purchase_arrival_inform/wizard/purchase_confirm_wizard.py b/sf_purchase_arrival_inform/wizard/purchase_confirm_wizard.py new file mode 100644 index 00000000..d3a74de1 --- /dev/null +++ b/sf_purchase_arrival_inform/wizard/purchase_confirm_wizard.py @@ -0,0 +1,64 @@ +from odoo import models, fields, api, _ + + +class PurchaseConfirmWizard(models.TransientModel): + _name = 'purchase.confirm.wizard' + _description = '采购订单确认向导' + + purchase_order_id = fields.Many2one('purchase.order', string='采购订单', required=True) + line_ids = fields.One2many('purchase.confirm.wizard.line', 'wizard_id', string='产品明细') + + @api.model_create_multi + def create(self, vals_list): + """创建向导时自动生成产品明细""" + records = super().create(vals_list) + + for record in records: + if record.purchase_order_id: + # 自动创建产品明细行 + line_data = [] + for line in record.purchase_order_id.order_line: + line_data.append({ + 'wizard_id': record.id, + 'product_id': line.product_id.id, + 'product_name': line.product_id.name, + 'product_qty': line.product_qty, + 'product_uom': line.product_uom.name, + 'uom_rounding': line.product_uom.rounding, + }) + + if line_data: + self.env['purchase.confirm.wizard.line'].create(line_data) + + return records + + def action_confirm(self): + """确认采购订单""" + if self.purchase_order_id: + # 调用原始的确认方法 + self.purchase_order_id._execute_original_confirm() + + # 返回关闭向导并刷新采购订单页面 + return { + 'type': 'ir.actions.act_window_close', + 'infos': { + 'title': _('成功'), + 'message': _('采购订单已确认成功!'), + } + } + + def action_cancel(self): + """取消操作""" + return {'type': 'ir.actions.act_window_close'} + + +class PurchaseConfirmWizardLine(models.TransientModel): + _name = 'purchase.confirm.wizard.line' + _description = '采购订单确认向导明细' + + wizard_id = fields.Many2one('purchase.confirm.wizard', string='向导', ondelete='cascade') + product_id = fields.Many2one('product.product', string='产品') + product_name = fields.Char(string='产品名称') + product_qty = fields.Float(string='数量') + product_uom = fields.Char(string='单位') + uom_rounding = fields.Float(string='舍入精度') \ No newline at end of file diff --git a/sf_purchase_arrival_inform/wizard/purchase_confirm_wizard_view.xml b/sf_purchase_arrival_inform/wizard/purchase_confirm_wizard_view.xml new file mode 100644 index 00000000..33af4cf2 --- /dev/null +++ b/sf_purchase_arrival_inform/wizard/purchase_confirm_wizard_view.xml @@ -0,0 +1,35 @@ + + + + + purchase.confirm.wizard.form + purchase.confirm.wizard + + + + 确认提示:以下产品数量将按单位舍入精度向上取整处理,请核实后确认是否继续。 + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file