[IMP] contract: 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

[12.0][IMP] - Add strat/stop wizard to contract line

[12.0][IMP] - Add pause button to contract line

[IMP] - Add state filed in contract line form

[FIX] - stop don't change date_end for finished contract line

[IMP] - Change contract line buttons visibility

Add renewal process with termination notice

[FIX] - don't consider stop_date If it is after the contract line end_date

[IMP] - consider more cases in stop_plan_successor

[IMP] - cancel upcoming line on stop

[IMP] - Chnage next invoice date on un-cancel

[IMP] - Post message in contract on contract line actions

[IMP] - check contract line overlap

[FIX] - invoice last period for post-paid case

[IMP] - Add primary views for contract

[IMP] - don't use related filed for partner_id and pricelist_id

[FIX] - fix stop_plan_successor case 5

contract line start in the suspension period and end after it

[IMP] - improve cancel/uncancel process

[FIX] - Test if start_date is set before compute

[FIX] - date_end include in the period in auto_renew case

[FIX] - in suspension case, contract line should start a day after the end

[IMP] - confirm message on contract line cancel

[IMP] - hide recurring_invoicing_type if recurring_rule_type is monthlylastday

for the monthlylastday case, pre-paid is logicly impossible,
if monthlylastday is set, we consider only post-paid case

[IMP] - Improve unit tests

[IMP] - store last_date_invoiced on contract_line

Improve CRITERIA_ALLOWED_DICT

[IMP] - code improvement

[IMP] - Use last_date_invoiced to set marker in invoice description

[IMP] - add migration script to init last_day_invoiced and some other improvement

[FIX] - a contract line suspended should start a day after the suspension end

[IMP] - don't allow to unlink uncnaceled contrac line

[FIX] - check date_start before onchange

[FIX] - compute recurring_next_date for contract

[IMP] - get contract line default data onchange product_id

[IMP] - Add responsible to contract form view

[FIX] - contract recurring_next_date ignore canceled lines

[FIX] - fix _get_invoiced_period if recurring_next_date manually updated

[IMP] - archive contract_line on contract archive
This commit is contained in:
sbejaoui
2018-10-30 18:50:41 +01:00
committed by Pedro M. Baeza
parent 336fb4502e
commit 2e701748e3
30 changed files with 4072 additions and 1213 deletions

View File

@@ -1 +1,2 @@
from . import models
from . import wizards

View File

@@ -4,29 +4,37 @@
# 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.1',
'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'],
"external_dependencies": {"python": ["dateutil"]},
'data': [
'wizards/contract_line_wizard.xml',
'security/ir.model.access.csv',
'security/contract_security.xml',
'report/report_contract.xml',
'report/contract_views.xml',
'data/contract_cron.xml',
'data/contract_renew_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,

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding='UTF-8'?>
<odoo noupdate="1">
<record model="ir.cron" id="contract_line_cron_for_renew">
<field name="name">Renew Contract lines</field>
<field name="model_id" ref="model_account_analytic_invoice_line"/>
<field name="state">code</field>
<field name="code">model.cron_renew_contract_line()</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall" />
</record>
</odoo>

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Copyright 2018 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import SUPERUSER_ID, api
_logger = logging.getLogger(__name__)
def migrate(cr, version):
"""Copy recurrence info from contract to contract lines and compute
last_date_invoiced"""
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"""
)
_logger.info("order all contract line")
env = api.Environment(cr, SUPERUSER_ID, {})
contract_lines = env["account.analytic.invoice.line"].search(
[("recurring_next_date", "!=", False)]
)
contract_lines._init_last_date_invoiced()

View File

@@ -0,0 +1,24 @@
# Copyright 2018 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import SUPERUSER_ID, api
_logger = logging.getLogger(__name__)
def migrate(cr, version):
"""
set recurring_next_date to false for finished contract
"""
_logger.info("order all contract line")
with api.Environment(cr, SUPERUSER_ID, {}) as env:
contracts = env["account.analytic.account"].search([])
finished_contract = contracts.filtered(
lambda c: not c.create_invoice_visibility
)
cr.execute(
"UPDATE account_analytic_account set recurring_next_date=null where id in (%)"
% ','.join(finished_contract.ids)
)

View File

@@ -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

View File

@@ -0,0 +1,71 @@
# 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 Recurring 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['res.company']._company_default_get(
self._name
),
)
@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)

View File

@@ -0,0 +1,227 @@
# 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 = 'Abstract Recurring 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,
)
date_start = fields.Date(string='Date Start')
recurring_next_date = fields.Date(string='Date of Next Invoice')
is_canceled = fields.Boolean(string="Canceled", default=False)
is_auto_renew = fields.Boolean(string="Auto Renew", default=False)
auto_renew_interval = fields.Integer(
default=1,
string='Renew Every',
help="Renew every (Days/Week/Month/Year)",
)
auto_renew_rule_type = fields.Selection(
[('monthly', 'Month(s)'), ('yearly', 'Year(s)')],
default='yearly',
string='Renewal type',
help="Specify Interval for automatic renewal.",
)
termination_notice_interval = fields.Integer(
default=1, string='Termination Notice Before'
)
termination_notice_rule_type = fields.Selection(
[('daily', 'Day(s)'), ('weekly', 'Week(s)'), ('monthly', 'Month(s)')],
default='monthly',
string='Termination Notice type',
)
contract_id = fields.Many2one(
string='Contract',
comodel_name='account.abstract.analytic.contract',
required=True,
ondelete='cascade',
oldname='analytic_account_id',
)
@api.depends(
'automatic_price',
'specific_price',
'product_id',
'quantity',
'contract_id.pricelist_id',
'contract_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.contract_id.pricelist_id.id,
partner=line.contract_id.partner_id.id,
date=line.env.context.get(
'old_date', fields.Date.context_today(line)
),
)
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.contract_id.pricelist_id:
cur = line.contract_id.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.context_today(self)
partner = self.contract_id.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.contract_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}
@api.onchange('product_id')
def _onchange_product_id_recurring_info(self):
for rec in self:
rec.date_start = fields.Date.today()
if rec.product_id.is_contract:
rec.recurring_rule_type = rec.product_id.recurring_rule_type
rec.recurring_invoicing_type = (
rec.product_id.recurring_invoicing_type
)
rec.recurring_interval = rec.product_id.recurring_interval
rec.is_auto_renew = rec.product_id.is_auto_renew
rec.auto_renew_interval = rec.product_id.auto_renew_interval
rec.auto_renew_rule_type = rec.product_id.auto_renew_rule_type
rec.termination_notice_interval = (
rec.product_id.termination_notice_interval
)
rec.termination_notice_rule_type = (
rec.product_id.termination_notice_rule_type
)

View File

@@ -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]

View File

@@ -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)

View File

@@ -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}

View File

@@ -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',
)

View File

@@ -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'
)

228
contract/models/contract.py Normal file
View File

@@ -0,0 +1,228 @@
# 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',
'recurring_invoice_line_ids.is_canceled',
)
def _compute_recurring_next_date(self):
for contract in self:
recurring_next_date = contract.recurring_invoice_line_ids.filtered(
lambda l: l.recurring_next_date and not l.is_canceled
).mapped('recurring_next_date')
if recurring_next_date:
contract.recurring_next_date = min(recurring_next_date)
@api.depends('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.context_today(contract_line)
vals['recurring_next_date'] = fields.Date.context_today(
contract_line
)
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()

View File

@@ -0,0 +1,963 @@
# Copyright 2017 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import timedelta
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from .contract_line_constraints import get_allowed
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='Contract',
required=True,
index=True,
ondelete='cascade',
oldname='analytic_account_id',
)
date_start = fields.Date(
string='Date Start',
default=lambda self: fields.Date.context_today(self),
)
date_end = fields.Date(string='Date End', index=True)
recurring_next_date = fields.Date(string='Date of Next Invoice')
last_date_invoiced = fields.Date(
string='Last Date Invoiced', readonly=True, copy=False
)
create_invoice_visibility = fields.Boolean(
compute='_compute_create_invoice_visibility'
)
successor_contract_line_id = fields.Many2one(
comodel_name='account.analytic.invoice.line',
string="Successor Contract Line",
required=False,
readonly=True,
copy=False,
help="In case of restart after suspension, this field contain the new "
"contract line created.",
)
predecessor_contract_line_id = fields.Many2one(
comodel_name='account.analytic.invoice.line',
string="Predecessor Contract Line",
required=False,
readonly=True,
copy=False,
help="Contract Line origin of this one.",
)
is_plan_successor_allowed = fields.Boolean(
string="Plan successor allowed?", compute='_compute_allowed'
)
is_stop_plan_successor_allowed = fields.Boolean(
string="Stop/Plan successor allowed?", compute='_compute_allowed'
)
is_stop_allowed = fields.Boolean(
string="Stop allowed?", compute='_compute_allowed'
)
is_cancel_allowed = fields.Boolean(
string="Cancel allowed?", compute='_compute_allowed'
)
is_un_cancel_allowed = fields.Boolean(
string="Un-Cancel allowed?", compute='_compute_allowed'
)
state = fields.Selection(
string="State",
selection=[
('upcoming', 'Upcoming'),
('in-progress', 'In-progress'),
('upcoming-close', 'Upcoming Close'),
('closed', 'Closed'),
('canceled', 'Canceled'),
],
compute="_compute_state",
)
active = fields.Boolean(
string="Active", related="contract_id.active", strore=True
)
@api.multi
def _compute_state(self):
today = fields.Date.context_today(self)
for rec in self:
if rec.date_start:
if rec.is_canceled:
rec.state = 'canceled'
elif today < rec.date_start:
rec.state = 'upcoming'
elif not rec.date_end or (
today <= rec.date_end and rec.is_auto_renew
):
rec.state = 'in-progress'
elif today <= rec.date_end and not rec.is_auto_renew:
rec.state = 'upcoming-close'
else:
rec.state = 'closed'
@api.depends(
'date_start',
'date_end',
'last_date_invoiced',
'is_auto_renew',
'successor_contract_line_id',
'predecessor_contract_line_id',
'is_canceled',
)
def _compute_allowed(self):
for rec in self:
if rec.date_start:
allowed = get_allowed(
rec.date_start,
rec.date_end,
rec.last_date_invoiced,
rec.is_auto_renew,
rec.successor_contract_line_id,
rec.predecessor_contract_line_id,
rec.is_canceled,
)
if allowed:
rec.is_plan_successor_allowed = allowed.plan_successor
rec.is_stop_plan_successor_allowed = (
allowed.stop_plan_successor
)
rec.is_stop_allowed = allowed.stop
rec.is_cancel_allowed = allowed.cancel
rec.is_un_cancel_allowed = allowed.uncancel
@api.constrains('is_auto_renew', 'successor_contract_line_id', 'date_end')
def _check_allowed(self):
"""
logical impossible combination:
* a line with is_auto_renew True should have date_end and
couldn't have successor_contract_line_id
* a line without date_end can't have successor_contract_line_id
"""
for rec in self:
if rec.is_auto_renew:
if rec.successor_contract_line_id:
raise ValidationError(
_(
"A contract line with a successor "
"can't be set to auto-renew"
)
)
if not rec.date_end:
raise ValidationError(
_("An auto-renew line must have a end date")
)
else:
if not rec.date_end and rec.successor_contract_line_id:
raise ValidationError(
_(
"A contract line with a successor "
"must have a end date"
)
)
@api.constrains('successor_contract_line_id', 'date_end')
def _check_overlap_successor(self):
for rec in self:
if rec.date_end and rec.successor_contract_line_id:
if rec.date_end >= rec.successor_contract_line_id.date_start:
raise ValidationError(
_("Contract line and its successor overlapped")
)
@api.constrains('predecessor_contract_line_id', 'date_start')
def _check_overlap_predecessor(self):
for rec in self:
if rec.predecessor_contract_line_id:
if rec.date_start <= rec.predecessor_contract_line_id.date_end:
raise ValidationError(
_("Contract line and its predecessor overlapped")
)
@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',
'is_auto_renew',
'auto_renew_rule_type',
'auto_renew_interval',
)
def _onchange_is_auto_renew(self):
"""Date end should be auto-computed if a contract line is set to
auto_renew"""
for rec in self.filtered('is_auto_renew'):
if rec.date_start:
rec.date_end = (
self.date_start
+ self.get_relative_delta(
rec.auto_renew_rule_type, rec.auto_renew_interval
)
- relativedelta(days=1)
)
@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 date of next invoice anterior "
"to the start of the contract line '%s'"
)
% line.name
)
@api.constrains('date_start', 'date_end', 'last_date_invoiced')
def _check_last_date_invoiced(self):
for rec in self.filtered('last_date_invoiced'):
if rec.date_start and rec.date_start > rec.last_date_invoiced:
raise ValidationError(
_(
"You can't have the start date after the date of last "
"invoice for the contract line '%s'"
)
% rec.name
)
if rec.date_end and rec.date_end < rec.last_date_invoiced:
raise ValidationError(
_(
"You can't have the end date before the date of last "
"invoice for the contract line '%s'"
)
% rec.name
)
@api.constrains('recurring_next_date')
def _check_recurring_next_date_recurring_invoices(self):
for rec in self.filtered('contract_id.recurring_invoices'):
if not rec.recurring_next_date and (
not rec.last_date_invoiced
or rec.last_date_invoiced < rec.date_end
):
raise ValidationError(
_(
"You must supply a date of next invoice for contract "
"line '%s'"
)
% rec.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 line '%s'")
% line.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 line '%s' start date can't be later than"
" end date"
)
% line.name
)
@api.depends('recurring_next_date', 'date_start', 'date_end')
def _compute_create_invoice_visibility(self):
today = fields.Date.context_today(self)
for rec in self:
if rec.date_start:
if today < rec.date_start:
rec.create_invoice_visibility = False
else:
rec.create_invoice_visibility = bool(
rec.recurring_next_date
)
@api.model
def _get_recurring_create_invoice_domain(self, contract=False):
domain = []
date_ref = fields.Date.context_today(self)
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),
('is_canceled', '=', False),
]
)
return domain
@api.model
def recurring_create_invoice(self, contract=False):
domain = self._get_recurring_create_invoice_domain(contract)
contract_to_invoice = self.read_group(
domain, ['id', 'contract_id'], ['contract_id']
)
return self._recurring_create_invoice(contract_to_invoice)
@api.model
def _recurring_create_invoice(self, contract_to_invoice):
"""Create invoices from contracts
:return: invoices created
"""
invoices = self.env['account.invoice']
for contract in contract_to_invoice:
lines = self.search(contract['__domain'])
if lines:
invoices |= lines._create_invoice()
lines._update_recurring_next_date()
return invoices
@api.multi
def _create_invoice(self):
"""
:return: invoice created
"""
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
first_date_invoiced, last_date_invoiced = self._get_invoiced_period()
name = self._insert_markers(first_date_invoiced, last_date_invoiced)
invoice_line_vals.update(
{
'name': name,
'account_analytic_id': contract.id,
'price_unit': self.price_unit,
}
)
return invoice_line_vals
@api.multi
def _get_invoiced_period(self):
self.ensure_one()
first_date_invoiced = (
self.last_date_invoiced + relativedelta(days=1)
if self.last_date_invoiced
else self.date_start
)
if self.recurring_rule_type == 'monthlylastday':
last_date_invoiced = self.recurring_next_date
else:
if self.recurring_invoicing_type == 'pre-paid':
last_date_invoiced = (
self.recurring_next_date
+ self.get_relative_delta(
self.recurring_rule_type, self.recurring_interval
)
- relativedelta(days=1)
)
else:
last_date_invoiced = self.recurring_next_date - relativedelta(
days=1
)
if self.date_end and self.date_end < last_date_invoiced:
last_date_invoiced = self.date_end
return first_date_invoiced, last_date_invoiced
@api.multi
def _insert_markers(self, first_date_invoiced, last_date_invoiced):
self.ensure_one()
lang_obj = self.env['res.lang']
lang = lang_obj.search(
[('code', '=', self.contract_id.partner_id.lang)]
)
date_format = lang.date_format or '%m/%d/%Y'
name = self.name
name = name.replace(
'#START#', first_date_invoiced.strftime(date_format)
)
name = name.replace('#END#', last_date_invoiced.strftime(date_format))
return name
@api.multi
def _update_recurring_next_date(self):
for rec in self:
old_date = rec.recurring_next_date
new_date = old_date + self.get_relative_delta(
rec.recurring_rule_type, rec.recurring_interval
)
if rec.recurring_rule_type == 'monthlylastday':
rec.last_date_invoiced = (
old_date
if rec.date_end and old_date < rec.date_end
else rec.date_end
)
elif rec.recurring_invoicing_type == 'post-paid':
rec.last_date_invoiced = (
old_date - relativedelta(days=1)
if rec.date_end and old_date < rec.date_end
else rec.date_end
)
elif rec.recurring_invoicing_type == 'pre-paid':
rec.last_date_invoiced = (
new_date - relativedelta(days=1)
if rec.date_end and new_date < rec.date_end
else rec.date_end
)
if (
rec.last_date_invoiced
and rec.last_date_invoiced == rec.date_end
):
rec.recurring_next_date = False
else:
rec.recurring_next_date = new_date
@api.multi
def _init_last_date_invoiced(self):
"""Used to init last_date_invoiced for migration purpose"""
for rec in self:
if rec.recurring_rule_type == 'monthlylastday':
last_date_invoiced = (
rec.recurring_next_date
- self.get_relative_delta(
rec.recurring_rule_type, rec.recurring_interval
)
)
elif rec.recurring_invoicing_type == 'post-paid':
last_date_invoiced = (
rec.recurring_next_date
- self.get_relative_delta(
rec.recurring_rule_type, rec.recurring_interval
)
) - relativedelta(days=1)
if last_date_invoiced > rec.date_start:
rec.last_date_invoiced = last_date_invoiced
@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.multi
def _delay(self, delay_delta):
"""
Delay a contract line
:param delay_delta: delay relative delta
:return: delayed contract line
"""
for rec in self:
if rec.last_date_invoiced:
raise ValidationError(
_(
"You can't delay a contract line "
"invoiced at least one time."
)
)
new_date_start = rec.date_start + delay_delta
rec.recurring_next_date = self._compute_first_recurring_next_date(
new_date_start,
rec.recurring_invoicing_type,
rec.recurring_rule_type,
rec.recurring_interval,
)
if rec.date_end:
rec.date_end += delay_delta
rec.date_start = new_date_start
@api.multi
def stop(self, date_end, post_message=True):
"""
Put date_end on contract line
We don't consider contract lines that end's before the new end date
:param date_end: new date end for contract line
:return: True
"""
if not all(self.mapped('is_stop_allowed')):
raise ValidationError(_('Stop not allowed for this line'))
for rec in self:
if date_end < rec.date_start:
rec.cancel()
else:
if not rec.date_end or rec.date_end > date_end:
if post_message:
old_date_end = rec.date_end
msg = _(
"""Contract line for <strong>{product}</strong>
stopped: <br/>
- <strong>End</strong>: {old_end} -- {new_end}
""".format(
product=rec.name,
old_end=old_date_end,
new_end=rec.date_end,
)
)
rec.contract_id.message_post(body=msg)
rec.write({'date_end': date_end, 'is_auto_renew': False})
else:
rec.write({'is_auto_renew': False})
return True
@api.multi
def _prepare_value_for_plan_successor(
self, date_start, date_end, is_auto_renew, recurring_next_date=False
):
self.ensure_one()
if not recurring_next_date:
recurring_next_date = self._compute_first_recurring_next_date(
date_start,
self.recurring_invoicing_type,
self.recurring_rule_type,
self.recurring_interval,
)
new_vals = self.read()[0]
new_vals.pop("id", None)
values = self._convert_to_write(new_vals)
values['date_start'] = date_start
values['date_end'] = date_end
values['recurring_next_date'] = recurring_next_date
values['is_auto_renew'] = is_auto_renew
values['predecessor_contract_line_id'] = self.id
return values
@api.multi
def plan_successor(
self,
date_start,
date_end,
is_auto_renew,
recurring_next_date=False,
post_message=True,
):
"""
Create a copy of a contract line in a new interval
:param date_start: date_start for the successor_contract_line
:param date_end: date_end for the successor_contract_line
:param is_auto_renew: is_auto_renew option for successor_contract_line
:param recurring_next_date: recurring_next_date for the
successor_contract_line
:return: successor_contract_line
"""
contract_line = self.env['account.analytic.invoice.line']
for rec in self:
if not rec.is_plan_successor_allowed:
raise ValidationError(
_('Plan successor not allowed for this line')
)
rec.is_auto_renew = False
new_line = self.create(
rec._prepare_value_for_plan_successor(
date_start, date_end, is_auto_renew, recurring_next_date
)
)
rec.successor_contract_line_id = new_line
contract_line |= new_line
if post_message:
msg = _(
"""Contract line for <strong>{product}</strong>
planned a successor: <br/>
- <strong>Start</strong>: {new_date_start}
<br/>
- <strong>End</strong>: {new_date_end}
""".format(
product=rec.name,
new_date_start=new_line.date_start,
new_date_end=new_line.date_end,
)
)
rec.contract_id.message_post(body=msg)
return contract_line
@api.multi
def stop_plan_successor(self, date_start, date_end, is_auto_renew):
"""
Stop a contract line for a defined period and start it later
Cases to consider:
* contract line end's before the suspension period:
-> apply stop
* contract line start before the suspension period and end in it
-> apply stop at suspension start date
-> apply plan successor:
- date_start: suspension.date_end
- date_end: date_end + (contract_line.date_end
- suspension.date_start)
* contract line start before the suspension period and end after it
-> apply stop at suspension start date
-> apply plan successor:
- date_start: suspension.date_end
- date_end: date_end + (suspension.date_end
- suspension.date_start)
* contract line start and end's in the suspension period
-> apply delay
- delay: suspension.date_end - contract_line.date_start
* contract line start in the suspension period and end after it
-> apply delay
- delay: suspension.date_end - contract_line.date_start
* contract line start and end after the suspension period
-> apply delay
- delay: suspension.date_end - suspension.start_date
:param date_start: suspension start date
:param date_end: suspension end date
:param is_auto_renew: is the new line is set to auto_renew
:return: created contract line
"""
if not all(self.mapped('is_stop_plan_successor_allowed')):
raise ValidationError(
_('Stop/Plan successor not allowed for this line')
)
contract_line = self.env['account.analytic.invoice.line']
for rec in self:
if rec.date_start >= date_start:
if rec.date_start < date_end:
delay = (date_end - rec.date_start) + timedelta(days=1)
else:
delay = (date_end - date_start) + timedelta(days=1)
rec._delay(delay)
contract_line |= rec
else:
if rec.date_end and rec.date_end < date_start:
rec.stop(date_start, post_message=False)
elif (
rec.date_end
and rec.date_end > date_start
and rec.date_end < date_end
):
new_date_start = date_end + relativedelta(days=1)
new_date_end = (
date_end
+ (rec.date_end - date_start)
+ relativedelta(days=1)
)
rec.stop(
date_start - relativedelta(days=1), post_message=False
)
contract_line |= rec.plan_successor(
new_date_start,
new_date_end,
is_auto_renew,
post_message=False,
)
else:
new_date_start = date_end + relativedelta(days=1)
if rec.date_end:
new_date_end = (
rec.date_end
+ (date_end - date_start)
+ relativedelta(days=1)
)
else:
new_date_end = rec.date_end
rec.stop(
date_start - relativedelta(days=1), post_message=False
)
contract_line |= rec.plan_successor(
new_date_start,
new_date_end,
is_auto_renew,
post_message=False,
)
msg = _(
"""Contract line for <strong>{product}</strong>
suspended: <br/>
- <strong>Suspension Start</strong>: {new_date_start}
<br/>
- <strong>Suspension End</strong>: {new_date_end}
""".format(
product=rec.name,
new_date_start=date_start,
new_date_end=date_end,
)
)
rec.contract_id.message_post(body=msg)
return contract_line
@api.multi
def cancel(self):
if not all(self.mapped('is_cancel_allowed')):
raise ValidationError(_('Cancel not allowed for this line'))
for contract in self.mapped('contract_id'):
lines = self.filtered(lambda l, c=contract: l.contract_id == c)
msg = _(
"""Contract line canceled: %s"""
% "<br/>- ".join(
[
"<strong>%s</strong>" % name
for name in lines.mapped('name')
]
)
)
contract.message_post(body=msg)
self.mapped('predecessor_contract_line_id').write(
{'successor_contract_line_id': False}
)
return self.write({'is_canceled': True})
@api.multi
def uncancel(self, recurring_next_date):
if not all(self.mapped('is_un_cancel_allowed')):
raise ValidationError(_('Un-cancel not allowed for this line'))
for contract in self.mapped('contract_id'):
lines = self.filtered(lambda l, c=contract: l.contract_id == c)
msg = _(
"""Contract line Un-canceled: %s"""
% "<br/>- ".join(
[
"<strong>%s</strong>" % name
for name in lines.mapped('name')
]
)
)
contract.message_post(body=msg)
for rec in self:
if rec.predecessor_contract_line_id:
rec.predecessor_contract_line_id.successor_contract_line_id = (
rec
)
rec.is_canceled = False
rec.recurring_next_date = recurring_next_date
return True
@api.multi
def action_uncancel(self):
self.ensure_one()
context = {
'default_contract_line_id': self.id,
'default_recurring_next_date': fields.Date.context_today(self),
}
context.update(self.env.context)
view_id = self.env.ref(
'contract.contract_line_wizard_uncancel_form_view'
).id
return {
'type': 'ir.actions.act_window',
'name': 'Un-Cancel Contract Line',
'res_model': 'account.analytic.invoice.line.wizard',
'view_type': 'form',
'view_mode': 'form',
'views': [(view_id, 'form')],
'target': 'new',
'context': context,
}
@api.multi
def action_plan_successor(self):
self.ensure_one()
context = {
'default_contract_line_id': self.id,
'default_is_auto_renew': self.is_auto_renew,
}
context.update(self.env.context)
view_id = self.env.ref(
'contract.contract_line_wizard_plan_successor_form_view'
).id
return {
'type': 'ir.actions.act_window',
'name': 'Plan contract line successor',
'res_model': 'account.analytic.invoice.line.wizard',
'view_type': 'form',
'view_mode': 'form',
'views': [(view_id, 'form')],
'target': 'new',
'context': context,
}
@api.multi
def action_stop(self):
self.ensure_one()
context = {
'default_contract_line_id': self.id,
'default_date_end': self.date_end,
}
context.update(self.env.context)
view_id = self.env.ref(
'contract.contract_line_wizard_stop_form_view'
).id
return {
'type': 'ir.actions.act_window',
'name': 'Resiliate contract line',
'res_model': 'account.analytic.invoice.line.wizard',
'view_type': 'form',
'view_mode': 'form',
'views': [(view_id, 'form')],
'target': 'new',
'context': context,
}
@api.multi
def action_stop_plan_successor(self):
self.ensure_one()
context = {
'default_contract_line_id': self.id,
'default_is_auto_renew': self.is_auto_renew,
}
context.update(self.env.context)
view_id = self.env.ref(
'contract.contract_line_wizard_stop_plan_successor_form_view'
).id
return {
'type': 'ir.actions.act_window',
'name': 'Suspend contract line',
'res_model': 'account.analytic.invoice.line.wizard',
'view_type': 'form',
'view_mode': 'form',
'views': [(view_id, 'form')],
'target': 'new',
'context': context,
}
@api.multi
def _get_renewal_dates(self):
self.ensure_one()
date_start = self.date_end + relativedelta(days=1)
date_end = (
date_start
+ self.get_relative_delta(
self.auto_renew_rule_type, self.auto_renew_interval
)
- relativedelta(days=1)
)
return date_start, date_end
@api.multi
def renew(self):
res = self.env['account.analytic.invoice.line']
for rec in self:
is_auto_renew = rec.is_auto_renew
rec.stop(rec.date_end, post_message=False)
date_start, date_end = rec._get_renewal_dates()
new_line = rec.plan_successor(
date_start, date_end, is_auto_renew, post_message=False
)
new_line._onchange_date_start()
res |= new_line
msg = _(
"""Contract line for <strong>{product}</strong>
renewed: <br/>
- <strong>Start</strong>: {new_date_start}
<br/>
- <strong>End</strong>: {new_date_end}
""".format(
product=rec.name,
new_date_start=date_start,
new_date_end=date_end,
)
)
rec.contract_id.message_post(body=msg)
return res
@api.model
def _contract_line_to_renew_domain(self):
date_ref = fields.Date.context_today(self) + self.get_relative_delta(
self.termination_notice_rule_type, self.termination_notice_interval
)
return [
('is_auto_renew', '=', True),
('date_end', '<=', date_ref),
('is_canceled', '=', False),
]
@api.model
def cron_renew_contract_line(self):
domain = self._contract_line_to_renew_domain()
to_renew = self.search(domain)
to_renew.renew()
@api.model
def fields_view_get(
self, view_id=None, view_type='form', toolbar=False, submenu=False
):
default_contract_type = self.env.context.get('default_contract_type')
if view_type == 'tree' and default_contract_type == 'purchase':
view_id = self.env.ref(
'contract.account_analytic_invoice_line_purchase_view_tree'
).id
if view_type == 'form':
if default_contract_type == 'purchase':
view_id = self.env.ref(
'contract.account_analytic_invoice_line_purchase_view_form'
).id
elif default_contract_type == 'sale':
view_id = self.env.ref(
'contract.account_analytic_invoice_line_sale_view_form'
).id
return super(AccountAnalyticInvoiceLine, self).fields_view_get(
view_id, view_type, toolbar, submenu
)
@api.multi
def unlink(self):
"""stop unlink uncnacled lines"""
if not all(self.mapped('is_canceled')):
raise ValidationError(
_("Contract line must be canceled before delete")
)
return super(AccountAnalyticInvoiceLine, self).unlink()

View File

@@ -0,0 +1,428 @@
# Copyright 2018 ACSONE SA/NV.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import itertools
from collections import namedtuple
from odoo.fields import Date
Criteria = namedtuple(
'Criteria',
[
'when', # Contract line relatively to today (BEFORE, IN, AFTER)
'has_date_end', # Is date_end set on contract line (bool)
'has_last_date_invoiced', # Is last_date_invoiced set on contract line
'is_auto_renew', # Is is_auto_renew set on contract line (bool)
'has_successor', # Is contract line has_successor (bool)
'predecessor_has_successor',
# Is contract line predecessor has successor (bool)
# In almost of the cases
# contract_line.predecessor.successor == contract_line
# But at cancel action,
# contract_line.predecessor.successor == False
# This is to permit plan_successor on predecessor
# If contract_line.predecessor.successor != False
# and contract_line is canceled, we don't allow uncancel
# else we re-link contract_line and its predecessor
'canceled', # Is contract line canceled (bool)
],
)
Allowed = namedtuple(
'Allowed',
['plan_successor', 'stop_plan_successor', 'stop', 'cancel', 'uncancel'],
)
def _expand_none(criteria):
variations = []
for attribute, value in criteria._asdict().items():
if value is None:
if attribute == 'when':
variations.append(['BEFORE', 'IN', 'AFTER'])
else:
variations.append([True, False])
else:
variations.append([value])
return itertools.product(*variations)
def _add(matrix, criteria, allowed):
""" Expand None values to True/False combination """
for c in _expand_none(criteria):
matrix[c] = allowed
CRITERIA_ALLOWED_DICT = {
Criteria(
when='BEFORE',
has_date_end=True,
has_last_date_invoiced=False,
is_auto_renew=True,
has_successor=False,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=False,
stop_plan_successor=True,
stop=True,
cancel=True,
uncancel=False,
),
Criteria(
when='BEFORE',
has_date_end=True,
has_last_date_invoiced=False,
is_auto_renew=False,
has_successor=True,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=False,
stop_plan_successor=False,
stop=True,
cancel=True,
uncancel=False,
),
Criteria(
when='BEFORE',
has_date_end=True,
has_last_date_invoiced=False,
is_auto_renew=False,
has_successor=False,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=True,
stop_plan_successor=True,
stop=True,
cancel=True,
uncancel=False,
),
Criteria(
when='BEFORE',
has_date_end=False,
has_last_date_invoiced=False,
is_auto_renew=False,
has_successor=False,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=False,
stop_plan_successor=True,
stop=True,
cancel=True,
uncancel=False,
),
Criteria(
when='IN',
has_date_end=True,
has_last_date_invoiced=False,
is_auto_renew=True,
has_successor=False,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=False,
stop_plan_successor=True,
stop=True,
cancel=True,
uncancel=False,
),
Criteria(
when='IN',
has_date_end=True,
has_last_date_invoiced=False,
is_auto_renew=False,
has_successor=True,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=False,
stop_plan_successor=False,
stop=True,
cancel=True,
uncancel=False,
),
Criteria(
when='IN',
has_date_end=True,
has_last_date_invoiced=False,
is_auto_renew=False,
has_successor=False,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=True,
stop_plan_successor=True,
stop=True,
cancel=True,
uncancel=False,
),
Criteria(
when='IN',
has_date_end=False,
has_last_date_invoiced=False,
is_auto_renew=False,
has_successor=False,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=False,
stop_plan_successor=True,
stop=True,
cancel=True,
uncancel=False,
),
Criteria(
when='BEFORE',
has_date_end=True,
has_last_date_invoiced=True,
is_auto_renew=True,
has_successor=False,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=False,
stop_plan_successor=True,
stop=True,
cancel=False,
uncancel=False,
),
Criteria(
when='BEFORE',
has_date_end=True,
has_last_date_invoiced=True,
is_auto_renew=False,
has_successor=True,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=False,
stop_plan_successor=False,
stop=True,
cancel=False,
uncancel=False,
),
Criteria(
when='BEFORE',
has_date_end=True,
has_last_date_invoiced=True,
is_auto_renew=False,
has_successor=False,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=True,
stop_plan_successor=True,
stop=True,
cancel=False,
uncancel=False,
),
Criteria(
when='BEFORE',
has_date_end=False,
has_last_date_invoiced=True,
is_auto_renew=False,
has_successor=False,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=False,
stop_plan_successor=True,
stop=True,
cancel=False,
uncancel=False,
),
Criteria(
when='IN',
has_date_end=True,
has_last_date_invoiced=True,
is_auto_renew=True,
has_successor=False,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=False,
stop_plan_successor=True,
stop=True,
cancel=False,
uncancel=False,
),
Criteria(
when='IN',
has_date_end=True,
has_last_date_invoiced=True,
is_auto_renew=False,
has_successor=True,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=False,
stop_plan_successor=False,
stop=True,
cancel=False,
uncancel=False,
),
Criteria(
when='IN',
has_date_end=True,
has_last_date_invoiced=True,
is_auto_renew=False,
has_successor=False,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=True,
stop_plan_successor=True,
stop=True,
cancel=False,
uncancel=False,
),
Criteria(
when='IN',
has_date_end=False,
has_last_date_invoiced=True,
is_auto_renew=False,
has_successor=False,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=False,
stop_plan_successor=True,
stop=True,
cancel=False,
uncancel=False,
),
Criteria(
when='AFTER',
has_date_end=True,
has_last_date_invoiced=None,
is_auto_renew=True,
has_successor=False,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=False,
stop_plan_successor=False,
stop=False,
cancel=False,
uncancel=False,
),
Criteria(
when='AFTER',
has_date_end=True,
has_last_date_invoiced=None,
is_auto_renew=False,
has_successor=True,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=False,
stop_plan_successor=False,
stop=False,
cancel=False,
uncancel=False,
),
Criteria(
when='AFTER',
has_date_end=True,
has_last_date_invoiced=None,
is_auto_renew=False,
has_successor=False,
predecessor_has_successor=None,
canceled=False,
): Allowed(
plan_successor=True,
stop_plan_successor=False,
stop=False,
cancel=False,
uncancel=False,
),
Criteria(
when=None,
has_date_end=None,
has_last_date_invoiced=None,
is_auto_renew=None,
has_successor=None,
predecessor_has_successor=False,
canceled=True,
): Allowed(
plan_successor=False,
stop_plan_successor=False,
stop=False,
cancel=False,
uncancel=True,
),
Criteria(
when=None,
has_date_end=None,
has_last_date_invoiced=None,
is_auto_renew=None,
has_successor=None,
predecessor_has_successor=True,
canceled=True,
): Allowed(
plan_successor=False,
stop_plan_successor=False,
stop=False,
cancel=False,
uncancel=False,
),
}
criteria_allowed_dict = {}
for c in CRITERIA_ALLOWED_DICT:
_add(criteria_allowed_dict, c, CRITERIA_ALLOWED_DICT[c])
def compute_when(date_start, date_end):
today = Date.today()
if today < date_start:
return 'BEFORE'
if date_end and today > date_end:
return 'AFTER'
return 'IN'
def compute_criteria(
date_start,
date_end,
has_last_date_invoiced,
is_auto_renew,
successor_contract_line_id,
predecessor_contract_line_id,
is_canceled,
):
return Criteria(
when=compute_when(date_start, date_end),
has_date_end=bool(date_end),
has_last_date_invoiced=bool(has_last_date_invoiced),
is_auto_renew=is_auto_renew,
has_successor=bool(successor_contract_line_id),
predecessor_has_successor=bool(
predecessor_contract_line_id.successor_contract_line_id
),
canceled=is_canceled,
)
def get_allowed(
date_start,
date_end,
has_last_date_invoiced,
is_auto_renew,
successor_contract_line_id,
predecessor_contract_line_id,
is_canceled,
):
criteria = compute_criteria(
date_start,
date_end,
has_last_date_invoiced,
is_auto_renew,
successor_contract_line_id,
predecessor_contract_line_id,
is_canceled,
)
if criteria in criteria_allowed_dict:
return criteria_allowed_dict[criteria]
return False

View 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_id',
copy=True,
string='Invoice Lines',
)

View File

@@ -0,0 +1,24 @@
# 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_id = fields.Many2one(
string='Contract',
comodel_name='account.analytic.contract',
required=True,
ondelete='cascade',
oldname='analytic_account_id',
)

View File

@@ -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'
)

View File

@@ -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>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
<?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>
<sheet>
<group col="4">
<field colspan="4" name="product_id"/>
<field colspan="4" name="name"/>
<field colspan="2" name="quantity"/>
<field colspan="2" name="uom_id"/>
<field colspan="2" name="automatic_price"/>
<field name="specific_price" invisible="1"/>
<field colspan="2" name="price_unit"
attrs="{'readonly': [('automatic_price', '=', True)]}"/>
<field colspan="2" name="discount" groups="base.group_no_one"/>
</group>
<group col="4">
<field colspan="2" name="is_auto_renew"/>
<field colspan="2" name="is_canceled" invisible="1"/>
</group>
<group attrs="{'invisible':[('is_auto_renew', '=', False)]}">
<group>
<label for="auto_renew_interval"/>
<div>
<field name="auto_renew_interval"
class="oe_inline" nolabel="1"
attrs="{'required':[('is_auto_renew', '=', True)]}"/>
<field name="auto_renew_rule_type"
class="oe_inline" nolabel="1"
attrs="{'required':[('is_auto_renew', '=', True)]}"/>
</div>
</group>
<group>
<label for="termination_notice_interval"/>
<div>
<field name="termination_notice_interval"
class="oe_inline" nolabel="1"
attrs="{'required':[('is_auto_renew', '=', True)]}"/>
<field name="termination_notice_rule_type"
class="oe_inline" nolabel="1"
attrs="{'required':[('is_auto_renew', '=', True)]}"/>
</div>
</group>
</group>
<group name="recurrence_info">
<group>
<label for="recurring_interval"/>
<div>
<field name="recurring_interval"
class="oe_inline" nolabel="1"/>
<field name="recurring_rule_type"
class="oe_inline" nolabel="1"/>
</div>
</group>
<group>
<field name="recurring_invoicing_type"
attrs="{'invisible': [('recurring_rule_type', '=', 'monthlylastday')]}"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
</odoo>

View File

@@ -1,279 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_analytic_account_recurring_form_form" model="ir.ui.view">
<field name="name">Contract form</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="analytic.view_account_analytic_account_form"/>
<field name="mode">primary</field>
<field name="priority" eval="9999"/>
<field name="arch" type="xml">
<field name="partner_id" position="attributes">
<attribute name="attrs">{'required': [('recurring_invoices', '=', True)]}</attribute>
</field>
<xpath expr="//div[@name='button_box']/.." position="before">
<header>
<button name="action_contract_send" type="object" string="Send by Email" groups="base.group_user"/>
</header>
</xpath>
<xpath expr='//field[@name="code"]' position='before'>
<field name="contract_type" invisible="1" required="1"/>
</xpath>
<group name="main" position="after">
<separator string="Recurring Invoices"
attrs="{'invisible': [('recurring_invoices','!=',True)]}"
/>
<div>
<field name="recurring_invoices" class="oe_inline"/>
<field name="create_invoice_visibility" invisible="1"/>
<label for="recurring_invoices" />
<button name="recurring_create_invoice"
type="object"
attrs="{'invisible': ['|', ('recurring_invoices', '!=', True), ('create_invoice_visibility', '=', False)]}"
string="Create invoices"
class="oe_link"
groups="base.group_no_one"
/>
<button name="button_show_recurring_invoices"
type="object"
attrs="{'invisible': [('recurring_invoices','!=',True)]}"
string="⇒ Show recurring invoices"
class="oe_link"
/>
</div>
<group col="4" attrs="{'invisible': [('recurring_invoices','!=',True)]}">
<field name="contract_template_id" colspan="4" domain="['|', ('contract_type', '=', contract_type), ('contract_type', '=', False)]" context="{'default_contract_type': contract_type}"/>
<field name="journal_id"
domain="[('type', '=', contract_type),('company_id', '=', company_id)]"
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="date_end"/>
<field name="recurring_next_date"
attrs="{'required': [('recurring_invoices', '=', True)]}"
/>
</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">
<field name="sequence" widget="handle" />
<field name="product_id"/>
<field name="name"/>
<field name="quantity"/>
<field name="uom_id"/>
<field name="automatic_price"/>
<field name="price_unit" attrs="{'readonly': [('automatic_price', '=', True)]}"/>
<field name="specific_price" invisible="1"/>
<field name="discount" groups="base.group_no_one" />
<field name="price_subtotal"/>
</tree>
</field>
</div>
<group string="Legend (for the markers inside invoice lines description)"
name="group_legend" attrs="{'invisible': [('recurring_invoices','!=',True)]}">
<p colspan="2"> <strong>#START#</strong>: Start date of the invoiced period</p>
<p colspan="2"> <strong>#END#</strong>: End date of the invoiced period</p>
</group>
</group>
</field>
</record>
<record id="account_analytic_account_sale_form" model="ir.ui.view">
<field name="name">account.analytic.account.sale.form</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="account_analytic_account_recurring_form_form"/>
<field name="mode">primary</field>
<field name="priority" eval="20"/>
<field name="arch" type="xml">
<field name="partner_id" position="attributes">
<attribute name="string">Customer</attribute>
<attribute name="domain">[('customer', '=', True)]</attribute>
<attribute name="context">{'default_customer': True, 'default_supplier': False}</attribute>
</field>
<field name="journal_id" position="attributes">
<attribute name="domain">[('type', '=', 'sale'),('company_id', '=', company_id)]</attribute>
</field>
<field name="product_id" position="attributes">
<attribute name="domain">[('sale_ok', '=', True)]</attribute>
</field>
</field>
</record>
<record id="account_analytic_account_purchase_form" model="ir.ui.view">
<field name="name">account.analytic.account.purchase.form</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="account_analytic_account_recurring_form_form"/>
<field name="mode">primary</field>
<field name="priority" eval="20"/>
<field name="arch" type="xml">
<field name="partner_id" position="attributes">
<attribute name="string">Supplier</attribute>
<attribute name="domain">[('supplier', '=', True)]</attribute>
<attribute name="context">{'default_customer': False, 'default_supplier': True}</attribute>
</field>
<field name="journal_id" position="attributes">
<attribute name="domain">[('type', '=', 'purchase'),('company_id', '=', company_id)]</attribute>
</field>
<field name="product_id" position="attributes">
<attribute name="domain">[('purchase_ok', '=', True)]</attribute>
</field>
<xpath expr="//field[@name='recurring_invoice_line_ids']/tree/field[@name='automatic_price']" position="attributes">
<attribute name="invisible">True</attribute>
</xpath>
</field>
</record>
<!-- Inherited Analytic Account list for contracts -->
<record id="view_account_analytic_account_journal_tree" model="ir.ui.view">
<field name="name">Contract list</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="analytic.view_account_analytic_account_list" />
<field name="mode">primary</field>
<field name="priority" eval="9999"/>
<field name="arch" type="xml">
<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>
<!-- Analytic Account search view for contract -->
<record id="view_account_analytic_account_contract_search" model="ir.ui.view">
<field name="name">Contract search</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="analytic.view_account_analytic_account_search"/>
<field name="arch" type="xml">
<field name="partner_id" position="after">
<field name="partner_id" filter_domain="[('partner_id', 'child_of', self)]"
string="Partner and dependents"/>
</field>
<field name="name" position="after">
<field name="journal_id"/>
<field name="pricelist_id"/>
<separator/>
<filter name="recurring_invoices"
string="Recurring Invoices"
domain="[('recurring_invoices','=',True)]"
/>
<separator/>
<filter name="not_finished"
string="Valid"
domain="['|', ('date_end', '=', False), ('date_end', '&gt;=', time.strftime('%Y-%m-%d'))]"
/>
<filter name="finished"
string="Finished"
domain="[('date_end', '&lt;', time.strftime('%Y-%m-%d'))]"
/>
<group expand="0" string="Group By...">
<filter name="next_invoice"
string="Next Invoice"
domain="[]"
context="{'group_by':'recurring_next_date'}"
/>
<filter name="date_end"
string="Date End"
domain="[]"
context="{'group_by':'date_end'}"
/>
</group>
</field>
</field>
</record>
<!-- Action Sales/Sales/Contracts -->
<record id="action_account_analytic_sale_overdue_all" model="ir.actions.act_window">
<field name="name">Customer Contracts</field>
<field name="res_model">account.analytic.account</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('contract_type', '=', 'sale')]</field>
<field name="context">{'is_contract':1, 'search_default_not_finished':1, 'search_default_recurring_invoices':1, 'default_recurring_invoices': 1, 'default_contract_type': 'sale'}</field>
<field name="search_view_id" ref="view_account_analytic_account_contract_search"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new contract.
</p>
</field>
</record>
<record id="action_account_analytic_sale_overdue_all_tree" model="ir.actions.act_window.view">
<field name="sequence" eval="1"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="view_account_analytic_account_journal_tree"/>
<field name="act_window_id" ref="action_account_analytic_sale_overdue_all"/>
</record>
<record id="action_account_analytic_sale_overdue_all_form" model="ir.actions.act_window.view">
<field name="sequence" eval="2"/>
<field name="view_mode">form</field>
<field name="view_id" ref="account_analytic_account_sale_form"/>
<field name="act_window_id" ref="action_account_analytic_sale_overdue_all"/>
</record>
<menuitem id="menu_action_account_analytic_sale_overdue_all"
parent="account.menu_finance_receivables"
action="action_account_analytic_sale_overdue_all"
sequence="99"
/>
<!-- Action Purchases/Purchases/Contracts -->
<record id="action_account_analytic_purchase_overdue_all" model="ir.actions.act_window">
<field name="name">Supplier Contracts</field>
<field name="res_model">account.analytic.account</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('contract_type', '=', 'purchase')]</field>
<field name="context">{'is_contract':1, 'search_default_not_finished':1, 'search_default_recurring_invoices':1, 'default_recurring_invoices': 1, 'default_contract_type': 'purchase'}</field>
<field name="search_view_id" ref="view_account_analytic_account_contract_search"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new contract.
</p>
</field>
</record>
<record id="action_account_analytic_purchase_overdue_all_tree" model="ir.actions.act_window.view">
<field name="sequence" eval="1"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="view_account_analytic_account_journal_tree"/>
<field name="act_window_id" ref="action_account_analytic_purchase_overdue_all"/>
</record>
<record id="action_account_analytic_purchase_overdue_all_form" model="ir.actions.act_window.view">
<field name="sequence" eval="2"/>
<field name="view_mode">form</field>
<field name="view_id" ref="account_analytic_account_purchase_form"/>
<field name="act_window_id" ref="action_account_analytic_purchase_overdue_all"/>
</record>
<menuitem id="menu_action_account_analytic_purchase_overdue_all"
parent="account.menu_finance_payables"
action="action_account_analytic_purchase_overdue_all"
sequence="99"
/>
</odoo>

314
contract/views/contract.xml Normal file
View File

@@ -0,0 +1,314 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_analytic_account_recurring_form_form"
model="ir.ui.view">
<field name="name">Contract form</field>
<field name="model">account.analytic.account</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_contract_send"
type="object"
string="Send by Email"
groups="base.group_user"/>
<button name="recurring_create_invoice"
type="object"
attrs="{'invisible': [('create_invoice_visibility', '=', False)]}"
string="Create invoices"
groups="base.group_no_one"/>
</header>
<sheet string="Contract">
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" type="object"
name="toggle_active" icon="fa-archive">
<field name="active" widget="boolean_button"
options="{&quot;terminology&quot;: &quot;archive&quot;}"/>
</button>
<button name="contract.act_recurring_invoices"
type="action"
string="Invoices"
icon="fa-list"
class="oe_stat_button"/>
</div>
<div class="oe_title">
<label for="name" string="Contract Name"
class="oe_edit_only"/>
<h3>
<field name="name" class="oe_inline"
placeholder="e.g. Contract XYZ"/>
</h3>
</div>
<group name="main">
<group>
<field name="partner_id" required="1"/>
<field name="user_id"/>
</group>
<group>
<field name="contract_template_id"
domain="['|', ('contract_type', '=', contract_type), ('contract_type', '=', False)]"
context="{'default_contract_type': contract_type}"/>
<field name="contract_type" invisible="1"
required="1"/>
</group>
</group>
<group name="recurring_invoices">
<group>
<field name="journal_id" required="1"/>
<field name="recurring_next_date"/>
</group>
<group>
<field name="pricelist_id"/>
<field name="date_end"/>
</group>
</group>
<notebook>
<page name="recurring_invoice_line"
string="Recurring Invoices">
<field name="recurring_invoice_line_ids" context="{'default_contract_type': contract_type}"/>
</page>
<page name="info" string="Other Information">
<div invisible="1">
<field name="recurring_invoices"
class="oe_inline"/>
<label for="recurring_invoices"/>
</div>
<field name="create_invoice_visibility"
invisible="1"/>
<group>
<field name="code"/>
<field name="group_id"/>
<field name="company_id"
options="{'no_create': True}"
groups="base.group_multi_company"/>
<field name="currency_id"
options="{'no_create': True}"
groups="base.group_multi_currency"/>
</group>
<group string="Legend (for the markers inside invoice lines description)"
name="group_legend">
<p colspan="2"><strong>#START#</strong>: Start
date
of the
invoiced period
</p>
<p colspan="2"><strong>#END#</strong>: End date
of
the
invoiced period
</p>
</group>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"
widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<record id="account_analytic_account_sale_form" model="ir.ui.view">
<field name="name">account.analytic.account.sale.form</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id"
ref="account_analytic_account_recurring_form_form"/>
<field name="mode">primary</field>
<field name="priority" eval="20"/>
<field name="arch" type="xml">
<field name="partner_id" position="attributes">
<attribute name="string">Customer</attribute>
<attribute name="domain">[('customer', '=', True)]</attribute>
<attribute name="context">{'default_customer': True,
'default_supplier': False}
</attribute>
</field>
<field name="journal_id" position="attributes">
<attribute name="domain">[('type', '=', 'sale'),('company_id', '=', company_id)]</attribute>
</field>
</field>
</record>
<record id="account_analytic_account_purchase_form" model="ir.ui.view">
<field name="name">account.analytic.account.purchase.form</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id"
ref="account_analytic_account_recurring_form_form"/>
<field name="mode">primary</field>
<field name="priority" eval="20"/>
<field name="arch" type="xml">
<field name="partner_id" position="attributes">
<attribute name="string">Supplier</attribute>
<attribute name="domain">[('supplier', '=', True)]</attribute>
<attribute name="context">{'default_customer': False,
'default_supplier': True}
</attribute>
</field>
<field name="journal_id" position="attributes">
<attribute name="domain">[('type', '=', 'purchase'),('company_id', '=', company_id)]</attribute>
</field>
</field>
</record>
<!-- Inherited Analytic Account list for contracts -->
<record id="view_account_analytic_account_journal_tree" model="ir.ui.view">
<field name="name">Contract list</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id"
ref="analytic.view_account_analytic_account_list"/>
<field name="mode">primary</field>
<field name="priority" eval="9999"/>
<field name="arch" type="xml">
<field name="partner_id" position="before">
<field name="journal_id" groups="account.group_account_user"/>
</field>
</field>
</record>
<!-- Analytic Account search view for contract -->
<record id="view_account_analytic_account_contract_search"
model="ir.ui.view">
<field name="name">Contract search</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id"
ref="analytic.view_account_analytic_account_search"/>
<field name="arch" type="xml">
<field name="partner_id" position="after">
<field name="partner_id"
filter_domain="[('partner_id', 'child_of', self)]"
string="Partner and dependents"/>
</field>
<field name="name" position="after">
<field name="journal_id"/>
<field name="pricelist_id"/>
<separator/>
<filter name="recurring_invoices"
string="Recurring Invoices"
domain="[('recurring_invoices','=',True)]"
/>
<separator/>
<filter name="not_finished"
string="Valid"
domain="['|', ('date_end', '=', False), ('date_end', '&gt;=', time.strftime('%Y-%m-%d'))]"
/>
<filter name="finished"
string="Finished"
domain="[('date_end', '&lt;', time.strftime('%Y-%m-%d'))]"
/>
<group expand="0" string="Group By...">
<filter name="next_invoice"
string="Next Invoice"
domain="[]"
context="{'group_by':'recurring_next_date'}"
/>
<filter name="date_end"
string="Date End"
domain="[]"
context="{'group_by':'date_end'}"
/>
</group>
</field>
</field>
</record>
<!-- Action Sales/Sales/Contracts -->
<record id="action_account_analytic_sale_overdue_all"
model="ir.actions.act_window">
<field name="name">Customer Contracts</field>
<field name="res_model">account.analytic.account</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('contract_type', '=', 'sale')]</field>
<field name="context">{'is_contract':1,
'search_default_not_finished':1,
'search_default_recurring_invoices':1,
'default_recurring_invoices': 1, 'default_contract_type': 'sale'}
</field>
<field name="search_view_id"
ref="view_account_analytic_account_contract_search"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new contract.
</p>
</field>
</record>
<record id="action_account_analytic_sale_overdue_all_tree"
model="ir.actions.act_window.view">
<field name="sequence" eval="1"/>
<field name="view_mode">tree</field>
<field name="view_id"
ref="view_account_analytic_account_journal_tree"/>
<field name="act_window_id"
ref="action_account_analytic_sale_overdue_all"/>
</record>
<record id="action_account_analytic_sale_overdue_all_form"
model="ir.actions.act_window.view">
<field name="sequence" eval="2"/>
<field name="view_mode">form</field>
<field name="view_id" ref="account_analytic_account_sale_form"/>
<field name="act_window_id"
ref="action_account_analytic_sale_overdue_all"/>
</record>
<menuitem id="menu_action_account_analytic_sale_overdue_all"
parent="account.menu_finance_receivables"
action="action_account_analytic_sale_overdue_all"
sequence="99"
/>
<!-- Action Purchases/Purchases/Contracts -->
<record id="action_account_analytic_purchase_overdue_all"
model="ir.actions.act_window">
<field name="name">Supplier Contracts</field>
<field name="res_model">account.analytic.account</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('contract_type', '=', 'purchase')]</field>
<field name="context">{'is_contract':1,
'search_default_not_finished':1,
'search_default_recurring_invoices':1,
'default_recurring_invoices': 1, 'default_contract_type':
'purchase'}
</field>
<field name="search_view_id"
ref="view_account_analytic_account_contract_search"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new contract.
</p>
</field>
</record>
<record id="action_account_analytic_purchase_overdue_all_tree"
model="ir.actions.act_window.view">
<field name="sequence" eval="1"/>
<field name="view_mode">tree</field>
<field name="view_id"
ref="view_account_analytic_account_journal_tree"/>
<field name="act_window_id"
ref="action_account_analytic_purchase_overdue_all"/>
</record>
<record id="action_account_analytic_purchase_overdue_all_form"
model="ir.actions.act_window.view">
<field name="sequence" eval="2"/>
<field name="view_mode">form</field>
<field name="view_id" ref="account_analytic_account_purchase_form"/>
<field name="act_window_id"
ref="action_account_analytic_purchase_overdue_all"/>
</record>
<menuitem id="menu_action_account_analytic_purchase_overdue_all"
parent="account.menu_finance_payables"
action="action_account_analytic_purchase_overdue_all"
sequence="99"
/>
</odoo>

View File

@@ -0,0 +1,166 @@
<?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="//sheet" position="before">
<header>
<field name="state" widget="statusbar"/>
</header>
</xpath>
<xpath expr="//form" position="attributes">
<attribute name="string">Contract Line</attribute>
</xpath>
<xpath expr="//group[@name='recurrence_info']" position="inside">
<group>
<field name="create_invoice_visibility" invisible="1"/>
<field name="date_start" required="1"/>
<field name="recurring_next_date"/>
</group>
<group>
<field name="date_end"
attrs="{'required': [('is_auto_renew', '=', True)]}"/>
</group>
<group>
<field name="predecessor_contract_line_id"/>
</group>
<group>
<field name="successor_contract_line_id"/>
</group>
</xpath>
</field>
</record>
<record id="account_analytic_invoice_line_sale_view_form"
model="ir.ui.view">
<field name="name">account.analytic.invoice.line.sale.form</field>
<field name="model">account.analytic.invoice.line</field>
<field name="inherit_id"
ref="account_analytic_invoice_line_view_form"/>
<field name="mode">primary</field>
<field name="priority" eval="20"/>
<field name="arch" type="xml">
<field name="product_id" position="attributes">
<attribute name="domain">[('sale_ok', '=', True)]</attribute>
</field>
</field>
</record>
<record id="account_analytic_invoice_line_purchase_view_form"
model="ir.ui.view">
<field name="name">account.analytic.invoice.line.purchase.form</field>
<field name="model">account.analytic.invoice.line</field>
<field name="inherit_id"
ref="account_analytic_invoice_line_view_form"/>
<field name="mode">primary</field>
<field name="priority" eval="20"/>
<field name="arch" type="xml">
<field name="product_id" position="attributes">
<attribute name="domain">[('purchase_ok', '=', True)]
</attribute>
</field>
<xpath expr="//field[@name='automatic_price']"
position="attributes">
<attribute name="invisible">True</attribute>
</xpath>
</field>
</record>
<record id="account_analytic_invoice_line_view_tree" model="ir.ui.view">
<field name="name">account.analytic.invoice.line.tree</field>
<field name="model">account.analytic.invoice.line</field>
<field name="arch" type="xml">
<tree decoration-muted="is_canceled"
decoration-info="create_invoice_visibility and not is_canceled">
<field name="sequence" widget="handle"/>
<field name="product_id"/>
<field name="name"/>
<field name="quantity"/>
<field name="uom_id"/>
<field name="automatic_price"/>
<field name="price_unit"
attrs="{'readonly': [('automatic_price', '=', True)]}"/>
<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"/>
<field name="last_date_invoiced"
groups="base.group_no_one"/>
<field name="create_invoice_visibility"
invisible="1"/>
<field name="is_plan_successor_allowed"
invisible="1"/>
<field name="is_stop_plan_successor_allowed"
invisible="1"/>
<field name="is_stop_allowed"
invisible="1"/>
<field name="is_cancel_allowed"
invisible="1"/>
<field name="is_un_cancel_allowed"
invisible="1"/>
<field name="is_auto_renew" invisible="1"/>
<field name="is_canceled" invisible="1"/>
<button name="action_plan_successor"
string="Plan Start" type="object"
icon="fa-calendar text-success"
attrs="{'invisible': [('is_plan_successor_allowed', '=', False)]}"/>
<button name="action_stop_plan_successor"
string="Stop Plan Successor"
type="object"
icon="fa-pause text-muted"
attrs="{'invisible': [('is_stop_plan_successor_allowed', '=', False)]}"/>
<button name="action_stop" string="Stop"
type="object"
icon="fa-stop text-danger"
attrs="{'invisible': [('is_stop_allowed', '=', False)]}"/>
<button name="cancel" string="Cancel"
type="object"
icon="fa-ban text-danger"
confirm="Are you sure you want to cancel this line"
attrs="{'invisible': [('is_cancel_allowed', '=', False)]}"/>
<button name="action_uncancel"
string="Un-cancel" type="object"
icon="fa-ban text-success"
attrs="{'invisible': [('is_un_cancel_allowed', '=', False)]}"/>
<button name="renew" string="Renew"
type="object"
icon="fa-fast-forward text-success"
groups="base.group_no_one"
attrs="{'invisible': [('is_auto_renew', '=', False)]}"/>
</tree>
</field>
</record>
<record id="account_analytic_invoice_line_purchase_view_tree"
model="ir.ui.view">
<field name="name">account.analytic.invoice.line.purchase.tree</field>
<field name="model">account.analytic.invoice.line</field>
<field name="mode">primary</field>
<field name="priority" eval="20"/>
<field name="inherit_id"
ref="account_analytic_invoice_line_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='automatic_price']"
position="attributes">
<attribute name="invisible">True</attribute>
</xpath>
</field>
</record>
</odoo>

View File

@@ -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'}"

View 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>

View File

@@ -0,0 +1 @@
from . import contract_line_wizard

View File

@@ -0,0 +1,48 @@
# Copyright 2018 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountAnalyticInvoiceLineWizard(models.TransientModel):
_name = 'account.analytic.invoice.line.wizard'
_description = 'Contract Line Wizard'
date_start = fields.Date(string='Date Start')
date_end = fields.Date(string='Date End')
recurring_next_date = fields.Date(string='Next Invoice Date')
is_auto_renew = fields.Boolean(string="Auto Renew", default=False)
contract_line_id = fields.Many2one(
comodel_name="account.analytic.invoice.line",
string="Contract Line",
required=True,
)
@api.multi
def stop(self):
for wizard in self:
wizard.contract_line_id.stop(wizard.date_end)
return True
@api.multi
def plan_successor(self):
for wizard in self:
wizard.contract_line_id.plan_successor(
wizard.date_start, wizard.date_end, wizard.is_auto_renew
)
return True
@api.multi
def stop_plan_successor(self):
for wizard in self:
wizard.contract_line_id.stop_plan_successor(
wizard.date_start, wizard.date_end, wizard.is_auto_renew
)
return True
@api.multi
def uncancel(self):
for wizard in self:
wizard.contract_line_id.uncancel(wizard.recurring_next_date)
return True

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2018 ACSONE SA/NV
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="contract_line_wizard_stop_form_view">
<field name="name">contract.line.stop.wizard.form (in contract)</field>
<field name="model">account.analytic.invoice.line.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<field name="contract_line_id" invisible="True"/>
<field string="Stop Date" name="date_end" required="True"/>
</group>
<footer>
<button name="stop"
string="Validate"
class="btn-primary"
type="object"/>
<button string="Cancel"
class="btn-default"
special="cancel"/>
</footer>
</form>
</field>
</record>
<record model="ir.ui.view" id="contract_line_wizard_plan_successor_form_view">
<field name="name">contract.line.plan_successor.wizard.form (in contract)</field>
<field name="model">account.analytic.invoice.line.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<field name="contract_line_id" invisible="True"/>
<field name="date_start" required="True"/>
<field name="date_end" attrs="{'required': [('is_auto_renew', '=', True)]}"/>
<field name="is_auto_renew"/>
</group>
<footer>
<button name="plan_successor"
string="Validate"
class="btn-primary"
type="object"/>
<button string="Cancel"
class="btn-default"
special="cancel"/>
</footer>
</form>
</field>
</record>
<record model="ir.ui.view" id="contract_line_wizard_stop_plan_successor_form_view">
<field name="name">contract.line.stop_plan_successor.wizard.form (in contract)</field>
<field name="model">account.analytic.invoice.line.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<field name="contract_line_id" invisible="True"/>
<field string="Suspension Start Date" name="date_start" required="True"/>
<field string="Suspension End Date" name="date_end" required="True"/>
<field name="is_auto_renew" invisible="1"/>
</group>
<footer>
<button name="stop_plan_successor"
string="Validate"
class="btn-primary"
type="object"/>
<button string="Cancel"
class="btn-default"
special="cancel"/>
</footer>
</form>
</field>
</record>
<record model="ir.ui.view" id="contract_line_wizard_uncancel_form_view">
<field name="name">contract.line.stop_plan_successor.wizard.form (in contract)</field>
<field name="model">account.analytic.invoice.line.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<field name="contract_line_id" invisible="True"/>
<field name="recurring_next_date" required="True"/>
</group>
<footer>
<button name="uncancel"
string="Validate"
class="btn-primary"
type="object"/>
<button string="Cancel"
class="btn-default"
special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>