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}