diff --git a/CHANGELOG.md b/CHANGELOG.md index 555c285..27274ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +v 2.2.0 [24-Ene-2024] + - Mejora: Soporte para complemento Comercio Exterior 2.0 + - **IMPORTANTE**: Aunque no lo uses, esto afecta al JS de facturación, por + lo que tienes que forzar el refresco (CTRL+F5) si tienes algún problema. + + v 2.1.0 [26-Dic-2023] - Mejora: Se agrega filtro por día en facturas. diff --git a/VERSION b/VERSION index 7ec1d6d..ccbccc3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.0 +2.2.0 diff --git a/source/app/controllers/cfdi_xml.py b/source/app/controllers/cfdi_xml.py index d66a39d..ee3d961 100644 --- a/source/app/controllers/cfdi_xml.py +++ b/source/app/controllers/cfdi_xml.py @@ -117,10 +117,10 @@ SAT = { 'schema': ' http://www.sat.gob.mx/CartaPorte20 http://www.sat.gob.mx/sitio_internet/cfd/CartaPorte/CartaPorte20.xsd', }, 'comercioe': { - 'version': '1.1', - 'prefix': 'cce11', - 'xmlns': 'http://www.sat.gob.mx/ComercioExterior11', - 'schema': ' http://www.sat.gob.mx/ComercioExterior11 http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior11/ComercioExterior11.xsd', + 'version': '2.0', + 'prefix': 'cce20', + 'xmlns': 'http://www.sat.gob.mx/ComercioExterior20', + 'schema': ' http://www.sat.gob.mx/ComercioExterior20 http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior20/ComercioExterior20.xsd', } } @@ -144,6 +144,7 @@ class CFDI(object): self._comercio_exterior = False self._divisas = '' self._tipo_de_comprobante = '' + self._exportacion = DEFAULT['exportacion'] self.error = '' def _now(self): @@ -168,7 +169,9 @@ class CFDI(object): if 'nomina' in datos: self._nomina(datos['nomina']) - return self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8')) + xml = self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8')) + + return xml def add_sello(self, sello, cert_txt): self._cfdi.attrib['Sello'] = sello @@ -198,6 +201,8 @@ class CFDI(object): self._leyendas = bool(datos['complementos'].get('leyendas', False)) self._carta_porte = bool(datos['complementos'].get('cartaporte', False)) self._comercio_exterior = bool(datos['complementos'].get('comercioe', False)) + if self._comercio_exterior: + self._exportacion = datos['complementos']['comercioe'].pop('Exportacion') self._divisas = datos['comprobante'].pop('divisas', '') @@ -288,7 +293,7 @@ class CFDI(object): # ~ cfdi4 if not 'Exportacion' in attributes: - attributes['Exportacion'] = DEFAULT['exportacion'] + attributes['Exportacion'] = self._exportacion self._tipo_de_comprobante = attributes['TipoDeComprobante'] @@ -515,6 +520,47 @@ class CFDI(object): return + def _complemento_comercio_exterior(self, datos): + prefix = SAT['comercioe']['prefix'] + + emisor = datos.pop('emisor') + propietarios = datos.pop('propietarios', {}) + receptor = datos.pop('receptor') + destinatario = datos.pop('destinatario', {}) + mercancias = datos.pop('mercancias') + + attr = {'Version': SAT['comercioe']['version']} + attr.update(datos) + ce = ET.SubElement( + self._complemento, f'{prefix}:ComercioExterior', attr) + + attributes = {} + if 'Curp' in emisor: + attributes = {'Curp': emisor.pop('Curp')} + node = ET.SubElement(ce, '{}:Emisor'.format(prefix), attributes) + ET.SubElement(node, '{}:Domicilio'.format(prefix), emisor) + + attributes = {} + if 'NumRegIdTrib' in receptor: + attributes = {'NumRegIdTrib': receptor.pop('NumRegIdTrib')} + node = ET.SubElement(ce, '{}:Receptor'.format(prefix), attributes) + ET.SubElement(node, '{}:Domicilio'.format(prefix), receptor) + + node = ET.SubElement(ce, '{}:Mercancias'.format(prefix)) + fields = ('Marca', 'Modelo', 'SubModelo', 'NumeroSerie') + for row in mercancias: + detalle = {} + for f in fields: + if f in row and row[f]: + detalle[f] = row[f] + row.pop(f) + concepto = ET.SubElement(node, '{}:Mercancia'.format(prefix), row) + if detalle: + ET.SubElement( + concepto, '{}:DescripcionesEspecificas'.format(prefix), detalle) + + return + def _complementos(self, datos): if not datos: return @@ -639,58 +685,7 @@ class CFDI(object): ET.SubElement(node_leyend, '{}:Leyenda'.format(pre), leyend) if self._comercio_exterior: - prefix = SAT['comercioe']['prefix'] datos = datos.pop('comercioe') - emisor = datos.pop('emisor') - # ~ propietario = datos.pop('propietario') - receptor = datos.pop('receptor') - destinatario = datos.pop('destinatario') - conceptos = datos.pop('mercancias') + self._complemento_comercio_exterior(datos) - # ~ attributes = {} - # ~ attributes['xmlns:{}'.format(pre)] = \ - # ~ 'http://www.sat.gob.mx/ComercioExterior11' - # ~ attributes['xsi:schemaLocation'] = \ - # ~ 'http://www.sat.gob.mx/ComercioExterior11 ' \ - # ~ 'http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior11/ComercioExterior11.xsd' - - attr = {'Version': SAT['comercioe']['version']} - attr.update(datos) - ce = ET.SubElement( - self._complemento, f'{prefix}:ComercioExterior', attr) - - attributes = {} - if 'Curp' in emisor: - attributes = {'Curp': emisor.pop('Curp')} - node = ET.SubElement(ce, '{}:Emisor'.format(prefix), attributes) - ET.SubElement(node, '{}:Domicilio'.format(prefix), emisor) - - # ~ if propietario: - # ~ ET.SubElement(ce, '{}:Propietario'.format(prefix), propietario) - - attributes = {} - if 'NumRegIdTrib' in receptor: - attributes = {'NumRegIdTrib': receptor.pop('NumRegIdTrib')} - node = ET.SubElement(ce, '{}:Receptor'.format(prefix), attributes) - ET.SubElement(node, '{}:Domicilio'.format(prefix), receptor) - - attributes = {} - if 'NumRegIdTrib' in destinatario: - attributes = {'NumRegIdTrib': destinatario.pop('NumRegIdTrib')} - if 'Nombre' in destinatario: - attributes.update({'Nombre': destinatario.pop('Nombre')}) - node = ET.SubElement(ce, '{}:Destinatario'.format(prefix), attributes) - ET.SubElement(node, '{}:Domicilio'.format(prefix), destinatario) - - node = ET.SubElement(ce, '{}:Mercancias'.format(prefix)) - fields = ('Marca', 'Modelo', 'SubModelo', 'NumeroSerie') - for row in conceptos: - detalle = {} - for f in fields: - if f in row: - detalle[f] = row.pop(f) - concepto = ET.SubElement(node, '{}:Mercancia'.format(prefix), row) - if detalle: - ET.SubElement( - concepto, '{}:DescripcionesEspecificas'.format(prefix), detalle) return diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 5b50c77..f1cfb29 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -1022,6 +1022,51 @@ class LIBO(object): return + def _comercio_exterior(self, data): + if not data: + return + + emisor = data.pop('emisor') + receptor = data.pop('receptor') + mercancias = data.pop('mercancias') + + for k, v in data.items(): + self._set_cell(f'{{cce.{k}}}', v) + for k, v in emisor.items(): + self._set_cell(f'{{cce.emisor.{k}}}', v) + for k, v in receptor.items(): + self._set_cell(f'{{cce.receptor.{k}}}', v) + + first = True + count = len(mercancias) - 1 + for i, mercancia in enumerate(mercancias): + no_identificacion = mercancia['NoIdentificacion'] + fraccion = mercancia['FraccionArancelaria'] + unidad = mercancia['UnidadAduana'] + cantidad = mercancia['CantidadAduana'] + valor_unitario = mercancia['ValorUnitarioAduana'] + valor_dolares = mercancia['ValorDolares'] + if first: + first = False + cell_1 = self._set_cell('{cce.mercancia.noidentificacion}', no_identificacion) + cell_2 = self._set_cell('{cce.mercancia.fraccionarancelaria}', fraccion) + cell_3 = self._set_cell('{cce.mercancia.unidadaduana}', unidad) + cell_4 = self._set_cell('{cce.mercancia.cantidadaduana}', cantidad) + cell_5 = self._set_cell('{cce.mercancia.valorunitarioaduana}', valor_unitario) + cell_6 = self._set_cell('{cce.mercancia.valordolares}', valor_dolares) + if count > 0: + row = cell_1.CellAddress.Row + 1 + self._sheet.getRows().insertByIndex(row, count) + self._copy_paste_rows(cell_1, count) + else: + cell_1 = self._set_cell(v=no_identificacion, cell=cell_1) + cell_2 = self._set_cell(v=fraccion, cell=cell_2) + cell_3 = self._set_cell(v=unidad, cell=cell_3) + cell_4 = self._set_cell(v=cantidad, cell=cell_4) + cell_5 = self._set_cell(v=valor_unitario, cell=cell_5) + cell_6 = self._set_cell(v=valor_dolares, cell=cell_6) + return + def _nomina(self, data): if not data: return @@ -1258,6 +1303,7 @@ class LIBO(object): self._divisas(data.get('divisas', {})) self._leyendas(data.get('leyendas', '')) self._carta_porte(data.get('carta_porte', {})) + self._comercio_exterior(data.get('comercio_exterior', {})) self._timbre(data['timbre']) @@ -1571,6 +1617,59 @@ class LIBO(object): rows = tuple(data[1:]) return rows, '' + def _data_to_dict(self, rows): + data = {k: v for k, v in rows if v} + return data + + def _current_region_to_tuple(self, cursor): + data = [] + cursor.collapseToCurrentRegion() + rows = cursor.getDataArray()[1:] + + if len(rows) == 1: + return data + + keys = rows[0] + data = [dict(zip(keys, values)) for values in rows[1:]] + return data + + def _get_data_ce(self, doc): + msg = '' + data = {} + try: + sheet = doc.Sheets[0] + rango = sheet['A2:B10'] + data = self._data_to_dict(rango.DataArray) + rango = sheet['A13:B23'] + data['emisor'] = self._data_to_dict(rango.DataArray) + rango = sheet['A26:B36'] + data['receptor'] = self._data_to_dict(rango.DataArray) + rango = sheet['A39:B50'] + data['destinatario'] = self._data_to_dict(rango.DataArray) + cursor = sheet.createCursorByRange(sheet['E12']) + data['propietarios'] = self._current_region_to_tuple(cursor) + cursor = sheet.createCursorByRange(sheet['A53']) + data['mercancias'] = self._current_region_to_tuple(cursor) + except Exception as e: + msg = str(e) + + return data, msg + + def get_ce(self, path): + options = {'AsTemplate': True, 'Hidden': True} + doc = self._doc_open(path, options) + if doc is None: + return (), 'No se pudo abrir la plantilla' + + data, msg = self._get_data_ce(doc) + doc.close(True) + + if len(data) == 1: + msg = 'Sin datos para importar' + return (), msg + + return data, '' + def to_pdf(data, emisor_rfc, ods=False, pdf_from='1'): rfc = data['emisor']['rfc'] @@ -1602,8 +1701,13 @@ def to_pdf(data, emisor_rfc, ods=False, pdf_from='1'): default = f'plantilla_donatarias_{version}_{version_donatarias}.ods' version = f'{version}_cd_{version_donatarias}' + if 'comercio_exterior' in data: + version_cce = data['comercio_exterior']['version'] + default = f'plantilla_cce_{version}_{version_cce}.ods' + version = f'{version}_cce_{version_cce}' + template_name = f'{rfc.lower()}_{version}.ods' - # ~ print('T', template_name, default) + # ~ print('\nT', template_name, default) if APP_LIBO: app = LIBO() @@ -2317,6 +2421,7 @@ def upload_file(rfc, opt, file_obj): '_4.0_cp_2.0.ods', '_4.0_ccp_2.0.ods', '_4.0_cd_1.1.ods', + '_4.0_cce_2.0.ods', '_4.0.json', ) if opt in versions: @@ -2438,6 +2543,15 @@ def upload_file(rfc, opt, file_obj): name = '{}_nomina.ods'.format(rfc.lower()) path = _join(PATH_MEDIA, 'tmp', name) + elif opt == 'ceods': + tmp = file_obj.filename.split('.') + ext = tmp[-1].lower() + if ext != 'ods': + msg = 'Extensión de archivo incorrecta, selecciona un archivo ODS' + return {'status': 'server', 'name': msg, 'ok': False} + + name = '{}_ce.ods'.format(rfc.lower()) + path = _join(PATH_MEDIA, 'tmp', name) if save_file(path, file_obj.file.read()): return {'status': 'server', 'name': file_obj.filename, 'ok': True} @@ -2915,6 +3029,20 @@ def import_invoice(rfc): return (), 'No se encontro LibreOffice' +def import_ceods(rfc): + name = '{}_ce.ods'.format(rfc.lower()) + path = _join(PATH_MEDIA, 'tmp', name) + if not is_file(path): + return (), 'No se encontró la plantilla' + + if APP_LIBO: + app = LIBO() + if app.is_running: + return app.get_ce(path) + + return (), 'No se encontro LibreOffice' + + def calc_to_date(value): return datetime.date.fromordinal(int(value) + 693594) diff --git a/source/app/controllers/utils.py b/source/app/controllers/utils.py index 00dd02e..313a788 100644 --- a/source/app/controllers/utils.py +++ b/source/app/controllers/utils.py @@ -265,6 +265,7 @@ class CfdiToDict(object): 'leyendasFisc': 'http://www.sat.gob.mx/leyendasFiscales', 'cartaporte20': 'http://www.sat.gob.mx/CartaPorte20', 'nomina12': 'http://www.sat.gob.mx/nomina12', + 'cce20': 'http://www.sat.gob.mx/ComercioExterior20', } tipo_figura = { '01': '[01] Operador', @@ -513,6 +514,29 @@ class CfdiToDict(object): self._values['carta_porte'] = values + self._complemento_comercio_exterior(complemento) + + return + + def _complemento_comercio_exterior(self, complemento): + path = '//cce20:ComercioExterior' + comercio_exterior = complemento.xpath(path, namespaces=self.NS) + + if comercio_exterior: + values = CaseInsensitiveDict(comercio_exterior[0].attrib) + + for node in comercio_exterior[0]: + if 'Emisor' in node.tag: + values['emisor'] = CaseInsensitiveDict(node.attrib) + values['emisor'].update(CaseInsensitiveDict(node[0].attrib)) + elif 'Receptor' in node.tag: + values['receptor'] = CaseInsensitiveDict(node.attrib) + values['receptor'].update(CaseInsensitiveDict(node[0].attrib)) + elif 'Mercancias' in node.tag: + mercancias = [ + CaseInsensitiveDict(m.attrib) for m in node] + values['mercancias'] = mercancias + self._values['comercio_exterior'] = values return @@ -861,9 +885,6 @@ def get_cert(args): def make_xml(data, certificado): cert = SATCertificate(certificado.cer, certificado.key_enc.encode()) - # ~ if DEBUG: - # ~ data['emisor']['Rfc'] = certificado.rfc - # ~ data['emisor']['RegimenFiscal'] = '603' cfdi = CFDI() xml = ET.parse(BytesIO(cfdi.get_xml(data).encode())) @@ -1087,6 +1108,7 @@ def _save_template(rfc, name, file_obj): rfc = rfc.lower() path = _join(PATHS['USER'], f'{rfc}{name}') + if save_file(path, file_obj.file.read()): result['ok'] = True diff --git a/source/app/models/db.py b/source/app/models/db.py index 291b315..7b7f1f3 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -70,6 +70,9 @@ class StorageEngine(object): def _get_importinvoice(self, values): return main.import_invoice() + def _get_importceods(self, values): + return main.import_ceods() + def _get_main(self, values, session): return main.config_main(session['userobj']) diff --git a/source/app/models/main.py b/source/app/models/main.py index 936b765..ed75c0f 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -157,7 +157,7 @@ def upload_file(rfc, opt, file_obj): result = util.upload_file(rfc, opt, file_obj) if result['ok']: - names = ('bdfl', 'employees', 'nomina', 'products', 'invoiceods') + names = ('bdfl', 'employees', 'nomina', 'products', 'invoiceods', 'ceods') if not opt in names: Configuracion.add({opt: file_obj.filename}) return result @@ -268,6 +268,18 @@ def import_invoice(): return {'ok': True, 'rows': tuple(products)} +def import_ceods(): + log.info('Importando plantilla...') + emisor = Emisor.select()[0] + data, msg = util.import_ceods(emisor.rfc) + + if not data: + return {'ok': False, 'msg': msg} + + log.info('Plantilla importada...') + return {'ok': True, 'data': data} + + def get_doc(type_doc, id, rfc): types = { 'xml': 'application/xml', @@ -5611,12 +5623,9 @@ class Facturas(BaseModel): if not valores: return - # ~ values = utils.loads(valores) - data = { 'factura': invoice, 'nombre': 'comercioe', - # ~ 'valores': utils.dumps(values), 'valores': valores, } FacturasComplementos.create(**data) diff --git a/source/app/settings.py b/source/app/settings.py index 669bed2..a2458d9 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -39,7 +39,7 @@ except ImportError: DEBUG = DEBUG -VERSION = '2.1.0' +VERSION = '2.2.0' EMAIL_SUPPORT = ('soporte@empresalibre.mx',) TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION) diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index c3943a9..74aca5f 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -22,7 +22,7 @@ var tipo_relacion = '' var anticipo = false var donativo = false var cfg_invoice = new Object() -var values_comercioe = null +//~ var values_comercioe = null var values_global = '' @@ -102,7 +102,15 @@ var invoices_controllers = { $$('cmd_carta_add_product').attachEvent('onItemClick', cmd_carta_add_product_click) $$('cmd_carta_copy_from_invoice').attachEvent('onItemClick', cmd_carta_copy_from_invoice_click) $$('cmd_carta_import_json').attachEvent('onItemClick', cmd_carta_import_json_click) + $$('cmd_import_json_comercioe').attachEvent('onItemClick', cmd_import_json_comercioe_click) + $$('cmd_ce_import_ods').attachEvent('onItemClick', cmd_ce_import_ods_click) + $$('cmd_ce_tipo_cambio').attachEvent('onItemClick', cmd_ce_tipo_cambio_click) + $$('cmd_ce_add_propietario').attachEvent('onItemClick', cmd_ce_add_propietario_click) + $$('cmd_ce_add_mercancia').attachEvent('onItemClick', cmd_ce_add_mercancia_click) + $$('grid_ce_propietarios').attachEvent('onItemClick', grid_ce_propietarios_click) + $$('grid_ce_mercancias').attachEvent('onItemClick', grid_ce_mercancias_click) + $$('cmd_show_global_information').attachEvent('onItemClick', cmd_show_global_information_click) webix.extend($$('grid_invoices'), webix.ProgressBar) @@ -239,6 +247,7 @@ function default_config(){ $$('tv_invoice').getTabbar().hideOption('Comercio Exterior') }else{ $$('tv_invoice').getTabbar().showOption('Comercio Exterior') + _set_default_comercio_exterior() } cfg_invoice['leyendasfiscales'] = values.cfdi_leyendasfiscales cfg_invoice['edu'] = values.cfdi_edu @@ -574,6 +583,12 @@ function validate_invoice(values){ } } + //~ validate comercio exterior + var usar_comercioe = $$('chk_cfdi_usar_comercioe').getValue() + if(usar_comercioe){ + var values = _get_values_comercio_exterior() + } + return true } @@ -848,14 +863,14 @@ function guardar_y_timbrar(values){ var usar_comercioe = $$('chk_cfdi_usar_comercioe').getValue() if(usar_comercioe){ - data['comercioe'] = values_comercioe + data['comercioe'] = _get_values_comercio_exterior() } if(!save_invoice(data)){ return } - values_comercioe = null + //~ values_comercioe = null values_global = '' $$('chk_cfdi_usar_comercioe').setValue(false) @@ -907,16 +922,11 @@ function cmd_timbrar_click(id, e, node){ usar_ine = $$('chk_cfdi_usar_ine').getValue() if(usar_ine){ - msg += 'Estas usando el complemento INE

' + msg += 'Estas usando el complemento: INE

' } if($$('chk_cfdi_usar_comercioe').getValue()){ - msg += 'Estas usando el complemento Comercio Exterior

' - if(values_comercioe === null){ - msg = 'El complemento de Comercio Exterior esta vacío' - msg_error(msg) - return - } + msg += 'Estas usando el complemento:
Comercio Exterior

' } if(tipo_comprobante == 'T'){ @@ -2952,9 +2962,54 @@ function up_invoice_json_on_after_file_add(obj){ } -function _set_from_json_comercioe(data){ +function _set_default_comercio_exterior(){ + const controls = { + lst_ce_exportacion: '02', + lst_ce_motivo_traslado: '', + lst_ce_clave_pedimento: 'A1', + lst_ce_certificado_origen: '0', + txt_ce_numero_certificado: '', + txt_ce_numero_exportador: '', + lst_ce_incoterm: 'CFR', + txt_ce_observaciones: '', + txt_ce_tipo_cambio_usd: '', + txt_ce_total_usd: '', + }; + + Object.keys(controls).forEach(key => { + $$(key).setValue(controls[key]) + }); + + var grid = $$('grid_ce_emisor') + grid.clearAll() + grid.add({id: 0}) + + var grid = $$('grid_ce_receptor') + grid.clearAll() + grid.add({id: 0}) + + var grid = $$('grid_ce_destinatario') + grid.clearAll() + grid.add({id: 0}) + + var grid = $$('grid_ce_propietarios') + grid.clearAll() + + var grid = $$('grid_ce_mercancias') + grid.clearAll() +} + + +function _set_from_json_comercioe(data, json){ + + _set_default_comercio_exterior() + try{ - values_comercioe = JSON.parse(data) + if(json){ + values = JSON.parse(data) + }else{ + values = data + } }catch(e){ msg_error('Revisa el archivo JSON') webix.alert({ @@ -2964,6 +3019,66 @@ function _set_from_json_comercioe(data){ }) return } + + const controls = { + Exportacion: 'lst_ce_exportacion', + MotivoTraslado: 'lst_ce_motivo_traslado', + ClaveDePedimento: 'lst_ce_clave_pedimento', + CertificadoOrigen: 'lst_ce_certificado_origen', + NumCertificadoOrigen: 'txt_ce_numero_certificado', + NumeroExportadorConfiable: 'txt_ce_numero_exportador', + Incoterm: 'lst_ce_incoterm', + Observaciones: 'txt_ce_observaciones', + TipoCambioUSD: 'txt_ce_tipo_cambio_usd', + TotalUSD: 'txt_ce_total_usd', + }; + + Object.keys(controls).forEach(key => { + if(key in values){ + $$(controls[key]).setValue(values[key]) + } + }); + + var grid = $$('grid_ce_emisor') + grid.clearAll() + if ('emisor' in values) { + grid.add(values['emisor']) + } else { + grid.add({id: 0}) + } + + var grid = $$('grid_ce_receptor') + grid.clearAll() + if ('receptor' in values) { + grid.add(values['receptor']) + } else { + grid.add({id: 0}) + } + + var grid = $$('grid_ce_destinatario') + grid.clearAll() + if ('destinatario' in values) { + grid.add(values['destinatario']) + } else { + grid.add({id: 0}) + } + + var grid = $$('grid_ce_propietarios') + grid.clearAll() + if ('propietarios' in values) { + values['propietarios'].forEach(function(row, index){ + row['delete'] = '-' + grid.add(row) + }) + } + + var grid = $$('grid_ce_mercancias') + grid.clearAll() + values['mercancias'].forEach(function(row, index){ + row['delete'] = '-' + grid.add(row) + }) + msg = 'Valores cargados correctamente' msg_ok(msg) } @@ -2979,7 +3094,7 @@ function up_invoice_json_comercioe_on_after_file_add(obj){ let reader = new FileReader() reader.readAsText(obj.file) reader.onload = function(){ - _set_from_json_comercioe(reader.result) + _set_from_json_comercioe(reader.result, true) } $$('win_import_json_comercioe').close() } @@ -3000,3 +3115,175 @@ function cmd_save_global_information_click(){ function cmd_win_global_close_click(){ $$('win_global_information').close() } + + +function cmd_ce_tipo_cambio_click(){ + window.open('https://www.banxico.org.mx/tipcamb/tipCamMIAction.do?idioma=sp', '_blank') +} + + +function cmd_ce_add_propietario_click(){ + var g = $$('grid_ce_propietarios') + g.add({delete: '-'}) +} + + +function grid_ce_propietarios_click(id, e, node){ + if(id.column != 'delete'){ + return + } + this.remove(id.row) +} + + +function cmd_ce_add_mercancia_click(){ + var g = $$('grid_ce_mercancias') + g.add({delete: '-'}) +} + + +function grid_ce_mercancias_click(id, e, node){ + if(id.column != 'delete'){ + return + } + this.remove(id.row) +} + + + +function _get_values_comercio_exterior(){ + var form = $$('form_comercio_exterior') + + if(!form.validate()) { + msg_error('Valores inválidos en Comercio Exterior') + return + } + + const controls = { + Exportacion: 'lst_ce_exportacion', + MotivoTraslado: 'lst_ce_motivo_traslado', + ClaveDePedimento: 'lst_ce_clave_pedimento', + CertificadoOrigen: 'lst_ce_certificado_origen', + NumCertificadoOrigen: 'txt_ce_numero_certificado', + NumeroExportadorConfiable: 'txt_ce_numero_exportador', + Incoterm: 'lst_ce_incoterm', + Observaciones: 'txt_ce_observaciones', + TipoCambioUSD: 'txt_ce_tipo_cambio_usd', + TotalUSD: 'txt_ce_total_usd', + }; + + var values = new Object() + + Object.keys(controls).forEach(key => { + var value = $$(controls[key]).getValue().trim() + if(value){ + values[key] = value + } + }); + + var propietarios = $$('grid_ce_propietarios').data.getRange() + propietarios.forEach(function(row, index){ + delete row['id'] + delete row['delete'] + }) + + var mercancias = $$('grid_ce_mercancias').data.getRange() + mercancias.forEach(function(row, index){ + delete row['id'] + delete row['delete'] + }) + + var emisor = $$('grid_ce_emisor').data.getRange()[0] + delete emisor['id'] + + var receptor = $$('grid_ce_receptor').data.getRange()[0] + delete receptor['id'] + + var destinatario = $$('grid_ce_destinatario').data.getRange()[0] + delete destinatario['id'] + + values['emisor'] = emisor + values['propietarios'] = propietarios + values['receptor'] = receptor + values['destinatario'] = destinatario + values['mercancias'] = mercancias + + return values +} + + +function cmd_ce_import_ods_click(){ + win_ce_import_ods.init() + $$('win_ce_import_ods').show() +} + + +function cmd_ce_upload_ods_click(){ + var form = $$('form_ce_import_ods') + + var values = form.getValues() + + if(!$$('lst_ce_up_template').count()){ + $$('win_ce_import_ods').close() + return + } + + if($$('lst_ce_up_template').count() > 1){ + msg = 'Selecciona solo un archivo' + msg_error(msg) + return + } + + var template = $$('ce_up_template').files.getItem($$('ce_up_template').files.getFirstId()) + + if(template.type.toLowerCase() != 'ods'){ + msg = 'Archivo inválido.\n\nSe requiere un archivo ODS' + msg_error(msg) + return + } + + msg = '¿Estás seguro de importar este archivo?' + webix.confirm({ + title: 'Importar datos de plantilla', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + $$('ce_up_template').send() + } + } + }) +} + + +function ce_up_template_complete(response){ + if(response.status != 'server'){ + msg = 'Ocurrio un error al subir el archivo' + msg_error(msg) + return + } + msg = 'Archivo subido correctamente.\n\nComenzando importación.' + msg_ok(msg) + $$('win_ce_import_ods').close() + + webix.ajax().get('/values/importceods', { + error: function(text, data, xhr) { + msg_error('Error al consultar') + }, + success: function(text, data, xhr){ + var values = data.json() + if (values.ok){ + _set_from_json_comercioe(values.data, false) + //~ msg_ok('Plantilla importada correctamente...') + }else{ + webix.alert({ + title: 'Error al importar', + text: values.msg, + type: 'alert-error', + }) + } + } + }) +} \ No newline at end of file diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js index 68d6c22..3c3f04c 100644 --- a/source/static/js/ui/admin.js +++ b/source/static/js/ui/admin.js @@ -605,6 +605,7 @@ var opt_templates_cfdi = [ {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: '_4.0_cce_2.0.ods', value: 'CFDI v4.0 - Comercio Exterior v2.0'}, {id: '_4.0.json', value: 'CFDI v4.0 - JSON'}, {id: '_3.3.ods', value: 'CFDI v3.3'}, {id: '_3.3_cn_1.2.ods', value: 'CFDI v3.3 - Nómina v1.2'}, diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js index 2ad6da8..790d277 100644 --- a/source/static/js/ui/invoices.js +++ b/source/static/js/ui/invoices.js @@ -1210,13 +1210,268 @@ var controls_carta_porte = [ ] +var opt_ce_exportacion = [ + {id: '02', value: '[02] Definitiva con clave A1'}, + {id: '03', value: '[03] Temporal'}, + {id: '04', value: '[04] Definitiva con clave distinta a A1'}, +] + + +var opt_ce_motivo_traslado = [ + {id: '', value: ''}, + {id: '01', value: '[01] Envío de mercancias facturadas con anterioridad'}, + {id: '02', value: '[02] Reubicación de mercancías propias'}, + {id: '03', value: '[03] Envío de mercancías objeto de contrato de consignación'}, + {id: '04', value: '[04] Envío de mercancías para posterior enajenación'}, + {id: '05', value: '[05] Envío de mercancías propiedad de terceros'}, + {id: '99', value: '[99] Otros'}, +] + + +var opt_ce_clave_pedimento = [ + {id: 'A1', value: '[A1] Definitiva'}, +] + + +var opt_ce_certificado_origen = [ + {id: '0', value: '[0] No funge'}, + {id: '1', value: '[1] Funge'}, +] + + +var opt_ce_incoterm = [ + {id: 'CFR', value: '[CFR] Coste y flete (puerto de destino convenido)'}, + {id: 'CIF', value: '[CIF] Coste, seguro y flete (puerto de destino convenido)'}, + {id: 'CPT', value: '[CPT] Trasnporte pagado hasta el lugar de destino convenido'}, + {id: 'CIP', value: '[CIP] Trasnporte y seguro pagado hasta el lugar de destino convenido'}, + {id: 'DAP', value: '[DAP] Entregada en lugar'}, + {id: 'DDP', value: '[DDP] Entregada derechos pagados en lugar de destino convenido'}, + {id: 'DPU', value: '[DPU] Entregada y descargada en lugar acordado'}, + {id: 'EXW', value: '[EXW] En fábrica (lugar convenido)'}, + {id: 'FCA', value: '[FCA] Franco transportista (lugar designado)'}, + {id: 'FAS', value: '[FAS] Franco al costado del buque (puerto de carga convenido)'}, + {id: 'FOB', value: '[FOB] Franco a bordo (puerto de carga convenido)'}, +] + + +var body_ce_datos_generales = {rows:[ + {cols: [{maxWidth: 15}, + {view: 'richselect', id: 'lst_ce_exportacion', label: 'Exportación: ', + value: '02', labelWidth: 130, minWidth: 400, options: opt_ce_exportacion}, + {view: 'richselect', id: 'lst_ce_motivo_traslado', label: 'Motivo Traslado: ', + labelWidth: 130, minWidth: 500, options: opt_ce_motivo_traslado}, + {}, + {maxWidth: 15}]}, + {cols: [{maxWidth: 15}, + {view: 'richselect', id: 'lst_ce_clave_pedimento', label: 'Clave de Pedimento: ', + value: 'A1', labelWidth: 130, minWidth: 400, maxWidth: 400, options: opt_ce_clave_pedimento}, + {view: 'richselect', id: 'lst_ce_certificado_origen', label: 'Certificado Origen: ', + value: '0', labelWidth: 130, maxWidth: 300, options: opt_ce_certificado_origen}, + {view: 'text', id: 'txt_ce_numero_certificado', label: 'Nº Certificado:', labelWidth: 100}, + {maxWidth: 15}]}, + {cols: [{maxWidth: 15}, + {view: 'text', id: 'txt_ce_numero_exportador', label: 'Nº Exportador: ', + labelWidth: 130, minWidth: 400, maxWidth: 500}, + {view: 'richselect', id: 'lst_ce_incoterm', label: 'Incoterm: ', value: 'CFR', + labelWidth: 130, minWidth: 500, maxWidth: 500, options: opt_ce_incoterm}, + {view: 'text', id: 'txt_ce_observaciones', label: 'Observaciones:', labelWidth: 100}, + {maxWidth: 15}]}, + {cols: [{maxWidth: 15}, + {view: 'text', id: 'txt_ce_total_usd', label: 'Total USD: ', + labelWidth: 130, minWidth: 400, inputAlign: 'right'}, + {view: 'text', id: 'txt_ce_tipo_cambio_usd', label: 'Tipo Cambio USD:', labelWidth: 130, + inputAlign: 'right'}, + {}, + {view: 'button', id: 'cmd_ce_tipo_cambio', label: 'TC Banxico', + type: 'iconButton', autowidth: true, icon: ''}, + {maxWidth: 15}]}, +]} + + +var grid_cols_ce_emisor = [ + {id: 'id', header: 'ID', hidden: true}, + {id: 'Curp', header: 'Curp', editor: 'text', fillspace: 1}, + {id: 'Calle', header: 'Calle *', editor: 'text', fillspace: 1}, + {id: 'NumeroExterior', header: 'Numero Exterior', editor: 'text', fillspace: 1}, + {id: 'NumeroInterior', header: 'Número Interior', editor: 'text', fillspace: 1}, + {id: 'Colonia', header: 'Colonia', editor: 'text', fillspace: 1}, + {id: 'Municipio', header: 'Municipio', editor: 'text', fillspace: 1}, + {id: 'Estado', header: 'Estado *', editor: 'text', fillspace: 1}, + {id: 'Pais', header: 'País *', editor: 'text', fillspace: 1}, + {id: 'CodigoPostal', header: 'C.P. *', editor: 'text', fillspace: 1}, +] + + +var grid_ce_emisor = { + view: 'datatable', + id: 'grid_ce_emisor', + multiselect: false, + adjust: true, + autoheight: true, + headermenu: true, + editable: true, + columns: grid_cols_ce_emisor, + data: [{id: 0}], +} + +var body_ce_emisor = {rows:[ + grid_ce_emisor +]} + + +var grid_cols_ce_propietarios = [ + {id: 'id', header: 'ID', hidden: true}, + {id: 'delete', header: '', width: 30, css: 'delete'}, + {id: 'NumRegIdTrib', header: 'Registro Fiscal', editor: 'text', fillspace: 1}, + {id: 'ResidenciaFiscal', header: 'Residencia Fiscal', editor: 'text', fillspace: 1}, +] + + +var grid_ce_propietarios = { + view: 'datatable', + id: 'grid_ce_propietarios', + multiselect: false, + adjust: true, + autoheight: true, + headermenu: true, + editable: true, + footer: false, + columns: grid_cols_ce_propietarios, +} + + +var body_ce_propietarios = {rows:[ + {cols: [ + {view: 'button', id: 'cmd_ce_add_propietario', label: 'Agregar Propietario', + icon: 'plus', type: 'iconButton', autowidth: true, align: 'center'}, + {}, + ]}, + {maxHeight: 10}, + grid_ce_propietarios +]} + + +var grid_cols_ce_receptor = [ + {id: 'id', header: 'ID', hidden: true}, + {id: 'NumRegIdTrib', header: 'Registro Fiscal', editor: 'text', fillspace: 1}, + {id: 'Calle', header: 'Calle *', editor: 'text', fillspace: 1}, + {id: 'NumeroExterior', header: 'Numero Exterior', editor: 'text', fillspace: 1}, + {id: 'NumeroInterior', header: 'Número Interior', editor: 'text', fillspace: 1}, + {id: 'Colonia', header: 'Colonia', editor: 'text', fillspace: 1}, + {id: 'Municipio', header: 'Municipio', editor: 'text', fillspace: 1}, + {id: 'Estado', header: 'Estado *', editor: 'text', fillspace: 1}, + {id: 'Pais', header: 'País *', editor: 'text', fillspace: 1}, + {id: 'CodigoPostal', header: 'C.P. *', editor: 'text', fillspace: 1}, +] + + +var grid_ce_receptor = { + view: 'datatable', + id: 'grid_ce_receptor', + multiselect: false, + adjust: true, + autoheight: true, + headermenu: true, + editable: true, + columns: grid_cols_ce_receptor, + data: [{id: 0}], +} + + +var body_ce_receptor = {rows:[ + grid_ce_receptor +]} + + +var grid_cols_ce_destinatario = [ + {id: 'id', header: 'ID', hidden: true}, + {id: 'NumRegIdTrib', header: 'Registro Fiscal', editor: 'text', fillspace: 1}, + {id: 'Nombre', header: 'Nombre', editor: 'text', fillspace: 1}, + {id: 'Calle', header: 'Calle *', editor: 'text', fillspace: 1}, + {id: 'NumeroExterior', header: 'No Exterior', editor: 'text', fillspace: 1}, + {id: 'NumeroInterior', header: 'No Interior', editor: 'text', fillspace: 1}, + {id: 'Colonia', header: 'Colonia', editor: 'text', fillspace: 1}, + {id: 'Municipio', header: 'Municipio', editor: 'text', fillspace: 1}, + {id: 'Estado', header: 'Estado *', editor: 'text', fillspace: 1}, + {id: 'Pais', header: 'País *', editor: 'text', fillspace: 1}, + {id: 'CodigoPostal', header: 'C.P. *', editor: 'text', fillspace: 1}, +] + + +var grid_ce_destinatario = { + view: 'datatable', + id: 'grid_ce_destinatario', + multiselect: false, + adjust: true, + autoheight: true, + headermenu: true, + editable: true, + columns: grid_cols_ce_destinatario, + data: [{id: 0}], +} + + +var body_ce_destinatario = {rows:[ + grid_ce_destinatario +]} + + +var grid_cols_ce_mercancias = [ + {id: 'id', header: 'ID', hidden: true}, + {id: 'delete', header: '', width: 30, css: 'delete'}, + {id: 'NoIdentificacion', header: 'Clave', editor: 'text', fillspace: 1}, + {id: 'FraccionArancelaria', header: 'Fraccion Arancelaria', editor: 'text', fillspace: 1}, + {id: 'CantidadAduana', header: 'Cantidad Aduana', editor: 'text', fillspace: 1}, + {id: 'UnidadAduana', header: 'Unidad Aduana', editor: 'text', fillspace: 1}, + {id: 'ValorUnitarioAduana', header: 'PU Aduana', editor: 'text', fillspace: 1}, + {id: 'ValorDolares', header: 'Valor USD', editor: 'text', fillspace: 1}, + {id: 'Marca', header: 'Marca', editor: 'text', hidden: true, fillspace: 1}, + {id: 'Modelo', header: 'Modelo', editor: 'text', hidden: true, fillspace: 1}, + {id: 'SubModelo', header: 'SubModelo', editor: 'text', hidden: true, fillspace: 1}, + {id: 'NumeroSerie', header: 'Serie', editor: 'text', hidden: true, fillspace: 1}, +] + + +var grid_ce_mercancias = { + view: 'datatable', + id: 'grid_ce_mercancias', + multiselect: false, + adjust: true, + autoheight: true, + headermenu: true, + editable: true, + footer: true, + columns: grid_cols_ce_mercancias, +} + + +var body_ce_mercancias = {rows:[ + {cols: [ + {view: 'button', id: 'cmd_ce_add_mercancia', label: 'Agregar Mercancía', + icon: 'plus', type: 'iconButton', autowidth: true, align: 'center'}, + {}, + ]}, + {maxHeight: 10}, + grid_ce_mercancias +]} + + var controls_comercio_exterior = [ {cols: [{maxWidth: 15}, {view: 'checkbox', id: 'chk_cfdi_usar_comercioe', labelWidth: 0, - labelRight: 'Usar el complemento Comercio Exterior'}, {}, + labelRight: 'Usar el complemento Comercio Exterior'}, + {}, {view: 'button', id: 'cmd_import_json_comercioe', label: 'Importar JSON', icon: 'upload', type: 'iconButton', autowidth: true, align: 'center'}, - {maxWidth: 15}]} + {view: 'button', id: 'cmd_ce_import_ods', label: 'Importar ODS', + icon: 'upload', type: 'iconButton', autowidth: true, align: 'center'}, + {maxWidth: 15}]}, + {view: 'fieldset', label: 'Datos generales', body: body_ce_datos_generales}, + {view: 'fieldset', label: 'Emisor', body: body_ce_emisor}, + {view: 'fieldset', label: 'Propietario', body: body_ce_propietarios}, + {view: 'fieldset', label: 'Receptor', body: body_ce_receptor}, + {view: 'fieldset', label: 'Destinatario', body: body_ce_destinatario}, + {view: 'fieldset', label: 'Mercancías', body: body_ce_mercancias}, ] @@ -1324,7 +1579,13 @@ var win_import_invoice = { width: 400, modal: true, position: 'center', - head: 'Importar Factura de Plantilla', + head: {view: 'toolbar', + elements: [ + {view: 'label', label: 'Importar Factura en Lote'}, + {view: 'icon', icon: 'times-circle', + click: '$$("win_import_invoice").close()'}, + ] + }, body: body_upload_invoice, }) $$('cmd_upload_invoice').attachEvent('onItemClick', cmd_upload_invoice_click) @@ -1473,3 +1734,41 @@ var win_global_information = { $$('cmd_win_global_close').attachEvent('onItemClick', cmd_win_global_close_click) } } + + +var body_win_ce_import_ods = {rows: [ + {view: 'form', id: 'form_ce_import_ods', rows: [ + {cols: [{}, + {view: 'uploader', id: 'ce_up_template', autosend: false, + link: 'lst_ce_up_template', value: 'Seleccionar Archivo', + upload: '/files/ceods'}, {}]}, + {cols: [ + {view: 'list', id: 'lst_ce_up_template', + type: 'uploader', autoheight: true, borderless: true}]}, + {cols: [{}, {view: 'button', id: 'cmd_ce_upload_ods', + label: 'Importar Plantilla'}, {}]}, + ]}, +]} + + +var win_ce_import_ods = { + init: function(){ + webix.ui({ + view: 'window', + id: 'win_ce_import_ods', + width: 400, + modal: true, + position: 'center', + head: {view: 'toolbar', + elements: [ + {view: 'label', label: 'Importar desde archivo ODS'}, + {view: 'icon', icon: 'times-circle', + click: '$$("win_ce_import_ods").close()'}, + ] + }, + body: body_win_ce_import_ods, + }) + $$('cmd_ce_upload_ods').attachEvent('onItemClick', cmd_ce_upload_ods_click) + $$('ce_up_template').attachEvent('onUploadComplete', ce_up_template_complete) + } +} \ No newline at end of file diff --git a/source/templates/plantilla_cce_4.0_2.0.ods b/source/templates/plantilla_cce_4.0_2.0.ods new file mode 100644 index 0000000..a71d80a Binary files /dev/null and b/source/templates/plantilla_cce_4.0_2.0.ods differ diff --git a/source/xslt/cadena.xslt b/source/xslt/cadena.xslt index e238e33..218d9d2 100644 --- a/source/xslt/cadena.xslt +++ b/source/xslt/cadena.xslt @@ -11,7 +11,7 @@ - + diff --git a/source/xslt/comercioexterior20.xslt b/source/xslt/comercioexterior20.xslt new file mode 100644 index 0000000..feaeb0e --- /dev/null +++ b/source/xslt/comercioexterior20.xslt @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file