mirror of
https://github.com/OCA/contract.git
synced 2025-02-13 17:57:24 +02:00
[IMP] - Make recurrence mechanism on contract line
Make recurrence mechanism on contract line and some other refactoring [FIX] - Keep contract_cron on account_analytic_account model contract_cron defined with no_update option. Changing it, will cause issue to past version installation. [IMP] - Fix recurring_next_date default value recurring_next_date should have start_date as default value in prepaid policy and start_date + invoicing_interval if postpaid [FIX] - Fix test check no journal [IMP] - Return created invoices on recurring_create_invoice [IMP] - Specific process to compute recurring_next_date for monthly-last-day fixes: #198 [ADD] - Add Post-migration script to bring recurrence info from contract to contract lines [ADD] - Add search filter based on date_end and recurring_next_date - not_finished filter in contract search view - finished filter in contract search view - Next Invoice group by in contract search view [ADD] - Add unit tests - cases to compute first recurring next date - contract recurring_next_date - contract date_end [IMP] - Improve Unit tests
This commit is contained in:
@@ -4,17 +4,19 @@
|
||||
# Copyright 2016-2018 Tecnativa - Carlos Dauden
|
||||
# Copyright 2017 Tecnativa - Vicent Cubells
|
||||
# Copyright 2016-2017 LasLabs Inc.
|
||||
# Copyright 2018 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
'name': 'Contracts Management - Recurring',
|
||||
'version': '12.0.1.0.0',
|
||||
'name': 'Recurring - Contracts Management',
|
||||
'version': '12.0.2.0.0',
|
||||
'category': 'Contract Management',
|
||||
'license': 'AGPL-3',
|
||||
'author': "OpenERP SA, "
|
||||
"Tecnativa, "
|
||||
"LasLabs, "
|
||||
"Odoo Community Association (OCA)",
|
||||
"Tecnativa, "
|
||||
"LasLabs, "
|
||||
"ACSONE SA/NV, "
|
||||
"Odoo Community Association (OCA)",
|
||||
'website': 'https://github.com/oca/contract',
|
||||
'depends': ['base', 'account', 'analytic'],
|
||||
'data': [
|
||||
@@ -24,9 +26,12 @@
|
||||
'report/contract_views.xml',
|
||||
'data/contract_cron.xml',
|
||||
'data/mail_template.xml',
|
||||
'views/account_analytic_account_view.xml',
|
||||
'views/account_analytic_contract_view.xml',
|
||||
'views/abstract_contract_line.xml',
|
||||
'views/contract.xml',
|
||||
'views/contract_template_line.xml',
|
||||
'views/contract_template.xml',
|
||||
'views/account_invoice_view.xml',
|
||||
'views/contract_line.xml',
|
||||
'views/res_partner_view.xml',
|
||||
],
|
||||
'installable': True,
|
||||
|
||||
19
contract/migrations/12.0.2.0.0/post-migration.py
Normal file
19
contract/migrations/12.0.2.0.0/post-migration.py
Normal file
@@ -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).
|
||||
|
||||
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 res_partner
|
||||
|
||||
69
contract/models/abstract_contract.py
Normal file
69
contract/models/abstract_contract.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# Copyright 2004-2010 OpenERP SA
|
||||
# Copyright 2014 Angel Moya <angel.moya@domatix.com>
|
||||
# Copyright 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
||||
# Copyright 2016-2018 Carlos Dauden <carlos.dauden@tecnativa.com>
|
||||
# Copyright 2016-2017 LasLabs Inc.
|
||||
# Copyright 2018 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, models, fields
|
||||
|
||||
|
||||
class AbstractAccountAnalyticContract(models.AbstractModel):
|
||||
_name = 'account.abstract.analytic.contract'
|
||||
_description = 'Abstract Account Analytic Contract'
|
||||
|
||||
# These fields will not be synced to the contract
|
||||
NO_SYNC = ['name', 'partner_id']
|
||||
|
||||
name = fields.Char(required=True)
|
||||
# Needed for avoiding errors on several inherited behaviors
|
||||
partner_id = fields.Many2one(
|
||||
comodel_name="res.partner", string="Partner (always False)"
|
||||
)
|
||||
pricelist_id = fields.Many2one(
|
||||
comodel_name='product.pricelist', string='Pricelist'
|
||||
)
|
||||
contract_type = fields.Selection(
|
||||
selection=[('sale', 'Customer'), ('purchase', 'Supplier')],
|
||||
default='sale',
|
||||
)
|
||||
|
||||
journal_id = fields.Many2one(
|
||||
'account.journal',
|
||||
string='Journal',
|
||||
default=lambda s: s._default_journal(),
|
||||
domain="[('type', '=', contract_type),"
|
||||
"('company_id', '=', company_id)]",
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
required=True,
|
||||
default=lambda self: self.env.user.company_id,
|
||||
)
|
||||
|
||||
@api.onchange('contract_type')
|
||||
def _onchange_contract_type(self):
|
||||
if self.contract_type == 'purchase':
|
||||
self.recurring_invoice_line_ids.filtered('automatic_price').update(
|
||||
{'automatic_price': False}
|
||||
)
|
||||
self.journal_id = self.env['account.journal'].search(
|
||||
[
|
||||
('type', '=', self.contract_type),
|
||||
('company_id', '=', self.company_id.id),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _default_journal(self):
|
||||
company_id = self.env.context.get(
|
||||
'company_id', self.env.user.company_id.id
|
||||
)
|
||||
domain = [
|
||||
('type', '=', self.contract_type),
|
||||
('company_id', '=', company_id),
|
||||
]
|
||||
return self.env['account.journal'].search(domain, limit=1)
|
||||
184
contract/models/abstract_contract_line.py
Normal file
184
contract/models/abstract_contract_line.py
Normal file
@@ -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',
|
||||
)
|
||||
@@ -8,5 +8,5 @@ class AccountInvoice(models.Model):
|
||||
_inherit = 'account.invoice'
|
||||
|
||||
contract_id = fields.Many2one(
|
||||
'account.analytic.account',
|
||||
string='Contract')
|
||||
'account.analytic.account', string='Contract'
|
||||
)
|
||||
|
||||
223
contract/models/contract.py
Normal file
223
contract/models/contract.py
Normal file
@@ -0,0 +1,223 @@
|
||||
# Copyright 2004-2010 OpenERP SA
|
||||
# Copyright 2014 Angel Moya <angel.moya@domatix.com>
|
||||
# Copyright 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
||||
# Copyright 2016-2018 Carlos Dauden <carlos.dauden@tecnativa.com>
|
||||
# Copyright 2016-2017 LasLabs Inc.
|
||||
# Copyright 2018 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.translate import _
|
||||
|
||||
|
||||
class AccountAnalyticAccount(models.Model):
|
||||
_name = 'account.analytic.account'
|
||||
_inherit = [
|
||||
'account.analytic.account',
|
||||
'account.abstract.analytic.contract',
|
||||
]
|
||||
|
||||
contract_template_id = fields.Many2one(
|
||||
string='Contract Template', comodel_name='account.analytic.contract'
|
||||
)
|
||||
recurring_invoice_line_ids = fields.One2many(
|
||||
string='Invoice Lines',
|
||||
comodel_name='account.analytic.invoice.line',
|
||||
inverse_name='contract_id',
|
||||
copy=True,
|
||||
)
|
||||
recurring_invoices = fields.Boolean(
|
||||
string='Generate recurring invoices automatically'
|
||||
)
|
||||
user_id = fields.Many2one(
|
||||
comodel_name='res.users',
|
||||
string='Responsible',
|
||||
index=True,
|
||||
default=lambda self: self.env.user,
|
||||
)
|
||||
create_invoice_visibility = fields.Boolean(
|
||||
compute='_compute_create_invoice_visibility'
|
||||
)
|
||||
recurring_next_date = fields.Date(
|
||||
compute='_compute_recurring_next_date',
|
||||
string='Date of Next Invoice',
|
||||
store=True,
|
||||
)
|
||||
date_end = fields.Date(
|
||||
compute='_compute_date_end', string='Date End', store=True
|
||||
)
|
||||
|
||||
@api.depends('recurring_invoice_line_ids.date_end')
|
||||
def _compute_date_end(self):
|
||||
for contract in self:
|
||||
contract.date_end = False
|
||||
date_end = contract.recurring_invoice_line_ids.mapped('date_end')
|
||||
if date_end and all(date_end):
|
||||
contract.date_end = max(date_end)
|
||||
|
||||
@api.depends('recurring_invoice_line_ids.recurring_next_date')
|
||||
def _compute_recurring_next_date(self):
|
||||
for contract in self:
|
||||
recurring_next_date = contract.recurring_invoice_line_ids.filtered(
|
||||
'create_invoice_visibility'
|
||||
).mapped('recurring_next_date')
|
||||
if recurring_next_date:
|
||||
contract.recurring_next_date = min(recurring_next_date)
|
||||
|
||||
@api.depends('recurring_invoice_line_ids.create_invoice_visibility')
|
||||
def _compute_create_invoice_visibility(self):
|
||||
for contract in self:
|
||||
contract.create_invoice_visibility = any(
|
||||
contract.recurring_invoice_line_ids.mapped(
|
||||
'create_invoice_visibility'
|
||||
)
|
||||
)
|
||||
|
||||
@api.onchange('contract_template_id')
|
||||
def _onchange_contract_template_id(self):
|
||||
"""Update the contract fields with that of the template.
|
||||
|
||||
Take special consideration with the `recurring_invoice_line_ids`,
|
||||
which must be created using the data from the contract lines. Cascade
|
||||
deletion ensures that any errant lines that are created are also
|
||||
deleted.
|
||||
"""
|
||||
contract_template_id = self.contract_template_id
|
||||
if not contract_template_id:
|
||||
return
|
||||
for field_name, field in contract_template_id._fields.items():
|
||||
if field.name == 'recurring_invoice_line_ids':
|
||||
lines = self._convert_contract_lines(contract_template_id)
|
||||
self.recurring_invoice_line_ids = lines
|
||||
elif not any(
|
||||
(
|
||||
field.compute,
|
||||
field.related,
|
||||
field.automatic,
|
||||
field.readonly,
|
||||
field.company_dependent,
|
||||
field.name in self.NO_SYNC,
|
||||
)
|
||||
):
|
||||
self[field_name] = self.contract_template_id[field_name]
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def _onchange_partner_id(self):
|
||||
self.pricelist_id = self.partner_id.property_product_pricelist.id
|
||||
|
||||
@api.constrains('partner_id', 'recurring_invoices')
|
||||
def _check_partner_id_recurring_invoices(self):
|
||||
for contract in self.filtered('recurring_invoices'):
|
||||
if not contract.partner_id:
|
||||
raise ValidationError(
|
||||
_("You must supply a customer for the contract '%s'")
|
||||
% contract.name
|
||||
)
|
||||
|
||||
@api.multi
|
||||
def _convert_contract_lines(self, contract):
|
||||
self.ensure_one()
|
||||
new_lines = []
|
||||
for contract_line in contract.recurring_invoice_line_ids:
|
||||
vals = contract_line._convert_to_write(contract_line.read()[0])
|
||||
# Remove template link field
|
||||
vals.pop('contract_template_id', False)
|
||||
vals['date_start'] = fields.Date.today()
|
||||
vals['recurring_next_date'] = fields.Date.today()
|
||||
self.recurring_invoice_line_ids._onchange_date_start()
|
||||
new_lines.append((0, 0, vals))
|
||||
return new_lines
|
||||
|
||||
@api.multi
|
||||
def _prepare_invoice(self, date_invoice, journal=None):
|
||||
self.ensure_one()
|
||||
if not self.partner_id:
|
||||
if self.contract_type == 'purchase':
|
||||
raise ValidationError(
|
||||
_("You must first select a Supplier for Contract %s!")
|
||||
% self.name
|
||||
)
|
||||
else:
|
||||
raise ValidationError(
|
||||
_("You must first select a Customer for Contract %s!")
|
||||
% self.name
|
||||
)
|
||||
if not journal:
|
||||
journal = (
|
||||
self.journal_id
|
||||
if self.journal_id.type == self.contract_type
|
||||
else self.env['account.journal'].search(
|
||||
[
|
||||
('type', '=', self.contract_type),
|
||||
('company_id', '=', self.company_id.id),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
)
|
||||
if not journal:
|
||||
raise ValidationError(
|
||||
_("Please define a %s journal for the company '%s'.")
|
||||
% (self.contract_type, self.company_id.name or '')
|
||||
)
|
||||
currency = (
|
||||
self.pricelist_id.currency_id
|
||||
or self.partner_id.property_product_pricelist.currency_id
|
||||
or self.company_id.currency_id
|
||||
)
|
||||
invoice_type = 'out_invoice'
|
||||
if self.contract_type == 'purchase':
|
||||
invoice_type = 'in_invoice'
|
||||
invoice = self.env['account.invoice'].new(
|
||||
{
|
||||
'reference': self.code,
|
||||
'type': invoice_type,
|
||||
'partner_id': self.partner_id.address_get(['invoice'])[
|
||||
'invoice'
|
||||
],
|
||||
'currency_id': currency.id,
|
||||
'date_invoice': date_invoice,
|
||||
'journal_id': journal.id,
|
||||
'origin': self.name,
|
||||
'company_id': self.company_id.id,
|
||||
'contract_id': self.id,
|
||||
'user_id': self.partner_id.user_id.id,
|
||||
}
|
||||
)
|
||||
# Get other invoice values from partner onchange
|
||||
invoice._onchange_partner_id()
|
||||
return invoice._convert_to_write(invoice._cache)
|
||||
|
||||
@api.multi
|
||||
def action_contract_send(self):
|
||||
self.ensure_one()
|
||||
template = self.env.ref('contract.email_contract_template', False)
|
||||
compose_form = self.env.ref('mail.email_compose_message_wizard_form')
|
||||
ctx = dict(
|
||||
default_model='account.analytic.account',
|
||||
default_res_id=self.id,
|
||||
default_use_template=bool(template),
|
||||
default_template_id=template and template.id or False,
|
||||
default_composition_mode='comment',
|
||||
)
|
||||
return {
|
||||
'name': _('Compose Email'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'mail.compose.message',
|
||||
'views': [(compose_form.id, 'form')],
|
||||
'view_id': compose_form.id,
|
||||
'target': 'new',
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def recurring_create_invoice(self):
|
||||
return self.env[
|
||||
'account.analytic.invoice.line'
|
||||
].recurring_create_invoice(self)
|
||||
|
||||
@api.model
|
||||
def cron_recurring_create_invoice(self):
|
||||
self.env['account.analytic.invoice.line'].recurring_create_invoice()
|
||||
250
contract/models/contract_line.py
Normal file
250
contract/models/contract_line.py
Normal file
@@ -0,0 +1,250 @@
|
||||
# Copyright 2017 LasLabs Inc.
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class AccountAnalyticInvoiceLine(models.Model):
|
||||
_name = 'account.analytic.invoice.line'
|
||||
_inherit = 'account.abstract.analytic.contract.line'
|
||||
|
||||
contract_id = fields.Many2one(
|
||||
comodel_name='account.analytic.account',
|
||||
string='Analytic Account',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
oldname='analytic_account_id',
|
||||
)
|
||||
date_start = fields.Date(string='Date Start', default=fields.Date.today())
|
||||
date_end = fields.Date(string='Date End', index=True)
|
||||
recurring_next_date = fields.Date(
|
||||
copy=False, string='Date of Next Invoice'
|
||||
)
|
||||
create_invoice_visibility = fields.Boolean(
|
||||
compute='_compute_create_invoice_visibility'
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
comodel_name="res.partner",
|
||||
string="Partner (always False)",
|
||||
related='contract_id.partner_id',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
pricelist_id = fields.Many2one(
|
||||
comodel_name='product.pricelist',
|
||||
string='Pricelist',
|
||||
related='contract_id.pricelist_id',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _compute_first_recurring_next_date(
|
||||
self,
|
||||
date_start,
|
||||
recurring_invoicing_type,
|
||||
recurring_rule_type,
|
||||
recurring_interval,
|
||||
):
|
||||
if recurring_rule_type == 'monthlylastday':
|
||||
return date_start + self.get_relative_delta(
|
||||
recurring_rule_type, recurring_interval - 1
|
||||
)
|
||||
if recurring_invoicing_type == 'pre-paid':
|
||||
return date_start
|
||||
return date_start + self.get_relative_delta(
|
||||
recurring_rule_type, recurring_interval
|
||||
)
|
||||
|
||||
@api.onchange(
|
||||
'date_start',
|
||||
'recurring_invoicing_type',
|
||||
'recurring_rule_type',
|
||||
'recurring_interval',
|
||||
)
|
||||
def _onchange_date_start(self):
|
||||
for rec in self.filtered('date_start'):
|
||||
rec.recurring_next_date = self._compute_first_recurring_next_date(
|
||||
rec.date_start,
|
||||
rec.recurring_invoicing_type,
|
||||
rec.recurring_rule_type,
|
||||
rec.recurring_interval,
|
||||
)
|
||||
|
||||
@api.constrains('recurring_next_date', 'date_start')
|
||||
def _check_recurring_next_date_start_date(self):
|
||||
for line in self.filtered('recurring_next_date'):
|
||||
if line.date_start and line.recurring_next_date:
|
||||
if line.date_start > line.recurring_next_date:
|
||||
raise ValidationError(
|
||||
_(
|
||||
"You can't have a next invoicing date before the "
|
||||
"start of the contract '%s'"
|
||||
)
|
||||
% line.contract_id.name
|
||||
)
|
||||
|
||||
@api.constrains('recurring_next_date')
|
||||
def _check_recurring_next_date_recurring_invoices(self):
|
||||
for line in self.filtered('contract_id.recurring_invoices'):
|
||||
if not line.recurring_next_date:
|
||||
raise ValidationError(
|
||||
_(
|
||||
"You must supply a next invoicing date for contract "
|
||||
"'%s'"
|
||||
)
|
||||
% line.contract_id.name
|
||||
)
|
||||
|
||||
@api.constrains('date_start')
|
||||
def _check_date_start_recurring_invoices(self):
|
||||
for line in self.filtered('contract_id.recurring_invoices'):
|
||||
if not line.date_start:
|
||||
raise ValidationError(
|
||||
_("You must supply a start date for contract '%s'")
|
||||
% line.contract_id.name
|
||||
)
|
||||
|
||||
@api.constrains('date_start', 'date_end')
|
||||
def _check_start_end_dates(self):
|
||||
for line in self.filtered('date_end'):
|
||||
if line.date_start and line.date_end:
|
||||
if line.date_start > line.date_end:
|
||||
raise ValidationError(
|
||||
_(
|
||||
"Contract '%s' start date can't be later than "
|
||||
"end date"
|
||||
)
|
||||
% line.contract_id.name
|
||||
)
|
||||
|
||||
@api.depends('recurring_next_date', 'date_end')
|
||||
def _compute_create_invoice_visibility(self):
|
||||
for line in self:
|
||||
line.create_invoice_visibility = not line.date_end or (
|
||||
line.recurring_next_date
|
||||
and line.date_end
|
||||
and line.recurring_next_date <= line.date_end
|
||||
)
|
||||
|
||||
@api.model
|
||||
def recurring_create_invoice(self, contract=False):
|
||||
domain = []
|
||||
date_ref = fields.Date.today()
|
||||
if contract:
|
||||
contract.ensure_one()
|
||||
date_ref = contract.recurring_next_date
|
||||
domain.append(('contract_id', '=', contract.id))
|
||||
|
||||
domain.extend(
|
||||
[
|
||||
('contract_id.recurring_invoices', '=', True),
|
||||
('recurring_next_date', '<=', date_ref),
|
||||
'|',
|
||||
('date_end', '=', False),
|
||||
('date_end', '>=', date_ref),
|
||||
]
|
||||
)
|
||||
lines = self.search(domain).filtered('create_invoice_visibility')
|
||||
if lines:
|
||||
return lines._recurring_create_invoice()
|
||||
return False
|
||||
|
||||
@api.multi
|
||||
def _recurring_create_invoice(self):
|
||||
"""Create invoices from contracts
|
||||
|
||||
:return: invoices created
|
||||
"""
|
||||
invoices = self.env['account.invoice']
|
||||
for contract in self.mapped('contract_id'):
|
||||
lines = self.filtered(lambda l: l.contract_id == contract)
|
||||
invoices |= lines._create_invoice()
|
||||
lines._update_recurring_next_date()
|
||||
return invoices
|
||||
|
||||
@api.multi
|
||||
def _create_invoice(self):
|
||||
"""
|
||||
:param invoice: If not False add lines to this invoice
|
||||
:return: invoice created or updated
|
||||
"""
|
||||
contract = self.mapped('contract_id')
|
||||
date_invoice = min(self.mapped('recurring_next_date'))
|
||||
invoice = self.env['account.invoice'].create(
|
||||
contract._prepare_invoice(date_invoice)
|
||||
)
|
||||
for line in self:
|
||||
invoice_line_vals = line._prepare_invoice_line(invoice.id)
|
||||
if invoice_line_vals:
|
||||
self.env['account.invoice.line'].create(invoice_line_vals)
|
||||
invoice.compute_taxes()
|
||||
return invoice
|
||||
|
||||
@api.multi
|
||||
def _prepare_invoice_line(self, invoice_id):
|
||||
self.ensure_one()
|
||||
invoice_line = self.env['account.invoice.line'].new(
|
||||
{
|
||||
'invoice_id': invoice_id,
|
||||
'product_id': self.product_id.id,
|
||||
'quantity': self.quantity,
|
||||
'uom_id': self.uom_id.id,
|
||||
'discount': self.discount,
|
||||
}
|
||||
)
|
||||
# Get other invoice line values from product onchange
|
||||
invoice_line._onchange_product_id()
|
||||
invoice_line_vals = invoice_line._convert_to_write(invoice_line._cache)
|
||||
# Insert markers
|
||||
contract = self.contract_id
|
||||
lang_obj = self.env['res.lang']
|
||||
lang = lang_obj.search([('code', '=', contract.partner_id.lang)])
|
||||
date_format = lang.date_format or '%m/%d/%Y'
|
||||
name = self._insert_markers(date_format)
|
||||
invoice_line_vals.update(
|
||||
{
|
||||
'name': name,
|
||||
'account_analytic_id': contract.id,
|
||||
'price_unit': self.price_unit,
|
||||
}
|
||||
)
|
||||
return invoice_line_vals
|
||||
|
||||
@api.multi
|
||||
def _insert_markers(self, date_format):
|
||||
self.ensure_one()
|
||||
date_from = fields.Date.from_string(self.recurring_next_date)
|
||||
date_to = date_from + self.get_relative_delta(
|
||||
self.recurring_rule_type, self.recurring_interval
|
||||
)
|
||||
name = self.name
|
||||
name = name.replace('#START#', date_from.strftime(date_format))
|
||||
name = name.replace('#END#', date_to.strftime(date_format))
|
||||
return name
|
||||
|
||||
@api.multi
|
||||
def _update_recurring_next_date(self):
|
||||
for line in self:
|
||||
ref_date = line.recurring_next_date or fields.Date.today()
|
||||
old_date = fields.Date.from_string(ref_date)
|
||||
new_date = old_date + self.get_relative_delta(
|
||||
line.recurring_rule_type, line.recurring_interval
|
||||
)
|
||||
line.recurring_next_date = new_date
|
||||
|
||||
@api.model
|
||||
def get_relative_delta(self, recurring_rule_type, interval):
|
||||
if recurring_rule_type == 'daily':
|
||||
return relativedelta(days=interval)
|
||||
elif recurring_rule_type == 'weekly':
|
||||
return relativedelta(weeks=interval)
|
||||
elif recurring_rule_type == 'monthly':
|
||||
return relativedelta(months=interval)
|
||||
elif recurring_rule_type == 'monthlylastday':
|
||||
return relativedelta(months=interval, day=31)
|
||||
else:
|
||||
return relativedelta(years=interval)
|
||||
22
contract/models/contract_template.py
Normal file
22
contract/models/contract_template.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Copyright 2004-2010 OpenERP SA
|
||||
# Copyright 2014 Angel Moya <angel.moya@domatix.com>
|
||||
# Copyright 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
||||
# Copyright 2016-2018 Carlos Dauden <carlos.dauden@tecnativa.com>
|
||||
# Copyright 2016-2017 LasLabs Inc.
|
||||
# Copyright 2018 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class AccountAnalyticContract(models.Model):
|
||||
_name = 'account.analytic.contract'
|
||||
_inherit = 'account.abstract.analytic.contract'
|
||||
_description = "Account Analytic Contract"
|
||||
|
||||
recurring_invoice_line_ids = fields.One2many(
|
||||
comodel_name='account.analytic.contract.line',
|
||||
inverse_name='contract_template_id',
|
||||
copy=True,
|
||||
string='Invoice Lines',
|
||||
)
|
||||
38
contract/models/contract_template_line.py
Normal file
38
contract/models/contract_template_line.py
Normal file
@@ -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,
|
||||
)
|
||||
@@ -8,35 +8,47 @@ class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
sale_contract_count = fields.Integer(
|
||||
string='Sale Contracts',
|
||||
compute='_compute_contract_count',
|
||||
string='Sale Contracts', compute='_compute_contract_count'
|
||||
)
|
||||
purchase_contract_count = fields.Integer(
|
||||
string='Purchase Contracts',
|
||||
compute='_compute_contract_count',
|
||||
string='Purchase Contracts', compute='_compute_contract_count'
|
||||
)
|
||||
|
||||
def _compute_contract_count(self):
|
||||
contract_model = self.env['account.analytic.account']
|
||||
today = fields.Date.today()
|
||||
fetch_data = contract_model.read_group([
|
||||
('recurring_invoices', '=', True),
|
||||
('partner_id', 'child_of', self.ids),
|
||||
'|',
|
||||
('date_end', '=', False),
|
||||
('date_end', '>=', today)],
|
||||
['partner_id', 'contract_type'], ['partner_id', 'contract_type'],
|
||||
lazy=False)
|
||||
result = [[data['partner_id'][0], data['contract_type'],
|
||||
data['__count']] for data in fetch_data]
|
||||
fetch_data = contract_model.read_group(
|
||||
[
|
||||
('recurring_invoices', '=', True),
|
||||
('partner_id', 'child_of', self.ids),
|
||||
'|',
|
||||
('date_end', '=', False),
|
||||
('date_end', '>=', today),
|
||||
],
|
||||
['partner_id', 'contract_type'],
|
||||
['partner_id', 'contract_type'],
|
||||
lazy=False,
|
||||
)
|
||||
result = [
|
||||
[data['partner_id'][0], data['contract_type'], data['__count']]
|
||||
for data in fetch_data
|
||||
]
|
||||
for partner in self:
|
||||
partner_child_ids = partner.child_ids.ids + partner.ids
|
||||
partner.sale_contract_count = sum([
|
||||
r[2] for r in result
|
||||
if r[0] in partner_child_ids and r[1] == 'sale'])
|
||||
partner.purchase_contract_count = sum([
|
||||
r[2] for r in result
|
||||
if r[0] in partner_child_ids and r[1] == 'purchase'])
|
||||
partner.sale_contract_count = sum(
|
||||
[
|
||||
r[2]
|
||||
for r in result
|
||||
if r[0] in partner_child_ids and r[1] == 'sale'
|
||||
]
|
||||
)
|
||||
partner.purchase_contract_count = sum(
|
||||
[
|
||||
r[2]
|
||||
for r in result
|
||||
if r[0] in partner_child_ids and r[1] == 'purchase'
|
||||
]
|
||||
)
|
||||
|
||||
def act_show_contract(self):
|
||||
""" This opens contract view
|
||||
@@ -55,14 +67,16 @@ class ResPartner(models.Model):
|
||||
default_partner_id=self.id,
|
||||
default_recurring_invoices=True,
|
||||
default_pricelist_id=self.property_product_pricelist.id,
|
||||
),
|
||||
)
|
||||
)
|
||||
return res
|
||||
|
||||
def _get_act_window_contract_xml(self, contract_type):
|
||||
if contract_type == 'purchase':
|
||||
return self.env['ir.actions.act_window'].for_xml_id(
|
||||
'contract', 'action_account_analytic_purchase_overdue_all')
|
||||
'contract', 'action_account_analytic_purchase_overdue_all'
|
||||
)
|
||||
else:
|
||||
return self.env['ir.actions.act_window'].for_xml_id(
|
||||
'contract', 'action_account_analytic_sale_overdue_all')
|
||||
'contract', 'action_account_analytic_sale_overdue_all'
|
||||
)
|
||||
|
||||
@@ -9,34 +9,54 @@
|
||||
<div class="oe_structure"/>
|
||||
<div class="row" id="partner_info">
|
||||
<div class="col-xs-5 col-xs-offset-7">
|
||||
<p id="partner_info"><strong>Partner:</strong></p>
|
||||
<div t-field="o.partner_id" t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "mobile", "fax", "email"], "no_marker": true, "phone_icons": true}'/>
|
||||
<p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
|
||||
<p id="partner_info">
|
||||
<strong>Partner:</strong>
|
||||
</p>
|
||||
<div t-field="o.partner_id"
|
||||
t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "mobile", "fax", "email"], "no_marker": true, "phone_icons": true}'/>
|
||||
<p t-if="o.partner_id.vat">VAT:
|
||||
<span t-field="o.partner_id.vat"/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" id="header_info">
|
||||
<div class="col-xs-3">
|
||||
<strong>Date Start: </strong><p t-field="o.date_start"/>
|
||||
<strong>Responsible: </strong><p t-field="o.user_id"/>
|
||||
<strong>Contract: </strong><p t-field="o.code"/>
|
||||
<strong>Responsible:</strong>
|
||||
<p t-field="o.user_id"/>
|
||||
<strong>Contract:</strong>
|
||||
<p t-field="o.code"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" id="invoice_info">
|
||||
<t t-set="total" t-value="0"/>
|
||||
<div class="col-xs-12">
|
||||
<t t-set="total" t-value="0"/>
|
||||
<p id="services_info"><strong>Recurring Items</strong></p>
|
||||
<p id="services_info">
|
||||
<strong>Recurring Items</strong>
|
||||
</p>
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><strong>Description</strong></th>
|
||||
<th class="text-right"><strong>Quantity</strong></th>
|
||||
<th class="text-right"><strong>Unit Price</strong></th>
|
||||
<th class="text-right"><strong>Price</strong></th>
|
||||
<th>
|
||||
<strong>Description</strong>
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<strong>Quantity</strong>
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<strong>Unit Price</strong>
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<strong>Price</strong>
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<strong>Date Start</strong>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="o.recurring_invoice_line_ids" t-as="l">
|
||||
<tr t-foreach="o.recurring_invoice_line_ids"
|
||||
t-as="l">
|
||||
<td>
|
||||
<span t-field="l.name"/>
|
||||
</td>
|
||||
@@ -44,12 +64,18 @@
|
||||
<span t-field="l.quantity"/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-field="l.price_unit" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
|
||||
<span t-field="l.price_unit"
|
||||
t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-field="l.price_subtotal" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
|
||||
<span t-field="l.price_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
|
||||
</td>
|
||||
<t t-set="total" t-value="total + l.price_subtotal"/>
|
||||
<td>
|
||||
<span t-field="l.date_start"/>
|
||||
</td>
|
||||
<t t-set="total"
|
||||
t-value="total + l.price_subtotal"/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -57,9 +83,12 @@
|
||||
<div class="col-xs-4 pull-right">
|
||||
<table class="table table-condensed">
|
||||
<tr class="border-black">
|
||||
<td><strong>Total</strong></td>
|
||||
<td>
|
||||
<strong>Total</strong>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-esc="total" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
|
||||
<span t-esc="total"
|
||||
t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -7,6 +7,10 @@ from odoo.exceptions import ValidationError
|
||||
from odoo.tests import common
|
||||
|
||||
|
||||
def to_date(date):
|
||||
return fields.Date.to_date(date)
|
||||
|
||||
|
||||
class TestContractBase(common.SavepointCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@@ -14,51 +18,85 @@ class TestContractBase(common.SavepointCase):
|
||||
cls.partner = cls.env.ref('base.res_partner_2')
|
||||
cls.product = cls.env.ref('product.product_product_2')
|
||||
cls.product.taxes_id += cls.env['account.tax'].search(
|
||||
[('type_tax_use', '=', 'sale')], limit=1)
|
||||
cls.product.description_sale = 'Test description sale'
|
||||
cls.template_vals = {
|
||||
'recurring_rule_type': 'yearly',
|
||||
'recurring_interval': 12345,
|
||||
'name': 'Test Contract Template',
|
||||
}
|
||||
cls.template = cls.env['account.analytic.contract'].create(
|
||||
cls.template_vals,
|
||||
[('type_tax_use', '=', 'sale')], limit=1
|
||||
)
|
||||
# For being sure of the applied price
|
||||
cls.env['product.pricelist.item'].create({
|
||||
'pricelist_id': cls.partner.property_product_pricelist.id,
|
||||
'product_id': cls.product.id,
|
||||
'compute_price': 'formula',
|
||||
'base': 'list_price',
|
||||
})
|
||||
cls.contract = cls.env['account.analytic.account'].create({
|
||||
'name': 'Test Contract',
|
||||
'partner_id': cls.partner.id,
|
||||
'pricelist_id': cls.partner.property_product_pricelist.id,
|
||||
'recurring_invoices': True,
|
||||
'date_start': '2016-02-15',
|
||||
'recurring_next_date': '2016-02-29',
|
||||
})
|
||||
cls.contract2 = cls.env['account.analytic.account'].create({
|
||||
'name': 'Test Contract 2',
|
||||
'partner_id': cls.partner.id,
|
||||
'pricelist_id': cls.partner.property_product_pricelist.id,
|
||||
'recurring_invoices': True,
|
||||
'date_start': '2016-02-15',
|
||||
'recurring_next_date': '2016-02-29',
|
||||
'contract_type': 'purchase',
|
||||
})
|
||||
cls.line_vals = {
|
||||
'analytic_account_id': cls.contract.id,
|
||||
cls.product.description_sale = 'Test description sale'
|
||||
cls.line_template_vals = {
|
||||
'product_id': cls.product.id,
|
||||
'name': 'Services from #START# to #END#',
|
||||
'quantity': 1,
|
||||
'uom_id': cls.product.uom_id.id,
|
||||
'price_unit': 100,
|
||||
'discount': 50,
|
||||
'recurring_rule_type': 'yearly',
|
||||
'recurring_interval': 1,
|
||||
}
|
||||
cls.template_vals = {
|
||||
'name': 'Test Contract Template',
|
||||
'recurring_invoice_line_ids': [(0, 0, cls.line_template_vals)],
|
||||
}
|
||||
cls.template = cls.env['account.analytic.contract'].create(
|
||||
cls.template_vals
|
||||
)
|
||||
# For being sure of the applied price
|
||||
cls.env['product.pricelist.item'].create(
|
||||
{
|
||||
'pricelist_id': cls.partner.property_product_pricelist.id,
|
||||
'product_id': cls.product.id,
|
||||
'compute_price': 'formula',
|
||||
'base': 'list_price',
|
||||
}
|
||||
)
|
||||
cls.contract = cls.env['account.analytic.account'].create(
|
||||
{
|
||||
'name': 'Test Contract',
|
||||
'partner_id': cls.partner.id,
|
||||
'pricelist_id': cls.partner.property_product_pricelist.id,
|
||||
'recurring_invoices': True,
|
||||
}
|
||||
)
|
||||
cls.contract2 = cls.env['account.analytic.account'].create(
|
||||
{
|
||||
'name': 'Test Contract 2',
|
||||
'partner_id': cls.partner.id,
|
||||
'pricelist_id': cls.partner.property_product_pricelist.id,
|
||||
'recurring_invoices': True,
|
||||
'contract_type': 'purchase',
|
||||
'recurring_invoice_line_ids': [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
'product_id': cls.product.id,
|
||||
'name': 'Services from #START# to #END#',
|
||||
'quantity': 1,
|
||||
'uom_id': cls.product.uom_id.id,
|
||||
'price_unit': 100,
|
||||
'discount': 50,
|
||||
'recurring_rule_type': 'monthly',
|
||||
'recurring_interval': 1,
|
||||
'date_start': '2016-02-15',
|
||||
'recurring_next_date': '2016-02-29',
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
cls.line_vals = {
|
||||
'contract_id': cls.contract.id,
|
||||
'product_id': cls.product.id,
|
||||
'name': 'Services from #START# to #END#',
|
||||
'quantity': 1,
|
||||
'uom_id': cls.product.uom_id.id,
|
||||
'price_unit': 100,
|
||||
'discount': 50,
|
||||
'recurring_rule_type': 'monthly',
|
||||
'recurring_interval': 1,
|
||||
'date_start': '2016-02-15',
|
||||
'recurring_next_date': '2016-02-29',
|
||||
}
|
||||
cls.acct_line = cls.env['account.analytic.invoice.line'].create(
|
||||
cls.line_vals,
|
||||
cls.line_vals
|
||||
)
|
||||
|
||||
|
||||
@@ -67,7 +105,9 @@ class TestContract(TestContractBase):
|
||||
if overrides is None:
|
||||
overrides = {}
|
||||
vals = self.line_vals.copy()
|
||||
vals['analytic_account_id'] = self.template.id
|
||||
del vals['contract_id']
|
||||
del vals['date_start']
|
||||
vals['contract_template_id'] = self.template.id
|
||||
vals.update(overrides)
|
||||
return self.env['account.analytic.contract.line'].create(vals)
|
||||
|
||||
@@ -90,7 +130,7 @@ class TestContract(TestContractBase):
|
||||
self.assertEqual(self.acct_line.price_unit, 10)
|
||||
|
||||
def test_contract(self):
|
||||
recurring_next_date = fields.Date.to_date('2016-03-29')
|
||||
recurring_next_date = to_date('2016-03-29')
|
||||
self.assertAlmostEqual(self.acct_line.price_subtotal, 50.0)
|
||||
res = self.acct_line._onchange_product_id()
|
||||
self.assertIn('uom_id', res['domain'])
|
||||
@@ -100,81 +140,96 @@ class TestContract(TestContractBase):
|
||||
self.contract.partner_id = self.partner.id
|
||||
self.contract.recurring_create_invoice()
|
||||
self.invoice_monthly = self.env['account.invoice'].search(
|
||||
[('contract_id', '=', self.contract.id)])
|
||||
[('contract_id', '=', self.contract.id)]
|
||||
)
|
||||
self.assertTrue(self.invoice_monthly)
|
||||
self.assertEqual(self.contract.recurring_next_date,
|
||||
recurring_next_date)
|
||||
self.assertEqual(
|
||||
self.acct_line.recurring_next_date, recurring_next_date
|
||||
)
|
||||
self.inv_line = self.invoice_monthly.invoice_line_ids[0]
|
||||
self.assertTrue(self.inv_line.invoice_line_tax_ids)
|
||||
self.assertAlmostEqual(self.inv_line.price_subtotal, 50.0)
|
||||
self.assertEqual(self.contract.partner_id.user_id,
|
||||
self.invoice_monthly.user_id)
|
||||
self.assertEqual(
|
||||
self.contract.partner_id.user_id, self.invoice_monthly.user_id
|
||||
)
|
||||
|
||||
def test_contract_daily(self):
|
||||
recurring_next_date = fields.Date.to_date('2016-03-01')
|
||||
self.contract.recurring_next_date = '2016-02-29'
|
||||
self.contract.recurring_rule_type = 'daily'
|
||||
recurring_next_date = to_date('2016-03-01')
|
||||
self.acct_line.recurring_next_date = '2016-02-29'
|
||||
self.acct_line.recurring_rule_type = 'daily'
|
||||
self.contract.pricelist_id = False
|
||||
self.contract.cron_recurring_create_invoice()
|
||||
self.contract.recurring_create_invoice()
|
||||
invoice_daily = self.env['account.invoice'].search(
|
||||
[('contract_id', '=', self.contract.id)])
|
||||
[('contract_id', '=', self.contract.id)]
|
||||
)
|
||||
self.assertTrue(invoice_daily)
|
||||
self.assertEqual(self.contract.recurring_next_date,
|
||||
recurring_next_date)
|
||||
self.assertEqual(
|
||||
self.acct_line.recurring_next_date, recurring_next_date
|
||||
)
|
||||
|
||||
def test_contract_weekly(self):
|
||||
recurring_next_date = fields.Date.to_date('2016-03-07')
|
||||
self.contract.recurring_next_date = '2016-02-29'
|
||||
self.contract.recurring_rule_type = 'weekly'
|
||||
self.contract.recurring_invoicing_type = 'post-paid'
|
||||
recurring_next_date = to_date('2016-03-07')
|
||||
self.acct_line.recurring_next_date = '2016-02-29'
|
||||
self.acct_line.recurring_rule_type = 'weekly'
|
||||
self.acct_line.recurring_invoicing_type = 'post-paid'
|
||||
self.contract.recurring_create_invoice()
|
||||
invoices_weekly = self.env['account.invoice'].search(
|
||||
[('contract_id', '=', self.contract.id)])
|
||||
[('contract_id', '=', self.contract.id)]
|
||||
)
|
||||
self.assertTrue(invoices_weekly)
|
||||
self.assertEqual(
|
||||
self.contract.recurring_next_date, recurring_next_date)
|
||||
self.acct_line.recurring_next_date, recurring_next_date
|
||||
)
|
||||
|
||||
def test_contract_yearly(self):
|
||||
recurring_next_date = fields.Date.to_date('2017-02-28')
|
||||
self.contract.recurring_next_date = '2016-02-29'
|
||||
self.contract.recurring_rule_type = 'yearly'
|
||||
recurring_next_date = to_date('2017-02-28')
|
||||
self.acct_line.recurring_next_date = '2016-02-29'
|
||||
self.acct_line.recurring_rule_type = 'yearly'
|
||||
self.contract.recurring_create_invoice()
|
||||
invoices_weekly = self.env['account.invoice'].search(
|
||||
[('contract_id', '=', self.contract.id)])
|
||||
[('contract_id', '=', self.contract.id)]
|
||||
)
|
||||
self.assertTrue(invoices_weekly)
|
||||
self.assertEqual(
|
||||
self.contract.recurring_next_date, recurring_next_date)
|
||||
self.acct_line.recurring_next_date, recurring_next_date
|
||||
)
|
||||
|
||||
def test_contract_monthly_lastday(self):
|
||||
recurring_next_date = fields.Date.to_date('2016-03-31')
|
||||
self.contract.recurring_next_date = '2016-02-29'
|
||||
self.contract.recurring_invoicing_type = 'post-paid'
|
||||
self.contract.recurring_rule_type = 'monthlylastday'
|
||||
recurring_next_date = to_date('2016-03-31')
|
||||
self.acct_line.recurring_next_date = '2016-02-29'
|
||||
self.acct_line.recurring_invoicing_type = 'post-paid'
|
||||
self.acct_line.recurring_rule_type = 'monthlylastday'
|
||||
self.contract.recurring_create_invoice()
|
||||
invoices_monthly_lastday = self.env['account.invoice'].search(
|
||||
[('contract_id', '=', self.contract.id)])
|
||||
[('contract_id', '=', self.contract.id)]
|
||||
)
|
||||
self.assertTrue(invoices_monthly_lastday)
|
||||
self.assertEqual(self.contract.recurring_next_date,
|
||||
recurring_next_date)
|
||||
self.assertEqual(
|
||||
self.acct_line.recurring_next_date, recurring_next_date
|
||||
)
|
||||
|
||||
def test_onchange_partner_id(self):
|
||||
self.contract._onchange_partner_id()
|
||||
self.assertEqual(self.contract.pricelist_id,
|
||||
self.contract.partner_id.property_product_pricelist)
|
||||
self.assertEqual(
|
||||
self.contract.pricelist_id,
|
||||
self.contract.partner_id.property_product_pricelist,
|
||||
)
|
||||
|
||||
def test_onchange_date_start(self):
|
||||
recurring_next_date = fields.Date.to_date('2016-01-01')
|
||||
self.contract.date_start = recurring_next_date
|
||||
self.contract._onchange_date_start()
|
||||
self.assertEqual(self.contract.recurring_next_date,
|
||||
recurring_next_date)
|
||||
recurring_next_date = to_date('2016-01-01')
|
||||
self.acct_line.date_start = recurring_next_date
|
||||
self.acct_line._onchange_date_start()
|
||||
self.assertEqual(
|
||||
self.acct_line.recurring_next_date, recurring_next_date
|
||||
)
|
||||
|
||||
def test_uom(self):
|
||||
uom_litre = self.env.ref('uom.product_uom_litre')
|
||||
self.acct_line.uom_id = uom_litre.id
|
||||
self.acct_line._onchange_product_id()
|
||||
self.assertEqual(self.acct_line.uom_id,
|
||||
self.acct_line.product_id.uom_id)
|
||||
self.assertEqual(
|
||||
self.acct_line.uom_id, self.acct_line.product_id.uom_id
|
||||
)
|
||||
|
||||
def test_onchange_product_id(self):
|
||||
line = self.env['account.analytic.invoice.line'].new()
|
||||
@@ -187,45 +242,55 @@ class TestContract(TestContractBase):
|
||||
self.assertAlmostEqual(self.acct_line.price_subtotal, 100.0)
|
||||
|
||||
def test_check_journal(self):
|
||||
contract_no_journal = self.contract.copy()
|
||||
contract_no_journal.journal_id = False
|
||||
journal = self.env['account.journal'].search([('type', '=', 'sale')])
|
||||
journal.write({'type': 'general'})
|
||||
with self.assertRaises(ValidationError):
|
||||
contract_no_journal.recurring_create_invoice()
|
||||
self.contract.recurring_create_invoice()
|
||||
|
||||
def test_check_date_end(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self.contract.date_end = '2015-12-31'
|
||||
self.acct_line.date_end = '2015-12-31'
|
||||
|
||||
def test_check_recurring_next_date_start_date(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self.contract.write({
|
||||
'date_start': '2017-01-01',
|
||||
'recurring_next_date': '2016-01-01',
|
||||
})
|
||||
self.acct_line.write(
|
||||
{
|
||||
'date_start': '2017-01-01',
|
||||
'recurring_next_date': '2016-01-01',
|
||||
}
|
||||
)
|
||||
|
||||
def test_check_recurring_next_date_recurring_invoices(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self.contract.write({
|
||||
'recurring_invoices': True,
|
||||
'recurring_next_date': False,
|
||||
})
|
||||
self.contract.write({'recurring_invoices': True})
|
||||
self.acct_line.write({'recurring_next_date': False})
|
||||
|
||||
def test_check_date_start_recurring_invoices(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self.contract.write({
|
||||
'recurring_invoices': True,
|
||||
'date_start': False,
|
||||
})
|
||||
self.contract.write({'recurring_invoices': True})
|
||||
self.acct_line.write({'date_start': False})
|
||||
|
||||
def test_onchange_contract_template_id(self):
|
||||
"""It should change the contract values to match the template."""
|
||||
self.contract.contract_template_id = self.template
|
||||
self.contract._onchange_contract_template_id()
|
||||
res = {
|
||||
'recurring_rule_type': self.contract.recurring_rule_type,
|
||||
'recurring_interval': self.contract.recurring_interval,
|
||||
'recurring_invoice_line_ids': [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
'product_id': self.product.id,
|
||||
'name': 'Services from #START# to #END#',
|
||||
'quantity': 1,
|
||||
'uom_id': self.product.uom_id.id,
|
||||
'price_unit': 100,
|
||||
'discount': 50,
|
||||
'recurring_rule_type': 'yearly',
|
||||
'recurring_interval': 1,
|
||||
},
|
||||
)
|
||||
]
|
||||
}
|
||||
del self.template_vals['name']
|
||||
self.assertDictEqual(res, self.template_vals)
|
||||
@@ -234,19 +299,17 @@ class TestContract(TestContractBase):
|
||||
"""It should create invoice lines for the contract lines."""
|
||||
|
||||
self.acct_line.unlink()
|
||||
self.line_vals['analytic_account_id'] = self.template.id
|
||||
self.env['account.analytic.contract.line'].create(self.line_vals)
|
||||
self.contract.contract_template_id = self.template
|
||||
|
||||
self.assertFalse(self.contract.recurring_invoice_line_ids,
|
||||
'Recurring lines were not removed.')
|
||||
|
||||
self.assertFalse(
|
||||
self.contract.recurring_invoice_line_ids,
|
||||
'Recurring lines were not removed.',
|
||||
)
|
||||
self.contract.contract_template_id = self.template
|
||||
self.contract._onchange_contract_template_id()
|
||||
del self.line_vals['analytic_account_id']
|
||||
|
||||
self.assertEqual(len(self.contract.recurring_invoice_line_ids), 1)
|
||||
|
||||
for key, value in self.line_vals.items():
|
||||
for key, value in self.line_template_vals.items():
|
||||
test_value = self.contract.recurring_invoice_line_ids[0][key]
|
||||
try:
|
||||
test_value = test_value.id
|
||||
@@ -262,7 +325,8 @@ class TestContract(TestContractBase):
|
||||
self.contract._onchange_contract_type()
|
||||
self.assertEqual(self.contract.journal_id.type, 'sale')
|
||||
self.assertEqual(
|
||||
self.contract.journal_id.company_id, self.contract.company_id)
|
||||
self.contract.journal_id.company_id, self.contract.company_id
|
||||
)
|
||||
|
||||
def test_contract_onchange_product_id_domain_blank(self):
|
||||
"""It should return a blank UoM domain when no product."""
|
||||
@@ -286,18 +350,19 @@ class TestContract(TestContractBase):
|
||||
)
|
||||
line.product_id.uom_id = self.env.ref('uom.product_uom_day').id
|
||||
line._onchange_product_id()
|
||||
self.assertEqual(line.uom_id,
|
||||
line.product_id.uom_id)
|
||||
self.assertEqual(line.uom_id, line.product_id.uom_id)
|
||||
|
||||
def test_contract_onchange_product_id_name(self):
|
||||
"""It should update the name for the line."""
|
||||
line = self._add_template_line()
|
||||
line.product_id.description_sale = 'Test'
|
||||
line._onchange_product_id()
|
||||
self.assertEqual(line.name,
|
||||
'\n'.join([line.product_id.name,
|
||||
line.product_id.description_sale,
|
||||
]))
|
||||
self.assertEqual(
|
||||
line.name,
|
||||
'\n'.join(
|
||||
[line.product_id.name, line.product_id.description_sale]
|
||||
),
|
||||
)
|
||||
|
||||
def test_contract_count(self):
|
||||
"""It should return sale contract count."""
|
||||
@@ -313,48 +378,48 @@ class TestContract(TestContractBase):
|
||||
def test_same_date_start_and_date_end(self):
|
||||
"""It should create one invoice with same start and end date."""
|
||||
account_invoice_model = self.env['account.invoice']
|
||||
self.contract.write({
|
||||
'date_start': fields.Date.today(),
|
||||
'date_end': fields.Date.today(),
|
||||
'recurring_next_date': fields.Date.today(),
|
||||
})
|
||||
self.acct_line.write(
|
||||
{
|
||||
'date_start': fields.Date.today(),
|
||||
'date_end': fields.Date.today(),
|
||||
'recurring_next_date': fields.Date.today(),
|
||||
}
|
||||
)
|
||||
self.contract._compute_recurring_next_date()
|
||||
init_count = account_invoice_model.search_count(
|
||||
[('contract_id', '=', self.contract.id)])
|
||||
self.contract.cron_recurring_create_invoice()
|
||||
[('contract_id', '=', self.contract.id)]
|
||||
)
|
||||
self.contract.recurring_create_invoice()
|
||||
last_count = account_invoice_model.search_count(
|
||||
[('contract_id', '=', self.contract.id)])
|
||||
[('contract_id', '=', self.contract.id)]
|
||||
)
|
||||
self.assertEqual(last_count, init_count + 1)
|
||||
self.contract.recurring_create_invoice()
|
||||
last_count = account_invoice_model.search_count(
|
||||
[('contract_id', '=', self.contract.id)]
|
||||
)
|
||||
self.assertEqual(last_count, init_count + 1)
|
||||
with self.assertRaises(ValidationError):
|
||||
self.contract.recurring_create_invoice()
|
||||
|
||||
def test_compute_create_invoice_visibility(self):
|
||||
self.contract.write({
|
||||
'recurring_next_date': '2017-01-01',
|
||||
'date_start': '2016-01-01',
|
||||
'date_end': False,
|
||||
})
|
||||
self.acct_line.write(
|
||||
{
|
||||
'recurring_next_date': '2017-01-01',
|
||||
'date_start': '2016-01-01',
|
||||
'date_end': False,
|
||||
}
|
||||
)
|
||||
self.assertTrue(self.contract.create_invoice_visibility)
|
||||
self.contract.date_end = '2017-01-01'
|
||||
self.acct_line.date_end = '2017-01-01'
|
||||
self.contract.refresh()
|
||||
self.assertTrue(self.contract.create_invoice_visibility)
|
||||
self.contract.date_end = '2016-01-01'
|
||||
self.acct_line.date_end = '2016-01-01'
|
||||
self.contract.refresh()
|
||||
self.assertFalse(self.contract.create_invoice_visibility)
|
||||
|
||||
def test_extend_invoice(self):
|
||||
account_invoice_model = self.env['account.invoice']
|
||||
self.contract.recurring_create_invoice()
|
||||
invoice = account_invoice_model.search(
|
||||
[('contract_id', '=', self.contract.id)])
|
||||
invoice.origin = 'Orig Invoice'
|
||||
self.contract._create_invoice(invoice)
|
||||
self.assertEqual(invoice.origin, 'Orig Invoice Test Contract')
|
||||
invoice_count = account_invoice_model.search_count(
|
||||
[('contract_id', '=', self.contract.id)])
|
||||
self.assertEqual(invoice_count, 1)
|
||||
self.assertEqual(len(invoice.invoice_line_ids), 2)
|
||||
|
||||
def test_act_show_contract(self):
|
||||
show_contract = self.partner.\
|
||||
with_context(contract_type='sale').act_show_contract()
|
||||
show_contract = self.partner.with_context(
|
||||
contract_type='sale'
|
||||
).act_show_contract()
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
'name': 'Customer Contracts',
|
||||
@@ -364,5 +429,104 @@ class TestContract(TestContractBase):
|
||||
'xml_id': 'contract.action_account_analytic_sale_overdue_all',
|
||||
},
|
||||
show_contract,
|
||||
'There was an error and the view couldn\'t be opened.'
|
||||
'There was an error and the view couldn\'t be opened.',
|
||||
)
|
||||
|
||||
def test_compute_first_recurring_next_date(self):
|
||||
"""Test different combination to compute recurring_next_date
|
||||
Combination format
|
||||
{
|
||||
'recurring_next_date': ( # date
|
||||
date_start, # date
|
||||
recurring_invoicing_type, # ('pre-paid','post-paid',)
|
||||
recurring_rule_type, # ('daily', 'weekly', 'monthly',
|
||||
# 'monthlylastday', 'yearly'),
|
||||
recurring_interval, # integer
|
||||
),
|
||||
}
|
||||
"""
|
||||
|
||||
def error_message(
|
||||
date_start,
|
||||
recurring_invoicing_type,
|
||||
recurring_rule_type,
|
||||
recurring_interval,
|
||||
):
|
||||
return "Error in %s every %d %s case, start with %s " % (
|
||||
recurring_invoicing_type,
|
||||
recurring_interval,
|
||||
recurring_rule_type,
|
||||
date_start,
|
||||
)
|
||||
|
||||
combinations = [
|
||||
(
|
||||
to_date('2018-01-01'),
|
||||
(to_date('2018-01-01'), 'pre-paid', 'monthly', 1),
|
||||
),
|
||||
(
|
||||
to_date('2018-01-01'),
|
||||
(to_date('2018-01-01'), 'pre-paid', 'monthly', 2),
|
||||
),
|
||||
(
|
||||
to_date('2018-02-01'),
|
||||
(to_date('2018-01-01'), 'post-paid', 'monthly', 1),
|
||||
),
|
||||
(
|
||||
to_date('2018-03-01'),
|
||||
(to_date('2018-01-01'), 'post-paid', 'monthly', 2),
|
||||
),
|
||||
(
|
||||
to_date('2018-01-31'),
|
||||
(to_date('2018-01-05'), 'post-paid', 'monthlylastday', 1),
|
||||
),
|
||||
(
|
||||
to_date('2018-01-31'),
|
||||
(to_date('2018-01-06'), 'pre-paid', 'monthlylastday', 1),
|
||||
),
|
||||
(
|
||||
to_date('2018-02-28'),
|
||||
(to_date('2018-01-05'), 'pre-paid', 'monthlylastday', 2),
|
||||
),
|
||||
(
|
||||
to_date('2018-01-05'),
|
||||
(to_date('2018-01-05'), 'pre-paid', 'yearly', 1),
|
||||
),
|
||||
(
|
||||
to_date('2019-01-05'),
|
||||
(to_date('2018-01-05'), 'post-paid', 'yearly', 1),
|
||||
),
|
||||
]
|
||||
contract_line_env = self.env['account.analytic.invoice.line']
|
||||
for recurring_next_date, combination in combinations:
|
||||
self.assertEqual(
|
||||
recurring_next_date,
|
||||
contract_line_env._compute_first_recurring_next_date(
|
||||
*combination
|
||||
),
|
||||
error_message(*combination),
|
||||
)
|
||||
|
||||
def test_recurring_next_date(self):
|
||||
"""recurring next date for a contract is the min for all lines"""
|
||||
self.contract.recurring_create_invoice()
|
||||
self.assertEqual(
|
||||
self.contract.recurring_next_date,
|
||||
min(
|
||||
self.contract.recurring_invoice_line_ids.mapped(
|
||||
'recurring_next_date'
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
def test_date_end(self):
|
||||
"""recurring next date for a contract is the min for all lines"""
|
||||
self.assertFalse(self.contract.date_end)
|
||||
self.acct_line.date_end = '2018-01-01'
|
||||
self.assertEqual(
|
||||
self.contract.date_end,
|
||||
max(self.contract.recurring_invoice_line_ids.mapped('date_end')),
|
||||
)
|
||||
self.acct_line.copy()
|
||||
self.acct_line.date_end = False
|
||||
self.assertFalse(self.contract.date_end)
|
||||
|
||||
39
contract/views/abstract_contract_line.xml
Normal file
39
contract/views/abstract_contract_line.xml
Normal file
@@ -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>
|
||||
@@ -48,35 +48,16 @@
|
||||
attrs="{'required': [('recurring_invoices', '=', True)]}"
|
||||
/>
|
||||
<field name="pricelist_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<label for="recurring_interval"/>
|
||||
<div>
|
||||
<field name="recurring_interval"
|
||||
class="oe_inline"
|
||||
attrs="{'required': [('recurring_invoices', '=', True)]}"
|
||||
/>
|
||||
<field name="recurring_rule_type"
|
||||
class="oe_inline"
|
||||
attrs="{'required': [('recurring_invoices', '=', True)]}"
|
||||
/>
|
||||
</div>
|
||||
<field name="recurring_invoicing_type"
|
||||
attrs="{'required': [('recurring_invoices', '=', True)]}"
|
||||
/>
|
||||
<field name="date_start"
|
||||
attrs="{'required': [('recurring_invoices', '=', True)]}"
|
||||
/>
|
||||
<field name="recurring_next_date"/>
|
||||
<field name="date_end"/>
|
||||
<field name="recurring_next_date"
|
||||
attrs="{'required': [('recurring_invoices', '=', True)]}"
|
||||
/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
<label for="recurring_invoice_line_ids"
|
||||
attrs="{'invisible': [('recurring_invoices','=',False)]}"
|
||||
/>
|
||||
<div attrs="{'invisible': [('recurring_invoices','=',False)]}">
|
||||
<field name="recurring_invoice_line_ids">
|
||||
<tree string="Account Analytic Lines" editable="bottom">
|
||||
<tree string="Account Analytic Lines">
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="product_id"/>
|
||||
<field name="name"/>
|
||||
@@ -87,6 +68,12 @@
|
||||
<field name="specific_price" invisible="1"/>
|
||||
<field name="discount" groups="base.group_no_one" />
|
||||
<field name="price_subtotal"/>
|
||||
<field name="recurring_interval" invisible="1"/>
|
||||
<field name="recurring_rule_type" invisible="1"/>
|
||||
<field name="recurring_invoicing_type" invisible="1"/>
|
||||
<field name="date_start" required="1"/>
|
||||
<field name="date_end"/>
|
||||
<field name="recurring_next_date" required="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</div>
|
||||
@@ -155,9 +142,6 @@
|
||||
<field name="partner_id" position="before">
|
||||
<field name="journal_id" groups="account.group_account_user"/>
|
||||
</field>
|
||||
<field name="partner_id" position="after">
|
||||
<field name="recurring_next_date" invisible="not context.get('is_contract', False)"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
28
contract/views/contract_line.xml
Normal file
28
contract/views/contract_line.xml
Normal file
@@ -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>
|
||||
@@ -16,24 +16,10 @@
|
||||
<field name="pricelist_id" />
|
||||
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
<group name="group_main_right">
|
||||
<field name="recurring_invoicing_type" />
|
||||
<label for="recurring_interval" />
|
||||
<div>
|
||||
<field name="recurring_interval"
|
||||
class="oe_inline"
|
||||
required="True"
|
||||
/>
|
||||
<field name="recurring_rule_type"
|
||||
class="oe_inline"
|
||||
required="True"
|
||||
/>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<group name="group_invoice_lines" string="Invoice Lines">
|
||||
<field name="recurring_invoice_line_ids" nolabel="1">
|
||||
<tree string="Account Analytic Lines" editable="bottom">
|
||||
<tree string="Account Analytic Lines">
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="product_id" />
|
||||
<field name="name" />
|
||||
@@ -44,6 +30,9 @@
|
||||
<field name="specific_price" invisible="1"/>
|
||||
<field name="discount" groups="base.group_no_one" />
|
||||
<field name="price_subtotal" />
|
||||
<field name="recurring_rule_type" invisible="1"/>
|
||||
<field name="recurring_interval" invisible="1"/>
|
||||
<field name="recurring_invoicing_type" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
@@ -64,9 +53,6 @@
|
||||
<tree string="Contract Templates">
|
||||
<field name="name" />
|
||||
<field name="contract_type" />
|
||||
<field name="recurring_rule_type" />
|
||||
<field name="recurring_interval" />
|
||||
<field name="recurring_invoicing_type" />
|
||||
<field name="pricelist_id" />
|
||||
</tree>
|
||||
</field>
|
||||
@@ -79,23 +65,12 @@
|
||||
<search string="Contract Templates">
|
||||
<field name="name" />
|
||||
<field name="contract_type" />
|
||||
<field name="recurring_rule_type" />
|
||||
<field name="recurring_interval" />
|
||||
<field name="recurring_invoicing_type" />
|
||||
<field name="pricelist_id" />
|
||||
<field name="journal_id" />
|
||||
<filter name="contract_type"
|
||||
string="Contract Type"
|
||||
context="{'group_by': 'contract_type'}"
|
||||
/>
|
||||
<filter name="recurring_rule_type"
|
||||
string="Recurrence"
|
||||
context="{'group_by': 'recurring_rule_type'}"
|
||||
/>
|
||||
<filter name="recurring_invoicing_type"
|
||||
string="Invoicing type"
|
||||
context="{'group_by': 'recurring_invoicing_type'}"
|
||||
/>
|
||||
<filter name="pricelist_id"
|
||||
string="Pricelist"
|
||||
context="{'group_by': 'pricelist_id'}"
|
||||
17
contract/views/contract_template_line.xml
Normal file
17
contract/views/contract_template_line.xml
Normal file
@@ -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>
|
||||
Reference in New Issue
Block a user