nested_o2m.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. from lxml.builder import E
  2. from odoo import fields, models, api, Command
  3. class Product(models.Model):
  4. _name = _description = 'ttu.product'
  5. class Root(models.Model):
  6. _name = _description = 'ttu.root'
  7. product_id = fields.Many2one('ttu.product')
  8. product_qty = fields.Integer()
  9. qty_producing = fields.Integer()
  10. qty_produced = fields.Integer(compute='_get_produced_qty')
  11. move_raw_ids = fields.One2many('ttu.child', 'root_raw_id')
  12. move_finished_ids = fields.One2many('ttu.child', 'root_id')
  13. @api.depends('move_finished_ids.move_line_ids.qty_done')
  14. def _get_produced_qty(self):
  15. for r in self:
  16. r.qty_produced = sum(r.mapped('move_finished_ids.move_line_ids.qty_done'))
  17. @api.onchange('qty_producing')
  18. def _onchange_producing(self):
  19. production_move = self.move_finished_ids.filtered(
  20. lambda move: move.product_id == self.product_id
  21. )
  22. if not production_move:
  23. # Happens when opening the mo?
  24. return
  25. for line in production_move.move_line_ids:
  26. line.qty_done = 0
  27. qty_producing = self.qty_producing - self.qty_produced
  28. vals = production_move._set_quantity_done_prepare_vals(qty_producing)
  29. if vals['to_create']:
  30. for res in vals['to_create']:
  31. production_move.move_line_ids.new(res)
  32. if vals['to_write']:
  33. for move_line, res in vals['to_write']:
  34. move_line.update(res)
  35. for move in (self.move_raw_ids | self.move_finished_ids.filtered(lambda m: m.product_id != self.product_id)):
  36. new_qty = qty_producing * move.unit_factor
  37. for line in move.move_line_ids:
  38. line.qty_done = 0
  39. vals = move._set_quantity_done_prepare_vals(new_qty)
  40. if vals['to_create']:
  41. for res in vals['to_create']:
  42. move.move_line_ids.new(res)
  43. if vals['to_write']:
  44. for move_line, res in vals['to_write']:
  45. move_line.update(res)
  46. def _get_default_form_view(self):
  47. move_subview = E.tree(
  48. {'editable': 'bottom'},
  49. E.field(name='product_id'),
  50. E.field(name='unit_factor'),
  51. E.field(name='quantity_done'),
  52. E.field(
  53. {'name': 'move_line_ids', 'invisible': '1'},
  54. E.tree(
  55. E.field(name='qty_done', invisible='1'),
  56. E.field(name='product_id', invisible='1'),
  57. E.field(name='move_id', invisible='1'),
  58. E.field(name='id', invisible='1'),
  59. )
  60. )
  61. )
  62. t = E.form(
  63. E.field(name='product_id'),
  64. E.field(name='product_qty'),
  65. E.field(name='qty_producing'),
  66. E.field({'name': 'move_raw_ids', 'on_change': '1'}, move_subview),
  67. E.field({'name': 'move_finished_ids', 'on_change': '1'}, move_subview),
  68. )
  69. # deoptimise to ensure we call onchange most of the time, as im the real
  70. # case this is done as a result of the metric fuckton of computes, but
  71. # here the near complete lack of computes causes most of the onchange
  72. # triggers to get disabled
  73. for f in t.iter('field'):
  74. f.set('on_change', '1')
  75. return t
  76. class Child(models.Model):
  77. _name = _description = 'ttu.child'
  78. product_id = fields.Many2one('ttu.product')
  79. unit_factor = fields.Integer(default=1, required=True) # should be computed but we can ignore that
  80. quantity_done = fields.Integer(
  81. compute='_quantity_done_compute',
  82. inverse='_quantity_done_set'
  83. )
  84. root_raw_id = fields.Many2one('ttu.root')
  85. root_id = fields.Many2one('ttu.root')
  86. move_line_ids = fields.One2many('ttu.grandchild', 'move_id')
  87. def _set_quantity_done_prepare_vals(self, qty):
  88. res = {'to_write': [], 'to_create': []}
  89. for ml in self.move_line_ids:
  90. ml_qty = ml.product_uom_qty - ml.qty_done
  91. if ml_qty <= 0:
  92. continue
  93. taken_qty = min(qty, ml_qty)
  94. res['to_write'].append((ml, {'qty_done': ml.qty_done + taken_qty}))
  95. qty -= taken_qty
  96. if qty <= 0:
  97. break
  98. if qty > 0:
  99. res['to_create'].append({
  100. 'move_id': self.id,
  101. 'product_id': self.product_id.id,
  102. 'product_uom_qty': 0,
  103. 'qty_done': qty,
  104. })
  105. return res
  106. @api.depends('move_line_ids.qty_done')
  107. def _quantity_done_compute(self):
  108. for move in self:
  109. move.quantity_done = sum(move.mapped('move_line_ids.qty_done'))
  110. def _quantity_done_set(self):
  111. quantity_done = self[0].quantity_done # any call to create will invalidate `move.quantity_done`
  112. for move in self:
  113. move_lines = move.move_line_ids
  114. if not move_lines:
  115. if quantity_done:
  116. # do not impact reservation here
  117. move_line = self.env['ttu.grandchild'].create({
  118. 'move_id': move.id,
  119. 'product_id': move.product_id.id,
  120. 'product_uom_qty': 0,
  121. 'qty_done': quantity_done,
  122. })
  123. move.write({'move_line_ids': [Command.link(move_line.id)]})
  124. elif len(move_lines) == 1:
  125. move_lines[0].qty_done = quantity_done
  126. else:
  127. # Bypass the error if we're trying to write the same value.
  128. ml_quantity_done = sum(l.qty_done for l in move_lines)
  129. assert quantity_done == ml_quantity_done, "Cannot set the done quantity from this stock move, work directly with the move lines."
  130. class Grandchild(models.Model):
  131. _name = _description = 'ttu.grandchild'
  132. product_id = fields.Many2one('ttu.product')
  133. product_uom_qty = fields.Integer()
  134. qty_done = fields.Integer()
  135. move_id = fields.Many2one('ttu.child')