package service; import Common; import tink.core.Error; /** * Order Service * @author web-wizard,fbarbut */ class OrderService { static function canHaveFloatQt(product:db.Product):Bool { return product.hasFloatQt || product.wholesale || product.variablePrice; } /** * Make a product Order * * @param quantity * @param productId */ public static function make(user:db.User, quantity:Float, product:db.Product, ?distribId:Int, ?paid:Bool, ?user2:db.User, ?invert:Bool):db.UserContract { var t = sugoi.i18n.Locale.texts; if (product.contract.type == db.Contract.TYPE_VARORDER && distribId == null) throw "You have to provide a distribId"; if (quantity == null) throw "Quantity is null"; // quantity if (!canHaveFloatQt(product)) { if (!tools.FloatTool.isInt(quantity)) { throw new tink.core.Error(t._("Error : product \"::product::\" quantity should be integer", {product: product.name})); } } // multiweight : make one row per product if (product.multiWeight && quantity > 1.0) { if (product.multiWeight && quantity != Math.abs(quantity)) throw t._("multi-weighing products should be ordered only with integer quantities"); var o = null; for (i in 0...Math.round(quantity)) { o = make(user, 1, product, distribId, paid, user2, invert); } return o; } // checks if (quantity <= 0) return null; // check for previous orders on the same distrib var prevOrders = new List(); if (distribId == null) { prevOrders = db.UserContract.manager.search($product == product && $user == user, true); } else { prevOrders = db.UserContract.manager.search($product == product && $user == user && $distributionId == distribId, true); } // Create order object var o = new db.UserContract(); o.product = product; o.quantity = quantity; o.productPrice = product.price; if (product.contract.hasPercentageOnOrders()) { o.feesRate = product.contract.percentageValue; } o.user = user; if (user2 != null) { o.user2 = user2; if (invert != null) o.flags.set(InvertSharedOrder); } if (paid != null) o.paid = paid; if (distribId != null) o.distribution = db.Distribution.manager.get(distribId); // cumulate quantities if there is a similar previous order if (prevOrders.length > 0 && !product.multiWeight) { for (prevOrder in prevOrders) { // if (!prevOrder.paid) { o.quantity += prevOrder.quantity; prevOrder.delete(); // } } } // create a basket object if (distribId != null) { var dist = o.distribution; var basket = db.Basket.getOrCreate(user, dist.place, dist.date); o.basket = basket; } o.insert(); // Stocks if (o.product.stock != null) { var c = o.product.contract; if (c.hasStockManagement()) { // trace("stock for "+quantity+" x "+product.name); if (o.product.stock == 0) { if (App.current.session != null) { App.current.session.addMessage(t._("There is no more '::productName::' in stock, we removed it from your order", {productName: o.product.name}), true); } o.quantity -= quantity; if (o.quantity <= 0) { o.delete(); return null; } } else if (o.product.stock - quantity < 0) { var canceled = quantity - o.product.stock; o.quantity -= canceled; o.update(); if (App.current.session != null) { var msg = t._("We reduced your order of '::productName::' to quantity ::oQuantity:: because there is no available products anymore", {productName: o.product.name, oQuantity: o.quantity}); App.current.session.addMessage(msg, true); } o.product.lock(); o.product.stock = 0; o.product.update(); App.current.event(StockMove({product: o.product, move: 0 - (quantity - canceled)})); } else { o.product.lock(); o.product.stock -= quantity; o.product.update(); App.current.event(StockMove({product: o.product, move: 0 - quantity})); } } } return o; } /** * Edit an existing order (quantity) */ public static function edit(order:db.UserContract, newquantity:Float, ?paid:Bool, ?user2:db.User, ?invert:Bool) { var t = sugoi.i18n.Locale.texts; order.lock(); // quantity if (newquantity == null) newquantity = 0; if (!canHaveFloatQt(order.product)) { if (!tools.FloatTool.isInt(newquantity)) { throw new tink.core.Error(t._("Error : product \"::product::\" quantity should be integer", {product: order.product.name})); } } // paid if (paid != null) { order.paid = paid; } else { if (order.quantity < newquantity) order.paid = false; } // shared order if (user2 != null) { order.user2 = user2; if (invert == true) order.flags.set(InvertSharedOrder); if (invert == false) order.flags.unset(InvertSharedOrder); } else { order.user2 = null; order.flags.unset(InvertSharedOrder); } // stocks var e:Event = null; if (order.product.stock != null) { var c = order.product.contract; if (c.hasStockManagement()) { if (newquantity < order.quantity) { // on commande moins que prévu : incrément de stock order.product.lock(); order.product.stock += (order.quantity - newquantity); e = StockMove({product: order.product, move: 0 - (order.quantity - newquantity)}); } else { // on commande plus que prévu : décrément de stock var addedquantity = newquantity - order.quantity; if (order.product.stock - addedquantity < 0) { // stock is not enough, reduce order newquantity = order.quantity + order.product.stock; if (App.current.session != null) App.current.session.addMessage(t._("We reduced your order of '::productName::' to quantity ::oQuantity:: because there is no available products anymore", { productName: order.product.name, oQuantity: newquantity }), true); e = StockMove({product: order.product, move: 0 - order.product.stock}); order.product.lock(); order.product.stock = 0; } else { // stock is big enough order.product.lock(); order.product.stock -= addedquantity; e = StockMove({product: order.product, move: 0 - addedquantity}); } } order.product.update(); } } // update order if (newquantity == 0) { order.quantity = 0; order.paid = true; order.update(); } else { order.quantity = newquantity; order.update(); } App.current.event(e); return order; } /** * Delete an order */ public static function delete(order:db.UserContract) { var t = sugoi.i18n.Locale.texts; if (order == null) throw new Error(t._("This order has already been deleted.")); order.lock(); if (order.quantity == 0) { var contract = order.product.contract; var user = order.user; // Amap Contract if (contract.type == db.Contract.TYPE_CONSTORDERS) { order.delete(); if (contract.amap.hasPayments()) { var orders = contract.getUserOrders(user); if (orders.length == 0) { var operation = db.Operation.findCOrderTransactionFor(contract, user); if (operation != null) operation.delete(); } } } else { // Variable orders contract // Get the basket for this user var place = order.distribution.place; var basket = db.Basket.get(user, place, order.distribution.date); if (contract.amap.hasPayments()) { var orders = basket.getOrders(); // Check if it is the last order, if yes then delete the related operation if (orders.length == 1 && orders.first().id == order.id) { var operation = db.Operation.findVOrderTransactionFor(order.distribution.getKey(), user, place.amap); if (operation != null) operation.delete(); } } order.delete(); } } else { throw new Error(t._("Deletion not possible: quantity is not zero.")); } } /** * Prepare a simple dataset, ready to be displayed */ public static function prepare(orders:Iterable):Array { var out = new Array(); var orders = Lambda.array(orders); var view = App.current.view; var t = sugoi.i18n.Locale.texts; for (o in orders) { var x:UserOrder = cast {}; x.id = o.id; x.userId = o.user.id; x.userName = o.user.getCoupleName(); x.userEmail = o.user.email; // shared order if (o.user2 != null) { x.userId2 = o.user2.id; x.userName2 = o.user2.getCoupleName(); x.userEmail2 = o.user2.email; } // deprecated x.productId = o.product.id; x.productRef = o.product.ref; x.productQt = o.product.qt; x.productUnit = o.product.unitType; x.productPrice = o.productPrice; x.productImage = o.product.getImage(); x.productHasFloatQt = o.product.hasFloatQt; x.productHasVariablePrice = o.product.variablePrice; // new way x.product = o.product.infos(); x.product.price = o.productPrice; // do not use current price, but price of the order x.quantity = o.quantity; // smartQt if (x.quantity == 0.0) { x.smartQt = t._("Canceled"); } else if (x.productHasFloatQt || x.productHasVariablePrice || o.product.wholesale) { x.smartQt = view.smartQt(x.quantity, x.productQt, x.productUnit); } else { x.smartQt = Std.string(x.quantity); } // product name. if (x.productHasVariablePrice || x.productQt == null || x.productUnit == null) { x.productName = o.product.name; } else { x.productName = o.product.name + " " + view.formatNum(x.productQt) + " " + view.unit(x.productUnit, x.productQt > 1); } x.subTotal = o.quantity * o.productPrice; var c = o.product.contract; if (o.feesRate != 0) { x.fees = x.subTotal * (o.feesRate / 100); x.percentageName = c.percentageName; x.percentageValue = o.feesRate; x.total = x.subTotal + x.fees; } else { x.total = x.subTotal; } // flags x.paid = o.paid; x.invertSharedOrder = o.flags.has(InvertSharedOrder); x.contractId = c.id; x.contractName = c.name; x.canModify = o.canModify(); out.push(x); } return sort(out); } /** * Confirms an order : create real orders from tmp orders in session * @param order */ public static function confirmSessionOrder(tmpOrder:OrderInSession) { var orders = []; var user = db.User.manager.get(tmpOrder.userId); for (o in tmpOrder.products) { o.product = db.Product.manager.get(o.productId); orders.push(make(user, o.quantity, o.product, o.distributionId)); } App.current.event(MakeOrder(orders)); App.current.session.data.order = null; return orders; } /** * Send an order-by-products report to the coordinator */ public static function sendOrdersByProductReport(d:db.Distribution) { var t = sugoi.i18n.Locale.texts; var m = new sugoi.mail.Mail(); m.addRecipient(d.contract.contact.email, d.contract.contact.getName()); m.setSender(App.config.get("default_email"), t._("Cagette.net")); m.setSubject('[${d.contract.amap.name}] Distribution du ${App.current.view.dDate(d.date)} (${d.contract.name})'); var orders = service.ReportService.getOrdersByProduct(d); var html = App.current.processTemplate("mail/ordersByProduct.mtt", { contract: d.contract, distribution: d, orders: orders, formatNum: App.current.view.formatNum, currency: App.current.view.currency, dDate: App.current.view.dDate, hHour: App.current.view.hHour, group: d.contract.amap }); m.setHtmlBody(html); App.sendMail(m); } /** * Order summary for a member * WARNING : its for one distrib, not for a whole basket ! */ public static function sendOrderSummaryToMembers(d:db.Distribution) { var t = sugoi.i18n.Locale.texts; var title = '[${d.contract.amap.name}] Votre commande pour le ${App.current.view.dDate(d.date)} (${d.contract.name})'; for (user in d.getUsers()) { var m = new sugoi.mail.Mail(); m.addRecipient(user.email, user.getName(), user.id); if (user.email2 != null) m.addRecipient(user.email2, user.getName(), user.id); m.setSender(App.config.get("default_email"), t._("Cagette.net")); m.setSubject(title); var orders = prepare(d.contract.getUserOrders(user, d)); var html = App.current.processTemplate("mail/orderSummaryForMember.mtt", { contract: d.contract, distribution: d, orders: orders, formatNum: App.current.view.formatNum, currency: App.current.view.currency, dDate: App.current.view.dDate, hHour: App.current.view.hHour, group: d.contract.amap }); m.setHtmlBody(html); App.sendMail(m); } } public static function sort(orders:Array) { // order by lastname (+lastname2 if exists), then contract orders.sort(function(a, b) { if (a.userName + a.userId + a.userName2 + a.userId2 + a.contractId > b.userName + b.userId + b.userName2 + b.userId2 + b.contractId) { return 1; } if (a.userName + a.userId + a.userName2 + a.userId2 + a.contractId < b.userName + b.userId + b.userName2 + b.userId2 + b.contractId) { return -1; } return 0; }); return orders; } }