*&---------------------------------------------------------------------* *& Report ZMIKNE_SALDOS_ENVIO *&---------------------------------------------------------------------* *& MIKNE × Higasket — Envío de saldos de cuentas al middleware México *& Layout: Interaccion_Campos_Middleware_Mexico V1.1 (27/10/2026), *& pestaña "Saldos de cuentas" *& 向墨西哥中间件发送科目余额 (基于 V1.1 字段规范「Saldos de cuentas」) *& *& Flujo (igual que ZMIKNE_CATALOGO_ENVIO): *& 1. POST /api/login → obtiene token Bearer (24 h) *& 2. POST /api/saldos → envía encabezado + partidas *& 3. Lee E_CODE ('S'/'E'), E_MSG y FOLIO de la respuesta *& *& Tipo de envío / 发送类型 (FIGLMX_FILE_TYP): *& N = normal: primer envío del periodo. FIGLMX_MOD_DATE vacía. *& 正常:该期间首次发送,FIGLMX_MOD_DATE 留空。 *& C = complementaria: reemplaza el periodo ya enviado. *& FIGLMX_MOD_DATE obligatoria (fecha de la modificación). *& 补充:替换已发送的期间,FIGLMX_MOD_DATE 必填(修改日期)。 *& Si el periodo ya existe y se envía N → HTTP 409, reenviar como C. *& 期间已存在却以 N 发送 → HTTP 409,请改为 C 重新发送。 *& *& Origen (SAP S/4 HANA) / 数据来源: *& - Cuentas, código agrupador y naturaleza: igual que *& ZMIKNE_CATALOGO_ENVIO (SKB1 + SKAT + estructura P_VERSN). Se envían *& todas las cuentas del catálogo, aunque estén en cero. *& 科目、分组代码和借贷性质与 ZMIKNE_CATALOGO_ENVIO 相同,全部科目都发送(含零余额)。 *& - Importes: ACDOCA del ledger P_RLDNR en moneda local (HSL). *& saldo inicial = periodos anteriores del ejercicio, incluido el *& arrastre de saldos (periodo 000) *& debe = suma de partidas S del mes haber = suma de partidas H *& Mes 13 = periodos especiales 013 a 016 (cierre). *& Debe y haber son los TOTALES de cargos y abonos, no el neto. *& 借方/贷方为本月借方合计与贷方合计,不是净额。 *& - Signo: el de SAP (haber y saldos acreedores negativos; *& saldo final = inicial + debe + haber). El middleware los convierte *& al signo del SAT al generar el XML. *& 金额保持 SAP 符号(贷方为负),由中间件转换为 SAT 格式。 *&---------------------------------------------------------------------* REPORT zmikne_saldos_envio. CONSTANTS: gc_host TYPE string VALUE 'https://higasket-ce.mikne.com.mx', gc_usuario TYPE string VALUE 'higasket', gc_password TYPE string VALUE '*** CONFIGURAR ***'. * ---- Estructuras del layout V1.1 (Saldos de cuentas) ---------------- TYPES: BEGIN OF ty_partida, figlmx_code TYPE c LENGTH 10, " Código agrupador SAT figlmx_acctno TYPE c LENGTH 20, " Cuenta de mayor figlmx_acctdesc TYPE c LENGTH 50, " Descripción figlmx_nature TYPE c LENGTH 1, " D / A figlmx_bbalance TYPE p LENGTH 12 DECIMALS 2, " Saldo inicial CURR 22,2 figlmx_debit TYPE p LENGTH 12 DECIMALS 2, " Debe CURR 22,2 figlmx_credit TYPE p LENGTH 12 DECIMALS 2, " Haber CURR 22,2 figlmx_ebalance TYPE p LENGTH 12 DECIMALS 2, " Saldo final CURR 22,2 END OF ty_partida. TYPES: BEGIN OF ty_rango, " estructura de balance → código SAT ergsl TYPE fagl_011zc-ergsl, vonkt TYPE fagl_011zc-vonkt, bilkt TYPE fagl_011zc-bilkt, END OF ty_rango. DATA: gt_partidas TYPE STANDARD TABLE OF ty_partida, gt_rangos TYPE STANDARD TABLE OF ty_rango, gv_token TYPE string, gv_json TYPE string, gv_status TYPE i, gv_resp TYPE string. PARAMETERS: p_bukrs TYPE bukrs DEFAULT '8600', p_mes TYPE numc2 DEFAULT '06', p_anio TYPE numc4 DEFAULT '2026', p_tipo TYPE c LENGTH 1 DEFAULT 'N', " N normal | C complementaria p_fmod TYPE datum, " solo si p_tipo = C p_rldnr TYPE fins_ledger DEFAULT '0L', " ledger p_versn TYPE fagl_011zc-versn DEFAULT 'EAMX', " estructura con códigos SAT p_spras TYPE spras DEFAULT 'S'. " idioma de la descripción AT SELECTION-SCREEN. IF p_tipo <> 'N' AND p_tipo <> 'C'. MESSAGE 'P_TIPO debe ser N o C / 类型必须为 N 或 C' TYPE 'E'. ENDIF. IF p_tipo = 'C' AND p_fmod IS INITIAL. MESSAGE 'Con tipo C indique P_FMOD / 类型 C 需填写修改日期' TYPE 'E'. ENDIF. START-OF-SELECTION. PERFORM obtener_saldos. IF gt_partidas IS INITIAL. WRITE: / '无余额数据 / Sin saldos para el periodo'. RETURN. ENDIF. PERFORM login. IF gv_token IS INITIAL. WRITE: / '登录失败 / LOGIN FALLÓ'. RETURN. ENDIF. PERFORM enviar_saldos. *&---------------------------------------------------------------------* *& Llenar gt_partidas con los saldos del periodo *&---------------------------------------------------------------------* FORM obtener_saldos. TYPES: BEGIN OF ty_mov, racct TYPE racct, poper TYPE poper, drcrk TYPE shkzg, hsl TYPE fins_vhcur12, END OF ty_mov. DATA: lt_mov TYPE STANDARD TABLE OF ty_mov, lv_ktopl TYPE ktopl, lv_desde TYPE poper, lv_hasta TYPE poper. " Mes 1–12 → periodo del mes; mes 13 → periodos especiales 013–016 IF p_mes = '13'. lv_desde = '013'. lv_hasta = '016'. ELSE. lv_desde = |0{ p_mes }|. lv_hasta = lv_desde. ENDIF. SELECT SINGLE ktopl FROM t001 WHERE bukrs = @p_bukrs INTO @lv_ktopl. SELECT b~saknr, t~txt50 FROM skb1 AS b LEFT OUTER JOIN skat AS t ON t~saknr = b~saknr AND t~ktopl = @lv_ktopl AND t~spras = @p_spras WHERE b~bukrs = @p_bukrs AND b~xloeb = @abap_false ORDER BY b~saknr INTO TABLE @DATA(lt_ctas). SELECT ergsl, vonkt, bilkt FROM fagl_011zc WHERE versn = @p_versn AND ktopl = @lv_ktopl INTO TABLE @gt_rangos. " Movimientos acumulados por cuenta, periodo y debe/haber (incluye arrastre 000) SELECT racct, poper, drcrk, SUM( hsl ) AS hsl FROM acdoca WHERE rldnr = @p_rldnr AND rbukrs = @p_bukrs AND gjahr = @p_anio AND poper <= @lv_hasta GROUP BY racct, poper, drcrk INTO TABLE @lt_mov. LOOP AT lt_ctas INTO DATA(ls_cta). DATA(ls) = VALUE ty_partida( ). PERFORM codigo_sat USING ls_cta-saknr CHANGING ls-figlmx_code. IF ls-figlmx_code IS INITIAL. WRITE: / 'Sin código agrupador en / 无 SAT 分组代码:', p_versn, ls_cta-saknr. CONTINUE. ENDIF. ls-figlmx_acctno = |{ ls_cta-saknr ALPHA = OUT }|. ls-figlmx_acctdesc = COND #( WHEN ls_cta-txt50 IS NOT INITIAL THEN ls_cta-txt50 ELSE |Cuenta { ls-figlmx_acctno }| ). PERFORM naturaleza USING ls-figlmx_code CHANGING ls-figlmx_nature. LOOP AT lt_mov INTO DATA(ls_m) WHERE racct = ls_cta-saknr. IF ls_m-poper < lv_desde. ls-figlmx_bbalance = ls-figlmx_bbalance + ls_m-hsl. ELSEIF ls_m-drcrk = 'S'. ls-figlmx_debit = ls-figlmx_debit + ls_m-hsl. ELSE. ls-figlmx_credit = ls-figlmx_credit + ls_m-hsl. " negativo (signo SAP) ENDIF. ENDLOOP. ls-figlmx_ebalance = ls-figlmx_bbalance + ls-figlmx_debit + ls-figlmx_credit. APPEND ls TO gt_partidas. ENDLOOP. ENDFORM. *&---------------------------------------------------------------------* *& Código agrupador SAT de una cuenta: rango de la estructura P_VERSN *& 科目的 SAT 分组代码:报表结构 P_VERSN 中所在的项目 *&---------------------------------------------------------------------* FORM codigo_sat USING iv_saknr TYPE saknr CHANGING cv_code TYPE clike. CLEAR cv_code. LOOP AT gt_rangos INTO DATA(ls_r) WHERE vonkt <= iv_saknr AND bilkt >= iv_saknr. cv_code = condense( ls_r-ergsl ). EXIT. ENDLOOP. ENDFORM. *&---------------------------------------------------------------------* *& Naturaleza por código agrupador (igual que ZMIKNE_CATALOGO_ENVIO) *& 按分组代码确定借贷性质 *&---------------------------------------------------------------------* FORM naturaleza USING iv_code TYPE clike CHANGING cv_nat TYPE clike. DATA(lv_mayor) = substring_before( val = |{ iv_code }.| sub = '.' ). IF lv_mayor = '171' OR lv_mayor = '172' OR lv_mayor = '183'. cv_nat = 'A'. ELSEIF lv_mayor = '402'. cv_nat = 'D'. ELSEIF iv_code(1) CA '234'. cv_nat = 'A'. ELSE. cv_nat = 'D'. ENDIF. ENDFORM. *&---------------------------------------------------------------------* *& 1) POST /api/login → token *&---------------------------------------------------------------------* FORM login. DATA(lv_body) = |\{"usuario":"{ gc_usuario }","password":"{ gc_password }"\}|. PERFORM http_post USING '/api/login' lv_body '' CHANGING gv_status gv_resp. IF gv_status = 200. " extraer "token":"..." de la respuesta FIND REGEX '"token"\s*:\s*"([^"]+)"' IN gv_resp SUBMATCHES gv_token. WRITE: / '登录成功 / Login correcto. Token obtenido.'. ELSE. WRITE: / 'HTTP', gv_status, gv_resp. ENDIF. ENDFORM. *&---------------------------------------------------------------------* *& 2) Armar JSON del layout y POST /api/saldos *&---------------------------------------------------------------------* FORM enviar_saldos. DATA: lt_json TYPE string_table, lv_fmod TYPE string. " Importes con NUMBER = RAW: punto decimal y signo a la izquierda (-1500.00) LOOP AT gt_partidas INTO DATA(ls). APPEND |\{"FIGLMX_CODE":"{ escape( val = condense( ls-figlmx_code ) format = cl_abap_format=>e_json_string ) }",| && |"FIGLMX_ACCTNO":"{ escape( val = condense( ls-figlmx_acctno ) format = cl_abap_format=>e_json_string ) }",| && |"FIGLMX_ACCTDESC":"{ escape( val = ls-figlmx_acctdesc format = cl_abap_format=>e_json_string ) }",| && |"FIGLMX_NATURE":"{ ls-figlmx_nature }",| && |"FIGLMX_BBALANCE":"{ ls-figlmx_bbalance NUMBER = RAW }",| && |"FIGLMX_DEBIT":"{ ls-figlmx_debit NUMBER = RAW }",| && |"FIGLMX_CREDIT":"{ ls-figlmx_credit NUMBER = RAW }",| && |"FIGLMX_EBALANCE":"{ ls-figlmx_ebalance NUMBER = RAW }"\}| TO lt_json. ENDLOOP. IF p_tipo = 'C'. lv_fmod = |{ p_fmod DATE = RAW }|. " AAAAMMDD ENDIF. gv_json = |\{"encabezado":\{| && |"FIGLMX_VERSION":"1.3",| && |"FIGLMX_TAXNO":"HME151105EJ3",| && |"FIGLMX_MONTH":"{ p_mes }",| && |"FIGLMX_YEAR":"{ p_anio }",| && |"FIGLMX_FILE_TYP":"{ p_tipo }",| && |"FIGLMX_MOD_DATE":"{ lv_fmod }"\},| && |"partidas":[{ concat_lines_of( table = lt_json sep = `,` ) }]\}|. PERFORM http_post USING '/api/saldos' gv_json gv_token CHANGING gv_status gv_resp. DATA: lv_ecode TYPE string, lv_emsg TYPE string, lv_folio TYPE string, lv_adv TYPE string. FIND REGEX '"E_CODE"\s*:\s*"([^"]+)"' IN gv_resp SUBMATCHES lv_ecode. FIND REGEX '"E_MSG"\s*:\s*"([^"]+)"' IN gv_resp SUBMATCHES lv_emsg. FIND REGEX '"FOLIO"\s*:\s*"([^"]+)"' IN gv_resp SUBMATCHES lv_folio. FIND REGEX '"totalAdvertencias"\s*:\s*(\d+)' IN gv_resp SUBMATCHES lv_adv. WRITE: / 'HTTP 状态码:', gv_status. WRITE: / 'E_CODE:', lv_ecode, 'E_MSG:', lv_emsg. IF lv_ecode = 'S'. WRITE: / '发送成功 / Envío correcto. Folio:', lv_folio. IF lv_adv IS NOT INITIAL AND lv_adv <> '0'. " Advertencias: no rechazan el envío (cuadre de saldos, cuentas fuera del catálogo) " 警告:不影响接收(余额勾稽、科目不在科目表中) WRITE: / '警告 / Advertencias:', lv_adv, '→', gv_resp. ENDIF. ELSE. WRITE: / '发送失败 / Envío rechazado. Revisar detalle:', gv_resp. ENDIF. ENDFORM. *&---------------------------------------------------------------------* *& Utilería HTTP (mismo patrón validado en la prueba de conectividad) *&---------------------------------------------------------------------* FORM http_post USING iv_ruta TYPE string iv_body TYPE string iv_token TYPE string CHANGING cv_status TYPE i cv_resp TYPE string. DATA lo_http TYPE REF TO if_http_client. cl_http_client=>create_by_url( EXPORTING url = |{ gc_host }{ iv_ruta }| IMPORTING client = lo_http EXCEPTIONS OTHERS = 4 ). IF sy-subrc <> 0. WRITE: / '创建 HTTP 客户端失败'. RETURN. ENDIF. lo_http->request->set_method( if_http_request=>co_request_method_post ). lo_http->request->set_header_field( name = 'Content-Type' value = 'application/json' ). IF iv_token IS NOT INITIAL. lo_http->request->set_header_field( name = 'Authorization' value = |Bearer { iv_token }| ). ENDIF. lo_http->request->set_cdata( iv_body ). lo_http->send( EXCEPTIONS OTHERS = 1 ). IF sy-subrc <> 0. WRITE: / '发送请求失败'. lo_http->close( ). RETURN. ENDIF. lo_http->receive( EXCEPTIONS OTHERS = 1 ). IF sy-subrc <> 0. WRITE: / '接收响应失败'. lo_http->close( ). RETURN. ENDIF. lo_http->response->get_status( IMPORTING code = cv_status ). cv_resp = lo_http->response->get_cdata( ). lo_http->close( ). ENDFORM.