def test_predictive_lead_scoring(self): """ We test here computation of lead probability based on PLS Bayes. We will use 3 different values for each possible variables: country_id : 1,2,3 state_id: 1,2,3 email_state: correct, incorrect, None phone_state: correct, incorrect, None source_id: 1,2,3 stage_id: 1,2,3 + the won stage And we will compute all of this for 2 different team_id Note : We assume here that original bayes computation is correct as we don't compute manually the probabilities.""" Lead = self.env['crm.lead'] state_values = ['correct', 'incorrect', None] source_ids = self.env['utm.source'].search([], limit=3).ids state_ids = self.env['res.country.state'].search([], limit=3).ids country_ids = self.env['res.country'].search([], limit=3).ids stage_ids = self.env['crm.stage'].search([], limit=3).ids team_ids = self.env['crm.team'].create([{'name': 'Team Test 1'}, {'name': 'Team Test 2'}]).ids # create bunch of lost and won crm_lead leads_to_create = [] # for team 1 for i in range(3): leads_to_create.append(self._get_lead_values(team_ids[0], 'team_1_%s' % str(i), country_ids[i], state_ids[i], state_values[i], state_values[i], source_ids[i], stage_ids[i])) leads_to_create.append( self._get_lead_values(team_ids[0], 'team_1_%s' % str(3), country_ids[0], state_ids[1], state_values[2], state_values[0], source_ids[2], stage_ids[1])) # for team 2 leads_to_create.append( self._get_lead_values(team_ids[1], 'team_2_%s' % str(0), country_ids[0], state_ids[1], state_values[2], state_values[0], source_ids[1], stage_ids[2])) leads_to_create.append( self._get_lead_values(team_ids[1], 'team_2_%s' % str(1), country_ids[0], state_ids[1], state_values[0], state_values[1], source_ids[2], stage_ids[1])) leads_to_create.append( self._get_lead_values(team_ids[1], 'team_2_%s' % str(2), country_ids[0], state_ids[2], state_values[0], state_values[1], source_ids[2], stage_ids[0])) leads_to_create.append( self._get_lead_values(team_ids[1], 'team_2_%s' % str(3), country_ids[0], state_ids[1], state_values[2], state_values[0], source_ids[2], stage_ids[1])) leads = Lead.create(leads_to_create) # Set the PLS config self.env['ir.config_parameter'].sudo().set_param("crm.pls_start_date", "2000-01-01") self.env['ir.config_parameter'].sudo().set_param("crm.pls_fields", "country_id,state_id,email_state,phone_state,source_id") # set leads as won and lost # for Team 1 leads[0].action_set_lost() leads[1].action_set_lost() leads[2].action_set_won() # for Team 2 leads[4].action_set_lost() leads[5].action_set_lost() leads[6].action_set_won() # rebuild frequencies table and recompute automated_probability for all leads. Lead._cron_update_automated_probabilities() # As the cron is computing and writing in SQL queries, we need to invalidate the cache leads.invalidate_cache() self.assertEquals(tools.float_compare(leads[3].automated_probability, 33.49, 2), 0) self.assertEquals(tools.float_compare(leads[7].automated_probability, 7.74, 2), 0)
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 _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 _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 _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 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 _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 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 _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 _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 _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 _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 _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 write(self, values): res = super().write(values) for bom in self: if float_compare( bom.product_qty, 0, precision_rounding=bom.product_uom_id.rounding) <= 0: raise UserError(_('The quantity to produce must be positive!')) return res
def _check_main_currency_rounding(self): if any(precision.name == 'Account' and tools.float_compare(self.env.company.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_amount(self): if float_compare(self.amount_max, self.amount, precision_rounding=self.currency_id.rounding or 0.01) == -1: raise ValidationError( _("Please set an amount smaller than %s.") % (self.amount_max)) if self.amount <= 0: raise ValidationError( _("The value of the payment amount must be positive."))
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 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 _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 _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 _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 _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 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') documents = { k: v for k, v in documents.items() if k[0].state != 'cancel' } order._log_decrease_ordered_quantity(documents) return res
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 test_channel_statistics(self): channel_publisher = self.channel.with_user(self.user_publisher) # slide type computation self.assertEqual(channel_publisher.total_slides, len(channel_publisher.slide_content_ids)) self.assertEqual(channel_publisher.nbr_infographic, len(channel_publisher.slide_content_ids.filtered(lambda s: s.slide_type == 'infographic'))) self.assertEqual(channel_publisher.nbr_presentation, len(channel_publisher.slide_content_ids.filtered(lambda s: s.slide_type == 'presentation'))) self.assertEqual(channel_publisher.nbr_document, len(channel_publisher.slide_content_ids.filtered(lambda s: s.slide_type == 'document'))) self.assertEqual(channel_publisher.nbr_video, len(channel_publisher.slide_content_ids.filtered(lambda s: s.slide_type == 'video'))) # slide statistics computation self.assertEqual(float_compare(channel_publisher.total_time, sum(s.completion_time for s in channel_publisher.slide_content_ids), 3), 0) # members computation self.assertEqual(channel_publisher.members_count, 1) channel_publisher.action_add_member() self.assertEqual(channel_publisher.members_count, 1) channel_publisher._action_add_members(self.user_emp.partner_id) channel_publisher.invalidate_cache(['partner_ids']) self.assertEqual(channel_publisher.members_count, 2) self.assertEqual(channel_publisher.partner_ids, self.user_publisher.partner_id | self.user_emp.partner_id)
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 _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.free_qty 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