diff --git a/.gitignore b/.gitignore index f5d45f4..1280f9f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ vedev/ # Virtualenv .env/ virtual/ +env docs/build cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 07bb63d..caebc4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +v 2.0.0 [31-Mar-2022] +---------------------- + - Primera versión de timbrado con CFDI4 + - **IMPORTANTE** NO intentes timbrar si **antes** no has validado en nuestro demo que puedes timbrar tus CFDIs habituales. + + v 1.47.0 [28-Mar-2022] ---------------------- - Mejora: Soporte basico para complemento Comercio Exterior. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..b66a3c0 --- /dev/null +++ b/TODO.md @@ -0,0 +1,2 @@ +[ ] Permitir más de un remolque en la Carta Porte +[ ] Representación impresa de Comercio Exterior diff --git a/VERSION b/VERSION index 21998d3..227cea2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.47.0 +2.0.0 diff --git a/docs/delete tables.md b/docs/delete tables.md new file mode 100644 index 0000000..422755c --- /dev/null +++ b/docs/delete tables.md @@ -0,0 +1,18 @@ +prefacturasdetalle; +prefacturasimpuestos; +prefacturas; + +ticketsimpuestos; +ticketsdetalle; +tickets; + +facturasrelacionadas; +facturaspersonalizados; +facturaspagos; +facturasimpuestos; +facturasdetalle; +facturascomplementos; +facturas; + +cfdipagos; +movimientosbanco; diff --git a/source/app/controllers/cfdi_xml.py b/source/app/controllers/cfdi_xml.py index 5e17926..f02e587 100644 --- a/source/app/controllers/cfdi_xml.py +++ b/source/app/controllers/cfdi_xml.py @@ -25,16 +25,21 @@ from logbook import Logger log = Logger('XML') -CFDI_ACTUAL = 'cfdi33' +CFDI_ACTUAL = 'cfdi40' NOMINA_ACTUAL = 'nomina12' +DEFAULT = { + 'exportacion': '01', +} + + SAT = { 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'cfdi32': { - 'version': '3.2', + 'cfdi40': { + 'version': '4.0', 'prefix': 'cfdi', - 'xmlns': 'http://www.sat.gob.mx/cfd/3', - 'schema': 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd', + 'xmlns': 'http://www.sat.gob.mx/cfd/4', + 'schema': 'http://www.sat.gob.mx/cfd/4 http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd', }, 'cfdi33': { 'version': '3.3', @@ -42,6 +47,12 @@ SAT = { 'xmlns': 'http://www.sat.gob.mx/cfd/3', 'schema': 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv33.xsd', }, + 'cfdi32': { + 'version': '3.2', + 'prefix': 'cfdi', + 'xmlns': 'http://www.sat.gob.mx/cfd/3', + 'schema': 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd', + }, 'nomina11': { 'version': '1.1', 'prefix': 'nomina', @@ -80,10 +91,10 @@ SAT = { 'schema': ' http://www.sat.gob.mx/iedu http://www.sat.gob.mx/sitio_internet/cfd/iedu/iedu.xsd', }, 'pagos': { - 'version': '1.0', - 'prefix': 'pago10', - 'xmlns': 'http://www.sat.gob.mx/Pagos', - 'schema': ' http://www.sat.gob.mx/Pagos http://www.sat.gob.mx/sitio_internet/cfd/Pagos/Pagos10.xsd', + 'version': '2.0', + 'prefix': 'pago20', + 'xmlns': 'http://www.sat.gob.mx/Pagos20', + 'schema': ' http://www.sat.gob.mx/Pagos20 http://www.sat.gob.mx/sitio_internet/cfd/Pagos/Pagos20.xsd', }, 'divisas': { 'version': '1.0', @@ -140,6 +151,7 @@ class CFDI(object): return '' self._comprobante(datos['comprobante']) + self._informacion_global(datos['global']) self._relacionados(datos['relacionados']) self._emisor(datos['emisor']) self._receptor(datos['receptor']) @@ -270,9 +282,21 @@ class CFDI(object): if not 'Fecha' in attributes: attributes['Fecha'] = self._now() + # ~ cfdi4 + if not 'Exportacion' in attributes: + attributes['Exportacion'] = DEFAULT['exportacion'] + self._cfdi = ET.Element('{}:Comprobante'.format(self._pre), attributes) return + def _informacion_global(self, datos): + if not datos: + return + + node_name = '{}:InformacionGlobal'.format(self._pre) + node = ET.SubElement(self._cfdi, node_name, datos) + return + def _relacionados(self, datos): if not datos or not datos['tipo'] or not datos['cfdis']: return @@ -292,6 +316,7 @@ class CFDI(object): return def _receptor(self, datos): + datos['Nombre'] = datos['Nombre'].upper() node_name = '{}:Receptor'.format(self._pre) emisor = ET.SubElement(self._cfdi, node_name, datos) return @@ -553,14 +578,41 @@ class CFDI(object): if 'pagos' in datos: datos = datos.pop('pagos') + totales = datos.pop('totales') relacionados = datos.pop('relacionados') + taxes_pay = datos.pop('taxes_pay') pre = SAT['pagos']['prefix'] + attributes = {'Version': SAT['pagos']['version']} pagos = ET.SubElement( self._complemento, '{}:Pagos'.format(pre), attributes) + + ET.SubElement(pagos, '{}:Totales'.format(pre), totales) + node_pago = ET.SubElement(pagos, '{}:Pago'.format(pre), datos) for row in relacionados: - ET.SubElement(node_pago, '{}:DoctoRelacionado'.format(pre), row) + taxes = row.pop('taxes') + node = ET.SubElement(node_pago, f'{pre}:DoctoRelacionado', row) + node_tax = ET.SubElement(node, f'{pre}:ImpuestosDR') + if taxes['retenciones']: + node = ET.SubElement(node_tax, f'{pre}:RetencionesDR') + for tax in taxes['retenciones']: + ET.SubElement(node, f'{pre}:RetencionDR', tax) + if taxes['traslados']: + node = ET.SubElement(node_tax, f'{pre}:TrasladosDR') + for tax in taxes['traslados']: + ET.SubElement(node, f'{pre}:TrasladoDR', tax) + + node_tax = ET.SubElement(node_pago, f'{pre}:ImpuestosP') + if taxes_pay['retenciones']: + node = ET.SubElement(node_tax, f'{pre}:RetencionsP') + for key, importe in taxes_pay['retenciones'].items(): + attr = {'ImpuestoP': key, 'ImporteP': importe} + ET.SubElement(node, f'{pre}:RetencionP', attr) + if taxes_pay['traslados']: + node = ET.SubElement(node_tax, f'{pre}:TrasladosP') + for key, tax in taxes_pay['traslados'].items(): + ET.SubElement(node, f'{pre}:TrasladoP', tax) if 'leyendas' in datos: pre = SAT['leyendas']['prefix'] diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py index 1b333ed..1981b6e 100644 --- a/source/app/controllers/main.py +++ b/source/app/controllers/main.py @@ -797,3 +797,27 @@ class AppSATUnidadesPeso(object): user = req.env['beaker.session']['userobj'] req.context['result'] = self._db.sat_unidades_peso_post(values, user) resp.status = falcon.HTTP_200 + + +class AppSATRegimenes(object): + + def __init__(self, db): + self._db = db + + def on_get(self, req, resp): + values = req.params + user = req.env['beaker.session']['userobj'] + req.context['result'] = self._db.sat_regimenes_get(values, user) + resp.status = falcon.HTTP_200 + + +class AppSociosRegimenes(object): + + def __init__(self, db): + self._db = db + + def on_get(self, req, resp): + values = req.params + user = req.env['beaker.session']['userobj'] + req.context['result'] = self._db.socios_regimenes_get(values, user) + resp.status = falcon.HTTP_200 diff --git a/source/app/controllers/pacs/comerciodigital/comercio.py b/source/app/controllers/pacs/comerciodigital/comercio.py index a06c5a7..4e08469 100644 --- a/source/app/controllers/pacs/comerciodigital/comercio.py +++ b/source/app/controllers/pacs/comerciodigital/comercio.py @@ -41,6 +41,12 @@ logging.getLogger('requests').setLevel(logging.ERROR) TIMEOUT = 10 +NAMESPACES = { + '3.3': 'http://www.sat.gob.mx/cfd/3', + '4.0': 'http://www.sat.gob.mx/cfd/4', + 'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital', +} + def pretty_print_POST(req): """ @@ -63,7 +69,7 @@ class PACComercioDigital(object): ws = 'https://{}.comercio-digital.mx/{}' api = 'https://app2.comercio-digital.mx/{}' URL = { - 'timbra': ws.format('ws', 'timbre/timbrarV5.aspx'), + 'timbra': ws.format('ws', 'timbre4/timbrarV5'), 'cancel': ws.format('cancela', 'cancela4/cancelarUuid'), 'cancelxml': ws.format('cancela', 'cancela4/cancelarXml'), 'status': ws.format('cancela', 'arws/consultaEstatus'), @@ -78,7 +84,7 @@ class PACComercioDigital(object): '702': '702 Error rfc/empresa invalido', } NS_CFDI = { - 'cfdi': 'http://www.sat.gob.mx/cfd/3', + 'cfdi': 'http://www.sat.gob.mx/cfd/4', 'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital', } @@ -86,7 +92,7 @@ class PACComercioDigital(object): ws = 'https://pruebas.comercio-digital.mx/{}' ws6 = 'https://pruebas6.comercio-digital.mx/arws/{}' URL = { - 'timbra': ws.format('timbre/timbrarV5.aspx'), + 'timbra': ws.format('timbre4/timbrarV5'), 'cancel': ws.format('cancela4/cancelarUuid'), 'cancelxml': ws.format('cancela4/cancelarXml'), 'status': ws6.format('consultaEstatus'), @@ -175,26 +181,29 @@ class PACComercioDigital(object): info['key'] = base64.b64encode(info['key_enc']).decode() info['cer'] = base64.b64encode(info['cer_ori']).decode() - NS_CFDI = { - 'cfdi': 'http://www.sat.gob.mx/cfd/3', - 'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital', - } tree = ET.fromstring(cfdi.encode()) + version = tree.attrib['Version'] + + namespaces = { + 'cfdi': NAMESPACES[version], + 'tdf': NAMESPACES['tdf'], + } + tipo = tree.xpath( 'string(//cfdi:Comprobante/@TipoDeComprobante)', - namespaces=NS_CFDI) + namespaces=namespaces) total = tree.xpath( 'string(//cfdi:Comprobante/@Total)', - namespaces=NS_CFDI) + namespaces=namespaces) rfc_emisor = tree.xpath( 'string(//cfdi:Comprobante/cfdi:Emisor/@Rfc)', - namespaces=NS_CFDI) + namespaces=namespaces) rfc_receptor = tree.xpath( 'string(//cfdi:Comprobante/cfdi:Receptor/@Rfc)', - namespaces=NS_CFDI) + namespaces=namespaces) uid = tree.xpath( 'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@UUID)', - namespaces=NS_CFDI) + namespaces=namespaces) data = ( f"USER={auth['user']}", f"PWDW={auth['pass']}", diff --git a/source/app/controllers/pacs/finkok/finkok.py b/source/app/controllers/pacs/finkok/finkok.py index 60e2f8a..9ef693d 100644 --- a/source/app/controllers/pacs/finkok/finkok.py +++ b/source/app/controllers/pacs/finkok/finkok.py @@ -74,7 +74,7 @@ class PACFinkok(object): WS = 'https://facturacion.finkok.com/servicios/soap/{}.wsdl' NS_TYPE = 'ns1' if DEBUG: - WS = 'http://demo-facturacion.finkok.com/servicios/soap/{}.wsdl' + WS = 'https://demo-facturacion.finkok.com/servicios/soap/{}.wsdl' NS_TYPE = 'ns0' URL = { 'quick_stamp': False, diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 548bbf1..407158d 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -78,9 +78,11 @@ import segno from .pacs.cfdi_cert import SATCertificate from settings import ( + CFDI_VERSIONS, EXT, MXN, PATHS, + PRE_DEFAULT, ) @@ -636,6 +638,12 @@ class LIBO(object): self._set_cell('{cfdi.%s}' % k, v) return + def _informacion_global(self, data): + for k, v in data.items(): + print(k, v) + self._set_cell('{cfdi.%s}' % k, v) + return + def _emisor(self, data): for k, v in data.items(): self._set_cell('{emisor.%s}' % k, v) @@ -873,8 +881,6 @@ class LIBO(object): image = self._template.createInstance('com.sun.star.drawing.GraphicObjectShape') gp = self._create_instance('com.sun.star.graphic.GraphicProvider') pd.add(image) - # ~ image.GraphicURL = data['path_cbb'] - # ~ properties = self._set_properties({'URL': self._path_url(data['path_cbb'])}) instance = 'com.sun.star.io.SequenceInputStream' stream = self._create_instance(instance) @@ -883,11 +889,11 @@ class LIBO(object): image.Graphic = gp.queryGraphic(properties) s = Size() - s.Width = 4150 - s.Height = 4500 + s.Width = 4000 + s.Height = 4000 image.setSize(s) image.Anchor = self._set_cell('{timbre.cbb}') - # ~ _kill(data['path_cbb']) + return def _donataria(self, data): @@ -1119,6 +1125,8 @@ class LIBO(object): return def _cfdipays(self, data): + version = data['Version'] + related = data.pop('related', []) for k, v in data.items(): if k.lower() in ('monto',): @@ -1221,6 +1229,7 @@ class LIBO(object): pakings = data.pop('pakings', []) self._comprobante(data['comprobante']) + self._informacion_global(data['informacion_global']) self._emisor(data['emisor']) self._receptor(data['receptor']) self._conceptos(data['conceptos'], pakings) @@ -1358,6 +1367,7 @@ class LIBO(object): 'codigo_postal', 'notas', 'correo', + 'regimen_fiscal', ) rows = tuple([dict(zip(fields, r)) for r in data[1:]]) msg = 'Empleados importados correctamente' @@ -1553,36 +1563,40 @@ class LIBO(object): def to_pdf(data, emisor_rfc, ods=False, pdf_from='1'): rfc = data['emisor']['rfc'] - default = 'plantilla_factura.ods' if DEBUG: rfc = emisor_rfc version = data['comprobante']['version'] + default = f'plantilla_factura_{version}.ods' if 'nomina' in data and data['nomina']: - default = 'plantilla_nomina.ods' - version = '{}_{}'.format(data['nomina']['version'], version) + version_nomina = data['nomina']['version'] + default = f'plantilla_nomina_{version}_{version_nomina}.ods' + version = f'{version}_cn_{version_nomina}' if 'carta_porte' in data: - default = 'plantilla_factura_cp.ods' - version = '{}_cp_{}'.format(version, data['carta_porte']['version']) + default = 'plantilla_factura_ccp.ods' + version = '{}_ccp_{}'.format(version, data['carta_porte']['version']) - pagos = '' if data.get('pagos', False): - version = '1.0' - pagos = 'pagos_' + version_pagos = data['pays']['version'] + default = f'plantilla_pagos_{version}_{version_pagos}.ods' + version = f'{version}_cp_{version_pagos}' if pdf_from == '2': return to_pdf_from_json(rfc, version, data) + donativo = '' + if data['donativo']: + donativo = '_donativo' + + template_name = f'{rfc.lower()}_{version}.ods' + # ~ print('T', template_name, default) + if APP_LIBO: app = LIBO() if app.is_running: - donativo = '' - if data['donativo']: - donativo = '_donativo' - name = '{}_{}{}{}.ods'.format(rfc.lower(), pagos, version, donativo) - path = get_template_ods(name, default) + path = get_template_ods(template_name, default) if path: return app.pdf(path, data, ods) @@ -1662,10 +1676,11 @@ def html_to_pdf(data): def import_employees(rfc): + msg = 'No se pudo cargar el archivo' name = '{}_employees.ods'.format(rfc.lower()) path = _join(PATH_MEDIA, 'tmp', name) if not is_file(path): - return () + return (), msg msg = 'LibreOffice no se pudo iniciar' if APP_LIBO: @@ -1754,7 +1769,7 @@ def _comprobante(doc, options): data['tiporelacion'] = options.get('tiporelacion', '') return data - if data['version'] == '3.3': + if data['version'] in CFDI_VERSIONS: tipos = { 'I': 'ingreso', 'E': 'egreso', @@ -1839,7 +1854,7 @@ def _receptor(doc, version, values): return data data['usocfdi'] = values['usocfdi'] - data.update(values['receptor']) + # ~ data.update(values['receptor']) return data @@ -1857,7 +1872,7 @@ def _conceptos(doc, version, options): data.append(values) continue - if version == '3.3': + if version in CFDI_VERSIONS: if 'noidentificacion' in values: values['noidentificacion'] = '{}\n(SAT {})'.format( values['noidentificacion'], values['ClaveProdServ']) @@ -1921,7 +1936,7 @@ def _totales(doc, cfdi, version): # ~ for n in node.getchildren(): for n in list(node): tmp = CaseInsensitiveDict(n.attrib.copy()) - if version == '3.3': + if version in CFDI_VERSIONS: tasa = round(float(tmp['tasaocuota']), DECIMALES) title = 'Traslado {} {}'.format(tn.get(tmp['impuesto']), tasa) else: @@ -1933,7 +1948,7 @@ def _totales(doc, cfdi, version): # ~ for n in node.getchildren(): for n in list(node): tmp = CaseInsensitiveDict(n.attrib.copy()) - if version == '3.3': + if version in CFDI_VERSIONS: title = 'Retención {} {}'.format( tn.get(tmp['impuesto']), '') else: @@ -1965,7 +1980,7 @@ def _totales(doc, cfdi, version): def _timbre(doc, version, values, pdf_from='1'): CADENA = '||{version}|{UUID}|{FechaTimbrado}|{selloCFD}|{noCertificadoSAT}||' - if version == '3.3': + if version in CFDI_VERSIONS: CADENA = '||{Version}|{UUID}|{FechaTimbrado}|{SelloCFD}|{NoCertificadoSAT}||' node = doc.find('{}Complemento/{}TimbreFiscalDigital'.format( PRE[version], PRE['TIMBRE'])) @@ -2095,19 +2110,53 @@ def _nomina(doc, data, values, version_cfdi): return info +def _get_info_pays_2(node): + pre_pays = PRE_DEFAULT['PAGOS']['PRE'] + data = CaseInsensitiveDict(node.attrib.copy()) + + path = f"{pre_pays}Totales" + totales = node.find(path) + data.update(CaseInsensitiveDict(totales.attrib.copy())) + + path = f"{pre_pays}Pago" + node_pay = node.find(path) + data.update(CaseInsensitiveDict(node_pay.attrib.copy())) + + related = [] + for n in node_pay: + attr = CaseInsensitiveDict(n.attrib.copy()) + if attr: + attr['metododepagodr'] = '' + related.append(attr) + + data['related'] = related + + return data + + def _cfdipays(doc, data, version): - node = doc.find('{}Complemento/{}Pagos'.format(PRE[version], PRE['pagos'])) + #todo: Obtener versión de complemento + if version == '4.0': + pre_pays = PRE_DEFAULT['PAGOS']['PRE'] + path = f"{PRE[version]}Complemento/{pre_pays}Pagos" + node = doc.find(path) + else: + node = doc.find('{}Complemento/{}Pagos'.format(PRE[version], PRE['pagos'])) + if node is None: return {} - info = CaseInsensitiveDict(node.attrib.copy()) - related = [] - for n1 in node: - info.update(CaseInsensitiveDict(n1.attrib.copy())) - for n2 in n1: - related.append(CaseInsensitiveDict(n2.attrib.copy())) + if version == '4.0': + info = _get_info_pays_2(node) + else: + info = CaseInsensitiveDict(node.attrib.copy()) + related = [] + for n1 in node: + info.update(CaseInsensitiveDict(n1.attrib.copy())) + for n2 in n1: + related.append(CaseInsensitiveDict(n2.attrib.copy())) - info['related'] = related + info['related'] = related data['comprobante']['totalenletras'] = to_letters( float(info['monto']), info['monedap']) @@ -2120,6 +2169,7 @@ def get_data_from_xml(invoice, values, pdf_from='1'): data = {'cancelada': invoice.cancelada, 'donativo': False} if hasattr(invoice, 'donativo'): data['donativo'] = invoice.donativo + doc = parse_xml(invoice.xml) data['comprobante'] = _comprobante(doc, values) version = data['comprobante']['version'] @@ -2226,11 +2276,33 @@ class UpFile(object): return +def save_template(rfc, opt, file_obj): + result = {'status': 'error', 'ok': False} + + name_template = f'{rfc}{opt}' + path_template = _join(PATH_MEDIA, 'templates', name_template) + + if save_file(path_template, file_obj.file.read()): + result = {'status': 'server', 'name': file_obj.filename, 'ok': True} + + return result + + def upload_file(rfc, opt, file_obj): rfc = rfc.lower() tmp = file_obj.filename.split('.') ext = tmp[-1].lower() + versions = ('_3.2.ods', + '_3.3.ods', '_3.3_cn_1.2.ods', '_3.3_ccp_2.0.ods', + '_4.0.ods', + '_4.0_cn_1.2.ods', + '_4.0_cp_2.0.ods', + '_4.0_ccp_2.0.ods', + '_4.0_cd_1.1.ods') + if opt in versions: + return save_template(rfc, opt, file_obj) + EXTENSIONS = { 'txt_plantilla_factura_32': EXT['ODS'], 'txt_plantilla_factura_33': EXT['ODS'], diff --git a/source/app/controllers/utils.py b/source/app/controllers/utils.py index 6df126f..cbcb16e 100644 --- a/source/app/controllers/utils.py +++ b/source/app/controllers/utils.py @@ -55,7 +55,7 @@ from dateutil import parser from .cfdi_xml import CFDI -from settings import DEBUG, DB_COMPANIES, PATHS, TEMPLATE_CANCEL, RFCS +from settings import DEBUG, DB_COMPANIES, PATHS, TEMPLATE_CANCEL, RFCS, PRE from .pacs.cfdi_cert import SATCertificate from .pacs import PACComercioDigital @@ -88,6 +88,7 @@ PACS = { 'finkok': PACFinkok, 'comercio': PACComercioDigital, } + NS_CFDI = { 'cfdi': 'http://www.sat.gob.mx/cfd/3', 'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital', @@ -255,11 +256,15 @@ class SendMail(object): class CfdiToDict(object): + NS_VERSION = { + 'cfdi3.3': 'http://www.sat.gob.mx/cfd/3', + 'cfdi4.0': 'http://www.sat.gob.mx/cfd/4', + } NS = { - 'cfdi': 'http://www.sat.gob.mx/cfd/3', 'divisas': 'http://www.sat.gob.mx/divisas', 'leyendasFisc': 'http://www.sat.gob.mx/leyendasFiscales', 'cartaporte20': 'http://www.sat.gob.mx/CartaPorte20', + 'nomina12': 'http://www.sat.gob.mx/nomina12', } tipo_figura = { '01': '[01] Operador', @@ -267,6 +272,53 @@ class CfdiToDict(object): '03': '[03] Arrendador', '04': '[04] Notificado', } + REGIMEN_FISCAL = { + '601': '[601] General de Ley Personas Morales', + '603': '[603] Personas Morales con Fines no Lucrativos', + '605': '[605] Sueldos y Salarios e Ingresos Asimilados a Salarios', + '606': '[606] Arrendamiento', + '607': '[607] Régimen de Enajenación o Adquisición de Bienes', + '608': '[608] Demás ingresos', + '610': '[610] Residentes en el Extranjero sin Establecimiento Permanente en México', + '611': '[611] Ingresos por Dividendos (socios y accionistas)', + '612': '[612] Personas Físicas con Actividades Empresariales y Profesionales', + '614': '[614] Ingresos por intereses', + '615': '[615] Régimen de los ingresos por obtención de premios', + '616': '[616] Sin obligaciones fiscales', + '620': '[620] Sociedades Cooperativas de Producción que optan por diferir sus ingresos', + '621': '[621] Incorporación Fiscal', + '622': '[622] Actividades Agrícolas, Ganaderas, Silvícolas y Pesqueras', + '623': '[623] Opcional para Grupos de Sociedades', + '624': '[624] Coordinados', + '625': '[625] Régimen de las Actividades Empresariales con ingresos a través de Plataformas Tecnológicas', + '626': '[626] Régimen Simplificado de Confianza', + } + USO_CFDI = { + 'G01': '[G01] Adquisición de mercancías.', + 'G02': '[G02] Devoluciones, descuentos o bonificaciones.', + 'G03': '[G03] Gastos en general.', + 'I01': '[I01] Construcciones.', + 'I02': '[I02] Mobiliario y equipo de oficina por inversiones.', + 'I03': '[I03] Equipo de transporte.', + 'I04': '[I04] Equipo de computo y accesorios.', + 'I05': '[I05] Dados, troqueles, moldes, matrices y herramental.', + 'I06': '[I06] Comunicaciones telefónicas.', + 'I07': '[I07] Comunicaciones satelitales.', + 'I08': '[I08] Otra maquinaria y equipo.', + 'D01': '[D01] Honorarios médicos, dentales y gastos hospitalarios.', + 'D02': '[D02] Gastos médicos por incapacidad o discapacidad.', + 'D03': '[D03] Gastos funerales.', + 'D04': '[D04] Donativos.', + 'D05': '[D05] Intereses reales efectivamente pagados por créditos hipotecarios (casa habitación).', + 'D06': '[D06] Aportaciones voluntarias al SAR.', + 'D07': '[D07] Primas por seguros de gastos médicos.', + 'D08': '[D08] Gastos de transportación escolar obligatoria.', + 'D09': '[D09] Depósitos en cuentas para el ahorro, primas que tengan como base planes de pensiones.', + 'D10': '[D10] Pagos por servicios educativos (colegiaturas).', + 'S01': '[S01] Sin efectos fiscales.', + 'CP01': '[CP01] Pagos', + 'CN01': '[CN01] Nómina', + } PAISES = { 'MEX': 'México', } @@ -305,8 +357,30 @@ class CfdiToDict(object): 'YUC': 'Yucatán', 'ZAC': 'Zacatecas', } + PERIODICIDAD = { + '01': '[01] Diario', + '02': '[02] Semanal', + '03': '[03] Quincenal', + '04': '[04] Mensual', + '05': '[05] Bimestral', + } + MESES = { + '01': '[01] Enero', + '02': '[02] Febrero', + '03': '[03] Marzo', + '04': '[04] Abril', + '05': '[05] Mayo', + '06': '[06] Junio', + '07': '[07] Julio', + '08': '[08] Agosto', + '09': '[09] Septiembre', + '10': '[10] Octubre', + '11': '[11] Noviembre', + '12': '[12] Diciembre', + } def __init__(self, xml): + self.version = '' self._values = { 'leyendas': (), } @@ -318,9 +392,42 @@ class CfdiToDict(object): return self._values def _get_values(self): + self.version = self._root.attrib['Version'] + ns = f'cfdi{self.version}' + self.NS['cfdi'] = self.NS_VERSION[ns] + self._informacion_global() + self._receptor() self._complementos() return + def _informacion_global(self): + self._values['informacion_global'] = {} + + path = '//cfdi:InformacionGlobal' + data = self._root.xpath(path, namespaces=self.NS) + if not data: + return + + data = data[0] + attr = CaseInsensitiveDict(data.attrib) + + value = f"Periodicidad Factura Global: {self.PERIODICIDAD[attr['Periodicidad']]} " + value += f"del mes {self.MESES[attr['Meses']]} " + value += f"del año {attr['Año']}" + self._values['informacion_global'] = {'informacion_global': value} + return + + def _receptor(self): + path = '//cfdi:Receptor' + receptor = self._root.xpath(path, namespaces=self.NS)[0] + attr = CaseInsensitiveDict(receptor.attrib) + attr['usocfdi'] = self.USO_CFDI[attr['UsoCFDI']] + if self.version == '4.0': + attr['domiciliofiscal'] = attr['DomicilioFiscalReceptor'] + attr['regimenfiscal'] = self.REGIMEN_FISCAL[attr['RegimenFiscalReceptor']] + self._values['receptor'] = attr + return + def _set_carta_porte_domicilio(self, data): municipio = data['Municipio'] estado = self.ESTADOS[data['Estado']] @@ -332,6 +439,14 @@ class CfdiToDict(object): path = '//cfdi:Complemento' complemento = self._root.xpath(path, namespaces=self.NS)[0] + path = '//nomina12:Nomina' + nomina = complemento.xpath(path, namespaces=self.NS) + if nomina: + for node in nomina[0]: + if 'Receptor' in node.tag: + attr = CaseInsensitiveDict(node.attrib) + self._values['receptor'].update(attr) + path = '//divisas:Divisas' divisas = complemento.xpath(path, namespaces=self.NS) if divisas: @@ -725,6 +840,9 @@ def xml_cancel(xml, auth, cert, name): def get_client_balance(auth, rfc=''): + if not auth: + return 'p/c' + pac = PACS[auth['pac']]() balance = pac.client_balance(auth, rfc) if pac.error: @@ -760,8 +878,10 @@ def make_xml(data, certificado): def get_pac_by_rfc(cfdi): tree = ET.fromstring(cfdi.encode()) + version = tree.attrib['Version'] + namespaces = {'cfdi': PRE[version][1:-1], 'tdf': PRE['TIMBRE'][1:-1]} path = 'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@RfcProvCertif)' - rfc_pac = tree.xpath(path, namespaces=NS_CFDI) + rfc_pac = tree.xpath(path, namespaces=namespaces) return RFCS[rfc_pac] @@ -827,17 +947,20 @@ def cancel_xml_sign(invoice, args, auth, certificado): def _get_data_sat(xml): BF = 'string(//*[local-name()="{}"]/@{})' - NS_CFDI = {'cfdi': 'http://www.sat.gob.mx/cfd/3'} + # ~ NS_CFDI = {'cfdi': 'http://www.sat.gob.mx/cfd/3'} try: tree = ET.fromstring(xml.encode()) + version = tree.attrib['Version'] + namespaces = {'cfdi': PRE[version][1:-1]} + emisor = escape( - tree.xpath('string(//cfdi:Emisor/@rfc)', namespaces=NS_CFDI) or - tree.xpath('string(//cfdi:Emisor/@Rfc)', namespaces=NS_CFDI) + tree.xpath('string(//cfdi:Emisor/@rfc)', namespaces=namespaces) or + tree.xpath('string(//cfdi:Emisor/@Rfc)', namespaces=namespaces) ) receptor = escape( - tree.xpath('string(//cfdi:Receptor/@rfc)', namespaces=NS_CFDI) or - tree.xpath('string(//cfdi:Receptor/@Rfc)', namespaces=NS_CFDI) + tree.xpath('string(//cfdi:Receptor/@rfc)', namespaces=namespaces) or + tree.xpath('string(//cfdi:Receptor/@Rfc)', namespaces=namespaces) ) total = tree.get('total') or tree.get('Total') uuid = tree.xpath(BF.format('TimbreFiscalDigital', 'UUID')) diff --git a/source/app/main.py b/source/app/main.py index 6346cd4..05fcb02 100644 --- a/source/app/main.py +++ b/source/app/main.py @@ -25,6 +25,8 @@ from controllers.main import (AppEmpresas, AppWareHouse, AppWareHouseProduct, AppSATUnidadesPeso, + AppSATRegimenes, + AppSociosRegimenes, ) @@ -78,6 +80,8 @@ api.add_route('/warehouseproduct', AppWareHouseProduct(db)) api.add_route('/ticketsdetails', AppTicketsDetails(db)) api.add_route('/users', AppUsers(db)) api.add_route('/satunidadespeso', AppSATUnidadesPeso(db)) +api.add_route('/satregimenes', AppSATRegimenes(db)) +api.add_route('/sociosregimenes', AppSociosRegimenes(db)) session_options = { diff --git a/source/app/models/db.py b/source/app/models/db.py index 3d55374..e410cd2 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -526,6 +526,12 @@ class StorageEngine(object): def sat_unidades_peso_post(self, args, user): return main.SATUnidadesPeso.post(args, user) + def sat_regimenes_get(self, filters, user): + return main.SATRegimenes.get_data(filters, user) + + def socios_regimenes_get(self, filters, user): + return main.SociosRegimenes.get_data(filters, user) + # Companies only in MV def _get_empresas(self, values): return main.companies_get() diff --git a/source/app/models/main.py b/source/app/models/main.py index 10ea921..85a73d7 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -32,9 +32,9 @@ if __name__ == '__main__': from controllers import util -from settings import log, COMPANIES, VERSION, PATH_CP, PRE, CURRENT_CFDI, \ +from settings import log, COMPANIES, VERSION, PATH_CP, PRE, \ INIT_VALUES, DEFAULT_PASSWORD, DECIMALES, IMPUESTOS, DEFAULT_SAT_PRODUCTO, \ - CANCEL_SIGNATURE, PUBLIC, DEFAULT_SERIE_TICKET, CURRENT_CFDI_NOMINA, \ + CANCEL_SIGNATURE, PUBLIC, DEFAULT_SERIE_TICKET, \ DEFAULT_SAT_NOMINA, DECIMALES_TAX, TITLE_APP, MV, DECIMALES_PRECIOS, \ DEFAULT_CFDIPAY, CURRENCY_MN @@ -50,6 +50,7 @@ from settings import ( IS_MV, MXN, PATHS, + PRE_DEFAULT, URL, VALUES_PDF, VERSION as VERSION_EMPRESA_LIBRE, @@ -466,7 +467,7 @@ class Configuracion(BaseModel): 'chk_config_codigo_barras', 'chk_config_precio_con_impuestos', 'chk_llevar_inventario', - 'chk_use_packing', + # ~ 'chk_use_packing', 'chk_multi_stock', ) data = (Configuracion @@ -692,8 +693,6 @@ class Configuracion(BaseModel): ) elif keys['fields'] == 'templates': fields = ( - 'txt_plantilla_factura_32', - 'txt_plantilla_factura_33', 'txt_plantilla_factura_html', 'txt_plantilla_factura_css', 'txt_plantilla_factura_json', @@ -939,6 +938,26 @@ class SATRegimenes(BaseModel): ) return tuple(rows) + @classmethod + def _get_actives(cls, filters, user): + where = ((SATRegimenes.activo==True) & (SATRegimenes.fisica==True)) + if (filters['morales']=='true'): + where = ((SATRegimenes.activo==True) & (SATRegimenes.moral==True)) + + rows = (SATRegimenes + .select( + SATRegimenes.id, + SATRegimenes.name.alias('value')) + .where(where) + .dicts() + ) + return tuple(rows) + + @classmethod + def get_data(cls, filters, user): + opt = filters['opt'] + return getattr(cls, f'_get_{opt}')(filters, user) + class Emisor(BaseModel): rfc = TextField(unique=True) @@ -1027,8 +1046,6 @@ class Emisor(BaseModel): 'ong_autorizacion': obj.autorizacion, 'ong_fecha': obj.fecha_autorizacion, 'ong_fecha_dof': obj.fecha_dof, - # ~ 'correo_timbrado': obj.correo_timbrado, - # ~ 'token_timbrado': obj.token_timbrado, 'token_soporte': obj.token_soporte, 'emisor_registro_patronal': obj.registro_patronal, 'regimenes': [row.id for row in obj.regimenes] @@ -1745,7 +1762,7 @@ class SATImpuestos(BaseModel): tipo = 'R' row = { - 'key': IMPUESTOS.get(values['impuesto']), + 'key': IMPUESTOS.get(values['impuesto'], '000'), 'name': values['impuesto'], 'tipo': tipo, 'tasa': abs(tasa), @@ -2744,6 +2761,7 @@ class Socios(BaseModel): uso_cfdi = ForeignKeyField(SATUsoCfdi, null=True) tags = ManyToManyField(Tags, related_name='socios_tags') plantilla = TextField(default='') + regimen_fiscal = TextField(default='') def __str__(self): t = '{} ({})' @@ -2787,6 +2805,13 @@ class Socios(BaseModel): if fields['pais'] != 'México': fields['pais'] = fields['pais'].upper() + if 'regimenes' in fields: + fields['regimenes'] = utils.loads(fields['regimenes']) + if isinstance(fields['regimenes'], list): + fields['regimenes'] = tuple(map(int, fields['regimenes'])) + else: + fields['regimenes'] = (fields['regimenes'],) + return fields @classmethod @@ -2802,18 +2827,9 @@ class Socios(BaseModel): str(CondicionesPago.get(id=row['condicion_pago'])) row['partner_balance'] = row.pop('saldo_cliente') row['partner_email_fp'] = row.pop('correo_facturasp') + row['regimenes'] = SociosRegimenes.get_by_socio(row['id']) return row - #~ return {'data': data['rows'][:100], 'pos':0, 'total_count': 1300} - #~ start = 0 - #~ count = 0 - #~ end = 100 - #~ if values: - #~ {'start': '100', 'count': '100', 'continue': 'true'} - #~ start = int(values['start']) - #~ cont = int(values['count']) - #~ end = start + count - total = Socios.select().count() rows = (Socios @@ -2829,19 +2845,23 @@ class Socios(BaseModel): @classmethod def get_by_client(cls, values): id = int(values.get('id', 0)) + if id: row = (Socios .select( Socios.id, Socios.nombre, Socios.rfc, SATFormaPago.key.alias('forma_pago'), - SATUsoCfdi.key.alias('uso_cfdi')) + SATUsoCfdi.key.alias('uso_cfdi'), + Socios.codigo_postal) .join(SATFormaPago, JOIN.LEFT_OUTER).switch(Socios) .join(SATUsoCfdi, JOIN.LEFT_OUTER).switch(Socios) .where((Socios.id==id) & (Socios.es_cliente==True)) .dicts() ) if len(row): - return {'ok': True, 'row': row[0]} + client = row[0] + client['regimenes'] = SociosRegimenes.get_by_key(client['id']) + return {'ok': True, 'row': client} return {'ok': False} name = values.get('name', '') @@ -2849,7 +2869,8 @@ class Socios(BaseModel): rows = (Socios .select(Socios.id, Socios.nombre, Socios.rfc, SATFormaPago.key.alias('forma_pago'), - SATUsoCfdi.key.alias('uso_cfdi')) + SATUsoCfdi.key.alias('uso_cfdi'), + Socios.codigo_postal) .join(SATFormaPago, JOIN.LEFT_OUTER).switch(Socios) .join(SATUsoCfdi, JOIN.LEFT_OUTER).switch(Socios) .where((Socios.es_cliente==True & Socios.es_activo==True) & @@ -2863,6 +2884,7 @@ class Socios(BaseModel): def add(cls, values): accounts = util.loads(values.pop('accounts', '[]')) fields = cls._clean(cls, values) + regimenes = fields.pop('regimenes', ()) w = ((Socios.rfc==fields['rfc']) & (Socios.slug==fields['slug'])) if Socios.select().where(w).exists(): @@ -2892,6 +2914,16 @@ class Socios(BaseModel): except IntegrityError: pass + for regimen in regimenes: + try: + fields = { + 'socio': obj, + 'regimen': regimen, + } + SociosRegimenes.create(**fields) + except IntegrityError: + pass + row = { 'id': obj.id, 'rfc': obj.rfc, @@ -2905,6 +2937,8 @@ class Socios(BaseModel): def actualizar(cls, values, id): fields = cls._clean(cls, values) fields.pop('accounts', '') + regimenes = fields.pop('regimenes', ()) + try: q = Socios.update(**fields).where(Socios.id==id) q.execute() @@ -2913,6 +2947,19 @@ class Socios(BaseModel): data = {'ok': False, 'row': {}, 'new': True, 'msg': msg} return data + obj = Socios.get(Socios.id==id) + q = SociosRegimenes.delete().where(SociosRegimenes.socio==id) + q.execute() + for regimen in regimenes: + try: + fields = { + 'socio': obj, + 'regimen': regimen, + } + SociosRegimenes.create(**fields) + except IntegrityError: + pass + obj = Socios.get(Socios.id==id) row = { 'id': id, @@ -2934,6 +2981,8 @@ class Socios(BaseModel): q = SociosCuentasBanco.delete().where(SociosCuentasBanco.socio==id) q.execute() + q = SociosRegimenes.delete().where(SociosRegimenes.socio==id) + q.execute() q = Socios.delete().where(Socios.id==id) return bool(q.execute()) @@ -3040,6 +3089,52 @@ class SociosCuentasBanco(BaseModel): return account.socio == invoice.cliente +class SociosRegimenes(BaseModel): + socio = ForeignKeyField(Socios) + regimen = ForeignKeyField(SATRegimenes) + + class Meta: + indexes = ( + (('socio', 'regimen'), True), + ) + + @classmethod + def get_by_key(cls, socio, user=None): + fields = (SATRegimenes.key.alias('id'), SATRegimenes.name.alias('value')) + where = (SociosRegimenes.socio == socio) + regimenes = (SociosRegimenes + .select(*fields) + .where(where) + .join(SATRegimenes).switch(SociosRegimenes) + .dicts() + ) + return tuple(regimenes) + + @classmethod + def get_by_socio(cls, socio, user=None): + fields = (SATRegimenes.id,) + where = (SociosRegimenes.socio == socio) + regimenes = (SociosRegimenes + .select(*fields) + .where(where) + .join(SATRegimenes).switch(SociosRegimenes) + .tuples() + ) + regimenes = [r[0] for r in regimenes] + return regimenes + + @classmethod + def _get_by_id(cls, filters, user): + id = int(filters['id']) + return cls.get_by_key(id) + + @classmethod + def get_data(cls, filters, user): + # ~ print('FILERS', filters) + opt = filters['opt'] + return getattr(cls, f'_get_{opt}')(filters, user) + + class Contactos(BaseModel): socio = ForeignKeyField(Socios) titulo = ForeignKeyField(TipoTitulo) @@ -3615,7 +3710,6 @@ class Sucursales(BaseModel): def _create(cls, args): try: values = utils.loads(args) - print(values) Sucursales.create(**values) result = {'ok': True} except Exception as e: @@ -3806,6 +3900,7 @@ class Productos(BaseModel): cantidad_empaque = DecimalField(default=0.0, max_digits=14, decimal_places=4, auto_round=True) is_discontinued = BooleanField(default=False) + objeto_impuesto = TextField(default='02') class Meta: order_by = ('descripcion',) @@ -3829,6 +3924,7 @@ class Productos(BaseModel): Productos.descuento, Productos.inventario, Productos.existencia, + Productos.objeto_impuesto, ) where = ( (Productos.es_activo==True) & @@ -3908,7 +4004,8 @@ class Productos(BaseModel): Productos.valor_unitario, Productos.descuento, Productos.inventario, - Productos.existencia) + Productos.existencia, + Productos.objeto_impuesto) .join(SATUnidades).switch(Productos) .where((Productos.es_activo==True) & ((Productos.clave==clave) | (Productos.codigo_barras==clave))) @@ -4081,7 +4178,8 @@ class Productos(BaseModel): Productos.inventario, Productos.existencia, Productos.minimo, - Productos.cantidad_empaque.alias('cant_by_packing'), + Productos.objeto_impuesto, + # ~ Productos.cantidad_empaque.alias('cant_by_packing'), ) .where(Productos.id==id).dicts()[0] ) @@ -4344,7 +4442,7 @@ class RangosPrecios(BaseModel): class Facturas(BaseModel): cliente = ForeignKeyField(Socios) - version = TextField(default=CURRENT_CFDI) + version = TextField(default=PRE_DEFAULT['CFDI']['VERSION']) serie = TextField(default='') folio = BigIntegerField(default=0) fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S']) @@ -4389,6 +4487,9 @@ class Facturas(BaseModel): egreso_anticipo = BooleanField(default=False) tipo_relacion = TextField(default='') error = TextField(default='') + exportacion = TextField(default='01') + receptor_regimen = TextField(default='') + periodicidad = TextField(default='') class Meta: order_by = ('fecha',) @@ -4620,23 +4721,33 @@ class Facturas(BaseModel): obj = SATTipoRelacion.get(SATTipoRelacion.key==invoice.tipo_relacion) values['tiporelacion'] = str(obj) - receptor = Socios.select().where(Socios.id==invoice.cliente.id).dicts()[0] - values['receptor'] = {} - for k, v in receptor.items(): - values['receptor'][k] = v - - use_packing = Configuracion.get_bool('chk_use_packing') - if use_packing: - w = FacturasDetalle.factura == invoice - q = (FacturasDetalle - .select(FacturasDetalle.empaques) - .where(w) - .order_by(FacturasDetalle.id.asc()) - .tuples()) - values['pakings'] = [str(int(r[0])) for r in q] + # ~ use_packing = Configuracion.get_bool('chk_use_packing') + # ~ if use_packing: + # ~ w = FacturasDetalle.factura == invoice + # ~ q = (FacturasDetalle + # ~ .select(FacturasDetalle.empaques) + # ~ .where(w) + # ~ .order_by(FacturasDetalle.id.asc()) + # ~ .tuples()) + # ~ values['pakings'] = [str(int(r[0])) for r in q] return values + def _get_not_in_xml2(self, invoice, data): + fields = ( + Socios.calle, + Socios.no_exterior, + Socios.no_interior, + Socios.colonia, + Socios.municipio, + Socios.estado, + Socios.pais, + ) + where = (Socios.id==invoice.cliente.id) + partner = Socios.select(*fields).where(where).dicts()[0] + data['receptor'].update(partner) + return + @classmethod def get_pdf(cls, id, rfc, sync=True): try: @@ -4654,7 +4765,9 @@ class Facturas(BaseModel): #Tmp to v2 data = util.get_data_from_xml(obj, values, pdf_from) + data.update(utils.CfdiToDict(obj.xml).values) + cls._get_not_in_xml2(cls, obj, data) doc = util.to_pdf(data, emisor.rfc, pdf_from=pdf_from) @@ -5021,7 +5134,9 @@ class Facturas(BaseModel): 'id': obj.cliente.id, 'nombre': obj.cliente.nombre, 'rfc': obj.cliente.rfc, + 'codigo_postal': obj.cliente.codigo_postal, 'notas': obj.notas, + 'regimenes': SociosRegimenes.get_by_key(obj.cliente.id) } invoice = { 'tipo_comprobante': obj.tipo_comprobante, @@ -5197,12 +5312,11 @@ class Facturas(BaseModel): return result - def _calculate_totals(self, invoice, products, tipo_comprobante, user): tax_locales = Configuracion.get_bool('chk_config_tax_locales') tax_locales_truncate = Configuracion.get_bool('chk_config_tax_locales_truncate') tax_decimals = Configuracion.get_bool('chk_config_tax_decimals') - use_packing = Configuracion.get_bool('chk_use_packing') + # ~ use_packing = Configuracion.get_bool('chk_use_packing') subtotal = 0 descuento_cfdi = 0 @@ -5237,9 +5351,9 @@ class Facturas(BaseModel): precio_final = valor_unitario - descuento importe = round(cantidad * precio_final, DECIMALES) - if use_packing and p.cantidad_empaque: - product['empaques'] = utils.round_up( - cantidad / float(p.cantidad_empaque)) + # ~ if use_packing and p.cantidad_empaque: + # ~ product['empaques'] = utils.round_up( + # ~ cantidad / float(p.cantidad_empaque)) product['cantidad'] = cantidad product['valor_unitario'] = valor_unitario @@ -5519,7 +5633,14 @@ class Facturas(BaseModel): tax_decimals = Configuracion.get_bool('chk_config_tax_decimals') decimales_precios = Configuracion.get_bool('chk_config_decimales_precios') invoice_by_ticket = Configuracion.get_bool('chk_config_invoice_by_ticket') - is_global = (invoice.cliente.rfc == RFCS['PUBLIC']) and invoice_by_ticket + is_global = bool(invoice.periodicidad) + + data_global = {} + if is_global: + now = utils.now() + data_global['Periodicidad'] = invoice.periodicidad + data_global['Meses'] = now.strftime('%m') + data_global['Año'] = now.strftime('%Y') frm_vu = FORMAT if decimales_precios: @@ -5586,11 +5707,15 @@ class Facturas(BaseModel): 'Nombre': emisor.nombre, 'RegimenFiscal': invoice.regimen_fiscal, } + receptor = { 'Rfc': invoice.cliente.rfc, 'Nombre': invoice.cliente.nombre, 'UsoCFDI': invoice.uso_cfdi, + 'DomicilioFiscalReceptor': invoice.cliente.codigo_postal, + 'RegimenFiscalReceptor': invoice.receptor_regimen } + if invoice.cliente.tipo_persona == 4: if invoice.cliente.pais: receptor['ResidenciaFiscal'] = invoice.cliente.pais @@ -5612,11 +5737,13 @@ class Facturas(BaseModel): 'NoIdentificacion': key, 'Cantidad': FORMAT.format(row.cantidad), 'ClaveUnidad': row.unidad, - 'Unidad': SATUnidades.get(SATUnidades.key==row.unidad).name[:20], 'Descripcion': row.descripcion, 'ValorUnitario': frm_vu.format(row.valor_unitario), 'Importe': FORMAT.format(row.importe), } + if not is_global: + concepto['Unidad'] = SATUnidades.get(SATUnidades.key==row.unidad).name[:20] + if row.descuento: concepto['Descuento'] = FORMAT.format(row.descuento) @@ -5701,6 +5828,16 @@ class Facturas(BaseModel): taxes['retenciones'] = retenciones concepto['impuestos'] = taxes + + # cfdi4 + if not is_global: + concepto['ObjetoImp'] = row.producto.objeto_impuesto + else: + if taxes: + concepto['ObjetoImp'] = '02' + else: + concepto['ObjetoImp'] = '01' + conceptos.append(concepto) impuestos = {} @@ -5749,11 +5886,14 @@ class Facturas(BaseModel): if tax_decimals: xml_importe = FORMAT_TAX.format(tax.importe) + xml_tax_base = FORMAT_TAX.format(tax.base) else: xml_importe = FORMAT.format(tax.importe) + xml_tax_base = FORMAT.format(tax.base) if tax.impuesto.tipo == 'T': traslado = { + "Base": xml_tax_base, "Impuesto": tax.impuesto.key, "TipoFactor": tipo_factor, "TasaOCuota": str(tax.impuesto.tasa), @@ -5793,6 +5933,7 @@ class Facturas(BaseModel): 'donativo': donativo, 'edu': is_edu, 'complementos': complementos, + 'global': data_global, } return utils.make_xml(data, certificado) @@ -6434,7 +6575,7 @@ class PreFacturas(BaseModel): } data['comprobante'] = obj - data['comprobante']['version'] = CURRENT_CFDI + data['comprobante']['version'] = PRE_DEFAULT['CFDI']['VERSION'] data['comprobante']['folio'] = str(data['comprobante']['folio']) data['comprobante']['seriefolio'] = '{}-{}'.format( data['comprobante']['serie'], data['comprobante']['folio']) @@ -6893,6 +7034,8 @@ class PreFacturasDetalle(BaseModel): 'rfc': q.cliente.rfc, 'forma_pago': q.forma_pago, 'uso_cfdi': q.uso_cfdi, + 'codigo_postal': q.cliente.codigo_postal, + 'regimenes': SociosRegimenes.get_by_key(q.cliente.id), 'notas': q.notas, } @@ -7140,6 +7283,7 @@ class CfdiPagos(BaseModel): error = TextField(default='') cancelada = BooleanField(default=False) fecha_cancelacion = DateTimeField(null=True) + receptor_regimen = TextField(default='') class Meta: order_by = ('movimiento',) @@ -7263,6 +7407,7 @@ class CfdiPagos(BaseModel): partner = related[0].factura.cliente partner_name = related[0].factura.cliente.nombre + receptor_regimen = related[0].factura.receptor_regimen emisor = Emisor.select()[0] # ~ regimen_fiscal = related[0].factura.regimen_fiscal @@ -7302,6 +7447,7 @@ class CfdiPagos(BaseModel): fields['folio'] = self._get_folio(self, serie) fields['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal fields['regimen_fiscal'] = regimen_fiscal + fields['receptor_regimen'] = receptor_regimen with database_proxy.atomic() as txn: obj = CfdiPagos.create(**fields) @@ -7319,7 +7465,92 @@ class CfdiPagos(BaseModel): data = {'ok': True, 'row': row, 'new': True} return data + def _get_taxes_by_pay(self, pay, taxes_pay): + invoice = Facturas.get(Facturas.uuid==pay['IdDocumento']) + impuestos = {} + traslados = [] + retenciones = [] + + where = (FacturasImpuestos.factura==invoice) + taxes = FacturasImpuestos.select().where(where) + + tax_proporcion = pay['ImpPagado'] / invoice.total + # ~ print('Total', invoice.total) + # ~ print('Pagado', pay['ImpPagado']) + # ~ print('proporcion', tax_proporcion) + + for tax in taxes: + if tax.impuesto.key == '000': + # ~ tasa = str(round(tax.impuesto.tasa * 100, 2)) + # ~ simporte = FORMAT.format(tax.importe) + # ~ if tax.impuesto.tipo == 'T': + # ~ traslado = { + # ~ 'ImpLocTrasladado': tax.impuesto.name, + # ~ 'TasadeTraslado': tasa, + # ~ 'Importe': simporte, + # ~ } + # ~ locales_trasladados.append(traslado) + # ~ total_locales_trasladados += tax.importe + # ~ else: + # ~ retencion = { + # ~ 'ImpLocRetenido': tax.impuesto.name, + # ~ 'TasadeRetencion': tasa, + # ~ 'Importe': simporte, + # ~ } + # ~ locales_retenciones.append(retencion) + # ~ total_locales_retenciones += tax.importe + continue + + tipo_factor = 'Tasa' + if tax.impuesto.factor != 'T': + tipo_factor = 'Cuota' + + import_dr = round(tax.importe * tax_proporcion, 2) + # ~ xml_importe = FORMAT.format(tax.importe) + xml_importe = FORMAT.format(import_dr) + base_dr = round(tax.base * tax_proporcion, 2) + # ~ xml_tax_base = FORMAT.format(tax.base) + xml_tax_base = FORMAT.format(base_dr) + + values = { + "BaseDR": xml_tax_base, + "ImpuestoDR": tax.impuesto.key, + "TipoFactorDR": tipo_factor, + "TasaOCuotaDR": str(tax.impuesto.tasa), + "ImporteDR": xml_importe, + } + tax_key = tax.impuesto.key + if tax.impuesto.tipo == 'T': + traslados.append(values) + if tax_key in taxes_pay['traslados']: + # ~ taxes_pay['traslados'][tax_key]['ImporteP'] += tax.importe + taxes_pay['traslados'][tax_key]['ImporteP'] += import_dr + else: + values = { + # ~ "BaseP": tax.base, + "BaseP": base_dr, + "ImpuestoP": tax.impuesto.key, + "TipoFactorP": tipo_factor, + "TasaOCuotaP": str(tax.impuesto.tasa), + # ~ "ImporteP": tax.importe, + "ImporteP": import_dr, + } + taxes_pay['traslados'][tax_key] = values + else: + retenciones.append(values) + if tax_key in taxes_pay['retenciones']: + taxes_pay['retenciones'][tax_key] += tax.importe + else: + taxes_pay['retenciones'][tax_key] = tax.importe + + impuestos['traslados'] = traslados + impuestos['retenciones'] = retenciones + + return impuestos + def _get_related_xml(self, id_mov, currency): + TAX_IVA_16 = '002|0.160000' + filters = (FacturasPagos.movimiento==id_mov) related = tuple(FacturasPagos.select( Facturas.uuid.alias('IdDocumento'), @@ -7336,13 +7567,21 @@ class CfdiPagos(BaseModel): .where(filters) .dicts()) + taxes_pay = {'retenciones': {}, 'traslados': {}, 'totales': {}} + for r in related: + r['taxes'] = self._get_taxes_by_pay(self, r, taxes_pay) # ~ print('\n\nMONEDA', currency, r['MonedaDR']) r['IdDocumento'] = str(r['IdDocumento']) r['Folio'] = str(r['Folio']) r['NumParcialidad'] = str(r['NumParcialidad']) r['TipoCambioDR'] = FORMAT6.format(r['TipoCambioDR']) - r['MetodoDePagoDR'] = DEFAULT_CFDIPAY['WAYPAY'] + # ~ r['MetodoDePagoDR'] = DEFAULT_CFDIPAY['WAYPAY'] + + # REVISAR + r['EquivalenciaDR'] = '1' + r['ObjetoImpDR'] = '02' + r['ImpSaldoAnt'] = FORMAT.format(r['ImpSaldoAnt']) r['ImpPagado'] = FORMAT.format(r['ImpPagado']) if round(r['ImpSaldoInsoluto'], 2) == 0.0: @@ -7354,7 +7593,28 @@ class CfdiPagos(BaseModel): if not r['Serie']: del r['Serie'] - return related + total_tax_iva_16_base = 0 + total_tax_iva_16_importe = 0 + + for key, importe in taxes_pay['retenciones'].items(): + taxes_pay['retenciones'][key] = FORMAT.format(importe) + for k, tax in taxes_pay['traslados'].items(): + tax_type = taxes_pay['traslados'][k]['ImpuestoP'] + tax_tasa = taxes_pay['traslados'][k]['TasaOCuotaP'] + tax_base = taxes_pay['traslados'][k]['BaseP'] + importe = taxes_pay['traslados'][k]['ImporteP'] + if f'{tax_type}|{tax_tasa}' == TAX_IVA_16: + total_tax_iva_16_base += tax_base + total_tax_iva_16_importe += importe + taxes_pay['traslados'][k]['BaseP'] = FORMAT.format(tax_base) + taxes_pay['traslados'][k]['ImporteP'] = FORMAT.format(importe) + + taxes_pay['totales'] = { + 'TotalTrasladosBaseIVA16': FORMAT.format(total_tax_iva_16_base), + 'TotalTrasladosImpuestoIVA16': FORMAT.format(total_tax_iva_16_importe), + } + + return related, taxes_pay def _generate_xml(self, invoice): emisor = Emisor.select()[0] @@ -7367,9 +7627,9 @@ class CfdiPagos(BaseModel): cfdi['Folio'] = str(invoice.folio) cfdi['Fecha'] = invoice.fecha.isoformat()[:19] cfdi['NoCertificado'] = certificado.serie - # ~ cfdi['Certificado'] = cert.cer_txt cfdi['SubTotal'] = '0' cfdi['Moneda'] = DEFAULT_CFDIPAY['CURRENCY'] + # ~ cfdi['TipoCambio'] = DEFAULT_CFDIPAY['TC'] cfdi['Total'] = '0' cfdi['TipoDeComprobante'] = invoice.tipo_comprobante cfdi['LugarExpedicion'] = invoice.lugar_expedicion @@ -7390,6 +7650,8 @@ class CfdiPagos(BaseModel): 'Rfc': invoice.socio.rfc, 'Nombre': invoice.socio.nombre, 'UsoCFDI': DEFAULT_CFDIPAY['USED'], + 'DomicilioFiscalReceptor': invoice.socio.codigo_postal, + 'RegimenFiscalReceptor': invoice.receptor_regimen } if invoice.socio.tipo_persona == 4: if invoice.socio.pais: @@ -7404,19 +7666,23 @@ class CfdiPagos(BaseModel): 'Descripcion': DEFAULT_CFDIPAY['DESCRIPTION'], 'ValorUnitario': '0', 'Importe': '0', + 'ObjetoImp': '01', },) impuestos = {} mov = invoice.movimiento currency = mov.moneda - related_docs = self._get_related_xml(self, invoice.movimiento, currency) + related_docs, taxes_pay = self._get_related_xml(self, invoice.movimiento, currency) + totales = taxes_pay.pop('totales') pagos = { 'FechaPago': mov.fecha.isoformat()[:19], 'FormaDePagoP': mov.forma_pago.key, 'MonedaP': currency, + 'TipoCambioP': '1', 'Monto': FORMAT.format(mov.deposito), 'relacionados': related_docs, + 'taxes_pay': taxes_pay, } if mov.numero_operacion: pagos['NumOperacion'] = mov.numero_operacion @@ -7431,10 +7697,12 @@ class CfdiPagos(BaseModel): pagos['RfcEmisorCtaBen'] = mov.cuenta.banco.rfc pagos['CtaBeneficiario'] = mov.cuenta.cuenta - if currency != CURRENCY_MN: pagos['TipoCambioP'] = FORMAT_TAX.format(mov.tipo_cambio) + totales['MontoTotalPagos'] = pagos['Monto'] + pagos['totales'] = totales + complementos = {'pagos': pagos} data = { 'comprobante': cfdi, @@ -7446,6 +7714,7 @@ class CfdiPagos(BaseModel): 'donativo': {}, 'edu': False, 'complementos': complementos, + 'global': {}, } return utils.make_xml(data, certificado) @@ -7565,6 +7834,8 @@ class CfdiPagos(BaseModel): target = emisor.rfc + '/' + str(obj.fecha)[:7].replace('-', '/') values = cls._get_not_in_xml(cls, obj, emisor) data = util.get_data_from_xml(obj, values) + data['informacion_global'] = {} + obj = SATFormaPago.get(SATFormaPago.key==data['pays']['FormaDePagoP']) data['pays']['formadepago'] = '{} ({})'.format(obj.name, obj.key) doc = util.to_pdf(data, emisor.rfc) @@ -8167,6 +8438,7 @@ class Tickets(BaseModel): @classmethod def invoice(cls, values, user): is_invoice_day = util.get_bool(values['is_invoice_day']) + periodicidad = values['periodicidad'] id_client = int(values['client']) tickets = util.loads(values['tickets']) invoice_by_ticket = Configuracion.get_bool('chk_config_invoice_by_ticket') @@ -8183,12 +8455,21 @@ class Tickets(BaseModel): return data else: client = Socios.get(Socios.id==id_client) + periodicidad = '' if client.forma_pago is None: msg = 'La Forma de Pago del cliente, no esta asignada' data = {'ok': False, 'msg': msg} return data + try: + receptor_regimen = SociosRegimenes.get_by_key(client)[0]['id'] + except Exception as e: + log.error(e) + msg = 'Error al obtener el Regimen Fiscal del receptor' + data = {'ok': False, 'msg': msg} + return data + payment_type = cls._get_payment_type(cls, tickets) emisor = Emisor.select()[0] @@ -8196,15 +8477,13 @@ class Tickets(BaseModel): data['cliente'] = client data['serie'] = cls._get_serie(cls, user, True) data['folio'] = cls._get_folio_invoice(cls, data['serie']) - # ~ data['forma_pago'] = client.forma_pago.key data['forma_pago'] = payment_type data['tipo_cambio'] = 1.00 data['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal - if client.uso_cfdi is None: - data['uso_cfdi'] = 'P01' - else: - data['uso_cfdi'] = client.uso_cfdi.key + data['uso_cfdi'] = client.uso_cfdi.key data['regimen_fiscal'] = emisor.regimenes[0].key + data['receptor_regimen'] = receptor_regimen + data['periodicidad'] = periodicidad with database_proxy.atomic() as txn: obj = Facturas.create(**data) @@ -8669,6 +8948,7 @@ class Empleados(BaseModel): codigo_postal = TextField(default='') notas = TextField(default='') correo = TextField(default='') + regimen_fiscal = TextField(default='') class Meta: order_by = ('nombre_completo',) @@ -8720,10 +9000,10 @@ class Empleados(BaseModel): obj = Empleados.create(**data) en += 1 - msg = 'Empleados encontrados: {}
'.format(len(rows)) - msg += 'Empleados nuevos: {}
'.format(en) - msg += 'Empleados actualizados: {}
'.format(ea) - msg += 'Empleados no importados: {}'.format(len(rows) - en - ea) + msg = 'Empleados:

Encontrados: {}
'.format(len(rows)) + msg += 'Nuevos: {}
'.format(en) + msg += 'Actualizados: {}
'.format(ea) + msg += 'No importados: {}'.format(len(rows) - en - ea) return {'ok': True, 'msg': msg} def _get(self): @@ -8755,13 +9035,13 @@ class Empleados(BaseModel): try: q = Empleados.delete().where(Empleados.id==id) return bool(q.execute()) - except IntegrityError: + except Exception as e: return False class CfdiNomina(BaseModel): empleado = ForeignKeyField(Empleados) - version = TextField(default=CURRENT_CFDI) + version = TextField(default=PRE_DEFAULT['CFDI']['VERSION']) serie = TextField(default='N') folio = IntegerField(default=0) fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S']) @@ -8802,7 +9082,7 @@ class CfdiNomina(BaseModel): acuse = TextField(default='') tipo_relacion = TextField(default='') error = TextField(default='') - version_nomina = TextField(default=CURRENT_CFDI_NOMINA) + version_nomina = TextField(default=PRE_DEFAULT['NOMINA']['VERSION']) registro_patronal = TextField(default='') rfc_patron_origen = TextField(default='') tipo_nomina = ForeignKeyField(SATTipoNomina) @@ -9441,7 +9721,7 @@ class CfdiNomina(BaseModel): comprobante['Serie'] = cfdi.serie comprobante['Folio'] = str(cfdi.folio) comprobante['Fecha'] = cfdi.fecha.isoformat()[:19] - comprobante['FormaPago'] = cfdi.forma_pago + # ~ comprobante['FormaPago'] = cfdi.forma_pago comprobante['NoCertificado'] = certificado.serie comprobante['Certificado'] = certificado.cer_txt comprobante['SubTotal'] = FORMAT.format(cfdi.subtotal) @@ -9468,6 +9748,8 @@ class CfdiNomina(BaseModel): receptor = { 'Rfc': cfdi.empleado.rfc, 'Nombre': cfdi.empleado.nombre_completo, + 'DomicilioFiscalReceptor': cfdi.empleado.codigo_postal, + 'RegimenFiscalReceptor': cfdi.empleado.regimen_fiscal, 'UsoCFDI': cfdi.uso_cfdi, } @@ -9481,6 +9763,7 @@ class CfdiNomina(BaseModel): 'Descripcion': row.descripcion, 'ValorUnitario': FORMAT.format(row.valor_unitario), 'Importe': FORMAT.format(row.importe), + 'ObjetoImp': '01', } if row.descuento: concepto['Descuento'] = FORMAT.format(row.descuento) @@ -9841,7 +10124,11 @@ class CfdiNomina(BaseModel): return b'', name values = cls._get_not_in_xml(cls, obj, emisor) + data = util.get_data_from_xml(obj, values) + + data.update(utils.CfdiToDict(obj.xml).values) + doc = util.to_pdf(data, emisor.rfc) # ~ if sync: @@ -10405,10 +10692,10 @@ def authenticate(args): def get_cp(cp): con = sqlite3.connect(PATH_CP) cursor = con.cursor() - sql = """ - SELECT colonia, municipio, estado, municipios.id_municipio - FROM colonias, municipios, estados - WHERE colonias.id_municipio=municipios.id + sql = """SELECT estado, municipio, clave, colonia, key_estado + FROM codigos, colonias, municipios, estados + WHERE codigos.id_colonia=colonias.id + AND codigos.id_municipio=municipios.id AND municipios.id_estado=estados.id AND cp=? ORDER BY colonia""" @@ -10420,15 +10707,18 @@ def get_cp(cp): data = {} if rows: data = { - 'estado': rows[0][2], + 'estado': rows[0][0], 'municipio': rows[0][1], - 'key_municipio': str(rows[0][3]).zfill(3), + 'key_municipio': rows[0][2], + 'key_estado': rows[0][4], } - print(data) if len(rows) == 1: - data['colonia'] = rows[0][0] + data['colonia'] = rows[0][3] else: - data['colonia'] = [r[0] for r in rows] + data['colonia'] = [r[3] for r in rows] + + print('CP', cp, data) + return data @@ -10546,6 +10836,7 @@ def _crear_tablas(rfc): PartnerInvoices, WareHouseProduct, SATUnidadesPeso, + SociosRegimenes, ] log.info('Creando tablas...') database_proxy.create_tables(tablas, True) @@ -10604,6 +10895,7 @@ def _migrate_tables(rfc=''): PartnerInvoices, WareHouseProduct, SATUnidadesPeso, + SociosRegimenes, ] log.info('Creando tablas nuevas...') database_proxy.create_tables(tablas, True) @@ -10647,6 +10939,10 @@ def _migrate_tables(rfc=''): correo_facturasp = TextField(default='') migrations.append( migrator.add_column('socios', 'correo_facturasp', correo_facturasp)) + if not 'regimen_fiscal' in columns: + regimen_fiscal = TextField(default='') + migrations.append( + migrator.add_column('socios', 'regimen_fiscal', regimen_fiscal)) columns = [c.name for c in database_proxy.get_columns('folios')] if not 'plantilla' in columns: @@ -10696,6 +10992,9 @@ def _migrate_tables(rfc=''): migrations.append(migrator.add_column('cfdipagos', 'socio_id', socio)) migrations.append(migrator.drop_column('cfdipagos', 'cancelado')) migrations.append(migrator.add_column('cfdipagos', 'cancelada', cancelada)) + if not 'receptor_regimen' in columns: + receptor_regimen = TextField(default='') + migrations.append(migrator.add_column('cfdipagos', 'receptor_regimen', receptor_regimen)) if not 'fecha_cancelacion' in columns: fecha_cancelacion = DateTimeField(null=True) @@ -10729,6 +11028,10 @@ def _migrate_tables(rfc=''): is_discontinued = BooleanField(default=False) migrations.append(migrator.add_column( table, 'is_discontinued', is_discontinued)) + if not 'objeto_impuesto' in columns: + objeto_impuesto = TextField(default='02') + migrations.append(migrator.add_column(table, 'objeto_impuesto', objeto_impuesto)) + if 'almacen_id' in columns: migrations.append(migrator.drop_column(table, 'almacen_id')) @@ -10745,6 +11048,15 @@ def _migrate_tables(rfc=''): if not 'divisas' in columns: divisas = TextField(default='') migrations.append(migrator.add_column(table, 'divisas', divisas)) + if not 'exportacion' in columns: + new_field = TextField(default='01') + migrations.append(migrator.add_column(table, 'exportacion', new_field)) + if not 'receptor_regimen' in columns: + receptor_regimen = TextField(default='') + migrations.append(migrator.add_column(table, 'receptor_regimen', receptor_regimen)) + if not 'periodicidad' in columns: + periodicidad = TextField(default='') + migrations.append(migrator.add_column(table, 'periodicidad', periodicidad)) table = 'almacenes' columns = [c.name for c in database_proxy.get_columns(table)] @@ -10772,6 +11084,13 @@ def _migrate_tables(rfc=''): warehouse = ForeignKeyField(Almacenes, null=True, to_field=Almacenes.id) migrations.append(migrator.add_column(table, field, warehouse)) + table = 'empleados' + field = 'regimen_fiscal' + columns = [c.name for c in database_proxy.get_columns(table)] + if not field in columns: + regimen_fiscal = TextField(default='') + migrations.append(migrator.add_column(table, field, regimen_fiscal)) + if migrations: with database_proxy.atomic() as txn: migrate(*migrations) @@ -10779,7 +11098,24 @@ def _migrate_tables(rfc=''): Configuracion.add({'version': VERSION}) log.info('Tablas migradas correctamente...') - _importar_valores('', rfc) + # ~ _importar_valores('', rfc) + + log.info('Actualizando valores...') + try: + q = SATRegimenes.update(**{'activo': True}).where(SATRegimenes.key=='616') + q.execute() + except Exception as e: + log.error(e) + else: + log.info('Valores actualizados...') + + try: + q = SATEstados.update(**{'key': 'CMX'}).where(SATEstados.key=='DIF') + q.execute() + except Exception as e: + log.error(e) + else: + log.info('Valores actualizados...') return diff --git a/source/app/settings.py b/source/app/settings.py index 00624bc..986f01f 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -42,7 +42,8 @@ except ImportError: DEBUG = DEBUG -VERSION = '1.47.0' +VERSION = '2.0.0' + EMAIL_SUPPORT = ('soporte@empresalibre.mx',) TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION) @@ -127,25 +128,45 @@ if 'win' in sys.platform: PATH_XMLSEC = os.path.join(PATH_BIN, 'xmlsec.exe') +PRE_DEFAULT = { + 'CFDI': {'VERSION': '4.0', 'PRE': '{http://www.sat.gob.mx/cfd/4}'}, + 'NOMINA': {'VERSION': '1.2', 'PRE': '{http://www.sat.gob.mx/nomina12}'}, + 'PAGOS': {'VERSION': '2.0', 'PRE': '{http://www.sat.gob.mx/Pagos20}'}, + 'TIBRE': {'VERSION': '1.1', 'PRE': '{http://www.sat.gob.mx/TimbreFiscalDigital}'}, +} + +pre2 ='{http://www.sat.gob.mx/cfd/2}' +pre3 ='{http://www.sat.gob.mx/cfd/3}' +PRE_HISTORY = { + 'CFDI': {'2.0': pre2, '2.2': pre2, + '3.0': pre3, '3.2': pre3, '3.3': pre3}, + 'NOMINA': {'1.1': '{http://www.sat.gob.mx/nomina}'}, + 'PAGOS': {'1.0': '{http://www.sat.gob.mx/Pagos}'}, +} + PRE = { '2.0': '{http://www.sat.gob.mx/cfd/2}', '2.2': '{http://www.sat.gob.mx/cfd/2}', '3.0': '{http://www.sat.gob.mx/cfd/3}', '3.2': '{http://www.sat.gob.mx/cfd/3}', '3.3': '{http://www.sat.gob.mx/cfd/3}', + '4.0': '{http://www.sat.gob.mx/cfd/4}', 'TIMBRE': '{http://www.sat.gob.mx/TimbreFiscalDigital}', 'DONATARIA': '{http://www.sat.gob.mx/donat}', 'INE': '{http://www.sat.gob.mx/ine}', 'LOCALES': '{http://www.sat.gob.mx/implocal}', 'NOMINA': { '1.1': '{http://www.sat.gob.mx/nomina}', - '1.2': '{http://www.sat.gob.mx/nomina12}', }, - 'pagos': '{http://www.sat.gob.mx/Pagos}', + 'PAGOS': { + '1.0': '{http://www.sat.gob.mx/Pagos}', + } } -CURRENT_CFDI = '3.3' -CURRENT_CFDI_NOMINA = '1.2' +# To delete +# ~ CURRENT_CFDI = '4.0' +# ~ CURRENT_CFDI_NOMINA = '1.2' + DECIMALES = 2 DECIMALES_TAX = 4 DECIMALES_PRECIOS = 4 @@ -168,7 +189,8 @@ DEFAULT_CFDIPAY = { 'TYPE': 'P', 'WAYPAY': 'PPD', 'CURRENCY': 'XXX', - 'USED': 'P01', + 'TC': '1', + 'USED': 'CP01', 'KEYSAT': '84111506', 'UNITKEY': 'ACT', 'DESCRIPTION': 'Pago', @@ -181,7 +203,7 @@ PUBLIC = 'Público en general' DEFAULT_SAT_NOMINA = { 'SERIE': 'N', 'FORMA_PAGO': '99', - 'USO_CFDI': 'P01', + 'USO_CFDI': 'CN01', 'CLAVE': '84111505', 'UNIDAD': 'ACT', 'DESCRIPCION': 'Pago de nómina', @@ -193,6 +215,7 @@ CURRENCY_MN = 'MXN' # ~ v2 CANCEL_VERSION = ('3.3', '4.0') +CFDI_VERSIONS = CANCEL_VERSION IS_MV = MV DB_COMPANIES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'rfc.db')) diff --git a/source/db/cp.db b/source/db/cp.db index 2f632fa..924d225 100644 Binary files a/source/db/cp.db and b/source/db/cp.db differ diff --git a/source/db/valores_iniciales.json b/source/db/valores_iniciales.json index 7af3182..9ca6de0 100644 --- a/source/db/valores_iniciales.json +++ b/source/db/valores_iniciales.json @@ -104,7 +104,7 @@ {"key": "611", "name": "Ingresos por Dividendos (socios y accionistas)", "fisica": true, "activo": false}, {"key": "612", "name": "Personas Físicas con Actividades Empresariales y Profesionales", "fisica": true, "activo": true, "default": true}, {"key": "614", "name": "Ingresos por intereses", "fisica": true, "activo": true}, - {"key": "616", "name": "Sin obligaciones fiscales", "fisica": true, "activo": false}, + {"key": "616", "name": "Sin obligaciones fiscales", "fisica": true, "activo": true}, {"key": "620", "name": "Sociedades Cooperativas de Producción que optan por diferir sus ingresos", "moral": true, "activo": false}, {"key": "621", "name": "Incorporación Fiscal", "fisica": true, "activo": true}, {"key": "622", "name": "Actividades Agrícolas, Ganaderas, Silvícolas y Pesqueras", "fisica": true, "moral": true, "activo": false}, @@ -682,7 +682,10 @@ {"key": "D08", "name": "Gastos de transportación escolar obligatoria.", "activo": false}, {"key": "D09", "name": "Depósitos en cuentas para el ahorro, primas que tengan como base planes de pensiones.", "activo": false}, {"key": "D10", "name": "Pagos por servicios educativos (colegiaturas)", "activo": true}, - {"key": "P01", "name": "Por definir", "moral": true, "activo": true} + {"key": "P01", "name": "Por definir", "moral": true, "activo": true}, + {"key": "S01", "name": "Sin efectos fiscales.", "moral": true, "activo": true}, + {"key": "CP01", "name": "Pagos", "moral": true, "activo": true}, + {"key": "CN01", "name": "Nómina", "moral": true, "activo": true} ] }, { @@ -728,7 +731,7 @@ "pais": "MEX" }, { - "key": "DIF", + "key": "CMX", "name": "Ciudad de M\u00e9xico", "pais": "MEX" }, diff --git a/source/static/js/controller/admin.js b/source/static/js/controller/admin.js index 0016d98..38db792 100644 --- a/source/static/js/controller/admin.js +++ b/source/static/js/controller/admin.js @@ -76,15 +76,13 @@ var controllers = { //~ Opciones tb_options = $$('tab_options').getTabbar() tb_options.attachEvent('onChange', tab_options_change) - $$('txt_plantilla_factura_32').attachEvent('onItemClick', txt_plantilla_factura_32_click) - $$('txt_plantilla_factura_33').attachEvent('onItemClick', txt_plantilla_factura_33_click) $$('txt_plantilla_factura_html').attachEvent('onItemClick', txt_plantilla_factura_html_click) $$('txt_plantilla_factura_json').attachEvent('onItemClick', txt_plantilla_factura_json_click) $$('txt_plantilla_factura_css').attachEvent('onItemClick', txt_plantilla_factura_css_click) $$('txt_plantilla_ticket').attachEvent('onItemClick', txt_plantilla_ticket_click) - $$('txt_plantilla_donataria').attachEvent('onItemClick', txt_plantilla_donataria_click) - $$('txt_plantilla_nomina1233').attachEvent('onItemClick', txt_plantilla_nomina1233_click) - $$('txt_plantilla_pagos10').attachEvent('onItemClick', txt_plantilla_pagos10_click) + //~ $$('txt_plantilla_donataria').attachEvent('onItemClick', txt_plantilla_donataria_click) + //~ $$('txt_plantilla_nomina1233').attachEvent('onItemClick', txt_plantilla_nomina1233_click) + //~ $$('txt_plantilla_pagos10').attachEvent('onItemClick', txt_plantilla_pagos10_click) $$('make_pdf_from').attachEvent('onChange', opt_make_pdf_from_on_change) $$('cmd_template_upload').attachEvent('onItemClick', cmd_template_upload_click) @@ -503,7 +501,9 @@ function set_config_templates(){ success: function(text, data, xhr) { var values = data.json() Object.keys(values).forEach(function(key){ - show(key, values[key]) + if(key!='txt_plantilla_donataria'){ + show(key, values[key]) + } }) } }) @@ -1181,42 +1181,42 @@ function txt_plantilla_factura_json_click(e){ } -function txt_plantilla_donataria_click(e){ +//~ function txt_plantilla_donataria_click(e){ - var body_elements = [ - {cols: [{width: 100}, {view: 'uploader', id: 'up_template', autosend: true, link: 'lst_files', - value: 'Seleccionar archivo', upload: '/files/txt_plantilla_donataria', - width: 200}, {width: 100}]}, - {view: 'list', id: 'lst_files', type: 'uploader', autoheight:true, - borderless: true}, - {}, - {cols: [{}, {view: 'button', label: 'Cerrar', autowidth: true, - click:("$$('win_template').close();")}, {}]} - ] + //~ var body_elements = [ + //~ {cols: [{width: 100}, {view: 'uploader', id: 'up_template', autosend: true, link: 'lst_files', + //~ value: 'Seleccionar archivo', upload: '/files/txt_plantilla_donataria', + //~ width: 200}, {width: 100}]}, + //~ {view: 'list', id: 'lst_files', type: 'uploader', autoheight:true, + //~ borderless: true}, + //~ {}, + //~ {cols: [{}, {view: 'button', label: 'Cerrar', autowidth: true, + //~ click:("$$('win_template').close();")}, {}]} + //~ ] - var w = webix.ui({ - view: 'window', - id: 'win_template', - modal: true, - position: 'center', - head: 'Subir Plantilla Donataria', - body: { - view: 'form', - elements: body_elements, - } - }) + //~ var w = webix.ui({ + //~ view: 'window', + //~ id: 'win_template', + //~ modal: true, + //~ position: 'center', + //~ head: 'Subir Plantilla Donataria', + //~ body: { + //~ view: 'form', + //~ elements: body_elements, + //~ } + //~ }) - w.show() + //~ w.show() - $$('up_template').attachEvent('onUploadComplete', function(response){ - if(response.ok){ - $$('txt_plantilla_donataria').setValue(response.name) - msg_ok('Plantilla cargada correctamente') - }else{ - msg_error(response.name) - } - }) -} + //~ $$('up_template').attachEvent('onUploadComplete', function(response){ + //~ if(response.ok){ + //~ $$('txt_plantilla_donataria').setValue(response.name) + //~ msg_ok('Plantilla cargada correctamente') + //~ }else{ + //~ msg_error(response.name) + //~ } + //~ }) +//~ } function txt_plantilla_nomina1233_click(e){ @@ -1257,42 +1257,42 @@ function txt_plantilla_nomina1233_click(e){ } -function txt_plantilla_pagos10_click(e){ +//~ function txt_plantilla_pagos10_click(e){ - var body_elements = [ - {cols: [{width: 100}, {view: 'uploader', id: 'up_template', - autosend: true, link: 'lst_files', value: 'Seleccionar archivo', - upload: '/files/txt_plantilla_pagos10', width: 200}, {width: 100}]}, - {view: 'list', id: 'lst_files', type: 'uploader', autoheight:true, - borderless: true}, - {}, - {cols: [{}, {view: 'button', label: 'Cerrar', autowidth: true, - click:("$$('win_template').close();")}, {}]} - ] + //~ var body_elements = [ + //~ {cols: [{width: 100}, {view: 'uploader', id: 'up_template', + //~ autosend: true, link: 'lst_files', value: 'Seleccionar archivo', + //~ upload: '/files/txt_plantilla_pagos10', width: 200}, {width: 100}]}, + //~ {view: 'list', id: 'lst_files', type: 'uploader', autoheight:true, + //~ borderless: true}, + //~ {}, + //~ {cols: [{}, {view: 'button', label: 'Cerrar', autowidth: true, + //~ click:("$$('win_template').close();")}, {}]} + //~ ] - var w = webix.ui({ - view: 'window', - id: 'win_template', - modal: true, - position: 'center', - head: 'Subir Plantilla Factura de Pago', - body: { - view: 'form', - elements: body_elements, - } - }) + //~ var w = webix.ui({ + //~ view: 'window', + //~ id: 'win_template', + //~ modal: true, + //~ position: 'center', + //~ head: 'Subir Plantilla Factura de Pago', + //~ body: { + //~ view: 'form', + //~ elements: body_elements, + //~ } + //~ }) - w.show() + //~ w.show() - $$('up_template').attachEvent('onUploadComplete', function(response){ - if(response.ok){ - $$('txt_plantilla_pagos10').setValue(response.name) - msg_ok('Plantilla cargada correctamente') - }else{ - msg_error(response.name) - } - }) -} + //~ $$('up_template').attachEvent('onUploadComplete', function(response){ + //~ if(response.ok){ + //~ $$('txt_plantilla_pagos10').setValue(response.name) + //~ msg_ok('Plantilla cargada correctamente') + //~ }else{ + //~ msg_error(response.name) + //~ } + //~ }) +//~ } function tab_options_change(nv, ov){ diff --git a/source/static/js/controller/bancos.js b/source/static/js/controller/bancos.js index b4bbe33..ae95103 100644 --- a/source/static/js/controller/bancos.js +++ b/source/static/js/controller/bancos.js @@ -993,6 +993,8 @@ function send_stamp_cfdi_pay(id_mov){ var g = $$('grid_cfdi_pay') var data = {'opt': 'stamp', 'id_mov': id_mov} + var close = $$('chk_pay_close_when_stamp').getValue() + //~ ToDo Actualizar cantidad de facturas de pago en el movimiento webix.ajax().sync().post('cfdipay', data, { @@ -1010,6 +1012,9 @@ function send_stamp_cfdi_pay(id_mov){ } } }) + + $$('multi_bancos').setValue('banco_home') + } function save_cfdi_pay(form){ diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index e060d42..24eab91 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -291,6 +291,11 @@ function cmd_new_invoice_click(){ grid_totals.add({id: 1, concepto: 'SubTotal', importe: 0}) $$('cmd_cfdi_relacionados').disable() $$('multi_invoices').setValue('invoices_new') + + var lst = $$('lst_invoice_client_regimen') + lst.setValue('') + lst.getList().clearAll() + form.focus('search_client_name') } @@ -363,19 +368,25 @@ function validate_invoice(values){ msg_error(msg) return false } - } var tipo_comprobante = $$('lst_tipo_comprobante').getValue() if(tipo_comprobante != 'T'){ if(values.id_partner == 0){ - webix.UIManager.setFocus('search_client_name') + focus('search_client_name') msg = 'Selecciona un cliente' msg_error(msg) return false } } + var regimen_fiscal = $$('lst_invoice_client_regimen').getValue() + if(!regimen_fiscal){ + msg = 'El Regimen Fiscal del Cliente es obligatorio.' + msg_error(msg) + return false + } + if(!grid.count()){ webix.UIManager.setFocus('search_product_id') msg = 'Agrega al menos un producto o servicio' @@ -685,6 +696,7 @@ function guardar_y_timbrar(values){ data['metodo_pago'] = $$('lst_metodo_pago').getValue() data['uso_cfdi'] = $$('lst_uso_cfdi').getValue() data['regimen_fiscal'] = $$('lst_regimen_fiscal').getValue() + data['receptor_regimen'] = $$('lst_invoice_client_regimen').getValue() data['relacionados'] = ids data['tipo_relacion'] = tipo_relacion data['anticipo'] = anticipo @@ -904,6 +916,12 @@ function search_client_by_id(id){ function set_client(row){ + if(!row.codigo_postal){ + msg = 'El cliente no tiene capturado su Código Postal, es obligatorio.' + msg_error(msg) + return + } + var form = $$('form_invoice') var html = '' form.setValues({ @@ -913,6 +931,20 @@ function set_client(row){ html += row.nombre + ' (' + row.rfc + ')' $$('lbl_client').setValue(html) $$('cmd_cfdi_relacionados').enable() + + var lst = $$('lst_invoice_client_regimen') + lst.getList().clearAll() + if(row.regimenes==undefined){ + var options = {opt: 'by_id', id: row.id} + webix.ajax().sync().get('/sociosregimenes', options, function(text, data){ + var values = data.json() + lst.getList().parse(values) + }) + }else{ + lst.getList().parse(row.regimenes) + } + lst.setValue(lst.getPopup().getList().getFirstId()) + form.focus('search_product_id') } @@ -1204,11 +1236,9 @@ function grid_carta_ubicaciones_before_edit_stop(state, editor){ msg = 'No se encontró el C.P., asegurate de que sea correcto' msg_error(msg) } else { - row['Estado'] = opt_carta_estados.find(x => x.value === values.estado).id + //~ row['Estado'] = opt_carta_estados.find(x => x.value === values.estado).id + row['Estado'] = values.key_estado row['Municipio'] = values.key_municipio - //~ $$('colonia').define('suggest', []) - //~ $$('colonia').define('suggest', values.colonia) - //~ $$('colonia').refresh() g.refresh() msg_ok('Municipio:\n' + values.municipio) } diff --git a/source/static/js/controller/nomina.js b/source/static/js/controller/nomina.js index 9a7292c..fee1238 100644 --- a/source/static/js/controller/nomina.js +++ b/source/static/js/controller/nomina.js @@ -255,10 +255,10 @@ function up_employees_upload_complete(response){ function delete_empleado(id){ webix.ajax().del('/employees', {id: id}, function(text, xml, xhr){ var msg = 'Empleado eliminado correctamente' - if (xhr.status == 200){ + if(xhr.status == 200){ $$('grid_employees').remove(id); msg_ok(msg) - } else { + }else{ msg = 'El Empleado tiene recibos timbrados' msg_error(msg) } diff --git a/source/static/js/controller/partners.js b/source/static/js/controller/partners.js index e6dd2ab..1f2b544 100644 --- a/source/static/js/controller/partners.js +++ b/source/static/js/controller/partners.js @@ -95,6 +95,7 @@ function cmd_new_partner_click(id, e, node){ $$('partner_balance').define('readonly', !cfg_partners['chk_config_change_balance_partner']) get_partner_banks() get_partner_accounts_bank(0) + get_sat_regimenes() } @@ -123,6 +124,7 @@ function cmd_edit_partner_click(){ }, success: function(text, data, xhr){ var values = data.json() + $$('form_partner').clearValidation() $$('form_partner').setValues(values) $$('forma_pago').getList().load('/values/formapago') @@ -132,8 +134,10 @@ function cmd_edit_partner_click(){ if(values.tipo_persona == 1){ query = table_usocfdi.chain().find({fisica: true}).data() + get_sat_regimenes() }else if(values.tipo_persona == 2){ query = table_usocfdi.chain().find({moral: true}).data() + get_sat_regimenes(true) }else{ query = [{id: 'P01', value: 'Por definir'}] } @@ -145,12 +149,15 @@ function cmd_edit_partner_click(){ $$('cuenta_proveedor').enable() } get_partner_accounts_bank(row['id']) + pause(250) + $$('lst_receptor_regimenes_fiscales').select(values.regimenes) } }) $$('multi_partners').setValue('partners_new') $$('tab_partner').setValue('Datos Fiscales') get_partner_banks() + } @@ -239,7 +246,17 @@ function cmd_save_partner_click(id, e, node){ } } + var ids_regimenes = $$('lst_receptor_regimenes_fiscales').getSelectedId() + if(values.tipo_persona < 3){ + if(!ids_regimenes){ + msg = 'Selecciona al menos un Regimen Fiscal' + msg_error(msg) + return + } + } + values['accounts'] = $$('grid_partner_account_bank').data.getRange() + values['regimenes'] = ids_regimenes webix.ajax().post('/partners', values, { error:function(text, data, XmlHttpRequest){ @@ -343,18 +360,28 @@ function opt_tipo_change(new_value, old_value){ $$('id_fiscal').define('value', '') show('id_fiscal', new_value == 4) + $$('lst_receptor_regimenes_fiscales').clearAll() + var regimen_616 = {id: 11, value: 'Sin obligaciones fiscales'} + if (new_value == 1 || new_value == 2){ $$("rfc").define("value", "") $$("rfc").define("readonly", false) + moral = false + if(new_value == 2){ + moral = true + } + get_sat_regimenes(moral) } else if (new_value == 3) { $$("rfc").define("value", RFC_PUBLICO) $$("nombre").define("value", PUBLICO) $$("rfc").define("readonly", true) + $$('lst_receptor_regimenes_fiscales').parse(regimen_616) } else if (new_value == 4) { $$("rfc").define("value", RFC_EXTRANJERO) $$("rfc").define("readonly", true) $$("pais").define("readonly", false) $$("pais").define("value", "") + $$('lst_receptor_regimenes_fiscales').parse(regimen_616) } $$("nombre").refresh(); @@ -372,10 +399,12 @@ function opt_tipo_change(new_value, old_value){ }else if (new_value == 2){ query = table_usocfdi.chain().find({moral: true}).data() }else{ - query = [{id: 'P01', value: 'Por definir'}] + query = [{id: 'S01', value: 'Sin efectos fiscales. '}] } $$('lst_uso_cfdi_socio').getList().parse(query) $$('lst_uso_cfdi_socio').refresh() + + } @@ -619,3 +648,21 @@ function partner_delete_account_bank(row){ } }) } + + +function get_sat_regimenes(morales=false){ + var data = {opt: 'actives', morales: morales} + webix.ajax().sync().get('/satregimenes', data, { + error: function(text, data, xhr) { + msg = 'Error al consultar' + msg_error(msg) + }, + success: function(text, data, xhr) { + var values = data.json() + $$('lst_receptor_regimenes_fiscales').clearAll() + $$('lst_receptor_regimenes_fiscales').parse(values) + } + }) + +} + diff --git a/source/static/js/controller/products.js b/source/static/js/controller/products.js index c7b31c5..39e8307 100644 --- a/source/static/js/controller/products.js +++ b/source/static/js/controller/products.js @@ -19,7 +19,7 @@ function products_default_config(){ if(cfg_products['inventario']){ $$('grid_products').showColumn('existencia') } - show('cant_by_packing', values.chk_use_packing) + //~ show('cant_by_packing', values.chk_use_packing) show('cmd_show_exists', values.chk_multi_stock) } }) @@ -123,6 +123,7 @@ function cmd_edit_product_click(){ get_taxes() $$('unidad').getList().load('/values/unidades') configurar_producto() + var grid = $$('grid_products') var row = grid.getSelectedItem() if(row == undefined){ @@ -131,9 +132,10 @@ function cmd_edit_product_click(){ } $$('categoria').getList().load('/values/categorias') - webix.ajax().get('/products', {id:row['id']}, { + + webix.ajax().get('/products', {id: row['id']}, { error: function(text, data, xhr) { - msg_error() + msg_error(text) }, success: function(text, data, xhr){ var values = data.json() @@ -237,10 +239,10 @@ function cmd_save_product_click(id, e, node){ var values = form.getValues(); - if(!isFinite(values.cant_by_packing)){ - msg_error('La cantidad por empaque debe ser un número') - return - } + //~ if(!isFinite(values.cant_by_packing)){ + //~ msg_error('La cantidad por empaque debe ser un número') + //~ return + //~ } if(!validate_sat_key_product(values.clave_sat, false)){ msg_error('La clave SAT no existe') diff --git a/source/static/js/controller/tickets.js b/source/static/js/controller/tickets.js index bb0b4ca..31f937d 100644 --- a/source/static/js/controller/tickets.js +++ b/source/static/js/controller/tickets.js @@ -611,12 +611,13 @@ function cmd_cancelar_ticket_click(){ function chk_is_invoice_day_change(new_value, old_value){ var value = Boolean(new_value) + show('fs_ticket_search_client', !value) + enable('lst_periodicidad', value) } function send_timbrar_invoice(id){ - //~ webix.ajax().get('/values/timbrar', {id: id, update: false}, function(text, data){ webix.ajax().post('invoices', {opt: 'timbrar', id: id, update: false}, function(text, data){ var values = data.json() if(values.ok){ @@ -683,6 +684,7 @@ function cmd_new_invoice_from_ticket_click(){ data['client'] = values.id_partner data['tickets'] = tickets data['is_invoice_day'] = chk.getValue() + data['periodicidad'] = $$('lst_periodicidad').getValue() data['opt'] = 'invoice' msg = 'Todos los datos son correctos.

¿Estás seguro de generar esta factura?' diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js index e9f1e35..acd55a4 100644 --- a/source/static/js/ui/admin.js +++ b/source/static/js/ui/admin.js @@ -596,8 +596,18 @@ var type_make_pdf = [ ] +//~ Templates var opt_templates_cfdi = [ - {id: '_3.3_cp_2.0.ods', value: 'CFDI v3.3 - Carta Porte 2.0'}, + {id: '_4.0.ods', value: 'CFDI v4.0'}, + {id: '_4.0_cn_1.2.ods', value: 'CFDI v4.0 - Nómina v1.2'}, + {id: '_4.0_cp_2.0.ods', value: 'CFDI v4.0 - Pagos v2.0'}, + {id: '_4.0_ccp_2.0.ods', value: 'CFDI v4.0 - Carta Porte v2.0'}, + {id: '_4.0_cd_1.1.ods', value: 'CFDI v4.0 - Donativos v1.1'}, + {id: '_3.3.ods', value: 'CFDI v3.3'}, + {id: '_3.3_cn_1.2.ods', value: 'CFDI v3.3 - Nómina v1.2'}, + {id: '_3.3_ccp_2.0.ods', value: 'CFDI v3.3 - Carta Porte v2.0'}, + {id: '_3.3_cp_1.0.ods', value: 'CFDI v3.3 - Pagos v1.0'}, + {id: '_3.2.ods', value: 'CFDI v3.2'}, ] @@ -610,14 +620,6 @@ var options_templates = [ {}, {maxWidth: 20} ]}, {maxHeight: 50}, - {cols: [{maxWidth: 20}, - {view: 'search', id: 'txt_plantilla_factura_32', name: 'plantilla_factura_32', - label: 'Plantilla Factura v3.2 (ODS): ', labelPosition: 'top', - icon: 'file'}, {maxWidth: 25}, - {view: 'search', id: 'txt_plantilla_factura_33', labelPosition: 'top', - label: 'Plantilla Factura v3.3 (ODS): ', icon: 'file'}, - {maxWidth: 20} ]}, - {maxHeight: 20}, {cols: [{maxWidth: 20}, {view: 'search', id: 'txt_plantilla_factura_html', name: 'plantilla_factura_html', label: 'Plantilla Factura v3.3 (HTML): ', labelPosition: 'top', @@ -632,25 +634,19 @@ var options_templates = [ label: 'Plantilla Factura v3.3 (JSON): ', labelPosition: 'top', icon: 'file'}, {maxWidth: 25}, {}, {maxWidth: 20} ]}, - - {maxHeight: 20}, - {cols: [{maxWidth: 20}, - {view: 'search', id: 'txt_plantilla_nomina1233', name: 'plantilla_nomina1233', - label: 'Plantilla Nomina v1.2 - Cfdi 3.3 (ODS): ', labelPosition: 'top', - icon: 'file'}, {maxWidth: 40}, {}]}, - {maxHeight: 20}, - {cols: [{maxWidth: 20}, - {view: 'search', id: 'txt_plantilla_pagos10', name: 'plantilla_pagos10', - label: 'Plantilla Factura de Pagos v1.0 - Cfdi 3.3 (ODS): ', - labelPosition: 'top', icon: 'file'}, {maxWidth: 40}, {}]}, {maxHeight: 20}, + //~ {cols: [{maxWidth: 20}, + //~ {view: 'search', id: 'txt_plantilla_pagos10', name: 'plantilla_pagos10', + //~ label: 'Plantilla Factura de Pagos v1.0 - Cfdi 3.3 (ODS): ', + //~ labelPosition: 'top', icon: 'file'}, {maxWidth: 40}, {}]}, + //~ {maxHeight: 20}, {cols: [{maxWidth: 20}, {view: 'search', id: 'txt_plantilla_ticket', name: 'plantilla_ticket', label: 'Plantilla para Tickets (ODS): ', labelPosition: 'top', icon: 'file'}, - {view: 'search', id: 'txt_plantilla_donataria', name: 'plantilla_donataria', - label: 'Plantilla Donataria (solo ONGs): ', labelPosition: 'top', - icon: 'file'}, + //~ {view: 'search', id: 'txt_plantilla_donataria', name: 'plantilla_donataria', + //~ label: 'Plantilla Donataria (solo ONGs): ', labelPosition: 'top', + //~ icon: 'file'}, {}]}, {maxHeight: 20}, {cols: [{maxWidth: 20}, @@ -832,7 +828,7 @@ var options_admin_products = [ var options_admin_complements = [ - {maxHeight: 20}, + {maxHeight: 10}, {template: 'Complemento de Nómina', type: 'section'}, {cols: [{maxWidth: 15}, {view: 'checkbox', id: 'chk_usar_nomina', labelWidth: 0, @@ -842,7 +838,7 @@ var options_admin_complements = [ {view: 'text', id: 'txt_config_nomina_folio', name: 'config_nomina_folio', label: 'Folio', labelWidth: 50, labelAlign: 'right'}, {maxWidth: 15}]}, - {maxHeight: 20}, + {maxHeight: 10}, {template: 'Complemento de Pagos', type: 'section'}, {cols: [{maxWidth: 15}, {view: 'checkbox', id: 'chk_config_pagos', labelWidth: 0, @@ -854,25 +850,25 @@ var options_admin_complements = [ {view: 'text', id: 'txt_config_cfdipay_folio', name: 'txt_config_cfdipay_serie', label: 'Folio', labelWidth: 50, labelAlign: 'right'}, {maxWidth: 15}]}, - {maxHeight: 20}, + {maxHeight: 10}, {template: 'Complemento de Divisas', type: 'section'}, {cols: [{maxWidth: 15}, {view: 'checkbox', id: 'chk_config_divisas', labelWidth: 0, labelRight: 'Usar complemento de divisas'}, {maxWidth: 15}]}, - {maxHeight: 20}, + {maxHeight: 10}, {template: 'Complemento INE', type: 'section'}, {cols: [{maxWidth: 15}, {view: 'checkbox', id: 'chk_config_ine', labelWidth: 0, labelRight: 'Usar el complemento INE'}, {maxWidth: 15}]}, - {maxHeight: 20}, + {maxHeight: 10}, {template: 'Complemento para escuelas EDU', type: 'section'}, {cols: [{maxWidth: 15}, {view: 'checkbox', id: 'chk_config_edu', labelWidth: 0, labelRight: 'Usar el complemento EDU'}, {maxWidth: 15}]}, - {maxHeight: 20}, + {maxHeight: 10}, {template: 'Complemento Leyendas Fiscales', type: 'section'}, {cols: [{maxWidth: 15}, {view: 'checkbox', id: 'chk_config_leyendas_fiscales', labelWidth: 0, @@ -881,7 +877,7 @@ var options_admin_complements = [ type: 'form', align: 'center', autowidth: true, disabled: true}, {}, {maxWidth: 15} ]}, - {maxHeight: 20}, + {maxHeight: 10}, {template: 'Complemento para Carta Porte', type: 'section'}, {cols: [{maxWidth: 15}, {view: 'checkbox', id: 'chk_config_carta_porte', labelWidth: 0, @@ -1174,6 +1170,7 @@ var admin_taxes = [ 'CEDULAR', 'CMIC', 'SUPERVISION', + 'MANO DE OBRA', ] var admin_sat_impuestos = {cols: [{maxWidth: 15}, diff --git a/source/static/js/ui/bancos.js b/source/static/js/ui/bancos.js index 459d9a8..4b2fa03 100644 --- a/source/static/js/ui/bancos.js +++ b/source/static/js/ui/bancos.js @@ -347,6 +347,8 @@ var toolbar_bank_pay = [ type: 'iconButton', autowidth: true, icon: 'minus'}, {view: 'button', id: 'cmd_pay_delete', label: 'Eliminar', type: 'iconButton', autowidth: true, icon: 'ban'}, + {view: 'checkbox', id: 'chk_pay_close_when_stamp', + label: 'Cerrar al timbrar', tooltip: 'Cerrar al timbrar'}, {}, {view: 'icon', click: '$$("multi_bancos").setValue("banco_home")', icon: 'times-circle'} diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js index 4e488f2..5f99dec 100644 --- a/source/static/js/ui/invoices.js +++ b/source/static/js/ui/invoices.js @@ -418,6 +418,7 @@ var suggest_partners = { {id: 'rfc', adjust: 'data'}, {id: 'forma_pago', hidden: true}, {id: 'uso_cfdi', hidden: true}, + {id: 'codigo_postal', hidden: true}, ], dataFeed:function(text){ if (text.length > 2){ @@ -596,6 +597,10 @@ var controls_generate = [ autowidth:true}, {view: 'label', id: 'lbl_client', name: 'lbl_client', label: 'Ninguno'}, + ]}, + {cols: [{ + view: 'richselect', id: 'lst_invoice_client_regimen', + label: 'Regimen Fiscal: ', labelWidth: 150, options: []} ]} ]}}, {view: 'fieldset', label: 'Buscar Producto', body: {rows: [ @@ -1388,7 +1393,6 @@ var win_carta_import_json = { head: 'Importar Carta Porte JSON', body: body_upload_carta_json, }) - //~ $$('cmd_upload_invoice').attachEvent('onItemClick', cmd_upload_invoice_click) $$('up_invoice_json').attachEvent('onAfterFileAdd', up_invoice_json_on_after_file_add) } } diff --git a/source/static/js/ui/partners.js b/source/static/js/ui/partners.js index d7aeab2..7a125c0 100644 --- a/source/static/js/ui/partners.js +++ b/source/static/js/ui/partners.js @@ -96,7 +96,7 @@ var controls_fiscales = [ {cols: [{view: 'text', id: 'no_interior', name: 'no_interior', width: 300, label: 'No Interior: '},{}]}, {cols: [{view: 'search', id: 'codigo_postal', name: 'codigo_postal', - width: 300, label: 'C.P.: ', attributes: {maxlength: 5}},{}]}, + width: 300, label: 'C.P.: ', attributes: {maxlength: 5}, required: true},{}]}, {view: 'text', id: 'colonia', name: 'colonia', label: 'Colonia: '}, {view: 'text', id: 'municipio', name: 'municipio', label: 'Municipio: '}, {view: 'text', id: 'estado', name: 'estado', label: 'Estado: '}, @@ -122,6 +122,12 @@ var controls_fiscales = [ {view: 'richselect', id: 'lst_uso_cfdi_socio', name: 'uso_cfdi_socio', label: 'Uso del CFDI', options: []}, {}, + ]}, + {template: 'Regimenes Fiscales', type: 'section'}, + {cols: [ + {view: 'list', id: 'lst_receptor_regimenes_fiscales', data: [], + select: 'multiselect', width: 600, height: 125, required: true}, + {}, ]} ] @@ -159,7 +165,7 @@ var controls_others = [ label: 'Cuenta Proveedor: ', disabled: true}, {}] }, {view: 'checkbox', name: 'es_ong', label: 'Es ONG: ', value: false}, - {view: 'text', name: 'tags', label: 'Etiquetas', + {view: 'text', name: 'tags', label: 'Etiquetas', disabled: true, tooltip: 'Utiles para filtrados rápidos. Separa por comas.'}, {view: 'textarea' , height: 200, name: 'notas', label: 'Notas'}, ] diff --git a/source/static/js/ui/products.js b/source/static/js/ui/products.js index eeb3472..df4ab56 100644 --- a/source/static/js/ui/products.js +++ b/source/static/js/ui/products.js @@ -111,6 +111,14 @@ var suggest_sat_producto = { } +var opt_tax_object = [ + {id: '01', value: '[01] No objeto de impuesto.'}, + {id: '02', value: '[02] Sí objeto de impuesto.'}, + {id: '03', value: '[03] Sí objeto del impuesto y no obligado al desglose.'}, + {id: '04', value: '[04] Sí objeto del impuesto y no causa impuesto.'}, +] + + var controls_generals = [ {view: 'checkbox', id: 'es_activo_producto', name: 'es_activo_producto', label: 'Activo: ', value: true, @@ -141,10 +149,14 @@ var controls_generals = [ {view: "richselect", id: "unidad", name: "unidad", label: "Unidad", width: 300, labelWidth: 130, labelAlign: "right", required: true, invalidMessage: "La Unidad es requerida", options: []}, - {view: 'text', id: 'cant_by_packing', name: 'cant_by_packing', - labelAlign: 'right', labelWidth: 150, inputAlign: "right", - label: 'Cantidad por empaque:'}, + {view: 'richselect', id: 'objeto_impuesto', name: 'objeto_impuesto', label: 'Objeto de Impuestos', + width: 500, labelWidth: 150, labelAlign: "right", required: true, + invalidMessage: 'Este campo es requerido', options: opt_tax_object}, {}, + //~ {view: 'text', id: 'cant_by_packing', name: 'cant_by_packing', + //~ labelAlign: 'right', labelWidth: 150, inputAlign: "right", + //~ label: 'Cantidad por empaque:'}, + //~ {}, //~ {view: 'text', id: 'tags_producto', name: 'tags_producto', //~ labelAlign: 'right', label: 'Etiquetas', //~ placeholder: 'Separadas por comas'} diff --git a/source/static/js/ui/tickets.js b/source/static/js/ui/tickets.js index e5407e6..bdd148e 100644 --- a/source/static/js/ui/tickets.js +++ b/source/static/js/ui/tickets.js @@ -233,9 +233,21 @@ var cells_new_ticket = [ ] +var opt_periodicidad = [ + {id: '01', value: '[01] Diario'}, + {id: '02', value: '[02] Semanal'}, + {id: '03', value: '[03] Quincenal'}, + {id: '04', value: '[04] Mensual'}, + //~ {id: '05', value: '[05] Bimestral'}, +] + + var toolbar_ticket_invoice = {view: 'toolbar', elements: [{}, {view: 'checkbox', id: 'chk_is_invoice_day', labelWidth: 0, width: 150, - labelRight: 'Es factura del día'}, {}, + labelRight: 'Es factura del día'}, + {view: 'richselect', id: 'lst_periodicidad', labelWidth: 90, width: 250, + label: 'Periodicidad:', options: opt_periodicidad, value: '01', disabled: true}, + {}, {view: 'button', id: 'cmd_close_ticket_invoice', label: 'Cerrar', type: 'danger', autowidth: true, align: 'center'} ]} diff --git a/source/templates/plantilla_factura.ods b/source/templates/plantilla_factura_3.3.ods similarity index 100% rename from source/templates/plantilla_factura.ods rename to source/templates/plantilla_factura_3.3.ods diff --git a/source/templates/plantilla_factura_4.0.ods b/source/templates/plantilla_factura_4.0.ods new file mode 100644 index 0000000..982474a Binary files /dev/null and b/source/templates/plantilla_factura_4.0.ods differ diff --git a/source/templates/plantilla_factura_cp.ods b/source/templates/plantilla_factura_ccp.ods similarity index 100% rename from source/templates/plantilla_factura_cp.ods rename to source/templates/plantilla_factura_ccp.ods diff --git a/source/templates/plantilla_nomina.ods b/source/templates/plantilla_nomina_3.3_1.2.ods similarity index 100% rename from source/templates/plantilla_nomina.ods rename to source/templates/plantilla_nomina_3.3_1.2.ods diff --git a/source/templates/plantilla_nomina_4.0_1.2.ods b/source/templates/plantilla_nomina_4.0_1.2.ods new file mode 100644 index 0000000..7d8c93a Binary files /dev/null and b/source/templates/plantilla_nomina_4.0_1.2.ods differ diff --git a/source/templates/plantilla_pagos.ods b/source/templates/plantilla_pagos_3.3_1.0.ods similarity index 100% rename from source/templates/plantilla_pagos.ods rename to source/templates/plantilla_pagos_3.3_1.0.ods diff --git a/source/templates/plantilla_pagos_4.0_2.0.ods b/source/templates/plantilla_pagos_4.0_2.0.ods new file mode 100644 index 0000000..07b2528 Binary files /dev/null and b/source/templates/plantilla_pagos_4.0_2.0.ods differ diff --git a/source/xslt/cadena.xslt b/source/xslt/cadena.xslt index f77402c..e238e33 100644 --- a/source/xslt/cadena.xslt +++ b/source/xslt/cadena.xslt @@ -1,347 +1,401 @@ - - - - - - - - - - - - - - - - - - - - - - - - ||| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + ||| + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/xslt/cadena3.3.xslt b/source/xslt/cadena3.3.xslt new file mode 100644 index 0000000..f77402c --- /dev/null +++ b/source/xslt/cadena3.3.xslt @@ -0,0 +1,347 @@ + + + + + + + + + + + + + + + + + + + + + + + + ||| + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/xslt/pagos20.xslt b/source/xslt/pagos20.xslt new file mode 100644 index 0000000..1e6cf98 --- /dev/null +++ b/source/xslt/pagos20.xslt @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/xslt/utilerias.xslt b/source/xslt/utilerias.xslt index d5dd14e..4ae4bf4 100644 --- a/source/xslt/utilerias.xslt +++ b/source/xslt/utilerias.xslt @@ -1,22 +1,22 @@ - + - - - | - - - + + + | + + + - - - - | - - - - - - - + + + + | + + + + + + + diff --git a/source/xslt/utilerias1.1.xslt b/source/xslt/utilerias1.1.xslt new file mode 100644 index 0000000..d5dd14e --- /dev/null +++ b/source/xslt/utilerias1.1.xslt @@ -0,0 +1,22 @@ + + + + + + | + + + + + + + + | + + + + + + + +