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