// Chromis POS - The New Face of Open Source POS // Copyright (c) (c) 2015-2016 // http://www.chromis.co.uk // // This file is part of Chromis POS // // Chromis POS is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Chromis POS 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 General Public License for more details. // // You should have received a copy of the GNU General Public License // package uk.chromis.pos.promotion; import java.awt.Component; import java.awt.Dialog; import java.awt.Font; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.UIManager; import javax.swing.plaf.FontUIResource; import uk.chromis.basic.BasicException; import uk.chromis.data.loader.Session; import uk.chromis.pos.forms.DataLogicSales; import uk.chromis.pos.scripting.ScriptEngine; import uk.chromis.pos.scripting.ScriptException; import uk.chromis.pos.scripting.ScriptFactory; import uk.chromis.pos.ticket.TaxInfo; import uk.chromis.pos.ticket.TicketInfo; import uk.chromis.pos.ticket.TicketLineInfo; public class PromotionSupport { protected Session m_session; protected DataLogicSales m_salesLogic; protected DataLogicPromotions m_DataLogicPromotions; HashMap< String, PromotionInfo> m_promotionCache; List<String> m_AllProductPromotions; private Component m_Parent; public PromotionSupport(Component parent, DataLogicSales salesLogic, DataLogicPromotions logicPromotions) { m_Parent = parent; m_salesLogic = salesLogic; m_DataLogicPromotions = logicPromotions; m_promotionCache = new HashMap(); m_AllProductPromotions = null; } public void clearPromotionCache() { m_promotionCache.clear(); m_AllProductPromotions = null; } // Get promotions that are enabled and operate on all products private List<String> getAllProductPromotionsIds() { if (m_AllProductPromotions == null) { try { m_AllProductPromotions = new ArrayList<String>(); List<PromotionInfo> promos = m_DataLogicPromotions.getAllProductPromotions(); for (PromotionInfo p : promos) { // Add to the cache and store the ID m_AllProductPromotions.add(p.getID()); m_promotionCache.putIfAbsent(p.getID(), p); } } catch (BasicException ex) { Logger.getLogger(PromotionSupport.class.getName()).log(Level.SEVERE, null, ex); } } return m_AllProductPromotions; } private PromotionInfo getCachedPromotion(String id) { PromotionInfo promotion = m_promotionCache.get(id); if (promotion == null) { try { promotion = m_DataLogicPromotions.getPromotionInfo(id); } catch (BasicException ex) { Logger.getLogger(PromotionSupport.class.getName()).log(Level.SEVERE, null, ex); } if (promotion != null) { m_promotionCache.put(id, promotion); } } return promotion; } // Check to see if any promotion scripts need to run // Supported events: // Event // promotion.addline // promotion.changeline // promotion.removeline // public Boolean checkPromotions(String event, Boolean bwithActions, TicketInfo ticket, int selectedindex, int effectedIndex, String productID) throws ScriptException { if (ticket == null || event == null) { return false; } Logger.getLogger(PromotionSupport.class.getName()).log(Level.INFO, "checkPromotions for event: {0}", event); HashMap< String, PromotionInfo> promotions = new HashMap(); // Get product specific promotions for this ticket for (TicketLineInfo line : ticket.getLines()) { String id = line.getPromotionId(); if (id != null) { // Only add if not already in the list if (promotions.get(id) == null) { // Promotion on this product so need to run the promotion script PromotionInfo p = getCachedPromotion(id); if (p != null && p.getIsEnabled()) { promotions.putIfAbsent(id, p); } } } } // Now add promotions that are run for all products List<String> ids = getAllProductPromotionsIds(); for (String id : ids) { PromotionInfo p = getCachedPromotion(id); if (p != null && p.getIsEnabled()) { promotions.putIfAbsent(id, p); } } if (!(promotions.isEmpty()) && bwithActions) { // Promotion scripts need running for (String promotionID : promotions.keySet()) { evalScript(promotions.get(promotionID), event, ticket, selectedindex, effectedIndex, productID); } } return !(promotions.isEmpty()); } public Object evalScript(PromotionInfo promotion, String event, TicketInfo ticket, int selectedindex, int effectedIndex, String productID) throws ScriptException { Logger.getLogger(PromotionSupport.class.getName()).log(Level.INFO, "Running script for: {0}", promotion.getName()); ScriptEngine script = ScriptFactory.getScriptEngine(ScriptFactory.BEANSHELL); // These are the variables available inside the script script.put("ticket", ticket); script.put("dlsales", m_salesLogic); script.put("dlpromotions", m_DataLogicPromotions); script.put("support", this); script.put("promotion", promotion); script.put("event", event); script.put("selectedindex", selectedindex); script.put("effectedindex", effectedIndex); script.put("productid", productID); return script.eval(promotion.getScript()); } // Script support functions // Display a message as a popup dialog public void ShowMessage(String title, String message) { // Get details of the original font before we change it otherwise all dialogboxes will use new settings JOptionPane pane = new JOptionPane(); Font originalFont = pane.getFont(); UIManager.put("OptionPane.buttonFont", new FontUIResource(new Font("ARIAL", Font.PLAIN, 20))); JLabel FontText = new JLabel(message); JOptionPane newpane = new JOptionPane(); newpane.setMessage(FontText); Dialog dlg = newpane.createDialog(title); dlg.setVisible(true); // Return to default settings UIManager.put("OptionPane.buttonFont", new FontUIResource(new Font(originalFont.getName(), originalFont.getStyle(), originalFont.getSize()))); } // Count products in this promotion currently on the ticket public int CountProductsInPromotion(PromotionInfo promotion, TicketInfo ticket, boolean bIncludePromotionAdded) { int count = 0; for (TicketLineInfo line : ticket.getLines()) { if (bIncludePromotionAdded || line.isPromotionAdded() == false) { String id = line.getPromotionId(); if (id != null && id.contentEquals(promotion.getID())) { ++count; } } } return count; } // Calculate the quantity of products in this promotion currently on the ticket public Double CountProductsInPromotionQty(PromotionInfo promotion, TicketInfo ticket, boolean bIncludePromotionAdded) { Double count = 0.0; for (TicketLineInfo line : ticket.getLines()) { if (bIncludePromotionAdded || line.isPromotionAdded() == false) { String id = line.getPromotionId(); if (id != null && id.contentEquals(promotion.getID())) { count += line.getMultiply(); } } } return count; } public class LineList implements Comparable<LineList> { private int m_index; TicketLineInfo m_line; public LineList(int index, TicketLineInfo line) { m_index = index; m_line = line; } public int getIndex() { return m_index; } public void setIndex(int idx) { m_index = idx; } public TicketLineInfo getLine() { return m_line; } public Double getPrice() { return m_line.getPrice(); } public boolean equals(Object o) { if (!(o instanceof LineList)) { return false; } LineList l = (LineList) o; return l.m_index == m_index; } public int hashCode() { return m_index; } public String toString() { return "Index: " + m_index + " Line: " + m_line; } // Compare on price public int compareTo(LineList l) { if (l.getPrice() == getPrice()) { return 0; } int diff = (int) intPart(l.getPrice() - getPrice()); if (l.getPrice() < getPrice()) { return diff + 1; } return diff - 1; } } // Find products in this promotion currently on the ticket // returns an array of indexes into ticket.getLines() public List<LineList> FindProductsInPromotion(PromotionInfo promotion, TicketInfo ticket, boolean bIncludePromotionAdded) { List<LineList> aIndexes = new ArrayList<LineList>(); for (int i = 0; i < ticket.getLinesCount(); ++i) { TicketLineInfo line = ticket.getLines().get(i); if (bIncludePromotionAdded || line.isPromotionAdded() == false) { if (promotion.getAllProducts()) { aIndexes.add(new LineList(i, line)); } else { String id = line.getPromotionId(); if (id != null && id.contentEquals(promotion.getID())) { aIndexes.add(new LineList(i, line)); } } } } return aIndexes; } // Find all instances of the given product currently on the ticket // returns an array of indexes into ticket.getLines() public List<LineList> FindProductsInTicket(String promotionid, String productID, TicketInfo ticket, boolean bIncludePromotionAdded) { List<LineList> aIndexes = new ArrayList<LineList>(); for (int i = 0; i < ticket.getLinesCount(); ++i) { TicketLineInfo line = ticket.getLines().get(i); if (bIncludePromotionAdded || line.isPromotionAdded() == false) { String id = line.getProductID(); if (id != null && id.contentEquals(productID)) { String promoid = line.getPromotionId(); if (promoid != null && promoid.contentEquals(promotionid)) { aIndexes.add(new LineList(i, line)); } } } } return aIndexes; } // Discount all products in this ticket having the given promotion ID public void DiscountProducts(TicketInfo ticket, String promotionid, String sDiscountMessage, Double minQuantity, Double discountrate, Boolean bWithReset) { if (bWithReset) { for (int i = 0; i < ticket.getLines().size(); ++i) { TicketLineInfo line = ticket.getLine(i); String id = line.getPromotionId(); if (id != null) { if (id.contentEquals(promotionid)) { RemovePromotionAddedLine(ticket, i + 1); line.setDiscounted("no"); } } } } // Check quantity to ensure enough products on the ticket to qualify PromotionInfo p = getCachedPromotion(promotionid); if (minQuantity > CountProductsInPromotionQty(p, ticket, false)) { // Insufficient products - remove any discounts RemoveDiscountPromotion(ticket, promotionid); } else { for (int i = 0; i < ticket.getLines().size(); ++i) { TicketLineInfo line = ticket.getLine(i); String id = line.getPromotionId(); if (id != null) { if (id.contentEquals(promotionid)) { DiscountProductPercent(ticket, i, sDiscountMessage, discountrate); } } } } } private static String sYes = new String("yes"); // Discount a product by adding a line below the product public void DiscountProductPercent(TicketInfo ticket, int lineIndex, String sDiscountMessage, Double discountrate) { TicketLineInfo productline = ticket.getLine(lineIndex); if (productline.isPromotionAdded() == false && sYes.contentEquals(productline.getDiscounted()) == false) { double discount = (intPart(productline.getPrice() * discountrate) / -100d); TicketLineInfo discountline = new TicketLineInfo( productline.getProductID(), sDiscountMessage, productline.getProductTaxCategoryID(), productline.getMultiply(), discount, productline.getTaxInfo()); discountline.setPromotionAdded(true); discountline.setProperty("product.promotionid", productline.getPromotionId()); ticket.insertLine(lineIndex + 1, discountline); productline.setDiscounted("yes"); } } // Discount a product by adding a line below the product // If fixedPrice is used to calculate a discount based on the original // price and qty is set to 1. // If not, the qty and discount rate are applied public void DiscountProductQty(TicketInfo ticket, int lineIndex, String sDiscountMessage, Double qty, Double discountrate ) { TicketLineInfo productline = ticket.getLine(lineIndex); if (productline.isPromotionAdded() == false && sYes.contentEquals(productline.getDiscounted()) == false) { Double setPrice; Double setQty; setPrice = productline.getPrice() * (discountrate / 100d) * -1; setQty = qty; TicketLineInfo discountline = new TicketLineInfo( productline.getProductID(), sDiscountMessage, productline.getProductTaxCategoryID(), setQty, setPrice, productline.getTaxInfo()); discountline.setPromotionAdded(true); discountline.setProperty("product.promotionid", productline.getPromotionId()); ticket.insertLine(lineIndex + 1, discountline); productline.setDiscounted("yes"); } } // Discount a group of products by adding a single discount // to the end of the ticket. public void DiscountProductGroup(TicketInfo ticket, PromotionInfo promotion, String sDiscountMessage, Double qty, Double price, TaxInfo tax) { String tcID = null; if (tax != null) { tcID = tax.getTaxCategoryID(); } TicketLineInfo discountline = new TicketLineInfo( null, sDiscountMessage, tcID, qty, price, tax); discountline.setPromotionAdded(true); discountline.setProperty("product.promotionid", promotion.getID()); ticket.addLine(discountline); } // Remove discount on a product // Pass in the id product, all promotion discount lines for that // product anywhere on the ticket will be removed public void RemoveDiscountProduct(TicketInfo ticket, String productID) { if (productID == null) { return; } // Start looking from the end of the ticket to cope with re-indexing // after a line delete int index = ticket.getLines().size() - 1; while (index >= 0) { TicketLineInfo line = ticket.getLine(index); if (!line.isPromotionAdded() && productID.contentEquals(line.getProductID())) { RemovePromotionAddedLine(ticket, index + 1); line.setDiscounted("no"); } --index; } } // Remove a coupon for a promotion // Pass in the id of the promotion, all promotion coupon lines for that // promotion anywhere on the ticket will be removed // If promotionID is null, ALL coupons in the ticket are removed public void RemoveCouponPromotion(TicketInfo ticket, String promotionID) { ticket.removeCoupon(promotionID); } // Remove discount on a promotion // Pass in the id of the promotion, all promotion discount lines for that // promotion anywhere on the ticket will be removed public void RemoveDiscountPromotion(TicketInfo ticket, String promotionID) { if (promotionID == null) { return; } // Start looking from the end of the ticket to cope with re-indexing // after a line delete int index = ticket.getLines().size() - 1; while (index >= 0) { TicketLineInfo line = ticket.getLine(index); String p = line.getPromotionId(); if (p != null && promotionID.contentEquals(p)) { if (line.isPromotionAdded()) { RemovePromotionAddedLine(ticket, index); } else { line.setDiscounted("no"); } } --index; } } // Remove a line added by a promotion on the ticket line at the given index // Pass in the index of the line, subsequent promotion lines // will also be removed public void RemovePromotionAddedLine(TicketInfo ticket, int lineIndex) { int index = lineIndex; TicketLineInfo productline = null; if (index < ticket.getLines().size()) { productline = ticket.getLine(index); } while (productline != null && productline.isPromotionAdded()) { ticket.removeLine(index); if (index < ticket.getLines().size()) { productline = ticket.getLine(index); } else { productline = null; } } } // See if the indexed line is in the given promotion public Boolean isProductInPromotion(TicketInfo ticket, int index, PromotionInfo promotion) { if (promotion == null) { return false; } if (promotion.getAllProducts() == true) { return true; } TicketLineInfo productline = ticket.getLine(index); if (productline != null && promotion != null) { String id = productline.getPromotionId(); if (id != null && promotion.getID().contentEquals(id)) { return true; } } return false; } double intPart(double d) { int i = (int) d; return (double) i; } // Add discounts to the ticket for the products in aLine // The discounts are added as sufficient products are found while // going through the list. If you want cheapest products discounted first, // ensure you call Collections.sort(aLines) first public void addDiscounts(TicketInfo ticket, List<LineList> aLines, String sDiscountMessage, Double qtyBuy, Double qtyToDiscount, Double discountrate, Boolean bWholeUnitsOnly) { // Count the number of items in the ticket Double qty = 0.0; for (LineList l : aLines) { TicketLineInfo productline = ticket.getLine(l.getIndex()); qty += productline.getMultiply(); } if (bWholeUnitsOnly) { qty = intPart(qty); } // Calculate the total number of free items Double qtyFree = qty / qtyBuy; if (bWholeUnitsOnly) { qtyFree = intPart(qtyFree); } qtyFree = qtyFree * qtyToDiscount; if (qtyFree > 0 && discountrate > 0.0 ) { // Step through discounting items until qtyFree is reached Double totalDiscount = 0.0; Double qtyFound = 0.0; for (int i = 0; i < aLines.size(); ++i ) { LineList l = aLines.get(i); int itemIndex = l.getIndex(); Double itemqty = ticket.getLine(itemIndex).getMultiply(); qtyFound += itemqty; Double qtyDiscount = itemqty; if (bWholeUnitsOnly) { qtyDiscount = intPart(qtyDiscount); } if (totalDiscount + qtyDiscount > qtyFree) { qtyDiscount = qtyFree - totalDiscount; } DiscountProductQty(ticket, itemIndex, sDiscountMessage, qtyDiscount, 100d ); // A new line is added to the ticket so re-index ones after for( int j = 0; j< aLines.size(); ++j ) { int index = aLines.get(j).getIndex(); if(index > itemIndex) { aLines.get(j).setIndex( index+1 ); } } totalDiscount += qtyDiscount; qtyFound += qtyDiscount; if (totalDiscount >= qtyFree) { break; } } } } // Add discounts to the ticket for the products in aLine // The discounts are added as sufficient products are found while // going through the list. If you want cheapest products discounted first, // ensure you call Collections.sort(aLines) first public void addDiscountFixedPrice(TicketInfo ticket, PromotionInfo promotion, List<LineList> aLines, String sDiscountMessage, Double qtyBuy, Double fixedPrice, Boolean bWholeUnitsOnly) { // Count the number of items in the ticket Double qty = 0.0; for (LineList l : aLines) { TicketLineInfo productline = ticket.getLine(l.getIndex()); qty += productline.getMultiply(); } if (bWholeUnitsOnly) { qty = intPart(qty); } // Calculate the total number of free items Double qtyFree = 0d; while( qtyFree + qtyBuy <= qty ){ qtyFree = qtyFree + qtyBuy; } if (bWholeUnitsOnly) { qtyFree = intPart(qtyFree); } // Tax is chargable at the highest item tax rate so need to // keep track of that. TaxInfo tax = null; if (qtyFree > 0) { // Step through discounting items until qtyFree is reached Double totalDiscount = 0.0; Double qtyFound = 0.0; for (int i = 0; i < aLines.size(); ++i ) { LineList l = aLines.get(i); int itemIndex = l.getIndex(); Double itemqty = ticket.getLine(itemIndex).getMultiply(); qtyFound += itemqty; Double qtyDiscount = itemqty; if (bWholeUnitsOnly) { qtyDiscount = intPart(qtyDiscount); } if (totalDiscount + qtyDiscount > qtyFree) { qtyDiscount = qtyFree - totalDiscount; } DiscountProductQty(ticket, itemIndex, sDiscountMessage, qtyDiscount, 100d ); // A new line is added to the ticket so re-index ones after for( int j = 0; j< aLines.size(); ++j ) { int index = aLines.get(j).getIndex(); if(index > itemIndex) { aLines.get(j).setIndex( index+1 ); } } totalDiscount += qtyDiscount; qtyFound += qtyDiscount; TicketLineInfo info = ticket.getLine(itemIndex); // Save the highest tax rate - we must charge that rate // on the final price (UK tax laws) if (tax == null || tax.getRate() < info.getTaxInfo().getRate()) { tax = info.getTaxInfo(); } if (totalDiscount >= qtyFree) { break; } } // We have now discounted all products involved the deal to zero, // Now add a line to the end of the ticket for the // actual price, use the highest tax rate we found fixedPrice = fixedPrice / (1+tax.getRate()); DiscountProductGroup(ticket, promotion, sDiscountMessage, qtyFree/qtyBuy, fixedPrice, tax); } } // For every forEvery products you buy, you get qtyDiscounted products discounted // This version is for when any product in the promotion can be used in the // calculation, i.e. mix and match from a range. // bWholeUnitsOnly should be true to calculate the discounts on whole units // only or false to calculate the discounts on partial products such as weighed // items. public void UpdateMutibuyGroup( TicketInfo ticket, PromotionInfo promotion, String sDiscountMessage, Double forEvery, Double qtyDiscounted, Double discountrate, Boolean bWholeUnitsOnly) { // Remove existing discount RemoveDiscountPromotion(ticket, promotion.getID()); // Get list of products in this promotion List<LineList> aLines = FindProductsInPromotion(promotion, ticket, false); if (aLines.size() > 0) { // Convert to a price ordered list so cheapest items discounted first Collections.sort(aLines); addDiscounts(ticket, aLines, sDiscountMessage, forEvery, qtyDiscounted, discountrate, bWholeUnitsOnly); } } // For every forEvery products you buy, you get qtyDiscounted products discounted // This version is for when all products must be the same. // bWholeUnitsOnly should be true to calculate the discounts on whole units // only or false to calculate the discounts on partial products such as weighed // items. public void UpdateMutibuySingleProduct( TicketInfo ticket, String productID, PromotionInfo promotion, String sDiscountMessage, Double forEvery, Double qtyDiscounted, Double discountrate, Boolean bWholeUnitsOnly) { // Remove existing discount RemoveDiscountProduct(ticket, productID); // Get list of products in this ticket List<LineList> aLines = FindProductsInTicket( promotion.getID(), productID, ticket, false); if (aLines.size() > 0) { addDiscounts(ticket, aLines, sDiscountMessage, forEvery, qtyDiscounted, discountrate, bWholeUnitsOnly); } } // For every qtyBuy products you buy, you get qtySomeFree products free // This version is for when all products must be the same. // bWholeUnitsOnly should be true to calculate the discounts on whole units // only or false to calculate the discounts on partial products such as weighed // items. public void UpdateBuySomeGetSomeFreeSingleProduct( TicketInfo ticket, String productID, PromotionInfo promotion, String sDiscountMessage, Double qtyBuy, Double qtySomeFree, Boolean bWholeUnitsOnly) { // Remove existing discount RemoveDiscountProduct(ticket, productID); // Get list of products in this ticket List<LineList> aLines = FindProductsInTicket( promotion.getID(), productID, ticket, false); if (aLines.size() > 0) { addDiscounts(ticket, aLines, sDiscountMessage, qtyBuy, qtySomeFree, 100d, bWholeUnitsOnly); } } // For every qtyBuy products you buy, you get qtySomeFree products free // This version is for when any product in the promotion can be used in the // calculation, i.e. mix and match from a range. // bWholeUnitsOnly should be true to calculate the discounts on whole units // only or false to calculate the discounts on partial products such as weighed // items. public void UpdateBuySomeGetSomeGroup( TicketInfo ticket, PromotionInfo promotion, String sDiscountMessage, Double qtyBuy, Double qtySomeFree, Boolean bWholeUnitsOnly) { // Remove existing discount RemoveDiscountPromotion(ticket, promotion.getID()); // Get list of products in this promotion List<LineList> aLines = FindProductsInPromotion(promotion, ticket, false); if (aLines.size() > 0) { // Convert to a price ordered list so cheapest items discounted first Collections.sort(aLines); addDiscounts(ticket, aLines, sDiscountMessage, qtyBuy, qtySomeFree, 100d, bWholeUnitsOnly); } } // Buy qtyBuy products at the given price // This version is for when all products must be the same. // bWholeUnitsOnly should be true to calculate the discounts on whole units // only or false to calculate the discounts on partial products such as weighed // items. public void UpdateFixedPriceSingleProduct( TicketInfo ticket, String productID, PromotionInfo promotion, String sDiscountMessage, Double qtyBuy, Double price, Boolean bWholeUnitsOnly) { // Remove existing discount RemoveDiscountProduct(ticket, productID); // Get list of products in this ticket List<LineList> aLines = FindProductsInTicket( promotion.getID(), productID, ticket, false); if (aLines.size() > 0) { addDiscountFixedPrice(ticket, promotion, aLines, sDiscountMessage, qtyBuy, price, bWholeUnitsOnly); } } // Buy qtyBuy products at the given price // This version is for when any product in the promotion can be used in the // calculation, i.e. mix and match from a range. // bWholeUnitsOnly should be true to calculate the discounts on whole units // only or false to calculate the discounts on partial products such as weighed // items. public void UpdateFixedPriceGroup( TicketInfo ticket, PromotionInfo promotion, String sDiscountMessage, Double qtyBuy, Double price, Boolean bWholeUnitsOnly) { // Remove existing discount RemoveDiscountPromotion(ticket, promotion.getID()); // Get list of products in this promotion List<LineList> aLines = FindProductsInPromotion(promotion, ticket, false); if (aLines.size() > 0) { // Convert to a price ordered list so cheapest items discounted first Collections.sort(aLines); // Give discount on products qualifying addDiscountFixedPrice(ticket, promotion, aLines, sDiscountMessage, qtyBuy, price, bWholeUnitsOnly); } } // Buy two or more different products at a fixed price // An array or properties is passed in, at least one product with each // property must be in the ticket to qualify. i.e // List<String> properties = new String [] = {"meal.snack", "meal.drink", "meal.sandwich"}; // // public void UpdateMealDeal( TicketInfo ticket, PromotionInfo promotion, String sDiscountMessage, List<String> properties, String productKeyName, Double price) { // Remove existing discount RemoveDiscountPromotion(ticket, promotion.getID()); // Get list of products in this promotion List<LineList> aLines = FindProductsInPromotion(promotion, ticket, false); // Lowest price items first Collections.sort(aLines); if (aLines.size() > 0) { // Count how many products of each type in the ticket Double[] count = new Double[properties.size()]; for (int i = 0; i < properties.size(); ++i) { count[i] = 0d; } // Go through products counting how many of each type for (LineList l : aLines) { // Get the product property and compare it with the ones we are looking for String prop = l.getLine().getProperty(productKeyName); if (prop != null) { for (int j = 0; j < properties.size(); ++j) { if (prop.contentEquals(properties.get(j))) { // Found a match - count it count[j] += l.getLine().getMultiply(); } } } } // Number of discounts to give is the smallest number of the product // type counters Double smallest = -1d; for (int j = 0; j < properties.size(); ++j) { if (smallest == -1 || count[j] < smallest) { smallest = intPart(count[j]); } } if (smallest > 0) { // So now discount the correct number of each type of product // starting at the highest value items for (int j = 0; j < properties.size(); ++j) { count[j] = smallest; } // Tax is chargable at the highest item tax rate so need to // keep track of that. TaxInfo tax = null; int itemIndex = 0; // Go through products discounting each type in turn for (int j = 0; j < properties.size(); ++j) { itemIndex = aLines.size() - 1; // Start at the end (highest value) while (itemIndex >= 0 && count[j] > 0) { int ticketIndex = aLines.get(itemIndex).getIndex(); TicketLineInfo info = ticket.getLine(ticketIndex); String prop = info.getProperty(productKeyName); if (!info.isPromotionAdded() && prop != null) { if (prop.contentEquals(properties.get(j))) { // Discount this item Double qty = count[j]; if (qty > info.getMultiply()) { qty = info.getMultiply(); } count[j] -= qty; DiscountProductQty(ticket, ticketIndex, sDiscountMessage, qty, 100d ); // ticketindexes need adjusting to accomodate the new line for (int l = 0; l < aLines.size(); ++l) { if (aLines.get(l).getIndex() > ticketIndex) { aLines.get(l).setIndex(aLines.get(l).getIndex() + 1); } } // Save the highest tax rate - we must charge that rate // on the final price (UK tax laws) if (tax == null || tax.getRate() < info.getTaxInfo().getRate()) { tax = info.getTaxInfo(); } } } --itemIndex; } } // We have now discounted all products involved the deal to zero, // Now add a line to the end of the ticket for the meal deal's // actual price, use the highest tax rate we found DiscountProductGroup(ticket, promotion, sDiscountMessage, smallest, price, tax); } } } public void AddCoupon( TicketInfo ticket, PromotionInfo promotion, List<String> couponLines) { int line = 0; for (String text : couponLines) { ticket.addCouponLine(promotion.getID(), line++, text); } } // To debug scripts in a proper debugging environment, // cut and paste the entire script into the function below, removing // any existing function code first, then edit your script in the // database using the resource editor to change DEBUGMODE to true // The script will now divert to the function below. // Note that DEBUGMODE needs to be set to false in the function below. // // When you have it all working, you can cut and paste your code back into // the script. Make sure you remove the code from the function below and // that DEBUGMODE is false in the resource script. public void testcode(PromotionInfo promotion, String event, TicketInfo ticket, int selectedindex, DataLogicSales dlsales, DataLogicPromotions dlpromotions, PromotionSupport support, int effectedindex, String productid) { } }