From 1856b51c3433df35ad44ac0c0fcb56f8f3f03832 Mon Sep 17 00:00:00 2001 From: PolComas Date: Wed, 20 Aug 2025 10:13:29 +0200 Subject: [PATCH] [16.0][IMP] subscription_oca: Improve subscription lines view with section/note support and added filters [IMP] subscription_oca: Very minor fixes to Pol's original changes --- .../models/sale_subscription_line.py | 60 ++++++++- subscription_oca/readme/CONTRIBUTORS.rst | 4 + .../tests/test_subscription_oca.py | 118 ++++++++++++++++++ .../views/sale_subscription_views.xml | 109 ++++++++++++++-- 4 files changed, 281 insertions(+), 10 deletions(-) diff --git a/subscription_oca/models/sale_subscription_line.py b/subscription_oca/models/sale_subscription_line.py index a780e4075b..d466e609c8 100644 --- a/subscription_oca/models/sale_subscription_line.py +++ b/subscription_oca/models/sale_subscription_line.py @@ -40,9 +40,33 @@ class SaleSubscriptionLine(models.Model): readonly=False, ) - @api.depends("product_id", "price_unit", "product_uom_qty", "discount", "tax_ids") + sequence = fields.Integer(default=10, help="Display order on the subscription.") + display_type = fields.Selection( + selection=[("line_section", "Section"), ("line_note", "Note")], + default=False, + help="Technical field for sorting lines in UI.", + ) + + @api.depends( + "product_id", + "price_unit", + "product_uom_qty", + "discount", + "tax_ids", + "display_type", + ) def _compute_subtotal(self): for record in self: + if record.display_type: + record.update( + { + "amount_tax_line_amount": 0.0, + "price_total": 0.0, + "price_subtotal": 0.0, + } + ) + continue + price = record.price_unit * (1 - (record.discount or 0.0) / 100.0) taxes = record.tax_ids.compute_all( price, @@ -80,9 +104,11 @@ def _compute_subtotal(self): index=True, ) - @api.depends("product_id") + @api.depends("product_id", "display_type") def _compute_name(self): for record in self: + if record.display_type: + continue if not record.product_id: record.name = False lang = get_lang(self.env, record.sale_subscription_id.partner_id.lang).code @@ -91,9 +117,15 @@ def _compute_name(self): lang=lang ).get_product_multiline_description_sale() - @api.depends("product_id", "sale_subscription_id.fiscal_position_id") + @api.depends( + "product_id", "display_type", "sale_subscription_id.fiscal_position_id" + ) def _compute_tax_ids(self): for line in self: + if line.display_type: + line.tax_ids = [fields.Command.clear()] + continue + fpos = ( line.sale_subscription_id.fiscal_position_id or line.sale_subscription_id.fiscal_position_id._get_fiscal_position( @@ -110,9 +142,13 @@ def _compute_tax_ids(self): "product_id", "sale_subscription_id.partner_id", "sale_subscription_id.pricelist_id", + "display_type", ) def _compute_price_unit(self): for record in self: + if record.display_type: + record.price_unit = 0.0 + continue if not record.product_id: continue if ( @@ -143,9 +179,13 @@ def _compute_price_unit(self): "tax_ids", "sale_subscription_id.partner_id", "sale_subscription_id.pricelist_id", + "display_type", ) def _compute_discount(self): for record in self: + if record.display_type: + record.discount = 0.0 + continue if not ( record.product_id and record.product_id.uom_id @@ -289,6 +329,12 @@ def _get_display_price(self, product): def _prepare_sale_order_line(self): self.ensure_one() + if self.display_type: + return { + "display_type": self.display_type, + "name": self.name or "", + "sequence": self.sequence, + } return { "product_id": self.product_id.id, "name": self.name, @@ -298,10 +344,17 @@ def _prepare_sale_order_line(self): "price_subtotal": self.price_subtotal, "tax_id": self.tax_ids, "product_uom": self.product_id.uom_id.id, + "sequence": self.sequence, } def _prepare_account_move_line(self): self.ensure_one() + if self.display_type: + return { + "display_type": self.display_type, + "name": self.name or "", + "sequence": self.sequence, + } account = ( self.product_id.property_account_income_id or self.product_id.categ_id.property_account_income_categ_id @@ -316,4 +369,5 @@ def _prepare_account_move_line(self): "tax_ids": [(6, 0, self.tax_ids.ids)], "product_uom_id": self.product_id.uom_id.id, "account_id": account.id, + "sequence": self.sequence, } diff --git a/subscription_oca/readme/CONTRIBUTORS.rst b/subscription_oca/readme/CONTRIBUTORS.rst index 8ddfb05210..c066f70193 100644 --- a/subscription_oca/readme/CONTRIBUTORS.rst +++ b/subscription_oca/readme/CONTRIBUTORS.rst @@ -5,3 +5,7 @@ * `Ooops404 `__: * Ilyas + +* `Batista10 `__: + + * Pol Comas i Romero diff --git a/subscription_oca/tests/test_subscription_oca.py b/subscription_oca/tests/test_subscription_oca.py index 6710bfa66f..c575c3b86d 100644 --- a/subscription_oca/tests/test_subscription_oca.py +++ b/subscription_oca/tests/test_subscription_oca.py @@ -696,3 +696,121 @@ def _collect_all_sub_test_results(self, subscription): ) test_res.append(group_stage_ids) return test_res + + def test_display_type_section_line_computes_and_prepares(self): + # Section line + line = self.env["sale.subscription.line"].create( + { + "company_id": 1, + "sale_subscription_id": self.sub1.id, + "display_type": "line_section", + "name": "Plans and prices", + "sequence": 5, + } + ) + + # Computes: must be all zero or empty + self.assertEqual(line.price_unit, 0.0) + self.assertEqual(line.discount, 0.0) + self.assertEqual(line.price_subtotal, 0.0) + self.assertEqual(line.price_total, 0.0) + self.assertEqual(line.amount_tax_line_amount, 0.0) + self.assertFalse(line.tax_ids, "tax_ids must be empty with display_type") + + # The name should not be overwritten by the product (it keeps what we set) + self.assertEqual(line.name, "Plans and prices") + + # Prepare values: only display fields + so_vals = line._prepare_sale_order_line() + self.assertEqual(so_vals.get("display_type"), "line_section") + self.assertEqual(so_vals.get("name"), "Plans and prices") + self.assertEqual(so_vals.get("sequence"), 5) + + # It should not include product/price keys + self.assertNotIn("product_id", so_vals) + self.assertNotIn("price_unit", so_vals) + self.assertNotIn("discount", so_vals) + self.assertNotIn("tax_id", so_vals) + + aml_vals = line._prepare_account_move_line() + self.assertEqual(aml_vals.get("display_type"), "line_section") + self.assertEqual(aml_vals.get("name"), "Plans and prices") + self.assertEqual(aml_vals.get("sequence"), 5) + self.assertNotIn("product_id", aml_vals) + self.assertNotIn("price_unit", aml_vals) + self.assertNotIn("discount", aml_vals) + self.assertNotIn("tax_ids", aml_vals) + + def test_display_type_note_line_computes_and_prepares(self): + # Note line + line = self.env["sale.subscription.line"].create( + { + "company_id": 1, + "sale_subscription_id": self.sub1.id, + "display_type": "line_note", + "name": "Note: discount applicable from the 2nd year", + "sequence": 15, + } + ) + # Computes: zero / empty + self.assertEqual(line.price_unit, 0.0) + self.assertEqual(line.discount, 0.0) + self.assertEqual(line.price_subtotal, 0.0) + self.assertEqual(line.price_total, 0.0) + self.assertEqual(line.amount_tax_line_amount, 0.0) + self.assertFalse(line.tax_ids) + + # Prepare values: only display fields + so_vals = line._prepare_sale_order_line() + self.assertEqual(so_vals.get("display_type"), "line_note") + self.assertEqual( + so_vals.get("name"), "Note: discount applicable from the 2nd year" + ) + self.assertEqual(so_vals.get("sequence"), 15) + self.assertNotIn("product_id", so_vals) + self.assertNotIn("price_unit", so_vals) + self.assertNotIn("discount", so_vals) + self.assertNotIn("tax_id", so_vals) + + aml_vals = line._prepare_account_move_line() + self.assertEqual(aml_vals.get("display_type"), "line_note") + self.assertEqual( + aml_vals.get("name"), "Note: discount applicable from the 2nd year" + ) + self.assertEqual(aml_vals.get("sequence"), 15) + self.assertNotIn("product_id", aml_vals) + self.assertNotIn("price_unit", aml_vals) + self.assertNotIn("discount", aml_vals) + self.assertNotIn("tax_ids", aml_vals) + + def test_display_type_toggle_from_normal_line(self): + # Start with a normal line (with product) so that there are amounts > 0 + line = self.create_sub_line(self.sub1, self.product_1.id) + self.assertGreater(line.price_subtotal, 0.0) + self.assertTrue(line.tax_ids) + + # Now we convert it to a display line (note or section) + line.display_type = "line_note" + # Computes must be all zero/empty + self.assertEqual(line.price_unit, 0.0) + self.assertEqual(line.discount, 0.0) + self.assertEqual(line.price_subtotal, 0.0) + self.assertEqual(line.price_total, 0.0) + self.assertEqual(line.amount_tax_line_amount, 0.0) + self.assertFalse(line.tax_ids) + + # Prepared values have to be display + so_vals = line._prepare_sale_order_line() + self.assertEqual(so_vals.get("display_type"), "line_note") + self.assertIn("name", so_vals) + self.assertNotIn("product_id", so_vals) + self.assertNotIn("price_unit", so_vals) + self.assertNotIn("discount", so_vals) + + aml_vals = line._prepare_account_move_line() + self.assertEqual(aml_vals.get("display_type"), "line_note") + self.assertIn("name", aml_vals) + self.assertNotIn("product_id", aml_vals) + self.assertNotIn("price_unit", aml_vals) + self.assertNotIn("discount", aml_vals) + self.assertNotIn("tax_ids", aml_vals) diff --git a/subscription_oca/views/sale_subscription_views.xml b/subscription_oca/views/sale_subscription_views.xml index 912aab2eb7..5f7bc8b5be 100644 --- a/subscription_oca/views/sale_subscription_views.xml +++ b/subscription_oca/views/sale_subscription_views.xml @@ -118,19 +118,58 @@ string="Subscription lines" name="subscription_lines_page" > - + - + + + + + + + + + + + + - - - - + + + + + + + + + sale.subscription.line.form + sale.subscription.line + +
+ + + + + + + + + + + +
+
+
+ sale.subscription.tree sale.subscription @@ -389,6 +465,25 @@ string="Next invoice date" date="recurring_next_date" /> + + + + + + +