From 50f8bf5ab1d9885e980488161e6e37dd19b32db5 Mon Sep 17 00:00:00 2001 From: guanhuan Date: Thu, 22 May 2025 11:37:41 +0800 Subject: [PATCH 01/22] =?UTF-8?q?=E9=9C=80=E6=B1=82=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sf_demand_plan/__init__.py | 3 + sf_demand_plan/__manifest__.py | 31 ++++ sf_demand_plan/models/__init__.py | 4 + sf_demand_plan/models/sale_order.py | 14 ++ .../models/sf_production_demand_plan.py | 158 ++++++++++++++++++ sf_demand_plan/security/ir.model.access.csv | 3 + sf_demand_plan/static/src/scss/style.css | 3 + sf_demand_plan/views/demand_plan.xml | 95 +++++++++++ 8 files changed, 311 insertions(+) create mode 100644 sf_demand_plan/__init__.py create mode 100644 sf_demand_plan/__manifest__.py create mode 100644 sf_demand_plan/models/__init__.py create mode 100644 sf_demand_plan/models/sale_order.py create mode 100644 sf_demand_plan/models/sf_production_demand_plan.py create mode 100644 sf_demand_plan/security/ir.model.access.csv create mode 100644 sf_demand_plan/static/src/scss/style.css create mode 100644 sf_demand_plan/views/demand_plan.xml diff --git a/sf_demand_plan/__init__.py b/sf_demand_plan/__init__.py new file mode 100644 index 00000000..cde864ba --- /dev/null +++ b/sf_demand_plan/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/sf_demand_plan/__manifest__.py b/sf_demand_plan/__manifest__.py new file mode 100644 index 00000000..fba85e4c --- /dev/null +++ b/sf_demand_plan/__manifest__.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +{ + 'name': '机企猫智能工厂 需求计划', + 'version': '1.0', + 'summary': '智能工厂计划管理', + 'sequence': 1, + 'description': """ +在本模块,支持齐套检查与下达生产 + """, + 'category': 'sf', + 'website': 'https://www.sf.jikimo.com', + 'depends': ['sf_plan'], + 'data': [ + 'security/ir.model.access.csv', + 'views/demand_plan.xml', + ], + 'demo': [ + ], + 'assets': { + 'web.assets_qweb': [ + ], + 'web.assets_backend': [ + 'sf_demand_plan/static/src/scss/style.css', + ] + }, + 'license': 'LGPL-3', + 'installable': True, + 'application': False, + 'auto_install': False, +} diff --git a/sf_demand_plan/models/__init__.py b/sf_demand_plan/models/__init__.py new file mode 100644 index 00000000..a0554c11 --- /dev/null +++ b/sf_demand_plan/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import sf_production_demand_plan +from . import sale_order diff --git a/sf_demand_plan/models/sale_order.py b/sf_demand_plan/models/sale_order.py new file mode 100644 index 00000000..80499fcf --- /dev/null +++ b/sf_demand_plan/models/sale_order.py @@ -0,0 +1,14 @@ +from odoo import models, fields, api, _ + + +class ReSaleOrder(models.Model): + _inherit = 'sale.order' + + def sale_order_create_line(self, product, item): + ret = super(ReSaleOrder, self).sale_order_create_line(product, item) + vals = { + 'sale_order_id': ret.order_id.id, + 'sale_order_line_id': ret.id, + } + self.env['sf.production.demand.plan'].sudo().create(vals) + return ret diff --git a/sf_demand_plan/models/sf_production_demand_plan.py b/sf_demand_plan/models/sf_production_demand_plan.py new file mode 100644 index 00000000..8f250aed --- /dev/null +++ b/sf_demand_plan/models/sf_production_demand_plan.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + + +class sf_production_plan(models.Model): + _name = 'sf.production.demand.plan' + _description = 'sf_production_demand_plan' + + def _get_machining_precision(self): + machinings = self.env['sf.machining.accuracy'].sudo().search([]) + + list = [(m.sync_id, m.name) for m in machinings] + return list + + priority = fields.Selection([ + ('1', '紧急'), + ('2', '高'), + ('3', '中'), + ('4', '低'), + ], string='优先级', default='3') + status = fields.Selection([ + ('20', '待确认'), + ('30', '需求确认'), + ('50', '已下达'), + ('100', '取消'), + ], string='状态') + + sale_order_id = fields.Many2one(comodel_name="sale.order", + string="销售订单") + sale_order_line_id = fields.Many2one(comodel_name="sale.order.line", + string="销售订单行") + company_id = fields.Many2one( + related='sale_order_id.company_id', + store=True, index=True, precompute=True) + partner_id = fields.Many2one( + comodel_name='res.partner', + related='sale_order_line_id.order_partner_id', + string="客户", + store=True, index=True) + order_remark = fields.Text(related='sale_order_id.remark', + string="订单备注", store=True) + glb_url = fields.Char(related='sale_order_line_id.glb_url', string='glb文件地址') + product_id = fields.Many2one( + comodel_name='product.product', + related='sale_order_line_id.product_id', + string='产品', store=True, index=True) + model_id = fields.Char('模型ID', related='product_id.model_id') + part_name = fields.Char('零件名称', related='product_id.part_name') + part_number = fields.Char('零件图号', compute='_compute_part_number', store=True) + is_incoming_material = fields.Boolean('客供料', related='sale_order_line_id.is_incoming_material', store=True) + supply_method = fields.Selection([ + ('automation', "自动化产线加工"), + ('manual', "人工线下加工"), + ('purchase', "外购"), + ('outsourcing', "委外加工"), + ], string='供货方式', related='sale_order_line_id.supply_method', store=True) + product_uom_qty = fields.Float( + string="需求数量", + related='sale_order_line_id.product_uom_qty', store=True) + deadline_of_delivery = fields.Date('客户交期', related='sale_order_id.deadline_of_delivery', store=True) + inventory_quantity_auto_apply = fields.Float( + string="成品库存", + compute='_compute_inventory_quantity_auto_apply' + ) + qty_delivered = fields.Float( + "交货数量", related='sale_order_line_id.qty_delivered') + qty_to_deliver = fields.Float( + "待交货数量", related='sale_order_line_id.qty_to_deliver') + model_long = fields.Char('尺寸', compute='_compute_model_long') + materials_id = fields.Char('材料', compute='_compute_materials_id', store=True) + model_machining_precision = fields.Selection(selection=_get_machining_precision, string='精度', + related='product_id.model_machining_precision') + model_process_parameters_ids = fields.Many2many('sf.production.process.parameter', + 'plan_process_parameter_rel', + string='表面工艺', + compute='_compute_model_process_parameters_ids' + + ) + product_remark = fields.Char("产品备注", related='product_id.model_remark') + order_code = fields.Char('E-SHOP订单号', related='sale_order_id.order_code') + order_state = fields.Selection( + string='订单状态', + related='sale_order_line_id.state') + route_id = fields.Many2one('stock.route', string='路线', related='sale_order_line_id.route_id', store=True) + date_order = fields.Datetime('下单日期', related='sale_order_id.date_order') + plan_remark = fields.Text("计划备注") + + processing_time = fields.Char('程序工时') + planned_start_date = fields.Date('计划开工日期') + actual_start_date = fields.Date('实际开工日期') + actual_end_date = fields.Date('实际完工日期') + print_count = fields.Char('打印次数', default='T0C0') + + sequence = fields.Integer('序号') + + @api.depends('product_id.part_number', 'product_id.model_name') + def _compute_part_number(self): + for line in self: + if line.product_id: + if line.product_id.part_number: + line.part_number = line.product_id.part_number + else: + if line.product_id.model_name: + line.part_number = line.product_id.model_name.rsplit('.', 1)[0] + else: + line.part_number = None + + @api.depends('product_id.length', 'product_id.width', 'product_id.height') + def _compute_model_long(self): + for line in self: + if line.product_id: + line.model_long = f"{line.product_id.length}*{line.product_id.width}*{line.product_id.height}" + else: + line.model_long = None + + @api.depends('product_id.materials_id') + def _compute_materials_id(self): + for line in self: + if line.product_id: + line.materials_id = f"{line.product_id.materials_id.name}*{line.product_id.materials_type_id.name}" + else: + line.materials_id = None + + @api.depends('product_id.model_process_parameters_ids') + def _compute_model_process_parameters_ids(self): + for line in self: + if line.product_id and line.product_id.model_process_parameters_ids: + line.model_process_parameters_ids = [(6, 0, line.product_id.model_process_parameters_ids.ids)] + else: + line.model_process_parameters_ids = [(5, 0, 0)] + + def _compute_inventory_quantity_auto_apply(self): + location_id = self.env['stock.location'].search([('name', '=', '成品存货区')], limit=1).id + product_ids = self.mapped('product_id').ids + if product_ids: + quant_data = self.env['stock.quant'].read_group( + domain=[ + ('product_id', 'in', product_ids), + ('location_id', '=', location_id) + ], + fields=['product_id', 'inventory_quantity_auto_apply'], + groupby=['product_id'] + ) + quantity_map = {item['product_id'][0]: item['inventory_quantity_auto_apply'] for item in quant_data} + else: + quantity_map = {} + for line in self: + if line.product_id: + line.inventory_quantity_auto_apply = quantity_map.get(line.product_id.id, 0.0) + else: + line.inventory_quantity_auto_apply = 0.0 + + @api.constrains('planned_start_date') + def _check_planned_start_date(self): + for record in self: + if record.planned_start_date and record.planned_start_date < fields.Date.today(): + raise ValidationError("计划开工日期必须大于或等于今天。") diff --git a/sf_demand_plan/security/ir.model.access.csv b/sf_demand_plan/security/ir.model.access.csv new file mode 100644 index 00000000..cb2af3fc --- /dev/null +++ b/sf_demand_plan/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_sf_production_demand_plan,sf.production.demand.plan,model_sf_production_demand_plan,base.group_user,1,0,0,0 +access_sf_production_demand_plan_for_dispatch,sf.production.demand.plan for dispatch,model_sf_production_demand_plan,sf_base.group_plan_dispatch,1,1,0,0 diff --git a/sf_demand_plan/static/src/scss/style.css b/sf_demand_plan/static/src/scss/style.css new file mode 100644 index 00000000..90b00db8 --- /dev/null +++ b/sf_demand_plan/static/src/scss/style.css @@ -0,0 +1,3 @@ +.demand_plan_tree th:not(.o_list_record_selector,.row_no,[data-name=sequence]) { + min-width: 98px !important; +} \ No newline at end of file diff --git a/sf_demand_plan/views/demand_plan.xml b/sf_demand_plan/views/demand_plan.xml new file mode 100644 index 00000000..6cc6a998 --- /dev/null +++ b/sf_demand_plan/views/demand_plan.xml @@ -0,0 +1,95 @@ + + + sf.production.demand.plan.tree + sf.production.demand.plan + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + sf.production.demand.plan.search + sf.production.demand.plan + + + + + + + + + + + + + + + + + + + + + + + + + + 需求计划 + ir.actions.act_window + sf.production.demand.plan + tree + + + + + \ No newline at end of file From e46e6dfc2ad910c81224b05f40ac9e1885a1f608 Mon Sep 17 00:00:00 2001 From: guanhuan Date: Thu, 22 May 2025 17:40:56 +0800 Subject: [PATCH 02/22] =?UTF-8?q?=E9=9C=80=E6=B1=82=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../models/sf_production_demand_plan.py | 49 +++++++++++++++++-- sf_demand_plan/static/src/scss/style.css | 4 ++ sf_demand_plan/views/demand_plan.xml | 4 +- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/sf_demand_plan/models/sf_production_demand_plan.py b/sf_demand_plan/models/sf_production_demand_plan.py index 8f250aed..d0983cc2 100644 --- a/sf_demand_plan/models/sf_production_demand_plan.py +++ b/sf_demand_plan/models/sf_production_demand_plan.py @@ -20,12 +20,13 @@ class sf_production_plan(models.Model): ('4', '低'), ], string='优先级', default='3') status = fields.Selection([ + ('10', '草稿'), ('20', '待确认'), ('30', '需求确认'), + ('40', '待下达生产'), ('50', '已下达'), ('100', '取消'), - ], string='状态') - + ], string='状态', compute='_compute_status', store=True) sale_order_id = fields.Many2one(comodel_name="sale.order", string="销售订单") sale_order_line_id = fields.Many2one(comodel_name="sale.order.line", @@ -75,7 +76,7 @@ class sf_production_plan(models.Model): 'plan_process_parameter_rel', string='表面工艺', compute='_compute_model_process_parameters_ids' - + , store=True ) product_remark = fields.Char("产品备注", related='product_id.model_remark') order_code = fields.Char('E-SHOP订单号', related='sale_order_id.order_code') @@ -88,12 +89,35 @@ class sf_production_plan(models.Model): processing_time = fields.Char('程序工时') planned_start_date = fields.Date('计划开工日期') - actual_start_date = fields.Date('实际开工日期') + actual_start_date = fields.Date('实际开工日期', compute='_compute_actual_start_date') actual_end_date = fields.Date('实际完工日期') print_count = fields.Char('打印次数', default='T0C0') sequence = fields.Integer('序号') + @api.depends('sale_order_id.state', 'sale_order_id.mrp_production_ids.schedule_state', 'sale_order_id.order_line') + def _compute_status(self): + for record in self: + if record.sale_order_id: + sale_order_state = record.sale_order_id.state + if sale_order_state in ('draft', 'sent', 'supply method'): + record.status = '20' # 待确认 + if record.supply_method in ('purchase', 'outsourcing') and sale_order_state in ( + 'sale', 'processing', 'physical_distribution', 'delivered', + 'done') and sale_order_state != 'cancel': + record.status = '50' # 已下达 + if record.supply_method in ('automation', 'manual'): + if sale_order_state in ( + 'sale', 'processing', 'physical_distribution', 'delivered', + 'done') and sale_order_state != 'cancel': + record.status = '30' # 需求确认 + # 检查所有制造订单的排程单状态 + if all(order.product_id == record.product_id and order.schedule_state != '未排' for order in + record.sale_order_id.mrp_production_ids): + record.status = '50' # 已下达 + if sale_order_state == 'cancel' or not record.sale_order_line_id: + record.status = '100' # 取消 + @api.depends('product_id.part_number', 'product_id.model_name') def _compute_part_number(self): for line in self: @@ -151,6 +175,23 @@ class sf_production_plan(models.Model): else: line.inventory_quantity_auto_apply = 0.0 + @api.depends('sale_order_id.mrp_production_ids.workorder_ids.date_start') + def _compute_actual_start_date(self): + for record in self: + if record.sale_order_id and record.sale_order_id.mrp_production_ids: + manufacturing_orders = record.sale_order_id.mrp_production_ids.filtered( + lambda mo: mo.product_id == record.product_id) + if manufacturing_orders: + start_dates = [ + workorder.date_start.date() for mo in manufacturing_orders + for workorder in mo.workorder_ids if workorder.date_start + ] + record.actual_start_date = min(start_dates) if start_dates else None + else: + record.actual_start_date = None + else: + record.actual_start_date = None + @api.constrains('planned_start_date') def _check_planned_start_date(self): for record in self: diff --git a/sf_demand_plan/static/src/scss/style.css b/sf_demand_plan/static/src/scss/style.css index 90b00db8..6c8e57fd 100644 --- a/sf_demand_plan/static/src/scss/style.css +++ b/sf_demand_plan/static/src/scss/style.css @@ -1,3 +1,7 @@ .demand_plan_tree th:not(.o_list_record_selector,.row_no,[data-name=sequence]) { min-width: 98px !important; +} + +.demand_plan_tree .o_list_table_grouped th:not(.o_list_record_selector,.row_no,[data-name=sequence]) { + min-width: 98px !important; } \ No newline at end of file diff --git a/sf_demand_plan/views/demand_plan.xml b/sf_demand_plan/views/demand_plan.xml index 6cc6a998..1975b008 100644 --- a/sf_demand_plan/views/demand_plan.xml +++ b/sf_demand_plan/views/demand_plan.xml @@ -3,7 +3,7 @@ sf.production.demand.plan.tree sf.production.demand.plan - + @@ -24,7 +24,7 @@ - + From a5243970d5cdcde85693a52b36d339cba8356213 Mon Sep 17 00:00:00 2001 From: guanhuan Date: Fri, 23 May 2025 14:14:50 +0800 Subject: [PATCH 03/22] =?UTF-8?q?=E9=9C=80=E6=B1=82=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../models/sf_production_demand_plan.py | 53 ++++++++++++++++--- sf_demand_plan/views/demand_plan.xml | 1 + 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/sf_demand_plan/models/sf_production_demand_plan.py b/sf_demand_plan/models/sf_production_demand_plan.py index d0983cc2..83b4c800 100644 --- a/sf_demand_plan/models/sf_production_demand_plan.py +++ b/sf_demand_plan/models/sf_production_demand_plan.py @@ -28,9 +28,9 @@ class sf_production_plan(models.Model): ('100', '取消'), ], string='状态', compute='_compute_status', store=True) sale_order_id = fields.Many2one(comodel_name="sale.order", - string="销售订单") + string="销售订单", readonly=True) sale_order_line_id = fields.Many2one(comodel_name="sale.order.line", - string="销售订单行") + string="销售订单行", readonly=True) company_id = fields.Many2one( related='sale_order_id.company_id', store=True, index=True, precompute=True) @@ -86,12 +86,15 @@ class sf_production_plan(models.Model): route_id = fields.Many2one('stock.route', string='路线', related='sale_order_line_id.route_id', store=True) date_order = fields.Datetime('下单日期', related='sale_order_id.date_order') plan_remark = fields.Text("计划备注") - + material_check = fields.Selection([ + ('0', "未齐套"), + ('1', "已齐套"), + ], string='投料齐套检查', compute='_compute_material_check', store=True) processing_time = fields.Char('程序工时') planned_start_date = fields.Date('计划开工日期') - actual_start_date = fields.Date('实际开工日期', compute='_compute_actual_start_date') - actual_end_date = fields.Date('实际完工日期') - print_count = fields.Char('打印次数', default='T0C0') + actual_start_date = fields.Date('实际开工日期', compute='_compute_actual_start_date', store=True) + actual_end_date = fields.Date('实际完工日期', compute='_compute_actual_end_date', store=True) + print_count = fields.Char('打印次数', default='T0C0', readonly=True) sequence = fields.Integer('序号') @@ -192,6 +195,44 @@ class sf_production_plan(models.Model): else: record.actual_start_date = None + @api.depends('sale_order_id.mrp_production_ids.workorder_ids.state', + 'sale_order_id.mrp_production_ids.workorder_ids.date_finished') + def _compute_actual_end_date(self): + for record in self: + if record.sale_order_id and record.sale_order_id.mrp_production_ids: + manufacturing_orders = record.sale_order_id.mrp_production_ids.filtered( + lambda mo: mo.product_id == record.product_id) + finished_orders = manufacturing_orders.filtered(lambda mo: mo.state == 'done') + if len(finished_orders) >= record.product_uom_qty: + end_dates = [ + workorder.date_finished.date() for mo in finished_orders + for workorder in mo.workorder_ids if workorder.date_finished + ] + record.actual_end_date = max(end_dates) if end_dates else None + else: + record.actual_end_date = None + else: + record.actual_end_date = None + + @api.depends('sale_order_id.mrp_production_ids.move_raw_ids.forecast_availability', + 'sale_order_id.mrp_production_ids.move_raw_ids.product_uom_qty') + def _compute_material_check(self): + for record in self: + if record.sale_order_id and record.sale_order_id.mrp_production_ids: + manufacturing_orders = record.sale_order_id.mrp_production_ids.filtered( + lambda mo: mo.product_id == record.product_id) + if manufacturing_orders: + total_reserved = sum(mo.forecast_availability for mo in manufacturing_orders) + total_to_consume = sum(mo.product_uom_qty for mo in manufacturing_orders) + if total_reserved >= total_to_consume: + record.material_check = '1' # 已齐套 + else: + record.material_check = '0' # 未齐套 + else: + record.material_check = None + else: + record.material_check = None + @api.constrains('planned_start_date') def _check_planned_start_date(self): for record in self: diff --git a/sf_demand_plan/views/demand_plan.xml b/sf_demand_plan/views/demand_plan.xml index 1975b008..9c9ee0ce 100644 --- a/sf_demand_plan/views/demand_plan.xml +++ b/sf_demand_plan/views/demand_plan.xml @@ -33,6 +33,7 @@ + From f4babfcd245d29dda085cac1164404f01c725690 Mon Sep 17 00:00:00 2001 From: guanhuan Date: Fri, 23 May 2025 17:26:21 +0800 Subject: [PATCH 04/22] =?UTF-8?q?=E4=B8=8B=E8=BE=BE=E7=94=9F=E4=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../models/sf_production_demand_plan.py | 16 +++++++++++----- sf_demand_plan/views/demand_plan.xml | 2 ++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/sf_demand_plan/models/sf_production_demand_plan.py b/sf_demand_plan/models/sf_production_demand_plan.py index 83b4c800..556a6d98 100644 --- a/sf_demand_plan/models/sf_production_demand_plan.py +++ b/sf_demand_plan/models/sf_production_demand_plan.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from odoo import models, fields, api, _ from odoo.exceptions import ValidationError +from odoo.tools import float_compare class sf_production_plan(models.Model): @@ -215,16 +216,18 @@ class sf_production_plan(models.Model): record.actual_end_date = None @api.depends('sale_order_id.mrp_production_ids.move_raw_ids.forecast_availability', - 'sale_order_id.mrp_production_ids.move_raw_ids.product_uom_qty') + 'sale_order_id.mrp_production_ids.move_raw_ids.quantity_done') def _compute_material_check(self): for record in self: if record.sale_order_id and record.sale_order_id.mrp_production_ids: manufacturing_orders = record.sale_order_id.mrp_production_ids.filtered( lambda mo: mo.product_id == record.product_id) - if manufacturing_orders: - total_reserved = sum(mo.forecast_availability for mo in manufacturing_orders) - total_to_consume = sum(mo.product_uom_qty for mo in manufacturing_orders) - if total_reserved >= total_to_consume: + if manufacturing_orders and manufacturing_orders.move_raw_ids: + total_forecast_availability = sum(manufacturing_orders.mapped('move_raw_ids.forecast_availability')) + total_quantity_done = sum(manufacturing_orders.mapped('move_raw_ids.quantity_done')) + total_sum = total_forecast_availability + total_quantity_done + if float_compare(total_sum, record.product_uom_qty, + precision_rounding=record.product_id.uom_id.rounding) >= 0: record.material_check = '1' # 已齐套 else: record.material_check = '0' # 未齐套 @@ -238,3 +241,6 @@ class sf_production_plan(models.Model): for record in self: if record.planned_start_date and record.planned_start_date < fields.Date.today(): raise ValidationError("计划开工日期必须大于或等于今天。") + + def release_production_order(self): + pass diff --git a/sf_demand_plan/views/demand_plan.xml b/sf_demand_plan/views/demand_plan.xml index 9c9ee0ce..94d81fea 100644 --- a/sf_demand_plan/views/demand_plan.xml +++ b/sf_demand_plan/views/demand_plan.xml @@ -43,6 +43,8 @@ +