from lxml.builder import E from odoo import fields, models, api, Command class Product(models.Model): _name = _description = 'ttu.product' class Root(models.Model): _name = _description = 'ttu.root' product_id = fields.Many2one('ttu.product') product_qty = fields.Integer() qty_producing = fields.Integer() qty_produced = fields.Integer(compute='_get_produced_qty') move_raw_ids = fields.One2many('ttu.child', 'root_raw_id') move_finished_ids = fields.One2many('ttu.child', 'root_id') @api.depends('move_finished_ids.move_line_ids.qty_done') def _get_produced_qty(self): for r in self: r.qty_produced = sum(r.mapped('move_finished_ids.move_line_ids.qty_done')) @api.onchange('qty_producing') def _onchange_producing(self): production_move = self.move_finished_ids.filtered( lambda move: move.product_id == self.product_id ) if not production_move: # Happens when opening the mo? return for line in production_move.move_line_ids: line.qty_done = 0 qty_producing = self.qty_producing - self.qty_produced vals = production_move._set_quantity_done_prepare_vals(qty_producing) if vals['to_create']: for res in vals['to_create']: production_move.move_line_ids.new(res) if vals['to_write']: for move_line, res in vals['to_write']: move_line.update(res) for move in (self.move_raw_ids | self.move_finished_ids.filtered(lambda m: m.product_id != self.product_id)): new_qty = qty_producing * move.unit_factor for line in move.move_line_ids: line.qty_done = 0 vals = move._set_quantity_done_prepare_vals(new_qty) if vals['to_create']: for res in vals['to_create']: move.move_line_ids.new(res) if vals['to_write']: for move_line, res in vals['to_write']: move_line.update(res) def _get_default_form_view(self): move_subview = E.list( {'editable': 'bottom'}, E.field(name='product_id'), E.field(name='unit_factor'), E.field(name='quantity_done'), E.field( {'name': 'move_line_ids', 'column_invisible': '1'}, E.list( E.field(name='qty_done', invisible='1'), E.field(name='product_id', invisible='1'), E.field(name='move_id', invisible='1'), E.field(name='id', invisible='1'), ) ) ) t = E.form( E.field(name='product_id'), E.field(name='product_qty'), E.field(name='qty_producing'), E.field({'name': 'move_raw_ids', 'on_change': '1'}, move_subview), E.field({'name': 'move_finished_ids', 'on_change': '1'}, move_subview), ) # deoptimise to ensure we call onchange most of the time, as im the real # case this is done as a result of the metric fuckton of computes, but # here the near complete lack of computes causes most of the onchange # triggers to get disabled for f in t.iter('field'): f.set('on_change', '1') return t class Child(models.Model): _name = _description = 'ttu.child' product_id = fields.Many2one('ttu.product') unit_factor = fields.Integer(default=1, required=True) # should be computed but we can ignore that quantity_done = fields.Integer( compute='_quantity_done_compute', inverse='_quantity_done_set' ) root_raw_id = fields.Many2one('ttu.root') root_id = fields.Many2one('ttu.root') move_line_ids = fields.One2many('ttu.grandchild', 'move_id') def _set_quantity_done_prepare_vals(self, qty): res = {'to_write': [], 'to_create': []} for ml in self.move_line_ids: ml_qty = ml.product_uom_qty - ml.qty_done if ml_qty <= 0: continue taken_qty = min(qty, ml_qty) res['to_write'].append((ml, {'qty_done': ml.qty_done + taken_qty})) qty -= taken_qty if qty <= 0: break if qty > 0: res['to_create'].append({ 'move_id': self.id, 'product_id': self.product_id.id, 'product_uom_qty': 0, 'qty_done': qty, }) return res @api.depends('move_line_ids.qty_done') def _quantity_done_compute(self): for move in self: move.quantity_done = sum(move.mapped('move_line_ids.qty_done')) def _quantity_done_set(self): quantity_done = self[0].quantity_done # any call to create will invalidate `move.quantity_done` for move in self: move_lines = move.move_line_ids if not move_lines: if quantity_done: # do not impact reservation here move_line = self.env['ttu.grandchild'].create({ 'move_id': move.id, 'product_id': move.product_id.id, 'product_uom_qty': 0, 'qty_done': quantity_done, }) move.write({'move_line_ids': [Command.link(move_line.id)]}) elif len(move_lines) == 1: move_lines[0].qty_done = quantity_done else: # Bypass the error if we're trying to write the same value. ml_quantity_done = sum(l.qty_done for l in move_lines) assert quantity_done == ml_quantity_done, "Cannot set the done quantity from this stock move, work directly with the move lines." class Grandchild(models.Model): _name = _description = 'ttu.grandchild' product_id = fields.Many2one('ttu.product') product_uom_qty = fields.Integer() qty_done = fields.Integer() move_id = fields.Many2one('ttu.child')