code from amapei

This commit is contained in:
cagette@ct8
2020-09-26 18:25:18 +00:00
parent 42fe367911
commit 72b4699871
1966 changed files with 220437 additions and 2 deletions
+423
View File
@@ -0,0 +1,423 @@
package service;
import Common;
/**
* Distribution Service
* @author web-wizard
*/
class DistributionService
{
/**
* It will update the name of the operation with the new number of distributions
* as well as the total amount
* @param contract -
*/
public static function updateAmapContractOperations(contract:db.Contract) {
//Update all operations for this amap contract when payments are enabled
if (contract.type == db.Contract.TYPE_CONSTORDERS && contract.amap.hasPayments()) {
//Get all the users who have orders for this contract
var users = contract.getUsers();
for ( user in users ){
//Get the one operation for this amap contract and user
var operation = db.Operation.findCOrderTransactionFor(contract, user);
if (operation != null)
{
//Get all the orders for this contract and user
var orders = contract.getUserOrders(user);
//Update this operation with the new number of distributions, this will affect the name of the operation
//as well as the total amount to pay
db.Operation.updateOrderOperation(operation, orders);
}
}
}
}
/**
* checks if dates are correct and if that there is no other distribution in the same time range
* and for the same contract and place
* @param d
*/
public static function checkDistrib(d:db.Distribution) {
//Generic variables
var t = sugoi.i18n.Locale.texts;
var view = App.current.view;
var c = d.contract;
var distribs1;
var distribs2;
var distribs3;
//We are checking that there is no existing distribution with an overlapping time frame for the same place and contract
if (d.id == null) { //We need to check there the id as $id != null doesn't work in the manager.search
//Looking for existing distributions with a time range overlapping the start of the about to be created distribution
distribs1 = db.Distribution.manager.search($contract == c && $place == d.place && $date <= d.date && $end >= d.date, false);
//Looking for existing distributions with a time range overlapping the end of the about to be created distribution
distribs2 = db.Distribution.manager.search($contract == c && $place == d.place && $date <= d.end && $end >= d.end, false);
//Looking for existing distributions with a time range included in the time range of the about to be created distribution
distribs3 = db.Distribution.manager.search($contract == c && $place == d.place && $date >= d.date && $end <= d.end, false);
}
else {
//Looking for existing distributions with a time range overlapping the start of the about to be created distribution
distribs1 = db.Distribution.manager.search($contract == c && $place == d.place && $date <= d.date && $end >= d.date && $id != d.id, false);
//Looking for existing distributions with a time range overlapping the end of the about to be created distribution
distribs2 = db.Distribution.manager.search($contract == c && $place == d.place && $date <= d.end && $end >= d.end && $id != d.id, false);
//Looking for existing distributions with a time range included in the time range of the about to be created distribution
distribs3 = db.Distribution.manager.search($contract == c && $place == d.place && $date >= d.date && $end <= d.end && $id != d.id, false);
}
if (distribs1.length != 0 || distribs2.length != 0 || distribs3.length != 0) {
throw new tink.core.Error(t._("There is already a distribution at this place overlapping with the time range you've selected."));
}
if (d.date.getTime() > c.endDate.getTime()) throw new tink.core.Error(t._("The date of the delivery must be prior to the end of the contract (::contractEndDate::)", {contractEndDate:view.hDate(c.endDate)}));
if (d.date.getTime() < c.startDate.getTime()) throw new tink.core.Error(t._("The date of the delivery must be after the begining of the contract (::contractBeginDate::)", {contractBeginDate:view.hDate(c.startDate)}));
if (c.type == db.Contract.TYPE_VARORDER ) {
if (d.date.getTime() < d.orderEndDate.getTime() ) throw new tink.core.Error(t._("The distribution start date must be set after the orders end date."));
if (d.orderStartDate.getTime() > d.orderEndDate.getTime() ) throw new tink.core.Error(t._("The orders end date must be set after the orders start date !"));
}
}
/**
* Creates a new distribution and prevents distribution overlapping and other checks
* @param contract -
* @param date -
* @param end -
* @param placeId -
* @param distributor1Id -
* @param distributor2Id -
* @param distributor3Id -
* @param distributor4Id -
* @param orderStartDate -
* @param orderEndDate -
* @param distributionCycle -
* @param dispatchEvent=true -
* @return db.Distribution
*/
public static function create(contract:db.Contract,date:Date,end:Date,placeId:Int,
?distributor1Id:Int,?distributor2Id:Int,?distributor3Id:Int,?distributor4Id:Int,
?orderStartDate:Date,?orderEndDate:Date,?distributionCycle:db.DistributionCycle,?dispatchEvent=true):db.Distribution {
var d = new db.Distribution();
d.contract = contract;
d.date = date;
d.place = db.Place.manager.get(placeId);
d.distributionCycle = distributionCycle;
if(distributor1Id != null) d.distributor1 = db.User.manager.get(distributor1Id);
if(distributor2Id != null) d.distributor2 = db.User.manager.get(distributor2Id);
if(distributor3Id != null) d.distributor3 = db.User.manager.get(distributor3Id);
if(distributor4Id != null) d.distributor4 = db.User.manager.get(distributor4Id);
if(contract.type==db.Contract.TYPE_VARORDER){
d.orderStartDate = orderStartDate;
d.orderEndDate = orderEndDate;
}
if (end == null) {
d.end = DateTools.delta(d.date, 1000.0 * 60 * 60);
}
else {
d.end = new Date(d.date.getFullYear(), d.date.getMonth(), d.date.getDate(), end.getHours(), end.getMinutes(), 0);
}
DistributionService.checkDistrib(d);
if(distributionCycle == null && dispatchEvent) {
var e :Event = NewDistrib(d);
App.current.event(e);
}
if (d.date == null){
return d;
} else {
d.insert();
//In case this is a distrib for an amap contract with payments enabled, it will update all the operations
//names and amounts with the new number of distribs
updateAmapContractOperations(d.contract);
return d;
}
}
/**
* Modifies an existing distribution and prevents distribution overlapping and other checks
* @param d -
* @param date -
* @param end -
* @param placeId -
* @param distributor1Id -
* @param distributor2Id -
* @param distributor3Id -
* @param distributor4Id -
* @param orderStartDate -
* @param orderEndDate -
* @return db.Distribution
*/
public static function edit(d:db.Distribution,date:Date,end:Date,placeId:Int,
distributor1Id:Int,distributor2Id:Int,distributor3Id:Int,distributor4Id:Int,
orderStartDate:Date,orderEndDate:Date,?dispatchEvent=true):db.Distribution {
//We prevent others from modifying it
d.lock();
d.date = date;
d.place = db.Place.manager.get(placeId);
d.distributor1 = db.User.manager.get(distributor1Id);
d.distributor2 = db.User.manager.get(distributor2Id);
d.distributor3 = db.User.manager.get(distributor3Id);
d.distributor4 = db.User.manager.get(distributor4Id);
if(d.contract.type==db.Contract.TYPE_VARORDER){
d.orderStartDate = orderStartDate;
d.orderEndDate = orderEndDate;
}
if (end == null) {
d.end = DateTools.delta(d.date, 1000.0 * 60 * 60);
}
else {
d.end = new Date(d.date.getFullYear(), d.date.getMonth(), d.date.getDate(), end.getHours(), end.getMinutes(), 0);
}
DistributionService.checkDistrib(d);
if(dispatchEvent) App.current.event(EditDistrib(d));
if (d.date == null){
return d;
} else {
d.update();
return d;
}
}
/**
* Checks whether there are orders with non zero quantity for non amap contract
* @param d -
* @return Bool
*/
public static function canDelete(d:db.Distribution):Bool{
if (d.contract.type == db.Contract.TYPE_CONSTORDERS) return true;
var quantity = 0.0;
for ( order in d.getOrders() ){
quantity += order.quantity;
}
return quantity == 0.0;
}
/**
* Deletes a distribution
* @param d -
* @param dispatchEvent=true -
*/
public static function delete(d:db.Distribution,?dispatchEvent=true) {
var t = sugoi.i18n.Locale.texts;
if ( !canDelete(d) ) {
throw new tink.core.Error(t._("Deletion non possible: some orders are saved for this delivery."));
}
var contract = d.contract;
d.lock();
if (dispatchEvent) {
App.current.event(DeleteDistrib(d));
}
d.delete();
//In case this is a distrib for an amap contract with payments enabled, it will update all the operations
//names and amounts with the new number of distribs
updateAmapContractOperations(contract);
}
/**
* Computes the correct start and end dates
* @param dc -
* @param datePointer -
*/
public static function getDates(dc:db.DistributionCycle, datePointer:Date) {
//Generic variables
var t = sugoi.i18n.Locale.texts;
var startDate = new Date(datePointer.getFullYear(),datePointer.getMonth(),datePointer.getDate(),dc.startHour.getHours(),dc.startHour.getMinutes(),0);
var orderStartDate = null;
var orderEndDate = null;
if (dc.contract.type == db.Contract.TYPE_VARORDER){
if (dc.daysBeforeOrderEnd == null || dc.daysBeforeOrderStart == null) throw new tink.core.Error(t._("daysBeforeOrderEnd or daysBeforeOrderStart is null"));
var a = DateTools.delta(startDate, -1.0 * dc.daysBeforeOrderStart * 1000 * 60 * 60 * 24);
var h : Date = dc.openingHour;
orderStartDate = new Date(a.getFullYear(), a.getMonth(), a.getDate(), h.getHours(), h.getMinutes(), 0);
var a = DateTools.delta(startDate, -1.0 * dc.daysBeforeOrderEnd * 1000 * 60 * 60 * 24);
var h : Date = dc.closingHour;
orderEndDate = new Date(a.getFullYear(), a.getMonth(), a.getDate(), h.getHours(), h.getMinutes(), 0);
}
return { date: startDate, orderStartDate: orderStartDate, orderEndDate: orderEndDate };
}
/**
* Creates all the distributions from the first date
* @param dc -
*/
public static function createCycleDistribs(dc:db.DistributionCycle) {
//Generic variables
var t = sugoi.i18n.Locale.texts;
//switch end date to 23:59 to avoid the last distribution to be skipped
dc.endDate = tools.DateTool.setHourMinute(dc.endDate,23,59);
if (dc.id == null) throw new tink.core.Error(t._("this distributionCycle has not been recorded"));
//iterations
//For first distrib
var datePointer = new Date(dc.startDate.getFullYear(), dc.startDate.getMonth(), dc.startDate.getDate(), 12, 0, 0);
//why hour=12 ? because if we set hour to 0, it switch to 23 (-1) or 1 (+1) on daylight saving time switch dates, thus changing the day!!
var firstDistribDate = new Date(datePointer.getFullYear(),datePointer.getMonth(),datePointer.getDate(),dc.startHour.getHours(),dc.startHour.getMinutes(),0);
for(i in 0...100) {
if(i != 0){ //All distribs except the first one
var oneDay = 1000 * 60 * 60 * 24.0;
switch(dc.cycleType) {
case Weekly :
datePointer = DateTools.delta(datePointer, oneDay * 7.0);
App.log("on ajoute "+(oneDay * 7.0)+"millisec pour ajouter 7 jours");
App.log('pointer : $datePointer');
case BiWeekly :
datePointer = DateTools.delta(datePointer, oneDay * 14.0);
case TriWeekly :
datePointer = DateTools.delta(datePointer, oneDay * 21.0);
case Monthly :
var n = tools.DateTool.getWhichNthDayOfMonth(firstDistribDate);
var dayOfWeek = firstDistribDate.getDay();
var nextMonth = new Date(datePointer.getFullYear(), datePointer.getMonth() + 1, 1, 0, 0, 0);
datePointer = tools.DateTool.getNthDayOfMonth(nextMonth.getFullYear(), nextMonth.getMonth(), dayOfWeek, n);
if (datePointer.getMonth() != nextMonth.getMonth()) {
datePointer = tools.DateTool.getNthDayOfMonth(nextMonth.getFullYear(), nextMonth.getMonth(), dayOfWeek, n - 1);
}
}
}
//stop if cycle end is reached
if (datePointer.getTime() > dc.endDate.getTime()) {
break;
}
var dates = getDates(dc, datePointer);
service.DistributionService.create(dc.contract,dates.date,
new Date(datePointer.getFullYear(),datePointer.getMonth(),datePointer.getDate(),dc.endHour.getHours(),dc.endHour.getMinutes(),0),
dc.place.id,null,null,null,null,dates.orderStartDate,dates.orderEndDate,dc);
}
}
/**
* Deletes all distributions which are part of this cycle
* @param cycle -
*/
public static function deleteCycleDistribs(cycle:db.DistributionCycle){
cycle.lock();
//Generic variables
var t = sugoi.i18n.Locale.texts;
var view = App.current.view;
var children = db.Distribution.manager.search($distributionCycle == cycle, true);
var messages = [];
if(children.length != 0) {
var contract = Lambda.array(children)[0].contract;
for ( d in children ){
if (d.contract.type == db.Contract.TYPE_VARORDER && !canDelete(d) ){
messages.push(t._("The delivery of the ::delivDate:: could not be deleted because it has orders.", {delivDate:view.hDate(d.date)}));
}else{
d.delete();
}
}
//In case this is a distrib cycle for an amap contract with payments enabled, it will update all the operations
//names and amounts with the new number of distribs
updateAmapContractOperations(contract);
}
cycle.delete();
return messages;
}
/**
* Creates a new distribution cycle and prevents distribution overlapping and other checks
* @param contract -
* @param cycleType -
* @param startDate -
* @param endDate -
* @param startHour -
* @param endHour -
* @param daysBeforeOrderStart -
* @param daysBeforeOrderEnd -
* @param openingHour -
* @param closingHour -
* @param placeId -
* @param dispatchEvent=true -
* @return db.DistributionCycle
*/
public static function createCycle(contract:db.Contract,cycleType:db.DistributionCycle.CycleType,startDate:Date,endDate:Date,
startHour:Date,endHour:Date,daysBeforeOrderStart:Null<Int>,daysBeforeOrderEnd:Null<Int>,openingHour:Null<Date>,closingHour:Null<Date>,
placeId:Int,?dispatchEvent=true):db.DistributionCycle {
//Generic variables
var t = sugoi.i18n.Locale.texts;
var view = App.current.view;
var dc = new db.DistributionCycle();
dc.contract = contract;
dc.cycleType = cycleType;
dc.startDate = startDate;
dc.endDate = endDate;
dc.startHour = startHour;
dc.endHour = endHour;
dc.place = db.Place.manager.get(placeId);
if (contract.type == db.Contract.TYPE_VARORDER) {
dc.daysBeforeOrderStart = daysBeforeOrderStart;
dc.daysBeforeOrderEnd = daysBeforeOrderEnd;
dc.openingHour = openingHour;
dc.closingHour = closingHour;
}
if (dc.endDate.getTime() > contract.endDate.getTime()) {
throw new tink.core.Error(t._("The date of the delivery must be prior to the end of the contract (::contractEndDate::)", {contractEndDate:view.hDate(contract.endDate)}));
}
if (dc.startDate.getTime() < contract.startDate.getTime()) {
throw new tink.core.Error(t._("The date of the delivery must be after the begining of the contract (::contractBeginDate::)", {contractBeginDate:view.hDate(contract.startDate)}));
}
if(dispatchEvent){
App.current.event(NewDistribCycle(dc));
}
dc.insert();
createCycleDistribs(dc);
return dc;
}
}
+81
View File
@@ -0,0 +1,81 @@
package service;
using Lambda;
using tools.ObjectListTool;
/**
* Service for managing groups
* @author fbarbut
*/
class GroupService
{
public function new()
{
}
/**
* copy groups.
* @param g
*/
public static function duplicateGroup(g:db.Amap){
var d = new db.Amap();
d.name = g.name+" (copy)";
d.contact = g.contact;
d.txtIntro = g.txtIntro;
d.txtHome = g.txtHome;
d.txtDistrib = g.txtDistrib;
d.extUrl = g.extUrl;
d.membershipRenewalDate = g.membershipRenewalDate;
d.membershipPrice = g.membershipPrice;
d.vatRates = g.vatRates;
d.flags = g.flags;
d.groupType = g.groupType;
d.image = g.image;
d.regOption = g.regOption;
d.currency = g.currency;
d.currencyCode = g.currencyCode;
d.allowedPaymentsType = g.allowedPaymentsType;
d.checkOrder = g.checkOrder;
d.IBAN = g.IBAN;
d.insert();
//put me in the group
return d;
}
static function duplicateCategories(from:db.Amap,to:db.Amap){
}
static function duplicateContract(){
}
/**
Get users with rights in this group
**/
public static function getGroupMembersWithRights(group:db.Amap,?rights:Array<db.UserAmap.Right>):Array<db.User>{
var membersWithAnyRights = db.UserAmap.manager.search($rights!=null && $amap==group,false).array();
if(rights==null){
return Lambda.map(membersWithAnyRights,function(ua) return ua.user).array();
}else{
var members = [];
for( m in membersWithAnyRights){
for(r in rights){
if(m.hasRight(r)){
members.push(m.user);
break;
}
}
}
return members.deduplicate();
}
}
}
+468
View File
@@ -0,0 +1,468 @@
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<db.UserContract>();
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<db.UserContract>):Array<UserOrder> {
var out = new Array<UserOrder>();
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 m = new sugoi.mail.Mail();
m.addRecipient(d.contract.contact.email , d.contract.contact.getName());
m.setSender(App.config.get("default_email"),"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 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"),"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<UserOrder>){
//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;
}
}
+182
View File
@@ -0,0 +1,182 @@
package service;
import Common;
/**
* Payment Service
* @author web-wizard
*/
class PaymentService
{
/**
* Get all available payment types, including one from plugins
*/
public static function getAllPaymentTypes(){
var types = [
new payment.Cash(),
new payment.Check(),
new payment.Transfer(),
new payment.MoneyPot(),
];
var e = App.current.event(GetPaymentTypes({types:types}));
return switch(e){
case GetPaymentTypes(d): d.types;
default : null;
}
}
public static function getAllowedPaymentTypes(group:db.Amap):Array<payment.Payment>{
var out :Array<payment.Payment> = [];
//populate with activated payment types.
var all = getAllPaymentTypes();
if ( group.allowedPaymentsType == null ) return [];
for ( t in group.allowedPaymentsType){
var found = Lambda.find(all, function(a) return a.type == t);
if (found != null) out.push(found);
}
return out;
}
public static function getPaymentTypesForManualEntry(group:db.Amap){
var out = [];
var paymentTypes = [];
var allowedPaymentTypes = service.PaymentService.getAllowedPaymentTypes(group);
if ( !Lambda.exists(allowedPaymentTypes, function(obj) return obj.type == "moneypot" ) ) {
paymentTypes = allowedPaymentTypes;
}
else {
paymentTypes = service.PaymentService.getAllPaymentTypes();
}
for ( t in paymentTypes ){
if(t.type != "moneypot") out.push({label:t.name,value:t.type});
}
return out;
}
/**
* Auto validate a distribution.
* This is called by the hourly cron
*
* @param distrib
*/
public static function validateDistribution(distrib:db.Distribution) {
for ( user in distrib.getUsers()){
var basket = db.Basket.get(user, distrib.place, distrib.date);
validateBasket(basket);
}
//finally validate distrib
distrib.lock();
distrib.validated = true;
distrib.update();
}
public static function unvalidateDistribution(distrib:db.Distribution) {
for ( user in distrib.getUsers()){
var basket = db.Basket.get(user, distrib.place, distrib.date);
unvalidateBasket(basket);
}
//finally validate distrib
distrib.lock();
distrib.validated = false;
distrib.update();
}
/**
* Auto validate a basket
*
* @param basket
*/
public static function validateBasket(basket:db.Basket) {
if (basket == null || basket.isValidated()) return false;
//mark orders as paid
var orders = basket.getOrders();
for ( order in orders ){
order.lock();
order.paid = true;
order.update();
}
//validate order operation and payments
var operation = basket.getOrderOperation(false);
if (operation != null){
operation.lock();
operation.pending = false;
operation.update();
for ( payment in basket.getPayments()){
if ( payment.pending){
payment.lock();
payment.pending = false;
payment.update();
}
}
var o = orders.first();
updateUserBalance(o.user, o.distribution.place.amap);
}
return true;
}
public static function unvalidateBasket(basket:db.Basket) {
if (basket == null || !basket.isValidated()) return false;
//mark orders as paid
var orders = basket.getOrders();
for ( order in orders ){
order.lock();
order.paid = false;
order.update();
}
//validate order operation and payments
var operation = basket.getOrderOperation(false);
if (operation != null){
operation.lock();
operation.pending = true;
operation.update();
for ( payment in basket.getPayments()){
if (!payment.pending){
payment.lock();
payment.pending = true;
payment.update();
}
}
var o = orders.first();
updateUserBalance(o.user, o.distribution.place.amap);
}
return true;
}
/**
* update user balance
*/
public static function updateUserBalance(user:db.User,group:db.Amap){
var ua = db.UserAmap.getOrCreate(user, group);
var b = sys.db.Manager.cnx.request('SELECT SUM(amount) FROM Operation WHERE userId=${user.id} and groupId=${group.id} and !(type=2 and pending=1)').getFloatResult(0);
b = Math.round(b * 100) / 100;
ua.balance = b;
ua.update();
}
}
+29
View File
@@ -0,0 +1,29 @@
package service;
class PlaceService{
/**
* Geocode a place with Google Geocode API
*/
public static function geocode(p:db.Place):{lat:Float,lng:Float}{
var apiKey = App.config.get("google_geocoding_key");
if(apiKey==null) return null;
var gc = new sugoi.apis.google.GeoCode(apiKey);
var address = p.getAddress();
//var comp = "administrative_area:" + p.city + "|postal_code:" + p.zipCode + "|country:FR";
//Sys.print(address+"<br/>"+comp+"<br/>");
var geo = gc.geocode( address , null);
var coords = geo[0].geometry.location;
p.lock();
p.lat = coords.lat;
p.lng = coords.lng;
p.update();
return {lat:p.lat,lng:p.lng};
}
}
+57
View File
@@ -0,0 +1,57 @@
package service;
class ProductService{
/**
* Batch disable products
*/
public static function batchDisableProducts(productIds:Array<Int>){
var data = {pids:productIds,enable:false};
var contract = db.Product.manager.get(productIds[0], true).contract;
var products = contract.getProducts(false);
App.current.event( BatchEnableProducts(data) );
for ( pid in data.pids){
var p = db.Product.manager.get(pid, true);
if ( Lambda.find(products,function(p) return p.id==pid)==null ) throw 'product $pid is not in this contract !';
p.active = false;
p.update();
}
}
/**
* Batch enable products
*/
public static function batchEnableProducts(productIds:Array<Int>){
var data = {pids:productIds,enable:true};
var contract = db.Product.manager.get(productIds[0], true).contract;
var products = contract.getProducts(false);
App.current.event( BatchEnableProducts(data) );
for ( pid in data.pids){
var p = db.Product.manager.get(pid, true);
if ( Lambda.find(products,function(p) return p.id==pid)==null ) throw 'product $pid is not in this contract !';
p.active = true;
p.update();
}
}
inline public static function getHTPrice(ttcPrice:Float,vatRate:Float):Float{
return ttcPrice / (1 + vatRate / 100);
}
}
+115
View File
@@ -0,0 +1,115 @@
package service;
import Common;
class ReportService{
/**
Get orders grouped by products.
*/
public static function getOrdersByProduct( distribution:db.Distribution, ?csv = false):Array<OrderByProduct>{
var view = App.current.view;
var t = sugoi.i18n.Locale.texts;
var where = "";
var exportName = t._("Delivery ::contractName:: of the ", {contractName:distribution.contract.name}) + distribution.date.toString().substr(0, 10);
where += ' and p.contractId = ${distribution.contract.id}';
if (distribution.contract.type == db.Contract.TYPE_VARORDER ) {
where += ' and up.distributionId = ${distribution.id}';
}
//Product price will be an average if price changed
var sql = 'select
SUM(quantity) as quantity,
MAX(p.id) as pid,
p.name as pname,
AVG(up.productPrice) as price,
AVG(p.vat) as vat,
p.ref as ref,
SUM(quantity*up.productPrice) as totalTTC
from UserContract up, Product p
where up.productId = p.id
$where
group by ref,pname,price
order by pname asc;';
var res = sys.db.Manager.cnx.request(sql).results();
var orders = [];
//populate with full product names
for ( r in res){
var o : OrderByProduct = {
quantity:1.0 * r.quantity,
smartQt:"",
pid:r.pid,
pname:r.pname,
ref:r.ref,
priceHT: service.ProductService.getHTPrice(r.price,r.vat),
priceTTC: r.price,
vat:r.vat,
totalTTC : r.totalTTC,
totalHT : service.ProductService.getHTPrice( r.totalTTC ,r.vat),
weightOrVolume:"",
};
//smartQt
var p = db.Product.manager.get(r.pid, false);
if( p.hasFloatQt || p.variablePrice ){
o.smartQt = view.smartQt(o.quantity, p.qt, p.unitType);
}else{
o.smartQt = Std.string(o.quantity);
}
o.weightOrVolume = view.smartQt(o.quantity, p.qt, p.unitType);
if ( /*p.hasFloatQt || p.variablePrice ||*/ p.qt==null || p.unitType==null){
o.pname = p.name;
}else{
o.pname = p.name + " " + view.formatNum(p.qt) +" " + view.unit(p.unitType, o.quantity > 1);
}
//special case : if product is multiweight, we should count the records number ( and not SUM quantities )
if (p.multiWeight){
sql = 'select
COUNT(up.id) as quantity
from UserContract up, Product p
where up.productId = p.id and up.quantity > 0 and p.id=${p.id}
$where';
var count = sys.db.Manager.cnx.request(sql).getIntResult(0);
o.smartQt = ""+count;
}
orders.push(o);
}
if (csv) {
var data = new Array<Dynamic>();
for (o in orders) {
data.push({
"quantity":view.formatNum(o.quantity),
"pname":o.pname,
"ref":o.ref,
"priceHT":view.formatNum(o.priceHT),
"priceTTC":view.formatNum(o.priceTTC),
"totalHT":view.formatNum(o.totalHT),
"totalTTC":view.formatNum(o.totalTTC),
});
}
sugoi.tools.Csv.printCsvDataFromObjects(data, ["quantity", "pname","ref", "priceHT","priceTTC","totalHT","totalTTC"],"Export-"+exportName+"-par produits");
return null;
}else{
return orders;
}
}
public static function getTurnoverFromOrdersByProducts(ordersByProduct:Array<OrderByProduct>):{turnoverHT:Float,turnoverTTC:Float}{
var out = {turnoverHT:0.0,turnoverTTC:0.0};
for( o in ordersByProduct){
out.turnoverHT += o.totalHT;
out.turnoverTTC += o.totalTTC;
}
return out;
}
}
+120
View File
@@ -0,0 +1,120 @@
package service;
import tink.core.Error;
/**
* User Service
* @author fbarbut
*/
class UserService
{
var user : db.User;
public function new(u:db.User)
{
this.user = u;
}
/**
* User login service
* @param email
* @param password
*/
public static function login(email:String, password:String){
var t = sugoi.i18n.Locale.texts;
//user exists ?
var user = db.User.manager.select( $email == email || $email2 == email , true);
if (user == null) throw new Error(404,t._("There is no account with this email"));
//new account
if (!user.isFullyRegistred()) {
var group = user.getAmaps().first();
user.sendInvitation(group);
var text = t._("Your account have not been validated yet. We sent an e-mail to ::email:: to finalize your subscription!",{email:user.email});
throw new Error(403,text);
}
var pass = haxe.crypto.Md5.encode( App.config.get('key') + password );
if (user.pass != pass) {
throw new Error(403,t._("Invalid password"));
}
db.User.login(user, email);
//register the user to the current group if needed
var group = App.current.getCurrentGroup();
if (group != null && group.regOption == db.Amap.RegOption.Open && db.UserAmap.get(user, group) == null){
user.makeMemberOf(group);
}
}
/**
Full registration by a user himself
**/
public static function register(firstName:String, lastName:String, email:String, phone:String, pass:String){
var t = sugoi.i18n.Locale.texts;
if (!sugoi.form.validators.EmailValidator.check(email)){
throw new Error(500,t._("Invalid email address"));
}
if ( db.User.getSameEmail(email).length > 0 ) {
throw new Error(409,t._("We already have an account with this email address"));
}
var user = new db.User();
user.email = email;
user.firstName = firstName;
user.lastName = lastName;
user.phone = phone;
user.setPass(pass);
user.insert();
var group = App.current.getCurrentGroup();
if (group != null && group.regOption == db.Amap.RegOption.Open){
user.makeMemberOf(group);
}
db.User.login(user, email);
}
/**
Soft registration :
- Somebody creates/import a new user ,
- or pre-registration in a waiting list
**/
public static function softRegistration(firstName:String, lastName:String, email:String){
var t = sugoi.i18n.Locale.texts;
if (!sugoi.form.validators.EmailValidator.check(email)){
throw new Error(500,t._("Invalid email address"));
}
if ( db.User.getSameEmail(email).length > 0 ) {
throw new Error(409,t._("We already have an account with this email address"));
}
var user = new db.User();
user.email = email;
user.firstName = firstName;
user.lastName = lastName;
user.insert();
return user;
}
/**
* get users belonging to a group
* @param group -
* @return Array<db.User>
*/
public static function getFromGroup(group:db.Amap):Array<db.User>{
return Lambda.array( group.getMembers() );
}
}
+140
View File
@@ -0,0 +1,140 @@
package service;
import tink.core.Error;
import db.UserAmap.Right;
class WaitingListService{
public static function registerToWl(user:db.User,group:db.Amap,message:String){
var t = sugoi.i18n.Locale.texts;
canRegister(user,group);
var w = new db.WaitingList();
w.user = user;
w.group = group;
w.message = message;
w.insert();
//emails
var html = t._("<p><b>::name::</b> suscribed to the waiting list of <b>::group::</b> on ::date::</p>",{
group:group.name,
name:user.name,
date:App.current.view.hDate(Date.now())
});
if(message!=null && message!=""){
html += t._("<p>He/she left this message :<br/>\"::message::\"</p>",{message:message});
}
for( u in service.GroupService.getGroupMembersWithRights(group,[Right.GroupAdmin,Right.Membership]) ){
App.quickMail(
u.email,
t._("[::group::] ::name:: suscribed to the waiting list.",{group:group.name,name:user.name}),
html,
group
);
}
}
public static function canRegister(user:db.User,group:db.Amap){
var t = sugoi.i18n.Locale.texts;
if ( db.WaitingList.manager.select($amapId == group.id && $user == user) != null) {
throw new Error(t._("You are already in the waiting list of this group"));
}
if ( db.UserAmap.manager.select($amapId == group.id && $user == user) != null) {
throw new Error(t._("You are already member of this group."));
}
}
/**
the user cancels his request
**/
public static function removeFromWl(user:db.User,group:db.Amap){
var t = sugoi.i18n.Locale.texts;
if ( user == null) {
throw new Error(t._("You should be logged in."));
}
var wl = db.WaitingList.manager.select($amapId == group.id && $user == user,true);
if ( wl == null) {
throw new Error(t._("You are not in the waiting list of this group"));
}
wl.delete();
}
/**
an admin cancels a request
**/
public static function cancelRequest(user:db.User,group:db.Amap){
var t = sugoi.i18n.Locale.texts;
if ( user == null) throw "user is null";
var wl = db.WaitingList.manager.select($amapId == group.id && $user == user,true);
if ( wl == null) throw "this user is not in waiting list";
//email the requester
App.quickMail(
wl.user.email,
t._("[::group::] Membership request refused.",{group:group.name}),
t._("Your membership request for <b>::group::</b> has been refused.",{group:group.name})
);
//email others admin
for( u in service.GroupService.getGroupMembersWithRights(group,[Right.GroupAdmin,Right.Membership]) ){
if(u.id==App.current.user.id) continue;
App.quickMail(
u.email,
t._("[::group::] ::name:: membership request has been refused by ::admin::.",{group:group.name, name:user.name, admin:App.current.user.name}),
t._("<p><b>::name::</b> was registred to the waiting list.</p><p><b>::admin::</b> has refused his/her request.</p>",{name:user.name, admin:App.current.user.name}),
group
);
}
wl.delete();
}
/**
an admin approves a request
**/
public static function approveRequest(user:db.User,group:db.Amap){
var t = sugoi.i18n.Locale.texts;
if ( user == null) throw "user is null";
var wl = db.WaitingList.manager.select($amapId == group.id && $user == user,true);
if ( wl == null) throw "this user is not in waiting list";
if (db.UserAmap.get(user, group, false) == null){
var ua = new db.UserAmap();
ua.amap = wl.group;
ua.user = wl.user;
ua.insert();
}
wl.delete();
//email the requester
App.quickMail(
wl.user.email,
t._("[::group::] Membership request accepted.",{group:group.name}),
t._("<p>Your membership request for <b>::group::</b> has been accepted !</p><p>You're now a member of the group.</p>",{group:group.name}),
group
);
//email others admin
for( u in service.GroupService.getGroupMembersWithRights(group,[Right.GroupAdmin,Right.Membership]) ){
if(u.id==App.current.user.id) continue;
App.quickMail(
u.email,
t._("[::group::] ::name:: membership request has been accepted by ::admin::.",{group:group.name, name:user.name, admin:App.current.user.name}),
t._("<p><b>::name::</b> was registred to the waiting list.</p><p><b>::admin::</b> has accepted his/her request.</p>",{name:user.name, admin:App.current.user.name}),
group
);
}
wl.delete();
}
}