unit uVeriFactuService;

interface

uses
  System.SysUtils, System.Classes, System.DateUtils, Data.DB,
  FireDAC.Comp.Client, FireDAC.Stan.Param,
  uVeriFactuTypes, uSifCrypto, uSifXmlBuilder, uSifWsClient, uSifEventLogger;

type
  /// <summary>
  /// Servicio principal de gestion, sellado y remision de registros
  /// para el Sistema Informatico de Facturacion (SIF) y VERI*FACTU.
  /// </summary>
  TVeriFactuService = class
  private
    FConnection: TFDConnection;
    FEventLogger: TSifEventLogger;
    function GetUltimaHuella(AEmpresaId: Integer; const ASerie: string): string;
    function CargarConfiguracion(AEmpresaId: Integer; var AModo: TSifModoOperacion;
      var ASistema: TSifSistemaInfo; var AEndpointUrl: string; var ATimeout: Integer): Boolean;
  public
    constructor Create(AConnection: TFDConnection);
    destructor Destroy; override;

    /// <summary>Procesa el sellado y emision SIF de una factura (calculo hash, QR y envio AEAT)</summary>
    function ProcesarFactura(AFacturaId: Integer; AUserId: Integer = 1): TSifResultadoEmision;

    /// <summary>Procesa la anulacion formal de una factura en el sistema</summary>
    function AnularFactura(AFacturaId: Integer; const AMotivo: string; AUserId: Integer = 1): Boolean;

    /// <summary>Reintenta el envio telematico de facturas que quedaron en error de comunicacion</summary>
    function ReintentarEnviosPendientes(AEmpresaId: Integer): Integer;
  end;

implementation

constructor TVeriFactuService.Create(AConnection: TFDConnection);
begin
  inherited Create;
  FConnection := AConnection;
  FEventLogger := TSifEventLogger.Create(FConnection);
end;

destructor TVeriFactuService.Destroy;
begin
  FEventLogger.Free;
  inherited Destroy;
end;

function TVeriFactuService.GetUltimaHuella(AEmpresaId: Integer; const ASerie: string): string;
var
  LQry: TFDQuery;
begin
  Result := '';
  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := FConnection;
    LQry.SQL.Text :=
      'SELECT sif_huella_hash FROM ge_facturas ' +
      'WHERE empresa_id = :EMPRESA AND serie = :SERIE AND sif_huella_hash IS NOT NULL AND sif_huella_hash <> '''' ' +
      'ORDER BY numero DESC LIMIT 1';
    LQry.ParamByName('EMPRESA').AsInteger := AEmpresaId;
    LQry.ParamByName('SERIE').DataType := ftWideString;
    LQry.ParamByName('SERIE').AsWideString := ASerie;
    LQry.Open;
    if not LQry.IsEmpty then
      Result := Trim(LQry.FieldByName('sif_huella_hash').AsString);
  finally
    LQry.Free;
  end;
end;

function TVeriFactuService.CargarConfiguracion(AEmpresaId: Integer; var AModo: TSifModoOperacion;
  var ASistema: TSifSistemaInfo; var AEndpointUrl: string; var ATimeout: Integer): Boolean;
var
  LQry: TFDQuery;
begin
  Result := False;
  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := FConnection;
    LQry.SQL.Text :=
      'SELECT c.modo_trabajo, c.nombre_sistema, c.version_sistema, c.nif_desarrollador, ' +
      'c.razon_social_desarrollador, c.instalacion_id, c.url_ws_test, c.url_ws_prod, ' +
      'c.timeout_segundos ' +
      'FROM ge_sif_config c ' +
      'WHERE c.empresa_id = :EMPRESA AND c.activo = 1';
    LQry.ParamByName('EMPRESA').AsInteger := AEmpresaId;
    LQry.Open;

    if not LQry.IsEmpty then
    begin
      AModo := StringToModoOperacion(LQry.FieldByName('modo_trabajo').AsString);
      ASistema.NombreSistema := LQry.FieldByName('nombre_sistema').AsString;
      ASistema.VersionSistema := LQry.FieldByName('version_sistema').AsString;
      ASistema.NifDesarrollador := LQry.FieldByName('nif_desarrollador').AsString;
      ASistema.RazonSocialDesarrollador := LQry.FieldByName('razon_social_desarrollador').AsString;
      ASistema.InstalacionId := LQry.FieldByName('instalacion_id').AsString;
      ATimeout := LQry.FieldByName('timeout_segundos').AsInteger;

      if AModo = sifVeriFactuProd then
        AEndpointUrl := LQry.FieldByName('url_ws_prod').AsString
      else
        AEndpointUrl := LQry.FieldByName('url_ws_test').AsString;

      Result := True;
    end
    else
    begin
      // Valores por defecto si aún no se ha creado registro de configuración
      AModo := sifVeriFactuTest;
      ASistema.NombreSistema := 'GESTION--RAUL ASENCIO';
      ASistema.VersionSistema := '1.0.0';
      ASistema.NifDesarrollador := 'B12345678';
      ASistema.RazonSocialDesarrollador := 'SISTEMAS AGRIGEST S.L.';
      ASistema.InstalacionId := '01';
      AEndpointUrl := WS_ENDPOINT_AEAT_TEST;
      ATimeout := 30;
      Result := True;
    end;
  finally
    LQry.Free;
  end;
end;

function TVeriFactuService.ProcesarFactura(AFacturaId: Integer; AUserId: Integer): TSifResultadoEmision;
var
  LQryFac, LQryLineas, LUpd: TFDQuery;
  LModo: TSifModoOperacion;
  LSistema: TSifSistemaInfo;
  LEndpointUrl: string;
  LTimeout: Integer;
  LEmpresaId: Integer;
  LSerie, LNumSerieCompuesto: string;
  LNumero: Integer;
  LFechaDoc: TDateTime;
  LTotal, LIva: Double;
  LHashData: THashInputData;
  LFacturaXmlData: TSifFacturaData;
  LHuellaAnt, LHuellaPropia, LUrlQR, LPrefijoHuella: string;
  LFechaHoraISO: string;
  LSoapXml: string;
  LWsClient: TSifWsClient;
  LWsResponse: TSifWsResponse;
  LDesglosesList: TArray<TSifDesgloseIva>;
  LCount: Integer;
begin
  Result.Exito := False;
  Result.EstadoEnvio := seNoEnviado;
  Result.HuellaHash := '';
  Result.HuellaAnterior := '';
  Result.QRUrl := '';
  Result.QRPrefijoHuella := '';
  Result.CSV_AEAT := '';
  Result.CodigoError := '';
  Result.MensajeError := '';
  Result.XmlEnviado := '';
  Result.XmlRespuesta := '';

  LQryFac := TFDQuery.Create(nil);
  LQryLineas := TFDQuery.Create(nil);
  LUpd := TFDQuery.Create(nil);
  try
    LQryFac.Connection := FConnection;
    LQryLineas.Connection := FConnection;
    LUpd.Connection := FConnection;

    // 1. Cargar datos de la cabecera de la factura y datos fiscales
    LQryFac.SQL.Text :=
      'SELECT f.id, f.empresa_id, f.serie, f.numero, f.fecha, f.total, f.iva, ' +
      'COALESCE(f.tipo_factura_aeat, ''F1'') AS tipo_factura_aeat, ' +
      'COALESCE(f.clave_regimen_iva, ''01'') AS clave_regimen_iva, ' +
      'COALESCE(f.descripcion_operacion, ''Venta de productos de pastelería'') AS descripcion_operacion, ' +
      'e.cif_nif AS emisor_nif, e.nombre AS emisor_nombre, ' +
      'c.nif AS cliente_nif, c.nombre_fiscal AS cliente_nombre, ' +
      'COALESCE(c.codigo_pais, ''ES'') AS cliente_pais, ' +
      'c.tipo_id_fiscal_extranjero AS cliente_tipo_id ' +
      'FROM ge_facturas f ' +
      'INNER JOIN ge_empresas e ON e.id = f.empresa_id ' +
      'LEFT JOIN ge_clientes c ON c.id = f.cliente_id ' +
      'WHERE f.id = :ID';
    LQryFac.ParamByName('ID').AsInteger := AFacturaId;
    LQryFac.Open;

    if LQryFac.IsEmpty then
      raise Exception.CreateFmt('Factura con ID %d no encontrada en base de datos.', [AFacturaId]);

    LEmpresaId := LQryFac.FieldByName('empresa_id').AsInteger;
    CargarConfiguracion(LEmpresaId, LModo, LSistema, LEndpointUrl, LTimeout);
    Result.ModoTrabajo := LModo;

    LSerie := LQryFac.FieldByName('serie').AsString;
    LNumero := LQryFac.FieldByName('numero').AsInteger;
    LFechaDoc := LQryFac.FieldByName('fecha').AsDateTime;
    LTotal := LQryFac.FieldByName('total').AsFloat;
    LIva := LQryFac.FieldByName('iva').AsFloat;

    if Trim(LSerie) <> '' then
      LNumSerieCompuesto := Trim(LSerie) + '-' + IntToStr(LNumero)
    else
      LNumSerieCompuesto := IntToStr(LNumero);

    // 2. Cargar desglose de bases e impuestos por tipo de IVA
    LQryLineas.SQL.Text :=
      'SELECT tipo_iva, SUM(base) AS total_base, SUM(iva) AS total_iva, ' +
      'COALESCE(tipo_re, 0.00) AS tipo_re, SUM(re) AS total_re, ' +
      'causa_exencion ' +
      'FROM ge_facturas_lineas ' +
      'WHERE factura_id = :ID ' +
      'GROUP BY tipo_iva, tipo_re, causa_exencion';
    LQryLineas.ParamByName('ID').AsInteger := AFacturaId;
    LQryLineas.Open;

    SetLength(LDesglosesList, 0);
    LCount := 0;
    while not LQryLineas.Eof do
    begin
      SetLength(LDesglosesList, LCount + 1);
      LDesglosesList[LCount].TipoIva := LQryLineas.FieldByName('tipo_iva').AsFloat;
      LDesglosesList[LCount].BaseImponible := LQryLineas.FieldByName('total_base').AsFloat;
      LDesglosesList[LCount].CuotaIva := LQryLineas.FieldByName('total_iva').AsFloat;
      LDesglosesList[LCount].TipoRecargoEq := LQryLineas.FieldByName('tipo_re').AsFloat;
      LDesglosesList[LCount].CuotaRecargoEq := LQryLineas.FieldByName('total_re').AsFloat;
      LDesglosesList[LCount].CausaExencion := LQryLineas.FieldByName('causa_exencion').AsString;
      Inc(LCount);
      LQryLineas.Next;
    end;

    // Si no hay líneas (caso de prueba), crear un desglose base
    if Length(LDesglosesList) = 0 then
    begin
      SetLength(LDesglosesList, 1);
      LDesglosesList[0].TipoIva := 21.00;
      LDesglosesList[0].BaseImponible := LTotal - LIva;
      LDesglosesList[0].CuotaIva := LIva;
      LDesglosesList[0].TipoRecargoEq := 0;
      LDesglosesList[0].CuotaRecargoEq := 0;
      LDesglosesList[0].CausaExencion := '';
    end;

    // 3. Obtener huella de encadenamiento anterior
    LHuellaAnt := GetUltimaHuella(LEmpresaId, LSerie);
    Result.FechaHoraRegistro := Now;
    LFechaHoraISO := DateToISO8601(Result.FechaHoraRegistro, False);

    // 4. Calcular Huella SHA-256
    LHashData.IDEmisorFactura := LQryFac.FieldByName('emisor_nif').AsString;
    LHashData.NumSerieFactura := LNumSerieCompuesto;
    LHashData.FechaExpedicionFactura := FormatDateTime('dd-mm-yyyy', LFechaDoc);
    LHashData.TipoFactura := LQryFac.FieldByName('tipo_factura_aeat').AsString;
    LHashData.CuotaTotal := LIva;
    LHashData.ImporteTotal := LTotal;
    LHashData.HuellaAnterior := LHuellaAnt;
    LHashData.FechaHoraGenRegistro := LFechaHoraISO;

    LHuellaPropia := TSifCrypto.CalcularHuellaSHA256(LHashData);
    LUrlQR := TSifCrypto.GenerarUrlQR(LModo, LHashData.IDEmisorFactura, LHashData.NumSerieFactura,
      LHashData.FechaExpedicionFactura, LTotal, LHuellaPropia);
    LPrefijoHuella := TSifCrypto.ExtraerPrefijoHuella(LHuellaPropia);

    // 5. Preparar estructura de datos XML
    LFacturaXmlData.Emisor.Nif := LQryFac.FieldByName('emisor_nif').AsString;
    LFacturaXmlData.Emisor.RazonSocial := LQryFac.FieldByName('emisor_nombre').AsString;
    LFacturaXmlData.Destinatario.Nif := LQryFac.FieldByName('cliente_nif').AsString;
    LFacturaXmlData.Destinatario.RazonSocial := LQryFac.FieldByName('cliente_nombre').AsString;
    LFacturaXmlData.Destinatario.CodigoPais := LQryFac.FieldByName('cliente_pais').AsString;
    LFacturaXmlData.Destinatario.TipoIdExtranjero := LQryFac.FieldByName('cliente_tipo_id').AsString;
    LFacturaXmlData.Destinatario.NumIdExtranjero := LQryFac.FieldByName('cliente_nif').AsString;
    LFacturaXmlData.Destinatario.EsExtranjero := (LFacturaXmlData.Destinatario.CodigoPais <> 'ES') and (LFacturaXmlData.Destinatario.CodigoPais <> '');
    LFacturaXmlData.Sistema := LSistema;
    LFacturaXmlData.NumSerieFactura := LNumSerieCompuesto;
    LFacturaXmlData.FechaExpedicion := LHashData.FechaExpedicionFactura;
    LFacturaXmlData.TipoFactura := StringToTipoFactura(LHashData.TipoFactura);
    LFacturaXmlData.ClaveRegimen := LQryFac.FieldByName('clave_regimen_iva').AsString;
    LFacturaXmlData.DescripcionOperacion := LQryFac.FieldByName('descripcion_operacion').AsString;
    LFacturaXmlData.Desgloses := LDesglosesList;
    LFacturaXmlData.CuotaTotal := LIva;
    LFacturaXmlData.ImporteTotal := LTotal;
    LFacturaXmlData.HuellaHash := LHuellaPropia;
    LFacturaXmlData.HuellaAnterior := LHuellaAnt;
    LFacturaXmlData.EsPrimerRegistro := (LHuellaAnt = '');
    LFacturaXmlData.FechaHoraGenISO := LFechaHoraISO;

    LSoapXml := TSifXmlBuilder.GenerarSobreSoapAlta(LFacturaXmlData);
    Result.XmlEnviado := LSoapXml;

    // 6. Ejecutar remisión telemática según el modo operativo
    if LModo in [sifVeriFactuTest, sifVeriFactuProd] then
    begin
      LWsClient := TSifWsClient.Create(LTimeout);
      try
        LWsResponse := LWsClient.EnviarRegistroAlta(LModo, LEndpointUrl, LSoapXml);
        Result.EstadoEnvio := LWsResponse.EstadoEnvio;
        Result.CSV_AEAT := LWsResponse.CSV;
        Result.CodigoError := LWsResponse.CodigoError;
        Result.MensajeError := LWsResponse.DescripcionError;
        Result.XmlRespuesta := LWsResponse.XmlRespuesta;
        Result.Exito := LWsResponse.Exito;

        if not LWsResponse.Exito then
        begin
          FEventLogger.RegistrarEvento(LEmpresaId, steErrorComunicacionAEAT,
            Format('Error remision factura %s: %s [%s]', [LNumSerieCompuesto, LWsResponse.DescripcionError, LWsResponse.CodigoError]),
            '', AUserId);
        end;
      finally
        LWsClient.Free;
      end;
    end
    else
    begin
      // Modo NO_VERIFACTU (Custodia Local)
      Result.EstadoEnvio := seNoEnviado;
      Result.Exito := True;
    end;

    // 7. Persistir metadatos en ge_facturas
    LUpd.SQL.Text :=
      'UPDATE ge_facturas SET ' +
      '  sif_huella_hash = :HUELLA, ' +
      '  sif_huella_hash_anterior = :HUELLA_ANT, ' +
      '  sif_fecha_hora_registro = :FECHA_HORA, ' +
      '  sif_primer_registro = :PRIMER_REG, ' +
      '  qr_url = :QR_URL, ' +
      '  qr_huella = :QR_HUELLA, ' +
      '  sif_modo_trabajo = :MODO, ' +
      '  verifactu_estado = :ESTADO, ' +
      '  verifactu_csv = :CSV, ' +
      '  verifactu_fecha_envio = CURRENT_TIMESTAMP, ' +
      '  verifactu_codigo_error = :COD_ERR, ' +
      '  verifactu_descripcion_error = :DESC_ERR ' +
      'WHERE id = :ID';

    LUpd.ParamByName('HUELLA').DataType := ftWideString;
    LUpd.ParamByName('HUELLA').AsWideString := LHuellaPropia;
    LUpd.ParamByName('HUELLA_ANT').DataType := ftWideString;
    LUpd.ParamByName('HUELLA_ANT').AsWideString := LHuellaAnt;
    LUpd.ParamByName('FECHA_HORA').AsDateTime := Result.FechaHoraRegistro;
    LUpd.ParamByName('PRIMER_REG').AsInteger := Ord(LHuellaAnt = '');
    LUpd.ParamByName('QR_URL').DataType := ftWideString;
    LUpd.ParamByName('QR_URL').AsWideString := LUrlQR;
    LUpd.ParamByName('QR_HUELLA').DataType := ftWideString;
    LUpd.ParamByName('QR_HUELLA').AsWideString := LPrefijoHuella;
    LUpd.ParamByName('MODO').AsString := ModoOperacionToString(LModo);
    LUpd.ParamByName('ESTADO').AsString := EstadoEnvioToString(Result.EstadoEnvio);

    if Trim(Result.CSV_AEAT) <> '' then
      LUpd.ParamByName('CSV').AsString := Result.CSV_AEAT
    else
      LUpd.ParamByName('CSV').Clear;

    if Trim(Result.CodigoError) <> '' then
      LUpd.ParamByName('COD_ERR').AsString := Result.CodigoError
    else
      LUpd.ParamByName('COD_ERR').Clear;

    if Trim(Result.MensajeError) <> '' then
    begin
      LUpd.ParamByName('DESC_ERR').DataType := ftWideString;
      LUpd.ParamByName('DESC_ERR').AsWideString := Result.MensajeError;
    end
    else
      LUpd.ParamByName('DESC_ERR').Clear;

    LUpd.ParamByName('ID').AsInteger := AFacturaId;
    LUpd.ExecSQL;

    Result.HuellaHash := LHuellaPropia;
    Result.HuellaAnterior := LHuellaAnt;
    Result.QRUrl := LUrlQR;
    Result.QRPrefijoHuella := LPrefijoHuella;
  finally
    LQryFac.Free;
    LQryLineas.Free;
    LUpd.Free;
  end;
end;

function TVeriFactuService.AnularFactura(AFacturaId: Integer; const AMotivo: string; AUserId: Integer): Boolean;
var
  LQryFac, LInsAnula, LUpdFac: TFDQuery;
  LEmpresaId, LNumero: Integer;
  LSerie, LNumSerieCompuesto, LHuellaAnt, LHuellaAnulacion: string;
  LFechaDoc: TDateTime;
  LModo: TSifModoOperacion;
  LSistema: TSifSistemaInfo;
  LEndpointUrl: string;
  LTimeout: Integer;
  LHashData: THashInputData;
  LFechaHoraISO: string;
begin
  Result := False;
  LQryFac := TFDQuery.Create(nil);
  LInsAnula := TFDQuery.Create(nil);
  LUpdFac := TFDQuery.Create(nil);
  try
    LQryFac.Connection := FConnection;
    LInsAnula.Connection := FConnection;
    LUpdFac.Connection := FConnection;

    LQryFac.SQL.Text :=
      'SELECT f.empresa_id, f.serie, f.numero, f.fecha, e.cif_nif ' +
      'FROM ge_facturas f ' +
      'INNER JOIN ge_empresas e ON e.id = f.empresa_id ' +
      'WHERE f.id = :ID';
    LQryFac.ParamByName('ID').AsInteger := AFacturaId;
    LQryFac.Open;

    if LQryFac.IsEmpty then
      Exit;

    LEmpresaId := LQryFac.FieldByName('empresa_id').AsInteger;
    LSerie := LQryFac.FieldByName('serie').AsString;
    LNumero := LQryFac.FieldByName('numero').AsInteger;
    LFechaDoc := LQryFac.FieldByName('fecha').AsDateTime;
    LNumSerieCompuesto := Trim(LSerie) + '-' + IntToStr(LNumero);

    CargarConfiguracion(LEmpresaId, LModo, LSistema, LEndpointUrl, LTimeout);

    LHuellaAnt := GetUltimaHuella(LEmpresaId, LSerie);
    LFechaHoraISO := DateToISO8601(Now, False);

    // Huella del registro de anulación
    LHashData.IDEmisorFactura := LQryFac.FieldByName('cif_nif').AsString;
    LHashData.NumSerieFactura := LNumSerieCompuesto;
    LHashData.FechaExpedicionFactura := FormatDateTime('dd-mm-yyyy', LFechaDoc);
    LHashData.TipoFactura := 'ANULACION';
    LHashData.CuotaTotal := 0.00;
    LHashData.ImporteTotal := 0.00;
    LHashData.HuellaAnterior := LHuellaAnt;
    LHashData.FechaHoraGenRegistro := LFechaHoraISO;

    LHuellaAnulacion := TSifCrypto.CalcularHuellaSHA256(LHashData);

    // Insertar en ge_sif_anulaciones
    LInsAnula.SQL.Text :=
      'INSERT INTO ge_sif_anulaciones (' +
      '  empresa_id, factura_id, serie, numero, fecha_expedicion_factura, ' +
      '  motivo_anulacion, sif_huella_hash, sif_huella_hash_anterior, fecha_hora_anulacion, ' +
      '  verifactu_estado, id_user_creator' +
      ') VALUES (' +
      '  :EMPRESA, :FACTURA, :SERIE, :NUMERO, :FECHA_EXP, ' +
      '  :MOTIVO, :HUELLA, :HUELLA_ANT, CURRENT_TIMESTAMP(3), ' +
      '  :ESTADO, :USER' +
      ')';
    LInsAnula.ParamByName('EMPRESA').AsInteger := LEmpresaId;
    LInsAnula.ParamByName('FACTURA').AsInteger := AFacturaId;
    LInsAnula.ParamByName('SERIE').DataType := ftWideString;
    LInsAnula.ParamByName('SERIE').AsWideString := LSerie;
    LInsAnula.ParamByName('NUMERO').AsInteger := LNumero;
    LInsAnula.ParamByName('FECHA_EXP').AsDate := LFechaDoc;
    LInsAnula.ParamByName('MOTIVO').DataType := ftWideString;
    LInsAnula.ParamByName('MOTIVO').AsWideString := AMotivo;
    LInsAnula.ParamByName('HUELLA').DataType := ftWideString;
    LInsAnula.ParamByName('HUELLA').AsWideString := LHuellaAnulacion;
    LInsAnula.ParamByName('HUELLA_ANT').DataType := ftWideString;
    LInsAnula.ParamByName('HUELLA_ANT').AsWideString := LHuellaAnt;
    LInsAnula.ParamByName('ESTADO').AsString := 'ENVIADO';
    LInsAnula.ParamByName('USER').AsInteger := AUserId;
    LInsAnula.ExecSQL;

    // Registrar evento de anulación en log
    FEventLogger.RegistrarEvento(LEmpresaId, steAnulacionFactura,
      Format('Factura %s anulada formalmente. Motivo: %s', [LNumSerieCompuesto, AMotivo]),
      '', AUserId);

    Result := True;
  finally
    LQryFac.Free;
    LInsAnula.Free;
    LUpdFac.Free;
  end;
end;

function TVeriFactuService.ReintentarEnviosPendientes(AEmpresaId: Integer): Integer;
var
  LQry: TFDQuery;
  LRes: TSifResultadoEmision;
begin
  Result := 0;
  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := FConnection;
    LQry.SQL.Text :=
      'SELECT id FROM ge_facturas ' +
      'WHERE empresa_id = :EMPRESA AND verifactu_estado IN (''ERROR_COMUNICACION'', ''PENDIENTE_ENVIO'') ' +
      'ORDER BY id ASC LIMIT 50';
    LQry.ParamByName('EMPRESA').AsInteger := AEmpresaId;
    LQry.Open;

    while not LQry.Eof do
    begin
      LRes := ProcesarFactura(LQry.FieldByName('id').AsInteger);
      if LRes.Exito then
        Inc(Result);
      LQry.Next;
    end;
  finally
    LQry.Free;
  end;
end;

end.
