Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixUI/hesabix_ui/lib/models/business_models.dart

654 lines
24 KiB
Dart
Executable file

import 'package:shamsi_date/shamsi_date.dart';
import '../utils/number_normalizer.dart';
enum BusinessType {
company('شرکت'),
shop('مغازه'),
store('فروشگاه'),
union('اتحادیه'),
club('باشگاه'),
institute('موسسه'),
individual('شخصی');
const BusinessType(this.displayName);
final String displayName;
}
enum BusinessField {
manufacturing('تولیدی'),
commercial('بازرگانی'),
service('خدماتی'),
other('سایر');
const BusinessField(this.displayName);
final String displayName;
}
class BusinessData {
// مرحله 1: اطلاعات پایه
String name;
BusinessType? businessType;
BusinessField? businessField;
// مرحله 2: اطلاعات تماس
String? address;
String? phone;
String? mobile;
String? postalCode;
// مرحله 3: اطلاعات قانونی
String? nationalId;
String? registrationNumber;
String? economicId;
// مرحله 4: اطلاعات جغرافیایی
String? country;
String? province;
String? city;
// مرحله 5: سال(های) مالی
List<FiscalYearData> fiscalYears;
// ارزها
int? defaultCurrencyId;
List<int> currencyIds;
/// دادهٔ نمونه همراه ایجاد کسب‌وکار (فقط مسیر فرم؛ ایمپورت .hbx این فیلد را ندارد)
bool includeSampleData;
BusinessData({
this.name = '',
this.businessType,
this.businessField,
this.address,
this.phone,
this.mobile,
this.postalCode,
this.nationalId,
this.registrationNumber,
this.economicId,
this.country,
this.province,
this.city,
List<FiscalYearData>? fiscalYears,
this.defaultCurrencyId,
List<int>? currencyIds,
this.includeSampleData = false,
}) : fiscalYears = fiscalYears ?? <FiscalYearData>[],
currencyIds = currencyIds ?? <int>[];
// تبدیل به Map برای ارسال به API
Map<String, dynamic> toJson() {
return {
'name': name,
// بک‌اند انتظار مقادیر فارسی enum را دارد
'business_type': businessType?.displayName,
'business_field': businessField?.displayName,
'address': address,
'phone': _normalizeDigitsField(phone),
'mobile': _normalizeDigitsField(mobile),
'postal_code': _normalizeDigitsField(postalCode),
'national_id': _normalizeDigitsField(nationalId),
'registration_number': registrationNumber,
'economic_id': economicId,
'country': country,
'province': province,
'city': city,
'fiscal_years': fiscalYears.map((e) => e.toJson()).toList(),
'default_currency_id': defaultCurrencyId,
'currency_ids': _buildCurrencyIdsPayload(),
'include_sample_data': includeSampleData,
};
}
List<int>? _buildCurrencyIdsPayload() {
if (defaultCurrencyId == null && currencyIds.isEmpty) return null;
final setIds = <int>{...currencyIds};
if (defaultCurrencyId != null) setIds.add(defaultCurrencyId!);
return setIds.toList();
}
// کپی کردن با تغییرات
BusinessData copyWith({
String? name,
BusinessType? businessType,
BusinessField? businessField,
String? address,
String? phone,
String? mobile,
String? postalCode,
String? nationalId,
String? registrationNumber,
String? economicId,
String? country,
String? province,
String? city,
List<FiscalYearData>? fiscalYears,
int? defaultCurrencyId,
List<int>? currencyIds,
bool? includeSampleData,
}) {
return BusinessData(
name: name ?? this.name,
businessType: businessType ?? this.businessType,
businessField: businessField ?? this.businessField,
address: address ?? this.address,
phone: phone ?? this.phone,
mobile: mobile ?? this.mobile,
postalCode: postalCode ?? this.postalCode,
nationalId: nationalId ?? this.nationalId,
registrationNumber: registrationNumber ?? this.registrationNumber,
economicId: economicId ?? this.economicId,
country: country ?? this.country,
province: province ?? this.province,
city: city ?? this.city,
fiscalYears: fiscalYears ?? this.fiscalYears,
defaultCurrencyId: defaultCurrencyId ?? this.defaultCurrencyId,
currencyIds: currencyIds ?? this.currencyIds,
includeSampleData: includeSampleData ?? this.includeSampleData,
);
}
// بررسی اعتبار مرحله 1
bool isStep1Valid() {
return name.isNotEmpty && businessType != null && businessField != null;
}
// بررسی اعتبار مرحله 2 (اختیاری)
bool isStep2Valid() {
// اعتبارسنجی موبایل اگر وارد شده باشد
if (mobile != null && mobile!.isNotEmpty) {
if (!_isValidMobile(mobile!)) {
return false;
}
}
// اعتبارسنجی تلفن ثابت اگر وارد شده باشد
if (phone != null && phone!.isNotEmpty) {
if (!_isValidPhone(phone!)) {
return false;
}
}
return true;
}
// بررسی اعتبار مرحله 3 (اختیاری)
bool isStep3Valid() {
// اعتبارسنجی کد ملی اگر وارد شده باشد
if (nationalId != null && nationalId!.isNotEmpty) {
if (!_isValidNationalId(nationalId!)) {
return false;
}
}
return true;
}
// بررسی اعتبار مرحله 4 (اطلاعات جغرافیایی - اختیاری)
bool isStep4Valid() {
return true;
}
// بررسی اعتبار مرحله 5 (سال مالی - اجباری)
bool isFiscalStepValid() {
if (fiscalYears.isEmpty) return false;
final fy = fiscalYears.first;
if (fy.title.trim().isEmpty || fy.startDate == null || fy.endDate == null) return false;
if (fy.startDate!.isAfter(fy.endDate!)) return false;
return true;
}
// بررسی اعتبار ارز پیش‌فرض (الزامی)
bool isCurrencyStepValid() {
return defaultCurrencyId != null;
}
// بررسی اعتبار کل فرم
bool isFormValid() {
return isStep1Valid() && isStep2Valid() && isStep3Valid() && isStep4Valid() && isFiscalStepValid() && isCurrencyStepValid();
}
// اعتبارسنجی شماره موبایل ایرانی
bool _isValidMobile(String mobile) {
// حذف فاصله‌ها و کاراکترهای اضافی
String cleanMobile = _stripPhoneFormatting(mobile);
// بررسی فرمت‌های مختلف موبایل ایرانی
RegExp mobileRegex = RegExp(r'^(\+98|0)?9\d{9}$');
if (!mobileRegex.hasMatch(cleanMobile)) {
return false;
}
// بررسی طول نهایی (باید 11 رقم باشد)
String finalMobile = cleanMobile.startsWith('+98')
? cleanMobile.substring(3)
: cleanMobile.startsWith('0')
? cleanMobile
: '0$cleanMobile';
return finalMobile.length == 11 && finalMobile.startsWith('09');
}
// اعتبارسنجی شماره تلفن ثابت ایرانی
bool _isValidPhone(String phone) {
// حذف فاصله‌ها و کاراکترهای اضافی
String cleanPhone = _stripPhoneFormatting(phone);
// بررسی فرمت‌های مختلف تلفن ثابت ایرانی
RegExp phoneRegex = RegExp(r'^(\+98|0)?[1-9]\d{7,9}$');
if (!phoneRegex.hasMatch(cleanPhone)) {
return false;
}
// بررسی طول نهایی (باید 8-11 رقم باشد)
String finalPhone = cleanPhone.startsWith('+98')
? cleanPhone.substring(3)
: cleanPhone.startsWith('0')
? cleanPhone
: '0$cleanPhone';
return finalPhone.length >= 8 && finalPhone.length <= 11;
}
// اعتبارسنجی کد ملی ایرانی
bool _isValidNationalId(String nationalId) {
// حذف فاصله‌ها و کاراکترهای اضافی
String cleanId = toEnglishDigits(nationalId).replaceAll(RegExp(r'[\s\-]'), '');
// بررسی طول (باید 10 رقم باشد)
if (cleanId.length != 10) {
return false;
}
// بررسی اینکه همه کاراکترها عدد باشند
if (!RegExp(r'^\d{10}$').hasMatch(cleanId)) {
return false;
}
// بررسی الگوریتم کد ملی
int sum = 0;
for (int i = 0; i < 9; i++) {
sum += int.parse(cleanId[i]) * (10 - i);
}
int remainder = sum % 11;
int checkDigit = remainder < 2 ? remainder : 11 - remainder;
return checkDigit == int.parse(cleanId[9]);
}
// دریافت پیام خطای اعتبارسنجی
String? getValidationError(String field) {
switch (field) {
case 'mobile':
if (mobile != null && mobile!.isNotEmpty && !_isValidMobile(mobile!)) {
return 'شماره موبایل نامعتبر است. مثال: 09123456789';
}
break;
case 'phone':
if (phone != null && phone!.isNotEmpty && !_isValidPhone(phone!)) {
return 'شماره تلفن ثابت نامعتبر است. مثال: 02112345678';
}
break;
case 'nationalId':
if (nationalId != null && nationalId!.isNotEmpty && !_isValidNationalId(nationalId!)) {
return 'کد ملی نامعتبر است. مثال: 1234567890';
}
break;
}
return null;
}
String _stripPhoneFormatting(String value) {
final normalized = toEnglishDigits(value);
return normalized.replaceAll(RegExp(r'[\s\-\(\)]'), '');
}
String? _normalizeDigitsField(String? value) {
if (value == null) return null;
final trimmed = value.trim();
if (trimmed.isEmpty) return null;
return toEnglishDigits(trimmed);
}
}
class FiscalYearData {
String title;
DateTime? startDate;
DateTime? endDate;
bool isLast;
FiscalYearData({
this.title = '',
this.startDate,
this.endDate,
this.isLast = true,
});
Map<String, dynamic> toJson() {
return {
'title': title,
'start_date': startDate?.toIso8601String().split('T').first,
'end_date': endDate?.toIso8601String().split('T').first,
'is_last': isLast,
};
}
}
class BusinessResponse {
final int id;
final String name;
final String businessType;
final String businessField;
final int ownerId;
final String? address;
final String? phone;
final String? mobile;
final String? nationalId;
final String? registrationNumber;
final String? economicId;
final String? country;
final String? province;
final String? city;
final String? postalCode;
final String? logoFileId;
final String? stampFileId;
final double? defaultCreditLimit;
final bool checkCreditEnabledByDefault;
final bool publicCatalogShowContact;
final bool publicCatalogShowBaseSalesPrice;
/// منطقهٔ زمانی نمایش (IANA)؛ null = پیش‌فرض سیستم
final String? displayTimezone;
// تنظیمات محاسبه سود فاکتور
final String? invoiceProfitCalculationMethod;
final String? invoiceProfitCalculationBasis;
final bool invoiceProfitIncludeOverhead;
final String? invoiceProfitOverheadType;
final double? invoiceProfitOverheadPercent;
final String? invoiceProfitCalculationType;
/// زمان شناسایی بهای تمام‌شده قطعی در دفتر (با ISO API هم‌نام)
final String? invoiceProfitLedgerRecognitionBasis;
/// کسری لایه در مبنای سود FIFO/LIFO: perpetual_mixed | average_purchase_on_shortage
final String invoiceProfitFifoShortageMode;
final bool invoiceSyncUpdateSalesPriceEnabled;
final bool invoiceSyncUpdatePurchasePriceEnabled;
final String? invoiceSyncSalesPriceBasis;
final String? invoiceSyncPurchasePriceBasis;
/// none | draft | posted
final String invoiceWarehouseReleaseMode;
/// direct_inventory | grni_two_step | grni_legacy
final String invoicePurchaseAccountingMode;
/// reject | use_default_warehouse
final String invoiceMissingLineWarehousePolicy;
final int? invoiceDefaultWarehouseId;
final bool invoiceDefaultWarehouseFillDocumentHeader;
/// خروج با کسری برای کالای فله‌ای هنگام قطعی حواله
final bool allowNegativeInventoryForBulk;
/// خروج با کسری برای کالای یونیک هنگام قطعی حواله
final bool allowNegativeInventoryForUnique;
/// حواله انتقال همیشه کنترل کسری کامل
final bool warehouseTransferRequirePositiveStock;
// تنظیمات کالای هزینه‌شده / کالای درآمدشده
/// simple | two_step
final String goodsExpenseIncomeWorkflowMode;
final bool goodsExpenseIncomeAutoPostInSimpleMode;
final String goodsExpenseIncomeDefaultExpenseAccountCode;
final String goodsExpenseIncomeDefaultIncomeAccountCode;
/// goods_docs | physical_adjustment | ask
final String goodsExpenseIncomeStockCountMode;
final bool goodsExpenseIncomeAllowManualUnitCost;
final bool goodsExpenseIncomeRequirePerson;
/// مبنای درصد تخفیف کلی فاکتور (کد API)
final String invoiceGlobalDiscountPercentBasis;
/// اثر تخفیف کلی بر مالیات (کد API)
final String invoiceGlobalDiscountTaxMode;
final double? invoiceGlobalDiscountMaxPercent;
final double? invoiceGlobalDiscountMaxAmount;
/// سیاست تسعیر فاکتور (as_of_source، document_date_effective، when_no_rate)
final Map<String, dynamic>? fxRevaluationPolicy;
final Map<String, dynamic>? defaultCurrency;
final DateTime createdAt;
final DateTime updatedAt;
// Soft Delete fields
final String? deletedAt;
final String? deletionRequestedAt;
final String? autoDeleteAt;
final bool isDeleted;
final bool isDeletionPending;
final bool? sampleDataSeeded;
final String? sampleDataError;
BusinessResponse({
required this.id,
required this.name,
required this.businessType,
required this.businessField,
required this.ownerId,
this.address,
this.phone,
this.mobile,
this.nationalId,
this.registrationNumber,
this.economicId,
this.country,
this.province,
this.city,
this.postalCode,
this.logoFileId,
this.stampFileId,
this.defaultCreditLimit,
this.checkCreditEnabledByDefault = false,
this.publicCatalogShowContact = false,
this.publicCatalogShowBaseSalesPrice = true,
this.displayTimezone,
this.invoiceProfitCalculationMethod,
this.invoiceProfitCalculationBasis,
this.invoiceProfitIncludeOverhead = false,
this.invoiceProfitOverheadType,
this.invoiceProfitOverheadPercent,
this.invoiceProfitCalculationType,
this.invoiceProfitLedgerRecognitionBasis,
this.invoiceProfitFifoShortageMode = 'perpetual_mixed',
this.invoiceSyncUpdateSalesPriceEnabled = false,
this.invoiceSyncUpdatePurchasePriceEnabled = false,
this.invoiceSyncSalesPriceBasis,
this.invoiceSyncPurchasePriceBasis,
this.invoiceWarehouseReleaseMode = 'draft',
this.invoicePurchaseAccountingMode = 'direct_inventory',
this.invoiceMissingLineWarehousePolicy = 'reject',
this.invoiceDefaultWarehouseId,
this.invoiceDefaultWarehouseFillDocumentHeader = true,
this.allowNegativeInventoryForBulk = false,
this.allowNegativeInventoryForUnique = false,
this.warehouseTransferRequirePositiveStock = true,
this.goodsExpenseIncomeWorkflowMode = 'simple',
this.goodsExpenseIncomeAutoPostInSimpleMode = true,
this.goodsExpenseIncomeDefaultExpenseAccountCode = '70407',
this.goodsExpenseIncomeDefaultIncomeAccountCode = '60103',
this.goodsExpenseIncomeStockCountMode = 'goods_docs',
this.goodsExpenseIncomeAllowManualUnitCost = false,
this.goodsExpenseIncomeRequirePerson = false,
this.invoiceGlobalDiscountPercentBasis = 'subtotal_after_line_discount',
this.invoiceGlobalDiscountTaxMode = 'recalculate_tax_proportional',
this.invoiceGlobalDiscountMaxPercent,
this.invoiceGlobalDiscountMaxAmount,
this.fxRevaluationPolicy,
this.defaultCurrency,
required this.createdAt,
required this.updatedAt,
this.deletedAt,
this.deletionRequestedAt,
this.autoDeleteAt,
this.isDeleted = false,
this.isDeletionPending = false,
this.sampleDataSeeded,
this.sampleDataError,
});
static String _normalizeInvoiceWarehouseReleaseMode(String? raw) {
final s = (raw ?? 'draft').toString().trim().toLowerCase();
if (s == 'none' || s == 'posted' || s == 'draft') return s;
return 'draft';
}
static String _normalizeInvoicePurchaseAccountingMode(String? raw) {
final s = (raw ?? 'direct_inventory').toString().trim().toLowerCase();
if (s == 'direct_inventory' || s == 'grni_two_step' || s == 'grni_legacy') {
return s;
}
return 'direct_inventory';
}
static String _normalizeInvoiceMissingLineWarehousePolicy(String? raw) {
final s = (raw ?? 'reject').toString().trim().toLowerCase();
if (s == 'use_default_warehouse' || s == 'default_warehouse' || s == 'auto_default') {
return 'use_default_warehouse';
}
return 'reject';
}
factory BusinessResponse.fromJson(Map<String, dynamic> json) {
return BusinessResponse(
id: json['id'],
name: json['name'],
businessType: json['business_type'],
businessField: json['business_field'],
ownerId: json['owner_id'],
address: json['address'],
phone: json['phone'],
mobile: json['mobile'],
nationalId: json['national_id'],
registrationNumber: json['registration_number'],
economicId: json['economic_id'],
country: json['country'],
province: json['province'],
city: json['city'],
postalCode: json['postal_code'],
logoFileId: json['logo_file_id'],
stampFileId: json['stamp_file_id'],
defaultCreditLimit: (json['default_credit_limit'] as num?)?.toDouble(),
checkCreditEnabledByDefault: (json['check_credit_enabled_by_default'] as bool?) ?? false,
publicCatalogShowContact: (json['public_catalog_show_contact'] as bool?) ?? false,
publicCatalogShowBaseSalesPrice: (json['public_catalog_show_base_sales_price'] as bool?) ?? true,
displayTimezone: json['display_timezone'] as String?,
invoiceProfitCalculationMethod: json['invoice_profit_calculation_method'] as String?,
invoiceProfitCalculationBasis: json['invoice_profit_calculation_basis'] as String?,
invoiceProfitIncludeOverhead: (json['invoice_profit_include_overhead'] as bool?) ?? false,
invoiceProfitOverheadType: json['invoice_profit_overhead_type'] as String?,
invoiceProfitOverheadPercent: (json['invoice_profit_overhead_percent'] as num?)?.toDouble(),
invoiceProfitCalculationType: json['invoice_profit_calculation_type'] as String?,
invoiceProfitLedgerRecognitionBasis:
(json['invoice_profit_ledger_recognition_basis'] as String?) ??
'warehouse_document_posting',
invoiceProfitFifoShortageMode:
(json['invoice_profit_fifo_shortage_mode'] as String?) ?? 'perpetual_mixed',
invoiceSyncUpdateSalesPriceEnabled:
(json['invoice_sync_update_sales_price_enabled'] as bool?) ?? false,
invoiceSyncUpdatePurchasePriceEnabled:
(json['invoice_sync_update_purchase_price_enabled'] as bool?) ?? false,
invoiceSyncSalesPriceBasis: json['invoice_sync_sales_price_basis'] as String?,
invoiceSyncPurchasePriceBasis: json['invoice_sync_purchase_price_basis'] as String?,
invoiceWarehouseReleaseMode: _normalizeInvoiceWarehouseReleaseMode(
json['invoice_warehouse_release_mode'] as String?,
),
invoicePurchaseAccountingMode: _normalizeInvoicePurchaseAccountingMode(
json['invoice_purchase_accounting_mode'] as String?,
),
invoiceMissingLineWarehousePolicy: _normalizeInvoiceMissingLineWarehousePolicy(
json['invoice_missing_line_warehouse_policy'] as String?,
),
invoiceDefaultWarehouseId: (json['invoice_default_warehouse_id'] as num?)?.toInt(),
invoiceDefaultWarehouseFillDocumentHeader:
(json['invoice_default_warehouse_fill_document_header'] as bool?) ?? true,
allowNegativeInventoryForBulk: (json['allow_negative_inventory_for_bulk'] as bool?) ?? false,
allowNegativeInventoryForUnique: (json['allow_negative_inventory_for_unique'] as bool?) ?? false,
warehouseTransferRequirePositiveStock:
(json['warehouse_transfer_require_positive_stock'] as bool?) ?? true,
goodsExpenseIncomeWorkflowMode:
(json['goods_expense_income_workflow_mode'] as String?) ?? 'simple',
goodsExpenseIncomeAutoPostInSimpleMode:
(json['goods_expense_income_auto_post_in_simple_mode'] as bool?) ?? true,
goodsExpenseIncomeDefaultExpenseAccountCode:
(json['goods_expense_income_default_expense_account_code'] as String?) ?? '70407',
goodsExpenseIncomeDefaultIncomeAccountCode:
(json['goods_expense_income_default_income_account_code'] as String?) ?? '60103',
goodsExpenseIncomeStockCountMode:
(json['goods_expense_income_stock_count_mode'] as String?) ?? 'goods_docs',
goodsExpenseIncomeAllowManualUnitCost:
(json['goods_expense_income_allow_manual_unit_cost'] as bool?) ?? false,
goodsExpenseIncomeRequirePerson:
(json['goods_expense_income_require_person'] as bool?) ?? false,
invoiceGlobalDiscountPercentBasis:
(json['invoice_global_discount_percent_basis'] as String?) ??
'subtotal_after_line_discount',
invoiceGlobalDiscountTaxMode: (json['invoice_global_discount_tax_mode'] as String?) ??
'recalculate_tax_proportional',
invoiceGlobalDiscountMaxPercent:
(json['invoice_global_discount_max_percent'] as num?)?.toDouble(),
invoiceGlobalDiscountMaxAmount:
(json['invoice_global_discount_max_amount'] as num?)?.toDouble(),
fxRevaluationPolicy: json['fx_revaluation_policy'] != null
? Map<String, dynamic>.from(json['fx_revaluation_policy'] as Map)
: null,
defaultCurrency: json['default_currency'] != null ? Map<String, dynamic>.from(json['default_currency'] as Map) : null,
createdAt: _parseDateTime(json['created_at'] ?? json['created_at_raw']),
updatedAt: _parseDateTime(json['updated_at'] ?? json['updated_at_raw']),
deletedAt: json['deleted_at'] as String?,
deletionRequestedAt: json['deletion_requested_at'] as String?,
autoDeleteAt: json['auto_delete_at'] as String?,
isDeleted: (json['is_deleted'] as bool?) ?? false,
isDeletionPending: (json['is_deletion_pending'] as bool?) ?? false,
sampleDataSeeded: json['sample_data_seeded'] as bool?,
sampleDataError: json['sample_data_error'] as String?,
);
}
static DateTime _parseDateTime(dynamic value) {
if (value == null) return DateTime.now();
if (value is DateTime) return value;
if (value is int) {
// epoch ms
return DateTime.fromMillisecondsSinceEpoch(value);
}
if (value is String) {
// Jalali format: YYYY/MM/DD [HH:MM:SS]
if (value.contains('/') && !value.contains('-')) {
try {
final parts = value.split(' ');
final dateParts = parts[0].split('/');
if (dateParts.length == 3) {
final year = int.parse(dateParts[0]);
final month = int.parse(dateParts[1]);
final day = int.parse(dateParts[2]);
int hour = 0, minute = 0, second = 0;
if (parts.length > 1) {
final timeParts = parts[1].split(':');
if (timeParts.length >= 2) {
hour = int.parse(timeParts[0]);
minute = int.parse(timeParts[1]);
if (timeParts.length >= 3) {
second = int.parse(timeParts[2]);
}
}
}
final j = Jalali(year, month, day);
final dt = j.toDateTime();
return DateTime(dt.year, dt.month, dt.day, hour, minute, second);
}
} catch (_) {
// fallthrough
}
}
// ISO or other parseable formats
try {
return DateTime.parse(value);
} catch (_) {
return DateTime.now();
}
}
return DateTime.now();
}
}