unit uEdicomInvoiceExporter;

interface

uses
  System.SysUtils, System.Classes, System.Variants, Data.DB, FireDAC.Comp.Client,
  Xml.XMLDoc, Xml.XMLIntf, Vcl.Dialogs;

type
  TEEdicomValidationError = class(Exception);

  /// <summary>
  /// Realiza la comprobación de datos críticos de la factura y sus líneas para EDICOM
  /// </summary>
  function ValidarFacturaEdicom(const AFacturaId: string; AConn: TFDConnection; out AErrMessage: string): Boolean;

  /// <summary>
  /// Genera el string XML según la estructura #INVOIC_ECI_VARIAS_LINEAS.xml
  /// </summary>
  function GenerarXmlInvoic(const AFacturaId: string; AConn: TFDConnection): string;

  /// <summary>
  /// Exporta el XML de la factura a un archivo seleccionado por el usuario
  /// </summary>
  procedure ExportarFacturaXmlEdicom(const AFacturaId: string; AConn: TFDConnection);

  /// <summary>
  /// Normaliza el código de departamento para el segmento RFF+API (Buyer_department_id).
  /// En el estándar EDI/EDICOM Gran Consumo (ECI) debe ser X(3) (ej. '0206' -> '206').
  /// </summary>
  function NormalizeDepartmentCode(const ADept: string): string;

implementation

uses
  dmg_Main, uEdicomDesadvExporter;

function ValidarFacturaEdicom(const AFacturaId: string; AConn: TFDConnection; out AErrMessage: string): Boolean;
var
  QryCab, QryDet, QryEmp: TFDQuery;
  LLineCount: Integer;
  LErrors: TStringList;
  LClienteId, LEmpresaId: string;
begin
  Result := False;
  AErrMessage := '';
  LErrors := TStringList.Create;
  QryCab := TFDQuery.Create(nil);
  QryDet := TFDQuery.Create(nil);
  QryEmp := TFDQuery.Create(nil);
  try
    QryCab.Connection := AConn;
    QryDet.Connection := AConn;
    QryEmp.Connection := AConn;

    // 1. Obtener Datos Cabecera Factura y Cliente
    QryCab.SQL.Text :=
      'SELECT f.id, f.serie, f.numero, f.fecha, f.total, f.base_imponible, f.iva, f.referencia, ' +
      '       f.empresa_id, f.cliente_id, f.clientedestino_id, ' +
      '       c.nif AS cliente_nif, c.nombre_fiscal AS cliente_nombre, c.ean AS cliente_gln, ' +
      '       d.ean_facturacion AS destino_ean_facturacion, d.ean_cliente AS destino_ean_cliente, ' +
      '       d.ean_emisor AS destino_ean_emisor, d.ean_receptor AS destino_ean_receptor, ' +
      '       d.departamento AS destino_departamento ' +
      'FROM ge_facturas f ' +
      'LEFT JOIN ge_clientes c ON f.cliente_id = c.id ' +
      'LEFT JOIN ge_clientes_destinos d ON d.id = COALESCE(f.clientedestino_id, (SELECT alb.clientedestino_id FROM ge_albaranes alb WHERE alb.id_factura = f.id AND alb.clientedestino_id IS NOT NULL LIMIT 1)) ' +
      'WHERE f.id = :id';
    QryCab.ParamByName('id').AsString := AFacturaId;
    QryCab.Open;

    if QryCab.IsEmpty then
    begin
      AErrMessage := 'La factura no existe en la base de datos.';
      Exit;
    end;

    LEmpresaId := QryCab.FieldByName('empresa_id').AsString;
    LClienteId := QryCab.FieldByName('cliente_id').AsString;

    // Validar Datos Emisor / Empresa
    QryEmp.SQL.Text :=
      'SELECT e.cif_nif, e.nombre, e.gln AS emp_gln, ed.gln_empresa AS edicom_gln ' +
      'FROM ge_empresas e ' +
      'LEFT JOIN ge_edicom_config ed ON ed.id_empresa = e.id AND ed.activo = 1 ' +
      'WHERE e.id = :emp_id';
    QryEmp.ParamByName('emp_id').AsString := LEmpresaId;
    QryEmp.Open;

    if QryEmp.FieldByName('cif_nif').AsString = '' then
      LErrors.Add('- NIF / CIF de la empresa emisor no está informado.');

    // Validar Datos Cliente (Receptor)
    if Trim(QryCab.FieldByName('cliente_nif').AsString) = '' then
      LErrors.Add('- NIF del cliente comprador no está informado.');

    // Validar Datos Cliente (Receptor)
    if (Trim(QryCab.FieldByName('cliente_gln').AsString) = '') and (Trim(QryCab.FieldByName('cliente_nif').AsString) = '') then
      LErrors.Add('- NIF/CIF o Código GLN/EAN del cliente comprador (IV / BY) no está configurado (ge_clientes.ean / nif).');

    if QryCab.FieldByName('fecha').IsNull then
      LErrors.Add('- Fecha de la factura no válida.');

    // Validar Datos específicos de Gran Consumo (El Corte Inglés / Hipercor)
    var LDestDepto := NormalizeDepartmentCode(QryCab.FieldByName('destino_departamento').AsString);
    var LDestEmisor := Trim(QryCab.FieldByName('destino_ean_emisor').AsString);
    var LIsECI := (UpperCase(Trim(QryCab.FieldByName('cliente_nif').AsString)) = 'A28017895') or
                  (Pos('8422416', QryCab.FieldByName('cliente_gln').AsString) = 1) or
                  (Pos('8422416', QryCab.FieldByName('destino_ean_facturacion').AsString) = 1) or
                  (Pos('8422416', QryCab.FieldByName('destino_ean_emisor').AsString) = 1) or
                  (Pos('8422416', QryCab.FieldByName('destino_ean_receptor').AsString) = 1);

    if LIsECI and (LDestDepto = '') then
      LErrors.Add('- Código de departamento no configurado en el destino (ge_clientes_destinos.departamento). El Corte Inglés requiere obligatoriamente el código de departamento en RFF+API (ej: 206).');

    if LIsECI and (LDestEmisor = '') then
      LErrors.Add('- Punto operacional (ean_emisor) no configurado en el destino (ge_clientes_destinos.ean_emisor). El Corte Inglés requiere el punto operacional para la sucursal de entrega en NAD+BY.');

    // 2. Validar Líneas de la Factura
    QryDet.SQL.Text :=
      'SELECT l.id, l.posicion, l.descripcion, l.cantidad, l.precio, l.base, l.total, ' +
      '       l.articulo_id, COALESCE(NULLIF(TRIM(a.ean), ''''), NULLIF(TRIM(l.articulo_codigo), '''')) AS articulo_ean, ' +
      '       l.articulo_codigo AS articulo_codigo ' +
      'FROM ge_facturas_lineas l ' +
      'LEFT JOIN ge_articulos a ON l.articulo_id = a.id ' +
      'WHERE l.factura_id = :fac_id ' +
      'ORDER BY l.posicion, l.id';
    QryDet.ParamByName('fac_id').AsString := AFacturaId;
    QryDet.Open;

    LLineCount := 0;
    while not QryDet.Eof do
    begin
      Inc(LLineCount);
      var LEan := Trim(QryDet.FieldByName('articulo_ean').AsString);
      if LEan = '' then
        LEan := Trim(QryDet.FieldByName('articulo_codigo').AsString);

      if (LEan = '') or (Length(LEan) < 8) then
        LErrors.Add(Format('- Línea %d (%s): El artículo no dispone de código EAN/UPC válido en su ficha (ge_articulos.ean). EDICOM requiere obligatoriamente identificador tipo EAN o UPC (no admite código interno SA).', 
          [LLineCount, QryDet.FieldByName('descripcion').AsString]));

      if QryDet.FieldByName('cantidad').AsFloat <= 0 then
        LErrors.Add(Format('- Línea %d: Cantidad inválida (debe ser mayor a 0).', [LLineCount]));

      QryDet.Next;
    end;

    if LLineCount = 0 then
      LErrors.Add('- La factura no contiene ninguna línea de detalle.');

    // Resultado final de validación
    if LErrors.Count > 0 then
    begin
      AErrMessage := 'No se puede generar/enviar la factura EDI por los siguientes datos faltantes:' + sLineBreak + LErrors.Text;
      Result := False;
      Exit;
    end;
    Result := True;
  finally
    LErrors.Free;
    QryCab.Free;
    QryDet.Free;
    QryEmp.Free;
  end;
end;

function ResolveInvoicReceptorGln(
  const AClienteGln, AClienteNif, AClienteCp, AClientePais,
        ADestinoEanFacturacion, ADestinoEanReceptor, ADestinoCp, ADestinoPais: string): string;
var
  LCp, LPais: string;
begin
  // 1. Si el destino tiene un ean_facturacion explícito configurado (NAD+IV), usarlo con MÁXIMA prioridad
  Result := Trim(ADestinoEanFacturacion);
  if Pos('-', Result) > 0 then
    Result := Trim(Copy(Result, 1, Pos('-', Result) - 1));

  // Si para El Corte Inglés se guardó erróneamente '8422416000508' o '8422416000504', normalizar
  // para que aplique la regla oficial de Gran Consumo (8422416200508 Península / 8422416200720 Canarias).
  if (Result = '8422416000508') or (Result = '8422416000504') then
    Result := '';

  if Result <> '' then Exit;

  // 2. Determinar CP y País efectivos
  LCp := Trim(ADestinoCp);
  if LCp = '' then LCp := Trim(AClienteCp);

  LPais := UpperCase(Trim(ADestinoPais));
  if LPais = '' then LPais := UpperCase(Trim(AClientePais));

  // 3. Reglas administrativas oficiales de Gran Consumo (El Corte Inglés / Hipercor / Supercor)
  // Identificadores de ECI: NIF A28017895, GLN grupo 8422416000504 o prefijo 8422416
  if (UpperCase(Trim(AClienteNif)) = 'A28017895') or
     (Pos('8422416', AClienteGln) = 1) or
     (Pos('8422416', ADestinoEanReceptor) = 1) or
     (Pos('5600000', AClienteGln) = 1) or
     (Pos('5600000', ADestinoEanReceptor) = 1) then
  begin
    // Portugal
    if (LPais = 'PT') or (LPais = 'PORTUGAL') or (Pos('5600000', AClienteGln) = 1) or (Pos('5600000', ADestinoEanReceptor) = 1) then
      Result := '5600000025895'
    // Canarias (Provincias 35 - Las Palmas y 38 - Santa Cruz de Tenerife)
    else if (Copy(LCp, 1, 2) = '35') or (Copy(LCp, 1, 2) = '38') then
      Result := '8422416200720'
    // Península y resto de España
    else
      Result := '8422416200508';
    Exit;
  end;

  // 4. Si hay ean_receptor explícito en el destino, usarlo
  Result := Trim(ADestinoEanReceptor);
  if Pos('-', Result) > 0 then
    Result := Trim(Copy(Result, 1, Pos('-', Result) - 1));
  if Result <> '' then Exit;

  // 5. Clientes estándar: usar el GLN configurado en el cliente
  Result := Trim(AClienteGln);
  if Pos('-', Result) > 0 then
    Result := Trim(Copy(Result, 1, Pos('-', Result) - 1));
end;

function NormalizeDepartmentCode(const ADept: string): string;
begin
  Result := Trim(ADept);
  // Para EDI/EDICOM Gran Consumo (El Corte Inglés), el código de departamento (Buyer_department_id -> RFF+API)
  // tiene una longitud estándar máxima de 3 caracteres X(3) (ej: '206', '041').
  // Si en la base de datos se almacenó con ceros sobrantes iniciales como '0206', se eliminan hasta quedar en 3 caracteres.
  while (Length(Result) > 3) and (Result[1] = '0') do
    Delete(Result, 1, 1);
  if Length(Result) > 3 then
    Result := Copy(Result, 1, 3);
end;

function ResolveInvoicBuyerGln(const AClienteGln, ADestinoEanCliente, ADestinoEanEmisor: string): string;
begin
  // 1. Punto operacional: ean_emisor tiene máxima prioridad para el comprador / sucursal (NAD+BY)
  Result := Trim(ADestinoEanEmisor);
  if Pos('-', Result) > 0 then
    Result := Trim(Copy(Result, 1, Pos('-', Result) - 1));
  if Result <> '' then Exit;

  // 2. Si no hay ean_emisor, usar ean_cliente explícito
  Result := Trim(ADestinoEanCliente);
  if Pos('-', Result) > 0 then
    Result := Trim(Copy(Result, 1, Pos('-', Result) - 1));
  if Result <> '' then Exit;

  // 3. Fallback al GLN del cliente
  Result := Trim(AClienteGln);
  if Pos('-', Result) > 0 then
    Result := Trim(Copy(Result, 1, Pos('-', Result) - 1));
end;

function GenerarXmlInvoic(const AFacturaId: string; AConn: TFDConnection): string;
var
  XMLDoc: IXMLDocument;
  NodeHeader, NodeParties, NodeDocs, NodeTaxes, NodeTotals, NodeNotes, NodeGroupHeader: IXMLNode;
  NodeLines, NodeAddLine, NodeSuppLine, NodeTaxesLine, NodeTotalsLine, NodeNotesLine: IXMLNode;
  QryCab, QryDet, QryEmp: TFDQuery;
  LFacturaNum, LFechaStr, LGlnEmisor, LGlnCliente, LNifCliente: string;
  LCountLines: Integer;

  function FloatToStrFormat(AVor: Double; ADecimals: Integer = 2): string;
  begin
    Result := FormatFloat('0.' + StringOfChar('0', ADecimals), AVor);
    Result := StringReplace(Result, ',', '.', [rfReplaceAll]);
  end;

begin
  QryCab := TFDQuery.Create(nil);
  QryDet := TFDQuery.Create(nil);
  QryEmp := TFDQuery.Create(nil);
  try
    QryCab.Connection := AConn;
    QryDet.Connection := AConn;
    QryEmp.Connection := AConn;

    QryCab.SQL.Text :=
      'SELECT f.id, f.serie, f.numero, f.fecha, f.total, f.base_imponible, f.iva, f.referencia, ' +
      '       f.empresa_id, f.cliente_id, f.clientedestino_id, ' +
      '       f.edi_referencia_pedido_cliente, ' +
      '       c.nif AS cliente_nif, c.nombre_fiscal AS cliente_nombre, c.ean AS cliente_gln, ' +
      '       c.codigo_postal AS cliente_cp, c.codigo_pais AS cliente_pais, ' +
      '       d.ean_facturacion AS destino_ean_facturacion, d.ean_cliente AS destino_ean_cliente, ' +
      '       d.ean_emisor AS destino_ean_emisor, d.ean_receptor AS destino_ean_receptor, ' +
      '       d.cp AS destino_cp, d.pais AS destino_pais, ' +
      '       d.departamento AS destino_departamento, ' +
      '       d.centro AS destino_centro, ' +
      '       COALESCE((SELECT tipo_iva FROM ge_facturas_lineas WHERE factura_id = f.id ORDER BY posicion, id LIMIT 1), 21.0) AS tipo_iva_cab, ' +
      '       COALESCE((SELECT CONCAT(COALESCE(alb.serie, ''''), ''/'', alb.numero) FROM ge_albaranes alb WHERE alb.id_factura = f.id LIMIT 1), '''') AS primer_albaran, ' +
      '       COALESCE((SELECT COALESCE(NULLIF(TRIM(p.numero_pedido), ''''), NULLIF(TRIM(alb.edi_referencia_pedido_cliente), '''')) ' +
      '                 FROM ge_albaranes alb ' +
      '                 LEFT JOIN ge_pedidos p ON alb.id_pedido = p.id ' +
      '                 WHERE alb.id_factura = f.id LIMIT 1), '''') AS pedido_cliente ' +
      'FROM ge_facturas f ' +
      'LEFT JOIN ge_clientes c ON f.cliente_id = c.id ' +
      'LEFT JOIN ge_clientes_destinos d ON d.id = COALESCE(f.clientedestino_id, (SELECT alb.clientedestino_id FROM ge_albaranes alb WHERE alb.id_factura = f.id AND alb.clientedestino_id IS NOT NULL LIMIT 1)) ' +
      'WHERE f.id = :id';
    QryCab.ParamByName('id').AsString := AFacturaId;
    QryCab.Open;

    QryEmp.SQL.Text :=
      'SELECT e.cif_nif, e.nombre, e.gln AS emp_gln, ed.gln_empresa AS edicom_gln ' +
      'FROM ge_empresas e ' +
      'LEFT JOIN ge_edicom_config ed ON ed.id_empresa = e.id AND ed.activo = 1 ' +
      'WHERE e.id = :emp_id';
    QryEmp.ParamByName('emp_id').AsString := QryCab.FieldByName('empresa_id').AsString;
    QryEmp.Open;

    QryDet.SQL.Text :=
      'SELECT l.id, l.posicion, l.descripcion, l.cantidad, l.precio, l.base, l.total, l.tipo_iva, ' +
      '       l.articulo_id, COALESCE(NULLIF(TRIM(a.ean), ''''), NULLIF(TRIM(l.articulo_codigo), '''')) AS articulo_ean, ' +
      '       l.articulo_codigo AS articulo_codigo, ' +
      '       COALESCE(alb.serie, '''') AS alb_serie, COALESCE(alb.numero, 0) AS alb_numero ' +
      'FROM ge_facturas_lineas l ' +
      'LEFT JOIN ge_articulos a ON l.articulo_id = a.id ' +
      'LEFT JOIN ge_albaranes_lineas albl ON l.edi_numero_linea_albaran = albl.id ' +
      'LEFT JOIN ge_albaranes alb ON (albl.albaran_id = alb.id OR alb.id_factura = l.factura_id) ' +
      'WHERE l.factura_id = :fac_id ' +
      'ORDER BY l.posicion, l.id';
    QryDet.ParamByName('fac_id').AsString := AFacturaId;
    QryDet.Open;
    LCountLines := QryDet.RecordCount;

    XMLDoc := TXMLDocument.Create(nil);
    XMLDoc.Active := True;
    XMLDoc.Version := '1.0';
    XMLDoc.Encoding := 'UTF-8';
    XMLDoc.Options := [doNodeAutoIndent];

    // 1. Cabecera (HEADER)
    NodeHeader := XMLDoc.AddChild('HEADER');
    LFacturaNum := QryCab.FieldByName('serie').AsString + '-' + QryCab.FieldByName('numero').AsString;
    LFechaStr := FormatDateTime('yyyy-mm-dd"T"hh:nn:ss', QryCab.FieldByName('fecha').AsDateTime);

    NodeHeader.AddChild('Invoice_number').Text := LFacturaNum;
    NodeHeader.AddChild('Project').Text := 'ES_ECI_OUT';
    NodeHeader.AddChild('Type').Text := '380';
    NodeHeader.AddChild('Date').Text := LFechaStr;
    NodeHeader.AddChild('Currency_code').Text := 'EUR';

    LGlnEmisor := Trim(QryEmp.FieldByName('edicom_gln').AsString);
    if LGlnEmisor = '' then LGlnEmisor := Trim(QryEmp.FieldByName('emp_gln').AsString);
    if LGlnEmisor = '' then LGlnEmisor := Trim(QryEmp.FieldByName('cif_nif').AsString);

    LGlnCliente := Trim(QryCab.FieldByName('cliente_gln').AsString);
    if Pos('-', LGlnCliente) > 0 then
      LGlnCliente := Trim(Copy(LGlnCliente, 1, Pos('-', LGlnCliente) - 1));
    LNifCliente := Trim(QryCab.FieldByName('cliente_nif').AsString);

    // Resolución inteligente de GLNs según la estructura administrativa oficial de Gran Consumo
    var LGlnReceptor := ResolveInvoicReceptorGln(
      LGlnCliente, LNifCliente,
      QryCab.FieldByName('cliente_cp').AsString, QryCab.FieldByName('cliente_pais').AsString,
      QryCab.FieldByName('destino_ean_facturacion').AsString, QryCab.FieldByName('destino_ean_receptor').AsString,
      QryCab.FieldByName('destino_cp').AsString, QryCab.FieldByName('destino_pais').AsString
    );

    var LGlnComprador := ResolveInvoicBuyerGln(
      LGlnCliente,
      QryCab.FieldByName('destino_ean_cliente').AsString,
      QryCab.FieldByName('destino_ean_emisor').AsString
    );

    var LGlnDestino := Trim(QryCab.FieldByName('destino_ean_receptor').AsString);
    if Pos('-', LGlnDestino) > 0 then
      LGlnDestino := Trim(Copy(LGlnDestino, 1, Pos('-', LGlnDestino) - 1));
    if LGlnDestino = '' then
      LGlnDestino := LGlnComprador;

    // 2. Interlocutores (PARTIES) con <Qualifier>
    // SU: Vendedor (Emisor)
    NodeParties := NodeHeader.AddChild('PARTIES');
    NodeParties.AddChild('Qualifier').Text := 'SU';
    if Length(LGlnEmisor) = 13 then NodeParties.AddChild('Identifier_type').Text := 'GLN'
    else NodeParties.AddChild('Identifier_type').Text := 'VAT';
    NodeParties.AddChild('Identifier').Text := LGlnEmisor;
    if Trim(QryEmp.FieldByName('cif_nif').AsString) <> '' then
    begin
      NodeParties.AddChild('Tax_id').Text := Trim(QryEmp.FieldByName('cif_nif').AsString);
      NodeParties.AddChild('Tax_id_type').Text := 'Vat_number';
    end;

    // IV: Facturado a / Receptor de Factura (A quien se factura - NAD+IV)
    NodeParties := NodeHeader.AddChild('PARTIES');
    NodeParties.AddChild('Qualifier').Text := 'IV';
    if (Length(LGlnReceptor) = 13) and (StrToInt64Def(LGlnReceptor, 0) > 0) then
      NodeParties.AddChild('Identifier_type').Text := 'GLN'
    else if LGlnReceptor <> '' then
      NodeParties.AddChild('Identifier_type').Text := 'GLN'
    else
      NodeParties.AddChild('Identifier_type').Text := 'VAT';

    if LGlnReceptor <> '' then
      NodeParties.AddChild('Identifier').Text := LGlnReceptor
    else
      NodeParties.AddChild('Identifier').Text := LNifCliente;

    if LNifCliente <> '' then
    begin
      NodeParties.AddChild('Tax_id').Text := LNifCliente;
      NodeParties.AddChild('Tax_id_type').Text := 'Vat_number';
    end;

    // BY: Comprador / Quién pide (NAD+BY)
    NodeParties := NodeHeader.AddChild('PARTIES');
    NodeParties.AddChild('Qualifier').Text := 'BY';
    if (Length(LGlnComprador) = 13) and (StrToInt64Def(LGlnComprador, 0) > 0) then
      NodeParties.AddChild('Identifier_type').Text := 'GLN'
    else if LGlnComprador <> '' then
      NodeParties.AddChild('Identifier_type').Text := 'GLN'
    else
      NodeParties.AddChild('Identifier_type').Text := 'VAT';

    if LGlnComprador <> '' then
      NodeParties.AddChild('Identifier').Text := LGlnComprador
    else
      NodeParties.AddChild('Identifier').Text := LNifCliente;

    if LNifCliente <> '' then
    begin
      NodeParties.AddChild('Tax_id').Text := LNifCliente;
      NodeParties.AddChild('Tax_id_type').Text := 'Vat_number';
    end;

    // Código de departamento del comprador (Buyer_department_id -> RFF+API en EDIFACT)
    var LDepto := NormalizeDepartmentCode(QryCab.FieldByName('destino_departamento').AsString);
    if LDepto <> '' then
    begin
      var NodeAddParties := NodeParties.AddChild('ADDITIONAL_GROUP_PARTIES');
      NodeAddParties.AddChild('Name').Text := 'GENERAL';
      var NodePropParties := NodeAddParties.AddChild('ADDITIONAL_GROUP_PROPERTY_PARTIES');
      NodePropParties.AddChild('Name').Text := 'Buyer_department_id';
      NodePropParties.AddChild('Value').Text := LDepto;
    end;

    // DP: Lugar de Entrega / Destino (Quién recibe - NAD+DP)
    if LGlnDestino <> '' then
    begin
      NodeParties := NodeHeader.AddChild('PARTIES');
      NodeParties.AddChild('Qualifier').Text := 'DP';
      NodeParties.AddChild('Identifier_type').Text := 'GLN';
      NodeParties.AddChild('Identifier').Text := LGlnDestino;
      if LNifCliente <> '' then
      begin
        NodeParties.AddChild('Tax_id').Text := LNifCliente;
        NodeParties.AddChild('Tax_id_type').Text := 'Vat_number';
      end;
    end;

    // 3. Documentos de Referencia (SUPPORTING_DOCS mandatorios)
    // Referencia de albarán en formato DESADV (7 dígitos numéricos) para trazabilidad DESADV ↔ INVOIC
    var LPrimerAlb := '';
    if not QryDet.IsEmpty and (QryDet.FieldByName('alb_numero').AsInteger > 0) then
      LPrimerAlb := FormatDesadvAlbaranNumber(
        Trim(QryDet.FieldByName('alb_serie').AsString),
        QryDet.FieldByName('alb_numero').AsString)
    else
    begin
      var LRawAlb := Trim(QryCab.FieldByName('primer_albaran').AsString);
      if LRawAlb <> '' then
      begin
        var LSlashPos := Pos('/', LRawAlb);
        if LSlashPos > 0 then
          LPrimerAlb := FormatDesadvAlbaranNumber(
            Copy(LRawAlb, 1, LSlashPos - 1),
            Copy(LRawAlb, LSlashPos + 1, MaxInt))
        else
          LPrimerAlb := FormatDesadvAlbaranNumber('', LRawAlb);
      end;
    end;
    if LPrimerAlb = '' then LPrimerAlb := LFacturaNum;

    // Albarán (Tipo 351)
    NodeDocs := NodeHeader.AddChild('SUPPORTING_DOCS');
    NodeDocs.AddChild('Type').Text := '351';
    NodeDocs.AddChild('Identifier').Text := LPrimerAlb;

    // Pedido (Tipo 220)
    var LPedido := Trim(QryCab.FieldByName('pedido_cliente').AsString);
    if LPedido = '' then LPedido := Trim(QryCab.FieldByName('edi_referencia_pedido_cliente').AsString);
    if LPedido = '' then LPedido := Trim(QryCab.FieldByName('referencia').AsString);
    if LPedido = '' then LPedido := 'PED-' + LFacturaNum;

    NodeDocs := NodeHeader.AddChild('SUPPORTING_DOCS');
    NodeDocs.AddChild('Type').Text := '220';
    NodeDocs.AddChild('Identifier').Text := LPedido;

    // 4. Impuestos Cabecera (TAXES)
    NodeTaxes := NodeHeader.AddChild('TAXES');
    NodeTaxes.AddChild('Type').Text := 'VAT';
    NodeTaxes.AddChild('Rate').Text := FloatToStrFormat(QryCab.FieldByName('tipo_iva_cab').AsFloat, 2);
    NodeTaxes.AddChild('Amount').Text := FloatToStrFormat(QryCab.FieldByName('iva').AsFloat, 2);

    // 5. Totales Cabecera (TOTALS)
    NodeTotals := NodeHeader.AddChild('TOTALS');
    NodeTotals.AddChild('Total_amount').Text := FloatToStrFormat(QryCab.FieldByName('total').AsFloat, 2);
    NodeTotals.AddChild('Net_amount').Text := FloatToStrFormat(QryCab.FieldByName('base_imponible').AsFloat, 2);
    NodeTotals.AddChild('Gross_amount').Text := FloatToStrFormat(QryCab.FieldByName('base_imponible').AsFloat, 2);
    NodeTotals.AddChild('Taxable_amount').Text := FloatToStrFormat(QryCab.FieldByName('base_imponible').AsFloat, 2);

    // 6. Observaciones (NOTES)
    NodeNotes := NodeHeader.AddChild('NOTES');
    NodeNotes.AddChild('Type').Text := 'AAI';
    NodeNotes.AddChild('Text').Text := 'Factura generada desde GESTION--RAUL ASENCIO';

    // 7. Bloque de Grupo de Cabecera (ADDITIONAL_GROUP_HEADER)
    NodeGroupHeader := NodeHeader.AddChild('ADDITIONAL_GROUP_HEADER');
    NodeGroupHeader.AddChild('Name').Text := 'GENERAL';
    var NodeProp := NodeGroupHeader.AddChild('ADDITIONAL_GROUP_PROPERTY_HEADER');
    NodeProp.AddChild('Name').Text := 'Total_lines';
    NodeProp.AddChild('Value').Text := IntToStr(LCountLines);

    // 8. Cargar Líneas de Factura (LINES)
    LCountLines := 0;
    QryDet.First;
    while not QryDet.Eof do
    begin
      Inc(LCountLines);
      NodeLines := NodeHeader.AddChild('LINES');
      NodeLines.AddChild('Line_ID').Text := IntToStr(LCountLines);

      var LEan := Trim(QryDet.FieldByName('articulo_ean').AsString);
      if LEan = '' then LEan := Trim(QryDet.FieldByName('articulo_codigo').AsString);

      if Length(LEan) = 12 then NodeLines.AddChild('Identifier_type').Text := 'UPC'
      else NodeLines.AddChild('Identifier_type').Text := 'EAN';

      NodeLines.AddChild('Identifier').Text := LEan;
      NodeLines.AddChild('Description').Text := QryDet.FieldByName('descripcion').AsString;
      NodeLines.AddChild('Quantity').Text := FloatToStrFormat(QryDet.FieldByName('cantidad').AsFloat, 0);
      NodeLines.AddChild('Unit_gross_price').Text := FloatToStrFormat(QryDet.FieldByName('precio').AsFloat, 2);

      NodeSuppLine := NodeLines.AddChild('SUPPORTING_DOCS_LINES');
      NodeSuppLine.AddChild('Type').Text := '351';
      if QryDet.FieldByName('alb_numero').AsInteger > 0 then
        NodeSuppLine.AddChild('Identifier').Text := FormatDesadvAlbaranNumber(
          Trim(QryDet.FieldByName('alb_serie').AsString),
          QryDet.FieldByName('alb_numero').AsString)
      else
        NodeSuppLine.AddChild('Identifier').Text := LPrimerAlb;

      var LCant: Double := QryDet.FieldByName('cantidad').AsFloat;
      var LPrec: Double := QryDet.FieldByName('precio').AsFloat;
      var LTotalLin: Double := QryDet.FieldByName('total').AsFloat;
      var LTipoIva: Double := QryDet.FieldByName('tipo_iva').AsFloat;
      var LBaseLin: Double := QryDet.FieldByName('base').AsFloat;

      if LBaseLin <= 0 then
      begin
        if (LCant > 0) and (LPrec > 0) then LBaseLin := LCant * LPrec
        else if LTotalLin > 0 then
        begin
          if LTipoIva > 0 then LBaseLin := LTotalLin / (1.0 + (LTipoIva / 100.0))
          else LBaseLin := LTotalLin;
        end;
      end;
      if LTotalLin <= 0 then LTotalLin := LBaseLin * (1.0 + (LTipoIva / 100.0));
      var LIvaCuotaLin: Double := LTotalLin - LBaseLin;
      if LIvaCuotaLin < 0 then LIvaCuotaLin := 0;

      NodeTaxesLine := NodeLines.AddChild('TAXES_LINES');
      NodeTaxesLine.AddChild('Type').Text := 'VAT';
      NodeTaxesLine.AddChild('Rate').Text := FloatToStrFormat(LTipoIva, 2);
      NodeTaxesLine.AddChild('Amount').Text := FloatToStrFormat(LIvaCuotaLin, 2);

      NodeTotalsLine := NodeLines.AddChild('TOTALS_LINES');
      NodeTotalsLine.AddChild('Net_amount').Text := FloatToStrFormat(LBaseLin, 2);

      QryDet.Next;
    end;

    XMLDoc.SaveToXML(Result);
  finally
    QryCab.Free;
    QryDet.Free;
    QryEmp.Free;
  end;
end;

procedure ExportarFacturaXmlEdicom(const AFacturaId: string; AConn: TFDConnection);
var
  LErrMsg, LXmlContent, LSerie, LNumero, LDefaultFileName: string;
  SaveDlg: TSaveDialog;
  StringList: TStringList;
  Qry: TFDQuery;
begin
  // 1. Validar Datos Críticos
  if not ValidarFacturaEdicom(AFacturaId, AConn, LErrMsg) then
  begin
    MessageDlg(LErrMsg, mtWarning, [mbOK], 0);
    Exit;
  end;

  // Obtener Serie y Número para el nombre predeterminado
  LSerie := '';
  LNumero := AFacturaId;
  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := AConn;
    Qry.SQL.Text := 'SELECT serie, numero FROM ge_facturas WHERE id = :id';
    Qry.ParamByName('id').AsString := AFacturaId;
    Qry.Open;
    if not Qry.IsEmpty then
    begin
      LSerie := Qry.FieldByName('serie').AsString;
      LNumero := Qry.FieldByName('numero').AsString;
    end;
  finally
    Qry.Free;
  end;

  if LSerie <> '' then
    LDefaultFileName := Format('INVOICE_%s_%s.xml', [LSerie, LNumero])
  else
    LDefaultFileName := Format('INVOICE_%s.xml', [LNumero]);

  // 2. Generar XML
  LXmlContent := GenerarXmlInvoic(AFacturaId, AConn);

  // 3. Guardar Archivo
  SaveDlg := TSaveDialog.Create(nil);
  StringList := TStringList.Create;
  try
    SaveDlg.Title := 'Guardar Factura XML EDICOM (INVOIC)';
    SaveDlg.Filter := 'Archivos XML (*.xml)|*.xml|Todos los archivos (*.*)|*.*';
    SaveDlg.DefaultExt := 'xml';
    SaveDlg.FileName := LDefaultFileName;

    if SaveDlg.Execute then
    begin
      StringList.Text := LXmlContent;
      StringList.SaveToFile(SaveDlg.FileName, TEncoding.UTF8);
      ShowMessage('Factura exportada con éxito en:' + sLineBreak + SaveDlg.FileName);
    end;
  finally
    StringList.Free;
    SaveDlg.Free;
  end;
end;

end.
