Skip to content

Commit 24ffc31

Browse files
authored
Merge pull request #27 from touchmegit1/dev
Dev
2 parents 45631ae + e1258c8 commit 24ffc31

File tree

13 files changed

+407
-235
lines changed

13 files changed

+407
-235
lines changed

backend/src/main/java/com/smalltrend/controller/CRM/TicketController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public ResponseEntity<TicketResponse> updateTicket(
6262
}
6363

6464
@DeleteMapping("/tickets/{id}")
65-
@PreAuthorize("hasAnyRole('ADMIN', 'CASHIER')")
65+
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER', 'CASHIER')")
6666
public ResponseEntity<Void> deleteTicket(@PathVariable("id") Long id) {
6767
ticketService.deleteTicket(id);
6868
return ResponseEntity.noContent().build();

backend/src/main/java/com/smalltrend/service/inventory/purchase/PurchaseOrderService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.smalltrend.entity.enums.PurchaseOrderStatus;
88
import com.smalltrend.repository.*;
99
import com.smalltrend.service.inventory.shared.InventoryManagerNotificationService;
10+
import com.smalltrend.service.inventory.shared.InventoryStockService;
1011
import com.smalltrend.service.products.VariantPriceService;
1112

1213
import lombok.RequiredArgsConstructor;
@@ -47,6 +48,7 @@ public class PurchaseOrderService {
4748
private final UnitConversionRepository unitConversionRepository;
4849
private final VariantPriceService variantPriceService;
4950
private final InventoryManagerNotificationService inventoryManagerNotificationService;
51+
private final InventoryStockService inventoryStockService;
5052

5153
// ═══════════════════════════════════════════════════════════
5254
// Public API
@@ -963,6 +965,7 @@ private void updateStock(PurchaseOrder order, List<PurchaseOrderItemRequest> ite
963965
.quantity(finalQty)
964966
.build();
965967
inventoryStockRepository.save(stock);
968+
inventoryStockService.syncConvertedStocksFromBase(baseVariant, targetLocation, batch);
966969

967970
StockMovement movement = StockMovement.builder()
968971
.variant(baseVariant)

backend/src/main/java/com/smalltrend/service/inventory/shared/InventoryStockService.java

Lines changed: 112 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
import lombok.RequiredArgsConstructor;
2020
import org.springframework.stereotype.Service;
2121
import org.springframework.transaction.annotation.Transactional;
22+
import java.time.LocalDate;
23+
import java.util.Collections;
24+
import java.util.List;
25+
import java.util.Map;
2226

2327
@Service
2428
@RequiredArgsConstructor
@@ -95,29 +99,124 @@ public void importStock(StockImportRequest request) {
9599
InventoryStock savedStock = inventoryStockRepository.save(stock);
96100
outOfStockNotificationService.handleStockTransition(savedStock, oldQty, savedStock.getQuantity(), "IMPORT_STOCK");
97101

102+
syncConvertedStocksFromBase(baseVariant, location, batch);
103+
98104
// Ghi lại lịch sử
99105
recordMovement(baseVariant, batch, location, StockTransactionType.IMPORT, actualQuantity, "IMPORT", null,
100106
request.getNotes());
101107
}
102108

109+
public void syncConvertedStocksFromBase(ProductVariant baseVariant, Location location, ProductBatch baseBatch) {
110+
if (baseVariant == null || baseVariant.getId() == null || !baseVariant.isBaseUnit() || location == null
111+
|| location.getId() == null) {
112+
return;
113+
}
114+
115+
List<UnitConversion> conversions = unitConversionRepository.findByVariantId(baseVariant.getId());
116+
if (conversions == null || conversions.isEmpty()) {
117+
return;
118+
}
119+
120+
int baseStockAtLocation = inventoryStockRepository.findByVariantId(baseVariant.getId())
121+
.stream()
122+
.filter(stock -> stock.getLocation() != null
123+
&& stock.getLocation().getId() != null
124+
&& stock.getLocation().getId().equals(location.getId()))
125+
.mapToInt(stock -> stock.getQuantity() != null ? stock.getQuantity() : 0)
126+
.sum();
127+
128+
for (UnitConversion conversion : conversions) {
129+
if (conversion == null
130+
|| conversion.getToUnit() == null
131+
|| conversion.getToUnit().getId() == null
132+
|| conversion.getConversionFactor() == null
133+
|| conversion.getConversionFactor().intValue() <= 0) {
134+
continue;
135+
}
136+
137+
ProductVariant convertedVariant = findConvertedVariant(baseVariant, conversion.getToUnit().getId());
138+
if (convertedVariant == null || convertedVariant.getId() == null) {
139+
continue;
140+
}
141+
142+
int convertedQty = baseStockAtLocation / conversion.getConversionFactor().intValue();
143+
ProductBatch convertedBatch = resolveConvertedBatch(convertedVariant, baseBatch);
144+
145+
InventoryStock convertedStock = inventoryStockRepository
146+
.findByVariantIdAndBatchIdAndLocationId(convertedVariant.getId(), convertedBatch.getId(), location.getId())
147+
.orElseGet(() -> InventoryStock.builder()
148+
.variant(convertedVariant)
149+
.batch(convertedBatch)
150+
.location(location)
151+
.quantity(0)
152+
.build());
153+
154+
int oldQty = convertedStock.getQuantity() != null ? convertedStock.getQuantity() : 0;
155+
convertedStock.setQuantity(convertedQty);
156+
InventoryStock savedConvertedStock = inventoryStockRepository.save(convertedStock);
157+
outOfStockNotificationService.handleStockTransition(
158+
savedConvertedStock,
159+
oldQty,
160+
savedConvertedStock.getQuantity(),
161+
"CONVERSION_SYNC"
162+
);
163+
}
164+
}
165+
166+
private ProductVariant findConvertedVariant(ProductVariant baseVariant, Integer toUnitId) {
167+
if (baseVariant == null || baseVariant.getProduct() == null || baseVariant.getProduct().getId() == null || toUnitId == null) {
168+
return null;
169+
}
170+
171+
return productVariantRepository.findByProductIdAndUnitId(baseVariant.getProduct().getId(), toUnitId)
172+
.stream()
173+
.filter(candidate -> candidate != null
174+
&& candidate.getId() != null
175+
&& !candidate.getId().equals(baseVariant.getId())
176+
&& hasSameAttributes(baseVariant, candidate))
177+
.findFirst()
178+
.orElse(null);
179+
}
180+
181+
private ProductBatch resolveConvertedBatch(ProductVariant convertedVariant, ProductBatch baseBatch) {
182+
List<ProductBatch> existingBatches = productBatchRepository.findByVariantId(convertedVariant.getId());
183+
184+
if (baseBatch != null && baseBatch.getBatchNumber() != null) {
185+
ProductBatch matchedBatch = existingBatches.stream()
186+
.filter(batch -> batch != null && baseBatch.getBatchNumber().equals(batch.getBatchNumber()))
187+
.findFirst()
188+
.orElse(null);
189+
if (matchedBatch != null) {
190+
return matchedBatch;
191+
}
192+
}
193+
194+
if (existingBatches != null && !existingBatches.isEmpty()) {
195+
return existingBatches.get(0);
196+
}
197+
198+
return productBatchRepository.save(ProductBatch.builder()
199+
.variant(convertedVariant)
200+
.batchNumber(baseBatch != null ? baseBatch.getBatchNumber() : null)
201+
.mfgDate(baseBatch != null && baseBatch.getMfgDate() != null ? baseBatch.getMfgDate() : LocalDate.now())
202+
.expiryDate(baseBatch != null && baseBatch.getExpiryDate() != null ? baseBatch.getExpiryDate() : LocalDate.now().plusYears(1))
203+
.costPrice(baseBatch != null ? baseBatch.getCostPrice() : null)
204+
.build());
205+
}
206+
207+
private boolean hasSameAttributes(ProductVariant left, ProductVariant right) {
208+
Map<String, String> leftAttrs = left != null && left.getAttributes() != null ? left.getAttributes() : Collections.emptyMap();
209+
Map<String, String> rightAttrs = right != null && right.getAttributes() != null ? right.getAttributes() : Collections.emptyMap();
210+
return leftAttrs.equals(rightAttrs);
211+
}
212+
103213
/**
104214
* Trừ tồn kho (VD: Khi hoàn thành hóa đơn SaleOrder)
105215
* Tự động quy đổi qua base unit
106216
*/
107217
@Transactional
108218
// Trừ stock.
109219
public void deductStock(ProductVariant variant, int quantity, Long orderId, String notes) {
110-
if (!variant.isBaseUnit()) {
111-
var directVariantStocks = inventoryStockRepository.findByVariantId(variant.getId());
112-
boolean hasDirectVariantStock = directVariantStocks.stream()
113-
.anyMatch(stock -> stock.getQuantity() != null && stock.getQuantity() > 0);
114-
115-
if (hasDirectVariantStock) {
116-
deductFromStocks(variant, quantity, directVariantStocks, orderId, notes);
117-
return;
118-
}
119-
}
120-
121220
ProductVariant baseVariant = variant;
122221
int deductQuantity = quantity;
123222

@@ -156,6 +255,7 @@ private void deductFromStocks(ProductVariant stockVariant, int quantityToDeduct,
156255
stock.setQuantity(currentQty - qtyToTake);
157256
InventoryStock savedStock = inventoryStockRepository.save(stock);
158257
outOfStockNotificationService.handleStockTransition(savedStock, currentQty, savedStock.getQuantity(), "SALE_ORDER");
258+
syncConvertedStocksFromBase(stockVariant, stock.getLocation(), stock.getBatch());
159259

160260
recordMovement(stockVariant, stock.getBatch(), stock.getLocation(),
161261
StockTransactionType.SALE, -qtyToTake, "SALE_ORDER", orderId, notes);
@@ -179,21 +279,6 @@ public void restockFromRefund(ProductVariant variant, int quantity, Long referen
179279
throw new RuntimeException("Refund quantity must be greater than 0");
180280
}
181281

182-
if (!variant.isBaseUnit()) {
183-
var directVariantStocks = inventoryStockRepository.findByVariantId(variant.getId());
184-
if (directVariantStocks != null && !directVariantStocks.isEmpty()) {
185-
InventoryStock stock = directVariantStocks.get(0);
186-
int oldQty = stock.getQuantity() != null ? stock.getQuantity() : 0;
187-
stock.setQuantity(oldQty + quantity);
188-
InventoryStock savedStock = inventoryStockRepository.save(stock);
189-
outOfStockNotificationService.handleStockTransition(savedStock, oldQty, savedStock.getQuantity(), "REFUND");
190-
191-
recordMovement(variant, stock.getBatch(), stock.getLocation(),
192-
StockTransactionType.ADJUSTMENT, quantity, "REFUND", referenceId, notes);
193-
return;
194-
}
195-
}
196-
197282
ProductVariant baseVariant = variant;
198283
int restockQuantity = quantity;
199284

@@ -220,6 +305,7 @@ public void restockFromRefund(ProductVariant variant, int quantity, Long referen
220305
stock.setQuantity(oldQty + restockQuantity);
221306
InventoryStock savedStock = inventoryStockRepository.save(stock);
222307
outOfStockNotificationService.handleStockTransition(savedStock, oldQty, savedStock.getQuantity(), "REFUND");
308+
syncConvertedStocksFromBase(baseVariant, stock.getLocation(), stock.getBatch());
223309

224310
recordMovement(baseVariant, stock.getBatch(), stock.getLocation(),
225311
StockTransactionType.ADJUSTMENT, restockQuantity, "REFUND", referenceId, notes);

backend/src/main/java/com/smalltrend/service/products/ProductVariantService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,8 @@ private ProductVariantRespone mapToResponse(ProductVariant variant) {
490490
.mapToInt(stock -> stock.getQuantity() != null ? stock.getQuantity() : 0)
491491
.sum();
492492

493-
if (stockQty == 0 && product != null && product.getId() != null && variant.getUnit() != null && variant.getUnit().getId() != null) {
493+
if (isConversionDerivedVariant && product != null && product.getId() != null
494+
&& variant.getUnit() != null && variant.getUnit().getId() != null) {
494495
List<UnitConversion> conversions = unitConversionRepository.findByProductIdAndToUnitId(
495496
product.getId(),
496497
variant.getUnit().getId());

backend/src/main/java/com/smalltrend/service/products/UnitConversionService.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,7 @@ public UnitConversionResponse addConversion(Integer variantId, UnitConversionReq
9090

9191
BigDecimal resolvedSellPrice = request.getSellPrice() != null
9292
? request.getSellPrice()
93-
: baseVariant.getSellPrice();
94-
if (resolvedSellPrice == null) {
95-
throw new RuntimeException("Giá bán quy đổi không được để trống.");
96-
}
93+
: BigDecimal.ZERO;
9794

9895
// ─── 1. Tạo quy đổi đơn vị ────────────────────────────────────────────
9996
UnitConversion conversion = UnitConversion.builder()

0 commit comments

Comments
 (0)