+
@@ -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 @@
+
+
+
+
+
+
Select suppliers to compare:
+
+
+
no suppliers found.
+
+
+
+
+
+
+
+
+
\ 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 report selected
+
+
+
+
+
+
+
+
+
+
+
-
-
-
\ 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