Files
dealerhub/backend/apps/finance/models.py
T

428 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""完整财务模型:
第一部分:业务往来(应收、应付、收款、付款、核销明细)
第二部分:总账系统(会计科目、会计期间、记账凭证、凭证分录)
设计原则:
- 新会计准则,借贷记账法
- 业务与财务一体化:销售/采购/收付款过账时,自动或按需生成对应记账凭证
- 辅助核算:支持客户、供应商等辅助维度
- 凭证支持草稿、已过账、已作废状态机管理
"""
from decimal import Decimal
from django.conf import settings
from django.db import models
from apps.core.base_models import TenantScopedModel
# ============================================================
# 第一部分:应收应付与核销(AR / AP)
# ============================================================
class Receivable(TenantScopedModel):
"""应收单(销售/其他应收)。"""
STATUS_CHOICES = [
("open", "未收"),
("partial", "部分已收"),
("paid", "已结清"),
("cancelled", "已取消"),
]
customer = models.ForeignKey(
"partner.Customer",
on_delete=models.PROTECT,
related_name="receivables",
)
bill_no = models.CharField(max_length=64, help_text="应收单号(系统生成)")
source_type = models.CharField(max_length=32, default="sale", help_text="来源:sale/manual/...")
source_ref = models.CharField(max_length=64, blank=True, default="")
bill_date = models.DateField()
due_date = models.DateField(null=True, blank=True)
total_amount = models.DecimalField(max_digits=18, decimal_places=4, default=0)
paid_amount = models.DecimalField(max_digits=18, decimal_places=4, default=0)
status = models.CharField(max_length=16, choices=STATUS_CHOICES, default="open")
remark = models.TextField(blank=True, default="")
class Meta:
db_table = "finance_receivable"
unique_together = [("tenant", "bill_no")]
verbose_name_plural = "应收单"
indexes = [
models.Index(fields=["tenant", "customer", "status"]),
models.Index(fields=["tenant", "bill_date"]),
]
ordering = ["-bill_date", "-id"]
@property
def balance(self) -> Decimal:
return self.total_amount - self.paid_amount
def __str__(self):
return f"{self.bill_no} ({self.customer.name}) {self.total_amount}"
class Payable(TenantScopedModel):
"""应付单(采购/其他应付)。"""
STATUS_CHOICES = [
("open", "未付"),
("partial", "部分已付"),
("paid", "已结清"),
("cancelled", "已取消"),
]
supplier = models.ForeignKey(
"partner.Supplier",
on_delete=models.PROTECT,
related_name="payables",
)
bill_no = models.CharField(max_length=64)
source_type = models.CharField(max_length=32, default="purchase")
source_ref = models.CharField(max_length=64, blank=True, default="")
bill_date = models.DateField()
due_date = models.DateField(null=True, blank=True)
total_amount = models.DecimalField(max_digits=18, decimal_places=4, default=0)
paid_amount = models.DecimalField(max_digits=18, decimal_places=4, default=0)
status = models.CharField(max_length=16, choices=STATUS_CHOICES, default="open")
remark = models.TextField(blank=True, default="")
class Meta:
db_table = "finance_payable"
unique_together = [("tenant", "bill_no")]
verbose_name_plural = "应付单"
indexes = [
models.Index(fields=["tenant", "supplier", "status"]),
models.Index(fields=["tenant", "bill_date"]),
]
ordering = ["-bill_date", "-id"]
@property
def balance(self) -> Decimal:
return self.total_amount - self.paid_amount
def __str__(self):
return f"{self.bill_no} ({self.supplier.name}) {self.total_amount}"
class Receipt(TenantScopedModel):
"""收款单。"""
STATUS_CHOICES = [
("draft", "草稿"),
("posted", "已过账"),
("cancelled", "已取消"),
]
customer = models.ForeignKey(
"partner.Customer",
on_delete=models.PROTECT,
related_name="receipts",
)
bill_no = models.CharField(max_length=64)
bill_date = models.DateField()
amount = models.DecimalField(max_digits=18, decimal_places=4)
method = models.CharField(max_length=32, blank=True, default="", help_text="现金/银行/微信/支付宝")
status = models.CharField(max_length=16, choices=STATUS_CHOICES, default="draft")
remark = models.TextField(blank=True, default="")
class Meta:
db_table = "finance_receipt"
unique_together = [("tenant", "bill_no")]
verbose_name_plural = "收款单"
ordering = ["-bill_date", "-id"]
def __str__(self):
return f"{self.bill_no} {self.customer.name} {self.amount}"
class Payment(TenantScopedModel):
"""付款单。"""
STATUS_CHOICES = [
("draft", "草稿"),
("posted", "已过账"),
("cancelled", "已取消"),
]
supplier = models.ForeignKey(
"partner.Supplier",
on_delete=models.PROTECT,
related_name="payments",
)
bill_no = models.CharField(max_length=64)
bill_date = models.DateField()
amount = models.DecimalField(max_digits=18, decimal_places=4)
method = models.CharField(max_length=32, blank=True, default="")
status = models.CharField(max_length=16, choices=STATUS_CHOICES, default="draft")
remark = models.TextField(blank=True, default="")
class Meta:
db_table = "finance_payment"
unique_together = [("tenant", "bill_no")]
verbose_name_plural = "付款单"
ordering = ["-bill_date", "-id"]
def __str__(self):
return f"{self.bill_no} {self.supplier.name} {self.amount}"
class Allocation(TenantScopedModel):
"""核销明细。"""
KIND_CHOICES = [
("receipt", "收款核销"),
("payment", "付款核销"),
]
receivable = models.ForeignKey(
Receivable,
null=True,
blank=True,
on_delete=models.CASCADE,
related_name="allocations",
)
payable = models.ForeignKey(
Payable,
null=True,
blank=True,
on_delete=models.CASCADE,
related_name="allocations",
)
receipt = models.ForeignKey(
Receipt,
null=True,
blank=True,
on_delete=models.CASCADE,
related_name="allocations",
)
payment = models.ForeignKey(
Payment,
null=True,
blank=True,
on_delete=models.CASCADE,
related_name="allocations",
)
kind = models.CharField(max_length=16, choices=KIND_CHOICES)
amount = models.DecimalField(max_digits=18, decimal_places=4)
class Meta:
db_table = "finance_allocation"
verbose_name_plural = "核销明细"
indexes = [
models.Index(fields=["tenant", "receivable"]),
models.Index(fields=["tenant", "payable"]),
]
# ============================================================
# 第二部分:总账系统(科目、期间、凭证、分录)
# ============================================================
class Account(TenantScopedModel):
"""会计科目。
类别(5 大类):
- asset 资产
- liability 负债
- equity 所有者权益
- revenue 收入
- expense 费用
余额方向:
- debit 借方余额(资产/费用)
- credit 贷方余额(负债/权益/收入)
"""
CATEGORY_CHOICES = [
("asset", "资产"),
("liability", "负债"),
("equity", "所有者权益"),
("revenue", "收入"),
("expense", "费用"),
]
BALANCE_TYPE_CHOICES = [
("debit", "借方余额"),
("credit", "贷方余额"),
]
code = models.CharField(max_length=32, help_text="科目编号(GB/T 标准 4-2-2)")
name = models.CharField(max_length=64)
category = models.CharField(max_length=16, choices=CATEGORY_CHOICES)
balance_type = models.CharField(max_length=8, choices=BALANCE_TYPE_CHOICES)
parent = models.ForeignKey(
"self",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="children",
)
is_active = models.BooleanField(default=True)
is_system = models.BooleanField(default=False, help_text="系统预置科目(不可删)")
aux_customer = models.BooleanField(default=False, help_text="是否需要客户辅助核算")
aux_supplier = models.BooleanField(default=False, help_text="是否需要供应商辅助核算")
remark = models.CharField(max_length=255, blank=True, default="")
class Meta:
db_table = "finance_account"
unique_together = [("tenant", "code")]
verbose_name_plural = "会计科目"
ordering = ["code"]
def __str__(self):
return f"{self.code} {self.name}"
class Period(TenantScopedModel):
"""会计期间(月度)。"""
STATUS_CHOICES = [
("open", "未结"),
("closed", "已结账"),
]
# YYYY-MM
code = models.CharField(max_length=16, help_text="期间代码,如 2026-09")
start_date = models.DateField()
end_date = models.DateField()
status = models.CharField(max_length=16, choices=STATUS_CHOICES, default="open")
closed_at = models.DateTimeField(null=True, blank=True)
closed_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
class Meta:
db_table = "finance_period"
unique_together = [("tenant", "code")]
verbose_name_plural = "会计期间"
ordering = ["-code"]
def __str__(self):
return f"{self.code} ({self.get_status_display()})"
class Voucher(TenantScopedModel):
"""记账凭证。"""
STATUS_CHOICES = [
("draft", "草稿"),
("posted", "已过账"),
("cancelled", "已作废"),
]
SOURCE_CHOICES = [
("manual", "手工"),
("sale", "销售单"),
("sale_cost", "销售成本"),
("purchase", "采购单"),
("receipt", "收款"),
("payment", "付款"),
("inventory_adjust", "库存调整"),
("period_close", "期末结转"),
]
bill_no = models.CharField(max_length=64, help_text="凭证号 V{YYYYMM}{seq:04d}")
period = models.ForeignKey(
Period,
on_delete=models.PROTECT,
related_name="vouchers",
)
source_type = models.CharField(max_length=24, choices=SOURCE_CHOICES, default="manual")
source_ref = models.CharField(max_length=64, blank=True, default="")
voucher_date = models.DateField()
total_debit = models.DecimalField(max_digits=18, decimal_places=4, default=0)
total_credit = models.DecimalField(max_digits=18, decimal_places=4, default=0)
status = models.CharField(max_length=16, choices=STATUS_CHOICES, default="draft")
summary = models.CharField(max_length=255, blank=True, default="", help_text="摘要")
posted_at = models.DateTimeField(null=True, blank=True)
posted_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
class Meta:
db_table = "finance_voucher"
unique_together = [("tenant", "bill_no")]
verbose_name_plural = "记账凭证"
indexes = [
models.Index(fields=["tenant", "period", "-voucher_date"]),
models.Index(fields=["tenant", "source_type", "source_ref"]),
]
ordering = ["-voucher_date", "-id"]
def __str__(self):
return f"{self.bill_no} ({self.voucher_date}) 借/贷:{self.total_debit}/{self.total_credit} [{self.status}]"
class VoucherEntry(TenantScopedModel):
"""凭证分录(一条 = 一借或一贷)。"""
voucher = models.ForeignKey(
Voucher,
on_delete=models.CASCADE,
related_name="entries",
)
account = models.ForeignKey(
Account,
on_delete=models.PROTECT,
related_name="voucher_entries",
)
# 辅助核算
customer = models.ForeignKey(
"partner.Customer",
null=True,
blank=True,
on_delete=models.PROTECT,
related_name="voucher_entries",
)
supplier = models.ForeignKey(
"partner.Supplier",
null=True,
blank=True,
on_delete=models.PROTECT,
related_name="voucher_entries",
)
summary = models.CharField(max_length=255, blank=True, default="")
debit = models.DecimalField(max_digits=18, decimal_places=4, default=0, help_text="借方")
credit = models.DecimalField(max_digits=18, decimal_places=4, default=0, help_text="贷方")
class Meta:
db_table = "finance_voucher_entry"
verbose_name_plural = "凭证分录"
ordering = ["id"]
def __str__(self):
return f"{self.voucher.bill_no} #{self.id} {self.account.code} 借:{self.debit} 贷:{self.credit}"
class StatementShare(TenantScopedModel):
"""对账单公开分享链接(token 化匿名访问)。"""
customer = models.ForeignKey(
"partner.Customer", on_delete=models.CASCADE, related_name="statement_shares"
)
token = models.UUIDField(unique=True, db_index=True)
date_from = models.DateField()
date_to = models.DateField()
expires_at = models.DateTimeField(null=True, blank=True)
revoked = models.BooleanField(default=False)
class Meta:
db_table = "finance_statement_share"
verbose_name_plural = "对账单分享"
ordering = ["-created_at"]
def __str__(self):
return f"share:{self.token} {self.customer.code} {self.date_from}~{self.date_to}"