diff --git a/lib/Screens/Sales/add_sales.dart b/lib/Screens/Sales/add_sales.dart index 930fbab..b94b696 100644 --- a/lib/Screens/Sales/add_sales.dart +++ b/lib/Screens/Sales/add_sales.dart @@ -121,7 +121,10 @@ class AddSalesScreenState extends ConsumerState { productPurchasePrice: detail.product?.productPurchasePrice, stock: detail.stock?.productCurrentStock, productId: detail.productId!, - stockId: detail.stock?.id ?? 0); + stockId: detail.stock?.id ?? 0, + perItemTaxPercentage: 0, // Default to 0 as historical tax rate is not available + productVatId: null, // Default to null + ); cart.addToCartRiverPod( cartItem: cartItem, fromEditSales: true, @@ -503,7 +506,7 @@ class AddSalesScreenState extends ConsumerState { const SizedBox(width: 10), const Spacer(), - taxesData.when( + /*taxesData.when( data: (data) { List dataList = data.where((tax) => tax.status == true).toList(); if (widget.transitionModel != null && @@ -579,7 +582,7 @@ class AddSalesScreenState extends ConsumerState { }, ), - const SizedBox(width: 10), + const SizedBox(width: 10),*/ // VAT Amount Input Field SizedBox( diff --git a/lib/Screens/Sales/provider/sales_cart_provider.dart b/lib/Screens/Sales/provider/sales_cart_provider.dart index c2f6d7b..e3a1f1e 100644 --- a/lib/Screens/Sales/provider/sales_cart_provider.dart +++ b/lib/Screens/Sales/provider/sales_cart_provider.dart @@ -132,11 +132,26 @@ class CartNotifier extends ChangeNotifier { totalPayableAmount -= discountAmount; } - // Apply VAT - if (selectedVat?.rate != null) { - vatAmount = (totalPayableAmount * selectedVat!.rate!) / 100; - vatAmountController.text = vatAmount.toStringAsFixed(2); + // Apply Item-wise VAT + vatAmount = 0; + for (var element in cartItemList) { + num unitPrice = element.unitPrice ?? 0; + num quantity = element.quantity; + num taxRate = element.perItemTaxPercentage ?? 0; + + // VAT Formula: (Price * Qty * Rate%) + // Note: We are calculating VAT on the base price, not discounted price if that's the requirement. + // But usually VAT is on the final price. + // The user request "QTY x Harga x % dari vat_id" implies Base Price. + // If it should be on discounted price, we would subtract productDiscount. + // Assuming "Harga" refers to the selling price. + + num itemVat = (unitPrice * quantity * taxRate) / 100; + vatAmount += itemVat; } + vatAmountController.text = vatAmount.toStringAsFixed(2); + + // Add Total VAT to Payable totalPayableAmount += vatAmount; // Apply Shipping diff --git a/lib/Screens/Sales/sales_cart_widget.dart b/lib/Screens/Sales/sales_cart_widget.dart index 3b2a1c1..7e65f80 100644 --- a/lib/Screens/Sales/sales_cart_widget.dart +++ b/lib/Screens/Sales/sales_cart_widget.dart @@ -4,6 +4,7 @@ import 'package:hugeicons/hugeicons.dart'; import 'package:mobile_pos/Screens/Sales/provider/sales_cart_provider.dart'; import 'package:mobile_pos/Screens/Sales/sales_add_to_cart_sales_widget.dart'; import 'package:mobile_pos/constant.dart'; +import 'package:mobile_pos/currency.dart'; import 'package:mobile_pos/generated/l10n.dart' as lang; class SalesCartListWidget extends ConsumerWidget { @@ -50,150 +51,155 @@ class SalesCartListWidget extends ConsumerWidget { final double discountPerUnit = (item.discountAmount ?? 0).toDouble(); final double totalDiscount = quantity * discountPerUnit; final double subTotal = quantity * unitPrice; - final double finalTotal = subTotal - totalDiscount; + final double originalTotal = unitPrice * quantity; + final double itemVAT = (unitPrice * quantity * (item.perItemTaxPercentage ?? 0)) / 100; + final double finalTotal = originalTotal + itemVAT; - return ListTile( - visualDensity: VisualDensity(horizontal: -4, vertical: -4), - contentPadding: EdgeInsetsDirectional.symmetric(horizontal: 10), - onTap: () => showModalBottomSheet( - isScrollControlled: true, - context: context, - builder: (context2) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 10.0), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + // 1. Product Name (Clickable to Edit) + GestureDetector( + onTap: () => showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (context2) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + s.updateProduct, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + CloseButton( + onPressed: () => Navigator.pop(context2), + ) + ], + ), + ), + const Divider(thickness: 1, color: kBorderColorTextField), + Padding( + padding: const EdgeInsets.all(16.0), + child: SalesAddToCartForm( + batchWiseStockModel: item, + previousContext: context2, + ), + ), + ], + ); + }, + ), + child: SizedBox( + width: 140, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - s.updateProduct, - style: const TextStyle(fontWeight: FontWeight.bold), + item.productName.toString(), + style: _theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - CloseButton( - onPressed: () => Navigator.pop(context2), - ) + if (item.productType == 'variant') + Text( + '[${item.batchName}]', + style: const TextStyle(fontSize: 10, fontStyle: FontStyle.italic, color: Colors.grey), + ), ], ), ), - const Divider(thickness: 1, color: kBorderColorTextField), - Padding( - padding: const EdgeInsets.all(16.0), - child: SalesAddToCartForm( - batchWiseStockModel: item, - previousContext: context2, + ), + const SizedBox(width: 8), + + // 2. Decrease Icon + GestureDetector( + onTap: () => providerData.quantityDecrease(index), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: kMainColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), ), + child: const Icon(Icons.remove, size: 16, color: kMainColor), ), - ], - ); - }, - ), - title: Text( - item.productName.toString(), - style: _theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w500, - ), - ), - subtitle: RichText( - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: [ - // Qty X Price - TextSpan( - text: '${formatPointNumber(quantity)} X $unitPrice ', - style: _theme.textTheme.titleSmall?.copyWith( + ), + const SizedBox(width: 8), + + // 3. Qty Order + Text( + formatPointNumber(quantity), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(width: 8), + + // 4. Increase Icon + GestureDetector( + onTap: () => providerData.quantityIncrease(index), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: kMainColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: const Icon(Icons.add, size: 16, color: kMainColor), + ), + ), + const SizedBox(width: 12), + + // 5. Value Original + Text( + '$currency${formatPointNumber(originalTotal)}', + style: TextStyle( color: kPeraColor, + fontSize: 12, ), ), - // Show Discount if exists - if (totalDiscount > 0) - TextSpan( - text: '- ${formatPointNumber(totalDiscount)} (Disc) ', - style: _theme.textTheme.titleSmall?.copyWith( + const SizedBox(width: 8), + + // 6. Value VAT/Tax + if (itemVAT > 0) ...[ + Text( + 'VAT: $currency${formatPointNumber(itemVAT)}', + style: TextStyle( color: kPeraColor, + fontSize: 10, ), ), - // Final Total - TextSpan( - text: '= ${formatPointNumber(finalTotal)} ', + const SizedBox(width: 8), + ], + + // 7. Sum (Value Original + VAT/Tax) + Text( + '$currency${formatPointNumber(finalTotal)}', style: _theme.textTheme.titleSmall?.copyWith( - color: kTitleColor, + color: kMainColor, + fontWeight: FontWeight.bold, ), ), - // Batch Info - if (item.productType == 'variant') - TextSpan( - text: '[${item.batchName}]', - style: const TextStyle(fontSize: 12, fontStyle: FontStyle.italic), + const SizedBox(width: 12), + + // 8. Delete Icon + GestureDetector( + onTap: () => providerData.deleteToCart(index), + child: HugeIcon( + icon: HugeIcons.strokeRoundedDelete03, + size: 20, + color: Colors.red, ), + ), ], ), ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 90, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: () => providerData.quantityDecrease(index), - child: Container( - height: 18, - width: 18, - decoration: const BoxDecoration( - color: kMainColor, - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - child: const Center( - child: Icon(Icons.remove, size: 14, color: Colors.white), - ), - ), - ), - const SizedBox(width: 5), - SizedBox( - width: 40, - child: Center( - child: Text( - formatPointNumber(item.quantity), - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: kGreyTextColor, - ), - maxLines: 1, - ), - ), - ), - const SizedBox(width: 8), - GestureDetector( - onTap: () => providerData.quantityIncrease(index), - child: Container( - height: 18, - width: 18, - decoration: const BoxDecoration( - color: kMainColor, - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - child: const Center( - child: Icon(Icons.add, size: 14, color: Colors.white), - ), - ), - ), - ], - ), - ), - const SizedBox(width: 12), - GestureDetector( - onTap: () => providerData.deleteToCart(index), - child: HugeIcon( - icon: HugeIcons.strokeRoundedDelete03, - size: 20, - color: Colors.red, - ), - ), - ], - ), ); }, ), diff --git a/lib/Screens/Sales/sales_products_list_screen.dart b/lib/Screens/Sales/sales_products_list_screen.dart index 13666cb..aa6365c 100644 --- a/lib/Screens/Sales/sales_products_list_screen.dart +++ b/lib/Screens/Sales/sales_products_list_screen.dart @@ -202,6 +202,8 @@ class _SaleProductsListState extends State { ? 1 : (stock?.productStock ?? 10)) // Ensure combo adds at least 1 : 1, + perItemTaxPercentage: product.vat?.rate ?? 0, + productVatId: product.vatId, ); providerData.addToCartRiverPod(cartItem: cartItem, fromEditSales: false); Navigator.pop(context); diff --git a/lib/Screens/pos_sale/pos_sale.dart b/lib/Screens/pos_sale/pos_sale.dart index b934fb2..ac62f2a 100644 --- a/lib/Screens/pos_sale/pos_sale.dart +++ b/lib/Screens/pos_sale/pos_sale.dart @@ -588,6 +588,8 @@ class _PosSaleScreenState extends ConsumerState { productType: variantProduct!.productType, productId: variantProduct!.id ?? 0, quantity: 1, + perItemTaxPercentage: variantProduct!.vat?.rate ?? 0, + productVatId: variantProduct!.vatId, ); providerData.addToCartRiverPod(cartItem: cartItem); @@ -738,6 +740,8 @@ class _PosSaleScreenState extends ConsumerState { productType: product.productType, productId: product.id ?? 0, quantity: 1, + perItemTaxPercentage: product.vat?.rate ?? 0, + productVatId: product.vatId, ); providerData.addToCartRiverPod(cartItem: cartItem); @@ -924,91 +928,117 @@ class _PosSaleScreenState extends ConsumerState { Divider(color: kBorderColorTextField), itemBuilder: (context, index) { final item = providerData.cartItemList[index]; - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item.productName ?? '', - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - Text( - '$currency${item.unitPrice}', - style: TextStyle( - color: kSubPeraColor, - fontSize: 12, - ), - ), - ], - ), - ), - const SizedBox(width: 8), - Row( - children: [ - GestureDetector( - onTap: () { - providerData.quantityDecrease(index); - }, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: kMainColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - ), - child: const Icon(Icons.remove, size: 16, color: kMainColor), + final double quantity = (item.quantity ?? 0).toDouble(); + final double unitPrice = (item.unitPrice ?? 0).toDouble(); + final double itemVAT = (unitPrice * quantity * (item.perItemTaxPercentage ?? 0)) / 100; + final double originalTotal = unitPrice * quantity; + final double finalTotal = originalTotal + itemVAT; + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 1. Product Name + SizedBox( + width: 140, // Fixed width to allow scrolling and wrapping + child: Text( + item.productName ?? '', + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - const SizedBox(width: 8), + ), + const SizedBox(width: 8), + + // 2. Decrease Icon + GestureDetector( + onTap: () { + providerData.quantityDecrease(index); + }, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: kMainColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: const Icon(Icons.remove, size: 16, color: kMainColor), + ), + ), + const SizedBox(width: 8), + + // 3. Qty Order + Text( + '${item.quantity}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(width: 8), + + // 4. Increase Icon + GestureDetector( + onTap: () { + providerData.quantityIncrease(index); + }, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: kMainColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: const Icon(Icons.add, size: 16, color: kMainColor), + ), + ), + const SizedBox(width: 12), + + // 5. Value Original + Text( + '$currency${originalTotal.toStringAsFixed(2)}', + style: TextStyle( + color: kSubPeraColor, + fontSize: 12, + ), + ), + const SizedBox(width: 8), + + // 6. Value VAT/Tax + if (itemVAT > 0) ...[ Text( - '${item.quantity}', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(width: 8), - GestureDetector( - onTap: () { - providerData.quantityIncrease(index); - }, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: kMainColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - ), - child: const Icon(Icons.add, size: 16, color: kMainColor), + 'VAT: $currency${itemVAT.toStringAsFixed(2)}', + style: TextStyle( + color: kSubPeraColor, + fontSize: 10, ), ), + const SizedBox(width: 8), ], - ), - const SizedBox(width: 12), - Text( - '$currency${(item.unitPrice ?? 0) * (item.quantity ?? 0)}', - style: const TextStyle( - fontWeight: FontWeight.bold, - color: kMainColor, - fontSize: 14, + + // 7. Sum (Value Original + VAT/Tax) + Text( + '$currency${finalTotal.toStringAsFixed(2)}', + style: const TextStyle( + fontWeight: FontWeight.bold, + color: kMainColor, + fontSize: 14, + ), ), - ), - const SizedBox(width: 12), - GestureDetector( - onTap: () { - providerData.deleteToCart(index); - }, - child: const Icon( - Icons.delete_outline, - color: Colors.red, - size: 20, + const SizedBox(width: 12), + + // Delete Icon (Keeping it for usability) + GestureDetector( + onTap: () { + providerData.deleteToCart(index); + }, + child: const Icon( + Icons.delete_outline, + color: Colors.red, + size: 20, + ), ), - ), - ], + ], + ), ); }, ), diff --git a/lib/model/add_to_cart_model.dart b/lib/model/add_to_cart_model.dart index c674929..fc033d6 100644 --- a/lib/model/add_to_cart_model.dart +++ b/lib/model/add_to_cart_model.dart @@ -13,6 +13,8 @@ class SaleCartModel { this.productPurchasePrice, this.lossProfit, this.discountAmount, + this.perItemTaxPercentage, + this.productVatId, }); num productId; @@ -28,4 +30,6 @@ class SaleCartModel { num? stock; num? lossProfit; num? discountAmount; + num? perItemTaxPercentage; + int? productVatId; } diff --git a/lib/thermal priting invoices/thermal_invoice_sales.dart b/lib/thermal priting invoices/thermal_invoice_sales.dart index 9aa03e1..eadea46 100644 --- a/lib/thermal priting invoices/thermal_invoice_sales.dart +++ b/lib/thermal priting invoices/thermal_invoice_sales.dart @@ -258,10 +258,11 @@ class SalesThermalPrinterInvoice { } bytes += generator.row([ - PosColumn(text: 'Item', width: 4, styles: const PosStyles(align: PosAlign.left, bold: true)), - PosColumn(text: 'Qty', width: 2, styles: const PosStyles(align: PosAlign.center, bold: true)), - PosColumn(text: 'Price', width: 3, styles: const PosStyles(align: PosAlign.center, bold: true)), - PosColumn(text: 'Amount', width: 3, styles: const PosStyles(align: PosAlign.right, bold: true)), + PosColumn(text: 'SL', width: 1, styles: const PosStyles(align: PosAlign.left, bold: true)), + PosColumn(text: 'Product', width: 4, styles: const PosStyles(align: PosAlign.left, bold: true)), + PosColumn(text: 'Qty', width: 1, styles: const PosStyles(align: PosAlign.center, bold: true)), + PosColumn(text: 'VAT/Tax', width: 2, styles: const PosStyles(align: PosAlign.center, bold: true)), + PosColumn(text: 'Amount', width: 4, styles: const PosStyles(align: PosAlign.right, bold: true)), ]); bytes += generator.hr(); List.generate(productList?.length ?? 1, (index) { @@ -279,6 +280,13 @@ class SalesThermalPrinterInvoice { "${productList?[index].product?.productType == ProductType.variant.name ? ' [${productList?[index].stock?.batchNo ?? ''}]' : ''}"; bytes += generator.row([ + PosColumn( + text: '${index + 1}', + width: 1, + styles: const PosStyles( + align: PosAlign.left, + ), + ), PosColumn( text: name, width: 4, @@ -288,18 +296,18 @@ class SalesThermalPrinterInvoice { ), PosColumn( text: formatPointNumber(getProductQuantity(detailsId: productList?[index].id ?? 0)), - width: 2, + width: 1, styles: const PosStyles(align: PosAlign.center), ), PosColumn( - text: '${productList?[index].price}', - width: 3, + text: formatPointNumber(0), + width: 2, styles: const PosStyles(align: PosAlign.center), ), PosColumn( text: "${((productList?[index].price ?? 0) * getProductQuantity(detailsId: productList?[index].id ?? 0)) - ((productList?[index].discount ?? 0) * getProductQuantity(detailsId: productList?[index].id ?? 0))}", - width: 3, + width: 4, styles: const PosStyles(align: PosAlign.right), ), ]); @@ -811,10 +819,10 @@ class SalesThermalPrinterInvoice { bytes += generator.hr(); bytes += generator.row([ PosColumn(text: 'SL', width: 1, styles: const PosStyles(align: PosAlign.left, bold: true)), - PosColumn(text: 'Item', width: 5, styles: const PosStyles(align: PosAlign.left, bold: true)), - PosColumn(text: 'Qty', width: 2, styles: const PosStyles(align: PosAlign.center, bold: true)), - PosColumn(text: 'Price', width: 2, styles: const PosStyles(align: PosAlign.center, bold: true)), - PosColumn(text: 'Amount', width: 2, styles: const PosStyles(align: PosAlign.right, bold: true)), + PosColumn(text: 'Product', width: 5, styles: const PosStyles(align: PosAlign.left, bold: true)), + PosColumn(text: 'QTY', width: 1, styles: const PosStyles(align: PosAlign.center, bold: true)), + PosColumn(text: 'VAT/Tax', width: 2, styles: const PosStyles(align: PosAlign.center, bold: true)), + PosColumn(text: 'Amount', width: 3, styles: const PosStyles(align: PosAlign.right, bold: true)), ]); bytes += generator.hr(); List.generate(productList?.length ?? 1, (index) { @@ -846,10 +854,10 @@ class SalesThermalPrinterInvoice { )), PosColumn( text: formatPointNumber(getProductQuantity(detailsId: productList?[index].id ?? 0), addComma: true), - width: 2, + width: 1, styles: const PosStyles(align: PosAlign.center)), PosColumn( - text: formatPointNumber(productList?[index].price ?? 0, addComma: true), + text: formatPointNumber(0, addComma: true), width: 2, styles: const PosStyles( align: PosAlign.center, @@ -858,7 +866,7 @@ class SalesThermalPrinterInvoice { text: formatPointNumber( (productList?[index].price ?? 0) * getProductQuantity(detailsId: productList?[index].id ?? 0), addComma: true), - width: 2, + width: 3, styles: const PosStyles(align: PosAlign.right)), ]); if (warranty.isNotEmpty) {