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

Adding support for preceding and ordering document refs #38

Merged
merged 3 commits into from
Oct 14, 2024
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ jobs:

steps:
- name: Check out code
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
id: go

- name: Lint
uses: golangci/golangci-lint-action@v2
uses: golangci/golangci-lint-action@v6
with:
version: v1.58
7 changes: 1 addition & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Unlike other tax regimes, Italy requires simplified invoices to include the cust

## Sources

You can find copies of the Italian FatturaPA schema in the [schemas folder](./schema).
You can find copies of the Italian FatturaPA schema in the [schemas folder](./schemas).

Key websites:

Expand All @@ -46,11 +46,6 @@ The FatturaPA XML schema is quite large and complex. This library is not complet
Some of the optional elements currently not supported include:

- `Allegati` (attachments)
- `DatiOrdineAcquisto` (data related to purchase orders)
- `DatiContratto` (data related to contracts)
- `DatiConvenzione` (data related to conventions)
- `DatiRicezione` (data related to receipts)
- `DatiFattureCollegate` (data related to linked invoices)
- `DatiBollo` (data related to duty stamps)

## Usage
Expand Down
105 changes: 86 additions & 19 deletions body.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/invopop/gobl/addons/it/sdi"
"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/org"
"github.com/invopop/gobl/regimes/it"
)

Expand All @@ -26,15 +27,31 @@ const stampDutyCode = "SI"
// fatturaElettronicaBody contains all invoice data apart from the parties
// involved, which are contained in FatturaElettronicaHeader.
type fatturaElettronicaBody struct {
DatiGenerali *datiGenerali
DatiGenerali *GeneralData `xml:"DatiGenerali,omitempty"`
DatiBeniServizi *datiBeniServizi
DatiPagamento *datiPagamento `xml:",omitempty"`
}

// datiGenerali contains general data about the invoice such as retained taxes,
// GeneralData contains general data about the invoice such as retained taxes,
// invoice number, invoice date, document type, etc.
type datiGenerali struct {
DatiGeneraliDocumento *datiGeneraliDocumento
type GeneralData struct {
Document *datiGeneraliDocumento `xml:"DatiGeneraliDocumento"`
Purchases []*DocumentRef `xml:"DatiOrdineAcquisto,omitempty"`
Contracts []*DocumentRef `xml:"DatiContratto,omitempty"`
Tender []*DocumentRef `xml:"DatiConvenzione,omitempty"`
Receiving []*DocumentRef `xml:"DatiRicezione,omitempty"`
Preceding []*DocumentRef `xml:"DatiFattureCollegate,omitempty"`
}

// DocumentRef contains data about a previous document.
type DocumentRef struct {
Lines []int `xml:"RiferimentoNumeroLinea"` // detail row of the invoice referred to (if the reference is to the entire invoice, this is not filled in)
Code string `xml:"IdDocumento"` // document number
IssueDate string `xml:"Data,omitempty"` // document date (expressed according to the ISO 8601:2004 format)
LineCode string `xml:"NumItem,omitempty"` // identification of the single item on the document (e.g. in the case of a purchase order, this is the number of the row of the purchase order, or, in the case of a contract, it is the number of the row of the contract, etc. )
OrderCode string `xml:"CodiceCommessaConvenzione,omitempty"` // order or agreement code
CUPCode string `xml:"CodiceCUP,omitempty"` // code managed by the CIPE (Interministerial Committee for Economic Planning) which characterises every public investment project (Individual Project Code).
CIGCode string `xml:"CodiceCIG,omitempty"` // Tender procedure identification code
}

type datiGeneraliDocumento struct {
Expand Down Expand Up @@ -71,7 +88,7 @@ func newFatturaElettronicaBody(inv *bill.Invoice) (*fatturaElettronicaBody, erro
return nil, err
}

dg, err := newDatiGenerali(inv)
dg, err := newGeneralData(inv)
if err != nil {
return nil, err
}
Expand All @@ -83,7 +100,57 @@ func newFatturaElettronicaBody(inv *bill.Invoice) (*fatturaElettronicaBody, erro
}, nil
}

func newDatiGenerali(inv *bill.Invoice) (*datiGenerali, error) {
func newGeneralData(inv *bill.Invoice) (*GeneralData, error) {
gd := new(GeneralData)
var err error
if gd.Document, err = newGeneralDataDocument(inv); err != nil {
return nil, err
}
gd.Preceding = newDocumentRefs(inv.Preceding)
if o := inv.Ordering; o != nil {
gd.Purchases = newDocumentRefs(o.Purchases)
gd.Contracts = newDocumentRefs(o.Contracts)
gd.Tender = newDocumentRefs(o.Tender)
gd.Receiving = newDocumentRefs(o.Receiving)
}
return gd, nil
}

func newDocumentRefs(refs []*org.DocumentRef) []*DocumentRef {
out := make([]*DocumentRef, len(refs))
for i, ref := range refs {
out[i] = newDocumentRef(ref)
}
return out
}

func newDocumentRef(ref *org.DocumentRef) *DocumentRef {
dr := &DocumentRef{
Lines: ref.Lines,
Code: ref.Series.Join(ref.Code).String(),
}
if ref.IssueDate != nil {
dr.IssueDate = ref.IssueDate.String()
}
for _, id := range ref.Identities {
switch id.Key {
case org.IdentityKeyOrder:
dr.OrderCode = string(id.Code)
case org.IdentityKeyItem:
dr.LineCode = string(id.Code)
}
switch id.Type {
case sdi.IdentityTypeCIG:
dr.CIGCode = string(id.Code)
case sdi.IdentityTypeCUP:
dr.CUPCode = string(id.Code)
}
}

return dr
}

func newGeneralDataDocument(inv *bill.Invoice) (*datiGeneraliDocumento, error) {
dr, err := extractRetainedTaxes(inv)
if err != nil {
return nil, err
Expand All @@ -104,19 +171,19 @@ func newDatiGenerali(inv *bill.Invoice) (*datiGenerali, error) {
code = cbc.Code(fmt.Sprintf("%s-%s", inv.Series, inv.Code))
}

return &datiGenerali{
DatiGeneraliDocumento: &datiGeneraliDocumento{
TipoDocumento: codeTipoDocumento,
Divisa: string(inv.Currency),
Data: inv.IssueDate.String(),
Numero: code.String(),
DatiRitenuta: dr,
DatiBollo: newDatiBollo(inv.Charges),
ImportoTotaleDocumento: formatAmount(&inv.Totals.Payable),
ScontoMaggiorazione: extractPriceAdjustments(inv),
Causale: extractInvoiceReasons(inv),
},
}, nil
doc := &datiGeneraliDocumento{
TipoDocumento: codeTipoDocumento,
Divisa: string(inv.Currency),
Data: inv.IssueDate.String(),
Numero: code.String(),
DatiRitenuta: dr,
DatiBollo: newDatiBollo(inv.Charges),
ImportoTotaleDocumento: formatAmount(&inv.Totals.Payable),
ScontoMaggiorazione: extractPriceAdjustments(inv),
Causale: extractInvoiceReasons(inv),
}

return doc, nil
}

func findCodeTipoDocumento(inv *bill.Invoice) (string, error) {
Expand Down
11 changes: 9 additions & 2 deletions cmd/gobl.fatturapa/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
package main

import (
"bytes"
"encoding/json"
"fmt"

"github.com/invopop/gobl"
fatturapa "github.com/invopop/gobl.fatturapa"
"github.com/invopop/gobl/l10n"
"github.com/invopop/xmldsig"
Expand Down Expand Up @@ -55,8 +58,12 @@ func (c *convertOpts) runE(cmd *cobra.Command, args []string) error {
return err
}

env, err := fatturapa.UnmarshalGOBL(input)
if err != nil {
buf := new(bytes.Buffer)
if _, err := buf.ReadFrom(input); err != nil {
panic(err)
}
env := new(gobl.Envelope)
if err := json.Unmarshal(buf.Bytes(), env); err != nil {
return err
}

Expand Down
19 changes: 0 additions & 19 deletions converter.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package fatturapa

import (
"bytes"
"encoding/json"
"io"
"time"

"github.com/invopop/gobl"
"github.com/invopop/xmldsig"
)

Expand Down Expand Up @@ -72,18 +68,3 @@ func NewConverter(opts ...Option) *Converter {

return c
}

// UnmarshalGOBL converts the given JSON document to a GOBL Envelope
func UnmarshalGOBL(reader io.Reader) (*gobl.Envelope, error) {
buf := new(bytes.Buffer)
if _, err := buf.ReadFrom(reader); err != nil {
return nil, err
}

env := new(gobl.Envelope)
if err := json.Unmarshal(buf.Bytes(), env); err != nil {
return nil, err
}

return env, nil
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.22
toolchain go1.22.5

require (
github.com/invopop/gobl v0.200.0-rc3
github.com/invopop/gobl v0.202.0
github.com/invopop/xmldsig v0.8.0
github.com/lestrrat-go/libxml2 v0.0.0-20240521004304-a75c203ac627
github.com/spf13/cobra v1.8.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/gobl v0.200.0-rc3 h1:Y/sQMQHufdlNx5Jlx78bnrValco/JLIqA9WbJjbLi5M=
github.com/invopop/gobl v0.200.0-rc3/go.mod h1:DmPohPel8b3ta4nDKnXRNzWQlB89cN74e0/WwPUEZUU=
github.com/invopop/gobl v0.202.0 h1:iUKk7FCKHbEKzuK7qAIxRkZAuh/8g09F9+Ufvs7aWbM=
github.com/invopop/gobl v0.202.0/go.mod h1:DmPohPel8b3ta4nDKnXRNzWQlB89cN74e0/WwPUEZUU=
github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI=
github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/invopop/validation v0.7.0 h1:NBPLqvYGmLZLQuk5jh0PbaBBetJW7f2VEk/BTWJkGBU=
Expand Down
8 changes: 4 additions & 4 deletions retained_taxes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,25 @@ import (
)

func TestDatiRitenuta(t *testing.T) {
t.Run("When retained taxes are NOT present", func(t *testing.T) {
t.Run("when retained taxes are NOT present", func(t *testing.T) {
t.Run("should be empty", func(t *testing.T) {
env := test.LoadTestFile("invoice-simple.json")
doc, err := test.ConvertFromGOBL(env)
require.NoError(t, err)

dr := doc.FatturaElettronicaBody[0].DatiGenerali.DatiGeneraliDocumento.DatiRitenuta
dr := doc.FatturaElettronicaBody[0].DatiGenerali.Document.DatiRitenuta

assert.Empty(t, dr)
})
})

t.Run("When retained taxes are present", func(t *testing.T) {
t.Run("when retained taxes are present", func(t *testing.T) {
t.Run("should contain the correct retainted taxes", func(t *testing.T) {
env := test.LoadTestFile("invoice-irpef.json")
doc, err := test.ConvertFromGOBL(env)
require.NoError(t, err)

dr := doc.FatturaElettronicaBody[0].DatiGenerali.DatiGeneraliDocumento.DatiRitenuta
dr := doc.FatturaElettronicaBody[0].DatiGenerali.Document.DatiRitenuta

require.Len(t, dr, 2)

Expand Down
Loading
Loading