From 03745d5c4a8f4e160d31f31d38c6f1fefa9e3f67 Mon Sep 17 00:00:00 2001 From: Kitti U Date: Sun, 17 May 2020 21:37:40 +0700 Subject: [PATCH] [12.0][IMP] account_spread_cost_revenue, auto spread on validate invoice --- .../models/account_invoice.py | 5 + .../models/account_invoice_line.py | 41 ++++ .../models/account_spread_template.py | 76 +++++++- .../readme/CONFIGURE.rst | 2 + .../readme/HISTORY.rst | 5 + account_spread_cost_revenue/readme/USAGE.rst | 3 + .../security/account_spread_security.xml | 14 ++ .../security/ir.model.access.csv | 2 + account_spread_cost_revenue/tests/__init__.py | 1 + .../tests/test_account_invoice_auto_spread.py | 175 ++++++++++++++++++ .../views/account_spread_template.xml | 15 ++ 11 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 account_spread_cost_revenue/tests/test_account_invoice_auto_spread.py diff --git a/account_spread_cost_revenue/models/account_invoice.py b/account_spread_cost_revenue/models/account_invoice.py index 53bba642..7de31920 100644 --- a/account_spread_cost_revenue/models/account_invoice.py +++ b/account_spread_cost_revenue/models/account_invoice.py @@ -7,6 +7,11 @@ from odoo import api, models class AccountInvoice(models.Model): _inherit = 'account.invoice' + def action_invoice_open(self): + for invoice in self: + invoice.invoice_line_ids.create_auto_spread() + return super().action_invoice_open() + @api.multi def action_move_create(self): """Invoked when validating the invoices.""" diff --git a/account_spread_cost_revenue/models/account_invoice_line.py b/account_spread_cost_revenue/models/account_invoice_line.py index 43c9c62f..88fe9a64 100644 --- a/account_spread_cost_revenue/models/account_invoice_line.py +++ b/account_spread_cost_revenue/models/account_invoice_line.py @@ -2,6 +2,7 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import _, api, fields, models +from odoo.exceptions import UserError class AccountInvoiceLine(models.Model): @@ -65,3 +66,43 @@ class AccountInvoiceLine(models.Model): 'target': 'new', 'context': ctx, } + + def create_auto_spread(self): + """ Create auto spread table for each invoice line, when needed """ + + def _filter_line(aline, iline): + """ Find matching template auto line with invoice line """ + if aline.product_id and iline.product_id != aline.product_id: + return False + if aline.account_id and iline.account_id != aline.account_id: + return False + if aline.analytic_account_id and \ + iline.account_analytic_id != aline.analytic_account_id: + return False + return True + + for line in self: + if line.spread_check == 'linked': + continue + spread_type = ( + 'sale' if line.invoice_type in ['out_invoice', 'out_refund'] + else 'purchase') + spread_auto = self.env['account.spread.template.auto'].search( + [('template_id.auto_spread', '=', True), + ('template_id.spread_type', '=', spread_type)]) + matched = spread_auto.filtered(lambda a, i=line: _filter_line(a, i)) + template = matched.mapped('template_id') + if not template: + continue + elif len(template) > 1: + raise UserError( + _('Too many auto spread templates (%s) matched with the ' + 'invoice line, %s') % (len(template), line.display_name)) + # Found auto spread template for this invoice line, create it + wizard = self.env['account.spread.invoice.line.link.wizard'].new({ + 'invoice_line_id': line.id, + 'company_id': line.company_id.id, + 'spread_action_type': 'template', + 'template_id': template.id, + }) + wizard.confirm() diff --git a/account_spread_cost_revenue/models/account_spread_template.py b/account_spread_cost_revenue/models/account_spread_template.py index 2d946e72..943d4c35 100644 --- a/account_spread_cost_revenue/models/account_spread_template.py +++ b/account_spread_cost_revenue/models/account_spread_template.py @@ -1,7 +1,8 @@ # Copyright 2018-2019 Onestein () # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import api, fields, models +from odoo import api, fields, models, _ +from odoo.exceptions import UserError class AccountSpreadTemplate(models.Model): @@ -45,6 +46,16 @@ class AccountSpreadTemplate(models.Model): ('year', 'Year')], help="Period length for the entries") start_date = fields.Date() + auto_spread = fields.Boolean( + string='Auto assign template on invoice validate', + help="If checked, provide option to auto create spread during " + "invoice validation, based on product/account/analytic in invoice line." + ) + auto_spread_ids = fields.One2many( + comodel_name='account.spread.template.auto', + string='Auto Spread On', + inverse_name='template_id', + ) @api.model def default_get(self, fields): @@ -61,6 +72,14 @@ class AccountSpreadTemplate(models.Model): res['spread_journal_id'] = default_journal.id return res + @api.constrains('auto_spread', 'auto_spread_ids') + def _check_product_account(self): + for rec in self.filtered('auto_spread'): + for line in rec.auto_spread_ids: + if not line.product_id and not line.account_id: + raise UserError(_('Please select product and/or account ' + 'on auto spread options')) + @api.onchange('spread_type', 'company_id') def onchange_spread_type(self): company = self.company_id @@ -107,3 +126,58 @@ class AccountSpreadTemplate(models.Model): spread_vals['invoice_type'] = invoice_type return spread_vals + + @api.constrains('auto_spread_ids', 'auto_spread') + def _check_auto_spread_ids_unique(self): + query = """ + select product_id, account_id, analytic_account_id + from ( + select product_id, account_id, analytic_account_id, count(*) + from account_spread_template_auto a + join account_spread_template b on a.template_id = b.id + where b.auto_spread = true and b.id in %s + group by product_id, account_id, analytic_account_id + ) x where x.count > 1 """ + self._cr.execute(query, [self._ids]) + results = [] + for res in self._cr.fetchall(): + product = self.env['product.product'].browse(res[0]) + account = self.env['account.account'].browse(res[1]) + analytic = self.env['account.analytic.account'].browse(res[2]) + results.append('%s / %s / %s' % (product.name, account.name, analytic.name)) + if results: + raise UserError( + _('Followings are duplicated combinations,\n\n%s' % '\n'.join(results))) + + +class AccountSpreadTemplateAuto(models.Model): + _name = 'account.spread.template.auto' + _description = 'Auto create spread, based on product/account/analytic' + + template_id = fields.Many2one( + comodel_name='account.spread.template', + string='Spread Template', + required=True, + ondelete='cascade', + index=True, + ) + company_id = fields.Many2one( + related='template_id.company_id', + store=True, + ) + name = fields.Char( + required=True, + default='/', + ) + product_id = fields.Many2one( + comodel_name='product.product', + string='Product', + ) + account_id = fields.Many2one( + comodel_name='account.account', + string='Account', + ) + analytic_account_id = fields.Many2one( + comodel_name='account.analytic.account', + string='Analytic', + ) diff --git a/account_spread_cost_revenue/readme/CONFIGURE.rst b/account_spread_cost_revenue/readme/CONFIGURE.rst index 594bb201..6a5cca1d 100644 --- a/account_spread_cost_revenue/readme/CONFIGURE.rst +++ b/account_spread_cost_revenue/readme/CONFIGURE.rst @@ -15,6 +15,8 @@ This module by default allows the spreading even before the receipt of the invoi so that it is possible to work on the plan of the cost/revenue spreading. To disable this feature, on the form view of the company disable the *Allow Spread Planning* option. +In Spread Template, there is also option to *Auto assign template on invoice validate*, based on the preset invoice line criteria. + On the form view of the company, the *Auto-post spread lines* option forces the account moves created during the cost/revenue spreading to be automatically posted. When this option is false, the user can enable/disable the automatic posting by the flag *Auto-post lines* present in the spread board. diff --git a/account_spread_cost_revenue/readme/HISTORY.rst b/account_spread_cost_revenue/readme/HISTORY.rst index 99014e15..66ca64b7 100644 --- a/account_spread_cost_revenue/readme/HISTORY.rst +++ b/account_spread_cost_revenue/readme/HISTORY.rst @@ -1,3 +1,8 @@ +12.0.2.0.0 +~~~~~~~~~~ + +* [ENH] In spread template, add option to auto create spread on invoice validation + 12.0.1.1.0 ~~~~~~~~~~ diff --git a/account_spread_cost_revenue/readme/USAGE.rst b/account_spread_cost_revenue/readme/USAGE.rst index a514e602..74dbb1d3 100644 --- a/account_spread_cost_revenue/readme/USAGE.rst +++ b/account_spread_cost_revenue/readme/USAGE.rst @@ -71,6 +71,9 @@ Under Invoicing -> Configuration -> Accounting -> Spread Templates, create a new * *Spread Balance Sheet Account* * *Expense/Revenue Account* This option visible if invoice line account is balance sheet account, user need to specify this too. * *Journal* +* *Auto assign template on invoice validate* When creating a new Spread Costs/Revenues Board, select the right template. This way the above fields will be copied to the Spread Board. + +If *Auto assign template on invoice validate* is checked, this template will be used to auto create spread, if the underlining invoice match the preset product/account/analytic criteria. diff --git a/account_spread_cost_revenue/security/account_spread_security.xml b/account_spread_cost_revenue/security/account_spread_security.xml index 15e1b675..9090c609 100644 --- a/account_spread_cost_revenue/security/account_spread_security.xml +++ b/account_spread_cost_revenue/security/account_spread_security.xml @@ -9,5 +9,19 @@ ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + Account Spread Template multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + + Account Spread Tempalte Auto multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + diff --git a/account_spread_cost_revenue/security/ir.model.access.csv b/account_spread_cost_revenue/security/ir.model.access.csv index a4e7d04a..9e67a7ca 100644 --- a/account_spread_cost_revenue/security/ir.model.access.csv +++ b/account_spread_cost_revenue/security/ir.model.access.csv @@ -5,3 +5,5 @@ access_account_spread_cost_revenue_line_full,Full access on account.spread.line, access_account_spread_cost_revenue_line_read,Read access on account.spread.line,model_account_spread_line,account.group_account_user,1,0,0,0 access_account_spread_cost_revenue_template_full,Full access on account.spread.template,model_account_spread_template,account.group_account_manager,1,1,1,1 access_account_spread_cost_revenue_template_read,Read access on account.spread.template,model_account_spread_template,account.group_account_user,1,0,0,0 +access_account_spread_cost_revenue_template_auto_full,Full access on account.spread.template.auto,model_account_spread_template_auto,account.group_account_manager,1,1,1,1 +access_account_spread_cost_revenue_template_auto_read,Read access on account.spread.template.auto,model_account_spread_template_auto,account.group_account_user,1,0,0,0 diff --git a/account_spread_cost_revenue/tests/__init__.py b/account_spread_cost_revenue/tests/__init__.py index cbf5a95a..055f442a 100644 --- a/account_spread_cost_revenue/tests/__init__.py +++ b/account_spread_cost_revenue/tests/__init__.py @@ -3,3 +3,4 @@ from . import test_account_spread_cost_revenue from . import test_compute_spread_board from . import test_account_invoice_spread +from . import test_account_invoice_auto_spread diff --git a/account_spread_cost_revenue/tests/test_account_invoice_auto_spread.py b/account_spread_cost_revenue/tests/test_account_invoice_auto_spread.py new file mode 100644 index 00000000..94ad0494 --- /dev/null +++ b/account_spread_cost_revenue/tests/test_account_invoice_auto_spread.py @@ -0,0 +1,175 @@ +# Copyright 2018-2019 Onestein () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tools import convert_file +from odoo.modules.module import get_resource_path +from odoo.exceptions import UserError +from odoo.tests import common + + +class TestAccountInvoiceSpread(common.TransactionCase): + + def _load(self, module, *args): + convert_file( + self.cr, + 'account_spread_cost_revenue', + get_resource_path(module, *args), + {}, 'init', False, 'test', self.registry._assertion_report) + + def setUp(self): + super().setUp() + self._load('account', 'test', 'account_minimal_test.xml') + + type_receivable = self.env.ref('account.data_account_type_receivable') + type_payable = self.env.ref('account.data_account_type_payable') + type_revenue = self.env.ref('account.data_account_type_revenue') + + self.invoice_account = self.env['account.account'].create({ + 'name': 'test_account_receivable', + 'code': '123', + 'user_type_id': type_receivable.id, + 'reconcile': True + }) + + self.account_payable = self.env['account.account'].create({ + 'name': 'test_account_payable', + 'code': '321', + 'user_type_id': type_payable.id, + 'reconcile': True + }) + + self.account_revenue = self.env['account.account'].create({ + 'name': 'test_account_revenue', + 'code': '864', + 'user_type_id': type_revenue.id, + 'reconcile': True + }) + + self.invoice_line_account = self.account_payable + + self.spread_account = self.env['account.account'].create({ + 'name': 'test spread account_payable', + 'code': '765', + 'user_type_id': type_payable.id, + 'reconcile': True + }) + + partner = self.env['res.partner'].create({ + 'name': 'Partner Name', + 'supplier': True, + }) + + # Purchase Invoice + self.invoice = self.env['account.invoice'].create({ + 'partner_id': partner.id, + 'account_id': self.invoice_account.id, + 'type': 'in_invoice', + }) + self.invoice_line = self.env['account.invoice.line'].create({ + 'quantity': 1.0, + 'price_unit': 1000.0, + 'invoice_id': self.invoice.id, + 'name': 'product that cost 1000', + 'account_id': self.invoice_account.id, + }) + + # Sales Invoice + self.invoice_2 = self.env['account.invoice'].create({ + 'partner_id': partner.id, + 'account_id': self.invoice_account.id, + 'type': 'out_invoice', + }) + self.invoice_line_2 = self.env['account.invoice.line'].create({ + 'quantity': 1.0, + 'price_unit': 1000.0, + 'invoice_id': self.invoice_2.id, + 'name': 'product that cost 1000', + 'account_id': self.invoice_line_account.id, + }) + + def test_01_no_auto_spread_sheet(self): + + self.env['account.spread.template'].create({ + 'name': 'test', + 'spread_type': 'purchase', + 'period_number': 5, + 'period_type': 'month', + 'spread_account_id': self.account_payable.id, + 'spread_journal_id': self.ref( + 'account_spread_cost_revenue.expenses_journal'), + 'auto_spread': False, # Auto Spread = False + 'auto_spread_ids': [ + (0, 0, {'account_id': self.invoice_account.id})] + }) + + self.assertFalse(self.invoice_line.spread_id) + self.invoice.action_invoice_open() + self.assertFalse(self.invoice_line.spread_id) + + def test_02_new_auto_spread_sheet_purchase(self): + + self.env['account.spread.template'].create({ + 'name': 'test 1', + 'spread_type': 'purchase', + 'period_number': 5, + 'period_type': 'month', + 'spread_account_id': self.account_payable.id, + 'spread_journal_id': self.ref( + 'account_spread_cost_revenue.expenses_journal'), + 'auto_spread': True, # Auto Spread + 'auto_spread_ids': [ + (0, 0, {'account_id': self.invoice_account.id})] + }) + template2 = self.env['account.spread.template'].create({ + 'name': 'test 2', + 'spread_type': 'purchase', + 'period_number': 5, + 'period_type': 'month', + 'spread_account_id': self.account_payable.id, + 'spread_journal_id': self.ref( + 'account_spread_cost_revenue.expenses_journal'), + 'auto_spread': True, # Auto Spread + 'auto_spread_ids': [ + (0, 0, {'account_id': self.invoice_account.id})] + }) + template2._check_auto_spread_ids_unique() + + self.assertFalse(self.invoice_line.spread_id) + with self.assertRaises(UserError): # too many auto_spread_ids matched + self.invoice.action_invoice_open() + + template2.auto_spread = False # Do not use this template + self.invoice.action_invoice_open() + self.assertTrue(self.invoice_line.spread_id) + + spread_lines = self.invoice_line.spread_id.line_ids + self.assertTrue(spread_lines) + + for line in spread_lines: + line.create_move() + self.assertTrue(line.move_id) + + def test_03_new_auto_spread_sheet_sale(self): + + self.env['account.spread.template'].create({ + 'name': 'test', + 'spread_type': 'sale', + 'period_number': 5, + 'period_type': 'month', + 'spread_account_id': self.account_revenue.id, + 'spread_journal_id': self.ref( + 'account_spread_cost_revenue.sales_journal'), + 'auto_spread': True, # Auto Spread + 'auto_spread_ids': [(0, 0, {'account_id': self.invoice_line_account.id})] + }) + + self.assertFalse(self.invoice_line_2.spread_id) + self.invoice_2.action_invoice_open() + self.assertTrue(self.invoice_line_2.spread_id) + + spread_lines = self.invoice_line_2.spread_id.line_ids + self.assertTrue(spread_lines) + + for line in spread_lines: + line.create_move() + self.assertTrue(line.move_id) diff --git a/account_spread_cost_revenue/views/account_spread_template.xml b/account_spread_cost_revenue/views/account_spread_template.xml index 300d555b..3eebe57e 100644 --- a/account_spread_cost_revenue/views/account_spread_template.xml +++ b/account_spread_cost_revenue/views/account_spread_template.xml @@ -28,6 +28,21 @@ +
+ +
+

+ Automatically use this spread template on invoice validation for invoice lines using below product and/or account and/or analytic, +

+ + + + + + + +