diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py index 8ead6af8961..a1a7754a076 100644 --- a/addons/account/__manifest__.py +++ b/addons/account/__manifest__.py @@ -17,6 +17,7 @@ Core mechanisms for the accounting modules. To display the menuitems, install th 'security/ir.model.access.csv', 'data/data_account_type.xml', 'data/account_data.xml', + 'data/ir_cron_data.xml', 'views/account_menuitem.xml', 'views/account_payment_view.xml', 'wizard/account_reconcile_view.xml', diff --git a/addons/account/data/ir_cron_data.xml b/addons/account/data/ir_cron_data.xml new file mode 100644 index 00000000000..6e3ceb702aa --- /dev/null +++ b/addons/account/data/ir_cron_data.xml @@ -0,0 +1,14 @@ + + + + + Account: generate recurring vendor bills + + code + False + model._cron_generate_recurring_vendor_bill() + 1 + days + -1 + + diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 8e4e3c95d14..e4bd400016c 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -229,6 +229,12 @@ class AccountInvoice(models.Model): payment_lines.update(line.mapped('matched_debit_ids.debit_move_id.id')) self.payment_move_line_ids = self.env['account.move.line'].browse(list(payment_lines)) + @api.depends('date_invoice', 'is_recurrency_enabled', 'recurrency_interval', 'recurrency_type') + def _compute_next_recurrency_date(self): + #initially set the recurrency_date + for invoice in self.filtered(lambda inv: inv.is_recurrency_enabled and inv.recurrency_interval and inv.recurrency_type and inv.date_invoice and not inv.next_recurrency_date): + invoice.next_recurrency_date = invoice._get_next_recurrency_date(invoice.date_invoice) + name = fields.Char(string='Reference/Description', index=True, readonly=True, states={'draft': [('readonly', False)]}, copy=False, help='The name that will be used on account move lines') @@ -367,6 +373,18 @@ class AccountInvoice(models.Model): #fields use to set the sequence, on the first invoice of the journal sequence_number_next = fields.Char(string='Next Number', compute="_get_sequence_number_next", inverse="_set_sequence_next") sequence_number_next_prefix = fields.Char(string='Next Number Prefix', compute="_get_sequence_prefix") + #recurrent vendor bills fields + is_recurrency_enabled = fields.Boolean(string="Generate Recurring Document(s)", copy=False, help="Tick this box to generate recurring bills. The recurring bills will only be generated if this bill is validated.") + recurrency_interval = fields.Integer(string='Every', default=1, copy=False, help="Define the recurrency of the vendor bill.") + recurrency_type = fields.Selection([ + ('days', 'Day(s)'), + ('weeks', 'Week(s)'), + ('months', 'Month(s)'), + ('years', 'Year(s)'), + ], string="Recurrency Type", default='months', copy=False) + # Technical field depicting whether the document is generated by cron or by user + is_recurring_document = fields.Boolean(string="Recurring Document", copy=False) + next_recurrency_date = fields.Date(compute=_compute_next_recurrency_date, _string='Next Recurrency Date', copy=False, store=True) _sql_constraints = [ ('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'), @@ -450,6 +468,52 @@ class AccountInvoice(models.Model): self.type == 'in_refund' and self.state == 'draft' and _('Vendor Credit Note') or \ self.type == 'in_refund' and _('Vendor Credit Note - %s') % (self.number) + def _get_next_recurrency_date(self, current_date): + """ + This method is used to return next date based on recurrency configuration of the record. + + :param current_date: Origin date from which next date is to be generated. + :returns: future date on which a duplicated document will be generated by crons. + """ + assert self.recurrency_type, "Invalid `Recurrency` interval! Please set it to either of these: 'Day(s)', 'Week(s)', 'Month(s)' or 'Year(s)'." + self.ensure_one() + recurring_next_date = fields.Date.from_string(current_date) + relativedelta(**{self.recurrency_type: self.recurrency_interval}) + # In case of monthly recurrent invoice, future invoices should always be created on same day as of it's main invoice but + # if the reference invoice was made on last day of the month, then the recurring invoice should be created on last day of next month. + # example, main invoice is set to 31st Jan, the next invoices should be created on 28/29th Feb, 31st Mar, 30 Apr and so on + if self.recurrency_type == 'months': + reference_invoice_day = fields.Date.from_string(self.date_invoice).day + recurring_next_date += relativedelta(day=reference_invoice_day) + if reference_invoice_day == fields.Date.from_string(self.date_invoice) + relativedelta(day=31): + recurring_next_date += relativedelta(day=31) + return recurring_next_date + + @api.model + def _cron_generate_recurring_vendor_bill(self): + today = fields.Date.today() + domain = [ + ('is_recurrency_enabled', '=', True), + ('state', 'not in', ['draft', 'cancel']), + ('next_recurrency_date', '<=', today) + ] + for invoice in self.search(domain): + while invoice.next_recurrency_date <= today: + invoice._create_recurrent_invoice() + invoice.next_recurrency_date = invoice._get_next_recurrency_date(invoice.next_recurrency_date) + + def _create_recurrent_invoice(self): + self.ensure_one() + invoice = self.copy({ + 'origin': '', + 'date_invoice': self.next_recurrency_date, + 'is_recurring_document': True, + }) + # set the due date + #invoice._onchange_payment_term_date_invoice() + link = '%s' % (self.id, self.number) + invoice.message_post(body=_('This bill has been generated by %s') % link) + return invoice + @api.model def create(self, vals): onchanges = { diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py index 8d562328c4f..d7a37b62a76 100644 --- a/addons/account/models/res_config_settings.py +++ b/addons/account/models/res_config_settings.py @@ -56,10 +56,12 @@ class ResConfigSettings(models.TransientModel): tax_exigibility = fields.Boolean(string='Cash Basis', related='company_id.tax_exigibility') tax_cash_basis_journal_id = fields.Many2one('account.journal', related='company_id.tax_cash_basis_journal_id', string="Tax Cash Basis Journal") account_hide_setup_bar = fields.Boolean(string='Hide Setup Bar', related='company_id.account_setup_bar_closed',help="Tick if you wish to hide the setup bar on the dashboard") + group_allow_recurring = fields.Boolean(string="Recurring Vendor Bills", implied_group="account.group_allow_recurring") @api.multi def set_values(self): super(ResConfigSettings, self).set_values() + self.env.ref('account.recurrent_vendor_bills_cron').write({'active': self.group_allow_recurring}) if self.group_multi_currency: self.env.ref('base.group_user').write({'implied_ids': [(4, self.env.ref('product.group_sale_pricelist').id)]}) """ install a chart of accounts for the given company (if required) """ diff --git a/addons/account/security/account_security.xml b/addons/account/security/account_security.xml index c527729765c..1bcf45e5121 100644 --- a/addons/account/security/account_security.xml +++ b/addons/account/security/account_security.xml @@ -32,6 +32,11 @@ + + Allow to create recurring vendor bills + + + Allow the cash rounding management diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index 8d6fb294413..02b00337259 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -6,6 +6,7 @@ from . import test_account_move_closed_period from . import test_account_supplier_invoice from . import test_account_validate_account_move from . import test_account_invoice_rounding +from . import test_account_supplier_invoice_recurrent from . import test_bank_statement_reconciliation #TODO re-enableand fix this test #from . import test_bank_stmt_reconciliation_widget_ui diff --git a/addons/account/tests/test_account_supplier_invoice_recurrent.py b/addons/account/tests/test_account_supplier_invoice_recurrent.py new file mode 100644 index 00000000000..47f681dd3cb --- /dev/null +++ b/addons/account/tests/test_account_supplier_invoice_recurrent.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +from dateutil.relativedelta import relativedelta + +from odoo.addons.account.tests.account_test_classes import AccountingTestCase +from odoo.tests import tagged + + +@tagged('post_install', '-at_install') +class TestAccountRecurrent(AccountingTestCase): + + def apply_vendor_cron(self): + self.env.ref('account.recurrent_vendor_bills_cron').method_direct_trigger() + + def test_create_recurring_vendor_bills(self): + AccountInvoice = self.env['account.invoice'] + invoice_account_id = self.env['account.account'].search([('user_type_id', '=', self.ref('account.data_account_type_receivable'))], limit=1).id + invoice_line_account_id = self.env['account.account'].search([('user_type_id', '=', self.ref('account.data_account_type_expenses'))], limit=1).id + purchase_journal = self.env['account.journal'].search([('type', '=', 'purchase')], limit=1) + + invoice_line_data = [ + (0, 0, + { + 'product_id': self.ref('product.product_product_4'), + 'name': 'product 4 that cost 100', + 'quantity': 1.0, + 'price_unit': 100.0, + 'account_id': invoice_line_account_id, + } + ), + (0, 0, + { + 'product_id': self.ref('product.product_product_5'), + 'name': 'product 5 that cost 200', + 'quantity': 2.0, + 'price_unit': 200.0, + 'account_id': invoice_line_account_id, + } + ) + ] + + # Create an invoice dated 2 months before today + invoice = AccountInvoice.create({ + 'partner_id': self.ref('base.res_partner_2'), + 'account_id': invoice_account_id, + 'name': 'invoice test recurrent', + 'type': 'in_invoice', + 'reference_type': 'none', + 'date_invoice': datetime.today() + relativedelta(months=-2), + 'journal_id': purchase_journal.id, + 'is_recurrency_enabled': True, + 'recurrency_interval': 1, + 'recurrency_type': 'months', + 'invoice_line_ids': invoice_line_data + }) + recurring_domain = [('type', '=', 'in_invoice'), ('state', '=', 'draft'), ('is_recurring_document', '=', True)] + + # After executing cron, verify that no bill is auto generated if the recurrent bill is in `draft` state + self.apply_vendor_cron() + recurring_invoice_count = AccountInvoice.search_count(recurring_domain) + self.assertEquals(recurring_invoice_count, 0, 'Recurring bills should not be generated when reference bill is still `draft`.') + + # After validating bill, run cron and check that 2 recurring bill have been generated + invoice.action_invoice_open() # Validate invoice + self.apply_vendor_cron() + recurring_invoice_count = previous_recurring_invoice_count = AccountInvoice.search_count(recurring_domain) + self.assertEquals(recurring_invoice_count, 2, '2 recurring bills should be generated.') + + # verify that bills aren't generated if reference bill is in `cancel` state + purchase_journal.write({'update_posted': True}) # Allow to cancel the invoice + inv2_data = { + 'name': 'invoice test recurrent 2', + 'date_invoice': datetime.today() + relativedelta(months=-2), + 'is_recurrency_enabled': True, + 'recurrency_interval': 1, + 'recurrency_type': 'months', + } + invoice2 = invoice.copy(default=inv2_data) + invoice2.action_invoice_cancel() # Cancel the invoice + self.apply_vendor_cron() + recurring_invoice_count = AccountInvoice.search_count(recurring_domain) + self.assertEquals(previous_recurring_invoice_count, recurring_invoice_count, 'Recurring bills should not be generated when reference bill is in `cancel` state.') + + # verify that vendor bills are generated even when invoice is in `paid` state + inv3_data = { + 'name': 'invoice test recurrent 3', + 'date_invoice': datetime.today() + relativedelta(months=-2), + 'is_recurrency_enabled': True, + 'recurrency_interval': 1, + 'recurrency_type': 'months', + } + + invoice3 = invoice.copy(default=inv3_data) + invoice3.action_invoice_open() # Validate invoice + Payment = self.env['account.payment'] + payment_vals = Payment.with_context(default_invoice_ids=[(4, invoice3.id, None)]).default_get(Payment._fields.keys()) + payment_vals.update({ + 'payment_method_id': self.ref("account.account_payment_method_manual_out"), + 'journal_id': self.env['account.journal'].search([('type', '=', 'bank')], limit=1).id + }) + Payment.create(payment_vals).action_validate_invoice_payment() # Register the payment + self.apply_vendor_cron() + recurring_invoice_count = AccountInvoice.search_count(recurring_domain) + self.assertEquals(recurring_invoice_count, 4, '4 recurring bills should be generated.') diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index 7c8e8aafb92..a2b4a4de431 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -334,6 +334,12 @@ + + + + + + @@ -574,6 +580,10 @@ Not Draft + + + + diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml index 75566cc7ae6..605d7e70ec9 100644 --- a/addons/account/views/res_config_settings_views.xml +++ b/addons/account/views/res_config_settings_views.xml @@ -434,9 +434,20 @@ - Automated Entries - - + Automated Entries + + + + + + + + + Create recurring vendor bills + + + + @@ -452,7 +463,7 @@ - +