Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add different tax rates #14

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
13 changes: 11 additions & 2 deletions cmd/gobl.ksef/send.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"os"
"regexp"
"time"

"github.com/invopop/gobl"
Expand Down Expand Up @@ -60,6 +61,7 @@ func (c *sendOpts) runE(cmd *cobra.Command, args []string) error {
ksef_api.WithID(nip),
ksef_api.WithToken(token),
ksef_api.WithKeyPath(keyPath),
ksef_api.WithDebugClient(),
)

env, err := SendInvoice(client, data)
Expand Down Expand Up @@ -191,7 +193,14 @@ func saveFile(name string, data []byte) error {

func filename(inv *bill.Invoice) string {
if inv.Series != "" {
return inv.Series + "-" + inv.Code + ".xml"
return sanitizeFilename(inv.Series + "_" + inv.Code + ".xml")
}
return inv.Code + ".xml"
return sanitizeFilename(inv.Code + ".xml")
}

func sanitizeFilename(filename string) string {
re := regexp.MustCompile(`[^\w\.-]`)
sanitized := re.ReplaceAllString(filename, "_")

return sanitized
}
165 changes: 114 additions & 51 deletions invoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,59 @@

/**/
import (
"slices"

Check failure on line 5 in invoice.go

View workflow job for this annotation

GitHub Actions / Test

package slices is not in GOROOT (/opt/hostedtoolcache/go/1.19.13/x64/src/slices)

"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/l10n"
"github.com/invopop/gobl/regimes/pl"
"github.com/invopop/gobl/tax"
)

const (
regionDomestic = "domestic"
regionEU = "EU"
regionNonEU = "non-EU"
)

// Inv defines the XML structure for KSeF invoice
type Inv struct {
CurrencyCode string `xml:"KodWaluty"`
IssueDate string `xml:"P_1"`
IssuePlace string `xml:"P_1M,omitempty"`
SequentialNumber string `xml:"P_2"`
CompletionDate string `xml:"P_6,omitempty"`
StartDate string `xml:"P_6_Od,omitempty"`
EndDate string `xml:"P_6_Do,omitempty"`
StandardRateNetSale string `xml:"P_13_1,omitempty"`
StandardRateTax string `xml:"P_14_1,omitempty"`
StandardRateTaxConvertedToPln string `xml:"P_14_1W,omitempty"`
ReducedRateNetSale string `xml:"P_13_2,omitempty"`
ReducedRateTax string `xml:"P_14_2,omitempty"`
ReducedRateTaxConvertedToPln string `xml:"P_14_2W,omitempty"`
SuperReducedRateNetSale string `xml:"P_13_3,omitempty"`
SuperReducedRateTax string `xml:"P_14_3,omitempty"`
SuperReducedRateTaxConvertedToPln string `xml:"P_14_3W,omitempty"`
TaxiRateNetSale string `xml:"P_13_4,omitempty"`
TaxiRateTax string `xml:"P_14_4,omitempty"`
TaxiRateTaxConvertedToPln string `xml:"P_14_4W,omitempty"`
SpecialProcedureNetSale string `xml:"P_13_5,omitempty"`
SpecialProcedureTax string `xml:"P_14_5,omitempty"`
ZeroTaxExceptIntraCommunityNetSale string `xml:"P_13_6_1,omitempty"`
IntraCommunityNetSale string `xml:"P_13_6_2,omitempty"`
ExportNetSale string `xml:"P_13_6_3,omitempty"`
TaxExemptNetSale string `xml:"P_13_7,omitempty"`
InternationalNetSale string `xml:"P_13_8,omitempty"`
OtherNetSale string `xml:"P_13_9,omitempty"`
EUServiceNetSale string `xml:"P_13_10,omitempty"`
MarginNetSale string `xml:"P_13_11,omitempty"`
TotalAmountReceivable string `xml:"P_15"`
Annotations *Annotations `xml:"Adnotacje"`
InvoiceType string `xml:"RodzajFaktury"`
CorrectionReason string `xml:"PrzyczynaKorekty,omitempty"`
CorrectionType string `xml:"TypKorekty,omitempty"`
CorrectedInv *CorrectedInv `xml:"DaneFaKorygowanej,omitempty"`
Lines []*Line `xml:"FaWiersz"`
Payment *Payment `xml:"Platnosc"`
CurrencyCode string `xml:"KodWaluty"`
IssueDate string `xml:"P_1"`
IssuePlace string `xml:"P_1M,omitempty"`
SequentialNumber string `xml:"P_2"`
CompletionDate string `xml:"P_6,omitempty"`
StartDate string `xml:"P_6_Od,omitempty"`
EndDate string `xml:"P_6_Do,omitempty"`
StandardRateNetSale string `xml:"P_13_1,omitempty"`
StandardRateTax string `xml:"P_14_1,omitempty"`
StandardRateTaxConvertedToPln string `xml:"P_14_1W,omitempty"`
ReducedRateNetSale string `xml:"P_13_2,omitempty"`
ReducedRateTax string `xml:"P_14_2,omitempty"`
ReducedRateTaxConvertedToPln string `xml:"P_14_2W,omitempty"`
SuperReducedRateNetSale string `xml:"P_13_3,omitempty"`
SuperReducedRateTax string `xml:"P_14_3,omitempty"`
SuperReducedRateTaxConvertedToPln string `xml:"P_14_3W,omitempty"`
TaxiRateNetSale string `xml:"P_13_4,omitempty"`
TaxiRateTax string `xml:"P_14_4,omitempty"`
TaxiRateTaxConvertedToPln string `xml:"P_14_4W,omitempty"`
SpecialProcedureNetSale string `xml:"P_13_5,omitempty"`
SpecialProcedureTax string `xml:"P_14_5,omitempty"`
DomesticZeroTaxNetSale string `xml:"P_13_6_1,omitempty"`
EUZeroTaxNetSale string `xml:"P_13_6_2,omitempty"`
ExportNetSale string `xml:"P_13_6_3,omitempty"`
TaxExemptNetSale string `xml:"P_13_7,omitempty"`
TaxNAInternationalNetSale string `xml:"P_13_8,omitempty"`
TaxNAEUNetSale string `xml:"P_13_9,omitempty"`
EUServiceNetSale string `xml:"P_13_10,omitempty"`
MarginNetSale string `xml:"P_13_11,omitempty"`
TotalAmountReceivable string `xml:"P_15"`
Annotations *Annotations `xml:"Adnotacje"`
InvoiceType string `xml:"RodzajFaktury"`
CorrectionReason string `xml:"PrzyczynaKorekty,omitempty"`
CorrectionType string `xml:"TypKorekty,omitempty"`
CorrectedInv *CorrectedInv `xml:"DaneFaKorygowanej,omitempty"`
Lines []*Line `xml:"FaWiersz"`
Payment *Payment `xml:"Platnosc"`
}

// Annotations defines the XML structure for KSeF annotations
Expand All @@ -61,7 +70,7 @@
}

// newAnnotations sets annotations data
func newAnnotations() *Annotations {
func newAnnotations(inv *bill.Invoice) *Annotations {
// default values for the most common case,
// For fields P_16 to P_18 and field P_23 2 means "no", 1 means "yes".
// for others 1 means "yes", no value means "no"
Expand All @@ -75,14 +84,19 @@
SimplifiedProcedureBySecondTaxpayer: 2,
NoMarginProcedures: 1,
}

if inv.Tax != nil && slices.Contains(inv.Tax.Tags, tax.TagReverseCharge) {
Annotations.ReverseCharge = 1
}

return Annotations
}

// NewInv gets invoice data from GOBL invoice
func NewInv(inv *bill.Invoice) *Inv {
cu := inv.Currency.Def().Subunits
Inv := &Inv{
Annotations: newAnnotations(),
Annotations: newAnnotations(inv),
CurrencyCode: string(inv.Currency),
IssueDate: inv.IssueDate.String(),
SequentialNumber: invoiceNumber(inv.Series, inv.Code),
Expand All @@ -106,24 +120,16 @@
if inv.OperationDate != nil {
Inv.CompletionDate = inv.OperationDate.String()
}

reg := region(inv)

for _, cat := range inv.Totals.Taxes.Categories {
if cat.Code != tax.CategoryVAT {
continue
}

for _, rate := range cat.Rates {
if rate.Percent != nil {
if rate.Key == tax.RateStandard {
Inv.StandardRateNetSale = rate.Base.Rescale(cu).String()
Inv.StandardRateTax = rate.Amount.Rescale(cu).String()
} else if rate.Key == tax.RateReduced {
Inv.ReducedRateNetSale = rate.Base.Rescale(cu).String()
Inv.ReducedRateTax = rate.Amount.Rescale(cu).String()
} else if rate.Key == tax.RateSuperReduced {
Inv.SuperReducedRateNetSale = rate.Base.Rescale(cu).String()
Inv.SuperReducedRateTax = rate.Amount.Rescale(cu).String()
}
}
setTaxRate(Inv, rate, cu, reg)
}
}

Expand All @@ -136,3 +142,60 @@
}
return series + "-" + code
}

func setTaxRate(inv *Inv, rate *tax.RateTotal, cu uint32, region string) {
if rate.Percent == nil {
return
}

base := rate.Base.Rescale(cu).String()
taxAmount := rate.Amount.Rescale(cu).String()

switch rate.Key {
case tax.RateStandard:
inv.StandardRateNetSale = base
inv.StandardRateTax = taxAmount
case tax.RateReduced:
inv.ReducedRateNetSale = base
inv.ReducedRateTax = taxAmount
case tax.RateSuperReduced:
inv.SuperReducedRateNetSale = base
inv.SuperReducedRateTax = taxAmount
case tax.RateSpecial:
if rate.Ext.Has(pl.ExtKeyKSeFVATSpecial) && rate.Ext[pl.ExtKeyKSeFVATSpecial].String() == "taxi" {
inv.TaxiRateNetSale = base
inv.TaxiRateTax = taxAmount
}
case tax.RateZero:
switch region {
case regionDomestic:
inv.DomesticZeroTaxNetSale = base
case regionEU:
inv.EUZeroTaxNetSale = base
case regionNonEU:
inv.ExportNetSale = base
}
case tax.RateExempt:
inv.TaxExemptNetSale = base
case pl.TaxRateNotPursuant:
switch region {
case regionEU:
inv.TaxNAEUNetSale = base
case regionNonEU:
inv.TaxNAInternationalNetSale = base
}
}
}

func region(inv *bill.Invoice) string {
if inv.Supplier == nil || inv.Customer == nil || inv.Supplier.TaxID == nil || inv.Customer.TaxID == nil {
return regionDomestic
}
if isEUCountry(inv.Supplier.TaxID.Country) || isEUCountry(inv.Customer.TaxID.Country) {
return regionEU
}
if inv.Supplier.TaxID.Country != l10n.PL || inv.Customer.TaxID.Country != l10n.PL {
return regionNonEU
}
return regionDomestic
}
10 changes: 10 additions & 0 deletions ksef.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ func NewDocument(env *gobl.Envelope) (*Invoice, error) {
return nil, fmt.Errorf("invalid type %T", env.Document)
}

// Invert if we're dealing with a credit note
if inv.Type == bill.InvoiceTypeCreditNote {
if err := inv.Invert(); err != nil {
return nil, fmt.Errorf("inverting invoice: %w", err)
}
if err := inv.Calculate(); err != nil {
return nil, fmt.Errorf("inverting invoice: %w", err)
}
}

invoice := &Invoice{
XMLName: xml.Name{Local: RootElementName},
XSINamespace: XSINamespace,
Expand Down
23 changes: 22 additions & 1 deletion lines.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package ksef
import (
"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/num"
"github.com/invopop/gobl/regimes/pl"
"github.com/invopop/gobl/tax"
)

// Line defines the XML structure for KSeF item line
Expand Down Expand Up @@ -32,12 +34,31 @@ func newLine(line *bill.Line) *Line {
Quantity: line.Quantity.String(),
UnitDiscount: unitDiscount(line),
NetPriceTotal: line.Total.String(),
TaxRate: line.Taxes[0].Percent.Rescale(2).StringWithoutSymbol(),
TaxRate: newTaxRate(line.Taxes.Get(tax.CategoryVAT)),
}

return Line
}

// newTaxRate returns tax rate as string value with one of the values:
// "23", "22", "8", "7", "5", "4", "3", "0", "np", "zw"
func newTaxRate(t *tax.Combo) string {
if t == nil {
return ""
}

switch t.Rate {
case tax.RateZero:
return "0"
case tax.RateExempt:
return "zw"
case pl.TaxRateNotPursuant:
return "np"
default:
return t.Percent.Rescale(2).StringWithoutSymbol()
}
}

func unitDiscount(line *bill.Line) string {
if len(line.Discounts) == 0 {
return ""
Expand Down
Loading
Loading