mirror of
https://github.com/OCA/contract.git
synced 2025-02-13 17:57:24 +02:00
128
contract/README.rst
Normal file
128
contract/README.rst
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
================================
|
||||||
|
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/13.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-13-0/contract-13-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/13.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.
|
||||||
|
|
||||||
|
**Table of contents**
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:local:
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
To view discount field in contract line, you need to set *Discount on lines* in
|
||||||
|
user access rights.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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:%2013.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
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
* Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
||||||
|
* Carlos Dauden <carlos.dauden@tecnativa.com>
|
||||||
|
* Angel Moya <angel.moya@domatix.com>
|
||||||
|
* Dave Lasley <dave@laslabs.com>
|
||||||
|
* Vicent Cubells <vicent.cubells@tecnativa.com>
|
||||||
|
* Miquel Raïch <miquel.raich@eficent.com>
|
||||||
|
* Souheil Bejaoui <souheil.bejaoui@acsone.eu>
|
||||||
|
* Thomas Binsfeld <thomas.binsfeld@acsone.eu>
|
||||||
|
* Rafael Blasco <rafael.blasco@tecnativa.com>
|
||||||
|
* Guillaume Vandamme <guillaume.vandamme@acsone.eu>
|
||||||
|
* Raphaël Reverdy <raphael.reverdy@akretion.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/13.0/contract>`_ project on GitHub.
|
||||||
|
|
||||||
|
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||||
2
contract/__init__.py
Normal file
2
contract/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from . import models
|
||||||
|
from . import wizards
|
||||||
47
contract/__manifest__.py
Normal file
47
contract/__manifest__.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# 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 Tecnativa - Pedro M. Baeza
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Recurring - Contracts Management",
|
||||||
|
"version": "13.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"],
|
||||||
|
"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/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",
|
||||||
|
],
|
||||||
|
"installable": True,
|
||||||
|
}
|
||||||
14
contract/data/contract_cron.xml
Normal file
14
contract/data/contract_cron.xml
Normal 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>
|
||||||
14
contract/data/contract_renew_cron.xml
Normal file
14
contract/data/contract_renew_cron.xml
Normal 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>
|
||||||
9
contract/data/ir_ui_menu.xml
Normal file
9
contract/data/ir_ui_menu.xml
Normal 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>
|
||||||
68
contract/data/mail_template.xml
Normal file
68
contract/data/mail_template.xml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?xml version="1.0" ?>
|
||||||
|
<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 <%s>' % (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;">
|
||||||
|
<strong>REFERENCES</strong><br />
|
||||||
|
Contract: <strong>${object.name}</strong><br />
|
||||||
|
% if object.date_start:
|
||||||
|
Contract Date Start: ${object.date_start or ''}<br />
|
||||||
|
% endif
|
||||||
|
|
||||||
|
% if object.user_id:
|
||||||
|
% if object.user_id.email:
|
||||||
|
Your Contact: <a href="mailto:${object.user_id.email or ''}?subject=Contract%20${object.name}">${object.user_id.name}</a>
|
||||||
|
% else:
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
]]></field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
1829
contract/i18n/am.po
Normal file
1829
contract/i18n/am.po
Normal file
File diff suppressed because it is too large
Load Diff
1830
contract/i18n/ar.po
Normal file
1830
contract/i18n/ar.po
Normal file
File diff suppressed because it is too large
Load Diff
1831
contract/i18n/bg.po
Normal file
1831
contract/i18n/bg.po
Normal file
File diff suppressed because it is too large
Load Diff
1828
contract/i18n/bs.po
Normal file
1828
contract/i18n/bs.po
Normal file
File diff suppressed because it is too large
Load Diff
1946
contract/i18n/ca.po
Normal file
1946
contract/i18n/ca.po
Normal file
File diff suppressed because it is too large
Load Diff
1773
contract/i18n/contract.pot
Normal file
1773
contract/i18n/contract.pot
Normal file
File diff suppressed because it is too large
Load Diff
1829
contract/i18n/cs.po
Normal file
1829
contract/i18n/cs.po
Normal file
File diff suppressed because it is too large
Load Diff
1829
contract/i18n/da.po
Normal file
1829
contract/i18n/da.po
Normal file
File diff suppressed because it is too large
Load Diff
2033
contract/i18n/de.po
Normal file
2033
contract/i18n/de.po
Normal file
File diff suppressed because it is too large
Load Diff
1836
contract/i18n/el_GR.po
Normal file
1836
contract/i18n/el_GR.po
Normal file
File diff suppressed because it is too large
Load Diff
1830
contract/i18n/en_GB.po
Normal file
1830
contract/i18n/en_GB.po
Normal file
File diff suppressed because it is too large
Load Diff
2023
contract/i18n/es.po
Normal file
2023
contract/i18n/es.po
Normal file
File diff suppressed because it is too large
Load Diff
1830
contract/i18n/es_AR.po
Normal file
1830
contract/i18n/es_AR.po
Normal file
File diff suppressed because it is too large
Load Diff
1886
contract/i18n/es_CL.po
Normal file
1886
contract/i18n/es_CL.po
Normal file
File diff suppressed because it is too large
Load Diff
1826
contract/i18n/es_CO.po
Normal file
1826
contract/i18n/es_CO.po
Normal file
File diff suppressed because it is too large
Load Diff
1830
contract/i18n/es_CR.po
Normal file
1830
contract/i18n/es_CR.po
Normal file
File diff suppressed because it is too large
Load Diff
1826
contract/i18n/es_DO.po
Normal file
1826
contract/i18n/es_DO.po
Normal file
File diff suppressed because it is too large
Load Diff
1826
contract/i18n/es_EC.po
Normal file
1826
contract/i18n/es_EC.po
Normal file
File diff suppressed because it is too large
Load Diff
1830
contract/i18n/es_ES.po
Normal file
1830
contract/i18n/es_ES.po
Normal file
File diff suppressed because it is too large
Load Diff
1836
contract/i18n/es_MX.po
Normal file
1836
contract/i18n/es_MX.po
Normal file
File diff suppressed because it is too large
Load Diff
1826
contract/i18n/es_PY.po
Normal file
1826
contract/i18n/es_PY.po
Normal file
File diff suppressed because it is too large
Load Diff
1830
contract/i18n/es_VE.po
Normal file
1830
contract/i18n/es_VE.po
Normal file
File diff suppressed because it is too large
Load Diff
1827
contract/i18n/et.po
Normal file
1827
contract/i18n/et.po
Normal file
File diff suppressed because it is too large
Load Diff
1825
contract/i18n/eu.po
Normal file
1825
contract/i18n/eu.po
Normal file
File diff suppressed because it is too large
Load Diff
1829
contract/i18n/fa.po
Normal file
1829
contract/i18n/fa.po
Normal file
File diff suppressed because it is too large
Load Diff
1979
contract/i18n/fi.po
Normal file
1979
contract/i18n/fi.po
Normal file
File diff suppressed because it is too large
Load Diff
2042
contract/i18n/fr.po
Normal file
2042
contract/i18n/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
1832
contract/i18n/fr_CA.po
Normal file
1832
contract/i18n/fr_CA.po
Normal file
File diff suppressed because it is too large
Load Diff
1830
contract/i18n/fr_CH.po
Normal file
1830
contract/i18n/fr_CH.po
Normal file
File diff suppressed because it is too large
Load Diff
2069
contract/i18n/gl.po
Normal file
2069
contract/i18n/gl.po
Normal file
File diff suppressed because it is too large
Load Diff
1826
contract/i18n/gl_ES.po
Normal file
1826
contract/i18n/gl_ES.po
Normal file
File diff suppressed because it is too large
Load Diff
1829
contract/i18n/he.po
Normal file
1829
contract/i18n/he.po
Normal file
File diff suppressed because it is too large
Load Diff
1890
contract/i18n/hi_IN.po
Normal file
1890
contract/i18n/hi_IN.po
Normal file
File diff suppressed because it is too large
Load Diff
1913
contract/i18n/hr.po
Normal file
1913
contract/i18n/hr.po
Normal file
File diff suppressed because it is too large
Load Diff
1936
contract/i18n/hr_HR.po
Normal file
1936
contract/i18n/hr_HR.po
Normal file
File diff suppressed because it is too large
Load Diff
1835
contract/i18n/hu.po
Normal file
1835
contract/i18n/hu.po
Normal file
File diff suppressed because it is too large
Load Diff
1829
contract/i18n/id.po
Normal file
1829
contract/i18n/id.po
Normal file
File diff suppressed because it is too large
Load Diff
2043
contract/i18n/it.po
Normal file
2043
contract/i18n/it.po
Normal file
File diff suppressed because it is too large
Load Diff
1829
contract/i18n/ja.po
Normal file
1829
contract/i18n/ja.po
Normal file
File diff suppressed because it is too large
Load Diff
1829
contract/i18n/ko.po
Normal file
1829
contract/i18n/ko.po
Normal file
File diff suppressed because it is too large
Load Diff
1830
contract/i18n/lt.po
Normal file
1830
contract/i18n/lt.po
Normal file
File diff suppressed because it is too large
Load Diff
1831
contract/i18n/lt_LT.po
Normal file
1831
contract/i18n/lt_LT.po
Normal file
File diff suppressed because it is too large
Load Diff
1830
contract/i18n/lv.po
Normal file
1830
contract/i18n/lv.po
Normal file
File diff suppressed because it is too large
Load Diff
1829
contract/i18n/mk.po
Normal file
1829
contract/i18n/mk.po
Normal file
File diff suppressed because it is too large
Load Diff
1829
contract/i18n/mn.po
Normal file
1829
contract/i18n/mn.po
Normal file
File diff suppressed because it is too large
Load Diff
1830
contract/i18n/nb.po
Normal file
1830
contract/i18n/nb.po
Normal file
File diff suppressed because it is too large
Load Diff
1832
contract/i18n/nb_NO.po
Normal file
1832
contract/i18n/nb_NO.po
Normal file
File diff suppressed because it is too large
Load Diff
2069
contract/i18n/nl.po
Normal file
2069
contract/i18n/nl.po
Normal file
File diff suppressed because it is too large
Load Diff
1830
contract/i18n/nl_BE.po
Normal file
1830
contract/i18n/nl_BE.po
Normal file
File diff suppressed because it is too large
Load Diff
1881
contract/i18n/nl_NL.po
Normal file
1881
contract/i18n/nl_NL.po
Normal file
File diff suppressed because it is too large
Load Diff
1831
contract/i18n/pl.po
Normal file
1831
contract/i18n/pl.po
Normal file
File diff suppressed because it is too large
Load Diff
2050
contract/i18n/pt.po
Normal file
2050
contract/i18n/pt.po
Normal file
File diff suppressed because it is too large
Load Diff
1978
contract/i18n/pt_BR.po
Normal file
1978
contract/i18n/pt_BR.po
Normal file
File diff suppressed because it is too large
Load Diff
1839
contract/i18n/pt_PT.po
Normal file
1839
contract/i18n/pt_PT.po
Normal file
File diff suppressed because it is too large
Load Diff
1838
contract/i18n/ro.po
Normal file
1838
contract/i18n/ro.po
Normal file
File diff suppressed because it is too large
Load Diff
1876
contract/i18n/ru.po
Normal file
1876
contract/i18n/ru.po
Normal file
File diff suppressed because it is too large
Load Diff
1829
contract/i18n/sk.po
Normal file
1829
contract/i18n/sk.po
Normal file
File diff suppressed because it is too large
Load Diff
1832
contract/i18n/sk_SK.po
Normal file
1832
contract/i18n/sk_SK.po
Normal file
File diff suppressed because it is too large
Load Diff
1827
contract/i18n/sl.po
Normal file
1827
contract/i18n/sl.po
Normal file
File diff suppressed because it is too large
Load Diff
1828
contract/i18n/sr.po
Normal file
1828
contract/i18n/sr.po
Normal file
File diff suppressed because it is too large
Load Diff
1829
contract/i18n/sr@latin.po
Normal file
1829
contract/i18n/sr@latin.po
Normal file
File diff suppressed because it is too large
Load Diff
1829
contract/i18n/sv.po
Normal file
1829
contract/i18n/sv.po
Normal file
File diff suppressed because it is too large
Load Diff
1829
contract/i18n/th.po
Normal file
1829
contract/i18n/th.po
Normal file
File diff suppressed because it is too large
Load Diff
2017
contract/i18n/tr.po
Normal file
2017
contract/i18n/tr.po
Normal file
File diff suppressed because it is too large
Load Diff
1921
contract/i18n/tr_TR.po
Normal file
1921
contract/i18n/tr_TR.po
Normal file
File diff suppressed because it is too large
Load Diff
1830
contract/i18n/uk.po
Normal file
1830
contract/i18n/uk.po
Normal file
File diff suppressed because it is too large
Load Diff
1827
contract/i18n/vi.po
Normal file
1827
contract/i18n/vi.po
Normal file
File diff suppressed because it is too large
Load Diff
1830
contract/i18n/vi_VN.po
Normal file
1830
contract/i18n/vi_VN.po
Normal file
File diff suppressed because it is too large
Load Diff
2032
contract/i18n/zh_CN.po
Normal file
2032
contract/i18n/zh_CN.po
Normal file
File diff suppressed because it is too large
Load Diff
1828
contract/i18n/zh_TW.po
Normal file
1828
contract/i18n/zh_TW.po
Normal file
File diff suppressed because it is too large
Load Diff
28
contract/migrations/13.0.1.0.0/noupdate_changes.xml
Normal file
28
contract/migrations/13.0.1.0.0/noupdate_changes.xml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8' ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="rule_contract_contract_multi_company" model="ir.rule">
|
||||||
|
<field
|
||||||
|
name="domain_force"
|
||||||
|
>['|',('company_id','=',False),('company_id','in',company_ids)]</field>
|
||||||
|
</record>
|
||||||
|
<record id="rule_contract_line_multi_company" model="ir.rule">
|
||||||
|
<field
|
||||||
|
name="domain_force"
|
||||||
|
>['|',('company_id','=',False),('company_id','in',company_ids)]</field>
|
||||||
|
</record>
|
||||||
|
<record id="rule_contract_template_multi_company" model="ir.rule">
|
||||||
|
<field
|
||||||
|
name="domain_force"
|
||||||
|
>['|',('company_id','=',False),('company_id','in',company_ids)]</field>
|
||||||
|
</record>
|
||||||
|
<record id="rule_contract_template_line_multi_company" model="ir.rule">
|
||||||
|
<field
|
||||||
|
name="domain_force"
|
||||||
|
>['|',('company_id','=',False),('company_id','in',company_ids)]</field>
|
||||||
|
</record>
|
||||||
|
<record id="contract_tag_multi_company_rule" model="ir.rule">
|
||||||
|
<field
|
||||||
|
name="domain_force"
|
||||||
|
>['|',('company_id','=',False),('company_id','in',company_ids)]</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
11
contract/migrations/13.0.1.0.0/post-migration.py
Normal file
11
contract/migrations/13.0.1.0.0/post-migration.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Copyright 2020 Tecnativa - Pedro M. Baeza
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
|
from openupgradelib import openupgrade # pylint: disable=W7936
|
||||||
|
|
||||||
|
|
||||||
|
@openupgrade.migrate()
|
||||||
|
def migrate(env, version):
|
||||||
|
openupgrade.load_data(
|
||||||
|
env.cr, "contract", "migrations/13.0.1.0.0/noupdate_changes.xml"
|
||||||
|
)
|
||||||
15
contract/models/__init__.py
Normal file
15
contract/models/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 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 account_move
|
||||||
|
from . import res_partner
|
||||||
|
from . import contract_tag
|
||||||
|
from . import res_company
|
||||||
|
from . import res_config_settings
|
||||||
|
from . import contract_terminate_reason
|
||||||
69
contract/models/abstract_contract.py
Normal file
69
contract/models/abstract_contract.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# Copyright 2004-2010 OpenERP SA
|
||||||
|
# Copyright 2014 Angel Moya <angel.moya@domatix.com>
|
||||||
|
# Copyright 2015-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
|
||||||
250
contract/models/abstract_contract_line.py
Normal file
250
contract/models/abstract_contract_line.py
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
# 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_context(
|
||||||
|
force_company=line.contract_id.company_id.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}
|
||||||
21
contract/models/account_move.py
Normal file
21
contract/models/account_move.py
Normal 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
|
||||||
|
)
|
||||||
525
contract/models/contract.py
Normal file
525
contract/models/contract.py
Normal file
@@ -0,0 +1,525 @@
|
|||||||
|
# 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
|
||||||
|
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",
|
||||||
|
]
|
||||||
|
|
||||||
|
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,
|
||||||
|
track_visibility="onchange",
|
||||||
|
)
|
||||||
|
terminate_comment = fields.Text(
|
||||||
|
string="Termination Comment",
|
||||||
|
readonly=True,
|
||||||
|
copy=False,
|
||||||
|
track_visibility="onchange",
|
||||||
|
)
|
||||||
|
terminate_date = fields.Date(
|
||||||
|
string="Termination Date",
|
||||||
|
readonly=True,
|
||||||
|
copy=False,
|
||||||
|
track_visibility="onchange",
|
||||||
|
)
|
||||||
|
|
||||||
|
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_context(
|
||||||
|
force_company=self.company_id.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_context(force_company=self.company_id.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_context(
|
||||||
|
force_company=self.company_id.id, default_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(l):
|
||||||
|
return (
|
||||||
|
not l.is_canceled
|
||||||
|
and l.recurring_next_date
|
||||||
|
and l.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)
|
||||||
|
invoice_vals["invoice_line_ids"].append((0, 0, invoice_line_vals))
|
||||||
|
invoices_values.append(invoice_vals)
|
||||||
|
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
|
||||||
|
|
||||||
|
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._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,
|
||||||
|
}
|
||||||
|
)
|
||||||
1063
contract/models/contract_line.py
Normal file
1063
contract/models/contract_line.py
Normal file
File diff suppressed because it is too large
Load Diff
428
contract/models/contract_line_constraints.py
Normal file
428
contract/models/contract_line_constraints.py
Normal 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=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
|
||||||
233
contract/models/contract_recurrency_mixin.py
Normal file
233
contract/models/contract_recurrency_mixin.py
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
# 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
|
||||||
15
contract/models/contract_tag.py
Normal file
15
contract/models/contract_tag.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 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(requirment=True)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
"res.company", string="Company", default=lambda self: self.env.company.id,
|
||||||
|
)
|
||||||
22
contract/models/contract_template.py
Normal file
22
contract/models/contract_template.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Copyright 2004-2010 OpenERP SA
|
||||||
|
# Copyright 2014 Angel Moya <angel.moya@domatix.com>
|
||||||
|
# Copyright 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
||||||
|
# Copyright 2016-2018 Carlos Dauden <carlos.dauden@tecnativa.com>
|
||||||
|
# Copyright 2016-2017 LasLabs Inc.
|
||||||
|
# Copyright 2018 ACSONE SA/NV
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class 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",
|
||||||
|
)
|
||||||
23
contract/models/contract_template_line.py
Normal file
23
contract/models/contract_template_line.py
Normal 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",
|
||||||
|
)
|
||||||
15
contract/models/contract_terminate_reason.py
Normal file
15
contract/models/contract_terminate_reason.py
Normal 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
|
||||||
|
)
|
||||||
17
contract/models/res_company.py
Normal file
17
contract/models/res_company.py
Normal 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",
|
||||||
|
)
|
||||||
19
contract/models/res_config_settings.py
Normal file
19
contract/models/res_config_settings.py
Normal 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",
|
||||||
|
)
|
||||||
71
contract/models/res_partner.py
Normal file
71
contract/models/res_partner.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Copyright 2017 Carlos Dauden <carlos.dauden@tecnativa.com>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
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 _compute_contract_count(self):
|
||||||
|
contract_model = self.env["contract.contract"]
|
||||||
|
fetch_data = contract_model.read_group(
|
||||||
|
[("partner_id", "child_of", self.ids)],
|
||||||
|
["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)
|
||||||
|
res.update(
|
||||||
|
context=dict(
|
||||||
|
self.env.context,
|
||||||
|
search_default_partner_id=self.id,
|
||||||
|
default_partner_id=self.id,
|
||||||
|
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_supplier_contract"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return self.env["ir.actions.act_window"].for_xml_id(
|
||||||
|
"contract", "action_customer_contract"
|
||||||
|
)
|
||||||
2
contract/readme/CONFIGURE.rst
Normal file
2
contract/readme/CONFIGURE.rst
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
To view discount field in contract line, you need to set *Discount on lines* in
|
||||||
|
user access rights.
|
||||||
11
contract/readme/CONTRIBUTORS.rst
Normal file
11
contract/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
* Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
||||||
|
* Carlos Dauden <carlos.dauden@tecnativa.com>
|
||||||
|
* Angel Moya <angel.moya@domatix.com>
|
||||||
|
* Dave Lasley <dave@laslabs.com>
|
||||||
|
* Vicent Cubells <vicent.cubells@tecnativa.com>
|
||||||
|
* Miquel Raïch <miquel.raich@eficent.com>
|
||||||
|
* Souheil Bejaoui <souheil.bejaoui@acsone.eu>
|
||||||
|
* Thomas Binsfeld <thomas.binsfeld@acsone.eu>
|
||||||
|
* Rafael Blasco <rafael.blasco@tecnativa.com>
|
||||||
|
* Guillaume Vandamme <guillaume.vandamme@acsone.eu>
|
||||||
|
* Raphaël Reverdy <raphael.reverdy@akretion.com>
|
||||||
4
contract/readme/DESCRIPTION.rst
Normal file
4
contract/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
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.
|
||||||
2
contract/readme/ROADMAP.rst
Normal file
2
contract/readme/ROADMAP.rst
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
* Recover states and others functional fields in Contracts.
|
||||||
|
* Add recurrence flag at template level.
|
||||||
25
contract/readme/USAGE.rst
Normal file
25
contract/readme/USAGE.rst
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#. 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.
|
||||||
11
contract/report/contract_views.xml
Normal file
11
contract/report/contract_views.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<report
|
||||||
|
id="report_contract"
|
||||||
|
model="contract.contract"
|
||||||
|
string="Contract"
|
||||||
|
report_type="qweb-pdf"
|
||||||
|
name="contract.report_contract_document"
|
||||||
|
file="contract.report_contract"
|
||||||
|
/>
|
||||||
|
</odoo>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user