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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 633195e813e47b48266e5c5dd70026b7fb22f3a3c839d53a9c79e37cb4861cd9
4
- data.tar.gz: 4970c4b593fb55f38ee90819b063444664bb07e0a00bc8b039f0e1b4d6c340c3
3
+ metadata.gz: 97a71a1ec9c4f50865c877824d8e0383eadd0586aafb647e01bc3d42cc160a7c
4
+ data.tar.gz: 5f2c1498b2ed86ab1845473badf845aae69ec3061de66c4e9328855e7d6e97ae
5
5
  SHA512:
6
- metadata.gz: fbcecd6f82d560df0dd182e1cd0d57439a3c2c4f4de2cf5ac2980e838ad2746831d5775d54c894a2e474113f67ed7096610cb1f1a184aa2a0512851deab6fb40
7
- data.tar.gz: 52a36d88796c7699fd29d193df20a16fb85651da782963bdd243e38557b6a73221478ce25b711083fdea4fa19d3535cfbfbe4d53090670d387dba70435f20a9d
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.1.0...HEAD
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). Biblioteca Ruby más una interfaz de terminal.
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): el
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 y el guardado ocurren dentro del mismo
259
- candado: si el guardado falla, el correlativo no se consume.
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 = Date.today) = on > @limit_date
50
+ def expired?(on = Invoicehn.today) = on > @limit_date
51
51
 
52
- def days_remaining(from = Date.today) = (@limit_date - from).to_i
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: Date.today)
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: Date.today)
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: Date.today, currency: currency)
79
+ ExchangeRate.new(rate: to_decimal(data), date: Invoicehn.today, currency: currency)
67
80
  end
68
81
 
69
82
  private
@@ -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: Date.today,
206
+ date: Invoicehn.today,
207
207
  currency: currency,
208
208
  source: @prompt.ask(Locale.t("invoice.exchange_source"),
209
209
  default: ExchangeRate::DEFAULT_SOURCE)