Skip to content

Commit

Permalink
Merge pull request #38 from invopop/ordering-doc-refs
Browse files Browse the repository at this point in the history
Adding support for preceding and ordering document refs
  • Loading branch information
samlown authored Oct 14, 2024
2 parents b801ce8 + ee4505c commit f4d4ead
Show file tree
Hide file tree
Showing 17 changed files with 1,199 additions and 1,745 deletions.
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

0 comments on commit f4d4ead

Please sign in to comment.