odoo-addon-contract 17.0.1.4.3.2__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.
Files changed (130) hide show
  1. odoo/addons/contract/README.rst +14 -10
  2. odoo/addons/contract/__manifest__.py +3 -10
  3. odoo/addons/contract/controllers/main.py +1 -8
  4. odoo/addons/contract/data/contract_cron.xml +0 -2
  5. odoo/addons/contract/data/mail_template.xml +18 -17
  6. odoo/addons/contract/data/template_mail_notification.xml +1 -1
  7. odoo/addons/contract/i18n/am.po +146 -821
  8. odoo/addons/contract/i18n/ar.po +146 -821
  9. odoo/addons/contract/i18n/bg.po +146 -821
  10. odoo/addons/contract/i18n/bs.po +146 -821
  11. odoo/addons/contract/i18n/ca.po +835 -900
  12. odoo/addons/contract/i18n/ca_ES.po +146 -821
  13. odoo/addons/contract/i18n/contract.pot +145 -818
  14. odoo/addons/contract/i18n/cs.po +146 -821
  15. odoo/addons/contract/i18n/da.po +146 -821
  16. odoo/addons/contract/i18n/de.po +712 -953
  17. odoo/addons/contract/i18n/el_GR.po +146 -821
  18. odoo/addons/contract/i18n/en_GB.po +146 -821
  19. odoo/addons/contract/i18n/es.po +714 -947
  20. odoo/addons/contract/i18n/es_AR.po +550 -877
  21. odoo/addons/contract/i18n/es_CL.po +146 -821
  22. odoo/addons/contract/i18n/es_CO.po +146 -821
  23. odoo/addons/contract/i18n/es_CR.po +146 -821
  24. odoo/addons/contract/i18n/es_DO.po +146 -821
  25. odoo/addons/contract/i18n/es_EC.po +146 -821
  26. odoo/addons/contract/i18n/es_MX.po +146 -821
  27. odoo/addons/contract/i18n/es_PY.po +146 -821
  28. odoo/addons/contract/i18n/es_VE.po +146 -821
  29. odoo/addons/contract/i18n/et.po +146 -821
  30. odoo/addons/contract/i18n/eu.po +146 -821
  31. odoo/addons/contract/i18n/fa.po +146 -821
  32. odoo/addons/contract/i18n/fi.po +422 -848
  33. odoo/addons/contract/i18n/fr.po +713 -953
  34. odoo/addons/contract/i18n/fr_CA.po +146 -821
  35. odoo/addons/contract/i18n/fr_CH.po +146 -821
  36. odoo/addons/contract/i18n/fr_FR.po +454 -850
  37. odoo/addons/contract/i18n/gl.po +257 -846
  38. odoo/addons/contract/i18n/gl_ES.po +146 -821
  39. odoo/addons/contract/i18n/he.po +146 -821
  40. odoo/addons/contract/i18n/hi_IN.po +191 -831
  41. odoo/addons/contract/i18n/hr.po +211 -837
  42. odoo/addons/contract/i18n/hr_HR.po +223 -839
  43. odoo/addons/contract/i18n/hu.po +146 -821
  44. odoo/addons/contract/i18n/id.po +146 -821
  45. odoo/addons/contract/i18n/it.po +753 -902
  46. odoo/addons/contract/i18n/ja.po +146 -821
  47. odoo/addons/contract/i18n/ko.po +146 -821
  48. odoo/addons/contract/i18n/lt.po +146 -821
  49. odoo/addons/contract/i18n/lt_LT.po +146 -821
  50. odoo/addons/contract/i18n/lv.po +146 -821
  51. odoo/addons/contract/i18n/mk.po +146 -821
  52. odoo/addons/contract/i18n/mn.po +146 -821
  53. odoo/addons/contract/i18n/nb.po +146 -821
  54. odoo/addons/contract/i18n/nb_NO.po +146 -821
  55. odoo/addons/contract/i18n/nl.po +699 -953
  56. odoo/addons/contract/i18n/nl_BE.po +146 -821
  57. odoo/addons/contract/i18n/nl_NL.po +191 -831
  58. odoo/addons/contract/i18n/pl.po +146 -821
  59. odoo/addons/contract/i18n/pt.po +415 -839
  60. odoo/addons/contract/i18n/pt_BR.po +704 -947
  61. odoo/addons/contract/i18n/pt_PT.po +146 -821
  62. odoo/addons/contract/i18n/ro.po +146 -821
  63. odoo/addons/contract/i18n/ru.po +191 -831
  64. odoo/addons/contract/i18n/sk.po +146 -821
  65. odoo/addons/contract/i18n/sk_SK.po +146 -821
  66. odoo/addons/contract/i18n/sl.po +146 -821
  67. odoo/addons/contract/i18n/sr.po +146 -821
  68. odoo/addons/contract/i18n/sr@latin.po +146 -821
  69. odoo/addons/contract/i18n/sv.po +784 -933
  70. odoo/addons/contract/i18n/th.po +146 -821
  71. odoo/addons/contract/i18n/tr.po +611 -879
  72. odoo/addons/contract/i18n/tr_TR.po +221 -838
  73. odoo/addons/contract/i18n/uk.po +146 -821
  74. odoo/addons/contract/i18n/vi.po +146 -821
  75. odoo/addons/contract/i18n/vi_VN.po +146 -821
  76. odoo/addons/contract/i18n/zh_CN.po +407 -840
  77. odoo/addons/contract/i18n/zh_TW.po +150 -822
  78. odoo/addons/contract/migrations/18.0.2.0.0/end-migrate.py +27 -0
  79. odoo/addons/contract/migrations/18.0.2.0.0/pre-migrate.py +94 -0
  80. odoo/addons/contract/models/__init__.py +2 -6
  81. odoo/addons/contract/models/account_move.py +0 -8
  82. odoo/addons/contract/models/account_move_line.py +14 -0
  83. odoo/addons/contract/models/contract.py +272 -308
  84. odoo/addons/contract/models/contract_line.py +37 -859
  85. odoo/addons/contract/models/{contract_recurrency_mixin.py → contract_recurring_mixin.py} +101 -82
  86. odoo/addons/contract/models/contract_tag.py +1 -3
  87. odoo/addons/contract/models/contract_template.py +81 -2
  88. odoo/addons/contract/models/contract_template_line.py +250 -3
  89. odoo/addons/contract/report/contract_views.xml +0 -2
  90. odoo/addons/contract/report/report_contract.xml +13 -13
  91. odoo/addons/contract/security/contract_security.xml +6 -15
  92. odoo/addons/contract/security/contract_tag.xml +1 -3
  93. odoo/addons/contract/security/ir.model.access.csv +0 -2
  94. odoo/addons/contract/static/description/index.html +24 -18
  95. odoo/addons/contract/static/src/img/contract_icon.svg +4 -0
  96. odoo/addons/contract/static/src/js/contract_portal_tour.esm.js +6 -4
  97. odoo/addons/contract/tests/test_contract.py +82 -928
  98. odoo/addons/contract/tests/test_multicompany.py +5 -4
  99. odoo/addons/contract/tests/test_portal.py +6 -3
  100. odoo/addons/contract/views/contract.xml +92 -235
  101. odoo/addons/contract/views/contract_line.xml +48 -117
  102. odoo/addons/contract/views/contract_portal_templates.xml +187 -224
  103. odoo/addons/contract/views/contract_tag.xml +3 -3
  104. odoo/addons/contract/views/contract_template.xml +100 -72
  105. odoo/addons/contract/views/contract_template_line.xml +76 -5
  106. odoo/addons/contract/views/res_config_settings.xml +5 -6
  107. odoo/addons/contract/views/res_partner_view.xml +0 -5
  108. odoo/addons/contract/wizards/__init__.py +0 -2
  109. odoo/addons/contract/wizards/contract_manually_create_invoice.py +6 -6
  110. odoo/addons/contract/wizards/contract_manually_create_invoice.xml +2 -3
  111. {odoo_addon_contract-17.0.1.4.3.2.dist-info → odoo_addon_contract-18.0.2.0.8.dist-info}/METADATA +18 -13
  112. odoo_addon_contract-18.0.2.0.8.dist-info/RECORD +132 -0
  113. {odoo_addon_contract-17.0.1.4.3.2.dist-info → odoo_addon_contract-18.0.2.0.8.dist-info}/WHEEL +1 -1
  114. odoo/addons/contract/data/contract_renew_cron.xml +0 -14
  115. odoo/addons/contract/models/abstract_contract.py +0 -82
  116. odoo/addons/contract/models/abstract_contract_line.py +0 -271
  117. odoo/addons/contract/models/contract_line_constraints.py +0 -429
  118. odoo/addons/contract/models/contract_terminate_reason.py +0 -14
  119. odoo/addons/contract/models/res_company.py +0 -15
  120. odoo/addons/contract/models/res_config_settings.py +0 -18
  121. odoo/addons/contract/security/contract_terminate_reason.xml +0 -23
  122. odoo/addons/contract/security/groups.xml +0 -9
  123. odoo/addons/contract/views/abstract_contract_line.xml +0 -117
  124. odoo/addons/contract/views/contract_terminate_reason.xml +0 -38
  125. odoo/addons/contract/wizards/contract_contract_terminate.py +0 -42
  126. odoo/addons/contract/wizards/contract_contract_terminate.xml +0 -33
  127. odoo/addons/contract/wizards/contract_line_wizard.py +0 -53
  128. odoo/addons/contract/wizards/contract_line_wizard.xml +0 -111
  129. odoo_addon_contract-17.0.1.4.3.2.dist-info/RECORD +0 -143
  130. {odoo_addon_contract-17.0.1.4.3.2.dist-info → odoo_addon_contract-18.0.2.0.8.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,10 @@
1
- # Copyright 2018 ACSONE SA/NV.
2
- # Copyright 2020 Tecnativa - Pedro M. Baeza
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 ContractRecurrencyBasicMixin(models.AbstractModel):
11
- _name = "contract.recurrency.basic.mixin"
12
- _description = "Basic recurrency mixin for abstract contract models"
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 Interval for automatic invoice generation.",
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
- recurring_interval = fields.Integer(
46
- default=1,
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
- date_start = fields.Date(default=lambda self: fields.Date.context_today(self))
72
+ last_date_invoiced = fields.Date(
73
+ readonly=True,
74
+ copy=False,
75
+ )
80
76
  recurring_next_date = fields.Date(
81
- compute="_compute_recurring_next_date", store=True, readonly=False, copy=True
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
- The next period normally depends on recurrence options only.
178
- It is however possible to provide it a next invoice date, in
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
- # regular algorithm
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
- # special algorithm when the next invoice date is forced
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
- recurring_next_date = next_period_date_start + relativedelta(
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
- return recurring_next_date
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.abstract.contract.line"
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>