From c0e0c377ce8fe32dfc373a294e73192b526160f1 Mon Sep 17 00:00:00 2001 From: Jan Date: Sat, 13 Dec 2025 10:22:02 +0100 Subject: [PATCH 001/104] Remove unused `AzureMapsController`, clean up commented decode logic, and introduce `@PreAuthorize` annotations in controllers to enforce role-based access controls. --- .../calculation/DashboardController.java | 2 + .../calculation/PremiseController.java | 2 - .../configuration/AppsController.java | 5 +- .../CalculationDumpController.java | 3 ++ .../controller/maps/AzureMapsController.java | 48 ------------------- 5 files changed, 9 insertions(+), 51 deletions(-) delete mode 100644 src/main/java/de/avatic/lcc/controller/maps/AzureMapsController.java diff --git a/src/main/java/de/avatic/lcc/controller/calculation/DashboardController.java b/src/main/java/de/avatic/lcc/controller/calculation/DashboardController.java index e61e41d..7b1386d 100644 --- a/src/main/java/de/avatic/lcc/controller/calculation/DashboardController.java +++ b/src/main/java/de/avatic/lcc/controller/calculation/DashboardController.java @@ -3,6 +3,7 @@ package de.avatic.lcc.controller.calculation; import de.avatic.lcc.dto.calculation.execution.CalculationProcessingOverviewDTO; import de.avatic.lcc.service.calculation.execution.CalculationJobProcessorManagementService; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -18,6 +19,7 @@ public class DashboardController { } @GetMapping({"/", ""}) + @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')") public ResponseEntity getDashboardData() { return ResponseEntity.ok(calculationJobProcessorManagementService.getCalculationOverview()); } diff --git a/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java b/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java index a649ada..21eb49c 100644 --- a/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java +++ b/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java @@ -83,7 +83,6 @@ public class PremiseController { public ResponseEntity findMaterialsAndSuppliers(@RequestParam String search) { try { -// String decodedValue = URLDecoder.decode(search, StandardCharsets.UTF_8); return ResponseEntity.ok(premiseSearchStringAnalyzerService.findMaterialAndSuppliers(search)); } catch (Exception e) { throw new BadRequestException("Bad string encoding", "Unable to decode request", e); @@ -217,5 +216,4 @@ public class PremiseController { } - } diff --git a/src/main/java/de/avatic/lcc/controller/configuration/AppsController.java b/src/main/java/de/avatic/lcc/controller/configuration/AppsController.java index 3880a60..e96dc2f 100644 --- a/src/main/java/de/avatic/lcc/controller/configuration/AppsController.java +++ b/src/main/java/de/avatic/lcc/controller/configuration/AppsController.java @@ -4,6 +4,7 @@ import com.azure.core.annotation.BodyParam; import de.avatic.lcc.dto.configuration.apps.AppDTO; import de.avatic.lcc.service.apps.AppsService; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -16,21 +17,23 @@ public class AppsController { private final AppsService appsService; public AppsController(AppsService appsService) { - this.appsService = appsService; } @GetMapping({"", "/"}) + @PreAuthorize("hasRole('SERVICE')") public ResponseEntity> listApps() { return ResponseEntity.ok(appsService.listApps()); } @PostMapping({"", "/"}) + @PreAuthorize("hasRole('SERVICE')") public ResponseEntity updateApp(@RequestBody AppDTO dto) { return ResponseEntity.ok(appsService.updateApp(dto)); } @DeleteMapping({"/{id}", "/{id}/"}) + @PreAuthorize("hasRole('SERVICE')") public ResponseEntity deleteApp(@PathVariable Integer id) { appsService.deleteApp(id); return ResponseEntity.ok().build(); diff --git a/src/main/java/de/avatic/lcc/controller/configuration/CalculationDumpController.java b/src/main/java/de/avatic/lcc/controller/configuration/CalculationDumpController.java index 01ebff1..627d538 100644 --- a/src/main/java/de/avatic/lcc/controller/configuration/CalculationDumpController.java +++ b/src/main/java/de/avatic/lcc/controller/configuration/CalculationDumpController.java @@ -5,6 +5,7 @@ import de.avatic.lcc.repositories.error.DumpRepository; import de.avatic.lcc.repositories.pagination.SearchQueryPagination; import jakarta.validation.constraints.Min; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -20,11 +21,13 @@ public class CalculationDumpController { } @GetMapping({"/dump/{id}", "/dump/{id}/"}) + @PreAuthorize("hasRole('SERVICE')") public ResponseEntity getDump(@PathVariable Integer id) { return ResponseEntity.ok(dumpRepository.getDump(id)); } @GetMapping({"/dump/", "/dump"}) + @PreAuthorize("hasRole('SERVICE')") public ResponseEntity> listDumps( @RequestParam(defaultValue = "20") @Min(1) int limit, @RequestParam(defaultValue = "1") @Min(1) int page) { diff --git a/src/main/java/de/avatic/lcc/controller/maps/AzureMapsController.java b/src/main/java/de/avatic/lcc/controller/maps/AzureMapsController.java deleted file mode 100644 index a6ce88b..0000000 --- a/src/main/java/de/avatic/lcc/controller/maps/AzureMapsController.java +++ /dev/null @@ -1,48 +0,0 @@ -package de.avatic.lcc.controller.maps; - -import com.azure.core.credential.AccessToken; -import com.azure.identity.DefaultAzureCredentialBuilder; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.time.OffsetDateTime; -import java.util.HashMap; -import java.util.Map; - -@RestController -@RequestMapping("/api/maps") -public class AzureMapsController { - - @Value("${azure.maps.client.id}") - private String mapsClientId; - - @Value("${azure.maps.subscription.key}") - private String mapsSubscriptionKey; - - @GetMapping("/token") - @PreAuthorize("isAuthenticated()") - public ResponseEntity> getAzureMapsToken() { - try { - // Verwende die DefaultAzureCredential für die Authentifizierung - var credential = new DefaultAzureCredentialBuilder().build(); - - // Fordere ein Token für Azure Maps an - AccessToken token = credential.getToken( - new com.azure.core.credential.TokenRequestContext() - .addScopes("https://atlas.microsoft.com/.default") - ).block(); - - Map response = new HashMap<>(); - response.put("token", token.getToken()); - response.put("expiresOn", token.getExpiresAt().toEpochSecond()); - - return ResponseEntity.ok(response); - } catch (Exception e) { - return ResponseEntity.internalServerError().build(); - } - } -} \ No newline at end of file -- 2.45.3 From 5d804543d75a9da5bf491817000cb7d64f1240db Mon Sep 17 00:00:00 2001 From: Jan Date: Sat, 13 Dec 2025 10:28:52 +0100 Subject: [PATCH 002/104] Remove unused `TransitNodeDTO` and clean up formatting in `BulkFileType`. --- src/main/java/de/avatic/lcc/dto/bulk/BulkFileType.java | 1 - .../de/avatic/lcc/dto/calculation/TransitNodeDTO.java | 8 -------- 2 files changed, 9 deletions(-) delete mode 100644 src/main/java/de/avatic/lcc/dto/calculation/TransitNodeDTO.java diff --git a/src/main/java/de/avatic/lcc/dto/bulk/BulkFileType.java b/src/main/java/de/avatic/lcc/dto/bulk/BulkFileType.java index c55edf7..26ff083 100644 --- a/src/main/java/de/avatic/lcc/dto/bulk/BulkFileType.java +++ b/src/main/java/de/avatic/lcc/dto/bulk/BulkFileType.java @@ -9,7 +9,6 @@ public enum BulkFileType { this.fileType = fileType; } - public String getFileType() { return fileType; } diff --git a/src/main/java/de/avatic/lcc/dto/calculation/TransitNodeDTO.java b/src/main/java/de/avatic/lcc/dto/calculation/TransitNodeDTO.java deleted file mode 100644 index 5107ff3..0000000 --- a/src/main/java/de/avatic/lcc/dto/calculation/TransitNodeDTO.java +++ /dev/null @@ -1,8 +0,0 @@ -package de.avatic.lcc.dto.calculation; - -public class TransitNodeDTO { - - private Integer id; - - -} -- 2.45.3 From 94692e12ce46355b5abb78ec9c2287bea7392362 Mon Sep 17 00:00:00 2001 From: Jan Date: Sat, 13 Dec 2025 17:00:55 +0100 Subject: [PATCH 003/104] Add logging to enhance debugging in taxation services. --- .../lcc/service/api/EUTaxationApiService.java | 4 ++ .../service/api/TaxationResolverService.java | 59 ++++++++++++++----- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/main/java/de/avatic/lcc/service/api/EUTaxationApiService.java b/src/main/java/de/avatic/lcc/service/api/EUTaxationApiService.java index 70d2c24..ec3766e 100644 --- a/src/main/java/de/avatic/lcc/service/api/EUTaxationApiService.java +++ b/src/main/java/de/avatic/lcc/service/api/EUTaxationApiService.java @@ -53,12 +53,16 @@ public class EUTaxationApiService { request.setReferenceDate(getCurrentDate()); request.setTradeMovement(TradeMovementCode.fromValue(tradeMovement)); + logger.info("Lookup Measure for {} and {}", goodsCode, countryCode); + JAXBElement requestElement = objectFactory.createGoodsMeasForWs(request); @SuppressWarnings("unchecked") JAXBElement responseElement = (JAXBElement) webServiceTemplate.marshalSendAndReceive(requestElement); + logger.info("Lookup Measure for {} and {} success: {} Measures received.", goodsCode, countryCode, responseElement.getValue().getReturn().getResult().getMeasures().getMeasure().size()); + return CompletableFuture.completedFuture(responseElement.getValue()); } diff --git a/src/main/java/de/avatic/lcc/service/api/TaxationResolverService.java b/src/main/java/de/avatic/lcc/service/api/TaxationResolverService.java index bcd7f12..bdcccdb 100644 --- a/src/main/java/de/avatic/lcc/service/api/TaxationResolverService.java +++ b/src/main/java/de/avatic/lcc/service/api/TaxationResolverService.java @@ -17,6 +17,8 @@ import de.avatic.lcc.util.exception.base.InternalErrorException; import eu.europa.ec.taxation.taric.client.GoodsMeasForWsResponse; import eu.europa.ec.taxation.taric.client.GoodsMeasuresForWsResponse; import jakarta.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.*; @@ -34,6 +36,9 @@ public class TaxationResolverService { private final NomenclatureService nomenclatureService; private final CountryPropertyRepository countryPropertyRepository; + private final Logger logger = LoggerFactory.getLogger(TaxationResolverService.class); + + public TaxationResolverService(CountryRepository countryRepository, EUTaxationApiService eUTaxationApiService, PropertyRepository propertyRepository, ZolltarifnummernApiService zolltarifnummernApiService, NomenclatureService nomenclatureService, CountryPropertyRepository countryPropertyRepository) { this.countryRepository = countryRepository; this.eUTaxationApiService = eUTaxationApiService; @@ -73,26 +78,30 @@ public class TaxationResolverService { var singleResponses = doSingleRequests(joined.toList()); return Stream.of( - byCustomUnion.getOrDefault(CustomUnionType.NONE,Collections.emptyList()).stream().collect(Collectors.toMap( - r -> r, - r -> new TaxationResolverApiResponse( - r.material(), - true, - singleResponses.keySet().stream().filter(k -> k.origin.equals(r)).map(singleResponses::get).toList()))), + byCustomUnion.getOrDefault(CustomUnionType.NONE, Collections.emptyList()).stream().collect(Collectors.toMap( + r -> r, + r -> new TaxationResolverApiResponse( + r.material(), + true, + singleResponses.keySet().stream().filter(k -> k.origin.equals(r)).map(singleResponses::get).toList()))), - byCustomUnion.getOrDefault(CustomUnionType.EU,Collections.emptyList()).stream().collect(Collectors.toMap( - r -> r, - r -> new TaxationResolverApiResponse( - r.material(), - false, - null)))) + byCustomUnion.getOrDefault(CustomUnionType.EU, Collections.emptyList()).stream().collect(Collectors.toMap( + r -> r, + r -> new TaxationResolverApiResponse( + r.material(), + false, + null)))) - .flatMap(map -> map.entrySet().stream()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,(r1,_) -> r1)); + .flatMap(map -> map.entrySet().stream()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (r1, _) -> r1)); } private List resolveIncompleteHsCodesIntern(List request) { - return request.stream().flatMap(r -> nomenclatureService.getNomenclature(r.material().getHsCode()).stream().map(hsCode -> new TaxationResolverSingleRequest(hsCode, r.countryId(), r))).toList(); + var singleRequests = request.stream().flatMap(r -> nomenclatureService.getNomenclature(r.material().getHsCode()).stream().map(hsCode -> new TaxationResolverSingleRequest(hsCode, r.countryId(), r))).toList(); + + logger.info("Resolved {} incomplete hs codes to {} hs code leaves ", request.size(), singleRequests.size()); + + return singleRequests; } private List resolveIncompleteHsCodes(List request) { @@ -244,6 +253,9 @@ public class TaxationResolverService { } public List getTariffRates(List requests) { + + logger.info("Do taxation resolution for {} requests", requests.size()); + var goodMeasures = doRequests(requests); return goodMeasures.keySet().stream().map(r -> mapToResponse(r, goodMeasures.get(r))).toList(); } @@ -251,12 +263,15 @@ public class TaxationResolverService { private TaxationResolverResponse mapToResponse(TaxationResolverRequest request, TaxationResolverApiResponse apiResponse) { // source is EU country. - if(!apiResponse.requestExecuted) + if (!apiResponse.requestExecuted) return new TaxationResolverResponse(0.0, null, request.material().getHsCode(), request.material(), request.countryId()); List measForWsResponse = apiResponse.apiResponse(); + logger.info("============================"); + logger.info("Resolved measures for: {}, {}", request.material(), request.countryId()); + try { String selectedHsCode = null; Double selectedDuty = null; @@ -277,11 +292,21 @@ public class TaxationResolverService { var measureType = MeasureType.fromMeasureCode(measure.getMeasureType().getMeasureType()); boolean maybeRelevant = measureType.map(MeasureType::containsRelevantDuty).orElse(false); + logger.info("Measure ({}{}, {}): is_relevant: {}", measureType.map(MeasureType::getSeries).orElse("UNKNOWN"), measureType.map(MeasureType::getMeasureCode).orElse(""), measureType.map(MeasureType::name).orElse("UNKNOWN"), maybeRelevant); + if (maybeRelevant) { var duty = extractDuty(measure); if (duty.isPresent()) { + logger.info("Measure ({}{}, {}): duty: {}, hs code: {}", + measureType.map(MeasureType::getSeries).orElse("UNKNOWN"), + measureType.map(MeasureType::getMeasureCode).orElse(""), + measureType.map(MeasureType::name).orElse("UNKNOWN"), + measureType.map(MeasureType::getMeasureCode).orElse(null), + entry.getKey().getReturn().getResult().getRequest().getGoodsCode()); + + maxDuty = Math.max(maxDuty, duty.get()); minDuty = Math.min(minDuty, duty.get()); @@ -296,6 +321,10 @@ public class TaxationResolverService { } } + logger.info("============================"); + logger.info("Selected: measure: {}, duty: {}, max_duty: {}, min_duty: {}", selectedMeasure, selectedDuty, maxDuty, minDuty); + logger.info("============================"); + if (selectedDuty != null && (maxDuty - minDuty <= 0.02)) { return new TaxationResolverResponse(selectedDuty, selectedMeasure, selectedHsCode, request.material(), request.countryId()); } -- 2.45.3 From b75fe9bb99993b1cccd3ed167857f9eaf46efa4a Mon Sep 17 00:00:00 2001 From: Jan Date: Sat, 13 Dec 2025 17:24:14 +0100 Subject: [PATCH 004/104] Refine logging for `TaxationResolverService` to improve duty relevance and extraction insights. --- .../lcc/service/api/TaxationResolverService.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/avatic/lcc/service/api/TaxationResolverService.java b/src/main/java/de/avatic/lcc/service/api/TaxationResolverService.java index bdcccdb..f780ffb 100644 --- a/src/main/java/de/avatic/lcc/service/api/TaxationResolverService.java +++ b/src/main/java/de/avatic/lcc/service/api/TaxationResolverService.java @@ -292,18 +292,17 @@ public class TaxationResolverService { var measureType = MeasureType.fromMeasureCode(measure.getMeasureType().getMeasureType()); boolean maybeRelevant = measureType.map(MeasureType::containsRelevantDuty).orElse(false); - logger.info("Measure ({}{}, {}): is_relevant: {}", measureType.map(MeasureType::getSeries).orElse("UNKNOWN"), measureType.map(MeasureType::getMeasureCode).orElse(""), measureType.map(MeasureType::name).orElse("UNKNOWN"), maybeRelevant); if (maybeRelevant) { var duty = extractDuty(measure); if (duty.isPresent()) { - logger.info("Measure ({}{}, {}): duty: {}, hs code: {}", + logger.info("Measure ({}{}, {}): is_relevant: true, duty: {}, hs code: {}", measureType.map(MeasureType::getSeries).orElse("UNKNOWN"), measureType.map(MeasureType::getMeasureCode).orElse(""), measureType.map(MeasureType::name).orElse("UNKNOWN"), - measureType.map(MeasureType::getMeasureCode).orElse(null), + duty, entry.getKey().getReturn().getResult().getRequest().getGoodsCode()); @@ -316,8 +315,16 @@ public class TaxationResolverService { selectedMeasure = measureType.map(MeasureType::getMeasureCode).orElse(null); selectedHsCode = entry.getKey().getReturn().getResult().getRequest().getGoodsCode(); } + } else { + logger.info("Measure ({}{}, {}): is_relevant: true, no duty extracted", + measureType.map(MeasureType::getSeries).orElse("UNKNOWN"), + measureType.map(MeasureType::getMeasureCode).orElse(""), + measureType.map(MeasureType::name).orElse("UNKNOWN")); } + } else { + logger.info("Measure ({}{}, {}): is_relevant: false", measureType.map(MeasureType::getSeries).orElse("UNKNOWN"), measureType.map(MeasureType::getMeasureCode).orElse(""), measureType.map(MeasureType::name).orElse("UNKNOWN")); } + } } -- 2.45.3 From bce745e45892585334ccb4c9e812b200b967dbfd Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 14 Dec 2025 10:39:15 +0100 Subject: [PATCH 005/104] Bugfixes for #72, #73, #75, #68 --- .../lcc/service/bulk/BulkExportService.java | 8 +--- .../lcc/service/bulk/BulkImportService.java | 4 -- .../excelMapper/ContainerRateExcelMapper.java | 45 +++++++++++++++---- .../service/excelMapper/NodeExcelMapper.java | 16 +++++++ .../excelMapper/PackagingExcelMapper.java | 2 +- 5 files changed, 55 insertions(+), 20 deletions(-) diff --git a/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java b/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java index 6baf9f7..6bd323d 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java +++ b/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java @@ -24,7 +24,6 @@ public class BulkExportService { private final HeaderCellStyleProvider headerCellStyleProvider; private final ContainerRateExcelMapper containerRateExcelMapper; private final MatrixRateExcelMapper matrixRateExcelMapper; - private final MaterialExcelMapper materialExcelMapper; private final PackagingExcelMapper packagingExcelMapper; private final NodeExcelMapper nodeExcelMapper; private final HiddenNodeExcelMapper hiddenNodeExcelMapper; @@ -32,11 +31,10 @@ public class BulkExportService { private final String sheetPassword; private final MaterialFastExcelMapper materialFastExcelMapper; - public BulkExportService(@Value("${lcc.bulk.sheet_password}") String sheetPassword, HeaderCellStyleProvider headerCellStyleProvider, ContainerRateExcelMapper containerRateExcelMapper, MatrixRateExcelMapper matrixRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, HiddenNodeExcelMapper hiddenNodeExcelMapper, HiddenCountryExcelMapper hiddenCountryExcelMapper, MaterialFastExcelMapper materialFastExcelMapper) { + public BulkExportService(@Value("${lcc.bulk.sheet_password}") String sheetPassword, HeaderCellStyleProvider headerCellStyleProvider, ContainerRateExcelMapper containerRateExcelMapper, MatrixRateExcelMapper matrixRateExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, HiddenNodeExcelMapper hiddenNodeExcelMapper, HiddenCountryExcelMapper hiddenCountryExcelMapper, MaterialFastExcelMapper materialFastExcelMapper) { this.headerCellStyleProvider = headerCellStyleProvider; this.containerRateExcelMapper = containerRateExcelMapper; this.matrixRateExcelMapper = matrixRateExcelMapper; - this.materialExcelMapper = materialExcelMapper; this.packagingExcelMapper = packagingExcelMapper; this.nodeExcelMapper = nodeExcelMapper; this.hiddenNodeExcelMapper = hiddenNodeExcelMapper; @@ -93,10 +91,6 @@ public class BulkExportService { matrixRateExcelMapper.fillSheet(worksheet, style, periodId); matrixRateExcelMapper.createConstraints(workbook, worksheet); break; -// case MATERIAL: -// materialExcelMapper.fillSheet(worksheet, style); -// materialExcelMapper.createConstraints(worksheet); -// break; case PACKAGING: packagingExcelMapper.fillSheet(worksheet, style); packagingExcelMapper.createConstraints(workbook, worksheet); diff --git a/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java b/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java index aa77891..762192f 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java +++ b/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java @@ -96,10 +96,6 @@ public class BulkImportService { var matrixRates = matrixRateExcelMapper.extractSheet(sheet); matrixRateImportService.processMatrixRates(matrixRates); break; -// case MATERIAL: -// var materials = materialExcelMapper.extractSheet(sheet); -// materials.forEach(materialBulkImportService::processMaterialInstructions); -// break; case PACKAGING: var packaging = packagingExcelMapper.extractSheet(sheet); packaging.forEach(packagingBulkImportService::processPackagingInstructions); diff --git a/src/main/java/de/avatic/lcc/service/excelMapper/ContainerRateExcelMapper.java b/src/main/java/de/avatic/lcc/service/excelMapper/ContainerRateExcelMapper.java index 57afb87..b901ee5 100644 --- a/src/main/java/de/avatic/lcc/service/excelMapper/ContainerRateExcelMapper.java +++ b/src/main/java/de/avatic/lcc/service/excelMapper/ContainerRateExcelMapper.java @@ -4,9 +4,12 @@ import de.avatic.lcc.dto.generic.TransportType; import de.avatic.lcc.model.bulk.HiddenTableType; import de.avatic.lcc.model.bulk.header.ContainerRateHeader; import de.avatic.lcc.model.bulk.header.HiddenNodeHeader; +import de.avatic.lcc.model.db.nodes.Node; +import de.avatic.lcc.model.db.properties.SystemPropertyMappingId; import de.avatic.lcc.model.db.rates.ContainerRate; import de.avatic.lcc.repositories.NodeRepository; import de.avatic.lcc.repositories.rates.ContainerRateRepository; +import de.avatic.lcc.service.access.PropertyService; import de.avatic.lcc.service.bulk.helper.ConstraintGenerator; import de.avatic.lcc.service.bulk.helper.HeaderGenerator; import de.avatic.lcc.util.exception.internalerror.ExcelValidationError; @@ -16,6 +19,7 @@ import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import java.util.Optional; @Service public class ContainerRateExcelMapper { @@ -24,12 +28,14 @@ public class ContainerRateExcelMapper { private final ContainerRateRepository containerRateRepository; private final NodeRepository nodeRepository; private final ConstraintGenerator constraintGenerator; + private final PropertyService propertyService; - public ContainerRateExcelMapper(HeaderGenerator headerGenerator, ContainerRateRepository containerRateRepository, NodeRepository nodeRepository, ConstraintGenerator constraintGenerator) { + public ContainerRateExcelMapper(HeaderGenerator headerGenerator, ContainerRateRepository containerRateRepository, NodeRepository nodeRepository, ConstraintGenerator constraintGenerator, PropertyService propertyService) { this.headerGenerator = headerGenerator; this.containerRateRepository = containerRateRepository; this.nodeRepository = nodeRepository; this.constraintGenerator = constraintGenerator; + this.propertyService = propertyService; } public void fillSheet(Sheet sheet, CellStyle headerStyle, Integer periodId) { @@ -73,10 +79,20 @@ public class ContainerRateExcelMapper { return true; } + private String getRefStart() { + Optional ref = propertyService.getProperty(SystemPropertyMappingId.START_REF); + return ref.orElseThrow(() -> new InternalError("Unable to get start reference from properties")); + } + + private String getRefEnd() { + Optional ref = propertyService.getProperty(SystemPropertyMappingId.END_REF); + return ref.orElseThrow(() -> new InternalError("Unable to get end reference from properties")); + } + public List extractSheet(Sheet sheet) { headerGenerator.validateHeader(sheet, ContainerRateHeader.class); - var rates = new ArrayList(); + var rates = new ArrayList(); sheet.forEach(row -> { if (row.getRowNum() == 0) return; @@ -84,7 +100,18 @@ public class ContainerRateExcelMapper { rates.add(mapToEntity(row)); }); - return rates; + validateReferenceRoute(rates); + + return rates.stream().map(ExcelContainerRate::rate).toList(); + } + + private void validateReferenceRoute(List rates) { + var startRef = getRefStart(); + var endRef = getRefEnd(); + + if(rates.stream().noneMatch(rate -> rate.fromNode().getExternalMappingId().equals(startRef) || rate.toNode().getExternalMappingId().equals(endRef))) + throw new ExcelValidationError(String.format("Container rates must reference route (%s - %s) not found in container rates", startRef, endRef)); + } private boolean isEmpty(Row row) { @@ -109,7 +136,7 @@ public class ContainerRateExcelMapper { return result.toString(); } - private ContainerRate mapToEntity(Row row) { + private ExcelContainerRate mapToEntity(Row row) { ContainerRate entity = new ContainerRate(); validateConstraints(row); @@ -134,7 +161,7 @@ public class ContainerRateExcelMapper { entity.setRateTeu(BigDecimal.valueOf(row.getCell(ContainerRateHeader.RATE_TEU.ordinal()).getNumericCellValue())); entity.setRateHc(BigDecimal.valueOf(row.getCell(ContainerRateHeader.RATE_HC.ordinal()).getNumericCellValue())); - return entity; + return new ExcelContainerRate(entity, fromNode.orElseThrow(), toNode.orElseThrow()); } private void validateConstraints(Row row) { @@ -142,10 +169,12 @@ public class ContainerRateExcelMapper { constraintGenerator.validateStringCell(row, ContainerRateHeader.FROM_NODE.ordinal()); constraintGenerator.validateStringCell(row, ContainerRateHeader.TO_NODE.ordinal()); constraintGenerator.validateEnumConstraint(row, ContainerRateHeader.CONTAINER_RATE_TYPE.ordinal(), TransportType.class); - constraintGenerator.validateDecimalConstraint(row, ContainerRateHeader.RATE_FEU.ordinal(), 0.0, 1000000.0); - constraintGenerator.validateDecimalConstraint(row, ContainerRateHeader.RATE_TEU.ordinal(), 0.0, 1000000.0); - constraintGenerator.validateDecimalConstraint(row, ContainerRateHeader.RATE_HC.ordinal(), 0.0, 1000000.0); + constraintGenerator.validateDecimalConstraint(row, ContainerRateHeader.RATE_FEU.ordinal(), 1.0, 1000000.0); + constraintGenerator.validateDecimalConstraint(row, ContainerRateHeader.RATE_TEU.ordinal(), 1.0, 1000000.0); + constraintGenerator.validateDecimalConstraint(row, ContainerRateHeader.RATE_HC.ordinal(), 1.0, 1000000.0); constraintGenerator.validateIntegerConstraint(row, ContainerRateHeader.LEAD_TIME.ordinal(), 0, 365); } + + private record ExcelContainerRate(ContainerRate rate, Node fromNode, Node toNode) {} } diff --git a/src/main/java/de/avatic/lcc/service/excelMapper/NodeExcelMapper.java b/src/main/java/de/avatic/lcc/service/excelMapper/NodeExcelMapper.java index 5e2122c..660e281 100644 --- a/src/main/java/de/avatic/lcc/service/excelMapper/NodeExcelMapper.java +++ b/src/main/java/de/avatic/lcc/service/excelMapper/NodeExcelMapper.java @@ -1,5 +1,6 @@ package de.avatic.lcc.service.excelMapper; +import de.avatic.lcc.model.bulk.header.ContainerRateHeader; import de.avatic.lcc.model.excel.ExcelNode; import de.avatic.lcc.model.bulk.BulkInstruction; import de.avatic.lcc.model.bulk.BulkInstructionType; @@ -150,6 +151,11 @@ public class NodeExcelMapper { entity.setSource(Boolean.valueOf(row.getCell(NodeHeader.IS_SOURCE.ordinal()).getStringCellValue())); entity.setIntermediate(Boolean.valueOf(row.getCell(NodeHeader.IS_INTERMEDIATE.ordinal()).getStringCellValue())); entity.setDestination(Boolean.valueOf(row.getCell(NodeHeader.IS_DESTINATION.ordinal()).getStringCellValue())); + + if(!entity.getSource() && !entity.getDestination() && !entity.getIntermediate()) + throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter(ContainerRateHeader.FROM_NODE.ordinal()) + ": Node with mapping id " + row.getCell(NodeHeader.MAPPING_ID.ordinal()).getStringCellValue() + " must be either source, destination or intermediate"); + + entity.setPredecessorRequired(Boolean.valueOf(row.getCell(NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal()).getStringCellValue())); entity.setNodePredecessors(mapChainsFromCell(CellUtil.getCell(row, NodeHeader.PREDECESSOR_NODES.ordinal()).getStringCellValue())); entity.setOutboundCountries(mapOutboundCountriesFromCell(CellUtil.getCell(row, NodeHeader.OUTBOUND_COUNTRIES.ordinal()).getStringCellValue())); @@ -199,4 +205,14 @@ public class NodeExcelMapper { return Arrays.stream(chain.split(",")).map(String::trim).toList(); } + private String toExcelLetter(int columnIdx) { + StringBuilder result = new StringBuilder(); + columnIdx++; // Convert from 0-based to 1-based for the algorithm + while (columnIdx > 0) { + columnIdx--; // Adjust for 1-based indexing + result.insert(0, (char) ('A' + columnIdx % 26)); + columnIdx /= 26; + } + return result.toString(); + } } diff --git a/src/main/java/de/avatic/lcc/service/excelMapper/PackagingExcelMapper.java b/src/main/java/de/avatic/lcc/service/excelMapper/PackagingExcelMapper.java index cd1e0ec..f6e3860 100644 --- a/src/main/java/de/avatic/lcc/service/excelMapper/PackagingExcelMapper.java +++ b/src/main/java/de/avatic/lcc/service/excelMapper/PackagingExcelMapper.java @@ -60,7 +60,7 @@ public class PackagingExcelMapper { private void mapToRow(Packaging packaging, ArrayList headers, Row row) { Optional shu = packagingDimensionRepository.getById(packaging.getShuId()); - Optional hu = packagingDimensionRepository.getById(packaging.getShuId()); + Optional hu = packagingDimensionRepository.getById(packaging.getHuId()); row.createCell(PackagingHeader.OPERATION.ordinal()).setCellValue(BulkInstructionType.UPDATE.name()); -- 2.45.3 From adc1ee0d04c6fc355b5669b7e0dda213e1b460a6 Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 14 Dec 2025 13:10:14 +0100 Subject: [PATCH 006/104] Bugfix: annualCost in d2d routing (Issue #79) --- .../de/avatic/lcc/repositories/MaterialRepository.java | 1 + .../de/avatic/lcc/service/api/DistanceApiService.java | 2 +- .../bulk/bulkImport/MaterialBulkImportService.java | 1 + .../steps/RouteSectionCostCalculationService.java | 10 ++++++---- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/avatic/lcc/repositories/MaterialRepository.java b/src/main/java/de/avatic/lcc/repositories/MaterialRepository.java index 8582b0a..c4b5d52 100644 --- a/src/main/java/de/avatic/lcc/repositories/MaterialRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/MaterialRepository.java @@ -75,6 +75,7 @@ public class MaterialRepository { return jdbcTemplate.query(query, new MaterialMapper(), params.toArray()); } + @Transactional public Optional getByPartNumber(String partNumber) { if (partNumber == null) { return Optional.empty(); diff --git a/src/main/java/de/avatic/lcc/service/api/DistanceApiService.java b/src/main/java/de/avatic/lcc/service/api/DistanceApiService.java index 1150ebd..1f237d4 100644 --- a/src/main/java/de/avatic/lcc/service/api/DistanceApiService.java +++ b/src/main/java/de/avatic/lcc/service/api/DistanceApiService.java @@ -78,7 +78,7 @@ public class DistanceApiService { Optional cachedDistance = distanceMatrixRepository.getDistance(from, isUsrFrom, to, isUsrTo); if (cachedDistance.isPresent() && cachedDistance.get().getState() == DistanceMatrixState.VALID) { - logger.info("Found cached distance from node {} to node {}", from.getExternalMappingId(), to.getExternalMappingId()); + logger.info("Found cached distance from node {} (user: {}) to node {} (user {})", from.getExternalMappingId(), isUsrFrom, to.getExternalMappingId(), isUsrTo); return cachedDistance; } diff --git a/src/main/java/de/avatic/lcc/service/bulk/bulkImport/MaterialBulkImportService.java b/src/main/java/de/avatic/lcc/service/bulk/bulkImport/MaterialBulkImportService.java index 2f9c4c1..e5ef06f 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/bulkImport/MaterialBulkImportService.java +++ b/src/main/java/de/avatic/lcc/service/bulk/bulkImport/MaterialBulkImportService.java @@ -29,6 +29,7 @@ public class MaterialBulkImportService { } } + private void updateMaterial(Material material) { var foundMaterial = materialRepository.getByPartNumber(material.getNormalizedPartNumber()); diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java index 377c058..31b2b31 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java @@ -64,7 +64,7 @@ public class RouteSectionCostCalculationService { // Set premise metadata result.setStacked(premise.getHuStackable()); - result.setUnmixedPrice(premise.getHuMixable()); + result.setUnmixedPrice(!premise.getHuMixable()); // Get nodes and distance Node fromNode = premise.getSupplierNodeId() != null ? nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow() : userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow(); @@ -81,8 +81,10 @@ public class RouteSectionCostCalculationService { result.setTransitTime(transitTime); // Calculate price and annual cost + BigDecimal huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(containerCalculation.getHu().getContentUnitCount()), 2, RoundingMode.HALF_UP); BigDecimal utilization = getUtilization(setId, RateType.CONTAINER); /* D2D is always 40ft container */ - double annualVolume = destination.getAnnualAmount() * containerCalculation.getHu().getVolume(DimensionUnit.M); + BigDecimal annualVolume =huAnnualAmount.multiply(BigDecimal.valueOf(containerCalculation.getHu().getVolume(DimensionUnit.M))); + BigDecimal annualWeight = huAnnualAmount.multiply(BigDecimal.valueOf(containerCalculation.getHu().getWeight(WeightUnit.KG))); PriceCalculationResult prices = calculatePrices( premise.getHuMixable(), @@ -102,7 +104,7 @@ public class RouteSectionCostCalculationService { var chanceRiskFactors = changeRiskFactorCalculationService.getChanceRiskFactors(setId, periodId); - BigDecimal annualCost = (containerCalculation.isWeightExceeded() ? prices.weightPrice : prices.volumePrice).multiply(BigDecimal.valueOf(annualVolume)); + BigDecimal annualCost = (containerCalculation.isWeightExceeded() ? prices.weightPrice.multiply(annualWeight) : prices.volumePrice.multiply(annualVolume)); BigDecimal annualRiskCost = annualCost.multiply(chanceRiskFactors.getRiskFactor()); BigDecimal annualChanceCost = annualCost.multiply(chanceRiskFactors.getChanceFactor()); @@ -300,7 +302,7 @@ public class RouteSectionCostCalculationService { throw new NoSuchElementException("Destination node not found for route section" + toNode.getName()); } - return distanceService.getDistanceForNode(optSrcNode.get(), optDestNode.get(), false); + return distanceService.getDistanceForNode(optSrcNode.get(), optDestNode.get()); } -- 2.45.3 From 06ad1415fb40201a17c7cc9d6f0474b809edc868 Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 14 Dec 2025 15:45:23 +0100 Subject: [PATCH 007/104] Bugfix: was mixing up user node and non-user node in RoutingService --- .../lcc/repositories/NodeRepository.java | 23 +++++++++++++++++++ .../service/calculation/RoutingService.java | 13 +++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/avatic/lcc/repositories/NodeRepository.java b/src/main/java/de/avatic/lcc/repositories/NodeRepository.java index 88f9e0b..7274846 100644 --- a/src/main/java/de/avatic/lcc/repositories/NodeRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/NodeRepository.java @@ -5,6 +5,7 @@ import de.avatic.lcc.model.db.ValidityTuple; import de.avatic.lcc.model.db.nodes.Node; import de.avatic.lcc.repositories.pagination.SearchQueryPagination; import de.avatic.lcc.repositories.pagination.SearchQueryResult; +import de.avatic.lcc.util.exception.internalerror.DatabaseException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; @@ -165,6 +166,9 @@ public class NodeRepository { return Optional.empty(); } + if(node.isUserNode()) + throw new DatabaseException("Cannot update user node in node repository."); + String updateNodeSql = """ UPDATE node SET country_id = ?, @@ -389,6 +393,25 @@ public class NodeRepository { @Transactional public List getByDistance(Node node, Integer regionRadius) { + if(node.isUserNode()) { + String query = """ + SELECT * FROM node + WHERE is_deprecated = FALSE AND + ( + 6371 * acos( + cos(radians(?)) * + cos(radians(geo_lat)) * + cos(radians(geo_lng) - radians(?)) + + sin(radians(?)) * + sin(radians(geo_lat)) + ) + ) <= ? + """; + + return jdbcTemplate.query(query, new NodeMapper(), node.getGeoLat(), node.getGeoLng(), node.getGeoLat(), regionRadius); + } + + String query = """ SELECT * FROM node WHERE is_deprecated = FALSE AND id != ? AND diff --git a/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java b/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java index a23d92c..c406f2d 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java @@ -431,7 +431,7 @@ public class RoutingService { TemporaryRateObject finalSection = null; SourceConnectionType connectionType = SourceConnectionType.NONE; - if (source.getId().equals(chain.getLast().getId())) { + if (!source.isUserNode() && source.getId().equals(chain.getLast().getId())) { connectionType = SourceConnectionType.CHAIN_END_IS_SOURCE_NODE; } else if (nearByNodes != null) { nearByNode = nearByNodes.stream().filter(n -> n.getId().equals(chain.getLast().getId())).findFirst().orElse(null); @@ -442,7 +442,7 @@ public class RoutingService { if (connectionType == SourceConnectionType.NONE) { // find final section: check if chain end and source node are identical, then check if chain end can be connected to // source node, if this is not possible use a near-by node - finalSection = connectNodes(source, chain.getLast(), container); + finalSection = connectNodes(source, chain.getLast(), container); if (finalSection != null) connectionType = SourceConnectionType.FINAL_SECTION_WITH_SOURCE_NODE; } @@ -690,7 +690,7 @@ public class RoutingService { if (container.getRates().contains(matrixRateObj)) return container.getRates().stream().filter(r -> r.equals(matrixRateObj)).findFirst().orElseThrow(); - Optional containerRate = containerRateRepository.findRoute(startNode.getId(), endNode.getId(), TransportType.ROAD); + Optional containerRate = startNode.isUserNode() ? Optional.empty() : containerRateRepository.findRoute(startNode.getId(), endNode.getId(), TransportType.ROAD); if (containerRate.isPresent()) { containerRateObj.setRate(containerRate.get()); @@ -987,7 +987,10 @@ public class RoutingService { if (this.type.equals(TemporaryRateObjectType.MATRIX)) { return Objects.equals(this.fromNode.getCountryId(), that.fromNode.getCountryId()) && Objects.equals(this.toNode.getCountryId(), that.toNode.getCountryId()); } else if (this.type.equals(TemporaryRateObjectType.CONTAINER) || this.type.equals(TemporaryRateObjectType.MAIN_RUN) || this.type.equals(TemporaryRateObjectType.POST_RUN) || this.type.equals(TemporaryRateObjectType.NEAR_BY)) { - return Objects.equals(this.fromNode.getId(), that.fromNode.getId()) && Objects.equals(this.toNode.getId(), that.toNode.getId()); + return Objects.equals(this.fromNode.getId(), that.fromNode.getId()) + && Objects.equals(this.toNode.getId(), that.toNode.getId()) + && Objects.equals(this.fromNode.isUserNode(), that.fromNode.isUserNode()) + && Objects.equals(this.toNode.isUserNode(), that.toNode.isUserNode()); } } @@ -1000,7 +1003,7 @@ public class RoutingService { if (containerRate != null) return Objects.hash(containerRate.getFromNodeId(), containerRate.getToNodeId()); - return Objects.hash(null, null); + return Objects.hash(fromNode.getId(), toNode.getId(), fromNode.isUserNode(), toNode.isUserNode()); } public void setRate(ContainerRate containerRate) { -- 2.45.3 From dd4ce8879bd748af052efbb7041b3ad51ce64660 Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 14 Dec 2025 16:24:42 +0100 Subject: [PATCH 008/104] Fill out section distance in database during routing --- .../lcc/service/calculation/RoutingService.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java b/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java index c406f2d..b1abddb 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java @@ -219,7 +219,7 @@ public class RoutingService { routeSection.setPostRun(section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.POST_RUN)); routeSection.setPreRun(false); - routeSection.setDistance(section.getApproxDistance()); + routeSection.setDistance(section.getApproxDistance() == 0.0 ? null : section.getApproxDistance()); return routeSection; @@ -479,7 +479,7 @@ public class RoutingService { } if (connectionType == SourceConnectionType.FINAL_SECTION_WITH_NEAR_BY_NODE || connectionType == SourceConnectionType.CHAIN_END_IS_NEAR_BY_NODE) { - duplicate.routeOverNearBy(source, nearByNode); + duplicate.routeOverNearBy(source, nearByNode, distanceService.getDistanceForNode(source, nearByNode)); } routes.add(duplicate); @@ -565,7 +565,6 @@ public class RoutingService { * - check if chain is routable * - add post run and main run */ - var mainruns = container.getMainRuns(); for (var mainRun : container.getMainRuns()) { @@ -631,6 +630,10 @@ public class RoutingService { if (routable) { qualityRoutable = true; routeObj.setQuality(quality); + + mainRunObj.setApproxDistance(distanceService.getDistanceForNode(mainRunStartNode, mainRunEndNode)); + postRunObj.setApproxDistance(distanceService.getDistanceForNode(mainRunEndNode, postRunEndNode)); + routeObj.addPostRunSection(postRunObj); routeObj.addMainRunSection(mainRunObj); container.addRoute(routeObj); @@ -694,6 +697,7 @@ public class RoutingService { if (containerRate.isPresent()) { containerRateObj.setRate(containerRate.get()); + containerRateObj.setApproxDistance(distanceService.getDistanceForNode(startNode, endNode)); container.getRates().add(containerRateObj); return containerRateObj; } else { @@ -891,8 +895,10 @@ public class RoutingService { return clone; } - public void routeOverNearBy(Node source, Node nearByNode) { - this.sections.add(new TemporaryRateObject(source, nearByNode, TemporaryRateObject.TemporaryRateObjectType.NEAR_BY)); + public void routeOverNearBy(Node source, Node nearByNode, double distance) { + var rate = new TemporaryRateObject(source, nearByNode, TemporaryRateObject.TemporaryRateObjectType.NEAR_BY); + rate.setApproxDistance(distance); + this.sections.add(rate); this.nearBy = true; } -- 2.45.3 From 6b73e4afdfc6460124f9514c02c2614f3a38c4f5 Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 14 Dec 2025 16:49:57 +0100 Subject: [PATCH 009/104] Fix for (Issue #81). Layers should be stored now correctly --- .../calculation/execution/CalculationExecutionService.java | 4 ++-- .../execution/steps/ContainerCalculationService.java | 4 ++-- .../execution/steps/RouteSectionCostCalculationService.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/CalculationExecutionService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/CalculationExecutionService.java index e56a515..b24a44b 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/CalculationExecutionService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/CalculationExecutionService.java @@ -142,7 +142,7 @@ public class CalculationExecutionService { BigDecimal leadTime = null; if (destination.getD2d()) { - var containerCalculation = containerCalculationService.doCalculation(setId, premiseToHuService.createHuFromPremise(premise), ContainerType.FEU); + var containerCalculation = containerCalculationService.doCalculation(setId, premiseToHuService.createHuFromPremise(premise), ContainerType.FEU, premise.getHuMixable()); sections = List.of(new SectionInfo(null, routeSectionCostCalculationService.doD2dCalculation(setId, periodId, premise, destination, containerCalculation), containerCalculation)); leadTime = BigDecimal.valueOf(destination.getLeadTimeD2d()); usedContainerType = ContainerType.FEU; @@ -246,7 +246,7 @@ public class CalculationExecutionService { // Get container calculation for (var containerType : ContainerType.values()) { - containerCalculation.put(containerType, containerCalculationService.doCalculation(setId, hu, containerType)); + containerCalculation.put(containerType, containerCalculationService.doCalculation(setId, hu, containerType, premise.getHuMixable())); } for (var containerType : ContainerType.values()) { diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ContainerCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ContainerCalculationService.java index 3ef74ca..9411e31 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ContainerCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ContainerCalculationService.java @@ -49,7 +49,7 @@ public class ContainerCalculationService { * @param containerType The type of container to be loaded * @return ContainerCalculationResult containing loading pattern and capacity information */ - public ContainerCalculationResult doCalculation(Integer setId, PackagingDimension hu, ContainerType containerType) { + public ContainerCalculationResult doCalculation(Integer setId, PackagingDimension hu, ContainerType containerType, boolean mixable) { var weightInKg = BigDecimal.valueOf(WeightUnit.KG.convertFromG(hu.getWeight())); var maxContainerLoad = BigDecimal.valueOf(getMaxContainerLoad(containerType, setId)); @@ -60,7 +60,7 @@ public class ContainerCalculationService { var solutionHorizontal = solveLayer(SolutionType.HORIZONTAL, dimensions, containerType.getLength(), containerType.getWidth()); var solutionVertical = solveLayer(SolutionType.VERTICAL, dimensions, containerType.getWidth(), containerType.getLength()); var bestSolution = solutionHorizontal.getTotal() < solutionVertical.getTotal() ? solutionVertical : solutionHorizontal; - int layers = getLayerCount(dimensions, containerType); + int layers = mixable ? getLayerCount(dimensions, containerType) : 1; if(PalletType.EURO_PALLET.fitsOn(hu) && bestSolution.getTotal() < containerType.getPalletCount(PalletType.EURO_PALLET)) { return new ContainerCalculationResult(Math.min(containerType.getPalletCount(PalletType.EURO_PALLET)*layers,maxUnitByWeight), layers, null, (containerType.getPalletCount(PalletType.EURO_PALLET)*layers) > maxUnitByWeight, containerType, dimensions, maxContainerLoad.intValueExact()); diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java index 31b2b31..daf571a 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java @@ -83,7 +83,7 @@ public class RouteSectionCostCalculationService { // Calculate price and annual cost BigDecimal huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(containerCalculation.getHu().getContentUnitCount()), 2, RoundingMode.HALF_UP); BigDecimal utilization = getUtilization(setId, RateType.CONTAINER); /* D2D is always 40ft container */ - BigDecimal annualVolume =huAnnualAmount.multiply(BigDecimal.valueOf(containerCalculation.getHu().getVolume(DimensionUnit.M))); + BigDecimal annualVolume = huAnnualAmount.multiply(BigDecimal.valueOf(containerCalculation.getHu().getVolume(DimensionUnit.M))); BigDecimal annualWeight = huAnnualAmount.multiply(BigDecimal.valueOf(containerCalculation.getHu().getWeight(WeightUnit.KG))); PriceCalculationResult prices = calculatePrices( -- 2.45.3 From 640f466441258b5de0158e2a939687b27ee40c23 Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 14 Dec 2025 17:26:59 +0100 Subject: [PATCH 010/104] showing empty fields in handling costs (not 0.00), to prevent that the 0.00 is set in the backend --- .../layout/edit/destination/DestinationEditHandlingCost.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/components/layout/edit/destination/DestinationEditHandlingCost.vue b/src/frontend/src/components/layout/edit/destination/DestinationEditHandlingCost.vue index 0c5758b..0c52506 100644 --- a/src/frontend/src/components/layout/edit/destination/DestinationEditHandlingCost.vue +++ b/src/frontend/src/components/layout/edit/destination/DestinationEditHandlingCost.vue @@ -58,7 +58,7 @@ export default { }, repackaging: { get() { - return this.destination?.repackaging_costs?.toFixed(2) ?? '0.00'; + return this.destination?.repackaging_costs?.toFixed(2) ?? ''; }, set(value) { return this.destination && (this.destination.repackaging_costs = value); @@ -66,7 +66,7 @@ export default { }, handling: { get() { - return this.destination?.handling_costs?.toFixed(2) ?? '0.00'; + return this.destination?.handling_costs?.toFixed(2) ?? ''; }, set(value) { return this.destination && (this.destination.handling_costs = value); @@ -74,7 +74,7 @@ export default { }, disposal: { get() { - return this.destination?.disposal_costs?.toFixed(2) ?? '0.00'; + return this.destination?.disposal_costs?.toFixed(2) ?? ''; }, set(value) { return this.destination && (this.destination.disposal_costs = value); -- 2.45.3 From 6ddd48400edbe025eff85bd79abb84dff78a91d9 Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 14 Dec 2025 17:35:07 +0100 Subject: [PATCH 011/104] Extend filtering logic in NodeRepository by including `external_mapping_id` in search criteria. Fix for Issue #77 --- .../java/de/avatic/lcc/repositories/NodeRepository.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/avatic/lcc/repositories/NodeRepository.java b/src/main/java/de/avatic/lcc/repositories/NodeRepository.java index 7274846..275d130 100644 --- a/src/main/java/de/avatic/lcc/repositories/NodeRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/NodeRepository.java @@ -106,8 +106,8 @@ public class NodeRepository { entities = jdbcTemplate.query(query, new NodeMapper(), pagination.getLimit(), pagination.getOffset()); totalCount = jdbcTemplate.queryForObject(countQuery, Integer.class); } else { - entities = jdbcTemplate.query(query, new NodeMapper(), "%" + filter + "%", "%" + filter + "%", "%" + filter + "%", pagination.getLimit(), pagination.getOffset()); - totalCount = jdbcTemplate.queryForObject(countQuery, Integer.class, "%" + filter + "%", "%" + filter + "%", "%" + filter + "%"); + entities = jdbcTemplate.query(query, new NodeMapper(), "%" + filter + "%", "%" + filter + "%", "%" + filter + "%", "%" + filter + "%", pagination.getLimit(), pagination.getOffset()); + totalCount = jdbcTemplate.queryForObject(countQuery, Integer.class, "%" + filter + "%", "%" + filter + "%", "%" + filter + "%", "%" + filter + "%"); } @@ -125,7 +125,7 @@ public class NodeRepository { queryBuilder.append(" AND node.is_deprecated = FALSE"); } if (filter != null) { - queryBuilder.append(" AND (node.name LIKE ? OR node.address LIKE ? OR country.iso_code LIKE ?)"); + queryBuilder.append(" AND (node.name LIKE ? OR node.external_mapping_id LIKE ? OR node.address LIKE ? OR country.iso_code LIKE ?)"); } return queryBuilder.toString(); @@ -143,7 +143,7 @@ public class NodeRepository { queryBuilder.append(" AND node.is_deprecated = FALSE"); } if (filter != null) { - queryBuilder.append(" AND (node.name LIKE ? OR node.address LIKE ? OR country.iso_code LIKE ?)"); + queryBuilder.append(" AND (node.name LIKE ? OR node.external_mapping_id LIKE ? OR node.address LIKE ? OR country.iso_code LIKE ?)"); } queryBuilder.append(" ORDER BY node.id LIMIT ? OFFSET ?"); return queryBuilder.toString(); -- 2.45.3 From eaf3d0da9debe6843f5d2a1b45aa21bcc6244525 Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 14 Dec 2025 19:17:25 +0100 Subject: [PATCH 012/104] Simplify distance assignment logic in RoutingService. --- .../java/de/avatic/lcc/service/calculation/RoutingService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java b/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java index b1abddb..83efa68 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java @@ -219,7 +219,7 @@ public class RoutingService { routeSection.setPostRun(section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.POST_RUN)); routeSection.setPreRun(false); - routeSection.setDistance(section.getApproxDistance() == 0.0 ? null : section.getApproxDistance()); + routeSection.setDistance(section.getApproxDistance()); return routeSection; -- 2.45.3 From 5bcf599e2f4e13d95d7d08af1848a4c37346c4cb Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 14 Dec 2025 19:29:00 +0100 Subject: [PATCH 013/104] Initialize `distance` field with `BigDecimal.ZERO` in `DistanceApiService`. --- src/main/java/de/avatic/lcc/service/api/DistanceApiService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/de/avatic/lcc/service/api/DistanceApiService.java b/src/main/java/de/avatic/lcc/service/api/DistanceApiService.java index 1f237d4..557f3a1 100644 --- a/src/main/java/de/avatic/lcc/service/api/DistanceApiService.java +++ b/src/main/java/de/avatic/lcc/service/api/DistanceApiService.java @@ -121,6 +121,8 @@ public class DistanceApiService { distance.setFromGeoLng(from.getGeoLng()); distance.setToGeoLat(to.getGeoLat()); distance.setToGeoLng(to.getGeoLng()); + + distance.setDistance(BigDecimal.ZERO); } return distance; -- 2.45.3 From 4f24fd88bf7dc1cdb2eeadeb08d0558851c7a567 Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 14 Dec 2025 19:58:55 +0100 Subject: [PATCH 014/104] Fixed packaging excel header and added packaging properties --- .../de/avatic/lcc/model/bulk/header/PackagingHeader.java | 2 +- .../resources/db/migration/V11__Schedule_Priority.sql | 3 ++- .../resources/db/migration/V12__Packaging_Properties.sql | 8 ++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 src/main/resources/db/migration/V12__Packaging_Properties.sql diff --git a/src/main/java/de/avatic/lcc/model/bulk/header/PackagingHeader.java b/src/main/java/de/avatic/lcc/model/bulk/header/PackagingHeader.java index 325264c..980485f 100644 --- a/src/main/java/de/avatic/lcc/model/bulk/header/PackagingHeader.java +++ b/src/main/java/de/avatic/lcc/model/bulk/header/PackagingHeader.java @@ -17,7 +17,7 @@ public enum PackagingHeader implements HeaderProvider { HU_DIMENSION_UNIT("HU Dimension unit"), HU_WEIGHT("HU gross weight"), HU_WEIGHT_UNIT("HU gross weight unit"), - HU_UNIT_COUNT("Units/HU [pieces]"); + HU_UNIT_COUNT("SHU Units/HU [SHU pieces]"); private final String header; diff --git a/src/main/resources/db/migration/V11__Schedule_Priority.sql b/src/main/resources/db/migration/V11__Schedule_Priority.sql index 45fee6f..20945fa 100644 --- a/src/main/resources/db/migration/V11__Schedule_Priority.sql +++ b/src/main/resources/db/migration/V11__Schedule_Priority.sql @@ -16,4 +16,5 @@ ALTER TABLE premise_destination ADD COLUMN distance_d2d DECIMAL(15, 2) DEFAULT NULL COMMENT 'travel distance between the two nodes in meters'; ALTER TABLE premise_route_section - ADD COLUMN distance DECIMAL(15, 2) DEFAULT NULL COMMENT 'travel distance between the two nodes in meters'; \ No newline at end of file + ADD COLUMN distance DECIMAL(15, 2) DEFAULT NULL COMMENT 'travel distance between the two nodes in meters'; + diff --git a/src/main/resources/db/migration/V12__Packaging_Properties.sql b/src/main/resources/db/migration/V12__Packaging_Properties.sql new file mode 100644 index 0000000..24630be --- /dev/null +++ b/src/main/resources/db/migration/V12__Packaging_Properties.sql @@ -0,0 +1,8 @@ +INSERT INTO packaging_property_type (name, external_mapping_id, data_type, validation_rule, is_required, description, property_group, sequence_number) +VALUES + ('Stackable', 'STACKABLE', 'BOOLEAN', NULL, FALSE, 'desc', 'general', 1), + ('Rust Prevention', 'RUST_PREVENTION', 'BOOLEAN', NULL, FALSE, 'desc', 'general', 2), + ('Mixable', 'MIXABLE', 'BOOLEAN', NULL, FALSE, 'desc', 'general', 3) +ON DUPLICATE KEY UPDATE + name = VALUES(name), + data_type = VALUES(data_type); \ No newline at end of file -- 2.45.3 From d6313387788975d622fe8d26bc686ab7b0aa9ce4 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 15 Dec 2025 00:02:08 +0000 Subject: [PATCH 015/104] Update dependency com.azure.spring:spring-cloud-azure-dependencies to v5.24.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index eaaa690..acbde50 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ 23 - 5.24.0 + 5.24.1 5.20.0 11.18.0 -- 2.45.3 From 356f7d98a7d3e38a022f6e49e010087aa5d833b6 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 15 Dec 2025 02:01:41 +0000 Subject: [PATCH 016/104] Update mysql Docker tag to v8.4 --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index c9dfb52..3d27214 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: mysql: - image: mysql:8.0 + image: mysql:8.4 container_name: lcc-mysql-local environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} -- 2.45.3 From 088a4c62d81e0d5f4930ed58d7c2d57a1c7f3725 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 15 Dec 2025 10:26:50 +0100 Subject: [PATCH 017/104] fixed so that properties with "0" as value are shown correctly --- src/frontend/src/components/layout/config/Property.vue | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/components/layout/config/Property.vue b/src/frontend/src/components/layout/config/Property.vue index 66417b0..805cb49 100644 --- a/src/frontend/src/components/layout/config/Property.vue +++ b/src/frontend/src/components/layout/config/Property.vue @@ -178,15 +178,16 @@ export default { } if (this.property.data_type === 'INT') { - this.value = parseNumberFromString(this.value, 0); + this.value = parseNumberFromString(this.value, 0, true); } if (this.property.data_type === 'PERCENTAGE') { - this.value = parseNumberFromString(this.value, 4); + this.value = parseNumberFromString(this.value, 4, true); } if (this.property.data_type === 'CURRENCY') { - this.value = parseNumberFromString(this.value, 2); + this.value = parseNumberFromString(this.value, 2, true); + console.log(this.property.name, " parsed from 'currency' property: '", this.value, "'") } } } -- 2.45.3 From e53dd7b920e24875aeaa3dee43c466c54502a25a Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 16 Dec 2025 20:25:17 +0100 Subject: [PATCH 018/104] Reworked excel reporting --- .../src/components/layout/report/Report.vue | 48 +- src/frontend/src/store/reports.js | 2 +- .../report/ReportingController.java | 6 +- .../avatic/lcc/dto/generic/ContainerType.java | 16 +- .../lcc/dto/report/ReportDestinationDTO.java | 52 +- .../lcc/dto/report/ReportPremisesDTO.java | 47 + .../lcc/model/bulk/header/HeaderProvider.java | 10 + .../bulk/header/ReportSummaryHeader.java | 49 ++ .../lcc/service/bulk/BulkExportService.java | 11 +- .../service/bulk/TemplateExportService.java | 10 +- .../bulk/helper/CellStyleProvider.java | 138 +++ .../bulk/helper/HeaderCellStyleProvider.java | 44 - .../service/bulk/helper/HeaderGenerator.java | 131 ++- .../service/report/ExcelReportingService.java | 816 ++++++++++++++---- .../transformer/report/ReportTransformer.java | 21 +- 15 files changed, 1093 insertions(+), 308 deletions(-) create mode 100644 src/main/java/de/avatic/lcc/model/bulk/header/ReportSummaryHeader.java create mode 100644 src/main/java/de/avatic/lcc/service/bulk/helper/CellStyleProvider.java delete mode 100644 src/main/java/de/avatic/lcc/service/bulk/helper/HeaderCellStyleProvider.java diff --git a/src/frontend/src/components/layout/report/Report.vue b/src/frontend/src/components/layout/report/Report.vue index 4ea9a50..3b5420b 100644 --- a/src/frontend/src/components/layout/report/Report.vue +++ b/src/frontend/src/components/layout/report/Report.vue @@ -171,16 +171,28 @@
- + :stretch-content="true">
total [€]
-
of MEK_B [%]
+
of MEK B [%]
+
+ +
+
Current scenario
+
{{ + report.overview.mek_b.total.toFixed(2) + }} € +
+
{{ + `${(report.overview.mek_b.percentage * 100).toFixed(2)}` + }} +
@@ -229,6 +241,20 @@
{{ (report.premises.tariff_rate * 100).toFixed(2) }}%
+
+
Oversea share
+
{{ (report.premises.oversea_share * 100).toFixed(2) }}%
+
+ +
+
Airfreight share
+
{{ (report.premises.air_freight_share * 100).toFixed(2) }}%
+
+
+
Safety stock [w-days]
+
{{ report.premises.safety_stock }}
+
+
Handling unit
@@ -260,7 +286,7 @@
Mixed transport
-
{{ report.premises.mixed ? 'Yes' : 'No' }}
+
{{ report.premises.mixable ? 'Yes' : 'No' }}
@@ -287,25 +313,13 @@
{{ destination.annual_quantity }}
-
-
Oversea share
-
{{ (destination.oversea_share * 100).toFixed(2) }}%
-
- -
-
Airfreight share
-
{{ (destination.air_freight_share * 100).toFixed(2) }}%
-
Transit time [days]
{{ destination.transport_time }}
-
-
Safety stock [w-days]
-
{{ destination.safety_stock }}
-
+ diff --git a/src/frontend/src/store/reports.js b/src/frontend/src/store/reports.js index 0e73e42..72795fc 100644 --- a/src/frontend/src/store/reports.js +++ b/src/frontend/src/store/reports.js @@ -37,7 +37,7 @@ export const useReportsStore = defineStore('reports', { return; const params = new URLSearchParams(); - params.append('material', this.materialId); + params.append('materials', [this.materialId]); params.append('sources', this.supplierIds); params.append('userSources', this.userSupplierIds); diff --git a/src/main/java/de/avatic/lcc/controller/report/ReportingController.java b/src/main/java/de/avatic/lcc/controller/report/ReportingController.java index 423dfe1..1982f1c 100644 --- a/src/main/java/de/avatic/lcc/controller/report/ReportingController.java +++ b/src/main/java/de/avatic/lcc/controller/report/ReportingController.java @@ -81,13 +81,13 @@ public class ReportingController { /** * Downloads an Excel report for the given material and source nodes. * - * @param materialId The ID of the material for which the report will be downloaded. + * @param materialIds The IDs of the materials for which the report will be downloaded. * @param nodeIds A list of node IDs (sources) to include in the downloaded report. * @return The Excel file as an attachment in the response. */ @GetMapping({"/download", "/download/"}) @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION', 'BASIC')") - public ResponseEntity downloadReport(@RequestParam(value = "material") Integer materialId, @RequestParam(value = "sources", required = false) List nodeIds, @RequestParam(value = "userSources", required = false) List userNodeIds) { + public ResponseEntity downloadReport(@RequestParam(value = "materials") List materialIds, @RequestParam(value = "sources", required = false) List nodeIds, @RequestParam(value = "userSources", required = false) List userNodeIds) { HttpHeaders headers = new HttpHeaders(); headers.add("Content-Disposition", "attachment; filename=lcc_report.xlsx"); @@ -96,6 +96,6 @@ public class ReportingController { .ok() .headers(headers) .contentType(MediaType.parseMediaType("application/vnd.ms-excel")) - .body(new InputStreamResource(excelReportingService.generateExcelReport(materialId, nodeIds, userNodeIds))); + .body(new InputStreamResource(excelReportingService.generateExcelReport(materialIds, nodeIds, userNodeIds))); } } diff --git a/src/main/java/de/avatic/lcc/dto/generic/ContainerType.java b/src/main/java/de/avatic/lcc/dto/generic/ContainerType.java index d1d0369..2df42a1 100644 --- a/src/main/java/de/avatic/lcc/dto/generic/ContainerType.java +++ b/src/main/java/de/avatic/lcc/dto/generic/ContainerType.java @@ -1,10 +1,10 @@ package de.avatic.lcc.dto.generic; public enum ContainerType { - FEU(12030, 2350, 2390, 67.7, 24,21), - TEU(5890 ,2350,2390, 33.0, 11,10), - HC(12030, 2350, 2690, 76.4, 24,21), - TRUCK(13600,2450, 2650, 88.3, 34, 33); + FEU(12030, 2350, 2390, 67.7, 24,21, "40' GP"), + TEU(5890 ,2350,2390, 33.0, 11,10, "20' GP"), + HC(12030, 2350, 2690, 76.4, 24,21,"40' HC"), + TRUCK(13600,2450, 2650, 88.3, 34, 33, "Truck"); private final int length; private final int width; @@ -12,14 +12,16 @@ public enum ContainerType { private final double volume; private final int euroPalletCount; private final int industrialPalletCount; + private final String description; - ContainerType(int length, int width, int height, double volume, int euroPalletCount, int industrialPalletCount) { + ContainerType(int length, int width, int height, double volume, int euroPalletCount, int industrialPalletCount, String description) { this.length = length; this.width = width; this.height = height; this.volume = volume; this.euroPalletCount = euroPalletCount; this.industrialPalletCount = industrialPalletCount; + this.description = description; } public int getLength() { @@ -40,4 +42,8 @@ public enum ContainerType { return palletType == PalletType.EURO_PALLET ? euroPalletCount : industrialPalletCount; } + + public String getDescription() { + return description; + } } diff --git a/src/main/java/de/avatic/lcc/dto/report/ReportDestinationDTO.java b/src/main/java/de/avatic/lcc/dto/report/ReportDestinationDTO.java index bed315e..0ac4807 100644 --- a/src/main/java/de/avatic/lcc/dto/report/ReportDestinationDTO.java +++ b/src/main/java/de/avatic/lcc/dto/report/ReportDestinationDTO.java @@ -16,24 +16,19 @@ public class ReportDestinationDTO { private List sections; /* general */ - @JsonProperty("oversea_share") - private Double overseaShare; - - @JsonProperty("air_freight_share") - private Double airFreightShare; @JsonProperty("transport_time") private Double transportTime; - @JsonProperty("safety_stock") - private Integer safetyStock; @JsonProperty("annual_quantity") private Integer annualQuantity; - private Integer layer; /* container */ + + private Integer layer; + @JsonProperty("unit_count") private Number unitCount; @@ -48,7 +43,6 @@ public class ReportDestinationDTO { @JsonProperty("container_rate") private Number rate; - private Boolean mixed; public Integer getId() { return id; @@ -82,38 +76,6 @@ public class ReportDestinationDTO { this.annualQuantity = annualQuantity; } - public Double getOverseaShare() { - return overseaShare; - } - - public void setOverseaShare(Double overseaShare) { - this.overseaShare = overseaShare; - } - - public Double getAirFreightShare() { - return airFreightShare; - } - - public void setAirFreightShare(Double airFreightShare) { - this.airFreightShare = airFreightShare; - } - - public Double getTransportTime() { - return transportTime; - } - - public void setTransportTime(Double transportTime) { - this.transportTime = transportTime; - } - - public Integer getSafetyStock() { - return safetyStock; - } - - public void setSafetyStock(Integer safetyStock) { - this.safetyStock = safetyStock; - } - public Integer getLayer() { return layer; } @@ -162,11 +124,11 @@ public class ReportDestinationDTO { this.rate = rate; } - public Boolean getMixed() { - return mixed; + public Double getTransportTime() { + return transportTime; } - public void setMixed(Boolean mixed) { - this.mixed = mixed; + public void setTransportTime(Double transportTime) { + this.transportTime = transportTime; } } diff --git a/src/main/java/de/avatic/lcc/dto/report/ReportPremisesDTO.java b/src/main/java/de/avatic/lcc/dto/report/ReportPremisesDTO.java index d325ac6..b5bf464 100644 --- a/src/main/java/de/avatic/lcc/dto/report/ReportPremisesDTO.java +++ b/src/main/java/de/avatic/lcc/dto/report/ReportPremisesDTO.java @@ -13,6 +13,16 @@ public class ReportPremisesDTO { @JsonProperty("tariff_rate") private Number tariffRate; + @JsonProperty("oversea_share") + private Number overseaShare; + + @JsonProperty("air_freight_share") + private Number airFreightShare; + + @JsonProperty("safety_stock") + private Number safetyStock; + + /* packaging */ @@ -33,6 +43,9 @@ public class ReportPremisesDTO { @JsonProperty("hu_unit_count") private Integer huUnitCount; + @JsonProperty("mixable") + private Boolean mixable; + public String getHsCode() { return hsCode; @@ -105,4 +118,38 @@ public class ReportPremisesDTO { public void setHuUnitCount(Integer huUnitCount) { this.huUnitCount = huUnitCount; } + + + + public Number getOverseaShare() { + return overseaShare; + } + + public void setOverseaShare(Number overseaShare) { + this.overseaShare = overseaShare; + } + + public Number getAirFreightShare() { + return airFreightShare; + } + + public void setAirFreightShare(Number airFreightShare) { + this.airFreightShare = airFreightShare; + } + + public Number getSafetyStock() { + return safetyStock; + } + + public void setSafetyStock(Number safetyStock) { + this.safetyStock = safetyStock; + } + + public Boolean getMixable() { + return mixable; + } + + public void setMixable(Boolean mixable) { + this.mixable = mixable; + } } diff --git a/src/main/java/de/avatic/lcc/model/bulk/header/HeaderProvider.java b/src/main/java/de/avatic/lcc/model/bulk/header/HeaderProvider.java index eeade12..2ecb607 100644 --- a/src/main/java/de/avatic/lcc/model/bulk/header/HeaderProvider.java +++ b/src/main/java/de/avatic/lcc/model/bulk/header/HeaderProvider.java @@ -3,4 +3,14 @@ package de.avatic.lcc.model.bulk.header; public interface HeaderProvider { String getHeader(); + default int occupiedCells() { + return 1; + } + + default int getColumn() { throw new UnsupportedOperationException();} + + default boolean useOrdinal() { + return true; + } + } diff --git a/src/main/java/de/avatic/lcc/model/bulk/header/ReportSummaryHeader.java b/src/main/java/de/avatic/lcc/model/bulk/header/ReportSummaryHeader.java new file mode 100644 index 0000000..c513739 --- /dev/null +++ b/src/main/java/de/avatic/lcc/model/bulk/header/ReportSummaryHeader.java @@ -0,0 +1,49 @@ +package de.avatic.lcc.model.bulk.header; + +public enum ReportSummaryHeader implements HeaderProvider{ + MATERIAL("Material", 0, 1), + SUPPLIER("Supplier", 1, 1), + MEK_A("MEK A", 2, 2), + LOGISTICS_COST("Logistics costs", 4, 2), + MEK_B("MEK B", 6, 2), + TRANSPORT("Transport", 8, 1), + HANDLING("Handling", 9, 1), + STORAGE("Storage", 10, 1), + REPACKAGING("Repackaging", 11, 1), + DISPOSAL("Disposal", 12, 1), + CAPITAL("Capital", 13, 1), + CUSTOM("Custom", 14, 1), + FCA_FEE("FCA Fee", 15, 1), + AIR_FREIGHT("Air freight", 16, 1); + + + private final int occupiedCells; + private final String header; + private final int column; + + ReportSummaryHeader(String header, int column, int occupiedCells) { + this.occupiedCells = occupiedCells; + this.header = header; + this.column = column; + } + + @Override + public boolean useOrdinal() { + return false; + } + + @Override + public String getHeader() { + return header; + } + + @Override + public int occupiedCells() { + return occupiedCells; + } + + @Override + public int getColumn() { + return column; + } +} diff --git a/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java b/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java index 6bd323d..6b0c1a6 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java +++ b/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java @@ -4,8 +4,7 @@ import de.avatic.lcc.dto.bulk.BulkFileType; import de.avatic.lcc.model.bulk.BulkFileTypes; import de.avatic.lcc.model.bulk.BulkOperation; import de.avatic.lcc.model.bulk.HiddenTableType; -import de.avatic.lcc.repositories.rates.ValidityPeriodRepository; -import de.avatic.lcc.service.bulk.helper.HeaderCellStyleProvider; +import de.avatic.lcc.service.bulk.helper.CellStyleProvider; import de.avatic.lcc.service.excelMapper.*; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.Sheet; @@ -21,7 +20,7 @@ import java.io.IOException; @Service public class BulkExportService { - private final HeaderCellStyleProvider headerCellStyleProvider; + private final CellStyleProvider cellStyleProvider; private final ContainerRateExcelMapper containerRateExcelMapper; private final MatrixRateExcelMapper matrixRateExcelMapper; private final PackagingExcelMapper packagingExcelMapper; @@ -31,8 +30,8 @@ public class BulkExportService { private final String sheetPassword; private final MaterialFastExcelMapper materialFastExcelMapper; - public BulkExportService(@Value("${lcc.bulk.sheet_password}") String sheetPassword, HeaderCellStyleProvider headerCellStyleProvider, ContainerRateExcelMapper containerRateExcelMapper, MatrixRateExcelMapper matrixRateExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, HiddenNodeExcelMapper hiddenNodeExcelMapper, HiddenCountryExcelMapper hiddenCountryExcelMapper, MaterialFastExcelMapper materialFastExcelMapper) { - this.headerCellStyleProvider = headerCellStyleProvider; + public BulkExportService(@Value("${lcc.bulk.sheet_password}") String sheetPassword, CellStyleProvider cellStyleProvider, ContainerRateExcelMapper containerRateExcelMapper, MatrixRateExcelMapper matrixRateExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, HiddenNodeExcelMapper hiddenNodeExcelMapper, HiddenCountryExcelMapper hiddenCountryExcelMapper, MaterialFastExcelMapper materialFastExcelMapper) { + this.cellStyleProvider = cellStyleProvider; this.containerRateExcelMapper = containerRateExcelMapper; this.matrixRateExcelMapper = matrixRateExcelMapper; this.packagingExcelMapper = packagingExcelMapper; @@ -66,7 +65,7 @@ public class BulkExportService { Workbook workbook = new XSSFWorkbook(); Sheet worksheet = workbook.createSheet(BulkFileTypes.valueOf(bulkFileType.name()).getSheetName()); - CellStyle style = headerCellStyleProvider.createHeaderCellStyle(workbook); + CellStyle style = cellStyleProvider.createHeaderCellStyle(workbook); if (bulkFileType.equals(BulkFileType.COUNTRY_MATRIX) || bulkFileType.equals(BulkFileType.NODE)) { diff --git a/src/main/java/de/avatic/lcc/service/bulk/TemplateExportService.java b/src/main/java/de/avatic/lcc/service/bulk/TemplateExportService.java index e5159f4..79954a8 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/TemplateExportService.java +++ b/src/main/java/de/avatic/lcc/service/bulk/TemplateExportService.java @@ -3,7 +3,7 @@ package de.avatic.lcc.service.bulk; import de.avatic.lcc.dto.bulk.BulkFileType; import de.avatic.lcc.model.bulk.*; import de.avatic.lcc.model.bulk.header.*; -import de.avatic.lcc.service.bulk.helper.HeaderCellStyleProvider; +import de.avatic.lcc.service.bulk.helper.CellStyleProvider; import de.avatic.lcc.service.bulk.helper.HeaderGenerator; import de.avatic.lcc.service.excelMapper.*; import org.apache.poi.ss.usermodel.CellStyle; @@ -24,7 +24,7 @@ public class TemplateExportService { private final HeaderGenerator headerGenerator; - private final HeaderCellStyleProvider headerCellStyleProvider; + private final CellStyleProvider cellStyleProvider; private final HiddenNodeExcelMapper hiddenNodeExcelMapper; private final HiddenCountryExcelMapper hiddenCountryExcelMapper; private final String sheetPassword; @@ -34,9 +34,9 @@ public class TemplateExportService { private final PackagingExcelMapper packagingExcelMapper; private final NodeExcelMapper nodeExcelMapper; - public TemplateExportService(@Value("${lcc.bulk.sheet_password}") String sheetPassword, HeaderGenerator headerGenerator, HeaderCellStyleProvider headerCellStyleProvider, HiddenNodeExcelMapper hiddenNodeExcelMapper, HiddenCountryExcelMapper hiddenCountryExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MatrixRateExcelMapper matrixRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper) { + public TemplateExportService(@Value("${lcc.bulk.sheet_password}") String sheetPassword, HeaderGenerator headerGenerator, CellStyleProvider cellStyleProvider, HiddenNodeExcelMapper hiddenNodeExcelMapper, HiddenCountryExcelMapper hiddenCountryExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MatrixRateExcelMapper matrixRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper) { this.headerGenerator = headerGenerator; - this.headerCellStyleProvider = headerCellStyleProvider; + this.cellStyleProvider = cellStyleProvider; this.hiddenNodeExcelMapper = hiddenNodeExcelMapper; this.hiddenCountryExcelMapper = hiddenCountryExcelMapper; this.sheetPassword = sheetPassword; @@ -53,7 +53,7 @@ public class TemplateExportService { Sheet sheet = workbook.createSheet(BulkFileTypes.valueOf(bulkFileType.name()).getSheetName()); - CellStyle style = headerCellStyleProvider.createHeaderCellStyle(workbook); + CellStyle style = cellStyleProvider.createHeaderCellStyle(workbook); if (bulkFileType.equals(BulkFileType.COUNTRY_MATRIX) || bulkFileType.equals(BulkFileType.NODE)) { diff --git a/src/main/java/de/avatic/lcc/service/bulk/helper/CellStyleProvider.java b/src/main/java/de/avatic/lcc/service/bulk/helper/CellStyleProvider.java new file mode 100644 index 0000000..ce6f063 --- /dev/null +++ b/src/main/java/de/avatic/lcc/service/bulk/helper/CellStyleProvider.java @@ -0,0 +1,138 @@ +package de.avatic.lcc.service.bulk.helper; + +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFColor; +import org.apache.poi.xssf.usermodel.XSSFFont; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +@Service +public class CellStyleProvider { + + public CellStyle createHeaderCellStyle(Workbook workbook) { + + XSSFFont headerFont = (XSSFFont) workbook.createFont(); + + headerFont.setColor(PredefinedColors.NEUTRAL.getColor()); + headerFont.setFontName("Arial"); + headerFont.setFontHeightInPoints((short) 11); + headerFont.setBold(true); + + CellStyle headerStyle = workbook.createCellStyle(); + headerStyle.setFont(headerFont); + headerStyle.setFillForegroundColor(PredefinedColors.TITLE.getColor()); + + headerStyle.setAlignment(HorizontalAlignment.CENTER); + headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + return updateBorderStyle(headerStyle, IndexedColors.WHITE); + } + + private XSSFFont createFontStyle(Workbook workbook, boolean bold) { + XSSFFont font = (XSSFFont) workbook.createFont(); + + font.setColor(PredefinedColors.TITLE.getColor()); + font.setFontName("Arial"); + font.setFontHeightInPoints((short) 11); + font.setBold(bold); + + return font; + } + + private CellStyle updateBorderStyle(CellStyle style, IndexedColors color) { + + style.setBorderBottom(BorderStyle.THIN); + style.setBorderTop(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + + style.setLeftBorderColor(color.getIndex()); + style.setRightBorderColor(color.getIndex()); + style.setBottomBorderColor(color.getIndex()); + style.setTopBorderColor(color.getIndex()); + + return style; + } + + + private CellStyle createStyle(Workbook workbook, Short textFormat, XSSFFont fontStyle) { + CellStyle style = workbook.createCellStyle(); + + style.setFont(fontStyle); + style.setAlignment(HorizontalAlignment.RIGHT); + style.setDataFormat(textFormat); + + return updateBorderStyle(style, IndexedColors.GREY_25_PERCENT); + } + + + private CellStyle updateHighlight(CellStyle style, PredefinedColors background) { + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + style.setFillForegroundColor(background.getColor()); + return style; + } + + public Map> createCellStyles(Workbook workbook) { + + Map> styles = new HashMap<>(); + + Map textFormat = new HashMap<>(); + textFormat.put(TextFormat.TEXT_ONLY, workbook.createDataFormat().getFormat("@")); + textFormat.put(TextFormat.CURRENCY, workbook.createDataFormat().getFormat("#,##0.00 €")); + textFormat.put(TextFormat.PERCENTAGE, workbook.createDataFormat().getFormat("0.00%")); + + var fontStyle = createFontStyle(workbook, false); + var headerFontStyle = createFontStyle(workbook, true); + + for (TextFormat currentFormat : TextFormat.values()) { + + Map colors = new HashMap<>(); + + for (PredefinedColors color : PredefinedColors.values()) { + + CellStyle style = createStyle(workbook, textFormat.get(currentFormat), color.getBold() ? headerFontStyle : fontStyle); + colors.put(color, updateHighlight(style, color)); + + } + + styles.put(currentFormat, colors); + } + + return styles; + } + + + public enum TextFormat { + TEXT_ONLY, + CURRENCY, + PERCENTAGE, + } + + public enum PredefinedColors { + HEADER(true, new XSSFColor(new byte[]{(byte) 195, (byte) 207, (byte) 223}, null)), + SUB_HEADER(true, new XSSFColor(new byte[]{(byte) 220, (byte) 227, (byte) 236}, null)), + LIGHT_BLUE_1(true, new XSSFColor(new byte[]{(byte) 220, (byte) 227, (byte) 236}, null)), + LIGHT_BLUE_2(true, new XSSFColor(new byte[]{(byte) 195, (byte) 207, (byte) 223}, null)), + GREEN_1(false, new XSSFColor(new byte[]{(byte) 160, (byte) 246, (byte) 211}, null)), + GREEN_2(false, new XSSFColor(new byte[]{(byte) 90, (byte) 240, (byte) 180}, null)), + TITLE(true, new XSSFColor(new byte[]{(byte) 0, (byte) 47, (byte) 84}, null)), + NEUTRAL(false, new XSSFColor(new byte[]{(byte) 255, (byte) 255, (byte) 255}, null)), + EXCEPTION(false, new XSSFColor(new byte[]{(byte) 188, (byte) 43, (byte) 114}, null)); + + final XSSFColor color; + private final boolean bold; + + PredefinedColors(boolean bold, XSSFColor xssfColor) { + this.color = xssfColor; + this.bold = bold; + } + + public XSSFColor getColor() { + return color; + } + + public boolean getBold() { return bold; } + } + +} diff --git a/src/main/java/de/avatic/lcc/service/bulk/helper/HeaderCellStyleProvider.java b/src/main/java/de/avatic/lcc/service/bulk/helper/HeaderCellStyleProvider.java deleted file mode 100644 index f218a98..0000000 --- a/src/main/java/de/avatic/lcc/service/bulk/helper/HeaderCellStyleProvider.java +++ /dev/null @@ -1,44 +0,0 @@ -package de.avatic.lcc.service.bulk.helper; - -import org.apache.poi.ss.usermodel.*; -import org.apache.poi.xssf.usermodel.XSSFCellStyle; -import org.apache.poi.xssf.usermodel.XSSFColor; -import org.apache.poi.xssf.usermodel.XSSFFont; -import org.springframework.stereotype.Service; - -@Service -public class HeaderCellStyleProvider { - - public CellStyle createHeaderCellStyle(Workbook workbook) { - - XSSFFont headerFont = (XSSFFont) workbook.createFont(); - - XSSFColor customTextColor = new XSSFColor(new byte[]{(byte)0, (byte)47, (byte)84}, null); // Blue - headerFont.setColor(customTextColor); - - headerFont.setFontName("Arial"); - headerFont.setFontHeightInPoints((short)10); - headerFont.setBold(true); - - - - XSSFColor customColor = new XSSFColor(new byte[]{(byte)90, (byte)240, (byte)180}, null); - - CellStyle headerStyle = workbook.createCellStyle(); - headerFont.setBold(true); - headerStyle.setFont(headerFont); - headerStyle.setFillForegroundColor(customColor); - - - headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); - headerStyle.setBorderBottom(BorderStyle.THIN); - headerStyle.setBorderTop(BorderStyle.THIN); - headerStyle.setBorderLeft(BorderStyle.THIN); - headerStyle.setBorderRight(BorderStyle.THIN); - headerStyle.setAlignment(HorizontalAlignment.CENTER); - return headerStyle; - } - - - -} diff --git a/src/main/java/de/avatic/lcc/service/bulk/helper/HeaderGenerator.java b/src/main/java/de/avatic/lcc/service/bulk/helper/HeaderGenerator.java index e9a3a83..cce553e 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/helper/HeaderGenerator.java +++ b/src/main/java/de/avatic/lcc/service/bulk/helper/HeaderGenerator.java @@ -6,6 +6,8 @@ import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.RegionUtil; import org.springframework.stereotype.Service; import java.util.EnumSet; @@ -13,13 +15,13 @@ import java.util.EnumSet; @Service public class HeaderGenerator { - private static final int ADD_COLUMN_SIZE = (10*256); + private static final int ADD_COLUMN_SIZE = (10 * 256); public & HeaderProvider> void validateHeader(Sheet sheet, Class headers) { Row row = sheet.getRow(0); - for(H header : EnumSet.allOf(headers)){ + for (H header : EnumSet.allOf(headers)) { Cell cell = row.getCell(header.ordinal()); - if(cell == null || !cell.getStringCellValue().equals(header.getHeader())){ + if (cell == null || !cell.getStringCellValue().equals(header.getHeader())) { throw new ExcelValidationError("Unable to validate header \"" + header.getHeader() + "\": Header of column " + toExcelLetter(header.ordinal()) + " has to be \"" + header.getHeader() + "\""); } } @@ -28,9 +30,9 @@ public class HeaderGenerator { public void validateHeader(Sheet sheet, String[] headers) { Row row = sheet.getRow(0); int idx = 0; - for(String header : headers){ + for (String header : headers) { Cell cell = row.getCell(idx++); - if(cell == null || !cell.getStringCellValue().equals(header)){ + if (cell == null || !cell.getStringCellValue().equals(header)) { throw new ExcelValidationError("Unable to validate header \"" + header + "\": Header of column " + toExcelLetter(idx) + " has to be \"" + header + "\""); } @@ -39,39 +41,132 @@ public class HeaderGenerator { public & HeaderProvider> void generateHeader(Sheet worksheet, Class headers, CellStyle style) { Row row = worksheet.createRow(0); + + Boolean usesOrdinals = null; + for (H header : EnumSet.allOf(headers)) { - Cell cell = row.createCell(header.ordinal()); - cell.setCellValue(header.getHeader()); - cell.setCellStyle(style); - worksheet.autoSizeColumn(header.ordinal()); + + if (usesOrdinals == null) usesOrdinals = header.useOrdinal(); + + if (usesOrdinals) { + Cell cell = row.createCell(header.ordinal()); + cell.setCellValue(header.getHeader()); + cell.setCellStyle(style); + worksheet.autoSizeColumn(header.ordinal()); + + } else { + Cell cell = row.createCell(header.getColumn()); + + if (header.occupiedCells() > 1) { + + + var merged = new CellRangeAddress(0, 0, header.getColumn(), header.getColumn() + header.occupiedCells() - 1); + + RegionUtil.setBorderBottom(style.getBorderBottom(), merged, worksheet); + RegionUtil.setBorderTop(style.getBorderTop(), merged, worksheet); + RegionUtil.setBorderLeft(style.getBorderLeft(), merged, worksheet); + RegionUtil.setBorderRight(style.getBorderRight(), merged, worksheet); + RegionUtil.setBottomBorderColor(style.getBottomBorderColor(), merged, worksheet); + RegionUtil.setTopBorderColor(style.getTopBorderColor(), merged, worksheet); + RegionUtil.setLeftBorderColor(style.getLeftBorderColor(), merged, worksheet); + RegionUtil.setRightBorderColor(style.getRightBorderColor(), merged, worksheet); + + worksheet.addMergedRegion(merged); + } + + cell.setCellValue(header.getHeader()); + cell.setCellStyle(style); + worksheet.autoSizeColumn(header.getColumn()); + } } } public void generateHeader(Sheet worksheet, String[] headers, CellStyle style) { + generateHeader(worksheet, headers, style, false); + } + + public void generateHeader(Sheet worksheet, String[] headers, CellStyle style, boolean mergeCells) { Row row = worksheet.createRow(0); - int idx = 0; - for (String header : headers) { - Cell cell = row.createCell(idx); - cell.setCellValue(header); - cell.setCellStyle(style); - worksheet.autoSizeColumn(idx++); + + for (int idx = 0; idx < headers.length; idx++) { + + if (headers[idx] != null || !mergeCells) { + String header = headers[idx]; + + Cell cell = row.createCell(idx); + cell.setCellValue(header); + cell.setCellStyle(style); + + if (mergeCells) { + int lookAhead = 1; + while (idx + lookAhead < headers.length && headers[idx + lookAhead] == null) lookAhead++; + + if (lookAhead > 1) { + var merged = new CellRangeAddress( + 0, + 0, + idx, + idx + lookAhead - 1 + ); + + RegionUtil.setBorderBottom(style.getBorderBottom(), merged, worksheet); + RegionUtil.setBorderTop(style.getBorderTop(), merged, worksheet); + RegionUtil.setBorderLeft(style.getBorderLeft(), merged, worksheet); + RegionUtil.setBorderRight(style.getBorderRight(), merged, worksheet); + RegionUtil.setBottomBorderColor(style.getBottomBorderColor(), merged, worksheet); + RegionUtil.setTopBorderColor(style.getTopBorderColor(), merged, worksheet); + RegionUtil.setLeftBorderColor(style.getLeftBorderColor(), merged, worksheet); + RegionUtil.setRightBorderColor(style.getRightBorderColor(), merged, worksheet); + + worksheet.addMergedRegion(merged); + } + } + + + } + + worksheet.autoSizeColumn(idx); } } + public void fixWidth(Sheet sheet, String[] headers, boolean respectMerged) { + + if (respectMerged) { + int idx = 0; + for (String header : headers) { + sheet.autoSizeColumn(idx, true); + sheet.setColumnWidth(idx, sheet.getColumnWidth(idx) + ADD_COLUMN_SIZE); + idx++; + } + } else + fixWidth(sheet, headers); + + } + + public void fixWidth(Sheet sheet, String[] headers) { int idx = 0; for (String header : headers) { sheet.autoSizeColumn(idx); - sheet.setColumnWidth(idx,sheet.getColumnWidth(idx)+ADD_COLUMN_SIZE); + sheet.setColumnWidth(idx, sheet.getColumnWidth(idx) + ADD_COLUMN_SIZE); idx++; } } public & HeaderProvider> void fixWidth(Sheet sheet, Class headers) { + Boolean usesOrdinals = null; + for (H header : EnumSet.allOf(headers)) { - sheet.autoSizeColumn(header.ordinal()); - sheet.setColumnWidth(header.ordinal(),sheet.getColumnWidth(header.ordinal())+ADD_COLUMN_SIZE); + if (usesOrdinals == null) usesOrdinals = header.useOrdinal(); + + if (usesOrdinals) { + sheet.autoSizeColumn(header.ordinal()); + sheet.setColumnWidth(header.ordinal(), sheet.getColumnWidth(header.ordinal()) + ADD_COLUMN_SIZE); + } else { + sheet.autoSizeColumn(header.getColumn()); + sheet.setColumnWidth(header.getColumn(), sheet.getColumnWidth(header.getColumn()) + ADD_COLUMN_SIZE); + } } } diff --git a/src/main/java/de/avatic/lcc/service/report/ExcelReportingService.java b/src/main/java/de/avatic/lcc/service/report/ExcelReportingService.java index 2c9990f..863462f 100644 --- a/src/main/java/de/avatic/lcc/service/report/ExcelReportingService.java +++ b/src/main/java/de/avatic/lcc/service/report/ExcelReportingService.java @@ -1,215 +1,721 @@ package de.avatic.lcc.service.report; import de.avatic.lcc.dto.generic.NodeDTO; -import de.avatic.lcc.dto.generic.TransportType; import de.avatic.lcc.dto.report.ReportDTO; import de.avatic.lcc.dto.report.ReportDestinationDTO; import de.avatic.lcc.dto.report.ReportEntryDTO; -import de.avatic.lcc.dto.report.ReportPremisesDTO; -import de.avatic.lcc.service.bulk.helper.HeaderCellStyleProvider; +import de.avatic.lcc.dto.report.ReportSectionDTO; +import de.avatic.lcc.model.bulk.header.ReportSummaryHeader; +import de.avatic.lcc.model.db.properties.SystemPropertyMappingId; +import de.avatic.lcc.repositories.MaterialRepository; +import de.avatic.lcc.repositories.properties.PropertyRepository; +import de.avatic.lcc.service.bulk.helper.CellStyleProvider; import de.avatic.lcc.service.bulk.helper.HeaderGenerator; -import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.CellStyle; -import org.apache.poi.ss.usermodel.Sheet; -import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.RegionUtil; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.core.io.ByteArrayResource; import org.springframework.stereotype.Service; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import java.util.*; +import java.util.stream.Stream; @Service public class ExcelReportingService { - private final ReportingService reportingService; - private final HeaderCellStyleProvider headerCellStyleProvider; - private final HeaderGenerator headerGenerator; + private static final Map rowSequence = new HashMap<>() { + { + put(0, new RowInfo(0, RowCaption.SUPPLIER, "Supplier", RowType.TITLE, true)); + put(1, new RowInfo(1, RowCaption.ADDRESS, "Address", RowType.SUB_TITLE, true)); - public ExcelReportingService(ReportingService reportingService, HeaderCellStyleProvider headerCellStyleProvider, HeaderGenerator headerGenerator) { + put(2, new RowInfo(2, RowCaption.SUMMARY, "Summary", RowType.HEADER, false)); + put(3, new RowInfo(3, RowCaption.SUMMARY_MEK_A, "MEK A", RowType.SPLIT_DATA, false)); + put(4, new RowInfo(4, RowCaption.SUMMARY_LOGISTICS_COST, "Logistics cost", RowType.SPLIT_DATA, false)); + put(5, new RowInfo(5, RowCaption.SUMMARY_MEK_B, "MEK B", RowType.SPLIT_DATA, false)); + + put(6, new RowInfo(6, RowCaption.WEIGHTED_COST_BREAKDOWN, "Weighted cost breakdown", RowType.HEADER, false)); + put(7, new RowInfo(7, RowCaption.BREAKDOWN_MEK_A, "MEK A", RowType.SPLIT_DATA, false)); + put(8, new RowInfo(8, RowCaption.BREAKDOWN_TRANSPORT, "Transport", RowType.SPLIT_DATA, false)); + put(9, new RowInfo(9, RowCaption.BREAKDOWN_HANDLING, "Handling", RowType.SPLIT_DATA, false)); + put(10, new RowInfo(10, RowCaption.BREAKDOWN_STORAGE, "Storage", RowType.SPLIT_DATA, false)); + put(11, new RowInfo(11, RowCaption.BREAKDOWN_REPACKAGING, "Repackaging", RowType.SPLIT_DATA, false)); + put(12, new RowInfo(12, RowCaption.BREAKDOWN_DISPOSAL, "Disposal", RowType.SPLIT_DATA, false)); + put(13, new RowInfo(13, RowCaption.BREAKDOWN_CAPITAL, "Capital", RowType.SPLIT_DATA, false)); + put(14, new RowInfo(14, RowCaption.BREAKDOWN_CUSTOM, "Custom", RowType.SPLIT_DATA, false)); + put(15, new RowInfo(15, RowCaption.BREAKDOWN_FCA_FEES, "FCA fees", RowType.SPLIT_DATA, false)); + put(16, new RowInfo(16, RowCaption.BREAKDOWN_TOTAL, "Total", RowType.SPLIT_DATA, false)); + + put(17, new RowInfo(17, RowCaption.FLUCTUATION, "Transport costs fluctuations", RowType.HEADER, false)); + put(18, new RowInfo(18, RowCaption.FLUCTUATION_CURRENT, "Current scenario", RowType.SPLIT_DATA, false)); + put(19, new RowInfo(19, RowCaption.FLUCTUATION_OPPORTUNITY, "Opportunity scenario", RowType.SPLIT_DATA, false)); + put(20, new RowInfo(20, RowCaption.FLUCTUATION_RISK, "Risk scenario", RowType.SPLIT_DATA, false)); + + put(21, new RowInfo(21, RowCaption.ASSUMPTIONS, "Assumptions", RowType.HEADER, false)); + put(22, new RowInfo(22, RowCaption.ASSUMPTIONS_MATERIAL, "Material", RowType.SUB_HEADER, false)); + put(23, new RowInfo(23, RowCaption.ASSUMPTIONS_PART_NUMBER, "Part number", RowType.DATA, false)); + put(24, new RowInfo(24, RowCaption.ASSUMPTIONS_HS_CODE, "HS code", RowType.DATA, false)); + put(25, new RowInfo(25, RowCaption.ASSUMPTIONS_TARIFF_RATE, "Tariff rate", RowType.DATA, false)); + put(26, new RowInfo(26, RowCaption.ASSUMPTIONS_OVERSEA_SHARE, "Oversea share", RowType.DATA, false)); + put(27, new RowInfo(27, RowCaption.ASSUMPTIONS_AIR_FREIGHT_SHARE, "Airfreight share", RowType.DATA, false)); + put(28, new RowInfo(28, RowCaption.ASSUMPTIONS_SAFETY_STOCK, "Safety stock [working days]", RowType.DATA, false)); + + put(29, new RowInfo(29, RowCaption.HANDLING_UNIT, "Handling unit", RowType.SUB_HEADER, false)); + put(30, new RowInfo(30, RowCaption.HANDLING_UNIT_LENGTH, "Length", RowType.DATA, false)); + put(31, new RowInfo(31, RowCaption.HANDLING_UNIT_WIDTH, "Width", RowType.DATA, false)); + put(32, new RowInfo(32, RowCaption.HANDLING_UNIT_HEIGHT, "Height", RowType.DATA, false)); + put(33, new RowInfo(33, RowCaption.HANDLING_UNIT_DIMENSION_UNIT, "Unit", RowType.DATA, false)); + put(34, new RowInfo(34, RowCaption.HANDLING_UNIT_WEIGHT, "Weight", RowType.DATA, false)); + put(35, new RowInfo(35, RowCaption.HANDLING_UNIT_DIMENSION_WEIGHT, "Unit", RowType.DATA, false)); + put(36, new RowInfo(36, RowCaption.HANDLING_UNIT_PIECES, "Pieces per HU", RowType.DATA, false)); + put(37, new RowInfo(37, RowCaption.HANDLING_UNIT_MIXED, "Mixed transport", RowType.DATA, false)); + + } + }; + private static final Map destinationRowSequence = new HashMap<>() { + { + put(0, new RowInfo(0, RowCaption.DESTINATION, "Destination", RowType.HEADER, true)); + + put(1, new RowInfo(1, RowCaption.DESTINATION_GENERAL, "General", RowType.SUB_HEADER, false)); + + put(2, new RowInfo(2, RowCaption.DESTINATION_ROUTE, "Route", RowType.DATA, false)); + put(3, new RowInfo(3, RowCaption.DESTINATION_ANNUAL_QUANTITY, "Annual quantity", RowType.DATA, false)); + put(4, new RowInfo(4, RowCaption.DESTINATION_TRANSPORT_TIME, "Transit time [days]", RowType.DATA, false)); + + put(5, new RowInfo(5, RowCaption.DESTINATION_CONTAINER, "Container calculation", RowType.SUB_HEADER, false)); + put(6, new RowInfo(6, RowCaption.DESTINATION_CONTAINER_LAYER, "Stacked layers", RowType.DATA, false)); + put(7, new RowInfo(7, RowCaption.DESTINATION_CONTAINER_UNIT_COUNT, "Container unit count", RowType.DATA, false)); + put(8, new RowInfo(8, RowCaption.DESTINATION_CONTAINER_TYPE, "Container type", RowType.DATA, false)); + put(9, new RowInfo(9, RowCaption.DESTINATION_CONTAINER_LIMIT, "Limiting factor", RowType.DATA, false)); + } + }; + private final ReportingService reportingService; + private final CellStyleProvider cellStyleProvider; + private final HeaderGenerator headerGenerator; + private final MaterialRepository materialRepository; + private final PropertyRepository propertyRepository; + + public ExcelReportingService(ReportingService reportingService, CellStyleProvider cellStyleProvider, HeaderGenerator headerGenerator, MaterialRepository materialRepository, PropertyRepository propertyRepository) { this.reportingService = reportingService; - this.headerCellStyleProvider = headerCellStyleProvider; + this.cellStyleProvider = cellStyleProvider; this.headerGenerator = headerGenerator; + this.materialRepository = materialRepository; + this.propertyRepository = propertyRepository; } - public ByteArrayResource generateExcelReport(Integer materialId, List nodeIds, List userNodeIds ) { + public ByteArrayResource generateExcelReport(List materialIds, List nodeIds, List userNodeIds) { + var reportingProperty = propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.REPORTING).orElseThrow(); + boolean includeAirfreight = reportingProperty.getCurrentValue().equals("MEK_C"); + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - var reports = reportingService.getReport(materialId, nodeIds, userNodeIds); - Workbook workbook = new XSSFWorkbook(); - Sheet sheet = workbook.createSheet("report"); - - CellStyle headerStyle = headerCellStyleProvider.createHeaderCellStyle(workbook); - - ArrayList headers = new ArrayList<>(); - headers.add(""); - headers.addAll(reports.stream().map(ReportDTO::getSupplier).map(NodeDTO::getName).toList()); - - headerGenerator.generateHeader(sheet, headers.toArray(String[]::new), headerStyle); - - List flatterers = reports.stream().map(ReportFlattener::new).toList(); + CellStyle headerStyle = cellStyleProvider.createHeaderCellStyle(workbook); + var styles = cellStyleProvider.createCellStyles(workbook); - while(true) { - boolean hasData = false; - boolean headerWritten = false; - - var row = sheet.createRow(sheet.getLastRowNum() + 1); - int cellIdx = 1 /* 0 is the header column */; - for(ReportFlattener flattener : flatterers) { - - Cell cell = row.createCell(cellIdx); - - - if(flattener.hasData(row.getRowNum())) { - if(!headerWritten) { - row.createCell(0).setCellValue(flattener.getHeader(row.getRowNum())); - headerWritten = true; - } - cell.setCellValue(flattener.getCell(row.getRowNum())); - hasData = true; - } - - cellIdx++; - } - - if(!hasData) break; - } - - headerGenerator.fixWidth(sheet, headers.toArray(String[]::new)); + fillSummarySheet(workbook, headerStyle, materialIds, nodeIds, userNodeIds, styles); + materialIds.forEach(id -> fillExcelSheet(workbook, headerStyle, id, nodeIds, userNodeIds, styles, includeAirfreight)); // Return the Excel file as an InputStreamSource workbook.write(outputStream); return new ByteArrayResource(outputStream.toByteArray()); - } catch ( - IOException e) { + } catch (IOException e) { throw new RuntimeException("Failed to generate template", e); } } - private static class ReportFlattener { + private void fillSummarySheet(Workbook workbook, CellStyle headerStyle, List materialIds, List nodeIds, List userNodeIds, Map> styles) { + Sheet sheet = workbook.createSheet("Summary"); - private static final String SUPPLIER_NAME = "Supplier"; - private static final String SUPPLIER_ADDRESS = "Address"; + int row = 1; - private static final String DESTINATION_NAME = "Destination"; - private static final String DESTINATION_ADDRESS = "Address"; - - private static final String DESTINATION_QUANTITY = "Annual quantity"; - private static final String DESTINATION_HS_CODE = "HS code"; - private static final String DESTINATION_TARIFF_RATE = "Tariff rate"; - private static final String DESTINATION_OVERSHARE = "Oversea share"; - private static final String DESTINATION_AIR_FREIGHT_SHARE = "Air freight share"; - private static final String DESTINATION_TRANSPORT_TIME = "Transport time"; - private static final String DESTINATION_SAFETY_STOCK = "Safety stock"; - - private static final String DESTINATION_WIDTH = "HU Width"; - private static final String DESTINATION_HEIGHT = "HU Height"; - private static final String DESTINATION_LENGTH = "HU Length"; - private static final String DESTINATION_WEIGHT = "HU Weight"; - private static final String DESTINATION_HU_UNIT_COUNT = "HU Unit count"; - - private static final String DESTINATION_WEIGHT_UNIT = "HU Weight unit"; - private static final String DESTINATION_DIMENSION_UNIT = "HU Unit"; - - private static final String DESTINATION_CONTAINER_LAYER = "Container layers"; - private static final String DESTINATION_CONTAINER_UNIT_COUNT = "Container unit count"; - private static final String DESTINATION_CONTAINER_UTILIZATION = "Container utilization"; - private static final String DESTINATION_CONTAINER_TYPE = "Container type"; - private static final String DESTINATION_CONTAINER_WEIGHT_EXCEEDED = "Container weight exceeded"; - private static final String DESTINATION_CONTAINER_RATE = "Container rate"; - private static final String DESTINATION_MIXED = "Mixed"; - - - private final ReportDTO report; - private final List data = new ArrayList<>(); - private final List dataHeader = new ArrayList<>(); - - public ReportFlattener(ReportDTO report) { - this.report = report; - flatten(); + for (Integer materialId : materialIds) { + for (var mapper : reportingService.getReport(materialId, nodeIds, userNodeIds).stream().map(report -> new SummaryMapper(report, styles)).toList()) { + mapper.map(sheet, row++); + } } - private void flatten() { + headerGenerator.generateHeader(sheet, ReportSummaryHeader.class, headerStyle); + headerGenerator.fixWidth(sheet, ReportSummaryHeader.class); - addData(SUPPLIER_NAME, report.getSupplier().getName()); - addData(SUPPLIER_ADDRESS, report.getSupplier().getAddress()); + } - // TODO: hardcoded (otherwise values wont match - report.getCost().keySet().forEach(costName -> addData(costName, report.getCost().get(costName))); - report.getOverview().keySet().forEach(riskName -> addData(riskName, report.getOverview().get(riskName))); + private void fillExcelSheet(Workbook workbook, CellStyle headerStyle, Integer materialId, List nodeIds, List userNodeIds, Map> styles, boolean includeAirfreight) { + var material = materialRepository.getById(materialId).orElseThrow(); + var reports = reportingService.getReport(materialId, nodeIds, userNodeIds); - commonPremisses(report.getPremises()); + Sheet sheet = workbook.createSheet(material.getPartNumber()); - report.getDestinations().forEach(this::flattenDestination); + ArrayList headers = new ArrayList<>(); + headers.add(""); + headers.addAll(reports.stream().flatMap(report -> Stream.of(report.getSupplier().getName(), null)).toList()); + + headerGenerator.generateHeader(sheet, headers.toArray(String[]::new), headerStyle, true); + List destinations = collectDestinations(reports); + + var row = 1; + var mapper = new ReportRowMapper(reports.stream().map(report -> new ReportMapper(report, destinations, styles, includeAirfreight)).toList(), destinations, styles, rowSequence, destinationRowSequence); + + boolean finished = false; + while (!finished) { + finished = !mapper.map(sheet, row++); } - private void commonPremisses(ReportPremisesDTO premises) { - addData(DESTINATION_HS_CODE, premises.getHsCode()); - addData(DESTINATION_TARIFF_RATE, premises.getTariffRate().toString()); + headerGenerator.fixWidth(sheet, headers.toArray(String[]::new), true); - addData(DESTINATION_WIDTH, premises.getWidth().toString()); - addData(DESTINATION_HEIGHT, premises.getHeight().toString()); - addData(DESTINATION_LENGTH, premises.getLength().toString()); - addData(DESTINATION_DIMENSION_UNIT, premises.getDimensionUnit().toString()); - addData(DESTINATION_WEIGHT, premises.getWeight().toString()); - addData(DESTINATION_WEIGHT_UNIT, premises.getWeightUnit().toString()); - addData(DESTINATION_HU_UNIT_COUNT, premises.getHuUnitCount().toString()); + } + + private List collectDestinations(List reports) { + Map destinations = new HashMap<>(); + + reports.forEach(r -> r.getDestinations().forEach(d -> { + if (!destinations.containsKey(d.getDestination().getId())) + destinations.put(d.getDestination().getId(), d.getDestination()); + })); + + return destinations.values().stream().toList(); + } + + private enum RowCaption { + SUPPLIER, ADDRESS, SUMMARY, SUMMARY_MEK_A, SUMMARY_LOGISTICS_COST, SUMMARY_MEK_B, WEIGHTED_COST_BREAKDOWN, BREAKDOWN_MEK_A, BREAKDOWN_TRANSPORT, BREAKDOWN_HANDLING, BREAKDOWN_STORAGE, BREAKDOWN_REPACKAGING, BREAKDOWN_DISPOSAL, BREAKDOWN_CAPITAL, BREAKDOWN_CUSTOM, BREAKDOWN_FCA_FEES, BREAKDOWN_TOTAL, FLUCTUATION, FLUCTUATION_CURRENT, FLUCTUATION_OPPORTUNITY, FLUCTUATION_RISK, ASSUMPTIONS, ASSUMPTIONS_MATERIAL, ASSUMPTIONS_PART_NUMBER, ASSUMPTIONS_HS_CODE, ASSUMPTIONS_TARIFF_RATE, ASSUMPTIONS_OVERSEA_SHARE, ASSUMPTIONS_AIR_FREIGHT_SHARE, ASSUMPTIONS_SAFETY_STOCK, HANDLING_UNIT, HANDLING_UNIT_LENGTH, HANDLING_UNIT_WIDTH, HANDLING_UNIT_HEIGHT, HANDLING_UNIT_DIMENSION_UNIT, HANDLING_UNIT_WEIGHT, HANDLING_UNIT_DIMENSION_WEIGHT, HANDLING_UNIT_PIECES, HANDLING_UNIT_MIXED, DESTINATION, DESTINATION_ROUTE, DESTINATION_ANNUAL_QUANTITY, DESTINATION_TRANSPORT_TIME, DESTINATION_CONTAINER, DESTINATION_CONTAINER_LAYER, DESTINATION_CONTAINER_UNIT_COUNT, DESTINATION_CONTAINER_TYPE, DESTINATION_CONTAINER_LIMIT, DESTINATION_GENERAL; + } + + + private enum RowType { + TITLE, SUB_TITLE, HEADER, SUB_HEADER, DATA, SPLIT_DATA + } + + private record RowInfo(int rowIdx, RowCaption captionType, String caption, RowType type, boolean hideCaption) { + } + + private record ReportRowMapper(List mapper, List destinations, + Map> styles, + Map rowSequence, Map destinationRowSequence) { + + + public boolean map(Sheet sheet, int rowIndex) { + + RowInfo info = getRowInfo(rowIndex); + int destinationIdx = getDestinationIdx(rowIndex); + + Row row = sheet.createRow(rowIndex); + + Cell cell = row.createCell(0); + + if (info.captionType == RowCaption.DESTINATION) { + cell.setCellValue(destinations.get(destinationIdx).getName()); + } else { + cell.setCellValue(info.hideCaption() ? "" : info.caption()); + } + + cell.setCellStyle(getStyle(info)); + + int columnIdx = 1; + for (var curMapper : mapper) { + curMapper.map(sheet, row, rowIndex, columnIdx, info, destinationIdx); + columnIdx += 2; + } + + return rowIndex < getLastIndex(); } - private void flattenDestination(ReportDestinationDTO destination) { - - var hasMainRun = destination.getSections().stream().anyMatch(s -> s.getTransportType().equals(TransportType.RAIL) || s.getTransportType().equals(TransportType.SEA)); - - addData(DESTINATION_NAME, destination.getDestination().getName()); - addData(DESTINATION_ADDRESS, destination.getDestination().getAddress()); - - addData(DESTINATION_QUANTITY, destination.getAnnualQuantity().toString()); - - addData(DESTINATION_OVERSHARE, destination.getOverseaShare().toString()); - - if(destination.getAirFreightShare() != null) - addData(DESTINATION_AIR_FREIGHT_SHARE, destination.getAirFreightShare().toString()); - addData(DESTINATION_TRANSPORT_TIME, destination.getTransportTime().toString()); - addData(DESTINATION_SAFETY_STOCK, destination.getSafetyStock().toString()); - - - addData(DESTINATION_CONTAINER_LAYER, !hasMainRun ? "-" : destination.getLayer().toString()); - - addData(DESTINATION_CONTAINER_UNIT_COUNT, !hasMainRun ? "-" : destination.getUnitCount().toString()); - addData(DESTINATION_CONTAINER_UTILIZATION, !hasMainRun ? "-" : destination.getUtilization().toString()); - addData(DESTINATION_CONTAINER_TYPE, !hasMainRun ? "-" : destination.getType().toString()); - addData(DESTINATION_CONTAINER_WEIGHT_EXCEEDED, !hasMainRun ? "-" : destination.getWeightExceeded().toString()); - addData(DESTINATION_CONTAINER_RATE, !hasMainRun ? "-" : destination.getRate().toString()); - addData(DESTINATION_MIXED, !hasMainRun ? "-" : destination.getMixed().toString()); - + private int getLastIndex() { + return rowSequence.size() + (destinations.size() * destinationRowSequence.size()) - 1; } - private void addData(String header, String data) { - this.dataHeader.add(header); - this.data.add(data); + private CellStyle getStyle(RowInfo info) { + return switch (info.type) { + case TITLE -> + this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.TITLE); + case SUB_TITLE, DATA, SPLIT_DATA -> + this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + case HEADER -> + this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.HEADER); + case SUB_HEADER -> + this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.SUB_HEADER); + }; } - private void addData(String header, ReportEntryDTO data) { - this.dataHeader.add(header); - this.data.add(data.getTotal().toString()); // + " (" + data.getPercentage().doubleValue()*100 + "%)"); + private int getDestinationIdx(int rowIndex) { + + if (rowIndex < rowSequence.size()) return -1; + + return (rowIndex - rowSequence.size()) / destinationRowSequence.size(); } - public String getCell(int rowIdx) { - return data.get(rowIdx); - } + private RowInfo getRowInfo(int rowIndex) { + if (rowIndex < rowSequence.size()) return rowSequence.get(rowIndex); - public String getHeader(int rowIdx) { - return dataHeader.get(rowIdx); - } - - public boolean hasData(int index) { - return data.size() > index; + return destinationRowSequence.get((rowIndex - rowSequence.size()) % destinationRowSequence.size()); } } + + private record ReportMapper(ReportDTO report, List destinations, + Map> styles, + boolean includeAirfreight) { + + public void map(Sheet sheet, Row row, int rowIdx, int columnIdx, RowInfo info, int destinationIdx) { + + + boolean mergeCells = false; + CellStyle selectedStyle = null; + + /* ********************************** + * Cell 1 + * **********************************/ + + Cell cell = row.createCell(columnIdx); + + switch (info.type) { + case TITLE -> { + cell.setCellValue(report.getSupplier().getName()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.TITLE); + mergeCells = true; + } + case SUB_TITLE -> { + cell.setCellValue(report.getSupplier().getAddress()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + mergeCells = true; + } + case HEADER -> { + cell.setCellValue(""); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.HEADER); + mergeCells = true; + } + case SUB_HEADER -> { + cell.setCellValue(""); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.SUB_HEADER); + mergeCells = true; + } + case DATA -> { + selectedStyle = setCellValue(cell, info, destinationIdx, false); + mergeCells = true; + } + case SPLIT_DATA -> { + selectedStyle = setCellValue(cell, info, destinationIdx, false); + } + + } + + if (selectedStyle != null) { + cell.setCellStyle(selectedStyle); + } + + /* ********************************** + * Cell 2 + * **********************************/ + + if (mergeCells) { + var merged = new CellRangeAddress(rowIdx, rowIdx, columnIdx, columnIdx + 1); + + if (selectedStyle != null) { + RegionUtil.setBorderBottom(selectedStyle.getBorderBottom(), merged, sheet); + RegionUtil.setBorderTop(selectedStyle.getBorderTop(), merged, sheet); + RegionUtil.setBorderLeft(selectedStyle.getBorderLeft(), merged, sheet); + RegionUtil.setBorderRight(selectedStyle.getBorderRight(), merged, sheet); + RegionUtil.setBottomBorderColor(selectedStyle.getBottomBorderColor(), merged, sheet); + RegionUtil.setTopBorderColor(selectedStyle.getTopBorderColor(), merged, sheet); + RegionUtil.setLeftBorderColor(selectedStyle.getLeftBorderColor(), merged, sheet); + RegionUtil.setRightBorderColor(selectedStyle.getRightBorderColor(), merged, sheet); + } + sheet.addMergedRegion(merged); + } else { + Cell cell2nd = row.createCell(columnIdx + 1); + var selectedStyle2nCell = setCellValue(cell2nd, info, destinationIdx, true); + if (selectedStyle2nCell != null) { + cell2nd.setCellStyle(selectedStyle2nCell); + } + } + + } + + private CellStyle setCellValue(Cell cell, RowInfo info, int destinationIdx, boolean secondColumn) { + + CellStyle selectedStyle = null; + + switch (info.captionType) { + case SUPPLIER -> { + cell.setCellValue(report.getSupplier().getName()); + } + case ADDRESS -> { + cell.setCellValue(report.getSupplier().getAddress()); + } + case SUMMARY, WEIGHTED_COST_BREAKDOWN, FLUCTUATION, ASSUMPTIONS, ASSUMPTIONS_MATERIAL, HANDLING_UNIT, + DESTINATION, DESTINATION_CONTAINER, DESTINATION_GENERAL -> { + cell.setCellValue(""); + } + case SUMMARY_MEK_A -> { + if (!secondColumn) { + cell.setCellValue(report.getOverview().get("mek_a").getTotal().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.CURRENCY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } else { + cell.setCellValue(report.getOverview().get("mek_a").getPercentage().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + } + case SUMMARY_LOGISTICS_COST -> { + if (!secondColumn) { + cell.setCellValue(report.getOverview().get("logistics").getTotal().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.CURRENCY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } else { + cell.setCellValue(report.getOverview().get("logistics").getPercentage().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + } + case SUMMARY_MEK_B, FLUCTUATION_CURRENT -> { + if (!secondColumn) { + cell.setCellValue(report.getOverview().get("mek_b").getTotal().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.CURRENCY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } else { + cell.setCellValue(report.getOverview().get("mek_b").getPercentage().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + } + case BREAKDOWN_MEK_A -> { + if (!secondColumn) { + cell.setCellValue(report.getCost().get("mek_a").getTotal().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.CURRENCY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } else { + cell.setCellValue(report.getCost().get("mek_a").getPercentage().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + } + case BREAKDOWN_TRANSPORT -> { + if (!secondColumn) { + cell.setCellValue(report.getCost().get("transport").getTotal().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.CURRENCY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } else { + cell.setCellValue(report.getCost().get("transport").getPercentage().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + } + case BREAKDOWN_HANDLING -> { + if (!secondColumn) { + cell.setCellValue(report.getCost().get("handling").getTotal().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.CURRENCY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } else { + cell.setCellValue(report.getCost().get("handling").getPercentage().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + } + case BREAKDOWN_STORAGE -> { + if (!secondColumn) { + cell.setCellValue(report.getCost().get("storage").getTotal().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.CURRENCY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } else { + cell.setCellValue(report.getCost().get("storage").getPercentage().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + } + case BREAKDOWN_REPACKAGING -> { + if (!secondColumn) { + cell.setCellValue(report.getCost().get("repacking").getTotal().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.CURRENCY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } else { + cell.setCellValue(report.getCost().get("repacking").getPercentage().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + } + case BREAKDOWN_DISPOSAL -> { + if (!secondColumn) { + cell.setCellValue(report.getCost().get("disposal").getTotal().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.CURRENCY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } else { + cell.setCellValue(report.getCost().get("disposal").getPercentage().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + } + case BREAKDOWN_CAPITAL -> { + if (!secondColumn) { + cell.setCellValue(report.getCost().get("capital").getTotal().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.CURRENCY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } else { + cell.setCellValue(report.getCost().get("capital").getPercentage().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + } + case BREAKDOWN_CUSTOM -> { + if (!secondColumn) { + cell.setCellValue(report.getCost().get("custom").getTotal().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.CURRENCY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } else { + cell.setCellValue(report.getCost().get("custom").getPercentage().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + } + case BREAKDOWN_FCA_FEES -> { + if (!secondColumn) { + cell.setCellValue(report.getCost().get("fca_fees").getTotal().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.CURRENCY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } else { + cell.setCellValue(report.getCost().get("fca_fees").getPercentage().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + } + case BREAKDOWN_TOTAL -> { + if (!secondColumn) { + cell.setCellValue(report.getCost().get("total").getTotal().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.CURRENCY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } else { + cell.setCellValue(report.getCost().get("total").getPercentage().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + } + case FLUCTUATION_OPPORTUNITY -> { + if (!secondColumn) { + cell.setCellValue(report.getOverview().get("opportunity_scenario").getTotal().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.CURRENCY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } else { + cell.setCellValue(report.getOverview().get("opportunity_scenario").getPercentage().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + } + case FLUCTUATION_RISK -> { + if (!secondColumn) { + cell.setCellValue(report.getOverview().get("risk_scenario").getTotal().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.CURRENCY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } else { + cell.setCellValue(report.getOverview().get("risk_scenario").getPercentage().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + } + case ASSUMPTIONS_PART_NUMBER -> { + cell.setCellValue(report.getMaterial().getPartNumber()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + case ASSUMPTIONS_HS_CODE -> { + cell.setCellValue(report.getPremises().getHsCode()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + case ASSUMPTIONS_TARIFF_RATE -> { + cell.setCellValue(report.getPremises().getTariffRate().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + case ASSUMPTIONS_OVERSEA_SHARE -> { + cell.setCellValue(report.getPremises().getOverseaShare().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + case ASSUMPTIONS_AIR_FREIGHT_SHARE -> { + if (includeAirfreight) { + cell.setCellValue(report.getPremises().getAirFreightShare().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.PERCENTAGE).get(CellStyleProvider.PredefinedColors.NEUTRAL); + + } else { + cell.setCellValue("-"); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + } + case ASSUMPTIONS_SAFETY_STOCK -> { + cell.setCellValue(report.getPremises().getSafetyStock().doubleValue()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + case HANDLING_UNIT_LENGTH -> { + cell.setCellValue(report.getPremises().getLength()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + case HANDLING_UNIT_WIDTH -> { + cell.setCellValue(report.getPremises().getWidth()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + case HANDLING_UNIT_HEIGHT -> { + cell.setCellValue(report.getPremises().getHeight()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + case HANDLING_UNIT_DIMENSION_UNIT -> { + cell.setCellValue(report.getPremises().getDimensionUnit().getDisplayedName()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + case HANDLING_UNIT_WEIGHT -> { + cell.setCellValue(report.getPremises().getWeight()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + case HANDLING_UNIT_DIMENSION_WEIGHT -> { + cell.setCellValue(report.getPremises().getWeightUnit().getDisplayedName()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + case HANDLING_UNIT_PIECES -> { + cell.setCellValue(report.getPremises().getHuUnitCount()); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + case HANDLING_UNIT_MIXED -> { + cell.setCellValue(report.getPremises().getMixable() ? "Yes" : "No"); + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + case DESTINATION_ROUTE -> { + var dest = getDestination(destinationIdx); + + if (dest == null) { + cell.setCellValue(""); + } else { + cell.setCellValue(getRoute(dest.getSections())); + } + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + case DESTINATION_ANNUAL_QUANTITY -> { + var dest = getDestination(destinationIdx); + + if (dest == null) { + cell.setCellValue(""); + } else { + cell.setCellValue(dest.getAnnualQuantity()); + } + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + + } + case DESTINATION_TRANSPORT_TIME -> { + var dest = getDestination(destinationIdx); + + if (dest == null) { + cell.setCellValue(""); + } else { + cell.setCellValue(dest.getTransportTime()); + } + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + } + case DESTINATION_CONTAINER_LAYER -> { + var dest = getDestination(destinationIdx); + + if (dest == null) { + cell.setCellValue(""); + } else { + cell.setCellValue(dest.getLayer()); + } + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + + } + case DESTINATION_CONTAINER_UNIT_COUNT -> { + var dest = getDestination(destinationIdx); + + if (dest == null) { + cell.setCellValue(""); + } else { + cell.setCellValue(dest.getUnitCount().intValue() * report.getPremises().getHuUnitCount()); + } + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + + } + case DESTINATION_CONTAINER_TYPE -> { + var dest = getDestination(destinationIdx); + + if (dest == null) { + cell.setCellValue(""); + } else { + cell.setCellValue(dest.getType().getDescription()); + } + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + + } + case DESTINATION_CONTAINER_LIMIT -> { + var dest = getDestination(destinationIdx); + + if (dest == null) { + cell.setCellValue(""); + } else { + cell.setCellValue(dest.getWeightExceeded() ? "Weight" : "Volume"); + } + selectedStyle = this.styles.get(CellStyleProvider.TextFormat.TEXT_ONLY).get(CellStyleProvider.PredefinedColors.NEUTRAL); + + } + } + + return selectedStyle; + } + + private String getRoute(List sections) { + + if (sections == null || sections.isEmpty()) + return ""; + + var firstNode = sections.getFirst().getFromNode(); + + StringBuilder route = new StringBuilder(firstNode.getExternalMappingId() == null ? shortened(firstNode.getName()) : firstNode.getExternalMappingId()); + + sections.forEach(s -> { + route.append(" > "); + route.append(s.getToNode().getExternalMappingId()); + }); + + return route.toString(); + } + + private String shortened(String name) { + if (name == null || name.length() < 10) + return name; + + return name.substring(0, 7) + "..."; + } + + private ReportDestinationDTO getDestination(int destinationIdx) { + if (destinationIdx == -1 || destinations.size() <= destinationIdx) + return null; + + var node = destinations.get(destinationIdx); + + if (node == null) return null; + + return report.getDestinations().stream().filter(d -> Objects.equals(d.getDestination().getId(), node.getId())).findFirst().orElse(null); + + } + + } + + private record SummaryMapper(ReportDTO report, + Map> styles) { + + public void map(Sheet sheet, int rowIdx) { + Row row = sheet.createRow(rowIdx); + + createStringCell(row, ReportSummaryHeader.MATERIAL, report.getMaterial().getPartNumber()); + createStringCell(row, ReportSummaryHeader.SUPPLIER, report.getSupplier().getName()); + + var currencyStyle = styles.get(CellStyleProvider.TextFormat.CURRENCY); + var percentStyle = styles.get(CellStyleProvider.TextFormat.PERCENTAGE); + + createSplitCell(row, ReportSummaryHeader.MEK_A, report.getOverview().get("mek_a"), currencyStyle.get(CellStyleProvider.PredefinedColors.NEUTRAL), percentStyle.get(CellStyleProvider.PredefinedColors.NEUTRAL)); + createSplitCell(row, ReportSummaryHeader.LOGISTICS_COST, report.getOverview().get("logistics"), currencyStyle.get(CellStyleProvider.PredefinedColors.LIGHT_BLUE_1), percentStyle.get(CellStyleProvider.PredefinedColors.LIGHT_BLUE_2)); + createSplitCell(row, ReportSummaryHeader.MEK_B, report.getOverview().get("mek_b"), currencyStyle.get(CellStyleProvider.PredefinedColors.GREEN_1), percentStyle.get(CellStyleProvider.PredefinedColors.GREEN_2)); + + createNumberCell(row, ReportSummaryHeader.TRANSPORT, report.getCost().get("transport").getTotal(), currencyStyle.get(CellStyleProvider.PredefinedColors.NEUTRAL)); + createNumberCell(row, ReportSummaryHeader.HANDLING, report.getCost().get("handling").getTotal(), currencyStyle.get(CellStyleProvider.PredefinedColors.NEUTRAL)); + createNumberCell(row, ReportSummaryHeader.STORAGE, report.getCost().get("storage").getTotal(), currencyStyle.get(CellStyleProvider.PredefinedColors.NEUTRAL)); + createNumberCell(row, ReportSummaryHeader.REPACKAGING, report.getCost().get("repacking").getTotal(), currencyStyle.get(CellStyleProvider.PredefinedColors.NEUTRAL)); + createNumberCell(row, ReportSummaryHeader.DISPOSAL, report.getCost().get("disposal").getTotal(), currencyStyle.get(CellStyleProvider.PredefinedColors.NEUTRAL)); + createNumberCell(row, ReportSummaryHeader.CAPITAL, report.getCost().get("capital").getTotal(), currencyStyle.get(CellStyleProvider.PredefinedColors.NEUTRAL)); + createNumberCell(row, ReportSummaryHeader.CUSTOM, report.getCost().get("custom").getTotal(), currencyStyle.get(CellStyleProvider.PredefinedColors.NEUTRAL)); + createNumberCell(row, ReportSummaryHeader.FCA_FEE, report.getCost().get("fca_fees").getTotal(), currencyStyle.get(CellStyleProvider.PredefinedColors.NEUTRAL)); + + if (report.getCost().containsKey("air_freight_cost")) + createNumberCell(row, ReportSummaryHeader.AIR_FREIGHT, report.getCost().get("air_freight_cost").getTotal(), currencyStyle.get(CellStyleProvider.PredefinedColors.NEUTRAL)); + else createStringCell(row, ReportSummaryHeader.AIR_FREIGHT, "-"); + + } + + private void createSplitCell(Row row, ReportSummaryHeader header, ReportEntryDTO value, CellStyle cellStyleHighlight1, CellStyle cellStyleHighlight2) { + Cell cell = row.createCell(header.getColumn()); + cell.setCellValue(value.getTotal().doubleValue()); + cell.setCellStyle(cellStyleHighlight1); + + Cell percentageCell = row.createCell(header.getColumn() + 1); + percentageCell.setCellValue(value.getPercentage().doubleValue()); + percentageCell.setCellStyle(cellStyleHighlight2); + } + + private void createStringCell(Row row, ReportSummaryHeader header, String value) { + Cell cell = row.createCell(header.getColumn()); + cell.setCellValue(value); + } + + private void createNumberCell(Row row, ReportSummaryHeader header, Number value, CellStyle style) { + Cell cell = row.createCell(header.getColumn()); + cell.setCellValue(value.doubleValue()); + cell.setCellStyle(style); + } + + + } } diff --git a/src/main/java/de/avatic/lcc/service/transformer/report/ReportTransformer.java b/src/main/java/de/avatic/lcc/service/transformer/report/ReportTransformer.java index 3f23b41..08b45fc 100644 --- a/src/main/java/de/avatic/lcc/service/transformer/report/ReportTransformer.java +++ b/src/main/java/de/avatic/lcc/service/transformer/report/ReportTransformer.java @@ -98,7 +98,7 @@ public class ReportTransformer { reportDTO.setStartDate(period.startDate); reportDTO.setEndDate(period.endDate); - reportDTO.setPremises(getPremisesDTO(job, premise)); + reportDTO.setPremises(getPremisesDTO(job, premise, destinations, includeAirfreight)); if (!destinations.isEmpty()) { @@ -171,9 +171,11 @@ public class ReportTransformer { return new WeightedTotalCosts(totalPreRunCost, totalMainRunCost, totalPostRunCost, totalD2D, totalCost); } - private ReportPremisesDTO getPremisesDTO(CalculationJob job, Premise premise) { + private ReportPremisesDTO getPremisesDTO(CalculationJob job, Premise premise, List destinations, boolean includeAirfreight) { ReportPremisesDTO premisesDTO = new ReportPremisesDTO(); + var destination = destinations.getFirst(); + var dimensionUnit = premise.getHuDisplayedDimensionUnit(); var weightUnit = premise.getHuDisplayedWeightUnit(); @@ -188,6 +190,14 @@ public class ReportTransformer { premisesDTO.setHsCode(premise.getHsCode()); premisesDTO.setTariffRate(premise.getTariffRate()); + if (includeAirfreight) + premisesDTO.setAirFreightShare(destination.getAirFreightShare().doubleValue()); + + premisesDTO.setOverseaShare(premise.getOverseaShare()); + + premisesDTO.setSafetyStock(destination.getSafetyStockInDays().intValue()); + + premisesDTO.setMixable(premise.getHuMixable()); return premisesDTO; } @@ -217,15 +227,8 @@ public class ReportTransformer { destinationDTO.setLayer(destination.getLayerCount()); - destinationDTO.setOverseaShare(premise.getOverseaShare().doubleValue()); - destinationDTO.setSafetyStock(destination.getSafetyStockInDays().intValue()); destinationDTO.setTransportTime(destination.getTotalTransitTime().doubleValue()); - if (includeAirfreight) - destinationDTO.setAirFreightShare(destination.getAirFreightShare().doubleValue()); - - - destinationDTO.setMixed(premise.getHuMixable()); destinationDTO.setRate(mainRun == null ? 0 : mainRun.getRate()); destinationDTO.setType(destination.getContainerType()); destinationDTO.setUtilization(destination.getContainerUtilization()); -- 2.45.3 From 1be35b5a8dd157b3cc0e28c8f41573b8b59e529e Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 16 Dec 2025 22:16:03 +0100 Subject: [PATCH 019/104] Bugfix: shipping frequency custom calculation. Stacking in container calcualtion --- .../calculation/execution/CalculationExecutionService.java | 4 ++-- .../execution/steps/ContainerCalculationService.java | 4 ++-- .../execution/steps/CustomCostCalculationService.java | 4 ++-- .../execution/steps/RouteSectionCostCalculationService.java | 2 ++ 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/CalculationExecutionService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/CalculationExecutionService.java index b24a44b..cd85c23 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/CalculationExecutionService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/CalculationExecutionService.java @@ -142,7 +142,7 @@ public class CalculationExecutionService { BigDecimal leadTime = null; if (destination.getD2d()) { - var containerCalculation = containerCalculationService.doCalculation(setId, premiseToHuService.createHuFromPremise(premise), ContainerType.FEU, premise.getHuMixable()); + var containerCalculation = containerCalculationService.doCalculation(setId, premiseToHuService.createHuFromPremise(premise), ContainerType.FEU, premise.getHuMixable(), premise.getHuStackable()); sections = List.of(new SectionInfo(null, routeSectionCostCalculationService.doD2dCalculation(setId, periodId, premise, destination, containerCalculation), containerCalculation)); leadTime = BigDecimal.valueOf(destination.getLeadTimeD2d()); usedContainerType = ContainerType.FEU; @@ -246,7 +246,7 @@ public class CalculationExecutionService { // Get container calculation for (var containerType : ContainerType.values()) { - containerCalculation.put(containerType, containerCalculationService.doCalculation(setId, hu, containerType, premise.getHuMixable())); + containerCalculation.put(containerType, containerCalculationService.doCalculation(setId, hu, containerType, premise.getHuMixable(), premise.getHuStackable())); } for (var containerType : ContainerType.values()) { diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ContainerCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ContainerCalculationService.java index 9411e31..32da301 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ContainerCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ContainerCalculationService.java @@ -49,7 +49,7 @@ public class ContainerCalculationService { * @param containerType The type of container to be loaded * @return ContainerCalculationResult containing loading pattern and capacity information */ - public ContainerCalculationResult doCalculation(Integer setId, PackagingDimension hu, ContainerType containerType, boolean mixable) { + public ContainerCalculationResult doCalculation(Integer setId, PackagingDimension hu, ContainerType containerType, boolean mixable, boolean stackable) { var weightInKg = BigDecimal.valueOf(WeightUnit.KG.convertFromG(hu.getWeight())); var maxContainerLoad = BigDecimal.valueOf(getMaxContainerLoad(containerType, setId)); @@ -60,7 +60,7 @@ public class ContainerCalculationService { var solutionHorizontal = solveLayer(SolutionType.HORIZONTAL, dimensions, containerType.getLength(), containerType.getWidth()); var solutionVertical = solveLayer(SolutionType.VERTICAL, dimensions, containerType.getWidth(), containerType.getLength()); var bestSolution = solutionHorizontal.getTotal() < solutionVertical.getTotal() ? solutionVertical : solutionHorizontal; - int layers = mixable ? getLayerCount(dimensions, containerType) : 1; + int layers = stackable ? getLayerCount(dimensions, containerType) : 1; if(PalletType.EURO_PALLET.fitsOn(hu) && bestSolution.getTotal() < containerType.getPalletCount(PalletType.EURO_PALLET)) { return new ContainerCalculationResult(Math.min(containerType.getPalletCount(PalletType.EURO_PALLET)*layers,maxUnitByWeight), layers, null, (containerType.getPalletCount(PalletType.EURO_PALLET)*layers) > maxUnitByWeight, containerType, dimensions, maxContainerLoad.intValueExact()); diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/CustomCostCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/CustomCostCalculationService.java index 55bfd61..2dd079c 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/CustomCostCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/CustomCostCalculationService.java @@ -62,7 +62,7 @@ public class CustomCostCalculationService { var transportationRiskCost = relevantSections.stream().map(s -> s.result().getAnnualRiskCost()).reduce(BigDecimal.ZERO, BigDecimal::add); - double huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(relevantSections.getFirst().containerResult().getHuUnitCount()),2, RoundingMode.HALF_UP).doubleValue(); + double huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(premise.getHuUnitCount()),0, RoundingMode.CEILING).doubleValue(); return getCustomCalculationResult(setId, premise, destination, getContainerShare(premise, relevantSections.getFirst().containerResult()), huAnnualAmount, transportationCost, transportationChanceCost, transportationRiskCost); } @@ -87,7 +87,7 @@ public class CustomCostCalculationService { var customValue = materialCost.add(fcaFee).add(transportationCost); var customDuties = customValue.multiply(tariffRate); - var annualCustomFee = BigDecimal.valueOf(shippingFrequency).multiply(BigDecimal.valueOf(customFee)).multiply(containerShare); + var annualCustomFee = BigDecimal.valueOf(shippingFrequency).multiply(BigDecimal.valueOf(customFee)); var annualCost = customDuties.add(annualCustomFee); var customRiskValue = materialCost.add(fcaFee).add(transportationRiskCost); diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java index daf571a..cfe5b39 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java @@ -229,6 +229,8 @@ public class RouteSectionCostCalculationService { volumePrice = cbmRate.divide(totalVolumeUtilization, 10, RoundingMode.HALF_UP); weightPrice = weightRate.divide(totalWeightUtilization, 10, RoundingMode.HALF_UP); utilization = weightExceeded ? totalWeightUtilization : totalVolumeUtilization; + //TODO: wenn shippingfreq > annual hu -> shippingfreq * containerprice. + // gleiches für containercalculation * shippingfreq < annual hu ammount. } return new PriceCalculationResult(volumePrice, weightPrice, utilization); -- 2.45.3 From 9ac3cb7815761df6d02848b5e47798171795ceea Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 17 Dec 2025 09:42:19 +0100 Subject: [PATCH 020/104] Bugfix: if hu amount is less than min shipping frequency, fix total utilization accordingly --- .../steps/CustomCostCalculationService.java | 15 ++------ .../RouteSectionCostCalculationService.java | 34 +++++++++++++++---- .../ShippingFrequencyCalculationService.java | 8 ++--- 3 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/CustomCostCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/CustomCostCalculationService.java index 2dd079c..1e2686d 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/CustomCostCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/CustomCostCalculationService.java @@ -37,17 +37,6 @@ public class CustomCostCalculationService { this.shippingFrequencyCalculationService = shippingFrequencyCalculationService; } - private BigDecimal getContainerShare(Premise premise, ContainerCalculationResult containerCalculationResult) { - var weightExceeded = containerCalculationResult.isWeightExceeded(); - var mixable = premise.getHuMixable(); - - if (mixable) { - return BigDecimal.valueOf(weightExceeded ? containerCalculationResult.getHuUtilizationByWeight() : containerCalculationResult.getHuUtilizationByVolume()); - } else { - return BigDecimal.ONE.divide(BigDecimal.valueOf(containerCalculationResult.getHuUnitCount()), 10, RoundingMode.HALF_UP); - } - } - public CustomResult doCalculation(Integer setId, Premise premise, Destination destination, List sections) { var destUnion = countryPropertyRepository.getByMappingIdAndCountryId(CountryPropertyMappingId.UNION, setId, destination.getCountryId()).orElseThrow(); @@ -64,13 +53,13 @@ public class CustomCostCalculationService { double huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(premise.getHuUnitCount()),0, RoundingMode.CEILING).doubleValue(); - return getCustomCalculationResult(setId, premise, destination, getContainerShare(premise, relevantSections.getFirst().containerResult()), huAnnualAmount, transportationCost, transportationChanceCost, transportationRiskCost); + return getCustomCalculationResult(setId, premise, destination, huAnnualAmount, transportationCost, transportationChanceCost, transportationRiskCost); } return CustomResult.EMPTY; } - private CustomResult getCustomCalculationResult(Integer setId, Premise premise, Destination destination, BigDecimal containerShare, double huAnnualAmount, BigDecimal transportationCost, BigDecimal transportationChanceCost, BigDecimal transportationRiskCost) { + private CustomResult getCustomCalculationResult(Integer setId, Premise premise, Destination destination, double huAnnualAmount, BigDecimal transportationCost, BigDecimal transportationChanceCost, BigDecimal transportationRiskCost) { var shippingFrequency = shippingFrequencyCalculationService.doCalculation(setId, huAnnualAmount); var customFee = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.CUSTOM_FEE, setId).orElseThrow().getCurrentValue()); diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java index cfe5b39..db414e9 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java @@ -40,8 +40,9 @@ public class RouteSectionCostCalculationService { private final ChangeRiskFactorCalculationService changeRiskFactorCalculationService; private final NodeRepository nodeRepository; private final UserNodeRepository userNodeRepository; + private final ShippingFrequencyCalculationService shippingFrequencyCalculationService; - public RouteSectionCostCalculationService(ContainerRateRepository containerRateRepository, MatrixRateRepository matrixRateRepository, RouteNodeRepository routeNodeRepository, DistanceService distanceService, PropertyRepository propertyRepository, ChangeRiskFactorCalculationService changeRiskFactorCalculationService, NodeRepository nodeRepository, UserNodeRepository userNodeRepository) { + public RouteSectionCostCalculationService(ContainerRateRepository containerRateRepository, MatrixRateRepository matrixRateRepository, RouteNodeRepository routeNodeRepository, DistanceService distanceService, PropertyRepository propertyRepository, ChangeRiskFactorCalculationService changeRiskFactorCalculationService, NodeRepository nodeRepository, UserNodeRepository userNodeRepository, ShippingFrequencyCalculationService shippingFrequencyCalculationService) { this.containerRateRepository = containerRateRepository; this.matrixRateRepository = matrixRateRepository; this.routeNodeRepository = routeNodeRepository; @@ -50,6 +51,7 @@ public class RouteSectionCostCalculationService { this.changeRiskFactorCalculationService = changeRiskFactorCalculationService; this.nodeRepository = nodeRepository; this.userNodeRepository = userNodeRepository; + this.shippingFrequencyCalculationService = shippingFrequencyCalculationService; } public CalculationJobRouteSection doD2dCalculation(Integer setId, Integer periodId, Premise premise, Destination destination, ContainerCalculationResult containerCalculation) { @@ -94,7 +96,10 @@ public class RouteSectionCostCalculationService { containerCalculation.getMaxContainerWeight(), BigDecimal.valueOf(containerCalculation.getTotalUtilizationByVolume()), BigDecimal.valueOf(containerCalculation.getHuUtilizationByWeight()), - utilization); + utilization, + shippingFrequencyCalculationService.doCalculation(setId, huAnnualAmount.doubleValue()), + huAnnualAmount.doubleValue(), + containerCalculation); result.setCbmPrice(!containerCalculation.isWeightExceeded()); result.setWeightPrice(containerCalculation.isWeightExceeded()); @@ -177,7 +182,10 @@ public class RouteSectionCostCalculationService { containerCalculation.getMaxContainerWeight(), BigDecimal.valueOf(containerCalculation.getTotalUtilizationByVolume()), BigDecimal.valueOf(containerCalculation.getTotalUtilizationByWeight()), - utilization); + utilization, + shippingFrequencyCalculationService.doCalculation(setId, huAnnualAmount.doubleValue()), + huAnnualAmount.doubleValue(), + containerCalculation); result.setCbmPrice(!containerCalculation.isWeightExceeded()); result.setWeightPrice(containerCalculation.isWeightExceeded()); @@ -211,7 +219,11 @@ public class RouteSectionCostCalculationService { int maxContainerWeight, BigDecimal totalVolumeUtilization, BigDecimal totalWeightUtilization, - BigDecimal propertyUtilization) { + BigDecimal propertyUtilization, + double shippingFrequency, + double annualHuAmount, + ContainerCalculationResult containerCalculationResult + ) { BigDecimal utilization; @@ -221,16 +233,26 @@ public class RouteSectionCostCalculationService { BigDecimal cbmRate = rate.divide(BigDecimal.valueOf(containerType.getVolume()), 10, RoundingMode.HALF_UP); BigDecimal weightRate = rate.divide(BigDecimal.valueOf(maxContainerWeight), 10, RoundingMode.HALF_UP); + if (huMixable) { volumePrice = cbmRate.divide(propertyUtilization, 10, RoundingMode.HALF_UP); weightPrice = weightRate.divide(BigDecimal.valueOf(1), 10, RoundingMode.HALF_UP); utilization = weightExceeded ? BigDecimal.ONE : propertyUtilization; } else { + + double huPerContainer = annualHuAmount / shippingFrequency; + + // if the shipping frequency is bigger than the annual amount the "totalXXUtilization" cannot be used. + if(huPerContainer < (containerCalculationResult.getHuUnitCount() * containerCalculationResult.getLayer())) { + + totalVolumeUtilization = BigDecimal.valueOf(huPerContainer * containerCalculationResult.getHu().getVolume(DimensionUnit.M)).divide(BigDecimal.valueOf(containerCalculationResult.getContainerType().getVolume()), 20, RoundingMode.HALF_UP); + totalWeightUtilization = BigDecimal.valueOf(huPerContainer * containerCalculationResult.getHu().getWeight(WeightUnit.KG)).divide(BigDecimal.valueOf(containerCalculationResult.getMaxContainerWeight()), 20, RoundingMode.HALF_UP); + } + volumePrice = cbmRate.divide(totalVolumeUtilization, 10, RoundingMode.HALF_UP); weightPrice = weightRate.divide(totalWeightUtilization, 10, RoundingMode.HALF_UP); utilization = weightExceeded ? totalWeightUtilization : totalVolumeUtilization; - //TODO: wenn shippingfreq > annual hu -> shippingfreq * containerprice. - // gleiches für containercalculation * shippingfreq < annual hu ammount. + } return new PriceCalculationResult(volumePrice, weightPrice, utilization); diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ShippingFrequencyCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ShippingFrequencyCalculationService.java index 04687f4..aa7213a 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ShippingFrequencyCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ShippingFrequencyCalculationService.java @@ -25,13 +25,13 @@ public class ShippingFrequencyCalculationService { } public double doCalculation(Integer setId, double huAnnualAmount) { - Integer minAnnualFrequency = Integer.parseInt(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.FREQ_MIN, setId).orElseThrow().getCurrentValue()); - Integer maxAnnualFrequency = Integer.parseInt(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.FREQ_MAX, setId).orElseThrow().getCurrentValue()); + int minAnnualFrequency = Integer.parseInt(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.FREQ_MIN, setId).orElseThrow().getCurrentValue()); + int maxAnnualFrequency = Integer.parseInt(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.FREQ_MAX, setId).orElseThrow().getCurrentValue()); - if (huAnnualAmount > maxAnnualFrequency.doubleValue()) + if (huAnnualAmount > (double) maxAnnualFrequency) return maxAnnualFrequency; - return Math.max(huAnnualAmount, minAnnualFrequency.doubleValue()); + return Math.max(huAnnualAmount, (double) minAnnualFrequency); } -- 2.45.3 From 1788a7ef1c8a011d43a1bf59dc95703ef3788714 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 17 Dec 2025 14:29:08 +0100 Subject: [PATCH 021/104] Refactor: Pass `ContainerCalculationResult` into cost calculation services and update `ShippingFrequencyCalculationService` logic to consider `HU per container` handling. --- .../configuration/apps/AppExchangeDTO.java | 4 ++++ .../ContainerCalculationResult.java | 5 ++++- .../CalculationExecutionService.java | 20 +++++++++++-------- .../steps/CustomCostCalculationService.java | 9 +++++---- .../steps/HandlingCostCalculationService.java | 11 +++++----- .../InventoryCostCalculationService.java | 5 +++-- .../RouteSectionCostCalculationService.java | 6 +++--- .../ShippingFrequencyCalculationService.java | 15 ++++++++++++-- 8 files changed, 50 insertions(+), 25 deletions(-) create mode 100644 src/main/java/de/avatic/lcc/dto/configuration/apps/AppExchangeDTO.java diff --git a/src/main/java/de/avatic/lcc/dto/configuration/apps/AppExchangeDTO.java b/src/main/java/de/avatic/lcc/dto/configuration/apps/AppExchangeDTO.java new file mode 100644 index 0000000..1ce2641 --- /dev/null +++ b/src/main/java/de/avatic/lcc/dto/configuration/apps/AppExchangeDTO.java @@ -0,0 +1,4 @@ +package de.avatic.lcc.dto.configuration.apps; + +public class AppExchangeDTO { +} diff --git a/src/main/java/de/avatic/lcc/model/calculation/ContainerCalculationResult.java b/src/main/java/de/avatic/lcc/model/calculation/ContainerCalculationResult.java index c2e69ca..cd879c9 100644 --- a/src/main/java/de/avatic/lcc/model/calculation/ContainerCalculationResult.java +++ b/src/main/java/de/avatic/lcc/model/calculation/ContainerCalculationResult.java @@ -215,7 +215,7 @@ public class ContainerCalculationResult { * @return The total utilization value for the container. */ public double getTotalUtilizationByVolume() { - return getHuUtilizationByVolume() * huUnitCount * layer; + return getHuUtilizationByVolume() * huUnitCount; } /** @@ -254,4 +254,7 @@ public class ContainerCalculationResult { return WeightUnit.KG.convertFromG(hu.getWeight()) / maxContainerWeight; } + public int getHuPerContainer() { + return this.huUnitCount; + } } diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/CalculationExecutionService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/CalculationExecutionService.java index cd85c23..dfad5a0 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/CalculationExecutionService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/CalculationExecutionService.java @@ -137,13 +137,15 @@ public class CalculationExecutionService { AirfreightResult airfreightCost = airfreightCalculationService.doCalculation(setId, periodId, premise, destination); ContainerType usedContainerType = null; + ContainerCalculationResult selectedContainerCalculation = null; + CalculationJobDestination destinationCalculationJob = new CalculationJobDestination(); boolean hasMainRun = true; BigDecimal leadTime = null; if (destination.getD2d()) { - var containerCalculation = containerCalculationService.doCalculation(setId, premiseToHuService.createHuFromPremise(premise), ContainerType.FEU, premise.getHuMixable(), premise.getHuStackable()); - sections = List.of(new SectionInfo(null, routeSectionCostCalculationService.doD2dCalculation(setId, periodId, premise, destination, containerCalculation), containerCalculation)); + selectedContainerCalculation = containerCalculationService.doCalculation(setId, premiseToHuService.createHuFromPremise(premise), ContainerType.FEU, premise.getHuMixable(), premise.getHuStackable()); + sections = List.of(new SectionInfo(null, routeSectionCostCalculationService.doD2dCalculation(setId, periodId, premise, destination, selectedContainerCalculation), selectedContainerCalculation)); leadTime = BigDecimal.valueOf(destination.getLeadTimeD2d()); usedContainerType = ContainerType.FEU; } else { @@ -161,6 +163,8 @@ public class CalculationExecutionService { s.result().setPostRun(false); }); } + + selectedContainerCalculation = bestContainerTypeResult.selectedContainerCalculation; } destinationCalculationJob.setD2D(destination.getD2d()); @@ -168,9 +172,9 @@ public class CalculationExecutionService { if(destination.getD2d()) destinationCalculationJob.setRateD2D(destination.getRateD2d()); - customCost = customCostCalculationService.doCalculation(setId, premise, destination, sections); - handlingCost = handlingCostCalculationService.doCalculation(setId, premise, destination, hasMainRun); - inventoryCost = inventoryCostCalculationService.doCalculation(setId, premise, destination, leadTime); + customCost = customCostCalculationService.doCalculation(setId, premise, destination, sections, selectedContainerCalculation); + handlingCost = handlingCostCalculationService.doCalculation(setId, premise, destination, hasMainRun, selectedContainerCalculation); + inventoryCost = inventoryCostCalculationService.doCalculation(setId, premise, destination, leadTime, selectedContainerCalculation); destinationCalculationJob.setContainerType(usedContainerType); @@ -209,7 +213,7 @@ public class CalculationExecutionService { destinationCalculationJob.setHuCount(sections.getFirst().containerResult().getHuUnitCount()); destinationCalculationJob.setAnnualAmount(BigDecimal.valueOf(destination.getAnnualAmount())); - destinationCalculationJob.setShippingFrequency(shippingFrequencyCalculationService.doCalculation(setId, destination.getAnnualAmount())); + destinationCalculationJob.setShippingFrequency(shippingFrequencyCalculationService.doCalculation(setId, destination.getAnnualAmount(), selectedContainerCalculation.getHuPerContainer(),!premise.getHuMixable())); var commonCost = destinationCalculationJob.getAnnualHandlingCost() .add(destinationCalculationJob.getAnnualDisposalCost()) @@ -264,10 +268,10 @@ public class CalculationExecutionService { } var bestContainerType = getBestContainerType(sectionInfos); - return new BestContainerTypeResult(bestContainerType, sectionInfos.get(bestContainerType)); + return new BestContainerTypeResult(bestContainerType, sectionInfos.get(bestContainerType), containerCalculation.get(bestContainerType)); } - private record BestContainerTypeResult(ContainerType containerType, List sections) { + private record BestContainerTypeResult(ContainerType containerType, List sections, ContainerCalculationResult selectedContainerCalculation) { } } diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/CustomCostCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/CustomCostCalculationService.java index 1e2686d..2b34abf 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/CustomCostCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/CustomCostCalculationService.java @@ -37,7 +37,7 @@ public class CustomCostCalculationService { this.shippingFrequencyCalculationService = shippingFrequencyCalculationService; } - public CustomResult doCalculation(Integer setId, Premise premise, Destination destination, List sections) { + public CustomResult doCalculation(Integer setId, Premise premise, Destination destination, List sections, ContainerCalculationResult containerCalculationResult) { var destUnion = countryPropertyRepository.getByMappingIdAndCountryId(CountryPropertyMappingId.UNION, setId, destination.getCountryId()).orElseThrow(); var sourceUnion = countryPropertyRepository.getByMappingIdAndCountryId(CountryPropertyMappingId.UNION, setId, premise.getCountryId()).orElseThrow(); @@ -53,14 +53,15 @@ public class CustomCostCalculationService { double huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(premise.getHuUnitCount()),0, RoundingMode.CEILING).doubleValue(); - return getCustomCalculationResult(setId, premise, destination, huAnnualAmount, transportationCost, transportationChanceCost, transportationRiskCost); + return getCustomCalculationResult(setId, premise, destination, huAnnualAmount, transportationCost, transportationChanceCost, transportationRiskCost, containerCalculationResult); } return CustomResult.EMPTY; } - private CustomResult getCustomCalculationResult(Integer setId, Premise premise, Destination destination, double huAnnualAmount, BigDecimal transportationCost, BigDecimal transportationChanceCost, BigDecimal transportationRiskCost) { - var shippingFrequency = shippingFrequencyCalculationService.doCalculation(setId, huAnnualAmount); + private CustomResult getCustomCalculationResult(Integer setId, Premise premise, Destination destination, double huAnnualAmount, BigDecimal transportationCost, BigDecimal transportationChanceCost, BigDecimal transportationRiskCost, ContainerCalculationResult containerCalculationResult) { + + var shippingFrequency = shippingFrequencyCalculationService.doCalculation(setId, huAnnualAmount, containerCalculationResult.getHuPerContainer(), !premise.getHuMixable()); var customFee = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.CUSTOM_FEE, setId).orElseThrow().getCurrentValue()); var tariffRate = premise.getTariffRate(); diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/HandlingCostCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/HandlingCostCalculationService.java index d172f77..7c22837 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/HandlingCostCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/HandlingCostCalculationService.java @@ -1,5 +1,6 @@ package de.avatic.lcc.service.calculation.execution.steps; +import de.avatic.lcc.model.calculation.ContainerCalculationResult; import de.avatic.lcc.model.calculation.HandlingResult; import de.avatic.lcc.model.db.packaging.LoadCarrierType; import de.avatic.lcc.model.db.packaging.PackagingDimension; @@ -30,13 +31,13 @@ public class HandlingCostCalculationService { this.shippingFrequencyCalculationService = shippingFrequencyCalculationService; } - public HandlingResult doCalculation(Integer setId, Premise premise, Destination destination, Boolean addRepackingAndDisposalCost) { + public HandlingResult doCalculation(Integer setId, Premise premise, Destination destination, Boolean addRepackingAndDisposalCost, ContainerCalculationResult containerCalculationResult) { var hu = premiseToHuService.createHuFromPremise(premise); - return (LoadCarrierType.SLC == hu.getLoadCarrierType() ? getSLCCost(setId, destination, hu, hu.getLoadCarrierType(), addRepackingAndDisposalCost) : getLLCCost(setId, destination, hu, hu.getLoadCarrierType(), addRepackingAndDisposalCost)); + return (LoadCarrierType.SLC == hu.getLoadCarrierType() ? getSLCCost(setId, premise, destination, hu, hu.getLoadCarrierType(), addRepackingAndDisposalCost, containerCalculationResult) : getLLCCost(setId, premise, destination, hu, hu.getLoadCarrierType(), addRepackingAndDisposalCost, containerCalculationResult)); } - private HandlingResult getSLCCost(Integer setId, Destination destination, PackagingDimension hu, LoadCarrierType loadCarrierType, boolean addRepackingAndDisposalCost) { + private HandlingResult getSLCCost(Integer setId, Premise premise, Destination destination, PackagingDimension hu, LoadCarrierType loadCarrierType, boolean addRepackingAndDisposalCost, ContainerCalculationResult containerCalculationResult) { var destinationHandling = destination.getHandlingCost(); var destinationDisposal = destination.getDisposalCost(); @@ -77,7 +78,7 @@ public class HandlingCostCalculationService { } - private HandlingResult getLLCCost(Integer setId, Destination destination, PackagingDimension hu, LoadCarrierType type, boolean addRepackingAndDisposalCost) { + private HandlingResult getLLCCost(Integer setId, Premise premise, Destination destination, PackagingDimension hu, LoadCarrierType type, boolean addRepackingAndDisposalCost, ContainerCalculationResult containerCalculationResult) { var destinationHandling = destination.getHandlingCost(); var destinationDisposal = destination.getDisposalCost(); @@ -93,7 +94,7 @@ public class HandlingCostCalculationService { BigDecimal booking = BigDecimal.valueOf(Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.BOOKING, setId).orElseThrow().getCurrentValue())); var annualRepacking = getRepackingCost(setId, hu, type, addRepackingAndDisposalCost, destinationRepacking).multiply(wageFactor).multiply( huAnnualAmount); - var annualHandling = ((handling.add(dispatch).add(release)).multiply(wageFactor).multiply(huAnnualAmount)).add(booking.multiply(BigDecimal.valueOf(shippingFrequencyCalculationService.doCalculation(setId, huAnnualAmount.doubleValue())))); + var annualHandling = ((handling.add(dispatch).add(release)).multiply(wageFactor).multiply(huAnnualAmount)).add(booking.multiply(BigDecimal.valueOf(shippingFrequencyCalculationService.doCalculation(setId, huAnnualAmount.doubleValue(), containerCalculationResult.getHuPerContainer(), !premise.getHuMixable())))); var annualDisposal = (disposal.multiply(huAnnualAmount)); return new HandlingResult(LoadCarrierType.LLC, annualRepacking, annualHandling, annualDisposal, annualRepacking.add(annualHandling).add(annualDisposal)); diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/InventoryCostCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/InventoryCostCalculationService.java index d9b0658..dafb733 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/InventoryCostCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/InventoryCostCalculationService.java @@ -1,5 +1,6 @@ package de.avatic.lcc.service.calculation.execution.steps; +import de.avatic.lcc.model.calculation.ContainerCalculationResult; import de.avatic.lcc.model.calculation.InventoryCostResult; import de.avatic.lcc.model.db.packaging.PackagingDimension; import de.avatic.lcc.model.db.premises.Premise; @@ -30,7 +31,7 @@ public class InventoryCostCalculationService { this.premiseToHuService = premiseToHuService; } - public InventoryCostResult doCalculation(Integer setId, Premise premise, Destination destination, BigDecimal leadTime) { + public InventoryCostResult doCalculation(Integer setId, Premise premise, Destination destination, BigDecimal leadTime, ContainerCalculationResult containerCalculationResult) { var fcaFee = BigDecimal.ZERO; @@ -53,7 +54,7 @@ public class InventoryCostCalculationService { var dailyAmount = annualAmount.divide(BigDecimal.valueOf(365), 10, RoundingMode.HALF_UP); var workdayAmount = annualAmount.divide(workdays, 10, RoundingMode.HALF_UP); - var opStock = (annualAmount.divide(BigDecimal.valueOf(Math.max(shippingFrequencyCalculationService.doCalculation(setId, huAnnualAmount),1)), 10, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(.5))); + var opStock = (annualAmount.divide(BigDecimal.valueOf(Math.max(shippingFrequencyCalculationService.doCalculation(setId, huAnnualAmount, containerCalculationResult.getHuPerContainer(), !premise.getHuMixable()),1)), 10, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(.5))); var safetyStock = safetyDays.multiply(workdayAmount); var stockedInventory = opStock.add(safetyStock); var inTransportStock = dailyAmount.multiply(leadTime); diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java index db414e9..6139420 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java @@ -97,7 +97,7 @@ public class RouteSectionCostCalculationService { BigDecimal.valueOf(containerCalculation.getTotalUtilizationByVolume()), BigDecimal.valueOf(containerCalculation.getHuUtilizationByWeight()), utilization, - shippingFrequencyCalculationService.doCalculation(setId, huAnnualAmount.doubleValue()), + shippingFrequencyCalculationService.doCalculation(setId, huAnnualAmount.doubleValue(), containerCalculation.getHuPerContainer(), !premise.getHuMixable()), huAnnualAmount.doubleValue(), containerCalculation); @@ -183,7 +183,7 @@ public class RouteSectionCostCalculationService { BigDecimal.valueOf(containerCalculation.getTotalUtilizationByVolume()), BigDecimal.valueOf(containerCalculation.getTotalUtilizationByWeight()), utilization, - shippingFrequencyCalculationService.doCalculation(setId, huAnnualAmount.doubleValue()), + shippingFrequencyCalculationService.doCalculation(setId, huAnnualAmount.doubleValue(), containerCalculation.getHuPerContainer(), !premise.getHuMixable()), huAnnualAmount.doubleValue(), containerCalculation); @@ -243,7 +243,7 @@ public class RouteSectionCostCalculationService { double huPerContainer = annualHuAmount / shippingFrequency; // if the shipping frequency is bigger than the annual amount the "totalXXUtilization" cannot be used. - if(huPerContainer < (containerCalculationResult.getHuUnitCount() * containerCalculationResult.getLayer())) { + if(huPerContainer < (containerCalculationResult.getHuPerContainer())) { totalVolumeUtilization = BigDecimal.valueOf(huPerContainer * containerCalculationResult.getHu().getVolume(DimensionUnit.M)).divide(BigDecimal.valueOf(containerCalculationResult.getContainerType().getVolume()), 20, RoundingMode.HALF_UP); totalWeightUtilization = BigDecimal.valueOf(huPerContainer * containerCalculationResult.getHu().getWeight(WeightUnit.KG)).divide(BigDecimal.valueOf(containerCalculationResult.getMaxContainerWeight()), 20, RoundingMode.HALF_UP); diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ShippingFrequencyCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ShippingFrequencyCalculationService.java index aa7213a..969e838 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ShippingFrequencyCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ShippingFrequencyCalculationService.java @@ -13,10 +13,16 @@ public class ShippingFrequencyCalculationService { this.propertyRepository = propertyRepository; } - public int doCalculation(Integer setId, int huAnnualAmount) { + public int doCalculation(Integer setId, int huAnnualAmount, int maxHuPerContainer, boolean fillContainer) { var minAnnualFrequency = Integer.parseInt(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.FREQ_MIN, setId).orElseThrow().getCurrentValue()); var maxAnnualFrequency = Integer.parseInt(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.FREQ_MAX, setId).orElseThrow().getCurrentValue()); + var fullContainers = huAnnualAmount / maxHuPerContainer; + + + if(fillContainer && huAnnualAmount > maxAnnualFrequency) + return fullContainers; + if (huAnnualAmount > maxAnnualFrequency) return maxAnnualFrequency; @@ -24,10 +30,15 @@ public class ShippingFrequencyCalculationService { } - public double doCalculation(Integer setId, double huAnnualAmount) { + public double doCalculation(Integer setId, double huAnnualAmount, int maxHuPerContainer, boolean fillContainer) { int minAnnualFrequency = Integer.parseInt(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.FREQ_MIN, setId).orElseThrow().getCurrentValue()); int maxAnnualFrequency = Integer.parseInt(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.FREQ_MAX, setId).orElseThrow().getCurrentValue()); + var fullContainers = huAnnualAmount / maxHuPerContainer; + + if(fillContainer && huAnnualAmount > (double) maxAnnualFrequency) + return fullContainers; + if (huAnnualAmount > (double) maxAnnualFrequency) return maxAnnualFrequency; -- 2.45.3 From 6add528c02f89b8c020235cf98e9a82acc7de891 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 17 Dec 2025 16:06:59 +0100 Subject: [PATCH 022/104] Add import/export functionality for apps, including client-side file handling and backend encryption/decryption logic --- .../src/components/UI/AppListItem.vue | 26 +++- .../src/components/layout/config/Apps.vue | 83 ++++++++++- src/frontend/src/store/apps.js | 12 ++ .../configuration/AppsController.java | 13 ++ .../configuration/apps/AppExchangeDTO.java | 10 ++ .../avatic/lcc/service/apps/AppsService.java | 134 +++++++++++++++++- .../transformer/apps/AppTransformer.java | 23 ++- 7 files changed, 287 insertions(+), 14 deletions(-) diff --git a/src/frontend/src/components/UI/AppListItem.vue b/src/frontend/src/components/UI/AppListItem.vue index fc022a2..eb41c46 100644 --- a/src/frontend/src/components/UI/AppListItem.vue +++ b/src/frontend/src/components/UI/AppListItem.vue @@ -1,10 +1,20 @@