unit frm_PedidosEditor;

interface

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

type
  TfrmPedidosEditor = class(TfrmBase)
    qMaster: TFDQuery;
    dsMaster: TDataSource;
    qDetail: TFDQuery;
    dsDetail: TDataSource;
    qClientList: TFDQuery;
    dsClientList: TDataSource;
    qDestList: TFDQuery;
    dsDestList: TDataSource;

    // Controles de Cabecera
    lblCliente: TLabel;
    dbClient: TDBLookupComboBox;
    btnBuscarCliente: TSpeedButton;
    btnNuevoCliente: TSpeedButton;
    lblDestino: TLabel;
    dbDest: TDBLookupComboBox;
    btnBuscarDestino: TSpeedButton;
    btnNuevoDestino: TSpeedButton;
    lblEmpresa: TLabel;
    dbEmpresa: TDBLookupComboBox;
    qEmpresas: TFDQuery;
    dsEmpresas: TDataSource;
    lblNumPedido: TLabel;
    dbNumPedido: TDBEdit;
    lblFechaPedido: TLabel;
    dbFechaPedido: TDBEdit;
    lblFechaEntrega: TLabel;
    dbFechaEntrega: TDBEdit;
    lblEstado: TLabel;
    dbEstado: TDBComboBox;
    lblObs: TLabel;
    dbObs: TDBMemo;
    lblOrigen: TLabel;
    dbOrigen: TDBEdit;
    pnlClienteInfo: TPanel;
    lblClienteInfo: TLabel;
    pnlDestinoInfo: TPanel;
    lblDestinoInfo: TLabel;

    // Controles de Totales en Pie
    lblBase: TLabel;
    dbBase: TDBEdit;
    lblIva: TLabel;
    dbIva: TDBEdit;
    lblTotal: TLabel;
    dbTotal: TDBEdit;
    tsRecogida: TTabSheet;
    lblInfoRecogida: TLabel;
    grpRecogidaInfo: TGroupBox;
    lblRecTransp: TLabel;
    edtRecTransp: TEdit;
    lblRecCodigo: TLabel;
    edtRecCodigo: TEdit;
    lblRecFecha: TLabel;
    edtRecFecha: TEdit;
    lblRecBultos: TLabel;
    edtRecBultos: TEdit;
    lblRecEstado: TLabel;
    edtRecEstado: TEdit;
    lblRecAlbaran: TLabel;
    edtRecAlbaran: TEdit;
    lblRecConductor: TLabel;
    edtRecConductor: TEdit;
    lblRecMatricula: TLabel;
    edtRecMatricula: TEdit;
    btnVerCarga: TButton;
    btnRegistrarFirma: TButton;
    btnImprimirEtiquetas: TButton;
    lblAlbaranDoc: TLabel;
    lblFacturaDoc: TLabel;
    edtAlbaranDoc: TEdit;
    edtFacturaDoc: TEdit;
    btnGenerarAlbaran: TBitBtn;

    // Preparación de Pedido en tsRecogida
    grpPreparacionPedido: TGroupBox;
    pnlPrepHeader: TPanel;
    lblPrepSSCC: TLabel;
    edtPrepSSCC: TEdit;
    lblPrepBultos: TLabel;
    edtPrepBultos: TEdit;
    btnPrepRefrescar: TButton;
    tvPreparacionBultos: TTreeView;

    // Pestaña EDI / Edicom
    tsEdi: TTabSheet;
    pnlEdiHeader: TPanel;
    lblEdiOrderType: TLabel;
    dbEdiOrderType: TDBEdit;
    lblEdiIncoterm: TLabel;
    dbEdiIncoterm: TDBEdit;
    lblFechaLimite: TLabel;
    dbFechaLimite: TDBEdit;
    lblEdiIdenticket: TLabel;
    dbEdiIdenticket: TDBEdit;
    pnlLeftEdi: TPanel;
    lblHeaderDiscounts: TLabel;
    gridHeaderDiscounts: TDBGrid;
    splitterEdi: TSplitter;
    pnlRightEdi: TPanel;
    lblLineDiscounts: TLabel;
    gridLineDiscounts: TDBGrid;
    qHeaderDiscounts: TFDQuery;
    dsHeaderDiscounts: TDataSource;
    qLineDiscounts: TFDQuery;
    dsLineDiscounts: TDataSource;

    procedure FormShow(Sender: TObject);
    procedure btnSaveClick(Sender: TObject);
    procedure btnSalirClick(Sender: TObject);
    procedure qMasterBeforePost(DataSet: TDataSet);
    procedure qDetailBeforePost(DataSet: TDataSet);
    procedure qDetailAfterPost(DataSet: TDataSet);
    procedure qMasterAfterScroll(DataSet: TDataSet);
    procedure dbClientCloseUp(Sender: TObject);
    procedure dsMasterDataChange(Sender: TObject; Field: TField);
    procedure btnAddLineClick(Sender: TObject);
    procedure btnDelLineClick(Sender: TObject);
    procedure btnTipsaRecogidaClick(Sender: TObject);
    procedure btnGenerarAlbaranClick(Sender: TObject);
    procedure btnVerCargaClick(Sender: TObject);
    procedure btnRegistrarFirmaClick(Sender: TObject);
    procedure btnImprimirEtiquetasClick(Sender: TObject);
    procedure btnDocsRelacionadosClick(Sender: TObject);
    procedure btnPasarAProduccionClick(Sender: TObject);
    procedure btnImprimirClick(Sender: TObject); override;
    procedure btnBuscarClienteClick(Sender: TObject);
    procedure btnNuevoClienteClick(Sender: TObject);
    procedure btnBuscarDestinoClick(Sender: TObject);
    procedure btnNuevoDestinoClick(Sender: TObject);
    procedure btnPrepRefrescarClick(Sender: TObject);
  protected
    procedure Loaded; override;
    function IsNewRecord: Boolean; override;
    procedure DoAnadir; override;
    procedure DoModificar; override;
    procedure DoPrimero; override;
    procedure DoAnterior; override;
    procedure DoSiguiente; override;
    procedure DoUltimo; override;
    procedure DoDuplicar; override;
  private
    FPedidoId: string;
    FParentForm: TForm;
    FIsNewRecord: Boolean;
    FSuggestedEmpresaId: Integer;
    FRecogidaId: Integer;
    FConfigId: Integer;
    FCodRecogida: string;
    lblTotalPalets: TLabel;
    edtTotalPalets: TEdit;

    procedure SetupTotalPaletsControl;
    procedure SetupAlbaranFacturaControls;
    procedure CargarInfoAlbaranFactura;
    procedure LoadData;
    procedure CalcularTotales;
    procedure FormatNumericFields(DataSet: TDataSet);
    procedure SetupDetailFieldProperties;
    procedure RecalcularBultosLineas;
    procedure RecalcularBultosLinea(DataSet: TDataSet);
    procedure UpdateClienteDetails;
    procedure UpdateDestinoDetails;
    procedure pgcDetailsChange(Sender: TObject);
    function ValidarYAsegurarDestino: Boolean;
    function GenerarAlbaranDesdePedido(const APedidoId: Integer; var AOutAlbId: Integer; var AOutAlbCode: string; var AErrorMsg: string): Boolean;
    function ObtenerAlbaranIdVinculado: Integer;
    procedure CargarArbolPreparacionPedido;
    function GenerarTextoPreparacionPedido(const AAlbaranId: Integer): string;
    function GenerarAlbaranCargaBasico(const AAlbaranId: Integer): string;
  public
    property PedidoId: string read FPedidoId write FPedidoId;
    property ParentForm: TForm read FParentForm write FParentForm;
    property SuggestedEmpresaId: Integer read FSuggestedEmpresaId write FSuggestedEmpresaId;
  end;

var
  frmPedidosEditor: TfrmPedidosEditor;

implementation

uses
  dmg_Main, uDbErrorHandler, frm_Pedidos, uAppTheme, frm_SelectProduct, frm_EnviosRecogida, frm_FirmaRecogida, Winapi.ShellAPI,
  uEnviosClient, uEnviosTypes, System.UITypes, System.Generics.Collections, System.Math, frm_AlbaranEnvioEditor, frm_AlbaranesEditor, frm_DocumentosRelacionados,
  uPedidoPrintService, frm_OrdenProduccionEditor, frm_SelectCliente, frm_SelectDestino, frm_ClientesEditor, frm_ClientesDestinosEditor,
  frm_SelectImpresoraEnvio, uPdfRotateService;

{$R *.dfm}

procedure TfrmPedidosEditor.Loaded;
begin
  inherited Loaded;
  qMaster.BeforePost := qMasterBeforePost;
  qMaster.SQL.Text := 'SELECT * FROM ge_pedidos WHERE id = :id';
  qDetail.SQL.Text := 'SELECT * FROM ge_pedidos_lineas WHERE id_pedido = :id';
  qClientList.SQL.Text := 'SELECT id, nombre_fiscal FROM ge_clientes WHERE activo = true ORDER BY nombre_fiscal';
  qDestList.SQL.Text := 'SELECT id, descripcion FROM ge_clientes_destinos WHERE id_cliente = :id_cliente AND activo = true ORDER BY descripcion';
  qDestList.ParamByName('id_cliente').DataType := ftInteger;
  qEmpresas.SQL.Text := 'SELECT id, nombre FROM ge_empresas WHERE activo = 1 ORDER BY nombre';
  qHeaderDiscounts.SQL.Text := 'SELECT tipo, codigo_motivo, porcentaje, importe FROM ge_pedidos_descuentos_cargos WHERE id_pedido = :id_pedido';
  qLineDiscounts.SQL.Text := 'SELECT l.descripcion AS articulo, ld.tipo, ld.codigo_motivo, ld.porcentaje, ld.importe FROM ge_pedidos_lineas_descuentos ld JOIN ge_pedidos_lineas l ON ld.id_pedido_linea = l.id WHERE l.id_pedido = :id_pedido';
end;

procedure TfrmPedidosEditor.FormShow(Sender: TObject);
begin
  TAppTheme.ApplyToForm(Self);
  TsListado.TabVisible := False;

  if not qEmpresas.Active or qEmpresas.IsEmpty then
  begin
    qEmpresas.Close;
    qEmpresas.Open;
  end;

  btnGuardar.OnClick := btnSaveClick;
  btnSalir.OnClick   := btnSalirClick;
  btnNuevoArticulo.OnClick := btnAddLineClick;
  btnEliminaArticulo.OnClick := btnDelLineClick;
  qDetail.AfterDelete := qDetailAfterPost;
  dbgItems.DataSource := dsDetail;

  // Configurar columnas de dbgItems en el orden exacto solicitado
  dbgItems.Columns.Clear;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'id_articulo';
    Title.Caption := 'Ref. Artículo';
    Width := 80;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'ean_producto';
    Title.Caption := 'EAN';
    Width := 110;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'descripcion';
    Title.Caption := 'Descripción';
    Width := 240;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'cantidad';
    Title.Caption := 'Cant.';
    Width := 55;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'bultos';
    Title.Caption := 'Bultos';
    Width := 55;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'numero_palets';
    Title.Caption := 'Nº Palets';
    Width := 65;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'precio';
    Title.Caption := 'Precio';
    Width := 75;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'total';
    Title.Caption := 'Total';
    Width := 85;
  end;

  lblClienteInfo.Caption := '';
  lblDestinoInfo.Caption := '';
  dsMaster.OnDataChange := dsMasterDataChange;

  if not qClientList.Active or qClientList.IsEmpty then
  begin
    qClientList.Close;
    qClientList.Open;
  end;

  dbOrigen.ReadOnly := True;
  dbNumPedido.TextHint := '(Auto: ID)';

  btnDuplicar.Visible := True;

  // Configurar botón Imprimir
  btnImprimir.Visible := True;
  btnImprimir.OnClick := btnImprimirClick;

  // Configurar botón Relacionados
  btnIA.Visible   := True;
  btnIA.Caption   := 'Relacionados';
  btnIA.ImageName := 'icon_documentos_relacionados';
  btnIA.Width     := 75;
  btnIA.OnClick   := btnDocsRelacionadosClick;

  // Configurar botón Producción (btnBuscar heredado)
  btnBuscar.Visible    := True;
  btnBuscar.Caption    := 'A Producción';
  btnBuscar.ImageIndex := 95;
  btnBuscar.ImageName  := 'Programación';
  btnBuscar.Width      := 85;
  btnBuscar.OnClick    := btnPasarAProduccionClick;

  // Configurar btnRect heredado para la solicitud de recogida
  btnRect.Visible   := True;
  btnRect.Caption   := 'Envíos';
  btnRect.ImageName := 'icon_envios_recogida';
  btnRect.Width     := 55;
  btnRect.OnClick   := btnTipsaRecogidaClick;
  btnRect.Enabled := False;

  // Configurar botón Generar Albarán
  if Assigned(btnGenerarAlbaran) then
  begin
    btnGenerarAlbaran.Visible := True;
    btnGenerarAlbaran.Caption := 'Albarán';
    btnGenerarAlbaran.ImageIndex := 6;
    btnGenerarAlbaran.ImageName := 'Albarán de Entrega';
    btnGenerarAlbaran.Width := 60;
    btnGenerarAlbaran.OnClick := btnGenerarAlbaranClick;
    btnGenerarAlbaran.Enabled := False;
  end;

  pgcDetails.OnChange := pgcDetailsChange;

  if FPedidoId <> '' then
    LoadData
  else
    DoAnadir;
end;

procedure TfrmPedidosEditor.SetupAlbaranFacturaControls;
begin
{  if not Assigned(lblAlbaranDoc) then
  begin
    lblAlbaranDoc := TLabel.Create(Self);
    lblAlbaranDoc.Parent := pnlHeader;
    lblAlbaranDoc.Caption := 'Albarán Venta:';
    lblAlbaranDoc.Font.Charset := DEFAULT_CHARSET;
    lblAlbaranDoc.Font.Color := clWindowText;
    lblAlbaranDoc.Font.Height := -12;
    lblAlbaranDoc.Font.Name := 'Segoe UI';
    lblAlbaranDoc.Font.Style := [fsBold];
    lblAlbaranDoc.Left := 16;
    lblAlbaranDoc.Top := 200;
  end;

  if not Assigned(edtAlbaranDoc) then
  begin
    edtAlbaranDoc := TEdit.Create(Self);
    edtAlbaranDoc.Parent := pnlHeader;
    edtAlbaranDoc.ReadOnly := True;
    edtAlbaranDoc.Font.Charset := DEFAULT_CHARSET;
    edtAlbaranDoc.Font.Color := clWindowText;
    edtAlbaranDoc.Font.Height := -12;
    edtAlbaranDoc.Font.Name := 'Segoe UI';
    edtAlbaranDoc.Font.Style := [fsBold];
    edtAlbaranDoc.Left := 110;
    edtAlbaranDoc.Top := 196;
    edtAlbaranDoc.Width := 120;
    edtAlbaranDoc.Height := 23;
    edtAlbaranDoc.Text := '—';
  end;

  if not Assigned(lblFacturaDoc) then
  begin
    lblFacturaDoc := TLabel.Create(Self);
    lblFacturaDoc.Parent := pnlHeader;
    lblFacturaDoc.Caption := 'Factura Venta:';
    lblFacturaDoc.Font.Charset := DEFAULT_CHARSET;
    lblFacturaDoc.Font.Color := clWindowText;
    lblFacturaDoc.Font.Height := -12;
    lblFacturaDoc.Font.Name := 'Segoe UI';
    lblFacturaDoc.Font.Style := [fsBold];
    lblFacturaDoc.Left := 244;
    lblFacturaDoc.Top := 200;
  end;

  if not Assigned(edtFacturaDoc) then
  begin
    edtFacturaDoc := TEdit.Create(Self);
    edtFacturaDoc.Parent := pnlHeader;
    edtFacturaDoc.ReadOnly := True;
    edtFacturaDoc.Font.Charset := DEFAULT_CHARSET;
    edtFacturaDoc.Font.Color := clWindowText;
    edtFacturaDoc.Font.Height := -12;
    edtFacturaDoc.Font.Name := 'Segoe UI';
    edtFacturaDoc.Font.Style := [fsBold];
    edtFacturaDoc.Left := 330;
    edtFacturaDoc.Top := 196;
    edtFacturaDoc.Width := 120;
    edtFacturaDoc.Height := 23;
    edtFacturaDoc.Text := '—';
  end;}
end;

procedure TfrmPedidosEditor.CargarInfoAlbaranFactura;
var
  Qry: TFDQuery;
  LAlbNum, LFacNum: string;
begin
  SetupAlbaranFacturaControls;

  if FPedidoId = '' then
  begin
    edtAlbaranDoc.Text := '—';
    edtFacturaDoc.Text := '—';
    Exit;
  end;

  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    Qry.SQL.Text :=
      'SELECT alb.serie AS albaran_serie, alb.numero AS albaran_numero, ' +
      '       fac.serie AS factura_serie, fac.numero AS factura_numero ' +
      'FROM ge_albaranes alb ' +
      'LEFT JOIN ge_facturas fac ON alb.id_factura = fac.id ' +
      'WHERE alb.id_pedido = :ped_id ' +
      '   OR alb.id IN (SELECT r.albaran_id FROM ge_envios_recogidas r WHERE r.pedido_id = :ped_id AND r.albaran_id IS NOT NULL) ' +
      'ORDER BY alb.id DESC ' +
      'LIMIT 1';
    Qry.ParamByName('ped_id').AsString := FPedidoId;
    Qry.Open;

    LAlbNum := '—';
    LFacNum := '—';

    if not Qry.IsEmpty then
    begin
      if not Qry.FieldByName('albaran_numero').IsNull and (Trim(Qry.FieldByName('albaran_numero').AsString) <> '') then
      begin
        if Trim(Qry.FieldByName('albaran_serie').AsString) <> '' then
          LAlbNum := Trim(Qry.FieldByName('albaran_serie').AsString) + '/' + Trim(Qry.FieldByName('albaran_numero').AsString)
        else
          LAlbNum := Trim(Qry.FieldByName('albaran_numero').AsString);
      end;

      if not Qry.FieldByName('factura_numero').IsNull and (Trim(Qry.FieldByName('factura_numero').AsString) <> '') then
      begin
        if Trim(Qry.FieldByName('factura_serie').AsString) <> '' then
          LFacNum := Trim(Qry.FieldByName('factura_serie').AsString) + '/' + Trim(Qry.FieldByName('factura_numero').AsString)
        else
          LFacNum := Trim(Qry.FieldByName('factura_numero').AsString);
      end;
    end;

    edtAlbaranDoc.Text := LAlbNum;
    edtFacturaDoc.Text := LFacNum;
  finally
    Qry.Free;
  end;
end;

procedure TfrmPedidosEditor.LoadData;
var
  Qry: TFDQuery;
  LIsEdi: Boolean;
begin
  FRecogidaId := 0;
  FConfigId   := 0;
  FCodRecogida := '';
  qMaster.Close;
  qMaster.ParamByName('id').AsString := FPedidoId;
  qMaster.Open;
  
  CargarInfoAlbaranFactura;

  LIsEdi := False;
  if not qMaster.IsEmpty then
  begin
    LIsEdi := (Trim(qMaster.FieldByName('edi_order_type').AsString) <> '') or
              (Trim(qMaster.FieldByName('edi_identicket').AsString) <> '');
  end;

  // Cargar las subconsultas de descuentos EDI solo si el pedido es de origen EDI o la pestaña EDI está visible
  if Assigned(qHeaderDiscounts) then
  begin
    qHeaderDiscounts.Close;
    if (FPedidoId <> '') and (LIsEdi or (Assigned(tsEdi) and (pgcDetails.ActivePage = tsEdi))) then
    begin
      if (qHeaderDiscounts.SQL.Count = 0) or (Trim(qHeaderDiscounts.SQL.Text) = '') then
        qHeaderDiscounts.SQL.Text := 'SELECT tipo, codigo_motivo, porcentaje, importe FROM ge_pedidos_descuentos_cargos WHERE id_pedido = :id_pedido';
      if qHeaderDiscounts.FindParam('id_pedido') <> nil then
        qHeaderDiscounts.ParamByName('id_pedido').AsString := FPedidoId;
      qHeaderDiscounts.Open;
    end;
  end;
  if Assigned(qLineDiscounts) then
  begin
    qLineDiscounts.Close;
    if (FPedidoId <> '') and (LIsEdi or (Assigned(tsEdi) and (pgcDetails.ActivePage = tsEdi))) then
    begin
      if (qLineDiscounts.SQL.Count = 0) or (Trim(qLineDiscounts.SQL.Text) = '') then
        qLineDiscounts.SQL.Text := 'SELECT l.descripcion AS articulo, ld.tipo, ld.codigo_motivo, ld.porcentaje, ld.importe FROM ge_pedidos_lineas_descuentos ld JOIN ge_pedidos_lineas l ON ld.id_pedido_linea = l.id WHERE l.id_pedido = :id_pedido';
      if qLineDiscounts.FindParam('id_pedido') <> nil then
        qLineDiscounts.ParamByName('id_pedido').AsString := FPedidoId;
      qLineDiscounts.Open;
    end;
  end;

  if not qMaster.IsEmpty then
  begin
    FIsNewRecord := False;
    qDestList.Close;
    qDestList.ParamByName('id_cliente').AsInteger := qMaster.FieldByName('cliente_id').AsInteger;
    qDestList.Open;

    dbEstado.Text := qMaster.FieldByName('estado').AsString;

    qDetail.Close;
    qDetail.ParamByName('id').AsInteger := qMaster.FieldByName('id').AsInteger;
    qDetail.Open;
    SetupDetailFieldProperties;
    RecalcularBultosLineas;
    FormatNumericFields(qDetail);
    CalcularTotales;

    // Habilitar btnRect si el pedido está guardado
    btnRect.Enabled := FPedidoId <> '';

    // Cargar detalles de recogida (excluyendo envíos anulados)
    lblInfoRecogida.Visible := True;
    grpRecogidaInfo.Visible := False;
    btnRect.Caption := 'Solicitar Recogida';
    btnRect.Width   := 65;
    
    Qry := TFDQuery.Create(nil);
    try
      Qry.Connection := dmgMain.dbConn;
      Qry.SQL.Text :=
        'SELECT r.id, r.config_id, r.cod_recogida, r.fecha_recogida, r.num_bultos, t.nombre AS transportista_nombre, ' +
        '       r.estado_descripcion, a.numero AS albaran_numero, a.serie AS albaran_serie, ' +
        '       r.conductor, r.matricula ' +
        'FROM ge_envios_recogidas r ' +
        'JOIN ge_envios_config c ON r.config_id = c.id ' +
        'JOIN ge_envios_transportistas t ON c.transportista_id = t.id ' +
        'LEFT JOIN ge_albaranes a ON r.albaran_id = a.id ' +
        'WHERE r.pedido_id = :ped AND r.estado_codigo <> 5';
      Qry.ParamByName('ped').AsInteger := qMaster.FieldByName('id').AsInteger;
      Qry.Open;
      
      if not Qry.IsEmpty then
      begin
        FRecogidaId := Qry.FieldByName('id').AsInteger;
        FConfigId   := Qry.FieldByName('config_id').AsInteger;
        FCodRecogida := Qry.FieldByName('cod_recogida').AsString;
        lblInfoRecogida.Visible := False;
        grpRecogidaInfo.Visible := True;
        
        btnRect.Caption := 'Ver Envío';
        btnRect.Width   := 75;
        
        edtRecTransp.Text := Qry.FieldByName('transportista_nombre').AsString;
        edtRecCodigo.Text := Qry.FieldByName('cod_recogida').AsString;
        edtRecFecha.Text  := FormatDateTime('dd/mm/yyyy', Qry.FieldByName('fecha_recogida').AsDateTime);
        edtRecBultos.Text := Qry.FieldByName('num_bultos').AsString;
        edtRecEstado.Text := Qry.FieldByName('estado_descripcion').AsString;
        edtRecConductor.Text := Qry.FieldByName('conductor').AsString;
        edtRecMatricula.Text := Qry.FieldByName('matricula').AsString;
        
        if not Qry.FieldByName('albaran_numero').IsNull then
          edtRecAlbaran.Text := Qry.FieldByName('albaran_serie').AsString + '/' + Qry.FieldByName('albaran_numero').AsString
        else
          edtRecAlbaran.Text := '—';
      end;
    finally
      Qry.Free;
    end;

    // Cargar preparación del pedido en el árbol de tsRecogida
    CargarArbolPreparacionPedido;
  end;

  if Assigned(btnGenerarAlbaran) then
    btnGenerarAlbaran.Enabled := (FPedidoId <> '') and (not qMaster.IsEmpty);
end;

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

procedure TfrmPedidosEditor.DoAnadir;
begin
  FIsNewRecord := True;
  FPedidoId := '';
  CargarInfoAlbaranFactura;

  if Assigned(btnGenerarAlbaran) then
    btnGenerarAlbaran.Enabled := False;

  if Assigned(qHeaderDiscounts) then
    qHeaderDiscounts.Close;
  if Assigned(qLineDiscounts) then
    qLineDiscounts.Close;
  if Assigned(qDestList) then
    qDestList.Close;
  
  qMaster.Close;
  qMaster.ParamByName('id').AsString := '';
  qMaster.Open;
  qMaster.Append;
  if FSuggestedEmpresaId > 0 then
    qMaster.FieldByName('empresa_id').AsInteger := FSuggestedEmpresaId
  else
    qMaster.FieldByName('empresa_id').AsInteger := StrToIntDef(dmgMain.CurrentCompanyId, 1);
  qMaster.FieldByName('fecha_pedido').AsDateTime := Date;
  qMaster.FieldByName('fecha_entrega').AsDateTime := Date + 7;
  qMaster.FieldByName('estado').AsString := 'PENDIENTE';
  dbEstado.Text := 'PENDIENTE';
  btnRect.Enabled := False;
  
  // Limpiar detalles de recogida
  lblInfoRecogida.Visible := True;
  grpRecogidaInfo.Visible := False;
  edtRecTransp.Clear;
  edtRecCodigo.Clear;
  edtRecFecha.Clear;
  edtRecBultos.Clear;
  edtRecEstado.Clear;
  edtRecAlbaran.Clear;
  if Assigned(tvPreparacionBultos) then
    tvPreparacionBultos.Items.Clear;
  if Assigned(edtPrepSSCC) then
    edtPrepSSCC.Clear;
  if Assigned(edtPrepBultos) then
    edtPrepBultos.Clear;
  
  qDetail.Close;
  qDetail.ParamByName('id').AsInteger := 0;
  qDetail.Open;
  SetupDetailFieldProperties;
  RecalcularBultosLineas;
  
  dbClient.SetFocus;
end;

procedure TfrmPedidosEditor.DoModificar;
begin
  dbClient.SetFocus;
end;

procedure TfrmPedidosEditor.qMasterBeforePost(DataSet: TDataSet);
begin
  if DataSet.State = dsEdit then
  begin
    if (DataSet.FieldByName('id').AsInteger > 0) and (Trim(DataSet.FieldByName('numero_pedido').AsString) = '') then
      DataSet.FieldByName('numero_pedido').AsString := DataSet.FieldByName('id').AsString;
  end;
end;

procedure TfrmPedidosEditor.btnSaveClick(Sender: TObject);
var
  LIsInsert: Boolean;
  LNeedAutoNum: Boolean;
  LNewId: Int64;
  LQryUpd: TFDQuery;
begin
  try
    if qMaster.State in [dsInsert, dsEdit] then
    begin
      if (qMaster.FindField('empresa_id') <> nil) and 
         (qMaster.FieldByName('empresa_id').IsNull or (qMaster.FieldByName('empresa_id').AsInteger <= 0)) then
      begin
        ShowMessage('Debe seleccionar una empresa para el pedido.');
        if Assigned(dbEmpresa) and dbEmpresa.CanFocus then
          dbEmpresa.SetFocus;
        Exit;
      end;

      if (qMaster.FindField('cliente_id') <> nil) and 
         (qMaster.FieldByName('cliente_id').IsNull or (qMaster.FieldByName('cliente_id').AsInteger <= 0)) then
      begin
        ShowMessage('Debe seleccionar un cliente para el pedido.');
        if Assigned(dbClient) and dbClient.CanFocus then
          dbClient.SetFocus;
        Exit;
      end;

      if not ValidarYAsegurarDestino then
        Exit;

      qMaster.FieldByName('estado').AsString := dbEstado.Text;
      if (qMaster.FindField('cliente_solicitante_id') <> nil) then
      begin
        if qMaster.FieldByName('cliente_solicitante_id').IsNull or (qMaster.FieldByName('cliente_solicitante_id').AsInteger = 0) then
        begin
          if (qMaster.FindField('clientedestino_id') <> nil) and (not qMaster.FieldByName('clientedestino_id').IsNull) and (qMaster.FieldByName('clientedestino_id').AsInteger > 0) then
            qMaster.FieldByName('cliente_solicitante_id').AsInteger := qMaster.FieldByName('clientedestino_id').AsInteger;
        end;
      end;

      LIsInsert := (qMaster.State = dsInsert);
      LNeedAutoNum := Trim(qMaster.FieldByName('numero_pedido').AsString) = '';

      if not LIsInsert and LNeedAutoNum then
      begin
        // En edición, si el usuario borró o dejó vacío el número de pedido, asignar su propio ID
        qMaster.FieldByName('numero_pedido').AsString := qMaster.FieldByName('id').AsString;
        LNeedAutoNum := False;
      end
      else if LIsInsert and LNeedAutoNum then
      begin
        // En nueva inserción sin número de pedido, asignamos un identificador temporal único
        // para satisfacer NOT NULL y el índice UNIQUE(empresa_id, numero_pedido)
        qMaster.FieldByName('numero_pedido').AsString := '__TMP_' + FormatDateTime('yyyymmddhhnnsszzz', Now) + '_' + IntToStr(Random(10000));
      end;

      qMaster.Post;

      if LIsInsert then
      begin
        LNewId := qMaster.FieldByName('id').AsLargeInt;
        if LNewId <= 0 then
        begin
          dmgMain.qryExec.Close;
          dmgMain.qryExec.SQL.Text := 'SELECT LAST_INSERT_ID()';
          dmgMain.qryExec.Open;
          LNewId := dmgMain.qryExec.Fields[0].AsLargeInt;
          dmgMain.qryExec.Close;
        end;

        if LNeedAutoNum and (LNewId > 0) then
        begin
          LQryUpd := TFDQuery.Create(nil);
          try
            LQryUpd.Connection := dmgMain.dbConn;
            LQryUpd.SQL.Text := 'UPDATE ge_pedidos SET numero_pedido = :num WHERE id = :id';
            LQryUpd.ParamByName('num').AsString := IntToStr(LNewId);
            LQryUpd.ParamByName('id').AsLargeInt := LNewId;
            LQryUpd.ExecSQL;
          finally
            LQryUpd.Free;
          end;
        end;

        if LNewId > 0 then
        begin
          FPedidoId := IntToStr(LNewId);
          FIsNewRecord := False;

          // Recargar cabecera para sincronizar dataset en memoria y dbNumPedido
          qMaster.Close;
          qMaster.ParamByName('id').AsString := FPedidoId;
          qMaster.Open;
          btnRect.Enabled := True;
        end;
      end;
    end;

    if qDetail.State in [dsInsert, dsEdit] then
      qDetail.Post;
      
    FIsNewRecord := False;
    ResetChangeTracking;
    if Sender <> nil then
      ShowMessage('Pedido guardado correctamente.');
  except
    on E: Exception do
      TDbErrorHandler.HandleException(E, 'Error al guardar el pedido');
  end;
end;

procedure TfrmPedidosEditor.btnSalirClick(Sender: TObject);
begin
  if qMaster.State in [dsInsert, dsEdit] then
    qMaster.Cancel;
  if qDetail.State in [dsInsert, dsEdit] then
    qDetail.Cancel;
  if FPedidoId <> '' then
    ModalResult := mrOk
  else
    ModalResult := mrCancel;
  Close;
end;

procedure TfrmPedidosEditor.dbClientCloseUp(Sender: TObject);
begin
  if not VarIsNull(dbClient.KeyValue) then
  begin
    qDestList.Close;
    qDestList.ParamByName('id_cliente').AsInteger := StrToIntDef(VarToStr(dbClient.KeyValue), 0);
    qDestList.Open;
  end;
  ControlChanged(Sender);
end;

procedure TfrmPedidosEditor.qDetailBeforePost(DataSet: TDataSet);
var
  LCantidadModificada: Boolean;
  LEan: string;
  LArtId: Integer;
  LCantidad, LUnidadesPalet: Double;
  QryPalet: TFDQuery;
begin
  DataSet.FieldByName('id_pedido').AsInteger := qMaster.FieldByName('id').AsInteger;
  DataSet.FieldByName('total').AsFloat := DataSet.FieldByName('cantidad').AsFloat * DataSet.FieldByName('precio').AsFloat;
  
  LArtId := DataSet.FieldByName('id_articulo').AsInteger;
  LCantidad := DataSet.FieldByName('cantidad').AsFloat;

  // 1. Obtener EAN del artículo si ean_producto está vacío
  if (DataSet.FindField('ean_producto') <> nil) and (Trim(DataSet.FieldByName('ean_producto').AsString) = '') and (LArtId > 0) then
  begin
    QryPalet := TFDQuery.Create(nil);
    try
      QryPalet.Connection := dmgMain.dbConn;
      QryPalet.SQL.Text := 'SELECT ean FROM ge_articulos WHERE id = :art';
      QryPalet.ParamByName('art').AsInteger := LArtId;
      QryPalet.Open;
      if not QryPalet.IsEmpty and not QryPalet.FieldByName('ean').IsNull then
        DataSet.FieldByName('ean_producto').AsString := Trim(QryPalet.FieldByName('ean').AsString);
    finally
      QryPalet.Free;
    end;
  end;

  // 2. Calcular número de palets a partir de unidades_palet en ge_codigos_barras_completo
  if (DataSet.FindField('numero_palets') <> nil) and (LCantidad > 0) then
  begin
    LEan := '';
    if DataSet.FindField('ean_producto') <> nil then
      LEan := Trim(DataSet.FieldByName('ean_producto').AsString);

    QryPalet := TFDQuery.Create(nil);
    try
      QryPalet.Connection := dmgMain.dbConn;
      QryPalet.SQL.Text :=
        'SELECT unidades_palet FROM ge_codigos_barras_completo ' +
        'WHERE (ean_13 = :ean AND :ean <> '''') OR (articulo_id = :art AND :art > 0) ' +
        'LIMIT 1';
      QryPalet.ParamByName('ean').AsString := LEan;
      QryPalet.ParamByName('art').AsInteger := LArtId;
      QryPalet.Open;

      if not QryPalet.IsEmpty and not QryPalet.FieldByName('unidades_palet').IsNull then
      begin
        LUnidadesPalet := QryPalet.FieldByName('unidades_palet').AsFloat;
        if LUnidadesPalet > 0 then
          DataSet.FieldByName('numero_palets').AsFloat := LCantidad / LUnidadesPalet;
      end;
    finally
      QryPalet.Free;
    end;
  end;

  LCantidadModificada := False;
  if DataSet.State = dsEdit then
    LCantidadModificada := not VarSameValue(DataSet.FieldByName('cantidad').Value, DataSet.FieldByName('cantidad').OldValue);

  if LCantidadModificada or DataSet.FieldByName('bultos').IsNull or (DataSet.FieldByName('bultos').AsInteger = 0) then
    RecalcularBultosLinea(DataSet);
end;

procedure TfrmPedidosEditor.qDetailAfterPost(DataSet: TDataSet);
begin
  CalcularTotales;
end;

procedure TfrmPedidosEditor.qMasterAfterScroll(DataSet: TDataSet);
begin
  if not FIsNewRecord and not DataSet.FieldByName('id').IsNull then
  begin
    qDetail.Close;
    qDetail.ParamByName('id').AsInteger := DataSet.FieldByName('id').AsInteger;
    qDetail.Open;
    SetupDetailFieldProperties;
    RecalcularBultosLineas;
    FormatNumericFields(qDetail);
    CalcularTotales;
  end;
end;

procedure TfrmPedidosEditor.SetupTotalPaletsControl;
begin
  if not Assigned(lblTotalPalets) then
  begin
    lblTotalPalets := TLabel.Create(Self);
    lblTotalPalets.Parent := pnlFooter;
    lblTotalPalets.Caption := 'Total Palets:';
    lblTotalPalets.Font.Charset := DEFAULT_CHARSET;
    lblTotalPalets.Font.Color := clWindowText;
    lblTotalPalets.Font.Height := -13;
    lblTotalPalets.Font.Name := 'Segoe UI';
    lblTotalPalets.Font.Style := [fsBold];
    lblTotalPalets.Alignment := taRightJustify;
    lblTotalPalets.Left := 400;
    lblTotalPalets.Top := 72;
  end;

  if not Assigned(edtTotalPalets) then
  begin
    edtTotalPalets := TEdit.Create(Self);
    edtTotalPalets.Parent := pnlFooter;
    edtTotalPalets.ReadOnly := True;
    edtTotalPalets.Font.Charset := DEFAULT_CHARSET;
    edtTotalPalets.Font.Color := clWindowText;
    edtTotalPalets.Font.Height := -13;
    edtTotalPalets.Font.Name := 'Segoe UI';
    edtTotalPalets.Font.Style := [fsBold];
    edtTotalPalets.Left := 490;
    edtTotalPalets.Top := 69;
    edtTotalPalets.Width := 90;
    edtTotalPalets.Height := 25;
    edtTotalPalets.Text := '0';
  end;
end;

procedure TfrmPedidosEditor.CalcularTotales;
var
  LBase, LIva, LTotal, LTotalPalets: Double;
  Bookmark: TBookmark;
begin
  SetupTotalPaletsControl;
  LBase := 0;
  LTotalPalets := 0;
  if qDetail.Active and not qDetail.IsEmpty then
  begin
    Bookmark := qDetail.GetBookmark;
    qDetail.DisableControls;
    try
      qDetail.First;
      while not qDetail.Eof do
      begin
        LBase := LBase + qDetail.FieldByName('total').AsFloat;
        if qDetail.FindField('numero_palets') <> nil then
          LTotalPalets := LTotalPalets + qDetail.FieldByName('numero_palets').AsFloat;
        qDetail.Next;
      end;
    finally
      qDetail.GotoBookmark(Bookmark);
      qDetail.FreeBookmark(Bookmark);
      qDetail.EnableControls;
    end;
  end;
  
  LIva := LBase * 0.10; // IVA 10%
  LTotal := LBase + LIva;
  
  if qMaster.Active then
  begin
    if (Abs(qMaster.FieldByName('base_imponible').AsFloat - LBase) > 0.001) or
       (Abs(qMaster.FieldByName('iva').AsFloat - LIva) > 0.001) or
       (Abs(qMaster.FieldByName('total').AsFloat - LTotal) > 0.001) then
    begin
      if not (qMaster.State in [dsInsert, dsEdit]) then
        qMaster.Edit;
      qMaster.FieldByName('base_imponible').AsFloat := LBase;
      qMaster.FieldByName('iva').AsFloat := LIva;
      qMaster.FieldByName('total').AsFloat := LTotal;
    end;
  end;
  
  dbBase.Text := FormatFloat('#,##0.00', LBase);
  dbIva.Text := FormatFloat('#,##0.00', LIva);
  dbTotal.Text := FormatFloat('#,##0.00', LTotal);

  if Assigned(edtTotalPalets) then
    edtTotalPalets.Text := FormatFloat('#,##0.##', LTotalPalets);
end;

procedure TfrmPedidosEditor.SetupDetailFieldProperties;
var
  Fld: TField;
begin
  Fld := qDetail.FindField('bultos');
  if Assigned(Fld) then
  begin
    Fld.ReadOnly := False;
    if Fld is TNumericField then
      TNumericField(Fld).DisplayFormat := '0';
  end;

  Fld := qDetail.FindField('numero_palets');
  if Assigned(Fld) then
  begin
    Fld.ReadOnly := False;
    if Fld is TNumericField then
      TNumericField(Fld).DisplayFormat := '0.##';
  end;
end;

procedure TfrmPedidosEditor.RecalcularBultosLineas;
var
  Bookmark: TBookmark;
begin
  if qDetail.Active and not qDetail.IsEmpty then
  begin
    Bookmark := qDetail.GetBookmark;
    qDetail.DisableControls;
    qDetail.BeforePost := nil;
    qDetail.AfterPost := nil;
    try
      qDetail.First;
      while not qDetail.Eof do
      begin
        if qDetail.FieldByName('bultos').IsNull or (qDetail.FieldByName('bultos').AsInteger = 0) then
        begin
          qDetail.Edit;
          RecalcularBultosLinea(qDetail);
          qDetail.Post;
        end;
        qDetail.Next;
      end;
    finally
      qDetail.BeforePost := qDetailBeforePost;
      qDetail.AfterPost := qDetailAfterPost;
      qDetail.GotoBookmark(Bookmark);
      qDetail.FreeBookmark(Bookmark);
      qDetail.EnableControls;
    end;
  end;
end;

procedure TfrmPedidosEditor.RecalcularBultosLinea(DataSet: TDataSet);
var
  LArticuloId: Integer;
  LCantidad: Double;
  LBultos: Double;
  QryEan: TFDQuery;
  LRemaining: Double;
  LUnidades: Double;
  LCajas: Integer;
  i: Integer;
  LNextUnidades: Double;
  LCapacities: TArray<Double>;
begin
  LArticuloId := DataSet.FieldByName('id_articulo').AsInteger;
  LCantidad := DataSet.FieldByName('cantidad').AsFloat;
  
  if (LArticuloId = 0) or (LCantidad <= 0) then
  begin
    DataSet.FieldByName('bultos').AsInteger := 0;
    Exit;
  end;
  
  QryEan := TFDQuery.Create(nil);
  try
    QryEan.Connection := dmgMain.dbConn;
    QryEan.SQL.Text := 'SELECT unidades FROM ge_articulos_ean WHERE articulo_id = :art ORDER BY unidades DESC';
    QryEan.ParamByName('art').AsInteger := LArticuloId;
    QryEan.Open;
    
    SetLength(LCapacities, 0);
    while not QryEan.Eof do
    begin
      LUnidades := QryEan.FieldByName('unidades').AsFloat;
      if LUnidades > 0 then
      begin
        SetLength(LCapacities, Length(LCapacities) + 1);
        LCapacities[Length(LCapacities) - 1] := LUnidades;
      end;
      QryEan.Next;
    end;
    
    LRemaining := LCantidad;
    LBultos := 0;
    
    if Length(LCapacities) > 0 then
    begin
      for i := 0 to Length(LCapacities) - 1 do
      begin
        if LRemaining <= 0 then Break;
        
        LUnidades := LCapacities[i];
        
        // 1. Tomar cajas completas de este nivel
        if LRemaining >= LUnidades then
        begin
          LCajas := Trunc(LRemaining / LUnidades);
          LBultos := LBultos + LCajas;
          LRemaining := LRemaining - (LCajas * LUnidades);
        end;
        
        if LRemaining <= 0 then Break;
        
        // 2. Determinar capacidad del nivel inferior
        if i < Length(LCapacities) - 1 then
          LNextUnidades := LCapacities[i+1]
        else
          LNextUnidades := 0;
          
        // Si lo que queda supera al nivel inferior, usamos 1 caja de este nivel superior (optimización)
        if LRemaining > LNextUnidades then
        begin
          LBultos := LBultos + 1;
          LRemaining := 0;
        end;
      end;
    end;
    
    if LRemaining > 0 then
      LBultos := LBultos + 1;
      
    DataSet.FieldByName('bultos').AsInteger := Trunc(LBultos);
  finally
    QryEan.Free;
  end;
end;

procedure TfrmPedidosEditor.FormatNumericFields(DataSet: TDataSet);
var
  i: Integer;
  Fld: TField;
begin
  for i := 0 to DataSet.FieldCount - 1 do
  begin
    Fld := DataSet.Fields[i];
    if Fld is TNumericField then
    begin
      if SameText(Fld.FieldName, 'cantidad') then
        TNumericField(Fld).DisplayFormat := '0.##'
      else if SameText(Fld.FieldName, 'numero_palets') then
        TNumericField(Fld).DisplayFormat := '0.##'
      else if SameText(Fld.FieldName, 'precio') then
        TNumericField(Fld).DisplayFormat := '#,##0.00##'
      else if SameText(Fld.FieldName, 'total') then
        TNumericField(Fld).DisplayFormat := '#,##0.00';
    end;
  end;
end;

procedure TfrmPedidosEditor.DoPrimero;
var
  LParent: TfrmPedidos;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmPedidos) then
    LParent := TfrmPedidos(FParentForm)
  else if Assigned(frmPedidos) then
    LParent := frmPedidos;

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

procedure TfrmPedidosEditor.DoAnterior;
var
  LParent: TfrmPedidos;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmPedidos) then
    LParent := TfrmPedidos(FParentForm)
  else if Assigned(frmPedidos) then
    LParent := frmPedidos;

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

procedure TfrmPedidosEditor.DoSiguiente;
var
  LParent: TfrmPedidos;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmPedidos) then
    LParent := TfrmPedidos(FParentForm)
  else if Assigned(frmPedidos) then
    LParent := frmPedidos;

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

procedure TfrmPedidosEditor.DoUltimo;
var
  LParent: TfrmPedidos;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmPedidos) then
    LParent := TfrmPedidos(FParentForm)
  else if Assigned(frmPedidos) then
    LParent := frmPedidos;

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

procedure TfrmPedidosEditor.btnAddLineClick(Sender: TObject);
var
  i: Integer;
  LPrice: Double;
  QryEan: TFDQuery;
begin
  if (qMaster.State in [dsInsert, dsEdit]) or (qMaster.FieldByName('id').AsInteger <= 0) then
  begin
    btnSaveClick(nil);
    if (qMaster.FieldByName('id').AsInteger <= 0) or (qMaster.State in [dsInsert, dsEdit]) then
      Exit;
  end;

  if not Assigned(frmSelectProduct) then
    Application.CreateForm(TfrmSelectProduct, frmSelectProduct);
    
  frmSelectProduct.MultiSelect := True;
  
  if frmSelectProduct.ShowModal = mrOk then
  begin
    qDetail.DisableControls;
    try
      for i := 0 to frmSelectProduct.SelectedIds.Count - 1 do
      begin
        qDetail.Append;
        if not qMaster.FieldByName('id').IsNull then
          qDetail.FieldByName('id_pedido').AsInteger := qMaster.FieldByName('id').AsInteger;
          
        qDetail.FieldByName('id_articulo').AsString := frmSelectProduct.SelectedIds[i];
        qDetail.FieldByName('descripcion').AsString := frmSelectProduct.SelectedDescs[i];
        
        // Traer EAN del artículo a la línea si el artículo dispone de EAN
        if qDetail.FindField('ean_producto') <> nil then
        begin
          QryEan := TFDQuery.Create(nil);
          try
            QryEan.Connection := dmgMain.dbConn;
            QryEan.SQL.Text := 'SELECT ean FROM ge_articulos WHERE id = :art';
            QryEan.ParamByName('art').AsString := frmSelectProduct.SelectedIds[i];
            QryEan.Open;
            if not QryEan.IsEmpty and not QryEan.FieldByName('ean').IsNull then
              qDetail.FieldByName('ean_producto').AsString := Trim(QryEan.FieldByName('ean').AsString);
          finally
            QryEan.Free;
          end;
        end;

        LPrice := StrToFloatDef(frmSelectProduct.SelectedPrices[i], 0);
        if not qMaster.FieldByName('cliente_id').IsNull then
          LPrice := dmgMain.GetPrecioArticuloCliente(qMaster.FieldByName('cliente_id').AsString, frmSelectProduct.SelectedIds[i], qMaster.FieldByName('fecha_pedido').AsDateTime, LPrice);
        qDetail.FieldByName('precio').AsFloat := LPrice;
        qDetail.FieldByName('cantidad').AsFloat := 1;
        qDetail.FieldByName('total').AsFloat := LPrice;
        
        qDetail.Post;
      end;
    finally
      qDetail.EnableControls;
    end;
  end;
end;

procedure TfrmPedidosEditor.btnDelLineClick(Sender: TObject);
begin
  if not qDetail.IsEmpty then
    qDetail.Delete;
end;

procedure TfrmPedidosEditor.DoDuplicar;
var
  LOldPedidoId: Integer;
  LNewPedidoId: Int64;
  LBaseNum: string;
  LNewOrderNum: string;
  i: Integer;
  Qry: TFDQuery;
begin
  if qMaster.IsEmpty or (FPedidoId = '') then Exit;
  
  if MessageDlg('¿Está seguro de que desea duplicar este pedido?', mtConfirmation, [mbYes, mbNo], 0) <> mrYes then
    Exit;
    
  LOldPedidoId := StrToIntDef(FPedidoId, 0);
  if LOldPedidoId = 0 then Exit;
  
  // Generar un número de pedido nuevo único basado en el actual o secuencial
  LBaseNum := Trim(qMaster.FieldByName('numero_pedido').AsString);
  if LBaseNum = '' then
    LBaseNum := IntToStr(LOldPedidoId);
  LNewOrderNum := LBaseNum + '_C';
  
  // Asegurar que no se repita
  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    Qry.SQL.Text := 'SELECT id FROM ge_pedidos WHERE empresa_id = :emp AND numero_pedido = :num';
    Qry.ParamByName('emp').AsString := dmgMain.CurrentCompanyId;
    Qry.ParamByName('num').AsString := LNewOrderNum;
    Qry.Open;
    i := 1;
    while not Qry.IsEmpty do
    begin
      LNewOrderNum := LBaseNum + '_C' + IntToStr(i);
      Qry.Close;
      Qry.ParamByName('num').AsString := LNewOrderNum;
      Qry.Open;
      Inc(i);
    end;
  finally
    Qry.Free;
  end;
  
  dmgMain.dbConn.StartTransaction;
  try
    // 1. Duplicar cabecera (excluyendo id)
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text :=
      'INSERT INTO ge_pedidos (empresa_id, cliente_id, clientedestino_id, numero_pedido, fecha_pedido, fecha_entrega, estado, base_imponible, total, iva, observaciones, id_user_creator, id_user_update) ' +
      'SELECT empresa_id, cliente_id, clientedestino_id, :new_num, CURRENT_TIMESTAMP, DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 7 DAY), ''PENDIENTE'', base_imponible, total, iva, observaciones, :user, :user ' +
      'FROM ge_pedidos WHERE id = :old_id';
    dmgMain.qryExec.ParamByName('new_num').AsString := LNewOrderNum;
    dmgMain.qryExec.ParamByName('old_id').AsInteger := LOldPedidoId;
    dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
    dmgMain.qryExec.ExecSQL;
    
    // Obtener el ID insertado
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text := 'SELECT LAST_INSERT_ID()';
    dmgMain.qryExec.Open;
    LNewPedidoId := dmgMain.qryExec.Fields[0].AsLargeInt;
    
    // 2. Duplicar líneas
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text :=
      'INSERT INTO ge_pedidos_lineas (id_pedido, id_articulo, descripcion, cantidad, precio, total) ' +
      'SELECT :new_id, id_articulo, descripcion, cantidad, precio, total ' +
      'FROM ge_pedidos_lineas WHERE id_pedido = :old_id';
    dmgMain.qryExec.ParamByName('new_id').AsLargeInt := LNewPedidoId;
    dmgMain.qryExec.ParamByName('old_id').AsInteger := LOldPedidoId;
    dmgMain.qryExec.ExecSQL;
    
    dmgMain.dbConn.Commit;
    
    FPedidoId := IntToStr(LNewPedidoId);
    FIsNewRecord := False;
    LoadData;
    
    if Assigned(FParentForm) and (FParentForm is TfrmPedidos) then
      TfrmPedidos(FParentForm).btnRefreshClick(nil);
      
    ShowMessage('Pedido duplicado con éxito. Nuevo Nº Pedido: ' + LNewOrderNum);
  except
    on E: Exception do
    begin
      dmgMain.dbConn.Rollback;
      ShowMessage('Error al duplicar el pedido: ' + E.Message);
    end;
  end;
end;

// ---------------------------------------------------------------------------
// Generación completa del Albarán de Venta para un Pedido (con stock y series)
// ---------------------------------------------------------------------------
function TfrmPedidosEditor.GenerarAlbaranDesdePedido(const APedidoId: Integer; var AOutAlbId: Integer; var AOutAlbCode: string; var AErrorMsg: string): Boolean;
var
  Qry, QryLines, QryStock: TFDQuery;
  LEmpId, LCliId, LDestId: Integer;
  LNumPedido, LObs: string;
  LSerie: string;
  LNumero: Integer;
  LAlbId: Integer;
  LHasSeriesRow: Boolean;
  LNeedsHeader: Boolean;
  LPosicion: Integer;
  LArtId, LArtStockId: Integer;
  LDesc: string;
  LPrecio, LTipoIva, LCantTotalReq, LCantPendiente: Double;
  LBultosReq, LCantBulto, LBultosLinea: Double;
  LStockId: Integer;
  LDisp, LTomar: Double;
  LLoteCode: string;
  LFabFecha, LFechaCaducidad: TDateTime;
  LDiasVida: Integer;
  LHasCaducidad: Boolean;
  LBaseLinea, LIvaLinea, LTotalLinea: Double;
  LTotalBase, LTotalIva, LTotalDoc: Double;
  LFormaPagoId: Variant;
begin
  Result := False;
  AOutAlbId := 0;
  AOutAlbCode := '';
  AErrorMsg := '';
  LTotalBase := 0;
  LTotalIva  := 0;
  LTotalDoc  := 0;
  LAlbId     := 0;
  LNeedsHeader := True;
  LHasSeriesRow := False;

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

    // 1. Obtener datos del pedido
    Qry.SQL.Text :=
      'SELECT p.empresa_id, p.cliente_id, p.clientedestino_id, p.numero_pedido, p.observaciones, ' +
      '       c.id_forma_pago ' +
      'FROM ge_pedidos p ' +
      'LEFT JOIN ge_clientes c ON c.id = p.cliente_id ' +
      'WHERE p.id = :id';
    Qry.ParamByName('id').AsInteger := APedidoId;
    Qry.Open;
    if Qry.IsEmpty then
    begin
      AErrorMsg := 'No se encontró el pedido #' + IntToStr(APedidoId);
      Exit;
    end;

    LEmpId     := Qry.FieldByName('empresa_id').AsInteger;
    LCliId     := Qry.FieldByName('cliente_id').AsInteger;
    LDestId    := Qry.FieldByName('clientedestino_id').AsInteger;
    LNumPedido := Qry.FieldByName('numero_pedido').AsString;
    LObs       := Qry.FieldByName('observaciones').AsString;
    if not Qry.FieldByName('id_forma_pago').IsNull then
      LFormaPagoId := Qry.FieldByName('id_forma_pago').AsInteger
    else
      LFormaPagoId := Null;
    Qry.Close;

    // 2. Comprobar si ya existe un albarán vinculado previo
    Qry.SQL.Text :=
      'SELECT id, serie, numero FROM ge_albaranes WHERE id_pedido = :ped AND (anulado = 0 OR anulado IS NULL) LIMIT 1';
    Qry.ParamByName('ped').AsInteger := APedidoId;
    Qry.Open;
    if not Qry.IsEmpty then
    begin
      LAlbId := Qry.FieldByName('id').AsInteger;
      LSerie := Trim(Qry.FieldByName('serie').AsString);
      LNumero := Qry.FieldByName('numero').AsInteger;
      Qry.Close;

      // Comprobar si tiene líneas
      Qry.SQL.Text := 'SELECT COUNT(*) AS total_lineas FROM ge_albaranes_lineas WHERE albaran_id = :alb';
      Qry.ParamByName('alb').AsInteger := LAlbId;
      Qry.Open;
      if Qry.FieldByName('total_lineas').AsInteger > 0 then
      begin
        AOutAlbId := LAlbId;
        AOutAlbCode := LSerie + '/' + IntToStr(LNumero);
        Result := True;
        Exit;
      end;
      Qry.Close;

      // Existe cabecera pero está vacía
      LNeedsHeader := False;
    end
    else
      Qry.Close;

    // 3. Obtener serie oficial de la empresa
    LSerie := '';
    LNumero := 0;
    if dmgMain.GetSerieYNumeroDocumento(IntToStr(LEmpId), 'ALBARAN', Date, '', LSerie, LNumero) and (LSerie <> '') then
    begin
      LHasSeriesRow := True;
    end
    else
    begin
      Qry.SQL.Text :=
        'SELECT serie, sig_numero_albaran AS prox_num ' +
        'FROM ge_series ' +
        'WHERE empresa_id = :emp AND activo = 1 AND (es_abono = 0 OR es_abono IS NULL) ' +
        'ORDER BY por_defecto DESC, ' +
        '         (CASE WHEN (CURDATE() >= fecha_inicio OR fecha_inicio IS NULL) AND (CURDATE() <= fecha_fin OR fecha_fin IS NULL) THEN 1 ELSE 0 END) DESC, ' +
        '         serie ASC LIMIT 1';
      Qry.ParamByName('emp').AsInteger := LEmpId;
      Qry.Open;
      if not Qry.IsEmpty and (Trim(Qry.FieldByName('serie').AsString) <> '') then
      begin
        LSerie := Trim(Qry.FieldByName('serie').AsString);
        LNumero := Qry.FieldByName('prox_num').AsInteger;
        if LNumero <= 0 then LNumero := 1;
        LHasSeriesRow := True;
      end
      else
      begin
        LSerie := FormatDateTime('yy', Date);
        LNumero := 1;
      end;
      Qry.Close;
    end;

    // 4. Crear o actualizar cabecera del albarán
    if LNeedsHeader then
    begin
      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text :=
        'INSERT INTO ge_albaranes (' +
        '  empresa_id, cliente_id, clientedestino_id, serie, numero, fecha, ' +
        '  base_imponible, iva, total, id_pedido, id_forma_pago, referencia, observaciones1, id_user_creator, id_user_update' +
        ') VALUES (' +
        '  :emp, :cli, :dest, :ser, :num, NOW(), 0, 0, 0, :ped, :fpago, :ref, :obs, :user, :user' +
        ')';
      dmgMain.qryExec.ParamByName('emp').AsInteger := LEmpId;
      dmgMain.qryExec.ParamByName('cli').AsInteger := LCliId;
      if LDestId > 0 then
        dmgMain.qryExec.ParamByName('dest').AsInteger := LDestId
      else
      begin
        dmgMain.qryExec.ParamByName('dest').DataType := ftInteger;
        dmgMain.qryExec.ParamByName('dest').Clear;
      end;
      dmgMain.qryExec.ParamByName('ser').AsString  := LSerie;
      dmgMain.qryExec.ParamByName('num').AsInteger := LNumero;
      dmgMain.qryExec.ParamByName('ped').AsInteger := APedidoId;
      if not VarIsNull(LFormaPagoId) then
        dmgMain.qryExec.ParamByName('fpago').AsInteger := Integer(LFormaPagoId)
      else
      begin
        dmgMain.qryExec.ParamByName('fpago').DataType := ftInteger;
        dmgMain.qryExec.ParamByName('fpago').Clear;
      end;
      dmgMain.qryExec.ParamByName('ref').AsString  := 'Pedido ' + LNumPedido;
      dmgMain.qryExec.ParamByName('obs').AsString  := LObs;
      dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
      dmgMain.qryExec.ExecSQL;

      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text := 'SELECT LAST_INSERT_ID() AS id';
      dmgMain.qryExec.Open;
      LAlbId := dmgMain.qryExec.FieldByName('id').AsInteger;
      dmgMain.qryExec.Close;

      if LAlbId <= 0 then
      begin
        AErrorMsg := 'No se pudo recuperar el ID generado para el albarán.';
        Exit;
      end;

      if LHasSeriesRow then
      begin
        dmgMain.qryExec.SQL.Text :=
          'UPDATE ge_series SET sig_numero_albaran = :num + 1 WHERE empresa_id = :emp AND serie = :ser AND activo = 1';
        dmgMain.qryExec.ParamByName('num').AsInteger := LNumero;
        dmgMain.qryExec.ParamByName('emp').AsInteger := LEmpId;
        dmgMain.qryExec.ParamByName('ser').AsString  := LSerie;
        dmgMain.qryExec.ExecSQL;
      end;
    end
    else
    begin
      // Si el albarán existía pero con serie 'A' o vacía, actualizar a serie oficial
      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text :=
        'UPDATE ge_albaranes SET serie = :ser, numero = :num, updated_at = NOW(), id_user_update = :user ' +
        'WHERE id = :id AND (serie = ''A'' OR serie IS NULL OR TRIM(serie) = '''')';
      dmgMain.qryExec.ParamByName('ser').AsString  := LSerie;
      dmgMain.qryExec.ParamByName('num').AsInteger := LNumero;
      dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
      dmgMain.qryExec.ParamByName('id').AsInteger  := LAlbId;
      dmgMain.qryExec.ExecSQL;
      if dmgMain.qryExec.RowsAffected > 0 then
      begin
        if LHasSeriesRow then
        begin
          dmgMain.qryExec.Close;
          dmgMain.qryExec.SQL.Text :=
            'UPDATE ge_series SET sig_numero_albaran = :num + 1 WHERE empresa_id = :emp AND serie = :ser AND activo = 1';
          dmgMain.qryExec.ParamByName('num').AsInteger := LNumero;
          dmgMain.qryExec.ParamByName('emp').AsInteger := LEmpId;
          dmgMain.qryExec.ParamByName('ser').AsString  := LSerie;
          dmgMain.qryExec.ExecSQL;
        end;
      end;
    end;

    // 5. Copiar líneas del pedido y asignar existencias FIFO en ge_stock_envasado
    QryLines.SQL.Text :=
      'SELECT pl.id, pl.id_articulo, pl.descripcion, pl.cantidad, pl.precio, ' +
      '       COALESCE(pl.bultos, 0) AS bultos, ' +
      '       COALESCE(ti.tipo, 10.0) AS tipo_iva, ' +
      '       COALESCE(NULLIF(a.id_maestro, 0), pl.id_articulo) AS id_articulo_stock, ' +
      '       a.ean AS art_ean ' +
      'FROM ge_pedidos_lineas pl ' +
      'LEFT JOIN ge_articulos a ON a.id = pl.id_articulo ' +
      'LEFT JOIN ge_tipos_iva ti ON ti.id = a.id_tipo_iva ' +
      'WHERE pl.id_pedido = :id ' +
      'ORDER BY pl.id ASC';
    QryLines.ParamByName('id').AsInteger := APedidoId;
    QryLines.Open;

    LPosicion := 1;
    while not QryLines.Eof do
    begin
      LArtId         := QryLines.FieldByName('id_articulo').AsInteger;
      LArtStockId    := QryLines.FieldByName('id_articulo_stock').AsInteger;
      LDesc          := QryLines.FieldByName('descripcion').AsString;
      LCantTotalReq  := QryLines.FieldByName('cantidad').AsFloat;
      LPrecio        := QryLines.FieldByName('precio').AsFloat;
      LBultosReq     := QryLines.FieldByName('bultos').AsFloat;
      LTipoIva       := QryLines.FieldByName('tipo_iva').AsFloat;
      LCantPendiente := LCantTotalReq;

      if (LBultosReq > 0) and (LCantTotalReq > 0) then
        LCantBulto := LCantTotalReq / LBultosReq
      else
        LCantBulto := 0;

      if LCantTotalReq > 0 then
      begin
        // Buscar stock FIFO disponible
        QryStock.Close;
        QryStock.SQL.Text :=
          'SELECT se.id AS stock_id, ' +
          '       GREATEST(0, se.cantidad_unidades - COALESCE(se.cantidad_reservada, 0)) AS disponible, ' +
          '       COALESCE(NULLIF(l.codigo_antiguo, ''''), l.codigo) AS lote_codigo, ' +
          '       l.fecha AS fecha_fabricacion, a.dias_vida ' +
          'FROM ge_stock_envasado se ' +
          'LEFT JOIN ge_lotes l ON se.id_lote = l.id ' +
          'LEFT JOIN ge_articulos a ON se.id_articulo = a.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 (LCantPendiente > 0) do
        begin
          LStockId  := QryStock.FieldByName('stock_id').AsInteger;
          LDisp     := QryStock.FieldByName('disponible').AsFloat;
          LTomar    := Min(LCantPendiente, LDisp);
          LLoteCode := QryStock.FieldByName('lote_codigo').AsString;

          if LCantBulto > 0 then
            LBultosLinea := Ceil(LTomar / LCantBulto)
          else
            LBultosLinea := 0;

          LHasCaducidad   := False;
          LFechaCaducidad := 0;
          if (not QryStock.FieldByName('fecha_fabricacion').IsNull) and
             (not QryStock.FieldByName('dias_vida').IsNull) and
             (QryStock.FieldByName('dias_vida').AsInteger > 0) then
          begin
            LFabFecha := QryStock.FieldByName('fecha_fabricacion').AsDateTime;
            LDiasVida := QryStock.FieldByName('dias_vida').AsInteger;
            LFechaCaducidad := LFabFecha + LDiasVida;
            LHasCaducidad := True;
          end;

          LBaseLinea  := LTomar * LPrecio;
          LIvaLinea   := LBaseLinea * (LTipoIva / 100.0);
          LTotalLinea := LBaseLinea + LIvaLinea;

          LTotalBase := LTotalBase + LBaseLinea;
          LTotalIva  := LTotalIva + LIvaLinea;
          LTotalDoc  := LTotalDoc + LTotalLinea;

          dmgMain.qryExec.Close;
          dmgMain.qryExec.SQL.Text :=
            'INSERT INTO ge_albaranes_lineas (' +
            '  albaran_id, articulo_id, articulo_codigo, descripcion, cantidad, bultos, cantidad_bulto, precio, ' +
            '  base, iva, total, tipo_iva, lotes, fecha_caducidad, posicion, id_linea_pedido, edi_ean_enviado, id_user_creator, id_user_update' +
            ') VALUES (' +
            '  :alb, :art, :artcod, :desc, :cant, :bultos, :cant_bulto, :precio, ' +
            '  :base, :iva_cant, :tot, :iva_tipo, :lote, :fcad, :pos, :id_ped_lin, :ean, :user, :user)';
          dmgMain.qryExec.ParamByName('alb').AsInteger      := LAlbId;
          dmgMain.qryExec.ParamByName('art').AsInteger      := LArtId;
          dmgMain.qryExec.ParamByName('artcod').AsString    := IntToStr(LArtId);
          dmgMain.qryExec.ParamByName('desc').AsString      := LDesc;
          dmgMain.qryExec.ParamByName('cant').AsFloat       := LTomar;
          dmgMain.qryExec.ParamByName('bultos').AsFloat     := LBultosLinea;
          dmgMain.qryExec.ParamByName('cant_bulto').AsFloat := LCantBulto;
          dmgMain.qryExec.ParamByName('precio').AsFloat     := LPrecio;
          dmgMain.qryExec.ParamByName('base').AsFloat       := LBaseLinea;
          dmgMain.qryExec.ParamByName('iva_cant').AsFloat   := LIvaLinea;
          dmgMain.qryExec.ParamByName('tot').AsFloat        := LTotalLinea;
          dmgMain.qryExec.ParamByName('iva_tipo').AsFloat   := LTipoIva;

          dmgMain.qryExec.ParamByName('lote').DataType := ftString;
          if LLoteCode <> '' then
            dmgMain.qryExec.ParamByName('lote').AsString := LLoteCode
          else
            dmgMain.qryExec.ParamByName('lote').Clear;

          dmgMain.qryExec.ParamByName('fcad').DataType := ftDate;
          if LHasCaducidad then
            dmgMain.qryExec.ParamByName('fcad').AsDate := LFechaCaducidad
          else
            dmgMain.qryExec.ParamByName('fcad').Clear;

          dmgMain.qryExec.ParamByName('pos').AsInteger        := LPosicion;
          dmgMain.qryExec.ParamByName('id_ped_lin').AsInteger := QryLines.FieldByName('id').AsInteger;
          dmgMain.qryExec.ParamByName('ean').AsString         := QryLines.FieldByName('art_ean').AsString;
          dmgMain.qryExec.ParamByName('user').AsString        := dmgMain.CurrentUserId;
          dmgMain.qryExec.ExecSQL;

          // Aumentar cantidad reservada en ge_stock_envasado
          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 = :id';
          dmgMain.qryExec.ParamByName('cant').AsFloat  := LTomar;
          dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
          dmgMain.qryExec.ParamByName('id').AsInteger  := LStockId;
          dmgMain.qryExec.ExecSQL;

          LCantPendiente := LCantPendiente - LTomar;
          Inc(LPosicion);
          QryStock.Next;
        end;

        // Remanente sin lote si faltara stock
        if LCantPendiente > 0 then
        begin
          if LCantBulto > 0 then
            LBultosLinea := Ceil(LCantPendiente / LCantBulto)
          else
            LBultosLinea := 0;

          LBaseLinea  := LCantPendiente * LPrecio;
          LIvaLinea   := LBaseLinea * (LTipoIva / 100.0);
          LTotalLinea := LBaseLinea + LIvaLinea;

          LTotalBase := LTotalBase + LBaseLinea;
          LTotalIva  := LTotalIva + LIvaLinea;
          LTotalDoc  := LTotalDoc + LTotalLinea;

          dmgMain.qryExec.Close;
          dmgMain.qryExec.SQL.Text :=
            'INSERT INTO ge_albaranes_lineas (' +
            '  albaran_id, articulo_id, articulo_codigo, descripcion, cantidad, bultos, cantidad_bulto, precio, ' +
            '  base, iva, total, tipo_iva, posicion, id_linea_pedido, edi_ean_enviado, id_user_creator, id_user_update' +
            ') VALUES (' +
            '  :alb, :art, :artcod, :desc, :cant, :bultos, :cant_bulto, :precio, ' +
            '  :base, :iva_cant, :tot, :iva_tipo, :pos, :id_ped_lin, :ean, :user, :user)';
          dmgMain.qryExec.ParamByName('alb').AsInteger        := LAlbId;
          dmgMain.qryExec.ParamByName('art').AsInteger        := LArtId;
          dmgMain.qryExec.ParamByName('artcod').AsString      := IntToStr(LArtId);
          dmgMain.qryExec.ParamByName('desc').AsString        := LDesc;
          dmgMain.qryExec.ParamByName('cant').AsFloat         := LCantPendiente;
          dmgMain.qryExec.ParamByName('bultos').AsFloat       := LBultosLinea;
          dmgMain.qryExec.ParamByName('cant_bulto').AsFloat   := LCantBulto;
          dmgMain.qryExec.ParamByName('precio').AsFloat       := LPrecio;
          dmgMain.qryExec.ParamByName('base').AsFloat         := LBaseLinea;
          dmgMain.qryExec.ParamByName('iva_cant').AsFloat     := LIvaLinea;
          dmgMain.qryExec.ParamByName('tot').AsFloat          := LTotalLinea;
          dmgMain.qryExec.ParamByName('iva_tipo').AsFloat     := LTipoIva;
          dmgMain.qryExec.ParamByName('pos').AsInteger        := LPosicion;
          dmgMain.qryExec.ParamByName('id_ped_lin').AsInteger := QryLines.FieldByName('id').AsInteger;
          dmgMain.qryExec.ParamByName('ean').AsString         := QryLines.FieldByName('art_ean').AsString;
          dmgMain.qryExec.ParamByName('user').AsString        := dmgMain.CurrentUserId;
          dmgMain.qryExec.ExecSQL;

          Inc(LPosicion);
        end;
      end;

      QryLines.Next;
    end;

    // 6. Actualizar totales en la cabecera del albarán
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text :=
      'UPDATE ge_albaranes SET base_imponible = :base, iva = :iva, total = :tot WHERE id = :id';
    dmgMain.qryExec.ParamByName('base').AsFloat := LTotalBase;
    dmgMain.qryExec.ParamByName('iva').AsFloat  := LTotalIva;
    dmgMain.qryExec.ParamByName('tot').AsFloat  := LTotalDoc;
    dmgMain.qryExec.ParamByName('id').AsInteger := LAlbId;
    dmgMain.qryExec.ExecSQL;

    // 7. Actualizar estado del pedido si procede
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text :=
      'UPDATE ge_pedidos SET estado = ''EN PREPARACION'', id_user_update = :user ' +
      'WHERE id = :id AND (estado = ''PENDIENTE'' OR estado IS NULL)';
    dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
    dmgMain.qryExec.ParamByName('id').AsInteger  := APedidoId;
    dmgMain.qryExec.ExecSQL;

    AOutAlbId   := LAlbId;
    AOutAlbCode := Trim(LSerie) + '/' + IntToStr(LNumero);
    Result      := True;
  finally
    Qry.Free;
    QryLines.Free;
    QryStock.Free;
  end;
end;

// ---------------------------------------------------------------------------
// Acción del botón Generar Albarán desde PedidosEditor
// ---------------------------------------------------------------------------
procedure TfrmPedidosEditor.btnGenerarAlbaranClick(Sender: TObject);
var
  LIdPedido, LExistingAlbId: Integer;
  LExistingSerie, LExistingNum, LAlbCode, LErrorMsg: string;
  LCountLineas: Integer;
  LNewAlbId: Integer;
  Qry: TFDQuery;
begin
  if qMaster.IsEmpty or (FPedidoId = '') then
  begin
    ShowMessage('Debe guardar el pedido antes de generar un albarán.');
    Exit;
  end;

  LIdPedido := qMaster.FieldByName('id').AsInteger;
  if LIdPedido = 0 then
  begin
    ShowMessage('El pedido no tiene un ID válido.');
    Exit;
  end;

  if qMaster.State in [dsInsert, dsEdit] then
    qMaster.Post;
  if qDetail.State in [dsInsert, dsEdit] then
    qDetail.Post;

  // 1. Verificar si ya existe un albarán vinculado al pedido
  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    Qry.SQL.Text :=
      'SELECT id, serie, numero FROM ge_albaranes WHERE id_pedido = :ped AND (anulado = 0 OR anulado IS NULL) LIMIT 1';
    Qry.ParamByName('ped').AsInteger := LIdPedido;
    Qry.Open;

    if not Qry.IsEmpty then
    begin
      LExistingAlbId := Qry.FieldByName('id').AsInteger;
      LExistingSerie := Trim(Qry.FieldByName('serie').AsString);
      LExistingNum   := Qry.FieldByName('numero').AsString;
      Qry.Close;

      // Comprobar cuántas líneas tiene
      Qry.SQL.Text := 'SELECT COUNT(*) AS total_lineas FROM ge_albaranes_lineas WHERE albaran_id = :alb';
      Qry.ParamByName('alb').AsInteger := LExistingAlbId;
      Qry.Open;
      LCountLineas := Qry.FieldByName('total_lineas').AsInteger;
      Qry.Close;

      if LCountLineas > 0 then
      begin
        // El albarán ya está completo
        if MessageDlg(
          'Este pedido ya dispone del albarán de venta ' + LExistingSerie + '/' + LExistingNum + '.' + sLineBreak +
          '¿Desea abrirlo en el editor de albaranes?',
          mtConfirmation, [mbYes, mbNo], 0) = mrYes then
        begin
          if not Assigned(frmAlbaranesEditor) then
            Application.CreateForm(TfrmAlbaranesEditor, frmAlbaranesEditor);
          frmAlbaranesEditor.ParentForm := Self;
          frmAlbaranesEditor.AlbaranId := IntToStr(LExistingAlbId);
          frmAlbaranesEditor.ShowModal;
          LoadData;
          if Assigned(FParentForm) and (FParentForm is TfrmPedidos) then
            TfrmPedidos(FParentForm).btnRefreshClick(nil);
        end;
        Exit;
      end
      else
      begin
        // El albarán existe pero está vacío o incompleto (por ejemplo, con serie 'A')
        if MessageDlg(
          'Se ha detectado un albarán vinculado sin líneas o incompleto (' + LExistingSerie + '/' + LExistingNum + ').' + sLineBreak +
          '¿Desea completarlo ahora con la serie correcta de la empresa y reservar el stock correspondiente?',
          mtConfirmation, [mbYes, mbNo], 0) <> mrYes then
          Exit;
      end;
    end
    else
    begin
      Qry.Close;
      if MessageDlg(
        '¿Desea generar el albarán de venta para el pedido Nº ' + qMaster.FieldByName('numero_pedido').AsString + '?',
        mtConfirmation, [mbYes, mbNo], 0) <> mrYes then
        Exit;
    end;
  finally
    Qry.Free;
  end;

  // 2. Ejecutar la generación del albarán dentro de una transacción
  LNewAlbId := 0;
  dmgMain.dbConn.StartTransaction;
  try
    if not GenerarAlbaranDesdePedido(LIdPedido, LNewAlbId, LAlbCode, LErrorMsg) then
      raise Exception.Create(LErrorMsg);

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

  ShowMessage('Albarán de venta generado con éxito: ' + LAlbCode);

  LoadData;
  CargarInfoAlbaranFactura;
  if Assigned(FParentForm) and (FParentForm is TfrmPedidos) then
    TfrmPedidos(FParentForm).btnRefreshClick(nil);

  // Abrir el albarán generado
  if LNewAlbId > 0 then
  begin
    if not Assigned(frmAlbaranesEditor) then
      Application.CreateForm(TfrmAlbaranesEditor, frmAlbaranesEditor);
    frmAlbaranesEditor.ParentForm := Self;
    frmAlbaranesEditor.AlbaranId := IntToStr(LNewAlbId);
    frmAlbaranesEditor.ShowModal;
    LoadData;
    if Assigned(FParentForm) and (FParentForm is TfrmPedidos) then
      TfrmPedidos(FParentForm).btnRefreshClick(nil);
  end;
end;

// ---------------------------------------------------------------------------
// Abrir el formulario de Recogida TIPSA para el pedido activo
// ---------------------------------------------------------------------------
procedure TfrmPedidosEditor.btnTipsaRecogidaClick(Sender: TObject);
var
  LIdPedido: Integer;
  LRecId: Integer;
begin
  if qMaster.IsEmpty or (FPedidoId = '') then
  begin
    ShowMessage('Debe guardar el pedido antes de solicitar una recogida.');
    Exit;
  end;

  LIdPedido := qMaster.FieldByName('id').AsInteger;
  if LIdPedido = 0 then
  begin
    ShowMessage('El pedido no tiene un ID válido.');
    Exit;
  end;

  // Comprobar si ya existe una recogida registrada activa para este pedido en ge_envios_recogidas
  dmgMain.qryExec.Close;
  dmgMain.qryExec.SQL.Text := 'SELECT id FROM ge_envios_recogidas WHERE pedido_id = :ped AND estado_codigo <> 5 LIMIT 1';
  dmgMain.qryExec.ParamByName('ped').AsInteger := LIdPedido;
  dmgMain.qryExec.Open;
  if not dmgMain.qryExec.IsEmpty then
  begin
    LRecId := dmgMain.qryExec.FieldByName('id').AsInteger;
    dmgMain.qryExec.Close;
    
    // Abrir el albarán de envío directamente
    if not Assigned(frmAlbaranEnvioEditor) then
      Application.CreateForm(TfrmAlbaranEnvioEditor, frmAlbaranEnvioEditor);
      
    frmAlbaranEnvioEditor.RecogidaId := LRecId;
    if frmAlbaranEnvioEditor.ShowModal = mrOk then
    begin
      LoadData;
      if Assigned(FParentForm) and (FParentForm is TfrmPedidos) then
        TfrmPedidos(FParentForm).btnRefreshClick(nil);
    end;
    Exit;
  end;
  dmgMain.qryExec.Close;

  if SameText(Trim(qMaster.FieldByName('estado').AsString), 'ENVIADO') then
  begin
    ShowMessage('Este pedido ya fue enviado. No se puede crear otra recogida.');
    Exit;
  end;

  if qMaster.State in [dsInsert, dsEdit] then
    qMaster.Post;
  if qDetail.State in [dsInsert, dsEdit] then
    qDetail.Post;

  if not Assigned(frmEnviosRecogida) then
    Application.CreateForm(TfrmEnviosRecogida, frmEnviosRecogida);

  frmEnviosRecogida.PedidoId := LIdPedido;

  try
    if frmEnviosRecogida.ShowModal = mrOk then
    begin
      var LGeneratedAlbId := frmEnviosRecogida.AlbaranId;

      // Recargar datos del pedido — el estado habrá cambiado a PROCESADO
      LoadData;
      // Refrescar el listado padre si está visible
      if Assigned(FParentForm) and (FParentForm is TfrmPedidos) then
        TfrmPedidos(FParentForm).btnRefreshClick(nil);

      // Abrir automáticamente el albarán de venta generado
      if LGeneratedAlbId > 0 then
      begin
        if not Assigned(frmAlbaranesEditor) then
          Application.CreateForm(TfrmAlbaranesEditor, frmAlbaranesEditor);
        frmAlbaranesEditor.ParentForm := Self;
        frmAlbaranesEditor.AlbaranId := IntToStr(LGeneratedAlbId);
        frmAlbaranesEditor.ShowModal;
      end;
    end
    else
    begin
      // Si se cancela o falla, recargar los datos de la base de datos para restaurar el estado visual
      LoadData;
    end;
  except
    on E: Exception do
    begin
      LoadData;
      raise;
    end;
  end;
end;

procedure TfrmPedidosEditor.btnPrepRefrescarClick(Sender: TObject);
begin
  CargarArbolPreparacionPedido;
end;

function TfrmPedidosEditor.ObtenerAlbaranIdVinculado: Integer;
var
  Qry: TFDQuery;
begin
  Result := 0;
  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;

    // 1. Si tenemos FRecogidaId, consultar albaran_id en ge_envios_recogidas
    if FRecogidaId > 0 then
    begin
      Qry.SQL.Text := 'SELECT albaran_id FROM ge_envios_recogidas WHERE id = :id';
      Qry.ParamByName('id').AsInteger := FRecogidaId;
      Qry.Open;
      if not Qry.IsEmpty and not Qry.FieldByName('albaran_id').IsNull then
        Result := Qry.FieldByName('albaran_id').AsInteger;
      Qry.Close;
    end;

    // 2. Si no lo encontramos y hay pedido guardado, buscar en ge_albaranes por id_pedido
    if (Result <= 0) and (FPedidoId <> '') and (StrToIntDef(FPedidoId, 0) > 0) then
    begin
      Qry.SQL.Text := 'SELECT id FROM ge_albaranes WHERE id_pedido = :ped ORDER BY id DESC LIMIT 1';
      Qry.ParamByName('ped').AsInteger := StrToIntDef(FPedidoId, 0);
      Qry.Open;
      if not Qry.IsEmpty and not Qry.FieldByName('id').IsNull then
        Result := Qry.FieldByName('id').AsInteger;
      Qry.Close;
    end;

    // 3. Si aún no, buscar en ge_envios_recogidas por pedido_id
    if (Result <= 0) and (FPedidoId <> '') and (StrToIntDef(FPedidoId, 0) > 0) then
    begin
      Qry.SQL.Text := 'SELECT albaran_id FROM ge_envios_recogidas WHERE pedido_id = :ped AND albaran_id IS NOT NULL ORDER BY id DESC LIMIT 1';
      Qry.ParamByName('ped').AsInteger := StrToIntDef(FPedidoId, 0);
      Qry.Open;
      if not Qry.IsEmpty and not Qry.FieldByName('id').IsNull then
        Result := Qry.FieldByName('albaran_id').AsInteger;
      Qry.Close;
    end;
  finally
    Qry.Free;
  end;
end;

procedure TfrmPedidosEditor.CargarArbolPreparacionPedido;
var
  QryBultos, QryContenido, QrySummary: TFDQuery;
  LAlbId: Integer;
  NodePalet, NodeEnvase, NodeContenido, ParentNode: TTreeNode;
  LBultoMap: TDictionary<Integer, TTreeNode>;
  LBultoId, LPadreId, LNivel, LNumBulto: Integer;
  LSscc, LEan, LLote, LCadStr, LText: string;
begin
  if not Assigned(tvPreparacionBultos) then Exit;

  tvPreparacionBultos.Items.BeginUpdate;
  LBultoMap := TDictionary<Integer, TTreeNode>.Create;
  try
    tvPreparacionBultos.Items.Clear;
    if Assigned(edtPrepSSCC) then edtPrepSSCC.Text := '—';
    if Assigned(edtPrepBultos) then edtPrepBultos.Text := '0';

    LAlbId := ObtenerAlbaranIdVinculado;
    if LAlbId <= 0 then
    begin
      tvPreparacionBultos.Items.Add(nil, '(Sin albarán ni preparación vinculada al pedido)');
      Exit;
    end;

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

      // Resumen de SSCC principal y cantidad total de bultos
      QrySummary.SQL.Text :=
        'SELECT ' +
        '  (SELECT sscc FROM ge_albaranes_bultos WHERE albaran_id = :alb1 AND nivel = 2 ORDER BY id ASC LIMIT 1) AS primer_sscc, ' +
        '  (SELECT COUNT(*) FROM ge_albaranes_bultos WHERE albaran_id = :alb2 AND (nivel = 2 OR (nivel = 3 AND bulto_padre_id IS NULL))) AS total_bultos';
      QrySummary.ParamByName('alb1').AsInteger := LAlbId;
      QrySummary.ParamByName('alb2').AsInteger := LAlbId;
      QrySummary.Open;
      if not QrySummary.IsEmpty then
      begin
        if not QrySummary.FieldByName('primer_sscc').IsNull and (Trim(QrySummary.FieldByName('primer_sscc').AsString) <> '') then
        begin
          if Assigned(edtPrepSSCC) then
            edtPrepSSCC.Text := QrySummary.FieldByName('primer_sscc').AsString;
        end;
        if Assigned(edtPrepBultos) then
          edtPrepBultos.Text := QrySummary.FieldByName('total_bultos').AsString;
      end;
      QrySummary.Close;

      // 1. Cargar bultos (Palets y Cajas)
      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;

      if QryBultos.IsEmpty then
      begin
        tvPreparacionBultos.Items.Add(nil, '(Sin bultos de preparación registrados para este pedido)');
        Exit;
      end;

      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 := tvPreparacionBultos.Items.AddChild(nil, LText);
          NodePalet.Data := TObject(IntPtr(LBultoId));
          LBultoMap.AddOrSetValue(LBultoId, NodePalet);
        end;

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

        QryBultos.Next;
      end;

      // 2. Cargar líneas de contenido
      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 := tvPreparacionBultos.Items.AddChild(ParentNode, LText);
          NodeContenido.Data := TObject(IntPtr(LBultoId));
        end;

        QryContenido.Next;
      end;

      tvPreparacionBultos.FullExpand;
    finally
      QryBultos.Free;
      QryContenido.Free;
      QrySummary.Free;
    end;
  finally
    LBultoMap.Free;
    tvPreparacionBultos.Items.EndUpdate;
  end;
end;

function TfrmPedidosEditor.GenerarTextoPreparacionPedido(const AAlbaranId: Integer): string;
var
  QryBultos, QryContenido: TFDQuery;
  LDoc: TStringList;
  LBultoId, LNivel, LNumBulto: Integer;
  LSscc, LEan, LLote, LCadStr: string;
begin
  Result := '';
  if AAlbaranId <= 0 then Exit;

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

    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 := AAlbaranId;
    QryBultos.Open;

    if QryBultos.IsEmpty then Exit;

    LDoc.Add('───────────────────────────────────────────────────');
    LDoc.Add('DETALLE DE PREPARACIÓN DEL PEDIDO (BULTOS Y LOTES):');
    LDoc.Add('───────────────────────────────────────────────────');

    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;

      if LNivel = 2 then
      begin
        LDoc.Add(Format('  [PALET #%d] SSCC: %s (Tipo: %s)', [
          LNumBulto, LSscc, QryBultos.FieldByName('tipo_embalaje').AsString
        ]));
      end
      else if LNivel = 3 then
      begin
        LDoc.Add(Format('    - [CAJA/ENVASE #%d] (Tipo: %s)', [
          LNumBulto, QryBultos.FieldByName('tipo_embalaje').AsString
        ]));
      end;

      // Cargar artículos en este bulto
      QryContenido.Close;
      QryContenido.SQL.Text :=
        'SELECT 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 lb.bulto_id = :bulto ' +
        'ORDER BY lb.id ASC';
      QryContenido.ParamByName('bulto').AsInteger := LBultoId;
      QryContenido.Open;

      while not QryContenido.Eof do
      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);

        if LNivel = 2 then
          LDoc.Add(Format('      * Art: %s (EAN: %s) | Lote: %s | Cad: %s | Cant: %s uds', [
            QryContenido.FieldByName('art_desc').AsString,
            LEan, LLote, LCadStr,
            FormatFloat('0.##', QryContenido.FieldByName('cantidad').AsFloat)
          ]))
        else
          LDoc.Add(Format('        * Art: %s (EAN: %s) | Lote: %s | Cad: %s | Cant: %s uds', [
            QryContenido.FieldByName('art_desc').AsString,
            LEan, LLote, LCadStr,
            FormatFloat('0.##', QryContenido.FieldByName('cantidad').AsFloat)
          ]));

        QryContenido.Next;
      end;

      QryBultos.Next;
    end;

    LDoc.Add('');
    Result := LDoc.Text;
  finally
    LDoc.Free;
    QryContenido.Free;
    QryBultos.Free;
  end;
end;

function TfrmPedidosEditor.GenerarAlbaranCargaBasico(const AAlbaranId: Integer): string;
var
  LDoc: TStringList;
  LQry: TFDQuery;
begin
  LDoc := TStringList.Create;
  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    LDoc.Add('===================================================');
    LDoc.Add('           ALBARÁN DE CARGA / EXPEDICIÓN           ');
    LDoc.Add('===================================================');
    LDoc.Add('Fecha emisión             : ' + FormatDateTime('dd/mm/yyyy hh:nn', Now));
    LDoc.Add('Nº Recogida / Ref.        : ' + FCodRecogida);
    LDoc.Add('Transportista             : ' + edtRecTransp.Text);
    LDoc.Add('Pedido                    : ' + dbNumPedido.Text);
    LDoc.Add('Albarán generado          : ' + edtRecAlbaran.Text);
    LDoc.Add('Fecha recogida            : ' + edtRecFecha.Text);
    LDoc.Add('Nº de bultos              : ' + edtRecBultos.Text);
    if Trim(edtRecConductor.Text) <> '' then
      LDoc.Add('Conductor / Transportista : ' + Trim(edtRecConductor.Text));
    if Trim(edtRecMatricula.Text) <> '' then
      LDoc.Add('Matrícula vehículo        : ' + Trim(edtRecMatricula.Text));
    LDoc.Add('───────────────────────────────────────────────────');
    LDoc.Add('DESTINATARIO:');
    if Trim(lblDestinoInfo.Caption) <> '' then
      LDoc.Add('  ' + StringReplace(string(lblDestinoInfo.Caption), sLineBreak, sLineBreak + '  ', [rfReplaceAll]))
    else if Trim(lblClienteInfo.Caption) <> '' then
      LDoc.Add('  ' + StringReplace(string(lblClienteInfo.Caption), sLineBreak, sLineBreak + '  ', [rfReplaceAll]));
    LDoc.Add('───────────────────────────────────────────────────');
    LDoc.Add('LÍNEAS DEL DOCUMENTO:');
    LDoc.Add('');

    if AAlbaranId > 0 then
    begin
      LQry.SQL.Text := 'SELECT descripcion, cantidad, COALESCE(bultos, 0) AS bultos FROM ge_albaranes_lineas WHERE albaran_id = :alb';
      LQry.ParamByName('alb').AsInteger := AAlbaranId;
    end
    else
    begin
      LQry.SQL.Text := 'SELECT descripcion, cantidad, COALESCE(bultos, 0) AS bultos FROM ge_pedidos_lineas WHERE id_pedido = :id';
      LQry.ParamByName('id').AsInteger := StrToIntDef(FPedidoId, 0);
    end;
    LQry.Open;
    while not LQry.Eof do
    begin
      LDoc.Add(Format('  %-40s  %8s ud. (%d bultos)', [
        LQry.FieldByName('descripcion').AsString,
        FormatFloat('#,##0.##', LQry.FieldByName('cantidad').AsFloat),
        LQry.FieldByName('bultos').AsInteger
      ]));
      LQry.Next;
    end;
    LDoc.Add('');
    LDoc.Add('═══════════════════════════════════════════════════');
    LDoc.Add('Firma del transportista / conductor:');
    LDoc.Add('');
    LDoc.Add('___________________________________________________');
    LDoc.Add('Generado: ' + FormatDateTime('dd/mm/yyyy hh:nn:ss', Now));

    Result := LDoc.Text;
  finally
    LQry.Free;
    LDoc.Free;
  end;
end;

procedure TfrmPedidosEditor.btnVerCargaClick(Sender: TObject);
var
  Qry: TFDQuery;
  LDoc: TStringList;
  LDocText: string;
  LPrepText: string;
  LAlbId: Integer;
  LSafeCod, LTemp: string;
  c: Char;
begin
  if (FRecogidaId = 0) and (FPedidoId = '') then Exit;

  // 1. Refrescar el árbol visual de preparación en la pestaña de recogida / envío
  CargarArbolPreparacionPedido;

  // 2. Obtener el albarán vinculado
  LAlbId := ObtenerAlbaranIdVinculado;

  // 3. Obtener el texto del albarán de carga
  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    LDocText := '';
    LSafeCod := FCodRecogida;

    if FRecogidaId > 0 then
    begin
      Qry.SQL.Text := 'SELECT cod_recogida, albaran_carga, albaran_id FROM ge_envios_recogidas WHERE id = :id';
      Qry.ParamByName('id').AsInteger := FRecogidaId;
      Qry.Open;
      if not Qry.IsEmpty then
      begin
        LDocText := Qry.FieldByName('albaran_carga').AsString;
        if Trim(LSafeCod) = '' then
          LSafeCod := Qry.FieldByName('cod_recogida').AsString;
        if (LAlbId <= 0) and not Qry.FieldByName('albaran_id').IsNull then
          LAlbId := Qry.FieldByName('albaran_id').AsInteger;
      end;
      Qry.Close;
    end;

    // Generar bloque formateado de datos de preparación
    LPrepText := GenerarTextoPreparacionPedido(LAlbId);

    // Si no había albarán de carga previo, armar uno básico
    if Trim(LDocText) = '' then
      LDocText := GenerarAlbaranCargaBasico(LAlbId);

    // Si tenemos datos de preparación y no están incluidos ya en el documento, anexarlos
    if (Trim(LPrepText) <> '') and (Pos('DETALLE DE PREPARACIÓN', LDocText) = 0) then
    begin
      if Pos('═══════════════════════════════════════════════════' + sLineBreak + 'Firma del transportista', LDocText) > 0 then
        LDocText := StringReplace(LDocText, '═══════════════════════════════════════════════════' + sLineBreak + 'Firma del transportista',
                                  LPrepText + sLineBreak + '═══════════════════════════════════════════════════' + sLineBreak + 'Firma del transportista', [])
      else if Pos('Firma del transportista', LDocText) > 0 then
        LDocText := StringReplace(LDocText, 'Firma del transportista', LPrepText + sLineBreak + 'Firma del transportista', [])
      else
        LDocText := LDocText + sLineBreak + LPrepText;
    end;

    if Trim(LDocText) = '' then
    begin
      ShowMessage('No se encuentra el albarán de carga para esta recogida.');
      Exit;
    end;

    LDoc := TStringList.Create;
    try
      LDoc.Text := LDocText;
      if Trim(LSafeCod) = '' then
        LSafeCod := 'PED_' + dbNumPedido.Text;
      for c in ['<', '>', ':', '"', '/', '\', '|', '?', '*'] do
        LSafeCod := LSafeCod.Replace(c, '_');
      LSafeCod := Trim(LSafeCod);
      LTemp := GetEnvironmentVariable('TEMP') + '\DocCarga_' + LSafeCod + '_Reopen.txt';
      LDoc.SaveToFile(LTemp, TEncoding.UTF8);
      ShellExecute(0, 'open', PChar(LTemp), nil, nil, SW_SHOWNORMAL);
    finally
      LDoc.Free;
    end;
  finally
    Qry.Free;
  end;
end;

procedure TfrmPedidosEditor.btnRegistrarFirmaClick(Sender: TObject);
begin
  if FRecogidaId = 0 then Exit;

  if not Assigned(frmFirmaRecogida) then
    Application.CreateForm(TfrmFirmaRecogida, frmFirmaRecogida);

  frmFirmaRecogida.RecogidaId := FRecogidaId;
  if frmFirmaRecogida.ShowModal = mrOk then
  begin
    LoadData; // Recargar datos para ver posibles cambios
    ShowMessage('Firma guardada correctamente.');
  end;
end;


procedure TfrmPedidosEditor.btnImprimirEtiquetasClick(Sender: TObject);
var
  LClient: IEnviosClient;
  LConfig: TEnviosConfig;
  LError: string;
  LEtiquetaBytes: TBytes;
  LPdfFile: string;
  LFS: TFileStream;
  LImpresora: string;
  LEsPDF: Boolean;
  LGirar180: Boolean;
begin
  if (FRecogidaId = 0) or (FConfigId = 0) or (FCodRecogida = '') then
  begin
    ShowMessage('No hay datos de recogida válidos para imprimir etiquetas.');
    Exit;
  end;

  try
    if not CargarEnviosConfig(FConfigId, LConfig) then
    begin
      ShowMessage('Error al cargar la configuración de envío para el transportista.');
      Exit;
    end;

    LGirar180 := True;
    // 1. Preguntar al usuario dónde desea imprimir antes de descargar
    if not TfrmSelectImpresoraEnvio.SeleccionarDestino(Self, LConfig.NombreTransp, LConfig.ModeloImpresora, LImpresora, LEsPDF, LGirar180) then
      Exit; // Usuario canceló el diálogo

    LClient := TEnviosClientFactory.Create(LConfig, LError);
    if not Assigned(LClient) then
      raise Exception.Create('Transportista no soportado: ' + LError);

    LClient.Login;

    // 2. Si el usuario eligió imprimir en impresora directa/térmica
    if not LEsPDF then
    begin
      if LClient.ImprimirEtiquetaTermica(FCodRecogida, LImpresora, LError, LGirar180) then
      begin
        ShowMessage('🏷️ Etiqueta enviada correctamente a la impresora "' + LImpresora + '".');
        Exit;
      end;

      if MessageDlg('No se pudo enviar la impresión a la impresora seleccionada (' + LError + ').' + sLineBreak + sLineBreak +
                    '¿Desea previsualizar y abrir la etiqueta en formato PDF?', mtConfirmation, [mbYes, mbNo], 0) <> mrYes then
        Exit;
    end;

    // 3. Generación y apertura en visor PDF
    LEtiquetaBytes := LClient.GetEtiqueta(FCodRecogida, True);

    if Length(LEtiquetaBytes) = 0 then
      raise Exception.Create('El transportista devolvió una etiqueta vacía.');

    LPdfFile := GetEnvironmentVariable('TEMP') + '\Etiqueta_' +
                StringReplace(FCodRecogida, '/', '_', [rfReplaceAll]) +
                '_' + FormatDateTime('yyyymmddhhnnss', Now) + '.pdf';
    LFS := TFileStream.Create(LPdfFile, fmCreate);
    try
      LFS.WriteBuffer(LEtiquetaBytes[0], Length(LEtiquetaBytes));
    finally
      LFS.Free;
    end;

    if LGirar180 then
      TPdfRotateService.RotarPdfArchivo180(LPdfFile);

    ShellExecute(0, 'open', PChar(LPdfFile), nil, nil, SW_SHOWNORMAL);
  except
    on E: Exception do
      ShowMessage('Error al descargar/imprimir la etiqueta: ' + E.Message);
  end;
end;

procedure TfrmPedidosEditor.UpdateClienteDetails;
var
  LClienteId: string;
begin
  if not qMaster.Active or qMaster.FieldByName('cliente_id').IsNull then
  begin
    lblClienteInfo.Caption := '';
    Exit;
  end;

  LClienteId := qMaster.FieldByName('cliente_id').AsString;
  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 TfrmPedidosEditor.UpdateDestinoDetails;
var
  LDestinoId: string;
begin
  if not qMaster.Active or qMaster.FieldByName('clientedestino_id').IsNull then
  begin
    lblDestinoInfo.Caption := '';
    Exit;
  end;

  LDestinoId := qMaster.FieldByName('clientedestino_id').AsString;
  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 TfrmPedidosEditor.dsMasterDataChange(Sender: TObject; Field: TField);
begin
  if (Field = nil) or SameText(Field.FieldName, 'cliente_id') then
    UpdateClienteDetails;
  if (Field = nil) or SameText(Field.FieldName, 'clientedestino_id') then
    UpdateDestinoDetails;
end;

procedure TfrmPedidosEditor.btnDocsRelacionadosClick(Sender: TObject);
begin
  if (Trim(FPedidoId) = '') or FIsNewRecord then
  begin
    ShowMessage('Debe guardar el pedido antes de consultar sus documentos relacionados.');
    Exit;
  end;

  TfrmDocumentosRelacionados.MostrarDocumentos(Self, dvtPedido, FPedidoId, dbNumPedido.Text);
end;

procedure TfrmPedidosEditor.pgcDetailsChange(Sender: TObject);
begin
  if (pgcDetails.ActivePage = tsRecogida) then
  begin
    CargarArbolPreparacionPedido;
  end;

  if (pgcDetails.ActivePage = tsEdi) and (FPedidoId <> '') then
  begin
    if Assigned(qHeaderDiscounts) and not qHeaderDiscounts.Active then
    begin
      if (qHeaderDiscounts.SQL.Count = 0) or (Trim(qHeaderDiscounts.SQL.Text) = '') then
        qHeaderDiscounts.SQL.Text := 'SELECT tipo, codigo_motivo, porcentaje, importe FROM ge_pedidos_descuentos_cargos WHERE id_pedido = :id_pedido';
      if qHeaderDiscounts.FindParam('id_pedido') <> nil then
        qHeaderDiscounts.ParamByName('id_pedido').AsString := FPedidoId;
      qHeaderDiscounts.Open;
    end;
    if Assigned(qLineDiscounts) and not qLineDiscounts.Active then
    begin
      if (qLineDiscounts.SQL.Count = 0) or (Trim(qLineDiscounts.SQL.Text) = '') then
        qLineDiscounts.SQL.Text := 'SELECT l.descripcion AS articulo, ld.tipo, ld.codigo_motivo, ld.porcentaje, ld.importe FROM ge_pedidos_lineas_descuentos ld JOIN ge_pedidos_lineas l ON ld.id_pedido_linea = l.id WHERE l.id_pedido = :id_pedido';
      if qLineDiscounts.FindParam('id_pedido') <> nil then
        qLineDiscounts.ParamByName('id_pedido').AsString := FPedidoId;
      qLineDiscounts.Open;
    end;
  end;
end;

procedure TfrmPedidosEditor.btnImprimirClick(Sender: TObject);
begin
  if (Trim(FPedidoId) = '') or FIsNewRecord then
  begin
    ShowMessage('Debe guardar el pedido antes de poder imprimirlo.');
    Exit;
  end;

  TPedidoPrintService.ImprimirPedido(Self, FPedidoId);
end;

function SeleccionarOrdenProduccionEditor(AOwner: TComponent; AQryOrders: TFDQuery): Integer;
var
  LDlg: TForm;
  LCombo: TComboBox;
  LLabel: TLabel;
  LBtnOk, LBtnCancel: TButton;
begin
  Result := 0;
  if AQryOrders.IsEmpty then Exit;

  if AQryOrders.RecordCount = 1 then
  begin
    AQryOrders.First;
    Result := AQryOrders.FieldByName('id').AsInteger;
    Exit;
  end;

  LDlg := TForm.CreateNew(AOwner);
  try
    LDlg.Caption := 'Seleccionar Orden de Producción';
    LDlg.ClientWidth := 460;
    LDlg.ClientHeight := 140;
    LDlg.Position := poScreenCenter;
    LDlg.BorderStyle := bsDialog;

    LLabel := TLabel.Create(LDlg);
    LLabel.Parent := LDlg;
    LLabel.SetBounds(24, 16, 412, 20);
    LLabel.Caption := 'Este pedido tiene varias órdenes. Seleccione la que desea abrir:';

    LCombo := TComboBox.Create(LDlg);
    LCombo.Parent := LDlg;
    LCombo.SetBounds(24, 42, 412, 26);
    LCombo.Style := csDropDownList;

    AQryOrders.First;
    while not AQryOrders.Eof do
    begin
      LCombo.Items.AddObject(
        Format('%s (%s) — %s', [
          AQryOrders.FieldByName('numero_orden').AsString,
          AQryOrders.FieldByName('obrador_nombre').AsString,
          FormatDateTime('dd/MM/yyyy', AQryOrders.FieldByName('fecha_produccion').AsDateTime)
        ]),
        TObject(IntPtr(AQryOrders.FieldByName('id').AsInteger))
      );
      AQryOrders.Next;
    end;
    LCombo.ItemIndex := 0;

    LBtnOk := TButton.Create(LDlg);
    LBtnOk.Parent := LDlg;
    LBtnOk.SetBounds(246, 88, 90, 30);
    LBtnOk.Caption := 'Abrir';
    LBtnOk.ModalResult := mrOk;
    LBtnOk.Default := True;

    LBtnCancel := TButton.Create(LDlg);
    LBtnCancel.Parent := LDlg;
    LBtnCancel.SetBounds(346, 88, 90, 30);
    LBtnCancel.Caption := 'Cancelar';
    LBtnCancel.ModalResult := mrCancel;
    LBtnCancel.Cancel := True;

    if LDlg.ShowModal = mrOk then
    begin
      if LCombo.ItemIndex >= 0 then
        Result := Integer(IntPtr(LCombo.Items.Objects[LCombo.ItemIndex]));
    end;
  finally
    LDlg.Free;
  end;
end;

procedure TfrmPedidosEditor.btnPasarAProduccionClick(Sender: TObject);
var
  LPedidoId: Integer;
  LNumeroPedido: string;
  LEmpresaId: Integer;
  LQryOrders: TFDQuery;
  LQryCounts: TFDQuery;
  LFrmOrden: TfrmOrdenProduccionEditor;
  LOrdersSummary: string;
  LSelectedOrdenId: Integer;
  LTotalLineas, LLineasAsignadas, LLineasPendientes: Integer;
  LPromptMsg: string;
  LDlgResult: Integer;
begin
  if FIsNewRecord or (Trim(FPedidoId) = '') then
  begin
    ShowMessage('Debe guardar el pedido antes de pasarlo a producción.');
    Exit;
  end;

  LPedidoId := StrToIntDef(FPedidoId, 0);
  if LPedidoId <= 0 then Exit;

  LNumeroPedido := qMaster.FieldByName('numero_pedido').AsString;
  LEmpresaId := qMaster.FieldByName('empresa_id').AsInteger;

  // 1. Obtener órdenes de producción existentes para este pedido
  LQryOrders := TFDQuery.Create(nil);
  try
    LQryOrders.Connection := dmgMain.dbConn;
    LQryOrders.SQL.Text :=
      'SELECT op.id, op.numero_orden, op.fecha_produccion, ' +
      '       COALESCE(obr.nombre, ''Sin Obrador'') AS obrador_nombre ' +
      'FROM ge_ordenes_produccion op ' +
      'LEFT JOIN ge_obradores obr ON op.id_obrador = obr.id ' +
      'WHERE op.id_pedido = :id_pedido ' +
      'ORDER BY op.id ASC';
    LQryOrders.ParamByName('id_pedido').AsInteger := LPedidoId;
    LQryOrders.Open;

    // 2. Contar líneas totales del pedido y líneas ya asignadas a producción
    LTotalLineas := 0;
    LLineasAsignadas := 0;
    LQryCounts := TFDQuery.Create(nil);
    try
      LQryCounts.Connection := dmgMain.dbConn;
      LQryCounts.SQL.Text :=
        'SELECT ' +
        '  COUNT(pl.id) AS total_lineas, ' +
        '  COUNT(DISTINCT opl.id_pedido_linea) AS lineas_asignadas ' +
        'FROM ge_pedidos_lineas pl ' +
        'LEFT JOIN ( ' +
        '  SELECT opl_sub.id_pedido_linea ' +
        '  FROM ge_ordenes_produccion_lineas opl_sub ' +
        '  JOIN ge_ordenes_produccion op_sub ON op_sub.id = opl_sub.id_orden ' +
        '  WHERE opl_sub.id_pedido_linea IS NOT NULL ' +
        ') opl ON opl.id_pedido_linea = pl.id ' +
        'WHERE pl.id_pedido = :id_pedido';
      LQryCounts.ParamByName('id_pedido').AsInteger := LPedidoId;
      LQryCounts.Open;
      if not LQryCounts.IsEmpty then
      begin
        LTotalLineas := LQryCounts.FieldByName('total_lineas').AsInteger;
        LLineasAsignadas := LQryCounts.FieldByName('lineas_asignadas').AsInteger;
      end;
    finally
      LQryCounts.Free;
    end;

    LLineasPendientes := LTotalLineas - LLineasAsignadas;
    if LLineasPendientes < 0 then LLineasPendientes := 0;

    // 3. Evaluar flujo según si ya existen órdenes o no
    if not LQryOrders.IsEmpty then
    begin
      LOrdersSummary := '';
      LQryOrders.First;
      while not LQryOrders.Eof do
      begin
        LOrdersSummary := LOrdersSummary + Format('  • %s (%s) — Fecha: %s' + sLineBreak, [
          LQryOrders.FieldByName('numero_orden').AsString,
          LQryOrders.FieldByName('obrador_nombre').AsString,
          FormatDateTime('dd/MM/yyyy', LQryOrders.FieldByName('fecha_produccion').AsDateTime)
        ]);
        LQryOrders.Next;
      end;

      if LLineasPendientes > 0 then
      begin
        LPromptMsg :=
          Format('El pedido %s ya cuenta con orden(es) de producción generada(s):' + sLineBreak +
                 '%s' + sLineBreak +
                 'Líneas pendientes de asignar a obrador: %d de %d.' + sLineBreak + sLineBreak +
                 '¿Desea crear una NUEVA orden de producción para otro obrador con las líneas restantes?' + sLineBreak + sLineBreak +
                 '• [SÍ]: Crear nueva orden (se precargarán los %d artículos pendientes).' + sLineBreak +
                 '• [NO]: Abrir orden de producción existente.' + sLineBreak +
                 '• [CANCELAR]: Volver sin cambios.',
                 [LNumeroPedido, LOrdersSummary, LLineasPendientes, LTotalLineas, LLineasPendientes]);

        LDlgResult := MessageDlg(LPromptMsg, mtConfirmation, [mbYes, mbNo, mbCancel], 0);

        if LDlgResult = mrCancel then
          Exit
        else if LDlgResult = mrNo then
        begin
          LSelectedOrdenId := SeleccionarOrdenProduccionEditor(Self, LQryOrders);
          if LSelectedOrdenId > 0 then
          begin
            LFrmOrden := TfrmOrdenProduccionEditor.Create(Application);
            try
              LFrmOrden.OrdenId := LSelectedOrdenId;
              LFrmOrden.ShowModal;
            finally
              LFrmOrden.Free;
            end;
          end;
          Exit;
        end;
      end
      else
      begin
        // Todas las líneas ya están asignadas
        LPromptMsg :=
          Format('Todas las líneas del pedido %s ya han sido asignadas a producción:' + sLineBreak +
                 '%s' + sLineBreak +
                 '¿Qué desea hacer?' + sLineBreak + sLineBreak +
                 '• [SÍ]: Abrir una orden existente para consultarla o editarla.' + sLineBreak +
                 '• [NO]: Crear una orden de producción ADICIONAL (para duplicar o fabricar extra).' + sLineBreak +
                 '• [CANCELAR]: Volver sin cambios.',
                 [LNumeroPedido, LOrdersSummary]);

        LDlgResult := MessageDlg(LPromptMsg, mtConfirmation, [mbYes, mbNo, mbCancel], 0);

        if LDlgResult = mrCancel then
          Exit
        else if LDlgResult = mrYes then
        begin
          LSelectedOrdenId := SeleccionarOrdenProduccionEditor(Self, LQryOrders);
          if LSelectedOrdenId > 0 then
          begin
            LFrmOrden := TfrmOrdenProduccionEditor.Create(Application);
            try
              LFrmOrden.OrdenId := LSelectedOrdenId;
              LFrmOrden.ShowModal;
            finally
              LFrmOrden.Free;
            end;
          end;
          Exit;
        end;
      end;
    end;

    // 4. Crear nueva orden de producción vinculada al pedido
    LFrmOrden := TfrmOrdenProduccionEditor.Create(Application);
    try
      LFrmOrden.InitialEmpresaId := LEmpresaId;
      LFrmOrden.InitialPedidoId := LPedidoId;
      if LFrmOrden.ShowModal = mrOk then
      begin
        // Si el pedido estaba PENDIENTE o PROCESADO, avanzar a EN PREPARACION
        if (qMaster.FieldByName('estado').AsString = 'PENDIENTE') or
           (qMaster.FieldByName('estado').AsString = 'PROCESADO') or
           (qMaster.FieldByName('estado').AsString = '') then
        begin
          try
            dmgMain.dbConn.ExecSQL(
              'UPDATE ge_pedidos SET estado = ''EN PREPARACION'' WHERE id = :id',
              [LPedidoId]
            );
            qMaster.Refresh;
          except
            // ignore
          end;
        end;
        ShowMessage(Format('El pedido %s se ha pasado a producción correctamente.', [LNumeroPedido]));
      end;
    finally
      LFrmOrden.Free;
    end;
  finally
    LQryOrders.Free;
  end;
end;

procedure TfrmPedidosEditor.btnBuscarClienteClick(Sender: TObject);
var
  LFrm: TfrmSelectCliente;
begin
  LFrm := TfrmSelectCliente.Create(Self);
  try
    if LFrm.ShowModal = mrOk then
    begin
      if LFrm.SelectedId <> '' then
      begin
        if not (qMaster.State in [dsInsert, dsEdit]) then
          qMaster.Edit;
        qMaster.FieldByName('cliente_id').AsString := LFrm.SelectedId;

        // Refrescar lista de destinos
        qDestList.Close;
        qDestList.ParamByName('id_cliente').AsInteger := StrToIntDef(LFrm.SelectedId, 0);
        qDestList.Open;

        if qDestList.RecordCount = 1 then
          qMaster.FieldByName('clientedestino_id').AsString := qDestList.FieldByName('id').AsString
        else if qDestList.RecordCount > 1 then
        begin
          qMaster.FieldByName('clientedestino_id').Clear;
          btnBuscarDestinoClick(nil);
        end
        else
          qMaster.FieldByName('clientedestino_id').Clear;
      end;
    end;
  finally
    LFrm.Free;
  end;
end;

procedure TfrmPedidosEditor.btnNuevoClienteClick(Sender: TObject);
var
  LFrmCli: TfrmClientesEditor;
  LNewId: string;
begin
  LFrmCli := TfrmClientesEditor.Create(Self);
  try
    LFrmCli.ClienteId := '';
    LFrmCli.ParentForm := Self;
    LFrmCli.ShowModal;
    LNewId := LFrmCli.ClienteId;
    if LNewId <> '' then
    begin
      qClientList.Close;
      qClientList.Open;

      if not (qMaster.State in [dsInsert, dsEdit]) then
        qMaster.Edit;
      qMaster.FieldByName('cliente_id').AsString := LNewId;

      qDestList.Close;
      qDestList.ParamByName('id_cliente').AsInteger := StrToIntDef(LNewId, 0);
      qDestList.Open;

      if qDestList.RecordCount = 1 then
        qMaster.FieldByName('clientedestino_id').AsString := qDestList.FieldByName('id').AsString;
    end;
  finally
    LFrmCli.Free;
  end;
end;

procedure TfrmPedidosEditor.btnBuscarDestinoClick(Sender: TObject);
var
  LFrm: TfrmSelectDestino;
  LClienteId: string;
begin
  if not qMaster.Active or (qMaster.FindField('cliente_id') = nil) or
     qMaster.FieldByName('cliente_id').IsNull or (qMaster.FieldByName('cliente_id').AsInteger <= 0) then
  begin
    ShowMessage('Debe seleccionar un cliente antes de buscar sucursales/destinos.');
    Exit;
  end;

  LClienteId := qMaster.FieldByName('cliente_id').AsString;
  LFrm := TfrmSelectDestino.Create(Self);
  try
    LFrm.ClienteId := LClienteId;
    if LFrm.ShowModal = mrOk then
    begin
      qDestList.Close;
      qDestList.ParamByName('id_cliente').AsInteger := StrToIntDef(LClienteId, 0);
      qDestList.Open;

      if LFrm.SelectedId <> '' then
      begin
        if not (qMaster.State in [dsInsert, dsEdit]) then
          qMaster.Edit;
        qMaster.FieldByName('clientedestino_id').AsString := LFrm.SelectedId;
      end;
    end;
  finally
    LFrm.Free;
  end;
end;

procedure TfrmPedidosEditor.btnNuevoDestinoClick(Sender: TObject);
var
  LClienteId, LNewId: string;
  Qry: TFDQuery;
  LFrm: TfrmClientesDestinosEditor;
begin
  if not qMaster.Active or (qMaster.FindField('cliente_id') = nil) or
     qMaster.FieldByName('cliente_id').IsNull or (qMaster.FieldByName('cliente_id').AsInteger <= 0) then
  begin
    ShowMessage('Debe seleccionar un cliente antes de crear un nuevo destino.');
    Exit;
  end;

  LClienteId := qMaster.FieldByName('cliente_id').AsString;
  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;
          
          qDestList.Close;
          qDestList.ParamByName('id_cliente').AsInteger := StrToIntDef(LClienteId, 0);
          qDestList.Open;

          if LNewId <> '' then
          begin
            if not (qMaster.State in [dsInsert, dsEdit]) then
              qMaster.Edit;
            qMaster.FieldByName('clientedestino_id').AsString := LNewId;
          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;

function TfrmPedidosEditor.ValidarYAsegurarDestino: Boolean;
var
  LClienteId: string;
  LDestinoId: string;
  LCantDestinos: Integer;
  LFrmSel: TfrmSelectDestino;
  LFrmDestEdit: TfrmClientesDestinosEditor;
  Qry: TFDQuery;
  LNewId: string;
begin
  Result := False;
  if not qMaster.Active or (qMaster.FindField('cliente_id') = nil) or
     qMaster.FieldByName('cliente_id').IsNull or (qMaster.FieldByName('cliente_id').AsInteger <= 0) then
  begin
    ShowMessage('Debe seleccionar un cliente.');
    Exit;
  end;

  LClienteId := qMaster.FieldByName('cliente_id').AsString;

  // Si ya tiene un destino seleccionado válido
  if (qMaster.FindField('clientedestino_id') <> nil) and 
     not qMaster.FieldByName('clientedestino_id').IsNull and 
     (qMaster.FieldByName('clientedestino_id').AsInteger > 0) then
  begin
    Result := True;
    Exit;
  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
        qDestList.Close;
        qDestList.ParamByName('id_cliente').AsInteger := StrToIntDef(LClienteId, 0);
        qDestList.Open;

        if LFrmSel.SelectedId <> '' then
        begin
          if not (qMaster.State in [dsInsert, dsEdit]) then
            qMaster.Edit;
          qMaster.FieldByName('clientedestino_id').AsString := LFrmSel.SelectedId;
          Result := not qMaster.FieldByName('clientedestino_id').IsNull;
        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;

            // 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;

            qDestList.Close;
            qDestList.ParamByName('id_cliente').AsInteger := StrToIntDef(LClienteId, 0);
            qDestList.Open;

            if LNewId <> '' then
            begin
              if not (qMaster.State in [dsInsert, dsEdit]) then
                qMaster.Edit;
              qMaster.FieldByName('clientedestino_id').AsString := LNewId;
              Result := not qMaster.FieldByName('clientedestino_id').IsNull;
            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;

end.
