unit uEdicomDesadvExporter;

interface

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

/// <summary>
/// Realiza la comprobación de datos críticos del albarán y sus líneas para DESADV EDICOM
/// </summary>
function ValidarAlbaranEdicom(const AlbaranId: string; AConn: TFDConnection; out AErrMessage: string): Boolean;

/// <summary>
/// Genera el string XML según la estructura de albaranes DESADV (DESADV_ECI_GC_COMPLETO.XML)
/// </summary>
function GenerarXmlDesadv(const AlbaranId: string; AConn: TFDConnection): string;

/// <summary>
/// Exporta el XML del albarán a un archivo seleccionado por el usuario
/// </summary>
procedure ExportarAlbaranXmlEdicom(const AlbaranId: string; AConn: TFDConnection);

/// <summary>
/// Formatea el número de albarán para DESADV: concatena serie+número como solo dígitos
/// y rellena con ceros a la izquierda hasta 7 caracteres. Ej: serie='26', numero='433' → '0026433'
/// </summary>
function FormatDesadvAlbaranNumber(const ASerie, ANumero: string): string;

implementation

uses
  dmg_Main, uValidacionFiscal;

// ---------------------------------------------------------------------------
// Funciones auxiliares para resolución de GLN y formateo DESADV
// ---------------------------------------------------------------------------

function FormatDesadvAlbaranNumber(const ASerie, ANumero: string): string;
var
  LCombined: string;
  I: Integer;
begin
  // Concatenar serie + número conservando solo dígitos
  LCombined := '';
  for I := 1 to Length(ASerie) do
    if CharInSet(ASerie[I], ['0'..'9']) then
      LCombined := LCombined + ASerie[I];
  for I := 1 to Length(ANumero) do
    if CharInSet(ANumero[I], ['0'..'9']) then
      LCombined := LCombined + ANumero[I];

  // Rellenar con ceros a la izquierda hasta 7 dígitos
  while Length(LCombined) < 7 do
    LCombined := '0' + LCombined;

  // Si excede 7 dígitos, tomar los últimos 7
  if Length(LCombined) > 7 then
    LCombined := Copy(LCombined, Length(LCombined) - 6, 7);

  Result := LCombined;
end;

function NormalizeDepartmentCodeDesadv(const ADept: string): string;
begin
  Result := Trim(ADept);
  // Código de departamento (Buyer_department_id → RFF+API / RFF+SD): longitud estándar X(3)
  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 ResolveDesadvReceptorGln(const AClienteGln, AClienteNif, ADestinoEanCliente: string): string;
begin
  // Para El Corte Inglés Central de Compras Gran Consumo: GLN 8422416000504
  if (UpperCase(Trim(AClienteNif)) = 'A28017895') or
     (Pos('8422416', AClienteGln) = 1) or
     (Pos('8422416', ADestinoEanCliente) = 1) then
  begin
    Result := '8422416000504';
    Exit;
  end;

  // Otros clientes: usar ean_cliente del destino
  Result := Trim(ADestinoEanCliente);
  if Pos('-', Result) > 0 then
    Result := Trim(Copy(Result, 1, Pos('-', Result) - 1));
  if Result <> '' then Exit;

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

function ResolveDesadvBuyerGln(const AClienteGln, ADestinoEanCliente,
  ADestinoEanEmisor, ADestinoEanReceptor: string): string;
begin
  // 1. ean_receptor: en DESADV, el comprador (BY) coincide con la sucursal operacional
  //    que realizó el pedido (NAD+BY del ORDERS), almacenado como ean_receptor (NAD+DP)
  Result := Trim(ADestinoEanReceptor);
  if Pos('-', Result) > 0 then
    Result := Trim(Copy(Result, 1, Pos('-', Result) - 1));
  if Result <> '' then Exit;

  // 2. ean_emisor (punto operacional alternativo)
  Result := Trim(ADestinoEanEmisor);
  if Pos('-', Result) > 0 then
    Result := Trim(Copy(Result, 1, Pos('-', Result) - 1));
  if Result <> '' then Exit;

  // 3. ean_cliente del destino
  Result := Trim(ADestinoEanCliente);
  if Pos('-', Result) > 0 then
    Result := Trim(Copy(Result, 1, Pos('-', Result) - 1));
  if Result <> '' then Exit;

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

function ObtenerIdentificadorEmbalaje(AClienteId, AArticuloId: Integer; const ADefaultEan: string; AConn: TFDConnection): string;
var
  QryCliArt: TFDQuery;
  LDun14, LEanCli: string;
begin
  Result := '';
  if (AClienteId > 0) and (AArticuloId > 0) and Assigned(AConn) then
  begin
    QryCliArt := TFDQuery.Create(nil);
    try
      QryCliArt.Connection := AConn;
      QryCliArt.SQL.Text :=
        'SELECT COALESCE(NULLIF(TRIM(dun14), ''''), '''') AS dun14_val, ' +
        '       COALESCE(NULLIF(TRIM(ean13), ''''), '''') AS ean13_val ' +
        'FROM ge_clientes_articulos ' +
        'WHERE id_cliente = :cli AND id_articulo = :art AND activo = 1 ' +
        'LIMIT 1';
      QryCliArt.ParamByName('cli').AsInteger := AClienteId;
      QryCliArt.ParamByName('art').AsInteger := AArticuloId;
      QryCliArt.Open;
      if not QryCliArt.IsEmpty then
      begin
        LDun14 := Trim(QryCliArt.FieldByName('dun14_val').AsString);
        LEanCli := Trim(QryCliArt.FieldByName('ean13_val').AsString);
        if LDun14 <> '' then
          Result := LDun14
        else if LEanCli <> '' then
          Result := LEanCli;
      end;
    finally
      QryCliArt.Free;
    end;
  end;

  if Result = '' then
    Result := Trim(ADefaultEan);
end;

function ValidarAlbaranEdicom(const AlbaranId: string; AConn: TFDConnection; out AErrMessage: string): Boolean;
var
  QryCab, QryDet, QryEmp: TFDQuery;
  LLineCount: Integer;
  LErrors: TStringList;
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 Cabecera del Albarán y Cliente
    QryCab.SQL.Text :=
      'SELECT a.id, a.serie, a.numero, a.fecha, a.total, a.empresa_id, a.cliente_id, a.clientedestino_id, ' +
      '       c.nif AS cliente_nif, c.nombre_fiscal AS cliente_nombre, c.ean AS cliente_gln, ' +
      '       d.ean_receptor AS destino_ean_receptor, d.ean_cliente AS destino_ean_cliente, ' +
      '       d.ean_emisor AS destino_ean_emisor, d.ean_facturacion AS destino_ean_facturacion ' +
      'FROM ge_albaranes a ' +
      'LEFT JOIN ge_clientes c ON a.cliente_id = c.id ' +
      'LEFT JOIN ge_clientes_destinos d ON a.clientedestino_id = d.id ' +
      'WHERE a.id = :id';
    QryCab.ParamByName('id').AsString := AlbaranId;
    QryCab.Open;

    if QryCab.IsEmpty then
    begin
      AErrMessage := 'El albarán no existe en la base de datos.';
      Exit;
    end;

    // Validar Datos Emisor
    QryEmp.SQL.Text := 'SELECT cif_nif, nombre FROM ge_empresas WHERE id = :emp_id';
    QryEmp.ParamByName('emp_id').AsString := QryCab.FieldByName('empresa_id').AsString;
    QryEmp.Open;

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

    // Validar Datos Cliente
    if (Trim(QryCab.FieldByName('cliente_gln').AsString) = '') and (Trim(QryCab.FieldByName('destino_ean_cliente').AsString) = '') then
      LErrors.Add('- Código GLN / EAN del cliente comprador no está configurado (ge_clientes.ean o ge_clientes_destinos.ean_cliente).');

    if QryCab.FieldByName('fecha').IsNull then
      LErrors.Add('- Fecha del albarán no válida.');

    // 2. Validar Líneas del Albarán
    QryDet.SQL.Text :=
      'SELECT l.id, l.posicion, l.descripcion, l.cantidad, l.precio, l.lotes, l.fecha_caducidad, ' +
      '       a.ean AS articulo_ean ' +
      'FROM ge_albaranes_lineas l ' +
      'LEFT JOIN ge_articulos a ON l.articulo_id = a.id ' +
      'WHERE l.albaran_id = :alb_id ' +
      'ORDER BY l.posicion, l.id';
    QryDet.ParamByName('alb_id').AsString := AlbaranId;
    QryDet.Open;

    LLineCount := 0;
    while not QryDet.Eof do
    begin
      Inc(LLineCount);
      if Trim(QryDet.FieldByName('articulo_ean').AsString) = '' then
        LErrors.Add(Format('- Línea %d (%s): El artículo no dispone de código EAN-13.', 
          [LLineCount, QryDet.FieldByName('descripcion').AsString]));

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

      QryDet.Next;
    end;

    if LLineCount = 0 then
      LErrors.Add('- El albarán no contiene ninguna línea de detalle.');

    if LErrors.Count > 0 then
    begin
      AErrMessage := 'No se puede generar el albarán DESADV EDI por los siguientes datos faltantes:' + sLineBreak + LErrors.Text;
      Result := False;
    end
    else
      Result := True;
  finally
    LErrors.Free;
    QryCab.Free;
    QryDet.Free;
    QryEmp.Free;
  end;
end;

function GenerarXmlDesadv(const AlbaranId: string; AConn: TFDConnection): string;
var
  XMLDoc: IXMLDocument;
  NodeHeader, NodeParties, NodeDocs, NodeGroupHeader, NodePackages, NodeLinePkg, NodeBatch, NodeAddId: IXMLNode;
  QryCab, QryDet, QryEmp, QryBultos, QryContenido: TFDQuery;
  LAlbNum, LNumPedido, LFechaStr, LLote, LExpiryStr, LGlnEmisor, LGlnCliente, LNifCliente, LIdType, LIdVal: string;
  LSsccVal, LTipoEmb, LEanVal, LPackagingId, LEmpGln, LBaseSSCC, LFirstSscc: string;
  LCountLines, LPkgSeq, LTotalCajasPalet, LBultosCant, LClienteId, LArtId, LBultoId, LLineInPkg, LIdxBulto, LBaseCant, LResto: Integer;
  LCantTotal, LCantEstaCaja: Double;

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

  function GenerarNuevoSSCC18: string;
  var
    QrySscc: TFDQuery;
    LPrefix: string;
    LNextSscc: Int64;
  begin
    LPrefix := Copy(LEmpGln, 1, 9);
    if Length(LPrefix) < 7 then
      LPrefix := '843701918'; // Prefijo GS1 por defecto de Raúl Asencio

    QrySscc := TFDQuery.Create(nil);
    try
      QrySscc.Connection := AConn;
      try
        QrySscc.SQL.Text := 'SELECT sscc FROM ge_contadores LIMIT 1';
        QrySscc.Open;
        if QrySscc.IsEmpty then
        begin
          LNextSscc := 1;
          QrySscc.Close;
          QrySscc.SQL.Text := 'INSERT INTO ge_contadores (id, sscc) VALUES (1, 2)';
          QrySscc.ExecSQL;
        end
        else
        begin
          LNextSscc := QrySscc.FieldByName('sscc').AsLargeInt;
          QrySscc.Close;
          QrySscc.SQL.Text := 'UPDATE ge_contadores SET sscc = sscc + 1';
          QrySscc.ExecSQL;
        end;

        Result := TValidacionFiscal.GenerarSSCC18(LPrefix, LNextSscc, 3);
      except
        Result := TValidacionFiscal.GenerarSSCC18(LPrefix, StrToInt64Def(LAlbNum, 1), 3);
      end;
    finally
      QrySscc.Free;
    end;
  end;

  function ObtenerOGenerarSSCCBulto(const ABultoId: Integer; const ACurrentSscc: string): string;
  var
    QryUpd: TFDQuery;
  begin
    Result := Trim(ACurrentSscc);
    if (Length(Result) = 18) and (StrToInt64Def(Result, 0) > 0) then Exit;

    Result := GenerarNuevoSSCC18;
    if (ABultoId > 0) and (Result <> '') then
    begin
      QryUpd := TFDQuery.Create(nil);
      try
        QryUpd.Connection := AConn;
        QryUpd.SQL.Text := 'UPDATE ge_albaranes_bultos SET sscc = :sscc WHERE id = :id';
        QryUpd.ParamByName('sscc').AsString := Result;
        QryUpd.ParamByName('id').AsInteger := ABultoId;
        QryUpd.ExecSQL;
      finally
        QryUpd.Free;
      end;
    end;
  end;

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

    QryCab.SQL.Text :=
      'SELECT a.id, a.serie, a.numero, a.fecha, a.total, a.empresa_id, a.cliente_id, a.clientedestino_id, a.id_pedido, ' +
      '       a.observaciones1, a.edi_referencia_pedido_cliente, a.edi_numero_sscc, ' +
      '       c.nif AS cliente_nif, c.nombre_fiscal AS cliente_nombre, c.ean AS cliente_gln, ' +
      '       d.ean_receptor AS destino_ean_receptor, d.ean_cliente AS destino_ean_cliente, ' +
      '       d.ean_emisor AS destino_ean_emisor, d.ean_facturacion AS destino_ean_facturacion, ' +
      '       d.departamento AS destino_departamento, d.centro AS destino_centro, ' +
      '       COALESCE(p1.numero_pedido, p2.numero_pedido, '''') AS numero_pedido ' +
      'FROM ge_albaranes a ' +
      'LEFT JOIN ge_clientes c ON a.cliente_id = c.id ' +
      'LEFT JOIN ge_clientes_destinos d ON a.clientedestino_id = d.id ' +
      'LEFT JOIN ge_pedidos p1 ON a.id_pedido = p1.id ' +
      'LEFT JOIN ge_envios_recogidas r ON r.albaran_id = a.id AND r.estado_codigo <> 5 ' +
      'LEFT JOIN ge_pedidos p2 ON r.pedido_id = p2.id ' +
      'WHERE a.id = :id';
    QryCab.ParamByName('id').AsString := AlbaranId;
    QryCab.Open;

    LClienteId := QryCab.FieldByName('cliente_id').AsInteger;

    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;

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

    // Cargar previamente las líneas del albarán para obtener LCountLines
    QryDet.SQL.Text :=
      'SELECT l.id, l.posicion, l.descripcion, l.cantidad, l.bultos, l.precio, l.lotes, l.fecha_caducidad, ' +
      '       l.articulo_id, a.id AS articulo_codigo, a.ean AS articulo_ean ' +
      'FROM ge_albaranes_lineas l ' +
      'LEFT JOIN ge_articulos a ON l.articulo_id = a.id ' +
      'WHERE l.albaran_id = :alb_id ' +
      'ORDER BY l.posicion, l.id';
    QryDet.ParamByName('alb_id').AsString := AlbaranId;
    QryDet.Open;
    LCountLines := QryDet.RecordCount;

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

    NodeHeader := XMLDoc.AddChild('HEADER');

    // 1. Cabecera Principal DESADV
    // Número de albarán: solo numérico, 7 dígitos (serie+número concatenados sin guiones)
    // Ej: serie='26', numero='433' → '0026433'
    LAlbNum := FormatDesadvAlbaranNumber(QryCab.FieldByName('serie').AsString, QryCab.FieldByName('numero').AsString);
    LFechaStr := FormatDateTime('yyyy-mm-dd"T"hh:nn:ss', QryCab.FieldByName('fecha').AsDateTime);

    // Resolución de Número de Pedido de Origen
    LNumPedido := Trim(QryCab.FieldByName('edi_referencia_pedido_cliente').AsString);
    if LNumPedido = '' then
      LNumPedido := Trim(QryCab.FieldByName('numero_pedido').AsString);
    if LNumPedido = '' then
      LNumPedido := LAlbNum;

    // Persistir el número de pedido en ge_albaranes si no estaba guardado
    if (Trim(QryCab.FieldByName('edi_referencia_pedido_cliente').AsString) = '') and (LNumPedido <> '') and (LNumPedido <> LAlbNum) then
    begin
      var QryPersist := TFDQuery.Create(nil);
      try
        QryPersist.Connection := AConn;
        QryPersist.SQL.Text := 'UPDATE ge_albaranes SET edi_referencia_pedido_cliente = :ref WHERE id = :id';
        QryPersist.ParamByName('ref').AsString := LNumPedido;
        QryPersist.ParamByName('id').AsString := AlbaranId;
        QryPersist.ExecSQL;
      finally
        QryPersist.Free;
      end;
    end;

    NodeHeader.AddChild('Despatch_advice_number').Text := LAlbNum;
    NodeHeader.AddChild('Project').Text := 'ES_ECI_GC_OUT';
    NodeHeader.AddChild('Type').Text := '398';
    NodeHeader.AddChild('Message_function').Text := '9';
    NodeHeader.AddChild('Date').Text := LFechaStr;
    NodeHeader.AddChild('Delivery_date').Text := LFechaStr;

    // Obtener GLN / NIF del Emisor desde la Base de Datos (ge_edicom_config -> ge_empresas)
    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);

    // Determinar Identificador de Cliente no vacío (BY / MR)
    LGlnCliente := Trim(QryCab.FieldByName('destino_ean_cliente').AsString);
    if LGlnCliente = '' then
      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);
    if LGlnCliente <> '' then
    begin
      LIdType := 'GLN';
      LIdVal := LGlnCliente;
    end
    else
    begin
      LIdType := 'VAT';
      LIdVal := LNifCliente;
    end;

    // 2. Interlocutores (PARTIES)
    // MS - Emisor del Mensaje (Mandatorio DESADV)
    NodeParties := NodeHeader.AddChild('PARTIES');
    NodeParties.AddChild('Qualifier').Text := 'MS';
    if (Length(LGlnEmisor) = 13) and (StrToInt64Def(LGlnEmisor, 0) > 0) then
      NodeParties.AddChild('Identifier_type').Text := 'GLN'
    else
      NodeParties.AddChild('Identifier_type').Text := 'VAT';
    NodeParties.AddChild('Identifier').Text := LGlnEmisor;

    // MR - Receptor del Mensaje (Mandatorio DESADV)
    // Para ECI: GLN corporativo genérico 8422416000016
    var LGlnReceptorMensaje := ResolveDesadvReceptorGln(
      LGlnCliente, LNifCliente,
      QryCab.FieldByName('destino_ean_cliente').AsString);
    NodeParties := NodeHeader.AddChild('PARTIES');
    NodeParties.AddChild('Qualifier').Text := 'MR';
    if (Length(LGlnReceptorMensaje) = 13) and (StrToInt64Def(LGlnReceptorMensaje, 0) > 0) then
      NodeParties.AddChild('Identifier_type').Text := 'GLN'
    else
      NodeParties.AddChild('Identifier_type').Text := 'VAT';
    NodeParties.AddChild('Identifier').Text := LGlnReceptorMensaje;

    // BY - Comprador (sucursal operacional que realizó el pedido)
    var LGlnBuyer := ResolveDesadvBuyerGln(
      LGlnCliente,
      QryCab.FieldByName('destino_ean_cliente').AsString,
      QryCab.FieldByName('destino_ean_emisor').AsString,
      QryCab.FieldByName('destino_ean_receptor').AsString);
    NodeParties := NodeHeader.AddChild('PARTIES');
    NodeParties.AddChild('Qualifier').Text := 'BY';
    if (Length(LGlnBuyer) = 13) and (StrToInt64Def(LGlnBuyer, 0) > 0) then
      NodeParties.AddChild('Identifier_type').Text := 'GLN'
    else
      NodeParties.AddChild('Identifier_type').Text := 'VAT';
    NodeParties.AddChild('Identifier').Text := LGlnBuyer;

    // Código de departamento del comprador (Buyer_department_id → RFF+API / RFF+SD en EDIFACT)
    var LDepto := NormalizeDepartmentCodeDesadv(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;

    // SU - Vendedor / Proveedor (GLN Emisor Dinámico)
    NodeParties := NodeHeader.AddChild('PARTIES');
    NodeParties.AddChild('Qualifier').Text := 'SU';
    if (Length(LGlnEmisor) = 13) and (StrToInt64Def(LGlnEmisor, 0) > 0) then
      NodeParties.AddChild('Identifier_type').Text := 'GLN'
    else
      NodeParties.AddChild('Identifier_type').Text := 'VAT';
    NodeParties.AddChild('Identifier').Text := LGlnEmisor;

    // DP - Punto de Entrega
    var LGlnDestino := Trim(QryCab.FieldByName('destino_ean_receptor').AsString);
    if Pos('-', LGlnDestino) > 0 then
      LGlnDestino := Trim(Copy(LGlnDestino, 1, Pos('-', LGlnDestino) - 1));

    NodeParties := NodeHeader.AddChild('PARTIES');
    NodeParties.AddChild('Qualifier').Text := 'DP';
    if LGlnDestino <> '' then
    begin
      NodeParties.AddChild('Identifier_type').Text := 'GLN';
      NodeParties.AddChild('Identifier').Text := LGlnDestino;
    end
    else
    begin
      NodeParties.AddChild('Identifier_type').Text := LIdType;
      NodeParties.AddChild('Identifier').Text := LIdVal;
    end;

    // 3. Documentos Relacionados (SUPPORTING_DOCS)
    // Type 220: Número de pedido del cliente
    NodeDocs := NodeHeader.AddChild('SUPPORTING_DOCS');
    NodeDocs.AddChild('Type').Text := '220';
    NodeDocs.AddChild('Identifier').Text := LNumPedido;

    // Type 351: Número de albarán
    NodeDocs := NodeHeader.AddChild('SUPPORTING_DOCS');
    NodeDocs.AddChild('Type').Text := '351';
    NodeDocs.AddChild('Identifier').Text := LAlbNum;

    // 4. Grupo Adicional (SITUADO ENTRE <SUPPORTING_DOCS> Y <PACKAGES>)
    // 4.1. Observaciones de Cabecera (si están informadas en observaciones1)
    var LObservaciones := Trim(QryCab.FieldByName('observaciones1').AsString);
    if LObservaciones <> '' then
    begin
      NodeGroupHeader := NodeHeader.AddChild('ADDITIONAL_GROUP_HEADER');
      NodeGroupHeader.AddChild('Name').Text := 'GENERAL';
      var NodeProp := NodeGroupHeader.AddChild('ADDITIONAL_GROUP_PROPERTY_HEADER');
      NodeProp.AddChild('Name').Text := 'Remarks';
      NodeProp.AddChild('Value').Text := LObservaciones;
    end;

    // 4.2. Total de líneas
    NodeGroupHeader := NodeHeader.AddChild('ADDITIONAL_GROUP_HEADER');
    NodeGroupHeader.AddChild('Name').Text := 'GENERAL';
    NodeGroupHeader.AddChild('ADDITIONAL_GROUP_PROPERTY_HEADER').AddChild('Name').Text := 'Total_lines';
    NodeGroupHeader.ChildNodes['ADDITIONAL_GROUP_PROPERTY_HEADER'].AddChild('Value').Text := IntToStr(LCountLines);

    // 5. Detalle de Embalaje y Líneas (PACKAGES) - Mercancía General por bultos
    // 5.1. Siempre incluir el paquete nivel 1 (Camión / Expedición)
    NodePackages := NodeHeader.AddChild('PACKAGES');
    NodePackages.AddChild('Package_ID').Text := '1';
    NodePackages.AddChild('Type').Text := '09';
    NodePackages.AddChild('Quantity').Text := '1';

    // 5.2. Cargar bultos individuales (cajas nivel 3 o unidades de transporte)
    QryBultos.SQL.Text :=
      'SELECT id, numero_bulto, bulto_padre_id, nivel, tipo_embalaje, sscc ' +
      'FROM ge_albaranes_bultos ' +
      'WHERE albaran_id = :alb AND (nivel = 3 OR (nivel = 2 AND id NOT IN (' +
      '  SELECT DISTINCT bulto_padre_id FROM ge_albaranes_bultos ' +
      '  WHERE albaran_id = :alb2 AND bulto_padre_id IS NOT NULL))) ' +
      'ORDER BY numero_bulto ASC, id ASC';
    QryBultos.ParamByName('alb').AsString := AlbaranId;
    QryBultos.ParamByName('alb2').AsString := AlbaranId;
    QryBultos.Open;

    LPkgSeq := 1;
    LFirstSscc := '';

    if not QryBultos.IsEmpty then
    begin
      while not QryBultos.Eof do
      begin
        LBultoId := QryBultos.FieldByName('id').AsInteger;
        Inc(LPkgSeq);

        NodePackages := NodeHeader.AddChild('PACKAGES');
        NodePackages.AddChild('Package_ID').Text := IntToStr(LPkgSeq);
        NodePackages.AddChild('Package_superior_level_ID').Text := '1';

        LSsccVal := ObtenerOGenerarSSCCBulto(LBultoId, QryBultos.FieldByName('sscc').AsString);
        if LSsccVal <> '' then
        begin
          NodePackages.AddChild('SSCC').Text := LSsccVal;
          if LFirstSscc = '' then
            LFirstSscc := LSsccVal;
        end;

        LTipoEmb := Trim(QryBultos.FieldByName('tipo_embalaje').AsString);
        if (LTipoEmb = '') or SameText(LTipoEmb, 'PX') or SameText(LTipoEmb, 'PE') then
          LTipoEmb := 'CT';
        NodePackages.AddChild('Type').Text := LTipoEmb;

        // Consultar contenidos detallados de este bulto en ge_albaranes_lineas_bultos
        QryContenido.Close;
        QryContenido.SQL.Text :=
          'SELECT lb.id, lb.cantidad, COALESCE(lb.lote, '''') AS lote, lb.fecha_caducidad, ' +
          '       al.articulo_id, COALESCE(al.descripcion, ''ARTICULO'') AS articulo_desc, ' +
          '       COALESCE(art.ean, al.edi_ean_enviado, '''') AS ean, ' +
          '       al.articulo_codigo ' +
          'FROM ge_albaranes_lineas_bultos lb ' +
          'INNER JOIN ge_albaranes_lineas al ON lb.linea_albaran_id = al.id ' +
          'LEFT JOIN ge_articulos art ON al.articulo_id = art.id ' +
          'WHERE lb.bulto_id = :bulto_id ' +
          'ORDER BY lb.id ASC';
        QryContenido.ParamByName('bulto_id').AsInteger := LBultoId;
        QryContenido.Open;

        LLineInPkg := 0;
        while not QryContenido.Eof do
        begin
          Inc(LLineInPkg);
          NodeLinePkg := NodePackages.AddChild('LINES_PACKAGES');
          NodeLinePkg.AddChild('Line_ID').Text := IntToStr(LLineInPkg);

          LEanVal := Trim(QryContenido.FieldByName('ean').AsString);
          if LEanVal = '' then
            LEanVal := Trim(QryContenido.FieldByName('articulo_codigo').AsString);
          if Length(LEanVal) = 12 then
            NodeLinePkg.AddChild('Identifier_type').Text := 'UPC'
          else
            NodeLinePkg.AddChild('Identifier_type').Text := 'EAN';
          NodeLinePkg.AddChild('Identifier').Text := LEanVal;

          NodeLinePkg.AddChild('Description').Text := QryContenido.FieldByName('articulo_desc').AsString;
          NodeLinePkg.AddChild('Quantity').Text := FloatToStrFormat(QryContenido.FieldByName('cantidad').AsFloat, 0);
          NodeLinePkg.AddChild('Packages_quantity').Text := '1';

          // Identificador adicional de embalaje (DUN-14 / EAN para ECI Gran Consumo)
          LArtId := QryContenido.FieldByName('articulo_id').AsInteger;
          LPackagingId := ObtenerIdentificadorEmbalaje(LClienteId, LArtId, LEanVal, AConn);
          if LPackagingId <> '' then
          begin
            NodeAddId := NodeLinePkg.AddChild('ADDITIONAL_IDENTIFIER_ITEM_LINES_PACKAGES');
            NodeAddId.AddChild('Identifier_type').Text := 'Packaging_unit_identifier';
            NodeAddId.AddChild('Identifier').Text := LPackagingId;
          end;

          LLote := QryContenido.FieldByName('lote').AsString;
          if LLote <> '' then
          begin
            NodeBatch := NodeLinePkg.AddChild('BATCH_LINES_PACKAGES');
            NodeBatch.AddChild('Batch_number').Text := LLote;
            if not QryContenido.FieldByName('fecha_caducidad').IsNull then
            begin
              LExpiryStr := FormatDateTime('yyyy-mm-dd"T"00:00:00', QryContenido.FieldByName('fecha_caducidad').AsDateTime);
              NodeBatch.AddChild('Expiry_date').Text := LExpiryStr;
            end;
          end;

          QryContenido.Next;
        end;

        QryBultos.Next;
      end;
    end
    else
    begin
      // Fallback si no existen bultos registrados en ge_albaranes_bultos:
      // Generar paquetes por bulto según la cantidad de bultos indicada en ge_albaranes_lineas
      QryDet.First;
      while not QryDet.Eof do
      begin
        LBultosCant := Trunc(QryDet.FieldByName('bultos').AsFloat);
        if LBultosCant <= 0 then LBultosCant := 1;
        LCantTotal := QryDet.FieldByName('cantidad').AsFloat;
        LBaseCant := Trunc(LCantTotal) div LBultosCant;
        LResto := Trunc(LCantTotal) mod LBultosCant;

        for LIdxBulto := 1 to LBultosCant do
        begin
          Inc(LPkgSeq);
          NodePackages := NodeHeader.AddChild('PACKAGES');
          NodePackages.AddChild('Package_ID').Text := IntToStr(LPkgSeq);
          NodePackages.AddChild('Package_superior_level_ID').Text := '1';

          LSsccVal := GenerarNuevoSSCC18;
          if LSsccVal <> '' then
          begin
            NodePackages.AddChild('SSCC').Text := LSsccVal;
            if LFirstSscc = '' then
              LFirstSscc := LSsccVal;
          end;

          NodePackages.AddChild('Type').Text := 'CT';

          NodeLinePkg := NodePackages.AddChild('LINES_PACKAGES');
          NodeLinePkg.AddChild('Line_ID').Text := '1';

          LEanVal := Trim(QryDet.FieldByName('articulo_ean').AsString);
          if LEanVal = '' then
            LEanVal := Trim(QryDet.FieldByName('articulo_codigo').AsString);
          if Length(LEanVal) = 12 then
            NodeLinePkg.AddChild('Identifier_type').Text := 'UPC'
          else
            NodeLinePkg.AddChild('Identifier_type').Text := 'EAN';
          NodeLinePkg.AddChild('Identifier').Text := LEanVal;

          NodeLinePkg.AddChild('Description').Text := QryDet.FieldByName('descripcion').AsString;

          if LIdxBulto <= LResto then
            LCantEstaCaja := LBaseCant + 1
          else
            LCantEstaCaja := LBaseCant;
          if (LCantEstaCaja = 0) and (LCantTotal > 0) then
            LCantEstaCaja := LCantTotal;

          NodeLinePkg.AddChild('Quantity').Text := FloatToStrFormat(LCantEstaCaja, 0);
          NodeLinePkg.AddChild('Packages_quantity').Text := '1';

          LArtId := QryDet.FieldByName('articulo_id').AsInteger;
          LPackagingId := ObtenerIdentificadorEmbalaje(LClienteId, LArtId, LEanVal, AConn);
          if LPackagingId <> '' then
          begin
            NodeAddId := NodeLinePkg.AddChild('ADDITIONAL_IDENTIFIER_ITEM_LINES_PACKAGES');
            NodeAddId.AddChild('Identifier_type').Text := 'Packaging_unit_identifier';
            NodeAddId.AddChild('Identifier').Text := LPackagingId;
          end;

          LLote := QryDet.FieldByName('lotes').AsString;
          if LLote <> '' then
          begin
            NodeBatch := NodeLinePkg.AddChild('BATCH_LINES_PACKAGES');
            NodeBatch.AddChild('Batch_number').Text := LLote;
            if not QryDet.FieldByName('fecha_caducidad').IsNull then
            begin
              LExpiryStr := FormatDateTime('yyyy-mm-dd"T"00:00:00', QryDet.FieldByName('fecha_caducidad').AsDateTime);
              NodeBatch.AddChild('Expiry_date').Text := LExpiryStr;
            end;
          end;
        end;

        QryDet.Next;
      end;
    end;

    // Persistir el primer SSCC en ge_albaranes si estaba vacío
    if (LFirstSscc <> '') and (Trim(QryCab.FieldByName('edi_numero_sscc').AsString) = '') then
    begin
      var QryAlbSscc := TFDQuery.Create(nil);
      try
        QryAlbSscc.Connection := AConn;
        QryAlbSscc.SQL.Text := 'UPDATE ge_albaranes SET edi_numero_sscc = :sscc WHERE id = :id AND (edi_numero_sscc IS NULL OR edi_numero_sscc = '''')';
        QryAlbSscc.ParamByName('sscc').AsString := LFirstSscc;
        QryAlbSscc.ParamByName('id').AsString := AlbaranId;
        QryAlbSscc.ExecSQL;
      finally
        QryAlbSscc.Free;
      end;
    end;

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

procedure ExportarAlbaranXmlEdicom(const AlbaranId: string; AConn: TFDConnection);
var
  LErrMsg, LXmlContent, LSerie, LNumero, LFileName: string;
  SaveDlg: TSaveDialog;
  StringList: TStringList;
  Qry: TFDQuery;
begin
  if not ValidarAlbaranEdicom(AlbaranId, AConn, LErrMsg) then
  begin
    MessageDlg(LErrMsg, mtWarning, [mbOK], 0);
    Exit;
  end;

  LXmlContent := GenerarXmlDesadv(AlbaranId, AConn);

  LSerie := '';
  LNumero := '';
  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := AConn;
    Qry.SQL.Text := 'SELECT serie, numero FROM ge_albaranes WHERE id = :id';
    Qry.ParamByName('id').AsString := AlbaranId;
    Qry.Open;
    if not Qry.IsEmpty then
    begin
      LSerie := Trim(Qry.FieldByName('serie').AsString);
      LNumero := Trim(Qry.FieldByName('numero').AsString);
    end;
  finally
    Qry.Free;
  end;

  if LSerie <> '' then
    LFileName := 'DESADV_' + LSerie + '_' + LNumero + '.xml'
  else if LNumero <> '' then
    LFileName := 'DESADV_' + LNumero + '.xml'
  else
    LFileName := 'DESADV_' + AlbaranId + '.xml';

  SaveDlg := TSaveDialog.Create(nil);
  StringList := TStringList.Create;
  try
    SaveDlg.Title := 'Guardar Albarán XML EDICOM (DESADV)';
    SaveDlg.Filter := 'Archivos XML (*.xml)|*.xml|Todos los archivos (*.*)|*.*';
    SaveDlg.DefaultExt := 'xml';
    SaveDlg.FileName := LFileName;

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

end.
