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

View file

@ -37,10 +37,10 @@
></dropdown> ></dropdown>
</div> </div>
<transition name="properties-fade" mode="out-in"> <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"> <transition-group name="property-item" tag="div">
<property v-for="property in properties" <property v-for="property of properties"
:key="`${selectedPeriod}-${selectedCountry.id}-${property.external_mapping_id}`" :key="`${selectedPeriodId}-${selectedCountry.id}-${property.external_mapping_id}`"
:property="property" :property="property"
:disabled="!isValidPeriodActive" :disabled="!isValidPeriodActive"
@save="saveProperty"></property> @save="saveProperty"></property>
@ -66,6 +66,7 @@ import IconButton from "@/components/UI/IconButton.vue";
import Tooltip from "@/components/UI/Tooltip.vue"; import Tooltip from "@/components/UI/Tooltip.vue";
import ModalDialog from "@/components/UI/ModalDialog.vue"; import ModalDialog from "@/components/UI/ModalDialog.vue";
import Dropdown from "@/components/UI/Dropdown.vue"; import Dropdown from "@/components/UI/Dropdown.vue";
import {usePropertiesStore} from "@/store/properties.js";
export default { export default {
name: "CountryProperties", name: "CountryProperties",
@ -94,12 +95,12 @@ export default {
loading() { loading() {
return this.countryStore.isLoading; return this.countryStore.isLoading;
}, },
...mapStores(useCountryStore, usePropertySetsStore), ...mapStores(useCountryStore, usePropertySetsStore, usePropertiesStore),
countries() { countries() {
return this.countryStore.getCountries; return this.countryStore.getCountries;
}, },
properties() { properties() {
return this.selectedCountry.properties; return this.countryStore.getSelectedCountry.properties;
}, },
isValidPeriodActive() { isValidPeriodActive() {
const state = this.propertySetsStore.getPeriodState(this.selectedPeriod); const state = this.propertySetsStore.getPeriodState(this.selectedPeriod);
@ -108,18 +109,24 @@ export default {
selectedCountry() { selectedCountry() {
return this.countryStore.getSelectedCountry; return this.countryStore.getSelectedCountry;
}, },
selectedPeriodId() {
return this.propertySetsStore.getSelectedPeriod;
},
selectedPeriod: { selectedPeriod: {
get() { get() {
return this.propertySetsStore.getSelectedPeriod return this.propertySetsStore.getSelectedPeriod
}, },
set(value) { async set(value) {
console.log(value)
this.propertySetsStore.setSelectedPeriod(value); this.propertySetsStore.setSelectedPeriod(value);
this.countryStore.selectPeriod(value); await this.countryStore.selectPeriod(value);
await this.propertiesStore.loadProperties(value);
} }
}, },
}, },
methods: { 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) { async saveProperty(property) {
this.countryStore.setProperty(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 ModalDialog from "@/components/UI/ModalDialog.vue";
import NotificationBar from "@/components/UI/NotificationBar.vue"; import NotificationBar from "@/components/UI/NotificationBar.vue";
import {usePropertySetsStore} from "@/store/propertySets.js"; import {usePropertySetsStore} from "@/store/propertySets.js";
import {useCountryStore} from "@/store/country.js";
export default { export default {
name: "Properties", name: "Properties",
@ -58,7 +59,7 @@ export default {
} }
}, },
computed: { computed: {
...mapStores(usePropertiesStore, usePropertySetsStore), ...mapStores(usePropertiesStore, usePropertySetsStore, useCountryStore),
loading() { loading() {
return this.propertiesStore.isLoading; return this.propertiesStore.isLoading;
}, },
@ -74,10 +75,10 @@ export default {
get() { get() {
return this.propertySetsStore.getSelectedPeriod return this.propertySetsStore.getSelectedPeriod
}, },
set(value) { async set(value) {
console.log(value)
this.propertySetsStore.setSelectedPeriod(value); this.propertySetsStore.setSelectedPeriod(value);
this.propertiesStore.loadProperties(value); await this.propertiesStore.loadProperties(value);
await this.countryStore.selectPeriod(value);
} }
}, },
properties() { 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, PhArchive,
PhFloppyDisk, PhFloppyDisk,
PhArrowCounterClockwise, PhArrowCounterClockwise,
PhCheck, PhBug, PhShuffle, PhStack PhCheck, PhBug, PhShuffle, PhStack, PhFile
} from "@phosphor-icons/vue"; } from "@phosphor-icons/vue";
const app = createApp(App); const app = createApp(App);
@ -59,6 +59,7 @@ app.component('PhStar', PhStar);
app.component('PhBug', PhBug); app.component('PhBug', PhBug);
app.component('PhShuffle', PhShuffle); app.component('PhShuffle', PhShuffle);
app.component('PhStack', PhStack ); app.component('PhStack', PhStack );
app.component('PhFile', PhFile);
app.use(router); 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> <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 { 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> </script>
<template>
<h2 class="page-header">Reporting</h2>
</template>
<style scoped> <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> </style>

View file

@ -43,8 +43,11 @@ export const useCountryStore = defineStore('country', {
await stage.checkStagedChanges(); await stage.checkStagedChanges();
}, },
async selectPeriod(periodId) { async selectPeriod(periodId) {
this.loading = true;
this.countryDetail = null;
this.selectedPeriodId = periodId; this.selectedPeriodId = periodId;
await this.loadCountryDetail(); await this.loadCountryDetail();
this.loading = false;
}, },
async selectCountry(countryId) { async selectCountry(countryId) {
this.selectedCountryId = 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.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; 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; 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. * @param materialId The ID of the material for which suppliers need to be found.
* @return A list of suppliers grouped by categories. * @return A list of suppliers grouped by categories.
*/ */
@GetMapping("/search") @GetMapping({"/search", "/search/"})
public ResponseEntity<List<List<NodeDTO>>> findSupplierForReporting(@RequestParam(value = "material") Integer materialId) { public ResponseEntity<List<List<NodeDTO>>> findSupplierForReporting(@RequestParam(value = "material") Integer materialId) {
return ResponseEntity.ok(reportingService.findSupplierForReporting(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. * @param nodeIds A list of node IDs (sources) to include in the report.
* @return The generated report details. * @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) { public ResponseEntity<List<ReportDTO>> getReport(@RequestParam(value = "material") Integer materialId, @RequestParam(value = "sources") List<Integer> nodeIds) {
return ResponseEntity.ok(reportingService.getReport(materialId, nodeIds)); return ResponseEntity.ok(reportingService.getReport(materialId, nodeIds));
} }

View file

@ -95,7 +95,7 @@ public class CalculationJobDestinationRepository {
ps.setObject(paramIndex++, destination.getAnnualAirFreightCost()); ps.setObject(paramIndex++, destination.getAnnualAirFreightCost());
// Transportation // 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.getHuCount());
ps.setObject(paramIndex++, destination.getLayerStructure()); ps.setObject(paramIndex++, destination.getLayerStructure());
ps.setObject(paramIndex++, destination.getLayerCount()); ps.setObject(paramIndex++, destination.getLayerCount());
@ -179,6 +179,8 @@ public class CalculationJobDestinationRepository {
entity.setTransportWeightExceeded(rs.getBoolean("transport_weight_exceeded")); entity.setTransportWeightExceeded(rs.getBoolean("transport_weight_exceeded"));
entity.setShippingFrequency(rs.getInt("shipping_frequency")); entity.setShippingFrequency(rs.getInt("shipping_frequency"));
entity.setAnnualTransportationCost(rs.getBigDecimal("annual_transportation_cost")); entity.setAnnualTransportationCost(rs.getBigDecimal("annual_transportation_cost"));
entity.setTotalTransitTime(rs.getInt("transit_time_in_days"));
entity.setContainerUtilization(rs.getBigDecimal("container_utilization"));
// Material Cost fields // Material Cost fields
entity.setMaterialCost(rs.getBigDecimal("material_cost")); entity.setMaterialCost(rs.getBigDecimal("material_cost"));

View file

@ -33,7 +33,7 @@ public class RouteRepository {
} }
public Optional<Route> getSelectedByDestinationId(Integer id) { 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); var route = jdbcTemplate.query(query, new RouteMapper(), id);
if(route.isEmpty()) { 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()) { if (periods.isEmpty()) {
return Optional.empty(); return Optional.empty();

View file

@ -196,8 +196,8 @@ public class CalculationExecutionService {
.add(destinationCalculationJob.getAnnualDisposalCost()) .add(destinationCalculationJob.getAnnualDisposalCost())
.add(destinationCalculationJob.getAnnualCapitalCost()) .add(destinationCalculationJob.getAnnualCapitalCost())
.add(destinationCalculationJob.getAnnualStorageCost()) .add(destinationCalculationJob.getAnnualStorageCost())
.add(materialCost) .add(materialCost.multiply(BigDecimal.valueOf(destination.getAnnualAmount())))
.add(fcaFee); .add(fcaFee.multiply(BigDecimal.valueOf(destination.getAnnualAmount())));
var totalCost = commonCost.add(destinationCalculationJob.getAnnualTransportationCost()).add(destinationCalculationJob.getAnnualCustomCost()); 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)); 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) { private double getAirfreightShare(double maxAirfreightShare, double overseaShare) {
maxAirfreightShare = maxAirfreightShare*100;
overseaShare = overseaShare*100;
// Ensure oversea share is within valid range // Ensure oversea share is within valid range
if (overseaShare < 0 || overseaShare > 100) { if (overseaShare < 0 || overseaShare > 100) {
throw new IllegalArgumentException("Oversea share must be between 0 and 100"); throw new IllegalArgumentException("Oversea share must be between 0 and 100");
@ -87,14 +90,14 @@ public class AirfreightCalculationService {
// Linear interpolation: y = mx + b // Linear interpolation: y = mx + b
// m = (0.2 * maxAirfreightShare - 0) / (50 - 0) = 0.004 * maxAirfreightShare // m = (0.2 * maxAirfreightShare - 0) / (50 - 0) = 0.004 * maxAirfreightShare
// b = 0 // b = 0
return (0.004 * maxAirfreightShare * overseaShare); return (0.004 * maxAirfreightShare * overseaShare)/100;
} }
// Second segment: from (50, 20% of max) to (100, max) // Second segment: from (50, 20% of max) to (100, max)
else { else {
// Linear interpolation: y = mx + b // Linear interpolation: y = mx + b
// m = (maxAirfreightShare - 0.2 * maxAirfreightShare) / (100 - 50) = 0.016 * maxAirfreightShare // m = (maxAirfreightShare - 0.2 * maxAirfreightShare) / (100 - 50) = 0.016 * maxAirfreightShare
// b = 0.2 * maxAirfreightShare - 50 * 0.016 * maxAirfreightShare = -0.6 * 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; 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.CustomResult;
import de.avatic.lcc.calculationmodel.SectionInfo;
import de.avatic.lcc.model.premises.Premise; import de.avatic.lcc.model.premises.Premise;
import de.avatic.lcc.model.premises.route.Destination; import de.avatic.lcc.model.premises.route.Destination;
import de.avatic.lcc.model.properties.CountryPropertyMappingId; 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.premise.RouteNodeRepository;
import de.avatic.lcc.repositories.properties.PropertyRepository; import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.service.CustomApiService; import de.avatic.lcc.service.CustomApiService;
import de.avatic.lcc.calculationmodel.SectionInfo;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -34,6 +36,17 @@ public class CustomCostCalculationService {
this.shippingFrequencyCalculationService = shippingFrequencyCalculationService; 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) { public CustomResult doD2dCalculation(Premise premise, Destination destination, List<SectionInfo> sections) {
var destUnion = countryPropertyRepository.getByMappingIdAndCountryId(CountryPropertyMappingId.UNION, destination.getCountryId()).orElseThrow(); 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())) if (!CustomUnionType.EU.name().equals(destUnion.getCurrentValue()) || !CustomUnionType.NONE.name().equals(sourceUnion.getCurrentValue()))
return CustomResult.EMPTY; 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) { 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 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); 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; return CustomResult.EMPTY;
} }
private CustomResult getCustomCalculationResult(Premise premise, Destination destination, BigDecimal transportationCost, BigDecimal transportationChanceCost, BigDecimal transportationRiskCost) { private CustomResult getCustomCalculationResult(Premise premise, Destination destination, BigDecimal containerShare, double huAnnualAmount, BigDecimal transportationCost, BigDecimal transportationChanceCost, BigDecimal transportationRiskCost) {
var shippingFrequency = shippingFrequencyCalculationService.doCalculation(destination.getAnnualAmount()); var shippingFrequency = shippingFrequencyCalculationService.doCalculation(huAnnualAmount);
var customFee = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.CUSTOM_FEE).orElseThrow().getCurrentValue()); 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; var fcaFee = BigDecimal.ZERO;
if (premise.getFcaEnabled()) { if (premise.getFcaEnabled()) {
@ -81,16 +99,16 @@ public class CustomCostCalculationService {
var customValue = materialCost.add(fcaFee).add(transportationCost); var customValue = materialCost.add(fcaFee).add(transportationCost);
var customDuties = customValue.multiply(tariffRate); var customDuties = customValue.multiply(tariffRate);
var annualCustomFee = shippingFrequency * customFee; var annualCustomFee = BigDecimal.valueOf(shippingFrequency).multiply(BigDecimal.valueOf(customFee)).multiply(containerShare);
var annualCost = customDuties.add(BigDecimal.valueOf(annualCustomFee)); var annualCost = customDuties.add(annualCustomFee);
var customRiskValue = materialCost.add(fcaFee).add(transportationRiskCost); var customRiskValue = materialCost.add(fcaFee).add(transportationRiskCost);
var customRiskDuties = customRiskValue.multiply(tariffRate); 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 customChanceValue = materialCost.add(fcaFee).add(transportationChanceCost);
var customChanceDuties = customChanceValue.multiply(tariffRate); 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); return new CustomResult(customValue, customRiskValue, customChanceValue, customDuties, tariffRate, annualCost, annualRiskCost, annualChanceCost);
} }
@ -116,7 +134,7 @@ public class CustomCostCalculationService {
private CustomUnionType getCustomUnionByCountryId(Integer countryId) { private CustomUnionType getCustomUnionByCountryId(Integer countryId) {
var property = countryPropertyRepository.getByMappingIdAndCountryId(CountryPropertyMappingId.UNION, countryId).orElseThrow(); var property = countryPropertyRepository.getByMappingIdAndCountryId(CountryPropertyMappingId.UNION, countryId).orElseThrow();
if(property.getCurrentValue() == null) if (property.getCurrentValue() == null)
return CustomUnionType.NONE; return CustomUnionType.NONE;
return CustomUnionType.valueOf(property.getCurrentValue().toUpperCase()); 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 org.springframework.stereotype.Service;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
@Service @Service
public class HandlingCostCalculationService { public class HandlingCostCalculationService {
@ -66,7 +67,7 @@ public class HandlingCostCalculationService {
private HandlingResult getLLCCost(Destination destination, PackagingDimension hu, LoadCarrierType type, boolean addRepackingCosts) { 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 handling = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.GLT_HANDLING).orElseThrow().getCurrentValue());
double release = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.GLT_RELEASE).orElseThrow().getCurrentValue()); double release = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.GLT_RELEASE).orElseThrow().getCurrentValue());
double dispatch = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.GLT_DISPATCH).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) { public InventoryCostResult doCalculation(Premise premise, Destination destination, BigDecimal leadTime) {
var fcaFee = BigDecimal.ZERO; var fcaFee = BigDecimal.ZERO;
if (premise.getFcaEnabled()) { if (premise.getFcaEnabled()) {
@ -41,6 +42,7 @@ public class InventoryCostCalculationService {
} }
var hu = premiseToHuService.createHuFromPremise(premise); 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 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())); 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 dailyAmount = annualAmount.divide(BigDecimal.valueOf(365), 10, RoundingMode.HALF_UP);
var workdayAmount = annualAmount.divide(workdays, 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 safetyStock = safetydays.multiply(workdayAmount);
var stockedInventory = opStock.add(safetyStock); var stockedInventory = opStock.add(safetyStock);
var inTransportStock = dailyAmount.multiply(leadTime); var inTransportStock = dailyAmount.multiply(leadTime);

View file

@ -79,9 +79,11 @@ public class RouteSectionCostCalculationService {
PriceCalculationResult prices = calculatePrices( PriceCalculationResult prices = calculatePrices(
premise.getHuMixable(), premise.getHuMixable(),
rate, rate,
containerCalculation.isWeightExceeded(),
ContainerType.FEU, ContainerType.FEU,
containerCalculation.getMaxContainerWeight(), containerCalculation.getMaxContainerWeight(),
BigDecimal.valueOf(containerCalculation.getTotalUtilizationByVolume()), BigDecimal.valueOf(containerCalculation.getTotalUtilizationByVolume()),
BigDecimal.valueOf(containerCalculation.getHuUtilizationByWeight()),
utilization); utilization);
result.setCbmPrice(!containerCalculation.isWeightExceeded()); result.setCbmPrice(!containerCalculation.isWeightExceeded());
@ -143,24 +145,26 @@ public class RouteSectionCostCalculationService {
result.setTransitTime(transitTime); result.setTransitTime(transitTime);
// Calculate price and annual cost // 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 utilization = getUtilization(section.getRateType());
BigDecimal annualVolume = BigDecimal.valueOf(huAnnualAmount * containerCalculation.getHu().getVolume(DimensionUnit.M)); BigDecimal annualVolume = huAnnualAmount.multiply(BigDecimal.valueOf(containerCalculation.getHu().getVolume(DimensionUnit.M)));
BigDecimal annualWeight = BigDecimal.valueOf(huAnnualAmount * containerCalculation.getHu().getWeight(WeightUnit.KG)); BigDecimal annualWeight = huAnnualAmount.multiply(BigDecimal.valueOf(containerCalculation.getHu().getWeight(WeightUnit.KG)));
PriceCalculationResult prices = calculatePrices( PriceCalculationResult prices = calculatePrices(
premise.getHuMixable(), premise.getHuMixable(),
rate, rate,
containerCalculation.isWeightExceeded(),
containerCalculation.getContainerType(), containerCalculation.getContainerType(),
containerCalculation.getMaxContainerWeight(), containerCalculation.getMaxContainerWeight(),
BigDecimal.valueOf(containerCalculation.getTotalUtilizationByVolume()), BigDecimal.valueOf(containerCalculation.getTotalUtilizationByVolume()),
BigDecimal.valueOf(containerCalculation.getTotalUtilizationByWeight()),
utilization); utilization);
result.setCbmPrice(!containerCalculation.isWeightExceeded()); result.setCbmPrice(!containerCalculation.isWeightExceeded());
result.setWeightPrice(containerCalculation.isWeightExceeded()); result.setWeightPrice(containerCalculation.isWeightExceeded());
result.setCbmPrice(prices.volumePrice); result.setCbmPrice(prices.volumePrice);
result.setWeightPrice(prices.weightPrice); result.setWeightPrice(prices.weightPrice);
result.setUtilization(prices.utilization); result.setUtilization(!containerCalculation.isWeightExceeded() || !premise.getHuMixable() ? prices.utilization : BigDecimal.valueOf(1.0));
var chanceRiskFactors = changeRiskFactorCalculationService.getChanceRiskFactors(); var chanceRiskFactors = changeRiskFactorCalculationService.getChanceRiskFactors();
@ -180,11 +184,14 @@ public class RouteSectionCostCalculationService {
private PriceCalculationResult calculatePrices( private PriceCalculationResult calculatePrices(
boolean huMixable, boolean huMixable,
BigDecimal rate, BigDecimal rate,
boolean weightExceeded,
ContainerType containerType, ContainerType containerType,
int maxContainerWeight, int maxContainerWeight,
BigDecimal totalVolumeUtilization, BigDecimal totalVolumeUtilization,
BigDecimal totalWeightUtilization,
BigDecimal propertyUtilization) { BigDecimal propertyUtilization) {
BigDecimal utilization; BigDecimal utilization;
BigDecimal volumePrice; BigDecimal volumePrice;
BigDecimal weightPrice; BigDecimal weightPrice;
@ -194,12 +201,12 @@ public class RouteSectionCostCalculationService {
if (huMixable) { if (huMixable) {
volumePrice = cbmRate.divide(propertyUtilization, 10, RoundingMode.HALF_UP); volumePrice = cbmRate.divide(propertyUtilization, 10, RoundingMode.HALF_UP);
weightPrice = weightRate.divide(propertyUtilization, 10, RoundingMode.HALF_UP); weightPrice = weightRate.divide(BigDecimal.valueOf(1), 10, RoundingMode.HALF_UP);
utilization = propertyUtilization; utilization = weightExceeded ? BigDecimal.ONE : propertyUtilization;
} else { } else {
volumePrice = cbmRate.divide(totalVolumeUtilization, 10, RoundingMode.HALF_UP); volumePrice = cbmRate.divide(totalVolumeUtilization, 10, RoundingMode.HALF_UP);
weightPrice = weightRate.divide(totalVolumeUtilization, 10, RoundingMode.HALF_UP); weightPrice = weightRate.divide(totalWeightUtilization, 10, RoundingMode.HALF_UP);
utilization = totalVolumeUtilization; utilization = weightExceeded ? totalWeightUtilization : totalVolumeUtilization;
} }
return new PriceCalculationResult(volumePrice, weightPrice, utilization); 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; 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.generic.NodeType;
import de.avatic.lcc.dto.report.ReportDTO; import de.avatic.lcc.dto.report.ReportDTO;
import de.avatic.lcc.dto.report.ReportDestinationDTO; import de.avatic.lcc.dto.report.ReportDestinationDTO;
@ -50,13 +49,15 @@ public class ReportTransformer {
List<CalculationJobDestination> destinations = calculationJobDestinationRepository.getDestinationsByJobId(job.getId()); List<CalculationJobDestination> destinations = calculationJobDestinationRepository.getDestinationsByJobId(job.getId());
Map<Integer, List<CalculationJobRouteSection>> sections = calculationJobRouteSectionRepository.getRouteSectionsByDestinationIds(destinations.stream().map(CalculationJobDestination::getId).toList()); Map<Integer, List<CalculationJobRouteSection>> sections = calculationJobRouteSectionRepository.getRouteSectionsByDestinationIds(destinations.stream().map(CalculationJobDestination::getId).toList());
var weightedTotalCost = getWeightedTotalCosts(sections);
Premise premise = premiseRepository.getPremiseById(job.getPremiseId()).orElseThrow(); Premise premise = premiseRepository.getPremiseById(job.getPremiseId()).orElseThrow();
reportDTO.setCost(getCostMap(job, destinations)); reportDTO.setCost(getCostMap(job, destinations, weightedTotalCost));
reportDTO.setRisk(getRisk(job, destinations)); reportDTO.setRisk(getRisk(job, destinations, weightedTotalCost));
reportDTO.setDestination(destinations.stream().map(d -> getDestinationDTO(d, sections.get(d.getId()), premise)).toList()); 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(); var source = reportDTO.getDestinations().getFirst().getSections().stream().map(ReportSectionDTO::getFromNode).filter(n -> n.getTypes().contains(NodeType.SOURCE)).findFirst().orElseThrow();
reportDTO.setSupplier(source); 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) { private ReportDestinationDTO getDestinationDTO(CalculationJobDestination destination, List<CalculationJobRouteSection> sections, Premise premise) {
var destinationNode = nodeRepository.getByDestinationId(destination.getPremiseDestinationId()).orElseThrow(); var destinationNode = nodeRepository.getByDestinationId(destination.getPremiseDestinationId()).orElseThrow();
var dimensionUnit = premise.getHuDisplayedDimensionUnit(); var dimensionUnit = premise.getHuDisplayedDimensionUnit();
var weightUnit = premise.getHuDisplayedWeightUnit(); var weightUnit = premise.getHuDisplayedWeightUnit();
@ -76,23 +103,26 @@ public class ReportTransformer {
var totalAnnualCost = sections.stream().map(CalculationJobRouteSection::getAnnualCost).reduce(BigDecimal.ZERO, BigDecimal::add); var totalAnnualCost = sections.stream().map(CalculationJobRouteSection::getAnnualCost).reduce(BigDecimal.ZERO, BigDecimal::add);
destinationDTO.getSections().forEach(s -> { 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 -> { 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.setDestination(nodeTransformer.toNodeDTO(destinationNode));
destinationDTO.setDimensionUnit(dimensionUnit); destinationDTO.setDimensionUnit(dimensionUnit);
destinationDTO.setWeightUnit(weightUnit); destinationDTO.setWeightUnit(weightUnit);
destinationDTO.setHeight(dimensionUnit.convertFromMM(premise.getIndividualHuHeight()).doubleValue()); destinationDTO.setHeight(dimensionUnit.convertFromMM(premise.getIndividualHuHeight()));
destinationDTO.setWidth(dimensionUnit.convertFromMM(premise.getIndividualHuWidth()).doubleValue()); destinationDTO.setWidth(dimensionUnit.convertFromMM(premise.getIndividualHuWidth()));
destinationDTO.setLength(dimensionUnit.convertFromMM(premise.getIndividualHuLength()).doubleValue()); destinationDTO.setLength(dimensionUnit.convertFromMM(premise.getIndividualHuLength()));
destinationDTO.setWeight(weightUnit.convertFromG(premise.getIndividualHuWeight()).doubleValue()); destinationDTO.setWeight(weightUnit.convertFromG(premise.getIndividualHuWeight()));
destinationDTO.setHuUnitCount(premise.getHuUnitCount());
destinationDTO.setLayer(destination.getLayerCount()); destinationDTO.setLayer(destination.getLayerCount());
destinationDTO.setUnitCount(premise.getHuUnitCount());
destinationDTO.setOverseaShare(premise.getOverseaShare().doubleValue()); destinationDTO.setOverseaShare(premise.getOverseaShare().doubleValue());
destinationDTO.setSafetyStock(destination.getSafetyStock().doubleValue()); destinationDTO.setSafetyStock(destination.getSafetyStock().doubleValue());
@ -105,10 +135,11 @@ public class ReportTransformer {
destinationDTO.setRate(mainRun == null ? 0 : mainRun.getRate()); destinationDTO.setRate(mainRun == null ? 0 : mainRun.getRate());
destinationDTO.setType(destination.getContainerType()); destinationDTO.setType(destination.getContainerType());
destinationDTO.setUtilization(destination.getContainerUtilization()); destinationDTO.setUtilization(destination.getContainerUtilization());
destinationDTO.setUnitCount(premise.getHuUnitCount()); destinationDTO.setUnitCount(destination.getHuCount());
destinationDTO.setWeightExceeded(destination.getTransportWeightExceeded()); destinationDTO.setWeightExceeded(destination.getTransportWeightExceeded());
destinationDTO.setHsCode(premise.getHsCode()); destinationDTO.setHsCode(premise.getHsCode());
destinationDTO.setTariffRate(destination.getTariffRate());
return destinationDTO; 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<>(); 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(); ReportEntryDTO airfreight = new ReportEntryDTO();
var airfreightValue = destination.stream().map(CalculationJobDestination::getAnnualAirFreightCost).reduce(BigDecimal.ZERO, BigDecimal::add);
airfreight.setTotal(airfreightValue); 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); risk.put("air_freight_cost", airfreight);
ReportEntryDTO worst = new ReportEntryDTO(); ReportEntryDTO worst = new ReportEntryDTO();
var worstValue = destination.stream().map(CalculationJobDestination::getTotalRiskCost).reduce(BigDecimal.ZERO, BigDecimal::add);
worst.setTotal(worstValue); 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); risk.put("worst_case_cost", worst);
ReportEntryDTO best = new ReportEntryDTO(); ReportEntryDTO best = new ReportEntryDTO();
var bestValue = destination.stream().map(CalculationJobDestination::getTotalChanceCost).reduce(BigDecimal.ZERO, BigDecimal::add);
best.setTotal(bestValue); 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); risk.put("best_case_cost", best);
return risk; 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<>(); 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(); 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.setTotal(totalValue);
total.setPercentage(BigDecimal.valueOf(100)); total.setPercentage(BigDecimal.valueOf(1));
cost.put("total", total); 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(); ReportEntryDTO material = new ReportEntryDTO();
var materialValue = destination.stream().map(CalculationJobDestination::getMaterialCost).reduce(BigDecimal.ZERO, BigDecimal::add);
material.setTotal(materialValue); 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); 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(); ReportEntryDTO fcaFees = new ReportEntryDTO();
var fcaFeesValues = destination.stream().map(CalculationJobDestination::getFcaCost).reduce(BigDecimal.ZERO, BigDecimal::add);
fcaFees.setTotal(fcaFeesValues); 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); cost.put("fcaFees", fcaFees);
ReportEntryDTO repacking = new ReportEntryDTO(); ReportEntryDTO repacking = new ReportEntryDTO();
var repackingValues = destination.stream().map(CalculationJobDestination::getAnnualRepackingCost).reduce(BigDecimal.ZERO, BigDecimal::add);
repacking.setTotal(repackingValues); 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); cost.put("repacking", repacking);
ReportEntryDTO handling = new ReportEntryDTO(); ReportEntryDTO handling = new ReportEntryDTO();
var handlingValues = destination.stream().map(CalculationJobDestination::getAnnualHandlingCost).reduce(BigDecimal.ZERO, BigDecimal::add);
handling.setTotal(handlingValues); 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); cost.put("handling", handling);
ReportEntryDTO storage = new ReportEntryDTO(); ReportEntryDTO storage = new ReportEntryDTO();
var storageValues = destination.stream().map(CalculationJobDestination::getAnnualStorageCost).reduce(BigDecimal.ZERO, BigDecimal::add);
storage.setTotal(storageValues); 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); cost.put("storage", storage);
ReportEntryDTO capital = new ReportEntryDTO(); ReportEntryDTO capital = new ReportEntryDTO();
var capitalValues = destination.stream().map(CalculationJobDestination::getAnnualCapitalCost).reduce(BigDecimal.ZERO, BigDecimal::add);
capital.setTotal(capitalValues); 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); cost.put("capital", capital);
ReportEntryDTO disposal = new ReportEntryDTO(); ReportEntryDTO disposal = new ReportEntryDTO();
var disposalValues = destination.stream().map(CalculationJobDestination::getAnnualDisposalCost).reduce(BigDecimal.ZERO, BigDecimal::add);
disposal.setTotal(disposalValues); 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); cost.put("disposal", disposal);
return cost; 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 -- transportation
is_d2d BOOLEAN DEFAULT FALSE, is_d2d BOOLEAN DEFAULT FALSE,
rate_d2d DECIMAL(15, 2) DEFAULT NULL, rate_d2d DECIMAL(15, 2) DEFAULT NULL,
container_type CHAR(8) CHECK (container_type IN container_type CHAR(8),
('TEU', 'FEU', 'HC', 'TRUCK')), hu_count INT UNSIGNED NOT NULL COMMENT 'number of handling units in total (full container, with layers)',
hu_count INT UNSIGNED NOT NULL COMMENT 'number of handling units int total',
layer_structure JSON COMMENT 'json representation of a single layer', 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', 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', 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 (calculation_job_id) REFERENCES calculation_job (id),
FOREIGN KEY (premise_destination_id) REFERENCES premise_destination (id), FOREIGN KEY (premise_destination_id) REFERENCES premise_destination (id),
INDEX idx_calculation_job_id (calculation_job_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 CREATE TABLE IF NOT EXISTS calculation_job_route_section