001/* Copyright (C) 2014 konik.io 002 * 003 * This file is part of the Konik library. 004 * 005 * The Konik library is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * The Konik library is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with the Konik library. If not, see <http://www.gnu.org/licenses/>. 017 */ 018package io.konik.validation; 019 020import com.google.common.base.Function; 021import com.neovisionaries.i18n.CurrencyCode; 022import io.konik.util.Amounts; 023import io.konik.util.Items; 024import io.konik.util.MonetarySummations; 025import io.konik.zugferd.Invoice; 026import io.konik.zugferd.entity.*; 027import io.konik.zugferd.entity.trade.MonetarySummation; 028import io.konik.zugferd.entity.trade.Settlement; 029import io.konik.zugferd.entity.trade.TradeTax; 030import io.konik.zugferd.entity.trade.item.*; 031import io.konik.zugferd.unece.codes.TaxCategory; 032import io.konik.zugferd.unece.codes.TaxCode; 033import io.konik.zugferd.unqualified.Amount; 034import org.slf4j.Logger; 035import org.slf4j.LoggerFactory; 036 037import javax.annotation.Nullable; 038import java.math.BigDecimal; 039import java.math.RoundingMode; 040import java.util.Arrays; 041import java.util.LinkedList; 042import java.util.List; 043import java.util.Map; 044import java.util.concurrent.ConcurrentHashMap; 045import java.util.concurrent.ConcurrentMap; 046 047/** 048 * Calculate the missing amounts of the invoice. 049 */ 050public final class AmountCalculator { 051 052 protected static Logger log = LoggerFactory.getLogger(AmountCalculator.class); 053 054 /** 055 * Calculates {@link MonetarySummation} for given {@link Invoice} basing on line {@link Item}s 056 * and global {@link io.konik.zugferd.entity.AllowanceCharge} and {@link LogisticsServiceCharge} 057 * 058 * @param invoice 059 * @return 060 */ 061 public static RecalculationResult recalculate(final Invoice invoice) { 062 assertNotNull(invoice); 063 064 CurrencyCode currency = getCurrency(invoice); 065 List<Item> items = Items.purchasableItemsOnly(invoice.getTrade().getItems()); 066 Settlement settlement = invoice.getTrade().getSettlement(); 067 068 TaxAggregator taxAggregator = new TaxAggregator(); 069 070 // If there are no items that can be used to recalculate monetary summation, return the current one 071 if (items.isEmpty()) { 072 return new RecalculationResult(MonetarySummations.newMonetarySummation(settlement.getMonetarySummation()), taxAggregator); 073 } 074 075 MonetarySummation monetarySummation = MonetarySummations.newMonetarySummation(currency); 076 monetarySummation.setAllowanceTotal(new InvoiceAllowanceTotalCalculator().apply(settlement)); 077 monetarySummation.setChargeTotal(new InvoiceChargeTotalCalculator().apply(settlement)); 078 079 log.debug("Starting recalculating line total from {} items...", items.size()); 080 int itemsCounter = 0; 081 082 for (Item item : items) { 083 log.debug("==> {}:", ++itemsCounter); 084 log.debug("Recalculating item: [{}]", item.getProduct().getName()); 085 086 Amount lineTotal = new ItemLineTotalCalculator().apply(item); 087 ItemTax itemTax = new ItemTaxExtractor().apply(item); 088 089 log.debug("Recalculated item line total = {}", lineTotal); 090 log.debug("Recalculated item tax = {}%", itemTax.getPercentage()); 091 092 taxAggregator.add(itemTax, lineTotal != null ? lineTotal.getValue() : BigDecimal.ZERO); 093 094 monetarySummation.setLineTotal(Amounts.add( 095 monetarySummation.getLineTotal(), 096 lineTotal 097 )); 098 099 log.debug("Current monetarySummation.lineTotal = {} (the sum of all line totals)", monetarySummation.getLineTotal()); 100 } 101 102 log.debug("==> DONE!"); 103 log.debug("Finished recalculating monetarySummation.lineTotal..."); 104 105 appendTaxFromInvoiceAllowanceCharge(settlement, taxAggregator); 106 107 appendTaxFromInvoiceServiceCharge(settlement, taxAggregator); 108 109 monetarySummation.setTaxBasisTotal(new Amount(taxAggregator.calculateTaxBasis(), currency)); 110 monetarySummation.setTaxTotal(new Amount(taxAggregator.calculateTaxTotal(), currency)); 111 112 monetarySummation.setGrandTotal(Amounts.add( 113 monetarySummation.getTaxBasisTotal(), 114 monetarySummation.getTaxTotal() 115 )); 116 117 log.debug("Recalculated grand total = {} (tax basis total + tax total)", monetarySummation.getGrandTotal()); 118 119 if (settlement.getMonetarySummation() != null && settlement.getMonetarySummation().getTotalPrepaid() != null) { 120 monetarySummation.setTotalPrepaid( 121 settlement.getMonetarySummation().getTotalPrepaid() 122 ); 123 } 124 125 monetarySummation.setDuePayable( 126 Amounts.add(monetarySummation.getGrandTotal(), Amounts.negate(monetarySummation.getTotalPrepaid())) 127 ); 128 129 MonetarySummation result = MonetarySummations.precise(monetarySummation, 2, RoundingMode.HALF_UP); 130 131 log.debug("Recalculating invoice monetary summation DONE!"); 132 log.debug(" ==> result: {}", result); 133 log.debug(""); 134 135 return new RecalculationResult(result, taxAggregator); 136 } 137 138 /** 139 * Calculates {@link SpecifiedMonetarySummation} for given {@link Item} 140 * 141 * @param item 142 * @return 143 */ 144 public static SpecifiedMonetarySummation calculateSpecifiedMonetarySummation(final Item item) { 145 log.debug("Recalculating specified monetary summation for [{}]", item.getProduct().getName()); 146 147 CurrencyCode currencyCode = getCurrency(item); 148 149 SpecifiedMonetarySummation monetarySummation = MonetarySummations.newSpecifiedMonetarySummation(currencyCode); 150 monetarySummation.setLineTotal(Amounts.setPrecision(new ItemLineTotalCalculator().apply(item), 2, RoundingMode.HALF_UP)); 151 monetarySummation.setTotalAllowanceCharge(Amounts.setPrecision(new ItemTotalAllowanceChargeCalculator(currencyCode).apply(item), 2, RoundingMode.HALF_UP)); 152 153 log.debug("==> lineTotal = {}", monetarySummation.getLineTotal()); 154 log.debug("==> totalAllowanceCharge = {}", monetarySummation.getTotalAllowanceCharge()); 155 156 return monetarySummation; 157 } 158 159 private static void appendTaxFromInvoiceServiceCharge(Settlement settlement, TaxAggregator taxAggregator) { 160 log.debug("Adding tax amounts from invoice service charge..."); 161 if (settlement.getServiceCharge() != null) { 162 for (LogisticsServiceCharge charge : settlement.getServiceCharge()) { 163 if (charge.getTradeTax() != null && charge.getAmount() != null) { 164 for (AppliedTax tax : charge.getTradeTax()) { 165 log.debug("==> added {} to {}%", charge.getAmount(), tax.getPercentage()); 166 167 taxAggregator.add(tax, charge.getAmount().getValue()); 168 } 169 } 170 } 171 } 172 } 173 174 private static void appendTaxFromInvoiceAllowanceCharge(Settlement settlement, TaxAggregator taxAggregator) { 175 log.debug("Adding tax amounts from invoice allowance charge..."); 176 if (settlement.getAllowanceCharge() != null) { 177 for (SpecifiedAllowanceCharge charge : settlement.getAllowanceCharge()) { 178 if (charge.getCategory() != null && charge.getActual() != null) { 179 BigDecimal amount = charge.getActual().getValue(); 180 if (charge.isDiscount()) { 181 amount = amount.negate(); 182 } 183 184 log.debug("==> added {} to {}%", amount, charge.getCategory().getPercentage()); 185 186 taxAggregator.add(charge.getCategory(), amount); 187 } 188 } 189 } 190 } 191 192 public static CurrencyCode getCurrency(final Invoice invoice) { 193 assertNotNull(invoice); 194 return invoice.getTrade().getSettlement().getCurrency(); 195 } 196 197 /** 198 * Extracts {@link CurrencyCode} from {@link Item} object. 199 * @param item 200 * @return 201 */ 202 public static CurrencyCode getCurrency(final Item item) { 203 assertNotNull(item); 204 205 SpecifiedAgreement agreement = item.getAgreement(); 206 if (agreement != null && agreement.getGrossPrice() != null && agreement.getGrossPrice().getChargeAmount() != null) { 207 return agreement.getGrossPrice().getChargeAmount().getCurrency(); 208 } 209 210 if (agreement != null && agreement.getNetPrice() != null && agreement.getNetPrice().getChargeAmount() != null) { 211 return agreement.getNetPrice().getChargeAmount().getCurrency(); 212 } 213 214 SpecifiedSettlement settlement = item.getSettlement(); 215 if (settlement != null && settlement.getMonetarySummation() != null && settlement.getMonetarySummation().getLineTotal() != null) { 216 return settlement.getMonetarySummation().getLineTotal().getCurrency(); 217 } 218 219 return null; 220 } 221 222 223 private static void assertNotNull(final Invoice invoice) { 224 if (invoice == null || invoice.getTrade() == null) { 225 throw new IllegalArgumentException("Invoice and Trade objects cannot be null"); 226 } 227 } 228 229 private static void assertNotNull(final Item item) { 230 if (item == null) { 231 throw new IllegalArgumentException("Item cannot be null"); 232 } 233 } 234 235 236 /** 237 * Helper class for calculating {@link Item}'s line total. 238 */ 239 static final class ItemLineTotalCalculator implements Function<Item, Amount> { 240 241 @Nullable 242 @Override 243 public Amount apply(@Nullable Item item) { 244 Amount originLineTotal = null; 245 246 if (item != null && item.getSettlement() != null && item.getSettlement().getMonetarySummation() != null) { 247 originLineTotal = Amounts.copy(item.getSettlement().getMonetarySummation().getLineTotal()); 248 } 249 250 if (item == null || item.getDelivery() == null || item.getAgreement() == null) { 251 return originLineTotal; 252 } 253 254 if (item.getAgreement().getNetPrice() == null) { 255 return originLineTotal; 256 } 257 258 BigDecimal quantity = item.getDelivery().getBilled() != null ? item.getDelivery().getBilled().getValue() : BigDecimal.ZERO; 259 Amount amount = item.getAgreement().getNetPrice().getChargeAmount(); 260 261 log.debug("Line total formula: {} (net price) x {} (quantity)", amount, quantity); 262 263 return Amounts.multiply(amount, quantity); 264 } 265 } 266 267 /** 268 * Helper class for calculating {@link Item}'s tax total. 269 */ 270 static final class ItemTaxTotalCalculator implements Function<Item, Amount> { 271 272 private final Amount lineTotal; 273 274 public ItemTaxTotalCalculator(final Amount lineTotal) { 275 this.lineTotal = lineTotal; 276 } 277 278 @Override 279 public Amount apply(@Nullable Item item) { 280 CurrencyCode currency = lineTotal.getCurrency(); 281 Amount taxTotal = Amounts.zero(currency); 282 283 if (item != null && item.getSettlement() != null && item.getSettlement().getTradeTax() != null) { 284 for (ItemTax tax : item.getSettlement().getTradeTax()) { 285 BigDecimal taxRate = tax.getPercentage().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP).setScale(2, RoundingMode.HALF_UP); 286 BigDecimal taxValue = lineTotal.getValue().multiply(taxRate).setScale(2, RoundingMode.HALF_UP); 287 288 taxTotal = Amounts.add(taxTotal, new Amount(taxValue, currency)); 289 } 290 } 291 return taxTotal; 292 } 293 } 294 295 /** 296 * Calculates total {@link io.konik.zugferd.entity.AllowanceCharge} for given {@link Item}. 297 */ 298 public static final class ItemTotalAllowanceChargeCalculator implements Function<Item, Amount> { 299 300 private final CurrencyCode currencyCode; 301 302 public ItemTotalAllowanceChargeCalculator(CurrencyCode currencyCode) { 303 this.currencyCode = currencyCode; 304 } 305 306 @Override 307 public Amount apply(@Nullable Item item) { 308 Amount totalAllowanceCharge = Amounts.zero(currencyCode); 309 BigDecimal quantity = BigDecimal.ONE; 310 311 if (item != null && item.getDelivery() != null && item.getDelivery().getBilled() != null) { 312 quantity = item.getDelivery().getBilled().getValue(); 313 } 314 315 if (item != null && item.getAgreement() != null && item.getAgreement().getGrossPrice() != null) { 316 GrossPrice grossPrice = item.getAgreement().getGrossPrice(); 317 318 if (grossPrice.getAllowanceCharges() != null && !grossPrice.getAllowanceCharges().isEmpty()) { 319 for (AllowanceCharge charge : grossPrice.getAllowanceCharges()) { 320 BigDecimal chargeValue = charge.getActual().getValue(); 321 if (charge.isDiscount()) { 322 chargeValue = chargeValue.negate(); 323 } 324 Amount amount = new Amount(chargeValue.multiply(quantity), currencyCode); 325 totalAllowanceCharge = Amounts.add(totalAllowanceCharge, amount); 326 } 327 328 totalAllowanceCharge = Amounts.setPrecision(totalAllowanceCharge, 2, RoundingMode.HALF_UP); 329 } 330 } 331 332 return totalAllowanceCharge; 333 } 334 } 335 336 public static final class InvoiceAllowanceTotalCalculator implements Function<Settlement, Amount> { 337 @Nullable 338 @Override 339 public Amount apply(@Nullable Settlement settlement) { 340 if (settlement == null || settlement.getAllowanceCharge() == null) { 341 return null; 342 } 343 344 BigDecimal chargeValue = BigDecimal.ZERO; 345 346 for (SpecifiedAllowanceCharge charge : settlement.getAllowanceCharge()) { 347 if (charge.isDiscount()) { 348 chargeValue = chargeValue.add(charge.getActual().getValue()); 349 } 350 } 351 Amount amount = new Amount(chargeValue, settlement.getCurrency()); 352 353 log.debug("Invoice allowance total = {}", amount); 354 355 return amount; 356 } 357 } 358 359 public static final class InvoiceChargeTotalCalculator implements Function<Settlement, Amount> { 360 @Nullable 361 @Override 362 public Amount apply(@Nullable Settlement settlement) { 363 if (settlement == null || settlement.getAllowanceCharge() == null) { 364 return null; 365 } 366 367 BigDecimal chargeValue = BigDecimal.ZERO; 368 369 for (SpecifiedAllowanceCharge charge : settlement.getAllowanceCharge()) { 370 if (charge.isSurcharge()) { 371 chargeValue = chargeValue.add(charge.getActual().getValue()); 372 } 373 } 374 375 for (LogisticsServiceCharge charge : settlement.getServiceCharge()) { 376 chargeValue = chargeValue.add(charge.getAmount().getValue()); 377 } 378 379 Amount amount = new Amount(chargeValue, settlement.getCurrency()); 380 381 log.debug("Invoice charge total = {}", amount); 382 383 return amount; 384 } 385 } 386 387 public static final class ItemTaxExtractor implements Function<Item, ItemTax> { 388 @Nullable 389 @Override 390 public ItemTax apply(@Nullable Item item) { 391 if (item == null || item.getSettlement() == null) { 392 return null; 393 } 394 395 if (item.getSettlement().getTradeTax().isEmpty()) { 396 return null; 397 } 398 399 return item.getSettlement().getTradeTax().get(0); 400 } 401 } 402 403 /** 404 * Helper class for aggregating tax information and calculating 405 * tax basis and tax total values. 406 */ 407 public static final class TaxAggregator { 408 409 private static final int PRECISION = 2; 410 private static final RoundingMode ROUNDING_MODE = RoundingMode.HALF_UP; 411 412 private final ConcurrentMap<Key, BigDecimal> map = new ConcurrentHashMap<Key, BigDecimal>(); 413 414 public void add(Tax tax, BigDecimal amount) { 415 Key key = Key.create(tax); 416 map.putIfAbsent(key, BigDecimal.ZERO); 417 map.put(key, map.get(key).add(amount)); 418 } 419 420 public BigDecimal getTaxBasisForTaxPercentage(final BigDecimal percentage) { 421 BigDecimal value = BigDecimal.ZERO; 422 for (Key key : map.keySet()) { 423 if (percentage.equals(key.getPercentage())) { 424 value = value.add(map.get(key)); 425 } 426 } 427 return value; 428 } 429 430 public BigDecimal calculateTaxBasis() { 431 log.debug("Recalculating tax basis for tax percentages: {}", Arrays.toString(map.keySet().toArray())); 432 BigDecimal taxBasis = BigDecimal.ZERO; 433 for (BigDecimal amount : map.values()) { 434 taxBasis = taxBasis.add(amount); 435 } 436 437 log.debug("Recalculated tax basis = {}", taxBasis); 438 439 return taxBasis; 440 } 441 442 public BigDecimal calculateTaxTotal() { 443 log.debug("Calculating tax total..."); 444 BigDecimal taxTotal = BigDecimal.ZERO; 445 for (Map.Entry<Key, BigDecimal> entry : map.entrySet()) { 446 BigDecimal percentage = entry.getKey().getPercentage(); 447 BigDecimal value = entry.getValue(); 448 BigDecimal taxAmount = calculateTaxAmount(percentage, value); 449 450 log.debug("===> {} x {}% = {}", value, percentage, taxAmount); 451 452 taxTotal = taxTotal.add(taxAmount); 453 } 454 455 log.debug("Recalculated tax total = {}", taxTotal); 456 457 return taxTotal; 458 } 459 460 public List<TradeTax> generateTradeTaxList(final CurrencyCode currencyCode, final List<TradeTax> previousList) { 461 List<TradeTax> taxes = new LinkedList<TradeTax>(); 462 463 for (Key key : map.keySet()) { 464 TradeTax tradeTax = new TradeTax(); 465 tradeTax.setType(key.getCode()); 466 tradeTax.setCategory(key.getCategory()); 467 tradeTax.setPercentage(key.getPercentage()); 468 469 BigDecimal basis = map.get(key); 470 BigDecimal calculated = calculateTaxAmount(key.getPercentage(), basis); 471 472 tradeTax.setBasis(new Amount(basis, currencyCode)); 473 tradeTax.setCalculated(new Amount(calculated, currencyCode)); 474 475 TradeTax existing = null; 476 if (previousList != null) { 477 for (TradeTax current : previousList) { 478 if (tradeTax.getType().equals(current.getType()) && 479 tradeTax.getCategory().equals(current.getCategory()) && 480 tradeTax.getPercentage().equals(current.getPercentage())) { 481 existing = current; 482 break; 483 } 484 } 485 } 486 487 if (existing != null) { 488 tradeTax.setExemptionReason(existing.getExemptionReason()); 489 490 if (existing.getAllowanceCharge() != null) { 491 tradeTax.setAllowanceCharge(new Amount(existing.getAllowanceCharge().getValue(), existing.getAllowanceCharge().getCurrency())); 492 } 493 494 if (existing.getLineTotal() != null) { 495 tradeTax.setLineTotal(new Amount(existing.getLineTotal().getValue(), existing.getLineTotal().getCurrency())); 496 } 497 } 498 499 taxes.add(tradeTax); 500 } 501 502 return taxes; 503 } 504 505 public static BigDecimal calculateTaxAmount(final BigDecimal percentage, final BigDecimal value) { 506 return value.multiply(percentage.divide(BigDecimal.valueOf(100))).setScale(PRECISION, ROUNDING_MODE); 507 } 508 509 @Override 510 public String toString() { 511 return "TaxAggregator{" + 512 "map=" + map + 513 '}'; 514 } 515 516 /** 517 * Helper key for {@link TaxAggregator} 518 */ 519 static final class Key { 520 private final BigDecimal percentage; 521 private final TaxCode code; 522 private final TaxCategory category; 523 524 private Key(final Tax tax) { 525 this.percentage = tax.getPercentage(); 526 this.category = tax.getCategory(); 527 this.code = tax.getType(); 528 } 529 530 public static Key create(final Tax tax) { 531 return new Key(tax); 532 } 533 534 public BigDecimal getPercentage() { 535 return percentage; 536 } 537 538 public TaxCode getCode() { 539 return code; 540 } 541 542 public TaxCategory getCategory() { 543 return category; 544 } 545 546 @Override 547 public boolean equals(Object o) { 548 if (this == o) return true; 549 if (!(o instanceof Key)) return false; 550 551 Key key = (Key) o; 552 553 if (!percentage.equals(key.percentage)) return false; 554 if (code != key.code) return false; 555 return category == key.category; 556 557 } 558 559 @Override 560 public int hashCode() { 561 int result = percentage.hashCode(); 562 result = 31 * result + (code != null ? code.hashCode() : 0); 563 result = 31 * result + (category != null ? category.hashCode() : 0); 564 return result; 565 } 566 567 @Override 568 public String toString() { 569 return "Key{" + 570 "percentage=" + percentage + 571 ", code=" + code + 572 ", category=" + category + 573 '}'; 574 } 575 } 576 } 577 578 public static final class RecalculationResult { 579 private final MonetarySummation monetarySummation; 580 private final TaxAggregator taxAggregator; 581 582 public RecalculationResult(MonetarySummation monetarySummation, TaxAggregator taxAggregator) { 583 this.monetarySummation = monetarySummation; 584 this.taxAggregator = taxAggregator; 585 } 586 587 public MonetarySummation getMonetarySummation() { 588 return monetarySummation; 589 } 590 591 public TaxAggregator getTaxAggregator() { 592 return taxAggregator; 593 } 594 } 595}