From aefa12ab9e8632b7404546a2950dbadfbeb1bad2 Mon Sep 17 00:00:00 2001 From: sbejaoui Date: Thu, 29 Nov 2018 16:57:51 +0100 Subject: [PATCH] [IMP] - store last_date_invoiced on contract_line Improve CRITERIA_ALLOWED_DICT [IMP] - code improvement [IMP] - Use last_date_invoiced to set marker in invoice description [IMP] - add migration script to init last_day_invoiced and some other improvement [FIX] - a contract line suspended should start a day after the suspension end --- contract/__manifest__.py | 2 +- contract/data/__init__.py | 1 - contract/data/contract_line_constraints.py | 281 ----------- .../migrations/12.0.2.0.0/post-migration.py | 15 +- .../migrations/12.0.2.0.0/pre-migration.py | 24 + contract/models/abstract_contract.py | 6 +- contract/models/abstract_contract_line.py | 8 +- contract/models/contract.py | 8 +- contract/models/contract_line.py | 445 +++++++++++------- contract/models/contract_line_constraints.py | 428 +++++++++++++++++ contract/tests/test_contract.py | 309 +++++++++--- contract/views/contract_line.xml | 2 + 12 files changed, 1010 insertions(+), 519 deletions(-) delete mode 100644 contract/data/__init__.py delete mode 100644 contract/data/contract_line_constraints.py create mode 100644 contract/migrations/12.0.2.0.0/pre-migration.py create mode 100644 contract/models/contract_line_constraints.py diff --git a/contract/__manifest__.py b/contract/__manifest__.py index 17c66376..10d14ddc 100644 --- a/contract/__manifest__.py +++ b/contract/__manifest__.py @@ -9,7 +9,7 @@ { 'name': 'Recurring - Contracts Management', - 'version': '12.0.2.0.0', + 'version': '12.0.2.0.1', 'category': 'Contract Management', 'license': 'AGPL-3', 'author': "OpenERP SA, " diff --git a/contract/data/__init__.py b/contract/data/__init__.py deleted file mode 100644 index 8cbf3fc0..00000000 --- a/contract/data/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import contract_line_constraints diff --git a/contract/data/contract_line_constraints.py b/contract/data/contract_line_constraints.py deleted file mode 100644 index 94b5c8b9..00000000 --- a/contract/data/contract_line_constraints.py +++ /dev/null @@ -1,281 +0,0 @@ -# Copyright 2018 ACSONE SA/NV. -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from collections import namedtuple -from odoo.fields import Date - -CRITERIA = namedtuple( - 'CRITERIA', - [ - 'WHEN', - 'HAS_DATE_END', - 'IS_AUTO_RENEW', - 'HAS_SUCCESSOR', - 'PREDECESSOR_HAS_SUCCESSOR', - 'CANCELED', - ], -) -ALLOWED = namedtuple( - 'ALLOWED', - ['PLAN_SUCCESSOR', 'STOP_PLAN_SUCCESSOR', 'STOP', 'CANCEL', 'UN_CANCEL'], -) - -CRITERIA_ALLOWED_DICT = { - CRITERIA( - WHEN='BEFORE', - HAS_DATE_END=True, - IS_AUTO_RENEW=True, - HAS_SUCCESSOR=False, - PREDECESSOR_HAS_SUCCESSOR=None, - CANCELED=False, - ): ALLOWED( - PLAN_SUCCESSOR=False, - STOP_PLAN_SUCCESSOR=True, - STOP=True, - CANCEL=True, - UN_CANCEL=False, - ), - CRITERIA( - WHEN='BEFORE', - HAS_DATE_END=True, - IS_AUTO_RENEW=False, - HAS_SUCCESSOR=True, - PREDECESSOR_HAS_SUCCESSOR=None, - CANCELED=False, - ): ALLOWED( - PLAN_SUCCESSOR=False, - STOP_PLAN_SUCCESSOR=False, - STOP=True, - CANCEL=True, - UN_CANCEL=False, - ), - CRITERIA( - WHEN='BEFORE', - HAS_DATE_END=True, - IS_AUTO_RENEW=False, - HAS_SUCCESSOR=False, - PREDECESSOR_HAS_SUCCESSOR=None, - CANCELED=False, - ): ALLOWED( - PLAN_SUCCESSOR=True, - STOP_PLAN_SUCCESSOR=True, - STOP=True, - CANCEL=True, - UN_CANCEL=False, - ), - CRITERIA( - WHEN='BEFORE', - HAS_DATE_END=False, - IS_AUTO_RENEW=False, - HAS_SUCCESSOR=False, - PREDECESSOR_HAS_SUCCESSOR=None, - CANCELED=False, - ): ALLOWED( - PLAN_SUCCESSOR=False, - STOP_PLAN_SUCCESSOR=True, - STOP=True, - CANCEL=True, - UN_CANCEL=False, - ), - CRITERIA( - WHEN='IN', - HAS_DATE_END=True, - IS_AUTO_RENEW=True, - HAS_SUCCESSOR=False, - PREDECESSOR_HAS_SUCCESSOR=None, - CANCELED=False, - ): ALLOWED( - PLAN_SUCCESSOR=False, - STOP_PLAN_SUCCESSOR=True, - STOP=True, - CANCEL=True, - UN_CANCEL=False, - ), - CRITERIA( - WHEN='IN', - HAS_DATE_END=True, - IS_AUTO_RENEW=False, - HAS_SUCCESSOR=True, - PREDECESSOR_HAS_SUCCESSOR=None, - CANCELED=False, - ): ALLOWED( - PLAN_SUCCESSOR=False, - STOP_PLAN_SUCCESSOR=False, - STOP=True, - CANCEL=True, - UN_CANCEL=False, - ), - CRITERIA( - WHEN='IN', - HAS_DATE_END=True, - IS_AUTO_RENEW=False, - HAS_SUCCESSOR=False, - PREDECESSOR_HAS_SUCCESSOR=None, - CANCELED=False, - ): ALLOWED( - PLAN_SUCCESSOR=True, - STOP_PLAN_SUCCESSOR=True, - STOP=True, - CANCEL=True, - UN_CANCEL=False, - ), - CRITERIA( - WHEN='IN', - HAS_DATE_END=False, - IS_AUTO_RENEW=False, - HAS_SUCCESSOR=False, - PREDECESSOR_HAS_SUCCESSOR=None, - CANCELED=False, - ): ALLOWED( - PLAN_SUCCESSOR=False, - STOP_PLAN_SUCCESSOR=True, - STOP=True, - CANCEL=True, - UN_CANCEL=False, - ), - CRITERIA( - WHEN='AFTER', - HAS_DATE_END=True, - IS_AUTO_RENEW=True, - HAS_SUCCESSOR=False, - PREDECESSOR_HAS_SUCCESSOR=None, - CANCELED=False, - ): ALLOWED( - PLAN_SUCCESSOR=False, - STOP_PLAN_SUCCESSOR=False, - STOP=False, - CANCEL=False, - UN_CANCEL=False, - ), - CRITERIA( - WHEN='AFTER', - HAS_DATE_END=True, - IS_AUTO_RENEW=False, - HAS_SUCCESSOR=True, - PREDECESSOR_HAS_SUCCESSOR=None, - CANCELED=False, - ): ALLOWED( - PLAN_SUCCESSOR=False, - STOP_PLAN_SUCCESSOR=False, - STOP=False, - CANCEL=False, - UN_CANCEL=False, - ), - CRITERIA( - WHEN='AFTER', - HAS_DATE_END=True, - IS_AUTO_RENEW=False, - HAS_SUCCESSOR=False, - PREDECESSOR_HAS_SUCCESSOR=None, - CANCELED=False, - ): ALLOWED( - PLAN_SUCCESSOR=True, - STOP_PLAN_SUCCESSOR=False, - STOP=False, - CANCEL=False, - UN_CANCEL=False, - ), - CRITERIA( - WHEN=None, - HAS_DATE_END=None, - IS_AUTO_RENEW=None, - HAS_SUCCESSOR=None, - PREDECESSOR_HAS_SUCCESSOR=False, - CANCELED=True, - ): ALLOWED( - PLAN_SUCCESSOR=False, - STOP_PLAN_SUCCESSOR=False, - STOP=False, - CANCEL=False, - UN_CANCEL=True, - ), - CRITERIA( - WHEN=None, - HAS_DATE_END=None, - IS_AUTO_RENEW=None, - HAS_SUCCESSOR=None, - PREDECESSOR_HAS_SUCCESSOR=True, - CANCELED=True, - ): ALLOWED( - PLAN_SUCCESSOR=False, - STOP_PLAN_SUCCESSOR=False, - STOP=False, - CANCEL=False, - UN_CANCEL=False, - ), -} - - -def compute_when(date_start, date_end): - today = Date.today() - if today < date_start: - return 'BEFORE' - if date_end and today > date_end: - return 'AFTER' - return 'IN' - - -def compute_criteria( - date_start, - date_end, - is_auto_renew, - successor_contract_line_id, - predecessor_contract_line_id, - is_canceled, -): - if is_canceled: - if ( - not predecessor_contract_line_id - or not predecessor_contract_line_id.successor_contract_line_id - ): - return CRITERIA( - WHEN=None, - HAS_DATE_END=None, - IS_AUTO_RENEW=None, - HAS_SUCCESSOR=None, - PREDECESSOR_HAS_SUCCESSOR=False, - CANCELED=True, - ) - else: - return CRITERIA( - WHEN=None, - HAS_DATE_END=None, - IS_AUTO_RENEW=None, - HAS_SUCCESSOR=None, - PREDECESSOR_HAS_SUCCESSOR=True, - CANCELED=True, - ) - when = compute_when(date_start, date_end) - has_date_end = date_end if not date_end else True - is_auto_renew = is_auto_renew - has_successor = True if successor_contract_line_id else False - canceled = is_canceled - return CRITERIA( - WHEN=when, - HAS_DATE_END=has_date_end, - IS_AUTO_RENEW=is_auto_renew, - HAS_SUCCESSOR=has_successor, - PREDECESSOR_HAS_SUCCESSOR=None, - CANCELED=canceled, - ) - - -def get_allowed( - date_start, - date_end, - is_auto_renew, - successor_contract_line_id, - predecessor_contract_line_id, - is_canceled, -): - criteria = compute_criteria( - date_start, - date_end, - is_auto_renew, - successor_contract_line_id, - predecessor_contract_line_id, - is_canceled, - ) - if criteria in CRITERIA_ALLOWED_DICT: - return CRITERIA_ALLOWED_DICT[criteria] - return False diff --git a/contract/migrations/12.0.2.0.0/post-migration.py b/contract/migrations/12.0.2.0.0/post-migration.py index a197d9b7..8720ef4a 100644 --- a/contract/migrations/12.0.2.0.0/post-migration.py +++ b/contract/migrations/12.0.2.0.0/post-migration.py @@ -1,10 +1,16 @@ # -*- coding: utf-8 -*- # Copyright 2018 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging +from odoo import SUPERUSER_ID, api + + +_logger = logging.getLogger(__name__) def migrate(cr, version): - """Copy recurrence info from contract to contract lines.""" + """Copy recurrence info from contract to contract lines and compute + last_date_invoiced""" cr.execute( """UPDATE account_analytic_invoice_line AS contract_line @@ -17,3 +23,10 @@ def migrate(cr, version): FROM account_analytic_account AS contract WHERE contract.id=contract_line.contract_id""" ) + + _logger.info("order all contract line") + env = api.Environment(cr, SUPERUSER_ID, {}) + contract_lines = env["account.analytic.invoice.line"].search( + [("recurring_next_date", "!=", False)] + ) + contract_lines._init_last_date_invoiced() diff --git a/contract/migrations/12.0.2.0.0/pre-migration.py b/contract/migrations/12.0.2.0.0/pre-migration.py new file mode 100644 index 00000000..e8f42894 --- /dev/null +++ b/contract/migrations/12.0.2.0.0/pre-migration.py @@ -0,0 +1,24 @@ +# Copyright 2018 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import SUPERUSER_ID, api + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + """ + set recurring_next_date to false for finished contract + """ + _logger.info("order all contract line") + with api.Environment(cr, SUPERUSER_ID, {}) as env: + contracts = env["account.analytic.account"].search([]) + finished_contract = contracts.filtered( + lambda c: not c.create_invoice_visibility + ) + cr.execute( + "UPDATE account_analytic_account set recurring_next_date=null where id in (%)" + % ','.join(finished_contract.ids) + ) diff --git a/contract/models/abstract_contract.py b/contract/models/abstract_contract.py index 06dca173..1f69daa2 100644 --- a/contract/models/abstract_contract.py +++ b/contract/models/abstract_contract.py @@ -11,7 +11,7 @@ from odoo import api, models, fields class AbstractAccountAnalyticContract(models.AbstractModel): _name = 'account.abstract.analytic.contract' - _description = 'Abstract Account Analytic Contract' + _description = 'Abstract Recurring Contract' # These fields will not be synced to the contract NO_SYNC = ['name', 'partner_id'] @@ -40,7 +40,9 @@ class AbstractAccountAnalyticContract(models.AbstractModel): 'res.company', string='Company', required=True, - default=lambda self: self.env.user.company_id, + default=lambda self: self.env['res.company']._company_default_get( + self._name + ), ) @api.onchange('contract_type') diff --git a/contract/models/abstract_contract_line.py b/contract/models/abstract_contract_line.py index e4438305..4dc9c8e4 100644 --- a/contract/models/abstract_contract_line.py +++ b/contract/models/abstract_contract_line.py @@ -14,7 +14,7 @@ from odoo.tools.translate import _ class AccountAbstractAnalyticContractLine(models.AbstractModel): _name = 'account.abstract.analytic.contract.line' - _description = 'Account Abstract Analytic Contract Line' + _description = 'Abstract Recurring Contract Line' product_id = fields.Many2one( 'product.product', string='Product', required=True @@ -130,7 +130,9 @@ class AccountAbstractAnalyticContractLine(models.AbstractModel): ), pricelist=line.contract_id.pricelist_id.id, partner=line.contract_id.partner_id.id, - date=line.env.context.get('old_date', fields.Date.today()), + date=line.env.context.get( + 'old_date', fields.Date.context_today(line) + ), ) line.price_unit = product.price else: @@ -182,7 +184,7 @@ class AccountAbstractAnalyticContractLine(models.AbstractModel): ): vals['uom_id'] = self.product_id.uom_id - date = self.recurring_next_date or fields.Date.today() + date = self.recurring_next_date or fields.Date.context_today(self) partner = self.contract_id.partner_id or self.env.user.partner_id product = self.product_id.with_context( diff --git a/contract/models/contract.py b/contract/models/contract.py index be422d25..ee144b67 100644 --- a/contract/models/contract.py +++ b/contract/models/contract.py @@ -62,7 +62,7 @@ class AccountAnalyticAccount(models.Model): recurring_next_date = contract.recurring_invoice_line_ids.filtered( 'create_invoice_visibility' ).mapped('recurring_next_date') - if recurring_next_date: + if recurring_next_date and all(recurring_next_date): contract.recurring_next_date = min(recurring_next_date) @api.depends('recurring_invoice_line_ids.create_invoice_visibility') @@ -123,8 +123,10 @@ class AccountAnalyticAccount(models.Model): vals = contract_line._convert_to_write(contract_line.read()[0]) # Remove template link field vals.pop('contract_template_id', False) - vals['date_start'] = fields.Date.today() - vals['recurring_next_date'] = fields.Date.today() + vals['date_start'] = fields.Date.context_today(contract_line) + vals['recurring_next_date'] = fields.Date.context_today( + contract_line + ) self.recurring_invoice_line_ids._onchange_date_start() new_lines.append((0, 0, vals)) return new_lines diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index 6d4cb7ed..3494b0b2 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -7,7 +7,7 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ from odoo.exceptions import ValidationError -from ..data.contract_line_constraints import get_allowed +from .contract_line_constraints import get_allowed class AccountAnalyticInvoiceLine(models.Model): @@ -16,14 +16,21 @@ class AccountAnalyticInvoiceLine(models.Model): contract_id = fields.Many2one( comodel_name='account.analytic.account', - string='Analytic Account', + string='Contract', required=True, + index=True, ondelete='cascade', oldname='analytic_account_id', ) - date_start = fields.Date(string='Date Start', default=fields.Date.today()) + date_start = fields.Date( + string='Date Start', + default=lambda self: fields.Date.context_today(self), + ) date_end = fields.Date(string='Date End', index=True) recurring_next_date = fields.Date(string='Date of Next Invoice') + last_date_invoiced = fields.Date( + string='Last Date Invoiced', readonly=True + ) create_invoice_visibility = fields.Boolean( compute='_compute_create_invoice_visibility' ) @@ -33,7 +40,8 @@ class AccountAnalyticInvoiceLine(models.Model): required=False, readonly=True, copy=False, - help="Contract Line created by this one.", + help="In case of restart after suspension, this field contain the new " + "contract line created.", ) predecessor_contract_line_id = fields.Many2one( comodel_name='account.analytic.invoice.line', @@ -72,7 +80,7 @@ class AccountAnalyticInvoiceLine(models.Model): @api.multi def _compute_state(self): - today = fields.Date.today() + today = fields.Date.context_today(self) for rec in self: if rec.is_canceled: rec.state = 'canceled' @@ -90,6 +98,7 @@ class AccountAnalyticInvoiceLine(models.Model): @api.depends( 'date_start', 'date_end', + 'last_date_invoiced', 'is_auto_renew', 'successor_contract_line_id', 'predecessor_contract_line_id', @@ -101,19 +110,20 @@ class AccountAnalyticInvoiceLine(models.Model): allowed = get_allowed( rec.date_start, rec.date_end, + rec.last_date_invoiced, rec.is_auto_renew, rec.successor_contract_line_id, rec.predecessor_contract_line_id, rec.is_canceled, ) if allowed: - rec.is_plan_successor_allowed = allowed.PLAN_SUCCESSOR + rec.is_plan_successor_allowed = allowed.plan_successor rec.is_stop_plan_successor_allowed = ( - allowed.STOP_PLAN_SUCCESSOR + allowed.stop_plan_successor ) - rec.is_stop_allowed = allowed.STOP - rec.is_cancel_allowed = allowed.CANCEL - rec.is_un_cancel_allowed = allowed.UN_CANCEL + rec.is_stop_allowed = allowed.stop + rec.is_cancel_allowed = allowed.cancel + rec.is_un_cancel_allowed = allowed.uncancel @api.constrains('is_auto_renew', 'successor_contract_line_id', 'date_end') def _check_allowed(self): @@ -135,14 +145,14 @@ class AccountAnalyticInvoiceLine(models.Model): ) if not rec.date_end: raise ValidationError( - _("An auto-renew line should have a " "date end ") + _("An auto-renew line must have a end date") ) else: if not rec.date_end and rec.successor_contract_line_id: raise ValidationError( _( "A contract line with a successor " - "should have date end" + "must have a end date" ) ) @@ -150,7 +160,7 @@ class AccountAnalyticInvoiceLine(models.Model): def _check_overlap_successor(self): for rec in self: if rec.date_end and rec.successor_contract_line_id: - if rec.date_end > rec.successor_contract_line_id.date_start: + if rec.date_end >= rec.successor_contract_line_id.date_start: raise ValidationError( _("Contract line and its successor overlapped") ) @@ -159,7 +169,7 @@ class AccountAnalyticInvoiceLine(models.Model): def _check_overlap_predecessor(self): for rec in self: if rec.predecessor_contract_line_id: - if rec.date_start < rec.predecessor_contract_line_id.date_end: + if rec.date_start <= rec.predecessor_contract_line_id.date_end: raise ValidationError( _("Contract line and its predecessor overlapped") ) @@ -183,7 +193,10 @@ class AccountAnalyticInvoiceLine(models.Model): ) @api.onchange( - 'is_auto_renew', 'auto_renew_rule_type', 'auto_renew_interval' + 'date_start', + 'is_auto_renew', + 'auto_renew_rule_type', + 'auto_renew_interval', ) def _onchange_is_auto_renew(self): """Date end should be auto-computed if a contract line is set to @@ -219,22 +232,45 @@ class AccountAnalyticInvoiceLine(models.Model): if line.date_start > line.recurring_next_date: raise ValidationError( _( - "You can't have a next invoicing date before the " - "start of the contract '%s'" + "You can't have a date of next invoice anterior " + "to the start of the contract line '%s'" ) - % line.contract_id.name + % line.name ) + @api.constrains('date_start', 'date_end', 'last_date_invoiced') + def _check_last_date_invoiced(self): + for rec in self.filtered('last_date_invoiced'): + if rec.date_start and rec.date_start > rec.last_date_invoiced: + raise ValidationError( + _( + "You can't have the start date after the date of last " + "invoice for the contract line '%s'" + ) + % rec.name + ) + if rec.date_end and rec.date_end < rec.last_date_invoiced: + raise ValidationError( + _( + "You can't have the end date before the date of last " + "invoice for the contract line '%s'" + ) + % rec.name + ) + @api.constrains('recurring_next_date') def _check_recurring_next_date_recurring_invoices(self): - for line in self.filtered('contract_id.recurring_invoices'): - if not line.recurring_next_date: + for rec in self.filtered('contract_id.recurring_invoices'): + if not rec.recurring_next_date and ( + not rec.last_date_invoiced + or rec.last_date_invoiced < rec.date_end + ): raise ValidationError( _( - "You must supply a next invoicing date for contract " - "'%s'" + "You must supply a date of next invoice for contract " + "line '%s'" ) - % line.contract_id.name + % rec.name ) @api.constrains('date_start') @@ -242,8 +278,8 @@ class AccountAnalyticInvoiceLine(models.Model): for line in self.filtered('contract_id.recurring_invoices'): if not line.date_start: raise ValidationError( - _("You must supply a start date for contract '%s'") - % line.contract_id.name + _("You must supply a start date for contract line '%s'") + % line.name ) @api.constrains('date_start', 'date_end') @@ -253,39 +289,28 @@ class AccountAnalyticInvoiceLine(models.Model): if line.date_start > line.date_end: raise ValidationError( _( - "Contract '%s' start date can't be later than " - "end date" + "Contract line '%s' start date can't be later than" + " end date" ) - % line.contract_id.name + % line.name ) @api.depends('recurring_next_date', 'date_start', 'date_end') def _compute_create_invoice_visibility(self): - today = fields.Date.today() - for line in self: - if line.date_start: - if today < line.date_start: - line.create_invoice_visibility = False - elif not line.date_end: - line.create_invoice_visibility = True - elif line.recurring_next_date: - if line.recurring_invoicing_type == 'pre-paid': - line.create_invoice_visibility = ( - line.recurring_next_date <= line.date_end - ) - else: - line.create_invoice_visibility = ( - line.recurring_next_date - - line.get_relative_delta( - line.recurring_rule_type, - line.recurring_interval, - ) - ) <= line.date_end + today = fields.Date.context_today(self) + for rec in self: + if rec.date_start: + if today < rec.date_start: + rec.create_invoice_visibility = False + else: + rec.create_invoice_visibility = bool( + rec.recurring_next_date + ) @api.model - def recurring_create_invoice(self, contract=False): + def _get_recurring_create_invoice_domain(self, contract=False): domain = [] - date_ref = fields.Date.today() + date_ref = fields.Date.context_today(self) if contract: contract.ensure_one() date_ref = contract.recurring_next_date @@ -296,39 +321,36 @@ class AccountAnalyticInvoiceLine(models.Model): ('contract_id.recurring_invoices', '=', True), ('recurring_next_date', '<=', date_ref), ('is_canceled', '=', False), - # '|', - # ('date_end', '=', False), - # ('date_end', '>=', date_ref), - # with this leaf, it's impossible to invoice the last period - # in post-paid case. - # i.e: date_end = 15/03 recurring_next_date = 31/03 - # A solution for this, is to only check recurring_next_date - # and filter with create_invoice_visibility ] ) - lines = self.search(domain).filtered('create_invoice_visibility') - if lines: - return lines._recurring_create_invoice() - return False + return domain - @api.multi - def _recurring_create_invoice(self): + @api.model + def recurring_create_invoice(self, contract=False): + domain = self._get_recurring_create_invoice_domain(contract) + contract_to_invoice = self.read_group( + domain, ['id', 'contract_id'], ['contract_id'] + ) + return self._recurring_create_invoice(contract_to_invoice) + + @api.model + def _recurring_create_invoice(self, contract_to_invoice): """Create invoices from contracts :return: invoices created """ invoices = self.env['account.invoice'] - for contract in self.mapped('contract_id'): - lines = self.filtered(lambda l: l.contract_id == contract) - invoices |= lines._create_invoice() - lines._update_recurring_next_date() + for contract in contract_to_invoice: + lines = self.search(contract['__domain']) + if lines: + invoices |= lines._create_invoice() + lines._update_recurring_next_date() return invoices @api.multi def _create_invoice(self): """ - :param invoice: If not False add lines to this invoice - :return: invoice created or updated + :return: invoice created """ contract = self.mapped('contract_id') date_invoice = min(self.mapped('recurring_next_date')) @@ -359,10 +381,8 @@ class AccountAnalyticInvoiceLine(models.Model): invoice_line_vals = invoice_line._convert_to_write(invoice_line._cache) # Insert markers contract = self.contract_id - lang_obj = self.env['res.lang'] - lang = lang_obj.search([('code', '=', contract.partner_id.lang)]) - date_format = lang.date_format or '%m/%d/%Y' - name = self._insert_markers(date_format) + first_date_invoiced, last_date_invoiced = self._get_invoiced_period() + name = self._insert_markers(first_date_invoiced, last_date_invoiced) invoice_line_vals.update( { 'name': name, @@ -373,26 +393,98 @@ class AccountAnalyticInvoiceLine(models.Model): return invoice_line_vals @api.multi - def _insert_markers(self, date_format): + def _get_invoiced_period(self): self.ensure_one() - date_from = fields.Date.from_string(self.recurring_next_date) - date_to = date_from + self.get_relative_delta( - self.recurring_rule_type, self.recurring_interval + first_date_invoiced = ( + self.last_date_invoiced + relativedelta(days=1) + if self.last_date_invoiced + else self.date_start ) + if self.recurring_rule_type == 'monthlylastday': + last_date_invoiced = first_date_invoiced + self.get_relative_delta( + self.recurring_rule_type, self.recurring_interval - 1 + ) + else: + last_date_invoiced = ( + first_date_invoiced + + self.get_relative_delta( + self.recurring_rule_type, self.recurring_interval + ) + - relativedelta(days=1) + ) + if self.date_end and self.date_end < last_date_invoiced: + last_date_invoiced = self.date_end + return first_date_invoiced, last_date_invoiced + + @api.multi + def _insert_markers(self, first_date_invoiced, last_date_invoiced): + self.ensure_one() + lang_obj = self.env['res.lang'] + lang = lang_obj.search( + [('code', '=', self.contract_id.partner_id.lang)] + ) + date_format = lang.date_format or '%m/%d/%Y' name = self.name - name = name.replace('#START#', date_from.strftime(date_format)) - name = name.replace('#END#', date_to.strftime(date_format)) + name = name.replace( + '#START#', first_date_invoiced.strftime(date_format) + ) + name = name.replace('#END#', last_date_invoiced.strftime(date_format)) return name @api.multi def _update_recurring_next_date(self): - for line in self: - ref_date = line.recurring_next_date or fields.Date.today() - old_date = fields.Date.from_string(ref_date) + for rec in self: + old_date = rec.recurring_next_date new_date = old_date + self.get_relative_delta( - line.recurring_rule_type, line.recurring_interval + rec.recurring_rule_type, rec.recurring_interval ) - line.recurring_next_date = new_date + + if rec.recurring_rule_type == 'monthlylastday': + rec.last_date_invoiced = ( + old_date + if rec.date_end and old_date < rec.date_end + else rec.date_end + ) + elif rec.recurring_invoicing_type == 'post-paid': + rec.last_date_invoiced = ( + old_date - relativedelta(days=1) + if rec.date_end and old_date < rec.date_end + else rec.date_end + ) + elif rec.recurring_invoicing_type == 'pre-paid': + rec.last_date_invoiced = ( + new_date - relativedelta(days=1) + if rec.date_end and new_date < rec.date_end + else rec.date_end + ) + if ( + rec.last_date_invoiced + and rec.last_date_invoiced == rec.date_end + ): + rec.recurring_next_date = False + else: + rec.recurring_next_date = new_date + + @api.multi + def _init_last_date_invoiced(self): + """Used to init last_date_invoiced for migration purpose""" + for rec in self: + if rec.recurring_rule_type == 'monthlylastday': + last_date_invoiced = ( + rec.recurring_next_date + - self.get_relative_delta( + rec.recurring_rule_type, rec.recurring_interval + ) + ) + elif rec.recurring_invoicing_type == 'post-paid': + last_date_invoiced = ( + rec.recurring_next_date + - self.get_relative_delta( + rec.recurring_rule_type, rec.recurring_interval + ) + ) - relativedelta(days=1) + if last_date_invoiced > rec.date_start: + rec.last_date_invoiced = last_date_invoiced @api.model def get_relative_delta(self, recurring_rule_type, interval): @@ -408,13 +500,20 @@ class AccountAnalyticInvoiceLine(models.Model): return relativedelta(years=interval) @api.multi - def delay(self, delay_delta): + def _delay(self, delay_delta): """ Delay a contract line :param delay_delta: delay relative delta :return: delayed contract line """ for rec in self: + if rec.last_date_invoiced: + raise ValidationError( + _( + "You can't delay a contract line " + "invoiced at least one time." + ) + ) old_date_start = rec.date_start old_date_end = rec.date_end new_date_start = rec.date_start + delay_delta @@ -424,30 +523,12 @@ class AccountAnalyticInvoiceLine(models.Model): rec.recurring_rule_type, rec.recurring_interval, ) - rec.date_end = ( - rec.date_end - if not rec.date_end - else rec.date_end + delay_delta - ) + if rec.date_end: + rec.date_end += delay_delta rec.date_start = new_date_start - msg = _( - """Contract line for {product} - delayed:
- - Start: {old_date_start} -- {new_date_start} -
- - End: {old_date_end} -- {new_date_end} - """.format( - product=rec.name, - old_date_start=old_date_start, - new_date_start=rec.date_start, - old_date_end=old_date_end, - new_date_end=rec.date_end, - ) - ) - rec.contract_id.message_post(body=msg) @api.multi - def stop(self, date_end): + def stop(self, date_end, post_message=True): """ Put date_end on contract line We don't consider contract lines that end's before the new end date @@ -460,25 +541,23 @@ class AccountAnalyticInvoiceLine(models.Model): if date_end < rec.date_start: rec.cancel() else: - old_date_end = rec.date_end - date_end = ( - rec.date_end - if rec.date_end and rec.date_end < date_end - else date_end - ) - rec.write({'date_end': date_end, 'is_auto_renew': False}) - - msg = _( - """Contract line for {product} - stopped:
- - End: {old_date_end} -- {new_date_end} - """.format( - product=rec.name, - old_date_end=old_date_end, - new_date_end=rec.date_end, - ) - ) - rec.contract_id.message_post(body=msg) + if not rec.date_end or rec.date_end > date_end: + if post_message: + old_date_end = rec.date_end + msg = _( + """Contract line for {product} + stopped:
+ - End: {old_end} -- {new_end} + """.format( + product=rec.name, + old_end=old_date_end, + new_end=rec.date_end, + ) + ) + rec.contract_id.message_post(body=msg) + rec.write({'date_end': date_end, 'is_auto_renew': False}) + else: + rec.write({'is_auto_renew': False}) return True @api.multi @@ -505,7 +584,12 @@ class AccountAnalyticInvoiceLine(models.Model): @api.multi def plan_successor( - self, date_start, date_end, is_auto_renew, recurring_next_date=False + self, + date_start, + date_end, + is_auto_renew, + recurring_next_date=False, + post_message=True, ): """ Create a copy of a contract line in a new interval @@ -530,20 +614,20 @@ class AccountAnalyticInvoiceLine(models.Model): ) rec.successor_contract_line_id = new_line contract_line |= new_line - - msg = _( - """Contract line for {product} - planned a successor:
- - Start: {new_date_start} -
- - End: {new_date_end} - """.format( - product=rec.name, - new_date_start=new_line.date_start, - new_date_end=new_line.date_end, + if post_message: + msg = _( + """Contract line for {product} + planned a successor:
+ - Start: {new_date_start} +
+ - End: {new_date_end} + """.format( + product=rec.name, + new_date_start=new_line.date_start, + new_date_end=new_line.date_end, + ) ) - ) - rec.contract_id.message_post(body=msg) + rec.contract_id.message_post(body=msg) return contract_line @api.multi @@ -590,34 +674,64 @@ class AccountAnalyticInvoiceLine(models.Model): delay = (date_end - rec.date_start) + timedelta(days=1) else: delay = (date_end - date_start) + timedelta(days=1) - rec.delay(delay) + rec._delay(delay) contract_line |= rec else: if rec.date_end and rec.date_end < date_start: - rec.stop(date_start) + rec.stop(date_start, post_message=False) elif ( rec.date_end and rec.date_end > date_start and rec.date_end < date_end ): - new_date_start = date_end - new_date_end = date_end + (rec.date_end - date_start) - rec.stop(date_start) + new_date_start = date_end + relativedelta(days=1) + new_date_end = ( + date_end + + (rec.date_end - date_start) + + relativedelta(days=1) + ) + rec.stop( + date_start - relativedelta(days=1), post_message=False + ) contract_line |= rec.plan_successor( - new_date_start, new_date_end, is_auto_renew + new_date_start, + new_date_end, + is_auto_renew, + post_message=False, ) else: - new_date_start = date_end - new_date_end = ( - rec.date_end - if not rec.date_end - else rec.date_end + (date_end - date_start) + new_date_start = date_end + relativedelta(days=1) + if rec.date_end: + new_date_end = ( + rec.date_end + + (date_end - date_start) + + relativedelta(days=1) + ) + else: + new_date_end = rec.date_end + + rec.stop( + date_start - relativedelta(days=1), post_message=False ) - rec.stop(date_start) contract_line |= rec.plan_successor( - new_date_start, new_date_end, is_auto_renew + new_date_start, + new_date_end, + is_auto_renew, + post_message=False, ) - + msg = _( + """Contract line for {product} + suspended:
+ - Suspension Start: {new_date_start} +
+ - Suspension End: {new_date_end} + """.format( + product=rec.name, + new_date_start=date_start, + new_date_end=date_end, + ) + ) + rec.contract_id.message_post(body=msg) return contract_line @api.multi @@ -671,7 +785,7 @@ class AccountAnalyticInvoiceLine(models.Model): self.ensure_one() context = { 'default_contract_line_id': self.id, - 'default_recurring_next_date': fields.Date.today(), + 'default_recurring_next_date': fields.Date.context_today(self), } context.update(self.env.context) view_id = self.env.ref( @@ -772,16 +886,31 @@ class AccountAnalyticInvoiceLine(models.Model): res = self.env['account.analytic.invoice.line'] for rec in self: is_auto_renew = rec.is_auto_renew - rec.stop(rec.date_end) + rec.stop(rec.date_end, post_message=False) date_start, date_end = rec._get_renewal_dates() - new_line = rec.plan_successor(date_start, date_end, is_auto_renew) + new_line = rec.plan_successor( + date_start, date_end, is_auto_renew, post_message=False + ) new_line._onchange_date_start() res |= new_line + msg = _( + """Contract line for {product} + renewed:
+ - Start: {new_date_start} +
+ - End: {new_date_end} + """.format( + product=rec.name, + new_date_start=date_start, + new_date_end=date_end, + ) + ) + rec.contract_id.message_post(body=msg) return res @api.model def _contract_line_to_renew_domain(self): - date_ref = fields.datetime.today() + self.get_relative_delta( + date_ref = fields.Date.context_today(self) + self.get_relative_delta( self.termination_notice_rule_type, self.termination_notice_interval ) return [ diff --git a/contract/models/contract_line_constraints.py b/contract/models/contract_line_constraints.py new file mode 100644 index 00000000..1995c726 --- /dev/null +++ b/contract/models/contract_line_constraints.py @@ -0,0 +1,428 @@ +# Copyright 2018 ACSONE SA/NV. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import itertools +from collections import namedtuple +from odoo.fields import Date + +Criteria = namedtuple( + 'Criteria', + [ + 'when', # Contract line relatively to today (BEFORE, IN, AFTER) + 'has_date_end', # Is date_end set on contract line (bool) + 'has_last_date_invoiced', # Is last_date_invoiced set on contract line + 'is_auto_renew', # Is is_auto_renew set on contract line (bool) + 'has_successor', # Is contract line has_successor (bool) + 'predecessor_has_successor', + # Is contract line predecessor has successor (bool) + # In almost of the cases + # contract_line.predecessor.successor == contract_line + # But at cancel action, + # contract_line.predecessor.successor == False + # This is to permit plan_successor on predecessor + # If contract_line.predecessor.successor != False + # and contract_line is canceled, we don't allow uncancel + # else we re-link contract_line and its predecessor + 'canceled', # Is contract line canceled (bool) + ], +) +Allowed = namedtuple( + 'Allowed', + ['plan_successor', 'stop_plan_successor', 'stop', 'cancel', 'uncancel'], +) + + +def _expand_none(criteria): + variations = [] + for attribute, value in criteria._asdict().items(): + if value is None: + if attribute == 'when': + variations.append(['BEFORE', 'IN', 'AFTER']) + else: + variations.append([True, False]) + else: + variations.append([value]) + return itertools.product(*variations) + + +def _add(matrix, criteria, allowed): + """ Expand None values to True/False combination """ + for c in _expand_none(criteria): + matrix[c] = allowed + + +CRITERIA_ALLOWED_DICT = { + Criteria( + when='BEFORE', + has_date_end=True, + has_last_date_invoiced=False, + is_auto_renew=True, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=True, + stop=True, + cancel=True, + uncancel=False, + ), + Criteria( + when='BEFORE', + has_date_end=True, + has_last_date_invoiced=False, + is_auto_renew=False, + has_successor=True, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=False, + stop=True, + cancel=True, + uncancel=False, + ), + Criteria( + when='BEFORE', + has_date_end=True, + has_last_date_invoiced=False, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=True, + stop_plan_successor=True, + stop=True, + cancel=True, + uncancel=False, + ), + Criteria( + when='BEFORE', + has_date_end=False, + has_last_date_invoiced=False, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=True, + stop=True, + cancel=True, + uncancel=False, + ), + Criteria( + when='IN', + has_date_end=True, + has_last_date_invoiced=False, + is_auto_renew=True, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=True, + stop=True, + cancel=True, + uncancel=False, + ), + Criteria( + when='IN', + has_date_end=True, + has_last_date_invoiced=False, + is_auto_renew=False, + has_successor=True, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=False, + stop=True, + cancel=True, + uncancel=False, + ), + Criteria( + when='IN', + has_date_end=True, + has_last_date_invoiced=False, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=True, + stop_plan_successor=True, + stop=True, + cancel=True, + uncancel=False, + ), + Criteria( + when='IN', + has_date_end=False, + has_last_date_invoiced=False, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=True, + stop=True, + cancel=True, + uncancel=False, + ), + Criteria( + when='BEFORE', + has_date_end=True, + has_last_date_invoiced=True, + is_auto_renew=True, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=True, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when='BEFORE', + has_date_end=True, + has_last_date_invoiced=True, + is_auto_renew=False, + has_successor=True, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=False, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when='BEFORE', + has_date_end=True, + has_last_date_invoiced=True, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=True, + stop_plan_successor=True, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when='BEFORE', + has_date_end=False, + has_last_date_invoiced=True, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=True, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when='IN', + has_date_end=True, + has_last_date_invoiced=True, + is_auto_renew=True, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=True, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when='IN', + has_date_end=True, + has_last_date_invoiced=True, + is_auto_renew=False, + has_successor=True, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=False, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when='IN', + has_date_end=True, + has_last_date_invoiced=True, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=True, + stop_plan_successor=True, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when='IN', + has_date_end=False, + has_last_date_invoiced=True, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=True, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when='AFTER', + has_date_end=True, + has_last_date_invoiced=None, + is_auto_renew=True, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=False, + stop=False, + cancel=False, + uncancel=False, + ), + Criteria( + when='AFTER', + has_date_end=True, + has_last_date_invoiced=None, + is_auto_renew=False, + has_successor=True, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=False, + stop=False, + cancel=False, + uncancel=False, + ), + Criteria( + when='AFTER', + has_date_end=True, + has_last_date_invoiced=None, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=True, + stop_plan_successor=False, + stop=False, + cancel=False, + uncancel=False, + ), + Criteria( + when=None, + has_date_end=None, + has_last_date_invoiced=None, + is_auto_renew=None, + has_successor=None, + predecessor_has_successor=False, + canceled=True, + ): Allowed( + plan_successor=False, + stop_plan_successor=False, + stop=False, + cancel=False, + uncancel=True, + ), + Criteria( + when=None, + has_date_end=None, + has_last_date_invoiced=None, + is_auto_renew=None, + has_successor=None, + predecessor_has_successor=True, + canceled=True, + ): Allowed( + plan_successor=False, + stop_plan_successor=False, + stop=False, + cancel=False, + uncancel=False, + ), +} +criteria_allowed_dict = {} + +for c in CRITERIA_ALLOWED_DICT: + _add(criteria_allowed_dict, c, CRITERIA_ALLOWED_DICT[c]) + + +def compute_when(date_start, date_end): + today = Date.today() + if today < date_start: + return 'BEFORE' + if date_end and today > date_end: + return 'AFTER' + return 'IN' + + +def compute_criteria( + date_start, + date_end, + has_last_date_invoiced, + is_auto_renew, + successor_contract_line_id, + predecessor_contract_line_id, + is_canceled, +): + return Criteria( + when=compute_when(date_start, date_end), + has_date_end=bool(date_end), + has_last_date_invoiced=bool(has_last_date_invoiced), + is_auto_renew=is_auto_renew, + has_successor=bool(successor_contract_line_id), + predecessor_has_successor=bool( + predecessor_contract_line_id.successor_contract_line_id + ), + canceled=is_canceled, + ) + + +def get_allowed( + date_start, + date_end, + has_last_date_invoiced, + is_auto_renew, + successor_contract_line_id, + predecessor_contract_line_id, + is_canceled, +): + criteria = compute_criteria( + date_start, + date_end, + has_last_date_invoiced, + is_auto_renew, + successor_contract_line_id, + predecessor_contract_line_id, + is_canceled, + ) + if criteria in criteria_allowed_dict: + return criteria_allowed_dict[criteria] + return False diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index 23b444e9..57abfd75 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -161,6 +161,7 @@ class TestContract(TestContractBase): def test_contract_daily(self): recurring_next_date = to_date('2018-02-23') + last_date_invoiced = to_date('2018-02-22') self.acct_line.recurring_next_date = '2018-02-22' self.acct_line.recurring_rule_type = 'daily' self.contract.pricelist_id = False @@ -172,9 +173,11 @@ class TestContract(TestContractBase): self.assertEqual( self.acct_line.recurring_next_date, recurring_next_date ) + self.assertEqual(self.acct_line.last_date_invoiced, last_date_invoiced) - def test_contract_weekly(self): + def test_contract_weekly_post_paid(self): recurring_next_date = to_date('2018-03-01') + last_date_invoiced = to_date('2018-02-21') self.acct_line.recurring_next_date = '2018-02-22' self.acct_line.recurring_rule_type = 'weekly' self.acct_line.recurring_invoicing_type = 'post-paid' @@ -186,11 +189,47 @@ class TestContract(TestContractBase): self.assertEqual( self.acct_line.recurring_next_date, recurring_next_date ) + self.assertEqual(self.acct_line.last_date_invoiced, last_date_invoiced) - def test_contract_yearly(self): + def test_contract_weekly_pre_paid(self): + recurring_next_date = to_date('2018-03-01') + last_date_invoiced = to_date('2018-02-28') + self.acct_line.recurring_next_date = '2018-02-22' + self.acct_line.recurring_rule_type = 'weekly' + self.acct_line.recurring_invoicing_type = 'pre-paid' + self.contract.recurring_create_invoice() + invoices_weekly = self.env['account.invoice'].search( + [('contract_id', '=', self.contract.id)] + ) + self.assertTrue(invoices_weekly) + self.assertEqual( + self.acct_line.recurring_next_date, recurring_next_date + ) + self.assertEqual(self.acct_line.last_date_invoiced, last_date_invoiced) + + def test_contract_yearly_post_paid(self): + recurring_next_date = to_date('2019-02-22') + last_date_invoiced = to_date('2018-02-21') + self.acct_line.recurring_next_date = '2018-02-22' + self.acct_line.recurring_rule_type = 'yearly' + self.acct_line.recurring_invoicing_type = 'post-paid' + self.contract.recurring_create_invoice() + invoices_weekly = self.env['account.invoice'].search( + [('contract_id', '=', self.contract.id)] + ) + self.assertTrue(invoices_weekly) + self.assertEqual( + self.acct_line.recurring_next_date, recurring_next_date + ) + self.assertEqual(self.acct_line.last_date_invoiced, last_date_invoiced) + + def test_contract_yearly_pre_paid(self): recurring_next_date = to_date('2019-02-22') + last_date_invoiced = to_date('2019-02-21') + self.acct_line.date_end = '2020-02-22' self.acct_line.recurring_next_date = '2018-02-22' self.acct_line.recurring_rule_type = 'yearly' + self.acct_line.recurring_invoicing_type = 'pre-paid' self.contract.recurring_create_invoice() invoices_weekly = self.env['account.invoice'].search( [('contract_id', '=', self.contract.id)] @@ -199,9 +238,11 @@ class TestContract(TestContractBase): self.assertEqual( self.acct_line.recurring_next_date, recurring_next_date ) + self.assertEqual(self.acct_line.last_date_invoiced, last_date_invoiced) def test_contract_monthly_lastday(self): recurring_next_date = to_date('2018-03-31') + last_date_invoiced = to_date('2018-02-22') self.acct_line.recurring_next_date = '2018-02-22' self.acct_line.recurring_invoicing_type = 'post-paid' self.acct_line.recurring_rule_type = 'monthlylastday' @@ -213,21 +254,41 @@ class TestContract(TestContractBase): self.assertEqual( self.acct_line.recurring_next_date, recurring_next_date ) + self.assertEqual(self.acct_line.last_date_invoiced, last_date_invoiced) def test_last_invoice_post_paid(self): - recurring_next_date = to_date('2018-04-30') - self.acct_line.recurring_next_date = '2018-03-31' - self.acct_line.date_end = '2018-03-15' + self.acct_line.date_start = '2018-01-01' self.acct_line.recurring_invoicing_type = 'post-paid' + self.acct_line.date_end = '2018-03-15' + self.acct_line._onchange_date_start() + self.assertTrue(self.acct_line.create_invoice_visibility) + self.assertEqual( + self.acct_line.recurring_next_date, to_date('2018-02-01') + ) + self.assertFalse(self.acct_line.last_date_invoiced) self.contract.recurring_create_invoice() - invoices = self.env['account.invoice'].search( - [('contract_id', '=', self.contract.id)] + self.assertEqual( + self.acct_line.recurring_next_date, to_date('2018-03-01') ) - self.assertTrue(invoices) self.assertEqual( - self.acct_line.recurring_next_date, recurring_next_date + self.acct_line.last_date_invoiced, to_date('2018-01-31') + ) + self.contract.recurring_create_invoice() + self.assertEqual( + self.acct_line.recurring_next_date, to_date('2018-04-01') + ) + self.assertEqual( + self.acct_line.last_date_invoiced, to_date('2018-02-28') + ) + self.contract.recurring_create_invoice() + self.assertEqual( + self.acct_line.last_date_invoiced, to_date('2018-03-15') ) + self.assertFalse(self.acct_line.recurring_next_date) self.assertFalse(self.acct_line.create_invoice_visibility) + invoices = self.env['account.invoice'].search( + [('contract_id', '=', self.contract.id)] + ) self.contract.recurring_create_invoice() new_invoices = self.env['account.invoice'].search( [('contract_id', '=', self.contract.id)] @@ -239,19 +300,38 @@ class TestContract(TestContractBase): ) def test_last_invoice_pre_paid(self): - recurring_next_date = to_date('2018-04-01') - self.acct_line.recurring_next_date = '2018-03-01' - self.acct_line.date_end = '2018-03-15' + self.acct_line.date_start = '2018-01-01' self.acct_line.recurring_invoicing_type = 'pre-paid' + self.acct_line.date_end = '2018-03-15' + self.acct_line._onchange_date_start() + self.assertTrue(self.acct_line.create_invoice_visibility) + self.assertEqual( + self.acct_line.recurring_next_date, to_date('2018-01-01') + ) + self.assertFalse(self.acct_line.last_date_invoiced) self.contract.recurring_create_invoice() - invoices = self.env['account.invoice'].search( - [('contract_id', '=', self.contract.id)] + self.assertEqual( + self.acct_line.recurring_next_date, to_date('2018-02-01') ) - self.assertTrue(invoices) self.assertEqual( - self.acct_line.recurring_next_date, recurring_next_date + self.acct_line.last_date_invoiced, to_date('2018-01-31') + ) + self.contract.recurring_create_invoice() + self.assertEqual( + self.acct_line.last_date_invoiced, to_date('2018-02-28') + ) + self.assertEqual( + self.acct_line.last_date_invoiced, to_date('2018-02-28') ) + self.contract.recurring_create_invoice() + self.assertEqual( + self.acct_line.last_date_invoiced, to_date('2018-03-15') + ) + self.assertFalse(self.acct_line.recurring_next_date) self.assertFalse(self.acct_line.create_invoice_visibility) + invoices = self.env['account.invoice'].search( + [('contract_id', '=', self.contract.id)] + ) self.contract.recurring_create_invoice() new_invoices = self.env['account.invoice'].search( [('contract_id', '=', self.contract.id)] @@ -454,48 +534,6 @@ class TestContract(TestContractBase): ) self.assertEqual(last_count, init_count + 1) - def test_compute_create_invoice_visibility(self): - self.acct_line.write( - { - 'recurring_next_date': '2018-01-15', - 'date_start': '2018-01-01', - 'is_auto_renew': False, - 'date_end': False, - } - ) - self.assertTrue(self.contract.create_invoice_visibility) - self.acct_line.date_end = '2018-02-01' - self.contract.refresh() - self.assertTrue(self.contract.create_invoice_visibility) - self.acct_line.date_end = '2018-01-01' - self.contract.refresh() - self.assertFalse(self.contract.create_invoice_visibility) - - def test_compute_create_invoice_visibility_for_contract_line(self): - self.acct_line.write( - { - 'recurring_next_date': '2018-01-15', - 'date_start': '2018-01-01', - 'is_auto_renew': False, - 'date_end': False, - } - ) - self.assertTrue(self.acct_line.create_invoice_visibility) - self.acct_line.date_end = '2018-02-01' - self.assertTrue(self.acct_line.create_invoice_visibility) - self.acct_line.date_end = '2018-01-01' - self.assertFalse(self.acct_line.create_invoice_visibility) - self.acct_line.write( - { - 'date_start': fields.Date.today() + relativedelta(months=2), - 'recurring_next_date': fields.Date.today() - + relativedelta(months=2), - 'is_auto_renew': False, - 'date_end': False, - } - ) - self.assertFalse(self.acct_line.create_invoice_visibility) - def test_act_show_contract(self): show_contract = self.partner.with_context( contract_type='sale' @@ -608,6 +646,10 @@ class TestContract(TestContractBase): self.acct_line.write({'date_end': False, 'is_auto_renew': False}) self.assertFalse(self.contract.date_end) + def test_last_date_invoiced_prepaid(self): + self.contract.recurring_create_invoice() + self + def test_stop_contract_line(self): """It should put end to the contract line""" self.acct_line.write( @@ -735,13 +777,21 @@ class TestContract(TestContractBase): self.acct_line.stop_plan_successor( suspension_start, suspension_end, True ) - self.assertEqual(self.acct_line.date_end, suspension_start) + self.assertEqual( + self.acct_line.date_end, suspension_start - relativedelta(days=1) + ) new_line = self.env['account.analytic.invoice.line'].search( [('predecessor_contract_line_id', '=', self.acct_line.id)] ) self.assertTrue(new_line) - new_date_end = suspension_end + (end_date - suspension_start) - self.assertEqual(new_line.date_start, suspension_end) + new_date_end = ( + suspension_end + + (end_date - suspension_start) + + relativedelta(days=1) + ) + self.assertEqual( + new_line.date_start, suspension_end + relativedelta(days=1) + ) self.assertEqual(new_line.date_end, new_date_end) def test_stop_plan_successor_contract_line_3(self): @@ -767,13 +817,21 @@ class TestContract(TestContractBase): self.acct_line.stop_plan_successor( suspension_start, suspension_end, True ) - self.assertEqual(self.acct_line.date_end, suspension_start) + self.assertEqual( + self.acct_line.date_end, suspension_start - relativedelta(days=1) + ) new_line = self.env['account.analytic.invoice.line'].search( [('predecessor_contract_line_id', '=', self.acct_line.id)] ) self.assertTrue(new_line) - new_date_end = end_date + (suspension_end - suspension_start) - self.assertEqual(new_line.date_start, suspension_end) + new_date_end = ( + end_date + + (suspension_end - suspension_start) + + relativedelta(days=1) + ) + self.assertEqual( + new_line.date_start, suspension_end + relativedelta(days=1) + ) self.assertEqual(new_line.date_end, new_date_end) def test_stop_plan_successor_contract_line_3_without_end_date(self): @@ -800,12 +858,16 @@ class TestContract(TestContractBase): self.acct_line.stop_plan_successor( suspension_start, suspension_end, False ) - self.assertEqual(self.acct_line.date_end, suspension_start) + self.assertEqual( + self.acct_line.date_end, suspension_start - relativedelta(days=1) + ) new_line = self.env['account.analytic.invoice.line'].search( [('predecessor_contract_line_id', '=', self.acct_line.id)] ) self.assertTrue(new_line) - self.assertEqual(new_line.date_start, suspension_end) + self.assertEqual( + new_line.date_start, suspension_end + relativedelta(days=1) + ) self.assertFalse(new_line.date_end) def test_stop_plan_successor_contract_line_4(self): @@ -1112,7 +1174,9 @@ class TestContract(TestContractBase): self.acct_line.stop_plan_successor( suspension_start, suspension_end, True ) - self.assertEqual(self.acct_line.date_end, suspension_start) + self.assertEqual( + self.acct_line.date_end, suspension_start - relativedelta(days=1) + ) new_line = self.env['account.analytic.invoice.line'].search( [('predecessor_contract_line_id', '=', self.acct_line.id)] ) @@ -1121,10 +1185,13 @@ class TestContract(TestContractBase): self.assertTrue(new_line.is_canceled) self.assertFalse(self.acct_line.successor_contract_line_id) self.assertEqual(new_line.predecessor_contract_line_id, self.acct_line) - new_line.uncancel(suspension_end) + new_line.uncancel(suspension_end + relativedelta(days=1)) self.assertFalse(new_line.is_canceled) self.assertEqual(self.acct_line.successor_contract_line_id, new_line) - self.assertEqual(new_line.recurring_next_date, suspension_end) + self.assertEqual( + new_line.recurring_next_date, + suspension_end + relativedelta(days=1), + ) def test_cancel_uncancel_with_predecessor_has_successor(self): suspension_start = fields.Date.today() + relativedelta(months=6) @@ -1199,3 +1266,107 @@ class TestContract(TestContractBase): self.assertTrue(new_line.is_auto_renew) self.assertEqual(new_line.date_start, to_date('2019-01-01')) self.assertEqual(new_line.date_end, to_date('2019-12-31')) + + def test_cron_recurring_create_invoice(self): + self.acct_line.date_start = '2018-01-01' + self.acct_line.recurring_invoicing_type = 'post-paid' + self.acct_line.date_end = '2018-03-15' + self.acct_line._onchange_date_start() + contracts = self.contract + for i in range(10): + contracts |= self.contract.copy() + self.env['account.analytic.account'].cron_recurring_create_invoice() + invoices = self.env['account.invoice'].search( + [('contract_id', 'in', contracts.ids)] + ) + self.assertEqual(len(contracts), len(invoices)) + + def test_get_invoiced_period_monthlylastday(self): + self.acct_line.date_start = '2018-01-05' + self.acct_line.recurring_invoicing_type = 'post-paid' + self.acct_line.recurring_rule_type = 'monthlylastday' + self.acct_line.date_end = '2018-03-15' + self.acct_line._onchange_date_start() + first, last = self.acct_line._get_invoiced_period() + self.assertEqual(first, to_date('2018-01-05')) + self.assertEqual(last, to_date('2018-01-31')) + self.contract.recurring_create_invoice() + first, last = self.acct_line._get_invoiced_period() + self.assertEqual(first, to_date('2018-02-01')) + self.assertEqual(last, to_date('2018-02-28')) + self.contract.recurring_create_invoice() + first, last = self.acct_line._get_invoiced_period() + self.assertEqual(first, to_date('2018-03-01')) + self.assertEqual(last, to_date('2018-03-15')) + + def test_get_invoiced_period_monthly_post_paid(self): + self.acct_line.date_start = '2018-01-05' + self.acct_line.recurring_invoicing_type = 'post-paid' + self.acct_line.recurring_rule_type = 'monthly' + self.acct_line.date_end = '2018-03-15' + self.acct_line._onchange_date_start() + first, last = self.acct_line._get_invoiced_period() + self.assertEqual(first, to_date('2018-01-05')) + self.assertEqual(last, to_date('2018-02-04')) + self.contract.recurring_create_invoice() + first, last = self.acct_line._get_invoiced_period() + self.assertEqual(first, to_date('2018-02-05')) + self.assertEqual(last, to_date('2018-03-04')) + self.contract.recurring_create_invoice() + first, last = self.acct_line._get_invoiced_period() + self.assertEqual(first, to_date('2018-03-05')) + self.assertEqual(last, to_date('2018-03-15')) + + def test_get_invoiced_period_monthly_pre_paid(self): + self.acct_line.date_start = '2018-01-05' + self.acct_line.recurring_invoicing_type = 'pre-paid' + self.acct_line.recurring_rule_type = 'monthly' + self.acct_line.date_end = '2018-03-15' + self.acct_line._onchange_date_start() + first, last = self.acct_line._get_invoiced_period() + self.assertEqual(first, to_date('2018-01-05')) + self.assertEqual(last, to_date('2018-02-04')) + self.contract.recurring_create_invoice() + first, last = self.acct_line._get_invoiced_period() + self.assertEqual(first, to_date('2018-02-05')) + self.assertEqual(last, to_date('2018-03-04')) + self.contract.recurring_create_invoice() + first, last = self.acct_line._get_invoiced_period() + self.assertEqual(first, to_date('2018-03-05')) + self.assertEqual(last, to_date('2018-03-15')) + + def test_get_invoiced_period_yearly_post_paid(self): + self.acct_line.date_start = '2018-01-05' + self.acct_line.recurring_invoicing_type = 'post-paid' + self.acct_line.recurring_rule_type = 'yearly' + self.acct_line.date_end = '2020-03-15' + self.acct_line._onchange_date_start() + first, last = self.acct_line._get_invoiced_period() + self.assertEqual(first, to_date('2018-01-05')) + self.assertEqual(last, to_date('2019-01-04')) + self.contract.recurring_create_invoice() + first, last = self.acct_line._get_invoiced_period() + self.assertEqual(first, to_date('2019-01-05')) + self.assertEqual(last, to_date('2020-01-04')) + self.contract.recurring_create_invoice() + first, last = self.acct_line._get_invoiced_period() + self.assertEqual(first, to_date('2020-01-05')) + self.assertEqual(last, to_date('2020-03-15')) + + def test_get_invoiced_period_yearly_pre_paid(self): + self.acct_line.date_start = '2018-01-05' + self.acct_line.recurring_invoicing_type = 'pre-paid' + self.acct_line.recurring_rule_type = 'yearly' + self.acct_line.date_end = '2020-03-15' + self.acct_line._onchange_date_start() + first, last = self.acct_line._get_invoiced_period() + self.assertEqual(first, to_date('2018-01-05')) + self.assertEqual(last, to_date('2019-01-04')) + self.contract.recurring_create_invoice() + first, last = self.acct_line._get_invoiced_period() + self.assertEqual(first, to_date('2019-01-05')) + self.assertEqual(last, to_date('2020-01-04')) + self.contract.recurring_create_invoice() + first, last = self.acct_line._get_invoiced_period() + self.assertEqual(first, to_date('2020-01-05')) + self.assertEqual(last, to_date('2020-03-15')) diff --git a/contract/views/contract_line.xml b/contract/views/contract_line.xml index e2356306..69ee02b1 100644 --- a/contract/views/contract_line.xml +++ b/contract/views/contract_line.xml @@ -101,6 +101,8 @@ +