forked from hesabix/arc
575 lines
18 KiB
Dart
Executable file
575 lines
18 KiB
Dart
Executable file
/// مدل سند حسابداری (Document)
|
|
class DocumentModel {
|
|
final int id;
|
|
final String code;
|
|
final int businessId;
|
|
final int fiscalYearId;
|
|
final int currencyId;
|
|
final int createdByUserId;
|
|
final DateTime registeredAt;
|
|
final DateTime documentDate;
|
|
final String documentType;
|
|
final bool isProforma;
|
|
final String? description;
|
|
final int? projectId;
|
|
final String? projectName;
|
|
final DateTime createdAt;
|
|
final DateTime updatedAt;
|
|
|
|
// اطلاعات مرتبط
|
|
final String? businessTitle;
|
|
final String? fiscalYearTitle;
|
|
final String? currencyCode;
|
|
final String? currencySymbol;
|
|
final String? createdByName;
|
|
final String? documentTypeName;
|
|
|
|
// محاسبات
|
|
final double totalDebit;
|
|
final double totalCredit;
|
|
final int linesCount;
|
|
|
|
// سطرهای سند (فقط برای جزئیات)
|
|
final List<DocumentLineModel>? lines;
|
|
|
|
// اطلاعات اضافی
|
|
final Map<String, dynamic>? extraInfo;
|
|
final Map<String, dynamic>? developerSettings;
|
|
|
|
// فیلدهای formatted از سرور (برای نمایش)
|
|
/// تاریخ نمایشی سند (بر اساس تقویم انتخابشده کاربر/خروجی سرور)
|
|
/// معمولاً از `document_date_formatted.date_only` یا `document_date` پر میشود.
|
|
final String? documentDateDisplay;
|
|
final String? documentDateRaw;
|
|
final String? registeredAtRaw;
|
|
final String? createdAtRaw;
|
|
final String? updatedAtRaw;
|
|
|
|
DocumentModel({
|
|
required this.id,
|
|
required this.code,
|
|
required this.businessId,
|
|
required this.fiscalYearId,
|
|
required this.currencyId,
|
|
required this.createdByUserId,
|
|
required this.registeredAt,
|
|
required this.documentDate,
|
|
required this.documentType,
|
|
required this.isProforma,
|
|
this.description,
|
|
this.projectId,
|
|
this.projectName,
|
|
required this.createdAt,
|
|
required this.updatedAt,
|
|
this.businessTitle,
|
|
this.fiscalYearTitle,
|
|
this.currencyCode,
|
|
this.currencySymbol,
|
|
this.createdByName,
|
|
this.documentTypeName,
|
|
required this.totalDebit,
|
|
required this.totalCredit,
|
|
required this.linesCount,
|
|
this.lines,
|
|
this.extraInfo,
|
|
this.developerSettings,
|
|
this.documentDateDisplay,
|
|
this.documentDateRaw,
|
|
this.registeredAtRaw,
|
|
this.createdAtRaw,
|
|
this.updatedAtRaw,
|
|
});
|
|
|
|
factory DocumentModel.fromJson(Map<String, dynamic> json) {
|
|
final String? documentDateRawIso = json['document_date_raw'] as String?;
|
|
final dynamic documentDateFormatted = json['document_date_formatted'];
|
|
final String? documentDateDisplay = (documentDateFormatted is Map<String, dynamic>)
|
|
? (documentDateFormatted['date_only'] as String?)
|
|
: null;
|
|
|
|
return DocumentModel(
|
|
id: json['id'] as int,
|
|
code: json['code'] as String,
|
|
businessId: json['business_id'] as int,
|
|
fiscalYearId: json['fiscal_year_id'] as int,
|
|
currencyId: json['currency_id'] as int,
|
|
createdByUserId: json['created_by_user_id'] as int,
|
|
registeredAt: _parseDateTime(json['registered_at_raw'] ?? json['registered_at']),
|
|
// document_date در بسیاری از پاسخها (بهخصوص جلالی) رشتهی غیر-ISO است و قابل parse نیست.
|
|
// برای داشتن DateTime معتبر، از document_date_raw (ISO/Gregorian) استفاده میکنیم.
|
|
documentDate: documentDateRawIso != null
|
|
? DateTime.parse(documentDateRawIso)
|
|
: _parseDateTime(json['document_date']),
|
|
documentType: json['document_type'] as String,
|
|
isProforma: json['is_proforma'] as bool? ?? false,
|
|
description: json['description'] as String?,
|
|
projectId: json['project_id'] as int?,
|
|
projectName: json['project_name'] as String?,
|
|
createdAt: _parseDateTime(json['created_at']),
|
|
updatedAt: _parseDateTime(json['updated_at']),
|
|
businessTitle: json['business_title'] as String?,
|
|
fiscalYearTitle: json['fiscal_year_title'] as String?,
|
|
currencyCode: json['currency_code'] as String?,
|
|
currencySymbol: json['currency_symbol'] as String?,
|
|
createdByName: json['created_by_name'] as String?,
|
|
documentTypeName: json['document_type_name'] as String?,
|
|
totalDebit: (json['total_debit'] as num?)?.toDouble() ?? 0.0,
|
|
totalCredit: (json['total_credit'] as num?)?.toDouble() ?? 0.0,
|
|
linesCount: json['lines_count'] as int? ?? 0,
|
|
lines: json['lines'] != null
|
|
? (json['lines'] as List)
|
|
.map((line) => DocumentLineModel.fromJson(line as Map<String, dynamic>))
|
|
.toList()
|
|
: null,
|
|
extraInfo: json['extra_info'] as Map<String, dynamic>?,
|
|
developerSettings: json['developer_settings'] as Map<String, dynamic>?,
|
|
documentDateDisplay: documentDateDisplay ??
|
|
(json['document_date'] as String?) ??
|
|
documentDateRawIso,
|
|
documentDateRaw: documentDateRawIso ?? json['document_date'] as String?,
|
|
registeredAtRaw: json['registered_at_raw'] as String? ?? json['registered_at'] as String?,
|
|
createdAtRaw: json['created_at_raw'] as String? ?? json['created_at'] as String?,
|
|
updatedAtRaw: json['updated_at_raw'] as String? ?? json['updated_at'] as String?,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'id': id,
|
|
'code': code,
|
|
'business_id': businessId,
|
|
'fiscal_year_id': fiscalYearId,
|
|
'currency_id': currencyId,
|
|
'created_by_user_id': createdByUserId,
|
|
'registered_at': registeredAt.toIso8601String(),
|
|
'document_date': documentDate.toIso8601String(),
|
|
'document_type': documentType,
|
|
'is_proforma': isProforma,
|
|
'description': description,
|
|
if (projectId != null) 'project_id': projectId,
|
|
'created_at': createdAt.toIso8601String(),
|
|
'updated_at': updatedAt.toIso8601String(),
|
|
'business_title': businessTitle,
|
|
'fiscal_year_title': fiscalYearTitle,
|
|
'currency_code': currencyCode,
|
|
'currency_symbol': currencySymbol,
|
|
'created_by_name': createdByName,
|
|
'total_debit': totalDebit,
|
|
'total_credit': totalCredit,
|
|
'lines_count': linesCount,
|
|
'lines': lines?.map((line) => line.toJson()).toList(),
|
|
'extra_info': extraInfo,
|
|
'developer_settings': developerSettings,
|
|
};
|
|
}
|
|
|
|
/// دریافت نام فارسی نوع سند
|
|
String getDocumentTypeName() {
|
|
// اگر document_type_name از سرور آمده باشد، از آن استفاده کن
|
|
if (documentTypeName != null && documentTypeName!.isNotEmpty) {
|
|
return documentTypeName!;
|
|
}
|
|
|
|
// در غیر این صورت از switch case استفاده کن
|
|
switch (documentType) {
|
|
case 'expense':
|
|
return 'هزینه';
|
|
case 'income':
|
|
return 'درآمد';
|
|
case 'receipt':
|
|
return 'دریافت';
|
|
case 'payment':
|
|
return 'پرداخت';
|
|
case 'transfer':
|
|
return 'انتقال';
|
|
case 'manual':
|
|
return 'سند دستی';
|
|
case 'invoice':
|
|
return 'فاکتور';
|
|
case 'invoice_sales':
|
|
return 'فروش';
|
|
case 'invoice_sales_return':
|
|
return 'برگشت از فروش';
|
|
case 'invoice_purchase':
|
|
return 'خرید';
|
|
case 'invoice_purchase_return':
|
|
return 'برگشت از خرید';
|
|
case 'invoice_direct_consumption':
|
|
return 'مصرف مستقیم';
|
|
case 'invoice_production':
|
|
return 'تولید';
|
|
case 'invoice_waste':
|
|
return 'ضایعات';
|
|
case 'production':
|
|
return 'تولید';
|
|
case 'opening_balance':
|
|
return 'موجودی اولیه';
|
|
case 'check':
|
|
return 'چک';
|
|
default:
|
|
return documentType;
|
|
}
|
|
}
|
|
|
|
/// آیا سند قابل ویرایش است؟
|
|
bool get isEditable => documentType == 'manual';
|
|
|
|
/// آیا سند قابل حذف است؟
|
|
bool get isDeletable => documentType == 'manual';
|
|
|
|
/// دریافت وضعیت سند
|
|
String get statusText => isProforma ? 'پیشنویس' : 'قطعی';
|
|
|
|
/// Parse DateTime from various formats (prefers UTC ISO with Z from API *_raw fields).
|
|
static DateTime _parseDateTime(dynamic value) {
|
|
if (value == null) return DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
|
if (value is DateTime) return value;
|
|
|
|
final dateStr = value.toString().trim();
|
|
if (dateStr.isEmpty) return DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
|
|
|
try {
|
|
if (dateStr.endsWith('Z')) {
|
|
return DateTime.parse(dateStr).toLocal();
|
|
}
|
|
return DateTime.parse(dateStr).toLocal();
|
|
} catch (_) {
|
|
// Jalali display string from API — cannot convert to DateTime reliably
|
|
return DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// مدل سطر سند حسابداری (Document Line)
|
|
class DocumentLineModel {
|
|
final int id;
|
|
final int documentId;
|
|
final int? accountId;
|
|
final int? personId;
|
|
final int? productId;
|
|
final int? bankAccountId;
|
|
final int? cashRegisterId;
|
|
final int? pettyCashId;
|
|
final int? checkId;
|
|
final double? quantity;
|
|
final double debit;
|
|
final double credit;
|
|
final String? description;
|
|
final Map<String, dynamic>? extraInfo;
|
|
final DateTime createdAt;
|
|
final DateTime updatedAt;
|
|
|
|
// اطلاعات مرتبط
|
|
final String? accountCode;
|
|
final String? accountName;
|
|
final String? personName;
|
|
final String? productName;
|
|
final String? bankAccountName;
|
|
final String? cashRegisterName;
|
|
final String? pettyCashName;
|
|
final String? checkNumber;
|
|
|
|
DocumentLineModel({
|
|
required this.id,
|
|
required this.documentId,
|
|
this.accountId,
|
|
this.personId,
|
|
this.productId,
|
|
this.bankAccountId,
|
|
this.cashRegisterId,
|
|
this.pettyCashId,
|
|
this.checkId,
|
|
this.quantity,
|
|
required this.debit,
|
|
required this.credit,
|
|
this.description,
|
|
this.extraInfo,
|
|
required this.createdAt,
|
|
required this.updatedAt,
|
|
this.accountCode,
|
|
this.accountName,
|
|
this.personName,
|
|
this.productName,
|
|
this.bankAccountName,
|
|
this.cashRegisterName,
|
|
this.pettyCashName,
|
|
this.checkNumber,
|
|
});
|
|
|
|
factory DocumentLineModel.fromJson(Map<String, dynamic> json) {
|
|
return DocumentLineModel(
|
|
id: json['id'] as int,
|
|
documentId: json['document_id'] as int,
|
|
accountId: json['account_id'] as int?,
|
|
personId: json['person_id'] as int?,
|
|
productId: json['product_id'] as int?,
|
|
bankAccountId: json['bank_account_id'] as int?,
|
|
cashRegisterId: json['cash_register_id'] as int?,
|
|
pettyCashId: json['petty_cash_id'] as int?,
|
|
checkId: json['check_id'] as int?,
|
|
quantity: (json['quantity'] as num?)?.toDouble(),
|
|
debit: (json['debit'] as num?)?.toDouble() ?? 0.0,
|
|
credit: (json['credit'] as num?)?.toDouble() ?? 0.0,
|
|
description: json['description'] as String?,
|
|
extraInfo: json['extra_info'] as Map<String, dynamic>?,
|
|
createdAt: DocumentModel._parseDateTime(json['created_at']),
|
|
updatedAt: DocumentModel._parseDateTime(json['updated_at']),
|
|
accountCode: json['account_code'] as String?,
|
|
accountName: json['account_name'] as String?,
|
|
personName: json['person_name'] as String?,
|
|
productName: json['product_name'] as String?,
|
|
bankAccountName: json['bank_account_name'] as String?,
|
|
cashRegisterName: json['cash_register_name'] as String?,
|
|
pettyCashName: json['petty_cash_name'] as String?,
|
|
checkNumber: json['check_number'] as String?,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'id': id,
|
|
'document_id': documentId,
|
|
'account_id': accountId,
|
|
'person_id': personId,
|
|
'product_id': productId,
|
|
'bank_account_id': bankAccountId,
|
|
'cash_register_id': cashRegisterId,
|
|
'petty_cash_id': pettyCashId,
|
|
'check_id': checkId,
|
|
'quantity': quantity,
|
|
'debit': debit,
|
|
'credit': credit,
|
|
'description': description,
|
|
'extra_info': extraInfo,
|
|
'created_at': createdAt.toIso8601String(),
|
|
'updated_at': updatedAt.toIso8601String(),
|
|
'account_code': accountCode,
|
|
'account_name': accountName,
|
|
'person_name': personName,
|
|
'product_name': productName,
|
|
'bank_account_name': bankAccountName,
|
|
'cash_register_name': cashRegisterName,
|
|
'petty_cash_name': pettyCashName,
|
|
'check_number': checkNumber,
|
|
};
|
|
}
|
|
|
|
/// دریافت نام کامل حساب
|
|
String get fullAccountName {
|
|
if (accountCode != null && accountName != null) {
|
|
return '$accountCode - $accountName';
|
|
}
|
|
return accountName ?? accountCode ?? '-';
|
|
}
|
|
|
|
/// دریافت نام طرفحساب
|
|
String? get counterpartyName {
|
|
return personName ??
|
|
bankAccountName ??
|
|
cashRegisterName ??
|
|
pettyCashName ??
|
|
checkNumber;
|
|
}
|
|
}
|
|
|
|
|
|
/// مدل درخواست ایجاد سطر سند
|
|
class DocumentLineCreateRequest {
|
|
final int accountId;
|
|
final int? personId;
|
|
final int? productId;
|
|
final int? bankAccountId;
|
|
final int? cashRegisterId;
|
|
final int? pettyCashId;
|
|
final int? checkId;
|
|
final double? quantity;
|
|
final double debit;
|
|
final double credit;
|
|
final String? description;
|
|
final Map<String, dynamic>? extraInfo;
|
|
|
|
DocumentLineCreateRequest({
|
|
required this.accountId,
|
|
this.personId,
|
|
this.productId,
|
|
this.bankAccountId,
|
|
this.cashRegisterId,
|
|
this.pettyCashId,
|
|
this.checkId,
|
|
this.quantity,
|
|
this.debit = 0,
|
|
this.credit = 0,
|
|
this.description,
|
|
this.extraInfo,
|
|
});
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'account_id': accountId,
|
|
if (personId != null) 'person_id': personId,
|
|
if (productId != null) 'product_id': productId,
|
|
if (bankAccountId != null) 'bank_account_id': bankAccountId,
|
|
if (cashRegisterId != null) 'cash_register_id': cashRegisterId,
|
|
if (pettyCashId != null) 'petty_cash_id': pettyCashId,
|
|
if (checkId != null) 'check_id': checkId,
|
|
if (quantity != null) 'quantity': quantity,
|
|
'debit': debit,
|
|
'credit': credit,
|
|
if (description != null) 'description': description,
|
|
if (extraInfo != null) 'extra_info': extraInfo,
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
/// مدل درخواست ایجاد سند دستی
|
|
class CreateManualDocumentRequest {
|
|
final String? code;
|
|
final DateTime documentDate;
|
|
final int? fiscalYearId;
|
|
final int currencyId;
|
|
final bool isProforma;
|
|
final String? description;
|
|
final List<DocumentLineCreateRequest> lines;
|
|
final Map<String, dynamic>? extraInfo;
|
|
final int? projectId;
|
|
|
|
CreateManualDocumentRequest({
|
|
this.code,
|
|
required this.documentDate,
|
|
this.fiscalYearId,
|
|
required this.currencyId,
|
|
this.isProforma = false,
|
|
this.description,
|
|
required this.lines,
|
|
this.extraInfo,
|
|
this.projectId,
|
|
});
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
if (code != null) 'code': code,
|
|
'document_date': documentDate.toIso8601String().split('T')[0], // فقط تاریخ
|
|
if (fiscalYearId != null) 'fiscal_year_id': fiscalYearId,
|
|
'currency_id': currencyId,
|
|
'is_proforma': isProforma,
|
|
if (description != null) 'description': description,
|
|
'lines': lines.map((line) => line.toJson()).toList(),
|
|
if (extraInfo != null) 'extra_info': extraInfo,
|
|
if (projectId != null) 'project_id': projectId,
|
|
};
|
|
}
|
|
|
|
/// اعتبارسنجی درخواست
|
|
String? validate() {
|
|
if (lines.length < 2) {
|
|
return 'سند باید حداقل 2 سطر داشته باشد';
|
|
}
|
|
|
|
double totalDebit = 0;
|
|
double totalCredit = 0;
|
|
|
|
for (int i = 0; i < lines.length; i++) {
|
|
final line = lines[i];
|
|
|
|
// بررسی که هر سطر یا بدهکار یا بستانکار داشته باشد
|
|
if (line.debit == 0 && line.credit == 0) {
|
|
return 'سطر ${i + 1} باید مقدار بدهکار یا بستانکار داشته باشد';
|
|
}
|
|
|
|
// نمیتواند هم بدهکار هم بستانکار داشته باشد
|
|
if (line.debit > 0 && line.credit > 0) {
|
|
return 'سطر ${i + 1} نمیتواند همزمان بدهکار و بستانکار داشته باشد';
|
|
}
|
|
|
|
totalDebit += line.debit;
|
|
totalCredit += line.credit;
|
|
}
|
|
|
|
// بررسی متوازن بودن سند
|
|
if ((totalDebit - totalCredit).abs() > 0.01) {
|
|
final diff = totalDebit - totalCredit;
|
|
return 'سند متوازن نیست. تفاوت: ${diff.toStringAsFixed(2)}';
|
|
}
|
|
|
|
return null; // اعتبارسنجی موفق
|
|
}
|
|
}
|
|
|
|
|
|
/// مدل درخواست ویرایش سند دستی
|
|
class UpdateManualDocumentRequest {
|
|
final String? code;
|
|
final DateTime? documentDate;
|
|
final int? currencyId;
|
|
final bool? isProforma;
|
|
final String? description;
|
|
final List<DocumentLineCreateRequest>? lines;
|
|
final Map<String, dynamic>? extraInfo;
|
|
final int? projectId;
|
|
|
|
UpdateManualDocumentRequest({
|
|
this.code,
|
|
this.documentDate,
|
|
this.currencyId,
|
|
this.isProforma,
|
|
this.description,
|
|
this.lines,
|
|
this.extraInfo,
|
|
this.projectId,
|
|
});
|
|
|
|
Map<String, dynamic> toJson() {
|
|
final map = <String, dynamic>{};
|
|
|
|
if (code != null) map['code'] = code;
|
|
if (documentDate != null) {
|
|
map['document_date'] = documentDate!.toIso8601String().split('T')[0];
|
|
}
|
|
if (currencyId != null) map['currency_id'] = currencyId;
|
|
if (isProforma != null) map['is_proforma'] = isProforma;
|
|
if (description != null) map['description'] = description;
|
|
if (lines != null) {
|
|
map['lines'] = lines!.map((line) => line.toJson()).toList();
|
|
}
|
|
if (extraInfo != null) map['extra_info'] = extraInfo;
|
|
if (projectId != null) map['project_id'] = projectId;
|
|
|
|
return map;
|
|
}
|
|
|
|
/// اعتبارسنجی درخواست
|
|
String? validate() {
|
|
if (lines != null) {
|
|
if (lines!.length < 2) {
|
|
return 'سند باید حداقل 2 سطر داشته باشد';
|
|
}
|
|
|
|
double totalDebit = 0;
|
|
double totalCredit = 0;
|
|
|
|
for (int i = 0; i < lines!.length; i++) {
|
|
final line = lines![i];
|
|
|
|
if (line.debit == 0 && line.credit == 0) {
|
|
return 'سطر ${i + 1} باید مقدار بدهکار یا بستانکار داشته باشد';
|
|
}
|
|
|
|
if (line.debit > 0 && line.credit > 0) {
|
|
return 'سطر ${i + 1} نمیتواند همزمان بدهکار و بستانکار داشته باشد';
|
|
}
|
|
|
|
totalDebit += line.debit;
|
|
totalCredit += line.credit;
|
|
}
|
|
|
|
if ((totalDebit - totalCredit).abs() > 0.01) {
|
|
final diff = totalDebit - totalCredit;
|
|
return 'سند متوازن نیست. تفاوت: ${diff.toStringAsFixed(2)}';
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|