Ejemplo n.º 1
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':
                currency.round(init_data['amount'])
                if currency else init_data['amount'],
                'name':
                init_data['payment_name'],
                'payment_method_id':
                init_data['payment_method_id'][0],
            })

        if float_is_zero(order.amount_total - order.amount_paid,
                         precision_rounding=currency.rounding):
            order.action_pos_order_paid()
            return {'type': 'ir.actions.act_window_close'}

        return self.launch_payment()
Ejemplo n.º 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._is_in() 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

            qty_done = move.product_uom._compute_quantity(
                move.quantity_done, move.product_id.uom_id)
            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_done, 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 qty_done
                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.º 3
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)
Ejemplo n.º 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()

        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 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 /
                                                   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.º 5
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.º 6
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

        # Match total residual amount.
        total_residual = 0.0
        for aml in candidates:
            if aml['account_internal_type'] == 'liquidity':
                total_residual += aml['aml_currency_id'] and aml[
                    'aml_amount_currency'] or aml['aml_balance']
            else:
                total_residual += aml['aml_currency_id'] and aml[
                    'aml_amount_residual_currency'] or aml[
                        'aml_amount_residual']
        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

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

        if line_residual > total_residual:
            amount_percentage = (total_residual / line_residual) * 100
        else:
            amount_percentage = (line_residual / total_residual) * 100
        return amount_percentage >= self.match_total_amount_param
Ejemplo n.º 7
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.º 8
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.º 9
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
Ejemplo n.º 10
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.º 11
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.º 12
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.º 13
0
    def is_zero(self, amount):
        """Returns true if ``amount`` is small enough to be treated as
           zero according to current currency's rounding rules.
           Warning: ``is_zero(amount1-amount2)`` is not always equivalent to
           ``compare_amounts(amount1,amount2) == 0``, as the former will round after
           computing the difference, while the latter will round before, giving
           different results for e.g. 0.006 and 0.002 at 2 digits precision.

           :param float amount: amount to compare with currency's zero

           With the new API, call it like: ``currency.is_zero(amount)``.
        """
        return tools.float_is_zero(amount, precision_rounding=self.rounding)
Ejemplo n.º 14
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.º 15
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.º 16
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.º 17
0
 def summary(self):
     res = super(EventRegistration, self).summary()
     if self.event_ticket_id.product_id.image_medium:
         res['image'] = '/web/image/product.product/%s/image_medium' % 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.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.º 18
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.pricelist_id.currency_id
     amount = order.amount_total - order.amount_paid
     data = self.read()[0]
     # add_payment expect a journal key
     data['journal'] = data['journal_id'][0]
     data['amount'] = currency.round(data['amount']) if currency else data['amount']
     if not float_is_zero(amount, precision_rounding=currency.rounding or 0.01):
         order.add_payment(data)
     if order.test_paid():
         order.action_pos_order_paid()
         return {'type': 'ir.actions.act_window_close'}
     return self.launch_payment()
Ejemplo n.º 19
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.º 20
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.º 21
0
    def action_sheet_move_create(self):
        if any(sheet.state != 'approve' for sheet in self):
            raise UserError(
                _("You can only generate accounting entry for approved expense(s)."
                  ))

        if any(not sheet.journal_id for sheet in self):
            raise UserError(
                _("Expenses must have an expense journal specified to generate accounting entries."
                  ))

        expense_line_ids = self.mapped('expense_line_ids')\
            .filtered(lambda r: not float_is_zero(r.total_amount, precision_rounding=(r.currency_id or self.env.user.company_id.currency_id).rounding))
        res = expense_line_ids.action_move_create()

        if not self.accounting_date:
            self.accounting_date = self.account_move_id.date

        if self.payment_mode == 'own_account' and expense_line_ids:
            self.write({'state': 'post'})
        else:
            self.write({'state': 'done'})
        self.activity_update()
        return res
Ejemplo n.º 22
0
    def do_change_standard_price(self, new_price, account_id):
        """ Changes the Standard Price of Product and creates an account move accordingly."""
        AccountMove = self.env['account.move']

        quant_locs = self.env['stock.quant'].sudo().read_group(
            [('product_id', 'in', self.ids)], ['location_id'], ['location_id'])
        quant_loc_ids = [loc['location_id'][0] for loc in quant_locs]
        locations = self.env['stock.location'].search([
            ('usage', '=', 'internal'),
            ('company_id', '=', self.env.user.company_id.id),
            ('id', 'in', quant_loc_ids)
        ])

        product_accounts = {
            product.id: product.product_tmpl_id.get_product_accounts()
            for product in self
        }

        for location in locations:
            for product in self.with_context(
                    location=location.id, compute_child=False).filtered(
                        lambda r: r.valuation == 'real_time'):
                diff = product.standard_price - new_price
                if float_is_zero(
                        diff, precision_rounding=product.currency_id.rounding):
                    raise UserError(
                        _("No difference between the standard price and the new price."
                          ))
                if not product_accounts[product.id].get(
                        'stock_valuation', False):
                    raise UserError(
                        _('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.'
                          ))
                qty_available = product.qty_available
                if qty_available:
                    # Accounting Entries
                    if diff * qty_available > 0:
                        debit_account_id = 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 = account_id

                    move_vals = {
                        'journal_id':
                        product_accounts[product.id]['stock_journal'].id,
                        'company_id':
                        location.company_id.id,
                        'ref':
                        product.default_code,
                        '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(diff * qty_available),
                            '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(diff * qty_available),
                             'product_id':
                             product.id,
                         })],
                    }
                    move = AccountMove.create(move_vals)
                    move.post()

        self.write({'standard_price': new_price})
        return True
Ejemplo n.º 23
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'
                # 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.º 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_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})
        invoice_id1 = sale_order.action_invoice_create()
        invoice1 = self.env['account.invoice'].browse(invoice_id1)

        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.action_invoice_open()

        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"
        )
    def _get_partner_move_lines(self, account_type, date_from, target_move,
                                period_length):
        # This method can receive the context key 'include_nullified_amount' {Boolean}
        # Do an invoice and a payment and unreconcile. The amount will be nullified
        # By default, the partner wouldn't appear in this report.
        # The context key allow it to appear
        # In case of a period_length of 30 days as of 2019-02-08, we want the following periods:
        # Name       Stop         Start
        # 1 - 30   : 2019-02-07 - 2019-01-09
        # 31 - 60  : 2019-01-08 - 2018-12-10
        # 61 - 90  : 2018-12-09 - 2018-11-10
        # 91 - 120 : 2018-11-09 - 2018-10-11
        # +120     : 2018-10-10
        periods = {}
        start = datetime.strptime(date_from, "%Y-%m-%d")
        date_from = datetime.strptime(date_from, "%Y-%m-%d").date()
        for i in range(5)[::-1]:
            stop = start - relativedelta(days=period_length)
            period_name = str((5 - (i + 1)) * period_length + 1) + '-' + str(
                (5 - i) * period_length)
            period_stop = (start - relativedelta(days=1)).strftime('%Y-%m-%d')
            if i == 0:
                period_name = '+' + str(4 * period_length)
            periods[str(i)] = {
                'name': period_name,
                'stop': period_stop,
                'start': (i != 0 and stop.strftime('%Y-%m-%d') or False),
            }
            start = stop

        res = []
        total = []
        cr = self.env.cr
        user_company = self.env.user.company_id
        user_currency = user_company.currency_id
        ResCurrency = self.env['res.currency'].with_context(date=date_from)
        company_ids = self._context.get('company_ids') or [user_company.id]
        move_state = ['draft', 'posted']
        if target_move == 'posted':
            move_state = ['posted']
        arg_list = (tuple(move_state), tuple(account_type))
        #build the reconciliation clause to see what partner needs to be printed
        reconciliation_clause = '(l.reconciled IS FALSE)'
        cr.execute(
            'SELECT debit_move_id, credit_move_id FROM account_partial_reconcile where max_date > %s',
            (date_from, ))
        reconciled_after_date = []
        for row in cr.fetchall():
            reconciled_after_date += [row[0], row[1]]
        if reconciled_after_date:
            reconciliation_clause = '(l.reconciled IS FALSE OR l.id IN %s)'
            arg_list += (tuple(reconciled_after_date), )
        arg_list += (date_from, tuple(company_ids))
        query = '''
            SELECT DISTINCT l.partner_id, UPPER(res_partner.name)
            FROM account_move_line AS l left join res_partner on l.partner_id = res_partner.id, account_account, account_move am
            WHERE (l.account_id = account_account.id)
                AND (l.move_id = am.id)
                AND (am.state IN %s)
                AND (account_account.internal_type IN %s)
                AND ''' + reconciliation_clause + '''
                AND (l.date <= %s)
                AND l.company_id IN %s
            ORDER BY UPPER(res_partner.name)'''
        cr.execute(query, arg_list)

        partners = cr.dictfetchall()
        # put a total of 0
        for i in range(7):
            total.append(0)

        # Build a string like (1,2,3) for easy use in SQL query
        partner_ids = [
            partner['partner_id'] for partner in partners
            if partner['partner_id']
        ]
        lines = dict(
            (partner['partner_id'] or False, []) for partner in partners)
        if not partner_ids:
            return [], [], {}

        # This dictionary will store the not due amount of all partners
        undue_amounts = {}
        query = '''SELECT l.id
                FROM account_move_line AS l, account_account, account_move am
                WHERE (l.account_id = account_account.id) AND (l.move_id = am.id)
                    AND (am.state IN %s)
                    AND (account_account.internal_type IN %s)
                    AND (COALESCE(l.date_maturity,l.date) >= %s)\
                    AND ((l.partner_id IN %s) OR (l.partner_id IS NULL))
                AND (l.date <= %s)
                AND l.company_id IN %s'''
        cr.execute(query, (tuple(move_state), tuple(account_type), date_from,
                           tuple(partner_ids), date_from, tuple(company_ids)))
        aml_ids = cr.fetchall()
        aml_ids = aml_ids and [x[0] for x in aml_ids] or []
        for line in self.env['account.move.line'].browse(aml_ids):
            partner_id = line.partner_id.id or False
            if partner_id not in undue_amounts:
                undue_amounts[partner_id] = 0.0
            line_amount = ResCurrency._compute(line.company_id.currency_id,
                                               user_currency, line.balance)
            if user_currency.is_zero(line_amount):
                continue
            for partial_line in line.matched_debit_ids:
                if partial_line.max_date <= date_from:
                    line_amount += ResCurrency._compute(
                        partial_line.company_id.currency_id, user_currency,
                        partial_line.amount)
            for partial_line in line.matched_credit_ids:
                if partial_line.max_date <= date_from:
                    line_amount -= ResCurrency._compute(
                        partial_line.company_id.currency_id, user_currency,
                        partial_line.amount)
            if not self.env.user.company_id.currency_id.is_zero(line_amount):
                undue_amounts[partner_id] += line_amount
                lines[partner_id].append({
                    'line': line,
                    'amount': line_amount,
                    'period': 6,
                })

        # Use one query per period and store results in history (a list variable)
        # Each history will contain: history[1] = {'<partner_id>': <partner_debit-credit>}
        history = []
        for i in range(5):
            args_list = (
                tuple(move_state),
                tuple(account_type),
                tuple(partner_ids),
            )
            dates_query = '(COALESCE(l.date_maturity,l.date)'

            if periods[str(i)]['start'] and periods[str(i)]['stop']:
                dates_query += ' BETWEEN %s AND %s)'
                args_list += (periods[str(i)]['start'],
                              periods[str(i)]['stop'])
            elif periods[str(i)]['start']:
                dates_query += ' >= %s)'
                args_list += (periods[str(i)]['start'], )
            else:
                dates_query += ' <= %s)'
                args_list += (periods[str(i)]['stop'], )
            args_list += (date_from, tuple(company_ids))

            query = '''SELECT l.id
                    FROM account_move_line AS l, account_account, account_move am
                    WHERE (l.account_id = account_account.id) AND (l.move_id = am.id)
                        AND (am.state IN %s)
                        AND (account_account.internal_type IN %s)
                        AND ((l.partner_id IN %s) OR (l.partner_id IS NULL))
                        AND ''' + dates_query + '''
                    AND (l.date <= %s)
                    AND l.company_id IN %s'''
            cr.execute(query, args_list)
            partners_amount = {}
            aml_ids = cr.fetchall()
            aml_ids = aml_ids and [x[0] for x in aml_ids] or []
            for line in self.env['account.move.line'].browse(aml_ids):
                partner_id = line.partner_id.id or False
                if partner_id not in partners_amount:
                    partners_amount[partner_id] = 0.0
                line_amount = ResCurrency._compute(line.company_id.currency_id,
                                                   user_currency, line.balance)
                if user_currency.is_zero(line_amount):
                    continue
                for partial_line in line.matched_debit_ids:
                    if partial_line.max_date <= date_from:
                        line_amount += ResCurrency._compute(
                            partial_line.company_id.currency_id, user_currency,
                            partial_line.amount)
                for partial_line in line.matched_credit_ids:
                    if partial_line.max_date <= date_from:
                        line_amount -= ResCurrency._compute(
                            partial_line.company_id.currency_id, user_currency,
                            partial_line.amount)

                if not self.env.user.company_id.currency_id.is_zero(
                        line_amount):
                    partners_amount[partner_id] += line_amount
                    lines[partner_id].append({
                        'line': line,
                        'amount': line_amount,
                        'period': i + 1,
                    })
            history.append(partners_amount)

        for partner in partners:
            if partner['partner_id'] is None:
                partner['partner_id'] = False
            at_least_one_amount = False
            values = {}
            undue_amt = 0.0
            if partner[
                    'partner_id'] in undue_amounts:  # Making sure this partner actually was found by the query
                undue_amt = undue_amounts[partner['partner_id']]

            total[6] = total[6] + undue_amt
            values['direction'] = undue_amt
            if not float_is_zero(values['direction'],
                                 precision_rounding=self.env.user.company_id.
                                 currency_id.rounding):
                at_least_one_amount = True

            for i in range(5):
                during = False
                if partner['partner_id'] in history[i]:
                    during = [history[i][partner['partner_id']]]
                # Adding counter
                total[(i)] = total[(i)] + (during and during[0] or 0)
                values[str(i)] = during and during[0] or 0.0
                if not float_is_zero(values[str(i)],
                                     precision_rounding=self.env.user.
                                     company_id.currency_id.rounding):
                    at_least_one_amount = True
            values['total'] = sum([values['direction']] +
                                  [values[str(i)] for i in range(5)])
            ## Add for total
            total[(i + 1)] += values['total']
            values['partner_id'] = partner['partner_id']
            if partner['partner_id']:
                browsed_partner = self.env['res.partner'].browse(
                    partner['partner_id'])
                values['name'] = browsed_partner.name and len(
                    browsed_partner.name) >= 45 and browsed_partner.name[
                        0:40] + '...' or browsed_partner.name
                values['trust'] = browsed_partner.trust
            else:
                values['name'] = _('Unknown Partner')
                values['trust'] = False

            if at_least_one_amount or (
                    self._context.get('include_nullified_amount')
                    and lines[partner['partner_id']]):
                res.append(values)

        return res, total, lines
Ejemplo n.º 26
0
    def write(self, vals):
        """ When editing a done stock.move.line, we impact the valuation. Users may increase or
        decrease the `qty_done` field. There are three cost method available: standard, average
        and fifo. We implement the logic in a similar way for standard and average: increase
        or decrease the original value with the standard or average price of today. In fifo, we
        have a different logic wheter the move is incoming or outgoing. If the move is incoming, we
        update the value and remaining_value/qty with the unit price of the move. If the move is
        outgoing and the user increases qty_done, we call _run_fifo and it'll consume layer(s) in
        the stack the same way a new outgoing move would have done. If the move is outoing and the
        user decreases qty_done, we either increase the last receipt candidate if one is found or
        we decrease the value with the last fifo price.
        """
        if 'qty_done' in vals:
            moves_to_update = {}
            for move_line in self.filtered(lambda ml: ml.state == 'done' and (
                    ml.move_id._is_in() or ml.move_id._is_out())):
                rounding = move_line.product_uom_id.rounding
                qty_difference = float_round(vals['qty_done'] -
                                             move_line.qty_done,
                                             precision_rounding=rounding)
                if not float_is_zero(qty_difference,
                                     precision_rounding=rounding):
                    moves_to_update[move_line.move_id] = qty_difference

            for move_id, qty_difference in moves_to_update.items():
                move_vals = {}
                if move_id.product_id.cost_method in ['standard', 'average']:
                    correction_value = qty_difference * move_id.product_id.standard_price
                    if move_id._is_in():
                        move_vals['value'] = move_id.value + correction_value
                    elif move_id._is_out():
                        move_vals['value'] = move_id.value - correction_value
                else:
                    if move_id._is_in():
                        correction_value = qty_difference * move_id.price_unit
                        new_remaining_value = move_id.remaining_value + correction_value
                        move_vals['value'] = move_id.value + correction_value
                        move_vals[
                            'remaining_qty'] = move_id.remaining_qty + qty_difference
                        move_vals[
                            'remaining_value'] = move_id.remaining_value + correction_value
                    elif move_id._is_out() and qty_difference > 0:
                        correction_value = self.env['stock.move']._run_fifo(
                            move_id, quantity=qty_difference)
                        # no need to adapt `remaining_qty` and `remaining_value` as `_run_fifo` took care of it
                        move_vals['value'] = move_id.value - correction_value
                    elif move_id._is_out() and qty_difference < 0:
                        candidates_receipt = self.env['stock.move'].search(
                            move_id._get_in_domain(),
                            order='date, id desc',
                            limit=1)
                        if candidates_receipt:
                            candidates_receipt.write({
                                'remaining_qty':
                                candidates_receipt.remaining_qty +
                                -qty_difference,
                                'remaining_value':
                                candidates_receipt.remaining_value +
                                (-qty_difference *
                                 candidates_receipt.price_unit),
                            })
                            correction_value = qty_difference * candidates_receipt.price_unit
                        else:
                            correction_value = qty_difference * move_id.product_id.standard_price
                        move_vals['value'] = move_id.value - correction_value
                move_id.write(move_vals)

                if move_id.product_id.valuation == 'real_time':
                    move_id.with_context(
                        force_valuation_amount=correction_value,
                        forced_quantity=qty_difference)._account_entry_move()
                if qty_difference > 0:
                    move_id.product_price_update_before_done(
                        forced_qty=qty_difference)
        return super(StockMoveLine, self).write(vals)
Ejemplo n.º 27
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.º 28
0
    def test_timesheet_order(self):
        """ Test timesheet invoicing with 'invoice on order' timetracked products
                1. create SO with 2 ordered product and confirm
                2. create invoice
                3. log timesheet
                4. add new SO line (ordered service)
                5. create new invoice
        """
        # 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_ordered_project_only = self.env['sale.order.line'].create({
            'name':
            self.product_order_timesheet4.name,
            'product_id':
            self.product_order_timesheet4.id,
            'product_uom_qty':
            10,
            'product_uom':
            self.product_order_timesheet4.uom_id.id,
            'price_unit':
            self.product_order_timesheet4.list_price,
            'order_id':
            sale_order.id,
        })
        so_line_ordered_global_project = self.env['sale.order.line'].create({
            'name':
            self.product_order_timesheet2.name,
            'product_id':
            self.product_order_timesheet2.id,
            'product_uom_qty':
            50,
            'product_uom':
            self.product_order_timesheet2.uom_id.id,
            'price_unit':
            self.product_order_timesheet2.list_price,
            'order_id':
            sale_order.id,
        })
        so_line_ordered_project_only.product_id_change()
        so_line_ordered_global_project.product_id_change()
        sale_order.action_confirm()
        task_serv2 = self.env['project.task'].search([
            ('sale_line_id', '=', so_line_ordered_global_project.id)
        ])
        project_serv1 = self.env['project.project'].search([
            ('sale_line_id', '=', so_line_ordered_project_only.id)
        ])

        self.assertEqual(
            sale_order.tasks_count, 1,
            "One task 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 2 'task in global project'"
        )
        self.assertEqual(
            sale_order.analytic_account_id, project_serv1.analytic_account_id,
            "The created project should be linked to the analytic account of the SO"
        )

        # create invoice
        invoice_id1 = sale_order.action_invoice_create()[0]
        invoice1 = self.env['account.invoice'].browse(invoice_id1)

        # let's log some timesheets (on the project created by so_line_ordered_project_only)
        timesheet1 = self.env['account.analytic.line'].create({
            'name':
            'Test Line',
            'project_id':
            task_serv2.project_id.id,
            'task_id':
            task_serv2.id,
            'unit_amount':
            10.5,
            'employee_id':
            self.employee_user.id,
        })
        self.assertEqual(
            so_line_ordered_global_project.qty_delivered, 10.5,
            'Timesheet directly on project does not increase delivered quantity on so line'
        )
        self.assertEqual(
            sale_order.invoice_status, 'invoiced',
            'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so'
        )
        self.assertEqual(
            timesheet1.timesheet_invoice_type, 'billable_fixed',
            "Timesheets linked to SO line with ordered product shoulbe be billable fixed"
        )
        self.assertFalse(
            timesheet1.timesheet_invoice_id,
            "The timesheet1 should not be linked to the invoice, since we are in ordered quantity"
        )

        timesheet2 = self.env['account.analytic.line'].create({
            'name':
            'Test Line',
            'project_id':
            task_serv2.project_id.id,
            'task_id':
            task_serv2.id,
            'unit_amount':
            39.5,
            'employee_id':
            self.employee_user.id,
        })
        self.assertEqual(
            so_line_ordered_global_project.qty_delivered, 50,
            'Sale Timesheet: timesheet does not increase delivered quantity on so line'
        )
        self.assertEqual(
            sale_order.invoice_status, 'invoiced',
            'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so'
        )
        self.assertEqual(
            timesheet2.timesheet_invoice_type, 'billable_fixed',
            "Timesheets linked to SO line with ordered product shoulbe be billable fixed"
        )
        self.assertFalse(
            timesheet2.timesheet_invoice_id,
            "The timesheet should not be linked to the invoice, since we are in ordered quantity"
        )

        timesheet3 = self.env['account.analytic.line'].create({
            'name':
            'Test Line',
            'project_id':
            task_serv2.project_id.id,
            'unit_amount':
            10,
            'employee_id':
            self.employee_user.id,
        })
        self.assertEqual(
            so_line_ordered_project_only.qty_delivered, 0.0,
            'Timesheet directly on project does not increase delivered quantity on so line'
        )
        self.assertEqual(
            timesheet3.timesheet_invoice_type, 'non_billable_project',
            "Timesheets without task shoulbe be 'no project found'")
        self.assertFalse(
            timesheet3.timesheet_invoice_id,
            "The timesheet should not be linked to the invoice, since we are in ordered quantity"
        )

        # log timesheet on task in global project (higher than the initial ordrered qty)
        timesheet4 = self.env['account.analytic.line'].create({
            'name':
            'Test Line',
            'project_id':
            task_serv2.project_id.id,
            'task_id':
            task_serv2.id,
            'unit_amount':
            5,
            'employee_id':
            self.employee_user.id,
        })
        self.assertEqual(
            sale_order.invoice_status, 'upselling',
            'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so'
        )
        self.assertFalse(
            timesheet4.timesheet_invoice_id,
            "The timesheet should not be linked to the invoice, since we are in ordered quantity"
        )

        # add so line with produdct "create task in new project".
        so_line_ordered_task_new_project = self.env['sale.order.line'].create({
            'name':
            self.product_order_timesheet3.name,
            'product_id':
            self.product_order_timesheet3.id,
            'product_uom_qty':
            3,
            'product_uom':
            self.product_order_timesheet3.uom_id.id,
            'price_unit':
            self.product_order_timesheet3.list_price,
            'order_id':
            sale_order.id,
        })

        self.assertEqual(
            sale_order.invoice_status, 'to invoice',
            'Sale Timesheet: Adding a new service line (so line) should put the SO in "to invocie" state.'
        )
        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,
            "No new project should have been created by the SO, when selling 'new task in new project' product, since it reuse the one from 'project only'."
        )

        # get first invoice line of sale line linked to timesheet1
        invoice_line_1 = so_line_ordered_global_project.invoice_lines.filtered(
            lambda line: line.invoice_id.id == invoice_id1)

        self.assertEqual(
            so_line_ordered_global_project.product_uom_qty,
            invoice_line_1.quantity,
            "The invoice (ordered) quantity should not change when creating timesheet"
        )

        # timesheet can be modified
        timesheet1.write({'unit_amount': 12})

        self.assertEqual(
            so_line_ordered_global_project.product_uom_qty,
            invoice_line_1.quantity,
            "The invoice (ordered) quantity should not change when modifying timesheet"
        )

        # create second invoice
        invoice_id2 = sale_order.action_invoice_create()[0]
        invoice2 = self.env['account.invoice'].browse(invoice_id2)

        self.assertEqual(
            len(sale_order.invoice_ids), 2,
            "A second invoice should have been created from the SO")
        self.assertTrue(
            float_is_zero(invoice2.amount_total -
                          so_line_ordered_task_new_project.price_unit * 3,
                          precision_digits=2),
            'Sale: invoice generation on timesheets product is wrong')

        self.assertFalse(
            timesheet1.timesheet_invoice_id,
            "The timesheet1 should not be linked to the invoice, since we are in ordered quantity"
        )
        self.assertFalse(
            timesheet2.timesheet_invoice_id,
            "The timesheet2 should not be linked to the invoice, since we are in ordered quantity"
        )
        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"
        )

        # validate the first invoice
        invoice1.action_invoice_open()

        self.assertEqual(
            so_line_ordered_global_project.product_uom_qty,
            invoice_line_1.quantity,
            "The invoice (ordered) quantity should not change when modifying timesheet"
        )
        self.assertFalse(
            timesheet1.timesheet_invoice_id,
            "The timesheet1 should not be linked to the invoice, since we are in ordered quantity"
        )
        self.assertFalse(
            timesheet2.timesheet_invoice_id,
            "The timesheet2 should not be linked to the invoice, since we are in ordered quantity"
        )
        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"
        )

        # timesheet can still be modified
        timesheet1.write({'unit_amount': 13})
Ejemplo n.º 29
0
    def generate_fec(self):
        self.ensure_one()
        # We choose to implement the flat file instead of the XML
        # file for 2 reasons :
        # 1) the XSD file impose to have the label on the account.move
        # but Eagle has the label on the account.move.line, so that's a
        # problem !
        # 2) CSV files are easier to read/use for a regular accountant.
        # So it will be easier for the accountant to check the file before
        # sending it to the fiscal administration
        company = self.env.user.company_id
        company_legal_data = self._get_company_legal_data(company)

        header = [
            u'JournalCode',  # 0
            u'JournalLib',  # 1
            u'EcritureNum',  # 2
            u'EcritureDate',  # 3
            u'CompteNum',  # 4
            u'CompteLib',  # 5
            u'CompAuxNum',  # 6  We use partner.id
            u'CompAuxLib',  # 7
            u'PieceRef',  # 8
            u'PieceDate',  # 9
            u'EcritureLib',  # 10
            u'Debit',  # 11
            u'Credit',  # 12
            u'EcritureLet',  # 13
            u'DateLet',  # 14
            u'ValidDate',  # 15
            u'Montantdevise',  # 16
            u'Idevise',  # 17
        ]

        rows_to_write = [header]
        # INITIAL BALANCE
        unaffected_earnings_xml_ref = self.env.ref(
            'account.data_unaffected_earnings')
        unaffected_earnings_line = True  # used to make sure that we add the unaffected earning initial balance only once
        if unaffected_earnings_xml_ref:
            #compute the benefit/loss of last year to add in the initial balance of the current year earnings account
            unaffected_earnings_results = self.do_query_unaffected_earnings()
            unaffected_earnings_line = False

        sql_query = '''
        SELECT
            'OUV' AS JournalCode,
            'Balance initiale' AS JournalLib,
            'OUVERTURE/' || %s AS EcritureNum,
            %s AS EcritureDate,
            MIN(aa.code) AS CompteNum,
            replace(replace(MIN(aa.name), '|', '/'), '\t', '') AS CompteLib,
            '' AS CompAuxNum,
            '' AS CompAuxLib,
            '-' AS PieceRef,
            %s AS PieceDate,
            '/' AS EcritureLib,
            replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit,
            replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit,
            '' AS EcritureLet,
            '' AS DateLet,
            %s AS ValidDate,
            '' AS Montantdevise,
            '' AS Idevise,
            MIN(aa.id) AS CompteID
        FROM
            account_move_line aml
            LEFT JOIN account_move am ON am.id=aml.move_id
            JOIN account_account aa ON aa.id = aml.account_id
            LEFT JOIN account_account_type aat ON aa.user_type_id = aat.id
        WHERE
            am.date < %s
            AND am.company_id = %s
            AND aat.include_initial_balance = 't'
            AND (aml.debit != 0 OR aml.credit != 0)
        '''

        # For official report: only use posted entries
        if self.export_type == "official":
            sql_query += '''
            AND am.state = 'posted'
            '''

        sql_query += '''
        GROUP BY aml.account_id, aat.type
        HAVING round(sum(aml.balance), %s) != 0
        AND aat.type not in ('receivable', 'payable')
        '''
        formatted_date_from = fields.Date.to_string(self.date_from).replace(
            '-', '')
        date_from = self.date_from
        formatted_date_year = date_from.year
        currency_digits = 2

        self._cr.execute(
            sql_query,
            (formatted_date_year, formatted_date_from, formatted_date_from,
             formatted_date_from, self.date_from, company.id, currency_digits))

        for row in self._cr.fetchall():
            listrow = list(row)
            account_id = listrow.pop()
            if not unaffected_earnings_line:
                account = self.env['account.account'].browse(account_id)
                if account.user_type_id.id == self.env.ref(
                        'account.data_unaffected_earnings').id:
                    #add the benefit/loss of previous fiscal year to the first unaffected earnings account found.
                    unaffected_earnings_line = True
                    current_amount = float(listrow[11].replace(
                        ',', '.')) - float(listrow[12].replace(',', '.'))
                    unaffected_earnings_amount = float(
                        unaffected_earnings_results[11].replace(
                            ',', '.')) - float(
                                unaffected_earnings_results[12].replace(
                                    ',', '.'))
                    listrow_amount = current_amount + unaffected_earnings_amount
                    if float_is_zero(listrow_amount,
                                     precision_digits=currency_digits):
                        continue
                    if listrow_amount > 0:
                        listrow[11] = str(listrow_amount).replace('.', ',')
                        listrow[12] = '0,00'
                    else:
                        listrow[11] = '0,00'
                        listrow[12] = str(-listrow_amount).replace('.', ',')
            rows_to_write.append(listrow)

        #if the unaffected earnings account wasn't in the selection yet: add it manually
        if (not unaffected_earnings_line and unaffected_earnings_results
                and (unaffected_earnings_results[11] != '0,00'
                     or unaffected_earnings_results[12] != '0,00')):
            #search an unaffected earnings account
            unaffected_earnings_account = self.env['account.account'].search(
                [('user_type_id', '=',
                  self.env.ref('account.data_unaffected_earnings').id)],
                limit=1)
            if unaffected_earnings_account:
                unaffected_earnings_results[
                    4] = unaffected_earnings_account.code
                unaffected_earnings_results[
                    5] = unaffected_earnings_account.name
            rows_to_write.append(unaffected_earnings_results)

        # INITIAL BALANCE - receivable/payable
        sql_query = '''
        SELECT
            'OUV' AS JournalCode,
            'Balance initiale' AS JournalLib,
            'OUVERTURE/' || %s AS EcritureNum,
            %s AS EcritureDate,
            MIN(aa.code) AS CompteNum,
            replace(MIN(aa.name), '|', '/') AS CompteLib,
            CASE WHEN rp.ref IS null OR rp.ref = ''
            THEN COALESCE('ID ' || rp.id, '')
            ELSE replace(rp.ref, '|', '/')
            END
            AS CompAuxNum,
            COALESCE(replace(rp.name, '|', '/'), '') AS CompAuxLib,
            '-' AS PieceRef,
            %s AS PieceDate,
            '/' AS EcritureLib,
            replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit,
            replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit,
            '' AS EcritureLet,
            '' AS DateLet,
            %s AS ValidDate,
            '' AS Montantdevise,
            '' AS Idevise,
            MIN(aa.id) AS CompteID
        FROM
            account_move_line aml
            LEFT JOIN account_move am ON am.id=aml.move_id
            LEFT JOIN res_partner rp ON rp.id=aml.partner_id
            JOIN account_account aa ON aa.id = aml.account_id
            LEFT JOIN account_account_type aat ON aa.user_type_id = aat.id
        WHERE
            am.date < %s
            AND am.company_id = %s
            AND aat.include_initial_balance = 't'
            AND (aml.debit != 0 OR aml.credit != 0)
        '''

        # For official report: only use posted entries
        if self.export_type == "official":
            sql_query += '''
            AND am.state = 'posted'
            '''

        sql_query += '''
        GROUP BY aml.account_id, aat.type, rp.ref, rp.id
        HAVING round(sum(aml.balance), %s) != 0
        AND aat.type in ('receivable', 'payable')
        '''
        self._cr.execute(
            sql_query,
            (formatted_date_year, formatted_date_from, formatted_date_from,
             formatted_date_from, self.date_from, company.id, currency_digits))

        for row in self._cr.fetchall():
            listrow = list(row)
            account_id = listrow.pop()
            rows_to_write.append(listrow)

        # LINES
        sql_query = '''
        SELECT
            replace(replace(aj.code, '|', '/'), '\t', '') AS JournalCode,
            replace(replace(aj.name, '|', '/'), '\t', '') AS JournalLib,
            replace(replace(am.name, '|', '/'), '\t', '') AS EcritureNum,
            TO_CHAR(am.date, 'YYYYMMDD') AS EcritureDate,
            aa.code AS CompteNum,
            replace(replace(aa.name, '|', '/'), '\t', '') AS CompteLib,
            CASE WHEN rp.ref IS null OR rp.ref = ''
            THEN COALESCE('ID ' || rp.id, '')
            ELSE replace(rp.ref, '|', '/')
            END
            AS CompAuxNum,
            COALESCE(replace(replace(rp.name, '|', '/'), '\t', ''), '') AS CompAuxLib,
            CASE WHEN am.ref IS null OR am.ref = ''
            THEN '-'
            ELSE replace(replace(am.ref, '|', '/'), '\t', '')
            END
            AS PieceRef,
            TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate,
            CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/'
                WHEN aml.name SIMILAR TO '[\t|\s|\n]*' THEN '/'
                ELSE replace(replace(replace(replace(aml.name, '|', '/'), '\t', ''), '\n', ''), '\r', '') END AS EcritureLib,
            replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit,
            replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit,
            CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet,
            CASE WHEN aml.full_reconcile_id IS NULL THEN '' ELSE TO_CHAR(rec.create_date, 'YYYYMMDD') END AS DateLet,
            TO_CHAR(am.date, 'YYYYMMDD') AS ValidDate,
            CASE
                WHEN aml.amount_currency IS NULL OR aml.amount_currency = 0 THEN ''
                ELSE replace(to_char(aml.amount_currency, '000000000000000D99'), '.', ',')
            END AS Montantdevise,
            CASE WHEN aml.currency_id IS NULL THEN '' ELSE rc.name END AS Idevise
        FROM
            account_move_line aml
            LEFT JOIN account_move am ON am.id=aml.move_id
            LEFT JOIN res_partner rp ON rp.id=aml.partner_id
            JOIN account_journal aj ON aj.id = am.journal_id
            JOIN account_account aa ON aa.id = aml.account_id
            LEFT JOIN res_currency rc ON rc.id = aml.currency_id
            LEFT JOIN account_full_reconcile rec ON rec.id = aml.full_reconcile_id
        WHERE
            am.date >= %s
            AND am.date <= %s
            AND am.company_id = %s
            AND (aml.debit != 0 OR aml.credit != 0)
        '''

        # For official report: only use posted entries
        if self.export_type == "official":
            sql_query += '''
            AND am.state = 'posted'
            '''

        sql_query += '''
        ORDER BY
            am.date,
            am.name,
            aml.id
        '''
        self._cr.execute(sql_query, (self.date_from, self.date_to, company.id))

        for row in self._cr.fetchall():
            rows_to_write.append(list(row))

        fecvalue = self._csv_write_rows(rows_to_write)
        end_date = fields.Date.to_string(self.date_to).replace('-', '')
        suffix = ''
        if self.export_type == "nonofficial":
            suffix = '-NONOFFICIAL'

        self.write({
            'fec_data':
            base64.encodestring(fecvalue),
            # Filename = <siren>FECYYYYMMDD where YYYMMDD is the closing date
            'filename':
            '%sFEC%s%s.csv' % (company_legal_data['siren'], end_date, suffix),
        })

        action = {
            'name':
            'FEC',
            'type':
            'ir.actions.act_url',
            'url':
            "web/content/?model=account.fr.fec&id=" + str(self.id) +
            "&filename_field=filename&field=fec_data&download=true&filename=" +
            self.filename,
            'target':
            'self',
        }
        return action
Ejemplo n.º 30
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
        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
        invoice_id1 = sale_order.action_invoice_create()
        invoice1 = self.env['account.invoice'].browse(invoice_id1)
        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
        invoice_id2 = sale_order.action_invoice_create()[0]
        invoice2 = self.env['account.invoice'].browse(invoice_id2)
        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.action_invoice_open()

        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"
        )