Fixed upto purchases and rate sync process

This commit is contained in:
2026-08-28 19:45:19 +05:30
parent 189f49ed07
commit eacca6aaea
21 changed files with 1203 additions and 401 deletions

View File

@@ -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"
)));
}
} }

View File

@@ -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());

View File

@@ -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;

View File

@@ -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

View File

@@ -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);
} }

View File

@@ -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();
} }

View File

@@ -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);
} }

View File

@@ -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

View File

@@ -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);
} }
} }

View File

@@ -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,

View File

@@ -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),
)), )),

View 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,
};
}
}

View File

@@ -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,11 +461,15 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
const SizedBox(height: 16), const SizedBox(height: 16),
if (isBusinessMode) ...[ if (isBusinessMode) ...[
DropdownButtonFormField<String>( Row(
children: [
Expanded(
flex: 3,
child: DropdownButtonFormField<String>(
value: _commodityCode, value: _commodityCode,
decoration: InputDecoration( decoration: const InputDecoration(
labelText: 'Commodity Code', labelText: 'Commodity Code',
prefixIcon: const Icon(LucideIcons.barChart2), prefixIcon: Icon(LucideIcons.barChart2),
), ),
items: _commodities.map((c) => DropdownMenuItem( items: _commodities.map((c) => DropdownMenuItem(
value: c['code'], value: c['code'],
@@ -469,6 +477,19 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
)).toList(), )).toList(),
onChanged: (val) => setState(() => _commodityCode = val!), 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),
], ],

View File

@@ -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,

View File

@@ -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();
});

View File

@@ -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,

View File

@@ -570,13 +570,21 @@ 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');
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'))); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Insufficient balance in From Account')));
return; return;
} }
}
setState(() => isSaving = true); setState(() => isSaving = true);
try {
// Determine internal type based on natures // Determine internal type based on natures
String type = 'TRANSFER'; String type = 'TRANSFER';
if (fromWallet == null) { if (fromWallet == null) {
@@ -682,6 +690,16 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
SnackBarService.showSuccess(context, 'Transaction saved successfully'); SnackBarService.showSuccess(context, 'Transaction saved successfully');
Navigator.pop(context); 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);
}
}
} }
@override @override

View File

@@ -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>(() {

View File

@@ -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());
} }

View File

@@ -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);