From f14fe6973e482128d1acc813c7fe8603966243aa Mon Sep 17 00:00:00 2001 From: Rafal Lyzwa Date: Thu, 9 Nov 2023 07:36:54 +0100 Subject: [PATCH 1/6] PL regime initial commit --- regimes/pl/README.md | 48 ++++++++++++++ regimes/pl/invoices.go | 104 ++++++++++++++++++++++++++++++ regimes/pl/pl.go | 60 ++++++++++++++++++ regimes/pl/tax_identity.go | 125 +++++++++++++++++++++++++++++++++++++ regimes/regimes.go | 1 + 5 files changed, 338 insertions(+) create mode 100644 regimes/pl/README.md create mode 100644 regimes/pl/invoices.go create mode 100644 regimes/pl/pl.go create mode 100644 regimes/pl/tax_identity.go diff --git a/regimes/pl/README.md b/regimes/pl/README.md new file mode 100644 index 00000000..990b9ffa --- /dev/null +++ b/regimes/pl/README.md @@ -0,0 +1,48 @@ +# 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 | `credit-transfer` | +| 4 | czek | `cheque` | +| 5 | kredyt | `loan` | +| 6 | przelew | `credit-transfer` | +| 7 | mobilna | `online` | + +#### 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" + } + } +} +``` diff --git a/regimes/pl/invoices.go b/regimes/pl/invoices.go new file mode 100644 index 00000000..11fd3960 --- /dev/null +++ b/regimes/pl/invoices.go @@ -0,0 +1,104 @@ +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, + bill.InvoiceTypeProforma, + bill.InvoiceTypeDebitNote, + )), + 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), + ), + // TODO check if name exists + ) +} + +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 + // TODO check if name exists + ), + ) +} + +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/pl.go b/regimes/pl/pl.go new file mode 100644 index 00000000..841694ec --- /dev/null +++ b/regimes/pl/pl.go @@ -0,0 +1,60 @@ +// Package it provides the Polish tax regime. +package pl + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/currency" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/l10n" + "github.com/invopop/gobl/pay" + "github.com/invopop/gobl/tax" +) + +func init() { + tax.RegisterRegime(New()) +} + +// 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: categories, // 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/tax_identity.go b/regimes/pl/tax_identity.go new file mode 100644 index 00000000..d46dc4c1 --- /dev/null +++ b/regimes/pl/tax_identity.go @@ -0,0 +1,125 @@ +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), + ), + validation.Field(&tID.Zone, validation.Required), + ) +} + +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" ) From 1a776277eae1ea4242a26e521464a29cd08a484b Mon Sep 17 00:00:00 2001 From: Rafal Lyzwa Date: Tue, 14 Nov 2023 09:40:53 +0100 Subject: [PATCH 2/6] Polish tax rates --- data/regimes/pl.json | 228 +++++++++++++++++++++++++++++++++++++++++++ regimes/pl/README.md | 28 ++++-- 2 files changed, 249 insertions(+), 7 deletions(-) create mode 100644 data/regimes/pl.json diff --git a/data/regimes/pl.json b/data/regimes/pl.json new file mode 100644 index 00000000..b5f3c126 --- /dev/null +++ b/data/regimes/pl.json @@ -0,0 +1,228 @@ +{ + "$schema": "https://gobl.org/draft-0/tax/regime", + "name": { + "en": "Poland", + "pl": "Polska" + }, + "time_zone": "Europe/Warsaw", + "country": "PL", + "currency": "PLN", + "extensions": [ + ], + "payment_means": [ + { + "key": "cash", + "name": { + "en": "Cash", + "pl": "Gotówka" + }, + "map": { + "sat-forma-pago": "1" + } + }, + { + "key": "card", + "name": { + "en": "Card", + "pl": "Karta" + }, + "map": { + "sat-forma-pago": "2" + } + }, + { + "key": "coupon", + "name": { + "en": "Coupon", + "pl": "Bon" + }, + "map": { + "sat-forma-pago": "3" + } + }, + { + "key": "cheque", + "name": { + "en": "Cheque", + "pl": "Czek" + }, + "map": { + "sat-forma-pago": "4" + } + }, + { + "key": "loan", + "name": { + "en": "Loan", + "pl": "Kredyt" + }, + "map": { + "sat-forma-pago": "5" + } + }, + { + "key": "credit-transfer", + "name": { + "en": "Wire transfer", + "pl": "Przelew" + }, + "map": { + "sat-forma-pago": "6" + } + }, + { + "key": "mobile", + "name": { + "en": "Mobile", + "pl": "Mobilna" + }, + "map": { + "sat-forma-pago": "7" + } + } + ], + "scenarios": [ + ], + "corrections": [ + ], + "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-first", + "name": { + "en": "First Reduced Rate", + "pl": "Stawka Obniżona Pierwsza" + }, + "values": [ + { + "percent": "8.0%" + }, + { + "percent": "7.0%" + } + ] + }, + { + "key": "reduced-second", + "name": { + "en": "Second Reduced Rate", + "pl": "Stawka Obniżona Druga" + }, + "values": [ + { + "percent": "5.0%" + } + ] + }, + { + "key": "taxi-lump-sum", + "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-art100sec1point4", + "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", + "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 index 990b9ffa..88e77014 100644 --- a/regimes/pl/README.md +++ b/regimes/pl/README.md @@ -21,13 +21,13 @@ The FA_VAT `TFormaPlatnosci` field specifies an invoice's means of payment. The | Code | Name | GOBL Payment Instructions Key | | ---- | ----------------------------------- | ----------------------------- | -| 1 | gotówka | `cash` | -| 2 | karta | `card` | -| 3 | bon | `credit-transfer` | -| 4 | czek | `cheque` | -| 5 | kredyt | `loan` | -| 6 | przelew | `credit-transfer` | -| 7 | mobilna | `online` | +| 1 | Gotówka | `cash` | +| 2 | Karta | `card` | +| 3 | Bon | `coupon` | +| 4 | Czek | `cheque` | +| 5 | Kredyt | `loan` | +| 6 | Przelew | `credit-transfer` | +| 7 | Mobilna | `mobile` | #### Example @@ -46,3 +46,17 @@ The following GOBL maps to the `1` (gotówka) value of the `TFormaPlatnosci` fie } } ``` + +#### 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) | From c170a534b3240ea07492fefea6549650a0e88fd0 Mon Sep 17 00:00:00 2001 From: Rafal Lyzwa Date: Thu, 16 Nov 2023 14:55:44 +0100 Subject: [PATCH 3/6] add missing files --- data/regimes/pl.json | 44 ++++------ regimes/pl/invoices.go | 39 ++++----- regimes/pl/pay.go | 90 +++++++++++++++++++ regimes/pl/pl.go | 45 +++++----- regimes/pl/tax_categories.go | 162 +++++++++++++++++++++++++++++++++++ 5 files changed, 315 insertions(+), 65 deletions(-) create mode 100644 regimes/pl/pay.go create mode 100644 regimes/pl/tax_categories.go diff --git a/data/regimes/pl.json b/data/regimes/pl.json index b5f3c126..9c1473d7 100644 --- a/data/regimes/pl.json +++ b/data/regimes/pl.json @@ -7,8 +7,6 @@ "time_zone": "Europe/Warsaw", "country": "PL", "currency": "PLN", - "extensions": [ - ], "payment_means": [ { "key": "cash", @@ -17,7 +15,7 @@ "pl": "Gotówka" }, "map": { - "sat-forma-pago": "1" + "favat-forma-platnosci": "1" } }, { @@ -27,17 +25,17 @@ "pl": "Karta" }, "map": { - "sat-forma-pago": "2" + "favat-forma-platnosci": "2" } }, { - "key": "coupon", + "key": "other+coupon", "name": { "en": "Coupon", "pl": "Bon" }, "map": { - "sat-forma-pago": "3" + "favat-forma-platnosci": "3" } }, { @@ -47,44 +45,40 @@ "pl": "Czek" }, "map": { - "sat-forma-pago": "4" + "favat-forma-platnosci": "4" } }, { - "key": "loan", + "key": "online+loan", "name": { "en": "Loan", "pl": "Kredyt" }, "map": { - "sat-forma-pago": "5" + "favat-forma-platnosci": "5" } }, { "key": "credit-transfer", "name": { - "en": "Wire transfer", + "en": "Wire Transfer", "pl": "Przelew" }, "map": { - "sat-forma-pago": "6" + "favat-forma-platnosci": "6" } }, { - "key": "mobile", + "key": "other+mobile", "name": { "en": "Mobile", "pl": "Mobilna" }, "map": { - "sat-forma-pago": "7" + "favat-forma-platnosci": "7" } } ], - "scenarios": [ - ], - "corrections": [ - ], "categories": [ { "code": "VAT", @@ -113,7 +107,7 @@ ] }, { - "key": "reduced-first", + "key": "reduced", "name": { "en": "First Reduced Rate", "pl": "Stawka Obniżona Pierwsza" @@ -128,7 +122,7 @@ ] }, { - "key": "reduced-second", + "key": "super-reduced", "name": { "en": "Second Reduced Rate", "pl": "Stawka Obniżona Druga" @@ -140,7 +134,7 @@ ] }, { - "key": "taxi-lump-sum", + "key": "special", "name": { "en": "Lump sum taxi rate", "pl": "Ryczałt dla taksówek" @@ -158,7 +152,7 @@ "key": "zero-wdt", "name": { "en": "Zero Rate - WDT", - "pl": "Stawka zerowa - WDT" + "pl": "Stawka Zerowa - WDT" }, "values": [ { @@ -170,7 +164,7 @@ "key": "zero-domestic", "name": { "en": "Zero Rate - domestic", - "pl": "Stawka zerowa - krajowe" + "pl": "Stawka Zerowa - krajowe" }, "values": [ { @@ -182,7 +176,7 @@ "key": "zero-export", "name": { "en": "Zero Rate - export", - "pl": "Stawka zerowa - export" + "pl": "Stawka Zerowa - export" }, "values": [ { @@ -199,7 +193,7 @@ "exempt": true }, { - "key": "np-art100sec1point4", + "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" @@ -207,7 +201,7 @@ "exempt": true }, { - "key": "np", + "key": "np-art100sec1point4", "name": { "en": "Not pursuant excluding art100 section 1 point4", "pl": "Niepodlegające opodatkowaniu z wyłączeniem art100 sekcja 1 punkt 4" diff --git a/regimes/pl/invoices.go b/regimes/pl/invoices.go index 11fd3960..e123d027 100644 --- a/regimes/pl/invoices.go +++ b/regimes/pl/invoices.go @@ -31,11 +31,10 @@ func (v *invoiceValidator) validate() error { bill.InvoiceTypeStandard, bill.InvoiceTypeCorrective, bill.InvoiceTypeProforma, - bill.InvoiceTypeDebitNote, )), - validation.Field(&inv.Preceding, - validation.Each(validation.By(v.preceding)), - ), + // validation.Field(&inv.Preceding, + // validation.Each(validation.By(v.preceding)), + // ), validation.Field(&inv.Supplier, validation.Required, validation.By(v.supplier), @@ -86,19 +85,19 @@ func (v *invoiceValidator) commercialCustomer(value interface{}) error { ) } -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, - ), - ) -} +// 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..f91efcc6 --- /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_VATFormaPlatnosci: "1", + }, + }, + { + Key: pay.MeansKeyCard, + Name: i18n.String{ + i18n.EN: "Card", + i18n.PL: "Karta", + }, + Map: cbc.CodeMap{ + KeyFA_VATFormaPlatnosci: "2", + }, + }, + { + Key: pay.MeansKeyOther.With(MeansKeyCoupon), + Name: i18n.String{ + i18n.EN: "Coupon", + i18n.PL: "Bon", + }, + Map: cbc.CodeMap{ + KeyFA_VATFormaPlatnosci: "3", + }, + }, + { + Key: pay.MeansKeyCheque, + Name: i18n.String{ + i18n.EN: "Cheque", + i18n.PL: "Czek", + }, + Map: cbc.CodeMap{ + KeyFA_VATFormaPlatnosci: "4", + }, + }, + { + Key: pay.MeansKeyOnline.With(MeansKeyLoan), + Name: i18n.String{ + i18n.EN: "Loan", + i18n.PL: "Kredyt", + }, + Map: cbc.CodeMap{ + KeyFA_VATFormaPlatnosci: "5", + }, + }, + { + Key: pay.MeansKeyCreditTransfer, + Name: i18n.String{ + i18n.EN: "Wire Transfer", + i18n.PL: "Przelew", + }, + Map: cbc.CodeMap{ + KeyFA_VATFormaPlatnosci: "6", + }, + }, + { + Key: pay.MeansKeyOther.With(MeansKeyMobile), + Name: i18n.String{ + i18n.EN: "Mobile", + i18n.PL: "Mobilna", + }, + Map: cbc.CodeMap{ + KeyFA_VATFormaPlatnosci: "7", + }, + }, +} diff --git a/regimes/pl/pl.go b/regimes/pl/pl.go index 841694ec..5d9bc13d 100644 --- a/regimes/pl/pl.go +++ b/regimes/pl/pl.go @@ -3,10 +3,10 @@ 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/pay" "github.com/invopop/gobl/tax" ) @@ -14,6 +14,11 @@ func init() { tax.RegisterRegime(New()) } +// Custom keys used typically in meta or codes information. +const ( + KeyFA_VATFormaPlatnosci cbc.Key = "favat-forma-platnosci" // for mapping to TFormaPlatnosci's codes +) + // New instantiates a new Polish regime. func New() *tax.Regime { return &tax.Regime{ @@ -23,15 +28,15 @@ func New() *tax.Regime { i18n.EN: "Poland", i18n.PL: "Polska", }, - TimeZone: "Europe/Warsaw", - ChargeKeys: chargeKeyDefinitions, // charges.go + 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: categories, // categories.go + // Extensions: extensionKeys, // extensions.go + // Tags: invoiceTags, + // Scenarios: scenarios, // scenarios.go + Validator: Validate, + // Calculator: Calculate, + Categories: taxCategories, // tax_categories.go } } @@ -42,19 +47,19 @@ func Validate(doc interface{}) error { return validateTaxIdentity(obj) case *bill.Invoice: return validateInvoice(obj) - case *pay.Instructions: - return validatePayInstructions(obj) - case *pay.Advance: - return validatePayAdvance(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 -} +// func Calculate(doc interface{}) error { +// switch obj := doc.(type) { +// case *tax.Identity: +// return normalizeTaxIdentity(obj) +// } +// return nil +// } 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, + }, + }, + }, +} From db335cc7c58b8fcec0d9ae2e5bdb704fe3b26cf9 Mon Sep 17 00:00:00 2001 From: Rafal Lyzwa Date: Wed, 29 Nov 2023 13:10:37 +0100 Subject: [PATCH 4/6] add test --- regimes/pl/examples/invoice-pl-pl.yaml | 50 ++++++++++ regimes/pl/examples/out/invoice-pl-pl.json | 107 +++++++++++++++++++++ regimes/pl/tax_identity.go | 1 - 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 regimes/pl/examples/invoice-pl-pl.yaml create mode 100644 regimes/pl/examples/out/invoice-pl-pl.json 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/tax_identity.go b/regimes/pl/tax_identity.go index d46dc4c1..dd8fcf8b 100644 --- a/regimes/pl/tax_identity.go +++ b/regimes/pl/tax_identity.go @@ -42,7 +42,6 @@ func validateTaxIdentity(tID *tax.Identity) error { validation.Required, validation.By(validateTaxCode), ), - validation.Field(&tID.Zone, validation.Required), ) } From 96a7ab6d1eeeeb6e8175793ee71e9b7f69b41e33 Mon Sep 17 00:00:00 2001 From: Rafal Lyzwa Date: Wed, 29 Nov 2023 15:02:42 +0100 Subject: [PATCH 5/6] add invoice types --- regimes/pl/pay.go | 14 ++--- regimes/pl/pl.go | 7 +-- regimes/pl/scenarios.go | 110 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 regimes/pl/scenarios.go diff --git a/regimes/pl/pay.go b/regimes/pl/pay.go index f91efcc6..71fb0039 100644 --- a/regimes/pl/pay.go +++ b/regimes/pl/pay.go @@ -24,7 +24,7 @@ var paymentMeansKeyDefinitions = []*tax.KeyDefinition{ i18n.PL: "Gotówka", }, Map: cbc.CodeMap{ - KeyFA_VATFormaPlatnosci: "1", + KeyFA_VATPaymentType: "1", }, }, { @@ -34,7 +34,7 @@ var paymentMeansKeyDefinitions = []*tax.KeyDefinition{ i18n.PL: "Karta", }, Map: cbc.CodeMap{ - KeyFA_VATFormaPlatnosci: "2", + KeyFA_VATPaymentType: "2", }, }, { @@ -44,7 +44,7 @@ var paymentMeansKeyDefinitions = []*tax.KeyDefinition{ i18n.PL: "Bon", }, Map: cbc.CodeMap{ - KeyFA_VATFormaPlatnosci: "3", + KeyFA_VATPaymentType: "3", }, }, { @@ -54,7 +54,7 @@ var paymentMeansKeyDefinitions = []*tax.KeyDefinition{ i18n.PL: "Czek", }, Map: cbc.CodeMap{ - KeyFA_VATFormaPlatnosci: "4", + KeyFA_VATPaymentType: "4", }, }, { @@ -64,7 +64,7 @@ var paymentMeansKeyDefinitions = []*tax.KeyDefinition{ i18n.PL: "Kredyt", }, Map: cbc.CodeMap{ - KeyFA_VATFormaPlatnosci: "5", + KeyFA_VATPaymentType: "5", }, }, { @@ -74,7 +74,7 @@ var paymentMeansKeyDefinitions = []*tax.KeyDefinition{ i18n.PL: "Przelew", }, Map: cbc.CodeMap{ - KeyFA_VATFormaPlatnosci: "6", + KeyFA_VATPaymentType: "6", }, }, { @@ -84,7 +84,7 @@ var paymentMeansKeyDefinitions = []*tax.KeyDefinition{ i18n.PL: "Mobilna", }, Map: cbc.CodeMap{ - KeyFA_VATFormaPlatnosci: "7", + KeyFA_VATPaymentType: "7", }, }, } diff --git a/regimes/pl/pl.go b/regimes/pl/pl.go index 5d9bc13d..995ec857 100644 --- a/regimes/pl/pl.go +++ b/regimes/pl/pl.go @@ -16,7 +16,8 @@ func init() { // Custom keys used typically in meta or codes information. const ( - KeyFA_VATFormaPlatnosci cbc.Key = "favat-forma-platnosci" // for mapping to TFormaPlatnosci's codes + 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. @@ -32,8 +33,8 @@ func New() *tax.Regime { // ChargeKeys: chargeKeyDefinitions, // charges.go PaymentMeansKeys: paymentMeansKeyDefinitions, // pay.go // Extensions: extensionKeys, // extensions.go - // Tags: invoiceTags, - // Scenarios: scenarios, // scenarios.go + Tags: invoiceTags, + Scenarios: scenarios, // scenarios.go Validator: Validate, // Calculator: Calculate, Categories: taxCategories, // tax_categories.go 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", + }, + }, + }, +} From 814a977d1ea7a5fc62c27e5fbf229cb8a7577ff5 Mon Sep 17 00:00:00 2001 From: Rafal Lyzwa Date: Mon, 4 Dec 2023 13:18:58 +0100 Subject: [PATCH 6/6] improve party validation for PL regime --- regimes/pl/invoices.go | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/regimes/pl/invoices.go b/regimes/pl/invoices.go index e123d027..c875510f 100644 --- a/regimes/pl/invoices.go +++ b/regimes/pl/invoices.go @@ -29,8 +29,7 @@ func (v *invoiceValidator) validate() error { // Rectification state determined by Preceding value. validation.Field(&inv.Type, validation.In( bill.InvoiceTypeStandard, - bill.InvoiceTypeCorrective, - bill.InvoiceTypeProforma, + // bill.InvoiceTypeCorrective, )), // validation.Field(&inv.Preceding, // validation.Each(validation.By(v.preceding)), @@ -60,7 +59,18 @@ func (v *invoiceValidator) supplier(value interface{}) error { tax.RequireIdentityCode, validation.By(validatePolishTaxIdentity), ), - // TODO check if name exists + validation.Field(&obj.Name, + validation.When( + len(obj.People) == 0, + validation.Required, + ), + ), + validation.Field(&obj.People[0].Name, + validation.When( + obj.Name == "", + validation.Required, + ), + ), ) } @@ -80,7 +90,18 @@ func (v *invoiceValidator) commercialCustomer(value interface{}) error { obj.TaxID.Country.In(l10n.PL), validation.By(validatePolishTaxIdentity), ), // TODO check if id is valid when other entity - // TODO check if name exists + ), + validation.Field(&obj.Name, + validation.When( + len(obj.People) == 0, + validation.Required, + ), + ), + validation.Field(&obj.People[0].Name, + validation.When( + obj.Name == "", + validation.Required, + ), ), ) }