From ca3c15ecd27685077e05807b2df03b4fc3e334d2 Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 7 Sep 2025 23:48:51 +0200 Subject: [PATCH] BACKEND: several bugfixing in calculation and reporting FRONTEND: Start implementing reporting --- .../src/components/UI/ReportChart.vue | 15 ++ .../src/components/UI/ReportRoute.vue | 15 ++ .../layout/assistant/SupplierItem.vue | 32 ++- .../layout/config/CountryProperties.vue | 23 ++- .../components/layout/config/Properties.vue | 9 +- .../layout/report/DestinationCost.vue | 14 ++ .../components/layout/report/OverviewCost.vue | 14 ++ .../src/components/layout/report/Report.vue | 14 ++ .../layout/report/SelectForReport.vue | 166 ++++++++++++++++ .../components/layout/report/WeightedCost.vue | 14 ++ src/frontend/src/main.js | 3 +- src/frontend/src/pages/Reporting.vue | 89 ++++++++- src/frontend/src/store/country.js | 3 + src/frontend/src/store/reportSearch.js | 185 ++++++++++++++++++ src/frontend/src/store/reports.js | 135 +++++++++++++ .../report/ReportingController.java | 9 +- .../CalculationJobDestinationRepository.java | 4 +- .../repositories/premise/RouteRepository.java | 2 +- .../rates/ValidityPeriodRepository.java | 9 +- .../CalculationExecutionService.java | 4 +- .../steps/AirfreightCalculationService.java | 7 +- .../steps/CustomCostCalculationService.java | 42 ++-- .../steps/HandlingCostCalculationService.java | 3 +- .../InventoryCostCalculationService.java | 4 +- .../RouteSectionCostCalculationService.java | 23 ++- .../ShippingFrequencyCalculationService.java | 11 ++ .../transformer/report/ReportTransformer.java | 144 ++++++++++---- src/main/resources/schema.sql | 8 +- 28 files changed, 906 insertions(+), 95 deletions(-) create mode 100644 src/frontend/src/components/UI/ReportChart.vue create mode 100644 src/frontend/src/components/UI/ReportRoute.vue create mode 100644 src/frontend/src/components/layout/report/DestinationCost.vue create mode 100644 src/frontend/src/components/layout/report/OverviewCost.vue create mode 100644 src/frontend/src/components/layout/report/Report.vue create mode 100644 src/frontend/src/components/layout/report/SelectForReport.vue create mode 100644 src/frontend/src/components/layout/report/WeightedCost.vue create mode 100644 src/frontend/src/store/reportSearch.js create mode 100644 src/frontend/src/store/reports.js diff --git a/src/frontend/src/components/UI/ReportChart.vue b/src/frontend/src/components/UI/ReportChart.vue new file mode 100644 index 0000000..a6c8902 --- /dev/null +++ b/src/frontend/src/components/UI/ReportChart.vue @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/src/frontend/src/components/UI/ReportRoute.vue b/src/frontend/src/components/UI/ReportRoute.vue new file mode 100644 index 0000000..459e43e --- /dev/null +++ b/src/frontend/src/components/UI/ReportRoute.vue @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/src/frontend/src/components/layout/assistant/SupplierItem.vue b/src/frontend/src/components/layout/assistant/SupplierItem.vue index 3a93e8d..7b222fa 100644 --- a/src/frontend/src/components/layout/assistant/SupplierItem.vue +++ b/src/frontend/src/components/layout/assistant/SupplierItem.vue @@ -1,13 +1,12 @@ - @@ -41,6 +40,16 @@ export default { type: Boolean, required: false, default: false + }, + selected: { + type: Boolean, + required: false, + default: false + }, + showTrash: { + type: Boolean, + required: false, + default: true } }, methods: { @@ -53,6 +62,7 @@ export default { \ No newline at end of file diff --git a/src/frontend/src/components/layout/report/OverviewCost.vue b/src/frontend/src/components/layout/report/OverviewCost.vue new file mode 100644 index 0000000..855e0d2 --- /dev/null +++ b/src/frontend/src/components/layout/report/OverviewCost.vue @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/src/frontend/src/components/layout/report/Report.vue b/src/frontend/src/components/layout/report/Report.vue new file mode 100644 index 0000000..b75a6ba --- /dev/null +++ b/src/frontend/src/components/layout/report/Report.vue @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/src/frontend/src/components/layout/report/SelectForReport.vue b/src/frontend/src/components/layout/report/SelectForReport.vue new file mode 100644 index 0000000..91b736e --- /dev/null +++ b/src/frontend/src/components/layout/report/SelectForReport.vue @@ -0,0 +1,166 @@ + + + + + + + \ No newline at end of file diff --git a/src/frontend/src/components/layout/report/WeightedCost.vue b/src/frontend/src/components/layout/report/WeightedCost.vue new file mode 100644 index 0000000..26ffd42 --- /dev/null +++ b/src/frontend/src/components/layout/report/WeightedCost.vue @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/src/frontend/src/main.js b/src/frontend/src/main.js index 802702f..bfbfee8 100644 --- a/src/frontend/src/main.js +++ b/src/frontend/src/main.js @@ -26,7 +26,7 @@ import { PhArchive, PhFloppyDisk, PhArrowCounterClockwise, - PhCheck, PhBug, PhShuffle, PhStack + PhCheck, PhBug, PhShuffle, PhStack, PhFile } from "@phosphor-icons/vue"; const app = createApp(App); @@ -59,6 +59,7 @@ app.component('PhStar', PhStar); app.component('PhBug', PhBug); app.component('PhShuffle', PhShuffle); app.component('PhStack', PhStack ); +app.component('PhFile', PhFile); app.use(router); diff --git a/src/frontend/src/pages/Reporting.vue b/src/frontend/src/pages/Reporting.vue index 9deafc5..d531ea1 100644 --- a/src/frontend/src/pages/Reporting.vue +++ b/src/frontend/src/pages/Reporting.vue @@ -1,14 +1,95 @@ + + - \ No newline at end of file diff --git a/src/frontend/src/store/country.js b/src/frontend/src/store/country.js index 834f945..04297cb 100644 --- a/src/frontend/src/store/country.js +++ b/src/frontend/src/store/country.js @@ -43,8 +43,11 @@ export const useCountryStore = defineStore('country', { await stage.checkStagedChanges(); }, async selectPeriod(periodId) { + this.loading = true; + this.countryDetail = null; this.selectedPeriodId = periodId; await this.loadCountryDetail(); + this.loading = false; }, async selectCountry(countryId) { this.selectedCountryId = countryId; diff --git a/src/frontend/src/store/reportSearch.js b/src/frontend/src/store/reportSearch.js new file mode 100644 index 0000000..6c23b2c --- /dev/null +++ b/src/frontend/src/store/reportSearch.js @@ -0,0 +1,185 @@ +import {defineStore} from 'pinia' +import {config} from '@/config' +import {useErrorStore} from "@/store/error.js"; +import {useStageStore} from "@/store/stage.js"; +import {usePropertySetsStore} from "@/store/propertySets.js"; +import logger from "@/logger.js"; + +export const useReportSearchStore = defineStore('reportSearch', { + state() { + return { + suppliers: [], + selectedIds: [], + remainingSuppliers: [], + material: null, + loading: false, + } + }, + getters: { + getSuppliers(state) { + return state.remainingSuppliers; + }, + isSelected(state) { + return function (id) { + return state.selectedIds.includes(id); + } + }, + getSelectedIds(state) { + return state.selectedIds; + }, + getMaterialId(state) { + return state.material?.id; + }, + getMaterial(state) { + return state.material; + }, + }, + actions: { + async selectSupplier(id) { + + if (!this.selectedIds.includes(id)) + this.selectedIds.push(id) + else { + const index = this.selectedIds.findIndex(selectedId => selectedId === id); + this.selectedIds.splice(index, 1); + } + + + this.updateShownSuppliers(); + }, + updateShownSuppliers() { + const toBeShown = [] + + this.suppliers.forEach(supplierList => { + const shouldInclude = this.selectedIds.every(id => supplierList.some(s => s.id === id)) + + if (shouldInclude) + toBeShown.push(...supplierList); + }) + + + this.remainingSuppliers = toBeShown.filter((item, index) => + toBeShown.findIndex(obj => obj.id === item.id) === index + ); + }, + async setMaterial(material) { + this.material = material; + await this.updateSuppliers(); + }, + async updateSuppliers() { + if (this.getMaterialId == null) return; + + this.loading = true; + this.suppliers = []; + this.selectedIds = []; + this.remainingSuppliers = []; + + const params = new URLSearchParams(); + params.append('material', this.getMaterialId); + const url = `${config.backendUrl}/reports/search/${params.size === 0 ? '' : '?'}${params.toString()}`; + + this.suppliers = await this.performRequest('GET', url, null).catch(e => { + this.loading = false; + }); + + this.updateShownSuppliers(); + + this.loading = false; + }, + async performRequest(method, url, body, expectResponse = true) { + + const params = { + method: method, + headers: { + 'Content-Type': 'application/json' + } + }; + + if (body) { + params.body = JSON.stringify(body); + } + + const request = {url: url, params: params}; + logger.info("Request:", request); + + const response = await fetch(url, params + ).catch(e => { + const error = { + code: 'Network error.', + message: "Please check your internet connection.", + trace: null + } + + logger.error(error); + const errorStore = useErrorStore(); + void errorStore.addError(error, {store: this, request: request}); + + throw e; + }); + + let data = null; + if (expectResponse) { + data = await response.json().catch(e => { + const error = { + code: 'Malformed response', + message: "Malformed server response. Please contact support.", + trace: null + } + + logger.error(error); + const errorStore = useErrorStore(); + void errorStore.addError(error, {store: this, request: request}); + throw e; + }); + + if (!response.ok) { + const error = { + code: data.error.code, + title: data.error.title, + message: data.error.message, + trace: data.error.trace + } + + logger.error(error); + const errorStore = useErrorStore(); + void errorStore.addError(error, {store: this, request: request}); + throw new Error('Internal backend error'); + } + } else { + if (!response.ok) { + + const data = await response.json().catch(e => { + const error = { + code: "Return code error " + response.status, + message: "Server returned wrong response code", + trace: null + } + logger.error(error); + const errorStore = useErrorStore(); + void errorStore.addError(error, {store: this, request: request}); + throw new Error('Internal backend error'); + + }); + + const error = { + code: data.error.code, + title: data.error.title, + message: data.error.message, + trace: data.error.trace + } + + logger.error(error); + const errorStore = useErrorStore(); + void errorStore.addError(error, {store: this, request: request}); + throw new Error('Internal backend error'); + + + } + } + + logger.info("Response:", data); + return data; + } + }, + +}); \ No newline at end of file diff --git a/src/frontend/src/store/reports.js b/src/frontend/src/store/reports.js new file mode 100644 index 0000000..1b99460 --- /dev/null +++ b/src/frontend/src/store/reports.js @@ -0,0 +1,135 @@ +import {defineStore} from 'pinia' +import {config} from '@/config' +import {useErrorStore} from "@/store/error.js"; +import {useStageStore} from "@/store/stage.js"; +import {usePropertySetsStore} from "@/store/propertySets.js"; +import logger from "@/logger.js"; + +export const useReportsStore = defineStore('reports', { + state() { + return { + reports: [], + loading: false, + } + }, + getters: { + + }, + actions: { + async fetchReports(materialId, supplierIds) { + if (supplierIds == null || materialId == null) return; + + this.loading = true; + this.reports = []; + + console.log("fetchreports") + + const params = new URLSearchParams(); + params.append('material', materialId); + params.append('sources', supplierIds); + + const url = `${config.backendUrl}/reports/view/${params.size === 0 ? '' : '?'}${params.toString()}`; + + this.reports = await this.performRequest('GET', url, null).catch(e => { + this.loading = false; + }); + + this.loading = false; + }, + async performRequest(method, url, body, expectResponse = true) { + + const params = { + method: method, + headers: { + 'Content-Type': 'application/json' + } + }; + + if (body) { + params.body = JSON.stringify(body); + } + + const request = {url: url, params: params}; + logger.info("Request:", request); + + const response = await fetch(url, params + ).catch(e => { + const error = { + code: 'Network error.', + message: "Please check your internet connection.", + trace: null + } + + logger.error(error); + const errorStore = useErrorStore(); + void errorStore.addError(error, {store: this, request: request}); + + throw e; + }); + + let data = null; + if (expectResponse) { + data = await response.json().catch(e => { + const error = { + code: 'Malformed response', + message: "Malformed server response. Please contact support.", + trace: null + } + + logger.error(error); + const errorStore = useErrorStore(); + void errorStore.addError(error, {store: this, request: request}); + throw e; + }); + + if (!response.ok) { + const error = { + code: data.error.code, + title: data.error.title, + message: data.error.message, + trace: data.error.trace + } + + logger.error(error); + const errorStore = useErrorStore(); + void errorStore.addError(error, {store: this, request: request}); + throw new Error('Internal backend error'); + } + } else { + if (!response.ok) { + + const data = await response.json().catch(e => { + const error = { + code: "Return code error " + response.status, + message: "Server returned wrong response code", + trace: null + } + logger.error(error); + const errorStore = useErrorStore(); + void errorStore.addError(error, {store: this, request: request}); + throw new Error('Internal backend error'); + + }); + + const error = { + code: data.error.code, + title: data.error.title, + message: data.error.message, + trace: data.error.trace + } + + logger.error(error); + const errorStore = useErrorStore(); + void errorStore.addError(error, {store: this, request: request}); + throw new Error('Internal backend error'); + + + } + } + + logger.info("Response:", data); + return data; + } + }, + +}); \ No newline at end of file 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 a608436..b16ff93 100644 --- a/src/main/java/de/avatic/lcc/controller/report/ReportingController.java +++ b/src/main/java/de/avatic/lcc/controller/report/ReportingController.java @@ -8,7 +8,10 @@ import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -40,7 +43,7 @@ public class ReportingController { * @param materialId The ID of the material for which suppliers need to be found. * @return A list of suppliers grouped by categories. */ - @GetMapping("/search") + @GetMapping({"/search", "/search/"}) public ResponseEntity>> findSupplierForReporting(@RequestParam(value = "material") Integer materialId) { return ResponseEntity.ok(reportingService.findSupplierForReporting(materialId)); } @@ -52,7 +55,7 @@ public class ReportingController { * @param nodeIds A list of node IDs (sources) to include in the report. * @return The generated report details. */ - @GetMapping("/view") + @GetMapping({"/view","/view/"}) public ResponseEntity> getReport(@RequestParam(value = "material") Integer materialId, @RequestParam(value = "sources") List nodeIds) { return ResponseEntity.ok(reportingService.getReport(materialId, nodeIds)); } diff --git a/src/main/java/de/avatic/lcc/repositories/calculation/CalculationJobDestinationRepository.java b/src/main/java/de/avatic/lcc/repositories/calculation/CalculationJobDestinationRepository.java index 8f3065b..1440648 100644 --- a/src/main/java/de/avatic/lcc/repositories/calculation/CalculationJobDestinationRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/calculation/CalculationJobDestinationRepository.java @@ -95,7 +95,7 @@ public class CalculationJobDestinationRepository { ps.setObject(paramIndex++, destination.getAnnualAirFreightCost()); // Transportation - ps.setObject(paramIndex++, destination.getContainerType() != null ? destination.getContainerType().name() : null); + ps.setObject(paramIndex++, destination.getContainerType().name()); ps.setObject(paramIndex++, destination.getHuCount()); ps.setObject(paramIndex++, destination.getLayerStructure()); ps.setObject(paramIndex++, destination.getLayerCount()); @@ -179,6 +179,8 @@ public class CalculationJobDestinationRepository { entity.setTransportWeightExceeded(rs.getBoolean("transport_weight_exceeded")); entity.setShippingFrequency(rs.getInt("shipping_frequency")); entity.setAnnualTransportationCost(rs.getBigDecimal("annual_transportation_cost")); + entity.setTotalTransitTime(rs.getInt("transit_time_in_days")); + entity.setContainerUtilization(rs.getBigDecimal("container_utilization")); // Material Cost fields entity.setMaterialCost(rs.getBigDecimal("material_cost")); diff --git a/src/main/java/de/avatic/lcc/repositories/premise/RouteRepository.java b/src/main/java/de/avatic/lcc/repositories/premise/RouteRepository.java index e67ec4d..e06a3fc 100644 --- a/src/main/java/de/avatic/lcc/repositories/premise/RouteRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/premise/RouteRepository.java @@ -33,7 +33,7 @@ public class RouteRepository { } public Optional getSelectedByDestinationId(Integer id) { - String query = "SELECT * FROM premise_route WHERE premise_destination_id = ?"; + String query = "SELECT * FROM premise_route WHERE premise_destination_id = ? AND is_selected = TRUE"; var route = jdbcTemplate.query(query, new RouteMapper(), id); if(route.isEmpty()) { diff --git a/src/main/java/de/avatic/lcc/repositories/rates/ValidityPeriodRepository.java b/src/main/java/de/avatic/lcc/repositories/rates/ValidityPeriodRepository.java index eb30a90..654a97a 100644 --- a/src/main/java/de/avatic/lcc/repositories/rates/ValidityPeriodRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/rates/ValidityPeriodRepository.java @@ -202,7 +202,14 @@ public class ValidityPeriodRepository { """; - var periods = jdbcTemplate.query(validityPeriodSql, new ValidityPeriodMapper(), materialId, nodeIds.toArray(), nodeIds.size()); + Object[] params = new Object[1 + nodeIds.size() + 1]; + params[0] = materialId; + for (int i = 0; i < nodeIds.size(); i++) { + params[i + 1] = nodeIds.get(i); + } + params[params.length - 1] = nodeIds.size(); + + var periods = jdbcTemplate.query(validityPeriodSql, new ValidityPeriodMapper(), params); if (periods.isEmpty()) { return Optional.empty(); 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 288dbab..c9fb552 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 @@ -196,8 +196,8 @@ public class CalculationExecutionService { .add(destinationCalculationJob.getAnnualDisposalCost()) .add(destinationCalculationJob.getAnnualCapitalCost()) .add(destinationCalculationJob.getAnnualStorageCost()) - .add(materialCost) - .add(fcaFee); + .add(materialCost.multiply(BigDecimal.valueOf(destination.getAnnualAmount()))) + .add(fcaFee.multiply(BigDecimal.valueOf(destination.getAnnualAmount()))); var totalCost = commonCost.add(destinationCalculationJob.getAnnualTransportationCost()).add(destinationCalculationJob.getAnnualCustomCost()); var totalRiskCost = commonCost.add(customCost.getAnnualRiskCost()).add(sections.stream().map(SectionInfo::result).map(CalculationJobRouteSection::getAnnualRiskCost).reduce(BigDecimal.ZERO, BigDecimal::add)); diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/AirfreightCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/AirfreightCalculationService.java index 7db55b3..1c40bf5 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/AirfreightCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/AirfreightCalculationService.java @@ -77,6 +77,9 @@ public class AirfreightCalculationService { private double getAirfreightShare(double maxAirfreightShare, double overseaShare) { + maxAirfreightShare = maxAirfreightShare*100; + overseaShare = overseaShare*100; + // Ensure oversea share is within valid range if (overseaShare < 0 || overseaShare > 100) { throw new IllegalArgumentException("Oversea share must be between 0 and 100"); @@ -87,14 +90,14 @@ public class AirfreightCalculationService { // Linear interpolation: y = mx + b // m = (0.2 * maxAirfreightShare - 0) / (50 - 0) = 0.004 * maxAirfreightShare // b = 0 - return (0.004 * maxAirfreightShare * overseaShare); + return (0.004 * maxAirfreightShare * overseaShare)/100; } // Second segment: from (50, 20% of max) to (100, max) else { // Linear interpolation: y = mx + b // m = (maxAirfreightShare - 0.2 * maxAirfreightShare) / (100 - 50) = 0.016 * maxAirfreightShare // b = 0.2 * maxAirfreightShare - 50 * 0.016 * maxAirfreightShare = -0.6 * maxAirfreightShare - return (0.016 * maxAirfreightShare * overseaShare - 0.6 * maxAirfreightShare); + return (0.016 * maxAirfreightShare * overseaShare - 0.6 * maxAirfreightShare)/100; } } 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 ee82a0e..c279ba9 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 @@ -1,6 +1,8 @@ package de.avatic.lcc.service.calculation.execution.steps; +import de.avatic.lcc.calculationmodel.ContainerCalculationResult; import de.avatic.lcc.calculationmodel.CustomResult; +import de.avatic.lcc.calculationmodel.SectionInfo; import de.avatic.lcc.model.premises.Premise; import de.avatic.lcc.model.premises.route.Destination; import de.avatic.lcc.model.properties.CountryPropertyMappingId; @@ -10,10 +12,10 @@ import de.avatic.lcc.repositories.country.CountryPropertyRepository; import de.avatic.lcc.repositories.premise.RouteNodeRepository; import de.avatic.lcc.repositories.properties.PropertyRepository; import de.avatic.lcc.service.CustomApiService; -import de.avatic.lcc.calculationmodel.SectionInfo; import org.springframework.stereotype.Service; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.List; @@ -34,6 +36,17 @@ 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 doD2dCalculation(Premise premise, Destination destination, List sections) { var destUnion = countryPropertyRepository.getByMappingIdAndCountryId(CountryPropertyMappingId.UNION, destination.getCountryId()).orElseThrow(); @@ -42,7 +55,9 @@ public class CustomCostCalculationService { if (!CustomUnionType.EU.name().equals(destUnion.getCurrentValue()) || !CustomUnionType.NONE.name().equals(sourceUnion.getCurrentValue())) return CustomResult.EMPTY; - return getCustomCalculationResult(premise, destination, sections.getFirst().result().getAnnualCost(), sections.getFirst().result().getAnnualChanceCost(), sections.getFirst().result().getAnnualRiskCost()); + double huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(sections.getFirst().containerResult().getHuUnitCount()),2, RoundingMode.HALF_UP).doubleValue(); + + return getCustomCalculationResult(premise, destination, getContainerShare(premise, sections.getFirst().containerResult()), huAnnualAmount, sections.getFirst().result().getAnnualCost(), sections.getFirst().result().getAnnualChanceCost(), sections.getFirst().result().getAnnualRiskCost()); } public CustomResult doCalculation(Premise premise, Destination destination, List sections) { @@ -58,19 +73,22 @@ public class CustomCostCalculationService { var transportationChanceCost = relevantSections.stream().map(s -> s.result().getAnnualChanceCost()).reduce(BigDecimal.ZERO, BigDecimal::add); var transportationRiskCost = relevantSections.stream().map(s -> s.result().getAnnualRiskCost()).reduce(BigDecimal.ZERO, BigDecimal::add); - return getCustomCalculationResult(premise, destination, transportationCost, transportationChanceCost, transportationRiskCost); + + double huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(relevantSections.getFirst().containerResult().getHuUnitCount()),2, RoundingMode.HALF_UP).doubleValue(); + + return getCustomCalculationResult(premise, destination, getContainerShare(premise, relevantSections.getFirst().containerResult()), huAnnualAmount, transportationCost, transportationChanceCost, transportationRiskCost); } return CustomResult.EMPTY; } - private CustomResult getCustomCalculationResult(Premise premise, Destination destination, BigDecimal transportationCost, BigDecimal transportationChanceCost, BigDecimal transportationRiskCost) { - var shippingFrequency = shippingFrequencyCalculationService.doCalculation(destination.getAnnualAmount()); + private CustomResult getCustomCalculationResult(Premise premise, Destination destination, BigDecimal containerShare, double huAnnualAmount, BigDecimal transportationCost, BigDecimal transportationChanceCost, BigDecimal transportationRiskCost) { + var shippingFrequency = shippingFrequencyCalculationService.doCalculation(huAnnualAmount); var customFee = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.CUSTOM_FEE).orElseThrow().getCurrentValue()); - var tariffRate = premise.getTariffRate() == null ? customApiService.getTariffRate(premise.getHsCode(), premise.getCountryId()) : premise.getTariffRate(); + var tariffRate = premise.getTariffRate() == null ? customApiService.getTariffRate(premise.getHsCode(), premise.getCountryId()) : premise.getTariffRate(); - var materialCost = premise.getMaterialCost(); + var materialCost = premise.getMaterialCost().multiply(BigDecimal.valueOf(destination.getAnnualAmount())); var fcaFee = BigDecimal.ZERO; if (premise.getFcaEnabled()) { @@ -81,16 +99,16 @@ public class CustomCostCalculationService { var customValue = materialCost.add(fcaFee).add(transportationCost); var customDuties = customValue.multiply(tariffRate); - var annualCustomFee = shippingFrequency * customFee; - var annualCost = customDuties.add(BigDecimal.valueOf(annualCustomFee)); + var annualCustomFee = BigDecimal.valueOf(shippingFrequency).multiply(BigDecimal.valueOf(customFee)).multiply(containerShare); + var annualCost = customDuties.add(annualCustomFee); var customRiskValue = materialCost.add(fcaFee).add(transportationRiskCost); var customRiskDuties = customRiskValue.multiply(tariffRate); - var annualRiskCost = customRiskDuties.add(BigDecimal.valueOf(annualCustomFee)); + var annualRiskCost = customRiskDuties.add(annualCustomFee); var customChanceValue = materialCost.add(fcaFee).add(transportationChanceCost); var customChanceDuties = customChanceValue.multiply(tariffRate); - var annualChanceCost = customChanceDuties.add(BigDecimal.valueOf(annualCustomFee)); + var annualChanceCost = customChanceDuties.add(annualCustomFee); return new CustomResult(customValue, customRiskValue, customChanceValue, customDuties, tariffRate, annualCost, annualRiskCost, annualChanceCost); } @@ -116,7 +134,7 @@ public class CustomCostCalculationService { private CustomUnionType getCustomUnionByCountryId(Integer countryId) { var property = countryPropertyRepository.getByMappingIdAndCountryId(CountryPropertyMappingId.UNION, countryId).orElseThrow(); - if(property.getCurrentValue() == null) + if (property.getCurrentValue() == null) return CustomUnionType.NONE; return CustomUnionType.valueOf(property.getCurrentValue().toUpperCase()); 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 521b668..eba674d 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 @@ -12,6 +12,7 @@ import de.avatic.lcc.repositories.properties.PropertyRepository; import org.springframework.stereotype.Service; import java.math.BigDecimal; +import java.math.RoundingMode; @Service public class HandlingCostCalculationService { @@ -66,7 +67,7 @@ public class HandlingCostCalculationService { private HandlingResult getLLCCost(Destination destination, PackagingDimension hu, LoadCarrierType type, boolean addRepackingCosts) { - int huAnnualAmount = destination.getAnnualAmount() / hu.getContentUnitCount(); + double huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(hu.getContentUnitCount()),2, RoundingMode.UP ).doubleValue(); double handling = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.GLT_HANDLING).orElseThrow().getCurrentValue()); double release = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.GLT_RELEASE).orElseThrow().getCurrentValue()); double dispatch = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.GLT_DISPATCH).orElseThrow().getCurrentValue()); 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 910b831..26bda83 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 @@ -32,6 +32,7 @@ public class InventoryCostCalculationService { public InventoryCostResult doCalculation(Premise premise, Destination destination, BigDecimal leadTime) { + var fcaFee = BigDecimal.ZERO; if (premise.getFcaEnabled()) { @@ -41,6 +42,7 @@ public class InventoryCostCalculationService { } var hu = premiseToHuService.createHuFromPremise(premise); + double huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(hu.getContentUnitCount()),0, RoundingMode.UP ).doubleValue(); var safetydays = BigDecimal.valueOf(Integer.parseInt(countryPropertyRepository.getByMappingIdAndCountryId(CountryPropertyMappingId.SAFETY_STOCK, premise.getCountryId()).orElseThrow().getCurrentValue())); var workdays = BigDecimal.valueOf(Integer.parseInt(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.WORKDAYS).orElseThrow().getCurrentValue())); @@ -51,7 +53,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(destination.getAnnualAmount()),1)), 10, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(.5))); + var opStock = (annualAmount.divide(BigDecimal.valueOf(Math.max(shippingFrequencyCalculationService.doCalculation(huAnnualAmount),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 cbc6a8b..99274db 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 @@ -79,9 +79,11 @@ public class RouteSectionCostCalculationService { PriceCalculationResult prices = calculatePrices( premise.getHuMixable(), rate, + containerCalculation.isWeightExceeded(), ContainerType.FEU, containerCalculation.getMaxContainerWeight(), BigDecimal.valueOf(containerCalculation.getTotalUtilizationByVolume()), + BigDecimal.valueOf(containerCalculation.getHuUtilizationByWeight()), utilization); result.setCbmPrice(!containerCalculation.isWeightExceeded()); @@ -143,24 +145,26 @@ public class RouteSectionCostCalculationService { result.setTransitTime(transitTime); // Calculate price and annual cost - int huAnnualAmount = destination.getAnnualAmount() / containerCalculation.getHu().getContentUnitCount(); + BigDecimal huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(containerCalculation.getHu().getContentUnitCount()), 2, RoundingMode.HALF_UP); BigDecimal utilization = getUtilization(section.getRateType()); - BigDecimal annualVolume = BigDecimal.valueOf(huAnnualAmount * containerCalculation.getHu().getVolume(DimensionUnit.M)); - BigDecimal annualWeight = BigDecimal.valueOf(huAnnualAmount * containerCalculation.getHu().getWeight(WeightUnit.KG)); + 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(), rate, + containerCalculation.isWeightExceeded(), containerCalculation.getContainerType(), containerCalculation.getMaxContainerWeight(), BigDecimal.valueOf(containerCalculation.getTotalUtilizationByVolume()), + BigDecimal.valueOf(containerCalculation.getTotalUtilizationByWeight()), utilization); result.setCbmPrice(!containerCalculation.isWeightExceeded()); result.setWeightPrice(containerCalculation.isWeightExceeded()); result.setCbmPrice(prices.volumePrice); result.setWeightPrice(prices.weightPrice); - result.setUtilization(prices.utilization); + result.setUtilization(!containerCalculation.isWeightExceeded() || !premise.getHuMixable() ? prices.utilization : BigDecimal.valueOf(1.0)); var chanceRiskFactors = changeRiskFactorCalculationService.getChanceRiskFactors(); @@ -180,11 +184,14 @@ public class RouteSectionCostCalculationService { private PriceCalculationResult calculatePrices( boolean huMixable, BigDecimal rate, + boolean weightExceeded, ContainerType containerType, int maxContainerWeight, BigDecimal totalVolumeUtilization, + BigDecimal totalWeightUtilization, BigDecimal propertyUtilization) { + BigDecimal utilization; BigDecimal volumePrice; BigDecimal weightPrice; @@ -194,12 +201,12 @@ public class RouteSectionCostCalculationService { if (huMixable) { volumePrice = cbmRate.divide(propertyUtilization, 10, RoundingMode.HALF_UP); - weightPrice = weightRate.divide(propertyUtilization, 10, RoundingMode.HALF_UP); - utilization = propertyUtilization; + weightPrice = weightRate.divide(BigDecimal.valueOf(1), 10, RoundingMode.HALF_UP); + utilization = weightExceeded ? BigDecimal.ONE : propertyUtilization; } else { volumePrice = cbmRate.divide(totalVolumeUtilization, 10, RoundingMode.HALF_UP); - weightPrice = weightRate.divide(totalVolumeUtilization, 10, RoundingMode.HALF_UP); - utilization = totalVolumeUtilization; + weightPrice = weightRate.divide(totalWeightUtilization, 10, RoundingMode.HALF_UP); + utilization = weightExceeded ? totalWeightUtilization : totalVolumeUtilization; } 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 f815e29..c06e410 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 @@ -24,4 +24,15 @@ public class ShippingFrequencyCalculationService { } + public double doCalculation(double huAnnualAmount) { + Integer minAnnualFrequency = Integer.parseInt(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.FREQ_MIN).orElseThrow().getCurrentValue()); + Integer maxAnnualFrequency = Integer.parseInt(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.FREQ_MAX).orElseThrow().getCurrentValue()); + + if (huAnnualAmount > maxAnnualFrequency.doubleValue()) + return maxAnnualFrequency; + + return Math.max(huAnnualAmount, minAnnualFrequency.doubleValue()); + + } + } 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 ea846ca..0ae7e34 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 @@ -1,6 +1,5 @@ package de.avatic.lcc.service.transformer.report; -import de.avatic.lcc.dto.generic.ContainerType; import de.avatic.lcc.dto.generic.NodeType; import de.avatic.lcc.dto.report.ReportDTO; import de.avatic.lcc.dto.report.ReportDestinationDTO; @@ -50,13 +49,15 @@ public class ReportTransformer { List destinations = calculationJobDestinationRepository.getDestinationsByJobId(job.getId()); Map> sections = calculationJobRouteSectionRepository.getRouteSectionsByDestinationIds(destinations.stream().map(CalculationJobDestination::getId).toList()); + var weightedTotalCost = getWeightedTotalCosts(sections); + Premise premise = premiseRepository.getPremiseById(job.getPremiseId()).orElseThrow(); - reportDTO.setCost(getCostMap(job, destinations)); - reportDTO.setRisk(getRisk(job, destinations)); + reportDTO.setCost(getCostMap(job, destinations, weightedTotalCost)); + reportDTO.setRisk(getRisk(job, destinations, weightedTotalCost)); reportDTO.setDestination(destinations.stream().map(d -> getDestinationDTO(d, sections.get(d.getId()), premise)).toList()); - if(!reportDTO.getDestinations().isEmpty()) { + if (!reportDTO.getDestinations().isEmpty()) { var source = reportDTO.getDestinations().getFirst().getSections().stream().map(ReportSectionDTO::getFromNode).filter(n -> n.getTypes().contains(NodeType.SOURCE)).findFirst().orElseThrow(); reportDTO.setSupplier(source); } @@ -65,9 +66,35 @@ public class ReportTransformer { } + private WeightedTotalCosts getWeightedTotalCosts(Map> sectionsMap) { + + BigDecimal totalPreRunCost = BigDecimal.ZERO; + BigDecimal totalMainRunCost = BigDecimal.ZERO; + BigDecimal totalPostRunCost = BigDecimal.ZERO; + + BigDecimal totalCost = BigDecimal.ZERO; + + for (List sections : sectionsMap.values()) { + for (CalculationJobRouteSection section : sections) { + + if (section.getPreRun()) + totalPreRunCost = totalPreRunCost.add(section.getAnnualCost()); + if(section.getMainRun()) + totalMainRunCost = totalMainRunCost.add(section.getAnnualCost()); + if(section.getPostRun()) + totalPostRunCost = totalPostRunCost.add(section.getAnnualCost()); + + totalCost = totalCost.add(section.getAnnualCost()); + } + } + + return new WeightedTotalCosts(totalPreRunCost, totalMainRunCost, totalPostRunCost, totalCost); + } + private ReportDestinationDTO getDestinationDTO(CalculationJobDestination destination, List sections, Premise premise) { var destinationNode = nodeRepository.getByDestinationId(destination.getPremiseDestinationId()).orElseThrow(); + var dimensionUnit = premise.getHuDisplayedDimensionUnit(); var weightUnit = premise.getHuDisplayedWeightUnit(); @@ -76,23 +103,26 @@ public class ReportTransformer { var totalAnnualCost = sections.stream().map(CalculationJobRouteSection::getAnnualCost).reduce(BigDecimal.ZERO, BigDecimal::add); destinationDTO.getSections().forEach(s -> { - s.getCost().setPercentage(s.getCost().getTotal().doubleValue()/totalAnnualCost.doubleValue()); + s.getCost().setPercentage(s.getCost().getTotal().doubleValue() / totalAnnualCost.doubleValue()); }); destinationDTO.getSections().forEach(s -> { - s.getDuration().setPercentage(s.getDuration().getTotal().doubleValue()/totalAnnualCost.doubleValue()); + s.getDuration().setPercentage(s.getDuration().getTotal().doubleValue() / totalAnnualCost.doubleValue()); }); + destinationDTO.setId(destination.getId()); + destinationDTO.setAnnualQuantity(destination.getAnnualAmount().intValue()); + destinationDTO.setDestination(nodeTransformer.toNodeDTO(destinationNode)); destinationDTO.setDimensionUnit(dimensionUnit); destinationDTO.setWeightUnit(weightUnit); - destinationDTO.setHeight(dimensionUnit.convertFromMM(premise.getIndividualHuHeight()).doubleValue()); - destinationDTO.setWidth(dimensionUnit.convertFromMM(premise.getIndividualHuWidth()).doubleValue()); - destinationDTO.setLength(dimensionUnit.convertFromMM(premise.getIndividualHuLength()).doubleValue()); - destinationDTO.setWeight(weightUnit.convertFromG(premise.getIndividualHuWeight()).doubleValue()); + destinationDTO.setHeight(dimensionUnit.convertFromMM(premise.getIndividualHuHeight())); + destinationDTO.setWidth(dimensionUnit.convertFromMM(premise.getIndividualHuWidth())); + destinationDTO.setLength(dimensionUnit.convertFromMM(premise.getIndividualHuLength())); + destinationDTO.setWeight(weightUnit.convertFromG(premise.getIndividualHuWeight())); + destinationDTO.setHuUnitCount(premise.getHuUnitCount()); destinationDTO.setLayer(destination.getLayerCount()); - destinationDTO.setUnitCount(premise.getHuUnitCount()); destinationDTO.setOverseaShare(premise.getOverseaShare().doubleValue()); destinationDTO.setSafetyStock(destination.getSafetyStock().doubleValue()); @@ -105,10 +135,11 @@ public class ReportTransformer { destinationDTO.setRate(mainRun == null ? 0 : mainRun.getRate()); destinationDTO.setType(destination.getContainerType()); destinationDTO.setUtilization(destination.getContainerUtilization()); - destinationDTO.setUnitCount(premise.getHuUnitCount()); + destinationDTO.setUnitCount(destination.getHuCount()); destinationDTO.setWeightExceeded(destination.getTransportWeightExceeded()); destinationDTO.setHsCode(premise.getHsCode()); + destinationDTO.setTariffRate(destination.getTariffRate()); return destinationDTO; } @@ -133,85 +164,124 @@ public class ReportTransformer { } - private Map getRisk(CalculationJob job, List destination) { + private Map getRisk(CalculationJob job, List destination, WeightedTotalCosts weightedTotalCost) { Map risk = new HashMap<>(); - var totalValue = BigDecimal.valueOf(1); // job.getWeightedTotalCosts(); TODO since this is not stored in table, needs to be calculated + var annualAmount = destination.stream().map(CalculationJobDestination::getAnnualAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + + + var airfreightValue = destination.stream().map(CalculationJobDestination::getAnnualAirFreightCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP); + var worstValue = destination.stream().map(CalculationJobDestination::getTotalRiskCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP); + var bestValue = destination.stream().map(CalculationJobDestination::getTotalChanceCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP); + + + var totalValue = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getTotalCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP); + //var totalValue = weightedTotalCost.totalCost.divide(annualAmount, 2, RoundingMode.HALF_UP); ReportEntryDTO airfreight = new ReportEntryDTO(); - var airfreightValue = destination.stream().map(CalculationJobDestination::getAnnualAirFreightCost).reduce(BigDecimal.ZERO, BigDecimal::add); airfreight.setTotal(airfreightValue); - airfreight.setPercentage(airfreightValue.divide(totalValue, 2, RoundingMode.HALF_UP)); + airfreight.setPercentage(airfreightValue.divide(totalValue, 4, RoundingMode.HALF_UP)); risk.put("air_freight_cost", airfreight); ReportEntryDTO worst = new ReportEntryDTO(); - var worstValue = destination.stream().map(CalculationJobDestination::getTotalRiskCost).reduce(BigDecimal.ZERO, BigDecimal::add); worst.setTotal(worstValue); - worst.setPercentage(worstValue.divide(totalValue, 2, RoundingMode.HALF_UP)); + worst.setPercentage(worstValue.divide(totalValue, 4, RoundingMode.HALF_UP)); risk.put("worst_case_cost", worst); ReportEntryDTO best = new ReportEntryDTO(); - var bestValue = destination.stream().map(CalculationJobDestination::getTotalChanceCost).reduce(BigDecimal.ZERO, BigDecimal::add); best.setTotal(bestValue); - best.setPercentage(bestValue.divide(totalValue, 2, RoundingMode.HALF_UP)); + best.setPercentage(bestValue.divide(totalValue, 4, RoundingMode.HALF_UP)); risk.put("best_case_cost", best); return risk; } - private Map getCostMap(CalculationJob job, List destination) { + private Map getCostMap(CalculationJob job, List destination, WeightedTotalCosts weightedTotalCost) { Map cost = new HashMap<>(); + var annualAmount = destination.stream().map(CalculationJobDestination::getAnnualAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + + var materialValue = destination.stream().map(CalculationJobDestination::getMaterialCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(BigDecimal.valueOf(destination.size()), 4, RoundingMode.HALF_UP); + var fcaFeesValues = destination.stream().map(CalculationJobDestination::getFcaCost).reduce(BigDecimal.ZERO, BigDecimal::add); + var repackingValues = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualRepackingCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP); + var handlingValues = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualHandlingCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP); + var storageValues = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualStorageCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP); + var capitalValues = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualCapitalCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP); + var disposalValues = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualDisposalCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP); + var customValues = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualCustomCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP); + + var preRunValues = weightedTotalCost.totalPreRunCost.divide(annualAmount, 4, RoundingMode.HALF_UP); + var mainRunValues = weightedTotalCost.totalMainRunCost.divide(annualAmount, 4, RoundingMode.HALF_UP); + var postRunValues = weightedTotalCost.totalPostRunCost.divide(annualAmount, 4, RoundingMode.HALF_UP); + + + var totalValue = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getTotalCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP); + ReportEntryDTO total = new ReportEntryDTO(); - var totalValue = BigDecimal.valueOf(1); // job.getWeightedTotalCosts(); TODO since this is not stored in table, needs to be calculated total.setTotal(totalValue); - total.setPercentage(BigDecimal.valueOf(100)); + total.setPercentage(BigDecimal.valueOf(1)); cost.put("total", total); + ReportEntryDTO preRun = new ReportEntryDTO(); + preRun.setTotal(preRunValues); + preRun.setPercentage(preRunValues.divide(totalValue, 4, RoundingMode.HALF_UP)); + cost.put("preRun", preRun); + + ReportEntryDTO mainRun = new ReportEntryDTO(); + mainRun.setTotal(mainRunValues); + mainRun.setPercentage(mainRunValues.divide(totalValue, 4, RoundingMode.HALF_UP)); + cost.put("mainRun", mainRun); + + ReportEntryDTO postRun = new ReportEntryDTO(); + postRun.setTotal(postRunValues); + postRun.setPercentage(postRunValues.divide(totalValue, 4, RoundingMode.HALF_UP)); + cost.put("postRun", postRun); + ReportEntryDTO material = new ReportEntryDTO(); - var materialValue = destination.stream().map(CalculationJobDestination::getMaterialCost).reduce(BigDecimal.ZERO, BigDecimal::add); material.setTotal(materialValue); - material.setPercentage(materialValue.divide(totalValue, 2, RoundingMode.HALF_UP)); + material.setPercentage(materialValue.divide(totalValue, 4, RoundingMode.HALF_UP)); cost.put("material", material); + ReportEntryDTO custom = new ReportEntryDTO(); + custom.setTotal(customValues); + custom.setPercentage(customValues.divide(totalValue, 4, RoundingMode.HALF_UP)); + cost.put("custom", custom); + ReportEntryDTO fcaFees = new ReportEntryDTO(); - var fcaFeesValues = destination.stream().map(CalculationJobDestination::getFcaCost).reduce(BigDecimal.ZERO, BigDecimal::add); fcaFees.setTotal(fcaFeesValues); - fcaFees.setPercentage(fcaFeesValues.divide(totalValue, 2, RoundingMode.HALF_UP)); + fcaFees.setPercentage(fcaFeesValues.divide(totalValue, 4, RoundingMode.HALF_UP)); cost.put("fcaFees", fcaFees); ReportEntryDTO repacking = new ReportEntryDTO(); - var repackingValues = destination.stream().map(CalculationJobDestination::getAnnualRepackingCost).reduce(BigDecimal.ZERO, BigDecimal::add); repacking.setTotal(repackingValues); - repacking.setPercentage(repackingValues.divide(totalValue, 2, RoundingMode.HALF_UP)); + repacking.setPercentage(repackingValues.divide(totalValue, 4, RoundingMode.HALF_UP)); cost.put("repacking", repacking); ReportEntryDTO handling = new ReportEntryDTO(); - var handlingValues = destination.stream().map(CalculationJobDestination::getAnnualHandlingCost).reduce(BigDecimal.ZERO, BigDecimal::add); handling.setTotal(handlingValues); - handling.setPercentage(handlingValues.divide(totalValue, 2, RoundingMode.HALF_UP)); + handling.setPercentage(handlingValues.divide(totalValue, 4, RoundingMode.HALF_UP)); cost.put("handling", handling); ReportEntryDTO storage = new ReportEntryDTO(); - var storageValues = destination.stream().map(CalculationJobDestination::getAnnualStorageCost).reduce(BigDecimal.ZERO, BigDecimal::add); storage.setTotal(storageValues); - storage.setPercentage(storageValues.divide(totalValue, 2, RoundingMode.HALF_UP)); + storage.setPercentage(storageValues.divide(totalValue, 4, RoundingMode.HALF_UP)); cost.put("storage", storage); ReportEntryDTO capital = new ReportEntryDTO(); - var capitalValues = destination.stream().map(CalculationJobDestination::getAnnualCapitalCost).reduce(BigDecimal.ZERO, BigDecimal::add); capital.setTotal(capitalValues); - capital.setPercentage(capitalValues.divide(totalValue, 2, RoundingMode.HALF_UP)); + capital.setPercentage(capitalValues.divide(totalValue, 4, RoundingMode.HALF_UP)); cost.put("capital", capital); ReportEntryDTO disposal = new ReportEntryDTO(); - var disposalValues = destination.stream().map(CalculationJobDestination::getAnnualDisposalCost).reduce(BigDecimal.ZERO, BigDecimal::add); disposal.setTotal(disposalValues); - disposal.setPercentage(disposalValues.divide(totalValue, 2, RoundingMode.HALF_UP)); + disposal.setPercentage(disposalValues.divide(totalValue, 4, RoundingMode.HALF_UP)); cost.put("disposal", disposal); return cost; } + private record WeightedTotalCosts(BigDecimal totalPreRunCost, BigDecimal totalMainRunCost, + BigDecimal totalPostRunCost, BigDecimal totalCost) { + } } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 0eb17bb..30ecad5 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -516,9 +516,8 @@ CREATE TABLE IF NOT EXISTS calculation_job_destination -- transportation is_d2d BOOLEAN DEFAULT FALSE, rate_d2d DECIMAL(15, 2) DEFAULT NULL, - container_type CHAR(8) CHECK (container_type IN - ('TEU', 'FEU', 'HC', 'TRUCK')), - hu_count INT UNSIGNED NOT NULL COMMENT 'number of handling units int total', + container_type CHAR(8), + hu_count INT UNSIGNED NOT NULL COMMENT 'number of handling units in total (full container, with layers)', layer_structure JSON COMMENT 'json representation of a single layer', layer_count INT UNSIGNED NOT NULL COMMENT 'number of layers per full container or truck', transport_weight_exceeded BOOLEAN DEFAULT FALSE COMMENT 'limiting factor: TRUE if weight limited or FALSE if volume limited', @@ -533,7 +532,8 @@ CREATE TABLE IF NOT EXISTS calculation_job_destination FOREIGN KEY (calculation_job_id) REFERENCES calculation_job (id), FOREIGN KEY (premise_destination_id) REFERENCES premise_destination (id), INDEX idx_calculation_job_id (calculation_job_id), - INDEX idx_premise_destination_id (premise_destination_id) + INDEX idx_premise_destination_id (premise_destination_id), + CONSTRAINT chk_container_type CHECK (container_type IN ('TEU', 'FEU', 'HC', 'TRUCK')) ); CREATE TABLE IF NOT EXISTS calculation_job_route_section