BACKEND: several bugfixing in calculation and reporting

FRONTEND: Start implementing reporting
This commit is contained in:
Jan 2025-09-07 23:48:51 +02:00
parent c47531a335
commit ca3c15ecd2
28 changed files with 906 additions and 95 deletions

View file

@ -0,0 +1,15 @@
<script lang="ts">
import {defineComponent} from 'vue'
export default defineComponent({
name: "ReportChart"
})
</script>
<template>
</template>
<style scoped>
</style>

View file

@ -0,0 +1,15 @@
<script lang="ts">
import {defineComponent} from 'vue'
export default defineComponent({
name: "ReportRoute"
})
</script>
<template>
</template>
<style scoped>
</style>

View file

@ -1,13 +1,12 @@
<template>
<div class="item-container">
<div class="item-container" :class="{'selected-item': selected}">
<flag :iso="isoCode" size="l"></flag>
<div class="supplier-item-text">
<div class="supplier-item-name"> <span class="user-icon" v-if="isUserSupplier"><ph-user weight="fill" ></ph-user></span> {{name}}</div>
<div class="supplier-item-address">{{ address }}</div>
</div>
<icon-button icon="trash" @click="deleteClick"></icon-button>
<icon-button v-if="showTrash" icon="trash" @click="deleteClick"></icon-button>
</div>
</template>
@ -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 {
<style scoped>
.item-container {
display: flex;
justify-content: space-between;
@ -63,13 +73,25 @@ export default {
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
overflow: hidden;
gap: 2.4rem;
flex: 0 0 50rem
flex: 0 0 50rem;
transition: background-color 0.3s ease;
}
.item-container:hover {
background-color: rgba(107, 134, 156, 0.02);
}
.selected-item {
background-color: #c3cfdf;
}
.selected-item:hover {
background-color: #a6b6ca;
}
.user-icon {
display: inline-flex;
align-items: center;
@ -79,7 +101,7 @@ export default {
.supplier-item-name {
font-size: 1.6rem;
font-weight: 600;
color: #001D33;
color: #002F54;
display: flex;
gap: 0.8rem;
align-items: center;

View file

@ -37,10 +37,10 @@
></dropdown>
</div>
<transition name="properties-fade" mode="out-in">
<div v-if="!loading" class="properties-list" :key="selectedPeriod">
<div v-if="!loading" class="properties-list">
<transition-group name="property-item" tag="div">
<property v-for="property in properties"
:key="`${selectedPeriod}-${selectedCountry.id}-${property.external_mapping_id}`"
<property v-for="property of properties"
:key="`${selectedPeriodId}-${selectedCountry.id}-${property.external_mapping_id}`"
:property="property"
:disabled="!isValidPeriodActive"
@save="saveProperty"></property>
@ -66,6 +66,7 @@ import IconButton from "@/components/UI/IconButton.vue";
import Tooltip from "@/components/UI/Tooltip.vue";
import ModalDialog from "@/components/UI/ModalDialog.vue";
import Dropdown from "@/components/UI/Dropdown.vue";
import {usePropertiesStore} from "@/store/properties.js";
export default {
name: "CountryProperties",
@ -94,12 +95,12 @@ export default {
loading() {
return this.countryStore.isLoading;
},
...mapStores(useCountryStore, usePropertySetsStore),
...mapStores(useCountryStore, usePropertySetsStore, usePropertiesStore),
countries() {
return this.countryStore.getCountries;
},
properties() {
return this.selectedCountry.properties;
return this.countryStore.getSelectedCountry.properties;
},
isValidPeriodActive() {
const state = this.propertySetsStore.getPeriodState(this.selectedPeriod);
@ -108,18 +109,24 @@ export default {
selectedCountry() {
return this.countryStore.getSelectedCountry;
},
selectedPeriodId() {
return this.propertySetsStore.getSelectedPeriod;
},
selectedPeriod: {
get() {
return this.propertySetsStore.getSelectedPeriod
},
set(value) {
console.log(value)
async set(value) {
this.propertySetsStore.setSelectedPeriod(value);
this.countryStore.selectPeriod(value);
await this.countryStore.selectPeriod(value);
await this.propertiesStore.loadProperties(value);
}
},
},
methods: {
buildDate(date) {
return `${date[0]}-${date[1].toString().padStart(2, '0')}-${date[2].toString().padStart(2, '0')} ${date[3].toString().padStart(2, '0')}:${date[4].toString().padStart(2, '0')}:${date[5].toString().padStart(2, '0')}`
},
async saveProperty(property) {
this.countryStore.setProperty(property);

View file

@ -47,6 +47,7 @@ import Tooltip from "@/components/UI/Tooltip.vue";
import ModalDialog from "@/components/UI/ModalDialog.vue";
import NotificationBar from "@/components/UI/NotificationBar.vue";
import {usePropertySetsStore} from "@/store/propertySets.js";
import {useCountryStore} from "@/store/country.js";
export default {
name: "Properties",
@ -58,7 +59,7 @@ export default {
}
},
computed: {
...mapStores(usePropertiesStore, usePropertySetsStore),
...mapStores(usePropertiesStore, usePropertySetsStore, useCountryStore),
loading() {
return this.propertiesStore.isLoading;
},
@ -74,10 +75,10 @@ export default {
get() {
return this.propertySetsStore.getSelectedPeriod
},
set(value) {
console.log(value)
async set(value) {
this.propertySetsStore.setSelectedPeriod(value);
this.propertiesStore.loadProperties(value);
await this.propertiesStore.loadProperties(value);
await this.countryStore.selectPeriod(value);
}
},
properties() {

View file

@ -0,0 +1,14 @@
<script>
export default {
name: "DestinationCost"
}
</script>
<template>
</template>
<style scoped>
</style>

View file

@ -0,0 +1,14 @@
<script>
export default {
name: "OverviewCost"
}
</script>
<template>
</template>
<style scoped>
</style>

View file

@ -0,0 +1,14 @@
<script>
export default {
name: "SingleReport"
}
</script>
<template>
</template>
<style scoped>
</style>

View file

@ -0,0 +1,166 @@
<template>
<div class="container">
<div><h3 class="sub-header">Prepare report</h3></div>
<div class="search-bar-container">
<div class="caption">Find material:</div>
<div class="search-bar">
<autosuggest-searchbar @selected="searchReport"
:fetch-suggestions="getMaterial"
placeholder="Select material for reporting"
title-resolver="part_number"
:initial-value="partNumber"
></autosuggest-searchbar>
</div>
</div>
<div class="content-container" v-if="suppliers.length !== 0">
<div class="caption">Select suppliers to compare:</div>
<ul class="item-list">
<li class="item-list-element" v-for="supplier in suppliers" :key="supplier.id"
@click="selectSupplier(supplier.id)">
<supplier-item :id="String(supplier.id)"
:iso-code="supplier.country.iso_code"
:address="supplier.address"
:name="supplier.name"
:show-trash="false"
:is-user-supplier="supplier.isUserSupplier"
:selected="isSelected(supplier.id)"
></supplier-item>
</li>
</ul>
</div>
<div class="content-container-empty" v-else>no suppliers found.</div>
<div class="footer">
<basic-button :show-icon="false" @click="close('accept')" :disabled="!hasSelectedSuppliers">OK</basic-button>
<basic-button :show-icon="false" variant="secondary" @click="close('discard')">Cancel</basic-button>
</div>
</div>
</template>
<script>
import AutosuggestSearchbar from "@/components/UI/AutoSuggestSearchBar.vue";
import BasicButton from "@/components/UI/BasicButton.vue";
import {mapStores} from "pinia";
import {useMaterialStore} from "@/store/material.js";
import {useReportSearchStore} from "@/store/reportSearch.js";
import SupplierItem from "@/components/layout/assistant/SupplierItem.vue";
export default {
name: "SelectForReport",
components: {SupplierItem, BasicButton, AutosuggestSearchbar},
data() {
return {
selectedMaterialId: null,
}
},
created() {
this.selectedMaterialId = this.reportSearchStore.getMaterial?.id;
},
computed: {
...mapStores(useMaterialStore, useReportSearchStore),
suppliers() {
return this.reportSearchStore.getSuppliers;
},
hasSelectedSuppliers() {
return this.reportSearchStore.getSelectedIds.length > 0;
},
partNumber() {
return this.reportSearchStore.getMaterial?.part_number;
}
},
methods: {
close(action) {
const ids = this.reportSearchStore.getSelectedIds;
this.$emit('close', {action: action, supplierIds: ids, materialId: this.selectedMaterialId});
},
isSelected(id) {
return this.reportSearchStore.isSelected(id);
},
selectSupplier(id) {
this.reportSearchStore.selectSupplier(id);
},
async getMaterial(query) {
const materialQuery = {searchTerm: query};
await this.materialStore.setQuery(materialQuery);
return this.materialStore.materials;
},
async searchReport(material) {
this.selectedMaterialId = material.id;
this.reportSearchStore.setMaterial(material);
}
}
}
</script>
<style scoped>
.container {
display: flex;
flex-direction: column;
gap: 2.4rem;
min-width: 90vw;
min-height: min(80vh, 100rem);
}
.search-bar-container {
display: flex;
align-items: center;
gap: 1.6rem;
}
.search-bar {
flex: 1 0 auto;
}
.caption {
font-size: 1.4rem;
font-weight: 500;
}
.content-container {
flex: 1;
overflow-x: auto; /* Enable horizontal scrolling */
overflow-y: hidden; /* Prevent vertical overflow */
min-height: 0; /* Allow flex item to shrink below content size */
}
.content-container-empty {
flex: 1;
overflow-x: auto; /* Enable horizontal scrolling */
overflow-y: hidden; /* Prevent vertical overflow */
min-height: 0; /* Allow flex item to shrink below content size */
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
font-weight: 400;
color: #6B869C;
}
.footer {
display: flex;
flex-direction: row;
gap: 1.6rem;
align-items: center;
justify-content: flex-end;
}
.item-list {
display: flex;
list-style: none;
gap: 2.4rem 2.4rem;
margin: 4.8rem;
flex-wrap: nowrap;
min-width: max-content;
}
.item-list-element {
display: flex;
justify-content: space-between;
cursor: pointer;
}
</style>

View file

@ -0,0 +1,14 @@
<script>
export default {
name: "ReportWeightedCost"
}
</script>
<template>
</template>
<style scoped>
</style>

View file

@ -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);

View file

@ -1,14 +1,95 @@
<template>
<div>
<div class="header-container">
<h2 class="page-header">Reporting</h2>
<div class="header-controls">
<basic-button @click="showModal = true" icon="file">Create report</basic-button>
</div>
</div>
<div v-if="hasReport">
<box>
</box>
</div>
<div v-else class="empty-container">
<box><span class="space-around">No report selected</span></box>
</div>
<modal :state="showModal">
<select-for-report @close="closeModal"></select-for-report>
</modal>
</div>
</template>
<script>
import Modal from "@/components/UI/Modal.vue";
import BasicButton from "@/components/UI/BasicButton.vue";
import SelectForReport from "@/components/layout/report/SelectForReport.vue";
import {mapStores} from "pinia";
import {useReportsStore} from "@/store/reports.js";
import Box from "@/components/UI/Box.vue";
export default {
name: "Reporting"
name: "Reporting",
components: {Box, SelectForReport, BasicButton, Modal},
data() {
return {
showModal: false,
}
},
computed: {
...mapStores(useReportsStore),
hasReport() {
return false;
}
},
methods: {
closeModal(data) {
console.log("closeModal: ", data.action)
if (data.action === 'accept') {
console.log("create report")
this.reportsStore.fetchReports(data.materialId, data.supplierIds);
}
this.showModal = false;
}
},
created() {
if(!this.hasReport)
this.showModal = true;
}
}
</script>
<template>
<h2 class="page-header">Reporting</h2>
</template>
<style scoped>
.space-around {
margin: 3rem;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1.6rem;
}
.header-controls {
display: flex;
gap: 1.6rem;
}
.empty-container {
display: flex;
justify-content: center;
align-items: center;
font-size: 1.6rem;
font-weight: 500;
}
</style>

View file

@ -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;

View file

@ -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;
}
},
});

View file

@ -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;
}
},
});

View file

@ -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<List<List<NodeDTO>>> 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<List<ReportDTO>> getReport(@RequestParam(value = "material") Integer materialId, @RequestParam(value = "sources") List<Integer> nodeIds) {
return ResponseEntity.ok(reportingService.getReport(materialId, nodeIds));
}

View file

@ -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"));

View file

@ -33,7 +33,7 @@ public class RouteRepository {
}
public Optional<Route> 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()) {

View file

@ -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();

View file

@ -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));

View file

@ -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;
}
}

View file

@ -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<SectionInfo> 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<SectionInfo> 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());

View file

@ -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());

View file

@ -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);

View file

@ -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);

View file

@ -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());
}
}

View file

@ -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<CalculationJobDestination> destinations = calculationJobDestinationRepository.getDestinationsByJobId(job.getId());
Map<Integer, List<CalculationJobRouteSection>> 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<Integer, List<CalculationJobRouteSection>> sectionsMap) {
BigDecimal totalPreRunCost = BigDecimal.ZERO;
BigDecimal totalMainRunCost = BigDecimal.ZERO;
BigDecimal totalPostRunCost = BigDecimal.ZERO;
BigDecimal totalCost = BigDecimal.ZERO;
for (List<CalculationJobRouteSection> 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<CalculationJobRouteSection> 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<String, ReportEntryDTO> getRisk(CalculationJob job, List<CalculationJobDestination> destination) {
private Map<String, ReportEntryDTO> getRisk(CalculationJob job, List<CalculationJobDestination> destination, WeightedTotalCosts weightedTotalCost) {
Map<String, ReportEntryDTO> 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<String, ReportEntryDTO> getCostMap(CalculationJob job, List<CalculationJobDestination> destination) {
private Map<String, ReportEntryDTO> getCostMap(CalculationJob job, List<CalculationJobDestination> destination, WeightedTotalCosts weightedTotalCost) {
Map<String, ReportEntryDTO> 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) {
}
}

View file

@ -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