Fixed double entry issue

This commit is contained in:
2026-08-30 21:06:46 +05:30
parent eb39d5ff90
commit a0e7a898fa
7 changed files with 198 additions and 53 deletions

View File

@@ -72,11 +72,7 @@ public class PurchaseOrderController {
Long userId = Long.valueOf(authentication.getDetails().toString()); Long userId = Long.valueOf(authentication.getDetails().toString());
List<PurchaseOrderItem> items = (po != null) ? po.getItems() : null; List<PurchaseOrderItem> items = (po != null) ? po.getItems() : null;
return purchaseOrderService.markAsReceived(userId, id, items) return purchaseOrderService.markAsReceived(userId, id, items)
.map(ResponseEntity::ok) .map(ResponseEntity::ok);
.onErrorResume(e -> {
e.printStackTrace();
return Mono.just(ResponseEntity.internalServerError().build());
});
} }
@PostMapping("/{id}/payments") @PostMapping("/{id}/payments")

View File

@@ -45,4 +45,59 @@ public class LedgerService {
public Mono<Wallet> getCustomerLedger(Long userId, Long customerId, String customerName) { public Mono<Wallet> getCustomerLedger(Long userId, Long customerId, String customerName) {
return getOrCreateSystemLedger(userId, "Customer: " + customerName, "RECEIVABLES", "CUSTOMER_" + customerId); return getOrCreateSystemLedger(userId, "Customer: " + customerName, "RECEIVABLES", "CUSTOMER_" + customerId);
} }
public Mono<Wallet> getOrCreateUserCashWallet(Long userId) {
return walletRepository.findByOwnerIdAndNature(userId, "CASH")
.filter(w -> w.getSubNature() == null)
.next()
.switchIfEmpty(Mono.defer(() ->
walletService.createWallet(
userId, "Cash", "CASH", null, "#10B981", "INR",
BigDecimal.ZERO, null, null, null, null, null, null
)
));
}
public Mono<Wallet> getOrCreateUserBankWallet(Long userId) {
return walletRepository.findByOwnerIdAndNature(userId, "SAVINGS")
.filter(w -> w.getSubNature() == null)
.next()
.switchIfEmpty(Mono.defer(() ->
walletService.createWallet(
userId, "Bank Account", "SAVINGS", null, "#3B82F6", "INR",
BigDecimal.ZERO, null, null, null, null, null, null
)
));
}
public Mono<Wallet> getOrCreateWalletByNature(Long userId, String nature) {
String normalizedNature = nature != null ? nature.toUpperCase() : "CASH";
String defaultName = normalizedNature.substring(0, 1).toUpperCase() + normalizedNature.substring(1).toLowerCase();
if ("SAVINGS".equalsIgnoreCase(normalizedNature)) defaultName = "Bank Account";
if ("CASH".equalsIgnoreCase(normalizedNature)) defaultName = "Cash";
String finalName = defaultName;
return walletRepository.findByOwnerIdAndNature(userId, normalizedNature)
.filter(w -> w.getSubNature() == null)
.next()
.switchIfEmpty(Mono.defer(() ->
walletService.createWallet(
userId, finalName, normalizedNature, null, "#3B82F6", "INR",
BigDecimal.ZERO, null, null, null, null, null, null
)
));
}
public Mono<Wallet> resolvePaymentWallet(Long userId, Long walletId, String paymentMethod) {
if (walletId != null) {
return walletRepository.findById(walletId)
.filter(w -> !"INVENTORY".equals(w.getSubNature()) && !"COGS".equals(w.getSubNature()) && (w.getSubNature() == null || (!w.getSubNature().startsWith("VENDOR_") && !w.getSubNature().startsWith("CUSTOMER_"))))
.switchIfEmpty(Mono.defer(() -> resolvePaymentWallet(userId, null, paymentMethod)));
}
if (paymentMethod != null && ("CASH".equalsIgnoreCase(paymentMethod) || "Cash".equalsIgnoreCase(paymentMethod))) {
return getOrCreateUserCashWallet(userId);
} else {
return getOrCreateUserBankWallet(userId);
}
}
} }

View File

@@ -9,6 +9,7 @@ import com.kifi.api.repository.inventory.InventoryMovementRepository;
import com.kifi.api.repository.inventory.InventoryMovementItemRepository; import com.kifi.api.repository.inventory.InventoryMovementItemRepository;
import com.kifi.api.repository.inventory.InventoryLocationRepository; import com.kifi.api.repository.inventory.InventoryLocationRepository;
import com.kifi.api.repository.inventory.ProductBomRepository; import com.kifi.api.repository.inventory.ProductBomRepository;
import com.kifi.api.repository.inventory.InventoryItemRepository;
import com.kifi.api.repository.business.BusinessFeatureRepository; import com.kifi.api.repository.business.BusinessFeatureRepository;
import com.kifi.api.entity.inventory.InventoryBalance; import com.kifi.api.entity.inventory.InventoryBalance;
import com.kifi.api.entity.inventory.InventoryLocation; import com.kifi.api.entity.inventory.InventoryLocation;
@@ -30,6 +31,7 @@ import java.math.BigDecimal;
public class ProductService { public class ProductService {
private final ProductRepository productRepository; private final ProductRepository productRepository;
private final ProductImageRepository productImageRepository; private final ProductImageRepository productImageRepository;
private final InventoryItemRepository inventoryItemRepository;
private final InventoryBalanceRepository inventoryBalanceRepository; private final InventoryBalanceRepository inventoryBalanceRepository;
private final InventoryMovementRepository inventoryMovementRepository; private final InventoryMovementRepository inventoryMovementRepository;
private final InventoryMovementItemRepository inventoryMovementItemRepository; private final InventoryMovementItemRepository inventoryMovementItemRepository;
@@ -51,14 +53,26 @@ public class ProductService {
return product; return product;
}) })
) )
.flatMap(product -> inventoryBalanceRepository.findByProductId(product.getId()) .flatMap(product -> inventoryItemRepository.findByProductId(product.getId())
.map(InventoryBalance::getQuantity) .filter(item -> "AVAILABLE".equalsIgnoreCase(item.getStatus()) || item.getStatus() == null)
.map(item -> {
if (item.getGrossWeight() != null && item.getGrossWeight().compareTo(BigDecimal.ZERO) > 0) {
return item.getGrossWeight();
}
return BigDecimal.ONE;
})
.reduce(BigDecimal.ZERO, BigDecimal::add) .reduce(BigDecimal.ZERO, BigDecimal::add)
.map(totalStock -> { .map(totalStock -> {
product.setCurrentStock(totalStock); product.setCurrentStock(totalStock);
return product; return product;
}) })
.defaultIfEmpty(product) .defaultIfEmpty(product)
.map(p -> {
if (p.getCurrentStock() == null) {
p.setCurrentStock(BigDecimal.ZERO);
}
return p;
})
); );
} }

View File

@@ -7,6 +7,7 @@ import com.kifi.api.repository.business.BusinessFeatureRepository;
import com.kifi.api.repository.invoice.InvoiceItemRepository; import com.kifi.api.repository.invoice.InvoiceItemRepository;
import com.kifi.api.repository.invoice.InvoicePaymentRepository; import com.kifi.api.repository.invoice.InvoicePaymentRepository;
import com.kifi.api.repository.invoice.InvoiceRepository; import com.kifi.api.repository.invoice.InvoiceRepository;
import com.kifi.api.repository.inventory.InventoryItemRepository;
import com.kifi.api.service.inventory.ProductService; import com.kifi.api.service.inventory.ProductService;
import com.kifi.api.service.inventory.InventoryItemService; import com.kifi.api.service.inventory.InventoryItemService;
import com.kifi.api.service.accounting.LedgerService; import com.kifi.api.service.accounting.LedgerService;
@@ -20,6 +21,7 @@ import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -31,6 +33,7 @@ public class InvoiceService {
private final InvoiceRepository invoiceRepository; private final InvoiceRepository invoiceRepository;
private final InvoiceItemRepository invoiceItemRepository; private final InvoiceItemRepository invoiceItemRepository;
private final InvoicePaymentRepository invoicePaymentRepository; private final InvoicePaymentRepository invoicePaymentRepository;
private final InventoryItemRepository inventoryItemRepository;
private final BusinessFeatureRepository businessFeatureRepository; private final BusinessFeatureRepository businessFeatureRepository;
private final ProductService productService; private final ProductService productService;
private final InventoryItemService inventoryItemService; private final InventoryItemService inventoryItemService;
@@ -112,10 +115,12 @@ public class InvoiceService {
.walletId(invoice.getPaymentWalletId()) .walletId(invoice.getPaymentWalletId())
.build(); .build();
return invoicePaymentRepository.save(payment).flatMap(savedPayment -> { return invoicePaymentRepository.save(payment).flatMap(savedPayment -> {
if (savedPayment.getWalletId() != null) { return ledgerService.resolvePaymentWallet(userId, savedPayment.getWalletId(), savedPayment.getPaymentMethod())
.flatMap(targetWallet -> {
savedPayment.setWalletId(targetWallet.getId());
Transaction transaction = Transaction.builder() Transaction transaction = Transaction.builder()
.userId(userId) .userId(userId)
.toWalletId(savedPayment.getWalletId()) .toWalletId(targetWallet.getId())
.type("INCOME") .type("INCOME")
.amount(savedPayment.getAmount()) .amount(savedPayment.getAmount())
.date(savedPayment.getPaymentDate()) .date(savedPayment.getPaymentDate())
@@ -123,9 +128,10 @@ public class InvoiceService {
.notes(savedPayment.getPaymentMethod()) .notes(savedPayment.getPaymentMethod())
.createdAt(LocalDateTime.now()) .createdAt(LocalDateTime.now())
.build(); .build();
return transactionService.addTransaction(userId, transaction).thenReturn(savedInvoice); return transactionService.addTransaction(userId, transaction)
} .then(invoicePaymentRepository.save(savedPayment))
return Mono.just(savedInvoice); .thenReturn(savedInvoice);
});
}); });
} }
return Mono.just(savedInvoice); return Mono.just(savedInvoice);
@@ -141,10 +147,13 @@ public class InvoiceService {
return Flux.fromIterable(invoice.getItems()) return Flux.fromIterable(invoice.getItems())
.concatMap(item -> { .concatMap(item -> {
if (item.getInventoryItemId() != null) { if (item.getInventoryItemId() != null) {
return inventoryItemService.updateItem(userId, item.getInventoryItemId(), return inventoryItemRepository.findById(item.getInventoryItemId())
com.kifi.api.entity.inventory.InventoryItem.builder() .flatMap(invItem -> {
.status("SOLD") invItem.setStatus("SOLD");
.build()); invItem.setUpdatedAt(LocalDateTime.now());
BigDecimal cost = invItem.getPurchaseCost() != null ? invItem.getPurchaseCost() : BigDecimal.ZERO;
return inventoryItemRepository.save(invItem).thenReturn(cost);
}).defaultIfEmpty(BigDecimal.ZERO);
} else if (item.getProductId() != null) { } else if (item.getProductId() != null) {
InventoryMovement movement = InventoryMovement.builder() InventoryMovement movement = InventoryMovement.builder()
.userId(userId) .userId(userId)
@@ -158,11 +167,37 @@ public class InvoiceService {
movementItem.setQuantity(item.getQuantity()); movementItem.setQuantity(item.getQuantity());
movement.setItems(java.util.Collections.singletonList(movementItem)); movement.setItems(java.util.Collections.singletonList(movementItem));
return productService.adjustStock(userId, item.getProductId(), movement); return productService.adjustStock(userId, item.getProductId(), movement)
.thenReturn(BigDecimal.ZERO);
} }
return Mono.just(item); return Mono.just(BigDecimal.ZERO);
}) })
.then(Mono.just(invoice)); .reduce(BigDecimal.ZERO, BigDecimal::add)
.flatMap(totalCogs -> {
if (totalCogs.compareTo(BigDecimal.ZERO) > 0) {
return Mono.zip(
ledgerService.getInventoryAssetLedger(userId),
ledgerService.getCogsLedger(userId)
).flatMap(ledgers -> {
Transaction cogsTx = Transaction.builder()
.userId(userId)
.fromWalletId(ledgers.getT1().getId()) // Inventory Asset (Asset decreases)
.toWalletId(ledgers.getT2().getId()) // COGS (Expense increases)
.type("EXPENSE")
.amount(totalCogs)
.date(invoice.getIssueDate() != null ? invoice.getIssueDate() : java.time.LocalDate.now())
.description("COGS for Invoice #" + invoice.getInvoiceNumber())
.createdAt(LocalDateTime.now())
.build();
return transactionService.addTransaction(userId, cogsTx);
}).onErrorResume(err -> {
System.err.println("COGS relief transaction error: " + err.getMessage());
return Mono.empty();
});
}
return Mono.empty();
})
.thenReturn(invoice);
} }
return Mono.just(invoice); return Mono.just(invoice);
}); });
@@ -248,10 +283,12 @@ public class InvoiceService {
} }
} }
return processMono.flatMap(processedInv -> { return processMono.flatMap(processedInv -> {
if (savedPayment.getWalletId() != null) { return ledgerService.resolvePaymentWallet(userId, savedPayment.getWalletId(), savedPayment.getPaymentMethod())
.flatMap(targetWallet -> {
savedPayment.setWalletId(targetWallet.getId());
Transaction transaction = Transaction.builder() Transaction transaction = Transaction.builder()
.userId(userId) .userId(userId)
.toWalletId(savedPayment.getWalletId()) .toWalletId(targetWallet.getId())
.type("INCOME") .type("INCOME")
.amount(savedPayment.getAmount()) .amount(savedPayment.getAmount())
.date(savedPayment.getPaymentDate()) .date(savedPayment.getPaymentDate())
@@ -259,9 +296,10 @@ public class InvoiceService {
.notes(savedPayment.getPaymentMethod()) .notes(savedPayment.getPaymentMethod())
.createdAt(LocalDateTime.now()) .createdAt(LocalDateTime.now())
.build(); .build();
return transactionService.addTransaction(userId, transaction).thenReturn(savedPayment); return transactionService.addTransaction(userId, transaction)
} .then(invoicePaymentRepository.save(savedPayment))
return Mono.just(savedPayment); .thenReturn(savedPayment);
});
}); });
}); });
}); });

View File

@@ -90,7 +90,7 @@ public class PurchaseOrderService {
item.setPoId(savedPo.getId()); item.setPoId(savedPo.getId());
return item; return item;
}) })
.flatMap(purchaseOrderItemRepository::save) .concatMap(purchaseOrderItemRepository::save)
.collectList() .collectList()
.map(savedItems -> { .map(savedItems -> {
savedPo.setItems(savedItems); savedPo.setItems(savedItems);
@@ -126,7 +126,7 @@ public class PurchaseOrderService {
item.setPoId(id); item.setPoId(id);
return item; return item;
}) })
.flatMap(purchaseOrderItemRepository::save)) .concatMap(purchaseOrderItemRepository::save))
.collectList() .collectList()
.map(savedItems -> { .map(savedItems -> {
savedPo.setItems(savedItems); savedPo.setItems(savedItems);
@@ -160,7 +160,7 @@ public class PurchaseOrderService {
item.setPoId(id); item.setPoId(id);
return item; return item;
}) })
.flatMap(purchaseOrderItemRepository::save)) .concatMap(purchaseOrderItemRepository::save))
.collectList(); .collectList();
} else { } else {
itemsProcess = purchaseOrderItemRepository.findByPoId(id).collectList(); itemsProcess = purchaseOrderItemRepository.findByPoId(id).collectList();
@@ -179,10 +179,10 @@ public class PurchaseOrderService {
.build()) .build())
.map(InventoryLocation::getId))); .map(InventoryLocation::getId)));
// 2. Update stock & inventory items for all received items // 2. Update stock & inventory items for all received items sequentially
Mono<Void> stockUpdates = locationIdMono.flatMap(locId -> Mono<Void> stockUpdates = locationIdMono.flatMap(locId ->
Flux.fromIterable(savedItems) Flux.fromIterable(savedItems)
.flatMap(item -> { .concatMap(item -> {
if (item.getProductId() != null) { if (item.getProductId() != null) {
InventoryItem invItem = InventoryItem.builder() InventoryItem invItem = InventoryItem.builder()
.userId(userId) .userId(userId)
@@ -195,6 +195,7 @@ public class PurchaseOrderService {
.huid(item.getHuid()) .huid(item.getHuid())
.grossWeight(item.getWeight()) .grossWeight(item.getWeight())
.netWeight(item.getWeight()) .netWeight(item.getWeight())
.status("AVAILABLE")
.build(); .build();
BigDecimal itemQty = (item.getWeight() != null && item.getWeight().compareTo(BigDecimal.ZERO) > 0) BigDecimal itemQty = (item.getWeight() != null && item.getWeight().compareTo(BigDecimal.ZERO) > 0)
@@ -279,7 +280,41 @@ public class PurchaseOrderService {
BigDecimal newPaid = po.getAmountPaid() != null ? po.getAmountPaid().add(savedPayment.getAmount()) : savedPayment.getAmount(); BigDecimal newPaid = po.getAmountPaid() != null ? po.getAmountPaid().add(savedPayment.getAmount()) : savedPayment.getAmount();
po.setAmountPaid(newPaid); po.setAmountPaid(newPaid);
po.setUpdatedAt(LocalDateTime.now()); po.setUpdatedAt(LocalDateTime.now());
return purchaseOrderRepository.save(po).thenReturn(savedPayment);
Mono<Void> txMono = Mono.empty();
if (po.getVendorId() != null && savedPayment.getAmount() != null && savedPayment.getAmount().compareTo(BigDecimal.ZERO) > 0) {
txMono = vendorRepository.findById(po.getVendorId())
.flatMap(vendor -> Mono.zip(
ledgerService.resolvePaymentWallet(userId, null, savedPayment.getPaymentMethod()),
ledgerService.getVendorLedger(userId, vendor.getId(), vendor.getName())
))
.flatMap(wallets -> {
com.kifi.api.entity.Transaction tx = com.kifi.api.entity.Transaction.builder()
.userId(userId)
.fromWalletId(wallets.getT1().getId()) // Cash / Bank (Asset decreases)
.toWalletId(wallets.getT2().getId()) // Vendor Payable (Liability decreases / is settled)
.type("EXPENSE")
.amount(savedPayment.getAmount())
.date(savedPayment.getPaymentDate() != null ? savedPayment.getPaymentDate().toLocalDate() : java.time.LocalDate.now())
.description("Payment for PO #" + po.getPoNumber())
.notes(savedPayment.getPaymentMethod())
.createdAt(LocalDateTime.now())
.build();
return transactionService.addTransaction(userId, tx)
.flatMap(txSaved -> {
savedPayment.setTransactionId(txSaved.getId());
return purchasePaymentRepository.save(savedPayment);
}).then();
})
.onErrorResume(err -> {
System.err.println("Failed to post vendor payment transaction: " + err.getMessage());
return Mono.empty();
});
}
return purchaseOrderRepository.save(po)
.then(txMono)
.thenReturn(savedPayment);
}); });
}); });
} }

View File

@@ -240,12 +240,16 @@ class _ProductListScreenState extends ConsumerState<ProductListScreen> {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text( Text(
(p.currentStock != null) p.currentStock != null
? 'Stock: ${p.currentStock ?? 0}' ? (p.currentStock! > 0
? 'Stock: ${p.currentStock! % 1 == 0 ? p.currentStock!.toInt() : p.currentStock}'
: 'Out of Stock')
: 'Untracked', : 'Untracked',
style: TextStyle( style: TextStyle(
color: (p.currentStock != null) color: p.currentStock != null
? Colors.orange ? (p.currentStock! > 0
? const Color(0xFF10B981)
: Colors.red)
: Colors.grey, : Colors.grey,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

View File

@@ -421,7 +421,10 @@ class _InvoiceBuilderScreenState extends ConsumerState<InvoiceBuilderScreen> {
totalAmount: grandTotal, totalAmount: grandTotal,
amountPaid: validAmountPaid, amountPaid: validAmountPaid,
paymentMethod: validAmountPaid > 0 ? _paymentMethod : null, paymentMethod: validAmountPaid > 0 ? _paymentMethod : null,
paymentWalletId: validAmountPaid > 0 ? (_selectedWalletId ?? ref.read(walletProvider).value?.firstOrNull?.id) : null, paymentWalletId: validAmountPaid > 0
? (_selectedWalletId ??
ref.read(walletProvider).value?.where((w) => w.nature == 'CASH' || w.nature == 'SAVINGS').firstOrNull?.id)
: null,
nextPaymentDate: balanceDue > 0.01 ? (_nextPaymentDate ?? DateTime.now().add(const Duration(days: 30))) : null, nextPaymentDate: balanceDue > 0.01 ? (_nextPaymentDate ?? DateTime.now().add(const Duration(days: 30))) : null,
status: widget.existingInvoice != null status: widget.existingInvoice != null
? (isFullyPaid ? 'PAID' : (validAmountPaid > 0.01 ? 'PARTIAL' : widget.existingInvoice!.status)) ? (isFullyPaid ? 'PAID' : (validAmountPaid > 0.01 ? 'PARTIAL' : widget.existingInvoice!.status))