001package io.konik.csv.converter;
002
003import com.google.common.base.Function;
004import com.google.common.base.Strings;
005import com.google.common.collect.Lists;
006import com.neovisionaries.i18n.CurrencyCode;
007import io.konik.csv.model.Row;
008import io.konik.zugferd.Invoice;
009import io.konik.zugferd.entity.*;
010import io.konik.zugferd.entity.trade.*;
011import io.konik.zugferd.entity.trade.item.*;
012import io.konik.zugferd.profile.ConformanceLevel;
013import io.konik.zugferd.unece.codes.DocumentCode;
014import io.konik.zugferd.unece.codes.TaxCategory;
015import io.konik.zugferd.unece.codes.TaxCode;
016import io.konik.zugferd.unece.codes.UnitOfMeasurement;
017import io.konik.zugferd.unqualified.Amount;
018import io.konik.zugferd.unqualified.Quantity;
019import io.konik.zugferd.unqualified.ZfDateDay;
020
021import javax.annotation.Nullable;
022import java.math.BigDecimal;
023import java.math.RoundingMode;
024import java.util.List;
025import java.util.Map;
026import java.util.Objects;
027import java.util.concurrent.ConcurrentHashMap;
028import java.util.concurrent.ConcurrentMap;
029import java.util.concurrent.atomic.AtomicInteger;
030
031/**
032 * Converter from {@link Row} to {@link Invoice}
033 */
034public class RowToInvoiceConverter {
035
036        private static final ConcurrentMap<String, DocumentCode> codes = new ConcurrentHashMap<String, DocumentCode>();
037
038        static {
039                codes.put("rechnung", DocumentCode._380);
040                codes.put("gutschriftsanzeige", DocumentCode._380);
041                codes.put("angebot", DocumentCode._310);
042                codes.put("bestellung", DocumentCode._220);
043                codes.put("proformarechnung", DocumentCode._325);
044                codes.put("teilrechnung", DocumentCode._326);
045                codes.put("korrigierte rechnung", DocumentCode._384);
046                codes.put("konsolidierte rechnung", DocumentCode._385);
047                codes.put("vorauszahlungsrechnung", DocumentCode._386);
048                codes.put("invoice", DocumentCode._380);
049                codes.put("credit note", DocumentCode._381);
050                codes.put("offer", DocumentCode._310);
051                codes.put("order", DocumentCode._220);
052                codes.put("proforma invoice", DocumentCode._325);
053                codes.put("partial invoice", DocumentCode._326);
054                codes.put("corrected invoice", DocumentCode._384);
055                codes.put("consolidated invoice", DocumentCode._385);
056                codes.put("prepayment invoice", DocumentCode._386);
057        }
058
059        public static Invoice convert(Row row) {
060                Objects.requireNonNull(row);
061
062                return new Process().run(row);
063        }
064
065        protected static DocumentCode getCode(String code) {
066                if (code != null) {
067                        String key = code.trim().toLowerCase();
068
069                        if (codes.containsKey(key)) {
070                                return codes.get(key);
071                        }
072                }
073
074                return DocumentCode._380;
075        }
076
077        /**
078         * Internal class to manage conversion state in a thread safe manner.
079         */
080        private static class Process {
081
082                private CurrencyCode currencyCode;
083                private String customerNumber;
084                private ConcurrentMap<BigDecimal, TaxAccumulator> calculatedTax = new ConcurrentHashMap<BigDecimal, TaxAccumulator>();
085
086                protected Invoice run(Row row) {
087                        Header header = mapHeader(row.getHeader());
088
089                        TradeParty buyer = mapTradeParty(row.getRecipient());
090                        TradeParty seller = mapTradeParty(row.getIssuer());
091
092                        Agreement agreement = new Agreement()
093                                        .setBuyer(buyer)
094                                        .setSeller(seller);
095
096                        Delivery delivery = new Delivery(header.getIssued());
097
098                        Settlement settlement = mapSettlement(row);
099
100                        Trade trade = createTrade(row, agreement, delivery, settlement);
101
102                        Invoice invoice = new Invoice(ConformanceLevel.EXTENDED);
103                        invoice.setHeader(header);
104                        invoice.setTrade(trade);
105
106                        return invoice;
107                }
108
109                private Trade createTrade(Row row, Agreement agreement, Delivery delivery, Settlement settlement) {
110                        Trade trade = new Trade()
111                                        .setAgreement(agreement)
112                                        .setDelivery(delivery)
113                                        .setSettlement(settlement);
114
115                        for (Item item : transformToItems(row.getItems())) {
116                                trade.addItem(item);
117                        }
118                        return trade;
119                }
120
121                private Settlement mapSettlement(Row row) {
122                        Row.BankInformation bankInformation = row.getIssuer().getBankInfo();
123                        PaymentMeans paymentMeans = new PaymentMeans()
124                                        .addInformation(row.getComments())
125                                        .setPayeeAccount(new CreditorFinancialAccount(bankInformation.getIban()))
126                                        .setPayeeInstitution(new FinancialInstitution(bankInformation.getBic()).setName(bankInformation.getBankName()));
127
128                        computeCalculatedTax(row);
129
130                        Settlement settlement = new Settlement()
131                                        .setCurrency(currencyCode)
132                                        .addPaymentMeans(paymentMeans)
133                                        .setPaymentReference(row.getHeader().getReference())
134                                        .setMonetarySummation(calculateMonetarySummation());
135
136                        addTradeTaxesFromCalculatedTax(settlement);
137                        return settlement;
138                }
139
140                private MonetarySummation calculateMonetarySummation() {
141                        MonetarySummation monetarySummation = new MonetarySummation()
142                                        .setLineTotal(new Amount(BigDecimal.ZERO, currencyCode))
143                                        .setChargeTotal(new Amount(BigDecimal.ZERO, currencyCode))
144                                        .setAllowanceTotal(new Amount(BigDecimal.ZERO, currencyCode))
145                                        .setTaxBasisTotal(new Amount(BigDecimal.ZERO, currencyCode))
146                                        .setTaxTotal(new Amount(BigDecimal.ZERO, currencyCode))
147                                        .setGrandTotal(new Amount(BigDecimal.ZERO, currencyCode));
148
149                        for (Map.Entry<BigDecimal, TaxAccumulator> entry : calculatedTax.entrySet()) {
150                                BigDecimal lineTotal = monetarySummation.getLineTotal().getValue();
151                                BigDecimal taxBasisTotal = monetarySummation.getTaxBasisTotal().getValue();
152                                BigDecimal taxTotal = monetarySummation.getTaxTotal().getValue();
153                                BigDecimal grandTotal = monetarySummation.getGrandTotal().getValue();
154
155                                BigDecimal curLineTotal = entry.getValue().lineTotal;
156                                BigDecimal curTaxAmount = entry.getValue().taxAmount;
157
158                                monetarySummation.setLineTotal(new Amount(lineTotal.add(curLineTotal), currencyCode));
159                                monetarySummation.setTaxBasisTotal(new Amount(taxBasisTotal.add(curLineTotal), currencyCode));
160                                monetarySummation.setTaxTotal(new Amount(taxTotal.add(curTaxAmount), currencyCode));
161                                monetarySummation.setGrandTotal(new Amount(grandTotal.add(curTaxAmount).add(curLineTotal), currencyCode));
162                        }
163                        return monetarySummation;
164                }
165
166                private void addTradeTaxesFromCalculatedTax(Settlement settlement) {
167                        for (Map.Entry<BigDecimal, TaxAccumulator> entry : calculatedTax.entrySet()) {
168                                BigDecimal lineTotal = entry.getValue().lineTotal;
169                                BigDecimal taxAmount = entry.getValue().taxAmount;
170
171                                TradeTax tradeTax = new TradeTax()
172                                                .setType(TaxCode.VAT)
173                                                .setPercentage(entry.getKey())
174                                                .setCategory(TaxCategory.S)
175                                                .setBasis(new Amount(lineTotal, currencyCode))
176                                                .setCalculated(new Amount(taxAmount, currencyCode));
177                                tradeTax.setLineTotal(new Amount(lineTotal, currencyCode));
178
179                                settlement.addTradeTax(tradeTax);
180                        }
181                }
182
183                private void computeCalculatedTax(Row row) {
184                        for (Row.Item item : row.getItems()) {
185                                if (item != null) {
186                                        BigDecimal percent = item.getTaxPercent();
187                                        BigDecimal unitPrice = item.getUnitPrice();
188                                        BigDecimal quantity = item.getQuantity();
189
190                                        if (unitPrice != null && percent != null && quantity != null) {
191                                                BigDecimal lineTotal = unitPrice.multiply(quantity);
192                                                BigDecimal taxAmount = lineTotal.multiply(
193                                                                percent.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
194                                                );
195
196                                                TaxAccumulator taxAccumulator = new TaxAccumulator(taxAmount, lineTotal);
197                                                if (calculatedTax.containsKey(percent)) {
198                                                        taxAccumulator = calculatedTax.get(percent).accumulate(taxAccumulator);
199                                                }
200
201                                                calculatedTax.put(percent, taxAccumulator);
202                                        }
203                                }
204                        }
205                }
206
207                private Header mapHeader(Row.Header rowHeader) {
208                        Header header = new Header();
209
210                        if (rowHeader != null) {
211                                if (rowHeader.getIssued() != null) {
212                                        header.setIssued(new ZfDateDay(rowHeader.getIssued()));
213                                }
214
215                                if (rowHeader.getDueDate() != null) {
216                                        header.setContractualDueDate(new ZfDateDay(rowHeader.getDueDate()));
217                                }
218
219                                header.setCode(getCode(rowHeader.getType()))
220                                                .setInvoiceNumber(rowHeader.getInvoiceNumber())
221                                                .setName(rowHeader.getType());
222
223                                if (!Strings.isNullOrEmpty(rowHeader.getNote())) {
224                                        header.addNote(new Note(rowHeader.getNote()));
225                                }
226
227                                currencyCode = rowHeader.getCurrency();
228
229                                customerNumber = rowHeader.getCustomerNumber();
230                        }
231
232                        return header;
233                }
234
235                private TradeParty mapTradeParty(Row.TradeParty tradeParty) {
236                        TradeParty recipient = new TradeParty();
237
238                        if (tradeParty != null) {
239                                recipient.setName(tradeParty.getName())
240                                                .setId(customerNumber)
241                                                .setContact(mapContact(tradeParty))
242                                                .setAddress(mapAddress(tradeParty));
243
244                                if (tradeParty.getTaxes() != null) {
245                                        List<TaxRegistration> taxRegistrations = mapTaxRegistrations(tradeParty.getTaxes());
246                                        TaxRegistration[] array = new TaxRegistration[tradeParty.getTaxes().size()];
247
248                                        recipient.addTaxRegistrations(
249                                                        taxRegistrations.toArray(array)
250                                        );
251                                }
252                        }
253
254                        return recipient;
255                }
256
257                private List<TaxRegistration> mapTaxRegistrations(List<Row.Tax> taxes) {
258                        return Lists.transform(taxes, new Function<Row.Tax, TaxRegistration>() {
259                                @Nullable
260                                @Override
261                                public TaxRegistration apply(Row.Tax tax) {
262                                        return new TaxRegistration(tax.getNumber(), tax.getType());
263                                }
264                        });
265                }
266
267                private Contact mapContact(Row.TradeParty tradeParty) {
268                        return new Contact(tradeParty.getContactName(), null, null, null, tradeParty.getEmail());
269                }
270
271                private Address mapAddress(Row.TradeParty tradeParty) {
272                        return new Address(tradeParty.getPostcode(), tradeParty.getAddressLine1(), tradeParty.getAddressLine2(), tradeParty.getCity(), tradeParty.getCountryCode());
273                }
274
275                /**
276                 * Transforms list of {@link io.konik.csv.model.Row.Item} to {@link Item}
277                 * @param items
278                 * @return
279                 */
280                private List<Item> transformToItems(List<Row.Item> items) {
281                        final AtomicInteger index = new AtomicInteger(0);
282
283                        return Lists.transform(items, new Function<Row.Item, Item>() {
284                                public Item apply(Row.Item rowItem) {
285                                        Item item = new Item();
286                                        if (rowItem != null) {
287                                                String assignedId = String.format("%d", index.incrementAndGet());
288
289                                                Product product = mapProduct(assignedId, rowItem);
290
291                                                SpecifiedDelivery delivery = mapDelivery(rowItem);
292
293                                                SpecifiedSettlement settlement = mapSettlement(rowItem);
294
295                                                SpecifiedAgreement agreement = mapAgreement(rowItem);
296
297                                                item.setPosition(new PositionDocument(assignedId));
298                                                item.setProduct(product);
299                                                item.setDelivery(delivery);
300                                                item.setSettlement(settlement);
301                                                item.setAgreement(agreement);
302                                        }
303                                        return item;
304                                }
305
306                                private SpecifiedAgreement mapAgreement(Row.Item rowItem) {
307                                        SpecifiedAgreement agreement = new SpecifiedAgreement();
308                                        agreement.setNetPrice(new Price(new Amount(rowItem.getUnitPrice(), currencyCode)));
309                                        agreement.setGrossPrice(new GrossPrice(new Amount(rowItem.getUnitPrice(), currencyCode)));
310                                        return agreement;
311                                }
312
313                                private SpecifiedSettlement mapSettlement(Row.Item rowItem) {
314                                        ItemTax itemTax = mapItemTax(rowItem);
315
316                                        SpecifiedMonetarySummation monetarySummation = mapMonetarySummation(rowItem);
317
318                                        SpecifiedSettlement settlement = new SpecifiedSettlement();
319                                        settlement.addTradeTax(itemTax);
320                                        settlement.setMonetarySummation(monetarySummation);
321                                        return settlement;
322                                }
323
324                                private SpecifiedMonetarySummation mapMonetarySummation(Row.Item rowItem) {
325                                        BigDecimal lineTotal = BigDecimal.ZERO;
326                                        if (rowItem.getUnitPrice() != null && rowItem.getQuantity() != null) {
327                                                lineTotal = rowItem.getUnitPrice().multiply(rowItem.getQuantity());
328                                        }
329                                        SpecifiedMonetarySummation monetarySummation = new SpecifiedMonetarySummation();
330                                        monetarySummation.setLineTotal(new Amount(lineTotal, currencyCode));
331                                        return monetarySummation;
332                                }
333
334                                private ItemTax mapItemTax(Row.Item rowItem) {
335                                        ItemTax itemTax = new ItemTax().setType(TaxCode.VAT);
336                                        BigDecimal percent = rowItem.getTaxPercent() != null ? rowItem.getTaxPercent() : BigDecimal.ZERO;
337                                        itemTax.setPercentage(percent);
338                                        itemTax.setCategory(TaxCategory.S);
339                                        return itemTax;
340                                }
341
342                                private SpecifiedDelivery mapDelivery(Row.Item rowItem) {
343                                        SpecifiedDelivery delivery = new SpecifiedDelivery();
344
345                                        BigDecimal quantity = rowItem.getQuantity() != null ? rowItem.getQuantity() : BigDecimal.ZERO;
346                                        UnitOfMeasurement unit = rowItem.getUnit() != null ? rowItem.getUnit() : UnitOfMeasurement.UNIT;
347
348                                        delivery.setBilled(new Quantity(quantity, unit));
349
350                                        return delivery;
351                                }
352
353                                private Product mapProduct(String assignedId, Row.Item rowItem) {
354                                        return new Product().setName(rowItem.getName())
355                                                        .setBuyerAssignedId(assignedId)
356                                                        .setSellerAssignedId(assignedId);
357                                }
358                        });
359                }
360
361                private static class TaxAccumulator {
362                        final public BigDecimal taxAmount;
363                        final public BigDecimal lineTotal;
364
365                        public TaxAccumulator(BigDecimal taxAmount, BigDecimal lineTotal) {
366                                this.taxAmount = taxAmount;
367                                this.lineTotal = lineTotal;
368                        }
369
370                        public TaxAccumulator accumulate(TaxAccumulator taxAccumulator) {
371                                return new TaxAccumulator(
372                                                taxAccumulator.taxAmount.add(taxAmount),
373                                                taxAccumulator.lineTotal.add(lineTotal)
374                                );
375                        }
376                }
377        }
378}