I have a series of Receive and Issue transactions. Received inventory can be at different costs per job material item, possibly received in multiple transactions. And there are two possible methods of receipt: Mfg Receipt and Receipt Lines. Where neither exist, the cost defaults to average inventory cost, which is reliably stored in Cost Type. Received cost is not reliably stored in an Issue transaction's Cost Type, so I have to search backward in the receipt transactions. This precedence is handled by COALESCE.
The aim is to return (reverse) issue to inventory at the same cost at which it was received. Subsequent receipts are possible, at a different cost, and returns should be at that cost, and not at costs from receipts prior or subsequent within the same job.
Transaction (prefix imt) joined to Cost Type (prefix int) look like this for Material ID 21:
Each MaterialIssue transactions can pair to each Receipt Lines, so I have to restrict the inner result to a single receive transaction to prevent Cartesian product, without which, I could get multiple results, as in material item 21:
I have tried to accomplish with this CTE, using ROW() ranking, commented with the imaginary desired method, but it's mind-bending and I'm not sure how to go about it:
DECLARE @bShowNonInventory BIT = 1
DECLARE @nCostingMethod INT = 1
DECLARE @vJobID VARCHAR (11) = '23532-02-01'
; WITH
MaterialIssues as (
SELECT
MI.imtJobID JobID
, MI.imtJobAssemblyID AssemblyID
, MI.imtJobMaterialID MaterialID
, SUM(
(
COALESCE(
MR.intActualUnitMaterialCost
, RL.intActualUnitMaterialCost
, intUnitMaterialCost
)
)
* - (intQuantity)
) NetCost
FROM PartTransactions MI -- Material Issue
INNER JOIN PartTransactionCosts
ON imtPartTransactionID = intPartTransactionID
LEFT JOIN /* get single, most recent received cost */
( SELECT * FROM
(
SELECT
imtJobID, imtJobAssemblyID, imtJobMaterialID
, imtPartTransactionID
, intActualUnitMaterialCost
, ROW_Number()
OVER (Partition by imtJobID, imtJobAssemblyID, imtJobMaterialID
Order by imtPartTransactionID DESC) as rn
--^ set of results from ranking too broad
FROM PartTransactionCosts
JOIN PartTransactions
on imtPartTransactionID=intPartTransactionID
WHERE imtTableName='MfgReceipts'
AND intCostType = 4
--< need outer reference here somehow: imtPartTransactionID < (earlier) MI.imtPartTransactionID
) as d
WHERE rn=1
) MR -- Receipt by Mfg Receipt
ON MI.imtJobID=MR.imtJobID
AND MI.imtJobAssemblyID=MR.imtJobAssemblyID
AND MI.imtJobMaterialID=MR.imtJobMaterialID
AND MR.imtPartTransactionID < MI.imtPartTransactionID --< dubious restriction on inner result
LEFT JOIN
( SELECT * FROM
(
SELECT imtJobID, imtJobAssemblyID, imtJobMaterialID
, intActualUnitMaterialCost, imtPartTransactionID
, ROW_Number()
OVER (Partition by imtJobID, imtJobAssemblyID, imtJobMaterialID
Order by imtPartTransactionID DESC) as rn
FROM PartTransactionCosts
JOIN PartTransactions
on imtPartTransactionID=intPartTransactionID
WHERE imtTableName='ReceiptLines'
AND intCostType = 4
--< need outer reference here somehow: imtPartTransactionID < (earlier) MI.imtPartTransactionID
) as d
WHERE rn=1
) RL -- Receipt by ReceiptLine
ON MI.imtJobID=RL.imtJobID
AND MI.imtJobAssemblyID=RL.imtJobAssemblyID
AND MI.imtJobMaterialID=RL.imtJobMaterialID
AND RL.imtPartTransactionID < MI.imtPartTransactionID --< dubious restriction on inner result
WHERE
imtSource = 3
AND imtNonInventoryTransaction <= @bShowNonInventory
AND intCostType = @nCostingMethod
GROUP BY MI.imtJobID
, MI.imtJobAssemblyID
, MI.imtJobMaterialID
)
-- test query:
Select * from
MaterialIssues
WHERE JobID = @vJobID
Note that this problem is very similar to Restricting inner query with outer query atttribute - but I don't see a requirement to restrict the inner set to one, most recent, but not subsequent result.
In summary, the net result of quantities received and (return) issued for item 21 is zero, but the net cost should also be zero, but is not correct.
Thank you in advance for your suggestions.
EDIT: DDL and sample data:
USE [master]
GO
CREATE DATABASE [test]
CONTAINMENT = NONE
ON PRIMARY
( NAME = N'test', FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL12.MSSQLSERVER\MSSQL\DATA\test.mdf' , SIZE = 3072KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB )
LOG ON
( NAME = N'test_log', FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL12.MSSQLSERVER\MSSQL\DATA\test_log.ldf' , SIZE = 199680KB , MAXSIZE = 2048GB , FILEGROWTH = 10%)
GO
USE test
CREATE TABLE [test].dbo.[PartTransactions](
[imtPartTransactionID] [int] NOT NULL,
[imtTableName] [nvarchar](30) NOT NULL,
[imtTransactionType] [tinyint] NOT NULL,
[imtSource] [tinyint] NOT NULL,
[imtNonInventoryTransaction] [bit] NOT NULL,
[imtJobID] [nvarchar](20) NOT NULL,
[imtJobAssemblyID] [int] NOT NULL,
[imtJobMaterialID] [int] NOT NULL,
[imtPartID] [nvarchar](30) NOT NULL,
) ON [PRIMARY]
GO
CREATE TABLE [test].[dbo].[PartTransactionCosts](
[intPartTransactionID] [int] NOT NULL,
[intPartTransactionCostID] [int] NOT NULL,
[intCostType] [tinyint] NOT NULL,
[intQuantity] [numeric](15, 5) NOT NULL,
[intUnitMaterialCost] [numeric](15, 5) NOT NULL,
[intActualUnitMaterialCost] [numeric](15, 5) NOT NULL,
) ON [PRIMARY]
GO
-- USE [test]
GO
INSERT [dbo].[PartTransactions] ([imtPartTransactionID], [imtTableName], [imtTransactionType], [imtSource], [imtNonInventoryTransaction], [imtJobID], [imtJobAssemblyID], [imtJobMaterialID], [imtPartID]) VALUES (117718, N'MaterialIssueLines', 2, 3, 0, N'23532-02-01', 0, 21, N'26271/2GC')
GO
INSERT [dbo].[PartTransactions] ([imtPartTransactionID], [imtTableName], [imtTransactionType], [imtSource], [imtNonInventoryTransaction], [imtJobID], [imtJobAssemblyID], [imtJobMaterialID], [imtPartID]) VALUES (117719, N'MfgReceipts', 1, 2, 1, N'23532-02-01', 0, 21, N'26271/2GC')
GO
INSERT [dbo].[PartTransactions] ([imtPartTransactionID], [imtTableName], [imtTransactionType], [imtSource], [imtNonInventoryTransaction], [imtJobID], [imtJobAssemblyID], [imtJobMaterialID], [imtPartID]) VALUES (117726, N'MaterialIssueLines', 2, 3, 0, N'23532-02-01', 0, 21, N'26271/2GC')
GO
INSERT [dbo].[PartTransactions] ([imtPartTransactionID], [imtTableName], [imtTransactionType], [imtSource], [imtNonInventoryTransaction], [imtJobID], [imtJobAssemblyID], [imtJobMaterialID], [imtPartID]) VALUES (117725, N'MfgReceipts', 1, 2, 1, N'23532-02-01', 0, 21, N'26271/2GC')
GO
INSERT [dbo].[PartTransactions] ([imtPartTransactionID], [imtTableName], [imtTransactionType], [imtSource], [imtNonInventoryTransaction], [imtJobID], [imtJobAssemblyID], [imtJobMaterialID], [imtPartID]) VALUES (117727, N'MaterialIssueLines', 2, 3, 0, N'23532-02-01', 0, 21, N'26271/2GC')
GO
INSERT [dbo].[PartTransactions] ([imtPartTransactionID], [imtTableName], [imtTransactionType], [imtSource], [imtNonInventoryTransaction], [imtJobID], [imtJobAssemblyID], [imtJobMaterialID], [imtPartID]) VALUES (117513, N'MfgReceipts', 1, 2, 1, N'23532-02-01', 0, 21, N'26271/2GC')
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117718, 1, 1, CAST(555.00000 AS Numeric(15, 5)), CAST(1.61513 AS Numeric(15, 5)), CAST(1.61513 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117718, 2, 2, CAST(555.00000 AS Numeric(15, 5)), CAST(1.53503 AS Numeric(15, 5)), CAST(1.53503 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117718, 3, 3, CAST(555.00000 AS Numeric(15, 5)), CAST(0.00000 AS Numeric(15, 5)), CAST(0.00000 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117718, 4, 4, CAST(555.00000 AS Numeric(15, 5)), CAST(1.24865 AS Numeric(15, 5)), CAST(1.24865 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117719, 1, 1, CAST(555.00000 AS Numeric(15, 5)), CAST(1.61513 AS Numeric(15, 5)), CAST(1.61513 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117719, 2, 2, CAST(555.00000 AS Numeric(15, 5)), CAST(1.53503 AS Numeric(15, 5)), CAST(1.53503 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117719, 3, 3, CAST(555.00000 AS Numeric(15, 5)), CAST(0.00000 AS Numeric(15, 5)), CAST(0.00000 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117719, 4, 4, CAST(555.00000 AS Numeric(15, 5)), CAST(1.16410 AS Numeric(15, 5)), CAST(1.16410 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117726, 1, 1, CAST(1100.00000 AS Numeric(15, 5)), CAST(1.61513 AS Numeric(15, 5)), CAST(1.61513 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117726, 2, 2, CAST(1100.00000 AS Numeric(15, 5)), CAST(1.53503 AS Numeric(15, 5)), CAST(1.53503 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117726, 3, 3, CAST(1100.00000 AS Numeric(15, 5)), CAST(0.00000 AS Numeric(15, 5)), CAST(0.00000 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117726, 4, 4, CAST(1100.00000 AS Numeric(15, 5)), CAST(1.16410 AS Numeric(15, 5)), CAST(1.16410 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117725, 1, 1, CAST(555.00000 AS Numeric(15, 5)), CAST(1.61513 AS Numeric(15, 5)), CAST(1.61513 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117725, 2, 2, CAST(555.00000 AS Numeric(15, 5)), CAST(1.53503 AS Numeric(15, 5)), CAST(1.53503 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117725, 3, 3, CAST(555.00000 AS Numeric(15, 5)), CAST(0.00000 AS Numeric(15, 5)), CAST(0.00000 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117725, 4, 4, CAST(555.00000 AS Numeric(15, 5)), CAST(1.16410 AS Numeric(15, 5)), CAST(1.16410 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117727, 1, 1, CAST(10.00000 AS Numeric(15, 5)), CAST(1.61513 AS Numeric(15, 5)), CAST(1.61513 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117727, 2, 2, CAST(10.00000 AS Numeric(15, 5)), CAST(1.53503 AS Numeric(15, 5)), CAST(1.53503 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117727, 3, 3, CAST(10.00000 AS Numeric(15, 5)), CAST(0.00000 AS Numeric(15, 5)), CAST(0.00000 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117727, 4, 4, CAST(10.00000 AS Numeric(15, 5)), CAST(1.16410 AS Numeric(15, 5)), CAST(1.16410 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117513, 1, 1, CAST(555.00000 AS Numeric(15, 5)), CAST(1.61513 AS Numeric(15, 5)), CAST(1.61513 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117513, 2, 2, CAST(555.00000 AS Numeric(15, 5)), CAST(1.53503 AS Numeric(15, 5)), CAST(1.53503 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117513, 3, 3, CAST(555.00000 AS Numeric(15, 5)), CAST(0.00000 AS Numeric(15, 5)), CAST(0.00000 AS Numeric(15, 5)))
GO
INSERT [dbo].[PartTransactionCosts] ([intPartTransactionID], [intPartTransactionCostID], [intCostType], [intQuantity], [intUnitMaterialCost], [intActualUnitMaterialCost]) VALUES (117513, 4, 4, CAST(555.00000 AS Numeric(15, 5)), CAST(1.24865 AS Numeric(15, 5)), CAST(1.24865 AS Numeric(15, 5)))
GO
UPDATE: What this does.
First of all, this is from a DB backend for an ERP system called M1. Its table structures are immutable.
Receiving material to inventory or directly to a job is done through "part transactions." A part transaction joins to one of 4 possible part transaction cost types: 1) average cost 2) Last cost (regardless of the job in question) 3) Standard cost 4) Actual cost (as received directly to a job). Which is to be used is fixed by company-wide accounting policy settings.
Average cost is used for transactions that issue to a job from inventory, or return to inventory (unused) from a job. This is calculated in application code (VB) upon each transaction.
Material issues and returns NOT involving inventory are received directly to a job through various kinds of receipts, such as from a Purchase Order or Manufacturing Receipt (tables: ReceiptLines, MfgReceipt) - and a number of other possible methods (e.g. DMR, RMI, Inspection, Scrap, Shipment, Adjustment). These entities do not have the same footprint and are stored in different tables.
Unfortunately, the application code does not reliably store the "received" cost to a job via Receipt (PO) or Manufacturing receipt, so the data in PartTransactionCost (type 4, Actual) cannot be used as intended. Usually it does calculate correctly, but mysteriously sometimes it doesn't. It's a bug.
Therefore the only reliable way to return materials from a job at the same cost at which they were recieved by any kind of receipt, is to get the actual cost from the applicable receipt record. The COALESCE function selects the non-null join, in order of precedence, and where no receipt is found (i.e. all null results), the cost of last resort is the Average cost.
In the sample data given, There was one receipt of 555 quantity of item 21 at a unit cost of 1.25, then all 555 were returned (i.e. unused). This is done through a MaterialIssue transaction (whose sign is positive, though MaterialIssues have a negative Qty), resulting in a Net Quantity 0, Net Cost 0.00.
Then two more receipts were done for a total quantity of 1100, but at a different recieved unit cost than that of the first receipt. Then two more return transactions occured resulting in an net issue quantity again of 0. However, the code I inherited did not result in a net cost of zero, because the simple join produced a cross product of 3 x 3 receive and return transactions, for a spurious result of 9 components in the sum. So the net returned cost was wrong by multiples. This error was not noticed for a long time because that sequence of transactions is rare.
It must be noted that in this example the Last costs happen to be correct, but since they cannot be relied upon, I needed to get the actual received cost from the receipt record (table: MfgReceipts), and not from the receipt transaction. And the same technique is needed for other reciept types, such as in the table ReceiptLines. But only one receipt record can be used. In the solution provided, the last receipt's actual cost is returned only once, and used in the return transaction in lieu of the stored "last cost" - this results in a net issue quantity of zero and a net cost of zero.