unit frm_AlbaranesEditor;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, System.UITypes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.Buttons, Vcl.Menus,
  Vcl.ComCtrls, Vcl.Grids, Vcl.DBGrids, Data.DB, FireDAC.Comp.Client, uAppTheme,
  FireDAC.Stan.Param, FireDAC.Stan.Intf, FireDAC.Stan.Option,
  FireDAC.Stan.Error, FireDAC.DatS, FireDAC.Phys.Intf, FireDAC.DApt.Intf,
  FireDAC.Stan.Async, FireDAC.DApt, FireDAC.Comp.DataSet, uBaseForm;

type
  TfrmAlbaranesEditor = class(TfrmBase)
    Label1: TLabel;
    edtNumero: TEdit;
    Label2: TLabel;
    dtpFecha: TDateTimePicker;
    Label3: TLabel;
    cbCliente: TComboBox;
    Label4: TLabel;
    cbSerie: TComboBox;
    lblTotal: TLabel;
    edtTotal: TEdit;
    dsLines: TDataSource;
    qryLines: TFDQuery;
    lblBase: TLabel;
    edtBase: TEdit;
    sgIvaDesglose: TStringGrid;
    lblDestino: TLabel;
    lblFactura: TLabel;
    lblFormaPago: TLabel;
    cbDestino: TComboBox;
    cbFormaPago: TComboBox;
    chkFacturado: TCheckBox;
    chkAnulado: TCheckBox;
    edtFactura: TEdit;
    pnlClienteInfo: TPanel;
    lblClienteInfo: TLabel;
    pnlDestinoInfo: TPanel;
    lblDestinoInfo: TLabel;
    lblImpuestos: TLabel;
    edtIva: TEdit;
    lbldesgloseiva: TLabel;
    btnBuscarCliente: TSpeedButton;
    btnNuevoCliente: TSpeedButton;
    btnBuscarDestino: TSpeedButton;
    btnNuevoDestino: TSpeedButton;
    lblObservaciones: TLabel;
    edtObservaciones1: TEdit;
    tsEdiInfo: TTabSheet;
    lblEdiEstadoEnvio: TLabel;
    lblEdiMessageId: TLabel;
    lblEdiEnviadoAt: TLabel;
    lblEdiRefPedidoCliente: TLabel;
    lblEdiNumeroSSCC: TLabel;
    edtEdiEstadoEnvio: TEdit;
    edtEdiMessageId: TEdit;
    edtEdiEnviadoAt: TEdit;
    edtEdiRefPedidoCliente: TEdit;
    edtEdiNumeroSSCC: TEdit;
    pmLineas: TPopupMenu;
    mnuTrazabilidadLote: TMenuItem;

    // Controles árbol de bultos EDI
    pnlEdiTreeContainer: TPanel;
    pnlEdiTreeToolbar: TPanel;
    btnEdiAddPalet: TButton;
    btnEdiScanEnvase: TButton;
    btnEdiDeleteBulto: TButton;
    btnEdiRefreshTree: TButton;
    btnEdiExportDesadv: TButton;
    btnEdiPrintBultosECI: TButton;
    btnEdiAddBultosECI: TButton;
    tvEdiBultos: TTreeView;

    procedure FormCreate(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure btnSaveClick(Sender: TObject);
    procedure btnAddLineClick(Sender: TObject);
    procedure btnDelLineClick(Sender: TObject);
    procedure btnSalirClick(Sender: TObject);
    procedure chkFacturadoClick(Sender: TObject);
    procedure dbgItemsDblClick(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure qryLinesAfterPost(DataSet: TDataSet);
    procedure qryLinesAfterDelete(DataSet: TDataSet);
    procedure qryLinesBeforePost(DataSet: TDataSet);
    procedure cbClienteChange(Sender: TObject);
    procedure cbDestinoChange(Sender: TObject);
    procedure edtFacturaDblClick(Sender: TObject);
    procedure cbSerieChange(Sender: TObject);
    procedure btnBuscarClienteClick(Sender: TObject);
    procedure btnNuevoClienteClick(Sender: TObject);
    procedure btnBuscarDestinoClick(Sender: TObject);
    procedure btnNuevoDestinoClick(Sender: TObject);
    procedure btnFacturarClick(Sender: TObject);
    procedure pmLineasPopup(Sender: TObject);
    procedure mnuTrazabilidadLoteClick(Sender: TObject);
    procedure btnDocsRelacionadosClick(Sender: TObject);
    procedure btnEdiAddPaletClick(Sender: TObject);
    procedure btnEdiScanEnvaseClick(Sender: TObject);
    procedure btnEdiDeleteBultoClick(Sender: TObject);
    procedure btnEdiRefreshTreeClick(Sender: TObject);
    procedure btnEdiExportDesadvClick(Sender: TObject);
    procedure btnEdiPrintBultosECIClick(Sender: TObject);
    procedure btnEdiAddBultosECIClick(Sender: TObject);
    procedure btnImprimirClick(Sender: TObject); override;
    procedure btnEliminarClick(Sender: TObject);
  protected
    procedure DoAnadir; override;
    procedure DoModificar; override;
    procedure DoPrimero; override;
    procedure DoAnterior; override;
    procedure DoSiguiente; override;
    procedure DoUltimo; override;
    procedure DoDuplicar; override;
    function IsNewRecord: Boolean; override;
  private
    { Private declarations }
    FAlbaranId: string;
    FClientIds: TStringList;
    FDestinationIds: TStringList;
    FFormaPagoIds: TStringList;
    FIdFactura: string;
    FTotalBase: Double;
    FTotalIva: Double;
    FTotalDoc: Double;
    FParentForm: TForm;
    FIsNewRecord: Boolean;
    FArbolEdiCargado: Boolean;

    procedure pgcDetailsChange(Sender: TObject);
    procedure CargarArbolBultosEdi;
    procedure LoadData;
    procedure LoadHeader;
    procedure LoadLines;
    procedure LoadClientes;
    procedure LoadFormasPago;
    procedure LoadDestinos(const AClienteId: string);
    procedure RecalculateTotal;
    procedure UpdateClienteDetails;
    procedure UpdateDestinoDetails;
    procedure qryLinesFieldChange(Sender: TField);
    procedure qryLinesBultosChange(Sender: TField);
    procedure qryLinesCantidadBultoChange(Sender: TField);
    procedure qryLinesBeforeDelete(DataSet: TDataSet);
    function LineaTieneSalidaBultos(const ALineId: Integer): Boolean;
    procedure LiberarReservaStock(AArticuloId: Integer; const ALoteCode: string; ACantidad: Double);
    procedure AumentarReservaStock(AArticuloId: Integer; const ALoteCode: string; ACantidad: Double);
    procedure SetupFieldChangeEvents;
    procedure SetupGridColumns;
    procedure SetupIvaPickList;
    procedure UpdateLockState;
    function ValidarYAsegurarDestino: Boolean;
  public
    { Public declarations }
    property AlbaranId: string read FAlbaranId write FAlbaranId;
    property ParentForm: TForm read FParentForm write FParentForm;
    function GenerarPaletSSCC(const AAlbaranId: Integer): string;
    function EscanearEnvaseDUN14(const AAlbaranId, APaletBultoNum: Integer; const ABarcode: string): Boolean;
  end;

var
  frmAlbaranesEditor: TfrmAlbaranesEditor;

implementation

uses
  dmg_Main, frm_SelectProduct, frm_SelectLote, uDbErrorHandler, System.Generics.Collections, System.Math, frm_Albaranes, frm_FacturasEditor,
  frm_ClientesEditor, frm_ClientesDestinosEditor, frm_SelectCliente, frm_SelectDestino, uValidacionFiscal, uGS1BarcodeParser, uEdicomDesadvExporter,
  frm_LotesCreator, frm_StockEnvasadoEditor, frm_DocumentosRelacionados, System.StrUtils, uAlbaranPrintService, uEtiquetaBultoECIService;

{$R *.dfm}

procedure TfrmAlbaranesEditor.FormCreate(Sender: TObject);
begin
  inherited;
  if not Assigned(FClientIds) then
    FClientIds := TStringList.Create;

  if not Assigned(FDestinationIds) then
    FDestinationIds := TStringList.Create;

  if not Assigned(FFormaPagoIds) then
    FFormaPagoIds := TStringList.Create;
end;

procedure TfrmAlbaranesEditor.FormShow(Sender: TObject);
var
  LIdx, LNumAuto: Integer;
  Qry: TFDQuery;
  LSerieAuto: string;
begin
  if not Assigned(FClientIds) then
    FClientIds := TStringList.Create;

  if not Assigned(FDestinationIds) then
    FDestinationIds := TStringList.Create;

  if not Assigned(FFormaPagoIds) then
    FFormaPagoIds := TStringList.Create;

  // Tema estándar
  TAppTheme.ApplyToForm(Self);

  // Vincular eventos a la barra superior heredada
  btnGuardar.OnClick := btnSaveClick;
  btnSalir.OnClick := btnSalirClick;
  btnNuevoArticulo.OnClick := btnAddLineClick;
  btnEliminaArticulo.OnClick := btnDelLineClick;
  btnIA.OnClick := btnDocsRelacionadosClick;

  // Vincular la grilla heredada y su menú contextual
  dbgItems.DataSource := dsLines;
  dbgItems.OnDblClick := dbgItemsDblClick;
  dbgItems.PopupMenu := pmLineas;

  // Ocultar botones de navegación/herramientas innecesarios
  btnCancelar.Visible := True;
  btnAnadir.Visible := True;
  btnEliminar.Visible := True;
  btnEliminar.OnClick := btnEliminarClick;
  btnPrimero.Visible := True;
  btnAnterior.Visible := True;
  btnSiguiente.Visible := True;
  btnUltimo.Visible := True;
  btnModificar.Visible := True;
  btnBuscar.Visible := False;
  btnImprimir.Visible := True;
  btnImprimir.OnClick := btnImprimirClick;
  btnIA.Visible := True;
  btnIA.Caption := 'Relacionados';
  btnIA.ImageName := 'icon_documentos_relacionados';
  btnIA.Width := 75;
  btnDuplicar.Visible := True;
  btnRect.Visible := True;
  btnRect.Caption := 'Facturar';
  btnRect.ImageName := 'icon_facturar';
  btnRect.Width := 65;
  btnRect.OnClick := btnFacturarClick;
  edtFactura.OnDblClick := edtFacturaDblClick;
  btnAyuda.Visible := False;
  btnBuscarArticulo.Visible := False;
  btnExcel.Visible := False;

  // Ocultar pestaña de listado heredada
  TsListado.TabVisible := False;
  pgcDetails.ActivePage := tsDatosEnvio;

  // Configurar columnas de la grilla heredada dbgItems
  SetupGridColumns;

  LoadClientes;
  LoadFormasPago;
  cbDestino.OnChange := cbDestinoChange;
  pgcDetails.OnChange := pgcDetailsChange;
  lblClienteInfo.Caption := '';
  lblDestinoInfo.Caption := '';

  qryLines.BeforePost := qryLinesBeforePost;
  qryLines.BeforeDelete := qryLinesBeforeDelete;

  // Configurar sgIvaDesglose
  sgIvaDesglose.ColCount := 3;
  sgIvaDesglose.RowCount := 2;
  sgIvaDesglose.FixedRows := 1;
  sgIvaDesglose.FixedCols := 0;
  sgIvaDesglose.Cells[0, 0] := '% IVA';
  sgIvaDesglose.Cells[1, 0] := 'Base Imponible';
  sgIvaDesglose.Cells[2, 0] := 'Cuota IVA';
  sgIvaDesglose.ColWidths[0] := 80;
  sgIvaDesglose.ColWidths[1] := 160;
  sgIvaDesglose.ColWidths[2] := 160;

  // Cargar series reales en el combo una sola vez
  if cbSerie.Items.Count = 0 then
  begin
    cbSerie.Items.BeginUpdate;
    try
      Qry := TFDQuery.Create(nil);
      try
        Qry.Connection := dmgMain.dbConn;
        Qry.SQL.Text := 'SELECT DISTINCT serie FROM ge_series WHERE activo = 1 AND empresa_id = :emp ORDER BY por_defecto DESC, serie ASC';
        Qry.ParamByName('emp').AsString := dmgMain.CurrentCompanyId;
        Qry.Open;
        while not Qry.Eof do
        begin
          cbSerie.Items.Add(Qry.FieldByName('serie').AsString);
          Qry.Next;
        end;
      finally
        Qry.Free;
      end;
    finally
      cbSerie.Items.EndUpdate;
    end;
  end;

  FIsNewRecord := (FAlbaranId = '');

  if not FIsNewRecord then
    LoadData
  else
  begin
    dtpFecha.Date := Now;
    LSerieAuto := '';
    if Assigned(FParentForm) and (FParentForm is TfrmAlbaranes) and Assigned(TfrmAlbaranes(FParentForm).cbSeries) then
    begin
      if TfrmAlbaranes(FParentForm).cbSeries.ItemIndex > 0 then
        LSerieAuto := TfrmAlbaranes(FParentForm).cbSeries.Text;
    end;

    if dmgMain.GetSerieYNumeroDocumento(dmgMain.CurrentCompanyId, 'ALBARAN', dtpFecha.Date, LSerieAuto, LSerieAuto, LNumAuto) then
    begin
      LIdx := cbSerie.Items.IndexOf(LSerieAuto);
      if LIdx <> -1 then
        cbSerie.ItemIndex := LIdx
      else
      begin
        cbSerie.Items.Add(LSerieAuto);
        cbSerie.ItemIndex := cbSerie.Items.Count - 1;
      end;
      edtNumero.Text := IntToStr(LNumAuto);
    end
    else
    begin
      LIdx := -1;
      Qry := TFDQuery.Create(nil);
      try
        Qry.Connection := dmgMain.dbConn;
        Qry.SQL.Text := 'SELECT serie FROM ge_series WHERE activo = 1 AND empresa_id = :emp ORDER BY por_defecto DESC LIMIT 1';
        Qry.ParamByName('emp').AsString := dmgMain.CurrentCompanyId;
        Qry.Open;
        if not Qry.IsEmpty then
          LIdx := cbSerie.Items.IndexOf(Qry.FieldByName('serie').AsString);
      finally
        Qry.Free;
      end;

      if LIdx <> -1 then
        cbSerie.ItemIndex := LIdx
      else if cbSerie.Items.Count > 0 then
        cbSerie.ItemIndex := 0;

      if cbSerie.ItemIndex <> -1 then
        cbSerieChange(cbSerie)
      else
        edtNumero.Text := dmgMain.GetNextIDS('ge_albaranes');
    end;
    edtTotal.Text := '0,00';
    edtIva.Text := '0,00';
    edtBase.Text := '0,00';
    
    sgIvaDesglose.RowCount := 2;
    sgIvaDesglose.Cells[0, 1] := '';
    sgIvaDesglose.Cells[1, 1] := '';
    sgIvaDesglose.Cells[2, 1] := '';

    FTotalBase := 0;
    FTotalIva := 0;
    FTotalDoc := 0;
    
    // Preparar qryLines vacía (id=0 para que no devuelva registros)
    qryLines.Close;
    qryLines.SQL.Text := 'SELECT *, (total - base) as iva FROM ge_albaranes_lineas WHERE albaran_id = :id';
    qryLines.ParamByName('id').AsInteger := 0;
    qryLines.Open;
    SetupFieldChangeEvents;
  end;
  SetupIvaPickList;
  chkFacturado.OnClick := chkFacturadoClick;
  UpdateLockState;
end;

procedure TfrmAlbaranesEditor.btnSalirClick(Sender: TObject);
begin
  Close;
end;

procedure TfrmAlbaranesEditor.btnAddLineClick(Sender: TObject);
var
  i: Integer;
  LPrice, LIvaRate, LIva: Double;
begin
  if chkFacturado.Checked then
  begin
    MessageDlg('⚠️ El albarán ya ha sido FACTURADO. No se permite añadir nuevas líneas.', mtWarning, [mbOK], 0);
    Exit;
  end;

  if not Assigned(frmSelectProduct) then
    Application.CreateForm(TfrmSelectProduct, frmSelectProduct);
    
  frmSelectProduct.MultiSelect := True;
  
  if frmSelectProduct.ShowModal = mrOk then
  begin
    qryLines.DisableControls;
    try
      for i := 0 to frmSelectProduct.SelectedIds.Count - 1 do
      begin
        qryLines.Append;
        if FAlbaranId <> '' then
          qryLines.FieldByName('albaran_id').AsInteger := StrToIntDef(FAlbaranId, 0);
          
        qryLines.FieldByName('articulo_id').AsString := frmSelectProduct.SelectedIds[i];
        qryLines.FieldByName('articulo_codigo').AsString := frmSelectProduct.SelectedCodes[i];
        qryLines.FieldByName('descripcion').AsString := frmSelectProduct.SelectedDescs[i];
        
        LPrice := StrToFloatDef(frmSelectProduct.SelectedPrices[i], 0);
        if cbCliente.ItemIndex <> -1 then
          LPrice := dmgMain.GetPrecioArticuloCliente(FClientIds[cbCliente.ItemIndex], frmSelectProduct.SelectedIds[i], dtpFecha.Date, LPrice);
        LIvaRate := StrToFloatDef(frmSelectProduct.SelectedIvaRates[i], 0);
        
        qryLines.FieldByName('precio').AsFloat := LPrice;
        qryLines.FieldByName('cantidad').AsFloat := 1;
        qryLines.FieldByName('tipo_iva').AsFloat := LIvaRate;
        
        LIva := LPrice * (LIvaRate / 100.0);
        qryLines.FieldByName('base').AsFloat := LPrice;
        qryLines.FieldByName('total').AsFloat := LPrice + LIva;
        if qryLines.FindField('id_user_creator') <> nil then
          qryLines.FieldByName('id_user_creator').AsString := dmgMain.CurrentUserId;
        if qryLines.FindField('id_user_update') <> nil then
          qryLines.FieldByName('id_user_update').AsString := dmgMain.CurrentUserId;
        qryLines.Post;
      end;
    finally
      qryLines.EnableControls;
    end;
  end;
end;

function TfrmAlbaranesEditor.LineaTieneSalidaBultos(const ALineId: Integer): Boolean;
var
  Qry: TFDQuery;
begin
  Result := False;
  if ALineId <= 0 then Exit;

  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    Qry.SQL.Text := 'SELECT id FROM ge_albaranes_lineas_bultos WHERE linea_albaran_id = :lid LIMIT 1';
    Qry.ParamByName('lid').AsInteger := ALineId;
    Qry.Open;
    Result := not Qry.IsEmpty;
  finally
    Qry.Free;
  end;
end;

procedure TfrmAlbaranesEditor.btnDelLineClick(Sender: TObject);
var
  LLineId: Integer;
begin
  if qryLines.IsEmpty then Exit;

  if chkFacturado.Checked then
  begin
    MessageDlg('⚠️ El albarán ya ha sido FACTURADO. No se permite eliminar líneas.', mtWarning, [mbOK], 0);
    Exit;
  end;

  LLineId := qryLines.FieldByName('id').AsInteger;
  if (LLineId > 0) and LineaTieneSalidaBultos(LLineId) then
  begin
    MessageDlg('⚠️ No se puede eliminar esta línea del albarán porque ya tiene lecturas de bultos y salida de mercancía registrada en almacén.' + sLineBreak + sLineBreak +
               'Para poder eliminar esta línea, primero vacíe o elimine los bultos asociados en la pestaña EDI / Almacén.', mtWarning, [mbOK], 0);
    Exit;
  end;

  qryLines.Delete;
end;

procedure TfrmAlbaranesEditor.LiberarReservaStock(AArticuloId: Integer; const ALoteCode: string; ACantidad: Double);
var
  QryArt, QryStock: TFDQuery;
  LArtStockId, LStockId: Integer;
  LPend, LTomar, LReservada: Double;
  LLote: string;
begin
  if (AArticuloId <= 0) or (ACantidad <= 0.0001) then Exit;

  LLote := Trim(ALoteCode);
  LPend := ACantidad;

  QryArt := TFDQuery.Create(nil);
  QryStock := TFDQuery.Create(nil);
  try
    QryArt.Connection := dmgMain.dbConn;
    QryStock.Connection := dmgMain.dbConn;

    // 1. Obtener id_articulo real en ge_stock_envasado (considerando id_maestro)
    QryArt.SQL.Text := 'SELECT COALESCE(NULLIF(id_maestro, 0), id) AS id_stock FROM ge_articulos WHERE id = :art';
    QryArt.ParamByName('art').AsInteger := AArticuloId;
    QryArt.Open;
    if QryArt.IsEmpty then Exit;
    LArtStockId := QryArt.FieldByName('id_stock').AsInteger;

    // 2. Intentar liberar primero del lote específico
    if (LLote <> '') and (LPend > 0) then
    begin
      QryStock.SQL.Text :=
        'SELECT se.id, COALESCE(se.cantidad_reservada, 0) AS reservada ' +
        'FROM ge_stock_envasado se ' +
        'INNER JOIN ge_lotes l ON se.id_lote = l.id ' +
        'WHERE se.id_articulo = :art AND (l.codigo = :lote OR l.codigo_antiguo = :lote) ' +
        '  AND COALESCE(se.cantidad_reservada, 0) > 0 LIMIT 1';
      QryStock.ParamByName('art').AsInteger := LArtStockId;
      QryStock.ParamByName('lote').AsString := LLote;
      QryStock.Open;
      if not QryStock.IsEmpty then
      begin
        LStockId := QryStock.FieldByName('id').AsInteger;
        LReservada := QryStock.FieldByName('reservada').AsFloat;
        LTomar := Min(LPend, LReservada);
        if LTomar > 0 then
        begin
          dmgMain.qryExec.Close;
          dmgMain.qryExec.SQL.Text :=
            'UPDATE ge_stock_envasado ' +
            'SET cantidad_reservada = GREATEST(0, COALESCE(cantidad_reservada, 0) - :cant), ' +
            '    updated_at = NOW(), id_user_update = :user ' +
            'WHERE id = :sid';
          dmgMain.qryExec.ParamByName('cant').AsFloat  := LTomar;
          dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
          dmgMain.qryExec.ParamByName('sid').AsInteger := LStockId;
          dmgMain.qryExec.ExecSQL;

          LPend := LPend - LTomar;
        end;
      end;
    end;

    // 3. Si aún queda remanente, liberar por FIFO de cualquier registro con reserva del artículo
    if LPend > 0 then
    begin
      QryStock.Close;
      QryStock.SQL.Text :=
        'SELECT id, COALESCE(cantidad_reservada, 0) AS reservada ' +
        'FROM ge_stock_envasado ' +
        'WHERE id_articulo = :art AND COALESCE(cantidad_reservada, 0) > 0 ' +
        'ORDER BY id ASC';
      QryStock.ParamByName('art').AsInteger := LArtStockId;
      QryStock.Open;
      while (not QryStock.Eof) and (LPend > 0) do
      begin
        LStockId := QryStock.FieldByName('id').AsInteger;
        LReservada := QryStock.FieldByName('reservada').AsFloat;
        LTomar := Min(LPend, LReservada);
        if LTomar > 0 then
        begin
          dmgMain.qryExec.Close;
          dmgMain.qryExec.SQL.Text :=
            'UPDATE ge_stock_envasado ' +
            'SET cantidad_reservada = GREATEST(0, COALESCE(cantidad_reservada, 0) - :cant), ' +
            '    updated_at = NOW(), id_user_update = :user ' +
            'WHERE id = :sid';
          dmgMain.qryExec.ParamByName('cant').AsFloat  := LTomar;
          dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
          dmgMain.qryExec.ParamByName('sid').AsInteger := LStockId;
          dmgMain.qryExec.ExecSQL;

          LPend := LPend - LTomar;
        end;
        QryStock.Next;
      end;
    end;
  finally
    QryArt.Free;
    QryStock.Free;
  end;
end;

procedure TfrmAlbaranesEditor.AumentarReservaStock(AArticuloId: Integer; const ALoteCode: string; ACantidad: Double);
var
  QryArt, QryStock: TFDQuery;
  LArtStockId, LStockId: Integer;
  LPend, LTomar, LDisponible: Double;
  LLote: string;
begin
  if (AArticuloId <= 0) or (ACantidad <= 0.0001) then Exit;

  LLote := Trim(ALoteCode);
  LPend := ACantidad;

  QryArt := TFDQuery.Create(nil);
  QryStock := TFDQuery.Create(nil);
  try
    QryArt.Connection := dmgMain.dbConn;
    QryStock.Connection := dmgMain.dbConn;

    // 1. Obtener id_articulo real en ge_stock_envasado (considerando id_maestro)
    QryArt.SQL.Text := 'SELECT COALESCE(NULLIF(id_maestro, 0), id) AS id_stock FROM ge_articulos WHERE id = :art';
    QryArt.ParamByName('art').AsInteger := AArticuloId;
    QryArt.Open;
    if QryArt.IsEmpty then Exit;
    LArtStockId := QryArt.FieldByName('id_stock').AsInteger;

    // 2. Intentar reservar primero del lote específico si se indicó
    if (LLote <> '') and (LPend > 0) then
    begin
      QryStock.SQL.Text :=
        'SELECT se.id, GREATEST(0, se.cantidad_unidades - COALESCE(se.cantidad_reservada, 0)) AS disponible ' +
        'FROM ge_stock_envasado se ' +
        'INNER JOIN ge_lotes l ON se.id_lote = l.id ' +
        'WHERE se.id_articulo = :art AND (l.codigo = :lote OR l.codigo_antiguo = :lote) ' +
        '  AND (se.cantidad_unidades - COALESCE(se.cantidad_reservada, 0)) > 0 LIMIT 1';
      QryStock.ParamByName('art').AsInteger := LArtStockId;
      QryStock.ParamByName('lote').AsString := LLote;
      QryStock.Open;
      if not QryStock.IsEmpty then
      begin
        LStockId := QryStock.FieldByName('id').AsInteger;
        LDisponible := QryStock.FieldByName('disponible').AsFloat;
        LTomar := Min(LPend, LDisponible);
        if LTomar > 0 then
        begin
          dmgMain.qryExec.Close;
          dmgMain.qryExec.SQL.Text :=
            'UPDATE ge_stock_envasado ' +
            'SET cantidad_reservada = COALESCE(cantidad_reservada, 0) + :cant, ' +
            '    updated_at = NOW(), id_user_update = :user ' +
            'WHERE id = :sid';
          dmgMain.qryExec.ParamByName('cant').AsFloat  := LTomar;
          dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
          dmgMain.qryExec.ParamByName('sid').AsInteger := LStockId;
          dmgMain.qryExec.ExecSQL;

          LPend := LPend - LTomar;
        end;
      end;
    end;

    // 3. Si aún queda cantidad por reservar, reservar por FIFO en existencias disponibles del artículo
    if LPend > 0 then
    begin
      QryStock.Close;
      QryStock.SQL.Text :=
        'SELECT se.id, GREATEST(0, se.cantidad_unidades - COALESCE(se.cantidad_reservada, 0)) AS disponible ' +
        'FROM ge_stock_envasado se ' +
        'LEFT JOIN ge_lotes l ON se.id_lote = l.id ' +
        'WHERE se.id_articulo = :art AND (se.cantidad_unidades - COALESCE(se.cantidad_reservada, 0)) > 0 ' +
        'ORDER BY l.fecha ASC, l.id ASC, se.id ASC';
      QryStock.ParamByName('art').AsInteger := LArtStockId;
      QryStock.Open;
      while (not QryStock.Eof) and (LPend > 0) do
      begin
        LStockId := QryStock.FieldByName('id').AsInteger;
        LDisponible := QryStock.FieldByName('disponible').AsFloat;
        LTomar := Min(LPend, LDisponible);
        if LTomar > 0 then
        begin
          dmgMain.qryExec.Close;
          dmgMain.qryExec.SQL.Text :=
            'UPDATE ge_stock_envasado ' +
            'SET cantidad_reservada = COALESCE(cantidad_reservada, 0) + :cant, ' +
            '    updated_at = NOW(), id_user_update = :user ' +
            'WHERE id = :sid';
          dmgMain.qryExec.ParamByName('cant').AsFloat  := LTomar;
          dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
          dmgMain.qryExec.ParamByName('sid').AsInteger := LStockId;
          dmgMain.qryExec.ExecSQL;

          LPend := LPend - LTomar;
        end;
        QryStock.Next;
      end;
    end;
  finally
    QryArt.Free;
    QryStock.Free;
  end;
end;

procedure TfrmAlbaranesEditor.qryLinesBeforeDelete(DataSet: TDataSet);
var
  LLineId, LArtId: Integer;
  LLote: string;
  LCant: Double;
begin
  if chkFacturado.Checked then
  begin
    MessageDlg('⚠️ El albarán ya ha sido FACTURADO. No se permite eliminar líneas.', mtWarning, [mbOK], 0);
    Abort;
  end;

  LLineId := DataSet.FieldByName('id').AsInteger;
  if (LLineId > 0) and LineaTieneSalidaBultos(LLineId) then
  begin
    MessageDlg('⚠️ No se puede eliminar esta línea del albarán porque ya tiene lecturas de bultos y salida de mercancía registrada en almacén.' + sLineBreak + sLineBreak +
               'Para poder eliminar esta línea, primero vacíe o elimine los bultos asociados en la pestaña EDI / Almacén.', mtWarning, [mbOK], 0);
    Abort;
  end;

  LArtId := DataSet.FieldByName('articulo_id').AsInteger;
  LLote := Trim(DataSet.FieldByName('lotes').AsString);
  LCant := DataSet.FieldByName('cantidad').AsFloat;

  if (LArtId > 0) and (LCant > 0) then
    LiberarReservaStock(LArtId, LLote, LCant);
end;

procedure TfrmAlbaranesEditor.qryLinesAfterDelete(DataSet: TDataSet);
begin
  RecalculateTotal;
end;

procedure TfrmAlbaranesEditor.qryLinesAfterPost(DataSet: TDataSet);
begin
  RecalculateTotal;
end;

procedure TfrmAlbaranesEditor.qryLinesBeforePost(DataSet: TDataSet);
var
  LPrecio, LCantidad, LTipoIva, LSubtotal, LPorcDto, LDtoAmt, LBase, LIva, LTotal: Double;
  LLineId, LOldArtId, LNewArtId: Integer;
  LOldArt, LCurArt, LOldCant, LCurCant, LOldLote, LCurLote: Variant;
  LOldLoteStr, LNewLoteStr: string;
  LOldCantVal, LNewCantVal: Double;
begin
  if chkFacturado.Checked then
  begin
    MessageDlg('⚠️ El albarán ya ha sido FACTURADO. No se permite modificar precios, cantidades ni líneas.', mtWarning, [mbOK], 0);
    Abort;
  end;

  LLineId := DataSet.FieldByName('id').AsInteger;

  // Bloquear modificación de líneas existentes con bultos ya escaneados en almacén
  if (DataSet.State = dsEdit) and (LLineId > 0) and LineaTieneSalidaBultos(LLineId) then
  begin
    LOldArt := DataSet.FieldByName('articulo_id').OldValue;
    LCurArt := DataSet.FieldByName('articulo_id').Value;
    LOldCant := DataSet.FieldByName('cantidad').OldValue;
    LCurCant := DataSet.FieldByName('cantidad').Value;

    if (not VarIsNull(LOldArt) and not VarIsNull(LCurArt) and (LOldArt <> LCurArt)) or
       (not VarIsNull(LOldCant) and not VarIsNull(LCurCant) and (Abs(Double(LOldCant) - Double(LCurCant)) > 0.001)) then
    begin
      MessageDlg('⚠️ No se pueden modificar las unidades ni el artículo de esta línea porque ya dispone de salidas de mercancía / bultos escaneados en almacén.' + sLineBreak + sLineBreak +
                 'Para realizar cambios en esta línea, primero elimine o vacíe los bultos escaneados desde el editor de preparación de pedidos.', mtWarning, [mbOK], 0);
      Abort;
    end;
  end;

  // Control y ajuste de reservas de stock al modificar o añadir líneas
  if DataSet.State = dsEdit then
  begin
    LOldArt := DataSet.FieldByName('articulo_id').OldValue;
    LOldLote := DataSet.FieldByName('lotes').OldValue;
    LOldCant := DataSet.FieldByName('cantidad').OldValue;

    if not VarIsNull(LOldArt) then LOldArtId := Integer(LOldArt) else LOldArtId := 0;
    if not VarIsNull(LOldLote) then LOldLoteStr := Trim(VarToStr(LOldLote)) else LOldLoteStr := '';
    if not VarIsNull(LOldCant) then LOldCantVal := Double(LOldCant) else LOldCantVal := 0;

    LNewArtId := DataSet.FieldByName('articulo_id').AsInteger;
    LNewLoteStr := Trim(DataSet.FieldByName('lotes').AsString);
    LNewCantVal := DataSet.FieldByName('cantidad').AsFloat;

    if (LOldArtId <> LNewArtId) or (LOldLoteStr <> LNewLoteStr) or (Abs(LOldCantVal - LNewCantVal) > 0.0001) then
    begin
      // Liberar la reserva previa si existía
      if (LOldArtId > 0) and (LOldCantVal > 0) then
        LiberarReservaStock(LOldArtId, LOldLoteStr, LOldCantVal);

      // Aplicar la nueva reserva
      if (LNewArtId > 0) and (LNewCantVal > 0) then
        AumentarReservaStock(LNewArtId, LNewLoteStr, LNewCantVal);
    end;
  end
  else if DataSet.State = dsInsert then
  begin
    LNewArtId := DataSet.FieldByName('articulo_id').AsInteger;
    LNewLoteStr := Trim(DataSet.FieldByName('lotes').AsString);
    LNewCantVal := DataSet.FieldByName('cantidad').AsFloat;

    if (LNewArtId > 0) and (LNewCantVal > 0) then
      AumentarReservaStock(LNewArtId, LNewLoteStr, LNewCantVal);
  end;

  if DataSet.FieldByName('albaran_id').IsNull or (DataSet.FieldByName('albaran_id').AsInteger = 0) then
    if FAlbaranId <> '' then
      DataSet.FieldByName('albaran_id').AsInteger := StrToIntDef(FAlbaranId, 0);

  LPrecio := DataSet.FieldByName('precio').AsFloat;
  LCantidad := DataSet.FieldByName('cantidad').AsFloat;
  LTipoIva := DataSet.FieldByName('tipo_iva').AsFloat;
  LPorcDto := DataSet.FieldByName('descuento_porcentaje').AsFloat;
  LDtoAmt := DataSet.FieldByName('descuento_precio').AsFloat;
  
  LSubtotal := LPrecio * LCantidad;
  
  // Si hay porcentaje de descuento, calcular el importe del descuento
  if LPorcDto <> 0 then
  begin
    LDtoAmt := LSubtotal * (LPorcDto / 100.0);
    DataSet.FieldByName('descuento_precio').AsFloat := LDtoAmt;
  end;
  
  LBase := LSubtotal - LDtoAmt;
  LIva := LBase * (LTipoIva / 100.0);
  LTotal := LBase + LIva;
  
  DataSet.FieldByName('base').AsFloat := LBase;
  DataSet.FieldByName('total').AsFloat := LTotal;
end;

procedure TfrmAlbaranesEditor.btnSaveClick(Sender: TObject);
var
  LIsNew: Boolean;
  LSerieAuto: string;
  LNumAuto, LIdx: Integer;
begin
  if chkFacturado.Checked or (FIdFactura <> '') then
  begin
    MessageDlg('⚠️ Este albarán ya ha sido FACTURADO. No se puede modificar ni guardar.', mtWarning, [mbOK], 0);
    Exit;
  end;

  if (cbCliente.ItemIndex = -1) then
  begin
    ShowMessage('Debe seleccionar un cliente.');
    Exit;
  end;

  if not ValidarYAsegurarDestino then
    Exit;

  // Comprobar si la fecha del documento se encuentra fuera del rango de la serie actual
  if dmgMain.GetSerieYNumeroDocumento(dmgMain.CurrentCompanyId, 'ALBARAN', dtpFecha.Date, cbSerie.Text, LSerieAuto, LNumAuto) then
  begin
    if not SameText(cbSerie.Text, LSerieAuto) then
    begin
      LIdx := cbSerie.Items.IndexOf(LSerieAuto);
      if LIdx <> -1 then
        cbSerie.ItemIndex := LIdx
      else
      begin
        cbSerie.Items.Add(LSerieAuto);
        cbSerie.ItemIndex := cbSerie.Items.Count - 1;
      end;
      edtNumero.Text := IntToStr(LNumAuto);
    end;
  end;

  LIsNew := FIsNewRecord;
  dmgMain.qryExec.Close;
  
  if LIsNew then
  begin
    dmgMain.qryExec.SQL.Text := 
      'INSERT INTO ge_albaranes (empresa_id, cliente_id, clientedestino_id, serie, numero, fecha, observaciones1, base_imponible, iva, total, facturado, anulado, ' +
      'edi_referencia_pedido_cliente, edi_numero_sscc, id_forma_pago, id_user_creator, id_user_update) ' +
      'VALUES (:emp, :cli, :dest, :ser, :num, :fec, :obs1, :base, :iva, :tot, :fact, :anul, :edi_ref, :edi_sscc, :id_forma_pago, :user, :user)';
  end
  else
  begin
    dmgMain.qryExec.SQL.Text := 
      'UPDATE ge_albaranes SET cliente_id = :cli, clientedestino_id = :dest, serie = :ser, numero = :num, ' +
      'fecha = :fec, observaciones1 = :obs1, base_imponible = :base, iva = :iva, total = :tot, facturado = :fact, anulado = :anul, ' +
      'edi_referencia_pedido_cliente = :edi_ref, edi_numero_sscc = :edi_sscc, id_forma_pago = :id_forma_pago, id_user_update = :user ' +
      'WHERE id = :id';
    dmgMain.qryExec.ParamByName('id').AsInteger := StrToIntDef(FAlbaranId, 0);
  end;
  if LIsNew then
    dmgMain.qryExec.ParamByName('emp').AsString := dmgMain.CurrentCompanyId;
  dmgMain.qryExec.ParamByName('cli').AsString := FClientIds[cbCliente.ItemIndex];
  
  dmgMain.qryExec.ParamByName('dest').DataType := ftString;
  if cbDestino.ItemIndex <> -1 then
    dmgMain.qryExec.ParamByName('dest').AsString := FDestinationIds[cbDestino.ItemIndex]
  else
    dmgMain.qryExec.ParamByName('dest').Clear;

  dmgMain.qryExec.ParamByName('ser').AsString := cbSerie.Text;
  dmgMain.qryExec.ParamByName('num').AsInteger := StrToIntDef(edtNumero.Text, 0);
  dmgMain.qryExec.ParamByName('fec').AsDateTime := dtpFecha.DateTime;

  dmgMain.qryExec.ParamByName('obs1').DataType := ftString;
  if Trim(edtObservaciones1.Text) <> '' then
    dmgMain.qryExec.ParamByName('obs1').AsString := Trim(edtObservaciones1.Text)
  else
    dmgMain.qryExec.ParamByName('obs1').Clear;

  dmgMain.qryExec.ParamByName('base').AsFloat := FTotalBase;
  dmgMain.qryExec.ParamByName('iva').AsFloat := FTotalIva;
  dmgMain.qryExec.ParamByName('tot').AsFloat := FTotalDoc;
  dmgMain.qryExec.ParamByName('fact').AsBoolean := chkFacturado.Checked;
  dmgMain.qryExec.ParamByName('anul').AsBoolean := chkAnulado.Checked;
  
  dmgMain.qryExec.ParamByName('edi_ref').DataType := ftString;
  if Trim(edtEdiRefPedidoCliente.Text) <> '' then
    dmgMain.qryExec.ParamByName('edi_ref').AsString := Trim(edtEdiRefPedidoCliente.Text)
  else
    dmgMain.qryExec.ParamByName('edi_ref').Clear;

  dmgMain.qryExec.ParamByName('edi_sscc').DataType := ftString;
  if Trim(edtEdiNumeroSSCC.Text) <> '' then
    dmgMain.qryExec.ParamByName('edi_sscc').AsString := Trim(edtEdiNumeroSSCC.Text)
  else
    dmgMain.qryExec.ParamByName('edi_sscc').Clear;

  dmgMain.qryExec.ParamByName('id_forma_pago').DataType := ftInteger;
  if (cbFormaPago.ItemIndex > 0) and Assigned(FFormaPagoIds) and (cbFormaPago.ItemIndex < FFormaPagoIds.Count) and (FFormaPagoIds[cbFormaPago.ItemIndex] <> '') then
    dmgMain.qryExec.ParamByName('id_forma_pago').AsInteger := StrToIntDef(FFormaPagoIds[cbFormaPago.ItemIndex], 0)
  else
    dmgMain.qryExec.ParamByName('id_forma_pago').Clear;

  dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
  
  try
    dmgMain.qryExec.ExecSQL;

    if LIsNew then
    begin
      // Obtener el ID auto-generado
      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text := 'SELECT LAST_INSERT_ID() as new_id';
      dmgMain.qryExec.Open;
      FAlbaranId := dmgMain.qryExec.FieldByName('new_id').AsString;
      dmgMain.qryExec.Close;

      // Incrementar el contador en la tabla de series
      dmgMain.qryExec.SQL.Text := 'UPDATE ge_series SET sig_numero_albaran = sig_numero_albaran + 1 ' +
                                  'WHERE activo = 1 AND empresa_id = :emp AND serie = :ser';
      dmgMain.qryExec.ParamByName('emp').AsString := dmgMain.CurrentCompanyId;
      dmgMain.qryExec.ParamByName('ser').AsString := cbSerie.Text;
      dmgMain.qryExec.ExecSQL;

      qryLines.First;
      while not qryLines.Eof do
      begin
        if qryLines.FieldByName('albaran_id').IsNull or (qryLines.FieldByName('albaran_id').AsInteger = 0) then
        begin
          qryLines.Edit;
          qryLines.FieldByName('albaran_id').AsInteger := StrToIntDef(FAlbaranId, 0);
          qryLines.Post;
        end;
        qryLines.Next;
      end;
    end;
    
    FIsNewRecord := False;
    ResetChangeTracking;
    ResetModifiedState;
    if Assigned(FParentForm) and (FParentForm is TfrmAlbaranes) then
      TfrmAlbaranes(FParentForm).btnRefreshClick(nil);
    ShowMessage('Albarán guardado correctamente.');
  except
    on E: Exception do
      TDbErrorHandler.HandleException(E, dmgMain.qryExec.SQL.Text);
  end;
end;

procedure TfrmAlbaranesEditor.RecalculateTotal;
var
  LBase, LIva, LTotal: Double;
  LBookmark: TBookmark;
  LVatBases: TDictionary<Double, Double>;
  LVatAmounts: TDictionary<Double, Double>;
  LRate: Double;
  LKey: Double;
  LLineBase: Double;
  LRow: Integer;
begin
  LBase := 0;
  LIva := 0;
  LTotal := 0;
  LVatBases := TDictionary<Double, Double>.Create;
  LVatAmounts := TDictionary<Double, Double>.Create;
  try
    LBookmark := qryLines.GetBookmark;
    qryLines.DisableControls;
    try
      qryLines.First;
      while not qryLines.Eof do
      begin
        LLineBase := (qryLines.FieldByName('cantidad').AsFloat * qryLines.FieldByName('precio').AsFloat) - qryLines.FieldByName('descuento_precio').AsFloat;
        LBase := LBase + LLineBase;
        LIva := LIva + qryLines.FieldByName('iva').AsFloat;
        LTotal := LTotal + qryLines.FieldByName('total').AsFloat;
        
        LRate := qryLines.FieldByName('tipo_iva').AsFloat;
        if LVatBases.ContainsKey(LRate) then
        begin
          LVatBases[LRate] := LVatBases[LRate] + LLineBase;
          LVatAmounts[LRate] := LVatAmounts[LRate] + qryLines.FieldByName('iva').AsFloat;
        end
        else
        begin
          LVatBases.Add(LRate, LLineBase);
          LVatAmounts.Add(LRate, qryLines.FieldByName('iva').AsFloat);
        end;
        qryLines.Next;
      end;
    finally
      qryLines.GotoBookmark(LBookmark);
      qryLines.EnableControls;
    end;

    // Rellenar sgIvaDesglose
    sgIvaDesglose.RowCount := 2;
    sgIvaDesglose.Cells[0, 1] := '';
    sgIvaDesglose.Cells[1, 1] := '';
    sgIvaDesglose.Cells[2, 1] := '';

    LRow := 1;
    for LKey in LVatAmounts.Keys do
    begin
      if LRow >= sgIvaDesglose.RowCount then
        sgIvaDesglose.RowCount := LRow + 1;
      sgIvaDesglose.Cells[0, LRow] := FormatFloat('0.#', LKey) + '%';
      sgIvaDesglose.Cells[1, LRow] := FormatFloat('#,##0.00', LVatBases[LKey]);
      sgIvaDesglose.Cells[2, LRow] := FormatFloat('#,##0.00', LVatAmounts[LKey]);
      Inc(LRow);
    end;
    
    FTotalBase := LBase;
    FTotalIva := LIva;
    FTotalDoc := LTotal;

    edtBase.Text := FormatFloat('#,##0.00', LBase);
    edtIva.Text := FormatFloat('#,##0.00', LIva);
    edtTotal.Text := FormatFloat('#,##0.00', LTotal);

    // Actualizar automáticamente los totales en la cabecera ge_albaranes si el documento ya está guardado
    if (FAlbaranId <> '') and not FIsNewRecord then
    begin
      var LQryUpd := TFDQuery.Create(nil);
      try
        LQryUpd.Connection := dmgMain.dbConn;
        LQryUpd.SQL.Text :=
          'UPDATE ge_albaranes ' +
          'SET base_imponible = :base, iva = :iva, total = :tot ' +
          'WHERE id = :id';
        LQryUpd.ParamByName('base').AsFloat := FTotalBase;
        LQryUpd.ParamByName('iva').AsFloat := FTotalIva;
        LQryUpd.ParamByName('tot').AsFloat := FTotalDoc;
        LQryUpd.ParamByName('id').AsInteger := StrToIntDef(FAlbaranId, 0);
        LQryUpd.ExecSQL;
      finally
        LQryUpd.Free;
      end;
      
      if Assigned(FParentForm) and (FParentForm is TfrmAlbaranes) then
        TfrmAlbaranes(FParentForm).btnRefreshClick(nil);
    end;
  finally
    LVatBases.Free;
    LVatAmounts.Free;
  end;
end;


procedure TfrmAlbaranesEditor.pgcDetailsChange(Sender: TObject);
begin
  if (pgcDetails.ActivePage = tsEdiInfo) and not FArbolEdiCargado then
    CargarArbolBultosEdi;
end;

procedure TfrmAlbaranesEditor.CargarArbolBultosEdi;
var
  QryBultos, QryContenido: TFDQuery;
  LAlbId: Integer;
  NodePalet, NodeEnvase, NodeContenido, ParentNode: TTreeNode;
  LBultoMap: TDictionary<Integer, TTreeNode>;
  LBultoId, LPadreId, LNivel, LNumBulto: Integer;
  LSscc, LEan, LLote, LCadStr, LText: string;
begin
  FArbolEdiCargado := True;
  tvEdiBultos.Items.BeginUpdate;
  LBultoMap := TDictionary<Integer, TTreeNode>.Create;
  try
    tvEdiBultos.Items.Clear;
    LAlbId := StrToIntDef(FAlbaranId, 0);
    if LAlbId <= 0 then Exit;

    QryBultos := TFDQuery.Create(nil);
    QryContenido := TFDQuery.Create(nil);
    try
      QryBultos.Connection := dmgMain.dbConn;
      QryContenido.Connection := dmgMain.dbConn;

      // 1. Cargar TODOS los bultos del albarán en 1 sola consulta plana ordenada por nivel
      QryBultos.SQL.Text :=
        'SELECT id, albaran_id, nivel, bulto_padre_id, sscc, tipo_embalaje, numero_bulto ' +
        'FROM ge_albaranes_bultos ' +
        'WHERE albaran_id = :alb ' +
        'ORDER BY nivel ASC, id ASC';
      QryBultos.ParamByName('alb').AsInteger := LAlbId;
      QryBultos.Open;

      while not QryBultos.Eof do
      begin
        LBultoId := QryBultos.FieldByName('id').AsInteger;
        LNivel := QryBultos.FieldByName('nivel').AsInteger;
        LNumBulto := QryBultos.FieldByName('numero_bulto').AsInteger;
        LSscc := QryBultos.FieldByName('sscc').AsString;

        ParentNode := nil;
        if not QryBultos.FieldByName('bulto_padre_id').IsNull then
        begin
          LPadreId := QryBultos.FieldByName('bulto_padre_id').AsInteger;
          LBultoMap.TryGetValue(LPadreId, ParentNode);
        end;

        if LNivel = 2 then
        begin
          LText := Format('📦 Palet #%d — SSCC: %s (Tipo: %s)', [
            LNumBulto, LSscc,
            QryBultos.FieldByName('tipo_embalaje').AsString
          ]);
          NodePalet := tvEdiBultos.Items.AddChild(nil, LText);
          NodePalet.Data := TObject(IntPtr(LBultoId));
          LBultoMap.AddOrSetValue(LBultoId, NodePalet);
          NodePalet.Expand(True);
        end;

        if LNivel = 3 then
        begin
          if LSscc <> '' then
            LText := Format('📥 Bulto/Caja #%d — SSCC: %s (Tipo: %s)', [
              LNumBulto, LSscc,
              QryBultos.FieldByName('tipo_embalaje').AsString
            ])
          else
            LText := Format('📥 Envase/Caja #%d — Tipo: %s', [
              LNumBulto,
              QryBultos.FieldByName('tipo_embalaje').AsString
            ]);
          NodeEnvase := tvEdiBultos.Items.AddChild(ParentNode, LText);
          NodeEnvase.Data := TObject(IntPtr(LBultoId));
          LBultoMap.AddOrSetValue(LBultoId, NodeEnvase);
        end;

        QryBultos.Next;
      end;

      // 2. Cargar TODO el contenido de líneas del albarán en 1 sola consulta
      QryContenido.SQL.Text :=
        'SELECT lb.bulto_id, lb.cantidad, lb.lote, lb.fecha_caducidad, ' +
        '       al.articulo_id, al.descripcion AS art_desc, ' +
        '       COALESCE(art.ean, al.edi_ean_enviado, '''') AS ean ' +
        '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 al.albaran_id = :alb ' +
        'ORDER BY lb.id ASC';
      QryContenido.ParamByName('alb').AsInteger := LAlbId;
      QryContenido.Open;

      while not QryContenido.Eof do
      begin
        LBultoId := QryContenido.FieldByName('bulto_id').AsInteger;
        if LBultoMap.TryGetValue(LBultoId, ParentNode) then
        begin
          LEan := QryContenido.FieldByName('ean').AsString;
          LLote := QryContenido.FieldByName('lote').AsString;
          LCadStr := '—';
          if not QryContenido.FieldByName('fecha_caducidad').IsNull then
            LCadStr := FormatDateTime('dd/mm/yyyy', QryContenido.FieldByName('fecha_caducidad').AsDateTime);

          LText := Format('🏷️ Art. #%d (%s) | EAN: %s | Lote: %s | Cad: %s | Cant: %s uds', [
            QryContenido.FieldByName('articulo_id').AsInteger,
            QryContenido.FieldByName('art_desc').AsString,
            LEan, LLote, LCadStr,
            FormatFloat('0.##', QryContenido.FieldByName('cantidad').AsFloat)
          ]);
          NodeContenido := tvEdiBultos.Items.AddChild(ParentNode, LText);
          NodeContenido.Data := TObject(IntPtr(LBultoId));
        end;

        QryContenido.Next;
      end;

    finally
      QryBultos.Free;
      QryContenido.Free;
    end;
  finally
    LBultoMap.Free;
    tvEdiBultos.Items.EndUpdate;
  end;
end;

procedure TfrmAlbaranesEditor.btnEdiAddPaletClick(Sender: TObject);
var
  LAlbId: Integer;
  LSSCC: string;
begin
  LAlbId := StrToIntDef(FAlbaranId, 0);
  if LAlbId <= 0 then
  begin
    MessageDlg('Debe guardar el albarán antes de generar palets SSCC.', mtWarning, [mbOK], 0);
    Exit;
  end;

  LSSCC := GenerarPaletSSCC(LAlbId);
  if LSSCC <> '' then
  begin
    edtEdiNumeroSSCC.Text := LSSCC;
    CargarArbolBultosEdi;
    MessageDlg('Se ha generado el Palet SSCC ' + LSSCC + ' correctamente.', mtInformation, [mbOK], 0);
  end;
end;

procedure TfrmAlbaranesEditor.btnEdiScanEnvaseClick(Sender: TObject);
var
  LAlbId, LPaletBultoId: Integer;
  LSelectedNode: TTreeNode;
  LBarcode: string;
begin
  LAlbId := StrToIntDef(FAlbaranId, 0);
  if LAlbId <= 0 then
  begin
    MessageDlg('Debe guardar el albarán antes de escanear envases.', mtWarning, [mbOK], 0);
    Exit;
  end;

  LSelectedNode := tvEdiBultos.Selected;
  if not Assigned(LSelectedNode) then
  begin
    MessageDlg('Por favor, seleccione en el árbol el Palet (SSCC) en el que desea añadir el envase.', mtWarning, [mbOK], 0);
    Exit;
  end;

  while (LSelectedNode.Parent <> nil) do
    LSelectedNode := LSelectedNode.Parent;

  LPaletBultoId := Integer(IntPtr(LSelectedNode.Data));
  if LPaletBultoId <= 0 then
  begin
    MessageDlg('Palet seleccionado no válido.', mtError, [mbOK], 0);
    Exit;
  end;

  LBarcode := '';
  if InputQuery('Escanear Envase (DUN-14 / GS1-128)',
                'Introduzca o escanee el código de barras DUN-14/GS1-128 del envase/caja:', LBarcode) then
  begin
    if Trim(LBarcode) <> '' then
    begin
      if EscanearEnvaseDUN14(LAlbId, LPaletBultoId, Trim(LBarcode)) then
      begin
        CargarArbolBultosEdi;
        MessageDlg('Envase/Caja registrado y asociado al Palet correctamente.', mtInformation, [mbOK], 0);
      end
      else
        MessageDlg('No se pudo decodificar la etiqueta de embalaje o no se encontró el artículo coincidente.', mtError, [mbOK], 0);
    end;
  end;
end;

procedure TfrmAlbaranesEditor.btnEdiDeleteBultoClick(Sender: TObject);
var
  LAlbId, LBultoId: Integer;
  LSelectedNode: TTreeNode;
  Qry: TFDQuery;
begin
  LAlbId := StrToIntDef(FAlbaranId, 0);
  LSelectedNode := tvEdiBultos.Selected;
  if (LAlbId <= 0) or not Assigned(LSelectedNode) then
  begin
    MessageDlg('Seleccione un elemento del árbol para eliminar.', mtWarning, [mbOK], 0);
    Exit;
  end;

  LBultoId := Integer(IntPtr(LSelectedNode.Data));
  if LBultoId <= 0 then Exit;

  if MessageDlg('¿Está seguro de que desea eliminar el bulto seleccionado y su contenido?',
                 mtConfirmation, [mbYes, mbNo], 0) = mrYes then
  begin
    Qry := TFDQuery.Create(nil);
    try
      Qry.Connection := dmgMain.dbConn;
      Qry.SQL.Text :=
        'DELETE FROM ge_albaranes_lineas_bultos ' +
        'WHERE bulto_id = :id OR bulto_id IN (SELECT id FROM ge_albaranes_bultos WHERE bulto_padre_id = :id)';
      Qry.ParamByName('id').AsInteger := LBultoId;
      Qry.ExecSQL;

      Qry.SQL.Text := 'DELETE FROM ge_albaranes_bultos WHERE id = :id OR bulto_padre_id = :id';
      Qry.ParamByName('id').AsInteger := LBultoId;
      Qry.ExecSQL;
    finally
      Qry.Free;
    end;

    CargarArbolBultosEdi;
  end;
end;

procedure TfrmAlbaranesEditor.btnEdiRefreshTreeClick(Sender: TObject);
begin
  CargarArbolBultosEdi;
end;

procedure TfrmAlbaranesEditor.btnEdiExportDesadvClick(Sender: TObject);
begin
  if (FAlbaranId = '') then
  begin
    MessageDlg('Guarde primero los cambios del albarán antes de generar el fichero DESADV.', mtWarning, [mbOK], 0);
    Exit;
  end;

  ExportarAlbaranXmlEdicom(FAlbaranId, dmgMain.dbConn);
  LoadHeader;
end;

procedure TfrmAlbaranesEditor.btnEdiPrintBultosECIClick(Sender: TObject);
begin
  if (FAlbaranId = '') or (StrToIntDef(FAlbaranId, 0) <= 0) then
  begin
    MessageDlg('Guarde primero los cambios del albarán antes de imprimir etiquetas.', mtWarning, [mbOK], 0);
    Exit;
  end;

  TEtiquetaBultoECIService.ImprimirEtiquetasBultoAlbaran(Self, FAlbaranId, True);
  CargarArbolBultosEdi;
  LoadHeader;
end;

procedure TfrmAlbaranesEditor.btnEdiAddBultosECIClick(Sender: TObject);
var
  LCount: Integer;
begin
  if (FAlbaranId = '') or (StrToIntDef(FAlbaranId, 0) <= 0) then
  begin
    MessageDlg('Guarde primero los cambios del albarán antes de generar bultos.', mtWarning, [mbOK], 0);
    Exit;
  end;

  LCount := TEtiquetaBultoECIService.AsegurarBultosAlbaran(FAlbaranId, dmgMain.dbConn);
  CargarArbolBultosEdi;
  LoadHeader;
  MessageDlg(Format('Se han generado/verificado %d bultos con sus códigos SSCC-18 para este albarán.', [LCount]),
    mtInformation, [mbOK], 0);
end;

procedure TfrmAlbaranesEditor.LoadData;
begin
  LoadHeader;
  LoadLines;
  RecalculateTotal;
end;

procedure TfrmAlbaranesEditor.LoadHeader;
var
  LClienteId, LDestinoId: string;
begin
  dmgMain.qryExec.Close;
  dmgMain.qryExec.SQL.Text := 
    'SELECT a.*, f.numero as factura_numero ' +
    'FROM ge_albaranes a ' +
    'LEFT JOIN ge_facturas f ON a.id_factura = f.id ' +
    'WHERE a.id = :id';
  dmgMain.qryExec.ParamByName('id').AsString := FAlbaranId;
  dmgMain.qryExec.Open;
  
  if not dmgMain.qryExec.IsEmpty then
  begin
    edtNumero.Text := dmgMain.qryExec.FieldByName('numero').AsString;
    cbSerie.ItemIndex := cbSerie.Items.IndexOf(dmgMain.qryExec.FieldByName('serie').AsString);
    if cbSerie.ItemIndex = -1 then
    begin
      cbSerie.Items.Add(dmgMain.qryExec.FieldByName('serie').AsString);
      cbSerie.ItemIndex := cbSerie.Items.Count - 1;
    end;
    dtpFecha.Date := dmgMain.qryExec.FieldByName('fecha').AsDateTime;
    edtTotal.Text := FormatFloat('#,##0.00', dmgMain.qryExec.FieldByName('total').AsFloat);
    
    LClienteId := TdmgMain.CleanUUID(dmgMain.qryExec.FieldByName('cliente_id').AsString);
    cbCliente.ItemIndex := FClientIds.IndexOf(LClienteId);
    if (cbCliente.ItemIndex = -1) and (LClienteId <> '') then
    begin
      var LQryCli := TFDQuery.Create(nil);
      try
        LQryCli.Connection := dmgMain.dbConn;
        LQryCli.SQL.Text := 'SELECT nombre_fiscal FROM ge_clientes WHERE id = :id';
        LQryCli.ParamByName('id').AsString := LClienteId;
        LQryCli.Open;
        if not LQryCli.IsEmpty then
        begin
          cbCliente.Items.Add(LQryCli.FieldByName('nombre_fiscal').AsString);
          FClientIds.Add(LClienteId);
          cbCliente.ItemIndex := cbCliente.Items.Count - 1;
        end;
      finally
        LQryCli.Free;
      end;
    end;
    
    LDestinoId := TdmgMain.CleanUUID(dmgMain.qryExec.FieldByName('clientedestino_id').AsString);
    chkFacturado.Checked := dmgMain.qryExec.FieldByName('facturado').AsInteger <> 0;
    chkAnulado.Checked := dmgMain.qryExec.FieldByName('anulado').AsInteger <> 0;
    FIdFactura := TdmgMain.CleanUUID(dmgMain.qryExec.FieldByName('id_factura').AsString);
    if not dmgMain.qryExec.FieldByName('factura_numero').IsNull then
      edtFactura.Text := dmgMain.qryExec.FieldByName('factura_numero').AsString
    else
      edtFactura.Text := '';

    // Cargar datos EDI
    edtEdiEstadoEnvio.Text := dmgMain.qryExec.FieldByName('edi_estado_envio').AsString;
    edtEdiMessageId.Text := dmgMain.qryExec.FieldByName('edi_message_id').AsString;
    if not dmgMain.qryExec.FieldByName('edi_enviado_at').IsNull then
      edtEdiEnviadoAt.Text := DateTimeToStr(dmgMain.qryExec.FieldByName('edi_enviado_at').AsDateTime)
    else
      edtEdiEnviadoAt.Text := '';
    edtEdiRefPedidoCliente.Text := dmgMain.qryExec.FieldByName('edi_referencia_pedido_cliente').AsString;
    if (Trim(edtEdiRefPedidoCliente.Text) = '') and not dmgMain.qryExec.FieldByName('id_pedido').IsNull then
    begin
      var LQryPed := TFDQuery.Create(nil);
      try
        LQryPed.Connection := dmgMain.dbConn;
        LQryPed.SQL.Text := 'SELECT COALESCE(numero_pedido, '''') AS num_ped FROM ge_pedidos WHERE id = :id';
        LQryPed.ParamByName('id').AsString := dmgMain.qryExec.FieldByName('id_pedido').AsString;
        LQryPed.Open;
        if not LQryPed.IsEmpty then
          edtEdiRefPedidoCliente.Text := LQryPed.FieldByName('num_ped').AsString;
      finally
        LQryPed.Free;
      end;
    end;
    edtEdiNumeroSSCC.Text := dmgMain.qryExec.FieldByName('edi_numero_sscc').AsString;

    if not dmgMain.qryExec.FieldByName('observaciones1').IsNull then
      edtObservaciones1.Text := dmgMain.qryExec.FieldByName('observaciones1').AsString
    else
      edtObservaciones1.Text := '';

    if dmgMain.qryExec.FindField('id_forma_pago') <> nil then
    begin
      var LFormaPagoId := dmgMain.qryExec.FieldByName('id_forma_pago').AsString;
      if (LFormaPagoId <> '') and Assigned(FFormaPagoIds) then
      begin
        var LIdxFP := FFormaPagoIds.IndexOf(LFormaPagoId);
        if LIdxFP >= 0 then
          cbFormaPago.ItemIndex := LIdxFP
        else
          cbFormaPago.ItemIndex := 0;
      end
      else
        cbFormaPago.ItemIndex := 0;
    end;
      
    // Cargar destinos para este cliente
    LoadDestinos(LClienteId);
    cbDestino.ItemIndex := FDestinationIds.IndexOf(LDestinoId);
    UpdateClienteDetails;
    UpdateDestinoDetails;

    // Actualizar estado de bloqueo por albarán facturado
    UpdateLockState;

    // Marcamos para cargar el árbol EDI perezosamente al cambiar a la pestaña EDI
    FArbolEdiCargado := False;
  end;
  dmgMain.qryExec.Close;
end;

procedure TfrmAlbaranesEditor.LoadLines;
begin
  qryLines.DisableControls;
  try
    qryLines.Close;
    qryLines.SQL.Text := 'SELECT *, (total - base) as iva FROM ge_albaranes_lineas WHERE albaran_id = :id ORDER BY posicion';
    qryLines.ParamByName('id').AsString := FAlbaranId;
    qryLines.Open;
    SetupFieldChangeEvents;
    SetupGridColumns;
  finally
    qryLines.EnableControls;
  end;
end;

procedure TfrmAlbaranesEditor.SetupGridColumns;
begin
  dbgItems.Columns.Clear;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'articulo_codigo';
    Title.Caption := 'Ref. Artículo';
    Width := 90;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'descripcion';
    Title.Caption := 'Descripción';
    Width := 250;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'lotes';
    Title.Caption := 'Lote';
    Width := 90;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'fecha_caducidad';
    Title.Caption := 'F. Caducidad';
    Width := 85;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'bultos';
    Title.Caption := 'Bultos';
    Width := 55;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'cantidad_bulto';
    Title.Caption := 'Uds. Bulto';
    Width := 70;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'cantidad';
    Title.Caption := 'Cant.';
    Width := 50;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'precio';
    Title.Caption := 'Precio';
    Width := 75;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'descuento_porcentaje';
    Title.Caption := '% Dto.';
    Width := 55;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'descuento_precio';
    Title.Caption := 'Dto. Importe';
    Width := 75;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'tipo_iva';
    Title.Caption := '% IVA';
    Width := 55;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'total';
    Title.Caption := 'Total';
    Width := 90;
  end;
end;

procedure TfrmAlbaranesEditor.LoadClientes;
begin
  if Assigned(FClientIds) and (FClientIds.Count > 0) and (cbCliente.Items.Count > 0) then Exit;

  cbCliente.Items.BeginUpdate;
  try
    cbCliente.Items.Clear;
    if not Assigned(FClientIds) then
      FClientIds := TStringList.Create
    else
      FClientIds.Clear;

    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text := 'SELECT id, nombre_fiscal FROM ge_clientes WHERE activo = true ORDER BY nombre_fiscal';
    dmgMain.qryExec.Open;
    while not dmgMain.qryExec.Eof do
    begin
      cbCliente.Items.Add(dmgMain.qryExec.FieldByName('nombre_fiscal').AsString);
      FClientIds.Add(TdmgMain.CleanUUID(dmgMain.qryExec.FieldByName('id').AsString));
      dmgMain.qryExec.Next;
    end;
    dmgMain.qryExec.Close;
  finally
    cbCliente.Items.EndUpdate;
  end;
end;

procedure TfrmAlbaranesEditor.LoadFormasPago;
var
  Qry: TFDQuery;
begin
  if not Assigned(FFormaPagoIds) then
    FFormaPagoIds := TStringList.Create
  else
    FFormaPagoIds.Clear;

  cbFormaPago.Items.Clear;
  cbFormaPago.Items.Add('[Sin Forma de Pago]');
  FFormaPagoIds.Add('');

  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    Qry.SQL.Text := 'SELECT id, descripcion FROM ge_formas_pago WHERE activo = 1 ORDER BY descripcion';
    Qry.Open;
    while not Qry.Eof do
    begin
      cbFormaPago.Items.Add(Qry.FieldByName('descripcion').AsString);
      FFormaPagoIds.Add(Qry.FieldByName('id').AsString);
      Qry.Next;
    end;
  finally
    Qry.Free;
  end;
  cbFormaPago.ItemIndex := 0;
end;

procedure TfrmAlbaranesEditor.FormDestroy(Sender: TObject);
begin
  if Assigned(FClientIds) then
    FreeAndNil(FClientIds);
  if Assigned(FDestinationIds) then
    FreeAndNil(FDestinationIds);
  if Assigned(FFormaPagoIds) then
    FreeAndNil(FFormaPagoIds);
end;

procedure TfrmAlbaranesEditor.cbClienteChange(Sender: TObject);
var
  LQryCli: TFDQuery;
  LFormaPagoId: string;
  LIdxFP: Integer;
begin
  if cbCliente.ItemIndex <> -1 then
  begin
    LoadDestinos(FClientIds[cbCliente.ItemIndex]);

    // Auto-seleccionar forma de pago predeterminada del cliente
    LQryCli := TFDQuery.Create(nil);
    try
      LQryCli.Connection := dmgMain.dbConn;
      LQryCli.SQL.Text := 'SELECT id_forma_pago FROM ge_clientes WHERE id = :id';
      LQryCli.ParamByName('id').AsString := FClientIds[cbCliente.ItemIndex];
      LQryCli.Open;
      if not LQryCli.IsEmpty then
      begin
        LFormaPagoId := LQryCli.FieldByName('id_forma_pago').AsString;
        if (LFormaPagoId <> '') and Assigned(FFormaPagoIds) then
        begin
          LIdxFP := FFormaPagoIds.IndexOf(LFormaPagoId);
          if LIdxFP >= 0 then
            cbFormaPago.ItemIndex := LIdxFP;
        end;
      end;
    finally
      LQryCli.Free;
    end;
  end
  else
  begin
    cbDestino.Items.Clear;
    if Assigned(FDestinationIds) then
      FDestinationIds.Clear;
  end;
  UpdateClienteDetails;
  UpdateDestinoDetails;
end;

procedure TfrmAlbaranesEditor.edtFacturaDblClick(Sender: TObject);
begin
  if (FIdFactura <> '') or chkFacturado.Checked then
  begin
    if FIdFactura <> '' then
    begin
      if not Assigned(frmFacturasEditor) then
        Application.CreateForm(TfrmFacturasEditor, frmFacturasEditor);
      frmFacturasEditor.ParentForm := Self;
      frmFacturasEditor.FacturaId := FIdFactura;
      frmFacturasEditor.ShowModal;
    end
    else
      btnFacturarClick(Sender);
  end
  else
    btnFacturarClick(Sender);
end;

procedure TfrmAlbaranesEditor.LoadDestinos(const AClienteId: string);
begin
  cbDestino.Items.Clear;
  if not Assigned(FDestinationIds) then
    FDestinationIds := TStringList.Create
  else
    FDestinationIds.Clear;
  
  if AClienteId = '' then Exit;
  
  dmgMain.qryExec.Close;
  dmgMain.qryExec.SQL.Text := 'SELECT id, descripcion FROM ge_clientes_destinos WHERE id_cliente = :cli AND activo = true ORDER BY descripcion';
  dmgMain.qryExec.ParamByName('cli').AsString := AClienteId;
  dmgMain.qryExec.Open;
  while not dmgMain.qryExec.Eof do
  begin
    cbDestino.Items.Add(dmgMain.qryExec.FieldByName('descripcion').AsString);
    FDestinationIds.Add(TdmgMain.CleanUUID(dmgMain.qryExec.FieldByName('id').AsString));
    dmgMain.qryExec.Next;
  end;
  dmgMain.qryExec.Close;
end;

procedure TfrmAlbaranesEditor.cbDestinoChange(Sender: TObject);
begin
  UpdateDestinoDetails;
end;

procedure TfrmAlbaranesEditor.UpdateClienteDetails;
var
  LClienteId: string;
begin
  if cbCliente.ItemIndex = -1 then
  begin
    lblClienteInfo.Caption := '';
    Exit;
  end;

  LClienteId := FClientIds[cbCliente.ItemIndex];
  dmgMain.qryExec.Close;
  dmgMain.qryExec.SQL.Text := 'SELECT nif, domicilio, telefono, poblacion, codigo_postal, provincia FROM ge_clientes WHERE id = :id';
  dmgMain.qryExec.ParamByName('id').AsString := LClienteId;
  dmgMain.qryExec.Open;
  if not dmgMain.qryExec.IsEmpty then
  begin
    lblClienteInfo.Caption := 
      'NIF: ' + dmgMain.qryExec.FieldByName('nif').AsString + ' | Tel: ' + dmgMain.qryExec.FieldByName('telefono').AsString + sLineBreak +
      'Dir: ' + dmgMain.qryExec.FieldByName('domicilio').AsString + sLineBreak +
      'Pob: ' + dmgMain.qryExec.FieldByName('poblacion').AsString + ' (' + dmgMain.qryExec.FieldByName('codigo_postal').AsString + ') - ' + dmgMain.qryExec.FieldByName('provincia').AsString;
  end
  else
    lblClienteInfo.Caption := '';
  dmgMain.qryExec.Close;
end;

procedure TfrmAlbaranesEditor.UpdateDestinoDetails;
var
  LDestinoId: string;
begin
  if cbDestino.ItemIndex = -1 then
  begin
    lblDestinoInfo.Caption := '';
    Exit;
  end;

  LDestinoId := FDestinationIds[cbDestino.ItemIndex];
  dmgMain.qryExec.Close;
  dmgMain.qryExec.SQL.Text := 'SELECT direccion, poblacion, cp, provincia FROM ge_clientes_destinos WHERE id = :id';
  dmgMain.qryExec.ParamByName('id').AsString := LDestinoId;
  dmgMain.qryExec.Open;
  if not dmgMain.qryExec.IsEmpty then
  begin
    lblDestinoInfo.Caption := 
      'Dir: ' + dmgMain.qryExec.FieldByName('direccion').AsString + sLineBreak +
      'Pob: ' + dmgMain.qryExec.FieldByName('poblacion').AsString + ' (' + dmgMain.qryExec.FieldByName('cp').AsString + ') - ' + dmgMain.qryExec.FieldByName('provincia').AsString;
  end
  else
    lblDestinoInfo.Caption := '';
  dmgMain.qryExec.Close;
end;

procedure TfrmAlbaranesEditor.SetupIvaPickList;
var
  I: Integer;
  LCol: TColumn;
begin
  LCol := nil;
  for I := 0 to dbgItems.Columns.Count - 1 do
  begin
    if SameText(dbgItems.Columns[I].FieldName, 'tipo_iva') then
    begin
      LCol := dbgItems.Columns[I];
      Break;
    end;
  end;
  
  if Assigned(LCol) then
  begin
    if LCol.PickList.Count > 0 then Exit; // Ya cargado en memoria, no requiere round-trip remoto

    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text := 'SELECT tipo FROM ge_tipos_iva WHERE activo = true ORDER BY tipo';
    dmgMain.qryExec.Open;
    LCol.PickList.Clear;
    while not dmgMain.qryExec.Eof do
    begin
      LCol.PickList.Add(FormatFloat('0.##', dmgMain.qryExec.FieldByName('tipo').AsFloat));
      dmgMain.qryExec.Next;
    end;
    dmgMain.qryExec.Close;
  end;
end;

procedure TfrmAlbaranesEditor.SetupFieldChangeEvents;
begin
  if qryLines.FindField('articulo_codigo') <> nil then
    qryLines.FieldByName('articulo_codigo').OnChange := qryLinesFieldChange;
  if qryLines.FindField('precio') <> nil then
    qryLines.FieldByName('precio').OnChange := qryLinesFieldChange;
  if qryLines.FindField('cantidad') <> nil then
    qryLines.FieldByName('cantidad').OnChange := qryLinesFieldChange;
  if qryLines.FindField('tipo_iva') <> nil then
    qryLines.FieldByName('tipo_iva').OnChange := qryLinesFieldChange;
  if qryLines.FindField('descuento_porcentaje') <> nil then
    qryLines.FieldByName('descuento_porcentaje').OnChange := qryLinesFieldChange;
  if qryLines.FindField('descuento_precio') <> nil then
    qryLines.FieldByName('descuento_precio').OnChange := qryLinesFieldChange;
  if qryLines.FindField('bultos') <> nil then
    qryLines.FieldByName('bultos').OnChange := qryLinesBultosChange;
  if qryLines.FindField('cantidad_bulto') <> nil then
    qryLines.FieldByName('cantidad_bulto').OnChange := qryLinesCantidadBultoChange;
end;

procedure TfrmAlbaranesEditor.qryLinesFieldChange(Sender: TField);
var
  LPrecio, LCantidad, LTipoIva, LSubtotal, LPorcDto, LDtoAmt, LBase, LIva, LTotal: Double;
  Qry: TFDQuery;
  LCodeStr, LClienteId: string;
  LArtId: Integer;
begin
  if not (qryLines.State in [dsEdit, dsInsert]) then Exit;
  
  if qryLines.FindField('articulo_codigo') <> nil then
    qryLines.FieldByName('articulo_codigo').OnChange := nil;
  if qryLines.FindField('precio') <> nil then
    qryLines.FieldByName('precio').OnChange := nil;
  if qryLines.FindField('cantidad') <> nil then
    qryLines.FieldByName('cantidad').OnChange := nil;
  if qryLines.FindField('tipo_iva') <> nil then
    qryLines.FieldByName('tipo_iva').OnChange := nil;
  if qryLines.FindField('descuento_porcentaje') <> nil then
    qryLines.FieldByName('descuento_porcentaje').OnChange := nil;
  if qryLines.FindField('descuento_precio') <> nil then
    qryLines.FieldByName('descuento_precio').OnChange := nil;

  try
    if SameText(Sender.FieldName, 'articulo_codigo') then
    begin
      LCodeStr := Trim(Sender.AsString);
      if LCodeStr <> '' then
      begin
        Qry := TFDQuery.Create(nil);
        try
          Qry.Connection := dmgMain.dbConn;
          Qry.SQL.Text :=
            'SELECT a.id, a.descripcion, COALESCE(a.pvp1, 0) AS pvp1, COALESCE(t.tipo, 21.00) as tipo_iva ' +
            'FROM ge_articulos a ' +
            'LEFT JOIN ge_tipos_iva t ON a.id_tipo_iva = t.id ' +
            'WHERE (CAST(a.id AS CHAR) = :code OR a.ean = :code OR EXISTS (SELECT 1 FROM ge_articulos_ean ean WHERE ean.articulo_id = a.id AND ean.ean = :code)) AND a.activo = 1 LIMIT 1';
          Qry.ParamByName('code').AsString := LCodeStr;
          Qry.Open;

          if not Qry.IsEmpty then
          begin
            LArtId := Qry.FieldByName('id').AsInteger;
            qryLines.FieldByName('articulo_id').AsInteger := LArtId;
            qryLines.FieldByName('articulo_codigo').AsString := Qry.FieldByName('id').AsString;
            qryLines.FieldByName('descripcion').AsString := Qry.FieldByName('descripcion').AsString;

            LPrecio := Qry.FieldByName('pvp1').AsFloat;
            LTipoIva := Qry.FieldByName('tipo_iva').AsFloat;

            LCantidad := qryLines.FieldByName('cantidad').AsFloat;
            if LCantidad <= 0 then LCantidad := 1.0;

            LClienteId := '';
            if (cbCliente.ItemIndex <> -1) and (FClientIds.Count > cbCliente.ItemIndex) then
              LClienteId := FClientIds[cbCliente.ItemIndex];

            // Jerarquía de precios: 1) Grupo -> 2) Cliente Especial -> 3) PVP1 Tarifa
            if (LClienteId <> '') and (LArtId > 0) then
              LPrecio := dmgMain.GetPrecioArticuloCliente(LClienteId, IntToStr(LArtId), dtpFecha.Date, LPrecio, LCantidad);

            qryLines.FieldByName('precio').AsFloat := LPrecio;
            qryLines.FieldByName('tipo_iva').AsFloat := LTipoIva;
          end;
        finally
          Qry.Free;
        end;
      end;
    end;

    LPrecio := qryLines.FieldByName('precio').AsFloat;
    LCantidad := qryLines.FieldByName('cantidad').AsFloat;
    LTipoIva := qryLines.FieldByName('tipo_iva').AsFloat;
    
    if SameText(Sender.FieldName, 'descuento_porcentaje') then
    begin
      LPorcDto := qryLines.FieldByName('descuento_porcentaje').AsFloat;
      LDtoAmt := (LPrecio * LCantidad) * (LPorcDto / 100.0);
      qryLines.FieldByName('descuento_precio').AsFloat := LDtoAmt;
    end
    else if SameText(Sender.FieldName, 'descuento_precio') then
    begin
      LDtoAmt := qryLines.FieldByName('descuento_precio').AsFloat;
      if (LPrecio * LCantidad) <> 0 then
        qryLines.FieldByName('descuento_porcentaje').AsFloat := (LDtoAmt / (LPrecio * LCantidad)) * 100.0
      else
        qryLines.FieldByName('descuento_porcentaje').AsFloat := 0;
    end
    else
    begin
      LPorcDto := qryLines.FieldByName('descuento_porcentaje').AsFloat;
      LDtoAmt := qryLines.FieldByName('descuento_precio').AsFloat;
    end;

    LSubtotal := LPrecio * LCantidad;
    LBase := LSubtotal - LDtoAmt;
    LIva := LBase * (LTipoIva / 100.0);
    LTotal := LBase + LIva;

    qryLines.FieldByName('base').AsFloat := LBase;
    qryLines.FieldByName('total').AsFloat := LTotal;
    
    if qryLines.FindField('iva') <> nil then
      qryLines.FieldByName('iva').AsFloat := LIva;
  finally
    SetupFieldChangeEvents;
  end;
end;

procedure TfrmAlbaranesEditor.qryLinesBultosChange(Sender: TField);
begin
  if qryLines.State in [dsEdit, dsInsert] then
  begin
    qryLines.FieldByName('cantidad').OnChange := nil;
    try
      qryLines.FieldByName('cantidad').AsFloat := Sender.AsFloat * qryLines.FieldByName('cantidad_bulto').AsFloat;
    finally
      qryLines.FieldByName('cantidad').OnChange := qryLinesFieldChange;
    end;
  end;
end;

procedure TfrmAlbaranesEditor.qryLinesCantidadBultoChange(Sender: TField);
begin
  if qryLines.State in [dsEdit, dsInsert] then
  begin
    qryLines.FieldByName('cantidad').OnChange := nil;
    try
      qryLines.FieldByName('cantidad').AsFloat := qryLines.FieldByName('bultos').AsFloat * Sender.AsFloat;
    finally
      qryLines.FieldByName('cantidad').OnChange := qryLinesFieldChange;
    end;
  end;
end;

{ --- Virtual Navigation / Action Overrides --- }

procedure TfrmAlbaranesEditor.cbSerieChange(Sender: TObject);
var
  Qry: TFDQuery;
begin
  if not FIsNewRecord then Exit;

  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    Qry.SQL.Text := 'SELECT sig_numero_albaran FROM ge_series WHERE activo = 1 AND empresa_id = :emp AND serie = :ser';
    Qry.ParamByName('emp').AsString := dmgMain.CurrentCompanyId;
    Qry.ParamByName('ser').AsString := cbSerie.Text;
    Qry.Open;
    if not Qry.IsEmpty then
      edtNumero.Text := Qry.FieldByName('sig_numero_albaran').AsString
    else
      edtNumero.Text := '1';
  finally
    Qry.Free;
  end;
end;

procedure TfrmAlbaranesEditor.DoAnadir;
var
  LInitialSerie, LSerieAuto: string;
  LNumAuto, LIdx: Integer;
  Qry: TFDQuery;
begin
  FIsNewRecord := True;
  FAlbaranId := '';
  dtpFecha.Date := Now;

  LInitialSerie := '';
  if Assigned(FParentForm) and (FParentForm is TfrmAlbaranes) and Assigned(TfrmAlbaranes(FParentForm).cbSeries) then
  begin
    if TfrmAlbaranes(FParentForm).cbSeries.ItemIndex > 0 then
      LInitialSerie := TfrmAlbaranes(FParentForm).cbSeries.Text;
  end;

  if dmgMain.GetSerieYNumeroDocumento(dmgMain.CurrentCompanyId, 'ALBARAN', dtpFecha.Date, LInitialSerie, LSerieAuto, LNumAuto) then
  begin
    LIdx := cbSerie.Items.IndexOf(LSerieAuto);
    if LIdx <> -1 then
      cbSerie.ItemIndex := LIdx
    else
    begin
      cbSerie.Items.Add(LSerieAuto);
      cbSerie.ItemIndex := cbSerie.Items.Count - 1;
    end;
    edtNumero.Text := IntToStr(LNumAuto);
  end
  else
  begin
    LIdx := -1;
    Qry := TFDQuery.Create(nil);
    try
      Qry.Connection := dmgMain.dbConn;
      Qry.SQL.Text := 'SELECT serie FROM ge_series WHERE activo = 1 AND empresa_id = :emp ORDER BY por_defecto DESC LIMIT 1';
      Qry.ParamByName('emp').AsString := dmgMain.CurrentCompanyId;
      Qry.Open;
      if not Qry.IsEmpty then
        LIdx := cbSerie.Items.IndexOf(Qry.FieldByName('serie').AsString);
    finally
      Qry.Free;
    end;

    if LIdx <> -1 then
      cbSerie.ItemIndex := LIdx
    else if cbSerie.Items.Count > 0 then
      cbSerie.ItemIndex := 0;

    if cbSerie.ItemIndex <> -1 then
      cbSerieChange(cbSerie)
    else
      edtNumero.Text := dmgMain.GetNextIDS('ge_albaranes');
  end;
  dtpFecha.Date := Now;
  edtTotal.Text := '0,00';
  edtIva.Text := '0,00';
  edtBase.Text := '0,00';
  sgIvaDesglose.RowCount := 2;
  sgIvaDesglose.Cells[0, 1] := '';
  sgIvaDesglose.Cells[1, 1] := '';
  sgIvaDesglose.Cells[2, 1] := '';
  FTotalBase := 0;
  FTotalIva := 0;
  FTotalDoc := 0;
  cbCliente.ItemIndex := -1;
  cbDestino.Items.Clear;
  if Assigned(FDestinationIds) then
    FDestinationIds.Clear;
  chkFacturado.Checked := False;
  edtFactura.Text := '';
  FIdFactura := '';
  edtEdiEstadoEnvio.Text := 'NO_ENVIADO';
  edtEdiMessageId.Clear;
  edtEdiEnviadoAt.Clear;
  edtEdiRefPedidoCliente.Clear;
  edtEdiNumeroSSCC.Clear;
  UpdateClienteDetails;
  UpdateDestinoDetails;
  CargarArbolBultosEdi;

  // Preparar qryLines vacía
  qryLines.Close;
  qryLines.SQL.Text := 'SELECT *, (total - base) as iva FROM ge_albaranes_lineas WHERE albaran_id = :id';
  qryLines.ParamByName('id').AsString := '00000000-0000-0000-0000-000000000000';
  qryLines.Open;
  SetupFieldChangeEvents;

  cbCliente.SetFocus;
end;

procedure TfrmAlbaranesEditor.DoModificar;
begin
  if chkFacturado.Checked or (FIdFactura <> '') then
  begin
    MessageDlg('⚠️ Este albarán ya ha sido FACTURADO. No se puede modificar.', mtWarning, [mbOK], 0);
    Exit;
  end;
  cbCliente.SetFocus;
end;

function TfrmAlbaranesEditor.IsNewRecord: Boolean;
begin
  Result := FIsNewRecord;
end;

procedure TfrmAlbaranesEditor.DoPrimero;
var
  LParent: TfrmAlbaranes;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmAlbaranes) then
    LParent := TfrmAlbaranes(FParentForm)
  else if Assigned(frmAlbaranes) then
    LParent := frmAlbaranes;

  if Assigned(LParent) and Assigned(LParent.QryMain) and not LParent.QryMain.IsEmpty then
  begin
    LParent.QryMain.First;
    FAlbaranId := LParent.QryMain.FieldByName('id').AsString;
    LoadData;
  end;
end;

procedure TfrmAlbaranesEditor.DoAnterior;
var
  LParent: TfrmAlbaranes;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmAlbaranes) then
    LParent := TfrmAlbaranes(FParentForm)
  else if Assigned(frmAlbaranes) then
    LParent := frmAlbaranes;

  if Assigned(LParent) and Assigned(LParent.QryMain) and not LParent.QryMain.IsEmpty then
  begin
    LParent.QryMain.Prior;
    FAlbaranId := LParent.QryMain.FieldByName('id').AsString;
    LoadData;
  end;
end;

procedure TfrmAlbaranesEditor.DoSiguiente;
var
  LParent: TfrmAlbaranes;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmAlbaranes) then
    LParent := TfrmAlbaranes(FParentForm)
  else if Assigned(frmAlbaranes) then
    LParent := frmAlbaranes;

  if Assigned(LParent) and Assigned(LParent.QryMain) and not LParent.QryMain.IsEmpty then
  begin
    LParent.QryMain.Next;
    FAlbaranId := LParent.QryMain.FieldByName('id').AsString;
    LoadData;
  end;
end;

procedure TfrmAlbaranesEditor.DoUltimo;
var
  LParent: TfrmAlbaranes;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmAlbaranes) then
    LParent := TfrmAlbaranes(FParentForm)
  else if Assigned(frmAlbaranes) then
    LParent := frmAlbaranes;

  if Assigned(LParent) and Assigned(LParent.QryMain) and not LParent.QryMain.IsEmpty then
  begin
    LParent.QryMain.Last;
    FAlbaranId := LParent.QryMain.FieldByName('id').AsString;
    LoadData;
  end;
end;

procedure TfrmAlbaranesEditor.btnNuevoClienteClick(Sender: TObject);
var
  LNewId: string;
begin
  if not Assigned(frmClientesEditor) then
    Application.CreateForm(TfrmClientesEditor, frmClientesEditor);
  
  frmClientesEditor.ClienteId := ''; // Nuevo cliente
  frmClientesEditor.ParentForm := Self;
  frmClientesEditor.ShowModal;
  
  LNewId := frmClientesEditor.ClienteId;
  if LNewId <> '' then
  begin
    LoadClientes;
    cbCliente.ItemIndex := FClientIds.IndexOf(LNewId);
    if cbCliente.ItemIndex <> -1 then
      cbClienteChange(cbCliente);
  end;
end;

procedure TfrmAlbaranesEditor.btnBuscarClienteClick(Sender: TObject);
var
  LFrm: TfrmSelectCliente;
  LIdx: Integer;
begin
  LFrm := TfrmSelectCliente.Create(Self);
  try
    if LFrm.ShowModal = mrOk then
    begin
      if LFrm.SelectedId <> '' then
      begin
        LIdx := FClientIds.IndexOf(LFrm.SelectedId);
        if LIdx = -1 then
        begin
          LoadClientes;
          LIdx := FClientIds.IndexOf(LFrm.SelectedId);
        end;

        if LIdx <> -1 then
        begin
          cbCliente.ItemIndex := LIdx;
          cbClienteChange(cbCliente);

          // Si el cliente tiene 1 solo destino, auto-seleccionarlo; si tiene varios, abrir selector de destinos
          if cbDestino.Items.Count = 1 then
          begin
            cbDestino.ItemIndex := 0;
            cbDestinoChange(cbDestino);
          end
          else if cbDestino.Items.Count > 1 then
            btnBuscarDestinoClick(nil);
        end;
      end;
    end;
  finally
    LFrm.Free;
  end;
end;

procedure TfrmAlbaranesEditor.btnBuscarDestinoClick(Sender: TObject);
var
  LFrm: TfrmSelectDestino;
  LClienteId: string;
begin
  if cbCliente.ItemIndex = -1 then
  begin
    ShowMessage('Debe seleccionar un cliente antes de buscar sucursales/destinos.');
    Exit;
  end;

  LClienteId := FClientIds[cbCliente.ItemIndex];
  LFrm := TfrmSelectDestino.Create(Self);
  try
    LFrm.ClienteId := LClienteId;
    if LFrm.ShowModal = mrOk then
    begin
      LoadDestinos(LClienteId);
      if LFrm.SelectedId <> '' then
      begin
        cbDestino.ItemIndex := FDestinationIds.IndexOf(LFrm.SelectedId);
        cbDestinoChange(cbDestino);
      end;
    end;
  finally
    LFrm.Free;
  end;
end;

function TfrmAlbaranesEditor.ValidarYAsegurarDestino: Boolean;
var
  LClienteId: string;
  LDestinoId: string;
  LCantDestinos: Integer;
  LFrmSel: TfrmSelectDestino;
  LFrmDestEdit: TfrmClientesDestinosEditor;
  Qry: TFDQuery;
  LNewId: string;
begin
  Result := False;
  if (cbCliente.ItemIndex = -1) then
  begin
    ShowMessage('Debe seleccionar un cliente.');
    Exit;
  end;

  LClienteId := FClientIds[cbCliente.ItemIndex];

  // Si ya tiene un destino seleccionado válido
  if (cbDestino.ItemIndex <> -1) and Assigned(FDestinationIds) and (cbDestino.ItemIndex < FDestinationIds.Count) then
  begin
    LDestinoId := FDestinationIds[cbDestino.ItemIndex];
    if (LDestinoId <> '') and (LDestinoId <> '0') then
    begin
      Result := True;
      Exit;
    end;
  end;

  // Comprobar si el cliente ya tiene destinos registrados en la BD
  dmgMain.qryExec.Close;
  dmgMain.qryExec.SQL.Text := 'SELECT COUNT(*) as cant FROM ge_clientes_destinos WHERE id_cliente = :cli AND activo = 1';
  dmgMain.qryExec.ParamByName('cli').AsString := LClienteId;
  dmgMain.qryExec.Open;
  LCantDestinos := dmgMain.qryExec.FieldByName('cant').AsInteger;
  dmgMain.qryExec.Close;

  if LCantDestinos > 0 then
  begin
    ShowMessage('Este documento requiere un destino o sucursal de entrega asignado.' + sLineBreak +
                'Por favor, seleccione el destino de la lista a continuación.');
    LFrmSel := TfrmSelectDestino.Create(Self);
    try
      LFrmSel.ClienteId := LClienteId;
      if LFrmSel.ShowModal = mrOk then
      begin
        LoadDestinos(LClienteId);
        if LFrmSel.SelectedId <> '' then
        begin
          cbDestino.ItemIndex := FDestinationIds.IndexOf(LFrmSel.SelectedId);
          cbDestinoChange(cbDestino);
          Result := (cbDestino.ItemIndex <> -1);
        end;
      end;
    finally
      LFrmSel.Free;
    end;
  end
  else
  begin
    ShowMessage('El cliente seleccionado no tiene ninguna sucursal o destino de entrega configurado.' + sLineBreak +
                'A continuación se abrirá el formulario para crear el destino con la dirección del cliente ya cumplimentada.');
    LFrmDestEdit := TfrmClientesDestinosEditor.Create(Self);
    try
      Qry := TFDQuery.Create(nil);
      try
        Qry.Connection := dmgMain.dbConn;
        Qry.CachedUpdates := False;
        Qry.SQL.Text := 'SELECT id, id_cliente, descripcion, direccion, poblacion, cp, ' +
                        'provincia, pais, activo, departamento, centro, ean_facturacion, ean_cliente, ean_emisor, ean_receptor ' +
                        'FROM ge_clientes_destinos WHERE id_cliente = :id_cliente LIMIT 1';
        Qry.ParamByName('id_cliente').AsString := LClienteId;
        Qry.Open;

        Qry.Append;
        Qry.FieldByName('id_cliente').AsString := LClienteId;
        Qry.FieldByName('activo').AsInteger := 1;

        LFrmDestEdit.DataSet := Qry;
        LFrmDestEdit.IsNew := True;
        LFrmDestEdit.ClienteId := LClienteId;

        // Precargar automáticamente con los datos del cliente
        LFrmDestEdit.btnCopiarClienteClick(nil);

        if LFrmDestEdit.ShowModal = mrOk then
        begin
          try
            dmgMain.qryExec.Close;
            dmgMain.qryExec.SQL.Text :=
              'INSERT INTO ge_clientes_destinos (id_cliente, descripcion, direccion, ' +
              'poblacion, cp, provincia, pais, activo, departamento, centro, ean_facturacion, ean_cliente, ean_emisor, ean_receptor, id_user_creator, id_user_update) ' +
              'VALUES (:id_cliente, :desc, :dir, :pob, :cp, :prov, :pais, :act, :dept, :centro, :ean_fac, :ean_cli, :ean_emi, :ean_rec, :user, :user)';

            dmgMain.qryExec.ParamByName('id_cliente').AsString := LClienteId;
            dmgMain.qryExec.ParamByName('desc').AsString := Qry.FieldByName('descripcion').AsString;
            dmgMain.qryExec.ParamByName('dir').AsString := Qry.FieldByName('direccion').AsString;
            dmgMain.qryExec.ParamByName('pob').AsString := Qry.FieldByName('poblacion').AsString;
            dmgMain.qryExec.ParamByName('cp').AsString := Qry.FieldByName('cp').AsString;
            dmgMain.qryExec.ParamByName('prov').AsString := Qry.FieldByName('provincia').AsString;
            dmgMain.qryExec.ParamByName('pais').AsString := Qry.FieldByName('pais').AsString;
            dmgMain.qryExec.ParamByName('act').AsBoolean := Qry.FieldByName('activo').AsInteger <> 0;
            dmgMain.qryExec.ParamByName('dept').AsString := Qry.FieldByName('departamento').AsString;
            dmgMain.qryExec.ParamByName('centro').AsString := Qry.FieldByName('centro').AsString;
            dmgMain.qryExec.ParamByName('ean_fac').AsString := Qry.FieldByName('ean_facturacion').AsString;
            dmgMain.qryExec.ParamByName('ean_cli').AsString := Qry.FieldByName('ean_cliente').AsString;
            dmgMain.qryExec.ParamByName('ean_emi').AsString := Qry.FieldByName('ean_emisor').AsString;
            dmgMain.qryExec.ParamByName('ean_rec').AsString := Qry.FieldByName('ean_receptor').AsString;
            dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
            dmgMain.qryExec.ExecSQL;

            dmgMain.qryExec.Close;
            dmgMain.qryExec.SQL.Text := 'SELECT LAST_INSERT_ID()';
            dmgMain.qryExec.Open;
            LNewId := dmgMain.qryExec.Fields[0].AsString;
            dmgMain.qryExec.Close;

            LoadDestinos(LClienteId);
            if LNewId <> '' then
            begin
              cbDestino.ItemIndex := FDestinationIds.IndexOf(LNewId);
              cbDestinoChange(cbDestino);
              Result := (cbDestino.ItemIndex <> -1);
            end;
          except
            on E: Exception do
              ShowMessage('Error al guardar el nuevo destino: ' + E.Message);
          end;
        end
        else
        begin
          Qry.Cancel;
        end;
      finally
        Qry.Free;
      end;
    finally
      LFrmDestEdit.Free;
    end;
  end;

  if not Result then
    ShowMessage('No se puede guardar el documento sin un destino de entrega asignado.');
end;

procedure TfrmAlbaranesEditor.btnNuevoDestinoClick(Sender: TObject);
var
  LClienteId, LNewId: string;
  Qry: TFDQuery;
  LFrm: TfrmClientesDestinosEditor;
begin
  if cbCliente.ItemIndex = -1 then
  begin
    ShowMessage('Debe seleccionar un cliente antes de crear un nuevo destino.');
    Exit;
  end;

  LClienteId := FClientIds[cbCliente.ItemIndex];
  
  LFrm := TfrmClientesDestinosEditor.Create(Self);
  try
    Qry := TFDQuery.Create(nil);
    try
      Qry.Connection := dmgMain.dbConn;
      Qry.CachedUpdates := False;
      Qry.SQL.Text := 'SELECT id, id_cliente, descripcion, direccion, poblacion, cp, ' +
                      'provincia, pais, activo, departamento, centro, ean_facturacion, ean_cliente, ean_emisor, ean_receptor ' +
                      'FROM ge_clientes_destinos WHERE id_cliente = :id_cliente LIMIT 1';
      Qry.ParamByName('id_cliente').AsString := LClienteId;
      Qry.Open;
      
      Qry.Append;
      Qry.FieldByName('id_cliente').AsString := LClienteId;
      Qry.FieldByName('activo').AsInteger := 1;
      
      LFrm.DataSet := Qry;
      LFrm.IsNew := True;
      LFrm.ClienteId := LClienteId;
      
      if LFrm.ShowModal = mrOk then
      begin
        try
          dmgMain.qryExec.Close;
          dmgMain.qryExec.SQL.Text :=
            'INSERT INTO ge_clientes_destinos (id_cliente, descripcion, direccion, ' +
            'poblacion, cp, provincia, pais, activo, departamento, centro, ean_facturacion, ean_cliente, ean_emisor, ean_receptor, id_user_creator, id_user_update) ' +
            'VALUES (:id_cliente, :desc, :dir, :pob, :cp, :prov, :pais, :act, :dept, :centro, :ean_fac, :ean_cli, :ean_emi, :ean_rec, :user, :user)';

          dmgMain.qryExec.ParamByName('id_cliente').AsString := LClienteId;
          dmgMain.qryExec.ParamByName('desc').AsString := Qry.FieldByName('descripcion').AsString;
          dmgMain.qryExec.ParamByName('dir').AsString := Qry.FieldByName('direccion').AsString;
          dmgMain.qryExec.ParamByName('pob').AsString := Qry.FieldByName('poblacion').AsString;
          dmgMain.qryExec.ParamByName('cp').AsString := Qry.FieldByName('cp').AsString;
          dmgMain.qryExec.ParamByName('prov').AsString := Qry.FieldByName('provincia').AsString;
          dmgMain.qryExec.ParamByName('pais').AsString := Qry.FieldByName('pais').AsString;
          dmgMain.qryExec.ParamByName('act').AsBoolean := Qry.FieldByName('activo').AsInteger <> 0;
          dmgMain.qryExec.ParamByName('dept').AsString := Qry.FieldByName('departamento').AsString;
          dmgMain.qryExec.ParamByName('centro').AsString := Qry.FieldByName('centro').AsString;
          dmgMain.qryExec.ParamByName('ean_fac').AsString := Qry.FieldByName('ean_facturacion').AsString;
          dmgMain.qryExec.ParamByName('ean_cli').AsString := Qry.FieldByName('ean_cliente').AsString;
          dmgMain.qryExec.ParamByName('ean_emi').AsString := Qry.FieldByName('ean_emisor').AsString;
          dmgMain.qryExec.ParamByName('ean_rec').AsString := Qry.FieldByName('ean_receptor').AsString;
          dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
          dmgMain.qryExec.ExecSQL;
          
          // Obtener el ID del nuevo registro insertado
          dmgMain.qryExec.Close;
          dmgMain.qryExec.SQL.Text := 'SELECT LAST_INSERT_ID()';
          dmgMain.qryExec.Open;
          LNewId := dmgMain.qryExec.Fields[0].AsString;
          dmgMain.qryExec.Close;
          
          LoadDestinos(LClienteId);
          if LNewId <> '' then
          begin
            cbDestino.ItemIndex := FDestinationIds.IndexOf(LNewId);
            cbDestinoChange(cbDestino);
          end;
        except
          on E: Exception do
            ShowMessage('Error al guardar el nuevo destino: ' + E.Message);
        end;
      end
      else
      begin
        Qry.Cancel;
      end;
    finally
      Qry.Free;
    end;
  finally
    LFrm.Free;
  end;
end;

procedure TfrmAlbaranesEditor.DoDuplicar;
var
  LOldAlbaranId, LNewAlbaranId: string;
  LNextNum: Integer;
  Qry: TFDQuery;
begin
  if FAlbaranId = '' then Exit;
  
  if MessageDlg('¿Está seguro de que desea duplicar este albarán?', mtConfirmation, [mbYes, mbNo], 0) <> mrYes then
    Exit;

  LOldAlbaranId := FAlbaranId;
  
  LNextNum := 1;
  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    Qry.SQL.Text := 'SELECT sig_numero_albaran FROM ge_series WHERE activo = 1 AND empresa_id = :emp AND serie = :ser';
    Qry.ParamByName('emp').AsString := dmgMain.CurrentCompanyId;
    Qry.ParamByName('ser').AsString := cbSerie.Text;
    Qry.Open;
    if not Qry.IsEmpty then
      LNextNum := Qry.FieldByName('sig_numero_albaran').AsInteger;
  finally
    Qry.Free;
  end;

  dmgMain.dbConn.StartTransaction;
  try
    // 1. Duplicar cabecera (sin id)
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text := 
      'INSERT INTO ge_albaranes (empresa_id, cliente_id, clientedestino_id, serie, numero, referencia, fecha, base_imponible, iva, total, facturado, id_pedido, id_factura, ' +
      'id_user_creator, id_user_update) ' +
      'SELECT empresa_id, cliente_id, clientedestino_id, serie, :new_num, referencia, CURRENT_TIMESTAMP, base_imponible, iva, total, 0, NULL, NULL, :user, :user ' +
      'FROM ge_albaranes WHERE id = :old_id';
    dmgMain.qryExec.ParamByName('old_id').AsInteger := StrToIntDef(LOldAlbaranId, 0);
    dmgMain.qryExec.ParamByName('new_num').AsInteger := LNextNum;
    dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
    dmgMain.qryExec.ExecSQL;

    // Obtener ID generado
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text := 'SELECT LAST_INSERT_ID() as new_id';
    dmgMain.qryExec.Open;
    LNewAlbaranId := dmgMain.qryExec.FieldByName('new_id').AsString;
    dmgMain.qryExec.Close;

    // 2. Duplicar líneas
    dmgMain.qryExec.SQL.Text :=
      'INSERT INTO ge_albaranes_lineas (albaran_id, articulo_codigo, articulo_id, posicion, descripcion, lotes, fecha_caducidad, bultos, cantidad_bulto, cantidad, precio, tipo_iva, base, ' +
      'total, descuento_porcentaje, descuento_precio, iva, id_user_creator, id_user_update) ' +
      'SELECT :new_id, articulo_codigo, articulo_id, posicion, descripcion, lotes, fecha_caducidad, bultos, cantidad_bulto, cantidad, precio, tipo_iva, base, total, descuento_porcentaje, descuento_precio, iva, :user, :user ' +
      'FROM ge_albaranes_lineas WHERE albaran_id = :old_id';
    dmgMain.qryExec.ParamByName('new_id').AsInteger := StrToIntDef(LNewAlbaranId, 0);
    dmgMain.qryExec.ParamByName('old_id').AsInteger := StrToIntDef(LOldAlbaranId, 0);
    dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
    dmgMain.qryExec.ExecSQL;

    // 2.1 Aumentar reservas de stock para las líneas duplicadas
    var LQryDupLines := TFDQuery.Create(nil);
    try
      LQryDupLines.Connection := dmgMain.dbConn;
      LQryDupLines.SQL.Text := 'SELECT articulo_id, lotes, cantidad FROM ge_albaranes_lineas WHERE albaran_id = :new_id';
      LQryDupLines.ParamByName('new_id').AsInteger := StrToIntDef(LNewAlbaranId, 0);
      LQryDupLines.Open;
      while not LQryDupLines.Eof do
      begin
        AumentarReservaStock(LQryDupLines.FieldByName('articulo_id').AsInteger,
                             LQryDupLines.FieldByName('lotes').AsString,
                             LQryDupLines.FieldByName('cantidad').AsFloat);
        LQryDupLines.Next;
      end;
    finally
      LQryDupLines.Free;
    end;

    // 3. Incrementar contador serie
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text := 'UPDATE ge_series SET sig_numero_albaran = sig_numero_albaran + 1 ' +
                                'WHERE activo = 1 AND empresa_id = :emp AND serie = :ser';
    dmgMain.qryExec.ParamByName('emp').AsString := dmgMain.CurrentCompanyId;
    dmgMain.qryExec.ParamByName('ser').AsString := cbSerie.Text;
    dmgMain.qryExec.ExecSQL;

    dmgMain.dbConn.Commit;
    
    FAlbaranId := LNewAlbaranId;
    FIsNewRecord := False;
    LoadData;
    
    if Assigned(FParentForm) and (FParentForm is TfrmAlbaranes) then
      TfrmAlbaranes(FParentForm).btnRefreshClick(nil);
      
  except
    on E: Exception do
    begin
      dmgMain.dbConn.Rollback;
      TDbErrorHandler.HandleException(E, 'Error al duplicar el albarán');
    end;
  end;
end;

function TfrmAlbaranesEditor.GenerarPaletSSCC(const AAlbaranId: Integer): string;
var
  Qry: TFDQuery;
  LGlnEmp: string;
  LPrefix9: string;
  LNextSscc: Int64;
  LNextBultoNum: Integer;
  LSSCC18: string;
begin
  Result := '';
  if AAlbaranId <= 0 then Exit;

  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;

    // 1. Obtener los 9 primeros dígitos del GLN de la empresa activa
    Qry.SQL.Text := 'SELECT gln FROM ge_empresas WHERE id = :emp';
    Qry.ParamByName('emp').AsString := dmgMain.CurrentCompanyId;
    Qry.Open;
    LGlnEmp := Trim(Qry.FieldByName('gln').AsString);
    Qry.Close;

    if Length(LGlnEmp) >= 9 then
      LPrefix9 := Copy(LGlnEmp, 1, 9)
    else
      LPrefix9 := '843700368'; // Prefijo GS1 predeterminado

    // 2. Obtener e incrementar el contador SSCC en ge_contadores
    Qry.SQL.Text := 'SELECT sscc FROM ge_contadores LIMIT 1';
    Qry.Open;

    if Qry.IsEmpty then
    begin
      LNextSscc := 1;
      Qry.Close;
      Qry.SQL.Text := 'INSERT INTO ge_contadores (id, sscc) VALUES (1, 2)';
      Qry.ExecSQL;
    end
    else
    begin
      LNextSscc := Qry.FieldByName('sscc').AsLargeInt;
      Qry.Close;
      Qry.SQL.Text := 'UPDATE ge_contadores SET sscc = sscc + 1';
      Qry.ExecSQL;
    end;

    // 3. Generar el código SSCC-18 estandarizado
    LSSCC18 := TValidacionFiscal.GenerarSSCC18(LPrefix9, LNextSscc, 3);
    Result := LSSCC18;

    // 4. Calcular el siguiente numero_bulto correlativo para este albarán
    Qry.SQL.Text := 'SELECT COALESCE(MAX(numero_bulto), 0) + 1 AS next_num FROM ge_albaranes_bultos WHERE albaran_id = :alb';
    Qry.ParamByName('alb').AsInteger := AAlbaranId;
    Qry.Open;
    LNextBultoNum := Qry.FieldByName('next_num').AsInteger;
    Qry.Close;

    // 5. Insertar el palet (nivel 2) en ge_albaranes_bultos
    Qry.SQL.Text :=
      'INSERT INTO ge_albaranes_bultos (albaran_id, numero_bulto, bulto_padre_id, nivel, tipo_embalaje, sscc, id_user_creator) ' +
      'VALUES (:alb, :num, NULL, 2, ''PE'', :sscc, :user)';
    Qry.ParamByName('alb').AsInteger := AAlbaranId;
    Qry.ParamByName('num').AsInteger := LNextBultoNum;
    Qry.ParamByName('sscc').AsString := LSSCC18;
    Qry.ParamByName('user').AsString := dmgMain.CurrentUserId;
    Qry.ExecSQL;

    // Actualizar campo edi_numero_sscc en la cabecera si estaba vacío
    Qry.SQL.Text := 'UPDATE ge_albaranes SET edi_numero_sscc = :sscc WHERE id = :alb AND (edi_numero_sscc IS NULL OR edi_numero_sscc = '''')';
    Qry.ParamByName('sscc').AsString := LSSCC18;
    Qry.ParamByName('alb').AsInteger := AAlbaranId;
    Qry.ExecSQL;
  finally
    Qry.Free;
  end;
end;

function TfrmAlbaranesEditor.EscanearEnvaseDUN14(const AAlbaranId, APaletBultoNum: Integer; const ABarcode: string): Boolean;
var
  Qry: TFDQuery;
  LData: TGS1BarcodeData;
  LNextBultoNum, LNewBultoId, LLineaId, LArticuloId: Integer;
begin
  Result := False;
  if (AAlbaranId <= 0) or (APaletBultoNum <= 0) or (Trim(ABarcode) = '') then Exit;

  // 1. Decodificar etiqueta DUN-14 / GS1-128
  LData := TGS1BarcodeParser.Parse(ABarcode);
  if not LData.IsValid then Exit;

  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;

    // Resolver id_articulo desde el EAN13 o GTIN
    LArticuloId := 0;
    Qry.SQL.Text := 'SELECT id FROM ge_articulos WHERE ean = :ean LIMIT 1';
    Qry.ParamByName('ean').AsString := LData.EAN13;
    Qry.Open;
    if not Qry.IsEmpty then
      LArticuloId := Qry.FieldByName('id').AsInteger;
    Qry.Close;

    // Resolver linea_albaran_id coincidente de ge_albaranes_lineas
    LLineaId := 0;
    Qry.SQL.Text :=
      'SELECT id FROM ge_albaranes_lineas ' +
      'WHERE albaran_id = :alb AND (articulo_id = :art OR edi_ean_enviado = :ean) ' +
      'LIMIT 1';
    Qry.ParamByName('alb').AsInteger := AAlbaranId;
    Qry.ParamByName('art').AsInteger := LArticuloId;
    Qry.ParamByName('ean').AsString := LData.EAN13;
    Qry.Open;
    if not Qry.IsEmpty then
      LLineaId := Qry.FieldByName('id').AsInteger;
    Qry.Close;

    // Calcular siguiente numero_bulto para el albarán
    Qry.SQL.Text := 'SELECT COALESCE(MAX(numero_bulto), 0) + 1 AS next_num FROM ge_albaranes_bultos WHERE albaran_id = :alb';
    Qry.ParamByName('alb').AsInteger := AAlbaranId;
    Qry.Open;
    LNextBultoNum := Qry.FieldByName('next_num').AsInteger;
    Qry.Close;

    // 2. Insertar bulto nivel 3 (Envase/Caja) asociando al palet padre (APaletBultoNum = bulto_padre_id)
    Qry.SQL.Text :=
      'INSERT INTO ge_albaranes_bultos (albaran_id, numero_bulto, bulto_padre_id, nivel, tipo_embalaje, sscc, id_user_creator) ' +
      'VALUES (:alb, :num, :padre, 3, ''CT'', '''', :user)';
    Qry.ParamByName('alb').AsInteger := AAlbaranId;
    Qry.ParamByName('num').AsInteger := LNextBultoNum;
    Qry.ParamByName('padre').AsInteger := APaletBultoNum;
    Qry.ParamByName('user').AsString := dmgMain.CurrentUserId;
    Qry.ExecSQL;

    // Obtener ID del bulto recién insertado
    Qry.SQL.Text := 'SELECT LAST_INSERT_ID() AS new_id';
    Qry.Open;
    LNewBultoId := Qry.FieldByName('new_id').AsInteger;
    Qry.Close;

    // 3. Insertar el contenido en ge_albaranes_lineas_bultos si tenemos una línea válida
    if (LNewBultoId > 0) and (LLineaId > 0) then
    begin
      Qry.SQL.Text :=
        'INSERT INTO ge_albaranes_lineas_bultos (bulto_id, linea_albaran_id, cantidad, lote, fecha_caducidad) ' +
        'VALUES (:bnum, :lin, :cant, :lote, :fcad)';
      Qry.ParamByName('bnum').AsInteger := LNewBultoId;
      Qry.ParamByName('lin').AsInteger := LLineaId;
      Qry.ParamByName('cant').AsFloat := LData.Cantidad;
      Qry.ParamByName('lote').AsString := LData.Lote;
      
      Qry.ParamByName('fcad').DataType := ftDate;
      if LData.HasCaducidad and (LData.FechaCaducidad > 0) then
        Qry.ParamByName('fcad').AsDate := LData.FechaCaducidad
      else
        Qry.ParamByName('fcad').Clear;

      Qry.ExecSQL;
    end;

    Result := True;
  finally
    Qry.Free;
  end;
end;

procedure TfrmAlbaranesEditor.UpdateLockState;
var
  LIsFacturado: Boolean;
begin
  LIsFacturado := chkFacturado.Checked or (FIdFactura <> '');

  dbgItems.ReadOnly := LIsFacturado;
  btnNuevoArticulo.Enabled := not LIsFacturado;
  btnEliminaArticulo.Enabled := not LIsFacturado;

  edtNumero.ReadOnly := LIsFacturado;
  dtpFecha.Enabled := not LIsFacturado;
  cbCliente.Enabled := not LIsFacturado;
  cbSerie.Enabled := not LIsFacturado;
  cbDestino.Enabled := not LIsFacturado;
  btnNuevoCliente.Enabled := not LIsFacturado;
  btnNuevoDestino.Enabled := not LIsFacturado;
  chkAnulado.Enabled := not LIsFacturado;
  if Assigned(edtObservaciones1) then
    edtObservaciones1.ReadOnly := LIsFacturado;

  btnEliminar.Enabled := not LIsFacturado;
  btnGuardar.Enabled := not LIsFacturado;
  btnModificar.Enabled := not LIsFacturado;
end;

procedure TfrmAlbaranesEditor.chkFacturadoClick(Sender: TObject);
begin
  UpdateLockState;
end;

procedure TfrmAlbaranesEditor.dbgItemsDblClick(Sender: TObject);
var
  LArtId, LLineId: Integer;
  LFormSelectLote: TfrmSelectLote;
begin
  if chkFacturado.Checked then
  begin
    MessageDlg('⚠️ El albarán ya ha sido FACTURADO. No se permite modificar lotes.', mtWarning, [mbOK], 0);
    Exit;
  end;

  if qryLines.IsEmpty then Exit;

  if Assigned(dbgItems.SelectedField) and
     (SameText(dbgItems.SelectedField.FieldName, 'lotes') or SameText(dbgItems.SelectedField.FieldName, 'fecha_caducidad')) then
  begin
    LArtId := qryLines.FieldByName('articulo_id').AsInteger;
    if LArtId <= 0 then
    begin
      MessageDlg('Debe seleccionar primero un artículo en esta línea.', mtWarning, [mbOK], 0);
      Exit;
    end;

    LLineId := qryLines.FieldByName('id').AsInteger;
    if (LLineId > 0) and LineaTieneSalidaBultos(LLineId) then
    begin
      MessageDlg('⚠️ No se puede modificar el lote de esta línea porque ya dispone de salidas de mercancía / bultos escaneados en almacén.' + sLineBreak + sLineBreak +
                 'Para realizar cambios en el lote, primero elimine o vacíe los bultos escaneados desde el editor de preparación de pedidos.', mtWarning, [mbOK], 0);
      Exit;
    end;

    LFormSelectLote := TfrmSelectLote.Create(Self);
    try
      LFormSelectLote.ArticuloId := LArtId;
      if LFormSelectLote.ShowModal = mrOk then
      begin
        if not (qryLines.State in [dsEdit, dsInsert]) then
          qryLines.Edit;

        qryLines.FieldByName('lotes').AsString := LFormSelectLote.SelectedLoteCode;
        if LFormSelectLote.HasFechaCaducidad then
          qryLines.FieldByName('fecha_caducidad').AsDateTime := LFormSelectLote.SelectedFechaCaducidad;

        qryLines.Post;
      end;
    finally
      LFormSelectLote.Free;
    end;
  end;
end;

procedure TfrmAlbaranesEditor.pmLineasPopup(Sender: TObject);
var
  LLote: string;
begin
  if not qryLines.Active or qryLines.IsEmpty then
  begin
    mnuTrazabilidadLote.Enabled := False;
    mnuTrazabilidadLote.Caption := 'Mostrar trazabilidad completa de Lote';
    Exit;
  end;

  LLote := Trim(qryLines.FieldByName('lotes').AsString);
  if LLote <> '' then
  begin
    mnuTrazabilidadLote.Enabled := True;
    mnuTrazabilidadLote.Caption := Format('📦 Mostrar trazabilidad completa de Lote (%s)', [LLote]);
  end
  else
  begin
    mnuTrazabilidadLote.Enabled := False;
    mnuTrazabilidadLote.Caption := 'Mostrar trazabilidad completa de Lote (Sin lote asignado)';
  end;
end;

procedure TfrmAlbaranesEditor.mnuTrazabilidadLoteClick(Sender: TObject);
var
  LLoteCode: string;
  LArtId: Integer;
  LQrySearch: TFDQuery;
  LLoteId: Integer;
  LStockId: Integer;
  LFormLotesCreator: TfrmLotesCreator;
  LFormStockEditor: TfrmStockEnvasadoEditor;
begin
  if not qryLines.Active or qryLines.IsEmpty then Exit;

  LLoteCode := Trim(qryLines.FieldByName('lotes').AsString);
  LArtId := qryLines.FieldByName('articulo_id').AsInteger;

  if LLoteCode = '' then
  begin
    MessageDlg('La línea seleccionada no tiene ningún lote asignado.', mtInformation, [mbOK], 0);
    Exit;
  end;

  LLoteId := 0;
  LStockId := 0;

  LQrySearch := TFDQuery.Create(nil);
  try
    LQrySearch.Connection := dmgMain.dbConn;
    // 1. Buscar en ge_lotes
    LQrySearch.SQL.Text := 'SELECT id FROM ge_lotes WHERE codigo = :cod OR codigo_antiguo = :cod ORDER BY id DESC LIMIT 1';
    LQrySearch.ParamByName('cod').AsString := LLoteCode;
    LQrySearch.Open;
    if not LQrySearch.IsEmpty then
      LLoteId := LQrySearch.FieldByName('id').AsInteger;

    // 2. Si no o además, buscar en ge_stock_envasado
    if LLoteId > 0 then
    begin
      LQrySearch.Close;
      LQrySearch.SQL.Text := 'SELECT id FROM ge_stock_envasado WHERE id_lote = :lid ORDER BY id DESC LIMIT 1';
      LQrySearch.ParamByName('lid').AsInteger := LLoteId;
      LQrySearch.Open;
      if not LQrySearch.IsEmpty then
        LStockId := LQrySearch.FieldByName('id').AsInteger;
    end
    else if LArtId > 0 then
    begin
      LQrySearch.Close;
      LQrySearch.SQL.Text := 'SELECT id FROM ge_stock_envasado WHERE id_articulo = :art ORDER BY id DESC LIMIT 1';
      LQrySearch.ParamByName('art').AsInteger := LArtId;
      LQrySearch.Open;
      if not LQrySearch.IsEmpty then
        LStockId := LQrySearch.FieldByName('id').AsInteger;
    end;
  finally
    LQrySearch.Free;
  end;

  if LLoteId > 0 then
  begin
    LFormLotesCreator := TfrmLotesCreator.Create(Self);
    try
      LFormLotesCreator.LoteId := LLoteId;
      LFormLotesCreator.OpenTraceabilityTab := True;
      LFormLotesCreator.ShowModal;
    finally
      LFormLotesCreator.Free;
    end;
  end
  else if LStockId > 0 then
  begin
    LFormStockEditor := TfrmStockEnvasadoEditor.Create(Self);
    try
      LFormStockEditor.StockId := LStockId;
      LFormStockEditor.OpenTraceabilityTab := True;
      LFormStockEditor.ShowModal;
    finally
      LFormStockEditor.Free;
    end;
  end
  else
  begin
    LFormLotesCreator := TfrmLotesCreator.Create(Self);
    try
      LFormLotesCreator.LoteId := 0;
      LFormLotesCreator.OpenTraceabilityTab := True;
      LFormLotesCreator.ShowModal;
    finally
      LFormLotesCreator.Free;
    end;
  end;
end;

procedure TfrmAlbaranesEditor.btnDocsRelacionadosClick(Sender: TObject);
begin
  if (Trim(FAlbaranId) = '') or FIsNewRecord then
  begin
    ShowMessage('Debe guardar el albarán antes de consultar sus documentos relacionados.');
    Exit;
  end;

  TfrmDocumentosRelacionados.MostrarDocumentos(Self, dvtAlbaran, FAlbaranId, cbSerie.Text + '/' + edtNumero.Text);
end;

procedure TfrmAlbaranesEditor.btnFacturarClick(Sender: TObject);
var
  LAlbaranId, LClienteId, LSerieAlb, LNumAlb, LRefAlb: string;
  LSerieFra: string;
  LNumFra: Integer;
  LBase, LIva, LTotal: Double;
  LFormaPagoId: Variant;
  LFacturaId: string;
begin
  if FIsNewRecord or (Trim(FAlbaranId) = '') then
  begin
    MessageDlg('Debe guardar el albarán antes de generar la factura.', mtWarning, [mbOK], 0);
    Exit;
  end;

  LAlbaranId := FAlbaranId;
  LSerieAlb := cbSerie.Text;
  LNumAlb := edtNumero.Text;

  // 1. Comprobar si ya está facturado
  if chkFacturado.Checked or (FIdFactura <> '') then
  begin
    if MessageDlg('Este albarán ya ha sido facturado' +
                  IfThen(edtFactura.Text <> '', ' (Factura Nº: ' + edtFactura.Text + ')', '') + '.' + sLineBreak + sLineBreak +
                  '¿Desea abrir la factura asociada para consultarla?',
                  mtInformation, [mbYes, mbNo], 0) = mrYes then
    begin
      if FIdFactura <> '' then
      begin
        if not Assigned(frmFacturasEditor) then
          Application.CreateForm(TfrmFacturasEditor, frmFacturasEditor);
        frmFacturasEditor.FacturaId := FIdFactura;
        frmFacturasEditor.ShowModal;
      end
      else
      begin
        var LQryFact := TFDQuery.Create(nil);
        try
          LQryFact.Connection := dmgMain.dbConn;
          LQryFact.SQL.Text := 'SELECT id_factura FROM ge_albaranes WHERE id = :id';
          LQryFact.ParamByName('id').AsString := LAlbaranId;
          LQryFact.Open;
          if not LQryFact.IsEmpty and (LQryFact.FieldByName('id_factura').AsString <> '') then
          begin
            if not Assigned(frmFacturasEditor) then
              Application.CreateForm(TfrmFacturasEditor, frmFacturasEditor);
            frmFacturasEditor.FacturaId := LQryFact.FieldByName('id_factura').AsString;
            frmFacturasEditor.ShowModal;
          end;
        finally
          LQryFact.Free;
        end;
      end;
    end;
    Exit;
  end;

  // 2. Comprobar si está anulado
  if chkAnulado.Checked then
  begin
    MessageDlg('No se puede facturar un albarán anulado.', mtWarning, [mbOK], 0);
    Exit;
  end;

  // 3. Comprobar si tiene líneas
  if qryLines.IsEmpty then
  begin
    MessageDlg('El albarán no contiene líneas de detalle para facturar.', mtWarning, [mbOK], 0);
    Exit;
  end;

  // 4. Obtener Serie y Número siguiente de Factura (usando la serie del albarán como sugerencia)
  LSerieFra := '';
  LNumFra := 0;
  if not dmgMain.GetSerieYNumeroDocumento(dmgMain.CurrentCompanyId, 'FACTURA', Date, LSerieAlb, LSerieFra, LNumFra) then
  begin
    var LQrySer := TFDQuery.Create(nil);
    try
      LQrySer.Connection := dmgMain.dbConn;
      LQrySer.SQL.Text := 'SELECT serie, sig_numero_factura FROM ge_series WHERE activo = 1 AND empresa_id = :emp ORDER BY por_defecto DESC LIMIT 1';
      LQrySer.ParamByName('emp').AsString := dmgMain.CurrentCompanyId;
      LQrySer.Open;
      if not LQrySer.IsEmpty then
      begin
        LSerieFra := LQrySer.FieldByName('serie').AsString;
        LNumFra := LQrySer.FieldByName('sig_numero_factura').AsInteger;
      end
      else
      begin
        LSerieFra := 'A';
        LNumFra := StrToIntDef(dmgMain.GetNextIDS('ge_facturas'), 1);
      end;
    finally
      LQrySer.Free;
    end;
  end;

  // 5. Confirmación de facturación mostrando el número de Factura a generar
  if MessageDlg('¿Desea generar la Factura Nº ' + LSerieFra + '/' + IntToStr(LNumFra) + ' a partir del albarán ' + LSerieAlb + '/' + LNumAlb + ' (' + cbCliente.Text + ')?',
                mtConfirmation, [mbYes, mbNo], 0) <> mrYes then
    Exit;

  // 6. Preparar datos
  LClienteId := FClientIds[cbCliente.ItemIndex];
  LBase := FTotalBase;
  LIva := FTotalIva;
  LTotal := FTotalDoc;
  LRefAlb := 'Albarán ' + LSerieAlb + '/' + LNumAlb;
  if Trim(edtObservaciones1.Text) <> '' then
    LRefAlb := LRefAlb + ' - ' + Trim(edtObservaciones1.Text);

  if (cbFormaPago.ItemIndex > 0) and Assigned(FFormaPagoIds) and (cbFormaPago.ItemIndex < FFormaPagoIds.Count) and (FFormaPagoIds[cbFormaPago.ItemIndex] <> '') then
    LFormaPagoId := StrToIntDef(FFormaPagoIds[cbFormaPago.ItemIndex], 0)
  else
    LFormaPagoId := Null;

  // 7. Transacción de facturación
  dmgMain.dbConn.StartTransaction;
  try
    // 7.1 Insertar Cabecera en ge_facturas
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text :=
      'INSERT INTO ge_facturas (' +
      '  empresa_id, cliente_id, clientedestino_id, serie, numero, fecha, ' +
      '  base_imponible, iva, total, pagada, id_forma_pago, ' +
      '  referencia, id_user_creator, id_user_update' +
      ') VALUES (' +
      '  :emp, :cli, :dest, :ser, :num, :fec, ' +
      '  :base, :iva, :tot, 0, :fp, ' +
      '  :ref, :user, :user' +
      ')';
    dmgMain.qryExec.ParamByName('emp').AsString := dmgMain.CurrentCompanyId;
    dmgMain.qryExec.ParamByName('cli').AsString := LClienteId;

    dmgMain.qryExec.ParamByName('dest').DataType := ftInteger;
    if (cbDestino.ItemIndex >= 0) and Assigned(FDestinationIds) and (cbDestino.ItemIndex < FDestinationIds.Count) and (FDestinationIds[cbDestino.ItemIndex] <> '') then
      dmgMain.qryExec.ParamByName('dest').AsInteger := StrToIntDef(FDestinationIds[cbDestino.ItemIndex], 0)
    else
      dmgMain.qryExec.ParamByName('dest').Clear;

    dmgMain.qryExec.ParamByName('ser').AsString := LSerieFra;
    dmgMain.qryExec.ParamByName('num').AsInteger := LNumFra;
    dmgMain.qryExec.ParamByName('fec').AsDate := Date;
    dmgMain.qryExec.ParamByName('base').AsFloat := LBase;
    dmgMain.qryExec.ParamByName('iva').AsFloat := LIva;
    dmgMain.qryExec.ParamByName('tot').AsFloat := LTotal;
    dmgMain.qryExec.ParamByName('fp').DataType := ftInteger;
    if not VarIsNull(LFormaPagoId) then
      dmgMain.qryExec.ParamByName('fp').AsInteger := Integer(LFormaPagoId)
    else
      dmgMain.qryExec.ParamByName('fp').Clear;
    dmgMain.qryExec.ParamByName('ref').AsString := LRefAlb;
    dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
    dmgMain.qryExec.ExecSQL;

    // Obtener ID generado
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text := 'SELECT LAST_INSERT_ID() as new_id';
    dmgMain.qryExec.Open;
    LFacturaId := dmgMain.qryExec.FieldByName('new_id').AsString;
    dmgMain.qryExec.Close;

    // Incrementar contador de serie
    dmgMain.qryExec.SQL.Text :=
      'UPDATE ge_series SET sig_numero_factura = sig_numero_factura + 1 ' +
      'WHERE activo = 1 AND empresa_id = :emp AND serie = :ser';
    dmgMain.qryExec.ParamByName('emp').AsString := dmgMain.CurrentCompanyId;
    dmgMain.qryExec.ParamByName('ser').AsString := LSerieFra;
    dmgMain.qryExec.ExecSQL;

    // 7.2 Copiar líneas de ge_albaranes_lineas a ge_facturas_lineas
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text :=
      'INSERT INTO ge_facturas_lineas (' +
      '  factura_id, posicion, articulo_id, articulo_codigo, descripcion, ' +
      '  bultos, cantidad_bulto, cantidad, precio, descuento_porc, ' +
      '  descuento_cant, tipo_iva, base, iva, total, ' +
      '  lotes, fecha_caducidad, edi_numero_linea_albaran, ' +
      '  id_user_creator, id_user_update' +
      ') ' +
      'SELECT ' +
      '  :factura_id, posicion, articulo_id, articulo_codigo, descripcion, ' +
      '  COALESCE(bultos, 0), COALESCE(cantidad_bulto, 1), cantidad, precio, ' +
      '  COALESCE(descuento_porcentaje, 0), COALESCE(descuento_precio, 0), ' +
      '  tipo_iva, base, (total - base) as iva, total, ' +
      '  lotes, fecha_caducidad, id, ' +
      '  :user, :user ' +
      'FROM ge_albaranes_lineas ' +
      'WHERE albaran_id = :albaran_id ' +
      'ORDER BY posicion, id';
    dmgMain.qryExec.ParamByName('factura_id').AsString := LFacturaId;
    dmgMain.qryExec.ParamByName('albaran_id').AsString := LAlbaranId;
    dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
    dmgMain.qryExec.ExecSQL;

    // 7.3 Actualizar ge_albaranes vinculando la factura
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text :=
      'UPDATE ge_albaranes ' +
      'SET facturado = 1, id_factura = :factura_id, id_user_update = :user ' +
      'WHERE id = :albaran_id';
    dmgMain.qryExec.ParamByName('factura_id').AsString := LFacturaId;
    dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
    dmgMain.qryExec.ParamByName('albaran_id').AsString := LAlbaranId;
    dmgMain.qryExec.ExecSQL;

    dmgMain.dbConn.Commit;

    // Actualizar interfaz del editor de albarán
    chkFacturado.Checked := True;
    FIdFactura := LFacturaId;
    edtFactura.Text := LSerieFra + '/' + IntToStr(LNumFra);
    UpdateLockState;

    if Assigned(FParentForm) and (FParentForm is TfrmAlbaranes) then
      TfrmAlbaranes(FParentForm).btnRefreshClick(nil);

    if MessageDlg('Factura ' + LSerieFra + '/' + IntToStr(LNumFra) + ' creada con ÉXITO.' + sLineBreak + sLineBreak +
                  '¿Desea abrir el editor de facturas para verla o imprimirla?',
                  mtInformation, [mbYes, mbNo], 0) = mrYes then
    begin
      if not Assigned(frmFacturasEditor) then
        Application.CreateForm(TfrmFacturasEditor, frmFacturasEditor);
      frmFacturasEditor.FacturaId := LFacturaId;
      frmFacturasEditor.ShowModal;
    end;

  except
    on E: Exception do
    begin
      dmgMain.dbConn.Rollback;
      TDbErrorHandler.HandleException(E, 'Error al facturar el albarán');
    end;
  end;
end;

procedure TfrmAlbaranesEditor.btnImprimirClick(Sender: TObject);
begin
  if (Trim(FAlbaranId) = '') or FIsNewRecord then
  begin
    ShowMessage('Debe guardar el albarán antes de poder imprimirlo.');
    Exit;
  end;

  TAlbaranPrintService.ImprimirAlbaran(Self, FAlbaranId);
end;

procedure TfrmAlbaranesEditor.btnEliminarClick(Sender: TObject);
var
  Qry, QryLinesAux, QryStockAux: TFDQuery;
  LSerie, LMsg: string;
  LNumero, LPedidoId, LStockId, LArtStockId: Integer;
  LFacturado: Boolean;
  LLoteCode: string;
  LPend, LTomar: Double;
begin
  if (Trim(FAlbaranId) = '') or FIsNewRecord then
  begin
    ShowMessage('No hay ningún albarán guardado para eliminar.');
    Exit;
  end;

  // 1. Validar si el albarán está facturado (en memoria o en BD)
  if chkFacturado.Checked or (Trim(FIdFactura) <> '') then
  begin
    MessageDlg('⚠️ Este albarán ya ha sido FACTURADO.' + sLineBreak +
               'No se puede eliminar ni modificar un albarán que ya ha sido facturado.', mtError, [mbOK], 0);
    Exit;
  end;

  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    Qry.SQL.Text := 'SELECT serie, numero, facturado, id_factura, id_pedido FROM ge_albaranes WHERE id = :id';
    Qry.ParamByName('id').AsString := FAlbaranId;
    Qry.Open;
    if Qry.IsEmpty then
    begin
      ShowMessage('El albarán no existe en la base de datos.');
      Exit;
    end;

    LSerie := Trim(Qry.FieldByName('serie').AsString);
    LNumero := Qry.FieldByName('numero').AsInteger;
    LFacturado := (Qry.FieldByName('facturado').AsInteger <> 0) or 
                  (not Qry.FieldByName('id_factura').IsNull and (Qry.FieldByName('id_factura').AsInteger > 0));
    LPedidoId := 0;
    if not Qry.FieldByName('id_pedido').IsNull then
      LPedidoId := Qry.FieldByName('id_pedido').AsInteger;
  finally
    Qry.Free;
  end;

  if LFacturado then
  begin
    MessageDlg('⚠️ Este albarán (' + LSerie + '/' + IntToStr(LNumero) + ') ya ha sido FACTURADO en la base de datos.' + sLineBreak +
               'No se puede eliminar ni modificar un albarán facturado.', mtError, [mbOK], 0);
    Exit;
  end;

  // 2. Diálogo de confirmación
  LMsg := '¿Está seguro de que desea ELIMINAR el albarán ' + LSerie + '/' + IntToStr(LNumero) + '?' + sLineBreak + sLineBreak +
          '• Esta acción liberará el stock reservado asociado a este albarán.' + sLineBreak;
  if LPedidoId > 0 then
    LMsg := LMsg + '• El pedido de venta vinculado quedará en estado PENDIENTE, listo para volver a ser albaranado.' + sLineBreak;

  if MessageDlg(LMsg, mtConfirmation, [mbYes, mbNo], 0) <> mrYes then
    Exit;

  // 3. Ejecución en transacción atómica
  dmgMain.dbConn.StartTransaction;
  try
    // A. Liberar reservas en ge_stock_envasado de todas las líneas del albarán
    QryLinesAux := TFDQuery.Create(nil);
    QryStockAux := TFDQuery.Create(nil);
    try
      QryLinesAux.Connection := dmgMain.dbConn;
      QryStockAux.Connection := dmgMain.dbConn;

      QryLinesAux.SQL.Text :=
        'SELECT al.articulo_id, al.lotes, al.cantidad, ' +
        '       COALESCE(NULLIF(a.id_maestro, 0), al.articulo_id) AS id_articulo_stock ' +
        'FROM ge_albaranes_lineas al ' +
        'LEFT JOIN ge_articulos a ON al.articulo_id = a.id ' +
        'WHERE al.albaran_id = :alb';
      QryLinesAux.ParamByName('alb').AsString := FAlbaranId;
      QryLinesAux.Open;

      while not QryLinesAux.Eof do
      begin
        LArtStockId := QryLinesAux.FieldByName('id_articulo_stock').AsInteger;
        LLoteCode   := Trim(QryLinesAux.FieldByName('lotes').AsString);
        LPend       := QryLinesAux.FieldByName('cantidad').AsFloat;

        // Intentar liberar primero del lote específico
        if (LLoteCode <> '') and (LPend > 0) then
        begin
          QryStockAux.Close;
          QryStockAux.SQL.Text :=
            'SELECT se.id, COALESCE(se.cantidad_reservada, 0) AS reservada ' +
            'FROM ge_stock_envasado se ' +
            'INNER JOIN ge_lotes l ON se.id_lote = l.id ' +
            'WHERE se.id_articulo = :art AND (l.codigo = :lote OR l.codigo_antiguo = :lote) ' +
            '  AND COALESCE(se.cantidad_reservada, 0) > 0 LIMIT 1';
          QryStockAux.ParamByName('art').AsInteger := LArtStockId;
          QryStockAux.ParamByName('lote').AsString := LLoteCode;
          QryStockAux.Open;
          if not QryStockAux.IsEmpty then
          begin
            LStockId := QryStockAux.FieldByName('id').AsInteger;
            LTomar   := Min(LPend, QryStockAux.FieldByName('reservada').AsFloat);
            if LTomar > 0 then
            begin
              dmgMain.qryExec.Close;
              dmgMain.qryExec.SQL.Text :=
                'UPDATE ge_stock_envasado ' +
                'SET cantidad_reservada = GREATEST(0, COALESCE(cantidad_reservada, 0) - :cant), ' +
                '    updated_at = NOW(), id_user_update = :user ' +
                'WHERE id = :sid';
              dmgMain.qryExec.ParamByName('cant').AsFloat  := LTomar;
              dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
              dmgMain.qryExec.ParamByName('sid').AsInteger := LStockId;
              dmgMain.qryExec.ExecSQL;

              LPend := LPend - LTomar;
            end;
          end;
        end;

        // Si aún queda por liberar, usar FIFO en otros registros del artículo
        if LPend > 0 then
        begin
          QryStockAux.Close;
          QryStockAux.SQL.Text :=
            'SELECT id, COALESCE(cantidad_reservada, 0) AS reservada ' +
            'FROM ge_stock_envasado ' +
            'WHERE id_articulo = :art AND COALESCE(cantidad_reservada, 0) > 0 ' +
            'ORDER BY id ASC';
          QryStockAux.ParamByName('art').AsInteger := LArtStockId;
          QryStockAux.Open;
          while (not QryStockAux.Eof) and (LPend > 0) do
          begin
            LStockId := QryStockAux.FieldByName('id').AsInteger;
            LTomar   := Min(LPend, QryStockAux.FieldByName('reservada').AsFloat);
            if LTomar > 0 then
            begin
              dmgMain.qryExec.Close;
              dmgMain.qryExec.SQL.Text :=
                'UPDATE ge_stock_envasado ' +
                'SET cantidad_reservada = GREATEST(0, COALESCE(cantidad_reservada, 0) - :cant), ' +
                '    updated_at = NOW(), id_user_update = :user ' +
                'WHERE id = :sid';
              dmgMain.qryExec.ParamByName('cant').AsFloat  := LTomar;
              dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
              dmgMain.qryExec.ParamByName('sid').AsInteger := LStockId;
              dmgMain.qryExec.ExecSQL;

              LPend := LPend - LTomar;
            end;
            QryStockAux.Next;
          end;
        end;

        QryLinesAux.Next;
      end;
    finally
      QryLinesAux.Free;
      QryStockAux.Free;
    end;

    // B. Eliminar bultos asociados al albarán
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text :=
      'DELETE FROM ge_albaranes_lineas_bultos WHERE linea_albaran_id IN (SELECT id FROM ge_albaranes_lineas WHERE albaran_id = :id)';
    dmgMain.qryExec.ParamByName('id').AsString := FAlbaranId;
    dmgMain.qryExec.ExecSQL;

    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text := 'DELETE FROM ge_albaranes_bultos WHERE albaran_id = :id';
    dmgMain.qryExec.ParamByName('id').AsString := FAlbaranId;
    dmgMain.qryExec.ExecSQL;

    // C. Eliminar líneas del albarán
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text := 'DELETE FROM ge_albaranes_lineas WHERE albaran_id = :id';
    dmgMain.qryExec.ParamByName('id').AsString := FAlbaranId;
    dmgMain.qryExec.ExecSQL;

    // D. Desvincular recogidas de envíos asociadas a este albarán
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text := 'UPDATE ge_envios_recogidas SET albaran_id = NULL WHERE albaran_id = :id';
    dmgMain.qryExec.ParamByName('id').AsString := FAlbaranId;
    dmgMain.qryExec.ExecSQL;

    // E. Desvincular y restaurar el pedido asociado si existe
    if LPedidoId > 0 then
    begin
      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text := 'UPDATE ge_albaranes SET id_pedido = NULL WHERE id = :id';
      dmgMain.qryExec.ParamByName('id').AsString := FAlbaranId;
      dmgMain.qryExec.ExecSQL;

      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text :=
        'UPDATE ge_pedidos SET estado = ''PENDIENTE'', updated_at = NOW(), id_user_update = :user WHERE id = :ped';
      dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
      dmgMain.qryExec.ParamByName('ped').AsInteger := LPedidoId;
      dmgMain.qryExec.ExecSQL;
    end;

    // F. Eliminar la cabecera del albarán
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text := 'DELETE FROM ge_albaranes WHERE id = :id';
    dmgMain.qryExec.ParamByName('id').AsString := FAlbaranId;
    dmgMain.qryExec.ExecSQL;

    dmgMain.dbConn.Commit;
  except
    on E: Exception do
    begin
      dmgMain.dbConn.Rollback;
      ShowMessage('Error al eliminar el albarán: ' + E.Message);
      Exit;
    end;
  end;

  ShowMessage('El albarán ' + LSerie + '/' + IntToStr(LNumero) + ' ha sido eliminado con éxito.' + sLineBreak +
              'El stock reservado ha sido liberado.');

  if Assigned(FParentForm) and (FParentForm is TfrmAlbaranes) then
    TfrmAlbaranes(FParentForm).btnRefreshClick(nil);

  ModalResult := mrOk;
  Close;
end;

end.
