diff --git a/modular_types/__init__.py b/modular_types/__init__.py new file mode 100644 index 00000000000..aee8895e7a3 --- /dev/null +++ b/modular_types/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/modular_types/__manifest__.py b/modular_types/__manifest__.py new file mode 100644 index 00000000000..2d21b238304 --- /dev/null +++ b/modular_types/__manifest__.py @@ -0,0 +1,19 @@ +{ + 'name': 'Sales Modular Products', + 'version': '1.0', + 'summary': 'Automate creation of modular product lines on sales orders.', + 'author': 'ksoz', + 'depends': ['sale_management', 'product', 'mrp'], + 'license': 'LGPL-3', + 'data': [ + 'security/ir.model.access.csv', + 'wizards/modular_wizard_views.xml', + 'views/modular_type_views.xml', + 'views/product_template_views.xml', + 'views/mrp_bom_views.xml', + 'views/sale_order_view.xml', + 'views/mrp_production_view.xml', + ], + 'installable': True, + 'application': False, +} diff --git a/modular_types/models/__init__.py b/modular_types/models/__init__.py new file mode 100644 index 00000000000..0b319dde1c1 --- /dev/null +++ b/modular_types/models/__init__.py @@ -0,0 +1,5 @@ +from . import modular_type_config +from . import product_template +from . import mrp_bom +from . import sale_order +from . import mrp diff --git a/modular_types/models/modular_type_config.py b/modular_types/models/modular_type_config.py new file mode 100644 index 00000000000..84967f65714 --- /dev/null +++ b/modular_types/models/modular_type_config.py @@ -0,0 +1,15 @@ +from odoo import models, fields + + +class ModularTypeConfig(models.Model): + _name = 'modular.type.config' + _description = 'Configuration for Modular Types' + + name = fields.Char(string='Modular Type Name', required=True) + default_quantity = fields.Float(string='Default Quantity', default=1.0, required=True) + component_product_id = fields.Many2one( + 'product.product', + string='Component Product', + required=True, + help="The product that represents this modular component on the sales order line." + ) diff --git a/modular_types/models/mrp.py b/modular_types/models/mrp.py new file mode 100644 index 00000000000..21d66a2b7d5 --- /dev/null +++ b/modular_types/models/mrp.py @@ -0,0 +1,38 @@ +from odoo import api, models, fields + + +class StockMove(models.Model): + _inherit = 'stock.move' + + modular_type_id = fields.Many2one( + 'modular.type.config', + string="Module Type", + readonly=True + ) + + +class MrpProduction(models.Model): + _inherit = 'mrp.production' + + @api.model_create_multi + def create(self, vals_list): + custom_values_map = {} + for i, vals in enumerate(vals_list): + if 'custom_modular_values' in vals: + custom_values_map[i] = vals.pop('custom_modular_values') + + mos = super().create(vals_list) + + for i, mo in enumerate(mos): + custom_values = custom_values_map.get(i) + if custom_values and mo.move_raw_ids: + for custom_val in custom_values: + move_to_update = mo.move_raw_ids.filtered( + lambda m: m.product_id.id == custom_val['component_product_id'] + ) + if move_to_update: + move_to_update.write({ + 'product_uom_qty': custom_val['quantity'], + 'modular_type_id': custom_val['modular_type_id'], + }) + return mos diff --git a/modular_types/models/mrp_bom.py b/modular_types/models/mrp_bom.py new file mode 100644 index 00000000000..30eafe76cd9 --- /dev/null +++ b/modular_types/models/mrp_bom.py @@ -0,0 +1,11 @@ +from odoo import models, fields + + +class MrpBomLine(models.Model): + _inherit = 'mrp.bom.line' + + modular_type_id = fields.Many2one( + 'modular.type.config', + string='Modular Type', + help="The modular type configuration for this BOM line." + ) diff --git a/modular_types/models/product_template.py b/modular_types/models/product_template.py new file mode 100644 index 00000000000..4dbcc2107a6 --- /dev/null +++ b/modular_types/models/product_template.py @@ -0,0 +1,12 @@ +from odoo import models, fields + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + modular_type_ids = fields.Many2many( + 'modular.type.config', + 'component_product_id', + string='Modular Types', + help="Select the modular types applicable to this product template." + ) diff --git a/modular_types/models/sale_order.py b/modular_types/models/sale_order.py new file mode 100644 index 00000000000..871b01fc620 --- /dev/null +++ b/modular_types/models/sale_order.py @@ -0,0 +1,75 @@ +from odoo import api, models, fields + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + has_modular_type = fields.Boolean( + string='Has Modular Type', + compute='_compute_has_modular_type', + help="Technical field to check if the product's BoM has modular components." + ) + source_modular_type_id = fields.Many2one( + 'modular.type.config', + string="Source Modular Type", + readonly=True, + copy=False + ) + + @api.depends('product_id') + def _compute_has_modular_type(self): + if not self.product_id: + self.has_modular_type = False + return + + boms = self.env['mrp.bom']._bom_find( + self.product_id, + company_id=self.company_id.id, + bom_type='normal' + ) + for line in self: + line.has_modular_type = False + bom = boms.get(line.product_id) + if bom: + if any(bom_line.modular_type_id for bom_line in bom.bom_line_ids): + line.has_modular_type = True + + def action_configure_modular_line(self): + self.ensure_one() + + boms_dict = self.env['mrp.bom']._bom_find( + self.product_id, + company_id=self.company_id.id, + bom_type='normal' + ) + bom = boms_dict.get(self.product_id) + + if not bom: + return True + + wizard_lines_vals = [] + for bom_line in bom.bom_line_ids: + if bom_line.modular_type_id: + wizard_lines_vals.append((0, 0, { + 'modular_type_id': bom_line.modular_type_id.id, + 'component_product_id': bom_line.product_id.id, + 'default_quantity': bom_line.product_qty * self.product_uom_qty, + 'new_quantity': bom_line.product_qty * self.product_uom_qty, + })) + + if not wizard_lines_vals: + return True + + wizard = self.env['modular.line.generator.wizard'].create({ + 'sale_order_id': self.order_id.id, + 'wizard_line_ids': wizard_lines_vals, + }) + + return { + 'name': 'Set Modular Type Values', + 'type': 'ir.actions.act_window', + 'res_model': 'modular.line.generator.wizard', + 'view_mode': 'form', + 'res_id': wizard.id, + 'target': 'new', + } diff --git a/modular_types/security/ir.model.access.csv b/modular_types/security/ir.model.access.csv new file mode 100644 index 00000000000..dd2911b0109 --- /dev/null +++ b/modular_types/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_modular_type_config_user,modular.type.config.user,model_modular_type_config,base.group_user,1,1,1,1 +access_modular_line_generator_wizard,access_modular_line_generator_wizard,modular_types.model_modular_line_generator_wizard,base.group_user,1,1,1,1 +access_modular_line_generator_wizard_line,access_modular_line_generator_wizard_line,modular_types.model_modular_line_generator_wizard_line,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/modular_types/views/modular_type_views.xml b/modular_types/views/modular_type_views.xml new file mode 100644 index 00000000000..0df8694f2f8 --- /dev/null +++ b/modular_types/views/modular_type_views.xml @@ -0,0 +1,48 @@ + + + + + modular.type.config.form + modular.type.config + + + + + + + + + + + + + + + + + + + view.modular_type_list + modular.type.config + + + + + + + + + + + Modular Types + modular.type.config + list,form + + + + + diff --git a/modular_types/views/mrp_bom_views.xml b/modular_types/views/mrp_bom_views.xml new file mode 100644 index 00000000000..4ca029b9941 --- /dev/null +++ b/modular_types/views/mrp_bom_views.xml @@ -0,0 +1,14 @@ + + + + mrp.bom.form.modular + mrp.bom + + + + + + + + + \ No newline at end of file diff --git a/modular_types/views/mrp_production_view.xml b/modular_types/views/mrp_production_view.xml new file mode 100644 index 00000000000..40bd5b92494 --- /dev/null +++ b/modular_types/views/mrp_production_view.xml @@ -0,0 +1,15 @@ + + + + + mrp.production.form.modular + mrp.production + + + + + + + + + \ No newline at end of file diff --git a/modular_types/views/product_template_views.xml b/modular_types/views/product_template_views.xml new file mode 100644 index 00000000000..13cf3585738 --- /dev/null +++ b/modular_types/views/product_template_views.xml @@ -0,0 +1,13 @@ + + + + product.template.form.inherit + product.template + + + + + + + + diff --git a/modular_types/views/sale_order_view.xml b/modular_types/views/sale_order_view.xml new file mode 100644 index 00000000000..c894773a20f --- /dev/null +++ b/modular_types/views/sale_order_view.xml @@ -0,0 +1,21 @@ + + + + + sale.order.form.modular.line.button + sale.order + + + + + + + + + + diff --git a/modular_types/wizards/__init__.py b/modular_types/wizards/__init__.py new file mode 100644 index 00000000000..f5a0416d9f3 --- /dev/null +++ b/modular_types/wizards/__init__.py @@ -0,0 +1 @@ +from . import modular_wizard diff --git a/modular_types/wizards/modular_wizard.py b/modular_types/wizards/modular_wizard.py new file mode 100644 index 00000000000..a9fd0b453d0 --- /dev/null +++ b/modular_types/wizards/modular_wizard.py @@ -0,0 +1,57 @@ +from odoo import models, fields + + +class ModularLineGeneratorWizard(models.TransientModel): + _name = 'modular.line.generator.wizard' + _description = 'Wizard to Generate Modular Lines' + + sale_order_id = fields.Many2one('sale.order', string="Sales Order", readonly=True) + sale_order_line_id = fields.Many2one('sale.order.line', string="Sales Order Line") + wizard_line_ids = fields.One2many( + 'modular.line.generator.wizard.line', + 'wizard_id', + string='Modular Lines' + ) + + def action_apply(self): + self.ensure_one() + so_line = self.sale_order_line_id or self.env['sale.order.line'].browse( + self.env.context.get('active_id') + ) + if not so_line: + return + + so_line.modular_value_ids.unlink() + vals_list = [] + for line in self.wizard_line_ids: + vals_list.append({ + 'sale_order_line_id': so_line.id, + 'modular_type_id': line.modular_type_id.id, + 'component_product_id': line.component_product_id.id, + 'quantity': line.new_quantity, + }) + self.env['sale.order.line.modular.value'].create(vals_list) + return {'type': 'ir.actions.act_window_close'} + + def action_confirm(self): + for line in self.wizard_line_ids: + if line.new_quantity > 0: + self.env['sale.order.line'].create({ + 'order_id': self.sale_order_id.id, + 'product_id': line.component_product_id.id, + 'product_uom_qty': line.new_quantity, + 'price_unit': 0, + 'source_modular_type_id': line.modular_type_id.id, + }) + return {'type': 'ir.actions.act_window_close'} + + +class ModularLineGeneratorWizardLine(models.TransientModel): + _name = 'modular.line.generator.wizard.line' + _description = 'Line for Modular Line Generator Wizard' + + wizard_id = fields.Many2one('modular.line.generator.wizard', required=True, ondelete='cascade') + modular_type_id = fields.Many2one('modular.type.config', string="Module Type", readonly=True) + component_product_id = fields.Many2one('product.product', string="Component", readonly=True) + default_quantity = fields.Float(string="BoM Qty", readonly=True) + new_quantity = fields.Float(string="Quantity", help="The quantity to add to the Sales Order.") diff --git a/modular_types/wizards/modular_wizard_views.xml b/modular_types/wizards/modular_wizard_views.xml new file mode 100644 index 00000000000..3ac12408128 --- /dev/null +++ b/modular_types/wizards/modular_wizard_views.xml @@ -0,0 +1,27 @@ + + + + modular.line.generator.wizard.form + modular.line.generator.wizard + + + + Review and adjust the quantities for the modular components below. + + + + + + + + + + + + + + + \ No newline at end of file
+ Review and adjust the quantities for the modular components below. +