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: diff --git a/addendas/README.md b/addendas/README.md new file mode 100644 index 0000000..48dfe35 --- /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 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/addendas.go b/addendas/addendas.go new file mode 100644 index 0000000..4dc3e99 --- /dev/null +++ b/addendas/addendas.go @@ -0,0 +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, error) { + list := make([]any, 0) + + if isMabe(inv) { + ad, err := newMabe(inv) + if err != nil { + return nil, err + } + list = append(list, ad) + } + + return list, nil +} diff --git a/addendas/mabe.go b/addendas/mabe.go new file mode 100644 index 0000000..b636f4c --- /dev/null +++ b/addendas/mabe.go @@ -0,0 +1,410 @@ +package addendas + +import ( + "encoding/xml" + "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/org" + "github.com/invopop/validation" +) + +// 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" +) + +// MabeTipoDocumentoMap maps 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 ( + 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 +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 *MabeDetalles `xml:"mabe:Detalles"` + + Descuentos *MabeDescuentos `xml:"mabe:Descuentos,omitempty"` + Subtotal *MabeImporte `xml:"mabe:Subtotal"` + Traslados *MabeTraslados `xml:"mabe:Traslados"` + Retenciones *MabeRetenciones `xml:"mabe:Retenciones"` + 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"` +} + +// 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"` + 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"` +} + +// 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"` + 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 isMabe(inv *bill.Invoice) bool { + if inv.Supplier == nil { + return false + } + id := extractIdentity(inv.Supplier.Identities, MabeKeyIdentityProviderCode) + return id != cbc.CodeEmpty +} + +// newMabe provides a new Mabe addenda. +func newMabe(inv *bill.Invoice) (*MabeFactura, error) { + if err := validateInvoiceForMabe(inv); err != nil { + return nil, err + } + + // 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), + + Version: MabeVersion, + TipoDocumento: MabeTipoDocumentoMap[inv.Type], + Folio: formatMabeFolio(inv), + Fecha: inv.IssueDate.String(), + OrdenCompra: extractIdentity(inv.Ordering.Identities, MabeKeyIdentityPurchaseOrder).String(), + Referencia1: extractIdentity(inv.Ordering.Identities, MabeKeyIdentityRef1).String(), + Referencia2: ref2.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) + + 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(MabeKeyIdentityProviderCode)), + ) +} + +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(MabeKeyIdentityArticleCode)), + ) +} + +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(MabeKeyIdentityDeliveryPlant)), + ) +} + +func validateOrderingForMabe(value interface{}) error { + ord, _ := value.(*bill.Ordering) + if ord == nil { + return nil + } + return validation.ValidateStruct(ord, + validation.Field(&ord.Identities, + org.HasIdentityKey(MabeKeyIdentityPurchaseOrder), + org.HasIdentityKey(MabeKeyIdentityRef1), + ), + ) +} + +func validMabeInvoiceTypes() []interface{} { + var types []interface{} + for t := range MabeTipoDocumentoMap { + types = append(types, t) + } + return types +} + +func newMabeMoneda(inv *bill.Invoice) *MabeMoneda { + return &MabeMoneda{TipoMoneda: string(inv.Currency)} +} + +func newMabeProveedor(inv *bill.Invoice) *MabeProveedor { + if inv.Supplier == nil { + return nil + } + id := extractIdentity(inv.Supplier.Identities, MabeKeyIdentityProviderCode) + return &MabeProveedor{ + Codigo: id.String(), + } +} + +func newMabeEntrega(inv *bill.Invoice) *MabeEntrega { + rec := inv.Delivery.Receiver + id := extractIdentity(rec.Identities, MabeKeyIdentityDeliveryPlant) + e := &MabeEntrega{ + PlantaEntrega: id.String(), + } + + 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 := internal.TotalInvoiceDiscount(inv) + + if d.IsZero() { + return nil + } + + return &MabeDescuentos{ + Tipo: MabeNotApplicable, + Descripcion: MabeNotApplicable, //nolint:misspell + Importe: d.String(), + } +} + +func newMabeDetalles(inv *bill.Invoice) *MabeDetalles { + var detalles []*MabeDetalle + + for _, line := range inv.Lines { + id := extractIdentity(line.Item.Identities, MabeKeyIdentityArticleCode) + d := &MabeDetalle{ + NoLineaArticulo: line.Index, + CodigoArticulo: id.String(), + Descripcion: line.Item.Name, //nolint:misspell + Unidad: internal.ClaveUnidad(line), + Cantidad: line.Quantity.String(), + PrecioSinIva: line.Item.Price.String(), + ImporteSinIva: line.Sum.String(), + } + + detalles = append(detalles, d) + } + + if len(detalles) == 0 { + return nil + } + + return &MabeDetalles{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: format.TaxPercent(rate.Percent), + Importe: rate.Amount.String(), + } + + if catDef.Retained { + retenciones = append(retenciones, t) + } else { + traslados = append(traslados, t) + } + } + } + + if len(traslados) > 0 { + mabe.Traslados = &MabeTraslados{traslados} + } + + if len(retenciones) > 0 { + mabe.Retenciones = &MabeRetenciones{retenciones} + } +} + +func formatMabeFolio(inv *bill.Invoice) string { + return fmt.Sprintf("%s%s", inv.Series, inv.Code) +} + +func extractIdentity(ids []*org.Identity, key cbc.Key) cbc.Code { + if ids == nil { + return "" + } + for _, id := range ids { + if id.Key == key { + return id.Code + } + } + return "" +} diff --git a/addendas/mabe_test.go b/addendas/mabe_test.go new file mode 100644 index 0000000..a56c84b --- /dev/null +++ b/addendas/mabe_test.go @@ -0,0 +1,83 @@ +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.MabeKeyIdentityProviderCode, + 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-delivery-plant") + inv.Delivery.Receiver.Identities = []*org.Identity{ + { + Key: addendas.MabeKeyIdentityDeliveryPlant, + Code: "S001", + }, + } + + assertValidationError(t, inv, "lines: (0: (item: (identities: missing key mx-mabe-article-code") + inv.Lines[0].Item.Identities = []*org.Identity{ + { + Key: addendas.MabeKeyIdentityArticleCode, + Code: "12345", + }, + } + + assertValidationError(t, inv, "ordering: cannot be blank") + inv.Ordering = &bill.Ordering{} + + assertValidationError(t, inv, "ordering: (identities: missing key mx-mabe-purchase-order.)") + inv.Ordering.Identities = []*org.Identity{ + { + Key: addendas.MabeKeyIdentityPurchaseOrder, + Code: "12345", + }, + } + + assertValidationError(t, inv, "ordering: (identities: missing key mx-mabe-reference1") + 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) +} + +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/cfdi.go b/cfdi.go index a29ae24..c704089 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" @@ -67,7 +70,8 @@ type Document struct { Conceptos *Conceptos `xml:"cfdi:Conceptos"` //nolint:misspell Impuestos *Impuestos `xml:"cfdi:Impuestos,omitempty"` - Complementos []interface{} `xml:"cfdi:Complemento>*,omitempty"` + Complemento *internal.Nodes `xml:"cfdi:Complemento,omitempty"` + Addenda *internal.Nodes `xml:"cfdi:Addenda,omitempty"` } // NewDocument converts a GOBL envelope into a CFDI document @@ -77,13 +81,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), @@ -105,14 +109,18 @@ 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 { return nil, err } + if err := addAddendas(document, inv); err != nil { + return nil, err + } + return document, nil } @@ -126,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) { @@ -141,6 +169,18 @@ func addComplementos(doc *Document, complements []*schema.Object) error { return nil } +func addAddendas(doc *Document, inv *bill.Invoice) error { + ads, err := addendas.For(inv) + if err != nil { + return err + } + + for _, ad := range ads { + doc.AppendAddenda(ad) + } + return nil +} + func formatIssueDate(date cal.Date) string { dateTime := civil.DateTime{Date: date.Date, Time: civil.Time{}} return dateTime.String() @@ -196,15 +236,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/cfdi_test.go b/cfdi_test.go index 3f1c151..f9c5a24 100644 --- a/cfdi_test.go +++ b/cfdi_test.go @@ -25,14 +25,14 @@ 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) 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/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 } diff --git a/food_vouchers.go b/food_vouchers.go index 0c4756c..544b5eb 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,8 +52,8 @@ func addValesDeDespensa(doc *Document, fvc *mx.FoodVouchers) { } doc.VDNamespace = VDNamespace - doc.SchemaLocation = doc.SchemaLocation + " " + formatSchemaLocation(VDNamespace, VDSchemaLocation) - doc.Complementos = append(doc.Complementos, vd) + doc.SchemaLocation = doc.SchemaLocation + " " + format.SchemaLocation(VDNamespace, VDSchemaLocation) + 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 2e8c113..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].(*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 6f7c801..ab06c79 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,8 +65,8 @@ func addEstadoCuentaCombustible(doc *Document, fc *mx.FuelAccountBalance) { } doc.ECCNamespace = ECCNamespace - doc.SchemaLocation = doc.SchemaLocation + " " + formatSchemaLocation(ECCNamespace, ECCSchemaLocation) - doc.Complementos = append(doc.Complementos, ecc) + doc.SchemaLocation = doc.SchemaLocation + " " + format.SchemaLocation(ECCNamespace, ECCSchemaLocation) + doc.AppendComplemento(ecc) } // nolint:misspell diff --git a/fuel_account_balance_test.go b/fuel_account_balance_test.go deleted file mode 100644 index d9be309..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].(*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/go.mod b/go.mod index 5ca6ad2..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.61.0 + 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 @@ -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..b304ee0 100644 --- a/go.sum +++ b/go.sum @@ -19,12 +19,16 @@ 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/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.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/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= github.com/invopop/validation v0.3.0/go.mod h1:qIBG6APYLp2Wu3/96p3idYjP8ffTKVmQBfKiZbw0Hts= github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= 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..5d919d5 --- /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()) +} + +// ClaveProdServ 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/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"` +} diff --git a/lines.go b/lines.go index f8c3240..d7c9d3e 100644 --- a/lines.go +++ b/lines.go @@ -1,14 +1,9 @@ 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" -) - -// Default keys -const ( - DefaultClaveUnidad = "ZZ" // Mutuamente definida + "github.com/invopop/gobl/tax" ) // Conceptos list invoice lines @@ -28,56 +23,32 @@ 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), + 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: newImpuestosFromLine(line), + 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 -} diff --git a/taxes.go b/taxes.go index e426327..7e074c3 100644 --- a/taxes.go +++ b/taxes.go @@ -1,109 +1,138 @@ 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/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: format.TaxPercent(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, - TasaOCuota: formatTaxPercent(tax.Percent), + Impuesto: catDef.Map[mx.KeySATImpuesto].String(), + TasaOCuota: format.TaxPercent(tax.Percent), TipoFactor: TipoFactorTasa, } - return traslado -} - -func formatTaxPercent(percent *num.Percentage) string { - return percent.Amount.Rescale(6).String() + return i } diff --git a/test/data/addenda-mabe.json b/test/data/addenda-mabe.json new file mode 100644 index 0000000..f869c59 --- /dev/null +++ b/test/data/addenda-mabe.json @@ -0,0 +1,177 @@ +{ + "$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": [ + { + "key": "mx-mabe-provider-code", + "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": [ + { + "key": "mx-mabe-article-code", + "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": { + "identities": [ + { + "key": "mx-mabe-purchase-order", + "code": "9100000000" + }, + { + "key": "mx-mabe-reference1", + "code": "123456" + }, + { + "key": "mx-mabe-reference2", + "code": "654321" + } + ] + }, + "payment": { + "instructions": { + "key": "credit-transfer" + } + }, + "delivery": { + "receiver": { + "name": "ESTUFAS 30", + "addresses": [ + { + "street": "Calle 1", + "locality": "Mexico D.F.", + "code": "12345" + } + ], + "identities": [ + { + "key": "mx-mabe-delivery-plant", + "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" + } + } +} diff --git a/test/data/bare-minimum-addenda-mabe.json b/test/data/bare-minimum-addenda-mabe.json new file mode 100644 index 0000000..4cc61f3 --- /dev/null +++ b/test/data/bare-minimum-addenda-mabe.json @@ -0,0 +1,128 @@ +{ + "$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": [ + { + "key": "mx-mabe-provider-code", + "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": [ + { + "key": "mx-mabe-article-code", + "code": "CODE123" + } + ], + "ext": { + "mx-cfdi-prod-serv": "50211502" + } + }, + "sum": "200.00", + "taxes": [ + { + "cat": "VAT", + "rate": "standard", + "percent": "16.0%" + } + ], + "total": "200.00" + } + ], + "ordering": { + "identities": [ + { + "key": "mx-mabe-purchase-order", + "code": "91000000" + }, + { + "key": "mx-mabe-reference1", + "code": "900900" + } + ] + }, + "payment": { + "instructions": { + "key": "credit-transfer" + } + }, + "delivery": { + "receiver": { + "name": "ESTUFAS 30", + "identities": [ + { + "key": "mx-mabe-delivery-plant", + "code": "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" + } + } +} 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/addenda-mabe.xml b/test/data/out/addenda-mabe.xml new file mode 100644 index 0000000..f7a108f --- /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..bad05b5 --- /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/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 c7ef357..fe598a8 100644 --- a/test/data/out/invoice.xml +++ b/test/data/out/invoice.xml @@ -1,5 +1,5 @@ - + @@ -8,13 +8,20 @@ + + + + - + + + + + - \ No newline at end of file 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/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 52649f6..98ab699 100644 --- a/test/schema/schema.xsd +++ b/test/schema/schema.xsd @@ -1,7 +1,8 @@ - - - + + + + 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 + + + + + + + +