def _reconcile_out_with_ins(lines, out, ins, demand, only_matching_move_dest=True): index_to_remove = [] for index, in_ in enumerate(ins): if float_is_zero( in_['qty'], precision_rounding=out.product_id.uom_id.rounding): continue if only_matching_move_dest and in_[ 'move_dests'] and out.id not in in_['move_dests']: continue taken_from_in = min(demand, in_['qty']) demand -= taken_from_in lines.append( self._prepare_report_line(taken_from_in, move_in=in_['move'], move_out=out)) in_['qty'] -= taken_from_in if in_['qty'] <= 0: index_to_remove.append(index) if float_is_zero( demand, precision_rounding=out.product_id.uom_id.rounding): break for index in index_to_remove[::-1]: ins.pop(index) return demand
def product_price_update_before_done(self, forced_qty=None): tmpl_dict = defaultdict(lambda: 0.0) # adapt standard price on incomming moves if the product cost_method is 'average' std_price_update = {} for move in self.filtered(lambda move: move.location_id.usage in ('supplier', 'production') and move. product_id.cost_method == 'average'): product_tot_qty_available = move.product_id.qty_available + tmpl_dict[ move.product_id.id] rounding = move.product_id.uom_id.rounding if float_is_zero(product_tot_qty_available, precision_rounding=rounding): new_std_price = move._get_price_unit() elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding): new_std_price = move._get_price_unit() else: # Get the standard price amount_unit = std_price_update.get( (move.company_id.id, move.product_id.id)) or move.product_id.standard_price qty = forced_qty or move.product_qty new_std_price = ( (amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / ( product_tot_qty_available + move.product_qty) tmpl_dict[move.product_id.id] += move.product_qty # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products move.product_id.with_context( force_company=move.company_id.id).sudo().write( {'standard_price': new_std_price}) std_price_update[move.company_id.id, move.product_id.id] = new_std_price
def _compute_average_price(self, qty_invoiced, qty_to_invoice, stock_moves): """Go over the valuation layers of `stock_moves` to value `qty_to_invoice` while taking care of ignoring `qty_invoiced`. If `qty_to_invoice` is greater than what's possible to value with the valuation layers, use the product's standard price. :param qty_invoiced: quantity already invoiced :param qty_to_invoice: quantity to invoice :param stock_moves: recordset of `stock.move` :returns: the anglo saxon price unit :rtype: float """ self.ensure_one() if not qty_to_invoice: return 0.0 # if True, consider the incoming moves is_returned = self.env.context.get('is_returned', False) returned_quantities = defaultdict(float) for move in stock_moves: if move.origin_returned_move_id: returned_quantities[move.origin_returned_move_id.id] += abs(sum(move.sudo().stock_valuation_layer_ids.mapped('quantity'))) candidates = stock_moves\ .sudo()\ .filtered(lambda m: is_returned == bool(m.origin_returned_move_id and sum(m.stock_valuation_layer_ids.mapped('quantity')) >= 0))\ .mapped('stock_valuation_layer_ids')\ .sorted() qty_to_take_on_candidates = qty_to_invoice tmp_value = 0 # to accumulate the value taken on the candidates for candidate in candidates: if not candidate.quantity: continue candidate_quantity = abs(candidate.quantity) if candidate.stock_move_id.id in returned_quantities: candidate_quantity -= returned_quantities[candidate.stock_move_id.id] if float_is_zero(candidate_quantity, precision_rounding=candidate.uom_id.rounding): continue # correction entries if not float_is_zero(qty_invoiced, precision_rounding=candidate.uom_id.rounding): qty_ignored = min(qty_invoiced, candidate_quantity) qty_invoiced -= qty_ignored candidate_quantity -= qty_ignored if float_is_zero(candidate_quantity, precision_rounding=candidate.uom_id.rounding): continue qty_taken_on_candidate = min(qty_to_take_on_candidates, candidate_quantity) qty_to_take_on_candidates -= qty_taken_on_candidate tmp_value += qty_taken_on_candidate * \ ((candidate.value + sum(candidate.stock_valuation_layer_ids.mapped('value'))) / candidate.quantity) if float_is_zero(qty_to_take_on_candidates, precision_rounding=candidate.uom_id.rounding): break # If there's still quantity to invoice but we're out of candidates, we chose the standard # price to estimate the anglo saxon price unit. if not float_is_zero(qty_to_take_on_candidates, precision_rounding=self.uom_id.rounding): negative_stock_value = self.standard_price * qty_to_take_on_candidates tmp_value += negative_stock_value return tmp_value / qty_to_invoice
def _compute_average_price(self, qty_invoiced, qty_to_invoice, stock_moves): """Go over the valuation layers of `stock_moves` to value `qty_to_invoice` while taking care of ignoring `qty_invoiced`. If `qty_to_invoice` is greater than what's possible to value with the valuation layers, use the product's standard price. :param qty_invoiced: quantity already invoiced :param qty_to_invoice: quantity to invoice :param stock_moves: recordset of `stock.move` :returns: the anglo saxon price unit :rtype: float """ self.ensure_one() if not qty_to_invoice: return 0.0 if not qty_to_invoice: return 0 candidates = stock_moves\ .sudo()\ .mapped('stock_valuation_layer_ids')\ .sorted() qty_to_take_on_candidates = qty_to_invoice tmp_value = 0 # to accumulate the value taken on the candidates for candidate in candidates: candidate_quantity = abs(candidate.quantity) if float_is_zero(candidate_quantity, precision_rounding=candidate.uom_id.rounding): continue # correction entries if not float_is_zero(qty_invoiced, precision_rounding=candidate.uom_id.rounding): qty_ignored = min(qty_invoiced, candidate_quantity) qty_invoiced -= qty_ignored candidate_quantity -= qty_ignored if float_is_zero(candidate_quantity, precision_rounding=candidate.uom_id.rounding): continue qty_taken_on_candidate = min(qty_to_take_on_candidates, candidate_quantity) qty_to_take_on_candidates -= qty_taken_on_candidate tmp_value += qty_taken_on_candidate * \ ((candidate.value + sum(candidate.stock_valuation_layer_ids.mapped('value'))) / candidate.quantity) if float_is_zero(qty_to_take_on_candidates, precision_rounding=candidate.uom_id.rounding): break # If there's still quantity to invoice but we're out of candidates, we chose the standard # price to estimate the anglo saxon price unit. if not float_is_zero(qty_to_take_on_candidates, precision_rounding=self.uom_id.rounding): negative_stock_value = self.standard_price * qty_to_take_on_candidates tmp_value += negative_stock_value return tmp_value / qty_to_invoice
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 change_prod_qty(self): precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') for wizard in self: production = wizard.mo_id produced = sum(production.move_finished_ids.filtered(lambda m: m.product_id == production.product_id).mapped('quantity_done')) if wizard.product_qty < produced: format_qty = '%.{precision}f'.format(precision=precision) raise UserError(_("You have already processed %s. Please input a quantity higher than %s ") % (format_qty % produced, format_qty % produced)) production.write({'product_qty': wizard.product_qty}) done_moves = production.move_finished_ids.filtered(lambda x: x.state == 'done' and x.product_id == production.product_id) qty_produced = production.product_id.uom_id._compute_quantity(sum(done_moves.mapped('product_qty')), production.product_uom_id) factor = production.product_uom_id._compute_quantity(production.product_qty - qty_produced, production.bom_id.product_uom_id) / production.bom_id.product_qty boms, lines = production.bom_id.explode(production.product_id, factor, picking_type=production.bom_id.picking_type_id) for line, line_data in lines: production._update_raw_move(line, line_data) operation_bom_qty = {} for bom, bom_data in boms: for operation in bom.routing_id.operation_ids: operation_bom_qty[operation.id] = bom_data['qty'] self._update_product_to_produce(production, production.product_qty - qty_produced) moves = production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) moves._action_assign() for wo in production.workorder_ids: operation = wo.operation_id if operation_bom_qty.get(operation.id): cycle_number = math.ceil(operation_bom_qty[operation.id] / operation.workcenter_id.capacity) # TODO: float_round UP wo.duration_expected = (operation.workcenter_id.time_start + operation.workcenter_id.time_stop + cycle_number * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency) quantity = wo.qty_production - wo.qty_produced if production.product_id.tracking == 'serial': quantity = 1.0 if not float_is_zero(quantity, precision_digits=precision) else 0.0 else: quantity = quantity if (quantity > 0) else 0 if float_is_zero(quantity, precision_digits=precision): wo.final_lot_id = False wo.active_move_line_ids.unlink() wo.qty_producing = quantity if wo.qty_produced < wo.qty_production and wo.state == 'done': wo.state = 'progress' # assign moves; last operation receive all unassigned moves # TODO: following could be put in a function as it is similar as code in _workorders_create # TODO: only needed when creating new moves moves_raw = production.move_raw_ids.filtered(lambda move: move.operation_id == operation and move.state not in ('done', 'cancel')) if wo == production.workorder_ids[-1]: moves_raw |= production.move_raw_ids.filtered(lambda move: not move.operation_id) moves_finished = production.move_finished_ids.filtered(lambda move: move.operation_id == operation) #TODO: code does nothing, unless maybe by_products? moves_raw.mapped('move_line_ids').write({'workorder_id': wo.id}) (moves_finished + moves_raw).write({'workorder_id': wo.id}) if quantity > 0 and wo.move_raw_ids.filtered(lambda x: x.product_id.tracking != 'none') and not wo.active_move_line_ids: wo._generate_lot_ids() return {}
def product_price_update_before_done(self, forced_qty=None): tmpl_dict = defaultdict(lambda: 0.0) # adapt standard price on incomming moves if the product cost_method is 'average' std_price_update = {} for move in self.filtered( lambda move: move._is_in() and move.with_company( move.company_id).product_id.cost_method == 'average'): product_tot_qty_available = move.product_id.sudo().with_company( move.company_id).quantity_svl + tmpl_dict[move.product_id.id] rounding = move.product_id.uom_id.rounding valued_move_lines = move._get_in_move_lines() qty_done = 0 for valued_move_line in valued_move_lines: qty_done += valued_move_line.product_uom_id._compute_quantity( valued_move_line.qty_done, move.product_id.uom_id) qty = forced_qty or qty_done if float_is_zero(product_tot_qty_available, precision_rounding=rounding): new_std_price = move._get_price_unit() elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding) or \ float_is_zero(product_tot_qty_available + qty, precision_rounding=rounding): new_std_price = move._get_price_unit() else: # Get the standard price amount_unit = std_price_update.get( (move.company_id.id, move.product_id.id)) or move.product_id.with_company( move.company_id).standard_price new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / ( product_tot_qty_available + qty) tmpl_dict[move.product_id.id] += qty_done # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products move.product_id.with_company(move.company_id.id).with_context( disable_auto_svl=True).sudo().write( {'standard_price': new_std_price}) std_price_update[move.company_id.id, move.product_id.id] = new_std_price # adapt standard price on incomming moves if the product cost_method is 'fifo' for move in self.filtered( lambda move: move.with_company(move.company_id).product_id. cost_method == 'fifo' and float_is_zero( move.product_id.sudo().quantity_svl, precision_rounding=move.product_id.uom_id.rounding)): move.product_id.with_company(move.company_id.id).sudo().write( {'standard_price': move._get_price_unit()})
def _website_price(self): qty = self._context.get('quantity', 1.0) partner = self.env.user.partner_id current_website = self.env['website'].get_current_website() pricelist = current_website.get_current_pricelist() company_id = current_website.company_id context = dict(self._context, pricelist=pricelist.id, partner=partner) self2 = self.with_context( context) if self._context != context else self ret = self.env.user.has_group( 'sale.group_show_price_subtotal' ) and 'total_excluded' or 'total_included' for p, p2 in pycompat.izip(self, self2): taxes = partner.property_account_position_id.map_tax( p.sudo().taxes_id.filtered( lambda x: x.company_id == company_id)) p.website_price = taxes.compute_all(p2.price, pricelist.currency_id, quantity=qty, product=p2, partner=partner)[ret] price_without_pricelist = taxes.compute_all( p.list_price, pricelist.currency_id)[ret] p.website_price_difference = False if float_is_zero( price_without_pricelist - p.website_price, precision_rounding=pricelist.currency_id.rounding) else True p.website_public_price = taxes.compute_all(p2.lst_price, quantity=qty, product=p2, partner=partner)[ret]
def _compute_new_value(self): for reval in self: reval.new_value = reval.current_value_svl + reval.added_value if not float_is_zero(reval.current_quantity_svl, precision_rounding=self.product_id.uom_id.rounding): reval.new_value_by_qty = reval.new_value / reval.current_quantity_svl else: reval.new_value_by_qty = 0.0
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.company.currency_id.decimal_places for landed_cost in self: total_amount = sum(landed_cost.valuation_adjustment_lines.mapped('additional_landed_cost')) if not tools.float_is_zero(total_amount - landed_cost.amount_total, precision_digits=prec_digits): 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(not tools.float_is_zero(cost_line.price_unit - val_amount, precision_digits=prec_digits) for cost_line, val_amount in val_to_cost_lines.items()): return False return True
def _svl_empty_stock(self, description, product_category=None, product_template=None): impacted_product_ids = [] impacted_products = self.env['product.product'] products_orig_quantity_svl = {} # get the impacted products domain = [('type', '=', 'product')] if product_category is not None: domain += [('categ_id', '=', product_category.id)] elif product_template is not None: domain += [('product_tmpl_id', '=', product_template.id)] else: raise ValueError() products = self.env['product.product'].search_read(domain, ['quantity_svl']) for product in products: impacted_product_ids.append(product['id']) products_orig_quantity_svl[product['id']] = product['quantity_svl'] impacted_products |= self.env['product.product'].browse(impacted_product_ids) # empty out the stock for the impacted products empty_stock_svl_list = [] for product in impacted_products: # FIXME sle: why not use products_orig_quantity_svl here? if float_is_zero(product.quantity_svl, precision_rounding=product.uom_id.rounding): # FIXME: create an empty layer to track the change? continue svsl_vals = product._prepare_out_svl_vals(product.quantity_svl, self.env.company) svsl_vals['description'] = description + svsl_vals.pop('rounding_adjustment', '') svsl_vals['company_id'] = self.env.company.id empty_stock_svl_list.append(svsl_vals) return empty_stock_svl_list, products_orig_quantity_svl, impacted_products
def create(self, values): line = super(SaleOrderLine, self).create(values) precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') # check ordered quantity to avoid create project/task when expensing service products if line.state == 'sale' and not float_is_zero(line.product_uom_qty, precision_digits=precision): line._timesheet_service_generation() return line
def action_pos_order_paid(self): self.ensure_one() # TODO: add support for mix of cash and non-cash payments when both cash_rounding and only_round_cash_method are True if not self.config_id.cash_rounding \ or self.config_id.only_round_cash_method \ and not any(p.payment_method_id.is_cash_count for p in self.payment_ids): total = self.amount_total else: total = float_round(self.amount_total, precision_rounding=self.config_id.rounding_method.rounding, rounding_method=self.config_id.rounding_method.rounding_method) isPaid = float_is_zero(total - self.amount_paid, precision_rounding=self.currency_id.rounding) if not isPaid and not self.config_id.cash_rounding: raise UserError(_("Order %s is not fully paid.", self.name)) elif not isPaid and self.config_id.cash_rounding: currency = self.currency_id if self.config_id.rounding_method.rounding_method == "HALF-UP": maxDiff = currency.round(self.config_id.rounding_method.rounding / 2) else: maxDiff = currency.round(self.config_id.rounding_method.rounding) diff = currency.round(self.amount_total - self.amount_paid) if not abs(diff) <= maxDiff: raise UserError(_("Order %s is not fully paid.", self.name)) self.write({'state': 'paid'}) return True
def _create_out_svl(self, forced_quantity=None): """Create a `stock.valuation.layer` from `self`. :param forced_quantity: under some circunstances, the quantity to value is different than the initial demand of the move (Default value = None) """ svl_vals_list = [] for move in self: move = move.with_company(move.company_id) valued_move_lines = move._get_out_move_lines() valued_quantity = 0 for valued_move_line in valued_move_lines: valued_quantity += valued_move_line.product_uom_id._compute_quantity( valued_move_line.qty_done, move.product_id.uom_id) if float_is_zero( forced_quantity or valued_quantity, precision_rounding=move.product_id.uom_id.rounding): continue svl_vals = move.product_id._prepare_out_svl_vals( forced_quantity or valued_quantity, move.company_id) svl_vals.update(move._prepare_common_svl_vals()) if forced_quantity: svl_vals[ 'description'] = 'Correction of %s (modification of past move)' % move.picking_id.name or move.name svl_vals['description'] += svl_vals.pop('rounding_adjustment', '') svl_vals_list.append(svl_vals) return self.env['stock.valuation.layer'].sudo().create(svl_vals_list)
def _sale_get_invoice_price(self, order): """ Based on the current move line, compute the price to reinvoice the analytic line that is going to be created (so the price of the sale line). """ self.ensure_one() unit_amount = self.quantity amount = (self.credit or 0.0) - (self.debit or 0.0) if self.product_id.expense_policy == 'sales_price': return self.product_id.with_context( partner=order.partner_id.id, date_order=order.date_order, pricelist=order.pricelist_id.id, uom=self.product_uom_id.id).price uom_precision_digits = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') if float_is_zero(unit_amount, precision_digits=uom_precision_digits): return 0.0 # Prevent unnecessary currency conversion that could be impacted by exchange rate # fluctuations if self.company_id.currency_id and amount and self.company_id.currency_id == order.currency_id: return abs(amount / unit_amount) price_unit = abs(amount / unit_amount) currency_id = self.company_id.currency_id if currency_id and currency_id != order.currency_id: price_unit = currency_id._convert( price_unit, order.currency_id, order.company_id, order.date_order or fields.Date.today()) return price_unit
def check(self): """Check the order: if the order is not paid: continue payment, if the order is paid print ticket. """ self.ensure_one() order = self.env['pos.order'].browse( self.env.context.get('active_id', False)) currency = order.currency_id init_data = self.read()[0] if not float_is_zero(init_data['amount'], precision_rounding=currency.rounding): order.add_payment({ 'pos_order_id': order.id, 'amount': order._get_rounded_amount(init_data['amount']), 'name': init_data['payment_name'], 'payment_method_id': init_data['payment_method_id'][0], }) if order._is_pos_order_paid(): order.action_pos_order_paid() order._create_order_picking() return {'type': 'ir.actions.act_window_close'} return self.launch_payment()
def _compute_document_discount(self): for move in self: document_discount = 0 discount_used = move.discount_type and not float_is_zero(move.discount_value, precision_digits=move.currency_id.decimal_places) if discount_used: if move.discount_type == 'fixed': document_discount = move.discount_value * -1 else: document_discount = (move.amount_gross * (move.discount_value / 100)) * -1 document_discount = float_round(document_discount, precision_digits=move.currency_id.decimal_places) discount_lines = move.line_ids.filtered(lambda f: f.is_document_discount_line) if discount_lines: document_discount_tax_amount = float_round( sum(line.price_total - line.price_subtotal for line in discount_lines), precision_digits=move.currency_id.decimal_places ) else: document_discount_tax_amount = 0 move.update({ 'document_discount': document_discount, 'document_discount_tax_amount': document_discount_tax_amount, 'has_document_discount': discount_used, 'discount_value_percent': move.discount_value, })
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 not line.order_id.invoice_policy: 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' else: 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.order_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 run(self, procurements, raise_user_error=True): """Fulfil `procurements` with the help of stock rules. Procurements are needs of products at a certain location. To fulfil these needs, we need to create some sort of documents (`stock.move` by default, but extensions of `_run_` methods allow to create every type of documents). :param procurements: the description of the procurement :type list: list of `~flectra.addons.stock.models.stock_rule.ProcurementGroup.Procurement` :param raise_user_error: will raise either an UserError or a ProcurementException :type raise_user_error: boolan, optional :raises UserError: if `raise_user_error` is True and a procurement isn't fulfillable :raises ProcurementException: if `raise_user_error` is False and a procurement isn't fulfillable """ def raise_exception(procurement_errors): if raise_user_error: dummy, errors = zip(*procurement_errors) raise UserError('\n'.join(errors)) else: raise ProcurementException(procurement_errors) actions_to_run = defaultdict(list) procurement_errors = [] for procurement in procurements: procurement.values.setdefault('company_id', procurement.location_id.company_id) procurement.values.setdefault('priority', '0') procurement.values.setdefault('date_planned', fields.Datetime.now()) if ( procurement.product_id.type not in ('consu', 'product') or float_is_zero(procurement.product_qty, precision_rounding=procurement.product_uom.rounding) ): continue rule = self._get_rule(procurement.product_id, procurement.location_id, procurement.values) if not rule: error = _('No rule has been found to replenish "%s" in "%s".\nVerify the routes configuration on the product.') %\ (procurement.product_id.display_name, procurement.location_id.display_name) procurement_errors.append((procurement, error)) else: action = 'pull' if rule.action == 'pull_push' else rule.action actions_to_run[action].append((procurement, rule)) if procurement_errors: raise_exception(procurement_errors) for action, procurements in actions_to_run.items(): if hasattr(self.env['stock.rule'], '_run_%s' % action): try: getattr(self.env['stock.rule'], '_run_%s' % action)(procurements) except ProcurementException as e: procurement_errors += e.procurement_exceptions else: _logger.error("The method _run_%s doesn't exist on the procurement rules" % action) if procurement_errors: raise_exception(procurement_errors) return True
def _should_bypass_set_qty_producing(self): if self.state in ('done', 'cancel'): return True # Do not update extra product quantities if float_is_zero(self.product_uom_qty, precision_rounding=self.product_uom.rounding): return True if self.has_tracking != 'none' or self.state == 'done': return True return False
def _compute_document_discount(self): for move in self: discount_used = move.discount_type and not float_is_zero(move.discount_value, precision_digits=move.currency_id.decimal_places) if discount_used: amount_gross = 0 for line in move.invoice_line_ids: amount_gross += line.quantity * line.price_unit untaxed_amount = amount_gross taxed_amt = 0 for line in move.line_ids: if line.tax_ids: for tax in line.tax_ids: taxed_amt += line.price_unit * tax.amount / 100 total_amt = untaxed_amount + taxed_amt if move.discount_type == 'fixed': document_discount = move.discount_value if document_discount >= total_amt: raise UserError(_("Discount Cannot be more than or equal to Total Amount")) for line in move.invoice_line_ids: if line.product_id: tax = 0 if line.tax_ids: for taxes_line in line.tax_ids: tax += line.price_unit * taxes_line.amount / 100 line_price = line.price_unit + tax discount_value = line_price * document_discount / total_amt discount_ratio = discount_value / line_price * 100 if discount_ratio: line.update({ 'discount': discount_ratio }) move._move_autocomplete_invoice_lines_values() else: document_discount = (total_amt * (move.discount_value / 100)) if document_discount >= total_amt: raise UserError(_("Discount Cannat be more than or equal to Total Amount")) for line in move.invoice_line_ids: if line.product_id: tax = 0 if line.tax_ids: for taxes_line in line.tax_ids: tax += line.price_unit * taxes_line.amount / 100 line_price = line.price_unit + tax discount_value = line_price * document_discount / total_amt discount_ratio = discount_value / line_price * 100 if discount_ratio: line.update({ 'discount': discount_ratio }) move._move_autocomplete_invoice_lines_values()
def _search_difference_qty(self, operator, value): if operator == '=': result = True elif operator == '!=': result = False else: raise NotImplementedError() lines = self.search([('inventory_id', '=', self.env.context.get('default_inventory_id'))]) line_ids = lines.filtered(lambda line: float_is_zero(line.difference_qty, line.product_id.uom_id.rounding) == result).ids return [('id', 'in', line_ids)]
def test_paid(self): if self.config_id.cash_rounding: total = float_round( self.amount_total, precision_rounding=self.config_id.rounding_method.rounding, rounding_method=self.config_id.rounding_method.rounding_method) return float_is_zero( total - self.amount_paid, precision_rounding=self.config_id.currency_id.rounding) else: return super(PosOrder, self).test_paid()
def test_timesheet_manual(self): """ Test timesheet invoicing with 'invoice on delivery' timetracked products """ # create SO and confirm it sale_order = self.env['sale.order'].create({ 'partner_id': self.partner_usd.id, 'partner_invoice_id': self.partner_usd.id, 'partner_shipping_id': self.partner_usd.id, 'pricelist_id': self.pricelist_usd.id, }) so_line_manual_global_project = self.env['sale.order.line'].create({ 'name': self.product_delivery_manual2.name, 'product_id': self.product_delivery_manual2.id, 'product_uom_qty': 50, 'product_uom': self.product_delivery_manual2.uom_id.id, 'price_unit': self.product_delivery_manual2.list_price, 'order_id': sale_order.id, }) so_line_manual_only_project = self.env['sale.order.line'].create({ 'name': self.product_delivery_manual4.name, 'product_id': self.product_delivery_manual4.id, 'product_uom_qty': 20, 'product_uom': self.product_delivery_manual4.uom_id.id, 'price_unit': self.product_delivery_manual4.list_price, 'order_id': sale_order.id, }) # confirm SO sale_order.action_confirm() self.assertTrue(sale_order.project_project_id, "Sales Order should have create a project") self.assertEqual(sale_order.invoice_status, 'no', 'Sale Timesheet: manually product should not need to be invoiced on so confirmation') # let's log some timesheets (on task and project) timesheet1 = self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': self.project_global.id, # global project 'task_id': so_line_manual_global_project.task_id.id, 'unit_amount': 6, 'employee_id': self.employee_manager.id, }) timesheet2 = self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': sale_order.project_project_id.id, # global project 'unit_amount': 3, 'employee_id': self.employee_manager.id, }) self.assertEqual(so_line_manual_global_project.task_id.sale_line_id, so_line_manual_global_project, "Task from a milestone product should be linked to its SO line too") self.assertEqual(timesheet1.timesheet_invoice_type, 'billable_fixed', "Milestone timesheet goes in billable fixed category") self.assertTrue(float_is_zero(so_line_manual_global_project.qty_delivered, precision_digits=2), "Milestone Timesheeting should not incremented the delivered quantity on the SO line") self.assertEqual(so_line_manual_global_project.qty_to_invoice, 0.0, "Manual service should not be affected by timesheet on their created task.") self.assertEqual(so_line_manual_only_project.qty_to_invoice, 0.0, "Manual service should not be affected by timesheet on their created project.") self.assertEqual(sale_order.invoice_status, 'no', 'Sale Timesheet: "invoice on delivery" should not need to be invoiced on so confirmation')
def _compute_hide_line(self): for rec in self: report = rec.report_id r = (rec.currency_id or report.company_id.currency_id).rounding if report.hide_account_at_0 and ( float_is_zero(rec.initial_balance, precision_rounding=r) and float_is_zero(rec.final_balance, precision_rounding=r) and float_is_zero(rec.debit, precision_rounding=r) and float_is_zero(rec.credit, precision_rounding=r)): rec.hide_line = True elif report.limit_hierarchy_level and report.show_hierarchy_level: if report.hide_parent_hierarchy_level: distinct_level = rec.level != report.show_hierarchy_level if rec.account_group_id and distinct_level: rec.hide_line = True elif rec.level and distinct_level: rec.hide_line = True elif not report.hide_parent_hierarchy_level and \ rec.level > report.show_hierarchy_level: rec.hide_line = True
def _compute_kit_quantities(self, product_id, kit_qty, kit_bom, filters): """ Computes the quantity delivered or received when a kit is sold or purchased. A ratio 'qty_processed/qty_needed' is computed for each component, and the lowest one is kept to define the kit's quantity delivered or received. :param product_id: The kit itself a.k.a. the finished product :param kit_qty: The quantity from the order line :param kit_bom: The kit's BoM :param filters: Dict of lambda expression to define the moves to consider and the ones to ignore :return: The quantity delivered or received """ qty_ratios = [] boms, bom_sub_lines = kit_bom.explode(product_id, kit_qty) for bom_line, bom_line_data in bom_sub_lines: # skip service since we never deliver them if bom_line.product_id.type == 'service': continue if float_is_zero( bom_line_data['qty'], precision_rounding=bom_line.product_uom_id.rounding): # As BoMs allow components with 0 qty, a.k.a. optionnal components, we simply skip those # to avoid a division by zero. continue bom_line_moves = self.filtered(lambda m: m.bom_line_id == bom_line) if bom_line_moves: # We compute the quantities needed of each components to make one kit. # Then, we collect every relevant moves related to a specific component # to know how many are considered delivered. uom_qty_per_kit = bom_line_data['qty'] / bom_line_data[ 'original_qty'] qty_per_kit = bom_line.product_uom_id._compute_quantity( uom_qty_per_kit, bom_line.product_id.uom_id, round=False) if not qty_per_kit: continue incoming_moves = bom_line_moves.filtered( filters['incoming_moves']) outgoing_moves = bom_line_moves.filtered( filters['outgoing_moves']) qty_processed = sum( incoming_moves.mapped('product_qty')) - sum( outgoing_moves.mapped('product_qty')) # We compute a ratio to know how many kits we can produce with this quantity of that specific component qty_ratios.append( float_round(qty_processed / qty_per_kit, precision_rounding=bom_line.product_id.uom_id. rounding)) else: return 0.0 if qty_ratios: # Now that we have every ratio by components, we keep the lowest one to know how many kits we can produce # with the quantities delivered of each component. We use the floor division here because a 'partial kit' # doesn't make sense. return min(qty_ratios) // 1 else: return 0.0
def _action_done(self, cancel_backorder=False): # Init a dict that will group the moves by valuation type, according to `move._is_valued_type`. valued_moves = { valued_type: self.env['stock.move'] for valued_type in self._get_valued_types() } for move in self: if float_is_zero(move.quantity_done, precision_rounding=move.product_uom.rounding): continue for valued_type in self._get_valued_types(): if getattr(move, '_is_%s' % valued_type)(): valued_moves[valued_type] |= move # AVCO application valued_moves['in'].product_price_update_before_done() res = super(StockMove, self)._action_done(cancel_backorder=cancel_backorder) # '_action_done' might have created an extra move to be valued for move in res - self: for valued_type in self._get_valued_types(): if getattr(move, '_is_%s' % valued_type)(): valued_moves[valued_type] |= move stock_valuation_layers = self.env['stock.valuation.layer'].sudo() # Create the valuation layers in batch by calling `moves._create_valued_type_svl`. for valued_type in self._get_valued_types(): todo_valued_moves = valued_moves[valued_type] if todo_valued_moves: todo_valued_moves._sanity_check_for_valuation() stock_valuation_layers |= getattr( todo_valued_moves, '_create_%s_svl' % valued_type)() for svl in stock_valuation_layers: if not svl.product_id.valuation == 'real_time': continue if svl.currency_id.is_zero(svl.value): continue svl.stock_move_id._account_entry_move(svl.quantity, svl.description, svl.id, svl.value) stock_valuation_layers._check_company() # For every in move, run the vacuum for the linked product. products_to_vacuum = valued_moves['in'].mapped('product_id') company = valued_moves['in'].mapped('company_id') and valued_moves[ 'in'].mapped('company_id')[0] or self.env.company for product_to_vacuum in products_to_vacuum: product_to_vacuum._run_fifo_vacuum(company) return res
def _amount_by_group(self): for order in self: if float_is_zero( order.document_discount, precision_digits=order.currency_id.decimal_places): super(SaleOrder, order)._amount_by_group() continue currency = order.currency_id or order.company_id.currency_id fmt = partial(formatLang, self.with_context(lang=order.partner_id.lang).env, currency_obj=currency) res = {} for line in order.order_line: price_reduce = line.price_unit * (1.0 - line.discount / 100.0) taxes = line.tax_id.compute_all( price_reduce, quantity=line.product_uom_qty, product=line.product_id, partner=order.partner_shipping_id, )['taxes'] for tax in line.tax_id: group = tax.tax_group_id res.setdefault(group, {'amount': 0.0, 'base': 0.0}) for t in taxes: if t['id'] == tax.id or t[ 'id'] in tax.children_tax_ids.ids: res[group]['amount'] += t['amount'] res[group]['base'] += t['base'] for distribution in order._get_document_tax_distribution( order.amount_discountable).values(): group = distribution['tax'].tax_group_id taxes = distribution['tax'].compute_all( order.document_discount * distribution['factor'], partner=order.partner_shipping_id, is_refund=True, )['taxes'] for t in taxes: if t['id'] == distribution['tax'].id or t[ 'id'] in distribution['tax'].children_tax_ids.ids: res[group]['amount'] += t['amount'] res[group]['base'] += t['base'] res = sorted(res.items(), key=lambda l: l[0].sequence) order.amount_by_group = [( l[0].name, l[1]['amount'], l[1]['base'], fmt(l[1]['amount']), fmt(l[1]['base']), len(res), ) for l in res]
def write(self, vals): if 'qty_done' in vals: for move_line in self: if move_line.state != 'done': continue move = move_line.move_id rounding = move.product_id.uom_id.rounding diff = vals['qty_done'] - move_line.qty_done if float_is_zero(diff, precision_rounding=rounding): continue self._create_correction_svl(move, diff) return super(StockMoveLine, self).write(vals)
def create(self, vals_list): move_lines = super(StockMoveLine, self).create(vals_list) for move_line in move_lines: if move_line.state != 'done': continue move = move_line.move_id rounding = move.product_id.uom_id.rounding diff = move_line.qty_done if float_is_zero(diff, precision_rounding=rounding): continue self._create_correction_svl(move, diff) return move_lines