Passed
Push — main ( a8cb20...c828ff )
by Sat CFDI
01:45
created

satdigitalinvoice.gui_functions.center_location()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
nop 1
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 .formatting_functions.common import fecha_mes
25
from .utils import add_month
26
27
logger = logging.getLogger(__name__)
28
logger.level = logging.INFO
29
30
sat_manager = sat.SAT()
31
32
PERIODICIDAD = {
33
    "Mensual": 1,
34
    "Bimestral": 2,
35
    "Trimestral": 3,
36
    "Cuatrimestral": 4,
37
    "Semestral": 6,
38
    "Anual": 12
39
}
40
41
42
def create_cfdi(receptor_cif, factura_details, emisor_cif):
43
    emisor = cfdi40.Emisor(
44
        rfc=emisor_cif['Rfc'],
45
        nombre=emisor_cif['RazonSocial'],
46
        regimen_fiscal=emisor_cif['RegimenFiscal']
47
    )
48
    emisor = emisor | factura_details.get('Emisor', {})
49
    invoice = cfdi40.Comprobante(
50
        emisor=emisor,
51
        lugar_expedicion=emisor_cif['CodigoPostal'],
52
        receptor=cfdi40.Receptor(
53
            rfc=factura_details['Receptor'],
54
            nombre=receptor_cif['RazonSocial'],
55
            uso_cfdi=factura_details['UsoCFDI'],
56
            domicilio_fiscal_receptor=receptor_cif['CodigoPostal'],
57
            regimen_fiscal_receptor=receptor_cif['RegimenFiscal']
58
        ),
59
        metodo_pago=factura_details['MetodoPago'],
60
        forma_pago=factura_details['FormaPago'],
61
        # serie=serie,
62
        # folio=folio,
63
        conceptos=factura_details["Conceptos"]
64
    )
65
    invoice = invoice.process()
66
67
    expected_total = factura_details.get('Total')
68
    if expected_total is not None:
69
        if expected_total != invoice['Total']:
70
            print(f"{factura_details['Receptor']}: Total '{expected_total}' is invalid, expected '{invoice['Total']}'")
71
            return None
72
73
    return invoice
74
75
76
def parse_periodo_mes_ajuste(periodo_mes_ajuste: str):
77
    parts = periodo_mes_ajuste.split(".")
78
    if len(parts) != 2:
79
        raise ValueError("Periodo Invalido")
80
81
    periodo, mes_ajuste = parts
82
    if not mes_ajuste.isnumeric():
83
        raise ValueError("Periodo Invalido")
84
85
    mes_ajuste = int(mes_ajuste)
86
    if not (12 >= int(mes_ajuste) >= 1):
87
        raise ValueError("Periodo Invalido")
88
89
    if periodo not in PERIODICIDAD:
90
        raise ValueError("Periodo Invalido")
91
92
    return PERIODICIDAD[periodo], mes_ajuste
93
94
95
def format_concepto_desc(concepto, periodo):
96
    concepto = concepto.copy()
97
    template = facturacion_environment.from_string(concepto["Descripcion"])
98
    concepto["Descripcion"] = template.render(
99
        periodo=periodo
100
    )
101
    return concepto
102
103
104
def validad_facturas(clients, facturas):
105
    is_valid = True
106
    for factura_details in facturas:
107
        cliente = clients.get(factura_details['Receptor'])
108
        if not cliente:
109
            print(f"{factura_details['Receptor']}: client not found")
110
            is_valid = False
111
112
        for c in factura_details["Conceptos"]:
113
            periodo_mes_ajuste = c.get("_periodo_mes_ajuste", "")
114
            try:
115
                parse_periodo_mes_ajuste(periodo_mes_ajuste)
116
            except ValueError:
117
                print(f"{factura_details['Receptor']}: _periodo_mes_ajuste '{periodo_mes_ajuste}' is invalid")
118
                is_valid = False
119
120
        if factura_details["MetodoPago"] == "PPD" and factura_details["FormaPago"] != "99":
121
            print(f"{factura_details['Receptor']}: FormaPago '{factura_details['FormaPago']}' is invalid, expected '99' for PPD")
122
            is_valid = False
123
124
    return is_valid
125
126
127
def period_desc(dp: DatePeriod):
128
    return fecha_mes(date(year=dp.year, month=dp.month, day=1))
129
130
131
def periodicidad_desc(dp: DatePeriod, periodo_mes_ajuste, offset):
132
    periodo_meses, mes_ajuste = parse_periodo_mes_ajuste(periodo_mes_ajuste)
133
134
    if (dp.month - mes_ajuste) % periodo_meses == 0:
135
        if offset:
136
            dp = add_month(dp, offset)
137
138
        periodo = period_desc(dp)
139
        if periodo_meses > 1:
140
            periodo += " AL " + period_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 = periodicidad_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
    try:
203
        return round(Decimal(importe_pago), 2)
204
    except InvalidOperation:
205
        raise ValueError("Importe de Pago es invalido")
206
207
208
def pago_factura(factura_pagar, fecha_pago: datetime, forma_pago: str, importe_pago: Decimal = None):
209
    c = factura_pagar
210
    invoice = Comprobante.pago_comprobantes(
211
        comprobantes=[
212
            PagoComprobante(
213
                comprobante=c,
214
                num_parcialidad=c.ultima_num_parcialidad + 1,
215
                imp_saldo_ant=c.saldo_pendiente,
216
                imp_pagado=importe_pago
217
            )
218
        ],
219
        fecha_pago=fecha_pago,
220
        forma_pago=forma_pago,
221
    )
222
    return invoice.process()
223
224
225
def find_ajustes(facturas, mes_ajuste):
226
    for f in facturas:
227
        rfc = f["Receptor"]
228
        for concepto in f["Conceptos"]:
229
            _, mes_aj = parse_periodo_mes_ajuste(concepto['_periodo_mes_ajuste'])
230
            if mes_aj == mes_ajuste:
231
                yield rfc, concepto
232
233
234
def archivos_folder(dp: DatePeriod):
235
    if dp.month:
236
        return os.path.join(ARCHIVOS_DIRECTORY, str(dp.year), str(dp.year) + "-{:02d}".format(dp.month))
237
    return os.path.join(ARCHIVOS_DIRECTORY, str(dp.year))
238
239
240
def archivos_filename(dp: DatePeriod, ext="xlsx"):
241
    return os.path.join(archivos_folder(dp), f"{dp}.{ext}")
242
243
244
def exportar_facturas(all_invoices, dp: DatePeriod, emisor_cif, rfc_prediales):
245
    emisor_rfc = emisor_cif['Rfc']
246
    emisor_regimen = emisor_cif['RegimenFiscal']
247
248
    emitidas = filter_invoices_iter(invoices=all_invoices.values(), fecha=dp, rfc_emisor=emisor_rfc)
249
    emitidas_pagos = filter_payments_iter(invoices=all_invoices, fecha=dp, rfc_emisor=emisor_rfc)
250
    emitidas_pagos = list(emitidas_pagos)
251
252
    recibidas = filter_invoices_iter(invoices=all_invoices.values(), fecha=dp, rfc_receptor=emisor_rfc)
253
    recibidas_pagos = filter_payments_iter(invoices=all_invoices, fecha=dp, rfc_receptor=emisor_rfc)
254
255
    recibidas_pagos = list(recibidas_pagos)
256
    pagos_hechos_iva = [
257
        p
258
        for p in recibidas_pagos
259
        if sum(x.get("Importe", 0) for x in p.impuestos.get("Traslados", {}).values()) > 0
260
           and p.comprobante["Receptor"].get("RegimenFiscalReceptor") in (emisor_regimen, None)
261
    ]
262
    prediales = [p for p in recibidas_pagos if p.comprobante["Emisor"]["Rfc"] in rfc_prediales]
263
264
    archivo_excel = archivos_filename(dp)
265
    os.makedirs(os.path.dirname(archivo_excel), exist_ok=True)
266
267
    workbook = xlsxwriter.Workbook(archivo_excel)
268
    # EMITIDAS
269
    invoices_export(workbook, "EMITIDAS", emitidas)
270
    payments_export(workbook, "EMITIDAS PAGOS", emitidas_pagos)
271
272
    # RECIBIDAS
273
    invoices_export(workbook, "RECIBIDAS", recibidas)
274
    payments_export(workbook, "RECIBIDAS PAGOS", recibidas_pagos)
275
276
    # SPECIALES
277
    payments_export(workbook, f"RECIBIDAS PAGOS IVA {emisor_regimen.code}", pagos_hechos_iva)
278
    if prediales:
279
        payments_export(workbook, "PREDIALES", prediales)
280
281
    # RETENCIONES
282
    if dp.month is None:
283
        archivo_retenciones = archivos_filename(dp, ext="retenciones.txt")
284
        pagos_agrupados = payments_groupby_receptor(emitidas_pagos)
285
        payments_retentions_export(archivo_retenciones, pagos_agrupados)
286
287
    try:
288
        workbook.close()
289
        return archivo_excel
290
    except FileCreateError:
291
        return None
292
293
294
def generate_pdf_template(template_name, fields):
295
    increment_template = facturacion_environment.get_template(template_name)
296
    md5_document = increment_template.render(
297
        fields
298
    )
299
    html = markdown(md5_document)
300
    pdf = HTML(string=html).write_pdf(
301
        target=None,
302
        stylesheets=[
303
            os.path.join(SOURCE_DIRECTORY, "markdown_styles", "markdown6.css"),
304
            CSS(
305
                string='@page { width: Letter; margin: 1.6cm 1.6cm 1.6cm 1.6cm; }'
306
            )
307
        ]
308
    )
309
    return pdf
310
311
312
def mf_pago_fmt(cfdi):
313
    i = cfdi
314
    if i['TipoDeComprobante'] == "I":
315
        return i['TipoDeComprobante'].code + ' ' + i['MetodoPago'].code + ' ' + (i['FormaPago'].code if i['FormaPago'].code != '99' else '  ')
316
    return i['TipoDeComprobante'].code + '       '
317
318
319
def ajustes_directory(dp: DatePeriod):
320
    return os.path.join(archivos_folder(dp), 'ajustes')
321
322
323
def preview_cfdis(cfdis):
324
    outfile = os.path.join(TEMP_DIRECTORY, "factura.html")
325
    Representable.html_write_all(
326
        objs=cfdis,
327
        target=outfile,
328
    )
329
    os.startfile(
330
        os.path.abspath(outfile)
331
    )
332
333
334
def center_location(element) -> tuple[int, int]:
335
    return tuple((c + s // 3) for c, s in zip(element.current_location(), element.size))
336