diff --git a/CHANGELOG.md b/CHANGELOG.md index ce2fae89..57bb0793 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,27 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Added +- ae: added UAE regime + +## [v0.205.1] - 2024-11-19 + +### Added + +- `org`: `Address` includes `LineOne()`, `LineTwo()`, `CompleteNumber()` methods to help with conversion to other formats with some regional formatting. + +### Changes + +- `bill`: `Invoice` can now have empty lines if discounts or charges present. + +### Fixes + +- `ch`: Deleted Supplier validation (not needed for under 2300 CHF/year) +- `bill`: `Invoice` `GetExtensions` method now works correctly if missing totals [Issue #424](https://github.com/invopop/gobl/issues/424). + +## [v0.205.0] - 2024-11-12 + +### Added + - `org`: `Address` now includes a `state` code, for countries that require them. - `es-tbai-v1`: normalize address information to automatically add new `es-tbai-region` extension to invoices. - `org`: `Inbox` now supports `email` field, with auto-normalization of URLs and emails in the `code` field. diff --git a/data/regimes/ae.json b/data/regimes/ae.json new file mode 100644 index 00000000..c8af2bc0 --- /dev/null +++ b/data/regimes/ae.json @@ -0,0 +1,172 @@ +{ + "$schema": "https://gobl.org/draft-0/tax/regime-def", + "name": { + "ar": "الإمارات العربية المتحدة", + "en": "United Arab Emirates" + }, + "time_zone": "Asia/Dubai", + "country": "AE", + "currency": "AED", + "tags": [ + { + "schema": "bill/invoice", + "list": [ + { + "key": "simplified", + "name": { + "de": "Vereinfachte Rechnung", + "en": "Simplified Invoice", + "es": "Factura Simplificada", + "it": "Fattura Semplificata" + }, + "desc": { + "de": "Wird für B2C-Transaktionen verwendet, wenn die Kundendaten nicht verfügbar sind. Bitte wenden Sie sich an die örtlichen Behörden, um die Grenzwerte zu ermitteln.", + "en": "Used for B2C transactions when the client details are not available, check with local authorities for limits.", + "es": "Usado para transacciones B2C cuando los detalles del cliente no están disponibles, consulte con las autoridades locales para los límites.", + "it": "Utilizzato per le transazioni B2C quando i dettagli del cliente non sono disponibili, controllare con le autorità locali per i limiti." + } + }, + { + "key": "reverse-charge", + "name": { + "de": "Umkehr der Steuerschuld", + "en": "Reverse Charge", + "es": "Inversión del Sujeto Pasivo", + "it": "Inversione del soggetto passivo" + } + }, + { + "key": "self-billed", + "name": { + "de": "Rechnung durch den Leistungsempfänger", + "en": "Self-billed", + "es": "Facturación por el destinatario", + "it": "Autofattura" + } + }, + { + "key": "customer-rates", + "name": { + "de": "Kundensätze", + "en": "Customer rates", + "es": "Tarifas aplicables al destinatario", + "it": "Aliquote applicabili al destinatario" + } + }, + { + "key": "partial", + "name": { + "de": "Teilweise", + "en": "Partial", + "es": "Parcial", + "it": "Parziale" + } + } + ] + } + ], + "scenarios": [ + { + "schema": "bill/invoice", + "list": [ + { + "tags": [ + "reverse-charge" + ], + "note": { + "key": "legal", + "src": "reverse-charge", + "text": "Reverse Charge" + } + }, + { + "tags": [ + "simplified" + ], + "note": { + "key": "legal", + "src": "simplified", + "text": "Simplified Tax Invoice" + } + } + ] + } + ], + "corrections": [ + { + "schema": "bill/invoice", + "types": [ + "credit-note" + ] + } + ], + "categories": [ + { + "code": "VAT", + "name": { + "ar": "ضريبة القيمة المضافة", + "en": "VAT" + }, + "title": { + "ar": "ضريبة القيمة المضافة", + "en": "Value Added Tax" + }, + "rates": [ + { + "key": "zero", + "name": { + "ar": "معدل صفر", + "en": "Zero Rate" + }, + "desc": { + "ar": "نسبة ضريبة قيمة مضافة 0٪ تطبق على الصادرات المحددة والمناطق المعينة والخدمات الأساسية.", + "en": "A VAT rate of 0% applicable to specific exports, designated areas, and essential services." + }, + "values": [ + { + "percent": "0.0%" + } + ] + }, + { + "key": "standard", + "name": { + "ar": "معدل قياسي", + "en": "Standard Rate" + }, + "desc": { + "ar": "ينطبق على معظم السلع والخدمات ما لم ينص على خلاف ذلك.", + "en": "Applies to most goods and services unless specified otherwise." + }, + "values": [ + { + "since": "2018-01-01", + "percent": "5%" + } + ] + }, + { + "key": "exempt", + "name": { + "ar": "معفى", + "en": "Exempt" + }, + "desc": { + "ar": "بعض السلع والخدمات، مثل الخدمات المالية والعقارات السكنية، معفاة من ضريبة القيمة المضافة.", + "en": "Certain goods and services, such as financial services and residential real estate, are exempt from VAT." + }, + "exempt": true + } + ], + "sources": [ + { + "title": { + "ar": "الهيئة الاتحادية للضرائب", + "en": "Federal Tax Authority - UAE VAT Regulations" + }, + "url": "https://www.tax.gov.ae" + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/ae/invoice-ae-ae-stnr.yaml b/examples/ae/invoice-ae-ae-stnr.yaml new file mode 100644 index 00000000..a0de1e97 --- /dev/null +++ b/examples/ae/invoice-ae-ae-stnr.yaml @@ -0,0 +1,47 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +uuid: "3aea7b56-59d8-4beb-90bd-f8f280d852a0" +currency: "AED" +issue_date: "2022-02-01" +series: "SAMPLE" +code: "001" + +supplier: + tax_id: + country: "AE" + code: "123456789012346" + name: "Provide One LLC" + emails: + - addr: "billing@example.ae" + addresses: + - num: "16" + street: "Sheikh Zayed Road" + locality: "Dubai" + code: "00000" + country: "AE" + +customer: + tax_id: + country: "AE" + code: "123456789012345" + name: "Sample Consumer" + emails: + - addr: "email@sample.ae" + addresses: + - num: "25" + street: "Al Maryah Island" + locality: "Abu Dhabi" + code: "00000" + country: "AE" + +lines: + - quantity: 20 + item: + name: "Development services" + price: "90.00" + unit: "h" + discounts: + - percent: "5%" + reason: "Special discount" + taxes: + - cat: VAT + rate: standard diff --git a/examples/ae/invoice-ae-simplified.yaml b/examples/ae/invoice-ae-simplified.yaml new file mode 100644 index 00000000..a3f2a6a8 --- /dev/null +++ b/examples/ae/invoice-ae-simplified.yaml @@ -0,0 +1,35 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +uuid: "3aea7b56-59d8-4beb-90bd-f8f280d852a0" +currency: "AED" +issue_date: "2022-02-01" +series: "SAMPLE" +code: "001" +tax: + tags: + - "simplified" +supplier: + tax_id: + country: "AE" + code: "123456789012346" + name: "Provide One LLC" + emails: + - addr: "billing@example.ae" + addresses: + - num: "16" + street: "Sheikh Zayed Road" + locality: "Dubai" + code: "00000" + country: "AE" + +lines: + - quantity: 20 + item: + name: "Development services" + price: "90.00" + unit: "h" + discounts: + - percent: "10%" + reason: "Special discount" + taxes: + - cat: VAT + rate: "standard" \ No newline at end of file diff --git a/examples/ae/out/invoice-ae-ae-stnr.json b/examples/ae/out/invoice-ae-ae-stnr.json new file mode 100644 index 00000000..a75b109c Binary files /dev/null and b/examples/ae/out/invoice-ae-ae-stnr.json differ diff --git a/examples/ae/out/invoice-ae-simplified.json b/examples/ae/out/invoice-ae-simplified.json new file mode 100644 index 00000000..cc875719 Binary files /dev/null and b/examples/ae/out/invoice-ae-simplified.json differ diff --git a/regimes/ae/README.md b/regimes/ae/README.md new file mode 100644 index 00000000..401f40c2 --- /dev/null +++ b/regimes/ae/README.md @@ -0,0 +1,41 @@ +# United Arab Emirates (AE) Tax Regime + +This document provides an overview of the tax regime in the United Arab Emirates. + +## Value-Added Tax (VAT) + +The UAE VAT system categorizes goods and services into three main VAT rates: + +- **Standard Rate (5%)**: Applies to most goods and services in the UAE. +- **Zero Rate (0%)**: Applies to certain essential goods, exports, and specific services. +- **Exempt**: Certain types of goods and services are exempt from VAT, including some financial services or residential real estate. + +Businesses required to charge VAT must register with the UAE Federal Tax Authority to obtain a Tax Registration Number (TRN). Registration can be completed online via the [VAT registration portal](https://tax.gov.ae/en/services/vat.registration.aspx). + +## VAT Registration Requirements + +Businesses in the UAE must evaluate their eligibility for VAT registration based on their revenue and expenses. The registration requirements are as follows: + +- **Mandatory Registration**: Businesses are required to register for VAT if the total value of taxable supplies and imports exceeds **AED 375,000** in a 12-month period. +- **Voluntary Registration**: Businesses may choose to register voluntarily if the total value of taxable supplies and imports, or taxable expenses, exceeds **AED 187,500** within a 12-month period. + +**Note:** Businesses below the voluntary registration threshold are not permitted to register for VAT and, therefore, will issue invoices without a TRN. + +For more information, visit the [Federal Tax Authority website](https://tax.gov.ae/en/taxes/Vat/vat.topics/registration.for.vat.aspx) + +### TRN Validation + +Currently, no checksum method is available for validating the TRN. Therefore, verification must be performed directly through the official UAE government website. + +### Alcohol Sales Tax + +In Dubai, a **30% sales tax** on alcohol previously existed but has since been abolished. Some sources suggest that a similar tax persists in Abu Dhabi, but without official confirmation, this has not been included in the UAE tax regime. + +## VAT Invoicing Requirements + +There are two types of VAT invoices: the standard and the simplified invoice. + +**Simplified VAT Invoice**: Allowed in the following cases: + +- When the recipient of goods or services is **not VAT registered**. +- When the recipient of goods or services **is VAT registered**, and the transaction value does not exceed **AED 10,000**. diff --git a/regimes/ae/ae.go b/regimes/ae/ae.go new file mode 100644 index 00000000..23fe55f5 --- /dev/null +++ b/regimes/ae/ae.go @@ -0,0 +1,64 @@ +// Package ae provides the tax region definition for United Arab Emirates. +package ae + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/currency" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/regimes/common" + "github.com/invopop/gobl/tax" +) + +func init() { + tax.RegisterRegimeDef(New()) +} + +// New provides the tax region definition for AE. +func New() *tax.RegimeDef { + return &tax.RegimeDef{ + Country: "AE", + Currency: currency.AED, + Name: i18n.String{ + i18n.EN: "United Arab Emirates", + i18n.AR: "الإمارات العربية المتحدة", + }, + TimeZone: "Asia/Dubai", + Tags: []*tax.TagSet{ + common.InvoiceTags(), + }, + Scenarios: []*tax.ScenarioSet{ + invoiceScenarios, + }, + Corrections: []*tax.CorrectionDefinition{ + { + Schema: bill.ShortSchemaInvoice, + Types: []cbc.Key{ + bill.InvoiceTypeCreditNote, + }, + }, + }, + Validator: Validate, + Normalizer: Normalize, + Categories: taxCategories, + } +} + +// Validate function assesses the document type to determine if validation is required. +// Note that, under the AE tax regime, validation of the supplier's tax ID is not necessary if it does not meet the specified threshold (refer to the README section for more details). +func Validate(doc interface{}) error { + switch obj := doc.(type) { + case *tax.Identity: + return validateTaxIdentity(obj) + } + return nil +} + +// Normalize attempts to clean up the object passed to it. +func Normalize(doc any) { + switch obj := doc.(type) { + case *tax.Identity: + tax.NormalizeIdentity(obj) + + } +} diff --git a/regimes/ae/scenarios.go b/regimes/ae/scenarios.go new file mode 100644 index 00000000..071fb240 --- /dev/null +++ b/regimes/ae/scenarios.go @@ -0,0 +1,32 @@ +// Package ae provides tax scenarios specific to UAE VAT regulations. +package ae + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/tax" +) + +var invoiceScenarios = &tax.ScenarioSet{ + Schema: bill.ShortSchemaInvoice, + List: []*tax.Scenario{ + // Reverse Charges + { + Tags: []cbc.Key{tax.TagReverseCharge}, + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: tax.TagReverseCharge, + Text: "Reverse Charge", + }, + }, + // Simplified Tax Invoice + { + Tags: []cbc.Key{tax.TagSimplified}, + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: tax.TagSimplified, + Text: "Simplified Tax Invoice", + }, + }, + }, +} diff --git a/regimes/ae/scenarios_test.go b/regimes/ae/scenarios_test.go new file mode 100644 index 00000000..1d02870b --- /dev/null +++ b/regimes/ae/scenarios_test.go @@ -0,0 +1,101 @@ +package ae_test + +import ( + "testing" + + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cal" + "github.com/invopop/gobl/num" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/tax" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func testInvoiceReverseCharge(t *testing.T) *bill.Invoice { + t.Helper() + return &bill.Invoice{ + Code: "123TEST", + Currency: "AED", + Supplier: &org.Party{ + Name: "Test Supplier", + TaxID: &tax.Identity{ + Country: "AE", + Code: "123456789012345", + }, + }, + Customer: &org.Party{ + Name: "Test Customer", + TaxID: &tax.Identity{ + Country: "AE", + Code: "123456789012346", + }, + }, + IssueDate: cal.MakeDate(2023, 1, 15), + Tags: tax.WithTags(tax.TagReverseCharge), + Lines: []*bill.Line{ + { + Quantity: num.MakeAmount(5, 0), + Item: &org.Item{ + Name: "Service Item", + Price: num.MakeAmount(5000, 2), + }, + Taxes: tax.Set{ + { + Category: "VAT", + Rate: "standard", + }, + }, + }, + }, + } +} + +func testInvoiceSimplified(t *testing.T) *bill.Invoice { + t.Helper() + return &bill.Invoice{ + Code: "123TEST", + Currency: "AED", + Supplier: &org.Party{ + Name: "Test Supplier", + TaxID: &tax.Identity{ + Country: "AE", + Code: "123456789012345", + }, + }, + IssueDate: cal.MakeDate(2023, 1, 15), + Tags: tax.WithTags(tax.TagSimplified), + Lines: []*bill.Line{ + { + Quantity: num.MakeAmount(3, 0), + Item: &org.Item{ + Name: "Product Item", + Price: num.MakeAmount(2000, 2), + }, + Taxes: tax.Set{ + { + Category: "VAT", + Rate: "standard", + }, + }, + }, + }, + } +} + +func TestInvoiceScenarios(t *testing.T) { + i := testInvoiceReverseCharge(t) + require.NoError(t, i.Calculate()) + require.NoError(t, i.Validate()) + assert.Len(t, i.Notes, 1) + assert.Equal(t, i.Notes[0].Src, tax.TagReverseCharge) + assert.Equal(t, i.Notes[0].Text, "Reverse Charge") + + i = testInvoiceSimplified(t) + require.NoError(t, i.Calculate()) + require.NoError(t, i.Validate()) + assert.Len(t, i.Notes, 1) + assert.Equal(t, i.Notes[0].Src, tax.TagSimplified) + assert.Equal(t, i.Notes[0].Text, "Simplified Tax Invoice") +} diff --git a/regimes/ae/tax_categories.go b/regimes/ae/tax_categories.go new file mode 100644 index 00000000..69154e49 --- /dev/null +++ b/regimes/ae/tax_categories.go @@ -0,0 +1,80 @@ +// Package ae defines VAT tax categories specific to the United Arab Emirates. +package ae + +import ( + "github.com/invopop/gobl/cal" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/num" + "github.com/invopop/gobl/tax" +) + +var taxCategories = []*tax.CategoryDef{ + { + Code: tax.CategoryVAT, + Name: i18n.String{ + i18n.EN: "VAT", + i18n.AR: "ضريبة القيمة المضافة", + }, + Title: i18n.String{ + i18n.EN: "Value Added Tax", + i18n.AR: "ضريبة القيمة المضافة", + }, + Sources: []*tax.Source{ + { + Title: i18n.String{ + i18n.EN: "Federal Tax Authority - UAE VAT Regulations", + i18n.AR: "الهيئة الاتحادية للضرائب", + }, + URL: "https://www.tax.gov.ae", + }, + }, + Retained: false, + Rates: []*tax.RateDef{ + { + Key: tax.RateZero, + Name: i18n.String{ + i18n.EN: "Zero Rate", + i18n.AR: "معدل صفر", + }, + Description: i18n.String{ + i18n.EN: "A VAT rate of 0% applicable to specific exports, designated areas, and essential services.", + i18n.AR: "نسبة ضريبة قيمة مضافة 0٪ تطبق على الصادرات المحددة والمناطق المعينة والخدمات الأساسية.", + }, + Values: []*tax.RateValueDef{ + { + Percent: num.MakePercentage(0, 3), + }, + }, + }, + { + Key: tax.RateStandard, + Name: i18n.String{ + i18n.EN: "Standard Rate", + i18n.AR: "معدل قياسي", + }, + Description: i18n.String{ + i18n.EN: "Applies to most goods and services unless specified otherwise.", + i18n.AR: "ينطبق على معظم السلع والخدمات ما لم ينص على خلاف ذلك.", + }, + Values: []*tax.RateValueDef{ + { + Since: cal.NewDate(2018, 1, 1), + Percent: num.MakePercentage(5, 2), + }, + }, + }, + { + Key: tax.RateExempt, + Name: i18n.String{ + i18n.EN: "Exempt", + i18n.AR: "معفى", + }, + Exempt: true, + Description: i18n.String{ + i18n.EN: "Certain goods and services, such as financial services and residential real estate, are exempt from VAT.", + i18n.AR: "بعض السلع والخدمات، مثل الخدمات المالية والعقارات السكنية، معفاة من ضريبة القيمة المضافة.", + }, + }, + }, + }, +} diff --git a/regimes/ae/tax_identity.go b/regimes/ae/tax_identity.go new file mode 100644 index 00000000..52580e58 --- /dev/null +++ b/regimes/ae/tax_identity.go @@ -0,0 +1,39 @@ +// Package ae provides the tax identity validation specific to the United Arab Emirates. +package ae + +import ( + "errors" + "regexp" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/tax" + "github.com/invopop/validation" +) + +var ( + // TRN in UAE is a 15-digit number + trnRegex = regexp.MustCompile(`^\d{15}$`) +) + +// validateTaxIdentity checks to ensure the UAE TRN format is correct. +func validateTaxIdentity(tID *tax.Identity) error { + return validation.ValidateStruct(tID, + validation.Field(&tID.Code, validation.By(validateTRNCode)), + ) +} + +// validateTRNCode checks that the TRN is a valid 15-digit format. +func validateTRNCode(value interface{}) error { + code, ok := value.(cbc.Code) + if !ok || code == "" { + return nil + } + val := code.String() + + // Check if TRN matches the 15-digit pattern + if !trnRegex.MatchString(val) { + return errors.New("must be a 15-digit number") + } + + return nil +} diff --git a/regimes/ae/tax_identity_test.go b/regimes/ae/tax_identity_test.go new file mode 100644 index 00000000..fc305160 --- /dev/null +++ b/regimes/ae/tax_identity_test.go @@ -0,0 +1,43 @@ +// Package ae_test provides tests for the UAE TRN (Tax Registration Number) validation. +package ae_test + +import ( + "testing" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/regimes/ae" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" +) + +func TestValidateTaxIdentity(t *testing.T) { + tests := []struct { + name string + code cbc.Code + err string + }{ + {name: "good 1", code: "123456789012345"}, + {name: "good 2", code: "187654321098765"}, + {name: "good 3", code: "100111222333444"}, + + // Invalid formats + {name: "too short", code: "12345678901234", err: "must be a 15-digit number"}, + {name: "too long", code: "1234567890123456", err: "must be a 15-digit number"}, + {name: "non-numeric", code: "12345678ABCD345", err: "must be a 15-digit number"}, + {name: "not normalized", code: "1234-5678-9012-345", err: "must be a 15-digit number"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tID := &tax.Identity{Country: "AE", Code: tt.code} + err := ae.Validate(tID) + if tt.err == "" { + assert.NoError(t, err) + } else { + if assert.Error(t, err) { + assert.Contains(t, err.Error(), tt.err) + } + } + }) + } +} diff --git a/regimes/regimes.go b/regimes/regimes.go index 8a3b49c4..9f6279ab 100644 --- a/regimes/regimes.go +++ b/regimes/regimes.go @@ -5,6 +5,7 @@ package regimes import ( // Import all the regime definitions which will automatically // add themselves to the tax regime register. + _ "github.com/invopop/gobl/regimes/ae" _ "github.com/invopop/gobl/regimes/at" _ "github.com/invopop/gobl/regimes/be" _ "github.com/invopop/gobl/regimes/br"