odoo-addon-contract 17.0.1.4.5__py3-none-any.whl → 18.0.2.0.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 (58) 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/ca.po +120 -33
  8. odoo/addons/contract/i18n/contract.pot +141 -823
  9. odoo/addons/contract/migrations/18.0.2.0.0/pre-migrate.py +90 -0
  10. odoo/addons/contract/models/__init__.py +2 -6
  11. odoo/addons/contract/models/account_move.py +0 -8
  12. odoo/addons/contract/models/account_move_line.py +14 -0
  13. odoo/addons/contract/models/contract.py +266 -308
  14. odoo/addons/contract/models/contract_line.py +34 -861
  15. odoo/addons/contract/models/{contract_recurrency_mixin.py → contract_recurring_mixin.py} +101 -82
  16. odoo/addons/contract/models/contract_tag.py +1 -3
  17. odoo/addons/contract/models/contract_template.py +81 -2
  18. odoo/addons/contract/models/contract_template_line.py +249 -3
  19. odoo/addons/contract/report/contract_views.xml +0 -2
  20. odoo/addons/contract/report/report_contract.xml +13 -13
  21. odoo/addons/contract/security/contract_security.xml +6 -15
  22. odoo/addons/contract/security/contract_tag.xml +1 -3
  23. odoo/addons/contract/security/ir.model.access.csv +0 -2
  24. odoo/addons/contract/static/description/index.html +24 -18
  25. odoo/addons/contract/static/src/js/contract_portal_tour.esm.js +6 -3
  26. odoo/addons/contract/tests/test_contract.py +42 -927
  27. odoo/addons/contract/tests/test_multicompany.py +5 -4
  28. odoo/addons/contract/tests/test_portal.py +6 -3
  29. odoo/addons/contract/views/contract.xml +91 -234
  30. odoo/addons/contract/views/contract_line.xml +48 -117
  31. odoo/addons/contract/views/contract_portal_templates.xml +181 -222
  32. odoo/addons/contract/views/contract_tag.xml +3 -3
  33. odoo/addons/contract/views/contract_template.xml +100 -72
  34. odoo/addons/contract/views/contract_template_line.xml +76 -5
  35. odoo/addons/contract/views/res_config_settings.xml +5 -6
  36. odoo/addons/contract/views/res_partner_view.xml +0 -5
  37. odoo/addons/contract/wizards/__init__.py +0 -2
  38. odoo/addons/contract/wizards/contract_manually_create_invoice.py +6 -6
  39. odoo/addons/contract/wizards/contract_manually_create_invoice.xml +2 -3
  40. {odoo_addon_contract-17.0.1.4.5.dist-info → odoo_addon_contract-18.0.2.0.0.8.dist-info}/METADATA +17 -13
  41. {odoo_addon_contract-17.0.1.4.5.dist-info → odoo_addon_contract-18.0.2.0.0.8.dist-info}/RECORD +43 -56
  42. odoo/addons/contract/data/contract_renew_cron.xml +0 -14
  43. odoo/addons/contract/models/abstract_contract.py +0 -82
  44. odoo/addons/contract/models/abstract_contract_line.py +0 -271
  45. odoo/addons/contract/models/contract_line_constraints.py +0 -429
  46. odoo/addons/contract/models/contract_terminate_reason.py +0 -14
  47. odoo/addons/contract/models/res_company.py +0 -15
  48. odoo/addons/contract/models/res_config_settings.py +0 -18
  49. odoo/addons/contract/security/contract_terminate_reason.xml +0 -23
  50. odoo/addons/contract/security/groups.xml +0 -9
  51. odoo/addons/contract/views/abstract_contract_line.xml +0 -117
  52. odoo/addons/contract/views/contract_terminate_reason.xml +0 -38
  53. odoo/addons/contract/wizards/contract_contract_terminate.py +0 -42
  54. odoo/addons/contract/wizards/contract_contract_terminate.xml +0 -33
  55. odoo/addons/contract/wizards/contract_line_wizard.py +0 -53
  56. odoo/addons/contract/wizards/contract_line_wizard.xml +0 -111
  57. {odoo_addon_contract-17.0.1.4.5.dist-info → odoo_addon_contract-18.0.2.0.0.8.dist-info}/WHEEL +0 -0
  58. {odoo_addon_contract-17.0.1.4.5.dist-info → odoo_addon_contract-18.0.2.0.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,264 @@
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
+ product = line.product_id.with_context(
198
+ quantity=line.env.context.get("contract_line_qty", line.quantity),
199
+ pricelist=pricelist.id,
200
+ partner=line.contract_id.partner_id.id,
201
+ uom=line.uom_id.id,
202
+ date=line.env.context.get(
203
+ "old_date", fields.Date.context_today(line)
204
+ ),
205
+ )
206
+ line.price_unit = pricelist._get_product_price(product, quantity=1)
207
+ else:
208
+ line.price_unit = line.specific_price
209
+
210
+ # Tip in https://github.com/odoo/odoo/issues/23891#issuecomment-376910788
211
+ @api.onchange("price_unit")
212
+ def _inverse_price_unit(self):
213
+ for line in self.filtered(lambda x: not x.automatic_price):
214
+ line.specific_price = line.price_unit
215
+
216
+ @api.depends("quantity", "price_unit", "discount")
217
+ def _compute_price_subtotal(self):
218
+ for line in self:
219
+ subtotal = line.quantity * line.price_unit
220
+ subtotal *= 1 - (line.discount / 100)
221
+ cur = (
222
+ line.contract_id.pricelist_id.currency_id
223
+ if line.contract_id.pricelist_id
224
+ else None
225
+ )
226
+ line.price_subtotal = cur.round(subtotal) if cur else subtotal
227
+
228
+ # === Recurrence Field Synchronization ===
229
+
230
+ def _set_recurrence_field(self, field):
231
+ """Sync recurrence field from header or keep local depending on config."""
232
+ for record in self:
233
+ record[field] = (
234
+ record[field]
235
+ if record.contract_id.line_recurrence
236
+ else record.contract_id[field]
237
+ )
238
+
239
+ @api.depends("contract_id.recurring_rule_type", "contract_id.line_recurrence")
240
+ def _compute_recurring_rule_type(self):
241
+ self._set_recurrence_field("recurring_rule_type")
242
+
243
+ @api.depends("contract_id.recurring_invoicing_type", "contract_id.line_recurrence")
244
+ def _compute_recurring_invoicing_type(self):
245
+ self._set_recurrence_field("recurring_invoicing_type")
246
+
247
+ @api.depends("contract_id.recurring_interval", "contract_id.line_recurrence")
248
+ def _compute_recurring_interval(self):
249
+ self._set_recurrence_field("recurring_interval")
250
+
251
+ @api.depends("contract_id.date_start", "contract_id.line_recurrence")
252
+ def _compute_date_start(self):
253
+ self._set_recurrence_field("date_start")
254
+
255
+ @api.depends("contract_id.line_recurrence")
256
+ def _compute_recurring_next_date(self):
257
+ res = super()._compute_recurring_next_date()
258
+ self._set_recurrence_field("recurring_next_date")
259
+ return res
260
+
261
+ # === Constraints & Onchange ===
262
+
263
+ @api.constrains("discount")
264
+ def _check_discount(self):
265
+ for line in self:
266
+ if line.discount > 100:
267
+ raise ValidationError(
268
+ self.env._("Discount should be less or equal to 100")
269
+ )
@@ -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>