Bỏ qua menu, tới nội dung
Thành viên The MONA Group
Thành viên The MONA Group
Sản phẩm và viết theo yêu cầu
1900 636 648Tư vấn miễn phí, 8h–22h
MONA OS 2.0Hệ thống quản trị MONA tự phát triển, nối 7 phần mềm nội bộ về một dữ liệu, có Gấu Quản Trị trả lời câu hỏi điều hành.7 phần mềm1 trục dữ liệu8 AI AgentXem MONA OS hoạt động
Dự án giáo dụcThe IELTS WorkshopPhần mềm quản lý chuỗi 11 cơ sở luyện thi, kèm app cho học viên20238 tháng12 ngườiXem hồ sơ dự án Xem thêm dự án Khoa Y
Dự án bán lẻHình minh hoạ gấu MONA bán trang sức tại quầy kính, cạnh màn hình bán hàng và tồn khoNgọc Thành JewelryPhần mềm quản lý chuỗi cửa hàng trang sức, tích hợp xuất hoá đơn20246 tháng7 ngườiXem hồ sơ dự án
Dự án sản xuấtHình minh hoạ gấu MONA hướng dẫn công nhân học bài trên phần mềm đào tạo trong phòng họp nhà máyPolytex Far Eastern Việt NamPhần mềm đào tạo công nhân viên chạy trong mạng nội bộ nhà máy20234 tháng8 ngườiXem hồ sơ dự án
Dự án nhân sựHình minh hoạ gấu MONA đứng lớp đào tạo cho bác sĩ, điều dưỡng bằng phần mềm học trực tuyếnBệnh viện Hoàn Mỹ Sài GònViệc đào tạo nhân sự chuyển từ Excel sang phần mềm MONA viết riêng20247 tuần13 moduleXem hồ sơ dự án
Sản phẩm có sẵnHình minh hoạ gấu MONA phục vụ bàn, khách quét mã QR trên bàn để gọi mónnhahang.aiGọi món tại bàn bằng mã QR, kết nối bếp, quản lý bàn, định lượng nguyên liệuNhà hàngQuán ănChuỗi ăn uốngXem nhahang.ai
Sản phẩm có sẵnMONA HouseTự lập hoá đơn tiền phòng, gửi mã QR qua Zalo, báo có khi khách chuyển khoảnNhà trọCăn hộ cho thuêXem MONA House
Sản phẩm có sẵnMONA NHTQĐặt hàng Taobao, 1688, Tmall, ví khách hàng và theo dõi vận đơn cho công ty nhập hàngTaobao1688TmallXem MONA NHTQ
Tư vấn miễn phí, 8h–22h mỗi ngày
Kiến thức phần mềm

Tính giá vốn xuất kho bình quân gia quyền trong phần mềm kho: phiếu nhập, trả hàng và làm tròn

Lê Minh NhựtCập nhật · Đăng · 22 phút đọc

Tính giá vốn xuất kho bình quân gia quyền trong phần mềm kho: phiếu nhập, trả hàng và làm tròn

Phần mềm kho tính giá vốn bình quân gia quyền đúng khi mỗi mặt hàng lưu số lượng tồn và tổng giá trị tồn bằng đồng nguyên, còn đơn giá luôn bằng giá trị chia số lượng và không được lưu dạng đã làm tròn. Phiếu xuất làm tròn đúng một lần, phiếu trả nhà cung cấp trừ theo giá nhập gốc, hàng khách trả cộng theo giá vốn của phiếu bán, và với bình quân sau mỗi lần nhập, phiếu nhập ghi lùi ngày (vào máy muộn nhưng mang ngày cũ) buộc tính lại mọi phiếu của mặt hàng từ ngày đó. Căn cứ là Chuẩn mực kế toán số 02 (VAS 02) đoạn 15, cho tính bình quân theo kỳ hoặc sau mỗi lần nhập, và Thông tư 99/2025/TT-BTC, có hiệu lực từ 01/01/2026 và thay Thông tư 200/2014/TT-BTC (tra ngày 03/11/2026); các con số trong bài là kết quả chạy mã Python 3.9 với module decimal.

Ví dụ xuyên suốt là một mặt hàng với sổ tháng 10 gồm 7 phiếu: 3 phiếu xuất, 2 phiếu nhập, 1 phiếu trả hàng nhà cung cấp và 1 phiếu khách trả hàng. Một phiếu nhập 100 cái, ghi bổ sung ngày 28/10 nhưng thuộc ngày 06/10, làm đổi giá trị của 4 trong 7 dòng đã có và kéo tổng giá vốn tháng xuống 214.815 đồng.

Quy định cho phép những cách tính nào và Thông tư 99 đổi gì

VAS 02, ban hành theo Quyết định 149/2001/QĐ-BTC ngày 31/12/2001, liệt kê bốn phương pháp ở đoạn 13: giá đích danh, bình quân gia quyền, nhập trước xuất trước và nhập sau xuất trước. Đoạn 15 định nghĩa bình quân gia quyền là giá trị trung bình của hàng tồn đầu kỳ và hàng nhập trong kỳ, tính theo thời kỳ hoặc mỗi khi nhập một lô, “phụ thuộc vào tình hình của doanh nghiệp”. Bài tính hai cách của đoạn 15 và nhập trước xuất trước (FIFO), bỏ qua đích danh (đoạn 14 cho doanh nghiệp có ít loại mặt hàng hoặc mặt hàng ổn định và nhận diện được) và nhập sau xuất trước. Khái niệm hàng tồn kho và ba phương pháp phổ biến đã có ở bài Inventory là gì.

Đoạn 06, 13, 15 và 24 của Chuẩn mực kế toán Việt Nam số 02 về hàng tồn kho
Bốn đoạn của VAS 02 mà bài dùng. Nguồn: bản .doc của chuẩn mực trên thuvienphapluat.vn, MONA dàn lại chữ của bản đó để chụp, tra ngày 03/11/2026.

VAS 02 không có đoạn riêng cho hàng trả lại. Đoạn 06 trừ chiết khấu thương mại và giảm giá hàng mua sai quy cách, phẩm chất khỏi chi phí mua, và đoạn 24 ghi giá gốc hàng đã bán vào chi phí cùng kỳ với doanh thu của hàng đó. Theo cách hiểu của MONA, hai đoạn này dẫn tới hai quy tắc lập trình ở các phần sau: hàng trả nhà cung cấp rời kho theo giá nhập gốc, và hàng khách trả quay về kho theo giá vốn đã ghi lúc bán.

Thông tư 99/2025/TT-BTC ký ngày 27/10/2025, theo Điều 31 khoản 1 có hiệu lực từ 01/01/2026, áp dụng cho năm tài chính bắt đầu từ hoặc sau ngày đó và thay Thông tư 200/2014/TT-BTC, trừ các nội dung kế toán cổ phần hóa doanh nghiệp nhà nước ở khoản 2. Khoản 3 cho doanh nghiệp nhỏ và vừa quyền chọn áp dụng Thông tư 99. Xem thêm phần mềm kế toán và ERP (hệ thống quản trị doanh nghiệp) cần tính năng gì theo Thông tư 99.

Điều 31 Thông tư 99/2025/TT-BTC nêu hiệu lực từ 01/01/2026 và thay thế Thông tư 200/2014
Điều 31 khoản 1 và khoản 2 của Thông tư 99/2025/TT-BTC. Nguồn: bản PDF có chữ ký số ngày 30/10/2025 (thông tư đề ngày 27/10/2025), đăng trên luatvietnam.vn, chụp ngày 03/11/2026.

Bản PDF thông tư tải được ngày 03/11/2026 chỉ gồm phần thân 31 điều. Hướng dẫn tài khoản hàng tồn kho (152, 156) nằm ở Phụ lục II, và MONA chưa đọc được phụ lục đó từ văn bản gốc. Bản tóm tắt của KPMG Việt Nam (tra ngày 03/11/2026) nêu Thông tư 99 giới thiệu phương pháp chi phí tiêu chuẩn và bỏ hai khái niệm kê khai thường xuyên, kiểm kê định kỳ (hai cách ghi sổ hàng tồn kho: ghi theo từng phiếu hoặc chỉ cập nhật khi kiểm kê). Hai điểm này chưa được đối chiếu với phụ lục. Kế toán trưởng cần xác nhận hai điểm đó trước khi dùng làm yêu cầu của phần mềm.

Ba cách tính của bài khác nhau ở thời điểm biết giá vốn của một phiếu xuất:

Cách tính Căn cứ trong VAS 02 Đơn giá xuất lấy từ Biết giá vốn phiếu xuất khi nào
Bình quân sau mỗi lần nhập Đoạn 15, “mỗi khi nhập một lô” Giá trị tồn chia số lượng tồn ngay trước phiếu xuất Ngay khi lập phiếu, nhưng đổi nếu có phiếu nhập ghi lùi
Bình quân cả kỳ Đoạn 15, “theo thời kỳ” Giá trị đầu kỳ cộng nhập trong kỳ, chia số lượng tương ứng Khi khóa kỳ
Nhập trước xuất trước (FIFO) Đoạn 13(c) và 16 Các lô nhập cũ nhất còn hàng Ngay khi lập phiếu, nhưng đổi nếu có lô ghi lùi

Lưu giá trị bằng đồng nguyên và làm tròn bằng Decimal

Giá trị tồn là số nguyên đồng, số lượng là số thập phân, và mọi phép tính dùng Decimal thay cho float. Tài liệu Python nêu số thập phân biểu diễn chính xác, trong khi nhị phân không biểu diễn chính xác 1,1 và 2,2: 1.1 + 2.2 ra 3.3000000000000003. Ngữ cảnh mặc định của Decimal làm tròn half-even (ROUND_HALF_EVEN, độ chính xác 28 chữ số). Số 2,5 thành 2, giống hàm round() có sẵn của Python.

Tài liệu Python decimal nêu ROUND_HALF_EVEN và ROUND_HALF_UP kèm cách làm tròn khi đúng giữa hai số
Hai chế độ làm tròn mà bài so sánh. Nguồn: docs.python.org, mục decimal của Python 3.9, chụp ngày 03/11/2026.

Mã chọn ROUND_HALF_UP, tức 0,5 đồng làm tròn lên. Đây là lựa chọn của người viết mã, vì hai văn bản MONA đã đọc không quy định kiểu làm tròn đồng. Kế toán cần chốt kiểu làm tròn rồi ghi vào quy chế. Mã gồm hàm dong, kiểu phiếu Ev (một dòng của sổ kho) và bốn loại phiếu:

# gia_von.py - inventory costing for one item. Python 3.9, standard library only.
from dataclasses import dataclass
from decimal import Decimal as D, ROUND_HALF_UP
from typing import Dict, List, Optional, Tuple

RECEIPT, ISSUE, PURCHASE_RETURN, SALES_RETURN = (
    "RECEIPT", "ISSUE", "PURCHASE_RETURN", "SALES_RETURN")
def dong(x: D) -> D:
    """Round to a whole dong; 0.5 goes up (Decimal's default is half-even)."""
    return x.quantize(D(1), rounding=ROUND_HALF_UP)
@dataclass(frozen=True)
class Ev:
    id: str
    day: str                    # ISO date, sorts correctly as text
    kind: str
    qty: D
    unit: Optional[D] = None    # unit cost, receipts only
    ref: Optional[str] = None   # receipt id (purchase return) or issue id (sales return)
Row = Tuple[Ev, D, D, D]        # event, amount in dong, qty after, value after

Kết quả chạy của ba phép làm tròn trên cùng số 2,5 và một số đồng lẻ:

round(2.5) = 2 | round(3.5) = 4
half-even 2.5 -> 2 | half-up 2.5 -> 3 | half-up 6249333.333 -> 6249333

Cách lưu dữ liệu MONA đề xuất, chưa chạy thử trên cơ sở dữ liệu: bảng tồn mỗi mặt hàng giữ số lượng (kiểu thập phân, ví dụ ba chữ số lẻ cho hàng cân đo) và tổng giá trị (số nguyên đồng); sổ kho chỉ thêm dòng, mỗi dòng ghi số tiền đã làm tròn của chính phiếu đó. Phương án ngược lại là lưu cột đơn giá bình quân làm tròn đến đồng, đọc nhanh và dễ nhìn trên màn hình kho, nhưng sai số cộng dồn như phần cuối bài đo. Nếu báo cáo cần hiện đơn giá, tính giá trị chia số lượng lúc đọc và chỉ làm tròn để hiển thị.

Bình quân sau mỗi lần nhập, từng bước trên sổ tháng 10

Cách tính này giữ hai biến qty và value. Mỗi loại phiếu ghi số tiền theo quy tắc riêng:

Loại phiếu Số tiền (đồng) Tồn kho đổi
RECEIPT (nhập) Số lượng nhân giá nhập, làm tròn Cộng số lượng và số tiền
ISSUE (xuất) Giá trị tồn nhân số lượng xuất chia số lượng tồn, làm tròn một lần; xuất hết thì lấy nốt giá trị còn lại Trừ số lượng và số tiền
PURCHASE_RETURN (trả nhà cung cấp) Số tiền của phiếu nhập gốc nhân số lượng trả chia số lượng phiếu nhập gốc Trừ số lượng và số tiền
SALES_RETURN (khách trả hàng) Số tiền của phiếu bán gốc nhân số lượng trả chia số lượng phiếu bán gốc Cộng số lượng và số tiền

Hàm moving_average (bình quân sau mỗi lần nhập) sắp phiếu theo ngày (thứ tự nhập được giữ khi trùng ngày), từ chối phiếu xuất vượt tồn và phiếu trả vượt số đã nhập hoặc đã bán:

def moving_average(events: List[Ev], open_qty: D, open_value: D) -> List[Row]:
    """Average recalculated after every movement. Value is the source of truth;
    the unit cost is always value / qty and is never stored rounded."""
    qty, value = D(open_qty), D(open_value)
    amount: Dict[str, D] = {}
    qty_of: Dict[str, D] = {}
    returned: Dict[str, D] = {}
    rows: List[Row] = []
    for e in sorted(events, key=lambda e: e.day):    # stable: same-day order is kept
        if e.kind == RECEIPT:
            a = dong(e.qty * e.unit)
            qty += e.qty
            value += a
        elif e.kind == ISSUE:
            if e.qty > qty:
                raise ValueError(f"{e.id}: issue {e.qty} but stock is {qty}")
            a = value if e.qty == qty else dong(value * e.qty / qty)
            qty -= e.qty
            value -= a
        elif e.kind == PURCHASE_RETURN:      # supplier credits the ORIGINAL price
            returned[e.ref] = returned.get(e.ref, D(0)) + e.qty
            if returned[e.ref] > qty_of[e.ref] or e.qty > qty:
                raise ValueError(f"{e.id}: more returned than received or in stock")
            a = dong(amount[e.ref] * e.qty / qty_of[e.ref])
            qty -= e.qty
            value -= a
        else:                                # SALES_RETURN: back at the cost of the sale
            returned[e.ref] = returned.get(e.ref, D(0)) + e.qty
            if returned[e.ref] > qty_of[e.ref]:
                raise ValueError(f"{e.id}: more returned than sold")
            a = dong(amount[e.ref] * e.qty / qty_of[e.ref])
            qty += e.qty
            value += a
        amount[e.id], qty_of[e.id] = a, e.qty
        rows.append((e, a, qty, value))
    return rows

Sổ tháng 10 mở đầu bằng 100 cái giá 50.000 đồng (5.000.000 đồng). Phiếu R1 nhập 170 cái giá 53.300, R2 nhập 200 cái giá 55.900, PR1 trả nhà cung cấp 20 cái của R2, S1, S2, S3 xuất 120, 200 và 90 cái, SR1 là khách trả 10 cái của S1:

import random
from decimal import Decimal as D, ROUND_HALF_EVEN
from gia_von import (Ev, RECEIPT, ISSUE, PURCHASE_RETURN, SALES_RETURN, dong,
                     moving_average, periodic_average, fifo, naive_rounded_average)


def vn(x) -> str:
    return f"{int(x):,}".replace(",", ".")
OPEN_QTY, OPEN_VALUE = D(100), D(5_000_000)           # 100 units at 50,000
LEDGER = [
    Ev("R1", "2026-10-03", RECEIPT, D(170), D(53_300)),
    Ev("S1", "2026-10-08", ISSUE, D(120)),
    Ev("R2", "2026-10-12", RECEIPT, D(200), D(55_900)),
    Ev("PR1", "2026-10-15", PURCHASE_RETURN, D(20), ref="R2"),
    Ev("S2", "2026-10-18", ISSUE, D(200)),
    Ev("SR1", "2026-10-20", SALES_RETURN, D(10), ref="S1"),
    Ev("S3", "2026-10-25", ISSUE, D(90)),
]

Kết quả chạy, các cột lần lượt là mã phiếu, ngày, loại, số lượng, số tiền của phiếu, tồn lượng, tồn giá trị và đơn giá bình quân sau phiếu:

opening: 100 x 50.000 = 5.000.000
id   ngay       loai               sl     so tien  ton sl     ton tien   don gia TB
R1   10-03      RECEIPT           170   9.061.000     270   14.061.000   52077.7778
S1   10-08      ISSUE             120   6.249.333     150    7.811.667   52077.7800
R2   10-12      RECEIPT           200  11.180.000     350   18.991.667   54261.9057
PR1  10-15      PURCHASE_RETURN    20   1.118.000     330   17.873.667   54162.6273
S2   10-18      ISSUE             200  10.832.525     130    7.041.142   54162.6308
SR1  10-20      SALES_RETURN       10     520.778     140    7.561.920   54013.7143
S3   10-25      ISSUE              90   4.861.234      50    2.700.686   54013.7200
check: opening + receipts - purchase return + sales return - issues = closing: 5.000.000 + 20.241.000 - 1.118.000 + 520.778 - 21.943.092 = 2.700.686

Phiếu S1 xuất 120 cái từ tồn 270 cái giá trị 14.061.000 đồng, nên giá vốn là 6.249.333,33 và được làm tròn thành 6.249.333 đồng. Đơn giá bình quân sau phiếu xuất nhích từ 52.077,7778 lên 52.077,7800, vì phần 0,33 đồng bị cắt khỏi giá trị còn 7.811.667 đồng. Phiếu xuất không đổi đơn giá ở mức đồng, chỉ đổi ở chữ số thập phân thứ ba trở đi. Không đồng nào lạc đi, theo phép kiểm cuối kết quả: đầu kỳ cộng nhập, trừ trả nhà cung cấp, cộng khách trả, trừ xuất ra đúng 2.700.686 đồng tồn cuối.

Trả hàng nhà cung cấp và khách trả hàng đổi đơn giá thế nào

Phiếu PR1 trả 20 cái của lô R2 (giá 55.900) nên rời kho với 1.118.000 đồng, và đơn giá bình quân giảm từ 54.261,9057 xuống 54.162,6273 vì lô bị trả đắt hơn mức trung bình. Phiếu SR1 nhận lại 10 trong 120 cái của S1 với 520.778 đồng, bằng đúng một phần mười hai giá vốn 6.249.333 đồng của S1.

Nếu hai phiếu này đi theo giá bình quân đang chạy, sổ kho lệch khỏi chứng từ gốc. Kết quả chạy với cùng sổ:

PR1: at the original price 1.118.000 | at the running average 1.085.238 | gap -32.762
SR1: at the original price 520.778 | at the running average 541.626 | gap 20.848

Nhà cung cấp ghi có theo giá hóa đơn gốc 1.118.000 đồng mà kho trừ 1.085.238 đồng thì 32.762 đồng nằm lơ lửng giữa sổ kho và công nợ phải trả. Khách trả hàng theo giá bình quân đang chạy cho giá vốn hoàn lại 541.626 đồng, cao hơn giá vốn đã ghi lúc bán 20.848 đồng, nên doanh thu bị trả lại và giá vốn được hoàn không còn khớp nhau, trái với tinh thần đoạn 24 của VAS 02 theo cách hiểu của MONA.

Các trường hợp phần mềm phải từ chối, kèm kết quả chạy:

Ba sổ thử, mỗi sổ trên tồn đầu kỳ 100 cái giá trị 5.000.000 đồng:

for name, evs, fn in [
    ("issue above stock", [Ev("X", "2026-10-01", ISSUE, D(101))], moving_average),
    ("return more than received", [Ev("A", "2026-10-01", RECEIPT, D(5), D(10)),
                                    Ev("B", "2026-10-02", PURCHASE_RETURN, D(6), ref="A")], moving_average),
    ("return of a lot already sold (FIFO)", [Ev("A", "2026-10-01", RECEIPT, D(5), D(10)),
                                            Ev("B", "2026-10-02", ISSUE, D(103)),
                                            Ev("C", "2026-10-03", PURCHASE_RETURN, D(5), ref="A")], fifo),
]:
    try:
        fn(evs, OPEN_QTY, OPEN_VALUE)
        raise AssertionError("should have refused: " + name)
    except ValueError as err:
        print(f"{name}: ValueError: {err}")

Kết quả chạy:

issue above stock: ValueError: X: issue 101 but stock is 100
return more than received: ValueError: B: more returned than received or in stock
return of a lot already sold (FIFO): ValueError: C: lot A no longer holds 5

Bình quân gia quyền không theo dõi lô. Phiếu trả nhà cung cấp chỉ bị chặn khi vượt số lượng của phiếu nhập gốc hoặc vượt tồn. Hàng của lô đó đã bán hết vẫn có thể bị trả, miễn là kho còn hàng cùng mã từ lô khác, và lỗi này không bị phát hiện. FIFO thì phát hiện được, vì mỗi lô là một lớp có số lượng riêng. Doanh nghiệp hay trả hàng theo từng lô cần đưa quy tắc này vào bước chọn phương pháp.

Bình quân cả kỳ và FIFO trên cùng một sổ

Bình quân cả kỳ gom nhập và trả nhà cung cấp của cả tháng vào một nhóm, chia ra một đơn giá, rồi áp cho mọi phiếu xuất. Số tiền làm tròn từng phiếu có thể lệch tổng vài đồng. Phiếu xuất cuối cùng của danh sách (đã xếp theo ngày) nhận phần chênh:

def periodic_average(events: List[Ev], open_qty: D, open_value: D):
    """One price for the whole period, known only when the period closes."""
    pool_qty, pool_val = D(open_qty), D(open_value)
    amount: Dict[str, D] = {}
    for e in events:
        if e.kind == RECEIPT:
            amount[e.id] = dong(e.qty * e.unit)
            pool_qty += e.qty
            pool_val += amount[e.id]
    for e in events:
        if e.kind == PURCHASE_RETURN:
            rec = next(x for x in events if x.id == e.ref)
            pool_qty -= e.qty
            pool_val -= dong(amount[e.ref] * e.qty / rec.qty)
    net = sum((e.qty for e in events if e.kind == ISSUE), D(0)) \
        - sum((e.qty for e in events if e.kind == SALES_RETURN), D(0))
    cogs = pool_val if net == pool_qty else dong(pool_val * net / pool_qty)
    lines = {}                               # cost per issue / sales return line
    for e in events:
        if e.kind in (ISSUE, SALES_RETURN):
            part = dong(pool_val * e.qty / pool_qty)
            lines[e.id] = part if e.kind == ISSUE else -part
    last = [e.id for e in events if e.kind == ISSUE][-1]
    lines[last] += cogs - sum(lines.values())    # last issue absorbs the rounding gap
    return {"price": pool_val / pool_qty, "cogs": cogs, "lines": lines,
            "closing_qty": pool_qty - net, "closing_value": pool_val - cogs}

FIFO giữ danh sách lô, mỗi lô có số lượng và giá trị còn lại. Phiếu xuất lấy từ lô cũ nhất. Phiếu khách trả hàng tạo lô mới ở cuối hàng với giá vốn của phiếu bán; chỗ đặt hàng trả lại (cuối hàng hay về lô gốc) là quyết định chính sách của doanh nghiệp, mã chọn cuối hàng:

def fifo(events: List[Ev], open_qty: D, open_value: D) -> List[Row]:
    """Layers [qty left, value left, source id]; the oldest layer is consumed first.
    A sales return becomes a new layer at the cost of the sale, placed at the end."""
    layers = [[D(open_qty), D(open_value), "OPEN"]]
    amount: Dict[str, D] = {}
    qty_of: Dict[str, D] = {}
    rows: List[Row] = []

    def take(layer, q: D) -> D:
        v = layer[1] if q == layer[0] else dong(layer[1] * q / layer[0])
        layer[0] -= q
        layer[1] -= v
        return v

    for e in sorted(events, key=lambda e: e.day):
        if e.kind == RECEIPT:
            a = dong(e.qty * e.unit)
            layers.append([e.qty, a, e.id])
        elif e.kind == ISSUE:
            if e.qty > sum(L[0] for L in layers):
                raise ValueError(f"{e.id}: issue {e.qty} but stock is less")
            need, a = e.qty, D(0)
            while need > 0:
                L = layers[0]
                q = min(L[0], need)
                a += take(L, q)
                need -= q
                if L[0] == 0:
                    layers.pop(0)
        elif e.kind == PURCHASE_RETURN:
            L = next((x for x in layers if x[2] == e.ref and x[0] >= e.qty), None)
            if L is None:
                raise ValueError(f"{e.id}: lot {e.ref} no longer holds {e.qty}")
            a = take(L, e.qty)
        else:
            a = dong(amount[e.ref] * e.qty / qty_of[e.ref])
            layers.append([e.qty, a, e.id])
        amount[e.id], qty_of[e.id] = a, e.qty
        rows.append((e, a, sum(L[0] for L in layers), sum(L[1] for L in layers)))
    return rows

Kết quả của bình quân cả kỳ trên sổ tháng 10: đơn giá của tháng, cogs (giá vốn thuần của tháng, tức tổng giá vốn phiếu xuất trừ giá vốn hàng khách trả), tồn cuối và giá vốn từng dòng:

price 53606.6667 | cogs 21.442.667 | closing 50 2.680.333
lines: {'S1': '6.432.800', 'S2': '10.721.333', 'SR1': '-536.067', 'S3': '4.824.601'}

Ba cách đặt cạnh nhau trên đúng 7 phiếu, với giá vốn thuần là tổng giá vốn phiếu xuất trừ giá vốn hàng khách trả:

method                         cogs  closing qty  closing value
moving average           21.422.314           50      2.700.686
periodic average         21.442.667           50      2.680.333
FIFO                     21.381.500           50      2.741.500

Tồn cuối đều là 50 cái, nhưng giá trị tồn chênh nhau tới 61.167 đồng giữa FIFO (2.741.500) và bình quân cả kỳ (2.680.333). Giá nhập trong sổ này tăng từ 50.000 lên 53.300 rồi 55.900, nên FIFO xuất hàng rẻ trước và giữ lại hàng đắt, giá vốn thấp nhất và tồn cuối cao nhất. Giá giảm thì thứ tự đảo lại. Các con số này của sổ giả lập do MONA đặt, không đại diện cho doanh nghiệp nào.

Về chọn cách tính, MONA nhìn thế này. Hàng nhập đều đặn với giá gần nhau và cần giá vốn ngay trên báo cáo bán hàng: bình quân sau mỗi lần nhập, trả bằng việc phải tính lại khi có phiếu ghi lùi. Doanh nghiệp chỉ cần giá vốn khi chốt tháng: bình quân cả kỳ, trả bằng việc báo cáo lãi lỗ trong tháng chỉ có giá vốn tạm. Hàng có hạn dùng hoặc cần truy từng lô: FIFO, trả bằng việc phải lưu từng lớp và có quy tắc riêng cho hàng trả. Khi chọn, đoạn 27(a) của VAS 02 yêu cầu báo cáo tài chính nêu phương pháp đã dùng.

Phiếu nhập đến sau phiếu bán buộc tính lại những dòng nào

Lô 100 cái giá 51.000 đồng về kho ngày 06/10 nhưng kế toán chỉ nhập vào phần mềm ngày 28/10. Lô này đứng trước phiếu S1 ngày 08/10, nên đơn giá bình quân từ 06/10 trở đi khác, và mọi phiếu xuất cùng phiếu trả hàng phụ thuộc phiếu xuất phải tính lại. Hàm không cần sửa gì: chỉ thêm phiếu vào sổ rồi chạy lại từ đầu, vì hàm sắp theo ngày.

LATE = Ev("R0", "2026-10-06", RECEIPT, D(100), D(51_000))   # entered on 28/10
ALL = LEDGER + [LATE]
mv2 = moving_average(ALL, OPEN_QTY, OPEN_VALUE)

Kết quả chạy so từng dòng trước và sau (cột lệch là số tiền sau trừ số tiền trước), kèm sổ đầy đủ sau khi chèn phiếu:

id   ngay          truoc         sau      lech
R1   10-03     9.061.000   9.061.000         0
S1   10-08     6.249.333   6.214.378   -34.955
R2   10-12    11.180.000  11.180.000         0
PR1  10-15     1.118.000   1.118.000         0
S2   10-18    10.832.525  10.701.685  -130.840
SR1  10-20       520.778     517.865    -2.913
S3   10-25     4.861.234   4.809.301   -51.933
net cogs of the month: 21.422.314 -> 21.207.499 | change -214.815
entries whose amount changed: 4 of 7 | closing value 2.700.686 -> 8.015.501
moving average after the late receipt
id   ngay       loai               sl     so tien  ton sl     ton tien   don gia TB
R1   10-03      RECEIPT           170   9.061.000     270   14.061.000   52077.7778
R0   10-06      RECEIPT           100   5.100.000     370   19.161.000   51786.4865
S1   10-08      ISSUE             120   6.214.378     250   12.946.622   51786.4880
R2   10-12      RECEIPT           200  11.180.000     450   24.126.622   53614.7156
PR1  10-15      PURCHASE_RETURN    20   1.118.000     430   23.008.622   53508.4233
S2   10-18      ISSUE             200  10.701.685     230   12.306.937   53508.4217
SR1  10-20      SALES_RETURN       10     517.865     240   12.824.802   53436.6750
S3   10-25      ISSUE              90   4.809.301     150    8.015.501   53436.6733
periodic after the late receipt: price 53132.7273 cogs 21.253.091 closing 7.969.909 | one new price: every issue line of the month takes it
FIFO after the late receipt: cogs 20.891.500 closing 8.331.500

Bốn trong bảy dòng đổi giá trị: S1 giảm 34.955 đồng, S2 giảm 130.840, S3 giảm 51.933 và phiếu khách trả SR1 giảm 2.913, vì nó bám theo giá vốn mới của S1. Giá vốn thuần của tháng từ 21.422.314 xuống 21.207.499 đồng, tức giảm 214.815 đồng. Giá trị tồn cuối tăng lên 8.015.501 đồng chủ yếu vì tồn cuối nay là 150 cái chứ không còn 50 cái.

Với bình quân cả kỳ, cùng phiếu ghi lùi đổi đơn giá tháng từ 53.606,6667 xuống 53.132,7273, nên mọi dòng xuất của tháng nhận đơn giá mới. Tháng còn mở thì chưa dòng nào đã chốt, tháng đã khóa sổ (không cho sửa số) thì cả tháng phải viết lại. Với FIFO, lô ghi lùi đổi lớp mà từng phiếu xuất lấy: giá vốn thuần là 20.891.500 đồng và tồn cuối 8.331.500 đồng. Cả ba cách đều tính lại. Khác nhau ở phạm vi: bình quân sau mỗi lần nhập kéo theo mọi phiếu từ ngày ghi lùi trở đi, kể cả phiếu nằm ở kỳ đã khóa.

Phần mềm kho làm bốn việc khi nhận phiếu ghi lùi. Một, tính lại riêng mặt hàng đó, từ ngày sớm nhất bị ảnh hưởng. Hai, giữ số tiền cũ và mới của từng dòng đổi trong bảng lưu vết, vì Điều 28 điểm b Thông tư 99 yêu cầu phần mềm kế toán khi sửa phải lưu lại dấu vết nội dung đã ghi sổ theo trình tự thời gian; MONA áp yêu cầu đó vào việc tính lại giá vốn. Ba, chặn hai người sửa cùng một mặt hàng trong lúc tính lại (xem cách khóa dòng tồn trong MySQL). Bốn, hỏi kế toán trưởng phiếu ghi lùi chạm kỳ đã khóa thì viết lại giá vốn kỳ đó hay hạch toán một khoản điều chỉnh vào kỳ hiện tại; MONA không dẫn điều luật nào cho lựa chọn này nên để chính sách nội bộ quyết định.

Điều 28 Thông tư 99/2025/TT-BTC yêu cầu phần mềm kế toán lưu dấu vết khi sửa số liệu đã ghi sổ
Điều 28 khoản 1 điểm b của Thông tư 99/2025/TT-BTC (trang 29 của bản PDF). Nguồn: bản PDF đăng trên luatvietnam.vn, chụp ngày 03/11/2026; bài sổ kế toán chạy song song và nhật ký lưu vết tóm tắt cả điều này.

Chi phí tính lại tăng theo số phiếu sau ngày ghi lùi. Tài liệu FIFO and Moving Average của ERPNext (tra ngày 03/11/2026) nêu phiếu nhập ghi lùi buộc tính lại sổ kho phía sau và việc này có thể chậm khi lịch sử dài. MONA đề xuất chạy việc đó nền, theo từng mặt hàng. Ngày chốt sổ của đơn lập lúc 23:30 cũng gây ra loại lệch kỳ tương tự, xem múi giờ và ngày chốt sổ.

Một trường hợp mã chưa xử lý: bán khi kho chưa đủ hàng vì phiếu nhập chưa vào máy. Hàm hiện từ chối phiếu xuất vượt tồn (kết quả chạy ở phần trước). Doanh nghiệp cho phép bán khi tồn kho âm cần thêm quy tắc tính giá vốn tạm bằng đơn giá bình quân cuối cùng và định giá lại khi phiếu nhập về; đoạn đó chưa có trong mã của bài.

Làm tròn đến đồng làm lệch giá trị tồn kho bao nhiêu

Hàm sau viết cách lưu đơn giá làm tròn đến đồng để so sánh. Hàm giữ book là nhập trừ giá vốn xuất đã ghi, và shelf là số lượng nhân đơn giá làm tròn. Ở hệ thống chính xác, hai số này bằng nhau:

def naive_rounded_average(events: List[Ev], open_qty: D, open_price: D):
    """What many spreadsheets and tables do: keep the average as a rounded dong
    price. Returns the books (receipts minus issue costs) and the shelf value
    (qty x price); in an exact system the two are equal.
    Receipts and issues only; returns are left out to keep the model short."""
    qty, price = D(open_qty), D(open_price)
    book = qty * price
    issue_price: Dict[str, D] = {}
    for e in sorted(events, key=lambda e: e.day):
        if e.kind == RECEIPT:
            book += dong(e.qty * e.unit)
            price = dong((qty * price + e.qty * e.unit) / (qty + e.qty))
            qty += e.qty
        elif e.kind == ISSUE:
            issue_price[e.id] = price
            book -= e.qty * price
            qty -= e.qty
    return {"qty": qty, "price": price, "book": book, "shelf": qty * price}

Phép thử tạo 2.000 sổ ngẫu nhiên (hạt giống 20261103), mỗi sổ 6 đến 40 phiếu nhập xuất của một mặt hàng với đơn giá có phần lẻ, và bán hết hàng ở cuối. Với cách giữ giá trị, vòng lặp khẳng định tồn bằng 0 thì giá trị bằng 0 và nhập trừ xuất khớp từng đồng:

random.seed(20261103)


def random_ledger():
    evs, stock, day = [], 0, 1
    for i in range(random.randint(6, 40)):
        day += 1
        if stock == 0 or random.random() < 0.4:
            q = random.randint(1, 300)
            evs.append(Ev(f"R{i}", f"2026-01-{day:02d}" if day < 29 else f"2026-02-{day - 28:02d}",
                          RECEIPT, D(q), D(random.randint(10_000, 99_999)) + D(random.randint(0, 99)) / 100))
            stock += q
        else:
            q = random.randint(1, stock)
            evs.append(Ev(f"S{i}", f"2026-01-{day:02d}" if day < 29 else f"2026-02-{day - 28:02d}", ISSUE, D(q)))
            stock -= q
    if stock:
        evs.append(Ev("SZ", "2026-03-01", ISSUE, D(stock)))   # sell everything left
    return evs


worst_naive, residue_naive, exact_ok, residues = D(0), 0, 0, []
for _ in range(2000):
    evs = random_ledger()
    rows = moving_average(evs, D(0), D(0))
    rec_v = sum(dong(e.qty * e.unit) for e in evs if e.kind == RECEIPT)
    iss_v = sum(a for e, a, q, v in rows if e.kind == ISSUE)
    assert rec_v - iss_v == rows[-1][3] == 0 and rows[-1][2] == 0   # exact: stock at zero is worth zero
    exact_ok += 1
    nv = naive_rounded_average(evs, D(0), D(0))
    worst_naive = max(worst_naive, abs(nv["book"]))
    residue_naive += nv["book"] != 0
    residues.append(abs(nv["book"]))
print("exact method: 2000 of 2000 ledgers end at stock 0 and value 0 (asserted)", exact_ok)
print("rounded-price method: ledgers ending with a non-zero value at stock 0:", residue_naive,
      "| worst residue (dong):", vn(worst_naive))
residues.sort()
print("rounded-price method: median residue (dong):", vn(residues[len(residues) // 2]))

Kết quả chạy:

exact method: 2000 of 2000 ledgers end at stock 0 and value 0 (asserted) 2000
rounded-price method: ledgers ending with a non-zero value at stock 0: 1996 | worst residue (dong): 2.122
rounded-price method: median residue (dong): 186

Cả 2.000 sổ giữ giá trị khớp từng đồng. Cách lưu đơn giá làm tròn để lại giá trị dư ở 1.996 sổ, trung vị 186 đồng và cao nhất 2.122 đồng khi kho đã hết hàng. Phần dư là giá vốn ghi thừa hoặc thiếu so với tiền nhập. Phần dư không tự mất đi. Quy tắc làm cho cách giữ giá trị khớp nằm ở một dòng của hàm xuất: phiếu xuất hết số lượng tồn thì lấy toàn bộ giá trị còn lại thay vì nhân với đơn giá.

Giới hạn của phép thử: sổ ngẫu nhiên, chỉ có phiếu nhập và xuất, không có phiếu trả; số 186 và 2.122 đồng là kết quả của giả lập này và không phải số đo của một doanh nghiệp. Phép thử chỉ cho thấy cơ chế sai số cộng dồn, và độ lớn trong thực tế còn tùy giá và số phiếu. Kế toán dùng ba phép kiểm sau để soát phần mềm kho, mỗi phép chạy được ngay trên bảng sổ kho:

  1. Cộng giá trị đầu kỳ, nhập và khách trả, trừ giá trị trả nhà cung cấp và giá trị xuất, phải bằng giá trị cuối kỳ, chính xác từng đồng.
  2. Số lượng tồn bằng 0 thì giá trị tồn phải bằng 0.
  3. Giá vốn từng phiếu xuất của tháng, cộng lại, phải bằng giá vốn thuần của tháng do cách tính cả kỳ báo (với cách cả kỳ, một phiếu nhận phần chênh làm tròn).

Hỏi đáp

Bình quân gia quyền tính theo tháng hay sau mỗi lần nhập?

Cả hai đều nằm trong đoạn 15 của VAS 02, và doanh nghiệp chọn theo tình hình của mình. Hai cách cho giá vốn khác nhau trên cùng sổ (21.422.314 đồng so với 21.442.667 đồng ở ví dụ tháng 10), nên cách đã chọn cần ghi vào thuyết minh theo đoạn 27(a), xem phần so sánh ba cách ở trên.

Khách trả hàng thì tính giá vốn theo giá nào?

Theo giá vốn đã ghi trên phiếu bán gốc, chia theo số lượng trả, không theo đơn giá bình quân đang chạy: ở ví dụ là 520.778 đồng thay vì 541.626 đồng, lệch 20.848 đồng. Đơn giá bình quân đang chạy chỉ dùng cho phiếu xuất mới.

Trả hàng nhà cung cấp có làm đổi giá bình quân không?

Có. Lô bị trả có giá khác mức trung bình thì đơn giá đổi: ví dụ trả 20 cái giá 55.900 đồng làm đơn giá giảm từ 54.261,9057 xuống 54.162,6273, và số tiền trừ phải bằng giá hóa đơn gốc để khớp công nợ phải trả.

Nhập kho bù ngày cũ có phải tính lại cả tháng không?

Với bình quân sau mỗi lần nhập, phải tính lại mọi phiếu của mặt hàng từ ngày ghi lùi trở đi: 4 trong 7 dòng đổi ở ví dụ. Với bình quân cả kỳ, chỉ cần tính lại một đơn giá của kỳ chứa phiếu ghi lùi, nếu kỳ đó chưa khóa.

Dùng float cho giá vốn được không?

Không: float nhị phân không biểu diễn chính xác nhiều số thập phân (1,1 + 2,2 ra 3,3000000000000003 theo tài liệu Python), và hàm round() có sẵn làm tròn 2,5 thành 2. Dùng Decimal với ROUND_HALF_UP và lưu giá trị bằng số nguyên đồng.

Phiếu nhập, trả hàng và ngày ghi sổ của kho mỗi doanh nghiệp một kiểu. Phép tính phải khớp quy trình đó trước khi viết mã. MONA viết phần mềm kho theo quy trình nhập xuất đang chạy; mô tả nằm ở phần mềm quản lý sản xuất, kho và vận tải, danh sách công cụ có ở 10 phần mềm quản lý kho, và khách cần tính giá vốn theo một cách chưa có sẵn có thể gọi hotline 1900 636 648. Chứng từ đi kèm có trong bài mẫu phiếu nhập kho, cách đọc kết quả có ở bài giá vốn hàng bán và vòng quay hàng tồn kho.

Đội MONA gặp khách hàng để khảo sát quy trình trước khi làm phần mềm

Kiến thức phần mềm

Gửi MONA quy trình đang chạy để nhận hướng làm phần mềm

Đội MONA Software khảo sát nghiệp vụ, thiết kế cơ sở dữ liệu, lập trình, bàn giao và đi cùng qua giai đoạn vận hành thử.