Skip to Content
Go Realm v1 is released 🎉
Accountingবই (Print Edition)অধ্যায় ২৭: Inventory Accounting

অধ্যায় ২৭ — 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 / recost

Inventory দিয়ে শুরু কারণ এটা প্রথম 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

ঘটনাDebitCredit
Receipt (bill)Inventory (net), VAT ReceivableA/P [supplier]
Landed cost (freight, শুল্ক)InventoryA/P [transporter] / Bank
Issue (বিক্রি) — আয়A/R [গ্রাহক]Product Sales, VAT Payable
Issue (বিক্রি) — খরচCOGSInventory
Return in (credit note)Sales Returns, VAT Payable; InventoryA/R; COGS
Return out (debit note)A/P [supplier]Inventory, VAT Receivable
Count: কমInventory LossInventory
Count: বেশিInventoryInventory Loss (Gain)
TransferInventory [to warehouse]Inventory [from warehouse]
NRV write-downInventory Loss & Write-downAllowance for Inventory Write-down (contra)
Write-down ফেরানোAllowanceInventory 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 + note

Return

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,500

Freight-টা 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,000

COGS-এ নয় — 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,500

Average-এ-ও 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 false

Account-গুলো 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) -- 5150

Count-এর তারিখ মাসের শেষ দিন, 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 ≤ Σ cost

Allowance-টা 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 চলাকালে movementsystem_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 ≠ 0DECIMAL(18,6); qty 0 → value 0, ধুলো 5150-এ
Revenue আর COGS এক entry-তে1145-এর line-এর উৎস invoice, movement নয়; recost-এ আয়ের entry ছুঁতে হয়দুটো entry, একটা transaction
1145-এ হাতে JVsubledger ≠ 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 ↔ একটা movement

Ordering

(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 — দুটো রাখা ছিল; পরের অধ্যায়ে সেগুলো কাজে লাগবে।