baseline: 批次A-D 成果 + membership 半成品(测试红)

This commit is contained in:
agent
2026-09-11 23:11:35 +08:00
commit b3f3095d53
311 changed files with 40540 additions and 0 deletions
+427
View File
@@ -0,0 +1,427 @@
"""完整财务模型:
第一部分:业务往来(应收、应付、收款、付款、核销明细)
第二部分:总账系统(会计科目、会计期间、记账凭证、凭证分录)
设计原则:
- 新会计准则,借贷记账法
- 业务与财务一体化:销售/采购/收付款过账时,自动或按需生成对应记账凭证
- 辅助核算:支持客户、供应商等辅助维度
- 凭证支持草稿、已过账、已作废状态机管理
"""
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}"