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.
351 lines
13 KiB
351 lines
13 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.
|
|
# 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,
|
|
}
|