*&---------------------------------------------------------------------* *& Report ZMIKNE_CATALOGO_ENVIO *&---------------------------------------------------------------------* *& MIKNE × Higasket — Envío del catálogo de cuentas al middleware México *& Layout: Interaccion_Campos_Middleware_Mexico V1.1 (27/10/2026) *& 向墨西哥中间件发送会计科目表 (基于 V1.1 字段规范) *& *& Flujo (igual que ZMIKNE_HIGASKET_TEST ya probado): *& 1. POST /api/login → obtiene token Bearer (24 h) *& 2. POST /api/catalogo → envía encabezado + partidas *& 3. Lee E_CODE ('S'/'E'), E_MSG y FOLIO de la respuesta *& *& Origen (SAP S/4 HANA) / 数据来源: *& - Cuentas: todas las de SKB1 de P_BUKRS no marcadas para borrado *& (incluye las técnicas y transitorias: si tienen saldo o movimiento, *& el SAT exige que estén en el catálogo). Descripción: SKAT-TXT50. *& 公司代码下全部未删除科目(含技术/过渡科目),描述取 SKAT-TXT50。 *& - Código agrupador SAT: posición de la cuenta en la estructura de *& balance P_VERSN (EAMX) — FAGL_011ZC-ERGSL. *& SAT 分组代码:取自报表结构 P_VERSN(EAMX)的项目。 *& - Naturaleza: por código agrupador (2xx, 3xx, 4xx acreedoras; 171, *& 172 y 183 acreedoras; 402 deudora). *& - S/4 no maneja niveles: todas van como nivel 2 sin SubCtaDe. El *& middleware genera las cuentas de mayor (nivel 1) por código *& agrupador y aplica las equivalencias validadas por el contador. *& S/4 无科目级次:全部按 2 级发送,由中间件按 SAT 分组代码生成 1 级科目。 *& Si alguna cuenta no tiene código agrupador en P_VERSN, el programa *& las lista y no envía nada (el SAT rechaza cuentas sin código). *& 若有科目在 P_VERSN 中没有分组代码,程序列出这些科目并停止发送。 *&---------------------------------------------------------------------* REPORT zmikne_catalogo_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 ------------------------------------ TYPES: BEGIN OF ty_partida, index TYPE c LENGTH 10, figlmx_code TYPE c LENGTH 20, figlmx_acctno TYPE c LENGTH 20, figlmx_acctdesc TYPE c LENGTH 50, figlmx_subacct TYPE c LENGTH 20, figlmx_level TYPE c LENGTH 2, figlmx_nature TYPE c LENGTH 1, 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_versn TYPE fagl_011zc-versn DEFAULT 'EAMX', " estructura con códigos SAT p_spras TYPE spras DEFAULT 'S'. " idioma de la descripción START-OF-SELECTION. PERFORM obtener_catalogo. IF gt_partidas IS INITIAL. RETURN. ENDIF. PERFORM login. IF gv_token IS INITIAL. WRITE: / '登录失败 / LOGIN FALLÓ'. RETURN. ENDIF. PERFORM enviar_catalogo. *&---------------------------------------------------------------------* *& Cuentas de P_BUKRS con su código agrupador SAT y naturaleza *& 公司代码科目及其 SAT 分组代码和借贷性质 *&---------------------------------------------------------------------* FORM obtener_catalogo. DATA: lv_ktopl TYPE ktopl, lv_idx TYPE i, lv_faltan TYPE i. SELECT SINGLE ktopl FROM t001 WHERE bukrs = @p_bukrs INTO @lv_ktopl. IF sy-subrc <> 0. WRITE: / 'Sociedad no existe / 公司代码不存在:', p_bukrs. RETURN. ENDIF. 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. 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. lv_faltan += 1. WRITE: / 'Sin código agrupador en / 无 SAT 分组代码:', p_versn, ls_cta-saknr, ls_cta-txt50. CONTINUE. ENDIF. lv_idx += 1. ls-index = |{ lv_idx }|. 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 }| ). ls-figlmx_subacct = ''. ls-figlmx_level = '2'. PERFORM naturaleza USING ls-figlmx_code CHANGING ls-figlmx_nature. APPEND ls TO gt_partidas. ENDLOOP. IF lv_faltan > 0. WRITE: / lv_faltan, 'cuenta(s) sin código agrupador: no se envía. Complete', p_versn. WRITE: / '有科目缺少 SAT 分组代码,未发送。请补全报表结构。'. CLEAR gt_partidas. ENDIF. 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 (el middleware aplica las excepciones *& validadas por el contador en config/cuentas-sat.json) *& 按分组代码确定借贷性质 *&---------------------------------------------------------------------* 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'. " depreciación / deterioro / amortización acumulada ELSEIF lv_mayor = '402'. cv_nat = 'D'. " devoluciones y descuentos sobre ingresos 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/catalogo *&---------------------------------------------------------------------* FORM enviar_catalogo. DATA lv_partidas TYPE string. LOOP AT gt_partidas INTO DATA(ls). DATA(lv_p) = |\{"INDEX":"{ ls-index }","FIGLMX_CODE":"{ ls-figlmx_code }",| && |"FIGLMX_ACCTNO":"{ ls-figlmx_acctno }",| && |"FIGLMX_ACCTDESC":"{ escape( val = ls-figlmx_acctdesc format = cl_abap_format=>e_json_string ) }",| && |"FIGLMX_SUBACCT":"{ ls-figlmx_subacct }",| && |"FIGLMX_LEVEL":"{ ls-figlmx_level }",| && |"FIGLMX_NATURE":"{ ls-figlmx_nature }"\}|. IF lv_partidas IS INITIAL. lv_partidas = lv_p. ELSE. lv_partidas = |{ lv_partidas },{ lv_p }|. ENDIF. ENDLOOP. gv_json = |\{"encabezado":\{| && |"FIGLMX_VERSION":"1.3",| && |"FIGLMX_TAXNO":"HME151105EJ3",| && |"FIGLMX_MONTH":"{ p_mes }",| && |"FIGLMX_YEAR":"{ p_anio }"\},| && |"partidas":[{ lv_partidas }]\}|. PERFORM http_post USING '/api/catalogo' gv_json gv_token CHANGING gv_status gv_resp. DATA: lv_ecode TYPE string, lv_emsg TYPE string, lv_folio 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. WRITE: / 'HTTP 状态码:', gv_status. WRITE: / 'E_CODE:', lv_ecode, 'E_MSG:', lv_emsg. IF lv_ecode = 'S'. WRITE: / '发送成功 / Envío correcto. Folio:', lv_folio. 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.