Skip to content

Commit b29c2e1

Browse files
committed
Merge PR #1453 into 19.0
Signed-off-by jelenapoblet
2 parents 8090483 + 9d27b23 commit b29c2e1

7 files changed

Lines changed: 206 additions & 2 deletions

File tree

subscription_oca/README.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,17 @@ To create subscriptions with the sale of a product:
7272
*Subscribable product* and *Subscription template*
7373
3. Create a sales order with the product and confirm it.
7474

75+
Invoice and delivery addresses:
76+
77+
- Each subscription has an *Invoice address* and a *Delivery address*.
78+
They default to the customer's corresponding addresses and can be
79+
overridden per subscription.
80+
- These addresses are propagated to the recurring invoices (the invoice
81+
is addressed to the invoice address and records the delivery one) and
82+
to the sale orders generated by the subscription.
83+
- The fields are shown when the *Customer Addresses* setting (group
84+
*Display Delivery / Invoice addresses*) is enabled.
85+
7586
Known issues / Roadmap
7687
======================
7788

subscription_oca/models/sale_subscription.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,24 @@ class SaleSubscription(models.Model):
3434
partner_id = fields.Many2one(
3535
comodel_name="res.partner", required=True, string="Partner", index=True
3636
)
37+
partner_invoice_id = fields.Many2one(
38+
comodel_name="res.partner",
39+
string="Invoice address",
40+
compute="_compute_partner_address_ids",
41+
store=True,
42+
readonly=False,
43+
help="Address the recurring invoices are addressed to. "
44+
"Defaults to the customer's invoice address.",
45+
)
46+
partner_shipping_id = fields.Many2one(
47+
comodel_name="res.partner",
48+
string="Delivery address",
49+
compute="_compute_partner_address_ids",
50+
store=True,
51+
readonly=False,
52+
help="Delivery address used on the recurring invoices and orders. "
53+
"Defaults to the customer's delivery address.",
54+
)
3755
fiscal_position_id = fields.Many2one(
3856
"account.fiscal.position",
3957
string="Fiscal Position",
@@ -263,6 +281,21 @@ def calculate_recurring_next_date(self, start_date):
263281
else:
264282
self.recurring_next_date = self._get_next_invoice_date(start_date)
265283

284+
@api.depends("partner_id")
285+
def _compute_partner_address_ids(self):
286+
for subscription in self:
287+
if not subscription.partner_id:
288+
subscription.partner_invoice_id = False
289+
subscription.partner_shipping_id = False
290+
continue
291+
addresses = subscription.partner_id.address_get(["invoice", "delivery"])
292+
subscription.partner_invoice_id = addresses.get(
293+
"invoice", subscription.partner_id.id
294+
)
295+
subscription.partner_shipping_id = addresses.get(
296+
"delivery", subscription.partner_id.id
297+
)
298+
266299
@api.onchange("partner_id")
267300
def onchange_partner_id(self):
268301
self.pricelist_id = self.partner_id.property_product_pricelist
@@ -309,6 +342,8 @@ def _prepare_sale_order(self, line_ids=False):
309342
self.ensure_one()
310343
return {
311344
"partner_id": self.partner_id.id,
345+
"partner_invoice_id": (self.partner_invoice_id.id or self.partner_id.id),
346+
"partner_shipping_id": (self.partner_shipping_id.id or self.partner_id.id),
312347
"pricelist_id": self.pricelist_id.id,
313348
"fiscal_position_id": self.fiscal_position_id.id,
314349
"date_order": datetime.now(),
@@ -320,10 +355,17 @@ def _prepare_sale_order(self, line_ids=False):
320355

321356
def _prepare_account_move(self, line_ids):
322357
self.ensure_one()
358+
# The invoice is addressed to the invoice address, like a sale order
359+
# invoice is created on ``partner_invoice_id``. ``commercial_partner_id``
360+
# still rolls up to the contracting company, so the receivable is kept
361+
# on the parent. ``partner_shipping_id`` is passed explicitly so it is
362+
# not re-derived from the (invoice) partner_id.
363+
invoice_partner = self.partner_invoice_id or self.partner_id
323364
values = {
324-
"partner_id": self.partner_id.id,
365+
"partner_id": invoice_partner.id,
366+
"partner_shipping_id": (self.partner_shipping_id.id or self.partner_id.id),
325367
"invoice_date": self.recurring_next_date,
326-
"invoice_payment_term_id": self.partner_id.property_payment_term_id.id,
368+
"invoice_payment_term_id": invoice_partner.property_payment_term_id.id,
327369
"invoice_origin": self.name,
328370
"invoice_user_id": self.user_id.id,
329371
"partner_bank_id": self.company_id.partner_id.bank_ids[:1].id,

subscription_oca/readme/USAGE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,14 @@ To create subscriptions with the sale of a product:
2525
2. Create the product and in the sales tab, complete the fields
2626
*Subscribable product* and *Subscription template*
2727
3. Create a sales order with the product and confirm it.
28+
29+
Invoice and delivery addresses:
30+
31+
- Each subscription has an *Invoice address* and a *Delivery address*.
32+
They default to the customer's corresponding addresses and can be
33+
overridden per subscription.
34+
- These addresses are propagated to the recurring invoices (the invoice
35+
is addressed to the invoice address and records the delivery one) and
36+
to the sale orders generated by the subscription.
37+
- The fields are shown when the *Customer Addresses* setting
38+
(group *Display Delivery / Invoice addresses*) is enabled.

subscription_oca/static/description/index.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,17 @@ <h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
422422
<em>Subscribable product</em> and <em>Subscription template</em></li>
423423
<li>Create a sales order with the product and confirm it.</li>
424424
</ol>
425+
<p>Invoice and delivery addresses:</p>
426+
<ul class="simple">
427+
<li>Each subscription has an <em>Invoice address</em> and a <em>Delivery address</em>.
428+
They default to the customer’s corresponding addresses and can be
429+
overridden per subscription.</li>
430+
<li>These addresses are propagated to the recurring invoices (the invoice
431+
is addressed to the invoice address and records the delivery one) and
432+
to the sale orders generated by the subscription.</li>
433+
<li>The fields are shown when the <em>Customer Addresses</em> setting (group
434+
<em>Display Delivery / Invoice addresses</em>) is enabled.</li>
435+
</ul>
425436
</div>
426437
<div class="section" id="known-issues-roadmap">
427438
<h2><a class="toc-backref" href="#toc-entry-2">Known issues / Roadmap</a></h2>

subscription_oca/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
from . import test_subscription_oca
44
from . import test_subscription_security
55
from . import test_subscription_recurrence_dates
6+
from . import test_subscription_partner_addresses
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Copyright 2026 Domatix - Alvaro
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
5+
6+
7+
class TestSubscriptionPartnerAddresses(AccountTestInvoicingCommon):
8+
"""Invoice and delivery addresses configured on the subscription must be
9+
propagated to the recurring invoices and to the generated sale orders."""
10+
11+
@classmethod
12+
def setUpClass(cls):
13+
super().setUpClass()
14+
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
15+
cls.env.user.group_ids += cls.env.ref("sales_team.group_sale_manager")
16+
cls.addr_customer = cls.env["res.partner"].create({"name": "Main customer"})
17+
cls.addr_invoice_address = cls.env["res.partner"].create(
18+
{
19+
"name": "Billing dept",
20+
"type": "invoice",
21+
"parent_id": cls.addr_customer.id,
22+
}
23+
)
24+
cls.addr_shipping_address = cls.env["res.partner"].create(
25+
{
26+
"name": "Warehouse",
27+
"type": "delivery",
28+
"parent_id": cls.addr_customer.id,
29+
}
30+
)
31+
# Prefixed on purpose: AccountTestInvoicingCommon < ProductCommon already
32+
# defines cls.pricelist (and cls.company/cls.product_a); avoid shadowing.
33+
cls.addr_pricelist = cls.env["product.pricelist"].create(
34+
{"name": "Addr PL", "currency_id": cls.company.currency_id.id}
35+
)
36+
cls.addr_template = cls.env["sale.subscription.template"].create(
37+
{
38+
"name": "Addr template",
39+
"code": "ADDR",
40+
"recurring_rule_type": "months",
41+
"recurring_interval": 1,
42+
"invoicing_mode": "draft",
43+
}
44+
)
45+
cls.addr_stage = cls.env["sale.subscription.stage"].search(
46+
[("type", "=", "in_progress")], limit=1
47+
)
48+
49+
def _new_subscription(self, partner):
50+
sub = self.env["sale.subscription"].create(
51+
{
52+
"partner_id": partner.id,
53+
"template_id": self.addr_template.id,
54+
"pricelist_id": self.addr_pricelist.id,
55+
"stage_id": self.addr_stage.id,
56+
}
57+
)
58+
self.env["sale.subscription.line"].create(
59+
{
60+
"sale_subscription_id": sub.id,
61+
"product_id": self.product_a.id,
62+
"product_uom_qty": 1.0,
63+
"price_unit": 100.0,
64+
"tax_ids": [(6, 0, [])],
65+
}
66+
)
67+
return sub
68+
69+
def test_addresses_default_from_partner_children(self):
70+
sub = self._new_subscription(self.addr_customer)
71+
self.assertEqual(sub.partner_invoice_id, self.addr_invoice_address)
72+
self.assertEqual(sub.partner_shipping_id, self.addr_shipping_address)
73+
74+
def test_addresses_default_to_partner_without_children(self):
75+
plain = self.env["res.partner"].create({"name": "No child"})
76+
sub = self._new_subscription(plain)
77+
self.assertEqual(sub.partner_invoice_id, plain)
78+
self.assertEqual(sub.partner_shipping_id, plain)
79+
80+
def test_addresses_empty_without_partner(self):
81+
# A record without partner yet (e.g. while filling the form) must
82+
# keep both addresses empty instead of crashing.
83+
sub = self.env["sale.subscription"].new({})
84+
self.assertFalse(sub.partner_invoice_id)
85+
self.assertFalse(sub.partner_shipping_id)
86+
87+
def test_invoice_uses_subscription_addresses(self):
88+
sub = self._new_subscription(self.addr_customer)
89+
invoice = sub.create_invoice()
90+
# The invoice is addressed to the invoice address, with its delivery one.
91+
self.assertEqual(invoice.partner_id, self.addr_invoice_address)
92+
self.assertEqual(invoice.partner_shipping_id, self.addr_shipping_address)
93+
94+
def test_invoice_commercial_partner_rolls_up_to_customer(self):
95+
# Addressing the invoice to a child invoice contact must not move the
96+
# receivable away from the contracting company.
97+
sub = self._new_subscription(self.addr_customer)
98+
invoice = sub.create_invoice()
99+
self.assertEqual(invoice.commercial_partner_id, self.addr_customer)
100+
101+
def test_invoice_follows_manual_address_override(self):
102+
other_invoice = self.env["res.partner"].create(
103+
{
104+
"name": "Alt billing",
105+
"type": "invoice",
106+
"parent_id": self.addr_customer.id,
107+
}
108+
)
109+
sub = self._new_subscription(self.addr_customer)
110+
sub.partner_invoice_id = other_invoice
111+
invoice = sub.create_invoice()
112+
self.assertEqual(invoice.partner_id, other_invoice)
113+
114+
def test_sale_order_carries_addresses(self):
115+
self.addr_template.invoicing_mode = "sale_and_invoice"
116+
sub = self._new_subscription(self.addr_customer)
117+
order = sub.create_sale_order()
118+
self.assertEqual(order.partner_id, self.addr_customer)
119+
self.assertEqual(order.partner_invoice_id, self.addr_invoice_address)
120+
self.assertEqual(order.partner_shipping_id, self.addr_shipping_address)

subscription_oca/views/sale_subscription_views.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@
7272
<group>
7373
<group name="left_group">
7474
<field name="partner_id" />
75+
<field
76+
name="partner_invoice_id"
77+
groups="account.group_delivery_invoice_address"
78+
/>
79+
<field
80+
name="partner_shipping_id"
81+
groups="account.group_delivery_invoice_address"
82+
/>
7583
<field name="pricelist_id" />
7684
<field
7785
name="date_start"

0 commit comments

Comments
 (0)