Fixed upto purchases and rate sync process
This commit is contained in:
@@ -9,20 +9,27 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/kifi-v2/inventory/commodity-rates")
|
@RequestMapping("/api/kifi-v2/inventory/commodity-rates")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class CommodityRateController {
|
public class CommodityRateController {
|
||||||
private final CommodityRateService rateService;
|
private final CommodityRateService rateService;
|
||||||
|
|
||||||
@GetMapping("/{commodityName}")
|
@GetMapping("/latest")
|
||||||
public Flux<CommodityRateHistory> getRateHistory(@PathVariable String commodityName) {
|
public Flux<CommodityRateHistory> getAllLatestRates() {
|
||||||
return rateService.getRateHistory(commodityName);
|
return rateService.getAllLatestRates();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{commodityName}/latest")
|
@GetMapping("/{commodityCode}")
|
||||||
public Mono<ResponseEntity<CommodityRateHistory>> getLatestRate(@PathVariable String commodityName) {
|
public Flux<CommodityRateHistory> getRateHistory(@PathVariable String commodityCode) {
|
||||||
return rateService.getLatestRate(commodityName)
|
return rateService.getRateHistory(commodityCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{commodityCode}/latest")
|
||||||
|
public Mono<ResponseEntity<CommodityRateHistory>> getLatestRate(@PathVariable String commodityCode) {
|
||||||
|
return rateService.getLatestRate(commodityCode)
|
||||||
.map(ResponseEntity::ok)
|
.map(ResponseEntity::ok)
|
||||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||||
}
|
}
|
||||||
@@ -32,12 +39,28 @@ public class CommodityRateController {
|
|||||||
Authentication authentication,
|
Authentication authentication,
|
||||||
@RequestBody CommodityRateHistory rateHistory) {
|
@RequestBody CommodityRateHistory rateHistory) {
|
||||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
String code = rateHistory.getCommodityCode() != null ? rateHistory.getCommodityCode() : rateHistory.getCommodityName();
|
||||||
|
String name = rateHistory.getCommodityName() != null ? rateHistory.getCommodityName() : code;
|
||||||
return rateService.addRate(
|
return rateService.addRate(
|
||||||
rateHistory.getCommodityName(),
|
code,
|
||||||
|
name,
|
||||||
rateHistory.getPurity(),
|
rateHistory.getPurity(),
|
||||||
rateHistory.getRate(),
|
rateHistory.getRate(),
|
||||||
rateHistory.getSource() != null ? rateHistory.getSource() : "MANUAL",
|
rateHistory.getSource() != null ? rateHistory.getSource() : "MANUAL",
|
||||||
userId
|
userId
|
||||||
).map(ResponseEntity::ok);
|
).map(ResponseEntity::ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{commodityCode}/sync")
|
||||||
|
public Mono<ResponseEntity<Map<String, Object>>> syncRatesToProducts(
|
||||||
|
Authentication authentication,
|
||||||
|
@PathVariable String commodityCode) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return rateService.syncRatesToProducts(commodityCode, userId)
|
||||||
|
.map(count -> ResponseEntity.ok(Map.<String, Object>of(
|
||||||
|
"commodityCode", commodityCode,
|
||||||
|
"updatedCount", count,
|
||||||
|
"message", "Successfully synced rates for " + count + " products"
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ public class ProductCategoryController {
|
|||||||
existing.setParentCategoryId(category.getParentCategoryId());
|
existing.setParentCategoryId(category.getParentCategoryId());
|
||||||
existing.setHasChild(category.getHasChild());
|
existing.setHasChild(category.getHasChild());
|
||||||
existing.setCommodityCode(category.getCommodityCode());
|
existing.setCommodityCode(category.getCommodityCode());
|
||||||
|
existing.setPurityFactor(category.getPurityFactor() != null ? category.getPurityFactor() : 1.0);
|
||||||
existing.setDefaultHsn(category.getDefaultHsn());
|
existing.setDefaultHsn(category.getDefaultHsn());
|
||||||
existing.setDefaultGst(category.getDefaultGst());
|
existing.setDefaultGst(category.getDefaultGst());
|
||||||
existing.setHuidRequired(category.getHuidRequired());
|
existing.setHuidRequired(category.getHuidRequired());
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import java.time.LocalDateTime;
|
|||||||
public class CommodityRateHistory {
|
public class CommodityRateHistory {
|
||||||
@Id
|
@Id
|
||||||
private Long id;
|
private Long id;
|
||||||
|
private String commodityCode;
|
||||||
private String commodityName;
|
private String commodityName;
|
||||||
private String purity;
|
private String purity;
|
||||||
private BigDecimal rate;
|
private BigDecimal rate;
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ public class ProductCategory {
|
|||||||
@Builder.Default
|
@Builder.Default
|
||||||
private Boolean hasChild = false;
|
private Boolean hasChild = false;
|
||||||
private String commodityCode;
|
private String commodityCode;
|
||||||
|
@Builder.Default
|
||||||
|
private Double purityFactor = 1.0;
|
||||||
private String defaultHsn;
|
private String defaultHsn;
|
||||||
private java.math.BigDecimal defaultGst;
|
private java.math.BigDecimal defaultGst;
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import com.kifi.api.entity.TransactionItem;
|
|||||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface TransactionItemRepository extends ReactiveCrudRepository<TransactionItem, Long> {
|
public interface TransactionItemRepository extends ReactiveCrudRepository<TransactionItem, Long> {
|
||||||
Flux<TransactionItem> findByTransactionId(Long transactionId);
|
Flux<TransactionItem> findByTransactionId(Long transactionId);
|
||||||
|
Mono<Void> deleteByTransactionId(Long transactionId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
package com.kifi.api.repository.inventory;
|
package com.kifi.api.repository.inventory;
|
||||||
|
|
||||||
import com.kifi.api.entity.inventory.CommodityRateHistory;
|
import com.kifi.api.entity.inventory.CommodityRateHistory;
|
||||||
|
import org.springframework.data.r2dbc.repository.Query;
|
||||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
public interface CommodityRateHistoryRepository extends ReactiveCrudRepository<CommodityRateHistory, Long> {
|
public interface CommodityRateHistoryRepository extends ReactiveCrudRepository<CommodityRateHistory, Long> {
|
||||||
|
Flux<CommodityRateHistory> findByCommodityCodeOrderByEffectiveAtDesc(String commodityCode);
|
||||||
Flux<CommodityRateHistory> findByCommodityNameOrderByEffectiveAtDesc(String commodityName);
|
Flux<CommodityRateHistory> findByCommodityNameOrderByEffectiveAtDesc(String commodityName);
|
||||||
|
|
||||||
|
@Query("SELECT DISTINCT ON (commodity_code) * FROM commodity_rate_history WHERE commodity_code IS NOT NULL ORDER BY commodity_code, effective_at DESC")
|
||||||
|
Flux<CommodityRateHistory> findLatestRatesForAllCommodities();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ import reactor.core.publisher.Flux;
|
|||||||
|
|
||||||
public interface ProductCategoryRepository extends ReactiveCrudRepository<ProductCategory, Long> {
|
public interface ProductCategoryRepository extends ReactiveCrudRepository<ProductCategory, Long> {
|
||||||
Flux<ProductCategory> findByUserId(Long userId);
|
Flux<ProductCategory> findByUserId(Long userId);
|
||||||
|
Flux<ProductCategory> findByUserIdAndCommodityCode(Long userId, String commodityCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ public class TransactionService {
|
|||||||
public Mono<Transaction> updateTransaction(Long id, Long userId, Transaction updatedTransaction) {
|
public Mono<Transaction> updateTransaction(Long id, Long userId, Transaction updatedTransaction) {
|
||||||
return transactionRepository.findById(id)
|
return transactionRepository.findById(id)
|
||||||
.filter(t -> t.getUserId().equals(userId))
|
.filter(t -> t.getUserId().equals(userId))
|
||||||
|
.switchIfEmpty(Mono.error(new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.NOT_FOUND, "Transaction not found")))
|
||||||
.flatMap(t -> {
|
.flatMap(t -> {
|
||||||
// Revert old balances
|
// Revert old balances
|
||||||
Mono<Void> revertBalances = updateWalletBalances(t.getToWalletId(), t.getFromWalletId(), t.getAmount());
|
Mono<Void> revertBalances = updateWalletBalances(t.getToWalletId(), t.getFromWalletId(), t.getAmount());
|
||||||
@@ -152,9 +153,26 @@ public class TransactionService {
|
|||||||
t.setAmount(updatedTransaction.getAmount());
|
t.setAmount(updatedTransaction.getAmount());
|
||||||
t.setDate(updatedTransaction.getDate());
|
t.setDate(updatedTransaction.getDate());
|
||||||
t.setDescription(updatedTransaction.getDescription());
|
t.setDescription(updatedTransaction.getDescription());
|
||||||
|
t.setDueDate(updatedTransaction.getDueDate());
|
||||||
|
t.setAlertSchedule(updatedTransaction.getAlertSchedule());
|
||||||
|
t.setAlertTime(updatedTransaction.getAlertTime());
|
||||||
|
|
||||||
return revertBalances.then(applyNewBalances).then(transactionRepository.save(t));
|
return revertBalances.then(applyNewBalances).then(transactionRepository.save(t));
|
||||||
}).flatMap(this::populateItemsAndAttachments);
|
})
|
||||||
|
.flatMap(savedTx -> {
|
||||||
|
Mono<Void> deleteOldItems = transactionItemRepository.deleteByTransactionId(savedTx.getId());
|
||||||
|
Mono<Void> saveNewItems = Mono.empty();
|
||||||
|
if (updatedTransaction.getItems() != null && !updatedTransaction.getItems().isEmpty()) {
|
||||||
|
for (TransactionItem item : updatedTransaction.getItems()) {
|
||||||
|
item.setId(null);
|
||||||
|
item.setTransactionId(savedTx.getId());
|
||||||
|
item.setCreatedAt(LocalDateTime.now());
|
||||||
|
}
|
||||||
|
saveNewItems = transactionItemRepository.saveAll(updatedTransaction.getItems()).then();
|
||||||
|
}
|
||||||
|
return deleteOldItems.then(saveNewItems).thenReturn(savedTx);
|
||||||
|
})
|
||||||
|
.flatMap(this::populateItemsAndAttachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
|||||||
@@ -1,38 +1,82 @@
|
|||||||
package com.kifi.api.service.inventory;
|
package com.kifi.api.service.inventory;
|
||||||
|
|
||||||
import com.kifi.api.entity.inventory.CommodityRateHistory;
|
import com.kifi.api.entity.inventory.CommodityRateHistory;
|
||||||
|
import com.kifi.api.entity.inventory.Product;
|
||||||
|
import com.kifi.api.entity.inventory.ProductCategory;
|
||||||
import com.kifi.api.repository.inventory.CommodityRateHistoryRepository;
|
import com.kifi.api.repository.inventory.CommodityRateHistoryRepository;
|
||||||
|
import com.kifi.api.repository.inventory.ProductCategoryRepository;
|
||||||
|
import com.kifi.api.repository.inventory.ProductRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
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.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class CommodityRateService {
|
public class CommodityRateService {
|
||||||
private final CommodityRateHistoryRepository rateHistoryRepository;
|
private final CommodityRateHistoryRepository rateHistoryRepository;
|
||||||
|
private final ProductCategoryRepository productCategoryRepository;
|
||||||
|
private final ProductRepository productRepository;
|
||||||
|
|
||||||
public Mono<CommodityRateHistory> addRate(String commodityName, String purity, BigDecimal rate, String source, Long userId) {
|
public Mono<CommodityRateHistory> addRate(String commodityCode, String commodityName, String purity, BigDecimal rate, String source, Long userId) {
|
||||||
CommodityRateHistory history = CommodityRateHistory.builder()
|
CommodityRateHistory history = CommodityRateHistory.builder()
|
||||||
.commodityName(commodityName)
|
.commodityCode(commodityCode)
|
||||||
|
.commodityName(commodityName != null && !commodityName.isEmpty() ? commodityName : commodityCode)
|
||||||
.purity(purity)
|
.purity(purity)
|
||||||
.rate(rate)
|
.rate(rate)
|
||||||
.effectiveAt(LocalDateTime.now())
|
.effectiveAt(LocalDateTime.now())
|
||||||
.source(source)
|
.source(source != null ? source : "MANUAL")
|
||||||
.createdBy(userId)
|
.createdBy(userId)
|
||||||
.createdAt(LocalDateTime.now())
|
.createdAt(LocalDateTime.now())
|
||||||
.build();
|
.build();
|
||||||
return rateHistoryRepository.save(history);
|
return rateHistoryRepository.save(history);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flux<CommodityRateHistory> getRateHistory(String commodityName) {
|
public Flux<CommodityRateHistory> getRateHistory(String commodityCode) {
|
||||||
return rateHistoryRepository.findByCommodityNameOrderByEffectiveAtDesc(commodityName);
|
return rateHistoryRepository.findByCommodityCodeOrderByEffectiveAtDesc(commodityCode)
|
||||||
|
.switchIfEmpty(rateHistoryRepository.findByCommodityNameOrderByEffectiveAtDesc(commodityCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mono<CommodityRateHistory> getLatestRate(String commodityName) {
|
public Mono<CommodityRateHistory> getLatestRate(String commodityCode) {
|
||||||
return getRateHistory(commodityName).next();
|
return getRateHistory(commodityCode).next();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Flux<CommodityRateHistory> getAllLatestRates() {
|
||||||
|
return rateHistoryRepository.findLatestRatesForAllCommodities();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Integer> syncRatesToProducts(String commodityCode, Long userId) {
|
||||||
|
return getLatestRate(commodityCode)
|
||||||
|
.flatMap(latestRate ->
|
||||||
|
productCategoryRepository.findByUserIdAndCommodityCode(userId, commodityCode)
|
||||||
|
.flatMap(category -> {
|
||||||
|
double categoryPurity = category.getPurityFactor() != null ? category.getPurityFactor() : 1.0;
|
||||||
|
return productRepository.findByCategoryId(category.getId())
|
||||||
|
.flatMap(product -> {
|
||||||
|
double productPurity = product.getPurityFactor() != null ? product.getPurityFactor() : categoryPurity;
|
||||||
|
BigDecimal baseRate = latestRate.getRate().multiply(BigDecimal.valueOf(productPurity));
|
||||||
|
|
||||||
|
if ("WEIGHT_BASED".equalsIgnoreCase(product.getPriceCalcRule())) {
|
||||||
|
BigDecimal making = product.getMakingCharges() != null ? BigDecimal.valueOf(product.getMakingCharges()) : BigDecimal.ZERO;
|
||||||
|
BigDecimal newPrice = baseRate;
|
||||||
|
if ("PERCENTAGE".equalsIgnoreCase(product.getMakingChargesType())) {
|
||||||
|
newPrice = baseRate.add(baseRate.multiply(making).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP));
|
||||||
|
} else {
|
||||||
|
newPrice = baseRate.add(making);
|
||||||
|
}
|
||||||
|
product.setSellingPrice(newPrice.setScale(2, RoundingMode.HALF_UP));
|
||||||
|
}
|
||||||
|
product.setUpdatedAt(LocalDateTime.now());
|
||||||
|
return productRepository.save(product);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.collectList()
|
||||||
|
.map(list -> list.size())
|
||||||
|
)
|
||||||
|
.defaultIfEmpty(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,7 +201,8 @@ CREATE TABLE IF NOT EXISTS product_categories (
|
|||||||
default_gst DECIMAL(5, 2),
|
default_gst DECIMAL(5, 2),
|
||||||
huid_required BOOLEAN DEFAULT FALSE,
|
huid_required BOOLEAN DEFAULT FALSE,
|
||||||
default_making_charge DECIMAL(15, 2),
|
default_making_charge DECIMAL(15, 2),
|
||||||
making_charge_type VARCHAR(50),
|
commodity_code VARCHAR(10),
|
||||||
|
purity_factor DECIMAL(5, 4) DEFAULT 1.0,
|
||||||
base_unit VARCHAR(20) DEFAULT 'pcs',
|
base_unit VARCHAR(20) DEFAULT 'pcs',
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
sort_order INTEGER DEFAULT 0,
|
sort_order INTEGER DEFAULT 0,
|
||||||
@@ -210,7 +211,8 @@ CREATE TABLE IF NOT EXISTS product_categories (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS commodity_rate_history (
|
CREATE TABLE IF NOT EXISTS commodity_rate_history (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
commodity_name VARCHAR(100) NOT NULL,
|
commodity_code VARCHAR(100) NOT NULL,
|
||||||
|
commodity_name VARCHAR(100),
|
||||||
purity VARCHAR(50),
|
purity VARCHAR(50),
|
||||||
rate DECIMAL(15, 2) NOT NULL,
|
rate DECIMAL(15, 2) NOT NULL,
|
||||||
effective_at TIMESTAMP NOT NULL,
|
effective_at TIMESTAMP NOT NULL,
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ class DioClient {
|
|||||||
|
|
||||||
DioClient._internal()
|
DioClient._internal()
|
||||||
: dio = Dio(BaseOptions(
|
: dio = Dio(BaseOptions(
|
||||||
baseUrl: 'https://app.technobeesolutions.in/api/kifi-v2',
|
//baseUrl: 'https://app.technobeesolutions.in/api/kifi-v2',
|
||||||
//baseUrl: 'http://192.168.0.104:8080/api/kifi-v2', // Local Mac IP for physical device testing
|
//baseUrl: 'http://192.168.0.104:8080/api/kifi-v2', // Local Mac IP for physical device testing
|
||||||
|
baseUrl: 'http://192.168.1.5:8080/api/kifi-v2', // Current Local Mac IP (192.168.1.5)
|
||||||
connectTimeout: const Duration(seconds: 10),
|
connectTimeout: const Duration(seconds: 10),
|
||||||
receiveTimeout: const Duration(seconds: 10),
|
receiveTimeout: const Duration(seconds: 10),
|
||||||
)),
|
)),
|
||||||
|
|||||||
54
kifi-app/lib/features/inventory/domain/commodity_rate.dart
Normal file
54
kifi-app/lib/features/inventory/domain/commodity_rate.dart
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
class CommodityRateHistory {
|
||||||
|
final int? id;
|
||||||
|
final String commodityCode;
|
||||||
|
final String? commodityName;
|
||||||
|
final String? purity;
|
||||||
|
final double rate;
|
||||||
|
final DateTime effectiveAt;
|
||||||
|
final String? source;
|
||||||
|
final int? createdBy;
|
||||||
|
final DateTime? createdAt;
|
||||||
|
|
||||||
|
CommodityRateHistory({
|
||||||
|
this.id,
|
||||||
|
required this.commodityCode,
|
||||||
|
this.commodityName,
|
||||||
|
this.purity,
|
||||||
|
required this.rate,
|
||||||
|
required this.effectiveAt,
|
||||||
|
this.source,
|
||||||
|
this.createdBy,
|
||||||
|
this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CommodityRateHistory.fromJson(Map<String, dynamic> json) {
|
||||||
|
return CommodityRateHistory(
|
||||||
|
id: json['id'] is int ? json['id'] : int.tryParse(json['id']?.toString() ?? ''),
|
||||||
|
commodityCode: (json['commodityCode'] ?? json['commodity_code'] ?? json['commodityName'] ?? json['commodity_name'] ?? '').toString(),
|
||||||
|
commodityName: json['commodityName'] ?? json['commodity_name'],
|
||||||
|
purity: json['purity'],
|
||||||
|
rate: (json['rate'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
effectiveAt: json['effectiveAt'] != null
|
||||||
|
? DateTime.parse(json['effectiveAt'].toString())
|
||||||
|
: (json['effective_at'] != null ? DateTime.parse(json['effective_at'].toString()) : DateTime.now()),
|
||||||
|
source: json['source'],
|
||||||
|
createdBy: json['createdBy'] ?? json['created_by'],
|
||||||
|
createdAt: json['createdAt'] != null
|
||||||
|
? DateTime.parse(json['createdAt'].toString())
|
||||||
|
: (json['created_at'] != null ? DateTime.parse(json['created_at'].toString()) : null),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
'commodityCode': commodityCode,
|
||||||
|
'commodityName': commodityName ?? commodityCode,
|
||||||
|
if (purity != null) 'purity': purity,
|
||||||
|
'rate': rate,
|
||||||
|
'effectiveAt': effectiveAt.toIso8601String(),
|
||||||
|
if (source != null) 'source': source,
|
||||||
|
if (createdBy != null) 'createdBy': createdBy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -243,9 +243,10 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
final _hsnController = TextEditingController();
|
final _hsnController = TextEditingController();
|
||||||
final _gstController = TextEditingController();
|
final _gstController = TextEditingController();
|
||||||
final _makingChargeController = TextEditingController();
|
final _makingChargeController = TextEditingController();
|
||||||
|
final _purityFactorController = TextEditingController(text: '1.0');
|
||||||
|
|
||||||
int? _selectedParentId;
|
int? _selectedParentId;
|
||||||
bool _isLeaf = false;
|
bool _isLeaf = true;
|
||||||
bool _huidRequired = false;
|
bool _huidRequired = false;
|
||||||
String _commodityCode = 'XAU';
|
String _commodityCode = 'XAU';
|
||||||
String _makingChargeType = 'PER_GRAM';
|
String _makingChargeType = 'PER_GRAM';
|
||||||
@@ -271,6 +272,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
_hsnController.text = c.defaultHsn ?? '';
|
_hsnController.text = c.defaultHsn ?? '';
|
||||||
_gstController.text = c.defaultGst?.toString() ?? '';
|
_gstController.text = c.defaultGst?.toString() ?? '';
|
||||||
_makingChargeController.text = c.defaultMakingCharge?.toString() ?? '';
|
_makingChargeController.text = c.defaultMakingCharge?.toString() ?? '';
|
||||||
|
_purityFactorController.text = (c.purityFactor ?? 1.0).toString();
|
||||||
_huidRequired = c.huidRequired;
|
_huidRequired = c.huidRequired;
|
||||||
_commodityCode = c.commodityCode ?? 'XAU';
|
_commodityCode = c.commodityCode ?? 'XAU';
|
||||||
|
|
||||||
@@ -293,6 +295,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
_hsnController.dispose();
|
_hsnController.dispose();
|
||||||
_gstController.dispose();
|
_gstController.dispose();
|
||||||
_makingChargeController.dispose();
|
_makingChargeController.dispose();
|
||||||
|
_purityFactorController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,6 +307,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
parentCategoryId: _selectedParentId,
|
parentCategoryId: _selectedParentId,
|
||||||
hasChild: !_isLeaf,
|
hasChild: !_isLeaf,
|
||||||
commodityCode: _isLeaf ? _commodityCode : null,
|
commodityCode: _isLeaf ? _commodityCode : null,
|
||||||
|
purityFactor: _isLeaf ? (double.tryParse(_purityFactorController.text) ?? 1.0) : 1.0,
|
||||||
defaultHsn: _isLeaf ? _hsnController.text.trim() : null,
|
defaultHsn: _isLeaf ? _hsnController.text.trim() : null,
|
||||||
defaultGst: _isLeaf ? double.tryParse(_gstController.text) : null,
|
defaultGst: _isLeaf ? double.tryParse(_gstController.text) : null,
|
||||||
huidRequired: _isLeaf ? _huidRequired : false,
|
huidRequired: _isLeaf ? _huidRequired : false,
|
||||||
@@ -457,17 +461,34 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
if (isBusinessMode) ...[
|
if (isBusinessMode) ...[
|
||||||
DropdownButtonFormField<String>(
|
Row(
|
||||||
value: _commodityCode,
|
children: [
|
||||||
decoration: InputDecoration(
|
Expanded(
|
||||||
labelText: 'Commodity Code',
|
flex: 3,
|
||||||
prefixIcon: const Icon(LucideIcons.barChart2),
|
child: DropdownButtonFormField<String>(
|
||||||
),
|
value: _commodityCode,
|
||||||
items: _commodities.map((c) => DropdownMenuItem(
|
decoration: const InputDecoration(
|
||||||
value: c['code'],
|
labelText: 'Commodity Code',
|
||||||
child: Text(c['label']!),
|
prefixIcon: Icon(LucideIcons.barChart2),
|
||||||
)).toList(),
|
),
|
||||||
onChanged: (val) => setState(() => _commodityCode = val!),
|
items: _commodities.map((c) => DropdownMenuItem(
|
||||||
|
value: c['code'],
|
||||||
|
child: Text(c['label']!),
|
||||||
|
)).toList(),
|
||||||
|
onChanged: (val) => setState(() => _commodityCode = val!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: _buildTextField(
|
||||||
|
controller: _purityFactorController,
|
||||||
|
label: 'Purity (e.g. 0.916)',
|
||||||
|
icon: LucideIcons.gem,
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import 'package:kifi_app/features/inventory/domain/inventory_item.dart';
|
|||||||
import 'package:kifi_app/features/inventory/domain/product.dart';
|
import 'package:kifi_app/features/inventory/domain/product.dart';
|
||||||
import 'package:kifi_app/features/inventory/providers/products_provider.dart';
|
import 'package:kifi_app/features/inventory/providers/products_provider.dart';
|
||||||
import 'package:kifi_app/features/inventory/providers/product_categories_provider.dart';
|
import 'package:kifi_app/features/inventory/providers/product_categories_provider.dart';
|
||||||
|
import 'package:kifi_app/features/inventory/providers/commodity_rates_provider.dart';
|
||||||
import 'package:kifi_app/core/network/dio_client.dart';
|
import 'package:kifi_app/core/network/dio_client.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:lucide_icons/lucide_icons.dart';
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
@@ -63,12 +64,26 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final categoriesState = ref.watch(productCategoriesProvider);
|
final categoriesState = ref.watch(productCategoriesProvider);
|
||||||
final category = categoriesState.value?.firstWhere(
|
final commodityRatesState = ref.watch(commodityRatesProvider);
|
||||||
|
|
||||||
|
final category = categoriesState.value?.where(
|
||||||
(c) => c.id == widget.product.categoryId,
|
(c) => c.id == widget.product.categoryId,
|
||||||
);
|
).firstOrNull;
|
||||||
|
|
||||||
final String unit = category?.baseUnit ?? 'g';
|
final String unit = category?.baseUnit ?? 'g';
|
||||||
final double currentRate = category?.dailyRate ?? 0.0;
|
final double purityFactor = category?.purityFactor ?? (widget.product.purityFactor ?? 1.0);
|
||||||
|
|
||||||
|
double commodityRate = 0.0;
|
||||||
|
if (category?.commodityCode != null && commodityRatesState.value != null) {
|
||||||
|
final match = commodityRatesState.value!.where(
|
||||||
|
(r) => r.commodityCode.toUpperCase() == category!.commodityCode!.toUpperCase()
|
||||||
|
).firstOrNull;
|
||||||
|
if (match != null) {
|
||||||
|
commodityRate = match.rate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final double currentRate = commodityRate > 0 ? (commodityRate * purityFactor) : (category?.dailyRate ?? 0.0);
|
||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: _fetchLedger,
|
onRefresh: _fetchLedger,
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../../core/network/dio_client.dart';
|
||||||
|
import '../domain/commodity_rate.dart';
|
||||||
|
|
||||||
|
class CommodityRatesNotifier extends AsyncNotifier<List<CommodityRateHistory>> {
|
||||||
|
@override
|
||||||
|
FutureOr<List<CommodityRateHistory>> build() async {
|
||||||
|
return _fetchLatestRates();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<CommodityRateHistory>> _fetchLatestRates() async {
|
||||||
|
try {
|
||||||
|
final response = await DioClient().dio.get('/inventory/commodity-rates/latest');
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final List<dynamic> data = response.data;
|
||||||
|
return data.map((e) => CommodityRateHistory.fromJson(e)).toList();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback if empty or endpoint fails
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<CommodityRateHistory>> fetchRateHistory(String commodityCode) async {
|
||||||
|
try {
|
||||||
|
final response = await DioClient().dio.get('/inventory/commodity-rates/$commodityCode');
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final List<dynamic> data = response.data;
|
||||||
|
return data.map((e) => CommodityRateHistory.fromJson(e)).toList();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Error fetching history
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<CommodityRateHistory?> addRate(
|
||||||
|
String commodityCode,
|
||||||
|
double rate, {
|
||||||
|
String? commodityName,
|
||||||
|
String? purity,
|
||||||
|
String? source,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final payload = {
|
||||||
|
'commodityCode': commodityCode,
|
||||||
|
'commodityName': commodityName ?? commodityCode,
|
||||||
|
if (purity != null) 'purity': purity,
|
||||||
|
'rate': rate,
|
||||||
|
'source': source ?? 'MANUAL',
|
||||||
|
};
|
||||||
|
|
||||||
|
final response = await DioClient().dio.post(
|
||||||
|
'/inventory/commodity-rates',
|
||||||
|
data: payload,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200 && response.data != null) {
|
||||||
|
final newRate = CommodityRateHistory.fromJson(response.data);
|
||||||
|
state = await AsyncValue.guard(() => _fetchLatestRates());
|
||||||
|
return newRate;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> syncRateToProducts(String commodityCode) async {
|
||||||
|
try {
|
||||||
|
final response = await DioClient().dio.post(
|
||||||
|
'/inventory/commodity-rates/$commodityCode/sync',
|
||||||
|
);
|
||||||
|
if (response.statusCode == 200 && response.data != null) {
|
||||||
|
return (response.data['updatedCount'] as num?)?.toInt() ?? 0;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final commodityRatesProvider =
|
||||||
|
AsyncNotifierProvider<CommodityRatesNotifier, List<CommodityRateHistory>>(() {
|
||||||
|
return CommodityRatesNotifier();
|
||||||
|
});
|
||||||
@@ -9,6 +9,7 @@ class ProductCategory {
|
|||||||
final int? parentCategoryId;
|
final int? parentCategoryId;
|
||||||
final bool hasChild;
|
final bool hasChild;
|
||||||
final String? commodityCode;
|
final String? commodityCode;
|
||||||
|
final double? purityFactor;
|
||||||
final String? defaultHsn;
|
final String? defaultHsn;
|
||||||
final double? defaultGst;
|
final double? defaultGst;
|
||||||
final bool huidRequired;
|
final bool huidRequired;
|
||||||
@@ -26,6 +27,7 @@ class ProductCategory {
|
|||||||
this.parentCategoryId,
|
this.parentCategoryId,
|
||||||
this.hasChild = false,
|
this.hasChild = false,
|
||||||
this.commodityCode,
|
this.commodityCode,
|
||||||
|
this.purityFactor = 1.0,
|
||||||
this.defaultHsn,
|
this.defaultHsn,
|
||||||
this.defaultGst,
|
this.defaultGst,
|
||||||
this.huidRequired = false,
|
this.huidRequired = false,
|
||||||
@@ -44,16 +46,17 @@ class ProductCategory {
|
|||||||
name: json['name'],
|
name: json['name'],
|
||||||
parentCategoryId: json['parentCategoryId'],
|
parentCategoryId: json['parentCategoryId'],
|
||||||
hasChild: json['hasChild'] ?? false,
|
hasChild: json['hasChild'] ?? false,
|
||||||
commodityCode: json['commodityCode'],
|
commodityCode: json['commodityCode'] ?? json['commodity_code'],
|
||||||
defaultHsn: json['defaultHsn'],
|
purityFactor: (json['purityFactor'] ?? json['purity_factor'] as num?)?.toDouble() ?? 1.0,
|
||||||
defaultGst: (json['defaultGst'] as num?)?.toDouble(),
|
defaultHsn: json['defaultHsn'] ?? json['default_hsn'],
|
||||||
huidRequired: json['huidRequired'] ?? false,
|
defaultGst: (json['defaultGst'] ?? json['default_gst'] as num?)?.toDouble(),
|
||||||
defaultMakingCharge: (json['defaultMakingCharge'] as num?)?.toDouble(),
|
huidRequired: json['huidRequired'] ?? json['huid_required'] ?? false,
|
||||||
makingChargeType: json['makingChargeType'],
|
defaultMakingCharge: (json['defaultMakingCharge'] ?? json['default_making_charge'] as num?)?.toDouble(),
|
||||||
baseUnit: json['baseUnit'] ?? 'pcs',
|
makingChargeType: json['makingChargeType'] ?? json['making_charge_type'],
|
||||||
isActive: json['isActive'] ?? true,
|
baseUnit: json['baseUnit'] ?? json['base_unit'] ?? 'pcs',
|
||||||
sortOrder: json['sortOrder'] ?? 0,
|
isActive: json['isActive'] ?? json['is_active'] ?? true,
|
||||||
dailyRate: (json['dailyRate'] as num?)?.toDouble(),
|
sortOrder: json['sortOrder'] ?? json['sort_order'] ?? 0,
|
||||||
|
dailyRate: (json['dailyRate'] ?? json['daily_rate'] as num?)?.toDouble(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +68,7 @@ class ProductCategory {
|
|||||||
'parentCategoryId': parentCategoryId,
|
'parentCategoryId': parentCategoryId,
|
||||||
'hasChild': hasChild,
|
'hasChild': hasChild,
|
||||||
'commodityCode': commodityCode,
|
'commodityCode': commodityCode,
|
||||||
|
'purityFactor': purityFactor ?? 1.0,
|
||||||
'defaultHsn': defaultHsn,
|
'defaultHsn': defaultHsn,
|
||||||
'defaultGst': defaultGst,
|
'defaultGst': defaultGst,
|
||||||
'huidRequired': huidRequired,
|
'huidRequired': huidRequired,
|
||||||
|
|||||||
@@ -570,117 +570,135 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
|||||||
final amount = double.tryParse(amountText);
|
final amount = double.tryParse(amountText);
|
||||||
if (amount == null) return;
|
if (amount == null) return;
|
||||||
|
|
||||||
if (fromWallet != null && fromWallet!.balance < amount) {
|
final isFromPayableOrLiability = fromWallet != null && (fromWallet!.nature == 'PAYABLES' || fromWallet!.nature == 'LOAN');
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Insufficient balance in From Account')));
|
if (fromWallet != null && !isFromPayableOrLiability) {
|
||||||
return;
|
double effectiveBalance = fromWallet!.balance;
|
||||||
|
if (widget.transaction != null && widget.transaction!.fromWalletId == fromWallet!.id) {
|
||||||
|
effectiveBalance += widget.transaction!.amount;
|
||||||
|
}
|
||||||
|
if (effectiveBalance < amount) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Insufficient balance in From Account')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() => isSaving = true);
|
setState(() => isSaving = true);
|
||||||
|
|
||||||
// Determine internal type based on natures
|
try {
|
||||||
String type = 'TRANSFER';
|
// Determine internal type based on natures
|
||||||
if (fromWallet == null) {
|
String type = 'TRANSFER';
|
||||||
type = 'INCOME';
|
if (fromWallet == null) {
|
||||||
} else if (toWallet == null) { // Though toWallet is mandatory, just in case
|
type = 'INCOME';
|
||||||
type = 'EXPENSE';
|
} else if (toWallet == null) { // Though toWallet is mandatory, just in case
|
||||||
} else if (fromWallet?.nature == 'INCOME') {
|
type = 'EXPENSE';
|
||||||
type = 'INCOME';
|
} else if (fromWallet?.nature == 'INCOME') {
|
||||||
} else if (toWallet?.nature == 'PAYABLES' || toWallet?.nature == 'EXPENSE') {
|
type = 'INCOME';
|
||||||
type = 'EXPENSE';
|
} else if (toWallet?.nature == 'PAYABLES' || toWallet?.nature == 'EXPENSE') {
|
||||||
} else if (toWallet?.nature == 'INVESTMENTS') {
|
type = 'EXPENSE';
|
||||||
type = 'INVESTMENT';
|
} else if (toWallet?.nature == 'INVESTMENTS') {
|
||||||
}
|
type = 'INVESTMENT';
|
||||||
|
|
||||||
if (isRecurring && widget.transaction == null) {
|
|
||||||
DateTime nextDate = selectedDate;
|
|
||||||
if (recurringFrequency == 'DAILY') {
|
|
||||||
nextDate = nextDate.add(const Duration(days: 1));
|
|
||||||
} else if (recurringFrequency == 'WEEKLY') {
|
|
||||||
nextDate = nextDate.add(const Duration(days: 7));
|
|
||||||
} else if (recurringFrequency == 'MONTHLY') {
|
|
||||||
nextDate = DateTime(nextDate.year, nextDate.month + 1, nextDate.day);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final rt = RecurringTransaction(
|
if (isRecurring && widget.transaction == null) {
|
||||||
id: 0,
|
DateTime nextDate = selectedDate;
|
||||||
type: type,
|
if (recurringFrequency == 'DAILY') {
|
||||||
amount: amount,
|
nextDate = nextDate.add(const Duration(days: 1));
|
||||||
categoryId: selectedCategory?.id,
|
} else if (recurringFrequency == 'WEEKLY') {
|
||||||
fromWalletId: fromWallet?.id,
|
nextDate = nextDate.add(const Duration(days: 7));
|
||||||
toWalletId: toWallet?.id,
|
} else if (recurringFrequency == 'MONTHLY') {
|
||||||
frequency: recurringFrequency,
|
nextDate = DateTime(nextDate.year, nextDate.month + 1, nextDate.day);
|
||||||
description: descriptionController.text.trim(),
|
}
|
||||||
nextExecutionDate: nextDate,
|
|
||||||
status: 'ACTIVE',
|
|
||||||
);
|
|
||||||
await ref.read(recurringTransactionProvider.notifier).addRecurringTransaction(rt);
|
|
||||||
|
|
||||||
final tx = Transaction(
|
final rt = RecurringTransaction(
|
||||||
id: 0,
|
id: 0,
|
||||||
type: type,
|
type: type,
|
||||||
amount: amount,
|
amount: amount,
|
||||||
date: selectedDate,
|
categoryId: selectedCategory?.id,
|
||||||
description: descriptionController.text.trim(),
|
fromWalletId: fromWallet?.id,
|
||||||
categoryId: selectedCategory?.id,
|
toWalletId: toWallet?.id,
|
||||||
fromWalletId: fromWallet?.id,
|
frequency: recurringFrequency,
|
||||||
toWalletId: toWallet?.id,
|
description: descriptionController.text.trim(),
|
||||||
dueDate: dueDate,
|
nextExecutionDate: nextDate,
|
||||||
alertSchedule: alertSchedule == 'NONE' ? null : alertSchedule,
|
status: 'ACTIVE',
|
||||||
alertTime: alertSchedule == 'NONE' ? null : '${alertTime.hour.toString().padLeft(2, '0')}:${alertTime.minute.toString().padLeft(2, '0')}:00',
|
);
|
||||||
items: lineItems.isNotEmpty ? lineItems : null,
|
await ref.read(recurringTransactionProvider.notifier).addRecurringTransaction(rt);
|
||||||
);
|
|
||||||
await ref.read(transactionProvider.notifier).addTransaction(tx, base64Attachments: base64Attachments);
|
|
||||||
} else {
|
|
||||||
final tx = Transaction(
|
|
||||||
id: widget.transaction?.id ?? 0,
|
|
||||||
type: type,
|
|
||||||
amount: amount,
|
|
||||||
date: selectedDate,
|
|
||||||
description: descriptionController.text.trim(),
|
|
||||||
categoryId: selectedCategory?.id,
|
|
||||||
fromWalletId: fromWallet?.id,
|
|
||||||
toWalletId: toWallet?.id,
|
|
||||||
dueDate: dueDate,
|
|
||||||
alertSchedule: alertSchedule == 'NONE' ? null : alertSchedule,
|
|
||||||
alertTime: alertSchedule == 'NONE' ? null : '${alertTime.hour.toString().padLeft(2, '0')}:${alertTime.minute.toString().padLeft(2, '0')}:00',
|
|
||||||
items: lineItems.isNotEmpty ? lineItems : null,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (widget.transaction == null) {
|
final tx = Transaction(
|
||||||
|
id: 0,
|
||||||
|
type: type,
|
||||||
|
amount: amount,
|
||||||
|
date: selectedDate,
|
||||||
|
description: descriptionController.text.trim(),
|
||||||
|
categoryId: selectedCategory?.id,
|
||||||
|
fromWalletId: fromWallet?.id,
|
||||||
|
toWalletId: toWallet?.id,
|
||||||
|
dueDate: dueDate,
|
||||||
|
alertSchedule: alertSchedule == 'NONE' ? null : alertSchedule,
|
||||||
|
alertTime: alertSchedule == 'NONE' ? null : '${alertTime.hour.toString().padLeft(2, '0')}:${alertTime.minute.toString().padLeft(2, '0')}:00',
|
||||||
|
items: lineItems.isNotEmpty ? lineItems : null,
|
||||||
|
);
|
||||||
await ref.read(transactionProvider.notifier).addTransaction(tx, base64Attachments: base64Attachments);
|
await ref.read(transactionProvider.notifier).addTransaction(tx, base64Attachments: base64Attachments);
|
||||||
} else {
|
} else {
|
||||||
await ref.read(transactionProvider.notifier).updateTransaction(
|
final tx = Transaction(
|
||||||
tx,
|
id: widget.transaction?.id ?? 0,
|
||||||
base64Attachments: base64Attachments,
|
type: type,
|
||||||
deletedAttachmentIds: deletedAttachmentIds,
|
amount: amount,
|
||||||
|
date: selectedDate,
|
||||||
|
description: descriptionController.text.trim(),
|
||||||
|
categoryId: selectedCategory?.id,
|
||||||
|
fromWalletId: fromWallet?.id,
|
||||||
|
toWalletId: toWallet?.id,
|
||||||
|
dueDate: dueDate,
|
||||||
|
alertSchedule: alertSchedule == 'NONE' ? null : alertSchedule,
|
||||||
|
alertTime: alertSchedule == 'NONE' ? null : '${alertTime.hour.toString().padLeft(2, '0')}:${alertTime.minute.toString().padLeft(2, '0')}:00',
|
||||||
|
items: lineItems.isNotEmpty ? lineItems : null,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (alertSchedule != 'NONE' && dueDate != null) {
|
if (widget.transaction == null) {
|
||||||
DateTime alertDate = dueDate!;
|
await ref.read(transactionProvider.notifier).addTransaction(tx, base64Attachments: base64Attachments);
|
||||||
if (alertSchedule == '1_DAY_BEFORE') {
|
} else {
|
||||||
alertDate = dueDate!.subtract(const Duration(days: 1));
|
await ref.read(transactionProvider.notifier).updateTransaction(
|
||||||
} else if (alertSchedule == '2_DAYS_BEFORE') {
|
tx,
|
||||||
alertDate = dueDate!.subtract(const Duration(days: 2));
|
base64Attachments: base64Attachments,
|
||||||
} else if (alertSchedule == '1_WEEK_BEFORE') {
|
deletedAttachmentIds: deletedAttachmentIds,
|
||||||
alertDate = dueDate!.subtract(const Duration(days: 7));
|
|
||||||
}
|
|
||||||
|
|
||||||
final notifyDate = DateTime(alertDate.year, alertDate.month, alertDate.day, alertTime.hour, alertTime.minute);
|
|
||||||
if (notifyDate.isAfter(DateTime.now())) {
|
|
||||||
NotificationService().scheduleNotification(
|
|
||||||
id: DateTime.now().millisecondsSinceEpoch.remainder(100000),
|
|
||||||
title: 'Payment Due!',
|
|
||||||
body: 'Rs. ${amount.toStringAsFixed(0)} is due for ${toWallet?.name}',
|
|
||||||
scheduledDate: notifyDate,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mounted) {
|
if (alertSchedule != 'NONE' && dueDate != null) {
|
||||||
SnackBarService.showSuccess(context, 'Transaction saved successfully');
|
DateTime alertDate = dueDate!;
|
||||||
Navigator.pop(context);
|
if (alertSchedule == '1_DAY_BEFORE') {
|
||||||
|
alertDate = dueDate!.subtract(const Duration(days: 1));
|
||||||
|
} else if (alertSchedule == '2_DAYS_BEFORE') {
|
||||||
|
alertDate = dueDate!.subtract(const Duration(days: 2));
|
||||||
|
} else if (alertSchedule == '1_WEEK_BEFORE') {
|
||||||
|
alertDate = dueDate!.subtract(const Duration(days: 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
final notifyDate = DateTime(alertDate.year, alertDate.month, alertDate.day, alertTime.hour, alertTime.minute);
|
||||||
|
if (notifyDate.isAfter(DateTime.now())) {
|
||||||
|
NotificationService().scheduleNotification(
|
||||||
|
id: DateTime.now().millisecondsSinceEpoch.remainder(100000),
|
||||||
|
title: 'Payment Due!',
|
||||||
|
body: 'Rs. ${amount.toStringAsFixed(0)} is due for ${toWallet?.name}',
|
||||||
|
scheduledDate: notifyDate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
SnackBarService.showSuccess(context, 'Transaction saved successfully');
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error saving transaction: $e');
|
||||||
|
if (mounted) {
|
||||||
|
SnackBarService.showError(context, 'Failed to save transaction: ${e.toString()}');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => isSaving = false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,11 @@ class PaginatedTransactionNotifier extends Notifier<PaginatedTransactionState> {
|
|||||||
final updatedList = state.transactions.where((t) => t.id != id).toList();
|
final updatedList = state.transactions.where((t) => t.id != id).toList();
|
||||||
state = state.copyWith(transactions: updatedList);
|
state = state.copyWith(transactions: updatedList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void updateTransactionInState(Transaction tx) {
|
||||||
|
final updatedList = state.transactions.map((t) => t.id == tx.id ? tx : t).toList();
|
||||||
|
state = state.copyWith(transactions: updatedList);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final paginatedTransactionProvider = NotifierProvider<PaginatedTransactionNotifier, PaginatedTransactionState>(() {
|
final paginatedTransactionProvider = NotifierProvider<PaginatedTransactionNotifier, PaginatedTransactionState>(() {
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ class TransactionNotifier extends AsyncNotifier<List<Transaction>> {
|
|||||||
Future<void> updateTransaction(Transaction transaction, {List<Map<String, String>>? base64Attachments, List<int>? deletedAttachmentIds}) async {
|
Future<void> updateTransaction(Transaction transaction, {List<Map<String, String>>? base64Attachments, List<int>? deletedAttachmentIds}) async {
|
||||||
final updatedTransaction = await ref.read(apiRepositoryProvider).updateTransaction(transaction);
|
final updatedTransaction = await ref.read(apiRepositoryProvider).updateTransaction(transaction);
|
||||||
|
|
||||||
bool hasChanges = false;
|
|
||||||
if (base64Attachments != null && base64Attachments.isNotEmpty) {
|
if (base64Attachments != null && base64Attachments.isNotEmpty) {
|
||||||
for (var attachment in base64Attachments) {
|
for (var attachment in base64Attachments) {
|
||||||
await ref.read(apiRepositoryProvider).addAttachment(
|
await ref.read(apiRepositoryProvider).addAttachment(
|
||||||
@@ -62,28 +61,21 @@ class TransactionNotifier extends AsyncNotifier<List<Transaction>> {
|
|||||||
attachment['base64Content']!
|
attachment['base64Content']!
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
hasChanges = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deletedAttachmentIds != null && deletedAttachmentIds.isNotEmpty) {
|
if (deletedAttachmentIds != null && deletedAttachmentIds.isNotEmpty) {
|
||||||
for (var id in deletedAttachmentIds) {
|
for (var id in deletedAttachmentIds) {
|
||||||
await ref.read(apiRepositoryProvider).deleteAttachment(id);
|
await ref.read(apiRepositoryProvider).deleteAttachment(id);
|
||||||
}
|
}
|
||||||
hasChanges = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasChanges) {
|
ref.invalidate(walletProvider);
|
||||||
ref.invalidateSelf();
|
ref.invalidateSelf();
|
||||||
} else {
|
|
||||||
if (state.value != null) {
|
|
||||||
final updatedList = state.value!.map((t) => t.id == updatedTransaction.id ? updatedTransaction : t).toList();
|
|
||||||
state = AsyncValue.data(updatedList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteTransaction(int id) async {
|
Future<void> deleteTransaction(int id) async {
|
||||||
await ref.read(apiRepositoryProvider).deleteTransaction(id);
|
await ref.read(apiRepositoryProvider).deleteTransaction(id);
|
||||||
|
ref.invalidate(walletProvider);
|
||||||
if (state.value != null) {
|
if (state.value != null) {
|
||||||
state = AsyncValue.data(state.value!.where((t) => t.id != id).toList());
|
state = AsyncValue.data(state.value!.where((t) => t.id != id).toList());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import '../../../core/widgets/premium_text_field.dart';
|
|||||||
import '../../../core/widgets/smart_search_dropdown.dart';
|
import '../../../core/widgets/smart_search_dropdown.dart';
|
||||||
import '../../inventory/providers/products_provider.dart';
|
import '../../inventory/providers/products_provider.dart';
|
||||||
import '../../inventory/providers/product_categories_provider.dart';
|
import '../../inventory/providers/product_categories_provider.dart';
|
||||||
|
import '../../inventory/providers/commodity_rates_provider.dart';
|
||||||
import '../providers/vendors_provider.dart';
|
import '../providers/vendors_provider.dart';
|
||||||
import '../../business/providers/business_provider.dart';
|
import '../../business/providers/business_provider.dart';
|
||||||
import '../../transactions/presentation/widgets/attachment_gallery_screen.dart';
|
import '../../transactions/presentation/widgets/attachment_gallery_screen.dart';
|
||||||
@@ -735,13 +736,34 @@ class _AddItemSheetState extends ConsumerState<_AddItemSheet> {
|
|||||||
if (_selectedProductId == null) return;
|
if (_selectedProductId == null) return;
|
||||||
final qty = int.tryParse(_qtyCtrl.text) ?? 1;
|
final qty = int.tryParse(_qtyCtrl.text) ?? 1;
|
||||||
|
|
||||||
|
final products = ref.read(productsProvider).value ?? [];
|
||||||
|
final categories = ref.read(productCategoriesProvider).value ?? [];
|
||||||
|
final commodityRates = ref.read(commodityRatesProvider).value ?? [];
|
||||||
|
|
||||||
|
final selectedProduct = products.where((p) => p.id == _selectedProductId).firstOrNull;
|
||||||
|
final selectedCat = categories.where((c) => c.id == selectedProduct?.categoryId).firstOrNull;
|
||||||
|
|
||||||
|
double initialRate = 0.0;
|
||||||
|
if (selectedCat?.commodityCode != null) {
|
||||||
|
final matchRate = commodityRates.where(
|
||||||
|
(r) => r.commodityCode.toUpperCase() == selectedCat!.commodityCode!.toUpperCase()
|
||||||
|
).firstOrNull;
|
||||||
|
if (matchRate != null && matchRate.rate > 0) {
|
||||||
|
final purity = selectedCat?.purityFactor ?? (selectedProduct?.purityFactor ?? 1.0);
|
||||||
|
initialRate = matchRate.rate * purity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (initialRate == 0.0) {
|
||||||
|
initialRate = selectedProduct?.sellingPrice ?? 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
final List<PurchaseOrderItem> items = [];
|
final List<PurchaseOrderItem> items = [];
|
||||||
for (int i = 0; i < qty; i++) {
|
for (int i = 0; i < qty; i++) {
|
||||||
items.add(PurchaseOrderItem(
|
items.add(PurchaseOrderItem(
|
||||||
productId: _selectedProductId,
|
productId: _selectedProductId,
|
||||||
quantity: 1.0, // Each row is 1 unit initially, or could represent pieces/weight
|
quantity: 1.0,
|
||||||
unitPrice: 0.0,
|
unitPrice: initialRate,
|
||||||
total: 0.0,
|
total: initialRate > 0 ? initialRate : 0.0,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Navigator.pop(context, items);
|
Navigator.pop(context, items);
|
||||||
|
|||||||
Reference in New Issue
Block a user