Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ package ksef

/**/
import (
"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 @@ type Annotations struct {
}

// 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 @@ func newAnnotations() *Annotations {
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 @@ func NewInv(inv *bill.Invoice) *Inv {
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 @@ func invoiceNumber(series string, code string) string {
}
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