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
Executable
+306
View File
@@ -0,0 +1,306 @@
import db.User;
import thx.semver.Version;
import Common;
class App extends sugoi.BaseApp {
public static var current : App = null;
public static var t : sugoi.i18n.translator.ITranslator;
public static var config = sugoi.BaseApp.config;
public var eventDispatcher :hxevents.Dispatcher<Event>;
public var plugins : Array<sugoi.plugin.IPlugIn>;
/**
* Version management
* @doc https://github.com/fponticelli/thx.semver
*/
//public static var VERSION = ([0,9,2] : Version).withPre("july");
public static var VERSION = ([0,9,2] : Version).withPre(MyMacros.getGitShortSHA(), MyMacros.getGitCommitDate());
public function new(){
super();
}
public static function main() {
App.t = sugoi.form.Form.translator = new sugoi.i18n.translator.TMap(getTranslationArray(), "fr");
sugoi.BaseApp.main();
}
/**
* Init plugins and event dispatcher just before launching the app
*/
override public function mainLoop() {
eventDispatcher = new hxevents.Dispatcher<Event>();
plugins = [];
//internal plugins
plugins.push(new plugin.Tutorial());
//optionnal plugins
#if plugins
plugins.push( new hosted.HostedPlugIn() );
plugins.push( new pro.ProPlugIn() );
plugins.push( new connector.ConnectorPlugIn() );
plugins.push( new pro.LemonwayEC() );
plugins.push( new who.WhoPlugIn() );
#end
super.mainLoop();
}
public function getCurrentGroup(){
if (session == null) return null;
if (session.data == null ) return null;
var a = session.data.amapId;
if (a == null) {
return null;
}else {
return db.Amap.manager.get(a,false);
}
}
override function beforeDispatch() {
//send "current page" event
event( Page(this.uri) );
super.beforeDispatch();
}
public function getPlugin(name:String):sugoi.plugin.IPlugIn {
for (p in plugins) {
if (p.getName() == name) return p;
}
return null;
}
public static function log(t:Dynamic) {
if(App.config.DEBUG) {
neko.Web.logMessage(Std.string(t)); //write in Apache error log
#if weblog
Weblog.log(t); //write en Weblog console (https://lib.haxe.org/p/weblog/)
#end
}
}
public function event(e:Event) {
if(e==null) return null;
this.eventDispatcher.dispatch(e);
return e;
}
/**
* Translate DB objects fields in forms
*/
public static function getTranslationArray() {
//var t = sugoi.i18n.Locale.texts;
var out = new Map<String,String>();
//out.set("firstName", t._("First name") );
//out.set("lastName", t._("Last name"));
out.set("firstName2", "Prénom du conjoint");
out.set("lastName2", "Nom du conjoint");
out.set("email2", "e-mail du conjoint");
//out.set("pass", t._("Password") );
//out.set("address1", t._("address") );
//out.set("address2", t._("address") );
out.set("zipCode", "code postal");
out.set("city", "commune");
out.set("phone", "téléphone");
out.set("phone2", "téléphone du conjoint");
out.set("select", "sélectionnez");
out.set("contract", "Contrat");
out.set("place", "Lieu");
out.set("name", "Nom");
out.set("cdate", "Date d'entrée dans le groupe");
out.set("quantity", "Quantité");
out.set("paid", "Payé");
out.set("user2", "(facultatif) partagé avec ");
out.set("product", "Produit");
out.set("user", "Adhérent");
out.set("txtIntro", "Texte de présentation du groupe");
out.set("txtHome", "Texte en page d'accueil pour les adhérents connectés");
out.set("txtDistrib", "Texte à faire figurer sur les listes d'émargement lors des distributions");
out.set("extUrl", "URL du site du groupe.");
out.set("distributor1", "Distributeur 1");
out.set("distributor2", "Distributeur 2");
out.set("distributor3", "Distributeur 3");
out.set("distributor4", "Distributeur 4");
out.set("distributorNum", "Nbre de distributeurs nécessaires (de 0 à 4)");
out.set("startDate", "Date de début");
out.set("endDate", "Date de fin");
out.set("orderStartDate", "Date ouverture des commandes");
out.set("orderEndDate", "Date fermeture des commandes");
out.set("openingHour", "Heure d'ouverture");
out.set("closingHour", "Heure de fermeture");
out.set("date", "Date de distribution");
out.set("active", "actif");
out.set("contact", "Reponsable");
out.set("vendor", "Producteur");
out.set("text", "Texte");
out.set("flags", "Options");
out.set("4h", "Recevoir des notifications par email 4h avant les distributions");
out.set("HasEmailNotif4h", "Recevoir des notifications par email 4h avant les distributions");
out.set("24h", "Recevoir des notifications par email 24h avant les distributions");
out.set("HasEmailNotif24h", "Recevoir des notifications par email 24h avant les distributions");
out.set("Ouverture", "Recevoir des notifications par email pour l'ouverture des commandes");
out.set("Tuto", "Activer tutoriels");
out.set("HasMembership", "Gestion des adhésions");
out.set("DayOfWeek", "Jour de la semaine");
out.set("Monday", "Lundi");
out.set("Tuesday", "Mardi");
out.set("Wednesday", "Mercredi");
out.set("Thursday", "Jeudi");
out.set("Friday", "Vendredi");
out.set("Saturday", "Samedi");
out.set("Sunday", "Dimanche");
out.set("cycleType", "Récurrence");
out.set("Weekly", "hebdomadaire");
out.set("Monthly", "mensuelle");
out.set("BiWeekly", "toutes les deux semaines");
out.set("TriWeekly", "toutes les 3 semaines");
out.set("price", "prix TTC");
out.set("uname", "Nom");
out.set("pname", "Produit");
out.set("organic", "Agriculture biologique");
out.set("hasFloatQt", "Autoriser quantités \"à virgule\"");
out.set("membershipRenewalDate", "Adhésions : Date de renouvellement");
out.set("membershipPrice", "Adhésions : Coût de l'adhésion");
out.set("UsersCanOrder", "Les adhérents peuvent saisir leur commande en ligne");
out.set("StockManagement", "Gestion des stocks");
out.set("contact", "Responsable");
out.set("PercentageOnOrders", "Ajouter des frais au pourcentage de la commande");
out.set("percentageValue", "Pourcentage des frais");
out.set("percentageName", "Libellé pour ces frais");
out.set("fees", "frais");
out.set("AmapAdmin", "Administrateur du groupe");
out.set("Membership", "Accès à la gestion des adhérents");
out.set("Messages", "Accès à la messagerie");
out.set("vat", "TVA");
out.set("desc", "Description");
out.set("ShopMode", "Mode boutique");
out.set("ComputeMargin", "Appliquer une marge à la place des pourcentages");
out.set("ShopCategoriesFromTaxonomy", "Catégoriser automatiquement les produits");
out.set("HidePhone", "Masquer le téléphone du responsable sur la page publique");
out.set("PhoneRequired", "Saisie du numéro de téléphone obligatoire");
out.set("ref", "Référence");
out.set("linkText", "Intitulé du lien");
out.set("linkUrl", "URL du lien");
out.set("Amap", "AMAP");
out.set("GroupedOrders", "Groupement d'achat");
out.set("ProducerDrive", "Collectif de producteurs");
out.set("FarmShop", "Vente à la ferme");
out.set("regOption", "Inscription de nouveaux adhérents");
out.set("Closed", "Fermé : Le coordinateur ajoute les nouveaux adhérents");
out.set("WaitingList", "Liste d'attente");
out.set("Open", "Ouvert : tout le monde peut s'inscrire");
out.set("Full", "Complet : Le groupe n'accepte plus de nouveaux adhérents");
out.set("percent", "Pourcentage");
out.set("pinned", "Mets en avant les produits");
out.set("CagetteNetwork", "Me lister dans l'annuaire des groupes Cagette.net");
out.set("unitType", "Unité");
out.set("qt", "Quantité");
out.set("Unit", "Pièce");
out.set("Kilogram", "Kilogrammes");
out.set("Gram", "Grammes");
out.set("Litre", "Litres");
out.set("htPrice", "Prix H.T");
out.set("amount", "Montant");
out.set("HasPayments", "Gestion des paiements");
out.set("byMember", "Par adhérent");
out.set("byProduct", "Par produit");
out.set("variablePrice", "Variable price based on weight");
return out;
}
public function populateAmapMembers() {
return user.amap.getMembersFormElementData();
}
public static function getMailer():sugoi.mail.IMailer {
var mailer : sugoi.mail.IMailer = new sugoi.mail.BufferedMailer();
if(App.config.DEBUG){
//Dev env : emails are written to tmp folder
mailer = new sugoi.mail.DebugMailer();
}else{
if (sugoi.db.Variable.get("mailer") == null){
var msg = sugoi.i18n.Locale.texts._("Please configure the email settings in a <href='/admin/emails'>this section</a>");
throw sugoi.ControllerAction.ErrorAction("/",msg);
}
if (sugoi.db.Variable.get("mailer") == "mandrill"){
//Buffered emails with Mandrill
untyped mailer.defineFinalMailer("mandrill");
}else{
//Buffered emails with SMTP
untyped mailer.defineFinalMailer("smtp");
}
}
return mailer;
}
/**
* Send an email
*/
public static function sendMail(m:sugoi.mail.Mail, ?group:db.Amap, ?listId:String, ?sender:db.User){
if (group == null) group = App.current.user == null ? null:App.current.user.getAmap();
current.event(SendEmail(m));
var params = group==null ? null : {remoteId:group.id};
getMailer().send(m,params,function(o){});
}
public static function quickMail(to:String, subject:String, html:String,?group:db.Amap){
var e = new sugoi.mail.Mail();
e.setSubject(subject);
e.setRecipient(to);
e.setSender(App.config.get("default_email"),"Cagette.net");
var html = App.current.processTemplate("mail/message.mtt", {text:html,group:group});
e.setHtmlBody(html);
App.sendMail(e);
}
/**
* process a template and returns the generated string
* @param tpl
* @param ctx
*/
public function processTemplate(tpl:String, ctx:Dynamic):String {
Reflect.setField(ctx, 'HOST', App.config.HOST);
Reflect.setField(ctx, 'hDate', App.current.view.hDate);
//i18n functions
ctx._ = App.current.view._;
ctx.__ = App.current.view.__;
var tpl = loadTemplate(tpl);
var html = tpl.execute(ctx);
#if php
if ( html.substr(0, 4) == "null") html = html.substr(4);
#end
return html;
}
}
+83
View File
@@ -0,0 +1,83 @@
package;
typedef CalEvent = {
name:String,
//start:Date,
//end:Date,
color:Int,
}
/**
* Calendar utility
*
* @author fbarbut<francois.barbut@gmail.com>
*/
class Calendar
{
public static var COLOR_CONTRACT = 0xC91F25;
public static var COLOR_DELIVERY = 0x7BAD1C;
public static var COLOR_ORDER = 0xFF9615;
public function new()
{
}
public static function getMonthViewMap():Map<String,Array<CalEvent>> {
var n = Date.now();
var m = n.getMonth();//0-11
var pointer = Date.now();
var out = new Map<String,Array<CalEvent>>();
//find last monday
for ( i in 0...40) {
if ( pointer.getDay() == 1 ) {
break;
}
pointer = DateTools.delta(pointer, -1000.0 * 60 * 60 * 24);
}
//go ahead for at least 27 days
for ( i in 0...28) {
out.set( pointer.toString().substr(0, 10), [] );
pointer = DateTools.delta(pointer, 1000.0 * 60 * 60 * 24);
}
//find end
for ( i in 0...40) {
//if ( pointer.getDay() == 1 && pointer.getMonth() != m) break;
out.set( pointer.toString().substr(0, 10), [] );
pointer = DateTools.delta(pointer, 1000.0 * 60 * 60 * 24);
}
return out;
}
/**
* get ordered CalEvents from an unordered stringMap
*/
public static function mapToArray(input : Map<String,Array<CalEvent>>) : Array<{d:Date,events:Array<CalEvent>}> {
var keys = [];
for (k in input.keys()) keys.push(k);
keys.sort(function(a, b) {
return Math.round(Date.fromString(a).getTime()/1000) - Math.round(Date.fromString(b).getTime()/1000);
});
var out = [];
for ( k in keys) {
var x = input.get(k);
out.push( { d:Date.fromString(k),events:x } );
}
return out;
}
}
+385
View File
@@ -0,0 +1,385 @@
package;
import Common;
using tools.ObjectListTool;
using Lambda;
using tools.ObjectListTool;
/**
* MultiDistrib represents many db.Distribution
which happen on the same day + same place.
*
* @author fbarbut
*/
class MultiDistrib
{
public var distributions : Array<db.Distribution>;
public var contracts : Array<db.Contract>;
//public var actions : Array<Link>;
public var extraHtml : String;
public var type : Null<Int>; //contract type, both contract types cannot be mixed in a same multidistrib.
public function new(){
distributions = [];
contracts = [];
extraHtml = "";
//actions = [];
}
public static function get(date:Date, place:db.Place,contractType:Int){
var m = new MultiDistrib();
var start = tools.DateTool.setHourMinute(date, 0, 0);
var end = tools.DateTool.setHourMinute(date, 23, 59);
var contracts = place.amap.getContracts().array();
//filter by type
if(contractType==db.Contract.TYPE_VARORDER){
for(c in contracts.copy() ){
if(c.type!=db.Contract.TYPE_VARORDER) contracts.remove(c);
}
}else if(contractType==db.Contract.TYPE_CONSTORDERS){
for(c in contracts.copy() ){
if(c.type!=db.Contract.TYPE_CONSTORDERS) contracts.remove(c);
}
}
var cids = contracts.getIds();
m.distributions = db.Distribution.manager.search(($contractId in cids) && ($date >= start) && ($date <= end) && $place==place, { orderBy:date }, false).array();
m.type = contractType;
return m;
}
/**
Get multidistribs from a time range + place + type
**/
public static function getFromTimeRange(group:db.Amap,from:Date,to:Date,?contractType:Int):Array<MultiDistrib>{
var multidistribs = [];
var start = tools.DateTool.setHourMinute(from, 0, 0);
var end = tools.DateTool.setHourMinute(to, 23, 59);
var cids = group.getContracts().getIds();
var distributions = db.Distribution.manager.search(($contractId in cids) && ($date >= start) && ($date <= end) , { orderBy:date }, false).array();
//sort by day-place-type
var multidistribs = new Map<String,MultiDistrib>();
for ( d in distributions){
//filter by contractType
if(contractType!=null){
if(d.contract.type!=contractType) continue;
}
var key = d.getKey() + "-" + d.contract.type;
if(multidistribs[key]==null){
var m = new MultiDistrib();
m.distributions.push(d);
m.type = d.contract.type;
multidistribs[key] = m;
}else{
multidistribs[key].distributions.push(d);
}
}
var multidistribs = Lambda.array(multidistribs);
//trigger event
for(md in multidistribs) App.current.event(MultiDistribEvent(md));
//sort by date desc
multidistribs.sort(function(x,y){
return Math.round( x.getDate().getTime()/1000 ) - Math.round(y.getDate().getTime()/1000 );
});
return multidistribs;
}
/**
* TODO : refacto this to use getFromTimeRange();
*/
/*public static function getNextMultiDeliveries(group:db.Amap){
var out = new Map < String, {
place:db.Place, //common delivery place
startDate:Date, //global delivery start
endDate:Date, //global delivery stop
orderStartDate:Date, //global orders opening date
orderEndDate:Date, //global orders closing date
active:Bool,
products:Array<ProductInfo>, //available products ( if no order )
myOrders:Array<{distrib:db.Distribution,orders:Array<UserOrder>}> //my orders
}>();
var n = Date.now();
var now = new Date(n.getFullYear(), n.getMonth(), n.getDate(), 0, 0, 0);
var contracts = db.Contract.getActiveContracts(group);
var cids = Lambda.map(contracts, function(p) return p.id);
//var pids = Lambda.map(db.Product.manager.search($contractId in cids,false), function(x) return x.id);
//var out = UserContract.manager.search(($userId == id || $userId2 == id) && $productId in pids, lock);
//available deliveries
var inSixMonth = DateTools.delta(now, 1000.0 * 60 * 60 * 24 * 30 * 6);
var distribs = db.Distribution.manager.search(($contractId in cids) && $date >= now && $date <= inSixMonth , { orderBy:date }, false);
for (d in distribs) {
//we had the distribution key ( place+date ) and the contract type in order to separate constant and varying contracts
var key = d.getKey() + "|" + d.contract.type;
var o = out.get(key);
if (o == null) o = {place:d.place, startDate:d.date, active:null, endDate:d.end, products:[], myOrders:[], orderStartDate:null,orderEndDate:null};
//user orders
var orders = [];
if(App.current.user!=null) orders = d.contract.getUserOrders(App.current.user,d);
if (orders.length > 0){
o.myOrders.push({distrib:d,orders:service.OrderService.prepare(orders)});
}else{
//no "order block" if no shop mode
if (!group.hasShopMode() ) {
continue;
}
//if its a constant order contract, skip this delivery
if (d.contract.type == db.Contract.TYPE_CONSTORDERS){
continue;
}
//products preview if no orders
for ( p in d.contract.getProductsPreview(9)){
o.products.push( p.infos(null,false) );
}
}
if (d.contract.type == db.Contract.TYPE_VARORDER){
//old distribs may have an empty orderStartDate
if (d.orderStartDate == null) {
continue;
}
//if order opening is more far than 1 month, skip it
// if (d.orderStartDate.getTime() > inOneMonth.getTime() ){
// continue;
// }
//display closest opening date
if (o.orderStartDate == null){
o.orderStartDate = d.orderStartDate;
}else if (o.orderStartDate.getTime() > d.orderStartDate.getTime()){
o.orderStartDate = d.orderStartDate;
}
//display most far closing date
if (o.orderEndDate == null){
o.orderEndDate = d.orderEndDate;
}else if (o.orderEndDate.getTime() < d.orderEndDate.getTime()){
o.orderEndDate = d.orderEndDate;
}
out.set(key, o);
}else{
//in constant orders, add block only if there is an order
if(o.myOrders.length>0) out.set(key, o);
}
}
//shuffle and limit product lists
for ( o in out){
o.products = thx.Arrays.shuffle(o.products);
o.products = o.products.slice(0, 9);
}
//decide if active or not
var now = Date.now();
for( o in out){
if (o.orderStartDate == null) continue; //constant orders
if (now.getTime() >= o.orderStartDate.getTime() && now.getTime() <= o.orderEndDate.getTime() ){
//order currently open
o.active = true;
}else {
o.active = false;
}
}
return Lambda.array(out);
}*/
public function getPlace(){
if(distributions.length==0) throw "This multidistrib is empty";
return distributions[0].place;
}
public function getDate(){
if(distributions.length==0) throw "This multidistrib is empty";
return distributions[0].date;
}
public function getEndDate(){
if(distributions.length==0) throw "This multidistrib is empty";
return distributions[0].end;
}
public function getProductsExcerpt():Array<ProductInfo>{
var key = "productsExcerpt-"+getKey();
var cache:Array<Int> = sugoi.db.Cache.get(key);
if(cache!=null){
var out = [];
//try{
for( pid in cache.array()){
var p = db.Product.manager.get(pid,false);
if(p!=null) out.push(p.infos());
}
//}catch(e:Dynamic){
// sugoi.db.Cache.destroy(key);
// }
return out;
}
var products = [];
for( d in distributions){
for ( p in d.contract.getProductsPreview(9)){
products.push( p.infos(null,false) );
}
}
products = thx.Arrays.shuffle(products);
products = products.slice(0, 9);
sugoi.db.Cache.set(key, products.map(function(p)return p.id).array(), 3600 );
return products;
}
public function userHasOrders(user:db.User):Bool{
if(user==null) return false;
for ( d in distributions){
if(d.getUserOrders(user).length>0) return true;
}
return false;
}
/**
orders currently open ?
**/
public function isActive(){
if (getOrdersStartDate() == null) return false; //constant orders
var now = Date.now();
if (now.getTime() >= getOrdersStartDate().getTime() && now.getTime() <= getOrdersEndDate().getTime() ){
return true;
}else {
return false;
}
}
public function getOrdersStartDate(){
var date = null;
for( d in distributions ){
if(d.orderStartDate==null) continue;
//display closest opening date
if (date == null){
date = d.orderStartDate;
}else if (date.getTime() > d.orderStartDate.getTime()){
date = d.orderStartDate;
}
}
return date;
}
/*public function hasOnlyConstantOrders(){
for(d in distributions){
if( d.contract.type==db.Contract.TYPE_VARORDER ) {
return false;
}
}
return true;
}*/
public function getOrdersEndDate(){
var date = null;
for( d in distributions ){
if(d.orderEndDate==null) continue;
//display most far closing date
if (date == null){
date = d.orderEndDate;
}else if (date.getTime() < d.orderEndDate.getTime()){
date = d.orderEndDate;
}
}
return date;
}
/**
* Get all orders involved in this multidistrib
*/
public function getOrders(){
var out = [];
for ( d in distributions){
out = out.concat(d.getOrders().array());
}
return out;
}
/**
* Get orders for a user in this multidistrib
* @param user
*/
public function getUserOrders(user:db.User){
var out = [];
for ( d in distributions){
var pids = Lambda.map( d.contract.getProducts(false), function(x) return x.id);
var userOrders = db.UserContract.manager.search( $userId == user.id && $distributionId==d.id && $productId in pids , false);
for( o in userOrders ){
out.push(o);
}
}
return out;
}
public function getUsers(){
var users = [];
for ( o in getOrders()) users.push(o.user);
return users.deduplicate();
}
public function isConfirmed():Bool{
//cannot be in future
if(getDate().getTime()>Date.now().getTime()) return false;
return Lambda.count( distributions, function(d) return d.validated) == distributions.length;
}
public function checkConfirmed():Bool{
var orders = getOrders();
var c = Lambda.count( orders, function(d) return d.paid) == orders.length;
if (c){
for ( d in distributions){
if (!d.validated){
d.lock();
d.validated = true;
d.update();
}
}
}
return c;
}
//get key by date-place-type
public function getKey(){
return distributions[0].getKey() + "-" + distributions[0].contract.type;
}
}
+82
View File
@@ -0,0 +1,82 @@
import haxe.macro.Expr;
import haxe.macro.Context;
import StringTools;
class MyMacros {
/* based on
http://code.haxe.org/category/macros/add-git-commit-hash-in-build.html
http://stackoverflow.com/questions/8611486/how-to-get-the-last-commit-date-for-a-bunch-of-files-in-git*/
public static macro function getGitCommitDate():haxe.macro.ExprOf<String> {
#if !display
var process = new sys.io.Process('git', ['log', '-1', '--format=%ci']);
if (process.exitCode() != 0) {
var message = process.stderr.readAll().toString();
var pos = haxe.macro.Context.currentPos();
Context.error("Cannot execute `git log -1 --format=%ci`. " + message, pos);
}
// read the output of the process
var commitDate:String = process.stdout.readLine();
commitDate = StringTools.replace(commitDate, " ", ".");
commitDate = StringTools.replace(commitDate, "-", ".");
commitDate = StringTools.replace(commitDate, ":", ".");
commitDate = commitDate.substr(0,16);
// Generates a string expression
return macro $v{commitDate};
#else
// `#if display` is used for code completion. In this case returning an
// empty string is good enough; We don't want to call git on every hint.
var commitDate:String = "";
return macro $v{commitDate};
#end
}
public static macro function getGitShortSHA():haxe.macro.ExprOf<String> {
#if !display
var process = new sys.io.Process('git', ['log', '-1', '--format=%h']);
if (process.exitCode() != 0) {
var message = process.stderr.readAll().toString();
var pos = haxe.macro.Context.currentPos();
Context.error("Cannot execute `git log -1 --format=%h`. " + message, pos);
}
// read the output of the process
var commitShortSHA:String = process.stdout.readLine();
commitShortSHA = "C"+commitShortSHA;
// Generates a string expression
return macro $v{commitShortSHA};
#else
// `#if display` is used for code completion. In this case returning an
// empty string is good enough; We don't want to call git on every hint.
var commitShortSHA:String = "xxxxxxx";
return macro $v{commitShortSHA};
#end
}
public static macro function getGitBranch():haxe.macro.ExprOf<String> {
#if !display
var process = new sys.io.Process('git', ['symbolic-ref', 'HEAD', '--short']);
if (process.exitCode() != 0) {
var message = process.stderr.readAll().toString();
var pos = haxe.macro.Context.currentPos();
Context.error("Cannot execute `git symbolic-ref HEAD --short`. " + message, pos);
}
// read the output of the process
var commitBranch:String = process.stdout.readLine();
commitBranch = StringTools.replace(commitBranch, "-", ".");
commitBranch = StringTools.replace(commitBranch, "_", ".");
// Generates a string expression
return macro $v{commitBranch};
#else
// `#if display` is used for code completion. In this case returning an
// empty string is good enough; We don't want to call git on every hint.
var commitBranch:String = "";
return macro $v{commitBranch};
#end
}
}
Executable
+318
View File
@@ -0,0 +1,318 @@
using Std;
import Common;
import haxe.Utf8;
import tools.ArrayTool;
class View extends sugoi.BaseView {
var t : sugoi.i18n.GetText;
public function new() {
super();
this.Std = Std;
this.Date = Date;
this.Web = sugoi.Web;
this.Lambda = Lambda;
this.VERSION = App.VERSION.toString();
this.ArrayTool = ArrayTool;
this.t = sugoi.i18n.Locale.texts;
}
public function count(i) {
return Lambda.count(i);
}
public function abs(n){
return Math.abs(n);
}
/**
* init view in main loop, just before rendering
*/
override function init() {
super.init();
//tuto widget display
var u = App.current.user;
if (u!=null && u.tutoState!=null) {
//trace("view init "+u.tutoState.name+" , "+u.tutoState.step);
this.displayTuto(u.tutoState.name, u.tutoState.step);
}
}
function getCurrentGroup(){
return App.current.getCurrentGroup();
}
function getUser(uid:Int):db.User {
return db.User.manager.get(uid, false);
}
function getProduct (pid:Int){
return db.Product.manager.get(pid, false);
}
/**
* Round a number to r digits after coma.
*
* @param n
* @param r
*/
public function roundTo(n:Float, r:Int):Float {
return Math.round(n * Math.pow(10,r)) / Math.pow(10,r) ;
}
public function color(id:Int) {
if (id == null) throw "color cant be null";
//try{
return intToHex(db.CategoryGroup.COLORS[id]);
//}catch (e:Dynamic) return "#000000";
}
/**
* convert a RVB color from Int to Hexa
* @param c
* @param leadingZeros=6
*/
public function intToHex(c:Int, ?leadingZeros=6):String {
var h = StringTools.hex(c);
while (h.length<leadingZeros)
h="0"+h;
return "#"+h;
}
/**
* Format prices
*/
public function formatNum(n:Float):String {
if (n == null) return "";
//round with 2 digits after comma
var out = Std.string(roundTo(n, 2));
//add a zero, 1,8-->1,80
if (out.indexOf(".")!=-1 && out.split(".")[1].length == 1) out = out +"0";
//french : replace point by comma
return out.split(".").join(",");
}
/**
* Price per Kg/Liter...
* @param qt
* @param unit
*/
public function pricePerUnit(price:Float,qt:Float, unit:Unit){
if (unit==null || qt == null || qt == 0 || price==null || price==0) return "";
var _price = price / qt;
var _unit = unit;
//turn small prices in Kg
if (_price < 1 ){
switch(unit){
case Gram:
_price *= 1000;
_unit = Kilogram;
case Centilitre:
_price *= 100;
_unit = Litre;
default :
}
}
return formatNum(_price) + "&nbsp;" + currency() + "/" + this.unit(_unit);
}
/**
* clean numbers in views
* to avoid bugs like : 13.79 - 13.79 = 1.77635683940025e-15
*/
public function numClean(f:Float):Float{
return Math.round(f * 100) / 100;
}
/**
* max length for strings, usefull for tables
*/
public function short(text:String, length:Int){
if (Utf8.length(text) > length){
return Utf8.sub(text,0, length)+"";
}else{
return text;
}
}
public function isToday(d:Date) {
var n = Date.now();
return d.getDate() == n.getDate() && d.getMonth() == n.getMonth() && d.getFullYear() == n.getFullYear();
}
/**
* Prints a measuring unit
*/
public function unit(u:Unit,?plural=false){
t = sugoi.i18n.Locale.texts;
return switch(u){
case Kilogram: t._("Kg.||kilogramms");
case Gram: t._("g.||gramms");
case null,Piece: if(plural) t._("pieces||unit of a product)") else t._("piece||unit of a product)");
case Litre: t._("L.||liter");
case Centilitre: t._("cl.||centiliter");
}
}
public function currency(){
if (App.current.user == null || App.current.user.amap == null){
return "";
}else{
return App.current.user.amap.getCurrency();
}
}
public static var DAYS = null;
public static var MONTHS = null;
public static var HOURS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23];
public static var MINUTES = [0,5,10,15,20,25,30,35,40,45,50,55];
public function initDate(){
t = sugoi.i18n.Locale.texts;
DAYS = [t._("Sunday"), t._("Monday"), t._("Tuesday"), t._("Wednesday"), t._("Thursday"), t._("Friday"), t._("Saturday")];
MONTHS = [t._("January"), t._("February"), t._("March"), t._("April"), t._("May"), t._("June"), t._("July"), t._("August"), t._("September"), t._("October"), t._("November"), t._("December")];
this.DAYS = DAYS;
this.MONTHS = MONTHS;
this.HOURS = HOURS;
this.MINUTES = MINUTES;
}
/**
* human readable date + time
*/
public function hDate(date:Date):String {
if (date == null) return t._("no date set");
if (DAYS == null) initDate();
var out = DAYS[date.getDay()] + " " + date.getDate() + " " + MONTHS[date.getMonth()];
out += " " + date.getFullYear();
if ( date.getHours() != 0 || date.getMinutes() != 0){
out += " " + sugoi.i18n.Locale.texts._("at||time : at 12:30") + " " + StringTools.lpad(Std.string(date.getHours()), "0", 2) + ":" + StringTools.lpad(Std.string(date.getMinutes()), "0", 2);
}
return out;
}
/**
* Human readable hour
*/
public function hHour(date:Date){
return StringTools.lpad(date.getHours().string(), "0", 2) + ":" + StringTools.lpad(date.getMinutes().string(), "0", 2);
}
public function oHour(hour:Int,min:Int){
return StringTools.lpad(hour.string(), "0", 2) + ":" + StringTools.lpad(min.string(), "0", 2);
}
/**
* human readable date
*/
public function dDate(date:Date):String {
if (date == null) return t._("no date set");
if (DAYS == null) initDate();
return DAYS[date.getDay()] + " " + date.getDate() + " " + MONTHS[date.getMonth()] + " " + date.getFullYear();
}
public function getDate(date:Date) {
if (date == null) throw "date is null";
if (DAYS == null) initDate();
return {
dow: DAYS[date.getDay()],
d : date.getDate(),
m: MONTHS[date.getMonth()],
y: date.getFullYear(),
h: StringTools.lpad(Std.string(date.getHours()),"0",2),
i: StringTools.lpad(Std.string(date.getMinutes()),"0",2)
};
}
public function getProductImage(e):String {
return Std.string(e).substr(2).toLowerCase()+".png";
}
public function prepare(orders:Iterable<db.UserContract>){
return service.OrderService.prepare(orders);
}
public function displayTuto(tuto:String, step:Int) {
if (tuto == null) return;
var t = plugin.Tutorial.all().get(tuto);
//check if we are on the correct page (last step page)
//otherwise the popovers could be displayed on wrong elements
var previous = t.steps[step - 1];
if (previous != null) {
switch(previous.action) {
case TAPage(uri):
var here = sugoi.Web.getURI();
if (!plugin.Tutorial.match(uri,here)) {
return;
}
default:
}
}
this.tuto = { name:tuto, step:step };
}
/**
* renvoie 0 si c'est user.firstName qui est connecté,
* renvoie 1 si c'est user.firstName2 qui est connecté
* @return
*/
public function whichUser():Int {
if (App.current.session.data == null) return 0;
return App.current.session.data.whichUser == null?0:App.current.session.data.whichUser;
}
public function isAmap(){
return App.current.user.amap.groupType == db.Amap.GroupType.Amap;
}
public function getBasket(userId, placeId, date){
var user = getUser(userId);
var place = db.Place.manager.get(placeId, false);
return db.Basket.getOrCreate(user, place, date);
}
public function getPlatform(){
return #if neko "Neko" #else "PHP" #end ;
}
/**
* Smart quantity (tm) : displays human readable quantity
* 0.33 x Lemon 12kg => 2kg Lemon
*/
public function smartQt(orderQt:Float, productQt:Float, unit:Unit):String{
if (orderQt == null) orderQt = 1;
if (productQt == null) productQt = 1;
if (unit == null) unit = Unit.Piece;
if (unit == Unit.Piece && productQt == 1 ){
return this.formatNum(orderQt);
}else{
return this.formatNum(orderQt * productQt) + "&nbsp;" + this.unit(unit,orderQt*productQt>1);
}
}
}
+100
View File
@@ -0,0 +1,100 @@
package controller;
import sugoi.form.Form;
class Account extends Controller
{
public function new()
{
super();
}
function doDefault() {
}
@tpl('form.mtt')
function doEdit() {
var form = sugoi.form.Form.fromSpod(app.user);
form.removeElement(form.getElement("lang"));
form.removeElement(form.getElement("pass"));
form.removeElement(form.getElement("rights"));
form.removeElement(form.getElement("cdate"));
form.removeElement(form.getElement("ldate"));
form.removeElement( form.getElement("apiKey") );
if (form.isValid()) {
if (app.user.id != form.getValueOf("id")) {
throw "access forbidden";
}
var admin = app.user.isAdmin();
form.toSpod(app.user);
//check email is valid
if (!sugoi.form.validators.EmailValidator.check(app.user.email)){
throw Error("/account/edit", t._("Email ::em:: is invalid", {em:app.user.email}));
}
if (app.user.email2!=null && !sugoi.form.validators.EmailValidator.check(app.user.email2)){
throw Error("/account/edit", t._("Email ::em:: is invalid", {em:app.user.email2}));
}
//check email is available
var sameEmail = db.User.getSameEmail(app.user.email,app.user.email2);
if( sameEmail.length > 0 && sameEmail.first().id!=app.user.id){
throw Error("/account/edit", t._("This email is already used by another account."));
}
if (!admin) { app.user.rights.unset(Admin); }
app.user.update();
throw Ok('/contract', t._("Your account has been updated"));
}
view.form = form;
view.title = t._("Modify my account");
}
function doQuit(){
if (checkToken()){
var name = app.user.amap.name;
var ua = db.UserAmap.get(app.user, app.user.amap,true);
ua.delete();
App.current.session.data.amapId = null;
throw Ok("/user/choose?show=1", t._("You left the group ::groupName::", {groupName:name}));
}
}
/**
* user payments history
*/
@tpl('account/payments.mtt')
function doPayments(){
var m = app.user;
var browse:Int->Int->List<Dynamic>;
//default display
browse = function(index:Int, limit:Int) {
return db.Operation.getOperationsWithIndex(m,app.user.amap,index,limit,true);
}
var count = db.Operation.countOperations(m,app.user.amap);
var rb = new sugoi.tools.ResultsBrowser(count, 10, browse);
view.rb = rb;
view.member = m;
view.balance = db.UserAmap.get(m,app.user.amap).balance;
}
}
+53
View File
@@ -0,0 +1,53 @@
package controller;
import db.UserContract;
import sugoi.form.Form;
class Amap extends Controller
{
public function new()
{
super();
}
@tpl("amap/default.mtt")
function doDefault() {
var contracts = db.Contract.getActiveContracts(app.user.amap, true, false);
for ( c in Lambda.array(contracts).copy()) {
if (c.endDate.getTime() < Date.now().getTime() ) contracts.remove(c);
}
view.contracts = contracts;
}
@tpl("form.mtt")
function doEdit() {
if (!app.user.isAmapManager()) throw t._("You don't have access to this section");
var group = app.user.amap;
var form = Form.fromSpod(group);
if (form.checkToken()) {
if(form.getValueOf("id") != app.user.amap.id) {
var editedGroup = db.Amap.manager.get(form.getValueOf("id"),false);
throw Error("/amap/edit",'Erreur, vous êtes en train de modifier "${editedGroup.name}" alors que vous êtes connecté à "${app.user.amap.name}"');
}
form.toSpod(group);
if (group.extUrl != null){
if ( group.extUrl.indexOf("http://") ==-1 && group.extUrl.indexOf("https://") ==-1 ){
group.extUrl = "http://" + group.extUrl;
}
}
group.update();
throw Ok("/amapadmin", t._("The group has been updated."));
}
view.form = form;
}
}
+367
View File
@@ -0,0 +1,367 @@
package controller;
import db.UserAmap;
import haxe.Http;
import neko.Web;
import sugoi.form.Form;
import Common;
import sugoi.form.elements.IntSelect;
import sugoi.form.elements.StringInput;
class AmapAdmin extends Controller
{
public function new()
{
super();
if (!app.user.isAmapManager()) throw Error("/", t._("Access forbidden"));
//lance un event pour demander aux plugins si ils veulent ajouter un item dans la nav
var nav = new Array<Link>();
if (app.user.amap.hasPayments()){
nav.push({id:"payments",link:"/amapadmin/payments",name: t._("Payments") });
}
var e = Nav(nav,"groupAdmin");
app.event(e);
view.nav = e.getParameters()[0];
}
@tpl("amapadmin/default.mtt")
function doDefault() {
view.membersNum = UserAmap.manager.count($amap == app.user.amap);
view.contractsNum = app.user.amap.getActiveContracts().length;
//ping cagette groups directory
if (Std.random(10) == 0 && app.user.amap.flags.has(db.Amap.AmapFlags.CagetteNetwork)){
var req = new Http("http://annuaire.cagette.net/api/ping?url="+StringTools.urlEncode( "http://" + App.config.HOST ) );
try{
req.request();
}catch (e:Dynamic){
App.current.logError("Error while contacting annuaire.cagette.net : "+e);
}
}
}
@tpl("amapadmin/addimage.mtt")
function doAddimage() {
if (!app.user.isAmapManager()) throw Error("/", t._("Access forbidden"));
var user = app.user;
view.image = user.amap.image;
var request = new Map();
try {
request = sugoi.tools.Utils.getMultipart(1024 * 1024 * 12); //12Mb
}catch (e:Dynamic) {
throw Error("/amapadmin", t._("The sent image was too big. The maximum allowed size is 12MB"));
}
if (request.exists("image")) {
//Image
var image = request.get("image");
if (image != null && image.length > 0) {
var img : sugoi.db.File = null;
if ( Sys.systemName() == "Windows") {
img = sugoi.db.File.create(request.get("image"), request.get("image_filename"));
}else {
img = sugoi.tools.UploadedImage.resizeAndStore(request.get("image"), request.get("image_filename"), 400, 400);
}
user.amap.lock();
if (user.amap.image != null) {
//delete previous file
user.amap.image.lock();
user.amap.image.delete();
}
user.amap.image = img;
user.amap.update();
throw Ok('/amapadmin/', t._("Image updated"));
}
}
}
@tpl("amapadmin/rights.mtt")
public function doRights() {
//liste les gens qui ont des droits dans le groupe
var users = db.UserAmap.manager.search($rights != null && $amap == app.user.amap, false);
//cleaning
for ( u in Lambda.array(users)) {
//rights peut etre null (null seralisé) et pas null en DB
if (u.rights == null || u.rights.length == 0) {
u.lock();
Reflect.setField(u, "rights", null);
u.update();
users.remove(u);
continue;
}
//rights on a deleted contract
for ( r in u.rights) {
switch(r) {
case ContractAdmin(cid):
if (cid == null) continue;
var c = db.Contract.manager.get(cid);
if (c == null) {
u.lock();
u.removeRight(r);
u.update();
}
default :
}
}
}
view.users = users;
}
@tpl("form.mtt")
public function doEditRight(?u:db.User) {
var form = new sugoi.form.Form("editRight");
if (u == null) {
form.addElement( new IntSelect("user", t._("Member") , app.user.amap.getMembersFormElementData(), null, true) );
}
var data = [];
//for (r in db.UserAmap.Right.getConstructors()) {
//if (r == "ContractAdmin") continue; //managed later
//data.push({label:r,value:r});
//}
data.push({label:t._("Group administrator"), value:"GroupAdmin"});
data.push({label:t._("Membership management"),value:"Membership"});
data.push({label:t._("Messages"),value:"Messages"});
var ua : db.UserAmap = null;
var populate :Array<String> = null;
if (u != null) {
ua = db.UserAmap.get(u, app.user.amap, true);
if (ua == null) throw "no user";
if (ua.rights == null) ua.rights = [];
//populate form
populate = ua.rights.map(function(x) return x.getName());
}
form.addElement( new sugoi.form.elements.CheckboxGroup("rights", t._("Rights"), data, populate, true, true) );
form.addElement( new sugoi.form.elements.Html("html","<hr/>"));
//Rights on contracts
var data = [];
var populate :Array<String> = [];
data.push({value:"contractAll",label:t._("All contracts")});
for (r in app.user.amap.getActiveContracts(true)) {
data.push( { label:r.name , value:"contract"+Std.string(r.id) } );
}
if(ua!=null && ua.rights!=null){
for ( r in ua.rights) {
switch(r) {
case Right.ContractAdmin(cid):
if (cid == null) {
populate.push("contractAll");
}else {
populate.push("contract"+cid);
}
default://
}
}
}
form.addElement( new sugoi.form.elements.CheckboxGroup("rights", t._("Contracts management") , data, populate, true, true) );
if (form.checkToken()) {
var wasManager = app.user.isAmapManager();
if (u == null) {
ua = db.UserAmap.manager.select($userId == Std.parseInt(form.getValueOf("user")) && $amapId == app.user.amap.id, true);
}
ua.rights = [];
var arr : Array<String> = cast form.getElement("rights").value;
for ( r in arr) {
if (r.substr(0, 8) == "contract") {
if (r == "contractAll") {
ua.rights.push( Right.ContractAdmin() );
}else {
ua.rights.push( Right.ContractAdmin(Std.parseInt(r.substr(8)) ) );
}
}else {
ua.rights.push( db.UserAmap.Right.createByName(r) );
}
}
//avoid "cut my own hands" problem
if (ua.user.id == app.user.id && wasManager ) {
var isManager = false;
for ( r in ua.rights) {
if (r.equals(db.UserAmap.Right.GroupAdmin)) {
isManager = true;
break;
}
}
if (isManager == false) {
throw Error("/amapadmin/rights", t._("You cannot strip yourself of admin rights."));
}
}
if (ua.rights.length == 0) ua.rights = null;
ua.update();
if (ua.rights == null) {
throw Ok("/amapadmin/rights", t._("Rights removed"));
}else {
throw Ok("/amapadmin/rights", t._("Rights created or modified"));
}
}
if (u == null) {
view.title = t._("Give rights to a user");
}else {
view.title = t._("Modify the rights of ::user::",{user:u.getName()});
}
view.form = form;
}
@tpl('form.mtt')
public function doVatRates() {
var f = new sugoi.form.Form("vat");
var a = app.user.amap;
if (a.vatRates == null) {
a.lock();
var x = new db.Amap();
a.vatRates = x.vatRates;
a.update();
}
var i = 1;
for (k in a.vatRates.keys()) {
f.addElement(new StringInput(i+"-k", t._("Name ")+i, k));
f.addElement(new StringInput(i + "-v", t._("Rate ")+i, Std.string(a.vatRates.get(k)) ));
//f.addElement(new sugoi.form.elements.Html("<hr/>"));
i++;
}
var j = i;
for (x in 0...5 - i) {
f.addElement(new StringInput(i+"-k", t._("Name ")+i, ""));
f.addElement(new StringInput(i + "-v", t._("Rate ")+i, ""));
//f.addElement(new sugoi.form.elements.Html("<hr/>"));
i++;
}
if (f.isValid()) {
var d = f.getData();
var vats = new Map<String,Float>();
var filter = new sugoi.form.filters.FloatFilter();
for (i in 1...5) {
if (d.get(i + "-k") == null) continue;
vats.set(d.get(i + "-k"), filter.filter( d.get(i + "-v")) );
}
a.lock();
a.vatRates = vats;
a.update();
throw Ok("/amapadmin", t._("Rate updated"));
}
view.title = t._("Edit VAT rates");
view.form = f;
}
function doCategories(d:haxe.web.Dispatch) {
d.dispatch(new controller.Categories());
}
/**
* Set up group currency. Default is EURO
*/
@tpl("form.mtt")
function doCurrency(){
view.title = t._("Currency used by your group.");
var f = new sugoi.form.Form("curr");
f.addElement(new sugoi.form.elements.StringInput("currency", t._("Currency symbol"), app.user.amap.getCurrency()));
f.addElement(new sugoi.form.elements.StringInput("currencyCode", t._("3 digit ISO code"), app.user.amap.currencyCode));
if ( f.isValid()){
app.user.amap.lock();
app.user.amap.currency = f.getValueOf("currency");
app.user.amap.currencyCode = f.getValueOf("currencyCode");
app.user.amap.update();
throw Ok("/amapadmin/currency", t._("Currency updated"));
}
view.form = f;
}
/**
* payment configuration
*/
@tpl("form.mtt")
function doPayments(){
var f = new sugoi.form.Form("paymentTypes");
var types = service.PaymentService.getAllPaymentTypes();
var formdata = [for (t in types){label:t.name, value:t.type}];
var selected = app.user.amap.allowedPaymentsType;
f.addElement(new sugoi.form.elements.CheckboxGroup("paymentTypes", t._("Authorized payment types"),formdata, selected) );
if (app.user.amap.checkOrder == ""){
app.user.amap.lock();
app.user.amap.checkOrder = app.user.amap.name;
app.user.amap.update();
}
f.addElement( new sugoi.form.elements.StringInput("checkOrder", t._("Make the check payable to"), app.user.amap.checkOrder, false));
f.addElement( new sugoi.form.elements.StringInput("IBAN", t._("IBAN of your bank account for transfers"), app.user.amap.IBAN, false));
f.addElement(new sugoi.form.elements.Checkbox("allowMoneyPotWithNegativeBalance", t._("Allow money pots with negative balance"), app.user.amap.allowMoneyPotWithNegativeBalance));
if (f.isValid()){
var p = f.getValueOf("paymentTypes");
var a = app.user.amap;
a.lock();
a.allowedPaymentsType = p;
a.checkOrder = f.getValueOf("checkOrder");
a.IBAN = f.getValueOf("IBAN");
a.allowMoneyPotWithNegativeBalance = f.getValueOf("allowMoneyPotWithNegativeBalance");
a.update();
throw Ok("/amapadmin/payments", t._("Payment options updated"));
}
view.title = t._("Options of payment");
view.form = f;
}
}
+111
View File
@@ -0,0 +1,111 @@
package controller;
import db.UserAmap;
import haxe.Json;
import neko.Web;
/**
* REST JSON API
*
* @author fbarbut
*/
class Api extends Controller
{
/**
* Public infos about this Cagette.net installation
*/
public function doDefault(){
var json : Dynamic = {
version:App.VERSION.toString(),
debug:App.config.DEBUG,
email:App.config.get("webmaster_email"),
groups:[]
};
for ( g in db.Amap.manager.all()){
//a strange way to exclude "test" accounts
if ( UserAmap.manager.count($amapId == g.id) > 20){
var place = g.getMainPlace();
var d = {
name:g.name,
cagetteNetwork:g.flags.has(db.Amap.AmapFlags.CagetteNetwork),
id:g.id,
url:"http://" + Web.getHostName() + "/group/" + g.id,
membersNum : g.getMembersNum(),
contracts: Lambda.array(Lambda.map(g.getActiveContracts(false), function(c) return c.name)),
place : {name:place.name, address1:place.address1,address2:place.address2,zipCode:place.zipCode,city:place.city }
};
json.groups.push(d);
}
}
Sys.print( Json.stringify(json) );
}
/*public function doError(){
sugoi.Web.setReturnCode(403);
}*/
#if plugins
//cagette-pro
public function doPro(d:haxe.web.Dispatch) {
d.dispatch(new pro.controller.api.Main());
}
#end
public function doShop(d:haxe.web.Dispatch) {
d.dispatch(new controller.api.Shop());
}
public function doOrder(d:haxe.web.Dispatch) {
d.dispatch(new controller.api.Order());
}
/**
* Get distribution planning for this group
*
* @param group
*/
public function doPlanning(group:db.Amap){
var contracts = group.getActiveContracts(true);
var cids = Lambda.map(contracts, function(p) return p.id);
var now = Date.now();
var now = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
var twoMonths = new Date(now.getFullYear(), now.getMonth()+2, now.getDate(), 0, 0, 0);
var distribs = db.Distribution.manager.search(($contractId in cids) && ($date >= now) && ($date<=twoMonths), { orderBy:date }, false);
var out = new Array<{id:Int,start:Date,end:Date,contract:String,contractId:Int,place:Dynamic}>();
for ( d in distribs){
var place = d.place;
var p = {name:place.name, address1:place.address1,address2:place.address2,zipCode:place.zipCode,city:place.city }
out.push({id:d.id,start:d.date,end:d.end,contract:d.contract.name,contractId:d.contract.id,place:p});
}
Sys.print(Json.stringify(out));
}
public function doUser(d:haxe.web.Dispatch){
d.dispatch(new controller.api.User());
}
public function doGroup(d:haxe.web.Dispatch){
d.dispatch(new controller.api.Group());
}
public function doProduct(d:haxe.web.Dispatch){
d.dispatch(new controller.api.Product());
}
}
+160
View File
@@ -0,0 +1,160 @@
package controller ;
class Categories extends controller.Controller
{
@tpl("categories/default.mtt")
public function doDefault() {
view.groups = db.CategoryGroup.manager.search($amap == app.user.amap, false);
checkToken();
}
/**
* genere le set par défaut de catégories
*/
public function doGenerate() {
if ( db.CategoryGroup.manager.search($amap == app.user.amap, false).length != 0) {
throw Error("/amapadmin/categories", t._("The category list is not empty.") );
}
function gen(catGroupName:String,color:Int,cats:Array<String>) {
var cg = new db.CategoryGroup();
cg.name = catGroupName;
cg.color = color;
cg.amap = app.user.amap;
cg.insert();
for (c in cats) {
var x = new db.Category();
x.categoryGroup = cg;
x.name = c;
x.insert();
}
}
var t = sugoi.i18n.Locale.texts;
gen(t._("Product types"),2, [t._("Vegetables"), t._("Fruits"), t._("Fish"), t._("Red meat"), t._("Breads"), t._("Grocery"), t._("Beverages") ]);
gen(t._("Labels"),0, [t._("Certified organic agriculture"), t._("Uncertified organic agriculture"), t._("Non organic") ]);
throw Ok("/amapadmin/categories", t._("Default categories have been created") );
}
/**
* modifie un groupe de categories
*/
@tpl('form.mtt')
function doEditGroup(g:db.CategoryGroup) {
var form = sugoi.form.Form.fromSpod(g);
form.removeElementByName("color");
form.removeElementByName("amapId");
form.addElement(new form.ColorRadioGroup("color", t._("Color") , Std.string(g.color) ));
if (form.isValid()) {
form.toSpod(g);
g.update();
throw Ok("/amapadmin/categories", t._("Group modified"));
}
view.title = t._("Modify the group ") + g.name;
view.form = form;
}
@tpl('form.mtt')
function doInsertGroup() {
var g = new db.CategoryGroup();
var form = sugoi.form.Form.fromSpod(g );
form.removeElementByName("color");
form.removeElementByName("amapId");
form.addElement(new form.ColorRadioGroup("color", "Couleur", Std.string(g.color)));
if (form.isValid()) {
form.toSpod(g);
g.amap = app.user.amap;
g.insert();
throw Ok("/amapadmin/categories", t._("Group added"));
}
view.title = t._("Create a group of categories");
view.form = form;
}
@tpl('form.mtt')
function doInsert(g:db.CategoryGroup) {
var c = new db.Category();
var form = sugoi.form.Form.fromSpod(c);
form.removeElementByName("categoryGroupId");
if (form.isValid()) {
form.toSpod(c);
c.categoryGroup = g;
c.insert();
throw Ok("/amapadmin/categories", t._("Category added"));
}
view.title = t._("Create a category");
view.form = form;
}
@tpl('form.mtt')
function doEdit(c:db.Category) {
var form = sugoi.form.Form.fromSpod(c);
form.removeElementByName("categoryGroupId");
if (form.isValid()) {
form.toSpod(c);
c.update();
throw Ok("/amapadmin/categories","Category modified");
}
view.title = t._("Modify the category ") + c.name;
view.form = form;
}
function doDeleteGroup(g:db.CategoryGroup,args:{token:String}) {
if ( checkToken()) {
if (g.getCategories().length > 0) throw Error("/amapadmin/categories", t._("All categories must be removed from this group before it can be deleted."));
g.lock();
g.delete();
throw Ok("/amapadmin/categories", t._("Group deleted"));
}else {
throw Redirect("/amapadmin/categories");
}
}
function doDelete(c:db.Category,args:{token:String}) {
if ( checkToken()) {
c.lock();
c.delete();
throw Ok("/amapadmin/categories", t._("Category deleted"));
}else {
throw Redirect("/amapadmin/categories");
}
}
}
+619
View File
@@ -0,0 +1,619 @@
package controller;
import db.UserContract;
import sugoi.form.elements.DateDropdowns;
import sugoi.form.elements.Input;
import sugoi.form.elements.Selectbox;
import sugoi.form.Form;
import db.Contract;
import Common;
import plugin.Tutorial;
using Std;
import service.OrderService;
class Contract extends Controller
{
public function new()
{
super();
}
@tpl("contract/view.mtt")
public function doView(c:db.Contract) {
view.category = 'amap';
view.c = c;
}
/**
* "my account" page
*/
@tpl("contract/default.mtt")
function doDefault() {
//Create the list of links to change the language
var langs = App.config.get("langs").split(";");
var langNames = App.config.get("langnames").split(";");
var i=0;
var langLinks = "";
for (lang in langs)
{
langLinks += "<li><a href=\"?lang=" + langs[i] + "\">" + langNames[i] + "</a></li>";
i++;
}
view.langLinks = langLinks;
view.langText = langNames[langs.indexOf(app.session.lang)];
//change account lang
if (app.params.exists("lang") && app.user!=null){
app.user.lock();
app.user.lang = app.params.get("lang");
app.user.update();
}
var ua = db.UserAmap.get(app.user, app.user.amap);
if (ua == null) throw Error("/", t._("You are not a member of this group"));
var constOrders = null;
var varOrders = new Map<String,Array<db.UserContract>>();
var a = App.current.user.amap;
var oneMonthAgo = DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 30);
//constant orders
var contracts = db.Contract.manager.search($type == db.Contract.TYPE_CONSTORDERS && $amap == a && $endDate > oneMonthAgo, false);
constOrders = [];
for ( c in contracts){
var orders = app.user.getOrdersFromContracts([c]);
if (orders.length == 0) continue;
constOrders.push({contract:c, orders:service.OrderService.prepare(orders) });
}
//variable orders, grouped by date
var contracts = db.Contract.manager.search($type == db.Contract.TYPE_VARORDER && $amap == a && $endDate > oneMonthAgo, false);
for (c in contracts) {
var ds = c.getDistribs(false);
for (d in ds) {
//store orders in a stringmap like "2015-01-01" => [order1,order2,...]
var k = d.date.toString().substr(0, 10);
var orders = app.user.getOrdersFromDistrib(d);
if (orders.length > 0) {
if (!varOrders.exists(k)) {
varOrders.set(k, Lambda.array(orders));
}else {
var z = varOrders.get(k).concat(Lambda.array(orders));
varOrders.set(k, z);
}
}
}
}
//final structure
var varOrders2 = new Array<{date:Date,orders:Array<UserOrder>}>();
for ( k in varOrders.keys()) {
var d = new Date(k.split("-")[0].parseInt(), k.split("-")[1].parseInt() - 1, k.split("-")[2].parseInt(), 0, 0, 0);
var orders = service.OrderService.prepare( Lambda.list(varOrders[k]) );
varOrders2.push({date:d,orders:orders});
}
//sort by date desc
varOrders2.sort(function(b, a) {
return Math.round(a.date.getTime()/1000)-Math.round(b.date.getTime()/1000);
});
view.varOrders = varOrders2;
view.constOrders = constOrders;
// tutorials
if (app.user.isAmapManager()) {
//actions
if (app.params.exists('startTuto') ) {
//start a tuto
app.user.lock();
var t = app.params.get('startTuto');
app.user.tutoState = {name:t,step:0};
app.user.update();
}
//tuto state
var tutos = new Array<{name:String,completion:Float,key:String}>();
for ( k in Tutorial.all().keys() ) {
var t = Tutorial.all().get(k);
var completion = null;
if (app.user.tutoState!=null && app.user.tutoState.name == k) completion = app.user.tutoState.step / t.steps.length;
tutos.push( { name:t.name, completion:completion , key:k } );
}
view.tutos = tutos;
}
//should be able to stop tuto in any case
if (app.params.exists('stopTuto')) {
//stopped tuto from a tuto window
app.user.lock();
app.user.tutoState = null;
app.user.update();
view.stopTuto = true;
}
checkToken();
view.userAmap = ua;
}
/**
* Edit a contract
*/
@tpl("form.mtt")
function doEdit(c:db.Contract) {
view.category = 'contractadmin';
if (!app.user.isContractManager(c)) throw Error('/', t._("Forbidden action"));
view.title = t._("Edit contract ::contractName::",{contractName:c.name});
var group = c.amap;
var currentContact = c.contact;
var form = Form.fromSpod(c);
form.removeElement( form.getElement("amapId") );
form.removeElement(form.getElement("type"));
form.getElement("userId").required = true;
app.event(EditContract(c,form));
if (form.checkToken()) {
form.toSpod(c);
c.amap = group;
//checks & warnings
if (c.hasPercentageOnOrders() && c.percentageValue==null) throw Error("/contract/edit/"+c.id, t._("If you would like to add fees to the order, define a rate (%) and a label."));
if (c.hasStockManagement()) {
for (p in c.getProducts()) {
if (p.stock == null) {
app.session.addMessage(t._("Warning about management of stock. Please fill the field \"stock\" for all your products"), true);
break;
}
}
}
//no stock mgmt for constant orders
if (c.hasStockManagement() && c.type==db.Contract.TYPE_CONSTORDERS) {
c.flags.unset(ContractFlags.StockManagement);
app.session.addMessage(t._("Managing stock is not available for CSA contracts"), true);
}
c.update();
//update rights
if ( c.contact != null && (currentContact==null || c.contact.id!=currentContact.id) ) {
var ua = db.UserAmap.get(c.contact, app.user.amap, true);
ua.giveRight(ContractAdmin(c.id));
ua.giveRight(Messages);
ua.giveRight(Membership);
ua.update();
//remove rights to old contact
if (currentContact != null) {
var x = db.UserAmap.get(currentContact, c.amap, true);
if (x != null) {
x.removeRight(ContractAdmin(c.id));
x.update();
}
}
}
throw Ok("/contractAdmin/view/"+c.id, t._("Contract updated"));
}
view.form = form;
}
@tpl("contract/insertChoose.mtt")
function doInsertChoose() {
//checkToken();
}
/**
* Créé un nouveau contrat
*/
@tpl("form.mtt")
function doInsert(?type:Int) {
if (!app.user.canManageAllContracts()) throw Error('/', t._("Forbidden action"));
if (type == null) throw Redirect('/contract/insertChoose');
view.title = if (type == db.Contract.TYPE_CONSTORDERS)t._("Create a contract with fixed orders") else t._("Create a contract with variable orders");
var c = new db.Contract();
var form = Form.fromSpod(c);
form.removeElement( form.getElement("amapId") );
form.removeElement(form.getElement("type"));
form.getElement("userId").required = true;
if (form.checkToken()) {
form.toSpod(c);
c.amap = app.user.amap;
//trace(app.user.amap);
//trace(c.amap);
c.type = type;
c.insert();
//right
if (c.contact != null) {
var ua = db.UserAmap.get(c.contact, app.user.amap, true);
ua.giveRight(ContractAdmin(c.id));
ua.giveRight(Messages);
ua.giveRight(Membership);
ua.update();
}
throw Ok("/contractAdmin/view/"+c.id, t._("New contract created"));
}
view.form = form;
}
/**
* Delete a contract (... and its products, orders & distributions)
*/
function doDelete(c:db.Contract) {
if (!app.user.canManageAllContracts()) throw Error("/contractAdmin", t._("You don't have the authorization to remove a contract"));
if (checkToken()) {
c.lock();
//check if there is orders in this contract
var products = c.getProducts();
var orders = db.UserContract.manager.search($productId in Lambda.map(products, function(p) return p.id));
var qt = 0.0;
for ( o in orders) qt += o.quantity; //there could be "zero c qt" orders
if (qt > 0) {
throw Error("/contractAdmin", t._("You cannot delete this contract because some orders are linked to it."));
}
//remove admin rights and delete contract
if(c.contact!=null){
var ua = db.UserAmap.get(c.contact, c.amap, true);
if (ua != null) {
ua.removeRight(ContractAdmin(c.id));
ua.update();
}
}
app.event(DeleteContract(c));
c.delete();
throw Ok("/contractAdmin", t._("Contract deleted"));
}
throw Error("/contractAdmin", t._("Token error"));
}
/**
* Make an order by contract ( standard mode )
* The form is prepopulated if orders have already been made.
*
* It should work for constant orders ( will display one column )
* or varying orders ( with as many columns as distributions dates )
*
*/
@tpl("contract/order.mtt")
function doOrder(c:db.Contract ) {
//checks
if (app.user.amap.hasPayments()) throw Redirect("/contract/orderAndPay/" + c.id);
if (app.user.amap.hasShopMode()) throw Redirect("/shop");
if (!c.isUserOrderAvailable()) throw Error("/", t._("This contract is not opened for orders"));
var distributions = [];
// If its a varying contract, we display a column by distribution
if (c.type == db.Contract.TYPE_VARORDER) {
distributions = db.Distribution.getOpenToOrdersDeliveries(c);
}else{
distributions = [null];
}
//list of distribs with a list of product and optionnaly an order
var userOrders = new Array< {distrib:db.Distribution,datas:Array<{order:db.UserContract,product:db.Product}>} >();
var products = c.getProducts();
if ( c.type == db.Contract.TYPE_VARORDER ){
for ( d in distributions){
var datas = [];
for ( p in products) {
var ua = { order:null, product:p };
var order = db.UserContract.manager.select($user == app.user && $productId == p.id && $distributionId==d.id, true);
if (order != null) ua.order = order;
datas.push(ua);
}
userOrders.push({distrib:d,datas:datas});
}
}else{
var datas = [];
for ( p in products) {
var ua = { order:null, product:p };
var order = db.UserContract.manager.select($user == app.user && $productId == p.id, true);
if (order != null) ua.order = order;
datas.push(ua);
}
userOrders.push({distrib:null,datas:datas});
}
//form check
if (checkToken()) {
//get dsitrib if needed
//var distrib : db.Distribution = null;
//if (c.type == db.Contract.TYPE_VARORDER) {
//distrib = db.Distribution.manager.get(Std.parseInt(app.params.get("distribution")), false);
//}
for (k in app.params.keys()) {
if (k.substr(0, 1) != "d") continue;
var qt = app.params.get(k);
if (qt == "") continue;
var pid = null;
var did = null;
try{
pid = Std.parseInt(k.split("-")[1].substr(1));
did = Std.parseInt(k.split("-")[0].substr(1));
}catch (e:Dynamic){trace("unable to parse key "+k); }
//find related element in userOrders
var uo = null;
for ( x in userOrders){
if (x.distrib!=null && x.distrib.id != did) {
continue;
}else{
for (a in x.datas){
if (a.product.id == pid){
uo = a;
break;
}
}
}
}
if (uo == null) throw t._("Could not find the product ::produ:: and delivery ::deliv::", {produ:pid, deliv:did});
var q = 0.0;
if (uo.product.hasFloatQt ) {
var param = StringTools.replace(qt, ",", ".");
q = Std.parseFloat(param);
}else {
q = Std.parseInt(qt);
}
if (uo.order != null) {
OrderService.edit(uo.order, q);
}else {
OrderService.make(app.user, q, uo.product, did);
}
}
throw Ok("/contract/order/"+c.id, t._("Your order has been updated"));
}
view.c = view.contract = c;
view.userOrders = userOrders;
}
/**
* Make an order by contract ( standard mode ) + payment process
*/
@tpl("contract/orderAndPay.mtt")
function doOrderAndPay(c:db.Contract ) {
//checks
if (!app.user.amap.hasPayments()) throw Redirect("/contract/order/" + c.id);
if (app.user.amap.hasShopMode()) throw Redirect("/");
if (!c.isUserOrderAvailable()) throw Error("/", t._("This contract is not opened for orders"));
var distributions = [];
/* If its a varying contract, we display a column by distribution*/
if (c.type == db.Contract.TYPE_VARORDER) {
distributions = db.Distribution.getOpenToOrdersDeliveries(c);
}
//list of distribs with a list of product and optionnaly an order
var userOrders = new Array< {distrib:db.Distribution,datas:Array<{order:db.UserContract,product:db.Product}>} >();
var products = c.getProducts();
for ( d in distributions){
var datas = [];
for ( p in products) {
var ua = { order:null, product:p };
var order : db.UserContract = null;
if (c.type == db.Contract.TYPE_VARORDER) {
order = db.UserContract.manager.select($user == app.user && $productId == p.id && $distributionId==d.id, true);
}else {
order = db.UserContract.manager.select($user == app.user && $productId == p.id, true);
}
if (order != null) ua.order = order;
datas.push(ua);
}
userOrders.push({distrib:d,datas:datas});
}
//form check
if (checkToken()) {
//get distrib if needed
var distrib = null;
if (c.type == db.Contract.TYPE_VARORDER) {
distrib = db.Distribution.manager.get(Std.parseInt(app.params.get("distribution")), false);
}
var orders : OrderInSession = {products:[],userId:app.user.id,total:0};
for (k in app.params.keys()) {
if (k.substr(0, 1) != "d") continue;
var qt = app.params.get(k);
if (qt == "") continue;
var pid = null;
var did = null;
try{
pid = Std.parseInt(k.split("-")[1].substr(1));
did = Std.parseInt(k.split("-")[0].substr(1));
}catch (e:Dynamic){
trace("unable to parse key "+k);
}
//find related element in userOrders
var uo = null;
for ( x in userOrders){
if (x.distrib!=null && x.distrib.id != did) {
continue;
}else{
for (a in x.datas){
if (a.product.id == pid){
uo = a;
break;
}
}
}
}
if (uo == null) throw t._("Could not find the product ::produ:: and delivery ::deliv::", {produ:pid, deliv:did});
//quantity
var q = 0.0;
if (uo.product.hasFloatQt ) {
var param = StringTools.replace(qt, ",", ".");
q = Std.parseFloat(param);
}else {
q = Std.parseInt(qt);
}
orders.products.push({productId:pid, quantity:q, distributionId:did});
var p = db.Product.manager.get(pid, false);
orders.total += p.getPrice() * q;
}
App.current.session.data.order = orders;
//Go to payments page
if (c.type == db.Contract.TYPE_CONSTORDERS) {
throw Ok("/contract/order/"+c.id, t._("Your CSA order has been saved"));
}else{
throw Ok("/transaction/pay/", t._("In order to save your order, please choose a means of payment."));
}
}
view.c = view.contract = c;
view.userOrders = userOrders;
}
/**
* A user edit an order for a multidistrib.
*/
@tpl("contract/orderByDate.mtt")
function doEditOrderByDate(date:Date) {
if (app.user.amap.hasPayments()) {
//when payments are active, the user cannot modify his order
throw Redirect("/");
}
// cannot edit order if date is in the past
if (Date.now().getTime() > date.getTime()) {
var msg = t._("This delivery has already taken place, you can no longer modify the order.");
if (app.user.isContractManager()) msg += t._("<br/>As the manager of the contract you can modify the order from this page: <a href='/contractAdmin'>Management of contracts</a>");
throw Error("/contract", msg);
}
// Il faut regarder le contrat de chaque produit et verifier si le contrat est toujours ouvert à la commande.
var d1 = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
var d2 = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59);
var cids = Lambda.map(app.user.amap.getActiveContracts(true), function(c) return c.id);
var distribs = db.Distribution.manager.search(($contractId in cids) && $date >= d1 && $date <=d2 , false);
var orders = db.UserContract.manager.search($userId==app.user.id && $distributionId in Lambda.map(distribs,function(d)return d.id) );
view.orders = service.OrderService.prepare(orders);
view.date = date;
//form check
if (checkToken()) {
var orders_out = [];
for (k in app.params.keys()) {
var param = app.params.get(k);
if (k.substr(0, "product".length) == "product") {
//trouve le produit dans userOrders
var pid = Std.parseInt(k.substr("product".length));
var order = Lambda.find(orders, function(uo) return uo.product.id == pid);
if (order == null) throw t._("Error, could not find the order");
var q = 0.0;
if (order.product.hasFloatQt ) {
param = StringTools.replace(param, ",", ".");
q = Std.parseFloat(param);
}else {
q = Std.parseInt(param);
}
var quantity = Math.abs( q==null?0:q );
if ( order.distribution.canOrderNow() ) {
//met a jour la commande
var o = OrderService.edit(order, quantity);
if(o!=null) orders_out.push( o );
}
}
}
app.event(MakeOrder(orders_out));
throw Ok("/contract", t._("Your order has been updated"));
}
}
}
+1027
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
package controller;
/**
* Base Cagette.net Controller
* @author fbarbut
*/
class Controller extends sugoi.BaseController
{
var t: sugoi.i18n.GetText;
public function new()
{
super();
//gettext translator
this.t = sugoi.i18n.Locale.texts;
}
public function checkIsLogged(){
if(app.user==null) {
throw new tink.core.Error(t._("You should be logged in to perform this action."));
}
}
}
+477
View File
@@ -0,0 +1,477 @@
package controller;
import sugoi.db.Cache;
import sugoi.Web;
import sugoi.mail.Mail;
import Common;
using Lambda;
using tools.DateTool;
class Cron extends Controller
{
public function doDefault()
{
}
/**
* CLI only en prod
*/
function canRun() {
if (App.current.user != null && App.current.user.isAdmin()){
return true;
}else if (App.config.DEBUG) {
return true;
}else {
if (Web.isModNeko) {
Sys.print("only CLI.");
return false;
}else {
return true;
}
}
}
public function doMinute() {
print("Cron.doMinute is called");
if (!canRun()) return;
app.event(MinutelyCron);
sendEmailsfromBuffer();
}
/**
* Hourly Cron
*
* this function can be locally tested with `neko index.n cron/hour > cron.log`
*/
public function doHour() {
app.event(HourlyCron);
distribNotif(4,db.User.UserFlags.HasEmailNotif4h); //4h before
distribNotif(24,db.User.UserFlags.HasEmailNotif24h); //24h before
distribNotif(0, db.User.UserFlags.HasEmailNotifOuverture); //on command open
distribValidationNotif();
//sendOrdersByProductWhenOrdersClose();
}
public function doDaily() {
if (!canRun()) return;
app.event(DailyCron);
//ERRORS MONITORING
var n = Date.now();
var yest24h = new Date(n.getFullYear(), n.getMonth(), n.getDate(), 0, 0, 0);
var yest0h = DateTools.delta(yest24h, -1000 * 60 * 60 * 24);
var errors = sugoi.db.Error.manager.search( $date < yest24h && $date > yest0h );
if (errors.length > 0) {
var report = new StringBuf();
report.add("<h1>" + App.config.NAME + " : ERRORS</h1>");
for (e in errors) {
report.add("<div><pre>"+e.error + " at URL " + e.url + " ( user : " + (e.user!=null?e.user.toString():"none") + ", IP : " + e.ip + ")</pre></div><hr/>");
}
var m = new Mail();
m.setSender(App.config.get("default_email"),"Cagette.net");
m.addRecipient(App.config.get("webmaster_email"));
m.setSubject(App.config.NAME+" Errors");
m.setHtmlBody( app.processTemplate("mail/message.mtt", { text:report.toString() } ) );
App.sendMail(m);
}
//DEMO CONTRATS deletion after 7 days ( see controller.Group.doCreate() )
db.Contract.manager.delete($name == "Contrat AMAP Maraîcher Exemple" && $startDate < DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 7));
db.Contract.manager.delete($name == "Contrat Poulet Exemple" && $startDate < DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 7));
//Old Messages cleaning
db.Message.manager.delete($date < DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 30 * 6));
//DB cleaning : I dont know how, but some people have empty string emails...
/*for ( u in db.User.manager.search($email == "", true)){
u.email = Std.random(9999) + "@cagette.net";
u.update();
}
for ( u in db.User.manager.search($email2 == "", true)){
u.email2 = null;
u.update();
}*/
}
/**
* Send email notifications to users before a distribution
* @param hour
* @param flag
*/
function distribNotif(hour:Int, flag:db.User.UserFlags) {
//trouve les distrib qui commencent dans le nombre d'heures demandé
//on recherche celles qui commencent jusqu'à une heure avant pour ne pas en rater
var from = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * (hour-1));
var to = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * hour);
//if (App.config.DEBUG) from = DateTools.delta(from, 1000.0 * 60 * 60 * 24 * -30);
// dans le cas HasEmailNotifOuverture la date à prendre est le orderStartDate
// et non pas date qui est la date de la distribution
var distribs;
if ( db.User.UserFlags.HasEmailNotifOuverture == flag )
distribs = db.Distribution.manager.search( $orderStartDate >= from && $orderStartDate <= to , false);
else
distribs = db.Distribution.manager.search( $date >= from && $date <= to , false);
//Sys.print("distribNotif "+hour+" from "+from+" to "+to+"<br/>\n");
//on s'arrete immédiatement si aucune distibution trouvée
if (distribs.length == 0) return;
//cherche plus tard si on a pas une "grappe" de distrib
/*while (true) {
var extraDistribs ;
if ( db.User.UserFlags.HasEmailNotifOuverture != flag )
extraDistribs = db.Distribution.manager.search( $date >= to && $date <DateTools.delta(to,1000.0*60*60) , false);
else
extraDistribs = db.Distribution.manager.search( $orderStartDate >= to && $orderStartDate <DateTools.delta(to,1000.0*60*60) , false);
for ( e in extraDistribs) distribs.add(e);
if (extraDistribs.length > 0) {
//on fait un tour de plus avec une heure plus tard
to = DateTools.delta(h, 1000.0 * 60 * 60);
}else {
//plus de distribs
break;
}
}*/
//on vérifie dans le cache du jour que ces distrib n'ont pas deja été traitées lors d'un cron précédent
var cacheId = Date.now().toString().substr(0, 10)+Std.string(flag);
var dist :Array<Int> = sugoi.db.Cache.get(cacheId);
if (dist != null) {
for (d in Lambda.array(distribs)) {
if (Lambda.exists(dist, function(x) return x == d.id)) {
// Comment this line in case of local test
distribs.remove(d);
}
}
}else {
dist = [];
}
//toutes les distribs trouvées ont deja été traitées
if (distribs.length == 0) return;
//stocke cache
for (d in distribs) dist.push(d.id);
Cache.set(cacheId, dist, 24 * 60 * 60);
//We have now the distribs we want to notify about.
var distribsByContractId = new Map<Int,db.Distribution>();
for (d in distribs) {
if (d == null || d.contract==null) continue;
distribsByContractId.set(d.contract.id, d);
}
//Boucle sur les distributions pour gerer le cas de plusieurs distributions le même jour sur le même contrat
var orders = [];
for (d in distribs) {
if (d == null || d.contract==null) continue;
//get orders for both type of contracts
for ( x in d.contract.getOrders(d)) orders.push(x);
}
/*
* Group orders by users-group to receive separate emails by groups for the same user.
* Map key is $userId-$groupId
*/
var users = new Map <String,{
user:db.User,
distrib:db.Distribution,
products:Array<db.UserContract>,
vendors:Array<db.Vendor>
}>();
for (o in orders) {
var x = users.get(o.user.id+"-"+o.product.contract.amap.id);
if (x == null) x = {user:o.user,distrib:null,products:[],vendors:[]};
x.distrib = distribsByContractId.get(o.product.contract.id);
x.products.push(o);
users.set(o.user.id+"-"+o.product.contract.amap.id, x);
//trace (o.userId+"-"+o.product.contract.amap.id, x);Sys.print("<br/>\n");
// Prévenir également le deuxième user en cas des commandes alternées
if (o.user2 != null) {
var x = users.get(o.user2.id+"-"+o.product.contract.amap.id);
if (x == null) x = {user:o.user2,distrib:null,products:[],vendors:[]};
x.distrib = distribsByContractId.get(o.product.contract.id);
x.products.push(o);
users.set(o.user2.id+"-"+o.product.contract.amap.id, x);
//trace (o.user2.id+"-"+o.product.contract.amap.id, x);Sys.print("<br/>\n");
}
}
//remove zero qt orders
for( k in users.keys()){
var x = users.get(k);
var total = 0.0;
for( o in x.products) total += o.quantity;
if(total==0.0) users.remove(k);
}
// Dans le cas de l'ouverture de commande, ce sont tous les users qu'il faut intégrer
if ( db.User.UserFlags.HasEmailNotifOuverture == flag )
{
for (d in distribs) {
var memberList = d.contract.amap.getMembers();
for (u in memberList) {
var x = users.get(u.id+"-"+d.contract.amap.id);
if (x == null) x = {user:u,distrib:null,products:[],vendors:[]};
x.distrib = distribsByContractId.get(d.contract.id);
x.vendors.push(d.contract.vendor);
users.set(u.id+"-"+d.contract.amap.id, x);
//print(u.id+"-"+d.contract.amap.id, x);
}
}
}
for ( u in users) {
if (u.user.flags.has(flag) ) {
if (u.user.email != null) {
var group = u.distrib.contract.amap;
this.t = sugoi.i18n.Locale.init(u.user.lang); //switch to the user language
var text;
if ( db.User.UserFlags.HasEmailNotifOuverture == flag )
{
//order opening notif
text = t._("Opening of orders for the delivery of <b>::date::</b>", {date:view.hDate(u.distrib.date)});
text += "<br/>";
text += t._("The following suppliers are involved :");
text += "<br/><ul>";
for ( v in u.vendors) {
text += "<li>" + v + "</li>";
}
text += "</ul>";
}else{
//Distribution notif to the users
var d = u.distrib;
text = t._("Do not forget the delivery on <b>::day::</b> from ::from:: to ::to::<br/>", {day:view.dDate(d.date),from:view.hHour(d.date),to:view.hHour(d.end)});
text += t._("Your products to collect :") + "<br/><ul>";
for ( p in u.products) {
text += "<li>"+p.quantity+" x "+p.product.getName();
// Gerer le cas des contrats en alternance
if (p.user2 != null) {
text += " " + t._("alternated with") + " ";
if (u.user == p.user)
text += p.user2.getCoupleName();
else
text += p.user.getCoupleName();
}
text += "</li>";
}
text += "</ul>";
}
if (u.distrib.isDistributor(u.user)) {
text += t._("<b>Warning: you are in charge of the delivery ! Do not forget to print the attendance sheet.</b>");
}
try{
var m = new Mail();
m.setSender(App.config.get("default_email"), "Cagette.net");
if(group.contact!=null) m.setReplyTo(group.contact.email, group.name);
m.addRecipient(u.user.email, u.user.getName());
if (u.user.email2 != null) m.addRecipient(u.user.email2);
m.setSubject( group.name+" : "+t._("Distribution on ::date::",{date:app.view.hDate(u.distrib.date)}) );
m.setHtmlBody( app.processTemplate("mail/message.mtt", { text:text,group:group } ) );
App.sendMail(m , u.distrib.contract.amap);
}catch (e:Dynamic){
app.logError(e); //email could be invalid
}
}
}
}
}
/**
* Check if there is a multi-distrib to validate.
*
* Autovalidate it after 10 days
*/
function distribValidationNotif(){
var now = Date.now();
var from = now.setHourMinute( now.getHours(), 0 );
var to = now.setHourMinute( now.getHours()+1 , 0);
var explain = t._("<p>This step is important in order to:</p>");
explain += t._("<ul><li>Update orders if delivered quantities are different from ordered quantities</li>");
explain += t._("<li>Confirm the reception of payments (checks, cash, transfers) in order to mark orders as 'paid'</li></ul>");
/*
* warn administrator if a distribution just ended
*/
var ds = db.Distribution.manager.search( !$validated && ($end >= from) && ($end < to) , false);
for ( d in Lambda.array(ds)){
if ( d.contract.type != db.Contract.TYPE_VARORDER ){
ds.remove(d);
}else if ( !d.contract.amap.hasPayments() ){
ds.remove(d);
}
}
var ds = tools.ObjectListTool.deduplicateDistribsByKey(ds);
var view = App.current.view;
for ( d in ds ){
// var subj = "["+d.contract.amap.name+"] " + t._("Validation of the ::date:: distribution",{date:view.hDate(d.date)});
var subj = t._("[::group::] Validation of the ::date:: distribution",{group : d.contract.amap.name , date : view.hDate(d.date)});
var url = "http://" + App.config.HOST + "/distribution/validate/"+d.date.toString().substr(0,10)+"/"+d.place.id;
var html = t._("<p>Your distribution just finished, don't forget to <b>validate</b> it</p>");
html += explain;
html += t._("<p><a href='::distriburl::'>Click here to validate the distribution</a> (You must be connected to your group Cagette)", {distriburl:url});
App.quickMail(d.contract.amap.contact.email, subj, html);
}
/*
* warn administrator if a distribution ended 3 days ago
*/
var from = now.setHourMinute( now.getHours() , 0 ).deltaDays(-3);
var to = now.setHourMinute( now.getHours()+1 , 0).deltaDays(-3);
//warn administrator if a distribution just ended
var ds = db.Distribution.manager.search( !$validated && ($end >= from) && ($end < to) , false);
for ( d in Lambda.array(ds)){
if ( d.contract.type != db.Contract.TYPE_VARORDER ){
ds.remove(d);
}else if ( !d.contract.amap.hasPayments() ){
ds.remove(d);
}
}
var ds = tools.ObjectListTool.deduplicateDistribsByKey(ds);
for ( d in ds ){
// var subj = d.contract.amap.name + t._(": Validation of the delivery of the ") + App.current.view.hDate(d.date);
var subj = t._("[::group::] Validation of the ::date:: distribution",{group : d.contract.amap.name , date : view.hDate(d.date)});
var url = "http://" + App.config.HOST + "/distribution/validate/"+d.date.toString().substr(0,10)+"/"+d.place.id;
var html = t._("<p>Reminder: you have a delivery to validate.</p>");
html += explain;
html += t._("<p><a href='::distriburl::'>Click here to validate the delivery</a> (You must be connected to your Cagette group)", {distriburl:url});
App.quickMail(d.contract.amap.contact.email, subj, html);
}
/*
* Autovalidate unvalidated distributions after 10 days
*/
var from = now.setHourMinute( now.getHours() , 0 ).deltaDays( 0 - db.Distribution.DISTRIBUTION_VALIDATION_LIMIT );
var to = now.setHourMinute( now.getHours() + 1 , 0).deltaDays( 0 - db.Distribution.DISTRIBUTION_VALIDATION_LIMIT );
print('AUTOVALIDATION');
print('Find distributions from $from to $to');
var ds = db.Distribution.manager.search( !$validated && ($end >= from) && ($end < to) , true);
for ( d in Lambda.array(ds)){
if ( d.contract.type != db.Contract.TYPE_VARORDER ){
ds.remove(d);
}else if ( !d.contract.amap.hasPayments() ){
ds.remove(d);
}
}
for ( d in ds){
print(d.toString());
service.PaymentService.validateDistribution(d);
}
//email
var ds = tools.ObjectListTool.deduplicateDistribsByKey(ds);
for ( d in ds ){
// var subj = d.contract.amap.name + t._(": Validation of the distribution of the ") + App.current.view.hDate(d.date);
var subj = t._("[::group::] Validation of the ::date:: distribution",{group : d.contract.amap.name , date : view.hDate(d.date)});
var html = t._("<p>As you did not validate it manually after 10 days, <br/>the delivery of the ::deliveryDate:: has been validated automatically</p>", {deliveryDate:App.current.view.hDate(d.date)});
App.quickMail(d.contract.amap.contact.email, subj, html);
}
}
/**
* Send emails from buffer.
*
* Warning, if the cron is executed each minute,
* you should consider the right amount of emails to send each minute in order to avoid overlaping and getting in concurrency problems.
* (like "SELECT * FROM BufferedMail WHERE sdate IS NULL ORDER BY cdate DESC LIMIT 100 FOR UPDATE Lock wait timeout exceeded; try restarting transaction")
*/
function sendEmailsfromBuffer(){
print("<h3>Send Emails from Buffer</h3>");
//send
for( e in sugoi.db.BufferedMail.manager.search($sdate==null,{limit:50,orderBy:-cdate},false) ){
e.lock();
if(e.isSent()) continue;
print('#${e.id} - ${e.title}');
e.finallySend();
Sys.sleep(0.1);
}
//delete old emails
var threeMonthsAgo = DateTools.delta(Date.now(), -1000.0*60*60*24*30*3);
sugoi.db.BufferedMail.manager.delete($cdate < threeMonthsAgo);
//emails that cannot be sent
for( e in sugoi.db.BufferedMail.manager.search($tries>100,{limit:50,orderBy:-cdate},true) ){
if(e.sender.email != App.config.get("default_email")){
var str = t._("Sorry, the email entitled <b>::title::</b> could not be sent.",{title:e.title});
App.quickMail(e.sender.email,t._("Email not sent"),str);
}
e.delete();
}
}
/**
* Email product report when orders close
**/
function sendOrdersByProductWhenOrdersClose(){
var range = tools.DateTool.getLastHourRange();
// Sys.println("Time is "+Date.now()+"<br/>");
// Sys.println('Find all distributions that have closed in the last hour from ${range.from} to ${range.to} \n<br/>');
for ( d in db.Distribution.manager.search($orderEndDate >= range.from && $orderEndDate < range.to, false)){
service.OrderService.sendOrdersByProductReport(d);
}
}
function print(text){
Sys.println( text + "<br/>" );
}
}
+577
View File
@@ -0,0 +1,577 @@
package controller;
import db.UserContract;
import sugoi.form.Form;
import sugoi.form.elements.HourDropDowns;
using tools.DateTool;
import Common;
class Distribution extends Controller
{
/**
* Attendance sheet by user-product (single distrib)
*/
@tpl('distribution/list.mtt')
function doList(d:db.Distribution) {
view.distrib = d;
view.place = d.place;
view.contract = d.contract;
view.orders = service.OrderService.prepare(d.getOrders());
}
/**
* Attendance sheet by product-user (single distrib)
*/
@tpl('distribution/listByProductUser.mtt')
function doListByProductUser(d:db.Distribution) {
view.distrib = d;
view.place = d.place;
view.contract = d.contract;
// view.orders = UserContract.prepare(d.getOrders());
//make a 2 dimensons table : data[userId][productId]
//WARNING : BUGS WILL APPEAR if there is many Order line for the same product
var data = new Map<Int,Map<Int,UserOrder>>();
var products = [];
var uo = d.getOrders();
for(o in uo){
products.push(o.product);
}
for ( o in service.OrderService.prepare(uo)) {
var user = data[o.userId];
if (user == null) user = new Map();
user[o.productId] = o;
data[o.userId] = user;
}
//products
var products = tools.ObjectListTool.deduplicate(products);
products.sort(function(b, a) {
return (a.name < b.name)?1:-1;
});
view.products = products;
//users
var users = Lambda.array(d.getUsers());
// var usersMap = tools.ObjectListTool.toIdMap(users);
users.sort(function(b, a) {
return (a.lastName < b.lastName)?1:-1;
});
view.users = users;
// view.usersMap = usersMap;
view.orders = data;
//total to pay by user
view.totalByUser = function(uid:Int){
var total = 0.0;
for( o in data[uid]) total+= o.total;
return total;
}
//total qty of product
view.totalByProduct = function(pid:Int){
var total = 0.0;
for( uid in data.keys()){
var x = data[uid][pid];
if(x!=null) total+= x.quantity;
}
return total;
}
}
/**
* Attendance sheet to print ( mutidistrib )
*/
@tpl('distribution/listByDate.mtt')
function doListByDate(date:Date,place:db.Place, ?type:String, ?fontSize:String) {
if (!app.user.isContractManager()) throw Error('/', t._("Forbidden action"));
view.place = place;
if (type == null) {
//display form
var f = new sugoi.form.Form("listBydate", null, sugoi.form.Form.FormMethod.GET);
f.addElement(new sugoi.form.elements.RadioGroup("type", "Affichage", [
{ value:"one", label:t._("One person per page") },
{ value:"contract", label:t._("One person per page sorted by contract") },
{ value:"all", label:t._("All") },
{ value:"allshort", label:t._("All but without prices and totals") },
],"all"));
f.addElement(new sugoi.form.elements.RadioGroup("fontSize", "Taille de police", [
{ value:"S" , label:"S" },
{ value:"M" , label:"M" },
{ value:"L" , label:"L" },
{ value:"XL", label:"XL" },
], "S", "S", false));
view.form = f;
app.setTemplate("form.mtt");
if (f.checkToken()) {
var suburl = f.getValueOf("type")+"/"+f.getValueOf("fontSize");
var url = '/distribution/listByDate/' + date.toString().substr(0, 10)+"/"+place.id+"/"+suburl;
throw Redirect( url );
}
return;
}else {
view.date = date;
view.fontRatio = switch(fontSize){
case "M" : 125; //100x1.25
case "L" : 156; //125x1.25
case "XL": 195; //156x1.25
default : 100;
};
switch(type) {
case "one":
app.setTemplate("distribution/listByDateOnePage.mtt");
case "allshort" :
app.setTemplate("distribution/listByDateShort.mtt");
case "contract" :
app.setTemplate("distribution/listByDateOnePageContract.mtt");
}
var d1 = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
var d2 = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59);
var contracts = app.user.amap.getActiveContracts(true);
//var cids = Lambda.map(contracts, function(c) return c.id);
var cconst = [];
var cvar = [];
for ( c in contracts) {
if (c.type == db.Contract.TYPE_CONSTORDERS) cconst.push(c.id);
if (c.type == db.Contract.TYPE_VARORDER) cvar.push(c.id);
}
//commandes variables
var distribs = db.Distribution.manager.search(($contractId in cvar) && $date >= d1 && $date <= d2 && $place==place, false);
var orders = db.UserContract.manager.search($distributionId in Lambda.map(distribs, function(d) return d.id) , { orderBy:userId } );
//commandes fixes
var distribs = db.Distribution.manager.search(($contractId in cconst) && $date >= d1 && $date <= d2 && $place==place, false);
var orders = Lambda.array(orders);
for ( d in distribs) {
var orders2 = db.UserContract.manager.search($productId in Lambda.map(d.contract.getProducts(), function(d) return d.id) , { orderBy:userId } );
orders = orders.concat(Lambda.array(orders2));
}
var orders3 = service.OrderService.prepare(Lambda.list(orders));
view.orders = orders3;
if (type == "csv") {
var data = new Array<Dynamic>();
for (o in orders3) {
data.push( {
"name":o.userName,
"productName":o.productName,
"price":view.formatNum(o.productPrice),
"quantity":o.quantity,
"fees":view.formatNum(o.fees),
"total":view.formatNum(o.total),
"paid":o.paid
});
}
sugoi.tools.Csv.printCsvDataFromObjects(data, ["name", "productName", "price", "quantity","fees","total", "paid"],"Export-commandes-"+date.toString().substr(0,10)+"-Cagette");
return;
}
}
}
function doDelete(d:db.Distribution) {
if (!app.user.isContractManager(d.contract)) throw Error('/', t._("Forbidden action"));
var contractId = d.contract.id;
try {
service.DistributionService.delete(d);
}
catch(e:tink.core.Error){
throw Error("/contractAdmin/distributions/" + contractId, e.message);
}
throw Ok("/contractAdmin/distributions/" + contractId, t._("the delivery has been deleted"));
}
/**
* Edit a distribution
*/
@tpl('form.mtt')
function doEdit(d:db.Distribution) {
if (!app.user.isContractManager(d.contract)) throw Error('/', t._('Forbidden action') );
var contract = d.contract;
var form = sugoi.form.Form.fromSpod(d);
form.removeElement(form.getElement("contractId"));
form.removeElement(form.getElement("end"));
form.removeElement(form.getElement("distributionCycleId"));
var x = new sugoi.form.elements.HourDropDowns("end", t._("End time") ,d.end,true);
form.addElement(x, 3);
if (d.contract.type == db.Contract.TYPE_VARORDER ) {
form.addElement(new sugoi.form.elements.DatePicker("orderStartDate", t._("Orders opening date"), d.orderStartDate));
form.addElement(new sugoi.form.elements.DatePicker("orderEndDate", t._("Orders closing date"), d.orderEndDate));
}
if (form.isValid()) {
var orderStartDate = null;
var orderEndDate = null;
try{
if (d.contract.type == db.Contract.TYPE_VARORDER ) {
orderStartDate = form.getValueOf("orderStartDate");
orderEndDate = form.getValueOf("orderEndDate");
}
d = service.DistributionService.edit(d,
form.getValueOf("date"),
form.getValueOf("end"),
form.getValueOf("placeId"),
form.getValueOf("distributor1Id"),
form.getValueOf("distributor2Id"),
form.getValueOf("distributor3Id"),
form.getValueOf("distributor4Id"),
orderStartDate,
orderEndDate);
}
catch(e:tink.core.Error){
throw Error('/contractAdmin/distributions/' + contract.id,e.message);
}
if (d.date == null) {
var msg = t._("The distribution has been proposed to the supplier, please wait for its validation");
throw Ok('/contractAdmin/distributions/'+contract.id, msg );
}
else {
throw Ok('/contractAdmin/distributions/'+contract.id, t._("The distribution has been recorded") );
}
}
else {
app.event(PreEditDistrib(d));
}
view.form = form;
view.title = t._("Edit a distribution");
}
@tpl('form.mtt')
function doEditCycle(d:db.DistributionCycle) {
if (!app.user.isContractManager(d.contract)) throw Error('/', 'Action interdite');
var form = sugoi.form.Form.fromSpod(d);
form.removeElement(form.getElement("contractId"));
if (form.isValid()) {
form.toSpod(d); //update model
d.update();
throw Ok('/contractAdmin/distributions/'+d.contract.id, t._("The delivery is now up to date"));
}
view.form = form;
view.title = t._("Modify a delivery");
}
@tpl("form.mtt")
public function doInsert(contract:db.Contract) {
if (!app.user.isContractManager(contract)) throw Error('/', t._('Forbidden action') );
var d = new db.Distribution();
d.place = contract.amap.getMainPlace();
var form = sugoi.form.Form.fromSpod(d);
form.removeElement(form.getElement("contractId"));
form.removeElement(form.getElement("distributionCycleId"));
form.removeElement(form.getElement("end"));
var x = new sugoi.form.elements.HourDropDowns("end", t._("End time") );
form.addElement(x, 3);
//default values
form.getElement("date").value = DateTool.now().deltaDays(30).setHourMinute(19, 0);
form.getElement("end").value = DateTool.now().deltaDays(30).setHourMinute(20, 0);
if (contract.type == db.Contract.TYPE_VARORDER ) {
form.addElement(new sugoi.form.elements.DatePicker("orderStartDate", t._("Orders opening date"),DateTool.now().deltaDays(10).setHourMinute(8, 0)));
form.addElement(new sugoi.form.elements.DatePicker("orderEndDate", t._("Orders closing date"),DateTool.now().deltaDays(20).setHourMinute(23, 59)));
}
if (form.isValid()) {
var createdDistrib = null;
var orderStartDate = null;
var orderEndDate = null;
try {
if (contract.type == db.Contract.TYPE_VARORDER ) {
orderStartDate = form.getValueOf("orderStartDate");
orderEndDate = form.getValueOf("orderEndDate");
}
createdDistrib = service.DistributionService.create(
contract,
form.getValueOf("date"),
form.getValueOf("end"),
form.getValueOf("placeId"),
form.getValueOf("distributor1Id"),
form.getValueOf("distributor2Id"),
form.getValueOf("distributor3Id"),
form.getValueOf("distributor4Id"),
orderStartDate,
orderEndDate);
}
catch(e:tink.core.Error){
throw Error('/contractAdmin/distributions/' + contract.id,e.message);
}
if (createdDistrib.date == null) {
var html = t._("Your request for a delivery has been sent to <b>::supplierName::</b>.<br/>Be patient, you will receive an e-mail indicating if the request has been validated or refused.", {supplierName:contract.vendor.name});
var btn = "<a href='/contractAdmin/distributions/" + contract.id + "' class='btn btn-primary'>OK</a>";
App.current.view.extraNotifBlock = App.current.processTemplate("block/modal.mtt",{html:html,title:t._("Distribution request sent"),btn:btn} );
} else {
throw Ok('/contractAdmin/distributions/'+ createdDistrib.contract.id , t._("The distribution has been recorded") );
}
}else{
//event
app.event(PreNewDistrib(contract));
}
view.form = form;
view.title = t._("Create a distribution");
}
/**
* create a distribution cycle for a contract
*/
@tpl("form.mtt")
public function doInsertCycle(contract:db.Contract) {
if (!app.user.isContractManager(contract)) throw Error('/', t._("Forbidden action"));
var dc = new db.DistributionCycle();
dc.place = contract.amap.getMainPlace();
var form = sugoi.form.Form.fromSpod(dc);
form.removeElementByName("contractId");
form.getElement("startDate").value = DateTool.now();
form.getElement("endDate").value = DateTool.now().deltaDays(30);
//start hour
form.removeElementByName("startHour");
var x = new HourDropDowns("startHour", t._("Start time"), DateTool.now().setHourMinute( 19, 0) , true);
form.addElement(x, 5);
//end hour
form.removeElement(form.getElement("endHour"));
var x = new HourDropDowns("endHour", t._("End time"), DateTool.now().setHourMinute(20, 0), true);
form.addElement(x, 6);
if (contract.type == db.Contract.TYPE_VARORDER){
form.getElement("daysBeforeOrderStart").value = 10;
form.getElement("daysBeforeOrderStart").required = true;
form.removeElementByName("openingHour");
var x = new HourDropDowns("openingHour", t._("Opening time"), DateTool.now().setHourMinute(8, 0) , true);
form.addElement(x, 8);
form.getElement("daysBeforeOrderEnd").value = 2;
form.getElement("daysBeforeOrderEnd").required = true;
form.removeElementByName("closingHour");
var x = new HourDropDowns("closingHour", t._("Closing time"), DateTool.now().setHourMinute(23, 0) , true);
form.addElement(x, 10);
}else{
form.removeElementByName("daysBeforeOrderStart");
form.removeElementByName("daysBeforeOrderEnd");
form.removeElementByName("openingHour");
form.removeElementByName("closingHour");
}
if (form.isValid()) {
var createdDistribCycle = null;
var daysBeforeOrderStart = null;
var daysBeforeOrderEnd = null;
var openingHour = null;
var closingHour = null;
try{
if (contract.type == db.Contract.TYPE_VARORDER) {
daysBeforeOrderStart = form.getValueOf("daysBeforeOrderStart");
daysBeforeOrderEnd = form.getValueOf("daysBeforeOrderEnd");
openingHour = form.getValueOf("openingHour");
closingHour = form.getValueOf("closingHour");
}
createdDistribCycle = service.DistributionService.createCycle(
contract,
form.getElement("cycleType").getValue(),
form.getValueOf("startDate"),
form.getValueOf("endDate"),
form.getValueOf("startHour"),
form.getValueOf("endHour"),
daysBeforeOrderStart,
daysBeforeOrderEnd,
openingHour,
closingHour,
form.getValueOf("placeId"));
}
catch(e:tink.core.Error){
throw Error('/contractAdmin/distributions/' + contract.id,e.message);
}
if (createdDistribCycle != null) {
throw Ok('/contractAdmin/distributions/'+ contract.id, t._("The delivery has been saved"));
}
}
else{
dc.contract = contract;
app.event(PreNewDistribCycle(dc));
}
view.form = form;
view.title = t._("Schedule a recurrent delivery");
}
/**
* Delete a distribution cycle
*/
public function doDeleteCycle(cycle:db.DistributionCycle){
if (!app.user.isContractManager(cycle.contract)) throw Error('/', t._("Forbidden action"));
var contractId = cycle.contract.id;
var messages = service.DistributionService.deleteCycleDistribs(cycle);
if (messages.length > 0){
App.current.session.addMessage( messages.join("<br/>"),true);
}
throw Ok("/contractAdmin/distributions/" + contractId, t._("Recurrent deliveries deleted"));
}
/**
* Doodle-like participation planning
*/
@tpl("distribution/planning.mtt")
public function doPlanning(contract:db.Contract) {
view.contract = contract;
var doodle = new Map<Int,{user:db.User,planning:Map<Int,Bool>}>();
var distribs = contract.getDistribs(true, 150);
for ( d in distribs ) {
for (u in [d.distributor1, d.distributor2, d.distributor3, d.distributor4]) {
if (u != null) {
var udoodle = doodle.get(u.id);
if (udoodle == null) udoodle = { user:u, planning:new Map<Int,Bool>() };
udoodle.planning.set(d.id, true);
doodle.set(u.id, udoodle);
}
}
}
view.distribs = distribs;
view.doodle = doodle;
}
/**
* Ajax service for doPlanning()
*/
public function doRegister(args: { register:Bool, distrib:db.Distribution } ) {
if (args != null) {
var d = args.distrib;
d.lock();
if (args.register) {
if (d.distributor1 == null) d.distributor1 = app.user;
else if (d.distributor2 == null) d.distributor2 = app.user;
else if (d.distributor3 == null) d.distributor3 = app.user;
else if (d.distributor4 == null) d.distributor4 = app.user;
}else {
if (d.distributor1 == app.user) d.distributor1 = null;
else if (d.distributor2 == app.user) d.distributor2 = null;
else if (d.distributor3 == app.user) d.distributor3 = null;
else if (d.distributor4 == app.user) d.distributor4 = null;
}
d.update();
}
}
/**
* Validate a multi-distrib
* @param date
* @param place
*/
@tpl('distribution/validate.mtt')
public function doValidate(date:Date, place:db.Place){
if (!app.user.isAmapManager()) throw t._("Forbidden access");
var md = MultiDistrib.get(date, place, db.Contract.TYPE_VARORDER);
view.confirmed = md.checkConfirmed();
view.users = md.getUsers();
view.date = date;
view.place = place;
}
/**
* Admin can autovalidate a multidistrib
*/
@admin
public function doAutovalidate(date:Date,place:db.Place){
var md = MultiDistrib.get(date,place,db.Contract.TYPE_VARORDER);
for ( d in md.distributions){
if(d.validated) continue;
service.PaymentService.validateDistribution(d);
}
throw Ok("/contractAdmin",t._("This distribution have been validated"));
}
@admin
public function doUnvalidate(date:Date,place:db.Place){
var md = MultiDistrib.get(date,place,db.Contract.TYPE_VARORDER);
for ( d in md.distributions){
if(!d.validated) continue;
service.PaymentService.unvalidateDistribution(d);
}
throw Ok("/contractAdmin",t._("This distribution have been Unvalidated"));
}
}
+368
View File
@@ -0,0 +1,368 @@
package controller;
import sugoi.form.elements.StringInput;
import service.OrderService;
import service.WaitingListService;
/**
* Groups
*/
class Group extends controller.Controller
{
/**
* Public page of a group
*/
@tpl('group/view.mtt')
function doDefault(group:db.Amap){
if (group.regOption == db.Amap.RegOption.Open) {
app.session.data.amapId = group.id;
throw Redirect("/");
}
view.group = group;
view.contracts = group.getActiveContracts();
view.pageTitle = group.name;
group.getMainPlace(); //just to update cache
if (app.user != null){
view.isMember = Lambda.has(app.user.getAmaps(), group);
}
}
/**
* Register to a waiting list.
* the user can be logged or not !
*/
@tpl('form.mtt')
function doList(group:db.Amap){
//checks
if (group.regOption != db.Amap.RegOption.WaitingList) throw Redirect("/group/" + group.id);
if (app.user != null) {
try{
WaitingListService.canRegister(app.user,group);
}catch(e:tink.core.Error){
throw Error("/group/" + group.id,e.message);
}
}
//build form
var form = new sugoi.form.Form("reg");
if (app.user == null){
form.addElement(new StringInput("userFirstName", t._("Your firstname"),"",true));
form.addElement(new StringInput("userLastName", t._("Your lastname") ,"",true));
form.addElement(new StringInput("userEmail", t._("Your e-mail"), "", true));
}
form.addElement(new sugoi.form.elements.TextArea("msg", t._("Leave a message")));
if (form.isValid()){
try{
if (app.user == null){
var f = form;
var user = service.UserService.softRegistration(f.getValueOf("userFirstName"),f.getValueOf("userLastName"), f.getValueOf("userEmail") );
db.User.login(user, user.email);
}
WaitingListService.registerToWl(app.user,group,form.getValueOf("msg"));
throw Ok("/group/" + group.id,t._("Your subscription to the waiting list has been recorded. You will receive an e-mail as soon as your request is processed.") );
}catch(e:tink.core.Error){
throw Error("/group/list/" + group.id,e.message);
}
}
view.title = t._("Subscription to \"::groupeName::\" waiting list", {groupeName:group.name});
view.form = form;
}
/**
Cancel suscription request
**/
function doListCancel(group:db.Amap){
try{
WaitingListService.removeFromWl(app.user,group);
}catch(e:tink.core.Error){
throw Error("/group/" + group.id,e.message);
}
throw Ok("/group/" + group.id,t._("You've been removed from the waiting list"));
}
/**
* Register direclty in an open group
*
* the user can be logged or not !
@tpl('form.mtt')
function doRegister(group:db.Amap){
if (group.regOption != db.Amap.RegOption.Open) throw Redirect("/group/" + group.id);
if (app.user != null){
if ( db.UserAmap.manager.select($amapId == group.id && $user == app.user) != null) throw Error("/group/" + group.id, t._("You are already member of this group."));
}
var form = new sugoi.form.Form("reg");
form.submitButtonLabel = t._("Join the group");
form.addElement(new sugoi.form.elements.Html("html",t._("Confirm your subscription to \"::groupName::\"", {groupName:group.name})));
if (app.user == null){
form.addElement(new StringInput("userFirstName", t._("Your firstname"),"",true));
form.addElement(new StringInput("userLastName", t._("Your lastname"), "", true));
var em = new StringInput("userEmail", t._("Your e-mail"), "", true);
em.addValidator(new EmailValidator());
form.addElement(em);
form.addElement(new StringInput("address", t._("Address"), "", true));
form.addElement(new StringInput("zipCode", t._("Zip code"), "", true));
form.addElement(new StringInput("city", t._("City"), "", true));
form.addElement(new StringInput("phone", t._("Phone"), "", true));
}
if (form.isValid()){
if (app.user == null){
var f = form;
var user = new db.User();
user.email = f.getValueOf("userEmail");
user.firstName = f.getValueOf("userFirstName");
user.lastName = f.getValueOf("userLastName");
user.address1 = f.getValueOf("address");
user.zipCode = f.getValueOf("zipCode");
user.city = f.getValueOf("city");
user.phone = f.getValueOf("phone");
if ( db.User.getSameEmail(user.email).length > 0 ) {
throw Ok("/user/login",t._("You already subscribed to Cagette.net, please log in on this page"));
}
user.insert();
app.session.setUser(user);
}
var w = new db.UserAmap();
w.user = app.user;
w.amap = group;
w.insert();
throw Ok("/user/choose", t._("Your subscription has been taken into account"));
}
view.title = t._("Subscription to \"::groupName::\"", {groupName:group.name});
view.form = form;
}*/
/**
* create a new group
*/
@tpl("form.mtt")
function doCreate() {
view.title = t._("Create a new Cagette Group");
var f = new sugoi.form.Form("c");
f.addElement(new StringInput("name", t._("Name of your group"), "", true));
//group type
var data = [
{label:t._("CSA"),value:"0"},
{label:t._("Grouped orders"),value:"1"},
{label:t._("Farmers collective"),value:"2"},
{label:t._("Farm shop"),value:"3"},
];
var gt = new sugoi.form.elements.RadioGroup("type", t._("Group type"), data ,"1","1",true,true,true);
f.addElement(gt);
if (f.checkToken()) {
var user = app.user;
var g = new db.Amap();
g.name = f.getValueOf("name");
g.contact = user;
var type:db.Amap.GroupType = Type.createEnumIndex(db.Amap.GroupType, Std.parseInt(f.getValueOf("type")) );
switch(type){
case null :
throw "unknown group type";
case db.Amap.GroupType.Amap :
g.flags.set(db.Amap.AmapFlags.HasMembership);
g.regOption = db.Amap.RegOption.WaitingList;
case db.Amap.GroupType.GroupedOrders :
g.flags.set(db.Amap.AmapFlags.ShopMode);
g.flags.set(db.Amap.AmapFlags.HasMembership);
g.regOption = db.Amap.RegOption.WaitingList;
case db.Amap.GroupType.ProducerDrive :
g.flags.set(db.Amap.AmapFlags.ShopMode);
g.regOption = db.Amap.RegOption.Open;
g.flags.set(db.Amap.AmapFlags.PhoneRequired);
case db.Amap.GroupType.FarmShop :
g.flags.set(db.Amap.AmapFlags.ShopMode);
g.regOption = db.Amap.RegOption.Open;
g.flags.set(db.Amap.AmapFlags.PhoneRequired);
}
g.groupType = type;
g.insert();
var ua = new db.UserAmap();
ua.user = user;
ua.amap = g;
ua.rights = [db.UserAmap.Right.GroupAdmin,db.UserAmap.Right.Membership,db.UserAmap.Right.Messages,db.UserAmap.Right.ContractAdmin(null)];
ua.insert();
//example datas
var place = new db.Place();
place.name = t._("Market square");
place.zipCode = "000";
place.city = "St Martin de la Cagette";
place.amap = g;
place.insert();
//contrat AMAP
var vendor = new db.Vendor();
vendor.amap = g;
vendor.name = "Jean Martin EARL";
vendor.zipCode = "000";
vendor.city = "Langon";
vendor.email = "jean@cagette.net";
vendor.insert();
if (type == Amap){
var contract = new db.Contract();
contract.name = t._("CSA contract Vegetables - Example");
contract.description = t._("This contract is an example where the customer has to commit to buy the whole year as with AMAPs");
contract.amap = g;
contract.type = 0;
contract.vendor = vendor;
contract.startDate = Date.now();
contract.endDate = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * 24 * 364);
contract.contact = user;
contract.distributorNum = 2;
contract.insert();
var p = new db.Product();
p.name = t._("Big basket of vegetables");
p.price = 15;
p.organic = true;
p.contract = contract;
p.insert();
var p = new db.Product();
p.name = t._("Small basket of vegetables");
p.price = 10;
p.organic = true;
p.contract = contract;
p.insert();
OrderService.make(user, 1, p, null, true);
var d = new db.Distribution();
d.contract = contract;
d.date = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * 24 * 14);
d.end = DateTools.delta(d.date, 1000.0 * 60 * 90);
d.place = place;
d.insert();
}
//contrat variable
var vendor = new db.Vendor();
vendor.amap = g;
vendor.name = t._("Farm Galinette");
vendor.zipCode = "000";
vendor.city = t._("Bazas");
vendor.email = "galinette@cagette.net";
vendor.insert();
var contract = new db.Contract();
contract.name = t._("Chicken Contract - Example");
contract.description = t._("Example of contract with variable orders. It is allowed to order something else at every delivery.");
contract.amap = g;
contract.type = 1;
contract.vendor = vendor;
contract.startDate = Date.now();
contract.endDate = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * 24 * 364);
contract.contact = user;
contract.distributorNum = 2;
contract.flags.set(db.Contract.ContractFlags.UsersCanOrder);
contract.insert();
var egg = new db.Product();
egg.name = t._("12 eggs");
egg.price = 5;
//egg.type = 6;
egg.organic = true;
egg.contract = contract;
egg.insert();
var p = new db.Product();
p.name = t._("Chicken");
//p.type = 2;
p.price = 9.50;
p.organic = true;
p.contract = contract;
p.insert();
var d = new db.Distribution();
d.contract = contract;
d.orderStartDate = Date.now();
d.orderEndDate = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * 24 * 19);
d.date = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * 24 * 21);
d.end = DateTools.delta(d.date, 1000.0 * 60 * 90);
d.place = place;
d.insert();
OrderService.make(user, 2, egg, d.id);
OrderService.make(user, 1, p, d.id);
App.current.session.data.amapId = g.id;
app.session.data.newGroup = true;
throw Redirect("/");
}
view.form= f;
}
/**
Displays a google map in a popup
**/
@tpl('group/place.mtt')
public function doPlace(place:db.Place){
view.place = place;
//build adress for google maps
var addr = "";
if (place.address1 != null) addr += place.address1;
if (place.address2 != null) addr += ", " + place.address2;
if (place.zipCode != null) addr += " " + place.zipCode;
if (place.city != null) addr += " " + place.city;
view.addr = view.escapeJS(addr);
}
/**
* Groups map
*/
@tpl("group/map.mtt")
public function doMap(?args:{?lat:Float,?lng:Float,?address:String}){
view.container = "container-fluid";
//if no param is sent, focus on Paris
if (args == null || (args.address == null && args.lat == null && args.lng == null)){
args = {lat:48.855675, lng:2.3472365};
}
view.lat = args.lat;
view.lng = args.lng;
view.address = args.address;
}
}
+273
View File
@@ -0,0 +1,273 @@
package controller;
import sugoi.db.Variable;
import sugoi.form.elements.StringInput;
import thx.semver.Version;
/**
* ...
* @author fbarbut<francois.barbut@gmail.com>
*/
class Install extends controller.Controller
{
/**
* checks if its a first install or an update
*/
@tpl("install/default.mtt")
public function doDefault() {
if (db.User.manager.get(1) == null) {
throw Redirect("/install/firstInstall");
}else {
//throw Error("/", "L'utilisateur admin a déjà été créé. Essayez de vous connecter avec admin@cagette.net, mot de passe : admin");
var status = new Array<{parameter:String,valid:Bool,message:String}>();
status.push(getVersionStatus());
view.status = status;
}
}
public function doDiagnostics(){
var webroot = sugoi.Web.getCwd();
if(!sys.FileSystem.exists(webroot+"file")){
Sys.println("no File directory : created");
sys.FileSystem.createDirectory(webroot+"file");
}
if(!sys.FileSystem.exists(webroot+"file/.htaccess")){
Sys.println("no .htaccess file in 'File' directory");
}
if(!sys.FileSystem.exists(webroot+"../tmp")) {
Sys.println("no tmp directory : created");
sys.FileSystem.createDirectory(webroot+"../tmp");
}
}
/**
* First install
*/
@tpl("form.mtt")
public function doFirstInstall(){
view.title = "Installation de Cagette.net";
var f = new sugoi.form.Form("c");
f.addElement(new StringInput("amapName", t._("Name of your group"),"",true));
f.addElement(new StringInput("userFirstName", t._("Your firstname"),"",true));
f.addElement(new StringInput("userLastName", t._("Your lastname"),"",true));
if (f.checkToken()) {
var user = new db.User();
user.firstName = f.getValueOf("userFirstName");
user.lastName = f.getValueOf("userLastName");
user.email = "admin@cagette.net";
user.setPass("admin");
user.insert();
var amap = new db.Amap();
amap.name = f.getValueOf("amapName");
amap.contact = user;
amap.flags.set(db.Amap.AmapFlags.HasMembership);
//amap.flags.set(db.Amap.AmapFlags.IsAmap);
amap.insert();
var ua = new db.UserAmap();
ua.user = user;
ua.amap = amap;
ua.rights = [db.UserAmap.Right.GroupAdmin,db.UserAmap.Right.Membership,db.UserAmap.Right.Messages,db.UserAmap.Right.ContractAdmin(null)];
ua.insert();
//example datas
var place = new db.Place();
place.name = t._("Marketplace");
place.amap = amap;
place.address1 = t._("Place Jules Verne");
place.zipCode = "00000";
place.city = t._("St Martin de la Cagette");
place.insert();
var vendor = new db.Vendor();
vendor.amap = amap;
vendor.name = t._("Jean Martin EURL");
vendor.email = "jean.martin@cagette.net";
vendor.zipCode = "00000";
vendor.city = "Martignac";
vendor.insert();
var contract = new db.Contract();
contract.name = t._("Vegetables Contract Example");
contract.amap = amap;
contract.type = 0;
contract.vendor = vendor;
contract.startDate = Date.now();
contract.endDate = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * 24 * 364);
contract.contact = user;
contract.distributorNum = 2;
contract.insert();
var p = new db.Product();
p.name = t._("Big basket of vegetables");
p.price = 15;
p.vat = 5;
p.contract = contract;
p.insert();
var p = new db.Product();
p.name = t._("Small basket of vegetables");
p.price = 10;
p.vat = 5;
p.contract = contract;
p.insert();
var uc = new db.UserContract();
uc.user = user;
uc.product = p;
uc.paid = true;
uc.quantity = 1;
uc.productPrice = 10;
uc.insert();
var d = new db.Distribution();
d.contract = contract;
d.date = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * 24 * 14);
d.end = DateTools.delta(d.date, 1000.0 * 60 * 90);
d.place = place;
d.insert();
App.current.user = null;
App.current.session.setUser(user);
App.current.session.data.amapId = amap.id;
throw Ok("/", t._("Group and user 'admin' created. Your email is 'admin@cagette.net' and your password is 'admin'"));
}
view.form= f;
}
/**
* get version status
*/
private function getVersionStatus(){
var out = {parameter:"version", valid:false, message:""};
var v = Variable.get("version");
if (v == null || v=="") {
Variable.set("version", App.VERSION.toString());
v = App.VERSION.toString();
}
var v :thx.semver.Version = thx.semver.Version.stringToVersion(v);
if (v.lessThan(App.VERSION)){
//need update !
out.valid = false;
out.message = t._("You must update your database to version ") +App.VERSION.toString()+"";
}else{
out.valid = true;
out.message = t._("Current version") +v.toString()+"";
}
return out;
}
/**
* perform migrations from a version to another
*/
@admin
public function doUpdateversion(){
var log = [];
var currentVersion = thx.semver.Version.stringToVersion(Variable.get("version"));
//Migrations to 0.9.2
if (currentVersion.lessThan( thx.semver.Version.arrayToVersion([0,9,2]) )){
log.push(t._("Installation of the dictionnary of products (taxonomy)"));
_0_9_2_installTaxonomy();
log.push(t._("Improvement on saving orders"));
_0_9_2_dbMigration();
sugoi.db.Variable.set("version", "0.9.2");
}
//Migrations to 1.0.0
//...
throw Ok("/install", t._("Following update have been performed:<ul>")+Lambda.map(log,function(x) return "<li>"+x+"</li>").join("")+"</ul>");
}
@admin
function _0_9_2_installTaxonomy(){
db.TxpCategory.manager.delete(true);
db.TxpSubCategory.manager.delete(true);
db.TxpProduct.manager.delete(true);
var taxo = sys.io.File.getContent(sugoi.Web.getCwd() + "../data/productTaxonomy.json");
var taxo = haxe.Json.parse(taxo);
var categories : Array<Dynamic> = taxo.categories;
var subcategories : Array<Dynamic> = taxo.subCategories;
var products : Array<Dynamic> = taxo.products;
for ( c in categories){
var cat = new db.TxpCategory();
cat.id = c.id;
cat.name = c.name;
cat.insert();
}
for ( sc in subcategories){
var scat = new db.TxpSubCategory();
scat.id = sc.id;
scat.name = sc.name;
scat.category = db.TxpCategory.manager.get(Std.parseInt(sc.category));
scat.insert();
}
for ( p in products){
var pro = new db.TxpProduct();
pro.name = p.name;
pro.id = p.id;
pro.category = db.TxpCategory.manager.get(Std.parseInt(p.category));
pro.subCategory = db.TxpSubCategory.manager.get(Std.parseInt(p.subCategory));
pro.insert();
}
}
@admin
function _0_9_2_dbMigration(){
//recompute prices on orders
for ( order in db.UserContract.manager.all(true)){
order.productPrice = order.product.price;
order.feesRate = order.product.contract.percentageValue;
order.update();
}
//activate payment orders
for ( a in db.Amap.manager.all(true)){
a.allowedPaymentsType = ["cash", "transfer", "check"];
a.update();
}
}
}
+261
View File
@@ -0,0 +1,261 @@
package controller;
import db.Distribution;
import db.UserContract;
import haxe.Json;
import haxe.web.Dispatch;
import sugoi.form.elements.StringInput;
import sugoi.tools.ResultsBrowser;
import Common;
import tools.ArrayTool;
class Main extends Controller {
/**
* public pages
*/
function doGroup(d:haxe.web.Dispatch){
d.dispatch(new controller.Group());
}
/**
Group homepage
**/
@tpl("home.mtt")
function doDefault() {
view.category = 'home';
var group = app.getCurrentGroup();
if ( app.user!=null && group == null) {
throw Redirect("/user/choose");
}else if (app.user == null && (group==null || group.regOption!=db.Amap.RegOption.Open) ) {
throw Redirect("/user/login");
}
view.amap = group;
//contract with open orders
var openContracts = Lambda.filter(group.getActiveContracts(), function(c) return c.isUserOrderAvailable());
view.openContracts = openContracts;
//register to become "distributor"
view.contractsWithDistributors = app.user==null ? [] : Lambda.filter(app.user.amap.getActiveContracts(), function(c) return c.distributorNum > 0);
//freshly created group
view.newGroup = app.session.data.newGroup == true;
var n = Date.now();
var now = new Date(n.getFullYear(), n.getMonth(), n.getDate(), 0, 0, 0);
var in3Month = DateTools.delta(now, 1000.0 * 60 * 60 * 24 * 30 * 3);
var distribs = MultiDistrib.getFromTimeRange(group,now,in3Month);
view.distribs = distribs;
//view functions
view.getWhosTurn = function(orderId:Int, distrib:Distribution) {
return db.UserContract.manager.get(orderId, false).getWhosTurn(distrib);
}
//event for additionnal blocks on home page
var e = Blocks([], "home");
app.event(e);
view.blocks = e.getParameters()[0];
//message if phone is required
if(app.user!=null && app.user.amap.flags.has(db.Amap.AmapFlags.PhoneRequired) && app.user.phone==null){
app.session.addMessage(t._("Members of this group should provide a phone number. <a href='/account/edit'>Please click here to update your account</a>."),true);
}
}
//login and stuff
function doUser(d:Dispatch) {
d.dispatch(new controller.User());
}
function doCron(d:Dispatch) {
d.dispatch(new controller.Cron());
}
/**
* JSON REST API Entry point
*/
function doApi(d:Dispatch) {
try {
d.dispatch(new controller.Api());
}catch (e:tink.core.Error){
//manage tink Errors (service errors)
sugoi.Web.setReturnCode(e.code);
Sys.print(Json.stringify( {error:{code:e.code,message:e.message,stack:e.exceptionStack}} ));
}catch (e:Dynamic){
//manage other errors
sugoi.Web.setReturnCode(500);
var stack = if ( App.config.DEBUG ) haxe.CallStack.toString(haxe.CallStack.exceptionStack()) else "";
App.current.logError(e, stack);
Sys.print(Json.stringify( {error:{code:500,message : Std.string(e), stack:stack }} ));
}
}
@tpl("cssDemo.mtt")
function doCssdemo() {
view.category = 'home';
}
@tpl("form.mtt")
function doInstall(d:Dispatch) {
d.dispatch(new controller.Install());
}
function doP(d:Dispatch) {
/*
* Invalid array access
Stack (ADMIN|DEBUG)
Called from C:\HaxeToolkit\haxe\std/haxe/web/Dispatch.hx line 463
Called from controller/Main.hx line 117
*
var plugin = d.parts.shift();
for ( p in App.plugins) {
var n = Type.getClassName(Type.getClass(p)).toLowerCase();
n = n.split(".").pop();
if (plugin == n) {
d.dispatch( p.getController() );
return;
}
}
throw Error("/","Plugin '"+plugin+"' introuvable.");
*/
d.dispatch(new controller.Plugin());
}
@logged
function doMember(d:Dispatch) {
view.category = 'members';
d.dispatch(new controller.Member());
}
@logged
function doStats(d:Dispatch) {
view.category = 'stats';
d.dispatch(new Stats());
}
@logged
function doAccount(d:Dispatch) {
view.category = 'account';
d.dispatch(new controller.Account());
}
@logged
function doVendor(d:Dispatch) {
view.category = 'contractadmin';
d.dispatch(new controller.Vendor());
}
@logged
function doPlace(d:Dispatch) {
view.category = 'contractadmin';
d.dispatch(new controller.Place());
}
@logged
function doTransaction(d:Dispatch) {
view.category = 'members';
d.dispatch(new controller.Transaction());
}
@logged
function doDistribution(d:Dispatch) {
view.category = 'contractadmin';
d.dispatch(new controller.Distribution());
}
@logged
function doMembership(d:Dispatch) {
view.category = 'members';
d.dispatch(new controller.Membership());
}
function doShop(d:Dispatch) {
view.category = 'shop';
d.dispatch(new controller.Shop());
}
@tpl('shop/default2.mtt')
function doShop2(place:db.Place, date:String) {
view.category = 'shop';
view.place = place;
view.date = date;
}
@logged
function doProduct(d:Dispatch) {
view.category = 'contractadmin';
d.dispatch(new controller.Product());
}
@logged
function doAmap(d:Dispatch) {
view.category = 'amap';
d.dispatch(new controller.Amap());
}
@logged
function doContract(d:Dispatch) {
view.category = 'contract';
d.dispatch(new Contract());
}
@logged
function doContractAdmin(d:Dispatch) {
view.category = 'contractadmin';
d.dispatch(new ContractAdmin());
}
@logged
function doMessages(d:Dispatch) {
view.category = 'messages';
d.dispatch(new Messages());
}
@logged
function doAmapadmin(d:Dispatch) {
view.category = 'amapadmin';
d.dispatch(new AmapAdmin());
}
@logged
function doValidate(date:Date, place:db.Place, user:db.User, d:haxe.web.Dispatch){
var v = new controller.Validate();
v.date = date;
v.place = place;
v.user = user;
d.dispatch(v);
}
@admin
function doAdmin(d:Dispatch) {
d.dispatch(new controller.admin.Admin());
}
@admin
function doDb(d:Dispatch) {
d.parts = []; //disable haxe.web.Dispatch
sys.db.Admin.handler();
}
}
+741
View File
@@ -0,0 +1,741 @@
package controller;
import Common;
import haxe.Utf8;
import sugoi.form.Form;
import sugoi.form.elements.Selectbox;
import sugoi.form.validators.EmailValidator;
import sugoi.tools.Utils;
class Member extends Controller
{
public function new()
{
super();
if (!app.user.canAccessMembership()) throw Redirect("/");
}
@logged
@tpl('member/default.mtt')
function doDefault(?args: { ?search:String, ?select:String } ) {
checkToken();
var browse:Int->Int->List<Dynamic>;
var uids = db.UserAmap.manager.search($amap == app.user.getAmap(), false);
var uids = Lambda.map(uids, function(ua) return ua.user.id);
if (args != null && args.search != null) {
//SEARCH
browse = function(index:Int, limit:Int) {
var search = "%"+StringTools.trim(args.search)+"%";
return db.User.manager.search(
($lastName.like(search) ||
$lastName2.like(search) ||
$address1.like(search) ||
$address2.like(search) ||
$firstName.like(search) ||
$firstName2.like(search)
) && $id in uids , { orderBy:-id }, false);
}
view.search = args.search;
}else if(args!=null && args.select!=null){
//SELECTION
switch(args.select) {
case "nocontract":
if (app.params.exists("csv")) {
sugoi.tools.Csv.printCsvDataFromObjects(Lambda.array(db.User.getUsers_NoContracts()), ["firstName", "lastName", "email"], t._("Without contracts"));
return;
}else {
browse = function(index:Int, limit:Int) { return db.User.getUsers_NoContracts(index, limit); }
}
case "contract":
if (app.params.exists("csv")) {
sugoi.tools.Csv.printCsvDataFromObjects(Lambda.array(db.User.getUsers_Contracts()), ["firstName", "lastName", "email"], t._("With orders"));
return;
}else {
browse = function(index:Int, limit:Int) { return db.User.getUsers_Contracts(index, limit); }
}
case "nomembership" :
if (app.params.exists("csv")) {
sugoi.tools.Csv.printCsvDataFromObjects(Lambda.array(db.User.getUsers_NoMembership()), ["firstName", "lastName", "email"], t._("Memberships to be renewed"));
return;
}else {
browse = function(index:Int, limit:Int) { return db.User.getUsers_NoMembership(index, limit); }
}
case "newusers" :
if (app.params.exists("csv")) {
sugoi.tools.Csv.printCsvDataFromObjects(Lambda.array(db.User.getUsers_NewUsers()), ["firstName", "lastName", "email"], t._("Never connected"));
return;
}else {
browse = function(index:Int, limit:Int) { return db.User.getUsers_NewUsers(index, limit); }
}
default:
throw t._("Unknown selection");
}
view.select = args.select;
}else {
if (app.params.exists("csv")) {
var headers = ["firstName", "lastName", "email","phone", "firstName2", "lastName2","email2","phone2", "address1","address2","zipCode","city"];
sugoi.tools.Csv.printCsvDataFromObjects(Lambda.array(db.User.manager.search( $id in uids, {orderBy:lastName}, false)), headers, t._("Members"));
return;
}else {
//default display
browse = function(index:Int, limit:Int) {
return db.User.manager.search( $id in uids, { limit:[index,limit], orderBy:lastName }, false);
}
}
}
var count = uids.length;
var rb = new sugoi.tools.ResultsBrowser(count, (args.select!=null||args.search!=null)?1000:10, browse);
view.members = rb;
if (args.select == null || args.select != "newusers") {
//count new users
view.newUsers = db.User.getUsers_NewUsers().length;
}
view.waitingList = db.WaitingList.manager.count($group == app.user.amap);
}
/**
* Move to waiting list
*/
function doMovetowl(u:db.User){
var ua = db.UserAmap.get(u, app.user.amap, true);
ua.delete();
var wl = new db.WaitingList();
wl.user = u;
wl.group = app.user.amap;
wl.insert();
throw Ok("/member", u.getName() +" "+ t._("is now on waiting list.") );
}
/**
* Display waiting list
*/
@tpl('member/waiting.mtt')
function doWaiting(?args:{?add:db.User,?remove:db.User}){
if (args != null){
if (args.add != null){
service.WaitingListService.approveRequest(args.add,app.user.amap);
throw Ok("/member/waiting", t._("Membership request accepted") );
}else if (args.remove != null){
service.WaitingListService.cancelRequest(args.remove,app.user.amap);
throw Ok("/member/waiting", t._("Membership request refused") );
}
}
view.waitingList = db.WaitingList.manager.search($group == app.user.amap,{orderBy:-date});
}
/**
* Send an invitation to a new member
*/
function doInviteMember(u:db.User){
if (checkToken() ) {
u.sendInvitation(app.user.amap);
throw Ok('/member/view/'+u.id, t._("Invitation sent.") );
}
}
/**
* Invite 'never logged' users
*/
function doInvite() {
if (checkToken()) {
var users = db.User.getUsers_NewUsers();
try{
for ( u in users) {
u.sendInvitation(app.user.amap);
Sys.sleep(0.2);
}
}catch (e:String){
if (e.indexOf("curl") >-1) {
App.current.logError(e, haxe.CallStack.toString(haxe.CallStack.exceptionStack()));
throw Error("/member", t._("An error occurred while sending emails, please retry"));
}
}
throw Ok('/member', t._("Congratulations, you just sent <b>::userLength::</b> invitations", {userLength:users.length}));
}
}
@tpl("member/view.mtt")
function doView(member:db.User) {
view.member = member;
var userAmap = db.UserAmap.get(member, app.user.amap);
if (userAmap == null) throw Error("/member", t._("This person does not belong to your group"));
view.userAmap = userAmap;
view.canLoginAs = (db.UserAmap.manager.count($userId == member.id) == 1 && app.user.isAmapManager()) || app.user.isAdmin();
//orders
var row = { constOrders:new Array<UserOrder>(), varOrders:new Map<String,Array<UserOrder>>() };
//commandes fixes
var contracts = db.Contract.manager.search($type == db.Contract.TYPE_CONSTORDERS && $amap == app.user.amap && $endDate > DateTools.delta(Date.now(),-1000.0*60*60*24*30), false);
var orders = member.getOrdersFromContracts(contracts);
row.constOrders = service.OrderService.prepare(orders);
//commandes variables groupées par date de distrib
var contracts = db.Contract.manager.search($type == db.Contract.TYPE_VARORDER && $amap == app.user.amap && $endDate > DateTools.delta(Date.now(),-1000.0*60*60*24*30), false);
var distribs = new Map<String,List<db.UserContract>>();
for (c in contracts) {
var ds = c.getDistribs();
for (d in ds) {
var k = d.date.toString().substr(0, 10);
var orders = member.getOrdersFromDistrib(d);
if (orders.length > 0) {
if (!distribs.exists(k)) {
distribs.set(k, orders);
}else {
var v = distribs.get(k);
for ( o in orders ) v.add(o);
distribs.set(k, v);
}
}
}
}
for ( k in distribs.keys()){
var d = distribs.get(k);
var d2 = service.OrderService.prepare(d);
row.varOrders.set(k,d2);
}
view.userContracts = row;
checkToken(); //to insert a token in tpl
}
/**
* Admin : Log in as this user for debugging purpose
* @param user
* @param amap
*/
function doLoginas(member:db.User, amap:db.Amap) {
if (!app.user.isAdmin()){
if (!app.user.isAmapManager()) return;
if (member.isAdmin()) return;
if ( db.UserAmap.manager.count($userId == member.id) > 1 ) return;
}
App.current.session.setUser(member);
App.current.session.data.amapId = amap.id;
throw Redirect("/member/view/" + member.id );
}
@tpl('member/lastMessages.mtt')
function doLastMessages(member:db.User){
var out = new Array<{date:Date,subject:String,success:String,failure:String}>();
var threeMonth = DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 30.5 * 3);
for ( m in sugoi.db.BufferedMail.manager.search($remoteId == app.user.amap.id && $cdate > threeMonth, {limit:10, orderBy:-cdate})){
var status : sugoi.mail.IMailer.MailerResult = m.status;
if ( status!=null && status.get(member.email)!=null ){
var r = m.getMailerResultMessage(member.email);
out.push( {date:m.cdate,subject:m.title,success:r.success,failure:r.failure} );
}
}
view.emails = out;
}
/**
* Edit a Member
*/
@tpl('form.mtt')
function doEdit(member:db.User) {
if (member.isAdmin() && !app.user.isAdmin()) throw Error("/", t._("You cannot modify the account of an administrator"));
var form = sugoi.form.Form.fromSpod(member);
//cleaning
form.removeElement( form.getElement("rights") );
form.removeElement( form.getElement("lang") );
form.removeElement( form.getElement("ldate") );
form.removeElement( form.getElement("apiKey") );
var isReg = member.isFullyRegistred();
var groupNum = db.UserAmap.manager.count($userId == member.id);
//an administrator can modify a user's email only if he's not member elsewhere
if (groupNum > 1){
form.removeElementByName("email");
form.removeElementByName("email2");
app.session.addMessage(t._("For security reasons, you cannot modify the e-mail of this person because this person is a member of more than 1 group."));
}
//an administrator can modify a user's pass only if he's a not registred user.
if (!isReg){
app.session.addMessage(t._("This person did not define yet a password. You are exceptionaly authorized to do it. Please don't forget to tell this person."));
form.getElement("pass").required = false;
}else{
form.removeElement( form.getElement("pass") );
}
if (form.checkToken()) {
if (app.user.amap.flags.has(db.Amap.AmapFlags.PhoneRequired) && form.getValueOf("phone") == null ){
throw Error("/member/edit/"+member.id, t._("Phone number is required in this group."));
}
form.toSpod(member);
//check that the given emails are not already used elsewhere
var sim = db.User.getSameEmail(member.email,member.email2);
for ( s in sim) {
if (s.id == member.id) sim.remove(s);
}
if (sim.length > 0) {
//Let's merge the 2 users if it has no orders.
var id = sim.first().id;
if (db.UserContract.manager.search( $userId == id || $userId2 == id , false).length == 0) {
//merge
member.merge( sim.first() );
app.session.addMessage(t._("This e-mail was used by another user account. As this user account was not used, it has been merged into the current user account."));
} else {
var str = t._("Warning, this e-mail or this name already exists for another account : ");
str += Lambda.map(sim, function(u) return "<a href='/member/view/" + u.id + "'>" + u.getCoupleName() + "</a>").join(",");
str += " "+t._("These accounts can't be merged because the second account has orders");
throw Error("/member/edit/" + member.id, str);
}
}
if (!isReg) member.setPass(form.getValueOf("pass"));
member.update();
if (!App.config.DEBUG && groupNum == 1) {
//warn the user that his email has been updated
if (form.getValueOf("email") != member.email) {
var m = new sugoi.mail.Mail();
m.setSender(App.config.get("default_email"), t._("Cagette.net"));
m.addRecipient(member.email);
m.setSubject(t._("Change your e-mail in your account Cagette.net"));
m.setHtmlBody( app.processTemplate("mail/message.mtt", { text:app.user.getName() + t._(" just modified your e-mail in your account Cagette.net.<br/>Your e-mail is now:")+form.getValueOf("email") } ) );
App.sendMail(m);
}
if (form.getValueOf("email2") != member.email2 && member.email2!=null) {
var m = new sugoi.mail.Mail();
m.setSender(App.config.get("default_email"),"Cagette.net");
m.addRecipient(member.email2);
m.setSubject(t._("Change the e-mail of your account Cagette.net"));
m.setHtmlBody( app.processTemplate("mail/message.mtt", { text:app.user.getName() +t._(" just modified your e-mail in your account Cagette.net.<br/>Your e-mail is now:")+form.getValueOf("email2") } ) );
App.sendMail(m);
}
}
throw Ok('/member/view/'+member.id, t._("This member has beed updated"));
}
view.form = form;
}
/**
* Remove a user from this group
*/
function doDelete(user:db.User,?args:{confirm:Bool,token:String}) {
if (checkToken()) {
if (!app.user.canAccessMembership()) throw t._("You cannot do that.");
if (user.id == app.user.id) throw Error("/member/view/" + user.id, t._("You cannot delete yourself."));
if ( Lambda.count(user.getOrders(app.user.amap),function(x) return x.quantity>0) > 0 && !args.confirm) {
throw Error("/member/view/"+user.id, t._("Warning, this account has orders. <a class='btn btn-default btn-xs' href='/member/delete/::userid::?token=::argstoken::&confirm=1'>Remove anyway</a>", {userid:user.id, argstoken:args.token}));
}
var ua = db.UserAmap.get(user, app.user.amap, true);
if (ua != null) {
ua.delete();
throw Ok("/member", t._("::user:: has been removed from your group",{user:user.getName()}));
}else {
throw Error("/member", t._("This person does not belong to \"::amapname::\"", {amapname:app.user.amap.name}));
}
}else {
throw Redirect("/member/view/"+user.id);
}
}
@tpl('form.mtt')
function doMerge(user:db.User) {
if (!app.user.canAccessMembership()) throw Error("/","Action interdite");
view.title = t._("Merge an account with another one");
view.text = t._("This action allows you to merge two accounts (when you have duplicates in the database for example).<br/>Contracts of account 2 will be moved to account 1, and account 2 will be deleted. Warning, it is not possible to cancel this action.");
var form = new Form("merge");
var members = app.user.amap.getMembers();
var members = Lambda.array(Lambda.map(members, function(x) return { key:Std.string(x.id), value:x.getName() } ));
var mlist = new Selectbox("member1", t._("Account 1"), members, Std.string(user.id));
form.addElement( mlist );
var mlist = new Selectbox("member2", t._("Account 2"), members);
form.addElement( mlist );
if (form.checkToken()) {
var m1 = Std.parseInt(form.getElement("member1").value);
var m2 = Std.parseInt(form.getElement("member2").value);
var m1 = db.User.manager.get(m1,true);
var m2 = db.User.manager.get(m2,true);
//if (m1.amapId != m2.amapId) throw "ils ne sont pas de la même amap !";
//on prend tout à m2 pour donner à m1
//change usercontracts
var contracts = db.UserContract.manager.search($user==m2 || $user2==m2,true);
for (c in contracts) {
if (c.user.id == m2.id) c.user = m1;
if (c.user2!=null && c.user2.id == m2.id) c.user2 = m1;
c.update();
}
//group memberships
var adh = db.UserAmap.manager.search($user == m2, true);
for ( a in adh) {
a.user = m1;
a.update();
}
//change contacts
var contacts = db.Contract.manager.search($contact==m2,true);
for (c in contacts) {
c.contact = m1;
c.update();
}
//if (m2.amap.contact == m2) {
//m1.amap.lock();
//m1.amap.contact = m1;
//m1.amap.update();
//}
m2.delete();
throw Ok("/member/view/" + m1.id, t._("Both accounts have been merged"));
}
view.form = form;
}
@tpl('member/import.mtt')
function doImport(?args: { confirm:Bool } ) {
var step = 1;
var request = Utils.getMultipart(1024 * 1024 * 4); //4mb
//on recupere le contenu de l'upload
var data = request.get("file");
if ( data != null) {
var csv = new sugoi.tools.Csv();
csv.setHeaders([t._("Firstname"), t._("Lastname"), t._("E-mail"), t._("Mobile phone"), t._("Partner's firstname"), t._("Partner's lastname"), t._("Partner's e-mail"), t._("Partner's Mobile phone"), t._("Address 1"), t._("Address 2"), t._("Post code"), t._("City")]);
//utf8 encode if needed
try{
if (!haxe.Utf8.validate(data)){
data = haxe.Utf8.encode(data);
}
}catch (e:Dynamic){ }
var unregistred = csv.importDatas(data);
/*var checkEmail = function(email){
if ( !sugoi.form.validators.EmailValidator.check(email) ) {
throw Error("/member", t._("The email <b>::email::</b> is invalid, please update your CSV file",{email:email}) );
}
}*/
//cleaning
for ( user in unregistred.copy() ) {
//check nom+prenom
if (user[0] == null || user[1] == null) {
throw Error("/member/import", t._("You must fill the name and the firstname of the person. This line is incomplete: ") + user);
}
if (user[2] == null) {
throw Error("/member/import", t._("Each person must have an e-mail to be able to log in. ::user0:: ::user1:: don't have one. ", {user0:user[0], user1:user[1]}) +user);
}
//uppercase du nom
if (user[1] != null) user[1] = user[1].toUpperCase();
if (user[5] != null) user[5] = user[5].toUpperCase();
//lowercase email
if (user[2] != null){
user[2] = user[2].toLowerCase();
//checkEmail(user[2]);
}
if (user[6] != null){
user[6] = user[6].toLowerCase();
//checkEmail(user[6]);
}
}
//utf-8 check
for ( row in unregistred.copy()) {
for ( i in 0...row.length) {
var t = row[i];
if (t != "" && t != null) {
try{
if (!Utf8.validate(t)) {
t = Utf8.encode(t);
}
}catch (e:Dynamic) {}
row[i] = t;
}
}
}
//put already registered people in another list
var registred = [];
for (r in unregistred.copy()) {
//var firstName = r[0];
//var lastName = r[1];
var email = r[2];
//var firstName2 = r[4];
//var lastName2 = r[5];
var email2 = r[6];
var us = db.User.getSameEmail(email, email2);
if (us.length > 0) {
unregistred.remove(r);
registred.push(r);
}
}
app.session.data.csvUnregistered = unregistred;
app.session.data.csvRegistered = registred;
view.data = unregistred;
view.data2 = registred;
step = 2;
}
if (args != null && args.confirm) {
//import unregistered members
var i : Iterable<Dynamic> = cast app.session.data.csvUnregistered;
for (u in i) {
if (u[0] == null || u[0] == "null" || u[0] == "") continue;
var user = new db.User();
user.firstName = u[0];
user.lastName = u[1];
user.email = u[2];
if (user.email != null && user.email != "null" &&!EmailValidator.check(user.email)) {
throw t._("The E-mail ::useremail:: is invalid, please modify your file", {useremail:user.email});
}
user.phone = u[3];
user.firstName2 = u[4];
user.lastName2 = u[5];
user.email2 = u[6];
if (user.email2 != null && user.email2 != "null" && !EmailValidator.check(user.email2)) {
App.log(u);
throw t._("The E-mail of the partner of ::userFirstName:: ::userLastName:: '::userEmail::' is invalid, please check your file", {userFirstName:user.firstName, userLastName:user.lastName, userEmail:user.email2});
}
user.phone2 = u[7];
user.address1 = u[8];
user.address2 = u[9];
user.zipCode = u[10];
user.city = u[11];
user.insert();
var ua = new db.UserAmap();
ua.user = user;
ua.amap = app.user.amap;
ua.insert();
}
//import registered members
var i : Iterable<Array<String>> = cast app.session.data.csvRegistered;
for (u in i) {
var email = u[2];
var email2 = u[6];
var us = db.User.getSameEmail(email, email2);
var userAmaps = db.UserAmap.manager.search($amap == app.user.amap && $userId in Lambda.map(us, function(u) return u.id), false);
//member exists but is not member of this group.
if (userAmaps.length == 0) {
var ua = new db.UserAmap();
ua.user = us.first();
ua.amap = app.user.amap;
ua.insert();
}
}
view.numImported = app.session.data.csvUnregistered.length + app.session.data.csvRegistered.length;
app.session.data.csvUnregistered = null;
app.session.data.csvRegistered = null;
step = 3;
}
if (step == 1) {
//reset import when back to import page
app.session.data.csvUnregistered = null;
app.session.data.csvRegistered = null;
}
view.step = step;
}
@tpl("user/insert.mtt")
public function doInsert() {
if (!app.user.canAccessMembership()) throw Error("/", t._("Forbidden action"));
var m = new db.User();
var form = sugoi.form.Form.fromSpod(m);
form.removeElement(form.getElement("lang"));
form.removeElement(form.getElement("rights"));
form.removeElement(form.getElement("pass"));
form.removeElement(form.getElement("ldate") );
form.removeElement( form.getElement("apiKey") );
form.addElement(new sugoi.form.elements.Checkbox("warnAmapManager", t._("Send an E-mail to the person in charge of the group"), true));
form.getElement("email").addValidator(new EmailValidator());
form.getElement("email2").addValidator(new EmailValidator());
if (form.isValid()) {
//check doublon de User et de UserAmap
var userSims = db.User.getSameEmail(form.getValueOf("email"),form.getValueOf("email2"));
view.userSims = userSims;
var userAmaps = db.UserAmap.manager.search($amap == app.user.amap && $userId in Lambda.map(userSims, function(u) return u.id), false);
view.userAmaps = userAmaps;
if (userAmaps.length > 0) {
//user deja enregistré dans cette amap
throw Error('/member/view/' + userAmaps.first().user.id, t._("This person is already member of this group"));
}else if (userSims.length > 0) {
//des users existent avec ce nom ,
//if (userSims.length == 1) {
// si yen a qu'un on l'inserte
var ua = new db.UserAmap();
ua.user = userSims.first();
ua.amap = app.user.amap;
ua.insert();
throw Ok('/member/', t._("This person already had an account on Cagette.net, and is now member of your group."));
/*}else {
//demander validation avant d'inserer le userAmap
//TODO
throw Error('/member', t._("Not possible to add this person because there are already some people in the database having the same firstname and name. Please contact the administrator.")+userSims);
}*/
return;
}else {
if (app.user.amap.flags.has(db.Amap.AmapFlags.PhoneRequired) && form.getValueOf("phone") == null ){
throw Error("/member/insert", t._("Phone number is required in this group."));
}
//insert user
var u = new db.User();
form.toSpod(u);
u.lang = app.user.lang;
u.insert();
//insert userAmap
var ua = new db.UserAmap();
ua.user = u;
ua.amap = app.user.getAmap();
ua.insert();
if (form.getValueOf("warnAmapManager") == "1") {
var url = "http://" + App.config.HOST + "/member/view/" + u.id;
var text = t._("::admin:: just keyed-in contact details of a new member: <br/><strong>::newMember::</strong><br/> <a href='::url::'>See contact details</a>",{admin:app.user.getName(),newMember:u.getCoupleName(),url:url});
App.quickMail(
app.user.getAmap().contact.email,
app.user.amap.name +" - "+ t._("New member") + " : " + u.getCoupleName(),
app.processTemplate("mail/message.mtt", { text:text } )
);
}
throw Ok('/member/', t._("This person is now member of the group"));
}
}
view.form = form;
}
/**
* user payments history
*/
@tpl('member/payments.mtt')
function doPayments(m:db.User){
service.PaymentService.updateUserBalance(m, app.user.amap);
var browse:Int->Int->List<Dynamic>;
//default display
browse = function(index:Int, limit:Int) {
return db.Operation.getOperationsWithIndex(m,app.user.amap,index,limit,true);
}
var count = db.Operation.countOperations(m,app.user.amap);
var rb = new sugoi.tools.ResultsBrowser(count, 10, browse);
view.rb = rb;
view.member = m;
view.balance = db.UserAmap.get(m, app.user.amap).balance;
checkToken();
}
@tpl('member/balance.mtt')
function doBalance(){
view.balanced = db.UserAmap.manager.search($amap == app.user.amap && $balance == 0.0, false);
view.credit = db.UserAmap.manager.search($amap == app.user.amap && $balance > 0, false);
view.debt = db.UserAmap.manager.search($amap == app.user.amap && $balance < 0, false);
}
}
+73
View File
@@ -0,0 +1,73 @@
package controller;
import sugoi.form.elements.IntSelect;
using Std;
/**
* Membership management
*
* @author fbarbut<francois.barbut@gmail.com>
*/
class Membership extends controller.Controller
{
@tpl("membership/default.mtt")
function doDefault(member:db.User) {
var userAmap = db.UserAmap.get(member, app.user.amap,true);
if (userAmap == null) throw Error("/member", t._("This person is not a member of your group"));
//formulaire
var f = new sugoi.form.Form("membership");
var year = Date.now().getFullYear();
var data = [];
var now = Date.now();
for ( x in 0...5) {
var y = now.getFullYear() - x;
var yy = DateTools.delta(now, DateTools.days(365) * -x);
data.push({label:app.user.amap.getPeriodName(yy),value:app.user.amap.getMembershipYear(yy)});
}
f.addElement(new IntSelect("year", t._("Period"), data,app.user.amap.getMembershipYear(),true));
f.addElement(new sugoi.form.elements.DateDropdowns("date", t._("Date of payment of subscription"), null, true));
if (f.isValid()) {
var y : Int = f.getValueOf("year");
if (db.Membership.get(member, app.user.amap, y) != null) throw Error("/membership/"+member.id, t._("This subscription has been already keyed-in"));
var cotis = new db.Membership();
cotis.amap = app.user.amap;
cotis.user = member;
cotis.year = y;
cotis.date = f.getElement("date").value;
cotis.insert();
throw Ok("/membership/"+member.id, t._("Subscription saved"));
}
//années de cotisation
var memberships = db.Membership.manager.search($user == member && $amap == app.user.amap,{orderBy:-year}, false);
//for ( m in memberships) {
//Reflect.setField(m, 'yearDate', new Date(m.year, 1, 1, 1, 1, 1));
//}
view.memberships = memberships;
//view
view.form = f;
view.member = member;
checkToken();
}
public function doDelete(member:db.User, year:Int,?args:{token:String}) {
if (checkToken()) {
var cotis = db.Membership.get(member, app.user.amap, year, true);
if (cotis == null) throw Error("/", t._("This subscription does not exist"));
cotis.delete();
throw Ok("/membership/" + member.id, t._("Subscription deleted"));
}else {
throw Error("/", "Bad Token");
}
}
}
+211
View File
@@ -0,0 +1,211 @@
package controller;
import db.Message;
import db.UserContract;
import sugoi.form.ListData;
import sugoi.form.elements.*;
import sugoi.form.Form;
class Messages extends Controller
{
public function new()
{
super();
if (!app.user.canAccessMessages()) throw Redirect("/");
}
@tpl("messages/default.mtt")
function doDefault() {
var form = new Form("msg");
var senderName = "";
var senderMail = "";
if (App.current.session.data.whichUser == 1 && app.user.email2 != null) {
senderMail = app.user.email2;
senderName = app.user.firstName2 + " " + app.user.lastName2;
}else {
senderMail = app.user.email;
senderName = app.user.firstName + " " + app.user.lastName;
}
var lists = getLists();
form.addElement( new StringInput("senderName", t._("Sender name"),senderName,true));
form.addElement( new StringInput("senderMail", t._("Sender E-Mail"),senderMail,true));
form.addElement( new StringSelect("list", t._("Recipients"),lists,"1", true,null,"style='width:500px;'"));
form.addElement( new StringInput("subject", t._("Subject:"),"",false,null,"style='width:500px;'") );
form.addElement( new TextArea("text", t._("Message:"), "", false, null, "style='width:500px;height:350px;'") );
if (form.checkToken()) {
var listId = form.getElement("list").value;
var dest = getSelection(listId);
var mails = [];
for ( d in dest) {
if (d.email != null) mails.push(d.email);
if (d.email2 != null) mails.push(d.email2);
}
//send mail confirmation link
var e = new sugoi.mail.Mail();
e.setSubject(form.getValueOf("subject"));
for ( x in mails) e.addRecipient(x);
e.setSender(App.config.get("default_email"),form.getValueOf("senderName"));
e.setReplyTo(form.getValueOf("senderMail"),form.getValueOf("senderName"));
//sender : default email ( explicitly tells that the server send an email on behalf of the user )
//e.setHeader("Sender", App.config.get("default_email"));
var text :String = form.getValueOf("text");
var html = app.processTemplate("mail/message.mtt", { text:text,group:app.user.amap,list:getListName(listId) });
e.setHtmlBody(html);
App.sendMail(e,app.user.getAmap(),listId,app.user);
//store message
var lm = new db.Message();
lm.amap = app.user.amap;
lm.recipients = Lambda.array(Lambda.map(e.getRecipients(), function(x) return x.email));
lm.title = e.getSubject();
lm.date = Date.now();
lm.body = e.getHtmlBody();
if (listId != null) lm.recipientListId = listId;
lm.sender = app.user;
lm.insert();
throw Ok("/messages", t._("The message has been sent"));
}
view.form = form;
if (app.user.isAmapManager()) {
view.sentMessages = Message.manager.search($amap == app.user.amap && $recipientListId!=null, {orderBy:-date,limit:20}, false);
}else {
view.sentMessages = Message.manager.search($sender == app.user && $recipientListId!=null && $amap == app.user.amap , {orderBy:-date,limit:20}, false);
}
}
@tpl("messages/message.mtt")
public function doMessage(msg:Message) {
if (!app.user.isAmapManager() && msg.sender.id != app.user.id) throw Error("/", t._("Non authorized access"));
view.list = getListName(msg.recipientListId);
view.msg = msg;
//make status easier to display
var s = new Array<{email:String,success:String,failure:String}>();
/*if (msg.status != null){
for ( k in msg.status.keys()) {
var r = msg.getMailerResultMessage(k);
s.push({email:k,success:r.success,failure:r.failure});
}
}*/
view.status = s;
}
function getLists() :FormData<String>{
var out = [
{value:'1', label: t._("Everyone")},
{value:'2', label: t._("The board: persons in charge + contracts + memberships")},
];
out.push( { value:'3', label: t._("TEST: me + spouse") } );
out.push( { value:'4', label: t._("Members without contract/order") } );
if(app.user.amap.hasMembership()) out.push( { value:'5', label:t._("Memberships to be renewed")} );
var contracts = db.Contract.getActiveContracts(app.user.amap,true);
for ( c in contracts) {
var label = t._("Subscribers") + " " + c.toString();
out.push({value:'c'+c.id,label:label});
}
return out ;
}
/**
* get list name from id
* @param listId
*/
function getListName(listId:String) {
var l = getLists();
for (ll in l) {
if (ll.value == listId) return ll.label;
}
return null;
}
function getSelection(listId:String) {
if (listId.substr(0, 1) == "c") {
//contrats
var contract = Std.parseInt(listId.substr(1));
var pids = db.Product.manager.search($contractId == contract, false);
var pids = Lambda.map(pids, function(x) return x.id);
var up = db.UserContract.manager.search($productId in pids, false);
var users = [];
for ( order in up) {
if (!Lambda.has(users, order.user)) {
users.push(order.user);
}
if (order.user2 != null && !Lambda.has(users, order.user2)) {
users.push(order.user2);
}
}
return users;
}else {
var out = [];
switch(listId) {
case "1":
//tout le monde
out = Lambda.array(app.user.amap.getMembers());
case "2":
var users = [];
users.push(app.user.amap.contact);
for ( c in db.Contract.manager.search($amap == app.user.amap)) {
if (!Lambda.has(users, c.contact)) {
users.push(c.contact);
}
}
//ajouter les autres personnes ayant les droits Admin ou Gestion Adhérents ou Gestion Contrats
for (ua in Lambda.array(db.UserAmap.manager.search($rights != null && $amap == app.user.amap, false))) {
if (ua.hasRight(GroupAdmin) || ua.hasRight(Membership) || ua.hasRight(ContractAdmin())) {
if (!Lambda.has(users, ua.user)) users.push(ua.user);
}
}
out = users;
case "3":
//moi
return [app.user];
case "4":
return Lambda.array(db.User.getUsers_NoContracts());
case "5":
return Lambda.array(db.User.getUsers_NoMembership());
}
return out;
}
}
}
+87
View File
@@ -0,0 +1,87 @@
package controller;
import sugoi.form.Form;
/**
* Place controller
*/
class Place extends Controller
{
public function new()
{
super();
}
@tpl('place/view.mtt')
function doView(place:db.Place) {
view.place = place;
//build adress for google maps
var addr = "";
if (place.address1 != null) addr += place.address1;
if (place.address2 != null) addr += ", " + place.address2;
if (place.zipCode != null) addr += " " + place.zipCode;
if (place.city != null) addr += " " + place.city;
view.addr = view.escapeJS(addr);
}
@tpl('form.mtt')
function doEdit(p:db.Place) {
var currentAddress = p.getAddress();
var f = sugoi.form.Form.fromSpod(p);
if (f.isValid()) {
f.toSpod(p);
if(currentAddress!=p.getAddress() || p.lat==null){
try{
service.PlaceService.geocode(p);
}catch(e:Dynamic){
App.current.session.addMessage(t._("Oops, we're unable to find where is located this address. This place will not be shown on the map.")+'<br/>$e',true);
}
}
p.amap = app.user.amap;
p.update();
throw Ok('/contractAdmin',t._("this place has been updated"));
}
view.form = f;
view.title = t._("Edit a place");
}
@tpl("form.mtt")
public function doInsert() {
var d = new db.Place();
var f = sugoi.form.Form.fromSpod(d);
if (f.isValid()) {
f.toSpod(d);
d.amap = app.user.amap;
d.insert();
throw Ok('/contractAdmin',t._("The place has been registred") );
}
view.form = f;
view.title = t._("Register a new delivery place");
}
public function doDelete(p:db.Place) {
if (!app.user.isAmapManager()) throw "forbidden";
if (checkToken()) {
if (db.Distribution.manager.search($placeId == p.id).length > 0)
throw Error('/contractAdmin', t._("You can't delete this place because one or more distributions are linked to this place.") );
p.lock();
p.delete();
throw Ok("/contractAdmin", t._("Place deleted") );
}
}
}
+39
View File
@@ -0,0 +1,39 @@
package controller;
/**
* ...
* @author fbarbut<francois.barbut@gmail.com>
*/
class Plugin extends sugoi.BaseController
{
public function new()
{
super();
}
#if plugins
//cagette-hosted
public function doHosted(d:haxe.web.Dispatch) {
d.dispatch(new hosted.controller.Main());
}
//cagette-pro
public function doPro(d:haxe.web.Dispatch) {
d.dispatch(new pro.controller.Main());
}
//cagette-connector
public function doConnector(d:haxe.web.Dispatch) {
d.dispatch(new connector.controller.Main());
}
//cagette-wholesale-order
public function doWho(d:haxe.web.Dispatch) {
d.dispatch(new who.controller.Main());
}
#end
}
+339
View File
@@ -0,0 +1,339 @@
package controller;
import sugoi.form.Form;
import Common;
import sugoi.form.ListData.FormData;
import sugoi.form.elements.FloatInput;
import sugoi.form.elements.FloatSelect;
import sugoi.form.elements.IntSelect;
using Std;
class Product extends Controller
{
public function new()
{
super();
view.nav = ["contractadmin","products"];
}
@tpl('form.mtt')
function doEdit(d:db.Product) {
if (!app.user.canManageContract(d.contract)) throw t._("Forbidden access");
var f = sugoi.form.Form.fromSpod(d);
//stock mgmt ?
if (!d.contract.hasStockManagement()) f.removeElementByName('stock');
//VAT selector
f.removeElement( f.getElement('vat') );
var data :FormData<Float> = [];
for (k in app.user.amap.vatRates.keys()) {
data.push( { label:k, value:app.user.amap.vatRates[k] } );
}
f.addElement( new FloatSelect("vat", "TVA", data, d.vat ) );
f.removeElementByName("contractId");
//Product Taxonomy:
//view.taxo = db.TxpProduct.manager.all();
//f.addElement(new form.TxpProduct("txpProduct", "taxo",null,false) );
var txId = d.txpProduct == null ? "" : Std.string(d.txpProduct.id);
var html = '<div id="pInput"></div><script language="javascript">_.getProductInput("pInput","${d.name}","$txId","${f.name}");</script>';
f.addElement(new sugoi.form.elements.Html("html",html, 'Nom'),1);
if (f.isValid()) {
f.toSpod(d);
app.event(EditProduct(d));
d.update();
throw Ok('/contractAdmin/products/'+d.contract.id, t._("The product has been updated"));
}else{
app.event(PreEditProduct(d));
}
view.form = f;
view.title = t._("Modify a product");
}
@tpl("form.mtt")
public function doInsert(contract:db.Contract ) {
if (!app.user.isContractManager(contract)) throw Error("/", t._("Forbidden action"));
var d = new db.Product();
var f = sugoi.form.Form.fromSpod(d);
f.removeElementByName("contractId");
//stock mgmt ?
if (!contract.hasStockManagement()) f.removeElementByName('stock');
//vat selector
f.removeElement( f.getElement('vat') );
var data = [];
for (k in app.user.amap.vatRates.keys()) {
data.push( { value:app.user.amap.vatRates[k], label:k } );
}
f.addElement( new FloatSelect("vat", "TVA", data, d.vat ) );
var formName = f.name;
var html = '<div id="pInput"></div><script language="javascript">_.getProductInput("pInput","",null,"$formName");</script>';
f.addElement(new sugoi.form.elements.Html("html",html, 'Nom'),1);
if (f.isValid()) {
f.toSpod(d);
d.contract = contract;
app.event(NewProduct(d));
d.insert();
throw Ok('/contractAdmin/products/'+d.contract.id, t._("The product has been saved"));
}else{
app.event(PreNewProduct(contract));
}
view.form = f;
view.title = t._("Key-in a new product");
}
public function doDelete(p:db.Product) {
if (!app.user.canManageContract(p.contract)) throw t._("Forbidden access");
if (checkToken()) {
app.event(DeleteProduct(p));
var orders = db.UserContract.manager.search($productId == p.id, false);
if (orders.length > 0) {
throw Error("/contractAdmin", t._("Not possible to delete this product because some orders are referencing it"));
}
var cid = p.contract.id;
p.lock();
p.delete();
throw Ok("/contractAdmin/products/"+cid, t._("Product deleted"));
}
throw Error("/contractAdmin", t._("Token error"));
}
@tpl('product/import.mtt')
function doImport(c:db.Contract, ?args: { confirm:Bool } ) {
if (!app.user.canManageContract(c)) throw t._("Forbidden access");
var csv = new sugoi.tools.Csv();
csv.step = 1;
var request = sugoi.tools.Utils.getMultipart(1024 * 1024 * 4);
csv.setHeaders( ["productName","price","ref","desc","qt","unit","organic","floatQt","vat","stock"] );
view.contract = c;
// get the uploaded file content
if (request.get("file") != null) {
//convert to utf-8 if needed
var csvData = request.get("file");
try{
if (!haxe.Utf8.validate(csvData)){
csvData = haxe.Utf8.encode(csvData);
}
}catch (e:Dynamic){ }
var datas = csv.importDatasAsMap(csvData);
app.session.data.csvImportedData = datas;
csv.step = 2;
view.csv = csv;
}
if (args != null && args.confirm) {
var i : Iterable<Map<String,String>> = cast app.session.data.csvImportedData;
var fv = new sugoi.form.filters.FloatFilter();
for (p in i) {
if (p["productName"] != null){
var product = new db.Product();
product.name = p["productName"];
product.price = fv.filterString(p["price"]);
product.ref = p["ref"];
product.desc = p["desc"];
product.vat = fv.filterString(p["vat"]);
product.qt = fv.filterString(p["qt"]);
if(p["unit"]!=null){
product.unitType = switch(p["unit"].toLowerCase()){
case "kg" : Kilogram;
case "g" : Gram;
case "l" : Litre;
case "cl" : Centilitre;
case "litre" : Litre;
default : Piece;
}
}
if (p["stock"] != null) product.stock = fv.filterString(p["stock"]);
product.organic = p["organic"] != null;
product.hasFloatQt = p["floatQt"] != null;
product.contract = c;
product.insert();
}
}
view.numImported = app.session.data.csvImportedData.length;
app.session.data.csvImportedData = null;
csv.step = 3;
}
if (csv.step == 1) {
//reset import when back to import page
app.session.data.csvImportedData = null;
}
view.step = csv.step;
}
@tpl("product/categorize.mtt")
public function doCategorize(contract:db.Contract) {
if (!app.user.canManageContract(contract)) throw t._("Forbidden access");
if (db.CategoryGroup.get(app.user.amap).length == 0) throw Error("/contractAdmin", t._("You must first define categories before you can assign a category to a product"));
//var form = new sugoi.form.Form("cat");
//
//for ( g in db.CategoryGroup.get(app.user.amap)) {
//var data = [];
//for ( c in g.getCategories()) {
//data.push({key:Std.string(c.id),value:c.name});
//}
//form.addElement(new sugoi.form.elements.Selectbox("cats"+g.id,g.name,data));
//}
//
//view.form = form;
view.c = contract;
}
/**
* init du Tagger
* @param contract
*/
public function doCategorizeInit(contract:db.Contract) {
if (!app.user.canManageContract(contract)) throw t._("Forbidden access");
var data : TaggerInfos = {
products:[],
categories:[]
}
for (p in contract.getProducts()) {
data.products.push({product:p.infos(),categories:Lambda.array(Lambda.map(p.getCategories(),function(x) return x.id))});
}
for (cg in db.CategoryGroup.get(app.user.amap)) {
var x = { id:cg.id, categoryGroupName:cg.name, color:App.current.view.intToHex(db.CategoryGroup.COLORS[cg.color]),tags:[] };
for (t in cg.getCategories()) {
x.tags.push({id:t.id,name:t.name});
}
data.categories.push(x);
}
Sys.print(haxe.Json.stringify(data));
}
public function doCategorizeSubmit(contract:db.Contract) {
if (!app.user.canManageContract(contract)) throw t._("Forbidden access");
var data : TaggerInfos = haxe.Json.parse(app.params.get("data"));
db.ProductCategory.manager.unsafeDelete("delete from ProductCategory where productId in (" + Lambda.map(contract.getProducts(), function(t) return t.id).join(",")+")");
for (p in data.products) {
for (t in p.categories) {
var x = new db.ProductCategory();
x.category = db.Category.manager.get(t, false);
x.product = db.Product.manager.get(p.product.id,false);
x.insert();
}
}
Sys.print(t._("Modifications saved"));
}
@tpl('product/addimage.mtt')
function doAddImage(product:db.Product) {
if (!app.user.canManageContract(product.contract)) throw t._("Forbidden access");
view.c = product.contract;
view.image = product.image;
var request = sugoi.tools.Utils.getMultipart(1024 * 1024 * 12); //12Mb
if (request.exists("image")) {
//Image
var image = request.get("image");
if (image != null && image.length > 0) {
var img : sugoi.db.File = null;
if ( Sys.systemName() == "Windows") {
img = sugoi.db.File.create(request.get("image"), request.get("image_filename"));
}else {
img = sugoi.tools.UploadedImage.resizeAndStore(request.get("image"), request.get("image_filename"), 400, 400);
}
product.lock();
if (product.image != null) {
//efface ancienne
product.image.lock();
product.image.delete();
}
product.image = img;
product.update();
throw Ok('/product/addImage/'+product.id,'Image mise à jour');
}
}
}
@tpl('product/compose.mtt')
function doCompose(){
}
function doGetTaxo(){
var out : TxpDictionnary = {products:new Map(), categories:new Map(), subCategories:new Map()};
for ( p in db.TxpProduct.manager.all()){
out.products.set(p.id, {id:p.id, name:p.name, category:p.category.id, subCategory:p.subCategory.id});
}
for ( c in db.TxpCategory.manager.all()){
out.categories.set(c.id, {id:c.id,name:c.name });
}
for ( c in db.TxpSubCategory.manager.all()){
out.subCategories.set(c.id, {id:c.id,name:c.name });
}
Sys.print(haxe.Serializer.run(out));
}
}
+264
View File
@@ -0,0 +1,264 @@
package controller;
import Common;
import tools.ArrayTool;
import service.OrderService;
class Shop extends Controller
{
var distribs : List<db.Distribution>;
var contracts : List<db.Contract>;
@tpl('shop/default.mtt')
public function doDefault(place:db.Place,date:Date) {
var products = getProducts(place,date);
view.products = products;
view.place = place;
view.date = date;
view.group = place.amap;
view.infos = ArrayTool.groupByDate(Lambda.array(distribs), "orderEndDate");
//message if phone is required
if(app.user!=null && app.user.amap.flags.has(db.Amap.AmapFlags.PhoneRequired) && app.user.phone==null){
app.session.addMessage(t._("Members of this group should provide a phone number. <a href='/account/edit'>Please click here to update your account</a>."),true);
}
//event for additionnal blocks on home page
var e = Blocks([], "shop");
app.event(e);
view.blocks = e.getParameters()[0];
}
/**
* prints the full product list and current cart in JSON
*/
public function doInit(place:db.Place,date:Date) {
//init order serverside if needed
var order :OrderInSession = app.session.data.order;
if ( order == null) {
app.session.data.order = order = cast {products:[]};
}
var products = [];
var categs = new Array<{name:String,pinned:Bool,categs:Array<CategoryInfo>}>();
if (place.amap.flags.has(db.Amap.AmapFlags.ShopCategoriesFromTaxonomy)){
//TAXO CATEGORIES
products = getProducts(place, date, true);
}else{
//CUSTOM CATEGORIES
products = getProducts(place, date, false);
}
categs = place.amap.getCategoryGroups();
//clean
for ( p in order.products){
p.product = null;
}
Sys.print( haxe.Serializer.run( {products:products,categories:categs,order:order} ) );
}
/**
* Get the available products list
*/
private function getProducts(place,date,?categsFromTaxo=false):Array<ProductInfo> {
contracts = db.Contract.getActiveContracts(app.getCurrentGroup());
for (c in Lambda.array(contracts)) {
//only varying orders
if (c.type != db.Contract.TYPE_VARORDER) {
contracts.remove(c);
}
if (!c.isVisibleInShop()) {
contracts.remove(c);
}
}
view.contracts = contracts;
var now = Date.now();
var cids = Lambda.map(contracts, function(c) return c.id);
var d1 = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
var d2 = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59);
//distribs open to orders, and where distribDate is in the date given as parameter
distribs = db.Distribution.manager.search(($contractId in cids) && $orderStartDate <= now && $orderEndDate >= now && $date > d1 && $end < d2 && $place == place, false);
var products = [];
for ( d in distribs){
for (p in d.contract.getProducts(true)){
products.push( p.infos(categsFromTaxo,null,d) );
}
}
return products;
/*var cids = Lambda.map(distribs, function(d) return d.contract.id);
var products = db.Product.manager.search(($contractId in cids) && $active==true, { orderBy:name }, false);
return Lambda.array(Lambda.map(products, function(p) return p.infos(categsFromTaxo)));*/
}
/**
* Overlay window loaded by Ajax for product Infos
*/
@tpl('shop/productInfo.mtt')
public function doProductInfo(p:db.Product,?args:{distribution:db.Distribution}) {
var d = args!=null && args.distribution!=null ? args.distribution : null;
view.p = p.infos(null,null,d);
view.product = p;
view.vendor = p.contract.vendor;
}
/**
* receive cart
*/
public function doSubmit() {
var order : OrderInSession = haxe.Json.parse(app.params.get("data"));
app.session.data.order = order;
}
/**
* add a product to the cart
*/
public function doAdd(productId:Int, quantity:Int) {
var order : OrderInSession = app.session.data.order;
if ( order == null) order = cast { products:[] };
order.products.push( { productId:productId, quantity:quantity } );
Sys.print( haxe.Json.stringify( {success:true} ) );
}
/**
* remove a product from cart
*/
public function doRemove(pid:Int) {
var order:OrderInSession = app.session.data.order;
if ( order == null) return;
for ( p in order.products.copy()) {
if (p.productId == pid) {
order.products.remove(p);
}
}
Sys.print( haxe.Json.stringify( { success:true } ) );
}
/**
* validate the order
*/
@tpl('shop/needLogin.mtt')
public function doValidate(place:db.Place, date:Date){
//login is needed : display a loginbox
if (app.user == null) {
view.redirect = "/shop/validate/" + place.id + "/" + date.toString().substr(0, 10);
view.group = place.amap;
view.register = true;
view.message = t._("In order to confirm your order, You need to authenticate.");
return;
}
//add the user to this group if needed
if (place.amap.regOption == db.Amap.RegOption.Open && db.UserAmap.get(app.user, place.amap) == null){
app.user.makeMemberOf(place.amap);
}
var order : OrderInSession = app.session.data.order;
if (order == null || order.products == null || order.products.length == 0) {
throw Error("/", t._("Your order is empty") );
}
if (place == null) throw "place cannot be empty";
if (date == null) throw "date cannot be empty";
var products = getProducts(place, date);
var errors = [];
order.total = 0.0;
//cleaning
for (o in order.products.copy()) {
var p = db.Product.manager.get(o.productId, false);
//check that the products are from this group (we never know...)
if (p.contract.amap.id != app.user.amap.id){
app.session.data.order = null;
throw Error("/", t._("This cart is invalid") );
}
//check if the product is available
if (Lambda.find(products, function(x) return x.id == o.productId) == null) {
errors.push( t._("This distribution does not supply the product <b>::pname::</b>",{pname:p.name}) );
order.products.remove(o);
continue;
}else{
o.product = p;
}
//find distrib
var d = Lambda.find(distribs, function(d) return d.contract.id == p.contract.id);
if ( d == null ){
errors.push( t._("This distribution does not supply the product <b>::pname::</b>",{pname:p.name}) );
order.products.remove(o);
continue;
}else{
o.distributionId = d.id;
}
//moderate order according available stocks
if (p.stock != null && p.contract.hasStockManagement() ) {
if (p.stock - o.quantity < 0) {
var canceled = o.quantity - p.stock;
o.quantity -= canceled;
errors.push(t._("Order of ::pname:: reduced to ::oquantity:: to match remaining stock", {pname:p.name, oquantity:o.quantity}));
}
}
order.total += p.getPrice() * o.quantity;
}
order.userId = app.user.id;
if (errors.length > 0) {
app.session.addMessage(errors.join("<br/>"), true);
}
//store cart in session
app.session.data.order = order;
//debug
for ( p in order.products){
if (p.distributionId == null){
App.current.logError(place.amap.name + " : panier sans distrib Id : " + Std.string(order) );
break;
}
}
if (app.user.amap.hasPayments()){
//Go to payments page
throw Redirect("/transaction/pay/");
}else{
//no payments, confirm direclty
OrderService.confirmSessionOrder(order);
throw Ok("/contract", t._("Your order has been confirmed") );
}
}
}
+18
View File
@@ -0,0 +1,18 @@
package controller;
class Stats extends Controller
{
public function new()
{
super();
}
@tpl("stats/default.mtt")
function doDefault() {
}
}
+294
View File
@@ -0,0 +1,294 @@
package controller;
import db.Operation.OperationType;
import Common;
import service.OrderService;
using Lambda;
/**
* Transction controller
* @author fbarbut
*/
class Transaction extends controller.Controller
{
/**
* A manager inserts manually a payment
*/
@tpl('form.mtt')
public function doInsertPayment(user:db.User){
if (!app.user.isContractManager()) throw Error("/", t._("Action forbidden"));
var t = sugoi.i18n.Locale.texts;
var op = new db.Operation();
op.user = user;
op.date = Date.now();
var f = new sugoi.form.Form("payement");
f.addElement(new sugoi.form.elements.StringInput("name", t._("Label||label or name for a payment"), null, true));
f.addElement(new sugoi.form.elements.FloatInput("amount", t._("Amount"), null, true));
f.addElement(new sugoi.form.elements.DatePicker("date", t._("Date"), Date.now(), true));
var paymentTypes = service.PaymentService.getPaymentTypesForManualEntry(app.user.amap);
f.addElement(new sugoi.form.elements.StringSelect("Mtype", t._("Payment type"), paymentTypes, null, true));
//related operation
var unpaid = db.Operation.manager.search($user == user && $group == app.user.amap && $type != Payment ,{limit:20,orderBy:-date});
var data = unpaid.map(function(x) return {label:x.name, value:x.id}).array();
f.addElement(new sugoi.form.elements.IntSelect("unpaid", t._("As a payment for :"), data, null, false));
if (f.isValid()){
f.toSpod(op);
op.type = db.Operation.OperationType.Payment;
var data : db.Operation.PaymentInfos = {type:f.getValueOf("Mtype")};
op.data = data;
op.group = app.user.amap;
op.user = user;
if (f.getValueOf("unpaid") != null){
var t2 = db.Operation.manager.get(f.getValueOf("unpaid"));
op.relation = t2;
if (t2.amount + op.amount == 0) {
op.pending = false;
t2.lock();
t2.pending = false;
t2.update();
}
}
op.insert();
service.PaymentService.updateUserBalance(user, app.user.amap);
throw Ok("/member/payments/" + user.id, t._("Payment recorded") );
}
view.title = t._("Record a payment for ::user::",{user:user.getCoupleName()}) ;
view.form = f;
}
@tpl('form.mtt')
public function doEdit(op:db.Operation){
if (!app.user.canAccessMembership() || op.group.id != app.user.amap.id ) {
throw Error("/member/payments/" + op.user.id, t._("Action forbidden"));
}
if (op.getPaymentType() == "lemonway-ec") throw Error("/member/payments/"+op.user.id, t._("Editing a credit card payment is not allowed"));
op.lock();
var f = new sugoi.form.Form("payement");
f.addElement(new sugoi.form.elements.StringInput("name", t._("Label||label or name for a payment"), op.name, true));
f.addElement(new sugoi.form.elements.FloatInput("amount", t._("Amount"), op.amount, true));
f.addElement(new sugoi.form.elements.DatePicker("date", t._("Date"), op.date, true));
//f.addElement(new sugoi.form.elements.DatePicker("pending", t._("Confirmed"), !op.pending, true));
//related operation
var unpaid = db.Operation.manager.search( $user == op.user && $group == op.group && $type != Payment ,{limit:20,orderBy:-date});
var data = unpaid.map(function(x) return {label:x.name, value:x.id}).array();
if (op.relation != null) data.push({label:op.relation.name,value:op.relation.id});
f.addElement(new sugoi.form.elements.IntSelect("unpaid", t._("As a payment for :"), data, op.relation!=null ? op.relation.id : null, false));
if (f.isValid()){
f.toSpod(op);
op.pending = false;
if (f.getValueOf("unpaid") != null){
var t2 = db.Operation.manager.get(f.getValueOf("unpaid"));
op.relation = t2;
if (t2.amount + op.amount == 0) {
op.pending = false;
t2.lock();
t2.pending = false;
t2.update();
}
}
op.update();
throw Ok("/member/payments/" + op.user.id, t._("Operation updated"));
}
view.form = f;
}
/**
* Delete an operation
*/
public function doDelete(op:db.Operation){
if (!app.user.canAccessMembership() || op.group.id != app.user.amap.id ) throw Error("/member/payments/" + op.user.id, t._("Action forbidden"));
//cannot delete a bank card payment op
if (op.getPaymentType() == "lemonway-ec"){
throw Error("/member/payments/" + op.user.id, t._("Deleting a credit card payment is not allowed"));
}
//only an admin can delete an order op
if((op.type == db.Operation.OperationType.VOrder || op.type == db.Operation.OperationType.COrder) && !app.user.isAdmin()){
throw Error("/member/payments/" + op.user.id, t._("Action forbidden"));
}
if (checkToken()){
op.delete();
throw Ok("/member/payments/" + op.user.id, t._("Operation deleted"));
}
}
/**
* payement entry page
* @param distribKey
*/
@tpl("transaction/pay.mtt")
public function doPay() {
view.category = 'home';
var order : OrderInSession = app.session.data.order;
if (order == null) throw Redirect("/");
if (order.products.length == 0) throw Error("/", t._("Your cart is empty"));
view.amount = order.total;
view.paymentTypes = service.PaymentService.getAllowedPaymentTypes(app.user.amap);
view.allowMoneyPotWithNegativeBalance = app.user.amap.allowMoneyPotWithNegativeBalance;
view.futurebalance = db.UserAmap.get(app.user, app.user.amap).balance - order.total;
}
/**
* pay by check
*/
@tpl("transaction/check.mtt")
public function doCheck(){
//order in session
var tmpOrder : OrderInSession = app.session.data.order;
if (tmpOrder == null) throw Redirect("/contract");
if (tmpOrder.products.length == 0) throw Error("/", t._("Your cart is empty"));
//get a code
var d = db.Distribution.manager.get(tmpOrder.products[0].distributionId, false);
var code = payment.Check.getCode(d.date, d.place, app.user);
view.code = code;
view.amount = tmpOrder.total;
//if (checkToken()){
//record order
var orders = OrderService.confirmSessionOrder(tmpOrder);
var ops = db.Operation.onOrderConfirm(orders);
var ordersGrouped = tools.ObjectListTool.groupOrdersByKey(orders);
if (Lambda.array(ordersGrouped).length == 1){
//all orders are for the same multidistrib
var name = t._("Check for the order of ::date::", {date:view.hDate(d.date)}) + " ("+code+")";
db.Operation.makePaymentOperation(app.user,app.user.amap, payment.Check.TYPE, tmpOrder.total, name, ops[0] );
}else{
//orders are for multiple distribs : create one payment
db.Operation.makePaymentOperation(app.user,app.user.amap,payment.Check.TYPE, tmpOrder.total, t._("Check") + " ("+code+")" );
}
//throw Ok("/contract", t._("Your payment by check has been saved. It will be validated by a coordinator at the delivery."));
//}
}
/**
* pay by transfer
*/
@tpl("transaction/transfer.mtt")
public function doTransfer(){
//order in session
var tmpOrder : OrderInSession = app.session.data.order;
if (tmpOrder == null) throw Redirect("/contract");
if (tmpOrder.products.length == 0) throw Error("/", t._("Your cart is empty"));
//get a code
var d = db.Distribution.manager.get(tmpOrder.products[0].distributionId, false);
var code = payment.Check.getCode(d.date, d.place, app.user);
view.code = code;
view.amount = tmpOrder.total;
//if (checkToken()){
//record order
var orders = OrderService.confirmSessionOrder(tmpOrder);
var ops = db.Operation.onOrderConfirm(orders);
var ordersGrouped = tools.ObjectListTool.groupOrdersByKey(orders);
if (Lambda.array(ordersGrouped).length == 1){
//one multidistrib
var name = t._("Transfer for the order of ::date::", {date:view.hDate(d.date)}) + " ("+code+")";
db.Operation.makePaymentOperation(app.user,app.user.amap,payment.Transfer.TYPE, tmpOrder.total, name, ops[0] );
}else{
//many distribs
db.Operation.makePaymentOperation(app.user,app.user.amap,payment.Transfer.TYPE, tmpOrder.total, t._("Bank transfer")+" ("+code+")" );
}
//throw Ok("/contract", t._("Your payment by transfer has been saved. It will be validated by a coordinator."));
//}
}
/**
* pay by cash
*/
@tpl("transaction/cash.mtt")
public function doCash(){
//order in session
var tmpOrder : OrderInSession = app.session.data.order;
if (tmpOrder == null) throw Redirect("/contract");
if (tmpOrder.products.length == 0) throw Error("/", t._("Your cart is empty"));
view.amount = tmpOrder.total;
var d = db.Distribution.manager.get(tmpOrder.products[0].distributionId, false);
//if (checkToken()){
//record order
var orders = OrderService.confirmSessionOrder(tmpOrder);
var ops = db.Operation.onOrderConfirm(orders);
var ordersGrouped = tools.ObjectListTool.groupOrdersByKey(orders);
if (Lambda.array(ordersGrouped).length == 1){
//same multidistrib
var name = t._("Cash for the order of ::date::", {date:view.hDate(d.date)});
db.Operation.makePaymentOperation(app.user,app.user.amap,payment.Cash.TYPE, tmpOrder.total, name , ops[0] );
}else{
//various distribs
db.Operation.makePaymentOperation(app.user, app.user.amap, payment.Cash.TYPE, tmpOrder.total, t._("Cash payment"));
}
//throw Ok("/contract", t._("Your order is validated, you commited to pay in cash at the delivery."));
//}
}
/**
* Use the money pot
*/
@tpl("transaction/moneypot.mtt")
public function doMoneypot(){
//order in session
var tmpOrder : OrderInSession = app.session.data.order;
if (tmpOrder == null) throw Redirect("/contract");
if (tmpOrder.products.length == 0) throw Error("/", t._("Your cart is empty"));
var futureBalance = db.UserAmap.get(app.user, app.user.amap).balance - tmpOrder.total;
if (!app.user.amap.allowMoneyPotWithNegativeBalance && futureBalance < 0) {
throw Error("/transaction/pay", t._("You do not have sufficient funds to pay this order with your money pot."));
}
//record order
var orders = OrderService.confirmSessionOrder(tmpOrder);
var ops = db.Operation.onOrderConfirm(orders);
view.amount = tmpOrder.total;
view.balance = db.UserAmap.get(app.user, app.user.amap).balance;
}
}
+255
View File
@@ -0,0 +1,255 @@
package controller;
import haxe.crypto.Md5;
import sugoi.form.elements.Input;
import sugoi.form.Form;
import sugoi.form.elements.IntInput;
import sugoi.form.elements.StringInput;
import sugoi.form.validators.EmailValidator;
import ufront.mail.*;
class User extends Controller
{
public function new()
{
super();
}
@tpl("user/default.mtt")
function doDefault() {
}
@tpl("user/login.mtt")
function doLogin() {
if (App.current.user != null) {
throw Redirect('/');
}
//if its needed to redirect after login
if (app.params.exists("redirect")){
view.redirect = app.params.exists("redirect");
}else{
view.redirect = "/";
}
}
/**
* Choose which group to connect to.
*/
@logged
@tpl("user/choose.mtt")
function doChoose(?args: { amap:db.Amap } ) {
if (app.user == null) throw t._("You are not connected");
var amaps = db.UserAmap.manager.search($user == app.user, false);
if (amaps.length == 1 && !app.params.exists("show")) {
//qu'une amap
app.session.data.amapId = amaps.first().amap.id;
throw Redirect('/');
}else{
view.noGroup = true; //force template to not display current group
}
if (args!=null && args.amap!=null) {
//select a group
var which = app.session.data==null ? 0 : app.session.data.whichUser ;
app.session.data.order = null;
app.session.data.newGroup = null;
app.session.data.amapId = args.amap.id;
app.session.data.whichUser = which;
throw Redirect('/');
}
view.amaps = amaps;
view.wl = db.WaitingList.manager.search($user == app.user, false);
#if plugins
view.pros = pro.db.PUserCompany.getCompanies(app.user);
#end
}
function doLogout() {
App.current.session.delete();
throw Redirect('/');
}
/**
* Ask for password renewal by mail
* when password is forgotten
*/
@tpl("user/forgottenPassword.mtt")
function doForgottenPassword(?key:String, ?u:db.User){
//STEP 1
var step = 1;
var error : String = null;
var url = "/user/forgottenPassword";
//ask for mail
var askmailform = new Form("askemail");
askmailform.addElement(new StringInput("email", t._("Please key-in your E-Mail address"),null,true));
//change pass form
var chpassform = new Form("chpass");
var pass1 = new StringInput("pass1", t._("Your new password"),null,true);
pass1.password = true;
chpassform.addElement(pass1);
var pass2 = new StringInput("pass2", t._("Again your new password"),null,true);
pass2.password = true;
chpassform.addElement(pass2);
var uid = new IntInput("uid","uid", u == null?null:u.id);
uid.inputType = ITHidden;
chpassform.addElement(uid);
if (askmailform.isValid()) {
//STEP 2
//send password renewal email
step = 2;
var email :String = askmailform.getValueOf("email");
var user = db.User.manager.select(email == $email, false);
//could be user 2
if(user==null) user = db.User.manager.select(email == $email2, false);
//user not found
if (user == null) throw Error(url, t._("This E-mail is not linked to a known account"));
//create token
var token = haxe.crypto.Md5.encode("chp"+Std.random(1000000000));
sugoi.db.Cache.set(token, user.id, 60 * 60 * 24 * 30);
var m = new sugoi.mail.Mail();
m.setSender(App.config.get("default_email"), t._("Cagette.net"));
m.setRecipient(email, user.name);
m.setSubject( "["+App.config.NAME+"] : "+t._("Password change"));
m.setHtmlBody( app.processTemplate('mail/forgottenPassword.mtt', { user:user, link:'http://' + App.config.HOST + '/user/forgottenPassword/'+token+"/"+user.id }) );
App.sendMail(m);
}
if (key != null && u!=null) {
//check key and propose to change pass
step = 3;
if ( u.id == sugoi.db.Cache.get(key) ) {
view.form = chpassform;
}else {
error = t._("Invalid request");
}
}
if (chpassform.isValid()) {
//change pass
step = 4;
if ( chpassform.getValueOf("pass1") == chpassform.getValueOf("pass2")) {
var uid = Std.parseInt( chpassform.getValueOf("uid") );
var user = db.User.manager.get(uid, true);
var pass = chpassform.getValueOf("pass1");
user.setPass(pass);
user.update();
var m = new sugoi.mail.Mail();
m.setSender(App.config.get("default_email"), t._("Cagette.net"));
m.setRecipient(user.email, user.name);
if(user.email2!=null) m.setRecipient(user.email2, user.name);
m.setSubject( "["+App.config.NAME+"] : "+t._("New password confirmed"));
var emails = [user.email];
if(user.email2!=null) emails.push(user.email2);
var params = {
user:user,
emails:emails.join(", "),
password:pass,
NAME:App.config.NAME
}
m.setHtmlBody( app.processTemplate('mail/newPasswordConfirmed.mtt', params) );
App.sendMail(m);
}else {
error = t._("You must key-in two times the same password");
}
}
if (step == 1) {
view.form = askmailform;
}
view.step = step;
view.error = error;
}
/**
* generate a custom key for transactionnal emails, valid during the current day
*/
//function getKey(m:db.User) {
//return haxe.crypto.Md5.encode(App.config.get("key")+m.email+(Date.now().getDate())).substr(0,12);
//}
@logged
@tpl("form.mtt")
function doDefinePassword(?key:String, ?u:db.User){
if (app.user.isFullyRegistred()) throw Error("/", t._("You already have a password"));
var form = new Form("definepass");
var pass1 = new StringInput("pass1", t._("Your new password"));
var pass2 = new StringInput("pass2", t._("Again your new password"));
pass1.password = true;
pass2.password = true;
form.addElement(pass1);
form.addElement(pass2);
if (form.isValid()) {
if ( form.getValueOf("pass1") == form.getValueOf("pass2")) {
app.user.lock();
app.user.setPass(form.getValueOf("pass1"));
app.user.update();
throw Ok('/', t._("Congratulations, your account is now protected by a password."));
}else {
form.addError( t._("You must key-in two times the same password"));
}
}
view.form = form;
view.title = t._("Create a password for your account");
}
/**
* landing page when coming from an invitation
* @param k
*/
public function doValidate(k:String ) {
var uid = Std.parseInt(sugoi.db.Cache.get("validation" + k));
if (uid == null || uid==0) throw Error('/user/login', t._("Your invitation is invalid or expired ($k)"));
var user = db.User.manager.get(uid, true);
db.User.login(user, user.email);
var groups = user.getAmaps();
if(groups.length>0) app.session.data.amapId = groups.first().id;
sugoi.db.Cache.destroy("validation" + k);
throw Ok("/user/definePassword", t._("Congratulations ::userName::, your account is validated!", {userName:user.getName()}));
}
}
+154
View File
@@ -0,0 +1,154 @@
package controller;
import db.Operation.OperationType;
using Lambda;
/**
* Distribution validation
* @author fbarbut
*/
class Validate extends controller.Controller
{
public var date : Date;
public var user : db.User;
public var place : db.Place;
@tpl('validate/user.mtt')
public function doDefault(){
view.member = user;
if (!app.user.amap.hasShopMode()){
//get last operations and check balance
view.operations = db.Operation.getLastOperations(this.user,place.amap,10);
view.balance = db.UserAmap.get(this.user, place.amap).balance;
}
var b = db.Basket.get(user, place, date);
view.orders = service.OrderService.prepare(b.getOrders());
view.place = place;
view.date = date;
view.basket = b;
checkToken();
}
public function doDeleteOp(op:db.Operation){
if (checkToken()){
op.lock();
op.delete();
service.PaymentService.updateUserBalance(user, app.user.amap);
throw Ok("/validate/"+date+"/"+place.id+"/"+user.id, t._("Operation deleted"));
}
}
public function doValidateOp(op:db.Operation){
if (checkToken()){
op.lock();
op.pending = false;
op.update();
service.PaymentService.updateUserBalance(user, app.user.amap);
throw Ok("/validate/"+date+"/"+place.id+"/"+user.id, t._("Operation validated"));
}
}
@tpl('form.mtt')
public function doAddRefund(){
if (!app.user.isContractManager()) throw t._("Forbidden access");
var o = new db.Operation();
o.user = user;
o.date = Date.now();
var b = db.Basket.get(user, place, date);
var op = b.getOrderOperation(false);
if(op==null) throw "unable to find related order operation";
var f = new sugoi.form.Form(t._("payment"));
f.addElement(new sugoi.form.elements.StringInput("name", t._("Label"), t._("Refund"), true));
f.addElement(new sugoi.form.elements.FloatInput("amount", t._("Amount"), null, true));
f.addElement(new sugoi.form.elements.DatePicker("date", "Date", Date.now(), true));
var paymentTypes = service.PaymentService.getPaymentTypesForManualEntry(app.user.amap);
f.addElement(new sugoi.form.elements.StringSelect("Mtype", t._("Payment type"), paymentTypes, null, true));
if (f.isValid()){
f.toSpod(o);
o.type = db.Operation.OperationType.Payment;
var data : db.Operation.PaymentInfos = {type:f.getValueOf("Mtype")};
o.data = data;
o.group = app.user.amap;
o.user = user;
o.relation = op;
o.amount = 0-Math.abs(o.amount);
o.insert();
App.current.event(NewOperation(o));
service.PaymentService.updateUserBalance(user, app.user.amap);
throw Ok("/validate/"+date+"/"+place.id+"/"+user.id, t._("Refund saved"));
}
view.title = t._("Key-in a refund for ::user::",{user:user.getCoupleName()});
view.form = f;
}
@tpl('form.mtt')
public function doAddPayment(){
if (!app.user.isContractManager()) throw Error("/",t._("Forbidden access"));
var o = new db.Operation();
o.user = user;
o.date = Date.now();
var f = new sugoi.form.Form("payment");
f.addElement(new sugoi.form.elements.StringInput("name", t._("Label"), t._("Additional payment"), true));
f.addElement(new sugoi.form.elements.FloatInput("amount", t._("Amount"), null, true));
f.addElement(new sugoi.form.elements.DatePicker("date", t._("Date"), Date.now(), true));
var paymentTypes = service.PaymentService.getPaymentTypesForManualEntry(app.user.amap);
f.addElement(new sugoi.form.elements.StringSelect("Mtype", t._("Payment type"), paymentTypes, null, true));
var b = db.Basket.get(user, place, date);
var op = b.getOrderOperation(false);
if(op==null) throw "unable to find related order operation";
if (f.isValid()){
f.toSpod(o);
o.type = db.Operation.OperationType.Payment;
var data : db.Operation.PaymentInfos = {type:f.getValueOf("Mtype")};
o.data = data;
o.group = app.user.amap;
o.user = user;
o.relation = op;
o.insert();
service.PaymentService.updateUserBalance(user, app.user.amap);
throw Ok("/validate/"+date+"/"+place.id+"/"+user.id, t._("Payment saved"));
}
view.title = t._("Key-in a payment for ::user::",{user:user.getCoupleName()});
view.form = f;
}
public function doValidate(){
if (checkToken()){
var basket = db.Basket.get(user, place, date);
service.PaymentService.validateBasket(basket);
throw Ok("/distribution/validate/"+date+"/"+place.id, t._("Order validated"));
}
}
}
+124
View File
@@ -0,0 +1,124 @@
package controller;
import db.UserContract;
import sugoi.form.elements.Selectbox;
import sugoi.form.Form;
import neko.Web;
import sugoi.tools.Utils;
class Vendor extends Controller
{
public function new()
{
super();
if (!app.user.isContractManager()) throw t._("Forbidden access");
}
@logged
@tpl('vendor/default.mtt')
function doDefault() {
var browse:Int->Int->List<Dynamic>;
//default display
browse = function(index:Int, limit:Int) {
return db.Vendor.manager.search($id > index && $amap==app.user.amap, { limit:limit, orderBy:-id }, false);
}
var count = db.Vendor.manager.count($amap==app.user.amap);
var rb = new sugoi.tools.ResultsBrowser(count, 10, browse);
view.vendors = rb;
}
@tpl("vendor/view.mtt")
function doView(vendor:db.Vendor) {
view.vendor = vendor;
}
@tpl('form.mtt')
function doEdit(vendor:db.Vendor) {
var form = sugoi.form.Form.fromSpod(vendor);
form.removeElement( form.getElement("amapId") );
if (form.isValid()) {
form.toSpod(vendor); //update model
vendor.amap = app.user.amap;
vendor.update();
throw Ok('/contractAdmin', t._("This supplier has been updated"));
}
view.form = form;
}
@tpl("form.mtt")
public function doInsert() {
var m = new db.Vendor();
var form = sugoi.form.Form.fromSpod(m);
form.removeElement(form.getElement("amapId"));
if (form.isValid()) {
form.toSpod(m); //update model
m.amap = app.user.amap;
m.insert();
throw Ok('/contractAdmin/', t._("This supplier has been saved"));
}
view.form = form;
}
public function doDelete(v:db.Vendor) {
if (!app.user.isAmapManager()) throw t._("Forbidden action");
if (checkToken()) {
if (db.Contract.manager.search($vendorId == v.id).length > 0) throw Error('/contractAdmin', t._("You cannot delete this supplier because some contracts (current or old) are referencing this supplier."));
v.lock();
v.delete();
throw Ok("/contractAdmin", t._("Supplier deleted"));
}
}
@tpl('vendor/addimage.mtt')
function doAddImage(v:db.Vendor) {
view.vendor = v;
view.image = v.image;
var request = sugoi.tools.Utils.getMultipart(1024 * 1024 * 12); //12Mb
if (request.exists("image")) {
//Image
var image = request.get("image");
if (image != null && image.length > 0) {
var img : sugoi.db.File = null;
if ( Sys.systemName() == "Windows") {
img = sugoi.db.File.create(request.get("image"), request.get("image_filename"));
}else {
img = sugoi.tools.UploadedImage.resizeAndStore(request.get("image"), request.get("image_filename"), 400, 400);
}
v.lock();
if (v.image != null) {
//efface ancienne
v.image.lock();
v.image.delete();
}
v.image = img;
v.update();
throw Ok('/contractAdmin/', t._("Image updated"));
}
}
}
}
+223
View File
@@ -0,0 +1,223 @@
package controller.admin;
import haxe.web.Dispatch;
import Common;
class Admin extends Controller {
public function new() {
super();
view.category = 'admin';
//trigger a "Nav" event
var nav = new Array<Link>();
var e = Nav(nav,"admin");
app.event(e);
view.nav = e.getParameters()[0];
}
@tpl("admin/default.mtt")
function doDefault() {
view.now = Date.now();
}
@tpl("admin/emails.mtt")
function doEmails() {
var browse = function(index:Int, limit:Int) {
return sugoi.db.BufferedMail.manager.search($sdate==null,{limit:[index,limit],orderBy:-cdate},false);
}
var count = sugoi.db.BufferedMail.manager.count($sdate==null);
view.browser = new sugoi.tools.ResultsBrowser(count,10,browse);
view.num = count;
}
@tpl("form.mtt")
function doSmtp() {
var f = new sugoi.form.Form("emails");
var data = [
{label:"SMTP",value:"smtp"},
{label:"Mandrill API",value:"mandrill"},
];
var mailer = sugoi.db.Variable.get("mailer")==null ? "smtp" : sugoi.db.Variable.get("mailer");
var host = sugoi.db.Variable.get("smtp_host")==null ? App.config.get("smtp_host") : sugoi.db.Variable.get("smtp_host");
var port = sugoi.db.Variable.get("smtp_port")==null ? App.config.get("smtp_port") : sugoi.db.Variable.get("smtp_port");
var user = sugoi.db.Variable.get("smtp_user")==null ? App.config.get("smtp_user") : sugoi.db.Variable.get("smtp_user");
var pass = sugoi.db.Variable.get("smtp_pass")==null ? App.config.get("smtp_pass") : sugoi.db.Variable.get("smtp_pass");
f.addElement(new sugoi.form.elements.StringSelect("mailer", "Mailer", data, mailer ));
f.addElement(new sugoi.form.elements.StringInput("smtp_host", "host", host));
f.addElement(new sugoi.form.elements.StringInput("smtp_port", "port", port));
f.addElement(new sugoi.form.elements.StringInput("smtp_user", "user", user));
f.addElement(new sugoi.form.elements.StringInput("smtp_pass", "pass", pass));
if (f.isValid()){
for ( k in ["mailer","smtp_host","smtp_port","smtp_user","smtp_pass"]){
sugoi.db.Variable.set(k, f.getValueOf(k));
}
throw Ok("/admin/emails", t._("Configuration updated") );
}
view.title = t._("Email service configuration");
view.form = f;
}
function doPlugins(d:Dispatch) {
d.dispatch(new controller.admin.Plugins());
}
@tpl("admin/taxo.mtt")
function doTaxo(){
view.categ = db.TxpCategory.manager.all();
}
/**
* Display errors logged in DB
*/
@tpl("admin/errors.mtt")
function doErrors( args:{?user: Int, ?like: String, ?empty:Bool} ) {
view.now = Date.now();
view.u = args.user!=null ? db.User.manager.get(args.user,false) : null;
view.like = args.like!=null ? args.like : "";
var sql = "";
if( args.user!=null ) sql += " AND uid="+args.user;
//if( args.like!=null && args.like != "" ) sql += " AND error like "+sys.db.Manager.cnx.quote("%"+args.like+"%");
if (args.empty) {
sys.db.Manager.cnx.request("truncate table Error");
}
var errorsStats = sys.db.Manager.cnx.request("select count(id) as c, DATE_FORMAT(date,'%y-%m-%d') as day from Error where date > NOW()- INTERVAL 1 MONTH "+sql+" group by day order by day").results();
view.errorsStats = errorsStats;
view.browser = new sugoi.tools.ResultsBrowser(
sugoi.db.Error.manager.unsafeCount("SELECT count(*) FROM Error WHERE 1 "+sql),
20,
function(start, limit) { return sugoi.db.Error.manager.unsafeObjects("SELECT * FROM Error WHERE 1 "+sql+" ORDER BY date DESC LIMIT "+start+","+limit,false); }
);
}
@tpl("admin/graph.mtt")
function doGraph(?key:String,?year:Int,?month:Int){
var from = new Date(year,month,1,0,0,0);
var to = new Date(year,month+1,0,23,59,59);
if(app.params.exists("recompute")){
switch(key){
case "basket":
for( d in 1...to.getDate()){
var _from = new Date(year,month,d,0,0,0);
var _to = new Date(year,month,d,23,59,59);
var value = db.Basket.manager.count($cdate>=_from && $cdate<=_to);
var g = db.Graph.record(key,value, _from );
// trace(value,_from,g);
}
}
}
var data = db.Graph.getRange(key,from,to);
view.data = data;
view.from = from;
view.to = to;
view.key = key;
var averageValue = 0.0;
var total = 0.0;
for( d in data) total += d.value;
averageValue = total/data.length;
view.total = total;
view.averageValue = averageValue;
}
function doFixDistribValidation() {
Sys.println("===== Liste des distributions ayant été re-validées ====<br>");
//Get the current group
var group = app.user.amap;
//Get all the contracts for this given group
var contractIds = Lambda.map(group.getContracts(),function(x) return x.id);
//Get all the validated distributions for this given group
var validatedDistribs = db.Distribution.manager.search( ($contractId in contractIds) && $validated == true, {orderBy:date}, false);
for (distrib in validatedDistribs){
service.PaymentService.validateDistribution(distrib);
Sys.println(distrib.toString() + "<br>");
}
Sys.println("===== Fin de la liste ====");
}
function doCheckDistribValidation() {
Sys.println("===== Liste des distributions validées ayant des opérations/paiements non validés ====<br>");
//Get the current group
var group = app.user.amap;
//Get all the contracts for this given group
var contractIds = Lambda.map(group.getContracts(),function(x) return x.id);
//Get all the validated distributions for this given group
var validatedDistribs = db.Distribution.manager.search( ($contractId in contractIds) && $validated == true, {orderBy:date}, false);
for (distrib in validatedDistribs){
for (user in distrib.getUsers()){
var basket = db.Basket.get(user, distrib.place, distrib.date);
if (basket == null || basket.isValidated()) continue;
for (order in basket.getOrders()){
if (!order.paid) {
Sys.println(order.distribution.toString() + "<br>");
Sys.println(order.toString() + "<br>");
}
}
var operation = basket.getOrderOperation(false);
if (operation != null){
if (operation.pending) {
Sys.println(distrib.toString() + "<br>");
Sys.println(operation.toString() + "<br>");
}
for ( payment in basket.getPayments()){
if (payment.pending){
Sys.println(distrib.toString() + "<br>");
Sys.println(payment.toString() + "<br>");
}
}
}
}
}
Sys.println("===== Fin de la liste ====");
}
}
+32
View File
@@ -0,0 +1,32 @@
package controller.admin;
/**
* ...
* @author fbarbut<francois.barbut@gmail.com>
*/
class Plugins extends controller.Controller
{
public function new()
{
super();
}
@tpl("admin/plugins/default.mtt")
public function doDefault() {
view.plugins = App.current.plugins;
}
public function doInstall(plugin:String) {
/*var p = App.current.getPlugin(plugin);
if (p == null) throw Error("/admin/plugins","Le plugin '"+plugin+"' introuvable");
if(p.isInstalled()) throw Ok("/admin/plugins","Le plugin '"+plugin+"' est déjà installé");
p.install();
throw Ok("/admin/plugins","Le plugin '"+plugin+"' est correctement installé");*/
}
}
+80
View File
@@ -0,0 +1,80 @@
package controller.api;
import haxe.Json;
import Common;
/**
* Groups API
* @author fbarbut
*/
class Group extends Controller
{
/**
* JSON map datas
*
* Request by zone : http://localhost/api/group/map?minLat=42.8115217450979&maxLat=51.04139389812637=&minLng=-18.369140624999996&maxLng=23.13720703125
* Request by location : http://localhost/api/group/map?lat=48.85&lng=2.32
* Request by address : http://localhost/api/group/map?address=105%20avenue%20d%27ivry%20Paris
*/
public function doMap(args:{?minLat:Float, ?maxLat:Float, ?minLng:Float, ?maxLng:Float, ?lat:Float, ?lng:Float, ?address:String}) {
var out = new Array<GroupOnMap>();
var places = new List<db.Place>();
if (args.minLat != null && args.maxLat != null && args.minLng != null && args.maxLng != null){
//Request by zone
#if plugins
var sql = "select p.* from Place p, Hosting h where h.id=p.amapId and h.visible=1 and ";
sql += 'p.lat > ${args.minLat} and p.lat < ${args.maxLat} and p.lng > ${args.minLng} and p.lng < ${args.maxLng}';
#else
var sql = "select p.* from Place p where ";
sql += 'p.lat > ${args.minLat} and p.lat < ${args.maxLat} and p.lng > ${args.minLng} and p.lng < ${args.maxLng}';
#end
places = db.Place.manager.unsafeObjects(sql, false);
}else if (args.lat!=null && args.lng!=null){
//Request by location
places = findGroupByDist(args.lat, args.lng);
}else{
//Request by address
if (args.address == null) throw "Please provide parameters";
var geocode = new sugoi.apis.google.GeoCode(App.config.get("google_geocoding_key"));
var loc = geocode.geocode(args.address)[0].geometry.location;
args.lat = loc.lat;
args.lng = loc.lng;
places = findGroupByDist(args.lat, args.lng);
}
for ( p in places){
out.push({
id : p.amap.id,
name : p.amap.name,
image : p.amap.image==null ? null : view.file(p.amap.image),
place : p.getInfos()
});
}
Sys.print(haxe.Json.stringify({success:true,groups:out}));
}
/**
* ~~ Pythagore rulez ~~
*/
function findGroupByDist(lat:Float, lng:Float,?limit=5){
#if plugins
var sql = 'select p.*,SQRT( POW(p.lat-$lat,2) + POW(p.lng-$lng,2) ) as dist from Place p, Hosting h ';
sql += "where h.id=p.amapId and h.visible=1 and p.lat is not null ";
sql += 'order by dist asc LIMIT $limit';
#else
var sql = 'select p.*,SQRT( POW(p.lat-$lat,2) + POW(p.lng-$lng,2) ) as dist from Place p ';
sql += "where p.lat is not null ";
sql += 'order by dist asc LIMIT $limit';
#end
return db.Place.manager.unsafeObjects(sql, false);
}
}
+142
View File
@@ -0,0 +1,142 @@
package controller.api;
import haxe.Json;
import tink.core.Error;
import service.OrderService;
/**
* Public order API
*/
class Order extends Controller
{
/**
* get orders of a user from a contractId (constant contract) or a distributionId (varying contract)
*/
public function doGet(userId:Int){
checkIsLogged();
//params
var p = app.params;
var distributionId = Std.parseInt(p.get("distributionId"));
var contractId = Std.parseInt(p.get("contractId"));
if (distributionId == null && contractId == null) throw "You should provide a contractId or a distributionId";
var user = db.User.manager.get(userId, false);
if (user == null) throw 'user #$userId doesn\'t exists';
var c : db.Contract = null;
var d : db.Distribution = null;
if (distributionId == null) {
c = db.Contract.manager.get(contractId, false);
}else{
d = db.Distribution.manager.get(distributionId, false);
c = d.contract;
}
//rights
if (!app.user.canManageContract(c)) throw new Error(t._("You do not have the authorization to manage this contract"));
if (d != null && d.validated) throw new Error(t._("This delivery has been already validated"));
if (c.type == db.Contract.TYPE_VARORDER && d == null ) throw "this contract is a 'varying order contract', please provide a distributionId";
//get datas
var pids = tools.ObjectListTool.getIds(c.getProducts(false));
var orders;
if (c.type == db.Contract.TYPE_VARORDER) {
orders = db.UserContract.manager.search($user == user && $distributionId==d.id && ($productId in pids), true);
}else {
orders = db.UserContract.manager.search($user == user && ($productId in pids), true);
}
var orders = OrderService.prepare(orders);
Sys.print(tink.Json.stringify({success:true,orders:orders}));
}
/**
* Update orders of a user ( from react OrderBox component )
* @param userId
*/
public function doUpdate(userId:Int){
checkIsLogged();
//GET params
var p = app.params;
var distributionId = Std.parseInt(p.get("distributionId"));
var contractId = Std.parseInt(p.get("contractId"));
//POST payload
var data = new Array<{id:Int,productId:Int,qt:Float,paid:Bool,invertSharedOrder:Bool,userId2:Int}>();
data = haxe.Json.parse( StringTools.urlDecode(sugoi.Web.getPostData()) ).orders;
if (distributionId == null && contractId == null) throw "You should provide a contractId or a distributionId";
var user = db.User.manager.get(userId, false);
if (user == null) throw 'user #$userId doesn\'t exists';
var c : db.Contract = null;
var d : db.Distribution = null;
if (distributionId == null) {
c = db.Contract.manager.get(contractId, false);
}else{
d = db.Distribution.manager.get(distributionId, false);
c = d.contract;
}
var pids = tools.ObjectListTool.getIds(c.getProducts(false));
//rights & checks
//fbarbut 2018-11-13 : too many problems when people try to edit the order of someone who left the group...
//if (!user.isMemberOf(c.amap)) throw new Error(t._("::user:: is not member of this group", {user:user.name}));
if (!app.user.canManageContract(c)) throw new Error(t._("You do not have the authorization to manage this contract"));
if (d != null && d.validated) throw new Error(t._("This delivery has been already validated"));
if (c.type == db.Contract.TYPE_VARORDER && d == null ) throw "this contract is a 'varying order contract', please provide a distributionId";
/*
* record orders
**/
//find existing orders
var exOrders = null;
if (c.type == db.Contract.TYPE_VARORDER) {
exOrders = db.UserContract.manager.search($user == user && $distributionId==d.id && ($productId in pids), true);
}else {
exOrders = db.UserContract.manager.search($user == user && ($productId in pids), true);
}
var orders = [];
for (o in data) {
//get product
var product = db.Product.manager.get(o.productId, false);
if (product.contract.id != c.id) throw "product " + o.productId + " is not in contract " + c.id;
//find existing order
var uo = Lambda.find(exOrders, function(uo) return uo.id == o.id);
//user2 + invert
var user2 : db.User = null;
var invert = false;
if ( o.userId2 != null ) {
user2 = db.User.manager.get(o.userId2,false);
if (user2 == null) throw t._("Unable to find user #::num::",{num:o.userId2});
if (!user2.isMemberOf(product.contract.amap)) throw t._("::user:: is not part of this group",{user:user2});
if (user.id == user2.id) throw t._("Both selected accounts must be different ones");
invert = o.invertSharedOrder;
}
//record order
if (uo != null) {
//existing record
var o = OrderService.edit(uo, o.qt, o.paid , user2, invert);
if (o != null) orders.push(o);
}else {
//new record
var o = OrderService.make(user, o.qt , product, d == null ? null : d.id, o.paid , user2, invert);
if (o != null) orders.push(o);
}
}
app.event(MakeOrder(orders));
db.Operation.onOrderConfirm(orders);
Sys.print(Json.stringify({success:true, orders:data}));
}
}
+20
View File
@@ -0,0 +1,20 @@
package controller.api;
import Common;
/**
* Product API
* @author fbarbut
*/
class Product extends Controller
{
public function doGet(args:{?contractId:db.Contract}) {
if(args==null || args.contractId==null) throw "invalid params";
var out = {products:new Array<ProductInfo>()};
for( p in args.contractId.getProducts(false)) out.products.push(p.infos(false,false));
Sys.print(tink.Json.stringify(out));
}
}
+209
View File
@@ -0,0 +1,209 @@
package controller.api;
import haxe.Json;
import tink.core.Error;
import Common;
import db.Amap;
import tools.ArrayTool;
using tools.ObjectListTool;
using Lambda;
class Shop extends Controller
{
/**
* @doc https://app.swaggerhub.com/apis/Cagette.net/Cagette.net/0.9.2#/shop/get_shop_categories
*/
public function doCategories(args:{date:String, place:db.Place}){
var out = new Array<CategoryInfo>();
var group = args.place.amap;
if (group.flags.has(ShopCategoriesFromTaxonomy)){
//TAXO CATEGORIES
var taxoCategs = db.TxpCategory.manager.all(false);
for (txp in taxoCategs){
var c : CategoryInfo = {id:txp.id, name:txp.name, subcategories:[]};
for (sc in txp.getSubCategories()){
c.subcategories.push({id:sc.id,name:sc.name});
}
out.push(c);
}
}else{
//CUSTOM CATEGORIES
var catGroups = db.CategoryGroup.get(group);
for (cat in catGroups){
var c : CategoryInfo = {id:cat.id, name:cat.name, subcategories:[]};
for ( sc in cat.getCategories() ){
c.subcategories.push({id:sc.id,name:sc.name});
}
out.push(c);
}
}
Sys.print(Json.stringify({success:true,categories:out}));
}
/**
* @doc https://app.swaggerhub.com/apis/Cagette.net/Cagette.net/0.9.2#/shop/get_shop_products
*/
public function doProducts(args:{date:String, place:db.Place, ?category:Int, ?subcategory:Int}){
if ( args == null || (args.category == null && args.subcategory == null)) throw "You should provide a category Id or a subcategory Id";
//need some optimization : populating all thses objects eats memory, and we need only the ids !
var products = getProducts(args.place, Date.fromString(args.date), args.place.amap.flags.has(ShopCategoriesFromTaxonomy));
var pids = products.getIds();
var categsFromTaxo = args.place.amap.flags.has(ShopCategoriesFromTaxonomy);
var catName = "undefined category";
if( categsFromTaxo ){
/**
* Use Taxonomy :
* - Category is TxpCatgory
* - Subcategory us TxpSubCategory
* - Products are linked to TxpProduct which belongs to a TxpCatgory and a TxpSubCategory
*/
var sql = "";
if (args.subcategory != null){
sql = 'SELECT p.* FROM Product p, TxpProduct tp, TxpSubCategory sc
WHERE p.txpProductId = tp.id
AND tp.subCategoryId = sc.id
AND sc.id = ${args.subcategory}
AND p.id IN ( ${pids.join(",")} )';
var cat = db.TxpSubCategory.manager.get(args.subcategory, false);
if (cat == null) throw 'unknown subcategory #' + args.subcategory;
catName = cat.name;
}else if (args.category != null){
sql = 'SELECT p.* FROM Product p, TxpProduct tp, TxpCategory c
WHERE p.txpProductId = tp.id
AND tp.categoryId = c.id
AND c.id = ${args.category}
AND p.id IN ( ${pids.join(",")} )';
var cat = db.TxpCategory.manager.get(args.subcategory, false);
if (cat == null) throw 'unknown category #' + args.category;
catName = cat.name;
}
products = db.Product.manager.unsafeObjects(sql,false).array();
}else{
/**
* Use custom categories :
* - Category is CategoryGroup
* - Subcategory is Category
* - Products are tagged with ProductCategory
*/
var sql = "";
if (args.subcategory != null){
sql = 'SELECT p.* FROM Product p, ProductCategory pc, Category c
WHERE pc.productId = p.id
AND pc.categoryId = c.id
AND c.id = ${args.subcategory}
AND p.id IN ( ${pids.join(",")} )';
var cat = db.Category.manager.get(args.subcategory, false);
if (cat == null) throw 'unknown subcategory #' + args.subcategory;
catName = cat.name;
}else if (args.category != null){
sql = 'SELECT p.* FROM Product p, ProductCategory pc, Category c, CategoryGroup cg
WHERE pc.productId = p.id
AND pc.categoryId = c.id
AND c.categoryGroupId = cg.id
AND cg.id = ${args.category}
AND p.id IN ( ${pids.join(",")} )';
var cat = db.CategoryGroup.manager.get(args.category, false);
if (cat == null) throw 'unknown category #' + args.category;
catName = cat.name;
}
products = db.Product.manager.unsafeObjects(sql,false).array();
}
//to productInfos
var products : Array<ProductInfo> = products.map( function(p) return p.infos(categsFromTaxo,true) ).array();
if (args.category != null){
Sys.print(Json.stringify( {success:true, products:products, category:catName} ));
}else{
Sys.print(Json.stringify( {success:true, products:products, subcategory:catName} ));
}
}
public function doInit(args:{place:db.Place, date:String}){
var out = {place:args.place.getInfos(), orderEndDates: new Array<{date:String,contracts:Array<String>}>() };
//order end dates
var contracts = db.Contract.getActiveContracts(args.place.amap);
for (c in Lambda.array(contracts)) {
if (c.type != db.Contract.TYPE_VARORDER) contracts.remove(c);//only varying orders
if (!c.isVisibleInShop()) contracts.remove(c);
}
var date = Date.fromString(args.date);
var now = Date.now();
var cids = Lambda.map(contracts, function(c) return c.id);
var d1 = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
var d2 = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59);
var distribs = db.Distribution.manager.search(($contractId in cids) && $orderStartDate <= now && $orderEndDate >= now && $date > d1 && $end < d2 && $place == args.place, false);
var distribByDate = ArrayTool.groupByDate(Lambda.array(distribs), "orderEndDate");
out.orderEndDates = [];
for ( k in distribByDate.keys() ) {
out.orderEndDates.push( {date:k , contracts: distribByDate.get(k).map( function(x) return x.contract.name)} );
}
Sys.print(Json.stringify( out ));
}
private function getProductInfos(place:db.Place, date, ?categsFromTaxo = false):Array<ProductInfo>{
var products = getProducts(place, date, categsFromTaxo);
return Lambda.array(Lambda.map(products, function(p) return p.infos(categsFromTaxo)));
}
/**
* Get the available products list
*/
private function getProducts(place:db.Place,date,?categsFromTaxo=false):Array<db.Product> {
var contracts = db.Contract.getActiveContracts(place.amap);
for (c in Lambda.array(contracts)) {
if (c.type != db.Contract.TYPE_VARORDER) contracts.remove(c);//only varying orders
if (!c.isVisibleInShop()) contracts.remove(c);
}
var now = Date.now();
var cids = Lambda.map(contracts, function(c) return c.id);
var d1 = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
var d2 = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59);
var distribs = db.Distribution.manager.search(($contractId in cids) && $orderStartDate <= now && $orderEndDate >= now && $date > d1 && $end < d2 && $place == place, false);
var cids = Lambda.map(distribs, function(d) return d.contract.id);
return Lambda.array(db.Product.manager.search(($contractId in cids) && $active==true, { orderBy:name }, false));
}
}
+57
View File
@@ -0,0 +1,57 @@
package controller.api;
import haxe.Json;
import tink.core.Error;
import Common;
/**
* Public user API
*/
class User extends Controller
{
/**
* Login
*/
public function doLogin(){
//cleaning
var email = StringTools.trim(App.current.params.get("email")).toLowerCase();
var pass = StringTools.trim(App.current.params.get("password"));
service.UserService.login(email, pass);
Sys.print(Json.stringify({success:true}));
}
/**
* Register
*/
public function doRegister(){
//cleaning
var p = app.params;
var email = StringTools.trim(p.get("email")).toLowerCase();
var pass = StringTools.trim(p.get("password"));
var firstName = StringTools.trim(p.get("firstName"));
var lastName = StringTools.trim(p.get("lastName")).toUpperCase();
var phone = p.exists("phone") ? StringTools.trim(p.get("phone")) : null;
service.UserService.register(firstName, lastName, email, phone, pass);
Sys.print(Json.stringify({success:true}));
}
/**
* get users of current group
*/
@logged
function doGetFromGroup(){
if(!app.user.canAccessMembership()) throw new tink.core.Error(403,"Access forbidden");
var members:Array<UserInfo> = service.UserService.getFromGroup(app.user.amap).map(function(m) return m.infos() );
Sys.print(tink.Json.stringify({users:members}));
}
}
Executable
+373
View File
@@ -0,0 +1,373 @@
package db;
import sugoi.form.ListData.FormData;
import sys.db.Object;
import sys.db.Types;
import Common;
enum AmapFlags {
HasMembership; //membership management
ShopMode; //shop mode / standard mode
HasPayments; //manage payments and user balance
ComputeMargin; //compute margin instead of percentage
CagetteNetwork; //register in cagette.net groups directory
ShopCategoriesFromTaxonomy; //the custom categories are not used anymore, use product taxonomy instead
HidePhone; //Hide manager phone on group public page
PhoneRequired; //phone number of members is required for this group
}
//user registration options
enum RegOption{
Closed;
WaitingList;
Open;
Full;
}
enum GroupType{
Amap; //CSA / GASAP / AMAP
GroupedOrders; //groupements d'achat
ProducerDrive; //drive de producteurs
FarmShop; //vente à la ferme
}
/**
* AMAP
*/
class Amap extends Object
{
public var id : SId;
public var name : SString<64>;
@formPopulate("getMembersFormElementData")
@:relation(userId)
public var contact : SNull<User>;
public var txtIntro:SNull<SText>; //introduction de l'amap
public var txtHome:SNull<SText>; //texte accueil adhérents
public var txtDistrib:SNull<SText>; //sur liste d'emargement
public var extUrl : SNull<SString<64>>; //lien sur logo du groupe
public var membershipRenewalDate : SNull<SDate>;
@hideInForms public var membershipPrice : SNull<STinyInt>;
@hideInForms
public var vatRates : SData<Map<String,Float>>;
public var flags:SFlags<AmapFlags>;
public var groupType:SNull<SEnum<GroupType>>;
@hideInForms @:relation(imageId)
public var image : SNull<sugoi.db.File>;
@hideInForms public var cdate : SDateTime;
@hideInForms @:relation(placeId) public var mainPlace : SNull<db.Place>;
public var regOption : SEnum<RegOption>;
@hideInForms public var currency:SString<12>; //name or symbol.
@hideInForms public var currencyCode:SString<3>; //https://fr.wikipedia.org/wiki/ISO_4217
//payments
@hideInForms public var allowedPaymentsType:SNull<SData<Array<String>>>;
@hideInForms public var checkOrder:SNull<SString<64>>;
@hideInForms public var IBAN:SNull<SString<40>>;
@hideInForms public var allowMoneyPotWithNegativeBalance:SNull<SBool>;
public function new()
{
super();
flags = cast 0;
flags.set(CagetteNetwork);
flags.set(ShopMode);
vatRates = ["5,5%" => 5.5, "20%" => 20];
cdate = Date.now();
regOption = Open;
currency = "";
currencyCode = "EUR";
checkOrder = "";
}
/**
* find the most common delivery place
*/
public function getMainPlace() {
if (mainPlace != null && Std.random(100) != 0) {
return mainPlace;
}else {
this.lock();
var places = getPlaces();
//just 1 place
if (places.length == 1) {
this.mainPlace = places.first();
this.update();
return this.mainPlace;
}
//no places !
if (places.length == 0) return null;
var pids = Lambda.map(places, function(x) return x.id);
var res = sys.db.Manager.cnx.request("select placeId,count(placeId) as top from Distribution where placeId IN ("+pids.join(",")+") group by placeId order by top desc").results();
var res = res.first();
var pid :Int = null;
if (res == null){
pid = this.getPlaces().first().id;
}else{
pid = Std.parseInt(res.placeId);
}
if (pid != 0 && pid != null) {
var p = db.Place.manager.get(pid, false);
this.mainPlace = p;
this.update();
return p;
}else {
return null;
}
}
}
/**
* Methods to get flags in templates
*/
public function hasMembership():Bool {
return flags != null && flags.has(HasMembership);
}
public function hasShopMode() {
return flags.has(ShopMode);
}
public function canExposePhone() {
return !flags.has(HidePhone);
}
public function hasPayments(){
return flags != null && flags.has(HasPayments);
}
public function hasTaxonomy(){
return flags != null && flags.has(ShopCategoriesFromTaxonomy);
}
public function hasPhoneRequired(){
return flags != null && flags.has(PhoneRequired);
}
public function getCategoryGroups() {
//if (flags.has(ShopCategoriesFromTaxonomy)){
//return Lambda.array( cast db.TxpCategory.manager.all(false) );
//}else{
//return Lambda.array( db.CategoryGroup.get(this) );
//}
var t = sugoi.i18n.Locale.texts;
var categs = new Array<{id:Int,name:String,color:String,pinned:Bool,categs:Array<CategoryInfo>}>();
if (this.flags.has(db.Amap.AmapFlags.ShopCategoriesFromTaxonomy)){
//TAXO CATEGORIES
var taxoCategs = db.TxpCategory.manager.all(false);
var c : Array<CategoryInfo> = Lambda.array(Lambda.map( taxoCategs, function(c){return {id:c.id, name:c.name, subcategories:null}; }));
categs.push({
id:0,
name: t._("Product type"),
pinned:false,
color:"#583816",
categs: c
});
}else{
//CUSTOM CATEGORIES
var catGroups = db.CategoryGroup.get(this);
for ( cg in catGroups){
var color = App.current.view.intToHex(db.CategoryGroup.COLORS[cg.color]);
categs.push({
id:cg.id,
name:cg.name,
pinned:cg.pinned,
color:color,
categs: Lambda.array(Lambda.map( cg.getCategories(), function(c) return c.infos()))
});
}
}
return categs;
}
//public function canAddMember():Bool {
// return isAboOk(true);
//}
/**
* Renvoie la liste des contrats actifs
* @param large=false
*/
public function getActiveContracts(?large=false) {
return Contract.getActiveContracts(this, large, false);
}
public function getContracts() {
return Contract.manager.search($amap == this, false);
}
/**
* récupere les produits des contracts actifs
*/
public function getProducts() {
var contracts = db.Contract.getActiveContracts(App.current.user.amap,false,false);
return Product.manager.search( $contractId in Lambda.map(contracts, function(c) return c.id),{orderBy:name}, false);
}
/**
* get next multi-deliveries
*/
public function getDeliveries(?limit=3){
var out = new Map<String,db.Distribution>();
for ( c in getActiveContracts()){
for ( d in c.getDistribs(true,3)){
out.set(d.getKey(), d);
}
}
var out = Lambda.array(out);
out.sort(function(a, b){
return Math.round(a.date.getTime() / 1000) - Math.round(b.date.getTime() / 1000);
});
return out.slice(0,limit);
}
public function getPlaces() {
return Place.manager.search($amap == this, false);
}
public function getVendors() {
return Vendor.manager.search($amap == this, false);
}
public function getMembers() {
return User.manager.unsafeObjects("Select u.* from User u,UserAmap ua where u.id=ua.userId and ua.amapId="+this.id+" order by u.lastName", false);
}
public function getMembersNum():Int{
return UserAmap.manager.count($amapId == this.id);
}
public function getMembersFormElementData():FormData<Int> {
var m = getMembers();
var out = [];
for (mm in m) {
out.push({label:mm.getCoupleName() , value:mm.id});
}
return out;
}
override public function toString() {
if (name != '' && name != null) {
return name;
}else {
return 'group#' + id;
}
}
/**
* pour avoir le nom de la periode de cotisation pour une date donnée
*/
public function getPeriodName(?d:Date):String {
if (d == null) d = Date.now();
var year = getMembershipYear(d);
return getPeriodNameFromYear(year);
}
/**
* Si la date de renouvellement est en janvier ou février, on note la cotisation avec l'année en cours,
* sinon c'est "à cheval" donc on note la cotis avec l'année la plus ancienne (ex:2014 pour une cotis 2014-2015)
*/
public function getMembershipYear(?d:Date):Int {
if (d == null) d = Date.now();
var year = d.getFullYear();
var n = membershipRenewalDate;
if (n == null) n = Date.now();
var renewalDate = new Date(year, n.getMonth(), n.getDate(), 0, 0, 0);
//if (membershipRenewalDate.getMonth() <= 1) {
if (d.getTime() < renewalDate.getTime()) {
return year-1;
}else {
return year;
}
//}else {
//return year - 1;
//}
}
/**
* à partir d'une année de cotis enregistrée, afficher le nom de la periode
* @param y
*/
public function getPeriodNameFromYear(y:Int):String {
if (membershipRenewalDate!=null && membershipRenewalDate.getMonth() <= 1) {
return Std.string(y);
}else {
return Std.string(y) + "-" + Std.string(y+1);
}
}
override public function insert(){
if (txtHome == null){
var t = sugoi.i18n.Locale.texts;
txtHome = t._("Welcome in the group of ::name::!\n You can look at the delivery planning or make a new order.",{name:this.name});
}
App.current.event(NewGroup(this,App.current.user));
super.insert();
}
public function getCurrency():String{
if (currency == ""){
lock();
currency = "";
currencyCode = "EUR";
update();
}
return currency;
}
public static function getLabels(){
var t = sugoi.i18n.Locale.texts;
return [
"name" => t._("Group name"),
"txtIntro" => t._("Short description"),
"txtHome" => t._("Homepage text"),
"txtDistrib" => t._("Text for distribution lists"),
"extUrl" => t._("Group website URL"),
"membershipRenewalDate" => t._("Membership renewal date"),
"flags" => t._("Options"),
"groupType" => t._("Group type"),
"regOption" => t._("Registration setting"),
"contact" => t._("Main contact"),
];
}
}
+130
View File
@@ -0,0 +1,130 @@
package db;
import sys.db.Object;
import sys.db.Types;
/**
* Basket : represents the orders of a user for specific date + place
*/
//@:index(userId,placeId,ddate,unique)
class Basket extends Object
{
public var id : SId;
public var cdate : SDateTime; //date when the order has been placed
public var num : SInt; //order number
//TODO : link baskets to a multidistrib ID.
//2018-07-21 fbarbut : we cannot use keys like this, because some distribution's place or date may change after orders are made.
//@:relation(userId) public var user : db.User;
//@:relation(placeId) public var place : db.Place;
//public var ddate : SDate; //date of the delivery
public static var CACHE = new Map<String,db.Basket>();
public function new(){
super();
cdate = Date.now();
}
public static function emptyCache(){
CACHE = new Map<String,db.Basket>();
}
public static function get(user:db.User, place:db.Place, date:Date, ?lock = false):db.Basket{
date = tools.DateTool.setHourMinute(date, 0, 0);
//caching
// var k = user.id + "-" + place.id + "-" + date.toString().substr(0, 10);
// var b = CACHE.get(k);
var b = null;
// if (b == null){
var md = MultiDistrib.get(date, place,db.Contract.TYPE_VARORDER);
var orders = md.getUserOrders(user);
for( o in orders){
if(o.basket!=null) {
b = o.basket;
break;
}
}
// CACHE.set(k, b);
// }
return b;
}
/**
* Get a Basket or create it if it doesn't exists.
* Also link existing orders to this basket
* @param user
* @param place
* @param date
*/
public static function getOrCreate(user, place, date){
var b = get(user, place, date, true);
date = tools.DateTool.setHourMinute(date, 0, 0);
if (b == null){
//compute basket number
var md = MultiDistrib.get(date, place,db.Contract.TYPE_VARORDER);
b = new Basket();
b.num = md.getUsers().length + 1;
//TODO : should be more safe to do something like "b.num = MAX(num)+1 FROM Basket"
b.insert();
//try to find orders and link them to the basket
var dids = tools.ObjectListTool.getIds(md.distributions);
for ( o in db.UserContract.manager.search( ($distributionId in dids) && ($user == user), true)){
o.basket = b;
o.update();
}
}
return b;
}
/**
* Get basket's orders
*/
public function getOrders(){
return db.UserContract.manager.search($basket == this, false);
}
/**
* Returns the list of operations which paid this basket
* @return
*/
public function getPayments():Iterable<db.Operation>{
var op = getOrderOperation(false);
if (op == null){
return [];
}else{
return op.getRelatedPayments();
}
}
public function getOrderOperation(?onlyPending=true):db.Operation{
var order = getOrders().first();
if(order==null) return null;
var key = db.Distribution.makeKey(order.distribution.date, order.distribution.place);
return db.Operation.findVOrderTransactionFor(key, order.user, order.distribution.place.amap, onlyPending);
}
public function isValidated(){
var ordersPaid = Lambda.count(getOrders(), function(o) return !o.paid) == 0;
var op = getOrderOperation(false);
var orderOperationNotPending = op!=null ? op.pending == false : true;
var paymentOperationsNotPending = Lambda.count(getPayments(), function(p) return p.pending) == 0;
return ordersPaid && orderOperationNotPending && paymentOperationsNotPending;
}
}
+22
View File
@@ -0,0 +1,22 @@
package db;
import sys.db.Types;
import Common;
class Category extends sys.db.Object
{
public var id : SId;
public var name :SString<128>;
@:relation(categoryGroupId) public var categoryGroup:db.CategoryGroup;
/**
* get category color in hexa
*/
public function getColor():String {
return App.current.view.intToHex(db.CategoryGroup.COLORS[categoryGroup.color]);
}
public function infos():CategoryInfo{
return {id:id, name:name, /*parent:categoryGroup.id, /*pinned:categoryGroup.pinned*/};
}
}
+47
View File
@@ -0,0 +1,47 @@
package db;
import sys.db.Types;
class CategoryGroup extends sys.db.Object
{
public var id : SId;
public var name : SString<128>;
public var color : STinyInt; //color id
public var pinned : SBool; //if true, the products tagged with these categories will be pinned on top of the shop.
@:relation(amapId) public var amap:db.Amap;
@:skip public static var COLORS = [
0x7BAD1C, //vert clair
0x007700, //vert foncé
0x583816, //marron
0xD97801, //orange carotte
0xB1933D, //sable
0xC91F25, //rouge
0x6F0D2E, //vin
0x616161, //gris
];
public function new(){
super();
pinned = false;
color = 7;
}
public function getCategories() {
return db.Category.manager.search($categoryGroup == this, false);
}
public static function get(amap:db.Amap):List<CategoryGroup> {
return manager.search($amap == amap, false);
}
public static function getLabels(){
var t = sugoi.i18n.Locale.texts;
return [
"name" => t._("Category group name"),
"pinned" => t._("Pinned on top"),
"color" => t._("Color"),
];
}
}
+285
View File
@@ -0,0 +1,285 @@
package db;
import sugoi.form.ListData.FormData;
import sys.db.Object;
import sys.db.Types;
enum ContractFlags {
UsersCanOrder; //adhérents peuvent saisir eux meme la commande en ligne
StockManagement; //gestion des commandes
PercentageOnOrders; //calcul d'une commission supplémentaire
//LogisticMgmt; //gestion logistique
//SubGroups; //sous groupes pour commandes groupées
//InviteFriends; //peut inviter des amis à participer à la commande
}
/**
* Contract
*
* un contrat réunissant pluseiurs produits d'un meme fournisseur
* qui sont livrés au meme endroit et meme moment;
*
*/
class Contract extends Object
{
public var id : SId;
public var name : SString<64>;
//responsable
@formPopulate("populate") @:relation(userId) public var contact : SNull<User>;
@formPopulate("populateVendor") @:relation(vendorId) public var vendor : Vendor;
public var startDate:SDate;
public var endDate :SDate;
public var description:SNull<SText>;
@:relation(amapId) public var amap:Amap;
public var distributorNum:STinyInt;
public var flags : SFlags<ContractFlags>;
public var percentageValue : SNull<SInt>; //fees percentage
public var percentageName : SNull<SString<64>>; //fee name
public var type : SInt;
@:skip public static var TYPE_CONSTORDERS = 0; //CSA contract
@:skip public static var TYPE_VARORDER = 1; //varying orders contract
@:skip var cache_hasActiveDistribs : Bool;
public function new()
{
super();
flags = cast 0;
distributorNum = 0;
}
/**
* The products can be ordered currently ?
*
* @deprecated it depends on distributions
*/
public function isUserOrderAvailable():Bool {
if (type == TYPE_CONSTORDERS ) {
return isVisibleInShop();
}else {
//if ( cache_hasActiveDistribs != null ) return cache_hasActiveDistribs;
//for varying orders, we need to know if there are some available deliveries
var n = Date.now();
var d = db.Distribution.manager.count( $orderStartDate <= n && $orderEndDate >= n && $contractId==this.id);
//tmp : add the "old" deliveries which have a null orderStartDate
//d += db.Distribution.manager.count( $orderStartDate == null && $date > n && $contractId == this.id );
//cache_hasActiveDistribs = d > 0;
//return cache_hasActiveDistribs && isVisibleInShop();
return d>0 && isVisibleInShop();
}
}
/**
* The products can be displayed in a shop ?
*/
public function isVisibleInShop():Bool {
//yes if the contract is active and the 'UsersCanOrder' flag is checked
var n = Date.now().getTime();
return flags.has(UsersCanOrder) && n < this.endDate.getTime() && n > this.startDate.getTime();
}
/**
* is currently open to orders
*/
public function hasRunningOrders(){
var now = Date.now();
var n = now.getTime();
var contractOpen = flags.has(UsersCanOrder) && n < this.endDate.getTime() && n > this.startDate.getTime();
var d = db.Distribution.manager.count( $orderStartDate <= now && $orderEndDate > now && $contractId==this.id);
return contractOpen && d > 0;
}
public function hasPercentageOnOrders():Bool {
return flags.has(PercentageOnOrders) && percentageValue!=null && percentageValue!=0;
}
public function hasStockManagement():Bool {
return flags.has(StockManagement);
}
/**
* computes a 'percentage' fee or a 'margin' fee
* depending on the group settings
*
* @param basePrice
*/
public function computeFees(basePrice:Float) {
if (!hasPercentageOnOrders()) return 0.0;
if (amap.flags.has(ComputeMargin)) {
//commercial margin
return (basePrice / ((100 - percentageValue) / 100)) - basePrice;
}else {
//add a percentage
return percentageValue / 100 * basePrice;
}
}
/**
*
* @param amap
* @param large = false Si true, montre les contrats terminés depuis moins d'un mois
* @param lock = false
*/
public static function getActiveContracts(amap:Amap,?large = false, ?lock = false) {
var now = Date.now();
var end = Date.now();
if (large) {
end = DateTools.delta(end , -1000.0 * 60 * 60 * 24 * 30);
return db.Contract.manager.search($amap == amap && $endDate > end,{orderBy:-vendorId}, lock);
}else {
return db.Contract.manager.search($amap == amap && $endDate > now && $startDate < now,{orderBy:-vendorId}, lock);
}
}
/**
* get products in this contract
* @param onlyActive = true
* @return
*/
public function getProducts(?onlyActive = true):List<Product> {
if (onlyActive) {
return Product.manager.search($contract==this && $active==true,{orderBy:name},false);
}else {
return Product.manager.search($contract==this,{orderBy:name},false);
}
}
/**
* get a few products to display
* @param limit = 6
*/
public function getProductsPreview(?limit = 6){
return Product.manager.search($contract==this && $active==true,{limit:limit,orderBy:-id},false);
}
/**
* get users who have orders in this contract ( including user2 )
* @return Array<db.User>
*/
public function getUsers():Array<db.User> {
var pids = getProducts().map(function(x) return x.id);
var ucs = UserContract.manager.search($productId in pids, false);
var ucs2 = [];
for( uc in ucs) {
ucs2.push(uc.user);
if(uc.user2!=null) ucs2.push(uc.user2);
}
//comme un user peut avoir plusieurs produits au sein d'un contrat, il faut dédupliquer cette liste
var out = new Map<Int,db.User>();
for (u in ucs2) {
out.set(u.id, u);
}
return Lambda.array(out);
}
/**
* Get all orders of this contract
* @param d A delivery is needed for varying orders contract
* @return
*/
public function getOrders(?d:db.Distribution):Array<db.UserContract> {
if (type == TYPE_VARORDER && d == null) throw "This type of contract must have a delivery";
//get product ids, some of the products may have been disabled but we keep the order
var pids = getProducts(false).map(function(x) return x.id);
var ucs = new List<db.UserContract>();
if (type == TYPE_VARORDER) {
ucs = UserContract.manager.search( ($productId in pids) && $distribution==d,{orderBy:userId}, false);
}else {
ucs = UserContract.manager.search( ($productId in pids) ,{orderBy:userId}, false);
}
return Lambda.array(ucs);
}
/**
* Get orders for a user.
*
* @param d
* @return
*/
public function getUserOrders(u:db.User,?d:db.Distribution):Array<db.UserContract> {
if (type == TYPE_VARORDER && d == null) throw "This type of contract must have a delivery";
var pids = getProducts(false).map(function(x) return x.id);
var ucs = new List<db.UserContract>();
if (d != null && d.contract.type==db.Contract.TYPE_VARORDER) {
ucs = UserContract.manager.search( ($productId in pids) && $distribution==d && ($user==u || $user2==u ), false);
}else {
ucs = UserContract.manager.search( ($productId in pids) && ($user==u || $user2==u ),false);
}
return Lambda.array(ucs);
}
public function getDistribs(excludeOld = true,?limit=999):List<Distribution> {
if (excludeOld) {
//still include deliveries which just expired in last 24h
return Distribution.manager.search($end > DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24) && $contract == this, { orderBy:date,limit:limit } );
}else{
return Distribution.manager.search( $contract == this, { orderBy:date,limit:limit } );
}
}
override function toString() {
return name+" du "+this.startDate.toString().substr(0,10)+" au "+this.endDate.toString().substr(0,10);
}
public function populate() {
return App.current.user.amap.getMembersFormElementData();
}
/**
* get a vendor list as form data
* @return
*/
public function populateVendor():FormData<Int>{
var vendors = Vendor.manager.search($amap == App.current.user.amap, false);
var out = [];
for (v in vendors) {
out.push({label:v.name, value:v.id });
}
return out;
}
public static function getLabels(){
var t = sugoi.i18n.Locale.texts;
return [
"name" => t._("Contract name"),
"startDate" => t._("Start date"),
"endDate" => t._("End date"),
"description" => t._("Description"),
"distributorNum" => t._("Number of required distributors (0 to 4)"),
"flags" => t._("Options"),
"percentageValue" => t._("Fees percentage"),
"percentageName" => t._("Fees label"),
"contact" => t._("Contact"),
"vendor" => t._("Farmer"),
];
}
}
+271
View File
@@ -0,0 +1,271 @@
package db;
import sugoi.form.ListData;
import sys.db.Object;
import sys.db.Types;
/**
* Distrib
*/
class Distribution extends Object
{
public var id : SId;
@:relation(contractId)
public var contract : Contract;
@formPopulate("placePopulate")
@:relation(placeId)
public var place : Place;
//when orders are open
@hideInForms public var orderStartDate : SNull<SDateTime>;
@hideInForms public var orderEndDate : SNull<SDateTime>;
//start and end date for delivery
public var date : SDateTime;
public var end : SDateTime;
@:relation(distributionCycleId) public var distributionCycle : SNull<DistributionCycle>;
@formPopulate("populate") @:relation(distributor1Id) public var distributor1 : SNull<db.User>;
@formPopulate("populate") @:relation(distributor2Id) public var distributor2 : SNull<db.User>;
@formPopulate("populate") @:relation(distributor3Id) public var distributor3 : SNull<db.User>;
@formPopulate("populate") @:relation(distributor4Id) public var distributor4 : SNull<db.User>;
@hideInForms public var validated :SBool;
public static var DISTRIBUTION_VALIDATION_LIMIT = 10;
public function new()
{
super();
date = Date.now();
end = DateTools.delta(date, 1000 * 60 * 90);
validated = false;
}
/**
* get group members list as form data
*/
public function populate():FormData<Int> {
if(App.current.user!=null && App.current.user.getAmap()!=null){
return App.current.user.getAmap().getMembersFormElementData();
}else{
return [];
}
}
/**
* get groups places as form data
* @return
*/
public function placePopulate():FormData<Int> {
var out = [];
var places = new List();
if(this.contract!=null){
//edit form
places = db.Place.manager.search($amapId == this.contract.amap.id, false);
}else{
//insert form
places = db.Place.manager.search($amapId == App.current.user.amap.id, false);
}
for (p in places) out.push( { label:p.name,value:p.id} );
return out;
}
public function hasEnoughDistributors() {
var n = contract.distributorNum;
var d = 0;
if (distributor1 != null) d++;
if (distributor2 != null) d++;
if (distributor3 != null) d++;
if (distributor4 != null) d++;
return (d >= n) ;
}
public function isDistributor(u:User) {
if (u == null) return false;
return (distributor1!=null && u.id == distributor1.id) ||
(distributor2!=null && u.id == distributor2.id) ||
(distributor3!=null && u.id == distributor3.id) ||
(distributor4!=null && u.id == distributor4.id);
}
/**
* String to identify this distribution (debug use only)
*/
override public function toString() {
return "#" + id + " Delivery " + date.toString() + " of " + contract.name;
}
public function getOrders() {
if ( this.contract.type == Contract.TYPE_CONSTORDERS){
var pids = db.Product.manager.search($contract == this.contract, false);
var pids = Lambda.map(pids, function(x) return x.id);
return UserContract.manager.search( ($productId in pids), false);
}else{
return UserContract.manager.search($distribution == this, false);
}
}
public function getUserOrders(user:db.User){
if ( this.contract.type == Contract.TYPE_CONSTORDERS){
var pids = db.Product.manager.search($contract == this.contract, false);
var pids = Lambda.map(pids, function(x) return x.id);
return UserContract.manager.search( (($productId in pids) && ($user==user || $user2==user) ), false);
}else{
return UserContract.manager.search($distribution == this && $user==user, false);
}
}
public function getUsers():Iterable<db.User>{
return tools.ObjectListTool.deduplicate( Lambda.map(getOrders(), function(x) return x.user ) );
}
/**
* Get TTC turnover for this distribution
*/
public function getTurnOver(){
var sql = "select SUM(quantity * productPrice) from UserContract where productId IN (" + tools.ObjectListTool.getIds(contract.getProducts()).join(",") +") ";
if (contract.type == db.Contract.TYPE_VARORDER) {
sql += " and distributionId=" + this.id;
}
return sys.db.Manager.cnx.request(sql).getFloatResult(0);
}
/**
* Get HT turnover for this distribution
*/
public function getHTTurnOver(){
var pids = tools.ObjectListTool.getIds(contract.getProducts(false));
var sql = "select SUM(uc.quantity * (p.price/(1+p.vat/100)) ) from UserContract uc, Product p ";
sql += "where uc.productId IN (" + pids.join(",") +") ";
sql += "and p.id=uc.productId ";
if (contract.type == db.Contract.TYPE_VARORDER) {
sql += " and uc.distributionId=" + this.id;
}
return sys.db.Manager.cnx.request(sql).getFloatResult(0);
}
/**
*
*/
public function canOrderNow() {
if (orderEndDate == null) {
return this.contract.isUserOrderAvailable();
}else {
var n = Date.now().getTime();
var f = this.contract.flags.has(UsersCanOrder);
return f && n < orderEndDate.getTime() && n > orderStartDate.getTime();
}
}
/**
* Get next multi-devliveries
* ( deliveries including more than one vendors )
*/
/*public static function getNextMultiDeliveries(){
var out = new Map<String,{place:Place,startDate:Date,endDate:Date,active:Bool,products:Array<ProductInfo>}>();
return Lambda.array(manager.search($orderStartDate <= Date.now() && $orderEndDate >= Date.now() && $contract==contract,false));
var now = Date.now();
var contracts = Contract.getActiveContracts(App.current.user.amap);
var cids = Lambda.map(contracts, function(p) return p.id);
//available deliveries + some of the next deliveries
var distribs = db.Distribution.manager.search(($contractId in cids) && $orderEndDate >= now, { orderBy:date }, false);
var inOneMonth = DateTools.delta(now, 1000.0 * 60 * 60 * 24 * 30);
for (d in distribs) {
var o = out.get(d.getKey());
if (o == null) o = {place:d.place, startDate:d.date,active:null, endDate:d.end, products:[]};
for ( p in d.contract.getProductsPreview(8)){
if (o.products.length >= 8) break;
o.products.push( p.infos() );
}
if (d.orderStartDate.getTime() <= now.getTime() ){
//order currently open
o.active = true;
}else if (d.orderStartDate.getTime() <= inOneMonth.getTime() ){
//open soon
o.active = false;
}else{
continue;
}
out.set(d.getKey(), o);
}
return Lambda.array(out);
}*/
override public function update(){
this.end = new Date(this.date.getFullYear(), this.date.getMonth(), this.date.getDate(), this.end.getHours(), this.end.getMinutes(), 0);
super.update();
}
/**
* Get open to orders deliveries
* @param contract
*/
public static function getOpenToOrdersDeliveries(contract:db.Contract){
return Lambda.array(manager.search($orderStartDate <= Date.now() && $orderEndDate >= Date.now() && $contract==contract,{orderBy:date},false));
}
/**
* Return a string like $placeId-$date.
*
* It's an ID representing all the distributions happening on that day at that place.
*/
public function getKey():String{
return db.Distribution.makeKey(this.date, this.place);
}
public static function makeKey(date, place){
return date.toString().substr(0, 10) +"|"+Std.string(place.id);
}
public static function getLabels(){
var t = sugoi.i18n.Locale.texts;
return [
"date" => t._("Date"),
"endDate" => t._("End hour"),
"place" => t._("Place"),
"distributor1" => t._("Distributor #1"),
"distributor2" => t._("Distributor #2"),
"distributor3" => t._("Distributor #3"),
"distributor4" => t._("Distributor #4"),
];
}
}
+54
View File
@@ -0,0 +1,54 @@
package db;
import sys.db.Object;
import sys.db.Types;
using tools.DateTool;
enum CycleType {
Weekly;
Monthly;
BiWeekly;
TriWeekly;
}
/**
* Distribution cycle
*/
class DistributionCycle extends Object
{
public var id : SId;
@:relation(contractId) public var contract : Contract;
public var cycleType:SEnum<CycleType>;
public var startDate : SDate; //debut
public var endDate : SDate; //fin de la recurrence
public var startHour : SDateTime;
public var endHour : SDateTime;
public var daysBeforeOrderStart:SNull<STinyInt>;
public var daysBeforeOrderEnd:SNull<STinyInt>;
public var openingHour:SNull<SDate>;
public var closingHour:SNull<SDate>;
@formPopulate("placePopulate") @:relation(placeId) public var place : Place;
public function new() {
super();
}
public static function getLabels(){
var t = sugoi.i18n.Locale.texts;
return [
"cycleType" => t._("Fréquence"),
"startDate" => t._("Date de début"),
"endDate" => t._("Date de fin"),
"daysBeforeOrderStart" => t._("Ouverture de commande (nbre de jours avant distribution)"),
"daysBeforeOrderEnd" => t._("Fermeture de commande (nbre de jours avant distribution)"),
"place" => t._("Place"),
];
}
public function placePopulate():Array<{label:String,value:Int}> {
var out = [];
if ( App.current.user == null || App.current.user.amap == null ) return out;
var places = db.Place.manager.search($amapId == App.current.user.amap.id, false);
for (p in places) out.push( { label:p.name,value :p.id } );
return out;
}
}
+45
View File
@@ -0,0 +1,45 @@
package db;
import sys.db.Object;
import sys.db.Types;
/**
* Tool to Graph daily values
*/
@:id(key,date)
class Graph extends Object{
public var key:SString<128>;
public var date:SDate;
public var value:SFloat;
/**
* record a value
*/
public static function record(key:String,value:Float,?date:Date){
if(date==null) date = Date.now();
date = new Date(date.getFullYear(),date.getMonth(),date.getDate(),0,0,0);
var o = manager.select($key==key && $date==date,true);
if(o == null){
o = new Graph();
o.date = date;
o.key = key;
o.value = value;
o.insert();
}else{
o.value = value;
o.update();
}
return o;
}
/**
* Get a list of records in a time frame
*/
public static function getRange(key:String,from:Date,to:Date):Array<db.Graph>{
return Lambda.array(manager.search($key==key && $date>=from && $date<=to, false));
}
}
+24
View File
@@ -0,0 +1,24 @@
package db;
import sys.db.Object;
import sys.db.Types;
@:id(userId,amapId,year)
class Membership extends Object
{
@:relation(amapId)
public var amap : Amap;
@:relation(userId)
public var user : db.User;
//année de cotisation (année la plus ancienne si a cheval sur deux années : 2014-2015 -> 2014)
public var year : Int;
public var date : SNull<SDate>;
public static function get(user:User, amap:Amap,year:Int, ?lock = false) {
return manager.select($user == user && $amap == amap && $year == year, lock);
}
}
+28
View File
@@ -0,0 +1,28 @@
package db;
import sys.db.Object;
import sys.db.Types;
import tink.core.Noise;
import tink.core.Outcome;
import sugoi.mail.IMailer;
/**
* Message sent from the message Section
*/
class Message extends Object
{
public var id : SId;
@:relation(amapId) public var amap : SNull<Amap>;
@:relation(senderId) public var sender : SNull<User>;
public var recipientListId : SNull<SString<12>>;
public var recipients : SNull<SData<Array<String>>>;
public var title : SString<128>;
public var body : SText;
public var date : SDateTime;
}
+431
View File
@@ -0,0 +1,431 @@
package db;
import sys.db.Types;
import tink.core.Error;
enum OperationType{
VOrder; //order on a varying order contract
COrder;//order on a constant order contract
Payment;
Membership;
}
typedef PaymentInfos = {type:String, ?remoteOpId:Int, ?netAmount:Float};
typedef VOrderInfos = {basketId:Int};
typedef COrderInfos = {contractId:Int};
/**
* Payment operation
*
* @author fbarbut
*/
class Operation extends sys.db.Object
{
public var id : SId;
public var name : SString<128>;
public var amount : SFloat;
public var date : SDateTime;
public var type : SEnum<OperationType>;
public var data : SData<Dynamic>;
@hideInForms @:relation(relationId) public var relation : SNull<db.Operation>; //linked to another operation : ie a payment pays an order
@formPopulate("populate") @:relation(userId) public var user : db.User;
@hideInForms @:relation(groupId) public var group : db.Amap;
public var pending : SBool; //a pending payment means the payment has not been confirmed, a pending order means the ordre can still change before closing.
public function getTypeIndex(){
var e : OperationType = type;
return e.getIndex();
}
public function new(){
super();
pending = false;
}
/**
* if operation is a payment, give the payment type
*/
public function getPaymentType():String{
switch(type){
case Payment:
var x : PaymentInfos = this.data;
if (data == null){
return null;
}else{
return x.type;
}
default : return null;
}
}
/**
* get translated payment type name
*/
public function getPaymentTypeName(){
var t = getPaymentType();
if (t == null) return null;
for ( pt in service.PaymentService.getAllPaymentTypes()){
if (pt.type == t) return pt.name;
}
return null;
}
/**
* get payments linked to this order transaction
*/
public function getRelatedPayments(){
return db.Operation.manager.search($relation == this, false);
}
public function getOrderInfos(){
return switch(type){
case COrder, VOrder : this.data;
default : null;
}
}
public function getPaymentInfos():PaymentInfos{
return switch(type){
case Payment : this.data;
default : null;
}
}
public static function countOperations(user:db.User, group:db.Amap):Int{
return manager.count($user == user && $group == group);
}
/**
* get all user operations
* @param user -
* @param group -
* @param reverse=false -
*/
public static function getOperations(user:db.User, group:db.Amap,?reverse=false ){
if(reverse) {
return manager.search($user == user && $group == group,{orderBy:-date},false);
}
return manager.search($user == user && $group == group,{orderBy:date},false);
}
public static function getOperationsWithIndex(user:db.User, group:db.Amap,index:Int,limit:Int,?reverse=false ){
if(reverse) {
return manager.search($user == user && $group == group, { limit:[index,limit], orderBy:-date }, false);
}
return manager.search($user == user && $group == group, { limit:[index,limit], orderBy:date },false);
}
/*public static function getOrder_Operations(user:db.User, group:db.Amap,?limit=50 ){
//return manager.search($user == user && $group == group && $type!=Payment,{orderBy:date},false);
//return manager.search($user == user && $group == group && $relation==null,{orderBy:date},false);
return manager.search($user == user && $group == group,{orderBy:date,limit:limit},false);
}*/
public static function getPaymentOperations(user:db.User, group:db.Amap,?limit=50){
return manager.search($user == user && $group == group && $type == Payment, {orderBy:date,limit:limit},false);
}
public static function getLastOperations(user:db.User, group:db.Amap, ?limit = 50){
var c = manager.count($user == user && $group == group);
c -= limit;
if (c < 0) c = 0;
return manager.search($user == user && $group == group,{orderBy:date,limit:[c,limit]},false);
}
/**
* Create a new transaction
* @param orders
*/
public static function makeOrderOperation(orders: Array<db.UserContract>, ?basket:db.Basket){
if (orders == null) throw "orders are null";
if (orders.length == 0) throw "no orders";
if (orders[0].user == null ) throw "no user in order";
var t = sugoi.i18n.Locale.texts;
var _amount = 0.0;
for ( o in orders ){
var t = o.quantity * o.productPrice;
_amount += t + t * (o.feesRate / 100);
}
var contract = orders[0].product.contract;
var op = new db.Operation();
var user = orders[0].user;
var group = orders[0].product.contract.amap;
if (contract.type == db.Contract.TYPE_CONSTORDERS){
//Constant orders
var dNum = contract.getDistribs(false).length;
op.name = "" + contract.name + " (" + contract.vendor.name+") " + dNum + " " + t._("deliveries");
op.amount = dNum * (0 - _amount);
op.date = Date.now();
op.type = COrder;
var data : COrderInfos = {contractId:contract.id};
op.data = data;
op.user = user;
op.group = group;
op.pending = true;
}else{
if (basket == null) throw "varying contract orders should have a basket";
//varying orders
var date = App.current.view.dDate(orders[0].distribution.date);
op.name = t._("Order for ::date::",{date:date});
op.amount = 0 - _amount;
op.date = Date.now();
op.type = VOrder;
var data : VOrderInfos = {basketId:basket.id};
op.data = data;
op.user = user;
op.group = group;
op.pending = true;
}
op.insert();
service.PaymentService.updateUserBalance(op.user, op.group);
return op;
}
/**
* update an order operation
*/
public static function updateOrderOperation(op:db.Operation, orders: Array<db.UserContract>, ?basket:db.Basket){
op.lock();
var t = sugoi.i18n.Locale.texts;
var _amount = 0.0;
for ( o in orders ){
var a = o.quantity * o.productPrice;
_amount += a + a * (o.feesRate / 100);
}
var contract = orders[0].product.contract;
if (contract.type == db.Contract.TYPE_CONSTORDERS){
//Constant orders
var dNum = contract.getDistribs(false).length;
op.name = "" + contract.name + " (" + contract.vendor.name+") "+ dNum + " " + t._("deliveries");
op.amount = dNum * (0 - _amount);
}else{
if (basket == null) throw "varying contract orders should have a basket";
op.amount = 0 - _amount;
}
//op.date = Date.now(); //leave original date
op.update();
service.PaymentService.updateUserBalance(op.user, op.group);
return op;
}
/**
* Record a new payment operation
* @param type
* @param amount
* @param name
* @param relation
*/
public static function makePaymentOperation(user:db.User,group:db.Amap,type:String, amount:Float, name:String, ?relation:db.Operation ){
var t = new db.Operation();
t.amount = Math.abs(amount);
t.date = Date.now();
t.name = name;
t.group = group;
t.pending = true;
t.user = user;
t.type = Payment;
var data : PaymentInfos = {type:type};
t.data = data;
if(relation!=null) t.relation = relation;
t.insert();
service.PaymentService.updateUserBalance(user, group);
return t;
}
/**
* when updating a (varying) order , we need to update the existing pending transaction
*/
public static function findVOrderTransactionFor(dkey:String, user:db.User, group:db.Amap,?onlyPending=true):db.Operation{
//throw 'find $dkey for user ${user.id} in group ${group.id} , onlyPending:$onlyPending';
var date = dkey.split("|")[0];
var placeId = Std.parseInt(dkey.split("|")[1]);
var transactions = new List();
if (onlyPending){
transactions = manager.search($user == user && $group == group && $pending == true && $type==VOrder , {orderBy:-date}, true);
}else{
transactions = manager.search($user == user && $group == group && $type==VOrder , {orderBy:-date}, true);
}
//throw transactions;
var place = db.Place.manager.get(placeId,false);
var date = Date.fromString(date);
var basket = db.Basket.get(user, place, date);
if(basket==null) throw new Error('No basket found for user #'+user.id+', place #'+place.id+', date '+date);
for ( t in transactions){
switch(t.type){
case VOrder :
var data : VOrderInfos = t.data;
if ( data == null) continue;
if (data.basketId == basket.id) return t;
default :
continue;
}
}
return null;
}
/**
* when updating a constant order, we need to update the existing operation.
*/
public static function findCOrderTransactionFor(contract:db.Contract, user:db.User):db.Operation{
if (contract.type != db.Contract.TYPE_CONSTORDERS) throw "contract type should be TYPE_CONSTORDERS";
var transactions = manager.search($user == user && $group == contract.amap && $amount<=0 && $type==COrder, {orderBy:date,limit:100}, true);
for ( t in transactions){
switch(t.type){
case COrder :
//var id = Lambda.find(orders, function(x) return db.UserContract.manager.get(x, false) != null);
//if (id == null) {
////all orders in this transaction dont exists anymore
//t.delete();
//continue;
//}else{
//for ( i in orders){
//var order = db.UserContract.manager.get(i);
//if (order == null) continue;
//if (order.product.contract.id == contract.id) return t;
//}
//}
var data : COrderInfos = t.data;
if (data == null) continue;
if (data.contractId == contract.id) return t;
default :
continue;
}
}
return null;
}
/**
* Create/update the needed order operations and returns the related operations
* @param orders
*/
public static function onOrderConfirm(orders:Array<db.UserContract>):Array<db.Operation>{
if (orders.length == 0) return null;
if (orders[0] == null) return null;
var out = [];
var user = orders[0].user;
var group = orders[0].product.contract.amap;
//should not go further if group has not activated payements
if (user==null || !group.hasPayments()) return null;
//we consider that ALL orders are from the same contract type : varying or constant
if (orders[0].product.contract.type == db.Contract.TYPE_VARORDER ){
// varying contract :
//manage separatly orders which occur at different dates
var ordersGroup = tools.ObjectListTool.groupOrdersByKey(orders);
for ( orders in ordersGroup){
//find basket
var basket = null;
for ( o in orders) {
if (o.basket != null) {
basket = o.basket;
break;
}
}
//get all orders for the same multidistrib, in order to update related operation.
var k = orders[0].distribution.getKey();
var allOrders = db.UserContract.getUserOrdersByMultiDistrib(k, user, group);
//existing transaction
var existing = db.Operation.findVOrderTransactionFor( k , user, group, false);
var op;
if (existing != null){
op = db.Operation.updateOrderOperation(existing,allOrders,basket);
}else{
op = db.Operation.makeOrderOperation(allOrders,basket);
}
out.push(op);
//delete order and payment operations if sum of orders qt is 0
/*var sum = 0.0;
for ( o in allOrders) sum += o.quantity;
if ( sum == 0 ) {
existing.delete();
op.delete();
}*/
}
}else{
// constant contract
// create/update a transaction computed like $distribNumber * $price.
var contract = orders[0].product.contract;
var existing = db.Operation.findCOrderTransactionFor( contract , user);
if (existing != null){
out.push( db.Operation.updateOrderOperation(existing, contract.getUserOrders(user) ) );
}else{
out.push( db.Operation.makeOrderOperation( contract.getUserOrders(user) ) );
}
}
return out;
}
public function populate(){
return App.current.user.getAmap().getMembersFormElementData();
}
/*public static function getLabels(){
var t = sugoi.i18n.Locale.texts;
return [
"name" => t._("Text"),
"date" => t._("Date"),
"endDate" => t._("End date"),
"place" => t._("Place"),
"distributor1" => t._("Distributor #1"),
"distributor2" => t._("Distributor #2"),
"distributor3" => t._("Distributor #3"),
"distributor4" => t._("Distributor #4"),
];
}*/
}
+87
View File
@@ -0,0 +1,87 @@
package db;
import sys.db.Object;
import sys.db.Types;
import Common;
class Place extends Object
{
public var id : SId;
public var name : SString<64>;
public var address1:SNull<SString<64>>;
public var address2:SNull<SString<64>>;
public var zipCode:SString<32>;
public var city:SString<64>;
public var country:SNull<SString<64>>;
//latitude/longitude
public var lat:SNull<SFloat>;
public var lng:SNull<SFloat>;
@hideInForms @:relation(amapId) public var amap : Amap;
public function new()
{
super();
country = "France";
}
override function toString() {
if (name == null) {
return "place";
}else {
return name;
}
}
public function getFullAddress(){
var str = new StringBuf();
str.add(name+", \n");
if (address1 != null) str.add(address1 + ", \n");
if (address2 != null) str.add(address2 + ", \n");
if (zipCode != null) str.add(zipCode);
if (city != null) str.add(" - "+city);
return str.toString();
}
/**
* get adress without 'name' field.
*/
public function getAddress(){
var str = new StringBuf();
if (address1 != null) str.add(address1 + ", \n");
if (address2 != null) str.add(address2 + ", \n");
if (zipCode != null) str.add(zipCode);
if (city != null) str.add(" - "+city);
if(country != null) str.add(", "+country);
return str.toString();
}
public static function getLabels():Map<String,String>{
var t = sugoi.i18n.Locale.texts;
return [
"name" => t._("Name"),
"address1" => t._("Address 1"),
"address2" => t._("Address 2"),
"zipCode" => t._("Zip code"),
"city" => t._("City"),
"country" => t._("Country"),
"lat" => t._("Latitude"),
"lng" => t._("Longitude"),
];
}
public function getInfos():PlaceInfos{
return {
id:id,
name:name,
address1:address1,
address2:address2,
zipCode:zipCode,
city:city,
latitude : lat,
longitude: lng
}
}
}
+206
View File
@@ -0,0 +1,206 @@
package db;
import sys.db.Object;
import sys.db.Types;
import Common;
/**
* Product
*/
class Product extends Object
{
public var id : SId;
public var name : SString<128>;
public var ref : SNull<SString<32>>; //référence produit
@:relation(contractId)
public var contract : Contract;
//prix TTC
public var price : SFloat;
public var vat : SFloat;
public var desc : SNull<SText>;
public var stock : SNull<SFloat>; //if qantity can be float, stock should be float
public var unitType : SNull<SEnum<Unit>>; // Kg / L / g / units
public var qt : SNull<SFloat>;
public var organic : SBool;
public var variablePrice : Bool; //price can vary depending on weighting of the product
public var multiWeight : Bool; //product cannot be cumulated in one order record
//https://docs.google.com/document/d/1IqHN8THT6zbKrLdHDClKZLWgKWeL0xw6cYOiFofw04I/edit
@hideInForms public var wholesale : Bool; //this product is a wholesale product (crate,bag,pallet)
@hideInForms public var retail : Bool; //this products is a fraction of a wholesale product
@hideInForms public var bulk : Bool; //(vrac) warn the customer this product is not packaged
public var hasFloatQt:SBool; //this product can be ordered in "float" quantity
@hideInForms @:relation(imageId) public var image : SNull<sugoi.db.File>;
@:relation(txpProductId) public var txpProduct : SNull<db.TxpProduct>; //taxonomy
public var active : SBool; //if false, product disabled, not visible on front office
public function new()
{
super();
//type = 0;
organic = false;
hasFloatQt = false;
active = true;
variablePrice = false;
multiWeight = false;
wholesale = false;
retail = false;
bulk = false;
vat = 5.5;
unitType = Unit.Piece;
qt = 1;
}
/**
* Returns product image URL
*/
public function getImage() {
if (image == null) {
if (txpProduct != null){
return "/img/taxo/cat" + txpProduct.category.id + ".png";
}else{
return "/img/unknown.png";
}
}else {
return App.current.view.file(image);
}
}
public function getName(){
if (unitType != null && qt != null && qt != 0){
return name +" " + qt + " " + App.current.view.unit(unitType);
}else{
return name;
}
}
override function toString() {
return getName();
}
/**
* get price including margins
*/
public function getPrice():Float{
return price + contract.computeFees(price);
}
/**
get product infos as an anonymous object
@param CategFromTaxo=false
@param populateCategories=tru
@return
**/
public function infos(?CategFromTaxo=false,?populateCategories=true,?distribution:db.Distribution):ProductInfo {
var o :ProductInfo = {
id : id,
ref : ref,
name : name,
image : getImage(),
contractId : contract.id,
price : getPrice(),
vat : vat,
vatValue: (vat != 0 && vat != null) ? ( this.price - (this.price / (vat/100+1)) ) : null,
contractTax : contract.percentageValue,
contractTaxName : contract.percentageName,
desc : App.current.view.nl2br(desc),
categories : null,
subcategories:null,
orderable : this.contract.isUserOrderAvailable(),
stock : contract.hasStockManagement() ? this.stock : null,
hasFloatQt : hasFloatQt,
qt:qt,
unitType:unitType,
organic:organic,
variablePrice:variablePrice,
wholesale:wholesale,
active: active,
distributionId : distribution==null ? null : distribution.id,
}
if(populateCategories){
if (CategFromTaxo){
o.categories = [txpProduct == null?null:txpProduct.category.id];
o.subcategories = [txpProduct == null?null:txpProduct.subCategory.id];
}else{
o.categories = Lambda.array(Lambda.map(getCategories(), function(c) return c.id));
o.subcategories = o.categories;
}
}
App.current.event(ProductInfosEvent(o,distribution));
return o;
}
/**
* customs categs
*/
public function getCategories() {
//"Types de produits" categGroup first
//var pc = db.ProductCategory.manager.search($productId == id, {orderBy:categoryId}, false);
return Lambda.map(db.ProductCategory.manager.search($productId == id,{orderBy:categoryId},false), function(x) return x.category);
}
/**
* general categs
*/
public function getFullCategorization(){
if (txpProduct == null) return [];
return txpProduct.getFullCategorization();
}
public static function getByRef(c:db.Contract, ref:String){
var pids = tools.ObjectListTool.getIds(c.getProducts(false));
return db.Product.manager.select($ref == ref && $id in pids, false);
}
/**
* Fix values that will make mysql 5.7 scream
*/
function check(){
if(this.vat==null) this.vat=0;
if(this.name.length>128) this.name = this.name.substr(0,128);
}
override public function update(){
check();
super.update();
}
override public function insert(){
check();
super.insert();
}
public static function getLabels(){
var t = sugoi.i18n.Locale.texts;
return [
"name" => t._("Product name"),
"ref" => t._("Product ID"),
"price" => t._("Price"),
"desc" => t._("Description"),
"stock" => t._("Stock"),
"unitType" => t._("Base unit"),
"qt" => t._("Quantity"),
"hasFloatQt" => t._("Allow fractional quantities"),
"active" => t._("Available"),
"organic" => t._("Organic agriculture"),
"vat" => t._("VAT Rate"),
"variablePrice" => t._("Variable price based on weight"),
"multiWeight" => t._("Multi-weighing"),
];
}
}
+27
View File
@@ -0,0 +1,27 @@
package db;
import sys.db.Types;
@:id(productId,categoryId)
class ProductCategory extends sys.db.Object
{
@:relation(productId)
public var product : db.Product;
@:relation(categoryId)
public var category : db.Category;
public static function getOrCreate(product, category){
var x = db.ProductCategory.manager.select($product==product && $category==category,true);
if(x==null){
x = new db.ProductCategory();
x.product = product;
x.category = category;
x.insert();
}
return x;
}
}
+23
View File
@@ -0,0 +1,23 @@
package db;
import sys.db.Object;
import sys.db.Types;
/**
* ...
* @author fbarbut
*/
class TxpCategory extends Object
{
public var id : SId;
public var name : SString<128>;
public function getSubCategories(){
return db.TxpSubCategory.manager.search($category == this, false);
}
override public function toString(){
return '#$id-$name';
}
}
+27
View File
@@ -0,0 +1,27 @@
package db;
import sys.db.Object;
import sys.db.Types;
/**
* ...
* @author fbarbut
*/
class TxpProduct extends Object
{
public var id : SId;
public var name : SString<128>;
@:relation(categoryId) public var category : db.TxpCategory;
@:relation(subCategoryId) public var subCategory : db.TxpSubCategory;
override public function toString(){
return '#$id-$name';
}
public function getFullCategorization():Array<String>{
return [category.name, subCategory.name, name];
}
}
+26
View File
@@ -0,0 +1,26 @@
package db;
import sys.db.Object;
import sys.db.Types;
/**
* ...
* @author fbarbut
*/
class TxpSubCategory extends Object
{
public var id : SId;
public var name : SString<128>;
@:relation(categoryId) public var category:db.TxpCategory;
public function getProducts(){
return db.TxpProduct.manager.search($subCategory == this, false);
}
override public function toString(){
return '#$id-$name';
}
}
Executable
+574
View File
@@ -0,0 +1,574 @@
package db;
import sys.db.Object;
import sys.db.Types;
import db.UserAmap;
import Common;
enum UserFlags {
HasEmailNotif4h; //send notifications by mail 4h before
HasEmailNotif24h; //send notifications by mail 24h before
HasEmailNotifOuverture; //send notifications by mail on command open
//Tuto; //enable tutorials
}
/**
* Site-wide right
*/
enum RightSite {
Admin;
}
@:index(email,unique)
class User extends Object {
public var id : SId;
public var lang : SString<2>;
@:skip public var name(get, set) : String;
public var pass : STinyText;
public var rights : SFlags<RightSite>;
public var firstName:SString<32>;
public var lastName:SString<32>;
public var email : SString<64>;
public var phone:SNull<SString<19>>;
public var firstName2:SNull<SString<32>>;
public var lastName2:SNull<SString<32>>;
public var email2 : SNull<SString<64>>;
public var phone2:SNull<SString<19>>;
public var address1:SNull<SString<64>>;
public var address2:SNull<SString<64>>;
public var zipCode:SNull<SString<32>>;
public var city:SNull<SString<25>>;
@:skip public var amap(get_amap, null) : Amap;
public var cdate : SDate; //creation
public var ldate : SNull<SDateTime>; //derniere connexion
public var flags : SFlags<UserFlags>;
@hideInForms public var tutoState : SNull<SData<{name:String,step:Int}>>; //tutorial state
public var apiKey : SNull<SString<128>>; //private API key
public function new() {
super();
//default values
cdate = Date.now();
rights = sys.db.Types.SFlags.ofInt(0);
flags = sys.db.Types.SFlags.ofInt(0);
flags.set(HasEmailNotif24h);
flags.set(HasEmailNotifOuverture);
lang = "fr";
pass = "";
}
public override function toString() {
return getName()+" ["+id+"]";
}
public function isAdmin() {
return rights.has(Admin) || id==1;
}
public static function login(user:db.User, email:String) {
user.lock();
user.ldate = Date.now();
user.update();
App.current.session.setUser(user);
if (App.current.session.data == null) App.current.session.data = {};
//Who's connected, user1 or user2 ?
App.current.session.data.whichUser = (email == user.email) ? 0 : 1;
}
/**
* is this user the manager of the current group
*/
public function isAmapManager() {
var a = getAmap();
if (a == null) return false;
var ua = getUserAmap(a);
if (ua == null) return false;
return ua.hasRight(Right.GroupAdmin);
}
function getUserAmap(amap:db.Amap):db.UserAmap {
return db.UserAmap.get(this, amap);
}
public function isFullyRegistred(){
return pass != null && pass != "";
}
public function makeMemberOf(group:db.Amap){
var ua = db.UserAmap.get(this, group);
if (ua == null) {
ua = new db.UserAmap();
ua.user = this;
ua.amap = group;
ua.insert();
}
return ua;
}
/**
* Est ce que ce membre a la gestion de ce contrat
* si null, est ce qu'il a la gestion d'un des contrat, n'importe lequel (utilse pour afficher 'gestion contrat' dans la nav )
* @param contract
*/
public function isContractManager(?contract:db.Contract ) {
if (isAdmin()) return true;
if (contract != null) {
return canManageContract(contract);
}else {
var ua = getUserAmap(getAmap());
if (ua == null) return false;
if (ua.rights == null) return false;
for (r in ua.rights) {
switch(r) {
case Right.ContractAdmin(cid):
return true;
default:
}
}
return false;
}
}
public function canManageAllContracts(){
if (isAdmin()) return true;
var ua = getUserAmap(getAmap());
if (ua == null) return false;
if (ua.rights == null) return false;
for (r in ua.rights) {
switch(r) {
case Right.ContractAdmin(cid):
if(cid==null) return true;
default:
}
}
return false;
}
public function canAccessMessages():Bool {
var ua = getUserAmap(getAmap());
if (ua == null) return false;
if (ua.hasRight(Right.Messages)) return true;
return false;
}
public function canAccessMembership():Bool {
var ua = getUserAmap(getAmap());
if (ua == null) return false;
if (ua.hasRight(Right.Membership)) return true;
return false;
}
public function canManageContract(c:db.Contract):Bool {
var ua = getUserAmap(c.amap);
if (ua == null) return false;
if (ua.hasRight(Right.ContractAdmin())) return true;
if (ua.hasRight(Right.ContractAdmin(c.id))) return true;
return false;
}
public function getContractManager(?lock=false) {
return Contract.manager.search($amap == amap && $contact == this, false);
}
public function getName() {
return get_name();
}
public function get_name() {
return lastName + " " + firstName;
}
public function getCoupleName() {
var n = lastName + " " + firstName;
if (lastName2 != null) {
n = n + " / " + lastName2 + " " + firstName2;
}
return n;
}
public function set_name(name:String) {
var name = name.split(' ');
firstName = name[0];
lastName = name[1];
return firstName+" "+lastName;
}
/**
* Encode a user password
* @param p
*/
public function setPass(p:String) {
if (p == null){
this.pass = "";
}else{
this.pass = haxe.crypto.Md5.encode( App.config.get('key') + StringTools.trim(p));
}
return this.pass;
}
/**
* Renvoie les commandes actuelles du user
* @param _amap force une amap
* @param lock=false
*/
public function getOrders(?_amap:db.Amap,?lock = false):List<UserContract> {
var a = _amap == null ? getAmap() : _amap;
var c = a.getActiveContracts(true);
var cids = Lambda.map(c,function(m) return m.id);
var pids = Lambda.map(db.Product.manager.search($contractId in cids,false), function(x) return x.id);
var out = UserContract.manager.search(($userId == id || $userId2 == id) && $productId in pids, lock);
return out;
}
/**
* renvoie les commandes à partir d'une liste de contrats
*/
public function getOrdersFromContracts(c:Iterable<db.Contract>):List<db.UserContract> {
var cids = Lambda.map(c,function(m) return m.id);
var pids = Lambda.map(db.Product.manager.search($contractId in cids,false), function(x) return x.id);
return UserContract.manager.search(($userId == id || $userId2 == id) && $productId in pids, false);
}
/**
* renvoie les commandes de contrat variables à partir d'une distribution
*/
public function getOrdersFromDistrib(d:db.Distribution):List<db.UserContract> {
var pids = Lambda.map(db.Product.manager.search($contractId == d.contract.id, false), function(x) return x.id);
return UserContract.manager.search(($userId == id || $userId2 == id) && $distributionId==d.id && $productId in pids , false);
}
public function get_amap():Amap {
return getAmap();
}
/**
* renvoie l'amap selectionnée par le user en cours
*/
public function getAmap() {
if (App.current.user != null && id != App.current.user.id) throw "This function is valid only for the current user";
if (App.current.session == null) return null;
if (App.current.session.data == null ) return null;
var a = App.current.session.data.amapId;
if (a == null) {
return null;
}else {
return Amap.manager.get(a,false);
}
}
/**
* get groups this user belongs to
*/
public function getAmaps():List<db.Amap> {
return Lambda.map(UserAmap.manager.search($user == this, false), function(o) return o.amap);
}
public function isMemberOf(amap:Amap) {
return UserAmap.manager.select($user == this && $amapId == amap.id, false) != null;
}
/**
* Renvoie la liste des contrats dans lequel l'adherent a des commandes
* @param lock=false
* @return
*/
public function getContracts(?lock=false):Array<Contract> {
var out = [];
var ucs = getOrders(lock);
for (uc in ucs) {
if (!Lambda.has(out, uc.product.contract)) {
out.push(uc.product.contract);
}
}
return out;
}
/**
* Merge fields of 2 users, then delete the second (u2)
* Be carefull : check before calling this function that u2 can be safely deleted !
*/
public function merge(u2:db.User) {
this.lock();
u2.lock();
var m = function(a, b) {
return a == null || a=="" ? b : a;
}
this.address1 = m(this.address1, u2.address1);
this.address2 = m(this.address2, u2.address2);
this.zipCode = m(this.zipCode, u2.zipCode);
this.city = m(this.city, u2.city);
//find how to merge the 2 names in each account
if (this.email == u2.email) {
this.firstName = m(this.firstName, u2.firstName);
this.lastName = m(this.lastName, u2.lastName);
this.phone = m(this.phone, u2.phone);
} else if (this.email == u2.email2) {
this.firstName = m(this.firstName, u2.firstName2);
this.lastName = m(this.lastName, u2.lastName2);
this.phone = m(this.phone, u2.phone2);
}
if (this.email2 == u2.email) {
this.firstName2 = m(this.firstName2, u2.firstName);
this.lastName2 = m(this.lastName2, u2.lastName);
this.phone2 = m(this.phone2, u2.phone);
} else if (this.email2 == u2.email2) {
this.firstName2 = m(this.firstName2, u2.firstName2);
this.lastName2 = m(this.lastName2, u2.lastName2);
this.phone2 = m(this.phone2, u2.phone2);
}
u2.delete();
this.update();
}
public static function getOrCreate(firstName:String, lastName:String, email:String):db.User{
var u = db.User.manager.select($email == email || $email2 == email, true);
if (u == null){
u = new db.User();
u.firstName = firstName;
u.lastName = lastName;
u.email = email;
u.insert();
}
return u;
}
/**
* Search for similar users in the DB ( same firstName+lastName or same email )
*/
public static function __getSimilar(firstName:String, lastName:String, email:String,?firstName2:String, ?lastName2:String, ?email2:String):List<db.User> {
var out = new Array();
out = Lambda.array(User.manager.search($firstName.like(firstName) && $lastName.like(lastName), false));
out = out.concat(Lambda.array(User.manager.search($email.like(email), false)));
out = out.concat(Lambda.array(User.manager.search($firstName2.like(firstName) && $lastName2.like(lastName), false)));
out = out.concat(Lambda.array(User.manager.search($email2.like(email), false)));
//recherche pour le deuxieme user
if (lastName2 != "" && lastName2 != null && firstName2 != "" && firstName2 != null) {
out = out.concat(Lambda.array(User.manager.search($firstName.like(firstName2) && $lastName.like(lastName2), false)));
out = out.concat(Lambda.array(User.manager.search($firstName2.like(firstName2) && $lastName2.like(lastName2), false)));
}
if (email2 != null && email2 != "") {
out = out.concat(Lambda.array(User.manager.search($email.like(email2), false)));
out = out.concat(Lambda.array(User.manager.search($email2.like(email2), false)));
}
//dedouble
var x = new Map<Int,db.User>();
for ( oo in out) {
x.set(oo.id, oo);
}
return Lambda.list(x);
}
/**
* Search for similar users in the DB ( same email )
*/
public static function getSameEmail(email:String, ?email2:String){
var out = new Array();
out = out.concat(Lambda.array(User.manager.search($email.like(email), false)));
out = out.concat(Lambda.array(User.manager.search($email2.like(email), false)));
if (email2 != null && email2 != "") {
out = out.concat(Lambda.array(User.manager.search($email.like(email2), false)));
out = out.concat(Lambda.array(User.manager.search($email2.like(email2), false)));
}
return Lambda.list(out);
}
/**
* Get users with no contracts
**/
public static function getUsers_NoContracts(?index:Int,?limit:Int):List<db.User> {
var productsIds = App.current.user.getAmap().getProducts().map(function(x) return x.id);
var uc = UserContract.manager.search($productId in productsIds, false);
var uc2 = uc.map(function(x) return x.user.id); //liste des userId avec un contrat dans cette amap
// J. Le Clerc - BUGFIX#1 Ne pas oublier les contrats alternés
for (u in uc) {
if (u.user2 != null) {
uc2.add(u.user2.id);
}
}
if (uc2.length > 0){
//les gens qui sont dans cette amap et qui n'ont pas de contrat de cette amap
var ua = db.UserAmap.manager.unsafeObjects("select * from UserAmap where amapId=" + App.current.user.getAmap().id +" and userId NOT IN(" + uc2.join(",") + ")", false);
return Lambda.map(ua, function(x) return x.user);
}else{
return App.current.user.amap.getMembers();
}
}
/**
* User with contracts
*/
public static function getUsers_Contracts(?index:Int,?limit:Int):List<db.User> {
var productsIds = App.current.user.getAmap().getProducts().map(function(x) return x.id);
if (productsIds.length == 0) return new List();
return db.User.manager.unsafeObjects("select u.* from User u, UserContract uc where uc.productId IN(" + productsIds.join(",") + ") AND (uc.userId=u.id OR uc.userId2=u.id) group by u.id ORDER BY u.lastName", false);
}
public static function getUsers_NoMembership(?index:Int,?limit:Int):List<db.User> {
var ua = new List();
if (index == null && limit == null) {
ua = db.UserAmap.manager.search($amap == App.current.user.amap, false);
}else {
ua = db.UserAmap.manager.search($amap == App.current.user.amap,{limit:[index,limit]}, false);
}
for (u in Lambda.array(ua)) {
if (u.hasValidMembership()) ua.remove(u);
}
return Lambda.map(ua, function(x) return x.user);
}
public static function getUsers_NewUsers(?index:Int, ?limit:Int):List<db.User> {
var uas = db.UserAmap.manager.search($amap == App.current.user.amap, false);
var ids = Lambda.map(uas, function(x) return x.user.id);
if (index == null && limit == null) {
return db.User.manager.search($pass == "" && ($id in ids), {orderBy:lastName} ,false);
}else {
return db.User.manager.search($pass == "" && ($id in ids), {limit:[index, limit] ,orderBy:lastName} , false);
}
}
public function sendInvitation(group:db.Amap) {
var t = sugoi.i18n.Locale.texts;
if (isFullyRegistred()) throw t._("This user cannot receive an invitation");
/*var group : db.Amap = null;
if (App.current.user == null) {
group = this.getAmaps().first();
}else {
//prend l'amap du user connecté qui a lancé l'invite.
group = App.current.user.amap;
}*/
//store token
var k = sugoi.db.Session.generateId();
sugoi.db.Cache.set("validation" + k, this.id, 60 * 60 * 24 * 30); //expire in 1 month
var e = new sugoi.mail.Mail();
if (group != null){
e.setSubject(t._("Invitation")+" "+group.name);
}else{
e.setSubject(t._("Invitation Cagette.net"));
}
e.addRecipient(this.email,this.getName());
e.setSender(App.config.get("default_email"),t._("Cagette.net"));
var html = App.current.processTemplate("mail/invitation.mtt", {
email:email,
email2:email2,
groupName:(group == null?null:group.name),
name:firstName,
k:k
} );
e.setHtmlBody(html);
App.sendMail(e);
}
/**
* cleaning before saving
*/
override public function insert() {
clean();
super.insert();
}
override public function update() {
clean();
super.update();
}
function clean() {
//emails
this.email = this.email.toLowerCase();
if (this.email2 != null) this.email2 = this.email2.toLowerCase();
//lastname
if (this.lastName != null) this.lastName = this.lastName.toUpperCase();
if (this.lastName2 != null) this.lastName2 = this.lastName2.toUpperCase();
if(pass==null) pass="";
}
public function infos():UserInfo{
return {
id:id,
name : getName(),
email : email
};
}
/**
* get form labels
*/
public static function getLabels():Map<String,String>{
var t = sugoi.i18n.Locale.texts;
return [
"firstName" => t._("First name"),
"lastName" => t._("Last name"),
"email" => t._("Email"),
"phone" => t._("Phone"),
"firstName2"=> t._("Partner first name"),
"lastName2" => t._("Partner last name"),
"email2" => t._("Partner email"),
"phone2" => t._("Partner phone"),
"lang" => t._("Language"),
"address1" => t._("Address 1"),
"address2" => t._("Address 2"),
"zipCode" => t._("Zip code"),
"city" => t._("City"),
"rights" => t._("Rights"),
"cdate" => t._("Registration date"),
"flags" => t._("Options"),
"pass" => t._("Password"),
];
}
}
+128
View File
@@ -0,0 +1,128 @@
package db;
import sys.db.Object;
import sys.db.Types;
enum Right{
GroupAdmin; //can manage whole group
ContractAdmin(?cid:Int); //can manage one or all contracts
Membership; //can manage group members
Messages; //can send messages
}
/**
* A user which is member of a group
*/
@:id(userId,amapId)
class UserAmap extends Object
{
@:relation(amapId) public var amap : db.Amap;
@:relation(userId) public var user : db.User;
public var rights : SNull<SData<Array<Right>>>;
public var balance : SFloat; //account balance in group currency
static var CACHE = new Map<String,db.UserAmap>();
public function new(){
super();
balance = 0;
}
public static function get(user:User, amap:Amap, ?lock = false) {
if (user == null || amap == null) return null;
//SPOD doesnt cache elements with double primary key, so lets do it manually
var c = CACHE.get(user.id + "-" + amap.id);
if (c == null) {
c = manager.select($user == user && $amap == amap, true/*lock*/);
CACHE.set(user.id + "-" + amap.id,c);
}
return c;
}
public static function getOrCreate(user:db.User, group:db.Amap){
var ua = get(user, group);
if ( ua == null){
ua = new UserAmap();
ua.user = user;
ua.amap = group;
ua.insert();
}
return ua;
}
/**
* give right and update DB
*/
public function giveRight(r:Right) {
if (hasRight(r)) return;
if (rights == null) rights = [];
lock();
rights.push(r);
update();
}
/**
* remove right and update DB
*/
public function removeRight(r:Right) {
if (rights == null) return;
var newrights = [];
for (right in rights.copy()) {
if ( !Type.enumEq(right, r) ) {
newrights.push(right);
}
}
rights = newrights;
update();
}
public function hasRight(r:Right):Bool {
if (this.user.isAdmin()) return true;
if (rights == null) return false;
for ( right in rights) {
if ( Type.enumEq(r,right) ) return true;
}
return false;
}
public function getRightName(r:Right):String {
var t = sugoi.i18n.Locale.texts;
return switch(r) {
case Right.GroupAdmin : t._("Administrator");
case Right.Messages : t._("Messaging");
case Right.Membership : t._("Members management");
case Right.ContractAdmin(cid) :
if (cid == null) {
t._("Management of all contracts");
}else {
var c = db.Contract.manager.get(cid);
if(c==null) {
t._("Deleted contract");
}else{
t._("::name:: contract management",{name:c.name});
}
}
}
}
public function hasValidMembership():Bool {
if (amap.membershipRenewalDate == null) return false;
var cotis = db.Membership.get(this.user, this.amap, this.amap.getMembershipYear());
return cotis != null;
}
override public function insert(){
App.current.event(NewMember(this.user,this.amap));
super.insert();
}
public function getPaymentOperations(){
return db.Operation.getPaymentOperations(user, amap);
}
public function getLastOperations(limit){
return db.Operation.getLastOperations(user, amap, limit);
}
}
+200
View File
@@ -0,0 +1,200 @@
package db;
import sys.db.Object;
import sys.db.Types;
import Common;
/**
* a product order
*/
class UserContract extends Object
{
public var id : SId;
@formPopulate("populate") @:relation(userId)
public var user : User;
//shared order
@formPopulate("populate") @:relation(userId2)
public var user2 : SNull<User>;
public var quantity : SFloat;
@formPopulate("populateProducts") @:relation(productId)
public var product : Product;
//store price (1 unit price) and fees (percentage not amount ) rate when the order is done
public var productPrice : SFloat;
public var feesRate : SInt; //fees in percentage
public var paid : SBool;
//if not null : varying orders
@:relation(distributionId)
public var distribution:SNull<db.Distribution>;
@:relation(basketId)
public var basket:SNull<db.Basket>;
public var date : SDateTime;
public var flags : SFlags<OrderFlags>;
public function new()
{
super();
quantity = 1;
paid = false;
date = Date.now();
flags = cast 0;
feesRate = 0;
}
public function populate() {
return App.current.user.getAmap().getMembersFormElementData();
}
/**
* For shared alternated orders in AMAP contracts
* @param distrib
* @return false -> user , true -> user2
*/
public function getWhosTurn(distrib:Distribution) {
if (distrib == null) throw "distribution is null";
if (user2 == null) throw "this contract is not shared";
//compter le nbre de distrib pour ce contrat
var c = Distribution.manager.count( $contract == product.contract && $date >= product.contract.startDate && $date <= distrib.date);
var r = c % 2 == 0;
if (flags.has(InvertSharedOrder)){
return !r;
}else{
return r;
}
}
override public function toString() {
if(product==null) return quantity +"x produit inconnu";
return quantity + "x" + product.name;
}
public function hasInvertSharedOrder():Bool{
return flags.has(InvertSharedOrder);
}
/**
* On peut modifier si ça na pas deja été payé + commande encore ouvertes
*/
public function canModify():Bool {
var can = false;
if (this.product.contract.type == db.Contract.TYPE_VARORDER) {
if (this.distribution.orderStartDate == null) {
can = true;
}else {
var n = Date.now().getTime();
can = n > this.distribution.orderStartDate.getTime() && n < this.distribution.orderEndDate.getTime();
}
}else {
can = this.product.contract.isUserOrderAvailable();
}
return can && !this.paid;
}
/**
* get users orders for a distribution
*/
public static function getOrders(contract:db.Contract, ?distribution:db.Distribution, ?csv = false):Array<UserOrder>{
var view = App.current.view;
var orders = new Array<db.UserContract>();
if (contract.type == db.Contract.TYPE_VARORDER ) {
orders = contract.getOrders(distribution);
}else {
orders = contract.getOrders();
}
var orders = service.OrderService.prepare(orders);
//CSV export
if (csv) {
var t = sugoi.i18n.Locale.texts;
var data = new Array<Dynamic>();
for (o in orders) {
data.push( {
"name":o.userName,
"productName":o.productName,
"price":view.formatNum(o.productPrice),
"quantity":view.formatNum(o.quantity),
"fees":view.formatNum(o.fees),
"total":view.formatNum(o.total),
"paid":o.paid
});
}
var exportName = "";
if (distribution != null){
exportName = contract.amap.name + " - " + t._("Delivery ::contractName:: ", {contractName:contract.name}) + distribution.date.toString().substr(0, 10);
}else{
exportName = contract.amap.name + " - " + contract.name;
}
sugoi.tools.Csv.printCsvDataFromObjects(data, ["name", "productName", "price", "quantity", "fees", "total", "paid"], exportName+" - " + t._("Per member"));
return null;
}else{
return orders;
}
}
/**
* Get the orders (varying orders) of a user for a multidistrib ( distribs with same day + same place )
*
* @param distribKey "$date|$placeId"
*/
public static function getUserOrdersByMultiDistrib(distribKey:String, user:db.User,group:db.Amap):Array<db.UserContract>{
//var contracts = db.Contract.getActiveContracts(group);
var contracts = db.Contract.manager.search($amap == group, false); //should be able to edit a contract which is closed
for ( c in Lambda.array(contracts)){
if (c.type == db.Contract.TYPE_CONSTORDERS){
contracts.remove(c); //only varying orders
}
}
var cids = Lambda.map(contracts, function(x) return x.id);
var start = Date.fromString(distribKey.split("|")[0] + " 00:00:00");
var end = Date.fromString(distribKey.split("|")[0] + " 23:59:00");
var ds = db.Distribution.manager.search($date > start && $date < end && ($contractId in cids), false);
var out = [];
for (d in ds) {
out = out.concat(Lambda.array(user.getOrdersFromDistrib(d)));
}
return out;
}
public static function getTotalPrice(tmpOrder:OrderInSession){
var t = 0.0;
for ( o in tmpOrder.products){
var p = db.Product.manager.get(o.productId, false);
t += o.quantity * p.getPrice();
}
return t;
}
function check(){
if(quantity==null) quantity == 1;
}
override function update(){
check();
super.update();
}
override function insert(){
check();
super.insert();
}
}
+61
View File
@@ -0,0 +1,61 @@
package db;
import sys.db.Object;
import sys.db.Types;
/**
* Vendor (producteur)
*/
class Vendor extends Object
{
public var id : SId;
public var name : SString<128>;
public var email : STinyText;
public var phone:SNull<SString<19>>;
public var address1:SNull<SString<64>>;
public var address2:SNull<SString<64>>;
public var zipCode:SString<32>;
public var city:SString<25>;
public var desc : SNull<SText>;
public var linkText:SNull<SString<256>>;
public var linkUrl:SNull<SString<256>>;
@hideInForms @:relation(imageId) public var image : SNull<sugoi.db.File>;
@:relation(amapId) public var amap : SNull<Amap>;
public function new()
{
super();
var t = sugoi.i18n.Locale.texts;
name = t._("Supplier");
}
override function toString() {
return name;
}
public function getActiveContracts(){
var now = Date.now();
return db.Contract.manager.search($vendor == this && $startDate < now && $endDate > now ,{orderBy:-startDate}, false);
}
public static function getLabels(){
var t = sugoi.i18n.Locale.texts;
return [
"name" => t._("Supplier name"),
"desc" => t._("Description"),
"email" => t._("Email"),
"phone" => t._("Phone"),
"address1" => t._("Address 1"),
"address2" => t._("Address 2"),
"zipCode" => t._("Zip code"),
"city" => t._("City"),
"linkText" => t._("Link text"),
"linkUrl" => t._("Link URL"),
];
}
}
+20
View File
@@ -0,0 +1,20 @@
package db;
import sys.db.Object;
import sys.db.Types;
@:id(userId,amapId)
class WaitingList extends Object
{
@:relation(amapId) public var group : Amap;
@:relation(userId) public var user : db.User;
public var date : SDateTime;
public var message : SText;
public function new(){
super();
message = "";
date = Date.now();
}
}
+46
View File
@@ -0,0 +1,46 @@
package form;
import sugoi.form.elements.RadioGroup;
import Common;
class ColorRadioGroup extends RadioGroup
{
public function new(name:String, label:String,selected:String)
{
var data = [];
var i = 0;
for (c in db.CategoryGroup.COLORS) {
data.push( { value:Std.string(c), label:Std.string(i) } );
i++;
}
super(name, label, data, selected, "1", false, true);
}
override public function render():String
{
var s = "";
var n = parentForm.name + "_" +name;
var c = 0;
if (data != null)
{
for (row in data)
{
var radio = "<input type=\"radio\" name=\""+n+"\" id=\""+n+c+"\" value=\"" + row.label + "\" " + (row.label == Std.string(value) ? "checked":"") +" />\n";
var img = "<div style='margin-right:16px;width:32px;height:32px;background:"+App.current.view.intToHex(Std.parseInt(row.value))+";'></div>";
s += "<label for=\"" + n+c + "\" class='checkbox' style='display: inline-block;'>"+radio + " "+img+" </label>";
c++;
}
}
return s;
}
}
+50
View File
@@ -0,0 +1,50 @@
package form;
import db.Product;
import sugoi.form.elements.RadioGroup;
import db.Product;
import Common;
/**
* list des types de produits avec image
* @author
*/
class ProductTypeRadioGroup extends RadioGroup
{
public function new(name:String, label:String,selected:String)
{
var data = [];
var i = 0;
for (e in Type.getEnumConstructs(ProductType)) {
data.push( { key:Std.string(i), value:Std.string(e) } );
i++;
}
super(name, label, data, selected, "1", false, true);
}
override public function render():String
{
var s = "";
var n = parentForm.name + "_" +name;
var c = 0;
if (data != null)
{
for (row in data)
{
var radio = "<input type=\"radio\" name=\""+n+"\" id=\""+n+c+"\" value=\"" + row.key + "\" " + (row.key == Std.string(value) ? "checked":"") +" />\n";
var e = Type.createEnumIndex(ProductType, Std.parseInt(row.key));
var img = "<img src='/img/"+Std.string(e).toLowerCase().substring(2)+".png' />";
s += "<label for=\"" + n+c + "\" class='checkbox' style='display: inline-block;'>"+radio + " "+img+" </label>";
c++;
}
}
return s;
}
}
+27
View File
@@ -0,0 +1,27 @@
package form;
import Common;
/**
* ...
* @author fbarbut
*/
class UnitQuantity extends sugoi.form.elements.FloatInput
{
var unit : Unit;
public function new(name, label, value, ?required=false,unit){
super(name, label, value, required);
this.unit = unit;
}
override function render(){
var r = super.render();
return '
<div class="input-group">
'+r+'
<div class="input-group-addon">'+App.current.view.unit(unit)+'</div>
</div>';
}
}
+22
View File
@@ -0,0 +1,22 @@
package payment;
/**
* ...
* @author fbarbut
*/
class Cash extends payment.Payment
{
public static var TYPE = "cash";
public function new()
{
var t = sugoi.i18n.Locale.texts;
this.type = TYPE;
this.icon = '<i class="fa fa-credit-card" aria-hidden="true"></i>';
this.name = t._("Cash");
//this.desc = t._("Pay by cash at product distribution");
this.link = "/transaction/cash";
}
}
+24
View File
@@ -0,0 +1,24 @@
package payment;
/**
* ...
* @author fbarbut
*/
class Check extends payment.Payment
{
public static var TYPE = "check";
public function new()
{
var t = sugoi.i18n.Locale.texts;
this.type = TYPE;
this.icon = '<i class="fa fa-credit-card" aria-hidden="true"></i>';
this.name = t._("Check");
this.link = "/transaction/check";
}
public static function getCode(date:Date,place:db.Place,user:db.User){
return date.toString().substr(0, 10) + "-" + place.id + "-" + user.id;
}
}
+22
View File
@@ -0,0 +1,22 @@
package payment;
/**
* ...
* @author fbarbut
*/
class MoneyPot extends payment.Payment
{
public static var TYPE = "moneypot";
public function new()
{
var t = sugoi.i18n.Locale.texts;
this.type = TYPE;
this.icon = '<i class="glyphicon glyphicon-piggy-bank" aria-hidden="true"></i>';
this.name = t._("Money pot");
//this.desc = t._("Pay by cash at product distribution");
this.link = "/transaction/moneypot";
}
}
+15
View File
@@ -0,0 +1,15 @@
package payment;
/**
* ...
* @author fbarbut
*/
class Payment
{
public var type:String;
public var icon:String;
public var name:String; //translated name
public var link:String;
}
+21
View File
@@ -0,0 +1,21 @@
package payment;
/**
* ...
* @author fbarbut
*/
class Transfer extends payment.Payment
{
public static var TYPE = "transfer";
public function new()
{
var t = sugoi.i18n.Locale.texts;
this.type = TYPE;
this.icon = '<i class="fa fa-credit-card" aria-hidden="true"></i>';
this.name = t._("Bank transfer");
this.link = "/transaction/transfer";
}
}
+109
View File
@@ -0,0 +1,109 @@
package plugin;
import Common;
import sugoi.plugin.*;
/**
* Tutorials internal plugin
*
* Its listening to events to know if
* the user can go to the next step of his tutorial
*
* User's tutorial state is stored in user.tutoState.
* The JS widget is triggered in view.init()
*
*
*/
class Tutorial extends PlugIn implements IPlugIn
{
public function new() {
super();
App.current.eventDispatcher.add(onEvent);
}
/**
* catch events
*/
public function onEvent(e:Event) {
//no need to continue if tutos are disabled
if ( App.current.user==null || App.current.user.tutoState==null ) return;
switch(e) {
//a page is displayed
case Page(uri):
var ts = App.current.user.tutoState;
if (ts == null) return;
var tuto = TutoDatas.get(ts.name);
var step = tuto.steps[ts.step];
if (step == null ) return;
//skip steps if action is "next"
while (step.action.equals(TANext)) {
if (ts.step + 1 >= tuto.steps.length) break;
ts.step++;
step = tuto.steps[ts.step];
}
//trace( "tuto active, listening to step="+ts.step );
switch(step.action) {
case TAPage(_uri):
//trace(""+_uri+"="+uri+" ?");
if (match(_uri, uri)) {
//trace("ok");
var u = App.current.user;
u.lock();
if ( ts.step+1 >= tuto.steps.length) {
//tuto finished
u.tutoState = null;
}else {
//next step
u.tutoState.step = ts.step+1;
}
u.update();
}
default:
}
default :
}
}
/**
* to know if the current uri matches with the tuto step uri
*/
public static function match(pattern:String, uri:String):Bool {
if (pattern.indexOf("*") > -1) {
//the url contains a wildcard
// ~/http:\/\/(\w+).com/ match urls like http://anything.com
var s = pattern;
s = StringTools.replace(s, "/", "\\/"); //escape antislashes
s = StringTools.replace(s, "*", "(\\w+)");
var e = new EReg(s,"");
return e.match(uri);
}else {
return pattern == uri;
}
}
public static function all() {
TutoDatas.get("intro");//just to init translation
return TutoDatas.TUTOS;
}
}
+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();
}
}
+299
View File
@@ -0,0 +1,299 @@
package test;
/**
* Test distribution creation
*
* @author web-wizard
*/
class TestDistributions extends haxe.unit.TestCase
{
public function new(){
super();
}
/**
*
*/
override function setup(){
TestSuite.initDB();
TestSuite.initDatas();
}
/**
* Check that we can add a distribution outside existing time range for a specific contract and place
* Check that we can't add a distribution overlapping with existing distribution for a specific contract and place
*/
function testOverlapping() {
var existingDistrib = TestSuite.DISTRIB_FRUITS_PLACE_DU_VILLAGE;
var e1 = null;
try {
var distrib1 = service.DistributionService.create(existingDistrib.contract,new Date(2018, 5, 1, 18, 0, 0),new Date(2018, 5, 1, 18, 30, 0),
existingDistrib.place.id,null,null,null,null,new Date(2018, 4, 1, 18, 0, 0),new Date(2018, 4, 30, 18, 30, 0));
}
catch(x:tink.core.Error) {
e1 = x;
}
assertEquals(e1, null);
//existingDistrib.date <= distrib2.date && existingDistrib.end >= distrib2.date
var e2 = null;
try {
var distrib2 = service.DistributionService.create(existingDistrib.contract,new Date(2017, 5, 1, 19, 30, 0),new Date(2017, 5, 1, 20, 30, 0),
existingDistrib.place.id,null,null,null,null,new Date(2017, 4, 1, 18, 0, 0),new Date(2017, 4, 30, 18, 30, 0));
}
catch(x:tink.core.Error) {
e2 = x;
}
assertEquals(e2.message, "There is already a distribution at this place overlapping with the time range you've selected.");
//existingDistrib.date <= distrib3.end && existingDistrib.end >= distrib3.end
var e3 = null;
try{
var distrib3 = service.DistributionService.create(existingDistrib.contract,new Date(2017, 5, 1, 17, 30, 0),new Date(2017, 5, 1, 19, 30, 0),
existingDistrib.place.id,null,null,null,null,new Date(2017, 4, 1, 18, 0, 0),new Date(2017, 4, 30, 18, 30, 0));
}
catch(x:tink.core.Error){
e3 = x;
}
assertEquals(e3.message, "There is already a distribution at this place overlapping with the time range you've selected.");
//existingDistrib.date >= distrib4.date && existingDistrib.end <= distrib4.end
var e4 = null;
try{
var distrib4 = service.DistributionService.create(existingDistrib.contract,new Date(2017, 5, 1, 17, 30, 0),new Date(2017, 5, 1, 21, 30, 0),
existingDistrib.place.id,null,null,null,null,new Date(2017, 4, 1, 18, 0, 0),new Date(2017, 4, 30, 18, 30, 0));
}
catch(x:tink.core.Error){
e4 = x;
}
assertEquals(e4.message, "There is already a distribution at this place overlapping with the time range you've selected.");
//existingDistrib.date > distrib5.end
var e5 = null;
try{
var distrib5 = service.DistributionService.create(existingDistrib.contract,new Date(2017, 5, 1, 17, 30, 0),new Date(2017, 5, 1, 18, 59, 0),
existingDistrib.place.id,null,null,null,null,new Date(2017, 4, 1, 18, 0, 0),new Date(2017, 4, 30, 18, 30, 0));
}
catch(x:tink.core.Error){
e5 = x;
}
assertEquals(e5, null);
//existingDistrib.date > distrib6.end
var e6 = null;
try{
var distrib6 = service.DistributionService.create(existingDistrib.contract,new Date(2017, 3, 1, 17, 30, 0),new Date(2017, 3, 1, 18, 59, 0),
existingDistrib.place.id,null,null,null,null,new Date(2017, 2, 1, 18, 0, 0),new Date(2017, 3, 30, 18, 30, 0));
}
catch(x:tink.core.Error){
e6 = x;
}
assertEquals(e6.message, "The distribution start date must be set after the orders end date.");
}
/**
* Check that we can add a distribution outside existing time range for a specific contract and place
* Check that we can't add a distribution overlapping with existing distribution for a specific contract and place
*/
function testEdit() {
//TO DO
assertEquals(true, true);
}
function testCreateCycle() {
TestSuite.CONTRAT_LEGUMES.startDate = new Date(2018, 0, 1, 0, 0, 0);
TestSuite.CONTRAT_LEGUMES.endDate = new Date(2019, 11, 31, 23, 59, 0);
TestSuite.CONTRAT_LEGUMES.update();
var weeklyDistribCycle = service.DistributionService.createCycle(TestSuite.CONTRAT_LEGUMES,Weekly,new Date(2018, 11, 24, 0, 0, 0),
new Date(2019, 0, 24, 0, 0, 0),new Date(2018, 5, 4, 13, 0, 0),new Date(2018, 5, 4, 14, 0, 0),10,2,
new Date(2018, 5, 4, 8, 0, 0),new Date(2018, 5, 4, 23, 0, 0),TestSuite.PLACE_DU_VILLAGE.id);
var weeklyDistribs = Lambda.array(db.Distribution.manager.search($distributionCycle == weeklyDistribCycle, false));
assertEquals(weeklyDistribs.length, 5);
assertEquals(weeklyDistribs[0].date.toString(), new Date(2018, 11, 24, 13, 0, 0).toString());
assertEquals(weeklyDistribs[0].end.toString(), new Date(2018, 11, 24, 14, 0, 0).toString());
assertEquals(weeklyDistribs[1].date.toString(), new Date(2018, 11, 31, 13, 0, 0).toString());
assertEquals(weeklyDistribs[1].end.toString(), new Date(2018, 11, 31, 14, 0, 0).toString());
assertEquals(weeklyDistribs[2].date.toString(), new Date(2019, 0, 7, 13, 0, 0).toString());
assertEquals(weeklyDistribs[2].end.toString(), new Date(2019, 0, 7, 14, 0, 0).toString());
assertEquals(weeklyDistribs[3].date.toString(), new Date(2019, 0, 14, 13, 0, 0).toString());
assertEquals(weeklyDistribs[3].end.toString(), new Date(2019, 0, 14, 14, 0, 0).toString());
assertEquals(weeklyDistribs[4].date.toString(), new Date(2019, 0, 21, 13, 0, 0).toString());
assertEquals(weeklyDistribs[4].end.toString(), new Date(2019, 0, 21, 14, 0, 0).toString());
service.DistributionService.deleteCycleDistribs(weeklyDistribCycle);
var monthlyDistribCycle = service.DistributionService.createCycle(TestSuite.CONTRAT_LEGUMES,Monthly,new Date(2018, 9, 30, 0, 0, 0),
new Date(2019, 2, 31, 0, 0, 0),new Date(2018, 5, 4, 13, 0, 0),new Date(2018, 5, 4, 14, 0, 0),10,2,
new Date(2018, 5, 4, 8, 0, 0),new Date(2018, 5, 4, 23, 0, 0),TestSuite.PLACE_DU_VILLAGE.id);
var monthlyDistribs = Lambda.array(db.Distribution.manager.search($distributionCycle == monthlyDistribCycle, false));
assertEquals(monthlyDistribs.length, 6);
assertEquals(monthlyDistribs[0].date.toString(), new Date(2018, 9, 30, 13, 0, 0).toString());
assertEquals(monthlyDistribs[0].end.toString(), new Date(2018, 9, 30, 14, 0, 0).toString());
assertEquals(monthlyDistribs[1].date.toString(), new Date(2018, 10, 27, 13, 0, 0).toString());
assertEquals(monthlyDistribs[1].end.toString(), new Date(2018, 10, 27, 14, 0, 0).toString());
assertEquals(monthlyDistribs[2].date.toString(), new Date(2018, 11, 25, 13, 0, 0).toString());
assertEquals(monthlyDistribs[2].end.toString(), new Date(2018, 11, 25, 14, 0, 0).toString());
assertEquals(monthlyDistribs[3].date.toString(), new Date(2019, 0, 29, 13, 0, 0).toString());
assertEquals(monthlyDistribs[3].end.toString(), new Date(2019, 0, 29, 14, 0, 0).toString());
assertEquals(monthlyDistribs[4].date.toString(), new Date(2019, 1, 26, 13, 0, 0).toString());
assertEquals(monthlyDistribs[4].end.toString(), new Date(2019, 1, 26, 14, 0, 0).toString());
assertEquals(monthlyDistribs[5].date.toString(), new Date(2019, 2, 26, 13, 0, 0).toString());
assertEquals(monthlyDistribs[5].end.toString(), new Date(2019, 2, 26, 14, 0, 0).toString());
service.DistributionService.deleteCycleDistribs(monthlyDistribCycle);
var biweeklyDistribCycle = service.DistributionService.createCycle(TestSuite.CONTRAT_LEGUMES,BiWeekly,new Date(2018, 9, 30, 0, 0, 0),
new Date(2019, 0, 31, 0, 0, 0),new Date(2018, 5, 4, 13, 0, 0),new Date(2018, 5, 4, 14, 0, 0),10,2,
new Date(2018, 5, 4, 8, 0, 0),new Date(2018, 5, 4, 23, 0, 0),TestSuite.PLACE_DU_VILLAGE.id);
var biweeklyDistribs = Lambda.array(db.Distribution.manager.search($distributionCycle == biweeklyDistribCycle, false));
assertEquals(biweeklyDistribs.length, 7);
assertEquals(biweeklyDistribs[0].date.toString(), new Date(2018, 9, 30, 13, 0, 0).toString());
assertEquals(biweeklyDistribs[0].end.toString(), new Date(2018, 9, 30, 14, 0, 0).toString());
assertEquals(biweeklyDistribs[1].date.toString(), new Date(2018, 10, 13, 13, 0, 0).toString());
assertEquals(biweeklyDistribs[1].end.toString(), new Date(2018, 10, 13, 14, 0, 0).toString());
assertEquals(biweeklyDistribs[2].date.toString(), new Date(2018, 10, 27, 13, 0, 0).toString());
assertEquals(biweeklyDistribs[2].end.toString(), new Date(2018, 10, 27, 14, 0, 0).toString());
assertEquals(biweeklyDistribs[3].date.toString(), new Date(2018, 11, 11, 13, 0, 0).toString());
assertEquals(biweeklyDistribs[3].end.toString(), new Date(2018, 11, 11, 14, 0, 0).toString());
assertEquals(biweeklyDistribs[4].date.toString(), new Date(2018, 11, 25, 13, 0, 0).toString());
assertEquals(biweeklyDistribs[4].end.toString(), new Date(2018, 11, 25, 14, 0, 0).toString());
assertEquals(biweeklyDistribs[5].date.toString(), new Date(2019, 0, 8, 13, 0, 0).toString());
assertEquals(biweeklyDistribs[5].end.toString(), new Date(2019, 0, 8, 14, 0, 0).toString());
assertEquals(biweeklyDistribs[6].date.toString(), new Date(2019, 0, 22, 13, 0, 0).toString());
assertEquals(biweeklyDistribs[6].end.toString(), new Date(2019, 0, 22, 14, 0, 0).toString());
service.DistributionService.deleteCycleDistribs(biweeklyDistribCycle);
var triweeklyDistribCycle = service.DistributionService.createCycle(TestSuite.CONTRAT_LEGUMES,TriWeekly,new Date(2018, 9, 30, 0, 0, 0),
new Date(2019, 0, 31, 0, 0, 0),new Date(2018, 5, 4, 13, 0, 0),new Date(2018, 5, 4, 14, 0, 0),10,2,
new Date(2018, 5, 4, 8, 0, 0),new Date(2018, 5, 4, 23, 0, 0),TestSuite.PLACE_DU_VILLAGE.id);
var triweeklyDistribs = Lambda.array(db.Distribution.manager.search($distributionCycle == triweeklyDistribCycle, false));
assertEquals(triweeklyDistribs.length, 5);
assertEquals(triweeklyDistribs[0].date.toString(), new Date(2018, 9, 30, 13, 0, 0).toString());
assertEquals(triweeklyDistribs[0].end.toString(), new Date(2018, 9, 30, 14, 0, 0).toString());
assertEquals(triweeklyDistribs[1].date.toString(), new Date(2018, 10, 20, 13, 0, 0).toString());
assertEquals(triweeklyDistribs[1].end.toString(), new Date(2018, 10, 20, 14, 0, 0).toString());
assertEquals(triweeklyDistribs[2].date.toString(), new Date(2018, 11, 11, 13, 0, 0).toString());
assertEquals(triweeklyDistribs[2].end.toString(), new Date(2018, 11, 11, 14, 0, 0).toString());
assertEquals(triweeklyDistribs[3].date.toString(), new Date(2019, 0, 1, 13, 0, 0).toString());
assertEquals(triweeklyDistribs[3].end.toString(), new Date(2019, 0, 1, 14, 0, 0).toString());
assertEquals(triweeklyDistribs[4].date.toString(), new Date(2019, 0, 22, 13, 0, 0).toString());
assertEquals(triweeklyDistribs[4].end.toString(), new Date(2019, 0, 22, 14, 0, 0).toString());
service.DistributionService.deleteCycleDistribs(triweeklyDistribCycle);
}
function testDelete() {
//A variable contract with a distribution that has orders
var ordersDistrib = TestSuite.DISTRIB_LEGUMES_RUE_SAUCISSE;
var ordersDistribId = ordersDistrib.id;
var chicken = TestSuite.CHICKEN;
var order = service.OrderService.make(TestSuite.FRANCOIS, 1, chicken, ordersDistrib.id);
var e = null;
try{
service.DistributionService.delete(ordersDistrib);
}
catch(x:tink.core.Error){
e = x;
}
assertEquals(e.message, "Deletion non possible: some orders are saved for this delivery.");
assertTrue(db.Distribution.manager.get(ordersDistribId) != null);
//A variable contract with a distribution that has no orders
var noOrdersDistrib = TestSuite.DISTRIB_FRUITS_PLACE_DU_VILLAGE;
var noOrdersDistribId = noOrdersDistrib.id;
var e = null;
try{
service.DistributionService.delete(noOrdersDistrib);
}
catch(x:tink.core.Error){
e = x;
}
assertEquals(e, null);
assertEquals(db.Distribution.manager.get(noOrdersDistribId), null);
//An Amap contract with a distribution that has orders
var amapDistrib = TestSuite.DISTRIB_CONTRAT_AMAP;
var amapDistribId = amapDistrib.id;
var panier = TestSuite.PANIER_AMAP_LEGUMES;
var amapOrder = service.OrderService.make(TestSuite.FRANCOIS, 1, panier, amapDistrib.id);
var e = null;
try{
service.DistributionService.delete(amapDistrib);
}
catch(x:tink.core.Error){
e = x;
}
assertEquals(e, null);
assertEquals(db.Distribution.manager.get(amapDistribId), null);
}
function testUpdateAmapContractOperations(){
var t = sugoi.i18n.Locale.texts;
//Take an amap contract with payments enabled
//Take 2 users and make orders for each
var amapDistrib = TestSuite.DISTRIB_CONTRAT_AMAP;
var contract = amapDistrib.contract;
var panier = TestSuite.PANIER_AMAP_LEGUMES;
var francoisOrder = service.OrderService.make(TestSuite.FRANCOIS, 1, panier, amapDistrib.id);
db.Operation.onOrderConfirm([francoisOrder]);
var sebOrder = service.OrderService.make(TestSuite.SEB, 3, panier, amapDistrib.id);
db.Operation.onOrderConfirm([sebOrder]);
//Check initial operation names and amounts
var francoisOperation = db.Operation.findCOrderTransactionFor(contract, TestSuite.FRANCOIS);
assertEquals(francoisOperation.name, "Contrat AMAP Légumes (La ferme de la Galinette) 1 deliveries");
assertEquals(francoisOperation.amount, -13);
var sebOperation = db.Operation.findCOrderTransactionFor(contract, TestSuite.SEB);
assertEquals(sebOperation.name, "Contrat AMAP Légumes (La ferme de la Galinette) 1 deliveries");
assertEquals(sebOperation.amount, -39);
//Add a distrib
var distrib = null;
var e = null;
try{
distrib = service.DistributionService.create(contract,new Date(2018, 5, 1, 18, 0, 0),new Date(2018, 5, 1, 18, 30, 0),
amapDistrib.place.id,null,null,null,null,new Date(2018, 4, 1, 18, 0, 0),new Date(2018, 4, 30, 18, 30, 0));
}
catch(x:tink.core.Error){
e = x;
}
//Check names and amounts are modified accordingly
assertEquals(francoisOperation.name, "Contrat AMAP Légumes (La ferme de la Galinette) 2 deliveries");
assertEquals(francoisOperation.amount, -26);
var sebOperation = db.Operation.findCOrderTransactionFor(contract, TestSuite.SEB);
assertEquals(sebOperation.name, "Contrat AMAP Légumes (La ferme de la Galinette) 2 deliveries");
assertEquals(sebOperation.amount, -78);
//Add a distrib cycle
var weeklyDistribCycle = service.DistributionService.createCycle(contract,Weekly,new Date(2018, 11, 24, 0, 0, 0),
new Date(2019, 0, 24, 0, 0, 0),new Date(2018, 5, 4, 13, 0, 0),new Date(2018, 5, 4, 14, 0, 0),10,2,
new Date(2018, 5, 4, 8, 0, 0),new Date(2018, 5, 4, 23, 0, 0),TestSuite.PLACE_DU_VILLAGE.id);
//Check names and amounts are modified accordingly
assertEquals(francoisOperation.name, "Contrat AMAP Légumes (La ferme de la Galinette) 7 deliveries");
assertEquals(francoisOperation.amount, -91);
var sebOperation = db.Operation.findCOrderTransactionFor(contract, TestSuite.SEB);
assertEquals(sebOperation.name, "Contrat AMAP Légumes (La ferme de la Galinette) 7 deliveries");
assertEquals(sebOperation.amount, -273);
//Delete the distrib cycle
service.DistributionService.deleteCycleDistribs(weeklyDistribCycle);
//Check names and amounts are modified accordingly
assertEquals(francoisOperation.name, "Contrat AMAP Légumes (La ferme de la Galinette) 2 deliveries");
assertEquals(francoisOperation.amount, -26);
var sebOperation = db.Operation.findCOrderTransactionFor(contract, TestSuite.SEB);
assertEquals(sebOperation.name, "Contrat AMAP Légumes (La ferme de la Galinette) 2 deliveries");
assertEquals(sebOperation.amount, -78);
}
}
+473
View File
@@ -0,0 +1,473 @@
package test;
import Common;
import service.OrderService;
/**
* Test order making, updating and deleting
*
* @author fbarbut
*/
class TestOrders extends haxe.unit.TestCase
{
public function new(){
super();
}
var c : db.Contract;
var p : db.Product;
var bob : db.User;
/**
* get a contract + a user + a product + empty orders
*/
override function setup(){
TestSuite.initDB();
TestSuite.initDatas();
db.Basket.emptyCache();
c = TestSuite.DISTRIB_FRUITS_PLACE_DU_VILLAGE.contract;
p = TestSuite.STRAWBERRIES;
p.lock();
p.stock = 8;
p.update();
bob = db.User.manager.get(1);
}
/**
Test Basket creation and numbering
**/
public function testBasket(){
var d = TestSuite.DISTRIB_FRUITS_PLACE_DU_VILLAGE;
var o = OrderService.make(TestSuite.FRANCOIS, 3, TestSuite.STRAWBERRIES, d.id);
assertEquals(1, o.basket.num);
var o = OrderService.make(TestSuite.SEB, 1, TestSuite.STRAWBERRIES, d.id);
assertEquals(2, o.basket.num);
//order again, should keep existing basket number
var o = OrderService.make(TestSuite.FRANCOIS, 1, TestSuite.APPLES, d.id);
assertEquals(1, o.basket.num);
//check bug of 2018-07 : changing the date and place of the distribution leads to lost basket (because basket were indexed on user-date-place)
d.lock();
d.date = new Date(2028,1,1,0,0,0);
var place = new db.Place();
place.name = "Chez Momo";
place.zipCode = "54";
place.amap = d.contract.amap;
place.insert();
d.place = place;
d.update();
//Seb's basket is still 2
var basket = db.Basket.get(TestSuite.SEB,place,d.date);
assertEquals(2, basket.num);
var o = OrderService.make(TestSuite.SEB, 1, TestSuite.APPLES, d.id);
assertEquals(2, o.basket.num);
var o2 = OrderService.edit(o,5,true,null,false);
assertEquals(2, o2.basket.num);
//order to a different distrib in same contract should start a new numbering
var d2 = service.DistributionService.create(d.contract,new Date(2026,6,6,0,0,0),new Date(2026,6,6,1,0,0),place.id,null,null,null,null,new Date(2026,6,4,0,0,0),new Date(2026,6,5,0,0,0));
var o = OrderService.make(TestSuite.SEB, 12, TestSuite.APPLES, d2.id);
assertEquals(1, o.basket.num);
}
/**
* make orders & stock management
*/
public function testStocks(){
var stock = p.stock;
assertTrue(c.type == db.Contract.TYPE_VARORDER);
assertTrue(c.flags.has(db.Contract.ContractFlags.StockManagement));
assertTrue(stock == 8);
//bob orders 3 strawberries, stock fall to 2
//order is update to 6 berries
App.current.eventDispatcher.addOnce(function(e:Event){
switch(e){
case StockMove(e):
assertTrue(e.move==-3);
assertTrue(e.product==p);
default:
}
});
var o = OrderService.make(bob, 3, p, TestSuite.DISTRIB_FRUITS_PLACE_DU_VILLAGE.id);
assertTrue(p.stock == 5);
assertTrue(o.quantity == 3);
//bob orders 6 more. stock fall to 0, order is reduced to 5
//quantity is not 9 but 8
App.current.eventDispatcher.addOnce(function(e:Event){
switch(e){
case StockMove(e):
assertTrue(e.move==-5);
assertTrue(e.product==p);
default:
}
});
var o = OrderService.make(bob, 6, p, TestSuite.DISTRIB_FRUITS_PLACE_DU_VILLAGE.id);
assertTrue(p.stock == 0);
assertTrue(o.quantity == 8);
//bob orders again but cant order anything
var o = OrderService.make(bob, 3, p, TestSuite.DISTRIB_FRUITS_PLACE_DU_VILLAGE.id);
assertTrue(p.stock == 0);
assertTrue(o.quantity == 8);
}
/**
* test edit orders and stock management
*/
function testOrderEdit(){
var o = db.UserContract.manager.select( $user == bob && $product == p, true);
//no order, stock at 8
assertEquals(p.stock , 8);
assertEquals(o , null);
//bob orders 3 strawberries
var o = OrderService.make(bob, 3, p , TestSuite.DISTRIB_FRUITS_PLACE_DU_VILLAGE.id);
assertEquals(o.product.name, p.name);
assertEquals(o.quantity, 3);
assertEquals(p.stock , 5);
//order edit, order 6 berries
App.current.eventDispatcher.addOnce(function(e:Event){
switch(e){
case StockMove(e):
assertTrue(e.move==-3);
assertTrue(e.product==p);
default:
}
});
var o = OrderService.edit(o, 6);
assertTrue(p.stock == 2);
assertTrue(o.quantity == 6);
//order edit, order 9 berries. ( 3 more, but stock fall to 0, reduced to 2 )
App.current.eventDispatcher.addOnce(function(e:Event){
switch(e){
case StockMove(e):
assertEquals( -2.0 , e.move );
assertEquals( p , e.product);
default:
}
});
var o = OrderService.edit(o, 9);
assertEquals(0.0 , p.stock);
assertEquals(8.0 , o.quantity);
//order more, but stock at 0
var o = OrderService.edit(o, 12);
assertEquals(0.0 , p.stock);
assertEquals(8.0 , o.quantity);
//order less
var o = OrderService.edit(o, 6);
assertEquals(2.0 , p.stock);
assertEquals(6.0 , o.quantity);
//floatQt : ordering float quantities should throw an exception
var err = null;
try{
var o = OrderService.edit(o, 6.4);
}catch(e:tink.core.Error){
err = e.message;
}
assertTrue( err!=null );
}
/**
* test orders with multiweight product
*/
function testOrderWithMultiWeightProduct(){
var chicken = TestSuite.CHICKEN;
var distrib = db.Distribution.manager.select($contract == chicken.contract, false);
var order = OrderService.make(bob, 1, chicken, distrib.id);
assertEquals(1.0, order.quantity);
assertEquals(chicken.id, order.product.id);
assertEquals(chicken.price, order.productPrice);
//order 2 more, should not aggregate because multiWeight is true
var order2 = OrderService.make(bob, 2, chicken, distrib.id);
assertTrue(order2.id != order.id);
//we should get 3 different orders
var orders = distrib.getOrders();
//trace(OrderService.prepare(orders));
assertEquals(3, orders.length);
for ( o in orders){
assertEquals(o.user.id, bob.id);
assertEquals(o.product.id, chicken.id);
assertEquals(1.0, o.quantity);
}
}
/**
* @author fbarbut
* @date 2018-01-26
* order a product, edit and set qt to zero, order again.
* the same record should be re-used ( if not multiweight )
*/
function testMakeOrderAndZeroQuantity(){
var fraises = TestSuite.STRAWBERRIES;
var distrib = db.Distribution.manager.select($contract == fraises.contract, false);
var order = OrderService.make(bob, 1, fraises, distrib.id);
order = OrderService.edit(order, 0);
assertEquals(0.0, order.quantity);
var order2 = OrderService.make(bob, 1, fraises, distrib.id);
var bobOrders = [];
for ( o in distrib.getOrders()) if (o.user.id == bob.id) bobOrders.push(o);
assertFalse(fraises.multiWeight);
assertEquals(1, bobOrders.length);
assertEquals(1.0, bobOrders[0].quantity);
}
/**
* Test order deletion and operation deletion in various contexts
@author jbarbic
*/
function testDelete(){
var t = sugoi.i18n.Locale.texts;
//[Test case] Should throw an error when trying to delete order and that the quantity is not zero
var amapDistrib = TestSuite.DISTRIB_CONTRAT_AMAP;
var amapContract = amapDistrib.contract;
var order = OrderService.make(TestSuite.FRANCOIS, 1, TestSuite.PANIER_AMAP_LEGUMES, amapDistrib.id);
var orderId = order.id;
db.Operation.onOrderConfirm([order]);
var e1 = null;
try {
service.OrderService.delete(order);
}
catch(x:tink.core.Error){
e1 = x;
}
assertEquals(e1.message, "Deletion not possible: quantity is not zero.");
assertTrue(db.UserContract.manager.get(orderId) != null);
//[Test case] Amap contract and quantity zero with payments disabled
//Check that order is deleted
order = OrderService.edit(order, 0);
var e2 = null;
try {
OrderService.delete(order);
}
catch(x:tink.core.Error){
e2 = x;
}
assertEquals(e2, null);
assertEquals(db.UserContract.manager.get(orderId), null);
//[Test case] Amap contract and quantity zero with payments enabled and 2 orders
//Check that first order is deleted but operation amount is at 0
//Check that operation is deleted only at the second order deletion
var order1 = OrderService.make(TestSuite.FRANCOIS, 1, TestSuite.PANIER_AMAP_LEGUMES, amapDistrib.id);
db.Operation.onOrderConfirm([order1]);
var order1Id = order1.id;
order1 = OrderService.edit(order1, 0);
db.Operation.onOrderConfirm([order1]);
var order2 = OrderService.make(TestSuite.FRANCOIS, 1, TestSuite.PANIER_AMAP_LEGUMES, amapDistrib.id);
db.Operation.onOrderConfirm([order2]);
var order2Id = order2.id;
order2 = OrderService.edit(order2, 0);
db.Operation.onOrderConfirm([order2]);
var operation = db.Operation.findCOrderTransactionFor(amapContract, TestSuite.FRANCOIS);
var operationId = operation.id;
var e3 = null;
try {
service.OrderService.delete(order1);
}
catch(x:tink.core.Error){
e3 = x;
}
assertEquals(e3, null);
assertEquals(db.UserContract.manager.get(order1Id), null);
assertTrue(db.Operation.manager.get(operationId) != null);
assertEquals(operation.name, "Contrat AMAP Légumes (La ferme de la Galinette) 1 deliveries");
assertEquals(operation.amount, 0);
var e4 = null;
try {
service.OrderService.delete(order2);
}
catch(x:tink.core.Error){
e4 = x;
}
assertEquals(null, e4);
assertEquals(null, db.UserContract.manager.get(order2Id));
assertEquals(null, db.Operation.manager.get(operationId));
//[Test case] Var Order contract and quantity zero with payments disabled
//Check that order is deleted
var variableDistrib = TestSuite.DISTRIB_FRUITS_PLACE_DU_VILLAGE;
var g = variableDistrib.contract.amap;
g.lock();
g.flags.unset(HasPayments);
g.update();
var order = OrderService.make(TestSuite.FRANCOIS, 2, TestSuite.STRAWBERRIES, variableDistrib.id);
var orderId = order.id;
db.Operation.onOrderConfirm([order]);
order = OrderService.edit(order, 0);
db.Operation.onOrderConfirm([order]);
var e1 = null;
try {
service.OrderService.delete(order);
}
catch(x:tink.core.Error){
e1 = x;
}
assertEquals(false,variableDistrib.contract.amap.hasPayments() );
assertEquals(null, e1);
assertEquals(null, db.UserContract.manager.get(orderId));
//[Test case] Var Order contract and quantity zero with payments enabled and 2 orders
//Check that first order is deleted
//Check that operation is deleted only at the second order deletion
variableDistrib = TestSuite.DISTRIB_FRUITS_PLACE_DU_VILLAGE;
var variableContract = variableDistrib.contract;
var g = variableDistrib.contract.amap;
g.lock();
g.flags.set(HasPayments);
g.update();
assertTrue(variableContract.amap.hasPayments());
var order1 = OrderService.make(TestSuite.FRANCOIS, 2, TestSuite.STRAWBERRIES, variableDistrib.id);
db.Operation.onOrderConfirm([order1]);
var order1Id = order1.id;
var order2 = OrderService.make(TestSuite.FRANCOIS, 3, TestSuite.APPLES, variableDistrib.id);
db.Operation.onOrderConfirm([order2]);
var order2Id = order2.id;
order1 = OrderService.edit(order1, 0);
db.Operation.onOrderConfirm([order1]);
order2 = OrderService.edit(order2, 0);
db.Operation.onOrderConfirm([order2]);
assertEquals(2, variableContract.getUserOrders(TestSuite.FRANCOIS,variableDistrib).length); //François has 2 orders
var basket = db.Basket.get(TestSuite.FRANCOIS,variableDistrib.place,variableDistrib.date);
assertEquals(2, basket.getOrders().length);
var operation1 = db.Operation.findVOrderTransactionFor(order1.distribution.getKey(), TestSuite.FRANCOIS, variableContract.amap);
var operation1Id = operation1.id;
var operation2 = db.Operation.findVOrderTransactionFor(order2.distribution.getKey(), TestSuite.FRANCOIS, variableContract.amap);
var operation2Id = operation2.id;
assertEquals(operation1Id,operation2Id);
var e2 = null;
try {
//delete strawberries order
service.OrderService.delete(order1);
}
catch(x:tink.core.Error){
e2 = x;
}
assertEquals(null, e2);
assertEquals(null, db.UserContract.manager.get(order1Id) ); //order 1 is deleted
assertTrue( db.Operation.manager.get(operation1Id) != null); //operation should be here
assertEquals(0.0,operation1.amount);
assertEquals(1 , basket.getOrders().length);
var e3 = null;
try {
//delete apple order
service.OrderService.delete(order2);
}
catch(x:tink.core.Error){
e3 = x;
}
assertEquals(null, e3);
assertEquals(null, db.UserContract.manager.get(order2Id)); //order 2 is deleted
assertEquals(null, db.Operation.manager.get(operation1Id)); //operation should be deleted
assertEquals(0 , basket.getOrders().length);
//[Test case] 2 VarOrderContracts and quantity zero with payments enabled and 1 order each
//Check that first order is deleted but operation amount is at 0
//Check that operation is deleted only at the second order deletion
var variableDistrib1 = TestSuite.DISTRIB_LEGUMES_RUE_SAUCISSE;
var order1 = OrderService.make(TestSuite.FRANCOIS, 2, TestSuite.COURGETTES, variableDistrib1.id);
assertTrue(order1.basket!=null);
// trace("WE GOT A BASKET "+order1.basket.id);
// trace("... THEN RE-GET BASKET user "+TestSuite.FRANCOIS.id+" place "+variableDistrib1.place.id+" date "+variableDistrib1.date);
var basket = db.Basket.get(TestSuite.FRANCOIS,variableDistrib1.place,variableDistrib1.date);
assertTrue(basket!=null);
assertEquals(1, basket.getOrders().length);
db.Operation.onOrderConfirm([order1]);
var order1Id = order1.id;
var variableDistrib2 = TestSuite.DISTRIB_PATISSERIES;
var order2 = OrderService.make(TestSuite.FRANCOIS, 3, TestSuite.FLAN, variableDistrib2.id);
db.Operation.onOrderConfirm([order2]);
var order2Id = order2.id;
order1 = OrderService.edit(order1, 0);
db.Operation.onOrderConfirm([order1]);
order2 = OrderService.edit(order2, 0);
db.Operation.onOrderConfirm([order2]);
//check basket
var basket = db.Basket.get(TestSuite.FRANCOIS,variableDistrib1.place,variableDistrib1.date);
assertEquals(2, basket.getOrders().length);
var operation = db.Operation.findVOrderTransactionFor(order1.distribution.getKey(), TestSuite.FRANCOIS, variableDistrib1.contract.amap);
var operationId = operation.id;
var e4 = null;
try {
service.OrderService.delete(order1);
}
catch(x:tink.core.Error){
e4 = x;
}
assertEquals(e4, null);
assertEquals(db.UserContract.manager.get(order1Id), null);
assertTrue(db.Operation.manager.get(operationId) != null); //op should still be here
assertEquals(0.0,operation.amount);//...with amount 0
var e5 = null;
try {
service.OrderService.delete(order2);
}
catch(x:tink.core.Error){
e5 = x;
}
assertEquals(e5, null);
assertEquals(db.UserContract.manager.get(order2Id), null);
assertEquals(null, db.Operation.manager.get(operationId), null); //op should have been deleted
}
}
+64
View File
@@ -0,0 +1,64 @@
package test;
/**
* Test payments
*
* @author web-wizard
*/
class TestPayments extends haxe.unit.TestCase
{
public function new(){
super();
}
override function setup(){
TestSuite.initDB();
TestSuite.initDatas();
db.Basket.emptyCache();
}
function testValidateDistribution() {
//Take a contract with payments enabled
//Take 2 users and make orders for each
var distrib = TestSuite.DISTRIB_LEGUMES_RUE_SAUCISSE;
var contract = distrib.contract;
var product = TestSuite.COURGETTES;
var francoisOrder = service.OrderService.make(TestSuite.FRANCOIS, 1, product, distrib.id);
var francoisOrderOperation = db.Operation.onOrderConfirm([francoisOrder]);
var sebOrder = service.OrderService.make(TestSuite.SEB, 3, product, distrib.id);
var sebOrderOperation = db.Operation.onOrderConfirm([sebOrder]);
//They both pay by check
var francoisPayment = db.Operation.makePaymentOperation(TestSuite.FRANCOIS,contract.amap, payment.Check.TYPE, product.price, "Payment by check", francoisOrderOperation[0]);
var sebPayment = db.Operation.makePaymentOperation(TestSuite.SEB,contract.amap, payment.Check.TYPE, 3 * product.price, "Payment by check", sebOrderOperation[0] );
//Autovalidate this old distrib and check that all the payments are validated
service.PaymentService.validateDistribution(distrib);
//distrib should be validated
assertTrue(contract.amap.hasPayments());
assertEquals(true, distrib.validated);
//orders should be marked as paid
assertEquals(true, francoisOrder.paid);
assertEquals(true, sebOrder.paid);
//order operation is not pending
var francoisOperation = db.Operation.findVOrderTransactionFor(francoisOrder.distribution.getKey(), TestSuite.FRANCOIS, contract.amap, false);
var sebOperation = db.Operation.findVOrderTransactionFor(sebOrder.distribution.getKey(), TestSuite.SEB, contract.amap, false);
assertEquals(francoisOperation.pending, false);
assertEquals(sebOperation.pending, false);
//payment operation is not pending
assertEquals(francoisPayment.pending, false);
assertEquals(sebPayment.pending, false);
//basket are validated
var b = db.Basket.get(TestSuite.SEB,distrib.place,distrib.date);
assertEquals(true, b.isValidated());
var b = db.Basket.get(TestSuite.FRANCOIS,distrib.place,distrib.date);
assertEquals(true, b.isValidated());
}
}
+171
View File
@@ -0,0 +1,171 @@
package test;
import Common;
import test.TestSuite;
import service.ReportService;
import service.OrderService;
/**
* Test order reports
*
* @author fbarbut
*/
class TestReports extends haxe.unit.TestCase
{
public function new(){
super();
}
override function setup(){
TestSuite.initDB();
TestSuite.initDatas();
}
function testOrdersByProduct(){
//record orders
var seb = TestSuite.SEB;
var francois = TestSuite.FRANCOIS;
var julie = TestSuite.JULIE;
//distrib de légumes
var d = TestSuite.DISTRIB_LEGUMES_RUE_SAUCISSE;
var carrots = TestSuite.CARROTS;
var courgettes = TestSuite.COURGETTES;
var poulet = TestSuite.CHICKEN;
OrderService.make(seb,4,courgettes,d.id);
OrderService.make(seb,1,poulet,d.id);
OrderService.make(francois,6,courgettes,d.id);
OrderService.make(francois,2,poulet,d.id);
OrderService.make(francois,3,carrots,d.id);
OrderService.make(julie,8,carrots,d.id);
OrderService.make(julie,3,poulet,d.id);
//record orders on ANOTHER distrib
var d2 = service.DistributionService.create(
d.contract,
new Date(2018,2,12,0,0,0),
new Date(2018,2,12,0,3,0),
d.contract.amap.getPlaces().first().id,
null,null,null,null,
new Date(2018,2,8,0,0,0),
new Date(2018,2,11,0,0,0)
);
OrderService.make(julie,6,carrots,d2.id);
OrderService.make(julie,1,poulet,d2.id);
var orders = ReportService.getOrdersByProduct(d);
//courgettes x 10
var courgettesOrder = Lambda.find(orders, function(o) return o.pid==courgettes.id);
assertEquals( 10.0 , courgettesOrder.quantity );
assertEquals( 35.0 , courgettesOrder.totalTTC );
assertEquals( 33.18 , tools.FloatTool.clean(courgettesOrder.totalHT) );
//the report stays the same, even if the product has a new price.
courgettes.lock();
courgettes.price+=4;
courgettes.update();
var orders = ReportService.getOrdersByProduct(d);
var courgettesOrder = Lambda.find(orders, function(o) return o.pid==courgettes.id);
assertEquals( 10.0 , courgettesOrder.quantity );
assertEquals( 35.0 , courgettesOrder.totalTTC );
assertEquals( 33.18 , tools.FloatTool.clean(courgettesOrder.totalHT) );
}
/**
* run once at the beginning
*/
/*override function setup(){
sys.db.Manager.cnx.request("TRUNCATE TABLE UserContract;");
var bubar = db.User.manager.get(1);
var seb = db.User.manager.get(2);
//fruits from group 1
var fraises = db.Product.manager.get(2);
fraises.stock = 30;
var pommes = db.Product.manager.get(3);
var distrib = fraises.contract.getDistribs().first();
db.UserContract.make(bubar, 4, fraises, distrib.id);
db.UserContract.make(seb, 2, pommes, distrib.id);
//vegetables from group 2
var courgettes = db.Product.manager.get(4);
var carottes = db.Product.manager.get(5);
var distrib = courgettes.contract.getDistribs().first();
db.UserContract.make(bubar, 1, carottes ,distrib.id);
db.UserContract.make(seb, 5, courgettes ,distrib.id);
}*/
/**
* test a simple report with just a time frame
*/
/*public function testTimeFrameReport(){
var fraises = db.Product.manager.get(2);
var pommes = db.Product.manager.get(3);
var courgettes = db.Product.manager.get(4);
var carottes = db.Product.manager.get(5);
//check we got the right products
assertEquals("Fraises",fraises.name);
assertEquals("Pommes",pommes.name);
assertEquals("Courgettes",courgettes.name);
assertEquals("Carottes", carottes.name);
var options = { startDate:new Date(2017, 5, 1, 0, 0, 0), endDate:new Date(2017, 5, 31, 0, 0, 0), groups:[], contracts:[] };
var rep = new pro.OrderReport(options);
var data = rep.byProduct();
assertEquals(4,data.length); //should be the 4 products
for ( d in data){
switch(d.pname){
case "Fraises": assertEquals(d.qt, 4);
case "Pommes": assertEquals(d.qt, 2);
case "Carottes": assertEquals(d.qt, 1);
case "Courgettes": assertEquals(d.qt, 5);
}
}
}*/
/**
* test a report with time frame + group
*/
/*public function testGroupReport(){
var options = { startDate:new Date(2017, 5, 1, 0, 0, 0), endDate:new Date(2017, 5, 31, 0, 0, 0), groups:[1], contracts:[] };
var rep = new pro.OrderReport(options);
var data = rep.byProduct();
assertEquals(2,data.length); //should be the 2 products
for ( d in data){
switch(d.pname){
case "Fraises": assertEquals(d.qt, 4);
case "Pommes": assertEquals(d.qt, 2);
}
}
}*/
}
+413
View File
@@ -0,0 +1,413 @@
package test;
import Common;
/**
* CAGETTE.NET TEST SUITE
* @author fbarbut
*/
class TestSuite
{
static function main() {
connectDb();
var r = new haxe.unit.TestRunner();
//Cagette core tests
r.add(new test.TestUser());
r.add(new test.TestOrders());
r.add(new test.TestTools());
r.add(new test.TestDistributions());
r.add(new test.TestPayments());
r.add(new test.TestReports());
#if plugins
//Cagette-pro tests, keep in this order
r.add(new pro.test.TestProductService());
r.add(new pro.test.TestRemoteCatalog());
r.add(new pro.test.TestDistribService());
r.add(new pro.test.TestReports());
//wholesale-order tests
r.add(new who.test.TestWho());
#end
r.run();
}
static function connectDb() {
var dbstr = Sys.args()[0];
var dbreg = ~/([^:]+):\/\/([^:]+):([^@]*?)@([^:]+)(:[0-9]+)?\/(.*?)$/;
if( !dbreg.match(dbstr) )
throw "Configuration requires a valid database attribute, format is : mysql://user:password@host:port/dbname";
var port = dbreg.matched(5);
var dbparams = {
user:dbreg.matched(2),
pass:dbreg.matched(3),
host:dbreg.matched(4),
port:port == null ? 3306 : Std.parseInt(port.substr(1)),
database:dbreg.matched(6),
socket:null
};
sys.db.Manager.cnx = sys.db.Mysql.connect(dbparams);
sys.db.Manager.initialize();
}
public static function initDB(){
//NUKE EVERYTHING BWAAAAH !!
sys.db.Manager.cleanup(); //cleanup cache objects
sql("DROP DATABASE tests;");
sql("CREATE DATABASE tests;");
sql("USE tests;");
var tables : Array<Dynamic> = [
//cagette
db.TxpProduct.manager,
db.TxpCategory.manager,
db.TxpSubCategory.manager,
db.Category.manager,
db.CategoryGroup.manager,
db.ProductCategory.manager,
db.Basket.manager,
db.UserContract.manager,
db.UserAmap.manager,
db.Operation.manager,
db.User.manager,
db.Amap.manager,
db.Contract.manager,
db.Product.manager,
db.Vendor.manager,
db.Place.manager,
db.Distribution.manager,
db.DistributionCycle.manager,
//sugoi tables
sugoi.db.Cache.manager,
sugoi.db.Error.manager,
sugoi.db.File.manager,
sugoi.db.Session.manager,
sugoi.db.Variable.manager,
];
for(t in tables) createTable(t);
#if plugins
//add Cpro datas : we need those tables even in cagette core tests
pro.test.ProTestSuite.initDB();
pro.test.ProTestSuite.initDatas();
#end
}
public static function createTable( m ){
if ( sys.db.TableCreate.exists(m) ){
drop(m);
}
// Sys.println("Creating table "+ m.dbInfos().name);
sys.db.TableCreate.create(m);
}
public static function truncate(m){
sql("TRUNCATE TABLE "+m.dbInfos().name+";");
}
public static function drop(m){
sql("DROP TABLE "+m.dbInfos().name+";");
}
public static function sql(sql){
return sys.db.Manager.cnx.request(sql);
}
//shortcut to datas
public static var FRANCOIS:db.User = null;
public static var SEB:db.User = null;
public static var JULIE:db.User = null;
public static var CHICKEN:db.Product = null;
public static var STRAWBERRIES:db.Product = null;
public static var APPLES:db.Product = null;
public static var AMAP_DU_JARDIN:db.Amap = null;
public static var LOCAVORES:db.Amap = null;
public static var PANIER_AMAP_LEGUMES:db.Product = null;
public static var DISTRIB_CONTRAT_AMAP:db.Distribution = null;
public static var DISTRIB_FRUITS_PLACE_DU_VILLAGE:db.Distribution = null;
public static var DISTRIB_LEGUMES_RUE_SAUCISSE:db.Distribution = null;
public static var CONTRAT_LEGUMES:db.Contract = null;
public static var PLACE_DU_VILLAGE:db.Place = null;
public static var COURGETTES:db.Product = null;
public static var CARROTS:db.Product = null;
public static var FLAN:db.Product = null;
public static var CROISSANT:db.Product = null;
public static var DISTRIB_PATISSERIES:db.Distribution = null;
public static function initDatas(){
//USERS
var f = new db.User();
f.firstName = "François";
f.lastName = "B";
f.email = "francois@alilo.fr";
f.insert();
FRANCOIS = f;
var u = new db.User();
u.firstName = "Seb";
u.lastName = "Z";
u.email = "sebastien@alilo.fr";
u.insert();
SEB = u;
var u = new db.User();
u.firstName = "Julie";
u.lastName = "B";
u.email = "julie@alilo.fr";
u.insert();
JULIE = u;
initApp(u);
//GROUP "AMAP du Jardin public"
var a = new db.Amap();
a.name = "AMAP du Jardin public";
a.contact = f;
a.flags.set(db.Amap.AmapFlags.HasPayments);
a.insert();
AMAP_DU_JARDIN = a;
var place = new db.Place();
place.name = "Place du village";
place.zipCode = "00000";
place.city = "St Martin";
place.amap = a;
place.insert();
PLACE_DU_VILLAGE = place;
//VENDOR "Ferme de la galinette"
var v = new db.Vendor();
v.name = "La ferme de la Galinette";
v.email = "galinette@gmail.com";
v.zipCode = "00000";
v.city = "Bourligheim";
v.insert();
var c = new db.Contract();
c.name = "Contrat AMAP Légumes";
c.startDate = new Date(2017, 1, 1, 0, 0, 0);
c.endDate = new Date(2030, 12, 31, 23, 59, 0);
c.vendor = v;
c.amap = a;
c.type = db.Contract.TYPE_CONSTORDERS;
c.insert();
var p = new db.Product();
p.name = "Panier Légumes";
p.price = 13;
p.contract = c;
p.insert();
PANIER_AMAP_LEGUMES = p;
var d = new db.Distribution();
d.date = new Date(2017, 5, 1, 19, 0, 0);
d.end = new Date(2017, 5, 1, 20, 0, 0);
d.orderStartDate = new Date(2017, 4, 1, 20, 0, 0);
d.orderEndDate = new Date(2017, 4, 30, 20, 0, 0);
d.contract = c;
d.place = place;
d.insert();
DISTRIB_CONTRAT_AMAP = d;
//varying contract for strawberries with stock mgmt
var c = new db.Contract();
c.name = "Commande fruits";
c.vendor = v;
c.startDate = new Date(2017, 1, 1, 0, 0, 0);
c.endDate = new Date(2030, 12, 31, 23, 59, 0);
c.flags.set(db.Contract.ContractFlags.StockManagement);
c.type = db.Contract.TYPE_VARORDER;
c.amap = a;
c.insert();
var p = new db.Product();
p.name = "Fraises";
p.qt = 1;
p.unitType = Common.Unit.Kilogram;
p.price = 10;
p.organic = true;
p.contract = c;
p.stock = 8;
p.insert();
STRAWBERRIES = p;
var p = new db.Product();
p.name = "Pommes";
p.qt = 1;
p.unitType = Common.Unit.Kilogram;
p.price = 6;
p.organic = true;
p.contract = c;
p.stock = 12;
p.insert();
APPLES = p;
var d = new db.Distribution();
d.date = new Date(2017, 5, 1, 19, 0, 0);
d.end = new Date(2017, 5, 1, 20, 0, 0);
d.orderStartDate = new Date(2017, 4, 1, 20, 0, 0);
d.orderEndDate = new Date(2017, 4, 30, 20, 0, 0);
d.contract = c;
d.place = place;
d.insert();
DISTRIB_FRUITS_PLACE_DU_VILLAGE = d;
//second group
var a = new db.Amap();
a.name = "Les Locavores de la Rue Saucisse";
a.contact = f;
a.flags.set(db.Amap.AmapFlags.HasPayments);
a.insert();
LOCAVORES = a;
var place = new db.Place();
place.name = "Rue Saucisse";
place.zipCode = "00000";
place.city = "St Martin";
place.amap = a;
place.insert();
var v = new db.Vendor();
v.name = "La ferme de la courgette enragée";
v.email = "courgette@gmail.com";
v.zipCode = "00000";
v.city = "Bourligeac";
v.insert();
var c = new db.Contract();
c.name = "Commande Legumes";
c.startDate = new Date(2017, 1, 1, 0, 0, 0);
c.endDate = new Date(2030, 12, 31, 23, 59, 0);
c.vendor = v;
c.amap = a;
c.type = db.Contract.TYPE_VARORDER;
c.insert();
CONTRAT_LEGUMES = c;
var p = new db.Product();
p.name = "Courgettes";
p.qt = 1;
p.unitType = Common.Unit.Kilogram;
p.price = 3.5;
p.vat = 5.5;
p.organic = true;
p.contract = c;
p.insert();
COURGETTES = p;
var p = new db.Product();
p.name = "Carottes";
p.qt = 1;
p.unitType = Common.Unit.Kilogram;
p.price = 2.8;
p.vat = 5.5;
p.contract = c;
p.insert();
CARROTS = p;
var p = new db.Product();
p.name = "Poulet";
p.qt = 1.5;
p.unitType = Common.Unit.Kilogram;
p.price = 15;
p.vat = 5.5;
p.multiWeight = true;
p.hasFloatQt = true;
p.contract = c;
p.insert();
CHICKEN = p;
var d = service.DistributionService.create(c,new Date(2017, 5, 1, 19, 0, 0),new Date(2017, 5, 1, 19, 2, 0),place.id,null,null,null,null,new Date(2017, 4, 10, 19, 0, 0),new Date(2017, 4, 20, 19, 0, 0));
DISTRIB_LEGUMES_RUE_SAUCISSE = d;
//PASTRY
var c = new db.Contract();
c.name = "Commande Pâtisseries";
c.startDate = new Date(2017, 1, 1, 0, 0, 0);
c.endDate = new Date(2017, 12, 31, 23, 59, 0);
c.vendor = v;
c.amap = a;
c.type = db.Contract.TYPE_VARORDER;
c.insert();
var p = new db.Product();
p.name = "Flan";
p.qt = 1;
p.unitType = Common.Unit.Kilogram;
p.price = 3.5;
p.organic = true;
p.contract = c;
p.insert();
FLAN = p;
var p = new db.Product();
p.name = "Croissant";
p.qt = 1;
p.unitType = Common.Unit.Kilogram;
p.price = 2.8;
p.contract = c;
p.insert();
CROISSANT = p;
var d = new db.Distribution();
d.date = new Date(2017, 5, 1, 19, 0, 0);
d.contract = c;
d.place = place;
d.insert();
DISTRIB_PATISSERIES = d;
}
static function initApp(u:db.User){
//setup App
var app = App.current = new App();
App.config.DEBUG = true;
app.initLang("en");
app.eventDispatcher = new hxevents.Dispatcher<Event>();
app.plugins = [];
//internal plugins
app.plugins.push(new plugin.Tutorial());
//optionnal plugins
#if plugins
//app.plugins.push( new hosted.HostedPlugIn() );
app.plugins.push( new pro.ProPlugIn() );
app.plugins.push( new connector.ConnectorPlugIn() );
//app.plugins.push( new pro.LemonwayEC() );
app.plugins.push( new who.WhoPlugIn() );
#end
App.current.user = u;
App.current.view = new View();
}
}
+92
View File
@@ -0,0 +1,92 @@
package test;
import Common;
/**
* Test various tools
*
* @author fbarbut
*/
class TestTools extends haxe.unit.TestCase
{
public function new(){
super();
}
function testDateRanges(){
// test last hour range
var now = Date.fromString("2018-01-01 00:30:12");
var r = tools.DateTool.getLastHourRange(now);
assertEquals("2017-12-31 23:00:00",r.from.toString());
assertEquals("2018-01-01 00:00:00",r.to.toString());
// test last minute
var now = Date.fromString("2018-01-01 00:30:12");
var r = tools.DateTool.getLastMinuteRange(now);
assertEquals("2018-01-01 00:29:00",r.from.toString());
assertEquals("2018-01-01 00:30:00",r.to.toString());
}
function testFloatTool(){
assertEquals( true , tools.FloatTool.isEqual(10.0,10.000) );
assertEquals( true , tools.FloatTool.isEqual(10.0,10) );
assertEquals( true , tools.FloatTool.isEqual(10,10) );
assertEquals( true , tools.FloatTool.isEqual(10.00000001,10) );
assertEquals( false , tools.FloatTool.isEqual(10.02,10) );
assertEquals( false , tools.FloatTool.isInt(10.08) );
assertEquals( true , tools.FloatTool.isInt(10.00) );
}
/*
@admin
public function doTests() {
var assertTrue = function(val, ?desc="") {
if (val) {
Sys.println("OK : <br/>");
}else {
Sys.println("ERROR : "+desc+"<br/>");
}
}
//test les fonctions de cotisation
var amap = db.Amap.manager.get(1);
amap.membershipRenewalDate = new Date(2015, 0, 1,0,0,0);
amap.update();
assertTrue(amap.getMembershipYear(new Date(2015, 3, 3, 0, 0, 0) ) == 2015);
assertTrue(amap.getPeriodName(new Date(2015, 3, 3, 0, 0, 0)) == "2015");
assertTrue(amap.getMembershipYear(new Date(2014, 8, 8, 0, 0, 0) ) == 2014);
assertTrue(amap.getPeriodName(new Date(2014, 8, 8, 0, 0, 0) ) == "2014");
assertTrue(amap.getMembershipYear(new Date(2013, 11, 12, 0, 0, 0) ) == 2013);
assertTrue(amap.getPeriodName(new Date(2013, 11, 12, 0, 0, 0) ) == "2013");
amap.membershipRenewalDate = new Date(2015, 8, 1,0,0,0);
amap.update();
assertTrue(amap.getMembershipYear(new Date(2015, 3, 3, 0, 0, 0) ) == 2014);
assertTrue(amap.getPeriodName(new Date(2015, 3, 3, 0, 0, 0)) == "2014-2015");
var d = amap.getMembershipYear(new Date(2015, 8, 8, 0, 0, 0) );
assertTrue( d == 2015, "le 8 sept 2015, on doit etre en cotis 2015, la c " + d);
assertTrue(amap.getPeriodName(new Date(2015, 8, 8, 0, 0, 0)) == "2015-2016");
var d = new Date(2013, 11, 12, 0, 0, 0) ;
assertTrue( amap.getMembershipYear(d) == 2013, "le 12 oct 2013, on doit etre en cotis 2013 , là c " + amap.getMembershipYear(d) );
assertTrue(amap.getPeriodName(d) == "2013-2014", "le 12 oct 2013, on doit etre en 2013-2014 , là c " + amap.getPeriodName(d) );
}*/
}
+48
View File
@@ -0,0 +1,48 @@
package test;
import db.UserAmap;
import Common;
/**
* Test user rights to view contracts
*
* @author web-wizard
*/
class TestUser extends haxe.unit.TestCase
{
public function new(){
super();
}
var contract : db.Contract;
var user : db.User;
var group1 : db.Amap;
var group2 : db.Amap;
var userAmap : db.UserAmap;
/**
* get a contract + a user
*/
override function setup(){
TestSuite.initDB();
TestSuite.initDatas();
contract = db.Contract.manager.get(3);
user = db.User.manager.get(1);
group1 = db.Amap.manager.get(1);
group2 = db.Amap.manager.get(2);
}
/**
* Check that a user who has admin rights for his/her group can't view a contract from a group
he/she doesn't belong to
*/
function testViewContract(){
userAmap = db.UserAmap.getOrCreate(user, group1);
userAmap.giveRight(Right.GroupAdmin);
assertFalse(user.canManageContract(contract));
}
}
+51
View File
@@ -0,0 +1,51 @@
package tools;
/**
* Some utility functions for arrays
*/
class ArrayTool
{
/**
* shuffle (randomize) an array
*/
//public static function shuffle<T>(arr:Array<T>):Array<T>
//{
//if (arr!=null) {
//for (i in 0...arr.length) {
//var j = Std.random(arr.length);
//var a = arr[i];
//var b = arr[j];
//arr[i] = b;
//arr[j] = a;
//}
//}
//return arr;
//}
/**
* Group a list of objects by date
* @param objs List of objects
* @param dateParamName Name of the object field which is a date
* @return
*/
public static function groupByDate<T>(objs:Array<T>,dateFieldName:String):Map<String,Array<T>>{
var out = new Map<String, Array<T> >();
for ( o in objs){
var d : Date = Reflect.field(o, dateFieldName);
var group = out.get(d.toString());
if (group == null) group = new Array<T>();
group.push(o);
out.set(d.toString(), group);
}
return out;
}
public static function mapLength<T>(m:Map<T,Dynamic>):Int{
var i = 0;
for (x in m) i++;
return i;
}
}
+52
View File
@@ -0,0 +1,52 @@
package tools;
/**
* Date tool
* @author fbarbut
*/
class DateTool
{
public static function now():Date{
return Date.now();
}
public static function deltaDays(d:Date,n:Int):Date{
return DateTools.delta(d, n * 1000 * 60 * 60 * 24.0);
}
public static function setHourMinute(d:Date, hour:Int, minute:Int):Date{
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), hour, minute, 0);
}
public static function setDateMonth(d:Date, date:Int, month:Int):Date{
return new Date(d.getFullYear(), month, date, d.getHours(), d.getMinutes(), 0);
}
public static function getLastHourRange(?now:Date){
if(now==null) now = Date.now();
var HOUR = 1000.0 * 60 * 60;
var to = setHourMinute(now,now.getHours(),0);
var from = DateTools.delta(to, -HOUR );
return {from:from,to:to};
}
public static function getLastMinuteRange(?now:Date){
if(now==null) now = Date.now();
var MIN = 1000.0 * 60;
var to = setHourMinute(now,now.getHours(),now.getMinutes());
var from = DateTools.delta(to, -MIN );
return {from:from,to:to};
}
public static function getWhichNthDayOfMonth(date:Date):Int {
return Math.floor((date.getDate() - 1) / 7) + 1;
}
public static function getNthDayOfMonth(year:Int,month:Int,dayOfWeek:Int,n:Int):Date {
//dayOfWeek and getDay() follow the same value system: from 0 (Sunday) to 6 (Saturday)
return deltaDays(new Date(year,month,1+7*n, 0, 0, 0), -new Date(year,month,8-(dayOfWeek+1), 0, 0, 0).getDay()-1);
}
}
+24
View File
@@ -0,0 +1,24 @@
package tools;
class FloatTool{
/**
Tries to fix buddy float comparison in Neko...
**/
public static function isEqual(a:Float,b:Float){
a = Math.round(a*10000);
b = Math.round(b*10000);
return a==b;
}
public static function isInt(f:Float){
return isEqual(f , Math.round(f) );
}
//prevent float comparison bug in Neko
public static function clean(f:Float){
return Math.round(f*100)/100;
}
}
+142
View File
@@ -0,0 +1,142 @@
package tools;
import Common.UserOrder;
/**
* Utility to work on sys.db.Object lists
* @author fbarbut
*/
class ObjectListTool
{
/**
* Get a list of IDs from an object list
*/
public static function getIds( objs:Iterable<sys.db.Object> ):Array<Int>{
var out = new Array<Int>();
for ( o in objs ) out.push(untyped o.id);
return out;
}
/**
* deduplicate objects on IDs
*/
public static function deduplicate<T>(objs:Iterable<T>):Array<T>{
var out = new Map<Int,T>();
for ( u in objs) out.set( untyped u.id, u );
return Lambda.array(out);
}
public static function toIdMap<T>(objs:Iterable<T>):Map<Int,T>{
var out = new Map<Int,T>();
for ( u in objs) out.set( untyped u.id, u );
return out;
}
/**
* Deduplicate user orders. (merge orders on same product from a same user)
* @param orders
* @return
*/
public static function deduplicateOrders(orders:Array<UserOrder>):Array<UserOrder>{
var out = new Map<String,UserOrder>();
for ( o in orders){
var key = o.userId + "-" + o.userId2 + "-" + o.productId;
var x = out.get(key);
if ( x == null){
x = o;
}else{
//null safety
if (x.fees == null) x.fees = 0;
if (o.fees == null) o.fees = 0;
//merge
x.quantity += o.quantity;
x.fees += o.fees;
x.subTotal += o.subTotal;
x.total += o.total;
}
out.set(key, x);
}
return Lambda.array(out);
}
/**
* Deduplicate distributions by key (date+placeId)
* @param distribs
*/
public static function deduplicateDistribsByKey(distribs:Iterable<db.Distribution>){
var out = new Map<String,db.Distribution>();
for ( d in distribs) out.set(d.getKey(), d);
return Lambda.array(out);
}
/**
* Group distributions by key (date+placeId)
* @param distribs
*/
public static function groupDistribsByKey(distribs:Iterable<db.Distribution>){
var out = new Map<String,Array<db.Distribution>>();
for ( d in distribs) {
var v = out.get(d.getKey());
if (v == null) v = [];
v.push(d);
out.set(d.getKey(), v);
}
return out;
}
/**
* Groupe orders by multidistrib key (date+placeId)
*/
public static function groupOrdersByKey(ucs:Iterable<db.UserContract>){
var out = new Map<String,Array<db.UserContract>>();
for ( uc in ucs) {
var k = uc.distribution.getKey();
var v = out.get(k);
if (v == null) v = [];
v.push(uc);
out.set(k, v);
}
return out;
}
/**
* Group distributions by group and day, order by day
*/
public static function groupDistributionsByGroupAndDay(dists:Iterable<db.Distribution>){
var out = new Map<String,Array<db.Distribution>>();
for ( d in dists){
var k = d.date.toString().substr(0, 10) + "-" + d.contract.amap.id;
var x = out[k];
if (x == null) x = [];
x.push(d);
out[k] = x;
}
//sort keys
var keys = [];
for ( k in out.keys()) keys.push(k);
keys.sort(function(a, b){ if (a > b) return 1 else return -1; });
var out2 = [];
for ( k in keys) out2.push(out[k]);
return out2;
}
}
+36
View File
@@ -0,0 +1,36 @@
package tools;
/**
* ...
* @author fbarbut
*/
class StockTool
{
/**
* Stock dispatching between groups.
*
* i.e we got 10kg of potatoes to dispatch with 3 groups.
* I dont want to have 3,3333 kg for each group , but something like [3,3,4]
*/
public static function dispatch(stock:Int, groups:Int){
var out = [];
var modulo = stock % groups;
stock -= modulo;
var s = Math.round(stock / groups);
for ( i in 0...groups) out.push(s);
for (i in 0...modulo){
out[i % out.length]++;
}
return out;
}
}