def _update_move_lines(self): """ update a move line to save the workorder line data""" self.ensure_one() if self.lot_id: move_lines = self.move_id.move_line_ids.filtered( lambda ml: ml.lot_id == self.lot_id and not ml.lot_produced_ids ) else: move_lines = self.move_id.move_line_ids.filtered( lambda ml: not ml.lot_id and not ml.lot_produced_ids) # Sanity check: if the product is a serial number and `lot` is already present in the other # consumed move lines, raise. if self.product_id.tracking != 'none' and not self.lot_id: raise UserError( _('Please enter a lot or serial number for %s !' % self.product_id.display_name)) if self.lot_id and self.product_id.tracking == 'serial' and self.lot_id in self.move_id.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.' )) # Update reservation and quantity done for ml in move_lines: rounding = ml.product_uom_id.rounding if float_compare(self.qty_done, 0, precision_rounding=rounding) <= 0: break quantity_to_process = min(self.qty_done, ml.product_uom_qty - ml.qty_done) self.qty_done -= quantity_to_process new_quantity_done = (ml.qty_done + quantity_to_process) # if we produce less than the reserved quantity to produce the finished products # in different lots, # we create different component_move_lines to record which one was used # on which lot of finished product if float_compare(new_quantity_done, ml.product_uom_qty, precision_rounding=rounding) >= 0: ml.write({ 'qty_done': new_quantity_done, 'lot_produced_ids': self._get_produced_lots(), }) 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_ids': self._get_produced_lots(), } ml.copy(default=default) ml.with_context(bypass_reservation_update=True).write({ 'product_uom_qty': new_qty_reserved, 'qty_done': 0 })
def _create_extra_move_lines(self): """Create new sml if quantity produced is bigger than the reserved one""" vals_list = [] quants = self.env['stock.quant']._gather(self.product_id, self.move_id.location_id, lot_id=self.lot_id, strict=False) # Search for a sub-locations where the product is available. # Loop on the quants to get the locations. If there is not enough # quantity into stock, we take the move location. Anyway, no # reservation is made, so it is still possible to change it afterwards. for quant in quants: quantity = quant.quantity - quant.reserved_quantity rounding = quant.product_uom_id.rounding if (float_compare(quant.quantity, 0, precision_rounding=rounding) <= 0 or float_compare( quantity, 0, precision_rounding=rounding) <= 0): continue vals = { 'move_id': self.move_id.id, 'product_id': self.product_id.id, 'location_id': quant.location_id.id, 'location_dest_id': self.move_id.location_dest_id.id, 'product_uom_qty': 0, 'product_uom_id': quant.product_uom_id.id, 'qty_done': min(quantity, self.qty_done), 'lot_produced_ids': self._get_produced_lots(), } if self.lot_id: vals.update({'lot_id': self.lot_id.id}) vals_list.append(vals) self.qty_done -= vals['qty_done'] # If all the qty_done is distributed, we can close the loop if float_compare( self.qty_done, 0, precision_rounding=self.product_uom_id.rounding) <= 0: break if float_compare(self.qty_done, 0, precision_rounding=self.product_uom_id.rounding) > 0: vals = { 'move_id': self.move_id.id, 'product_id': self.product_id.id, 'location_id': self.move_id.location_id.id, 'location_dest_id': self.move_id.location_dest_id.id, 'product_uom_qty': 0, 'product_uom_id': self.product_uom_id.id, 'qty_done': self.qty_done, 'lot_produced_ids': self._get_produced_lots(), } if self.lot_id: vals.update({'lot_id': self.lot_id.id}) vals_list.append(vals) return vals_list
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 _onchange_quantity_context(self): product_qty = False if self.product_id: self.product_uom_id = self.product_id.uom_id if self.product_id and self.location_id and self.product_id.uom_id.category_id == self.product_uom_id.category_id: # TDE FIXME: last part added because crash theoretical_qty = self.product_id.get_theoretical_quantity( self.product_id.id, self.location_id.id, lot_id=self.prod_lot_id.id, package_id=self.package_id.id, owner_id=self.partner_id.id, to_uom=self.product_uom_id.id, ) else: theoretical_qty = 0 # Sanity check on the lot. if self.prod_lot_id: if self.product_id.tracking == 'none' or self.product_id != self.prod_lot_id.product_id: self.prod_lot_id = False if self.prod_lot_id and self.product_id.tracking == 'serial': # We force `product_qty` to 1 for SN tracked product because it's # the only relevant value aside 0 for this kind of product. self.product_qty = 1 elif self.product_id and float_compare(self.product_qty, self.theoretical_qty, precision_rounding=self.product_uom_id.rounding) == 0: # We update `product_qty` only if it equals to `theoretical_qty` to # avoid to reset quantity when user manually set it. self.product_qty = theoretical_qty self.theoretical_qty = theoretical_qty
def action_validate(self): self.ensure_one() if self.filtered(lambda repair: any(op.product_uom_qty < 0 for op in repair.operations)): raise UserError(_("You can not enter negative quantities.")) if self.product_id.type == 'consu': return self.action_repair_confirm() precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') available_qty_owner = self.env['stock.quant']._get_available_quantity(self.product_id, self.location_id, self.lot_id, owner_id=self.partner_id, strict=True) available_qty_noown = self.env['stock.quant']._get_available_quantity(self.product_id, self.location_id, self.lot_id, strict=True) for available_qty in [available_qty_owner, available_qty_noown]: if float_compare(available_qty, self.product_qty, precision_digits=precision) >= 0: return self.action_repair_confirm() else: return { 'name': _('Insufficient Quantity'), 'view_mode': 'form', 'res_model': 'stock.warn.insufficient.qty.repair', 'view_id': self.env.ref('repair.stock_warn_insufficient_qty_repair_form_view').id, 'type': 'ir.actions.act_window', 'context': { 'default_product_id': self.product_id.id, 'default_location_id': self.location_id.id, 'default_repair_id': self.id }, 'target': 'new' }
def _compute_is_produced(self): self.is_produced = False for order in self.filtered(lambda p: p.production_id): rounding = order.production_id.product_uom_id.rounding order.is_produced = float_compare(order.qty_produced, order.production_id.product_qty, precision_rounding=rounding) >= 0
def _refresh_wo_lines(self): """ Modify exisiting workorder line in order to match the reservation on stock move line. The strategy is to remove the line that were not processed yet then call _generate_lines_values that recreate workorder line depending the reservation. """ for workorder in self: raw_moves = workorder.move_raw_ids.filtered( lambda move: move.state not in ('done', 'cancel')) wl_to_unlink = self.env['mrp.workorder.line'] for move in raw_moves: rounding = move.product_uom.rounding qty_already_consumed = 0.0 workorder_lines = workorder.raw_workorder_line_ids.filtered( lambda w: w.move_id == move) for wl in workorder_lines: if not wl.qty_done: wl_to_unlink |= wl continue qty_already_consumed += wl.qty_done qty_to_consume = self._prepare_component_quantity( move, workorder.qty_producing) wl_to_unlink.unlink() if float_compare(qty_to_consume, qty_already_consumed, precision_rounding=rounding) > 0: line_values = workorder._generate_lines_values( move, qty_to_consume - qty_already_consumed) self.env['mrp.workorder.line'].create(line_values)
def _defaults_from_finished_workorder_line(self, reference_lot_lines): for r_line in reference_lot_lines: # see which lot we could suggest and its related qty_producing if not r_line.lot_id: continue candidates = self.finished_workorder_line_ids.filtered( lambda line: line.lot_id == r_line.lot_id) rounding = self.product_uom_id.rounding if not candidates: self.write({ 'finished_lot_id': r_line.lot_id.id, 'qty_producing': r_line.qty_done, }) return True elif float_compare(candidates.qty_done, r_line.qty_done, precision_rounding=rounding) < 0: self.write({ 'finished_lot_id': r_line.lot_id.id, 'qty_producing': r_line.qty_done - candidates.qty_done, }) return True return False
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') if not order_line.is_expense} if values.get('partner_shipping_id'): new_partner = self.env['res.partner'].browse(values.get('partner_shipping_id')) for record in self: picking = record.mapped('picking_ids').filtered(lambda x: x.state not in ('done', 'cancel')) addresses = (record.partner_shipping_id.display_name, new_partner.display_name) message = _("""The delivery address has been changed on the Sales Order<br/> From <strong>"%s"</strong> To <strong>"%s"</strong>, You should probably update the partner on this document.""") % addresses picking.activity_schedule('mail.mail_activity_data_warning', note=message, user_id=self.env.user.id) 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 float_compare(order_line.product_uom_qty, pre_order_line_qty.get(order_line, 0.0), order_line.product_uom.rounding) < 0: to_log[order_line] = (order_line.product_uom_qty, pre_order_line_qty.get(order_line, 0.0)) 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 _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 action_validate(self): if not self.exists(): return self.ensure_one() if not self.user_has_groups('stock.group_stock_manager'): raise UserError(_("Only a stock manager can validate an inventory adjustment.")) if self.state != 'confirm': raise UserError(_( "You can't validate the inventory '%s', maybe this inventory " + "has been already validated or isn't ready.") % (self.name)) 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', 'views': [(False, 'form')], 'res_model': 'stock.track.confirmation', 'target': 'new', 'res_id': wiz.id, } self._action_done() self.line_ids._check_company() self._check_company() return True
def _compute_outdated(self): grouped_quants = self.env['stock.quant'].read_group( [('product_id', 'in', self.product_id.ids), ('location_id', 'in', self.location_id.ids)], ['product_id', 'location_id', 'lot_id', 'package_id', 'owner_id', 'quantity:sum'], ['product_id', 'location_id', 'lot_id', 'package_id', 'owner_id'], lazy=False) quants = { (quant['product_id'][0], quant['location_id'][0], quant['lot_id'] and quant['lot_id'][0], quant['package_id'] and quant['package_id'][0], quant['owner_id'] and quant['owner_id'][0]): quant['quantity'] for quant in grouped_quants } for line in self: if line.state == 'done' or not line.id: line.outdated = False continue qty = quants.get(( line.product_id.id, line.location_id.id, line.prod_lot_id.id, line.package_id.id, line.partner_id.id, ), 0 ) if float_compare(qty, line.theoretical_qty, precision_rounding=line.product_uom_id.rounding) != 0: line.outdated = True else: line.outdated = False
def _run_pull(self, procurements): moves_values_by_company = defaultdict(list) mtso_products_by_locations = defaultdict(list) # To handle the `mts_else_mto` procure method, we do a preliminary loop to # isolate the products we would need to read the forecasted quantity, # in order to to batch the read. We also make a sanitary check on the # `location_src_id` field. for procurement, rule in procurements: if not rule.location_src_id: msg = _('No source location defined on stock rule: %s!') % ( rule.name, ) raise UserError(msg) if rule.procure_method == 'mts_else_mto': mtso_products_by_locations[rule.location_src_id].append( procurement.product_id.id) # Get the forecasted quantity for the `mts_else_mto` procurement. forecasted_qties_by_loc = {} for location, product_ids in mtso_products_by_locations.items(): products = self.env['product.product'].browse( product_ids).with_context(location=location.id) forecasted_qties_by_loc[location] = { product.id: product.virtual_available for product in products } # Prepare the move values, adapt the `procure_method` if needed. for procurement, rule in procurements: procure_method = rule.procure_method if rule.procure_method == 'mts_else_mto': qty_needed = procurement.product_uom._compute_quantity( procurement.product_qty, procurement.product_id.uom_id) qty_available = forecasted_qties_by_loc[rule.location_src_id][ procurement.product_id.id] if float_compare(qty_needed, qty_available, precision_rounding=procurement.product_id. uom_id.rounding) <= 0: procure_method = 'make_to_stock' forecasted_qties_by_loc[rule.location_src_id][ procurement.product_id.id] -= qty_needed else: procure_method = 'make_to_order' move_values = rule._get_stock_move_values(*procurement) move_values['procure_method'] = procure_method moves_values_by_company[procurement.company_id.id].append( move_values) for company_id, moves_values in moves_values_by_company.items(): # create the move as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example) moves = self.env['stock.move'].sudo().with_context( force_company=company_id).create(moves_values) # Since action_confirm launch following procurement_group we should activate it. moves._action_confirm() return True
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._action_launch_stock_rule(previous_product_uom_qty) 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 _sale_can_be_reinvoice(self): """ determine if the generated analytic line should be reinvoiced or not. For Vendor Bill flow, if the product has a 'erinvoice policy' and is a cost, then we will find the SO on which reinvoice the AAL """ self.ensure_one() uom_precision_digits = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') return float_compare(self.credit or 0.0, self.debit or 0.0, precision_digits=uom_precision_digits ) != 1 and self.product_id.expense_policy not in [ False, 'no' ]
def _compute_allowed_lots_domain(self): """ Check if all the finished products has been assigned to a serial number or a lot in other workorders. If yes, restrict the selectable lot to the lot/sn used in other workorders. """ productions = self.mapped('production_id') treated = self.browse() for production in productions: if production.product_id.tracking == 'none': continue rounding = production.product_uom_id.rounding finished_workorder_lines = production.workorder_ids.mapped( 'finished_workorder_line_ids').filtered( lambda wl: wl.product_id == production.product_id) qties_done_per_lot = defaultdict(list) for finished_workorder_line in finished_workorder_lines: # It is possible to have finished workorder lines without a lot (eg using the dummy # test type). Ignore them when computing the allowed lots. if finished_workorder_line.lot_id: qties_done_per_lot[ finished_workorder_line.lot_id.id].append( finished_workorder_line.qty_done) qty_to_produce = production.product_qty allowed_lot_ids = self.env['stock.production.lot'] qty_produced = sum( [max(qty_dones) for qty_dones in qties_done_per_lot.values()]) if float_compare(qty_produced, qty_to_produce, precision_rounding=rounding) < 0: # If we haven't produced enough, all lots are available allowed_lot_ids = self.env['stock.production.lot'].search([ ('product_id', '=', production.product_id.id), ('company_id', '=', production.company_id.id), ]) else: # If we produced enough, only the already produced lots are available allowed_lot_ids = self.env['stock.production.lot'].browse( qties_done_per_lot.keys()) workorders = production.workorder_ids.filtered( lambda wo: wo.state not in ('done', 'cancel')) for workorder in workorders: if workorder.product_tracking == 'serial': workorder.allowed_lots_domain = allowed_lot_ids - workorder.finished_workorder_line_ids.filtered( lambda wl: wl.product_id == production.product_id ).mapped('lot_id') else: workorder.allowed_lots_domain = allowed_lot_ids treated |= workorder (self - treated).allowed_lots_domain = False
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 _strict_consumption_check(self): if self.consumption == 'strict': for move in self.move_raw_ids: lines = self._workorder_line_ids().filtered( lambda l: l.move_id == move) qty_done = sum(lines.mapped('qty_done')) qty_to_consume = sum(lines.mapped('qty_to_consume')) rounding = self.product_uom_id.rounding if float_compare(qty_done, qty_to_consume, precision_rounding=rounding) != 0: raise UserError( _('You should consume the quantity of %s defined in the BoM. If you want to consume more or less components, change the consumption setting on the BoM.' ) % lines[0].product_id.name)
def _reservation_is_updatable(self, quantity, reserved_quant): self.ensure_one() if self.lot_produced_ids: ml_remaining_qty = self.qty_done - self.product_uom_qty ml_remaining_qty = self.product_uom_id._compute_quantity( ml_remaining_qty, self.product_id.uom_id, rounding_method="HALF-UP") if float_compare( ml_remaining_qty, quantity, precision_rounding=self.product_id.uom_id.rounding) < 0: return False return super(StockMoveLine, self)._reservation_is_updatable(quantity, reserved_quant)
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 not float_is_zero( self.qty_done, self.product_uom_id.rounding): if float_compare( self.qty_done, 1.0, precision_rounding=self.product_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 _record_production(self): # Check all the product_produce line have a move id (the user can add product # to consume directly in the wizard) for line in self._workorder_line_ids(): if not line.move_id: # Find move_id that would match if line.raw_product_produce_id: moves = self.move_raw_ids else: moves = self.move_finished_ids move_id = moves.filtered(lambda m: m.product_id == line.product_id and m.state not in ('done', 'cancel')) if not move_id: # create a move to assign it to the line if line.raw_product_produce_id: values = { 'name': self.production_id.name, 'reference': self.production_id.name, 'product_id': line.product_id.id, 'product_uom': line.product_uom_id.id, 'location_id': self.production_id.location_src_id.id, 'location_dest_id': line.product_id.property_stock_production.id, 'raw_material_production_id': self.production_id.id, 'group_id': self.production_id.procurement_group_id.id, 'origin': self.production_id.name, 'state': 'confirmed', 'company_id': self.production_id.company_id.id, } else: values = self.production_id._get_finished_move_value(line.product_id.id, 0, line.product_uom_id.id) move_id = self.env['stock.move'].create(values) line.move_id = move_id.id # because of an ORM limitation (fields on transient models are not # recomputed by updates in non-transient models), the related fields on # this model are not recomputed by the creations above self.invalidate_cache(['move_raw_ids', 'move_finished_ids']) # Save product produce lines data into stock moves/move lines quantity = self.qty_producing 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) self._update_finished_move() self._update_moves() if self.production_id.state == 'confirmed': self.production_id.write({ 'date_start': datetime.now(), })
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._get_cash_basis_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 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') location_id = self.location_id if self.picking_id and self.picking_id.picking_type_code == 'incoming': location_id = self.picking_id.location_dest_id available_qty = sum(self.env['stock.quant']._gather( self.product_id, 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: ctx = dict(self.env.context) ctx.update({ 'default_product_id': self.product_id.id, 'default_location_id': self.location_id.id, 'default_scrap_id': self.id }) return { 'name': _('Insufficient Quantity'), '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': ctx, 'target': 'new' }
def _action_launch_stock_rule(self, previous_product_uom_qty=False): """ 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') procurements = [] for line in self: if line.state != 'sale' or not line.product_id.type in ('consu','product'): continue qty = line._get_qty_procurement(previous_product_uom_qty) if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0: continue group_id = line._get_procurement_group() if not group_id: group_id = self.env['procurement.group'].create(line._prepare_procurement_group_vals()) 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 line_uom = line.product_uom quant_uom = line.product_id.uom_id product_qty, procurement_uom = line_uom._adjust_uom_quantities(product_qty, quant_uom) procurements.append(self.env['procurement.group'].Procurement( line.product_id, product_qty, procurement_uom, line.order_id.partner_shipping_id.property_stock_customer, line.name, line.order_id.name, line.order_id.company_id, values)) if procurements: self.env['procurement.group'].run(procurements) return True
def _check_amount_and_confirm_order(self): self.ensure_one() for order in self.sale_order_ids.filtered(lambda so: so.state in ('draft', 'sent')): if float_compare(self.amount, order.amount_total, 2) == 0: order.with_context(send_email=True).action_confirm() else: _logger.warning( '<%s> transaction AMOUNT MISMATCH for order %s (ID %s): expected %r, got %r', self.acquirer_id.provider,order.name, order.id, order.amount_total, self.amount, ) order.message_post( subject=_("Amount Mismatch (%s)") % self.acquirer_id.provider, body=_("The order was not confirmed despite response from the acquirer (%s): order total is %r but acquirer replied with %r.") % ( self.acquirer_id.provider, order.amount_total, self.amount, ) )
def _select_seller(self, partner_id=False, quantity=0.0, date=None, uom_id=False, params=False): self.ensure_one() if date is None: date = fields.Date.context_today(self) precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') res = self.env['product.supplierinfo'] sellers = self._prepare_sellers(params) if self.env.context.get('force_company'): sellers = sellers.filtered( lambda s: not s.company_id or s.company_id.id == self.env. context['force_company']) for seller in sellers: # Set quantity in UoM of seller quantity_uom_seller = quantity if quantity_uom_seller and uom_id and uom_id != seller.product_uom: quantity_uom_seller = uom_id._compute_quantity( quantity_uom_seller, seller.product_uom) if seller.date_start and seller.date_start > date: continue if seller.date_end and seller.date_end < date: continue if partner_id and seller.name not in [ partner_id, partner_id.parent_id ]: continue if float_compare(quantity_uom_seller, seller.min_qty, precision_digits=precision) == -1: continue if seller.product_id and seller.product_id != self: continue if not res or res.name == seller.name: res |= seller return res.sorted('price')[:1]
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_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 _update_moves(self): """ Once the production is done. Modify the workorder lines into stock move line with the registered lot and quantity done. """ # Before writting produce quantities, we ensure they respect the bom strictness self._strict_consumption_check() vals_list = [] workorder_lines_to_process = self._workorder_line_ids().filtered( lambda line: line.product_id != self.product_id and line.qty_done > 0) for line in workorder_lines_to_process: line._update_move_lines() if float_compare( line.qty_done, 0, precision_rounding=line.product_uom_id.rounding) > 0: vals_list += line._create_extra_move_lines() self._workorder_line_ids().filtered( lambda line: line.product_id != self.product_id).unlink() self.env['stock.move.line'].create(vals_list)
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')