arc/docs/INVOICE_ACCOUNTING_FIX_SCENARIO.md
2026-04-14 19:34:55 +03:30

541 lines
21 KiB
Markdown
Executable file

# سناریو کامل رفع مشکل شماره 1: ثبت حسابداری مصرف مستقیم، ضایعات و تولید
## خلاصه مشکل
فاکتورهای **مصرف مستقیم**، **ضایعات** و **تولید** هیچ ثبت حسابداری در فاکتور ایجاد نمی‌کنند. ثبت‌های حسابداری فقط در پست حواله انبار انجام می‌شود که می‌تواند منجر به عدم ثبت هزینه/درآمد شود.
---
## اطلاعات حساب‌ها از میگریشن
از فایل `20251011_010001_replace_accounts_chart_seed.py`:
### حساب‌های مربوطه:
1. **70406** - "هزینه ملزومات مصرفی" (Direct Consumption Expense)
- نوع: هزینه عملیاتی
- استفاده: برای مصرف مستقیم
2. **70407** - "هزینه کسری و ضایعات کالا" (Waste Expense)
- نوع: هزینه عملیاتی
- استفاده: برای ضایعات
3. **10106** - "موجودی کالای در جریان ساخت" (Work In Progress - WIP)
- نوع: دارایی جاری
- استفاده: برای تولید (مواد اولیه → WIP → محصول نهایی)
4. **10102** - "موجودی کالا" (Inventory)
- نوع: دارایی جاری
- استفاده: برای موجودی کالا
5. **40001** - "بهای تمام شده کالای فروخته شده" (COGS)
- نوع: بهای تمام شده
- استفاده: برای COGS
---
## سناریو 1: فاکتور مصرف مستقیم (Direct Consumption)
### توضیح:
مصرف مستقیم یعنی استفاده از کالا برای اهداف داخلی (مثلاً مصرف ملزومات اداری، مواد اولیه برای تعمیرات، و غیره).
### منطق حسابداری:
**وقتی کالا از انبار خارج می‌شود:**
```
Dr. هزینه ملزومات مصرفی (70406) - مبلغ بهای تمام‌شده
Cr. موجودی کالا (10102) - مبلغ بهای تمام‌شده
```
### پیاده‌سازی:
**در تابع `create_invoice` برای `INVOICE_DIRECT_CONSUMPTION`:**
```python
elif invoice_type == INVOICE_DIRECT_CONSUMPTION:
# محاسبه COGS برای خطوط
# توجه: COGS باید از حواله انبار محاسبه شود (FIFO/Average)
# اما برای ثبت اولیه در فاکتور، می‌توان از cost_price استفاده کرد
# محاسبه مجموع COGS از خطوط
total_cogs = Decimal(0)
for line in lines_input:
extra_info = line.get("extra_info") or {}
qty = Decimal(str(line.get("quantity", 0) or 0))
# اولویت: cogs_amount > cost_price > unit_price
if extra_info.get("cogs_amount") is not None:
cogs_line = Decimal(str(extra_info.get("cogs_amount")))
elif extra_info.get("cost_price") is not None:
cogs_line = qty * Decimal(str(extra_info.get("cost_price")))
else:
# fallback: استفاده از unit_price (اگر موجود باشد)
unit_price = extra_info.get("unit_price")
if unit_price:
cogs_line = qty * Decimal(str(unit_price))
else:
cogs_line = Decimal(0)
total_cogs += cogs_line
# ثبت حسابداری
if total_cogs > 0:
# بدهکار: هزینه مصرف مستقیم
db.add(DocumentLine(
document_id=document.id,
account_id=accounts["direct_consumption"].id, # 70406
debit=total_cogs,
credit=Decimal(0),
description="هزینه مصرف مستقیم کالا",
))
# بستانکار: موجودی کالا
db.add(DocumentLine(
document_id=document.id,
account_id=accounts["inventory"].id, # 10102
debit=Decimal(0),
credit=total_cogs,
description="خروج کالا از موجودی (مصرف مستقیم)",
))
```
### مثال عددی:
**فاکتور مصرف مستقیم:**
- کالا: کاغذ A4 - 100 برگ
- موجودی: 1000 برگ با بهای تمام‌شده 500,000 ریال
- مصرف: 100 برگ
- COGS: 50,000 ریال (100 * 500)
**ثبت حسابداری:**
```
Dr. هزینه ملزومات مصرفی (70406) 50,000
Cr. موجودی کالا (10102) 50,000
```
---
## سناریو 2: فاکتور ضایعات (Waste)
### توضیح:
ضایعات یعنی کالاهایی که به دلایل مختلف (خرابی، تاریخ مصرف گذشته، و غیره) از بین رفته‌اند.
### منطق حسابداری:
**وقتی کالا به عنوان ضایعات از انبار خارج می‌شود:**
```
Dr. هزینه کسری و ضایعات کالا (70407) - مبلغ بهای تمام‌شده
Cr. موجودی کالا (10102) - مبلغ بهای تمام‌شده
```
### پیاده‌سازی:
**در تابع `create_invoice` برای `INVOICE_WASTE`:**
```python
elif invoice_type == INVOICE_WASTE:
# محاسبه COGS برای خطوط (مشابه مصرف مستقیم)
total_cogs = Decimal(0)
for line in lines_input:
extra_info = line.get("extra_info") or {}
qty = Decimal(str(line.get("quantity", 0) or 0))
if extra_info.get("cogs_amount") is not None:
cogs_line = Decimal(str(extra_info.get("cogs_amount")))
elif extra_info.get("cost_price") is not None:
cogs_line = qty * Decimal(str(extra_info.get("cost_price")))
else:
unit_price = extra_info.get("unit_price")
if unit_price:
cogs_line = qty * Decimal(str(unit_price))
else:
cogs_line = Decimal(0)
total_cogs += cogs_line
# ثبت حسابداری
if total_cogs > 0:
# بدهکار: هزینه ضایعات
db.add(DocumentLine(
document_id=document.id,
account_id=accounts["waste_expense"].id, # 70407
debit=total_cogs,
credit=Decimal(0),
description="هزینه کسری و ضایعات کالا",
))
# بستانکار: موجودی کالا
db.add(DocumentLine(
document_id=document.id,
account_id=accounts["inventory"].id, # 10102
debit=Decimal(0),
credit=total_cogs,
description="خروج کالا از موجودی (ضایعات)",
))
```
### مثال عددی:
**فاکتور ضایعات:**
- کالا: محصول منقضی شده - 50 عدد
- موجودی: 500 عدد با بهای تمام‌شده 1,000,000 ریال
- ضایعات: 50 عدد
- COGS: 100,000 ریال (50 * 2,000)
**ثبت حسابداری:**
```
Dr. هزینه کسری و ضایعات کالا (70407) 100,000
Cr. موجودی کالا (10102) 100,000
```
---
## سناریو 3: فاکتور تولید (Production)
### توضیح:
تولید یعنی تبدیل مواد اولیه به محصول نهایی. این فرآیند شامل دو مرحله است:
1. **خروج مواد اولیه** از موجودی
2. **ورود محصول نهایی** به موجودی
### منطق حسابداری:
**مرحله 1: خروج مواد اولیه (movement: "out")**
```
Dr. موجودی کالای در جریان ساخت (10106) - مبلغ بهای تمام‌شده مواد اولیه
Cr. موجودی کالا (10102) - مبلغ بهای تمام‌شده مواد اولیه
```
**مرحله 2: ورود محصول نهایی (movement: "in")**
```
Dr. موجودی کالا (10102) - مبلغ بهای تمام‌شده محصول نهایی
Cr. موجودی کالای در جریان ساخت (10106) - مبلغ بهای تمام‌شده محصول نهایی
```
**نکته:** مبلغ محصول نهایی معمولاً برابر با مجموع هزینه مواد اولیه + هزینه‌های تولید (دستمزد، سربار) است.
### پیاده‌سازی:
**در تابع `create_invoice` برای `INVOICE_PRODUCTION`:**
```python
elif invoice_type == INVOICE_PRODUCTION:
# جداسازی خطوط ورودی و خروجی
out_lines = [ln for ln in lines_input if (ln.get("extra_info") or {}).get("movement") == "out"]
in_lines = [ln for ln in lines_input if (ln.get("extra_info") or {}).get("movement") == "in"]
# محاسبه COGS برای مواد اولیه (خروج)
total_materials_cost = Decimal(0)
for line in out_lines:
extra_info = line.get("extra_info") or {}
qty = Decimal(str(line.get("quantity", 0) or 0))
if extra_info.get("cogs_amount") is not None:
cogs_line = Decimal(str(extra_info.get("cogs_amount")))
elif extra_info.get("cost_price") is not None:
cogs_line = qty * Decimal(str(extra_info.get("cost_price")))
else:
unit_price = extra_info.get("unit_price")
if unit_price:
cogs_line = qty * Decimal(str(unit_price))
else:
cogs_line = Decimal(0)
total_materials_cost += cogs_line
# محاسبه هزینه محصول نهایی (ورود)
total_finished_cost = Decimal(0)
for line in in_lines:
extra_info = line.get("extra_info") or {}
qty = Decimal(str(line.get("quantity", 0) or 0))
# برای محصول نهایی، cost_price باید شامل مواد اولیه + هزینه‌های تولید باشد
if extra_info.get("cost_price") is not None:
cost_line = qty * Decimal(str(extra_info.get("cost_price")))
elif extra_info.get("unit_price") is not None:
# اگر cost_price موجود نباشد، از unit_price استفاده می‌کنیم
cost_line = qty * Decimal(str(extra_info.get("unit_price")))
else:
# اگر هیچکدام موجود نباشد، از مجموع هزینه مواد اولیه استفاده می‌کنیم
cost_line = total_materials_cost if len(in_lines) == 1 else Decimal(0)
total_finished_cost += cost_line
# ثبت حسابداری برای مواد اولیه (خروج)
if total_materials_cost > 0:
# بدهکار: WIP
db.add(DocumentLine(
document_id=document.id,
account_id=accounts["wip"].id, # 10106
debit=total_materials_cost,
credit=Decimal(0),
description="انتقال مواد اولیه به WIP",
))
# بستانکار: موجودی کالا
db.add(DocumentLine(
document_id=document.id,
account_id=accounts["inventory"].id, # 10102
debit=Decimal(0),
credit=total_materials_cost,
description="خروج مواد اولیه از موجودی",
))
# ثبت حسابداری برای محصول نهایی (ورود)
if total_finished_cost > 0:
# بدهکار: موجودی کالا
db.add(DocumentLine(
document_id=document.id,
account_id=accounts["inventory"].id, # 10102
debit=total_finished_cost,
credit=Decimal(0),
description="ورود محصول نهایی به موجودی",
))
# بستانکار: WIP
db.add(DocumentLine(
document_id=document.id,
account_id=accounts["wip"].id, # 10106
debit=Decimal(0),
credit=total_finished_cost,
description="انتقال محصول نهایی از WIP",
))
```
### مثال عددی:
**فاکتور تولید:**
- **مواد اولیه (خروج):**
- آرد: 100 کیلوگرم با بهای تمام‌شده 500,000 ریال
- شکر: 20 کیلوگرم با بهای تمام‌شده 100,000 ریال
- مجموع: 600,000 ریال
- **محصول نهایی (ورود):**
- نان: 200 عدد با بهای تمام‌شده 3,500 ریال (شامل مواد اولیه + دستمزد + سربار)
- مجموع: 700,000 ریال
**ثبت حسابداری:**
**مرحله 1: خروج مواد اولیه**
```
Dr. موجودی کالای در جریان ساخت (10106) 600,000
Cr. موجودی کالا (10102) 600,000
```
**مرحله 2: ورود محصول نهایی**
```
Dr. موجودی کالا (10102) 700,000
Cr. موجودی کالای در جریان ساخت (10106) 700,000
```
**نتیجه:**
- WIP: 600,000 بدهکار - 700,000 بستانکار = 100,000 بستانکار (زیان تولید)
- موجودی: 600,000 بستانکار - 700,000 بدهکار = 100,000 بدهکار (افزایش موجودی)
**نکته:** اگر هزینه تولید بیشتر از مواد اولیه باشد، این تفاوت باید از حساب‌های هزینه تولید (دستمزد، سربار) تأمین شود. اما در فاکتور تولید فعلی، این هزینه‌ها در فاکتور ثبت نمی‌شوند و باید در اسناد جداگانه ثبت شوند.
---
## نکات مهم پیاده‌سازی
### 1. محاسبه COGS
**اولویت محاسبه COGS:**
1. `cogs_amount` از `extra_info` (اگر از حواله انبار محاسبه شده باشد)
2. `cost_price` از `extra_info` (قیمت تمام‌شده)
3. `unit_price` از `extra_info` (قیمت واحد - fallback)
**نکته:** برای دقت بیشتر، COGS باید از حواله انبار محاسبه شود (FIFO/Average). اما برای ثبت اولیه در فاکتور، می‌توان از `cost_price` استفاده کرد.
### 2. بررسی موجودی
**قبل از ثبت حسابداری:**
- باید بررسی شود که موجودی کافی وجود دارد
- این بررسی در پست حواله انبار انجام می‌شود
- اما در فاکتور، می‌توان هشدار داد
### 3. هماهنگی با حواله انبار
**رویکرد پیشنهادی:**
- ثبت حسابداری در فاکتور انجام می‌شود (برای اطمینان از ثبت)
- در پست حواله انبار، COGS دقیق محاسبه می‌شود (FIFO/Average)
- اگر COGS در حواله متفاوت باشد، باید تعدیل انجام شود
**یا:**
- ثبت حسابداری فقط در پست حواله انبار انجام می‌شود
- اما باید اطمینان حاصل شود که حواله حتماً پست می‌شود
### 4. پروفرما
**برای فاکتورهای پروفرما:**
- ثبت حسابداری انجام نمی‌شود (مشابه سایر فاکتورها)
- فقط در فاکتورهای قطعی ثبت می‌شود
---
## کد کامل برای اضافه کردن
**در تابع `create_invoice`، بخش ثبت حسابداری (خط 1266-1269):**
```python
# Direct consumption / Waste / Production
elif invoice_type in (INVOICE_DIRECT_CONSUMPTION, INVOICE_WASTE, INVOICE_PRODUCTION):
# برای این انواع، ثبت‌های موجودی و بهای تمام‌شده در فاکتور انجام می‌شود
# (نه فقط در پست حواله)
if invoice_type == INVOICE_DIRECT_CONSUMPTION:
# محاسبه COGS
total_cogs = _extract_cogs_total(lines_input)
if total_cogs > 0:
# بدهکار: هزینه مصرف مستقیم
db.add(DocumentLine(
document_id=document.id,
account_id=accounts["direct_consumption"].id,
debit=total_cogs,
credit=Decimal(0),
description="هزینه مصرف مستقیم کالا",
))
# بستانکار: موجودی کالا
db.add(DocumentLine(
document_id=document.id,
account_id=accounts["inventory"].id,
debit=Decimal(0),
credit=total_cogs,
description="خروج کالا از موجودی (مصرف مستقیم)",
))
elif invoice_type == INVOICE_WASTE:
# محاسبه COGS
total_cogs = _extract_cogs_total(lines_input)
if total_cogs > 0:
# بدهکار: هزینه ضایعات
db.add(DocumentLine(
document_id=document.id,
account_id=accounts["waste_expense"].id,
debit=total_cogs,
credit=Decimal(0),
description="هزینه کسری و ضایعات کالا",
))
# بستانکار: موجودی کالا
db.add(DocumentLine(
document_id=document.id,
account_id=accounts["inventory"].id,
debit=Decimal(0),
credit=total_cogs,
description="خروج کالا از موجودی (ضایعات)",
))
elif invoice_type == INVOICE_PRODUCTION:
# جداسازی خطوط ورودی و خروجی
out_lines = [ln for ln in lines_input if (ln.get("extra_info") or {}).get("movement") == "out"]
in_lines = [ln for ln in lines_input if (ln.get("extra_info") or {}).get("movement") == "in"]
# محاسبه COGS برای مواد اولیه (خروج)
total_materials_cost = _extract_cogs_total(out_lines)
# محاسبه هزینه محصول نهایی (ورود)
# برای محصول نهایی، از cost_price استفاده می‌کنیم
total_finished_cost = Decimal(0)
for line in in_lines:
extra_info = line.get("extra_info") or {}
qty = Decimal(str(line.get("quantity", 0) or 0))
if extra_info.get("cost_price") is not None:
cost_line = qty * Decimal(str(extra_info.get("cost_price")))
elif extra_info.get("unit_price") is not None:
cost_line = qty * Decimal(str(extra_info.get("unit_price")))
else:
# fallback: استفاده از مجموع هزینه مواد اولیه
cost_line = total_materials_cost if len(in_lines) == 1 else Decimal(0)
total_finished_cost += cost_line
# ثبت حسابداری برای مواد اولیه (خروج)
if total_materials_cost > 0:
# بدهکار: WIP
db.add(DocumentLine(
document_id=document.id,
account_id=accounts["wip"].id,
debit=total_materials_cost,
credit=Decimal(0),
description="انتقال مواد اولیه به WIP",
))
# بستانکار: موجودی کالا
db.add(DocumentLine(
document_id=document.id,
account_id=accounts["inventory"].id,
debit=Decimal(0),
credit=total_materials_cost,
description="خروج مواد اولیه از موجودی",
))
# ثبت حسابداری برای محصول نهایی (ورود)
if total_finished_cost > 0:
# بدهکار: موجودی کالا
db.add(DocumentLine(
document_id=document.id,
account_id=accounts["inventory"].id,
debit=total_finished_cost,
credit=Decimal(0),
description="ورود محصول نهایی به موجودی",
))
# بستانکار: WIP
db.add(DocumentLine(
document_id=document.id,
account_id=accounts["wip"].id,
debit=Decimal(0),
credit=total_finished_cost,
description="انتقال محصول نهایی از WIP",
))
```
---
## خلاصه تغییرات
### فایل: `hesabixAPI/app/services/invoice_service.py`
**بخش:** تابع `create_invoice`، خط 1266-1269
**تغییر:**
- به جای `pass`، منطق ثبت حسابداری برای مصرف مستقیم، ضایعات و تولید اضافه می‌شود
**تابع کمکی:**
- استفاده از `_extract_cogs_total` که قبلاً وجود دارد
- این تابع COGS را از خطوط محاسبه می‌کند
---
## تست سناریوها
### تست 1: مصرف مستقیم
1. ایجاد فاکتور مصرف مستقیم با یک کالا
2. بررسی ثبت حسابداری:
- بدهکار: 70406
- بستانکار: 10102
3. بررسی تعادل: بدهکار = بستانکار
### تست 2: ضایعات
1. ایجاد فاکتور ضایعات با یک کالا
2. بررسی ثبت حسابداری:
- بدهکار: 70407
- بستانکار: 10102
3. بررسی تعادل: بدهکار = بستانکار
### تست 3: تولید
1. ایجاد فاکتور تولید با مواد اولیه (out) و محصول نهایی (in)
2. بررسی ثبت حسابداری:
- خروج مواد: Dr. 10106, Cr. 10102
- ورود محصول: Dr. 10102, Cr. 10106
3. بررسی تعادل: مجموع بدهکار = مجموع بستانکار
---
**تاریخ:** 2025-01-XX
**تهیه کننده:** AI Assistant