def _start_nextworkorder(self): rounding = self.product_id.uom_id.rounding if self.next_work_order_id.state == 'pending' and ( (self.operation_id.batch == 'no' and float_compare(self.qty_production, self.qty_produced, precision_rounding=rounding) <= 0) or (self.operation_id.batch == 'yes' and float_compare(self.operation_id.batch_size, self.qty_produced, precision_rounding=rounding) <= 0)): self.next_work_order_id.state = 'ready'
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_compare(0.01, 0.02, precision_digits=3, precision_rounding=0.01) with self.assertRaises(AssertionError): float_round(0.01, precision_digits=3, precision_rounding=0.01)
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 action_validate(self): inventory_lines = self.line_ids.filtered( lambda l: l.product_id.tracking in ['lot', 'serial'] and not l. prod_lot_id and l.theoretical_qty != l.product_qty) lines = self.line_ids.filtered(lambda l: float_compare( l.product_qty, 1, precision_rounding=l.product_uom_id.rounding ) > 0 and l.product_id.tracking == 'serial' and l.prod_lot_id) if inventory_lines and not lines: wiz_lines = [(0, 0, { 'product_id': product.id, 'tracking': product.tracking }) for product in inventory_lines.mapped('product_id')] wiz = self.env['stock.track.confirmation'].create({ 'inventory_id': self.id, 'tracking_line_ids': wiz_lines }) return { 'name': _('Tracked Products in Inventory Adjustment'), 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': 'stock.track.confirmation', 'target': 'new', 'res_id': wiz.id, } else: self._action_done()
def write(self, values): if values.get('order_line') and self.state == 'sale': for order in self: pre_order_line_qty = { order_line: order_line.product_uom_qty for order_line in order.mapped('order_line') } res = super(SaleOrder, self).write(values) if values.get('order_line') and self.state == 'sale': for order in self: to_log = {} for order_line in order.order_line: if pre_order_line_qty.get( order_line, False) and float_compare( order_line.product_uom_qty, pre_order_line_qty[order_line], order_line.product_uom.rounding) < 0: to_log[order_line] = (order_line.product_uom_qty, pre_order_line_qty[order_line]) if to_log: documents = self.env[ 'stock.picking']._log_activity_get_documents( to_log, 'move_ids', 'UP') order._log_decrease_ordered_quantity(documents) return res
def _compute_consumed_less_than_planned(self): for order in self: order.consumed_less_than_planned = any( order.move_raw_ids.filtered(lambda move: float_compare( move.quantity_done, move.product_uom_qty, precision_rounding=move.product_uom.rounding) == -1))
def _check_main_currency_rounding(self): if any(precision.name == 'Account' and tools.float_compare( self.env.user.company_id.currency_id.rounding, 10**-precision.digits, precision_digits=6) == -1 for precision in self): raise ValidationError( _("You cannot define the decimal precision of 'Account' as greater than the rounding factor of the company's main currency" )) return True
def _onchange_qty_done(self): """ When the user is encoding a produce line for a tracked product, we apply some logic to help him. This onchange will warn him if he set `qty_done` to a non-supported value. """ res = {} if self.product_id.tracking == 'serial' and self.qty_done: if float_compare(self.qty_done, 1.0, precision_rounding=self.move_id.product_id.uom_id.rounding) != 0: message = _('You can only process 1.0 %s of products with unique serial number.') % self.product_id.uom_id.name res['warning'] = {'title': _('Warning'), 'message': message} return res
def _onchange_product_qty(self): lines = [] qty_todo = self.product_uom_id._compute_quantity(self.product_qty, self.production_id.product_uom_id, round=False) for move in self.production_id.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel') and m.bom_line_id): qty_to_consume = float_round(qty_todo * move.unit_factor, precision_rounding=move.product_uom.rounding) for move_line in move.move_line_ids: if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) <= 0: break if move_line.lot_produced_id or float_compare(move_line.product_uom_qty, move_line.qty_done, precision_rounding=move.product_uom.rounding) <= 0: continue to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty) lines.append({ 'move_id': move.id, 'qty_to_consume': to_consume_in_line, 'qty_done': to_consume_in_line, 'lot_id': move_line.lot_id.id, 'product_uom_id': move.product_uom.id, 'product_id': move.product_id.id, 'qty_reserved': min(to_consume_in_line, move_line.product_uom_qty), }) qty_to_consume -= to_consume_in_line if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0: if move.product_id.tracking == 'serial': while float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0: lines.append({ 'move_id': move.id, 'qty_to_consume': 1, 'qty_done': 1, 'product_uom_id': move.product_uom.id, 'product_id': move.product_id.id, }) qty_to_consume -= 1 else: lines.append({ 'move_id': move.id, 'qty_to_consume': qty_to_consume, 'qty_done': qty_to_consume, 'product_uom_id': move.product_uom.id, 'product_id': move.product_id.id, }) self.produce_line_ids = [(5,)] + [(0, 0, x) for x in lines]
def _prepare_reconciliation(self, st_line, move_lines=None, partner=None): ''' Reconcile the statement line with some move lines using this reconciliation model. :param st_line: An account.bank.statement.line record. :param move_lines: An account.move.line recordset. :param partner_id: An optional res.partner record. If not set, st_line.partner_id will be used. :return: Counterpart account.moves. ''' self.ensure_one() # Create counterpart_aml_dicts + payment_aml_rec. counterpart_aml_dicts = [] payment_aml_rec = self.env['account.move.line'] if move_lines: for aml in move_lines: if aml.account_id.internal_type == 'liquidity': payment_aml_rec |= aml else: amount = aml.currency_id and aml.amount_residual_currency or aml.amount_residual counterpart_aml_dicts.append({ 'name': aml.name if aml.name != '/' else aml.move_id.name, 'debit': amount < 0 and -amount or 0, 'credit': amount > 0 and amount or 0, 'move_line': aml, }) # Create new_aml_dicts. new_aml_dicts = self._get_write_off_move_lines_dict(st_line, move_lines=move_lines) line_residual = st_line.currency_id and st_line.amount_currency or st_line.amount line_currency = st_line.currency_id or st_line.journal_id.currency_id or st_line.company_id.currency_id total_residual = move_lines and sum(aml.currency_id and aml.amount_residual_currency or aml.amount_residual for aml in move_lines) or 0.0 total_residual -= sum(aml['debit'] - aml['credit'] for aml in new_aml_dicts) # Create open_balance_dict open_balance_dict = None if float_compare(line_residual, total_residual, precision_rounding=line_currency.rounding) != 0: if not partner and not st_line.partner_id: open_balance_dict = False else: balance = total_residual - line_residual partner = partner or st_line.partner_id open_balance_dict = { 'name': '%s : %s' % (st_line.name, _('Open Balance')), 'account_id': balance < 0 and partner.property_account_payable_id.id or partner.property_account_receivable_id.id, 'debit': balance > 0 and balance or 0, 'credit': balance < 0 and -balance or 0, } return { 'counterpart_aml_dicts': counterpart_aml_dicts, 'payment_aml_rec': payment_aml_rec, 'new_aml_dicts': new_aml_dicts, 'open_balance_dict': open_balance_dict }
def _onchange_product_id_check_availability(self): if not self.product_id or not self.product_uom_qty or not self.product_uom: self.product_packaging = False return {} if self.product_id.type == 'product': precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') product = self.product_id.with_context( warehouse=self.order_id.warehouse_id.id, lang=self.order_id.partner_id.lang or self.env.user.lang or 'en_US') product_qty = self.product_uom._compute_quantity( self.product_uom_qty, self.product_id.uom_id) if float_compare(product.virtual_available, product_qty, precision_digits=precision) == -1: is_available = self._check_routing() if not is_available: message = _('You plan to sell %s %s of %s but you only have %s %s available in %s warehouse.') % \ (self.product_uom_qty, self.product_uom.name, self.product_id.name, product.virtual_available, product.uom_id.name, self.order_id.warehouse_id.name) # We check if some products are available in other warehouses. if float_compare(product.virtual_available, self.product_id.virtual_available, precision_digits=precision) == -1: message += _('\nThere are %s %s available across all warehouses.\n\n') % \ (self.product_id.virtual_available, product.uom_id.name) for warehouse in self.env['stock.warehouse'].search( []): quantity = self.product_id.with_context( warehouse=warehouse.id).virtual_available if quantity > 0: message += "%s: %s %s\n" % ( warehouse.name, quantity, self.product_id.uom_id.name) warning_mess = { 'title': _('Not enough inventory!'), 'message': message } return {'warning': warning_mess} return {}
def _update_line_quantity(self, values): precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') line_products = self.filtered( lambda l: l.product_id.type in ['product', 'consu']) if line_products.mapped('qty_delivered') and float_compare( values['product_uom_qty'], max(line_products.mapped('qty_delivered')), precision_digits=precision) == -1: raise UserError( _('You cannot decrease the ordered quantity below the delivered quantity.\n' 'Create a return first.')) super(SaleOrderLine, self)._update_line_quantity(values)
def write(self, values): increased_lines = None decreased_lines = None increased_values = {} decreased_values = {} if 'product_uom_qty' in values: precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') increased_lines = self.sudo().filtered( lambda r: r.product_id.service_to_purchase and r. purchase_line_count and float_compare( r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1) decreased_lines = self.sudo().filtered( lambda r: r.product_id.service_to_purchase and r. purchase_line_count and float_compare( r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == 1) increased_values = { line.id: line.product_uom_qty for line in increased_lines } decreased_values = { line.id: line.product_uom_qty for line in decreased_lines } result = super(SaleOrderLine, self).write(values) if increased_lines: increased_lines._purchase_increase_ordered_qty( values['product_uom_qty'], increased_values) if decreased_lines: decreased_lines._purchase_decrease_ordered_qty( values['product_uom_qty'], decreased_values) return result
def _get_rounding_difference_vals(self, amount, amount_converted): if self.config_id.cash_rounding: partial_args = { 'name': 'Rounding line', 'move_id': self.move_id.id, } if float_compare( 0.0, amount, precision_rounding=self.currency_id.rounding) > 0: # loss partial_args[ 'account_id'] = self.config_id.rounding_method._get_loss_account_id( ).id return self._debit_amounts(partial_args, -amount, -amount_converted) if float_compare( 0.0, amount, precision_rounding=self.currency_id.rounding ) < 0: # profit partial_args[ 'account_id'] = self.config_id.rounding_method._get_profit_account_id( ).id return self._credit_amounts(partial_args, amount, amount_converted)
def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False): res = super(AccountMoveLine, self).reconcile(writeoff_acc_id=writeoff_acc_id, writeoff_journal_id=writeoff_journal_id) account_move_ids = [ l.move_id.id for l in self if float_compare( l.move_id.matched_percentage, 1, precision_digits=5) == 0 ] if account_move_ids: expense_sheets = self.env['hr.expense.sheet'].search([ ('account_move_id', 'in', account_move_ids), ('state', '!=', 'done') ]) expense_sheets.set_to_paid() return res
def write(self, values): lines = self.env['sale.order.line'] if 'product_uom_qty' in values: precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') lines = self.filtered( lambda r: r.state == 'sale' and not r.is_expense and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1) previous_product_uom_qty = { line.id: line.product_uom_qty for line in lines } res = super(SaleOrderLine, self).write(values) if lines: lines.with_context( previous_product_uom_qty=previous_product_uom_qty )._action_launch_stock_rule() return res
def do_produce(self): # Nothing to do for lots since values are created using default data (stock.move.lots) quantity = self.product_qty if float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) <= 0: raise UserError(_("The production order for '%s' has no quantity specified.") % self.product_id.display_name) for move in self.production_id.move_finished_ids: if move.product_id.tracking == 'none' and move.state not in ('done', 'cancel'): rounding = move.product_uom.rounding if move.product_id.id == self.production_id.product_id.id: move.quantity_done += float_round(quantity, precision_rounding=rounding) elif move.unit_factor: # byproducts handling move.quantity_done += float_round(quantity * move.unit_factor, precision_rounding=rounding) self.check_finished_move_lots() if self.production_id.state == 'confirmed': self.production_id.write({ 'state': 'progress', 'date_start': datetime.now(), }) return {'type': 'ir.actions.act_window_close'}
def action_validate(self): self.ensure_one() if self.product_id.type != 'product': return self.do_scrap() precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') available_qty = sum(self.env['stock.quant']._gather( self.product_id, self.location_id, self.lot_id, self.package_id, self.owner_id, strict=True).mapped('quantity')) scrap_qty = self.product_uom_id._compute_quantity( self.scrap_qty, self.product_id.uom_id) if float_compare(available_qty, scrap_qty, precision_digits=precision) >= 0: return self.do_scrap() else: return { 'name': _('Insufficient Quantity'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.warn.insufficient.qty.scrap', 'view_id': self.env.ref( 'stock.stock_warn_insufficient_qty_scrap_form_view').id, 'type': 'ir.actions.act_window', 'context': { 'default_product_id': self.product_id.id, 'default_location_id': self.location_id.id, 'default_scrap_id': self.id }, 'target': 'new' }
def _get_produced_qty(self): for production in self: done_moves = production.move_finished_ids.filtered( lambda x: x.state != 'cancel' and x.product_id.id == production .product_id.id) qty_produced = sum(done_moves.mapped('quantity_done')) wo_done = True if any([ x.state not in ('done', 'cancel') for x in production.workorder_ids ]): wo_done = False production.check_to_done = ( production.is_locked and done_moves and float_compare( qty_produced, production.product_qty, precision_rounding=production.product_uom_id.rounding) != -1 and (production.state not in ("done", "cancel")) and wo_done) production.qty_produced = qty_produced return True
def action_validate(self): self.ensure_one() precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') available_qty = self.env['stock.quant']._get_available_quantity(self.product_id, self.location_id, self.lot_id, strict=True) if float_compare(available_qty, self.product_qty, precision_digits=precision) >= 0: return self.action_unbuild() else: return { 'name': _('Insufficient Quantity'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.warn.insufficient.qty.unbuild', 'view_id': self.env.ref('mrp.stock_warn_insufficient_qty_unbuild_form_view').id, 'type': 'ir.actions.act_window', 'context': { 'default_product_id': self.product_id.id, 'default_location_id': self.location_id.id, 'default_unbuild_id': self.id }, 'target': 'new' }
def _onchange_qty_producing(self): """ Update stock.move.lot records, according to the new qty currently produced. """ moves = self.move_raw_ids.filtered(lambda move: move.state not in ('done', 'cancel') and move.product_id.tracking != 'none' and move.product_id.id != self.production_id.product_id.id) for move in moves: move_lots = self.active_move_line_ids.filtered(lambda move_lot: move_lot.move_id == move) if not move_lots: continue rounding = move.product_uom.rounding new_qty = float_round(move.unit_factor * self.qty_producing, precision_rounding=rounding) if move.product_id.tracking == 'lot': move_lots[0].product_qty = new_qty move_lots[0].qty_done = new_qty elif move.product_id.tracking == 'serial': # Create extra pseudo record qty_todo = float_round(new_qty - sum(move_lots.mapped('qty_done')), precision_rounding=rounding) if float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0: while float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0: self.active_move_line_ids += self.env['stock.move.line'].new({ 'move_id': move.id, 'product_id': move.product_id.id, 'lot_id': False, 'product_uom_qty': 0.0, 'product_uom_id': move.product_uom.id, 'qty_done': min(1.0, qty_todo), 'workorder_id': self.id, 'done_wo': False, 'location_id': move.location_id.id, 'location_dest_id': move.location_dest_id.id, 'date': move.date, }) qty_todo -= 1 elif float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0: qty_todo = abs(qty_todo) for move_lot in move_lots: if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0: break if not move_lot.lot_id and float_compare(qty_todo, move_lot.qty_done, precision_rounding=rounding) >= 0: qty_todo = float_round(qty_todo - move_lot.qty_done, precision_rounding=rounding) self.active_move_line_ids -= move_lot # Difference operator else: #move_lot.product_qty = move_lot.product_qty - qty_todo if float_compare(move_lot.qty_done - qty_todo, 0, precision_rounding=rounding) == 1: move_lot.qty_done = move_lot.qty_done - qty_todo else: move_lot.qty_done = 0 qty_todo = 0
def compare_amounts(self, amount1, amount2): """Compare ``amount1`` and ``amount2`` after rounding them according to the given currency's precision.. An amount is considered lower/greater than another amount if their rounded value is different. This is not the same as having a non-zero difference! For example 1.432 and 1.431 are equal at 2 digits precision, so this method would return 0. However 0.006 and 0.002 are considered different (returns 1) because they respectively round to 0.01 and 0.0, even though 0.006-0.002 = 0.004 which would be considered zero at 2 digits precision. :param float amount1: first amount to compare :param float amount2: second amount to compare :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than, equal to, or greater than ``amount2``, according to ``currency``'s rounding. With the new API, call it like: ``currency.compare_amounts(amount1, amount2)``. """ return tools.float_compare(amount1, amount2, precision_rounding=self.rounding)
def _generate_lot_ids(self): """ Generate stock move lines """ self.ensure_one() MoveLine = self.env['stock.move.line'] tracked_moves = self.move_raw_ids.filtered( lambda move: move.state not in ('done', 'cancel') and move.product_id.tracking != 'none' and move.product_id != self.production_id.product_id and move.bom_line_id) for move in tracked_moves: qty = move.unit_factor * self.qty_producing if move.product_id.tracking == 'serial': while float_compare(qty, 0.0, precision_rounding=move.product_uom.rounding) > 0: MoveLine.create({ 'move_id': move.id, 'product_uom_qty': 0, 'product_uom_id': move.product_uom.id, 'qty_done': min(1, qty), 'production_id': self.production_id.id, 'workorder_id': self.id, 'product_id': move.product_id.id, 'done_wo': False, 'location_id': move.location_id.id, 'location_dest_id': move.location_dest_id.id, }) qty -= 1 else: MoveLine.create({ 'move_id': move.id, 'product_uom_qty': 0, 'product_uom_id': move.product_uom.id, 'qty_done': qty, 'product_id': move.product_id.id, 'production_id': self.production_id.id, 'workorder_id': self.id, 'done_wo': False, 'location_id': move.location_id.id, 'location_dest_id': move.location_dest_id.id, })
def test_sale_with_taxes(self): """ Test SO with taxes applied on its lines and check subtotal applied on its lines and total applied on the SO """ # Create a tax with price included tax_include = self.env['account.tax'].create({ 'name': 'Tax with price include', 'amount': 10, 'price_include': True }) # Create a tax with price not included tax_exclude = self.env['account.tax'].create({ 'name': 'Tax with no price include', 'amount': 10, }) # Apply taxes on the sale order lines self.sol_product_order.write({'tax_id': [(4, tax_include.id)]}) self.sol_serv_deliver.write({'tax_id': [(4, tax_include.id)]}) self.sol_serv_order.write({'tax_id': [(4, tax_exclude.id)]}) self.sol_product_deliver.write({'tax_id': [(4, tax_exclude.id)]}) # Trigger onchange to reset discount, unit price, subtotal, ... for line in self.sale_order.order_line: line.product_id_change() line._onchange_discount() for line in self.sale_order.order_line: if line.tax_id.price_include: price = line.price_unit * line.product_uom_qty - line.price_tax else: price = line.price_unit * line.product_uom_qty self.assertEquals(float_compare(line.price_subtotal, price, precision_digits=2), 0) self.assertEquals(self.sale_order.amount_total, self.sale_order.amount_untaxed + self.sale_order.amount_tax, 'Taxes should be applied')
def record_production(self): if not self: return True self.ensure_one() if self.qty_producing <= 0: raise UserError(_('Please set the quantity you are currently producing. It should be different from zero.')) if (self.production_id.product_id.tracking != 'none') and not self.final_lot_id and self.move_raw_ids: raise UserError(_('You should provide a lot/serial number for the final product.')) # Update quantities done on each raw material line # For each untracked component without any 'temporary' move lines, # (the new workorder tablet view allows registering consumed quantities for untracked components) # we assume that only the theoretical quantity was used for move in self.move_raw_ids: if move.has_tracking == 'none' and (move.state not in ('done', 'cancel')) and move.bom_line_id\ and move.unit_factor and not move.move_line_ids.filtered(lambda ml: not ml.done_wo): rounding = move.product_uom.rounding if self.product_id.tracking != 'none': qty_to_add = float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding) move._generate_consumed_move_line(qty_to_add, self.final_lot_id) elif len(move._get_move_lines()) < 2: move.quantity_done += float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding) else: move._set_quantity_done(move.quantity_done + float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding)) # Transfer quantities from temporary to final move lots or make them final for move_line in self.active_move_line_ids: # Check if move_line already exists if move_line.qty_done <= 0: # rounding... move_line.sudo().unlink() continue if move_line.product_id.tracking != 'none' and not move_line.lot_id: raise UserError(_('You should provide a lot/serial number for a component.')) # Search other move_line where it could be added: lots = self.move_line_ids.filtered(lambda x: (x.lot_id.id == move_line.lot_id.id) and (not x.lot_produced_id) and (not x.done_move) and (x.product_id == move_line.product_id)) if lots: lots[0].qty_done += move_line.qty_done lots[0].lot_produced_id = self.final_lot_id.id self._link_to_quality_check(move_line, lots[0]) move_line.sudo().unlink() else: move_line.lot_produced_id = self.final_lot_id.id move_line.done_wo = True self.move_line_ids.filtered( lambda move_line: not move_line.done_move and not move_line.lot_produced_id and move_line.qty_done > 0 ).write({ 'lot_produced_id': self.final_lot_id.id, 'lot_produced_qty': self.qty_producing }) # If last work order, then post lots used # TODO: should be same as checking if for every workorder something has been done? if not self.next_work_order_id: production_move = self.production_id.move_finished_ids.filtered( lambda x: (x.product_id.id == self.production_id.product_id.id) and (x.state not in ('done', 'cancel'))) if production_move.product_id.tracking != 'none': move_line = production_move.move_line_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id) if move_line: 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.final_lot_id.id, 'product_uom_qty': self.qty_producing, 'product_uom_id': production_move.product_uom.id, 'qty_done': self.qty_producing, 'workorder_id': self.id, 'location_id': production_move.location_id.id, 'location_dest_id': location_dest_id, }) else: production_move._set_quantity_done(self.qty_producing) if not self.next_work_order_id: for by_product_move in self._get_byproduct_move_to_update(): if by_product_move.has_tracking != 'serial': values = self._get_byproduct_move_line(by_product_move, self.qty_producing * by_product_move.unit_factor) self.env['stock.move.line'].create(values) elif by_product_move.has_tracking == 'serial': qty_todo = by_product_move.product_uom._compute_quantity(self.qty_producing * by_product_move.unit_factor, by_product_move.product_id.uom_id) for i in range(0, int(float_round(qty_todo, precision_digits=0))): values = self._get_byproduct_move_line(by_product_move, 1) self.env['stock.move.line'].create(values) # Update workorder quantity produced self.qty_produced += self.qty_producing if self.final_lot_id: self.final_lot_id.use_next_on_work_order_id = self.next_work_order_id self.final_lot_id = False # One a piece is produced, you can launch the next work order self._start_nextworkorder() # Set a qty producing rounding = self.production_id.product_uom_id.rounding if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0: self.qty_producing = 0 elif self.production_id.product_id.tracking == 'serial': self._assign_default_final_lot_id() self.qty_producing = 1.0 self._generate_lot_ids() else: self.qty_producing = float_round(self.production_id.product_qty - self.qty_produced, precision_rounding=rounding) self._generate_lot_ids() if self.next_work_order_id and self.next_work_order_id.state not in ['done', 'cancel'] and self.production_id.product_id.tracking != 'none': self.next_work_order_id._assign_default_final_lot_id() if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0: self.button_finish() return True
def _compute_is_produced(self): rounding = self.production_id.product_uom_id.rounding self.is_produced = float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0
def _prepare_account_move_and_lines(self, session=None, move=None): res = super(PosOrder, self)._prepare_account_move_and_lines(session, move) unpaid_order = self.filtered(lambda o: o.account_move.id == res['move'].id) if unpaid_order: config_id = unpaid_order[0].config_id if config_id.cash_rounding and config_id.rounding_method: difference = 0.0 converted_amount = 0.0 config_id = unpaid_order[0].config_id company_id = unpaid_order[0].company_id different_currency = config_id.currency_id if config_id.currency_id.id != company_id.currency_id.id else False for order in unpaid_order: order_difference = order.amount_paid - order.amount_total difference += order_difference if config_id.currency_id.id != company_id.currency_id.id: converted_paid = different_currency._convert(order.amount_paid, company_id.currency_id, company_id, order.date_order) converted_total = different_currency._convert(order.amount_total, company_id.currency_id, company_id, order.date_order) converted_amount += converted_paid - converted_total else: converted_amount += order_difference if difference: profit_account = config_id.rounding_method._get_profit_account_id().id loss_account = config_id.rounding_method._get_loss_account_id().id difference_move_line = { 'name': 'Rounding Difference', 'partner_id': False, 'move_id': res['move'].id, } grouped_data_key = False if float_compare(0.0, difference, precision_rounding=config_id.currency_id.rounding) > 0: difference_move_line.update({ 'account_id': loss_account, 'credit': 0.0, 'debit': -converted_amount, }) if different_currency: difference_move_line.update({ 'currency_id': different_currency.id, 'amount_currency': -difference }) grouped_data_key = ('difference_rounding', False, loss_account, True, different_currency.id if different_currency else False) if float_compare(0.0, difference, precision_rounding=config_id.currency_id.rounding) < 0: difference_move_line.update({ 'account_id': profit_account, 'credit': converted_amount, 'debit': 0.0, }) if different_currency: difference_move_line.update({ 'currency_id': different_currency.id, 'amount_currency': difference }) grouped_data_key = ('difference_rounding', False, profit_account, False, different_currency.id if different_currency else False) if grouped_data_key: res['grouped_data'][grouped_data_key] = [difference_move_line] return res
def _action_launch_stock_rule(self): """ Launch procurement group run method with required/custom fields genrated by a sale order line. procurement group will launch '_run_pull', '_run_buy' or '_run_manufacture' depending on the sale order line product rule. """ precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') errors = [] for line in self: if line.state != 'sale' or not line.product_id.type in ('consu', 'product'): continue qty = line._get_qty_procurement() if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0: continue group_id = line.order_id.procurement_group_id if not group_id: group_id = self.env['procurement.group'].create({ 'name': line.order_id.name, 'move_type': line.order_id.picking_policy, 'sale_id': line.order_id.id, 'partner_id': line.order_id.partner_shipping_id.id, }) line.order_id.procurement_group_id = group_id else: # In case the procurement group is already created and the order was # cancelled, we need to update certain values of the group. updated_vals = {} if group_id.partner_id != line.order_id.partner_shipping_id: updated_vals.update( {'partner_id': line.order_id.partner_shipping_id.id}) if group_id.move_type != line.order_id.picking_policy: updated_vals.update( {'move_type': line.order_id.picking_policy}) if updated_vals: group_id.write(updated_vals) values = line._prepare_procurement_values(group_id=group_id) product_qty = line.product_uom_qty - qty procurement_uom = line.product_uom quant_uom = line.product_id.uom_id get_param = self.env['ir.config_parameter'].sudo().get_param if procurement_uom.id != quant_uom.id and get_param( 'stock.propagate_uom') != '1': product_qty = line.product_uom._compute_quantity( product_qty, quant_uom, rounding_method='HALF-UP') procurement_uom = quant_uom try: self.env['procurement.group'].run( line.product_id, product_qty, procurement_uom, line.order_id.partner_shipping_id.property_stock_customer, line.name, line.order_id.name, values) except UserError as error: errors.append(error.name) if errors: raise UserError('\n'.join(errors)) return True
def _generate_consumed_move_line(self, qty_to_add, final_lot, lot=False): if lot: move_lines = self.move_line_ids.filtered( lambda ml: ml.lot_id == lot and not ml.lot_produced_id) else: move_lines = self.move_line_ids.filtered( lambda ml: not ml.lot_id and not ml.lot_produced_id) # Sanity check: if the product is a serial number and `lot` is already present in the other # consumed move lines, raise. if lot and self.product_id.tracking == 'serial' and lot in self.move_line_ids.filtered( lambda ml: ml.qty_done).mapped('lot_id'): raise UserError( _('You cannot consume the same serial number twice. Please correct the serial numbers encoded.' )) for ml in move_lines: rounding = ml.product_uom_id.rounding if float_compare(qty_to_add, 0, precision_rounding=rounding) <= 0: break quantity_to_process = min(qty_to_add, ml.product_uom_qty - ml.qty_done) qty_to_add -= quantity_to_process new_quantity_done = (ml.qty_done + quantity_to_process) if float_compare(ml.product_uom_id._compute_quantity( new_quantity_done, ml.product_id.uom_id), ml.product_qty, precision_rounding=rounding) >= 0: ml.write({ 'qty_done': new_quantity_done, 'lot_produced_id': final_lot.id }) else: new_qty_reserved = ml.product_uom_qty - new_quantity_done default = { 'product_uom_qty': new_quantity_done, 'qty_done': new_quantity_done, 'lot_produced_id': final_lot.id } ml.copy(default=default) ml.with_context(bypass_reservation_update=True).write({ 'product_uom_qty': new_qty_reserved, 'qty_done': 0 }) if float_compare(qty_to_add, 0, precision_rounding=self.product_uom.rounding) > 0: # Search for a sub-location where the product is available. This might not be perfectly # correct if the quantity available is spread in several sub-locations, but at least # we should be closer to the reality. Anyway, no reservation is made, so it is still # possible to change it afterwards. quants = self.env['stock.quant']._gather(self.product_id, self.location_id, lot_id=lot, strict=False) available_quantity = self.product_id.uom_id._compute_quantity( self.env['stock.quant']._get_available_quantity( self.product_id, self.location_id, lot_id=lot, strict=False), self.product_uom) location_id = False if float_compare(qty_to_add, available_quantity, precision_rounding=self.product_uom.rounding) < 1: location_id = quants.filtered( lambda r: r.quantity > 0)[-1:].location_id vals = { 'move_id': self.id, 'product_id': self.product_id.id, 'location_id': location_id.id if location_id else self.location_id.id, 'production_id': self.raw_material_production_id.id, 'location_dest_id': self.location_dest_id.id, 'product_uom_qty': 0, 'product_uom_id': self.product_uom.id, 'qty_done': qty_to_add, 'lot_produced_id': final_lot.id, } if lot: vals.update({'lot_id': lot.id}) self.env['stock.move.line'].create(vals)
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.user.company_id.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_data in location_data.items(): location_orderpoints = location_data['orderpoints'] product_context = dict(self._context, location=location_orderpoints[0].location_id.id) substract_quantity = location_orderpoints._quantity_in_progress() for group in location_data['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_data['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(): self.env['procurement.group'].run(orderpoint.product_id, qty_rounded, orderpoint.product_uom, orderpoint.location_id, orderpoint.name, orderpoint.name, 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 {}