diff --git a/mrp_unbuild_bom_cust_qty/__manifest__.py b/mrp_unbuild_bom_cust_qty/__manifest__.py index d48cd813..e7c78650 100644 --- a/mrp_unbuild_bom_cust_qty/__manifest__.py +++ b/mrp_unbuild_bom_cust_qty/__manifest__.py @@ -7,10 +7,11 @@ """, "author": "Solvos", "license": "LGPL-3", - "version": "13.0.2.2.0", + "version": "13.0.3.0.0", "category": "Manufacturing", "website": "https://github.com/solvosci/slv-manufacture", "depends": ["mrp"], + "excludes": ["mrp_unbuild_tracked_raw_material"], "data": [ "security/mrp_unbuild_bom_cust_qty_security.xml", "security/ir.model.access.csv", diff --git a/mrp_unbuild_bom_cust_qty/models/mrp_unbuild.py b/mrp_unbuild_bom_cust_qty/models/mrp_unbuild.py index c0d963e5..b60a97d1 100644 --- a/mrp_unbuild_bom_cust_qty/models/mrp_unbuild.py +++ b/mrp_unbuild_bom_cust_qty/models/mrp_unbuild.py @@ -3,6 +3,13 @@ from odoo import _, api, models, fields from odoo.exceptions import ValidationError +from odoo.exceptions import UserError +from odoo.tools import float_round + +MESSAGE = ( + "Some of your components are tracked, you have to specify " + "a manufacturing order in order to retrieve the correct components." +) class MrpUnbuild(models.Model): @@ -163,6 +170,95 @@ def action_validate(self): ) % error_totals) return super().action_validate() + def action_unbuild(self): + """ We need to catch raise behavior when tracked products + are unbuild without an original manufacturing order and + go on with another workflow with + _bypass_tracked_product_without_mo() + + Copied from https://github.com/OCA/manufacture/blob/13.0/mrp_unbuild_tracked_raw_material/models/unbuild.py + """ + try: + res = super().action_unbuild() + except UserError as err: + # Unbuild is impossible because of MESSAGE + # original condition of this raise is : + # any(produce_move.has_tracking != 'none' + # and not self.mo_id for produce_move in produce_moves) + if hasattr(err, "name") and err.name == _(MESSAGE): + return self._bypass_tracked_product_without_mo() + # Here is the Odoo native raise + raise err + return res + + def _bypass_tracked_product_without_mo(self): + consume_moves = self.env["stock.move"].search( + [("consume_unbuild_id", "=", self.id)] + ) + produce_moves = self.env["stock.move"].search([("unbuild_id", "=", self.id)]) + finished_moves = consume_moves.filtered(lambda m: m.product_id == self.product_id) + consume_moves -= finished_moves + + """ + Fully copied from https://github.com/odoo/odoo/blob/4cc086d330b73514fbc64a7fcf22d8a7a9f1b691/addons/mrp/models/mrp_unbuild.py#L150-L199 + """ + if any(consume_move.has_tracking != 'none' and not self.mo_id for consume_move in consume_moves): + raise UserError(_('Some of your byproducts are tracked, you have to specify a manufacturing order in order to retrieve the correct byproducts.')) + + for finished_move in finished_moves: + if finished_move.has_tracking != 'none': + self.env['stock.move.line'].create({ + 'move_id': finished_move.id, + 'lot_id': self.lot_id.id, + 'qty_done': finished_move.product_uom_qty, + 'product_id': finished_move.product_id.id, + 'product_uom_id': finished_move.product_uom.id, + 'location_id': finished_move.location_id.id, + 'location_dest_id': finished_move.location_dest_id.id, + }) + else: + finished_move.quantity_done = finished_move.product_uom_qty + + # TODO: Will fail if user do more than one unbuild with lot on the same MO. Need to check what other unbuild has aready took + for move in produce_moves | consume_moves: + if move.has_tracking != 'none': + """ TODO this should be covered in + * _generate_move_from_bom_line() + * and _generate_move_from_bom_quants_total() + * OR _generate_produce_moves() + """ + pass + # original_move = move in produce_moves and self.mo_id.move_raw_ids or self.mo_id.move_finished_ids + # original_move = original_move.filtered(lambda m: m.product_id == move.product_id) + # needed_quantity = move.product_uom_qty + # moves_lines = original_move.mapped('move_line_ids') + # if move in produce_moves and self.lot_id: + # moves_lines = moves_lines.filtered(lambda ml: self.lot_id in ml.lot_produced_ids) + # for move_line in moves_lines: + # # Iterate over all move_lines until we unbuilded the correct quantity. + # taken_quantity = min(needed_quantity, move_line.qty_done) + # if taken_quantity: + # self.env['stock.move.line'].create({ + # 'move_id': move.id, + # 'lot_id': move_line.lot_id.id, + # 'qty_done': taken_quantity, + # 'product_id': move.product_id.id, + # 'product_uom_id': move_line.product_uom_id.id, + # 'location_id': move.location_id.id, + # 'location_dest_id': move.location_dest_id.id, + # }) + # needed_quantity -= taken_quantity + else: + move.quantity_done = float_round(move.product_uom_qty, precision_rounding=move.product_uom.rounding) + + finished_moves._action_done() + consume_moves._action_done() + produce_moves._action_done() + produced_move_line_ids = produce_moves.mapped('move_line_ids').filtered(lambda ml: ml.qty_done > 0) + consume_moves.mapped('move_line_ids').write({'produce_line_ids': [(6, 0, produced_move_line_ids.ids)]}) + + return self.write({'state': 'done'}) + def action_add_myself(self): self.ensure_one() self.bom_quants_total_ids = [( @@ -172,13 +268,37 @@ def action_add_myself(self): )] self.bom_custom_quants_add_myself = True + def _prepare_move_line_with_lot(self, move, bom_quant_id): + return { + "move_id": move.id, + "lot_id": bom_quant_id.lot_id.id, + "qty_done": bom_quant_id.custom_qty, + "product_id": move.product_id.id, + "product_uom_id": move.product_uom.id, + "location_id": move.location_id.id, + "location_dest_id": move.location_dest_id.id, + } + def _generate_move_from_bom_line(self, product, product_uom, quantity, bom_line_id=False, byproduct_id=False): # Default BoM line quantity is overwritten by custom ones + bom_quant_ids = False if self.bom_custom_quants and bom_line_id: bom_line_obj = self.bom_quants_total_ids.filtered(lambda x: x.bom_line_id.id == bom_line_id) quantity = bom_line_obj.total_qty + bom_quant_ids = bom_line_obj.bom_quant_ids + + move = super()._generate_move_from_bom_line(product, product_uom, quantity, bom_line_id=bom_line_id, byproduct_id=byproduct_id) + + if self.bom_custom_quants and bom_line_id and product.tracking != "none" and not all(bom_quant_ids.mapped("has_lot")): + raise UserError(_("You have to specify a lot for %s") % product.name) + + if bom_quant_ids and product.tracking != "none": + for bom_quant_id in bom_quant_ids: + self.env["stock.move.line"].create( + self._prepare_move_line_with_lot(move, bom_quant_id) + ) - return super()._generate_move_from_bom_line(product, product_uom, quantity, bom_line_id=bom_line_id, byproduct_id=byproduct_id) + return move def _generate_produce_moves(self): moves = super()._generate_produce_moves() @@ -194,15 +314,18 @@ def _generate_produce_moves(self): product_id = record.bom_line_id.product_id if product_id.type not in ["product", "consu"]: continue - product_uom_id = record.bom_line_id.product_uom_id - moves += self._generate_move_from_bom_quants_total(product_id, product_uom_id, record.total_qty) + moves += self._generate_move_from_bom_quants_total(record) return moves - def _generate_move_from_bom_quants_total(self, product, product_uom, quantity): + def _generate_move_from_bom_quants_total(self, quant_total): + product = quant_total.bom_line_id.product_id + product_uom = quant_total.bom_line_id.product_uom_id + quantity = quant_total.total_qty + location_id = product.property_stock_production or self.location_id location_dest_id = self.location_dest_id or product.with_context(force_company=self.company_id.id).property_stock_production warehouse = location_dest_id.get_warehouse() - return self.env['stock.move'].create({ + move = self.env['stock.move'].create({ 'name': self.name, 'date': self.create_date, 'product_id': product.id, @@ -216,6 +339,17 @@ def _generate_move_from_bom_quants_total(self, product, product_uom, quantity): 'company_id': self.company_id.id, }) + bom_quant_ids = quant_total.bom_quant_ids + if product.tracking != "none" and not all(bom_quant_ids.mapped("has_lot")): + raise UserError(_("You have to specify a lot for %s") % product.name) + if bom_quant_ids and product.tracking != "none": + for bom_quant_id in bom_quant_ids: + self.env["stock.move.line"].create( + self._prepare_move_line_with_lot(move, bom_quant_id) + ) + + return move + def _update_product_qty_from_bom_totals(self): if not self.env.user.has_group( "mrp_unbuild_bom_cust_qty.group_unbuild_force_exact_qty" diff --git a/mrp_unbuild_bom_cust_qty/models/mrp_unbuild_bom_mixin.py b/mrp_unbuild_bom_cust_qty/models/mrp_unbuild_bom_mixin.py index 7ac0f749..4dc82ab5 100644 --- a/mrp_unbuild_bom_cust_qty/models/mrp_unbuild_bom_mixin.py +++ b/mrp_unbuild_bom_cust_qty/models/mrp_unbuild_bom_mixin.py @@ -28,6 +28,7 @@ class MrpUnbuildBoMMixin(models.AbstractModel): check_company=True, string="Related BoM Line", ) + product_id = fields.Many2one(related="bom_line_id.product_id") product_uom_id = fields.Many2one(related="bom_line_id.product_uom_id") is_product_uom_diff = fields.Boolean( compute="_compute_is_product_uom_diff", diff --git a/mrp_unbuild_bom_cust_qty/models/mrp_unbuild_bom_quants.py b/mrp_unbuild_bom_cust_qty/models/mrp_unbuild_bom_quants.py index 088c3dba..5d3386e0 100644 --- a/mrp_unbuild_bom_cust_qty/models/mrp_unbuild_bom_quants.py +++ b/mrp_unbuild_bom_cust_qty/models/mrp_unbuild_bom_quants.py @@ -15,10 +15,31 @@ class MrpUnbuildBoMQuants(models.Model): digits='Product Unit of Measure', required=True, ) + lot_id = fields.Many2one( + comodel_name="stock.production.lot", + ) + has_lot = fields.Boolean( + compute="_compute_has_lot", + ) + lot_required = fields.Boolean( + compute="_compute_lot_required", + ) departure_date = fields.Datetime( readonly=True ) + @api.depends("lot_id") + def _compute_has_lot(self): + for record in self: + record.has_lot = bool(record.lot_id) + + @api.depends("bom_line_id") + def _compute_lot_required(self): + for record in self: + record.lot_required = ( + record.bom_line_id.product_id.tracking != "none" + ) + @api.constrains("custom_qty") def _check_custom_qty(self): for record in self: diff --git a/mrp_unbuild_bom_cust_qty/models/mrp_unbuild_bom_total.py b/mrp_unbuild_bom_cust_qty/models/mrp_unbuild_bom_total.py index b1548a33..9e0a9625 100644 --- a/mrp_unbuild_bom_cust_qty/models/mrp_unbuild_bom_total.py +++ b/mrp_unbuild_bom_cust_qty/models/mrp_unbuild_bom_total.py @@ -27,6 +27,10 @@ class MrpUnbuildBoMTotals(models.Model): deco_danger = fields.Boolean( compute="_compute_deco_danger" ) + bom_quant_ids = fields.Many2many( + comodel_name="mrp.unbuild.bom.quants", + compute="_compute_bom_quant_ids", + ) @api.depends("unbuild_id.product_qty", # "unbuild_id.bom_id.bom_line_ids.product_qty", @@ -58,9 +62,14 @@ def _compute_deco_danger(self): def _compute_total_qty(self): for record in self: record.total_qty = 0 + # TODO replace with new field record.bom_quant_ids for bom_quant in record.unbuild_id.bom_quants_ids.filtered(lambda x: x.bom_line_id.id == record.bom_line_id.id): record.total_qty += bom_quant.custom_qty + def _compute_bom_quant_ids(self): + for record in self: + record.bom_quant_ids = record.unbuild_id.bom_quants_ids.filtered(lambda x: x.bom_line_id.id == record.bom_line_id.id) + def product_weighing(self): self.ensure_one() mrp_unbuild_bom = self.env.ref( diff --git a/mrp_unbuild_bom_cust_qty/views/mrp_unbuild_bom_quants.xml b/mrp_unbuild_bom_cust_qty/views/mrp_unbuild_bom_quants.xml index 922d5088..196900c0 100644 --- a/mrp_unbuild_bom_cust_qty/views/mrp_unbuild_bom_quants.xml +++ b/mrp_unbuild_bom_cust_qty/views/mrp_unbuild_bom_quants.xml @@ -18,6 +18,29 @@ nolabel="1" style="font-size: 200%;" /> + + + +