diff --git a/data/regimes/pl.json b/data/regimes/pl.json new file mode 100644 index 00000000..9c1473d7 --- /dev/null +++ b/data/regimes/pl.json @@ -0,0 +1,222 @@ +{ + "$schema": "https://gobl.org/draft-0/tax/regime", + "name": { + "en": "Poland", + "pl": "Polska" + }, + "time_zone": "Europe/Warsaw", + "country": "PL", + "currency": "PLN", + "payment_means": [ + { + "key": "cash", + "name": { + "en": "Cash", + "pl": "Gotówka" + }, + "map": { + "favat-forma-platnosci": "1" + } + }, + { + "key": "card", + "name": { + "en": "Card", + "pl": "Karta" + }, + "map": { + "favat-forma-platnosci": "2" + } + }, + { + "key": "other+coupon", + "name": { + "en": "Coupon", + "pl": "Bon" + }, + "map": { + "favat-forma-platnosci": "3" + } + }, + { + "key": "cheque", + "name": { + "en": "Cheque", + "pl": "Czek" + }, + "map": { + "favat-forma-platnosci": "4" + } + }, + { + "key": "online+loan", + "name": { + "en": "Loan", + "pl": "Kredyt" + }, + "map": { + "favat-forma-platnosci": "5" + } + }, + { + "key": "credit-transfer", + "name": { + "en": "Wire Transfer", + "pl": "Przelew" + }, + "map": { + "favat-forma-platnosci": "6" + } + }, + { + "key": "other+mobile", + "name": { + "en": "Mobile", + "pl": "Mobilna" + }, + "map": { + "favat-forma-platnosci": "7" + } + } + ], + "categories": [ + { + "code": "VAT", + "name": { + "en": "VAT", + "pl": "VAT" + }, + "title": { + "en": "Value Added Tax", + "pl": "Podatek od wartości dodanej" + }, + "rates": [ + { + "key": "standard", + "name": { + "en": "Standard Rate", + "pl": "Stawka Podstawowa" + }, + "values": [ + { + "percent": "23.0%" + }, + { + "percent": "22.0%" + } + ] + }, + { + "key": "reduced", + "name": { + "en": "First Reduced Rate", + "pl": "Stawka Obniżona Pierwsza" + }, + "values": [ + { + "percent": "8.0%" + }, + { + "percent": "7.0%" + } + ] + }, + { + "key": "super-reduced", + "name": { + "en": "Second Reduced Rate", + "pl": "Stawka Obniżona Druga" + }, + "values": [ + { + "percent": "5.0%" + } + ] + }, + { + "key": "special", + "name": { + "en": "Lump sum taxi rate", + "pl": "Ryczałt dla taksówek" + }, + "values": [ + { + "percent": "4.0%" + }, + { + "percent": "3.0%" + } + ] + }, + { + "key": "zero-wdt", + "name": { + "en": "Zero Rate - WDT", + "pl": "Stawka Zerowa - WDT" + }, + "values": [ + { + "percent": "0.0%" + } + ] + }, + { + "key": "zero-domestic", + "name": { + "en": "Zero Rate - domestic", + "pl": "Stawka Zerowa - krajowe" + }, + "values": [ + { + "percent": "0.0%" + } + ] + }, + { + "key": "zero-export", + "name": { + "en": "Zero Rate - export", + "pl": "Stawka Zerowa - export" + }, + "values": [ + { + "percent": "0.0%" + } + ] + }, + { + "key": "exempt", + "name": { + "en": "Exempt", + "pl": "Zwolnione z opodatkowania" + }, + "exempt": true + }, + { + "key": "np", + "name": { + "en": "Not pursuant, pursuant to art100 section 1 point4", + "pl": "Niepodlegające opodatkowaniu na postawie wyłączeniem art100 sekcja 1 punkt 4" + }, + "exempt": true + }, + { + "key": "np-art100sec1point4", + "name": { + "en": "Not pursuant excluding art100 section 1 point4", + "pl": "Niepodlegające opodatkowaniu z wyłączeniem art100 sekcja 1 punkt 4" + }, + "exempt": true + }, + { + "key": "reverse-charge", + "name": { + "en": "Reverse Charge", + "pl": "Odwrotne obciążenie" + }, + "exempt": true + } + ] + } + ] +} \ No newline at end of file diff --git a/regimes/pl/README.md b/regimes/pl/README.md new file mode 100644 index 00000000..88e77014 --- /dev/null +++ b/regimes/pl/README.md @@ -0,0 +1,62 @@ +# pl GOBL Polish Tax Regime + +Poland uses the FA_VAT format for their e-invoicing system. + +Example PL GOBL files can be found in the [`examples`](./examples) (YAML uncalculated documents) and [`examples/out`](./examples/out) (JSON calculated envelopes) subdirectories. + +## Table of contents + +* [Public Documentation](#public-documentation) +* [Zones](#zones) +* [Local Codes](#local-codes) +* [Complements](#complements) + +## Public Documentation + +- [Wzór faktury)](http://crd.gov.pl/wzor/2021/11/29/11089/) + +### `TFormaPlatnosci` - Payment Means + +The FA_VAT `TFormaPlatnosci` field specifies an invoice's means of payment. The following table lists all the supported values and how GOBL will map them from the invoice's payment instructions key: + +| Code | Name | GOBL Payment Instructions Key | +| ---- | ----------------------------------- | ----------------------------- | +| 1 | Gotówka | `cash` | +| 2 | Karta | `card` | +| 3 | Bon | `coupon` | +| 4 | Czek | `cheque` | +| 5 | Kredyt | `loan` | +| 6 | Przelew | `credit-transfer` | +| 7 | Mobilna | `mobile` | + +#### Example + +The following GOBL maps to the `1` (gotówka) value of the `TFormaPlatnosci` field: + +```js +{ + "$schema": "https://gobl.org/draft-0/bill/invoice", + + // [...] + + "payment": { + "instructions": { + "key": "cash" + } + } +} +``` + +#### Document Type (TRodzajFaktury) + +All Polish invoices must be identified with a specific type code defined by the FA_VAT format. The following table helps identify how GOBL will map the expected Polish code with a combination of the Invoice Type and tax tags. + +| Code | Type | Tax Tags | Description | +| ------- | ------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| VAT | `standard` | | Regular invoice | +| UPR | `standard` | `simplified` | Simplified (no customer) | +| ZAL | `standard` | `partial` | Advance invioce | +| ROZ | `standard` | `settlement` | Settlement invoice | +| KOR | `corrective` | | Corrective (regular) | +| KOR_ZAL | `corrective` | `partial` | Corrective (advance) | +| KOR_ROZ | `corrective` | `settlement` | Corrective (settlement) | diff --git a/regimes/pl/examples/invoice-pl-pl.yaml b/regimes/pl/examples/invoice-pl-pl.yaml new file mode 100644 index 00000000..1c997ffb --- /dev/null +++ b/regimes/pl/examples/invoice-pl-pl.yaml @@ -0,0 +1,50 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +currency: "PLN" +issue_date: "2023-12-20" +code: "SAMPLE-001" + +supplier: + tax_id: + country: "PL" + code: "9876543210" + name: "Provide One S.L." + emails: + - addr: "billing@example.com" + addresses: + - num: "42" + street: "Calle Pradillo" + locality: "Madrid" + region: "Madrid" + code: "00-015" + country: "PL" + +customer: + tax_id: + country: "PL" + code: "1234567788" + name: "Sample Consumer" + addresses: + - num: "43" + street: "Calle Pradillo" + locality: "Madrid" + region: "Madrid" + code: "00-015" + country: "PL" + +lines: + - quantity: 20 + item: + name: "Development services" + price: "90.00" + unit: "h" + taxes: + - cat: VAT + rate: standard + - quantity: 1 + item: + name: "Financial service" + price: "10.00" + unit: "service" + taxes: + - cat: VAT + rate: reduced diff --git a/regimes/pl/examples/out/invoice-pl-pl.json b/regimes/pl/examples/out/invoice-pl-pl.json new file mode 100644 index 00000000..f937ff98 --- /dev/null +++ b/regimes/pl/examples/out/invoice-pl-pl.json @@ -0,0 +1,107 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "d9fbc8bc-89fe-11ee-80f4-92dde50d70fc", + "dig": { + "alg": "sha256", + "val": "a88d6b5523cbad826bf6aaacc31cc2a37ec02c403215c3c378d8e727d9abd463" + }, + "draft": true + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "type": "standard", + "code": "SAMPLE-001", + "issue_date": "2023-12-20", + "currency": "PLN", + "supplier": { + "name": "Provide One S.L.", + "tax_id": { "country": "PL", "code": "9876543210" }, + "addresses": [ + { + "num": "42", + "street": "Calle Pradillo", + "locality": "Madrid", + "region": "Madrid", + "code": "00-015", + "country": "PL" + } + ], + "emails": [{ "addr": "billing@example.com" }] + }, + "customer": { + "name": "Sample Consumer", + "tax_id": { "country": "PL", "code": "1234567788" }, + "addresses": [ + { + "num": "43", + "street": "Calle Pradillo", + "locality": "Madrid", + "region": "Madrid", + "code": "00-015", + "country": "PL" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "20", + "item": { + "name": "Development services", + "price": "90.00", + "unit": "h" + }, + "sum": "1800.00", + "taxes": [ + { "cat": "VAT", "rate": "standard", "percent": "23.0%" } + ], + "total": "1800.00" + }, + { + "i": 2, + "quantity": "1", + "item": { + "name": "Financial service", + "price": "10.00", + "unit": "service" + }, + "sum": "10.00", + "taxes": [ + { "cat": "VAT", "rate": "reduced", "percent": "8.0%" } + ], + "total": "10.00" + } + ], + "totals": { + "sum": "1810.00", + "total": "1810.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "1800.00", + "percent": "23.0%", + "amount": "414.00" + }, + { + "key": "reduced", + "base": "10.00", + "percent": "8.0%", + "amount": "0.80" + } + ], + "amount": "414.80" + } + ], + "sum": "414.80" + }, + "tax": "414.80", + "total_with_tax": "2224.80", + "payable": "2224.80" + } + } +} diff --git a/regimes/pl/invoices.go b/regimes/pl/invoices.go new file mode 100644 index 00000000..c875510f --- /dev/null +++ b/regimes/pl/invoices.go @@ -0,0 +1,124 @@ +package pl + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/currency" + "github.com/invopop/gobl/l10n" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/regimes/common" + "github.com/invopop/gobl/tax" + "github.com/invopop/validation" +) + +// invoiceValidator adds validation checks to invoices which are relevant +// for the region. +type invoiceValidator struct { + inv *bill.Invoice +} + +func validateInvoice(inv *bill.Invoice) error { + v := &invoiceValidator{inv: inv} + return v.validate() +} + +func (v *invoiceValidator) validate() error { + inv := v.inv + return validation.ValidateStruct(inv, + validation.Field(&inv.Currency, validation.In(currency.PLN, currency.EUR)), + // Only commercial and simplified supported at this time for Poland. + // Rectification state determined by Preceding value. + validation.Field(&inv.Type, validation.In( + bill.InvoiceTypeStandard, + // bill.InvoiceTypeCorrective, + )), + // validation.Field(&inv.Preceding, + // validation.Each(validation.By(v.preceding)), + // ), + validation.Field(&inv.Supplier, + validation.Required, + validation.By(v.supplier), + ), + validation.Field(&inv.Customer, + validation.When( + !inv.Tax.ContainsTag(common.TagSimplified), + validation.Required, + validation.By(v.commercialCustomer), + ), + ), + ) +} + +func (v *invoiceValidator) supplier(value interface{}) error { + obj, _ := value.(*org.Party) + if obj == nil { + return nil + } + return validation.ValidateStruct(obj, + validation.Field(&obj.TaxID, + validation.Required, + tax.RequireIdentityCode, + validation.By(validatePolishTaxIdentity), + ), + validation.Field(&obj.Name, + validation.When( + len(obj.People) == 0, + validation.Required, + ), + ), + validation.Field(&obj.People[0].Name, + validation.When( + obj.Name == "", + validation.Required, + ), + ), + ) +} + +func (v *invoiceValidator) commercialCustomer(value interface{}) error { + obj, _ := value.(*org.Party) + if obj == nil { + return nil + } + if obj.TaxID == nil { + return nil // validation already handled, this prevents panics + } + // Customers must have a tax ID if a Polish entity + return validation.ValidateStruct(obj, + validation.Field(&obj.TaxID, + validation.Required, + validation.When( + obj.TaxID.Country.In(l10n.PL), + validation.By(validatePolishTaxIdentity), + ), // TODO check if id is valid when other entity + ), + validation.Field(&obj.Name, + validation.When( + len(obj.People) == 0, + validation.Required, + ), + ), + validation.Field(&obj.People[0].Name, + validation.When( + obj.Name == "", + validation.Required, + ), + ), + ) +} + +// func (v *invoiceValidator) preceding(value interface{}) error { +// obj, _ := value.(*bill.Preceding) +// if obj == nil { +// return nil +// } +// return validation.ValidateStruct(obj, +// validation.Field(&obj.Changes, +// validation.Required, +// validation.Each(isValidCorrectionChangeKey), +// ), +// validation.Field(&obj.CorrectionMethod, +// validation.Required, +// isValidCorrectionMethodKey, +// ), +// ) +// } diff --git a/regimes/pl/pay.go b/regimes/pl/pay.go new file mode 100644 index 00000000..71fb0039 --- /dev/null +++ b/regimes/pl/pay.go @@ -0,0 +1,90 @@ +package pl + +import ( + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/pay" + "github.com/invopop/gobl/tax" +) + +// Regime Specific Payment Means Extension Keys +const ( + MeansKeyCoupon cbc.Key = "coupon" + MeansKeyCheque cbc.Key = "cheque" + MeansKeyLoan cbc.Key = "loan" + MeansKeyDebtRelief cbc.Key = "credit-transfer" + MeansKeyMobile cbc.Key = "mobile" +) + +var paymentMeansKeyDefinitions = []*tax.KeyDefinition{ + { + Key: pay.MeansKeyCash, + Name: i18n.String{ + i18n.EN: "Cash", + i18n.PL: "Gotówka", + }, + Map: cbc.CodeMap{ + KeyFA_VATPaymentType: "1", + }, + }, + { + Key: pay.MeansKeyCard, + Name: i18n.String{ + i18n.EN: "Card", + i18n.PL: "Karta", + }, + Map: cbc.CodeMap{ + KeyFA_VATPaymentType: "2", + }, + }, + { + Key: pay.MeansKeyOther.With(MeansKeyCoupon), + Name: i18n.String{ + i18n.EN: "Coupon", + i18n.PL: "Bon", + }, + Map: cbc.CodeMap{ + KeyFA_VATPaymentType: "3", + }, + }, + { + Key: pay.MeansKeyCheque, + Name: i18n.String{ + i18n.EN: "Cheque", + i18n.PL: "Czek", + }, + Map: cbc.CodeMap{ + KeyFA_VATPaymentType: "4", + }, + }, + { + Key: pay.MeansKeyOnline.With(MeansKeyLoan), + Name: i18n.String{ + i18n.EN: "Loan", + i18n.PL: "Kredyt", + }, + Map: cbc.CodeMap{ + KeyFA_VATPaymentType: "5", + }, + }, + { + Key: pay.MeansKeyCreditTransfer, + Name: i18n.String{ + i18n.EN: "Wire Transfer", + i18n.PL: "Przelew", + }, + Map: cbc.CodeMap{ + KeyFA_VATPaymentType: "6", + }, + }, + { + Key: pay.MeansKeyOther.With(MeansKeyMobile), + Name: i18n.String{ + i18n.EN: "Mobile", + i18n.PL: "Mobilna", + }, + Map: cbc.CodeMap{ + KeyFA_VATPaymentType: "7", + }, + }, +} diff --git a/regimes/pl/pl.go b/regimes/pl/pl.go new file mode 100644 index 00000000..995ec857 --- /dev/null +++ b/regimes/pl/pl.go @@ -0,0 +1,66 @@ +// Package it provides the Polish tax regime. +package pl + +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/l10n" + "github.com/invopop/gobl/tax" +) + +func init() { + tax.RegisterRegime(New()) +} + +// Custom keys used typically in meta or codes information. +const ( + KeyFA_VATPaymentType cbc.Key = "favat-forma-platnosci" // for mapping to TFormaPlatnosci's codes + KeyFA_VATInvoiceType cbc.Key = "favat-rodzaj-faktury" // for mapping to TRodzajFaktury's codes +) + +// New instantiates a new Polish regime. +func New() *tax.Regime { + return &tax.Regime{ + Country: l10n.PL, + Currency: currency.PLN, + Name: i18n.String{ + i18n.EN: "Poland", + i18n.PL: "Polska", + }, + TimeZone: "Europe/Warsaw", + // ChargeKeys: chargeKeyDefinitions, // charges.go + PaymentMeansKeys: paymentMeansKeyDefinitions, // pay.go + // Extensions: extensionKeys, // extensions.go + Tags: invoiceTags, + Scenarios: scenarios, // scenarios.go + Validator: Validate, + // Calculator: Calculate, + Categories: taxCategories, // tax_categories.go + } +} + +// Validate checks the document type and determines if it can be validated. +func Validate(doc interface{}) error { + switch obj := doc.(type) { + case *tax.Identity: + return validateTaxIdentity(obj) + case *bill.Invoice: + return validateInvoice(obj) + // case *pay.Instructions: + // return validatePayInstructions(obj) + // case *pay.Advance: + // return validatePayAdvance(obj) + } + return nil +} + +// Calculate will perform any regime specific calculations. +// func Calculate(doc interface{}) error { +// switch obj := doc.(type) { +// case *tax.Identity: +// return normalizeTaxIdentity(obj) +// } +// return nil +// } diff --git a/regimes/pl/scenarios.go b/regimes/pl/scenarios.go new file mode 100644 index 00000000..c914d7be --- /dev/null +++ b/regimes/pl/scenarios.go @@ -0,0 +1,110 @@ +package pl + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/regimes/common" + "github.com/invopop/gobl/tax" +) + +// Document tag keys +const ( + TagSettlement cbc.Key = "partial" +) + +var invoiceTags = []*tax.KeyDefinition{ + { + Key: TagSettlement, + Name: i18n.String{ + i18n.EN: "Settlement Invoice", + i18n.PL: "Faktura Rozliczeniowa", + }, + }, +} + +var scenarios = []*tax.ScenarioSet{ + invoiceScenarios, +} + +var invoiceScenarios = &tax.ScenarioSet{ + Schema: bill.ShortSchemaInvoice, + List: []*tax.Scenario{ + // **** Invoice Type **** + { + Types: []cbc.Key{bill.InvoiceTypeStandard}, + Name: i18n.String{ + i18n.EN: "Regular Invoice", + i18n.PL: "Faktura Podstawowa", + }, + Codes: cbc.CodeMap{ + KeyFA_VATInvoiceType: "VAT", + }, + }, + { + Types: []cbc.Key{bill.InvoiceTypeStandard}, + Tags: []cbc.Key{common.TagPartial}, + Name: i18n.String{ + i18n.EN: "Prepayment Invoice", + i18n.PL: `Faktura Zaliczkowa`, + }, + Codes: cbc.CodeMap{ + KeyFA_VATInvoiceType: "ZAL", + }, + }, + { + Types: []cbc.Key{bill.InvoiceTypeStandard}, + Tags: []cbc.Key{TagSettlement}, + Name: i18n.String{ + i18n.EN: "Settlement Invoice", + i18n.PL: "Faktura Rozliczeniowa", + }, + Codes: cbc.CodeMap{ + KeyFA_VATInvoiceType: "ROZ", + }, + }, + { + Types: []cbc.Key{bill.InvoiceTypeStandard}, + Tags: []cbc.Key{common.TagSimplified}, + Name: i18n.String{ + i18n.EN: "Simplified Invoice", + i18n.PL: "Faktura Uproszczona", + }, + Codes: cbc.CodeMap{ + KeyFA_VATInvoiceType: "UPR", + }, + }, + { + Types: []cbc.Key{bill.InvoiceTypeCorrective}, + Name: i18n.String{ + i18n.EN: "Corrective Invoice", + i18n.PL: "Faktura Korygująca", + }, + Codes: cbc.CodeMap{ + KeyFA_VATInvoiceType: "KOR", + }, + }, + { + Types: []cbc.Key{bill.InvoiceTypeCorrective}, + Tags: []cbc.Key{common.TagPartial}, + Name: i18n.String{ + i18n.EN: "Corrective Prepayment Invoice", + i18n.PL: `Faktura korygująca fakturę zaliczkową`, + }, + Codes: cbc.CodeMap{ + KeyFA_VATInvoiceType: "KOR_ZAL", + }, + }, + { + Types: []cbc.Key{bill.InvoiceTypeCorrective}, + Tags: []cbc.Key{TagSettlement}, + Name: i18n.String{ + i18n.EN: "Corrective Settlement Invoice", + i18n.PL: "Faktura korygująca fakturę rozliczeniową", + }, + Codes: cbc.CodeMap{ + KeyFA_VATInvoiceType: "KOR_ROZ", + }, + }, + }, +} diff --git a/regimes/pl/tax_categories.go b/regimes/pl/tax_categories.go new file mode 100644 index 00000000..d7426fa4 --- /dev/null +++ b/regimes/pl/tax_categories.go @@ -0,0 +1,162 @@ +package pl + +import ( + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/num" + "github.com/invopop/gobl/regimes/common" + "github.com/invopop/gobl/tax" +) + +// Tax rates specific to Poland. +const ( + TaxRateExempt cbc.Key = "exempt" + TaxRateNotPursuant cbc.Key = "np" + TaxRateNotPursuantArt100 cbc.Key = "np-art100sec1point4" + TaxRateReverseCharge cbc.Key = "reverse-charge" + TaxRateZeroWDT cbc.Key = "zero-wdt" + TaxRateZeroDomestic cbc.Key = "zero-domestic" + TaxRateZeroExport cbc.Key = "zero-export" +) + +var taxCategories = []*tax.Category{ + { + Code: common.TaxCategoryVAT, + Name: i18n.String{ + i18n.EN: "VAT", + i18n.PL: "VAT", + }, + Title: i18n.String{ + i18n.EN: "Value Added Tax", + i18n.PL: "Podatek od wartości dodanej", + }, + Retained: false, + Rates: []*tax.Rate{ + { + Key: common.TaxRateStandard, + Name: i18n.String{ + i18n.EN: "Standard Rate", + i18n.PL: "Stawka Podstawowa", + }, + Values: []*tax.RateValue{ + { + Percent: num.MakePercentage(230, 3), + }, + { + Percent: num.MakePercentage(220, 3), + }, + }, + }, + { + Key: common.TaxRateReduced, + Name: i18n.String{ + i18n.EN: "First Reduced Rate", + i18n.PL: "Stawka Obniżona Pierwsza", + }, + Values: []*tax.RateValue{ + { + Percent: num.MakePercentage(80, 3), + }, + { + Percent: num.MakePercentage(70, 3), + }, + }, + }, + { + Key: common.TaxRateSuperReduced, + Name: i18n.String{ + i18n.EN: "Second Reduced Rate", + i18n.PL: "Stawka Obniżona Druga", + }, + Values: []*tax.RateValue{ + { + Percent: num.MakePercentage(50, 3), + }, + }, + }, + { + Key: common.TaxRateSpecial, + Name: i18n.String{ + i18n.EN: "Lump sum taxi rate", + i18n.PL: "Ryczałt dla taksówek", + }, + Values: []*tax.RateValue{ + { + Percent: num.MakePercentage(40, 3), + }, + { + Percent: num.MakePercentage(30, 3), + }, + }, + }, + { + Key: TaxRateZeroWDT, + Name: i18n.String{ + i18n.EN: "Zero Rate - WDT", + i18n.PL: "Stawka Zerowa - WDT", + }, + Values: []*tax.RateValue{ + { + Percent: num.MakePercentage(0, 3), + }, + }, + }, + { + Key: TaxRateZeroDomestic, + Name: i18n.String{ + i18n.EN: "Zero Rate - domestic", + i18n.PL: "Stawka Zerowa - krajowe", + }, + Values: []*tax.RateValue{ + { + Percent: num.MakePercentage(0, 3), + }, + }, + }, + { + Key: TaxRateZeroExport, + Name: i18n.String{ + i18n.EN: "Zero Rate - export", + i18n.PL: "Stawka Zerowa - export", + }, + Values: []*tax.RateValue{ + { + Percent: num.MakePercentage(0, 3), + }, + }, + }, + { + Key: TaxRateExempt, + Name: i18n.String{ + i18n.EN: "Exempt", + i18n.PL: "Zwolnione z opodatkowania", + }, + Exempt: true, + }, + { + Key: TaxRateNotPursuant, + Name: i18n.String{ + i18n.EN: "Not pursuant, pursuant to art100 section 1 point4", + i18n.PL: "Niepodlegające opodatkowaniu na postawie wyłączeniem art100 sekcja 1 punkt 4", + }, + Exempt: true, + }, + { + Key: TaxRateNotPursuantArt100, + Name: i18n.String{ + i18n.EN: "Not pursuant excluding art100 section 1 point4", + i18n.PL: "Niepodlegające opodatkowaniu z wyłączeniem art100 sekcja 1 punkt 4", + }, + Exempt: true, + }, + { + Key: TaxRateReverseCharge, + Name: i18n.String{ + i18n.EN: "Reverse Charge", + i18n.PL: "Odwrotne obciążenie", + }, + Exempt: true, + }, + }, + }, +} diff --git a/regimes/pl/tax_identity.go b/regimes/pl/tax_identity.go new file mode 100644 index 00000000..dd8fcf8b --- /dev/null +++ b/regimes/pl/tax_identity.go @@ -0,0 +1,124 @@ +package pl + +import ( + "regexp" + "strconv" + "unicode" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/tax" + "github.com/invopop/validation" +) + +/* + * Sources of data: + * + * - https://tramites.aguascalientes.gob.mx/download/documentos/D20230407194800_Estructura%20RFC.pdf + * - https://pl.wikipedia.org/wiki/Numer_identyfikacji_podatkowej + * + */ + +// Tax Identity Type +const ( + TaxIdentityTypePolish cbc.Key = "polish" + TaxIdentityTypeOther cbc.Key = "other" +) + +// Tax Identity Patterns +const ( + TaxIdentityPatternPolish = `^[1-9]((\d[1-9])|([1-9]\d))\d{7}$` + TaxIdentityPatternOther = `^.{1,50}$` +) + +// Tax Identity Regexp +var ( + TaxIdentityRegexpPolish = regexp.MustCompile(TaxIdentityPatternPolish) + TaxIdentityRegexpOther = regexp.MustCompile(TaxIdentityPatternOther) +) + +func validateTaxIdentity(tID *tax.Identity) error { + return validation.ValidateStruct(tID, + validation.Field(&tID.Code, + validation.Required, + validation.By(validateTaxCode), + ), + ) +} + +func validatePolishTaxIdentity(value interface{}) error { + code, ok := value.(cbc.Code) + str := code.String() + if !ok { + return nil + } + if TaxIdentityRegexpPolish.MatchString(str) && validateNIPChecksum(code) { + return nil + } + return tax.ErrIdentityCodeInvalid +} + +func validateTaxCode(value interface{}) error { + code, ok := value.(cbc.Code) + if !ok { + return nil + } + if code == "" { + return nil + } + typ := DetermineTaxCodeType(code) + if typ.IsEmpty() { + return tax.ErrIdentityCodeInvalid + } + if typ == TaxIdentityTypePolish { + if validateNIPChecksum(code) { + return nil + } + return tax.ErrIdentityCodeInvalid + } + return nil +} + +// DetermineTaxCodeType determines the type of tax code or provides +// an empty key if it looks invalid. +func DetermineTaxCodeType(code cbc.Code) cbc.Key { + str := code.String() + switch { + case TaxIdentityRegexpPolish.MatchString(str): + return TaxIdentityTypePolish + case TaxIdentityRegexpOther.MatchString(str): + return TaxIdentityTypeOther + default: + return cbc.KeyEmpty + } +} + +func validateNIPChecksum(code cbc.Code) bool { + nipStr := code.String() + if len(nipStr) != 10 { + return false + } + + for _, char := range nipStr { + if !unicode.IsDigit(char) { + return false + } + } + + digits := make([]int, 10) + for i, char := range nipStr { + digit, err := strconv.Atoi(string(char)) + if err != nil { + return false + } + digits[i] = digit + } + + weights := [9]int{6, 5, 7, 2, 3, 4, 5, 6, 7} + checkSum := 0 + for i, digit := range digits[:9] { + checkSum += digit * weights[i] + } + checkSum %= 11 + + return checkSum == digits[9] +} diff --git a/regimes/regimes.go b/regimes/regimes.go index b4ccad33..3dbaabcb 100644 --- a/regimes/regimes.go +++ b/regimes/regimes.go @@ -12,6 +12,7 @@ import ( _ "github.com/invopop/gobl/regimes/it" _ "github.com/invopop/gobl/regimes/mx" _ "github.com/invopop/gobl/regimes/nl" + _ "github.com/invopop/gobl/regimes/pl" _ "github.com/invopop/gobl/regimes/pt" _ "github.com/invopop/gobl/regimes/us" )