From 7c578ca117593b639d1b14289db74cd885e0c9be Mon Sep 17 00:00:00 2001 From: Pauliina Ilmanen Date: Mon, 17 Mar 2025 10:51:55 +0200 Subject: [PATCH] Change Ceepos cost center code to be based on the tax percentage In Respa the tax precentage for same product may vary. Ceepos has a different product code for each tax percentage, so we need to get the correct product code based on the tax percentage when sending the order to Ceepos. Change Unit's `cost_center_code` field from CharField to JSONField. This was agreed to be the most straightforward way to implement this feature now. The field then contains a mapping of tax percentages to the correct cost center codes. Refs TTVA-240 --- payments/providers/cpu_ceepos.py | 13 +++- payments/tests/test_cpu_ceepos.py | 31 +++++++- .../0131_alter_unit_cost_center_code.py | 71 +++++++++++++++++++ resources/models/unit.py | 7 +- resources/models/utils.py | 11 +++ .../respa_admin/units/form/_general_info.html | 2 +- 6 files changed, 126 insertions(+), 9 deletions(-) create mode 100644 resources/migrations/0131_alter_unit_cost_center_code.py diff --git a/payments/providers/cpu_ceepos.py b/payments/providers/cpu_ceepos.py index 1bb70ec9d..99aa33231 100644 --- a/payments/providers/cpu_ceepos.py +++ b/payments/providers/cpu_ceepos.py @@ -139,9 +139,16 @@ def payload_add_products(self, payload, order): Order lines that contain bought products are retrieved through order""" - def _get_ceepos_product_code(order: Order) -> str: + def _get_ceepos_product_code(order: Order, order_line: OrderLine) -> str: + """ + Get the ceepos cost center code based on the tax percentage as + ceepos has different codes for different tax percentages. + """ resource = order.reservation.resource - return resource.unit.cost_center_code + unit_cost_center_codes = resource.unit.cost_center_code + tax = str(order_line.tax_percentage) + cost_center_code = unit_cost_center_codes.get(tax) + return cost_center_code def _get_order_line_description(order: Order) -> str: resource = order.reservation.resource @@ -177,7 +184,7 @@ def _get_ceepos_tax_code(order_line: OrderLine) -> str: for order_line in order_lines: items.append( { - "Code": _get_ceepos_product_code(order), + "Code": _get_ceepos_product_code(order, order_line), "Amount": order_line.quantity, "Price": price_as_sub_units(order_line.total_price), "Description": _get_order_line_description(order), diff --git a/payments/tests/test_cpu_ceepos.py b/payments/tests/test_cpu_ceepos.py index 832c11ac0..c3f995ad1 100644 --- a/payments/tests/test_cpu_ceepos.py +++ b/payments/tests/test_cpu_ceepos.py @@ -324,6 +324,33 @@ def test_payload_add_products_success(payment_provider, order_with_products): assert "Price" in product assert "Description" in product +@pytest.mark.parametrize( + "tax_percentage,ceepos_code", + ( + (Decimal('0'), "demo_00"), + (Decimal('10.00'), "demo_10"), + (Decimal('14.00'), "demo_14"), + (Decimal('24.00'), "demo_24"), + (Decimal('25.50'), "demo_255"), + ), +) +def test_payload_ceepos_product_code(payment_provider, order_with_products, tax_percentage, ceepos_code): + unit = order_with_products.reservation.resource.unit + unit.cost_center_code = { + "0.00": "demo_00", + "10.00": "demo_10", + "14.00": "demo_14", + "24.00": "demo_24", + "25.50": "demo_255" + } + unit.save() + + payload = {} + order_with_products.order_lines.all().update(tax_percentage=tax_percentage) + payment_provider.payload_add_products(payload, order_with_products) + + for product in payload["Products"]: + assert product["Code"] == ceepos_code @pytest.mark.parametrize( "tax_percentage,tax_code", @@ -338,7 +365,7 @@ def test_payload_add_products_success(payment_provider, order_with_products): def test_tax_code_mapping_in_qa(payment_provider, order_with_products, tax_percentage, tax_code): """Test the tax percentage is mapped to a correct code in qa environment""" payload = {} - + order_with_products.order_lines.all().update(tax_percentage=tax_percentage) payment_provider.payload_add_products(payload, order_with_products) @@ -361,7 +388,7 @@ def test_tax_code_mapping_in_production(provider_base_config, order_with_product provider_base_config["RESPA_PAYMENTS_CEEPOS_API_URL"] = "https://shop.tampere.fi/maksu.html" payment_provider = CPUCeeposProvider(config=provider_base_config) payload = {} - + order_with_products.order_lines.all().update(tax_percentage=tax_percentage) payment_provider.payload_add_products(payload, order_with_products) diff --git a/resources/migrations/0131_alter_unit_cost_center_code.py b/resources/migrations/0131_alter_unit_cost_center_code.py new file mode 100644 index 000000000..94ce78f0d --- /dev/null +++ b/resources/migrations/0131_alter_unit_cost_center_code.py @@ -0,0 +1,71 @@ +from decimal import Decimal +import django.contrib.postgres.fields.jsonb +from django.db import migrations + +TAX_PERCENTAGES = [ + Decimal(x) + for x in ( + "0.00", + "10.00", + "14.00", + "24.00", + "25.50", + ) +] + +def convert_cost_center_code(apps, schema_editor): + Unit = apps.get_model('resources', 'Unit') + for unit in Unit.objects.all(): + original_value = unit.cost_center_code_old + if original_value: + new_value = {str(tax): original_value for tax in TAX_PERCENTAGES} + unit.cost_center_code = new_value + unit.save(update_fields=['cost_center_code']) + print('Converting cost_center_code for unit {} from {} to {}'.format(unit.id, original_value, new_value), flush=True) + + +def reverse_cost_center_code(apps, schema_editor): + Unit = apps.get_model('resources', 'Unit') + for unit in Unit.objects.all(): + json_value = unit.cost_center_code + # Take the first value if available (priority to 0.00%) + if json_value and isinstance(json_value, dict): + # Try to get the 0.00% value first, or fall back to any value + if "0.00" in json_value: + unit.cost_center_code_old = json_value["0.00"] + elif len(json_value) > 0: + unit.cost_center_code_old = next(iter(json_value.values())) + else: + unit.cost_center_code_old = "" + else: + unit.cost_center_code_old = "" + unit.save(update_fields=['cost_center_code_old']) + print('Reverting cost_center_code for unit {} from {} to {}'.format(unit.id, json_value, unit.cost_center_code_old), flush=True) + +class Migration(migrations.Migration): + + dependencies = [ + ('resources', '0130_resource_people_capacity_upper'), + ] + + operations = [ + # First add a temporary field or rename existing field + migrations.RenameField( + model_name='unit', + old_name='cost_center_code', + new_name='cost_center_code_old', + ), + # Add the new JSONField + migrations.AddField( + model_name='unit', + name='cost_center_code', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, default=dict, verbose_name='CeePos Cost center code'), + ), + # Run the data migration with reverse function + migrations.RunPython(convert_cost_center_code, reverse_cost_center_code), + # Remove the old field + migrations.RemoveField( + model_name='unit', + name='cost_center_code_old', + ), + ] diff --git a/resources/models/unit.py b/resources/models/unit.py index 5e3b579a1..f724c7ef1 100644 --- a/resources/models/unit.py +++ b/resources/models/unit.py @@ -1,6 +1,7 @@ import pytz from django.conf import settings from django.contrib.gis.db import models +from django.contrib.postgres.fields import JSONField from django.core.validators import MinLengthValidator from django.db.models import Q from django.utils import timezone @@ -98,11 +99,11 @@ class Unit(ModifiableModel, AutoIdentifiedModel): manager_email = models.EmailField( verbose_name=_("Manager email"), max_length=100, null=True, blank=True ) - cost_center_code = models.CharField( + cost_center_code = JSONField( verbose_name=_("CeePos Cost center code"), - max_length=100, blank=True, - default="", + null=True, + default=dict, ) sap_cost_center_code = models.CharField( diff --git a/resources/models/utils.py b/resources/models/utils.py index 6583bdd41..59a537aca 100644 --- a/resources/models/utils.py +++ b/resources/models/utils.py @@ -107,11 +107,22 @@ def get_row_data(reservation, *, include_extra_fields, include_accounting_fields return row +def get_ceepos_cost_center_code_value(unit_cost_center_codes, tax_percentage): + """ + Get CeePos cost center code value based on tax percentage + unit_cost_center_codes: unit.cost_center_code + """ + if not unit_cost_center_codes: + return "" + return unit_cost_center_codes.get(str(tax_percentage), "") + def convert_value(reservation, name): value = reservation.get(name) or "" if value and name in RESERVATION_DATETIME_FIELDS: return localtime(value).replace(tzinfo=None) + if name == "cost_center_code": + return get_ceepos_cost_center_code_value(value, reservation["tax_percentage"]) return value diff --git a/respa_admin/templates/respa_admin/units/form/_general_info.html b/respa_admin/templates/respa_admin/units/form/_general_info.html index 32cb1bd7f..4822468ba 100644 --- a/respa_admin/templates/respa_admin/units/form/_general_info.html +++ b/respa_admin/templates/respa_admin/units/form/_general_info.html @@ -32,7 +32,7 @@

{% trans "Contact" %}

{% trans "Accounting" %}

- {% include "respa_admin/forms/_input.html" with field=form.cost_center_code %} + {% include "respa_admin/forms/_textarea_input.html" with field=form.cost_center_code %} {% include "respa_admin/forms/_input.html" with field=form.sap_cost_center_code %} {% include "respa_admin/forms/_input.html" with field=form.sap_sales_organization %} {% include "respa_admin/forms/_input.html" with field=form.sap_unit_id %}