Example #1
0
 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
Example #2
0
    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
Example #3
0
    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
Example #4
0
    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
Example #5
0
    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 {}
Example #7
0
    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()})
Example #8
0
    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]
Example #9
0
 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
Example #10
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
Example #11
0
    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
Example #12
0
 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
Example #13
0
    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
Example #14
0
    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)
Example #15
0
    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
Example #16
0
    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()
Example #17
0
    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,
            })
Example #18
0
 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'
Example #19
0
    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
Example #20
0
 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
Example #21
0
    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()
Example #22
0
 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)]
Example #23
0
 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()
Example #24
0
    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')
Example #25
0
 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
Example #26
0
 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
Example #27
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
Example #28
0
    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]
Example #29
0
 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)
Example #30
0
 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