package io.konik.csv.converter;
import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.neovisionaries.i18n.CurrencyCode;
import io.konik.csv.model.Row;
import io.konik.zugferd.Invoice;
import io.konik.zugferd.entity.*;
import io.konik.zugferd.entity.trade.*;
import io.konik.zugferd.entity.trade.item.*;
import io.konik.zugferd.profile.ConformanceLevel;
import io.konik.zugferd.unece.codes.DocumentCode;
import io.konik.zugferd.unece.codes.TaxCategory;
import io.konik.zugferd.unece.codes.TaxCode;
import io.konik.zugferd.unece.codes.UnitOfMeasurement;
import io.konik.zugferd.unqualified.Amount;
import io.konik.zugferd.unqualified.Quantity;
import io.konik.zugferd.unqualified.ZfDateDay;
import javax.annotation.Nullable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Converter from {@link Row} to {@link Invoice}
*/
public class RowToInvoiceConverter {
private static final ConcurrentMap<String, DocumentCode> codes = new ConcurrentHashMap<String, DocumentCode>();
static {
codes.put("rechnung", DocumentCode._380);
codes.put("gutschriftsanzeige", DocumentCode._380);
codes.put("angebot", DocumentCode._310);
codes.put("bestellung", DocumentCode._220);
codes.put("proformarechnung", DocumentCode._325);
codes.put("teilrechnung", DocumentCode._326);
codes.put("korrigierte rechnung", DocumentCode._384);
codes.put("konsolidierte rechnung", DocumentCode._385);
codes.put("vorauszahlungsrechnung", DocumentCode._386);
codes.put("invoice", DocumentCode._380);
codes.put("credit note", DocumentCode._381);
codes.put("offer", DocumentCode._310);
codes.put("order", DocumentCode._220);
codes.put("proforma invoice", DocumentCode._325);
codes.put("partial invoice", DocumentCode._326);
codes.put("corrected invoice", DocumentCode._384);
codes.put("consolidated invoice", DocumentCode._385);
codes.put("prepayment invoice", DocumentCode._386);
}
public static Invoice convert(Row row) {
Objects.requireNonNull(row);
return new Process().run(row);
}
protected static DocumentCode getCode(String code) {
if (code != null) {
String key = code.trim().toLowerCase();
if (codes.containsKey(key)) {
return codes.get(key);
}
}
return DocumentCode._380;
}
/**
* Internal class to manage conversion state in a thread safe manner.
*/
private static class Process {
private CurrencyCode currencyCode;
private String customerNumber;
private ConcurrentMap<BigDecimal, TaxAccumulator> calculatedTax = new ConcurrentHashMap<BigDecimal, TaxAccumulator>();
protected Invoice run(Row row) {
Header header = mapHeader(row.getHeader());
TradeParty buyer = mapTradeParty(row.getRecipient());
TradeParty seller = mapTradeParty(row.getIssuer());
Agreement agreement = new Agreement()
.setBuyer(buyer)
.setSeller(seller);
Delivery delivery = new Delivery(header.getIssued());
Settlement settlement = mapSettlement(row);
Trade trade = createTrade(row, agreement, delivery, settlement);
Invoice invoice = new Invoice(ConformanceLevel.EXTENDED);
invoice.setHeader(header);
invoice.setTrade(trade);
return invoice;
}
private Trade createTrade(Row row, Agreement agreement, Delivery delivery, Settlement settlement) {
Trade trade = new Trade()
.setAgreement(agreement)
.setDelivery(delivery)
.setSettlement(settlement);
for (Item item : transformToItems(row.getItems())) {
trade.addItem(item);
}
return trade;
}
private Settlement mapSettlement(Row row) {
Row.BankInformation bankInformation = row.getIssuer().getBankInfo();
PaymentMeans paymentMeans = new PaymentMeans()
.addInformation(row.getComments())
.setPayeeAccount(new CreditorFinancialAccount(bankInformation.getIban()))
.setPayeeInstitution(new FinancialInstitution(bankInformation.getBic()).setName(bankInformation.getBankName()));
computeCalculatedTax(row);
Settlement settlement = new Settlement()
.setCurrency(currencyCode)
.addPaymentMeans(paymentMeans)
.setPaymentReference(row.getHeader().getReference())
.setMonetarySummation(calculateMonetarySummation());
addTradeTaxesFromCalculatedTax(settlement);
return settlement;
}
private MonetarySummation calculateMonetarySummation() {
MonetarySummation monetarySummation = new MonetarySummation()
.setLineTotal(new Amount(BigDecimal.ZERO, currencyCode))
.setChargeTotal(new Amount(BigDecimal.ZERO, currencyCode))
.setAllowanceTotal(new Amount(BigDecimal.ZERO, currencyCode))
.setTaxBasisTotal(new Amount(BigDecimal.ZERO, currencyCode))
.setTaxTotal(new Amount(BigDecimal.ZERO, currencyCode))
.setGrandTotal(new Amount(BigDecimal.ZERO, currencyCode));
for (Map.Entry<BigDecimal, TaxAccumulator> entry : calculatedTax.entrySet()) {
BigDecimal lineTotal = monetarySummation.getLineTotal().getValue();
BigDecimal taxBasisTotal = monetarySummation.getTaxBasisTotal().getValue();
BigDecimal taxTotal = monetarySummation.getTaxTotal().getValue();
BigDecimal grandTotal = monetarySummation.getGrandTotal().getValue();
BigDecimal curLineTotal = entry.getValue().lineTotal;
BigDecimal curTaxAmount = entry.getValue().taxAmount;
monetarySummation.setLineTotal(new Amount(lineTotal.add(curLineTotal), currencyCode));
monetarySummation.setTaxBasisTotal(new Amount(taxBasisTotal.add(curLineTotal), currencyCode));
monetarySummation.setTaxTotal(new Amount(taxTotal.add(curTaxAmount), currencyCode));
monetarySummation.setGrandTotal(new Amount(grandTotal.add(curTaxAmount).add(curLineTotal), currencyCode));
}
return monetarySummation;
}
private void addTradeTaxesFromCalculatedTax(Settlement settlement) {
for (Map.Entry<BigDecimal, TaxAccumulator> entry : calculatedTax.entrySet()) {
BigDecimal lineTotal = entry.getValue().lineTotal;
BigDecimal taxAmount = entry.getValue().taxAmount;
TradeTax tradeTax = new TradeTax()
.setType(TaxCode.VAT)
.setPercentage(entry.getKey())
.setCategory(TaxCategory.S)
.setBasis(new Amount(lineTotal, currencyCode))
.setCalculated(new Amount(taxAmount, currencyCode));
tradeTax.setLineTotal(new Amount(lineTotal, currencyCode));
settlement.addTradeTax(tradeTax);
}
}
private void computeCalculatedTax(Row row) {
for (Row.Item item : row.getItems()) {
if (item != null) {
BigDecimal percent = item.getTaxPercent();
BigDecimal unitPrice = item.getUnitPrice();
BigDecimal quantity = item.getQuantity();
if (unitPrice != null && percent != null && quantity != null) {
BigDecimal lineTotal = unitPrice.multiply(quantity);
BigDecimal taxAmount = lineTotal.multiply(
percent.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
);
TaxAccumulator taxAccumulator = new TaxAccumulator(taxAmount, lineTotal);
if (calculatedTax.containsKey(percent)) {
taxAccumulator = calculatedTax.get(percent).accumulate(taxAccumulator);
}
calculatedTax.put(percent, taxAccumulator);
}
}
}
}
private Header mapHeader(Row.Header rowHeader) {
Header header = new Header();
if (rowHeader != null) {
if (rowHeader.getIssued() != null) {
header.setIssued(new ZfDateDay(rowHeader.getIssued()));
}
if (rowHeader.getDueDate() != null) {
header.setContractualDueDate(new ZfDateDay(rowHeader.getDueDate()));
}
header.setCode(getCode(rowHeader.getType()))
.setInvoiceNumber(rowHeader.getInvoiceNumber())
.setName(rowHeader.getType());
if (!Strings.isNullOrEmpty(rowHeader.getNote())) {
header.addNote(new Note(rowHeader.getNote()));
}
currencyCode = rowHeader.getCurrency();
customerNumber = rowHeader.getCustomerNumber();
}
return header;
}
private TradeParty mapTradeParty(Row.TradeParty tradeParty) {
TradeParty recipient = new TradeParty();
if (tradeParty != null) {
recipient.setName(tradeParty.getName())
.setId(customerNumber)
.setContact(mapContact(tradeParty))
.setAddress(mapAddress(tradeParty));
if (tradeParty.getTaxes() != null) {
List<TaxRegistration> taxRegistrations = mapTaxRegistrations(tradeParty.getTaxes());
TaxRegistration[] array = new TaxRegistration[tradeParty.getTaxes().size()];
recipient.addTaxRegistrations(
taxRegistrations.toArray(array)
);
}
}
return recipient;
}
private List<TaxRegistration> mapTaxRegistrations(List<Row.Tax> taxes) {
return Lists.transform(taxes, new Function<Row.Tax, TaxRegistration>() {
@Nullable
@Override
public TaxRegistration apply(Row.Tax tax) {
return new TaxRegistration(tax.getNumber(), tax.getType());
}
});
}
private Contact mapContact(Row.TradeParty tradeParty) {
return new Contact(tradeParty.getContactName(), null, null, null, tradeParty.getEmail());
}
private Address mapAddress(Row.TradeParty tradeParty) {
return new Address(tradeParty.getPostcode(), tradeParty.getAddressLine1(), tradeParty.getAddressLine2(), tradeParty.getCity(), tradeParty.getCountryCode());
}
/**
* Transforms list of {@link io.konik.csv.model.Row.Item} to {@link Item}
* @param items
* @return
*/
private List<Item> transformToItems(List<Row.Item> items) {
final AtomicInteger index = new AtomicInteger(0);
return Lists.transform(items, new Function<Row.Item, Item>() {
public Item apply(Row.Item rowItem) {
Item item = new Item();
if (rowItem != null) {
String assignedId = String.format("%d", index.incrementAndGet());
Product product = mapProduct(assignedId, rowItem);
SpecifiedDelivery delivery = mapDelivery(rowItem);
SpecifiedSettlement settlement = mapSettlement(rowItem);
SpecifiedAgreement agreement = mapAgreement(rowItem);
item.setPosition(new PositionDocument(assignedId));
item.setProduct(product);
item.setDelivery(delivery);
item.setSettlement(settlement);
item.setAgreement(agreement);
}
return item;
}
private SpecifiedAgreement mapAgreement(Row.Item rowItem) {
SpecifiedAgreement agreement = new SpecifiedAgreement();
agreement.setNetPrice(new Price(new Amount(rowItem.getUnitPrice(), currencyCode)));
agreement.setGrossPrice(new GrossPrice(new Amount(rowItem.getUnitPrice(), currencyCode)));
return agreement;
}
private SpecifiedSettlement mapSettlement(Row.Item rowItem) {
ItemTax itemTax = mapItemTax(rowItem);
SpecifiedMonetarySummation monetarySummation = mapMonetarySummation(rowItem);
SpecifiedSettlement settlement = new SpecifiedSettlement();
settlement.addTradeTax(itemTax);
settlement.setMonetarySummation(monetarySummation);
return settlement;
}
private SpecifiedMonetarySummation mapMonetarySummation(Row.Item rowItem) {
BigDecimal lineTotal = BigDecimal.ZERO;
if (rowItem.getUnitPrice() != null && rowItem.getQuantity() != null) {
lineTotal = rowItem.getUnitPrice().multiply(rowItem.getQuantity());
}
SpecifiedMonetarySummation monetarySummation = new SpecifiedMonetarySummation();
monetarySummation.setLineTotal(new Amount(lineTotal, currencyCode));
return monetarySummation;
}
private ItemTax mapItemTax(Row.Item rowItem) {
ItemTax itemTax = new ItemTax().setType(TaxCode.VAT);
BigDecimal percent = rowItem.getTaxPercent() != null ? rowItem.getTaxPercent() : BigDecimal.ZERO;
itemTax.setPercentage(percent);
itemTax.setCategory(TaxCategory.S);
return itemTax;
}
private SpecifiedDelivery mapDelivery(Row.Item rowItem) {
SpecifiedDelivery delivery = new SpecifiedDelivery();
BigDecimal quantity = rowItem.getQuantity() != null ? rowItem.getQuantity() : BigDecimal.ZERO;
UnitOfMeasurement unit = rowItem.getUnit() != null ? rowItem.getUnit() : UnitOfMeasurement.UNIT;
delivery.setBilled(new Quantity(quantity, unit));
return delivery;
}
private Product mapProduct(String assignedId, Row.Item rowItem) {
return new Product().setName(rowItem.getName())
.setBuyerAssignedId(assignedId)
.setSellerAssignedId(assignedId);
}
});
}
private static class TaxAccumulator {
final public BigDecimal taxAmount;
final public BigDecimal lineTotal;
public TaxAccumulator(BigDecimal taxAmount, BigDecimal lineTotal) {
this.taxAmount = taxAmount;
this.lineTotal = lineTotal;
}
public TaxAccumulator accumulate(TaxAccumulator taxAccumulator) {
return new TaxAccumulator(
taxAccumulator.taxAmount.add(taxAmount),
taxAccumulator.lineTotal.add(lineTotal)
);
}
}
}
}