invoicehn 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +114 -1
- data/README.md +263 -6
- data/lib/invoicehn/authorization.rb +4 -4
- data/lib/invoicehn/cli/builder.rb +15 -2
- data/lib/invoicehn/cli/wizard.rb +1 -1
- data/lib/invoicehn/compliance/validator.rb +136 -14
- data/lib/invoicehn/correlative.rb +46 -10
- data/lib/invoicehn/document_reference.rb +83 -0
- data/lib/invoicehn/invoice.rb +59 -15
- data/lib/invoicehn/issuance.rb +155 -22
- data/lib/invoicehn/ledger.rb +9 -1
- data/lib/invoicehn/renderers/json.rb +5 -2
- data/lib/invoicehn/renderers/pdf.rb +25 -5
- data/lib/invoicehn/renderers/text.rb +64 -5
- data/lib/invoicehn/settings.rb +44 -0
- data/lib/invoicehn/storage/json_store.rb +22 -7
- data/lib/invoicehn/test_support/contracts.rb +253 -0
- data/lib/invoicehn/test_support/memory.rb +112 -0
- data/lib/invoicehn/testing.rb +140 -0
- data/lib/invoicehn/version.rb +1 -1
- data/lib/invoicehn.rb +28 -0
- metadata +12 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97a71a1ec9c4f50865c877824d8e0383eadd0586aafb647e01bc3d42cc160a7c
|
|
4
|
+
data.tar.gz: 5f2c1498b2ed86ab1845473badf845aae69ec3061de66c4e9328855e7d6e97ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 94039271cdd437cc609cada52de779c80aa55259f7f58cfd6d6247d34832002df9a07f4443c3cdf3ba6d9c2e46824628e9ccb5a9e917acdcdcf87a569ade6f0d
|
|
7
|
+
data.tar.gz: 3d96cadbcd9ddbd4463a904e44e134cb8649303de6f1aaa7d09d8f00288879828ab5a33427f89083c18afcc660c2a7cc62bec75d25990a39765bed5bbb72ac6b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,118 @@ Este proyecto sigue [Versionado Semántico](https://semver.org/lang/es/).
|
|
|
5
5
|
|
|
6
6
|
## [Sin publicar]
|
|
7
7
|
|
|
8
|
+
## [0.2.0] — 2026-10-10
|
|
9
|
+
|
|
10
|
+
Notas de Crédito y de Débito, la tabla de códigos reformada, y correcciones
|
|
11
|
+
que una aplicación anfitriona con su propia base de datos necesita. Las líneas
|
|
12
|
+
citadas son de `doc/fuentes/acuerdo_481_2017_consolidado.txt` (texto
|
|
13
|
+
consolidado) y `doc/fuentes/acuerdo_609_2017_gaceta_34457_ocr.txt`.
|
|
14
|
+
|
|
15
|
+
### Corregido
|
|
16
|
+
|
|
17
|
+
- **Códigos de tipo de documento (Art. 10 num. 7 lit. c y sus pares).** La 0.1.0
|
|
18
|
+
traía la tabla del texto original de 2017. El Art. 1 del Acuerdo 609-2017
|
|
19
|
+
(*La Gaceta* 34,457) reescribió el literal c) de los Arts. 15, 17, 20, 23, 25,
|
|
20
|
+
27, 29 y 33 (OCR del 609, líneas 209, 231, 273, 320, 359, 378, 404 y 438). La
|
|
21
|
+
tabla vigente: 01 Factura (l. 626), 04 Recibo por Honorarios Profesionales
|
|
22
|
+
(l. 1039, no reformado), 05 Comprobante de Retención (l. 1750), 06 Nota de
|
|
23
|
+
Crédito (l. 1345), 07 Nota de Débito (l. 1438), 08 Guía de Remisión (l. 1535),
|
|
24
|
+
09 Ticket (l. 969), 10 Factura Prevalorada (l. 889), 11 Boleta de Compra
|
|
25
|
+
(l. 1122), 12 Constancia de Donación (l. 1259). 02 y 03 quedan sin asignar y se
|
|
26
|
+
rechazan.
|
|
27
|
+
- **Correlativo trabado.** Si el libro contable fallaba después de guardar el
|
|
28
|
+
documento, el documento quedaba en el store y el contador no avanzaba: toda
|
|
29
|
+
emisión posterior de ese identificador fallaba con `ImmutableDocument`. Ahora
|
|
30
|
+
el documento se borra si el asiento falla, dentro del bloque de `allocate`.
|
|
31
|
+
- **Anulación trabada.** Si el libro contable rechazaba el asiento de
|
|
32
|
+
`:anulacion`, el store quedaba con la forma ANULADA sin asiento, y reintentar
|
|
33
|
+
fallaba con «ya está anulada». Ahora `Issuance#annul` vuelve a guardar la
|
|
34
|
+
forma emitida (borra la anulada y guarda la original) si el asiento falla.
|
|
35
|
+
- **Fechas en servidores UTC.** Honduras es UTC-6; `Date.today` en un servidor UTC
|
|
36
|
+
estampaba la fecha de mañana desde las 18:00. Todas las fechas por defecto
|
|
37
|
+
salen de un reloj configurable (ver *Añadido*).
|
|
38
|
+
- **`invoicehn check` / `Issuance#health`** informaba "listo" con una
|
|
39
|
+
autorización de rango agotado y documentos restantes negativos.
|
|
40
|
+
`JsonStore#active_authorization` con `next_sequence:` ya no recurre a una
|
|
41
|
+
autorización que no cubre ese número, y `remaining` nunca es negativo.
|
|
42
|
+
|
|
43
|
+
### Añadido
|
|
44
|
+
|
|
45
|
+
- **Nota de Crédito (tipo 06, Arts. 25-26, l. 1300-1391) y Nota de Débito
|
|
46
|
+
(tipo 07, Arts. 27-28, l. 1393-1496).** `Invoice` acepta `reference:`
|
|
47
|
+
(`Invoicehn::DocumentReference` con CAI, correlativo y fecha de emisión del
|
|
48
|
+
comprobante, Arts. 26 y 28 num. 3) y `motivo:` (num. 4); el tipo lo declara
|
|
49
|
+
el correlativo. `Issuance#issue` acepta `reference:` y `motivo:`, y hay
|
|
50
|
+
atajos `issue_credit_note` / `issue_debit_note`. Cada identificador lleva su
|
|
51
|
+
propio contador y su propia autorización (Art. 59, l. 2259).
|
|
52
|
+
- **Reglas del validador para las notas**: referencia obligatoria; la de la NC
|
|
53
|
+
debe ser un Comprobante Fiscal (Art. 4 num. 32 y Art. 6) y la de la ND puede
|
|
54
|
+
ser "el Comprobante Fiscal o el documento" (Art. 28 num. 3 lit. b); motivo
|
|
55
|
+
obligatorio; nombre y RTN del adquirente (num. 1-2, sin alternativa de
|
|
56
|
+
CONSUMIDOR FINAL); moneda nacional (Art. 26 num. 9, Art. 28 num. 8); la
|
|
57
|
+
referencia no puede ser posterior a la nota. Las reglas propias de la
|
|
58
|
+
Factura (umbral de L 10,000.00, aviso de ventas mixtas, documentos del
|
|
59
|
+
exonerado del Art. 10 num. 8, moneda extranjera del Art. 11) se aplican sólo
|
|
60
|
+
a la Factura. No se encontró en las fuentes un monto máximo para las notas y
|
|
61
|
+
no se impone ninguno.
|
|
62
|
+
- **Renderers Text y PDF**: título "NOTA DE CRÉDITO" / "NOTA DE DÉBITO" (Arts. 25
|
|
63
|
+
y 27 num. 2), bloque del documento de referencia y motivo, leyenda "Copia:
|
|
64
|
+
Obligado Tributario" (Arts. 25 y 27 num. 7) y, en la NC, campos en blanco para
|
|
65
|
+
nombre, identidad y firma de quien la recibe (Art. 26 num. 7).
|
|
66
|
+
- **Reloj configurable**: `Invoicehn.configure { |c| c.today = -> { Date.current } }`
|
|
67
|
+
e `Invoicehn.today`. Por defecto `Date.today`, como antes. Una prueba recorre
|
|
68
|
+
`lib/` y falla si alguien vuelve a leer `Date.today` directamente.
|
|
69
|
+
- **Contrato del store/sequence/ledger** documentado en el README, con
|
|
70
|
+
`require "invoicehn/testing"` (verificaciones que lanzan
|
|
71
|
+
`Invoicehn::Testing::ContractError`), `require "invoicehn/test_support/contracts"`
|
|
72
|
+
(módulos de minitest `StoreContract`, `SequenceContract`, `LedgerContract`,
|
|
73
|
+
`IssuanceContract`) y `require "invoicehn/test_support/memory"` (adaptadores
|
|
74
|
+
en memoria de referencia).
|
|
75
|
+
- `Correlative::COMPROBANTES_FISCALES` (Art. 6) y `DOCUMENTOS_COMPLEMENTARIOS`
|
|
76
|
+
(Art. 7); `Correlative#credit_note?`, `#debit_note?`, `#note?`,
|
|
77
|
+
`#comprobante_fiscal?`; `Invoice#document_type`, `#document_name`,
|
|
78
|
+
`#factura?`, `#credit_note?`, `#debit_note?`, `#note?`.
|
|
79
|
+
- `invoicehn issue -f` acepta `reference` y `motivo` en el JSON.
|
|
80
|
+
- Pruebas del PDF leyéndolo de vuelta con `pdf-reader` (dependencia sólo de
|
|
81
|
+
desarrollo).
|
|
82
|
+
|
|
83
|
+
### Cambiado — revisar al actualizar
|
|
84
|
+
|
|
85
|
+
- **El store debe responder a `delete_document(document)`** (y `Issuance` usa
|
|
86
|
+
ahora `exists?`). Un store propio escrito para la 0.1.0 debe agregarlos.
|
|
87
|
+
- `Issuance.new` ya no construye `Config` cuando se le inyectan store, sequence
|
|
88
|
+
y ledger: no toca `~/.invoicehn`.
|
|
89
|
+
- **Autorizaciones registradas con la tabla vieja**: una autorización con
|
|
90
|
+
código 02 o 03 ya no se carga; una con 07 u 08 cambia de significado (07 es
|
|
91
|
+
ahora Nota de Débito, 08 Guía de Remisión). La 0.1.0 sólo emitía 01, así que
|
|
92
|
+
sólo afecta autorizaciones registradas y no usadas; revise
|
|
93
|
+
`authorizations.json`.
|
|
94
|
+
- La exportación CSV agrega al final las columnas `tipo`,
|
|
95
|
+
`documento_referencia` y `motivo`; el JSON del documento agrega
|
|
96
|
+
`document_type` (y `reference`/`motivo` en las notas); el libro JSONL agrega
|
|
97
|
+
`document_type` y `reference`.
|
|
98
|
+
- Mensajes: "no existe el documento …" en lugar de "no existe la factura …".
|
|
99
|
+
|
|
100
|
+
### Pendiente de criterio (no resuelto por las fuentes)
|
|
101
|
+
|
|
102
|
+
- Los Arts. 26 y 28 exigen RTN del adquirente sin alternativa: la gema rechaza
|
|
103
|
+
notas a consumidores finales sin RTN. Si el SAR admite otra práctica, es una
|
|
104
|
+
decisión del obligado tributario.
|
|
105
|
+
- El Art. 17 conserva la frase "El Ticket tendrá como código 03" (l. 988)
|
|
106
|
+
aunque el 609-2017 reformó su num. 4 a "09=Ticket"; la tabla sigue el
|
|
107
|
+
numeral reformado.
|
|
108
|
+
- Las notas se rechazan en moneda extranjera: el permiso del párrafo final del
|
|
109
|
+
Art. 11 habla sólo de "facturas" y los Arts. 26 num. 9 y 28 num. 8 piden la
|
|
110
|
+
Moneda Nacional Lempira. Una factura en dólares se ajustaría con una nota en
|
|
111
|
+
lempiras.
|
|
112
|
+
- La referencia de la Nota de Crédito debe ser un Comprobante Fiscal del Art. 6
|
|
113
|
+
(01, 04, 09, 10, 11, 12), no sólo una Factura; la de la Nota de Débito acepta
|
|
114
|
+
cualquier tipo reconocido por el "o el documento" del Art. 28 num. 3 lit. b.
|
|
115
|
+
- Ningún artículo (4 num. 31-32, 25-28) fija un monto máximo para las notas.
|
|
116
|
+
- `Issuance#annul` todavía guarda la forma anulada antes del asiento contable:
|
|
117
|
+
si el libro falla, el documento queda anulado sin asiento. Queda para una
|
|
118
|
+
versión siguiente.
|
|
119
|
+
|
|
8
120
|
## [0.1.0] — 2026-08-28
|
|
9
121
|
|
|
10
122
|
Primera versión. Emisión de Facturas (Comprobante Fiscal tipo 01) conforme al
|
|
@@ -55,5 +167,6 @@ Reglamento del Régimen de Facturación, Acuerdo 481-2017 y sus reformas
|
|
|
55
167
|
- La leyenda "La factura es beneficio de todos, exíjala" no está exigida por
|
|
56
168
|
ninguna norma; se admite como texto opcional.
|
|
57
169
|
|
|
58
|
-
[Sin publicar]: https://github.com/JorgePadilla/invoicehn/compare/v0.
|
|
170
|
+
[Sin publicar]: https://github.com/JorgePadilla/invoicehn/compare/v0.2.0...HEAD
|
|
171
|
+
[0.2.0]: https://github.com/JorgePadilla/invoicehn/compare/v0.1.0...v0.2.0
|
|
59
172
|
[0.1.0]: https://github.com/JorgePadilla/invoicehn/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
Facturación para Honduras conforme al **Reglamento del Régimen de Facturación,
|
|
7
7
|
Otros Documentos Fiscales y Registro Fiscal de Imprentas** (Acuerdo No. 481-2017
|
|
8
|
-
y sus reformas).
|
|
8
|
+
y sus reformas). Emite **Facturas** (tipo 01), **Notas de Crédito** (tipo 06) y
|
|
9
|
+
**Notas de Débito** (tipo 07). Biblioteca Ruby más una interfaz de terminal.
|
|
9
10
|
|
|
10
11
|
```
|
|
11
12
|
invoicehn setup # datos del emisor (Art. 10 num. 1)
|
|
@@ -22,7 +23,8 @@ Esto es lo primero que debe quedar claro, porque el software sólo cubre una mit
|
|
|
22
23
|
de lo que la ley exige.
|
|
23
24
|
|
|
24
25
|
**Cubre el contenido del documento.** Todos los campos que exigen los Artículos
|
|
25
|
-
10 (requisitos del formato) y 11 (requisitos al momento de la emisión)
|
|
26
|
+
10 (requisitos del formato) y 11 (requisitos al momento de la emisión) para la
|
|
27
|
+
Factura, y los Artículos 25-26 y 27-28 para las Notas de Crédito y de Débito: el
|
|
26
28
|
correlativo de 16 dígitos, el control del CAI, del rango autorizado y de la fecha
|
|
27
29
|
límite de emisión, la discriminación del ISV por tarifa, los descuentos, el total
|
|
28
30
|
en números y letras, y el redondeo estatutario.
|
|
@@ -88,6 +90,10 @@ invoicehn auth add --cai "ABCD12-345678-9ABCDE-F01234-567890-AB" \
|
|
|
88
90
|
--limit "2027-06-30"
|
|
89
91
|
```
|
|
90
92
|
|
|
93
|
+
El tercer grupo del correlativo es el **tipo de documento**, según la tabla
|
|
94
|
+
vigente tras el Acuerdo 609-2017 (ver *Los códigos de tipo de documento* más
|
|
95
|
+
abajo): `01` Factura, `06` Nota de Crédito, `07` Nota de Débito.
|
|
96
|
+
|
|
91
97
|
Un mismo identificador acumula autorizaciones con el tiempo: cuando un rango se
|
|
92
98
|
agota y el SAR concede otro que continúa la numeración, se registra el nuevo sin
|
|
93
99
|
borrar el anterior. El asignador elige la autorización vigente que cubra el
|
|
@@ -125,6 +131,39 @@ O desde un archivo, para integrarlo con otro sistema:
|
|
|
125
131
|
invoicehn issue -f venta.json
|
|
126
132
|
```
|
|
127
133
|
|
|
134
|
+
### Notas de Crédito y de Débito desde un archivo
|
|
135
|
+
|
|
136
|
+
Cada tipo de documento lleva su propio identificador, su propio contador y su
|
|
137
|
+
propia autorización del SAR (Art. 59: *"La autorización será por punto de
|
|
138
|
+
emisión y por tipo de documento"*). Registre primero el CAI de las notas:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
invoicehn auth add --cai "..." --from "000-001-06-00000001" \
|
|
142
|
+
--to "000-001-06-00000500" --limit "2027-06-30"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
El archivo es el de una venta más `reference` (el correlativo de la factura a
|
|
146
|
+
la que se aplica) y `motivo`:
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"customer": { "kind": "taxpayer", "name": "Distribuidora del Norte, S.A.",
|
|
151
|
+
"rtn": "05019005123456" },
|
|
152
|
+
"items": [{ "description": "Devolución: cemento gris bolsa 42.5 kg",
|
|
153
|
+
"quantity": 2, "unit_price": "235.00", "treatment": "gravado_15" }],
|
|
154
|
+
"reference": "000-001-01-00000001",
|
|
155
|
+
"motivo": "Devolución de 2 bolsas dañadas"
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
invoicehn issue -f nota.json --identifier 000-001-06 # Nota de Crédito
|
|
161
|
+
invoicehn issue -f nota.json --identifier 000-001-07 # Nota de Débito
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Si la factura se emitió fuera de esta instalación, `reference` puede ser un
|
|
165
|
+
objeto con sus tres datos: `{"correlative": "...", "cai": "...", "issue_date": "AAAA-MM-DD"}`.
|
|
166
|
+
|
|
128
167
|
### 4. Consultar, anular, exportar
|
|
129
168
|
|
|
130
169
|
```
|
|
@@ -196,6 +235,76 @@ issuance = Invoicehn::Issuance.new
|
|
|
196
235
|
factura = issuance.issue(customer: cliente, line_items: lineas)
|
|
197
236
|
```
|
|
198
237
|
|
|
238
|
+
### Notas de Crédito y de Débito
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
# Nota de Crédito (tipo 06) — devoluciones, descuentos posteriores, anulación
|
|
242
|
+
# de operaciones (Art. 4 num. 32).
|
|
243
|
+
nc = issuance.issue_credit_note(
|
|
244
|
+
reference: "000-001-01-00000042", # factura en el store: CAI y fecha se copian
|
|
245
|
+
motivo: "Devolución de 2 bolsas dañadas", # Art. 26 num. 4
|
|
246
|
+
customer: cliente, # con RTN: Art. 26 num. 1-2
|
|
247
|
+
line_items: lineas_devueltas
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Nota de Débito (tipo 07) — ajustes a cargo del adquirente (Art. 4 num. 31).
|
|
251
|
+
nd = issuance.issue_debit_note(
|
|
252
|
+
reference: Invoicehn::DocumentReference.new( # documento emitido fuera del store
|
|
253
|
+
correlative: "000-001-01-00009999",
|
|
254
|
+
cai: "ABCD12-345678-9ABCDE-F01234-567890-AB",
|
|
255
|
+
issue_date: Date.new(2026, 9, 1)
|
|
256
|
+
),
|
|
257
|
+
motivo: "Flete adicional",
|
|
258
|
+
customer: cliente,
|
|
259
|
+
line_items: lineas
|
|
260
|
+
)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Las firmas exactas:
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
Invoicehn::Issuance#issue(customer:, line_items:, identifier: "000-001-01",
|
|
267
|
+
currency: "HNL", exchange_rate: nil, notes: nil,
|
|
268
|
+
issue_date: Invoicehn.today, reference: nil, motivo: nil)
|
|
269
|
+
Invoicehn::Issuance#issue_credit_note(reference:, motivo:, customer:, line_items:,
|
|
270
|
+
identifier: "000-001-06", **opciones_de_issue)
|
|
271
|
+
Invoicehn::Issuance#issue_debit_note(reference:, motivo:, customer:, line_items:,
|
|
272
|
+
identifier: "000-001-07", **opciones_de_issue)
|
|
273
|
+
Invoicehn::Issuance#annul(correlative, reason:, on: Invoicehn.today) # Factura o nota
|
|
274
|
+
Invoicehn::DocumentReference.new(correlative:, cai:, issue_date:)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
El **tipo de documento lo decide el identificador**: `NNN-NNN-01` Factura,
|
|
278
|
+
`NNN-NNN-06` Nota de Crédito, `NNN-NNN-07` Nota de Débito. `issue` con un
|
|
279
|
+
identificador `-06`/`-07` y `reference:`/`motivo:` es equivalente a los dos
|
|
280
|
+
atajos. `reference:` acepta:
|
|
281
|
+
|
|
282
|
+
- un correlativo (`String` o `Correlative`) de un documento que el store tiene:
|
|
283
|
+
se busca y se copian su CAI y su fecha de emisión;
|
|
284
|
+
- un `DocumentReference` con los tres datos (Arts. 26 y 28 num. 3), para un
|
|
285
|
+
documento emitido fuera del store. Si el store sí lo tiene, los datos se
|
|
286
|
+
cotejan y una discrepancia se rechaza.
|
|
287
|
+
|
|
288
|
+
Cuando el original está en el store, además se rechaza la nota si el original
|
|
289
|
+
está ANULADO o si su adquirente tiene otro RTN. Esas dos verificaciones son de
|
|
290
|
+
coherencia, no texto del Reglamento.
|
|
291
|
+
|
|
292
|
+
### El reloj: la fecha de Honduras, no la del servidor
|
|
293
|
+
|
|
294
|
+
Honduras está en UTC-6 todo el año. Un servidor en UTC que leyera `Date.today`
|
|
295
|
+
estamparía, desde las 18:00 de Tegucigalpa, la fecha de **mañana** como fecha de
|
|
296
|
+
emisión, y daría por vencida una autorización horas antes de su fecha límite
|
|
297
|
+
(Art. 62). Todas las fechas por defecto de la gema (emisión, anulación,
|
|
298
|
+
validación, `health`, vigencia de la autorización) salen de `Invoicehn.today`:
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
# config/initializers/invoicehn.rb, con config.time_zone = "America/Tegucigalpa"
|
|
302
|
+
Invoicehn.configure { |c| c.today = -> { Date.current } }
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Sin configurar, el reloj es `Date.today` (la zona del proceso), como en la
|
|
306
|
+
0.1.0. El reloj debe devolver un `Date`; un `Time` o un `DateTime` se rechazan.
|
|
307
|
+
|
|
199
308
|
---
|
|
200
309
|
|
|
201
310
|
## Decisiones que conviene conocer
|
|
@@ -255,13 +364,61 @@ RTN reales. Se valida longitud y dígitos, nada más.
|
|
|
255
364
|
El asignador se lleva por la terna (establecimiento, punto de emisión, tipo de
|
|
256
365
|
documento) — el *identificador del documento* del Art. 10 num. 7 — y trabaja bajo
|
|
257
366
|
un candado exclusivo de archivo, de modo que dos procesos simultáneos no pueden
|
|
258
|
-
repetir ni omitir un número. La asignación
|
|
259
|
-
candado: si el guardado falla, el
|
|
367
|
+
repetir ni omitir un número. La asignación, el guardado y el asiento contable
|
|
368
|
+
ocurren dentro del mismo candado y son todo o nada: si el guardado falla, el
|
|
369
|
+
correlativo no se consume; si el libro contable rechaza el asiento, el documento
|
|
370
|
+
recién guardado se borra antes de que avance el contador. (La 0.1.0 dejaba el
|
|
371
|
+
documento en disco y el contador atrás, y toda emisión posterior fallaba.)
|
|
260
372
|
|
|
261
373
|
Un documento emitido es inmutable. La única corrección es la anulación
|
|
262
374
|
(Art. 41), que marca el registro con la leyenda **ANULADA** y **conserva el
|
|
263
375
|
correlativo consumido**.
|
|
264
376
|
|
|
377
|
+
### Los códigos de tipo de documento son los reformados
|
|
378
|
+
|
|
379
|
+
El Acuerdo 609-2017 (*La Gaceta* 34,457) reescribió el literal c) de los
|
|
380
|
+
artículos de cada documento. La tabla vigente, según el texto consolidado:
|
|
381
|
+
|
|
382
|
+
| Código | Documento | Artículo |
|
|
383
|
+
|---|---|---|
|
|
384
|
+
| 01 | Factura | 10 num. 7 lit. c |
|
|
385
|
+
| 04 | Recibo por Honorarios Profesionales | 18 num. 7 lit. c (no reformado) |
|
|
386
|
+
| 05 | Comprobante de Retención | 33 num. 5 lit. c |
|
|
387
|
+
| 06 | **Nota de Crédito** | 25 num. 5 lit. c |
|
|
388
|
+
| 07 | **Nota de Débito** | 27 num. 5 lit. c |
|
|
389
|
+
| 08 | Guía de Remisión | 29 num. 4 lit. c |
|
|
390
|
+
| 09 | Ticket | 17 num. 4 lit. a |
|
|
391
|
+
| 10 | Factura Prevalorada | 15 num. 6 lit. c |
|
|
392
|
+
| 11 | Boleta de Compra | 20 num. 5 lit. c |
|
|
393
|
+
| 12 | Constancia de Donación | 23 num. 7 lit. c |
|
|
394
|
+
|
|
395
|
+
El texto original de 2017 numeraba distinto (02 Prevalorada, 03 Ticket, 07 Nota
|
|
396
|
+
de Crédito, 08 Nota de Débito, 10 Retención...). La 0.1.0 traía esa tabla; la
|
|
397
|
+
0.2.0 la corrige. 02 y 03 quedaron sin asignar y se rechazan. La gema **emite**
|
|
398
|
+
01, 06 y 07; los demás códigos se reconocen (para validar referencias) pero no
|
|
399
|
+
se emiten.
|
|
400
|
+
|
|
401
|
+
### Las notas exigen el RTN del adquirente y se emiten en lempiras
|
|
402
|
+
|
|
403
|
+
Los Arts. 26 y 28 num. 1-2 piden *"Nombres y Apellidos, Razón o Denominación
|
|
404
|
+
Social del adquirente"* y *"Número de Registro Tributario Nacional (RTN) del
|
|
405
|
+
adquirente"*. A diferencia del Art. 11 num. 2 para la Factura, no ofrecen la
|
|
406
|
+
alternativa de CONSUMIDOR FINAL: una nota a un adquirente sin RTN se rechaza.
|
|
407
|
+
|
|
408
|
+
Los Arts. 26 num. 9 y 28 num. 8 piden la *"Denominación literal de la Moneda
|
|
409
|
+
Nacional Lempira o símbolo (L)"*. El permiso de emitir en *"otra denominación
|
|
410
|
+
monetaria"* con la tasa de cambio está en el párrafo final del Art. 11 y habla
|
|
411
|
+
sólo de *facturas*; los artículos de las notas no tienen ese párrafo. Una nota
|
|
412
|
+
en otra moneda se rechaza.
|
|
413
|
+
|
|
414
|
+
La Nota de Crédito debe aplicarse a un **Comprobante Fiscal** (Art. 4 num. 32,
|
|
415
|
+
Art. 26 num. 3 lit. b; la lista está en el Art. 6). La Nota de Débito admite
|
|
416
|
+
*"el Comprobante Fiscal o el documento"* (Art. 28 num. 3 lit. b). Ninguno de
|
|
417
|
+
los artículos fija un monto máximo para la nota, y la gema no inventa uno.
|
|
418
|
+
|
|
419
|
+
La Nota de Crédito imprime en blanco los campos de quien la recibe (nombres,
|
|
420
|
+
identidad y firma, Art. 26 num. 7), que se llenan a mano al entregarla.
|
|
421
|
+
|
|
265
422
|
### La fecha de emisión es la del sistema
|
|
266
423
|
|
|
267
424
|
No hay bandera para retrofechar. El Art. 43 obliga a custodiar los documentos en
|
|
@@ -296,10 +453,10 @@ Cada fila corresponde a pruebas automatizadas.
|
|
|
296
453
|
| Artículo | Requisito | Implementado en |
|
|
297
454
|
|---|---|---|
|
|
298
455
|
| 10 num. 1 | Datos de identificación del emisor | `Issuer` |
|
|
299
|
-
| 10 num. 2 | Denominación "Factura" | `Renderers::Text` |
|
|
456
|
+
| 10 num. 2 | Denominación "Factura" | `Renderers::Text`, `Renderers::Pdf` |
|
|
300
457
|
| 10 num. 3-5 | CAI, fecha límite y rango vigentes | `Authorization` |
|
|
301
458
|
| 10 num. 6 | Destino de los ejemplares | `Renderers::Text` |
|
|
302
|
-
| 10 num. 7 | Correlativo de 16 dígitos | `Correlative` |
|
|
459
|
+
| 10 num. 7 | Correlativo de 16 dígitos; códigos reformados por el 609-2017 | `Correlative` |
|
|
303
460
|
| 10 num. 8 | Datos del adquirente exonerado | `Customer::Exonerado` |
|
|
304
461
|
| 10 num. 10 | Descuentos y rebajas (formato) | `Renderers::Text` |
|
|
305
462
|
| 11 num. 1 | Requisitos para crédito fiscal | `Compliance::Validator` |
|
|
@@ -310,6 +467,13 @@ Cada fila corresponde a pruebas automatizadas.
|
|
|
310
467
|
| 11 num. 3 | Crédito fiscal sólo por ventas gravadas | `TaxSummary#credito_fiscal_base` |
|
|
311
468
|
| 11 párrafo final | Tasa de cambio a la fecha de emisión | `ExchangeRate` |
|
|
312
469
|
| 12 | Exportaciones con tasa cero | `TaxTreatment::GRAVADO_0` |
|
|
470
|
+
| 25 num. 2, 7 / 27 num. 2, 7 | "Nota de Crédito" / "Nota de Débito"; destino de los ejemplares | `Renderers::Text`, `Renderers::Pdf` |
|
|
471
|
+
| 25 num. 5 / 27 num. 5 | Códigos 06 y 07, serie propia | `Correlative`, `Sequence` |
|
|
472
|
+
| 26 num. 1-2 / 28 num. 1-2 | Nombre y RTN del adquirente | `Compliance::Validator` |
|
|
473
|
+
| 26 num. 3 / 28 num. 3 | CAI, correlativo y fecha del comprobante | `DocumentReference` |
|
|
474
|
+
| 26 num. 4 / 28 num. 4 | Motivo de la emisión | `Invoice#motivo` |
|
|
475
|
+
| 26 num. 7 | Datos y firma de quien recibe la NC | `Renderers::Text`, `Renderers::Pdf` |
|
|
476
|
+
| 26 num. 9 / 28 num. 8 | Moneda Nacional Lempira | `Compliance::Validator` |
|
|
313
477
|
| 41 | Leyenda ANULADA; correlativo conservado | `Invoice#annul` |
|
|
314
478
|
| 42 | Aviso de autorizaciones vencidas sin usar | `invoicehn check` |
|
|
315
479
|
| 43 | Custodia cronológica | `Storage::JsonStore` |
|
|
@@ -356,6 +520,95 @@ Invoicehn::Issuance.new(
|
|
|
356
520
|
|
|
357
521
|
---
|
|
358
522
|
|
|
523
|
+
## Integración con la aplicación anfitriona: store, sequence y ledger
|
|
524
|
+
|
|
525
|
+
`Invoicehn::Issuance.new(store:, sequence:, ledger:)` acepta cualquier objeto
|
|
526
|
+
que responda a los métodos de abajo (*duck typing*); la gema no depende de
|
|
527
|
+
Rails ni de ActiveRecord. Cuando se inyectan los tres, `Issuance` no toca el
|
|
528
|
+
sistema de archivos: no construye `Config` ni lee `~/.invoicehn`.
|
|
529
|
+
|
|
530
|
+
### store
|
|
531
|
+
|
|
532
|
+
| Método | Contrato |
|
|
533
|
+
|---|---|
|
|
534
|
+
| `issuer` → `Issuer` o `nil` | El emisor con los siete datos del Art. 10 num. 1. |
|
|
535
|
+
| `authorizations_for(identifier)` → `Array<Authorization>` | Todas las autorizaciones de ese `NNN-NNN-TT`, vencidas incluidas. |
|
|
536
|
+
| `active_authorization(identifier, next_sequence:, on:)` → `Authorization` o `nil` | Una autorización no vencida en `on` cuyo rango **cubra** `next_sequence` (la de menor inicio si hay varias). `nil` si ninguna lo cubre; no devolver una agotada. Con `next_sequence: nil`, la vigente de menor inicio. |
|
|
537
|
+
| `save_document(document)` → `document` | Guarda el documento (`document.to_h` es su forma serializable). Si ya existe y el nuevo **no** está anulado, lanza `Invoicehn::ImmutableDocument`; la forma anulada reemplaza a la emitida (Art. 41). |
|
|
538
|
+
| `delete_document(document)` → `document` | Borra el documento. `Issuance` lo llama **sólo** para deshacer una emisión cuyo asiento contable falló (dentro del bloque de `allocate`) o una anulación cuyo asiento falló (borra la forma ANULADA y vuelve a guardar la emitida). |
|
|
539
|
+
| `find(correlative)` → `Invoice` | El documento con ese correlativo (`String` o `Correlative`); `Invoicehn::DocumentNotFound` si no existe. `find(c).to_h` debe ser igual a lo guardado, incluidos `reference` y `motivo` de las notas. |
|
|
540
|
+
| `exists?(correlative)` → `Boolean` | |
|
|
541
|
+
|
|
542
|
+
La CLI usa además `save_issuer`, `authorizations`, `add_authorization`, `all(from:, to:)`, `settings` y `save_settings`; una aplicación que no use la CLI no los necesita.
|
|
543
|
+
|
|
544
|
+
### sequence
|
|
545
|
+
|
|
546
|
+
| Método | Contrato |
|
|
547
|
+
|---|---|
|
|
548
|
+
| `allocate(identifier) { \|correlative\| ... }` → valor del bloque | Calcula el siguiente correlativo, lo entrega al bloque **bajo candado** (en Postgres: `SELECT ... FOR UPDATE` dentro de una transacción) y avanza el contador **sólo si el bloque termina sin excepción**. Si el bloque lanza, el contador queda igual y la excepción se propaga. |
|
|
549
|
+
| `peek(identifier)` → `Correlative` | El próximo correlativo, sin consumirlo. |
|
|
550
|
+
| `issued_count(identifier)` → `Integer` | La última secuencia consumida (0 si ninguna). |
|
|
551
|
+
|
|
552
|
+
Cada identificador (`000-001-01`, `000-001-06`, `000-001-07`...) es una serie
|
|
553
|
+
independiente que empieza en 00000001.
|
|
554
|
+
|
|
555
|
+
### ledger
|
|
556
|
+
|
|
557
|
+
| Método | Contrato |
|
|
558
|
+
|---|---|
|
|
559
|
+
| `record(document, event:)` → cualquier cosa | Asienta la emisión (`event: :emision`) o la anulación (`event: :anulacion`). Si lanza, `Issuance` borra el documento recién guardado y el correlativo no se consume. |
|
|
560
|
+
|
|
561
|
+
`entries(from:, to:)` sólo lo usa la exportación de la CLI. `Ledger::Multi`
|
|
562
|
+
reparte a varios libros, pero no puede deshacer el asiento de uno si el
|
|
563
|
+
siguiente falla: un libro transaccional debería participar en la transacción
|
|
564
|
+
de la aplicación.
|
|
565
|
+
|
|
566
|
+
Si la aplicación envuelve la emisión en una transacción de base de datos
|
|
567
|
+
(recomendado), cualquier fallo revierte también el contador y el documento.
|
|
568
|
+
|
|
569
|
+
### Verificar los adaptadores
|
|
570
|
+
|
|
571
|
+
```ruby
|
|
572
|
+
require "invoicehn/test_support/contracts"
|
|
573
|
+
|
|
574
|
+
class InvoicehnStoreContractTest < ActiveSupport::TestCase
|
|
575
|
+
include Invoicehn::TestSupport::StoreContract
|
|
576
|
+
# Un store con emisor completo y autorización vigente para 000-001-01
|
|
577
|
+
# (y, opcionalmente, para 000-001-06 para probar las notas).
|
|
578
|
+
def contract_store = Facturacion::InvoicehnStore.new
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
class InvoicehnSequenceContractTest < ActiveSupport::TestCase
|
|
582
|
+
include Invoicehn::TestSupport::SequenceContract
|
|
583
|
+
def contract_sequence = Facturacion::InvoicehnSequence.new
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
class InvoicehnLedgerContractTest < ActiveSupport::TestCase
|
|
587
|
+
include Invoicehn::TestSupport::LedgerContract
|
|
588
|
+
def contract_ledger = Facturacion::InvoicehnLedger.new
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
class InvoicehnIssuanceContractTest < ActiveSupport::TestCase
|
|
592
|
+
include Invoicehn::TestSupport::IssuanceContract # emite una factura real
|
|
593
|
+
def contract_store = Facturacion::InvoicehnStore.new
|
|
594
|
+
def contract_sequence = Facturacion::InvoicehnSequence.new
|
|
595
|
+
def contract_ledger = Facturacion::InvoicehnLedger.new
|
|
596
|
+
end
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
`IssuanceContract` emite primero con un libro que rechaza el asiento y verifica
|
|
600
|
+
que no quede documento ni avance el contador; luego emite de verdad y verifica
|
|
601
|
+
que `find` devuelva lo emitido. Fuera de minitest, las mismas verificaciones
|
|
602
|
+
están en `require "invoicehn/testing"` (`Invoicehn::Testing.assert_store_contract`,
|
|
603
|
+
`assert_sequence_contract`, `assert_ledger_contract`, `assert_issuance_round_trip`),
|
|
604
|
+
que lanzan `Invoicehn::Testing::ContractError`. `invoicehn/test_support/memory`
|
|
605
|
+
trae adaptadores en memoria: la implementación completa más corta del contrato,
|
|
606
|
+
útil como modelo.
|
|
607
|
+
|
|
608
|
+
La CLI (`thor`, `tty-prompt`) y el PDF (`prawn`) no se cargan con
|
|
609
|
+
`require "invoicehn"`: una aplicación Rails los instala como dependencias de la
|
|
610
|
+
gema, pero no los carga si no los usa.
|
|
611
|
+
|
|
359
612
|
## Almacenamiento
|
|
360
613
|
|
|
361
614
|
Por defecto en `~/.invoicehn` (configurable con `INVOICEHN_HOME` o `--data-dir`):
|
|
@@ -384,6 +637,10 @@ Textos primarios, por número de *La Gaceta*:
|
|
|
384
637
|
| 34,457 | Acuerdo 609-2017 (primera reforma) |
|
|
385
638
|
| 34,792 | Acuerdo 725-2018 (segunda reforma — agrega los descuentos) |
|
|
386
639
|
| 34,811 | Acuerdo 817-2018 (tercera reforma) |
|
|
640
|
+
|
|
641
|
+
El Acuerdo 609-2017 es el que cambió los códigos de tipo de documento; las
|
|
642
|
+
notas siguen los Arts. 25-28 del texto consolidado
|
|
643
|
+
(`doc/fuentes/acuerdo_481_2017_consolidado.txt`, líneas 1300-1497).
|
|
387
644
|
| 33,316 | Decreto 278-2013 (tasas del ISV) |
|
|
388
645
|
|
|
389
646
|
Más la Ley del Impuesto Sobre Ventas (Decreto-Ley 24, Arts. 3 y 9) y el Código
|
|
@@ -47,9 +47,9 @@ module Invoicehn
|
|
|
47
47
|
# perderán su validez y no podrán ser utilizados cuando se haya vencido el
|
|
48
48
|
# plazo de tiempo autorizado." The fecha límite is the last day on which a
|
|
49
49
|
# document may be issued, so it is still usable on that date itself.
|
|
50
|
-
def expired?(on =
|
|
50
|
+
def expired?(on = Invoicehn.today) = on > @limit_date
|
|
51
51
|
|
|
52
|
-
def days_remaining(from =
|
|
52
|
+
def days_remaining(from = Invoicehn.today) = (@limit_date - from).to_i
|
|
53
53
|
|
|
54
54
|
def covers?(correlative)
|
|
55
55
|
correlative = Correlative.parse(correlative.to_s)
|
|
@@ -62,13 +62,13 @@ module Invoicehn
|
|
|
62
62
|
def capacity = @range_end.sequence - @range_start.sequence + 1
|
|
63
63
|
|
|
64
64
|
# Whether this authorization can still be used to issue the given number.
|
|
65
|
-
def usable?(correlative, on:
|
|
65
|
+
def usable?(correlative, on: Invoicehn.today)
|
|
66
66
|
!expired?(on) && covers?(correlative)
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
# Raises with the specific reason, so the caller can report which of the two
|
|
70
70
|
# conditions failed rather than a generic refusal.
|
|
71
|
-
def assert_usable!(correlative, on:
|
|
71
|
+
def assert_usable!(correlative, on: Invoicehn.today)
|
|
72
72
|
if expired?(on)
|
|
73
73
|
raise AuthorizationExpired,
|
|
74
74
|
"la fecha límite de emisión (#{@limit_date}) ya venció; " \
|
|
@@ -21,10 +21,23 @@ module Invoicehn
|
|
|
21
21
|
line_items: Array(data["items"] || data["line_items"]).map { |i| build_line(i, currency) },
|
|
22
22
|
currency: currency,
|
|
23
23
|
exchange_rate: build_rate(data["exchange_rate"], currency),
|
|
24
|
-
notes: data["notes"]
|
|
24
|
+
notes: data["notes"],
|
|
25
|
+
# Notes only (identifier NNN-NNN-06 or -07): the correlative of a
|
|
26
|
+
# document in the store, or {correlative, cai, issue_date} for one
|
|
27
|
+
# issued elsewhere — Arts. 26 and 28 num. 3 and 4.
|
|
28
|
+
reference: build_reference(data["reference"]),
|
|
29
|
+
motivo: data["motivo"]
|
|
25
30
|
)
|
|
26
31
|
end
|
|
27
32
|
|
|
33
|
+
def build_reference(value)
|
|
34
|
+
case value
|
|
35
|
+
when nil, "" then nil
|
|
36
|
+
when Hash then DocumentReference.from_h(value)
|
|
37
|
+
else value.to_s
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
28
41
|
def build_customer(data)
|
|
29
42
|
kind = (data["kind"] || data["tipo"] || "consumidor_final").to_s
|
|
30
43
|
|
|
@@ -63,7 +76,7 @@ module Invoicehn
|
|
|
63
76
|
return nil if data.nil?
|
|
64
77
|
return ExchangeRate.from_h(data) if data.is_a?(Hash)
|
|
65
78
|
|
|
66
|
-
ExchangeRate.new(rate: to_decimal(data), date:
|
|
79
|
+
ExchangeRate.new(rate: to_decimal(data), date: Invoicehn.today, currency: currency)
|
|
67
80
|
end
|
|
68
81
|
|
|
69
82
|
private
|
data/lib/invoicehn/cli/wizard.rb
CHANGED
|
@@ -203,7 +203,7 @@ module Invoicehn
|
|
|
203
203
|
def ask_exchange_rate(currency)
|
|
204
204
|
ExchangeRate.new(
|
|
205
205
|
rate: ask(Locale.t("invoice.exchange_rate")),
|
|
206
|
-
date:
|
|
206
|
+
date: Invoicehn.today,
|
|
207
207
|
currency: currency,
|
|
208
208
|
source: @prompt.ask(Locale.t("invoice.exchange_source"),
|
|
209
209
|
default: ExchangeRate::DEFAULT_SOURCE)
|