progress in invoices

This commit is contained in:
Hesabix 2025-10-07 01:32:30 +03:30
parent 7f6a78f642
commit 2591e9a7a9
4 changed files with 121 additions and 49 deletions

View file

@ -479,6 +479,7 @@ class AuthStore with ChangeNotifier {
Future<void> _ensureCurrencyForBusiness() async { Future<void> _ensureCurrencyForBusiness() async {
final business = _currentBusiness; final business = _currentBusiness;
if (business == null) return; if (business == null) return;
// اگر ارزی انتخاب نشده، یا کد/شناسه فعلی جزو ارزهای کسبوکار نیست // اگر ارزی انتخاب نشده، یا کد/شناسه فعلی جزو ارزهای کسبوکار نیست
final allowedCodes = business.currencies.map((c) => c.code).toSet(); final allowedCodes = business.currencies.map((c) => c.code).toSet();
final allowedIds = business.currencies.map((c) => c.id).toSet(); final allowedIds = business.currencies.map((c) => c.id).toSet();

View file

@ -18,6 +18,8 @@ import '../../models/customer_model.dart';
import '../../models/person_model.dart'; import '../../models/person_model.dart';
import '../../widgets/invoice/line_items_table.dart'; import '../../widgets/invoice/line_items_table.dart';
import '../../utils/number_formatters.dart'; import '../../utils/number_formatters.dart';
import '../../services/currency_service.dart';
import '../../core/api_client.dart';
class NewInvoicePage extends StatefulWidget { class NewInvoicePage extends StatefulWidget {
final int businessId; final int businessId;
@ -62,8 +64,37 @@ class _NewInvoicePageState extends State<NewInvoicePage> with SingleTickerProvid
void initState() { void initState() {
super.initState(); super.initState();
_tabController = TabController(length: 4, vsync: this); // شروع با 4 تب _tabController = TabController(length: 4, vsync: this); // شروع با 4 تب
// تنظیم نوع فاکتور پیشفرض
_selectedInvoiceType = InvoiceType.sales;
// تنظیم ارز پیشفرض از AuthStore // تنظیم ارز پیشفرض از AuthStore
_selectedCurrencyId = widget.authStore.selectedCurrencyId; _selectedCurrencyId = widget.authStore.selectedCurrencyId;
// اگر ارز انتخاب نشده، ارز پیشفرض را بارگذاری کن
if (_selectedCurrencyId == null) {
_loadDefaultCurrency();
}
// تنظیم تاریخهای پیشفرض
_invoiceDate = DateTime.now();
_dueDate = DateTime.now();
}
Future<void> _loadDefaultCurrency() async {
try {
final currencyService = CurrencyService(ApiClient());
final currencies = await currencyService.listBusinessCurrencies(businessId: widget.businessId);
if (currencies.isNotEmpty) {
// ارز پیشفرض را پیدا کن
final defaultCurrency = currencies.firstWhere(
(c) => c['is_default'] == true,
orElse: () => currencies.first,
);
setState(() {
_selectedCurrencyId = defaultCurrency['id'] as int;
});
// ارز پیشفرض بارگذاری شد
}
} catch (e) {
// خطا در بارگذاری ارز پیشفرض
}
} }

View file

@ -29,13 +29,25 @@ class _CurrencyPickerWidgetState extends State<CurrencyPickerWidget> {
List<Map<String, dynamic>> _currencies = []; List<Map<String, dynamic>> _currencies = [];
bool _isLoading = false; bool _isLoading = false;
String? _error; String? _error;
int? _selectedValue;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_selectedValue = widget.selectedCurrencyId;
_loadCurrencies(); _loadCurrencies();
} }
@override
void didUpdateWidget(CurrencyPickerWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.selectedCurrencyId != widget.selectedCurrencyId) {
setState(() {
_selectedValue = widget.selectedCurrencyId;
});
}
}
Future<void> _loadCurrencies() async { Future<void> _loadCurrencies() async {
setState(() { setState(() {
_isLoading = true; _isLoading = true;
@ -61,21 +73,33 @@ class _CurrencyPickerWidgetState extends State<CurrencyPickerWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isLoading) { if (_isLoading) {
return const SizedBox( return SizedBox(
height: 56, height: 56,
child: Center( child: InputDecorator(
decoration: InputDecoration(
labelText: widget.label ?? 'ارز',
hintText: widget.hintText ?? 'انتخاب ارز',
border: const OutlineInputBorder(),
enabled: false,
),
child: const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
),
); );
} }
if (_error != null) { if (_error != null) {
return Container( return SizedBox(
height: 56, height: 56,
padding: const EdgeInsets.all(16), child: InputDecorator(
decoration: BoxDecoration( decoration: InputDecoration(
border: Border.all(color: Colors.red), labelText: widget.label ?? 'ارز',
borderRadius: BorderRadius.circular(8), hintText: widget.hintText ?? 'انتخاب ارز',
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.red),
),
enabled: false,
), ),
child: Row( child: Row(
children: [ children: [
@ -83,7 +107,7 @@ class _CurrencyPickerWidgetState extends State<CurrencyPickerWidget> {
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
'خطا در بارگذاری ارزها: $_error', 'خطا در بارگذاری ارزها',
style: const TextStyle(color: Colors.red), style: const TextStyle(color: Colors.red),
), ),
), ),
@ -93,32 +117,46 @@ class _CurrencyPickerWidgetState extends State<CurrencyPickerWidget> {
), ),
], ],
), ),
),
); );
} }
if (_currencies.isEmpty) { if (_currencies.isEmpty) {
return Container( return SizedBox(
height: 56, height: 56,
padding: const EdgeInsets.all(16), child: InputDecorator(
decoration: BoxDecoration( decoration: InputDecoration(
border: Border.all(color: Colors.grey), labelText: widget.label ?? 'ارز',
borderRadius: BorderRadius.circular(8), hintText: widget.hintText ?? 'انتخاب ارز',
border: const OutlineInputBorder(),
enabled: false,
), ),
child: const Center( child: const Center(
child: Text('هیچ ارزی یافت نشد'), child: Text('هیچ ارزی یافت نشد'),
), ),
),
); );
} }
return DropdownButtonFormField<int>( return SizedBox(
value: widget.selectedCurrencyId, height: 56, // ارتفاع ثابت مثل سایر فیلدها
onChanged: widget.enabled ? widget.onChanged : null, child: InputDecorator(
decoration: InputDecoration( decoration: InputDecoration(
labelText: widget.label ?? 'ارز', labelText: widget.label ?? 'ارز',
hintText: widget.hintText ?? 'انتخاب ارز', hintText: widget.hintText ?? 'انتخاب ارز',
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
enabled: widget.enabled, enabled: widget.enabled,
), ),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: _selectedValue,
isExpanded: true,
onChanged: widget.enabled ? (value) {
setState(() {
_selectedValue = value;
});
widget.onChanged(value);
} : null,
items: _currencies.map((currency) { items: _currencies.map((currency) {
final isDefault = currency['is_default'] == true; final isDefault = currency['is_default'] == true;
return DropdownMenuItem<int>( return DropdownMenuItem<int>(
@ -153,12 +191,9 @@ class _CurrencyPickerWidgetState extends State<CurrencyPickerWidget> {
), ),
); );
}).toList(), }).toList(),
validator: (value) { ),
if (value == null) { ),
return 'انتخاب ارز الزامی است'; ),
}
return null;
},
); );
} }
} }

View file

@ -164,7 +164,8 @@ class _InvoiceLineItemsTableState extends State<InvoiceLineItemsTable> {
if (!isTaxable) return 0; if (!isTaxable) return 0;
final v = p['purchase_tax_rate']; final v = p['purchase_tax_rate'];
if (v is num && v > 0) return v; final rate = _toNum(v);
if (rate > 0) return rate;
// اگر محصول نرخ مالیات خرید نداشته باشد، از نرخ پیشفرض استفاده کن // اگر محصول نرخ مالیات خرید نداشته باشد، از نرخ پیشفرض استفاده کن
return _getDefaultTaxRateForInvoiceType(); return _getDefaultTaxRateForInvoiceType();
} }
@ -174,7 +175,8 @@ class _InvoiceLineItemsTableState extends State<InvoiceLineItemsTable> {
if (!isTaxable) return 0; if (!isTaxable) return 0;
final v = p['sales_tax_rate']; final v = p['sales_tax_rate'];
if (v is num && v > 0) return v; final rate = _toNum(v);
if (rate > 0) return rate;
// اگر محصول نرخ مالیات فروش نداشته باشد، از نرخ پیشفرض استفاده کن // اگر محصول نرخ مالیات فروش نداشته باشد، از نرخ پیشفرض استفاده کن
return _getDefaultTaxRateForInvoiceType(); return _getDefaultTaxRateForInvoiceType();
} }
@ -353,8 +355,11 @@ class _InvoiceLineItemsTableState extends State<InvoiceLineItemsTable> {
_notify(); _notify();
return; return;
} }
final mainUnit = p['main_unit']?.toString(); final mainUnit = p['main_unit']?.toString();
final secondaryUnit = p['secondary_unit']?.toString(); final secondaryUnit = p['secondary_unit']?.toString();
final taxRate = _defaultTaxRateFromProduct(p);
final updated = item.copyWith( final updated = item.copyWith(
productId: _toInt(p['id']), productId: _toInt(p['id']),
productCode: p['code']?.toString(), productCode: p['code']?.toString(),
@ -365,7 +370,7 @@ class _InvoiceLineItemsTableState extends State<InvoiceLineItemsTable> {
selectedUnit: mainUnit, selectedUnit: mainUnit,
baseSalesPriceMainUnit: _toNum(p['base_sales_price']), baseSalesPriceMainUnit: _toNum(p['base_sales_price']),
basePurchasePriceMainUnit: _toNum(p['base_purchase_price']), basePurchasePriceMainUnit: _toNum(p['base_purchase_price']),
taxRate: _defaultTaxRateFromProduct(p), taxRate: taxRate,
minOrderQty: _toInt(p['min_order_qty']), minOrderQty: _toInt(p['min_order_qty']),
trackInventory: p['track_inventory'] == true, trackInventory: p['track_inventory'] == true,
); );