Ejemplo n.º 1
0
 def _get_extra_move_lines_vals(self):
     res = super(PosSession, self)._get_extra_move_lines_vals()
     if not self.config_id.cash_rounding:
         return res
     rounding_difference = {'amount': 0.0, 'amount_converted': 0.0}
     rounding_vals = []
     for order in self.order_ids:
         if not order.is_invoiced:
             rounding_difference['amount'] += self.currency_id.round(
                 order.amount_paid - order.amount_total)
     if not self.is_in_company_currency:
         difference = sum(self.move_id.line_ids.mapped('debit')) - sum(
             self.move_id.line_ids.mapped('credit'))
         rounding_difference[
             'amount_converted'] = self.company_id.currency_id.round(
                 difference)
     else:
         rounding_difference['amount_converted'] = rounding_difference[
             'amount']
     if (not float_is_zero(rounding_difference['amount'],
                           precision_rounding=self.currency_id.rounding)
             or not float_is_zero(
                 rounding_difference['amount_converted'],
                 precision_rounding=self.company_id.currency_id.rounding)):
         rounding_vals += [
             self._get_rounding_difference_vals(
                 rounding_difference['amount'],
                 rounding_difference['amount_converted'])
         ]
     return res + rounding_vals
Ejemplo n.º 2
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

        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
Ejemplo n.º 3
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
Ejemplo n.º 4
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_context(force_company=move.company_id.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_list.append(svl_vals)
        return self.env['stock.valuation.layer'].sudo().create(svl_vals_list)
Ejemplo n.º 5
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_context(force_company=move.company_id.id).product_id.
                                  cost_method == 'average'):
            product_tot_qty_available = move.product_id.sudo().with_context(
                force_company=move.company_id.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_context(
                         force_company=move.company_id.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_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
Ejemplo n.º 6
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
Ejemplo n.º 7
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
Ejemplo n.º 8
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)
Ejemplo n.º 9
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_is_zero(0.0, precision_rounding=0.0)

        with self.assertRaises(AssertionError):
            float_is_zero(0.0, precision_rounding=-0.1)

        with self.assertRaises(AssertionError):
            float_compare(0.01,
                          0.02,
                          precision_digits=3,
                          precision_rounding=0.01)

        with self.assertRaises(AssertionError):
            float_compare(1.0, 1.0, precision_rounding=0.0)

        with self.assertRaises(AssertionError):
            float_compare(1.0, 1.0, precision_rounding=-0.1)

        with self.assertRaises(AssertionError):
            float_round(0.01, precision_digits=3, precision_rounding=0.01)

        with self.assertRaises(AssertionError):
            float_round(1.25, precision_rounding=0.0)

        with self.assertRaises(AssertionError):
            float_round(1.25, precision_rounding=-0.1)
Ejemplo n.º 10
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)]
Ejemplo n.º 11
0
 def _costs_generate(self):
     """ Calculates total costs at the end of the production.
     """
     self.ensure_one()
     AccountAnalyticLine = self.env['account.analytic.line'].sudo()
     for wc_line in self.workorder_ids.filtered(
             'workcenter_id.costs_hour_account_id'):
         vals = self._prepare_wc_analytic_line(wc_line)
         precision_rounding = wc_line.workcenter_id.costs_hour_account_id.currency_id.rounding
         if not float_is_zero(vals.get('amount', 0.0),
                              precision_rounding=precision_rounding):
             # we use SUPERUSER_ID as we do not guarantee an mrp user
             # has access to account analytic lines but still should be
             # able to produce orders
             AccountAnalyticLine.create(vals)
Ejemplo n.º 12
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:
         bom_line_moves = self.filtered(lambda m: m.bom_line_id == bom_line)
         if bom_line_moves:
             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
             # 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)
             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(qty_processed / qty_per_kit)
         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
Ejemplo n.º 13
0
 def _onchange_qty_done(self):
     """ When the user is encoding a produce line for a tracked product, we apply some logic to
     help him. This onchange will warn him if he set `qty_done` to a non-supported value.
     """
     res = {}
     if self.product_id.tracking == 'serial' and not float_is_zero(
             self.qty_done, self.product_uom_id.rounding):
         if float_compare(
                 self.qty_done,
                 1.0,
                 precision_rounding=self.product_uom_id.rounding) != 0:
             message = _(
                 'You can only process 1.0 %s of products with unique serial number.'
             ) % self.product_id.uom_id.name
             res['warning'] = {'title': _('Warning'), 'message': message}
     return res
Ejemplo n.º 14
0
 def _generate_moves(self):
     vals_list = []
     for line in self:
         virtual_location = line._get_virtual_location()
         rounding = line.product_id.uom_id.rounding
         if float_is_zero(line.difference_qty, precision_rounding=rounding):
             continue
         if line.difference_qty > 0:  # found more than expected
             vals = line._get_move_values(line.difference_qty,
                                          virtual_location.id,
                                          line.location_id.id, False)
         else:
             vals = line._get_move_values(abs(line.difference_qty),
                                          line.location_id.id,
                                          virtual_location.id, True)
         vals_list.append(vals)
     return self.env['stock.move'].create(vals_list)
Ejemplo n.º 15
0
    def run(self, procurements):
        """ Method used in a procurement case. The purpose is to supply the
        product passed as argument in the location also given as an argument.
        In order to be able to find a suitable location that provide the product
        it will search among stock.rule.
        """
        actions_to_run = defaultdict(list)
        errors = []
        for procurement in procurements:
            procurement.values.setdefault('company_id', self.env.company)
            procurement.values.setdefault('priority', '1')
            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:
                errors.append(_('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))
            else:
                action = 'pull' if rule.action == 'pull_push' else rule.action
                actions_to_run[action].append((procurement, rule))

        if errors:
            raise UserError('\n'.join(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 UserError as e:
                    errors.append(e.name)
            else:
                _logger.error("The method _run_%s doesn't exist on the procurement rules" % action)

        if errors:
            raise UserError('\n'.join(errors))
        return True
Ejemplo n.º 16
0
    def _check_rule_propositions(self, statement_line, candidates):
        ''' Check restrictions that can't be handled for each move.line separately.
        /!\ Only used by models having a type equals to 'invoice_matching'.
        :param statement_line:  An account.bank.statement.line record.
        :param candidates:      Fetched account.move.lines from query (dict).
        :return:                True if the reconciliation propositions are accepted. False otherwise.
        '''
        if not self.match_total_amount:
            return True
        if not candidates:
            return False

        # Match total residual amount.
        line_residual = statement_line.currency_id and statement_line.amount_currency or statement_line.amount
        line_currency = statement_line.currency_id or statement_line.journal_id.currency_id or statement_line.company_id.currency_id
        total_residual = 0.0
        for aml in candidates:
            if aml['account_internal_type'] == 'liquidity':
                partial_residual = aml['aml_currency_id'] and aml['aml_amount_currency'] or aml['aml_balance']
            else:
                partial_residual = aml['aml_currency_id'] and aml['aml_amount_residual_currency'] or aml['aml_amount_residual']
            partial_currency = aml['aml_currency_id'] and self.env['res.currency'].browse(aml['aml_currency_id']) or self.company_id.currency_id
            if partial_currency != line_currency:
                partial_residual = partial_currency._convert(partial_residual, line_currency, self.company_id, statement_line.date)
            total_residual += partial_residual

        # Statement line amount is equal to the total residual.
        if float_is_zero(total_residual - line_residual, precision_rounding=line_currency.rounding):
            return True

        line_residual_to_compare = line_residual if line_residual > 0.0 else -line_residual
        total_residual_to_compare = total_residual if line_residual > 0.0 else -total_residual

        if line_residual_to_compare > total_residual_to_compare:
            amount_percentage = (total_residual_to_compare / line_residual_to_compare) * 100
        elif total_residual:
            amount_percentage = (line_residual_to_compare / total_residual_to_compare) * 100 if total_residual_to_compare else 0.0
        else:
            return False
        return amount_percentage >= self.match_total_amount_param
Ejemplo n.º 17
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['company_id'] = self.env.company.id
            empty_stock_svl_list.append(svsl_vals)
        return empty_stock_svl_list, products_orig_quantity_svl, impacted_products
Ejemplo n.º 18
0
 def summary(self):
     res = super(EventRegistration, self).summary()
     if self.event_ticket_id.product_id.image_128:
         res['image'] = '/web/image/product.product/%s/image_128' % self.event_ticket_id.product_id.id
     information = res.setdefault('information', {})
     information.append((_('Name'), self.name))
     information.append((_('Ticket'), self.event_ticket_id.name
                         or _('None')))
     order = self.sale_order_id.sudo()
     order_line = self.sale_order_line_id.sudo()
     if not order or float_is_zero(
             order_line.price_total,
             precision_digits=order.currency_id.rounding):
         payment_status = _('Free')
     elif not order.invoice_ids or any(
             invoice.invoice_payment_state != 'paid'
             for invoice in order.invoice_ids):
         payment_status = _('To pay')
         res['alert'] = _('The registration must be paid')
     else:
         payment_status = _('Paid')
     information.append((_('Payment'), payment_status))
     return res
Ejemplo n.º 19
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()
            return {'type': 'ir.actions.act_window_close'}

        return self.launch_payment()
Ejemplo n.º 20
0
    def _run_fifo_vacuum(self, company=None):
        """Compensate layer valued at an estimated price with the price of future receipts
        if any. If the estimated price is equals to the real price, no layer is created but
        the original layer is marked as compensated.

        :param company: recordset of `res.company` to limit the execution of the vacuum
        """
        self.ensure_one()
        if company is None:
            company = self.env.company
        svls_to_vacuum = self.env['stock.valuation.layer'].sudo().search(
            [
                ('product_id', '=', self.id),
                ('remaining_qty', '<', 0),
                ('stock_move_id', '!=', False),
                ('company_id', '=', company.id),
            ],
            order='create_date, id')
        for svl_to_vacuum in svls_to_vacuum:
            domain = [('company_id', '=', svl_to_vacuum.company_id.id),
                      ('product_id', '=', self.id),
                      ('remaining_qty', '>', 0), '|',
                      ('create_date', '>', svl_to_vacuum.create_date), '&',
                      ('create_date', '=', svl_to_vacuum.create_date),
                      ('id', '>', svl_to_vacuum.id)]
            candidates = self.env['stock.valuation.layer'].sudo().search(
                domain)
            if not candidates:
                break
            qty_to_take_on_candidates = abs(svl_to_vacuum.remaining_qty)
            qty_taken_on_candidates = 0
            tmp_value = 0
            for candidate in candidates:
                qty_taken_on_candidate = min(candidate.remaining_qty,
                                             qty_to_take_on_candidates)
                qty_taken_on_candidates += qty_taken_on_candidate

                candidate_unit_cost = candidate.remaining_value / candidate.remaining_qty
                value_taken_on_candidate = qty_taken_on_candidate * candidate_unit_cost
                value_taken_on_candidate = candidate.currency_id.round(
                    value_taken_on_candidate)
                new_remaining_value = candidate.remaining_value - value_taken_on_candidate

                candidate_vals = {
                    'remaining_qty':
                    candidate.remaining_qty - qty_taken_on_candidate,
                    'remaining_value': new_remaining_value
                }
                candidate.write(candidate_vals)

                qty_to_take_on_candidates -= qty_taken_on_candidate
                tmp_value += value_taken_on_candidate
                if float_is_zero(qty_to_take_on_candidates,
                                 precision_rounding=self.uom_id.rounding):
                    break

            # Get the estimated value we will correct.
            remaining_value_before_vacuum = svl_to_vacuum.unit_cost * qty_taken_on_candidates
            new_remaining_qty = svl_to_vacuum.remaining_qty + qty_taken_on_candidates
            corrected_value = remaining_value_before_vacuum - tmp_value
            svl_to_vacuum.write({
                'remaining_qty': new_remaining_qty,
            })

            # Don't create a layer or an accounting entry if the corrected value is zero.
            if svl_to_vacuum.currency_id.is_zero(corrected_value):
                continue

            corrected_value = svl_to_vacuum.currency_id.round(corrected_value)
            move = svl_to_vacuum.stock_move_id
            vals = {
                'product_id':
                self.id,
                'value':
                corrected_value,
                'unit_cost':
                0,
                'quantity':
                0,
                'remaining_qty':
                0,
                'stock_move_id':
                move.id,
                'company_id':
                move.company_id.id,
                'description':
                'Revaluation of %s (negative inventory)' % move.picking_id.name
                or move.name,
                'stock_valuation_layer_id':
                svl_to_vacuum.id,
            }
            vacuum_svl = self.env['stock.valuation.layer'].sudo().create(vals)

            # If some negative stock were fixed, we need to recompute the standard price.
            product = self.with_context(force_company=company.id)
            if product.cost_method == 'average' and not float_is_zero(
                    product.quantity_svl,
                    precision_rounding=self.uom_id.rounding):
                product.sudo().write({
                    'standard_price':
                    product.value_svl / product.quantity_svl
                })

            # Create the account move.
            if self.valuation != 'real_time':
                continue
            vacuum_svl.stock_move_id._account_entry_move(
                vacuum_svl.quantity, vacuum_svl.description, vacuum_svl.id,
                vacuum_svl.value)
Ejemplo n.º 21
0
    def test_invoice(self):
        """ Test create and invoice from the SO, and check qty invoice/to invoice, and the related amounts """
        # lines are in draft
        for line in self.sale_order.order_line:
            self.assertTrue(
                float_is_zero(line.untaxed_amount_to_invoice,
                              precision_digits=2),
                "The amount to invoice should be zero, as the line is in draf state"
            )
            self.assertTrue(
                float_is_zero(line.untaxed_amount_invoiced,
                              precision_digits=2),
                "The invoiced amount should be zero, as the line is in draft state"
            )

        # Confirm the SO
        self.sale_order.action_confirm()

        # Check ordered quantity, quantity to invoice and invoiced quantity of SO lines
        for line in self.sale_order.order_line:
            if line.product_id.invoice_policy == 'delivery':
                self.assertEquals(
                    line.qty_to_invoice, 0.0,
                    'Quantity to invoice should be same as ordered quantity')
                self.assertEquals(
                    line.qty_invoiced, 0.0,
                    'Invoiced quantity should be zero as no any invoice created for SO'
                )
                self.assertEquals(
                    line.untaxed_amount_to_invoice, 0.0,
                    "The amount to invoice should be zero, as the line based on delivered quantity"
                )
                self.assertEquals(
                    line.untaxed_amount_invoiced, 0.0,
                    "The invoiced amount should be zero, as the line based on delivered quantity"
                )
            else:
                self.assertEquals(
                    line.qty_to_invoice, line.product_uom_qty,
                    'Quantity to invoice should be same as ordered quantity')
                self.assertEquals(
                    line.qty_invoiced, 0.0,
                    'Invoiced quantity should be zero as no any invoice created for SO'
                )
                self.assertEquals(
                    line.untaxed_amount_to_invoice,
                    line.product_uom_qty * line.price_unit,
                    "The amount to invoice should the total of the line, as the line is confirmed"
                )
                self.assertEquals(
                    line.untaxed_amount_invoiced, 0.0,
                    "The invoiced amount should be zero, as the line is confirmed"
                )

        # Let's do an invoice with invoiceable lines
        payment = self.env['sale.advance.payment.inv'].with_context(
            self.context).create({'advance_payment_method': 'delivered'})
        payment.create_invoices()

        invoice = self.sale_order.invoice_ids[0]

        # Update quantity of an invoice lines
        move_form = Form(invoice)
        with move_form.invoice_line_ids.edit(0) as line_form:
            line_form.quantity = 3.0
        with move_form.invoice_line_ids.edit(1) as line_form:
            line_form.quantity = 2.0
        invoice = move_form.save()

        # amount to invoice / invoiced should not have changed (amounts take only confirmed invoice into account)
        for line in self.sale_order.order_line:
            if line.product_id.invoice_policy == 'delivery':
                self.assertEquals(line.qty_to_invoice, 0.0,
                                  "Quantity to invoice should be zero")
                self.assertEquals(
                    line.qty_invoiced, 0.0,
                    "Invoiced quantity should be zero as delivered lines are not delivered yet"
                )
                self.assertEquals(
                    line.untaxed_amount_to_invoice, 0.0,
                    "The amount to invoice should be zero, as the line based on delivered quantity (no confirmed invoice)"
                )
                self.assertEquals(
                    line.untaxed_amount_invoiced, 0.0,
                    "The invoiced amount should be zero, as no invoice are validated for now"
                )
            else:
                if line == self.sol_prod_order:
                    self.assertEquals(
                        self.sol_prod_order.qty_to_invoice, 2.0,
                        "Changing the quantity on draft invoice update the qty to invoice on SO lines"
                    )
                    self.assertEquals(
                        self.sol_prod_order.qty_invoiced, 3.0,
                        "Changing the quantity on draft invoice update the invoiced qty on SO lines"
                    )
                else:
                    self.assertEquals(
                        self.sol_serv_order.qty_to_invoice, 1.0,
                        "Changing the quantity on draft invoice update the qty to invoice on SO lines"
                    )
                    self.assertEquals(
                        self.sol_serv_order.qty_invoiced, 2.0,
                        "Changing the quantity on draft invoice update the invoiced qty on SO lines"
                    )
                self.assertEquals(
                    line.untaxed_amount_to_invoice,
                    line.product_uom_qty * line.price_unit,
                    "The amount to invoice should the total of the line, as the line is confirmed (no confirmed invoice)"
                )
                self.assertEquals(
                    line.untaxed_amount_invoiced, 0.0,
                    "The invoiced amount should be zero, as no invoice are validated for now"
                )

        invoice.post()

        # Check quantity to invoice on SO lines
        for line in self.sale_order.order_line:
            if line.product_id.invoice_policy == 'delivery':
                self.assertEquals(
                    line.qty_to_invoice, 0.0,
                    "Quantity to invoice should be same as ordered quantity")
                self.assertEquals(
                    line.qty_invoiced, 0.0,
                    "Invoiced quantity should be zero as no any invoice created for SO"
                )
                self.assertEquals(
                    line.untaxed_amount_to_invoice, 0.0,
                    "The amount to invoice should be zero, as the line based on delivered quantity"
                )
                self.assertEquals(
                    line.untaxed_amount_invoiced, 0.0,
                    "The invoiced amount should be zero, as the line based on delivered quantity"
                )
            else:
                if line == self.sol_prod_order:
                    self.assertEquals(
                        line.qty_to_invoice, 2.0,
                        "The ordered sale line are totally invoiced (qty to invoice is zero)"
                    )
                    self.assertEquals(
                        line.qty_invoiced, 3.0,
                        "The ordered (prod) sale line are totally invoiced (qty invoiced come from the invoice lines)"
                    )
                else:
                    self.assertEquals(
                        line.qty_to_invoice, 1.0,
                        "The ordered sale line are totally invoiced (qty to invoice is zero)"
                    )
                    self.assertEquals(
                        line.qty_invoiced, 2.0,
                        "The ordered (serv) sale line are totally invoiced (qty invoiced = the invoice lines)"
                    )
                self.assertEquals(
                    line.untaxed_amount_to_invoice,
                    line.price_unit * line.qty_to_invoice,
                    "Amount to invoice is now set as qty to invoice * unit price since no price change on invoice, for ordered products"
                )
                self.assertEquals(
                    line.untaxed_amount_invoiced,
                    line.price_unit * line.qty_invoiced,
                    "Amount invoiced is now set as qty invoiced * unit price since no price change on invoice, for ordered products"
                )
Ejemplo n.º 22
0
    def test_invoice_with_discount(self):
        """ Test invoice with a discount and check discount applied on both SO lines and an invoice lines """
        # Update discount and delivered quantity on SO lines
        self.sol_prod_order.write({'discount': 20.0})
        self.sol_serv_deliver.write({'discount': 20.0, 'qty_delivered': 4.0})
        self.sol_serv_order.write({'discount': -10.0})
        self.sol_prod_deliver.write({'qty_delivered': 2.0})

        for line in self.sale_order.order_line.filtered(lambda l: l.discount):
            product_price = line.price_unit * line.product_uom_qty
            self.assertEquals(line.discount,
                              (product_price - line.price_subtotal) /
                              product_price * 100,
                              'Discount should be applied on order line')

        # lines are in draft
        for line in self.sale_order.order_line:
            self.assertTrue(
                float_is_zero(line.untaxed_amount_to_invoice,
                              precision_digits=2),
                "The amount to invoice should be zero, as the line is in draf state"
            )
            self.assertTrue(
                float_is_zero(line.untaxed_amount_invoiced,
                              precision_digits=2),
                "The invoiced amount should be zero, as the line is in draft state"
            )

        self.sale_order.action_confirm()

        for line in self.sale_order.order_line:
            self.assertTrue(
                float_is_zero(line.untaxed_amount_invoiced,
                              precision_digits=2),
                "The invoiced amount should be zero, as the line is in draft state"
            )

        self.assertEquals(self.sol_serv_order.untaxed_amount_to_invoice, 297,
                          "The untaxed amount to invoice is wrong")
        self.assertEquals(
            self.sol_serv_deliver.untaxed_amount_to_invoice,
            self.sol_serv_deliver.qty_delivered *
            self.sol_serv_deliver.price_reduce,
            "The untaxed amount to invoice should be qty deli * price reduce, so 4 * (180 - 36)"
        )
        self.assertEquals(
            self.sol_prod_deliver.untaxed_amount_to_invoice, 140,
            "The untaxed amount to invoice should be qty deli * price reduce, so 4 * (180 - 36)"
        )

        # Let's do an invoice with invoiceable lines
        payment = self.env['sale.advance.payment.inv'].with_context(
            self.context).create({'advance_payment_method': 'delivered'})
        payment.create_invoices()
        invoice = self.sale_order.invoice_ids[0]
        invoice.post()

        # Check discount appeared on both SO lines and invoice lines
        for line, inv_line in zip(self.sale_order.order_line,
                                  invoice.invoice_line_ids):
            self.assertEquals(
                line.discount, inv_line.discount,
                'Discount on lines of order and invoice should be same')
Ejemplo n.º 23
0
 def try_zero(amount, expected):
     self.assertEqual(float_is_zero(amount,
                                    precision_digits=3), expected,
                      "Rounding error: %s should be zero!" % amount)
Ejemplo n.º 24
0
    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))
            old_production_qty = production.product_qty
            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)
            documents = {}
            for line, line_data in lines:
                if line.child_bom_id and line.child_bom_id.type == 'phantom' or\
                        line.product_id.type not in ['product', 'consu']:
                    continue
                move = production.move_raw_ids.filtered(
                    lambda x: x.bom_line_id.id == line.id and x.state not in
                    ('done', 'cancel'))
                if move:
                    move = move[0]
                    old_qty = move.product_uom_qty
                else:
                    old_qty = 0
                iterate_key = production._get_document_iterate_key(move)
                if iterate_key:
                    document = self.env[
                        'stock.picking']._log_activity_get_documents(
                            {move: (line_data['qty'], old_qty)}, iterate_key,
                            'UP')
                    for key, value in document.items():
                        if documents.get(key):
                            documents[key] += [value]
                        else:
                            documents[key] = [value]

                production._update_raw_move(line, line_data)

            production._log_manufacture_exception(documents)
            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']
            finished_moves_modification = self._update_finished_moves(
                production, production.product_qty - qty_produced,
                old_production_qty)
            production._log_downside_manufactured_quantity(
                finished_moves_modification)
            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 = float_round(
                        operation_bom_qty[operation.id] /
                        operation.workcenter_id.capacity,
                        precision_digits=0,
                        rounding_method='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.finished_lot_id = False
                    wo._workorder_line_ids().unlink()
                wo.qty_producing = quantity
                if wo.qty_produced < wo.qty_production and wo.state == 'done':
                    wo.state = 'progress'
                if wo.qty_produced == wo.qty_production and wo.state == 'progress':
                    wo.state = 'done'
                    if wo.next_work_order_id.state == 'pending':
                        wo.next_work_order_id.state = 'ready'
                # 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 wo.state not in ('done', 'cancel'):
                    line_values = wo._update_workorder_lines()
                    wo._workorder_line_ids().create(line_values['to_create'])
                    if line_values['to_delete']:
                        line_values['to_delete'].unlink()
                    for line, vals in line_values['to_update'].items():
                        line.write(vals)
        return {}
Ejemplo n.º 25
0
    def _apply_rules(self, st_lines, excluded_ids=None, partner_map=None):
        ''' Apply criteria to get candidates for all reconciliation models.
        :param st_lines:        Account.bank.statement.lines recordset.
        :param excluded_ids:    Account.move.lines to exclude.
        :param partner_map:     Dict mapping each line with new partner eventually.
        :return:                A dict mapping each statement line id with:
            * aml_ids:      A list of account.move.line ids.
            * model:        An account.reconcile.model record (optional).
            * status:       'reconciled' if the lines has been already reconciled, 'write_off' if the write-off must be
                            applied on the statement line.
        '''
        available_models = self.filtered(lambda m: m.rule_type != 'writeoff_button')

        results = dict((r.id, {'aml_ids': []}) for r in st_lines)

        if not available_models:
            return results

        ordered_models = available_models.sorted(key=lambda m: (m.sequence, m.id))

        grouped_candidates = {}

        # Type == 'invoice_matching'.
        # Map each (st_line.id, model_id) with matching amls.
        invoices_models = ordered_models.filtered(lambda m: m.rule_type == 'invoice_matching')
        self.env['account.move'].flush(['state'])
        self.env['account.move.line'].flush(['balance', 'reconciled'])
        self.env['account.bank.statement.line'].flush(['company_id'])
        if invoices_models:
            query, params = invoices_models._get_invoice_matching_query(st_lines, excluded_ids=excluded_ids, partner_map=partner_map)
            self._cr.execute(query, params)
            query_res = self._cr.dictfetchall()

            for res in query_res:
                grouped_candidates.setdefault(res['id'], {})
                grouped_candidates[res['id']].setdefault(res['model_id'], [])
                grouped_candidates[res['id']][res['model_id']].append(res)

        # Type == 'writeoff_suggestion'.
        # Map each (st_line.id, model_id) with a flag indicating the st_line matches the criteria.
        write_off_models = ordered_models.filtered(lambda m: m.rule_type == 'writeoff_suggestion')
        if write_off_models:
            query, params = write_off_models._get_writeoff_suggestion_query(st_lines, excluded_ids=excluded_ids, partner_map=partner_map)
            self._cr.execute(query, params)
            query_res = self._cr.dictfetchall()

            for res in query_res:
                grouped_candidates.setdefault(res['id'], {})
                grouped_candidates[res['id']].setdefault(res['model_id'], True)

        # Keep track of already processed amls.
        amls_ids_to_exclude = set()

        # Keep track of already reconciled amls.
        reconciled_amls_ids = set()

        # Iterate all and create results.
        for line in st_lines:
            line_currency = line.currency_id or line.journal_id.currency_id or line.company_id.currency_id
            line_residual = line.currency_id and line.amount_currency or line.amount

            # Search for applicable rule.
            # /!\ BREAK are very important here to avoid applying multiple rules on the same line.
            for model in ordered_models:
                # No result found.
                if not grouped_candidates.get(line.id) or not grouped_candidates[line.id].get(model.id):
                    continue

                if model.rule_type == 'invoice_matching':
                    candidates = grouped_candidates[line.id][model.id]

                    # If some invoices match on the communication, suggest them.
                    # Otherwise, suggest all invoices having the same partner.
                    # N.B: The only way to match a line without a partner is through the communication.
                    first_batch_candidates = []
                    first_batch_candidates_proposed = []
                    second_batch_candidates = []
                    second_batch_candidates_proposed = []
                    third_batch_candidates = []
                    third_batch_candidates_proposed = []
                    for c in candidates:
                        # Don't take into account already reconciled lines.
                        if c['aml_id'] in reconciled_amls_ids:
                            continue

                        # Dispatch candidates between lines matching invoices with the communication or only the partner.
                        elif c['payment_reference_flag']:
                            if c['aml_id'] in amls_ids_to_exclude:
                                first_batch_candidates_proposed.append(c)
                            else:
                                first_batch_candidates.append(c)
                        elif c['communication_flag']:
                            if c['aml_id'] in amls_ids_to_exclude:
                                second_batch_candidates_proposed.append(c)
                            else:
                                second_batch_candidates.append(c)
                        elif not first_batch_candidates:
                            if c['aml_id'] in amls_ids_to_exclude:
                                third_batch_candidates_proposed.append(c)
                            else:
                                third_batch_candidates.append(c)
                    available_candidates = (first_batch_candidates + first_batch_candidates_proposed
                                            or second_batch_candidates + second_batch_candidates_proposed
                                            or third_batch_candidates + third_batch_candidates_proposed)

                    # Special case: the amount are the same, submit the line directly.
                    for c in available_candidates:
                        residual_amount = c['aml_currency_id'] and c['aml_amount_residual_currency'] or c['aml_amount_residual']

                        if float_is_zero(residual_amount - line_residual, precision_rounding=line_currency.rounding):
                            available_candidates = [c]
                            break

                    # Needed to handle check on total residual amounts.
                    if first_batch_candidates or first_batch_candidates_proposed or model._check_rule_propositions(line, available_candidates):
                        results[line.id]['model'] = model

                        # Add candidates to the result.
                        for candidate in available_candidates:
                            results[line.id]['aml_ids'].append(candidate['aml_id'])
                            amls_ids_to_exclude.add(candidate['aml_id'])

                        # Create write-off lines.
                        move_lines = self.env['account.move.line'].browse(results[line.id]['aml_ids'])
                        partner = partner_map and partner_map.get(line.id) and self.env['res.partner'].browse(partner_map[line.id])
                        reconciliation_results = model._prepare_reconciliation(line, move_lines, partner=partner)

                        # A write-off must be applied.
                        if reconciliation_results['new_aml_dicts']:
                            results[line.id]['status'] = 'write_off'

                        # Process auto-reconciliation.
                        if (first_batch_candidates or second_batch_candidates) and model.auto_reconcile:
                            # An open balance is needed but no partner has been found.
                            if reconciliation_results['open_balance_dict'] is False:
                                break

                            new_aml_dicts = reconciliation_results['new_aml_dicts']
                            if reconciliation_results['open_balance_dict']:
                                new_aml_dicts.append(reconciliation_results['open_balance_dict'])
                            if not line.partner_id and partner:
                                line.partner_id = partner
                            counterpart_moves = line.process_reconciliation(
                                counterpart_aml_dicts=reconciliation_results['counterpart_aml_dicts'],
                                payment_aml_rec=reconciliation_results['payment_aml_rec'],
                                new_aml_dicts=new_aml_dicts,
                            )
                            results[line.id]['status'] = 'reconciled'
                            results[line.id]['reconciled_lines'] = counterpart_moves.mapped('line_ids')

                            # The reconciled move lines are no longer candidates for another rule.
                            reconciled_amls_ids.update(move_lines.ids)

                        # Break models loop.
                        break

                elif model.rule_type == 'writeoff_suggestion' and grouped_candidates[line.id][model.id]:
                    results[line.id]['model'] = model
                    results[line.id]['status'] = 'write_off'

                    # Create write-off lines.
                    partner = partner_map and partner_map.get(line.id) and self.env['res.partner'].browse(partner_map[line.id])
                    reconciliation_results = model._prepare_reconciliation(line, partner=partner)

                    # An open balance is needed but no partner has been found.
                    if reconciliation_results['open_balance_dict'] is False:
                        break

                    # Process auto-reconciliation.
                    if model.auto_reconcile:
                        new_aml_dicts = reconciliation_results['new_aml_dicts']
                        if reconciliation_results['open_balance_dict']:
                            new_aml_dicts.append(reconciliation_results['open_balance_dict'])
                        if not line.partner_id and partner:
                            line.partner_id = partner
                        counterpart_moves = line.process_reconciliation(
                            counterpart_aml_dicts=reconciliation_results['counterpart_aml_dicts'],
                            payment_aml_rec=reconciliation_results['payment_aml_rec'],
                            new_aml_dicts=new_aml_dicts,
                        )
                        results[line.id]['status'] = 'reconciled'
                        results[line.id]['reconciled_lines'] = counterpart_moves.mapped('line_ids')

                    # Break models loop.
                    break
        return results
Ejemplo n.º 26
0
    def _get_write_off_move_lines_dict(self, st_line, move_lines=None):
        ''' Get move.lines dict (to be passed to the create()) corresponding to the reconciliation model's write-off lines.
        :param st_line:     An account.bank.statement.line record.
        :param move_lines:  An account.move.line recordset.
        :return: A list of dict representing move.lines to be created corresponding to the write-off lines.
        '''
        self.ensure_one()

        if self.rule_type == 'invoice_matching' and (not self.match_total_amount or (self.match_total_amount_param == 100)):
            return []

        line_residual = st_line.currency_id and st_line.amount_currency or st_line.amount
        line_currency = st_line.currency_id or st_line.journal_id.currency_id or st_line.company_id.currency_id
        total_residual = move_lines and sum(aml.currency_id and aml.amount_residual_currency or aml.amount_residual for aml in move_lines) or 0.0

        balance = total_residual - line_residual

        if not self.account_id or float_is_zero(balance, precision_rounding=line_currency.rounding):
            return []

        if self.amount_type == 'percentage':
            line_balance = balance * (self.amount / 100.0)
        elif self.amount_type == "regex":
            match = re.search(self.amount_from_label_regex, st_line.name)
            if match:
                line_balance = copysign(float(re.sub(r'\D' + self.decimal_separator, '', match.group(1)).replace(self.decimal_separator, '.')) * (1 if balance > 0.0 else -1), balance)
            else:
                line_balance = 0
        else:
            line_balance = self.amount * (1 if balance > 0.0 else -1)

        new_aml_dicts = []

        # First write-off line.
        writeoff_line = {
            'name': self.label or st_line.name,
            'account_id': self.account_id.id,
            'analytic_account_id': self.analytic_account_id.id,
            'analytic_tag_ids': [(6, 0, self.analytic_tag_ids.ids)],
            'debit': line_balance > 0 and line_balance or 0,
            'credit': line_balance < 0 and -line_balance or 0,
            'reconcile_model_id': self.id,
        }
        new_aml_dicts.append(writeoff_line)

        if self.tax_ids:
            writeoff_line['tax_ids'] = [(6, None, self.tax_ids.ids)]
            tax = self.tax_ids
            # Multiple taxes with force_tax_included results in wrong computation, so we
            # only allow to set the force_tax_included field if we have one tax selected
            if self.force_tax_included:
                tax = tax[0].with_context(force_price_include=True)
            new_aml_dicts += self._get_taxes_move_lines_dict(tax, writeoff_line)

        # Second write-off line.
        if self.has_second_line and self.second_account_id:
            remaining_balance = balance - sum(aml['debit'] - aml['credit'] for aml in new_aml_dicts)
            if self.second_amount_type == 'percentage':
                line_balance = remaining_balance * (self.second_amount / 100.0)
            elif self.second_amount_type == "regex":
                match = re.search(self.second_amount_from_label_regex, st_line.name)
                if match:
                    line_balance = copysign(float(re.sub(r'\D' + self.decimal_separator, '', match.group(1)).replace(self.decimal_separator, '.')), remaining_balance)
                else:
                    line_balance = 0
            else:
                line_balance = self.second_amount * (1 if remaining_balance > 0.0 else -1)

            second_writeoff_line = {
                'name': self.second_label or st_line.name,
                'account_id': self.second_account_id.id,
                'analytic_account_id': self.second_analytic_account_id.id,
                'analytic_tag_ids': [(6, 0, self.second_analytic_tag_ids.ids)],
                'debit': line_balance > 0 and line_balance or 0,
                'credit': line_balance < 0 and -line_balance or 0,
                'reconcile_model_id': self.id,
            }
            new_aml_dicts.append(second_writeoff_line)

            if self.second_tax_ids:
                second_writeoff_line['tax_ids'] = [(6, None, self.second_tax_ids.ids)]
                tax = self.second_tax_ids
                # Multiple taxes with force_tax_included results in wrong computation, so we
                # only allow to set the force_tax_included field if we have one tax selected
                if self.force_second_tax_included:
                    tax = tax[0].with_context(force_price_include=True)
                new_aml_dicts += self._get_taxes_move_lines_dict(tax, second_writeoff_line)

        return new_aml_dicts
Ejemplo n.º 27
0
    def test_timesheet_delivery(self):
        """ Test timesheet invoicing with 'invoice on delivery' timetracked products
                1. Create SO and confirm it
                2. log timesheet
                3. create invoice
                4. log other timesheet
                5. create a second invoice
                6. add new SO line (delivered service)
        """
        # create SO and confirm it
        self.env['res.currency.rate'].search([]).unlink()
        sale_order = self.env['sale.order'].create({
            'partner_id':
            self.partner_customer_usd.id,
            'partner_invoice_id':
            self.partner_customer_usd.id,
            'partner_shipping_id':
            self.partner_customer_usd.id,
            'pricelist_id':
            self.pricelist_usd.id,
        })
        so_line_deliver_global_project = self.env['sale.order.line'].create({
            'name':
            self.product_delivery_timesheet2.name,
            'product_id':
            self.product_delivery_timesheet2.id,
            'product_uom_qty':
            50,
            'product_uom':
            self.product_delivery_timesheet2.uom_id.id,
            'price_unit':
            self.product_delivery_timesheet2.list_price,
            'order_id':
            sale_order.id,
        })
        so_line_deliver_task_project = self.env['sale.order.line'].create({
            'name':
            self.product_delivery_timesheet3.name,
            'product_id':
            self.product_delivery_timesheet3.id,
            'product_uom_qty':
            20,
            'product_uom':
            self.product_delivery_timesheet3.uom_id.id,
            'price_unit':
            self.product_delivery_timesheet3.list_price,
            'order_id':
            sale_order.id,
        })
        so_line_deliver_global_project.product_id_change()
        so_line_deliver_task_project.product_id_change()

        # confirm SO
        sale_order.action_confirm()
        task_serv1 = self.env['project.task'].search([
            ('sale_line_id', '=', so_line_deliver_global_project.id)
        ])
        task_serv2 = self.env['project.task'].search([
            ('sale_line_id', '=', so_line_deliver_task_project.id)
        ])
        project_serv2 = self.env['project.project'].search([
            ('sale_line_id', '=', so_line_deliver_task_project.id)
        ])

        self.assertEqual(
            task_serv1.project_id, self.project_global,
            "Sale Timesheet: task should be created in global project")
        self.assertTrue(
            task_serv1,
            "Sale Timesheet: on SO confirmation, a task should have been created in global project"
        )
        self.assertTrue(
            task_serv2,
            "Sale Timesheet: on SO confirmation, a task should have been created in a new project"
        )
        self.assertEqual(
            sale_order.invoice_status, 'no',
            'Sale Timesheet: "invoice on delivery" should not need to be invoiced on so confirmation'
        )
        self.assertEqual(sale_order.analytic_account_id,
                         task_serv2.project_id.analytic_account_id,
                         "SO should have create a project")
        self.assertEqual(
            sale_order.tasks_count, 2,
            "Two tasks (1 per SO line) should have been created on SO confirmation"
        )
        self.assertEqual(
            len(sale_order.project_ids), 2,
            "One project should have been created by the SO, when confirmed + the one from SO line 1 'task in global project'"
        )
        self.assertEqual(
            sale_order.analytic_account_id, project_serv2.analytic_account_id,
            "The created project should be linked to the analytic account of the SO"
        )

        # let's log some timesheets
        timesheet1 = self.env['account.analytic.line'].create({
            'name':
            'Test Line',
            'project_id':
            task_serv1.project_id.id,  # global project
            'task_id':
            task_serv1.id,
            'unit_amount':
            10.5,
            'employee_id':
            self.employee_manager.id,
        })
        self.assertEqual(
            so_line_deliver_global_project.invoice_status, 'to invoice',
            'Sale Timesheet: "invoice on delivery" timesheets should set the so line in "to invoice" status when logged'
        )
        self.assertEqual(
            so_line_deliver_task_project.invoice_status, 'no',
            'Sale Timesheet: so line invoice status should not change when no timesheet linked to the line'
        )
        self.assertEqual(
            sale_order.invoice_status, 'to invoice',
            'Sale Timesheet: "invoice on delivery" timesheets should set the so in "to invoice" status when logged'
        )
        self.assertEqual(
            timesheet1.timesheet_invoice_type, 'billable_time',
            "Timesheets linked to SO line with delivered product shoulbe be billable time"
        )
        self.assertFalse(
            timesheet1.timesheet_invoice_id,
            "The timesheet1 should not be linked to the invoice yet")

        # invoice SO
        invoice1 = sale_order._create_invoices()
        self.assertTrue(
            float_is_zero(invoice1.amount_total -
                          so_line_deliver_global_project.price_unit * 10.5,
                          precision_digits=2),
            'Sale: invoice generation on timesheets product is wrong')
        self.assertEqual(
            timesheet1.timesheet_invoice_id, invoice1,
            "The timesheet1 should not be linked to the invoice 1, as we are in delivered quantity (even if invoice is in draft"
        )
        with self.assertRaises(
                UserError
        ):  # We can not modify timesheet linked to invoice (even draft ones)
            timesheet1.write({'unit_amount': 42})

        # log some timesheets again
        timesheet2 = self.env['account.analytic.line'].create({
            'name':
            'Test Line',
            'project_id':
            task_serv1.project_id.id,  # global project
            'task_id':
            task_serv1.id,
            'unit_amount':
            39.5,
            'employee_id':
            self.employee_user.id,
        })
        self.assertEqual(
            so_line_deliver_global_project.invoice_status, 'to invoice',
            'Sale Timesheet: "invoice on delivery" timesheets should set the so line in "to invoice" status when logged'
        )
        self.assertEqual(
            so_line_deliver_task_project.invoice_status, 'no',
            'Sale Timesheet: so line invoice status should not change when no timesheet linked to the line'
        )
        self.assertEqual(
            sale_order.invoice_status, 'to invoice',
            'Sale Timesheet: "invoice on delivery" timesheets should not modify the invoice_status of the so'
        )
        self.assertEqual(
            timesheet2.timesheet_invoice_type, 'billable_time',
            "Timesheets linked to SO line with delivered product shoulbe be billable time"
        )
        self.assertFalse(
            timesheet2.timesheet_invoice_id,
            "The timesheet2 should not be linked to the invoice yet")

        # create a second invoice
        invoice2 = sale_order._create_invoices()[0]
        self.assertEqual(
            len(sale_order.invoice_ids), 2,
            "A second invoice should have been created from the SO")
        self.assertEqual(
            so_line_deliver_global_project.invoice_status, 'invoiced',
            'Sale Timesheet: "invoice on delivery" timesheets should set the so line in "to invoice" status when logged'
        )
        self.assertEqual(
            sale_order.invoice_status, 'no',
            'Sale Timesheet: "invoice on delivery" timesheets should be invoiced completely by now'
        )
        self.assertEqual(
            timesheet2.timesheet_invoice_id, invoice2,
            "The timesheet2 should not be linked to the invoice 2")
        with self.assertRaises(
                UserError
        ):  # We can not modify timesheet linked to invoice (even draft ones)
            timesheet2.write({'unit_amount': 42})

        # add a line on SO
        so_line_deliver_only_project = self.env['sale.order.line'].create({
            'name':
            self.product_delivery_timesheet4.name,
            'product_id':
            self.product_delivery_timesheet4.id,
            'product_uom_qty':
            5,
            'product_uom':
            self.product_delivery_timesheet4.uom_id.id,
            'price_unit':
            self.product_delivery_timesheet4.list_price,
            'order_id':
            sale_order.id,
        })
        self.assertEqual(
            len(sale_order.project_ids), 2,
            "No new project should have been created by the SO, when selling 'project only' product, since it reuse the one from 'new task in new project'."
        )

        # let's log some timesheets on the project
        timesheet3 = self.env['account.analytic.line'].create({
            'name':
            'Test Line',
            'project_id':
            project_serv2.id,
            'unit_amount':
            7,
            'employee_id':
            self.employee_user.id,
        })
        self.assertTrue(
            float_is_zero(so_line_deliver_only_project.qty_delivered,
                          precision_digits=2),
            "Timesheeting on project should not incremented the delivered quantity on the SO line"
        )
        self.assertEqual(
            sale_order.invoice_status, 'no',
            'Sale Timesheet: "invoice on delivery" timesheets should be invoiced completely by now'
        )
        self.assertEqual(
            timesheet3.timesheet_invoice_type, 'non_billable_project',
            "Timesheets without task shoulbe be 'no project found'")
        self.assertFalse(
            timesheet3.timesheet_invoice_id,
            "The timesheet3 should not be linked to the invoice yet")

        # let's log some timesheets on the task (new task/new project)
        timesheet4 = self.env['account.analytic.line'].create({
            'name':
            'Test Line 4',
            'project_id':
            task_serv2.project_id.id,
            'task_id':
            task_serv2.id,
            'unit_amount':
            7,
            'employee_id':
            self.employee_user.id,
        })
        self.assertFalse(
            timesheet4.timesheet_invoice_id,
            "The timesheet4 should not be linked to the invoice yet")

        # modify a non invoiced timesheet
        timesheet4.write({'unit_amount': 42})

        self.assertFalse(
            timesheet4.timesheet_invoice_id,
            "The timesheet4 should not still be linked to the invoice")

        # validate the second invoice
        invoice2.post()

        self.assertEqual(
            timesheet1.timesheet_invoice_id, invoice1,
            "The timesheet1 should not be linked to the invoice 1, even after validation"
        )
        self.assertEqual(
            timesheet2.timesheet_invoice_id, invoice2,
            "The timesheet2 should not be linked to the invoice 1, even after validation"
        )
        self.assertFalse(
            timesheet3.timesheet_invoice_id,
            "The timesheet3 should not be linked to the invoice, since we are in ordered quantity"
        )
        self.assertFalse(
            timesheet4.timesheet_invoice_id,
            "The timesheet4 should not be linked to the invoice, since we are in ordered quantity"
        )
Ejemplo n.º 28
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_customer_usd.id,
            'partner_invoice_id':
            self.partner_customer_usd.id,
            'partner_shipping_id':
            self.partner_customer_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_ids,
                        "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'
        )

        project_serv2 = so_line_manual_only_project.project_id
        self.assertTrue(
            project_serv2,
            "A second project is created when selling 'project only' after SO confirmation."
        )
        self.assertEqual(
            sale_order.analytic_account_id, project_serv2.analytic_account_id,
            "The created project should be linked to the analytic account of the SO"
        )

        # 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':
            self.project_global.id,  # global project
            'unit_amount':
            3,
            'employee_id':
            self.employee_manager.id,
        })

        self.assertEqual(
            len(sale_order.project_ids), 2,
            "One project should have been created by the SO, when confirmed + the one coming from SO line 1 'task in global project'."
        )
        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'
        )

        self.assertEqual(
            timesheet1.timesheet_invoice_type, 'billable_fixed',
            "Timesheets linked to SO line with ordered product shoulbe be billable fixed since it is a milestone"
        )
        self.assertEqual(
            timesheet2.timesheet_invoice_type, 'non_billable_project',
            "Timesheets without task shoulbe be 'no project found'")
        self.assertFalse(timesheet1.timesheet_invoice_id,
                         "The timesheet1 should not be linked to the invoice")
        self.assertFalse(timesheet2.timesheet_invoice_id,
                         "The timesheet2 should not be linked to the invoice")

        # invoice SO
        sale_order.order_line.write({'qty_delivered': 5})
        invoice1 = sale_order._create_invoices()

        for invoice_line in invoice1.invoice_line_ids:
            self.assertEqual(
                invoice_line.quantity, 5,
                "The invoiced quantity should be 5, as manually set on SO lines"
            )

        self.assertFalse(
            timesheet1.timesheet_invoice_id,
            "The timesheet1 should not be linked to the invoice, since timesheets are used for time tracking in milestone"
        )
        self.assertFalse(
            timesheet2.timesheet_invoice_id,
            "The timesheet2 should not be linked to the invoice, since timesheets are used for time tracking in milestone"
        )

        # validate the invoice
        invoice1.post()

        self.assertFalse(
            timesheet1.timesheet_invoice_id,
            "The timesheet1 should not be linked to the invoice, even after invoice validation"
        )
        self.assertFalse(
            timesheet2.timesheet_invoice_id,
            "The timesheet2 should not be linked to the invoice, even after invoice validation"
        )
Ejemplo n.º 29
0
    def _change_standard_price(self, new_price, counterpart_account_id=False):
        """Helper to create the stock valuation layers and the account moves
        after an update of standard price.

        :param new_price: new standard price
        """
        # Handle stock valuation layers.
        svl_vals_list = []
        company_id = self.env.company
        for product in self:
            if product.cost_method not in ('standard', 'average'):
                continue
            quantity_svl = product.sudo().quantity_svl
            if float_is_zero(quantity_svl,
                             precision_rounding=product.uom_id.rounding):
                continue
            diff = new_price - product.standard_price
            value = company_id.currency_id.round(quantity_svl * diff)
            if company_id.currency_id.is_zero(value):
                continue

            svl_vals = {
                'company_id':
                company_id.id,
                'product_id':
                product.id,
                'description':
                _('Product value manually modified (from %s to %s)') %
                (product.standard_price, new_price),
                'value':
                value,
                'quantity':
                0,
            }
            svl_vals_list.append(svl_vals)
        stock_valuation_layers = self.env['stock.valuation.layer'].sudo(
        ).create(svl_vals_list)

        # Handle account moves.
        product_accounts = {
            product.id: product.product_tmpl_id.get_product_accounts()
            for product in self
        }
        am_vals_list = []
        for stock_valuation_layer in stock_valuation_layers:
            product = stock_valuation_layer.product_id
            value = stock_valuation_layer.value

            if product.valuation != 'real_time':
                continue

            # Sanity check.
            if counterpart_account_id is False:
                raise UserError(_('You must set a counterpart account.'))
            if not product_accounts[product.id].get('stock_valuation'):
                raise UserError(
                    _('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.'
                      ))

            if value < 0:
                debit_account_id = counterpart_account_id
                credit_account_id = product_accounts[
                    product.id]['stock_valuation'].id
            else:
                debit_account_id = product_accounts[
                    product.id]['stock_valuation'].id
                credit_account_id = counterpart_account_id

            move_vals = {
                'journal_id':
                product_accounts[product.id]['stock_journal'].id,
                'company_id':
                company_id.id,
                'ref':
                product.default_code,
                'stock_valuation_layer_ids':
                [(6, None, [stock_valuation_layer.id])],
                'line_ids': [(0, 0, {
                    'name':
                    _('%s changed cost from %s to %s - %s') %
                    (self.env.user.name, product.standard_price, new_price,
                     product.display_name),
                    'account_id':
                    debit_account_id,
                    'debit':
                    abs(value),
                    'credit':
                    0,
                    'product_id':
                    product.id,
                }),
                             (0, 0, {
                                 'name':
                                 _('%s changed cost from %s to %s - %s') %
                                 (self.env.user.name, product.standard_price,
                                  new_price, product.display_name),
                                 'account_id':
                                 credit_account_id,
                                 'debit':
                                 0,
                                 'credit':
                                 abs(value),
                                 'product_id':
                                 product.id,
                             })],
            }
            am_vals_list.append(move_vals)
        account_moves = self.env['account.move'].create(am_vals_list)
        if account_moves:
            account_moves.post()

        # Actually update the standard price.
        self.with_context(force_company=company_id.id).sudo().write(
            {'standard_price': new_price})
Ejemplo n.º 30
0
    def _run_fifo(self, quantity, company):
        self.ensure_one()

        # Find back incoming stock valuation layers (called candidates here) to value `quantity`.
        qty_to_take_on_candidates = quantity
        candidates = self.env['stock.valuation.layer'].sudo().with_context(
            active_test=False).search([
                ('product_id', '=', self.id),
                ('remaining_qty', '>', 0),
                ('company_id', '=', company.id),
            ])
        new_standard_price = 0
        tmp_value = 0  # to accumulate the value taken on the candidates
        for candidate in candidates:
            qty_taken_on_candidate = min(qty_to_take_on_candidates,
                                         candidate.remaining_qty)

            candidate_unit_cost = candidate.remaining_value / candidate.remaining_qty
            new_standard_price = candidate_unit_cost
            value_taken_on_candidate = qty_taken_on_candidate * candidate_unit_cost
            value_taken_on_candidate = candidate.currency_id.round(
                value_taken_on_candidate)
            new_remaining_value = candidate.remaining_value - value_taken_on_candidate

            candidate_vals = {
                'remaining_qty':
                candidate.remaining_qty - qty_taken_on_candidate,
                'remaining_value': new_remaining_value,
            }

            candidate.write(candidate_vals)

            qty_to_take_on_candidates -= qty_taken_on_candidate
            tmp_value += value_taken_on_candidate
            if float_is_zero(qty_to_take_on_candidates,
                             precision_rounding=self.uom_id.rounding):
                break

        # Update the standard price with the price of the last used candidate, if any.
        if new_standard_price and self.cost_method == 'fifo':
            self.sudo().with_context(
                force_company=company.id).standard_price = new_standard_price

        # If there's still quantity to value but we're out of candidates, we fall in the
        # negative stock use case. We chose to value the out move at the price of the
        # last out and a correction entry will be made once `_fifo_vacuum` is called.
        vals = {}
        if float_is_zero(qty_to_take_on_candidates,
                         precision_rounding=self.uom_id.rounding):
            vals = {
                'value': -tmp_value,
                'unit_cost': tmp_value / quantity,
            }
        else:
            assert qty_to_take_on_candidates > 0
            last_fifo_price = new_standard_price or self.standard_price
            negative_stock_value = last_fifo_price * -qty_to_take_on_candidates
            tmp_value += abs(negative_stock_value)
            vals = {
                'remaining_qty': -qty_to_take_on_candidates,
                'value': -tmp_value,
                'unit_cost': last_fifo_price,
            }
        return vals