Browse Source
[IMP] - Make recurrence mechanism on contract line
[IMP] - Make recurrence mechanism on contract line
Make recurrence mechanism on contract line and some other refactoring [FIX] - Keep contract_cron on account_analytic_account model contract_cron defined with no_update option. Changing it, will cause issue to past version installation. [IMP] - Fix recurring_next_date default value recurring_next_date should have start_date as default value in prepaid policy and start_date + invoicing_interval if postpaid [FIX] - Fix test check no journal [IMP] - Return created invoices on recurring_create_invoice [IMP] - Specific process to compute recurring_next_date for monthly-last-day fixes: #198 [ADD] - Add Post-migration script to bring recurrence info from contract to contract lines [ADD] - Add search filter based on date_end and recurring_next_date - not_finished filter in contract search view - finished filter in contract search view - Next Invoice group by in contract search view [ADD] - Add unit tests - cases to compute first recurring next date - contract recurring_next_date - contract date_end [IMP] - Improve Unit testspull/207/head
sbejaoui
6 years ago
22 changed files with 1305 additions and 941 deletions
-
13contract/__manifest__.py
-
19contract/migrations/12.0.2.0.0/post-migration.py
-
10contract/models/__init__.py
-
69contract/models/abstract_contract.py
-
184contract/models/abstract_contract_line.py
-
361contract/models/account_analytic_account.py
-
100contract/models/account_analytic_contract.py
-
221contract/models/account_analytic_contract_line.py
-
16contract/models/account_analytic_invoice_line.py
-
4contract/models/account_invoice.py
-
223contract/models/contract.py
-
250contract/models/contract_line.py
-
22contract/models/contract_template.py
-
38contract/models/contract_template_line.py
-
52contract/models/res_partner.py
-
63contract/report/report_contract.xml
-
390contract/tests/test_contract.py
-
39contract/views/abstract_contract_line.xml
-
34contract/views/contract.xml
-
28contract/views/contract_line.xml
-
33contract/views/contract_template.xml
-
17contract/views/contract_template_line.xml
@ -0,0 +1,19 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2018 ACSONE SA/NV |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
||||
|
|
||||
|
|
||||
|
def migrate(cr, version): |
||||
|
"""Copy recurrence info from contract to contract lines.""" |
||||
|
|
||||
|
cr.execute( |
||||
|
"""UPDATE account_analytic_invoice_line AS contract_line |
||||
|
SET recurring_rule_type=contract.recurring_rule_type, |
||||
|
recurring_invoicing_type=contract.recurring_invoicing_type, |
||||
|
recurring_interval=contract.recurring_interval, |
||||
|
recurring_next_date=contract.recurring_next_date, |
||||
|
date_start=contract.date_start, |
||||
|
date_end=contract.date_end |
||||
|
FROM account_analytic_account AS contract |
||||
|
WHERE contract.id=contract_line.contract_id""" |
||||
|
) |
@ -1,8 +1,10 @@ |
|||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
||||
|
|
||||
from . import account_analytic_contract |
|
||||
from . import account_analytic_account |
|
||||
from . import account_analytic_contract_line |
|
||||
from . import account_analytic_invoice_line |
|
||||
|
from . import abstract_contract |
||||
|
from . import abstract_contract_line |
||||
|
from . import contract_template |
||||
|
from . import contract |
||||
|
from . import contract_template_line |
||||
|
from . import contract_line |
||||
from . import account_invoice |
from . import account_invoice |
||||
from . import res_partner |
from . import res_partner |
@ -0,0 +1,69 @@ |
|||||
|
# Copyright 2004-2010 OpenERP SA |
||||
|
# Copyright 2014 Angel Moya <angel.moya@domatix.com> |
||||
|
# Copyright 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com> |
||||
|
# Copyright 2016-2018 Carlos Dauden <carlos.dauden@tecnativa.com> |
||||
|
# Copyright 2016-2017 LasLabs Inc. |
||||
|
# Copyright 2018 ACSONE SA/NV |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
||||
|
|
||||
|
from odoo import api, models, fields |
||||
|
|
||||
|
|
||||
|
class AbstractAccountAnalyticContract(models.AbstractModel): |
||||
|
_name = 'account.abstract.analytic.contract' |
||||
|
_description = 'Abstract Account Analytic Contract' |
||||
|
|
||||
|
# These fields will not be synced to the contract |
||||
|
NO_SYNC = ['name', 'partner_id'] |
||||
|
|
||||
|
name = fields.Char(required=True) |
||||
|
# Needed for avoiding errors on several inherited behaviors |
||||
|
partner_id = fields.Many2one( |
||||
|
comodel_name="res.partner", string="Partner (always False)" |
||||
|
) |
||||
|
pricelist_id = fields.Many2one( |
||||
|
comodel_name='product.pricelist', string='Pricelist' |
||||
|
) |
||||
|
contract_type = fields.Selection( |
||||
|
selection=[('sale', 'Customer'), ('purchase', 'Supplier')], |
||||
|
default='sale', |
||||
|
) |
||||
|
|
||||
|
journal_id = fields.Many2one( |
||||
|
'account.journal', |
||||
|
string='Journal', |
||||
|
default=lambda s: s._default_journal(), |
||||
|
domain="[('type', '=', contract_type)," |
||||
|
"('company_id', '=', company_id)]", |
||||
|
) |
||||
|
company_id = fields.Many2one( |
||||
|
'res.company', |
||||
|
string='Company', |
||||
|
required=True, |
||||
|
default=lambda self: self.env.user.company_id, |
||||
|
) |
||||
|
|
||||
|
@api.onchange('contract_type') |
||||
|
def _onchange_contract_type(self): |
||||
|
if self.contract_type == 'purchase': |
||||
|
self.recurring_invoice_line_ids.filtered('automatic_price').update( |
||||
|
{'automatic_price': False} |
||||
|
) |
||||
|
self.journal_id = self.env['account.journal'].search( |
||||
|
[ |
||||
|
('type', '=', self.contract_type), |
||||
|
('company_id', '=', self.company_id.id), |
||||
|
], |
||||
|
limit=1, |
||||
|
) |
||||
|
|
||||
|
@api.model |
||||
|
def _default_journal(self): |
||||
|
company_id = self.env.context.get( |
||||
|
'company_id', self.env.user.company_id.id |
||||
|
) |
||||
|
domain = [ |
||||
|
('type', '=', self.contract_type), |
||||
|
('company_id', '=', company_id), |
||||
|
] |
||||
|
return self.env['account.journal'].search(domain, limit=1) |
@ -0,0 +1,184 @@ |
|||||
|
# Copyright 2004-2010 OpenERP SA |
||||
|
# Copyright 2014 Angel Moya <angel.moya@domatix.com> |
||||
|
# Copyright 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com> |
||||
|
# Copyright 2016-2018 Carlos Dauden <carlos.dauden@tecnativa.com> |
||||
|
# Copyright 2016-2017 LasLabs Inc. |
||||
|
# Copyright 2018 ACSONE SA/NV |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
||||
|
|
||||
|
from odoo import api, models, fields |
||||
|
from odoo.addons import decimal_precision as dp |
||||
|
from odoo.exceptions import ValidationError |
||||
|
from odoo.tools.translate import _ |
||||
|
|
||||
|
|
||||
|
class AccountAbstractAnalyticContractLine(models.AbstractModel): |
||||
|
_name = 'account.abstract.analytic.contract.line' |
||||
|
_description = 'Account Abstract Analytic Contract Line' |
||||
|
|
||||
|
product_id = fields.Many2one( |
||||
|
'product.product', string='Product', required=True |
||||
|
) |
||||
|
|
||||
|
name = fields.Text(string='Description', required=True) |
||||
|
quantity = fields.Float(default=1.0, required=True) |
||||
|
uom_id = fields.Many2one( |
||||
|
'uom.uom', string='Unit of Measure', required=True |
||||
|
) |
||||
|
automatic_price = fields.Boolean( |
||||
|
string="Auto-price?", |
||||
|
help="If this is marked, the price will be obtained automatically " |
||||
|
"applying the pricelist to the product. If not, you will be " |
||||
|
"able to introduce a manual price", |
||||
|
) |
||||
|
specific_price = fields.Float(string='Specific Price') |
||||
|
price_unit = fields.Float( |
||||
|
string='Unit Price', |
||||
|
compute="_compute_price_unit", |
||||
|
inverse="_inverse_price_unit", |
||||
|
) |
||||
|
price_subtotal = fields.Float( |
||||
|
compute='_compute_price_subtotal', |
||||
|
digits=dp.get_precision('Account'), |
||||
|
string='Sub Total', |
||||
|
) |
||||
|
discount = fields.Float( |
||||
|
string='Discount (%)', |
||||
|
digits=dp.get_precision('Discount'), |
||||
|
help='Discount that is applied in generated invoices.' |
||||
|
' It should be less or equal to 100', |
||||
|
) |
||||
|
sequence = fields.Integer( |
||||
|
string="Sequence", |
||||
|
default=10, |
||||
|
help="Sequence of the contract line when displaying contracts", |
||||
|
) |
||||
|
recurring_rule_type = fields.Selection( |
||||
|
[ |
||||
|
('daily', 'Day(s)'), |
||||
|
('weekly', 'Week(s)'), |
||||
|
('monthly', 'Month(s)'), |
||||
|
('monthlylastday', 'Month(s) last day'), |
||||
|
('yearly', 'Year(s)'), |
||||
|
], |
||||
|
default='monthly', |
||||
|
string='Recurrence', |
||||
|
help="Specify Interval for automatic invoice generation.", |
||||
|
required=True, |
||||
|
) |
||||
|
recurring_invoicing_type = fields.Selection( |
||||
|
[('pre-paid', 'Pre-paid'), ('post-paid', 'Post-paid')], |
||||
|
default='pre-paid', |
||||
|
string='Invoicing type', |
||||
|
help="Specify if process date is 'from' or 'to' invoicing date", |
||||
|
required=True, |
||||
|
) |
||||
|
recurring_interval = fields.Integer( |
||||
|
default=1, |
||||
|
string='Repeat Every', |
||||
|
help="Repeat every (Days/Week/Month/Year)", |
||||
|
required=True, |
||||
|
) |
||||
|
|
||||
|
partner_id = fields.Many2one( |
||||
|
comodel_name="res.partner", string="Partner (always False)" |
||||
|
) |
||||
|
pricelist_id = fields.Many2one( |
||||
|
comodel_name='product.pricelist', string='Pricelist' |
||||
|
) |
||||
|
recurring_next_date = fields.Date( |
||||
|
copy=False, string='Date of Next Invoice' |
||||
|
) |
||||
|
|
||||
|
@api.depends( |
||||
|
'automatic_price', |
||||
|
'specific_price', |
||||
|
'product_id', |
||||
|
'quantity', |
||||
|
'pricelist_id', |
||||
|
'partner_id', |
||||
|
) |
||||
|
def _compute_price_unit(self): |
||||
|
"""Get the specific price if no auto-price, and the price obtained |
||||
|
from the pricelist otherwise. |
||||
|
""" |
||||
|
for line in self: |
||||
|
if line.automatic_price: |
||||
|
product = line.product_id.with_context( |
||||
|
quantity=line.env.context.get( |
||||
|
'contract_line_qty', line.quantity |
||||
|
), |
||||
|
pricelist=line.pricelist_id.id, |
||||
|
partner=line.partner_id.id, |
||||
|
date=line.env.context.get('old_date', fields.Date.today()), |
||||
|
) |
||||
|
line.price_unit = product.price |
||||
|
else: |
||||
|
line.price_unit = line.specific_price |
||||
|
|
||||
|
# Tip in https://github.com/odoo/odoo/issues/23891#issuecomment-376910788 |
||||
|
@api.onchange('price_unit') |
||||
|
def _inverse_price_unit(self): |
||||
|
"""Store the specific price in the no auto-price records.""" |
||||
|
for line in self.filtered(lambda x: not x.automatic_price): |
||||
|
line.specific_price = line.price_unit |
||||
|
|
||||
|
@api.multi |
||||
|
@api.depends('quantity', 'price_unit', 'discount') |
||||
|
def _compute_price_subtotal(self): |
||||
|
for line in self: |
||||
|
subtotal = line.quantity * line.price_unit |
||||
|
discount = line.discount / 100 |
||||
|
subtotal *= 1 - discount |
||||
|
if line.pricelist_id: |
||||
|
cur = line.pricelist_id.currency_id |
||||
|
line.price_subtotal = cur.round(subtotal) |
||||
|
else: |
||||
|
line.price_subtotal = subtotal |
||||
|
|
||||
|
@api.multi |
||||
|
@api.constrains('discount') |
||||
|
def _check_discount(self): |
||||
|
for line in self: |
||||
|
if line.discount > 100: |
||||
|
raise ValidationError( |
||||
|
_("Discount should be less or equal to 100") |
||||
|
) |
||||
|
|
||||
|
@api.multi |
||||
|
@api.onchange('product_id') |
||||
|
def _onchange_product_id(self): |
||||
|
if not self.product_id: |
||||
|
return {'domain': {'uom_id': []}} |
||||
|
|
||||
|
vals = {} |
||||
|
domain = { |
||||
|
'uom_id': [ |
||||
|
('category_id', '=', self.product_id.uom_id.category_id.id) |
||||
|
] |
||||
|
} |
||||
|
if not self.uom_id or ( |
||||
|
self.product_id.uom_id.category_id.id != self.uom_id.category_id.id |
||||
|
): |
||||
|
vals['uom_id'] = self.product_id.uom_id |
||||
|
|
||||
|
date = self.recurring_next_date or fields.Date.today() |
||||
|
partner = self.partner_id or self.env.user.partner_id |
||||
|
|
||||
|
product = self.product_id.with_context( |
||||
|
lang=partner.lang, |
||||
|
partner=partner.id, |
||||
|
quantity=self.quantity, |
||||
|
date=date, |
||||
|
pricelist=self.pricelist_id.id, |
||||
|
uom=self.uom_id.id, |
||||
|
) |
||||
|
|
||||
|
name = product.name_get()[0][1] |
||||
|
if product.description_sale: |
||||
|
name += '\n' + product.description_sale |
||||
|
vals['name'] = name |
||||
|
|
||||
|
vals['price_unit'] = product.price |
||||
|
self.update(vals) |
||||
|
return {'domain': domain} |
@ -1,361 +0,0 @@ |
|||||
# Copyright 2004-2010 OpenERP SA |
|
||||
# Copyright 2014 Angel Moya <angel.moya@domatix.com> |
|
||||
# Copyright 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com> |
|
||||
# Copyright 2016-2018 Carlos Dauden <carlos.dauden@tecnativa.com> |
|
||||
# Copyright 2016-2017 LasLabs Inc. |
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|
||||
|
|
||||
from dateutil.relativedelta import relativedelta |
|
||||
|
|
||||
from odoo import api, fields, models |
|
||||
from odoo.exceptions import ValidationError |
|
||||
from odoo.tools.translate import _ |
|
||||
|
|
||||
|
|
||||
class AccountAnalyticAccount(models.Model): |
|
||||
_name = 'account.analytic.account' |
|
||||
_inherit = ['account.analytic.account', |
|
||||
'account.analytic.contract', |
|
||||
] |
|
||||
|
|
||||
contract_template_id = fields.Many2one( |
|
||||
string='Contract Template', |
|
||||
comodel_name='account.analytic.contract', |
|
||||
) |
|
||||
recurring_invoice_line_ids = fields.One2many( |
|
||||
string='Invoice Lines', |
|
||||
comodel_name='account.analytic.invoice.line', |
|
||||
inverse_name='analytic_account_id', |
|
||||
copy=True, |
|
||||
) |
|
||||
date_start = fields.Date( |
|
||||
string='Date Start', |
|
||||
default=fields.Date.context_today, |
|
||||
) |
|
||||
date_end = fields.Date( |
|
||||
string='Date End', |
|
||||
index=True, |
|
||||
) |
|
||||
recurring_invoices = fields.Boolean( |
|
||||
string='Generate recurring invoices automatically', |
|
||||
) |
|
||||
recurring_next_date = fields.Date( |
|
||||
default=fields.Date.context_today, |
|
||||
copy=False, |
|
||||
string='Date of Next Invoice', |
|
||||
) |
|
||||
user_id = fields.Many2one( |
|
||||
comodel_name='res.users', |
|
||||
string='Responsible', |
|
||||
index=True, |
|
||||
default=lambda self: self.env.user, |
|
||||
) |
|
||||
create_invoice_visibility = fields.Boolean( |
|
||||
compute='_compute_create_invoice_visibility', |
|
||||
) |
|
||||
|
|
||||
@api.depends('recurring_next_date', 'date_end') |
|
||||
def _compute_create_invoice_visibility(self): |
|
||||
for contract in self: |
|
||||
contract.create_invoice_visibility = ( |
|
||||
not contract.date_end or |
|
||||
contract.recurring_next_date <= contract.date_end |
|
||||
) |
|
||||
|
|
||||
@api.onchange('contract_template_id') |
|
||||
def _onchange_contract_template_id(self): |
|
||||
"""Update the contract fields with that of the template. |
|
||||
|
|
||||
Take special consideration with the `recurring_invoice_line_ids`, |
|
||||
which must be created using the data from the contract lines. Cascade |
|
||||
deletion ensures that any errant lines that are created are also |
|
||||
deleted. |
|
||||
""" |
|
||||
contract = self.contract_template_id |
|
||||
if not contract: |
|
||||
return |
|
||||
for field_name, field in contract._fields.items(): |
|
||||
if field.name == 'recurring_invoice_line_ids': |
|
||||
lines = self._convert_contract_lines(contract) |
|
||||
self.recurring_invoice_line_ids = lines |
|
||||
elif not any(( |
|
||||
field.compute, field.related, field.automatic, |
|
||||
field.readonly, field.company_dependent, |
|
||||
field.name in self.NO_SYNC, |
|
||||
)): |
|
||||
self[field_name] = self.contract_template_id[field_name] |
|
||||
|
|
||||
@api.onchange('date_start') |
|
||||
def _onchange_date_start(self): |
|
||||
if self.date_start: |
|
||||
self.recurring_next_date = self.date_start |
|
||||
|
|
||||
@api.onchange('partner_id') |
|
||||
def _onchange_partner_id(self): |
|
||||
self.pricelist_id = self.partner_id.property_product_pricelist.id |
|
||||
|
|
||||
@api.constrains('partner_id', 'recurring_invoices') |
|
||||
def _check_partner_id_recurring_invoices(self): |
|
||||
for contract in self.filtered('recurring_invoices'): |
|
||||
if not contract.partner_id: |
|
||||
raise ValidationError( |
|
||||
_("You must supply a customer for the contract '%s'") % |
|
||||
contract.name |
|
||||
) |
|
||||
|
|
||||
@api.constrains('recurring_next_date', 'date_start') |
|
||||
def _check_recurring_next_date_start_date(self): |
|
||||
for contract in self.filtered('recurring_next_date'): |
|
||||
if contract.date_start > contract.recurring_next_date: |
|
||||
raise ValidationError( |
|
||||
_("You can't have a next invoicing date before the start " |
|
||||
"of the contract '%s'") % contract.name |
|
||||
) |
|
||||
|
|
||||
@api.constrains('recurring_next_date', 'recurring_invoices') |
|
||||
def _check_recurring_next_date_recurring_invoices(self): |
|
||||
for contract in self.filtered('recurring_invoices'): |
|
||||
if not contract.recurring_next_date: |
|
||||
raise ValidationError( |
|
||||
_("You must supply a next invoicing date for contract " |
|
||||
"'%s'") % contract.name |
|
||||
) |
|
||||
|
|
||||
@api.constrains('date_start', 'recurring_invoices') |
|
||||
def _check_date_start_recurring_invoices(self): |
|
||||
for contract in self.filtered('recurring_invoices'): |
|
||||
if not contract.date_start: |
|
||||
raise ValidationError( |
|
||||
_("You must supply a start date for contract '%s'") % |
|
||||
contract.name |
|
||||
) |
|
||||
|
|
||||
@api.constrains('date_start', 'date_end') |
|
||||
def _check_start_end_dates(self): |
|
||||
for contract in self.filtered('date_end'): |
|
||||
if contract.date_start > contract.date_end: |
|
||||
raise ValidationError( |
|
||||
_("Contract '%s' start date can't be later than end date") |
|
||||
% contract.name |
|
||||
) |
|
||||
|
|
||||
@api.multi |
|
||||
def _convert_contract_lines(self, contract): |
|
||||
self.ensure_one() |
|
||||
new_lines = [] |
|
||||
for contract_line in contract.recurring_invoice_line_ids: |
|
||||
vals = contract_line._convert_to_write(contract_line.read()[0]) |
|
||||
# Remove template link field named as analytic account field |
|
||||
vals.pop('analytic_account_id', False) |
|
||||
new_lines.append((0, 0, vals)) |
|
||||
return new_lines |
|
||||
|
|
||||
@api.model |
|
||||
def get_relative_delta(self, recurring_rule_type, interval): |
|
||||
if recurring_rule_type == 'daily': |
|
||||
return relativedelta(days=interval) |
|
||||
elif recurring_rule_type == 'weekly': |
|
||||
return relativedelta(weeks=interval) |
|
||||
elif recurring_rule_type == 'monthly': |
|
||||
return relativedelta(months=interval) |
|
||||
elif recurring_rule_type == 'monthlylastday': |
|
||||
return relativedelta(months=interval, day=31) |
|
||||
else: |
|
||||
return relativedelta(years=interval) |
|
||||
|
|
||||
@api.model |
|
||||
def _insert_markers(self, line, date_format): |
|
||||
date_from = fields.Date.from_string(line.date_from) |
|
||||
date_to = fields.Date.from_string(line.date_to) |
|
||||
name = line.name |
|
||||
name = name.replace('#START#', date_from.strftime(date_format)) |
|
||||
name = name.replace('#END#', date_to.strftime(date_format)) |
|
||||
return name |
|
||||
|
|
||||
@api.model |
|
||||
def _prepare_invoice_line(self, line, invoice_id): |
|
||||
invoice_line = self.env['account.invoice.line'].new({ |
|
||||
'invoice_id': invoice_id, |
|
||||
'product_id': line.product_id.id, |
|
||||
'quantity': line.quantity, |
|
||||
'uom_id': line.uom_id.id, |
|
||||
'discount': line.discount, |
|
||||
}) |
|
||||
# Get other invoice line values from product onchange |
|
||||
invoice_line._onchange_product_id() |
|
||||
invoice_line_vals = invoice_line._convert_to_write(invoice_line._cache) |
|
||||
# Insert markers |
|
||||
contract = line.analytic_account_id |
|
||||
lang_obj = self.env['res.lang'] |
|
||||
lang = lang_obj.search( |
|
||||
[('code', '=', contract.partner_id.lang)]) |
|
||||
date_format = lang.date_format or '%m/%d/%Y' |
|
||||
name = self._insert_markers(line, date_format) |
|
||||
invoice_line_vals.update({ |
|
||||
'name': name, |
|
||||
'account_analytic_id': contract.id, |
|
||||
'price_unit': line.price_unit, |
|
||||
}) |
|
||||
return invoice_line_vals |
|
||||
|
|
||||
@api.multi |
|
||||
def _prepare_invoice(self, journal=None): |
|
||||
self.ensure_one() |
|
||||
if not self.partner_id: |
|
||||
if self.contract_type == 'purchase': |
|
||||
raise ValidationError( |
|
||||
_("You must first select a Supplier for Contract %s!") % |
|
||||
self.name) |
|
||||
else: |
|
||||
raise ValidationError( |
|
||||
_("You must first select a Customer for Contract %s!") % |
|
||||
self.name) |
|
||||
if not journal: |
|
||||
journal = self.journal_id or self.env['account.journal'].search([ |
|
||||
('type', '=', self.contract_type), |
|
||||
('company_id', '=', self.company_id.id) |
|
||||
], limit=1) |
|
||||
if not journal: |
|
||||
raise ValidationError( |
|
||||
_("Please define a %s journal for the company '%s'.") % |
|
||||
(self.contract_type, self.company_id.name or '') |
|
||||
) |
|
||||
currency = ( |
|
||||
self.pricelist_id.currency_id or |
|
||||
self.partner_id.property_product_pricelist.currency_id or |
|
||||
self.company_id.currency_id |
|
||||
) |
|
||||
invoice_type = 'out_invoice' |
|
||||
if self.contract_type == 'purchase': |
|
||||
invoice_type = 'in_invoice' |
|
||||
invoice = self.env['account.invoice'].new({ |
|
||||
'reference': self.code, |
|
||||
'type': invoice_type, |
|
||||
'partner_id': self.partner_id.address_get( |
|
||||
['invoice'])['invoice'], |
|
||||
'currency_id': currency.id, |
|
||||
'journal_id': journal.id, |
|
||||
'date_invoice': self.recurring_next_date, |
|
||||
'origin': self.name, |
|
||||
'company_id': self.company_id.id, |
|
||||
'contract_id': self.id, |
|
||||
'user_id': self.partner_id.user_id.id, |
|
||||
}) |
|
||||
# Get other invoice values from partner onchange |
|
||||
invoice._onchange_partner_id() |
|
||||
return invoice._convert_to_write(invoice._cache) |
|
||||
|
|
||||
@api.multi |
|
||||
def _prepare_invoice_update(self, invoice): |
|
||||
vals = self._prepare_invoice() |
|
||||
update_vals = { |
|
||||
'contract_id': self.id, |
|
||||
'date_invoice': vals.get('date_invoice', False), |
|
||||
'reference': ' '.join(filter(None, [ |
|
||||
invoice.reference, vals.get('reference')])), |
|
||||
'origin': ' '.join(filter(None, [ |
|
||||
invoice.origin, vals.get('origin')])), |
|
||||
} |
|
||||
return update_vals |
|
||||
|
|
||||
@api.multi |
|
||||
def _create_invoice(self, invoice=False): |
|
||||
""" |
|
||||
:param invoice: If not False add lines to this invoice |
|
||||
:return: invoice created or updated |
|
||||
""" |
|
||||
self.ensure_one() |
|
||||
if invoice and invoice.state == 'draft': |
|
||||
invoice.update(self._prepare_invoice_update(invoice)) |
|
||||
else: |
|
||||
invoice = self.env['account.invoice'].create( |
|
||||
self._prepare_invoice()) |
|
||||
for line in self.recurring_invoice_line_ids: |
|
||||
invoice_line_vals = self._prepare_invoice_line(line, invoice.id) |
|
||||
if invoice_line_vals: |
|
||||
self.env['account.invoice.line'].create(invoice_line_vals) |
|
||||
invoice.compute_taxes() |
|
||||
return invoice |
|
||||
|
|
||||
@api.multi |
|
||||
def recurring_create_invoice(self): |
|
||||
"""Create invoices from contracts |
|
||||
|
|
||||
:return: invoices created |
|
||||
""" |
|
||||
invoices = self.env['account.invoice'] |
|
||||
for contract in self: |
|
||||
ref_date = contract.recurring_next_date or fields.Date.today() |
|
||||
if (contract.date_start > ref_date or |
|
||||
contract.date_end and contract.date_end < ref_date): |
|
||||
if self.env.context.get('cron'): |
|
||||
continue # Don't fail on cron jobs |
|
||||
raise ValidationError( |
|
||||
_("You must review start and end dates!\n%s") % |
|
||||
contract.name |
|
||||
) |
|
||||
old_date = fields.Date.from_string(ref_date) |
|
||||
new_date = old_date + self.get_relative_delta( |
|
||||
contract.recurring_rule_type, contract.recurring_interval) |
|
||||
ctx = self.env.context.copy() |
|
||||
ctx.update({ |
|
||||
'old_date': old_date, |
|
||||
'next_date': new_date, |
|
||||
# Force company for correct evaluation of domain access rules |
|
||||
'force_company': contract.company_id.id, |
|
||||
}) |
|
||||
# Re-read contract with correct company |
|
||||
invoices |= contract.with_context(ctx)._create_invoice() |
|
||||
contract.write({ |
|
||||
'recurring_next_date': fields.Date.to_string(new_date) |
|
||||
}) |
|
||||
return invoices |
|
||||
|
|
||||
@api.model |
|
||||
def cron_recurring_create_invoice(self): |
|
||||
today = fields.Date.today() |
|
||||
contracts = self.with_context(cron=True).search([ |
|
||||
('recurring_invoices', '=', True), |
|
||||
('recurring_next_date', '<=', today), |
|
||||
'|', |
|
||||
('date_end', '=', False), |
|
||||
('date_end', '>=', today), |
|
||||
]) |
|
||||
return contracts.recurring_create_invoice() |
|
||||
|
|
||||
@api.multi |
|
||||
def action_contract_send(self): |
|
||||
self.ensure_one() |
|
||||
template = self.env.ref( |
|
||||
'contract.email_contract_template', |
|
||||
False, |
|
||||
) |
|
||||
compose_form = self.env.ref('mail.email_compose_message_wizard_form') |
|
||||
ctx = dict( |
|
||||
default_model='account.analytic.account', |
|
||||
default_res_id=self.id, |
|
||||
default_use_template=bool(template), |
|
||||
default_template_id=template and template.id or False, |
|
||||
default_composition_mode='comment', |
|
||||
) |
|
||||
return { |
|
||||
'name': _('Compose Email'), |
|
||||
'type': 'ir.actions.act_window', |
|
||||
'view_type': 'form', |
|
||||
'view_mode': 'form', |
|
||||
'res_model': 'mail.compose.message', |
|
||||
'views': [(compose_form.id, 'form')], |
|
||||
'view_id': compose_form.id, |
|
||||
'target': 'new', |
|
||||
'context': ctx, |
|
||||
} |
|
||||
|
|
||||
@api.multi |
|
||||
def button_show_recurring_invoices(self): |
|
||||
self.ensure_one() |
|
||||
action = self.env.ref( |
|
||||
'contract.act_purchase_recurring_invoices') |
|
||||
if self.contract_type == 'sale': |
|
||||
action = self.env.ref( |
|
||||
'contract.act_recurring_invoices') |
|
||||
return action.read()[0] |
|
@ -1,100 +0,0 @@ |
|||||
# Copyright 2004-2010 OpenERP SA |
|
||||
# Copyright 2014 Angel Moya <angel.moya@domatix.com> |
|
||||
# Copyright 2016 Carlos Dauden <carlos.dauden@tecnativa.com> |
|
||||
# Copyright 2016-2017 LasLabs Inc. |
|
||||
# Copyright 2015-2017 Tecnativa - Pedro M. Baeza |
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|
||||
|
|
||||
from odoo import api, fields, models |
|
||||
|
|
||||
|
|
||||
class AccountAnalyticContract(models.Model): |
|
||||
_name = 'account.analytic.contract' |
|
||||
_description = "Account Analytic Contract" |
|
||||
|
|
||||
# These fields will not be synced to the contract |
|
||||
NO_SYNC = [ |
|
||||
'name', |
|
||||
'partner_id', |
|
||||
] |
|
||||
|
|
||||
name = fields.Char( |
|
||||
required=True, |
|
||||
) |
|
||||
# Needed for avoiding errors on several inherited behaviors |
|
||||
partner_id = fields.Many2one( |
|
||||
comodel_name="res.partner", |
|
||||
string="Partner (always False)", |
|
||||
) |
|
||||
contract_type = fields.Selection( |
|
||||
selection=[ |
|
||||
('sale', 'Customer'), |
|
||||
('purchase', 'Supplier'), |
|
||||
], default='sale', |
|
||||
) |
|
||||
pricelist_id = fields.Many2one( |
|
||||
comodel_name='product.pricelist', |
|
||||
string='Pricelist', |
|
||||
) |
|
||||
recurring_invoice_line_ids = fields.One2many( |
|
||||
comodel_name='account.analytic.contract.line', |
|
||||
inverse_name='analytic_account_id', |
|
||||
copy=True, |
|
||||
string='Invoice Lines', |
|
||||
) |
|
||||
recurring_rule_type = fields.Selection( |
|
||||
[('daily', 'Day(s)'), |
|
||||
('weekly', 'Week(s)'), |
|
||||
('monthly', 'Month(s)'), |
|
||||
('monthlylastday', 'Month(s) last day'), |
|
||||
('yearly', 'Year(s)'), |
|
||||
], |
|
||||
default='monthly', |
|
||||
string='Recurrence', |
|
||||
help="Specify Interval for automatic invoice generation.", |
|
||||
) |
|
||||
recurring_invoicing_type = fields.Selection( |
|
||||
[('pre-paid', 'Pre-paid'), |
|
||||
('post-paid', 'Post-paid'), |
|
||||
], |
|
||||
default='pre-paid', |
|
||||
string='Invoicing type', |
|
||||
help="Specify if process date is 'from' or 'to' invoicing date", |
|
||||
) |
|
||||
recurring_interval = fields.Integer( |
|
||||
default=1, |
|
||||
string='Repeat Every', |
|
||||
help="Repeat every (Days/Week/Month/Year)", |
|
||||
) |
|
||||
journal_id = fields.Many2one( |
|
||||
'account.journal', |
|
||||
string='Journal', |
|
||||
default=lambda s: s._default_journal(), |
|
||||
domain="[('type', '=', contract_type)," |
|
||||
"('company_id', '=', company_id)]", |
|
||||
) |
|
||||
company_id = fields.Many2one( |
|
||||
'res.company', |
|
||||
string='Company', |
|
||||
required=True, |
|
||||
default=lambda self: self.env.user.company_id, |
|
||||
) |
|
||||
|
|
||||
@api.onchange('contract_type') |
|
||||
def _onchange_contract_type(self): |
|
||||
if self.contract_type == 'purchase': |
|
||||
self.recurring_invoice_line_ids.filtered('automatic_price').update( |
|
||||
{'automatic_price': False}) |
|
||||
self.journal_id = self.env['account.journal'].search([ |
|
||||
('type', '=', self.contract_type), |
|
||||
('company_id', '=', self.company_id.id) |
|
||||
], limit=1) |
|
||||
|
|
||||
@api.model |
|
||||
def _default_journal(self): |
|
||||
company_id = self.env.context.get( |
|
||||
'company_id', self.env.user.company_id.id) |
|
||||
domain = [ |
|
||||
('type', '=', self.contract_type), |
|
||||
('company_id', '=', company_id)] |
|
||||
return self.env['account.journal'].search(domain, limit=1) |
|
@ -1,221 +0,0 @@ |
|||||
# Copyright 2004-2010 OpenERP SA |
|
||||
# Copyright 2014 Angel Moya <angel.moya@domatix.com> |
|
||||
# Copyright 2016 Carlos Dauden <carlos.dauden@tecnativa.com> |
|
||||
# Copyright 2016-2017 LasLabs Inc. |
|
||||
# Copyright 2015-2018 Tecnativa - Pedro M. Baeza |
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|
||||
|
|
||||
from dateutil.relativedelta import relativedelta |
|
||||
|
|
||||
from odoo import api, fields, models |
|
||||
from odoo.addons import decimal_precision as dp |
|
||||
from odoo.exceptions import ValidationError |
|
||||
from odoo.tools.translate import _ |
|
||||
|
|
||||
|
|
||||
class AccountAnalyticContractLine(models.Model): |
|
||||
_name = 'account.analytic.contract.line' |
|
||||
_description = 'Contract Lines' |
|
||||
_order = "sequence,id" |
|
||||
|
|
||||
product_id = fields.Many2one( |
|
||||
'product.product', |
|
||||
string='Product', |
|
||||
required=True, |
|
||||
) |
|
||||
analytic_account_id = fields.Many2one( |
|
||||
string='Contract', |
|
||||
comodel_name='account.analytic.contract', |
|
||||
required=True, |
|
||||
ondelete='cascade', |
|
||||
) |
|
||||
name = fields.Text( |
|
||||
string='Description', |
|
||||
required=True, |
|
||||
) |
|
||||
quantity = fields.Float( |
|
||||
default=1.0, |
|
||||
required=True, |
|
||||
) |
|
||||
uom_id = fields.Many2one( |
|
||||
'uom.uom', |
|
||||
string='Unit of Measure', |
|
||||
required=True, |
|
||||
) |
|
||||
automatic_price = fields.Boolean( |
|
||||
string="Auto-price?", |
|
||||
help="If this is marked, the price will be obtained automatically " |
|
||||
"applying the pricelist to the product. If not, you will be " |
|
||||
"able to introduce a manual price", |
|
||||
) |
|
||||
specific_price = fields.Float( |
|
||||
string='Specific Price', |
|
||||
) |
|
||||
price_unit = fields.Float( |
|
||||
string='Unit Price', |
|
||||
compute="_compute_price_unit", |
|
||||
inverse="_inverse_price_unit", |
|
||||
) |
|
||||
price_subtotal = fields.Float( |
|
||||
compute='_compute_price_subtotal', |
|
||||
digits=dp.get_precision('Account'), |
|
||||
string='Sub Total', |
|
||||
) |
|
||||
discount = fields.Float( |
|
||||
string='Discount (%)', |
|
||||
digits=dp.get_precision('Discount'), |
|
||||
help='Discount that is applied in generated invoices.' |
|
||||
' It should be less or equal to 100', |
|
||||
) |
|
||||
sequence = fields.Integer( |
|
||||
string="Sequence", |
|
||||
default=10, |
|
||||
help="Sequence of the contract line when displaying contracts", |
|
||||
) |
|
||||
date_from = fields.Date( |
|
||||
string='Date From', |
|
||||
compute='_compute_date_from', |
|
||||
help='Date from invoiced period', |
|
||||
) |
|
||||
date_to = fields.Date( |
|
||||
string='Date To', |
|
||||
compute='_compute_date_to', |
|
||||
help='Date to invoiced period', |
|
||||
) |
|
||||
|
|
||||
@api.depends( |
|
||||
'automatic_price', |
|
||||
'specific_price', |
|
||||
'product_id', |
|
||||
'quantity', |
|
||||
'analytic_account_id.pricelist_id', |
|
||||
'analytic_account_id.partner_id', |
|
||||
) |
|
||||
def _compute_price_unit(self): |
|
||||
"""Get the specific price if no auto-price, and the price obtained |
|
||||
from the pricelist otherwise. |
|
||||
""" |
|
||||
for line in self: |
|
||||
if line.automatic_price: |
|
||||
product = line.product_id.with_context( |
|
||||
quantity=line.env.context.get( |
|
||||
'contract_line_qty', line.quantity, |
|
||||
), |
|
||||
pricelist=line.analytic_account_id.pricelist_id.id, |
|
||||
partner=line.analytic_account_id.partner_id.id, |
|
||||
date=line.env.context.get('old_date', fields.Date.today()), |
|
||||
) |
|
||||
line.price_unit = product.price |
|
||||
else: |
|
||||
line.price_unit = line.specific_price |
|
||||
|
|
||||
# Tip in https://github.com/odoo/odoo/issues/23891#issuecomment-376910788 |
|
||||
@api.onchange('price_unit') |
|
||||
def _inverse_price_unit(self): |
|
||||
"""Store the specific price in the no auto-price records.""" |
|
||||
for line in self.filtered(lambda x: not x.automatic_price): |
|
||||
line.specific_price = line.price_unit |
|
||||
|
|
||||
@api.multi |
|
||||
@api.depends('quantity', 'price_unit', 'discount') |
|
||||
def _compute_price_subtotal(self): |
|
||||
for line in self: |
|
||||
subtotal = line.quantity * line.price_unit |
|
||||
discount = line.discount / 100 |
|
||||
subtotal *= 1 - discount |
|
||||
if line.analytic_account_id.pricelist_id: |
|
||||
cur = line.analytic_account_id.pricelist_id.currency_id |
|
||||
line.price_subtotal = cur.round(subtotal) |
|
||||
else: |
|
||||
line.price_subtotal = subtotal |
|
||||
|
|
||||
def _compute_date_from(self): |
|
||||
# When call from template line.analytic_account_id comodel is |
|
||||
# 'account.analytic.contract', |
|
||||
if self._name != 'account.analytic.invoice.line': |
|
||||
return |
|
||||
for line in self: |
|
||||
contract = line.analytic_account_id |
|
||||
date_start = ( |
|
||||
self.env.context.get('old_date') or fields.Date.from_string( |
|
||||
contract.recurring_next_date or fields.Date.today()) |
|
||||
) |
|
||||
if contract.recurring_invoicing_type == 'pre-paid': |
|
||||
date_from = date_start |
|
||||
else: |
|
||||
date_from = (date_start - contract.get_relative_delta( |
|
||||
contract.recurring_rule_type, |
|
||||
contract.recurring_interval) + relativedelta(days=1)) |
|
||||
line.date_from = fields.Date.to_string(date_from) |
|
||||
|
|
||||
def _compute_date_to(self): |
|
||||
# When call from template line.analytic_account_id comodel is |
|
||||
# 'account.analytic.contract', |
|
||||
if self._name != 'account.analytic.invoice.line': |
|
||||
return |
|
||||
for line in self: |
|
||||
contract = line.analytic_account_id |
|
||||
date_start = ( |
|
||||
self.env.context.get('old_date') or fields.Date.from_string( |
|
||||
contract.recurring_next_date or fields.Date.today()) |
|
||||
) |
|
||||
next_date = ( |
|
||||
self.env.context.get('next_date') or |
|
||||
date_start + contract.get_relative_delta( |
|
||||
contract.recurring_rule_type, contract.recurring_interval) |
|
||||
) |
|
||||
if contract.recurring_invoicing_type == 'pre-paid': |
|
||||
date_to = next_date - relativedelta(days=1) |
|
||||
else: |
|
||||
date_to = date_start |
|
||||
line.date_to = fields.Date.to_string(date_to) |
|
||||
|
|
||||
@api.multi |
|
||||
@api.constrains('discount') |
|
||||
def _check_discount(self): |
|
||||
for line in self: |
|
||||
if line.discount > 100: |
|
||||
raise ValidationError( |
|
||||
_("Discount should be less or equal to 100")) |
|
||||
|
|
||||
@api.multi |
|
||||
@api.onchange('product_id') |
|
||||
def _onchange_product_id(self): |
|
||||
if not self.product_id: |
|
||||
return {'domain': {'uom_id': []}} |
|
||||
|
|
||||
vals = {} |
|
||||
domain = {'uom_id': [ |
|
||||
('category_id', '=', self.product_id.uom_id.category_id.id)]} |
|
||||
if not self.uom_id or (self.product_id.uom_id.category_id.id != |
|
||||
self.uom_id.category_id.id): |
|
||||
vals['uom_id'] = self.product_id.uom_id |
|
||||
|
|
||||
if self.analytic_account_id._name == 'account.analytic.account': |
|
||||
date = ( |
|
||||
self.analytic_account_id.recurring_next_date or |
|
||||
fields.Date.today() |
|
||||
) |
|
||||
partner = self.analytic_account_id.partner_id |
|
||||
|
|
||||
else: |
|
||||
date = fields.Date.today() |
|
||||
partner = self.env.user.partner_id |
|
||||
|
|
||||
product = self.product_id.with_context( |
|
||||
lang=partner.lang, |
|
||||
partner=partner.id, |
|
||||
quantity=self.quantity, |
|
||||
date=date, |
|
||||
pricelist=self.analytic_account_id.pricelist_id.id, |
|
||||
uom=self.uom_id.id |
|
||||
) |
|
||||
|
|
||||
name = product.name_get()[0][1] |
|
||||
if product.description_sale: |
|
||||
name += '\n' + product.description_sale |
|
||||
vals['name'] = name |
|
||||
|
|
||||
vals['price_unit'] = product.price |
|
||||
self.update(vals) |
|
||||
return {'domain': domain} |
|
@ -1,16 +0,0 @@ |
|||||
# Copyright 2017 LasLabs Inc. |
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|
||||
|
|
||||
from odoo import fields, models |
|
||||
|
|
||||
|
|
||||
class AccountAnalyticInvoiceLine(models.Model): |
|
||||
_name = 'account.analytic.invoice.line' |
|
||||
_inherit = 'account.analytic.contract.line' |
|
||||
|
|
||||
analytic_account_id = fields.Many2one( |
|
||||
comodel_name='account.analytic.account', |
|
||||
string='Analytic Account', |
|
||||
required=True, |
|
||||
ondelete='cascade', |
|
||||
) |
|
@ -0,0 +1,223 @@ |
|||||
|
# Copyright 2004-2010 OpenERP SA |
||||
|
# Copyright 2014 Angel Moya <angel.moya@domatix.com> |
||||
|
# Copyright 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com> |
||||
|
# Copyright 2016-2018 Carlos Dauden <carlos.dauden@tecnativa.com> |
||||
|
# Copyright 2016-2017 LasLabs Inc. |
||||
|
# Copyright 2018 ACSONE SA/NV |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
||||
|
|
||||
|
from odoo import api, fields, models |
||||
|
from odoo.exceptions import ValidationError |
||||
|
from odoo.tools.translate import _ |
||||
|
|
||||
|
|
||||
|
class AccountAnalyticAccount(models.Model): |
||||
|
_name = 'account.analytic.account' |
||||
|
_inherit = [ |
||||
|
'account.analytic.account', |
||||
|
'account.abstract.analytic.contract', |
||||
|
] |
||||
|
|
||||
|
contract_template_id = fields.Many2one( |
||||
|
string='Contract Template', comodel_name='account.analytic.contract' |
||||
|
) |
||||
|
recurring_invoice_line_ids = fields.One2many( |
||||
|
string='Invoice Lines', |
||||
|
comodel_name='account.analytic.invoice.line', |
||||
|
inverse_name='contract_id', |
||||
|
copy=True, |
||||
|
) |
||||
|
recurring_invoices = fields.Boolean( |
||||
|
string='Generate recurring invoices automatically' |
||||
|
) |
||||
|
user_id = fields.Many2one( |
||||
|
comodel_name='res.users', |
||||
|
string='Responsible', |
||||
|
index=True, |
||||
|
default=lambda self: self.env.user, |
||||
|
) |
||||
|
create_invoice_visibility = fields.Boolean( |
||||
|
compute='_compute_create_invoice_visibility' |
||||
|
) |
||||
|
recurring_next_date = fields.Date( |
||||
|
compute='_compute_recurring_next_date', |
||||
|
string='Date of Next Invoice', |
||||
|
store=True, |
||||
|
) |
||||
|
date_end = fields.Date( |
||||
|
compute='_compute_date_end', string='Date End', store=True |
||||
|
) |
||||
|
|
||||
|
@api.depends('recurring_invoice_line_ids.date_end') |
||||
|
def _compute_date_end(self): |
||||
|
for contract in self: |
||||
|
contract.date_end = False |
||||
|
date_end = contract.recurring_invoice_line_ids.mapped('date_end') |
||||
|
if date_end and all(date_end): |
||||
|
contract.date_end = max(date_end) |
||||
|
|
||||
|
@api.depends('recurring_invoice_line_ids.recurring_next_date') |
||||
|
def _compute_recurring_next_date(self): |
||||
|
for contract in self: |
||||
|
recurring_next_date = contract.recurring_invoice_line_ids.filtered( |
||||
|
'create_invoice_visibility' |
||||
|
).mapped('recurring_next_date') |
||||
|
if recurring_next_date: |
||||
|
contract.recurring_next_date = min(recurring_next_date) |
||||
|
|
||||
|
@api.depends('recurring_invoice_line_ids.create_invoice_visibility') |
||||
|
def _compute_create_invoice_visibility(self): |
||||
|
for contract in self: |
||||
|
contract.create_invoice_visibility = any( |
||||
|
contract.recurring_invoice_line_ids.mapped( |
||||
|
'create_invoice_visibility' |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
@api.onchange('contract_template_id') |
||||
|
def _onchange_contract_template_id(self): |
||||
|
"""Update the contract fields with that of the template. |
||||
|
|
||||
|
Take special consideration with the `recurring_invoice_line_ids`, |
||||
|
which must be created using the data from the contract lines. Cascade |
||||
|
deletion ensures that any errant lines that are created are also |
||||
|
deleted. |
||||
|
""" |
||||
|
contract_template_id = self.contract_template_id |
||||
|
if not contract_template_id: |
||||
|
return |
||||
|
for field_name, field in contract_template_id._fields.items(): |
||||
|
if field.name == 'recurring_invoice_line_ids': |
||||
|
lines = self._convert_contract_lines(contract_template_id) |
||||
|
self.recurring_invoice_line_ids = lines |
||||
|
elif not any( |
||||
|
( |
||||
|
field.compute, |
||||
|
field.related, |
||||
|
field.automatic, |
||||
|
field.readonly, |
||||
|
field.company_dependent, |
||||
|
field.name in self.NO_SYNC, |
||||
|
) |
||||
|
): |
||||
|
self[field_name] = self.contract_template_id[field_name] |
||||
|
|
||||
|
@api.onchange('partner_id') |
||||
|
def _onchange_partner_id(self): |
||||
|
self.pricelist_id = self.partner_id.property_product_pricelist.id |
||||
|
|
||||
|
@api.constrains('partner_id', 'recurring_invoices') |
||||
|
def _check_partner_id_recurring_invoices(self): |
||||
|
for contract in self.filtered('recurring_invoices'): |
||||
|
if not contract.partner_id: |
||||
|
raise ValidationError( |
||||
|
_("You must supply a customer for the contract '%s'") |
||||
|
% contract.name |
||||
|
) |
||||
|
|
||||
|
@api.multi |
||||
|
def _convert_contract_lines(self, contract): |
||||
|
self.ensure_one() |
||||
|
new_lines = [] |
||||
|
for contract_line in contract.recurring_invoice_line_ids: |
||||
|
vals = contract_line._convert_to_write(contract_line.read()[0]) |
||||
|
# Remove template link field |
||||
|
vals.pop('contract_template_id', False) |
||||
|
vals['date_start'] = fields.Date.today() |
||||
|
vals['recurring_next_date'] = fields.Date.today() |
||||
|
self.recurring_invoice_line_ids._onchange_date_start() |
||||
|
new_lines.append((0, 0, vals)) |
||||
|
return new_lines |
||||
|
|
||||
|
@api.multi |
||||
|
def _prepare_invoice(self, date_invoice, journal=None): |
||||
|
self.ensure_one() |
||||
|
if not self.partner_id: |
||||
|
if self.contract_type == 'purchase': |
||||
|
raise ValidationError( |
||||
|
_("You must first select a Supplier for Contract %s!") |
||||
|
% self.name |
||||
|
) |
||||
|
else: |
||||
|
raise ValidationError( |
||||
|
_("You must first select a Customer for Contract %s!") |
||||
|
% self.name |
||||
|
) |
||||
|
if not journal: |
||||
|
journal = ( |
||||
|
self.journal_id |
||||
|
if self.journal_id.type == self.contract_type |
||||
|
else self.env['account.journal'].search( |
||||
|
[ |
||||
|
('type', '=', self.contract_type), |
||||
|
('company_id', '=', self.company_id.id), |
||||
|
], |
||||
|
limit=1, |
||||
|
) |
||||
|
) |
||||
|
if not journal: |
||||
|
raise ValidationError( |
||||
|
_("Please define a %s journal for the company '%s'.") |
||||
|
% (self.contract_type, self.company_id.name or '') |
||||
|
) |
||||
|
currency = ( |
||||
|
self.pricelist_id.currency_id |
||||
|
or self.partner_id.property_product_pricelist.currency_id |
||||
|
or self.company_id.currency_id |
||||
|
) |
||||
|
invoice_type = 'out_invoice' |
||||
|
if self.contract_type == 'purchase': |
||||
|
invoice_type = 'in_invoice' |
||||
|
invoice = self.env['account.invoice'].new( |
||||
|
{ |
||||
|
'reference': self.code, |
||||
|
'type': invoice_type, |
||||
|
'partner_id': self.partner_id.address_get(['invoice'])[ |
||||
|
'invoice' |
||||
|
], |
||||
|
'currency_id': currency.id, |
||||
|
'date_invoice': date_invoice, |
||||
|
'journal_id': journal.id, |
||||
|
'origin': self.name, |
||||
|
'company_id': self.company_id.id, |
||||
|
'contract_id': self.id, |
||||
|
'user_id': self.partner_id.user_id.id, |
||||
|
} |
||||
|
) |
||||
|
# Get other invoice values from partner onchange |
||||
|
invoice._onchange_partner_id() |
||||
|
return invoice._convert_to_write(invoice._cache) |
||||
|
|
||||
|
@api.multi |
||||
|
def action_contract_send(self): |
||||
|
self.ensure_one() |
||||
|
template = self.env.ref('contract.email_contract_template', False) |
||||
|
compose_form = self.env.ref('mail.email_compose_message_wizard_form') |
||||
|
ctx = dict( |
||||
|
default_model='account.analytic.account', |
||||
|
default_res_id=self.id, |
||||
|
default_use_template=bool(template), |
||||
|
default_template_id=template and template.id or False, |
||||
|
default_composition_mode='comment', |
||||
|
) |
||||
|
return { |
||||
|
'name': _('Compose Email'), |
||||
|
'type': 'ir.actions.act_window', |
||||
|
'view_type': 'form', |
||||
|
'view_mode': 'form', |
||||
|
'res_model': 'mail.compose.message', |
||||
|
'views': [(compose_form.id, 'form')], |
||||
|
'view_id': compose_form.id, |
||||
|
'target': 'new', |
||||
|
'context': ctx, |
||||
|
} |
||||
|
|
||||
|
@api.multi |
||||
|
def recurring_create_invoice(self): |
||||
|
return self.env[ |
||||
|
'account.analytic.invoice.line' |
||||
|
].recurring_create_invoice(self) |
||||
|
|
||||
|
@api.model |
||||
|
def cron_recurring_create_invoice(self): |
||||
|
self.env['account.analytic.invoice.line'].recurring_create_invoice() |
@ -0,0 +1,250 @@ |
|||||
|
# Copyright 2017 LasLabs Inc. |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
||||
|
|
||||
|
from dateutil.relativedelta import relativedelta |
||||
|
|
||||
|
from odoo import api, fields, models, _ |
||||
|
from odoo.exceptions import ValidationError |
||||
|
|
||||
|
|
||||
|
class AccountAnalyticInvoiceLine(models.Model): |
||||
|
_name = 'account.analytic.invoice.line' |
||||
|
_inherit = 'account.abstract.analytic.contract.line' |
||||
|
|
||||
|
contract_id = fields.Many2one( |
||||
|
comodel_name='account.analytic.account', |
||||
|
string='Analytic Account', |
||||
|
required=True, |
||||
|
ondelete='cascade', |
||||
|
oldname='analytic_account_id', |
||||
|
) |
||||
|
date_start = fields.Date(string='Date Start', default=fields.Date.today()) |
||||
|
date_end = fields.Date(string='Date End', index=True) |
||||
|
recurring_next_date = fields.Date( |
||||
|
copy=False, string='Date of Next Invoice' |
||||
|
) |
||||
|
create_invoice_visibility = fields.Boolean( |
||||
|
compute='_compute_create_invoice_visibility' |
||||
|
) |
||||
|
partner_id = fields.Many2one( |
||||
|
comodel_name="res.partner", |
||||
|
string="Partner (always False)", |
||||
|
related='contract_id.partner_id', |
||||
|
store=True, |
||||
|
readonly=True, |
||||
|
) |
||||
|
pricelist_id = fields.Many2one( |
||||
|
comodel_name='product.pricelist', |
||||
|
string='Pricelist', |
||||
|
related='contract_id.pricelist_id', |
||||
|
store=True, |
||||
|
readonly=True, |
||||
|
) |
||||
|
|
||||
|
@api.model |
||||
|
def _compute_first_recurring_next_date( |
||||
|
self, |
||||
|
date_start, |
||||
|
recurring_invoicing_type, |
||||
|
recurring_rule_type, |
||||
|
recurring_interval, |
||||
|
): |
||||
|
if recurring_rule_type == 'monthlylastday': |
||||
|
return date_start + self.get_relative_delta( |
||||
|
recurring_rule_type, recurring_interval - 1 |
||||
|
) |
||||
|
if recurring_invoicing_type == 'pre-paid': |
||||
|
return date_start |
||||
|
return date_start + self.get_relative_delta( |
||||
|
recurring_rule_type, recurring_interval |
||||
|
) |
||||
|
|
||||
|
@api.onchange( |
||||
|
'date_start', |
||||
|
'recurring_invoicing_type', |
||||
|
'recurring_rule_type', |
||||
|
'recurring_interval', |
||||
|
) |
||||
|
def _onchange_date_start(self): |
||||
|
for rec in self.filtered('date_start'): |
||||
|
rec.recurring_next_date = self._compute_first_recurring_next_date( |
||||
|
rec.date_start, |
||||
|
rec.recurring_invoicing_type, |
||||
|
rec.recurring_rule_type, |
||||
|
rec.recurring_interval, |
||||
|
) |
||||
|
|
||||
|
@api.constrains('recurring_next_date', 'date_start') |
||||
|
def _check_recurring_next_date_start_date(self): |
||||
|
for line in self.filtered('recurring_next_date'): |
||||
|
if line.date_start and line.recurring_next_date: |
||||
|
if line.date_start > line.recurring_next_date: |
||||
|
raise ValidationError( |
||||
|
_( |
||||
|
"You can't have a next invoicing date before the " |
||||
|
"start of the contract '%s'" |
||||
|
) |
||||
|
% line.contract_id.name |
||||
|
) |
||||
|
|
||||
|
@api.constrains('recurring_next_date') |
||||
|
def _check_recurring_next_date_recurring_invoices(self): |
||||
|
for line in self.filtered('contract_id.recurring_invoices'): |
||||
|
if not line.recurring_next_date: |
||||
|
raise ValidationError( |
||||
|
_( |
||||
|
"You must supply a next invoicing date for contract " |
||||
|
"'%s'" |
||||
|
) |
||||
|
% line.contract_id.name |
||||
|
) |
||||
|
|
||||
|
@api.constrains('date_start') |
||||
|
def _check_date_start_recurring_invoices(self): |
||||
|
for line in self.filtered('contract_id.recurring_invoices'): |
||||
|
if not line.date_start: |
||||
|
raise ValidationError( |
||||
|
_("You must supply a start date for contract '%s'") |
||||
|
% line.contract_id.name |
||||
|
) |
||||
|
|
||||
|
@api.constrains('date_start', 'date_end') |
||||
|
def _check_start_end_dates(self): |
||||
|
for line in self.filtered('date_end'): |
||||
|
if line.date_start and line.date_end: |
||||
|
if line.date_start > line.date_end: |
||||
|
raise ValidationError( |
||||
|
_( |
||||
|
"Contract '%s' start date can't be later than " |
||||
|
"end date" |
||||
|
) |
||||
|
% line.contract_id.name |
||||
|
) |
||||
|
|
||||
|
@api.depends('recurring_next_date', 'date_end') |
||||
|
def _compute_create_invoice_visibility(self): |
||||
|
for line in self: |
||||
|
line.create_invoice_visibility = not line.date_end or ( |
||||
|
line.recurring_next_date |
||||
|
and line.date_end |
||||
|
and line.recurring_next_date <= line.date_end |
||||
|
) |
||||
|
|
||||
|
@api.model |
||||
|
def recurring_create_invoice(self, contract=False): |
||||
|
domain = [] |
||||
|
date_ref = fields.Date.today() |
||||
|
if contract: |
||||
|
contract.ensure_one() |
||||
|
date_ref = contract.recurring_next_date |
||||
|
domain.append(('contract_id', '=', contract.id)) |
||||
|
|
||||
|
domain.extend( |
||||
|
[ |
||||
|
('contract_id.recurring_invoices', '=', True), |
||||
|
('recurring_next_date', '<=', date_ref), |
||||
|
'|', |
||||
|
('date_end', '=', False), |
||||
|
('date_end', '>=', date_ref), |
||||
|
] |
||||
|
) |
||||
|
lines = self.search(domain).filtered('create_invoice_visibility') |
||||
|
if lines: |
||||
|
return lines._recurring_create_invoice() |
||||
|
return False |
||||
|
|
||||
|
@api.multi |
||||
|
def _recurring_create_invoice(self): |
||||
|
"""Create invoices from contracts |
||||
|
|
||||
|
:return: invoices created |
||||
|
""" |
||||
|
invoices = self.env['account.invoice'] |
||||
|
for contract in self.mapped('contract_id'): |
||||
|
lines = self.filtered(lambda l: l.contract_id == contract) |
||||
|
invoices |= lines._create_invoice() |
||||
|
lines._update_recurring_next_date() |
||||
|
return invoices |
||||
|
|
||||
|
@api.multi |
||||
|
def _create_invoice(self): |
||||
|
""" |
||||
|
:param invoice: If not False add lines to this invoice |
||||
|
:return: invoice created or updated |
||||
|
""" |
||||
|
contract = self.mapped('contract_id') |
||||
|
date_invoice = min(self.mapped('recurring_next_date')) |
||||
|
invoice = self.env['account.invoice'].create( |
||||
|
contract._prepare_invoice(date_invoice) |
||||
|
) |
||||
|
for line in self: |
||||
|
invoice_line_vals = line._prepare_invoice_line(invoice.id) |
||||
|
if invoice_line_vals: |
||||
|
self.env['account.invoice.line'].create(invoice_line_vals) |
||||
|
invoice.compute_taxes() |
||||
|
return invoice |
||||
|
|
||||
|
@api.multi |
||||
|
def _prepare_invoice_line(self, invoice_id): |
||||
|
self.ensure_one() |
||||
|
invoice_line = self.env['account.invoice.line'].new( |
||||
|
{ |
||||
|
'invoice_id': invoice_id, |
||||
|
'product_id': self.product_id.id, |
||||
|
'quantity': self.quantity, |
||||
|
'uom_id': self.uom_id.id, |
||||
|
'discount': self.discount, |
||||
|
} |
||||
|
) |
||||
|
# Get other invoice line values from product onchange |
||||
|
invoice_line._onchange_product_id() |
||||
|
invoice_line_vals = invoice_line._convert_to_write(invoice_line._cache) |
||||
|
# Insert markers |
||||
|
contract = self.contract_id |
||||
|
lang_obj = self.env['res.lang'] |
||||
|
lang = lang_obj.search([('code', '=', contract.partner_id.lang)]) |
||||
|
date_format = lang.date_format or '%m/%d/%Y' |
||||
|
name = self._insert_markers(date_format) |
||||
|
invoice_line_vals.update( |
||||
|
{ |
||||
|
'name': name, |
||||
|
'account_analytic_id': contract.id, |
||||
|
'price_unit': self.price_unit, |
||||
|
} |
||||
|
) |
||||
|
return invoice_line_vals |
||||
|
|
||||
|
@api.multi |
||||
|
def _insert_markers(self, date_format): |
||||
|
self.ensure_one() |
||||
|
date_from = fields.Date.from_string(self.recurring_next_date) |
||||
|
date_to = date_from + self.get_relative_delta( |
||||
|
self.recurring_rule_type, self.recurring_interval |
||||
|
) |
||||
|
name = self.name |
||||
|
name = name.replace('#START#', date_from.strftime(date_format)) |
||||
|
name = name.replace('#END#', date_to.strftime(date_format)) |
||||
|
return name |
||||
|
|
||||
|
@api.multi |
||||
|
def _update_recurring_next_date(self): |
||||
|
for line in self: |
||||
|
ref_date = line.recurring_next_date or fields.Date.today() |
||||
|
old_date = fields.Date.from_string(ref_date) |
||||
|
new_date = old_date + self.get_relative_delta( |
||||
|
line.recurring_rule_type, line.recurring_interval |
||||
|
) |
||||
|
line.recurring_next_date = new_date |
||||
|
|
||||
|
@api.model |
||||
|
def get_relative_delta(self, recurring_rule_type, interval): |
||||
|
if recurring_rule_type == 'daily': |
||||
|
return relativedelta(days=interval) |
||||
|
elif recurring_rule_type == 'weekly': |
||||
|
return relativedelta(weeks=interval) |
||||
|
elif recurring_rule_type == 'monthly': |
||||
|
return relativedelta(months=interval) |
||||
|
elif recurring_rule_type == 'monthlylastday': |
||||
|
return relativedelta(months=interval, day=31) |
||||
|
else: |
||||
|
return relativedelta(years=interval) |
@ -0,0 +1,22 @@ |
|||||
|
# Copyright 2004-2010 OpenERP SA |
||||
|
# Copyright 2014 Angel Moya <angel.moya@domatix.com> |
||||
|
# Copyright 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com> |
||||
|
# Copyright 2016-2018 Carlos Dauden <carlos.dauden@tecnativa.com> |
||||
|
# Copyright 2016-2017 LasLabs Inc. |
||||
|
# Copyright 2018 ACSONE SA/NV |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
||||
|
|
||||
|
from odoo import fields, models |
||||
|
|
||||
|
|
||||
|
class AccountAnalyticContract(models.Model): |
||||
|
_name = 'account.analytic.contract' |
||||
|
_inherit = 'account.abstract.analytic.contract' |
||||
|
_description = "Account Analytic Contract" |
||||
|
|
||||
|
recurring_invoice_line_ids = fields.One2many( |
||||
|
comodel_name='account.analytic.contract.line', |
||||
|
inverse_name='contract_template_id', |
||||
|
copy=True, |
||||
|
string='Invoice Lines', |
||||
|
) |
@ -0,0 +1,38 @@ |
|||||
|
# Copyright 2004-2010 OpenERP SA |
||||
|
# Copyright 2014 Angel Moya <angel.moya@domatix.com> |
||||
|
# Copyright 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com> |
||||
|
# Copyright 2016-2018 Carlos Dauden <carlos.dauden@tecnativa.com> |
||||
|
# Copyright 2016-2017 LasLabs Inc. |
||||
|
# Copyright 2018 ACSONE SA/NV |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
||||
|
|
||||
|
from odoo import fields, models |
||||
|
|
||||
|
|
||||
|
class AccountAnalyticContractLine(models.Model): |
||||
|
_name = 'account.analytic.contract.line' |
||||
|
_inherit = 'account.abstract.analytic.contract.line' |
||||
|
_description = 'Contract Lines' |
||||
|
_order = "sequence,id" |
||||
|
|
||||
|
contract_template_id = fields.Many2one( |
||||
|
string='Contract', |
||||
|
comodel_name='account.analytic.contract', |
||||
|
required=True, |
||||
|
ondelete='cascade', |
||||
|
oldname='analytic_account_id', |
||||
|
) |
||||
|
partner_id = fields.Many2one( |
||||
|
comodel_name="res.partner", |
||||
|
string="Partner (always False)", |
||||
|
related='contract_template_id.partner_id', |
||||
|
store=True, |
||||
|
readonly=True, |
||||
|
) |
||||
|
pricelist_id = fields.Many2one( |
||||
|
comodel_name='product.pricelist', |
||||
|
string='Pricelist', |
||||
|
related='contract_template_id.pricelist_id', |
||||
|
store=True, |
||||
|
readonly=True, |
||||
|
) |
@ -0,0 +1,39 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<odoo> |
||||
|
|
||||
|
<record id="account_abstract_analytic_contract_line_view_form" model="ir.ui.view"> |
||||
|
<field name="name">Account Abstract Analytic Contract Line Form View</field> |
||||
|
<field name="model">account.abstract.analytic.contract.line</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form> |
||||
|
<group> |
||||
|
<field name="product_id"/> |
||||
|
<field name="name"/> |
||||
|
<field name="quantity" colspan="2"/> |
||||
|
<field name="uom_id" colspan="2"/> |
||||
|
<field name="automatic_price"/> |
||||
|
<field name="specific_price" invisible="1"/> |
||||
|
<field name="price_unit" |
||||
|
attrs="{'readonly': [('automatic_price', '=', True)]}" |
||||
|
colspan="2"/> |
||||
|
<field name="discount" colspan="2"/> |
||||
|
</group> |
||||
|
<group name="recurrence_info"> |
||||
|
<group> |
||||
|
<field name="recurring_invoicing_type"/> |
||||
|
</group> |
||||
|
<group> |
||||
|
<label for="recurring_interval"/> |
||||
|
<div> |
||||
|
<field name="recurring_interval" |
||||
|
class="oe_inline"/> |
||||
|
<field name="recurring_rule_type" |
||||
|
class="oe_inline"/> |
||||
|
</div> |
||||
|
</group> |
||||
|
</group> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
</odoo> |
@ -0,0 +1,28 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<odoo> |
||||
|
|
||||
|
<record id="account_analytic_invoice_line_view_form" model="ir.ui.view"> |
||||
|
<field name="name">account.analytic.invoice.line.form</field> |
||||
|
<field name="model">account.analytic.invoice.line</field> |
||||
|
<field name="inherit_id" |
||||
|
ref="account_abstract_analytic_contract_line_view_form"/> |
||||
|
<field name="mode">primary</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<xpath expr="//form" position="attributes"> |
||||
|
<attribute name="string">Contract Line</attribute> |
||||
|
</xpath> |
||||
|
<xpath expr="//group[@name='recurrence_info']" position="inside"> |
||||
|
<group> |
||||
|
<field name="date_start" required="1"/> |
||||
|
</group> |
||||
|
<group> |
||||
|
<field name="date_end"/> |
||||
|
</group> |
||||
|
<group> |
||||
|
<field name="recurring_next_date"/> |
||||
|
</group> |
||||
|
</xpath> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
</odoo> |
@ -0,0 +1,17 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<odoo> |
||||
|
|
||||
|
<record id="account_analytic_contract_line_view_form" model="ir.ui.view"> |
||||
|
<field name="name">account.analytic.contract.line.form</field> |
||||
|
<field name="model">account.analytic.contract.line</field> |
||||
|
<field name="inherit_id" |
||||
|
ref="account_abstract_analytic_contract_line_view_form"/> |
||||
|
<field name="mode">primary</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<xpath expr="//form" position="attributes"> |
||||
|
<attribute name="string">Contract Line Template</attribute> |
||||
|
</xpath> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
</odoo> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue