odoo-addon-contract 17.0.1.4.4__py3-none-any.whl → 18.0.2.0.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo/addons/contract/README.rst +14 -10
- odoo/addons/contract/__manifest__.py +3 -10
- odoo/addons/contract/controllers/main.py +1 -8
- odoo/addons/contract/data/contract_cron.xml +0 -2
- odoo/addons/contract/data/mail_template.xml +18 -17
- odoo/addons/contract/data/template_mail_notification.xml +1 -1
- odoo/addons/contract/i18n/am.po +141 -821
- odoo/addons/contract/i18n/ar.po +141 -821
- odoo/addons/contract/i18n/bg.po +141 -821
- odoo/addons/contract/i18n/bs.po +141 -821
- odoo/addons/contract/i18n/ca.po +831 -901
- odoo/addons/contract/i18n/ca_ES.po +141 -821
- odoo/addons/contract/i18n/contract.pot +140 -818
- odoo/addons/contract/i18n/cs.po +141 -821
- odoo/addons/contract/i18n/da.po +141 -821
- odoo/addons/contract/i18n/de.po +708 -954
- odoo/addons/contract/i18n/el_GR.po +141 -821
- odoo/addons/contract/i18n/en_GB.po +141 -821
- odoo/addons/contract/i18n/es.po +710 -948
- odoo/addons/contract/i18n/es_AR.po +548 -880
- odoo/addons/contract/i18n/es_CL.po +141 -821
- odoo/addons/contract/i18n/es_CO.po +141 -821
- odoo/addons/contract/i18n/es_CR.po +141 -821
- odoo/addons/contract/i18n/es_DO.po +141 -821
- odoo/addons/contract/i18n/es_EC.po +141 -821
- odoo/addons/contract/i18n/es_MX.po +141 -821
- odoo/addons/contract/i18n/es_PY.po +141 -821
- odoo/addons/contract/i18n/es_VE.po +141 -821
- odoo/addons/contract/i18n/et.po +141 -821
- odoo/addons/contract/i18n/eu.po +141 -821
- odoo/addons/contract/i18n/fa.po +141 -821
- odoo/addons/contract/i18n/fi.po +419 -850
- odoo/addons/contract/i18n/fr.po +706 -951
- odoo/addons/contract/i18n/fr_CA.po +141 -821
- odoo/addons/contract/i18n/fr_CH.po +141 -821
- odoo/addons/contract/i18n/fr_FR.po +449 -850
- odoo/addons/contract/i18n/gl.po +252 -846
- odoo/addons/contract/i18n/gl_ES.po +141 -821
- odoo/addons/contract/i18n/he.po +141 -821
- odoo/addons/contract/i18n/hi_IN.po +186 -831
- odoo/addons/contract/i18n/hr.po +206 -837
- odoo/addons/contract/i18n/hr_HR.po +218 -839
- odoo/addons/contract/i18n/hu.po +141 -821
- odoo/addons/contract/i18n/id.po +141 -821
- odoo/addons/contract/i18n/it.po +746 -900
- odoo/addons/contract/i18n/ja.po +141 -821
- odoo/addons/contract/i18n/ko.po +141 -821
- odoo/addons/contract/i18n/lt.po +141 -821
- odoo/addons/contract/i18n/lt_LT.po +141 -821
- odoo/addons/contract/i18n/lv.po +141 -821
- odoo/addons/contract/i18n/mk.po +141 -821
- odoo/addons/contract/i18n/mn.po +141 -821
- odoo/addons/contract/i18n/nb.po +141 -821
- odoo/addons/contract/i18n/nb_NO.po +141 -821
- odoo/addons/contract/i18n/nl.po +694 -953
- odoo/addons/contract/i18n/nl_BE.po +141 -821
- odoo/addons/contract/i18n/nl_NL.po +186 -831
- odoo/addons/contract/i18n/pl.po +141 -821
- odoo/addons/contract/i18n/pt.po +410 -839
- odoo/addons/contract/i18n/pt_BR.po +701 -949
- odoo/addons/contract/i18n/pt_PT.po +141 -821
- odoo/addons/contract/i18n/ro.po +141 -821
- odoo/addons/contract/i18n/ru.po +186 -831
- odoo/addons/contract/i18n/sk.po +141 -821
- odoo/addons/contract/i18n/sk_SK.po +141 -821
- odoo/addons/contract/i18n/sl.po +141 -821
- odoo/addons/contract/i18n/sr.po +141 -821
- odoo/addons/contract/i18n/sr@latin.po +141 -821
- odoo/addons/contract/i18n/sv.po +780 -934
- odoo/addons/contract/i18n/th.po +141 -821
- odoo/addons/contract/i18n/tr.po +556 -877
- odoo/addons/contract/i18n/tr_TR.po +216 -838
- odoo/addons/contract/i18n/uk.po +141 -821
- odoo/addons/contract/i18n/vi.po +141 -821
- odoo/addons/contract/i18n/vi_VN.po +141 -821
- odoo/addons/contract/i18n/zh_CN.po +407 -845
- odoo/addons/contract/i18n/zh_TW.po +145 -822
- odoo/addons/contract/migrations/18.0.2.0.0/end-migrate.py +27 -0
- odoo/addons/contract/migrations/18.0.2.0.0/pre-migrate.py +94 -0
- odoo/addons/contract/models/__init__.py +2 -6
- odoo/addons/contract/models/account_move.py +0 -8
- odoo/addons/contract/models/account_move_line.py +14 -0
- odoo/addons/contract/models/contract.py +272 -308
- odoo/addons/contract/models/contract_line.py +37 -859
- odoo/addons/contract/models/{contract_recurrency_mixin.py → contract_recurring_mixin.py} +101 -82
- odoo/addons/contract/models/contract_tag.py +1 -3
- odoo/addons/contract/models/contract_template.py +81 -2
- odoo/addons/contract/models/contract_template_line.py +250 -3
- odoo/addons/contract/report/contract_views.xml +0 -2
- odoo/addons/contract/report/report_contract.xml +13 -13
- odoo/addons/contract/security/contract_security.xml +6 -15
- odoo/addons/contract/security/contract_tag.xml +1 -3
- odoo/addons/contract/security/ir.model.access.csv +0 -2
- odoo/addons/contract/static/description/index.html +24 -18
- odoo/addons/contract/static/src/js/contract_portal_tour.esm.js +6 -4
- odoo/addons/contract/tests/test_contract.py +82 -928
- odoo/addons/contract/tests/test_multicompany.py +5 -4
- odoo/addons/contract/tests/test_portal.py +6 -3
- odoo/addons/contract/views/contract.xml +92 -235
- odoo/addons/contract/views/contract_line.xml +48 -117
- odoo/addons/contract/views/contract_portal_templates.xml +181 -222
- odoo/addons/contract/views/contract_tag.xml +3 -3
- odoo/addons/contract/views/contract_template.xml +100 -72
- odoo/addons/contract/views/contract_template_line.xml +76 -5
- odoo/addons/contract/views/res_config_settings.xml +5 -6
- odoo/addons/contract/views/res_partner_view.xml +0 -5
- odoo/addons/contract/wizards/__init__.py +0 -2
- odoo/addons/contract/wizards/contract_manually_create_invoice.py +6 -6
- odoo/addons/contract/wizards/contract_manually_create_invoice.xml +2 -3
- {odoo_addon_contract-17.0.1.4.4.dist-info → odoo_addon_contract-18.0.2.0.8.dist-info}/METADATA +18 -13
- odoo_addon_contract-18.0.2.0.8.dist-info/RECORD +132 -0
- {odoo_addon_contract-17.0.1.4.4.dist-info → odoo_addon_contract-18.0.2.0.8.dist-info}/WHEEL +1 -1
- odoo/addons/contract/data/contract_renew_cron.xml +0 -14
- odoo/addons/contract/models/abstract_contract.py +0 -82
- odoo/addons/contract/models/abstract_contract_line.py +0 -271
- odoo/addons/contract/models/contract_line_constraints.py +0 -429
- odoo/addons/contract/models/contract_terminate_reason.py +0 -14
- odoo/addons/contract/models/res_company.py +0 -15
- odoo/addons/contract/models/res_config_settings.py +0 -18
- odoo/addons/contract/security/contract_terminate_reason.xml +0 -23
- odoo/addons/contract/security/groups.xml +0 -9
- odoo/addons/contract/views/abstract_contract_line.xml +0 -117
- odoo/addons/contract/views/contract_terminate_reason.xml +0 -38
- odoo/addons/contract/wizards/contract_contract_terminate.py +0 -42
- odoo/addons/contract/wizards/contract_contract_terminate.xml +0 -33
- odoo/addons/contract/wizards/contract_line_wizard.py +0 -53
- odoo/addons/contract/wizards/contract_line_wizard.xml +0 -111
- odoo_addon_contract-17.0.1.4.4.dist-info/RECORD +0 -144
- {odoo_addon_contract-17.0.1.4.4.dist-info → odoo_addon_contract-18.0.2.0.8.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
# Copyright
|
|
2
|
-
# Copyright
|
|
1
|
+
# Copyright 2004-2010 OpenERP SA
|
|
2
|
+
# Copyright 2014 Angel Moya <angel.moya@domatix.com>
|
|
3
|
+
# Copyright 2015-2020 Tecnativa - Pedro M. Baeza
|
|
4
|
+
# Copyright 2016-2018 Tecnativa - Carlos Dauden
|
|
5
|
+
# Copyright 2016-2017 LasLabs Inc.
|
|
6
|
+
# Copyright 2018 ACSONE SA/NV
|
|
7
|
+
# Copyright 2021 Tecnativa - Víctor Martínez
|
|
3
8
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
4
9
|
|
|
5
10
|
from dateutil.relativedelta import relativedelta
|
|
@@ -7,9 +12,23 @@ from dateutil.relativedelta import relativedelta
|
|
|
7
12
|
from odoo import api, fields, models
|
|
8
13
|
|
|
9
14
|
|
|
10
|
-
class
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
class ContractRecurringMixin(models.AbstractModel):
|
|
16
|
+
"""Abstract model to support recurring invoicing logic."""
|
|
17
|
+
|
|
18
|
+
_name = "contract.recurring.mixin"
|
|
19
|
+
_description = "Contract Recurring Mixin"
|
|
20
|
+
|
|
21
|
+
date_start = fields.Date(
|
|
22
|
+
index=True,
|
|
23
|
+
default=lambda self: fields.Date.context_today(self),
|
|
24
|
+
help="Contract activation date (first recurrence starts here)",
|
|
25
|
+
)
|
|
26
|
+
date_end = fields.Date(
|
|
27
|
+
index=True, help="Optional contract termination date (limits recurrence)"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# === Recurrence Rule Fields ===
|
|
31
|
+
# Define how often the contract recurs (e.g., monthly, yearly) and the interval.
|
|
13
32
|
|
|
14
33
|
recurring_rule_type = fields.Selection(
|
|
15
34
|
[
|
|
@@ -23,7 +42,12 @@ class ContractRecurrencyBasicMixin(models.AbstractModel):
|
|
|
23
42
|
],
|
|
24
43
|
default="monthly",
|
|
25
44
|
string="Recurrence",
|
|
26
|
-
help="Specify
|
|
45
|
+
help="Specify interval for automatic invoice generation.",
|
|
46
|
+
)
|
|
47
|
+
recurring_interval = fields.Integer(
|
|
48
|
+
default=1,
|
|
49
|
+
string="Invoice Every",
|
|
50
|
+
help="Invoice every (Days/Week/Month/Year)",
|
|
27
51
|
)
|
|
28
52
|
recurring_invoicing_type = fields.Selection(
|
|
29
53
|
[("pre-paid", "Pre-paid"), ("post-paid", "Post-paid")],
|
|
@@ -42,45 +66,20 @@ class ContractRecurrencyBasicMixin(models.AbstractModel):
|
|
|
42
66
|
"date (in post-paid mode) or start date (in pre-paid mode)."
|
|
43
67
|
),
|
|
44
68
|
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
string="Invoice Every",
|
|
48
|
-
help="Invoice every (Days/Week/Month/Year)",
|
|
49
|
-
)
|
|
50
|
-
date_start = fields.Date()
|
|
51
|
-
recurring_next_date = fields.Date(string="Date of Next Invoice")
|
|
52
|
-
|
|
53
|
-
@api.depends("recurring_invoicing_type", "recurring_rule_type")
|
|
54
|
-
def _compute_recurring_invoicing_offset(self):
|
|
55
|
-
for rec in self:
|
|
56
|
-
method = self._get_default_recurring_invoicing_offset
|
|
57
|
-
rec.recurring_invoicing_offset = method(
|
|
58
|
-
rec.recurring_invoicing_type, rec.recurring_rule_type
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
@api.model
|
|
62
|
-
def _get_default_recurring_invoicing_offset(
|
|
63
|
-
self, recurring_invoicing_type, recurring_rule_type
|
|
64
|
-
):
|
|
65
|
-
if (
|
|
66
|
-
recurring_invoicing_type == "pre-paid"
|
|
67
|
-
or recurring_rule_type == "monthlylastday"
|
|
68
|
-
):
|
|
69
|
-
return 0
|
|
70
|
-
else:
|
|
71
|
-
return 1
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class ContractRecurrencyMixin(models.AbstractModel):
|
|
75
|
-
_inherit = "contract.recurrency.basic.mixin"
|
|
76
|
-
_name = "contract.recurrency.mixin"
|
|
77
|
-
_description = "Recurrency mixin for contract models"
|
|
69
|
+
# === Invoicing Configuration Fields ===
|
|
70
|
+
# Define when and how invoices should be issued within the recurrence.
|
|
78
71
|
|
|
79
|
-
|
|
72
|
+
last_date_invoiced = fields.Date(
|
|
73
|
+
readonly=True,
|
|
74
|
+
copy=False,
|
|
75
|
+
)
|
|
80
76
|
recurring_next_date = fields.Date(
|
|
81
|
-
|
|
77
|
+
string="Date of Next Invoice",
|
|
78
|
+
compute="_compute_recurring_next_date",
|
|
79
|
+
store=True,
|
|
80
|
+
readonly=False,
|
|
81
|
+
copy=True,
|
|
82
82
|
)
|
|
83
|
-
date_end = fields.Date(index=True)
|
|
84
83
|
next_period_date_start = fields.Date(
|
|
85
84
|
string="Next Period Start",
|
|
86
85
|
compute="_compute_next_period_date_start",
|
|
@@ -89,22 +88,10 @@ class ContractRecurrencyMixin(models.AbstractModel):
|
|
|
89
88
|
string="Next Period End",
|
|
90
89
|
compute="_compute_next_period_date_end",
|
|
91
90
|
)
|
|
92
|
-
last_date_invoiced = fields.Date(readonly=True, copy=False)
|
|
93
|
-
|
|
94
|
-
@api.depends("next_period_date_start")
|
|
95
|
-
def _compute_recurring_next_date(self):
|
|
96
|
-
for rec in self:
|
|
97
|
-
rec.recurring_next_date = self.get_next_invoice_date(
|
|
98
|
-
rec.next_period_date_start,
|
|
99
|
-
rec.recurring_invoicing_type,
|
|
100
|
-
rec.recurring_invoicing_offset,
|
|
101
|
-
rec.recurring_rule_type,
|
|
102
|
-
rec.recurring_interval,
|
|
103
|
-
max_date_end=rec.date_end,
|
|
104
|
-
)
|
|
105
91
|
|
|
106
92
|
@api.depends("last_date_invoiced", "date_start", "date_end")
|
|
107
93
|
def _compute_next_period_date_start(self):
|
|
94
|
+
"""Compute the start date of the next billing period."""
|
|
108
95
|
for rec in self:
|
|
109
96
|
if rec.last_date_invoiced:
|
|
110
97
|
next_period_date_start = rec.last_date_invoiced + relativedelta(days=1)
|
|
@@ -128,6 +115,7 @@ class ContractRecurrencyMixin(models.AbstractModel):
|
|
|
128
115
|
"recurring_next_date",
|
|
129
116
|
)
|
|
130
117
|
def _compute_next_period_date_end(self):
|
|
118
|
+
"""Compute the end date of the next billing period."""
|
|
131
119
|
for rec in self:
|
|
132
120
|
rec.next_period_date_end = self.get_next_period_date_end(
|
|
133
121
|
rec.next_period_date_start,
|
|
@@ -139,13 +127,40 @@ class ContractRecurrencyMixin(models.AbstractModel):
|
|
|
139
127
|
recurring_invoicing_offset=rec.recurring_invoicing_offset,
|
|
140
128
|
)
|
|
141
129
|
|
|
130
|
+
@api.depends("recurring_invoicing_type", "recurring_rule_type")
|
|
131
|
+
def _compute_recurring_invoicing_offset(self):
|
|
132
|
+
"""Compute the invoicing offset based on type and rule."""
|
|
133
|
+
for rec in self:
|
|
134
|
+
method = self._get_default_recurring_invoicing_offset
|
|
135
|
+
rec.recurring_invoicing_offset = method(
|
|
136
|
+
rec.recurring_invoicing_type, rec.recurring_rule_type
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@api.depends(
|
|
140
|
+
"next_period_date_start",
|
|
141
|
+
"recurring_invoicing_type",
|
|
142
|
+
"recurring_invoicing_offset",
|
|
143
|
+
"recurring_rule_type",
|
|
144
|
+
"recurring_interval",
|
|
145
|
+
"date_end",
|
|
146
|
+
)
|
|
147
|
+
def _compute_recurring_next_date(self):
|
|
148
|
+
"""Compute the next invoice date."""
|
|
149
|
+
for rec in self:
|
|
150
|
+
rec.recurring_next_date = self.get_next_invoice_date(
|
|
151
|
+
rec.next_period_date_start,
|
|
152
|
+
rec.recurring_invoicing_type,
|
|
153
|
+
rec.recurring_invoicing_offset,
|
|
154
|
+
rec.recurring_rule_type,
|
|
155
|
+
rec.recurring_interval,
|
|
156
|
+
max_date_end=rec.date_end,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# === Utility Methods ===
|
|
160
|
+
|
|
142
161
|
@api.model
|
|
143
162
|
def get_relative_delta(self, recurring_rule_type, interval):
|
|
144
|
-
"""Return a relativedelta for one period.
|
|
145
|
-
|
|
146
|
-
When added to the first day of the period,
|
|
147
|
-
it gives the first day of the next period.
|
|
148
|
-
"""
|
|
163
|
+
"""Return a relativedelta for one period based on rule type."""
|
|
149
164
|
if recurring_rule_type == "daily":
|
|
150
165
|
return relativedelta(days=interval)
|
|
151
166
|
elif recurring_rule_type == "weekly":
|
|
@@ -158,7 +173,7 @@ class ContractRecurrencyMixin(models.AbstractModel):
|
|
|
158
173
|
return relativedelta(months=3 * interval)
|
|
159
174
|
elif recurring_rule_type == "semesterly":
|
|
160
175
|
return relativedelta(months=6 * interval)
|
|
161
|
-
else:
|
|
176
|
+
else: # yearly
|
|
162
177
|
return relativedelta(years=interval)
|
|
163
178
|
|
|
164
179
|
@api.model
|
|
@@ -172,28 +187,21 @@ class ContractRecurrencyMixin(models.AbstractModel):
|
|
|
172
187
|
recurring_invoicing_type=False,
|
|
173
188
|
recurring_invoicing_offset=False,
|
|
174
189
|
):
|
|
175
|
-
"""Compute the end date for the next period.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
which case this method can adjust the next period based on that
|
|
180
|
-
too. In that scenario it required the invoicing type and offset
|
|
181
|
-
arguments.
|
|
182
|
-
"""
|
|
183
|
-
if not next_period_date_start:
|
|
184
|
-
return False
|
|
185
|
-
if max_date_end and next_period_date_start > max_date_end:
|
|
186
|
-
# start is past max date end: there is no next period
|
|
190
|
+
"""Compute the end date for the next period."""
|
|
191
|
+
if not next_period_date_start or (
|
|
192
|
+
max_date_end and next_period_date_start > max_date_end
|
|
193
|
+
):
|
|
187
194
|
return False
|
|
195
|
+
|
|
188
196
|
if not next_invoice_date:
|
|
189
|
-
#
|
|
197
|
+
# Regular case: use relative delta
|
|
190
198
|
next_period_date_end = (
|
|
191
199
|
next_period_date_start
|
|
192
200
|
+ self.get_relative_delta(recurring_rule_type, recurring_interval)
|
|
193
201
|
- relativedelta(days=1)
|
|
194
202
|
)
|
|
195
203
|
else:
|
|
196
|
-
#
|
|
204
|
+
# Forced invoice date: back-calculate period end
|
|
197
205
|
if recurring_invoicing_type == "pre-paid":
|
|
198
206
|
next_period_date_end = (
|
|
199
207
|
next_invoice_date
|
|
@@ -205,8 +213,8 @@ class ContractRecurrencyMixin(models.AbstractModel):
|
|
|
205
213
|
next_period_date_end = next_invoice_date - relativedelta(
|
|
206
214
|
days=recurring_invoicing_offset
|
|
207
215
|
)
|
|
216
|
+
|
|
208
217
|
if max_date_end and next_period_date_end > max_date_end:
|
|
209
|
-
# end date is past max_date_end: trim it
|
|
210
218
|
next_period_date_end = max_date_end
|
|
211
219
|
return next_period_date_end
|
|
212
220
|
|
|
@@ -220,6 +228,7 @@ class ContractRecurrencyMixin(models.AbstractModel):
|
|
|
220
228
|
recurring_interval,
|
|
221
229
|
max_date_end,
|
|
222
230
|
):
|
|
231
|
+
"""Compute the date of the next invoice based on all parameters."""
|
|
223
232
|
next_period_date_end = self.get_next_period_date_end(
|
|
224
233
|
next_period_date_start,
|
|
225
234
|
recurring_rule_type,
|
|
@@ -228,12 +237,22 @@ class ContractRecurrencyMixin(models.AbstractModel):
|
|
|
228
237
|
)
|
|
229
238
|
if not next_period_date_end:
|
|
230
239
|
return False
|
|
240
|
+
|
|
231
241
|
if recurring_invoicing_type == "pre-paid":
|
|
232
|
-
|
|
233
|
-
days=recurring_invoicing_offset
|
|
234
|
-
)
|
|
235
|
-
else: # post-paid
|
|
236
|
-
recurring_next_date = next_period_date_end + relativedelta(
|
|
242
|
+
return next_period_date_start + relativedelta(
|
|
237
243
|
days=recurring_invoicing_offset
|
|
238
244
|
)
|
|
239
|
-
|
|
245
|
+
else:
|
|
246
|
+
return next_period_date_end + relativedelta(days=recurring_invoicing_offset)
|
|
247
|
+
|
|
248
|
+
@api.model
|
|
249
|
+
def _get_default_recurring_invoicing_offset(
|
|
250
|
+
self, recurring_invoicing_type, recurring_rule_type
|
|
251
|
+
):
|
|
252
|
+
"""Return default offset in days based on invoicing type and rule."""
|
|
253
|
+
if (
|
|
254
|
+
recurring_invoicing_type == "pre-paid"
|
|
255
|
+
or recurring_rule_type == "monthlylastday"
|
|
256
|
+
):
|
|
257
|
+
return 0
|
|
258
|
+
return 1
|
|
@@ -10,8 +10,6 @@ class ContractTag(models.Model):
|
|
|
10
10
|
|
|
11
11
|
name = fields.Char(required=True)
|
|
12
12
|
company_id = fields.Many2one(
|
|
13
|
-
"res.company",
|
|
14
|
-
string="Company",
|
|
15
|
-
default=lambda self: self.env.company.id,
|
|
13
|
+
"res.company", string="Company", default=lambda self: self.env.company
|
|
16
14
|
)
|
|
17
15
|
color = fields.Integer("Color Index", default=0)
|
|
@@ -6,13 +6,66 @@
|
|
|
6
6
|
# Copyright 2018 ACSONE SA/NV
|
|
7
7
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
8
8
|
|
|
9
|
-
from odoo import fields, models
|
|
9
|
+
from odoo import api, fields, models
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class ContractTemplate(models.Model):
|
|
13
13
|
_name = "contract.template"
|
|
14
|
-
_inherit = "contract.abstract.contract"
|
|
15
14
|
_description = "Contract Template"
|
|
15
|
+
_inherit = "contract.recurring.mixin"
|
|
16
|
+
_check_company_auto = True
|
|
17
|
+
|
|
18
|
+
# Fields not synced to the actual contract
|
|
19
|
+
NO_SYNC = ["name", "partner_id", "company_id"]
|
|
20
|
+
|
|
21
|
+
# === Basic Info ===
|
|
22
|
+
|
|
23
|
+
name = fields.Char(required=True)
|
|
24
|
+
partner_id = fields.Many2one(
|
|
25
|
+
comodel_name="res.partner",
|
|
26
|
+
string="Partner",
|
|
27
|
+
index=True,
|
|
28
|
+
)
|
|
29
|
+
company_id = fields.Many2one(
|
|
30
|
+
comodel_name="res.company",
|
|
31
|
+
string="Company",
|
|
32
|
+
required=True,
|
|
33
|
+
default=lambda self: self.env.company,
|
|
34
|
+
)
|
|
35
|
+
pricelist_id = fields.Many2one(
|
|
36
|
+
comodel_name="product.pricelist",
|
|
37
|
+
string="Pricelist",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# === Contract Settings ===
|
|
41
|
+
|
|
42
|
+
contract_type = fields.Selection(
|
|
43
|
+
selection=[("sale", "Customer"), ("purchase", "Supplier")],
|
|
44
|
+
default="sale",
|
|
45
|
+
index=True,
|
|
46
|
+
)
|
|
47
|
+
line_recurrence = fields.Boolean(
|
|
48
|
+
string="Recurrence at line level?",
|
|
49
|
+
help="Check this if you want to control recurrence at the line level "
|
|
50
|
+
"instead of for the whole contract.",
|
|
51
|
+
)
|
|
52
|
+
generation_type = fields.Selection(
|
|
53
|
+
selection=[("invoice", "Invoice")],
|
|
54
|
+
default=lambda self: self._default_generation_type(),
|
|
55
|
+
help="Defines what document is automatically generated by the cron.",
|
|
56
|
+
)
|
|
57
|
+
journal_id = fields.Many2one(
|
|
58
|
+
comodel_name="account.journal",
|
|
59
|
+
string="Journal",
|
|
60
|
+
domain="[('type', '=', contract_type)]",
|
|
61
|
+
compute="_compute_journal_id",
|
|
62
|
+
store=True,
|
|
63
|
+
readonly=False,
|
|
64
|
+
index=True,
|
|
65
|
+
check_company=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# === Contract Line Templates ===
|
|
16
69
|
|
|
17
70
|
contract_line_ids = fields.One2many(
|
|
18
71
|
comodel_name="contract.template.line",
|
|
@@ -20,3 +73,29 @@ class ContractTemplate(models.Model):
|
|
|
20
73
|
copy=True,
|
|
21
74
|
string="Contract template lines",
|
|
22
75
|
)
|
|
76
|
+
|
|
77
|
+
def _get_valid_journal_type(self):
|
|
78
|
+
self.ensure_one()
|
|
79
|
+
if self.contract_type == "sale":
|
|
80
|
+
return ["sale"]
|
|
81
|
+
elif self.contract_type == "purchase":
|
|
82
|
+
return ["purchase"]
|
|
83
|
+
|
|
84
|
+
@api.model
|
|
85
|
+
def _default_generation_type(self):
|
|
86
|
+
"""Default generation type for the contract."""
|
|
87
|
+
return "invoice"
|
|
88
|
+
|
|
89
|
+
@api.depends("contract_type", "company_id")
|
|
90
|
+
def _compute_journal_id(self):
|
|
91
|
+
"""Auto-select a journal based on contract type and company."""
|
|
92
|
+
AccountJournal = self.env["account.journal"]
|
|
93
|
+
for contract in self:
|
|
94
|
+
# See Odoo account_move._search_default_journal()
|
|
95
|
+
company = contract.company_id or contract.env.company
|
|
96
|
+
domain = [
|
|
97
|
+
*self.env["account.journal"]._check_company_domain(company),
|
|
98
|
+
("type", "in", contract._get_valid_journal_type()),
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
contract.journal_id = AccountJournal.search(domain, limit=1).id or None
|
|
@@ -6,18 +6,265 @@
|
|
|
6
6
|
# Copyright 2018 ACSONE SA/NV
|
|
7
7
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
8
8
|
|
|
9
|
-
from odoo import fields, models
|
|
9
|
+
from odoo import api, fields, models
|
|
10
|
+
from odoo.exceptions import ValidationError
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class ContractTemplateLine(models.Model):
|
|
13
14
|
_name = "contract.template.line"
|
|
14
|
-
_inherit = "contract.
|
|
15
|
+
_inherit = "contract.recurring.mixin"
|
|
15
16
|
_description = "Contract Template Line"
|
|
16
17
|
_order = "sequence,id"
|
|
17
18
|
|
|
19
|
+
sequence = fields.Integer(
|
|
20
|
+
default=10,
|
|
21
|
+
help="Defines line ordering in the contract.",
|
|
22
|
+
)
|
|
18
23
|
contract_id = fields.Many2one(
|
|
19
|
-
string="Contract",
|
|
20
24
|
comodel_name="contract.template",
|
|
25
|
+
string="Contract",
|
|
21
26
|
required=True,
|
|
22
27
|
ondelete="cascade",
|
|
23
28
|
)
|
|
29
|
+
company_id = fields.Many2one(
|
|
30
|
+
related="contract_id.company_id", store=True, readonly=True
|
|
31
|
+
)
|
|
32
|
+
partner_id = fields.Many2one(
|
|
33
|
+
comodel_name="res.partner", related="contract_id.partner_id"
|
|
34
|
+
)
|
|
35
|
+
# === Product & UOM ===
|
|
36
|
+
product_id = fields.Many2one("product.product", string="Product")
|
|
37
|
+
name = fields.Text(
|
|
38
|
+
string="Description",
|
|
39
|
+
required=True,
|
|
40
|
+
compute="_compute_name",
|
|
41
|
+
store=True,
|
|
42
|
+
readonly=False,
|
|
43
|
+
)
|
|
44
|
+
quantity = fields.Float(default=1.0, required=True)
|
|
45
|
+
product_uom_category_id = fields.Many2one(
|
|
46
|
+
comodel_name="uom.category",
|
|
47
|
+
related="product_id.uom_id.category_id",
|
|
48
|
+
readonly=True,
|
|
49
|
+
)
|
|
50
|
+
uom_id = fields.Many2one(
|
|
51
|
+
comodel_name="uom.uom",
|
|
52
|
+
compute="_compute_uom_id",
|
|
53
|
+
store=True,
|
|
54
|
+
readonly=False,
|
|
55
|
+
string="Unit of Measure",
|
|
56
|
+
domain="[('category_id', '=', product_uom_category_id)]",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# === Pricing ===
|
|
60
|
+
|
|
61
|
+
automatic_price = fields.Boolean(
|
|
62
|
+
string="Auto-price?",
|
|
63
|
+
compute="_compute_automatic_price",
|
|
64
|
+
store=True,
|
|
65
|
+
readonly=False,
|
|
66
|
+
help=(
|
|
67
|
+
"If checked, the price will be taken from the pricelist. "
|
|
68
|
+
"Otherwise, it must be set manually."
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
specific_price = fields.Float()
|
|
72
|
+
price_unit = fields.Float(
|
|
73
|
+
string="Unit Price",
|
|
74
|
+
compute="_compute_price_unit",
|
|
75
|
+
inverse="_inverse_price_unit",
|
|
76
|
+
)
|
|
77
|
+
currency_id = fields.Many2one(
|
|
78
|
+
"res.currency"
|
|
79
|
+
) # Placeholder, overwritten in contract.line
|
|
80
|
+
price_subtotal = fields.Monetary(
|
|
81
|
+
string="Sub Total",
|
|
82
|
+
compute="_compute_price_subtotal",
|
|
83
|
+
)
|
|
84
|
+
discount = fields.Float(
|
|
85
|
+
string="Discount (%)",
|
|
86
|
+
digits="Discount",
|
|
87
|
+
help="Discount to apply on generated invoices. Must be ≤ 100.",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# === Recurrence Configuration ===
|
|
91
|
+
|
|
92
|
+
is_canceled = fields.Boolean(string="Canceled", default=False)
|
|
93
|
+
|
|
94
|
+
# === Display / Notes ===
|
|
95
|
+
|
|
96
|
+
display_type = fields.Selection(
|
|
97
|
+
selection=[("line_section", "Section"), ("line_note", "Note")],
|
|
98
|
+
default=False,
|
|
99
|
+
help="Technical field for UX purposes.",
|
|
100
|
+
)
|
|
101
|
+
note_invoicing_mode = fields.Selection(
|
|
102
|
+
selection=[
|
|
103
|
+
("with_previous_line", "With previous line"),
|
|
104
|
+
("with_next_line", "With next line"),
|
|
105
|
+
("custom", "Custom"),
|
|
106
|
+
],
|
|
107
|
+
default="with_previous_line",
|
|
108
|
+
help="When to invoice this note line relative to others.",
|
|
109
|
+
)
|
|
110
|
+
is_recurring_note = fields.Boolean(
|
|
111
|
+
compute="_compute_is_recurring_note",
|
|
112
|
+
string="Recurring Note",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# === Line-Level Recurrence Fields (computed from contract or local) ===
|
|
116
|
+
|
|
117
|
+
recurring_rule_type = fields.Selection(
|
|
118
|
+
compute="_compute_recurring_rule_type",
|
|
119
|
+
store=True,
|
|
120
|
+
readonly=False,
|
|
121
|
+
required=True,
|
|
122
|
+
copy=True,
|
|
123
|
+
)
|
|
124
|
+
recurring_invoicing_type = fields.Selection(
|
|
125
|
+
compute="_compute_recurring_invoicing_type",
|
|
126
|
+
store=True,
|
|
127
|
+
readonly=False,
|
|
128
|
+
required=True,
|
|
129
|
+
copy=True,
|
|
130
|
+
)
|
|
131
|
+
recurring_interval = fields.Integer(
|
|
132
|
+
compute="_compute_recurring_interval",
|
|
133
|
+
store=True,
|
|
134
|
+
readonly=False,
|
|
135
|
+
required=True,
|
|
136
|
+
copy=True,
|
|
137
|
+
)
|
|
138
|
+
date_start = fields.Date(
|
|
139
|
+
compute="_compute_date_start",
|
|
140
|
+
store=True,
|
|
141
|
+
readonly=False,
|
|
142
|
+
copy=True,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
@api.depends("product_id")
|
|
146
|
+
def _compute_name(self):
|
|
147
|
+
for line in self:
|
|
148
|
+
if line.product_id:
|
|
149
|
+
partner = line.contract_id.partner_id or line.env.user.partner_id
|
|
150
|
+
product = line.product_id.with_context(
|
|
151
|
+
lang=partner.lang,
|
|
152
|
+
partner=partner.id,
|
|
153
|
+
)
|
|
154
|
+
line.name = product.get_product_multiline_description_sale()
|
|
155
|
+
|
|
156
|
+
@api.depends("product_id")
|
|
157
|
+
def _compute_uom_id(self):
|
|
158
|
+
for line in self:
|
|
159
|
+
if not line.uom_id or (
|
|
160
|
+
line.product_id.uom_id.category_id.id != line.uom_id.category_id.id
|
|
161
|
+
):
|
|
162
|
+
line.uom_id = line.product_id.uom_id
|
|
163
|
+
|
|
164
|
+
@api.depends("contract_id.contract_type")
|
|
165
|
+
def _compute_automatic_price(self):
|
|
166
|
+
"""Reset automatic price if contract is switched to 'purchase'."""
|
|
167
|
+
self.filtered(
|
|
168
|
+
lambda line: line.contract_id.contract_type == "purchase"
|
|
169
|
+
and line.automatic_price
|
|
170
|
+
).automatic_price = False
|
|
171
|
+
|
|
172
|
+
@api.depends("display_type", "note_invoicing_mode")
|
|
173
|
+
def _compute_is_recurring_note(self):
|
|
174
|
+
for record in self:
|
|
175
|
+
record.is_recurring_note = (
|
|
176
|
+
record.display_type == "line_note"
|
|
177
|
+
and record.note_invoicing_mode == "custom"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
@api.depends(
|
|
181
|
+
"automatic_price",
|
|
182
|
+
"specific_price",
|
|
183
|
+
"product_id",
|
|
184
|
+
"quantity",
|
|
185
|
+
"contract_id.pricelist_id",
|
|
186
|
+
"contract_id.partner_id",
|
|
187
|
+
)
|
|
188
|
+
def _compute_price_unit(self):
|
|
189
|
+
for line in self:
|
|
190
|
+
if line.automatic_price and line.product_id:
|
|
191
|
+
pricelist = (
|
|
192
|
+
line.contract_id.pricelist_id
|
|
193
|
+
or line.contract_id.partner_id.with_company(
|
|
194
|
+
line.contract_id.company_id
|
|
195
|
+
).property_product_pricelist
|
|
196
|
+
)
|
|
197
|
+
qty = line.env.context.get("contract_line_qty", line.quantity)
|
|
198
|
+
product = line.product_id.with_context(
|
|
199
|
+
quantity=qty,
|
|
200
|
+
pricelist=pricelist.id,
|
|
201
|
+
partner=line.contract_id.partner_id.id,
|
|
202
|
+
uom=line.uom_id.id,
|
|
203
|
+
date=line.env.context.get(
|
|
204
|
+
"old_date", fields.Date.context_today(line)
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
line.price_unit = pricelist._get_product_price(product, quantity=qty)
|
|
208
|
+
else:
|
|
209
|
+
line.price_unit = line.specific_price
|
|
210
|
+
|
|
211
|
+
# Tip in https://github.com/odoo/odoo/issues/23891#issuecomment-376910788
|
|
212
|
+
@api.onchange("price_unit")
|
|
213
|
+
def _inverse_price_unit(self):
|
|
214
|
+
for line in self.filtered(lambda x: not x.automatic_price):
|
|
215
|
+
line.specific_price = line.price_unit
|
|
216
|
+
|
|
217
|
+
@api.depends("quantity", "price_unit", "discount")
|
|
218
|
+
def _compute_price_subtotal(self):
|
|
219
|
+
for line in self:
|
|
220
|
+
subtotal = line.quantity * line.price_unit
|
|
221
|
+
subtotal *= 1 - (line.discount / 100)
|
|
222
|
+
cur = (
|
|
223
|
+
line.contract_id.pricelist_id.currency_id
|
|
224
|
+
if line.contract_id.pricelist_id
|
|
225
|
+
else None
|
|
226
|
+
)
|
|
227
|
+
line.price_subtotal = cur.round(subtotal) if cur else subtotal
|
|
228
|
+
|
|
229
|
+
# === Recurrence Field Synchronization ===
|
|
230
|
+
|
|
231
|
+
def _set_recurrence_field(self, field):
|
|
232
|
+
"""Sync recurrence field from header or keep local depending on config."""
|
|
233
|
+
for record in self:
|
|
234
|
+
record[field] = (
|
|
235
|
+
record[field]
|
|
236
|
+
if record.contract_id.line_recurrence
|
|
237
|
+
else record.contract_id[field]
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
@api.depends("contract_id.recurring_rule_type", "contract_id.line_recurrence")
|
|
241
|
+
def _compute_recurring_rule_type(self):
|
|
242
|
+
self._set_recurrence_field("recurring_rule_type")
|
|
243
|
+
|
|
244
|
+
@api.depends("contract_id.recurring_invoicing_type", "contract_id.line_recurrence")
|
|
245
|
+
def _compute_recurring_invoicing_type(self):
|
|
246
|
+
self._set_recurrence_field("recurring_invoicing_type")
|
|
247
|
+
|
|
248
|
+
@api.depends("contract_id.recurring_interval", "contract_id.line_recurrence")
|
|
249
|
+
def _compute_recurring_interval(self):
|
|
250
|
+
self._set_recurrence_field("recurring_interval")
|
|
251
|
+
|
|
252
|
+
@api.depends("contract_id.date_start", "contract_id.line_recurrence")
|
|
253
|
+
def _compute_date_start(self):
|
|
254
|
+
self._set_recurrence_field("date_start")
|
|
255
|
+
|
|
256
|
+
@api.depends("contract_id.line_recurrence")
|
|
257
|
+
def _compute_recurring_next_date(self):
|
|
258
|
+
res = super()._compute_recurring_next_date()
|
|
259
|
+
self._set_recurrence_field("recurring_next_date")
|
|
260
|
+
return res
|
|
261
|
+
|
|
262
|
+
# === Constraints & Onchange ===
|
|
263
|
+
|
|
264
|
+
@api.constrains("discount")
|
|
265
|
+
def _check_discount(self):
|
|
266
|
+
for line in self:
|
|
267
|
+
if line.discount > 100:
|
|
268
|
+
raise ValidationError(
|
|
269
|
+
self.env._("Discount should be less or equal to 100")
|
|
270
|
+
)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8" ?>
|
|
2
2
|
<odoo>
|
|
3
|
-
|
|
4
3
|
<record id="report_contract" model="ir.actions.report">
|
|
5
4
|
<field name="name">Contract</field>
|
|
6
5
|
<field name="model">contract.contract</field>
|
|
@@ -10,5 +9,4 @@
|
|
|
10
9
|
<field name="binding_model_id" ref="model_contract_contract" />
|
|
11
10
|
<field name="binding_type">report</field>
|
|
12
11
|
</record>
|
|
13
|
-
|
|
14
12
|
</odoo>
|