Here you go. Added some notes, removed some strings specific to my company and replaced them with an explanation.
It is not the most efficient way to do this, but I didn’t feel like wrapping my head around recursion that day and it runs quickly enough. It will spit out a table that, if sorted by FullMtlSeq, will give you a neat indented layout. There isn’t anything beyond sums in the report layer.
/*
Variables
@Part: your part number
@ProdQty: Run size. Will amortize production hours, but not setup, on designated machining ops.
@Efficiency: Default is 1.0, which is current BOM. Higher number represents more efficient than current. Again, only affects production hours on machining ops.
@IncludeFixtures include the cost of reusable fixtures, which are designated with a specific part class.
@Lvl internal variable. Starts as zero. Used to track how deep in the BOM the loop is.
*/
SELECT
PartRev.PartNum
,FullMtlSeq = CAST('0' as varchar(max))
,Lvl = @Lvl
,MtlSeq = 0
,ParentPartNum = CAST('' as char(50))
,ProdQty = CAST(@ProdQty as decimal(12,4))
,QtyPer = CAST(1.000 as decimal(12,4))
,FixedQty = 0
INTO #Temp --Runs much quicker with a temp table than CTE/subquery
FROM erp.PartRev
INNER JOIN LastPartRev() AS LastPartRev --StoredProc that returns the most recent approved rev.
ON PartRev.PartNum = LastPartRev.PartNum
AND PartRev.RevisionNum = LastPartRev.RevisionNum
WHERE PartRev.PartNum = @Part
WHILE @@ROWCOUNT > 0
BEGIN
SET @Lvl += 1;
INSERT INTO #Temp
SELECT
PartNum = PartMtl.MtlPartNum
,FullMtlSeq = #Temp.FullMtlSeq + '/' + CAST(PartMtl.MtlSeq as varchar)
,Lvl = @Lvl
,PartMtl.MtlSeq
,PartMtl.PartNum
,ProdQty = CASE WHEN PartMtl.FixedQty = 1
THEN PartMtl.QtyPer
ELSE PartMtl.QtyPer * #Temp.ProdQty END
,PartMtl.QtyPer
,PartMtl.FixedQty
FROM #Temp
INNER JOIN LastPartRev() AS LastPartRev
ON #Temp.PartNum = LastPartRev.PartNum
INNER JOIN erp.PartMtl
ON LastPartRev.PartNum = PartMtl.PartNum
AND LastPartRev.RevisionNum = PartMtl.RevisionNum
AND PartMtl.AltMethod = ''
INNER JOIN erp.Part ON PartMtl.MtlPartNum = Part.PartNum
WHERE #Temp.Lvl = @Lvl - 1
AND (@IncludeFixtures = 1 OR Part.ClassID <> 'FixturePartClass')
END
SELECT
#Temp.PartNum
,Part.PartDescription
,Part.TypeCode
,Part.ClassID
,Rev = ISNULL(LastPartRev.RevisionNum,'')
,#Temp.FullMtlSeq
,#Temp.Lvl
,#Temp.MtlSeq
,#Temp.ParentPartNum
,#Temp.ProdQty
,#Temp.QtyPer
,#Temp.FixedQty
,MaterialCost = CASE Part.TypeCode WHEN 'P' THEN PartCost.AvgMaterialCost ELSE 0 END * #Temp.ProdQty
,OprSeq = ISNULL(PartOpr.OprSeq,0)
,OpCode = ISNULL(PartOpr.OpCode,'')
,ResourceGrp = ISNULL(PartOpDtl.ResourceGrpID,'')
,BurdenRate = ISNULL(ResourceGroup.ProdBurRate,0)
,LaborRate = ISNULL(ResourceGroup.ProdLabRate,0)
,PartOpr.EstSetHours
,ProdBurHrs = ISNULL(
PartOpr.EstProdHours
* #Temp.ProdQty
,0)
,ProdLaborHrs = ISNULL(
PartOpr.EstProdHours
* #Temp.ProdQty
/ CASE WHEN OpMaster.AnalysisCode = 'Machining' THEN @Efficiency ELSE 1 END
,0)
,SetupCost = ISNULL(
PartOpr.EstSetHours
* (ResourceGroup.ProdBurRate
+ ResourceGroup.ProdLabRate)
,0)
,ProdCost = ISNULL(
PartOpr.EstProdHours
* #Temp.ProdQty
* ResourceGroup.ProdBurRate
+ PartOpr.EstProdHours
* #Temp.ProdQty
/ CASE WHEN OpMaster.AnalysisCode = 'Machining' THEN @Efficiency ELSE 1 END
* ResourceGroup.ProdLabRate
,0)
FROM #Temp
LEFT JOIN LastPartRev() AS LastPartRev
ON #Temp.PartNum = LastPartRev.PartNum
INNER JOIN erp.Part ON #Temp.PartNum = Part.PartNum
INNER JOIN erp.PartCost
ON #Temp.PartNum = PartCost.PartNum
AND PartCost.CostID = 1
LEFT JOIN erp.PartOpr
ON #Temp.PartNum = PartOpr.PartNum
AND LastPartRev.RevisionNum = PartOpr.RevisionNum
AND PartOpr.AltMethod = ''
LEFT JOIN erp.PartOpDtl
ON PartOpr.PartNum = PartOpDtl.PartNum
AND PartOpr.RevisionNum = PartOpDtl.RevisionNum
AND PartOpr.AltMethod = PartOpDtl.AltMethod
AND PartOpr.OprSeq = PartOpDtl.OprSeq
AND PartOpr.PrimaryProdOpDtl = PartOpDtl.OpDtlSeq
LEFT JOIN erp.ResourceGroup
ON PartOpDtl.ResourceGrpID = ResourceGroup.ResourceGrpID
LEFT JOIN erp.OpMaster ON PartOpr.OpCode = OpMaster.OpCode
DROP TABLE #Temp