1919import lombok .RequiredArgsConstructor ;
2020import org .springframework .stereotype .Service ;
2121import 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 );
0 commit comments