You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
467 lines
17 KiB
467 lines
17 KiB
# 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 ContractContract(models.Model):
|
|
_name = 'contract.contract'
|
|
_description = "Contract"
|
|
_order = 'code, name asc'
|
|
_inherit = [
|
|
'mail.thread',
|
|
'mail.activity.mixin',
|
|
'contract.abstract.contract',
|
|
]
|
|
|
|
active = fields.Boolean(
|
|
default=True,
|
|
)
|
|
code = fields.Char(
|
|
string="Reference",
|
|
)
|
|
group_id = fields.Many2one(
|
|
string="Group",
|
|
comodel_name='account.analytic.account',
|
|
ondelete='restrict',
|
|
)
|
|
currency_id = fields.Many2one(
|
|
related="company_id.currency_id",
|
|
string="Currency",
|
|
readonly=True,
|
|
)
|
|
contract_template_id = fields.Many2one(
|
|
string='Contract Template', comodel_name='contract.template'
|
|
)
|
|
contract_line_ids = fields.One2many(
|
|
string='Contract lines',
|
|
comodel_name='contract.line',
|
|
inverse_name='contract_id',
|
|
copy=True,
|
|
)
|
|
|
|
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
|
|
)
|
|
payment_term_id = fields.Many2one(
|
|
comodel_name='account.payment.term', string='Payment Terms', index=True
|
|
)
|
|
invoice_count = fields.Integer(compute="_compute_invoice_count")
|
|
fiscal_position_id = fields.Many2one(
|
|
comodel_name='account.fiscal.position',
|
|
string='Fiscal Position',
|
|
ondelete='restrict',
|
|
)
|
|
invoice_partner_id = fields.Many2one(
|
|
string="Invoicing contact",
|
|
comodel_name='res.partner',
|
|
ondelete='restrict',
|
|
)
|
|
partner_id = fields.Many2one(
|
|
comodel_name='res.partner',
|
|
inverse='_inverse_partner_id',
|
|
required=True
|
|
)
|
|
|
|
commercial_partner_id = fields.Many2one(
|
|
'res.partner',
|
|
related='partner_id.commercial_partner_id',
|
|
store=True,
|
|
string='Commercial Entity',
|
|
index=True
|
|
)
|
|
tag_ids = fields.Many2many(comodel_name="contract.tag", string="Tags")
|
|
|
|
@api.multi
|
|
def _inverse_partner_id(self):
|
|
for rec in self:
|
|
if not rec.invoice_partner_id:
|
|
rec.invoice_partner_id = rec.partner_id.address_get(
|
|
['invoice']
|
|
)['invoice']
|
|
|
|
@api.multi
|
|
def _get_related_invoices(self):
|
|
self.ensure_one()
|
|
|
|
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)]
|
|
)
|
|
return invoices
|
|
|
|
@api.multi
|
|
def _compute_invoice_count(self):
|
|
for rec in self:
|
|
rec.invoice_count = len(rec._get_related_invoices())
|
|
|
|
@api.multi
|
|
def action_show_invoices(self):
|
|
self.ensure_one()
|
|
tree_view_ref = (
|
|
'account.invoice_supplier_tree'
|
|
if self.contract_type == 'purchase'
|
|
else 'account.invoice_tree_with_onboarding'
|
|
)
|
|
form_view_ref = (
|
|
'account.invoice_supplier_form'
|
|
if self.contract_type == 'purchase'
|
|
else 'account.invoice_form'
|
|
)
|
|
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)
|
|
action = {
|
|
'type': 'ir.actions.act_window',
|
|
'name': 'Invoices',
|
|
'res_model': 'account.invoice',
|
|
'view_type': 'form',
|
|
'view_mode': 'tree,kanban,form,calendar,pivot,graph,activity',
|
|
'domain': [('id', 'in', self._get_related_invoices().ids)],
|
|
}
|
|
if tree_view and form_view:
|
|
action['views'] = [(tree_view.id, 'tree'), (form_view.id, 'form')]
|
|
return action
|
|
|
|
@api.depends('contract_line_ids.date_end')
|
|
def _compute_date_end(self):
|
|
for contract in self:
|
|
contract.date_end = False
|
|
date_end = contract.contract_line_ids.mapped('date_end')
|
|
if date_end and all(date_end):
|
|
contract.date_end = max(date_end)
|
|
|
|
@api.depends(
|
|
'contract_line_ids.recurring_next_date',
|
|
'contract_line_ids.is_canceled',
|
|
)
|
|
def _compute_recurring_next_date(self):
|
|
for contract in self:
|
|
recurring_next_date = contract.contract_line_ids.filtered(
|
|
lambda l: l.recurring_next_date and not l.is_canceled
|
|
).mapped('recurring_next_date')
|
|
if recurring_next_date:
|
|
contract.recurring_next_date = min(recurring_next_date)
|
|
|
|
@api.depends('contract_line_ids.create_invoice_visibility')
|
|
def _compute_create_invoice_visibility(self):
|
|
for contract in self:
|
|
contract.create_invoice_visibility = any(
|
|
contract.contract_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 `contract_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 == 'contract_line_ids':
|
|
lines = self._convert_contract_lines(contract_template_id)
|
|
self.contract_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
|
|
self.fiscal_position_id = self.partner_id.property_account_position_id
|
|
if self.contract_type == 'purchase':
|
|
self.payment_term_id = \
|
|
self.partner_id.property_supplier_payment_term_id
|
|
else:
|
|
self.payment_term_id = \
|
|
self.partner_id.property_payment_term_id
|
|
self.invoice_partner_id = self.partner_id.address_get(['invoice'])[
|
|
'invoice'
|
|
]
|
|
return {
|
|
'domain': {
|
|
'invoice_partner_id': [
|
|
'|',
|
|
('id', 'parent_of', self.partner_id.id),
|
|
('id', 'child_of', self.partner_id.id),
|
|
]
|
|
}
|
|
}
|
|
|
|
@api.multi
|
|
def _convert_contract_lines(self, contract):
|
|
self.ensure_one()
|
|
new_lines = self.env['contract.line']
|
|
contract_line_model = self.env['contract.line']
|
|
for contract_line in contract.contract_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.context_today(contract_line)
|
|
vals['recurring_next_date'] = fields.Date.context_today(
|
|
contract_line
|
|
)
|
|
new_lines += contract_line_model.new(vals)
|
|
new_lines._onchange_date_start()
|
|
new_lines._onchange_is_auto_renew()
|
|
return new_lines
|
|
|
|
@api.multi
|
|
def _prepare_invoice(self, date_invoice, journal=None):
|
|
self.ensure_one()
|
|
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'
|
|
vinvoice = self.env['account.invoice'].new({
|
|
'partner_id': self.invoice_partner_id.id,
|
|
'type': invoice_type,
|
|
})
|
|
vinvoice._onchange_partner_id()
|
|
invoice_vals = vinvoice._convert_to_write(vinvoice._cache)
|
|
invoice_vals.update({
|
|
'name': self.code,
|
|
'currency_id': currency.id,
|
|
'date_invoice': date_invoice,
|
|
'journal_id': journal.id,
|
|
'origin': self.name,
|
|
'company_id': self.company_id.id,
|
|
'user_id': self.user_id.id,
|
|
})
|
|
if self.payment_term_id:
|
|
invoice_vals['payment_term_id'] = self.payment_term_id.id
|
|
if self.fiscal_position_id:
|
|
invoice_vals['fiscal_position_id'] = self.fiscal_position_id.id
|
|
return invoice_vals
|
|
|
|
@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='contract.contract',
|
|
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.model
|
|
def _finalize_invoice_values(self, invoice_values):
|
|
"""
|
|
This method adds the missing values in the invoice lines dictionaries.
|
|
|
|
If no account on the product, the invoice lines account is
|
|
taken from the invoice's journal in _onchange_product_id
|
|
This code is not in finalize_creation_from_contract because it's
|
|
not possible to create an invoice line with no account
|
|
|
|
:param invoice_values: dictionary (invoice values)
|
|
:return: updated dictionary (invoice values)
|
|
"""
|
|
# If no account on the product, the invoice lines account is
|
|
# taken from the invoice's journal in _onchange_product_id
|
|
# This code is not in finalize_creation_from_contract because it's
|
|
# 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:
|
|
name = invoice_line.name
|
|
account_analytic_id = invoice_line.account_analytic_id
|
|
price_unit = invoice_line.price_unit
|
|
invoice_line.invoice_id = new_invoice
|
|
invoice_line._onchange_product_id()
|
|
invoice_line.update(
|
|
{
|
|
'name': name,
|
|
'account_analytic_id': account_analytic_id,
|
|
'price_unit': price_unit,
|
|
}
|
|
)
|
|
return new_invoice._convert_to_write(new_invoice._cache)
|
|
|
|
@api.model
|
|
def _finalize_invoice_creation(self, invoices):
|
|
invoices.compute_taxes()
|
|
|
|
@api.model
|
|
def _finalize_and_create_invoices(self, invoices_values):
|
|
"""
|
|
This method:
|
|
- finalizes the invoices values (onchange's...)
|
|
- creates the invoices
|
|
- finalizes the created invoices (onchange's, tax computation...)
|
|
:param invoices_values: list of dictionaries (invoices values)
|
|
:return: created invoices (account.invoice)
|
|
"""
|
|
if isinstance(invoices_values, dict):
|
|
invoices_values = [invoices_values]
|
|
final_invoices_values = []
|
|
for invoice_values in invoices_values:
|
|
final_invoices_values.append(
|
|
self._finalize_invoice_values(invoice_values)
|
|
)
|
|
invoices = self.env['account.invoice'].create(final_invoices_values)
|
|
self._finalize_invoice_creation(invoices)
|
|
return invoices
|
|
|
|
@api.model
|
|
def _get_contracts_to_invoice_domain(self, date_ref=None):
|
|
"""
|
|
This method builds the domain to use to find all
|
|
contracts (contract.contract) to invoice.
|
|
:param date_ref: optional reference date to use instead of today
|
|
:return: list (domain) usable on contract.contract
|
|
"""
|
|
domain = []
|
|
if not date_ref:
|
|
date_ref = fields.Date.context_today(self)
|
|
domain.extend([('recurring_next_date', '<=', date_ref)])
|
|
return domain
|
|
|
|
@api.multi
|
|
def _get_lines_to_invoice(self, date_ref):
|
|
"""
|
|
This method fetches and returns the lines to invoice on the contract
|
|
(self), based on the given date.
|
|
:param date_ref: date used as reference date to find lines to invoice
|
|
:return: contract lines (contract.line recordset)
|
|
"""
|
|
self.ensure_one()
|
|
return self.contract_line_ids.filtered(
|
|
lambda l: not l.is_canceled
|
|
and l.recurring_next_date
|
|
and l.recurring_next_date <= date_ref
|
|
)
|
|
|
|
@api.multi
|
|
def _prepare_recurring_invoices_values(self, date_ref=False):
|
|
"""
|
|
This method builds the list of invoices values to create, based on
|
|
the lines to invoice of the contracts in self.
|
|
!!! The date of next invoice (recurring_next_date) is updated here !!!
|
|
:return: list of dictionaries (invoices values)
|
|
"""
|
|
invoices_values = []
|
|
for contract in self:
|
|
if not date_ref:
|
|
date_ref = contract.recurring_next_date
|
|
if not date_ref:
|
|
# this use case is possible when recurring_create_invoice is
|
|
# called for a finished contract
|
|
continue
|
|
contract_lines = contract._get_lines_to_invoice(date_ref)
|
|
if not contract_lines:
|
|
continue
|
|
invoice_values = contract._prepare_invoice(date_ref)
|
|
for line in contract_lines:
|
|
invoice_values.setdefault('invoice_line_ids', [])
|
|
invoice_line_values = line._prepare_invoice_line(
|
|
invoice_id=False
|
|
)
|
|
if invoice_line_values:
|
|
invoice_values['invoice_line_ids'].append(
|
|
(0, 0, invoice_line_values)
|
|
)
|
|
invoices_values.append(invoice_values)
|
|
contract_lines._update_recurring_next_date()
|
|
return invoices_values
|
|
|
|
@api.multi
|
|
def recurring_create_invoice(self):
|
|
"""
|
|
This method triggers the creation of the next invoices of the contracts
|
|
even if their next invoicing date is in the future.
|
|
"""
|
|
return self._recurring_create_invoice()
|
|
|
|
@api.multi
|
|
def _recurring_create_invoice(self, date_ref=False):
|
|
invoices_values = self._prepare_recurring_invoices_values(date_ref)
|
|
return self._finalize_and_create_invoices(invoices_values)
|
|
|
|
@api.model
|
|
def cron_recurring_create_invoice(self):
|
|
domain = self._get_contracts_to_invoice_domain()
|
|
contracts_to_invoice = self.search(domain)
|
|
date_ref = fields.Date.context_today(contracts_to_invoice)
|
|
contracts_to_invoice._recurring_create_invoice(date_ref)
|