Skip to content

Commit 4dc2053

Browse files
author
bosd
committed
fixup
1 parent 0c9ebb9 commit 4dc2053

6 files changed

Lines changed: 105 additions & 74 deletions

File tree

contract_invoice_offset/demo/contract_demo.xml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
<field name="recurring_invoicing_type">pre-paid</field>
1010
<field name="invoicing_offset_type">months</field>
1111
<field name="invoicing_offset_value">-1</field>
12-
<field name="contract_line_ids" eval="[
12+
<field
13+
name="contract_line_ids"
14+
eval="[
1315
(0, 0, {
1416
'name': 'Monthly Service (Advance)',
1517
'product_id': ref('product.product_product_4'),
@@ -19,7 +21,8 @@
1921
'recurring_rule_type': 'monthly',
2022
'date_start': time.strftime('%Y-%m-01'),
2123
})
22-
]" />
24+
]"
25+
/>
2326
</record>
2427

2528
<record id="contract_demo_offset_delay" model="contract.contract">
@@ -31,7 +34,9 @@
3134
<field name="recurring_invoicing_type">post-paid</field>
3235
<field name="invoicing_offset_type">weeks</field>
3336
<field name="invoicing_offset_value">2</field>
34-
<field name="contract_line_ids" eval="[
37+
<field
38+
name="contract_line_ids"
39+
eval="[
3540
(0, 0, {
3641
'name': 'Monthly Service (Delayed)',
3742
'product_id': ref('product.product_product_5'),
@@ -41,6 +46,7 @@
4146
'recurring_rule_type': 'monthly',
4247
'date_start': time.strftime('%Y-%m-01'),
4348
})
44-
]" />
49+
]"
50+
/>
4551
</record>
4652
</odoo>

contract_invoice_offset/models/contract.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ def _compute_recurring_next_date(self):
2323
# We need to override or hook into this.
2424
# The original method in contract.contract calls self.get_next_invoice_date
2525
# which we overrode in the mixin.
26-
# HOWEVER, the original method does NOT pass the new arguments (offset type/value).
26+
# HOWEVER, the original method does NOT pass the new arguments
27+
# (offset type/value).
2728
# We need to fully override this method to pass our new fields.
28-
29+
2930
# We can copy the original implementation and add our fields.
3031
for contract in self:
3132
recurring_next_date = contract.contract_line_ids.filtered(

contract_invoice_offset/models/contract_line.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# Copyright 2025 bosd
22
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
33

4-
from odoo import api, models
54
from dateutil.relativedelta import relativedelta
65

6+
from odoo import api, models
7+
78

89
class ContractLine(models.Model):
910
_inherit = "contract.line"
@@ -61,13 +62,17 @@ def _compute_recurring_next_date(self):
6162
@api.constrains("recurring_next_date", "date_start")
6263
def _check_recurring_next_date_start_date(self):
6364
# We must skip this check if we are doing "advance" invoicing (negative offset),
64-
# because recurring_next_date (The invoice date) WILL be before date_start (The service period start).
65-
65+
# because recurring_next_date (The invoice date) WILL be before date_start
66+
# (The service period start).
67+
6668
# Filter out lines that have a negative offset (advance payment)
67-
lines_to_check = self.filtered(lambda l: l.contract_id.invoicing_offset_value >= 0)
68-
69+
lines_to_check = self.filtered(
70+
lambda line: line.contract_id.invoicing_offset_value >= 0
71+
)
72+
6973
if lines_to_check:
70-
super(ContractLine, lines_to_check)._check_recurring_next_date_start_date()
74+
super(ContractLine, lines_to_check)._check_recurring_next_date_start_date()
75+
return
7176

7277
def _get_period_to_invoice(
7378
self, last_date_invoiced, recurring_next_date, stop_at_date_end=True

contract_invoice_offset/models/contract_recurring_mixin.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ class ContractRecurringMixin(models.AbstractModel):
2121
)
2222
invoicing_offset_value = fields.Integer(
2323
default=0,
24-
string="Invoicing Offset Value",
25-
help="Positive value to delay the invoice, negative value to invoice in advance. "
24+
help="Positive value delays invoice, negative value invoices in advance. "
2625
"E.g., -1 for 1 month in advance.",
2726
)
2827

@@ -59,14 +58,16 @@ def get_next_invoice_date(
5958
# Apply offset
6059
if invoicing_offset_type == "days":
6160
# Use original offset logic if type is days
62-
return base_date + relativedelta(days=recurring_invoicing_offset + invoicing_offset_value)
61+
return base_date + relativedelta(
62+
days=recurring_invoicing_offset + invoicing_offset_value
63+
)
6364
elif invoicing_offset_type == "weeks":
6465
return base_date + relativedelta(weeks=invoicing_offset_value)
6566
elif invoicing_offset_type == "months":
6667
return base_date + relativedelta(months=invoicing_offset_value)
6768
elif invoicing_offset_type == "years":
6869
return base_date + relativedelta(years=invoicing_offset_value)
69-
70+
7071
return base_date
7172

7273
@api.model
@@ -82,7 +83,8 @@ def get_next_period_date_end(
8283
invoicing_offset_type="days",
8384
invoicing_offset_value=0,
8485
):
85-
"""Compute the end date for the next period, supporting flexible reverse calculation."""
86+
"""Compute the end date for the next period, supporting flexible
87+
reverse calculation."""
8688
if not next_period_date_start or (
8789
max_date_end and next_period_date_start > max_date_end
8890
):
@@ -98,7 +100,7 @@ def get_next_period_date_end(
98100
else:
99101
# Forced invoice date: back-calculate period end
100102
# We need to reverse the offset to find the base date (start or end)
101-
103+
102104
# 1. Reverse the offset to get the 'base date' (which is start or end)
103105
base_date = next_invoice_date
104106
if invoicing_offset_type == "weeks":
@@ -107,8 +109,10 @@ def get_next_period_date_end(
107109
base_date -= relativedelta(months=invoicing_offset_value)
108110
elif invoicing_offset_type == "years":
109111
base_date -= relativedelta(years=invoicing_offset_value)
110-
else: # days
111-
base_date -= relativedelta(days=recurring_invoicing_offset + invoicing_offset_value)
112+
else: # days
113+
base_date -= relativedelta(
114+
days=recurring_invoicing_offset + invoicing_offset_value
115+
)
112116

113117
# 2. From base date, derive period end.
114118
if recurring_invoicing_type == "pre-paid":
Lines changed: 69 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,132 @@
11
# Copyright 2024 bosd
22
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
33

4-
from dateutil.relativedelta import relativedelta
5-
from odoo.tests import common, tagged
64
from odoo import fields
5+
from odoo.tests import common, tagged
6+
77

88
@tagged("post_install", "-at_install")
99
class TestContractInvoiceOffset(common.TransactionCase):
10-
1110
@classmethod
1211
def setUpClass(cls):
1312
super().setUpClass()
1413
cls.partner = cls.env["res.partner"].create({"name": "Test Partner"})
15-
cls.product = cls.env["product.product"].create({"name": "Test Service", "type": "service"})
16-
cls.contract = cls.env["contract.contract"].create({
17-
"name": "Test Offset Contract",
18-
"partner_id": cls.partner.id,
19-
"recurring_interval": 1,
20-
"recurring_rule_type": "monthly",
21-
"date_start": "2025-01-01",
22-
"contract_line_ids": [
23-
(0, 0, {
24-
"product_id": cls.product.id,
25-
"name": "Service",
26-
"quantity": 1,
27-
"price_unit": 100,
28-
"recurring_interval": 1,
29-
"recurring_rule_type": "monthly",
30-
"date_start": "2025-01-01",
31-
})
32-
]
33-
})
14+
cls.product = cls.env["product.product"].create(
15+
{"name": "Test Service", "type": "service"}
16+
)
17+
cls.contract = cls.env["contract.contract"].create(
18+
{
19+
"name": "Test Offset Contract",
20+
"partner_id": cls.partner.id,
21+
"recurring_interval": 1,
22+
"recurring_rule_type": "monthly",
23+
"date_start": "2025-01-01",
24+
"contract_line_ids": [
25+
(
26+
0,
27+
0,
28+
{
29+
"product_id": cls.product.id,
30+
"name": "Service",
31+
"quantity": 1,
32+
"price_unit": 100,
33+
"recurring_interval": 1,
34+
"recurring_rule_type": "monthly",
35+
"date_start": "2025-01-01",
36+
},
37+
)
38+
],
39+
}
40+
)
3441

3542
def test_prepaid_advance_one_month(self):
3643
"""Test Pre-paid + 1 month advance (offset -1 month)."""
37-
self.contract.write({
38-
"recurring_invoicing_type": "pre-paid",
39-
"invoicing_offset_type": "months",
40-
"invoicing_offset_value": -1,
41-
})
44+
self.contract.write(
45+
{
46+
"recurring_invoicing_type": "pre-paid",
47+
"invoicing_offset_type": "months",
48+
"invoicing_offset_value": -1,
49+
}
50+
)
4251
# Standard Next Invoice for 2025-01-01 start is 2025-01-01.
4352
# With -1 month offset, it should be 2024-12-01.
4453
self.contract._compute_recurring_next_date()
4554
self.assertEqual(
4655
self.contract.recurring_next_date,
4756
fields.Date.to_date("2024-12-01"),
48-
"Invoice date should be one month prior to start date"
57+
"Invoice date should be one month prior to start date",
4958
)
5059

5160
def test_postpaid_delayed_one_month(self):
5261
"""Test Post-paid + 1 month delay."""
53-
self.contract.write({
54-
"recurring_invoicing_type": "post-paid",
55-
"invoicing_offset_type": "months",
56-
"invoicing_offset_value": 1,
57-
})
58-
62+
self.contract.write(
63+
{
64+
"recurring_invoicing_type": "post-paid",
65+
"invoicing_offset_type": "months",
66+
"invoicing_offset_value": 1,
67+
}
68+
)
69+
5970
self.contract._compute_recurring_next_date()
6071
# End date of first period (2025-01-01 monthly) is 2025-01-31.
6172
# Next Invoice Date = 2025-01-31 + 1 month = 2025-02-28.
6273
self.assertEqual(
6374
self.contract.recurring_next_date,
6475
fields.Date.to_date("2025-02-28"),
65-
"Invoice date should be one month after period end"
76+
"Invoice date should be one month after period end",
6677
)
6778

6879
def test_days_fallback(self):
6980
"""Test that 'days' still works (backward compatibility logic)."""
70-
self.contract.write({
71-
"recurring_invoicing_type": "post-paid",
72-
"invoicing_offset_type": "days",
73-
"invoicing_offset_value": 5, # Additional 5 days
74-
})
81+
self.contract.write(
82+
{
83+
"recurring_invoicing_type": "post-paid",
84+
"invoicing_offset_type": "days",
85+
"invoicing_offset_value": 5, # Additional 5 days
86+
}
87+
)
7588
# Base offset (from contract logic if unmodified) is 1 day for post-paid.
76-
# Our logic: base_date (Jan 31) + days(recurring_offset(1) + offset_value(5)) = +6 days.
89+
# Our logic: base_date (Jan 31) + days(recurring_offset(1) + offset_value(5))
90+
# = +6 days.
7791
# Jan 31 + 6 days = Feb 6.
7892
self.contract._compute_recurring_next_date()
7993
self.assertEqual(
8094
self.contract.recurring_next_date,
8195
fields.Date.to_date("2025-02-06"),
82-
"Invoice date should be 6 days after period end"
96+
"Invoice date should be 6 days after period end",
8397
)
8498

8599
def test_apply_offset_on_invoice_creation(self):
86100
"""Verify that invoice creation respects the offsets."""
87-
self.contract.write({
88-
"recurring_invoicing_type": "pre-paid",
89-
"invoicing_offset_type": "months",
90-
"invoicing_offset_value": -1,
91-
})
101+
self.contract.write(
102+
{
103+
"recurring_invoicing_type": "pre-paid",
104+
"invoicing_offset_type": "months",
105+
"invoicing_offset_value": -1,
106+
}
107+
)
92108
# Next date is 2024-12-01. If we run invoice creation for that date,
93109
# we expect an invoice for the period 2025-01-01 to 2025-01-31.
94-
110+
95111
# Force next date just to be sure
96112
self.contract.recurring_next_date = "2024-12-01"
97-
113+
98114
invoice = self.contract._recurring_create_invoice()
99115
self.assertTrue(invoice, "Invoice should be created")
100-
116+
101117
# Check Invoice Date
102118
self.assertEqual(
103119
invoice.invoice_date,
104120
fields.Date.to_date("2024-12-01"),
105-
"Invoice date should be Dec 1st"
121+
"Invoice date should be Dec 1st",
106122
)
107-
123+
108124
# Check Line Description or Metadata if available to verify period?
109125
# Standard contract creates description "Service" (or template).
110126
# We can check contract line's last_date_invoiced.
111127
line = self.contract.contract_line_ids[0]
112128
self.assertEqual(
113129
line.last_date_invoiced,
114130
fields.Date.to_date("2025-01-31"),
115-
"Last date invoiced should be end of the service period (Jan 31)"
131+
"Last date invoiced should be end of the service period (Jan 31)",
116132
)

test-requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
odoo-addon-contract @ git+https://github.com/OCA/contract@refs/pull/1312/head#subdirectory=contract
2-

0 commit comments

Comments
 (0)