unit frm_FacturasEditor;

interface

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

type
  TfrmFacturasEditor = class(TfrmBase)
    Label1: TLabel;
    edtNumero: TEdit;
    Label2: TLabel;
    dtpFecha: TDateTimePicker;
    Label3: TLabel;
    cbCliente: TComboBox;
    Label4: TLabel;
    cbSerie: TComboBox;
    lblTotal: TLabel;
    edtTotal: TEdit;
    lblBase: TLabel;
    edtBase: TEdit;
    sgIvaDesglose: TStringGrid;
    lblDestino: TLabel;
    lblFechaCobro: TLabel;
    lblFormaPago: TLabel;
    cbDestino: TComboBox;
    cbFormaPago: TComboBox;
    chkPagada: TCheckBox;
    dtpFechaCobro: TDateTimePicker;
    dsLines: TDataSource;
    qryLines: TFDQuery;
    pnlClienteInfo: TPanel;
    lblClienteInfo: TLabel;
    pnlDestinoInfo: TPanel;
    lblDestinoInfo: TLabel;
    lblImpuestos: TLabel;
    edtIva: TEdit;
    lbldesgloseiva: TLabel;
    btnBuscarCliente: TSpeedButton;
    btnNuevoCliente: TSpeedButton;
    btnBuscarDestino: TSpeedButton;
    btnNuevoDestino: TSpeedButton;
    procedure FormShow(Sender: TObject);
    procedure btnSaveClick(Sender: TObject);
    procedure btnAddLineClick(Sender: TObject);
    procedure btnDelLineClick(Sender: TObject);
    procedure btnSalirClick(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure qryLinesAfterPost(DataSet: TDataSet);
    procedure qryLinesAfterDelete(DataSet: TDataSet);
    procedure qryLinesBeforePost(DataSet: TDataSet);
    procedure cbClienteChange(Sender: TObject);
    procedure cbDestinoChange(Sender: TObject);
    procedure cbSerieChange(Sender: TObject);
    procedure btnBuscarClienteClick(Sender: TObject);
    procedure btnNuevoClienteClick(Sender: TObject);
    procedure btnBuscarDestinoClick(Sender: TObject);
    procedure btnNuevoDestinoClick(Sender: TObject);
    procedure btnImprimirClick(Sender: TObject); override;
    procedure btnDocsRelacionadosClick(Sender: TObject);
  protected
    procedure DoAnadir; override;
    procedure DoModificar; override;
    procedure DoPrimero; override;
    procedure DoAnterior; override;
    procedure DoSiguiente; override;
    procedure DoUltimo; override;
    procedure DoDuplicar; override;
    function IsNewRecord: Boolean; override;
  private
    { Private declarations }
    FFacturaId: string;
    FClientIds: TStringList;
    FDestinationIds: TStringList;
    FFormaPagoIds: TStringList;
    FTotalBase: Double;
    FTotalIva: Double;
    FTotalDoc: Double;
    FParentForm: TForm;
    FIsNewRecord: Boolean;
    procedure LoadData;
    procedure LoadHeader;
    procedure LoadLines;
    procedure LoadClientes;
    procedure LoadFormasPago;
    procedure LoadDestinos(const AClienteId: string);
    procedure RecalculateTotal;
    procedure UpdateClienteDetails;
    procedure UpdateDestinoDetails;
    procedure qryLinesFieldChange(Sender: TField);
    procedure SetupFieldChangeEvents;
    procedure SetupIvaPickList;
    function ValidarYAsegurarDestino: Boolean;
  public
    { Public declarations }
    property FacturaId: string read FFacturaId write FFacturaId;
    property ParentForm: TForm read FParentForm write FParentForm;
  end;

var
  frmFacturasEditor: TfrmFacturasEditor;

implementation

uses
  dmg_Main, frm_SelectProduct, frm_SelectCliente, frm_SelectDestino, uDbErrorHandler, System.Generics.Collections, frm_Facturas,
  frm_ClientesEditor, frm_ClientesDestinosEditor, uFacturaPrintService, frm_DocumentosRelacionados;

{$R *.dfm}

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

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

  // Aplicar tema
  TAppTheme.ApplyToForm(Self);

  // Vincular eventos a la barra superior e inferior heredadas
  btnGuardar.OnClick := btnSaveClick;
  btnSalir.OnClick := btnSalirClick;
  btnNuevoArticulo.OnClick := btnAddLineClick;
  btnEliminaArticulo.OnClick := btnDelLineClick;
  btnImprimir.OnClick := btnImprimirClick;
  btnIA.OnClick := btnDocsRelacionadosClick;

  // Vincular la grilla heredada
  dbgItems.DataSource := dsLines;

  // Ocultar botones de navegación/herramientas innecesarios
  btnCancelar.Visible := True;
  btnAnadir.Visible := True;
  btnEliminar.Visible := False;
  btnPrimero.Visible := True;
  btnAnterior.Visible := True;
  btnSiguiente.Visible := True;
  btnUltimo.Visible := True;
  btnModificar.Visible := True;
  btnBuscar.Visible := False;
  btnImprimir.Visible := True;
  btnIA.Visible := True;
  btnIA.Caption := 'Relacionados';
  btnIA.ImageName := 'icon_documentos_relacionados';
  btnIA.Width := 75;
  btnDuplicar.Visible := True;
  btnRect.Visible := False;
  btnAyuda.Visible := False;
  btnBuscarArticulo.Visible := False;
  btnExcel.Visible := False;

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

  // Configurar columnas de la grilla heredada dbgItems
  dbgItems.Columns.Clear;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'articulo_codigo';
    Title.Caption := 'Ref. Artículo';
    Width := 90;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'descripcion';
    Title.Caption := 'Descripción';
    Width := 250;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'lotes';
    Title.Caption := 'Lote';
    Width := 90;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'fecha_caducidad';
    Title.Caption := 'F. Caducidad';
    Width := 85;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'precio';
    Title.Caption := 'Precio';
    Width := 75;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'cantidad';
    Title.Caption := 'Cant.';
    Width := 50;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'descuento_porc';
    Title.Caption := '% Dto.';
    Width := 55;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'descuento_cant';
    Title.Caption := 'Dto. Importe';
    Width := 75;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'tipo_iva';
    Title.Caption := '% IVA';
    Width := 55;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'total';
    Title.Caption := 'Total';
    Width := 90;
  end;

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

  qryLines.BeforePost := qryLinesBeforePost;

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

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

  FIsNewRecord := (FFacturaId = '');

  if not FIsNewRecord then
    LoadData
  else
  begin
    dtpFecha.Date := Now;
    if dmgMain.GetSerieYNumeroDocumento(dmgMain.CurrentCompanyId, 'FACTURA', dtpFecha.Date, '', LSerieAuto, LNumAuto) then
    begin
      LIdx := cbSerie.Items.IndexOf(LSerieAuto);
      if LIdx <> -1 then
        cbSerie.ItemIndex := LIdx;
      edtNumero.Text := IntToStr(LNumAuto);
    end
    else if cbSerie.Items.Count > 0 then
    begin
      cbSerie.ItemIndex := 0;
      cbSerieChange(cbSerie);
    end
    else
    begin
      edtNumero.Text := dmgMain.GetNextIDS('ge_facturas');
    end;
    edtTotal.Text := '0,00';
    edtIva.Text := '0,00';
    edtBase.Text := '0,00';
    
    sgIvaDesglose.RowCount := 2;
    sgIvaDesglose.Cells[0, 1] := '';
    sgIvaDesglose.Cells[1, 1] := '';
    sgIvaDesglose.Cells[2, 1] := '';

    FTotalBase := 0;
    FTotalIva := 0;
    FTotalDoc := 0;
    
    // Preparar qryLines vacía
    qryLines.Close;
    qryLines.SQL.Text := 'SELECT *, (total - base) as iva FROM ge_facturas_lineas WHERE factura_id = :id';
    qryLines.ParamByName('id').AsInteger := 0;
    qryLines.Open;
    SetupFieldChangeEvents;
  end;
  SetupIvaPickList;
end;

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

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

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

procedure TfrmFacturasEditor.btnSaveClick(Sender: TObject);
var
  LIsNew: Boolean;
  LSerieAuto: string;
  LNumAuto, LIdx: Integer;
begin
  if (cbCliente.ItemIndex = -1) then
  begin
    ShowMessage('Debe seleccionar un cliente.');
    Exit;
  end;

  if not ValidarYAsegurarDestino then
    Exit;

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

  LIsNew := FIsNewRecord;
  dmgMain.qryExec.Close;
  
  if LIsNew then
  begin
    dmgMain.qryExec.SQL.Text := 
      'INSERT INTO ge_facturas (empresa_id, cliente_id, clientedestino_id, serie, numero, fecha, base_imponible, iva, total, pagada, fecha_cobro, id_forma_pago, id_user_creator, id_user_update) ' +
      'VALUES (:emp, :cli, :dest, :ser, :num, :fec, :base, :iva, :tot, :pag, :feccob, :id_forma_pago, :user, :user)';
  end
  else
  begin
    dmgMain.qryExec.SQL.Text := 
      'UPDATE ge_facturas SET cliente_id = :cli, clientedestino_id = :dest, serie = :ser, numero = :num, ' +
      'fecha = :fec, base_imponible = :base, iva = :iva, total = :tot, pagada = :pag, fecha_cobro = :feccob, id_forma_pago = :id_forma_pago, id_user_update = :user ' +
      'WHERE id = :id';
    dmgMain.qryExec.ParamByName('id').AsInteger := StrToIntDef(FFacturaId, 0);
  end;
  if LIsNew then
    dmgMain.qryExec.ParamByName('emp').AsString := dmgMain.CurrentCompanyId;
  dmgMain.qryExec.ParamByName('cli').AsString := FClientIds[cbCliente.ItemIndex];

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

  dmgMain.qryExec.ParamByName('ser').AsString := cbSerie.Text;
  dmgMain.qryExec.ParamByName('num').AsInteger := StrToIntDef(edtNumero.Text, 0);
  dmgMain.qryExec.ParamByName('fec').AsDateTime := dtpFecha.DateTime;
  dmgMain.qryExec.ParamByName('base').AsFloat := FTotalBase;
  dmgMain.qryExec.ParamByName('iva').AsFloat := FTotalIva;
  dmgMain.qryExec.ParamByName('tot').AsFloat := FTotalDoc;
  dmgMain.qryExec.ParamByName('pag').AsBoolean := chkPagada.Checked;
  dmgMain.qryExec.ParamByName('feccob').DataType := ftDate;
  if chkPagada.Checked then
    dmgMain.qryExec.ParamByName('feccob').AsDate := dtpFechaCobro.Date
  else
    dmgMain.qryExec.ParamByName('feccob').Clear;

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

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

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

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

      qryLines.First;
      while not qryLines.Eof do
      begin
        if qryLines.FieldByName('factura_id').IsNull or (qryLines.FieldByName('factura_id').AsInteger = 0) then
        begin
          qryLines.Edit;
          qryLines.FieldByName('factura_id').AsInteger := StrToIntDef(FFacturaId, 0);
          qryLines.Post;
        end;
        qryLines.Next;
      end;
    end;
    
    FIsNewRecord := False;
    ResetChangeTracking;
    if Assigned(FParentForm) and (FParentForm is TfrmFacturas) then
      TfrmFacturas(FParentForm).btnRefreshClick(nil);
    ShowMessage('Factura guardada correctamente.');
  except
    on E: Exception do
      TDbErrorHandler.HandleException(E, dmgMain.qryExec.SQL.Text);
  end;
end;

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

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

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

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

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

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

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

procedure TfrmFacturasEditor.qryLinesBeforePost(DataSet: TDataSet);
var
  LPrecio, LCantidad, LTipoIva, LSubtotal, LPorcDto, LDtoAmt, LBase, LIva, LTotal: Double;
begin
  if DataSet.FieldByName('factura_id').IsNull or (DataSet.FieldByName('factura_id').AsInteger = 0) then
    if FFacturaId <> '' then
      DataSet.FieldByName('factura_id').AsInteger := StrToIntDef(FFacturaId, 0);

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

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

procedure TfrmFacturasEditor.LoadHeader;
var
  LClienteId, LDestinoId, LFormaPagoId, LSerie: string;
  LNumero: Integer;
  LFecha, LFechaCobro: TDateTime;
  LTotal: Double;
  LPagada: Boolean;
  LQryHdr, LQryCli, LQryDest: TFDQuery;
  LIdxCli, LIdxFP, LIdxDest: Integer;
begin
  LQryHdr := TFDQuery.Create(nil);
  try
    LQryHdr.Connection := dmgMain.dbConn;
    LQryHdr.SQL.Text := 'SELECT id, numero, serie, fecha, total, cliente_id, clientedestino_id, id_forma_pago, pagada, fecha_cobro FROM ge_facturas WHERE id = :id';
    LQryHdr.ParamByName('id').AsString := FFacturaId;
    LQryHdr.Open;

    if not LQryHdr.IsEmpty then
    begin
      LNumero := LQryHdr.FieldByName('numero').AsInteger;
      LSerie := LQryHdr.FieldByName('serie').AsString;
      LFecha := LQryHdr.FieldByName('fecha').AsDateTime;
      LTotal := LQryHdr.FieldByName('total').AsFloat;
      LClienteId := TdmgMain.CleanUUID(LQryHdr.FieldByName('cliente_id').AsString);

      LDestinoId := '';
      if (LQryHdr.FindField('clientedestino_id') <> nil) and not LQryHdr.FieldByName('clientedestino_id').IsNull then
        LDestinoId := TdmgMain.CleanUUID(LQryHdr.FieldByName('clientedestino_id').AsString);

      LFormaPagoId := '';
      if (LQryHdr.FindField('id_forma_pago') <> nil) and not LQryHdr.FieldByName('id_forma_pago').IsNull then
        LFormaPagoId := LQryHdr.FieldByName('id_forma_pago').AsString;

      LPagada := (LQryHdr.FieldByName('pagada').AsInteger <> 0);
      if not LQryHdr.FieldByName('fecha_cobro').IsNull then
        LFechaCobro := LQryHdr.FieldByName('fecha_cobro').AsDateTime
      else
        LFechaCobro := Now;

      // Asignar a controles
      edtNumero.Text := IntToStr(LNumero);
      cbSerie.ItemIndex := cbSerie.Items.IndexOf(LSerie);
      if cbSerie.ItemIndex = -1 then
      begin
        cbSerie.Items.Add(LSerie);
        cbSerie.ItemIndex := cbSerie.Items.Count - 1;
      end;
      dtpFecha.Date := LFecha;
      edtTotal.Text := FormatFloat('#,##0.00', LTotal);
      chkPagada.Checked := LPagada;
      dtpFechaCobro.Date := LFechaCobro;

      // Seleccionar cliente
      LIdxCli := -1;
      if Assigned(FClientIds) then
        LIdxCli := FClientIds.IndexOf(LClienteId);

      if (LIdxCli = -1) and (LClienteId <> '') then
      begin
        LQryCli := TFDQuery.Create(nil);
        try
          LQryCli.Connection := dmgMain.dbConn;
          LQryCli.SQL.Text := 'SELECT nombre_fiscal FROM ge_clientes WHERE id = :id';
          LQryCli.ParamByName('id').AsString := LClienteId;
          LQryCli.Open;
          if not LQryCli.IsEmpty then
          begin
            cbCliente.Items.Add(LQryCli.FieldByName('nombre_fiscal').AsString);
            if Assigned(FClientIds) then
              FClientIds.Add(LClienteId);
            cbCliente.ItemIndex := cbCliente.Items.Count - 1;
          end;
        finally
          LQryCli.Free;
        end;
      end
      else
        cbCliente.ItemIndex := LIdxCli;

      // Seleccionar forma de pago
      if (LFormaPagoId <> '') and Assigned(FFormaPagoIds) then
      begin
        LIdxFP := FFormaPagoIds.IndexOf(LFormaPagoId);
        if LIdxFP >= 0 then
          cbFormaPago.ItemIndex := LIdxFP
        else
          cbFormaPago.ItemIndex := 0;
      end
      else
        cbFormaPago.ItemIndex := 0;

      // Cargar destinos del cliente
      LoadDestinos(LClienteId);

      // Seleccionar destino
      if (LDestinoId <> '') and Assigned(FDestinationIds) then
      begin
        LIdxDest := FDestinationIds.IndexOf(LDestinoId);
        if LIdxDest = -1 then
        begin
          LQryDest := TFDQuery.Create(nil);
          try
            LQryDest.Connection := dmgMain.dbConn;
            LQryDest.SQL.Text := 'SELECT descripcion FROM ge_clientes_destinos WHERE id = :id';
            LQryDest.ParamByName('id').AsString := LDestinoId;
            LQryDest.Open;
            if not LQryDest.IsEmpty then
            begin
              cbDestino.Items.Add(LQryDest.FieldByName('descripcion').AsString);
              FDestinationIds.Add(LDestinoId);
              LIdxDest := cbDestino.Items.Count - 1;
            end;
          finally
            LQryDest.Free;
          end;
        end;
        cbDestino.ItemIndex := LIdxDest;
      end
      else
        cbDestino.ItemIndex := -1;

      UpdateClienteDetails;
      UpdateDestinoDetails;
    end;
  finally
    LQryHdr.Free;
  end;
end;

procedure TfrmFacturasEditor.LoadLines;
begin
  qryLines.Close;
  qryLines.SQL.Text := 'SELECT *, (total - base) as iva FROM ge_facturas_lineas WHERE factura_id = :id ORDER BY posicion';
  qryLines.ParamByName('id').AsString := FFacturaId;
  qryLines.Open;
  SetupFieldChangeEvents;
end;

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

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

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

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

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

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

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

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

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

procedure TfrmFacturasEditor.LoadDestinos(const AClienteId: string);
var
  LQry: TFDQuery;
begin
  cbDestino.Items.Clear;
  if Assigned(FDestinationIds) then
    FDestinationIds.Clear;
  
  if AClienteId = '' then Exit;
  
  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    LQry.SQL.Text := 'SELECT id, descripcion FROM ge_clientes_destinos WHERE id_cliente = :cli AND activo = true ORDER BY descripcion';
    LQry.ParamByName('cli').AsString := AClienteId;
    LQry.Open;
    while not LQry.Eof do
    begin
      cbDestino.Items.Add(LQry.FieldByName('descripcion').AsString);
      if Assigned(FDestinationIds) then
        FDestinationIds.Add(TdmgMain.CleanUUID(LQry.FieldByName('id').AsString));
      LQry.Next;
    end;
  finally
    LQry.Free;
  end;
end;

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

procedure TfrmFacturasEditor.UpdateClienteDetails;
var
  LClienteId: string;
  LQry: TFDQuery;
begin
  if (cbCliente.ItemIndex = -1) or not Assigned(FClientIds) or (cbCliente.ItemIndex >= FClientIds.Count) then
  begin
    lblClienteInfo.Caption := '';
    Exit;
  end;

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

procedure TfrmFacturasEditor.UpdateDestinoDetails;
var
  LDestinoId: string;
  LQry: TFDQuery;
begin
  if (cbDestino.ItemIndex = -1) or not Assigned(FDestinationIds) or (cbDestino.ItemIndex >= FDestinationIds.Count) then
  begin
    lblDestinoInfo.Caption := '';
    Exit;
  end;

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

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

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

procedure TfrmFacturasEditor.SetupFieldChangeEvents;
begin
  if qryLines.FindField('articulo_codigo') <> nil then
    qryLines.FieldByName('articulo_codigo').OnChange := qryLinesFieldChange;
  if qryLines.FindField('precio') <> nil then
    qryLines.FieldByName('precio').OnChange := qryLinesFieldChange;
  if qryLines.FindField('cantidad') <> nil then
    qryLines.FieldByName('cantidad').OnChange := qryLinesFieldChange;
  if qryLines.FindField('tipo_iva') <> nil then
    qryLines.FieldByName('tipo_iva').OnChange := qryLinesFieldChange;
  if qryLines.FindField('descuento_porc') <> nil then
    qryLines.FieldByName('descuento_porc').OnChange := qryLinesFieldChange;
  if qryLines.FindField('descuento_cant') <> nil then
    qryLines.FieldByName('descuento_cant').OnChange := qryLinesFieldChange;
end;

procedure TfrmFacturasEditor.qryLinesFieldChange(Sender: TField);
var
  LPrecio, LCantidad, LTipoIva, LSubtotal, LPorcDto, LDtoAmt, LBase, LIva, LTotal: Double;
  Qry: TFDQuery;
  LCodeStr, LClienteId: string;
  LArtId: Integer;
begin
  if not (qryLines.State in [dsEdit, dsInsert]) then Exit;
  
  if qryLines.FindField('articulo_codigo') <> nil then
    qryLines.FieldByName('articulo_codigo').OnChange := nil;
  if qryLines.FindField('precio') <> nil then
    qryLines.FieldByName('precio').OnChange := nil;
  if qryLines.FindField('cantidad') <> nil then
    qryLines.FieldByName('cantidad').OnChange := nil;
  if qryLines.FindField('tipo_iva') <> nil then
    qryLines.FieldByName('tipo_iva').OnChange := nil;
  if qryLines.FindField('descuento_porc') <> nil then
    qryLines.FieldByName('descuento_porc').OnChange := nil;
  if qryLines.FindField('descuento_cant') <> nil then
    qryLines.FieldByName('descuento_cant').OnChange := nil;
  try
    if SameText(Sender.FieldName, 'articulo_codigo') then
    begin
      LCodeStr := Trim(Sender.AsString);
      if LCodeStr <> '' then
      begin
        Qry := TFDQuery.Create(nil);
        try
          Qry.Connection := dmgMain.dbConn;
          Qry.SQL.Text :=
            'SELECT a.id, a.descripcion, COALESCE(a.pvp1, 0) AS pvp1, COALESCE(t.tipo, 21.00) as tipo_iva ' +
            'FROM ge_articulos a ' +
            'LEFT JOIN ge_tipos_iva t ON a.id_tipo_iva = t.id ' +
            'WHERE (CAST(a.id AS CHAR) = :code OR a.ean = :code OR EXISTS (SELECT 1 FROM ge_articulos_ean ean WHERE ean.articulo_id = a.id AND ean.ean = :code)) AND a.activo = 1 LIMIT 1';
          Qry.ParamByName('code').AsString := LCodeStr;
          Qry.Open;

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

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

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

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

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

            qryLines.FieldByName('precio').AsFloat := LPrecio;
            qryLines.FieldByName('tipo_iva').AsFloat := LTipoIva;
          end
          else
          begin
            MessageDlg('⚠️ No se encontró ningún artículo activo con la referencia "' + LCodeStr + '".', mtWarning, [mbOK], 0);
          end;
        finally
          Qry.Free;
        end;
      end;
    end;

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

    LSubtotal := LPrecio * LCantidad;
    LBase := LSubtotal - qryLines.FieldByName('descuento_cant').AsFloat;
    LIva := LBase * (LTipoIva / 100.0);
    LTotal := LBase + LIva;

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

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

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

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

procedure TfrmFacturasEditor.DoAnadir;
begin
  FIsNewRecord := True;
  FFacturaId := '';
  if cbSerie.Items.Count > 0 then
  begin
    cbSerie.ItemIndex := 0;
    cbSerieChange(cbSerie);
  end
  else
  begin
    edtNumero.Text := dmgMain.GetNextIDS('ge_facturas');
  end;
  dtpFecha.Date := Now;
  edtTotal.Text := '0,00';
  edtIva.Text := '0,00';
  edtBase.Text := '0,00';
  sgIvaDesglose.RowCount := 2;
  sgIvaDesglose.Cells[0, 1] := '';
  sgIvaDesglose.Cells[1, 1] := '';
  sgIvaDesglose.Cells[2, 1] := '';
  FTotalBase := 0;
  FTotalIva := 0;
  FTotalDoc := 0;
  cbCliente.ItemIndex := -1;
  cbDestino.Items.Clear;
  if Assigned(FDestinationIds) then
    FDestinationIds.Clear;
  chkPagada.Checked := False;
  dtpFechaCobro.Date := Now;
  UpdateClienteDetails;
  UpdateDestinoDetails;

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

  cbCliente.SetFocus;
end;

procedure TfrmFacturasEditor.DoModificar;
begin
  cbCliente.SetFocus;
end;

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

procedure TfrmFacturasEditor.DoPrimero;
var
  LParent: TfrmFacturas;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmFacturas) then
    LParent := TfrmFacturas(FParentForm)
  else if Assigned(frmFacturas) then
    LParent := frmFacturas;

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

procedure TfrmFacturasEditor.DoAnterior;
var
  LParent: TfrmFacturas;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmFacturas) then
    LParent := TfrmFacturas(FParentForm)
  else if Assigned(frmFacturas) then
    LParent := frmFacturas;

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

procedure TfrmFacturasEditor.DoSiguiente;
var
  LParent: TfrmFacturas;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmFacturas) then
    LParent := TfrmFacturas(FParentForm)
  else if Assigned(frmFacturas) then
    LParent := frmFacturas;

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

procedure TfrmFacturasEditor.DoUltimo;
var
  LParent: TfrmFacturas;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmFacturas) then
    LParent := TfrmFacturas(FParentForm)
  else if Assigned(frmFacturas) then
    LParent := frmFacturas;

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

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

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

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

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

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

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

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

  LClienteId := FClientIds[cbCliente.ItemIndex];

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

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

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

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

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

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

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

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

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

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

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

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

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

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

procedure TfrmFacturasEditor.DoDuplicar;
var
  LOldFacturaId, LNewFacturaId: string;
  LNextNum: Integer;
  Qry: TFDQuery;
begin
  if FFacturaId = '' then Exit;
  
  if MessageDlg('¿Está seguro de que desea duplicar esta factura?', mtConfirmation, [mbYes, mbNo], 0) <> mrYes then
    Exit;

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

  dmgMain.dbConn.StartTransaction;
  try
    // 1. Duplicar cabecera (sin id, auto-increment)
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text := 
      'INSERT INTO ge_facturas (empresa_id, cliente_id, clientedestino_id, serie, numero, referencia, fecha, base_imponible, total, iva, pagada, fecha_cobro, doc_cobro, id_forma_pago, id_user_creator, id_user_update) ' +
      'SELECT empresa_id, cliente_id, clientedestino_id, serie, :new_num, referencia, CURRENT_TIMESTAMP, base_imponible, total, iva, 0, NULL, NULL, id_forma_pago, :user, :user ' +
      'FROM ge_facturas WHERE id = :old_id';
    dmgMain.qryExec.ParamByName('old_id').AsInteger := StrToIntDef(LOldFacturaId, 0);
    dmgMain.qryExec.ParamByName('new_num').AsInteger := LNextNum;
    dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
    dmgMain.qryExec.ExecSQL;

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

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

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

    dmgMain.dbConn.Commit;
    
    FFacturaId := LNewFacturaId;
    FIsNewRecord := False;
    LoadData;
    
    if Assigned(FParentForm) and (FParentForm is TfrmFacturas) then
      TfrmFacturas(FParentForm).btnRefreshClick(nil);
      
    ShowMessage('Factura duplicada con éxito. Nuevo número: ' + IntToStr(LNextNum));
  except
    on E: Exception do
    begin
      dmgMain.dbConn.Rollback;
      ShowMessage('Error al duplicar la factura: ' + E.Message);
    end;
  end;
end;

procedure TfrmFacturasEditor.btnImprimirClick(Sender: TObject);
begin
  if (Trim(FFacturaId) = '') or FIsNewRecord then
  begin
    ShowMessage('Debe guardar la factura antes de poder imprimirla.');
    Exit;
  end;

  TFacturaPrintService.ImprimirFactura(Self, FFacturaId);
end;

procedure TfrmFacturasEditor.btnDocsRelacionadosClick(Sender: TObject);
begin
  if (Trim(FFacturaId) = '') or FIsNewRecord then
  begin
    ShowMessage('Debe guardar la factura antes de consultar sus documentos relacionados.');
    Exit;
  end;

  TfrmDocumentosRelacionados.MostrarDocumentos(Self, dvtFactura, FFacturaId, cbSerie.Text + '/' + edtNumero.Text);
end;

end.
