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 @@
+