Merge PR #577 into 14.0

Signed-off-by pedrobaeza
This commit is contained in:
OCA-git-bot
2021-04-05 06:14:42 +00:00
139 changed files with 162123 additions and 0 deletions

143
contract/README.rst Normal file
View File

@@ -0,0 +1,143 @@
================================
Recurring - Contracts Management
================================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fcontract-lightgray.png?logo=github
:target: https://github.com/OCA/contract/tree/14.0/contract
:alt: OCA/contract
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/contract-14-0/contract-14-0-contract
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/110/14.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
This module enables contracts management with recurring
invoicing functions. Also you can print and send by email contract report.
It works for customer contract and supplier contracts.
Contracts are shown in portal.
**Table of contents**
.. contents::
:local:
Configuration
=============
To view discount field in contract line, you need to set *Discount on lines* in
user access rights.
Contracts can be viewed on the portal (list and detail) if the user logged into the portal is a follower of the contract.
Usage
=====
#. Contracts are in Invoicing -> Customers -> Customer and Invoicing -> Vendors -> Supplier Contracts
#. When creating a contract, fill fields for selecting the invoicing parameters:
* a journal
* a price list (optional)
#. And add the lines to be invoiced with:
* the product with a description, a quantity and a price
* the recurrence parameters: interval (days, weeks, months, months last day or years),
start date, date of next invoice (automatically computed, can be modified) and end date (optional)
* auto-price, for having a price automatically obtained from the price list
* #START# or #END# in the description field to display the start/end date of
the invoiced period in the invoice line description
* pre-paid (invoice at period start) or post-paid (invoice at start of next period)
#. The "Generate Recurring Invoices from Contracts" cron runs daily to generate the invoices.
If you are in debug mode, you can click on the invoice creation button.
#. The *Show recurring invoices* shortcut on contracts shows all invoices created from the
contract.
#. The contract report can be printed from the Print menu
#. The contract can be sent by email with the *Send by Email* button
#. Contract templates can be created from the Configuration -> Contracts -> Contract Templates menu.
They allow to define default journal, price list and lines when creating a contract.
To use it, just select the template on the contract and fields will be filled automatically.
* Contracts appear in portal to following users in every contract:
.. image:: https://raw.githubusercontent.com/OCA/contract/14.0/contract/static/src/screenshots/portal-my.png
.. image:: https://raw.githubusercontent.com/OCA/contract/14.0/contract/static/src/screenshots/portal-list.png
.. image:: https://raw.githubusercontent.com/OCA/contract/14.0/contract/static/src/screenshots/portal-detail.png
Known issues / Roadmap
======================
* Recover states and others functional fields in Contracts.
* Add recurrence flag at template level.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/contract/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/contract/issues/new?body=module:%20contract%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Tecnativa
* ACSONE SA/NV
Contributors
~~~~~~~~~~~~
* Angel Moya <angel.moya@domatix.com>
* Dave Lasley <dave@laslabs.com>
* Miquel Raïch <miquel.raich@eficent.com>
* Souheil Bejaoui <souheil.bejaoui@acsone.eu>
* Thomas Binsfeld <thomas.binsfeld@acsone.eu>
* Guillaume Vandamme <guillaume.vandamme@acsone.eu>
* Raphaël Reverdy <raphael.reverdy@akretion.com>
* `Tecnativa <https://www.tecnativa.com>`_:
* Pedro M. Baeza
* Carlos Dauden
* Vicent Cubells
* Rafael Blasco
* Víctor Martínez
* Iván Antón <ozono@ozonomultimedia.com>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/contract <https://github.com/OCA/contract/tree/14.0/contract>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

3
contract/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from . import controllers
from . import models
from . import wizards

50
contract/__manifest__.py Normal file
View File

@@ -0,0 +1,50 @@
# Copyright 2004-2010 OpenERP SA
# Copyright 2014-2018 Tecnativa - Pedro M. Baeza
# Copyright 2015 Domatix
# Copyright 2016-2018 Tecnativa - Carlos Dauden
# Copyright 2017 Tecnativa - Vicent Cubells
# Copyright 2016-2017 LasLabs Inc.
# Copyright 2018-2019 ACSONE SA/NV
# Copyright 2020-2021 Tecnativa - Pedro M. Baeza
# Copyright 2020 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Recurring - Contracts Management",
"version": "14.0.1.0.0",
"category": "Contract Management",
"license": "AGPL-3",
"author": "Tecnativa, ACSONE SA/NV, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/contract",
"depends": ["base", "account", "product", "portal"],
"external_dependencies": {"python": ["dateutil"]},
"data": [
"security/groups.xml",
"security/contract_tag.xml",
"security/ir.model.access.csv",
"security/contract_security.xml",
"security/contract_terminate_reason.xml",
"report/report_contract.xml",
"report/contract_views.xml",
"data/contract_cron.xml",
"data/contract_renew_cron.xml",
"data/mail_template.xml",
"data/mail_message_subtype.xml",
"data/ir_ui_menu.xml",
"wizards/contract_line_wizard.xml",
"wizards/contract_manually_create_invoice.xml",
"wizards/contract_contract_terminate.xml",
"views/contract_tag.xml",
"views/assets.xml",
"views/abstract_contract_line.xml",
"views/contract.xml",
"views/contract_line.xml",
"views/contract_template.xml",
"views/contract_template_line.xml",
"views/res_partner_view.xml",
"views/res_config_settings.xml",
"views/contract_terminate_reason.xml",
"views/contract_portal_templates.xml",
],
"installable": True,
}

View File

@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import main

View File

@@ -0,0 +1,99 @@
# Copyright 2020 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, http
from odoo.exceptions import AccessError, MissingError
from odoo.http import request
from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager
class PortalContract(CustomerPortal):
def _prepare_portal_layout_values(self):
values = super()._prepare_portal_layout_values()
model = "contract.contract"
values["contract_count"] = 0
if request.env[model].check_access_rights("read", raise_exception=False):
values["contract_count"] = request.env[model].search_count([])
return values
def _contract_get_page_view_values(self, contract, access_token, **kwargs):
values = {
"page_name": "Contracts",
"contract": contract,
}
return self._get_page_view_values(
contract, access_token, values, "my_contracts_history", False, **kwargs
)
def _get_filter_domain(self, kw):
return []
@http.route(
["/my/contracts", "/my/contracts/page/<int:page>"],
type="http",
auth="user",
website=True,
)
def portal_my_contracts(
self, page=1, date_begin=None, date_end=None, sortby=None, **kw
):
values = self._prepare_portal_layout_values()
contract_obj = request.env["contract.contract"]
domain = self._get_filter_domain(kw)
searchbar_sortings = {
"date": {"label": _("Date"), "order": "recurring_next_date desc"},
"name": {"label": _("Name"), "order": "name desc"},
"code": {"label": _("Reference"), "order": "code desc"},
}
# default sort by order
if not sortby:
sortby = "date"
order = searchbar_sortings[sortby]["order"]
# count for pager
contract_count = contract_obj.search_count(domain)
# pager
pager = portal_pager(
url="/my/contracts",
url_args={
"date_begin": date_begin,
"date_end": date_end,
"sortby": sortby,
},
total=contract_count,
page=page,
step=self._items_per_page,
)
# content according to pager and archive selected
contracts = contract_obj.search(
domain, order=order, limit=self._items_per_page, offset=pager["offset"]
)
request.session["my_contracts_history"] = contracts.ids[:100]
values.update(
{
"date": date_begin,
"contracts": contracts,
"page_name": "Contracts",
"pager": pager,
"default_url": "/my/contracts",
"searchbar_sortings": searchbar_sortings,
"sortby": sortby,
}
)
return request.render("contract.portal_my_contracts", values)
@http.route(
["/my/contracts/<int:contract_contract_id>"],
type="http",
auth="public",
website=True,
)
def portal_my_contract_detail(self, contract_contract_id, access_token=None, **kw):
try:
contract_sudo = self._document_check_access(
"contract.contract", contract_contract_id, access_token
)
except (AccessError, MissingError):
return request.redirect("/my")
values = self._contract_get_page_view_values(contract_sudo, access_token, **kw)
return request.render("contract.portal_contract_page", values)

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record model="ir.cron" id="contract_cron_for_invoice">
<field name="name">Generate Recurring Invoices from Contracts</field>
<field name="model_id" ref="model_contract_contract" />
<field name="state">code</field>
<field name="code">model.cron_recurring_create_invoice()</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,14 @@
<?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_contract_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,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<menuitem
id="menu_config_contract"
name="Contracts"
sequence="1"
parent="account.menu_finance_configuration"
/>
</odoo>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="mail_message_subtype_invoice_created" model="mail.message.subtype">
<field name="name">Invoice created</field>
<field name="res_model">contract.contract</field>
</record>
<record
id="mail_message_subtype_contract_modification"
model="mail.message.subtype"
>
<field name="name">Contract modifications</field>
<field name="res_model">contract.contract</field>
<field name="default" eval="False" />
</record>
</odoo>

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="email_contract_template" model="mail.template">
<field name="name">Email Contract Template</field>
<field
name="email_from"
>${(object.user_id.email and '%s &lt;%s&gt;' % (object.user_id.name, object.user_id.email) or '')|safe}</field>
<field
name="subject"
>${object.company_id.name} Contract (Ref ${object.name or 'n/a'})</field>
<field name="partner_to">${object.partner_id.id}</field>
<field name="model_id" ref="model_contract_contract" />
<field name="auto_delete" eval="True" />
<field name="report_template" ref="contract.report_contract" />
<field name="report_name">Contract</field>
<field name="lang">${object.partner_id.lang}</field>
<field
name="body_html"
><![CDATA[
<div style="font-family: 'Lucida Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: #FFF; ">
<p>Hello ${object.partner_id.name or ''},</p>
<p>A new contract has been created: </p>
<p style="border-left: 1px solid #8e0000; margin-left: 30px;">
&nbsp;&nbsp;<strong>REFERENCES</strong><br />
&nbsp;&nbsp;Contract: <strong>${object.name}</strong><br />
% if object.date_start:
&nbsp;&nbsp;Contract Date Start: ${object.date_start or ''}<br />
% endif
% if object.user_id:
% if object.user_id.email:
&nbsp;&nbsp;Your Contact: <a href="mailto:${object.user_id.email or ''}?subject=Contract%20${object.name}">${object.user_id.name}</a>
% else:
&nbsp;&nbsp;Your Contact: ${object.user_id.name}
% endif
% endif
</p>
<br/>
<p>If you have any questions, do not hesitate to contact us.</p>
<p>Thank you for choosing ${object.company_id.name or 'us'}!</p>
<br/>
<br/>
<div style="width: 375px; margin: 0px; padding: 0px; background-color: #8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; background-repeat: repeat no-repeat;">
<h3 style="margin: 0px; padding: 2px 14px; font-size: 12px; color: #DDD;">
<strong style="text-transform:uppercase;">${object.company_id.name}</strong></h3>
</div>
<div style="width: 347px; margin: 0px; padding: 5px 14px; line-height: 16px; background-color: #F2F2F2;">
<span style="color: #222; margin-bottom: 5px; display: block; ">
${object.company_id.partner_id.sudo().with_context(show_address=True, html_format=True).name_get()[0][1] | safe}
</span>
% if object.company_id.phone:
<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
Phone: ${object.company_id.phone}
</div>
% endif
% if object.company_id.website:
<div>
Web: <a href="${object.company_id.website}">${object.company_id.website}</a>
</div>
%endif
<p></p>
</div>
<p></p>
<a href="${object.get_base_url()}/my/contracts/${object.id}?access_token=${object.access_token}" target="_blank" style="background-color:#875A7B;padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">View contract</a>
</div>
]]></field>
</record>
<record id="mail_template_contract_modification" model="mail.template">
<field name="name">Contract Modification Template</field>
<field
name="email_from"
>${(object.user_id.email and '%s &lt;%s&gt;' % (object.user_id.name, object.user_id.email) or '')|safe}</field>
<field
name="subject"
>${object.company_id.name} Contract (Ref ${object.name or 'n/a'}) - Modifications</field>
<field name="model_id" ref="model_contract_contract" />
<field name="lang">${object.partner_id.lang}</field>
<field
name="body_html"
><![CDATA[
<p>Hello</p>
<p>We have modifications on the contract that we want to notify you.</p>
]]></field>
</record>
<template
id="mail_notification_contract"
inherit_id="mail.mail_notification_paynow"
primary="True"
>
<xpath expr="//t[@t-raw='message.body']" position="after">
<t t-raw="0" />
</xpath>
</template>
<template id="template_contract_modification" name="Contract Modification">
<t t-call="contract.mail_notification_contract">
<table border="1" align="center">
<thead>
<tr>
<th name="th_date">Date</th>
<th name="th_description">Description</th>
</tr>
</thead>
<tbody>
<t t-foreach="record.modification_ids" t-as="modification">
<tr t-if="not modification.sent">
<td name="td_date">
<span t-field="modification.date" />
</td>
<td name="td_description">
<div t-field="modification.description" />
</td>
</tr>
</t>
</tbody>
</table>
</t>
</template>
</odoo>

2162
contract/i18n/am.po Normal file

File diff suppressed because it is too large Load Diff

2163
contract/i18n/ar.po Normal file

File diff suppressed because it is too large Load Diff

2163
contract/i18n/bg.po Normal file

File diff suppressed because it is too large Load Diff

2162
contract/i18n/bs.po Normal file

File diff suppressed because it is too large Load Diff

2349
contract/i18n/ca.po Normal file

File diff suppressed because it is too large Load Diff

2139
contract/i18n/contract.pot Normal file

File diff suppressed because it is too large Load Diff

2162
contract/i18n/cs.po Normal file

File diff suppressed because it is too large Load Diff

2162
contract/i18n/da.po Normal file

File diff suppressed because it is too large Load Diff

2455
contract/i18n/de.po Normal file

File diff suppressed because it is too large Load Diff

2166
contract/i18n/el_GR.po Normal file

File diff suppressed because it is too large Load Diff

2163
contract/i18n/en_GB.po Normal file

File diff suppressed because it is too large Load Diff

2320
contract/i18n/es.po Normal file

File diff suppressed because it is too large Load Diff

2449
contract/i18n/es_AR.po Normal file

File diff suppressed because it is too large Load Diff

2296
contract/i18n/es_CL.po Normal file

File diff suppressed because it is too large Load Diff

2161
contract/i18n/es_CO.po Normal file

File diff suppressed because it is too large Load Diff

2163
contract/i18n/es_CR.po Normal file

File diff suppressed because it is too large Load Diff

2161
contract/i18n/es_DO.po Normal file

File diff suppressed because it is too large Load Diff

2161
contract/i18n/es_EC.po Normal file

File diff suppressed because it is too large Load Diff

2166
contract/i18n/es_MX.po Normal file

File diff suppressed because it is too large Load Diff

2161
contract/i18n/es_PY.po Normal file

File diff suppressed because it is too large Load Diff

2163
contract/i18n/es_VE.po Normal file

File diff suppressed because it is too large Load Diff

2161
contract/i18n/et.po Normal file

File diff suppressed because it is too large Load Diff

2160
contract/i18n/eu.po Normal file

File diff suppressed because it is too large Load Diff

2162
contract/i18n/fa.po Normal file

File diff suppressed because it is too large Load Diff

2398
contract/i18n/fi.po Normal file

File diff suppressed because it is too large Load Diff

2472
contract/i18n/fr.po Normal file

File diff suppressed because it is too large Load Diff

2164
contract/i18n/fr_CA.po Normal file

File diff suppressed because it is too large Load Diff

2163
contract/i18n/fr_CH.po Normal file

File diff suppressed because it is too large Load Diff

2412
contract/i18n/gl.po Normal file

File diff suppressed because it is too large Load Diff

2161
contract/i18n/gl_ES.po Normal file

File diff suppressed because it is too large Load Diff

2162
contract/i18n/he.po Normal file

File diff suppressed because it is too large Load Diff

2211
contract/i18n/hi_IN.po Normal file

File diff suppressed because it is too large Load Diff

2241
contract/i18n/hr.po Normal file

File diff suppressed because it is too large Load Diff

2246
contract/i18n/hr_HR.po Normal file

File diff suppressed because it is too large Load Diff

2165
contract/i18n/hu.po Normal file

File diff suppressed because it is too large Load Diff

2162
contract/i18n/id.po Normal file

File diff suppressed because it is too large Load Diff

2450
contract/i18n/it.po Normal file

File diff suppressed because it is too large Load Diff

2162
contract/i18n/ja.po Normal file

File diff suppressed because it is too large Load Diff

2162
contract/i18n/ko.po Normal file

File diff suppressed because it is too large Load Diff

2163
contract/i18n/lt.po Normal file

File diff suppressed because it is too large Load Diff

2164
contract/i18n/lt_LT.po Normal file

File diff suppressed because it is too large Load Diff

2163
contract/i18n/lv.po Normal file

File diff suppressed because it is too large Load Diff

2162
contract/i18n/mk.po Normal file

File diff suppressed because it is too large Load Diff

2162
contract/i18n/mn.po Normal file

File diff suppressed because it is too large Load Diff

2163
contract/i18n/nb.po Normal file

File diff suppressed because it is too large Load Diff

2164
contract/i18n/nb_NO.po Normal file

File diff suppressed because it is too large Load Diff

2476
contract/i18n/nl.po Normal file

File diff suppressed because it is too large Load Diff

2163
contract/i18n/nl_BE.po Normal file

File diff suppressed because it is too large Load Diff

2205
contract/i18n/nl_NL.po Normal file

File diff suppressed because it is too large Load Diff

2164
contract/i18n/pl.po Normal file

File diff suppressed because it is too large Load Diff

2470
contract/i18n/pt.po Normal file

File diff suppressed because it is too large Load Diff

2416
contract/i18n/pt_BR.po Normal file

File diff suppressed because it is too large Load Diff

2168
contract/i18n/pt_PT.po Normal file

File diff suppressed because it is too large Load Diff

2167
contract/i18n/ro.po Normal file

File diff suppressed because it is too large Load Diff

2207
contract/i18n/ru.po Normal file

File diff suppressed because it is too large Load Diff

2162
contract/i18n/sk.po Normal file

File diff suppressed because it is too large Load Diff

2164
contract/i18n/sk_SK.po Normal file

File diff suppressed because it is too large Load Diff

2162
contract/i18n/sl.po Normal file

File diff suppressed because it is too large Load Diff

2162
contract/i18n/sr.po Normal file

File diff suppressed because it is too large Load Diff

2163
contract/i18n/sr@latin.po Normal file

File diff suppressed because it is too large Load Diff

2162
contract/i18n/sv.po Normal file

File diff suppressed because it is too large Load Diff

2162
contract/i18n/th.po Normal file

File diff suppressed because it is too large Load Diff

2379
contract/i18n/tr.po Normal file

File diff suppressed because it is too large Load Diff

2241
contract/i18n/tr_TR.po Normal file

File diff suppressed because it is too large Load Diff

2163
contract/i18n/uk.po Normal file

File diff suppressed because it is too large Load Diff

2161
contract/i18n/vi.po Normal file

File diff suppressed because it is too large Load Diff

2163
contract/i18n/vi_VN.po Normal file

File diff suppressed because it is too large Load Diff

2435
contract/i18n/zh_CN.po Normal file

File diff suppressed because it is too large Load Diff

2162
contract/i18n/zh_TW.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import contract_recurrency_mixin # should be first
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 contract_modification
from . import account_move
from . import res_partner
from . import contract_tag
from . import res_company
from . import res_config_settings
from . import contract_terminate_reason

View File

@@ -0,0 +1,69 @@
# Copyright 2004-2010 OpenERP SA
# Copyright 2014 Angel Moya <angel.moya@domatix.com>
# Copyright 2015-2020 Tecnativa - Pedro M. Baeza
# 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
class ContractAbstractContract(models.AbstractModel):
_inherit = "contract.recurrency.basic.mixin"
_name = "contract.abstract.contract"
_description = "Abstract Recurring Contract"
# These fields will not be synced to the contract
NO_SYNC = ["name", "partner_id", "company_id"]
name = fields.Char(required=True)
# Needed for avoiding errors on several inherited behaviors
partner_id = fields.Many2one(
comodel_name="res.partner", string="Partner", index=True
)
pricelist_id = fields.Many2one(comodel_name="product.pricelist", string="Pricelist")
contract_type = fields.Selection(
selection=[("sale", "Customer"), ("purchase", "Supplier")],
default="sale",
index=True,
)
journal_id = fields.Many2one(
comodel_name="account.journal",
string="Journal",
domain="[('type', '=', contract_type)," "('company_id', '=', company_id)]",
compute="_compute_journal_id",
store=True,
readonly=False,
index=True,
)
company_id = fields.Many2one(
"res.company",
string="Company",
required=True,
default=lambda self: self.env.company.id,
)
line_recurrence = fields.Boolean(
string="Recurrence at line level?",
help="Mark this check if you want to control recurrrence at line level instead"
" of all together for the whole contract.",
)
@api.onchange("contract_type")
def _onchange_contract_type(self):
if self.contract_type == "purchase":
self.contract_line_ids.filtered("automatic_price").update(
{"automatic_price": False}
)
@api.depends("contract_type", "company_id")
def _compute_journal_id(self):
AccountJournal = self.env["account.journal"]
for contract in self:
domain = [
("type", "=", contract.contract_type),
("company_id", "=", contract.company_id.id),
]
journal = AccountJournal.search(domain, limit=1)
if journal:
contract.journal_id = journal.id

View File

@@ -0,0 +1,260 @@
# 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 ContractAbstractContractLine(models.AbstractModel):
_inherit = "contract.recurrency.basic.mixin"
_name = "contract.abstract.contract.line"
_description = "Abstract Recurring Contract Line"
product_id = fields.Many2one("product.product", string="Product")
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")
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="Account",
string="Sub Total",
)
discount = fields.Float(
string="Discount (%)",
digits="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(
compute="_compute_recurring_rule_type",
store=True,
readonly=False,
required=True,
copy=True,
)
recurring_invoicing_type = fields.Selection(
compute="_compute_recurring_invoicing_type",
store=True,
readonly=False,
required=True,
copy=True,
)
recurring_interval = fields.Integer(
compute="_compute_recurring_interval",
store=True,
readonly=False,
required=True,
copy=True,
)
date_start = fields.Date(
compute="_compute_date_start",
store=True,
readonly=False,
copy=True,
)
last_date_invoiced = fields.Date(string="Last Date Invoiced")
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(
[
("daily", "Day(s)"),
("weekly", "Week(s)"),
("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="contract.abstract.contract",
required=True,
ondelete="cascade",
)
display_type = fields.Selection(
selection=[("line_section", "Section"), ("line_note", "Note")],
default=False,
help="Technical field for UX purpose.",
)
note_invoicing_mode = fields.Selection(
selection=[
("with_previous_line", "With previous line"),
("with_next_line", "With next line"),
("custom", "Custom"),
],
default="with_previous_line",
help="Defines when the Note is invoiced:\n"
"- With previous line: If the previous line can be invoiced.\n"
"- With next line: If the next line can be invoiced.\n"
"- Custom: Depending on the recurrence to be define.",
)
is_recurring_note = fields.Boolean(compute="_compute_is_recurring_note")
company_id = fields.Many2one(related="contract_id.company_id", store=True)
def _set_recurrence_field(self, field):
"""Helper method for computed methods that gets the equivalent field
in the header.
We need to re-assign the original value for avoiding a missing error.
"""
for record in self:
if record.contract_id.line_recurrence:
record[field] = record[field]
else:
record[field] = record.contract_id[field]
@api.depends("contract_id.recurring_rule_type", "contract_id.line_recurrence")
def _compute_recurring_rule_type(self):
self._set_recurrence_field("recurring_rule_type")
@api.depends("contract_id.recurring_invoicing_type", "contract_id.line_recurrence")
def _compute_recurring_invoicing_type(self):
self._set_recurrence_field("recurring_invoicing_type")
@api.depends("contract_id.recurring_interval", "contract_id.line_recurrence")
def _compute_recurring_interval(self):
self._set_recurrence_field("recurring_interval")
@api.depends("contract_id.date_start", "contract_id.line_recurrence")
def _compute_date_start(self):
self._set_recurrence_field("date_start")
@api.depends("contract_id.recurring_next_date", "contract_id.line_recurrence")
def _compute_recurring_next_date(self):
super()._compute_recurring_next_date()
self._set_recurrence_field("recurring_next_date")
@api.depends("display_type", "note_invoicing_mode")
def _compute_is_recurring_note(self):
for record in self:
record.is_recurring_note = (
record.display_type == "line_note"
and record.note_invoicing_mode == "custom"
)
@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:
pricelist = (
line.contract_id.pricelist_id
or line.contract_id.partner_id.with_company(
line.contract_id.company_id
).property_product_pricelist
)
product = line.product_id.with_context(
quantity=line.env.context.get(
"contract_line_qty",
line.quantity,
),
pricelist=pricelist.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.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.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.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,
)
vals["name"] = self.product_id.get_product_multiline_description_sale()
vals["price_unit"] = product.price
self.update(vals)
return {"domain": domain}

View File

@@ -0,0 +1,21 @@
# Copyright 2016 Tecnativa - Carlos Dauden
# Copyright 2018 ACSONE SA/NV.
# Copyright 2020 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class AccountMove(models.Model):
_inherit = "account.move"
# We keep this field for migration purpose
old_contract_id = fields.Many2one("contract.contract")
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
contract_line_id = fields.Many2one(
"contract.line", string="Contract Line", index=True
)

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

@@ -0,0 +1,648 @@
# Copyright 2004-2010 OpenERP SA
# Copyright 2014 Angel Moya <angel.moya@domatix.com>
# Copyright 2015-2020 Tecnativa - Pedro M. Baeza
# Copyright 2016-2018 Tecnativa - Carlos Dauden
# Copyright 2016-2017 LasLabs Inc.
# Copyright 2018 ACSONE SA/NV
# Copyright 2021 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tests import Form
from odoo.tools.translate import _
class ContractContract(models.Model):
_name = "contract.contract"
_description = "Contract"
_order = "code, name asc"
_inherit = [
"mail.thread",
"mail.activity.mixin",
"contract.abstract.contract",
"contract.recurrency.mixin",
"portal.mixin",
]
active = fields.Boolean(
default=True,
)
code = fields.Char(
string="Reference",
)
group_id = fields.Many2one(
string="Group",
comodel_name="account.analytic.account",
ondelete="restrict",
)
currency_id = fields.Many2one(
compute="_compute_currency_id",
inverse="_inverse_currency_id",
comodel_name="res.currency",
string="Currency",
)
manual_currency_id = fields.Many2one(
comodel_name="res.currency",
readonly=True,
)
contract_template_id = fields.Many2one(
string="Contract Template", comodel_name="contract.template"
)
contract_line_ids = fields.One2many(
string="Contract lines",
comodel_name="contract.line",
inverse_name="contract_id",
copy=True,
)
# Trick for being able to have 2 different views for the same o2m
# We need this as one2many widget doesn't allow to define in the view
# the same field 2 times with different views. 2 views are needed because
# one of them must be editable inline and the other not, which can't be
# parametrized through attrs.
contract_line_fixed_ids = fields.One2many(
string="Contract lines (fixed)",
comodel_name="contract.line",
inverse_name="contract_id",
)
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"
)
date_end = fields.Date(compute="_compute_date_end", store=True, readonly=False)
payment_term_id = fields.Many2one(
comodel_name="account.payment.term", string="Payment Terms", index=True
)
invoice_count = fields.Integer(compute="_compute_invoice_count")
fiscal_position_id = fields.Many2one(
comodel_name="account.fiscal.position",
string="Fiscal Position",
ondelete="restrict",
)
invoice_partner_id = fields.Many2one(
string="Invoicing contact",
comodel_name="res.partner",
ondelete="restrict",
)
partner_id = fields.Many2one(
comodel_name="res.partner", inverse="_inverse_partner_id", required=True
)
commercial_partner_id = fields.Many2one(
"res.partner",
compute_sudo=True,
related="partner_id.commercial_partner_id",
store=True,
string="Commercial Entity",
index=True,
)
tag_ids = fields.Many2many(comodel_name="contract.tag", string="Tags")
note = fields.Text(string="Notes")
is_terminated = fields.Boolean(string="Terminated", readonly=True, copy=False)
terminate_reason_id = fields.Many2one(
comodel_name="contract.terminate.reason",
string="Termination Reason",
ondelete="restrict",
readonly=True,
copy=False,
tracking=True,
)
terminate_comment = fields.Text(
string="Termination Comment",
readonly=True,
copy=False,
tracking=True,
)
terminate_date = fields.Date(
string="Termination Date",
readonly=True,
copy=False,
tracking=True,
)
modification_ids = fields.One2many(
comodel_name="contract.modification",
inverse_name="contract_id",
string="Modifications",
)
def get_formview_id(self, access_uid=None):
if self.contract_type == "sale":
return self.env.ref("contract.contract_contract_customer_form_view").id
else:
return self.env.ref("contract.contract_contract_supplier_form_view").id
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
records._set_start_contract_modification()
return records
def write(self, vals):
if "modification_ids" in vals:
res = super(
ContractContract, self.with_context(bypass_modification_send=True)
).write(vals)
self._modification_mail_send()
else:
res = super(ContractContract, self).write(vals)
return res
@api.model
def _set_start_contract_modification(self):
subtype_id = self.env.ref("contract.mail_message_subtype_contract_modification")
for record in self:
if record.contract_line_ids:
date_start = min(record.contract_line_ids.mapped("date_start"))
else:
date_start = record.create_date
record.message_subscribe(
partner_ids=[record.partner_id.id], subtype_ids=[subtype_id.id]
)
record.with_context(skip_modification_mail=True).write(
{
"modification_ids": [
(0, 0, {"date": date_start, "description": _("Contract start")})
]
}
)
@api.model
def _modification_mail_send(self):
for record in self:
modification_ids_not_sent = record.modification_ids.filtered(
lambda x: not x.sent
)
if modification_ids_not_sent:
if not self.env.context.get("skip_modification_mail"):
record.with_context(
default_subtype_id=self.env.ref(
"contract.mail_message_subtype_contract_modification"
).id,
).message_post_with_template(
self.env.ref("contract.mail_template_contract_modification").id,
email_layout_xmlid="contract.template_contract_modification",
)
modification_ids_not_sent.write({"sent": True})
def _compute_access_url(self):
for record in self:
record.access_url = "/my/contracts/{}".format(record.id)
def action_preview(self):
"""Invoked when 'Preview' button in contract form view is clicked."""
self.ensure_one()
return {
"type": "ir.actions.act_url",
"target": "self",
"url": self.get_portal_url(),
}
def _inverse_partner_id(self):
for rec in self:
if not rec.invoice_partner_id:
rec.invoice_partner_id = rec.partner_id.address_get(["invoice"])[
"invoice"
]
def _get_related_invoices(self):
self.ensure_one()
invoices = (
self.env["account.move.line"]
.search(
[
(
"contract_line_id",
"in",
self.contract_line_ids.ids,
)
]
)
.mapped("move_id")
)
# we are forced to always search for this for not losing possible <=v11
# generated invoices
invoices |= self.env["account.move"].search([("old_contract_id", "=", self.id)])
return invoices
def _get_computed_currency(self):
"""Helper method for returning the theoretical computed currency."""
self.ensure_one()
currency = self.env["res.currency"]
if any(self.contract_line_ids.mapped("automatic_price")):
# Use pricelist currency
currency = (
self.pricelist_id.currency_id
or self.partner_id.with_company(
self.company_id
).property_product_pricelist.currency_id
)
return currency or self.journal_id.currency_id or self.company_id.currency_id
@api.depends(
"manual_currency_id",
"pricelist_id",
"partner_id",
"journal_id",
"company_id",
)
def _compute_currency_id(self):
for rec in self:
if rec.manual_currency_id:
rec.currency_id = rec.manual_currency_id
else:
rec.currency_id = rec._get_computed_currency()
def _inverse_currency_id(self):
"""If the currency is different from the computed one, then save it
in the manual field.
"""
for rec in self:
if rec._get_computed_currency() != rec.currency_id:
rec.manual_currency_id = rec.currency_id
else:
rec.manual_currency_id = False
def _compute_invoice_count(self):
for rec in self:
rec.invoice_count = len(rec._get_related_invoices())
def action_show_invoices(self):
self.ensure_one()
tree_view = self.env.ref("account.view_invoice_tree", raise_if_not_found=False)
form_view = self.env.ref("account.view_move_form", raise_if_not_found=False)
action = {
"type": "ir.actions.act_window",
"name": "Invoices",
"res_model": "account.move",
"view_mode": "tree,kanban,form,calendar,pivot,graph,activity",
"domain": [("id", "in", self._get_related_invoices().ids)],
}
if tree_view and form_view:
action["views"] = [(tree_view.id, "tree"), (form_view.id, "form")]
return action
@api.depends("contract_line_ids.date_end")
def _compute_date_end(self):
for contract in self:
contract.date_end = False
date_end = contract.contract_line_ids.mapped("date_end")
if date_end and all(date_end):
contract.date_end = max(date_end)
@api.depends(
"contract_line_ids.recurring_next_date",
"contract_line_ids.is_canceled",
)
def _compute_recurring_next_date(self):
for contract in self:
recurring_next_date = contract.contract_line_ids.filtered(
lambda l: (
l.recurring_next_date
and not l.is_canceled
and (not l.display_type or l.is_recurring_note)
)
).mapped("recurring_next_date")
# we give priority to computation from date_start if modified
if (
contract._origin
and contract._origin.date_start != contract.date_start
or not recurring_next_date
):
super(ContractContract, contract)._compute_recurring_next_date()
else:
contract.recurring_next_date = min(recurring_next_date)
@api.depends("contract_line_ids.create_invoice_visibility")
def _compute_create_invoice_visibility(self):
for contract in self:
contract.create_invoice_visibility = any(
contract.contract_line_ids.mapped("create_invoice_visibility")
)
@api.onchange("contract_template_id")
def _onchange_contract_template_id(self):
"""Update the contract fields with that of the template.
Take special consideration with the `contract_line_ids`,
which must be created using the data from the contract lines. Cascade
deletion ensures that any errant lines that are created are also
deleted.
"""
contract_template_id = self.contract_template_id
if not contract_template_id:
return
for field_name, field in contract_template_id._fields.items():
if field.name == "contract_line_ids":
lines = self._convert_contract_lines(contract_template_id)
self.contract_line_ids += lines
elif not any(
(
field.compute,
field.related,
field.automatic,
field.readonly,
field.company_dependent,
field.name in self.NO_SYNC,
)
):
if self.contract_template_id[field_name]:
self[field_name] = self.contract_template_id[field_name]
@api.onchange("partner_id", "company_id")
def _onchange_partner_id(self):
partner = (
self.partner_id
if not self.company_id
else self.partner_id.with_company(self.company_id)
)
self.pricelist_id = partner.property_product_pricelist.id
self.fiscal_position_id = partner.env[
"account.fiscal.position"
].get_fiscal_position(partner.id)
if self.contract_type == "purchase":
self.payment_term_id = partner.property_supplier_payment_term_id
else:
self.payment_term_id = partner.property_payment_term_id
self.invoice_partner_id = self.partner_id.address_get(["invoice"])["invoice"]
return {
"domain": {
"invoice_partner_id": [
"|",
("id", "parent_of", self.partner_id.id),
("id", "child_of", self.partner_id.id),
]
}
}
def _convert_contract_lines(self, contract):
self.ensure_one()
new_lines = self.env["contract.line"]
contract_line_model = self.env["contract.line"]
for contract_line in contract.contract_line_ids:
vals = contract_line._convert_to_write(contract_line.read()[0])
# Remove template link field
vals.pop("contract_template_id", False)
vals["date_start"] = fields.Date.context_today(contract_line)
vals["recurring_next_date"] = fields.Date.context_today(contract_line)
new_lines += contract_line_model.new(vals)
new_lines._onchange_is_auto_renew()
return new_lines
def _prepare_invoice(self, date_invoice, journal=None):
"""Prepare in a Form the values for the generated invoice record.
:return: A tuple with the vals dictionary and the Form with the
preloaded values for being used in lines.
"""
self.ensure_one()
if not journal:
journal = (
self.journal_id
if self.journal_id.type == self.contract_type
else self.env["account.journal"].search(
[
("type", "=", self.contract_type),
("company_id", "=", self.company_id.id),
],
limit=1,
)
)
if not journal:
raise ValidationError(
_("Please define a %s journal for the company '%s'.")
% (self.contract_type, self.company_id.name or "")
)
invoice_type = "out_invoice"
if self.contract_type == "purchase":
invoice_type = "in_invoice"
move_form = Form(
self.env["account.move"]
.with_company(self.company_id)
.with_context(default_move_type=invoice_type)
)
move_form.partner_id = self.invoice_partner_id
if self.payment_term_id:
move_form.invoice_payment_term_id = self.payment_term_id
if self.fiscal_position_id:
move_form.fiscal_position_id = self.fiscal_position_id
invoice_vals = move_form._values_to_save(all_fields=True)
invoice_vals.update(
{
"ref": self.code,
"company_id": self.company_id.id,
"currency_id": self.currency_id.id,
"invoice_date": date_invoice,
"journal_id": journal.id,
"invoice_origin": self.name,
"user_id": self.user_id.id,
}
)
return invoice_vals, move_form
def action_contract_send(self):
self.ensure_one()
template = self.env.ref("contract.email_contract_template", False)
compose_form = self.env.ref("mail.email_compose_message_wizard_form")
ctx = dict(
default_model="contract.contract",
default_res_id=self.id,
default_use_template=bool(template),
default_template_id=template and template.id or False,
default_composition_mode="comment",
)
return {
"name": _("Compose Email"),
"type": "ir.actions.act_window",
"view_mode": "form",
"res_model": "mail.compose.message",
"views": [(compose_form.id, "form")],
"view_id": compose_form.id,
"target": "new",
"context": ctx,
}
@api.model
def _get_contracts_to_invoice_domain(self, date_ref=None):
"""
This method builds the domain to use to find all
contracts (contract.contract) to invoice.
:param date_ref: optional reference date to use instead of today
:return: list (domain) usable on contract.contract
"""
domain = []
if not date_ref:
date_ref = fields.Date.context_today(self)
domain.extend([("recurring_next_date", "<=", date_ref)])
return domain
def _get_lines_to_invoice(self, date_ref):
"""
This method fetches and returns the lines to invoice on the contract
(self), based on the given date.
:param date_ref: date used as reference date to find lines to invoice
:return: contract lines (contract.line recordset)
"""
self.ensure_one()
def can_be_invoiced(contract_line):
return (
not contract_line.is_canceled
and contract_line.recurring_next_date
and contract_line.recurring_next_date <= date_ref
)
lines2invoice = previous = self.env["contract.line"]
current_section = current_note = False
for line in self.contract_line_ids:
if line.display_type == "line_section":
current_section = line
elif line.display_type == "line_note" and not line.is_recurring_note:
if line.note_invoicing_mode == "with_previous_line":
if previous in lines2invoice:
lines2invoice |= line
current_note = False
elif line.note_invoicing_mode == "with_next_line":
current_note = line
elif line.is_recurring_note or not line.display_type:
if can_be_invoiced(line):
if current_section:
lines2invoice |= current_section
current_section = False
if current_note:
lines2invoice |= current_note
lines2invoice |= line
current_note = False
previous = line
return lines2invoice.sorted()
def _prepare_recurring_invoices_values(self, date_ref=False):
"""
This method builds the list of invoices values to create, based on
the lines to invoice of the contracts in self.
!!! The date of next invoice (recurring_next_date) is updated here !!!
:return: list of dictionaries (invoices values)
"""
invoices_values = []
for contract in self:
if not date_ref:
date_ref = contract.recurring_next_date
if not date_ref:
# this use case is possible when recurring_create_invoice is
# called for a finished contract
continue
contract_lines = contract._get_lines_to_invoice(date_ref)
if not contract_lines:
continue
invoice_vals, move_form = contract._prepare_invoice(date_ref)
invoice_vals["invoice_line_ids"] = []
for line in contract_lines:
invoice_line_vals = line._prepare_invoice_line(move_form=move_form)
if invoice_line_vals:
# Allow extension modules to return an empty dictionary for
# nullifying line
invoice_vals["invoice_line_ids"].append((0, 0, invoice_line_vals))
invoices_values.append(invoice_vals)
# Force the recomputation of journal items
del invoice_vals["line_ids"]
contract_lines._update_recurring_next_date()
return invoices_values
def recurring_create_invoice(self):
"""
This method triggers the creation of the next invoices of the contracts
even if their next invoicing date is in the future.
"""
invoice = self._recurring_create_invoice()
if invoice:
self.message_post(
body=_(
"Contract manually invoiced: "
'<a href="#" data-oe-model="%s" data-oe-id="%s">Invoice'
"</a>"
)
% (invoice._name, invoice.id)
)
return invoice
@api.model
def _invoice_followers(self, invoices):
invoice_create_subtype = self.env.ref(
"contract.mail_message_subtype_invoice_created"
)
for item in self:
partner_ids = item.message_follower_ids.filtered(
lambda x: invoice_create_subtype in x.subtype_ids
).mapped("partner_id")
if partner_ids:
(invoices & item._get_related_invoices()).message_subscribe(
partner_ids=partner_ids.ids
)
def _recurring_create_invoice(self, date_ref=False):
invoices_values = self._prepare_recurring_invoices_values(date_ref)
moves = self.env["account.move"].create(invoices_values)
self._invoice_followers(moves)
self._compute_recurring_next_date()
return moves
@api.model
def cron_recurring_create_invoice(self, date_ref=None):
if not date_ref:
date_ref = fields.Date.context_today(self)
domain = self._get_contracts_to_invoice_domain(date_ref)
invoices = self.env["account.move"]
# Invoice by companies, so assignation emails get correct context
companies_to_invoice = self.read_group(domain, ["company_id"], ["company_id"])
for row in companies_to_invoice:
contracts_to_invoice = self.search(row["__domain"]).with_context(
allowed_company_ids=[row["company_id"][0]]
)
invoices |= contracts_to_invoice._recurring_create_invoice(date_ref)
return invoices
def action_terminate_contract(self):
self.ensure_one()
context = {"default_contract_id": self.id}
return {
"type": "ir.actions.act_window",
"name": _("Terminate Contract"),
"res_model": "contract.contract.terminate",
"view_mode": "form",
"target": "new",
"context": context,
}
def _terminate_contract(
self, terminate_reason_id, terminate_comment, terminate_date
):
self.ensure_one()
if not self.env.user.has_group("contract.can_terminate_contract"):
raise UserError(_("You are not allowed to terminate contracts."))
self.contract_line_ids.filtered("is_stop_allowed").stop(terminate_date)
self.write(
{
"is_terminated": True,
"terminate_reason_id": terminate_reason_id.id,
"terminate_comment": terminate_comment,
"terminate_date": terminate_date,
}
)
return True
def action_cancel_contract_termination(self):
self.ensure_one()
self.write(
{
"is_terminated": False,
"terminate_reason_id": False,
"terminate_comment": False,
"terminate_date": False,
}
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,429 @@
# 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=True,
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=True,
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,41 @@
# Copyright 2020 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class ContractModification(models.Model):
_name = "contract.modification"
_description = "Contract Modification"
_order = "date desc"
date = fields.Date(required=True, string="Date")
description = fields.Text(required=True, string="Description")
contract_id = fields.Many2one(
string="Contract",
comodel_name="contract.contract",
required=True,
ondelete="cascade",
index=True,
)
sent = fields.Boolean(
string="Sent",
default=False,
)
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
if not self.env.context.get("bypass_modification_send"):
records.check_modification_ids_need_sent()
return records
def write(self, vals):
res = super().write(vals)
if not self.env.context.get("bypass_modification_send"):
self.check_modification_ids_need_sent()
return res
def check_modification_ids_need_sent(self):
self.mapped("contract_id")._modification_mail_send()

View File

@@ -0,0 +1,237 @@
# Copyright 2018 ACSONE SA/NV.
# Copyright 2020 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
class ContractRecurrencyBasicMixin(models.AbstractModel):
_name = "contract.recurrency.basic.mixin"
_description = "Basic recurrency mixin for abstract contract models"
recurring_rule_type = fields.Selection(
[
("daily", "Day(s)"),
("weekly", "Week(s)"),
("monthly", "Month(s)"),
("monthlylastday", "Month(s) last day"),
("quarterly", "Quarter(s)"),
("semesterly", "Semester(s)"),
("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 the invoice must be generated at the beginning "
"(pre-paid) or end (post-paid) of the period."
),
)
recurring_invoicing_offset = fields.Integer(
compute="_compute_recurring_invoicing_offset",
string="Invoicing offset",
help=(
"Number of days to offset the invoice from the period end "
"date (in post-paid mode) or start date (in pre-paid mode)."
),
)
recurring_interval = fields.Integer(
default=1,
string="Invoice Every",
help="Invoice every (Days/Week/Month/Year)",
)
date_start = fields.Date(string="Date Start")
recurring_next_date = fields.Date(string="Date of Next Invoice")
@api.depends("recurring_invoicing_type", "recurring_rule_type")
def _compute_recurring_invoicing_offset(self):
for rec in self:
method = self._get_default_recurring_invoicing_offset
rec.recurring_invoicing_offset = method(
rec.recurring_invoicing_type, rec.recurring_rule_type
)
@api.model
def _get_default_recurring_invoicing_offset(
self, recurring_invoicing_type, recurring_rule_type
):
if (
recurring_invoicing_type == "pre-paid"
or recurring_rule_type == "monthlylastday"
):
return 0
else:
return 1
class ContractRecurrencyMixin(models.AbstractModel):
_inherit = "contract.recurrency.basic.mixin"
_name = "contract.recurrency.mixin"
_description = "Recurrency mixin for contract models"
date_start = fields.Date(default=lambda self: fields.Date.context_today(self))
recurring_next_date = fields.Date(
compute="_compute_recurring_next_date", store=True, readonly=False, copy=True
)
date_end = fields.Date(string="Date End", index=True)
next_period_date_start = fields.Date(
string="Next Period Start",
compute="_compute_next_period_date_start",
)
next_period_date_end = fields.Date(
string="Next Period End",
compute="_compute_next_period_date_end",
)
last_date_invoiced = fields.Date(
string="Last Date Invoiced", readonly=True, copy=False
)
@api.depends("next_period_date_start")
def _compute_recurring_next_date(self):
for rec in self.filtered("next_period_date_start"):
rec.recurring_next_date = self.get_next_invoice_date(
rec.next_period_date_start,
rec.recurring_invoicing_type,
rec.recurring_invoicing_offset,
rec.recurring_rule_type,
rec.recurring_interval,
max_date_end=rec.date_end,
)
@api.depends("last_date_invoiced", "date_start", "date_end")
def _compute_next_period_date_start(self):
for rec in self:
if rec.last_date_invoiced:
next_period_date_start = rec.last_date_invoiced + relativedelta(days=1)
else:
next_period_date_start = rec.date_start
if rec.date_end and next_period_date_start > rec.date_end:
next_period_date_start = False
rec.next_period_date_start = next_period_date_start
@api.depends(
"next_period_date_start",
"recurring_invoicing_type",
"recurring_invoicing_offset",
"recurring_rule_type",
"recurring_interval",
"date_end",
"recurring_next_date",
)
def _compute_next_period_date_end(self):
for rec in self:
rec.next_period_date_end = self.get_next_period_date_end(
rec.next_period_date_start,
rec.recurring_rule_type,
rec.recurring_interval,
max_date_end=rec.date_end,
next_invoice_date=rec.recurring_next_date,
recurring_invoicing_type=rec.recurring_invoicing_type,
recurring_invoicing_offset=rec.recurring_invoicing_offset,
)
@api.model
def get_relative_delta(self, recurring_rule_type, interval):
"""Return a relativedelta for one period.
When added to the first day of the period,
it gives the first day of the next period.
"""
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=1)
elif recurring_rule_type == "quarterly":
return relativedelta(months=3 * interval)
elif recurring_rule_type == "semesterly":
return relativedelta(months=6 * interval)
else:
return relativedelta(years=interval)
@api.model
def get_next_period_date_end(
self,
next_period_date_start,
recurring_rule_type,
recurring_interval,
max_date_end,
next_invoice_date=False,
recurring_invoicing_type=False,
recurring_invoicing_offset=False,
):
"""Compute the end date for the next period.
The next period normally depends on recurrence options only.
It is however possible to provide it a next invoice date, in
which case this method can adjust the next period based on that
too. In that scenario it required the invoicing type and offset
arguments.
"""
if not next_period_date_start:
return False
if max_date_end and next_period_date_start > max_date_end:
# start is past max date end: there is no next period
return False
if not next_invoice_date:
# regular algorithm
next_period_date_end = (
next_period_date_start
+ self.get_relative_delta(recurring_rule_type, recurring_interval)
- relativedelta(days=1)
)
else:
# special algorithm when the next invoice date is forced
if recurring_invoicing_type == "pre-paid":
next_period_date_end = (
next_invoice_date
- relativedelta(days=recurring_invoicing_offset)
+ self.get_relative_delta(recurring_rule_type, recurring_interval)
- relativedelta(days=1)
)
else: # post-paid
next_period_date_end = next_invoice_date - relativedelta(
days=recurring_invoicing_offset
)
if max_date_end and next_period_date_end > max_date_end:
# end date is past max_date_end: trim it
next_period_date_end = max_date_end
return next_period_date_end
@api.model
def get_next_invoice_date(
self,
next_period_date_start,
recurring_invoicing_type,
recurring_invoicing_offset,
recurring_rule_type,
recurring_interval,
max_date_end,
):
next_period_date_end = self.get_next_period_date_end(
next_period_date_start,
recurring_rule_type,
recurring_interval,
max_date_end=max_date_end,
)
if not next_period_date_end:
return False
if recurring_invoicing_type == "pre-paid":
recurring_next_date = next_period_date_start + relativedelta(
days=recurring_invoicing_offset
)
else: # post-paid
recurring_next_date = next_period_date_end + relativedelta(
days=recurring_invoicing_offset
)
return recurring_next_date

View File

@@ -0,0 +1,17 @@
# Copyright 2019 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ContractTag(models.Model):
_name = "contract.tag"
_description = "Contract Tag"
name = fields.Char(required=True)
company_id = fields.Many2one(
"res.company",
string="Company",
default=lambda self: self.env.company.id,
)

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 ContractTemplate(models.Model):
_name = "contract.template"
_inherit = "contract.abstract.contract"
_description = "Contract Template"
contract_line_ids = fields.One2many(
comodel_name="contract.template.line",
inverse_name="contract_id",
copy=True,
string="Contract template lines",
)

View File

@@ -0,0 +1,23 @@
# 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 ContractTemplateLine(models.Model):
_name = "contract.template.line"
_inherit = "contract.abstract.contract.line"
_description = "Contract Template Line"
_order = "sequence,id"
contract_id = fields.Many2one(
string="Contract",
comodel_name="contract.template",
required=True,
ondelete="cascade",
)

View File

@@ -0,0 +1,15 @@
# Copyright 2020 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ContractTerminateReason(models.Model):
_name = "contract.terminate.reason"
_description = "Contract Termination Reason"
name = fields.Char(required=True)
terminate_comment_required = fields.Boolean(
string="Require a termination comment", default=True
)

View File

@@ -0,0 +1,17 @@
# Copyright 2019 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ResCompany(models.Model):
_inherit = "res.company"
create_new_line_at_contract_line_renew = fields.Boolean(
string="Create New Line At Contract Line Renew",
help="If checked, a new line will be generated at contract line renew "
"and linked to the original one as successor. The default "
"behavior is to extend the end date of the contract by a new "
"subscription period",
)

View File

@@ -0,0 +1,19 @@
# Copyright 2019 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
create_new_line_at_contract_line_renew = fields.Boolean(
related="company_id.create_new_line_at_contract_line_renew",
readonly=False,
string="Create New Line At Contract Line Renew",
help="If checked, a new line will be generated at contract line renew "
"and linked to the original one as successor. The default "
"behavior is to extend the end date of the contract by a new "
"subscription period",
)

View File

@@ -0,0 +1,80 @@
# Copyright 2017 Carlos Dauden <carlos.dauden@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from ast import literal_eval
from odoo import fields, models
class ResPartner(models.Model):
_inherit = "res.partner"
sale_contract_count = fields.Integer(
string="Sale Contracts",
compute="_compute_contract_count",
)
purchase_contract_count = fields.Integer(
string="Purchase Contracts",
compute="_compute_contract_count",
)
contract_ids = fields.One2many(
comodel_name="contract.contract",
inverse_name="partner_id",
string="Contracts",
)
def _get_partner_contract_domain(self):
self.ensure_one()
return [("partner_id", "child_of", self.ids)]
def _compute_contract_count(self):
contract_model = self.env["contract.contract"]
fetch_data = contract_model.read_group(
self._get_partner_contract_domain(),
["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"
]
)
def act_show_contract(self):
"""This opens contract view
@return: the contract view
"""
self.ensure_one()
contract_type = self._context.get("contract_type")
res = self._get_act_window_contract_xml(contract_type)
action_context = {k: v for k, v in self.env.context.items() if k != "group_by"}
action_context["default_partner_id"] = self.id
action_context["default_pricelist_id"] = self.property_product_pricelist.id
res["context"] = action_context
res["domain"] = (
literal_eval(res["domain"]) + self._get_partner_contract_domain()
)
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_supplier_contract"
)
else:
return self.env["ir.actions.act_window"]._for_xml_id(
"contract.action_customer_contract"
)

View File

@@ -0,0 +1,4 @@
To view discount field in contract line, you need to set *Discount on lines* in
user access rights.
Contracts can be viewed on the portal (list and detail) if the user logged into the portal is a follower of the contract.

View File

@@ -0,0 +1,16 @@
* Angel Moya <angel.moya@domatix.com>
* Dave Lasley <dave@laslabs.com>
* Miquel Raïch <miquel.raich@eficent.com>
* Souheil Bejaoui <souheil.bejaoui@acsone.eu>
* Thomas Binsfeld <thomas.binsfeld@acsone.eu>
* Guillaume Vandamme <guillaume.vandamme@acsone.eu>
* Raphaël Reverdy <raphael.reverdy@akretion.com>
* `Tecnativa <https://www.tecnativa.com>`_:
* Pedro M. Baeza
* Carlos Dauden
* Vicent Cubells
* Rafael Blasco
* Víctor Martínez
* Iván Antón <ozono@ozonomultimedia.com>

View File

@@ -0,0 +1,6 @@
This module enables contracts management with recurring
invoicing functions. Also you can print and send by email contract report.
It works for customer contract and supplier contracts.
Contracts are shown in portal.

View File

@@ -0,0 +1,2 @@
* Recover states and others functional fields in Contracts.
* Add recurrence flag at template level.

31
contract/readme/USAGE.rst Normal file
View File

@@ -0,0 +1,31 @@
#. Contracts are in Invoicing -> Customers -> Customer and Invoicing -> Vendors -> Supplier Contracts
#. When creating a contract, fill fields for selecting the invoicing parameters:
* a journal
* a price list (optional)
#. And add the lines to be invoiced with:
* the product with a description, a quantity and a price
* the recurrence parameters: interval (days, weeks, months, months last day or years),
start date, date of next invoice (automatically computed, can be modified) and end date (optional)
* auto-price, for having a price automatically obtained from the price list
* #START# or #END# in the description field to display the start/end date of
the invoiced period in the invoice line description
* pre-paid (invoice at period start) or post-paid (invoice at start of next period)
#. The "Generate Recurring Invoices from Contracts" cron runs daily to generate the invoices.
If you are in debug mode, you can click on the invoice creation button.
#. The *Show recurring invoices* shortcut on contracts shows all invoices created from the
contract.
#. The contract report can be printed from the Print menu
#. The contract can be sent by email with the *Send by Email* button
#. Contract templates can be created from the Configuration -> Contracts -> Contract Templates menu.
They allow to define default journal, price list and lines when creating a contract.
To use it, just select the template on the contract and fields will be filled automatically.
* Contracts appear in portal to following users in every contract:
.. image:: ../static/src/screenshots/portal-my.png
.. image:: ../static/src/screenshots/portal-list.png
.. image:: ../static/src/screenshots/portal-detail.png

Some files were not shown because too many files have changed in this diff Show More