baseline: 批次A-D 成果 + membership 半成品(测试红)
This commit is contained in:
@@ -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}"
|
||||
Reference in New Issue
Block a user