Fixed sales invoice issues in Ecommerce

This commit is contained in:
2026-09-03 08:34:23 +05:30
parent b4772ccf19
commit b123a34e8f
18 changed files with 2275 additions and 453 deletions

View File

@@ -12,6 +12,8 @@ import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
import '../../inventory/providers/products_provider.dart';
import '../../inventory/providers/product_categories_provider.dart';
import '../../inventory/providers/uoms_provider.dart';
import '../../inventory/domain/product.dart';
import '../domain/invoice.dart';
import '../providers/invoices_provider.dart';
import '../providers/customers_provider.dart';
@@ -21,6 +23,46 @@ import '../../../core/network/dio_client.dart';
import 'widgets/receive_payment_sheet.dart';
import 'invoice_builder_screen.dart';
bool _isCommodityProduct({
ProductCategory? category,
Product? product,
InvoiceItem? invoiceItem,
String? commodityCode,
String? huid,
}) {
final h = huid ?? invoiceItem?.huid;
if (h != null && h.trim().isNotEmpty) return true;
final code = commodityCode ??
category?.commodityCode ??
invoiceItem?.commodityCode;
if (code != null && code.trim().isNotEmpty && code.trim().toUpperCase() != 'NONE') {
return true;
}
return false;
}
String _resolveProductUnit({
required Product? product,
required ProductCategory? category,
required List<UnitOfMeasure> uoms,
bool isCommodity = false,
}) {
if (isCommodity) {
return (category != null && category.baseUnit.isNotEmpty) ? category.baseUnit : 'g';
}
if (product?.uomId != null) {
final uom = uoms.where((u) => u.id == product!.uomId).firstOrNull;
if (uom != null && uom.abbreviation != null && uom.abbreviation!.isNotEmpty) return uom.abbreviation!;
if (uom != null && uom.name.isNotEmpty) return uom.name;
}
if (category != null && category.baseUnit.isNotEmpty) {
return category.baseUnit;
}
return 'pcs';
}
String _resolveImageUrl(String path) {
if (path.startsWith('http://') || path.startsWith('https://')) {
return path;
@@ -185,10 +227,45 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
const Text('INVOICE NO', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.grey)),
const SizedBox(height: 2),
Text(latestInvoice.invoiceNumber, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: Colors.blue)),
if (latestInvoice.salesChannel != null && latestInvoice.salesChannel!.isNotEmpty) ...[
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.blue.withValues(alpha: 0.25)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(LucideIcons.shoppingBag, size: 10, color: Colors.blue),
const SizedBox(width: 4),
Text(
latestInvoice.salesChannel!,
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.blue),
),
],
),
),
],
if (latestInvoice.marketplaceOrderId != null && latestInvoice.marketplaceOrderId!.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
'Ref: ${latestInvoice.marketplaceOrderId}',
style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: Colors.grey.shade700),
),
],
const SizedBox(height: 10),
const Text('INVOICE DATE', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.grey)),
const SizedBox(height: 2),
Text(formatDate.format(latestInvoice.issueDate), style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
if (latestInvoice.placeOfSupply != null && latestInvoice.placeOfSupply!.isNotEmpty) ...[
const SizedBox(height: 8),
const Text('PLACE OF SUPPLY', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.grey)),
const SizedBox(height: 2),
Text(latestInvoice.placeOfSupply!, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600)),
],
],
),
),
@@ -233,19 +310,33 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
// Item Cards
...(latestInvoice.items).map((item) {
final product = products.where((p) => p.id == item.productId).firstOrNull;
final category = categories.where((c) => c.id == product?.categoryId).firstOrNull;
final weight = item.weight ?? item.quantity;
final metalAmount = weight * item.unitPrice;
final category = categories.where((c) => c.name == item.categoryName || c.id == product?.categoryId).firstOrNull;
final uoms = ref.watch(uomsProvider).value ?? [];
final isItemCommodity = _isCommodityProduct(
category: category,
product: product,
invoiceItem: item,
);
final qtyOrWeight = item.weight != null && item.weight! > 0 ? item.weight! : (item.quantity > 0 ? item.quantity : 1.0);
final baseAmount = qtyOrWeight * item.unitPrice;
final unit = _resolveProductUnit(product: product, category: category, uoms: uoms, isCommodity: isItemCommodity);
double makingChargeAmt = 0.0;
if (item.makingChargesType == 'PERCENTAGE') {
makingChargeAmt = metalAmount * (item.makingCharge / 100.0);
} else if (item.makingChargesType == 'PER_PIECE') {
makingChargeAmt = item.makingCharge;
} else {
makingChargeAmt = weight * item.makingCharge;
if (isItemCommodity) {
if (item.makingChargesType == 'PERCENTAGE') {
makingChargeAmt = baseAmount * (item.makingCharge / 100.0);
} else if (item.makingChargesType == 'PER_PIECE') {
makingChargeAmt = item.makingCharge;
} else {
makingChargeAmt = qtyOrWeight * item.makingCharge;
}
}
final taxableAmount = baseAmount + makingChargeAmt + item.otherCharges - item.discount;
final taxAmount = (taxableAmount * item.taxRate) / 100.0;
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
@@ -278,7 +369,7 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.productName ?? product?.name ?? item.description ?? 'Jewellery Item',
item.productName ?? product?.name ?? item.description ?? 'Item',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
),
const SizedBox(height: 4),
@@ -290,10 +381,12 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
_buildDetailBadge(item.categoryName ?? category!.name, const Color(0xFFD4AF37)),
if (item.sku != null || product?.sku != null)
_buildDetailBadge('SKU: ${item.sku ?? product!.sku}', Colors.blue),
if (item.huid != null)
if (isItemCommodity && item.huid != null && item.huid!.isNotEmpty)
_buildDetailBadge('HUID: ${item.huid}', Colors.purple),
if (item.hsnCode != null)
if (item.hsnCode != null && item.hsnCode!.isNotEmpty)
_buildDetailBadge('HSN: ${item.hsnCode}', Colors.grey),
if (!isItemCommodity)
_buildDetailBadge('Unit: $unit', Colors.teal),
],
),
],
@@ -306,27 +399,52 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
],
),
const Divider(height: 18),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Metal (${weight.toStringAsFixed(3)}g @ ₹${item.unitPrice.toStringAsFixed(2)}/g):',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
if (isItemCommodity) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Metal (${qtyOrWeight.toStringAsFixed(3)}$unit @ ₹${item.unitPrice.toStringAsFixed(2)}/$unit):',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
),
Text(formatCurrency.format(baseAmount), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12)),
],
),
if (makingChargeAmt > 0) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Making Charges (${item.makingChargesType == 'PERCENTAGE' ? '${item.makingCharge}%' : (item.makingChargesType == 'PER_PIECE' ? '${item.makingCharge}/pc' : '${item.makingCharge}/g')}):',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
),
Text(formatCurrency.format(makingChargeAmt), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12, color: Colors.indigo)),
],
),
Text(formatCurrency.format(metalAmount), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12)),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Making Charges (${item.makingChargesType == 'PERCENTAGE' ? '${item.makingCharge}%' : (item.makingChargesType == 'PER_PIECE' ? '${item.makingCharge}/pc' : '${item.makingCharge}/g')}):',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
),
Text(formatCurrency.format(makingChargeAmt), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12, color: Colors.indigo)),
],
),
] else ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Rate: ₹${item.unitPrice.toStringAsFixed(2)} / $unit • Qty: ${qtyOrWeight.toStringAsFixed(qtyOrWeight.truncateToDouble() == qtyOrWeight ? 0 : 2)} $unit',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600, fontWeight: FontWeight.w500),
),
Text(formatCurrency.format(baseAmount), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12)),
],
),
],
if (item.discount > 0) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Discount:', style: TextStyle(fontSize: 11, color: Colors.red)),
Text('- ${formatCurrency.format(item.discount)}', style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.red)),
],
),
],
if (item.taxRate > 0) ...[
const SizedBox(height: 4),
Row(
@@ -334,7 +452,7 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
children: [
Text('GST (${item.taxRate.toStringAsFixed(1)}%):', style: TextStyle(fontSize: 11, color: Colors.grey.shade500)),
Text(
'+ ${formatCurrency.format(((metalAmount + makingChargeAmt) * (item.taxRate / 100.0)))}',
'+ ${formatCurrency.format(taxAmount)}',
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
),
],
@@ -556,6 +674,7 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
final business = ref.read(businessProfileProvider).value;
final products = ref.read(productsProvider).value ?? [];
final categories = ref.read(productCategoriesProvider).value ?? [];
final uoms = ref.read(uomsProvider).value ?? [];
final formatCurrency = NumberFormat.currency(symbol: 'Rs. ', decimalDigits: 2);
final formatDate = DateFormat('dd MMM yyyy');
@@ -776,6 +895,19 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
final groupTotal = group.fold(0.0, (sum, i) => sum + i.total);
final gstRate = group.first.taxRate > 0 ? group.first.taxRate : (product?.gstRate ?? (category?.defaultGst ?? 0.0));
final isGroupCommodity = group.any((item) => _isCommodityProduct(
category: category,
product: product,
invoiceItem: item,
));
final groupUnit = _resolveProductUnit(
product: product,
category: category,
uoms: uoms,
isCommodity: isGroupCommodity,
);
return pw.Container(
margin: const pw.EdgeInsets.only(bottom: 14),
decoration: pw.BoxDecoration(
@@ -839,22 +971,40 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
style: const pw.TextStyle(fontSize: 7.5, color: PdfColors.grey800),
),
),
pw.Container(
padding: const pw.EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: pw.BoxDecoration(
color: PdfColors.amber50,
border: pw.Border.all(color: PdfColors.amber600, width: 0.5),
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(3)),
),
child: pw.Text(
'Purity: $purityStr',
style: pw.TextStyle(
fontSize: 7.5,
fontWeight: pw.FontWeight.bold,
color: PdfColors.brown800,
if (isGroupCommodity)
pw.Container(
padding: const pw.EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: pw.BoxDecoration(
color: PdfColors.amber50,
border: pw.Border.all(color: PdfColors.amber600, width: 0.5),
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(3)),
),
child: pw.Text(
'Purity: $purityStr',
style: pw.TextStyle(
fontSize: 7.5,
fontWeight: pw.FontWeight.bold,
color: PdfColors.brown800,
),
),
),
if (!isGroupCommodity)
pw.Container(
padding: const pw.EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: pw.BoxDecoration(
color: PdfColors.teal50,
border: pw.Border.all(color: PdfColors.teal400, width: 0.5),
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(3)),
),
child: pw.Text(
'Unit: $groupUnit',
style: pw.TextStyle(
fontSize: 7.5,
fontWeight: pw.FontWeight.bold,
color: PdfColors.teal900,
),
),
),
),
if (hsnStr != '-')
pw.Container(
padding: const pw.EdgeInsets.symmetric(horizontal: 6, vertical: 2),
@@ -900,49 +1050,71 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
4: pw.Alignment.centerRight,
5: pw.Alignment.centerRight,
},
headers: [
'Item / HUID',
'Weight / Qty',
'Rate',
'Making Chg',
'Other Chg',
'Total Amt',
],
data: group.map((item) {
String particular = '';
if (item.huid != null && item.huid!.isNotEmpty) {
particular = 'HUID: ${item.huid}';
} else if (item.sku != null && item.sku!.isNotEmpty) {
particular = 'SKU: ${item.sku}';
} else {
particular = 'Standard Item';
}
headers: isGroupCommodity
? [
'Item / HUID',
'Weight / Qty',
'Rate',
'Making Chg',
'Other Chg',
'Total Amt',
]
: [
'Item / SKU',
'Qty',
'Unit Price',
'Discount',
'GST %',
'Total Amt',
],
data: isGroupCommodity
? group.map((item) {
String particular = '';
if (item.huid != null && item.huid!.isNotEmpty) {
particular = 'HUID: ${item.huid}';
} else if (item.sku != null && item.sku!.isNotEmpty) {
particular = 'SKU: ${item.sku}';
} else {
particular = 'Standard Item';
}
final weight = item.weight ?? item.quantity;
final weightQty = item.weight != null && item.weight! > 0
? '${item.weight!.toStringAsFixed(item.weight!.truncateToDouble() == item.weight ? 0 : 3)} g'
: '${item.quantity.toStringAsFixed(item.quantity.truncateToDouble() == item.quantity ? 0 : 0)} pcs';
final weight = item.weight ?? item.quantity;
final weightQty = item.weight != null && item.weight! > 0
? '${item.weight!.toStringAsFixed(item.weight!.truncateToDouble() == item.weight ? 0 : 3)} $groupUnit'
: '${item.quantity.toStringAsFixed(item.quantity.truncateToDouble() == item.quantity ? 0 : 0)} pcs';
final rateUnit = item.weight != null && item.weight! > 0 ? '/ g' : '/ pc';
final rateUnit = item.weight != null && item.weight! > 0 ? '/ $groupUnit' : '/ pc';
double makingAmt = 0.0;
if (item.makingChargesType == 'PERCENTAGE') {
makingAmt = (weight * item.unitPrice) * (item.makingCharge / 100.0);
} else if (item.makingChargesType == 'PER_PIECE') {
makingAmt = item.makingCharge;
} else {
makingAmt = weight * item.makingCharge;
}
double makingAmt = 0.0;
if (item.makingChargesType == 'PERCENTAGE') {
makingAmt = (weight * item.unitPrice) * (item.makingCharge / 100.0);
} else if (item.makingChargesType == 'PER_PIECE') {
makingAmt = item.makingCharge;
} else {
makingAmt = weight * item.makingCharge;
}
return [
particular,
weightQty,
'${formatCurrency.format(item.unitPrice)} $rateUnit',
makingAmt > 0 ? formatCurrency.format(makingAmt) : '-',
item.otherCharges > 0 ? formatCurrency.format(item.otherCharges) : '-',
formatCurrency.format(item.total),
];
}).toList(),
return [
particular,
weightQty,
'${formatCurrency.format(item.unitPrice)} $rateUnit',
makingAmt > 0 ? formatCurrency.format(makingAmt) : '-',
item.otherCharges > 0 ? formatCurrency.format(item.otherCharges) : '-',
formatCurrency.format(item.total),
];
}).toList()
: group.map((item) {
final qty = item.weight != null && item.weight! > 0 ? item.weight! : (item.quantity > 0 ? item.quantity : 1.0);
final qtyStr = '${qty.toStringAsFixed(qty.truncateToDouble() == qty ? 0 : 2)} $groupUnit';
return [
item.sku ?? skuStr ?? 'Item',
qtyStr,
formatCurrency.format(item.unitPrice),
item.discount > 0 ? '-${formatCurrency.format(item.discount)}' : '-',
'${item.taxRate.toStringAsFixed(1)}%',
formatCurrency.format(item.total),
];
}).toList(),
),
// Group Subtotal & GST Calculation Box
@@ -959,7 +1131,9 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(
'Metal Subtotal: ${formatCurrency.format(groupSubtotal)} | Making: ${formatCurrency.format(groupMaking)}',
isGroupCommodity
? 'Metal Subtotal: ${formatCurrency.format(groupSubtotal)} | Making: ${formatCurrency.format(groupMaking)}'
: 'Items Subtotal: ${formatCurrency.format(groupSubtotal)}',
style: const pw.TextStyle(fontSize: 8, color: PdfColors.grey700),
),
pw.Text(