Files
pyGoEdge-UserPanel/billing/management/commands/generate_invoices.py
2025-11-18 03:36:49 +08:00

200 lines
8.5 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 datetime import date, timedelta
from decimal import Decimal, ROUND_HALF_UP
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.utils import timezone
from django.contrib.auth import get_user_model
from billing.models import Invoice, InvoiceItem
from domains.models import Domain, DomainTrafficDaily
from core.models import SystemSettings
User = get_user_model()
def month_range_from_str(month_str: str):
try:
year, month = map(int, month_str.split('-'))
start = date(year, month, 1)
except Exception as e:
raise CommandError(f"无效的 --month 参数,格式应为 YYYY-MM{e}")
# 下月第一天
if month == 12:
next_month_start = date(year + 1, 1, 1)
else:
next_month_start = date(year, month + 1, 1)
end = next_month_start - timedelta(days=1)
return start, end
def previous_month_range():
today = timezone.now().date()
this_month_start = today.replace(day=1)
last_day_prev = this_month_start - timedelta(days=1)
start_prev = last_day_prev.replace(day=1)
return start_prev, last_day_prev
class Command(BaseCommand):
help = "按自然月生成用户账单(套餐费 + 超量流量)。默认生成上一自然月,可用 --month 指定。"
def add_arguments(self, parser):
parser.add_argument('--month', type=str, help='计费月份,格式 YYYY-MM如 2025-10')
parser.add_argument('--user-id', type=int, help='仅为指定用户生成账单(可选)')
parser.add_argument('--overwrite', action='store_true', help='若已存在账单则覆盖重建(删除后重建)')
parser.add_argument('--dry-run', action='store_true', help='试运行,仅输出计算结果,不写入数据库')
@transaction.atomic
def handle(self, *args, **options):
# 计费周期
if options.get('month'):
period_start, period_end = month_range_from_str(options['month'])
else:
period_start, period_end = previous_month_range()
user_id = options.get('user_id')
overwrite = options.get('overwrite', False)
dry_run = options.get('dry_run', False)
self.stdout.write(self.style.NOTICE(
f"生成账单周期:{period_start}{period_end}"
))
users_qs = User.objects.all()
if user_id:
users_qs = users_qs.filter(id=user_id)
if not users_qs.exists():
raise CommandError(f"用户 {user_id} 不存在")
sys = SystemSettings.objects.order_by('id').first()
total_invoices = 0
total_amount = Decimal('0.00')
for user in users_qs:
# 处理已存在账单
existing = Invoice.objects.filter(user=user, period_start=period_start, period_end=period_end).first()
if existing and not overwrite:
self.stdout.write(self.style.WARNING(
f"跳过用户 {user.id}{user.username}该周期账单已存在Invoice#{existing.id}"
))
continue
if existing and overwrite and not dry_run:
existing.delete()
self.stdout.write(self.style.WARNING(
f"已删除旧账单:用户 {user.id}{user.username} Invoice#{existing.id}"
))
# 汇总用户域名的费用
domains = Domain.objects.filter(user=user, status=Domain.STATUS_ACTIVE)
amount_plan_total = Decimal('0.00')
amount_overage_total = Decimal('0.00')
items = []
for d in domains:
plan = d.current_plan
base_price = Decimal(str(plan.base_price_per_domain)) if plan and plan.base_price_per_domain is not None else Decimal('0.00')
included_gb = Decimal(str(plan.included_traffic_gb_per_domain)) if plan and plan.included_traffic_gb_per_domain is not None else None
overage_price = Decimal(str(plan.overage_price_per_gb)) if plan and plan.overage_price_per_gb is not None else Decimal('0.00')
# 若域名自定义超量单价优先
if d.custom_overage_price_per_gb is not None:
overage_price = Decimal(str(d.custom_overage_price_per_gb))
# 用户级默认免费额度覆盖
user_default_override = None
if hasattr(user, 'profile') and user.profile.default_free_traffic_gb_per_domain_override is not None:
user_default_override = Decimal(str(user.profile.default_free_traffic_gb_per_domain_override))
# 系统全局默认
sys_default_gb = Decimal(str(sys.default_free_traffic_gb_per_domain)) if sys and sys.default_free_traffic_gb_per_domain is not None else Decimal('0')
# 最终基础配额:优先 Plan其次用户覆盖再次系统默认
base_quota_gb = Decimal('0')
if included_gb is not None:
base_quota_gb = Decimal(str(included_gb))
elif user_default_override is not None:
base_quota_gb = user_default_override
else:
base_quota_gb = sys_default_gb
# 本周期域名额外赠送
extra_gb = Decimal(str(d.extra_free_traffic_gb_current_cycle or 0))
# 统计周期用量
traffic_qs = DomainTrafficDaily.objects.filter(domain=d, day__gte=period_start, day__lte=period_end)
total_bytes = sum(t.bytes for t in traffic_qs)
used_gb = Decimal(str(total_bytes)) / Decimal('1073741824') # 1024^3
used_gb = used_gb.quantize(Decimal('0.001'), rounding=ROUND_HALF_UP)
# 超量(不为负)
over_gb = used_gb - (base_quota_gb + extra_gb)
if over_gb < Decimal('0'):
over_gb = Decimal('0')
over_gb = over_gb.quantize(Decimal('0.001'), rounding=ROUND_HALF_UP)
# 费用累计
if base_price > 0:
amount_plan_total += base_price
items.append({
'domain': d,
'description': '基础套餐费用',
'quantity': Decimal('1'),
'unit_price': base_price,
'amount': base_price,
})
if over_gb > 0 and overage_price > 0:
over_amount = (over_gb * overage_price).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
amount_overage_total += over_amount
items.append({
'domain': d,
'description': '超量流量费用',
'quantity': over_gb,
'unit_price': overage_price,
'amount': over_amount,
})
amount_adjustment = Decimal('0.00')
amount_total = (amount_plan_total + amount_overage_total + amount_adjustment).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
if dry_run:
self.stdout.write(self.style.SUCCESS(
f"[DRY] 用户 {user.id}{user.username} 套餐费:¥{amount_plan_total} 超量费:¥{amount_overage_total} 总计:¥{amount_total}"
))
continue
# 创建账单与明细
invoice = Invoice.objects.create(
user=user,
period_start=period_start,
period_end=period_end,
amount_plan_total=amount_plan_total,
amount_overage_total=amount_overage_total,
amount_adjustment=amount_adjustment,
amount_total=amount_total,
status='unpaid',
)
for it in items:
InvoiceItem.objects.create(
invoice=invoice,
domain=it['domain'],
description=it['description'],
quantity=it['quantity'],
unit_price=it['unit_price'],
amount=it['amount'],
)
total_invoices += 1
total_amount += amount_total
self.stdout.write(self.style.SUCCESS(
f"已生成账单Invoice#{invoice.id} 用户 {user.id}{user.username} 总额:¥{amount_total}"
))
self.stdout.write(self.style.SUCCESS(
f"生成完成:共 {total_invoices} 个账单,合计金额 ¥{total_amount}"
))