Browse Source

{MIG} contract: Migration to 13.0 from 12.0.5.0

13.0-mig-contract
Administrator 5 years ago
parent
commit
3169811e6f
  1. 4
      contract/__manifest__.py
  2. 94
      contract/migrations/12.0.2.0.0/pre-migration.py
  3. 63
      contract/migrations/12.0.4.0.0/post-migration.py
  4. 147
      contract/migrations/12.0.4.0.0/pre-migration.py
  5. 4
      contract/models/__init__.py
  6. 21
      contract/models/abstract_contract.py
  7. 104
      contract/models/abstract_contract_line.py
  8. 12
      contract/models/account_invoice.py
  9. 12
      contract/models/account_move.py
  10. 4
      contract/models/account_move_line.py
  11. 150
      contract/models/contract.py
  12. 334
      contract/models/contract_line.py
  13. 1
      contract/models/contract_template_line.py
  14. 4
      contract/tests/test_contract.py
  15. 6
      contract/views/abstract_contract_line.xml
  16. 28
      contract/views/account_move.xml
  17. 28
      contract/views/contract.xml
  18. 8
      contract/views/contract_line.xml
  19. 1
      contract/views/contract_template.xml
  20. 6
      contract/views/res_partner_view.xml
  21. 4
      contract/wizards/contract_line_wizard.py

4
contract/__manifest__.py

@ -4,12 +4,12 @@
# 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
# Copyright 2018-2019 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': 'Recurring - Contracts Management', 'name': 'Recurring - Contracts Management',
'version': '12.0.4.2.5',
'version': '13.0.1.0.0', #from 12.0.5.0.0
'category': 'Contract Management', 'category': 'Contract Management',
'license': 'AGPL-3', 'license': 'AGPL-3',
'author': "OpenERP SA, " 'author': "OpenERP SA, "

94
contract/migrations/12.0.2.0.0/pre-migration.py

@ -1,94 +0,0 @@
# Copyright 2018 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from openupgradelib import openupgrade
_logger = logging.getLogger(__name__)
def _set_finished_contract(cr):
_logger.info("set recurring_next_date to false for finished contract")
openupgrade.logged_query(
cr,
"""
UPDATE account_analytic_account
SET recurring_next_date=NULL
WHERE recurring_next_date > date_end
""",
)
def _move_contract_recurrence_info_to_contract_line(cr):
_logger.info("Move contract data to line level")
openupgrade.logged_query(
cr,
"""
ALTER TABLE account_analytic_invoice_line
ADD COLUMN IF NOT EXISTS recurring_rule_type VARCHAR(255),
ADD COLUMN IF NOT EXISTS recurring_invoicing_type VARCHAR(255),
ADD COLUMN IF NOT EXISTS recurring_interval INTEGER,
ADD COLUMN IF NOT EXISTS recurring_next_date DATE,
ADD COLUMN IF NOT EXISTS date_start DATE,
ADD COLUMN IF NOT EXISTS date_end DATE
""",
)
openupgrade.logged_query(
cr,
"""
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.analytic_account_id
""",
)
def _move_contract_template_recurrence_info_to_contract_template_line(cr):
_logger.info("Move contract template data to line level")
openupgrade.logged_query(
cr,
"""
ALTER TABLE account_analytic_contract_line
ADD COLUMN IF NOT EXISTS recurring_rule_type VARCHAR(255),
ADD COLUMN IF NOT EXISTS recurring_invoicing_type VARCHAR(255),
ADD COLUMN IF NOT EXISTS recurring_interval INTEGER
""",
)
openupgrade.logged_query(
cr,
"""
UPDATE account_analytic_contract_line AS contract_template_line
SET
recurring_rule_type=contract_template.recurring_rule_type,
recurring_invoicing_type=contract_template.recurring_invoicing_type,
recurring_interval=contract_template.recurring_interval
FROM
account_analytic_contract AS contract_template
WHERE
contract_template.id=contract_template_line.analytic_account_id
""",
)
@openupgrade.migrate()
def migrate(env, version):
"""
set recurring_next_date to false for finished contract
"""
_logger.info(">> Pre-Migration 12.0.2.0.0")
cr = env.cr
_set_finished_contract(cr)
_move_contract_recurrence_info_to_contract_line(cr)
_move_contract_template_recurrence_info_to_contract_template_line(cr)

63
contract/migrations/12.0.4.0.0/post-migration.py

@ -1,63 +0,0 @@
# Copyright 2019 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from openupgradelib import openupgrade
from odoo.tools import parse_version
_logger = logging.getLogger(__name__)
def _update_no_update_ir_cron(env):
# Update ir.cron
env.ref('contract.contract_cron_for_invoice').model_id = env.ref(
'contract.model_contract_contract'
)
env.ref('contract.contract_line_cron_for_renew').model_id = env.ref(
'contract.model_contract_line'
)
env.ref('contract.email_contract_template').model_id = env.ref(
'contract.model_contract_contract'
)
def _init_last_date_invoiced_on_contract_lines(env):
_logger.info("init last_date_invoiced field for contract lines")
contract_lines = env["contract.line"].search(
[("recurring_next_date", "!=", False)]
)
contract_lines._init_last_date_invoiced()
def _init_invoicing_partner_id_on_contracts(env):
_logger.info("Populate invoicing partner field on contracts")
contracts = env["contract.contract"].search([])
contracts._inverse_partner_id()
def assign_salesman(env):
"""As v11 takes salesman from linked partner and now the salesman is a
field in the contract that is initialized to current user, we need
to assign to the recently converted contracts following old logic, or they
will have admin as responsible.
"""
openupgrade.logged_query(
env.cr, """
UPDATE contract_contract cc
SET user_id = rp.user_id
FROM res_partner rp
WHERE rp.id = cc.partner_id""",
)
@openupgrade.migrate()
def migrate(env, version):
_update_no_update_ir_cron(env)
if parse_version(version) < parse_version('12.0.2.0.0'):
# We check the version here as this post-migration script was in
# 12.0.2.0.0 and already done for those who used the module when
# it was a PR
_init_last_date_invoiced_on_contract_lines(env)
_init_invoicing_partner_id_on_contracts(env)
assign_salesman(env)

147
contract/migrations/12.0.4.0.0/pre-migration.py

@ -1,147 +0,0 @@
# Copyright 2019 ACSONE SA/NV
# Copyright 2019 Tecnativa 2019 - Pedro M. Baeza
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from openupgradelib import openupgrade
from psycopg2 import sql
_logger = logging.getLogger(__name__)
models_to_rename = [
# Contract Line Wizard
('account.analytic.invoice.line.wizard', 'contract.line.wizard'),
# Abstract Contract
('account.abstract.analytic.contract', 'contract.abstract.contract'),
# Abstract Contract Line
(
'account.abstract.analytic.contract.line',
'contract.abstract.contract.line',
),
# Contract Line
('account.analytic.invoice.line', 'contract.line'),
# Contract Template
('account.analytic.contract', 'contract.template'),
# Contract Template Line
('account.analytic.contract.line', 'contract.template.line'),
]
tables_to_rename = [
# Contract Line
('account_analytic_invoice_line', 'contract_line'),
# Contract Template
('account_analytic_contract', 'contract_template'),
# Contract Template Line
('account_analytic_contract_line', 'contract_template_line'),
]
columns_to_copy = {
'contract_line': [
('analytic_account_id', 'contract_id', None),
],
}
xmlids_to_rename = [
(
'contract.account_analytic_cron_for_invoice',
'contract.contract_cron_for_invoice',
),
(
'contract.account_analytic_contract_manager',
'contract.contract_template_manager',
),
(
'contract.account_analytic_contract_user',
'contract.contract_template_user',
),
(
'contract.account_analytic_invoice_line_manager',
'contract.contract_line_manager',
),
(
'contract.account_analytic_invoice_line_user',
'contract.contract_line_user',
),
(
'contract.account_analytic_contract_line_manager',
'contract.contract_template_line_manager',
),
(
'contract.account_analytic_contract_line_user',
'contract.contract_template_line_user',
),
]
def _get_contract_field_name(cr):
"""
Contract field changed the name from analytic_account_id to contract_id
in 12.0.2.0.0. This method used to get the contract field name in
account_analytic_invoice_line"""
return (
'contract_id'
if openupgrade.column_exists(
cr, 'account_analytic_invoice_line', 'contract_id'
)
else 'analytic_account_id'
)
def create_contract_records(cr):
contract_field_name = _get_contract_field_name(cr)
openupgrade.logged_query(
cr, """
CREATE TABLE contract_contract
(LIKE account_analytic_account INCLUDING ALL)""",
)
openupgrade.logged_query(
cr, sql.SQL("""
INSERT INTO contract_contract
SELECT * FROM account_analytic_account
WHERE id IN (SELECT DISTINCT {} FROM contract_line)
""").format(
sql.Identifier(contract_field_name),
),
)
# Deactivate disabled contracts
openupgrade.logged_query(
cr, """UPDATE contract_contract cc
SET active = False
FROM account_analytic_account aaa
WHERE aaa.id = cc.id
AND NOT aaa.recurring_invoices""",
)
# Handle id sequence
cr.execute("CREATE SEQUENCE IF NOT EXISTS contract_contract_id_seq")
cr.execute("SELECT setval('contract_contract_id_seq', "
"(SELECT MAX(id) FROM contract_contract))")
cr.execute("ALTER TABLE contract_contract ALTER id "
"SET DEFAULT NEXTVAL('contract_contract_id_seq')")
# Move common stuff from one table to the other
mapping = [
('ir_attachment', 'res_model', 'res_id'),
('mail_message', 'model', 'res_id'),
('mail_activity', 'res_model', 'res_id'),
('mail_followers', 'res_model', 'res_id'),
]
for table, model_column, id_column in mapping:
openupgrade.logged_query(
cr, sql.SQL("""
UPDATE {table} SET {model_column}='contract.contract'
WHERE {model_column}='account.analytic.account'
AND {id_column} IN (SELECT DISTINCT {col} FROM contract_line)
""").format(
table=sql.Identifier(table),
model_column=sql.Identifier(model_column),
id_column=sql.Identifier(id_column),
col=sql.Identifier(contract_field_name),
),
)
@openupgrade.migrate()
def migrate(env, version):
cr = env.cr
openupgrade.rename_models(cr, models_to_rename)
openupgrade.rename_tables(cr, tables_to_rename)
openupgrade.rename_xmlids(cr, xmlids_to_rename)
openupgrade.copy_columns(cr, columns_to_copy)
create_contract_records(cr)

4
contract/models/__init__.py

@ -6,6 +6,6 @@ from . import contract_template
from . import contract from . import contract
from . import contract_template_line from . import contract_template_line
from . import contract_line from . import contract_line
from . import account_invoice
from . import account_invoice_line
from . import account_move_line
from . import account_move
from . import res_partner from . import res_partner

21
contract/models/abstract_contract.py

@ -16,14 +16,27 @@ class ContractAbstractContract(models.AbstractModel):
# These fields will not be synced to the contract # These fields will not be synced to the contract
NO_SYNC = ['name', 'partner_id', 'company_id'] NO_SYNC = ['name', 'partner_id', 'company_id']
@api.model
def _get_default_invoice_incoterm(self):
''' Get the default incoterm for invoice. '''
return self.env.company.incoterm_id
incoterm_id = fields.Many2one('account.incoterms', string='Incoterm',
default=_get_default_invoice_incoterm,
help='International Commercial Terms are a series of predefined commercial terms used in international transactions.')
name = fields.Char(required=True) name = fields.Char(required=True)
date = fields.Date( string='Signed Date', default=lambda self: fields.Date.today())
# Needed for avoiding errors on several inherited behaviors # Needed for avoiding errors on several inherited behaviors
partner_id = fields.Many2one( partner_id = fields.Many2one(
comodel_name="res.partner", string="Partner", index=True comodel_name="res.partner", string="Partner", index=True
) )
pricelist_id = fields.Many2one(
comodel_name='product.pricelist', string='Pricelist'
)
pricelist_id = fields.Many2one('product.pricelist', string='Pricelist')
currency_id = fields.Many2one(
related="pricelist_id.currency_id",
string="Pricelist currency",
store=True, )
contract_type = fields.Selection( contract_type = fields.Selection(
selection=[('sale', 'Customer'), ('purchase', 'Supplier')], selection=[('sale', 'Customer'), ('purchase', 'Supplier')],
default='sale', default='sale',
@ -47,6 +60,8 @@ class ContractAbstractContract(models.AbstractModel):
), ),
) )
@api.onchange('contract_type') @api.onchange('contract_type')
def _onchange_contract_type(self): def _onchange_contract_type(self):
if self.contract_type == 'purchase': if self.contract_type == 'purchase':

104
contract/models/abstract_contract_line.py

@ -16,43 +16,43 @@ class ContractAbstractContractLine(models.AbstractModel):
_name = 'contract.abstract.contract.line' _name = 'contract.abstract.contract.line'
_description = 'Abstract Recurring Contract Line' _description = 'Abstract Recurring Contract Line'
product_id = fields.Many2one(
'product.product', string='Product', required=True
)
product_id = fields.Many2one('product.product', string='Product', required=True)
name = fields.Text(string='Description', required=True) name = fields.Text(string='Description', required=True)
quantity = fields.Float(default=1.0, 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?",
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 " help="If this is marked, the price will be obtained automatically "
"applying the pricelist to the product. If not, you will be " "applying the pricelist to the product. If not, you will be "
"able to introduce a manual price", "able to introduce a manual price",
) )
specific_price = fields.Float(string='Specific Price') specific_price = fields.Float(string='Specific Price')
price_unit = fields.Float(
string='Unit Price',
price_unit = fields.Float(string='Unit Price',
compute="_compute_price_unit", compute="_compute_price_unit",
inverse="_inverse_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',
)
tax_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)])
contract_id = fields.Many2one(string='Contract', comodel_name='contract.contract', required=True,ondelete='cascade',)
pricelist_id = fields.Many2one(related='contract_id.pricelist_id', depends=['contract_id'], string='Pricelist',store=True)
currency_id = fields.Many2one(related='contract_id.currency_id', depends=['contract_id'], store=True, string='Currency')
company_id = fields.Many2one(related='contract_id.company_id', string='Company', readonly=True, index=True)
price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', readonly=True, store=True,currency_field='currency_id')
price_tax = fields.Float(compute='_compute_amount', string='Total Tax', readonly=True, store=True,currency_field='currency_id')
price_total = fields.Monetary(compute='_compute_amount', string='Total', readonly=True, store=True,currency_field='currency_id')
# discount does not have any sense because can be a discounted pricelist or directly in price
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( sequence = fields.Integer(
string="Sequence", string="Sequence",
default=10, default=10,
help="Sequence of the contract line when displaying contracts", help="Sequence of the contract line when displaying contracts",
) )
recurring_rule_type = fields.Selection( recurring_rule_type = fields.Selection(
[ [
('daily', 'Day(s)'), ('daily', 'Day(s)'),
@ -70,9 +70,17 @@ class ContractAbstractContractLine(models.AbstractModel):
[('pre-paid', 'Pre-paid'), ('post-paid', 'Post-paid')], [('pre-paid', 'Pre-paid'), ('post-paid', 'Post-paid')],
default='pre-paid', default='pre-paid',
string='Invoicing type', string='Invoicing type',
help="Specify if process date is 'from' or 'to' invoicing date",
help="Specify if the invoice must be generated at the beginning (pre-paid) or end (post-paid) of the period.",
required=True, required=True,
) )
recurring_invoicing_offset = fields.Integer(
compute="_compute_recurring_invoicing_offset",
string="Invoicing offset",
help=(
"Number of days to offset the invoice from the period end "
"date (in post-paid mode) or start date (in pre-paid mode)."
)
)
recurring_interval = fields.Integer( recurring_interval = fields.Integer(
default=1, default=1,
string='Invoice Every', string='Invoice Every',
@ -108,12 +116,27 @@ class ContractAbstractContractLine(models.AbstractModel):
default='monthly', default='monthly',
string='Termination Notice type', string='Termination Notice type',
) )
contract_id = fields.Many2one(
string='Contract',
comodel_name='contract.abstract.contract',
required=True,
ondelete='cascade',
)
@api.model
def _get_default_recurring_invoicing_offset(
self, recurring_invoicing_type, recurring_rule_type
):
if (
recurring_invoicing_type == 'pre-paid'
or recurring_rule_type == 'monthlylastday'
):
return 0
else:
return 1
@api.depends('recurring_invoicing_type', 'recurring_rule_type')
def _compute_recurring_invoicing_offset(self):
for rec in self:
rec.recurring_invoicing_offset = (
self._get_default_recurring_invoicing_offset(
rec.recurring_invoicing_type, rec.recurring_rule_type
)
)
@api.depends( @api.depends(
'automatic_price', 'automatic_price',
@ -151,20 +174,32 @@ class ContractAbstractContractLine(models.AbstractModel):
for line in self.filtered(lambda x: not x.automatic_price): for line in self.filtered(lambda x: not x.automatic_price):
line.specific_price = line.price_unit line.specific_price = line.price_unit
@api.multi
@api.depends('quantity', 'price_unit', 'discount')
def _compute_price_subtotal(self):
@api.depends('quantity', 'price_unit', 'discount','tax_id')
def _compute_amount(self):
"""
Compute the amounts of the contract line.
"""
for line in self: for line in self:
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
taxes = line.tax_id.compute_all(price, line.contract_id.currency_id, line.quantity, product=line.product_id, partner=line.contract_id.partner_id)
# print(f"{line}")
line.update({
'price_tax': sum(t.get('amount', 0.0) for t in taxes.get('taxes', [])),
'price_total': taxes['total_included'],
'price_subtotal': taxes['total_excluded'],
})
subtotal = line.quantity * line.price_unit subtotal = line.quantity * line.price_unit
discount = line.discount / 100 discount = line.discount / 100
subtotal *= 1 - discount subtotal *= 1 - discount
if line.contract_id.pricelist_id: if line.contract_id.pricelist_id:
cur = line.contract_id.pricelist_id.currency_id cur = line.contract_id.pricelist_id.currency_id
line.price_subtotal = subtotal
line.price_subtotal = cur.round(subtotal) line.price_subtotal = cur.round(subtotal)
else: else:
line.price_subtotal = subtotal line.price_subtotal = subtotal
@api.multi
@api.constrains('discount') @api.constrains('discount')
def _check_discount(self): def _check_discount(self):
for line in self: for line in self:
@ -173,7 +208,6 @@ class ContractAbstractContractLine(models.AbstractModel):
_("Discount should be less or equal to 100") _("Discount should be less or equal to 100")
) )
@api.multi
@api.onchange('product_id') @api.onchange('product_id')
def _onchange_product_id(self): def _onchange_product_id(self):
if not self.product_id: if not self.product_id:
@ -201,6 +235,8 @@ class ContractAbstractContractLine(models.AbstractModel):
uom=self.uom_id.id, uom=self.uom_id.id,
) )
vals['name'] = self.product_id.get_product_multiline_description_sale() vals['name'] = self.product_id.get_product_multiline_description_sale()
vals['price_unit'] = product.price
vals['price_unit'] = product.lst_price # or .price?
vals['tax_id'] = product.taxes_id if self.contract_id.contract_type == 'sale' else product.supplier_taxes_id
self.update(vals) self.update(vals)
return {'domain': domain} return {'domain': domain}

12
contract/models/account_invoice.py

@ -1,12 +0,0 @@
# © 2016 Carlos Dauden <carlos.dauden@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class AccountInvoice(models.Model):
_inherit = 'account.invoice'
# We keep this field for migration purpose
old_contract_id = fields.Many2one(
'contract.contract', oldname='contract_id')

12
contract/models/account_move.py

@ -0,0 +1,12 @@
# Copyright 2018 ACSONE SA/NV.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class AccountMove(models.Model):
_inherit = 'account.move'
contract_id = fields.Many2one('contract.contract', string='Generated by contract', index=True, readonly=True)

4
contract/models/account_invoice_line.py → contract/models/account_move_line.py

@ -4,8 +4,8 @@
from odoo import fields, models from odoo import fields, models
class AccountInvoiceLine(models.Model):
_inherit = 'account.invoice.line'
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
contract_line_id = fields.Many2one( contract_line_id = fields.Many2one(
'contract.line', string='Contract Line', index=True 'contract.line', string='Contract Line', index=True

150
contract/models/contract.py

@ -7,7 +7,7 @@
# 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 odoo import api, fields, models from odoo import api, fields, models
from odoo.exceptions import ValidationError
from odoo.exceptions import ValidationError,UserError
from odoo.tools.translate import _ from odoo.tools.translate import _
@ -21,9 +21,7 @@ class ContractContract(models.Model):
'contract.abstract.contract', 'contract.abstract.contract',
] ]
active = fields.Boolean(
default=True,
)
active = fields.Boolean( default=True,)
code = fields.Char( code = fields.Char(
string="Reference", string="Reference",
) )
@ -32,11 +30,7 @@ class ContractContract(models.Model):
comodel_name='account.analytic.account', comodel_name='account.analytic.account',
ondelete='restrict', ondelete='restrict',
) )
currency_id = fields.Many2one(
related="company_id.currency_id",
string="Currency",
readonly=True,
)
contract_template_id = fields.Many2one( contract_template_id = fields.Many2one(
string='Contract Template', comodel_name='contract.template' string='Contract Template', comodel_name='contract.template'
) )
@ -92,7 +86,6 @@ class ContractContract(models.Model):
index=True index=True
) )
@api.multi
def _inverse_partner_id(self): def _inverse_partner_id(self):
for rec in self: for rec in self:
if not rec.invoice_partner_id: if not rec.invoice_partner_id:
@ -100,54 +93,32 @@ class ContractContract(models.Model):
['invoice'] ['invoice']
)['invoice'] )['invoice']
@api.multi
def _get_related_invoices(self): def _get_related_invoices(self):
self.ensure_one() self.ensure_one()
invoices = ( invoices = (
self.env['account.invoice.line']
.search(
[
(
'contract_line_id',
'in',
self.contract_line_ids.ids,
)
]
)
.mapped('invoice_id')
)
invoices |= self.env['account.invoice'].search(
[('old_contract_id', '=', self.id)]
)
self.env['account.move'].search([('contract_id','=',self.id),]) )
return invoices return invoices
@api.multi
def _compute_invoice_count(self): def _compute_invoice_count(self):
for rec in self: for rec in self:
rec.invoice_count = len(rec._get_related_invoices()) rec.invoice_count = len(rec._get_related_invoices())
@api.multi
def action_show_invoices(self): def action_show_invoices(self):
self.ensure_one() self.ensure_one()
tree_view_ref = ( tree_view_ref = (
'account.invoice_supplier_tree'
if self.contract_type == 'purchase'
else 'account.invoice_tree_with_onboarding'
'account.move_supplier_tree' if self.contract_type == 'purchase' else 'account.move_tree_with_onboarding'
) )
form_view_ref = ( form_view_ref = (
'account.invoice_supplier_form'
if self.contract_type == 'purchase'
else 'account.invoice_form'
'account.move_supplier_form' if self.contract_type == 'purchase' else 'account.move_form'
) )
tree_view = self.env.ref(tree_view_ref, raise_if_not_found=False) tree_view = self.env.ref(tree_view_ref, raise_if_not_found=False)
form_view = self.env.ref(form_view_ref, raise_if_not_found=False) form_view = self.env.ref(form_view_ref, raise_if_not_found=False)
action = { action = {
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'name': 'Invoices', 'name': 'Invoices',
'res_model': 'account.invoice',
'res_model': 'account.move',
'view_type': 'form', 'view_type': 'form',
'view_mode': 'tree,kanban,form,calendar,pivot,graph,activity',
'view_mode': 'tree,form,activity',
'domain': [('id', 'in', self._get_related_invoices().ids)], 'domain': [('id', 'in', self._get_related_invoices().ids)],
} }
if tree_view and form_view: if tree_view and form_view:
@ -234,7 +205,6 @@ class ContractContract(models.Model):
} }
} }
@api.multi
def _convert_contract_lines(self, contract): def _convert_contract_lines(self, contract):
self.ensure_one() self.ensure_one()
new_lines = self.env['contract.line'] new_lines = self.env['contract.line']
@ -252,49 +222,55 @@ class ContractContract(models.Model):
new_lines._onchange_is_auto_renew() new_lines._onchange_is_auto_renew()
return new_lines return new_lines
@api.multi
def _prepare_invoice(self, date_invoice, journal=None): def _prepare_invoice(self, date_invoice, journal=None):
"""
Taken from sale/models/sale.py
Prepare the dict of values to create the new invoice for a sales order. This method may be
overridden to implement custom invoice generation (making sure to call super() to establish
a clean extension chain).
"""
self.ensure_one() self.ensure_one()
if self.contract_type == 'sale':
invoice_type = 'out_invoice'
else: #purchase
invoice_type = 'in_invoice'
if not journal: 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 '')
)
if self.journal_id:
journal = self.journal_id
else:
journal = self.env['account.move'].with_context(force_company=self.company_id.id, default_type=invoice_type)._get_default_journal()
if not journal:
raise UserError(_('Please define an accounting %s journal for the company %s (%s).') % (self.contract_type, self.company_id.name, self.company_id.id))
currency = ( currency = (
self.pricelist_id.currency_id self.pricelist_id.currency_id
or self.partner_id.property_product_pricelist.currency_id or self.partner_id.property_product_pricelist.currency_id
or self.company_id.currency_id or self.company_id.currency_id
) )
invoice_type = 'out_invoice'
if self.contract_type == 'purchase':
invoice_type = 'in_invoice'
return {
'name': self.code,
invoice_vals = {
'name' : '/',
'date': date_invoice,
'ref': '%s contract id=%s'%(self.contract_type,self.id),
'type': invoice_type, 'type': invoice_type,
'partner_id': self.invoice_partner_id.id,
'currency_id': currency.id,
'date_invoice': date_invoice,
'journal_id': journal.id, 'journal_id': journal.id,
'origin': self.name,
'invoice_origin': 'Contract'+self.name+'id='+str(self.id),
'company_id': self.company_id.id, 'company_id': self.company_id.id,
'currency_id': currency.id,
'invoice_user_id': self.user_id and self.user_id.id,
'partner_id': self.invoice_partner_id.id,
# 'partner_shipping_id': self.partner_id.id,
'fiscal_position_id': self.fiscal_position_id.id or self.invoice_partner_id.property_account_position_id.id,
'invoice_incoterm_id': self.incoterm_id.id,
'invoice_payment_term_id': self.payment_term_id.id,
'invoice_line_ids': [],
'user_id': self.user_id.id, 'user_id': self.user_id.id,
'payment_term_id': self.payment_term_id.id,
'fiscal_position_id': self.fiscal_position_id.id,
'contract_id':self.id,
} }
return invoice_vals
@api.multi
def action_contract_send(self): def action_contract_send(self):
self.ensure_one() self.ensure_one()
template = self.env.ref('contract.email_contract_template', False) template = self.env.ref('contract.email_contract_template', False)
@ -335,31 +311,24 @@ class ContractContract(models.Model):
# taken from the invoice's journal in _onchange_product_id # taken from the invoice's journal in _onchange_product_id
# This code is not in finalize_creation_from_contract because it's # This code is not in finalize_creation_from_contract because it's
# not possible to create an invoice line with no account # not possible to create an invoice line with no account
new_invoice = self.env['account.invoice'].new(invoice_values)
for invoice_line in new_invoice.invoice_line_ids:
new_invoice = self.env['account.move'].new(invoice_values)
for invoice_line in new_invoice.line_ids:
name = invoice_line.name name = invoice_line.name
account_analytic_id = invoice_line.account_analytic_id
analytic_account_id = invoice_line.analytic_account_id
price_unit = invoice_line.price_unit price_unit = invoice_line.price_unit
invoice_line.invoice_id = new_invoice invoice_line.invoice_id = new_invoice
invoice_line._onchange_product_id() invoice_line._onchange_product_id()
invoice_line.update( invoice_line.update(
{ {
'name': name,
'account_analytic_id': account_analytic_id,
'price_unit': price_unit,
}
)
'name': name,
'analytic_account_id': analytic_account_id,
'price_unit': price_unit,
} )
return new_invoice._convert_to_record(new_invoice._cache)
return new_invoice._convert_to_write(new_invoice._cache) return new_invoice._convert_to_write(new_invoice._cache)
@api.model
def _finalize_invoice_creation(self, invoices):
for invoice in invoices:
payment_term = invoice.payment_term_id
fiscal_position = invoice.fiscal_position_id
invoice._onchange_partner_id()
invoice.payment_term_id = payment_term
invoice.fiscal_position_id = fiscal_position
invoices.compute_taxes()
@api.model @api.model
def _finalize_and_create_invoices(self, invoices_values): def _finalize_and_create_invoices(self, invoices_values):
@ -369,7 +338,7 @@ class ContractContract(models.Model):
- creates the invoices - creates the invoices
- finalizes the created invoices (onchange's, tax computation...) - finalizes the created invoices (onchange's, tax computation...)
:param invoices_values: list of dictionaries (invoices values) :param invoices_values: list of dictionaries (invoices values)
:return: created invoices (account.invoice)
:return: created invoices (account.move)
""" """
if isinstance(invoices_values, dict): if isinstance(invoices_values, dict):
invoices_values = [invoices_values] invoices_values = [invoices_values]
@ -378,8 +347,7 @@ class ContractContract(models.Model):
final_invoices_values.append( final_invoices_values.append(
self._finalize_invoice_values(invoice_values) self._finalize_invoice_values(invoice_values)
) )
invoices = self.env['account.invoice'].create(final_invoices_values)
self._finalize_invoice_creation(invoices)
invoices = self.env['account.move'].create(final_invoices_values)
return invoices return invoices
@api.model @api.model
@ -396,7 +364,6 @@ class ContractContract(models.Model):
domain.extend([('recurring_next_date', '<=', date_ref)]) domain.extend([('recurring_next_date', '<=', date_ref)])
return domain return domain
@api.multi
def _get_lines_to_invoice(self, date_ref): def _get_lines_to_invoice(self, date_ref):
""" """
This method fetches and returns the lines to invoice on the contract This method fetches and returns the lines to invoice on the contract
@ -411,7 +378,6 @@ class ContractContract(models.Model):
and l.recurring_next_date <= date_ref and l.recurring_next_date <= date_ref
) )
@api.multi
def _prepare_recurring_invoices_values(self, date_ref=False): def _prepare_recurring_invoices_values(self, date_ref=False):
""" """
This method builds the list of invoices values to create, based on This method builds the list of invoices values to create, based on
@ -433,9 +399,7 @@ class ContractContract(models.Model):
invoice_values = contract._prepare_invoice(date_ref) invoice_values = contract._prepare_invoice(date_ref)
for line in contract_lines: for line in contract_lines:
invoice_values.setdefault('invoice_line_ids', []) invoice_values.setdefault('invoice_line_ids', [])
invoice_line_values = line._prepare_invoice_line(
invoice_id=False
)
invoice_line_values = line._prepare_invoice_line() # function in contact_line
if invoice_line_values: if invoice_line_values:
invoice_values['invoice_line_ids'].append( invoice_values['invoice_line_ids'].append(
(0, 0, invoice_line_values) (0, 0, invoice_line_values)
@ -444,7 +408,6 @@ class ContractContract(models.Model):
contract_lines._update_recurring_next_date() contract_lines._update_recurring_next_date()
return invoices_values return invoices_values
@api.multi
def recurring_create_invoice(self): def recurring_create_invoice(self):
""" """
This method triggers the creation of the next invoices of the contracts This method triggers the creation of the next invoices of the contracts
@ -452,7 +415,6 @@ class ContractContract(models.Model):
""" """
return self._recurring_create_invoice() return self._recurring_create_invoice()
@api.multi
def _recurring_create_invoice(self, date_ref=False): def _recurring_create_invoice(self, date_ref=False):
invoices_values = self._prepare_recurring_invoices_values(date_ref) invoices_values = self._prepare_recurring_invoices_values(date_ref)
return self._finalize_and_create_invoices(invoices_values) return self._finalize_and_create_invoices(invoices_values)

334
contract/models/contract_line.py

@ -16,21 +16,15 @@ class ContractLine(models.Model):
_description = "Contract Line" _description = "Contract Line"
_inherit = 'contract.abstract.contract.line' _inherit = 'contract.abstract.contract.line'
sequence = fields.Integer(
string="Sequence",
)
contract_id = fields.Many2one(
comodel_name='contract.contract',
string='Contract',
required=True,
index=True,
auto_join=True,
ondelete='cascade',
)
display_type = fields.Selection([
('line_section', "Section"),
('line_note', "Note")], default=False, help="Technical field for UX purpose.")
analytic_account_id = fields.Many2one( analytic_account_id = fields.Many2one(
string="Analytic account", string="Analytic account",
comodel_name='account.analytic.account', comodel_name='account.analytic.account',
) )
date_start = fields.Date( date_start = fields.Date(
string='Date Start', string='Date Start',
required=True, required=True,
@ -41,6 +35,14 @@ class ContractLine(models.Model):
last_date_invoiced = fields.Date( last_date_invoiced = fields.Date(
string='Last Date Invoiced', readonly=True, copy=False string='Last Date Invoiced', readonly=True, copy=False
) )
next_period_date_start = fields.Date(
string='Next Period Start',
compute='_compute_next_period_date_start',
)
next_period_date_end = fields.Date(
string='Next Period End',
compute='_compute_next_period_date_end',
)
termination_notice_date = fields.Date( termination_notice_date = fields.Date(
string='Termination notice date', string='Termination notice date',
compute="_compute_termination_notice_date", compute="_compute_termination_notice_date",
@ -112,7 +114,6 @@ class ContractLine(models.Model):
default=True, default=True,
) )
@api.multi
@api.depends( @api.depends(
'date_end', 'date_end',
'termination_notice_rule_type', 'termination_notice_rule_type',
@ -129,7 +130,6 @@ class ContractLine(models.Model):
) )
) )
@api.multi
@api.depends('is_canceled', 'date_start', 'date_end', 'is_auto_renew') @api.depends('is_canceled', 'date_start', 'date_end', 'is_auto_renew')
def _compute_state(self): def _compute_state(self):
today = fields.Date.context_today(self) today = fields.Date.context_today(self)
@ -361,20 +361,140 @@ class ContractLine(models.Model):
date_start, date_start,
recurring_invoicing_type, recurring_invoicing_type,
recurring_rule_type, recurring_rule_type,
recurring_interval
):
# deprecated method for backward compatibility
return self.get_next_invoice_date(
date_start,
recurring_invoicing_type,
self._get_default_recurring_invoicing_offset(
recurring_invoicing_type, recurring_rule_type
),
recurring_rule_type,
recurring_interval,
max_date_end=False,
)
@api.model
def get_next_invoice_date(
self,
next_period_date_start,
recurring_invoicing_type,
recurring_invoicing_offset,
recurring_rule_type,
recurring_interval, recurring_interval,
max_date_end,
): ):
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
next_period_date_end = self.get_next_period_date_end(
next_period_date_start,
recurring_rule_type,
recurring_interval,
max_date_end=max_date_end,
) )
if not next_period_date_end:
return False
if recurring_invoicing_type == 'pre-paid':
recurring_next_date = (
next_period_date_start
+ relativedelta(days=recurring_invoicing_offset)
)
else: # post-paid
recurring_next_date = (
next_period_date_end
+ relativedelta(days=recurring_invoicing_offset)
)
return recurring_next_date
@api.model
def get_next_period_date_end(
self,
next_period_date_start,
recurring_rule_type,
recurring_interval,
max_date_end,
next_invoice_date=False,
recurring_invoicing_type=False,
recurring_invoicing_offset=False,
):
"""Compute the end date for the next period.
The next period normally depends on recurrence options only.
It is however possible to provide it a next invoice date, in
which case this method can adjust the next period based on that
too. In that scenario it required the invoicing type and offset
arguments.
"""
if not next_period_date_start:
return False
if max_date_end and next_period_date_start > max_date_end:
# start is past max date end: there is no next period
return False
if not next_invoice_date:
# regular algorithm
next_period_date_end = (
next_period_date_start
+ self.get_relative_delta(
recurring_rule_type, recurring_interval
)
- relativedelta(days=1)
)
else:
# special algorithm when the next invoice date is forced
if recurring_invoicing_type == 'pre-paid':
next_period_date_end = (
next_invoice_date
- relativedelta(days=recurring_invoicing_offset)
+ self.get_relative_delta(
recurring_rule_type, recurring_interval
)
- relativedelta(days=1)
)
else: # post-paid
next_period_date_end = (
next_invoice_date
- relativedelta(days=recurring_invoicing_offset)
)
if max_date_end and next_period_date_end > max_date_end:
# end date is past max_date_end: trim it
next_period_date_end = max_date_end
return next_period_date_end
@api.depends('last_date_invoiced', 'date_start', 'date_end')
def _compute_next_period_date_start(self):
for rec in self:
if rec.last_date_invoiced:
next_period_date_start = (
rec.last_date_invoiced + relativedelta(days=1)
)
else:
next_period_date_start = rec.date_start
if rec.date_end and next_period_date_start > rec.date_end:
next_period_date_start = False
rec.next_period_date_start = next_period_date_start
@api.depends(
'next_period_date_start',
'recurring_invoicing_type',
'recurring_invoicing_offset',
'recurring_rule_type',
'recurring_interval',
'date_end',
'recurring_next_date',
)
def _compute_next_period_date_end(self):
for rec in self:
rec.next_period_date_end = self.get_next_period_date_end(
rec.next_period_date_start,
rec.recurring_rule_type,
rec.recurring_interval,
max_date_end=rec.date_end,
next_invoice_date=rec.recurring_next_date,
recurring_invoicing_type=rec.recurring_invoicing_type,
recurring_invoicing_offset=rec.recurring_invoicing_offset,
)
@api.model @api.model
def compute_first_date_end(
def _get_first_date_end(
self, date_start, auto_renew_rule_type, auto_renew_interval self, date_start, auto_renew_rule_type, auto_renew_interval
): ):
return ( return (
@ -404,17 +524,20 @@ class ContractLine(models.Model):
@api.onchange( @api.onchange(
'date_start', 'date_start',
'date_end',
'recurring_invoicing_type', 'recurring_invoicing_type',
'recurring_rule_type', 'recurring_rule_type',
'recurring_interval', 'recurring_interval',
) )
def _onchange_date_start(self): def _onchange_date_start(self):
for rec in self.filtered('date_start'): for rec in self.filtered('date_start'):
rec.recurring_next_date = self._compute_first_recurring_next_date(
rec.recurring_next_date = self.get_next_invoice_date(
rec.date_start, rec.date_start,
rec.recurring_invoicing_type, rec.recurring_invoicing_type,
rec.recurring_invoicing_offset,
rec.recurring_rule_type, rec.recurring_rule_type,
rec.recurring_interval, rec.recurring_interval,
max_date_end=rec.date_end,
) )
@api.constrains('is_canceled', 'is_auto_renew') @api.constrains('is_canceled', 'is_auto_renew')
@ -499,70 +622,61 @@ class ContractLine(models.Model):
rec.recurring_next_date rec.recurring_next_date
) )
@api.multi
def _prepare_invoice_line(self, invoice_id=False):
def _prepare_invoice_line(self):
"""
Prepare the dict of values to create the new invoice line for a contact ( taken from sale/model/sale.py.
"""
self.ensure_one() self.ensure_one()
dates = self._get_period_to_invoice( dates = self._get_period_to_invoice(
self.last_date_invoiced, self.recurring_next_date self.last_date_invoiced, self.recurring_next_date
) )
invoice_line_vals = {
if self.contract_id.contract_type =='sale':
account_type = 'income'
else:
account_type = 'expense'
return {
'sequence': self.sequence,
'name': self._insert_markers(dates[0], dates[1]),
'account_id':self.product_id.product_tmpl_id._get_product_accounts()[account_type].id,
'product_id': self.product_id.id, 'product_id': self.product_id.id,
'quantity': self._get_quantity_to_invoice(*dates),
'uom_id': self.uom_id.id,
'product_uom_id': self.uom_id.id,
'quantity': self._get_quantity_to_invoice(*dates),#self.quantity
'discount': self.discount, 'discount': self.discount,
'contract_line_id': self.id,
'price_unit': self.price_unit,
'tax_ids': [(6, 0, self.tax_id.ids)],
'analytic_account_id': self.analytic_account_id.id,
# 'analytic_tag_ids': [(6, 0, self.product_id.analytic_tag_ids.ids)],
'contract_line_id': [(4, self.id)],
} }
if invoice_id:
invoice_line_vals['invoice_id'] = invoice_id.id
invoice_line = self.env['account.invoice.line'].new(invoice_line_vals)
# 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
name = self._insert_markers(dates[0], dates[1])
invoice_line_vals.update(
{
'name': name,
'account_analytic_id': self.analytic_account_id.id,
'price_unit': self.price_unit,
}
)
return invoice_line_vals
@api.multi
def _get_period_to_invoice( def _get_period_to_invoice(
self, last_date_invoiced, recurring_next_date, stop_at_date_end=True self, last_date_invoiced, recurring_next_date, stop_at_date_end=True
): ):
# TODO this method can now be removed, since
# TODO self.next_period_date_start/end have the same values
self.ensure_one() self.ensure_one()
first_date_invoiced = False
if not recurring_next_date: if not recurring_next_date:
return first_date_invoiced, last_date_invoiced, recurring_next_date
return False, False, False
first_date_invoiced = ( first_date_invoiced = (
last_date_invoiced + relativedelta(days=1) last_date_invoiced + relativedelta(days=1)
if last_date_invoiced if last_date_invoiced
else self.date_start else self.date_start
) )
if self.recurring_rule_type == 'monthlylastday':
last_date_invoiced = recurring_next_date
else:
if self.recurring_invoicing_type == 'pre-paid':
last_date_invoiced = (
recurring_next_date
+ self.get_relative_delta(
self.recurring_rule_type, self.recurring_interval
)
- relativedelta(days=1)
)
else:
last_date_invoiced = recurring_next_date - relativedelta(
days=1
)
if stop_at_date_end:
if self.date_end and self.date_end < last_date_invoiced:
last_date_invoiced = self.date_end
last_date_invoiced = self.get_next_period_date_end(
first_date_invoiced,
self.recurring_rule_type,
self.recurring_interval,
max_date_end=(self.date_end if stop_at_date_end else False),
next_invoice_date=recurring_next_date,
recurring_invoicing_type=self.recurring_invoicing_type,
recurring_invoicing_offset=self.recurring_invoicing_offset,
)
return first_date_invoiced, last_date_invoiced, recurring_next_date return first_date_invoiced, last_date_invoiced, recurring_next_date
@api.multi
def _insert_markers(self, first_date_invoiced, last_date_invoiced): def _insert_markers(self, first_date_invoiced, last_date_invoiced):
self.ensure_one() self.ensure_one()
lang_obj = self.env['res.lang'] lang_obj = self.env['res.lang']
@ -577,28 +691,22 @@ class ContractLine(models.Model):
name = name.replace('#END#', last_date_invoiced.strftime(date_format)) name = name.replace('#END#', last_date_invoiced.strftime(date_format))
return name return name
@api.multi
def _update_recurring_next_date(self): def _update_recurring_next_date(self):
for rec in self: for rec in self:
old_date = rec.recurring_next_date
new_date = old_date + self.get_relative_delta(
rec.recurring_rule_type, rec.recurring_interval
last_date_invoiced = rec.next_period_date_end
recurring_next_date = rec.get_next_invoice_date(
last_date_invoiced + relativedelta(days=1),
rec.recurring_invoicing_type,
rec.recurring_invoicing_offset,
rec.recurring_rule_type,
rec.recurring_interval,
max_date_end=rec.date_end,
) )
if rec.recurring_rule_type == 'monthlylastday':
last_date_invoiced = old_date
elif rec.recurring_invoicing_type == 'post-paid':
last_date_invoiced = old_date - relativedelta(days=1)
elif rec.recurring_invoicing_type == 'pre-paid':
last_date_invoiced = new_date - relativedelta(days=1)
if rec.date_end and last_date_invoiced >= rec.date_end:
rec.last_date_invoiced = rec.date_end
rec.recurring_next_date = False
else:
rec.last_date_invoiced = last_date_invoiced
rec.recurring_next_date = new_date
rec.write({
"recurring_next_date": recurring_next_date,
"last_date_invoiced": last_date_invoiced,
})
@api.multi
def _init_last_date_invoiced(self): def _init_last_date_invoiced(self):
"""Used to init last_date_invoiced for migration purpose""" """Used to init last_date_invoiced for migration purpose"""
for rec in self: for rec in self:
@ -609,8 +717,9 @@ class ContractLine(models.Model):
last_date_invoiced = ( last_date_invoiced = (
rec.recurring_next_date rec.recurring_next_date
- self.get_relative_delta( - self.get_relative_delta(
rec.recurring_rule_type, rec.recurring_interval
rec.recurring_rule_type, rec.recurring_interval - 1
) )
- relativedelta(days=1)
) )
elif rec.recurring_invoicing_type == 'post-paid': elif rec.recurring_invoicing_type == 'post-paid':
last_date_invoiced = ( last_date_invoiced = (
@ -618,12 +727,19 @@ class ContractLine(models.Model):
- self.get_relative_delta( - self.get_relative_delta(
rec.recurring_rule_type, rec.recurring_interval rec.recurring_rule_type, rec.recurring_interval
) )
) - relativedelta(days=1)
- relativedelta(days=1)
)
if last_date_invoiced > rec.date_start: if last_date_invoiced > rec.date_start:
rec.last_date_invoiced = last_date_invoiced rec.last_date_invoiced = last_date_invoiced
@api.model
@api.model @api.model
def get_relative_delta(self, recurring_rule_type, interval): def get_relative_delta(self, recurring_rule_type, interval):
"""Return a relativedelta for one period.
When added to the first day of the period,
it gives the first day of the next period.
"""
if recurring_rule_type == 'daily': if recurring_rule_type == 'daily':
return relativedelta(days=interval) return relativedelta(days=interval)
elif recurring_rule_type == 'weekly': elif recurring_rule_type == 'weekly':
@ -631,11 +747,11 @@ class ContractLine(models.Model):
elif recurring_rule_type == 'monthly': elif recurring_rule_type == 'monthly':
return relativedelta(months=interval) return relativedelta(months=interval)
elif recurring_rule_type == 'monthlylastday': elif recurring_rule_type == 'monthlylastday':
return relativedelta(months=interval, day=31)
return relativedelta(months=interval, day=1)
else: else:
return relativedelta(years=interval) return relativedelta(years=interval)
@api.multi
def _delay(self, delay_delta): def _delay(self, delay_delta):
""" """
Delay a contract line Delay a contract line
@ -651,17 +767,24 @@ class ContractLine(models.Model):
) )
) )
new_date_start = rec.date_start + delay_delta new_date_start = rec.date_start + delay_delta
rec.recurring_next_date = self._compute_first_recurring_next_date(
if rec.date_end:
new_date_end = rec.date_end + delay_delta
else:
new_date_end = False
new_recurring_next_date = self.get_next_invoice_date(
new_date_start, new_date_start,
rec.recurring_invoicing_type, rec.recurring_invoicing_type,
rec.recurring_invoicing_offset,
rec.recurring_rule_type, rec.recurring_rule_type,
rec.recurring_interval, rec.recurring_interval,
max_date_end=new_date_end
) )
if rec.date_end:
rec.date_end += delay_delta
rec.date_start = new_date_start
rec.write({
"date_start": new_date_start,
"date_end": new_date_end,
"recurring_next_date": new_recurring_next_date,
})
@api.multi
def stop(self, date_end, manual_renew_needed=False, post_message=True): def stop(self, date_end, manual_renew_needed=False, post_message=True):
""" """
Put date_end on contract line Put date_end on contract line
@ -706,17 +829,18 @@ class ContractLine(models.Model):
) )
return True return True
@api.multi
def _prepare_value_for_plan_successor( def _prepare_value_for_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
): ):
self.ensure_one() self.ensure_one()
if not recurring_next_date: if not recurring_next_date:
recurring_next_date = self._compute_first_recurring_next_date(
recurring_next_date = self.get_next_invoice_date(
date_start, date_start,
self.recurring_invoicing_type, self.recurring_invoicing_type,
self.recurring_invoicing_offset,
self.recurring_rule_type, self.recurring_rule_type,
self.recurring_interval, self.recurring_interval,
max_date_end=date_end,
) )
new_vals = self.read()[0] new_vals = self.read()[0]
new_vals.pop("id", None) new_vals.pop("id", None)
@ -729,7 +853,6 @@ class ContractLine(models.Model):
values['predecessor_contract_line_id'] = self.id values['predecessor_contract_line_id'] = self.id
return values return values
@api.multi
def plan_successor( def plan_successor(
self, self,
date_start, date_start,
@ -777,7 +900,6 @@ class ContractLine(models.Model):
rec.contract_id.message_post(body=msg) rec.contract_id.message_post(body=msg)
return contract_line return contract_line
@api.multi
def stop_plan_successor(self, date_start, date_end, is_auto_renew): def stop_plan_successor(self, date_start, date_end, is_auto_renew):
""" """
Stop a contract line for a defined period and start it later Stop a contract line for a defined period and start it later
@ -885,7 +1007,6 @@ class ContractLine(models.Model):
rec.contract_id.message_post(body=msg) rec.contract_id.message_post(body=msg)
return contract_line return contract_line
@api.multi
def cancel(self): def cancel(self):
if not all(self.mapped('is_cancel_allowed')): if not all(self.mapped('is_cancel_allowed')):
raise ValidationError(_('Cancel not allowed for this line')) raise ValidationError(_('Cancel not allowed for this line'))
@ -906,7 +1027,6 @@ class ContractLine(models.Model):
) )
return self.write({'is_canceled': True, 'is_auto_renew': False}) return self.write({'is_canceled': True, 'is_auto_renew': False})
@api.multi
def uncancel(self, recurring_next_date): def uncancel(self, recurring_next_date):
if not all(self.mapped('is_un_cancel_allowed')): if not all(self.mapped('is_un_cancel_allowed')):
raise ValidationError(_('Un-cancel not allowed for this line')) raise ValidationError(_('Un-cancel not allowed for this line'))
@ -931,7 +1051,6 @@ class ContractLine(models.Model):
rec.recurring_next_date = recurring_next_date rec.recurring_next_date = recurring_next_date
return True return True
@api.multi
def action_uncancel(self): def action_uncancel(self):
self.ensure_one() self.ensure_one()
context = { context = {
@ -953,7 +1072,6 @@ class ContractLine(models.Model):
'context': context, 'context': context,
} }
@api.multi
def action_plan_successor(self): def action_plan_successor(self):
self.ensure_one() self.ensure_one()
context = { context = {
@ -975,7 +1093,6 @@ class ContractLine(models.Model):
'context': context, 'context': context,
} }
@api.multi
def action_stop(self): def action_stop(self):
self.ensure_one() self.ensure_one()
context = { context = {
@ -997,7 +1114,6 @@ class ContractLine(models.Model):
'context': context, 'context': context,
} }
@api.multi
def action_stop_plan_successor(self): def action_stop_plan_successor(self):
self.ensure_one() self.ensure_one()
context = { context = {
@ -1019,16 +1135,14 @@ class ContractLine(models.Model):
'context': context, 'context': context,
} }
@api.multi
def _get_renewal_dates(self): def _get_renewal_dates(self):
self.ensure_one() self.ensure_one()
date_start = self.date_end + relativedelta(days=1) date_start = self.date_end + relativedelta(days=1)
date_end = self.compute_first_date_end(
date_end = self._get_first_date_end(
date_start, self.auto_renew_rule_type, self.auto_renew_interval date_start, self.auto_renew_rule_type, self.auto_renew_interval
) )
return date_start, date_end return date_start, date_end
@api.multi
def renew(self): def renew(self):
res = self.env['contract.line'] res = self.env['contract.line']
for rec in self: for rec in self:
@ -1055,7 +1169,6 @@ class ContractLine(models.Model):
rec.contract_id.message_post(body=msg) rec.contract_id.message_post(body=msg)
return res return res
@api.model
def _contract_line_to_renew_domain(self): def _contract_line_to_renew_domain(self):
return [ return [
('is_auto_renew', '=', True), ('is_auto_renew', '=', True),
@ -1063,13 +1176,11 @@ class ContractLine(models.Model):
('termination_notice_date', '<=', fields.Date.context_today(self)), ('termination_notice_date', '<=', fields.Date.context_today(self)),
] ]
@api.model
def cron_renew_contract_line(self): def cron_renew_contract_line(self):
domain = self._contract_line_to_renew_domain() domain = self._contract_line_to_renew_domain()
to_renew = self.search(domain) to_renew = self.search(domain)
to_renew.renew() to_renew.renew()
@api.model
def fields_view_get( def fields_view_get(
self, view_id=None, view_type='form', toolbar=False, submenu=False self, view_id=None, view_type='form', toolbar=False, submenu=False
): ):
@ -1091,7 +1202,6 @@ class ContractLine(models.Model):
view_id, view_type, toolbar, submenu view_id, view_type, toolbar, submenu
) )
@api.multi
def unlink(self): def unlink(self):
"""stop unlink uncnacled lines""" """stop unlink uncnacled lines"""
if not all(self.mapped('is_canceled')): if not all(self.mapped('is_canceled')):
@ -1100,9 +1210,9 @@ class ContractLine(models.Model):
) )
return super().unlink() return super().unlink()
@api.multi
def _get_quantity_to_invoice( def _get_quantity_to_invoice(
self, period_first_date, period_last_date, invoice_date self, period_first_date, period_last_date, invoice_date
): ):
self.ensure_one() self.ensure_one()
return self.quantity return self.quantity

1
contract/models/contract_template_line.py

@ -20,5 +20,4 @@ class ContractTemplateLine(models.Model):
comodel_name='contract.template', comodel_name='contract.template',
required=True, required=True,
ondelete='cascade', ondelete='cascade',
oldname='analytic_account_id',
) )

4
contract/tests/test_contract.py

@ -1322,7 +1322,7 @@ class TestContract(TestContractBase):
for i in range(10): for i in range(10):
contracts |= self.contract.copy() contracts |= self.contract.copy()
self.env['contract.contract'].cron_recurring_create_invoice() self.env['contract.contract'].cron_recurring_create_invoice()
invoice_lines = self.env['account.invoice.line'].search(
invoice_lines = self.env['account.move.line'].search(
[('contract_line_id', 'in', [('contract_line_id', 'in',
contracts.mapped('contract_line_ids').ids)] contracts.mapped('contract_line_ids').ids)]
) )
@ -1809,7 +1809,7 @@ class TestContract(TestContractBase):
self.assertEqual(self.contract.invoice_count, 3) self.assertEqual(self.contract.invoice_count, 3)
def test_contract_count_invoice(self): def test_contract_count_invoice(self):
invoices = self.env['account.invoice']
invoices = self.env['account.move']
invoices |= self.contract.recurring_create_invoice() invoices |= self.contract.recurring_create_invoice()
invoices |= self.contract.recurring_create_invoice() invoices |= self.contract.recurring_create_invoice()
invoices |= self.contract.recurring_create_invoice() invoices |= self.contract.recurring_create_invoice()

6
contract/views/abstract_contract_line.xml

@ -18,6 +18,8 @@
<field name="specific_price" invisible="1"/> <field name="specific_price" invisible="1"/>
<field colspan="2" name="price_unit" <field colspan="2" name="price_unit"
attrs="{'readonly': [('automatic_price', '=', True)]}"/> attrs="{'readonly': [('automatic_price', '=', True)]}"/>
<field colspan="2" name="tax_id" widget='many2many_tags'/>
<field colspan="2" name="discount" groups="base.group_no_one"/> <field colspan="2" name="discount" groups="base.group_no_one"/>
</group> </group>
<group col="4"> <group col="4">
@ -59,8 +61,8 @@
</div> </div>
</group> </group>
<group> <group>
<field name="recurring_invoicing_type"
attrs="{'invisible': [('recurring_rule_type', '=', 'monthlylastday')]}"/>
<field name="recurring_invoicing_type"/>
<field name="recurring_invoicing_offset"/>
</group> </group>
</group> </group>
</sheet> </sheet>

28
contract/views/account_move.xml

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Carlos Dauden <carlos.dauden@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="contract_in_view_move_form" model="ir.ui.view">
<field name="inherit_id" ref="account.view_move_form" />
<field name="model">account.move</field>
<field name="groups_id" eval="[(4, ref('account.group_account_invoice'))]"/>
<field type="xml" name="arch">
<field name="ref" position="after">
<field name="contract_id" />
</field>
</field>
</record>
<record id="contract_view_account_invoice_filter" model="ir.ui.view">
<field name="inherit_id" ref="account.view_account_invoice_filter" />
<field name="model">account.move</field>
<field type="xml" name="arch">
<filter name="duedate" position="after">
<filter string="Contract" name="contract" context="{'group_by': 'contract_id'}"/>
</filter>
</field>
</record>
</odoo>

28
contract/views/contract.xml

@ -33,13 +33,20 @@
widget="statinfo"/> widget="statinfo"/>
</button> </button>
</div> </div>
<div class="oe_title">
<label for="name" string="Contract Name"
<div class="col-xs-6">
<label for="name" string="Contract Name, "
class="oe_edit_only"/> class="oe_edit_only"/>
<label for="name" string="From Date"
class="oe_edit_only"/>
<h3> <h3>
<field name="name" class="oe_inline" <field name="name" class="oe_inline"
placeholder="e.g. Contract XYZ"/> placeholder="e.g. Contract XYZ"/>
<field name="date" class="oe_inline" style="text-indent: 2em;"/>
</h3> </h3>
</div> </div>
<group name="main"> <group name="main">
<group> <group>
@ -55,6 +62,9 @@
<field name="contract_type" invisible="1" <field name="contract_type" invisible="1"
required="1"/> required="1"/>
<field name="fiscal_position_id"/> <field name="fiscal_position_id"/>
<field name="incoterm_id"/>
<field name="payment_term_id"/>
</group> </group>
</group> </group>
@ -65,6 +75,7 @@
</group> </group>
<group> <group>
<field name="pricelist_id"/> <field name="pricelist_id"/>
<field name="currency_id"/>
<field name="date_end"/> <field name="date_end"/>
</group> </group>
</group> </group>
@ -125,8 +136,8 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="partner_id" position="attributes"> <field name="partner_id" position="attributes">
<attribute name="string">Customer</attribute> <attribute name="string">Customer</attribute>
<attribute name="domain">[('customer', '=', True)]</attribute>
<attribute name="context">{'default_customer': True, 'default_supplier': False}</attribute>
<attribute name="domain">[('customer_rank','&gt;=','1')]</attribute>
<attribute name="context">{'default_customer_rank': 1,'res_partner_search_mode': 'customer'}</attribute>
</field> </field>
<field name="journal_id" position="attributes"> <field name="journal_id" position="attributes">
<attribute name="domain">[('type', '=', 'sale'),('company_id', '=', company_id)]</attribute> <attribute name="domain">[('type', '=', 'sale'),('company_id', '=', company_id)]</attribute>
@ -144,8 +155,8 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="partner_id" position="attributes"> <field name="partner_id" position="attributes">
<attribute name="string">Supplier</attribute> <attribute name="string">Supplier</attribute>
<attribute name="domain">[('supplier', '=', True)]</attribute>
<attribute name="context">{'default_customer': False, 'default_supplier': True}</attribute>
<attribute name="domain">[('supplier_rank','&gt;=','1')]</attribute>
<attribute name="context">{'default_supplier_rank': 1,'res_partner_search_mode': 'supplier'}</attribute>
</field> </field>
<field name="journal_id" position="attributes"> <field name="journal_id" position="attributes">
<attribute name="domain">[('type', '=', 'purchase'),('company_id', '=', company_id)]</attribute> <attribute name="domain">[('type', '=', 'purchase'),('company_id', '=', company_id)]</attribute>
@ -212,6 +223,9 @@
domain="[]" domain="[]"
context="{'group_by':'date_end'}" context="{'group_by':'date_end'}"
/> />
<filter name="group_by_pricelist" string="Pricelist" context="{'group_by':'pricelist_id'}" domain="[]" />
<filter name="group_by_currency" string="Currency" context="{'group_by':'currency_id'}" domain="[]" />
</group> </group>
</search> </search>
</field> </field>
@ -221,7 +235,6 @@
<record id="action_customer_contract" model="ir.actions.act_window"> <record id="action_customer_contract" model="ir.actions.act_window">
<field name="name">Customer Contracts</field> <field name="name">Customer Contracts</field>
<field name="res_model">contract.contract</field> <field name="res_model">contract.contract</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field> <field name="view_mode">tree,form</field>
<field name="domain">[('contract_type', '=', 'sale')]</field> <field name="domain">[('contract_type', '=', 'sale')]</field>
<field name="context">{'is_contract':1, <field name="context">{'is_contract':1,
@ -260,7 +273,6 @@
<record id="action_supplier_contract" model="ir.actions.act_window"> <record id="action_supplier_contract" model="ir.actions.act_window">
<field name="name">Supplier Contracts</field> <field name="name">Supplier Contracts</field>
<field name="res_model">contract.contract</field> <field name="res_model">contract.contract</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field> <field name="view_mode">tree,form</field>
<field name="domain">[('contract_type', '=', 'purchase')]</field> <field name="domain">[('contract_type', '=', 'purchase')]</field>
<field name="context">{'is_contract':1, <field name="context">{'is_contract':1,

8
contract/views/contract_line.xml

@ -10,11 +10,16 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<header position="inside"> <header position="inside">
<field name="state" widget="statusbar"/> <field name="state" widget="statusbar"/>
</header> </header>
<group name="recurrence_info" position="inside"> <group name="recurrence_info" position="inside">
<group> <group>
<field name="company_id" invisible='False'/>
<field name="currency_id" invisible='False'/>
<field name="create_invoice_visibility" invisible="1"/> <field name="create_invoice_visibility" invisible="1"/>
<field name="date_start" required="1"/> <field name="date_start" required="1"/>
<field name="next_period_date_start"/>
<field name="recurring_next_date"/> <field name="recurring_next_date"/>
</group> </group>
<group> <group>
@ -81,6 +86,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree decoration-muted="is_canceled" <tree decoration-muted="is_canceled"
decoration-info="create_invoice_visibility and not is_canceled"> decoration-info="create_invoice_visibility and not is_canceled">
<field name="company_id" invisible='True'/>
<field name="sequence" widget="handle"/> <field name="sequence" widget="handle"/>
<field name="product_id"/> <field name="product_id"/>
<field name="name"/> <field name="name"/>
@ -90,6 +96,8 @@
<field name="automatic_price"/> <field name="automatic_price"/>
<field name="price_unit" <field name="price_unit"
attrs="{'readonly': [('automatic_price', '=', True)]}"/> attrs="{'readonly': [('automatic_price', '=', True)]}"/>
<field name="tax_id" widget="many2many_tags" options="{'no_create': True}" context="{'search_view_ref': 'account.account_tax_view_search'}" domain="[('type_tax_use','=','sale'),('company_id','=',parent.company_id)]"/>
<field name="specific_price" <field name="specific_price"
invisible="1"/> invisible="1"/>
<field name="discount" <field name="discount"

1
contract/views/contract_template.xml

@ -90,7 +90,6 @@
<record id="contract_template_action" model="ir.actions.act_window"> <record id="contract_template_action" model="ir.actions.act_window">
<field name="name">Contract Templates</field> <field name="name">Contract Templates</field>
<field name="res_model">contract.template</field> <field name="res_model">contract.template</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field> <field name="view_mode">tree,form</field>
<field name="search_view_id" ref="contract_template_search_view"/> <field name="search_view_id" ref="contract_template_search_view"/>
<field name="help" type="html"> <field name="help" type="html">

6
contract/views/res_partner_view.xml

@ -9,15 +9,17 @@
<field name="groups_id" eval="[(4, ref('account.group_account_invoice'))]"/> <field name="groups_id" eval="[(4, ref('account.group_account_invoice'))]"/>
<field type="xml" name="arch"> <field type="xml" name="arch">
<xpath expr="//div[@name='button_box']" position="inside"> <xpath expr="//div[@name='button_box']" position="inside">
<field name="customer_rank" invisible="1"/>
<field name="supplier_rank" invisible="1"/>
<button name="act_show_contract" type="object" class="oe_stat_button" <button name="act_show_contract" type="object" class="oe_stat_button"
icon="fa-book" context="{'default_contract_type': 'sale', 'contract_type': 'sale'}" icon="fa-book" context="{'default_contract_type': 'sale', 'contract_type': 'sale'}"
attrs="{'invisible': [('customer','=',False)]}"
attrs="{'invisible': [('customer_rank','=',0)]}"
help="Show the sale contracts for this partner"> help="Show the sale contracts for this partner">
<field name="sale_contract_count" widget="statinfo" string="Sale Contracts"/> <field name="sale_contract_count" widget="statinfo" string="Sale Contracts"/>
</button> </button>
<button name="act_show_contract" type="object" class="oe_stat_button" <button name="act_show_contract" type="object" class="oe_stat_button"
icon="fa-book" context="{'default_contract_type': 'purchase', 'contract_type': 'purchase'}" icon="fa-book" context="{'default_contract_type': 'purchase', 'contract_type': 'purchase'}"
attrs="{'invisible': [('supplier','=',False)]}"
attrs="{'invisible': [('supplier_rank','=',0)]}"
help="Show the purchase contracts for this partner"> help="Show the purchase contracts for this partner">
<field name="purchase_contract_count" widget="statinfo" string="Purchase Contracts"/> <field name="purchase_contract_count" widget="statinfo" string="Purchase Contracts"/>
</button> </button>

4
contract/wizards/contract_line_wizard.py

@ -27,7 +27,6 @@ class ContractLineWizard(models.TransientModel):
index=True, index=True,
) )
@api.multi
def stop(self): def stop(self):
for wizard in self: for wizard in self:
wizard.contract_line_id.stop( wizard.contract_line_id.stop(
@ -35,7 +34,6 @@ class ContractLineWizard(models.TransientModel):
) )
return True return True
@api.multi
def plan_successor(self): def plan_successor(self):
for wizard in self: for wizard in self:
wizard.contract_line_id.plan_successor( wizard.contract_line_id.plan_successor(
@ -43,7 +41,6 @@ class ContractLineWizard(models.TransientModel):
) )
return True return True
@api.multi
def stop_plan_successor(self): def stop_plan_successor(self):
for wizard in self: for wizard in self:
wizard.contract_line_id.stop_plan_successor( wizard.contract_line_id.stop_plan_successor(
@ -51,7 +48,6 @@ class ContractLineWizard(models.TransientModel):
) )
return True return True
@api.multi
def uncancel(self): def uncancel(self):
for wizard in self: for wizard in self:
wizard.contract_line_id.uncancel(wizard.recurring_next_date) wizard.contract_line_id.uncancel(wizard.recurring_next_date)

Loading…
Cancel
Save