From aae0b67f1ce282f51c6c0113a3b2709f05b9b86f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Fri, 17 Nov 2023 13:16:17 +0000 Subject: [PATCH 01/19] Add support for retained taxes --- cfdi.go | 4 +- cfdi_test.go | 2 +- go.mod | 4 +- go.sum | 4 ++ lines.go | 11 ++-- taxes.go | 133 ++++++++++++++++++++++++-------------- test/data/invoice.json | 60 +++++++++++++---- test/data/out/invoice.xml | 12 +++- 8 files changed, 154 insertions(+), 76 deletions(-) diff --git a/cfdi.go b/cfdi.go index a29ae24..2815067 100644 --- a/cfdi.go +++ b/cfdi.go @@ -105,8 +105,8 @@ func NewDocument(env *gobl.Envelope) (*Document, error) { CFDIRelacionados: newCfdiRelacionados(inv), Emisor: newEmisor(inv.Supplier), Receptor: newReceptor(inv.Customer), - Conceptos: newConceptos(inv.Lines), // nolint:misspell - Impuestos: newImpuestos(inv.Totals, &inv.Currency), + Conceptos: newConceptos(inv.Lines, inv.TaxRegime()), // nolint:misspell + Impuestos: newImpuestos(inv.Totals, &inv.Currency, inv.TaxRegime()), } if err := addComplementos(document, inv.Complements); err != nil { diff --git a/cfdi_test.go b/cfdi_test.go index 3f1c151..f4b9f07 100644 --- a/cfdi_test.go +++ b/cfdi_test.go @@ -25,7 +25,7 @@ func TestComprobanteIngreso(t *testing.T) { assert.Equal(t, "26015", doc.LugarExpedicion) assert.Equal(t, "400.40", doc.SubTotal) assert.Equal(t, "200.20", doc.Descuento) - assert.Equal(t, "232.23", doc.Total) + assert.Equal(t, "190.86", doc.Total) assert.Equal(t, "MXN", doc.Moneda) assert.Equal(t, "01", doc.Exportacion) assert.Equal(t, "PUE", doc.MetodoPago) diff --git a/go.mod b/go.mod index 5ca6ad2..2d06e64 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/invopop/gobl.cfdi go 1.20 require ( - github.com/invopop/gobl v0.61.0 + github.com/invopop/gobl v0.62.1-0.20231117145806-7750c374486b github.com/joho/godotenv v1.5.1 github.com/magefile/mage v1.15.0 github.com/spf13/cobra v1.7.0 @@ -30,7 +30,7 @@ require ( github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/google/uuid v1.3.0 // indirect - github.com/invopop/jsonschema v0.9.0 // indirect + github.com/invopop/jsonschema v0.12.0 // indirect github.com/invopop/validation v0.3.0 // indirect github.com/lestrrat-go/libxml2 v0.0.0-20201123224832-e6d9de61b80d github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693 // indirect diff --git a/go.sum b/go.sum index a589e8e..427b427 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,12 @@ github.com/invopop/gobl v0.60.0 h1:BRnpt3FzOmIpCDstJfQLaapOCLhBeqIjpsdsgQTtaB4= github.com/invopop/gobl v0.60.0/go.mod h1:kt3cQtFSOhPCYVlgiaRI267syjI+X1VRW7QHTmitc+Q= github.com/invopop/gobl v0.61.0 h1:gFLX/VTCrn3BH5QMk7mR58lCTPIV1EDJdXEni3Zi5+g= github.com/invopop/gobl v0.61.0/go.mod h1:kt3cQtFSOhPCYVlgiaRI267syjI+X1VRW7QHTmitc+Q= +github.com/invopop/gobl v0.62.1-0.20231117145806-7750c374486b h1:Q0150tTInews6yZ33h16YevRyH7lPz8g+8MMkftyBN4= +github.com/invopop/gobl v0.62.1-0.20231117145806-7750c374486b/go.mod h1:sEngvTr2gAxexosO0rmQInVSL8C613TUPvBcFT4xMyM= github.com/invopop/jsonschema v0.9.0 h1:m1Fe5PN4X9V7P1TCF+pA8Xly3Vj3pY905klC++8oOpM= github.com/invopop/jsonschema v0.9.0/go.mod h1:uMhbTEOXoPcOKzdYRfk914W6UTGA/cVcgEQxXh1MJ7g= +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.3.0 h1:o260kbjXzoBO/ypXDSSrCLL7SxEFUXBsX09YTE9AxZw= github.com/invopop/validation v0.3.0/go.mod h1:qIBG6APYLp2Wu3/96p3idYjP8ffTKVmQBfKiZbw0Hts= github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= diff --git a/lines.go b/lines.go index f8c3240..efbbe9d 100644 --- a/lines.go +++ b/lines.go @@ -4,6 +4,7 @@ import ( "github.com/invopop/gobl/bill" "github.com/invopop/gobl/num" "github.com/invopop/gobl/regimes/mx" + "github.com/invopop/gobl/tax" ) // Default keys @@ -28,21 +29,21 @@ type Concepto struct { Descuento string `xml:",attr,omitempty"` ObjetoImp string `xml:",attr"` - Impuestos *Impuestos `xml:"cfdi:Impuestos,omitempty"` + Impuestos *ConceptoImpuestos `xml:"cfdi:Impuestos,omitempty"` } // nolint:misspell -func newConceptos(lines []*bill.Line) *Conceptos { +func newConceptos(lines []*bill.Line, regime *tax.Regime) *Conceptos { var conceptos []*Concepto for _, line := range lines { - conceptos = append(conceptos, newConcepto(line)) + conceptos = append(conceptos, newConcepto(line, regime)) } return &Conceptos{conceptos} } -func newConcepto(line *bill.Line) *Concepto { +func newConcepto(line *bill.Line, regime *tax.Regime) *Concepto { concepto := &Concepto{ ClaveProdServ: mapToClaveProdServ(line), Cantidad: line.Quantity.String(), @@ -52,7 +53,7 @@ func newConcepto(line *bill.Line) *Concepto { Importe: line.Sum.String(), Descuento: formatOptionalAmount(totalLineDiscount(line)), ObjetoImp: ObjetoImpSi, - Impuestos: newImpuestosFromLine(line), + Impuestos: newConceptoImpuestos(line, regime), } return concepto diff --git a/taxes.go b/taxes.go index e426327..1ec0032 100644 --- a/taxes.go +++ b/taxes.go @@ -4,104 +4,137 @@ import ( "github.com/invopop/gobl/bill" "github.com/invopop/gobl/currency" "github.com/invopop/gobl/num" - "github.com/invopop/gobl/regimes/common" + "github.com/invopop/gobl/regimes/mx" "github.com/invopop/gobl/tax" ) // Impuestos store the invoice tax totals type Impuestos struct { - TotalImpuestosTrasladados string `xml:",attr,omitempty"` - Traslados *Traslados `xml:"cfdi:Traslados,omitempty"` + TotalImpuestosTrasladados string `xml:",attr,omitempty"` + TotalImpuestosRetenidos string `xml:",attr,omitempty"` + Retenciones *Retenciones `xml:"cfdi:Retenciones,omitempty"` + Traslados *Traslados `xml:"cfdi:Traslados,omitempty"` } -// Traslados list the applicable taxes of the invoice or a line +// ConceptoImpuestos store the line tax totals +type ConceptoImpuestos struct { + Traslados *Traslados `xml:"cfdi:Traslados,omitempty"` + Retenciones *Retenciones `xml:"cfdi:Retenciones,omitempty"` +} + +// Traslados lists the non-retained taxes of a line or the invoice type Traslados struct { - Traslado []*Traslado `xml:"cfdi:Traslado"` + Traslado []*Impuesto `xml:"cfdi:Traslado"` +} + +// Retenciones lists the retained taxes of a line or the invoice +type Retenciones struct { + Retencion []*Impuesto `xml:"cfdi:Retencion"` } -// Traslado stores the tax data of the invoice or a line -type Traslado struct { - Base string `xml:",attr"` +// Impuesto stores the tax data of the invoice or a line +type Impuesto struct { + Base string `xml:",attr,omitempty"` Importe string `xml:",attr,omitempty"` Impuesto string `xml:",attr"` TasaOCuota string `xml:",attr,omitempty"` - TipoFactor string `xml:",attr"` + TipoFactor string `xml:",attr,omitempty"` } -func newImpuestos(totals *bill.Totals, currency *currency.Code) *Impuestos { - impuestos := &Impuestos{ - TotalImpuestosTrasladados: totals.Tax.String(), - Traslados: newTraslados(totals.Taxes, currency), +func newImpuestos(totals *bill.Totals, currency *currency.Code, regime *tax.Regime) *Impuestos { + var traslados, retenciones []*Impuesto + totalTraslados, totalRetenciones := currency.Def().Zero(), currency.Def().Zero() + + for _, cat := range totals.Taxes.Categories { + catDef := regime.Category(cat.Code) + + for _, rate := range cat.Rates { + imp := newImpuesto(rate, currency, catDef) + + if catDef.Retained { + // Clear out fields not supported by retained totals + imp.Base = "" + imp.TasaOCuota = "" + imp.TipoFactor = "" + + retenciones = append(retenciones, imp) + totalRetenciones = totalRetenciones.Add(rate.Amount) + } else { + traslados = append(traslados, imp) + totalTraslados = totalTraslados.Add(rate.Amount) + } + } } - return impuestos -} + impuestos := &Impuestos{} + + if len(traslados) > 0 { + impuestos.Traslados = &Traslados{traslados} + impuestos.TotalImpuestosTrasladados = totalTraslados.String() + } -func newImpuestosFromLine(line *bill.Line) *Impuestos { - impuestos := &Impuestos{ - Traslados: newTrasladosFromLine(line), + if len(retenciones) > 0 { + impuestos.Retenciones = &Retenciones{retenciones} + impuestos.TotalImpuestosRetenidos = totalRetenciones.String() } return impuestos } -func newTraslados(taxTotal *tax.Total, currency *currency.Code) *Traslados { - var traslados []*Traslado - - for _, cat := range taxTotal.Categories { - if cat.Code != common.TaxCategoryVAT { - continue - } +func newImpuesto(rate *tax.RateTotal, currency *currency.Code, catDef *tax.Category) *Impuesto { + cu := currency.Def().Units // SAT expects tax total amounts with no more decimals than supported by the currency - for _, rate := range cat.Rates { - traslados = append(traslados, newTraslado(rate, currency)) - } + imp := &Impuesto{ + Base: rate.Base.Rescale(cu).String(), + Importe: rate.Amount.Rescale(cu).String(), + Impuesto: catDef.Map[mx.KeySATImpuesto].String(), + TasaOCuota: formatTaxPercent(rate.Percent), + TipoFactor: TipoFactorTasa, } - return &Traslados{traslados} + return imp } -func newTrasladosFromLine(line *bill.Line) *Traslados { - var traslados []*Traslado +func newConceptoImpuestos(line *bill.Line, regime *tax.Regime) *ConceptoImpuestos { + var traslados, retenciones []*Impuesto for _, tax := range line.Taxes { - if tax.Category != common.TaxCategoryVAT { - continue - } + catDef := regime.Category(tax.Category) + imp := newConceptoImpuesto(line, tax, catDef) - traslados = append(traslados, newTrasladoFromLineTax(line, tax)) + if catDef.Retained { + retenciones = append(retenciones, imp) + } else { + traslados = append(traslados, imp) + } } - return &Traslados{traslados} -} + impuestos := &ConceptoImpuestos{} -func newTraslado(rate *tax.RateTotal, currency *currency.Code) *Traslado { - cu := currency.Def().Units // SAT expects tax total amounts with no more decimals than supported by the currency + if len(traslados) > 0 { + impuestos.Traslados = &Traslados{traslados} + } - traslado := &Traslado{ - Base: rate.Base.Rescale(cu).String(), - Importe: rate.Amount.Rescale(cu).String(), - Impuesto: ImpuestoIVA, - TasaOCuota: formatTaxPercent(rate.Percent), - TipoFactor: TipoFactorTasa, + if len(retenciones) > 0 { + impuestos.Retenciones = &Retenciones{retenciones} } - return traslado + return impuestos } -func newTrasladoFromLineTax(line *bill.Line, tax *tax.Combo) *Traslado { +func newConceptoImpuesto(line *bill.Line, tax *tax.Combo, catDef *tax.Category) *Impuesto { // GOBL doesn't provide an amount at line level, so we calculate it taxAmount := tax.Percent.Of(line.Total) - traslado := &Traslado{ + i := &Impuesto{ Base: line.Total.String(), Importe: taxAmount.String(), - Impuesto: ImpuestoIVA, + Impuesto: catDef.Map[mx.KeySATImpuesto].String(), TasaOCuota: formatTaxPercent(tax.Percent), TipoFactor: TipoFactorTasa, } - return traslado + return i } func formatTaxPercent(percent *num.Percentage) string { diff --git a/test/data/invoice.json b/test/data/invoice.json index 12ebe65..d742690 100644 --- a/test/data/invoice.json +++ b/test/data/invoice.json @@ -4,17 +4,17 @@ "uuid": "c4ed7c55-fef6-11ed-98ea-e6a7901137ed", "dig": { "alg": "sha256", - "val": "eb37a935da73a6a47940425492bd00b6df54f61d8a2df4848528c2cb38520fff" + "val": "fb0df5bea43b678ae4966c14dfd25998e3be7787e61c7351ff007d0b954c4bef" }, "draft": true }, "doc": { "$schema": "https://gobl.org/draft-0/bill/invoice", + "type": "standard", "series": "LMC", "code": "0010", - "type": "standard", - "currency": "MXN", "issue_date": "2023-05-29", + "currency": "MXN", "supplier": { "name": "ESCUELA KEMPER URGATE", "tax_id": { @@ -44,11 +44,11 @@ "quantity": "2", "item": { "name": "Cigarros", + "price": "200.2020", + "unit": "piece", "ext": { "mx-cfdi-prod-serv": "50211502" - }, - "price": "200.2020", - "unit": "piece" + } }, "sum": "400.4040", "discounts": [ @@ -65,6 +65,14 @@ "cat": "VAT", "rate": "standard", "percent": "16.0%" + }, + { + "cat": "RVAT", + "percent": "10.6667%" + }, + { + "cat": "ISR", + "percent": "10%" } ], "total": "200.2020" @@ -79,7 +87,7 @@ } }, "totals": { - "sum": "200.2020", + "sum": "200.20", "total": "200.20", "taxes": { "categories": [ @@ -88,19 +96,43 @@ "rates": [ { "key": "standard", - "base": "200.2020", + "base": "200.20", "percent": "16.0%", - "amount": "32.0323" + "amount": "32.03" + } + ], + "amount": "32.03" + }, + { + "code": "RVAT", + "retained": true, + "rates": [ + { + "base": "200.20", + "percent": "10.6667%", + "amount": "21.35" + } + ], + "amount": "21.35" + }, + { + "code": "ISR", + "retained": true, + "rates": [ + { + "base": "200.20", + "percent": "10%", + "amount": "20.02" } ], - "amount": "32.0323" + "amount": "20.02" } ], - "sum": "32.0323" + "sum": "-9.34" }, - "tax": "32.03", - "total_with_tax": "232.23", - "payable": "232.23" + "tax": "-9.34", + "total_with_tax": "190.86", + "payable": "190.86" } } } diff --git a/test/data/out/invoice.xml b/test/data/out/invoice.xml index c7ef357..4c60325 100644 --- a/test/data/out/invoice.xml +++ b/test/data/out/invoice.xml @@ -1,5 +1,5 @@ - + @@ -8,10 +8,18 @@ + + + + - + + + + + From 1564b85435299e6524c350e7600130eefb32ae26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Fri, 17 Nov 2023 15:50:23 +0000 Subject: [PATCH 02/19] Validate using local copies of schemas --- test/schema/cfdv40.xsd | 850 ++++++++++++++++++++++++++++++++ test/schema/ecc12.xsd | 261 ++++++++++ test/schema/schema.xsd | 6 +- test/schema/valesdedespensa.xsd | 179 +++++++ 4 files changed, 1293 insertions(+), 3 deletions(-) create mode 100644 test/schema/cfdv40.xsd create mode 100644 test/schema/ecc12.xsd create mode 100644 test/schema/valesdedespensa.xsd diff --git a/test/schema/cfdv40.xsd b/test/schema/cfdv40.xsd new file mode 100644 index 0000000..736bc05 --- /dev/null +++ b/test/schema/cfdv40.xsd @@ -0,0 +1,850 @@ + + + + + + + Estándar de Comprobante Fiscal Digital por Internet. + + + + + + Nodo condicional para precisar la información relacionada con el comprobante global. + + + + + Atributo requerido para expresar el período al que corresponde la información del comprobante global. + + + + + Atributo requerido para expresar el mes o los meses al que corresponde la información del comprobante global. + + + + + Atributo requerido para expresar el año al que corresponde la información del comprobante global. + + + + + + + + + + + + + Nodo opcional para precisar la información de los comprobantes relacionados. + + + + + + Nodo requerido para precisar la información de los comprobantes relacionados. + + + + + Atributo requerido para registrar el folio fiscal (UUID) de un CFDI relacionado con el presente comprobante, por ejemplo: Si el CFDI relacionado es un comprobante de traslado que sirve para registrar el movimiento de la mercancía. Si este comprobante se usa como nota de crédito o nota de débito del comprobante relacionado. Si este comprobante es una devolución sobre el comprobante relacionado. Si éste sustituye a una factura cancelada. + + + + + + + + + + + + + + + Atributo requerido para indicar la clave de la relación que existe entre éste que se está generando y el o los CFDI previos. + + + + + + + Nodo requerido para expresar la información del contribuyente emisor del comprobante. + + + + + Atributo requerido para registrar la Clave del Registro Federal de Contribuyentes correspondiente al contribuyente emisor del comprobante. + + + + + Atributo requerido para registrar el nombre, denominación o razón social del contribuyente inscrito en el RFC, del emisor del comprobante. + + + + + + + + + + + + + Atributo requerido para incorporar la clave del régimen del contribuyente emisor al que aplicará el efecto fiscal de este comprobante. + + + + + Atributo condicional para expresar el número de operación proporcionado por el SAT cuando se trate de un comprobante a través de un PCECFDI o un PCGCFDISP. + + + + + + + + + + + + + + Nodo requerido para precisar la información del contribuyente receptor del comprobante. + + + + + Atributo requerido para registrar la Clave del Registro Federal de Contribuyentes correspondiente al contribuyente receptor del comprobante. + + + + + Atributo requerido para registrar el nombre(s), primer apellido, segundo apellido, según corresponda, denominación o razón social del contribuyente, inscrito en el RFC, del receptor del comprobante. + + + + + + + + + + + + + Atributo requerido para registrar el código postal del domicilio fiscal del receptor del comprobante. + + + + + + + + + + + + Atributo condicional para registrar la clave del país de residencia para efectos fiscales del receptor del comprobante, cuando se trate de un extranjero, y que es conforme con la especificación ISO 3166-1 alpha-3. Es requerido cuando se incluya el complemento de comercio exterior o se registre el atributo NumRegIdTrib. + + + + + Atributo condicional para expresar el número de registro de identidad fiscal del receptor cuando sea residente en el extranjero. Es requerido cuando se incluya el complemento de comercio exterior. + + + + + + + + + + + + Atributo requerido para incorporar la clave del régimen fiscal del contribuyente receptor al que aplicará el efecto fiscal de este comprobante. + + + + + Atributo requerido para expresar la clave del uso que dará a esta factura el receptor del CFDI. + + + + + + + Nodo requerido para listar los conceptos cubiertos por el comprobante. + + + + + + Nodo requerido para registrar la información detallada de un bien o servicio amparado en el comprobante. + + + + + + Nodo condicional para capturar los impuestos aplicables al presente concepto. + + + + + + Nodo opcional para asentar los impuestos trasladados aplicables al presente concepto. + + + + + + Nodo requerido para asentar la información detallada de un traslado de impuestos aplicable al presente concepto. + + + + + Atributo requerido para señalar la base para el cálculo del impuesto, la determinación de la base se realiza de acuerdo con las disposiciones fiscales vigentes. No se permiten valores negativos. + + + + + + + + + + + + Atributo requerido para señalar la clave del tipo de impuesto trasladado aplicable al concepto. + + + + + Atributo requerido para señalar la clave del tipo de factor que se aplica a la base del impuesto. + + + + + Atributo condicional para señalar el valor de la tasa o cuota del impuesto que se traslada para el presente concepto. Es requerido cuando el atributo TipoFactor tenga una clave que corresponda a Tasa o Cuota. + + + + + + + + + + + + Atributo condicional para señalar el importe del impuesto trasladado que aplica al concepto. No se permiten valores negativos. Es requerido cuando TipoFactor sea Tasa o Cuota. + + + + + + + + + + Nodo opcional para asentar los impuestos retenidos aplicables al presente concepto. + + + + + + Nodo requerido para asentar la información detallada de una retención de impuestos aplicable al presente concepto. + + + + + Atributo requerido para señalar la base para el cálculo de la retención, la determinación de la base se realiza de acuerdo con las disposiciones fiscales vigentes. No se permiten valores negativos. + + + + + + + + + + + + Atributo requerido para señalar la clave del tipo de impuesto retenido aplicable al concepto. + + + + + Atributo requerido para señalar la clave del tipo de factor que se aplica a la base del impuesto. + + + + + Atributo requerido para señalar la tasa o cuota del impuesto que se retiene para el presente concepto. + + + + + + + + + + + + Atributo requerido para señalar el importe del impuesto retenido que aplica al concepto. No se permiten valores negativos. + + + + + + + + + + + + + Nodo opcional para registrar información del contribuyente Tercero, a cuenta del que se realiza la operación. + + + + + Atributo requerido para registrar la Clave del Registro Federal de Contribuyentes del contribuyente Tercero, a cuenta del que se realiza la operación. + + + + + Atributo requerido para registrar el nombre, denominación o razón social del contribuyente Tercero correspondiente con el Rfc, a cuenta del que se realiza la operación. + + + + + + + + + + + + + Atributo requerido para incorporar la clave del régimen del contribuyente Tercero, a cuenta del que se realiza la operación. + + + + + Atributo requerido para incorporar el código postal del domicilio fiscal del Tercero, a cuenta del que se realiza la operación. + + + + + + + + + + + + + + Nodo opcional para introducir la información aduanera aplicable cuando se trate de ventas de primera mano de mercancías importadas o se trate de operaciones de comercio exterior con bienes o servicios. + + + + + Atributo requerido para expresar el número del pedimento que ampara la importación del bien que se expresa en el siguiente formato: últimos 2 dígitos del año de validación seguidos por dos espacios, 2 dígitos de la aduana de despacho seguidos por dos espacios, 4 dígitos del número de la patente seguidos por dos espacios, 1 dígito que corresponde al último dígito del año en curso, salvo que se trate de un pedimento consolidado iniciado en el año inmediato anterior o del pedimento original de una rectificación, seguido de 6 dígitos de la numeración progresiva por aduana. + + + + + + + + + + + + + Nodo opcional para asentar el número de cuenta predial con el que fue registrado el inmueble, en el sistema catastral de la entidad federativa de que trate, o bien para incorporar los datos de identificación del certificado de participación inmobiliaria no amortizable. + + + + + Atributo requerido para precisar el número de la cuenta predial del inmueble cubierto por el presente concepto, o bien para incorporar los datos de identificación del certificado de participación inmobiliaria no amortizable, tratándose de arrendamiento. + + + + + + + + + + + + + + + Nodo opcional donde se incluyen los nodos complementarios de extensión al concepto definidos por el SAT, de acuerdo con las disposiciones particulares para un sector o actividad específica. + + + + + + + + + + Nodo opcional para expresar las partes o componentes que integran la totalidad del concepto expresado en el comprobante fiscal digital por Internet. + + + + + + Nodo opcional para introducir la información aduanera aplicable cuando se trate de ventas de primera mano de mercancías importadas o se trate de operaciones de comercio exterior con bienes o servicios. + + + + + Atributo requerido para expresar el número del pedimento que ampara la importación del bien que se expresa en el siguiente formato: últimos 2 dígitos del año de validación seguidos por dos espacios, 2 dígitos de la aduana de despacho seguidos por dos espacios, 4 dígitos del número de la patente seguidos por dos espacios, 1 dígito que corresponde al último dígito del año en curso, salvo que se trate de un pedimento consolidado iniciado en el año inmediato anterior o del pedimento original de una rectificación, seguido de 6 dígitos de la numeración progresiva por aduana. + + + + + + + + + + + + + + Atributo requerido para expresar la clave del producto o del servicio amparado por la presente parte. Es requerido y deben utilizar las claves del catálogo de productos y servicios, cuando los conceptos que registren por sus actividades correspondan con dichos conceptos. + + + + + Atributo opcional para expresar el número de serie, número de parte del bien o identificador del producto o del servicio amparado por la presente parte. Opcionalmente se puede utilizar claves del estándar GTIN. + + + + + + + + + + + + + Atributo requerido para precisar la cantidad de bienes o servicios del tipo particular definido por la presente parte. + + + + + + + + + + + + Atributo opcional para precisar la unidad de medida propia de la operación del emisor, aplicable para la cantidad expresada en la parte. La unidad debe corresponder con la descripción de la parte. + + + + + + + + + + + + + Atributo requerido para precisar la descripción del bien o servicio cubierto por la presente parte. + + + + + + + + + + + + + Atributo opcional para precisar el valor o precio unitario del bien o servicio cubierto por la presente parte. No se permiten valores negativos. + + + + + Atributo opcional para precisar el importe total de los bienes o servicios de la presente parte. Debe ser equivalente al resultado de multiplicar la cantidad por el valor unitario expresado en la parte. No se permiten valores negativos. + + + + + + + + Atributo requerido para expresar la clave del producto o del servicio amparado por el presente concepto. Es requerido y deben utilizar las claves del catálogo de productos y servicios, cuando los conceptos que registren por sus actividades correspondan con dichos conceptos. + + + + + Atributo opcional para expresar el número de parte, identificador del producto o del servicio, la clave de producto o servicio, SKU o equivalente, propia de la operación del emisor, amparado por el presente concepto. Opcionalmente se puede utilizar claves del estándar GTIN. + + + + + + + + + + + + + Atributo requerido para precisar la cantidad de bienes o servicios del tipo particular definido por el presente concepto. + + + + + + + + + + + + Atributo requerido para precisar la clave de unidad de medida estandarizada aplicable para la cantidad expresada en el concepto. La unidad debe corresponder con la descripción del concepto. + + + + + Atributo opcional para precisar la unidad de medida propia de la operación del emisor, aplicable para la cantidad expresada en el concepto. La unidad debe corresponder con la descripción del concepto. + + + + + + + + + + + + + Atributo requerido para precisar la descripción del bien o servicio cubierto por el presente concepto. + + + + + + + + + + + + + Atributo requerido para precisar el valor o precio unitario del bien o servicio cubierto por el presente concepto. + + + + + Atributo requerido para precisar el importe total de los bienes o servicios del presente concepto. Debe ser equivalente al resultado de multiplicar la cantidad por el valor unitario expresado en el concepto. No se permiten valores negativos. + + + + + Atributo opcional para representar el importe de los descuentos aplicables al concepto. No se permiten valores negativos. + + + + + Atributo requerido para expresar si la operación comercial es objeto o no de impuesto. + + + + + + + + + + Nodo condicional para expresar el resumen de los impuestos aplicables. + + + + + + Nodo condicional para capturar los impuestos retenidos aplicables. Es requerido cuando en los conceptos se registre algún impuesto retenido. + + + + + + Nodo requerido para la información detallada de una retención de impuesto específico. + + + + + Atributo requerido para señalar la clave del tipo de impuesto retenido. + + + + + Atributo requerido para señalar el monto del impuesto retenido. No se permiten valores negativos. + + + + + + + + + + Nodo condicional para capturar los impuestos trasladados aplicables. Es requerido cuando en los conceptos se registre un impuesto trasladado. + + + + + + Nodo requerido para la información detallada de un traslado de impuesto específico. + + + + + Atributo requerido para señalar la suma de los atributos Base de los conceptos del impuesto trasladado. No se permiten valores negativos. + + + + + Atributo requerido para señalar la clave del tipo de impuesto trasladado. + + + + + Atributo requerido para señalar la clave del tipo de factor que se aplica a la base del impuesto. + + + + + Atributo condicional para señalar el valor de la tasa o cuota del impuesto que se traslada por los conceptos amparados en el comprobante. + + + + + + + + + + + + Atributo condicional para señalar la suma del importe del impuesto trasladado, agrupado por impuesto, TipoFactor y TasaOCuota. No se permiten valores negativos. + + + + + + + + + + + Atributo condicional para expresar el total de los impuestos retenidos que se desprenden de los conceptos expresados en el comprobante fiscal digital por Internet. No se permiten valores negativos. Es requerido cuando en los conceptos se registren impuestos retenidos. + + + + + Atributo condicional para expresar el total de los impuestos trasladados que se desprenden de los conceptos expresados en el comprobante fiscal digital por Internet. No se permiten valores negativos. Es requerido cuando en los conceptos se registren impuestos trasladados. + + + + + + + Nodo opcional donde se incluye el complemento Timbre Fiscal Digital de manera obligatoria y los nodos complementarios determinados por el SAT, de acuerdo con las disposiciones particulares para un sector o actividad específica. + + + + + + + + + + Nodo opcional para recibir las extensiones al presente formato que sean de utilidad al contribuyente. Para las reglas de uso del mismo, referirse al formato origen. + + + + + + + + + + + Atributo requerido con valor prefijado a 4.0 que indica la versión del estándar bajo el que se encuentra expresado el comprobante. + + + + + + + + + + Atributo opcional para precisar la serie para control interno del contribuyente. Este atributo acepta una cadena de caracteres. + + + + + + + + + + + + + Atributo opcional para control interno del contribuyente que expresa el folio del comprobante, acepta una cadena de caracteres. + + + + + + + + + + + + + Atributo requerido para la expresión de la fecha y hora de expedición del Comprobante Fiscal Digital por Internet. Se expresa en la forma AAAA-MM-DDThh:mm:ss y debe corresponder con la hora local donde se expide el comprobante. + + + + + Atributo requerido para contener el sello digital del comprobante fiscal, al que hacen referencia las reglas de resolución miscelánea vigente. El sello debe ser expresado como una cadena de texto en formato Base 64. + + + + + + + + + + Atributo condicional para expresar la clave de la forma de pago de los bienes o servicios amparados por el comprobante. + + + + + Atributo requerido para expresar el número de serie del certificado de sello digital que ampara al comprobante, de acuerdo con el acuse correspondiente a 20 posiciones otorgado por el sistema del SAT. + + + + + + + + + + + + Atributo requerido que sirve para incorporar el certificado de sello digital que ampara al comprobante, como texto en formato base 64. + + + + + + + + + + Atributo condicional para expresar las condiciones comerciales aplicables para el pago del comprobante fiscal digital por Internet. Este atributo puede ser condicionado mediante atributos o complementos. + + + + + + + + + + + + + Atributo requerido para representar la suma de los importes de los conceptos antes de descuentos e impuesto. No se permiten valores negativos. + + + + + Atributo condicional para representar el importe total de los descuentos aplicables antes de impuestos. No se permiten valores negativos. Se debe registrar cuando existan conceptos con descuento. + + + + + Atributo requerido para identificar la clave de la moneda utilizada para expresar los montos, cuando se usa moneda nacional se registra MXN. Conforme con la especificación ISO 4217. + + + + + Atributo condicional para representar el tipo de cambio FIX conforme con la moneda usada. Es requerido cuando la clave de moneda es distinta de MXN y de XXX. El valor debe reflejar el número de pesos mexicanos que equivalen a una unidad de la divisa señalada en el atributo moneda. Si el valor está fuera del porcentaje aplicable a la moneda tomado del catálogo c_Moneda, el emisor debe obtener del PAC que vaya a timbrar el CFDI, de manera no automática, una clave de confirmación para ratificar que el valor es correcto e integrar dicha clave en el atributo Confirmacion. + + + + + + + + + + + + Atributo requerido para representar la suma del subtotal, menos los descuentos aplicables, más las contribuciones recibidas (impuestos trasladados - federales y/o locales, derechos, productos, aprovechamientos, aportaciones de seguridad social, contribuciones de mejoras) menos los impuestos retenidos federales y/o locales. Si el valor es superior al límite que establezca el SAT en la Resolución Miscelánea Fiscal vigente, el emisor debe obtener del PAC que vaya a timbrar el CFDI, de manera no automática, una clave de confirmación para ratificar que el valor es correcto e integrar dicha clave en el atributo Confirmacion. No se permiten valores negativos. + + + + + Atributo requerido para expresar la clave del efecto del comprobante fiscal para el contribuyente emisor. + + + + + Atributo requerido para expresar si el comprobante ampara una operación de exportación. + + + + + Atributo condicional para precisar la clave del método de pago que aplica para este comprobante fiscal digital por Internet, conforme al Artículo 29-A fracción VII incisos a y b del CFF. + + + + + Atributo requerido para incorporar el código postal del lugar de expedición del comprobante (domicilio de la matriz o de la sucursal). + + + + + Atributo condicional para registrar la clave de confirmación que entregue el PAC para expedir el comprobante con importes grandes, con un tipo de cambio fuera del rango establecido o con ambos casos. Es requerido cuando se registra un tipo de cambio o un total fuera del rango establecido. + + + + + + + + + + + + diff --git a/test/schema/ecc12.xsd b/test/schema/ecc12.xsd new file mode 100644 index 0000000..2bad1f8 --- /dev/null +++ b/test/schema/ecc12.xsd @@ -0,0 +1,261 @@ + + + + + + + Complemento para el Comprobante Fiscal Digital por Internet (CFDI) para integrar la información aplicable al estado de cuenta emitido por un prestador de servicios de monedero electrónico + + + + + + Nodo requerido para enlistar los conceptos cubiertos por Estado de Cuenta de Combustible. + + + + + + Nodo requerido para la expresión de una transacción a ser reportada en el estado de cuenta del proveedor de monedero electrónico para operaciones de compra de combustibles. + + + + + + Nodo requerido para enlistar los impuestos trasladados aplicables de combustibles. + + + + + + Nodo para la definición de información detallada de un traslado de impuesto específico. + + + + + Atributo requerido para definir el tipo de impuesto trasladado. + + + + + + + Impuesto al Valor Agregado + + + + + Impuesto especial sobre productos y servicios + + + + + + + + Atributo requerido para señalar la tasa o la cuota del impuesto que se traslada por cada concepto amparado en el comprobante. Cuando se registre un porcentaje, por ejemplo 16%, debe expresarse como 0.16 y no como 16.00 + + + + + + + + + + + Atributo requerido para definir el importe o monto del impuesto trasladado. + + + + + + + + + + + + + + + + + + + Atributo requerido para la expresión del identificador o número del monedero electrónico. + + + + + + + + + + + Atributo requerido para la expresión de la Fecha y hora de expedición de la operación reportada. Se expresa en la forma aaaa-mm-ddThh:mm:ss, de acuerdo con la especificación ISO 8601. + + + + + + Atributo requerido del RFC del enajenante del combustible. + + + + + Atributo requerido para expresar la clave de cliente de la estación de servicio, a 10 caracteres. + + + + + + + + + + + + Atributo requerido para definir el volumen de combustible adquirido. + + + + + + + + + + + + + Atributo requerido para indicar la clave del tipo de combustible. + + + + + Atributo condicional para precisar la unidad de medida. + + + + + + + + + + + + Atributo requerido para expresar el nombre del combustible adquirido. + + + + + + + + + + + + Atributo requerido para referir el número de folio de cada operación realizada por cada monedero electrónico. + + + + + + + + + + + + Atributo requerido para definir el precio unitario del combustible adquirido. + + + + + + + + + + + + + Atributo requerido para definir el monto total de consumo de combustible. Debe ser equivalente al resultado de multiplicar la cantidad por el valor unitario, redondeado a centésimas. + + + + + + + + + + + + + + + + + + Atributo requerido que indica la versión del complemento. + + + + + + + + + + Atributo requerido para expresar el tipo de operación de acuerdo con el medio de pago. + + + + + + + + + + Atributo requerido para expresar el número de cuenta del adquirente del monedero electrónico + + + + + + + + + + + + Atributo requerido para representar la suma de todos los importes tipo ConceptoEstadoDeCuentaCombustible. + + + + + + + + + + + + Atributo requerido para expresar el monto total de consumo de combustible. + + + + + + + + + + + + diff --git a/test/schema/schema.xsd b/test/schema/schema.xsd index 52649f6..df3d19f 100644 --- a/test/schema/schema.xsd +++ b/test/schema/schema.xsd @@ -1,7 +1,7 @@ - - - + + + diff --git a/test/schema/valesdedespensa.xsd b/test/schema/valesdedespensa.xsd new file mode 100644 index 0000000..5aa4aec --- /dev/null +++ b/test/schema/valesdedespensa.xsd @@ -0,0 +1,179 @@ + + + + + + Complemento al Comprobante Fiscal Digital por Internet (CFDI) para integrar la información emitida por un prestador de servicios de monedero electrónico de vales de despensa. + + + + + + Nodo requerido para enlistar los conceptos cubiertos por los monederos electrónicos de vales de despensa. + + + + + + + Nodo requerido para la expresión de una transacción a ser reportada por el proveedor del monedero electrónico de vales de despensa. + + + + + + Atributo requerido para expresar el identificador o numero del monedero electrónico. + + + + + + + + + + + + + Atributo requerido para la expresión de la Fecha y hora de expedición de la operación reportada. Se expresa en la forma aaaa-mm-ddThh:mm:ss, de acuerdo con la especificación ISO 8601. + + + + + + + + + + + Atributo requerido para la expresión del Registro Federal de Contribuyentes del trabajador al que se le otorgó el monedero electrónico sin guiones o espacios + + + + + + Atributo requerido para la expresión de la CURP del trabajador al que se le otorgó el monedero electrónico. + + + + + Atributo requerido para la expresión del Nombre del trabajador al que se le otorgó el monedero electrónico sin guiones o espacios + + + + + + + + + + + + + Atributo opcional para la expresión del numero de seguridad social aplicable al trabajador. + + + + + + + + + + + + + Atributo requerido para expresar el importe del depósito efectuado al trabajador en el monedero electrónico. + + + + + + + + + + + + + + + + + + Atributo requerido con valor prefijado a 1.0 que indica la versión del estándar bajo el que se encuentra expresado el comprobante. + + + + + + Atributo requerido para expresar el tipo de operación de acuerdo con el medio de pago. + + + + + + + + + + + + Atributo opcional para expresar el registro patronal del adquirente del monedero electrónico. + + + + + + + + + + + + Atributo requerido para expresar el numero de cuenta del adquiriente del monedero electrónico. + + + + + + + + + + + + Atributo requerido para expresar el monto total de vales de despensa otorgados. + + + + + + + + + + + + + + Tipo definido para expresar claves del Registro Federal de Contribuyentes + + + + + + + + + + + Tipo definido para la expresión de una CURP + + + + + + + + From 0d809c81aec6d9eb5a498e3a828dd8666151bb60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Fri, 17 Nov 2023 15:55:28 +0000 Subject: [PATCH 03/19] Prevent empty "Complemento" tag from rendering --- cfdi.go | 6 +++++- food_vouchers.go | 2 +- food_vouchers_test.go | 2 +- fuel_account_balance.go | 2 +- fuel_account_balance_test.go | 2 +- test/data/out/bare-minimum-invoice.xml | 1 - test/data/out/credit-note.xml | 1 - test/data/out/invoice.xml | 1 - 8 files changed, 9 insertions(+), 8 deletions(-) diff --git a/cfdi.go b/cfdi.go index 2815067..a9da3fb 100644 --- a/cfdi.go +++ b/cfdi.go @@ -67,7 +67,11 @@ type Document struct { Conceptos *Conceptos `xml:"cfdi:Conceptos"` //nolint:misspell Impuestos *Impuestos `xml:"cfdi:Impuestos,omitempty"` - Complementos []interface{} `xml:"cfdi:Complemento>*,omitempty"` + Complementos []*ContentWrapper `xml:"cfdi:Complemento,omitempty"` + +// ContentWrapper is a struct necessary to wrap any arbitrary XML content within another tag. +type ContentWrapper struct { + Content interface{} `xml:",any"` } // NewDocument converts a GOBL envelope into a CFDI document diff --git a/food_vouchers.go b/food_vouchers.go index 0c4756c..bbe6c55 100644 --- a/food_vouchers.go +++ b/food_vouchers.go @@ -52,7 +52,7 @@ func addValesDeDespensa(doc *Document, fvc *mx.FoodVouchers) { doc.VDNamespace = VDNamespace doc.SchemaLocation = doc.SchemaLocation + " " + formatSchemaLocation(VDNamespace, VDSchemaLocation) - doc.Complementos = append(doc.Complementos, vd) + doc.Complementos = append(doc.Complementos, &ContentWrapper{vd}) } func newVDConceptos(lines []*mx.FoodVouchersLine) []*VDConcepto { diff --git a/food_vouchers_test.go b/food_vouchers_test.go index 2e8c113..0c4c585 100644 --- a/food_vouchers_test.go +++ b/food_vouchers_test.go @@ -16,7 +16,7 @@ func TestValesDeDespensa(t *testing.T) { require.Equal(t, 1, len(doc.Complementos)) - vd := doc.Complementos[0].(*cfdi.ValesDeDespensa) + vd := doc.Complementos[0].Content.(*cfdi.ValesDeDespensa) assert.Equal(t, "12345678901234567890", vd.RegistroPatronal) assert.Equal(t, "0123456789", vd.NumeroDeCuenta) diff --git a/fuel_account_balance.go b/fuel_account_balance.go index 6f7c801..aaf9bcc 100644 --- a/fuel_account_balance.go +++ b/fuel_account_balance.go @@ -65,7 +65,7 @@ func addEstadoCuentaCombustible(doc *Document, fc *mx.FuelAccountBalance) { doc.ECCNamespace = ECCNamespace doc.SchemaLocation = doc.SchemaLocation + " " + formatSchemaLocation(ECCNamespace, ECCSchemaLocation) - doc.Complementos = append(doc.Complementos, ecc) + doc.Complementos = append(doc.Complementos, &ContentWrapper{ecc}) } // nolint:misspell diff --git a/fuel_account_balance_test.go b/fuel_account_balance_test.go index d9be309..16ff2fe 100644 --- a/fuel_account_balance_test.go +++ b/fuel_account_balance_test.go @@ -16,7 +16,7 @@ func TestEstadoDeCuentaCombustible(t *testing.T) { require.Equal(t, 1, len(doc.Complementos)) - ecc := doc.Complementos[0].(*cfdi.EstadoDeCuentaCombustible) + ecc := doc.Complementos[0].Content.(*cfdi.EstadoDeCuentaCombustible) assert.Equal(t, "0123456789", ecc.NumeroDeCuenta) assert.Equal(t, "246.13", ecc.SubTotal) diff --git a/test/data/out/bare-minimum-invoice.xml b/test/data/out/bare-minimum-invoice.xml index 3b873ec..fba0f75 100644 --- a/test/data/out/bare-minimum-invoice.xml +++ b/test/data/out/bare-minimum-invoice.xml @@ -16,5 +16,4 @@ - \ No newline at end of file diff --git a/test/data/out/credit-note.xml b/test/data/out/credit-note.xml index 422a369..b825e7d 100644 --- a/test/data/out/credit-note.xml +++ b/test/data/out/credit-note.xml @@ -26,5 +26,4 @@ - \ No newline at end of file diff --git a/test/data/out/invoice.xml b/test/data/out/invoice.xml index 4c60325..fe598a8 100644 --- a/test/data/out/invoice.xml +++ b/test/data/out/invoice.xml @@ -24,5 +24,4 @@ - \ No newline at end of file From 03986e27a35b571ce66bf361bce426273f58772d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Fri, 17 Nov 2023 16:08:56 +0000 Subject: [PATCH 04/19] Make validation errors more informative --- examples_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_test.go b/examples_test.go index 7f48481..899a3b0 100644 --- a/examples_test.go +++ b/examples_test.go @@ -18,7 +18,6 @@ import ( 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) @@ -40,6 +39,7 @@ func TestXMLGeneration(t *testing.T) { assert.NoError(t, e) } if len(errs) > 0 { + assert.Fail(t, "Invalid XML:\n"+string(data)) return } From 4018d43d9bb11f2af8b6d50e41c5ac9a53283163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Tue, 21 Nov 2023 11:21:56 +0000 Subject: [PATCH 05/19] Add support for Addenda Mabe --- cfdi.go | 17 ++ go.mod | 2 +- go.sum | 10 +- mabe.go | 256 ++++++++++++++++++++ test/data/addenda-mabe.json | 155 ++++++++++++ test/data/bare-minimum-addenda-mabe.json | 109 +++++++++ test/data/out/addenda-mabe.xml | 47 ++++ test/data/out/bare-minimum-addenda-mabe.xml | 34 +++ test/schema/mabev1.xsd | 252 +++++++++++++++++++ test/schema/schema.xsd | 1 + 10 files changed, 874 insertions(+), 9 deletions(-) create mode 100644 mabe.go create mode 100644 test/data/addenda-mabe.json create mode 100644 test/data/bare-minimum-addenda-mabe.json create mode 100644 test/data/out/addenda-mabe.xml create mode 100644 test/data/out/bare-minimum-addenda-mabe.xml create mode 100644 test/schema/mabev1.xsd diff --git a/cfdi.go b/cfdi.go index a9da3fb..adccccf 100644 --- a/cfdi.go +++ b/cfdi.go @@ -68,6 +68,8 @@ type Document struct { Impuestos *Impuestos `xml:"cfdi:Impuestos,omitempty"` Complementos []*ContentWrapper `xml:"cfdi:Complemento,omitempty"` + Addendas []*ContentWrapper `xml:"cfdi:Addenda,omitempty"` +} // ContentWrapper is a struct necessary to wrap any arbitrary XML content within another tag. type ContentWrapper struct { @@ -117,6 +119,10 @@ func NewDocument(env *gobl.Envelope) (*Document, error) { return nil, err } + if err := addAddendas(document, inv); err != nil { + return nil, err + } + return document, nil } @@ -145,6 +151,17 @@ func addComplementos(doc *Document, complements []*schema.Object) error { return nil } +func addAddendas(doc *Document, inv *bill.Invoice) error { + if mx.IsMabeSupplier(inv) { + err := addAddendaMabe(doc, inv) + if err != nil { + return err + } + } + + return nil +} + func formatIssueDate(date cal.Date) string { dateTime := civil.DateTime{Date: date.Date, Time: civil.Time{}} return dateTime.String() diff --git a/go.mod b/go.mod index 2d06e64..189a989 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/invopop/gobl.cfdi go 1.20 require ( - github.com/invopop/gobl v0.62.1-0.20231117145806-7750c374486b + github.com/invopop/gobl v0.62.1-0.20231121111925-64c5be7b0226 github.com/joho/godotenv v1.5.1 github.com/magefile/mage v1.15.0 github.com/spf13/cobra v1.7.0 diff --git a/go.sum b/go.sum index 427b427..8bdc902 100644 --- a/go.sum +++ b/go.sum @@ -19,14 +19,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.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.60.0 h1:BRnpt3FzOmIpCDstJfQLaapOCLhBeqIjpsdsgQTtaB4= -github.com/invopop/gobl v0.60.0/go.mod h1:kt3cQtFSOhPCYVlgiaRI267syjI+X1VRW7QHTmitc+Q= -github.com/invopop/gobl v0.61.0 h1:gFLX/VTCrn3BH5QMk7mR58lCTPIV1EDJdXEni3Zi5+g= -github.com/invopop/gobl v0.61.0/go.mod h1:kt3cQtFSOhPCYVlgiaRI267syjI+X1VRW7QHTmitc+Q= -github.com/invopop/gobl v0.62.1-0.20231117145806-7750c374486b h1:Q0150tTInews6yZ33h16YevRyH7lPz8g+8MMkftyBN4= -github.com/invopop/gobl v0.62.1-0.20231117145806-7750c374486b/go.mod h1:sEngvTr2gAxexosO0rmQInVSL8C613TUPvBcFT4xMyM= -github.com/invopop/jsonschema v0.9.0 h1:m1Fe5PN4X9V7P1TCF+pA8Xly3Vj3pY905klC++8oOpM= -github.com/invopop/jsonschema v0.9.0/go.mod h1:uMhbTEOXoPcOKzdYRfk914W6UTGA/cVcgEQxXh1MJ7g= +github.com/invopop/gobl v0.62.1-0.20231121111925-64c5be7b0226 h1:Qca7HXgrRZOLuu++CdcFmlMWvLumkdiLR8/YfzjKuTE= +github.com/invopop/gobl v0.62.1-0.20231121111925-64c5be7b0226/go.mod h1:sEngvTr2gAxexosO0rmQInVSL8C613TUPvBcFT4xMyM= 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.3.0 h1:o260kbjXzoBO/ypXDSSrCLL7SxEFUXBsX09YTE9AxZw= diff --git a/mabe.go b/mabe.go new file mode 100644 index 0000000..6eb54e4 --- /dev/null +++ b/mabe.go @@ -0,0 +1,256 @@ +package cfdi + +import ( + "encoding/xml" + "fmt" + + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/num" + "github.com/invopop/gobl/regimes/mx" +) + +// Mabe schema constants +const ( + MabeVersion = "1.0" + MabeNamespace = "https://recepcionfe.mabempresa.com/cfd/addenda/v1" + MabeSchemaLocation = "https://recepcionfe.mabempresa.com/cfd/addenda/v1/mabev1.xsd" + MabeNotApplicable = "NA" +) + +// TipoDocumento valid values +const ( + MabeTipoDocumentoFactura = "FACTURA" + MabeTipoDocumentoNotaCredito = "NOTA CREDITO" + MabeTipoDocumentoNotaCargo = "NOTA CARGO" +) + +// MabeFactura is the root element of the Mabe addendum +type MabeFactura struct { + XMLName xml.Name `xml:"mabe:Factura"` + Namespace string `xml:"xmlns:mabe,attr"` + SchemaLocation string `xml:"xsi:schemaLocation,attr"` + + Version string `xml:"version,attr"` + TipoDocumento string `xml:"tipoDocumento,attr"` + Folio string `xml:"folio,attr"` + Fecha string `xml:"fecha,attr"` + OrdenCompra string `xml:"ordenCompra,attr"` + Referencia1 string `xml:"referencia1,attr"` + Referencia2 string `xml:"referencia2,attr,omitempty"` + + Moneda *MabeMoneda `xml:"mabe:Moneda"` + Proveedor *MabeProveedor `xml:"mabe:Proveedor"` + Entrega *MabeEntrega `xml:"mabe:Entrega"` + Detalles *[]*MabeDetalle `xml:"mabe:Detalles>mabe:Detalle"` + + Descuentos *MabeDescuentos `xml:"mabe:Descuentos,omitempty"` + Subtotal *MabeImporte `xml:"mabe:Subtotal"` + Traslados *[]*MabeImpuesto `xml:"mabe:Traslados>mabe:Traslado"` + Retenciones *[]*MabeImpuesto `xml:"mabe:Retenciones>mabe:Retencion"` + Total *MabeImporte `xml:"mabe:Total"` +} + +// MabeMoneda carries the data about the invoice's currency +type MabeMoneda struct { + TipoMoneda string `xml:"tipoMoneda,attr"` + TipoCambio string `xml:"tipoCambio,attr,omitempty"` // Not implemented yet + ImporteConLetra string `xml:"importeConLetra,attr,omitempty"` // Not implemented yet +} + +// MabeProveedor carries the data about the invoice's supplier +type MabeProveedor struct { + Codigo string `xml:"codigo,attr"` +} + +// MabeEntrega carries the data about the invoice's delivery +type MabeEntrega struct { + PlantaEntrega string `xml:"plantaEntrega,attr"` + Calle string `xml:"calle,attr,omitempty"` + NoExterior string `xml:"noExterior,attr,omitempty"` + NoInterior string `xml:"noInterior,attr,omitempty"` + CodigoPostal string `xml:"codigoPostal,attr,omitempty"` +} + +// MabeDetalle carries the data about one invoice's line +type MabeDetalle struct { + NoLineaArticulo int `xml:"noLineaArticulo,attr"` + CodigoArticulo string `xml:"codigoArticulo,attr"` + Descripcion string `xml:"descripcion,attr"` //nolint:misspell + Unidad string `xml:"unidad,attr"` + Cantidad string `xml:"cantidad,attr"` + PrecioSinIva string `xml:"precioSinIva,attr"` + ImporteSinIva string `xml:"importeSinIva,attr"` + PrecioConIva string `xml:"precioConIva,attr,omitempty"` // Not implemented yet + ImporteConIva string `xml:"importeConIva,attr,omitempty"` // Not implemented yet +} + +// MabeImporte carries the data about an invoice's total +type MabeImporte struct { + Importe string `xml:"importe,attr"` +} + +// MabeImpuesto carries the data about an invoice's tax +type MabeImpuesto struct { + Tipo string `xml:"tipo,attr"` + Tasa string `xml:"tasa,attr"` + Importe string `xml:"importe,attr"` +} + +// MabeDescuentos carries the data about an invoice's discount +type MabeDescuentos struct { + Tipo string `xml:"tipo,attr"` + Descripcion string `xml:"descripcion,attr"` //nolint:misspell + Importe string `xml:"importe,attr"` +} + +func addAddendaMabe(doc *Document, inv *bill.Invoice) error { + tipoDocumento, err := mapMabeTipoDocumento(inv) + if err != nil { + return err + } + + f := &MabeFactura{ + Namespace: MabeNamespace, + SchemaLocation: formatSchemaLocation(MabeNamespace, MabeSchemaLocation), + + Version: MabeVersion, + TipoDocumento: tipoDocumento, + Folio: formatMabeFolio(inv), + Fecha: inv.IssueDate.String(), + OrdenCompra: inv.Ordering.Code, + Referencia1: inv.Ext[mx.ExtKeyMabeReference1].String(), + Referencia2: inv.Ext[mx.ExtKeyMabeReference2].String(), + + Moneda: newMabeMoneda(inv), + Proveedor: newMabeProveedor(inv), + Entrega: newMabeEntrega(inv), + Descuentos: newMabeDescuentos(inv), + Detalles: newMabeDetalles(inv), + + Subtotal: newMabeImporte(inv.Totals.Sum), + Total: newMabeImporte(inv.Totals.TotalWithTax), + } + + setMabeTaxes(inv, f) + + doc.Addendas = append(doc.Addendas, &ContentWrapper{f}) + + return nil +} + +func mapMabeTipoDocumento(inv *bill.Invoice) (string, error) { + switch inv.Type { + case bill.InvoiceTypeStandard: + return MabeTipoDocumentoFactura, nil + case bill.InvoiceTypeCreditNote: + return MabeTipoDocumentoNotaCredito, nil + case bill.InvoiceTypeDebitNote: + return MabeTipoDocumentoNotaCargo, nil + default: + return "", fmt.Errorf("invalid invoice type: %s", inv.Type) + } +} + +func newMabeMoneda(inv *bill.Invoice) *MabeMoneda { + return &MabeMoneda{TipoMoneda: string(inv.Currency)} +} + +func newMabeProveedor(inv *bill.Invoice) *MabeProveedor { + return &MabeProveedor{ + Codigo: string(inv.Supplier.Ext[mx.ExtKeyMabeProviderCode]), + } +} + +func newMabeEntrega(inv *bill.Invoice) *MabeEntrega { + rec := inv.Delivery.Receiver + + e := &MabeEntrega{ + PlantaEntrega: string(rec.Ext[mx.ExtKeyMabeDeliveryPlant]), + } + + if len(rec.Addresses) > 0 { + addr := rec.Addresses[0] + + e.Calle = addr.Street + e.NoExterior = addr.Number + e.NoInterior = MabeNotApplicable + e.CodigoPostal = addr.Code + } + + return e +} + +func newMabeDescuentos(inv *bill.Invoice) *MabeDescuentos { + d := totalInvoiceDiscount(inv) + + if d.IsZero() { + return nil + } + + return &MabeDescuentos{ + Tipo: MabeNotApplicable, + Descripcion: MabeNotApplicable, //nolint:misspell + Importe: d.String(), + } +} + +func newMabeDetalles(inv *bill.Invoice) *[]*MabeDetalle { + var detalles []*MabeDetalle + + for _, line := range inv.Lines { + d := &MabeDetalle{ + NoLineaArticulo: line.Index, + CodigoArticulo: line.Item.Ext[mx.ExtKeyMabeItemCode].String(), + Descripcion: line.Item.Name, //nolint:misspell + Unidad: mapToClaveUnidad(line), + Cantidad: line.Quantity.String(), + PrecioSinIva: line.Item.Price.String(), + ImporteSinIva: line.Sum.String(), + } + + detalles = append(detalles, d) + } + + return &detalles +} + +func newMabeImporte(amount num.Amount) *MabeImporte { + return &MabeImporte{ + Importe: amount.String(), + } +} + +func setMabeTaxes(inv *bill.Invoice, mabe *MabeFactura) { + var traslados, retenciones []*MabeImpuesto + + for _, cat := range inv.Totals.Taxes.Categories { + catDef := inv.TaxRegime().Category(cat.Code) + + for _, rate := range cat.Rates { + t := &MabeImpuesto{ + Tipo: catDef.Name.In(i18n.ES), + Tasa: formatTaxPercent(rate.Percent), + Importe: rate.Amount.String(), + } + + if catDef.Retained { + retenciones = append(retenciones, t) + } else { + traslados = append(traslados, t) + } + } + } + + if len(traslados) > 0 { + mabe.Traslados = &traslados + } + + if len(retenciones) > 0 { + mabe.Retenciones = &retenciones + } +} + +func formatMabeFolio(inv *bill.Invoice) string { + return fmt.Sprintf("%s%s", inv.Series, inv.Code) +} diff --git a/test/data/addenda-mabe.json b/test/data/addenda-mabe.json new file mode 100644 index 0000000..6e752ba --- /dev/null +++ b/test/data/addenda-mabe.json @@ -0,0 +1,155 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "3aa3b38d147021722253109fa8a6a54cb543611b6ed7528cbdaa9030ecb21359" + }, + "draft": true + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "type": "standard", + "series": "LMC", + "code": "0010", + "issue_date": "2023-05-29", + "currency": "MXN", + "supplier": { + "name": "ESCUELA KEMPER URGATE", + "tax_id": { + "country": "MX", + "zone": "26015", + "code": "EKU9003173C9" + }, + "ext": { + "mx-cfdi-fiscal-regime": "601", + "mx-mabe-provider-code": "123456" + } + }, + "customer": { + "name": "UNIVERSIDAD ROBOTICA ESPAÑOLA", + "tax_id": { + "country": "MX", + "zone": "65000", + "code": "URE180429TM6" + }, + "ext": { + "mx-cfdi-fiscal-regime": "601", + "mx-cfdi-use": "G01" + } + }, + "lines": [ + { + "i": 1, + "quantity": "2", + "item": { + "name": "Cigarros", + "price": "100.00", + "ext": { + "mx-cfdi-prod-serv": "50211502", + "mx-mabe-item-code": "CODE123" + } + }, + "sum": "200.00", + "discounts": [ + { + "percent": "10.0%", + "amount": "20.00" + } + ], + "taxes": [ + { + "cat": "VAT", + "rate": "standard", + "percent": "16.0%" + }, + { + "cat": "RVAT", + "percent": "10.6667%" + }, + { + "cat": "ISR", + "percent": "10%" + } + ], + "total": "180.00" + } + ], + "ordering": { + "code": "9100000000" + }, + "payment": { + "instructions": { + "key": "credit-transfer" + } + }, + "delivery": { + "receiver": { + "name": "ESTUFAS 30", + "addresses": [ + { + "street": "Calle 1", + "locality": "Mexico D.F.", + "code": "12345" + } + ], + "ext": { + "mx-mabe-delivery-plant": "S001" + } + } + }, + "totals": { + "sum": "180.00", + "total": "180.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "180.00", + "percent": "16.0%", + "amount": "28.80" + } + ], + "amount": "28.80" + }, + { + "code": "RVAT", + "retained": true, + "rates": [ + { + "base": "180.00", + "percent": "10.6667%", + "amount": "19.20" + } + ], + "amount": "19.20" + }, + { + "code": "ISR", + "retained": true, + "rates": [ + { + "base": "180.00", + "percent": "10%", + "amount": "18.00" + } + ], + "amount": "18.00" + } + ], + "sum": "-8.40" + }, + "tax": "-8.40", + "total_with_tax": "171.60", + "payable": "171.60" + }, + "ext": { + "mx-mabe-reference-1": "123456", + "mx-mabe-reference-2": "789" + } + } +} \ No newline at end of file diff --git a/test/data/bare-minimum-addenda-mabe.json b/test/data/bare-minimum-addenda-mabe.json new file mode 100644 index 0000000..5158a25 --- /dev/null +++ b/test/data/bare-minimum-addenda-mabe.json @@ -0,0 +1,109 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "a8749e521981bc47aa0ff5bc6389e0d809dbe07f8b05c278e7674c3e79b8fe77" + }, + "draft": true + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "type": "standard", + "series": "LMC", + "code": "0010", + "issue_date": "2023-05-29", + "currency": "MXN", + "supplier": { + "name": "ESCUELA KEMPER URGATE", + "tax_id": { + "country": "MX", + "zone": "26015", + "code": "EKU9003173C9" + }, + "ext": { + "mx-cfdi-fiscal-regime": "601", + "mx-mabe-provider-code": "123456" + } + }, + "customer": { + "name": "UNIVERSIDAD ROBOTICA ESPAÑOLA", + "tax_id": { + "country": "MX", + "zone": "65000", + "code": "URE180429TM6" + }, + "ext": { + "mx-cfdi-fiscal-regime": "601", + "mx-cfdi-use": "G01" + } + }, + "lines": [ + { + "i": 1, + "quantity": "2", + "item": { + "name": "Cigarros", + "price": "100.00", + "ext": { + "mx-cfdi-prod-serv": "50211502", + "mx-mabe-item-code": "CODE123" + } + }, + "sum": "200.00", + "taxes": [ + { + "cat": "VAT", + "rate": "standard", + "percent": "16.0%" + } + ], + "total": "200.00" + } + ], + "ordering": { + "code": "91000000" + }, + "payment": { + "instructions": { + "key": "credit-transfer" + } + }, + "delivery": { + "receiver": { + "name": "ESTUFAS 30", + "ext": { + "mx-mabe-delivery-plant": "S001" + } + } + }, + "totals": { + "sum": "200.00", + "total": "200.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "200.00", + "percent": "16.0%", + "amount": "32.00" + } + ], + "amount": "32.00" + } + ], + "sum": "32.00" + }, + "tax": "32.00", + "total_with_tax": "232.00", + "payable": "232.00" + }, + "ext": { + "mx-mabe-reference-1": "900900" + } + } +} diff --git a/test/data/out/addenda-mabe.xml b/test/data/out/addenda-mabe.xml new file mode 100644 index 0000000..603c44f --- /dev/null +++ b/test/data/out/addenda-mabe.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/data/out/bare-minimum-addenda-mabe.xml b/test/data/out/bare-minimum-addenda-mabe.xml new file mode 100644 index 0000000..aa49a7d --- /dev/null +++ b/test/data/out/bare-minimum-addenda-mabe.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/schema/mabev1.xsd b/test/schema/mabev1.xsd new file mode 100644 index 0000000..64e1c3f --- /dev/null +++ b/test/schema/mabev1.xsd @@ -0,0 +1,252 @@ + + + + + + + Addenda Mabe v1.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tipo definido para expresar importes numericos con fracción a seis decimales + + + + + + + diff --git a/test/schema/schema.xsd b/test/schema/schema.xsd index df3d19f..98ab699 100644 --- a/test/schema/schema.xsd +++ b/test/schema/schema.xsd @@ -4,4 +4,5 @@ + From 1df6d372e067172cecbe4021e1c2cc098787befa Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Tue, 21 Nov 2023 14:58:08 +0000 Subject: [PATCH 06/19] Moving to addendas packages, using identities instead of ext --- addendas/README.md | 18 +++++++++ addendas/addendas.go | 14 +++++++ mabe.go => addendas/mabe.go | 73 ++++++++++++++++++++++++++++--------- cfdi.go | 27 ++++---------- food_vouchers.go | 3 +- fuel_account_balance.go | 3 +- go.mod | 2 +- go.sum | 2 + internal/calc.go | 24 ++++++++++++ internal/format/format.go | 28 ++++++++++++++ internal/internal.go | 2 + internal/lines.go | 29 +++++++++++++++ lines.go | 38 ++----------------- 13 files changed, 189 insertions(+), 74 deletions(-) create mode 100644 addendas/README.md create mode 100644 addendas/addendas.go rename mabe.go => addendas/mabe.go (80%) create mode 100644 internal/calc.go create mode 100644 internal/format/format.go create mode 100644 internal/internal.go create mode 100644 internal/lines.go diff --git a/addendas/README.md b/addendas/README.md new file mode 100644 index 0000000..5fae09d --- /dev/null +++ b/addendas/README.md @@ -0,0 +1,18 @@ +# CFDI Addendas + +"Addendas" add functionality to regular CFDI documents so that private companies can leverage existing infrastructure around the CFDI format and SAT to extract additional structured data. + +Each of the addendas currently supported are listed below, with instructions on the mappings and key fields. + +## MABE + +Most of the MABE Addenda fields are determined automatically from the base GOBL Invoice, with the exception of the following: + +| MABE Field | GOBL Invoice Value | Description | +| -------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| Order Code | `ordering.code = "-CODE-"` | Provided by Mabe for the order | +| Provider Code | `supplier.identities = [{"type":"MABE", "code":"-CODE-"}]` | Code issued by Mabe to identify the supplier | +| Delivery Plant | `delivery.receiver.identities = [{"type":"MABE-PLANT-ID","code":"-CODE-"}]` | Delivery Plant ID | +| Item Code | `lines[i].item.identities = [{"type":"MABE","code":"-CODE-"}]` | Article code provided by Mabe | +| Reference 1 | `ordering.identities = [{"type":"MABE-REF1","code":"-CODE-"}]` | Additional code required by Mabe in certain circumstances while ordering. | +| Reference 2 | NA | Always empty as not currently used by Mabe. | diff --git a/addendas/addendas.go b/addendas/addendas.go new file mode 100644 index 0000000..8efb97c --- /dev/null +++ b/addendas/addendas.go @@ -0,0 +1,14 @@ +package addendas + +import "github.com/invopop/gobl/bill" + +// For returns a set of addenda objects for the given invoice. +func For(inv *bill.Invoice) []any { + list := make([]any, 0) + + if isMabe(inv) { + list = append(list, newMabe(inv)) + } + + return list +} diff --git a/mabe.go b/addendas/mabe.go similarity index 80% rename from mabe.go rename to addendas/mabe.go index 6eb54e4..89d57dc 100644 --- a/mabe.go +++ b/addendas/mabe.go @@ -1,13 +1,17 @@ -package cfdi +package addendas import ( "encoding/xml" + "errors" "fmt" + "github.com/invopop/gobl.cfdi/internal" + "github.com/invopop/gobl.cfdi/internal/format" "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/i18n" "github.com/invopop/gobl/num" - "github.com/invopop/gobl/regimes/mx" + "github.com/invopop/gobl/org" ) // Mabe schema constants @@ -25,6 +29,14 @@ const ( MabeTipoDocumentoNotaCargo = "NOTA CARGO" ) +// Mabe specific identity codes. +const ( + MabeIdentityTypeCode = "MABE" + MabeRef1IdentityTypeCode = "MABE-REF1" + MabeRef2IdentityTypeCode = "MABE-REF2" + MabePlantIDIdentityTypeCode = "MABE-PLANT-ID" +) + // MabeFactura is the root element of the Mabe addendum type MabeFactura struct { XMLName xml.Name `xml:"mabe:Factura"` @@ -104,23 +116,35 @@ type MabeDescuentos struct { Importe string `xml:"importe,attr"` } -func addAddendaMabe(doc *Document, inv *bill.Invoice) error { +func isMabe(inv *bill.Invoice) bool { + if inv.Supplier == nil { + return false + } + id := extractIdentity(inv.Supplier.Identities, MabeIdentityTypeCode) + return id != cbc.CodeEmpty +} + +// newMabe provides a new Mabe addenda. +func newMabe(inv *bill.Invoice) (*MabeFactura, error) { tipoDocumento, err := mapMabeTipoDocumento(inv) if err != nil { - return err + return nil, err + } + if inv.Ordering == nil { + return nil, errors.New("missing ordering field") } f := &MabeFactura{ Namespace: MabeNamespace, - SchemaLocation: formatSchemaLocation(MabeNamespace, MabeSchemaLocation), + SchemaLocation: format.SchemaLocation(MabeNamespace, MabeSchemaLocation), Version: MabeVersion, TipoDocumento: tipoDocumento, Folio: formatMabeFolio(inv), Fecha: inv.IssueDate.String(), OrdenCompra: inv.Ordering.Code, - Referencia1: inv.Ext[mx.ExtKeyMabeReference1].String(), - Referencia2: inv.Ext[mx.ExtKeyMabeReference2].String(), + Referencia1: extractIdentity(inv.Ordering.Identities, MabeRef1IdentityTypeCode).String(), + Referencia2: "NA", Moneda: newMabeMoneda(inv), Proveedor: newMabeProveedor(inv), @@ -134,9 +158,7 @@ func addAddendaMabe(doc *Document, inv *bill.Invoice) error { setMabeTaxes(inv, f) - doc.Addendas = append(doc.Addendas, &ContentWrapper{f}) - - return nil + return f, nil } func mapMabeTipoDocumento(inv *bill.Invoice) (string, error) { @@ -157,16 +179,20 @@ func newMabeMoneda(inv *bill.Invoice) *MabeMoneda { } func newMabeProveedor(inv *bill.Invoice) *MabeProveedor { + if inv.Supplier == nil { + return nil + } + id := extractIdentity(inv.Supplier.Identities, MabeIdentityTypeCode) return &MabeProveedor{ - Codigo: string(inv.Supplier.Ext[mx.ExtKeyMabeProviderCode]), + Codigo: id.String(), } } func newMabeEntrega(inv *bill.Invoice) *MabeEntrega { rec := inv.Delivery.Receiver - + id := extractIdentity(rec.Identities, MabePlantIDIdentityTypeCode) e := &MabeEntrega{ - PlantaEntrega: string(rec.Ext[mx.ExtKeyMabeDeliveryPlant]), + PlantaEntrega: id.String(), } if len(rec.Addresses) > 0 { @@ -182,7 +208,7 @@ func newMabeEntrega(inv *bill.Invoice) *MabeEntrega { } func newMabeDescuentos(inv *bill.Invoice) *MabeDescuentos { - d := totalInvoiceDiscount(inv) + d := internal.TotalInvoiceDiscount(inv) if d.IsZero() { return nil @@ -199,11 +225,12 @@ func newMabeDetalles(inv *bill.Invoice) *[]*MabeDetalle { var detalles []*MabeDetalle for _, line := range inv.Lines { + id := extractIdentity(line.Item.Identities, MabeIdentityTypeCode) d := &MabeDetalle{ NoLineaArticulo: line.Index, - CodigoArticulo: line.Item.Ext[mx.ExtKeyMabeItemCode].String(), + CodigoArticulo: id.String(), Descripcion: line.Item.Name, //nolint:misspell - Unidad: mapToClaveUnidad(line), + Unidad: internal.ClaveUnidad(line), Cantidad: line.Quantity.String(), PrecioSinIva: line.Item.Price.String(), ImporteSinIva: line.Sum.String(), @@ -230,7 +257,7 @@ func setMabeTaxes(inv *bill.Invoice, mabe *MabeFactura) { for _, rate := range cat.Rates { t := &MabeImpuesto{ Tipo: catDef.Name.In(i18n.ES), - Tasa: formatTaxPercent(rate.Percent), + Tasa: format.TaxPercent(rate.Percent), Importe: rate.Amount.String(), } @@ -254,3 +281,15 @@ func setMabeTaxes(inv *bill.Invoice, mabe *MabeFactura) { func formatMabeFolio(inv *bill.Invoice) string { return fmt.Sprintf("%s%s", inv.Series, inv.Code) } + +func extractIdentity(ids []*org.Identity, typ cbc.Code) cbc.Code { + if ids == nil { + return "" + } + for _, id := range ids { + if id.Type == typ { + return id.Code + } + } + return "" +} diff --git a/cfdi.go b/cfdi.go index adccccf..b41381a 100644 --- a/cfdi.go +++ b/cfdi.go @@ -7,6 +7,9 @@ import ( "cloud.google.com/go/civil" "github.com/invopop/gobl" + "github.com/invopop/gobl.cfdi/addendas" + "github.com/invopop/gobl.cfdi/internal" + "github.com/invopop/gobl.cfdi/internal/format" "github.com/invopop/gobl/bill" "github.com/invopop/gobl/cal" "github.com/invopop/gobl/cbc" @@ -83,13 +86,13 @@ func NewDocument(env *gobl.Envelope) (*Document, error) { return nil, fmt.Errorf("invalid type %T", env.Document) } - discount := totalInvoiceDiscount(inv) + discount := internal.TotalInvoiceDiscount(inv) subtotal := inv.Totals.Total.Add(discount) document := &Document{ CFDINamespace: CFDINamespace, XSINamespace: XSINamespace, - SchemaLocation: formatSchemaLocation(CFDINamespace, CFDISchemaLocation), + SchemaLocation: format.SchemaLocation(CFDINamespace, CFDISchemaLocation), Version: CFDIVersion, TipoDeComprobante: lookupTipoDeComprobante(inv), @@ -152,13 +155,9 @@ func addComplementos(doc *Document, complements []*schema.Object) error { } func addAddendas(doc *Document, inv *bill.Invoice) error { - if mx.IsMabeSupplier(inv) { - err := addAddendaMabe(doc, inv) - if err != nil { - return err - } + for _, ad := range addendas.For(inv) { + doc.Addendas = append(doc.Addendas, &ContentWrapper{ad}) } - return nil } @@ -217,15 +216,3 @@ 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 { - td = td.Add(totalLineDiscount(l)) - } - return td -} diff --git a/food_vouchers.go b/food_vouchers.go index bbe6c55..69483ff 100644 --- a/food_vouchers.go +++ b/food_vouchers.go @@ -3,6 +3,7 @@ package cfdi import ( "encoding/xml" + "github.com/invopop/gobl.cfdi/internal/format" "github.com/invopop/gobl/regimes/mx" ) @@ -51,7 +52,7 @@ func addValesDeDespensa(doc *Document, fvc *mx.FoodVouchers) { } doc.VDNamespace = VDNamespace - doc.SchemaLocation = doc.SchemaLocation + " " + formatSchemaLocation(VDNamespace, VDSchemaLocation) + doc.SchemaLocation = doc.SchemaLocation + " " + format.SchemaLocation(VDNamespace, VDSchemaLocation) doc.Complementos = append(doc.Complementos, &ContentWrapper{vd}) } diff --git a/fuel_account_balance.go b/fuel_account_balance.go index aaf9bcc..fcaee78 100644 --- a/fuel_account_balance.go +++ b/fuel_account_balance.go @@ -3,6 +3,7 @@ package cfdi import ( "encoding/xml" + "github.com/invopop/gobl.cfdi/internal/format" "github.com/invopop/gobl/regimes/mx" ) @@ -64,7 +65,7 @@ func addEstadoCuentaCombustible(doc *Document, fc *mx.FuelAccountBalance) { } doc.ECCNamespace = ECCNamespace - doc.SchemaLocation = doc.SchemaLocation + " " + formatSchemaLocation(ECCNamespace, ECCSchemaLocation) + doc.SchemaLocation = doc.SchemaLocation + " " + format.SchemaLocation(ECCNamespace, ECCSchemaLocation) doc.Complementos = append(doc.Complementos, &ContentWrapper{ecc}) } diff --git a/go.mod b/go.mod index 189a989..1131173 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/invopop/gobl.cfdi go 1.20 require ( - github.com/invopop/gobl v0.62.1-0.20231121111925-64c5be7b0226 + github.com/invopop/gobl v0.62.2-0.20231121142901-bd3349ee3ec6 github.com/joho/godotenv v1.5.1 github.com/magefile/mage v1.15.0 github.com/spf13/cobra v1.7.0 diff --git a/go.sum b/go.sum index 8bdc902..0657604 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/gobl v0.62.1-0.20231121111925-64c5be7b0226 h1:Qca7HXgrRZOLuu++CdcFmlMWvLumkdiLR8/YfzjKuTE= github.com/invopop/gobl v0.62.1-0.20231121111925-64c5be7b0226/go.mod h1:sEngvTr2gAxexosO0rmQInVSL8C613TUPvBcFT4xMyM= +github.com/invopop/gobl v0.62.2-0.20231121142901-bd3349ee3ec6 h1:O2U9Fkj+1F6kI0GO3Cx8IBRbG69jCmyC6QPjz2SJrk4= +github.com/invopop/gobl v0.62.2-0.20231121142901-bd3349ee3ec6/go.mod h1:sEngvTr2gAxexosO0rmQInVSL8C613TUPvBcFT4xMyM= 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.3.0 h1:o260kbjXzoBO/ypXDSSrCLL7SxEFUXBsX09YTE9AxZw= diff --git a/internal/calc.go b/internal/calc.go new file mode 100644 index 0000000..17fba6d --- /dev/null +++ b/internal/calc.go @@ -0,0 +1,24 @@ +package internal + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/num" +) + +// TotalInvoiceDiscount calculates the total discount for the invoice. +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 { + td = td.Add(TotalLineDiscount(l)) + } + return td +} + +// TotalLineDiscount calculates the total discount for the line. +func TotalLineDiscount(l *bill.Line) num.Amount { + td := num.MakeAmount(0, l.Sum.Exp()) // discount's precision must match the "Importe" field's one + for _, d := range l.Discounts { + td = td.Add(d.Amount) + } + return td +} diff --git a/internal/format/format.go b/internal/format/format.go new file mode 100644 index 0000000..a77fc3f --- /dev/null +++ b/internal/format/format.go @@ -0,0 +1,28 @@ +// Package format contains helps to help format output. +package format + +import ( + "fmt" + + "github.com/invopop/gobl/num" +) + +// OptionalAmount provides empty string for zero amounts. +func OptionalAmount(a num.Amount) string { + if a.IsZero() { + return "" + } + + return a.String() +} + +// SchemaLocation provides a string with the namespace and schema location. +func SchemaLocation(namespace, schemaLocation string) string { + return fmt.Sprintf("%s %s", namespace, schemaLocation) +} + +// TaxPercent provides a string with the tax percentage rescaled according to +// CFDI requirements. +func TaxPercent(percent *num.Percentage) string { + return percent.Amount.Rescale(6).String() +} diff --git a/internal/internal.go b/internal/internal.go new file mode 100644 index 0000000..d22c74d --- /dev/null +++ b/internal/internal.go @@ -0,0 +1,2 @@ +// Package internal contains additional functionality required internally. +package internal diff --git a/internal/lines.go b/internal/lines.go new file mode 100644 index 0000000..da55c9a --- /dev/null +++ b/internal/lines.go @@ -0,0 +1,29 @@ +package internal + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/regimes/mx" +) + +// Default keys +const ( + DefaultClaveUnidad = "ZZ" // Mutuamente definida +) + +// ClaveUnidad determines the line item's "ClaveUnidad" value. +func ClaveUnidad(line *bill.Line) string { + if line.Item.Unit == "" { + return DefaultClaveUnidad + } + + return string(line.Item.Unit.UNECE()) +} + +// ClaveProdServe determines the line's Product-Service code +func ClaveProdServ(line *bill.Line) string { + if line.Item == nil { + return "" + } + + return string(line.Item.Ext[mx.ExtKeyCFDIProdServ]) +} diff --git a/lines.go b/lines.go index efbbe9d..d7c9d3e 100644 --- a/lines.go +++ b/lines.go @@ -1,17 +1,11 @@ package cfdi import ( + "github.com/invopop/gobl.cfdi/internal" "github.com/invopop/gobl/bill" - "github.com/invopop/gobl/num" - "github.com/invopop/gobl/regimes/mx" "github.com/invopop/gobl/tax" ) -// Default keys -const ( - DefaultClaveUnidad = "ZZ" // Mutuamente definida -) - // Conceptos list invoice lines // nolint:misspell type Conceptos struct { @@ -45,40 +39,16 @@ func newConceptos(lines []*bill.Line, regime *tax.Regime) *Conceptos { func newConcepto(line *bill.Line, regime *tax.Regime) *Concepto { concepto := &Concepto{ - ClaveProdServ: mapToClaveProdServ(line), + ClaveProdServ: internal.ClaveProdServ(line), Cantidad: line.Quantity.String(), - ClaveUnidad: mapToClaveUnidad(line), + ClaveUnidad: internal.ClaveUnidad(line), Descripcion: line.Item.Name, // nolint:misspell ValorUnitario: line.Item.Price.String(), Importe: line.Sum.String(), - Descuento: formatOptionalAmount(totalLineDiscount(line)), + Descuento: formatOptionalAmount(internal.TotalLineDiscount(line)), ObjetoImp: ObjetoImpSi, Impuestos: newConceptoImpuestos(line, regime), } return concepto } - -func mapToClaveUnidad(line *bill.Line) string { - if line.Item.Unit == "" { - return DefaultClaveUnidad - } - - return string(line.Item.Unit.UNECE()) -} - -func mapToClaveProdServ(line *bill.Line) string { - if line.Item == nil { - return "" - } - - return string(line.Item.Ext[mx.ExtKeyCFDIProdServ]) -} - -func totalLineDiscount(l *bill.Line) num.Amount { - td := num.MakeAmount(0, l.Sum.Exp()) // discount's precision must match the "Importe" field's one - for _, d := range l.Discounts { - td = td.Add(d.Amount) - } - return td -} From ef2da5b401d5076d6b81655031f531993123bd4a Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Tue, 21 Nov 2023 15:51:31 +0000 Subject: [PATCH 07/19] Fixing tests and formatting --- addendas/addendas.go | 11 +- cfdi.go | 6 +- go.mod | 2 +- go.sum | 6 +- internal/lines.go | 2 +- taxes.go | 10 +- test/data/addenda-mabe.json | 326 +++++++++++--------- test/data/bare-minimum-addenda-mabe.json | 37 ++- test/data/out/addenda-mabe.xml | 2 +- test/data/out/bare-minimum-addenda-mabe.xml | 2 +- 10 files changed, 222 insertions(+), 182 deletions(-) diff --git a/addendas/addendas.go b/addendas/addendas.go index 8efb97c..4dc3e99 100644 --- a/addendas/addendas.go +++ b/addendas/addendas.go @@ -1,14 +1,19 @@ +// Package addendas adds additional functionality for "Addendas" to the CFDI documents. package addendas import "github.com/invopop/gobl/bill" // For returns a set of addenda objects for the given invoice. -func For(inv *bill.Invoice) []any { +func For(inv *bill.Invoice) ([]any, error) { list := make([]any, 0) if isMabe(inv) { - list = append(list, newMabe(inv)) + ad, err := newMabe(inv) + if err != nil { + return nil, err + } + list = append(list, ad) } - return list + return list, nil } diff --git a/cfdi.go b/cfdi.go index b41381a..d4bdf96 100644 --- a/cfdi.go +++ b/cfdi.go @@ -155,7 +155,11 @@ func addComplementos(doc *Document, complements []*schema.Object) error { } func addAddendas(doc *Document, inv *bill.Invoice) error { - for _, ad := range addendas.For(inv) { + ads, err := addendas.For(inv) + if err != nil { + return err + } + for _, ad := range ads { doc.Addendas = append(doc.Addendas, &ContentWrapper{ad}) } return nil diff --git a/go.mod b/go.mod index 1131173..77bb6df 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/invopop/gobl.cfdi go 1.20 require ( - github.com/invopop/gobl v0.62.2-0.20231121142901-bd3349ee3ec6 + github.com/invopop/gobl v0.62.2-0.20231121145421-5d7dce0fd42d github.com/joho/godotenv v1.5.1 github.com/magefile/mage v1.15.0 github.com/spf13/cobra v1.7.0 diff --git a/go.sum b/go.sum index 0657604..3cec26c 100644 --- a/go.sum +++ b/go.sum @@ -19,10 +19,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.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.62.1-0.20231121111925-64c5be7b0226 h1:Qca7HXgrRZOLuu++CdcFmlMWvLumkdiLR8/YfzjKuTE= -github.com/invopop/gobl v0.62.1-0.20231121111925-64c5be7b0226/go.mod h1:sEngvTr2gAxexosO0rmQInVSL8C613TUPvBcFT4xMyM= -github.com/invopop/gobl v0.62.2-0.20231121142901-bd3349ee3ec6 h1:O2U9Fkj+1F6kI0GO3Cx8IBRbG69jCmyC6QPjz2SJrk4= -github.com/invopop/gobl v0.62.2-0.20231121142901-bd3349ee3ec6/go.mod h1:sEngvTr2gAxexosO0rmQInVSL8C613TUPvBcFT4xMyM= +github.com/invopop/gobl v0.62.2-0.20231121145421-5d7dce0fd42d h1:tPpq1L2YH8IyGS452WCPGlbsbjw8u0okxmuhZ21wZi0= +github.com/invopop/gobl v0.62.2-0.20231121145421-5d7dce0fd42d/go.mod h1:sEngvTr2gAxexosO0rmQInVSL8C613TUPvBcFT4xMyM= 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.3.0 h1:o260kbjXzoBO/ypXDSSrCLL7SxEFUXBsX09YTE9AxZw= diff --git a/internal/lines.go b/internal/lines.go index da55c9a..5d919d5 100644 --- a/internal/lines.go +++ b/internal/lines.go @@ -19,7 +19,7 @@ func ClaveUnidad(line *bill.Line) string { return string(line.Item.Unit.UNECE()) } -// ClaveProdServe determines the line's Product-Service code +// ClaveProdServ determines the line's Product-Service code func ClaveProdServ(line *bill.Line) string { if line.Item == nil { return "" diff --git a/taxes.go b/taxes.go index 1ec0032..7e074c3 100644 --- a/taxes.go +++ b/taxes.go @@ -1,9 +1,9 @@ package cfdi import ( + "github.com/invopop/gobl.cfdi/internal/format" "github.com/invopop/gobl/bill" "github.com/invopop/gobl/currency" - "github.com/invopop/gobl/num" "github.com/invopop/gobl/regimes/mx" "github.com/invopop/gobl/tax" ) @@ -88,7 +88,7 @@ func newImpuesto(rate *tax.RateTotal, currency *currency.Code, catDef *tax.Categ Base: rate.Base.Rescale(cu).String(), Importe: rate.Amount.Rescale(cu).String(), Impuesto: catDef.Map[mx.KeySATImpuesto].String(), - TasaOCuota: formatTaxPercent(rate.Percent), + TasaOCuota: format.TaxPercent(rate.Percent), TipoFactor: TipoFactorTasa, } @@ -130,13 +130,9 @@ func newConceptoImpuesto(line *bill.Line, tax *tax.Combo, catDef *tax.Category) Base: line.Total.String(), Importe: taxAmount.String(), Impuesto: catDef.Map[mx.KeySATImpuesto].String(), - TasaOCuota: formatTaxPercent(tax.Percent), + TasaOCuota: format.TaxPercent(tax.Percent), TipoFactor: TipoFactorTasa, } return i } - -func formatTaxPercent(percent *num.Percentage) string { - return percent.Amount.Rescale(6).String() -} diff --git a/test/data/addenda-mabe.json b/test/data/addenda-mabe.json index 6e752ba..75531da 100644 --- a/test/data/addenda-mabe.json +++ b/test/data/addenda-mabe.json @@ -1,155 +1,173 @@ { - "$schema": "https://gobl.org/draft-0/envelope", - "head": { - "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", - "dig": { - "alg": "sha256", - "val": "3aa3b38d147021722253109fa8a6a54cb543611b6ed7528cbdaa9030ecb21359" - }, - "draft": true - }, - "doc": { - "$schema": "https://gobl.org/draft-0/bill/invoice", - "type": "standard", - "series": "LMC", - "code": "0010", - "issue_date": "2023-05-29", - "currency": "MXN", - "supplier": { - "name": "ESCUELA KEMPER URGATE", - "tax_id": { - "country": "MX", - "zone": "26015", - "code": "EKU9003173C9" - }, - "ext": { - "mx-cfdi-fiscal-regime": "601", - "mx-mabe-provider-code": "123456" - } - }, - "customer": { - "name": "UNIVERSIDAD ROBOTICA ESPAÑOLA", - "tax_id": { - "country": "MX", - "zone": "65000", - "code": "URE180429TM6" - }, - "ext": { - "mx-cfdi-fiscal-regime": "601", - "mx-cfdi-use": "G01" - } - }, - "lines": [ - { - "i": 1, - "quantity": "2", - "item": { - "name": "Cigarros", - "price": "100.00", - "ext": { - "mx-cfdi-prod-serv": "50211502", - "mx-mabe-item-code": "CODE123" - } - }, - "sum": "200.00", - "discounts": [ - { - "percent": "10.0%", - "amount": "20.00" - } - ], - "taxes": [ - { - "cat": "VAT", - "rate": "standard", - "percent": "16.0%" - }, - { - "cat": "RVAT", - "percent": "10.6667%" - }, - { - "cat": "ISR", - "percent": "10%" - } - ], - "total": "180.00" - } - ], - "ordering": { - "code": "9100000000" - }, - "payment": { - "instructions": { - "key": "credit-transfer" - } - }, - "delivery": { - "receiver": { - "name": "ESTUFAS 30", - "addresses": [ - { - "street": "Calle 1", - "locality": "Mexico D.F.", - "code": "12345" - } - ], - "ext": { - "mx-mabe-delivery-plant": "S001" - } - } - }, - "totals": { - "sum": "180.00", - "total": "180.00", - "taxes": { - "categories": [ - { - "code": "VAT", - "rates": [ - { - "key": "standard", - "base": "180.00", - "percent": "16.0%", - "amount": "28.80" - } - ], - "amount": "28.80" - }, - { - "code": "RVAT", - "retained": true, - "rates": [ - { - "base": "180.00", - "percent": "10.6667%", - "amount": "19.20" - } - ], - "amount": "19.20" - }, - { - "code": "ISR", - "retained": true, - "rates": [ - { - "base": "180.00", - "percent": "10%", - "amount": "18.00" - } - ], - "amount": "18.00" - } - ], - "sum": "-8.40" - }, - "tax": "-8.40", - "total_with_tax": "171.60", - "payable": "171.60" - }, - "ext": { - "mx-mabe-reference-1": "123456", - "mx-mabe-reference-2": "789" - } - } -} \ No newline at end of file + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "XXX" + }, + "draft": true + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "type": "standard", + "series": "LMC", + "code": "0010", + "issue_date": "2023-05-29", + "currency": "MXN", + "supplier": { + "name": "ESCUELA KEMPER URGATE", + "tax_id": { + "country": "MX", + "zone": "26015", + "code": "EKU9003173C9" + }, + "identities": [ + { + "type": "MABE", + "code": "123456" + } + ], + "ext": { + "mx-cfdi-fiscal-regime": "601" + } + }, + "customer": { + "name": "UNIVERSIDAD ROBOTICA ESPAÑOLA", + "tax_id": { + "country": "MX", + "zone": "65000", + "code": "URE180429TM6" + }, + "ext": { + "mx-cfdi-fiscal-regime": "601", + "mx-cfdi-use": "G01" + } + }, + "lines": [ + { + "i": 1, + "quantity": "2", + "item": { + "name": "Cigarros", + "price": "100.00", + "identities": [ + { + "type": "MABE", + "code": "CODE123" + } + ], + "ext": { + "mx-cfdi-prod-serv": "50211502" + } + }, + "sum": "200.00", + "discounts": [ + { + "percent": "10.0%", + "amount": "20.00" + } + ], + "taxes": [ + { + "cat": "VAT", + "rate": "standard", + "percent": "16.0%" + }, + { + "cat": "RVAT", + "percent": "10.6667%" + }, + { + "cat": "ISR", + "percent": "10%" + } + ], + "total": "180.00" + } + ], + "ordering": { + "code": "9100000000", + "identities": [ + { + "type": "MABE-REF1", + "code": "123456" + } + ] + }, + "payment": { + "instructions": { + "key": "credit-transfer" + } + }, + "delivery": { + "receiver": { + "name": "ESTUFAS 30", + "addresses": [ + { + "street": "Calle 1", + "locality": "Mexico D.F.", + "code": "12345" + } + ], + "identities": [ + { + "type": "MABE-PLANT-ID", + "code": "S001" + } + ] + } + }, + "totals": { + "sum": "180.00", + "total": "180.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "180.00", + "percent": "16.0%", + "amount": "28.80" + } + ], + "amount": "28.80" + }, + { + "code": "RVAT", + "retained": true, + "rates": [ + { + "base": "180.00", + "percent": "10.6667%", + "amount": "19.20" + } + ], + "amount": "19.20" + }, + { + "code": "ISR", + "retained": true, + "rates": [ + { + "base": "180.00", + "percent": "10%", + "amount": "18.00" + } + ], + "amount": "18.00" + } + ], + "sum": "-8.40" + }, + "tax": "-8.40", + "total_with_tax": "171.60", + "payable": "171.60" + }, + "ext": { + "mx-mabe-reference-2": "789" + } + } +} diff --git a/test/data/bare-minimum-addenda-mabe.json b/test/data/bare-minimum-addenda-mabe.json index 5158a25..045e9f6 100644 --- a/test/data/bare-minimum-addenda-mabe.json +++ b/test/data/bare-minimum-addenda-mabe.json @@ -4,7 +4,7 @@ "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", "dig": { "alg": "sha256", - "val": "a8749e521981bc47aa0ff5bc6389e0d809dbe07f8b05c278e7674c3e79b8fe77" + "val": "XXX" }, "draft": true }, @@ -22,9 +22,14 @@ "zone": "26015", "code": "EKU9003173C9" }, + "identities": [ + { + "type": "MABE", + "code": "123456" + } + ], "ext": { - "mx-cfdi-fiscal-regime": "601", - "mx-mabe-provider-code": "123456" + "mx-cfdi-fiscal-regime": "601" } }, "customer": { @@ -46,9 +51,14 @@ "item": { "name": "Cigarros", "price": "100.00", + "identities": [ + { + "type": "MABE", + "code": "CODE123" + } + ], "ext": { - "mx-cfdi-prod-serv": "50211502", - "mx-mabe-item-code": "CODE123" + "mx-cfdi-prod-serv": "50211502" } }, "sum": "200.00", @@ -63,7 +73,13 @@ } ], "ordering": { - "code": "91000000" + "code": "91000000", + "identities": [ + { + "type": "MABE-REF1", + "code": "900900" + } + ] }, "payment": { "instructions": { @@ -73,9 +89,12 @@ "delivery": { "receiver": { "name": "ESTUFAS 30", - "ext": { - "mx-mabe-delivery-plant": "S001" - } + "identities": [ + { + "type": "MABE-PLANT-ID", + "code": "S001" + } + ] } }, "totals": { diff --git a/test/data/out/addenda-mabe.xml b/test/data/out/addenda-mabe.xml index 603c44f..6d44a33 100644 --- a/test/data/out/addenda-mabe.xml +++ b/test/data/out/addenda-mabe.xml @@ -25,7 +25,7 @@ - + diff --git a/test/data/out/bare-minimum-addenda-mabe.xml b/test/data/out/bare-minimum-addenda-mabe.xml index aa49a7d..bad05b5 100644 --- a/test/data/out/bare-minimum-addenda-mabe.xml +++ b/test/data/out/bare-minimum-addenda-mabe.xml @@ -17,7 +17,7 @@ - + From e8169ebee9f4080979b7daafa9329a6c615de563 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Tue, 21 Nov 2023 18:03:54 +0000 Subject: [PATCH 08/19] Moving to use identity key instead of type --- addendas/README.md | 16 ++++++++-------- addendas/mabe.go | 23 ++++++++++++----------- go.mod | 2 +- go.sum | 2 ++ test/data/addenda-mabe.json | 8 ++++---- test/data/bare-minimum-addenda-mabe.json | 11 ++++------- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/addendas/README.md b/addendas/README.md index 5fae09d..2bc118e 100644 --- a/addendas/README.md +++ b/addendas/README.md @@ -8,11 +8,11 @@ Each of the addendas currently supported are listed below, with instructions on Most of the MABE Addenda fields are determined automatically from the base GOBL Invoice, with the exception of the following: -| MABE Field | GOBL Invoice Value | Description | -| -------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------- | -| Order Code | `ordering.code = "-CODE-"` | Provided by Mabe for the order | -| Provider Code | `supplier.identities = [{"type":"MABE", "code":"-CODE-"}]` | Code issued by Mabe to identify the supplier | -| Delivery Plant | `delivery.receiver.identities = [{"type":"MABE-PLANT-ID","code":"-CODE-"}]` | Delivery Plant ID | -| Item Code | `lines[i].item.identities = [{"type":"MABE","code":"-CODE-"}]` | Article code provided by Mabe | -| Reference 1 | `ordering.identities = [{"type":"MABE-REF1","code":"-CODE-"}]` | Additional code required by Mabe in certain circumstances while ordering. | -| Reference 2 | NA | Always empty as not currently used by Mabe. | +| MABE Field | GOBL Invoice Value | Description | +| -------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| Order Code | `ordering.code = "-CODE-"` | Provided by Mabe for the order | +| Provider Code | `supplier.identities = [{"key":"mx-mabe-provider", "code":"-CODE-"}]` | Code issued by Mabe to identify the supplier | +| Delivery Plant | `delivery.receiver.identities = [{"key":"mx-mabe-plant","code":"-CODE-"}]` | Delivery Plant Code | +| Item Code | `lines[i].item.identities = [{"key":"mx-mabe-item","code":"-CODE-"}]` | Article code provided by Mabe | +| Reference 1 | `ordering.identities = [{"key":"mx-mabe-ref1","code":"-CODE-"}]` | Additional code required by Mabe in certain circumstances while ordering. | +| Reference 2 | NA | Always empty as not currently used by Mabe. | diff --git a/addendas/mabe.go b/addendas/mabe.go index 89d57dc..d9a4e9e 100644 --- a/addendas/mabe.go +++ b/addendas/mabe.go @@ -31,10 +31,11 @@ const ( // Mabe specific identity codes. const ( - MabeIdentityTypeCode = "MABE" - MabeRef1IdentityTypeCode = "MABE-REF1" - MabeRef2IdentityTypeCode = "MABE-REF2" - MabePlantIDIdentityTypeCode = "MABE-PLANT-ID" + MabeKeyIdentityProvider = "mx-mabe-provider" + MabeKeyIdentityRef1 = "mx-mabe-ref1" + MabeKeyIdentityRef2 = "mx-mabe-ref2" + MabeKeyIdentityPlant = "mx-mabe-plant" + MabeKeyIdentityItem = "mx-mabe-item" ) // MabeFactura is the root element of the Mabe addendum @@ -120,7 +121,7 @@ func isMabe(inv *bill.Invoice) bool { if inv.Supplier == nil { return false } - id := extractIdentity(inv.Supplier.Identities, MabeIdentityTypeCode) + id := extractIdentity(inv.Supplier.Identities, MabeKeyIdentityProvider) return id != cbc.CodeEmpty } @@ -143,7 +144,7 @@ func newMabe(inv *bill.Invoice) (*MabeFactura, error) { Folio: formatMabeFolio(inv), Fecha: inv.IssueDate.String(), OrdenCompra: inv.Ordering.Code, - Referencia1: extractIdentity(inv.Ordering.Identities, MabeRef1IdentityTypeCode).String(), + Referencia1: extractIdentity(inv.Ordering.Identities, MabeKeyIdentityRef1).String(), Referencia2: "NA", Moneda: newMabeMoneda(inv), @@ -182,7 +183,7 @@ func newMabeProveedor(inv *bill.Invoice) *MabeProveedor { if inv.Supplier == nil { return nil } - id := extractIdentity(inv.Supplier.Identities, MabeIdentityTypeCode) + id := extractIdentity(inv.Supplier.Identities, MabeKeyIdentityProvider) return &MabeProveedor{ Codigo: id.String(), } @@ -190,7 +191,7 @@ func newMabeProveedor(inv *bill.Invoice) *MabeProveedor { func newMabeEntrega(inv *bill.Invoice) *MabeEntrega { rec := inv.Delivery.Receiver - id := extractIdentity(rec.Identities, MabePlantIDIdentityTypeCode) + id := extractIdentity(rec.Identities, MabeKeyIdentityPlant) e := &MabeEntrega{ PlantaEntrega: id.String(), } @@ -225,7 +226,7 @@ func newMabeDetalles(inv *bill.Invoice) *[]*MabeDetalle { var detalles []*MabeDetalle for _, line := range inv.Lines { - id := extractIdentity(line.Item.Identities, MabeIdentityTypeCode) + id := extractIdentity(line.Item.Identities, MabeKeyIdentityItem) d := &MabeDetalle{ NoLineaArticulo: line.Index, CodigoArticulo: id.String(), @@ -282,12 +283,12 @@ func formatMabeFolio(inv *bill.Invoice) string { return fmt.Sprintf("%s%s", inv.Series, inv.Code) } -func extractIdentity(ids []*org.Identity, typ cbc.Code) cbc.Code { +func extractIdentity(ids []*org.Identity, key cbc.Key) cbc.Code { if ids == nil { return "" } for _, id := range ids { - if id.Type == typ { + if id.Key == key { return id.Code } } diff --git a/go.mod b/go.mod index 77bb6df..ff7d106 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/invopop/gobl.cfdi go 1.20 require ( - github.com/invopop/gobl v0.62.2-0.20231121145421-5d7dce0fd42d + github.com/invopop/gobl v0.62.2-0.20231121175846-f10ffeb5a094 github.com/joho/godotenv v1.5.1 github.com/magefile/mage v1.15.0 github.com/spf13/cobra v1.7.0 diff --git a/go.sum b/go.sum index 3cec26c..b0ba4d7 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/gobl v0.62.2-0.20231121145421-5d7dce0fd42d h1:tPpq1L2YH8IyGS452WCPGlbsbjw8u0okxmuhZ21wZi0= github.com/invopop/gobl v0.62.2-0.20231121145421-5d7dce0fd42d/go.mod h1:sEngvTr2gAxexosO0rmQInVSL8C613TUPvBcFT4xMyM= +github.com/invopop/gobl v0.62.2-0.20231121175846-f10ffeb5a094 h1:jPOc6SxQap9sq01c3LodCQi0NdC5UpfwW0g2r8cQUYg= +github.com/invopop/gobl v0.62.2-0.20231121175846-f10ffeb5a094/go.mod h1:sEngvTr2gAxexosO0rmQInVSL8C613TUPvBcFT4xMyM= 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.3.0 h1:o260kbjXzoBO/ypXDSSrCLL7SxEFUXBsX09YTE9AxZw= diff --git a/test/data/addenda-mabe.json b/test/data/addenda-mabe.json index 75531da..93f6cfd 100644 --- a/test/data/addenda-mabe.json +++ b/test/data/addenda-mabe.json @@ -24,7 +24,7 @@ }, "identities": [ { - "type": "MABE", + "key": "mx-mabe-provider", "code": "123456" } ], @@ -53,7 +53,7 @@ "price": "100.00", "identities": [ { - "type": "MABE", + "key": "mx-mabe-item", "code": "CODE123" } ], @@ -90,7 +90,7 @@ "code": "9100000000", "identities": [ { - "type": "MABE-REF1", + "key": "mx-mabe-ref1", "code": "123456" } ] @@ -112,7 +112,7 @@ ], "identities": [ { - "type": "MABE-PLANT-ID", + "key": "mx-mabe-plant", "code": "S001" } ] diff --git a/test/data/bare-minimum-addenda-mabe.json b/test/data/bare-minimum-addenda-mabe.json index 045e9f6..8a28890 100644 --- a/test/data/bare-minimum-addenda-mabe.json +++ b/test/data/bare-minimum-addenda-mabe.json @@ -24,7 +24,7 @@ }, "identities": [ { - "type": "MABE", + "key": "mx-mabe-provider", "code": "123456" } ], @@ -53,7 +53,7 @@ "price": "100.00", "identities": [ { - "type": "MABE", + "key": "mx-mabe-item", "code": "CODE123" } ], @@ -76,7 +76,7 @@ "code": "91000000", "identities": [ { - "type": "MABE-REF1", + "key": "mx-mabe-ref1", "code": "900900" } ] @@ -91,7 +91,7 @@ "name": "ESTUFAS 30", "identities": [ { - "type": "MABE-PLANT-ID", + "key": "mx-mabe-plant", "code": "S001" } ] @@ -120,9 +120,6 @@ "tax": "32.00", "total_with_tax": "232.00", "payable": "232.00" - }, - "ext": { - "mx-mabe-reference-1": "900900" } } } From 298d084329884087b0c5284e961f4080bebeeba5 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Tue, 21 Nov 2023 22:11:45 +0000 Subject: [PATCH 09/19] Using more complete identity keys --- addendas/mabe.go | 18 +++++++++--------- test/data/addenda-mabe.json | 9 +++------ test/data/bare-minimum-addenda-mabe.json | 6 +++--- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/addendas/mabe.go b/addendas/mabe.go index d9a4e9e..58f4152 100644 --- a/addendas/mabe.go +++ b/addendas/mabe.go @@ -31,11 +31,11 @@ const ( // Mabe specific identity codes. const ( - MabeKeyIdentityProvider = "mx-mabe-provider" - MabeKeyIdentityRef1 = "mx-mabe-ref1" - MabeKeyIdentityRef2 = "mx-mabe-ref2" - MabeKeyIdentityPlant = "mx-mabe-plant" - MabeKeyIdentityItem = "mx-mabe-item" + MabeKeyIdentityProviderID = "mx-mabe-provider-id" + MabeKeyIdentityRef1 = "mx-mabe-ref1" + MabeKeyIdentityRef2 = "mx-mabe-ref2" + MabeKeyIdentityPlantID = "mx-mabe-plant-id" + MabeKeyIdentityItemID = "mx-mabe-item-id" ) // MabeFactura is the root element of the Mabe addendum @@ -121,7 +121,7 @@ func isMabe(inv *bill.Invoice) bool { if inv.Supplier == nil { return false } - id := extractIdentity(inv.Supplier.Identities, MabeKeyIdentityProvider) + id := extractIdentity(inv.Supplier.Identities, MabeKeyIdentityProviderID) return id != cbc.CodeEmpty } @@ -183,7 +183,7 @@ func newMabeProveedor(inv *bill.Invoice) *MabeProveedor { if inv.Supplier == nil { return nil } - id := extractIdentity(inv.Supplier.Identities, MabeKeyIdentityProvider) + id := extractIdentity(inv.Supplier.Identities, MabeKeyIdentityProviderID) return &MabeProveedor{ Codigo: id.String(), } @@ -191,7 +191,7 @@ func newMabeProveedor(inv *bill.Invoice) *MabeProveedor { func newMabeEntrega(inv *bill.Invoice) *MabeEntrega { rec := inv.Delivery.Receiver - id := extractIdentity(rec.Identities, MabeKeyIdentityPlant) + id := extractIdentity(rec.Identities, MabeKeyIdentityPlantID) e := &MabeEntrega{ PlantaEntrega: id.String(), } @@ -226,7 +226,7 @@ func newMabeDetalles(inv *bill.Invoice) *[]*MabeDetalle { var detalles []*MabeDetalle for _, line := range inv.Lines { - id := extractIdentity(line.Item.Identities, MabeKeyIdentityItem) + id := extractIdentity(line.Item.Identities, MabeKeyIdentityItemID) d := &MabeDetalle{ NoLineaArticulo: line.Index, CodigoArticulo: id.String(), diff --git a/test/data/addenda-mabe.json b/test/data/addenda-mabe.json index 93f6cfd..f724ce0 100644 --- a/test/data/addenda-mabe.json +++ b/test/data/addenda-mabe.json @@ -24,7 +24,7 @@ }, "identities": [ { - "key": "mx-mabe-provider", + "key": "mx-mabe-provider-id", "code": "123456" } ], @@ -53,7 +53,7 @@ "price": "100.00", "identities": [ { - "key": "mx-mabe-item", + "key": "mx-mabe-item-id", "code": "CODE123" } ], @@ -112,7 +112,7 @@ ], "identities": [ { - "key": "mx-mabe-plant", + "key": "mx-mabe-plant-id", "code": "S001" } ] @@ -165,9 +165,6 @@ "tax": "-8.40", "total_with_tax": "171.60", "payable": "171.60" - }, - "ext": { - "mx-mabe-reference-2": "789" } } } diff --git a/test/data/bare-minimum-addenda-mabe.json b/test/data/bare-minimum-addenda-mabe.json index 8a28890..1fc87ca 100644 --- a/test/data/bare-minimum-addenda-mabe.json +++ b/test/data/bare-minimum-addenda-mabe.json @@ -24,7 +24,7 @@ }, "identities": [ { - "key": "mx-mabe-provider", + "key": "mx-mabe-provider-id", "code": "123456" } ], @@ -53,7 +53,7 @@ "price": "100.00", "identities": [ { - "key": "mx-mabe-item", + "key": "mx-mabe-item-id", "code": "CODE123" } ], @@ -91,7 +91,7 @@ "name": "ESTUFAS 30", "identities": [ { - "key": "mx-mabe-plant", + "key": "mx-mabe-plant-id", "code": "S001" } ] From d99297c84fe1c72bad64a3ebb25729527fe8ade8 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Wed, 22 Nov 2023 08:25:07 +0000 Subject: [PATCH 10/19] Updating readme with addendas link --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 35e852a..502d867 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,10 @@ sudo apt-get install libxml2-dev Tests can take a while to run as they download the complete XML documents to test against, please be patient. +## Addendas + +For details on support for converting Addendas, please see the [addendas package](addendas). + ## Command Line The GOBL to CFDI tool also includes a command line helper. You can find pre-built [gobl.cfdi binaries](https://github.com/invopop/gobl.cfdi/releases) in the github repository, or install manually in your Go environment with: From 85e171a2c087e83eacf28b63e53a4d9f603a322d Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Wed, 22 Nov 2023 08:32:26 +0000 Subject: [PATCH 11/19] Improving README and reference2 support --- addendas/README.md | 16 ++++++++-------- addendas/mabe.go | 9 ++++++++- test/data/addenda-mabe.json | 4 ++++ test/data/out/addenda-mabe.xml | 2 +- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/addendas/README.md b/addendas/README.md index 2bc118e..7be386d 100644 --- a/addendas/README.md +++ b/addendas/README.md @@ -8,11 +8,11 @@ Each of the addendas currently supported are listed below, with instructions on Most of the MABE Addenda fields are determined automatically from the base GOBL Invoice, with the exception of the following: -| MABE Field | GOBL Invoice Value | Description | -| -------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------- | -| Order Code | `ordering.code = "-CODE-"` | Provided by Mabe for the order | -| Provider Code | `supplier.identities = [{"key":"mx-mabe-provider", "code":"-CODE-"}]` | Code issued by Mabe to identify the supplier | -| Delivery Plant | `delivery.receiver.identities = [{"key":"mx-mabe-plant","code":"-CODE-"}]` | Delivery Plant Code | -| Item Code | `lines[i].item.identities = [{"key":"mx-mabe-item","code":"-CODE-"}]` | Article code provided by Mabe | -| Reference 1 | `ordering.identities = [{"key":"mx-mabe-ref1","code":"-CODE-"}]` | Additional code required by Mabe in certain circumstances while ordering. | -| Reference 2 | NA | Always empty as not currently used by Mabe. | +| MABE Field | GOBL Invoice Property | GOBL Invoice Value | Description | +| -------------- | ------------------------------ | ----------------------------------------------- | ------------------------------------------------------------------------- | +| Order Code | `ordering.code` | `"-CODE-"` | Provided by Mabe for the order | +| Provider Code | `supplier.identities` | `[{"key":"mx-mabe-provider", "code":"-CODE-"}]` | Code issued by Mabe to identify the supplier | +| Delivery Plant | `delivery.receiver.identities` | `[{"key":"mx-mabe-plant","code":"-CODE-"}]` | Delivery Plant Code | +| Item Code | `lines[i].item.identities` | `[{"key":"mx-mabe-item","code":"-CODE-"}]` | Article code provided by Mabe | +| Reference 1 | `ordering.identities` | `[{"key":"mx-mabe-ref1","code":"-CODE-"}]` | Additional code required by Mabe in certain circumstances while ordering. | +| Reference 2 | `ordering.identities` | `[{"key":"mx-mabe-ref2","code":"-CODE-"}]` | Set to `NA` be default as not currently used by Mabe. | diff --git a/addendas/mabe.go b/addendas/mabe.go index 58f4152..7006c56 100644 --- a/addendas/mabe.go +++ b/addendas/mabe.go @@ -135,6 +135,13 @@ func newMabe(inv *bill.Invoice) (*MabeFactura, error) { return nil, errors.New("missing ordering field") } + // Ref2 is not currently used by Mabe, so we set the default + // value to "NA". + ref2 := extractIdentity(inv.Ordering.Identities, MabeKeyIdentityRef2) + if ref2 == "" { + ref2 = "NA" + } + f := &MabeFactura{ Namespace: MabeNamespace, SchemaLocation: format.SchemaLocation(MabeNamespace, MabeSchemaLocation), @@ -145,7 +152,7 @@ func newMabe(inv *bill.Invoice) (*MabeFactura, error) { Fecha: inv.IssueDate.String(), OrdenCompra: inv.Ordering.Code, Referencia1: extractIdentity(inv.Ordering.Identities, MabeKeyIdentityRef1).String(), - Referencia2: "NA", + Referencia2: ref2.String(), Moneda: newMabeMoneda(inv), Proveedor: newMabeProveedor(inv), diff --git a/test/data/addenda-mabe.json b/test/data/addenda-mabe.json index f724ce0..b6b5b82 100644 --- a/test/data/addenda-mabe.json +++ b/test/data/addenda-mabe.json @@ -92,6 +92,10 @@ { "key": "mx-mabe-ref1", "code": "123456" + }, + { + "key": "mx-mabe-ref2", + "code": "654321" } ] }, diff --git a/test/data/out/addenda-mabe.xml b/test/data/out/addenda-mabe.xml index 6d44a33..f7a108f 100644 --- a/test/data/out/addenda-mabe.xml +++ b/test/data/out/addenda-mabe.xml @@ -25,7 +25,7 @@ - + From 869c2a674a6df7c7cd8451cd539e7abea9ab6a64 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Wed, 22 Nov 2023 08:33:54 +0000 Subject: [PATCH 12/19] Typo --- addendas/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addendas/README.md b/addendas/README.md index 7be386d..ea3a2c8 100644 --- a/addendas/README.md +++ b/addendas/README.md @@ -8,11 +8,11 @@ Each of the addendas currently supported are listed below, with instructions on Most of the MABE Addenda fields are determined automatically from the base GOBL Invoice, with the exception of the following: -| MABE Field | GOBL Invoice Property | GOBL Invoice Value | Description | +| MABE Field | GOBL Invoice Property | GOBL Invoice Value | Description | | -------------- | ------------------------------ | ----------------------------------------------- | ------------------------------------------------------------------------- | | Order Code | `ordering.code` | `"-CODE-"` | Provided by Mabe for the order | | Provider Code | `supplier.identities` | `[{"key":"mx-mabe-provider", "code":"-CODE-"}]` | Code issued by Mabe to identify the supplier | | Delivery Plant | `delivery.receiver.identities` | `[{"key":"mx-mabe-plant","code":"-CODE-"}]` | Delivery Plant Code | | Item Code | `lines[i].item.identities` | `[{"key":"mx-mabe-item","code":"-CODE-"}]` | Article code provided by Mabe | | Reference 1 | `ordering.identities` | `[{"key":"mx-mabe-ref1","code":"-CODE-"}]` | Additional code required by Mabe in certain circumstances while ordering. | -| Reference 2 | `ordering.identities` | `[{"key":"mx-mabe-ref2","code":"-CODE-"}]` | Set to `NA` be default as not currently used by Mabe. | +| Reference 2 | `ordering.identities` | `[{"key":"mx-mabe-ref2","code":"-CODE-"}]` | Set to `NA` by default as not currently used by Mabe. | From 7be59556bc507859558ac47b8508d78c143b83fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Tue, 21 Nov 2023 14:34:06 +0000 Subject: [PATCH 13/19] Marshal multiple complements or addendums properly --- cfdi.go | 32 +++++++++++++++++------ cfdi_test.go | 2 +- food_vouchers.go | 2 +- food_vouchers_test.go | 37 --------------------------- fuel_account_balance.go | 2 +- fuel_account_balance_test.go | 49 ------------------------------------ internal/nodes.go | 7 ++++++ 7 files changed, 34 insertions(+), 97 deletions(-) delete mode 100644 food_vouchers_test.go delete mode 100644 fuel_account_balance_test.go create mode 100644 internal/nodes.go diff --git a/cfdi.go b/cfdi.go index d4bdf96..c704089 100644 --- a/cfdi.go +++ b/cfdi.go @@ -70,13 +70,8 @@ type Document struct { Conceptos *Conceptos `xml:"cfdi:Conceptos"` //nolint:misspell Impuestos *Impuestos `xml:"cfdi:Impuestos,omitempty"` - Complementos []*ContentWrapper `xml:"cfdi:Complemento,omitempty"` - Addendas []*ContentWrapper `xml:"cfdi:Addenda,omitempty"` -} - -// ContentWrapper is a struct necessary to wrap any arbitrary XML content within another tag. -type ContentWrapper struct { - Content interface{} `xml:",any"` + Complemento *internal.Nodes `xml:"cfdi:Complemento,omitempty"` + Addenda *internal.Nodes `xml:"cfdi:Addenda,omitempty"` } // NewDocument converts a GOBL envelope into a CFDI document @@ -139,6 +134,26 @@ func (d *Document) Bytes() ([]byte, error) { return append([]byte(xml.Header), bytes...), nil } +// AppendComplemento appends a complement to the document +func (d *Document) AppendComplemento(c interface{}) { + // We keep it nil unless an element is added so that no empty node is marshalled to XML + if d.Complemento == nil { + d.Complemento = &internal.Nodes{} + } + + d.Complemento.Nodes = append(d.Complemento.Nodes, c) +} + +// AppendAddenda appends an addenda to the document +func (d *Document) AppendAddenda(c interface{}) { + // We keep it nil unless an element is added so that no empty node is marshalled to XML + if d.Addenda == nil { + d.Addenda = &internal.Nodes{} + } + + d.Addenda.Nodes = append(d.Addenda.Nodes, c) +} + func addComplementos(doc *Document, complements []*schema.Object) error { for _, c := range complements { switch o := c.Instance().(type) { @@ -159,8 +174,9 @@ func addAddendas(doc *Document, inv *bill.Invoice) error { if err != nil { return err } + for _, ad := range ads { - doc.Addendas = append(doc.Addendas, &ContentWrapper{ad}) + doc.AppendAddenda(ad) } return nil } diff --git a/cfdi_test.go b/cfdi_test.go index f4b9f07..f9c5a24 100644 --- a/cfdi_test.go +++ b/cfdi_test.go @@ -32,7 +32,7 @@ func TestComprobanteIngreso(t *testing.T) { assert.Equal(t, "03", doc.FormaPago) assert.Equal(t, "Pago a 30 días.", doc.CondicionesDePago) - assert.Equal(t, 0, len(doc.Complementos)) + assert.Nil(t, doc.Complemento) }) } diff --git a/food_vouchers.go b/food_vouchers.go index 69483ff..544b5eb 100644 --- a/food_vouchers.go +++ b/food_vouchers.go @@ -53,7 +53,7 @@ func addValesDeDespensa(doc *Document, fvc *mx.FoodVouchers) { doc.VDNamespace = VDNamespace doc.SchemaLocation = doc.SchemaLocation + " " + format.SchemaLocation(VDNamespace, VDSchemaLocation) - doc.Complementos = append(doc.Complementos, &ContentWrapper{vd}) + doc.AppendComplemento(vd) } func newVDConceptos(lines []*mx.FoodVouchersLine) []*VDConcepto { diff --git a/food_vouchers_test.go b/food_vouchers_test.go deleted file mode 100644 index 0c4c585..0000000 --- a/food_vouchers_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package cfdi_test - -import ( - "testing" - - cfdi "github.com/invopop/gobl.cfdi" - "github.com/invopop/gobl.cfdi/test" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestValesDeDespensa(t *testing.T) { - t.Run("should return a Document with the ValesDeDespensa data", func(t *testing.T) { - doc, err := test.NewDocumentFrom("food-vouchers.json") - require.NoError(t, err) - - require.Equal(t, 1, len(doc.Complementos)) - - vd := doc.Complementos[0].Content.(*cfdi.ValesDeDespensa) - - assert.Equal(t, "12345678901234567890", vd.RegistroPatronal) - assert.Equal(t, "0123456789", vd.NumeroDeCuenta) - assert.Equal(t, "30.52", vd.Total) - - require.Equal(t, 2, len(vd.Conceptos)) - - c := vd.Conceptos[0] - - assert.Equal(t, "ABC1234", c.Identificador) - assert.Equal(t, "2022-07-19T10:20:30", c.Fecha) - assert.Equal(t, "JUFA7608212V6", c.Rfc) - assert.Equal(t, "JUFA760821MDFRRR00", c.Curp) - assert.Equal(t, "Adriana Juarez Fernández", c.Nombre) - assert.Equal(t, "12345678901", c.NumSeguridadSocial) - assert.Equal(t, "10.12", c.Importe) - }) -} diff --git a/fuel_account_balance.go b/fuel_account_balance.go index fcaee78..ab06c79 100644 --- a/fuel_account_balance.go +++ b/fuel_account_balance.go @@ -66,7 +66,7 @@ func addEstadoCuentaCombustible(doc *Document, fc *mx.FuelAccountBalance) { doc.ECCNamespace = ECCNamespace doc.SchemaLocation = doc.SchemaLocation + " " + format.SchemaLocation(ECCNamespace, ECCSchemaLocation) - doc.Complementos = append(doc.Complementos, &ContentWrapper{ecc}) + doc.AppendComplemento(ecc) } // nolint:misspell diff --git a/fuel_account_balance_test.go b/fuel_account_balance_test.go deleted file mode 100644 index 16ff2fe..0000000 --- a/fuel_account_balance_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package cfdi_test - -import ( - "testing" - - cfdi "github.com/invopop/gobl.cfdi" - "github.com/invopop/gobl.cfdi/test" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestEstadoDeCuentaCombustible(t *testing.T) { - t.Run("should return a Document with the EstadoDeCuentaCombustible data", func(t *testing.T) { - doc, err := test.NewDocumentFrom("fuel-account-balance.json") - require.NoError(t, err) - - require.Equal(t, 1, len(doc.Complementos)) - - ecc := doc.Complementos[0].Content.(*cfdi.EstadoDeCuentaCombustible) - - assert.Equal(t, "0123456789", ecc.NumeroDeCuenta) - assert.Equal(t, "246.13", ecc.SubTotal) - assert.Equal(t, "400.00", ecc.Total) - - require.Equal(t, 2, len(ecc.Conceptos)) - - c := ecc.Conceptos[0] - - assert.Equal(t, "1234", c.Identificador) - assert.Equal(t, "2022-07-19T10:20:30", c.Fecha) - assert.Equal(t, "RWT860605OF5", c.Rfc) - assert.Equal(t, "8171650", c.ClaveEstacion) - assert.Equal(t, "9.661", c.Cantidad) - assert.Equal(t, "3", c.TipoCombustible) - assert.Equal(t, "Diesel", c.NombreCombustible) - assert.Equal(t, "2794668", c.FolioOperacion) - assert.Equal(t, "12.743", c.ValorUnitario) - assert.Equal(t, "123.11", c.Importe) - assert.Equal(t, "LTR", c.Unidad) - - require.Equal(t, 2, len(c.Traslados)) - - ct := c.Traslados[0] - - assert.Equal(t, "IVA", ct.Impuesto) - assert.Equal(t, "0.160000", ct.TasaOCuota) - assert.Equal(t, "19.70", ct.Importe) - }) -} diff --git a/internal/nodes.go b/internal/nodes.go new file mode 100644 index 0000000..87b7203 --- /dev/null +++ b/internal/nodes.go @@ -0,0 +1,7 @@ +package internal + +// Nodes is an auxiliary struct to marshal a sequence of arbitrary XML nodes, +// like the ones inside `cfdi:Complemento` or `cfdi:Addenda`. +type Nodes struct { + Nodes []interface{} `xml:",omitempty"` +} From 27b026b8a3a30c49cae102507d91ec92c182f2ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Tue, 21 Nov 2023 14:48:43 +0000 Subject: [PATCH 14/19] Avoid unrecommended use of pointers to slices --- addendas/mabe.go | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/addendas/mabe.go b/addendas/mabe.go index 7006c56..19d8954 100644 --- a/addendas/mabe.go +++ b/addendas/mabe.go @@ -52,15 +52,15 @@ type MabeFactura struct { Referencia1 string `xml:"referencia1,attr"` Referencia2 string `xml:"referencia2,attr,omitempty"` - Moneda *MabeMoneda `xml:"mabe:Moneda"` - Proveedor *MabeProveedor `xml:"mabe:Proveedor"` - Entrega *MabeEntrega `xml:"mabe:Entrega"` - Detalles *[]*MabeDetalle `xml:"mabe:Detalles>mabe:Detalle"` + Moneda *MabeMoneda `xml:"mabe:Moneda"` + Proveedor *MabeProveedor `xml:"mabe:Proveedor"` + Entrega *MabeEntrega `xml:"mabe:Entrega"` + Detalles *MabeDetalles `xml:"mabe:Detalles"` Descuentos *MabeDescuentos `xml:"mabe:Descuentos,omitempty"` Subtotal *MabeImporte `xml:"mabe:Subtotal"` - Traslados *[]*MabeImpuesto `xml:"mabe:Traslados>mabe:Traslado"` - Retenciones *[]*MabeImpuesto `xml:"mabe:Retenciones>mabe:Retencion"` + Traslados *MabeTraslados `xml:"mabe:Traslados"` + Retenciones *MabeRetenciones `xml:"mabe:Retenciones"` Total *MabeImporte `xml:"mabe:Total"` } @@ -85,6 +85,11 @@ type MabeEntrega struct { CodigoPostal string `xml:"codigoPostal,attr,omitempty"` } +// MabeDetalles carries the data about an invoice's lines +type MabeDetalles struct { + Detalle []*MabeDetalle `xml:"mabe:Detalle"` +} + // MabeDetalle carries the data about one invoice's line type MabeDetalle struct { NoLineaArticulo int `xml:"noLineaArticulo,attr"` @@ -103,6 +108,16 @@ type MabeImporte struct { Importe string `xml:"importe,attr"` } +// MabeTraslados carries the data about an invoice's taxes (expect retained ones) +type MabeTraslados struct { + Traslado []*MabeImpuesto `xml:"mabe:Traslado"` +} + +// MabeRetenciones carries the data about an invoice's retained taxes +type MabeRetenciones struct { + Retencion []*MabeImpuesto `xml:"mabe:Retencion"` +} + // MabeImpuesto carries the data about an invoice's tax type MabeImpuesto struct { Tipo string `xml:"tipo,attr"` @@ -229,7 +244,7 @@ func newMabeDescuentos(inv *bill.Invoice) *MabeDescuentos { } } -func newMabeDetalles(inv *bill.Invoice) *[]*MabeDetalle { +func newMabeDetalles(inv *bill.Invoice) *MabeDetalles { var detalles []*MabeDetalle for _, line := range inv.Lines { @@ -247,7 +262,11 @@ func newMabeDetalles(inv *bill.Invoice) *[]*MabeDetalle { detalles = append(detalles, d) } - return &detalles + if len(detalles) == 0 { + return nil + } + + return &MabeDetalles{detalles} } func newMabeImporte(amount num.Amount) *MabeImporte { @@ -278,11 +297,11 @@ func setMabeTaxes(inv *bill.Invoice, mabe *MabeFactura) { } if len(traslados) > 0 { - mabe.Traslados = &traslados + mabe.Traslados = &MabeTraslados{traslados} } if len(retenciones) > 0 { - mabe.Retenciones = &retenciones + mabe.Retenciones = &MabeRetenciones{retenciones} } } From 2253453509291e7b3e1601f194b91931b4ee0ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Wed, 22 Nov 2023 22:46:41 +0000 Subject: [PATCH 15/19] Check invoice is valid for generating addenda Mabe --- addendas/mabe.go | 112 +++++++++++++++++++++++++++++++++++++++--- addendas/mabe_test.go | 80 ++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 2 + 4 files changed, 188 insertions(+), 8 deletions(-) create mode 100644 addendas/mabe_test.go diff --git a/addendas/mabe.go b/addendas/mabe.go index 19d8954..bf5d868 100644 --- a/addendas/mabe.go +++ b/addendas/mabe.go @@ -2,7 +2,6 @@ package addendas import ( "encoding/xml" - "errors" "fmt" "github.com/invopop/gobl.cfdi/internal" @@ -12,6 +11,7 @@ import ( "github.com/invopop/gobl/i18n" "github.com/invopop/gobl/num" "github.com/invopop/gobl/org" + "github.com/invopop/validation" ) // Mabe schema constants @@ -29,6 +29,13 @@ const ( MabeTipoDocumentoNotaCargo = "NOTA CARGO" ) +// Maps the GOBL invoice types to Mabe's TipoDocumento +var MabeTipoDocumentoMap = map[cbc.Key]string{ + bill.InvoiceTypeStandard: MabeTipoDocumentoFactura, + bill.InvoiceTypeCreditNote: MabeTipoDocumentoNotaCredito, + bill.InvoiceTypeDebitNote: MabeTipoDocumentoNotaCargo, +} + // Mabe specific identity codes. const ( MabeKeyIdentityProviderID = "mx-mabe-provider-id" @@ -142,13 +149,9 @@ func isMabe(inv *bill.Invoice) bool { // newMabe provides a new Mabe addenda. func newMabe(inv *bill.Invoice) (*MabeFactura, error) { - tipoDocumento, err := mapMabeTipoDocumento(inv) - if err != nil { + if err := validateInvoiceForMabe(inv); err != nil { return nil, err } - if inv.Ordering == nil { - return nil, errors.New("missing ordering field") - } // Ref2 is not currently used by Mabe, so we set the default // value to "NA". @@ -162,7 +165,7 @@ func newMabe(inv *bill.Invoice) (*MabeFactura, error) { SchemaLocation: format.SchemaLocation(MabeNamespace, MabeSchemaLocation), Version: MabeVersion, - TipoDocumento: tipoDocumento, + TipoDocumento: MabeTipoDocumentoMap[inv.Type], Folio: formatMabeFolio(inv), Fecha: inv.IssueDate.String(), OrdenCompra: inv.Ordering.Code, @@ -184,6 +187,101 @@ func newMabe(inv *bill.Invoice) (*MabeFactura, error) { return f, nil } +func validateInvoiceForMabe(inv *bill.Invoice) error { + return validation.ValidateStruct(inv, + validation.Field(&inv.Type, validation.In(validMabeInvoiceTypes()...)), + validation.Field(&inv.Supplier, + validation.By(validateSupplierForMabe), + ), + validation.Field(&inv.Lines, + validation.Each(validation.By(validateLineForMabe), validation.Skip), + validation.Skip, // prevent GOBL validations from running + ), + validation.Field(&inv.Ordering, + validation.Required, + validation.By(validateOrderingForMabe), + ), + validation.Field(&inv.Delivery, + validation.Required, + validation.By(validateDeliveryForMabe), + ), + ) +} + +func validateSupplierForMabe(value interface{}) error { + sup, _ := value.(*org.Party) + if sup == nil { + return nil + } + return validation.ValidateStruct(sup, + validation.Field(&sup.Identities, org.HasIdentityKey(MabeKeyIdentityProviderID)), + ) +} + +func validateLineForMabe(value interface{}) error { + line, _ := value.(*bill.Line) + if line == nil { + return nil + } + return validation.ValidateStruct(line, + validation.Field(&line.Item, + validation.By(validateItemForMabe), + ), + ) +} + +func validateItemForMabe(value interface{}) error { + item, _ := value.(*org.Item) + if item == nil { + return nil + } + return validation.ValidateStruct(item, + validation.Field(&item.Identities, org.HasIdentityKey(MabeKeyIdentityItemID)), + ) +} + +func validateDeliveryForMabe(value interface{}) error { + del, _ := value.(*bill.Delivery) + if del == nil { + return nil + } + return validation.ValidateStruct(del, + validation.Field(&del.Receiver, + validation.Required, + validation.By(validateReceiverForMabe), + ), + ) +} + +func validateReceiverForMabe(value interface{}) error { + rec, _ := value.(*org.Party) + if rec == nil { + return nil + } + return validation.ValidateStruct(rec, + validation.Field(&rec.Identities, org.HasIdentityKey(MabeKeyIdentityPlantID)), + ) +} + +func validateOrderingForMabe(value interface{}) error { + ord, _ := value.(*bill.Ordering) + if ord == nil { + return nil + } + return validation.ValidateStruct(ord, + validation.Field(&ord.Code, validation.Required), + validation.Field(&ord.Identities, org.HasIdentityKey(MabeKeyIdentityRef1)), + ) +} + +func validMabeInvoiceTypes() []interface{} { + var types []interface{} + for t, _ := range MabeTipoDocumentoMap { + types = append(types, t) + } + return types +} + func mapMabeTipoDocumento(inv *bill.Invoice) (string, error) { switch inv.Type { case bill.InvoiceTypeStandard: diff --git a/addendas/mabe_test.go b/addendas/mabe_test.go new file mode 100644 index 0000000..4e720f7 --- /dev/null +++ b/addendas/mabe_test.go @@ -0,0 +1,80 @@ +package addendas_test + +import ( + "testing" + + "github.com/invopop/gobl.cfdi/addendas" + "github.com/invopop/gobl.cfdi/test" + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/org" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAddendaMabeValidation(t *testing.T) { + env, err := test.LoadTestEnvelope("bare-minimum-invoice.json") + require.NoError(t, err) + + inv := env.Extract().(*bill.Invoice) + + // Prepare the invoice to be raise all Mabe validation errors. + inv.Type = bill.InvoiceTypeProforma + inv.Supplier.Identities = []*org.Identity{ + { + Key: addendas.MabeKeyIdentityProviderID, + Code: "12345", + }, + } + + // Check every validation and then fix it. + assertValidationError(t, inv, "type: must be a valid value") + inv.Type = bill.InvoiceTypeStandard + + assertValidationError(t, inv, "delivery: cannot be blank") + inv.Delivery = &bill.Delivery{} + + assertValidationError(t, inv, "delivery: (receiver: cannot be blank") + inv.Delivery.Receiver = &org.Party{ + Name: "Test Receiver", + } + + assertValidationError(t, inv, "delivery: (receiver: (identities: missing key mx-mabe-plant-id") + inv.Delivery.Receiver.Identities = []*org.Identity{ + { + Key: addendas.MabeKeyIdentityPlantID, + Code: "S001", + }, + } + + assertValidationError(t, inv, "lines: (0: (item: (identities: missing key mx-mabe-item-id") + inv.Lines[0].Item.Identities = []*org.Identity{ + { + Key: addendas.MabeKeyIdentityItemID, + Code: "12345", + }, + } + + assertValidationError(t, inv, "ordering: cannot be blank") + inv.Ordering = &bill.Ordering{} + + assertValidationError(t, inv, "ordering: (code: cannot be blank") + inv.Ordering.Code = "12345" + + assertValidationError(t, inv, "ordering: (identities: missing key mx-mabe-ref1") + inv.Ordering.Identities = []*org.Identity{ + { + Key: addendas.MabeKeyIdentityRef1, + Code: "12345", + }, + } + + // All validation errors must be fixed by now. + _, err = addendas.For(inv) + require.NoError(t, err) +} + +func assertValidationError(t *testing.T, inv *bill.Invoice, expected string) { + _, err := addendas.For(inv) + require.Error(t, err) + assert.Contains(t, err.Error(), expected) +} diff --git a/go.mod b/go.mod index ff7d106..2754629 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/invopop/gobl.cfdi go 1.20 require ( - github.com/invopop/gobl v0.62.2-0.20231121175846-f10ffeb5a094 + github.com/invopop/gobl v0.62.2-0.20231122224337-f193b3066d06 github.com/joho/godotenv v1.5.1 github.com/magefile/mage v1.15.0 github.com/spf13/cobra v1.7.0 diff --git a/go.sum b/go.sum index b0ba4d7..fec83dd 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/invopop/gobl v0.62.2-0.20231121145421-5d7dce0fd42d h1:tPpq1L2YH8IyGS4 github.com/invopop/gobl v0.62.2-0.20231121145421-5d7dce0fd42d/go.mod h1:sEngvTr2gAxexosO0rmQInVSL8C613TUPvBcFT4xMyM= github.com/invopop/gobl v0.62.2-0.20231121175846-f10ffeb5a094 h1:jPOc6SxQap9sq01c3LodCQi0NdC5UpfwW0g2r8cQUYg= github.com/invopop/gobl v0.62.2-0.20231121175846-f10ffeb5a094/go.mod h1:sEngvTr2gAxexosO0rmQInVSL8C613TUPvBcFT4xMyM= +github.com/invopop/gobl v0.62.2-0.20231122224337-f193b3066d06 h1:QNX7G4qEmC+KeNjZD1feo6wxCAngu1vLugQFZpFuZd0= +github.com/invopop/gobl v0.62.2-0.20231122224337-f193b3066d06/go.mod h1:sEngvTr2gAxexosO0rmQInVSL8C613TUPvBcFT4xMyM= 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.3.0 h1:o260kbjXzoBO/ypXDSSrCLL7SxEFUXBsX09YTE9AxZw= From d7425f515f76716362073fb521390a8b337c76b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Wed, 22 Nov 2023 23:00:01 +0000 Subject: [PATCH 16/19] Fix linter offenses --- addendas/mabe.go | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/addendas/mabe.go b/addendas/mabe.go index bf5d868..93d82ac 100644 --- a/addendas/mabe.go +++ b/addendas/mabe.go @@ -29,7 +29,7 @@ const ( MabeTipoDocumentoNotaCargo = "NOTA CARGO" ) -// Maps the GOBL invoice types to Mabe's TipoDocumento +// MabeTipoDocumentoMap maps GOBL invoice types to Mabe's TipoDocumento var MabeTipoDocumentoMap = map[cbc.Key]string{ bill.InvoiceTypeStandard: MabeTipoDocumentoFactura, bill.InvoiceTypeCreditNote: MabeTipoDocumentoNotaCredito, @@ -276,25 +276,12 @@ func validateOrderingForMabe(value interface{}) error { func validMabeInvoiceTypes() []interface{} { var types []interface{} - for t, _ := range MabeTipoDocumentoMap { + for t := range MabeTipoDocumentoMap { types = append(types, t) } return types } -func mapMabeTipoDocumento(inv *bill.Invoice) (string, error) { - switch inv.Type { - case bill.InvoiceTypeStandard: - return MabeTipoDocumentoFactura, nil - case bill.InvoiceTypeCreditNote: - return MabeTipoDocumentoNotaCredito, nil - case bill.InvoiceTypeDebitNote: - return MabeTipoDocumentoNotaCargo, nil - default: - return "", fmt.Errorf("invalid invoice type: %s", inv.Type) - } -} - func newMabeMoneda(inv *bill.Invoice) *MabeMoneda { return &MabeMoneda{TipoMoneda: string(inv.Currency)} } From addc9cdf76880ae90898c20476a297c4803d9707 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Thu, 23 Nov 2023 15:08:10 +0000 Subject: [PATCH 17/19] Using instead of ordering.code --- addendas/README.md | 16 ++++++++-------- addendas/mabe.go | 9 ++++++--- addendas/mabe_test.go | 13 ++++++++----- test/data/addenda-mabe.json | 5 ++++- test/data/bare-minimum-addenda-mabe.json | 5 ++++- 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/addendas/README.md b/addendas/README.md index ea3a2c8..87627ee 100644 --- a/addendas/README.md +++ b/addendas/README.md @@ -8,11 +8,11 @@ Each of the addendas currently supported are listed below, with instructions on Most of the MABE Addenda fields are determined automatically from the base GOBL Invoice, with the exception of the following: -| MABE Field | GOBL Invoice Property | GOBL Invoice Value | Description | -| -------------- | ------------------------------ | ----------------------------------------------- | ------------------------------------------------------------------------- | -| Order Code | `ordering.code` | `"-CODE-"` | Provided by Mabe for the order | -| Provider Code | `supplier.identities` | `[{"key":"mx-mabe-provider", "code":"-CODE-"}]` | Code issued by Mabe to identify the supplier | -| Delivery Plant | `delivery.receiver.identities` | `[{"key":"mx-mabe-plant","code":"-CODE-"}]` | Delivery Plant Code | -| Item Code | `lines[i].item.identities` | `[{"key":"mx-mabe-item","code":"-CODE-"}]` | Article code provided by Mabe | -| Reference 1 | `ordering.identities` | `[{"key":"mx-mabe-ref1","code":"-CODE-"}]` | Additional code required by Mabe in certain circumstances while ordering. | -| Reference 2 | `ordering.identities` | `[{"key":"mx-mabe-ref2","code":"-CODE-"}]` | Set to `NA` by default as not currently used by Mabe. | +| MABE Field | GOBL Invoice Property | GOBL Invoice Value | Description | +| -------------- | ------------------------------ | -------------------------------------------------- | ------------------------------------------------------------------------- | +| Order Code | `ordering.identities` | `[{"key":"mx-mabe-order-id", "code":"-CODE-"}]` | Provided by Mabe for the order | +| Provider Code | `supplier.identities` | `[{"key":"mx-mabe-provider-id", "code":"-CODE-"}]` | Code issued by Mabe to identify the supplier | +| Delivery Plant | `delivery.receiver.identities` | `[{"key":"mx-mabe-plant-id","code":"-CODE-"}]` | Delivery Plant Code | +| Item Code | `lines[i].item.identities` | `[{"key":"mx-mabe-item-id","code":"-CODE-"}]` | Article code provided by Mabe | +| Reference 1 | `ordering.identities` | `[{"key":"mx-mabe-ref1","code":"-CODE-"}]` | Additional code required by Mabe in certain circumstances while ordering. | +| Reference 2 | `ordering.identities` | `[{"key":"mx-mabe-ref2","code":"-CODE-"}]` | Set to `NA` by default as not currently used by Mabe. | diff --git a/addendas/mabe.go b/addendas/mabe.go index 93d82ac..1f212e8 100644 --- a/addendas/mabe.go +++ b/addendas/mabe.go @@ -38,6 +38,7 @@ var MabeTipoDocumentoMap = map[cbc.Key]string{ // Mabe specific identity codes. const ( + MabeKeyIdentityOrderID = "mx-mabe-order-id" MabeKeyIdentityProviderID = "mx-mabe-provider-id" MabeKeyIdentityRef1 = "mx-mabe-ref1" MabeKeyIdentityRef2 = "mx-mabe-ref2" @@ -168,7 +169,7 @@ func newMabe(inv *bill.Invoice) (*MabeFactura, error) { TipoDocumento: MabeTipoDocumentoMap[inv.Type], Folio: formatMabeFolio(inv), Fecha: inv.IssueDate.String(), - OrdenCompra: inv.Ordering.Code, + OrdenCompra: extractIdentity(inv.Ordering.Identities, MabeKeyIdentityOrderID).String(), Referencia1: extractIdentity(inv.Ordering.Identities, MabeKeyIdentityRef1).String(), Referencia2: ref2.String(), @@ -269,8 +270,10 @@ func validateOrderingForMabe(value interface{}) error { return nil } return validation.ValidateStruct(ord, - validation.Field(&ord.Code, validation.Required), - validation.Field(&ord.Identities, org.HasIdentityKey(MabeKeyIdentityRef1)), + validation.Field(&ord.Identities, + org.HasIdentityKey(MabeKeyIdentityOrderID), + org.HasIdentityKey(MabeKeyIdentityRef1), + ), ) } diff --git a/addendas/mabe_test.go b/addendas/mabe_test.go index 4e720f7..48b9ad3 100644 --- a/addendas/mabe_test.go +++ b/addendas/mabe_test.go @@ -57,17 +57,20 @@ func TestAddendaMabeValidation(t *testing.T) { assertValidationError(t, inv, "ordering: cannot be blank") inv.Ordering = &bill.Ordering{} - assertValidationError(t, inv, "ordering: (code: cannot be blank") - inv.Ordering.Code = "12345" - - assertValidationError(t, inv, "ordering: (identities: missing key mx-mabe-ref1") + assertValidationError(t, inv, "ordering: (identities: missing key mx-mabe-order-id.)") inv.Ordering.Identities = []*org.Identity{ { - Key: addendas.MabeKeyIdentityRef1, + Key: addendas.MabeKeyIdentityOrderID, Code: "12345", }, } + assertValidationError(t, inv, "ordering: (identities: missing key mx-mabe-ref1") + inv.Ordering.Identities = append(inv.Ordering.Identities, &org.Identity{ + Key: addendas.MabeKeyIdentityRef1, + Code: "12345", + }) + // All validation errors must be fixed by now. _, err = addendas.For(inv) require.NoError(t, err) diff --git a/test/data/addenda-mabe.json b/test/data/addenda-mabe.json index b6b5b82..4eefaba 100644 --- a/test/data/addenda-mabe.json +++ b/test/data/addenda-mabe.json @@ -87,8 +87,11 @@ } ], "ordering": { - "code": "9100000000", "identities": [ + { + "key": "mx-mabe-order-id", + "code": "9100000000" + }, { "key": "mx-mabe-ref1", "code": "123456" diff --git a/test/data/bare-minimum-addenda-mabe.json b/test/data/bare-minimum-addenda-mabe.json index 1fc87ca..0bc5d42 100644 --- a/test/data/bare-minimum-addenda-mabe.json +++ b/test/data/bare-minimum-addenda-mabe.json @@ -73,8 +73,11 @@ } ], "ordering": { - "code": "91000000", "identities": [ + { + "key": "mx-mabe-order-id", + "code": "91000000" + }, { "key": "mx-mabe-ref1", "code": "900900" From 7d08ea0c1cc89d255efb35beb94909b8c2c0a659 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Thu, 23 Nov 2023 15:39:59 +0000 Subject: [PATCH 18/19] Standardizing on direct translation of identity keys --- addendas/README.md | 16 ++++++------- addendas/mabe.go | 30 ++++++++++++------------ addendas/mabe_test.go | 16 ++++++------- test/data/addenda-mabe.json | 12 +++++----- test/data/bare-minimum-addenda-mabe.json | 10 ++++---- 5 files changed, 42 insertions(+), 42 deletions(-) diff --git a/addendas/README.md b/addendas/README.md index 87627ee..48dfe35 100644 --- a/addendas/README.md +++ b/addendas/README.md @@ -8,11 +8,11 @@ Each of the addendas currently supported are listed below, with instructions on Most of the MABE Addenda fields are determined automatically from the base GOBL Invoice, with the exception of the following: -| MABE Field | GOBL Invoice Property | GOBL Invoice Value | Description | -| -------------- | ------------------------------ | -------------------------------------------------- | ------------------------------------------------------------------------- | -| Order Code | `ordering.identities` | `[{"key":"mx-mabe-order-id", "code":"-CODE-"}]` | Provided by Mabe for the order | -| Provider Code | `supplier.identities` | `[{"key":"mx-mabe-provider-id", "code":"-CODE-"}]` | Code issued by Mabe to identify the supplier | -| Delivery Plant | `delivery.receiver.identities` | `[{"key":"mx-mabe-plant-id","code":"-CODE-"}]` | Delivery Plant Code | -| Item Code | `lines[i].item.identities` | `[{"key":"mx-mabe-item-id","code":"-CODE-"}]` | Article code provided by Mabe | -| Reference 1 | `ordering.identities` | `[{"key":"mx-mabe-ref1","code":"-CODE-"}]` | Additional code required by Mabe in certain circumstances while ordering. | -| Reference 2 | `ordering.identities` | `[{"key":"mx-mabe-ref2","code":"-CODE-"}]` | Set to `NA` by default as not currently used by Mabe. | +| MABE Field | GOBL Invoice Property | GOBL Invoice Value | Description | +| ----------------------------------- | ------------------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------- | +| Purchase Order (Ordén de Compra) | `ordering.identities` | `[{"key":"mx-mabe-purchase-order", "code":"-CODE-"}]` | Provided by Mabe for the order | +| Provider Code (Código de Proveedor) | `supplier.identities` | `[{"key":"mx-mabe-provider-code", "code":"-CODE-"}]` | Code issued by Mabe to identify the supplier | +| Delivery Plant (Planta de Entrega) | `delivery.receiver.identities` | `[{"key":"mx-mabe-delivery-plant","code":"-CODE-"}]` | Delivery Plant Code | +| Article Code (Código de Artículo) | `lines[i].item.identities` | `[{"key":"mx-mabe-article-code","code":"-CODE-"}]` | Article code provided by Mabe | +| Reference 1 | `ordering.identities` | `[{"key":"mx-mabe-reference1","code":"-CODE-"}]` | Additional code required by Mabe in certain circumstances while ordering. | +| Reference 2 | `ordering.identities` | `[{"key":"mx-mabe-reference2","code":"-CODE-"}]` | Set to `NA` by default as not currently used by Mabe. | diff --git a/addendas/mabe.go b/addendas/mabe.go index 1f212e8..b636f4c 100644 --- a/addendas/mabe.go +++ b/addendas/mabe.go @@ -38,12 +38,12 @@ var MabeTipoDocumentoMap = map[cbc.Key]string{ // Mabe specific identity codes. const ( - MabeKeyIdentityOrderID = "mx-mabe-order-id" - MabeKeyIdentityProviderID = "mx-mabe-provider-id" - MabeKeyIdentityRef1 = "mx-mabe-ref1" - MabeKeyIdentityRef2 = "mx-mabe-ref2" - MabeKeyIdentityPlantID = "mx-mabe-plant-id" - MabeKeyIdentityItemID = "mx-mabe-item-id" + MabeKeyIdentityPurchaseOrder = "mx-mabe-purchase-order" + MabeKeyIdentityProviderCode = "mx-mabe-provider-code" + MabeKeyIdentityRef1 = "mx-mabe-reference1" + MabeKeyIdentityRef2 = "mx-mabe-reference2" + MabeKeyIdentityDeliveryPlant = "mx-mabe-delivery-plant" + MabeKeyIdentityArticleCode = "mx-mabe-article-code" ) // MabeFactura is the root element of the Mabe addendum @@ -144,7 +144,7 @@ func isMabe(inv *bill.Invoice) bool { if inv.Supplier == nil { return false } - id := extractIdentity(inv.Supplier.Identities, MabeKeyIdentityProviderID) + id := extractIdentity(inv.Supplier.Identities, MabeKeyIdentityProviderCode) return id != cbc.CodeEmpty } @@ -169,7 +169,7 @@ func newMabe(inv *bill.Invoice) (*MabeFactura, error) { TipoDocumento: MabeTipoDocumentoMap[inv.Type], Folio: formatMabeFolio(inv), Fecha: inv.IssueDate.String(), - OrdenCompra: extractIdentity(inv.Ordering.Identities, MabeKeyIdentityOrderID).String(), + OrdenCompra: extractIdentity(inv.Ordering.Identities, MabeKeyIdentityPurchaseOrder).String(), Referencia1: extractIdentity(inv.Ordering.Identities, MabeKeyIdentityRef1).String(), Referencia2: ref2.String(), @@ -215,7 +215,7 @@ func validateSupplierForMabe(value interface{}) error { return nil } return validation.ValidateStruct(sup, - validation.Field(&sup.Identities, org.HasIdentityKey(MabeKeyIdentityProviderID)), + validation.Field(&sup.Identities, org.HasIdentityKey(MabeKeyIdentityProviderCode)), ) } @@ -237,7 +237,7 @@ func validateItemForMabe(value interface{}) error { return nil } return validation.ValidateStruct(item, - validation.Field(&item.Identities, org.HasIdentityKey(MabeKeyIdentityItemID)), + validation.Field(&item.Identities, org.HasIdentityKey(MabeKeyIdentityArticleCode)), ) } @@ -260,7 +260,7 @@ func validateReceiverForMabe(value interface{}) error { return nil } return validation.ValidateStruct(rec, - validation.Field(&rec.Identities, org.HasIdentityKey(MabeKeyIdentityPlantID)), + validation.Field(&rec.Identities, org.HasIdentityKey(MabeKeyIdentityDeliveryPlant)), ) } @@ -271,7 +271,7 @@ func validateOrderingForMabe(value interface{}) error { } return validation.ValidateStruct(ord, validation.Field(&ord.Identities, - org.HasIdentityKey(MabeKeyIdentityOrderID), + org.HasIdentityKey(MabeKeyIdentityPurchaseOrder), org.HasIdentityKey(MabeKeyIdentityRef1), ), ) @@ -293,7 +293,7 @@ func newMabeProveedor(inv *bill.Invoice) *MabeProveedor { if inv.Supplier == nil { return nil } - id := extractIdentity(inv.Supplier.Identities, MabeKeyIdentityProviderID) + id := extractIdentity(inv.Supplier.Identities, MabeKeyIdentityProviderCode) return &MabeProveedor{ Codigo: id.String(), } @@ -301,7 +301,7 @@ func newMabeProveedor(inv *bill.Invoice) *MabeProveedor { func newMabeEntrega(inv *bill.Invoice) *MabeEntrega { rec := inv.Delivery.Receiver - id := extractIdentity(rec.Identities, MabeKeyIdentityPlantID) + id := extractIdentity(rec.Identities, MabeKeyIdentityDeliveryPlant) e := &MabeEntrega{ PlantaEntrega: id.String(), } @@ -336,7 +336,7 @@ func newMabeDetalles(inv *bill.Invoice) *MabeDetalles { var detalles []*MabeDetalle for _, line := range inv.Lines { - id := extractIdentity(line.Item.Identities, MabeKeyIdentityItemID) + id := extractIdentity(line.Item.Identities, MabeKeyIdentityArticleCode) d := &MabeDetalle{ NoLineaArticulo: line.Index, CodigoArticulo: id.String(), diff --git a/addendas/mabe_test.go b/addendas/mabe_test.go index 48b9ad3..a56c84b 100644 --- a/addendas/mabe_test.go +++ b/addendas/mabe_test.go @@ -21,7 +21,7 @@ func TestAddendaMabeValidation(t *testing.T) { inv.Type = bill.InvoiceTypeProforma inv.Supplier.Identities = []*org.Identity{ { - Key: addendas.MabeKeyIdentityProviderID, + Key: addendas.MabeKeyIdentityProviderCode, Code: "12345", }, } @@ -38,18 +38,18 @@ func TestAddendaMabeValidation(t *testing.T) { Name: "Test Receiver", } - assertValidationError(t, inv, "delivery: (receiver: (identities: missing key mx-mabe-plant-id") + assertValidationError(t, inv, "delivery: (receiver: (identities: missing key mx-mabe-delivery-plant") inv.Delivery.Receiver.Identities = []*org.Identity{ { - Key: addendas.MabeKeyIdentityPlantID, + Key: addendas.MabeKeyIdentityDeliveryPlant, Code: "S001", }, } - assertValidationError(t, inv, "lines: (0: (item: (identities: missing key mx-mabe-item-id") + assertValidationError(t, inv, "lines: (0: (item: (identities: missing key mx-mabe-article-code") inv.Lines[0].Item.Identities = []*org.Identity{ { - Key: addendas.MabeKeyIdentityItemID, + Key: addendas.MabeKeyIdentityArticleCode, Code: "12345", }, } @@ -57,15 +57,15 @@ func TestAddendaMabeValidation(t *testing.T) { assertValidationError(t, inv, "ordering: cannot be blank") inv.Ordering = &bill.Ordering{} - assertValidationError(t, inv, "ordering: (identities: missing key mx-mabe-order-id.)") + assertValidationError(t, inv, "ordering: (identities: missing key mx-mabe-purchase-order.)") inv.Ordering.Identities = []*org.Identity{ { - Key: addendas.MabeKeyIdentityOrderID, + Key: addendas.MabeKeyIdentityPurchaseOrder, Code: "12345", }, } - assertValidationError(t, inv, "ordering: (identities: missing key mx-mabe-ref1") + assertValidationError(t, inv, "ordering: (identities: missing key mx-mabe-reference1") inv.Ordering.Identities = append(inv.Ordering.Identities, &org.Identity{ Key: addendas.MabeKeyIdentityRef1, Code: "12345", diff --git a/test/data/addenda-mabe.json b/test/data/addenda-mabe.json index 4eefaba..f869c59 100644 --- a/test/data/addenda-mabe.json +++ b/test/data/addenda-mabe.json @@ -24,7 +24,7 @@ }, "identities": [ { - "key": "mx-mabe-provider-id", + "key": "mx-mabe-provider-code", "code": "123456" } ], @@ -53,7 +53,7 @@ "price": "100.00", "identities": [ { - "key": "mx-mabe-item-id", + "key": "mx-mabe-article-code", "code": "CODE123" } ], @@ -89,15 +89,15 @@ "ordering": { "identities": [ { - "key": "mx-mabe-order-id", + "key": "mx-mabe-purchase-order", "code": "9100000000" }, { - "key": "mx-mabe-ref1", + "key": "mx-mabe-reference1", "code": "123456" }, { - "key": "mx-mabe-ref2", + "key": "mx-mabe-reference2", "code": "654321" } ] @@ -119,7 +119,7 @@ ], "identities": [ { - "key": "mx-mabe-plant-id", + "key": "mx-mabe-delivery-plant", "code": "S001" } ] diff --git a/test/data/bare-minimum-addenda-mabe.json b/test/data/bare-minimum-addenda-mabe.json index 0bc5d42..4cc61f3 100644 --- a/test/data/bare-minimum-addenda-mabe.json +++ b/test/data/bare-minimum-addenda-mabe.json @@ -24,7 +24,7 @@ }, "identities": [ { - "key": "mx-mabe-provider-id", + "key": "mx-mabe-provider-code", "code": "123456" } ], @@ -53,7 +53,7 @@ "price": "100.00", "identities": [ { - "key": "mx-mabe-item-id", + "key": "mx-mabe-article-code", "code": "CODE123" } ], @@ -75,11 +75,11 @@ "ordering": { "identities": [ { - "key": "mx-mabe-order-id", + "key": "mx-mabe-purchase-order", "code": "91000000" }, { - "key": "mx-mabe-ref1", + "key": "mx-mabe-reference1", "code": "900900" } ] @@ -94,7 +94,7 @@ "name": "ESTUFAS 30", "identities": [ { - "key": "mx-mabe-plant-id", + "key": "mx-mabe-delivery-plant", "code": "S001" } ] From decc08f66f5e6ceca495409bb1bb2363efeb6071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Fri, 24 Nov 2023 10:25:41 +0000 Subject: [PATCH 19/19] Upgrade to latest GOBL --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 2754629..94e5dd7 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/invopop/gobl.cfdi go 1.20 require ( - github.com/invopop/gobl v0.62.2-0.20231122224337-f193b3066d06 + github.com/invopop/gobl v0.63.0 github.com/joho/godotenv v1.5.1 github.com/magefile/mage v1.15.0 github.com/spf13/cobra v1.7.0 diff --git a/go.sum b/go.sum index fec83dd..b304ee0 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/invopop/gobl v0.62.2-0.20231121175846-f10ffeb5a094 h1:jPOc6SxQap9sq01 github.com/invopop/gobl v0.62.2-0.20231121175846-f10ffeb5a094/go.mod h1:sEngvTr2gAxexosO0rmQInVSL8C613TUPvBcFT4xMyM= github.com/invopop/gobl v0.62.2-0.20231122224337-f193b3066d06 h1:QNX7G4qEmC+KeNjZD1feo6wxCAngu1vLugQFZpFuZd0= github.com/invopop/gobl v0.62.2-0.20231122224337-f193b3066d06/go.mod h1:sEngvTr2gAxexosO0rmQInVSL8C613TUPvBcFT4xMyM= +github.com/invopop/gobl v0.63.0 h1:eH75trQhOtCVyp2bK+ESE1AJZ9TwBXLdBsIOAfagts4= +github.com/invopop/gobl v0.63.0/go.mod h1:sEngvTr2gAxexosO0rmQInVSL8C613TUPvBcFT4xMyM= 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.3.0 h1:o260kbjXzoBO/ypXDSSrCLL7SxEFUXBsX09YTE9AxZw=