commit
87088759a9
|
|
@ -0,0 +1 @@
|
|||
from . import controllers
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
'name': 'HyperPay ApplePay CopyAndPay Fast-Checkout',
|
||||
'version': '14.0.1.0',
|
||||
'description': 'Technical module to add apple pay in any place in the document',
|
||||
'summary': 'Technical module to add apple pay in any place in the document',
|
||||
'author': 'Expert Co. Ltd.',
|
||||
'website': 'https://www.exp-sa.com',
|
||||
'license': 'LGPL-3',
|
||||
'category': 'Payment',
|
||||
'depends': [
|
||||
'payment_applepay'
|
||||
],
|
||||
'data': [
|
||||
'views/applepay_iframe.xml',
|
||||
'views/templates.xml'
|
||||
],
|
||||
'auto_install': True,
|
||||
'application': False,
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import main
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import json
|
||||
|
||||
from odoo.http import route, request, Controller
|
||||
|
||||
|
||||
class ApplePayFastCheckout(Controller):
|
||||
|
||||
@route('/applepay', type='http', auth='public', website=True, csrf=False)
|
||||
def apple_pay_iframe(self, **kwargs):
|
||||
acquirer_id = request.env['payment.acquirer'].sudo().search([('provider', '=', 'applepay')], limit=1)
|
||||
|
||||
if acquirer_id.state == 'test':
|
||||
url = "https://eu-test.oppwa.com/v1/paymentWidgets.js"
|
||||
else:
|
||||
url = "https://oppwa.com/v1/paymentWidgets.js"
|
||||
|
||||
response = request.render("applepay_fast_checkout.apple_pay_iframe", {'hyperpay_src': url})
|
||||
response.headers['Content-Security-Policy'] = "script-src blob: 'self' 'unsafe-inline' 'unsafe-eval' https://*; worker-src blob: 'self' 'unsafe-inline' 'unsafe-eval' https://*;connect-src 'self' https://* wss://*;frame-src 'self' blob: https://*;"
|
||||
|
||||
return response
|
||||
|
||||
@route('/applepay/checkout', type='json', auth='public', website=True)
|
||||
def apple_pay_create_checkout(self, **post):
|
||||
data = json.loads(request.httprequest.data.decode('utf-8'))
|
||||
processed_data = self._process_checkout_data(data)
|
||||
checkout_id = self._get_checkout_id(processed_data)
|
||||
return {'checkout_id': checkout_id}
|
||||
|
||||
def _process_checkout_data(self, data):
|
||||
return data
|
||||
|
||||
def _get_checkout_id(self, vals):
|
||||
return ''
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
var wpwlOptions = {
|
||||
applePay: {
|
||||
version: 3,
|
||||
displayName: "ENSAN",
|
||||
total: { label: "ENSAN", amount: "10" },
|
||||
checkAvailability: "applePayCapabilities",
|
||||
currencyCode: "SAR",
|
||||
supportedNetworks: ["mada", "masterCard", "visa"],
|
||||
merchantCapabilities: ["supports3DS", "supportsCredit", "supportsDebit"],
|
||||
merchantIdentifier: "8ac9a4ca811e7d6f018132e7a3654ddf",
|
||||
supportedCountries: ["SA"],
|
||||
buttonSource: "css",
|
||||
buttonStyle: "white",
|
||||
buttonType: "pay",
|
||||
countryCode: "SA",
|
||||
submitOnPaymentAuthorized: ["customer"],
|
||||
requiredShippingContactFields: ["phone"],
|
||||
onCancel: function () {
|
||||
if (window.wpwl && window.wpwl.checkout && window.wpwl.checkout.id) {
|
||||
window.wpwl.checkout.id = "";
|
||||
}
|
||||
},
|
||||
},
|
||||
locale: "ar-AB",
|
||||
onReady: function () {
|
||||
window.parent.document.addEventListener("applePayAmountUpdate", function (e) {
|
||||
try {
|
||||
window.wpwlOptions.applePay.total.amount = e.detail.amount;
|
||||
console.log("Apple Pay Amount updated to:", window.wpwlOptions.applePay.total.amount);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
// let isCheckoutPage = $("#oe_structure_website_sale_payment_1", window.parent.document).length;
|
||||
// if (isCheckoutPage) {
|
||||
// let applepayButton = $("apple-pay-button");
|
||||
// let totalAmount = $("tr#order_total", window.parent.document).find("span.oe_currency_value").text().replaceAll(",", "");
|
||||
// window.wpwlOptions.applePay.total.amount = totalAmount;
|
||||
// console.log("Apple Pay Amount updated to:", window.wpwlOptions.applePay.total.amount);
|
||||
// applepayButton.css({
|
||||
// "--apple-pay-button-height": "60px",
|
||||
// "--apple-pay-button-border-radius": "4px",
|
||||
// "--apple-pay-button-padding": "15px 5px",
|
||||
// "--apple-pay-button-box-sizing": "border-box",
|
||||
// });
|
||||
// }
|
||||
},
|
||||
createCheckout: function () {
|
||||
const iframeElement = window.frameElement; // The <iframe> element in the parent
|
||||
const parentDiv = iframeElement?.parentNode;
|
||||
let requestDataJson = {};
|
||||
|
||||
if (parentDiv) {
|
||||
let requestData = $(parentDiv).find("input[name='applepay_checkout_details_json']").val() || "{}";
|
||||
requestDataJson = JSON.parse(requestData);
|
||||
}
|
||||
|
||||
console.log(requestDataJson);
|
||||
const rootUrl = `${window.location.protocol}//${window.location.host}/`;
|
||||
return $.ajax({
|
||||
url: `${rootUrl}applepay/checkout`,
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify(requestDataJson),
|
||||
dataType: "json",
|
||||
}).then(function (response) {
|
||||
console.log(response);
|
||||
return response.result.checkout_id;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -4,10 +4,10 @@
|
|||
<template id="apple_pay_iframe" name="Apple Pay Iframe">
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://oppwa.com/v1/paymentWidgets.js" />
|
||||
<script t-att-src="hyperpay_src" />
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js" />
|
||||
<script src="/payment_applepay/static/src/js/applepay_iframe.js" />
|
||||
<link rel="stylesheet" href="/payment_applepay/static/src/scss/applepay_iframe_content.scss" />
|
||||
<script src="/applepay_fast_checkout/static/src/js/applepay_iframe.js" />
|
||||
<link rel="stylesheet" href="/applepay_fast_checkout/static/src/scss/applepay_iframe_content.scss" />
|
||||
</head>
|
||||
<body>
|
||||
<form t-attf-action="{{request.httprequest.url_root}}payment/applepay/return"
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="applepay_btn" name="ApplePay Button">
|
||||
<t t-set="iframe_width" t-value="iframe_width or '100%'"/>
|
||||
<t t-set="iframe_height" t-value="iframe_height or '70px'"/>
|
||||
<div id="applepay_parent_root">
|
||||
<!-- JSON Object contains all the details of the current checkout and will be sent to the backend for processing before -->
|
||||
<!-- sending a checkout request to ApplePay, DEFAULTS TO THE CURRENT ORDER-->
|
||||
<input type="hidden" name="applepay_checkout_details_json" value="{}"/>
|
||||
|
||||
<iframe t-attf-style="width: {{iframe_width}}; height: {{iframe_height}}; border: none;"
|
||||
t-attf-src="{{request.httprequest.url_root}}applepay">
|
||||
</iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
================================
|
||||
Recurring - Contracts Management
|
||||
================================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:124a989ae63b390105b95256e33dd7e90ea48cf66aca810d17ac432c21a016a1
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Production/Stable
|
||||
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fcontract-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/contract/tree/14.0/contract
|
||||
:alt: OCA/contract
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/contract-14-0/contract-14-0-contract
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
||||
:target: https://runboat.odoo-community.org/builds?repo=OCA/contract&target_branch=14.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
This module enables contracts management with recurring
|
||||
invoicing functions. Also you can print and send by email contract report.
|
||||
|
||||
It works for customer contract and supplier contracts.
|
||||
|
||||
Contracts are shown in portal.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
To view discount field in contract line, you need to set *Discount on lines* in
|
||||
user access rights.
|
||||
|
||||
Contracts can be viewed on the portal (list and detail) if the user logged into the portal is a follower of the contract.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
#. Contracts are in Invoicing -> Customers -> Customer and Invoicing -> Vendors -> Supplier Contracts
|
||||
#. When creating a contract, fill fields for selecting the invoicing parameters:
|
||||
|
||||
* a journal
|
||||
* a price list (optional)
|
||||
|
||||
#. And add the lines to be invoiced with:
|
||||
|
||||
* the product with a description, a quantity and a price
|
||||
* the recurrence parameters: interval (days, weeks, months, months last day or years),
|
||||
start date, date of next invoice (automatically computed, can be modified) and end date (optional)
|
||||
* auto-price, for having a price automatically obtained from the price list
|
||||
* #START# - #END# or #INVOICEMONTHNAME# in the description field to display
|
||||
the start/end date or the start month of the invoiced period in the invoice line description
|
||||
* pre-paid (invoice at period start) or post-paid (invoice at start of next period)
|
||||
|
||||
#. The "Generate Recurring Invoices from Contracts" cron runs daily to generate the invoices.
|
||||
If you are in debug mode, you can click on the invoice creation button.
|
||||
#. The *Show recurring invoices* shortcut on contracts shows all invoices created from the
|
||||
contract.
|
||||
#. The contract report can be printed from the Print menu
|
||||
#. The contract can be sent by email with the *Send by Email* button
|
||||
#. Contract templates can be created from the Configuration -> Contracts -> Contract Templates menu.
|
||||
They allow to define default journal, price list and lines when creating a contract.
|
||||
To use it, just select the template on the contract and fields will be filled automatically.
|
||||
|
||||
* Contracts appear in portal to following users in every contract:
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/OCA/contract/14.0/contract/static/src/screenshots/portal-my.png
|
||||
.. image:: https://raw.githubusercontent.com/OCA/contract/14.0/contract/static/src/screenshots/portal-list.png
|
||||
.. image:: https://raw.githubusercontent.com/OCA/contract/14.0/contract/static/src/screenshots/portal-detail.png
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
* Recover states and others functional fields in Contracts.
|
||||
* Add recurrence flag at template level.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/contract/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/contract/issues/new?body=module:%20contract%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
* Tecnativa
|
||||
* ACSONE SA/NV
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Angel Moya <angel.moya@domatix.com>
|
||||
* Dave Lasley <dave@laslabs.com>
|
||||
* Miquel Raïch <miquel.raich@eficent.com>
|
||||
* Souheil Bejaoui <souheil.bejaoui@acsone.eu>
|
||||
* Thomas Binsfeld <thomas.binsfeld@acsone.eu>
|
||||
* Guillaume Vandamme <guillaume.vandamme@acsone.eu>
|
||||
* Raphaël Reverdy <raphael.reverdy@akretion.com>
|
||||
|
||||
* `Tecnativa <https://www.tecnativa.com>`_:
|
||||
|
||||
* Pedro M. Baeza
|
||||
* Carlos Dauden
|
||||
* Vicent Cubells
|
||||
* Rafael Blasco
|
||||
* Víctor Martínez
|
||||
* Iván Antón <ozono@ozonomultimedia.com>
|
||||
* Eric Antones <eantones@nuobit.com>
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
This module is part of the `OCA/contract <https://github.com/OCA/contract/tree/14.0/contract>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from . import controllers
|
||||
from . import models
|
||||
from . import wizards
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
# Copyright 2004-2010 OpenERP SA
|
||||
# Copyright 2014-2018 Tecnativa - Pedro M. Baeza
|
||||
# Copyright 2015 Domatix
|
||||
# Copyright 2016-2018 Tecnativa - Carlos Dauden
|
||||
# Copyright 2017 Tecnativa - Vicent Cubells
|
||||
# Copyright 2016-2017 LasLabs Inc.
|
||||
# Copyright 2018-2019 ACSONE SA/NV
|
||||
# Copyright 2020-2021 Tecnativa - Pedro M. Baeza
|
||||
# Copyright 2020 Tecnativa - Víctor Martínez
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "Recurring - Contracts Management",
|
||||
"version": "14.0.2.14.1",
|
||||
"category": "Contract Management",
|
||||
"license": "AGPL-3",
|
||||
"author": "Tecnativa, ACSONE SA/NV, Odoo Community Association (OCA)",
|
||||
"website": "https://github.com/OCA/contract",
|
||||
"depends": ["base", "account", "product", "portal"],
|
||||
"development_status": "Production/Stable",
|
||||
"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/template_mail_notification.xml",
|
||||
"data/mail_message_subtype.xml",
|
||||
"data/ir_ui_menu.xml",
|
||||
"wizards/contract_line_wizard.xml",
|
||||
"wizards/contract_manually_create_invoice.xml",
|
||||
"wizards/contract_contract_terminate.xml",
|
||||
"views/contract_tag.xml",
|
||||
"views/account_move_views.xml",
|
||||
"views/assets.xml",
|
||||
"views/abstract_contract_line.xml",
|
||||
"views/contract.xml",
|
||||
"views/contract_line.xml",
|
||||
"views/contract_template.xml",
|
||||
"views/contract_template_line.xml",
|
||||
"views/res_partner_view.xml",
|
||||
"views/res_config_settings.xml",
|
||||
"views/contract_terminate_reason.xml",
|
||||
"views/contract_portal_templates.xml",
|
||||
],
|
||||
"installable": True,
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import main
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
# Copyright 2020-2022 Tecnativa - Víctor Martínez
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, http
|
||||
from odoo.exceptions import AccessError, MissingError
|
||||
from odoo.http import request
|
||||
|
||||
from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager
|
||||
|
||||
|
||||
class PortalContract(CustomerPortal):
|
||||
def _prepare_home_portal_values(self, counters):
|
||||
values = super()._prepare_home_portal_values(counters)
|
||||
if "contract_count" in counters:
|
||||
contract_model = request.env["contract.contract"]
|
||||
contract_count = (
|
||||
contract_model.search_count([])
|
||||
if contract_model.check_access_rights("read", raise_exception=False)
|
||||
else 0
|
||||
)
|
||||
values["contract_count"] = contract_count
|
||||
return values
|
||||
|
||||
def _contract_get_page_view_values(self, contract, access_token, **kwargs):
|
||||
values = {
|
||||
"page_name": "Contracts",
|
||||
"contract": contract,
|
||||
}
|
||||
return self._get_page_view_values(
|
||||
contract, access_token, values, "my_contracts_history", False, **kwargs
|
||||
)
|
||||
|
||||
def _get_filter_domain(self, kw):
|
||||
return []
|
||||
|
||||
@http.route(
|
||||
["/my/contracts", "/my/contracts/page/<int:page>"],
|
||||
type="http",
|
||||
auth="user",
|
||||
website=True,
|
||||
)
|
||||
def portal_my_contracts(
|
||||
self, page=1, date_begin=None, date_end=None, sortby=None, **kw
|
||||
):
|
||||
values = self._prepare_portal_layout_values()
|
||||
contract_obj = request.env["contract.contract"]
|
||||
# Avoid error if the user does not have access.
|
||||
if not contract_obj.check_access_rights("read", raise_exception=False):
|
||||
return request.redirect("/my")
|
||||
domain = self._get_filter_domain(kw)
|
||||
searchbar_sortings = {
|
||||
"date": {"label": _("Date"), "order": "recurring_next_date desc"},
|
||||
"name": {"label": _("Name"), "order": "name desc"},
|
||||
"code": {"label": _("Reference"), "order": "code desc"},
|
||||
}
|
||||
# default sort by order
|
||||
if not sortby:
|
||||
sortby = "date"
|
||||
order = searchbar_sortings[sortby]["order"]
|
||||
# count for pager
|
||||
contract_count = contract_obj.search_count(domain)
|
||||
# pager
|
||||
pager = portal_pager(
|
||||
url="/my/contracts",
|
||||
url_args={
|
||||
"date_begin": date_begin,
|
||||
"date_end": date_end,
|
||||
"sortby": sortby,
|
||||
},
|
||||
total=contract_count,
|
||||
page=page,
|
||||
step=self._items_per_page,
|
||||
)
|
||||
# content according to pager and archive selected
|
||||
contracts = contract_obj.search(
|
||||
domain, order=order, limit=self._items_per_page, offset=pager["offset"]
|
||||
)
|
||||
request.session["my_contracts_history"] = contracts.ids[:100]
|
||||
values.update(
|
||||
{
|
||||
"date": date_begin,
|
||||
"contracts": contracts,
|
||||
"page_name": "Contracts",
|
||||
"pager": pager,
|
||||
"default_url": "/my/contracts",
|
||||
"searchbar_sortings": searchbar_sortings,
|
||||
"sortby": sortby,
|
||||
}
|
||||
)
|
||||
return request.render("contract.portal_my_contracts", values)
|
||||
|
||||
@http.route(
|
||||
["/my/contracts/<int:contract_contract_id>"],
|
||||
type="http",
|
||||
auth="public",
|
||||
website=True,
|
||||
)
|
||||
def portal_my_contract_detail(self, contract_contract_id, access_token=None, **kw):
|
||||
try:
|
||||
contract_sudo = self._document_check_access(
|
||||
"contract.contract", contract_contract_id, access_token
|
||||
)
|
||||
except (AccessError, MissingError):
|
||||
return request.redirect("/my")
|
||||
values = self._contract_get_page_view_values(contract_sudo, access_token, **kw)
|
||||
return request.render("contract.portal_contract_page", values)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="mail_message_subtype_invoice_created" model="mail.message.subtype">
|
||||
<field name="name">Invoice created</field>
|
||||
<field name="res_model">contract.contract</field>
|
||||
</record>
|
||||
<record
|
||||
id="mail_message_subtype_contract_modification"
|
||||
model="mail.message.subtype"
|
||||
>
|
||||
<field name="name">Contract modifications</field>
|
||||
<field name="res_model">contract.contract</field>
|
||||
<field name="default" eval="False" />
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="email_contract_template" model="mail.template">
|
||||
<field name="name">Email Contract Template</field>
|
||||
<field
|
||||
name="email_from"
|
||||
>${(object.user_id.email and '%s <%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>
|
||||
<p></p>
|
||||
<a href="${object.get_base_url()}/my/contracts/${object.id}?access_token=${object.access_token}" target="_blank" style="background-color:#875A7B;padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">View contract</a>
|
||||
</div>
|
||||
]]></field>
|
||||
</record>
|
||||
<record id="mail_template_contract_modification" model="mail.template">
|
||||
<field name="name">Contract Modification Template</field>
|
||||
<field
|
||||
name="email_from"
|
||||
>${(object.user_id.email and '%s <%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'}) - Modifications</field>
|
||||
<field name="model_id" ref="model_contract_contract" />
|
||||
<field name="lang">${object.partner_id.lang}</field>
|
||||
<field
|
||||
name="body_html"
|
||||
><![CDATA[
|
||||
<p>Hello</p>
|
||||
<p>We have modifications on the contract that we want to notify you.</p>
|
||||
]]></field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<template
|
||||
id="mail_notification_contract"
|
||||
inherit_id="mail.mail_notification_paynow"
|
||||
primary="True"
|
||||
>
|
||||
<xpath expr="//t[@t-raw='message.body']" position="after">
|
||||
<t t-raw="0" />
|
||||
<t t-if="record._name == 'contract.contract'">
|
||||
<t
|
||||
t-set="share_url"
|
||||
t-value="record._get_share_url(redirect=True, signup_partner=True, share_token=True)"
|
||||
/>
|
||||
<t
|
||||
t-set="access_url"
|
||||
t-value="is_online and share_url and base_url + share_url or ''"
|
||||
/>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
<template id="template_contract_modification" name="Contract Modification">
|
||||
<t t-call="contract.mail_notification_contract">
|
||||
<table border="1" align="center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th name="th_date">Date</th>
|
||||
<th name="th_description">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="record.modification_ids" t-as="modification">
|
||||
<tr t-if="not modification.sent">
|
||||
<td name="td_date">
|
||||
<span t-field="modification.date" />
|
||||
</td>
|
||||
<td name="td_description">
|
||||
<div t-field="modification.description" />
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,16 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import contract_recurrency_mixin # should be first
|
||||
from . import abstract_contract
|
||||
from . import abstract_contract_line
|
||||
from . import contract_template
|
||||
from . import contract
|
||||
from . import contract_template_line
|
||||
from . import contract_line
|
||||
from . import contract_modification
|
||||
from . import account_move
|
||||
from . import res_partner
|
||||
from . import contract_tag
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import contract_terminate_reason
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
# 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",
|
||||
check_company=True,
|
||||
domain="[('company_id', '=', company_id)]",
|
||||
)
|
||||
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,
|
||||
check_company=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.",
|
||||
)
|
||||
generation_type = fields.Selection(
|
||||
string="Generation Type",
|
||||
selection=lambda self: self._selection_generation_type(),
|
||||
default=lambda self: self._default_generation_type(),
|
||||
help="Choose the document that will be automatically generated by cron.",
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _selection_generation_type(self):
|
||||
return [("invoice", "Invoice")]
|
||||
|
||||
@api.model
|
||||
def _default_generation_type(self):
|
||||
return "invoice"
|
||||
|
||||
@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
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
# 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)
|
||||
partner_id = fields.Many2one(
|
||||
comodel_name="res.partner", related="contract_id.partner_id"
|
||||
)
|
||||
quantity = fields.Float(default=1.0, required=True)
|
||||
allowed_uom_categ_id = fields.Many2one(related="product_id.uom_id.category_id")
|
||||
uom_id = fields.Many2one(
|
||||
"uom.uom",
|
||||
string="Unit of Measure",
|
||||
domain="[('category_id', '=?', allowed_uom_categ_id)]",
|
||||
)
|
||||
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",
|
||||
digits="Product Price",
|
||||
)
|
||||
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("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",
|
||||
"uom_id",
|
||||
)
|
||||
def _compute_price_unit(self):
|
||||
"""Get the specific price if no auto-price, and the price obtained
|
||||
from the pricelist otherwise.
|
||||
"""
|
||||
for line in self:
|
||||
if line.automatic_price:
|
||||
pricelist = (
|
||||
line.contract_id.pricelist_id
|
||||
or line.contract_id.partner_id.with_company(
|
||||
line.contract_id.company_id
|
||||
).property_product_pricelist
|
||||
)
|
||||
product = line.product_id.with_context(
|
||||
quantity=line.env.context.get(
|
||||
"contract_line_qty",
|
||||
line.quantity,
|
||||
),
|
||||
pricelist=pricelist.id,
|
||||
partner=line.contract_id.partner_id,
|
||||
date=line.env.context.get(
|
||||
"old_date", fields.Date.context_today(line)
|
||||
),
|
||||
uom=line.uom_id.id,
|
||||
)
|
||||
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):
|
||||
vals = {}
|
||||
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)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -0,0 +1,706 @@
|
|||
# Copyright 2004-2010 OpenERP SA
|
||||
# Copyright 2014 Angel Moya <angel.moya@domatix.com>
|
||||
# Copyright 2015-2020 Tecnativa - Pedro M. Baeza
|
||||
# Copyright 2016-2018 Tecnativa - Carlos Dauden
|
||||
# Copyright 2016-2017 LasLabs Inc.
|
||||
# Copyright 2018 ACSONE SA/NV
|
||||
# Copyright 2021 Tecnativa - Víctor Martínez
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.osv import expression
|
||||
from odoo.tests import Form
|
||||
from odoo.tools.translate import _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ContractContract(models.Model):
|
||||
_name = "contract.contract"
|
||||
_description = "Contract"
|
||||
_order = "code, name asc"
|
||||
_check_company_auto = True
|
||||
_inherit = [
|
||||
"mail.thread",
|
||||
"mail.activity.mixin",
|
||||
"contract.abstract.contract",
|
||||
"contract.recurrency.mixin",
|
||||
"portal.mixin",
|
||||
]
|
||||
|
||||
active = fields.Boolean(
|
||||
default=True,
|
||||
)
|
||||
code = fields.Char(
|
||||
string="Reference",
|
||||
)
|
||||
group_id = fields.Many2one(
|
||||
string="Group",
|
||||
comodel_name="account.analytic.account",
|
||||
ondelete="restrict",
|
||||
check_company=True,
|
||||
domain="[('company_id', '=', company_id)]",
|
||||
)
|
||||
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,
|
||||
check_company=True,
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
)
|
||||
invoice_count = fields.Integer(compute="_compute_invoice_count")
|
||||
fiscal_position_id = fields.Many2one(
|
||||
comodel_name="account.fiscal.position",
|
||||
string="Fiscal Position",
|
||||
ondelete="restrict",
|
||||
check_company=True,
|
||||
domain="[('company_id', '=', company_id)]",
|
||||
)
|
||||
invoice_partner_id = fields.Many2one(
|
||||
string="Invoicing contact",
|
||||
comodel_name="res.partner",
|
||||
ondelete="restrict",
|
||||
domain="['|', ('id', 'parent_of', partner_id), ('id', 'child_of', partner_id)]",
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
comodel_name="res.partner", inverse="_inverse_partner_id", required=True
|
||||
)
|
||||
|
||||
commercial_partner_id = fields.Many2one(
|
||||
"res.partner",
|
||||
compute_sudo=True,
|
||||
related="partner_id.commercial_partner_id",
|
||||
store=True,
|
||||
string="Commercial Entity",
|
||||
index=True,
|
||||
)
|
||||
tag_ids = fields.Many2many(comodel_name="contract.tag", string="Tags")
|
||||
note = fields.Text(string="Notes")
|
||||
is_terminated = fields.Boolean(string="Terminated", readonly=True, copy=False)
|
||||
terminate_reason_id = fields.Many2one(
|
||||
comodel_name="contract.terminate.reason",
|
||||
string="Termination Reason",
|
||||
ondelete="restrict",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
tracking=True,
|
||||
)
|
||||
terminate_comment = fields.Text(
|
||||
string="Termination Comment",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
tracking=True,
|
||||
)
|
||||
terminate_date = fields.Date(
|
||||
string="Termination Date",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
tracking=True,
|
||||
)
|
||||
modification_ids = fields.One2many(
|
||||
comodel_name="contract.modification",
|
||||
inverse_name="contract_id",
|
||||
string="Modifications",
|
||||
)
|
||||
|
||||
def get_formview_id(self, access_uid=None):
|
||||
if self.contract_type == "sale":
|
||||
return self.env.ref("contract.contract_contract_customer_form_view").id
|
||||
else:
|
||||
return self.env.ref("contract.contract_contract_supplier_form_view").id
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
records._set_start_contract_modification()
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
if "modification_ids" in vals:
|
||||
res = super(
|
||||
ContractContract, self.with_context(bypass_modification_send=True)
|
||||
).write(vals)
|
||||
self._modification_mail_send()
|
||||
else:
|
||||
res = super(ContractContract, self).write(vals)
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _set_start_contract_modification(self):
|
||||
subtype_id = self.env.ref("contract.mail_message_subtype_contract_modification")
|
||||
for record in self:
|
||||
if record.contract_line_ids:
|
||||
date_start = min(record.contract_line_ids.mapped("date_start"))
|
||||
else:
|
||||
date_start = record.create_date
|
||||
record.message_subscribe(
|
||||
partner_ids=[record.partner_id.id], subtype_ids=[subtype_id.id]
|
||||
)
|
||||
record.with_context(skip_modification_mail=True).write(
|
||||
{
|
||||
"modification_ids": [
|
||||
(0, 0, {"date": date_start, "description": _("Contract start")})
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _modification_mail_send(self):
|
||||
for record in self:
|
||||
modification_ids_not_sent = record.modification_ids.filtered(
|
||||
lambda x: not x.sent
|
||||
)
|
||||
if modification_ids_not_sent:
|
||||
if not self.env.context.get("skip_modification_mail"):
|
||||
record.with_context(
|
||||
default_subtype_id=self.env.ref(
|
||||
"contract.mail_message_subtype_contract_modification"
|
||||
).id,
|
||||
).message_post_with_template(
|
||||
self.env.ref("contract.mail_template_contract_modification").id,
|
||||
email_layout_xmlid="contract.template_contract_modification",
|
||||
)
|
||||
modification_ids_not_sent.write({"sent": True})
|
||||
|
||||
def _compute_access_url(self):
|
||||
for record in self:
|
||||
record.access_url = "/my/contracts/{}".format(record.id)
|
||||
|
||||
def action_preview(self):
|
||||
"""Invoked when 'Preview' button in contract form view is clicked."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
"type": "ir.actions.act_url",
|
||||
"target": "self",
|
||||
"url": self.get_portal_url(),
|
||||
}
|
||||
|
||||
def _inverse_partner_id(self):
|
||||
for rec in self:
|
||||
if not rec.invoice_partner_id:
|
||||
rec.invoice_partner_id = rec.partner_id.address_get(["invoice"])[
|
||||
"invoice"
|
||||
]
|
||||
|
||||
def _get_related_invoices(self):
|
||||
self.ensure_one()
|
||||
|
||||
invoices = (
|
||||
self.env["account.move.line"]
|
||||
.search(
|
||||
[
|
||||
(
|
||||
"contract_line_id",
|
||||
"in",
|
||||
self.contract_line_ids.ids,
|
||||
)
|
||||
]
|
||||
)
|
||||
.mapped("move_id")
|
||||
)
|
||||
# we are forced to always search for this for not losing possible <=v11
|
||||
# generated invoices
|
||||
invoices |= self.env["account.move"].search([("old_contract_id", "=", self.id)])
|
||||
return invoices
|
||||
|
||||
def _get_computed_currency(self):
|
||||
"""Helper method for returning the theoretical computed currency."""
|
||||
self.ensure_one()
|
||||
currency = self.env["res.currency"]
|
||||
if any(self.contract_line_ids.mapped("automatic_price")):
|
||||
# Use pricelist currency
|
||||
currency = (
|
||||
self.pricelist_id.currency_id
|
||||
or self.partner_id.with_company(
|
||||
self.company_id
|
||||
).property_product_pricelist.currency_id
|
||||
)
|
||||
return currency or self.journal_id.currency_id or self.company_id.currency_id
|
||||
|
||||
@api.depends(
|
||||
"manual_currency_id",
|
||||
"pricelist_id",
|
||||
"partner_id",
|
||||
"journal_id",
|
||||
"company_id",
|
||||
)
|
||||
def _compute_currency_id(self):
|
||||
for rec in self:
|
||||
if rec.manual_currency_id:
|
||||
rec.currency_id = rec.manual_currency_id
|
||||
else:
|
||||
rec.currency_id = rec._get_computed_currency()
|
||||
|
||||
def _inverse_currency_id(self):
|
||||
"""If the currency is different from the computed one, then save it
|
||||
in the manual field.
|
||||
"""
|
||||
for rec in self:
|
||||
if rec._get_computed_currency() != rec.currency_id:
|
||||
rec.manual_currency_id = rec.currency_id
|
||||
else:
|
||||
rec.manual_currency_id = False
|
||||
|
||||
def _compute_invoice_count(self):
|
||||
for rec in self:
|
||||
rec.invoice_count = len(rec._get_related_invoices())
|
||||
|
||||
def action_show_invoices(self):
|
||||
self.ensure_one()
|
||||
tree_view = self.env.ref("account.view_invoice_tree", raise_if_not_found=False)
|
||||
form_view = self.env.ref("account.view_move_form", raise_if_not_found=False)
|
||||
ctx = dict(self.env.context)
|
||||
if ctx.get("default_contract_type"):
|
||||
ctx["default_move_type"] = (
|
||||
"out_invoice"
|
||||
if ctx.get("default_contract_type") == "sale"
|
||||
else "in_invoice"
|
||||
)
|
||||
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)],
|
||||
"context": ctx,
|
||||
}
|
||||
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):
|
||||
# Compute the recurring_next_date on the contract based on the one
|
||||
# defined on line level.
|
||||
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")
|
||||
# Take the earliest or set it as False if contract is stopped
|
||||
# (no recurring_next_date).
|
||||
contract.recurring_next_date = (
|
||||
min(recurring_next_date) if recurring_next_date else False
|
||||
)
|
||||
|
||||
@api.depends("contract_line_ids.create_invoice_visibility")
|
||||
def _compute_create_invoice_visibility(self):
|
||||
for contract in self:
|
||||
contract.create_invoice_visibility = any(
|
||||
contract.contract_line_ids.mapped("create_invoice_visibility")
|
||||
)
|
||||
|
||||
@api.onchange("contract_template_id")
|
||||
def _onchange_contract_template_id(self):
|
||||
"""Update the contract fields with that of the template.
|
||||
|
||||
Take special consideration with the `contract_line_ids`,
|
||||
which must be created using the data from the contract lines. Cascade
|
||||
deletion ensures that any errant lines that are created are also
|
||||
deleted.
|
||||
"""
|
||||
contract_template_id = self.contract_template_id
|
||||
if not contract_template_id:
|
||||
return
|
||||
for field_name, field in contract_template_id._fields.items():
|
||||
if field.name == "contract_line_ids":
|
||||
lines = self._convert_contract_lines(contract_template_id)
|
||||
self.contract_line_ids += lines
|
||||
elif not any(
|
||||
(
|
||||
field.compute,
|
||||
field.related,
|
||||
field.automatic,
|
||||
field.readonly,
|
||||
field.company_dependent,
|
||||
field.name in self.NO_SYNC,
|
||||
)
|
||||
):
|
||||
if self.contract_template_id[field_name]:
|
||||
self[field_name] = self.contract_template_id[field_name]
|
||||
|
||||
@api.onchange("partner_id", "company_id")
|
||||
def _onchange_partner_id(self):
|
||||
partner = (
|
||||
self.partner_id
|
||||
if not self.company_id
|
||||
else self.partner_id.with_company(self.company_id)
|
||||
)
|
||||
self.pricelist_id = partner.property_product_pricelist.id
|
||||
self.fiscal_position_id = partner.env[
|
||||
"account.fiscal.position"
|
||||
].get_fiscal_position(partner.id)
|
||||
if self.contract_type == "purchase":
|
||||
self.payment_term_id = partner.property_supplier_payment_term_id
|
||||
else:
|
||||
self.payment_term_id = partner.property_payment_term_id
|
||||
self.invoice_partner_id = self.partner_id.address_get(["invoice"])["invoice"]
|
||||
|
||||
def _convert_contract_lines(self, contract):
|
||||
self.ensure_one()
|
||||
new_lines = self.env["contract.line"]
|
||||
contract_line_model = self.env["contract.line"]
|
||||
for contract_line in contract.contract_line_ids:
|
||||
vals = contract_line._convert_to_write(contract_line.read()[0])
|
||||
# Remove template link field
|
||||
vals.pop("contract_template_id", False)
|
||||
vals["date_start"] = fields.Date.context_today(contract_line)
|
||||
vals["recurring_next_date"] = fields.Date.context_today(contract_line)
|
||||
new_lines += contract_line_model.new(vals)
|
||||
new_lines._onchange_is_auto_renew()
|
||||
return new_lines
|
||||
|
||||
def _prepare_invoice(self, date_invoice, journal=None):
|
||||
"""Prepare in a Form the values for the generated invoice record.
|
||||
|
||||
:return: A tuple with the vals dictionary and the Form with the
|
||||
preloaded values for being used in lines.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not journal:
|
||||
journal = (
|
||||
self.journal_id
|
||||
if self.journal_id.type == self.contract_type
|
||||
else self.env["account.journal"].search(
|
||||
[
|
||||
("type", "=", self.contract_type),
|
||||
("company_id", "=", self.company_id.id),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
)
|
||||
if not journal:
|
||||
raise ValidationError(
|
||||
_("Please define a %s journal for the company '%s'.")
|
||||
% (self.contract_type, self.company_id.name or "")
|
||||
)
|
||||
invoice_type = "out_invoice"
|
||||
if self.contract_type == "purchase":
|
||||
invoice_type = "in_invoice"
|
||||
move_form = Form(
|
||||
self.env["account.move"]
|
||||
.with_company(self.company_id)
|
||||
.with_context(default_move_type=invoice_type, default_name="/"),
|
||||
view="contract.view_account_move_contract_helper_form",
|
||||
)
|
||||
move_form.partner_id = self.invoice_partner_id
|
||||
move_form.journal_id = journal
|
||||
move_form.currency_id = self.currency_id
|
||||
move_form.invoice_date = date_invoice
|
||||
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
|
||||
if invoice_type == "out_invoice" and self.user_id:
|
||||
move_form.invoice_user_id = self.user_id
|
||||
invoice_vals = move_form._values_to_save(all_fields=True)
|
||||
invoice_vals.update(
|
||||
{
|
||||
"ref": self.code,
|
||||
"date": date_invoice,
|
||||
"invoice_origin": self.name,
|
||||
}
|
||||
)
|
||||
return invoice_vals, move_form
|
||||
|
||||
def action_contract_send(self):
|
||||
self.ensure_one()
|
||||
template = self.env.ref("contract.email_contract_template", False)
|
||||
compose_form = self.env.ref("mail.email_compose_message_wizard_form")
|
||||
ctx = dict(
|
||||
default_model="contract.contract",
|
||||
default_res_id=self.id,
|
||||
default_use_template=bool(template),
|
||||
default_template_id=template and template.id or False,
|
||||
default_composition_mode="comment",
|
||||
)
|
||||
return {
|
||||
"name": _("Compose Email"),
|
||||
"type": "ir.actions.act_window",
|
||||
"view_mode": "form",
|
||||
"res_model": "mail.compose.message",
|
||||
"views": [(compose_form.id, "form")],
|
||||
"view_id": compose_form.id,
|
||||
"target": "new",
|
||||
"context": ctx,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_contracts_to_invoice_domain(self, date_ref=None):
|
||||
"""
|
||||
This method builds the domain to use to find all
|
||||
contracts (contract.contract) to invoice.
|
||||
:param date_ref: optional reference date to use instead of today
|
||||
:return: list (domain) usable on contract.contract
|
||||
"""
|
||||
domain = []
|
||||
if not date_ref:
|
||||
date_ref = fields.Date.context_today(self)
|
||||
domain.extend([("recurring_next_date", "<=", date_ref)])
|
||||
return domain
|
||||
|
||||
def _get_lines_to_invoice(self, date_ref):
|
||||
"""
|
||||
This method fetches and returns the lines to invoice on the contract
|
||||
(self), based on the given date.
|
||||
:param date_ref: date used as reference date to find lines to invoice
|
||||
:return: contract lines (contract.line recordset)
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
def can_be_invoiced(contract_line):
|
||||
return (
|
||||
not contract_line.is_canceled
|
||||
and contract_line.recurring_next_date
|
||||
and contract_line.recurring_next_date <= date_ref
|
||||
)
|
||||
|
||||
lines2invoice = previous = self.env["contract.line"]
|
||||
current_section = current_note = False
|
||||
for line in self.contract_line_ids:
|
||||
if line.display_type == "line_section":
|
||||
current_section = line
|
||||
elif line.display_type == "line_note" and not line.is_recurring_note:
|
||||
if line.note_invoicing_mode == "with_previous_line":
|
||||
if previous in lines2invoice:
|
||||
lines2invoice |= line
|
||||
current_note = False
|
||||
elif line.note_invoicing_mode == "with_next_line":
|
||||
current_note = line
|
||||
elif line.is_recurring_note or not line.display_type:
|
||||
if can_be_invoiced(line):
|
||||
if current_section:
|
||||
lines2invoice |= current_section
|
||||
current_section = False
|
||||
if current_note:
|
||||
lines2invoice |= current_note
|
||||
lines2invoice |= line
|
||||
current_note = False
|
||||
previous = line
|
||||
return lines2invoice.sorted()
|
||||
|
||||
def _prepare_recurring_invoices_values(self, date_ref=False):
|
||||
"""
|
||||
This method builds the list of invoices values to create, based on
|
||||
the lines to invoice of the contracts in self.
|
||||
!!! The date of next invoice (recurring_next_date) is updated here !!!
|
||||
:return: list of dictionaries (invoices values)
|
||||
"""
|
||||
invoices_values = []
|
||||
for contract in self:
|
||||
if not date_ref:
|
||||
date_ref = contract.recurring_next_date
|
||||
if not date_ref:
|
||||
# this use case is possible when recurring_create_invoice is
|
||||
# called for a finished contract
|
||||
continue
|
||||
contract_lines = contract._get_lines_to_invoice(date_ref)
|
||||
if not contract_lines:
|
||||
continue
|
||||
invoice_vals, move_form = contract._prepare_invoice(date_ref)
|
||||
invoice_vals["invoice_line_ids"] = []
|
||||
for line in contract_lines:
|
||||
invoice_line_vals = line._prepare_invoice_line(move_form=move_form)
|
||||
if invoice_line_vals:
|
||||
# Allow extension modules to return an empty dictionary for
|
||||
# nullifying line. We should then cleanup certain values.
|
||||
del invoice_line_vals["company_id"]
|
||||
del invoice_line_vals["company_currency_id"]
|
||||
invoice_vals["invoice_line_ids"].append((0, 0, invoice_line_vals))
|
||||
invoices_values.append(invoice_vals)
|
||||
# Force the recomputation of journal items
|
||||
del invoice_vals["line_ids"]
|
||||
contract_lines._update_recurring_next_date()
|
||||
return invoices_values
|
||||
|
||||
def recurring_create_invoice(self):
|
||||
"""
|
||||
This method triggers the creation of the next invoices of the contracts
|
||||
even if their next invoicing date is in the future.
|
||||
"""
|
||||
invoices = self._recurring_create_invoice()
|
||||
for invoice in invoices:
|
||||
self.message_post(
|
||||
body=_(
|
||||
"Contract manually invoiced: "
|
||||
'<a href="#" data-oe-model="%s" data-oe-id="%s">Invoice'
|
||||
"</a>"
|
||||
)
|
||||
% (invoice._name, invoice.id)
|
||||
)
|
||||
return invoices
|
||||
|
||||
@api.model
|
||||
def _invoice_followers(self, invoices):
|
||||
invoice_create_subtype = self.env.ref(
|
||||
"contract.mail_message_subtype_invoice_created"
|
||||
)
|
||||
for item in self:
|
||||
partner_ids = item.message_follower_ids.filtered(
|
||||
lambda x: invoice_create_subtype in x.subtype_ids
|
||||
).mapped("partner_id")
|
||||
if partner_ids:
|
||||
(invoices & item._get_related_invoices()).message_subscribe(
|
||||
partner_ids=partner_ids.ids
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _add_contract_origin(self, invoices):
|
||||
for item in self:
|
||||
for move in invoices & item._get_related_invoices():
|
||||
move.message_post(
|
||||
body=(
|
||||
_("%s by contract %s.")
|
||||
% (
|
||||
move._creation_message(),
|
||||
"<a href=# data-oe-model=contract.contract data-oe-id=%d>%s</a>"
|
||||
% (item.id, item.display_name),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
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._add_contract_origin(moves)
|
||||
self._invoice_followers(moves)
|
||||
self._compute_recurring_next_date()
|
||||
return moves
|
||||
|
||||
@api.model
|
||||
def _get_recurring_create_func(self, create_type="invoice"):
|
||||
"""
|
||||
Allows to retrieve the recurring create function depending
|
||||
on generate_type attribute
|
||||
"""
|
||||
if create_type == "invoice":
|
||||
return self.__class__._recurring_create_invoice
|
||||
|
||||
@api.model
|
||||
def _cron_recurring_create(self, date_ref=False, create_type="invoice"):
|
||||
"""
|
||||
The cron function in order to create recurrent documents
|
||||
from contracts.
|
||||
"""
|
||||
_recurring_create_func = self._get_recurring_create_func(
|
||||
create_type=create_type
|
||||
)
|
||||
if not date_ref:
|
||||
date_ref = fields.Date.context_today(self)
|
||||
domain = self._get_contracts_to_invoice_domain(date_ref)
|
||||
domain = expression.AND(
|
||||
[
|
||||
domain,
|
||||
[("generation_type", "=", create_type)],
|
||||
]
|
||||
)
|
||||
contracts = self.search(domain)
|
||||
companies = set(contracts.mapped("company_id"))
|
||||
# Invoice by companies, so assignation emails get correct context
|
||||
for company in companies:
|
||||
contracts_to_invoice = contracts.filtered(
|
||||
lambda c: c.company_id == company
|
||||
and (not c.date_end or c.recurring_next_date <= c.date_end)
|
||||
).with_company(company)
|
||||
_recurring_create_func(contracts_to_invoice, date_ref)
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def cron_recurring_create_invoice(self, date_ref=None):
|
||||
return self._cron_recurring_create(date_ref, create_type="invoice")
|
||||
|
||||
def action_terminate_contract(self):
|
||||
self.ensure_one()
|
||||
context = {"default_contract_id": self.id}
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"name": _("Terminate Contract"),
|
||||
"res_model": "contract.contract.terminate",
|
||||
"view_mode": "form",
|
||||
"target": "new",
|
||||
"context": context,
|
||||
}
|
||||
|
||||
def _terminate_contract(
|
||||
self, terminate_reason_id, terminate_comment, terminate_date
|
||||
):
|
||||
self.ensure_one()
|
||||
if not self.env.user.has_group("contract.can_terminate_contract"):
|
||||
raise UserError(_("You are not allowed to terminate contracts."))
|
||||
self.contract_line_ids.filtered("is_stop_allowed").stop(terminate_date)
|
||||
self.write(
|
||||
{
|
||||
"is_terminated": True,
|
||||
"terminate_reason_id": terminate_reason_id.id,
|
||||
"terminate_comment": terminate_comment,
|
||||
"terminate_date": terminate_date,
|
||||
}
|
||||
)
|
||||
return True
|
||||
|
||||
def action_cancel_contract_termination(self):
|
||||
self.ensure_one()
|
||||
self.write(
|
||||
{
|
||||
"is_terminated": False,
|
||||
"terminate_reason_id": False,
|
||||
"terminate_comment": False,
|
||||
"terminate_date": False,
|
||||
}
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,429 @@
|
|||
# Copyright 2018 ACSONE SA/NV.
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import itertools
|
||||
from collections import namedtuple
|
||||
|
||||
from odoo.fields import Date
|
||||
|
||||
Criteria = namedtuple(
|
||||
"Criteria",
|
||||
[
|
||||
"when", # Contract line relatively to today (BEFORE, IN, AFTER)
|
||||
"has_date_end", # Is date_end set on contract line (bool)
|
||||
"has_last_date_invoiced", # Is last_date_invoiced set on contract line
|
||||
"is_auto_renew", # Is is_auto_renew set on contract line (bool)
|
||||
"has_successor", # Is contract line has_successor (bool)
|
||||
"predecessor_has_successor",
|
||||
# Is contract line predecessor has successor (bool)
|
||||
# In almost of the cases
|
||||
# contract_line.predecessor.successor == contract_line
|
||||
# But at cancel action,
|
||||
# contract_line.predecessor.successor == False
|
||||
# This is to permit plan_successor on predecessor
|
||||
# If contract_line.predecessor.successor != False
|
||||
# and contract_line is canceled, we don't allow uncancel
|
||||
# else we re-link contract_line and its predecessor
|
||||
"canceled", # Is contract line canceled (bool)
|
||||
],
|
||||
)
|
||||
Allowed = namedtuple(
|
||||
"Allowed",
|
||||
["plan_successor", "stop_plan_successor", "stop", "cancel", "uncancel"],
|
||||
)
|
||||
|
||||
|
||||
def _expand_none(criteria):
|
||||
variations = []
|
||||
for attribute, value in criteria._asdict().items():
|
||||
if value is None:
|
||||
if attribute == "when":
|
||||
variations.append(["BEFORE", "IN", "AFTER"])
|
||||
else:
|
||||
variations.append([True, False])
|
||||
else:
|
||||
variations.append([value])
|
||||
return itertools.product(*variations)
|
||||
|
||||
|
||||
def _add(matrix, criteria, allowed):
|
||||
"""Expand None values to True/False combination"""
|
||||
for c in _expand_none(criteria):
|
||||
matrix[c] = allowed
|
||||
|
||||
|
||||
CRITERIA_ALLOWED_DICT = {
|
||||
Criteria(
|
||||
when="BEFORE",
|
||||
has_date_end=True,
|
||||
has_last_date_invoiced=False,
|
||||
is_auto_renew=True,
|
||||
has_successor=False,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=False,
|
||||
stop_plan_successor=True,
|
||||
stop=True,
|
||||
cancel=True,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="BEFORE",
|
||||
has_date_end=True,
|
||||
has_last_date_invoiced=False,
|
||||
is_auto_renew=False,
|
||||
has_successor=True,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=False,
|
||||
stop_plan_successor=False,
|
||||
stop=True,
|
||||
cancel=True,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="BEFORE",
|
||||
has_date_end=True,
|
||||
has_last_date_invoiced=False,
|
||||
is_auto_renew=False,
|
||||
has_successor=False,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=True,
|
||||
stop_plan_successor=True,
|
||||
stop=True,
|
||||
cancel=True,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="BEFORE",
|
||||
has_date_end=False,
|
||||
has_last_date_invoiced=False,
|
||||
is_auto_renew=False,
|
||||
has_successor=False,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=False,
|
||||
stop_plan_successor=True,
|
||||
stop=True,
|
||||
cancel=True,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="IN",
|
||||
has_date_end=True,
|
||||
has_last_date_invoiced=False,
|
||||
is_auto_renew=True,
|
||||
has_successor=False,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=False,
|
||||
stop_plan_successor=True,
|
||||
stop=True,
|
||||
cancel=True,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="IN",
|
||||
has_date_end=True,
|
||||
has_last_date_invoiced=False,
|
||||
is_auto_renew=False,
|
||||
has_successor=True,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=False,
|
||||
stop_plan_successor=False,
|
||||
stop=True,
|
||||
cancel=True,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="IN",
|
||||
has_date_end=True,
|
||||
has_last_date_invoiced=False,
|
||||
is_auto_renew=False,
|
||||
has_successor=False,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=True,
|
||||
stop_plan_successor=True,
|
||||
stop=True,
|
||||
cancel=True,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="IN",
|
||||
has_date_end=False,
|
||||
has_last_date_invoiced=False,
|
||||
is_auto_renew=False,
|
||||
has_successor=False,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=False,
|
||||
stop_plan_successor=True,
|
||||
stop=True,
|
||||
cancel=True,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="BEFORE",
|
||||
has_date_end=True,
|
||||
has_last_date_invoiced=True,
|
||||
is_auto_renew=True,
|
||||
has_successor=False,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=False,
|
||||
stop_plan_successor=True,
|
||||
stop=True,
|
||||
cancel=False,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="BEFORE",
|
||||
has_date_end=True,
|
||||
has_last_date_invoiced=True,
|
||||
is_auto_renew=False,
|
||||
has_successor=True,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=False,
|
||||
stop_plan_successor=False,
|
||||
stop=True,
|
||||
cancel=False,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="BEFORE",
|
||||
has_date_end=True,
|
||||
has_last_date_invoiced=True,
|
||||
is_auto_renew=False,
|
||||
has_successor=False,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=True,
|
||||
stop_plan_successor=True,
|
||||
stop=True,
|
||||
cancel=False,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="BEFORE",
|
||||
has_date_end=False,
|
||||
has_last_date_invoiced=True,
|
||||
is_auto_renew=False,
|
||||
has_successor=False,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=False,
|
||||
stop_plan_successor=True,
|
||||
stop=True,
|
||||
cancel=False,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="IN",
|
||||
has_date_end=True,
|
||||
has_last_date_invoiced=True,
|
||||
is_auto_renew=True,
|
||||
has_successor=False,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=False,
|
||||
stop_plan_successor=True,
|
||||
stop=True,
|
||||
cancel=False,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="IN",
|
||||
has_date_end=True,
|
||||
has_last_date_invoiced=True,
|
||||
is_auto_renew=False,
|
||||
has_successor=True,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=False,
|
||||
stop_plan_successor=False,
|
||||
stop=True,
|
||||
cancel=False,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="IN",
|
||||
has_date_end=True,
|
||||
has_last_date_invoiced=True,
|
||||
is_auto_renew=False,
|
||||
has_successor=False,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=True,
|
||||
stop_plan_successor=True,
|
||||
stop=True,
|
||||
cancel=False,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="IN",
|
||||
has_date_end=False,
|
||||
has_last_date_invoiced=True,
|
||||
is_auto_renew=False,
|
||||
has_successor=False,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=False,
|
||||
stop_plan_successor=True,
|
||||
stop=True,
|
||||
cancel=False,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="AFTER",
|
||||
has_date_end=True,
|
||||
has_last_date_invoiced=None,
|
||||
is_auto_renew=True,
|
||||
has_successor=False,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=False,
|
||||
stop_plan_successor=False,
|
||||
stop=True,
|
||||
cancel=False,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="AFTER",
|
||||
has_date_end=True,
|
||||
has_last_date_invoiced=None,
|
||||
is_auto_renew=False,
|
||||
has_successor=True,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=False,
|
||||
stop_plan_successor=False,
|
||||
stop=False,
|
||||
cancel=False,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when="AFTER",
|
||||
has_date_end=True,
|
||||
has_last_date_invoiced=None,
|
||||
is_auto_renew=False,
|
||||
has_successor=False,
|
||||
predecessor_has_successor=None,
|
||||
canceled=False,
|
||||
): Allowed(
|
||||
plan_successor=True,
|
||||
stop_plan_successor=False,
|
||||
stop=True,
|
||||
cancel=False,
|
||||
uncancel=False,
|
||||
),
|
||||
Criteria(
|
||||
when=None,
|
||||
has_date_end=None,
|
||||
has_last_date_invoiced=None,
|
||||
is_auto_renew=None,
|
||||
has_successor=None,
|
||||
predecessor_has_successor=False,
|
||||
canceled=True,
|
||||
): Allowed(
|
||||
plan_successor=False,
|
||||
stop_plan_successor=False,
|
||||
stop=False,
|
||||
cancel=False,
|
||||
uncancel=True,
|
||||
),
|
||||
Criteria(
|
||||
when=None,
|
||||
has_date_end=None,
|
||||
has_last_date_invoiced=None,
|
||||
is_auto_renew=None,
|
||||
has_successor=None,
|
||||
predecessor_has_successor=True,
|
||||
canceled=True,
|
||||
): Allowed(
|
||||
plan_successor=False,
|
||||
stop_plan_successor=False,
|
||||
stop=False,
|
||||
cancel=False,
|
||||
uncancel=False,
|
||||
),
|
||||
}
|
||||
criteria_allowed_dict = {}
|
||||
|
||||
for c in CRITERIA_ALLOWED_DICT:
|
||||
_add(criteria_allowed_dict, c, CRITERIA_ALLOWED_DICT[c])
|
||||
|
||||
|
||||
def compute_when(date_start, date_end):
|
||||
today = Date.today()
|
||||
if today < date_start:
|
||||
return "BEFORE"
|
||||
if date_end and today > date_end:
|
||||
return "AFTER"
|
||||
return "IN"
|
||||
|
||||
|
||||
def compute_criteria(
|
||||
date_start,
|
||||
date_end,
|
||||
has_last_date_invoiced,
|
||||
is_auto_renew,
|
||||
successor_contract_line_id,
|
||||
predecessor_contract_line_id,
|
||||
is_canceled,
|
||||
):
|
||||
return Criteria(
|
||||
when=compute_when(date_start, date_end),
|
||||
has_date_end=bool(date_end),
|
||||
has_last_date_invoiced=bool(has_last_date_invoiced),
|
||||
is_auto_renew=is_auto_renew,
|
||||
has_successor=bool(successor_contract_line_id),
|
||||
predecessor_has_successor=bool(
|
||||
predecessor_contract_line_id.successor_contract_line_id
|
||||
),
|
||||
canceled=is_canceled,
|
||||
)
|
||||
|
||||
|
||||
def get_allowed(
|
||||
date_start,
|
||||
date_end,
|
||||
has_last_date_invoiced,
|
||||
is_auto_renew,
|
||||
successor_contract_line_id,
|
||||
predecessor_contract_line_id,
|
||||
is_canceled,
|
||||
):
|
||||
criteria = compute_criteria(
|
||||
date_start,
|
||||
date_end,
|
||||
has_last_date_invoiced,
|
||||
is_auto_renew,
|
||||
successor_contract_line_id,
|
||||
predecessor_contract_line_id,
|
||||
is_canceled,
|
||||
)
|
||||
if criteria in criteria_allowed_dict:
|
||||
return criteria_allowed_dict[criteria]
|
||||
return False
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# Copyright 2020 Tecnativa - Víctor Martínez
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ContractModification(models.Model):
|
||||
|
||||
_name = "contract.modification"
|
||||
_description = "Contract Modification"
|
||||
_order = "date desc"
|
||||
|
||||
date = fields.Date(required=True, string="Date")
|
||||
description = fields.Text(required=True, string="Description")
|
||||
contract_id = fields.Many2one(
|
||||
string="Contract",
|
||||
comodel_name="contract.contract",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
index=True,
|
||||
)
|
||||
sent = fields.Boolean(
|
||||
string="Sent",
|
||||
default=False,
|
||||
)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
if not self.env.context.get("bypass_modification_send"):
|
||||
records.check_modification_ids_need_sent()
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if not self.env.context.get("bypass_modification_send"):
|
||||
self.check_modification_ids_need_sent()
|
||||
return res
|
||||
|
||||
def check_modification_ids_need_sent(self):
|
||||
self.mapped("contract_id")._modification_mail_send()
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
# 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:
|
||||
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
|
||||
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
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Copyright 2019 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ContractTag(models.Model):
|
||||
|
||||
_name = "contract.tag"
|
||||
_description = "Contract Tag"
|
||||
|
||||
name = fields.Char(required=True)
|
||||
company_id = fields.Many2one(
|
||||
"res.company",
|
||||
string="Company",
|
||||
default=lambda self: self.env.company.id,
|
||||
)
|
||||
color = fields.Integer("Color Index", default=0)
|
||||
|
|
@ -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",
|
||||
)
|
||||
|
|
@ -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",
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue