Skip to content

Commit

Permalink
Merge pull request #15 from invopop/fuel-complement
Browse files Browse the repository at this point in the history
Convert to EstadoDeCuentaCombustible complement
  • Loading branch information
cavalle authored Oct 5, 2023
2 parents c7fae2d + 96d765a commit 092f5e6
Show file tree
Hide file tree
Showing 14 changed files with 588 additions and 2,427 deletions.
35 changes: 30 additions & 5 deletions cfdi.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ import (
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/num"
"github.com/invopop/gobl/regimes/mx"
"github.com/invopop/gobl/schema"
"github.com/invopop/gobl/tax"
)

// CFDI schema constants
const (
CFDINamespace = "http://www.sat.gob.mx/cfd/4"
XSINamespace = "http://www.w3.org/2001/XMLSchema-instance"
SchemaLocation = "http://www.sat.gob.mx/cfd/4 http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd"
CFDIVersion = "4.0"
CFDINamespace = "http://www.sat.gob.mx/cfd/4"
CFDISchemaLocation = "http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd"
XSINamespace = "http://www.w3.org/2001/XMLSchema-instance"
CFDIVersion = "4.0"
)

// Hard-coded values for (yet) unsupported mappings
Expand All @@ -38,6 +39,7 @@ type Document struct {
XMLName xml.Name `xml:"cfdi:Comprobante"`
CFDINamespace string `xml:"xmlns:cfdi,attr"`
XSINamespace string `xml:"xmlns:xsi,attr"`
ECCNamespace string `xml:"xmlns:ecc12,attr,omitempty"`
SchemaLocation string `xml:"xsi:schemaLocation,attr"`
Version string `xml:"Version,attr"`

Expand All @@ -63,6 +65,8 @@ type Document struct {
Receptor *Receptor `xml:"cfdi:Receptor"`
Conceptos *Conceptos `xml:"cfdi:Conceptos"` //nolint:misspell
Impuestos *Impuestos `xml:"cfdi:Impuestos,omitempty"`

Complementos []interface{} `xml:"cfdi:Complemento>*,omitempty"`
}

// NewDocument converts a GOBL envelope into a CFDI document
Expand All @@ -78,7 +82,7 @@ func NewDocument(env *gobl.Envelope) (*Document, error) {
document := &Document{
CFDINamespace: CFDINamespace,
XSINamespace: XSINamespace,
SchemaLocation: SchemaLocation,
SchemaLocation: formatSchemaLocation(CFDINamespace, CFDISchemaLocation),
Version: CFDIVersion,

TipoDeComprobante: lookupTipoDeComprobante(inv),
Expand All @@ -104,6 +108,10 @@ func NewDocument(env *gobl.Envelope) (*Document, error) {
Impuestos: newImpuestos(inv.Totals, &inv.Currency),
}

if err := addComplementos(document, inv.Complements); err != nil {
return nil, err
}

return document, nil
}

Expand All @@ -117,6 +125,19 @@ func (d *Document) Bytes() ([]byte, error) {
return append([]byte(xml.Header), bytes...), nil
}

func addComplementos(d *Document, complements []*schema.Object) error {
for _, c := range complements {
switch o := c.Instance().(type) {
case *mx.FuelAccountBalance:
addEstadoCuentaCombustible(d, o)
default:
return fmt.Errorf("unsupported complement %T", o)
}
}

return nil
}

func formatIssueDate(date cal.Date) string {
dateTime := civil.DateTime{Date: date.Date, Time: civil.Time{}}
return dateTime.String()
Expand Down Expand Up @@ -173,6 +194,10 @@ func formatOptionalAmount(a num.Amount) string {
return a.String()
}

func formatSchemaLocation(namespace, schemaLocation string) string {
return fmt.Sprintf("%s %s", namespace, schemaLocation)
}

func totalInvoiceDiscount(i *bill.Invoice) num.Amount {
td := i.Currency.Def().Zero() // currency's precision is required by the SAT
for _, l := range i.Lines {
Expand Down
40 changes: 2 additions & 38 deletions cfdi_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package cfdi_test

import (
"fmt"
"path/filepath"
"testing"

"github.com/invopop/gobl.cfdi/test"
"github.com/lestrrat-go/libxml2"
"github.com/lestrrat-go/libxml2/xsd"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -35,6 +31,8 @@ func TestComprobanteIngreso(t *testing.T) {
assert.Equal(t, "PUE", doc.MetodoPago)
assert.Equal(t, "03", doc.FormaPago)
assert.Equal(t, "Pago a 30 días.", doc.CondicionesDePago)

assert.Equal(t, 0, len(doc.Complementos))
})
}

Expand All @@ -48,37 +46,3 @@ func TestComprobanteEgreso(t *testing.T) {
assert.Equal(t, "0003", doc.Folio)
})
}

func TestXMLGeneration(t *testing.T) {
schemaPath := filepath.Join(test.GetTestPath(), "schema", "cfdv40.xsd")
schema, err := xsd.ParseFromFile(schemaPath)
defer schema.Free()
require.NoError(t, err)

tests := []string{
"bare-minimum-invoice.json",
"invoice.json",
"credit-note.json",
}

for _, testFile := range tests {
name := fmt.Sprintf("should generate a schema-valid XML from %s", testFile)
t.Run(name, func(t *testing.T) {
doc, err := test.NewDocumentFrom(testFile)
require.NoError(t, err)

data, err := doc.Bytes()
require.NoError(t, err)

xmlDoc, err := libxml2.ParseString(string(data))
require.NoError(t, err)

err = schema.Validate(xmlDoc)
if err != nil {
for _, e := range err.(xsd.SchemaValidationError).Errors() {
require.NoError(t, e)
}
}
})
}
}
105 changes: 105 additions & 0 deletions examples_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package cfdi_test

import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"testing"

"github.com/invopop/gobl.cfdi/test"
"github.com/lestrrat-go/libxml2"
"github.com/lestrrat-go/libxml2/xsd"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

var updateOut = flag.Bool("update", false, "Update the XML files in the test/data/out directory")

func TestXMLGeneration(t *testing.T) {

schema, err := loadSchema()
require.NoError(t, err)

examples, err := lookupExamples()
require.NoError(t, err)

for _, example := range examples {
name := fmt.Sprintf("should convert %s example file successfully", example)

t.Run(name, func(t *testing.T) {
data, err := convertExample(example)
require.NoError(t, err)

outPath := filepath.Join(test.GetDataPath(), "out", strings.TrimSuffix(example, ".json")+".xml")

if *updateOut {
errs := validateDoc(schema, data)
for _, e := range errs {
assert.NoError(t, e)
}
if len(errs) > 0 {
return
}

err = os.WriteFile(outPath, data, 0644)
require.NoError(t, err)

return
}

expected, err := os.ReadFile(outPath)

require.False(t, os.IsNotExist(err), "output file %s missing, run tests with `--update` flag to create", filepath.Base(outPath))
require.NoError(t, err)
require.Equal(t, string(expected), string(data), "output file %s does not match, run tests with `--update` flag to update", filepath.Base(outPath))
})
}
}

func loadSchema() (*xsd.Schema, error) {
schemaPath := filepath.Join(test.GetTestPath(), "schema", "schema.xsd")
schema, err := xsd.ParseFromFile(schemaPath)
if err != nil {
return nil, err
}

return schema, nil
}

func lookupExamples() ([]string, error) {
examples, err := filepath.Glob(filepath.Join(test.GetDataPath(), "*.json"))
if err != nil {
return nil, err
}

for i, example := range examples {
examples[i] = filepath.Base(example)
}

return examples, nil
}

func convertExample(example string) ([]byte, error) {
doc, err := test.NewDocumentFrom(example)
if err != nil {
return nil, err
}

return doc.Bytes()
}

func validateDoc(schema *xsd.Schema, doc []byte) []error {
xmlDoc, err := libxml2.ParseString(string(doc))
if err != nil {
return []error{err}
}

err = schema.Validate(xmlDoc)
if err != nil {
return err.(xsd.SchemaValidationError).Errors()
}

return nil
}
107 changes: 107 additions & 0 deletions fuel_account_balance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package cfdi

import (
"encoding/xml"

"github.com/invopop/gobl/regimes/mx"
)

// ECC Schema constants
const (
ECCVersion = "1.2"
ECCTipoOperacion = "Tarjeta"
ECCNamespace = "http://www.sat.gob.mx/EstadoDeCuentaCombustible12"
ECCSchemaLocation = "http://www.sat.gob.mx/sitio_internet/cfd/EstadoDeCuentaCombustible/ecc12.xsd"
)

// EstadoDeCuentaCombustible stores the fuel account balance data
type EstadoDeCuentaCombustible struct {
XMLName xml.Name `xml:"ecc12:EstadoDeCuentaCombustible"`
Version string `xml:",attr"`
TipoOperacion string `xml:",attr"`

NumeroDeCuenta string `xml:",attr"`
SubTotal string `xml:",attr"`
Total string `xml:",attr"`

Conceptos []*ECCConcepto `xml:"ecc12:Conceptos>ecc12:ConceptoEstadoDeCuentaCombustible"` // nolint:misspell
}

// ECCConcepto stores the data of a fuel purchase
type ECCConcepto struct {
Identificador string `xml:",attr"`
Fecha string `xml:",attr"`
Rfc string `xml:",attr"`
ClaveEstacion string `xml:",attr"`
Cantidad string `xml:",attr"`
TipoCombustible string `xml:",attr"`
Unidad string `xml:",attr,omitempty"`
NombreCombustible string `xml:",attr"`
FolioOperacion string `xml:",attr"`
ValorUnitario string `xml:",attr"`
Importe string `xml:",attr"`

Traslados []*ECCTraslado `xml:"ecc12:Traslados>ecc12:Traslado"`
}

// ECCTraslado stores the tax data of a fuel purchase
type ECCTraslado struct {
Impuesto string `xml:",attr"`
TasaOCuota string `xml:",attr"`
Importe string `xml:",attr"`
}

func addEstadoCuentaCombustible(doc *Document, fc *mx.FuelAccountBalance) {
ecc := &EstadoDeCuentaCombustible{
Version: ECCVersion,
TipoOperacion: ECCTipoOperacion,

NumeroDeCuenta: fc.AccountNumber,
SubTotal: fc.Subtotal.String(),
Total: fc.Total.String(),

Conceptos: newECCConceptos(fc.Lines), // nolint:misspell
}

doc.ECCNamespace = ECCNamespace
doc.SchemaLocation = doc.SchemaLocation + " " + formatSchemaLocation(ECCNamespace, ECCSchemaLocation)
doc.Complementos = append(doc.Complementos, ecc)
}

// nolint:misspell
func newECCConceptos(lines []*mx.FuelAccountLine) []*ECCConcepto {
cs := make([]*ECCConcepto, len(lines))

for i, l := range lines {
cs[i] = &ECCConcepto{
Identificador: l.EWalletID.String(),
Fecha: l.PurchaseDateTime.String(),
Rfc: l.VendorTaxCode.String(),
ClaveEstacion: l.ServiceStationCode.String(),
Cantidad: l.Quantity.String(),
TipoCombustible: l.Item.Type.String(),
Unidad: l.Item.Unit.UNECE().String(),
NombreCombustible: l.Item.Name,
FolioOperacion: l.PurchaseCode.String(),
ValorUnitario: l.Item.Price.String(),
Importe: l.Total.String(),
Traslados: newECCTraslados(l.Taxes),
}
}

return cs
}

func newECCTraslados(taxes []*mx.FuelAccountTax) []*ECCTraslado {
ts := make([]*ECCTraslado, len(taxes))

for i, t := range taxes {
ts[i] = &ECCTraslado{
Impuesto: t.Code.String(),
TasaOCuota: t.Rate.String(),
Importe: t.Amount.String(),
}
}

return ts
}
Loading

0 comments on commit 092f5e6

Please sign in to comment.