unit uProvPreciosService;

interface

uses
  System.SysUtils, System.Classes, System.Math, Data.DB, FireDAC.Comp.Client,
  FireDAC.Stan.Param;

type
  TVariacionPrecioResult = record
    HayVariacion: Boolean;
    PrecioAnterior: Double;
    PrecioNuevo: Double;
    VariacionImporte: Double;
    VariacionPorcentaje: Double;
    Mensaje: string;
  end;

  TProvPreciosService = class
  public
    /// <summary>
    /// Calcula el precio unitario neto final aplicando los descuentos informados en la línea
    /// </summary>
    class function CalcularPrecioFinalUnitario(APrecio, ADescuentoPrecio, ADescuentoPorcentaje: Double; ACantidad: Double = 1.0): Double;

    /// <summary>
    /// Busca la compra anterior más reciente del mismo artículo/materia prima
    /// </summary>
    class function ObtenerUltimoPrecioCompra(AConn: TFDConnection;
      AEmpresaId, AProveedorId: Integer;
      const ACodigoProveedor: string;
      AIdMateriaPrima: Integer;
      const AFechaActual: TDateTime;
      AExcluirLineaId: Integer = 0;
      const ATipoDocActual: string = ''): Double;

    /// <summary>
    /// Comprueba el precio final con la compra anterior y registra la variación si existe desviación.
    /// Evita duplicidades actualizando el registro si la línea ya había sido procesada.
    /// </summary>
    class function ComprobarYRegistrarVariacion(AConn: TFDConnection;
      AEmpresaId, AProveedorId: Integer;
      const ACodigoProveedor, ADescripcion: string;
      AIdMateriaPrima: Integer;
      const ATipoDocumento: string; // 'ALBARAN' o 'FACTURA'
      AIdDocumento, AIdLinea: Integer;
      const ANumDocumento: string;
      AFechaDoc: TDateTime;
      APrecioFinalActual: Double;
      AUserId: Integer = 1): TVariacionPrecioResult;
  end;

implementation

class function TProvPreciosService.CalcularPrecioFinalUnitario(APrecio, ADescuentoPrecio, ADescuentoPorcentaje: Double; ACantidad: Double): Double;
begin
  Result := APrecio;
  if Abs(ADescuentoPrecio) > 0.0001 then
    Result := APrecio - Abs(ADescuentoPrecio)
  else if Abs(ADescuentoPorcentaje) > 0.0001 then
    Result := APrecio * (1.0 - (Abs(ADescuentoPorcentaje) / 100.0));

  if Result < 0 then
    Result := 0;
end;

class function TProvPreciosService.ObtenerUltimoPrecioCompra(AConn: TFDConnection;
  AEmpresaId, AProveedorId: Integer;
  const ACodigoProveedor: string;
  AIdMateriaPrima: Integer;
  const AFechaActual: TDateTime;
  AExcluirLineaId: Integer;
  const ATipoDocActual: string): Double;
var
  QryAlb, QryFac, QryArtProv: TFDQuery;
  LPrecioAlb, LPrecioFac: Double;
  LFechaAlb, LFechaFac: TDateTime;
  LFoundAlb, LFoundFac: Boolean;
  LCodigo: string;
begin
  Result := 0.0;
  if not Assigned(AConn) then Exit;

  LCodigo := Trim(ACodigoProveedor);
  LPrecioAlb := 0.0;
  LPrecioFac := 0.0;
  LFechaAlb := 0;
  LFechaFac := 0;
  LFoundAlb := False;
  LFoundFac := False;

  QryAlb := TFDQuery.Create(nil);
  QryFac := TFDQuery.Create(nil);
  QryArtProv := TFDQuery.Create(nil);
  try
    QryAlb.Connection := AConn;
    QryFac.Connection := AConn;
    QryArtProv.Connection := AConn;

    // 1. Buscar en albaranes anteriores
    if AIdMateriaPrima > 0 then
    begin
      QryAlb.SQL.Text :=
        'SELECT d.id, d.precio, d.descuento_precio, d.descuento_porcentaje, d.cantidad, a.fecha ' +
        'FROM ge_prov_albaranes_detalle d ' +
        'JOIN ge_prov_albaranes a ON d.idalbaran = a.id ' +
        'WHERE d.idempresa = :emp ' +
        '  AND d.id_propio = :mat ' +
        '  AND (a.fecha < :fecha OR (a.fecha = :fecha AND (:tipo <> ''ALBARAN'' OR d.id <> :excluir))) ' +
        'ORDER BY a.fecha DESC, d.id DESC LIMIT 1';
      QryAlb.ParamByName('mat').AsInteger := AIdMateriaPrima;
    end
    else
    begin
      QryAlb.SQL.Text :=
        'SELECT d.id, d.precio, d.descuento_precio, d.descuento_porcentaje, d.cantidad, a.fecha ' +
        'FROM ge_prov_albaranes_detalle d ' +
        'JOIN ge_prov_albaranes a ON d.idalbaran = a.id ' +
        'WHERE d.idempresa = :emp ' +
        '  AND d.idproveedor = :prov ' +
        '  AND d.codigo = :cod ' +
        '  AND (a.fecha < :fecha OR (a.fecha = :fecha AND (:tipo <> ''ALBARAN'' OR d.id <> :excluir))) ' +
        'ORDER BY a.fecha DESC, d.id DESC LIMIT 1';
      QryAlb.ParamByName('prov').AsInteger := AProveedorId;
      QryAlb.ParamByName('cod').AsString := LCodigo;
    end;
    QryAlb.ParamByName('emp').AsInteger := AEmpresaId;
    QryAlb.ParamByName('fecha').AsDate := AFechaActual;
    QryAlb.ParamByName('tipo').AsString := ATipoDocActual;
    QryAlb.ParamByName('excluir').AsInteger := AExcluirLineaId;
    QryAlb.Open;

    if not QryAlb.IsEmpty then
    begin
      LFoundAlb := True;
      LFechaAlb := QryAlb.FieldByName('fecha').AsDateTime;
      LPrecioAlb := CalcularPrecioFinalUnitario(
        QryAlb.FieldByName('precio').AsFloat,
        QryAlb.FieldByName('descuento_precio').AsFloat,
        QryAlb.FieldByName('descuento_porcentaje').AsFloat,
        QryAlb.FieldByName('cantidad').AsFloat
      );
    end;

    // 2. Buscar en facturas de proveedor anteriores
    if AIdMateriaPrima > 0 then
    begin
      QryFac.SQL.Text :=
        'SELECT d.id, d.precio, d.descuento_precio, d.descuento_porcentaje, d.cantidad, f.fecha ' +
        'FROM ge_prov_facturas_detalle d ' +
        'JOIN ge_prov_facturas f ON d.idfactura = f.id ' +
        'WHERE d.idempresa = :emp ' +
        '  AND d.id_propio = :mat ' +
        '  AND (f.fecha < :fecha OR (f.fecha = :fecha AND (:tipo <> ''FACTURA'' OR d.id <> :excluir))) ' +
        'ORDER BY f.fecha DESC, d.id DESC LIMIT 1';
      QryFac.ParamByName('mat').AsInteger := AIdMateriaPrima;
    end
    else
    begin
      QryFac.SQL.Text :=
        'SELECT d.id, d.precio, d.descuento_precio, d.descuento_porcentaje, d.cantidad, f.fecha ' +
        'FROM ge_prov_facturas_detalle d ' +
        'JOIN ge_prov_facturas f ON d.idfactura = f.id ' +
        'WHERE d.idempresa = :emp ' +
        '  AND d.idproveedor = :prov ' +
        '  AND d.codigo = :cod ' +
        '  AND (f.fecha < :fecha OR (f.fecha = :fecha AND (:tipo <> ''FACTURA'' OR d.id <> :excluir))) ' +
        'ORDER BY f.fecha DESC, d.id DESC LIMIT 1';
      QryFac.ParamByName('prov').AsInteger := AProveedorId;
      QryFac.ParamByName('cod').AsString := LCodigo;
    end;
    QryFac.ParamByName('emp').AsInteger := AEmpresaId;
    QryFac.ParamByName('fecha').AsDate := AFechaActual;
    QryFac.ParamByName('tipo').AsString := ATipoDocActual;
    QryFac.ParamByName('excluir').AsInteger := AExcluirLineaId;
    QryFac.Open;

    if not QryFac.IsEmpty then
    begin
      LFoundFac := True;
      LFechaFac := QryFac.FieldByName('fecha').AsDateTime;
      LPrecioFac := CalcularPrecioFinalUnitario(
        QryFac.FieldByName('precio').AsFloat,
        QryFac.FieldByName('descuento_precio').AsFloat,
        QryFac.FieldByName('descuento_porcentaje').AsFloat,
        QryFac.FieldByName('cantidad').AsFloat
      );
    end;

    // 3. Comparar fecha más reciente entre albaranes y facturas
    if LFoundAlb and LFoundFac then
    begin
      if LFechaAlb >= LFechaFac then
        Result := LPrecioAlb
      else
        Result := LPrecioFac;
    end
    else if LFoundAlb then
      Result := LPrecioAlb
    else if LFoundFac then
      Result := LPrecioFac;

    // 4. Si aún no hay precio histórico en líneas, consultar ge_proveedores_materias_primas
    if (Result <= 0.0001) and (LCodigo <> '') and (AProveedorId > 0) then
    begin
      QryArtProv.SQL.Text :=
        'SELECT ult_precio FROM ge_proveedores_materias_primas ' +
        'WHERE id_proveedor = :prov AND codigo_articulo_proveedor = :cod AND ult_precio > 0 ' +
        'LIMIT 1';
      QryArtProv.ParamByName('prov').AsInteger := AProveedorId;
      QryArtProv.ParamByName('cod').AsString := LCodigo;
      QryArtProv.Open;
      if not QryArtProv.IsEmpty then
        Result := QryArtProv.FieldByName('ult_precio').AsFloat;
    end;

  finally
    QryAlb.Free;
    QryFac.Free;
    QryArtProv.Free;
  end;
end;

class function TProvPreciosService.ComprobarYRegistrarVariacion(AConn: TFDConnection;
  AEmpresaId, AProveedorId: Integer;
  const ACodigoProveedor, ADescripcion: string;
  AIdMateriaPrima: Integer;
  const ATipoDocumento: string;
  AIdDocumento, AIdLinea: Integer;
  const ANumDocumento: string;
  AFechaDoc: TDateTime;
  APrecioFinalActual: Double;
  AUserId: Integer): TVariacionPrecioResult;
var
  LPrecioAnterior: Double;
  LDifImporte: Double;
  LPorcVar: Double;
  QryExec, QryCheck: TFDQuery;
  LExistingId: Integer;
begin
  Result.HayVariacion := False;
  Result.PrecioAnterior := 0.0;
  Result.PrecioNuevo := APrecioFinalActual;
  Result.VariacionImporte := 0.0;
  Result.VariacionPorcentaje := 0.0;
  Result.Mensaje := '';

  if not Assigned(AConn) or (APrecioFinalActual <= 0) then Exit;

  // 1. Obtener el precio de la compra anterior
  LPrecioAnterior := ObtenerUltimoPrecioCompra(
    AConn, AEmpresaId, AProveedorId, ACodigoProveedor, AIdMateriaPrima, AFechaDoc, AIdLinea, ATipoDocumento
  );

  Result.PrecioAnterior := LPrecioAnterior;

  // 2. Comprobar si hay variación respecto a la compra anterior
  if (LPrecioAnterior > 0.0001) and (Abs(APrecioFinalActual - LPrecioAnterior) > 0.0001) then
  begin
    LDifImporte := APrecioFinalActual - LPrecioAnterior;
    LPorcVar := (LDifImporte / LPrecioAnterior) * 100.0;

    Result.HayVariacion := True;
    Result.VariacionImporte := LDifImporte;
    Result.VariacionPorcentaje := LPorcVar;

    if LDifImporte > 0 then
      Result.Mensaje := Format('Subida de precio en "%s": %.4f € -> %.4f € (+%.2f %%)',
        [ADescripcion, LPrecioAnterior, APrecioFinalActual, LPorcVar])
    else
      Result.Mensaje := Format('Bajada de precio en "%s": %.4f € -> %.4f € (%.2f %%)',
        [ADescripcion, LPrecioAnterior, APrecioFinalActual, LPorcVar]);

    // 3. Registrar o actualizar en ge_prov_precios_variaciones
    QryCheck := TFDQuery.Create(nil);
    QryExec := TFDQuery.Create(nil);
    try
      QryCheck.Connection := AConn;
      QryExec.Connection := AConn;

      QryCheck.SQL.Text :=
        'SELECT id FROM ge_prov_precios_variaciones ' +
        'WHERE tipo_documento = :tipo AND id_linea = :linea LIMIT 1';
      QryCheck.ParamByName('tipo').AsString := ATipoDocumento;
      QryCheck.ParamByName('linea').AsInteger := AIdLinea;
      QryCheck.Open;

      if not QryCheck.IsEmpty then
      begin
        LExistingId := QryCheck.FieldByName('id').AsInteger;
        QryExec.SQL.Text :=
          'UPDATE ge_prov_precios_variaciones SET ' +
          '  id_empresa = :emp, ' +
          '  id_proveedor = :prov, ' +
          '  id_materia_prima = :mat, ' +
          '  codigo_articulo_proveedor = :cod, ' +
          '  descripcion = :desc, ' +
          '  id_documento = :doc_id, ' +
          '  documento = :doc_num, ' +
          '  fecha_documento = :fecha, ' +
          '  precio_anterior = :p_ant, ' +
          '  precio_nuevo = :p_nuevo, ' +
          '  variacion_importe = :v_imp, ' +
          '  variacion_porcentaje = :v_porc, ' +
          '  id_user_creator = :user ' +
          'WHERE id = :id';
        QryExec.ParamByName('id').AsInteger := LExistingId;
      end
      else
      begin
        QryExec.SQL.Text :=
          'INSERT INTO ge_prov_precios_variaciones (' +
          '  id_empresa, id_proveedor, id_materia_prima, codigo_articulo_proveedor, descripcion, ' +
          '  tipo_documento, id_documento, id_linea, documento, fecha_documento, ' +
          '  precio_anterior, precio_nuevo, variacion_importe, variacion_porcentaje, id_user_creator' +
          ') VALUES (' +
          '  :emp, :prov, :mat, :cod, :desc, ' +
          '  :tipo, :doc_id, :linea, :doc_num, :fecha, ' +
          '  :p_ant, :p_nuevo, :v_imp, :v_porc, :user' +
          ')';
        QryExec.ParamByName('tipo').AsString := ATipoDocumento;
        QryExec.ParamByName('linea').AsInteger := AIdLinea;
      end;

      QryExec.ParamByName('emp').AsInteger := AEmpresaId;
      QryExec.ParamByName('prov').AsInteger := AProveedorId;
      if AIdMateriaPrima > 0 then
        QryExec.ParamByName('mat').AsInteger := AIdMateriaPrima
      else
        QryExec.ParamByName('mat').Clear;
      QryExec.ParamByName('cod').AsString := ACodigoProveedor;
      QryExec.ParamByName('desc').AsString := Copy(ADescripcion, 1, 100);
      QryExec.ParamByName('doc_id').AsInteger := AIdDocumento;
      QryExec.ParamByName('doc_num').AsString := Copy(ANumDocumento, 1, 50);
      QryExec.ParamByName('fecha').AsDate := AFechaDoc;
      QryExec.ParamByName('p_ant').AsFloat := LPrecioAnterior;
      QryExec.ParamByName('p_nuevo').AsFloat := APrecioFinalActual;
      QryExec.ParamByName('v_imp').AsFloat := LDifImporte;
      QryExec.ParamByName('v_porc').AsFloat := LPorcVar;
      QryExec.ParamByName('user').AsInteger := AUserId;
      QryExec.ExecSQL;

    finally
      QryCheck.Free;
      QryExec.Free;
    end;
  end;

  // 4. Actualizar ult_precio en ge_proveedores_materias_primas si existe la referencia
  if (AProveedorId > 0) and (Trim(ACodigoProveedor) <> '') then
  begin
    QryExec := TFDQuery.Create(nil);
    try
      QryExec.Connection := AConn;
      QryExec.SQL.Text :=
        'UPDATE ge_proveedores_materias_primas ' +
        'SET ult_precio = :precio ' +
        'WHERE id_proveedor = :prov AND codigo_articulo_proveedor = :cod';
      QryExec.ParamByName('precio').AsFloat := APrecioFinalActual;
      QryExec.ParamByName('prov').AsInteger := AProveedorId;
      QryExec.ParamByName('cod').AsString := Trim(ACodigoProveedor);
      QryExec.ExecSQL;
    finally
      QryExec.Free;
    end;
  end;
end;

end.
