def _populate_factories(self): # Take a subset (50%) of bom to have some of then without any operation random = populate.Random('byproduct_subset_bom') boms_ids = self.env.registry.populated_models['mrp.bom'] boms_ids = random.sample(boms_ids, int(len(boms_ids) * 0.5)) boms = self.env['mrp.bom'].search( [('id', 'in', self.env.registry.populated_models['mrp.bom'])], order='sequence, product_id, id') product_manu_ids = OrderedSet() for bom in boms: if bom.product_id: product_manu_ids.add(bom.product_id.id) else: for product_id in bom.product_tmpl_id.product_variant_ids: product_manu_ids.add(product_id.id) product_manu = self.env['product.product'].browse(product_manu_ids) # product_no_manu is products which don't have any bom (leaves in the BoM trees) product_no_manu = self.env['product.product'].browse( self.env.registry.populated_models['product.product'] ) - product_manu product_no_manu_ids = product_no_manu.ids def get_product_uom_id(values, counter, random): return self.env['product.product'].browse( values['product_id']).uom_id.id return [ ('bom_id', populate.iterate(boms_ids)), ('product_id', populate.randomize(product_no_manu_ids)), ('product_uom_id', populate.compute(get_product_uom_id)), ('product_qty', populate.randint(1, 10)), ]
def _populate_factories(self): # TODO: tree of product by company to be more closer to the reality boms = self.env['mrp.bom'].search( [('id', 'in', self.env.registry.populated_models['mrp.bom'])], order='sequence, product_id, id') product_manu_ids = OrderedSet() for bom in boms: if bom.product_id: product_manu_ids.add(bom.product_id.id) else: for product_id in bom.product_tmpl_id.product_variant_ids: product_manu_ids.add(product_id.id) product_manu_ids = list(product_manu_ids) product_manu = self.env['product.product'].browse(product_manu_ids) # product_no_manu is products which don't have any bom (leaves in the BoM trees) product_no_manu = self.env['product.product'].browse( self.env.registry.populated_models['product.product'] ) - product_manu product_no_manu_ids = product_no_manu.ids def get_product_id(values, counter, random): bom = self.env['mrp.bom'].browse(values['bom_id']) last_product_bom = bom.product_id if bom.product_id else bom.product_tmpl_id.product_variant_ids[ -1] # TODO: index in list is in O(n) can be avoid by a cache dict (if performance issue) index_prod = product_manu_ids.index(last_product_bom.id) # Always choose a product futher in the recordset `product_manu` to avoid any loops # Or used a product in the `product_no_manu` sparsity = 0.4 # Increase the sparsity will decrease the density of the BoM trees => smaller Tree len_remaining_manu = len(product_manu_ids) - index_prod - 1 len_no_manu = len(product_no_manu_ids) threshold = len_remaining_manu / (len_remaining_manu + sparsity * len_no_manu) if random.random() <= threshold: # TODO: avoid copy the list (if performance issue) return random.choice(product_manu_ids[index_prod + 1:]) else: return random.choice(product_no_manu_ids) def get_product_uom_id(values, counter, random): return self.env['product.product'].browse( values['product_id']).uom_id.id return [ ('bom_id', populate.iterate(boms.ids)), ('sequence', populate.randint(1, 1000)), ('product_id', populate.compute(get_product_id)), ('product_uom_id', populate.compute(get_product_uom_id)), ('product_qty', populate.randint(1, 10)), ]
def _get_report_values(self, docids, data=None): orders = self.env["sale.order"].browse(data["order_ids"]).sorted( key=lambda o: (o.canteen_order_date, o.student_id.name)) grade_levels = OrderedSet() for order in orders: grade_levels.add(order.student_id.grade_level_id) dates = OrderedSet(orders.mapped("canteen_order_date")) return { "orders": orders, "grade_levels": grade_levels, "dates": dates, "self": self, }
def descendants(self, model_names, *kinds): """ Return the models corresponding to ``model_names`` and all those that inherit/inherits from them. """ assert all(kind in ('_inherit', '_inherits') for kind in kinds) funcs = [attrgetter(kind + '_children') for kind in kinds] models = OrderedSet() queue = deque(model_names) while queue: model = self[queue.popleft()] models.add(model._name) for func in funcs: queue.extend(func(model)) return models
def descendants(self, model_names, *kinds): """ Return the models corresponding to ``model_names`` and all those that inherit/inherits from them. """ assert all(kind in ('_inherit', '_inherits') for kind in kinds) funcs = [attrgetter(kind + '_children') for kind in kinds] models = OrderedSet() queue = deque(model_names) while queue: model = self[queue.popleft()] models.add(model._name) for func in funcs: queue.extend(func(model)) return models
def _get_in_move_lines(self): """ Returns the `stock.move.line` records of `self` considered as incoming. It is done thanks to the `_should_be_valued` method of their source and destionation location as well as their owner. :returns: a subset of `self` containing the incoming records :rtype: recordset """ self.ensure_one() res = OrderedSet() for move_line in self.move_line_ids: if move_line.owner_id and move_line.owner_id != move_line.company_id.partner_id: continue if not move_line.location_id._should_be_valued() and move_line.location_dest_id._should_be_valued(): res.add(move_line.id) return self.env['stock.move.line'].browse(res)
def action_explode(self): """ Explodes pickings """ # in order to explode a move, we must have a picking_type_id on that move because otherwise the move # won't be assigned to a picking and it would be weird to explode a move into several if they aren't # all grouped in the same picking. moves_ids_to_return = OrderedSet() moves_ids_to_unlink = OrderedSet() phantom_moves_vals_list = [] for move in self: if not move.picking_type_id or (move.production_id and move.production_id.product_id == move.product_id): moves_ids_to_return.add(move.id) continue bom = self.env['mrp.bom'].sudo()._bom_find(product=move.product_id, company_id=move.company_id.id, bom_type='phantom') if not bom: moves_ids_to_return.add(move.id) continue if move.picking_id.immediate_transfer: factor = move.product_uom._compute_quantity(move.quantity_done, bom.product_uom_id) / bom.product_qty else: factor = move.product_uom._compute_quantity(move.product_uom_qty, bom.product_uom_id) / bom.product_qty boms, lines = bom.sudo().explode(move.product_id, factor, picking_type=bom.picking_type_id) for bom_line, line_data in lines: if move.picking_id.immediate_transfer: phantom_moves_vals_list += move._generate_move_phantom(bom_line, 0, line_data['qty']) else: phantom_moves_vals_list += move._generate_move_phantom(bom_line, line_data['qty'], 0) # delete the move with original product which is not relevant anymore moves_ids_to_unlink.add(move.id) self.env['stock.move'].browse(moves_ids_to_unlink).sudo().unlink() if phantom_moves_vals_list: phantom_moves = self.env['stock.move'].create(phantom_moves_vals_list) phantom_moves._adjust_procure_method() moves_ids_to_return |= phantom_moves.action_explode().ids return self.env['stock.move'].browse(moves_ids_to_return)
def _action_done(self): """ This method is called during a move's `action_done`. It'll actually move a quant from the source location to the destination location, and unreserve if needed in the source location. This method is intended to be called on all the move lines of a move. This method is not intended to be called when editing a `done` move (that's what the override of `write` here is done. """ Quant = self.env['stock.quant'] # First, we loop over all the move lines to do a preliminary check: `qty_done` should not # be negative and, according to the presence of a picking type or a linked inventory # adjustment, enforce some rules on the `lot_id` field. If `qty_done` is null, we unlink # the line. It is mandatory in order to free the reservation and correctly apply # `action_done` on the next move lines. ml_ids_tracked_without_lot = OrderedSet() ml_ids_to_delete = OrderedSet() ml_ids_to_create_lot = OrderedSet() for ml in self: # Check here if `ml.qty_done` respects the rounding of `ml.product_uom_id`. uom_qty = float_round(ml.qty_done, precision_rounding=ml.product_uom_id.rounding, rounding_method='HALF-UP') precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure') qty_done = float_round(ml.qty_done, precision_digits=precision_digits, rounding_method='HALF-UP') if float_compare(uom_qty, qty_done, precision_digits=precision_digits) != 0: raise UserError(_('The quantity done for the product "%s" doesn\'t respect the rounding precision \ defined on the unit of measure "%s". Please change the quantity done or the \ rounding precision of your unit of measure.') % (ml.product_id.display_name, ml.product_uom_id.name)) qty_done_float_compared = float_compare(ml.qty_done, 0, precision_rounding=ml.product_uom_id.rounding) if qty_done_float_compared > 0: if ml.product_id.tracking != 'none': picking_type_id = ml.move_id.picking_type_id if picking_type_id: if picking_type_id.use_create_lots: # If a picking type is linked, we may have to create a production lot on # the fly before assigning it to the move line if the user checked both # `use_create_lots` and `use_existing_lots`. if ml.lot_name: if ml.product_id.tracking == 'lot' and not ml.lot_id: lot = self.env['stock.production.lot'].search([ ('company_id', '=', ml.company_id.id), ('product_id', '=', ml.product_id.id), ('name', '=', ml.lot_name), ], limit=1) if lot: ml.lot_id = lot.id else: ml_ids_to_create_lot.add(ml.id) else: ml_ids_to_create_lot.add(ml.id) elif not picking_type_id.use_create_lots and not picking_type_id.use_existing_lots: # If the user disabled both `use_create_lots` and `use_existing_lots` # checkboxes on the picking type, he's allowed to enter tracked # products without a `lot_id`. continue elif ml.is_inventory: # If an inventory adjustment is linked, the user is allowed to enter # tracked products without a `lot_id`. continue if not ml.lot_id and ml.id not in ml_ids_to_create_lot: ml_ids_tracked_without_lot.add(ml.id) elif qty_done_float_compared < 0: raise UserError(_('No negative quantities allowed')) elif not ml.is_inventory: ml_ids_to_delete.add(ml.id) if ml_ids_tracked_without_lot: mls_tracked_without_lot = self.env['stock.move.line'].browse(ml_ids_tracked_without_lot) raise UserError(_('You need to supply a Lot/Serial Number for product: \n - ') + '\n - '.join(mls_tracked_without_lot.mapped('product_id.display_name'))) ml_to_create_lot = self.env['stock.move.line'].browse(ml_ids_to_create_lot) ml_to_create_lot._create_and_assign_production_lot() mls_to_delete = self.env['stock.move.line'].browse(ml_ids_to_delete) mls_to_delete.unlink() mls_todo = (self - mls_to_delete) mls_todo._check_company() # Now, we can actually move the quant. ml_ids_to_ignore = OrderedSet() for ml in mls_todo: if ml.product_id.type == 'product': rounding = ml.product_uom_id.rounding # if this move line is force assigned, unreserve elsewhere if needed if not ml._should_bypass_reservation(ml.location_id) and float_compare(ml.qty_done, ml.product_uom_qty, precision_rounding=rounding) > 0: qty_done_product_uom = ml.product_uom_id._compute_quantity(ml.qty_done, ml.product_id.uom_id, rounding_method='HALF-UP') extra_qty = qty_done_product_uom - ml.product_qty ml._free_reservation(ml.product_id, ml.location_id, extra_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, ml_ids_to_ignore=ml_ids_to_ignore) # unreserve what's been reserved if not ml._should_bypass_reservation(ml.location_id) and ml.product_id.type == 'product' and ml.product_qty: try: Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) except UserError: Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) # move what's been actually done quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP') available_qty, in_date = Quant._update_available_quantity(ml.product_id, ml.location_id, -quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) if available_qty < 0 and ml.lot_id: # see if we can compensate the negative quants with some untracked quants untracked_qty = Quant._get_available_quantity(ml.product_id, ml.location_id, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) if untracked_qty: taken_from_untracked_qty = min(untracked_qty, abs(quantity)) Quant._update_available_quantity(ml.product_id, ml.location_id, -taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id) Quant._update_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) Quant._update_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id, in_date=in_date) ml_ids_to_ignore.add(ml.id) # Reset the reserved quantity as we just moved it to the destination location. mls_todo.with_context(bypass_reservation_update=True).write({ 'product_uom_qty': 0.00, 'date': fields.Datetime.now(), })