Integrated NomenclatureService for handling incomplete HS codes, added resolveIncompleteHsCodesIntern for enhanced nomenclature resolution, updated MaterialEdit.vue for dynamic HS code and tariff rate inputs, and introduced nomenclature master data table with initialization script.
This commit is contained in:
parent
305033e8f0
commit
a4d0f4f913
7 changed files with 17065 additions and 142 deletions
|
|
@ -1,4 +1,6 @@
|
|||
<template>
|
||||
<div class="outer-container">
|
||||
|
||||
<div class="container" :class="{ 'responsive': responsive }" @focusout="focusLost">
|
||||
|
||||
|
||||
|
|
@ -18,62 +20,44 @@
|
|||
|
||||
<div class="field-group">
|
||||
<div class="caption-column">HS code</div>
|
||||
<div class="input-column">
|
||||
<div v-if="!tariffUnlocked" class="input-column">
|
||||
<span>{{ hsCode }}</span>
|
||||
</div>
|
||||
<div v-else class="input-column">
|
||||
<div class="hs-code-container">
|
||||
<autosuggest-searchbar ref="hsCodeSearchbar" :activate-watcher="true" :fetch-suggestions="fetchHsCode"
|
||||
:initial-value="hsCode"
|
||||
@selected="hsCodeSelected" @blur="hsCodeChanged" :allow-free-input="true"
|
||||
placeholder="Find hs code" no-results-text="Not found."></autosuggest-searchbar>
|
||||
|
||||
<div class="text-container input-field-tariffrate">
|
||||
<input ref="hsCodeInput" :value="hsCode" @blur="hsCodeChanged" class="input-field" autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="field-group" :class="{ 'disabled': !disableTariff }">
|
||||
<div class="field-group">
|
||||
<div class="caption-column">Tariff rate [%]</div>
|
||||
<div class="input-column">
|
||||
<div v-if="!tariffUnlocked" class="input-column">
|
||||
<span>{{ tariffRatePercent }}</span>
|
||||
</div>
|
||||
<div v-else class="input-column">
|
||||
<div class="text-container input-field-tariffrate">
|
||||
<input ref="tariffRateInput" :value="tariffRatePercent" @blur="validateTariffRateEvent"
|
||||
class="input-field"
|
||||
:disabled="!disableTariff"
|
||||
autocomplete="off"/>
|
||||
<ph-warning v-if="showTariffWarning" size="18" class="warning-icon"></ph-warning>
|
||||
<div v-if="showTariffWarning" class="tariff-rate-info">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="tariff-rate-info-header">
|
||||
<div v-if="tariffUnlocked" class="tariff-rate-info-header">
|
||||
<ph-warning size="24"></ph-warning>
|
||||
<div v-if="tariffIsDefault">
|
||||
<div>Using default value</div>
|
||||
<div class="tariff-rate-info-headerbox">Automatic determination failed</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div>More than one custom measure found</div>
|
||||
<div class="tariff-rate-info-headerbox">Please review carefully</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tariff-rate-info-container">
|
||||
<template v-for="measure in measures" :key="measure.code">
|
||||
<div class="tariff-rate-info-cell-measure">Measure {{ measure.code }}</div>
|
||||
<div>Regulation {{ measure.regulation }}</div>
|
||||
<div class="tariff-rate-info-cell-tariffrate">{{ measure.tariffRate }}</div>
|
||||
</template>
|
||||
<div>
|
||||
<div>Tariff rate lookup failed</div>
|
||||
<div class="tariff-rate-info-headerbox">Please contact a customs expert to obtain HS code and tariff rate.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group" v-if="showCheckbox">
|
||||
<div class="caption-column"></div>
|
||||
<div class="input-column">
|
||||
<checkbox :checked="false" @checkbox-changed="checkBoxChanged">tariff lookup on close</checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -103,7 +87,7 @@ export default {
|
|||
SelectMaterial,
|
||||
Modal, PhArrowCounterClockwise, ModalDialog, AutosuggestSearchbar, InputField, Flag, IconButton
|
||||
},
|
||||
emits: ["update:tariffRate", "updateMaterial", "update:partNumber", "update:hsCode", "save", "close", "startLookup"],
|
||||
emits: ["update:tariffRate", "updateMaterial", "update:partNumber", "update:hsCode", "save", "close"],
|
||||
props: {
|
||||
description: {
|
||||
type: [String, null],
|
||||
|
|
@ -117,6 +101,10 @@ export default {
|
|||
required: true,
|
||||
validator: (value) => value === null || typeof value === 'number'
|
||||
},
|
||||
tariffUnlocked: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
tariffRate: {
|
||||
required: true,
|
||||
validator: (value) => value === null || typeof value === 'number'
|
||||
|
|
@ -135,33 +123,10 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useMaterialStore, useCustomsStore),
|
||||
...mapStores(useMaterialStore),
|
||||
tariffRatePercent() {
|
||||
return ((this.tariffRate ?? null) !== null) ? (this.tariffRate * 100).toFixed(2) : '';
|
||||
},
|
||||
showTariffWarning() {
|
||||
return this.customsStore.showWarning(this.countryId);
|
||||
},
|
||||
tariffIsDefault() {
|
||||
return this.tariffInfo?.default ?? false;
|
||||
},
|
||||
tariffInfo() {
|
||||
return this.customsStore.findByCountryId(this.countryId);
|
||||
},
|
||||
measures() {
|
||||
return this.tariffInfo.measures;
|
||||
},
|
||||
showCheckbox() {
|
||||
return this.countryId === null;
|
||||
},
|
||||
disableTariff() {
|
||||
return !this.doLookup;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
doLookup: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
},
|
||||
|
|
@ -198,35 +163,10 @@ export default {
|
|||
const multiple = Math.pow(10, digits);
|
||||
return Math.round(number * multiple) / multiple;
|
||||
},
|
||||
async fetchHsCode(query) {
|
||||
return await this.customsStore.findHsCode(query);
|
||||
},
|
||||
hsCodeChanged() {
|
||||
hsCodeChanged(event) {
|
||||
const currentValue = this.$refs.hsCodeSearchbar.searchQuery;
|
||||
this.$emit("update:hsCode", currentValue);
|
||||
this.$emit("update:hsCode", event.target.value);
|
||||
},
|
||||
async hsCodeSelected(hsCode) {
|
||||
let save = false;
|
||||
|
||||
if (hsCode !== this.hsCode) {
|
||||
this.$emit("update:hsCode", hsCode)
|
||||
save = true;
|
||||
}
|
||||
|
||||
if ((this.countryId ?? null) !== null) {
|
||||
const query = {hsCode: hsCode, countryIds: [this.countryId]};
|
||||
await this.customsStore.findTariffRate(query);
|
||||
this.validateTariffRate((this.customsStore.findByCountryId(this.countryId)?.value ?? 0.03) * 100);
|
||||
save = true;
|
||||
}
|
||||
|
||||
if (save)
|
||||
this.$emit('save', 'material');
|
||||
},
|
||||
checkBoxChanged(checked) {
|
||||
this.doLookup = checked
|
||||
this.$emit('startLookup', checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -234,27 +174,14 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
|
||||
.outer-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3.2rem;
|
||||
flex: 1 1 auto;
|
||||
|
||||
.field-group.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.field-group.disabled .caption-column,
|
||||
.field-group.disabled .input-field {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.field-group.disabled .text-container {
|
||||
background-color: #f3f4f6;
|
||||
cursor: not-allowed;
|
||||
border-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.input-field:disabled {
|
||||
cursor: not-allowed;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
color: #002F54;
|
||||
|
|
@ -322,6 +249,15 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
/* Optimierung für kleinere Bildschirme unter 1500px - nur wenn responsive aktiviert ist */
|
||||
@media (max-width: 1500px) {
|
||||
.container.responsive .input-field-tariffrate {
|
||||
min-width: auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.field-group {
|
||||
display: contents;
|
||||
}
|
||||
|
|
@ -449,12 +385,5 @@ export default {
|
|||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
/* Optimierung für kleinere Bildschirme unter 1500px - nur wenn responsive aktiviert ist */
|
||||
@media (max-width: 1500px) {
|
||||
.container.responsive .input-field-tariffrate {
|
||||
min-width: auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -57,6 +57,7 @@
|
|||
<material-edit :part-number="premise.material.part_number"
|
||||
:description="premise.material.name"
|
||||
:id="premise.material.id"
|
||||
:tariff-unlocked="premise.tariff_unlocked"
|
||||
v-model:hs-code="premise.hs_code"
|
||||
v-model:tariff-rate="premise.tariff_rate"
|
||||
v-model:country-id="premise.supplier.country.id"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
package de.avatic.lcc.repositories;
|
||||
|
||||
import de.avatic.lcc.service.api.EUTaxationApiService;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public class NomenclatureRepository {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final EUTaxationApiService eUTaxationApiService;
|
||||
|
||||
public NomenclatureRepository(JdbcTemplate jdbcTemplate, EUTaxationApiService eUTaxationApiService) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.eUTaxationApiService = eUTaxationApiService;
|
||||
}
|
||||
|
||||
public List<String> searchHsCode(String search) {
|
||||
String sql = """
|
||||
SELECT hs_code FROM nomenclature WHERE hs_code LIKE CONCAT(?, '%') LIMIT 10
|
||||
""";
|
||||
|
||||
return jdbcTemplate.queryForList (sql, String.class, search);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import de.avatic.lcc.model.db.properties.SystemPropertyMappingId;
|
|||
import de.avatic.lcc.model.eutaxation.MeasureType;
|
||||
import de.avatic.lcc.repositories.country.CountryRepository;
|
||||
import de.avatic.lcc.repositories.properties.PropertyRepository;
|
||||
import de.avatic.lcc.service.calculation.NomenclatureService;
|
||||
import de.avatic.lcc.util.exception.base.InternalErrorException;
|
||||
import eu.europa.ec.taxation.taric.client.GoodsMeasForWsResponse;
|
||||
import eu.europa.ec.taxation.taric.client.GoodsMeasuresForWsResponse;
|
||||
|
|
@ -27,12 +28,14 @@ public class TaxationResolverService {
|
|||
private final EUTaxationApiService eUTaxationApiService;
|
||||
private final PropertyRepository propertyRepository;
|
||||
private final ZolltarifnummernApiService zolltarifnummernApiService;
|
||||
private final NomenclatureService nomenclatureService;
|
||||
|
||||
public TaxationResolverService(CountryRepository countryRepository, EUTaxationApiService eUTaxationApiService, PropertyRepository propertyRepository, ZolltarifnummernApiService zolltarifnummernApiService) {
|
||||
public TaxationResolverService(CountryRepository countryRepository, EUTaxationApiService eUTaxationApiService, PropertyRepository propertyRepository, ZolltarifnummernApiService zolltarifnummernApiService, NomenclatureService nomenclatureService) {
|
||||
this.countryRepository = countryRepository;
|
||||
this.eUTaxationApiService = eUTaxationApiService;
|
||||
this.propertyRepository = propertyRepository;
|
||||
this.zolltarifnummernApiService = zolltarifnummernApiService;
|
||||
this.nomenclatureService = nomenclatureService;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -44,7 +47,7 @@ public class TaxationResolverService {
|
|||
filteredRequests.get(false).stream()
|
||||
.filter(r -> r.material().getHsCode() != null)
|
||||
.map(r -> new TaxationResolverSingleRequest(r.material().getHsCode(), r.countryId(), r)),
|
||||
resolveIncompleteHsCodes(filteredRequests.get(true)).stream());
|
||||
resolveIncompleteHsCodesIntern(filteredRequests.get(true)).stream());
|
||||
|
||||
|
||||
var singleResponses = doSingleRequests(joined.toList());
|
||||
|
|
@ -55,6 +58,10 @@ public class TaxationResolverService {
|
|||
));
|
||||
}
|
||||
|
||||
private List<TaxationResolverSingleRequest> resolveIncompleteHsCodesIntern(List<TaxationResolverRequest> request) {
|
||||
return request.stream().flatMap(r -> nomenclatureService.getNomenclature(r.material().getHsCode()).stream().map(hsCode -> new TaxationResolverSingleRequest(hsCode, r.countryId(), r))).toList();
|
||||
}
|
||||
|
||||
private List<TaxationResolverSingleRequest> resolveIncompleteHsCodes(List<TaxationResolverRequest> request) {
|
||||
|
||||
var futures = request.stream()
|
||||
|
|
@ -72,7 +79,14 @@ public class TaxationResolverService {
|
|||
private Map<TaxationResolverSingleRequest, GoodsMeasForWsResponse> doSingleRequests(List<TaxationResolverSingleRequest> requests) {
|
||||
|
||||
|
||||
Map<Integer, Country> countries = requests.stream().collect(Collectors.toMap(TaxationResolverSingleRequest::countryId, r -> countryRepository.getById(r.countryId()).orElseThrow()));
|
||||
Map<Integer, Country> countries = requests.stream()
|
||||
.map(TaxationResolverSingleRequest::countryId)
|
||||
.distinct()
|
||||
.collect(Collectors.toMap(
|
||||
countryId -> countryId,
|
||||
countryId -> countryRepository.getById(countryId).orElseThrow()
|
||||
));
|
||||
|
||||
Map<TaxationResolverSingleRequest, CompletableFuture<GoodsMeasForWsResponse>> futureMap =
|
||||
requests.stream()
|
||||
.collect(Collectors.toMap(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package de.avatic.lcc.service.calculation;
|
||||
|
||||
import de.avatic.lcc.repositories.NomenclatureRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class NomenclatureService {
|
||||
|
||||
private final NomenclatureRepository nomenclatureRepository;
|
||||
|
||||
public NomenclatureService(NomenclatureRepository nomenclatureRepository) {
|
||||
this.nomenclatureRepository = nomenclatureRepository;
|
||||
}
|
||||
|
||||
|
||||
public List<String> getNomenclature(String hsCode) {
|
||||
return nomenclatureRepository.searchHsCode(hsCode);
|
||||
}
|
||||
}
|
||||
|
|
@ -133,7 +133,8 @@ public class PremiseCreationService {
|
|||
.ifPresent(value -> premiseRepository.updateMaterial(Collections.singletonList(
|
||||
p.getId()),
|
||||
value.actualHsCode(),
|
||||
value.tariffRate() == null ? null : BigDecimal.valueOf(value.tariffRate())));
|
||||
value.tariffRate() == null ? null : BigDecimal.valueOf(value.tariffRate()),
|
||||
value.tariffRate() == null));
|
||||
}
|
||||
|
||||
private void findExistingPremise(TemporaryPremise premise, boolean createEmpty, Integer userId) {
|
||||
|
|
|
|||
16929
src/main/resources/db/migration/V11__Nomenclature.sql
Normal file
16929
src/main/resources/db/migration/V11__Nomenclature.sql
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue