/*
jBilling - The Enterprise Open Source Billing System
Copyright (C) 2003-2011 Enterprise jBilling Software Ltd. and Emiliano Conde
This file is part of jbilling.
jbilling is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
jbilling is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with jbilling. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sapienter.jbilling.server.pluggableTask;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import com.sapienter.jbilling.common.SessionInternalError;
import com.sapienter.jbilling.server.invoice.InvoiceBL;
import com.sapienter.jbilling.server.invoice.NewInvoiceDTO;
import com.sapienter.jbilling.server.invoice.db.InvoiceDTO;
import com.sapienter.jbilling.server.invoice.db.InvoiceLineDTO;
import com.sapienter.jbilling.server.order.db.OrderDTO;
import com.sapienter.jbilling.server.order.db.OrderLineDTO;
import com.sapienter.jbilling.server.process.PeriodOfTime;
import com.sapienter.jbilling.server.user.UserBL;
import com.sapienter.jbilling.server.util.Constants;
import com.sapienter.jbilling.server.util.PreferenceBL;
import org.joda.time.DateMidnight;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
/**
* This task will copy all the lines on the orders and invoices
* to the new invoice, considering the periods involved for each
* order, but not the fractions of perios. It will not copy the
* lines that are taxes.
* The quantity and total of each line will be multiplied by the
* amount of periods.
* @author Emil Created on 27-Apr-2003
*/
public class BasicCompositionTask extends PluggableTask implements InvoiceCompositionTask {
private static final Logger LOG = Logger.getLogger(BasicCompositionTask.class);
private Locale locale = null;
public void apply(NewInvoiceDTO invoiceDTO, Integer userId) throws TaskException {
/*
Process each order being included in this invoice
*/
for (int orderIndex = 0; orderIndex < invoiceDTO.getOrders().size(); orderIndex++) {
OrderDTO order = (OrderDTO) invoiceDTO.getOrders().get(orderIndex);
BigDecimal orderContribution = BigDecimal.ZERO;
// add customer notes
if (Integer.valueOf(1).equals(order.getNotesInInvoice()) && !StringUtils.isBlank(order.getNotes())) {
if (invoiceDTO.getCustomerNotes() == null) {
invoiceDTO.setCustomerNotes(order.getNotes());
} else {
invoiceDTO.setCustomerNotes(invoiceDTO.getCustomerNotes() + " " + order.getNotes());
}
}
/*
Add order lines - excluding taxes
*/
for (OrderLineDTO orderLine : order.getLines()) {
// skip deleted lines
if (orderLine.getDeleted() == 1) {
continue;
}
for (PeriodOfTime period : invoiceDTO.getPeriods().get(orderIndex)) {
LOG.debug("Adding order line from " + order.getId()
+ ", quantity " + orderLine.getQuantity()
+ ", price " + orderLine.getPrice());
LOG.debug("Period: " + period);
InvoiceLineDTO invoiceLine = null;
if (orderLine.getOrderLineType().getId() == Constants.ORDER_LINE_TYPE_ITEM) {
// compose order line description
String desc = composeDescription(order, period, orderLine.getDescription());
// determine the invoice line type (one-time, recurring, line from sub-account)
Integer type;
if (userId.equals(order.getUser().getId())) {
if (Constants.ORDER_PERIOD_ONCE.equals(order.getPeriodId())) {
type = Constants.INVOICE_LINE_TYPE_ITEM_ONETIME;
} else {
type = Constants.INVOICE_LINE_TYPE_ITEM_RECURRING;
}
} else {
type = Constants.INVOICE_LINE_TYPE_SUB_ACCOUNT;
}
BigDecimal periodAmount = calculatePeriodAmount(orderLine.getAmount(), period);
invoiceLine = new InvoiceLineDTO(null,
desc,
periodAmount,
orderLine.getPrice(),
orderLine.getQuantity(),
type,
0,
orderLine.getItemId(),
order.getUser().getId(),
null);
// link invoice line to the order that originally held the charge
invoiceLine.setOrderId(order.getId());
orderContribution = orderContribution.add(periodAmount);
} else if (orderLine.getOrderLineType().getId() == Constants.ORDER_LINE_TYPE_TAX) {
// tax items
int taxLineIndex = getTaxLineIndex(invoiceDTO.getResultLines(), orderLine.getDescription());
if (taxLineIndex >= 0) {
// tax already exists, add the total
invoiceLine = (InvoiceLineDTO) invoiceDTO.getResultLines().get(taxLineIndex);
BigDecimal periodAmount = calculatePeriodAmount(orderLine.getAmount(), period);
invoiceLine.setAmount(invoiceLine.getAmount().add(periodAmount));
orderContribution = orderContribution.add(periodAmount);
} else {
// tax has not yet been added, add a new invoice line
BigDecimal periodAmount = calculatePeriodAmount(orderLine.getAmount(), period);
invoiceLine = new InvoiceLineDTO(null,
orderLine.getDescription(),
periodAmount,
orderLine.getPrice(),
null,
Constants.INVOICE_LINE_TYPE_TAX,
0,
orderLine.getItemId(),
order.getUser().getId(),
null);
orderContribution = orderContribution.add(periodAmount);
}
} else if (orderLine.getOrderLineType().getId() == Constants.ORDER_LINE_TYPE_PENALTY) {
// penalty items
BigDecimal periodAmount = calculatePeriodAmount(orderLine.getAmount(), period);
invoiceLine = new InvoiceLineDTO(null,
orderLine.getDescription(),
periodAmount,
null,
null,
Constants.INVOICE_LINE_TYPE_PENALTY,
0,
orderLine.getItemId(),
order.getUser().getId(),
null);
orderContribution = orderContribution.add(periodAmount);
}
// for the invoice to make sense when it is displayed,
// each line has to be rounded to the decimals shown
invoiceDTO.addResultLine(invoiceLine);
}
}
// save the order contribution
saveOrderTotalContributionToInvoice(order.getId(), invoiceDTO, orderContribution);
}
/*
add delegated invoices
*/
for (InvoiceDTO invoice : invoiceDTO.getInvoices()) {
// the whole invoice will be added as a single line
// The text of this line has to be i18n
InvoiceBL bl = new InvoiceBL(invoice.getId());
Locale locale = getLocale(bl.getEntity().getUserId());
ResourceBundle bundle = ResourceBundle.getBundle("entityNotifications", locale);
SimpleDateFormat df = new SimpleDateFormat(bundle.getString("format.date"));
StringBuilder delLine = new StringBuilder();
delLine.append(bundle.getString("invoice.line.delegated"));
delLine.append(" ").append(invoice.getPublicNumber()).append(" ");
delLine.append(bundle.getString("invoice.line.delegated.due"));
delLine.append(" ").append(df.format(invoice.getDueDate()));
InvoiceLineDTO invoiceLine = new InvoiceLineDTO(null,
delLine.toString(),
invoice.getBalance(),
null,
null,
Constants.INVOICE_LINE_TYPE_DUE_INVOICE,
0,
null,
null,
0);
invoiceDTO.addResultLine(invoiceLine);
}
}
/**
* Returns the index of a tax line with the matching description. Used to find an existing
* tax line so that similar taxes can be consolidated;
*
* @param lines invoice lines
* @param desc tax line description
* @return index of tax line
*/
protected int getTaxLineIndex(List lines, String desc) {
for (int f = 0; f < lines.size(); f++) {
InvoiceLineDTO line = (InvoiceLineDTO) lines.get(f);
if (line.getTypeId() == Constants.ORDER_LINE_TYPE_TAX) {
if (line.getDescription().equals(desc)) {
return f;
}
}
}
return -1;
}
/**
* Saves the amount the order contributed to the invoice total.
*
* @param orderId order id
* @param invoiceDTO new invoice
* @param amount amount contributed by order
*/
protected void saveOrderTotalContributionToInvoice(Integer orderId, NewInvoiceDTO invoiceDTO, BigDecimal amount) {
Map<Integer, BigDecimal> orderTotalContributions = invoiceDTO.getOrderTotalContributions();
BigDecimal total = orderTotalContributions.get(orderId);
if (total == null) {
total = amount;
} else {
total = total.add(amount);
}
orderTotalContributions.put(orderId, total);
}
/**
* Composes the actual invoice line description based off of set entity preferences and the
* order period being processed.
*
* @param order order being processed
* @param period period of time being processed
* @param desc original order line description
* @return invoice line description
*/
public String composeDescription(OrderDTO order, PeriodOfTime period, String desc) {
Locale locale = getLocale(order.getBaseUserByUserId().getId());
ResourceBundle bundle = ResourceBundle.getBundle("entityNotifications", locale);
StringBuilder lineDescription = new StringBuilder();
lineDescription.append(desc);
/*
append the billing period to the order line for non one-time orders
*/
if (order.getOrderPeriod().getId() != Constants.ORDER_PERIOD_ONCE) {
// period ends at midnight of the next day (E.g., Oct 1 00:00, effectivley end-of-day Sept 30th).
// subtract 1 day from the end so the period print out looks human readable
DateMidnight start = period.getDateMidnightStart();
DateMidnight end = period.getDateMidnightEnd().minusDays(1);
DateTimeFormatter dateFormat = DateTimeFormat.forPattern(bundle.getString("format.date"));
LOG.debug("Composing for period " + start + " to " + end);
LOG.debug("Using date format: " + bundle.getString("format.date"));
// now add this to the line
lineDescription.append(" ");
lineDescription.append(bundle.getString("invoice.line.period"));
lineDescription.append(" ");
lineDescription.append(dateFormat.print(start));
lineDescription.append(" ");
lineDescription.append(bundle.getString("invoice.line.to"));
lineDescription.append(" ");
lineDescription.append(dateFormat.print(end));
}
/*
optionally append the order id if the entity has the preference set
*/
if (appendOrderId(order.getBaseUserByUserId().getCompany().getId())) {
lineDescription.append(bundle.getString("invoice.line.orderNumber"));
lineDescription.append(" ");
lineDescription.append(order.getId().toString());
}
return lineDescription.toString();
}
/**
* Calculates a price based on the period of time used.
*
* This plug-in does not do any period price calculation. The given price will be returned
* untouched (see {@link com.sapienter.jbilling.server.process.task.DailyProRateCompositionTask}).
*
* @param fullPrice full line price
* @param period period to calculate amount for
* @return period price
*/
public BigDecimal calculatePeriodAmount(BigDecimal fullPrice, PeriodOfTime period) {
return fullPrice;
}
/**
* Gets the locale for the give user.
*
* @param userId user to get locale for
* @return users locale
*/
protected Locale getLocale(Integer userId) {
if (locale == null) {
try {
UserBL user = new UserBL(userId);
locale = user.getLocale();
} catch (Exception e) {
throw new SessionInternalError("Exception occurred determining user locale for composition.", e);
}
}
return locale;
}
/**
* Returns true if the given entity wants the order ID appended to the invoice line description.
*
* @param entityId entity id
* @return true if order ID should be appended, false if not.
*/
protected boolean appendOrderId(Integer entityId) {
PreferenceBL pref = new PreferenceBL();
try {
pref.set(entityId, Constants.PREFERENCE_ORDER_IN_INVOICE_LINE);
} catch (Exception e) {
/* use default value */
}
return pref.getInt() == 1;
}
}