From 1002601e44689a9a3670a379e0809b0ae4d7c9c0 Mon Sep 17 00:00:00 2001 From: Dirk van der Laarse Date: Tue, 23 Sep 2025 12:18:48 +0000 Subject: [PATCH 1/2] fix: include zero-rated vat transactions in vat return --- .../test_value_added_tax_return.py | 67 +++++++++++++++++++ .../value_added_tax_return.py | 50 +++++++++++--- .../value_added_tax_return_settings.py | 6 +- 3 files changed, 112 insertions(+), 11 deletions(-) diff --git a/csf_za/tax_compliance/doctype/value_added_tax_return/test_value_added_tax_return.py b/csf_za/tax_compliance/doctype/value_added_tax_return/test_value_added_tax_return.py index 5470d2f..2d92ef3 100644 --- a/csf_za/tax_compliance/doctype/value_added_tax_return/test_value_added_tax_return.py +++ b/csf_za/tax_compliance/doctype/value_added_tax_return/test_value_added_tax_return.py @@ -134,6 +134,7 @@ def test_process_gl_entries(self, mock_cached_value, mock_transform, mock_get_ca "voucher": frappe._dict( { "voucher_type": "Sales Invoice", + "account": "VAT Account", "general_ledger_debit": 0, "general_ledger_credit": 15, "sales_invoice_taxes_total": 115, @@ -147,6 +148,7 @@ def test_process_gl_entries(self, mock_cached_value, mock_transform, mock_get_ca "voucher": frappe._dict( { "voucher_type": "Purchase Invoice", + "account": "VAT Account", "general_ledger_debit": 15, "general_ledger_credit": 0, "purchase_invoice_taxes_total": 115, @@ -160,6 +162,7 @@ def test_process_gl_entries(self, mock_cached_value, mock_transform, mock_get_ca "voucher": frappe._dict( { "voucher_type": "Sales Invoice", + "account": "VAT Account", "general_ledger_debit": 0, "general_ledger_credit": 15, "sales_invoice_taxes_total": -115, @@ -173,6 +176,7 @@ def test_process_gl_entries(self, mock_cached_value, mock_transform, mock_get_ca "voucher": frappe._dict( { "voucher_type": "Purchase Invoice", + "account": "VAT Account", "general_ledger_debit": 15, "general_ledger_credit": 0, "purchase_invoice_taxes_total": -115, @@ -186,6 +190,7 @@ def test_process_gl_entries(self, mock_cached_value, mock_transform, mock_get_ca "voucher": frappe._dict( { "voucher_type": "Journal Entry", + "account": "VAT Account", "general_ledger_debit": 15, "general_ledger_credit": 0, } @@ -220,6 +225,7 @@ def test_process_gl_entries(self, mock_cached_value, mock_transform, mock_get_ca "voucher": frappe._dict( { "voucher_type": "Journal Entry", + "account": "VAT Account", "general_ledger_debit": -15, "general_ledger_credit": 0, } @@ -254,6 +260,7 @@ def test_process_gl_entries(self, mock_cached_value, mock_transform, mock_get_ca "voucher": frappe._dict( { "voucher_type": "Journal Entry", + "account": "VAT Account", "general_ledger_debit": -15, "general_ledger_credit": 0, "is_cancelled": 1, @@ -311,3 +318,63 @@ def test_process_gl_entries(self, mock_cached_value, mock_transform, mock_get_ca self.assertIsNone(results[6].classification) self.assertIsNone(results[6].tax_amount) self.assertIsNone(results[6].incl_tax_amount) + + @patch( + "csf_za.tax_compliance.doctype.value_added_tax_return.value_added_tax_return.frappe.get_cached_doc" + ) + @patch( + "csf_za.tax_compliance.doctype.value_added_tax_return.value_added_tax_return.transform_gl_entries" + ) + @patch( + "csf_za.tax_compliance.doctype.value_added_tax_return.value_added_tax_return.frappe.get_cached_value" + ) + @patch( + "csf_za.tax_compliance.doctype.value_added_tax_return.value_added_tax_return.VAT_RETURN_SETTING_FIELD_MAP", + [ + { + "field_name": "zero_rated_vat_field", + "classification": "Output - C Zero Rated (excl goods exported)", + "reference_doctype": "Sales Invoice", + } + ], + ) + def test_process_gl_entries_with_zero_rate_invoice( + self, mock_cached_value, mock_transform, mock_get_cached_doc + ): + mock_settings = frappe._dict( + { + "tax_accounts": [frappe._dict({"account": "VAT Account"})], + "zero_rated_vat_field": "Zero Rated VAT Template", + } + ) + mock_vouchers = frappe._dict( + { + "SI-003": frappe._dict( + { + "voucher": frappe._dict( + { + "voucher_type": "Sales Invoice", + "account": "Debtors", # Not a tax account + "general_ledger_debit": 100, + "general_ledger_credit": 0, + "sales_invoice_taxes_total": 100, + "taxes_and_charges_template": "Zero Rated VAT Template", + "is_cancelled": 0, + } + ) + } + ) + } + ) + + mock_get_cached_doc.return_value = mock_settings + mock_transform.return_value = mock_vouchers + mock_cached_value.side_effect = lambda doctype, docname, fieldname: "Classified" + + vat_return = frappe.new_doc("Value-added Tax Return") + results = vat_return.process_gl_entries([]) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].classification, "Output - C Zero Rated (excl goods exported)") + self.assertEqual(results[0].tax_amount, 0) + self.assertEqual(results[0].incl_tax_amount, 100) diff --git a/csf_za/tax_compliance/doctype/value_added_tax_return/value_added_tax_return.py b/csf_za/tax_compliance/doctype/value_added_tax_return/value_added_tax_return.py index fd128c3..1592ff8 100644 --- a/csf_za/tax_compliance/doctype/value_added_tax_return/value_added_tax_return.py +++ b/csf_za/tax_compliance/doctype/value_added_tax_return/value_added_tax_return.py @@ -210,6 +210,7 @@ def get_gl_entries(self): .on((pitc.parent == pi.name) & (pitc.account_head == gle.account)) .select( gle.name, + gle.account, gle.voucher_type, gle.voucher_no, gle.posting_date, @@ -236,7 +237,22 @@ def get_gl_entries(self): .where( (gle.posting_date >= self.date_from) & (gle.posting_date <= self.date_to) - & (gle.account.isin(tax_accounts)) + & ( + gle.account.isin(tax_accounts) + | ( + (gle.voucher_type == "Sales Invoice") + & si.taxes_and_charges.isnotnull() + & si.debit_to.isnotnull() + & (gle.account == si.debit_to) + ) + # Exclude Purchase Invoices with 0 tax for now + # | ( + # (gle.voucher_type == "Purchase Invoice") + # & pi.taxes_and_charges.isnotnull() + # & pi.credit_to.isnotnull() + # & (gle.account == pi.credit_to) + # ) + ) ) ) @@ -261,7 +277,7 @@ def process_gl_entries(self, gl_entries): entry for entry in VAT_RETURN_SETTING_FIELD_MAP if vat_return_settings.get(entry["field_name"]) ] - vouchers = transform_gl_entries(gl_entries) + vouchers = transform_gl_entries(gl_entries, tax_accounts) for voucher_no, item in vouchers.items(): voucher = item.voucher @@ -270,7 +286,10 @@ def process_gl_entries(self, gl_entries): if voucher.is_cancelled: continue - voucher.tax_amount = voucher.general_ledger_debit or voucher.general_ledger_credit + if hasattr(voucher, "account") and voucher.account in tax_accounts: + voucher.tax_amount = voucher.general_ledger_debit or voucher.general_ledger_credit + else: + voucher.tax_amount = 0 voucher.classification_debugging = "šŸš€" if voucher.voucher_type in ("Sales Invoice", "Purchase Invoice"): @@ -278,12 +297,16 @@ def process_gl_entries(self, gl_entries): voucher.sales_invoice_taxes_total or voucher.purchase_invoice_taxes_total ) + # For vouchers with zero-rated tax (aka tax amount = 0), set the total amount + if not voucher.incl_tax_amount: + voucher.incl_tax_amount = voucher.general_ledger_debit or voucher.general_ledger_credit + # If the voucher_type is a reversal (e.g. Credit and Debit Notes, change the sign of tax_amount) if voucher.incl_tax_amount < 0 and voucher.tax_amount > 0: voucher.tax_amount = voucher.tax_amount * -1 voucher.classification_debugging += ( - "\nšŸš€ voucher_type is a 'Sales Invoice' or 'Purchase Invoice')" + "\nšŸš€ voucher_type is a 'Sales Invoice' or 'Purchase Invoice'" ) voucher.classification_debugging += ( f"\nšŸš€ taxes_and_charges_template = '{voucher.taxes_and_charges_template}'" @@ -414,7 +437,7 @@ def process_gl_entries(self, gl_entries): return [voucher.voucher for voucher in vouchers.values()] -def transform_gl_entries(gl_entries): +def transform_gl_entries(gl_entries, tax_accounts): """ Transform flat list of entries to a dict with voucher_no as key E.g. @@ -461,8 +484,19 @@ def transform_gl_entries(gl_entries): """ vouchers = {} for entry in gl_entries: - vouchers.setdefault(entry.name, frappe._dict({"voucher": entry, "linked_journal_entries": []}))[ - "linked_journal_entries" - ].append(entry) + voucher_no = entry.voucher_no + if voucher_no not in vouchers: + vouchers[voucher_no] = frappe._dict({"voucher": entry, "linked_journal_entries": []}) + else: + # If the new entry is from a tax account, and the old one is not, then it becomes the main voucher + if ( + hasattr(entry, "account") + and hasattr(vouchers[voucher_no].voucher, "account") + and entry.account in tax_accounts + and vouchers[voucher_no].voucher.account not in tax_accounts + ): + vouchers[voucher_no].voucher = entry + + vouchers[voucher_no]["linked_journal_entries"].append(entry) return vouchers diff --git a/csf_za/tax_compliance/doctype/value_added_tax_return_settings/value_added_tax_return_settings.py b/csf_za/tax_compliance/doctype/value_added_tax_return_settings/value_added_tax_return_settings.py index ed681f2..b5e8b75 100644 --- a/csf_za/tax_compliance/doctype/value_added_tax_return_settings/value_added_tax_return_settings.py +++ b/csf_za/tax_compliance/doctype/value_added_tax_return_settings/value_added_tax_return_settings.py @@ -12,17 +12,17 @@ }, { "field_name": "standard_rate_capital", - "classification": "Output - B Standard rate (only capital goods) ", + "classification": "Output - B Standard rate (only capital goods)", "reference_doctype": "Sales Invoice", }, { "field_name": "zero_rate_non_exported", - "classification": "Output - C Zero Rated (excl goods exported) ", + "classification": "Output - C Zero Rated (excl goods exported)", "reference_doctype": "Sales Invoice", }, { "field_name": "zero_rate_exported", - "classification": "Output - D Zero Rated (only goods exported) ", + "classification": "Output - D Zero Rated (only goods exported)", "reference_doctype": "Sales Invoice", }, { From 0c7ea7e89c08f15d9727800de2a817ecefce0f06 Mon Sep 17 00:00:00 2001 From: Dirk van der Laarse Date: Tue, 23 Sep 2025 12:19:02 +0000 Subject: [PATCH 2/2] chore: bump to v0.2.4 --- csf_za/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csf_za/__init__.py b/csf_za/__init__.py index d31c31e..788da1f 100644 --- a/csf_za/__init__.py +++ b/csf_za/__init__.py @@ -1 +1 @@ -__version__ = "0.2.3" +__version__ = "0.2.4"