uabh perhitungan tax

This commit is contained in:
2026-02-08 14:58:52 +07:00
parent 0bebe34f19
commit d144d24596
7 changed files with 289 additions and 221 deletions

View File

@@ -121,7 +121,10 @@ class AddSalesScreenState extends ConsumerState<AddSalesScreen> {
productPurchasePrice: detail.product?.productPurchasePrice, productPurchasePrice: detail.product?.productPurchasePrice,
stock: detail.stock?.productCurrentStock, stock: detail.stock?.productCurrentStock,
productId: detail.productId!, 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( cart.addToCartRiverPod(
cartItem: cartItem, cartItem: cartItem,
fromEditSales: true, fromEditSales: true,
@@ -503,7 +506,7 @@ class AddSalesScreenState extends ConsumerState<AddSalesScreen> {
const SizedBox(width: 10), const SizedBox(width: 10),
const Spacer(), const Spacer(),
taxesData.when( /*taxesData.when(
data: (data) { data: (data) {
List<VatModel> dataList = data.where((tax) => tax.status == true).toList(); List<VatModel> dataList = data.where((tax) => tax.status == true).toList();
if (widget.transitionModel != null && if (widget.transitionModel != null &&
@@ -579,7 +582,7 @@ class AddSalesScreenState extends ConsumerState<AddSalesScreen> {
}, },
), ),
const SizedBox(width: 10), const SizedBox(width: 10),*/
// VAT Amount Input Field // VAT Amount Input Field
SizedBox( SizedBox(

View File

@@ -132,11 +132,26 @@ class CartNotifier extends ChangeNotifier {
totalPayableAmount -= discountAmount; totalPayableAmount -= discountAmount;
} }
// Apply VAT // Apply Item-wise VAT
if (selectedVat?.rate != null) { vatAmount = 0;
vatAmount = (totalPayableAmount * selectedVat!.rate!) / 100; for (var element in cartItemList) {
vatAmountController.text = vatAmount.toStringAsFixed(2); 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; totalPayableAmount += vatAmount;
// Apply Shipping // Apply Shipping

View File

@@ -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/provider/sales_cart_provider.dart';
import 'package:mobile_pos/Screens/Sales/sales_add_to_cart_sales_widget.dart'; import 'package:mobile_pos/Screens/Sales/sales_add_to_cart_sales_widget.dart';
import 'package:mobile_pos/constant.dart'; import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/currency.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang; import 'package:mobile_pos/generated/l10n.dart' as lang;
class SalesCartListWidget extends ConsumerWidget { class SalesCartListWidget extends ConsumerWidget {
@@ -50,150 +51,155 @@ class SalesCartListWidget extends ConsumerWidget {
final double discountPerUnit = (item.discountAmount ?? 0).toDouble(); final double discountPerUnit = (item.discountAmount ?? 0).toDouble();
final double totalDiscount = quantity * discountPerUnit; final double totalDiscount = quantity * discountPerUnit;
final double subTotal = quantity * unitPrice; 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( return Padding(
visualDensity: VisualDensity(horizontal: -4, vertical: -4), padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 10.0),
contentPadding: EdgeInsetsDirectional.symmetric(horizontal: 10), child: SingleChildScrollView(
onTap: () => showModalBottomSheet( scrollDirection: Axis.horizontal,
isScrollControlled: true, child: Row(
context: context, children: [
builder: (context2) { // 1. Product Name (Clickable to Edit)
return Column( GestureDetector(
mainAxisSize: MainAxisSize.min, onTap: () => showModalBottomSheet(
children: [ isScrollControlled: true,
Padding( context: context,
padding: const EdgeInsets.symmetric(horizontal: 10.0), builder: (context2) {
child: Row( return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, 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: [ children: [
Text( Text(
s.updateProduct, item.productName.toString(),
style: const TextStyle(fontWeight: FontWeight.bold), style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
CloseButton( if (item.productType == 'variant')
onPressed: () => Navigator.pop(context2), Text(
) '[${item.batchName}]',
style: const TextStyle(fontSize: 10, fontStyle: FontStyle.italic, color: Colors.grey),
),
], ],
), ),
), ),
const Divider(thickness: 1, color: kBorderColorTextField), ),
Padding( const SizedBox(width: 8),
padding: const EdgeInsets.all(16.0),
child: SalesAddToCartForm( // 2. Decrease Icon
batchWiseStockModel: item, GestureDetector(
previousContext: context2, 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
title: Text( Text(
item.productName.toString(), formatPointNumber(quantity),
style: _theme.textTheme.titleMedium?.copyWith( style: const TextStyle(fontWeight: FontWeight.bold),
fontWeight: FontWeight.w500, ),
), const SizedBox(width: 8),
),
subtitle: RichText( // 4. Increase Icon
text: TextSpan( GestureDetector(
style: DefaultTextStyle.of(context).style, onTap: () => providerData.quantityIncrease(index),
children: [ child: Container(
// Qty X Price padding: const EdgeInsets.all(4),
TextSpan( decoration: BoxDecoration(
text: '${formatPointNumber(quantity)} X $unitPrice ', color: kMainColor.withOpacity(0.1),
style: _theme.textTheme.titleSmall?.copyWith( 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, color: kPeraColor,
fontSize: 12,
), ),
), ),
// Show Discount if exists const SizedBox(width: 8),
if (totalDiscount > 0)
TextSpan( // 6. Value VAT/Tax
text: '- ${formatPointNumber(totalDiscount)} (Disc) ', if (itemVAT > 0) ...[
style: _theme.textTheme.titleSmall?.copyWith( Text(
'VAT: $currency${formatPointNumber(itemVAT)}',
style: TextStyle(
color: kPeraColor, color: kPeraColor,
fontSize: 10,
), ),
), ),
// Final Total const SizedBox(width: 8),
TextSpan( ],
text: '= ${formatPointNumber(finalTotal)} ',
// 7. Sum (Value Original + VAT/Tax)
Text(
'$currency${formatPointNumber(finalTotal)}',
style: _theme.textTheme.titleSmall?.copyWith( style: _theme.textTheme.titleSmall?.copyWith(
color: kTitleColor, color: kMainColor,
fontWeight: FontWeight.bold,
), ),
), ),
// Batch Info const SizedBox(width: 12),
if (item.productType == 'variant')
TextSpan( // 8. Delete Icon
text: '[${item.batchName}]', GestureDetector(
style: const TextStyle(fontSize: 12, fontStyle: FontStyle.italic), 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,
),
),
],
),
); );
}, },
), ),

View File

@@ -202,6 +202,8 @@ class _SaleProductsListState extends State<SaleProductsList> {
? 1 ? 1
: (stock?.productStock ?? 10)) // Ensure combo adds at least 1 : (stock?.productStock ?? 10)) // Ensure combo adds at least 1
: 1, : 1,
perItemTaxPercentage: product.vat?.rate ?? 0,
productVatId: product.vatId,
); );
providerData.addToCartRiverPod(cartItem: cartItem, fromEditSales: false); providerData.addToCartRiverPod(cartItem: cartItem, fromEditSales: false);
Navigator.pop(context); Navigator.pop(context);

View File

@@ -588,6 +588,8 @@ class _PosSaleScreenState extends ConsumerState<PosSaleScreen> {
productType: variantProduct!.productType, productType: variantProduct!.productType,
productId: variantProduct!.id ?? 0, productId: variantProduct!.id ?? 0,
quantity: 1, quantity: 1,
perItemTaxPercentage: variantProduct!.vat?.rate ?? 0,
productVatId: variantProduct!.vatId,
); );
providerData.addToCartRiverPod(cartItem: cartItem); providerData.addToCartRiverPod(cartItem: cartItem);
@@ -738,6 +740,8 @@ class _PosSaleScreenState extends ConsumerState<PosSaleScreen> {
productType: product.productType, productType: product.productType,
productId: product.id ?? 0, productId: product.id ?? 0,
quantity: 1, quantity: 1,
perItemTaxPercentage: product.vat?.rate ?? 0,
productVatId: product.vatId,
); );
providerData.addToCartRiverPod(cartItem: cartItem); providerData.addToCartRiverPod(cartItem: cartItem);
@@ -924,91 +928,117 @@ class _PosSaleScreenState extends ConsumerState<PosSaleScreen> {
Divider(color: kBorderColorTextField), Divider(color: kBorderColorTextField),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = providerData.cartItemList[index]; final item = providerData.cartItemList[index];
return Row( final double quantity = (item.quantity ?? 0).toDouble();
crossAxisAlignment: CrossAxisAlignment.center, final double unitPrice = (item.unitPrice ?? 0).toDouble();
children: [ final double itemVAT = (unitPrice * quantity * (item.perItemTaxPercentage ?? 0)) / 100;
Expanded( final double originalTotal = unitPrice * quantity;
child: Column( final double finalTotal = originalTotal + itemVAT;
crossAxisAlignment: CrossAxisAlignment.start,
children: [ return SingleChildScrollView(
Text( scrollDirection: Axis.horizontal,
item.productName ?? '', child: Row(
style: const TextStyle( crossAxisAlignment: CrossAxisAlignment.center,
fontWeight: FontWeight.w600, children: [
fontSize: 14, // 1. Product Name
), SizedBox(
maxLines: 2, width: 140, // Fixed width to allow scrolling and wrapping
overflow: TextOverflow.ellipsis, child: Text(
), item.productName ?? '',
const SizedBox(height: 4), style: const TextStyle(
Text( fontWeight: FontWeight.w600,
'$currency${item.unitPrice}', fontSize: 14,
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),
), ),
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( Text(
'${item.quantity}', 'VAT: $currency${itemVAT.toStringAsFixed(2)}',
style: const TextStyle(fontWeight: FontWeight.bold), style: TextStyle(
), color: kSubPeraColor,
const SizedBox(width: 8), fontSize: 10,
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: 8),
], ],
),
const SizedBox(width: 12), // 7. Sum (Value Original + VAT/Tax)
Text( Text(
'$currency${(item.unitPrice ?? 0) * (item.quantity ?? 0)}', '$currency${finalTotal.toStringAsFixed(2)}',
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: kMainColor, color: kMainColor,
fontSize: 14, fontSize: 14,
),
), ),
), const SizedBox(width: 12),
const SizedBox(width: 12),
GestureDetector( // Delete Icon (Keeping it for usability)
onTap: () { GestureDetector(
providerData.deleteToCart(index); onTap: () {
}, providerData.deleteToCart(index);
child: const Icon( },
Icons.delete_outline, child: const Icon(
color: Colors.red, Icons.delete_outline,
size: 20, color: Colors.red,
size: 20,
),
), ),
), ],
], ),
); );
}, },
), ),

View File

@@ -13,6 +13,8 @@ class SaleCartModel {
this.productPurchasePrice, this.productPurchasePrice,
this.lossProfit, this.lossProfit,
this.discountAmount, this.discountAmount,
this.perItemTaxPercentage,
this.productVatId,
}); });
num productId; num productId;
@@ -28,4 +30,6 @@ class SaleCartModel {
num? stock; num? stock;
num? lossProfit; num? lossProfit;
num? discountAmount; num? discountAmount;
num? perItemTaxPercentage;
int? productVatId;
} }

View File

@@ -258,10 +258,11 @@ class SalesThermalPrinterInvoice {
} }
bytes += generator.row([ bytes += generator.row([
PosColumn(text: 'Item', width: 4, styles: const PosStyles(align: PosAlign.left, bold: true)), PosColumn(text: 'SL', width: 1, styles: const PosStyles(align: PosAlign.left, bold: true)),
PosColumn(text: 'Qty', width: 2, styles: const PosStyles(align: PosAlign.center, bold: true)), PosColumn(text: 'Product', width: 4, styles: const PosStyles(align: PosAlign.left, bold: true)),
PosColumn(text: 'Price', width: 3, styles: const PosStyles(align: PosAlign.center, bold: true)), PosColumn(text: 'Qty', width: 1, styles: const PosStyles(align: PosAlign.center, bold: true)),
PosColumn(text: 'Amount', width: 3, styles: const PosStyles(align: PosAlign.right, 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(); bytes += generator.hr();
List.generate(productList?.length ?? 1, (index) { List.generate(productList?.length ?? 1, (index) {
@@ -279,6 +280,13 @@ class SalesThermalPrinterInvoice {
"${productList?[index].product?.productType == ProductType.variant.name ? ' [${productList?[index].stock?.batchNo ?? ''}]' : ''}"; "${productList?[index].product?.productType == ProductType.variant.name ? ' [${productList?[index].stock?.batchNo ?? ''}]' : ''}";
bytes += generator.row([ bytes += generator.row([
PosColumn(
text: '${index + 1}',
width: 1,
styles: const PosStyles(
align: PosAlign.left,
),
),
PosColumn( PosColumn(
text: name, text: name,
width: 4, width: 4,
@@ -288,18 +296,18 @@ class SalesThermalPrinterInvoice {
), ),
PosColumn( PosColumn(
text: formatPointNumber(getProductQuantity(detailsId: productList?[index].id ?? 0)), text: formatPointNumber(getProductQuantity(detailsId: productList?[index].id ?? 0)),
width: 2, width: 1,
styles: const PosStyles(align: PosAlign.center), styles: const PosStyles(align: PosAlign.center),
), ),
PosColumn( PosColumn(
text: '${productList?[index].price}', text: formatPointNumber(0),
width: 3, width: 2,
styles: const PosStyles(align: PosAlign.center), styles: const PosStyles(align: PosAlign.center),
), ),
PosColumn( PosColumn(
text: text:
"${((productList?[index].price ?? 0) * getProductQuantity(detailsId: productList?[index].id ?? 0)) - ((productList?[index].discount ?? 0) * getProductQuantity(detailsId: productList?[index].id ?? 0))}", "${((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), styles: const PosStyles(align: PosAlign.right),
), ),
]); ]);
@@ -811,10 +819,10 @@ class SalesThermalPrinterInvoice {
bytes += generator.hr(); bytes += generator.hr();
bytes += generator.row([ bytes += generator.row([
PosColumn(text: 'SL', width: 1, styles: const PosStyles(align: PosAlign.left, bold: true)), 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: 'Product', 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: 'QTY', width: 1, styles: const PosStyles(align: PosAlign.center, bold: true)),
PosColumn(text: 'Price', width: 2, 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: 2, styles: const PosStyles(align: PosAlign.right, bold: true)), PosColumn(text: 'Amount', width: 3, styles: const PosStyles(align: PosAlign.right, bold: true)),
]); ]);
bytes += generator.hr(); bytes += generator.hr();
List.generate(productList?.length ?? 1, (index) { List.generate(productList?.length ?? 1, (index) {
@@ -846,10 +854,10 @@ class SalesThermalPrinterInvoice {
)), )),
PosColumn( PosColumn(
text: formatPointNumber(getProductQuantity(detailsId: productList?[index].id ?? 0), addComma: true), text: formatPointNumber(getProductQuantity(detailsId: productList?[index].id ?? 0), addComma: true),
width: 2, width: 1,
styles: const PosStyles(align: PosAlign.center)), styles: const PosStyles(align: PosAlign.center)),
PosColumn( PosColumn(
text: formatPointNumber(productList?[index].price ?? 0, addComma: true), text: formatPointNumber(0, addComma: true),
width: 2, width: 2,
styles: const PosStyles( styles: const PosStyles(
align: PosAlign.center, align: PosAlign.center,
@@ -858,7 +866,7 @@ class SalesThermalPrinterInvoice {
text: formatPointNumber( text: formatPointNumber(
(productList?[index].price ?? 0) * getProductQuantity(detailsId: productList?[index].id ?? 0), (productList?[index].price ?? 0) * getProductQuantity(detailsId: productList?[index].id ?? 0),
addComma: true), addComma: true),
width: 2, width: 3,
styles: const PosStyles(align: PosAlign.right)), styles: const PosStyles(align: PosAlign.right)),
]); ]);
if (warranty.isNotEmpty) { if (warranty.isNotEmpty) {