diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6e09929534d820d983bf4ac40d075e40affcfa29..10deb3709b23e63019e902212c52fcf222acbefb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ exclude: 'node_modules|.git' -default_stages: [commit] +default_stages: [pre-commit] fail_fast: false diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index d63f0bc02c7014480cc2bfbf3dc6b273856082f1..75b61985211c1bbc0f42adcb2e9fecb59214af5b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -63,18 +63,6 @@ "unallocated_amount", "difference_amount", "write_off_difference_amount", - "taxes_and_charges_section", - "purchase_taxes_and_charges_template", - "sales_taxes_and_charges_template", - "column_break_55", - "apply_tax_withholding_amount", - "tax_withholding_category", - "section_break_56", - "taxes", - "section_break_60", - "base_total_taxes_and_charges", - "column_break_61", - "total_taxes_and_charges", "deductions_or_loss_section", "deductions", "transaction_references", @@ -87,6 +75,20 @@ "project", "dimension_col_break", "cost_center", + "taxes_and_charges_tab", + "taxes_and_charges_section", + "purchase_taxes_and_charges_template", + "sales_taxes_and_charges_template", + "column_break_55", + "apply_tax_withholding_amount", + "tax_withholding_category", + "section_break_56", + "taxes", + "section_break_60", + "base_total_taxes_and_charges", + "column_break_61", + "total_taxes_and_charges", + "more_information_tab", "section_break_12", "status", "accounting_journal", @@ -102,6 +104,7 @@ "payment_request", "payment_order", "in_words", + "subscription_tab", "subscription_section", "subscription", "auto_repeat", @@ -435,7 +438,6 @@ "label": "Write Off Difference Amount" }, { - "collapsible": 1, "collapsible_depends_on": "deductions", "depends_on": "eval:(doc.paid_amount && doc.received_amount)", "fieldname": "deductions_or_loss_section", @@ -484,11 +486,9 @@ "read_only": 1 }, { - "collapsible": 1, "depends_on": "eval:(doc.paid_from && doc.paid_to && doc.paid_amount && doc.received_amount)", "fieldname": "section_break_12", - "fieldtype": "Section Break", - "label": "More Information" + "fieldtype": "Section Break" }, { "allow_on_submit": 1, @@ -547,10 +547,8 @@ "read_only": 1 }, { - "collapsible": 1, "fieldname": "subscription_section", - "fieldtype": "Section Break", - "label": "Subscription" + "fieldtype": "Section Break" }, { "allow_on_submit": 1, @@ -681,10 +679,9 @@ "label": "Apply Tax Withholding Amount" }, { - "collapsible": 1, "fieldname": "taxes_and_charges_section", "fieldtype": "Section Break", - "label": "Taxes and Charges" + "hide_border": 1 }, { "depends_on": "eval:doc.party_type == 'Supplier'", @@ -848,6 +845,21 @@ "options": "No\nYes", "print_hide": 1, "search_index": 1 + }, + { + "fieldname": "taxes_and_charges_tab", + "fieldtype": "Tab Break", + "label": "Taxes and Charges" + }, + { + "fieldname": "more_information_tab", + "fieldtype": "Tab Break", + "label": "More Information" + }, + { + "fieldname": "subscription_tab", + "fieldtype": "Tab Break", + "label": "Subscription" } ], "index_web_pages_for_search": 1, @@ -863,7 +875,7 @@ "table_fieldname": "payment_entries" } ], - "modified": "2024-11-08 13:54:35.257266", + "modified": "2024-12-23 14:10:15.883272", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", @@ -911,4 +923,4 @@ "states": [], "title_field": "title", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 5dee8810b0c3dbd3313bdec7c16bbe9f1b88b2a3..2d4e08a4d8744d0487aba49a593d6f6eb3b65560 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -176,6 +176,9 @@ class PaymentEntry(AccountsController): self.validate_reference_documents() self.set_amounts() self.validate_amounts() + + add_regional_taxes(self) + self.apply_taxes() self.set_amounts_after_tax() self.clear_unallocated_reference_document_rows() @@ -3640,6 +3643,11 @@ def add_regional_gl_entries(gl_entries, doc): return +@erpnext.allow_regional +def add_regional_taxes(doc): + return + + @frappe.whitelist() def set_payment_entry_as_reconciled(payment_entry): # @dokos for field in ["unreconciled_from_amount", "unreconciled_to_amount", "unreconciled_amount"]: diff --git a/erpnext/accounts/doctype/suspense_vat_accounts/__init__.py b/erpnext/accounts/doctype/suspense_vat_accounts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/erpnext/accounts/doctype/suspense_vat_accounts/suspense_vat_accounts.json b/erpnext/accounts/doctype/suspense_vat_accounts/suspense_vat_accounts.json new file mode 100644 index 0000000000000000000000000000000000000000..e6fa3589319af68e95288524f47b6863ec1c4eef --- /dev/null +++ b/erpnext/accounts/doctype/suspense_vat_accounts/suspense_vat_accounts.json @@ -0,0 +1,50 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-12-23 14:58:24.439991", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "vat_suspense_account", + "vat_control_account", + "tax_rate" + ], + "fields": [ + { + "fieldname": "vat_suspense_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "VAT Suspense Account", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "vat_control_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "VAT Control Account", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "tax_rate", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Tax Rate", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2024-12-23 15:00:56.043889", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Suspense VAT Accounts", + "owner": "Administrator", + "permissions": [], + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/suspense_vat_accounts/suspense_vat_accounts.py b/erpnext/accounts/doctype/suspense_vat_accounts/suspense_vat_accounts.py new file mode 100644 index 0000000000000000000000000000000000000000..0a11462b74cbf0e77a42e480f731c3bd9c83b7a0 --- /dev/null +++ b/erpnext/accounts/doctype/suspense_vat_accounts/suspense_vat_accounts.py @@ -0,0 +1,24 @@ +# Copyright (c) 2024, Dokos SAS and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class SuspenseVATAccounts(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + tax_rate: DF.Float + vat_control_account: DF.Link + vat_suspense_account: DF.Link + # end: auto-generated types + + pass diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 1e2a18363d27e771344a2f3d919fa532dd5d9a04..0f06af346bd2df5e23e9f469019623dd8138aa3a 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -662,6 +662,8 @@ regional_overrides = { "erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_total_days": "erpnext.regional.france.assets.get_total_days", "erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.date_difference": "erpnext.regional.france.assets.date_difference", "erpnext.buying.doctype.supplier.supplier.company_query": "erpnext.regional.france.extensions.supplier.company_query", + "erpnext.accounts.doctype.payment_entry.payment_entry.add_regional_taxes": "erpnext.regional.france.extensions.payment_entry.add_regional_taxes", + "erpnext.setup.doctype.company.company.regional_validation": "erpnext.regional.france.extensions.company.regional_validation", }, "United Arab Emirates": { "erpnext.controllers.taxes_and_totals.update_itemised_tax_data": "erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 00a97da192124e46f0c9b83050a4fa865d7090c1..24ff6e7241e0fbda398f1d552a93e3805c55a46c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -396,6 +396,7 @@ execute:frappe.delete_doc_if_exists("DocType", "Integration References") execute:frappe.delete_doc_if_exists("DocType", "Social Media Post") erpnext.patches.dokos.v4_0.update_advance_for_down_payments #2024-05-207 erpnext.patches.dokos.v4_0.check_enable_pappers +erpnext.patches.dokos.v4_0.suspense_accounts_for_vat_on_payments # @dokos diff --git a/erpnext/patches/dokos/v4_0/suspense_accounts_for_vat_on_payments.py b/erpnext/patches/dokos/v4_0/suspense_accounts_for_vat_on_payments.py new file mode 100644 index 0000000000000000000000000000000000000000..08360088bd6749a92065ae45e1e072e11fd56d2b --- /dev/null +++ b/erpnext/patches/dokos/v4_0/suspense_accounts_for_vat_on_payments.py @@ -0,0 +1,39 @@ +import frappe + +from erpnext.regional.france.setup import setup + + +def execute(): + companies = frappe.get_all("Company", filters={"country": "France"}) + if not companies: + return + + setup() + + if not frappe.db.exists("Account", dict(account_number=4457, is_group=1)): + return + + for acc in [ + {"account_number": 445742, "account_name": "TVA collectée sur encaissements - 20%", "tax_rate": 20}, + {"account_number": 445642, "account_name": "TVA déductible sur encaissements - 20%", "tax_rate": 20}, + ]: + for company in companies: + account = frappe.new_doc("Account") + account.account_name = acc["account_name"] + account.account_number = acc["account_number"] + account.parent_account = frappe.db.get_value( + "Account", filters=dict(account_number=4457, is_group=1, company=company.name) + ) + account.account_type = "Tax" + account.tax_rate = acc["tax_rate"] + account.company = company.name + account.insert(ignore_if_duplicate=True) + + for company in companies: + try: + doc = frappe.get_doc("Company", company) + doc.flags.ignore_mandatory = True + doc.flags.ignore_links = True + doc.save() + except Exception: + continue diff --git a/erpnext/regional/france/extensions/company.py b/erpnext/regional/france/extensions/company.py index a81b2d592288834d7d2edd57ff0470437b368201..2e2e22cd958e7e0863e829c0a4b7d08f1236b554 100644 --- a/erpnext/regional/france/extensions/company.py +++ b/erpnext/regional/france/extensions/company.py @@ -1,9 +1,37 @@ import frappe from frappe import _ +from frappe.utils import flt from erpnext.regional.france.pappers.document import PappersDocument +def regional_validation(doc): + for row in doc.suspense_accounts_settings: + if row.vat_suspense_account == row.vat_control_account: + frappe.throw(_("Please set different account for VAT Suspense Account and VAT Control Account")) + + for field in ("vat_suspense_account", "vat_control_account"): + if flt(frappe.db.get_value("Account", field, "tax_rate")) != flt(row.tax_rate): + frappe.db.set_value("Account", field, "tax_rate", flt(row.tax_rate)) + + if not doc.suspense_accounts_settings: + for acc_numbers in ("44574%", "44564%"): + for account in frappe.get_all("Account", filters=dict(account_number=("like", acc_numbers), account_type="Tax", company=doc.name, is_group=0), fields=["name", "tax_rate"]): + if control_account := frappe.db.get_value("Account", dict( + account_number=("like", f"{acc_numbers[:4]}%"), + name=("!=", account.name), + tax_rate=account.tax_rate, + account_type="Tax", + company=doc.name + ), + ): + doc.append("suspense_accounts_settings", { + "vat_suspense_account": account.name, + "vat_control_account": control_account, + "tax_rate": account.tax_rate + }) + + @frappe.whitelist() def get_extrait_pappers(siren): response = PappersDocument().get_extrait_pappers(siren) diff --git a/erpnext/regional/france/extensions/payment_entry.py b/erpnext/regional/france/extensions/payment_entry.py new file mode 100644 index 0000000000000000000000000000000000000000..f3dbb7dfadbbe0715d133a76411ec3d495db8e29 --- /dev/null +++ b/erpnext/regional/france/extensions/payment_entry.py @@ -0,0 +1,79 @@ +import frappe +from frappe import _ +from frappe.utils import flt + + +def add_regional_taxes(doc): + if frappe.db.get_value("Company", doc.company, "country") != "France": + return + + company = frappe.get_cached_doc("Company", doc.company) + if not company.suspense_accounts_settings: + return + + supense_accounts_mapping = { + account.vat_suspense_account: account.vat_control_account + for account in company.suspense_accounts_settings + } + supense_accounts = supense_accounts_mapping.keys() + + doc.set("taxes", []) + for reference in doc.references: + res = get_tax_reversal_entries( + doc.company, reference.reference_doctype, reference.reference_name, supense_accounts + ) + if len(res) == 1: + amount = 0.0 + append_taxes(doc, res[0], supense_accounts_mapping, amount) + else: + for row in res: + amount = abs(row.get("total")) + append_taxes(doc, row, supense_accounts_mapping, amount) + + +def append_taxes(doc, row, supense_accounts_mapping, amount=None): + tax_rate = frappe.get_cached_value("Account", row["account"], "tax_rate") + tax_amount = min(amount, (doc.get("received_amount") * flt(tax_rate) / 100.0)) + + doc.append( + "taxes", + { + "add_deduct_tax": "Deduct", + "included_in_paid_amount": 1 if not amount else 0, + "charge_type": "On Paid Amount" if not amount else "Actual", + "account_head": row["account"], + "rate": tax_rate, + "tax_amount": tax_amount, + "description": row["remarks"], + }, + ) + + doc.append( + "taxes", + { + "add_deduct_tax": "Add", + "included_in_paid_amount": 1 if not amount else 0, + "charge_type": "On Paid Amount" if not amount else "Actual", + "account_head": supense_accounts_mapping.get(row.account), + "rate": tax_rate, + "tax_amount": tax_amount, + "description": row["remarks"], + }, + ) + + +def get_tax_reversal_entries(company, voucher_type, voucher_no, pending_vat_accounts): + filters = { + "is_cancelled": 0, + "account": ["in", pending_vat_accounts], + "voucher_type": voucher_type, + "voucher_no": voucher_no, + "company": company, + } + + return frappe.db.get_all( + "GL Entry", + filters=filters, + fields=["account", "sum(credit-debit) as total", "remarks"], + group_by="account", + ) diff --git a/erpnext/regional/france/tests/test_tax_templates.py b/erpnext/regional/france/tests/test_tax_templates.py index d4f9fd4e7aad1f73aee94d4d9fa9d1c9407ed02e..2d8b34dd91857149ab2839717e8170b94a9474cc 100644 --- a/erpnext/regional/france/tests/test_tax_templates.py +++ b/erpnext/regional/france/tests/test_tax_templates.py @@ -1,6 +1,5 @@ import frappe from frappe.tests import IntegrationTestCase -from frappe.utils import cstr, flt from erpnext.regional.france.tests.test_assets import create_company @@ -24,7 +23,13 @@ class TestTaxTemplatesForFrance(IntegrationTestCase): frappe.get_all( "Sales Taxes and Charges Template", {"company": "_Test French Company 2"}, pluck="name" ), - ["TVA 20% Collectée - _FC2", "TVA 10% Collectée - _FC2", "TVA 5.5% Collectée - _FC2", "TVA 2.1% Collectée - _FC2"], + [ + "TVA 20% Collectée - _FC2", + "TVA collectée sur encaissements - 20% - _FC2", + "TVA 10% Collectée - _FC2", + "TVA 5.5% Collectée - _FC2", + "TVA 2.1% Collectée - _FC2", + ], ) self.assertSequenceSubset( frappe.get_all("Account", {"company": "_Test French Company 2"}, pluck="name"), diff --git a/erpnext/regional/france/tests/test_vat.py b/erpnext/regional/france/tests/test_vat.py new file mode 100644 index 0000000000000000000000000000000000000000..00a9be857efa006e5347ae3ddcf2d7c8cd65c2ef --- /dev/null +++ b/erpnext/regional/france/tests/test_vat.py @@ -0,0 +1,353 @@ +import frappe +from frappe.tests import IntegrationTestCase +from frappe.utils import nowdate + +from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.regional.france.tests.test_assets import create_company + + +class TestVATForFrance(IntegrationTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + company = "_Test French Company 3" + cls.company = create_company( + company_name=company, country="France", currency="EUR", chart_of_accounts="Plan Comptable Général" + ) + frappe.flags.country = "France" + + cls.debit_account = frappe.new_doc("Account") + cls.debit_account.update( + { + "account_name": "_Test Receivable EUR", + "parent_account": "C - Créances" + " - " + frappe.db.get_value("Company", company, "abbr"), + "company": company, + "is_group": 0, + "account_type": "Receivable", + "account_currency": "EUR", + } + ) + cls.debit_account.insert(ignore_if_duplicate=True) + + cls.credit_account = frappe.new_doc("Account") + cls.credit_account.update( + { + "account_name": "_Test Payables EUR", + "parent_account": "F - Dettes fournisseurs et comptes rattachés - Liability" + + " - " + + frappe.db.get_value("Company", company, "abbr"), + "company": company, + "is_group": 0, + "account_type": "Payable", + "account_currency": "EUR", + } + ) + cls.credit_account.insert(ignore_if_duplicate=True) + + cls.customer = frappe.new_doc("Customer") + cls.customer.update( + { + "customer_group": "_Test Customer Group", + "customer_name": "_Test Customer EUR", + "customer_type": "Individual", + "territory": "_Test Territory", + "default_currency": "EUR", + } + ) + cls.customer.insert(ignore_if_duplicate=True) + + cls.supplier = frappe.new_doc("Supplier") + cls.supplier.update( + { + "supplier_group": "_Test Supplier Group", + "supplier_name": "_Test Supplier EUR", + "territory": "_Test Territory", + "default_currency": "EUR", + } + ) + cls.supplier.insert(ignore_if_duplicate=True) + + if tax_template := frappe.db.get_value( + "Sales Taxes and Charges Template", dict(company=company, is_default=True) + ): + frappe.db.set_value("Sales Taxes and Charges Template", tax_template, "is_default", False) + + @classmethod + def tearDownClass(cls): + frappe.flags.country = None + + def test_accounts_are_created(self): + all_accounts = frappe.get_all( + "Account", filters={"account_number": ("is", "set")}, pluck="account_number" + ) + self.assertIn("445742", all_accounts) + self.assertIn("445642", all_accounts) + + company = frappe.get_doc("Company", TestVATForFrance.company) + company.suspense_accounts_settings = [] + company.save() + for row in company.suspense_accounts_settings: + self.assertEqual(row.vat_suspense_account[:4], row.vat_control_account[:4]) + + @IntegrationTestCase.change_settings("Accounts Settings", {"mandatory_accounting_journal": 0}) + def test_vat_on_sales_payments(self): + company = frappe.get_doc("Company", TestVATForFrance.company) + company.suspense_accounts_settings = [] + company.save() + company_abbr = company.abbr + + si = create_sales_invoice( + company=TestVATForFrance.company, + currency="EUR", + customer=TestVATForFrance.customer.name, + debit_to=TestVATForFrance.debit_account.name, + cost_center=f"Main - {company_abbr}", + income_account=f"701 - Ventes de produits finis - {company_abbr}", + do_not_save=1, + ) + si.taxes_and_charges = f"TVA collectée sur encaissements - 20% - {company_abbr}" + si.set_missing_values() + si.set_taxes() + si.calculate_taxes_and_totals() + si.insert() + si.submit() + + gl_entries = frappe.get_all( + "GL Entry", filters={"voucher_type": si.doctype, "voucher_no": si.name}, pluck="account" + ) + + self.assertIn(f"445742 - TVA collectée sur encaissements - 20% - {company_abbr}", gl_entries) + + pe = get_payment_entry("Sales Invoice", si.name) + pe.reference_no = "1" + pe.reference_date = nowdate() + pe.insert() + pe.submit() + + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_type": pe.doctype, "voucher_no": pe.name}, + fields=["account", "debit", "credit"], + ) + accounts = [gl.account for gl in gl_entries] + self.assertIn(f"445742 - TVA collectée sur encaissements - 20% - {company_abbr}", accounts) + self.assertIn(f"445720 - TVA 20% Collectée - {company_abbr}", accounts) + + tax_map = { + f"445742 - TVA collectée sur encaissements - 20% - {company_abbr}": "debit", + f"445720 - TVA 20% Collectée - {company_abbr}": "credit", + } + + result = [] + for gl_entry in gl_entries: + if field := tax_map.get(gl_entry.account): + result.append(gl_entry.get(field)) + + self.assertTrue(len(result), 2) + self.assertEqual(result[0], result[1]) + + @IntegrationTestCase.change_settings("Accounts Settings", {"mandatory_accounting_journal": 0}) + def test_multiple_vat_on_sales_payments(self): + company = frappe.get_doc("Company", TestVATForFrance.company) + + create_sales_tax_templates(company) + create_test_items(company) + company.suspense_accounts_settings = [] + company.save() + + si = create_sales_invoice( + company=TestVATForFrance.company, + currency="EUR", + customer=TestVATForFrance.customer.name, + debit_to=TestVATForFrance.debit_account.name, + cost_center=f"Main - {company.abbr}", + income_account=f"701 - Ventes de produits finis - {company.abbr}", + do_not_save=1, + ) + si.set("items", []) + for item in ["VATONPAYMENTS_10", "VATONPAYMENTS_20"]: + si.append("items", {"item_code": item, "rate": 100}) + + si.set_missing_values() + si.set_taxes() + si.calculate_taxes_and_totals() + si.insert() + si.submit() + + gl_entries = frappe.get_all( + "GL Entry", filters={"voucher_type": si.doctype, "voucher_no": si.name}, pluck="account" + ) + si.reload() + + self.assertIn(f"445742 - TVA collectée sur encaissements - 20% - {company.abbr}", gl_entries) + self.assertIn(f"445741 - TVA collectée sur encaissements - 10% - {company.abbr}", gl_entries) + + pe = get_payment_entry("Sales Invoice", si.name) + pe.reference_no = "1" + pe.reference_date = nowdate() + pe.insert() + pe.submit() + + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_type": pe.doctype, "voucher_no": pe.name}, + fields=["account", "debit", "credit"], + ) + accounts = [gl.account for gl in gl_entries] + self.assertIn(f"445742 - TVA collectée sur encaissements - 20% - {company.abbr}", accounts) + self.assertIn(f"445720 - TVA 20% Collectée - {company.abbr}", accounts) + self.assertIn(f"445741 - TVA collectée sur encaissements - 10% - {company.abbr}", accounts) + self.assertIn(f"445710 - TVA 10% Collectée - {company.abbr}", accounts) + + tax_map = { + f"445742 - TVA collectée sur encaissements - 20% - {company.abbr}": "debit", + f"445720 - TVA 20% Collectée - {company.abbr}": "credit", + f"445741 - TVA collectée sur encaissements - 10% - {company.abbr}": "debit", + f"445710 - TVA 10% Collectée - {company.abbr}": "credit", + } + + result = [] + for gl_entry in gl_entries: + if field := tax_map.get(gl_entry.account): + result.append(gl_entry.get(field)) + + self.assertTrue(len(result), 4) + self.assertEqual(result[0], result[1]) + self.assertEqual(result[2], result[3]) + + @IntegrationTestCase.change_settings("Accounts Settings", {"mandatory_accounting_journal": 0}) + def test_vat_on_purchase_payments(self): + company = frappe.get_doc("Company", TestVATForFrance.company) + + create_purchase_tax_templates(company) + + company.suspense_accounts_settings = [] + company.save() + company_abbr = company.abbr + + pi = make_purchase_invoice( + company=TestVATForFrance.company, + currency="EUR", + supplier=TestVATForFrance.supplier.name, + credit_to=TestVATForFrance.credit_account.name, + cost_center=f"Main - {company_abbr}", + warehouse=frappe.db.get_value("Warehouse", dict(is_group=0, company=company.name)), + expense_account=f"600 - Achats - {company_abbr}", + do_not_save=1, + ) + pi.taxes_and_charges = f"TVA déductible sur encaissements - 20% - {company_abbr}" + pi.set_missing_values() + pi.set_taxes() + pi.calculate_taxes_and_totals() + pi.insert() + pi.submit() + + gl_entries = frappe.get_all( + "GL Entry", filters={"voucher_type": pi.doctype, "voucher_no": pi.name}, pluck="account" + ) + + self.assertIn(f"445642 - TVA déductible sur encaissements - 20% - {company_abbr}", gl_entries) + + pe = get_payment_entry("Purchase Invoice", pi.name) + pe.reference_no = "1" + pe.reference_date = nowdate() + pe.insert() + pe.submit() + + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_type": pe.doctype, "voucher_no": pe.name}, + fields=["account", "debit", "credit"], + ) + accounts = [gl.account for gl in gl_entries] + self.assertIn(f"445642 - TVA déductible sur encaissements - 20% - {company_abbr}", accounts) + self.assertIn( + f"445662 - TVA déductible sur acquisition intracommunautaires - {company_abbr}", accounts + ) + + tax_map = { + f"445642 - TVA déductible sur encaissements - 20% - {company_abbr}": "credit", + f"445662 - TVA déductible sur acquisition intracommunautaires - {company_abbr}": "debit", + } + + result = [] + for gl_entry in gl_entries: + if field := tax_map.get(gl_entry.account): + result.append(gl_entry.get(field)) + + self.assertTrue(len(result), 2) + self.assertEqual(result[0], result[1]) + + +def create_sales_tax_templates(company): + if not frappe.db.exists("Account", f"445741 - TVA collectée sur encaissements - 10% - {company.abbr}"): + account = frappe.new_doc("Account") + account.account_number = 445741 + account.account_name = "TVA collectée sur encaissements - 10%" + account.company = company.name + account.account_type = "Tax" + account.parent_account = ( + f"4457 - Taxes sur le chiffre d'affaires collectées par l'entreprise - {company.abbr}" + ) + account.tax_rate = 10.0 + account.insert(ignore_if_duplicate=True) + + if not frappe.db.exists("Item Tax Template", f"TVA collectée sur encaissements - 10% - {company.abbr}"): + tax_template = frappe.new_doc("Item Tax Template") + tax_template.title = "TVA collectée sur encaissements - 10%" + tax_template.company = company.name + tax_template.applicable_for = "Sales" + tax_template.append( + "taxes", + { + "tax_type": frappe.db.get_value("Account", dict(account_number=445741, company=company.name)), + "tax_rate": 10.0, + "description": "TVA 10%", + }, + ) + tax_template.insert(ignore_if_duplicate=True) + + +def create_purchase_tax_templates(company): + if not frappe.db.exists("Account", f"445642 - TVA déductible sur encaissements - 20% - {company.abbr}"): + account = frappe.new_doc("Account") + account.account_number = 445642 + account.account_name = "TVA déductible sur encaissements -20%" + account.company = company.name + account.account_type = "Tax" + account.parent_account = f"4456 - Taxes sur le chiffre d'affaires déductibles - {company.abbr}" + account.tax_rate = 20.0 + account.insert(ignore_if_duplicate=True) + + if not frappe.db.exists("Item Tax Template", f"TVA déductible sur encaissements - 20% - {company.abbr}"): + tax_template = frappe.new_doc("Item Tax Template") + tax_template.title = "TVA déductible sur encaissements - 20%" + tax_template.company = company.name + tax_template.applicable_for = "Purchases" + tax_template.append( + "taxes", + { + "tax_type": frappe.db.get_value("Account", dict(account_number=445642, company=company.name)), + "tax_rate": 20.0, + "description": "TVA 20%", + }, + ) + tax_template.insert(ignore_if_duplicate=True) + + +def create_test_items(company): + for item in ["VATONPAYMENTS_10", "VATONPAYMENTS_20"]: + doc = frappe.new_doc("Item") + doc.item_code = item + doc.item_group = "Services" + doc.stock_uom = "_Test UOM" + doc.append( + "taxes", + { + "company": company.name, + "item_tax_template": f"TVA collectée sur encaissements - {item[-2:]}% - {company.abbr}", + }, + ) + doc.insert(ignore_if_duplicate=True) diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index b3e627a84a9b234231a314c21fa6702a2413c01d..486a5728a492b06996a449ffd282dab3f2bab090 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -121,6 +121,9 @@ "default_in_transit_warehouse", "manufacturing_section", "default_operating_cost_account", + "taxes_tab", + "section_break_wogu", + "suspense_accounts_settings", "dashboard_tab" ], "fields": [ @@ -853,6 +856,23 @@ { "fieldname": "column_break_pepz", "fieldtype": "Column Break" + }, + { + "fieldname": "taxes_tab", + "fieldtype": "Tab Break", + "label": "Taxes" + }, + { + "fieldname": "section_break_wogu", + "fieldtype": "Section Break" + }, + { + "depends_on": "eval:doc.country==\"France\"", + "documentation_url": "https://doc.dokos.io/dokos/parametrage/demarrage/taxes", + "fieldname": "suspense_accounts_settings", + "fieldtype": "Table", + "label": "Payment Suspense Accounts Settings", + "options": "Suspense VAT Accounts" } ], "icon": "fa fa-building", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 561b755dcd9f403dbefbfbdef71c124ac6b5168d..7b4b17ef8931d6cdbeef1fc8b23fe5ebcd7f0451 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -13,6 +13,7 @@ from frappe.desk.page.setup_wizard.setup_wizard import make_records from frappe.utils import cint, formatdate, get_timestamp, today from frappe.utils.nestedset import NestedSet, rebuild_tree +import erpnext from erpnext.accounts.doctype.account.account import get_account_currency from erpnext.setup.setup_wizard.operations.taxes_setup import setup_taxes_and_charges @@ -26,13 +27,14 @@ class Company(NestedSet): if TYPE_CHECKING: from frappe.types import DF + from erpnext.accounts.doctype.suspense_vat_accounts.suspense_vat_accounts import SuspenseVATAccounts + abbr: DF.Data accumulated_depreciation_account: DF.Link | None allow_account_creation_against_child_company: DF.Check asset_received_but_not_billed: DF.Link | None auto_err_frequency: DF.Literal["Daily", "Weekly", "Monthly"] auto_exchange_rate_revaluation: DF.Check - book_advance_payments_in_separate_party_account: DF.Check capital_work_in_progress_account: DF.Link | None chart_of_accounts: DF.Literal[None] company_description: DF.TextEditor | None @@ -98,6 +100,7 @@ class Company(NestedSet): stock_adjustment_account: DF.Link | None stock_received_but_not_billed: DF.Link | None submit_err_jv: DF.Check + suspense_accounts_settings: DF.Table[SuspenseVATAccounts] tax_id: DF.Data | None total_monthly_sales: DF.Currency transactions_annual_history: DF.Code | None @@ -152,6 +155,9 @@ class Company(NestedSet): self.set_chart_of_accounts() self.validate_parent_company() + frappe.flags.company = self.name + regional_validation(self) + def validate_abbr(self): if not self.abbr: self.abbr = "".join(c[0] for c in self.company_name.split()).upper() @@ -904,3 +910,8 @@ def get_default_company_address(name, sort_key="is_primary_address", existing_ad return max(out, key=lambda x: x[1])[0] # find max by sort_key else: return None + + +@erpnext.allow_regional +def regional_validation(doc): + return diff --git a/erpnext/setup/doctype/company/regional/france.js b/erpnext/setup/doctype/company/regional/france.js index 418d3a16424fdefab1dcef09d124fdd34abaa47f..b1ba904b17042dbd1e91d978e6ec2ba0da253563 100644 --- a/erpnext/setup/doctype/company/regional/france.js +++ b/erpnext/setup/doctype/company/regional/france.js @@ -1,4 +1,25 @@ frappe.ui.form.on("Company", { + setup(frm) { + frm.set_query("vat_suspense_account", "suspense_accounts_settings", () => { + return { + filters: { + company: frm.doc.name, + is_group: 0, + account_type: "Tax" + } + }; + }); + + frm.set_query("vat_control_account", "suspense_accounts_settings", () => { + return { + filters: { + company: frm.doc.name, + is_group: 0, + account_type: "Tax" + } + }; + }); + }, refresh(frm) { if (frm.doc.siren_number) { frm.add_custom_button( diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index cbe638f278a7e1ea37240ce36988bf8294566026..2b0c5f4d8b988a7634a3f4d5384174e34945fdfa 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -418,6 +418,21 @@ } ] }, + { + "title": "TVA collectée sur encaissements - 20%", + "taxes": [ + { + "account_head": { + "account_name": "TVA collectée sur encaissements - 20%", + "account_number": "445742", + "root_type": "Liability", + "tax_rate": 20.0 + }, + "description": "TVA 20%", + "rate": 20 + } + ] + }, { "title": "TVA 10% Collectée", "taxes": [ @@ -480,6 +495,21 @@ } ] }, + { + "title": "TVA déductible sur encaissements - 20%", + "taxes": [ + { + "account_head": { + "account_name": "TVA déductible sur encaissements - 20%", + "account_number": "445642", + "root_type": "Asset", + "tax_rate": 20.0 + }, + "description": "TVA 20%", + "rate": 20 + } + ] + }, { "title": "TVA 10% Déductible", "taxes": [ @@ -618,6 +648,21 @@ } ] }, + { + "title": "TVA collectée sur encaissements - 20%", + "taxes": [ + { + "tax_type": { + "account_name": "TVA collectée sur encaissements - 20%", + "account_number": "445742", + "root_type": "Liability", + "tax_rate": 20.0 + }, + "description": "TVA 20%", + "tax_rate": 20 + } + ] + }, { "title": "TVA 10% Collectée", "taxes": [ @@ -679,6 +724,22 @@ ], "applicable_for": "Purchases" }, + { + "title": "TVA déductible sur encaissements - 20%", + "taxes": [ + { + "tax_type": { + "account_name": "TVA déductible sur encaissements - 20%", + "account_number": "445642", + "root_type": "Asset", + "tax_rate": 20.0 + }, + "description": "TVA 20%", + "tax_rate": 20 + } + ], + "applicable_for": "Purchases" + }, { "title": "TVA 10% Déductible", "taxes": [ @@ -744,6 +805,23 @@ ], "applicable_for": "Purchases" }, + { + "title": "TVA déductible sur encaissements - 20%", + "taxes": [ + { + "tax_type": { + "account_name": "TVA déductible sur encaissements - 20%", + "account_number": "445642", + "root_type": "Asset", + "tax_rate": 20.0 + }, + "included_in_print_rate": 1, + "description": "TVA 20%", + "tax_rate": 20 + } + ], + "applicable_for": "Purchases" + }, { "title": "TVA 10% Déductible - Incluse dans le prix", "taxes": [