unit frm_MovimientoInternoEditor;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls,
  Vcl.Buttons, Vcl.Grids, Vcl.DBGrids, Data.DB, FireDAC.Comp.Client,
  FireDAC.Comp.DataSet, FireDAC.Stan.Param, System.UITypes, uAppTheme,
  uGS1BarcodeParser;

type
  TUbicacionItem = class
  public
    Tipo: string;            // 'ALMACEN', 'OBRADOR', 'TIENDA'
    Id: Integer;             // ID en la tabla correspondiente
    IdAlmacen: Integer;      // ID en ge_almacenes para imputar el stock
    IdEmpresa: Integer;      // ID en ge_empresas
    NombreEmpresa: string;
    Nombre: string;
    CifEmpresa: string;
  end;

  TStockProductoItem = class
  public
    StockEnvasadoId: Integer;
    IdArticulo: Integer;
    CodigoArticulo: string;
    NombreArticulo: string;
    IdLote: Integer;
    CodigoLote: string;
    IdFormatoPresentacion: Integer;
    NombreFormato: string;
    StockDisponible: Double;
    PrecioCoste: Double;
    TipoIvaPorcentaje: Double;
  end;

  TfrmMovimientoInternoEditor = class(TForm)
    pnlTop: TPanel;
    lblTitulo: TLabel;
    lblSubtitulo: TLabel;
    pnlBody: TPanel;
    gbOrigen: TGroupBox;
    lblTipoOrigen: TLabel;
    lblUbicacionOrigen: TLabel;
    lblEmpresaOrigenInfo: TLabel;
    cboTipoOrigen: TComboBox;
    cboUbicacionOrigen: TComboBox;
    gbDestino: TGroupBox;
    lblTipoDestino: TLabel;
    lblUbicacionDestino: TLabel;
    lblEmpresaDestinoInfo: TLabel;
    cboTipoDestino: TComboBox;
    cboUbicacionDestino: TComboBox;
    pnlAlertaInterempresa: TPanel;
    lblAlertaInterempresa: TLabel;
    gbProducto: TGroupBox;
    lblEscaner: TLabel;
    edtEscaner: TEdit;
    btnEscanear: TBitBtn;
    chkAutoAdd: TCheckBox;
    lblProducto: TLabel;
    edtArticulo: TEdit;
    btnBuscarArticulo: TBitBtn;
    lblLote: TLabel;
    cboLoteStock: TComboBox;
    lblStockDisp: TLabel;
    lblCantidad: TLabel;
    lblPrecioCoste: TLabel;
    edtStockDisp: TEdit;
    edtCantidad: TEdit;
    edtPrecioCoste: TEdit;
    btnAnadirLinea: TBitBtn;
    gbLineas: TGroupBox;
    dbgLineas: TDBGrid;
    btnEliminarLinea: TBitBtn;
    lblTotalLineas: TLabel;
    lblObs: TLabel;
    edtObs: TEdit;
    pnlBottom: TPanel;
    btnEjecutar: TBitBtn;
    btnCancelar: TBitBtn;
    mtLineas: TFDMemTable;
    dsLineas: TDataSource;

    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure cboTipoOrigenChange(Sender: TObject);
    procedure cboUbicacionOrigenChange(Sender: TObject);
    procedure cboTipoDestinoChange(Sender: TObject);
    procedure cboUbicacionDestinoChange(Sender: TObject);
    procedure edtEscanerKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure btnEscanearClick(Sender: TObject);
    procedure btnBuscarArticuloClick(Sender: TObject);
    procedure cboLoteStockChange(Sender: TObject);
    procedure btnAnadirLineaClick(Sender: TObject);
    procedure btnEliminarLineaClick(Sender: TObject);
    procedure btnEjecutarClick(Sender: TObject);

  private
    FSelectedArticuloId: Integer;
    FSelectedArticuloCodigo: string;
    FSelectedArticuloDesc: string;
    procedure SetupMemTable;
    procedure SetupGridColumns;
    procedure ActualizarTotalesLineas;
    procedure LimpiarComboObjetos(ACombo: TComboBox);
    procedure LimpiarSeleccionArticulo;
    procedure CargarUbicaciones(ACombo: TComboBox; const ATipo: string; ASelectId: Integer = 0);
    procedure ActualizarEstadoInterempresa;
    procedure CargarLotesArticuloSeleccionado;
    procedure ProcesarCodigoEscaneado(const ABarcode: string);
    function GetUbicacionSeleccionada(ACombo: TComboBox): TUbicacionItem;
    function GetStockItemSeleccionado: TStockProductoItem;
    function ObtenerOCrearClienteEmpresa(AEmpresaDestinoId: Integer; const ACifDestino, ANombreDestino: string): Integer;
    function ObtenerOCrearProveedorEmpresa(AEmpresaOrigenId: Integer; const ACifOrigen, ANombreOrigen: string): Integer;
    function AsegurarAlmacenParaUbicacion(AUbicacion: TUbicacionItem): Integer;
  public
  end;

var
  frmMovimientoInternoEditor: TfrmMovimientoInternoEditor;

implementation

uses
  dmg_Main, uDbErrorHandler, frm_SelectProduct;

{$R *.dfm}

procedure TfrmMovimientoInternoEditor.FormCreate(Sender: TObject);
begin
  // Aislamiento de tags
  cboTipoOrigen.Tag := 999;
  cboUbicacionOrigen.Tag := 999;
  cboTipoDestino.Tag := 999;
  cboUbicacionDestino.Tag := 999;
  edtEscaner.Tag := 999;
  btnEscanear.Tag := 999;
  chkAutoAdd.Tag := 999;
  edtArticulo.Tag := 999;
  btnBuscarArticulo.Tag := 999;
  cboLoteStock.Tag := 999;
  btnAnadirLinea.Tag := 999;
  btnEliminarLinea.Tag := 999;
  dbgLineas.Tag := 999;
  btnEjecutar.Tag := 999;
  btnCancelar.Tag := 999;

  SetupMemTable;
  SetupGridColumns;
  ActualizarTotalesLineas;
end;

procedure TfrmMovimientoInternoEditor.SetupMemTable;
begin
  mtLineas.Close;
  mtLineas.FieldDefs.Clear;
  mtLineas.FieldDefs.Add('id_stock_envasado', ftInteger);
  mtLineas.FieldDefs.Add('id_articulo', ftInteger);
  mtLineas.FieldDefs.Add('codigo_articulo', ftWideString, 30);
  mtLineas.FieldDefs.Add('descripcion_articulo', ftWideString, 150);
  mtLineas.FieldDefs.Add('id_lote', ftInteger);
  mtLineas.FieldDefs.Add('codigo_lote', ftWideString, 50);
  mtLineas.FieldDefs.Add('id_formato', ftInteger);
  mtLineas.FieldDefs.Add('formato_nombre', ftWideString, 100);
  mtLineas.FieldDefs.Add('stock_disponible', ftFloat);
  mtLineas.FieldDefs.Add('cantidad', ftFloat);
  mtLineas.FieldDefs.Add('precio_coste', ftFloat);
  mtLineas.FieldDefs.Add('tipo_iva', ftFloat);
  mtLineas.FieldDefs.Add('subtotal', ftFloat);
  mtLineas.CreateDataSet;

  TFloatField(mtLineas.FieldByName('stock_disponible')).DisplayFormat := '#,##0.00';
  TFloatField(mtLineas.FieldByName('cantidad')).DisplayFormat := '#,##0.00';
  TFloatField(mtLineas.FieldByName('precio_coste')).DisplayFormat := '#,##0.00 €';
  TFloatField(mtLineas.FieldByName('subtotal')).DisplayFormat := '#,##0.00 €';
end;

procedure TfrmMovimientoInternoEditor.SetupGridColumns;
var
  Col: TColumn;
begin
  dbgLineas.Columns.Clear;

  Col := dbgLineas.Columns.Add;
  Col.FieldName := 'codigo_articulo';
  Col.Title.Caption := 'Cód.';
  Col.Width := 55;

  Col := dbgLineas.Columns.Add;
  Col.FieldName := 'descripcion_articulo';
  Col.Title.Caption := 'Artículo';
  Col.Width := 210;

  Col := dbgLineas.Columns.Add;
  Col.FieldName := 'codigo_lote';
  Col.Title.Caption := 'Lote';
  Col.Width := 85;

  Col := dbgLineas.Columns.Add;
  Col.FieldName := 'formato_nombre';
  Col.Title.Caption := 'Presentación';
  Col.Width := 105;

  Col := dbgLineas.Columns.Add;
  Col.FieldName := 'cantidad';
  Col.Title.Caption := 'Cantidad';
  Col.Width := 65;
  Col.Alignment := taRightJustify;
  Col.Title.Alignment := taRightJustify;

  Col := dbgLineas.Columns.Add;
  Col.FieldName := 'precio_coste';
  Col.Title.Caption := 'Coste';
  Col.Width := 75;
  Col.Alignment := taRightJustify;
  Col.Title.Alignment := taRightJustify;

  Col := dbgLineas.Columns.Add;
  Col.FieldName := 'subtotal';
  Col.Title.Caption := 'Subtotal Coste';
  Col.Width := 90;
  Col.Alignment := taRightJustify;
  Col.Title.Alignment := taRightJustify;
end;

procedure TfrmMovimientoInternoEditor.ActualizarTotalesLineas;
var
  LTotalUds, LTotalValor: Double;
  LCount: Integer;
  LBookmark: TBookmark;
begin
  LCount := mtLineas.RecordCount;
  LTotalUds := 0;
  LTotalValor := 0;

  if (mtLineas.Active) and (LCount > 0) then
  begin
    mtLineas.DisableControls;
    LBookmark := mtLineas.GetBookmark;
    try
      mtLineas.First;
      while not mtLineas.Eof do
      begin
        LTotalUds := LTotalUds + mtLineas.FieldByName('cantidad').AsFloat;
        LTotalValor := LTotalValor + mtLineas.FieldByName('subtotal').AsFloat;
        mtLineas.Next;
      end;
    finally
      if mtLineas.BookmarkValid(LBookmark) then
        mtLineas.GotoBookmark(LBookmark);
      mtLineas.FreeBookmark(LBookmark);
      mtLineas.EnableControls;
    end;
  end;

  lblTotalLineas.Caption := Format(
    'Total: %d artículo(s) | %.2f uds en total | Valoración total de traspaso: %.2f €',
    [LCount, LTotalUds, LTotalValor]
  );
  btnEliminarLinea.Enabled := (LCount > 0);
  btnEjecutar.Enabled := (LCount > 0);
end;

procedure TfrmMovimientoInternoEditor.FormDestroy(Sender: TObject);
begin
  LimpiarComboObjetos(cboUbicacionOrigen);
  LimpiarComboObjetos(cboUbicacionDestino);
  LimpiarComboObjetos(cboLoteStock);
end;

procedure TfrmMovimientoInternoEditor.LimpiarComboObjetos(ACombo: TComboBox);
var
  I: Integer;
begin
  for I := 0 to ACombo.Items.Count - 1 do
  begin
    if Assigned(ACombo.Items.Objects[I]) then
      ACombo.Items.Objects[I].Free;
  end;
  ACombo.Items.Clear;
end;

procedure TfrmMovimientoInternoEditor.FormShow(Sender: TObject);
begin
  TAppTheme.ApplyToForm(Self);

  cboTipoOrigen.Items.Clear;
  cboTipoOrigen.Items.Add('ALMACEN');
  cboTipoOrigen.Items.Add('TIENDA');
  cboTipoOrigen.Items.Add('OBRADOR');
  cboTipoOrigen.ItemIndex := 0;

  cboTipoDestino.Items.Clear;
  cboTipoDestino.Items.Add('TIENDA');
  cboTipoDestino.Items.Add('ALMACEN');
  cboTipoDestino.Items.Add('OBRADOR');
  cboTipoDestino.ItemIndex := 0;

  CargarUbicaciones(cboUbicacionOrigen, 'ALMACEN');
  CargarUbicaciones(cboUbicacionDestino, 'TIENDA');

  cboUbicacionOrigenChange(nil);
  cboUbicacionDestinoChange(nil);

  edtEscaner.SetFocus;
end;

procedure TfrmMovimientoInternoEditor.CargarUbicaciones(ACombo: TComboBox; const ATipo: string; ASelectId: Integer);
var
  LQry: TFDQuery;
  LItem: TUbicacionItem;
  LSql: string;
begin
  LimpiarComboObjetos(ACombo);
  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;

    if ATipo = 'ALMACEN' then
    begin
      LSql :=
        'SELECT a.id, a.id_empresa, a.descripcion as nombre, e.nombre as empresa_nombre, e.cif as empresa_cif ' +
        'FROM ge_almacenes a ' +
        'LEFT JOIN ge_empresas e ON a.id_empresa = e.id ' +
        'WHERE a.activo = 1 ' +
        'ORDER BY e.nombre ASC, a.descripcion ASC';
      LQry.SQL.Text := LSql;
      LQry.Open;
      while not LQry.Eof do
      begin
        LItem := TUbicacionItem.Create;
        LItem.Tipo := 'ALMACEN';
        LItem.Id := LQry.FieldByName('id').AsInteger;
        LItem.IdAlmacen := LQry.FieldByName('id').AsInteger;
        LItem.IdEmpresa := LQry.FieldByName('id_empresa').AsInteger;
        LItem.NombreEmpresa := LQry.FieldByName('empresa_nombre').AsString;
        LItem.Nombre := LQry.FieldByName('nombre').AsString;
        LItem.CifEmpresa := LQry.FieldByName('empresa_cif').AsString;

        ACombo.Items.AddObject(
          Format('%s [%s]', [LItem.Nombre, LItem.NombreEmpresa]),
          LItem
        );
        LQry.Next;
      end;
    end
    else if ATipo = 'TIENDA' then
    begin
      LSql :=
        'SELECT t.id, t.id_empresa, t.id_almacen, t.nombre, e.nombre as empresa_nombre, e.cif as empresa_cif ' +
        'FROM ge_tiendas t ' +
        'LEFT JOIN ge_empresas e ON t.id_empresa = e.id ' +
        'WHERE t.activo = 1 ' +
        'ORDER BY e.nombre ASC, t.nombre ASC';
      LQry.SQL.Text := LSql;
      LQry.Open;
      while not LQry.Eof do
      begin
        LItem := TUbicacionItem.Create;
        LItem.Tipo := 'TIENDA';
        LItem.Id := LQry.FieldByName('id').AsInteger;
        LItem.IdAlmacen := LQry.FieldByName('id_almacen').AsInteger;
        LItem.IdEmpresa := LQry.FieldByName('id_empresa').AsInteger;
        LItem.NombreEmpresa := LQry.FieldByName('empresa_nombre').AsString;
        LItem.Nombre := LQry.FieldByName('nombre').AsString;
        LItem.CifEmpresa := LQry.FieldByName('empresa_cif').AsString;

        ACombo.Items.AddObject(
          Format('%s [%s]', [LItem.Nombre, LItem.NombreEmpresa]),
          LItem
        );
        LQry.Next;
      end;
    end
    else if ATipo = 'OBRADOR' then
    begin
      LSql :=
        'SELECT o.id, o.id_empresa, o.nombre, e.nombre as empresa_nombre, e.cif as empresa_cif ' +
        'FROM ge_obradores o ' +
        'LEFT JOIN ge_empresas e ON o.id_empresa = e.id ' +
        'WHERE o.activo = 1 ' +
        'ORDER BY e.nombre ASC, o.nombre ASC';
      LQry.SQL.Text := LSql;
      LQry.Open;
      while not LQry.Eof do
      begin
        LItem := TUbicacionItem.Create;
        LItem.Tipo := 'OBRADOR';
        LItem.Id := LQry.FieldByName('id').AsInteger;
        LItem.IdAlmacen := 0; // Se resolverá por empresa si no tiene directo
        LItem.IdEmpresa := LQry.FieldByName('id_empresa').AsInteger;
        LItem.NombreEmpresa := LQry.FieldByName('empresa_nombre').AsString;
        LItem.Nombre := LQry.FieldByName('nombre').AsString;
        LItem.CifEmpresa := LQry.FieldByName('empresa_cif').AsString;

        ACombo.Items.AddObject(
          Format('%s [%s]', [LItem.Nombre, LItem.NombreEmpresa]),
          LItem
        );
        LQry.Next;
      end;
    end;

  finally
    LQry.Free;
  end;

  if ACombo.Items.Count > 0 then
    ACombo.ItemIndex := 0;
end;

function TfrmMovimientoInternoEditor.GetUbicacionSeleccionada(ACombo: TComboBox): TUbicacionItem;
begin
  Result := nil;
  if (ACombo.ItemIndex >= 0) and (ACombo.ItemIndex < ACombo.Items.Count) then
    Result := TUbicacionItem(ACombo.Items.Objects[ACombo.ItemIndex]);
end;

function TfrmMovimientoInternoEditor.GetStockItemSeleccionado: TStockProductoItem;
begin
  Result := nil;
  if (cboLoteStock.ItemIndex >= 0) and (cboLoteStock.ItemIndex < cboLoteStock.Items.Count) then
    Result := TStockProductoItem(cboLoteStock.Items.Objects[cboLoteStock.ItemIndex]);
end;

procedure TfrmMovimientoInternoEditor.LimpiarSeleccionArticulo;
begin
  FSelectedArticuloId := 0;
  FSelectedArticuloCodigo := '';
  FSelectedArticuloDesc := '';
  edtArticulo.Text := '[Pulse Buscar o escanee para seleccionar un artículo...]';
  LimpiarComboObjetos(cboLoteStock);
  edtStockDisp.Text := '0.00';
  edtCantidad.Text := '1.00';
  edtPrecioCoste.Text := '0.00';
end;

procedure TfrmMovimientoInternoEditor.cboTipoOrigenChange(Sender: TObject);
begin
  if (mtLineas.Active) and (mtLineas.RecordCount > 0) then
  begin
    if MessageDlg('Al cambiar de origen se vaciará la lista de artículos seleccionados.'#13#10 +
                  '¿Desea continuar?', mtConfirmation, [mbYes, mbNo], 0) = mrNo then
      Exit;
    mtLineas.EmptyDataSet;
    ActualizarTotalesLineas;
  end;

  CargarUbicaciones(cboUbicacionOrigen, cboTipoOrigen.Text);
  cboUbicacionOrigenChange(nil);
end;

procedure TfrmMovimientoInternoEditor.cboTipoDestinoChange(Sender: TObject);
begin
  CargarUbicaciones(cboUbicacionDestino, cboTipoDestino.Text);
  cboUbicacionDestinoChange(nil);
end;

procedure TfrmMovimientoInternoEditor.cboUbicacionOrigenChange(Sender: TObject);
var
  LUbic: TUbicacionItem;
begin
  if (mtLineas.Active) and (mtLineas.RecordCount > 0) then
  begin
    if MessageDlg('Al cambiar de origen se vaciará la lista de artículos seleccionados.'#13#10 +
                  '¿Desea continuar?', mtConfirmation, [mbYes, mbNo], 0) = mrNo then
      Exit;
    mtLineas.EmptyDataSet;
    ActualizarTotalesLineas;
  end;

  LUbic := GetUbicacionSeleccionada(cboUbicacionOrigen);
  if Assigned(LUbic) then
    lblEmpresaOrigenInfo.Caption := Format('Empresa: %s (CIF: %s)', [LUbic.NombreEmpresa, LUbic.CifEmpresa])
  else
    lblEmpresaOrigenInfo.Caption := 'Empresa: [No seleccionada]';

  ActualizarEstadoInterempresa;
  LimpiarSeleccionArticulo;
end;

procedure TfrmMovimientoInternoEditor.cboUbicacionDestinoChange(Sender: TObject);
var
  LUbic: TUbicacionItem;
begin
  LUbic := GetUbicacionSeleccionada(cboUbicacionDestino);
  if Assigned(LUbic) then
    lblEmpresaDestinoInfo.Caption := Format('Empresa: %s (CIF: %s)', [LUbic.NombreEmpresa, LUbic.CifEmpresa])
  else
    lblEmpresaDestinoInfo.Caption := 'Empresa: [No seleccionada]';

  ActualizarEstadoInterempresa;
end;

procedure TfrmMovimientoInternoEditor.ActualizarEstadoInterempresa;
var
  LOrig, LDest: TUbicacionItem;
begin
  LOrig := GetUbicacionSeleccionada(cboUbicacionOrigen);
  LDest := GetUbicacionSeleccionada(cboUbicacionDestino);

  if (not Assigned(LOrig)) or (not Assigned(LDest)) then
  begin
    pnlAlertaInterempresa.Color := 16314864;
    lblAlertaInterempresa.Font.Color := 32896;
    lblAlertaInterempresa.Caption := 'ℹ Seleccione ubicación de origen y destino para verificar el tipo de traspaso.';
    Exit;
  end;

  if (LOrig.Tipo = LDest.Tipo) and (LOrig.Id = LDest.Id) then
  begin
    pnlAlertaInterempresa.Color := $00E0E7FF;
    lblAlertaInterempresa.Font.Color := $001D4ED8;
    lblAlertaInterempresa.Caption := '⚠️ El origen y el destino no pueden ser exactamente la misma ubicación.';
    btnEjecutar.Enabled := False;
    Exit;
  end;

  btnEjecutar.Enabled := (mtLineas.RecordCount > 0);

  if LOrig.IdEmpresa = LDest.IdEmpresa then
  begin
    pnlAlertaInterempresa.Color := $00F0FDF4; // Verde suave
    lblAlertaInterempresa.Font.Color := $0015803D; // Verde oscuro
    lblAlertaInterempresa.Caption := Format('✔ Traspaso Interno en la misma empresa (%s). Movimiento puramente logístico de existencias.', [LOrig.NombreEmpresa]);
  end
  else
  begin
    pnlAlertaInterempresa.Color := $00FEF3C7; // Ámbar suave
    lblAlertaInterempresa.Font.Color := $00B45309; // Ámbar oscuro
    lblAlertaInterempresa.Caption := Format('⚠️ TRASPASO INTEREMPRESA (%s ➔ %s): Se generará automáticamente un Albarán de Venta en origen y un Albarán de Compra en destino a precio de coste.', [LOrig.NombreEmpresa, LDest.NombreEmpresa]);
  end;
end;

procedure TfrmMovimientoInternoEditor.btnBuscarArticuloClick(Sender: TObject);
var
  LUbic: TUbicacionItem;
  LAlmId: Integer;
  LFrmSelect: TfrmSelectProduct;
begin
  LUbic := GetUbicacionSeleccionada(cboUbicacionOrigen);
  if not Assigned(LUbic) then
  begin
    ShowMessage('Primero debe seleccionar la ubicación de origen.');
    Exit;
  end;

  LAlmId := AsegurarAlmacenParaUbicacion(LUbic);
  if LAlmId <= 0 then
  begin
    ShowMessage('La ubicación de origen no tiene un almacén físico configurado.');
    Exit;
  end;

  LFrmSelect := TfrmSelectProduct.Create(Self);
  try
    LFrmSelect.FilterAlmacenStockId := LAlmId;
    if LFrmSelect.ShowModal = mrOk then
    begin
      FSelectedArticuloId := StrToIntDef(LFrmSelect.SelectedId, 0);
      FSelectedArticuloCodigo := LFrmSelect.SelectedCode;
      FSelectedArticuloDesc := LFrmSelect.SelectedDesc;
      edtArticulo.Text := Format('[%s] %s', [FSelectedArticuloCodigo, FSelectedArticuloDesc]);
      CargarLotesArticuloSeleccionado;
    end;
  finally
    LFrmSelect.Free;
  end;
end;

procedure TfrmMovimientoInternoEditor.CargarLotesArticuloSeleccionado;
var
  LUbic: TUbicacionItem;
  LQry: TFDQuery;
  LAlmId: Integer;
  LItem: TStockProductoItem;
  LDisp: Double;
begin
  LimpiarComboObjetos(cboLoteStock);
  edtStockDisp.Text := '0.00';
  edtCantidad.Text := '1.00';
  edtPrecioCoste.Text := '0.00';

  if FSelectedArticuloId <= 0 then Exit;

  LUbic := GetUbicacionSeleccionada(cboUbicacionOrigen);
  if not Assigned(LUbic) then Exit;

  LAlmId := AsegurarAlmacenParaUbicacion(LUbic);
  if LAlmId <= 0 then Exit;

  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    LQry.SQL.Text :=
      'SELECT se.id, se.id_articulo, CAST(a.id AS CHAR) as art_codigo, a.descripcion as art_nombre, ' +
      '  COALESCE(a.pvp1, 0) as art_coste, COALESCE(ti.tipo, 10.0) as iva_porc, ' +
      '  se.id_lote, l.codigo as lote_codigo, se.id_formato_presentacion, ' +
      '  fp.descripcion as formato_nombre, ' +
      '  (se.cantidad_unidades - COALESCE(se.cantidad_reservada, 0)) as stock_disponible ' +
      'FROM ge_stock_envasado se ' +
      'JOIN ge_articulos a ON se.id_articulo = a.id ' +
      'LEFT JOIN ge_tipos_iva ti ON a.id_tipo_iva = ti.id ' +
      'LEFT JOIN ge_lotes l ON se.id_lote = l.id ' +
      'LEFT JOIN ge_formatos_presentacion fp ON se.id_formato_presentacion = fp.id ' +
      'WHERE se.id_articulo = :art AND se.id_almacen = :alm AND (se.cantidad_unidades - COALESCE(se.cantidad_reservada, 0)) > 0 ' +
      'ORDER BY l.codigo ASC, se.created_at ASC';
    LQry.ParamByName('art').AsInteger := FSelectedArticuloId;
    LQry.ParamByName('alm').AsInteger := LAlmId;
    LQry.Open;

    if LQry.IsEmpty then
    begin
      cboLoteStock.Items.Add('[Sin lotes con stock disponible en esta ubicación]');
      Exit;
    end;

    while not LQry.Eof do
    begin
      LDisp := LQry.FieldByName('stock_disponible').AsFloat;
      LItem := TStockProductoItem.Create;
      LItem.StockEnvasadoId := LQry.FieldByName('id').AsInteger;
      LItem.IdArticulo := LQry.FieldByName('id_articulo').AsInteger;
      LItem.CodigoArticulo := LQry.FieldByName('art_codigo').AsString;
      LItem.NombreArticulo := LQry.FieldByName('art_nombre').AsString;
      LItem.IdLote := LQry.FieldByName('id_lote').AsInteger;
      LItem.CodigoLote := LQry.FieldByName('lote_codigo').AsString;
      LItem.IdFormatoPresentacion := LQry.FieldByName('id_formato_presentacion').AsInteger;
      LItem.NombreFormato := LQry.FieldByName('formato_nombre').AsString;
      LItem.StockDisponible := LDisp;
      LItem.PrecioCoste := LQry.FieldByName('art_coste').AsFloat;
      LItem.TipoIvaPorcentaje := LQry.FieldByName('iva_porc').AsFloat;

      cboLoteStock.Items.AddObject(
        Format('Lote: %s | Formato: %s | Disp: %.2f uds | Coste: %.2f €',
          [LItem.CodigoLote, LItem.NombreFormato, LDisp, LItem.PrecioCoste]),
        LItem
      );
      LQry.Next;
    end;
  finally
    LQry.Free;
  end;

  if cboLoteStock.Items.Count > 0 then
  begin
    cboLoteStock.ItemIndex := 0;
    cboLoteStockChange(nil);
  end;
end;

procedure TfrmMovimientoInternoEditor.cboLoteStockChange(Sender: TObject);
var
  LItem: TStockProductoItem;
begin
  LItem := GetStockItemSeleccionado;
  if Assigned(LItem) then
  begin
    edtStockDisp.Text := FormatFloat('0.00', LItem.StockDisponible);
    edtPrecioCoste.Text := FormatFloat('0.00', LItem.PrecioCoste);
  end
  else
  begin
    edtStockDisp.Text := '0.00';
    edtPrecioCoste.Text := '0.00';
  end;
end;

procedure TfrmMovimientoInternoEditor.edtEscanerKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  if Key = VK_RETURN then
  begin
    Key := 0;
    ProcesarCodigoEscaneado(Trim(edtEscaner.Text));
  end;
end;

procedure TfrmMovimientoInternoEditor.btnEscanearClick(Sender: TObject);
begin
  ProcesarCodigoEscaneado(Trim(edtEscaner.Text));
end;

procedure TfrmMovimientoInternoEditor.ProcesarCodigoEscaneado(const ABarcode: string);
var
  LRaw: string;
  LUbic: TUbicacionItem;
  LAlmId: Integer;
  LScanned: TGS1BarcodeData;
  LQry: TFDQuery;
  LArtId: Integer;
  LBoxUnits: Double;
  LTargetLote: string;
  LLoteCodigoToMatch: string;
  LCantidadFinal: Double;
  I: Integer;
  LLoteMatched: Boolean;
  LStockItem: TStockProductoItem;
  LCode1, LCode2, LCode3: string;
begin
  LRaw := Trim(ABarcode);
  if LRaw = '' then Exit;

  LUbic := GetUbicacionSeleccionada(cboUbicacionOrigen);
  if not Assigned(LUbic) then
  begin
    ShowMessage('Primero debe seleccionar la ubicación de origen.');
    Exit;
  end;

  LAlmId := AsegurarAlmacenParaUbicacion(LUbic);
  if LAlmId <= 0 then
  begin
    ShowMessage('La ubicación de origen no tiene un almacén físico configurado.');
    Exit;
  end;

  // 1. Decodificar código mediante el parser GS1 / DUN-14 / EAN-13
  LScanned := TGS1BarcodeParser.Parse(LRaw);
  LArtId := 0;
  LBoxUnits := 0;
  LTargetLote := Trim(LScanned.Lote);
  LLoteCodigoToMatch := '';
  LCantidadFinal := 1.0;

  LCode1 := LRaw;
  LCode2 := LScanned.GTIN;
  LCode3 := LScanned.EAN13;
  if LCode2 = '' then LCode2 := LCode1;
  if LCode3 = '' then LCode3 := LCode1;

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

    // A. Buscar en ge_articulos_ean (Empaquetados secundarios DUN-14 / EAN con unidades por caja)
    LQry.SQL.Text :=
      'SELECT articulo_id, unidades FROM ge_articulos_ean ' +
      'WHERE ean = :c1 OR ean = :c2 OR ean = :c3 ' +
      'ORDER BY unidades DESC LIMIT 1';
    LQry.ParamByName('c1').AsString := LCode1;
    LQry.ParamByName('c2').AsString := LCode2;
    LQry.ParamByName('c3').AsString := LCode3;
    LQry.Open;

    if not LQry.IsEmpty then
    begin
      LArtId := LQry.FieldByName('articulo_id').AsInteger;
      LBoxUnits := LQry.FieldByName('unidades').AsFloat;
      if LBoxUnits > 1.0 then
        LCantidadFinal := LBoxUnits
      else if LScanned.Cantidad > 0 then
        LCantidadFinal := LScanned.Cantidad;
    end;

    // B. Si no se encontró, buscar en ge_articulos (EAN-13 o ID directo)
    if LArtId <= 0 then
    begin
      LQry.Close;
      LQry.SQL.Text :=
        'SELECT id FROM ge_articulos ' +
        'WHERE (ean = :c1 OR ean = :c2 OR ean = :c3 OR CAST(id AS CHAR) = :c1) AND activo = 1 LIMIT 1';
      LQry.ParamByName('c1').AsString := LCode1;
      LQry.ParamByName('c2').AsString := LCode2;
      LQry.ParamByName('c3').AsString := LCode3;
      LQry.Open;

      if not LQry.IsEmpty then
      begin
        LArtId := LQry.FieldByName('id').AsInteger;
        if LScanned.Cantidad > 0 then
          LCantidadFinal := LScanned.Cantidad;
      end;
    end;

    // C. Si no se encontró, buscar en ge_clientes_articulos (DUN-14 o EAN de cliente)
    if LArtId <= 0 then
    begin
      LQry.Close;
      LQry.SQL.Text :=
        'SELECT ca.id_articulo, ca.dun14, ca.ean13, ean.unidades ' +
        'FROM ge_clientes_articulos ca ' +
        'LEFT JOIN ge_articulos_ean ean ON ean.articulo_id = ca.id_articulo AND (ean.ean = ca.dun14 OR ean.ean = ca.ean13) ' +
        'WHERE (ca.dun14 = :c1 OR ca.dun14 = :c2 OR ca.dun14 = :c3 OR ' +
        '       ca.ean13 = :c1 OR ca.ean13 = :c2 OR ca.ean13 = :c3) ' +
        '  AND ca.activo = 1 LIMIT 1';
      LQry.ParamByName('c1').AsString := LCode1;
      LQry.ParamByName('c2').AsString := LCode2;
      LQry.ParamByName('c3').AsString := LCode3;
      LQry.Open;

      if not LQry.IsEmpty then
      begin
        LArtId := LQry.FieldByName('id_articulo').AsInteger;
        LBoxUnits := LQry.FieldByName('unidades').AsFloat;
        if LBoxUnits > 1.0 then
          LCantidadFinal := LBoxUnits
        else if LScanned.Cantidad > 0 then
          LCantidadFinal := LScanned.Cantidad;
      end;
    end;

    // D. Si aún no se encontró, verificar si el código escaneado es un NÚMERO DE LOTE existente en origen
    if LArtId <= 0 then
    begin
      LQry.Close;
      LQry.SQL.Text :=
        'SELECT se.id_articulo, l.codigo as lote_codigo ' +
        'FROM ge_stock_envasado se ' +
        'JOIN ge_lotes l ON se.id_lote = l.id ' +
        'WHERE se.id_almacen = :alm AND (l.codigo = :code OR l.codigo_antiguo = :code) ' +
        '  AND (se.cantidad_unidades - COALESCE(se.cantidad_reservada, 0)) > 0 ' +
        'ORDER BY se.created_at DESC LIMIT 1';
      LQry.ParamByName('alm').AsInteger := LAlmId;
      LQry.ParamByName('code').AsString := LRaw;
      LQry.Open;

      if not LQry.IsEmpty then
      begin
        LArtId := LQry.FieldByName('id_articulo').AsInteger;
        LLoteCodigoToMatch := LQry.FieldByName('lote_codigo').AsString;
        LTargetLote := LLoteCodigoToMatch;
        LCantidadFinal := 1.0;
      end;
    end;

    // E. Si después de todas las búsquedas no existe
    if LArtId <= 0 then
    begin
      ShowMessage(Format('No se ha encontrado ningún artículo asociado al código "%s".'#13#10 +
        'Verifique que esté configurado en Códigos de Barras, EAN-13, DUN-14 o Lotes.', [LRaw]));
      edtEscaner.SelectAll;
      Exit;
    end;

    // Obtener datos del artículo encontrado
    LQry.Close;
    LQry.SQL.Text := 'SELECT id, descripcion FROM ge_articulos WHERE id = :id';
    LQry.ParamByName('id').AsInteger := LArtId;
    LQry.Open;

    if LQry.IsEmpty then
    begin
      ShowMessage('Artículo no encontrado en la base de datos.');
      Exit;
    end;

    FSelectedArticuloId := LArtId;
    FSelectedArticuloCodigo := IntToStr(LArtId);
    FSelectedArticuloDesc := LQry.FieldByName('descripcion').AsString;
    edtArticulo.Text := Format('[%s] %s', [FSelectedArticuloCodigo, FSelectedArticuloDesc]);

    // Cargar los lotes con stock disponibles en el almacén origen
    CargarLotesArticuloSeleccionado;

    // Ajustar la cantidad
    if LCantidadFinal <= 0 then LCantidadFinal := 1.0;
    edtCantidad.Text := FormatFloat('0.00', LCantidadFinal);

    // Intentar seleccionar el lote extraído o encontrado
    LLoteMatched := False;
    if LTargetLote <> '' then
    begin
      for I := 0 to cboLoteStock.Items.Count - 1 do
      begin
        LStockItem := TStockProductoItem(cboLoteStock.Items.Objects[I]);
        if Assigned(LStockItem) and (SameText(Trim(LStockItem.CodigoLote), LTargetLote) or
           (Pos(LTargetLote, LStockItem.CodigoLote) > 0)) then
        begin
          cboLoteStock.ItemIndex := I;
          cboLoteStockChange(nil);
          LLoteMatched := True;
          Break;
        end;
      end;
    end;

    // Si solo hay un lote en stock y no venía lote en el código, tomarlo por defecto
    if (not LLoteMatched) and (cboLoteStock.Items.Count = 1) and Assigned(cboLoteStock.Items.Objects[0]) then
    begin
      cboLoteStock.ItemIndex := 0;
      cboLoteStockChange(nil);
      LLoteMatched := True;
    end;

    // Si se activa auto-agregar y se ha seleccionado un lote válido
    if chkAutoAdd.Checked and LLoteMatched and (GetStockItemSeleccionado <> nil) then
    begin
      btnAnadirLineaClick(nil);
      edtEscaner.Clear;
      edtEscaner.SetFocus;
    end
    else
    begin
      edtEscaner.Clear;
      if not LLoteMatched then
        cboLoteStock.SetFocus
      else
        btnAnadirLinea.SetFocus;
    end;

  finally
    LQry.Free;
  end;
end;

procedure TfrmMovimientoInternoEditor.btnAnadirLineaClick(Sender: TObject);
var
  LItem: TStockProductoItem;
  LCantidad: Double;
  LTotalYaEnLista: Double;
  LCoste: Double;
begin
  if FSelectedArticuloId <= 0 then
  begin
    ShowMessage('Por favor, busque y seleccione primero un artículo.');
    btnBuscarArticulo.SetFocus;
    Exit;
  end;

  LItem := GetStockItemSeleccionado;
  if not Assigned(LItem) then
  begin
    ShowMessage('Por favor, seleccione un lote disponible del artículo.');
    cboLoteStock.SetFocus;
    Exit;
  end;

  LCantidad := StrToFloatDef(StringReplace(Trim(edtCantidad.Text), ',', '.', [rfReplaceAll]), 0);
  if LCantidad <= 0 then
  begin
    ShowMessage('La cantidad a traspasar debe ser mayor que cero.');
    edtCantidad.SetFocus;
    Exit;
  end;

  LCoste := StrToFloatDef(StringReplace(Trim(edtPrecioCoste.Text), ',', '.', [rfReplaceAll]), LItem.PrecioCoste);

  // Comprobar cantidad acumulada en caso de que ya se haya agregado previamente el mismo lote
  LTotalYaEnLista := 0;
  if mtLineas.Locate('id_stock_envasado', LItem.StockEnvasadoId, []) then
  begin
    LTotalYaEnLista := mtLineas.FieldByName('cantidad').AsFloat;
  end;

  if (LTotalYaEnLista + LCantidad) > LItem.StockDisponible then
  begin
    ShowMessage(Format(
      'La cantidad indicada (%.2f + en lista %.2f = %.2f) supera el stock disponible en origen (%.2f).',
      [LCantidad, LTotalYaEnLista, LCantidad + LTotalYaEnLista, LItem.StockDisponible]
    ));
    edtCantidad.SetFocus;
    Exit;
  end;

  if mtLineas.Locate('id_stock_envasado', LItem.StockEnvasadoId, []) then
  begin
    mtLineas.Edit;
    mtLineas.FieldByName('cantidad').AsFloat := LTotalYaEnLista + LCantidad;
    mtLineas.FieldByName('precio_coste').AsFloat := LCoste;
    mtLineas.FieldByName('subtotal').AsFloat := (LTotalYaEnLista + LCantidad) * LCoste;
    mtLineas.Post;
  end
  else
  begin
    mtLineas.Append;
    mtLineas.FieldByName('id_stock_envasado').AsInteger := LItem.StockEnvasadoId;
    mtLineas.FieldByName('id_articulo').AsInteger := LItem.IdArticulo;
    mtLineas.FieldByName('codigo_articulo').AsString := LItem.CodigoArticulo;
    mtLineas.FieldByName('descripcion_articulo').AsString := LItem.NombreArticulo;
    mtLineas.FieldByName('id_lote').AsInteger := LItem.IdLote;
    mtLineas.FieldByName('codigo_lote').AsString := LItem.CodigoLote;
    mtLineas.FieldByName('id_formato').AsInteger := LItem.IdFormatoPresentacion;
    mtLineas.FieldByName('formato_nombre').AsString := LItem.NombreFormato;
    mtLineas.FieldByName('stock_disponible').AsFloat := LItem.StockDisponible;
    mtLineas.FieldByName('cantidad').AsFloat := LCantidad;
    mtLineas.FieldByName('precio_coste').AsFloat := LCoste;
    mtLineas.FieldByName('tipo_iva').AsFloat := LItem.TipoIvaPorcentaje;
    mtLineas.FieldByName('subtotal').AsFloat := LCantidad * LCoste;
    mtLineas.Post;
  end;

  ActualizarTotalesLineas;
  LimpiarSeleccionArticulo;
  edtEscaner.SetFocus;
end;

procedure TfrmMovimientoInternoEditor.btnEliminarLineaClick(Sender: TObject);
begin
  if (mtLineas.Active) and (not mtLineas.IsEmpty) then
  begin
    mtLineas.Delete;
    ActualizarTotalesLineas;
  end;
end;

function TfrmMovimientoInternoEditor.AsegurarAlmacenParaUbicacion(AUbicacion: TUbicacionItem): Integer;
var
  LQry: TFDQuery;
  LUserId: Integer;
begin
  Result := 0;
  if not Assigned(AUbicacion) then Exit;

  if AUbicacion.Tipo = 'ALMACEN' then
  begin
    Result := AUbicacion.Id;
    Exit;
  end;

  if (AUbicacion.Tipo = 'TIENDA') and (AUbicacion.IdAlmacen > 0) then
  begin
    Result := AUbicacion.IdAlmacen;
    Exit;
  end;

  // Si es Tienda sin almacén o es Obrador, buscar o crear almacén correspondiente
  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;

    // Buscar si ya existe un almacén con el nombre de la tienda u obrador en esa empresa
    LQry.SQL.Text :=
      'SELECT id FROM ge_almacenes WHERE id_empresa = :emp AND descripcion LIKE :nom AND activo = 1 LIMIT 1';
    LQry.ParamByName('emp').AsInteger := AUbicacion.IdEmpresa;
    LQry.ParamByName('nom').AsString := '%' + Trim(AUbicacion.Nombre) + '%';
    LQry.Open;

    if not LQry.IsEmpty then
    begin
      Result := LQry.FieldByName('id').AsInteger;
      AUbicacion.IdAlmacen := Result;
      // Si era tienda, vincularlo en ge_tiendas
      if AUbicacion.Tipo = 'TIENDA' then
      begin
        LQry.Close;
        LQry.SQL.Text := 'UPDATE ge_tiendas SET id_almacen = :alm WHERE id = :id';
        LQry.ParamByName('alm').AsInteger := Result;
        LQry.ParamByName('id').AsInteger := AUbicacion.Id;
        LQry.ExecSQL;
      end;
      Exit;
    end;

    // Si no existe, crear uno nuevo
    LUserId := StrToIntDef(dmgMain.CurrentUserId, 1);
    LQry.Close;
    LQry.SQL.Text :=
      'INSERT INTO ge_almacenes (id_empresa, descripcion, activo, compartido, created_at, id_user_creator) ' +
      'VALUES (:emp, :desc, 1, 0, NOW(), :usr)';
    LQry.ParamByName('emp').AsInteger := AUbicacion.IdEmpresa;
    LQry.ParamByName('desc').AsString := 'Almacén ' + AUbicacion.Tipo + ' ' + AUbicacion.Nombre;
    LQry.ParamByName('usr').AsInteger := LUserId;
    LQry.ExecSQL;

    LQry.SQL.Text := 'SELECT LAST_INSERT_ID() as new_id';
    LQry.Open;
    Result := LQry.FieldByName('new_id').AsInteger;
    AUbicacion.IdAlmacen := Result;

    if AUbicacion.Tipo = 'TIENDA' then
    begin
      LQry.Close;
      LQry.SQL.Text := 'UPDATE ge_tiendas SET id_almacen = :alm WHERE id = :id';
      LQry.ParamByName('alm').AsInteger := Result;
      LQry.ParamByName('id').AsInteger := AUbicacion.Id;
      LQry.ExecSQL;
    end;

  finally
    LQry.Free;
  end;
end;

function TfrmMovimientoInternoEditor.ObtenerOCrearClienteEmpresa(AEmpresaDestinoId: Integer; const ACifDestino, ANombreDestino: string): Integer;
var
  LQry: TFDQuery;
  LUserId: Integer;
begin
  Result := 0;
  LUserId := StrToIntDef(dmgMain.CurrentUserId, 1);
  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;

    // Buscar por NIF
    if ACifDestino <> '' then
    begin
      LQry.SQL.Text := 'SELECT id FROM ge_clientes WHERE nif = :cif LIMIT 1';
      LQry.ParamByName('cif').AsString := ACifDestino;
      LQry.Open;
      if not LQry.IsEmpty then
      begin
        Result := LQry.FieldByName('id').AsInteger;
        Exit;
      end;
    end;

    // Buscar por nombre fiscal
    LQry.Close;
    LQry.SQL.Text := 'SELECT id FROM ge_clientes WHERE nombre_fiscal LIKE :nom LIMIT 1';
    LQry.ParamByName('nom').AsString := '%' + Trim(ANombreDestino) + '%';
    LQry.Open;
    if not LQry.IsEmpty then
    begin
      Result := LQry.FieldByName('id').AsInteger;
      Exit;
    end;

    // Si no existe, dar de alta el cliente automáticamente
    LQry.Close;
    LQry.SQL.Text :=
      'INSERT INTO ge_clientes (nif, nombre_fiscal, nombre_comercial, activo, created_at, id_user_creator) ' +
      'VALUES (:nif, :nom, :com, 1, NOW(), :usr)';
    LQry.ParamByName('nif').AsString := ACifDestino;
    LQry.ParamByName('nom').AsString := ANombreDestino;
    LQry.ParamByName('com').AsString := ANombreDestino;
    LQry.ParamByName('usr').AsInteger := LUserId;
    LQry.ExecSQL;

    LQry.SQL.Text := 'SELECT LAST_INSERT_ID() as new_id';
    LQry.Open;
    Result := LQry.FieldByName('new_id').AsInteger;
  finally
    LQry.Free;
  end;
end;

function TfrmMovimientoInternoEditor.ObtenerOCrearProveedorEmpresa(AEmpresaOrigenId: Integer; const ACifOrigen, ANombreOrigen: string): Integer;
var
  LQry: TFDQuery;
  LUserId: Integer;
begin
  Result := 0;
  LUserId := StrToIntDef(dmgMain.CurrentUserId, 1);
  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;

    // Buscar por NIF
    if ACifOrigen <> '' then
    begin
      LQry.SQL.Text := 'SELECT id FROM ge_proveedores WHERE nif = :cif LIMIT 1';
      LQry.ParamByName('cif').AsString := ACifOrigen;
      LQry.Open;
      if not LQry.IsEmpty then
      begin
        Result := LQry.FieldByName('id').AsInteger;
        Exit;
      end;
    end;

    // Buscar por nombre
    LQry.Close;
    LQry.SQL.Text := 'SELECT id FROM ge_proveedores WHERE nombre LIKE :nom LIMIT 1';
    LQry.ParamByName('nom').AsString := '%' + Trim(ANombreOrigen) + '%';
    LQry.Open;
    if not LQry.IsEmpty then
    begin
      Result := LQry.FieldByName('id').AsInteger;
      Exit;
    end;

    // Si no existe, dar de alta el proveedor automáticamente
    LQry.Close;
    LQry.SQL.Text :=
      'INSERT INTO ge_proveedores (nif, nombre, activo, tipo, created_at, id_user_creator) ' +
      'VALUES (:nif, :nom, 1, ''MATERIA_PRIMA'', NOW(), :usr)';
    LQry.ParamByName('nif').AsString := ACifOrigen;
    LQry.ParamByName('nom').AsString := ANombreOrigen;
    LQry.ParamByName('usr').AsInteger := LUserId;
    LQry.ExecSQL;

    LQry.SQL.Text := 'SELECT LAST_INSERT_ID() as new_id';
    LQry.Open;
    Result := LQry.FieldByName('new_id').AsInteger;
  finally
    LQry.Free;
  end;
end;

procedure TfrmMovimientoInternoEditor.btnEjecutarClick(Sender: TObject);
var
  LOrig, LDest: TUbicacionItem;
  LCantidad, LPrecioCoste, LBase, LIva, LTotal: Double;
  LTotalBase, LTotalIva, LTotalGeneral: Double;
  LAlmOrigenId, LAlmDestinoId, LUserId: Integer;
  LQry, LQryAux: TFDQuery;
  LExistingDestStockId: Integer;
  LEsInterempresa: Boolean;
  LAlbaranVentaId, LAlbaranCompraId: Integer;
  LClienteId, LProveedorId: Integer;
  LSerieVenta: string;
  LNumVenta: Integer;
  LDocCompraStr: string;
  LCountArticulos: Integer;
begin
  if (not mtLineas.Active) or (mtLineas.RecordCount = 0) then
  begin
    ShowMessage('Debe añadir al menos un artículo a la lista antes de realizar el traspaso.');
    ModalResult := mrNone;
    Exit;
  end;

  LOrig := GetUbicacionSeleccionada(cboUbicacionOrigen);
  LDest := GetUbicacionSeleccionada(cboUbicacionDestino);

  if (not Assigned(LOrig)) or (not Assigned(LDest)) then
  begin
    ShowMessage('Por favor, seleccione las ubicaciones de origen y destino.');
    ModalResult := mrNone;
    Exit;
  end;

  if (LOrig.Tipo = LDest.Tipo) and (LOrig.Id = LDest.Id) then
  begin
    ShowMessage('El origen y el destino no pueden ser la misma ubicación.');
    ModalResult := mrNone;
    Exit;
  end;

  LAlmOrigenId := AsegurarAlmacenParaUbicacion(LOrig);
  LAlmDestinoId := AsegurarAlmacenParaUbicacion(LDest);

  if (LAlmOrigenId <= 0) or (LAlmDestinoId <= 0) then
  begin
    ShowMessage('No se pudo resolver el almacén físico de origen o destino.');
    ModalResult := mrNone;
    Exit;
  end;

  LEsInterempresa := (LOrig.IdEmpresa <> LDest.IdEmpresa);
  LAlbaranVentaId := 0;
  LAlbaranCompraId := 0;
  LUserId := StrToIntDef(dmgMain.CurrentUserId, 1);
  LSerieVenta := '';
  LNumVenta := 0;
  LDocCompraStr := '';
  LCountArticulos := mtLineas.RecordCount;

  // Iniciar Transacción ACID
  dmgMain.dbConn.StartTransaction;
  LQry := TFDQuery.Create(nil);
  LQryAux := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    LQryAux.Connection := dmgMain.dbConn;

    // A. SI ES INTEREMPRESA: Crear cabecera única de Albarán de Venta y Albarán de Compra
    if LEsInterempresa then
    begin
      // Calcular suma total de bases e IVAs
      LTotalBase := 0;
      LTotalIva := 0;
      mtLineas.DisableControls;
      try
        mtLineas.First;
        while not mtLineas.Eof do
        begin
          LBase := mtLineas.FieldByName('cantidad').AsFloat * mtLineas.FieldByName('precio_coste').AsFloat;
          LIva := LBase * (mtLineas.FieldByName('tipo_iva').AsFloat / 100.0);
          LTotalBase := LTotalBase + LBase;
          LTotalIva := LTotalIva + LIva;
          mtLineas.Next;
        end;
      finally
        mtLineas.EnableControls;
      end;
      LTotalGeneral := LTotalBase + LTotalIva;

      // 1. ALBARÁN DE VENTA (Empresa Origen)
      LClienteId := ObtenerOCrearClienteEmpresa(LDest.IdEmpresa, LDest.CifEmpresa, LDest.NombreEmpresa);
      if not dmgMain.GetSerieYNumeroDocumento(IntToStr(LOrig.IdEmpresa), 'ALBARAN', Date, '', LSerieVenta, LNumVenta) then
      begin
        LSerieVenta := 'A';
        LNumVenta := 1;
      end;

      LQry.Close;
      LQry.SQL.Text :=
        'INSERT INTO ge_albaranes (empresa_id, cliente_id, serie, numero, fecha, base_imponible, iva, total, ' +
        '  referencia, observaciones1, created_at, id_user_creator) ' +
        'VALUES (:emp, :cli, :ser, :num, NOW(), :base, :iva, :tot, :ref, :obs, NOW(), :usr)';
      LQry.ParamByName('emp').AsInteger := LOrig.IdEmpresa;
      LQry.ParamByName('cli').AsInteger := LClienteId;
      LQry.ParamByName('ser').AsString := LSerieVenta;
      LQry.ParamByName('num').AsInteger := LNumVenta;
      LQry.ParamByName('base').AsFloat := LTotalBase;
      LQry.ParamByName('iva').AsFloat := LTotalIva;
      LQry.ParamByName('tot').AsFloat := LTotalGeneral;
      LQry.ParamByName('ref').AsString := 'Traspaso a ' + LDest.Nombre;
      LQry.ParamByName('obs').AsString := 'Albarán generado automáticamente por traspaso interno interempresa.';
      LQry.ParamByName('usr').AsInteger := LUserId;
      LQry.ExecSQL;

      LQry.SQL.Text := 'SELECT LAST_INSERT_ID() as new_id';
      LQry.Open;
      LAlbaranVentaId := LQry.FieldByName('new_id').AsInteger;

      // Incrementar contador de albarán en la serie
      LQry.Close;
      LQry.SQL.Text :=
        'UPDATE ge_series SET sig_numero_albaran = :nextnum + 1 ' +
        'WHERE empresa_id = :emp AND serie = :ser AND activo = 1';
      LQry.ParamByName('nextnum').AsInteger := LNumVenta;
      LQry.ParamByName('emp').AsInteger := LOrig.IdEmpresa;
      LQry.ParamByName('ser').AsString := LSerieVenta;
      LQry.ExecSQL;

      // 2. ALBARÁN DE COMPRA (Empresa Destino)
      LProveedorId := ObtenerOCrearProveedorEmpresa(LOrig.IdEmpresa, LOrig.CifEmpresa, LOrig.NombreEmpresa);
      LDocCompraStr := Format('%s-%d', [LSerieVenta, LNumVenta]);

      LQry.Close;
      LQry.SQL.Text :=
        'INSERT INTO ge_prov_albaranes (idempresa, idproveedor, fecha, documento, base, totaliva, totalentrada, ' +
        '  created_at, id_user_creator) ' +
        'VALUES (:emp, :prv, :fec, :doc, :base, :iva, :tot, NOW(), :usr)';
      LQry.ParamByName('emp').AsInteger := LDest.IdEmpresa;
      LQry.ParamByName('prv').AsInteger := LProveedorId;
      LQry.ParamByName('fec').AsDate := Date;
      LQry.ParamByName('doc').AsString := LDocCompraStr;
      LQry.ParamByName('base').AsFloat := LTotalBase;
      LQry.ParamByName('iva').AsFloat := LTotalIva;
      LQry.ParamByName('tot').AsFloat := LTotalGeneral;
      LQry.ParamByName('usr').AsInteger := LUserId;
      LQry.ExecSQL;

      LQry.SQL.Text := 'SELECT LAST_INSERT_ID() as new_id';
      LQry.Open;
      LAlbaranCompraId := LQry.FieldByName('new_id').AsInteger;
    end;

    // B. Procesar cada una de las líneas agregadas
    mtLineas.DisableControls;
    try
      mtLineas.First;
      while not mtLineas.Eof do
      begin
        LCantidad := mtLineas.FieldByName('cantidad').AsFloat;
        LPrecioCoste := mtLineas.FieldByName('precio_coste').AsFloat;
        LBase := LCantidad * LPrecioCoste;
        LIva := LBase * (mtLineas.FieldByName('tipo_iva').AsFloat / 100.0);
        LTotal := LBase + LIva;

        // 1. DESCONTAR STOCK EN ORIGEN
        LQry.Close;
        LQry.SQL.Text :=
          'UPDATE ge_stock_envasado ' +
          'SET cantidad_unidades = cantidad_unidades - :cant, updated_at = NOW(), id_user_update = :usr ' +
          'WHERE id = :id';
        LQry.ParamByName('cant').AsFloat := LCantidad;
        LQry.ParamByName('usr').AsInteger := LUserId;
        LQry.ParamByName('id').AsInteger := mtLineas.FieldByName('id_stock_envasado').AsInteger;
        LQry.ExecSQL;

        // 2. INCREMENTAR O INSERTAR STOCK EN DESTINO
        LQry.Close;
        LQry.SQL.Text :=
          'SELECT id FROM ge_stock_envasado ' +
          'WHERE id_almacen = :alm AND id_articulo = :art AND id_lote <=> :lot AND id_formato_presentacion <=> :fmt ' +
          'LIMIT 1';
        LQry.ParamByName('alm').AsInteger := LAlmDestinoId;
        LQry.ParamByName('art').AsInteger := mtLineas.FieldByName('id_articulo').AsInteger;
        if mtLineas.FieldByName('id_lote').AsInteger > 0 then
          LQry.ParamByName('lot').AsInteger := mtLineas.FieldByName('id_lote').AsInteger
        else
          LQry.ParamByName('lot').Clear;

        if mtLineas.FieldByName('id_formato').AsInteger > 0 then
          LQry.ParamByName('fmt').AsInteger := mtLineas.FieldByName('id_formato').AsInteger
        else
          LQry.ParamByName('fmt').Clear;

        LQry.Open;

        if not LQry.IsEmpty then
        begin
          LExistingDestStockId := LQry.FieldByName('id').AsInteger;
          LQry.Close;
          LQry.SQL.Text :=
            'UPDATE ge_stock_envasado ' +
            'SET cantidad_unidades = cantidad_unidades + :cant, updated_at = NOW(), id_user_update = :usr ' +
            'WHERE id = :id';
          LQry.ParamByName('cant').AsFloat := LCantidad;
          LQry.ParamByName('usr').AsInteger := LUserId;
          LQry.ParamByName('id').AsInteger := LExistingDestStockId;
          LQry.ExecSQL;
        end
        else
        begin
          LQry.Close;
          LQry.SQL.Text :=
            'INSERT INTO ge_stock_envasado (id_almacen, id_articulo, id_lote, id_formato_presentacion, ' +
            '  cantidad_unidades, cantidad_producida, created_at, id_user_creator) ' +
            'VALUES (:alm, :art, :lot, :fmt, :cant, :cant, NOW(), :usr)';
          LQry.ParamByName('alm').AsInteger := LAlmDestinoId;
          LQry.ParamByName('art').AsInteger := mtLineas.FieldByName('id_articulo').AsInteger;
          if mtLineas.FieldByName('id_lote').AsInteger > 0 then
            LQry.ParamByName('lot').AsInteger := mtLineas.FieldByName('id_lote').AsInteger
          else
            LQry.ParamByName('lot').Clear;

          if mtLineas.FieldByName('id_formato').AsInteger > 0 then
            LQry.ParamByName('fmt').AsInteger := mtLineas.FieldByName('id_formato').AsInteger
          else
            LQry.ParamByName('fmt').Clear;

          LQry.ParamByName('cant').AsFloat := LCantidad;
          LQry.ParamByName('usr').AsInteger := LUserId;
          LQry.ExecSQL;
        end;

        // 3. LÍNEAS DE ALBARÁN DE VENTA Y COMPRA SI ES INTEREMPRESA
        if LEsInterempresa then
        begin
          // Línea de albarán de venta
          LQry.Close;
          LQry.SQL.Text :=
            'INSERT INTO ge_albaranes_lineas (albaran_id, articulo_id, articulo_codigo, descripcion, lotes, ' +
            '  cantidad, precio, base, tipo_iva, iva, total, created_at, id_user_creator) ' +
            'VALUES (:alb, :art, :cod, :desc, :lot, :cant, :prc, :base, :tiva, :iva, :tot, NOW(), :usr)';
          LQry.ParamByName('alb').AsInteger := LAlbaranVentaId;
          LQry.ParamByName('art').AsInteger := mtLineas.FieldByName('id_articulo').AsInteger;
          LQry.ParamByName('cod').AsString := mtLineas.FieldByName('codigo_articulo').AsString;
          LQry.ParamByName('desc').AsString := mtLineas.FieldByName('descripcion_articulo').AsString;
          LQry.ParamByName('lot').AsString := mtLineas.FieldByName('codigo_lote').AsString;
          LQry.ParamByName('cant').AsFloat := LCantidad;
          LQry.ParamByName('prc').AsFloat := LPrecioCoste;
          LQry.ParamByName('base').AsFloat := LBase;
          LQry.ParamByName('tiva').AsFloat := mtLineas.FieldByName('tipo_iva').AsFloat;
          LQry.ParamByName('iva').AsFloat := LIva;
          LQry.ParamByName('tot').AsFloat := LTotal;
          LQry.ParamByName('usr').AsInteger := LUserId;
          LQry.ExecSQL;

          // Detalle de albarán de compra
          LQry.Close;
          LQry.SQL.Text :=
            'INSERT INTO ge_prov_albaranes_detalle (idempresa, idalbaran, idproveedor, codigo, id_propio, ' +
            '  descripcion, lote, cantidad, precio, base, total, validado, created_at, id_user_creator) ' +
            'VALUES (:emp, :alb, :prv, :cod, :propio, :desc, :lot, :cant, :prc, :base, :tot, 1, NOW(), :usr)';
          LQry.ParamByName('emp').AsInteger := LDest.IdEmpresa;
          LQry.ParamByName('alb').AsInteger := LAlbaranCompraId;
          LQry.ParamByName('prv').AsInteger := LProveedorId;
          LQry.ParamByName('cod').AsString := mtLineas.FieldByName('codigo_articulo').AsString;
          LQry.ParamByName('propio').AsInteger := mtLineas.FieldByName('id_articulo').AsInteger;
          LQry.ParamByName('desc').AsString := mtLineas.FieldByName('descripcion_articulo').AsString;
          LQry.ParamByName('lot').AsString := mtLineas.FieldByName('codigo_lote').AsString;
          LQry.ParamByName('cant').AsFloat := LCantidad;
          LQry.ParamByName('prc').AsFloat := LPrecioCoste;
          LQry.ParamByName('base').AsFloat := LBase;
          LQry.ParamByName('tot').AsFloat := LTotal;
          LQry.ParamByName('usr').AsInteger := LUserId;
          LQry.ExecSQL;
        end;

        // 4. REGISTRAR EL MOVIMIENTO INTERNO EN ge_movimientos_internos
        LQry.Close;
        LQry.SQL.Text :=
          'INSERT INTO ge_movimientos_internos (fecha, tipo_origen, id_origen, id_almacen_origen, id_empresa_origen, ' +
          '  tipo_destino, id_destino, id_almacen_destino, id_empresa_destino, id_articulo, id_lote, ' +
          '  id_formato_presentacion, cantidad, precio_unitario, es_interempresa, id_albaran_venta, ' +
          '  id_albaran_compra, observaciones, created_at, id_user_creator) ' +
          'VALUES (NOW(), :torigen, :idorigen, :almorigen, :emporigen, :tdestino, :iddestino, :almdestino, ' +
          '  :empdestino, :art, :lote, :fmt, :cant, :prc, :inter, :albventa, :albcompra, :obs, NOW(), :usr)';
        LQry.ParamByName('torigen').AsString := LOrig.Tipo;
        LQry.ParamByName('idorigen').AsInteger := LOrig.Id;
        LQry.ParamByName('almorigen').AsInteger := LAlmOrigenId;
        LQry.ParamByName('emporigen').AsInteger := LOrig.IdEmpresa;
        LQry.ParamByName('tdestino').AsString := LDest.Tipo;
        LQry.ParamByName('iddestino').AsInteger := LDest.Id;
        LQry.ParamByName('almdestino').AsInteger := LAlmDestinoId;
        LQry.ParamByName('empdestino').AsInteger := LDest.IdEmpresa;
        LQry.ParamByName('art').AsInteger := mtLineas.FieldByName('id_articulo').AsInteger;

        if mtLineas.FieldByName('id_lote').AsInteger > 0 then
          LQry.ParamByName('lote').AsInteger := mtLineas.FieldByName('id_lote').AsInteger
        else
          LQry.ParamByName('lote').Clear;

        if mtLineas.FieldByName('id_formato').AsInteger > 0 then
          LQry.ParamByName('fmt').AsInteger := mtLineas.FieldByName('id_formato').AsInteger
        else
          LQry.ParamByName('fmt').Clear;

        LQry.ParamByName('cant').AsFloat := LCantidad;
        LQry.ParamByName('prc').AsFloat := LPrecioCoste;

        if LEsInterempresa then
          LQry.ParamByName('inter').AsInteger := 1
        else
          LQry.ParamByName('inter').AsInteger := 0;

        if LAlbaranVentaId > 0 then
          LQry.ParamByName('albventa').AsInteger := LAlbaranVentaId
        else
          LQry.ParamByName('albventa').Clear;

        if LAlbaranCompraId > 0 then
          LQry.ParamByName('albcompra').AsInteger := LAlbaranCompraId
        else
          LQry.ParamByName('albcompra').Clear;

        LQry.ParamByName('obs').AsString := Trim(edtObs.Text);
        LQry.ParamByName('usr').AsInteger := LUserId;
        LQry.ExecSQL;

        mtLineas.Next;
      end;
    finally
      mtLineas.EnableControls;
    end;

    // Confirmar transacción ACID
    dmgMain.dbConn.Commit;

    if LEsInterempresa then
      ShowMessage(Format('Traspaso interempresa completado con éxito (%d artículos transferidos).'#13#10 +
        '• Stock actualizado en origen y destino.'#13#10 +
        '• Albarán de Venta generado en %s: %s-%d'#13#10 +
        '• Albarán de Compra generado en %s: %s',
        [LCountArticulos, LOrig.NombreEmpresa, LSerieVenta, LNumVenta, LDest.NombreEmpresa, LDocCompraStr]))
    else
      ShowMessage(Format('Traspaso interno completado con éxito (%d artículos transferidos).'#13#10 +
        '• Stock actualizado en las ubicaciones de origen y destino.', [LCountArticulos]));

    ModalResult := mrOk;

  except
    on E: Exception do
    begin
      dmgMain.dbConn.Rollback;
      ModalResult := mrNone;
      TDbErrorHandler.HandleException(E, 'Error al ejecutar el traspaso de stock');
    end;
  end;

  LQry.Free;
  LQryAux.Free;
end;

end.
