feat(inventory): consolidate inventory lines

Group inventory lines by location, product, unit and tracking Number
Optionally include internal tracking number
Create consolidated line and archive originals
Preserve counting data (value, user, date)
This commit is contained in:
Kheireddine Mehdi
2026-01-07 11:44:14 +01:00
parent 2d681f27f5
commit 5181f5c258
3 changed files with 402 additions and 1 deletions

View File

@ -27,6 +27,7 @@ import com.axelor.apps.stock.db.TrackingNumber;
import com.axelor.apps.stock.db.repo.InventoryLineRepository;
import com.axelor.apps.stock.db.repo.TrackingNumberRepository;
import com.axelor.auth.AuthUtils;
import com.axelor.auth.db.User;
import com.axelor.db.Query;
import com.axelor.inject.Beans;
import com.google.inject.Inject;
@ -34,12 +35,17 @@ import com.google.inject.persist.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class InventoryLineService {
@Inject private ProductRepository productRepository;
private TrackingNumberRepository trackingNumberRepository;
private InventoryLineRepository inventoryLineRepository;
@Inject private InventoryLineRepository inventoryLineRepository;
public InventoryLine createInventoryLine(
Inventory inventory,
@ -171,4 +177,370 @@ public class InventoryLineService {
Beans.get(InventoryLineRepository.class).save(line);
}
@Transactional
public void consolidateInventoryLines(StockLocation stockLocation, Boolean addInternalTrackingNumber) {
List<InventoryLine> inventoryLines =
inventoryLineRepository
.all()
.filter(
"self.stockLocation = ?1 " +
//"AND (self.isConsolidate = false OR self.isConsolidate is null) " +
"AND (self.archived = false OR self.archived is null)",
stockLocation
)
.fetch();
System.out.println("************* lines" + inventoryLines);
Map<String, List<InventoryLine>> groupedLines =
inventoryLines.stream()
.filter(line -> line.getProductName() != null)
.filter(line ->
!addInternalTrackingNumber ||
line.getInternalTrackingNumberStr() == null
)
.collect(Collectors.groupingBy(line -> {
String key =
line.getStockLocation().getId() + "|" +
(line.getProduct() != null
? line.getProduct().getId()
: line.getProductName().trim().toUpperCase()) + "|" +
(line.getUnit() != null ? line.getUnit().getId() : "NULL") + "|" +
(line.getTrackingNumber() != null
? line.getTrackingNumber().getId()
: "NULL") + "|" +
(line.getTrackingNumberStr() != null
? line.getTrackingNumberStr().trim().toUpperCase()
: "NULL");
if (addInternalTrackingNumber) {
key += "|" +
(line.getInternalTrackingNumberStr() != null
? line.getInternalTrackingNumberStr().trim().toUpperCase()
: "NULL");
}
return key;
}));
List<InventoryLine> consolidatedInventoryLines = new ArrayList<>();
System.out.println("************* lines" + groupedLines);
for (List<InventoryLine> lines : groupedLines.values()) {
if (lines.size() <= 1) {
continue;
}
InventoryLine consolidated1 = lines.stream()
.filter(InventoryLine::getIsConsolidate)
.findFirst()
.orElse(null);
boolean hasConsolidatedLine = consolidated1 != null;
if(!hasConsolidatedLine){
InventoryLine first = lines.get(0);
BigDecimal firstCounting = BigDecimal.ZERO;
User firstCountingByUser = null;
LocalDateTime firstCountingDate = null;
BigDecimal secondCounting = BigDecimal.ZERO;
User secondCountingByUser = null;
LocalDateTime secondCountingDate = null;
BigDecimal thirdCounting = BigDecimal.ZERO;
User thirdCountingByUser = null;
LocalDateTime thirdCountingDate = null;
BigDecimal forthCounting = BigDecimal.ZERO;
User forthCountingByUser = null;
LocalDateTime forthCountingDate = null;
BigDecimal controlCounting = BigDecimal.ZERO;
User controlCountingByUser = null;
LocalDateTime controlCountingDate = null;
BigDecimal fifthCounting = BigDecimal.ZERO;
User fifthCountingByUser = null;
LocalDateTime fifthCountingDate = null;
BigDecimal sixthCounting = BigDecimal.ZERO;
User sixthCountingByUser = null;
LocalDateTime sixthCountingDate = null;
for (InventoryLine line : lines) {
if (line.getFirstCounting() != null) {
firstCounting = firstCounting.add(line.getFirstCounting());
if (firstCountingByUser == null) {
firstCountingByUser = line.getFirstCountingByUser();
firstCountingDate = line.getFirstCountingDate();
}
}
if (line.getSecondCounting() != null) {
secondCounting = secondCounting.add(line.getSecondCounting());
if (secondCountingByUser == null) {
secondCountingByUser = line.getSecondCountingByUser();
secondCountingDate = line.getSecondCountingDate();
}
}
if (line.getThirdCounting() != null) {
thirdCounting = thirdCounting.add(line.getThirdCounting());
if (thirdCountingByUser == null) {
thirdCountingByUser = line.getThirdCountingByUser();
thirdCountingDate = line.getThirdCountingDate();
}
}
if (line.getForthCounting() != null) {
forthCounting = forthCounting.add(line.getForthCounting());
if (forthCountingByUser == null) {
forthCountingByUser = line.getForthCountingByUser();
forthCountingDate = line.getForthCountingDate();
}
}
if (line.getControlCounting() != null) {
controlCounting = controlCounting.add(line.getControlCounting());
if (controlCountingByUser == null) {
controlCountingByUser = line.getControlCountingByUser();
controlCountingDate = line.getControlCountingDate();
}
}
if (line.getFifthCounting() != null) {
fifthCounting = fifthCounting.add(line.getFifthCounting());
if (fifthCountingByUser == null) {
fifthCountingByUser = line.getFifthCountingByUser();
fifthCountingDate = line.getFifthCountingDate();
}
}
if (line.getSixthCounting() != null) {
sixthCounting = sixthCounting.add(line.getSixthCounting());
if (sixthCountingByUser == null) {
sixthCountingByUser = line.getSixthCountingByUser();
sixthCountingDate = line.getSixthCountingDate();
}
}
}
InventoryLine consolidated = new InventoryLine();
consolidated.setStockLocation(first.getStockLocation());
if(first.getProduct() != null)
consolidated.setProduct(first.getProduct());
consolidated.setProductName(first.getProductName().trim().toUpperCase());
if(first.getUnit() != null)
consolidated.setUnit(first.getUnit());
if (first.getTrackingNumber() != null) {
consolidated.setTrackingNumber(first.getTrackingNumber());
}
if (first.getTrackingNumberStr() != null) {
consolidated.setTrackingNumberStr(first.getTrackingNumberStr().trim().toUpperCase());
}
if (addInternalTrackingNumber && first.getInternalTrackingNumberStr() != null)
consolidated.setInternalTrackingNumberStr(first.getInternalTrackingNumberStr().trim().toUpperCase());
consolidated.setFirstCounting(firstCounting);
consolidated.setFirstCountingByUser(firstCountingByUser);
consolidated.setFirstCountingDate(firstCountingDate);
consolidated.setSecondCounting(secondCounting);
consolidated.setSecondCountingByUser(secondCountingByUser);
consolidated.setSecondCountingDate(secondCountingDate);
consolidated.setThirdCounting(thirdCounting);
consolidated.setThirdCountingByUser(thirdCountingByUser);
consolidated.setThirdCountingDate(thirdCountingDate);
consolidated.setForthCounting(forthCounting);
consolidated.setForthCountingByUser(forthCountingByUser);
consolidated.setForthCountingDate(forthCountingDate);
consolidated.setControlCounting(controlCounting);
consolidated.setControlCountingByUser(controlCountingByUser);
consolidated.setControlCountingDate(controlCountingDate);
consolidated.setFifthCounting(fifthCounting);
consolidated.setFifthCountingByUser(fifthCountingByUser);
consolidated.setFifthCountingDate(fifthCountingDate);
consolidated.setSixthCounting(sixthCounting);
consolidated.setSixthCountingByUser(sixthCountingByUser);
consolidated.setSixthCountingDate(sixthCountingDate);
consolidated.setIsConsolidate(true);
inventoryLineRepository.save(consolidated);
// Archive old lines
for (InventoryLine line : lines) {
line.setArchived(true);
inventoryLineRepository.save(line);
}
} else {
InventoryLine consolidated = consolidated1;
BigDecimal firstCounting =
consolidated.getFirstCounting() != null
? consolidated.getFirstCounting()
: BigDecimal.ZERO;
BigDecimal secondCounting =
consolidated.getSecondCounting() != null
? consolidated.getSecondCounting()
: BigDecimal.ZERO;
BigDecimal thirdCounting =
consolidated.getThirdCounting() != null
? consolidated.getThirdCounting()
: BigDecimal.ZERO;
BigDecimal forthCounting =
consolidated.getForthCounting() != null
? consolidated.getForthCounting()
: BigDecimal.ZERO;
BigDecimal controlCounting =
consolidated.getControlCounting() != null
? consolidated.getControlCounting()
: BigDecimal.ZERO;
BigDecimal fifthCounting =
consolidated.getFifthCounting() != null
? consolidated.getFifthCounting()
: BigDecimal.ZERO;
BigDecimal sixthCounting =
consolidated.getSixthCounting() != null
? consolidated.getSixthCounting()
: BigDecimal.ZERO;
User firstCountingByUser = consolidated.getFirstCountingByUser();
LocalDateTime firstCountingDate = consolidated.getFirstCountingDate();
User secondCountingByUser = consolidated.getSecondCountingByUser();
LocalDateTime secondCountingDate = consolidated.getSecondCountingDate();
User thirdCountingByUser = consolidated.getThirdCountingByUser();
LocalDateTime thirdCountingDate = consolidated.getThirdCountingDate();
User forthCountingByUser = consolidated.getForthCountingByUser();
LocalDateTime forthCountingDate = consolidated.getForthCountingDate();
User controlCountingByUser = consolidated.getControlCountingByUser();
LocalDateTime controlCountingDate = consolidated.getControlCountingDate();
User fifthCountingByUser = consolidated.getFifthCountingByUser();
LocalDateTime fifthCountingDate = consolidated.getFifthCountingDate();
User sixthCountingByUser = consolidated.getSixthCountingByUser();
LocalDateTime sixthCountingDate = consolidated.getSixthCountingDate();
for (InventoryLine line : lines) {
if (line.getIsConsolidate()) {
continue; // skip the consolidated line itself
}
if (line.getFirstCounting() != null) {
firstCounting = firstCounting.add(line.getFirstCounting());
if (firstCountingByUser == null) {
firstCountingByUser = line.getFirstCountingByUser();
firstCountingDate = line.getFirstCountingDate();
}
}
if (line.getSecondCounting() != null) {
secondCounting = secondCounting.add(line.getSecondCounting());
if (secondCountingByUser == null) {
secondCountingByUser = line.getSecondCountingByUser();
secondCountingDate = line.getSecondCountingDate();
}
}
if (line.getThirdCounting() != null) {
thirdCounting = thirdCounting.add(line.getThirdCounting());
if (thirdCountingByUser == null) {
thirdCountingByUser = line.getThirdCountingByUser();
thirdCountingDate = line.getThirdCountingDate();
}
}
if (line.getForthCounting() != null) {
forthCounting = forthCounting.add(line.getForthCounting());
if (forthCountingByUser == null) {
forthCountingByUser = line.getForthCountingByUser();
forthCountingDate = line.getForthCountingDate();
}
}
if (line.getControlCounting() != null) {
controlCounting = controlCounting.add(line.getControlCounting());
if (controlCountingByUser == null) {
controlCountingByUser = line.getControlCountingByUser();
controlCountingDate = line.getControlCountingDate();
}
}
if (line.getFifthCounting() != null) {
fifthCounting = fifthCounting.add(line.getFifthCounting());
if (fifthCountingByUser == null) {
fifthCountingByUser = line.getFifthCountingByUser();
fifthCountingDate = line.getFifthCountingDate();
}
}
if (line.getSixthCounting() != null) {
sixthCounting = sixthCounting.add(line.getSixthCounting());
if (sixthCountingByUser == null) {
sixthCountingByUser = line.getSixthCountingByUser();
sixthCountingDate = line.getSixthCountingDate();
}
}
line.setArchived(true);
inventoryLineRepository.save(line);
}
consolidated.setFirstCounting(firstCounting);
consolidated.setFirstCountingByUser(firstCountingByUser);
consolidated.setFirstCountingDate(firstCountingDate);
consolidated.setSecondCounting(secondCounting);
consolidated.setSecondCountingByUser(secondCountingByUser);
consolidated.setSecondCountingDate(secondCountingDate);
consolidated.setThirdCounting(thirdCounting);
consolidated.setThirdCountingByUser(thirdCountingByUser);
consolidated.setThirdCountingDate(thirdCountingDate);
consolidated.setForthCounting(forthCounting);
consolidated.setForthCountingByUser(forthCountingByUser);
consolidated.setForthCountingDate(forthCountingDate);
consolidated.setControlCounting(controlCounting);
consolidated.setControlCountingByUser(controlCountingByUser);
consolidated.setControlCountingDate(controlCountingDate);
consolidated.setFifthCounting(fifthCounting);
consolidated.setFifthCountingByUser(fifthCountingByUser);
consolidated.setFifthCountingDate(fifthCountingDate);
consolidated.setSixthCounting(sixthCounting);
consolidated.setSixthCountingByUser(sixthCountingByUser);
consolidated.setSixthCountingDate(sixthCountingDate);
inventoryLineRepository.save(consolidated);
}
}
}
}

View File

@ -22,9 +22,11 @@ import com.axelor.apps.base.db.repo.ProductRepository;
import com.axelor.apps.stock.db.Inventory;
import com.axelor.apps.stock.db.InventoryLine;
import com.axelor.apps.stock.db.StockLocationLine;
import com.axelor.apps.stock.db.StockLocation;
import com.axelor.apps.stock.db.TrackingNumber;
import com.axelor.apps.stock.db.repo.InventoryLineRepository;
import com.axelor.apps.stock.db.repo.InventoryRepository;
import com.axelor.apps.stock.db.repo.StockLocationRepository;
import com.axelor.apps.stock.db.repo.TrackingNumberRepository;
import com.axelor.apps.stock.service.InventoryLineService;
import com.axelor.exception.AxelorException;
@ -38,6 +40,7 @@ import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.math.BigDecimal;
import java.util.Map;
import java.util.LinkedHashMap;
@Singleton
public class InventoryLineController {
@ -221,4 +224,29 @@ public class InventoryLineController {
response.setFlash("Updated Successfully");
// response.setReload(true);
}
public void consolidateInventoryLines(ActionRequest request, ActionResponse response){
Object object = request.getContext().get("stockLocation");
LinkedHashMap<String, Object> stockLocationMap = (LinkedHashMap<String, Object>) object;
Integer stockLocationIdInt = (Integer) stockLocationMap.get("id");
Long stockLocationId = stockLocationIdInt.longValue();
StockLocation stockLocation = Beans.get(StockLocationRepository.class).find(stockLocationId);
Beans.get(InventoryLineService.class).consolidateInventoryLines(stockLocation,false);
response.setFlash("Updated Successfully");
}
public void consolidateInventoryLinesWithInternalTrackingNumber(ActionRequest request, ActionResponse response){
Object object = request.getContext().get("stockLocation");
LinkedHashMap<String, Object> stockLocationMap = (LinkedHashMap<String, Object>) object;
Integer stockLocationIdInt = (Integer) stockLocationMap.get("id");
Long stockLocationId = stockLocationIdInt.longValue();
StockLocation stockLocation = Beans.get(StockLocationRepository.class).find(stockLocationId);
Beans.get(InventoryLineService.class).consolidateInventoryLines(stockLocation,true);
response.setFlash("Updated Successfully");
}
}

View File

@ -59,6 +59,7 @@
<integer name="conformitySelect" title="Conformity" selection="stock.move.line.conformity.select"/>
<integer name="stateSelect" title="State" selection="inventory.line.state.select"/>
<boolean name="isConsolidate" title="is Consolidate"/>
<track>