Ejemplo n.º 1
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)
            qty = forced_qty or qty_done
            # If the current stock is negative, we should not average it with the incoming one
            if float_is_zero(product_tot_qty_available, precision_rounding=rounding) or product_tot_qty_available < 0:
                new_std_price = move._get_price_unit()
            elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding) or \
                    float_is_zero(product_tot_qty_available + qty, precision_rounding=rounding):
                new_std_price = move._get_price_unit()
            else:
                # Get the standard price
                amount_unit = std_price_update.get((move.company_id.id, move.product_id.id)) or move.product_id.standard_price
                new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / (product_tot_qty_available + qty)

            tmpl_dict[move.product_id.id] += qty_done
            # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
            move.product_id.with_context(force_company=move.company_id.id).sudo().write({'standard_price': new_std_price})
            std_price_update[move.company_id.id, move.product_id.id] = new_std_price
Ejemplo n.º 2
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.º 3
0
 def write(self, vals):
     if 'uom_id' in vals:
         new_uom = self.env['uom.uom'].browse(vals['uom_id'])
         updated = self.filtered(
             lambda template: template.uom_id != new_uom)
         done_moves = self.env['stock.move'].search(
             [('product_id', 'in', updated.with_context(
                 active_test=False).mapped('product_variant_ids').ids)],
             limit=1)
         if done_moves:
             raise UserError(
                 _("You cannot change the unit of measure as there are already stock moves for this product. If you want to change the unit of measure, you should rather archive this product and create a new one."
                   ))
     if 'type' in vals and vals['type'] != 'product' and sum(
             self.mapped('nbr_reordering_rules')) != 0:
         raise UserError(
             _('You still have some active reordering rules on this product. Please archive or delete them first.'
               ))
     if any('type' in vals and vals['type'] != prod_tmpl.type
            for prod_tmpl in self):
         existing_move_lines = self.env['stock.move.line'].search([
             ('product_id', 'in', self.mapped('product_variant_ids').ids),
             ('state', 'in', ['partially_available', 'assigned']),
         ])
         if existing_move_lines:
             raise UserError(
                 _("You can not change the type of a product that is currently reserved on a stock move. If you need to change the type, you should first unreserve the stock move."
                   ))
     if 'type' in vals and vals['type'] != 'product' and self.filtered(
             lambda p: p.type == 'product' and not float_is_zero(
                 p.qty_available, precision_rounding=p.uom_id.rounding)):
         raise UserError(
             _("Available quantity should be set to zero before changing type"
               ))
     return super(ProductTemplate, self).write(vals)
Ejemplo n.º 4
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.º 5
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.user.company_id.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.º 6
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.º 7
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.action_invoice_open()

        # Check discount appeared on both SO lines and invoice lines
        for line, inv_line in pycompat.izip(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.º 8
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.º 9
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.º 10
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.º 11
0
    def _check_rule_propositions(self, statement_line, candidates):
        ''' Check restrictions that can't be handled for each move.line separately.
        /!\ Only used by models having a type equals to 'invoice_matching'.
        :param statement_line:  An account.bank.statement.line record.
        :param candidates:      Fetched account.move.lines from query (dict).
        :return:                True if the reconciliation propositions are accepted. False otherwise.
        '''
        if not self.match_total_amount:
            return True
        if not candidates:
            return False

        # Match total residual amount.
        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

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

        if line_residual_to_compare > total_residual_to_compare:
            amount_percentage = (total_residual_to_compare / line_residual_to_compare) * 100
        elif total_residual:
            amount_percentage = (line_residual_to_compare / total_residual_to_compare) * 100
        else:
            return False
        return amount_percentage >= self.match_total_amount_param
Ejemplo n.º 12
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.º 13
0
 def test_paid(self):
     if self.config_id.cash_rounding:
         total = float_round(self.amount_total, precision_rounding=self.config_id.rounding_method.rounding, rounding_method=self.config_id.rounding_method.rounding_method)
         return float_is_zero(total - self.amount_paid, precision_rounding=self.config_id.currency_id.rounding)
     else:
         return super(PosOrder, self).test_paid()
Ejemplo n.º 14
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 Swerp 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.º 15
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
        }

        prec = self.env['decimal.precision'].precision_get('Product Price')
        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_digits=prec):
                    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.º 16
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:
                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_product_to_produce(
                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.final_lot_id = False
                    wo.active_move_line_ids.unlink()
                wo.qty_producing = quantity
                if wo.qty_produced < wo.qty_production and wo.state == 'done':
                    wo.state = 'progress'
                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 quantity > 0 and wo.move_raw_ids.filtered(
                        lambda x: x.product_id.tracking != 'none'
                ) and not wo.active_move_line_ids:
                    wo._generate_lot_ids()
        return {}
Ejemplo n.º 17
0
    def _apply_rules(self, st_lines, excluded_ids=None, partner_map=None):
        ''' Apply criteria to get candidates for all reconciliation models.
        :param st_lines:        Account.bank.statement.lines recordset.
        :param excluded_ids:    Account.move.lines to exclude.
        :param partner_map:     Dict mapping each line with new partner eventually.
        :return:                A dict mapping each statement line id with:
            * aml_ids:      A list of account.move.line ids.
            * model:        An account.reconcile.model record (optional).
            * status:       'reconciled' if the lines has been already reconciled, 'write_off' if the write-off must be
                            applied on the statement line.
        '''
        available_models = self.filtered(lambda m: m.rule_type != 'writeoff_button')

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

        if not available_models:
            return results

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

        grouped_candidates = {}

        # Type == 'invoice_matching'.
        # Map each (st_line.id, model_id) with matching amls.
        invoices_models = ordered_models.filtered(lambda m: m.rule_type == 'invoice_matching')
        if invoices_models:
            query, params = invoices_models._get_invoice_matching_query(st_lines, excluded_ids=excluded_ids, partner_map=partner_map)
            self._cr.execute(query, params)
            query_res = self._cr.dictfetchall()

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

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

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

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

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

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

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

                excluded_lines_found = False

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

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

                        # Dispatch candidates between lines matching invoices with the communication or only the partner.
                        if c['communication_flag']:
                            first_batch_candidates.append(c)
                        elif not first_batch_candidates:
                            second_batch_candidates.append(c)
                    available_candidates = first_batch_candidates or second_batch_candidates

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

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

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

                        # Add candidates to the result.
                        for candidate in available_candidates:

                            # Special case: the propositions match the rule but some of them are already consumed by
                            # another one. Then, suggest the remaining propositions to the user but don't make any
                            # automatic reconciliation.
                            if candidate['aml_id'] in amls_ids_to_exclude:
                                excluded_lines_found = True
                                continue

                            results[line.id]['aml_ids'].append(candidate['aml_id'])
                            amls_ids_to_exclude.add(candidate['aml_id'])

                        if excluded_lines_found:
                            break

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

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

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

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

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

                        # Break models loop.
                        break

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

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

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

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

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

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

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

        balance = total_residual - line_residual

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

        if self.amount_type == 'percentage':
            line_balance = balance * (self.amount / 100.0)
        else:
            line_balance = self.amount * (1 if balance > 0.0 else -1)

        new_aml_dicts = []

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

        if self.tax_id:
            writeoff_line['tax_ids'] = [(6, None, [self.tax_id.id])]
            tax = self.tax_id
            if self.force_tax_included:
                tax = tax.with_context(force_price_include=True)
            new_aml_dicts += self._get_taxes_move_lines_dict(tax, writeoff_line)

        # Second write-off line.
        if self.has_second_line and self.second_account_id:
            remaining_balance = balance - sum(aml['debit'] - aml['credit'] for aml in new_aml_dicts)
            if self.second_amount_type == 'percentage':
                line_balance = remaining_balance * (self.second_amount / 100.0)
            else:
                line_balance = self.second_amount * (1 if remaining_balance > 0.0 else -1)
            second_writeoff_line = {
                'name': self.second_label or st_line.name,
                'account_id': self.second_account_id.id,
                'analytic_account_id': self.second_analytic_account_id.id,
                'analytic_tag_ids': [(6, 0, self.second_analytic_tag_ids.ids)],
                'debit': line_balance > 0 and line_balance or 0,
                'credit': line_balance < 0 and -line_balance or 0,
            }
            new_aml_dicts.append(second_writeoff_line)

            if self.second_tax_id:
                second_writeoff_line['tax_ids'] = [(6, None, [self.second_tax_id.id])]
                tax = self.second_tax_id
                if self.force_second_tax_included:
                    tax = tax.with_context(force_price_include=True)
                new_aml_dicts += self._get_taxes_move_lines_dict(tax, second_writeoff_line)

        return new_aml_dicts
Ejemplo n.º 20
0
    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
        ctx = self._context
        periods = {}
        date_from = fields.Date.from_string(date_from)
        start = date_from
        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 = []
        partner_clause = ''
        cr = self.env.cr
        user_company = self.env.user.company_id
        user_currency = user_company.currency_id
        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),
            date_from,
            date_from,
        )
        if ctx.get('partner_ids'):
            partner_clause = 'AND (l.partner_id IN %s)'
            arg_list += (tuple(ctx['partner_ids'].ids), )
        if ctx.get('partner_categories'):
            partner_clause += 'AND (l.partner_id IN %s)'
            partner_ids = self.env['res.partner'].search([
                ('category_id', 'in', ctx['partner_categories'].ids)
            ]).ids
            arg_list += (tuple(partner_ids or [0]), )
        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 (
                        l.reconciled IS FALSE
                        OR l.id IN(
                            SELECT credit_move_id FROM account_partial_reconcile where max_date > %s
                            UNION ALL
                            SELECT debit_move_id FROM account_partial_reconcile where max_date > %s
                        )
                    )
                    ''' + partner_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 [], [], {}

        # 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
                    ORDER BY COALESCE(l.date_maturity, l.date)'''
            cr.execute(query, args_list)
            partners_amount = {}
            aml_ids = [x[0] for x in cr.fetchall()]
            # prefetch the fields that will be used; this avoid cache misses,
            # which look up the cache to determine the records to read, and has
            # quadratic complexity when the number of records is large...
            move_lines = self.env['account.move.line'].browse(aml_ids)
            move_lines.read([
                'partner_id', 'company_id', 'balance', 'matched_debit_ids',
                'matched_credit_ids'
            ])
            move_lines.mapped('matched_debit_ids').read(
                ['max_date', 'company_id', 'amount'])
            move_lines.mapped('matched_credit_ids').read(
                ['max_date', 'company_id', 'amount'])
            for line in move_lines:
                partner_id = line.partner_id.id or False
                if partner_id not in partners_amount:
                    partners_amount[partner_id] = 0.0
                line_amount = line.company_id.currency_id._convert(
                    line.balance, user_currency, user_company, date_from)
                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 += partial_line.company_id.currency_id._convert(
                            partial_line.amount, user_currency, user_company,
                            date_from)
                for partial_line in line.matched_credit_ids:
                    if partial_line.max_date <= date_from:
                        line_amount -= partial_line.company_id.currency_id._convert(
                            partial_line.amount, user_currency, user_company,
                            date_from)

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

        # 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
                ORDER BY COALESCE(l.date_maturity, l.date)'''
        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 = line.company_id.currency_id._convert(
                line.balance, user_currency, user_company, date_from)
            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 += partial_line.company_id.currency_id._convert(
                        partial_line.amount, user_currency, user_company,
                        date_from)
            for partial_line in line.matched_credit_ids:
                if partial_line.max_date <= date_from:
                    line_amount -= partial_line.company_id.currency_id._convert(
                        partial_line.amount, user_currency, user_company,
                        date_from)
            if not self.env.user.company_id.currency_id.is_zero(line_amount):
                undue_amounts[partner_id] += line_amount
                lines.setdefault(partner_id, [])
                lines[partner_id].append({
                    'line': line,
                    'amount': line_amount,
                    'period': 6,
                })

        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']:
                #browse the partner name and trust field in sudo, as we may not have full access to the record (but we still have to see it in the report)
                browsed_partner = self.env['res.partner'].sudo().browse(
                    partner['partner_id'])
                values['name'] = browsed_partner.name and len(
                    browsed_partner.name) >= 45 and not self.env.context.get(
                        'no_format') and browsed_partner.name[
                            0:41] + '...' 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.º 21
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"
        )
Ejemplo n.º 22
0
    def test_invoice_refund(self):
        """ Test invoice with a refund and check customer invoices credit note is created from respective invoice """
        # lines are in draft
        for line in self.sale_order.order_line:
            self.assertTrue(float_is_zero(line.untaxed_amount_to_invoice, precision_digits=2), "The amount to invoice should be zero, as the line is in draf state")
            self.assertTrue(float_is_zero(line.untaxed_amount_invoiced, precision_digits=2), "The invoiced amount should be zero, as the line is in draft state")

        # Confirm the SO
        self.sale_order.action_confirm()

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

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

        invoice = self.sale_order.invoice_ids[0]

        # Update quantity of an invoice lines
        invoice.invoice_line_ids[0].write({'quantity': 3.0})  # product ordered: from 5 to 3
        invoice.invoice_line_ids[1].write({'quantity': 2.0})  # service ordered: from 3 to 2

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

        invoice.action_invoice_open()

        # Check quantity to invoice on SO lines
        for line in self.sale_order.order_line:
            if line.product_id.invoice_policy == 'delivery':
                self.assertEquals(line.qty_to_invoice, 0.0, "Quantity to invoice should be same as ordered quantity")
                self.assertEquals(line.qty_invoiced, 0.0, "Invoiced quantity should be zero as no any invoice created for SO")
                self.assertEquals(line.untaxed_amount_to_invoice, 0.0, "The amount to invoice should be zero, as the line based on delivered quantity")
                self.assertEquals(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as the line based on delivered quantity")
            else:
                if line == self.sol_prod_order:
                    self.assertEquals(line.qty_to_invoice, 2.0, "The ordered sale line are totally invoiced (qty to invoice is zero)")
                    self.assertEquals(line.qty_invoiced, 3.0, "The ordered (prod) sale line are totally invoiced (qty invoiced come from the invoice lines)")
                else:
                    self.assertEquals(line.qty_to_invoice, 1.0, "The ordered sale line are totally invoiced (qty to invoice is zero)")
                    self.assertEquals(line.qty_invoiced, 2.0, "The ordered (serv) sale line are totally invoiced (qty invoiced = the invoice lines)")
                self.assertEquals(line.untaxed_amount_to_invoice, line.price_unit * line.qty_to_invoice, "Amount to invoice is now set as qty to invoice * unit price since no price change on invoice, for ordered products")
                self.assertEquals(line.untaxed_amount_invoiced, line.price_unit * line.qty_invoiced, "Amount invoiced is now set as qty invoiced * unit price since no price change on invoice, for ordered products")

        # Make a credit note
        credit_note_wizard = self.env['account.invoice.refund'].with_context({'active_ids': [invoice.id], 'active_id': invoice.id}).create({
            'filter_refund': 'modify',  # this is the only mode for which the SO line is linked to the refund (https://github.com/swerp/swerp/commit/e680f29560ac20133c7af0c6364c6ef494662eac)
            'description': 'reason test',
        })
        credit_note_wizard.invoice_refund()
        invoice_2 = self.sale_order.invoice_ids.sorted(key=lambda inv: inv.id, reverse=False)[-1]  # the first invoice, its refund, and the new invoice

        # Check invoice's type and number
        self.assertEquals(invoice_2.type, 'out_invoice', 'The last created invoiced should be a customer invoice')
        self.assertEquals(invoice_2.state, 'draft', 'Last Customer invoices should be in draft')

        # At this time, the invoice 1 and its refund are confirmed, so the amounts invoiced are zero. The third invoice
        # (2nd customer inv) is in draft state.
        for line in self.sale_order.order_line:
            if line.product_id.invoice_policy == 'delivery':
                self.assertEquals(line.qty_to_invoice, 0.0, "Quantity to invoice should be same as ordered quantity")
                self.assertEquals(line.qty_invoiced, 0.0, "Invoiced quantity should be zero as no any invoice created for SO")
                self.assertEquals(line.untaxed_amount_to_invoice, 0.0, "The amount to invoice should be zero, as the line based on delivered quantity")
                self.assertEquals(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as the line based on delivered quantity")
            else:
                if line == self.sol_prod_order:
                    self.assertEquals(line.qty_to_invoice, 2.0, "The qty to invoice does not change when confirming the new invoice (2)")
                    self.assertEquals(line.qty_invoiced, 3.0, "The ordered (prod) sale line does not change on invoice 2 confirmation")
                    self.assertEquals(line.untaxed_amount_to_invoice, line.price_unit * 5, "Amount to invoice is now set as qty to invoice * unit price since no price change on invoice, for ordered products")
                    self.assertEquals(line.untaxed_amount_invoiced, 0.0, "Amount invoiced is zero as the invoice 1 and its refund are reconcilied")
                else:
                    self.assertEquals(line.qty_to_invoice, 1.0, "The qty to invoice does not change when confirming the new invoice (2)")
                    self.assertEquals(line.qty_invoiced, 2.0, "The ordered (serv) sale line does not change on invoice 2 confirmation")
                    self.assertEquals(line.untaxed_amount_to_invoice, line.price_unit * 3, "Amount to invoice is now set as unit price * ordered qty - refund qty) even if the ")
                    self.assertEquals(line.untaxed_amount_invoiced, 0.0, "Amount invoiced is zero as the invoice 1 and its refund are reconcilied")

        # Change unit of ordered product on refund lines
        invoice_2.invoice_line_ids.filtered(lambda invl: invl.product_id == self.sol_prod_order.product_id).write({'price_unit': 100})
        invoice_2.invoice_line_ids.filtered(lambda invl: invl.product_id == self.sol_serv_order.product_id).write({'price_unit': 50})

        # Validate the refund
        invoice_2.action_invoice_open()

        for line in self.sale_order.order_line:
            if line.product_id.invoice_policy == 'delivery':
                self.assertEquals(line.qty_to_invoice, 0.0, "Quantity to invoice should be same as ordered quantity")
                self.assertEquals(line.qty_invoiced, 0.0, "Invoiced quantity should be zero as no any invoice created for SO")
                self.assertEquals(line.untaxed_amount_to_invoice, 0.0, "The amount to invoice should be zero, as the line based on delivered quantity")
                self.assertEquals(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as the line based on delivered quantity")
            else:
                if line == self.sol_prod_order:
                    self.assertEquals(line.qty_to_invoice, 2.0, "The qty to invoice does not change when confirming the new invoice (2)")
                    self.assertEquals(line.qty_invoiced, 3.0, "The ordered sale line are totally invoiced (qty invoiced = ordered qty)")
                    self.assertEquals(line.untaxed_amount_to_invoice, 1100.0, "")
                    self.assertEquals(line.untaxed_amount_invoiced, 300.0, "")
                else:
                    self.assertEquals(line.qty_to_invoice, 1.0, "The qty to invoice does not change when confirming the new invoice (2)")
                    self.assertEquals(line.qty_invoiced, 2.0, "The ordered sale line are totally invoiced (qty invoiced = ordered qty)")
                    self.assertEquals(line.untaxed_amount_to_invoice, 170.0, "")
                    self.assertEquals(line.untaxed_amount_invoiced, 100.0, "")
Ejemplo n.º 23
0
 def _is_difference_zero(self):
     for bank_stmt in self:
         bank_stmt.is_difference_zero = float_is_zero(
             bank_stmt.difference,
             precision_digits=bank_stmt.currency_id.decimal_places)
Ejemplo n.º 24
0
 def try_zero(amount, expected):
     self.assertEqual(float_is_zero(amount, precision_digits=3), expected,
                      "Rounding error: %s should be zero!" % amount)