mirror of
https://github.com/huge-dream/django-vue3-admin.git
synced 2026-09-25 13:51:02 +00:00
319 lines
14 KiB
Python
319 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
||
from __future__ import annotations
|
||
|
||
from django.db import models, transaction
|
||
from django.utils import timezone
|
||
from rest_framework import serializers
|
||
from dvadmin.utils.models import CoreModel, table_prefix
|
||
|
||
|
||
class Unit(CoreModel):
|
||
"""计量单位"""
|
||
unitname = models.CharField(max_length=50, verbose_name="计量单位名称", help_text="计量单位名称")
|
||
unitcode = models.CharField(max_length=20, verbose_name="计量单位代码", help_text="计量单位代码", null=True, blank=True)
|
||
status = models.IntegerField(default=1, verbose_name="可用状态", help_text="可用状态(1可用0不可用)")
|
||
|
||
class Meta:
|
||
db_table = table_prefix + "units"
|
||
verbose_name = "计量单位"
|
||
verbose_name_plural = verbose_name
|
||
ordering = ("-create_datetime",)
|
||
|
||
def __str__(self):
|
||
return self.unitname
|
||
|
||
|
||
class Currency(CoreModel):
|
||
"""货币"""
|
||
currencyname = models.CharField(max_length=50, verbose_name="货币名称", help_text="货币名称")
|
||
currencycode = models.CharField(max_length=20, verbose_name="货币代码", help_text="货币代码", null=True, blank=True)
|
||
currencysymbol = models.CharField(max_length=10, verbose_name="货币符号", help_text="货币符号")
|
||
factory = models.CharField(max_length=10, verbose_name="交易厂区", help_text="交易厂区(公司)", null=True, blank=True)
|
||
tax = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="税率", help_text="税率")
|
||
status = models.IntegerField(default=1, verbose_name="可用状态", help_text="可用状态(1可用0不可用)")
|
||
|
||
class Meta:
|
||
db_table = table_prefix + "currencies"
|
||
verbose_name = "币别税率"
|
||
verbose_name_plural = verbose_name
|
||
ordering = ("-create_datetime",)
|
||
|
||
def __str__(self):
|
||
return self.currencyname
|
||
|
||
|
||
class Supplier(CoreModel):
|
||
"""供应商信息"""
|
||
company_code = models.CharField(max_length=10, verbose_name="交易厂区", help_text="交易厂区(公司)", null=True, blank=True)
|
||
supplier_id = models.CharField(max_length=20, unique=True, verbose_name="供应商唯一ID", help_text="供应商唯一ID")
|
||
supplier_name = models.CharField(max_length=100, verbose_name="供应商全称", help_text="供应商全称")
|
||
supplier_short_name = models.CharField(max_length=50, verbose_name="供应商简称", help_text="供应商简称")
|
||
vendor_type = models.CharField(max_length=20, verbose_name="厂商性质", help_text="厂商性质", null=True, blank=True)
|
||
payment_terms = models.CharField(max_length=20, verbose_name="付款条件", help_text="付款条件", null=True, blank=True)
|
||
transaction_currency = models.CharField(max_length=20, verbose_name="交易货币", help_text="交易货币", null=True, blank=True)
|
||
incoterms = models.CharField(max_length=20, verbose_name="国际条款", help_text="国际条款", null=True, blank=True)
|
||
supplier_level = models.CharField(max_length=10, verbose_name="供应商等级", help_text="供应商等级", null=True, blank=True)
|
||
contact_person = models.CharField(max_length=20, verbose_name="联络人", help_text="联络人")
|
||
contact_phone = models.CharField(max_length=20, verbose_name="联络人电话", help_text="联络人电话")
|
||
contact_email = models.CharField(max_length=50, verbose_name="联络人邮箱", help_text="联络人邮箱(账号)")
|
||
country = models.CharField(max_length=20, verbose_name="国家", help_text="国家(如TW,CN)")
|
||
province = models.CharField(max_length=20, verbose_name="省州", help_text="省州")
|
||
city = models.CharField(max_length=20, verbose_name="城市", help_text="城市", null=True, blank=True)
|
||
address = models.CharField(max_length=500, verbose_name="详细地址", help_text="详细地址", null=True, blank=True)
|
||
postal_code = models.CharField(max_length=20, verbose_name="邮递区号", help_text="邮递区号", null=True, blank=True)
|
||
status = models.IntegerField(default=1, verbose_name="可用状态", help_text="可用状态(1可用0不可用)")
|
||
|
||
class Meta:
|
||
db_table = table_prefix + "suppliers"
|
||
verbose_name = "供应商信息"
|
||
verbose_name_plural = verbose_name
|
||
ordering = ("-create_datetime",)
|
||
|
||
def __str__(self):
|
||
return self.supplier_name
|
||
|
||
|
||
class Company(CoreModel):
|
||
"""公司信息"""
|
||
company_code = models.CharField(max_length=20, verbose_name="公司代码", help_text="公司代码")
|
||
company_name = models.CharField(max_length=100, verbose_name="公司全称", help_text="公司全称", null=True, blank=True)
|
||
company_short_name = models.CharField(max_length=50, verbose_name="公司简称", help_text="公司简称", null=True, blank=True)
|
||
company_address = models.CharField(max_length=500, verbose_name="公司地址", help_text="公司地址", null=True, blank=True)
|
||
status = models.IntegerField(default=1, verbose_name="可用状态", help_text="可用状态(1可用0不可用)")
|
||
createuser = models.CharField(max_length=20, verbose_name="创建人员", help_text="创建人员", null=True, blank=True)
|
||
updateuser = models.CharField(max_length=20, verbose_name="更新人员", help_text="更新人员", null=True, blank=True)
|
||
|
||
class Meta:
|
||
db_table = table_prefix + "company"
|
||
verbose_name = "公司信息"
|
||
verbose_name_plural = verbose_name
|
||
ordering = ("-create_datetime",)
|
||
|
||
def __str__(self):
|
||
return self.company_name or self.company_code
|
||
|
||
|
||
class SystemNoRule(CoreModel):
|
||
"""单据编号规则;取号请用类方法 `allocate_system_number` / `allocate_system_numbers`。"""
|
||
|
||
# 与前端「通用」厂区、规则维护一致
|
||
DEFAULT_SYSTEM_NO_COMPANY_CODE = "GENERAL"
|
||
|
||
RESET_CYCLE_CHOICES = (
|
||
("yy", "YY"),
|
||
("yyyy", "YYYY"),
|
||
("yymm", "YYMM"),
|
||
("yyyymm", "YYYYMM"),
|
||
("yymmdd", "YYMMDD"),
|
||
("yyyymmdd", "YYYYMMDD"),
|
||
)
|
||
|
||
RESET_CYCLE_STRFTIME = {
|
||
"yy": "%y",
|
||
"yyyy": "%Y",
|
||
"yymm": "%y%m",
|
||
"yyyymm": "%Y%m",
|
||
"yymmdd": "%y%m%d",
|
||
"yyyymmdd": "%Y%m%d",
|
||
}
|
||
|
||
RULE_CODE_CHOICES = (
|
||
("miscQTS", "杂采报价单"),
|
||
("miscRFS", "杂采询价单"),
|
||
)
|
||
|
||
company_code = models.CharField(max_length=20, verbose_name="交易厂区", help_text="交易厂区(公司)")
|
||
rule_code = models.CharField(max_length=50, choices=RULE_CODE_CHOICES, verbose_name="生成单据标识号", help_text="标识一组单号规则")
|
||
reset_cycle = models.CharField(max_length=16, choices=RESET_CYCLE_CHOICES, verbose_name="流水码重置类别", help_text="重置粒度")
|
||
prefix = models.CharField(max_length=20, verbose_name="单据头", help_text="单据开头固定字符", null=True, blank=True)
|
||
factory_code = models.CharField(max_length=20, verbose_name="厂区区分码", help_text="厂区区分码", null=True, blank=True)
|
||
seq_length = models.IntegerField(verbose_name="流水码长度", help_text="流水码长度", default=4)
|
||
createuser = models.CharField(max_length=50, verbose_name="单据创建人", help_text="单据创建人", null=True, blank=True)
|
||
updateuser = models.CharField(max_length=50, verbose_name="单据修改人", help_text="单据修改人", null=True, blank=True)
|
||
sequence_date = models.CharField(
|
||
max_length=12,
|
||
verbose_name="流水日期",
|
||
help_text="与 reset_cycle 对应的日期段(如 yymmdd→260324);新建/改重置类别时由服务端写入",
|
||
null=True,
|
||
blank=True,
|
||
)
|
||
prev_sequence = models.IntegerField(
|
||
verbose_name="上一流水码",
|
||
help_text="上一笔已成功发放的流水号;未发过为 0,与「下一流水码」相差 1",
|
||
default=0,
|
||
)
|
||
current_sequence = models.IntegerField(
|
||
verbose_name="下一流水码",
|
||
help_text="下一笔待发流水号;新建/重置周期后为 1,每次取号成功后 +1",
|
||
default=1,
|
||
)
|
||
last_generate_user = models.CharField(max_length=50, verbose_name="单据最后产生人", help_text="最后产生人", null=True, blank=True)
|
||
last_generate_time = models.DateTimeField(verbose_name="单据最后产生时间", help_text="最后产生时间", null=True, blank=True)
|
||
|
||
@classmethod
|
||
def format_sequence_date(cls, reset_cycle: str, when=None) -> str:
|
||
fmt = cls.RESET_CYCLE_STRFTIME.get(reset_cycle or "")
|
||
if not fmt:
|
||
return ""
|
||
dt = when if when is not None else timezone.now()
|
||
return dt.strftime(fmt)
|
||
|
||
@classmethod
|
||
def sequence_period_matches(cls, rule: SystemNoRule, seq_date: str) -> bool:
|
||
stored = (rule.sequence_date or "").strip()
|
||
return bool(stored) and stored == seq_date
|
||
|
||
@classmethod
|
||
def _allocate_next_code_for_locked_rule(cls, rule: SystemNoRule, now) -> str:
|
||
"""假定 rule 已由 select_for_update 锁定;计算下一单号并更新 rule 内存字段(未 save)。"""
|
||
seq_date = cls.format_sequence_date(rule.reset_cycle, now)
|
||
if not seq_date:
|
||
raise serializers.ValidationError("未支持的流水码重置类别")
|
||
|
||
if not cls.sequence_period_matches(rule, seq_date):
|
||
rule.prev_sequence = 0
|
||
new_seq = 1
|
||
else:
|
||
nxt = int(rule.current_sequence or 0)
|
||
new_seq = nxt if nxt > 0 else 1
|
||
if (
|
||
rule.last_generate_time is not None
|
||
and int(rule.prev_sequence or 0) == 0
|
||
and new_seq == 1
|
||
):
|
||
new_seq = 2
|
||
|
||
slen = max(int(rule.seq_length or 4), 1)
|
||
max_seq = 10**slen - 1
|
||
if new_seq > max_seq:
|
||
raise serializers.ValidationError(
|
||
f"流水码已达上限(长度 {slen} 位,最大 {max_seq}),请调整规则或联系管理员"
|
||
)
|
||
|
||
seq_str = str(new_seq).zfill(slen)
|
||
code = "".join(
|
||
[
|
||
rule.prefix or "",
|
||
rule.factory_code or "",
|
||
seq_date,
|
||
seq_str,
|
||
]
|
||
)
|
||
|
||
rule.sequence_date = seq_date
|
||
rule.prev_sequence = new_seq
|
||
rule.current_sequence = new_seq + 1
|
||
return code
|
||
|
||
@classmethod
|
||
def allocate_system_number(
|
||
cls,
|
||
company_code: str,
|
||
rule_code: str,
|
||
*,
|
||
username: str | None = None,
|
||
now=None,
|
||
) -> str:
|
||
"""
|
||
在事务内对匹配 (company_code, rule_code) 的规则行加锁,生成单号并更新流水字段。
|
||
:param company_code: 交易厂区(与规则表一致,常用 DEFAULT_SYSTEM_NO_COMPANY_CODE)
|
||
:param rule_code: 如 miscRFS / miscQTS
|
||
"""
|
||
codes = cls.allocate_system_numbers(company_code, rule_code, 1, username=username, now=now)
|
||
return codes[0]
|
||
|
||
@classmethod
|
||
def allocate_system_numbers(
|
||
cls,
|
||
company_code: str,
|
||
rule_code: str,
|
||
count: int,
|
||
*,
|
||
username: str | None = None,
|
||
now=None,
|
||
) -> list[str]:
|
||
"""同一事务内连续取 count 个号(一次锁表),用于批量生成报价单等场景。"""
|
||
if count <= 0:
|
||
return []
|
||
|
||
if not (company_code or "").strip() or not (rule_code or "").strip():
|
||
raise serializers.ValidationError("取号参数缺失:company_code 与 rule_code 必填")
|
||
|
||
company_code = str(company_code).strip()
|
||
rule_code = str(rule_code).strip()
|
||
now = now or timezone.now()
|
||
|
||
with transaction.atomic():
|
||
rule = (
|
||
cls.objects.select_for_update()
|
||
.filter(company_code=company_code, rule_code=rule_code)
|
||
.first()
|
||
)
|
||
if not rule:
|
||
raise serializers.ValidationError(
|
||
f"未配置取号规则:rule_code={rule_code},company_code={company_code},请在「单据编号规则」中维护。"
|
||
)
|
||
|
||
codes = []
|
||
for _ in range(count):
|
||
codes.append(cls._allocate_next_code_for_locked_rule(rule, now))
|
||
|
||
rule.last_generate_user = username
|
||
rule.last_generate_time = now
|
||
rule.save(
|
||
update_fields=[
|
||
"sequence_date",
|
||
"prev_sequence",
|
||
"current_sequence",
|
||
"last_generate_user",
|
||
"last_generate_time",
|
||
"update_datetime",
|
||
]
|
||
)
|
||
|
||
return codes
|
||
|
||
class Meta:
|
||
db_table = table_prefix + "system_no_rules"
|
||
verbose_name = "系统单号规则"
|
||
verbose_name_plural = verbose_name
|
||
ordering = ("-create_datetime",)
|
||
unique_together = ("company_code", "rule_code")
|
||
|
||
def __str__(self):
|
||
return f"{self.rule_code}@{self.company_code}"
|
||
|
||
|
||
class EmailNotice(CoreModel):
|
||
STATUS_CHOICES = (
|
||
("pending", "pending"),
|
||
("sending", "sending"),
|
||
("success", "success"),
|
||
("failed", "failed"),
|
||
)
|
||
|
||
subject = models.CharField(max_length=256, verbose_name="邮件主题")
|
||
body = models.TextField(null=True, blank=True, verbose_name="邮件正文")
|
||
to_emails = models.JSONField(default=list, verbose_name="收件人")
|
||
cc_emails = models.JSONField(default=list, verbose_name="抄送")
|
||
bcc_emails = models.JSONField(default=list, verbose_name="密送")
|
||
attachments = models.JSONField(default=list, verbose_name="附件")
|
||
biz_type = models.CharField(max_length=64, null=True, blank=True, verbose_name="业务类型")
|
||
biz_id = models.CharField(max_length=128, null=True, blank=True, verbose_name="业务标识")
|
||
status = models.CharField(max_length=16, choices=STATUS_CHOICES, default="pending", db_index=True, verbose_name="发送状态")
|
||
sent_at = models.DateTimeField(null=True, blank=True, verbose_name="发送时间")
|
||
message_id = models.CharField(max_length=128, null=True, blank=True, verbose_name="消息ID")
|
||
last_error = models.TextField(null=True, blank=True, verbose_name="失败原因")
|
||
payload = models.JSONField(default=dict, verbose_name="发送参数")
|
||
response = models.JSONField(default=dict, verbose_name="服务端响应")
|
||
retry_count = models.IntegerField(default=0, verbose_name="重试次数")
|
||
|
||
class Meta:
|
||
db_table = table_prefix + "proc_email_notice"
|
||
verbose_name = "邮件通知"
|
||
verbose_name_plural = verbose_name
|
||
ordering = ("-update_datetime", "-id")
|
||
|
||
def __str__(self) -> str: # pragma: no cover - simple repr
|
||
return self.subject
|