#include "hbclass.ch" REQUEST HB_CODEPAGE_ESWIN PROCEDURE Main() LOCAL cOutDir := hb_DirBase() + "csv_output" LOCAL cSingle := cOutDir + hb_ps() + "detalle.csv" LOCAL cBom := cOutDir + hb_ps() + "bom.csv" LOCAL cLarge := cOutDir + hb_ps() + "large.csv" LOCAL cSemantic := cOutDir + hb_ps() + "semantic_win.csv" LOCAL hIR := TestIR() LOCAL oCSV := TExportCSV():New():Create() LOCAL cContent LOCAL cExpectedHeader := hb_StrToUTF8( "Descripci" + Chr( 243 ) + "n", "ESWIN" ) LOCAL cExpectedSemanticHeader := hb_StrToUTF8( "Descripci" + Chr( 243 ) + "n art" + Chr( 237 ) + "culo n" + Chr( 186 ) + " (" + Chr( 128 ) + ")", "ESWIN" ) LOCAL cExpectedUnicode := hb_StrToUTF8( "Ni" + Chr( 241 ) + "o " + Chr( 186 ) + " " + Chr( 170 ) + " " + Chr( 128 ), "ESWIN" ) LOCAL nFailures := 0 LOCAL nProgress := 0 LOCAL nLines LOCAL oEngine LOCAL oDocument LOCAL aSemanticTables hb_cdpSelect( "ESWIN" ) IF ! hb_DirExists( cOutDir ) hb_DirBuild( cOutDir ) ENDIF DeleteIfExists( cSingle ) DeleteIfExists( cBom ) DeleteIfExists( cLarge ) DeleteIfExists( cSemantic ) DeleteIfExists( cOutDir + hb_ps() + "informe_Detalle_de_ventas.csv" ) DeleteIfExists( cOutDir + hb_ps() + "informe_Resumen_IVA.csv" ) oCSV:lOverwrite := .T. nFailures += AssertTrue( "exporta una tabla", oCSV:ExportTable( hIR, "detail", cSingle ), oCSV:GetLastError() ) cContent := MemoRead( cSingle ) nFailures += AssertTrue( "UTF-8 sin BOM", Left( cContent, 3 ) != Chr( 239 ) + Chr( 187 ) + Chr( 191 ), HexPrefix( cContent ) ) nFailures += AssertTrue( "cabecera con acento en UTF-8", cExpectedHeader $ cContent, cContent ) nFailures += AssertTrue( "ñ, º, ª y euro en UTF-8", cExpectedUnicode $ cContent, cContent ) nFailures += AssertTrue( "separador y decimal", "ART001;10;2,5" $ cContent, cContent ) nFailures += AssertTrue( "campo con separador entre comillas", '"Art; especial"' $ cContent, cContent ) nFailures += AssertTrue( "comillas internas duplicadas", '"Producto ""Especial"""' $ cContent, cContent ) nFailures += AssertTrue( "saltos de linea entre comillas", '"Linea 1' + Chr( 13 ) + Chr( 10 ) + 'Linea 2"' $ cContent, cContent ) nFailures += AssertTrue( "espacios extremos entre comillas", '" conserva "' $ cContent, cContent ) nFailures += AssertTrue( "fecha", "04/08/2026" $ cContent, cContent ) nFailures += AssertTrue( "fecha y hora", "04/08/2026 15:30:45" $ cContent, cContent ) nFailures += AssertTrue( "logicos", ";SI;NO;" $ cContent, cContent ) nFailures += AssertTrue( "NIL y cadena vacia", ";NO;;" $ cContent, cContent ) nFailures += AssertTrue( "CRLF", Chr( 13 ) + Chr( 10 ) $ cContent, "" ) nLines := CountOccurrences( cContent, "ART00" ) nFailures += AssertEqual( "tabla multipagina no duplica registros", nLines, 5 ) oCSV:lUtf8Bom := .T. nFailures += AssertTrue( "BOM opcional", oCSV:ExportTable( hIR, "summary", cBom ), oCSV:GetLastError() ) nFailures += AssertTrue( "BOM EF BB BF cuando se solicita", Left( MemoRead( cBom ), 3 ) == Chr( 239 ) + Chr( 187 ) + Chr( 191 ), HexPrefix( MemoRead( cBom ) ) ) oCSV:lUtf8Bom := .F. oCSV:lOverwrite := .F. nFailures += AssertTrue( "evita sobrescritura sin autorizacion", ! oCSV:ExportTable( hIR, "detail", cSingle ), oCSV:GetLastError() ) nFailures += AssertTrue( "exporta todas las tablas", oCSV:ExportAllTables( hIR, cOutDir, "informe" ), oCSV:GetLastError() ) nFailures += AssertTrue( "un archivo seguro por tabla", File( cOutDir + hb_ps() + "informe_Detalle_de_ventas.csv" ) .AND. File( cOutDir + hb_ps() + "informe_Resumen_IVA.csv" ), "" ) nFailures += AssertTrue( "informe sin tablas", ! oCSV:ExportAllTables( { "aDataTables" => {} }, cOutDir, "vacio" ), oCSV:GetLastError() ) oCSV:lOverwrite := .T. oCSV:SetProgressCallback( {| nCurrent, nTotal, nTable, nTables | nProgress := nCurrent, nTable + nTables >= 2 } ) nFailures += AssertTrue( "informe grande secuencial", oCSV:ExportTable( LargeIR( 20000 ), "large", cLarge ), oCSV:GetLastError() ) nFailures += AssertEqual( "progreso por registro", nProgress, 20000 ) nFailures += AssertTrue( "archivo grande creado", hb_FSize( cLarge ) > 200000, LTrim( Str( hb_FSize( cLarge ) ) ) ) oEngine := SemanticEngine() oDocument := oEngine:RunEngine() aSemanticTables := iif( HB_ISOBJECT( oDocument ), oDocument:GetDataTables(), {} ) nFailures += AssertTrue( "el caso semantico genera varias paginas", HB_ISOBJECT( oDocument ) .AND. oDocument:TotalPaginas() > 1, "" ) nFailures += AssertEqual( "IR contiene una tabla semantica", Len( aSemanticTables ), 1 ) nFailures += AssertEqual( "tabla semantica interna declarada UTF-8", aSemanticTables[ 1 ][ "cEncoding" ], "UTF-8" ) nFailures += AssertEqual( "titulo visible conservado", aSemanticTables[ 1 ][ "aColumns" ][ 1 ][ "cTitle" ], cExpectedSemanticHeader ) nFailures += AssertEqual( "campo conservado", aSemanticTables[ 1 ][ "aColumns" ][ 1 ][ "cField" ], "TEXTO" ) nFailures += AssertEqual( "filas reales sin repeticion de paginas", Len( aSemanticTables[ 1 ][ "aRows" ] ), 12 ) nFailures += AssertEqual( "formula evaluada en IR", aSemanticTables[ 1 ][ "aRows" ][ 12 ][ 1 ], "Linea 12" ) oCSV:lUtf8Bom := .F. oCSV:lOverwrite := .T. nFailures += AssertTrue( "exporta IR semantico del flujo Xailer", oCSV:ExportTable( oDocument, "semantic_table", cSemantic ), oCSV:GetLastError() ) cContent := MemoRead( cSemantic ) nFailures += AssertTrue( "IR Xailer exportado sin BOM", Left( cContent, 3 ) != Chr( 239 ) + Chr( 187 ) + Chr( 191 ), HexPrefix( cContent ) ) nFailures += AssertTrue( "titulo Windows compatible con LibreOffice en UTF-8", cExpectedSemanticHeader $ cContent, HexPrefix( cContent ) ) nFailures += AssertTrue( "IR Xailer no recodifica dos veces", !( hb_StrToUTF8( cExpectedSemanticHeader, "ESWIN" ) $ cContent ), cContent ) oEngine:End() oEngine := NIL oDocument := NIL oCSV:End() oCSV := NIL IF nFailures == 0 hb_MemoWrit( hb_DirBase() + "test_export_csv_result.log", "OK" ) QOut( "OK: TExportCSV tipos, UTF-8, escapado, tablas multiples y volumen" ) ErrorLevel( 0 ) ELSE hb_MemoWrit( hb_DirBase() + "test_export_csv_result.log", "ERROR:" + hb_NToS( nFailures ) ) QOut( "ERROR: fallos=" + hb_NToS( nFailures ) ) ErrorLevel( 1 ) ENDIF RETURN STATIC FUNCTION SemanticEngine() LOCAL oEngine := TLLEngine():New():Create() LOCAL nIndex LOCAL cHeader := hb_StrToUTF8( "Descripci" + Chr( 243 ) + "n art" + Chr( 237 ) + "culo n" + Chr( 186 ) + " (" + Chr( 128 ) + ")", "ESWIN" ) LOCAL hTemplate := { ; "cType" => "TDocument", "cFormatVersion" => "1.0", "cTemplateName" => "CSV semantic", ; "nPaperWidth" => 80, "nPaperHeight" => 30, ; "aObjects" => { { ; "cType" => "TTable", "cId" => "semantic_table", "cName" => "Datos", ; "nLeft" => 2.0, "nTop" => 2.0, "nWidth" => 76.0, "nHeight" => 26.0, ; "hSections" => { ; "header" => { "aLines" => { { "nHeight" => 3.0, "aCells" => { { "nWidth" => 76.0, ; "hContent" => { "cType" => "TText", "hObject" => { "cText" => Chr( 34 ) + cHeader + Chr( 34 ) } } } } } } }, ; "detail" => { "aLines" => { { "nHeight" => 3.0, "aCells" => { { "nWidth" => 76.0, ; "hContent" => { "cType" => "TText", "hObject" => { "cText" => "LINEAS.TEXTO" } } } } } } }, ; "footer" => { "aLines" => {} }, "groupHeader" => { "aLines" => {} }, "groupFooter" => { "aLines" => {} } } ; } } ; } // Reproduce el flujo real de Xailer: la salida externa es ESWIN, pero el // documento renderizable y sus tablas semanticas siguen siendo UTF-8. oEngine:SetEncodingWIN() oEngine:SetPlantillaContent( hb_jsonEncode( hTemplate, .T. ) ) oEngine:OpenJob() FOR nIndex := 1 TO 12 oEngine:SendDetalle( "LINEAS", { "TEXTO" => "Linea " + LTrim( Str( nIndex ) ) } ) NEXT RETURN oEngine STATIC FUNCTION TestIR() LOCAL cUnicode := "Ni" + Chr( 241 ) + "o " + Chr( 186 ) + " " + Chr( 170 ) + " " + Chr( 128 ) LOCAL aColumns := { ; Column( "Codigo", "codigo", "C" ), Column( "Cantidad", "cantidad", "N" ), Column( "Precio", "precio", "N" ), ; Column( "Descripci" + Chr( 243 ) + "n", "descripcion", "C" ), Column( "Fecha", "fecha", "D" ), ; Column( "Fecha hora", "fecha_hora", "T" ), Column( "Activo", "activo", "L" ), Column( "Inactivo", "inactivo", "L" ), ; Column( "Nulo", "nulo", "U" ), Column( "Vacio", "vacio", "C" ) ; } LOCAL aRows := { ; { "ART001", 10, 2.5, cUnicode, SToD( "20260804" ), hb_SToT( "20260804153045" ), .T., .F., NIL, "" }, ; { "ART002", 3, 12.75, "Art; especial", SToD( "20260804" ), hb_SToT( "20260804153045" ), .T., .F., NIL, "" }, ; { "ART003", 2, 5.0, 'Producto "Especial"', SToD( "20260804" ), hb_SToT( "20260804153045" ), .T., .F., NIL, "" }, ; { "ART004", 1, 1.0, "Linea 1" + Chr( 13 ) + Chr( 10 ) + "Linea 2", SToD( "20260804" ), hb_SToT( "20260804153045" ), .T., .F., NIL, "" }, ; { "ART005", 1, 1.0, " conserva ", SToD( "20260804" ), hb_SToT( "20260804153045" ), .T., .F., NIL, "" } ; } RETURN { ; "paginas" => { { "objetos" => {} }, { "objetos" => {} } }, ; "aDataTables" => { ; { "cId" => "detail", "cName" => "Detalle de ventas", "aColumns" => aColumns, "aRows" => aRows, "nRecordCount" => Len( aRows ) }, ; { "cId" => "summary", "cName" => "Resumen IVA", "aColumns" => { Column( "Base", "base", "N" ) }, "aRows" => { { 123.45 } }, "nRecordCount" => 1 } ; } ; } STATIC FUNCTION LargeIR( nRows ) LOCAL aRows := Array( nRows ) LOCAL nIndex FOR nIndex := 1 TO nRows aRows[ nIndex ] := { nIndex, "Registro " + LTrim( Str( nIndex ) ), nIndex / 10 } NEXT RETURN { "aDataTables" => { { "cId" => "large", "cName" => "Grande", ; "aColumns" => { Column( "Id", "id", "N" ), Column( "Texto", "texto", "C" ), Column( "Importe", "importe", "N" ) }, ; "aRows" => aRows, "nRecordCount" => nRows } } } STATIC FUNCTION Column( cTitle, cField, cType ) RETURN { "cId" => cField, "cTitle" => cTitle, "cField" => cField, "cType" => cType } STATIC FUNCTION DeleteIfExists( cFile ) IF File( cFile ); FErase( cFile ); ENDIF RETURN NIL STATIC FUNCTION CountOccurrences( cText, cNeedle ) LOCAL nCount := 0 LOCAL nStart := 1 LOCAL nAt DO WHILE ( nAt := At( cNeedle, SubStr( cText, nStart ) ) ) > 0 nCount++ nStart += nAt + Len( cNeedle ) - 1 ENDDO RETURN nCount STATIC FUNCTION HexPrefix( cText ) RETURN hb_NumToHex( Asc( SubStr( cText, 1, 1 ) ), 2 ) + " " + hb_NumToHex( Asc( SubStr( cText, 2, 1 ) ), 2 ) + " " + hb_NumToHex( Asc( SubStr( cText, 3, 1 ) ), 2 ) STATIC FUNCTION AssertTrue( cName, lValue, cDetail ) IF lValue QOut( "OK: " + cName ) RETURN 0 ENDIF QOut( "ERROR: " + cName + iif( Empty( cDetail ), "", " -> " + cDetail ) ) RETURN 1 STATIC FUNCTION AssertEqual( cName, uActual, uExpected ) RETURN AssertTrue( cName, uActual == uExpected, "actual=" + hb_ValToExp( uActual ) + ", esperado=" + hb_ValToExp( uExpected ) )