modificaciones facturacion electronica
This commit is contained in:
@@ -0,0 +1,817 @@
|
||||
{
|
||||
-----------------------------------------------------------------------------
|
||||
MODULO : CPPRMT001_ECF
|
||||
OBJETIVO : Compra electronica e-CF 41 (Comprobante de Compras) desde
|
||||
cpprmt001 (Facturas de proveedores).
|
||||
- Boton "Compra Electronica": trae el proximo e-NCF 41 del rango
|
||||
(vetb00071/vetb00073, tipo_doc 'FT', codigo_dgii '41') y abre la
|
||||
ventana cpfmwd041 donde se digita el NETO, el % de ISR, el tipo
|
||||
de retencion ISR (606) y si lleva ITBIS. El resto se calcula solo:
|
||||
subtotal = neto / (1 - %ISR)
|
||||
ISR retenido = subtotal - neto
|
||||
ITBIS (18%) = subtotal * 18% (si lleva ITBIS)
|
||||
ITBIS retenido = 100% del ITBIS
|
||||
total = subtotal + ITBIS
|
||||
- El e-NCF se RESERVA al grabar (dentro de la transaccion), asi no
|
||||
se pierden secuencias si cancelan.
|
||||
- La retencion de ISR de la distribucion contable debe estar en una
|
||||
cuenta marcada como retencion en el catalogo (cgtb00001.retencion).
|
||||
- Al grabar: guarda el rastreo en cptb00041 y envia el XML del 41.
|
||||
FECHA : Octubre 2026
|
||||
-----------------------------------------------------------------------------
|
||||
}
|
||||
IMPORT util
|
||||
GLOBALS "cpprgb000.4gl"
|
||||
|
||||
DEFINE wcp om.SaxDocumentHandler
|
||||
|
||||
-- estado de la compra electronica en curso
|
||||
DEFINE e41 RECORD
|
||||
activo BOOLEAN,
|
||||
ncf41 VARCHAR(13),
|
||||
nom41 VARCHAR(90),
|
||||
rnc41 VARCHAR(11),
|
||||
neto41 DECIMAL(12,2),
|
||||
porc41 DECIMAL(5,2),
|
||||
cisr41 SMALLINT,
|
||||
itbis41 CHAR(1),
|
||||
bs41 CHAR(1),
|
||||
sub41 DECIMAL(12,2),
|
||||
itb41 DECIMAL(12,2),
|
||||
isr41 DECIMAL(12,2),
|
||||
itbret41 DECIMAL(12,2),
|
||||
total41 DECIMAL(12,2)
|
||||
END RECORD
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
FUNCTION e41_reset()
|
||||
INITIALIZE e41.* TO NULL
|
||||
LET e41.activo = FALSE
|
||||
END FUNCTION
|
||||
|
||||
FUNCTION e41_activo() RETURNS BOOLEAN
|
||||
RETURN NVL(e41.activo, FALSE)
|
||||
END FUNCTION
|
||||
|
||||
FUNCTION e41_ncf() RETURNS VARCHAR(13)
|
||||
RETURN e41.ncf41
|
||||
END FUNCTION
|
||||
|
||||
FUNCTION e41_neto() RETURNS DECIMAL(12,2)
|
||||
RETURN e41.neto41
|
||||
END FUNCTION
|
||||
|
||||
FUNCTION e41_itbis() RETURNS DECIMAL(12,2)
|
||||
RETURN e41.itb41
|
||||
END FUNCTION
|
||||
|
||||
FUNCTION e41_isr() RETURNS DECIMAL(12,2)
|
||||
RETURN NVL(e41.isr41, 0)
|
||||
END FUNCTION
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# % de ISR permitidos: 0 = exento, 3 = servicios tecnicos,
|
||||
# 15 = honorarios / profesionales / alquileres
|
||||
#-----------------------------------------------------------------------------
|
||||
FUNCTION porc_isr_valido41(pporc)
|
||||
DEFINE pporc DECIMAL(5,2)
|
||||
|
||||
IF pporc IS NULL THEN
|
||||
RETURN FALSE
|
||||
END IF
|
||||
RETURN (pporc = 0 OR pporc = 3 OR pporc = 15)
|
||||
END FUNCTION
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Calcula los montos del 41 a partir del neto (lo que recibe el proveedor).
|
||||
# El ISR se saca por diferencia para que el neto cuadre exacto al centavo.
|
||||
#-----------------------------------------------------------------------------
|
||||
FUNCTION calcula41(pneto, pporc_isr, plleva_itbis)
|
||||
DEFINE pneto DECIMAL(12,2),
|
||||
pporc_isr DECIMAL(5,2),
|
||||
plleva_itbis CHAR(1),
|
||||
xsubtotal, xitbis, xisr, xitbis_ret, xtotal DECIMAL(12,2)
|
||||
|
||||
LET pneto = NVL(pneto, 0)
|
||||
LET pporc_isr = NVL(pporc_isr, 0)
|
||||
|
||||
LET xsubtotal = pneto / (1 - pporc_isr / 100) -- se redondea a 2 decimales
|
||||
LET xisr = xsubtotal - pneto
|
||||
IF plleva_itbis = 'S' THEN
|
||||
LET xitbis = xsubtotal * 18 / 100
|
||||
ELSE
|
||||
LET xitbis = 0
|
||||
END IF
|
||||
LET xitbis_ret = xitbis -- se retiene el 100%
|
||||
LET xtotal = xsubtotal + xitbis
|
||||
|
||||
RETURN xsubtotal, xitbis, xisr, xitbis_ret, xtotal
|
||||
END FUNCTION
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Deja solo los digitos (cedula/RNC sin guiones ni espacios)
|
||||
#-----------------------------------------------------------------------------
|
||||
FUNCTION e41_solo_digitos(ptexto)
|
||||
DEFINE ptexto, xres STRING,
|
||||
i INT
|
||||
|
||||
LET xres = ""
|
||||
FOR i = 1 TO ptexto.getLength()
|
||||
IF ptexto.getCharAt(i) MATCHES "[0-9]" THEN
|
||||
LET xres = xres.append(ptexto.getCharAt(i))
|
||||
END IF
|
||||
END FOR
|
||||
IF xres.getLength() = 0 THEN
|
||||
RETURN NULL
|
||||
END IF
|
||||
RETURN xres
|
||||
END FUNCTION
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Proximo e-NCF 41 SIN reservarlo (solo para mostrarlo).
|
||||
# Retorna NULL si no hay rango o se agoto.
|
||||
#-----------------------------------------------------------------------------
|
||||
FUNCTION e41_proximo_ncf() RETURNS VARCHAR(13)
|
||||
DEFINE xinicial, xfinal, xult, xnuevo INT
|
||||
|
||||
SELECT MIN(a.ncf_inicial), MAX(a.ncf_final) INTO xinicial, xfinal
|
||||
FROM vetb00071 a
|
||||
WHERE a.tipo_doc = 'FT' AND a.codigo_dgii = '41' AND a.disponible = 'S'
|
||||
IF xfinal IS NULL THEN
|
||||
CALL fgl_winmessage("COMPRA ELECTRONICA",
|
||||
"No hay rango disponible para el e-NCF 41 (vetb00071 tipo_doc FT, codigo 41).\nLlame a finanzas.","stop")
|
||||
RETURN NULL
|
||||
END IF
|
||||
|
||||
SELECT MAX(a.ncf) INTO xult FROM vetb00073 a
|
||||
WHERE a.tipo_doc = 'FT' AND a.codigo_dgii = '41'
|
||||
IF xult IS NULL OR xult < NVL(xinicial, 1) - 1 THEN
|
||||
LET xult = NVL(xinicial, 1) - 1
|
||||
END IF
|
||||
LET xnuevo = xult + 1
|
||||
IF xnuevo > xfinal THEN
|
||||
CALL fgl_winmessage("COMPRA ELECTRONICA","Se agoto el rango del e-NCF 41. Llame a finanzas.","stop")
|
||||
RETURN NULL
|
||||
END IF
|
||||
RETURN "E41" || (xnuevo USING "&&&&&&&&&&")
|
||||
END FUNCTION
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Reserva el e-NCF 41 (se llama DENTRO de la transaccion de grabado).
|
||||
# Si vetb00073 no tiene la fila FT/41 la crea desde ncf_inicial.
|
||||
# Retorna NULL si no hay rango o se agoto.
|
||||
#-----------------------------------------------------------------------------
|
||||
FUNCTION e41_reserva_ncf() RETURNS VARCHAR(13)
|
||||
DEFINE xinicial, xfinal, xnuevo INT
|
||||
|
||||
SELECT MIN(a.ncf_inicial), MAX(a.ncf_final) INTO xinicial, xfinal
|
||||
FROM vetb00071 a
|
||||
WHERE a.tipo_doc = 'FT' AND a.codigo_dgii = '41' AND a.disponible = 'S'
|
||||
IF xfinal IS NULL THEN
|
||||
CALL fgl_winmessage("COMPRA ELECTRONICA","No hay rango disponible para el e-NCF 41.","stop")
|
||||
RETURN NULL
|
||||
END IF
|
||||
|
||||
UPDATE vetb00073 SET ncf = ncf + 1, us_crea = usuarios, fech_crea = getdate()
|
||||
WHERE tipo_doc = 'FT' AND codigo_dgii = '41'
|
||||
IF STATUS < 0 THEN
|
||||
CALL fgl_winmessage("COMPRA ELECTRONICA","Error reservando el e-NCF 41: "||SQLCA.sqlcode,"stop")
|
||||
RETURN NULL
|
||||
END IF
|
||||
IF SQLCA.sqlerrd[3] = 0 THEN
|
||||
LET xnuevo = NVL(xinicial, 1)
|
||||
INSERT INTO vetb00073 (codigo_dgii, tipo_doc, ncf, sucid, us_crea, fech_crea)
|
||||
VALUES ('41', 'FT', xnuevo, 1, usuarios, getdate())
|
||||
IF STATUS < 0 THEN
|
||||
CALL fgl_winmessage("COMPRA ELECTRONICA","Error creando el contador del e-NCF 41: "||SQLCA.sqlcode,"stop")
|
||||
RETURN NULL
|
||||
END IF
|
||||
ELSE
|
||||
SELECT MAX(a.ncf) INTO xnuevo FROM vetb00073 a
|
||||
WHERE a.tipo_doc = 'FT' AND a.codigo_dgii = '41'
|
||||
END IF
|
||||
|
||||
IF xnuevo > xfinal THEN
|
||||
CALL fgl_winmessage("COMPRA ELECTRONICA","Se agoto el rango del e-NCF 41. Llame a finanzas.","stop")
|
||||
RETURN NULL
|
||||
END IF
|
||||
LET e41.ncf41 = "E41" || (xnuevo USING "&&&&&&&&&&")
|
||||
RETURN e41.ncf41
|
||||
END FUNCTION
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# TRUE si la cuenta esta marcada como retencion en el catalogo.
|
||||
# Si tiene codigo de ISR y en la ventana no se escogio, se toma de la cuenta.
|
||||
#-----------------------------------------------------------------------------
|
||||
FUNCTION e41_cuenta_isr(pcuenta)
|
||||
DEFINE pcuenta CHAR(8),
|
||||
xret CHAR(2),
|
||||
xcod SMALLINT
|
||||
|
||||
LET xret = NULL
|
||||
LET xcod = NULL
|
||||
SELECT a.retencion, a.codigo_isr INTO xret, xcod
|
||||
FROM cgtb00001 a
|
||||
WHERE a.cuenta_no = pcuenta
|
||||
IF xret IS NULL OR UPSHIFT(xret) <> "SI" THEN
|
||||
RETURN FALSE
|
||||
END IF
|
||||
IF e41.cisr41 IS NULL AND xcod IS NOT NULL THEN
|
||||
LET e41.cisr41 = xcod
|
||||
END IF
|
||||
RETURN TRUE
|
||||
END FUNCTION
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Llena el combo de tipos de retencion ISR (cgtb00077) de la ventana del 41
|
||||
#-----------------------------------------------------------------------------
|
||||
FUNCTION e41_carga_cisr()
|
||||
DEFINE cb ui.ComboBox,
|
||||
xcod SMALLINT,
|
||||
xdes CHAR(50)
|
||||
|
||||
LET cb = ui.ComboBox.forName("formonly.cisr41")
|
||||
IF cb IS NULL THEN
|
||||
RETURN
|
||||
END IF
|
||||
CALL cb.clear()
|
||||
DECLARE c_cisr41 CURSOR FOR
|
||||
SELECT a.codigo_isr, a.descripcion FROM cgtb00077 a ORDER BY a.codigo_isr
|
||||
FOREACH c_cisr41 INTO xcod, xdes
|
||||
CALL cb.addItem(xcod, (xcod USING "<&") || " - " || xdes CLIPPED)
|
||||
END FOREACH
|
||||
END FUNCTION
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
FUNCTION e41_recalcula()
|
||||
CALL calcula41(e41.neto41, e41.porc41, e41.itbis41)
|
||||
RETURNING e41.sub41, e41.itb41, e41.isr41, e41.itbret41, e41.total41
|
||||
DISPLAY e41.sub41, e41.itb41, e41.isr41, e41.itbret41, e41.total41
|
||||
TO sub41, itb41, isr41, itbret41, total41
|
||||
END FUNCTION
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Boton "Compra Electronica": valida el suplidor, muestra el proximo e-NCF
|
||||
# y captura neto / % ISR / tipo ISR / ITBIS. Retorna TRUE si se acepto.
|
||||
#-----------------------------------------------------------------------------
|
||||
FUNCTION e41_captura(pcod_sp, pcod_sp_sec)
|
||||
DEFINE pcod_sp, pcod_sp_sec SMALLINT,
|
||||
xnom VARCHAR(90),
|
||||
xrnc STRING,
|
||||
xproximo VARCHAR(13),
|
||||
xcodigo SMALLINT
|
||||
|
||||
-- tipo fiscal 41 en la lista de tipos NCF
|
||||
LET xcodigo = NULL
|
||||
SELECT a.codigo_fiscal INTO xcodigo FROM cgtb00074 a WHERE a.codigo_fiscal = 41
|
||||
IF xcodigo IS NULL THEN
|
||||
CALL fgl_winmessage("COMPRA ELECTRONICA",
|
||||
"No existe el tipo NCF 41 en cgtb00074. Agreguelo antes de emitir.","stop")
|
||||
RETURN FALSE
|
||||
END IF
|
||||
|
||||
-- el proveedor va en el bloque Comprador: cedula (11) o RNC (9) obligatorio
|
||||
SELECT a.nom_sp, a.rnc INTO xnom, xrnc FROM cotb00001 a
|
||||
WHERE a.cod_sp = pcod_sp AND a.cod_sp_sec = pcod_sp_sec
|
||||
LET xrnc = e41_solo_digitos(xrnc)
|
||||
IF xrnc IS NULL OR (xrnc.getLength() <> 9 AND xrnc.getLength() <> 11) THEN
|
||||
CALL fgl_winmessage("COMPRA ELECTRONICA",
|
||||
"El suplidor "||pcod_sp||"-"||pcod_sp_sec||" no tiene cedula (11) o RNC (9) valido en el maestro de suplidores.\nCorrijalo antes de emitir el 41.","stop")
|
||||
RETURN FALSE
|
||||
END IF
|
||||
|
||||
LET xproximo = e41_proximo_ncf()
|
||||
IF xproximo IS NULL THEN
|
||||
RETURN FALSE
|
||||
END IF
|
||||
|
||||
-- valores por defecto (si ya se habia capturado, se conservan)
|
||||
IF NOT NVL(e41.activo, FALSE) THEN
|
||||
INITIALIZE e41.* TO NULL
|
||||
LET e41.porc41 = 3
|
||||
LET e41.cisr41 = 3
|
||||
LET e41.itbis41 = 'S'
|
||||
LET e41.bs41 = '2'
|
||||
END IF
|
||||
LET e41.ncf41 = xproximo
|
||||
LET e41.nom41 = xnom CLIPPED
|
||||
LET e41.rnc41 = xrnc
|
||||
|
||||
OPEN WINDOW w_e41 WITH FORM "cpfmwd041"
|
||||
ATTRIBUTES(STYLE="dialog", TEXT="Compra electronica (e-CF 41)")
|
||||
CALL e41_carga_cisr()
|
||||
LET int_flag = FALSE
|
||||
|
||||
INPUT BY NAME e41.neto41, e41.porc41, e41.cisr41, e41.itbis41, e41.bs41
|
||||
WITHOUT DEFAULTS ATTRIBUTES(UNBUFFERED)
|
||||
BEFORE INPUT
|
||||
DISPLAY e41.ncf41, e41.nom41, e41.rnc41 TO ncf41, nom41, rnc41
|
||||
CALL e41_recalcula()
|
||||
|
||||
ON CHANGE porc41
|
||||
-- tipo de retencion sugerido segun el %
|
||||
CASE e41.porc41
|
||||
WHEN 3 LET e41.cisr41 = 3 -- otras rentas (servicios tecnicos)
|
||||
WHEN 15 LET e41.cisr41 = 2 -- honorarios (alquiler = 1, escoger a mano)
|
||||
WHEN 0 LET e41.cisr41 = NULL
|
||||
END CASE
|
||||
CALL e41_recalcula()
|
||||
|
||||
ON CHANGE itbis41
|
||||
CALL e41_recalcula()
|
||||
|
||||
AFTER FIELD neto41
|
||||
CALL e41_recalcula()
|
||||
|
||||
AFTER INPUT
|
||||
IF NOT int_flag THEN
|
||||
IF e41.neto41 IS NULL OR e41.neto41 <= 0 THEN
|
||||
CALL fgl_winmessage("COMPRA ELECTRONICA","Digite el neto a pagar.","exclamation")
|
||||
NEXT FIELD neto41
|
||||
END IF
|
||||
IF NOT porc_isr_valido41(e41.porc41) THEN
|
||||
CALL fgl_winmessage("COMPRA ELECTRONICA","El % de ISR debe ser 0, 3 o 15.","exclamation")
|
||||
NEXT FIELD porc41
|
||||
END IF
|
||||
IF e41.porc41 > 0 AND e41.cisr41 IS NULL THEN
|
||||
CALL fgl_winmessage("COMPRA ELECTRONICA","Escoja el tipo de retencion ISR (formato 606).","exclamation")
|
||||
NEXT FIELD cisr41
|
||||
END IF
|
||||
CALL e41_recalcula()
|
||||
END IF
|
||||
END INPUT
|
||||
|
||||
CLOSE WINDOW w_e41
|
||||
IF int_flag THEN
|
||||
LET int_flag = FALSE
|
||||
RETURN FALSE
|
||||
END IF
|
||||
LET e41.activo = TRUE
|
||||
RETURN TRUE
|
||||
END FUNCTION
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Guarda el 41 en cptb00041 (dentro de la transaccion de grabado)
|
||||
#-----------------------------------------------------------------------------
|
||||
FUNCTION e41_guarda(pcod_sp, pcod_sp_sec, pnum_doc, pfecha)
|
||||
DEFINE pcod_sp, pcod_sp_sec SMALLINT,
|
||||
pnum_doc CHAR(13),
|
||||
pfecha DATE
|
||||
|
||||
INSERT INTO cptb00041 (cod_sp, cod_sp_sec, num_doc, ncf, fecha, rnc, neto, porc_isr,
|
||||
codigo_isr, lleva_itbis, bien_servicio, subtotal, itbis,
|
||||
isr_retenido, itbis_retenido, total, us_crea, fech_crea)
|
||||
VALUES (pcod_sp, pcod_sp_sec, pnum_doc, e41.ncf41, pfecha, e41.rnc41, e41.neto41,
|
||||
e41.porc41, e41.cisr41, e41.itbis41, e41.bs41, e41.sub41, e41.itb41,
|
||||
e41.isr41, e41.itbret41, e41.total41, usuarios, getdate())
|
||||
IF STATUS < 0 THEN
|
||||
CALL fgl_winmessage("COMPRA ELECTRONICA","Error guardando el e-CF 41 (cptb00041): "||SQLCA.sqlcode,"stop")
|
||||
RETURN FALSE
|
||||
END IF
|
||||
RETURN TRUE
|
||||
END FUNCTION
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Arma y envia el XML del 41 a la DGII (despues del COMMIT) y guarda la
|
||||
# respuesta en cptb00041. Muestra el estado al usuario.
|
||||
#-----------------------------------------------------------------------------
|
||||
FUNCTION e41_envia(pcod_sp, pcod_sp_sec, pnum_doc, pfecha_orig, pfecha_proc, pdetalle)
|
||||
DEFINE pcod_sp, pcod_sp_sec SMALLINT,
|
||||
pnum_doc CHAR(13),
|
||||
pfecha_orig, pfecha_proc DATE,
|
||||
pdetalle CHAR(30)
|
||||
|
||||
DEFINE documento_encabezado RECORD
|
||||
tipoEcf, eNcf, indicadorMontoGravado, tipoPago, fechaLimitePago, FormadePAgo, montoPago, FechaVencimientoSecuencia,
|
||||
rncEmisor, razonSocialEmisor, direccionEmisor, Municipio, Provincia, telefonoEmisor,
|
||||
correoEmisor, webSite, codigoVendedor, numeroFacturaInterna, numeroPedidoInterno,
|
||||
fechaEmision, rncComprador, razonSocialComprador, montoGravadoTotal, montoGravadoI1,
|
||||
montoGravadoI2, montoGravadoI3, montoExento, itbis1, itbis2, itbis3, totalItbis, totalItbis1,
|
||||
totalItbis2, totalItbis3, montoTotal, tipoIngreso, terminoPago, TipoCuentaPago, NumeroCuentaPago, IdentificadorExtranjero,
|
||||
contactoComprador, CorreoComprador, DireccionComprador, MunicipioComprador, ProvinciaComprador, FechaEntrega, FechaOrdenCompra,
|
||||
NumeroOrdenCompra, CodigoInternoComprador,
|
||||
nombreFile, IndicadorNotaCredito, NCFmodificado, codigoModificacion, fechaNCFmodificado STRING
|
||||
END RECORD,
|
||||
documento_Detalle DYNAMIC ARRAY OF RECORD
|
||||
indicadorFacturacion, nombreItem, indicadorBienoServicio, cantidadItem, precioUnitarioItem,
|
||||
montoItem STRING,
|
||||
DescripcionItem VARCHAR(1000),
|
||||
NumeroLinea INT
|
||||
END RECORD,
|
||||
ret41 DYNAMIC ARRAY OF RECORD
|
||||
itbisRetenido DECIMAL(12,2),
|
||||
isrRetenido DECIMAL(12,2)
|
||||
END RECORD
|
||||
|
||||
DEFINE xrnc, xnombre, xdireccion VARCHAR(150),
|
||||
xprov, xmuni VARCHAR(20),
|
||||
xvence DATE,
|
||||
xamb STRING,
|
||||
phash, respuesta, xrespuesta, codigo, xhash STRING,
|
||||
fechafirma DATETIME YEAR TO SECOND,
|
||||
mensaje_trunc VARCHAR(2000),
|
||||
idx INT
|
||||
|
||||
DEFINE Pok RECORD
|
||||
trackId, ERROR, mensaje STRING
|
||||
END RECORD
|
||||
DEFINE p RECORD
|
||||
trackid STRING,
|
||||
codigo, estado, rnc, encf, secuenciaUtilizada, fechaRecepcion STRING,
|
||||
mensajes DYNAMIC ARRAY OF RECORD
|
||||
valor, codigo STRING
|
||||
END RECORD
|
||||
END RECORD
|
||||
|
||||
-- emisor
|
||||
SELECT a.rnc, a.nombre, a.direccion, a.id_dgii, a.id_municipio_dgii
|
||||
INTO xrnc, xnombre, xdireccion, xprov, xmuni
|
||||
FROM companias a
|
||||
SELECT MAX(a.fecha_vencimiento) INTO xvence FROM vetb00071 a
|
||||
WHERE a.tipo_doc = 'FT' AND a.codigo_dgii = '41'
|
||||
|
||||
LET documento_encabezado.tipoEcf = "41"
|
||||
LET documento_encabezado.eNcf = e41.ncf41
|
||||
LET documento_encabezado.nombreFile = xrnc CLIPPED, e41.ncf41 CLIPPED, ".xml"
|
||||
LET documento_encabezado.FechaVencimientoSecuencia = xvence USING "dd-mm-yyyy"
|
||||
LET documento_encabezado.rncEmisor = xrnc CLIPPED
|
||||
LET documento_encabezado.razonSocialEmisor = xnombre CLIPPED
|
||||
LET documento_encabezado.direccionEmisor = xdireccion CLIPPED
|
||||
LET documento_encabezado.Provincia = xprov CLIPPED
|
||||
LET documento_encabezado.Municipio = xmuni CLIPPED
|
||||
LET documento_encabezado.fechaEmision = pfecha_orig USING "dd-mm-yyyy"
|
||||
-- va a cuentas por pagar: credito, vence en la fecha de proceso
|
||||
LET documento_encabezado.tipoPago = '2'
|
||||
LET documento_encabezado.fechaLimitePago = NVL(pfecha_proc, pfecha_orig) USING "dd-mm-yyyy"
|
||||
LET documento_encabezado.FormadePAgo = '4'
|
||||
-- Comprador = el proveedor
|
||||
LET documento_encabezado.rncComprador = e41.rnc41
|
||||
LET documento_encabezado.razonSocialComprador = e41.nom41
|
||||
-- precios SIN ITBIS
|
||||
LET documento_encabezado.indicadorMontoGravado = '0'
|
||||
IF NVL(e41.itb41, 0) > 0 THEN
|
||||
LET documento_encabezado.montoGravadoTotal = e41.sub41 USING "<<<<<<<<<<<<<&.&&"
|
||||
LET documento_encabezado.montoGravadoI1 = e41.sub41 USING "<<<<<<<<<<<<<&.&&"
|
||||
LET documento_encabezado.itbis1 = '18'
|
||||
LET documento_encabezado.totalItbis = e41.itb41 USING "<<<<<<<<<<<<<&.&&"
|
||||
LET documento_encabezado.totalItbis1 = e41.itb41 USING "<<<<<<<<<<<<<&.&&"
|
||||
ELSE
|
||||
LET documento_encabezado.montoExento = e41.sub41 USING "<<<<<<<<<<<<<&.&&"
|
||||
END IF
|
||||
LET documento_encabezado.montoTotal = e41.total41 USING "<<<<<<<<<<<<<&.&&"
|
||||
LET documento_encabezado.montoPago = documento_encabezado.montoTotal
|
||||
|
||||
-- una linea con el concepto de la factura
|
||||
IF NVL(e41.itb41, 0) > 0 THEN
|
||||
LET documento_Detalle[1].indicadorFacturacion = 1
|
||||
ELSE
|
||||
LET documento_Detalle[1].indicadorFacturacion = 4
|
||||
END IF
|
||||
LET documento_Detalle[1].nombreItem = NVL(pdetalle CLIPPED, "COMPRA A PROVEEDOR INFORMAL")
|
||||
LET documento_Detalle[1].indicadorBienoServicio = NVL(e41.bs41, '2')
|
||||
LET documento_Detalle[1].cantidadItem = "1.00"
|
||||
LET documento_Detalle[1].precioUnitarioItem = e41.sub41 USING "<<<<<<<<<<<<<&.&&"
|
||||
LET documento_Detalle[1].montoItem = e41.sub41 USING "<<<<<<<<<<<<<&.&&"
|
||||
LET ret41[1].itbisRetenido = NVL(e41.itbret41, 0)
|
||||
LET ret41[1].isrRetenido = NVL(e41.isr41, 0)
|
||||
|
||||
CALL generafile41(documento_encabezado.*, documento_Detalle, ret41, 1, 1, 'adicionar')
|
||||
RETURNING phash, respuesta, fechafirma, codigo
|
||||
|
||||
LET mensaje_trunc = respuesta
|
||||
UPDATE cptb00041 SET hash = phash, ultima_respuesta = respuesta, fechafirmadigital = fechafirma
|
||||
WHERE cod_sp = pcod_sp AND cod_sp_sec = pcod_sp_sec AND num_doc = pnum_doc
|
||||
|
||||
-- respuesta del envio
|
||||
IF codigo = '200' THEN
|
||||
TRY
|
||||
CALL util.JSON.parse(respuesta, Pok)
|
||||
CATCH
|
||||
LET Pok.trackId = NULL
|
||||
LET Pok.mensaje = 'ERROR EN PARSE'
|
||||
END TRY
|
||||
ELSE
|
||||
LET Pok.trackId = NULL
|
||||
LET Pok.mensaje = respuesta
|
||||
END IF
|
||||
|
||||
-- estado en la DGII
|
||||
LET p.estado = NULL
|
||||
LET p.trackid = NULL
|
||||
CALL p.mensajes.clear()
|
||||
IF codigo = '200' THEN
|
||||
LET xamb = ambiente()
|
||||
CALL procesoEncf(' ', xamb.trim(), ' ', 0, 0, 'consultatrackid', respuesta)
|
||||
RETURNING xhash, xrespuesta, codigo
|
||||
TRY
|
||||
CALL util.JSON.parse(xrespuesta, p)
|
||||
CATCH
|
||||
LET p.estado = 'SIN CONEXION'
|
||||
LET p.mensajes[1].valor = 'NO SE PUDO CONSULTAR EL ESTADO EN LA DGII'
|
||||
END TRY
|
||||
END IF
|
||||
IF p.estado = ' ' OR p.estado IS NULL THEN
|
||||
LET p.estado = 'ERROR'
|
||||
END IF
|
||||
IF p.trackid IS NULL THEN
|
||||
LET p.trackid = Pok.trackId
|
||||
END IF
|
||||
LET mensaje_trunc = NVL(Pok.mensaje, mensaje_trunc)
|
||||
IF p.mensajes.getLength() > 0 THEN
|
||||
LET mensaje_trunc = NULL
|
||||
FOR idx = 1 TO p.mensajes.getLength()
|
||||
IF p.mensajes[idx].valor IS NOT NULL THEN
|
||||
IF mensaje_trunc IS NULL THEN
|
||||
LET mensaje_trunc = p.mensajes[idx].valor
|
||||
ELSE
|
||||
LET mensaje_trunc = mensaje_trunc CLIPPED, " | ", p.mensajes[idx].valor
|
||||
END IF
|
||||
END IF
|
||||
END FOR
|
||||
END IF
|
||||
UPDATE cptb00041 SET trackid = p.trackid, estado_dgii = p.estado, mensaje = mensaje_trunc
|
||||
WHERE cod_sp = pcod_sp AND cod_sp_sec = pcod_sp_sec AND num_doc = pnum_doc
|
||||
|
||||
IF UPSHIFT(p.estado) MATCHES "*RECHAZ*" THEN
|
||||
CALL fgl_winmessage("COMPRA ELECTRONICA RECHAZADA POR LA DGII",
|
||||
"e-NCF "||e41.ncf41 CLIPPED||"\n\nMotivo: "||NVL(mensaje_trunc,"(sin mensaje)"),"stop")
|
||||
ELSE
|
||||
CALL fgl_winmessage("COMPRA ELECTRONICA",
|
||||
"e-NCF "||e41.ncf41 CLIPPED||"\nEstado DGII: "||p.estado||"\n"||NVL(mensaje_trunc,""),"info")
|
||||
END IF
|
||||
END FUNCTION
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# e-CF 41 COMPRAS: la empresa le compra a una persona sin RNC / sin NCF.
|
||||
# Diferencias con el 43:
|
||||
# - Lleva bloque Comprador = datos del PROVEEDOR (cedula y nombre),
|
||||
# se pasan en documento_encabezado.rncComprador / razonSocialComprador.
|
||||
# - Puede llevar ITBIS (IndicadorFacturacion 1/2/3 por linea, 4 = exento).
|
||||
# - Lleva retenciones por linea y totales (ITBIS retenido e ISR retenido).
|
||||
# ret41 va alineado por indice con documento_Detalle (linea k <-> ret41[k]).
|
||||
# Los totales gravado/exento/ITBIS se toman de documento_encabezado tal como
|
||||
# cpprmt006 ya los llena para generafile. MontoTotal = antes de retenciones.
|
||||
# Confirmar orden/obligatoriedad de tags contra el XSD e-CF 41 en TesteCF.
|
||||
#-----------------------------------------------------------------------------
|
||||
FUNCTION generafile41(documento_encabezado, documento_Detalle, ret41, xsucid, xbodega, xoperacion)
|
||||
|
||||
DEFINE documento_encabezado RECORD
|
||||
tipoEcf, eNcf, indicadorMontoGravado, tipoPago, fechaLimitePago, FormadePAgo, montoPago, FechaVencimientoSecuencia,
|
||||
rncEmisor, razonSocialEmisor, direccionEmisor, Municipio, Provincia, telefonoEmisor,
|
||||
correoEmisor, webSite, codigoVendedor, numeroFacturaInterna, numeroPedidoInterno,
|
||||
fechaEmision, rncComprador, razonSocialComprador, montoGravadoTotal, montoGravadoI1,
|
||||
montoGravadoI2, montoGravadoI3, montoExento, itbis1, itbis2, itbis3, totalItbis, totalItbis1,
|
||||
totalItbis2, totalItbis3, montoTotal, tipoIngreso, terminoPago, TipoCuentaPago, NumeroCuentaPago, IdentificadorExtranjero,
|
||||
contactoComprador, CorreoComprador, DireccionComprador, MunicipioComprador, ProvinciaComprador, FechaEntrega, FechaOrdenCompra,
|
||||
NumeroOrdenCompra, CodigoInternoComprador,
|
||||
nombreFile, IndicadorNotaCredito, NCFmodificado, codigoModificacion, fechaNCFmodificado STRING
|
||||
END RECORD,
|
||||
documento_Detalle DYNAMIC ARRAY OF RECORD
|
||||
indicadorFacturacion, nombreItem, indicadorBienoServicio, cantidadItem, precioUnitarioItem,
|
||||
montoItem STRING,
|
||||
DescripcionItem VARCHAR(1000),
|
||||
NumeroLinea INT
|
||||
END RECORD,
|
||||
ret41 DYNAMIC ARRAY OF RECORD
|
||||
itbisRetenido DECIMAL(12,2),
|
||||
isrRetenido DECIMAL(12,2)
|
||||
END RECORD,
|
||||
xoperacion VARCHAR(30),
|
||||
xsucid, xbodega INT
|
||||
|
||||
DEFINE archivo, respuesta, codigo, phash, chFirma STRING,
|
||||
a om.SaxAttributes,
|
||||
fechaFirma DATETIME YEAR TO SECOND,
|
||||
k, linea INTEGER,
|
||||
itbisRet, isrRet, totItbisRet, totIsrRet DECIMAL(14,2),
|
||||
xamb STRING
|
||||
|
||||
-- En el 41 el proveedor (cedula y nombre) es obligatorio
|
||||
IF documento_encabezado.rncComprador IS NULL
|
||||
OR documento_encabezado.razonSocialComprador IS NULL THEN
|
||||
RETURN NULL, "El 41 requiere cedula y nombre del proveedor", NULL, "0"
|
||||
END IF
|
||||
|
||||
LET archivo = fgl_getenv("RUTA_XML_ORIGINALES"), documento_encabezado.nombreFile
|
||||
TRY
|
||||
LET wcp = om.XmlWriter.createFileWriter(archivo)
|
||||
CATCH
|
||||
RETURN NULL, "No se pudo crear el XML del 41 (RUTA_XML_ORIGINALES)", NULL, "0"
|
||||
END TRY
|
||||
IF wcp IS NULL THEN
|
||||
RETURN NULL, "No se pudo crear el XML del 41 (RUTA_XML_ORIGINALES)", NULL, "0"
|
||||
END IF
|
||||
|
||||
LET a = om.SaxAttributes.create()
|
||||
CALL wcp.startDocument()
|
||||
CALL a.addAttribute("xmlns:xs", "http://www.w3.org/2001/XMLSchema")
|
||||
CALL wcp.startElement("ECF", a)
|
||||
CALL a.clear()
|
||||
|
||||
#-------------------- Encabezado --------------------
|
||||
CALL wcp.startElement("Encabezado", NULL)
|
||||
CALL tagcp("Version", "1.0")
|
||||
|
||||
-- IdDoc (el 41 NO lleva TipoIngresos)
|
||||
CALL wcp.startElement("IdDoc", NULL)
|
||||
CALL tagcp("TipoeCF", "41")
|
||||
CALL tagcp("eNCF", documento_encabezado.eNcf CLIPPED)
|
||||
IF documento_encabezado.FechaVencimientoSecuencia IS NOT NULL THEN
|
||||
CALL tagcp("FechaVencimientoSecuencia", documento_encabezado.FechaVencimientoSecuencia)
|
||||
END IF
|
||||
IF montocp(documento_encabezado.montoGravadoTotal) > 0 THEN
|
||||
CALL tagcp("IndicadorMontoGravado", NVL(documento_encabezado.indicadorMontoGravado, "0"))
|
||||
END IF
|
||||
CALL tagcp("TipoPago", NVL(documento_encabezado.tipoPago, "1"))
|
||||
IF documento_encabezado.tipoPago = "2" AND documento_encabezado.fechaLimitePago IS NOT NULL THEN
|
||||
CALL tagcp("FechaLimitePago", documento_encabezado.fechaLimitePago)
|
||||
END IF
|
||||
CALL wcp.startElement("TablaFormasPago", NULL)
|
||||
CALL wcp.startElement("FormaDePago", NULL)
|
||||
CALL tagcp("FormaPago", NVL(documento_encabezado.FormadePAgo, "1"))
|
||||
CALL tagcp("MontoPago", documento_encabezado.montoTotal)
|
||||
CALL wcp.endElement("FormaDePago")
|
||||
CALL wcp.endElement("TablaFormasPago")
|
||||
CALL wcp.endElement("IdDoc")
|
||||
|
||||
-- Emisor (igual que el 43)
|
||||
CALL wcp.startElement("Emisor", NULL)
|
||||
CALL tagcp("RNCEmisor", documento_encabezado.rncEmisor)
|
||||
CALL tagcp("RazonSocialEmisor", cortacp(documento_encabezado.razonSocialEmisor, 150))
|
||||
CALL tagcp("NombreComercial", cortacp(documento_encabezado.razonSocialEmisor, 150))
|
||||
CALL tagcp("DireccionEmisor", cortacp(documento_encabezado.direccionEmisor, 100))
|
||||
IF codigo6cp(documento_encabezado.Municipio) IS NOT NULL THEN
|
||||
CALL tagcp("Municipio", codigo6cp(documento_encabezado.Municipio))
|
||||
END IF
|
||||
IF codigo6cp(documento_encabezado.Provincia) IS NOT NULL THEN
|
||||
CALL tagcp("Provincia", codigo6cp(documento_encabezado.Provincia))
|
||||
END IF
|
||||
IF documento_encabezado.correoEmisor IS NOT NULL THEN
|
||||
CALL tagcp("CorreoEmisor", documento_encabezado.correoEmisor)
|
||||
END IF
|
||||
IF documento_encabezado.numeroFacturaInterna IS NOT NULL THEN
|
||||
CALL tagcp("NumeroFacturaInterna", documento_encabezado.numeroFacturaInterna)
|
||||
END IF
|
||||
CALL tagcp("FechaEmision", documento_encabezado.fechaEmision)
|
||||
CALL wcp.endElement("Emisor")
|
||||
|
||||
-- Comprador = el PROVEEDOR al que se le compra
|
||||
CALL wcp.startElement("Comprador", NULL)
|
||||
CALL tagcp("RNCComprador", documento_encabezado.rncComprador CLIPPED)
|
||||
CALL tagcp("RazonSocialComprador", cortacp(documento_encabezado.razonSocialComprador, 150))
|
||||
CALL wcp.endElement("Comprador")
|
||||
|
||||
-- Totales de retencion (se suman de las lineas que se van a enviar)
|
||||
LET totItbisRet = 0
|
||||
LET totIsrRet = 0
|
||||
FOR k = 1 TO documento_Detalle.getLength()
|
||||
IF documento_Detalle[k].nombreItem IS NULL
|
||||
AND documento_Detalle[k].DescripcionItem IS NULL THEN
|
||||
CONTINUE FOR
|
||||
END IF
|
||||
IF k <= ret41.getLength() THEN
|
||||
LET totItbisRet = totItbisRet + NVL(ret41[k].itbisRetenido, 0)
|
||||
LET totIsrRet = totIsrRet + NVL(ret41[k].isrRetenido, 0)
|
||||
END IF
|
||||
END FOR
|
||||
|
||||
-- Totales (solo se escriben los montos que aplican)
|
||||
CALL wcp.startElement("Totales", NULL)
|
||||
CALL tagcpmonto("MontoGravadoTotal", documento_encabezado.montoGravadoTotal)
|
||||
CALL tagcpmonto("MontoGravadoI1", documento_encabezado.montoGravadoI1)
|
||||
CALL tagcpmonto("MontoGravadoI2", documento_encabezado.montoGravadoI2)
|
||||
CALL tagcpmonto("MontoGravadoI3", documento_encabezado.montoGravadoI3)
|
||||
CALL tagcpmonto("MontoExento", documento_encabezado.montoExento)
|
||||
IF montocp(documento_encabezado.montoGravadoI1) > 0 THEN
|
||||
CALL tagcp("ITBIS1", NVL(documento_encabezado.itbis1, "18"))
|
||||
END IF
|
||||
IF montocp(documento_encabezado.montoGravadoI2) > 0 THEN
|
||||
CALL tagcp("ITBIS2", NVL(documento_encabezado.itbis2, "16"))
|
||||
END IF
|
||||
IF montocp(documento_encabezado.montoGravadoI3) > 0 THEN
|
||||
CALL tagcp("ITBIS3", "0")
|
||||
END IF
|
||||
IF montocp(documento_encabezado.montoGravadoTotal) > 0 THEN
|
||||
CALL tagcp("TotalITBIS", NVL(documento_encabezado.totalItbis, "0.00"))
|
||||
CALL tagcpmonto("TotalITBIS1", documento_encabezado.totalItbis1)
|
||||
CALL tagcpmonto("TotalITBIS2", documento_encabezado.totalItbis2)
|
||||
IF montocp(documento_encabezado.montoGravadoI3) > 0 THEN
|
||||
CALL tagcp("TotalITBIS3", "0.00")
|
||||
END IF
|
||||
END IF
|
||||
CALL tagcp("MontoTotal", documento_encabezado.montoTotal)
|
||||
CALL tagcp("TotalITBISRetenido", totItbisRet USING "<<<<<<<<<<<<&.&&")
|
||||
CALL tagcp("TotalISRRetencion", totIsrRet USING "<<<<<<<<<<<<&.&&")
|
||||
CALL wcp.endElement("Totales")
|
||||
|
||||
CALL wcp.endElement("Encabezado")
|
||||
|
||||
#-------------------- DetallesItems --------------------
|
||||
CALL wcp.startElement("DetallesItems", NULL)
|
||||
LET linea = 0
|
||||
FOR k = 1 TO documento_Detalle.getLength()
|
||||
IF documento_Detalle[k].nombreItem IS NULL
|
||||
AND documento_Detalle[k].DescripcionItem IS NULL THEN
|
||||
CONTINUE FOR
|
||||
END IF
|
||||
LET linea = linea + 1
|
||||
LET itbisRet = 0
|
||||
LET isrRet = 0
|
||||
IF k <= ret41.getLength() THEN
|
||||
LET itbisRet = NVL(ret41[k].itbisRetenido, 0)
|
||||
LET isrRet = NVL(ret41[k].isrRetenido, 0)
|
||||
END IF
|
||||
CALL wcp.startElement("Item", NULL)
|
||||
CALL tagcp("NumeroLinea", linea USING "<<<<")
|
||||
CALL tagcp("IndicadorFacturacion", NVL(documento_Detalle[k].indicadorFacturacion, "4"))
|
||||
-- Retencion: en el 41 la empresa actua como agente de retencion
|
||||
CALL wcp.startElement("Retencion", NULL)
|
||||
CALL tagcp("IndicadorAgenteRetencionoPercepcion", "1")
|
||||
CALL tagcp("MontoITBISRetenido", itbisRet USING "<<<<<<<<<<<<&.&&")
|
||||
CALL tagcp("MontoISRRetenido", isrRet USING "<<<<<<<<<<<<&.&&")
|
||||
CALL wcp.endElement("Retencion")
|
||||
CALL tagcp("NombreItem",
|
||||
NVL(documento_Detalle[k].nombreItem, documento_Detalle[k].DescripcionItem))
|
||||
CALL tagcp("IndicadorBienoServicio", NVL(documento_Detalle[k].indicadorBienoServicio, "2"))
|
||||
CALL tagcp("CantidadItem", NVL(documento_Detalle[k].cantidadItem, "1"))
|
||||
CALL tagcp("PrecioUnitarioItem", documento_Detalle[k].precioUnitarioItem)
|
||||
CALL tagcp("MontoItem", documento_Detalle[k].montoItem)
|
||||
CALL wcp.endElement("Item")
|
||||
END FOR
|
||||
CALL wcp.endElement("DetallesItems")
|
||||
|
||||
#-------------------- Firma --------------------
|
||||
LET fechaFirma = CURRENT
|
||||
LET chFirma = util.Datetime.format(fechaFirma, "%d-%m-%Y %H:%M:%S")
|
||||
CALL tagcp("FechaHoraFirma", chFirma)
|
||||
|
||||
CALL wcp.endElement("ECF")
|
||||
CALL wcp.endDocument()
|
||||
|
||||
#-------------------- Firmar y enviar (igual que el 43) --------------------
|
||||
-- ambiente DGII de la empresa (companias.ambienteDgii, funcion ambiente())
|
||||
LET xamb = ambiente()
|
||||
LET xamb = xamb.trim()
|
||||
IF xamb IS NULL OR (xamb <> "test" AND xamb <> "certificacion" AND xamb <> "produccion") THEN
|
||||
RETURN NULL, "companias.ambienteDgii debe ser test, certificacion o produccion. No se envio a la DGII.", fechaFirma, "0"
|
||||
END IF
|
||||
CALL procesoEncf(archivo, xamb, "normal", xsucid, xbodega, xoperacion, ' ')
|
||||
RETURNING phash, respuesta, codigo
|
||||
|
||||
RETURN phash, respuesta, fechaFirma, codigo
|
||||
END FUNCTION
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Convierte un monto STRING a decimal (NULL o vacio = 0)
|
||||
FUNCTION montocp(valor STRING) RETURNS DECIMAL(14,2)
|
||||
DEFINE n DECIMAL(14,2)
|
||||
LET n = NULL
|
||||
TRY
|
||||
LET n = valor
|
||||
CATCH
|
||||
LET n = NULL
|
||||
END TRY
|
||||
RETURN NVL(n, 0)
|
||||
END FUNCTION
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Recorta un texto al largo maximo que permite el XSD
|
||||
FUNCTION cortacp(valor STRING, largo INTEGER) RETURNS STRING
|
||||
LET valor = valor.trim()
|
||||
IF valor.getLength() > largo THEN
|
||||
LET valor = valor.subString(1, largo)
|
||||
END IF
|
||||
RETURN valor.trim()
|
||||
END FUNCTION
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Provincia / Municipio DGII: codigo de 6 digitos con ceros a la izquierda
|
||||
# (10100 -> 010100). NULL o 0 = no se envia.
|
||||
FUNCTION codigo6cp(valor STRING) RETURNS STRING
|
||||
DEFINE n INTEGER
|
||||
LET n = NULL
|
||||
TRY
|
||||
LET n = valor
|
||||
CATCH
|
||||
LET n = NULL
|
||||
END TRY
|
||||
IF n IS NULL OR n = 0 THEN
|
||||
RETURN NULL
|
||||
END IF
|
||||
RETURN n USING "&&&&&&"
|
||||
END FUNCTION
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Escribe el tag solo si el monto es mayor que cero
|
||||
FUNCTION tagcpmonto(nombre STRING, valor STRING)
|
||||
IF montocp(valor) > 0 THEN
|
||||
CALL tagcp(nombre, valor)
|
||||
END IF
|
||||
END FUNCTION
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
FUNCTION tagcp(nombre STRING, valor STRING)
|
||||
CALL wcp.startElement(nombre, NULL)
|
||||
CALL wcp.characters(valor)
|
||||
CALL wcp.endElement(nombre)
|
||||
END FUNCTION
|
||||
Reference in New Issue
Block a user