Presupuesto contra real
Manual paso a paso
Las capturas muestran Q-Expert en inglés; la interfaz en español llegará pronto.
01La pregunta de negocio
Cada sucursal tiene un presupuesto mensual. La gerencia quiere ver, por sucursal y mes, el presupuesto contra lo que de verdad se vendió, neto de devoluciones. El presupuesto llega como una hoja con una columna por mes.
02Antes de empezar
- Descomprime el paquete del episodio:
Returns.csv,Budget.csv, los archivos de los episodios anteriores ystart.qep(el final del episodio 4). Agrega los dos archivos nuevos a la Library. - Importa
start.qepcon Import QEP y luego borra el CHECKPOINT de depuración del episodio 4 (el que guardaSales_Check.qvdy se detiene). Conserva el checkpoint que guardaSales.qvd. - Abre
Budget.csven Q-Explorator, la herramienta de escritorio del equipo de Q-Fábrica, para ver la forma ancha antes de que el CROSSTABLE la convierta en filas.
03Paso a paso
-
1
Carga las devoluciones como preceding load
Dos LOAD sobre una sola tabla:
- Arrastra
Returns.csval canvas: este RAW es el LOAD interno, conDate(Date#(ReturnDate, 'YYYY-MM-DD'))yQuantityrenombradoReturnedQty. - Conéctalo a un TXF
SaleReturny marca Preceding load en la conexión. - En las Expressions del TXF:
If(Match(Reason, 'DAMAGED', 'EXPIRED'), 'Quality', 'Commercial')comoReasonType.

Figura 5.1 — Carga las devoluciones como preceding load - Arrastra
-
2
Dale a cada devolución los datos de su venta
Un INNER JOIN en
SaleReturndesdeSales, que trae:SaleID, CustomerID, ProductID, BranchID, SalespersonID, ChannelName, CurrencyNetAmountCOP / QuantitycomoUnitNetCOP

Figura 5.2 — Dale a cada devolución los datos de su venta -
3
Una tabla de movimientos, neta de devoluciones
Tres nodos:
- Un TXF
Movement(NoConcatenate) leeSales, conSaleDatecomoMovementDatey'Sale'comoMovementType. - Un CONCATENATE agrega
SaleReturn, conReturnDatecomoMovementDate,'Return',-ReturnedQtycomoQuantityyRound(-ReturnedQty * UnitNetCOP)comoNetAmountCOP. - Un CHECKPOINT lo guarda en
$(vTxfPath)Movement.qvd.

Figura 5.3 — Una tabla de movimientos, neta de devoluciones - Un TXF
-
4
Convierte las columnas de meses del presupuesto en filas
Arrastra
Budget.csval canvas y conéctalo a un nodo CROSSTABLEtmp_Budget:- Campo de atributo
BudgetMonthName - Campo de datos
BudgetAmount - 1 columna calificadora (
BranchID)

Figura 5.4 — Convierte las columnas de meses del presupuesto en filas - Campo de atributo
-
5
Presupuesto y real lado a lado
Dos pasos:
- Un TXF
BudgetVsActualleetmp_BudgetconMatch(BudgetMonthName, 'Jan', 'Feb', 'Mar')comoMonthNum; luego un DROP detmp_Budget. - Un OUTER JOIN trae desde
MovementNum(Month(MovementDate))comoMonthNumySum(NetAmountCOP)comoActualAmountCOP, con GROUP BYBranchID, Num(Month(MovementDate)).

Figura 5.5 — Presupuesto y real lado a lado - Un TXF
-
6
Detente y revisa la comparación
Un CHECKPOINT de depuración después de
BudgetVsActualguarda$(vTxfPath)BudgetVsActual.qvdcon Exit Script marcado.
Figura 5.6 — Detente y revisa la comparación -
7
Genera el script
Generate Syntax. La última pestaña termina con
BudgetVsActual, elOUTER JOINcon suGROUP BY, elDROP TABLE [tmp_Budget]y elSTOREyEXIT SCRIPTdel checkpoint. ElSTOREde los movimientos va antes, justo después deMovement.
Figura 5.7 — Genera el script
04Revisa tu resultado
- Ejecuta el script en Qlik. Se detiene en el checkpoint, con
Transform/Movement.qvdyTransform/BudgetVsActual.qvdescritos. Movementtiene 319 filas: 298 ventas y 21 devoluciones (3 de las 24 devoluciones son de ventas que nunca se cargaron).BudgetVsActualtiene 24 filas, 8 sucursales por 3 meses, cada una conBudgetAmountyActualAmountCOP.- Q-Expert divide el script en
_Main, la única parte que pegas en Qlik, y un archivo.qvspor pestaña que_Mainlee conMUST_INCLUDE. Todos deben coincidir con los de abajo, salvo la fecha del encabezado y una línea// Q-Expert · Ref …con tu propia referencia.
_Main
Se pega en QlikEste es el único script que pegas en Qlik Sense: abre el Editor de carga de datos de la app, reemplaza su contenido por este y recarga. Cada línea $(MUST_INCLUDE=…) lee en su lugar uno de los archivos de abajo, así el resto del modelo nunca entra al editor.
Ver script · 47 líneas
/*---------------------------------------------------------------------------------------- Description [MAIN]: ------------------------------------------------------------------------------------- Author: The Q-Fábrica team - Date: [2026-11-02] ------------------------------------------------------------------------------------- Changes ------------------------------------------------------------------------------------- Author Date Detail ------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------- Copyright © Q-Expert Distributors. Q-Expert Qlik Developer Framework. www.qfabrica.com ----------------------------------------------------------------------------------------*/ SET ThousandSep=','; SET DecimalSep='.'; SET MoneyThousandSep=','; SET MoneyDecimalSep='.'; SET MoneyFormat='$ #,##0.00;-$ #,##0.00'; SET TimeFormat='hh:mm:ss'; SET DateFormat='YYYY-MM-DD'; SET TimestampFormat='YYYY-MM-DD hh:mm:ss'; SET MonthNames='Jan;Feb;Mar;Apr;May;Jun;Jul;Aug;Sep;Oct;Nov;Dec'; SET LongMonthNames='January;February;March;April;May;June;July;August;September;October;November;December'; SET DayNames='Mon;Tue;Wed;Thu;Fri;Sat;Sun'; SET LongDayNames='Monday;Tuesday;Wednesday;Thursday;Friday;Saturday;Sunday'; SET FirstWeekDay=0; SET BrokenWeeks=1; SET ReferenceDay=4; SET FirstMonthOfYear=1; LET vExecutionDate = Timestamp(Now(), 'YYYY-MM-DD hh:mm:ss'); SET vTxfPath = 'lib://QXDistributors/Transform/'; SET vPathRaw = 'lib://QXDistributors/'; SET vEnvironment = 'PROD'; // PROD, QA or DEV SET vBaseCurrency = 'COP'; // Currency every amount is converted to SET vPathModel = 'lib://QXDistributors/Model/'; // The model QVDs the apps load $(MUST_INCLUDE=$(vTxfPath)_QVS/Flow_1.qvs); $(MUST_INCLUDE=$(vTxfPath)_QVS/Flow_2.qvs); $(MUST_INCLUDE=$(vTxfPath)_QVS/Flow_3.qvs); $(MUST_INCLUDE=$(vTxfPath)_QVS/Flow_4.qvs); $(MUST_INCLUDE=$(vTxfPath)_QVS/Sales.qvs);
Flow_1.qvs
Archivo · lo lee MUST_INCLUDENo se pega: se guarda como archivo. En la pestaña Flow_1 de Generate Syntax, Export descarga Flow_1.qvs (Deploy escribe todas las pestañas en una carpeta de una vez). Ponlo en lib://QXDistributors/Transform/_QVS/Flow_1.qvs, la carpeta Transform/_QVS dentro de tu carpeta QXDistributors. Si el archivo no está, la recarga se detiene con un error: para eso existe MUST_INCLUDE.
Ver script · 35 líneas
/*---------------------------------------------------------------------------------------- Description [Flow_1]: ------------------------------------------------------------------------------------- Author: The Q-Fábrica team - Date: [2026-11-02] ------------------------------------------------------------------------------------- Changes ------------------------------------------------------------------------------------- Author Date Detail ------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------- Copyright © Q-Expert Distributors. Q-Expert Qlik Developer Framework. www.qfabrica.com ----------------------------------------------------------------------------------------*/ // ------------------------------------------------------------------------------------------------- // Table defined directly in the script (2 columns). [map_DocType_Src]: LOAD * INLINE [ "DocType", "DocTypeName" "NIT", "Company" "CC", "Person" ]; // ------------------------------------------------------------------------------------------------- // Mapping table (DocType → DocTypeName) from map_DocType_Src, for ApplyMap. [map_DocType]: MAPPING LOAD DocType, DocTypeName RESIDENT [map_DocType_Src]; DROP TABLE [map_DocType_Src];
Flow_2.qvs
Archivo · lo lee MUST_INCLUDENo se pega: se guarda como archivo. En la pestaña Flow_2 de Generate Syntax, Export descarga Flow_2.qvs (Deploy escribe todas las pestañas en una carpeta de una vez). Ponlo en lib://QXDistributors/Transform/_QVS/Flow_2.qvs, la carpeta Transform/_QVS dentro de tu carpeta QXDistributors. Si el archivo no está, la recarga se detiene con un error: para eso existe MUST_INCLUDE.
Ver script · 36 líneas
/*---------------------------------------------------------------------------------------- Description [Flow_2]: ------------------------------------------------------------------------------------- Author: The Q-Fábrica team - Date: [2026-11-02] ------------------------------------------------------------------------------------- Changes ------------------------------------------------------------------------------------- Author Date Detail ------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------- Copyright © Q-Expert Distributors. Q-Expert Qlik Developer Framework. www.qfabrica.com ----------------------------------------------------------------------------------------*/ // ------------------------------------------------------------------------------------------------- // Table defined directly in the script (2 columns). [map_CustomerStatus_Src]: LOAD * INLINE [ "Status", "CustomerStatus" "A", "Active" "I", "Inactive" "B", "Blocked" ]; // ------------------------------------------------------------------------------------------------- // Mapping table (Status → CustomerStatus) from map_CustomerStatus_Src, for ApplyMap. [map_CustomerStatus]: MAPPING LOAD Status, CustomerStatus RESIDENT [map_CustomerStatus_Src]; DROP TABLE [map_CustomerStatus_Src];
Flow_3.qvs
Archivo · lo lee MUST_INCLUDENo se pega: se guarda como archivo. En la pestaña Flow_3 de Generate Syntax, Export descarga Flow_3.qvs (Deploy escribe todas las pestañas en una carpeta de una vez). Ponlo en lib://QXDistributors/Transform/_QVS/Flow_3.qvs, la carpeta Transform/_QVS dentro de tu carpeta QXDistributors. Si el archivo no está, la recarga se detiene con un error: para eso existe MUST_INCLUDE.
Ver script · 36 líneas
/*---------------------------------------------------------------------------------------- Description [Flow_3]: ------------------------------------------------------------------------------------- Author: The Q-Fábrica team - Date: [2026-11-02] ------------------------------------------------------------------------------------- Changes ------------------------------------------------------------------------------------- Author Date Detail ------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------- Copyright © Q-Expert Distributors. Q-Expert Qlik Developer Framework. www.qfabrica.com ----------------------------------------------------------------------------------------*/ // ------------------------------------------------------------------------------------------------- // Table defined directly in the script (2 columns). [map_ChannelName_Src]: LOAD * INLINE [ "Channel", "ChannelName" "P", "Phone" "T", "In store" "W", "Web" ]; // ------------------------------------------------------------------------------------------------- // Mapping table (Channel → ChannelName) from map_ChannelName_Src, for ApplyMap. [map_ChannelName]: MAPPING LOAD Channel, ChannelName RESIDENT [map_ChannelName_Src]; DROP TABLE [map_ChannelName_Src];
Flow_4.qvs
Archivo · lo lee MUST_INCLUDENo se pega: se guarda como archivo. En la pestaña Flow_4 de Generate Syntax, Export descarga Flow_4.qvs (Deploy escribe todas las pestañas en una carpeta de una vez). Ponlo en lib://QXDistributors/Transform/_QVS/Flow_4.qvs, la carpeta Transform/_QVS dentro de tu carpeta QXDistributors. Si el archivo no está, la recarga se detiene con un error: para eso existe MUST_INCLUDE.
Ver script · 24 líneas
/*---------------------------------------------------------------------------------------- Description [Flow_4]: ------------------------------------------------------------------------------------- Author: The Q-Fábrica team - Date: [2026-11-02] ------------------------------------------------------------------------------------- Changes ------------------------------------------------------------------------------------- Author Date Detail ------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------- Copyright © Q-Expert Distributors. Q-Expert Qlik Developer Framework. www.qfabrica.com ----------------------------------------------------------------------------------------*/ // ------------------------------------------------------------------------------------------------- // Mapping table (BrandID → BrandName) from $(vPathRaw)Brands.txt, for ApplyMap. [map_BrandName]: MAPPING LOAD BrandID, BrandName FROM [$(vPathRaw)Brands.txt] (txt, utf8, delimiter is '|', embedded labels, msq);
Sales.qvs
Archivo · lo lee MUST_INCLUDENo se pega: se guarda como archivo. En la pestaña Sales de Generate Syntax, Export descarga Sales.qvs (Deploy escribe todas las pestañas en una carpeta de una vez). Ponlo en lib://QXDistributors/Transform/_QVS/Sales.qvs, la carpeta Transform/_QVS dentro de tu carpeta QXDistributors. Si el archivo no está, la recarga se detiene con un error: para eso existe MUST_INCLUDE.
Ver script · 535 líneas
/*---------------------------------------------------------------------------------------- Description [Sales]: ------------------------------------------------------------------------------------- Author: The Q-Fábrica team - Date: [2026-11-02] ------------------------------------------------------------------------------------- Changes ------------------------------------------------------------------------------------- Author Date Detail ------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------- Copyright © Q-Expert Distributors. Q-Expert Qlik Developer Framework. www.qfabrica.com ----------------------------------------------------------------------------------------*/ // ------------------------------------------------------------------------------------------------- // Empty table with 1 column (WHERE 1=0), schema only. [tmp_SalesRaw]: LOAD * INLINE [ "Dummy" ] WHERE 1=0; // ------------------------------------------------------------------------------------------------- // Loop FOR EACH … IN FileList() over $(vFile). FOR EACH vFile IN FileList('$(vPathRaw)Sales_*.csv') // ------------------------------------------------------------------------------------------------- // Raw load from $(vFile), concatenated into [tmp_SalesRaw]. CONCATENATE ([tmp_SalesRaw]) LOAD SaleID, SaleDate, CustomerID, ProductID, SalespersonID, BranchID, Quantity, UnitPrice, DiscountPct, Currency, Channel, FileName() AS SourceFile FROM [$(vFile)] (txt, utf8, delimiter is ',', embedded labels, msq); NEXT vFile; DROP FIELD Dummy FROM [tmp_SalesRaw]; LET vSalesRawRows = NoOfRows('tmp_SalesRaw'); TRACE Sales rows read from the monthly files: $(vSalesRawRows); // ------------------------------------------------------------------------------------------------- // Raw load of tmp_Parameter (no source set yet). [tmp_Parameter]: LOAD Parameter, Value FROM [$(vPathRaw)Parameters.txt] (txt, utf8, delimiter is ' ', embedded labels, msq); LET vCreditThreshold = Lookup('Value', 'Parameter', 'CreditThreshold', 'tmp_Parameter'); DROP TABLE [tmp_Parameter]; // ------------------------------------------------------------------------------------------------- // Mapping table (Num(Date#(HolidayDate, 'YYYY-MM-DD')) AS HolidayDate → HolidayName) from $(vPathRaw)Holidays.csv, for ApplyMap. [map_Holiday]: MAPPING LOAD DISTINCT Num(Date#(HolidayDate, 'YYYY-MM-DD')) AS HolidayDate, HolidayName FROM [$(vPathRaw)Holidays.csv] (txt, utf8, delimiter is ',', embedded labels, msq); QUALIFY *; UNQUALIFY CustomerID, ProductID, SalespersonID, BranchID, CategoryID, BrandID, DocNumber; // ------------------------------------------------------------------------------------------------- // Raw load of tmp_CustomerRaw (no source set yet). [tmp_CustomerRaw]: LOAD CustomerID, CustomerName, DocType, DocNumber, City, Segment, SignupDate, Status, CreditLimit FROM [$(vPathRaw)Customers.csv] (txt, utf8, delimiter is ',', embedded labels, msq); // ------------------------------------------------------------------------------------------------- // Raw load of tmp_ProductRaw (no source set yet). [tmp_ProductRaw]: LOAD ProductID, SKU, ProductName, CategoryID, BrandID, ListPrice, UnitCost, UnitOfMeasure, Status FROM [$(vPathRaw)Products.csv] (txt, utf8, delimiter is ',', embedded labels, msq); // ------------------------------------------------------------------------------------------------- // Raw load of tmp_SalespersonRaw (no source set yet). [tmp_SalespersonRaw]: LOAD SalespersonID, SalespersonName, BranchID, HireDate, MonthlyTarget FROM [$(vPathRaw)Salespeople.csv] (txt, utf8, delimiter is ',', embedded labels, msq); // ------------------------------------------------------------------------------------------------- // Raw load of tmp_BranchRaw (no source set yet). [tmp_BranchRaw]: LOAD BranchID, BranchName, Region, City, Format FROM [$(vPathRaw)Branches.csv] (txt, utf8, delimiter is ',', embedded labels, msq); // ------------------------------------------------------------------------------------------------- // Raw load of tmp_CategoryRaw (no source set yet). [tmp_CategoryRaw]: LOAD CategoryID, CategoryName, Family FROM [$(vPathRaw)Categories.csv] (txt, utf8, delimiter is ',', embedded labels, msq); UNQUALIFY *; // ------------------------------------------------------------------------------------------------- // Raw load of tmp_BrandRaw (no source set yet). [tmp_BrandRaw]: LOAD BrandID, BrandName, CountryOfOrigin FROM [$(vPathRaw)Brands.txt] (txt, utf8, delimiter is '|', embedded labels, msq); // ------------------------------------------------------------------------------------------------- // Transforms tmp_CustomerRaw. [tmp_CustomerClean]: LOAD CustomerID, DocNumber, [tmp_CustomerRaw.Segment] AS Segment, [tmp_CustomerRaw.CreditLimit] AS CreditLimit, Capitalize(Trim(tmp_CustomerRaw.CustomerName)) AS CustomerName, ApplyMap('map_DocType', tmp_CustomerRaw.DocType, 'Other') AS DocTypeName, If(Len(Trim(tmp_CustomerRaw.City)) = 0, 'No city', Trim(tmp_CustomerRaw.City)) AS CustomerCity, Date(Date#(tmp_CustomerRaw.SignupDate, 'DD/MM/YYYY')) AS SignupDate, ApplyMap('map_CustomerStatus', tmp_CustomerRaw.Status, 'Unknown') AS CustomerStatus, If(tmp_CustomerRaw.CreditLimit >= $(vCreditThreshold), 'High', 'Standard') AS CreditProfile, If(DocNumber = Previous(DocNumber), Peek('SurvivorID'), CustomerID) AS SurvivorID RESIDENT [tmp_CustomerRaw] ORDER BY DocNumber, CustomerID; // ------------------------------------------------------------------------------------------------- // Mapping table (CustomerID → SurvivorID) from tmp_CustomerClean, for ApplyMap. [map_CustomerSurvivor]: MAPPING LOAD CustomerID, SurvivorID RESIDENT [tmp_CustomerClean]; // ------------------------------------------------------------------------------------------------- // Transforms tmp_CustomerClean, combined via CONCATENATE. [Customer]: NOCONCATENATE LOAD CustomerID, DocNumber, Segment, CreditLimit, CustomerName, DocTypeName, CustomerCity, SignupDate, CustomerStatus, CreditProfile RESIDENT [tmp_CustomerClean] WHERE CustomerID = SurvivorID; CONCATENATE ([Customer]) // ------------------------------------------------------------------------------------------------- // Table defined directly in the script (3 columns). LOAD * INLINE [ "CustomerID", "CustomerName", "CustomerStatus" "-1", "Unknown customer", "Unknown" ]; DROP TABLE [tmp_CustomerClean]; // ------------------------------------------------------------------------------------------------- // Transforms tmp_ProductRaw, combined via LEFT JOIN. [Product]: LOAD ProductID, [tmp_ProductRaw.SKU] AS SKU, CategoryID, BrandID, [tmp_ProductRaw.ListPrice] AS ListPrice, [tmp_ProductRaw.UnitCost] AS UnitCost, [tmp_ProductRaw.UnitOfMeasure] AS UnitOfMeasure, Capitalize(Trim(tmp_ProductRaw.ProductName)) AS ProductName, ApplyMap('map_BrandName', BrandID, 'No brand') AS BrandName, Round( (tmp_ProductRaw.ListPrice - tmp_ProductRaw.UnitCost) / tmp_ProductRaw.ListPrice, 0.0001 ) AS MarginPct RESIDENT [tmp_ProductRaw] WHERE tmp_ProductRaw.Status = 'ACTIVE'; LEFT JOIN ([Product]) // ------------------------------------------------------------------------------------------------- // Transforms tmp_CategoryRaw. LOAD DISTINCT CategoryID, [tmp_CategoryRaw.CategoryName] AS CategoryName, [tmp_CategoryRaw.Family] AS Family RESIDENT [tmp_CategoryRaw]; LEFT JOIN ([Product]) // ------------------------------------------------------------------------------------------------- // Transforms tmp_BrandRaw. LOAD BrandID, CountryOfOrigin RESIDENT [tmp_BrandRaw]; // ------------------------------------------------------------------------------------------------- // Transforms tmp_SalespersonRaw, combined via LEFT JOIN. [Salesperson]: LEFT KEEP ([tmp_SalesRaw]) LOAD SalespersonID, BranchID AS HomeBranchID, [tmp_SalespersonRaw.MonthlyTarget] AS MonthlyTarget, Capitalize(Trim(tmp_SalespersonRaw.SalespersonName)) AS SalespersonName, Date(Date#(tmp_SalespersonRaw.HireDate, 'YYYY-MM-DD')) AS HireDate RESIDENT [tmp_SalespersonRaw]; // ------------------------------------------------------------------------------------------------- // Transforms tmp_BranchRaw. [Branch]: LOAD BranchID, [tmp_BranchRaw.BranchName] AS BranchName, [tmp_BranchRaw.Region] AS Region, [tmp_BranchRaw.City] AS BranchCity, [tmp_BranchRaw.Format] AS BranchFormat RESIDENT [tmp_BranchRaw]; DROP TABLE [tmp_BranchRaw]; DROP TABLE [tmp_BrandRaw]; DROP TABLE [tmp_CategoryRaw]; DROP TABLE [tmp_CustomerRaw]; DROP TABLE [tmp_ProductRaw]; DROP TABLE [tmp_SalespersonRaw]; // ------------------------------------------------------------------------------------------------- // Raw load of tmp_ExchangeRate (no source set yet). [tmp_ExchangeRate]: LOAD Currency, Rate, Date#(ValidFromDate, 'YYYY-MM-DD') AS ValidFromDate, Date#(ValidToDate, 'YYYY-MM-DD') AS ValidToDate FROM [$(vPathRaw)ExchangeRates.csv] (txt, utf8, delimiter is ',', embedded labels, msq); // ------------------------------------------------------------------------------------------------- // Associates SaleDate with the range [ValidFromDate, ValidToDate] from tmp_ExchangeRate. [tmp_RateBridge]: INTERVALMATCH (SaleDate, Currency) LOAD ValidFromDate, ValidToDate, Currency RESIDENT [tmp_ExchangeRate]; // ------------------------------------------------------------------------------------------------- // Transforms tmp_RateBridge, combined via LEFT JOIN. [tmp_SaleRate]: NOCONCATENATE LOAD SaleDate, ValidFromDate, ValidToDate, Currency RESIDENT [tmp_RateBridge]; LEFT JOIN ([tmp_SaleRate]) // ------------------------------------------------------------------------------------------------- // Transforms tmp_ExchangeRate. LOAD Currency, Rate, ValidFromDate, ValidToDate RESIDENT [tmp_ExchangeRate]; // ------------------------------------------------------------------------------------------------- // Transforms tmp_SalesRaw, combined via INNER JOIN. [tmp_Sales]: LOAD SaleID, ProductID, SalespersonID, BranchID, Quantity, UnitPrice, DiscountPct, Currency, SourceFile, Date(Date#(SaleDate, 'YYYY-MM-DD')) AS SaleDate, ApplyMap('map_CustomerSurvivor', CustomerID, -1) AS CustomerID, ApplyMap('map_ChannelName', Channel, 'Other') AS ChannelName, Quantity * UnitPrice * (1 - DiscountPct / 100) AS NetAmount RESIDENT [tmp_SalesRaw] WHERE Quantity > 0; INNER JOIN ([tmp_Sales]) // ------------------------------------------------------------------------------------------------- // Transforms Product. LOAD ProductID RESIDENT [Product]; LEFT JOIN ([tmp_Sales]) // ------------------------------------------------------------------------------------------------- // Transforms tmp_SaleRate. LOAD SaleDate, Currency, Rate RESIDENT [tmp_SaleRate] WHERE Currency <> '$(vBaseCurrency)'; DROP TABLE [tmp_SalesRaw]; DROP TABLE [tmp_ExchangeRate]; DROP TABLE [tmp_RateBridge]; DROP TABLE [tmp_SaleRate]; // ------------------------------------------------------------------------------------------------- // Raw load of tmp_AmountBand (no source set yet). [tmp_AmountBand]: LOAD FromAmount, ToAmount, Band AS AmountBand FROM [$(vPathRaw)AmountBands.csv] (txt, utf8, delimiter is ',', embedded labels, msq); // ------------------------------------------------------------------------------------------------- // Associates NetAmountCOP with the range [FromAmount, ToAmount] from tmp_AmountBand. [tmp_BandBridge]: INTERVALMATCH (NetAmountCOP) LOAD FromAmount, ToAmount RESIDENT [tmp_AmountBand]; // ------------------------------------------------------------------------------------------------- // Transforms tmp_BandBridge, combined via LEFT JOIN. [tmp_SaleBand]: NOCONCATENATE LOAD NetAmountCOP, FromAmount, ToAmount RESIDENT [tmp_BandBridge]; LEFT JOIN ([tmp_SaleBand]) // ------------------------------------------------------------------------------------------------- // Transforms tmp_AmountBand. LOAD FromAmount, ToAmount, AmountBand RESIDENT [tmp_AmountBand]; // ------------------------------------------------------------------------------------------------- // Transforms tmp_Sales, combined via LEFT JOIN. [Sales]: NOCONCATENATE LOAD *, Round(NetAmount * Alt(Rate, 1)) AS NetAmountCOP RESIDENT [tmp_Sales]; LEFT JOIN ([Sales]) // ------------------------------------------------------------------------------------------------- // Transforms tmp_SaleBand. LOAD NetAmountCOP, AmountBand RESIDENT [tmp_SaleBand]; DROP TABLE [tmp_Sales]; DROP TABLE [tmp_AmountBand]; DROP TABLE [tmp_BandBridge]; DROP TABLE [tmp_SaleBand]; DROP FIELD SourceFile FROM [Sales]; STORE [Sales] INTO [$(vTxfPath)Sales.qvd] (qvd); // ------------------------------------------------------------------------------------------------- // Transforms SaleReturn_PrecedingLoad, combined via INNER JOIN. [SaleReturn]: LOAD ReturnID, SaleID, Reason, ReturnedQty, ReturnDate, If(Match(Reason, 'DAMAGED', 'EXPIRED'), 'Quality', 'Commercial') AS ReasonType ; // ------------------------------------------------------------------------------------------------- // Raw load of SaleReturn_PrecedingLoad (no source set yet). LOAD ReturnID, SaleID, Reason, Quantity AS ReturnedQty, Date(Date#(ReturnDate, 'YYYY-MM-DD')) AS ReturnDate FROM [$(vPathRaw)Returns.csv] (txt, utf8, delimiter is ',', embedded labels, msq); INNER JOIN ([SaleReturn]) // ------------------------------------------------------------------------------------------------- // Transforms Sales. LOAD SaleID, CustomerID, ProductID, SalespersonID, BranchID, Currency, ChannelName, NetAmountCOP / Quantity AS UnitNetCOP RESIDENT [Sales]; // ------------------------------------------------------------------------------------------------- // Transforms Sales, combined via CONCATENATE. [Movement]: NOCONCATENATE LOAD SaleID, SaleDate AS MovementDate, CustomerID, ProductID, SalespersonID, BranchID, Quantity, Currency, ChannelName, NetAmountCOP, AmountBand, 'Sale' AS MovementType RESIDENT [Sales]; CONCATENATE ([Movement]) // ------------------------------------------------------------------------------------------------- // Transforms SaleReturn. LOAD SaleID, ReturnDate AS MovementDate, CustomerID, ProductID, SalespersonID, BranchID, Currency, ChannelName, ReasonType, 'Return' AS MovementType, -ReturnedQty AS Quantity, Round(-ReturnedQty * UnitNetCOP) AS NetAmountCOP RESIDENT [SaleReturn]; // ------------------------------------------------------------------------------------------------- // Raw load of Budget (no source set yet). [Budget]: LOAD BranchID, Jan, Feb, Mar FROM [$(vPathRaw)Budget.csv] (txt, utf8, delimiter is ',', embedded labels, msq); STORE [Movement] INTO [$(vTxfPath)Movement.qvd] (qvd); // ------------------------------------------------------------------------------------------------- // Unpivots columns into [BudgetMonthName] / [BudgetAmount] (CROSSTABLE). [tmp_Budget]: CROSSTABLE('BudgetMonthName', 'BudgetAmount', 1) LOAD BranchID, Jan, Feb, Mar RESIDENT [Budget]; DROP TABLE [Budget]; // ------------------------------------------------------------------------------------------------- // Transforms tmp_Budget, combined via OUTER JOIN. [BudgetVsActual]: LOAD BranchID, BudgetAmount, Match(BudgetMonthName, 'Jan', 'Feb', 'Mar') AS MonthNum RESIDENT [tmp_Budget]; OUTER JOIN ([BudgetVsActual]) // ------------------------------------------------------------------------------------------------- // Transforms Movement. LOAD BranchID, Num(Month(MovementDate)) AS MonthNum, Sum(NetAmountCOP) AS ActualAmountCOP RESIDENT [Movement] GROUP BY BranchID, Num(Month(MovementDate)); DROP TABLE [tmp_Budget]; STORE [BudgetVsActual] INTO [$(vTxfPath)BudgetVsActual.qvd] (qvd); EXIT SCRIPT;
05Por qué se hace así
- Concatenar, no restar tablas. Ventas y devoluciones en una sola tabla, las devoluciones con signo negativo, dejan que cada gráfico las compense solo. Ninguna expresión tiene que saber que hubo dos fuentes.
- Una devolución vale lo que cobró la venta. Tomar el monto unitario de la venta deja las devoluciones en los mismos pesos que la venta, tasa incluida.
- CROSSTABLE para archivos anchos. Una hoja con una columna por mes es fácil de digitar y difícil de modelar. Despivotada, el mes es un valor que puedes enlazar y filtrar.
- Agregar al entrar. El GROUP BY del OUTER JOIN trae exactamente un real por sucursal y mes, el mismo grano del presupuesto.
06Lo que sigue
Episodio 6: déjalo listo para la app.
Antes de modelar
Q-Explorator es la herramienta de escritorio del equipo de Q-Fábrica para explorar archivos QVD, CSV, Excel y Parquet, y para convertir archivos CSV, TXT y Excel en QVD. Abre en ella los archivos de cada episodio para ver primero su contenido y su calidad.
Conoce Q-Explorator