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.Mono;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/kifi-v2/inventory/commodity-rates")
|
||||
@RequiredArgsConstructor
|
||||
public class CommodityRateController {
|
||||
private final CommodityRateService rateService;
|
||||
|
||||
@GetMapping("/{commodityName}")
|
||||
public Flux<CommodityRateHistory> getRateHistory(@PathVariable String commodityName) {
|
||||
return rateService.getRateHistory(commodityName);
|
||||
@GetMapping("/latest")
|
||||
public Flux<CommodityRateHistory> getAllLatestRates() {
|
||||
return rateService.getAllLatestRates();
|
||||
}
|
||||
|
||||
@GetMapping("/{commodityName}/latest")
|
||||
public Mono<ResponseEntity<CommodityRateHistory>> getLatestRate(@PathVariable String commodityName) {
|
||||
return rateService.getLatestRate(commodityName)
|
||||
@GetMapping("/{commodityCode}")
|
||||
public Flux<CommodityRateHistory> getRateHistory(@PathVariable String commodityCode) {
|
||||
return rateService.getRateHistory(commodityCode);
|
||||
}
|
||||
|
||||
@GetMapping("/{commodityCode}/latest")
|
||||
public Mono<ResponseEntity<CommodityRateHistory>> getLatestRate(@PathVariable String commodityCode) {
|
||||
return rateService.getLatestRate(commodityCode)
|
||||
.map(ResponseEntity::ok)
|
||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||
}
|
||||
@@ -32,12 +39,28 @@ public class CommodityRateController {
|
||||
Authentication authentication,
|
||||
@RequestBody CommodityRateHistory rateHistory) {
|
||||
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(
|
||||
rateHistory.getCommodityName(),
|
||||
code,
|
||||
name,
|
||||
rateHistory.getPurity(),
|
||||
rateHistory.getRate(),
|
||||
rateHistory.getSource() != null ? rateHistory.getSource() : "MANUAL",
|
||||
userId
|
||||
).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.setHasChild(category.getHasChild());
|
||||
existing.setCommodityCode(category.getCommodityCode());
|
||||
existing.setPurityFactor(category.getPurityFactor() != null ? category.getPurityFactor() : 1.0);
|
||||
existing.setDefaultHsn(category.getDefaultHsn());
|
||||
existing.setDefaultGst(category.getDefaultGst());
|
||||
existing.setHuidRequired(category.getHuidRequired());
|
||||
|
||||
@@ -18,6 +18,7 @@ import java.time.LocalDateTime;
|
||||
public class CommodityRateHistory {
|
||||
@Id
|
||||
private Long id;
|
||||
private String commodityCode;
|
||||
private String commodityName;
|
||||
private String purity;
|
||||
private BigDecimal rate;
|
||||
|
||||
@@ -24,6 +24,8 @@ public class ProductCategory {
|
||||
@Builder.Default
|
||||
private Boolean hasChild = false;
|
||||
private String commodityCode;
|
||||
@Builder.Default
|
||||
private Double purityFactor = 1.0;
|
||||
private String defaultHsn;
|
||||
private java.math.BigDecimal defaultGst;
|
||||
@Builder.Default
|
||||
|
||||
@@ -4,8 +4,10 @@ import com.kifi.api.entity.TransactionItem;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Repository
|
||||
public interface TransactionItemRepository extends ReactiveCrudRepository<TransactionItem, Long> {
|
||||
Flux<TransactionItem> findByTransactionId(Long transactionId);
|
||||
Mono<Void> deleteByTransactionId(Long transactionId);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package com.kifi.api.repository.inventory;
|
||||
|
||||
import com.kifi.api.entity.inventory.CommodityRateHistory;
|
||||
import org.springframework.data.r2dbc.repository.Query;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface CommodityRateHistoryRepository extends ReactiveCrudRepository<CommodityRateHistory, Long> {
|
||||
Flux<CommodityRateHistory> findByCommodityCodeOrderByEffectiveAtDesc(String commodityCode);
|
||||
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> {
|
||||
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) {
|
||||
return transactionRepository.findById(id)
|
||||
.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 -> {
|
||||
// Revert old balances
|
||||
Mono<Void> revertBalances = updateWalletBalances(t.getToWalletId(), t.getFromWalletId(), t.getAmount());
|
||||
@@ -152,9 +153,26 @@ public class TransactionService {
|
||||
t.setAmount(updatedTransaction.getAmount());
|
||||
t.setDate(updatedTransaction.getDate());
|
||||
t.setDescription(updatedTransaction.getDescription());
|
||||
t.setDueDate(updatedTransaction.getDueDate());
|
||||
t.setAlertSchedule(updatedTransaction.getAlertSchedule());
|
||||
t.setAlertTime(updatedTransaction.getAlertTime());
|
||||
|
||||
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
|
||||
|
||||
@@ -1,38 +1,82 @@
|
||||
package com.kifi.api.service.inventory;
|
||||
|
||||
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.ProductCategoryRepository;
|
||||
import com.kifi.api.repository.inventory.ProductRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CommodityRateService {
|
||||
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()
|
||||
.commodityName(commodityName)
|
||||
.commodityCode(commodityCode)
|
||||
.commodityName(commodityName != null && !commodityName.isEmpty() ? commodityName : commodityCode)
|
||||
.purity(purity)
|
||||
.rate(rate)
|
||||
.effectiveAt(LocalDateTime.now())
|
||||
.source(source)
|
||||
.source(source != null ? source : "MANUAL")
|
||||
.createdBy(userId)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
return rateHistoryRepository.save(history);
|
||||
}
|
||||
|
||||
public Flux<CommodityRateHistory> getRateHistory(String commodityName) {
|
||||
return rateHistoryRepository.findByCommodityNameOrderByEffectiveAtDesc(commodityName);
|
||||
public Flux<CommodityRateHistory> getRateHistory(String commodityCode) {
|
||||
return rateHistoryRepository.findByCommodityCodeOrderByEffectiveAtDesc(commodityCode)
|
||||
.switchIfEmpty(rateHistoryRepository.findByCommodityNameOrderByEffectiveAtDesc(commodityCode));
|
||||
}
|
||||
|
||||
public Mono<CommodityRateHistory> getLatestRate(String commodityName) {
|
||||
return getRateHistory(commodityName).next();
|
||||
public Mono<CommodityRateHistory> getLatestRate(String commodityCode) {
|
||||
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),
|
||||
huid_required BOOLEAN DEFAULT FALSE,
|
||||
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',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
@@ -210,7 +211,8 @@ CREATE TABLE IF NOT EXISTS product_categories (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS commodity_rate_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
commodity_name VARCHAR(100) NOT NULL,
|
||||
commodity_code VARCHAR(100) NOT NULL,
|
||||
commodity_name VARCHAR(100),
|
||||
purity VARCHAR(50),
|
||||
rate DECIMAL(15, 2) NOT NULL,
|
||||
effective_at TIMESTAMP NOT NULL,
|
||||
|
||||
@@ -26,8 +26,9 @@ class DioClient {
|
||||
|
||||
DioClient._internal()
|
||||
: 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.1.5:8080/api/kifi-v2', // Current Local Mac IP (192.168.1.5)
|
||||
connectTimeout: 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 _gstController = TextEditingController();
|
||||
final _makingChargeController = TextEditingController();
|
||||
final _purityFactorController = TextEditingController(text: '1.0');
|
||||
|
||||
int? _selectedParentId;
|
||||
bool _isLeaf = false;
|
||||
bool _isLeaf = true;
|
||||
bool _huidRequired = false;
|
||||
String _commodityCode = 'XAU';
|
||||
String _makingChargeType = 'PER_GRAM';
|
||||
@@ -271,6 +272,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
_hsnController.text = c.defaultHsn ?? '';
|
||||
_gstController.text = c.defaultGst?.toString() ?? '';
|
||||
_makingChargeController.text = c.defaultMakingCharge?.toString() ?? '';
|
||||
_purityFactorController.text = (c.purityFactor ?? 1.0).toString();
|
||||
_huidRequired = c.huidRequired;
|
||||
_commodityCode = c.commodityCode ?? 'XAU';
|
||||
|
||||
@@ -293,6 +295,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
_hsnController.dispose();
|
||||
_gstController.dispose();
|
||||
_makingChargeController.dispose();
|
||||
_purityFactorController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -304,6 +307,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
parentCategoryId: _selectedParentId,
|
||||
hasChild: !_isLeaf,
|
||||
commodityCode: _isLeaf ? _commodityCode : null,
|
||||
purityFactor: _isLeaf ? (double.tryParse(_purityFactorController.text) ?? 1.0) : 1.0,
|
||||
defaultHsn: _isLeaf ? _hsnController.text.trim() : null,
|
||||
defaultGst: _isLeaf ? double.tryParse(_gstController.text) : null,
|
||||
huidRequired: _isLeaf ? _huidRequired : false,
|
||||
@@ -457,17 +461,34 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (isBusinessMode) ...[
|
||||
DropdownButtonFormField<String>(
|
||||
value: _commodityCode,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Commodity Code',
|
||||
prefixIcon: const Icon(LucideIcons.barChart2),
|
||||
),
|
||||
items: _commodities.map((c) => DropdownMenuItem(
|
||||
value: c['code'],
|
||||
child: Text(c['label']!),
|
||||
)).toList(),
|
||||
onChanged: (val) => setState(() => _commodityCode = val!),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _commodityCode,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Commodity Code',
|
||||
prefixIcon: Icon(LucideIcons.barChart2),
|
||||
),
|
||||
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),
|
||||
],
|
||||
|
||||
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/providers/products_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:intl/intl.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
@@ -63,12 +64,26 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
).firstOrNull;
|
||||
|
||||
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(
|
||||
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 bool hasChild;
|
||||
final String? commodityCode;
|
||||
final double? purityFactor;
|
||||
final String? defaultHsn;
|
||||
final double? defaultGst;
|
||||
final bool huidRequired;
|
||||
@@ -26,6 +27,7 @@ class ProductCategory {
|
||||
this.parentCategoryId,
|
||||
this.hasChild = false,
|
||||
this.commodityCode,
|
||||
this.purityFactor = 1.0,
|
||||
this.defaultHsn,
|
||||
this.defaultGst,
|
||||
this.huidRequired = false,
|
||||
@@ -44,16 +46,17 @@ class ProductCategory {
|
||||
name: json['name'],
|
||||
parentCategoryId: json['parentCategoryId'],
|
||||
hasChild: json['hasChild'] ?? false,
|
||||
commodityCode: json['commodityCode'],
|
||||
defaultHsn: json['defaultHsn'],
|
||||
defaultGst: (json['defaultGst'] as num?)?.toDouble(),
|
||||
huidRequired: json['huidRequired'] ?? false,
|
||||
defaultMakingCharge: (json['defaultMakingCharge'] as num?)?.toDouble(),
|
||||
makingChargeType: json['makingChargeType'],
|
||||
baseUnit: json['baseUnit'] ?? 'pcs',
|
||||
isActive: json['isActive'] ?? true,
|
||||
sortOrder: json['sortOrder'] ?? 0,
|
||||
dailyRate: (json['dailyRate'] as num?)?.toDouble(),
|
||||
commodityCode: json['commodityCode'] ?? json['commodity_code'],
|
||||
purityFactor: (json['purityFactor'] ?? json['purity_factor'] as num?)?.toDouble() ?? 1.0,
|
||||
defaultHsn: json['defaultHsn'] ?? json['default_hsn'],
|
||||
defaultGst: (json['defaultGst'] ?? json['default_gst'] as num?)?.toDouble(),
|
||||
huidRequired: json['huidRequired'] ?? json['huid_required'] ?? false,
|
||||
defaultMakingCharge: (json['defaultMakingCharge'] ?? json['default_making_charge'] as num?)?.toDouble(),
|
||||
makingChargeType: json['makingChargeType'] ?? json['making_charge_type'],
|
||||
baseUnit: json['baseUnit'] ?? json['base_unit'] ?? 'pcs',
|
||||
isActive: json['isActive'] ?? json['is_active'] ?? true,
|
||||
sortOrder: json['sortOrder'] ?? json['sort_order'] ?? 0,
|
||||
dailyRate: (json['dailyRate'] ?? json['daily_rate'] as num?)?.toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,6 +68,7 @@ class ProductCategory {
|
||||
'parentCategoryId': parentCategoryId,
|
||||
'hasChild': hasChild,
|
||||
'commodityCode': commodityCode,
|
||||
'purityFactor': purityFactor ?? 1.0,
|
||||
'defaultHsn': defaultHsn,
|
||||
'defaultGst': defaultGst,
|
||||
'huidRequired': huidRequired,
|
||||
|
||||
@@ -570,117 +570,135 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
||||
final amount = double.tryParse(amountText);
|
||||
if (amount == null) return;
|
||||
|
||||
if (fromWallet != null && fromWallet!.balance < amount) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Insufficient balance in From Account')));
|
||||
return;
|
||||
final isFromPayableOrLiability = fromWallet != null && (fromWallet!.nature == 'PAYABLES' || fromWallet!.nature == 'LOAN');
|
||||
if (fromWallet != null && !isFromPayableOrLiability) {
|
||||
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);
|
||||
|
||||
// Determine internal type based on natures
|
||||
String type = 'TRANSFER';
|
||||
if (fromWallet == null) {
|
||||
type = 'INCOME';
|
||||
} else if (toWallet == null) { // Though toWallet is mandatory, just in case
|
||||
type = 'EXPENSE';
|
||||
} else if (fromWallet?.nature == 'INCOME') {
|
||||
type = 'INCOME';
|
||||
} else if (toWallet?.nature == 'PAYABLES' || toWallet?.nature == 'EXPENSE') {
|
||||
type = 'EXPENSE';
|
||||
} 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);
|
||||
try {
|
||||
// Determine internal type based on natures
|
||||
String type = 'TRANSFER';
|
||||
if (fromWallet == null) {
|
||||
type = 'INCOME';
|
||||
} else if (toWallet == null) { // Though toWallet is mandatory, just in case
|
||||
type = 'EXPENSE';
|
||||
} else if (fromWallet?.nature == 'INCOME') {
|
||||
type = 'INCOME';
|
||||
} else if (toWallet?.nature == 'PAYABLES' || toWallet?.nature == 'EXPENSE') {
|
||||
type = 'EXPENSE';
|
||||
} else if (toWallet?.nature == 'INVESTMENTS') {
|
||||
type = 'INVESTMENT';
|
||||
}
|
||||
|
||||
final rt = RecurringTransaction(
|
||||
id: 0,
|
||||
type: type,
|
||||
amount: amount,
|
||||
categoryId: selectedCategory?.id,
|
||||
fromWalletId: fromWallet?.id,
|
||||
toWalletId: toWallet?.id,
|
||||
frequency: recurringFrequency,
|
||||
description: descriptionController.text.trim(),
|
||||
nextExecutionDate: nextDate,
|
||||
status: 'ACTIVE',
|
||||
);
|
||||
await ref.read(recurringTransactionProvider.notifier).addRecurringTransaction(rt);
|
||||
|
||||
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);
|
||||
} 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 (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);
|
||||
}
|
||||
|
||||
if (widget.transaction == null) {
|
||||
final rt = RecurringTransaction(
|
||||
id: 0,
|
||||
type: type,
|
||||
amount: amount,
|
||||
categoryId: selectedCategory?.id,
|
||||
fromWalletId: fromWallet?.id,
|
||||
toWalletId: toWallet?.id,
|
||||
frequency: recurringFrequency,
|
||||
description: descriptionController.text.trim(),
|
||||
nextExecutionDate: nextDate,
|
||||
status: 'ACTIVE',
|
||||
);
|
||||
await ref.read(recurringTransactionProvider.notifier).addRecurringTransaction(rt);
|
||||
|
||||
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);
|
||||
} else {
|
||||
await ref.read(transactionProvider.notifier).updateTransaction(
|
||||
tx,
|
||||
base64Attachments: base64Attachments,
|
||||
deletedAttachmentIds: deletedAttachmentIds,
|
||||
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 (alertSchedule != 'NONE' && dueDate != null) {
|
||||
DateTime alertDate = dueDate!;
|
||||
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 (widget.transaction == null) {
|
||||
await ref.read(transactionProvider.notifier).addTransaction(tx, base64Attachments: base64Attachments);
|
||||
} else {
|
||||
await ref.read(transactionProvider.notifier).updateTransaction(
|
||||
tx,
|
||||
base64Attachments: base64Attachments,
|
||||
deletedAttachmentIds: deletedAttachmentIds,
|
||||
);
|
||||
}
|
||||
|
||||
if (alertSchedule != 'NONE' && dueDate != null) {
|
||||
DateTime alertDate = dueDate!;
|
||||
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);
|
||||
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();
|
||||
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>(() {
|
||||
|
||||
@@ -52,7 +52,6 @@ class TransactionNotifier extends AsyncNotifier<List<Transaction>> {
|
||||
Future<void> updateTransaction(Transaction transaction, {List<Map<String, String>>? base64Attachments, List<int>? deletedAttachmentIds}) async {
|
||||
final updatedTransaction = await ref.read(apiRepositoryProvider).updateTransaction(transaction);
|
||||
|
||||
bool hasChanges = false;
|
||||
if (base64Attachments != null && base64Attachments.isNotEmpty) {
|
||||
for (var attachment in base64Attachments) {
|
||||
await ref.read(apiRepositoryProvider).addAttachment(
|
||||
@@ -62,28 +61,21 @@ class TransactionNotifier extends AsyncNotifier<List<Transaction>> {
|
||||
attachment['base64Content']!
|
||||
);
|
||||
}
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (deletedAttachmentIds != null && deletedAttachmentIds.isNotEmpty) {
|
||||
for (var id in deletedAttachmentIds) {
|
||||
await ref.read(apiRepositoryProvider).deleteAttachment(id);
|
||||
}
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
ref.invalidateSelf();
|
||||
} else {
|
||||
if (state.value != null) {
|
||||
final updatedList = state.value!.map((t) => t.id == updatedTransaction.id ? updatedTransaction : t).toList();
|
||||
state = AsyncValue.data(updatedList);
|
||||
}
|
||||
}
|
||||
ref.invalidate(walletProvider);
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
Future<void> deleteTransaction(int id) async {
|
||||
await ref.read(apiRepositoryProvider).deleteTransaction(id);
|
||||
ref.invalidate(walletProvider);
|
||||
if (state.value != null) {
|
||||
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 '../../inventory/providers/products_provider.dart';
|
||||
import '../../inventory/providers/product_categories_provider.dart';
|
||||
import '../../inventory/providers/commodity_rates_provider.dart';
|
||||
import '../providers/vendors_provider.dart';
|
||||
import '../../business/providers/business_provider.dart';
|
||||
import '../../transactions/presentation/widgets/attachment_gallery_screen.dart';
|
||||
@@ -735,13 +736,34 @@ class _AddItemSheetState extends ConsumerState<_AddItemSheet> {
|
||||
if (_selectedProductId == null) return;
|
||||
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 = [];
|
||||
for (int i = 0; i < qty; i++) {
|
||||
items.add(PurchaseOrderItem(
|
||||
productId: _selectedProductId,
|
||||
quantity: 1.0, // Each row is 1 unit initially, or could represent pieces/weight
|
||||
unitPrice: 0.0,
|
||||
total: 0.0,
|
||||
quantity: 1.0,
|
||||
unitPrice: initialRate,
|
||||
total: initialRate > 0 ? initialRate : 0.0,
|
||||
));
|
||||
}
|
||||
Navigator.pop(context, items);
|
||||
|
||||
Reference in New Issue
Block a user