"""完整财务模型: 第一部分:业务往来(应收、应付、收款、付款、核销明细) 第二部分:总账系统(会计科目、会计期间、记账凭证、凭证分录) 设计原则: - 新会计准则,借贷记账法 - 业务与财务一体化:销售/采购/收付款过账时,自动或按需生成对应记账凭证 - 辅助核算:支持客户、供应商等辅助维度 - 凭证支持草稿、已过账、已作废状态机管理 """ 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}"