Browse Source

[IMP] - Make recurrence mechanism on contract line

Make recurrence mechanism on contract line and some other refactoring

[FIX] - Keep contract_cron on account_analytic_account model

contract_cron defined with no_update option.
Changing it, will cause issue to past version installation.

[IMP] - Fix recurring_next_date default value

recurring_next_date should have start_date as default value in prepaid policy
and start_date + invoicing_interval if postpaid

[FIX] - Fix test check no journal

[IMP] - Return created invoices on recurring_create_invoice

[IMP] - Specific process to compute recurring_next_date for  monthly-last-day

fixes: #198

[ADD] - Add Post-migration script to bring recurrence info from contract to contract lines

[ADD] - Add search filter based on date_end and recurring_next_date

 - not_finished filter in contract search view
 - finished filter in contract search view
 - Next Invoice group by in contract search view

[ADD] - Add unit tests

- cases to compute first recurring next date
- contract recurring_next_date
- contract date_end

[IMP] - Improve Unit tests
pull/207/head
sbejaoui 6 years ago
parent
commit
b979a4a342
  1. 13
      contract/__manifest__.py
  2. 19
      contract/migrations/12.0.2.0.0/post-migration.py
  3. 10
      contract/models/__init__.py
  4. 69
      contract/models/abstract_contract.py
  5. 184
      contract/models/abstract_contract_line.py
  6. 361
      contract/models/account_analytic_account.py
  7. 100
      contract/models/account_analytic_contract.py
  8. 221
      contract/models/account_analytic_contract_line.py
  9. 16
      contract/models/account_analytic_invoice_line.py
  10. 4
      contract/models/account_invoice.py
  11. 223
      contract/models/contract.py
  12. 250
      contract/models/contract_line.py
  13. 22
      contract/models/contract_template.py
  14. 38
      contract/models/contract_template_line.py
  15. 52
      contract/models/res_partner.py
  16. 63
      contract/report/report_contract.xml
  17. 390
      contract/tests/test_contract.py
  18. 39
      contract/views/abstract_contract_line.xml
  19. 34
      contract/views/contract.xml
  20. 28
      contract/views/contract_line.xml
  21. 33
      contract/views/contract_template.xml
  22. 17
      contract/views/contract_template_line.xml

13
contract/__manifest__.py

@ -4,16 +4,18 @@
# Copyright 2016-2018 Tecnativa - Carlos Dauden # Copyright 2016-2018 Tecnativa - Carlos Dauden
# Copyright 2017 Tecnativa - Vicent Cubells # Copyright 2017 Tecnativa - Vicent Cubells
# Copyright 2016-2017 LasLabs Inc. # Copyright 2016-2017 LasLabs Inc.
# Copyright 2018 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{ {
'name': 'Contracts Management - Recurring',
'version': '12.0.1.0.0',
'name': 'Recurring - Contracts Management',
'version': '12.0.2.0.0',
'category': 'Contract Management', 'category': 'Contract Management',
'license': 'AGPL-3', 'license': 'AGPL-3',
'author': "OpenERP SA, " 'author': "OpenERP SA, "
"Tecnativa, " "Tecnativa, "
"LasLabs, " "LasLabs, "
"ACSONE SA/NV, "
"Odoo Community Association (OCA)", "Odoo Community Association (OCA)",
'website': 'https://github.com/oca/contract', 'website': 'https://github.com/oca/contract',
'depends': ['base', 'account', 'analytic'], 'depends': ['base', 'account', 'analytic'],
@ -24,9 +26,12 @@
'report/contract_views.xml', 'report/contract_views.xml',
'data/contract_cron.xml', 'data/contract_cron.xml',
'data/mail_template.xml', 'data/mail_template.xml',
'views/account_analytic_account_view.xml',
'views/account_analytic_contract_view.xml',
'views/abstract_contract_line.xml',
'views/contract.xml',
'views/contract_template_line.xml',
'views/contract_template.xml',
'views/account_invoice_view.xml', 'views/account_invoice_view.xml',
'views/contract_line.xml',
'views/res_partner_view.xml', 'views/res_partner_view.xml',
], ],
'installable': True, 'installable': True,

19
contract/migrations/12.0.2.0.0/post-migration.py

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Copyright 2018 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
def migrate(cr, version):
"""Copy recurrence info from contract to contract lines."""
cr.execute(
"""UPDATE account_analytic_invoice_line AS contract_line
SET recurring_rule_type=contract.recurring_rule_type,
recurring_invoicing_type=contract.recurring_invoicing_type,
recurring_interval=contract.recurring_interval,
recurring_next_date=contract.recurring_next_date,
date_start=contract.date_start,
date_end=contract.date_end
FROM account_analytic_account AS contract
WHERE contract.id=contract_line.contract_id"""
)

10
contract/models/__init__.py

@ -1,8 +1,10 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import account_analytic_contract
from . import account_analytic_account
from . import account_analytic_contract_line
from . import account_analytic_invoice_line
from . import abstract_contract
from . import abstract_contract_line
from . import contract_template
from . import contract
from . import contract_template_line
from . import contract_line
from . import account_invoice from . import account_invoice
from . import res_partner from . import res_partner

69
contract/models/abstract_contract.py

@ -0,0 +1,69 @@
# Copyright 2004-2010 OpenERP SA
# Copyright 2014 Angel Moya <angel.moya@domatix.com>
# Copyright 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com>
# Copyright 2016-2018 Carlos Dauden <carlos.dauden@tecnativa.com>
# Copyright 2016-2017 LasLabs Inc.
# Copyright 2018 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models, fields
class AbstractAccountAnalyticContract(models.AbstractModel):
_name = 'account.abstract.analytic.contract'
_description = 'Abstract Account Analytic Contract'
# These fields will not be synced to the contract
NO_SYNC = ['name', 'partner_id']
name = fields.Char(required=True)
# Needed for avoiding errors on several inherited behaviors
partner_id = fields.Many2one(
comodel_name="res.partner", string="Partner (always False)"
)
pricelist_id = fields.Many2one(
comodel_name='product.pricelist', string='Pricelist'
)
contract_type = fields.Selection(
selection=[('sale', 'Customer'), ('purchase', 'Supplier')],
default='sale',
)
journal_id = fields.Many2one(
'account.journal',
string='Journal',
default=lambda s: s._default_journal(),
domain="[('type', '=', contract_type),"
"('company_id', '=', company_id)]",
)
company_id = fields.Many2one(
'res.company',
string='Company',
required=True,
default=lambda self: self.env.user.company_id,
)
@api.onchange('contract_type')
def _onchange_contract_type(self):
if self.contract_type == 'purchase':
self.recurring_invoice_line_ids.filtered('automatic_price').update(
{'automatic_price': False}
)
self.journal_id = self.env['account.journal'].search(
[
('type', '=', self.contract_type),
('company_id', '=', self.company_id.id),
],
limit=1,
)
@api.model
def _default_journal(self):
company_id = self.env.context.get(
'company_id', self.env.user.company_id.id
)
domain = [
('type', '=', self.contract_type),
('company_id', '=', company_id),
]
return self.env['account.journal'].search(domain, limit=1)

184
contract/models/abstract_contract_line.py

@ -0,0 +1,184 @@
# Copyright 2004-2010 OpenERP SA
# Copyright 2014 Angel Moya <angel.moya@domatix.com>
# Copyright 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com>
# Copyright 2016-2018 Carlos Dauden <carlos.dauden@tecnativa.com>
# Copyright 2016-2017 LasLabs Inc.
# Copyright 2018 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models, fields
from odoo.addons import decimal_precision as dp
from odoo.exceptions import ValidationError
from odoo.tools.translate import _
class AccountAbstractAnalyticContractLine(models.AbstractModel):
_name = 'account.abstract.analytic.contract.line'
_description = 'Account Abstract Analytic Contract Line'
product_id = fields.Many2one(
'product.product', string='Product', required=True
)
name = fields.Text(string='Description', required=True)
quantity = fields.Float(default=1.0, required=True)
uom_id = fields.Many2one(
'uom.uom', string='Unit of Measure', required=True
)
automatic_price = fields.Boolean(
string="Auto-price?",
help="If this is marked, the price will be obtained automatically "
"applying the pricelist to the product. If not, you will be "
"able to introduce a manual price",
)
specific_price = fields.Float(string='Specific Price')
price_unit = fields.Float(
string='Unit Price',
compute="_compute_price_unit",
inverse="_inverse_price_unit",
)
price_subtotal = fields.Float(
compute='_compute_price_subtotal',
digits=dp.get_precision('Account'),
string='Sub Total',
)
discount = fields.Float(
string='Discount (%)',
digits=dp.get_precision('Discount'),
help='Discount that is applied in generated invoices.'
' It should be less or equal to 100',
)
sequence = fields.Integer(
string="Sequence",
default=10,
help="Sequence of the contract line when displaying contracts",
)
recurring_rule_type = fields.Selection(
[
('daily', 'Day(s)'),
('weekly', 'Week(s)'),
('monthly', 'Month(s)'),
('monthlylastday', 'Month(s) last day'),
('yearly', 'Year(s)'),
],
default='monthly',
string='Recurrence',
help="Specify Interval for automatic invoice generation.",
required=True,
)
recurring_invoicing_type = fields.Selection(
[('pre-paid', 'Pre-paid'), ('post-paid', 'Post-paid')],
default='pre-paid',
string='Invoicing type',
help="Specify if process date is 'from' or 'to' invoicing date",
required=True,
)
recurring_interval = fields.Integer(
default=1,
string='Repeat Every',
help="Repeat every (Days/Week/Month/Year)",
required=True,
)
partner_id = fields.Many2one(
comodel_name="res.partner", string="Partner (always False)"
)
pricelist_id = fields.Many2one(
comodel_name='product.pricelist', string='Pricelist'
)
recurring_next_date = fields.Date(
copy=False, string='Date of Next Invoice'
)
@api.depends(
'automatic_price',
'specific_price',
'product_id',
'quantity',
'pricelist_id',
'partner_id',
)
def _compute_price_unit(self):
"""Get the specific price if no auto-price, and the price obtained
from the pricelist otherwise.
"""
for line in self:
if line.automatic_price:
product = line.product_id.with_context(
quantity=line.env.context.get(
'contract_line_qty', line.quantity
),
pricelist=line.pricelist_id.id,
partner=line.partner_id.id,
date=line.env.context.get('old_date', fields.Date.today()),
)
line.price_unit = product.price
else:
line.price_unit = line.specific_price
# Tip in https://github.com/odoo/odoo/issues/23891#issuecomment-376910788
@api.onchange('price_unit')
def _inverse_price_unit(self):
"""Store the specific price in the no auto-price records."""
for line in self.filtered(lambda x: not x.automatic_price):
line.specific_price = line.price_unit
@api.multi
@api.depends('quantity', 'price_unit', 'discount')
def _compute_price_subtotal(self):
for line in self:
subtotal = line.quantity * line.price_unit
discount = line.discount / 100
subtotal *= 1 - discount
if line.pricelist_id:
cur = line.pricelist_id.currency_id
line.price_subtotal = cur.round(subtotal)
else:
line.price_subtotal = subtotal
@api.multi
@api.constrains('discount')
def _check_discount(self):
for line in self:
if line.discount > 100:
raise ValidationError(
_("Discount should be less or equal to 100")
)
@api.multi
@api.onchange('product_id')
def _onchange_product_id(self):
if not self.product_id:
return {'domain': {'uom_id': []}}
vals = {}
domain = {
'uom_id': [
('category_id', '=', self.product_id.uom_id.category_id.id)
]
}
if not self.uom_id or (
self.product_id.uom_id.category_id.id != self.uom_id.category_id.id
):
vals['uom_id'] = self.product_id.uom_id
date = self.recurring_next_date or fields.Date.today()
partner = self.partner_id or self.env.user.partner_id
product = self.product_id.with_context(
lang=partner.lang,
partner=partner.id,
quantity=self.quantity,
date=date,
pricelist=self.pricelist_id.id,
uom=self.uom_id.id,
)
name = product.name_get()[0][1]
if product.description_sale:
name += '\n' + product.description_sale
vals['name'] = name
vals['price_unit'] = product.price
self.update(vals)
return {'domain': domain}

361
contract/models/account_analytic_account.py

@ -1,361 +0,0 @@
# Copyright 2004-2010 OpenERP SA
# Copyright 2014 Angel Moya <angel.moya@domatix.com>
# Copyright 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com>
# Copyright 2016-2018 Carlos Dauden <carlos.dauden@tecnativa.com>
# Copyright 2016-2017 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.translate import _
class AccountAnalyticAccount(models.Model):
_name = 'account.analytic.account'
_inherit = ['account.analytic.account',
'account.analytic.contract',
]
contract_template_id = fields.Many2one(
string='Contract Template',
comodel_name='account.analytic.contract',
)
recurring_invoice_line_ids = fields.One2many(
string='Invoice Lines',
comodel_name='account.analytic.invoice.line',
inverse_name='analytic_account_id',
copy=True,
)
date_start = fields.Date(
string='Date Start',
default=fields.Date.context_today,
)
date_end = fields.Date(
string='Date End',
index=True,
)
recurring_invoices = fields.Boolean(
string='Generate recurring invoices automatically',
)
recurring_next_date = fields.Date(
default=fields.Date.context_today,
copy=False,
string='Date of Next Invoice',
)
user_id = fields.Many2one(
comodel_name='res.users',
string='Responsible',
index=True,
default=lambda self: self.env.user,
)
create_invoice_visibility = fields.Boolean(
compute='_compute_create_invoice_visibility',
)
@api.depends('recurring_next_date', 'date_end')
def _compute_create_invoice_visibility(self):
for contract in self:
contract.create_invoice_visibility = (
not contract.date_end or
contract.recurring_next_date <= contract.date_end
)
@api.onchange('contract_template_id')
def _onchange_contract_template_id(self):
"""Update the contract fields with that of the template.
Take special consideration with the `recurring_invoice_line_ids`,
which must be created using the data from the contract lines. Cascade
deletion ensures that any errant lines that are created are also
deleted.
"""
contract = self.contract_template_id
if not contract:
return
for field_name, field in contract._fields.items():
if field.name == 'recurring_invoice_line_ids':
lines = self._convert_contract_lines(contract)
self.recurring_invoice_line_ids = lines
elif not any((
field.compute, field.related, field.automatic,
field.readonly, field.company_dependent,
field.name in self.NO_SYNC,
)):
self[field_name] = self.contract_template_id[field_name]
@api.onchange('date_start')
def _onchange_date_start(self):
if self.date_start:
self.recurring_next_date = self.date_start
@api.onchange('partner_id')
def _onchange_partner_id(self):
self.pricelist_id = self.partner_id.property_product_pricelist.id
@api.constrains('partner_id', 'recurring_invoices')
def _check_partner_id_recurring_invoices(self):
for contract in self.filtered('recurring_invoices'):
if not contract.partner_id:
raise ValidationError(
_("You must supply a customer for the contract '%s'") %
contract.name
)
@api.constrains('recurring_next_date', 'date_start')
def _check_recurring_next_date_start_date(self):
for contract in self.filtered('recurring_next_date'):
if contract.date_start > contract.recurring_next_date:
raise ValidationError(
_("You can't have a next invoicing date before the start "
"of the contract '%s'") % contract.name
)
@api.constrains('recurring_next_date', 'recurring_invoices')
def _check_recurring_next_date_recurring_invoices(self):
for contract in self.filtered('recurring_invoices'):
if not contract.recurring_next_date:
raise ValidationError(
_("You must supply a next invoicing date for contract "
"'%s'") % contract.name
)
@api.constrains('date_start', 'recurring_invoices')
def _check_date_start_recurring_invoices(self):
for contract in self.filtered('recurring_invoices'):
if not contract.date_start:
raise ValidationError(
_("You must supply a start date for contract '%s'") %
contract.name
)
@api.constrains('date_start', 'date_end')
def _check_start_end_dates(self):
for contract in self.filtered('date_end'):
if contract.date_start > contract.date_end:
raise ValidationError(
_("Contract '%s' start date can't be later than end date")
% contract.name
)
@api.multi
def _convert_contract_lines(self, contract):
self.ensure_one()
new_lines = []
for contract_line in contract.recurring_invoice_line_ids:
vals = contract_line._convert_to_write(contract_line.read()[0])
# Remove template link field named as analytic account field
vals.pop('analytic_account_id', False)
new_lines.append((0, 0, vals))
return new_lines
@api.model
def get_relative_delta(self, recurring_rule_type, interval):
if recurring_rule_type == 'daily':
return relativedelta(days=interval)
elif recurring_rule_type == 'weekly':
return relativedelta(weeks=interval)
elif recurring_rule_type == 'monthly':
return relativedelta(months=interval)
elif recurring_rule_type == 'monthlylastday':
return relativedelta(months=interval, day=31)
else:
return relativedelta(years=interval)
@api.model
def _insert_markers(self, line, date_format):
date_from = fields.Date.from_string(line.date_from)
date_to = fields.Date.from_string(line.date_to)
name = line.name
name = name.replace('#START#', date_from.strftime(date_format))
name = name.replace('#END#', date_to.strftime(date_format))
return name
@api.model
def _prepare_invoice_line(self, line, invoice_id):
invoice_line = self.env['account.invoice.line'].new({
'invoice_id': invoice_id,
'product_id': line.product_id.id,
'quantity': line.quantity,
'uom_id': line.uom_id.id,
'discount': line.discount,
})
# Get other invoice line values from product onchange
invoice_line._onchange_product_id()
invoice_line_vals = invoice_line._convert_to_write(invoice_line._cache)
# Insert markers
contract = line.analytic_account_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(line, date_format)
invoice_line_vals.update({
'name': name,
'account_analytic_id': contract.id,
'price_unit': line.price_unit,
})
return invoice_line_vals
@api.multi
def _prepare_invoice(self, journal=None):
self.ensure_one()
if not self.partner_id:
if self.contract_type == 'purchase':
raise ValidationError(
_("You must first select a Supplier for Contract %s!") %
self.name)
else:
raise ValidationError(
_("You must first select a Customer for Contract %s!") %
self.name)
if not journal:
journal = self.journal_id or self.env['account.journal'].search([
('type', '=', self.contract_type),
('company_id', '=', self.company_id.id)
], limit=1)
if not journal:
raise ValidationError(
_("Please define a %s journal for the company '%s'.") %
(self.contract_type, self.company_id.name or '')
)
currency = (
self.pricelist_id.currency_id or
self.partner_id.property_product_pricelist.currency_id or
self.company_id.currency_id
)
invoice_type = 'out_invoice'
if self.contract_type == 'purchase':
invoice_type = 'in_invoice'
invoice = self.env['account.invoice'].new({
'reference': self.code,
'type': invoice_type,
'partner_id': self.partner_id.address_get(
['invoice'])['invoice'],
'currency_id': currency.id,
'journal_id': journal.id,
'date_invoice': self.recurring_next_date,
'origin': self.name,
'company_id': self.company_id.id,
'contract_id': self.id,
'user_id': self.partner_id.user_id.id,
})
# Get other invoice values from partner onchange
invoice._onchange_partner_id()
return invoice._convert_to_write(invoice._cache)
@api.multi
def _prepare_invoice_update(self, invoice):
vals = self._prepare_invoice()
update_vals = {
'contract_id': self.id,
'date_invoice': vals.get('date_invoice', False),
'reference': ' '.join(filter(None, [
invoice.reference, vals.get('reference')])),
'origin': ' '.join(filter(None, [
invoice.origin, vals.get('origin')])),
}
return update_vals
@api.multi
def _create_invoice(self, invoice=False):
"""
:param invoice: If not False add lines to this invoice
:return: invoice created or updated
"""
self.ensure_one()
if invoice and invoice.state == 'draft':
invoice.update(self._prepare_invoice_update(invoice))
else:
invoice = self.env['account.invoice'].create(
self._prepare_invoice())
for line in self.recurring_invoice_line_ids:
invoice_line_vals = self._prepare_invoice_line(line, invoice.id)
if invoice_line_vals:
self.env['account.invoice.line'].create(invoice_line_vals)
invoice.compute_taxes()
return invoice
@api.multi
def recurring_create_invoice(self):
"""Create invoices from contracts
:return: invoices created
"""
invoices = self.env['account.invoice']
for contract in self:
ref_date = contract.recurring_next_date or fields.Date.today()
if (contract.date_start > ref_date or
contract.date_end and contract.date_end < ref_date):
if self.env.context.get('cron'):
continue # Don't fail on cron jobs
raise ValidationError(
_("You must review start and end dates!\n%s") %
contract.name
)
old_date = fields.Date.from_string(ref_date)
new_date = old_date + self.get_relative_delta(
contract.recurring_rule_type, contract.recurring_interval)
ctx = self.env.context.copy()
ctx.update({
'old_date': old_date,
'next_date': new_date,
# Force company for correct evaluation of domain access rules
'force_company': contract.company_id.id,
})
# Re-read contract with correct company
invoices |= contract.with_context(ctx)._create_invoice()
contract.write({
'recurring_next_date': fields.Date.to_string(new_date)
})
return invoices
@api.model
def cron_recurring_create_invoice(self):
today = fields.Date.today()
contracts = self.with_context(cron=True).search([
('recurring_invoices', '=', True),
('recurring_next_date', '<=', today),
'|',
('date_end', '=', False),
('date_end', '>=', today),
])
return contracts.recurring_create_invoice()
@api.multi
def action_contract_send(self):
self.ensure_one()
template = self.env.ref(
'contract.email_contract_template',
False,
)
compose_form = self.env.ref('mail.email_compose_message_wizard_form')
ctx = dict(
default_model='account.analytic.account',
default_res_id=self.id,
default_use_template=bool(template),
default_template_id=template and template.id or False,
default_composition_mode='comment',
)
return {
'name': _('Compose Email'),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.compose.message',
'views': [(compose_form.id, 'form')],
'view_id': compose_form.id,
'target': 'new',
'context': ctx,
}
@api.multi
def button_show_recurring_invoices(self):
self.ensure_one()
action = self.env.ref(
'contract.act_purchase_recurring_invoices')
if self.contract_type == 'sale':
action = self.env.ref(
'contract.act_recurring_invoices')
return action.read()[0]

100
contract/models/account_analytic_contract.py

@ -1,100 +0,0 @@
# Copyright 2004-2010 OpenERP SA
# Copyright 2014 Angel Moya <angel.moya@domatix.com>
# Copyright 2016 Carlos Dauden <carlos.dauden@tecnativa.com>
# Copyright 2016-2017 LasLabs Inc.
# Copyright 2015-2017 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountAnalyticContract(models.Model):
_name = 'account.analytic.contract'
_description = "Account Analytic Contract"
# These fields will not be synced to the contract
NO_SYNC = [
'name',
'partner_id',
]
name = fields.Char(
required=True,
)
# Needed for avoiding errors on several inherited behaviors
partner_id = fields.Many2one(
comodel_name="res.partner",
string="Partner (always False)",
)
contract_type = fields.Selection(
selection=[
('sale', 'Customer'),
('purchase', 'Supplier'),
], default='sale',
)
pricelist_id = fields.Many2one(
comodel_name='product.pricelist',
string='Pricelist',
)
recurring_invoice_line_ids = fields.One2many(
comodel_name='account.analytic.contract.line',
inverse_name='analytic_account_id',
copy=True,
string='Invoice Lines',
)
recurring_rule_type = fields.Selection(
[('daily', 'Day(s)'),
('weekly', 'Week(s)'),
('monthly', 'Month(s)'),
('monthlylastday', 'Month(s) last day'),
('yearly', 'Year(s)'),
],
default='monthly',
string='Recurrence',
help="Specify Interval for automatic invoice generation.",
)
recurring_invoicing_type = fields.Selection(
[('pre-paid', 'Pre-paid'),
('post-paid', 'Post-paid'),
],
default='pre-paid',
string='Invoicing type',
help="Specify if process date is 'from' or 'to' invoicing date",
)
recurring_interval = fields.Integer(
default=1,
string='Repeat Every',
help="Repeat every (Days/Week/Month/Year)",
)
journal_id = fields.Many2one(
'account.journal',
string='Journal',
default=lambda s: s._default_journal(),
domain="[('type', '=', contract_type),"
"('company_id', '=', company_id)]",
)
company_id = fields.Many2one(
'res.company',
string='Company',
required=True,
default=lambda self: self.env.user.company_id,
)
@api.onchange('contract_type')
def _onchange_contract_type(self):
if self.contract_type == 'purchase':
self.recurring_invoice_line_ids.filtered('automatic_price').update(
{'automatic_price': False})
self.journal_id = self.env['account.journal'].search([
('type', '=', self.contract_type),
('company_id', '=', self.company_id.id)
], limit=1)
@api.model
def _default_journal(self):
company_id = self.env.context.get(
'company_id', self.env.user.company_id.id)
domain = [
('type', '=', self.contract_type),
('company_id', '=', company_id)]
return self.env['account.journal'].search(domain, limit=1)

221
contract/models/account_analytic_contract_line.py

@ -1,221 +0,0 @@
# Copyright 2004-2010 OpenERP SA
# Copyright 2014 Angel Moya <angel.moya@domatix.com>
# Copyright 2016 Carlos Dauden <carlos.dauden@tecnativa.com>
# Copyright 2016-2017 LasLabs Inc.
# Copyright 2015-2018 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
from odoo.addons import decimal_precision as dp
from odoo.exceptions import ValidationError
from odoo.tools.translate import _
class AccountAnalyticContractLine(models.Model):
_name = 'account.analytic.contract.line'
_description = 'Contract Lines'
_order = "sequence,id"
product_id = fields.Many2one(
'product.product',
string='Product',
required=True,
)
analytic_account_id = fields.Many2one(
string='Contract',
comodel_name='account.analytic.contract',
required=True,
ondelete='cascade',
)
name = fields.Text(
string='Description',
required=True,
)
quantity = fields.Float(
default=1.0,
required=True,
)
uom_id = fields.Many2one(
'uom.uom',
string='Unit of Measure',
required=True,
)
automatic_price = fields.Boolean(
string="Auto-price?",
help="If this is marked, the price will be obtained automatically "
"applying the pricelist to the product. If not, you will be "
"able to introduce a manual price",
)
specific_price = fields.Float(
string='Specific Price',
)
price_unit = fields.Float(
string='Unit Price',
compute="_compute_price_unit",
inverse="_inverse_price_unit",
)
price_subtotal = fields.Float(
compute='_compute_price_subtotal',
digits=dp.get_precision('Account'),
string='Sub Total',
)
discount = fields.Float(
string='Discount (%)',
digits=dp.get_precision('Discount'),
help='Discount that is applied in generated invoices.'
' It should be less or equal to 100',
)
sequence = fields.Integer(
string="Sequence",
default=10,
help="Sequence of the contract line when displaying contracts",
)
date_from = fields.Date(
string='Date From',
compute='_compute_date_from',
help='Date from invoiced period',
)
date_to = fields.Date(
string='Date To',
compute='_compute_date_to',
help='Date to invoiced period',
)
@api.depends(
'automatic_price',
'specific_price',
'product_id',
'quantity',
'analytic_account_id.pricelist_id',
'analytic_account_id.partner_id',
)
def _compute_price_unit(self):
"""Get the specific price if no auto-price, and the price obtained
from the pricelist otherwise.
"""
for line in self:
if line.automatic_price:
product = line.product_id.with_context(
quantity=line.env.context.get(
'contract_line_qty', line.quantity,
),
pricelist=line.analytic_account_id.pricelist_id.id,
partner=line.analytic_account_id.partner_id.id,
date=line.env.context.get('old_date', fields.Date.today()),
)
line.price_unit = product.price
else:
line.price_unit = line.specific_price
# Tip in https://github.com/odoo/odoo/issues/23891#issuecomment-376910788
@api.onchange('price_unit')
def _inverse_price_unit(self):
"""Store the specific price in the no auto-price records."""
for line in self.filtered(lambda x: not x.automatic_price):
line.specific_price = line.price_unit
@api.multi
@api.depends('quantity', 'price_unit', 'discount')
def _compute_price_subtotal(self):
for line in self:
subtotal = line.quantity * line.price_unit
discount = line.discount / 100
subtotal *= 1 - discount
if line.analytic_account_id.pricelist_id:
cur = line.analytic_account_id.pricelist_id.currency_id
line.price_subtotal = cur.round(subtotal)
else:
line.price_subtotal = subtotal
def _compute_date_from(self):
# When call from template line.analytic_account_id comodel is
# 'account.analytic.contract',
if self._name != 'account.analytic.invoice.line':
return
for line in self:
contract = line.analytic_account_id
date_start = (
self.env.context.get('old_date') or fields.Date.from_string(
contract.recurring_next_date or fields.Date.today())
)
if contract.recurring_invoicing_type == 'pre-paid':
date_from = date_start
else:
date_from = (date_start - contract.get_relative_delta(
contract.recurring_rule_type,
contract.recurring_interval) + relativedelta(days=1))
line.date_from = fields.Date.to_string(date_from)
def _compute_date_to(self):
# When call from template line.analytic_account_id comodel is
# 'account.analytic.contract',
if self._name != 'account.analytic.invoice.line':
return
for line in self:
contract = line.analytic_account_id
date_start = (
self.env.context.get('old_date') or fields.Date.from_string(
contract.recurring_next_date or fields.Date.today())
)
next_date = (
self.env.context.get('next_date') or
date_start + contract.get_relative_delta(
contract.recurring_rule_type, contract.recurring_interval)
)
if contract.recurring_invoicing_type == 'pre-paid':
date_to = next_date - relativedelta(days=1)
else:
date_to = date_start
line.date_to = fields.Date.to_string(date_to)
@api.multi
@api.constrains('discount')
def _check_discount(self):
for line in self:
if line.discount > 100:
raise ValidationError(
_("Discount should be less or equal to 100"))
@api.multi
@api.onchange('product_id')
def _onchange_product_id(self):
if not self.product_id:
return {'domain': {'uom_id': []}}
vals = {}
domain = {'uom_id': [
('category_id', '=', self.product_id.uom_id.category_id.id)]}
if not self.uom_id or (self.product_id.uom_id.category_id.id !=
self.uom_id.category_id.id):
vals['uom_id'] = self.product_id.uom_id
if self.analytic_account_id._name == 'account.analytic.account':
date = (
self.analytic_account_id.recurring_next_date or
fields.Date.today()
)
partner = self.analytic_account_id.partner_id
else:
date = fields.Date.today()
partner = self.env.user.partner_id
product = self.product_id.with_context(
lang=partner.lang,
partner=partner.id,
quantity=self.quantity,
date=date,
pricelist=self.analytic_account_id.pricelist_id.id,
uom=self.uom_id.id
)
name = product.name_get()[0][1]
if product.description_sale:
name += '\n' + product.description_sale
vals['name'] = name
vals['price_unit'] = product.price
self.update(vals)
return {'domain': domain}

16
contract/models/account_analytic_invoice_line.py

@ -1,16 +0,0 @@
# Copyright 2017 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class AccountAnalyticInvoiceLine(models.Model):
_name = 'account.analytic.invoice.line'
_inherit = 'account.analytic.contract.line'
analytic_account_id = fields.Many2one(
comodel_name='account.analytic.account',
string='Analytic Account',
required=True,
ondelete='cascade',
)

4
contract/models/account_invoice.py

@ -8,5 +8,5 @@ class AccountInvoice(models.Model):
_inherit = 'account.invoice' _inherit = 'account.invoice'
contract_id = fields.Many2one( contract_id = fields.Many2one(
'account.analytic.account',
string='Contract')
'account.analytic.account', string='Contract'
)

223
contract/models/contract.py

@ -0,0 +1,223 @@
# Copyright 2004-2010 OpenERP SA
# Copyright 2014 Angel Moya <angel.moya@domatix.com>
# Copyright 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com>
# Copyright 2016-2018 Carlos Dauden <carlos.dauden@tecnativa.com>
# Copyright 2016-2017 LasLabs Inc.
# Copyright 2018 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.translate import _
class AccountAnalyticAccount(models.Model):
_name = 'account.analytic.account'
_inherit = [
'account.analytic.account',
'account.abstract.analytic.contract',
]
contract_template_id = fields.Many2one(
string='Contract Template', comodel_name='account.analytic.contract'
)
recurring_invoice_line_ids = fields.One2many(
string='Invoice Lines',
comodel_name='account.analytic.invoice.line',
inverse_name='contract_id',
copy=True,
)
recurring_invoices = fields.Boolean(
string='Generate recurring invoices automatically'
)
user_id = fields.Many2one(
comodel_name='res.users',
string='Responsible',
index=True,
default=lambda self: self.env.user,
)
create_invoice_visibility = fields.Boolean(
compute='_compute_create_invoice_visibility'
)
recurring_next_date = fields.Date(
compute='_compute_recurring_next_date',
string='Date of Next Invoice',
store=True,
)
date_end = fields.Date(
compute='_compute_date_end', string='Date End', store=True
)
@api.depends('recurring_invoice_line_ids.date_end')
def _compute_date_end(self):
for contract in self:
contract.date_end = False
date_end = contract.recurring_invoice_line_ids.mapped('date_end')
if date_end and all(date_end):
contract.date_end = max(date_end)
@api.depends('recurring_invoice_line_ids.recurring_next_date')
def _compute_recurring_next_date(self):
for contract in self:
recurring_next_date = contract.recurring_invoice_line_ids.filtered(
'create_invoice_visibility'
).mapped('recurring_next_date')
if recurring_next_date:
contract.recurring_next_date = min(recurring_next_date)
@api.depends('recurring_invoice_line_ids.create_invoice_visibility')
def _compute_create_invoice_visibility(self):
for contract in self:
contract.create_invoice_visibility = any(
contract.recurring_invoice_line_ids.mapped(
'create_invoice_visibility'
)
)
@api.onchange('contract_template_id')
def _onchange_contract_template_id(self):
"""Update the contract fields with that of the template.
Take special consideration with the `recurring_invoice_line_ids`,
which must be created using the data from the contract lines. Cascade
deletion ensures that any errant lines that are created are also
deleted.
"""
contract_template_id = self.contract_template_id
if not contract_template_id:
return
for field_name, field in contract_template_id._fields.items():
if field.name == 'recurring_invoice_line_ids':
lines = self._convert_contract_lines(contract_template_id)
self.recurring_invoice_line_ids = lines
elif not any(
(
field.compute,
field.related,
field.automatic,
field.readonly,
field.company_dependent,
field.name in self.NO_SYNC,
)
):
self[field_name] = self.contract_template_id[field_name]
@api.onchange('partner_id')
def _onchange_partner_id(self):
self.pricelist_id = self.partner_id.property_product_pricelist.id
@api.constrains('partner_id', 'recurring_invoices')
def _check_partner_id_recurring_invoices(self):
for contract in self.filtered('recurring_invoices'):
if not contract.partner_id:
raise ValidationError(
_("You must supply a customer for the contract '%s'")
% contract.name
)
@api.multi
def _convert_contract_lines(self, contract):
self.ensure_one()
new_lines = []
for contract_line in contract.recurring_invoice_line_ids:
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()
self.recurring_invoice_line_ids._onchange_date_start()
new_lines.append((0, 0, vals))
return new_lines
@api.multi
def _prepare_invoice(self, date_invoice, journal=None):
self.ensure_one()
if not self.partner_id:
if self.contract_type == 'purchase':
raise ValidationError(
_("You must first select a Supplier for Contract %s!")
% self.name
)
else:
raise ValidationError(
_("You must first select a Customer for Contract %s!")
% self.name
)
if not journal:
journal = (
self.journal_id
if self.journal_id.type == self.contract_type
else self.env['account.journal'].search(
[
('type', '=', self.contract_type),
('company_id', '=', self.company_id.id),
],
limit=1,
)
)
if not journal:
raise ValidationError(
_("Please define a %s journal for the company '%s'.")
% (self.contract_type, self.company_id.name or '')
)
currency = (
self.pricelist_id.currency_id
or self.partner_id.property_product_pricelist.currency_id
or self.company_id.currency_id
)
invoice_type = 'out_invoice'
if self.contract_type == 'purchase':
invoice_type = 'in_invoice'
invoice = self.env['account.invoice'].new(
{
'reference': self.code,
'type': invoice_type,
'partner_id': self.partner_id.address_get(['invoice'])[
'invoice'
],
'currency_id': currency.id,
'date_invoice': date_invoice,
'journal_id': journal.id,
'origin': self.name,
'company_id': self.company_id.id,
'contract_id': self.id,
'user_id': self.partner_id.user_id.id,
}
)
# Get other invoice values from partner onchange
invoice._onchange_partner_id()
return invoice._convert_to_write(invoice._cache)
@api.multi
def action_contract_send(self):
self.ensure_one()
template = self.env.ref('contract.email_contract_template', False)
compose_form = self.env.ref('mail.email_compose_message_wizard_form')
ctx = dict(
default_model='account.analytic.account',
default_res_id=self.id,
default_use_template=bool(template),
default_template_id=template and template.id or False,
default_composition_mode='comment',
)
return {
'name': _('Compose Email'),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.compose.message',
'views': [(compose_form.id, 'form')],
'view_id': compose_form.id,
'target': 'new',
'context': ctx,
}
@api.multi
def recurring_create_invoice(self):
return self.env[
'account.analytic.invoice.line'
].recurring_create_invoice(self)
@api.model
def cron_recurring_create_invoice(self):
self.env['account.analytic.invoice.line'].recurring_create_invoice()

250
contract/models/contract_line.py

@ -0,0 +1,250 @@
# Copyright 2017 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class AccountAnalyticInvoiceLine(models.Model):
_name = 'account.analytic.invoice.line'
_inherit = 'account.abstract.analytic.contract.line'
contract_id = fields.Many2one(
comodel_name='account.analytic.account',
string='Analytic Account',
required=True,
ondelete='cascade',
oldname='analytic_account_id',
)
date_start = fields.Date(string='Date Start', default=fields.Date.today())
date_end = fields.Date(string='Date End', index=True)
recurring_next_date = fields.Date(
copy=False, string='Date of Next Invoice'
)
create_invoice_visibility = fields.Boolean(
compute='_compute_create_invoice_visibility'
)
partner_id = fields.Many2one(
comodel_name="res.partner",
string="Partner (always False)",
related='contract_id.partner_id',
store=True,
readonly=True,
)
pricelist_id = fields.Many2one(
comodel_name='product.pricelist',
string='Pricelist',
related='contract_id.pricelist_id',
store=True,
readonly=True,
)
@api.model
def _compute_first_recurring_next_date(
self,
date_start,
recurring_invoicing_type,
recurring_rule_type,
recurring_interval,
):
if recurring_rule_type == 'monthlylastday':
return date_start + self.get_relative_delta(
recurring_rule_type, recurring_interval - 1
)
if recurring_invoicing_type == 'pre-paid':
return date_start
return date_start + self.get_relative_delta(
recurring_rule_type, recurring_interval
)
@api.onchange(
'date_start',
'recurring_invoicing_type',
'recurring_rule_type',
'recurring_interval',
)
def _onchange_date_start(self):
for rec in self.filtered('date_start'):
rec.recurring_next_date = self._compute_first_recurring_next_date(
rec.date_start,
rec.recurring_invoicing_type,
rec.recurring_rule_type,
rec.recurring_interval,
)
@api.constrains('recurring_next_date', 'date_start')
def _check_recurring_next_date_start_date(self):
for line in self.filtered('recurring_next_date'):
if line.date_start and line.recurring_next_date:
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'"
)
% line.contract_id.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:
raise ValidationError(
_(
"You must supply a next invoicing date for contract "
"'%s'"
)
% line.contract_id.name
)
@api.constrains('date_start')
def _check_date_start_recurring_invoices(self):
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
)
@api.constrains('date_start', 'date_end')
def _check_start_end_dates(self):
for line in self.filtered('date_end'):
if line.date_start and line.date_end:
if line.date_start > line.date_end:
raise ValidationError(
_(
"Contract '%s' start date can't be later than "
"end date"
)
% line.contract_id.name
)
@api.depends('recurring_next_date', 'date_end')
def _compute_create_invoice_visibility(self):
for line in self:
line.create_invoice_visibility = not line.date_end or (
line.recurring_next_date
and line.date_end
and line.recurring_next_date <= line.date_end
)
@api.model
def recurring_create_invoice(self, contract=False):
domain = []
date_ref = fields.Date.today()
if contract:
contract.ensure_one()
date_ref = contract.recurring_next_date
domain.append(('contract_id', '=', contract.id))
domain.extend(
[
('contract_id.recurring_invoices', '=', True),
('recurring_next_date', '<=', date_ref),
'|',
('date_end', '=', False),
('date_end', '>=', date_ref),
]
)
lines = self.search(domain).filtered('create_invoice_visibility')
if lines:
return lines._recurring_create_invoice()
return False
@api.multi
def _recurring_create_invoice(self):
"""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()
return invoices
@api.multi
def _create_invoice(self):
"""
:param invoice: If not False add lines to this invoice
:return: invoice created or updated
"""
contract = self.mapped('contract_id')
date_invoice = min(self.mapped('recurring_next_date'))
invoice = self.env['account.invoice'].create(
contract._prepare_invoice(date_invoice)
)
for line in self:
invoice_line_vals = line._prepare_invoice_line(invoice.id)
if invoice_line_vals:
self.env['account.invoice.line'].create(invoice_line_vals)
invoice.compute_taxes()
return invoice
@api.multi
def _prepare_invoice_line(self, invoice_id):
self.ensure_one()
invoice_line = self.env['account.invoice.line'].new(
{
'invoice_id': invoice_id,
'product_id': self.product_id.id,
'quantity': self.quantity,
'uom_id': self.uom_id.id,
'discount': self.discount,
}
)
# Get other invoice line values from product onchange
invoice_line._onchange_product_id()
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)
invoice_line_vals.update(
{
'name': name,
'account_analytic_id': contract.id,
'price_unit': self.price_unit,
}
)
return invoice_line_vals
@api.multi
def _insert_markers(self, date_format):
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
)
name = self.name
name = name.replace('#START#', date_from.strftime(date_format))
name = name.replace('#END#', date_to.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)
new_date = old_date + self.get_relative_delta(
line.recurring_rule_type, line.recurring_interval
)
line.recurring_next_date = new_date
@api.model
def get_relative_delta(self, recurring_rule_type, interval):
if recurring_rule_type == 'daily':
return relativedelta(days=interval)
elif recurring_rule_type == 'weekly':
return relativedelta(weeks=interval)
elif recurring_rule_type == 'monthly':
return relativedelta(months=interval)
elif recurring_rule_type == 'monthlylastday':
return relativedelta(months=interval, day=31)
else:
return relativedelta(years=interval)

22
contract/models/contract_template.py

@ -0,0 +1,22 @@
# Copyright 2004-2010 OpenERP SA
# Copyright 2014 Angel Moya <angel.moya@domatix.com>
# Copyright 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com>
# Copyright 2016-2018 Carlos Dauden <carlos.dauden@tecnativa.com>
# Copyright 2016-2017 LasLabs Inc.
# Copyright 2018 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class AccountAnalyticContract(models.Model):
_name = 'account.analytic.contract'
_inherit = 'account.abstract.analytic.contract'
_description = "Account Analytic Contract"
recurring_invoice_line_ids = fields.One2many(
comodel_name='account.analytic.contract.line',
inverse_name='contract_template_id',
copy=True,
string='Invoice Lines',
)

38
contract/models/contract_template_line.py

@ -0,0 +1,38 @@
# Copyright 2004-2010 OpenERP SA
# Copyright 2014 Angel Moya <angel.moya@domatix.com>
# Copyright 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com>
# Copyright 2016-2018 Carlos Dauden <carlos.dauden@tecnativa.com>
# Copyright 2016-2017 LasLabs Inc.
# Copyright 2018 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class AccountAnalyticContractLine(models.Model):
_name = 'account.analytic.contract.line'
_inherit = 'account.abstract.analytic.contract.line'
_description = 'Contract Lines'
_order = "sequence,id"
contract_template_id = fields.Many2one(
string='Contract',
comodel_name='account.analytic.contract',
required=True,
ondelete='cascade',
oldname='analytic_account_id',
)
partner_id = fields.Many2one(
comodel_name="res.partner",
string="Partner (always False)",
related='contract_template_id.partner_id',
store=True,
readonly=True,
)
pricelist_id = fields.Many2one(
comodel_name='product.pricelist',
string='Pricelist',
related='contract_template_id.pricelist_id',
store=True,
readonly=True,
)

52
contract/models/res_partner.py

@ -8,35 +8,47 @@ class ResPartner(models.Model):
_inherit = 'res.partner' _inherit = 'res.partner'
sale_contract_count = fields.Integer( sale_contract_count = fields.Integer(
string='Sale Contracts',
compute='_compute_contract_count',
string='Sale Contracts', compute='_compute_contract_count'
) )
purchase_contract_count = fields.Integer( purchase_contract_count = fields.Integer(
string='Purchase Contracts',
compute='_compute_contract_count',
string='Purchase Contracts', compute='_compute_contract_count'
) )
def _compute_contract_count(self): def _compute_contract_count(self):
contract_model = self.env['account.analytic.account'] contract_model = self.env['account.analytic.account']
today = fields.Date.today() today = fields.Date.today()
fetch_data = contract_model.read_group([
fetch_data = contract_model.read_group(
[
('recurring_invoices', '=', True), ('recurring_invoices', '=', True),
('partner_id', 'child_of', self.ids), ('partner_id', 'child_of', self.ids),
'|', '|',
('date_end', '=', False), ('date_end', '=', False),
('date_end', '>=', today)],
['partner_id', 'contract_type'], ['partner_id', 'contract_type'],
lazy=False)
result = [[data['partner_id'][0], data['contract_type'],
data['__count']] for data in fetch_data]
('date_end', '>=', today),
],
['partner_id', 'contract_type'],
['partner_id', 'contract_type'],
lazy=False,
)
result = [
[data['partner_id'][0], data['contract_type'], data['__count']]
for data in fetch_data
]
for partner in self: for partner in self:
partner_child_ids = partner.child_ids.ids + partner.ids partner_child_ids = partner.child_ids.ids + partner.ids
partner.sale_contract_count = sum([
r[2] for r in result
if r[0] in partner_child_ids and r[1] == 'sale'])
partner.purchase_contract_count = sum([
r[2] for r in result
if r[0] in partner_child_ids and r[1] == 'purchase'])
partner.sale_contract_count = sum(
[
r[2]
for r in result
if r[0] in partner_child_ids and r[1] == 'sale'
]
)
partner.purchase_contract_count = sum(
[
r[2]
for r in result
if r[0] in partner_child_ids and r[1] == 'purchase'
]
)
def act_show_contract(self): def act_show_contract(self):
""" This opens contract view """ This opens contract view
@ -55,14 +67,16 @@ class ResPartner(models.Model):
default_partner_id=self.id, default_partner_id=self.id,
default_recurring_invoices=True, default_recurring_invoices=True,
default_pricelist_id=self.property_product_pricelist.id, default_pricelist_id=self.property_product_pricelist.id,
),
)
) )
return res return res
def _get_act_window_contract_xml(self, contract_type): def _get_act_window_contract_xml(self, contract_type):
if contract_type == 'purchase': if contract_type == 'purchase':
return self.env['ir.actions.act_window'].for_xml_id( return self.env['ir.actions.act_window'].for_xml_id(
'contract', 'action_account_analytic_purchase_overdue_all')
'contract', 'action_account_analytic_purchase_overdue_all'
)
else: else:
return self.env['ir.actions.act_window'].for_xml_id( return self.env['ir.actions.act_window'].for_xml_id(
'contract', 'action_account_analytic_sale_overdue_all')
'contract', 'action_account_analytic_sale_overdue_all'
)

63
contract/report/report_contract.xml

@ -9,34 +9,54 @@
<div class="oe_structure"/> <div class="oe_structure"/>
<div class="row" id="partner_info"> <div class="row" id="partner_info">
<div class="col-xs-5 col-xs-offset-7"> <div class="col-xs-5 col-xs-offset-7">
<p id="partner_info"><strong>Partner:</strong></p>
<div t-field="o.partner_id" t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "mobile", "fax", "email"], "no_marker": true, "phone_icons": true}'/>
<p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
<p id="partner_info">
<strong>Partner:</strong>
</p>
<div t-field="o.partner_id"
t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "mobile", "fax", "email"], "no_marker": true, "phone_icons": true}'/>
<p t-if="o.partner_id.vat">VAT:
<span t-field="o.partner_id.vat"/>
</p>
</div> </div>
</div> </div>
<div class="row" id="header_info"> <div class="row" id="header_info">
<div class="col-xs-3"> <div class="col-xs-3">
<strong>Date Start: </strong><p t-field="o.date_start"/>
<strong>Responsible: </strong><p t-field="o.user_id"/>
<strong>Contract: </strong><p t-field="o.code"/>
<strong>Responsible:</strong>
<p t-field="o.user_id"/>
<strong>Contract:</strong>
<p t-field="o.code"/>
</div> </div>
</div> </div>
<div class="row" id="invoice_info"> <div class="row" id="invoice_info">
<t t-set="total" t-value="0"/> <t t-set="total" t-value="0"/>
<div class="col-xs-12"> <div class="col-xs-12">
<t t-set="total" t-value="0"/> <t t-set="total" t-value="0"/>
<p id="services_info"><strong>Recurring Items</strong></p>
<p id="services_info">
<strong>Recurring Items</strong>
</p>
<table class="table table-condensed"> <table class="table table-condensed">
<thead> <thead>
<tr> <tr>
<th><strong>Description</strong></th>
<th class="text-right"><strong>Quantity</strong></th>
<th class="text-right"><strong>Unit Price</strong></th>
<th class="text-right"><strong>Price</strong></th>
<th>
<strong>Description</strong>
</th>
<th class="text-right">
<strong>Quantity</strong>
</th>
<th class="text-right">
<strong>Unit Price</strong>
</th>
<th class="text-right">
<strong>Price</strong>
</th>
<th class="text-right">
<strong>Date Start</strong>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr t-foreach="o.recurring_invoice_line_ids" t-as="l">
<tr t-foreach="o.recurring_invoice_line_ids"
t-as="l">
<td> <td>
<span t-field="l.name"/> <span t-field="l.name"/>
</td> </td>
@ -44,12 +64,18 @@
<span t-field="l.quantity"/> <span t-field="l.quantity"/>
</td> </td>
<td class="text-right"> <td class="text-right">
<span t-field="l.price_unit" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
<span t-field="l.price_unit"
t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
</td> </td>
<td class="text-right"> <td class="text-right">
<span t-field="l.price_subtotal" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
<span t-field="l.price_subtotal"
t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
</td> </td>
<t t-set="total" t-value="total + l.price_subtotal"/>
<td>
<span t-field="l.date_start"/>
</td>
<t t-set="total"
t-value="total + l.price_subtotal"/>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -57,9 +83,12 @@
<div class="col-xs-4 pull-right"> <div class="col-xs-4 pull-right">
<table class="table table-condensed"> <table class="table table-condensed">
<tr class="border-black"> <tr class="border-black">
<td><strong>Total</strong></td>
<td>
<strong>Total</strong>
</td>
<td class="text-right"> <td class="text-right">
<span t-esc="total" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
<span t-esc="total"
t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
</td> </td>
</tr> </tr>
</table> </table>

390
contract/tests/test_contract.py

@ -7,6 +7,10 @@ from odoo.exceptions import ValidationError
from odoo.tests import common from odoo.tests import common
def to_date(date):
return fields.Date.to_date(date)
class TestContractBase(common.SavepointCase): class TestContractBase(common.SavepointCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@ -14,51 +18,85 @@ class TestContractBase(common.SavepointCase):
cls.partner = cls.env.ref('base.res_partner_2') cls.partner = cls.env.ref('base.res_partner_2')
cls.product = cls.env.ref('product.product_product_2') cls.product = cls.env.ref('product.product_product_2')
cls.product.taxes_id += cls.env['account.tax'].search( cls.product.taxes_id += cls.env['account.tax'].search(
[('type_tax_use', '=', 'sale')], limit=1)
[('type_tax_use', '=', 'sale')], limit=1
)
cls.product.description_sale = 'Test description sale' cls.product.description_sale = 'Test description sale'
cls.template_vals = {
cls.line_template_vals = {
'product_id': cls.product.id,
'name': 'Services from #START# to #END#',
'quantity': 1,
'uom_id': cls.product.uom_id.id,
'price_unit': 100,
'discount': 50,
'recurring_rule_type': 'yearly', 'recurring_rule_type': 'yearly',
'recurring_interval': 12345,
'recurring_interval': 1,
}
cls.template_vals = {
'name': 'Test Contract Template', 'name': 'Test Contract Template',
'recurring_invoice_line_ids': [(0, 0, cls.line_template_vals)],
} }
cls.template = cls.env['account.analytic.contract'].create( cls.template = cls.env['account.analytic.contract'].create(
cls.template_vals,
cls.template_vals
) )
# For being sure of the applied price # For being sure of the applied price
cls.env['product.pricelist.item'].create({
cls.env['product.pricelist.item'].create(
{
'pricelist_id': cls.partner.property_product_pricelist.id, 'pricelist_id': cls.partner.property_product_pricelist.id,
'product_id': cls.product.id, 'product_id': cls.product.id,
'compute_price': 'formula', 'compute_price': 'formula',
'base': 'list_price', 'base': 'list_price',
})
cls.contract = cls.env['account.analytic.account'].create({
}
)
cls.contract = cls.env['account.analytic.account'].create(
{
'name': 'Test Contract', 'name': 'Test Contract',
'partner_id': cls.partner.id, 'partner_id': cls.partner.id,
'pricelist_id': cls.partner.property_product_pricelist.id, 'pricelist_id': cls.partner.property_product_pricelist.id,
'recurring_invoices': True, 'recurring_invoices': True,
'date_start': '2016-02-15',
'recurring_next_date': '2016-02-29',
})
cls.contract2 = cls.env['account.analytic.account'].create({
}
)
cls.contract2 = cls.env['account.analytic.account'].create(
{
'name': 'Test Contract 2', 'name': 'Test Contract 2',
'partner_id': cls.partner.id, 'partner_id': cls.partner.id,
'pricelist_id': cls.partner.property_product_pricelist.id, 'pricelist_id': cls.partner.property_product_pricelist.id,
'recurring_invoices': True, 'recurring_invoices': True,
'contract_type': 'purchase',
'recurring_invoice_line_ids': [
(
0,
0,
{
'product_id': cls.product.id,
'name': 'Services from #START# to #END#',
'quantity': 1,
'uom_id': cls.product.uom_id.id,
'price_unit': 100,
'discount': 50,
'recurring_rule_type': 'monthly',
'recurring_interval': 1,
'date_start': '2016-02-15', 'date_start': '2016-02-15',
'recurring_next_date': '2016-02-29', 'recurring_next_date': '2016-02-29',
'contract_type': 'purchase',
})
},
)
],
}
)
cls.line_vals = { cls.line_vals = {
'analytic_account_id': cls.contract.id,
'contract_id': cls.contract.id,
'product_id': cls.product.id, 'product_id': cls.product.id,
'name': 'Services from #START# to #END#', 'name': 'Services from #START# to #END#',
'quantity': 1, 'quantity': 1,
'uom_id': cls.product.uom_id.id, 'uom_id': cls.product.uom_id.id,
'price_unit': 100, 'price_unit': 100,
'discount': 50, 'discount': 50,
'recurring_rule_type': 'monthly',
'recurring_interval': 1,
'date_start': '2016-02-15',
'recurring_next_date': '2016-02-29',
} }
cls.acct_line = cls.env['account.analytic.invoice.line'].create( cls.acct_line = cls.env['account.analytic.invoice.line'].create(
cls.line_vals,
cls.line_vals
) )
@ -67,7 +105,9 @@ class TestContract(TestContractBase):
if overrides is None: if overrides is None:
overrides = {} overrides = {}
vals = self.line_vals.copy() vals = self.line_vals.copy()
vals['analytic_account_id'] = self.template.id
del vals['contract_id']
del vals['date_start']
vals['contract_template_id'] = self.template.id
vals.update(overrides) vals.update(overrides)
return self.env['account.analytic.contract.line'].create(vals) return self.env['account.analytic.contract.line'].create(vals)
@ -90,7 +130,7 @@ class TestContract(TestContractBase):
self.assertEqual(self.acct_line.price_unit, 10) self.assertEqual(self.acct_line.price_unit, 10)
def test_contract(self): def test_contract(self):
recurring_next_date = fields.Date.to_date('2016-03-29')
recurring_next_date = to_date('2016-03-29')
self.assertAlmostEqual(self.acct_line.price_subtotal, 50.0) self.assertAlmostEqual(self.acct_line.price_subtotal, 50.0)
res = self.acct_line._onchange_product_id() res = self.acct_line._onchange_product_id()
self.assertIn('uom_id', res['domain']) self.assertIn('uom_id', res['domain'])
@ -100,81 +140,96 @@ class TestContract(TestContractBase):
self.contract.partner_id = self.partner.id self.contract.partner_id = self.partner.id
self.contract.recurring_create_invoice() self.contract.recurring_create_invoice()
self.invoice_monthly = self.env['account.invoice'].search( self.invoice_monthly = self.env['account.invoice'].search(
[('contract_id', '=', self.contract.id)])
[('contract_id', '=', self.contract.id)]
)
self.assertTrue(self.invoice_monthly) self.assertTrue(self.invoice_monthly)
self.assertEqual(self.contract.recurring_next_date,
recurring_next_date)
self.assertEqual(
self.acct_line.recurring_next_date, recurring_next_date
)
self.inv_line = self.invoice_monthly.invoice_line_ids[0] self.inv_line = self.invoice_monthly.invoice_line_ids[0]
self.assertTrue(self.inv_line.invoice_line_tax_ids) self.assertTrue(self.inv_line.invoice_line_tax_ids)
self.assertAlmostEqual(self.inv_line.price_subtotal, 50.0) self.assertAlmostEqual(self.inv_line.price_subtotal, 50.0)
self.assertEqual(self.contract.partner_id.user_id,
self.invoice_monthly.user_id)
self.assertEqual(
self.contract.partner_id.user_id, self.invoice_monthly.user_id
)
def test_contract_daily(self): def test_contract_daily(self):
recurring_next_date = fields.Date.to_date('2016-03-01')
self.contract.recurring_next_date = '2016-02-29'
self.contract.recurring_rule_type = 'daily'
recurring_next_date = to_date('2016-03-01')
self.acct_line.recurring_next_date = '2016-02-29'
self.acct_line.recurring_rule_type = 'daily'
self.contract.pricelist_id = False self.contract.pricelist_id = False
self.contract.cron_recurring_create_invoice()
self.contract.recurring_create_invoice()
invoice_daily = self.env['account.invoice'].search( invoice_daily = self.env['account.invoice'].search(
[('contract_id', '=', self.contract.id)])
[('contract_id', '=', self.contract.id)]
)
self.assertTrue(invoice_daily) self.assertTrue(invoice_daily)
self.assertEqual(self.contract.recurring_next_date,
recurring_next_date)
self.assertEqual(
self.acct_line.recurring_next_date, recurring_next_date
)
def test_contract_weekly(self): def test_contract_weekly(self):
recurring_next_date = fields.Date.to_date('2016-03-07')
self.contract.recurring_next_date = '2016-02-29'
self.contract.recurring_rule_type = 'weekly'
self.contract.recurring_invoicing_type = 'post-paid'
recurring_next_date = to_date('2016-03-07')
self.acct_line.recurring_next_date = '2016-02-29'
self.acct_line.recurring_rule_type = 'weekly'
self.acct_line.recurring_invoicing_type = 'post-paid'
self.contract.recurring_create_invoice() self.contract.recurring_create_invoice()
invoices_weekly = self.env['account.invoice'].search( invoices_weekly = self.env['account.invoice'].search(
[('contract_id', '=', self.contract.id)])
[('contract_id', '=', self.contract.id)]
)
self.assertTrue(invoices_weekly) self.assertTrue(invoices_weekly)
self.assertEqual( self.assertEqual(
self.contract.recurring_next_date, recurring_next_date)
self.acct_line.recurring_next_date, recurring_next_date
)
def test_contract_yearly(self): def test_contract_yearly(self):
recurring_next_date = fields.Date.to_date('2017-02-28')
self.contract.recurring_next_date = '2016-02-29'
self.contract.recurring_rule_type = 'yearly'
recurring_next_date = to_date('2017-02-28')
self.acct_line.recurring_next_date = '2016-02-29'
self.acct_line.recurring_rule_type = 'yearly'
self.contract.recurring_create_invoice() self.contract.recurring_create_invoice()
invoices_weekly = self.env['account.invoice'].search( invoices_weekly = self.env['account.invoice'].search(
[('contract_id', '=', self.contract.id)])
[('contract_id', '=', self.contract.id)]
)
self.assertTrue(invoices_weekly) self.assertTrue(invoices_weekly)
self.assertEqual( self.assertEqual(
self.contract.recurring_next_date, recurring_next_date)
self.acct_line.recurring_next_date, recurring_next_date
)
def test_contract_monthly_lastday(self): def test_contract_monthly_lastday(self):
recurring_next_date = fields.Date.to_date('2016-03-31')
self.contract.recurring_next_date = '2016-02-29'
self.contract.recurring_invoicing_type = 'post-paid'
self.contract.recurring_rule_type = 'monthlylastday'
recurring_next_date = to_date('2016-03-31')
self.acct_line.recurring_next_date = '2016-02-29'
self.acct_line.recurring_invoicing_type = 'post-paid'
self.acct_line.recurring_rule_type = 'monthlylastday'
self.contract.recurring_create_invoice() self.contract.recurring_create_invoice()
invoices_monthly_lastday = self.env['account.invoice'].search( invoices_monthly_lastday = self.env['account.invoice'].search(
[('contract_id', '=', self.contract.id)])
[('contract_id', '=', self.contract.id)]
)
self.assertTrue(invoices_monthly_lastday) self.assertTrue(invoices_monthly_lastday)
self.assertEqual(self.contract.recurring_next_date,
recurring_next_date)
self.assertEqual(
self.acct_line.recurring_next_date, recurring_next_date
)
def test_onchange_partner_id(self): def test_onchange_partner_id(self):
self.contract._onchange_partner_id() self.contract._onchange_partner_id()
self.assertEqual(self.contract.pricelist_id,
self.contract.partner_id.property_product_pricelist)
self.assertEqual(
self.contract.pricelist_id,
self.contract.partner_id.property_product_pricelist,
)
def test_onchange_date_start(self): def test_onchange_date_start(self):
recurring_next_date = fields.Date.to_date('2016-01-01')
self.contract.date_start = recurring_next_date
self.contract._onchange_date_start()
self.assertEqual(self.contract.recurring_next_date,
recurring_next_date)
recurring_next_date = to_date('2016-01-01')
self.acct_line.date_start = recurring_next_date
self.acct_line._onchange_date_start()
self.assertEqual(
self.acct_line.recurring_next_date, recurring_next_date
)
def test_uom(self): def test_uom(self):
uom_litre = self.env.ref('uom.product_uom_litre') uom_litre = self.env.ref('uom.product_uom_litre')
self.acct_line.uom_id = uom_litre.id self.acct_line.uom_id = uom_litre.id
self.acct_line._onchange_product_id() self.acct_line._onchange_product_id()
self.assertEqual(self.acct_line.uom_id,
self.acct_line.product_id.uom_id)
self.assertEqual(
self.acct_line.uom_id, self.acct_line.product_id.uom_id
)
def test_onchange_product_id(self): def test_onchange_product_id(self):
line = self.env['account.analytic.invoice.line'].new() line = self.env['account.analytic.invoice.line'].new()
@ -187,45 +242,55 @@ class TestContract(TestContractBase):
self.assertAlmostEqual(self.acct_line.price_subtotal, 100.0) self.assertAlmostEqual(self.acct_line.price_subtotal, 100.0)
def test_check_journal(self): def test_check_journal(self):
contract_no_journal = self.contract.copy()
contract_no_journal.journal_id = False
journal = self.env['account.journal'].search([('type', '=', 'sale')]) journal = self.env['account.journal'].search([('type', '=', 'sale')])
journal.write({'type': 'general'}) journal.write({'type': 'general'})
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
contract_no_journal.recurring_create_invoice()
self.contract.recurring_create_invoice()
def test_check_date_end(self): def test_check_date_end(self):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.contract.date_end = '2015-12-31'
self.acct_line.date_end = '2015-12-31'
def test_check_recurring_next_date_start_date(self): def test_check_recurring_next_date_start_date(self):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.contract.write({
self.acct_line.write(
{
'date_start': '2017-01-01', 'date_start': '2017-01-01',
'recurring_next_date': '2016-01-01', 'recurring_next_date': '2016-01-01',
})
}
)
def test_check_recurring_next_date_recurring_invoices(self): def test_check_recurring_next_date_recurring_invoices(self):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.contract.write({
'recurring_invoices': True,
'recurring_next_date': False,
})
self.contract.write({'recurring_invoices': True})
self.acct_line.write({'recurring_next_date': False})
def test_check_date_start_recurring_invoices(self): def test_check_date_start_recurring_invoices(self):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.contract.write({
'recurring_invoices': True,
'date_start': False,
})
self.contract.write({'recurring_invoices': True})
self.acct_line.write({'date_start': False})
def test_onchange_contract_template_id(self): def test_onchange_contract_template_id(self):
"""It should change the contract values to match the template.""" """It should change the contract values to match the template."""
self.contract.contract_template_id = self.template self.contract.contract_template_id = self.template
self.contract._onchange_contract_template_id() self.contract._onchange_contract_template_id()
res = { res = {
'recurring_rule_type': self.contract.recurring_rule_type,
'recurring_interval': self.contract.recurring_interval,
'recurring_invoice_line_ids': [
(
0,
0,
{
'product_id': self.product.id,
'name': 'Services from #START# to #END#',
'quantity': 1,
'uom_id': self.product.uom_id.id,
'price_unit': 100,
'discount': 50,
'recurring_rule_type': 'yearly',
'recurring_interval': 1,
},
)
]
} }
del self.template_vals['name'] del self.template_vals['name']
self.assertDictEqual(res, self.template_vals) self.assertDictEqual(res, self.template_vals)
@ -234,19 +299,17 @@ class TestContract(TestContractBase):
"""It should create invoice lines for the contract lines.""" """It should create invoice lines for the contract lines."""
self.acct_line.unlink() self.acct_line.unlink()
self.line_vals['analytic_account_id'] = self.template.id
self.env['account.analytic.contract.line'].create(self.line_vals)
self.contract.contract_template_id = self.template self.contract.contract_template_id = self.template
self.assertFalse(self.contract.recurring_invoice_line_ids,
'Recurring lines were not removed.')
self.assertFalse(
self.contract.recurring_invoice_line_ids,
'Recurring lines were not removed.',
)
self.contract.contract_template_id = self.template
self.contract._onchange_contract_template_id() self.contract._onchange_contract_template_id()
del self.line_vals['analytic_account_id']
self.assertEqual(len(self.contract.recurring_invoice_line_ids), 1) self.assertEqual(len(self.contract.recurring_invoice_line_ids), 1)
for key, value in self.line_vals.items():
for key, value in self.line_template_vals.items():
test_value = self.contract.recurring_invoice_line_ids[0][key] test_value = self.contract.recurring_invoice_line_ids[0][key]
try: try:
test_value = test_value.id test_value = test_value.id
@ -262,7 +325,8 @@ class TestContract(TestContractBase):
self.contract._onchange_contract_type() self.contract._onchange_contract_type()
self.assertEqual(self.contract.journal_id.type, 'sale') self.assertEqual(self.contract.journal_id.type, 'sale')
self.assertEqual( self.assertEqual(
self.contract.journal_id.company_id, self.contract.company_id)
self.contract.journal_id.company_id, self.contract.company_id
)
def test_contract_onchange_product_id_domain_blank(self): def test_contract_onchange_product_id_domain_blank(self):
"""It should return a blank UoM domain when no product.""" """It should return a blank UoM domain when no product."""
@ -286,18 +350,19 @@ class TestContract(TestContractBase):
) )
line.product_id.uom_id = self.env.ref('uom.product_uom_day').id line.product_id.uom_id = self.env.ref('uom.product_uom_day').id
line._onchange_product_id() line._onchange_product_id()
self.assertEqual(line.uom_id,
line.product_id.uom_id)
self.assertEqual(line.uom_id, line.product_id.uom_id)
def test_contract_onchange_product_id_name(self): def test_contract_onchange_product_id_name(self):
"""It should update the name for the line.""" """It should update the name for the line."""
line = self._add_template_line() line = self._add_template_line()
line.product_id.description_sale = 'Test' line.product_id.description_sale = 'Test'
line._onchange_product_id() line._onchange_product_id()
self.assertEqual(line.name,
'\n'.join([line.product_id.name,
line.product_id.description_sale,
]))
self.assertEqual(
line.name,
'\n'.join(
[line.product_id.name, line.product_id.description_sale]
),
)
def test_contract_count(self): def test_contract_count(self):
"""It should return sale contract count.""" """It should return sale contract count."""
@ -313,48 +378,48 @@ class TestContract(TestContractBase):
def test_same_date_start_and_date_end(self): def test_same_date_start_and_date_end(self):
"""It should create one invoice with same start and end date.""" """It should create one invoice with same start and end date."""
account_invoice_model = self.env['account.invoice'] account_invoice_model = self.env['account.invoice']
self.contract.write({
self.acct_line.write(
{
'date_start': fields.Date.today(), 'date_start': fields.Date.today(),
'date_end': fields.Date.today(), 'date_end': fields.Date.today(),
'recurring_next_date': fields.Date.today(), 'recurring_next_date': fields.Date.today(),
})
}
)
self.contract._compute_recurring_next_date()
init_count = account_invoice_model.search_count( init_count = account_invoice_model.search_count(
[('contract_id', '=', self.contract.id)])
self.contract.cron_recurring_create_invoice()
[('contract_id', '=', self.contract.id)]
)
self.contract.recurring_create_invoice()
last_count = account_invoice_model.search_count( last_count = account_invoice_model.search_count(
[('contract_id', '=', self.contract.id)])
[('contract_id', '=', self.contract.id)]
)
self.assertEqual(last_count, init_count + 1) self.assertEqual(last_count, init_count + 1)
with self.assertRaises(ValidationError):
self.contract.recurring_create_invoice() self.contract.recurring_create_invoice()
last_count = account_invoice_model.search_count(
[('contract_id', '=', self.contract.id)]
)
self.assertEqual(last_count, init_count + 1)
def test_compute_create_invoice_visibility(self): def test_compute_create_invoice_visibility(self):
self.contract.write({
self.acct_line.write(
{
'recurring_next_date': '2017-01-01', 'recurring_next_date': '2017-01-01',
'date_start': '2016-01-01', 'date_start': '2016-01-01',
'date_end': False, 'date_end': False,
})
}
)
self.assertTrue(self.contract.create_invoice_visibility) self.assertTrue(self.contract.create_invoice_visibility)
self.contract.date_end = '2017-01-01'
self.acct_line.date_end = '2017-01-01'
self.contract.refresh()
self.assertTrue(self.contract.create_invoice_visibility) self.assertTrue(self.contract.create_invoice_visibility)
self.contract.date_end = '2016-01-01'
self.acct_line.date_end = '2016-01-01'
self.contract.refresh()
self.assertFalse(self.contract.create_invoice_visibility) self.assertFalse(self.contract.create_invoice_visibility)
def test_extend_invoice(self):
account_invoice_model = self.env['account.invoice']
self.contract.recurring_create_invoice()
invoice = account_invoice_model.search(
[('contract_id', '=', self.contract.id)])
invoice.origin = 'Orig Invoice'
self.contract._create_invoice(invoice)
self.assertEqual(invoice.origin, 'Orig Invoice Test Contract')
invoice_count = account_invoice_model.search_count(
[('contract_id', '=', self.contract.id)])
self.assertEqual(invoice_count, 1)
self.assertEqual(len(invoice.invoice_line_ids), 2)
def test_act_show_contract(self): def test_act_show_contract(self):
show_contract = self.partner.\
with_context(contract_type='sale').act_show_contract()
show_contract = self.partner.with_context(
contract_type='sale'
).act_show_contract()
self.assertDictContainsSubset( self.assertDictContainsSubset(
{ {
'name': 'Customer Contracts', 'name': 'Customer Contracts',
@ -364,5 +429,104 @@ class TestContract(TestContractBase):
'xml_id': 'contract.action_account_analytic_sale_overdue_all', 'xml_id': 'contract.action_account_analytic_sale_overdue_all',
}, },
show_contract, show_contract,
'There was an error and the view couldn\'t be opened.'
'There was an error and the view couldn\'t be opened.',
)
def test_compute_first_recurring_next_date(self):
"""Test different combination to compute recurring_next_date
Combination format
{
'recurring_next_date': ( # date
date_start, # date
recurring_invoicing_type, # ('pre-paid','post-paid',)
recurring_rule_type, # ('daily', 'weekly', 'monthly',
# 'monthlylastday', 'yearly'),
recurring_interval, # integer
),
}
"""
def error_message(
date_start,
recurring_invoicing_type,
recurring_rule_type,
recurring_interval,
):
return "Error in %s every %d %s case, start with %s " % (
recurring_invoicing_type,
recurring_interval,
recurring_rule_type,
date_start,
)
combinations = [
(
to_date('2018-01-01'),
(to_date('2018-01-01'), 'pre-paid', 'monthly', 1),
),
(
to_date('2018-01-01'),
(to_date('2018-01-01'), 'pre-paid', 'monthly', 2),
),
(
to_date('2018-02-01'),
(to_date('2018-01-01'), 'post-paid', 'monthly', 1),
),
(
to_date('2018-03-01'),
(to_date('2018-01-01'), 'post-paid', 'monthly', 2),
),
(
to_date('2018-01-31'),
(to_date('2018-01-05'), 'post-paid', 'monthlylastday', 1),
),
(
to_date('2018-01-31'),
(to_date('2018-01-06'), 'pre-paid', 'monthlylastday', 1),
),
(
to_date('2018-02-28'),
(to_date('2018-01-05'), 'pre-paid', 'monthlylastday', 2),
),
(
to_date('2018-01-05'),
(to_date('2018-01-05'), 'pre-paid', 'yearly', 1),
),
(
to_date('2019-01-05'),
(to_date('2018-01-05'), 'post-paid', 'yearly', 1),
),
]
contract_line_env = self.env['account.analytic.invoice.line']
for recurring_next_date, combination in combinations:
self.assertEqual(
recurring_next_date,
contract_line_env._compute_first_recurring_next_date(
*combination
),
error_message(*combination),
)
def test_recurring_next_date(self):
"""recurring next date for a contract is the min for all lines"""
self.contract.recurring_create_invoice()
self.assertEqual(
self.contract.recurring_next_date,
min(
self.contract.recurring_invoice_line_ids.mapped(
'recurring_next_date'
)
),
)
def test_date_end(self):
"""recurring next date for a contract is the min for all lines"""
self.assertFalse(self.contract.date_end)
self.acct_line.date_end = '2018-01-01'
self.assertEqual(
self.contract.date_end,
max(self.contract.recurring_invoice_line_ids.mapped('date_end')),
) )
self.acct_line.copy()
self.acct_line.date_end = False
self.assertFalse(self.contract.date_end)

39
contract/views/abstract_contract_line.xml

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_abstract_analytic_contract_line_view_form" model="ir.ui.view">
<field name="name">Account Abstract Analytic Contract Line Form View</field>
<field name="model">account.abstract.analytic.contract.line</field>
<field name="arch" type="xml">
<form>
<group>
<field name="product_id"/>
<field name="name"/>
<field name="quantity" colspan="2"/>
<field name="uom_id" colspan="2"/>
<field name="automatic_price"/>
<field name="specific_price" invisible="1"/>
<field name="price_unit"
attrs="{'readonly': [('automatic_price', '=', True)]}"
colspan="2"/>
<field name="discount" colspan="2"/>
</group>
<group name="recurrence_info">
<group>
<field name="recurring_invoicing_type"/>
</group>
<group>
<label for="recurring_interval"/>
<div>
<field name="recurring_interval"
class="oe_inline"/>
<field name="recurring_rule_type"
class="oe_inline"/>
</div>
</group>
</group>
</form>
</field>
</record>
</odoo>

34
contract/views/account_analytic_account_view.xml → contract/views/contract.xml

@ -48,35 +48,16 @@
attrs="{'required': [('recurring_invoices', '=', True)]}" attrs="{'required': [('recurring_invoices', '=', True)]}"
/> />
<field name="pricelist_id"/> <field name="pricelist_id"/>
<field name="company_id" groups="base.group_multi_company"/>
<label for="recurring_interval"/>
<div>
<field name="recurring_interval"
class="oe_inline"
attrs="{'required': [('recurring_invoices', '=', True)]}"
/>
<field name="recurring_rule_type"
class="oe_inline"
attrs="{'required': [('recurring_invoices', '=', True)]}"
/>
</div>
<field name="recurring_invoicing_type"
attrs="{'required': [('recurring_invoices', '=', True)]}"
/>
<field name="date_start"
attrs="{'required': [('recurring_invoices', '=', True)]}"
/>
<field name="recurring_next_date"/>
<field name="date_end"/> <field name="date_end"/>
<field name="recurring_next_date"
attrs="{'required': [('recurring_invoices', '=', True)]}"
/>
<field name="company_id" groups="base.group_multi_company"/>
</group> </group>
<label for="recurring_invoice_line_ids" <label for="recurring_invoice_line_ids"
attrs="{'invisible': [('recurring_invoices','=',False)]}" attrs="{'invisible': [('recurring_invoices','=',False)]}"
/> />
<div attrs="{'invisible': [('recurring_invoices','=',False)]}"> <div attrs="{'invisible': [('recurring_invoices','=',False)]}">
<field name="recurring_invoice_line_ids"> <field name="recurring_invoice_line_ids">
<tree string="Account Analytic Lines" editable="bottom">
<tree string="Account Analytic Lines">
<field name="sequence" widget="handle" /> <field name="sequence" widget="handle" />
<field name="product_id"/> <field name="product_id"/>
<field name="name"/> <field name="name"/>
@ -87,6 +68,12 @@
<field name="specific_price" invisible="1"/> <field name="specific_price" invisible="1"/>
<field name="discount" groups="base.group_no_one" /> <field name="discount" groups="base.group_no_one" />
<field name="price_subtotal"/> <field name="price_subtotal"/>
<field name="recurring_interval" invisible="1"/>
<field name="recurring_rule_type" invisible="1"/>
<field name="recurring_invoicing_type" invisible="1"/>
<field name="date_start" required="1"/>
<field name="date_end"/>
<field name="recurring_next_date" required="1"/>
</tree> </tree>
</field> </field>
</div> </div>
@ -155,9 +142,6 @@
<field name="partner_id" position="before"> <field name="partner_id" position="before">
<field name="journal_id" groups="account.group_account_user"/> <field name="journal_id" groups="account.group_account_user"/>
</field> </field>
<field name="partner_id" position="after">
<field name="recurring_next_date" invisible="not context.get('is_contract', False)"/>
</field>
</field> </field>
</record> </record>

28
contract/views/contract_line.xml

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_analytic_invoice_line_view_form" model="ir.ui.view">
<field name="name">account.analytic.invoice.line.form</field>
<field name="model">account.analytic.invoice.line</field>
<field name="inherit_id"
ref="account_abstract_analytic_contract_line_view_form"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//form" position="attributes">
<attribute name="string">Contract Line</attribute>
</xpath>
<xpath expr="//group[@name='recurrence_info']" position="inside">
<group>
<field name="date_start" required="1"/>
</group>
<group>
<field name="date_end"/>
</group>
<group>
<field name="recurring_next_date"/>
</group>
</xpath>
</field>
</record>
</odoo>

33
contract/views/account_analytic_contract_view.xml → contract/views/contract_template.xml

@ -16,24 +16,10 @@
<field name="pricelist_id" /> <field name="pricelist_id" />
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/> <field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
</group> </group>
<group name="group_main_right">
<field name="recurring_invoicing_type" />
<label for="recurring_interval" />
<div>
<field name="recurring_interval"
class="oe_inline"
required="True"
/>
<field name="recurring_rule_type"
class="oe_inline"
required="True"
/>
</div>
</group>
</group> </group>
<group name="group_invoice_lines" string="Invoice Lines"> <group name="group_invoice_lines" string="Invoice Lines">
<field name="recurring_invoice_line_ids" nolabel="1"> <field name="recurring_invoice_line_ids" nolabel="1">
<tree string="Account Analytic Lines" editable="bottom">
<tree string="Account Analytic Lines">
<field name="sequence" widget="handle" /> <field name="sequence" widget="handle" />
<field name="product_id" /> <field name="product_id" />
<field name="name" /> <field name="name" />
@ -44,6 +30,9 @@
<field name="specific_price" invisible="1"/> <field name="specific_price" invisible="1"/>
<field name="discount" groups="base.group_no_one" /> <field name="discount" groups="base.group_no_one" />
<field name="price_subtotal" /> <field name="price_subtotal" />
<field name="recurring_rule_type" invisible="1"/>
<field name="recurring_interval" invisible="1"/>
<field name="recurring_invoicing_type" invisible="1"/>
</tree> </tree>
</field> </field>
</group> </group>
@ -64,9 +53,6 @@
<tree string="Contract Templates"> <tree string="Contract Templates">
<field name="name" /> <field name="name" />
<field name="contract_type" /> <field name="contract_type" />
<field name="recurring_rule_type" />
<field name="recurring_interval" />
<field name="recurring_invoicing_type" />
<field name="pricelist_id" /> <field name="pricelist_id" />
</tree> </tree>
</field> </field>
@ -79,23 +65,12 @@
<search string="Contract Templates"> <search string="Contract Templates">
<field name="name" /> <field name="name" />
<field name="contract_type" /> <field name="contract_type" />
<field name="recurring_rule_type" />
<field name="recurring_interval" />
<field name="recurring_invoicing_type" />
<field name="pricelist_id" /> <field name="pricelist_id" />
<field name="journal_id" /> <field name="journal_id" />
<filter name="contract_type" <filter name="contract_type"
string="Contract Type" string="Contract Type"
context="{'group_by': 'contract_type'}" context="{'group_by': 'contract_type'}"
/> />
<filter name="recurring_rule_type"
string="Recurrence"
context="{'group_by': 'recurring_rule_type'}"
/>
<filter name="recurring_invoicing_type"
string="Invoicing type"
context="{'group_by': 'recurring_invoicing_type'}"
/>
<filter name="pricelist_id" <filter name="pricelist_id"
string="Pricelist" string="Pricelist"
context="{'group_by': 'pricelist_id'}" context="{'group_by': 'pricelist_id'}"

17
contract/views/contract_template_line.xml

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_analytic_contract_line_view_form" model="ir.ui.view">
<field name="name">account.analytic.contract.line.form</field>
<field name="model">account.analytic.contract.line</field>
<field name="inherit_id"
ref="account_abstract_analytic_contract_line_view_form"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//form" position="attributes">
<attribute name="string">Contract Line Template</attribute>
</xpath>
</field>
</record>
</odoo>
Loading…
Cancel
Save