অধ্যায় ২৭ — Inventory Accounting
Volume 2 · Part 4 — Application-specific Accounting · Chapter 27
পূর্বশর্ত: অধ্যায় ১৫ (Purchase — Dr কোথায়), অধ্যায় ১৩ (Sales ও Revenue), অধ্যায় ৯ ও ১৪ (Control account ও subledger), অধ্যায় ১৯ (Credit Note ও Debit Note)
১. Learning Objective
এই অধ্যায় শেষে আপনি পারবেন:
Inventory কেন asset, আর কোন মুহূর্তে সেটা খরচ (COGS) হয় — দুটো ঘটনা আলাদা করতে
Perpetual আর periodic পদ্ধতির পার্থক্য বলতে, আর software-এ কেন perpetual-ই একমাত্র পথ
Inventory-র cost-এ কী ঢোকে (দাম, freight, শুল্ক) আর কী ঢোকে না (VAT, গুদামভাড়া) — ঠিক করতে
FIFO আর Weighted Average — একই লেনদেনে দুই পদ্ধতির COGS হাতে হিসাব করতে
Stock movement-এর ছয়টি প্রকার (receipt, issue, return, adjustment, transfer, write-down) — প্রতিটির entry লিখতে
Quantity ledger আর value ledger — দুটো একসাথে, একটাই সত্য — নকশা করতে
Inventory subledger (item × warehouse) আর GL control account মেলাতে — অধ্যায় ১৪-এর তিন স্তর এখানে
Backdated receipt এলে পরের সব issue-র cost আবার হিসাব (recost) — কেন লাগে, কীভাবে হয়
Negative stock, physical count, NRV write-down — তিনটি নীতি ও তাদের entry
Cost layer, layer consumption, moving average — এই তিনটি table দিয়ে একটা costing engine লিখতেসময়: পড়া ৬০ মিনিট + অনুশীলন ৭০ মিনিট।
২. Concept Explanation
এই Part-এর ছাঁচ
Volume 1-এ একটা GL, একটা posting rule, একটা report engine বানিয়েছেন। Volume 2-এ প্রতিটি অধ্যায় একটা business application — আর প্রতিটির প্রশ্ন একটাই: এই application-এর কোন ঘটনা accounting-এর ঘটনা, আর সেটা engine-এ কীভাবে ঢোকে? প্রতিটি অধ্যায় একই পথে হাঁটবে:
Business Flow মাল এল, মাল গেল, মাল গুনলাম
↓
Financial Event "asset বাড়ল", "asset খরচ হলো", "asset হারাল"
↓
Debit / Credit Dr 1145 / Cr 2110; Dr 5140 / Cr 1145
↓
Journal Entry source = stock_movement
↓
Subledger item × warehouse: quantity + value
↓
General Ledger 1145 Inventory (control)
↓
Statement Impact Balance Sheet: inventory; Income Statement: COGS, gross profit
↓
Database Design stock_movements, cost_layers, item_warehouse_balances
↓
API / Service InventoryService.receive / issue / adjust / recostInventory দিয়ে শুরু কারণ এটা প্রথম subledger যেখানে টাকার অঙ্ক ব্যবহারকারী লেখেন না — system হিসাব করে। A/R-এ invoice-এর অঙ্ক invoice-এ লেখা; A/P-এ bill-এ। কিন্তু ১০টা POS terminal বিক্রি হলে তার cost কত — সেটা কেউ type করে না; গুদামে কী দামে কী ক্রমে ঢুকেছিল, সেখান থেকে engine বের করে। এটাই এই অধ্যায়ের সবচেয়ে বড় নকশা-পরিবর্তন।
দুটো মুহূর্ত
অধ্যায় ১৫-এর তিন প্রশ্নের তৃতীয়টা — “আবার বিক্রি করবেন?” → Inventory। কেন expense নয়? কারণ কেনার দিন কিছু খরচ হয়নি — টাকা এক রূপ থেকে অন্য রূপে গেছে (bank → মাল)। খরচ হয় যেদিন মালটা হাতছাড়া হয়:
০২-০৭ ২০টা terminal কিনলেন, ৩,৬০,০০০ ০৮-০৭ ১২টা বিক্রি, ৩,১২,০০০-এ
──────────────────────────────────── ──────────────────────────────────
Dr 1145 Inventory 3,60,000 Dr 1130 A/R 3,58,800
Cr 2110 A/P 3,60,000 Cr 4140 Product Sales 3,12,000
Cr 2140 VAT Payable 46,800
Income Statement: কিছুই না
Balance Sheet: asset +3,60,000 Dr 5140 COGS 2,22,000 ← ১২ × ১৮,৫০০
Cr 1145 Inventory 2,22,000
Income Statement: আয় 3,12,000, খরচ 2,22,000
Balance Sheet: inventory −2,22,000বিক্রির দিন দুটো entry — একটা আয়ের (অধ্যায় ১৩-এর মতোই), একটা খরচের (নতুন)। দ্বিতীয়টার অঙ্ক ২,২২,০০০ কোথা থেকে এল — সেটাই বাকি অধ্যায়।
Developer-এর দৃষ্টিতে: revenue entry-র উৎস
sales_invoice, তার অঙ্ক invoice-এ। COGS entry-র উৎসstock_movement, তার অঙ্ক costing engine-এ। দুটো আলাদা entry, দুটো আলাদা source_type — একটায় মেশাবেন না; section ৫-এ কারণ।
Perpetual বনাম periodic
পুরনো পদ্ধতি (periodic): সারা মাস কেনাকাটা “Purchases” খরচে লিখুন, মাস শেষে গুদাম গুনুন, তারপর:
COGS = opening stock + purchases − closing stock (গুনে)সরল, কিন্তু মাসের মাঝে কোনো দিন জানেন না stock কত, লাভ কত, কী চুরি গেল। Software-এ এর কোনো জায়গা নেই — perpetual: প্রতিটি movement-এ quantity আর value দুটোই সাথে সাথে বদলায়, যেকোনো মুহূর্তে SELECT দিলে stock। এই বইয়ে periodic শব্দটা আর আসবে না; শুধু জেনে রাখুন হিসাবরক্ষকরা কখনো কখনো ওই সূত্র দিয়ে আপনার সংখ্যা যাচাই করবেন (section ৫-এর যাচাই ৯৭ সেই সূত্রই)।
Cost-এ কী ঢোকে
Inventory-র মূল্য = মালটা বিক্রির উপযোগী অবস্থায় গুদামে আনতে যা লেগেছে:
ঢোকে ঢোকে না
───── ────────
supplier-এর দাম (trade discount বাদে) VAT — claimable হলে (1160-এ যায়)
freight, insurance (আনার) গুদামভাড়া, বিক্রয়কর্মীর বেতন
আমদানি শুল্ক, C&F settlement discount received (অধ্যায় ১৬)
আনার পথে handling গুদামে থাকা অবস্থায় অপচয়যা ঢোকে অথচ supplier-এর bill-এ নেই — freight, শুল্ক — তাকে বলে landed cost। ২০টা terminal-এর দাম ৩,৬০,০০০ + transport bill ১০,০০০ → ৩,৭০,০০০ ÷ ২০ = ১৮,৫০০ প্রতি unit। Transport bill-টা 5260 Travel-এ গেলে (সবচেয়ে সাধারণ ভুল) — এ মাসে খরচ ১০,০০০ বেশি, stock ১০,০০০ কম, আর প্রতিটি বিক্রির gross margin ৫০০ টাকা করে মিথ্যা।
Valuation — কোন ১০টা গেল?
গুদামে ৮টা terminal ১৮,৫০০-এ কেনা, ১২টা ২০,০০০-এ। ১০টা বিক্রি হলো। COGS কত? Terminal-গুলো একই রকম, কোনটা গেল বোঝার উপায় নেই। পদ্ধতি লাগে, আর পদ্ধতি একটা নীতি — item-ধরে ঠিক, বদলানো যায় না:
FIFO (First In, First Out) যা আগে এসেছে, সেটাই আগে গেছে ধরুন
COGS = 8 × 18,500 + 2 × 20,000 = 1,88,000
stock = 10 × 20,000 = 2,00,000
Weighted Average (moving) প্রতিটি receipt-এ নতুন গড়
গড় = (8 × 18,500 + 12 × 20,000) ÷ 20 = 3,88,000 ÷ 20 = 19,400
COGS = 10 × 19,400 = 1,94,000
stock = 10 × 19,400 = 1,94,000
Specific Identification প্রতিটি unit-এর নিজের cost (serial-ধরে)
গাড়ি, গয়না, serial-যুক্ত যন্ত্র — যেখানে কোনটা গেল সত্যিই জানা যায়
LIFO যা শেষে এসেছে সেটা আগে গেছে — IFRS-এ নিষিদ্ধ, এ বইয়ে নেইদুই পদ্ধতিতে COGS-এ ৬,০০০ পার্থক্য — একই মাল, একই বিক্রি। দাম বাড়তি বাজারে FIFO-তে COGS কম, লাভ বেশি, stock-এর মূল্য বাজারের কাছাকাছি। Average-এ সব মসৃণ। কোনটা “সঠিক” — কোনোটাই না; সঠিক হলো একবার বেছে সারাজীবন মানা। Software-এ পদ্ধতিটা items.costing_method — আর বদলাতে চাইলে একটা migration, একটা recost, একটা audit note (অধ্যায় ৬২)।
Developer-এর দৃষ্টিতে: FIFO মানে layer — প্রতিটি receipt একটা স্তর (qty, unit_cost), issue হলে পুরনো স্তর থেকে কাটে। Average মানে একটা সংখ্যা — balance value ÷ balance qty, প্রতিটি receipt-এ নতুন করে। FIFO-তে state বেশি (layers), কিন্তু প্রতিটি টাকার উৎস খুঁজে বের করা যায়; average-এ state কম, কিন্তু rounding নিয়ে সারাজীবন লড়াই। Section ৫-এ দুটোই।
ছয় প্রকার movement
Inventory-র প্রতিটি ঘটনা একটা stock movement — quantity-র দিক (+/−) আর একটা cost:
প্রকার qty cost কোথা থেকে GL-এর অন্য পাশ
───── ─── ────────────── ──────────────
receipt + bill / landed cost 2110 A/P (অধ্যায় ১৫; GRNI — অধ্যায় ২৮)
issue − costing engine (FIFO / avg) 5140 COGS
return in + মূল issue-র cost 5140 COGS (উল্টো) ← credit note
return out − মূল receipt-এর layer 2110 A/P ← debit note
adjustment ± গণনা: চলতি cost 5150 Inventory Loss / Gain
transfer −/+ একই cost, দুই warehouse 1145 ↔ 1145 (P&L স্পর্শ নেই)
write-down 0 NRV − cost 5150 / 1146 Allowance (contra)শেষ সারিটা আলাদা: quantity বদলায় না, শুধু মূল্য কমে। প্রতিটি movement-এর একটা GL line আছে 1145-এ, আর 1145-এর প্রতিটি line-এর একটা movement — এটাই subledger আর control account-এর সম্পর্ক, অধ্যায় ৯-এর নিয়ম (“control account-এ হাতে entry নয়”) এখানে সবচেয়ে কঠোর।
Subledger — item × warehouse
A/R-এর subledger ছিল গ্রাহক-ধরে (অধ্যায় ১৪-এর তিন স্তর)। Inventory-র subledger item × warehouse, আর প্রতিটি ঘরে দুটো সংখ্যা:
স্তর ১ GL 1145 Inventory 1,98,800 Dr
স্তর ২ Subledger item PT-200 @ Dhaka qty 10 value 1,98,800
item PT-200 @ Ctg qty 0 value 0
স্তর ৩ Layers / mov. layer 15-07 qty 9 × 20,000; layer 24-07 qty 1 × 18,800নিয়ম: GL 1145 == Σ subledger value == Σ layers (FIFO)। Quantity GL-এ নেই — GL শুধু টাকা জানে। তাই quantity ledger আর value ledger এক জায়গায় — একটা stock_movements table, প্রতিটি সারিতে qty আর value দুটোই। দুটো আলাদা system (inventory module-এ qty, accounting module-এ value) রাখলে ছয় মাসে দুটো আলাদা সত্য হবে, আর কোনটা ঠিক কেউ জানবে না — section ৭-এর প্রথম সারি।
Return — কোন cost-এ ফেরে
গ্রাহক ১টা terminal ফেরত দিল (অধ্যায় ১৯-এর credit note)। আয় উল্টে যায় (Dr 4180)। Stock-ও ফেরে — কিন্তু কত টাকায়? আজকের cost নয়; যে cost-এ গিয়েছিল, সেই cost-এ:
বিক্রির সময় COGS 1,88,000 ÷ 10 = 18,800 প্রতি unit (FIFO, ওই issue-র গড়)
ফেরত Dr 1145 Inventory 18,800
Cr 5140 COGS 18,800আজকের layer-এর দাম (২০,০০০) ধরলে ফেরত আসা মাল “মূল্য অর্জন” করত — একটা return দিয়ে লাভ বানানো যেত। তাই issue-র সময় কোন layer থেকে কত কাটা হলো সেটা রাখতে হয় (layer_consumptions) — return-এর সময় সেখানে ফেরা যায়।
Count, shrinkage, NRV
মাস শেষে গুদাম গোনা হয়। খাতায় ১১, হাতে ১০ — একটা নেই (চুরি, ভাঙা, ভুল delivery)। খাতাকে বাস্তবে নামান:
Dr 5150 Inventory Loss & Write-down 20,000 ← যে layer থেকে কাটল, তার cost
Cr 1145 Inventory 20,000আর দাম পড়ে গেলে? নতুন model এসেছে, পুরনোটা এখন ১৯,০০০-এর বেশি বিক্রি হবে না, বিক্রিতে ৫০০ খরচ। Net Realisable Value (NRV) = ১৯,০০০ − ৫০০ = ১৮,৫০০। নিয়ম: inventory-র মূল্য cost আর NRV-র মধ্যে যেটা কম (lower of cost and NRV)। Cost ২০,০০০ > NRV ১৮,৫০০ → প্রতি unit ১,৫০০ কমাতে হবে:
Dr 5150 Inventory Loss & Write-down 13,800
Cr 1146 Allowance for Inventory Write-down 13,800 ← contra asset, 1515-এর মতোLayer-এর cost বদলান না — allowance-এ রাখুন। কারণ দাম আবার উঠলে write-down ফেরানো যায় (cost পর্যন্ত), আর layer অক্ষত থাকলে “আসল cost কত ছিল” প্রশ্নের উত্তর থাকে।
Backdating — এই অধ্যায়ের engineering সমস্যা
০৮-০৭-এ ১২টা বিক্রি হলো, COGS ১৮,৫০০ করে। ১২-০৭-এ জানা গেল — ০৫-০৭ তারিখের একটা freight bill ৪,০০০ post হয়নি। সেটা post করলে ০২-০৭-এর receipt-এর landed cost বদলায় (১৮,৫০০ → ১৮,৭০০), আর ০৮-০৭-এর COGS-ও বদলায় — যা ইতিমধ্যে post হয়ে গেছে। A/R বা A/P-তে এ সমস্যা ছিল না: invoice-এর অঙ্ক invoice-এই থাকে, পরের ঘটনা তাকে বদলায় না। Inventory-তে প্রতিটি issue-র cost তার আগের সব receipt-এর function — তাই আগের একটা receipt বদলালে পরের সব issue-র cost আবার হিসাব:
recost(item, warehouse, from_date):
from_date থেকে সব movement (movement_date, seq) ক্রমে আবার চালান
যার cost বদলাল → পার্থক্যের একটা adjusting entry (খোলা period-এ)কতটা ভয়ানক? একটা receipt-এর পরে হাজার issue থাকলে হাজার entry — না; পার্থক্যগুলো একসাথে, একটা entry, narration-এ কারণ। আর period বন্ধ হলে পার্থক্য খোলা period-এ — অধ্যায় ১৮-এর DESCO-র নিয়ম। Section ৫-এ পুরো algorithm। এখন শুধু নীতিটা: movement-এর ক্রম তারিখ দিয়ে, entry-র ক্রম দিয়ে নয় — আর সেই ক্রম ভাঙলে recost।
যা এই অধ্যায়ে নেই
PO → GRN → bill, GRNI, three-way match অধ্যায় ২৮
Manufacturing: raw material → WIP → finished goods অধ্যায় ৫৮
Standard cost, variance অধ্যায় ৫৭
Multi-currency receipt অধ্যায় ৫৪
Opening stock import অধ্যায় ৬২
Batch / expiry / serial tracking-এর নকশা এখানে শুধু cost-এর অংশটুকু৩. Accounting Rule
Entry
| ঘটনা | Debit | Credit |
|---|---|---|
| Receipt (bill) | Inventory (net), VAT Receivable | A/P [supplier] |
| Landed cost (freight, শুল্ক) | Inventory | A/P [transporter] / Bank |
| Issue (বিক্রি) — আয় | A/R [গ্রাহক] | Product Sales, VAT Payable |
| Issue (বিক্রি) — খরচ | COGS | Inventory |
| Return in (credit note) | Sales Returns, VAT Payable; Inventory | A/R; COGS |
| Return out (debit note) | A/P [supplier] | Inventory, VAT Receivable |
| Count: কম | Inventory Loss | Inventory |
| Count: বেশি | Inventory | Inventory Loss (Gain) |
| Transfer | Inventory [to warehouse] | Inventory [from warehouse] |
| NRV write-down | Inventory Loss & Write-down | Allowance for Inventory Write-down (contra) |
| Write-down ফেরানো | Allowance | Inventory Loss & Write-down |
Cost
cost = supplier দাম (trade discount বাদে) + freight + শুল্ক + আনার handling
নয় = claimable VAT, গুদামভাড়া, বিক্রয় খরচ, settlement discount
unit cost = Σ cost ÷ qty; DECIMAL(18,4) — গোল করবেন নাValuation
FIFO receipt = layer; issue পুরনো layer থেকে; stock = Σ layer qty × layer cost
Average প্রতিটি receipt-এ avg = (value + receipt value) ÷ (qty + receipt qty); issue = qty × avg
Specific serial-ধরে, প্রতিটি unit-এর নিজের cost
পদ্ধতি item-ধরে, একবার; বদল = migration + recost + noteReturn
return in → মূল issue-র cost (layer_consumptions থেকে), আজকের cost নয়
return out → মূল receipt-এর layer; সেই layer থেকে qty কমেLower of cost and NRV
NRV = প্রত্যাশিত বিক্রয়মূল্য − বিক্রি সম্পন্ন করার খরচ
value = min(cost, NRV), item-ধরে; write-down allowance-এ (1146), layer অক্ষতSubledger == GL
GL 1145 == Σ item_warehouse_balances.value == Σ signed movement value == Σ layer (qty_remaining × unit_cost)
1145-এ হাতে journal নয়; প্রতিটি line-এর source একটা movementঅলঙ্ঘনীয়
Issue-র cost ব্যবহারকারী লেখেন না — engine হিসাব করে।
Movement-এর ক্রম তারিখ দিয়ে; ক্রম ভাঙলে recost।৪. Real Business Example
জুলাই ২০২৬ — নতুন বছর, নতুন ব্যবসা
একই software company, FY2027-এর প্রথম মাস (অধ্যায় ২৬-এর জুলাই)। Retail গ্রাহকদের জন্য যে POS software বানায়, তার সাথে এবার POS terminal-ও বিক্রি করবে — একটা trading line। প্রথমবার এই বইয়ে মাল আছে। COA-তে ছয়টা account যোগ হলো — অধ্যায় ৫-এর ফাঁকগুলো এখানে কাজে লাগল:
1145 Inventory ASSET post ← control; 1140 Advance আর 1150 Prepaid-এর মাঝে
1146 Allowance for Inventory Write-down ASSET post ← contra
2115 Goods Received Not Invoiced LIAB. post ← অধ্যায় ২৮-এর জন্য রাখা, এ মাসে শূন্য
4140 Product Sales REVENUE post
5140 Cost of Goods Sold EXPENSE post ← Direct Cost group
5150 Inventory Loss & Write-down EXPENSE post ← Direct Cost groupএকটা item: PT-200 POS Terminal, warehouse: Dhaka। costing_method = fifo। প্রতিটি ঘটনায় দুটো প্রশ্ন: qty কত বদলাল? আর value কত — কোথা থেকে? পাশাপাশি average-এ কী হতো, তা-ও দেখাব।
০২-০৭ — টেক সাপ্লাই থেকে ২০টা PT-200, ১৮,০০০ করে + VAT
qty +20
value 3,60,000 ← bill থেকে (অধ্যায় ১৫-এর bill, শুধু Dr 1145)
PB-07-0001 (supplier bill "TS/2026/0417")
Dr 1145 Inventory 3,60,000
Dr 1160 VAT Receivable 54,000
Cr 2110 A/P [টেক সাপ্লাই] 4,14,000
movement #1 receipt +20 unit 18,000 value 3,60,000 je = PB-07-0001
layer L1 qty 20 unit 18,000০৩-০৭ — Transport bill ১০,০০০, ওই ২০টা আনার (VAT নেই)
qty 0
value +10,000 ← landed cost, movement #1-এ যোগ
PB-07-0002 (transporter)
Dr 1145 Inventory 10,000
Cr 2110 A/P [দ্রুত পরিবহন] 10,000
movement #2 landed_cost qty 0 value 10,000 → L1 unit 18,000 → 18,500
layer L1 qty 20 unit 18,500 (3,70,000)
avg 3,70,000 ÷ 20 = 18,500Freight-টা 5260-এ গেলে জুলাইয়ের খরচ ১০,০০০ বেশি, আর প্রতিটি terminal-এর margin ৫০০ বেশি দেখাত।
০৮-০৭ — ABC Ltd-কে ১২টা, ২৬,০০০ করে + VAT
qty −12
value engine: FIFO L1 থেকে 12 × 18,500 = 2,22,000
avg 12 × 18,500 = 2,22,000 (এখনো একটাই দাম — একই)
SI-07-0001
Dr 1130 A/R [ABC Ltd] 3,58,800
Cr 4140 Product Sales 3,12,000
Cr 2140 VAT Payable 46,800
JV-07-0003 source = stock_movement #3
Dr 5140 Cost of Goods Sold 2,22,000
Cr 1145 Inventory 2,22,000
movement #3 issue −12 unit 18,500 value 2,22,000 je = JV-07-0003
consumption #3 ← L1 qty 12 @ 18,500
layer L1 qty 8 unit 18,500 (1,48,000)Gross profit এই বিক্রিতে ৩,১২,০০০ − ২,২২,০০০ = ৯০,০০০ (২৮.৮%)। Invoice-এর অঙ্ক ব্যবহারকারী লিখেছেন; COGS-এর অঙ্ক কেউ লেখেননি।
১৫-০৭ — আরও ১২টা, দাম বেড়ে ২০,০০০ + VAT; supplier নিজে পৌঁছে দিল (freight নেই)
qty +12
value 2,40,000
PB-07-0004
Dr 1145 Inventory 2,40,000
Dr 1160 VAT Receivable 36,000
Cr 2110 A/P [টেক সাপ্লাই] 2,76,000
movement #4 receipt +12 unit 20,000 value 2,40,000
layer L2 qty 12 unit 20,000
stock FIFO: L1 8 × 18,500 + L2 12 × 20,000 = 3,88,000 (20 units)
avg: 3,88,000 ÷ 20 = 19,400২০-০৭ — নদী গ্রুপকে ১০টা, ২৬,০০০ করে + VAT
qty −10
value engine: FIFO L1 8 × 18,500 + L2 2 × 20,000 = 1,48,000 + 40,000 = 1,88,000
avg 10 × 19,400 = 1,94,000
SI-07-0002
Dr 1130 A/R [নদী গ্রুপ] 2,99,000
Cr 4140 Product Sales 2,60,000
Cr 2140 VAT Payable 39,000
JV-07-0006 source = stock_movement #5
Dr 5140 Cost of Goods Sold 1,88,000
Cr 1145 Inventory 1,88,000
movement #5 issue −10 unit 18,800 (গড়) value 1,88,000
consumption #5 ← L1 qty 8 @ 18,500; #5 ← L2 qty 2 @ 20,000
layer L1 qty 0 (শেষ)
layer L2 qty 10 unit 20,000 (2,00,000)এখানেই দুই পদ্ধতি আলাদা হলো — ৬,০০০। একই ১০টা terminal, একই দামে বিক্রি; FIFO-তে লাভ ৭২,০০০, average-এ ৬৬,০০০। কোনোটা ভুল নয়।
২৪-০৭ — নদী গ্রুপ ১টা ফেরত দিল, অক্ষত (ভুল model order করেছিল)
qty +1
value মূল issue #5-এর গড়: 1,88,000 ÷ 10 = 18,800 (avg হলে 19,400)
আজকের L2-র 20,000 নয়
CN-07-0001 (অধ্যায় ১৯)
Dr 4180 Sales Returns & Allowances 26,000
Dr 2140 VAT Payable 3,900
Cr 1130 A/R [নদী গ্রুপ] 29,900
JV-07-0008 source = stock_movement #6
Dr 1145 Inventory 18,800
Cr 5140 Cost of Goods Sold 18,800
movement #6 return_in +1 unit 18,800 value 18,800 ref movement #5
layer L3 qty 1 unit 18,800 (return layer, তারিখ 24-07)
stock FIFO: L2 10 × 20,000 + L3 1 × 18,800 = 2,18,800 (11 units)
avg: 11 × 19,400 = 2,13,400৩১-০৭ — গণনা: খাতায় ১১, গুদামে ১০
qty −1
value FIFO: পুরনো layer L2 থেকে 20,000 avg: 19,400
JV-07-0011 source = stock_movement #7
Dr 5150 Inventory Loss & Write-down 20,000
Cr 1145 Inventory 20,000
movement #7 adjustment −1 unit 20,000 value 20,000 ref count CT-07-0001
layer L2 qty 9
stock FIFO: L2 9 × 20,000 + L3 1 × 18,800 = 1,98,800 (10 units)
avg: 10 × 19,400 = 1,94,000COGS-এ নয় — 5150-এ। “বিক্রি করে কত পেলাম” আর “গুদামে কত হারালাম” এক লাইনে মিশলে gross margin-এর মানে থাকে না।
৩১-০৭ — NRV: নতুন model ঘোষণা; PT-200 এখন বড়জোর ১৯,০০০-এ যাবে, বিক্রিতে ৫০০ খরচ
NRV = 19,000 − 500 = 18,500
FIFO: L2 9 × (20,000 − 18,500) = 13,500; L3 1 × (18,800 − 18,500) = 300; মোট 13,800
avg: 10 × (19,400 − 18,500) = 9,000
JV-07-0012 source = stock_movement #8 (write-down)
Dr 5150 Inventory Loss & Write-down 13,800
Cr 1146 Allowance for Inventory Write-down 13,800
movement #8 writedown qty 0 value 0 (1145 অক্ষত) allowance 13,800
stock @ cost 1,98,800; allowance (13,800); net 1,85,000 = 10 × 18,500Average-এ-ও net ১,৯৪,০০০ − ৯,০০০ = ১,৮৫,০০০ — NRV-র মেঝে দুই পদ্ধতিকে এক জায়গায় নামিয়ে আনল।
Stock Ledger — PT-200 @ Dhaka, জুলাই ২০২৬ (FIFO)
তারিখ # প্রকার In Out Bal Unit Value In Value Out Bal Value JE
───── ─ ────── ── ─── ─── ──── ──────── ───────── ───────── ──
02-07 1 receipt 20 20 18,000 3,60,000 3,60,000 PB-07-0001
03-07 2 landed cost 20 +500 10,000 3,70,000 PB-07-0002
08-07 3 issue 12 8 18,500 2,22,000 1,48,000 JV-07-0003
15-07 4 receipt 12 20 20,000 2,40,000 3,88,000 PB-07-0004
20-07 5 issue 10 10 18,800* 1,88,000 2,00,000 JV-07-0006
24-07 6 return in 1 11 18,800 18,800 2,18,800 JV-07-0008
31-07 7 adjustment 1 10 20,000 20,000 1,98,800 JV-07-0011
─── ─── ─── ──────── ───────── ─────────
33 23 10 6,28,800 4,30,000 1,98,800
* গড়: 8 × 18,500 + 2 × 20,000
write-down (#8): allowance 13,800 → net 1,85,000দুই পদ্ধতি পাশাপাশি
FIFO Average
───────────────────────── ──────── ────────
Purchases (landed) 6,10,000 6,10,000
COGS (নিট, return বাদে) 3,91,200 3,96,600
Shrinkage (5150) 20,000 19,400
Closing stock @ cost 1,98,800 1,94,000
NRV write-down (5150) 13,800 9,000
Closing stock, net 1,85,000 1,85,000
───────────────────────── ──────── ────────
Σ খরচ (5140 + 5150) 4,25,000 4,25,000শেষ সারিটা দেখুন: মোট খরচ পদ্ধতি-নিরপেক্ষ — কারণ দুটোই একই মেঝেতে (NRV) নেমেছে; নইলে ৬,০০০ পার্থক্য closing stock-এ থাকত আর পরের মাসে খরচ হতো। পদ্ধতি ঠিক করে কোন মাসে আর কোন লাইনে (COGS না loss), মোট নয়। Periodic সূত্র দিয়ে যাচাই: ০ + ৬,১০,০০০ − ১,৮৫,০০০ = ৪,২৫,০০০ ✓।
GL 1145 — জুলাই
1145 Inventory
─────────────────────────────────────────────────────────────
02-07 PB-07-0001 receipt #1 3,60,000
03-07 PB-07-0002 landed cost #2 10,000
08-07 JV-07-0003 issue #3 2,22,000
15-07 PB-07-0004 receipt #4 2,40,000
20-07 JV-07-0006 issue #5 1,88,000
24-07 JV-07-0008 return in #6 18,800
31-07 JV-07-0011 adjustment #7 20,000
───────── ─────────
6,28,800 4,30,000 Bal 1,98,800 Dr
subledger PT-200 @ Dhaka value 1,98,800 ✓ layers 9 × 20,000 + 1 × 18,800 = 1,98,800 ✓সাতটা line, সাতটা movement, এক-এক করে। কোনো JV হাতে লেখা নয়।
৫. Implementation — Software ও Database
Item master — নীতি এখানে
items (অধ্যায় ১৩, ১৫-এর table, বাড়ানো)
id, company_id, sku, name, uom
is_stock_item BOOLEAN -- false হলে অধ্যায় ১৫-এর পথ (expense / asset), movement নেই
costing_method VARCHAR(10) -- fifo | avg | specific; বদল = migration + recost
inventory_account_id BIGINT FK -- 1145
cogs_account_id BIGINT FK -- 5140
adjustment_account_id BIGINT FK -- 5150
revenue_account_id BIGINT FK -- 4140
allow_negative_stock BOOLEAN DEFAULT falseAccount-গুলো item-এ, movement-এ copy — অধ্যায় ১৩-এর revenue_account_id-র মতোই। Item group-ধরে default দিন, item-এ override।
stock_movements — একটাই সত্য
stock_movements
id BIGINT PK
company_id BIGINT FK
item_id BIGINT FK
warehouse_id BIGINT FK
movement_date DATE -- ঘটনার তারিখ; ক্রমের ভিত্তি
seq INT -- একই দিনে ক্রম
posting_date DATE -- GL-এর তারিখ; সাধারণত = movement_date; period বন্ধ হলে খোলা period
kind VARCHAR(15) -- receipt | issue | return_in | return_out | adjustment |
-- transfer_out | transfer_in | landed_cost | writedown
direction SMALLINT -- +1 | −1 | 0 (landed_cost, writedown)
quantity DECIMAL(18,4) -- সবসময় ≥ 0; দিক direction-এ
unit_cost DECIMAL(18,6) -- issue-তে engine লেখে; receipt-এ bill থেকে
total_cost DECIMAL(18,4) -- quantity × unit_cost (landed_cost: শুধু অঙ্ক)
source_type VARCHAR(30) -- purchase_invoice | sales_invoice | credit_note | debit_note |
-- stock_count | stock_transfer | landed_cost_bill | writedown
source_id BIGINT
ref_movement_id BIGINT FK NULL -- return_in → মূল issue; landed_cost → receipt; transfer_in → transfer_out
journal_entry_id BIGINT FK NULL -- receipt: bill-এর entry; বাকি: নিজের entry
cost_status VARCHAR(12) -- final | provisional (negative stock) | recosted
created_at, created_by
UNIQUE (company_id, item_id, warehouse_id, movement_date, seq)
INDEX (company_id, item_id, warehouse_id, movement_date, seq) ← recost এই ক্রমে পড়ে
INDEX (company_id, source_type, source_id)
INDEX (company_id, journal_entry_id)quantity ঋণাত্মক নয় — দিক আলাদা column-এ (অধ্যায় ৭-এর debit/credit-এর মতো)। movement_date আর posting_date আলাদা — অধ্যায় ১৫-এর তিন তারিখের প্রতিধ্বনি।
cost_layers ও layer_consumptions — FIFO-র স্মৃতি
cost_layers layer_consumptions
id id
item_id, warehouse_id issue_movement_id FK stock_movements -- কে নিল
receipt_movement_id FK stock_movements layer_id FK cost_layers -- কোথা থেকে
layer_date, seq -- receipt-এর quantity DECIMAL(18,4)
unit_cost DECIMAL(18,6) unit_cost DECIMAL(18,6) -- সেই সময়ের layer cost
qty_in DECIMAL(18,4)
qty_remaining DECIMAL(18,4) UNIQUE (issue_movement_id, layer_id)
CHECK (qty_remaining BETWEEN 0 AND qty_in)
INDEX (item_id, warehouse_id, layer_date, seq) -- FIFO: এই ক্রমে খায়layer_consumptions ছাড়া return-at-original-cost অসম্ভব, আর “এই COGS-এর ১,৮৮,০০০ কোথা থেকে এল” প্রশ্নের উত্তর নেই। Average পদ্ধতিতে layer লাগে না — কিন্তু table-টা রাখুন; একই engine দুই পদ্ধতি চালাবে।
item_warehouse_balances — cache, সত্য নয়
item_warehouse_balances
item_id, warehouse_id PK
qty_on_hand DECIMAL(18,4)
value_on_hand DECIMAL(18,4)
avg_cost DECIMAL(18,6) -- value ÷ qty; qty 0 হলে শেষ avg
last_movement_id BIGINT
version INT -- optimistic lock (অধ্যায় ৪৭)অধ্যায় ৯-এর account_balances-এর মতো — movement থেকে rebuild করা যায়, তাই সত্য নয়, cache। রাতের যাচাই ৯৭ এটা প্রমাণ করে।
Costing engine — দুই পদ্ধতি, এক interface
receive(m): -- kind ∈ (receipt, return_in, transfer_in)
lock balance(item, wh) -- SELECT … FOR UPDATE
if item.costing_method == 'fifo':
insert cost_layer(receipt_movement_id = m.id, unit_cost = m.unit_cost,
qty_in = qty_remaining = m.quantity, layer_date = m.movement_date)
bal.value += m.total_cost; bal.qty += m.quantity
bal.avg = bal.qty > 0 ? bal.value ÷ bal.qty : bal.avg
m.cost_status = 'final'
issue(m): -- kind ∈ (issue, return_out, adjustment−, transfer_out)
lock balance(item, wh)
if bal.qty < m.quantity and not item.allow_negative_stock:
reject "stock নেই: আছে {bal.qty}, চাই {m.quantity}"
if item.costing_method == 'fifo':
need = m.quantity; total = 0
প্রতিটি layer (qty_remaining > 0) ORDER BY layer_date, seq:
take = min(need, layer.qty_remaining)
insert layer_consumption(m.id, layer.id, take, layer.unit_cost)
layer.qty_remaining −= take; total += take × layer.unit_cost; need −= take
if need == 0: break
if need > 0: -- negative stock, allow_negative_stock = true
total += need × bal.avg; m.cost_status = 'provisional'
else: -- avg
total = m.quantity × bal.avg
m.unit_cost = total ÷ m.quantity; m.total_cost = round(total, 4)
bal.value −= m.total_cost; bal.qty −= m.quantity
-- avg বদলায় না issue-তে; qty 0 হলে value-ও 0 করুন (rounding ধুলো ঝাড়া)
if bal.qty == 0: bal.value = 0 (পার্থক্য → 5150, সাধারণত কয়েক পয়সা)
returnIn(m): -- credit note থেকে
orig = movement(m.ref_movement_id) -- মূল issue
m.unit_cost = orig.unit_cost -- আজকের নয়
receive(m) -- FIFO: নতুন layer, cost = orig.unit_cost
landedCost(m): -- freight bill, receipt-এর পরে
r = movement(m.ref_movement_id) -- মূল receipt
r.total_cost += m.total_cost; r.unit_cost = r.total_cost ÷ r.quantity
if fifo: layer(r).unit_cost = r.unit_cost -- বাকি qty-র cost বাড়ল
bal.value += m.total_cost; bal.avg = bal.value ÷ bal.qty
if র receipt-এর কিছু ইতিমধ্যে issue হয়েছে:
recost(item, wh, from = r.movement_date) -- issue-গুলোর COGS বাড়বেlandedCost-এর শেষ লাইনটা section ২-এর ০৫-০৭ তারিখের সমস্যা। একটা সরল নীতি অনেকে নেন: ইতিমধ্যে issue হওয়া অংশের freight সরাসরি 5140-এ (recost ছাড়া) — ছোট অঙ্কে যথেষ্ট, আর সেটা company_settings.landed_cost_policy।
Ordering ও recost
insertMovement(m):
-- ক্রম: একই দিনের মধ্যে seq = max(seq) + 1
-- backdated? ওই item/wh-এ m.movement_date-এর পরে movement আছে?
later = EXISTS movement WHERE item, wh AND (movement_date, seq) > (m.movement_date, m.seq)
apply(m) -- receive / issue
if later: recost(item, wh, from = m.movement_date)
recost(item, wh, from):
lock balance(item, wh)
-- ১. from-এর আগের অবস্থা: layers ও balance from-এর ঠিক আগ পর্যন্ত rebuild
reset layers/balance to state before (from) -- consumptions from-এর পরের সব মুছে, qty_remaining ফিরিয়ে
-- ২. from থেকে সব movement ক্রমে আবার চালান
diffs = []
প্রতিটি m IN movements WHERE item, wh AND (movement_date, seq) >= from ORDER BY movement_date, seq:
old = m.total_cost
apply(m) -- unit_cost, total_cost নতুন করে
if m.total_cost != old:
diffs.append(m, m.total_cost − old); m.cost_status = 'recosted'
-- ৩. পার্থক্যের entry — একটা, খোলা period-এ
if diffs:
entry = JournalEntry(source = 'recost', posting_date = openPeriodDate(today),
narration = "recost {item} @ {wh} from {from}: {cause}")
প্রতিটি (m, d) in diffs:
entry.signedLine(m.counterpart_account, +d) -- 5140 / 5150 / 1145-অন্য wh
entry.signedLine(1145, −d)
entry.mergeSameAccountLines(); post(entry)
প্রতিটি m: m.recost_entry_id = entry.idতিনটি কথা:
১. recost item × warehouse-ধরে — একটা item-এর জন্য অন্যদের কিছু হয় না
২. পার্থক্যের entry-র তারিখ আজ (খোলা period); মূল issue-র entry অক্ষত — অধ্যায় ৪৩-এর নীতি:
post হওয়া entry বদলায় না, নতুন entry দিয়ে ঠিক হয়
৩. বড় গুদামে recost রাতে, queue-তে; দিনের বেলা backdated movement ঢুকলে item-টা "recost pending" —
সেই item-এর নতুন issue অপেক্ষা করে (নইলে provisional cost দিয়ে আবার recost)Negative stock — একটা নীতি, তিনটি রূপ
block issue reject — সবচেয়ে নিরাপদ; retail-এ অসহ্য (মাল এসেছে, GRN হয়নি, বিক্রি হচ্ছে)
allow issue চলে, cost = শেষ avg, cost_status = provisional; receipt এলে recost → final
allow + cap allow, কিন্তু ৭ দিনের বেশি provisional থাকলে সতর্কতা (যাচাই ৯৮)allow বাছলে recost বাধ্যতামূলক — নইলে provisional COGS চিরকালের হয়ে যায়।
GL posting — movement থেকে entry
Receipt-এর entry bill-ই বানায় (অধ্যায় ১৫-এর buildDocumentEntry(bill, −1), line-এর debit_account_id = 1145) — movement শুধু সেই entry-র id ধরে রাখে। বাকিগুলোর নিজের entry:
postMovement(m):
if m.kind IN (receipt, landed_cost): -- bill-এর entry-তে আছে
m.journal_entry_id = m.source.journal_entry_id; return
counterpart = switch m.kind:
issue → item.cogs_account_id (5140)
return_in → item.cogs_account_id (5140, উল্টো)
return_out → বাকি debit note-এর entry-তে (2110; অধ্যায় ১৯)
adjustment → item.adjustment_account_id (5150)
transfer_out → item.inventory_account_id (1145, to-warehouse dimension)
writedown → 1146 Allowance (1145 নয়)
inv = m.kind == 'writedown' ? 1146 : item.inventory_account_id
entry = JournalEntry(voucher_type = 'JV', posting_date = m.posting_date,
source_type = 'stock_movement', source_id = m.id,
idempotency_key = ('stock_movement', m.id))
entry.signedLine(inv, m.direction × m.total_cost, dims = {warehouse: m.warehouse_id, item: m.item_id})
entry.signedLine(counterpart, −m.direction × m.total_cost)
assert entry.balanced(); post(entry)
m.journal_entry_id = entry.idঅধ্যায় ১৫-এর signedLine আর direction — এখানেও। Transfer-এ দুটো movement (out, in) একটাই entry: Dr 1145 [Ctg] / Cr 1145 [Dhaka] — GL-এ শূন্য নড়াচড়া, warehouse dimension-এ দুই দিক। P&L স্পর্শ হয় না — transfer-এ কেউ লাভ করে না।
Sales invoice — দুটো entry, একটা transaction
postSalesInvoice(inv):
BEGIN
revenueEntry = buildDocumentEntry(inv, +1); post(revenueEntry) -- অধ্যায় ১৩
প্রতিটি line l WHERE l.item.is_stock_item:
m = insertMovement(kind = issue, item = l.item, wh = l.warehouse,
movement_date = inv.invoice_date, quantity = l.quantity,
source = ('sales_invoice', inv.id))
issue(m); postMovement(m)
COMMITএকটা database transaction — আয়ের entry আছে অথচ COGS-এর নেই, এমন মুহূর্ত কখনো দেখা যাবে না (অধ্যায় ৪৭)। কিন্তু দুটো আলাদা entry — কারণ:
source_type আলাদা → 1145-এর প্রতিটি line-এর উৎস একটা movement (যাচাই ১০০)
recost → COGS বদলালে আয়ের entry অক্ষত
credit note → আয় ফেরে, stock না-ও ফিরতে পারে (ভাঙা মাল) — দুটো আলাদা সিদ্ধান্ত
reporting → "invoice-এর entry" আর "cost-এর entry" আলাদা খোঁজা যায়Physical count
stock_counts: id, warehouse_id, count_date, status (open | counted | posted), posted_entry_id
stock_count_lines: count_id, item_id, system_qty (freeze-এর সময়), counted_qty, variance_qty
postCount(c):
freeze: count চলাকালে ওই warehouse-এ movement block (নইলে system_qty নড়ে)
প্রতিটি line WHERE variance_qty != 0:
m = insertMovement(kind = adjustment, direction = sign(variance), quantity = |variance|,
movement_date = c.count_date, source = ('stock_count', c.id))
variance < 0 ? issue(m) : receive(m, unit_cost = bal.avg) -- বেশি পেলে avg-এ
postMovement(m) -- 5150Count-এর তারিখ মাসের শেষ দিন, entry-ও সেদিন — নইলে জুলাইয়ের loss আগস্টে।
NRV write-down
writedown(item, wh, nrv_unit, as_of):
bal = balance(item, wh); cost_unit = bal.value ÷ bal.qty
target_allowance = max(0, (cost_unit − nrv_unit) × bal.qty) -- FIFO: layer-ধরে যোগ
current = allowance(item, wh) -- 1146-এ item dimension
d = target_allowance − current
if d != 0: movement(kind = writedown, quantity = 0, total_cost = d); postMovement
-- d < 0 হলে reversal (দাম উঠেছে) — cost-এর উপরে কখনো নয়: target ≤ Σ costAllowance-টা 1146-এ, item/warehouse dimension সহ — যাতে item বিক্রি হয়ে গেলে তার allowance-ও ছাড়া যায় (issue-তে আনুপাতিক reversal, বা মাস শেষে পুরো recompute)।
API / Service
POST /inventory/movements {kind, item, warehouse, date, qty, unit_cost?, source}
→ engine cost দেয়; unit_cost শুধু receipt-এ গ্রহণযোগ্য
GET /inventory/balances?warehouse=&as_of= → subledger (qty, value, avg)
GET /inventory/items/{id}/ledger?wh=&from=&to= → stock ledger (section ৪-এর table)
GET /inventory/items/{id}/layers?wh= → FIFO layers, qty_remaining
POST /inventory/counts … /post → count → adjustment movements
POST /inventory/recost {item, warehouse, from} → job; admin only
GET /inventory/reconciliation?as_of= → যাচাই ৯৬–১০১-এর ফলPOST /movements-এ unit_cost issue-তে এলে reject — client cost পাঠাতে পারবে না, এটাই section ৩-এর অলঙ্ঘনীয়। Sales invoice আর purchase bill নিজেরা এই endpoint ডাকে না — service-এর ভিতরে issue()/receive() — একই transaction-এ।
রাতের যাচাই
৯৬. Σ item_warehouse_balances.value_on_hand == closing(1145) — warehouse dimension-ধরেও
৯৭. প্রতিটি item × wh: qty_on_hand == Σ direction × quantity; value_on_hand == Σ direction × total_cost;
FIFO হলে == Σ layer.qty_remaining × unit_cost; periodic সূত্র: opening + receipts − issues == closing
৯৮. qty_on_hand < 0 যেখানে allow_negative_stock = false → ০টি; provisional > ৭ দিন → সতর্কতা
৯৯. movement যার journal_entry_id IS NULL এবং posting_date ≤ আজ → ০টি
১০০. 1145-এ posted line যার source_type ∉ (stock_movement, purchase_invoice, debit_note, recost, opening_balance) → ০টি
১০১. 1146: balance ≤ 0; |1146| ≤ 1145 (item-ধরে); NRV < cost অথচ allowance 0 → সতর্কতা৯৬ ব্যর্থ হলে প্রথম সন্দেহ ১০০ — কেউ 1145-এ JV লিখেছে। ৯৭ ব্যর্থ হলে cache rebuild, তারপর কোন movement-এ apply ভুল হয়েছিল খোঁজা।
Test হিসেবে
test "FIFO: দুই layer থেকে issue":
receive(20 @ 18,500); receive(12 @ 20,000); issue(12)
m = issue(10)
assert m.total_cost == 1,88,000
assert consumptions(m) == [(L1, 8, 18,500), (L2, 2, 20,000)]
assert layer(L1).qty_remaining == 0 and layer(L2).qty_remaining == 10
test "avg: একই data":
item.costing_method = avg; (একই ক্রম)
assert issue(10).total_cost == 1,94,000
assert bal.avg == 19,400
test "return in মূল cost-এ, আজকের নয়":
m5 = issue(10) -- unit 18,800
r = returnIn(1, ref = m5)
assert r.unit_cost == 18,800 -- L2-র 20,000 নয়
assert Δ 5140 == −18,800
test "sales invoice: আয় ও COGS একই transaction":
inv = postSalesInvoice(12 × PT-200 @ 26,000)
assert entries(source = sales_invoice, inv.id).count == 1
assert entries(source = stock_movement).count == 1
assert Δ 4140 == +3,12,000 and Δ 5140 == +2,22,000 and Δ 1145 == −2,22,000
-- failure injection: postMovement throw → revenue entry-ও নেই
test "landed cost পরে এলে unit cost বাড়ে, issue হয়ে থাকলে recost":
receive(20 @ 18,000); issue(12) -- COGS 2,16,000
landedCost(10,000, ref = receipt)
assert layer.unit_cost == 18,500
assert recost entry: Dr 5140 6,000 / Cr 1145 6,000 -- 12 × 500
assert movement(issue).cost_status == 'recosted'
test "backdated receipt → পরের issue-র cost বদলায়, entry খোলা period-এ":
receive(10 @ 20,000, 15-07); issue(10, 20-07) -- COGS 2,00,000
receive( 5 @ 18,000, 10-07) -- backdated
assert issue(20-07).total_cost == 5 × 18,000 + 5 × 20,000 == 1,90,000
assert recost entry Dr 1145 10,000 / Cr 5140 10,000, posting_date = আজ
test "negative stock: block":
receive(5); assert rejects issue(6)
test "negative stock: allow → provisional → receipt-এ final":
item.allow_negative_stock = true
m = issue(6); assert m.cost_status == 'provisional'
receive(10 @ 19,000, তারিখ আগের)
assert m.cost_status == 'recosted' and m.unit_cost == 19,000
test "count: কম হলে 5150, COGS নয়":
postCount(system 11, counted 10)
assert Δ 5150 == +20,000 and Δ 5140 == 0 and bal.qty == 10
test "NRV: allowance, layer অক্ষত, reversal cost-এর উপরে নয়":
writedown(nrv 18,500); assert Δ 1146 == −13,800 and layer(L2).unit_cost == 20,000
writedown(nrv 25,000); assert balance(1146) == 0 -- cost পর্যন্তই ফেরে
test "transfer P&L স্পর্শ করে না":
transfer(5, Dhaka → Ctg)
assert Δ (all 5xxx) == 0 and Δ 1145 == 0
assert bal(Dhaka).qty == 5 and bal(Ctg).qty == 5 and bal(Ctg).value == 5 × 20,000
test "client cost পাঠাতে পারে না":
assert rejects POST /movements {kind: issue, unit_cost: 1}
test "1145-এ হাতে JV নয়":
assert rejects post(JV(Dr 1145 / Cr 5250)) -- control account (অধ্যায় ৯)৬. Financial Statement Impact
INCOME STATEMENT (জুলাই ২০২৬, trading line)
4140 Product Sales 5,72,000
4180 Sales Returns & Allowances (26,000)
Net Revenue 5,46,000
5140 Cost of Goods Sold 3,91,200 ← 2,22,000 + 1,88,000 − 18,800
5150 Inventory Loss & Write-down 33,800 ← 20,000 + 13,800
Gross Profit 1,21,000 22.2% (loss-এর আগে 1,54,800 = 28.4%)
BALANCE SHEET (৩১ জুলাই ২০২৬, নড়াচড়া)
1145 Inventory +1,98,800
1146 Allowance for Inventory Write-down (13,800)
Inventory, net +1,85,000 ← একটা লাইন, note-এ দুটো
1130 Accounts Receivable +6,27,900 (3,58,800 + 2,99,000 − 29,900)
2110 Accounts Payable +7,00,000 (4,14,000 + 10,000 + 2,76,000)
1160 VAT Receivable +90,000
2140 VAT Payable +81,900
CASH FLOW (indirect, অধ্যায় ২২)
operating: লাভ থেকে −1,85,000 (increase in inventory) ← টাকা মালে আটকে আছে
(direct: এ মাসে কোনো টাকা যায়নি — সব A/P-তে)তিনটি নতুন কথা। Gross profit এবার সত্যিকারের অর্থ পেল — অধ্যায় ২০-এ service company-র gross profit ছিল “developer-এর বেতনের পরে”; এখানে “মালের দামের পরে” — আর 5140/5150 আলাদা রাখায় দুটো প্রশ্নের দুটো উত্তর: দাম ঠিক আছে (২৮.৪%), গুদাম ঠিক নেই (৩৩,৮০০ হারিয়েছে)। Balance Sheet-এ inventory net — 1146 আলাদা দেখানো হয় না, note-এ। Cash flow-এ inventory বাড়া মানে টাকা কমা — ১,২১,০০০ লাভ, অথচ ১,৮৫,০০০ মালে বসে আছে; অধ্যায় ২২-এর working capital adjustment-এর নতুন লাইন।
৭. Common Developer Mistakes
| ভুল | কী ঘটে | সঠিক পথ |
|---|---|---|
| Qty inventory module-এ, value accounting-এ — দুই table | ছয় মাসে দুটো সত্য, কোনটা ঠিক জানা নেই | একটা stock_movements, qty + value এক সারিতে |
| Sale-এ COGS ব্যবহারকারী type করেন | যা খুশি cost, margin কল্পনা | engine হিসাব করে; API-তে issue-র unit_cost reject |
| COGS = বিক্রয়মূল্য | gross profit শূন্য | cost layer / avg থেকে |
| ক্রয় সরাসরি 5140-এ (periodic-এর অভ্যাস) | stock নেই, মাসের লাভ ওঠানামা | receipt → 1145, issue → 5140 |
| Freight 5260-এ | stock কম, margin মিথ্যা | landed cost → receipt-এ যোগ |
| VAT cost-এ | stock ফোলে, 1160 কম | claimable VAT 1160-এ (অধ্যায় ১৫) |
| Return in আজকের cost-এ | return দিয়ে লাভ তৈরি | layer_consumptions থেকে মূল cost |
| Backdated receipt, recost নেই | পরের সব COGS ভুল, চিরকাল | (movement_date, seq) ক্রম; recost job |
| Recost-এ মূল entry বদলানো | audit trail ভাঙে, বন্ধ period-এ হাত | পার্থক্যের নতুন entry, খোলা period-এ |
| Negative stock allow, provisional চিরকাল | COGS আন্দাজে | receipt-এ recost → final; ৭ দিনে সতর্কতা |
| Count-এর variance 5140-এ | চুরি “বিক্রির খরচ” | 5150 আলাদা |
| Count চলাকালে movement | system_qty নড়ে, variance ভুল | freeze |
| Write-down-এ layer cost কমানো | দাম উঠলে ফেরানো যায় না, মূল cost হারায় | 1146 allowance, layer অক্ষত |
| Transfer-এ 5140/5150 স্পর্শ | গুদাম বদলে লাভ-ক্ষতি | 1145 ↔ 1145, dimension |
| unit_cost DECIMAL(18,2) | avg-এ পয়সার ধুলো জমে, qty 0-তে value ≠ 0 | DECIMAL(18,6); qty 0 → value 0, ধুলো 5150-এ |
| Revenue আর COGS এক entry-তে | 1145-এর line-এর উৎস invoice, movement নয়; recost-এ আয়ের entry ছুঁতে হয় | দুটো entry, একটা transaction |
| 1145-এ হাতে JV | subledger ≠ GL, কে করেছে জানা নেই | control account, block (অধ্যায় ৯); যাচাই ১০০ |
| Balance cache-কে সত্য ধরা | movement আর cache আলাদা হলে cache জেতে | rebuild-able; যাচাই ৯৭ |
| Item-এ costing_method বদলে দেওয়া | পুরনো layer অনাথ, cost লাফায় | migration + recost + note |
প্রথম আর অষ্টম সবচেয়ে ব্যয়বহুল। প্রথমটা inventory system-এর মৃত্যু — দুটো সত্য মানে কোনো সত্য নেই, আর accountant প্রতি মাসে “কোনটা ঠিক” জিজ্ঞেস করে করে একদিন Excel-এ ফিরে যান। অষ্টমটা নীরব — একটা পুরনো তারিখের bill, কেউ recost করেনি, ছয় মাসের gross margin কয়েক শতাংশ ভুল, কারও চোখে পড়ে না।
৮. Exercises
সেট ক — entry লিখুন (item stock, VAT ১৫%)
১। ৫০টা keyboard, ১,২০০ করে + VAT, বাকিতে। পরদিন courier ২,৫০০ (VAT নেই)।
unit cost কত? দুটো entry।
২। ওই keyboard ৩০টা বিক্রি, ২,০০০ করে + VAT, বাকিতে। আয়ের entry আর COGS-এর entry।
৩। গ্রাহক ৩টা ফেরত দিল, অক্ষত। Credit note-এর entry আর stock-এর entry। cost কত?
৪। ৫টা keyboard ত্রুটিপূর্ণ, supplier-কে ফেরত; supplier credit দিল। Debit note-এর
entry (VAT সহ)। কোন layer থেকে কমবে?
৫। গণনায় ২টা কম। entry। 5140 না 5150 — কেন?
৬। Dhaka থেকে Chittagong-এ ১০টা transfer। entry। P&L-এ কী?
৭। Keyboard-এর নতুন model; পুরনোটা ১,০০০-এর বেশি যাবে না, বিক্রিতে ৫০।
stock ১০টা, cost ১,২৫০। write-down কত? entry। পরের মাসে দাম ১,৪০০ হলে?সেট খ — FIFO বনাম Average
৮। receipts: ১০ @ 100 (01-07), ১০ @ 120 (10-07), ১০ @ 140 (20-07)
issues: ১৫ (15-07), ১০ (25-07)
FIFO-তে দুটো issue-র COGS, closing stock। Average-এ (moving)। পার্থক্য কোথায় গেল?
৯। ৮ নম্বরে ০৫-০৭ তারিখে ৫ @ 90 একটা receipt পরে (২৮-০৭-এ) post হলো।
FIFO-তে কোন issue-র cost বদলাল, কত? Average-এ? recost entry লিখুন (তারিখ ২৮-০৭)।
১০। Average-এ: ৩ @ 10.00 → issue 1 → receipt 1 @ 10.01 → issue 3।
unit cost DECIMAL(18,2) হলে qty 0-তে value কত থাকে? DECIMAL(18,6) হলে?
সেই ধুলো কোথায় যাবে?
১১। Section ৪-এর জুলাইয়ে NRV write-down না থাকলে দুই পদ্ধতির Σ খরচ কত হতো?
পার্থক্য আগস্টে কোথায় দেখা দিত?সেট গ — কোনটা ভুল
১২। Dr 5140 COGS 3,60,000 / Dr 1160 54,000 / Cr A/P 4,14,000 — ২০টা terminal কেনা
১৩। Dr 5260 Travel 10,000 / Cr A/P 10,000 — ওই terminal-এর transport
১৪। Dr 1145 20,000 / Cr 5140 20,000 — গ্রাহক ফেরত, বিক্রির সময় cost ছিল 18,800
১৫। Dr 5140 20,000 / Cr 1145 20,000 — গণনায় ১টা কম
১৬। Dr 5150 13,800 / Cr 1145 13,800 — NRV write-down
১৭। Dr 1145 [Ctg] 1,00,000 / Cr 4140 1,00,000 — Dhaka → Ctg transfer
১৮। sales_invoice_lines.cogs_amount — ব্যবহারকারীর দেওয়া, form-এ
১৯। stock_movements-এ quantity ঋণাত্মক, direction column নেই
২০। একটা JV: Dr 1145 5,000 / Cr 5250 5,000, narration "stock ঠিক করা"সেট ঘ — নকশা
২১। Specific identification (serial-ধরে): schema-তে কী যোগ হবে? issue-তে
ব্যবহারকারী serial বাছেন — তাহলে "cost ব্যবহারকারী লেখেন না" নিয়মটা
কি ভাঙল?
২২। একটা item-এর costing_method fifo → avg বদলাতে হবে, ৩ বছরের data।
ধাপগুলো কী? কোন তারিখ থেকে? পুরনো layer-এর কী হবে? audit note-এ কী লিখবেন?
২৩। Recost-এ পার্থক্যের entry "আজ" post হয় — কিন্তু মূল issue ছিল বন্ধ period-এ।
ওই বন্ধ মাসের gross margin এখন ভুল। মেনে নেবেন? না নিলে বিকল্প কী?
(অধ্যায় ২৬-এর নিয়ম ২ মনে করুন)
২৪। Section ৪-এর জুলাইয়ে PT-200-এর সাথে যদি software license-ও একই invoice-এ
থাকত (is_stock_item = false): একটা invoice, কয়টা entry, কয়টা movement?
COGS-এ কী?
২৫। Multi-warehouse average: warehouse-ধরে আলাদা avg, না company-wide একটা?
transfer-এ কী হয় দুই ক্ষেত্রে? কোনটা বেছে নেবেন, কেন?উত্তর আছে Workbook-এর Answer Key, অধ্যায় ২৭-এ।
৯. Developer Challenge
একটি InventoryService নকশা করুন — costing engine, GL posting, আর reconciliation — Part 4-এর প্রথম project (Projects ও Exit Criteria: Inventory valuation + accounting posting + reconciliation)।
যা যা নকশা করবেন:
১. Schema —
stock_movements(qty + value এক সারিতে,movement_date+seqক্রম,direction,cost_status,ref_movement_id),cost_layers,layer_consumptions,item_warehouse_balances(cache,version)।items-এcosting_methodও চারটি account। Warehouse dimension GL line-এ।২.
receive()/issue()— FIFO ও average, একই interface;returnIn()মূল cost-এ;landedCost()receipt-এ যোগ, issue হয়ে থাকলে recost বা 5140 (নীতি)।DECIMAL(18,6), qty 0-তে value 0, ধুলো 5150-এ। Section ৫-এর তেরোটি test সবুজ।৩. Ordering ও
recost(item, wh, from)— backdated movement ঢুকলে স্বয়ংক্রিয়; পার্থক্য একটা entry-তে, খোলা period-এ, মূল entry অক্ষত; বড় গুদামে queue, “recost pending” item-এ নতুন issue-র আচরণ। ১,০০,০০০ movement-এর একটা item-এ recost < ৫ সেকেন্ড — কীভাবে (layers from-এর আগের snapshot?)।৪.
postMovement()— kind থেকে counterpart, receipt-এ bill-এর entry, transfer-এ 1145 ↔ 1145, writedown-এ 1146;idempotency_key = ('stock_movement', id)। Sales invoice: আয় + COGS দুটো entry, একটা transaction, failure injection test।৫. Negative stock নীতি — block / allow / allow+cap,
company_settings-এ; provisional → recosted → final-এর state machine; ৭ দিনের সতর্কতা।৬. Physical count — freeze,
stock_counts, variance → adjustment movement, count-এর তারিখে entry। Cycle count (প্রতিদিন কিছু item) হলে freeze কীভাবে সংকীর্ণ করবেন?৭. NRV — item-ধরে
nrv_unitকোথা থেকে (দাম তালিকা? শেষ বিক্রয়মূল্য?), allowance 1146 item dimension সহ, reversal cost-এর উপরে নয়, বিক্রি হওয়া unit-এর allowance ছাড়া।৮. Reconciliation — যাচাই ৯৬–১০১, আর একটা
/inventory/reconciliationপাতা: GL 1145 বনাম subledger বনাম layers, warehouse-ধরে, পার্থক্য থাকলে কোন movement/line অনাথ। অধ্যায় ১৪-এর তিন-মুখী reconciliation-এর inventory সংস্করণ।৯. অধ্যায় ২৮-এর জন্য এখন কী রেখে দেবেন — receipt-এর
source_type = goods_receipt, counterpart 2115 GRNI, bill-এ GRNI clear — যাতে schema ভাঙতে না হয়?৩ নম্বরটাই আসল পরীক্ষা। A/R, A/P, GL — এতদিন প্রতিটি entry-র অঙ্ক তার document-এ ছিল, আর একটা entry post হলে সে চিরকাল ঠিক। Inventory-তে একটা issue-র cost তার আগের সব receipt-এর function — তাই একটা backdated movement একশোটা entry-কে ভুল করে দিতে পারে। যদি recost ছাড়া engine বানান, ছয় মাস পরে কেউ একটা পুরনো freight bill post করবেন, আর সেদিন থেকে আপনার gross margin কল্পনা। Ordering তারিখে, recost স্বয়ংক্রিয়, পার্থক্য নতুন entry-তে — এটা পারলে অধ্যায় ৪৩-এর Reversal ও Correction আর অধ্যায় ৫৭-এর Cost Accounting আপনার কাছে পরিচিত মনে হবে।
১০. Summary Card
দুটো মুহূর্ত
কেনা Dr 1145 Inventory / Cr 2110 A/P asset; Income Statement স্পর্শ নেই
বিক্রি Dr 1130 A/R / Cr 4140 Sales, 2140 VAT আয় — invoice-এর অঙ্ক
Dr 5140 COGS / Cr 1145 Inventory খরচ — engine-এর অঙ্কCost
ঢোকে দাম + freight + শুল্ক + আনার handling → landed cost, receipt-এ যোগ
ঢোকে না claimable VAT, গুদামভাড়া, বিক্রয় খরচValuation
FIFO layer; issue পুরনো থেকে; layer_consumptions রাখুন
Average প্রতিটি receipt-এ avg = value ÷ qty; issue = qty × avg
item-ধরে একবার; বদল = migration + recost + note; LIFO নেইMovement
receipt + bill থেকে Cr 2110 return in + মূল issue-র cost Cr 5140
issue − engine Dr 5140 return out − মূল layer Dr 2110
adjust ± গণনা 5150 transfer −/+ একই cost 1145 ↔ 1145
writedown 0 NRV − cost Dr 5150 / Cr 1146 (allowance, layer অক্ষত)Subledger == GL
GL 1145 == Σ balances.value == Σ signed movement value == Σ layers
qty শুধু subledger-এ; 1145-এ হাতে JV নয়; প্রতিটি line ↔ একটা movementOrdering
(movement_date, seq) ক্রমে cost; backdated → recost(item, wh, from)
পার্থক্য → একটা নতুন entry, খোলা period-এ; মূল entry অক্ষতDeveloper checklist
□ stock_movements: qty + value এক সারিতে — একটাই সত্য
□ direction column, quantity ≥ 0; unit_cost DECIMAL(18,6)
□ items: costing_method, inventory/cogs/adjustment/revenue account
□ cost_layers + layer_consumptions (FIFO); balances = cache, rebuild-able
□ issue-র cost engine-এ; API-তে client unit_cost reject
□ landed cost receipt-এ; freight 5260-এ নয়
□ return in মূল cost-এ; return out মূল layer থেকে
□ (movement_date, seq) ক্রম; backdated → recost; পার্থক্য নতুন entry
□ negative stock নীতি; provisional → recosted; ৭ দিনে সতর্কতা
□ count: freeze, variance → 5150, count-এর তারিখে
□ NRV: 1146 allowance, layer অক্ষত, reversal ≤ cost
□ transfer: 1145 ↔ 1145, P&L নেই
□ sales invoice: আয় + COGS দুটো entry, একটা transaction
□ 1145 control: হাতে JV block
□ রাতে: ৯৬ GL == subledger, ৯৭ cache == movements == layers, ৯৮ negative/provisional,
৯৯ unposted movement, ১০০ 1145-এ অচেনা source, ১০১ allowanceপরবর্তী অধ্যায়
অধ্যায় ২৮ — Purchase ও GRNI: এই অধ্যায়ে মাল আর bill একই দিনে এসেছে — receipt-এর entry bill-ই বানিয়েছে। বাস্তবে মাল আগে আসে, bill পরে — কখনো দুই সপ্তাহ পরে, কখনো ভুল অঙ্কে। মাঝের সময়টায় stock গুদামে (1145 বাড়া দরকার), অথচ A/P-তে দেনা নেই (bill নেই)। সেই ফাঁকের account 2115 GRNI — Goods Received Not Invoiced, আর তাকে ঘিরে PO → GRN → bill-এর three-way match, দর-পার্থক্য (purchase price variance), আর মাস শেষে “মাল এসেছে, bill আসেনি” তালিকা। অধ্যায় ১৫-এর po_line_id আর এই অধ্যায়ের source_type = goods_receipt — দুটো রাখা ছিল; পরের অধ্যায়ে সেগুলো কাজে লাগবে।