in applications/accounting/src/main/java/org/apache/ofbiz/accounting/tax/TaxAuthorityServices.java [344:646]
private static List<GenericValue> getTaxAdjustments(Delegator delegator, GenericValue product,
GenericValue productStore,
String payToPartyId, String billToPartyId, Set<GenericValue> taxAuthoritySet,
BigDecimal itemPrice, BigDecimal itemQuantity, BigDecimal itemAmount,
BigDecimal shippingAmount, BigDecimal orderPromotionsAmount, BigDecimal weight) {
Timestamp nowTimestamp = UtilDateTime.nowTimestamp();
List<GenericValue> adjustments = new LinkedList<>();
if (weight == null) {
weight = BigDecimal.ONE;
}
if (payToPartyId == null) {
if (productStore != null) {
payToPartyId = productStore.getString("payToPartyId");
}
}
// store expr
EntityCondition storeCond = null;
if (productStore != null) {
storeCond = EntityCondition.makeCondition(
EntityCondition.makeCondition("productStoreId", EntityOperator.EQUALS, productStore.get(
"productStoreId")),
EntityOperator.OR,
EntityCondition.makeCondition("productStoreId", EntityOperator.EQUALS, null));
} else {
storeCond = EntityCondition.makeCondition("productStoreId", EntityOperator.EQUALS, null);
}
// build the TaxAuthority expressions (taxAuthGeoId, taxAuthPartyId)
List<EntityCondition> taxAuthCondOrList = new LinkedList<>();
// start with the _NA_ TaxAuthority...
taxAuthCondOrList.add(EntityCondition.makeCondition(
EntityCondition.makeCondition("taxAuthPartyId", EntityOperator.EQUALS, "_NA_"),
EntityOperator.AND,
EntityCondition.makeCondition("taxAuthGeoId", EntityOperator.EQUALS, "_NA_")));
for (GenericValue taxAuthority : taxAuthoritySet) {
EntityCondition taxAuthCond = EntityCondition.makeCondition(
EntityCondition.makeCondition("taxAuthPartyId", EntityOperator.EQUALS, taxAuthority.getString(
"taxAuthPartyId")),
EntityOperator.AND,
EntityCondition.makeCondition("taxAuthGeoId", EntityOperator.EQUALS, taxAuthority.getString(
"taxAuthGeoId")));
taxAuthCondOrList.add(taxAuthCond);
}
EntityCondition taxAuthoritiesCond = EntityCondition.makeCondition(taxAuthCondOrList, EntityOperator.OR);
try {
EntityCondition productCategoryCond;
productCategoryCond = setProductCategoryCond(delegator, product);
if (product == null && shippingAmount != null) {
EntityCondition taxShippingCond = EntityCondition.makeCondition(
EntityCondition.makeCondition("taxShipping", EntityOperator.EQUALS, null),
EntityOperator.OR,
EntityCondition.makeCondition("taxShipping", EntityOperator.EQUALS, "Y"));
productCategoryCond = EntityCondition.makeCondition(productCategoryCond, EntityOperator.OR,
taxShippingCond);
}
if (product == null && orderPromotionsAmount != null) {
EntityCondition taxOrderPromotionsCond = EntityCondition.makeCondition(
EntityCondition.makeCondition("taxPromotions", EntityOperator.EQUALS, null),
EntityOperator.OR,
EntityCondition.makeCondition("taxPromotions", EntityOperator.EQUALS, "Y"));
productCategoryCond = EntityCondition.makeCondition(productCategoryCond, EntityOperator.OR,
taxOrderPromotionsCond);
}
// build the main condition clause
List<EntityCondition> mainExprs = UtilMisc.toList(storeCond, taxAuthoritiesCond, productCategoryCond);
mainExprs.add(EntityCondition.makeCondition(EntityCondition.makeCondition("minItemPrice",
EntityOperator.EQUALS, null), EntityOperator.OR, EntityCondition.makeCondition("minItemPrice",
EntityOperator.LESS_THAN_EQUAL_TO, itemPrice)));
mainExprs.add(EntityCondition.makeCondition(EntityCondition.makeCondition("minPurchase",
EntityOperator.EQUALS, null), EntityOperator.OR, EntityCondition.makeCondition("minPurchase",
EntityOperator.LESS_THAN_EQUAL_TO, itemAmount)));
EntityCondition mainCondition = EntityCondition.makeCondition(mainExprs, EntityOperator.AND);
// finally ready... do the rate query
List<GenericValue> lookupList = EntityQuery.use(delegator).from("TaxAuthorityRateProduct")
.where(mainCondition).orderBy("minItemPrice", "minPurchase", "fromDate").filterByDate().queryList();
if (lookupList.isEmpty()) {
Debug.logWarning("In TaxAuthority Product Rate no records were found for condition:" + mainCondition.toString(), MODULE);
return adjustments;
}
// find the right entry(s) based on purchase amount
for (GenericValue taxAuthorityRateProduct : lookupList) {
BigDecimal taxRate = taxAuthorityRateProduct.get("taxPercentage") != null ? taxAuthorityRateProduct
.getBigDecimal("taxPercentage") : ZERO_BASE;
taxRate = taxRate.multiply(weight);
BigDecimal taxable = ZERO_BASE;
if (product != null && (product.get("taxable") == null || (product.get("taxable") != null && product
.getBoolean("taxable")))) {
taxable = taxable.add(itemAmount);
}
if (shippingAmount != null && (taxAuthorityRateProduct.get("taxShipping") == null
|| (taxAuthorityRateProduct.get("taxShipping") != null && taxAuthorityRateProduct.getBoolean(
"taxShipping")))) {
taxable = taxable.add(shippingAmount);
}
if (orderPromotionsAmount != null && (taxAuthorityRateProduct.get("taxPromotions") == null
|| (taxAuthorityRateProduct.get("taxPromotions") != null && taxAuthorityRateProduct.getBoolean(
"taxPromotions")))) {
taxable = taxable.add(orderPromotionsAmount);
}
if (taxable.compareTo(BigDecimal.ZERO) == 0) {
// this should make it less confusing if the taxable flag on the product is not
// Y/true, and there is no shipping and such
continue;
}
// taxRate is in percentage, so needs to be divided by 100
BigDecimal taxAmount = (taxable.multiply(taxRate)).divide(PERCENT_SCALE, TAX_SCALE,
TAX_ROUNDING);
String taxAuthGeoId = taxAuthorityRateProduct.getString("taxAuthGeoId");
String taxAuthPartyId = taxAuthorityRateProduct.getString("taxAuthPartyId");
// get glAccountId from TaxAuthorityGlAccount entity using the payToPartyId as
// the organizationPartyId
GenericValue taxAuthorityGlAccount = EntityQuery.use(delegator).from("TaxAuthorityGlAccount")
.where("taxAuthPartyId", taxAuthPartyId, "taxAuthGeoId", taxAuthGeoId, "organizationPartyId", payToPartyId).queryOne();
String taxAuthGlAccountId = null;
if (taxAuthorityGlAccount != null) {
taxAuthGlAccountId = taxAuthorityGlAccount.getString("glAccountId");
} else {
// TODO: what to do if no TaxAuthorityGlAccount found? Use some default, or is that done elsewhere later on?
Debug.logVerbose("what to do if no TaxAuthorityGlAccount found?", MODULE);
}
GenericValue productPrice = null;
if (product != null && taxAuthPartyId != null && taxAuthGeoId != null) {
// find a ProductPrice for the productId and taxAuth* values, and see if it has
// a priceWithTax value
productPrice = getProductPrice(delegator, product, productStore, taxAuthGeoId, taxAuthPartyId);
if (productPrice == null) {
GenericValue virtualProduct = ProductWorker.getParentProduct(product.getString("productId"), delegator);
if (virtualProduct != null) {
productPrice = getProductPrice(delegator, virtualProduct, productStore, taxAuthGeoId, taxAuthPartyId);
}
}
}
GenericValue taxAdjValue = delegator.makeValue("OrderAdjustment");
BigDecimal discountedSalesTax = BigDecimal.ZERO;
taxAdjValue.set("orderAdjustmentTypeId", "SALES_TAX");
if (productPrice != null && "Y".equals(productPrice.getString("taxInPrice"))
&& itemQuantity != BigDecimal.ZERO) {
// For example product price is 43 with 20% VAT(means product actual price is
// 35.83).
// itemPrice = 43;
// itemQuantity = 3;
// taxAmountIncludedInFullPrice = (43-(43/(1+(20/100))))*3 = 21.51
taxAdjValue.set("orderAdjustmentTypeId", "VAT_TAX");
BigDecimal taxAmountIncludedInFullPrice = itemPrice.subtract(itemPrice.divide(BigDecimal.ONE.add(
taxRate.divide(PERCENT_SCALE, 4, RoundingMode.HALF_UP)), 2, RoundingMode.HALF_UP)).multiply(
itemQuantity);
// If 1 quantity has 50% discount then itemAmount = 107.5 otherwise 129 (In case
// of no discount)
// Net price for each item
// netItemPrice = itemAmount / quantity = 107.5 / 3 = 35.833333333
BigDecimal netItemPrice = itemAmount.divide(itemQuantity, RoundingMode.HALF_UP);
// Calculate tax on the discounted price, be sure to round to 2 decimal places
// before multiplying by quantity
// netTax = (netItemPrice - netItemPrice / (1 + (taxRate/100))) * quantity
// netTax = (35.833333333-(35.833333333/(1+(20/100))))*3 = 17.92
BigDecimal netTax = netItemPrice.subtract(netItemPrice.divide(BigDecimal.ONE.add(taxRate.divide(
PERCENT_SCALE, 4, RoundingMode.HALF_UP)), 2, RoundingMode.HALF_UP)).multiply(itemQuantity);
// Subtract net tax from base tax (taxAmountIncludedFullPrice) to get the
// negative promotion tax adjustment amount
// discountedSalesTax = 17.92 - 21.51 = −3.59 (If no discounted item quantity
// then discountedSalesTax will be ZERO)
discountedSalesTax = netTax.subtract(taxAmountIncludedInFullPrice);
taxAdjValue.set("amountAlreadyIncluded", taxAmountIncludedInFullPrice);
taxAdjValue.set("amount", BigDecimal.ZERO);
} else {
taxAdjValue.set("amount", taxAmount);
}
taxAdjValue.set("sourcePercentage", taxRate);
taxAdjValue.set("taxAuthorityRateSeqId", taxAuthorityRateProduct.getString("taxAuthorityRateSeqId"));
// the primary Geo should be the main jurisdiction that the tax is for, and the
// secondary would just be to define a parent or wrapping jurisdiction of the
// primary
taxAdjValue.set("primaryGeoId", taxAuthGeoId);
taxAdjValue.set("comments", taxAuthorityRateProduct.getString("description"));
if (taxAuthPartyId != null) {
taxAdjValue.set("taxAuthPartyId", taxAuthPartyId);
}
if (taxAuthGlAccountId != null) {
taxAdjValue.set("overrideGlAccountId", taxAuthGlAccountId);
}
if (taxAuthGeoId != null) {
taxAdjValue.set("taxAuthGeoId", taxAuthGeoId);
}
// check to see if this party has a tax ID for this, and if the party is tax
// exempt in the primary (most-local) jurisdiction
if (UtilValidate.isNotEmpty(billToPartyId) && UtilValidate.isNotEmpty(taxAuthGeoId)) {
// see if partyId is a member of any groups, if so honor their tax exemptions
// look for PartyRelationship with partyRelationshipTypeId=GROUP_ROLLUP, the
// partyIdTo is the group member, so the partyIdFrom is the groupPartyId
Set<String> billToPartyIdSet = new HashSet<>();
billToPartyIdSet.add(billToPartyId);
List<GenericValue> partyRelationshipList = EntityQuery.use(delegator).from("PartyRelationship")
.where("partyIdTo", billToPartyId, "partyRelationshipTypeId", "GROUP_ROLLUP")
.cache().filterByDate().queryList();
for (GenericValue partyRelationship : partyRelationshipList) {
billToPartyIdSet.add(partyRelationship.getString("partyIdFrom"));
}
handlePartyTaxExempt(taxAdjValue, billToPartyIdSet, taxAuthGeoId, taxAuthPartyId, taxAmount,
nowTimestamp, delegator);
} else {
Debug.logInfo("NOTE: A tax calculation was done without a billToPartyId or taxAuthGeoId, so no tax exemptions or tax IDs "
+ "considered; billToPartyId=[" + billToPartyId + "] taxAuthGeoId=[" + taxAuthGeoId + "]", MODULE);
}
if (discountedSalesTax.compareTo(BigDecimal.ZERO) < 0) {
GenericValue taxAdjValueNegative = delegator.makeValue("OrderAdjustment");
taxAdjValueNegative.setFields(taxAdjValue);
taxAdjValueNegative.set("amountAlreadyIncluded", discountedSalesTax);
adjustments.add(taxAdjValueNegative);
}
adjustments.add(taxAdjValue);
if (productPrice != null && itemQuantity != null
&& productPrice.getBigDecimal("priceWithTax") != null
&& !"Y".equals(productPrice.getString("taxInPrice"))) {
BigDecimal priceWithTax = productPrice.getBigDecimal("priceWithTax");
BigDecimal price = productPrice.getBigDecimal("price");
BigDecimal baseSubtotal = price.multiply(itemQuantity);
BigDecimal baseTaxAmount = (baseSubtotal.multiply(taxRate)).divide(PERCENT_SCALE,
TAX_SCALE, TAX_ROUNDING);
// tax is not already in price so we want to add it in, but this is a VAT
// situation so adjust to make it as accurate as possible
// for VAT taxes if the calculated total item price plus calculated taxes is
// different from what would be
// expected based on the original entered price with taxes (if the price was
// entered this way), then create
// an adjustment that corrects for the difference, and this correction will be
// effectively subtracted from the
// price and not from the tax (the tax is meant to be calculated based on Tax
// Authority rules and so should
// not be shorted)
// TODO (don't think this is needed, but just to keep it in mind): get this to
// work with multiple VAT tax authorities instead of just one (right now will
// get incorrect totals if there are multiple taxes included in the price)
// TODO add constraint to ProductPrice lookup by any productStoreGroupId
// associated with the current productStore
BigDecimal enteredTotalPriceWithTax = priceWithTax.multiply(itemQuantity);
BigDecimal calcedTotalPriceWithTax = (baseSubtotal).add(baseTaxAmount);
if (!enteredTotalPriceWithTax.equals(calcedTotalPriceWithTax)) {
// if the calculated amount is higher than the entered amount we want the value
// to be negative
// to get it down to match the entered amount
// so, subtract the calculated amount from the entered amount (ie: correction =
// entered - calculated)
BigDecimal correctionAmount = enteredTotalPriceWithTax.subtract(calcedTotalPriceWithTax);
GenericValue correctionAdjValue = delegator.makeValue("OrderAdjustment");
correctionAdjValue.set("taxAuthorityRateSeqId", taxAuthorityRateProduct.getString(
"taxAuthorityRateSeqId"));
correctionAdjValue.set("amount", correctionAmount);
// don't set this, causes a doubling of the tax rate because calling code adds
// up all tax rates: correctionAdjValue.set("sourcePercentage", taxRate);
correctionAdjValue.set("orderAdjustmentTypeId", "VAT_PRICE_CORRECT");
// the primary Geo should be the main jurisdiction that the tax is for, and the
// secondary would just be to define a parent or wrapping jurisdiction of the
// primary
correctionAdjValue.set("primaryGeoId", taxAuthGeoId);
correctionAdjValue.set("comments", taxAuthorityRateProduct.getString("description"));
if (taxAuthPartyId != null) {
correctionAdjValue.set("taxAuthPartyId", taxAuthPartyId);
}
if (taxAuthGlAccountId != null) {
correctionAdjValue.set("overrideGlAccountId", taxAuthGlAccountId);
}
if (taxAuthGeoId != null) {
correctionAdjValue.set("taxAuthGeoId", taxAuthGeoId);
}
adjustments.add(correctionAdjValue);
}
}
}
} catch (GenericEntityException e) {
Debug.logError(e, "Problems looking up tax rates", MODULE);
return new LinkedList<>();
}
return adjustments;
}