From 71ecc030012337279021e98aaa13f57132d75ee3 Mon Sep 17 00:00:00 2001 From: Yannick Vaucher Date: Wed, 27 Jun 2018 18:43:39 +0200 Subject: [PATCH] Allow to change analytic fields with account_invoice_update --- account_invoice_update_wizard/__manifest__.py | 3 + .../tests/__init__.py | 1 + .../test_account_invoice_update_wizard.py | 198 ++++++++++++++++++ .../wizard/account_invoice_update.py | 93 +++++++- .../wizard/account_invoice_update_view.xml | 2 + 5 files changed, 293 insertions(+), 4 deletions(-) create mode 100644 account_invoice_update_wizard/tests/__init__.py create mode 100644 account_invoice_update_wizard/tests/test_account_invoice_update_wizard.py diff --git a/account_invoice_update_wizard/__manifest__.py b/account_invoice_update_wizard/__manifest__.py index 71e396b..eced67e 100644 --- a/account_invoice_update_wizard/__manifest__.py +++ b/account_invoice_update_wizard/__manifest__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # © 2017 Akretion (Alexis de Lattre ) +# Copyright 2018 Camptocamp # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { @@ -21,6 +22,8 @@ This module adds a button *Update Invoice* on Customer and Supplier invoices in * Salesman * Notes * Description of invoice lines +* Analytic account +* Analytic tags """, 'author': 'Akretion', diff --git a/account_invoice_update_wizard/tests/__init__.py b/account_invoice_update_wizard/tests/__init__.py new file mode 100644 index 0000000..09720fb --- /dev/null +++ b/account_invoice_update_wizard/tests/__init__.py @@ -0,0 +1 @@ +from . import test_account_invoice_update_wizard diff --git a/account_invoice_update_wizard/tests/test_account_invoice_update_wizard.py b/account_invoice_update_wizard/tests/test_account_invoice_update_wizard.py new file mode 100644 index 0000000..737f266 --- /dev/null +++ b/account_invoice_update_wizard/tests/test_account_invoice_update_wizard.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Camptocamp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError + + +class TestAccountInvoiceUpdateWizard(TransactionCase): + + def setUp(self): + super(TestAccountInvoiceUpdateWizard, self).setUp() + self.customer12 = self.env.ref('base.res_partner_12') + self.product16 = self.env.ref('product.product_product_16') + self.product24 = self.env.ref('product.product_product_24') + uom_unit = self.env.ref('product.product_uom_categ_unit') + + self.invoice1 = self.env['account.invoice'].create({ + 'name': 'Test invoice', + 'partner_id': self.customer12.id, + }) + self.inv_line1 = self.env['account.invoice.line'].create({ + 'invoice_id': self.invoice1.id, + 'name': "Line1", + 'product_id': self.product16.id, + 'product_uom_id': uom_unit.id, + 'account_id': self.invoice1.account_id.id, + 'price_unit': 42.0, + }) + self.inv_line2 = self.env['account.invoice.line'].create({ + 'invoice_id': self.invoice1.id, + 'name': "Line2", + 'product_id': self.product24.id, + 'product_uom_id': uom_unit.id, + 'account_id': self.invoice1.account_id.id, + 'price_unit': 1111.1, + }) + + self.aa1 = self.env.ref('analytic.analytic_partners_camp_to_camp') + self.aa2 = self.env.ref('analytic.analytic_nebula') + self.atag1 = self.env.ref('analytic.tag_contract') + self.atag2 = self.env['account.analytic.tag'].create({ + 'name': u'の', + }) + + def create_wizard(self): + UpdateWizard = self.env['account.invoice.update'].with_context( + active_model='account.invoice', + active_id=self.invoice1.id) + self.wiz = UpdateWizard.create({}) + + def test_add_analytic_account_line1(self): + """ Add analytic account on an invoice line + after the invoice has been approved. + + This will: + - update the move line + - create a new analytic line. + """ + self.invoice1.action_invoice_open() + self.create_wizard() + + wiz_line = self.wiz.line_ids.filtered( + lambda rec: rec.invoice_line_id == self.inv_line1) + wiz_line.account_analytic_id = self.aa1 + self.wiz.run() + + related_ml = self.invoice1.move_id.line_ids.filtered( + lambda rec: rec.product_id == self.product16) + self.assertEqual(related_ml.analytic_account_id, self.aa1) + self.assertEqual(related_ml.analytic_line_ids.account_id, self.aa1) + + def test_change_analytic_account_line1(self): + """ Change analytic account on an invoice line + after the invoice has been approved. + + This will: + - update the move line + - update the existing analytic line.""" + self.inv_line1.account_analytic_id = self.aa2 + + self.invoice1.action_invoice_open() + self.create_wizard() + + wiz_line = self.wiz.line_ids.filtered( + lambda rec: rec.invoice_line_id == self.inv_line1) + wiz_line.account_analytic_id = self.aa1 + self.wiz.run() + + related_ml = self.invoice1.move_id.line_ids.filtered( + lambda rec: rec.product_id == self.product16) + self.assertEqual(related_ml.analytic_account_id, self.aa1) + self.assertEqual(related_ml.analytic_line_ids.account_id, self.aa1) + + def test_error_grouped_move_lines(self): + """ Change analytic account on an invoice line + after the invoice has been approved where both + lines were grouped in the same move line. + + This will raise an error. + """ + self.invoice1.journal_id.group_invoice_lines = True + + self.inv_line2.product_id = self.product16 + self.inv_line2.unit_price = 42.0 + + self.invoice1.action_invoice_open() + self.create_wizard() + + line1 = self.wiz.line_ids[0] + line1.account_analytic_id = self.aa1 + with self.assertRaises(UserError): + self.wiz.run() + + def test_add_analytic_tags_line1(self): + """ Add analytic tags on an invoice line + after the invoice has been approved. + + This will update move line. + """ + self.invoice1.action_invoice_open() + self.create_wizard() + + wiz_line = self.wiz.line_ids.filtered( + lambda rec: rec.invoice_line_id == self.inv_line1) + wiz_line.analytic_tag_ids = self.atag2 + self.wiz.run() + + related_ml = self.invoice1.move_id.line_ids.filtered( + lambda rec: rec.product_id == self.product16) + self.assertEqual(related_ml.analytic_tag_ids, self.atag2) + self.assertFalse(related_ml.analytic_line_ids) + + def test_change_analytic_tags_line1(self): + """ Change analytic tags on an invoice line + after the invoice has been approved. + + It will update move line and analytic line + """ + self.inv_line1.account_analytic_id = self.aa2 + self.inv_line1.analytic_tag_ids = self.atag1 + + self.invoice1.action_invoice_open() + self.create_wizard() + + wiz_line = self.wiz.line_ids.filtered( + lambda rec: rec.invoice_line_id == self.inv_line1) + wiz_line.analytic_tag_ids = self.atag2 + self.wiz.run() + + related_ml = self.invoice1.move_id.line_ids.filtered( + lambda rec: rec.product_id == self.product16) + self.assertEqual(related_ml.analytic_tag_ids, self.atag2) + self.assertEqual(related_ml.analytic_line_ids.tag_ids, self.atag2) + + def test_add_analytic_info_line1(self): + """ Add analytic account and tags on an invoice line + after the invoice has been approved. + + This will: + - update move line + - create an analytic line + """ + self.invoice1.action_invoice_open() + self.create_wizard() + + wiz_line = self.wiz.line_ids.filtered( + lambda rec: rec.invoice_line_id == self.inv_line1) + wiz_line.account_analytic_id = self.aa1 + wiz_line.analytic_tag_ids = self.atag2 + self.wiz.run() + + related_ml = self.invoice1.move_id.line_ids.filtered( + lambda rec: rec.product_id == self.product16) + self.assertEqual(related_ml.analytic_account_id, self.aa1) + self.assertEqual(related_ml.analytic_tag_ids, self.atag2) + self.assertEqual(related_ml.analytic_line_ids.account_id, self.aa1) + self.assertEqual(related_ml.analytic_line_ids.tag_ids, self.atag2) + + def test_empty_analytic_account_line1(self): + """ Remove analytic account + after the invoice has been approved. + + This will raise an error as it is not implemented. + """ + self.inv_line1.account_analytic_id = self.aa2 + + self.invoice1.action_invoice_open() + self.create_wizard() + + wiz_line = self.wiz.line_ids.filtered( + lambda rec: rec.invoice_line_id == self.inv_line1) + wiz_line.account_analytic_id = False + self.wiz.run() + related_ml = self.invoice1.move_id.line_ids.filtered( + lambda rec: rec.product_id == self.product16) + self.assertFalse(related_ml.analytic_account_id) + self.assertFalse(related_ml.analytic_line_ids) diff --git a/account_invoice_update_wizard/wizard/account_invoice_update.py b/account_invoice_update_wizard/wizard/account_invoice_update.py index 0de0648..3cca0ac 100644 --- a/account_invoice_update_wizard/wizard/account_invoice_update.py +++ b/account_invoice_update_wizard/wizard/account_invoice_update.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # © 2017 Akretion (Alexis de Lattre ) +# Copyright 2018 Camptocamp # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import models, fields, api, _ @@ -48,11 +49,15 @@ class AccountInvoiceUpdate(models.TransientModel): for m2ofield in self._m2o_fields2update(): res[m2ofield] = invoice[m2ofield].id or False for line in invoice.invoice_line_ids: + aa_tags = line.analytic_tag_ids + aa_tags = [(6, 0, aa_tags.ids)] if aa_tags else False res['line_ids'].append([0, 0, { 'invoice_line_id': line.id, 'name': line.name, 'quantity': line.quantity, 'price_subtotal': line.price_subtotal, + 'account_analytic_id': line.account_analytic_id.id, + 'analytic_tag_ids': aa_tags, }]) return res @@ -110,6 +115,57 @@ class AccountInvoiceUpdate(models.TransientModel): mvals['ref'] = ref return mvals + @api.multi + def _get_matching_inv_line(self, move_line): + """ Find matching invoice line by product """ + # TODO make it accept more case as lines won't + # be grouped unless journal.group_invoice_line is True + inv_line = self.invoice_id.invoice_line_ids.filtered( + lambda rec: rec.product_id == move_line.product_id) + if len(inv_line) <> 1: + raise UserError( + "Cannot match a single invoice line to move line %s" % + move_line.name) + return inv_line + + @api.multi + def _prepare_move_line(self, inv_line): + mlvals = {} + inv_line_upd = self.line_ids.filtered( + lambda rec: rec.invoice_line_id == inv_line) + + ini_aa = inv_line.account_analytic_id + new_aa = inv_line_upd.account_analytic_id + + if ini_aa != new_aa: + mlvals['analytic_account_id'] = new_aa.id + + ini_aa_tags = inv_line.analytic_tag_ids + new_aa_tags = inv_line_upd.analytic_tag_ids + + if ini_aa_tags != new_aa_tags: + mlvals['analytic_tag_ids'] = [(6, None, new_aa_tags.ids)] + return mlvals + + @api.multi + def _prepare_analytic_line(self, inv_line): + alvals = {} + inv_line_upd = self.line_ids.filtered( + lambda rec: rec.invoice_line_id == inv_line) + + ini_aa = inv_line.account_analytic_id + new_aa = inv_line_upd.account_analytic_id + + if ini_aa != new_aa: + alvals['account_id'] = new_aa.id + + ini_aa_tags = inv_line.analytic_tag_ids + new_aa_tags = inv_line_upd.analytic_tag_ids + + if ini_aa_tags != new_aa_tags: + alvals['tag_ids'] = [(6, None, new_aa_tags.ids)] + return alvals + @api.multi def _update_payment_term_move(self): self.ensure_one() @@ -169,15 +225,40 @@ class AccountInvoiceUpdate(models.TransientModel): if ivals: updated = True inv.write(ivals) + if inv.move_id: + mvals = self._prepare_move() + if mvals: + inv.move_id.write(mvals) + for ml in inv.move_id.line_ids: + if ml.credit == 0.0: + continue + inv_line = self._get_matching_inv_line(ml) + mlvals = self._prepare_move_line(inv_line) + if mlvals: + updated = True + ml.write(mlvals) + aalines = ml.analytic_line_ids + alvals = self._prepare_analytic_line(inv_line) + if aalines and alvals: + updated = True + if ('account_id' in alvals and + alvals['account_id'] is False): + former_aa = inv_line.account_analytic_id + to_remove_aalines = aalines.filtered( + lambda rec: rec.account_id == former_aa) + # remove existing analytic line + to_remove_aalines.unlink() + else: + aalines.write(alvals) + elif 'account_id' in alvals: + # Create analytic lines if analytic account + # is added later + ml.create_analytic_lines() for line in self.line_ids: ilvals = self._prepare_invoice_line(line) if ilvals: updated = True line.invoice_line_id.write(ilvals) - if inv.move_id: - mvals = self._prepare_move() - if mvals: - inv.move_id.write(mvals) if updated: inv.message_post(_( 'Non-legal fields of invoice updated via the Invoice Update ' @@ -199,3 +280,7 @@ class AccountInvoiceLineUpdate(models.TransientModel): readonly=True) price_subtotal = fields.Float( string='Amount', readonly=True, digits=dp.get_precision('Account')) + account_analytic_id = fields.Many2one( + 'account.analytic.account', string='Analytic Account') + analytic_tag_ids = fields.Many2many( + 'account.analytic.tag', string='Analytic Tags') diff --git a/account_invoice_update_wizard/wizard/account_invoice_update_view.xml b/account_invoice_update_wizard/wizard/account_invoice_update_view.xml index 1eab7c7..a9f0824 100644 --- a/account_invoice_update_wizard/wizard/account_invoice_update_view.xml +++ b/account_invoice_update_wizard/wizard/account_invoice_update_view.xml @@ -30,6 +30,8 @@ + +