|
1
|
|
|
import logging |
|
2
|
|
|
import os |
|
3
|
|
|
from datetime import datetime, date |
|
4
|
|
|
from decimal import Decimal |
|
5
|
|
|
from decimal import InvalidOperation |
|
6
|
|
|
|
|
7
|
|
|
import xlsxwriter |
|
8
|
|
|
from babel.dates import format_date |
|
9
|
|
|
from markdown2 import markdown |
|
10
|
|
|
from satcfdi import DatePeriod |
|
11
|
|
|
from satcfdi.accounting import filter_invoices_iter, filter_payments_iter, invoices_export, payments_export |
|
12
|
|
|
from satcfdi.accounting.process import payments_groupby_receptor, payments_retentions_export |
|
13
|
|
|
from satcfdi.create.cfd import cfdi40 |
|
14
|
|
|
from satcfdi.create.cfd.cfdi40 import Comprobante, PagoComprobante |
|
15
|
|
|
from satcfdi.pacs import sat |
|
16
|
|
|
from satcfdi.printer import Representable |
|
17
|
|
|
# noinspection PyUnresolvedReferences |
|
18
|
|
|
from satcfdi.transform.catalog import CATALOGS |
|
19
|
|
|
from weasyprint import HTML, CSS |
|
20
|
|
|
from xlsxwriter.exceptions import FileCreateError |
|
21
|
|
|
|
|
22
|
|
|
from . import SOURCE_DIRECTORY, ARCHIVOS_DIRECTORY, TEMP_DIRECTORY |
|
23
|
|
|
from .environments import facturacion_environment |
|
24
|
|
|
from .utils import add_month |
|
25
|
|
|
|
|
26
|
|
|
logger = logging.getLogger(__name__) |
|
27
|
|
|
logger.level = logging.INFO |
|
28
|
|
|
|
|
29
|
|
|
sat_manager = sat.SAT() |
|
30
|
|
|
|
|
31
|
|
|
PERIODOS = { |
|
32
|
|
|
"Mensual": 1, |
|
33
|
|
|
"Bimestral": 2, |
|
34
|
|
|
"Trimestral": 3, |
|
35
|
|
|
"Cuatrimestral": 4, |
|
36
|
|
|
"Semestral": 6, |
|
37
|
|
|
"Anual": 12 |
|
38
|
|
|
} |
|
39
|
|
|
|
|
40
|
|
|
|
|
41
|
|
|
def create_cfdi(receptor_cif, factura_details, emisor_cif): |
|
42
|
|
|
emisor = cfdi40.Emisor( |
|
43
|
|
|
rfc=emisor_cif['Rfc'], |
|
44
|
|
|
nombre=emisor_cif['RazonSocial'], |
|
45
|
|
|
regimen_fiscal=emisor_cif['RegimenFiscal'] |
|
46
|
|
|
) |
|
47
|
|
|
emisor = emisor | factura_details.get('Emisor', {}) |
|
48
|
|
|
invoice = cfdi40.Comprobante( |
|
49
|
|
|
emisor=emisor, |
|
50
|
|
|
lugar_expedicion=emisor_cif['CodigoPostal'], |
|
51
|
|
|
receptor=cfdi40.Receptor( |
|
52
|
|
|
rfc=factura_details['Receptor'], |
|
53
|
|
|
nombre=receptor_cif['RazonSocial'], |
|
54
|
|
|
uso_cfdi=factura_details['UsoCFDI'], |
|
55
|
|
|
domicilio_fiscal_receptor=receptor_cif['CodigoPostal'], |
|
56
|
|
|
regimen_fiscal_receptor=receptor_cif['RegimenFiscal'] |
|
57
|
|
|
), |
|
58
|
|
|
metodo_pago=factura_details['MetodoPago'], |
|
59
|
|
|
forma_pago=factura_details['FormaPago'], |
|
60
|
|
|
# serie=serie, |
|
61
|
|
|
# folio=folio, |
|
62
|
|
|
conceptos=factura_details["Conceptos"] |
|
63
|
|
|
) |
|
64
|
|
|
invoice = invoice.process() |
|
65
|
|
|
|
|
66
|
|
|
expected_total = factura_details.get('Total') |
|
67
|
|
|
if expected_total is not None: |
|
68
|
|
|
if expected_total != invoice['Total']: |
|
69
|
|
|
logger.info(f"{factura_details['Receptor']}: Total '{expected_total}' is invalid, expected '{invoice['Total']}'") |
|
70
|
|
|
return None |
|
71
|
|
|
|
|
72
|
|
|
return invoice |
|
73
|
|
|
|
|
74
|
|
|
|
|
75
|
|
|
def parse_periodo_mes_ajuste(periodo_mes_ajuste: str): |
|
76
|
|
|
parts = periodo_mes_ajuste.split(".") |
|
77
|
|
|
if len(parts) != 2: |
|
78
|
|
|
raise ValueError("Periodo Invalido") |
|
79
|
|
|
|
|
80
|
|
|
periodo, mes_ajuste = parts |
|
81
|
|
|
if not mes_ajuste.isnumeric(): |
|
82
|
|
|
raise ValueError("Periodo Invalido") |
|
83
|
|
|
|
|
84
|
|
|
mes_ajuste = int(mes_ajuste) |
|
85
|
|
|
if not (12 >= int(mes_ajuste) >= 1): |
|
86
|
|
|
raise ValueError("Periodo Invalido") |
|
87
|
|
|
|
|
88
|
|
|
if periodo not in PERIODOS: |
|
89
|
|
|
raise ValueError("Periodo Invalido") |
|
90
|
|
|
|
|
91
|
|
|
return periodo, mes_ajuste |
|
92
|
|
|
|
|
93
|
|
|
|
|
94
|
|
|
def format_concepto_desc(concepto, periodo): |
|
95
|
|
|
concepto = concepto.copy() |
|
96
|
|
|
template = facturacion_environment.from_string(concepto["Descripcion"]) |
|
97
|
|
|
concepto["Descripcion"] = template.render( |
|
98
|
|
|
periodo=periodo |
|
99
|
|
|
) |
|
100
|
|
|
return concepto |
|
101
|
|
|
|
|
102
|
|
|
|
|
103
|
|
|
def validad_facturas(clients, facturas): |
|
104
|
|
|
is_valid = True |
|
105
|
|
|
for factura_details in facturas: |
|
106
|
|
|
cliente = clients.get(factura_details['Receptor']) |
|
107
|
|
|
if not cliente: |
|
108
|
|
|
logger.info(f"{factura_details['Receptor']}: client not found") |
|
109
|
|
|
is_valid = False |
|
110
|
|
|
|
|
111
|
|
|
for c in factura_details["Conceptos"]: |
|
112
|
|
|
periodo_mes_ajuste = c.get("_periodo_mes_ajuste", "") |
|
113
|
|
|
try: |
|
114
|
|
|
parse_periodo_mes_ajuste(periodo_mes_ajuste) |
|
115
|
|
|
except ValueError: |
|
116
|
|
|
logger.info(f"{factura_details['Receptor']}: _periodo_mes_ajuste '{periodo_mes_ajuste}' is invalid") |
|
117
|
|
|
is_valid = False |
|
118
|
|
|
|
|
119
|
|
|
if factura_details["MetodoPago"] == "PPD" and factura_details["FormaPago"] != "99": |
|
120
|
|
|
logger.info(f"{factura_details['Receptor']}: FormaPago '{factura_details['FormaPago']}' is invalid, expected '99' for PPD") |
|
121
|
|
|
is_valid = False |
|
122
|
|
|
|
|
123
|
|
|
return is_valid |
|
124
|
|
|
|
|
125
|
|
|
|
|
126
|
|
|
def year_month_desc(dp: DatePeriod): |
|
127
|
|
|
return format_date(date(year=dp.year, month=dp.month, day=1), locale='es_MX', format="'Mes de' MMMM 'del' y").upper() |
|
128
|
|
|
|
|
129
|
|
|
|
|
130
|
|
|
def periodo_desc(dp: DatePeriod, periodo_mes_ajuste, offset): |
|
131
|
|
|
periodo, mes_ajuste = parse_periodo_mes_ajuste(periodo_mes_ajuste) |
|
132
|
|
|
periodo_meses = PERIODOS[periodo] |
|
133
|
|
|
|
|
134
|
|
|
if (dp.month - mes_ajuste) % periodo_meses == 0: |
|
135
|
|
|
if offset: |
|
136
|
|
|
dp = add_month(dp, offset) |
|
137
|
|
|
|
|
138
|
|
|
periodo = year_month_desc(dp) |
|
139
|
|
|
if periodo_meses > 1: |
|
140
|
|
|
periodo += " AL " + year_month_desc( |
|
141
|
|
|
dp=add_month(dp, periodo_meses - 1) |
|
142
|
|
|
) |
|
143
|
|
|
|
|
144
|
|
|
return periodo |
|
145
|
|
|
return None |
|
146
|
|
|
|
|
147
|
|
|
|
|
148
|
|
|
def generate_ingresos(clients, facturas, dp, emisor_rfc): |
|
149
|
|
|
if not validad_facturas(clients, facturas): |
|
150
|
|
|
return |
|
151
|
|
|
|
|
152
|
|
|
emisor_cif = clients[emisor_rfc] |
|
153
|
|
|
|
|
154
|
|
|
def prepare_concepto(concepto): |
|
155
|
|
|
periodo = periodo_desc( |
|
156
|
|
|
dp, |
|
157
|
|
|
concepto['_periodo_mes_ajuste'], |
|
158
|
|
|
concepto.get('_desfase_mes') |
|
159
|
|
|
) |
|
160
|
|
|
if periodo and concepto['ValorUnitario'] is not None: |
|
161
|
|
|
return format_concepto_desc(concepto, periodo=periodo) |
|
162
|
|
|
|
|
163
|
|
|
def facturas_iter(): |
|
164
|
|
|
i = 0 |
|
165
|
|
|
for f in facturas: |
|
166
|
|
|
receptor_cif = clients[f['Receptor']] |
|
167
|
|
|
conceptos = [x for x in (prepare_concepto(c) for c in f["Conceptos"]) if x] |
|
|
|
|
|
|
168
|
|
|
if conceptos: |
|
169
|
|
|
f["Conceptos"] = conceptos |
|
170
|
|
|
yield create_cfdi(receptor_cif, f, emisor_cif) |
|
171
|
|
|
i += 1 |
|
172
|
|
|
|
|
173
|
|
|
cfdis = [i for i in facturas_iter()] |
|
174
|
|
|
|
|
175
|
|
|
if None in cfdis: |
|
176
|
|
|
return |
|
177
|
|
|
|
|
178
|
|
|
return cfdis |
|
179
|
|
|
|
|
180
|
|
|
|
|
181
|
|
|
def parse_fecha_pago(fecha_pago): |
|
182
|
|
|
if not fecha_pago: |
|
183
|
|
|
raise ValueError("Fecha de Pago es requerida") |
|
184
|
|
|
|
|
185
|
|
|
fecha_pago = datetime.fromisoformat(fecha_pago) |
|
186
|
|
|
if fecha_pago > datetime.now(): |
|
187
|
|
|
raise ValueError("Fecha de Pago es mayor a la fecha actual") |
|
188
|
|
|
|
|
189
|
|
|
if fecha_pago.replace(hour=12) > datetime.now(): |
|
190
|
|
|
fecha_pago = datetime.now() |
|
191
|
|
|
else: |
|
192
|
|
|
fecha_pago = fecha_pago.replace(hour=12) |
|
193
|
|
|
|
|
194
|
|
|
dif = datetime.now() - fecha_pago |
|
195
|
|
|
if dif.days > 30: |
|
196
|
|
|
raise ValueError("Fecha de Pago es de hace mas de 30 dias") |
|
197
|
|
|
|
|
198
|
|
|
return fecha_pago |
|
199
|
|
|
|
|
200
|
|
|
|
|
201
|
|
|
def parse_importe_pago(importe_pago: str): |
|
202
|
|
|
if not importe_pago: |
|
203
|
|
|
return None |
|
204
|
|
|
|
|
205
|
|
|
try: |
|
206
|
|
|
return round(Decimal(importe_pago), 2) |
|
207
|
|
|
except InvalidOperation: |
|
208
|
|
|
raise ValueError("Importe de Pago es invalido") |
|
209
|
|
|
|
|
210
|
|
|
|
|
211
|
|
|
def pago_factura(factura_pagar, fecha_pago: datetime, forma_pago: str, importe_pago: Decimal = None): |
|
212
|
|
|
c = factura_pagar |
|
213
|
|
|
invoice = Comprobante.pago_comprobantes( |
|
214
|
|
|
comprobantes=[ |
|
215
|
|
|
PagoComprobante( |
|
216
|
|
|
comprobante=c, |
|
217
|
|
|
num_parcialidad=c.ultima_num_parcialidad + 1, |
|
218
|
|
|
imp_saldo_ant=c.saldo_pendiente, |
|
219
|
|
|
imp_pagado=importe_pago |
|
220
|
|
|
) |
|
221
|
|
|
], |
|
222
|
|
|
fecha_pago=fecha_pago, |
|
223
|
|
|
forma_pago=forma_pago, |
|
224
|
|
|
) |
|
225
|
|
|
return invoice.process() |
|
226
|
|
|
|
|
227
|
|
|
|
|
228
|
|
|
def find_ajustes(facturas, mes_ajuste): |
|
229
|
|
|
for f in facturas: |
|
230
|
|
|
rfc = f["Receptor"] |
|
231
|
|
|
for concepto in f["Conceptos"]: |
|
232
|
|
|
_, mes_aj = parse_periodo_mes_ajuste(concepto['_periodo_mes_ajuste']) |
|
233
|
|
|
if mes_aj == mes_ajuste: |
|
234
|
|
|
yield rfc, concepto |
|
235
|
|
|
|
|
236
|
|
|
|
|
237
|
|
|
def archivos_folder(dp: DatePeriod): |
|
238
|
|
|
if dp.month: |
|
239
|
|
|
return os.path.join(ARCHIVOS_DIRECTORY, str(dp.year), str(dp.year) + "-{:02d}".format(dp.month)) |
|
240
|
|
|
return os.path.join(ARCHIVOS_DIRECTORY, str(dp.year)) |
|
241
|
|
|
|
|
242
|
|
|
|
|
243
|
|
|
def archivos_filename(dp: DatePeriod, ext="xlsx"): |
|
244
|
|
|
return os.path.join(archivos_folder(dp), f"{dp}.{ext}") |
|
245
|
|
|
|
|
246
|
|
|
|
|
247
|
|
|
def exportar_facturas(all_invoices, dp: DatePeriod, emisor_cif, rfc_prediales): |
|
248
|
|
|
emisor_rfc = emisor_cif['Rfc'] |
|
249
|
|
|
emisor_regimen = emisor_cif['RegimenFiscal'] |
|
250
|
|
|
|
|
251
|
|
|
emitidas = filter_invoices_iter(invoices=all_invoices.values(), fecha=dp, rfc_emisor=emisor_rfc) |
|
252
|
|
|
emitidas_pagos = filter_payments_iter(invoices=all_invoices, fecha=dp, rfc_emisor=emisor_rfc) |
|
253
|
|
|
emitidas_pagos = list(emitidas_pagos) |
|
254
|
|
|
|
|
255
|
|
|
recibidas = filter_invoices_iter(invoices=all_invoices.values(), fecha=dp, rfc_receptor=emisor_rfc) |
|
256
|
|
|
recibidas_pagos = filter_payments_iter(invoices=all_invoices, fecha=dp, rfc_receptor=emisor_rfc) |
|
257
|
|
|
|
|
258
|
|
|
recibidas_pagos = list(recibidas_pagos) |
|
259
|
|
|
pagos_hechos_iva = [ |
|
260
|
|
|
p |
|
261
|
|
|
for p in recibidas_pagos |
|
262
|
|
|
if sum(x.get("Importe", 0) for x in p.impuestos.get("Traslados", {}).values()) > 0 |
|
263
|
|
|
and p.comprobante["Receptor"].get("RegimenFiscalReceptor") in (emisor_regimen, None) |
|
264
|
|
|
] |
|
265
|
|
|
prediales = [p for p in recibidas_pagos if p.comprobante["Emisor"]["Rfc"] in rfc_prediales] |
|
266
|
|
|
|
|
267
|
|
|
archivo_excel = archivos_filename(dp) |
|
268
|
|
|
os.makedirs(os.path.dirname(archivo_excel), exist_ok=True) |
|
269
|
|
|
|
|
270
|
|
|
workbook = xlsxwriter.Workbook(archivo_excel) |
|
271
|
|
|
# EMITIDAS |
|
272
|
|
|
invoices_export(workbook, "EMITIDAS", emitidas) |
|
273
|
|
|
payments_export(workbook, "EMITIDAS PAGOS", emitidas_pagos) |
|
274
|
|
|
|
|
275
|
|
|
# RECIBIDAS |
|
276
|
|
|
invoices_export(workbook, "RECIBIDAS", recibidas) |
|
277
|
|
|
payments_export(workbook, "RECIBIDAS PAGOS", recibidas_pagos) |
|
278
|
|
|
|
|
279
|
|
|
# SPECIALES |
|
280
|
|
|
payments_export(workbook, f"RECIBIDAS PAGOS IVA {emisor_regimen.code}", pagos_hechos_iva) |
|
281
|
|
|
if prediales: |
|
282
|
|
|
payments_export(workbook, "PREDIALES", prediales) |
|
283
|
|
|
|
|
284
|
|
|
# RETENCIONES |
|
285
|
|
|
if dp.month is None: |
|
286
|
|
|
archivo_retenciones = archivos_filename(dp, ext="retenciones.txt") |
|
287
|
|
|
pagos_agrupados = payments_groupby_receptor(emitidas_pagos) |
|
288
|
|
|
payments_retentions_export(archivo_retenciones, pagos_agrupados) |
|
289
|
|
|
|
|
290
|
|
|
try: |
|
291
|
|
|
workbook.close() |
|
292
|
|
|
return archivo_excel |
|
293
|
|
|
except FileCreateError: |
|
294
|
|
|
return None |
|
295
|
|
|
|
|
296
|
|
|
|
|
297
|
|
|
def generate_pdf_template(template_name, fields): |
|
298
|
|
|
increment_template = facturacion_environment.get_template(template_name) |
|
299
|
|
|
md5_document = increment_template.render( |
|
300
|
|
|
fields |
|
301
|
|
|
) |
|
302
|
|
|
html = markdown(md5_document) |
|
303
|
|
|
pdf = HTML(string=html).write_pdf( |
|
304
|
|
|
target=None, |
|
305
|
|
|
stylesheets=[ |
|
306
|
|
|
os.path.join(SOURCE_DIRECTORY, "markdown_styles", "markdown6.css"), |
|
307
|
|
|
CSS( |
|
308
|
|
|
string='@page { width: Letter; margin: 1.6cm 1.6cm 1.6cm 1.6cm; }' |
|
309
|
|
|
) |
|
310
|
|
|
] |
|
311
|
|
|
) |
|
312
|
|
|
return pdf |
|
313
|
|
|
|
|
314
|
|
|
|
|
315
|
|
|
def generate_html_template(template_name, fields): |
|
316
|
|
|
increment_template = facturacion_environment.get_template(template_name) |
|
317
|
|
|
render = increment_template.render( |
|
318
|
|
|
fields |
|
319
|
|
|
) |
|
320
|
|
|
return render |
|
321
|
|
|
|
|
322
|
|
|
|
|
323
|
|
|
def mf_pago_fmt(cfdi): |
|
324
|
|
|
i = cfdi |
|
325
|
|
|
if i['TipoDeComprobante'] == "I": |
|
326
|
|
|
return i['TipoDeComprobante'].code + ' ' + i['MetodoPago'].code + ' ' + (i['FormaPago'].code if i['FormaPago'].code != '99' else '') |
|
327
|
|
|
return i['TipoDeComprobante'].code |
|
328
|
|
|
|
|
329
|
|
|
|
|
330
|
|
|
def ajustes_directory(dp: DatePeriod): |
|
331
|
|
|
return os.path.join(archivos_folder(dp), 'ajustes') |
|
332
|
|
|
|
|
333
|
|
|
|
|
334
|
|
|
def preview_cfdis(cfdis): |
|
335
|
|
|
outfile = os.path.join(TEMP_DIRECTORY, "factura.html") |
|
336
|
|
|
Representable.html_write_all( |
|
337
|
|
|
objs=cfdis, |
|
338
|
|
|
target=outfile, |
|
339
|
|
|
) |
|
340
|
|
|
os.startfile( |
|
341
|
|
|
os.path.abspath(outfile) |
|
342
|
|
|
) |
|
343
|
|
|
|