def _check_package(self): default_uom = self.product_id.uom_id pack = self.product_packaging qty = self.product_uom_qty q = default_uom._compute_quantity(pack.qty, self.product_uom) # We do not use the modulo operator to check if qty is a mltiple of q. Indeed the quantity # per package might be a float, leading to incorrect results. For example: # 8 % 1.6 = 1.5999999999999996 # 5.4 % 1.8 = 2.220446049250313e-16 if (qty and q and float_compare(qty / q, float_round(qty / q, precision_rounding=1.0), precision_rounding=0.001) != 0): newqty = qty - (qty % q) + q return { 'warning': { 'title': _('Warning'), 'message': _("This product is packaged by %.2f %s. You should sell %.2f %s." ) % (pack.qty, default_uom.name, newqty, self.product_uom.name), }, } return {}
def _get_price(self, pricelist, product, qty): sale_price_digits = self.env['decimal.precision'].precision_get( 'Product Price') price = pricelist.get_product_price(product, qty, False) if not price: price = product.list_price return float_round(price, precision_digits=sale_price_digits)
def _get_rounded_amount(self, amount): if self.config_id.cash_rounding: amount = float_round( amount, precision_rounding=self.config_id.rounding_method.rounding, rounding_method=self.config_id.rounding_method.rounding_method) return super(PosOrder, self)._get_rounded_amount(amount)
def _get_price(self, bom, factor, product): price = 0 if bom.routing_id: # routing are defined on a BoM and don't have a concept of quantity. # It means that the operation time are defined for the quantity on # the BoM (the user produces a batch of products). E.g the user # product a batch of 10 units with a 5 minutes operation, the time # will be the 5 for a quantity between 1-10, then doubled for # 11-20,... operation_cycle = float_round(factor, precision_rounding=1, rounding_method='UP') operations = self._get_operation_line(bom.routing_id, operation_cycle, 0) price += sum([op['total'] for op in operations]) for line in bom.bom_line_ids: if line._skip_bom_line(product): continue if line.child_bom_id: qty = line.product_uom_id._compute_quantity( line.product_qty * factor, line.child_bom_id.product_uom_id ) / line.child_bom_id.product_qty sub_price = self._get_price(line.child_bom_id, qty, line.product_id) price += sub_price else: prod_qty = line.product_qty * factor company = bom.company_id or self.env.company not_rounded_price = line.product_id.uom_id._compute_price( line.product_id.with_context( force_comany=company.id).standard_price, line.product_uom_id) * prod_qty price += company.currency_id.round(not_rounded_price) return price
def round(self, amount): """Compute the rounding on the amount passed as parameter. :param amount: the amount to round :return: the rounded amount depending the rounding value and the rounding method """ return float_round(amount, precision_rounding=self.rounding, rounding_method=self.rounding_method)
def _adyen_convert_amount(self, amount, currency): """ Adyen requires the amount to be multiplied by 10^k, where k depends on the currency code. """ k = CURRENCY_CODE_MAPS.get(currency.name, 2) paymentAmount = int(tools.float_round(amount, k) * (10**k)) return paymentAmount
def try_round(amount, expected, digits=3, method='HALF-UP'): value = float_round(amount, precision_digits=digits, rounding_method=method) result = float_repr(value, precision_digits=digits) self.assertEqual( result, expected, 'Rounding error: got %s, expected %s' % (result, expected))
def get_wallet_balance(self, user, include_config=True): result = float_round(sum( move['amount'] for move in self.env['lunch.cashmove.report'].search_read([( 'user_id', '=', user.id)], ['amount'])), precision_digits=2) if include_config: result += user.company_id.lunch_minimum_threshold return result
def infos(self, user_id=None): self._check_user_impersonification(user_id) user = request.env['res.users'].browse( user_id) if user_id else request.env.user infos = self._make_infos(user, order=False) lines = self._get_current_lines(user.id) if lines: lines = [ { 'id': line.id, 'product': (line.product_id.id, line.product_id.name, float_repr(float_round(line.product_id.price, 2), 2)), 'toppings': [(topping.name, float_repr(float_round(topping.price, 2), 2)) for topping in line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3], 'quantity': line.quantity, 'price': line.price, 'state': line.state, # Only used for _get_state 'note': line.note } for line in lines ] raw_state, state = self._get_state(lines) infos.update({ 'total': float_repr( float_round(sum(line['price'] for line in lines), 2), 2), 'raw_state': raw_state, 'state': state, 'lines': lines, }) return infos
def float_to_time(hours, moment='am', tz=None): """ Convert a number of hours into a time object. """ if hours == 12.0 and moment == 'pm': return time.max fractional, integral = math.modf(hours) if moment == 'pm': integral += 12 res = time(int(integral), int(float_round(60 * fractional, precision_digits=0)), 0) if tz: res = res.replace(tzinfo=pytz.timezone(tz)) return res
def test_rounding_invalid(self): """ verify that invalid parameters are forbidden """ with self.assertRaises(AssertionError): float_is_zero(0.01, precision_digits=3, precision_rounding=0.01) with self.assertRaises(AssertionError): float_is_zero(0.0, precision_rounding=0.0) with self.assertRaises(AssertionError): float_is_zero(0.0, precision_rounding=-0.1) with self.assertRaises(AssertionError): float_compare(0.01, 0.02, precision_digits=3, precision_rounding=0.01) with self.assertRaises(AssertionError): float_compare(1.0, 1.0, precision_rounding=0.0) with self.assertRaises(AssertionError): float_compare(1.0, 1.0, precision_rounding=-0.1) with self.assertRaises(AssertionError): float_round(0.01, precision_digits=3, precision_rounding=0.01) with self.assertRaises(AssertionError): float_round(1.25, precision_rounding=0.0) with self.assertRaises(AssertionError): float_round(1.25, precision_rounding=-0.1)
def get_operations(self, bom_id=False, qty=0, level=0): bom = self.env['mrp.bom'].browse(bom_id) lines = self._get_operation_line( bom.routing_id, float_round(qty / bom.product_qty, precision_rounding=1, rounding_method='UP'), level) values = { 'bom_id': bom_id, 'currency': self.env.company.currency_id, 'operations': lines, } return self.env.ref('mrp.report_mrp_operation_line').render( {'data': values})
def test_move_picking_with_package(self): """ 355.4 rounded with 0.001 precision is 355.40000000000003. check that nonetheless, moving a picking is accepted """ self.assertEqual(self.productA.uom_id.rounding, 0.001) self.assertEqual( float_round(355.4, precision_rounding=self.productA.uom_id.rounding), 355.40000000000003, ) location_dict = { 'location_id': self.stock_location.id, } quant = self.env['stock.quant'].create({ **location_dict, **{'product_id': self.productA.id, 'quantity': 355.4}, # important number }) package = self.env['stock.quant.package'].create({ **location_dict, **{'quant_ids': [(6, 0, [quant.id])]}, }) location_dict.update({ 'state': 'draft', 'location_dest_id': self.ship_location.id, }) move = self.env['stock.move'].create({ **location_dict, **{ 'name': "XXX", 'product_id': self.productA.id, 'product_uom': self.productA.uom_id.id, 'product_uom_qty': 355.40000000000003, # other number }}) picking = self.env['stock.picking'].create({ **location_dict, **{ 'picking_type_id': self.warehouse.in_type_id.id, 'move_lines': [(6, 0, [move.id])], }}) picking.action_confirm() picking.action_assign() move.quantity_done = move.reserved_availability picking.action_done()
def _compute_quantity(self, qty, to_unit, round=True, rounding_method='UP', raise_if_failure=True): """ Convert the given quantity from the current UoM `self` into a given one :param qty: the quantity to convert :param to_unit: the destination UoM record (uom.uom) :param raise_if_failure: only if the conversion is not possible - if true, raise an exception if the conversion is not possible (different UoM category), - otherwise, return the initial quantity """ if not self: return qty self.ensure_one() if self.category_id.id != to_unit.category_id.id: if raise_if_failure: raise UserError(_('The unit of measure %s defined on the order line doesn\'t belong to the same category than the unit of measure %s defined on the product. Please correct the unit of measure defined on the order line or on the product, they should belong to the same category.') % (self.name, to_unit.name)) else: return qty amount = qty / self.factor if to_unit: amount = amount * to_unit.factor if round: amount = tools.float_round(amount, precision_rounding=to_unit.rounding, rounding_method=rounding_method) return amount
def _get_operation_line(self, routing, qty, level): operations = [] total = 0.0 for operation in routing.operation_ids: operation_cycle = float_round(qty / operation.workcenter_id.capacity, precision_rounding=1, rounding_method='UP') duration_expected = operation_cycle * operation.time_cycle + operation.workcenter_id.time_stop + operation.workcenter_id.time_start total = ((duration_expected / 60.0) * operation.workcenter_id.costs_hour) operations.append({ 'level': level or 0, 'operation': operation, 'name': operation.name + ' - ' + operation.workcenter_id.name, 'duration_expected': duration_expected, 'total': self.env.company.currency_id.round(total), }) return operations
def _update_finished_move(self): """ Update the finished move & move lines in order to set the finished product lot on it as well as the produced quantity. This method get the information either from the last workorder or from the Produce wizard.""" production_move = self.production_id.move_finished_ids.filtered( lambda move: move.product_id == self.product_id and move.state not in ('done', 'cancel')) if production_move and production_move.product_id.tracking != 'none': if not self.finished_lot_id: raise UserError( _('You need to provide a lot for the finished product.')) move_line = production_move.move_line_ids.filtered( lambda line: line.lot_id.id == self.finished_lot_id.id) if move_line: if self.product_id.tracking == 'serial': raise UserError( _('You cannot produce the same serial number twice.')) move_line.product_uom_qty += self.qty_producing move_line.qty_done += self.qty_producing else: location_dest_id = production_move.location_dest_id._get_putaway_strategy( self.product_id).id or production_move.location_dest_id.id move_line.create({ 'move_id': production_move.id, 'product_id': production_move.product_id.id, 'lot_id': self.finished_lot_id.id, 'product_uom_qty': self.qty_producing, 'product_uom_id': self.product_uom_id.id, 'qty_done': self.qty_producing, 'location_id': production_move.location_id.id, 'location_dest_id': location_dest_id, }) else: rounding = production_move.product_uom.rounding production_move._set_quantity_done( float_round(self.qty_producing, precision_rounding=rounding))
def _compute_qty_remaining(self): for wo in self: wo.qty_remaining = float_round( wo.qty_production - wo.qty_produced, precision_rounding=wo.production_id.product_uom_id.rounding)
def compute_landed_cost(self): AdjustementLines = self.env['stock.valuation.adjustment.lines'] AdjustementLines.search([('cost_id', 'in', self.ids)]).unlink() digits = self.env['decimal.precision'].precision_get('Product Price') towrite_dict = {} for cost in self.filtered(lambda cost: cost.picking_ids): total_qty = 0.0 total_cost = 0.0 total_weight = 0.0 total_volume = 0.0 total_line = 0.0 all_val_line_values = cost.get_valuation_lines() for val_line_values in all_val_line_values: for cost_line in cost.cost_lines: val_line_values.update({ 'cost_id': cost.id, 'cost_line_id': cost_line.id }) self.env['stock.valuation.adjustment.lines'].create( val_line_values) total_qty += val_line_values.get('quantity', 0.0) total_weight += val_line_values.get('weight', 0.0) total_volume += val_line_values.get('volume', 0.0) former_cost = val_line_values.get('former_cost', 0.0) # round this because former_cost on the valuation lines is also rounded total_cost += tools.float_round( former_cost, precision_digits=digits) if digits else former_cost total_line += 1 for line in cost.cost_lines: value_split = 0.0 for valuation in cost.valuation_adjustment_lines: value = 0.0 if valuation.cost_line_id and valuation.cost_line_id.id == line.id: if line.split_method == 'by_quantity' and total_qty: per_unit = (line.price_unit / total_qty) value = valuation.quantity * per_unit elif line.split_method == 'by_weight' and total_weight: per_unit = (line.price_unit / total_weight) value = valuation.weight * per_unit elif line.split_method == 'by_volume' and total_volume: per_unit = (line.price_unit / total_volume) value = valuation.volume * per_unit elif line.split_method == 'equal': value = (line.price_unit / total_line) elif line.split_method == 'by_current_cost_price' and total_cost: per_unit = (line.price_unit / total_cost) value = valuation.former_cost * per_unit else: value = (line.price_unit / total_line) if digits: value = tools.float_round(value, precision_digits=digits, rounding_method='UP') fnc = min if line.price_unit > 0 else max value = fnc(value, line.price_unit - value_split) value_split += value if valuation.id not in towrite_dict: towrite_dict[valuation.id] = value else: towrite_dict[valuation.id] += value for key, value in towrite_dict.items(): AdjustementLines.browse(key).write( {'additional_landed_cost': value}) return True
def time_to_float(t): return float_round(t.hour + t.minute / 60 + t.second / 3600, precision_digits=2)
def test_20_bom_report(self): """ Simulate a crumble receipt with mrp and open the bom structure report and check that data insde are correct. """ uom_kg = self.env.ref('uom.product_uom_kgm') uom_litre = self.env.ref('uom.product_uom_litre') crumble = self.env['product.product'].create({ 'name': 'Crumble', 'type': 'product', 'uom_id': uom_kg.id, 'uom_po_id': uom_kg.id, }) butter = self.env['product.product'].create({ 'name': 'Butter', 'type': 'product', 'uom_id': uom_kg.id, 'uom_po_id': uom_kg.id, 'standard_price': 7.01 }) biscuit = self.env['product.product'].create({ 'name': 'Biscuit', 'type': 'product', 'uom_id': uom_kg.id, 'uom_po_id': uom_kg.id, 'standard_price': 1.5 }) bom_form_crumble = Form(self.env['mrp.bom']) bom_form_crumble.product_tmpl_id = crumble.product_tmpl_id bom_form_crumble.product_qty = 11 bom_form_crumble.product_uom_id = uom_kg bom_crumble = bom_form_crumble.save() with Form(bom_crumble) as bom: with bom.bom_line_ids.new() as line: line.product_id = butter line.product_uom_id = uom_kg line.product_qty = 5 with bom.bom_line_ids.new() as line: line.product_id = biscuit line.product_uom_id = uom_kg line.product_qty = 6 workcenter = self.env['mrp.workcenter'].create({ 'costs_hour': 10, 'name': 'Deserts Table' }) routing_form = Form(self.env['mrp.routing']) routing_form.name = "Crumble process" routing_crumble = routing_form.save() with Form(routing_crumble) as routing: with routing.operation_ids.new() as operation: operation.workcenter_id = workcenter operation.name = 'Prepare biscuits' operation.time_cycle_manual = 5 with routing.operation_ids.new() as operation: operation.workcenter_id = workcenter operation.name = 'Prepare butter' operation.time_cycle_manual = 3 with routing.operation_ids.new() as operation: operation.workcenter_id = workcenter operation.name = 'Mix manually' operation.time_cycle_manual = 5 bom_crumble.routing_id = routing_crumble.id # TEST BOM STRUCTURE VALUE WITH BOM QUANTITY report_values = self.env[ 'report.mrp.report_bom_structure']._get_report_data( bom_id=bom_crumble.id, searchQty=11, searchVariant=False) # 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes self.assertEqual( report_values['lines']['operations_time'], 13.0, 'Operation time should be the same for 1 unit or for the batch') # Operation cost is the sum of operation line. operation_cost = float_round(5 / 60 * 10, precision_digits=2) * 2 + float_round( 3 / 60 * 10, precision_digits=2) self.assertEqual( float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0, '13 minute for 10$/hours -> 2.16') for component_line in report_values['lines']['components']: # standard price * bom line quantity * current quantity / bom finished product quantity if component_line['prod_id'] == butter.id: # 5 kg of butter at 7.01$ for 11kg of crumble -> 35.05$ self.assertEqual( float_compare(component_line['total'], (7.01 * 5), precision_digits=2), 0) if component_line['prod_id'] == biscuit.id: # 6 kg of biscuits at 1.50$ for 11kg of crumble -> 9$ self.assertEqual( float_compare(component_line['total'], (1.5 * 6), precision_digits=2), 0) # total price = 35.05 + 9 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 46,21 self.assertEqual( float_compare(report_values['lines']['total'], 46.21, precision_digits=2), 0, 'Product Bom Price is not correct') self.assertEqual( float_compare(report_values['lines']['total'] / 11.0, 4.20, precision_digits=2), 0, 'Product Unit Bom Price is not correct') # TEST BOM STRUCTURE VALUE BY UNIT report_values = self.env[ 'report.mrp.report_bom_structure']._get_report_data( bom_id=bom_crumble.id, searchQty=1, searchVariant=False) # 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes self.assertEqual( report_values['lines']['operations_time'], 13.0, 'Operation time should be the same for 1 unit or for the batch') # Operation cost is the sum of operation line. operation_cost = float_round(5 / 60 * 10, precision_digits=2) * 2 + float_round( 3 / 60 * 10, precision_digits=2) self.assertEqual( float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0, '13 minute for 10$/hours -> 2.16') for component_line in report_values['lines']['components']: # standard price * bom line quantity * current quantity / bom finished product quantity if component_line['prod_id'] == butter.id: # 5 kg of butter at 7.01$ for 11kg of crumble -> / 11 for price per unit (3.19) self.assertEqual( float_compare(component_line['total'], (7.01 * 5) * (1 / 11), precision_digits=2), 0) if component_line['prod_id'] == biscuit.id: # 6 kg of biscuits at 1.50$ for 11kg of crumble -> / 11 for price per unit (0.82) self.assertEqual( float_compare(component_line['total'], (1.5 * 6) * (1 / 11), precision_digits=2), 0) # total price = 3.19 + 0.82 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 6,17 self.assertEqual( float_compare(report_values['lines']['total'], 6.17, precision_digits=2), 0, 'Product Unit Bom Price is not correct') # TEST OPERATION COST WHEN PRODUCED QTY > BOM QUANTITY report_values_12 = self.env[ 'report.mrp.report_bom_structure']._get_report_data( bom_id=bom_crumble.id, searchQty=12, searchVariant=False) report_values_22 = self.env[ 'report.mrp.report_bom_structure']._get_report_data( bom_id=bom_crumble.id, searchQty=22, searchVariant=False) operation_cost = float_round(10 / 60 * 10, precision_digits=2) * 2 + float_round( 6 / 60 * 10, precision_digits=2) # Both needs 2 operation cycle self.assertEqual(report_values_12['lines']['operations_cost'], report_values_22['lines']['operations_cost']) self.assertEqual(report_values_22['lines']['operations_cost'], operation_cost) report_values_23 = self.env[ 'report.mrp.report_bom_structure']._get_report_data( bom_id=bom_crumble.id, searchQty=23, searchVariant=False) operation_cost = float_round(15 / 60 * 10, precision_digits=2) * 2 + float_round( 9 / 60 * 10, precision_digits=2) self.assertEqual(report_values_23['lines']['operations_cost'], operation_cost) # Create a more complex BoM with a sub product cheese_cake = self.env['product.product'].create({ 'name': 'Cheese Cake 300g', 'type': 'product', }) cream = self.env['product.product'].create({ 'name': 'cream', 'type': 'product', 'uom_id': uom_litre.id, 'uom_po_id': uom_litre.id, 'standard_price': 5.17, }) bom_form_cheese_cake = Form(self.env['mrp.bom']) bom_form_cheese_cake.product_tmpl_id = cheese_cake.product_tmpl_id bom_form_cheese_cake.product_qty = 60 bom_form_cheese_cake.product_uom_id = self.uom_unit bom_cheese_cake = bom_form_cheese_cake.save() with Form(bom_cheese_cake) as bom: with bom.bom_line_ids.new() as line: line.product_id = cream line.product_uom_id = uom_litre line.product_qty = 3 with bom.bom_line_ids.new() as line: line.product_id = crumble line.product_uom_id = uom_kg line.product_qty = 5.4 workcenter_2 = self.env['mrp.workcenter'].create({ 'name': 'cake mounting', 'costs_hour': 20, 'time_start': 10, 'time_stop': 15 }) routing_form = Form(self.env['mrp.routing']) routing_form.name = "Cheese cake process" routing_cheese = routing_form.save() with Form(routing_cheese) as routing: with routing.operation_ids.new() as operation: operation.workcenter_id = workcenter operation.name = 'Mix cheese and crumble' operation.time_cycle_manual = 10 with routing.operation_ids.new() as operation: operation.workcenter_id = workcenter_2 operation.name = 'Cake mounting' operation.time_cycle_manual = 5 bom_cheese_cake.routing_id = routing_cheese.id # TEST CHEESE BOM STRUCTURE VALUE WITH BOM QUANTITY report_values = self.env[ 'report.mrp.report_bom_structure']._get_report_data( bom_id=bom_cheese_cake.id, searchQty=60, searchVariant=False) self.assertEqual( report_values['lines']['operations_time'], 40.0, 'Operation time should be the same for 1 unit or for the batch') # Operation cost is the sum of operation line. operation_cost = float_round( 10 / 60 * 10, precision_digits=2) + float_round(30 / 60 * 20, precision_digits=2) self.assertEqual( float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0) for component_line in report_values['lines']['components']: # standard price * bom line quantity * current quantity / bom finished product quantity if component_line['prod_id'] == cream.id: # 3 liter of cream at 5.17$ for 60 unit of cheese cake -> 15.51$ self.assertEqual( float_compare(component_line['total'], (3 * 5.17), precision_digits=2), 0) if component_line['prod_id'] == crumble.id: # 5.4 kg of crumble at the cost of a batch. crumble_cost = self.env[ 'report.mrp.report_bom_structure']._get_report_data( bom_id=bom_crumble.id, searchQty=5.4, searchVariant=False)['lines']['total'] self.assertEqual( float_compare(component_line['total'], crumble_cost, precision_digits=2), 0) # total price = 15.51 + crumble_cost + operation_cost(10 + 1.67 = 11.67) = 27.18 + crumble_cost self.assertEqual( float_compare(report_values['lines']['total'], 27.18 + crumble_cost, precision_digits=2), 0, 'Product Bom Price is not correct')
def _get_bom(self, bom_id=False, product_id=False, line_qty=False, line_id=False, level=False): bom = self.env['mrp.bom'].browse(bom_id) bom_quantity = line_qty if line_id: current_line = self.env['mrp.bom.line'].browse(int(line_id)) bom_quantity = current_line.product_uom_id._compute_quantity( line_qty, bom.product_uom_id) # Display bom components for current selected product variant if product_id: product = self.env['product.product'].browse(int(product_id)) else: product = bom.product_id or bom.product_tmpl_id.product_variant_id if product: attachments = self.env['mrp.document'].search([ '|', '&', ('res_model', '=', 'product.product'), ('res_id', '=', product.id), '&', ('res_model', '=', 'product.template'), ('res_id', '=', product.product_tmpl_id.id) ]) else: product = bom.product_tmpl_id attachments = self.env['mrp.document'].search([ ('res_model', '=', 'product.template'), ('res_id', '=', product.id) ]) operations = [] if bom.product_qty > 0: operations = self._get_operation_line( bom.routing_id, float_round(bom_quantity / bom.product_qty, precision_rounding=1, rounding_method='UP'), 0) company = bom.company_id or self.env.company lines = { 'bom': bom, 'bom_qty': bom_quantity, 'bom_prod_name': product.display_name, 'currency': company.currency_id, 'product': product, 'code': bom and bom.display_name or '', 'price': product.uom_id._compute_price( product.with_context(force_company=company.id).standard_price, bom.product_uom_id) * bom_quantity, 'total': sum([op['total'] for op in operations]), 'level': level or 0, 'operations': operations, 'operations_cost': sum([op['total'] for op in operations]), 'attachments': attachments, 'operations_time': sum([op['duration_expected'] for op in operations]) } components, total = self._get_bom_lines(bom, bom_quantity, product, line_id, level) lines['components'] = components lines['total'] += total return lines
def explode(self, product, quantity, picking_type=False): """ Explodes the BoM and creates two lists with all the information you need: bom_done and line_done Quantity describes the number of times you need the BoM: so the quantity divided by the number created by the BoM and converted into its UoM """ from collections import defaultdict graph = defaultdict(list) V = set() def check_cycle(v, visited, recStack, graph): visited[v] = True recStack[v] = True for neighbour in graph[v]: if visited[neighbour] == False: if check_cycle(neighbour, visited, recStack, graph) == True: return True elif recStack[neighbour] == True: return True recStack[v] = False return False boms_done = [(self, { 'qty': quantity, 'product': product, 'original_qty': quantity, 'parent_line': False })] lines_done = [] V |= set([product.product_tmpl_id.id]) bom_lines = [(bom_line, product, quantity, False) for bom_line in self.bom_line_ids] for bom_line in self.bom_line_ids: V |= set([bom_line.product_id.product_tmpl_id.id]) graph[product.product_tmpl_id.id].append( bom_line.product_id.product_tmpl_id.id) while bom_lines: current_line, current_product, current_qty, parent_line = bom_lines[ 0] bom_lines = bom_lines[1:] if current_line._skip_bom_line(current_product): continue line_quantity = current_qty * current_line.product_qty bom = self._bom_find(product=current_line.product_id, picking_type=picking_type or self.picking_type_id, company_id=self.company_id.id, bom_type='phantom') if bom: converted_line_quantity = current_line.product_uom_id._compute_quantity( line_quantity / bom.product_qty, bom.product_uom_id) bom_lines = [(line, current_line.product_id, converted_line_quantity, current_line) for line in bom.bom_line_ids] + bom_lines for bom_line in bom.bom_line_ids: graph[current_line.product_id.product_tmpl_id.id].append( bom_line.product_id.product_tmpl_id.id) if bom_line.product_id.product_tmpl_id.id in V and check_cycle( bom_line.product_id.product_tmpl_id.id, {key: False for key in V}, {key: False for key in V}, graph): raise UserError( _('Recursion error! A product with a Bill of Material should not have itself in its BoM or child BoMs!' )) V |= set([bom_line.product_id.product_tmpl_id.id]) boms_done.append((bom, { 'qty': converted_line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': current_line })) else: # We round up here because the user expects that if he has to consume a little more, the whole UOM unit # should be consumed. rounding = current_line.product_uom_id.rounding line_quantity = float_round(line_quantity, precision_rounding=rounding, rounding_method='UP') lines_done.append((current_line, { 'qty': line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': parent_line })) return boms_done, lines_done
def change_prod_qty(self): precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') for wizard in self: production = wizard.mo_id produced = sum( production.move_finished_ids.filtered( lambda m: m.product_id == production.product_id).mapped( 'quantity_done')) if wizard.product_qty < produced: format_qty = '%.{precision}f'.format(precision=precision) raise UserError( _("You have already processed %s. Please input a quantity higher than %s " ) % (format_qty % produced, format_qty % produced)) old_production_qty = production.product_qty production.write({'product_qty': wizard.product_qty}) done_moves = production.move_finished_ids.filtered( lambda x: x.state == 'done' and x.product_id == production. product_id) qty_produced = production.product_id.uom_id._compute_quantity( sum(done_moves.mapped('product_qty')), production.product_uom_id) factor = production.product_uom_id._compute_quantity( production.product_qty - qty_produced, production.bom_id. product_uom_id) / production.bom_id.product_qty boms, lines = production.bom_id.explode( production.product_id, factor, picking_type=production.bom_id.picking_type_id) documents = {} for line, line_data in lines: if line.child_bom_id and line.child_bom_id.type == 'phantom' or\ line.product_id.type not in ['product', 'consu']: continue move = production.move_raw_ids.filtered( lambda x: x.bom_line_id.id == line.id and x.state not in ('done', 'cancel')) if move: move = move[0] old_qty = move.product_uom_qty else: old_qty = 0 iterate_key = production._get_document_iterate_key(move) if iterate_key: document = self.env[ 'stock.picking']._log_activity_get_documents( {move: (line_data['qty'], old_qty)}, iterate_key, 'UP') for key, value in document.items(): if documents.get(key): documents[key] += [value] else: documents[key] = [value] production._update_raw_move(line, line_data) production._log_manufacture_exception(documents) operation_bom_qty = {} for bom, bom_data in boms: for operation in bom.routing_id.operation_ids: operation_bom_qty[operation.id] = bom_data['qty'] finished_moves_modification = self._update_finished_moves( production, production.product_qty - qty_produced, old_production_qty) production._log_downside_manufactured_quantity( finished_moves_modification) moves = production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) moves._action_assign() for wo in production.workorder_ids: operation = wo.operation_id if operation_bom_qty.get(operation.id): cycle_number = float_round( operation_bom_qty[operation.id] / operation.workcenter_id.capacity, precision_digits=0, rounding_method='UP') wo.duration_expected = ( operation.workcenter_id.time_start + operation.workcenter_id.time_stop + cycle_number * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency) quantity = wo.qty_production - wo.qty_produced if production.product_id.tracking == 'serial': quantity = 1.0 if not float_is_zero( quantity, precision_digits=precision) else 0.0 else: quantity = quantity if (quantity > 0) else 0 if float_is_zero(quantity, precision_digits=precision): wo.finished_lot_id = False wo._workorder_line_ids().unlink() wo.qty_producing = quantity if wo.qty_produced < wo.qty_production and wo.state == 'done': wo.state = 'progress' if wo.qty_produced == wo.qty_production and wo.state == 'progress': wo.state = 'done' if wo.next_work_order_id.state == 'pending': wo.next_work_order_id.state = 'ready' # assign moves; last operation receive all unassigned moves # TODO: following could be put in a function as it is similar as code in _workorders_create # TODO: only needed when creating new moves moves_raw = production.move_raw_ids.filtered( lambda move: move.operation_id == operation and move.state not in ('done', 'cancel')) if wo == production.workorder_ids[-1]: moves_raw |= production.move_raw_ids.filtered( lambda move: not move.operation_id) moves_finished = production.move_finished_ids.filtered( lambda move: move.operation_id == operation ) #TODO: code does nothing, unless maybe by_products? moves_raw.mapped('move_line_ids').write( {'workorder_id': wo.id}) (moves_finished + moves_raw).write({'workorder_id': wo.id}) if wo.state not in ('done', 'cancel'): line_values = wo._update_workorder_lines() wo._workorder_line_ids().create(line_values['to_create']) if line_values['to_delete']: line_values['to_delete'].unlink() for line, vals in line_values['to_update'].items(): line.write(vals) return {}
def _update_workorder_lines(self): """ Update workorder lines, according to the new qty currently produced. It returns a dict with line to create, update or delete. It do not directly write or unlink the line because this function is used in onchange and request that write on db (e.g. workorder creation). """ line_values = {'to_create': [], 'to_delete': [], 'to_update': {}} # moves are actual records move_finished_ids = self.move_finished_ids._origin.filtered( lambda move: move.product_id != self.product_id and move.state not in ('done', 'cancel')) move_raw_ids = self.move_raw_ids._origin.filtered( lambda move: move.state not in ('done', 'cancel')) for move in move_raw_ids | move_finished_ids: move_workorder_lines = self._workorder_line_ids().filtered( lambda w: w.move_id == move) # Compute the new quantity for the current component rounding = move.product_uom.rounding new_qty = self._prepare_component_quantity(move, self.qty_producing) # In case the production uom is different than the workorder uom # it means the product is serial and production uom is not the reference new_qty = self.product_uom_id._compute_quantity( new_qty, self.production_id.product_uom_id, round=False) qty_todo = float_round( new_qty - sum(move_workorder_lines.mapped('qty_to_consume')), precision_rounding=rounding) # Remove or lower quantity on exisiting workorder lines if float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0: qty_todo = abs(qty_todo) # Try to decrease or remove lines that are not reserved and # partialy reserved first. A different decrease strategy could # be define in _unreserve_order method. for workorder_line in move_workorder_lines.sorted( key=lambda wl: wl._unreserve_order()): if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0: break # If the quantity to consume on the line is lower than the # quantity to remove, the line could be remove. if float_compare(workorder_line.qty_to_consume, qty_todo, precision_rounding=rounding) <= 0: qty_todo = float_round(qty_todo - workorder_line.qty_to_consume, precision_rounding=rounding) if line_values['to_delete']: line_values['to_delete'] |= workorder_line else: line_values['to_delete'] = workorder_line # decrease the quantity on the line else: new_val = workorder_line.qty_to_consume - qty_todo # avoid to write a negative reserved quantity new_reserved = max( 0, workorder_line.qty_reserved - qty_todo) line_values['to_update'][workorder_line] = { 'qty_to_consume': new_val, 'qty_done': new_val, 'qty_reserved': new_reserved, } qty_todo = 0 else: # Search among wo lines which one could be updated qty_reserved_wl = defaultdict(float) # Try to update the line with the greater reservation first in # order to promote bigger batch. for workorder_line in move_workorder_lines.sorted( key=lambda wl: wl.qty_reserved, reverse=True): rounding = workorder_line.product_uom_id.rounding if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0: break move_lines = workorder_line._get_move_lines() qty_reserved_wl[ workorder_line.lot_id] += workorder_line.qty_reserved # The reserved quantity according to exisiting move line # already produced (with qty_done set) and other production # lines with the same lot that are currently on production. qty_reserved_remaining = sum( move_lines.mapped('product_uom_qty')) - sum( move_lines.mapped('qty_done')) - qty_reserved_wl[ workorder_line.lot_id] if float_compare(qty_reserved_remaining, 0, precision_rounding=rounding) > 0: qty_to_add = min(qty_reserved_remaining, qty_todo) line_values['to_update'][workorder_line] = { 'qty_done': workorder_line.qty_to_consume + qty_to_add, 'qty_to_consume': workorder_line.qty_to_consume + qty_to_add, 'qty_reserved': workorder_line.qty_reserved + qty_to_add, } qty_todo -= qty_to_add qty_reserved_wl[workorder_line.lot_id] += qty_to_add # If a line exists without reservation and without lot. It # means that previous operations could not find any reserved # quantity and created a line without lot prefilled. In this # case, the system will not find an existing move line with # available reservation anymore and will increase this line # instead of creating a new line without lot and reserved # quantities. if not workorder_line.qty_reserved and not workorder_line.lot_id and workorder_line.product_tracking != 'serial': line_values['to_update'][workorder_line] = { 'qty_done': workorder_line.qty_to_consume + qty_todo, 'qty_to_consume': workorder_line.qty_to_consume + qty_todo, } qty_todo = 0 # if there are still qty_todo, create new wo lines if float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0: for values in self._generate_lines_values(move, qty_todo): line_values['to_create'].append(values) return line_values
def _compute_price_rule(self, products_qty_partner, date=False, uom_id=False): """ Low-level method - Mono pricelist, multi products Returns: dict{product_id: (price, suitable_rule) for the given pricelist} Date in context can be a date, datetime, ... :param products_qty_partner: list of typles products, quantity, partner :param datetime date: validity date :param ID uom_id: intermediate unit of measure """ self.ensure_one() if not date: date = self._context.get('date') or fields.Date.today() date = fields.Date.to_date( date) # boundary conditions differ if we have a datetime if not uom_id and self._context.get('uom'): uom_id = self._context['uom'] if uom_id: # rebrowse with uom if given products = [ item[0].with_context(uom=uom_id) for item in products_qty_partner ] products_qty_partner = [ (products[index], data_struct[1], data_struct[2]) for index, data_struct in enumerate(products_qty_partner) ] else: products = [item[0] for item in products_qty_partner] if not products: return {} categ_ids = {} for p in products: categ = p.categ_id while categ: categ_ids[categ.id] = True categ = categ.parent_id categ_ids = list(categ_ids) is_product_template = products[0]._name == "product.template" if is_product_template: prod_tmpl_ids = [tmpl.id for tmpl in products] # all variants of all products prod_ids = [ p.id for p in list( chain.from_iterable( [t.product_variant_ids for t in products])) ] else: prod_ids = [product.id for product in products] prod_tmpl_ids = [ product.product_tmpl_id.id for product in products ] items = self._compute_price_rule_get_items(products_qty_partner, date, uom_id, prod_tmpl_ids, prod_ids, categ_ids) results = {} for product, qty, partner in products_qty_partner: results[product.id] = 0.0 suitable_rule = False # Final unit price is computed according to `qty` in the `qty_uom_id` UoM. # An intermediary unit price may be computed according to a different UoM, in # which case the price_uom_id contains that UoM. # The final price will be converted to match `qty_uom_id`. qty_uom_id = self._context.get('uom') or product.uom_id.id price_uom_id = product.uom_id.id qty_in_product_uom = qty if qty_uom_id != product.uom_id.id: try: qty_in_product_uom = self.env['uom.uom'].browse([ self._context['uom'] ])._compute_quantity(qty, product.uom_id) except UserError: # Ignored - incompatible UoM in context, use default product UoM pass # if Public user try to access standard price from website sale, need to call price_compute. # TDE SURPRISE: product can actually be a template price = product.price_compute('list_price')[product.id] price_uom = self.env['uom.uom'].browse([qty_uom_id]) for rule in items: if rule.min_quantity and qty_in_product_uom < rule.min_quantity: continue if is_product_template: if rule.product_tmpl_id and product.id != rule.product_tmpl_id.id: continue if rule.product_id and not ( product.product_variant_count == 1 and product.product_variant_id.id == rule.product_id.id): # product rule acceptable on template if has only one variant continue else: if rule.product_tmpl_id and product.product_tmpl_id.id != rule.product_tmpl_id.id: continue if rule.product_id and product.id != rule.product_id.id: continue if rule.categ_id: cat = product.categ_id while cat: if cat.id == rule.categ_id.id: break cat = cat.parent_id if not cat: continue if rule.base == 'pricelist' and rule.base_pricelist_id: price_tmp = rule.base_pricelist_id._compute_price_rule( [(product, qty, partner)], date, uom_id)[product.id][0] # TDE: 0 = price, 1 = rule price = rule.base_pricelist_id.currency_id._convert( price_tmp, self.currency_id, self.env.company, date, round=False) else: # if base option is public price take sale price else cost price of product # price_compute returns the price in the context UoM, i.e. qty_uom_id price = product.price_compute(rule.base)[product.id] convert_to_price_uom = (lambda price: product.uom_id. _compute_price(price, price_uom)) if price is not False: if rule.compute_price == 'fixed': price = convert_to_price_uom(rule.fixed_price) elif rule.compute_price == 'percentage': price = (price - (price * (rule.percent_price / 100))) or 0.0 else: # complete formula price_limit = price price = (price - (price * (rule.price_discount / 100))) or 0.0 if rule.price_round: price = tools.float_round( price, precision_rounding=rule.price_round) if rule.price_surcharge: price_surcharge = convert_to_price_uom( rule.price_surcharge) price += price_surcharge if rule.price_min_margin: price_min_margin = convert_to_price_uom( rule.price_min_margin) price = max(price, price_limit + price_min_margin) if rule.price_max_margin: price_max_margin = convert_to_price_uom( rule.price_max_margin) price = min(price, price_limit + price_max_margin) suitable_rule = rule break # Final price conversion into pricelist currency if suitable_rule and suitable_rule.compute_price != 'fixed' and suitable_rule.base != 'pricelist': if suitable_rule.base == 'standard_price': cur = product.cost_currency_id else: cur = product.currency_id price = cur._convert(price, self.currency_id, self.env.company, date, round=False) if not suitable_rule: cur = product.currency_id price = cur._convert(price, self.currency_id, self.env.company, date, round=False) results[product.id] = (price, suitable_rule and suitable_rule.id or False) return results
def _plan_prepare_values(self): currency = self.env.company.currency_id uom_hour = self.env.ref('uom.product_uom_hour') hour_rounding = uom_hour.rounding billable_types = [ 'non_billable', 'non_billable_project', 'billable_time', 'billable_fixed' ] values = { 'projects': self, 'currency': currency, 'timesheet_domain': [('project_id', 'in', self.ids)], 'profitability_domain': [('project_id', 'in', self.ids)], 'stat_buttons': self._plan_get_stat_button(), } # # Hours, Rates and Profitability # dashboard_values = { 'hours': dict.fromkeys(billable_types + ['total'], 0.0), 'rates': dict.fromkeys(billable_types + ['total'], 0.0), 'profit': { 'invoiced': 0.0, 'to_invoice': 0.0, 'cost': 0.0, 'total': 0.0, } } # hours from non-invoiced timesheets that are linked to canceled so canceled_hours_domain = [('project_id', 'in', self.ids), ('timesheet_invoice_type', '!=', False), ('so_line.state', '=', 'cancel')] total_canceled_hours = sum(self.env['account.analytic.line'].search( canceled_hours_domain).mapped('unit_amount')) dashboard_values['hours']['canceled'] = float_round( total_canceled_hours, precision_rounding=hour_rounding) dashboard_values['hours']['total'] += float_round( total_canceled_hours, precision_rounding=hour_rounding) # hours (from timesheet) and rates (by billable type) dashboard_domain = [('project_id', 'in', self.ids), ('timesheet_invoice_type', '!=', False), '|', ('so_line', '=', False), ('so_line.state', '!=', 'cancel') ] # force billable type dashboard_data = self.env['account.analytic.line'].read_group( dashboard_domain, ['unit_amount', 'timesheet_invoice_type'], ['timesheet_invoice_type']) dashboard_total_hours = sum( [data['unit_amount'] for data in dashboard_data]) + total_canceled_hours for data in dashboard_data: billable_type = data['timesheet_invoice_type'] dashboard_values['hours'][billable_type] = float_round( data.get('unit_amount'), precision_rounding=hour_rounding) dashboard_values['hours']['total'] += float_round( data.get('unit_amount'), precision_rounding=hour_rounding) # rates rate = round( data.get('unit_amount') / dashboard_total_hours * 100, 2) if dashboard_total_hours else 0.0 dashboard_values['rates'][billable_type] = rate dashboard_values['rates']['total'] += rate # rates from non-invoiced timesheets that are linked to canceled so dashboard_values['rates']['canceled'] = float_round( 100 * total_canceled_hours / (dashboard_total_hours or 1), precision_rounding=hour_rounding) # profitability, using profitability SQL report profit = dict.fromkeys([ 'invoiced', 'to_invoice', 'cost', 'expense_cost', 'expense_amount_untaxed_invoiced', 'total' ], 0.0) profitability_raw_data = self.env[ 'project.profitability.report'].read_group( [('project_id', 'in', self.ids)], [ 'project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_invoiced' ], ['project_id']) for data in profitability_raw_data: profit['invoiced'] += data.get('amount_untaxed_invoiced', 0.0) profit['to_invoice'] += data.get('amount_untaxed_to_invoice', 0.0) profit['cost'] += data.get('timesheet_cost', 0.0) profit['expense_cost'] += data.get('expense_cost', 0.0) profit['expense_amount_untaxed_invoiced'] += data.get( 'expense_amount_untaxed_invoiced', 0.0) profit['total'] = sum([profit[item] for item in profit.keys()]) dashboard_values['profit'] = profit values['dashboard'] = dashboard_values # # Time Repartition (per employee per billable types) # user_ids = self.env['project.task'].sudo().read_group( [('project_id', 'in', self.ids), ('user_id', '!=', False)], ['user_id'], ['user_id']) user_ids = [user_id['user_id'][0] for user_id in user_ids] employee_ids = self.env['res.users'].sudo().search_read( [('id', 'in', user_ids)], ['employee_ids']) # flatten the list of list employee_ids = list( itertools.chain.from_iterable( [employee_id['employee_ids'] for employee_id in employee_ids])) aal_employee_ids = self.env['account.analytic.line'].read_group( [('project_id', 'in', self.ids), ('employee_id', '!=', False)], ['employee_id'], ['employee_id']) employee_ids.extend( list(map(lambda x: x['employee_id'][0], aal_employee_ids))) employees = self.env['hr.employee'].sudo().browse(employee_ids) repartition_domain = [('project_id', 'in', self.ids), ('employee_id', '!=', False), ('timesheet_invoice_type', '!=', False) ] # force billable type # repartition data, without timesheet on cancelled so repartition_data = self.env['account.analytic.line'].read_group( repartition_domain + ['|', ('so_line', '=', False), ('so_line.state', '!=', 'cancel')], ['employee_id', 'timesheet_invoice_type', 'unit_amount'], ['employee_id', 'timesheet_invoice_type'], lazy=False) # read timesheet on cancelled so cancelled_so_timesheet = self.env['account.analytic.line'].read_group( repartition_domain + [('so_line.state', '=', 'cancel')], ['employee_id', 'unit_amount'], ['employee_id'], lazy=False) repartition_data += [{ **canceled, 'timesheet_invoice_type': 'canceled' } for canceled in cancelled_so_timesheet] # set repartition per type per employee repartition_employee = {} for employee in employees: repartition_employee[employee.id] = dict( employee_id=employee.id, employee_name=employee.name, non_billable_project=0.0, non_billable=0.0, billable_time=0.0, billable_fixed=0.0, canceled=0.0, total=0.0, ) for data in repartition_data: employee_id = data['employee_id'][0] repartition_employee.setdefault( employee_id, dict( employee_id=data['employee_id'][0], employee_name=data['employee_id'][1], non_billable_project=0.0, non_billable=0.0, billable_time=0.0, billable_fixed=0.0, canceled=0.0, total=0.0, ))[data['timesheet_invoice_type']] = float_round( data.get('unit_amount', 0.0), precision_rounding=hour_rounding) repartition_employee[employee_id][ '__domain_' + data['timesheet_invoice_type']] = data['__domain'] # compute total for employee_id, vals in repartition_employee.items(): repartition_employee[employee_id]['total'] = sum( [vals[inv_type] for inv_type in [*billable_types, 'canceled']]) hours_per_employee = [ repartition_employee[employee_id]['total'] for employee_id in repartition_employee ] values['repartition_employee_max'] = (max(hours_per_employee) if hours_per_employee else 1) or 1 values['repartition_employee'] = repartition_employee # # Table grouped by SO / SOL / Employees # timesheet_forecast_table_rows = self._table_get_line_values() if timesheet_forecast_table_rows: values['timesheet_forecast_table'] = timesheet_forecast_table_rows return values
def _procure_orderpoint_confirm(self, use_new_cursor=False, company_id=False): """ Create procurements based on orderpoints. :param bool use_new_cursor: if set, use a dedicated cursor and auto-commit after processing 1000 orderpoints. This is appropriate for batch jobs only. """ if company_id and self.env.company.id != company_id: # To ensure that the company_id is taken into account for # all the processes triggered by this method # i.e. If a PO is generated by the run of the procurements the # sequence to use is the one for the specified company not the # one of the user's company self = self.with_context(company_id=company_id, force_company=company_id) OrderPoint = self.env['stock.warehouse.orderpoint'] domain = self._get_orderpoint_domain(company_id=company_id) orderpoints_noprefetch = OrderPoint.with_context(prefetch_fields=False).search(domain, order=self._procurement_from_orderpoint_get_order()).ids while orderpoints_noprefetch: if use_new_cursor: cr = registry(self._cr.dbname).cursor() self = self.with_env(self.env(cr=cr)) OrderPoint = self.env['stock.warehouse.orderpoint'] orderpoints = OrderPoint.browse(orderpoints_noprefetch[:1000]) orderpoints_noprefetch = orderpoints_noprefetch[1000:] # Calculate groups that can be executed together location_data = OrderedDict() def makedefault(): return { 'products': self.env['product.product'], 'orderpoints': self.env['stock.warehouse.orderpoint'], 'groups': [] } for orderpoint in orderpoints: key = self._procurement_from_orderpoint_get_grouping_key([orderpoint.id]) if not location_data.get(key): location_data[key] = makedefault() location_data[key]['products'] += orderpoint.product_id location_data[key]['orderpoints'] += orderpoint location_data[key]['groups'] = self._procurement_from_orderpoint_get_groups([orderpoint.id]) for location_id, location_res in location_data.items(): location_orderpoints = location_res['orderpoints'] product_context = dict(self._context, location=location_orderpoints[0].location_id.id) substract_quantity = location_orderpoints._quantity_in_progress() for group in location_res['groups']: if group.get('from_date'): product_context['from_date'] = group['from_date'].strftime(DEFAULT_SERVER_DATETIME_FORMAT) if group['to_date']: product_context['to_date'] = group['to_date'].strftime(DEFAULT_SERVER_DATETIME_FORMAT) product_quantity = location_res['products'].with_context(product_context)._product_available() for orderpoint in location_orderpoints: try: op_product_virtual = product_quantity[orderpoint.product_id.id]['virtual_available'] if op_product_virtual is None: continue if float_compare(op_product_virtual, orderpoint.product_min_qty, precision_rounding=orderpoint.product_uom.rounding) <= 0: qty = max(orderpoint.product_min_qty, orderpoint.product_max_qty) - op_product_virtual remainder = orderpoint.qty_multiple > 0 and qty % orderpoint.qty_multiple or 0.0 if float_compare(remainder, 0.0, precision_rounding=orderpoint.product_uom.rounding) > 0: qty += orderpoint.qty_multiple - remainder if float_compare(qty, 0.0, precision_rounding=orderpoint.product_uom.rounding) < 0: continue qty -= substract_quantity[orderpoint.id] qty_rounded = float_round(qty, precision_rounding=orderpoint.product_uom.rounding) if qty_rounded > 0: values = orderpoint._prepare_procurement_values(qty_rounded, **group['procurement_values']) try: with self._cr.savepoint(): #TODO: make it batch self.env['procurement.group'].run([self.env['procurement.group'].Procurement( orderpoint.product_id, qty_rounded, orderpoint.product_uom, orderpoint.location_id, orderpoint.name, orderpoint.name, orderpoint.company_id, values)]) except UserError as error: self.env['stock.rule']._log_next_activity(orderpoint.product_id, error.name) self._procurement_from_orderpoint_post_process([orderpoint.id]) if use_new_cursor: cr.commit() except OperationalError: if use_new_cursor: orderpoints_noprefetch += [orderpoint.id] cr.rollback() continue else: raise try: if use_new_cursor: cr.commit() except OperationalError: if use_new_cursor: cr.rollback() continue else: raise if use_new_cursor: cr.commit() cr.close() return {}