Merge branch 'claude/app-50-bugs-fixes-1531ef' into main
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
# Generated by Django 5.1.15 on 2026-09-14 23:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bizapps', '0003_ledgerbook_ledgerentry_book_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='discount_percent',
|
||||
field=models.FloatField(default=0, verbose_name='درصد تخفیف'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='extra_discount_amount',
|
||||
field=models.FloatField(default=0, verbose_name='مبلغ تخفیف ویژه'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='extra_discount_label',
|
||||
field=models.CharField(blank=True, help_text='مثلا: فروش فوق\u200cالعاده، تخفیف دوره\u200cای', max_length=100, verbose_name='عنوان تخفیف ویژه'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='extra_discount_percent',
|
||||
field=models.FloatField(default=0, verbose_name='درصد تخفیف ویژه'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ledgerentry',
|
||||
name='base_amount',
|
||||
field=models.FloatField(blank=True, default=0, verbose_name='مبلغ پایه (پیش از تخفیف/مالیات)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ledgerentry',
|
||||
name='discount_percent',
|
||||
field=models.FloatField(blank=True, default=0, verbose_name='درصد تخفیف (پیش از مالیات)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ledgerentry',
|
||||
name='tax_percent',
|
||||
field=models.FloatField(blank=True, default=0, verbose_name='درصد مالیات/ارزش\u200cافزوده'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='expiry_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='تاریخ انقضا'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=True, verbose_name='فعال'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='production_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='تاریخ تولید'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.15 on 2026-09-15 10:21
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bizapps', '0004_invoice_discount_percent_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='transaction',
|
||||
name='reference_ledger_entry',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to='bizapps.ledgerentry'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,168 @@
|
||||
"""دادهی قدیمی: ردیفهای سادهٔ دفتر (غیرِ فاکتوری) که قبل از این نسخه ثبت
|
||||
شدهاند، هیچوقت سندِ دوطرفه نگرفته بودند — یعنی در ترازِ حسابها/دفتر
|
||||
معین/خروجیِ CSV دیده نمیشدند. این مهاجرت همان سندهایی را که از این پس
|
||||
بهصورت خودکار موقعِ ساخت/ویرایشِ هر ردیف ساخته میشوند، برای دادهی
|
||||
قدیمی هم میسازد.
|
||||
|
||||
عمداً کدِ حسابداریِ فعلی (apps.bizapps.api.accounting/services) را ایمپورت
|
||||
نمیکند — مهاجرتها باید با مدلِ همان لحظه (historical model) کار کنند،
|
||||
نه با کدِ همیشهدرحالتغییرِ اپ؛ به همین خاطر ثابتها و منطقِ لازم همینجا
|
||||
تکرار شدهاند.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
from django.db import migrations
|
||||
from django.utils import timezone
|
||||
|
||||
CATEGORY_ACCOUNT_CODES = {
|
||||
"sale_cash": "4001",
|
||||
"sale_credit": "4001",
|
||||
"service_income": "4002",
|
||||
"other_income": "4003",
|
||||
"purchase_cash": "5001",
|
||||
"purchase_credit": "5001",
|
||||
"rent": "5002",
|
||||
"salary": "5003",
|
||||
"utility": "5004",
|
||||
"transport": "5005",
|
||||
"other_expense": "5006",
|
||||
}
|
||||
DEBT_CATEGORIES = {"sale_credit", "purchase_credit"}
|
||||
ACCOUNT_CASH = "1001"
|
||||
ACCOUNT_RECEIVABLE = "1100"
|
||||
ACCOUNT_PAYABLE = "2100"
|
||||
|
||||
# (code, name, level, account_type, parent_code) — فقط زیرمجموعهای که این
|
||||
# بکفیل واقعاً لازم دارد؛ اگر حسابِ کاربر از قبل ساخته شده باشد، دوباره
|
||||
# ساخته نمیشود (get_or_create).
|
||||
NEEDED_ACCOUNTS = [
|
||||
("1000", "داراییها", 1, "asset", None),
|
||||
("1001", "صندوق", 2, "asset", "1000"),
|
||||
("1100", "حسابهای دریافتنی", 2, "asset", "1000"),
|
||||
("2000", "بدهیها", 1, "liability", None),
|
||||
("2100", "حسابهای پرداختنی", 2, "liability", "2000"),
|
||||
("4000", "درآمدها", 1, "income", None),
|
||||
("4001", "فروش کالا", 2, "income", "4000"),
|
||||
("4002", "درآمد خدمات", 2, "income", "4000"),
|
||||
("4003", "سایر درآمدها", 2, "income", "4000"),
|
||||
("5000", "هزینهها", 1, "expense", None),
|
||||
("5001", "بهای تمامشدهی کالای فروشرفته", 2, "expense", "5000"),
|
||||
("5002", "اجاره", 2, "expense", "5000"),
|
||||
("5003", "حقوق و دستمزد", 2, "expense", "5000"),
|
||||
("5004", "قبض و آبوبرق", 2, "expense", "5000"),
|
||||
("5005", "حملونقل", 2, "expense", "5000"),
|
||||
("5006", "سایر هزینهها", 2, "expense", "5000"),
|
||||
]
|
||||
|
||||
|
||||
def _ensure_account(Account, user_id, code, by_code):
|
||||
if code in by_code:
|
||||
return by_code[code]
|
||||
existing = Account.objects.filter(user_id=user_id, code=code).first()
|
||||
if existing is not None:
|
||||
by_code[code] = existing
|
||||
return existing
|
||||
spec = next((a for a in NEEDED_ACCOUNTS if a[0] == code), None)
|
||||
if spec is None:
|
||||
return None
|
||||
_, name, level, account_type, parent_code = spec
|
||||
parent = _ensure_account(Account, user_id, parent_code, by_code) if parent_code else None
|
||||
account = Account.objects.create(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=user_id,
|
||||
code=code,
|
||||
name=name,
|
||||
level=level,
|
||||
account_type=account_type,
|
||||
parent=parent,
|
||||
is_system=True,
|
||||
is_active=True,
|
||||
)
|
||||
by_code[code] = account
|
||||
return account
|
||||
|
||||
|
||||
def backfill_transactions(apps, schema_editor):
|
||||
LedgerEntry = apps.get_model("bizapps", "LedgerEntry")
|
||||
Account = apps.get_model("bizapps", "Account")
|
||||
Transaction = apps.get_model("bizapps", "Transaction")
|
||||
|
||||
entries = (
|
||||
LedgerEntry.objects.filter(linked_invoice__isnull=True)
|
||||
.exclude(transactions__isnull=False)
|
||||
.order_by("user_id", "entry_date")
|
||||
)
|
||||
|
||||
accounts_by_user: dict[str, dict[str, object]] = {}
|
||||
counters: dict[str, int] = {}
|
||||
|
||||
for entry in entries:
|
||||
code = CATEGORY_ACCOUNT_CODES.get(entry.category)
|
||||
if not code:
|
||||
continue
|
||||
is_debt = entry.category in DEBT_CATEGORIES
|
||||
amount = (entry.total_amount or entry.amount) if is_debt else entry.amount
|
||||
if not amount:
|
||||
continue
|
||||
|
||||
by_code = accounts_by_user.setdefault(entry.user_id, {})
|
||||
account = _ensure_account(Account, entry.user_id, code, by_code)
|
||||
if account is None:
|
||||
continue
|
||||
|
||||
if entry.category == "sale_credit":
|
||||
debit = _ensure_account(Account, entry.user_id, ACCOUNT_RECEIVABLE, by_code)
|
||||
credit = account
|
||||
ttype = "credit_sale"
|
||||
elif entry.category == "purchase_credit":
|
||||
debit = account
|
||||
credit = _ensure_account(Account, entry.user_id, ACCOUNT_PAYABLE, by_code)
|
||||
ttype = "purchase"
|
||||
elif entry.entry_type == "income":
|
||||
debit = _ensure_account(Account, entry.user_id, ACCOUNT_CASH, by_code)
|
||||
credit = account
|
||||
ttype = "cash_sale"
|
||||
else:
|
||||
debit = account
|
||||
credit = _ensure_account(Account, entry.user_id, ACCOUNT_CASH, by_code)
|
||||
ttype = "expense"
|
||||
|
||||
if debit is None or credit is None:
|
||||
continue
|
||||
|
||||
counters[entry.user_id] = counters.get(entry.user_id, 0) + 1
|
||||
number = f"LED-BKFL-{counters[entry.user_id]:06d}"
|
||||
|
||||
naive_dt = timezone.datetime.combine(entry.entry_date, timezone.datetime.min.time())
|
||||
aware_dt = timezone.make_aware(naive_dt) if timezone.is_naive(naive_dt) else naive_dt
|
||||
|
||||
Transaction.objects.create(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=entry.user_id,
|
||||
transaction_number=number,
|
||||
transaction_date=aware_dt,
|
||||
reference_ledger_entry_id=entry.id,
|
||||
transaction_type=ttype,
|
||||
debit_account=debit.code,
|
||||
credit_account=credit.code,
|
||||
debit_account_ref_id=debit.id,
|
||||
credit_account_ref_id=credit.id,
|
||||
amount=amount,
|
||||
description=entry.title,
|
||||
)
|
||||
|
||||
|
||||
def noop(apps, schema_editor):
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bizapps", "0005_transaction_reference_ledger_entry"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(backfill_transactions, noop),
|
||||
]
|
||||
Reference in New Issue
Block a user