def default_get(self, fields): res = super(MrpProductProduce, self).default_get(fields) if self._context and self._context.get('active_id'): production = self.env['mrp.production'].browse(self._context['active_id']) serial_finished = (production.product_id.tracking == 'serial') if serial_finished: todo_quantity = 1.0 else: main_product_moves = production.move_finished_ids.filtered(lambda x: x.product_id.id == production.product_id.id) todo_quantity = production.product_qty - sum(main_product_moves.mapped('quantity_done')) todo_quantity = todo_quantity if (todo_quantity > 0) else 0 if 'production_id' in fields: res['production_id'] = production.id if 'product_id' in fields: res['product_id'] = production.product_id.id if 'product_uom_id' in fields: res['product_uom_id'] = production.product_uom_id.id if 'serial' in fields: res['serial'] = bool(serial_finished) if 'product_qty' in fields: res['product_qty'] = todo_quantity if 'produce_line_ids' in fields: lines = [] for move in production.move_raw_ids.filtered(lambda x: (x.product_id.tracking != 'none') and x.state not in ('done', 'cancel')): qty_to_consume = todo_quantity / move.bom_line_id.bom_id.product_qty * move.bom_line_id.product_qty for move_line in move.move_line_ids: if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) < 0: break 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': 0.0, 'lot_id': move_line.lot_id.id, 'product_uom_id': move.product_uom.id, 'product_id': move.product_id.id, }) 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': 0.0, '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': 0.0, 'product_uom_id': move.product_uom.id, 'product_id': move.product_id.id, }) res['produce_line_ids'] = [(0, 0, x) for x in lines] return res
def write(self, values): lines = False changed_lines = False 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 float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1) changed_lines = self.filtered( lambda r: r.state == 'sale' and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) != 0) if changed_lines: orders = self.mapped('order_id') for order in orders: order_lines = changed_lines.filtered(lambda x: x.order_id == order) msg = "" if any([values['product_uom_qty'] < x.product_uom_qty for x in order_lines]): msg += "<b>" + _('The ordered quantity has been decreased. Do not forget to take it into account on your invoices and delivery orders.') + '</b>' msg += "<ul>" for line in order_lines: msg += "<li> %s:" % (line.product_id.display_name,) msg += "<br/>" + _("Ordered Quantity") + ": %s -> %s <br/>" % (line.product_uom_qty, float(values['product_uom_qty']),) if line.product_id.type in ('consu', 'product'): msg += _("Delivered Quantity") + ": %s <br/>" % (line.qty_delivered,) msg += _("Invoiced Quantity") + ": %s <br/>" % (line.qty_invoiced,) msg += "</ul>" order.message_post(body=msg) result = super(SaleOrderLine, self).write(values) if lines: lines._action_procurement_create() return result
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 = {} order_lines_to_run = self.env['sale.order.line'] for order_line in order.order_line: if order_line not in pre_order_line_qty: order_lines_to_run |= order_line elif 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]) elif float_compare(order_line.product_uom_qty, pre_order_line_qty[order_line], order_line.product_uom.rounding) > 0: order_lines_to_run |= 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) if order_lines_to_run: order_lines_to_run._action_launch_stock_rule(pre_order_line_qty) return res
def _check_availability_warning(self, product_id, product_qty, ignore_warehouse=False): if product_id.type == 'product': precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') product_by_wh = 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' ) if float_compare(product_by_wh.virtual_available, product_qty, precision_digits=precision) == -1: is_available = self._check_routing(product_id) 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, product_id.name, product_by_wh.virtual_available, product_by_wh.uom_id.name, self.order_id.warehouse_id.name) # We check if some products are available in other warehouses. if not ignore_warehouse and float_compare(product_by_wh.virtual_available, product_id.virtual_available, precision_digits=precision) == -1: message += _('\nThere are %s %s available across all warehouses.\n\n') % \ (product_id.virtual_available, product_by_wh.uom_id.name) for warehouse in self.env['stock.warehouse'].search([]): quantity = product_id.with_context(warehouse=warehouse.id).virtual_available if quantity > 0: message += "%s: %s %s\n" % (warehouse.name, quantity, product_id.uom_id.name) warning_mess = { 'title': _('Not enough inventory!'), 'message': message } return {'warning': warning_mess} return {}
def move_validate(self): """ Validate moves based on a production order. """ moves = self._filter_closed_moves() quant_obj = self.env["stock.quant"] moves_todo = self.env["stock.move"] moves_to_unreserve = self.env["stock.move"] # Create extra moves where necessary for move in moves: rounding = move.product_uom.rounding if float_compare(move.quantity_done, 0.0, precision_rounding=rounding) <= 0: continue moves_todo |= move moves_todo |= move._create_extra_move() # Split moves where necessary and move quants for move in moves_todo: rounding = move.product_uom.rounding if float_compare(move.quantity_done, move.product_uom_qty, precision_rounding=rounding) < 0: # Need to do some kind of conversion here qty_split = move.product_uom._compute_quantity( move.product_uom_qty - move.quantity_done, move.product_id.uom_id ) new_move = move.split(qty_split) # If you were already putting stock.move.lots on the next one in the work order, transfer those to the new move move.move_lot_ids.filtered(lambda x: not x.done_wo or x.quantity_done == 0.0).write( {"move_id": new_move} ) self.browse(new_move).quantity_done = 0.0 main_domain = [("qty", ">", 0)] preferred_domain = [("reservation_id", "=", move.id)] fallback_domain = [("reservation_id", "=", False)] fallback_domain2 = ["&", ("reservation_id", "!=", move.id), ("reservation_id", "!=", False)] preferred_domain_list = [preferred_domain] + [fallback_domain] + [fallback_domain2] if move.has_tracking == "none": quants = quant_obj.quants_get_preferred_domain( move.product_qty, move, domain=main_domain, preferred_domain_list=preferred_domain_list ) self.env["stock.quant"].quants_move(quants, move, move.location_dest_id) else: for movelot in move.move_lot_ids: if float_compare(movelot.quantity_done, 0, precision_rounding=rounding) > 0: if not movelot.lot_id: raise UserError(_("You need to supply a lot/serial number.")) qty = move.product_uom._compute_quantity(movelot.quantity_done, move.product_id.uom_id) quants = quant_obj.quants_get_preferred_domain( qty, move, lot_id=movelot.lot_id.id, domain=main_domain, preferred_domain_list=preferred_domain_list, ) self.env["stock.quant"].quants_move( quants, move, move.location_dest_id, lot_id=movelot.lot_id.id ) moves_to_unreserve |= move # Next move in production order if move.move_dest_id: move.move_dest_id.action_assign() moves_to_unreserve.quants_unreserve() moves_todo.write({"state": "done", "date": fields.Datetime.now()}) return moves_todo
def _prepare_move(self, line): category_id = line.asset_id.category_id depreciation_date = self.env.context.get('depreciation_date') or line.depreciation_date or fields.Date.context_today(self) company_currency = line.asset_id.company_id.currency_id current_currency = line.asset_id.currency_id prec = company_currency.decimal_places amount = current_currency.with_context(date=depreciation_date).compute(line.amount, company_currency) asset_name = line.asset_id.name + ' (%s/%s)' % (line.sequence, len(line.asset_id.depreciation_line_ids)) move_line_1 = { 'name': asset_name, 'account_id': category_id.account_depreciation_id.id, 'debit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, 'credit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, 'partner_id': line.asset_id.partner_id.id, 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'sale' else False, 'currency_id': company_currency != current_currency and current_currency.id or False, 'amount_currency': company_currency != current_currency and - 1.0 * line.amount or 0.0, } move_line_2 = { 'name': asset_name, 'account_id': category_id.account_depreciation_expense_id.id, 'credit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, 'debit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, 'partner_id': line.asset_id.partner_id.id, 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'purchase' else False, 'currency_id': company_currency != current_currency and current_currency.id or False, 'amount_currency': company_currency != current_currency and line.amount or 0.0, } move_vals = { 'ref': line.asset_id.code, 'date': depreciation_date or False, 'journal_id': category_id.journal_id.id, 'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)], } return move_vals
def money_transfer_draft(self): '''转账单的反审核按钮,外币要考虑是转入还是转出''' self.ensure_one() decimal_amount = self.env.ref('core.decimal_amount') for line in self.line_ids: if line.currency_amount > 0: if line.in_bank_id.currency_id: # 如果填充了转入账户的币别,则说明转入账户为外币 if float_compare(line.in_bank_id.balance, line.currency_amount, precision_digits=decimal_amount.digits) == -1: raise UserError('转入账户余额不足。\n转入账户余额:%s 本次转出外币金额:%s' % (line.in_bank_id.balance, line.currency_amount)) else: # 转入账户余额充足 line.in_bank_id.balance -= line.currency_amount line.out_bank_id.balance += line.amount else: # 转入账户为本位币 if float_compare(line.in_bank_id.balance, line.amount, precision_digits=decimal_amount.digits) == -1: raise UserError('转入账户余额不足。\n转入账户余额:%s 本次转出金额:%s' % (line.in_bank_id.balance, line.amount)) else: line.in_bank_id.balance -= line.amount line.out_bank_id.balance += line.currency_amount else: # 转入/转出账户都为本位币 if float_compare(line.in_bank_id.balance, line.amount, precision_digits=decimal_amount.digits) == -1: raise UserError('转入账户余额不足。\n转入账户余额:%s 本次转出金额:%s' % (line.in_bank_id.balance, line.amount)) else: line.in_bank_id.balance -= line.amount line.out_bank_id.balance += line.amount self.state = 'draft' return True
def _generate_lines_values(self, move, qty_to_consume): """ Create workorder line. First generate line based on the reservation, in order to match the reservation. If the quantity to consume is greater than the reservation quantity then create line with the correct quantity to consume but without lot or serial number. """ lines = [] is_tracked = move.product_id.tracking != 'none' for move_line in move.move_line_ids: if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) <= 0: break # move line already 'used' in workorder (from its lot for instance) 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 # search wo line on which the lot is not fully consumed or other reserved lot linked_wo_line = self.workorder_line_ids.filtered( lambda line: line.product_id == move_line.product_id and line.lot_id == move_line.lot_id ) if linked_wo_line: if float_compare(sum(linked_wo_line.mapped('qty_to_consume')), move_line.product_uom_qty - move_line.qty_done, precision_rounding=move.product_uom.rounding) < 0: to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty - move_line.qty_done - sum(linked_wo_line.mapped('qty_to_consume'))) else: continue else: to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty - move_line.qty_done) line = { 'move_id': move.id, 'product_id': move.product_id.id, 'product_uom_id': is_tracked and move.product_id.uom_id.id or move.product_uom.id, 'qty_to_consume': to_consume_in_line, 'qty_reserved': to_consume_in_line, 'lot_id': move_line.lot_id.id, 'qty_done': to_consume_in_line } lines.append(line) qty_to_consume -= to_consume_in_line # The move has not reserved the whole quantity so we create new wo lines 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: line = { 'move_id': move.id, 'product_id': move.product_id.id, 'product_uom_id': move.product_id.uom_id.id, 'qty_to_consume': 1, 'qty_done': 1, } lines.append(line) qty_to_consume -= 1 else: line = { 'move_id': move.id, 'product_id': move.product_id.id, 'product_uom_id': move.product_uom.id, 'qty_to_consume': qty_to_consume, 'qty_done': qty_to_consume } lines.append(line) return lines
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) 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 but you only have %s %s available in %s warehouse.') % \ (self.product_uom_qty, self.product_uom.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 _compute_invoice_status(self): """ Compute the invoice status of a SO line. Possible statuses: - no: if the SO is not in status 'sale' or 'done', we consider that there is nothing to invoice. This is also hte default value if the conditions of no other status is met. - to invoice: we refer to the quantity to invoice of the line. Refer to method `_get_to_invoice_qty()` for more information on how this quantity is calculated. - upselling: this is possible only for a product invoiced on ordered quantities for which we delivered more than expected. The could arise if, for example, a project took more time than expected but we decided not to invoice the extra cost to the client. This occurs onyl in state 'sale', so that when a SO is set to done, the upselling opportunity is removed from the list. - invoiced: the quantity invoiced is larger or equal to the quantity ordered. """ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') for line in self: if line.state not in ('sale', 'done'): line.invoice_status = 'no' elif not float_is_zero(line.qty_to_invoice, precision_digits=precision): line.invoice_status = 'to invoice' elif line.state == 'sale' and line.product_id.invoice_policy == 'order' and\ float_compare(line.qty_delivered, line.product_uom_qty, precision_digits=precision) == 1: line.invoice_status = 'upselling' elif float_compare(line.qty_invoiced, line.product_uom_qty, precision_digits=precision) >= 0: line.invoice_status = 'invoiced' else: line.invoice_status = 'no'
def _quant_create_from_move(self, qty, move, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, force_location_from=False, force_location_to=False): quant = super(StockQuant, self)._quant_create_from_move(qty, move, lot_id=lot_id, owner_id=owner_id, src_package_id=src_package_id, dest_package_id=dest_package_id, force_location_from=force_location_from, force_location_to=force_location_to) quant._account_entry_move(move) if move.product_id.valuation == 'real_time': # If the precision required for the variable quant cost is larger than the accounting # precision, inconsistencies between the stock valuation and the accounting entries # may arise. # For example, a box of 13 units is bought 15.00. If the products leave the # stock one unit at a time, the amount related to the cost will correspond to # round(15/13, 2)*13 = 14.95. To avoid this case, we split the quant in 12 + 1, then # record the difference on the new quant. # We need to make sure to able to extract at least one unit of the product. There is # an arbitrary minimum quantity set to 2.0 from which we consider we can extract a # unit and adapt the cost. curr_rounding = move.company_id.currency_id.rounding cost_rounded = float_round(quant.cost, precision_rounding=curr_rounding) cost_correct = cost_rounded if float_compare(quant.product_id.uom_id.rounding, 1.0, precision_digits=1) == 0\ and float_compare(quant.qty * quant.cost, quant.qty * cost_rounded, precision_rounding=curr_rounding) != 0\ and float_compare(quant.qty, 2.0, precision_rounding=quant.product_id.uom_id.rounding) >= 0: quant_correct = quant._quant_split(quant.qty - 1.0) cost_correct += (quant.qty * quant.cost) - (quant.qty * cost_rounded) quant.sudo().write({'cost': cost_rounded}) quant_correct.sudo().write({'cost': cost_correct}) return quant
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 _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(new_quantity_done, ml.product_uom_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) < 0: 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 _check_holidays(self): for holiday in self: if holiday.holiday_type != 'employee' or holiday.type != 'remove' or not holiday.employee_id or holiday.holiday_status_id.limit: continue leave_days = holiday.holiday_status_id.get_days(holiday.employee_id.id)[holiday.holiday_status_id.id] if float_compare(leave_days['remaining_leaves'], 0, precision_digits=2) == -1 or \ float_compare(leave_days['virtual_remaining_leaves'], 0, precision_digits=2) == -1: raise ValidationError(_('The number of remaining leaves is not sufficient for this leave type.\n' 'Please verify also the leaves waiting for validation.'))
def _check_holidays(self): for holiday in self: if holiday.holiday_type != 'employee' or not holiday.employee_id or holiday.holiday_status_id.allocation_type == 'no': continue leave_days = holiday.holiday_status_id.get_days(holiday.employee_id.id)[holiday.holiday_status_id.id] if float_compare(leave_days['remaining_leaves'], 0, precision_digits=2) == -1 or \ float_compare(leave_days['virtual_remaining_leaves'], 0, precision_digits=2) == -1: raise ValidationError(_('The number of remaining time off is not sufficient for this time off type.\n' 'Please also check the time off waiting for validation.'))
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 record_production(self): if not self: return True self.ensure_one() if float_compare(self.qty_producing, 0, precision_rounding=self.product_uom_id.rounding) <= 0: raise UserError(_('Please set the quantity you are currently producing. It should be different from zero.')) # If last work order, then post lots used if not self.next_work_order_id: self._update_finished_move() # Transfer quantities from temporary to final move line or make them final self._update_moves() # Transfer lot (if present) and quantity produced to a finished workorder line if self.product_tracking != 'none': self._create_or_update_finished_line() # Update workorder quantity produced self.qty_produced += self.qty_producing # Suggest a finished lot on the next workorder if self.next_work_order_id and self.production_id.product_id.tracking != 'none' and not self.next_work_order_id.finished_lot_id: self.next_work_order_id._defaults_from_finished_workorder_line(self.finished_workorder_line_ids) # As we may have changed the quantity to produce on the next workorder, # make sure to update its wokorder lines self.next_work_order_id._apply_update_workorder_lines() # One a piece is produced, you can launch the next work order self._start_nextworkorder() # Test if the production is done rounding = self.production_id.product_uom_id.rounding if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) < 0: previous_wo = self.env['mrp.workorder'] if self.product_tracking != 'none': previous_wo = self.env['mrp.workorder'].search([ ('next_work_order_id', '=', self.id) ]) candidate_found_in_previous_wo = False if previous_wo: candidate_found_in_previous_wo = self._defaults_from_finished_workorder_line(previous_wo.finished_workorder_line_ids) if not candidate_found_in_previous_wo: # self is the first workorder self.qty_producing = self.qty_remaining self.finished_lot_id = False if self.product_tracking == 'serial': self.qty_producing = 1 self._apply_update_workorder_lines() else: self.qty_producing = 0 self.button_finish() return True
def _wrong_receipt_done(self): if self.state == 'done': raise UserError(u'请不要重复审核!') batch_one_list_wh = [] batch_one_list = [] for line in self.line_in_ids: if line.amount < 0: raise UserError(u'购货金额不能小于 0!请修改。') if line.goods_id.force_batch_one: wh_move_lines = self.env['wh.move.line'].search( [('state', '=', 'done'), ('type', '=', 'in'), ('goods_id', '=', line.goods_id.id)]) for move_line in wh_move_lines: if (move_line.goods_id.id, move_line.lot) not in batch_one_list_wh and move_line.lot: batch_one_list_wh.append( (move_line.goods_id.id, move_line.lot)) if (line.goods_id.id, line.lot) in batch_one_list_wh: raise UserError(u'仓库已存在相同序列号的商品!\n商品:%s 序列号:%s' % (line.goods_id.name, line.lot)) for line in self.line_in_ids: if line.goods_qty <= 0 or line.price_taxed < 0: raise UserError(u'商品 %s 的数量和含税单价不能小于0!' % line.goods_id.name) if line.goods_id.force_batch_one: if line.goods_qty > 1: raise UserError(u'商品 %s 进行了序列号管理,数量必须为1' % line.goods_id.name) batch_one_list.append((line.goods_id.id, line.lot)) if len(batch_one_list) > len(set(batch_one_list)): raise UserError(u'不能创建相同序列号的商品!\n 序列号列表为%s' % [lot[1] for lot in batch_one_list]) for line in self.line_out_ids: if line.amount < 0: raise UserError(u'退货金额不能小于 0!请修改。') if line.goods_qty <= 0 or line.price_taxed < 0: raise UserError(u'商品 %s 的数量和含税单价不能小于0!' % line.goods_id.name) if not self.bank_account_id and self.payment: raise UserError(u'付款额不为空时,请选择结算账户!') decimal_amount = self.env.ref('core.decimal_amount') if float_compare(self.payment, self.amount, precision_digits=decimal_amount.digits) == 1: raise UserError(u'本次付款金额不能大于折后金额!\n付款金额:%s 折后金额:%s' % (self.payment, self.amount)) if float_compare(sum(cost_line.amount for cost_line in self.cost_line_ids), sum(line.share_cost for line in self.line_in_ids), precision_digits=decimal_amount.digits) != 0: raise UserError(u'采购费用还未分摊或分摊不正确!\n采购费用:%s 分摊总费用:%s' % (sum(cost_line.amount for cost_line in self.cost_line_ids), sum(line.share_cost for line in self.line_in_ids))) return
def move_validate(self): ''' Validate moves based on a production order. ''' moves = self._filter_closed_moves() quant_obj = self.env['stock.quant'] moves_todo = self.env['stock.move'] moves_to_unreserve = self.env['stock.move'] # Create extra moves where necessary for move in moves: # Here, the `quantity_done` was already rounded to the product UOM by the `do_produce` wizard. However, # it is possible that the user changed the value before posting the inventory by a value that should be # rounded according to the move's UOM. In this specific case, we chose to round up the value, because it # is what is expected by the user (if i consumed/produced a little more, the whole UOM unit should be # consumed/produced and the moves are split correctly). rounding = move.product_uom.rounding move.quantity_done = float_round(move.quantity_done, precision_rounding=rounding, rounding_method ='UP') if move.quantity_done <= 0: continue moves_todo |= move moves_todo |= move._create_extra_move() # Split moves where necessary and move quants for move in moves_todo: rounding = move.product_uom.rounding if float_compare(move.quantity_done, move.product_uom_qty, precision_rounding=rounding) < 0: # Need to do some kind of conversion here qty_split = move.product_uom._compute_quantity(move.product_uom_qty - move.quantity_done, move.product_id.uom_id) new_move = move.split(qty_split) # If you were already putting stock.move.lots on the next one in the work order, transfer those to the new move move.move_lot_ids.filtered(lambda x: not x.done_wo or x.quantity_done == 0.0).write({'move_id': new_move}) self.browse(new_move).quantity_done = 0.0 main_domain = [('qty', '>', 0)] preferred_domain = [('reservation_id', '=', move.id)] fallback_domain = [('reservation_id', '=', False)] fallback_domain2 = ['&', ('reservation_id', '!=', move.id), ('reservation_id', '!=', False)] preferred_domain_list = [preferred_domain] + [fallback_domain] + [fallback_domain2] if move.has_tracking == 'none': quants = quant_obj.quants_get_preferred_domain(move.product_qty, move, domain=main_domain, preferred_domain_list=preferred_domain_list) self.env['stock.quant'].quants_move(quants, move, move.location_dest_id) else: for movelot in move.move_lot_ids: if float_compare(movelot.quantity_done, 0, precision_rounding=rounding) > 0: if not movelot.lot_id: raise UserError(_('You need to supply a lot/serial number.')) qty = move.product_uom._compute_quantity(movelot.quantity_done, move.product_id.uom_id) quants = quant_obj.quants_get_preferred_domain(qty, move, lot_id=movelot.lot_id.id, domain=main_domain, preferred_domain_list=preferred_domain_list) self.env['stock.quant'].quants_move(quants, move, move.location_dest_id, lot_id = movelot.lot_id.id) moves_to_unreserve |= move # Next move in production order if move.move_dest_id: move.move_dest_id.action_assign() moves_to_unreserve.quants_unreserve() moves_todo.write({'state': 'done', 'date': fields.Datetime.now()}) return moves_todo
def create_move(self, post_move=True): created_moves = self.env["account.move"] for line in self: category_id = line.asset_id.category_id depreciation_date = ( self.env.context.get("depreciation_date") or line.depreciation_date or fields.Date.context_today(self) ) company_currency = line.asset_id.company_id.currency_id current_currency = line.asset_id.currency_id amount = current_currency.compute(line.amount, company_currency) sign = (category_id.journal_id.type == "purchase" or category_id.journal_id.type == "sale" and 1) or -1 asset_name = line.asset_id.name + " (%s/%s)" % (line.sequence, len(line.asset_id.depreciation_line_ids)) prec = self.env["decimal.precision"].precision_get("Account") move_line_1 = { "name": asset_name, "account_id": category_id.account_depreciation_id.id, "debit": 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, "credit": amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, "journal_id": category_id.journal_id.id, "partner_id": line.asset_id.partner_id.id, "analytic_account_id": category_id.account_analytic_id.id if category_id.type == "sale" else False, "currency_id": company_currency != current_currency and current_currency.id or False, "amount_currency": company_currency != current_currency and -sign * line.amount or 0.0, } move_line_2 = { "name": asset_name, "account_id": category_id.account_depreciation_expense_id.id, "credit": 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, "debit": amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, "journal_id": category_id.journal_id.id, "partner_id": line.asset_id.partner_id.id, "analytic_account_id": category_id.account_analytic_id.id if category_id.type == "purchase" else False, "currency_id": company_currency != current_currency and current_currency.id or False, "amount_currency": company_currency != current_currency and sign * line.amount or 0.0, } move_vals = { "ref": line.asset_id.code, "date": depreciation_date or False, "journal_id": category_id.journal_id.id, "line_ids": [(0, 0, move_line_1), (0, 0, move_line_2)], } move = self.env["account.move"].create(move_vals) line.write({"move_id": move.id, "move_check": True}) created_moves |= move if post_move and created_moves: created_moves.filtered( lambda m: any(m.asset_depreciation_ids.mapped("asset_id.category_id.open_asset")) ).post() return [x.id for x in created_moves]
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_id': self._get_final_lot().id, } 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_id': self._get_final_lot().id, } if self.lot_id: vals.update({'lot_id': self.lot_id.id}) vals_list.append(vals) return vals_list
def _action_procurement_create(self): """ Create procurements based on quantity ordered. If the quantity is increased, new procurements are created. If the quantity is decreased, no automated action is taken. """ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') new_procs = self.env['procurement.order'] # Empty recordset for line in self: if line.state != 'sale' or not line.product_id._need_procurement(): continue qty = 0.0 for proc in line.procurement_ids: qty += proc.product_qty if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0: continue if not line.order_id.procurement_group_id: vals = line.order_id._prepare_procurement_group() line.order_id.procurement_group_id = self.env["procurement.group"].create(vals) vals = line._prepare_order_line_procurement(group_id=line.order_id.procurement_group_id.id) vals['product_qty'] = line.product_uom_qty - qty new_proc = self.env["procurement.order"].create(vals) new_proc.message_post_with_view('mail.message_origin_link', values={'self': new_proc, 'origin': line.order_id}, subtype_id=self.env.ref('mail.mt_note').id) new_procs += new_proc new_procs.run() return new_procs
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 do_produce(self): # Nothing to do for lots since values are created using default data (stock.move.lots) moves = self.production_id.move_raw_ids quantity = self.product_qty if float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) <= 0: raise UserError(_('You should at least produce some quantity')) for move in moves.filtered(lambda x: x.product_id.tracking == 'none' and x.state not in ('done', 'cancel')): if move.unit_factor: rounding = move.product_uom.rounding move.quantity_done_store += float_round(quantity * move.unit_factor, precision_rounding=rounding) moves = self.production_id.move_finished_ids.filtered(lambda x: x.product_id.tracking == 'none' and x.state not in ('done', 'cancel')) for move in moves: rounding = move.product_uom.rounding if move.product_id.id == self.production_id.product_id.id: move.quantity_done_store += float_round(quantity, precision_rounding=rounding) elif move.unit_factor: # byproducts handling move.quantity_done_store += 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_launch_procurement_rule(self): """ Launch procurement group run method with required/custom fields genrated by a sale order line. procurement group will launch '_run_move', '_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 = 0.0 for move in line.move_ids.filtered(lambda r: r.state != 'cancel'): qty += move.product_qty if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0: continue if not line.order_id.procurement_group_id: line.order_id.procurement_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, }) values = line._prepare_procurement_values(group_id=line.order_id.procurement_group_id) product_qty = line.product_uom_qty - qty try: self.env['procurement.group'].run(line.product_id, product_qty, line.product_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 _check_sum(self): """ Check if each cost line its valuation lines sum to the correct amount and if the overall total amount is correct also """ prec_digits = self.env['decimal.precision'].precision_get('Account') for landed_cost in self: total_amount = sum(landed_cost.valuation_adjustment_lines.mapped('additional_landed_cost')) if not tools.float_compare(total_amount, landed_cost.amount_total, precision_digits=prec_digits) == 0: return False val_to_cost_lines = defaultdict(lambda: 0.0) for val_line in landed_cost.valuation_adjustment_lines: val_to_cost_lines[val_line.cost_line_id] += val_line.additional_landed_cost if any(tools.float_compare(cost_line.price_unit, val_amount, precision_digits=prec_digits) != 0 for cost_line, val_amount in val_to_cost_lines.iteritems()): return False return True
def _get_bom_delivered(self, bom=False): self.ensure_one() precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') # In the case of a kit, we need to check if all components are received or not. # nothing policy. A product can have several BoMs, we don't know which one was used when the # receipt was created. bom_delivered = {} if bom: bom_delivered[bom.id] = False product_uom_qty_bom = self.env['product.uom']._compute_qty_obj(self.product_uom, self.product_qty, bom.product_uom_id) boms, lines = bom.explode(self.product_id, product_uom_qty_bom) for bom_line, data in lines: qty = 0.0 for move in self.move_ids.filtered(lambda x: x.state == 'done' and x.product_id == bom_line.product_id): qty += self.env['product.uom']._compute_qty(move.product_uom.id, move.product_uom_qty, bom_line.product_uom_id.id) if float_compare(qty, data['qty'], precision_digits=precision) < 0: bom_delivered[bom.id] = False break else: bom_delivered[bom.id] = True if bom_delivered and any(bom_delivered.values()): return self.product_qty elif bom_delivered: return 0.0
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'] for seller in self._prepare_sellers(params): # 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 res |= seller break 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') 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 _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_qty = self.env["product.uom"]._compute_qty_obj( self.product_uom, self.product_uom_qty, self.product_id.uom_id ) if float_compare(self.product_id.virtual_available, product_qty, precision_digits=precision) == -1: is_available = self._check_routing() if not is_available: warning_mess = { "title": _("Not enough inventory!"), "message": _( "You plan to sell %.2f %s but you only have %.2f %s available!\nThe stock on hand is %.2f %s." ) % ( self.product_uom_qty, self.product_uom.name, self.product_id.virtual_available, self.product_id.uom_id.name, self.product_id.qty_available, self.product_id.uom_id.name, ), } return {"warning": warning_mess} return {}
def button_validate(self): if any(cost.state != 'draft' for cost in self): raise UserError(_('Only draft landed costs can be validated')) if any(not cost.valuation_adjustment_lines for cost in self): raise UserError(_('No valuation adjustments lines. You should maybe recompute the landed costs.')) if not self._check_sum(): raise UserError(_('Cost and adjustments lines do not match. You should maybe recompute the landed costs.')) for cost in self: move = self.env['account.move'].create({ 'journal_id': cost.account_journal_id.id, 'date': cost.date, 'ref': cost.name }) for line in cost.valuation_adjustment_lines.filtered(lambda line: line.move_id): per_unit = line.final_cost / line.quantity diff = per_unit - line.former_cost_per_unit # If the precision required for the variable diff is larger than the accounting # precision, inconsistencies between the stock valuation and the accounting entries # may arise. # For example, a landed cost of 15 divided in 13 units. If the products leave the # stock one unit at a time, the amount related to the landed cost will correspond to # round(15/13, 2)*13 = 14.95. To avoid this case, we split the quant in 12 + 1, then # record the difference on the new quant. # We need to make sure to able to extract at least one unit of the product. There is # an arbitrary minimum quantity set to 2.0 from which we consider we can extract a # unit and adapt the cost. curr_rounding = line.move_id.company_id.currency_id.rounding diff_rounded = tools.float_round(diff, precision_rounding=curr_rounding) diff_correct = diff_rounded quants = line.move_id.quant_ids.sorted(key=lambda r: r.qty, reverse=True) quant_correct = False if quants\ and tools.float_compare(quants[0].product_id.uom_id.rounding, 1.0, precision_digits=1) == 0\ and tools.float_compare(line.quantity * diff, line.quantity * diff_rounded, precision_rounding=curr_rounding) != 0\ and tools.float_compare(quants[0].qty, 2.0, precision_rounding=quants[0].product_id.uom_id.rounding) >= 0: # Search for existing quant of quantity = 1.0 to avoid creating a new one quant_correct = quants.filtered(lambda r: tools.float_compare(r.qty, 1.0, precision_rounding=quants[0].product_id.uom_id.rounding) == 0) if not quant_correct: quant_correct = quants[0]._quant_split(quants[0].qty - 1.0) else: quant_correct = quant_correct[0] quants = quants - quant_correct diff_correct += (line.quantity * diff) - (line.quantity * diff_rounded) diff = diff_rounded quant_dict = {} for quant in quants: quant_dict[quant] = quant.cost + diff if quant_correct: quant_dict[quant_correct] = quant_correct.cost + diff_correct for quant, value in quant_dict.items(): quant.sudo().write({'cost': value}) qty_out = 0 for quant in line.move_id.quant_ids: if quant.location_id.usage != 'internal': qty_out += quant.qty line._create_accounting_entries(move, qty_out) cost.write({'state': 'done', 'account_move_id': move.id}) move.post() return True
def _generate_lines_values(self, move, qty_to_consume): """ Create workorder line. First generate line based on the reservation, in order to prefill reserved quantity, lot and serial number. If the quantity to consume is greater than the reservation quantity then create line with the correct quantity to consume but without lot or serial number. """ lines = [] is_tracked = move.product_id.tracking == 'serial' if move in self.move_raw_ids._origin: # Get the inverse_name (many2one on line) of raw_workorder_line_ids initial_line_values = { self.raw_workorder_line_ids._get_raw_workorder_inverse_name(): self.id } else: # Get the inverse_name (many2one on line) of finished_workorder_line_ids initial_line_values = { self.finished_workorder_line_ids._get_finished_workoder_inverse_name( ): self.id } for move_line in move.move_line_ids: line = dict(initial_line_values) if float_compare( qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) <= 0: break # move line already 'used' in workorder (from its lot for instance) if move_line.lot_produced_ids or float_compare( move_line.product_uom_qty, move_line.qty_done, precision_rounding=move.product_uom.rounding) <= 0: continue # search wo line on which the lot is not fully consumed or other reserved lot linked_wo_line = self._workorder_line_ids().filtered( lambda line: line.move_id == move and line.lot_id == move_line. lot_id) if linked_wo_line: if float_compare( sum(linked_wo_line.mapped('qty_to_consume')), move_line.product_uom_qty - move_line.qty_done, precision_rounding=move.product_uom.rounding) < 0: to_consume_in_line = min( qty_to_consume, move_line.product_uom_qty - move_line.qty_done - sum(linked_wo_line.mapped('qty_to_consume'))) else: continue else: to_consume_in_line = min( qty_to_consume, move_line.product_uom_qty - move_line.qty_done) line.update({ 'move_id': move.id, 'product_id': move.product_id.id, 'product_uom_id': is_tracked and move.product_id.uom_id.id or move.product_uom.id, 'qty_to_consume': to_consume_in_line, 'qty_reserved': to_consume_in_line, 'lot_id': move_line.lot_id.id, 'qty_done': to_consume_in_line, }) lines.append(line) qty_to_consume -= to_consume_in_line # The move has not reserved the whole quantity so we create new wo lines if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0: line = dict(initial_line_values) if move.product_id.tracking == 'serial': while float_compare( qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0: line.update({ 'move_id': move.id, 'product_id': move.product_id.id, 'product_uom_id': move.product_id.uom_id.id, 'qty_to_consume': 1, 'qty_done': 1, }) lines.append(line) qty_to_consume -= 1 else: line.update({ 'move_id': move.id, 'product_id': move.product_id.id, 'product_uom_id': move.product_uom.id, 'qty_to_consume': qty_to_consume, 'qty_done': qty_to_consume, }) lines.append(line) return lines
def _compute_is_produced(self): self.is_produced = False for order in self.filtered(lambda p: p.production_id and p.production_id.product_uom_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_launch_procurement_rule(self): """ Launch procurement group run method with required/custom fields genrated by a sale order line. procurement group will launch '_run_move', '_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 = 0.0 for move in line.move_ids.filtered(lambda r: r.state != 'cancel'): qty += move.product_uom._compute_quantity( move.product_uom_qty, line.product_uom, rounding_method='HALF-UP') 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 olive_oil_tank_check(self, raise_if_not_merged=True, raise_if_empty=True): '''Returns quantity Always raises when there are reservations ''' self.ensure_one() sqo = self.env['stock.quant'] ppo = self.env['product.product'] prec = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') tank_type = self.olive_tank_type tank_type_label = dict( self.fields_get( 'olive_tank_type', 'selection')['olive_tank_type']['selection'])[tank_type] # Tank configuration checks if not tank_type: raise UserError( _("The stock location '%s' is not an olive oil tank.") % self.display_name) if not self.oil_product_id: raise UserError(_("Missing oil product on tank '%s'.") % self.name) if self.oil_product_id.olive_type != 'oil': raise UserError( _("Oil product '%s' configured on tank '%s' is not " "an olive oil product.") % (self.oil_product_id.display_name, self.name)) if tank_type != 'risouletto' and not self.olive_season_id: raise UserError( _("Olive season is not configured on tank '%s'.") % self.name) # raise if empty quant_qty_rg = sqo.read_group([('location_id', '=', self.id)], ['qty'], []) qty = quant_qty_rg and quant_qty_rg[0]['qty'] or 0 fcompare = float_compare(qty, 0, precision_digits=prec) if fcompare < 0: raise UserError( _("The tank '%s' has a negative quantity (%s).") % (self.name, qty)) elif fcompare == 0: if raise_if_empty: raise UserError(_("The tank '%s' is empty.") % self.name) return 0 # WARN : no further checks if empty # raise if there are reservations reserved_quants_count = sqo.search([('location_id', '=', self.id), ('reservation_id', '!=', False)], count=True) if reserved_quants_count: raise UserError( _("There are %d reserved quants in tank '%s'.") % (reserved_quants_count, self.name)) if raise_if_not_merged: quant_lot_rg = sqo.read_group([('location_id', '=', self.id)], ['qty', 'lot_id'], ['lot_id']) if len(quant_lot_rg) > 1: raise UserError( _("The tank '%s' (type '%s') is not merged: it " "contains several different lots.") % (self.name, tank_type_label)) # for risouletto, there are additionnal checks for raise_if_not_merged # see below quant_product_rg = sqo.read_group([('location_id', '=', self.id)], ['qty', 'product_id'], ['product_id']) if tank_type == 'risouletto': for quant_product in quant_product_rg: product = ppo.browse(quant_product['product_id'][0]) if raise_if_not_merged and product != self.oil_product_id: raise UserError( _("The tank '%s' (type '%s') contains '%s', " "so it not merged.") % (self.name, tank_type_label, product.display_name)) if product.olive_type != 'oil': raise UserError( _("The tank '%s' (type '%s') contains '%s', " "which is not an olive oil product.") % (self.name, tank_type_label, product.display_name)) else: # regular oil => always 1 product, same as configured on tank if len(quant_product_rg) > 1: raise UserError( _("There are several different products in tank '%s'. " "This should never happen in an oil tank which is " "not a risouletto tank.") % self.name) product = ppo.browse(quant_product_rg[0]['product_id'][0]) if product != self.oil_product_id: raise UserError( _("The tank '%s' (type '%s') contains '%s' but it is " "configured to contain '%s'. This should never " "happen.") % (self.name, tank_type_label, product.display_name, self.oil_product_id.display_name)) return qty
def _get_orderpoint_action(self): """Create manual orderpoints for missing product in each warehouses. It also removes orderpoints that have been replenish. In order to do it: - It uses the report.stock.quantity to find missing quantity per product/warehouse - It checks if orderpoint already exist to refill this location. - It checks if it exists other sources (e.g RFQ) tha refill the warehouse. - It creates the orderpoints for missing quantity that were not refill by an upper option. return replenish report ir.actions.act_window """ action = self.env["ir.actions.actions"]._for_xml_id( "stock.action_orderpoint_replenish") action['context'] = self.env.context orderpoints = self.env['stock.warehouse.orderpoint'].search([]) # Remove previous automatically created orderpoint that has been refilled. to_remove = orderpoints.filtered( lambda o: o.create_uid.id == SUPERUSER_ID and o.qty_to_order <= 0.0 and o.trigger == 'manual') to_remove.unlink() orderpoints = orderpoints - to_remove to_refill = defaultdict(float) qty_by_product_warehouse = self.env[ 'report.stock.quantity'].read_group( [('date', '=', fields.date.today()), ('state', '=', 'forecast')], ['product_id', 'product_qty', 'warehouse_id'], ['product_id', 'warehouse_id'], lazy=False) for group in qty_by_product_warehouse: warehouse_id = group.get( 'warehouse_id') and group['warehouse_id'][0] if group['product_qty'] >= 0.0 or not warehouse_id: continue to_refill[(group['product_id'][0], warehouse_id)] = group['product_qty'] if not to_refill: return action # Remove incoming quantity from other otigin than moves (e.g RFQ) product_ids, warehouse_ids = zip(*to_refill) # lot_stock_ids = [lot_stock_id_by_warehouse[w] for w in warehouse_ids] dummy, qty_by_product_wh = self.env['product.product'].browse( product_ids)._get_quantity_in_progress(warehouse_ids=warehouse_ids) rounding = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') for (product, warehouse), product_qty in to_refill.items(): qty_in_progress = qty_by_product_wh.get( (product, warehouse)) or 0.0 qty_in_progress += sum( orderpoints.filtered(lambda o: o.product_id.id == product and o .warehouse_id.id == warehouse).mapped( 'qty_to_order')) # Add qty to order for other orderpoint under this warehouse. if not qty_in_progress: continue to_refill[(product, warehouse)] = product_qty + qty_in_progress to_refill = { k: v for k, v in to_refill.items() if float_compare(v, 0.0, precision_digits=rounding) < 0.0 } lot_stock_id_by_warehouse = self.env['stock.warehouse'].search_read( [('id', 'in', [g[1] for g in to_refill.keys()])], ['lot_stock_id']) lot_stock_id_by_warehouse = { w['id']: w['lot_stock_id'][0] for w in lot_stock_id_by_warehouse } product_qty_available = {} for warehouse, group in groupby(sorted(to_refill, key=lambda p_w: p_w[1]), key=lambda p_w: p_w[1]): products = self.env['product.product'].browse( [p for p, w in group]) products_qty_available_list = products.with_context( location=lot_stock_id_by_warehouse[warehouse]).mapped( 'qty_available') product_qty_available.update({ (p.id, warehouse): q for p, q in zip(products, products_qty_available_list) }) orderpoint_values_list = [] for (product, warehouse), product_qty in to_refill.items(): lot_stock_id = lot_stock_id_by_warehouse[warehouse] orderpoint = self.filtered(lambda o: o.product_id == product and o. location_id == lot_stock_id) if orderpoint: orderpoint[0].qty_forecast += product_qty else: orderpoint_values = self.env[ 'stock.warehouse.orderpoint']._get_orderpoint_values( product, lot_stock_id) orderpoint_values.update({ 'name': _('Replenishment Report'), 'warehouse_id': warehouse, 'company_id': self.env['stock.warehouse'].browse( warehouse).company_id.id, }) orderpoint_values_list.append(orderpoint_values) orderpoints = self.env['stock.warehouse.orderpoint'].with_user( SUPERUSER_ID).create(orderpoint_values_list) for orderpoint in orderpoints: orderpoint.route_id = orderpoint.product_id.route_ids[:1] orderpoints.filtered(lambda o: not o.route_id)._set_default_route_id() return action
def _procure_orderpoint_confirm(self, use_new_cursor=False, company_id=None, raise_user_error=True): """ 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. """ self = self.with_company(company_id) orderpoints_noprefetch = self.read(['id']) orderpoints_noprefetch = [ orderpoint['id'] for orderpoint in orderpoints_noprefetch ] for orderpoints_batch in split_every(1000, orderpoints_noprefetch): if use_new_cursor: cr = registry(self._cr.dbname).cursor() self = self.with_env(self.env(cr=cr)) orderpoints_batch = self.env['stock.warehouse.orderpoint'].browse( orderpoints_batch) orderpoints_exceptions = [] while orderpoints_batch: procurements = [] for orderpoint in orderpoints_batch: if float_compare(orderpoint.qty_to_order, 0.0, precision_rounding=orderpoint.product_uom. rounding) == 1: date = datetime.combine(orderpoint.lead_days_date, time.min) values = orderpoint._prepare_procurement_values( date=date) procurements.append( self.env['procurement.group'].Procurement( orderpoint.product_id, orderpoint.qty_to_order, orderpoint.product_uom, orderpoint.location_id, orderpoint.name, orderpoint.name, orderpoint.company_id, values)) try: with self.env.cr.savepoint(): self.env['procurement.group'].with_context( from_orderpoint=True).run( procurements, raise_user_error=raise_user_error) except ProcurementException as errors: for procurement, error_msg in errors.procurement_exceptions: orderpoints_exceptions += [ (procurement.values.get('orderpoint_id'), error_msg) ] failed_orderpoints = self.env[ 'stock.warehouse.orderpoint'].concat( *[o[0] for o in orderpoints_exceptions]) if not failed_orderpoints: _logger.error('Unable to process orderpoints') break orderpoints_batch -= failed_orderpoints except OperationalError: if use_new_cursor: cr.rollback() continue else: raise else: orderpoints_batch._post_process_scheduler() break # Log an activity on product template for failed orderpoints. for orderpoint, error_msg in orderpoints_exceptions: existing_activity = self.env['mail.activity'].search([ ('res_id', '=', orderpoint.product_id.product_tmpl_id.id), ('res_model_id', '=', self.env.ref('product.model_product_template').id), ('note', '=', error_msg) ]) if not existing_activity: orderpoint.product_id.product_tmpl_id.activity_schedule( 'mail.mail_activity_data_warning', note=error_msg, user_id=orderpoint.product_id.responsible_id.id or SUPERUSER_ID, ) if use_new_cursor: cr.commit() cr.close() return {}
def _create_or_update_finished_line(self): """ 1. Check that the final lot and the quantity producing is valid regarding other workorders of this production 2. Save final lot and quantity producing to suggest on next workorder """ self.ensure_one() final_lot_quantity = self.qty_production rounding = self.product_uom_id.rounding # Get the max quantity possible for current lot in other workorders for workorder in (self.production_id.workorder_ids - self): # We add the remaining quantity to the produced quantity for the # current lot. For 5 finished products: if in the first wo it # creates 4 lot A and 1 lot B and in the second it create 3 lot A # and it remains 2 units to product, it could produce 5 lot A. # In this case we select 4 since it would conflict with the first # workorder otherwise. line = workorder.finished_workorder_line_ids.filtered( lambda line: line.lot_id == self.finished_lot_id) line_without_lot = workorder.finished_workorder_line_ids.filtered( lambda line: line.product_id == workorder.product_id and not line.lot_id) quantity_remaining = workorder.qty_remaining + line_without_lot.qty_done quantity = line.qty_done + quantity_remaining if line and float_compare(quantity, final_lot_quantity, precision_rounding=rounding) <= 0: final_lot_quantity = quantity elif float_compare(quantity_remaining, final_lot_quantity, precision_rounding=rounding) < 0: final_lot_quantity = quantity_remaining # final lot line for this lot on this workorder. current_lot_lines = self.finished_workorder_line_ids.filtered( lambda line: line.lot_id == self.finished_lot_id) # this lot has already been produced if float_compare(final_lot_quantity, current_lot_lines.qty_done + self.qty_producing, precision_rounding=rounding) < 0: raise UserError( _('You have produced %s %s of lot %s in the previous workorder. You are trying to produce %s in this one' ) % (final_lot_quantity, self.product_id.uom_id.name, self.finished_lot_id.name, current_lot_lines.qty_done + self.qty_producing)) # Update workorder line that regiter final lot created if not current_lot_lines: current_lot_lines = self.env['mrp.workorder.line'].create({ 'finished_workorder_id': self.id, 'product_id': self.product_id.id, 'lot_id': self.finished_lot_id.id, 'qty_done': self.qty_producing, }) else: current_lot_lines.qty_done += self.qty_producing
def _create_extra_move(self): ''' Creates an extra move if necessary depending on extra quantities than foreseen or extra moves''' self.ensure_one() quantity_to_split = 0 uom_qty_to_split = 0 extra_move = self.env['stock.move'] rounding = self.product_uom.rounding link_procurement = False # If more produced than the procurement linked, you should create an extra move if self.procurement_id and self.production_id and float_compare( self.production_id.qty_produced, self.procurement_id.product_qty, precision_rounding=rounding) > 0: done_moves_total = sum( self.production_id.move_finished_ids.filtered( lambda x: x.product_id == self.product_id and x.state == 'done').mapped('product_uom_qty')) # If you depassed the quantity before, you don't need to split anymore, but adapt the quantities if float_compare(done_moves_total, self.procurement_id.product_qty, precision_rounding=rounding) >= 0: quantity_to_split = 0 if float_compare(self.product_uom_qty, self.quantity_done, precision_rounding=rounding) < 0: self.product_uom_qty = self.quantity_done #TODO: could change qty on move_dest_id also (in case of 2-step in/out) else: quantity_to_split = done_moves_total + self.quantity_done - self.procurement_id.product_qty uom_qty_to_split = self.product_uom_qty - ( self.quantity_done - quantity_to_split ) #self.product_uom_qty - (self.procurement_id.product_qty + done_moves_total) if float_compare(uom_qty_to_split, quantity_to_split, precision_rounding=rounding) < 0: uom_qty_to_split = quantity_to_split self.product_uom_qty = self.quantity_done - quantity_to_split # You split also simply when the quantity done is bigger than foreseen elif float_compare(self.quantity_done, self.product_uom_qty, precision_rounding=rounding) > 0: quantity_to_split = self.quantity_done - self.product_uom_qty uom_qty_to_split = quantity_to_split # + no need to change existing self.product_uom_qty link_procurement = True if quantity_to_split: extra_move = self.copy( default={ 'quantity_done': quantity_to_split, 'product_uom_qty': uom_qty_to_split, 'production_id': self.production_id.id, 'raw_material_production_id': self.raw_material_production_id.id, 'procurement_id': link_procurement and self.procurement_id.id or False }) extra_move.action_confirm() if self.has_tracking != 'none': qty_todo = self.quantity_done - quantity_to_split for movelot in self.move_lot_ids.filtered(lambda x: x.done_wo): if movelot.quantity_done and movelot.done_wo: if float_compare(qty_todo, movelot.quantity_done, precision_rounding=rounding) >= 0: qty_todo -= movelot.quantity_done elif float_compare( qty_todo, 0, precision_rounding=rounding) > 0: #split remaining = movelot.quantity_done - qty_todo movelot.quantity_done = qty_todo movelot.copy( default={ 'move_id': extra_move.id, 'quantity_done': remaining }) qty_todo = 0 else: movelot.move_id = extra_move.id else: self.quantity_done -= quantity_to_split return extra_move
def test_invoice_with_grouping(self): invoice = self.inv_model.create({ 'date_invoice': self._date('01-01'), 'account_id': self.account_receivable.id, 'partner_id': self.env.ref('base.res_partner_2').id, 'journal_id': self.sale_journal.id, 'type': 'out_invoice', 'invoice_line_ids': [ (0, 0, { 'product_id': self.maint_product.id, 'name': 'Maintenance IPBX 12 mois', 'price_unit': 2400, 'quantity': 1, 'account_id': self.account_revenue.id, 'start_date': self._date('01-01'), 'end_date': self._date('12-31'), }), (0, 0, { 'product_id': self.maint_product.id, 'name': 'Maintenance téléphones 12 mois', 'price_unit': 12, 'quantity': 10, 'account_id': self.account_revenue.id, 'start_date': self._date('01-01'), 'end_date': self._date('12-31'), }), (0, 0, { 'product_id': self.maint_product.id, 'name': 'Maintenance Fax 6 mois', 'price_unit': 120.75, 'quantity': 1, 'account_id': self.account_revenue.id, 'start_date': self._date('01-01'), 'end_date': self._date('06-30'), }), (0, 0, { 'product_id': self.env.ref('product.product_product_5').id, 'name': 'HD IPBX', 'price_unit': 215.5, 'quantity': 1, 'account_id': self.account_revenue.id, }), ], }) invoice.action_invoice_open() self.assertTrue(invoice.move_id) iline_res = { (self._date('01-01'), self._date('12-31')): 2520, (self._date('01-01'), self._date('06-30')): 120.75, (False, False): 215.5, } precision = self.env['decimal.precision'].precision_get('Account') for mline in invoice.move_id.line_ids: if mline.account_id == self.account_revenue: amount = iline_res.pop( (fields.Date.to_string(mline.start_date), fields.Date.to_string(mline.end_date))) self.assertEquals( float_compare(amount, mline.credit, precision_digits=precision), 0)
def validate(self): self.ensure_one() assert self.state == 'produce' pr_oil = self.env['decimal.precision'].precision_get( 'Olive Oil Volume') origin = _('Olive oil bottling wizard') mpo = self.env['mrp.production'] splo = self.env['stock.production.lot'] sqo = self.env['stock.quant'] smo = self.env['stock.move'] mblo = self.env['mrp.bom.line'] oil_product = self.oil_product_id bottle_product = self.bottle_product_id bom = self.bom_id if self.quantity <= 0: raise UserError( _("The quantity of bottles to produce must be positive.")) assert self.src_location_id assert self.other_src_location_id assert self.dest_location_id assert self.expiry_date assert self.lot_name assert oil_product.tracking == 'lot' assert bottle_product.tracking == 'lot' if self.expiry_date < fields.Date.context_today(self): raise UserError(_("The expiry date should not be in the past.")) oil_start_qty_in_tank = self.src_location_id.olive_oil_tank_check( raise_if_empty=True, raise_if_reservation=True, raise_if_multi_lot=True) # Check we have enough oil oil_required_qty = self.quantity * self.bottle_volume if float_compare(oil_start_qty_in_tank, oil_required_qty, precision_digits=pr_oil) <= 0: raise UserError( _("The tank %s currently contains %s liters. This is not " "enough for this bottling (%s liters required).") % (self.src_location_id.name, oil_start_qty_in_tank, oil_required_qty)) # Check we have enough empty bottles other_product_bom_lines = mblo.search([('bom_id', '=', bom.id), ('product_id', '!=', oil_product.id)]) for bom_line in other_product_bom_lines: qty_required = self.quantity * bom_line.product_qty qrg = sqo.read_group( [('location_id', '=', self.other_src_location_id.id), ('product_id', '=', bom_line.product_id.id), ('reservation_id', '=', False)], ['qty'], []) free_start_qty = qrg and qrg[0]['qty'] or 0 uom = bom_line.product_id.uom_id if float_compare(free_start_qty, qty_required, precision_digits=0) <= 0: raise UserError( _("The stock location '%s' contains %s %s '%s' without reservation. " "This is not enough for this bottling (%s %s required).") % (self.other_src_location_id.display_name, free_start_qty, uom.name, bom_line.product_id.name, qty_required, uom.name)) mo = mpo.create({ 'product_id': bottle_product.id, 'product_qty': self.quantity, 'product_uom_id': bottle_product.uom_id.id, 'location_src_id': self.src_location_id.id, 'location_dest_id': self.dest_location_id.id, 'origin': origin, 'bom_id': bom.id, }) assert mo.state == 'confirmed' assert len(mo.move_raw_ids) > 0, 'Missing raw moves' assert len(mo.move_finished_ids) == 1, 'Wrong finished moves' assert mo.move_finished_ids[ 0].product_id == bottle_product, 'Wrong product on finished move' oil_raw_move = smo.search([('product_id.olive_type', '=', 'oil'), ('raw_material_production_id', '=', mo.id)]) other_raw_moves = smo.search([('product_id.olive_type', '!=', 'oil'), ('raw_material_production_id', '=', mo.id)]) for rmove in other_raw_moves: if rmove.product_id.tracking in ('lot', 'serial'): raise UserError( _("The bill of material has the component '%s' " "which is tracked by lot or serial. For the moment, " "the only supported scenario is where the only component " "of the bill of material tracked by lot is the oil.") % rmove.product_id.display_name) # BOM has already been checked, so this should really never happen assert len(oil_raw_move) == 1, 'Wrong number of oil raw moves' # HACK change source location for other raw moves other_raw_moves.write({'location_id': self.other_src_location_id.id}) mo.action_assign() if mo.availability != 'assigned': raise UserError( _("Could not reserve the raw material for this bottling operation. " "Check that you have enough oil and empty bottles.")) for move_lot in oil_raw_move.move_lot_ids: assert move_lot.lot_id move_lot.quantity_done = move_lot.quantity for rmove in other_raw_moves: rmove.quantity_done = rmove.product_uom_qty # raw lines should be green at this step # Create finished lot new_lot = splo.create({ 'product_id': bottle_product.id, 'name': self.lot_name, 'expiry_date': self.expiry_date, }) self.env['stock.move.lots'].create({ 'move_id': mo.move_finished_ids[0].id, 'product_id': bottle_product.id, 'production_id': mo.id, 'quantity': self.quantity, 'quantity_done': self.quantity, 'lot_id': new_lot.id, }) for raw_move_lot in oil_raw_move.move_lot_ids: assert not raw_move_lot.lot_produced_id oil_raw_move.move_lot_ids.write({'lot_produced_id': new_lot.id}) mo.write({ 'state': 'progress', 'date_start': datetime.now(), }) assert mo.post_visible is True mo.post_inventory() assert mo.check_to_done is True mo.button_mark_done() # Check oil end qty oil_end_qty_in_tank = self.src_location_id.olive_oil_tank_check() if float_compare(oil_end_qty_in_tank, oil_start_qty_in_tank - oil_required_qty, precision_digits=pr_oil): raise UserError( _("The end quantity in tank (%s L) is wrong. This should never happen." ) % oil_end_qty_in_tank) action = self.env['ir.actions.act_window'].for_xml_id( 'mrp', 'mrp_production_action') action.update({ 'res_id': mo.id, 'views': False, 'view_mode': 'form,tree,kanban,calendar', }) return action
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(new_quantity_done, ml.product_uom_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 test_order_to_invoice(self): # I create a new PoS order with 2 units of PC1 at 450 EUR (Tax Incl) and 3 units of PCSC349 at 300 EUR. (Tax Excl) self.pos_order_pos1 = self.PosOrder.create({ 'company_id': self.company_id, 'partner_id': self.partner1.id, 'pricelist_id': self.partner1.property_product_pricelist.id, 'lines': [(0, 0, { 'name': "OL/0001", 'product_id': self.product3.id, 'price_unit': 450, 'discount': 5.0, 'qty': 2.0, 'tax_ids': [(6, 0, self.product3.taxes_id.ids)], }), (0, 0, { 'name': "OL/0002", 'product_id': self.product4.id, 'price_unit': 300, 'discount': 5.0, 'qty': 3.0, 'tax_ids': [(6, 0, self.product4.taxes_id.ids)], })] }) # I click on the "Make Payment" wizard to pay the PoS order context_make_payment = { "active_ids": [self.pos_order_pos1.id], "active_id": self.pos_order_pos1.id } self.pos_make_payment = self.PosMakePayment.with_context( context_make_payment).create({ 'amount': (450 * 2 + 300 * 3 * 1.05) * 0.95, }) # I click on the validate button to register the payment. context_payment = {'active_id': self.pos_order_pos1.id} self.pos_make_payment.with_context(context_payment).check() # I check that the order is marked as paid and there is no invoice # attached to it self.assertEqual(self.pos_order_pos1.state, 'paid', "Order should be in paid state.") self.assertFalse(self.pos_order_pos1.invoice_id, 'Invoice should not be attached to order.') # I generate an invoice from the order self.invoice = self.pos_order_pos1.action_pos_order_invoice() # I test that the total of the attached invoice is correct self.amount_total = self.pos_order_pos1.amount_total self.assertEqual( float_compare(self.amount_total, 1752.75, precision_digits=2), 0, "Invoice not correct") """In order to test the reports on Bank Statement defined in point_of_sale module, I create a bank statement line, confirm it and print the reports""" # I select the period and journal for the bank statement context_journal = {'journal_type': 'bank'} self.assertTrue( self.AccountBankStatement.with_context( context_journal)._default_journal(), 'Journal has not been selected') journal = self.env['account.journal'].create({ 'name': 'Bank Test', 'code': 'BNKT', 'type': 'bank', 'company_id': self.company_id, }) # I create a bank statement with Opening and Closing balance 0. account_statement = self.AccountBankStatement.create({ 'balance_start': 0.0, 'balance_end_real': 0.0, 'date': time.strftime('%Y-%m-%d'), 'journal_id': journal.id, 'company_id': self.company_id, 'name': 'pos session test', }) # I create bank statement line account_statement_line = self.AccountBankStatementLine.create({ 'amount': 1000, 'partner_id': self.partner4.id, 'statement_id': account_statement.id, 'name': 'EXT001' }) # I modify the bank statement and set the Closing Balance. account_statement.write({ 'balance_end_real': 1000.0, }) # I reconcile the bank statement. new_aml_dicts = [{ 'account_id': self.partner4.property_account_receivable_id.id, 'name': "EXT001", 'credit': 1000.0, 'debit': 0.0, }] account_statement_line.process_reconciliations([{ 'new_aml_dicts': new_aml_dicts }]) # I confirm the bank statement using Confirm button self.AccountBankStatement.button_confirm_bank()
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'] LeadScoringFrequency = self.env['crm.lead.scoring.frequency'] 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 won_stage_id = self.env['crm.stage'].search([('is_won', '=', True)], limit=1).id 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])) leads_to_create.append( self._get_lead_values(team_ids[0], 'team_1_%s' % str(4), country_ids[1], state_ids[1], state_values[1], state_values[0], source_ids[1], stage_ids[0])) # for team 2 leads_to_create.append( self._get_lead_values(team_ids[1], 'team_2_%s' % str(5), 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(6), 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(7), 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(8), country_ids[0], state_ids[1], state_values[2], state_values[0], source_ids[2], stage_ids[1])) leads_to_create.append( self._get_lead_values(team_ids[1], 'team_2_%s' % str(9), country_ids[1], state_ids[0], state_values[1], state_values[0], source_ids[1], 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,tag_ids") # 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[5].action_set_lost() leads[6].action_set_lost() leads[7].action_set_won() # A. Test Full Rebuild # 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.assertEqual( tools.float_compare(leads[3].automated_probability, 33.49, 2), 0) self.assertEqual( tools.float_compare(leads[8].automated_probability, 7.74, 2), 0) # Test frequencies lead_4_stage_0_freq = LeadScoringFrequency.search([ ('team_id', '=', leads[4].team_id.id), ('variable', '=', 'stage_id'), ('value', '=', stage_ids[0]) ]) lead_4_stage_won_freq = LeadScoringFrequency.search([ ('team_id', '=', leads[4].team_id.id), ('variable', '=', 'stage_id'), ('value', '=', won_stage_id) ]) lead_4_country_freq = LeadScoringFrequency.search([ ('team_id', '=', leads[4].team_id.id), ('variable', '=', 'country_id'), ('value', '=', leads[4].country_id.id) ]) lead_4_email_state_freq = LeadScoringFrequency.search([ ('team_id', '=', leads[4].team_id.id), ('variable', '=', 'email_state'), ('value', '=', str(leads[4].email_state)) ]) lead_9_stage_0_freq = LeadScoringFrequency.search([ ('team_id', '=', leads[9].team_id.id), ('variable', '=', 'stage_id'), ('value', '=', stage_ids[0]) ]) lead_9_stage_won_freq = LeadScoringFrequency.search([ ('team_id', '=', leads[9].team_id.id), ('variable', '=', 'stage_id'), ('value', '=', won_stage_id) ]) lead_9_country_freq = LeadScoringFrequency.search([ ('team_id', '=', leads[9].team_id.id), ('variable', '=', 'country_id'), ('value', '=', leads[9].country_id.id) ]) lead_9_email_state_freq = LeadScoringFrequency.search([ ('team_id', '=', leads[9].team_id.id), ('variable', '=', 'email_state'), ('value', '=', str(leads[9].email_state)) ]) self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) self.assertEqual(lead_4_country_freq.won_count, 0.1) self.assertEqual(lead_4_email_state_freq.won_count, 1.1) self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1) self.assertEqual(lead_4_stage_won_freq.lost_count, 0.1) self.assertEqual(lead_4_country_freq.lost_count, 1.1) self.assertEqual(lead_4_email_state_freq.lost_count, 2.1) self.assertEqual(lead_9_stage_0_freq.won_count, 1.1) self.assertEqual(lead_9_stage_won_freq.won_count, 1.1) self.assertEqual(lead_9_country_freq.won_count, 0.0) # frequency does not exist self.assertEqual(lead_9_email_state_freq.won_count, 1.1) self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1) self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1) self.assertEqual(lead_9_country_freq.lost_count, 0.0) # frequency does not exist self.assertEqual(lead_9_email_state_freq.lost_count, 2.1) # B. Test Live Increment leads[4].action_set_lost() leads[9].action_set_won() # re-get frequencies that did not exists before lead_9_country_freq = LeadScoringFrequency.search([ ('team_id', '=', leads[9].team_id.id), ('variable', '=', 'country_id'), ('value', '=', leads[9].country_id.id) ]) # B.1. Test frequencies - team 1 should not impact team 2 self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged self.assertEqual(lead_4_stage_0_freq.lost_count, 3.1) # + 1 self.assertEqual( lead_4_stage_won_freq.lost_count, 0.1) # unchanged - consider stages with <= sequence when lost self.assertEqual(lead_4_country_freq.lost_count, 2.1) # + 1 self.assertEqual(lead_4_email_state_freq.lost_count, 3.1) # + 1 self.assertEqual(lead_9_stage_0_freq.won_count, 2.1) # + 1 self.assertEqual(lead_9_stage_won_freq.won_count, 2.1) # + 1 - consider every stages when won self.assertEqual(lead_9_country_freq.won_count, 1.1) # + 1 self.assertEqual(lead_9_email_state_freq.won_count, 2.1) # + 1 self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1) # unchanged self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1) # unchanged self.assertEqual(lead_9_country_freq.lost_count, 0.1) # unchanged (did not exists before) self.assertEqual(lead_9_email_state_freq.lost_count, 2.1) # unchanged # Propabilities of other leads should not be impacted as only modified lead are recomputed. self.assertEqual( tools.float_compare(leads[3].automated_probability, 33.49, 2), 0) self.assertEqual( tools.float_compare(leads[8].automated_probability, 7.74, 2), 0) self.assertEqual(leads[3].is_automated_probability, True) self.assertEqual(leads[8].is_automated_probability, True) # Restore -> Should decrease lost leads[4].toggle_active() self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1) # - 1 self.assertEqual( lead_4_stage_won_freq.lost_count, 0.1) # unchanged - consider stages with <= sequence when lost self.assertEqual(lead_4_country_freq.lost_count, 1.1) # - 1 self.assertEqual(lead_4_email_state_freq.lost_count, 2.1) # - 1 self.assertEqual(lead_9_stage_0_freq.won_count, 2.1) # unchanged self.assertEqual(lead_9_stage_won_freq.won_count, 2.1) # unchanged self.assertEqual(lead_9_country_freq.won_count, 1.1) # unchanged self.assertEqual(lead_9_email_state_freq.won_count, 2.1) # unchanged self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1) # unchanged self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1) # unchanged self.assertEqual(lead_9_country_freq.lost_count, 0.1) # unchanged self.assertEqual(lead_9_email_state_freq.lost_count, 2.1) # unchanged # set to won stage -> Should increase won leads[4].stage_id = won_stage_id self.assertEqual(lead_4_stage_0_freq.won_count, 2.1) # + 1 self.assertEqual(lead_4_stage_won_freq.won_count, 2.1) # + 1 self.assertEqual(lead_4_country_freq.won_count, 1.1) # + 1 self.assertEqual(lead_4_email_state_freq.won_count, 2.1) # + 1 self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1) # unchanged self.assertEqual(lead_4_stage_won_freq.lost_count, 0.1) # unchanged self.assertEqual(lead_4_country_freq.lost_count, 1.1) # unchanged self.assertEqual(lead_4_email_state_freq.lost_count, 2.1) # unchanged # Archive (was won, now lost) -> Should decrease won and increase lost leads[4].toggle_active() self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # - 1 self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # - 1 self.assertEqual(lead_4_country_freq.won_count, 0.1) # - 1 self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # - 1 self.assertEqual(lead_4_stage_0_freq.lost_count, 3.1) # + 1 self.assertEqual( lead_4_stage_won_freq.lost_count, 1.1 ) # consider stages with <= sequence when lostand as stage is won.. even won_stage lost_count is increased by 1 self.assertEqual(lead_4_country_freq.lost_count, 2.1) # + 1 self.assertEqual(lead_4_email_state_freq.lost_count, 3.1) # + 1 # Move to original stage -> Should do nothing (as lead is still lost) leads[4].stage_id = stage_ids[0] self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged self.assertEqual(lead_4_stage_0_freq.lost_count, 3.1) # unchanged self.assertEqual(lead_4_stage_won_freq.lost_count, 1.1) # unchanged self.assertEqual(lead_4_country_freq.lost_count, 2.1) # unchanged self.assertEqual(lead_4_email_state_freq.lost_count, 3.1) # unchanged # Restore -> Should decrease lost - at the end, frequencies should be like first frequencyes tests (except for 0.0 -> 0.1) leads[4].toggle_active() self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1) # - 1 self.assertEqual( lead_4_stage_won_freq.lost_count, 1.1) # unchanged - consider stages with <= sequence when lost self.assertEqual(lead_4_country_freq.lost_count, 1.1) # - 1 self.assertEqual(lead_4_email_state_freq.lost_count, 2.1) # - 1 # Probabilities should only be recomputed after modifying the lead itself. leads[3].stage_id = stage_ids[ 0] # probability should only change a bit as frequencies are almost the same (except 0.0 -> 0.1) leads[8].stage_id = stage_ids[ 0] # probability should change quite a lot # Test frequencies (should not have changed) self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1) # unchanged self.assertEqual(lead_4_stage_won_freq.lost_count, 1.1) # unchanged self.assertEqual(lead_4_country_freq.lost_count, 1.1) # unchanged self.assertEqual(lead_4_email_state_freq.lost_count, 2.1) # unchanged self.assertEqual(lead_9_stage_0_freq.won_count, 2.1) # unchanged self.assertEqual(lead_9_stage_won_freq.won_count, 2.1) # unchanged self.assertEqual(lead_9_country_freq.won_count, 1.1) # unchanged self.assertEqual(lead_9_email_state_freq.won_count, 2.1) # unchanged self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1) # unchanged self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1) # unchanged self.assertEqual(lead_9_country_freq.lost_count, 0.1) # unchanged self.assertEqual(lead_9_email_state_freq.lost_count, 2.1) # unchanged # Continue to test probability computation leads[3].probability = 40 self.assertEqual(leads[3].is_automated_probability, False) self.assertEqual(leads[8].is_automated_probability, True) self.assertEqual( tools.float_compare(leads[3].automated_probability, 20.87, 2), 0) self.assertEqual( tools.float_compare(leads[8].automated_probability, 2.43, 2), 0) self.assertEqual(tools.float_compare(leads[3].probability, 40, 2), 0) self.assertEqual(tools.float_compare(leads[8].probability, 2.43, 2), 0) # Test modify country_id leads[8].country_id = country_ids[1] self.assertEqual( tools.float_compare(leads[8].automated_probability, 34.38, 2), 0) self.assertEqual(tools.float_compare(leads[8].probability, 34.38, 2), 0) leads[8].country_id = country_ids[0] self.assertEqual( tools.float_compare(leads[8].automated_probability, 2.43, 2), 0) self.assertEqual(tools.float_compare(leads[8].probability, 2.43, 2), 0) # ---------------------------------------------- # Test tag_id frequencies and probability impact # ---------------------------------------------- tag_ids = self.env['crm.tag'].create([ { 'name': "Tag_test_1" }, { 'name': "Tag_test_2" }, ]).ids # tag_ids = self.env['crm.tag'].search([], limit=2).ids leads_with_tags = self.generate_leads_with_tags(tag_ids) leads_with_tags[:30].action_set_lost() # 60% lost on tag 1 leads_with_tags[31:50].action_set_won() # 40% won on tag 1 leads_with_tags[50:90].action_set_lost() # 80% lost on tag 2 leads_with_tags[91:100].action_set_won() # 20% won on tag 2 leads_with_tags[100:135].action_set_lost() # 70% lost on tag 1 and 2 leads_with_tags[136:150].action_set_won() # 30% won on tag 1 and 2 # tag 1 : won = 19+14 / lost = 30+35 # tag 2 : won = 9+14 / lost = 40+35 tag_1_freq = LeadScoringFrequency.search([('variable', '=', 'tag_id'), ('value', '=', tag_ids[0])]) tag_2_freq = LeadScoringFrequency.search([('variable', '=', 'tag_id'), ('value', '=', tag_ids[1])]) self.assertEqual(tools.float_compare(tag_1_freq.won_count, 33.1, 1), 0) self.assertEqual(tools.float_compare(tag_1_freq.lost_count, 65.1, 1), 0) self.assertEqual(tools.float_compare(tag_2_freq.won_count, 23.1, 1), 0) self.assertEqual(tools.float_compare(tag_2_freq.lost_count, 75.1, 1), 0) # Force recompute - A priori, no need to do this as, for each won / lost, we increment tag frequency. Lead._cron_update_automated_probabilities() leads_with_tags.invalidate_cache() lead_tag_1 = leads_with_tags[30] lead_tag_2 = leads_with_tags[90] lead_tag_1_2 = leads_with_tags[135] self.assertEqual( tools.float_compare(lead_tag_1.automated_probability, 33.69, 2), 0) self.assertEqual( tools.float_compare(lead_tag_2.automated_probability, 23.51, 2), 0) self.assertEqual( tools.float_compare(lead_tag_1_2.automated_probability, 28.05, 2), 0) lead_tag_1.tag_ids = [(5, 0, 0)] # remove all tags lead_tag_1_2.tag_ids = [(3, tag_ids[1], 0)] # remove tag 2 self.assertEqual( tools.float_compare(lead_tag_1.automated_probability, 28.6, 2), 0) self.assertEqual( tools.float_compare(lead_tag_2.automated_probability, 23.51, 2), 0) # no impact self.assertEqual( tools.float_compare(lead_tag_1_2.automated_probability, 33.69, 2), 0) lead_tag_1.tag_ids = [(4, tag_ids[1])] # add tag 2 lead_tag_2.tag_ids = [(4, tag_ids[0])] # add tag 1 lead_tag_1_2.tag_ids = [(3, tag_ids[0]), (4, tag_ids[1])] # remove tag 1 / add tag 2 self.assertEqual( tools.float_compare(lead_tag_1.automated_probability, 23.51, 2), 0) self.assertEqual( tools.float_compare(lead_tag_2.automated_probability, 28.05, 2), 0) self.assertEqual( tools.float_compare(lead_tag_1_2.automated_probability, 23.51, 2), 0) # go back to initial situation lead_tag_1.tag_ids = [(3, tag_ids[1]), (4, tag_ids[0])] # remove tag 2 / add tag 1 lead_tag_2.tag_ids = [(3, tag_ids[0])] # remove tag 1 lead_tag_1_2.tag_ids = [(4, tag_ids[0])] # add tag 1 self.assertEqual( tools.float_compare(lead_tag_1.automated_probability, 33.69, 2), 0) self.assertEqual( tools.float_compare(lead_tag_2.automated_probability, 23.51, 2), 0) self.assertEqual( tools.float_compare(lead_tag_1_2.automated_probability, 28.05, 2), 0) # set email_state for each lead and update probabilities leads.filtered(lambda lead: lead.id % 2 == 0).email_state = 'correct' leads.filtered(lambda lead: lead.id % 2 == 1).email_state = 'incorrect' Lead._cron_update_automated_probabilities() leads_with_tags.invalidate_cache() self.assertEqual( tools.float_compare(leads[3].automated_probability, 4.21, 2), 0) self.assertEqual( tools.float_compare(leads[8].automated_probability, 0.23, 2), 0) # remove all pls fields self.env['ir.config_parameter'].sudo().set_param( "crm.pls_fields", False) Lead._cron_update_automated_probabilities() leads_with_tags.invalidate_cache() self.assertEqual( tools.float_compare(leads[3].automated_probability, 34.38, 2), 0) self.assertEqual( tools.float_compare(leads[8].automated_probability, 50.0, 2), 0) # check if the probabilities are the same with the old param self.env['ir.config_parameter'].sudo().set_param( "crm.pls_fields", "country_id,state_id,email_state,phone_state,source_id") Lead._cron_update_automated_probabilities() leads_with_tags.invalidate_cache() self.assertEqual( tools.float_compare(leads[3].automated_probability, 4.21, 2), 0) self.assertEqual( tools.float_compare(leads[8].automated_probability, 0.23, 2), 0) # remove tag_ids from the calculation self.assertEqual( tools.float_compare(lead_tag_1.automated_probability, 28.6, 2), 0) self.assertEqual( tools.float_compare(lead_tag_2.automated_probability, 28.6, 2), 0) self.assertEqual( tools.float_compare(lead_tag_1_2.automated_probability, 28.6, 2), 0) lead_tag_1.tag_ids = [(5, 0, 0)] # remove all tags lead_tag_2.tag_ids = [(4, tag_ids[0])] # add tag 1 lead_tag_1_2.tag_ids = [(3, tag_ids[1], 0)] # remove tag 2 self.assertEqual( tools.float_compare(lead_tag_1.automated_probability, 28.6, 2), 0) self.assertEqual( tools.float_compare(lead_tag_2.automated_probability, 28.6, 2), 0) self.assertEqual( tools.float_compare(lead_tag_1_2.automated_probability, 28.6, 2), 0)
def _prepare_transaction(self, line, speeddict, action='create'): bdio = self.env['business.document.import'] account_analytic_id = account_id = False # convert to float float_fields = [ 'vat_eur', 'amount_eur', 'amount_currency', 'vat_20_id', 'vat_10_id', 'vat_55_id', 'vat_21_id' ] for float_field in float_fields: if line.get(float_field): try: line[float_field] = float(line[float_field]) except Exception: raise UserError( _("Cannot convert float field '%s' with value '%s'.") % (float_field, line.get(float_field))) else: line[float_field] = 0.0 total_vat_rates = line['vat_20_id'] + line['vat_10_id'] +\ line['vat_55_id'] + line['vat_21_id'] if float_compare(line['vat_eur'], total_vat_rates, precision_digits=2): raise UserError( _("Error in the Mooncard CSV file: for transaction ID '%s' " "the column 'vat_eur' (%.2f) doesn't have the same value " "as the sum of the 4 columns per VAT rate (%.2f)") % (line['id'], line['vat_eur'], total_vat_rates)) if line.get('charge_account'): account = bdio._match_account({'code': line['charge_account']}, [], speed_dict=speeddict['accounts']) account_id = account.id if line.get('analytic_code_1'): account_analytic_id = speeddict['analytic'].get( line['analytic_code_1'].lower()) ttype2odoo = { 'P': 'presentment', 'L': 'load', } if line.get('transaction_type') not in ttype2odoo: raise UserError( _("Wrong transaction type '%s'. The only possible values are " "'P' (presentment) or 'L' (load).") % line.get('transaction_type')) transaction_type = ttype2odoo[line['transaction_type']] vals = { 'transaction_type': transaction_type, 'description': line.get('title'), 'expense_categ_name': line.get('expense_category_name'), 'expense_account_id': account_id, 'account_analytic_id': account_analytic_id, 'vat_company_currency': line['vat_eur'], 'fr_vat_20_amount': line['vat_20_id'], 'fr_vat_10_amount': line['vat_10_id'], 'fr_vat_5_5_amount': line['vat_55_id'], 'fr_vat_2_1_amount': line['vat_21_id'], 'image_url': line.get('attachment'), 'receipt_number': line.get('receipt_code'), } if action == 'update': return vals # Continue with fields required for create country_id = False if line.get('country_code') and len(line['country_code']) == 3: logger.debug('search country with code %s with pycountry', line['country_code']) pcountry = pycountry.countries.get(alpha_3=line['country_code']) if pcountry and pcountry.alpha_2: country_id = speeddict['countries'].get(pcountry.alpha_2) currency_id = speeddict['currencies'].get( line.get('original_currency')) card_id = False if line.get('card_token'): card_id = speeddict['tokens'].get(line['card_token']) if not card_id: raise UserError( _("The CSV file contains the Moon Card '%s'. This " "card is not registered in Odoo, cf menu " "Accounting > Configuration > Miscellaneous > " "Moon Cards)") % line.get('card_token')) payment_date = False if (transaction_type == 'presentment' and line.get('date_authorization')): payment_date = self.convert_datetime_to_utc( line['date_authorization']) vals.update({ 'unique_import_id': line.get('id'), 'date': self.convert_datetime_to_utc(line['date_transaction']), 'payment_date': payment_date, 'card_id': card_id, 'country_id': country_id, 'merchant': line.get('supplier'), 'total_company_currency': line['amount_eur'], 'total_currency': line['amount_currency'], 'currency_id': currency_id, }) return vals
def _create_extra_move_lines(self): """Create new sml if quantity produced is bigger than the reserved one""" vals_list = [] # apply putaway location_dest_id = self.move_id.location_dest_id._get_putaway_strategy( self.product_id) or self.move_id.location_dest_id 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 quantity = self.product_id.uom_id._compute_quantity( quantity, self.product_uom_id, rounding_method='HALF-UP') rounding = quant.product_uom_id.rounding if (float_compare(quant.quantity, 0, precision_rounding=rounding) <= 0 or float_compare( quantity, 0, precision_rounding=self.product_uom_id.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': location_dest_id.id, 'product_uom_qty': 0, 'product_uom_id': self.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_id.uom_id.rounding) <= 0: break if float_compare( self.qty_done, 0, precision_rounding=self.product_id.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': 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 olive_oil_transfer(self, dest_loc, transfer_type, warehouse, dest_partner=False, partial_transfer_qty=False, origin=False, auto_validate=False): self.ensure_one() assert transfer_type in ('partial', 'full'), 'wrong transfer_type arg' if dest_loc == self: raise UserError( _("You are trying to transfer oil from '%s' to the same location!" ) % self.display_name) sqo = self.env['stock.quant'] smo = self.env['stock.move'] pr_oil = self.env['decimal.precision'].precision_get( 'Olive Oil Volume') src_loc = self raise_if_not_merged = False if transfer_type == 'partial': raise_if_not_merged = True src_qty = src_loc.olive_oil_tank_check( raise_if_not_merged=raise_if_not_merged) # compat src/dest if dest_loc.olive_tank_type: dest_loc.olive_oil_tank_check(raise_if_not_merged=False, raise_if_empty=False) dest_loc.olive_oil_tank_compatibility_check( src_loc.oil_product_id, src_loc.olive_season_id) if not warehouse.int_type_id: raise UserError( _("Internal picking type not configured on warehouse %s.") % warehouse.display_name) vals = { 'picking_type_id': warehouse.int_type_id.id, 'origin': origin, 'location_id': src_loc.id, 'location_dest_id': dest_loc.id, } pick = self.env['stock.picking'].create(vals) if transfer_type == 'full': quants = sqo.search([('location_id', '=', src_loc.id)]) for quant in quants: if float_compare(quant.qty, 0, precision_digits=2) < 0: raise UserError( _("There is a negative quant ID %d on olive tank %s. " "This should never happen.") % (quant.id, src_loc.display_name)) if quant.reservation_id: raise UserError( _("There is a reserved quant ID %d on olive tank %s. " "This must be investigated before trying a tank " "transfer again.") % (quant.id, src_loc.display_name)) mvals = { 'name': _('Full oil tank transfer'), 'origin': origin, 'product_id': quant.product_id.id, 'location_id': src_loc.id, 'location_dest_id': dest_loc.id, 'product_uom': quant.product_id.uom_id.id, 'product_uom_qty': quant.qty, 'restrict_lot_id': quant.lot_id.id or False, 'restrict_partner_id': quant.owner_id.id or False, 'picking_id': pick.id, } move = smo.create(mvals) qvals = {'reservation_id': move.id} if dest_partner and quant.owner_id != dest_partner: qvals['owner_id'] = dest_partner.id quant.sudo().write(qvals) elif transfer_type == 'partial': # we already checked above that the src loc has 1 lot if float_compare(partial_transfer_qty, 0, precision_digits=pr_oil) <= 0: raise UserError( _("The quantity to transfer (%s L) must be strictly positive." ) % partial_transfer_qty) if float_compare(partial_transfer_qty, src_qty, precision_digits=pr_oil) >= 0: raise UserError( _("The quantity to transfer (%s L) from tank '%s' is superior " "to its current oil quantity (%s L).") % (partial_transfer_qty, src_loc.name, src_qty)) product = src_loc.oil_product_id mvals = { 'name': _('Partial oil tank transfer'), 'origin': origin, 'product_id': product.id, 'location_id': src_loc.id, 'location_dest_id': dest_loc.id, 'product_uom': product.uom_id.id, 'product_uom_qty': partial_transfer_qty, 'picking_id': pick.id, } move = smo.create(mvals) # No need to reserve a particular quant, because we only have 1 lot # Hack for dest_partner is at the end of the method pick.action_confirm() pick.action_assign() pick.action_pack_operation_auto_fill() if auto_validate: pick.do_transfer() if transfer_type == 'partial' and dest_partner: move.quant_ids.sudo().write({'owner_id': dest_partner.id}) elif transfer_type == 'partial' and dest_partner: raise UserError( "We don't support partial transferts without auto_validate and " "with dest_partner") return pick
def _update_workorder_lines(self): """ Update workorder lines, according to the new qty currently produced. It returns a dict with line to create, update or delete. It do not directly write or unlink the line because this function is used in onchange and request that write on db (e.g. workorder creation). """ line_values = {'to_create': [], 'to_delete': [], 'to_update': {}} # moves are actual records move_finished_ids = self.move_finished_ids._origin.filtered( lambda move: move.product_id != self.product_id and move.state not in ('done', 'cancel')) move_raw_ids = self.move_raw_ids._origin.filtered( lambda move: move.state not in ('done', 'cancel')) for move in move_raw_ids | move_finished_ids: move_workorder_lines = self._workorder_line_ids().filtered( lambda w: w.move_id == move) # Compute the new quantity for the current component rounding = move.product_uom.rounding new_qty = self._prepare_component_quantity(move, self.qty_producing) # In case the production uom is different than the workorder uom # it means the product is serial and production uom is not the reference new_qty = self.product_uom_id._compute_quantity( new_qty, self.production_id.product_uom_id, round=False) qty_todo = float_round( new_qty - sum(move_workorder_lines.mapped('qty_to_consume')), precision_rounding=rounding) # Remove or lower quantity on exisiting workorder lines if float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0: qty_todo = abs(qty_todo) # Try to decrease or remove lines that are not reserved and # partialy reserved first. A different decrease strategy could # be define in _unreserve_order method. for workorder_line in move_workorder_lines.sorted( key=lambda wl: wl._unreserve_order()): if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0: break # If the quantity to consume on the line is lower than the # quantity to remove, the line could be remove. if float_compare(workorder_line.qty_to_consume, qty_todo, precision_rounding=rounding) <= 0: qty_todo = float_round(qty_todo - workorder_line.qty_to_consume, precision_rounding=rounding) if line_values['to_delete']: line_values['to_delete'] |= workorder_line else: line_values['to_delete'] = workorder_line # decrease the quantity on the line else: new_val = workorder_line.qty_to_consume - qty_todo # avoid to write a negative reserved quantity new_reserved = max( 0, workorder_line.qty_reserved - qty_todo) line_values['to_update'][workorder_line] = { 'qty_to_consume': new_val, 'qty_done': new_val, 'qty_reserved': new_reserved, } qty_todo = 0 else: # Search among wo lines which one could be updated qty_reserved_wl = defaultdict(float) # Try to update the line with the greater reservation first in # order to promote bigger batch. for workorder_line in move_workorder_lines.sorted( key=lambda wl: wl.qty_reserved, reverse=True): rounding = workorder_line.product_uom_id.rounding if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0: break move_lines = workorder_line._get_move_lines() qty_reserved_wl[ workorder_line.lot_id] += workorder_line.qty_reserved # The reserved quantity according to exisiting move line # already produced (with qty_done set) and other production # lines with the same lot that are currently on production. qty_reserved_remaining = sum( move_lines.mapped('product_uom_qty')) - sum( move_lines.mapped('qty_done')) - qty_reserved_wl[ workorder_line.lot_id] if float_compare(qty_reserved_remaining, 0, precision_rounding=rounding) > 0: qty_to_add = min(qty_reserved_remaining, qty_todo) line_values['to_update'][workorder_line] = { 'qty_done': workorder_line.qty_to_consume + qty_to_add, 'qty_to_consume': workorder_line.qty_to_consume + qty_to_add, 'qty_reserved': workorder_line.qty_reserved + qty_to_add, } qty_todo -= qty_to_add qty_reserved_wl[workorder_line.lot_id] += qty_to_add # If a line exists without reservation and without lot. It # means that previous operations could not find any reserved # quantity and created a line without lot prefilled. In this # case, the system will not find an existing move line with # available reservation anymore and will increase this line # instead of creating a new line without lot and reserved # quantities. if not workorder_line.qty_reserved and not workorder_line.lot_id and workorder_line.product_tracking != 'serial': line_values['to_update'][workorder_line] = { 'qty_done': workorder_line.qty_to_consume + qty_todo, 'qty_to_consume': workorder_line.qty_to_consume + qty_todo, } qty_todo = 0 # if there are still qty_todo, create new wo lines if float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0: for values in self._generate_lines_values(move, qty_todo): line_values['to_create'].append(values) # wo lines without move_id should also be deleted for wo_line in self._workorder_line_ids().filtered( lambda w: not w.move_id and (not w.finished_workorder_id or w.product_id != w. finished_workorder_id.product_id)): if not line_values['to_delete']: line_values['to_delete'] = wo_line elif wo_line not in line_values['to_delete']: line_values['to_delete'] |= wo_line return line_values
def record_production(self): if not self: return True self.ensure_one() self._check_sn_uniqueness() self._check_company() if float_compare(self.qty_producing, 0, precision_rounding=self.product_uom_id.rounding) <= 0: raise UserError( _('Please set the quantity you are currently producing. It should be different from zero.' )) if 'check_ids' not in self: for line in self.raw_workorder_line_ids | self.finished_workorder_line_ids: line._check_line_sn_uniqueness() # If last work order, then post lots used if not self.next_work_order_id: self._update_finished_move() # Transfer quantities from temporary to final move line or make them final self._update_moves() # Transfer lot (if present) and quantity produced to a finished workorder line if self.product_tracking != 'none': self._create_or_update_finished_line() # Update workorder quantity produced self.qty_produced += self.qty_producing # Suggest a finished lot on the next workorder if self.next_work_order_id and self.production_id.product_id.tracking != 'none' and not self.next_work_order_id.finished_lot_id: self.next_work_order_id._defaults_from_finished_workorder_line( self.finished_workorder_line_ids) # As we may have changed the quantity to produce on the next workorder, # make sure to update its wokorder lines self.next_work_order_id._apply_update_workorder_lines() # One a piece is produced, you can launch the next work order self._start_nextworkorder() # Test if the production is done rounding = self.production_id.product_uom_id.rounding if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) < 0: previous_wo = self.env['mrp.workorder'] if self.product_tracking != 'none': previous_wo = self.env['mrp.workorder'].search([ ('next_work_order_id', '=', self.id) ]) candidate_found_in_previous_wo = False if previous_wo: candidate_found_in_previous_wo = self._defaults_from_finished_workorder_line( previous_wo.finished_workorder_line_ids) if not candidate_found_in_previous_wo: # self is the first workorder self.qty_producing = self.qty_remaining self.finished_lot_id = False if self.product_tracking == 'serial': self.qty_producing = 1 self._apply_update_workorder_lines() else: self.qty_producing = 0 self.button_finish() return True
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(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_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 test_action_confirm_finish_phonecall(self): tag = 'helpdesk_phonecall_support.helpdesk_phonecall_service_tag_01' start_date_hour = '2017-09-04 14:49:20' finish_date_hour = '2017-09-04 15:20:40' values = { 'partner_id': self.partner.id, 'contact_partner_id': self.partner_contact.id, 'project_id': self.project.id, 'description': 'Teste', 'phonecall_tag_id': self.env.ref(tag).id, 'start_date_hour': start_date_hour, } # Finalmente criamos um atendimento phonecall = self.env['helpdesk.phonecall.service'].create(values) # Referencia para o wizard wizard = self.env['helpdesk.phonecall.confirm'].with_context( {'active_ids': [phonecall.id]}) # Executamos o metodo de finalizar o atendimento. Utilizamos o mock # para retornamos a data que queremos para que assim seja mais facil # verificar se o calculo de data esta correto with mock.patch('odoo.fields.Datetime.now') as dt: dt.return_value = finish_date_hour wizard.action_confirm_finish_phonecall() self.assertTrue(phonecall.finish_date_hour) self.assertEqual(phonecall.state, 'done') self.assertEqual(phonecall.finish_date_hour, finish_date_hour) # Executamos o metodo de finalizar o atendimento, verificamos # se ele obedece o conceito que apenas atendimentos em aberto podem # ser finalizados with self.assertRaises(UserError): wizard.action_confirm_finish_phonecall() timesheet = self.env['account.analytic.line'].search([ ('helpdesk_id', '=', phonecall.id) ]) self.assertEqual(phonecall.description, timesheet.name) self.assertEqual(phonecall.user_id.id, timesheet.user_id.id) self.assertEqual(phonecall.partner_id.id, timesheet.partner_id.id) self.assertEqual(phonecall.project_id.id, timesheet.project_id.id) self.assertEqual(phonecall.company_id.id, timesheet.company_id.id) self.assertEqual(phonecall.start_date_hour[:10], timesheet.date) # Verificamos a diferença de datas em segundos e a convertemos para # horas, já que o widget 'float_time' do Odoo considera o valor de # horas em segundos hours_diff_decimal = 1880 / 3600.0 result = float_compare(timesheet.unit_amount, hours_diff_decimal, precision_digits=12) # Verificamos se o resultado da comparacao e zero. No Python o valor # e considerado False self.assertFalse(result)
def onchange_price(self): '''当订单行的不含税单价改变时,改变含税单价''' price = self.price_taxed / (1 + self.tax_rate * 0.01) # 不含税单价 decimal = self.env.ref('core.decimal_price') if float_compare(price, self.price, precision_digits=decimal.digits) != 0: self.price_taxed = self.price * (1 + self.tax_rate * 0.01)
def _prepare_move(self, line): category_id = line.asset_id.category_id account_analytic_id = line.asset_id.account_analytic_id analytic_tag_ids = line.asset_id.analytic_tag_ids depreciation_date = self.env.context.get( 'depreciation_date' ) or line.depreciation_date or fields.Date.context_today(self) company_currency = line.asset_id.company_id.currency_id current_currency = line.asset_id.currency_id prec = company_currency.decimal_places amount = current_currency._convert(line.amount, company_currency, line.asset_id.company_id, depreciation_date) asset_name = line.asset_id.name + ' (%s/%s)' % ( line.sequence, len(line.asset_id.depreciation_line_ids)) move_line_1 = { 'name': asset_name, 'account_id': category_id.account_depreciation_id.id, 'debit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, 'credit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, 'partner_id': line.asset_id.partner_id.id, 'analytic_account_id': account_analytic_id.id if category_id.type == 'sale' else False, 'analytic_tag_ids': [(6, 0, analytic_tag_ids.ids)] if category_id.type == 'sale' else False, 'currency_id': company_currency != current_currency and current_currency.id or False, 'amount_currency': company_currency != current_currency and -1.0 * line.amount or 0.0, } move_line_2 = { 'name': asset_name, 'account_id': category_id.account_depreciation_expense_id.id, 'credit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, 'debit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, 'partner_id': line.asset_id.partner_id.id, 'analytic_account_id': account_analytic_id.id if category_id.type == 'purchase' else False, 'analytic_tag_ids': [(6, 0, analytic_tag_ids.ids)] if category_id.type == 'purchase' else False, 'currency_id': company_currency != current_currency and current_currency.id or False, 'amount_currency': company_currency != current_currency and line.amount or 0.0, } move_vals = { 'ref': line.asset_id.code, 'date': depreciation_date or False, 'journal_id': category_id.journal_id.id, 'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)], } return move_vals
def test_bom_report(self): """ Simulate a crumble receipt with mrp and open the bom structure report and check that data insde are correct. """ uom_kg = self.env.ref('uom.product_uom_kgm') uom_litre = self.env.ref('uom.product_uom_litre') crumble = self.env['product.product'].create({ 'name': 'Crumble', 'type': 'product', 'uom_id': uom_kg.id, 'uom_po_id': uom_kg.id, }) butter = self.env['product.product'].create({ 'name': 'Butter', 'type': 'product', 'uom_id': uom_kg.id, 'uom_po_id': uom_kg.id, 'standard_price': 7.01 }) biscuit = self.env['product.product'].create({ 'name': 'Biscuit', 'type': 'product', 'uom_id': uom_kg.id, 'uom_po_id': uom_kg.id, 'standard_price': 1.5 }) bom_form_crumble = Form(self.env['mrp.bom']) bom_form_crumble.product_tmpl_id = crumble.product_tmpl_id bom_form_crumble.product_qty = 11 bom_form_crumble.product_uom_id = uom_kg bom_crumble = bom_form_crumble.save() with Form(bom_crumble) as bom: with bom.bom_line_ids.new() as line: line.product_id = butter line.product_uom_id = uom_kg line.product_qty = 5 with bom.bom_line_ids.new() as line: line.product_id = biscuit line.product_uom_id = uom_kg line.product_qty = 6 workcenter = self.env['mrp.workcenter'].create({ 'costs_hour': 10, 'name': 'Deserts Table' }) routing_form = Form(self.env['mrp.routing']) routing_form.name = "Crumble process" routing_crumble = routing_form.save() with Form(routing_crumble) as routing: with routing.operation_ids.new() as operation: operation.workcenter_id = workcenter operation.name = 'Prepare biscuits' operation.time_cycle_manual = 5 with routing.operation_ids.new() as operation: operation.workcenter_id = workcenter operation.name = 'Prepare butter' operation.time_cycle_manual = 3 with routing.operation_ids.new() as operation: operation.workcenter_id = workcenter operation.name = 'Mix manually' operation.time_cycle_manual = 5 bom_crumble.routing_id = routing_crumble.id # TEST BOM STRUCTURE VALUE WITH BOM QUANTITY report_values = self.env[ 'report.mrp.report_bom_structure']._get_report_data( bom_id=bom_crumble.id, searchQty=11, searchVariant=False) # 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes self.assertEqual( report_values['lines']['operations_time'], 13.0, 'Operation time should be the same for 1 unit or for the batch') # Operation cost is the sum of operation line. operation_cost = float_round(5 / 60 * 10, precision_digits=2) * 2 + float_round( 3 / 60 * 10, precision_digits=2) self.assertEqual( float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0, '13 minute for 10$/hours -> 2.16') for component_line in report_values['lines']['components']: # standard price * bom line quantity * current quantity / bom finished product quantity if component_line['prod_id'] == butter.id: # 5 kg of butter at 7.01$ for 11kg of crumble -> 35.05$ self.assertEqual( float_compare(component_line['total'], (7.01 * 5), precision_digits=2), 0) if component_line['prod_id'] == biscuit.id: # 6 kg of biscuits at 1.50$ for 11kg of crumble -> 9$ self.assertEqual( float_compare(component_line['total'], (1.5 * 6), precision_digits=2), 0) # total price = 35.05 + 9 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 46,21 self.assertEqual( float_compare(report_values['lines']['total'], 46.21, precision_digits=2), 0, 'Product Bom Price is not correct') self.assertEqual( float_compare(report_values['lines']['total'] / 11.0, 4.20, precision_digits=2), 0, 'Product Unit Bom Price is not correct') # TEST BOM STRUCTURE VALUE BY UNIT report_values = self.env[ 'report.mrp.report_bom_structure']._get_report_data( bom_id=bom_crumble.id, searchQty=1, searchVariant=False) # 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes self.assertEqual( report_values['lines']['operations_time'], 13.0, 'Operation time should be the same for 1 unit or for the batch') # Operation cost is the sum of operation line. operation_cost = float_round(5 / 60 * 10, precision_digits=2) * 2 + float_round( 3 / 60 * 10, precision_digits=2) self.assertEqual( float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0, '13 minute for 10$/hours -> 2.16') for component_line in report_values['lines']['components']: # standard price * bom line quantity * current quantity / bom finished product quantity if component_line['prod_id'] == butter.id: # 5 kg of butter at 7.01$ for 11kg of crumble -> / 11 for price per unit (3.19) self.assertEqual( float_compare(component_line['total'], (7.01 * 5) * (1 / 11), precision_digits=2), 0) if component_line['prod_id'] == biscuit.id: # 6 kg of biscuits at 1.50$ for 11kg of crumble -> / 11 for price per unit (0.82) self.assertEqual( float_compare(component_line['total'], (1.5 * 6) * (1 / 11), precision_digits=2), 0) # total price = 3.19 + 0.82 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 6,17 self.assertEqual( float_compare(report_values['lines']['total'], 6.17, precision_digits=2), 0, 'Product Unit Bom Price is not correct') # TEST OPERATION COST WHEN PRODUCED QTY > BOM QUANTITY report_values_12 = self.env[ 'report.mrp.report_bom_structure']._get_report_data( bom_id=bom_crumble.id, searchQty=12, searchVariant=False) report_values_22 = self.env[ 'report.mrp.report_bom_structure']._get_report_data( bom_id=bom_crumble.id, searchQty=22, searchVariant=False) operation_cost = float_round(10 / 60 * 10, precision_digits=2) * 2 + float_round( 6 / 60 * 10, precision_digits=2) # Both needs 2 operation cycle self.assertEqual(report_values_12['lines']['operations_cost'], report_values_22['lines']['operations_cost']) self.assertEqual(report_values_22['lines']['operations_cost'], operation_cost) report_values_23 = self.env[ 'report.mrp.report_bom_structure']._get_report_data( bom_id=bom_crumble.id, searchQty=23, searchVariant=False) operation_cost = float_round(15 / 60 * 10, precision_digits=2) * 2 + float_round( 9 / 60 * 10, precision_digits=2) self.assertEqual(report_values_23['lines']['operations_cost'], operation_cost) # Create a more complex BoM with a sub product cheese_cake = self.env['product.product'].create({ 'name': 'Cheese Cake 300g', 'type': 'product', }) cream = self.env['product.product'].create({ 'name': 'cream', 'type': 'product', 'uom_id': uom_litre.id, 'uom_po_id': uom_litre.id, 'standard_price': 5.17, }) bom_form_cheese_cake = Form(self.env['mrp.bom']) bom_form_cheese_cake.product_tmpl_id = cheese_cake.product_tmpl_id bom_form_cheese_cake.product_qty = 60 bom_form_cheese_cake.product_uom_id = self.uom_unit bom_cheese_cake = bom_form_cheese_cake.save() with Form(bom_cheese_cake) as bom: with bom.bom_line_ids.new() as line: line.product_id = cream line.product_uom_id = uom_litre line.product_qty = 3 with bom.bom_line_ids.new() as line: line.product_id = crumble line.product_uom_id = uom_kg line.product_qty = 5.4 workcenter_2 = self.env['mrp.workcenter'].create({ 'name': 'cake mounting', 'costs_hour': 20, 'time_start': 10, 'time_stop': 15 }) routing_form = Form(self.env['mrp.routing']) routing_form.name = "Cheese cake process" routing_cheese = routing_form.save() with Form(routing_cheese) as routing: with routing.operation_ids.new() as operation: operation.workcenter_id = workcenter operation.name = 'Mix cheese and crumble' operation.time_cycle_manual = 10 with routing.operation_ids.new() as operation: operation.workcenter_id = workcenter_2 operation.name = 'Cake mounting' operation.time_cycle_manual = 5 bom_cheese_cake.routing_id = routing_cheese.id # TEST CHEESE BOM STRUCTURE VALUE WITH BOM QUANTITY report_values = self.env[ 'report.mrp.report_bom_structure']._get_report_data( bom_id=bom_cheese_cake.id, searchQty=60, searchVariant=False) self.assertEqual( report_values['lines']['operations_time'], 40.0, 'Operation time should be the same for 1 unit or for the batch') # Operation cost is the sum of operation line. operation_cost = float_round( 10 / 60 * 10, precision_digits=2) + float_round(30 / 60 * 20, precision_digits=2) self.assertEqual( float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0) for component_line in report_values['lines']['components']: # standard price * bom line quantity * current quantity / bom finished product quantity if component_line['prod_id'] == cream.id: # 3 liter of cream at 5.17$ for 60 unit of cheese cake -> 15.51$ self.assertEqual( float_compare(component_line['total'], (3 * 5.17), precision_digits=2), 0) if component_line['prod_id'] == crumble.id: # 5.4 kg of crumble at the cost of a batch. crumble_cost = self.env[ 'report.mrp.report_bom_structure']._get_report_data( bom_id=bom_crumble.id, searchQty=5.4, searchVariant=False)['lines']['total'] self.assertEqual( float_compare(component_line['total'], crumble_cost, precision_digits=2), 0) # total price = 15.51 + crumble_cost + operation_cost(10 + 1.67 = 11.67) = 27.18 + crumble_cost self.assertEqual( float_compare(report_values['lines']['total'], 27.18 + crumble_cost, precision_digits=2), 0, 'Product Bom Price is not correct')
def action_payslip_done(self): res = super(HrPayslip, self).action_payslip_done() precision = self.env['decimal.precision'].precision_get('Payroll') for slip in self: line_ids = [] debit_sum = 0.0 credit_sum = 0.0 date = slip.date or slip.date_to name = _('Payslip of %s') % (slip.employee_id.name) move_dict = { 'narration': name, 'ref': slip.number, 'z_analytic_account_id': slip.employee_id.z_analytic_account_id.id, 'journal_id': slip.journal_id.id, 'date': date, } for line in slip.details_by_salary_rule_category: amount = slip.credit_note and -line.total or line.total if float_is_zero(amount, precision_digits=precision): continue debit_account_id = line.salary_rule_id.account_debit.id credit_account_id = line.salary_rule_id.account_credit.id analytic_tag_ids = [ (4, analytic_tag.id, None) for analytic_tag in slip.employee_id.z_analytic_tag_ids ] if debit_account_id: debit_line = (0, 0, { 'name': line.name, 'partner_id': line._get_partner_id(credit_account=False), 'account_id': debit_account_id, 'journal_id': slip.journal_id.id, 'date': date, 'debit': amount > 0.0 and amount or 0.0, 'credit': amount < 0.0 and -amount or 0.0, 'analytic_account_id': slip.employee_id.z_analytic_account_id.id, 'analytic_tag_ids': analytic_tag_ids, 'tax_line_id': line.salary_rule_id.account_tax_id.id, }) line_ids.append(debit_line) debit_sum += debit_line[2]['debit'] - debit_line[2][ 'credit'] if credit_account_id: credit_line = (0, 0, { 'name': line.name, 'partner_id': line._get_partner_id(credit_account=True), 'account_id': credit_account_id, 'journal_id': slip.journal_id.id, 'date': date, 'debit': amount < 0.0 and -amount or 0.0, 'credit': amount > 0.0 and amount or 0.0, 'analytic_account_id': slip.employee_id.z_analytic_account_id.id, 'analytic_tag_ids': analytic_tag_ids, 'tax_line_id': line.salary_rule_id.account_tax_id.id, }) line_ids.append(credit_line) credit_sum += credit_line[2]['credit'] - credit_line[2][ 'debit'] if float_compare(credit_sum, debit_sum, precision_digits=precision) == -1: acc_id = slip.journal_id.default_credit_account_id.id if not acc_id: raise UserError( _('The Expense Journal "%s" has not properly configured the Credit Account!' ) % (slip.journal_id.name)) adjust_credit = (0, 0, { 'name': _('Adjustment Entry'), 'partner_id': False, 'account_id': acc_id, 'journal_id': slip.journal_id.id, 'date': date, 'debit': 0.0, 'credit': debit_sum - credit_sum, }) line_ids.append(adjust_credit) elif float_compare(debit_sum, credit_sum, precision_digits=precision) == -1: acc_id = slip.journal_id.default_debit_account_id.id if not acc_id: raise UserError( _('The Expense Journal "%s" has not properly configured the Debit Account!' ) % (slip.journal_id.name)) adjust_debit = (0, 0, { 'name': _('Adjustment Entry'), 'partner_id': False, 'account_id': acc_id, 'journal_id': slip.journal_id.id, 'date': date, 'debit': credit_sum - debit_sum, 'credit': 0.0, }) line_ids.append(adjust_debit) move_dict['line_ids'] = line_ids move = self.env['account.move'].create(move_dict) slip.write({'move_id': move.id, 'date': date}) move.post() return res
def _check_done_allocation(self): precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') self.ensure_one() return self.allocation_ids and float_compare( self.qty_cancelled, 0, precision_digits=precision) > 0
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']) values['responsible_moves'] = product_quantity[orderpoint.product_id.id]['responsible_moves'] 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 {}
def default_get(self, fields): res = super(MrpProductProduce, self).default_get(fields) if self._context and self._context.get('active_id'): production = self.env['mrp.production'].browse( self._context['active_id']) serial_finished = (production.product_id.tracking == 'serial') if serial_finished: todo_quantity = 1.0 else: main_product_moves = production.move_finished_ids.filtered( lambda x: x.product_id.id == production.product_id.id) todo_quantity = production.product_qty - sum( main_product_moves.mapped('quantity_done')) todo_quantity = todo_quantity if (todo_quantity > 0) else 0 if 'production_id' in fields: res['production_id'] = production.id if 'product_id' in fields: res['product_id'] = production.product_id.id if 'product_uom_id' in fields: res['product_uom_id'] = production.product_uom_id.id if 'serial' in fields: res['serial'] = bool(serial_finished) if 'product_qty' in fields: res['product_qty'] = todo_quantity if 'produce_line_ids' in fields: lines = [] for move in production.move_raw_ids.filtered( lambda x: (x.product_id.tracking != 'none') and x.state not in ('done', 'cancel') and x.bom_line_id): qty_to_consume = todo_quantity / move.bom_line_id.bom_id.product_qty * move.bom_line_id.product_qty 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': 0.0, 'lot_id': move_line.lot_id.id, 'product_uom_id': move.product_uom.id, 'product_id': move.product_id.id, }) 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': 0.0, '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': 0.0, 'product_uom_id': move.product_uom.id, 'product_id': move.product_id.id, }) res['produce_line_ids'] = [(0, 0, x) for x in lines] return res
def test_check(self): invoice1 = self.create_invoice( self.partner.id, self.bank, self.eur_currency.id, 2042.0, "Inv9032", ) invoice2 = self.create_invoice( self.partner.id, self.bank, self.eur_currency.id, 1012.0, "Inv9033", ) for inv in [invoice1, invoice2]: action = inv.create_account_payment_line() self.assertEqual(action["res_model"], "account.payment.order") self.payment_order = self.payment_order_model.browse(action["res_id"]) self.assertEqual(self.payment_order.payment_type, "outbound") self.assertEqual(self.payment_order.payment_mode_id, self.payment_mode) self.assertEqual(self.payment_order.journal_id, self.bank_journal) pay_lines = self.payment_line_model.search( [ ("partner_id", "=", self.partner.id), ("order_id", "=", self.payment_order.id), ] ) self.assertEqual(len(pay_lines), 2) asus_pay_line1 = pay_lines[0] accpre = self.env["decimal.precision"].precision_get("Account") self.assertEqual(asus_pay_line1.currency_id, self.eur_currency) self.assertEqual( asus_pay_line1.partner_bank_id, invoice1.partner_bank_id ) self.assertEqual( float_compare( asus_pay_line1.amount_currency, 2042, precision_digits=accpre ), 0, ) self.assertEqual(asus_pay_line1.communication_type, "normal") self.assertEqual(asus_pay_line1.communication, "Inv9032") self.payment_order.draft2open() self.assertEqual(self.payment_order.state, "open") bank_lines = self.bank_line_model.search( [("partner_id", "=", self.partner.id)] ) self.assertEqual(len(bank_lines), 1) asus_bank_line = bank_lines[0] self.assertEqual(asus_bank_line.currency_id, self.eur_currency) self.assertEqual( float_compare( asus_bank_line.amount_currency, 3054.0, precision_digits=accpre ), 0, ) self.assertEqual(asus_bank_line.communication_type, "normal") self.assertEqual(asus_bank_line.communication, "Inv9032-Inv9033") self.assertEqual( asus_bank_line.partner_bank_id, invoice1.partner_bank_id ) action = self.payment_order.open2generated() self.assertEqual(self.payment_order.state, "generated") self.assertEqual(action["res_model"], "ir.attachment") attachment = self.attachment_model.browse(action["res_id"]) self.assertEqual(attachment.datas_fname[-4:], ".pdf")
def move_validate(self): ''' Validate moves based on a production order. ''' moves = self._filter_closed_moves() quant_obj = self.env['stock.quant'] moves_todo = self.env['stock.move'] moves_to_unreserve = self.env['stock.move'] # Create extra moves where necessary for move in moves: # Here, the `quantity_done` was already rounded to the product UOM by the `do_produce` wizard. However, # it is possible that the user changed the value before posting the inventory by a value that should be # rounded according to the move's UOM. In this specific case, we chose to round up the value, because it # is what is expected by the user (if i consumed/produced a little more, the whole UOM unit should be # consumed/produced and the moves are split correctly). rounding = move.product_uom.rounding move.quantity_done = float_round(move.quantity_done, precision_rounding=rounding, rounding_method='UP') if move.quantity_done <= 0: continue moves_todo |= move moves_todo |= move._create_extra_move() # Split moves where necessary and move quants for move in moves_todo: rounding = move.product_uom.rounding if float_compare(move.quantity_done, move.product_uom_qty, precision_rounding=rounding) < 0: # Need to do some kind of conversion here qty_split = move.product_uom._compute_quantity( move.product_uom_qty - move.quantity_done, move.product_id.uom_id) new_move = move.split(qty_split) # If you were already putting stock.move.lots on the next one in the work order, transfer those to the new move move.move_lot_ids.filtered( lambda x: not x.done_wo or x.quantity_done == 0.0).write( {'move_id': new_move}) self.browse(new_move).quantity_done = 0.0 main_domain = [('qty', '>', 0)] preferred_domain = [('reservation_id', '=', move.id)] fallback_domain = [('reservation_id', '=', False)] fallback_domain2 = [ '&', ('reservation_id', '!=', move.id), ('reservation_id', '!=', False) ] preferred_domain_list = [preferred_domain] + [fallback_domain] + [ fallback_domain2 ] if move.has_tracking == 'none': quants = quant_obj.quants_get_preferred_domain( move.product_qty, move, domain=main_domain, preferred_domain_list=preferred_domain_list) self.env['stock.quant'].quants_move( quants, move, move.location_dest_id, owner_id=move.restrict_partner_id.id) else: for movelot in move.active_move_lot_ids: if float_compare(movelot.quantity_done, 0, precision_rounding=rounding) > 0: if not movelot.lot_id: raise UserError( _('You need to supply a lot/serial number.')) qty = move.product_uom._compute_quantity( movelot.quantity_done, move.product_id.uom_id) quants = quant_obj.quants_get_preferred_domain( qty, move, lot_id=movelot.lot_id.id, domain=main_domain, preferred_domain_list=preferred_domain_list) self.env['stock.quant'].quants_move( quants, move, move.location_dest_id, lot_id=movelot.lot_id.id, owner_id=move.restrict_partner_id.id) moves_to_unreserve |= move # Next move in production order if move.move_dest_id and move.move_dest_id.state not in ('done', 'cancel'): move.move_dest_id.action_assign() moves_to_unreserve.quants_unreserve() moves_todo.write({'state': 'done', 'date': fields.Datetime.now()}) return moves_todo