basic coupon handling in client display
authorKonrad Rosenbaum <konrad@silmor.de>
Thu, 29 Dec 2016 15:57:29 +0000 (16:57 +0100)
committerKonrad Rosenbaum <konrad@silmor.de>
Thu, 29 Dec 2016 15:57:29 +0000 (16:57 +0100)
Change-Id: I1937c81efa9568fc2fad8a72f612b295d989942f

28 files changed:
doc/seatplans.txt
doc/seatplans.xsd
iface/wext/MOEventSaleInfo [new file with mode: 0644]
iface/wext/MOSeatPlanDefPrice [new file with mode: 0644]
iface/wext/MOSeatPlanGroup [new file with mode: 0644]
iface/wext/MOSeatPlanVGroup [new file with mode: 0644]
iface/wext/eventsaleinfo.cpp [new file with mode: 0644]
iface/wext/eventsaleinfo.h [new file with mode: 0644]
iface/wext/seatplanobj.cpp [new file with mode: 0644]
iface/wext/seatplanobj.h [new file with mode: 0644]
iface/wext/wext.pri
pack
src/mwin/carttab.cpp
src/mwin/carttab.h
tzone
wob/classes/cart.wolf
wob/classes/event.wolf
wob/classes/order.wolf
wob/classes/seatplan.wolf
wob/db/cart.wolf
wob/db/db.wolf
wob/db/order.wolf
wob/transact/event.wolf
wob/transact/order.wolf
www/inc/classes/random.php
www/inc/db/barcodetable.php
www/inc/wext/cart.php
www/inc/wext/order.php

index c3193e2..210c429 100644 (file)
@@ -1,14 +1,17 @@
 Seat-Plans
 ===========
 
-Specification Version (YYYYnn): 201501
+Specification Version (YYYYnn): 201601
 
 Server Side Minimum:
 ---------------------
 
-<SeatPlan version="201501">
+<SeatPlan version="201601">
   <Group capacity="123" id="pr" name="Premium" numbered="no" price="1 3"/>
   <Group capacity="234" id="ch" name="Cheapo" numbered="no" price="2 4"/>
+  <VGroup capacity="maxcat" name="VIP" price="1 2"/>
+  <DefPrice capacity="200" price="1 2" cost="1300 ask"/>
+  <DefPrice capacity="200" price="3 4" cost="calc ask"/>
 </SeatPlan>
 
 In this simplest form there are two kinds of seats: "Premium" and "Cheapo".
@@ -17,19 +20,49 @@ In both cases seats are unnumbered (i.e. audience members can chose freely).
 <SeatPlan> defines the complete plan
   version - version number of this spec, it is generally assumed that newer
     versions are backwards compatible
-<Group> defines one group of seats
+<Group> defines one physical group of seats
   capacity - specifies how many seats there are in this group
   id - automatically generated ID for groups, must only contain letters and 
     digits, max. 8 chars, no spaces
   name - human readable name for the group
   numbered - "yes": the seats have numbers, "no" audience members can freely chose
-  price - space separated list of pricecategory IDs for price levels that can be
-    sold in this group, if missing: all categories of the event
-    
+  price - space separated list of pricecategory IDs or abbreviations for price
+    levels that can be sold in this group, if missing: all categories of the event
+<VGroup> defines a virtual group of seats that can span multiple groups
+  -> VGroups define upper limits for combinations of categories
+  -> e.g. VIP guests could get cheaper tickets, with a VIP category with different
+     price in each group, say there can be 4 VIP tickets total, but since VIPs can
+     freely chose between groupstotal there must be a capacity of 4 in each group
+     and another limit (the VGroup) that prevents purchase of more than 4 in total
+  -> VGroup cannot have sub-elements
+  name - human readable name for the group
+  capacity - number of seats in this group
+  price - the price categories restricted by this VGroup
+<DefPrice> defines a default for a price category
+  -> those are offered to be automatically generated when the seat plan is assigned to
+     an event
+  capacity - the automatically entered amount of seats for this category
+  price - a list of price category IDs or abbreviations that this setting applies to
+  cost - the automatically assigned cost, this attribute can have one or two items
+    separated by a space:
+    - first - either a numeric value of the price in cents or the keyword "calc", which
+      means to use the stored formula to pre-calculate a price
+    - second - the optional keyword "ask" to ask for confirmation from the user
+    => defaults are generated in the order they are listed in the seat plan, so the order
+       can be used to modify calculated values; each line is calculated at once
+
+The capacity attribute can have positive numeric values (absolute number of seats) or
+one of the following values:
+  maxcat - the maximum allowed by any of the used categories
+  mincat - the minimum allowed by any of the used categories
+  sumcat - the sum of all categories
+  auto - automatically counts the rows and seats defined in the group
+  -> sumcat is the default if there are no Row definitions
+  -> auto is the default if there are Row definitions
 
 To number seats by row add Row-elements to the Group tag:
 
-<SeatPlan version="201501">
+<SeatPlan version="201601">
   <Group capacity="123" id="pr" name="Premium" numbered="yes">
     <Row id="2" capacity="23" first="2"/>
     <Row id="3" capacity="25"/>
@@ -87,7 +120,7 @@ Graphical Seat Plans
 
 This part is never evaluated by the server, only by displays:
 
-<SeatPlan version="201501" size="100 200" bgcolor="darkred" tkcolor="black">
+<SeatPlan version="201601" size="100 200" bgcolor="darkred" tkcolor="black">
   <Group capacity="123" id="pr" name="Premium" numbered="yes" geo="0 20 100 100" bgcolor="#fcc">
     <Row id="2" capacity="23" first="2" geo="10 0 90 10/>
     <Row id="3" capacity="25" geo="0 20 100 10"/>
@@ -158,7 +191,7 @@ The background of the seat plan can be defined in an additional element:
 
   <!-- insert a simple icon or picture -->
   <Background type="image" geo="10 40 25 25" image="icon.png"/>
-  <Image name="icon.png">base64-encoded data</Image>
+  <Image name="icon.png"><Data>base64-encoded data</Data></Image>
 </SeatPlan>
 
 Attributes:
@@ -170,7 +203,7 @@ Attributes:
   Background:text/fontsize - font size in relative points of the seat plan, automatically scaled
   Background:text/content - textual content
   Background:image/image - link to an Image tag
-  Image - contains the actual image data base64 encoded
+  Image/Data - contains the actual image data base64 encoded
   Image/name - link name of the image, usually derived from the original file name
 
 Specifying Seats in Orders
index f1e1bda..d4da099 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <!-- SeatPlan XML Schema Definition
- (c) Konrad Rosenbaum, 2015
+ (c) Konrad Rosenbaum, 2015-16
  protected under the GNU GPL v.3 or at you option any newer
  -->
 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
@@ -10,6 +10,7 @@
        <xs:complexType name="SeatPlanType">
                <xs:choice maxOccurs="unbounded">
                        <xs:element name="Group"/>
+                       <xs:element name="VGroup"/>
                        <xs:element name="Background"/>
                        <xs:element name="Image" type="ImageType"/>
                </xs:choice>
        </xs:complexType>
 
        <xs:simpleType name="VersionType">
-               <xs:annotation><xs:documentation>A version number for the file format. It always consists of 4-digit year plus a 2-digit counter.</xs:documentation></xs:annotation>
+               <xs:annotation>
+                       <xs:documentation>
+                       A version number for the file format. It always consists of 4-digit year plus a 2-digit counter.
+                       </xs:documentation>
+               </xs:annotation>
                <xs:restriction base="xs:string">
                        <xs:pattern value="20[0-9]{4}"/>
                </xs:restriction>
        </xs:simpleType>
 
        <xs:simpleType name="SizeType">
-               <xs:annotation><xs:documentation>Two unsigned integers noting Width and Height.</xs:documentation></xs:annotation>
+               <xs:annotation>
+                       <xs:documentation>
+                       Two unsigned integers noting Width and Height.
+                       </xs:documentation>
+               </xs:annotation>
                <xs:restriction base="UnsignedList">
                        <xs:length value="2"/>
                </xs:restriction>
        </xs:simpleType>
        <xs:simpleType name="RectType">
-               <xs:annotation><xs:documentation>Two unsigned integers noting top left X,Y and Width,Height.</xs:documentation></xs:annotation>
+               <xs:annotation>
+                       <xs:documentation>
+                       Two unsigned integers noting top left X,Y and Width,Height.
+                       </xs:documentation>
+               </xs:annotation>
                <xs:restriction base="UnsignedList">
                        <xs:length value="4"/>
                </xs:restriction>
diff --git a/iface/wext/MOEventSaleInfo b/iface/wext/MOEventSaleInfo
new file mode 100644 (file)
index 0000000..132d246
--- /dev/null
@@ -0,0 +1 @@
+#include"eventsaleinfo.h"
diff --git a/iface/wext/MOSeatPlanDefPrice b/iface/wext/MOSeatPlanDefPrice
new file mode 100644 (file)
index 0000000..79dda50
--- /dev/null
@@ -0,0 +1 @@
+#include"seatplanobj.h"
diff --git a/iface/wext/MOSeatPlanGroup b/iface/wext/MOSeatPlanGroup
new file mode 100644 (file)
index 0000000..79dda50
--- /dev/null
@@ -0,0 +1 @@
+#include"seatplanobj.h"
diff --git a/iface/wext/MOSeatPlanVGroup b/iface/wext/MOSeatPlanVGroup
new file mode 100644 (file)
index 0000000..79dda50
--- /dev/null
@@ -0,0 +1 @@
+#include"seatplanobj.h"
diff --git a/iface/wext/eventsaleinfo.cpp b/iface/wext/eventsaleinfo.cpp
new file mode 100644 (file)
index 0000000..9d0e74e
--- /dev/null
@@ -0,0 +1,50 @@
+//
+// C++ Implementation: MOOrder
+//
+// Description: 
+//
+//
+// Author: Konrad Rosenbaum <konrad@silmor.de>, (C) 2009-2011
+//
+// Copyright: See README/COPYING.GPL files that come with this distribution
+//
+//
+
+#include "eventsaleinfo.h"
+
+#include "MOSeatPlan"
+#include "MOSeatPlanGroup"
+#include "MOSeatPlanVGroup"
+
+#include <QVariant>
+
+static int mymeta=
+       qRegisterMetaType<MOEventSaleInfo>()+
+       qRegisterMetaType<QList<MOEventSaleInfo> >()+
+       qRegisterMetaType<Nullable<MOEventSaleInfo> >();
+static bool mocv=
+       QMetaType::registerConverter<Nullable<MOEventSaleInfo>,MOEventSaleInfo>([](const Nullable<MOEventSaleInfo>&n){return n.value();})|
+       QMetaType::registerConverter<QList<MOEventSaleInfo>,QVariantList>([](const QList<MOEventSaleInfo>&n){QVariantList r;for(auto v:n)r<<QVariant::fromValue(v);return r;});
+
+MOSeatPlan MOEventSaleInfo::seatplan()const
+{
+       return MOSeatPlan::fromString(seatplaninfo().value().plan());
+}
+
+MOSeatPlan MOEventSaleInfo::seatplanAdjusted()const
+{
+       MOSeatPlan plan=MOSeatPlan::fromString(seatplaninfo().value().plan());
+       QList<MOSeatPlanGroup>grp;
+       for(auto g:plan.Group()){
+               g.adjust(*this);
+               grp.append(g);
+       }
+       plan.setGroup(grp);
+       QList<MOSeatPlanVGroup>vgrp;
+       for(auto g:plan.VGroup()){
+               g.adjust(*this);
+               vgrp.append(g);
+       }
+       plan.setVGroup(vgrp);
+       return plan;
+}
diff --git a/iface/wext/eventsaleinfo.h b/iface/wext/eventsaleinfo.h
new file mode 100644 (file)
index 0000000..c471d7a
--- /dev/null
@@ -0,0 +1,48 @@
+//
+// C++ Interface: unabstract
+//
+// Description: removes abstract flag from classes that only need to be abstract in PHP
+//
+//
+// Author: Konrad Rosenbaum <konrad@silmor.de>, (C) 2009-2011
+//
+// Copyright: See README/COPYING.GPL files that come with this distribution
+//
+//
+
+#ifndef MAGICSMOKE_MOEVENTSALEINFO_H
+#define MAGICSMOKE_MOEVENTSALEINFO_H
+
+#include "MOEventSaleInfoAbstract"
+#include "MOSeatPlan"
+
+#ifndef MSIFACE_EXPORT
+#define MSIFACE_EXPORT Q_DECL_IMPORT
+#endif
+
+
+
+//class MOSeatPlan;
+/**this class represents a seat group*/
+class MSIFACE_EXPORT MOEventSaleInfo:public MOEventSaleInfoAbstract
+{
+       Q_GADGET
+       WOBJECT(MOEventSaleInfo)
+       Q_PROPERTY(bool isValid READ isValid)
+       Q_PROPERTY(MOSeatPlan seatplan READ seatplan)
+       public:
+               /**returns whether the object is valid (it comes from the DB and it has been understood by the parser)*/
+               bool isValid()const{return !eventid().isNull();}
+
+               ///returns the parsed seat plan without sale info worked in
+               MOSeatPlan seatplan()const;
+
+               ///returns the parsed seat plan with all the ticket info merged in
+               MOSeatPlan seatplanAdjusted()const;
+};
+
+Q_DECLARE_METATYPE(MOEventSaleInfo)
+Q_DECLARE_METATYPE(QList<MOEventSaleInfo>)
+Q_DECLARE_METATYPE(Nullable<MOEventSaleInfo>)
+
+#endif
diff --git a/iface/wext/seatplanobj.cpp b/iface/wext/seatplanobj.cpp
new file mode 100644 (file)
index 0000000..b318a94
--- /dev/null
@@ -0,0 +1,104 @@
+//
+// C++ Implementation: MOOrder
+//
+// Description: 
+//
+//
+// Author: Konrad Rosenbaum <konrad@silmor.de>, (C) 2009-2011
+//
+// Copyright: See README/COPYING.GPL files that come with this distribution
+//
+//
+
+#include "seatplanobj.h"
+
+#include "MOEventSaleInfo"
+#include "MOEventPrice"
+
+#include <QVariant>
+
+#include <limits>
+
+static int mymeta=
+       qRegisterMetaType<MOSeatPlanDefPrice>()+
+       qRegisterMetaType<QList<MOSeatPlanDefPrice> >()+
+       qRegisterMetaType<Nullable<MOSeatPlanDefPrice> >()+
+       qRegisterMetaType<MOSeatPlanGroup>()+
+       qRegisterMetaType<QList<MOSeatPlanGroup> >()+
+       qRegisterMetaType<Nullable<MOSeatPlanGroup> >()+
+       qRegisterMetaType<MOSeatPlanVGroup>()+
+       qRegisterMetaType<QList<MOSeatPlanVGroup> >()+
+       qRegisterMetaType<Nullable<MOSeatPlanVGroup> >();
+static bool mocv=
+       QMetaType::registerConverter<Nullable<MOSeatPlanDefPrice>,MOSeatPlanDefPrice>([](const Nullable<MOSeatPlanDefPrice>&n){return n.value();})|
+       QMetaType::registerConverter<QList<MOSeatPlanDefPrice>,QVariantList>([](const QList<MOSeatPlanDefPrice>&n){QVariantList r;for(auto v:n)r<<QVariant::fromValue(v);return r;})|
+       QMetaType::registerConverter<Nullable<MOSeatPlanGroup>,MOSeatPlanGroup>([](const Nullable<MOSeatPlanGroup>&n){return n.value();})|
+       QMetaType::registerConverter<QList<MOSeatPlanGroup>,QVariantList>([](const QList<MOSeatPlanGroup>&n){QVariantList r;for(auto v:n)r<<QVariant::fromValue(v);return r;})|
+       QMetaType::registerConverter<Nullable<MOSeatPlanVGroup>,MOSeatPlanVGroup>([](const Nullable<MOSeatPlanVGroup>&n){return n.value();})|
+       QMetaType::registerConverter<QList<MOSeatPlanVGroup>,QVariantList>([](const QList<MOSeatPlanVGroup>&n){QVariantList r;for(auto v:n)r<<QVariant::fromValue(v);return r;});
+
+
+static inline
+QList<MOEventPrice> getcategories(const MOEventSaleInfo&ei,QString spec)
+{
+       const QStringList specl=spec.split(' ',QString::SkipEmptyParts);
+       QList<MOEventPrice>prc;
+       QList<int>ids;
+       QStringList abbrs;
+       for(QString sp:specl){
+               bool isint=false;
+               const int id=sp.toInt(&isint);
+               if(isint)ids<<id;
+               else abbrs<<sp;
+       }
+       for(const MOEventPrice&ep:ei.price())
+               if(ids.contains(ep.pricecategoryid())||abbrs.contains(ep.pricecategory().value().abbreviation()))
+                       prc.append(ep);
+       return prc;
+}
+
+void MOSeatPlanVGroup::adjust(const MOEventSaleInfo&ei)
+{
+       //retrieve categories
+       int max=-1,min=std::numeric_limits<int>::max(),sum=0;
+       QList<MOEventPrice>cats=getcategories(ei,price());
+       if(cats.size()==0)max=min=0;
+       for(const MOEventPrice&c:cats){
+               if(c.maxavailable()>max)max=c.maxavailable();
+               if(c.maxavailable()<min)min=c.maxavailable();
+               sum+=c.maxavailable();
+               mcategoryids<<c.pricecategoryid();
+               mcategoryabbr<<c.pricecategory().value().abbreviation();
+       }
+       //adjust capacity
+       bool isint=false;
+       const QString cap=capacity().value();
+       mcapacity=cap.toInt(&isint);
+       if(!isint){
+               if(cap=="maxcat")mcapacity=max;
+               else if(cap=="mincat")mcapacity=min;
+               else if(cap=="sumcat")mcapacity=sum;
+               //ignore auto and other values
+       }
+       //count tickets
+}
+
+void MOSeatPlanGroup::adjust(const MOEventSaleInfo&ei)
+{
+}
+
+static inline
+QRect str2rect(QString s)
+{
+       QList<int>il;
+       for(QString c:s.split(' ',QString::SkipEmptyParts))
+               il<<c.toInt();
+       if(il.size()!=4)return QRect();
+       return QRect(il[0],il[1],il[2],il[3]);
+}
+
+QRect MOSeatPlanGroup::geometry() const
+{
+       return str2rect(geo());
+}
+
diff --git a/iface/wext/seatplanobj.h b/iface/wext/seatplanobj.h
new file mode 100644 (file)
index 0000000..27d7a76
--- /dev/null
@@ -0,0 +1,161 @@
+//
+// C++ Interface: unabstract
+//
+// Description: removes abstract flag from classes that only need to be abstract in PHP
+//
+//
+// Author: Konrad Rosenbaum <konrad@silmor.de>, (C) 2009-2011
+//
+// Copyright: See README/COPYING.GPL files that come with this distribution
+//
+//
+
+#ifndef MAGICSMOKE_MOSEATPLANOBJ_H
+#define MAGICSMOKE_MOSEATPLANOBJ_H
+
+#include "MOSeatPlanDefPriceAbstract"
+#include "MOSeatPlanGroupAbstract"
+#include "MOSeatPlanVGroupAbstract"
+#include "misc.h"
+
+#include <QRect>
+
+#ifndef MSIFACE_EXPORT
+#define MSIFACE_EXPORT Q_DECL_IMPORT
+#endif
+
+class MOEventSaleInfo;
+
+/**this class represents a default price category definition*/
+class MSIFACE_EXPORT MOSeatPlanDefPrice:public MOSeatPlanDefPriceAbstract
+{
+       Q_GADGET
+       WOBJECT(MOSeatPlanDefPrice)
+       Q_PROPERTY(bool isValid READ isValid)
+       public:
+               /**returns whether the object is valid (it comes from the DB and it has been understood by the parser)*/
+               bool isValid()const{return !capacity().isNull() && !price().isNull() && !cost().isNull();}
+};
+
+Q_DECLARE_METATYPE(MOSeatPlanDefPrice)
+Q_DECLARE_METATYPE(QList<MOSeatPlanDefPrice>)
+Q_DECLARE_METATYPE(Nullable<MOSeatPlanDefPrice>)
+
+
+/**this class represents a seat group*/
+class MSIFACE_EXPORT MOSeatPlanGroup:public MOSeatPlanGroupAbstract
+{
+       Q_GADGET
+       WOBJECT(MOSeatPlanGroup)
+       Q_PROPERTY(bool isValid READ isValid)
+       Q_PROPERTY(int capacityNum READ capacityNum)
+       Q_PROPERTY(QRect geometry READ geometry)
+       Q_PROPERTY(QList<int> categoryIds READ categoryIds)
+       Q_PROPERTY(QStringList categoryAbbreviations READ categoryAbbreviations)
+       Q_PROPERTY(int numReservedTickets READ numReservedTickets)
+       Q_PROPERTY(int numOrderedTickets READ numOrderedTickets)
+       Q_PROPERTY(int numUsedTickets READ numUsedTickets)
+       Q_PROPERTY(int numCancelledTickets READ numCancelledTickets)
+       Q_PROPERTY(int numBlockedSeats READ numBlockedSeats)
+       public:
+               /**returns whether the object is valid (it comes from the DB and it has been understood by the parser)*/
+               bool isValid()const{return !capacity().isNull() && !price().isNull() && !id().isNull();}
+
+               ///numeric capacity
+               int capacityNum()const{return mcapacity;}
+
+               ///geometry as a rect
+               QRect geometry()const;
+
+               ///IDs of categories in this group
+               QList<int> categoryIds()const{return mcategoryids;}
+
+               ///Abbreviations of categories in this group
+               QStringList categoryAbbreviations()const{return mcategoryabbr;}
+
+               ///number of reserved tickets
+               int numReservedTickets()const{return mreserve;}
+
+               ///number of ordered (not yet used) tickets
+               int numOrderedTickets()const{return mordered;}
+
+               ///number of used tickets
+               int numUsedTickets()const{return mused;}
+
+               ///number of cancelled tickets
+               int numCancelledTickets()const{return mcancelled;}
+
+               ///number of blocked seats (reserved, ordered, used tickets)
+               int numBlockedSeats()const{return mblocked;}
+
+       private:
+               friend class MOEventSaleInfo;
+               void adjust(const MOEventSaleInfo&);
+
+               QList<int> mcategoryids;
+               QStringList mcategoryabbr;
+               int mreserve=0,mordered=0,mused=0,mcancelled=0,mblocked=0,mcapacity=0;
+};
+
+Q_DECLARE_METATYPE(MOSeatPlanGroup)
+Q_DECLARE_METATYPE(QList<MOSeatPlanGroup>)
+Q_DECLARE_METATYPE(Nullable<MOSeatPlanGroup>)
+
+
+
+/**this class represents a seat group*/
+class MSIFACE_EXPORT MOSeatPlanVGroup:public MOSeatPlanVGroupAbstract
+{
+       Q_GADGET
+       WOBJECT(MOSeatPlanVGroup)
+       Q_PROPERTY(bool isValid READ isValid)
+       Q_PROPERTY(int capacityNum READ capacityNum)
+       Q_PROPERTY(QList<int> categoryIds READ categoryIds)
+       Q_PROPERTY(QStringList categoryAbbreviations READ categoryAbbreviations)
+       Q_PROPERTY(int numReservedTickets READ numReservedTickets)
+       Q_PROPERTY(int numOrderedTickets READ numOrderedTickets)
+       Q_PROPERTY(int numUsedTickets READ numUsedTickets)
+       Q_PROPERTY(int numCancelledTickets READ numCancelledTickets)
+       Q_PROPERTY(int numBlockedSeats READ numBlockedSeats);
+       public:
+               /**returns whether the object is valid (it comes from the DB and it has been understood by the parser)*/
+               bool isValid()const{return !capacity().isNull() && !price().isNull();}
+
+               ///numeric capacity
+               int capacityNum()const{return mcapacity;}
+
+               ///IDs of categories in this group
+               QList<int> categoryIds()const{return mcategoryids;}
+
+               ///Abbreviations of categories in this group
+               QStringList categoryAbbreviations()const{return mcategoryabbr;}
+
+               ///number of reserved tickets
+               int numReservedTickets()const{return mreserve;}
+
+               ///number of ordered (not yet used) tickets
+               int numOrderedTickets()const{return mordered;}
+
+               ///number of used tickets
+               int numUsedTickets()const{return mused;}
+
+               ///number of cancelled tickets
+               int numCancelledTickets()const{return mcancelled;}
+
+               ///number of blocked seats (reserved, ordered, used tickets)
+               int numBlockedSeats()const{return mblocked;}
+
+       private:
+               friend class MOEventSaleInfo;
+               void adjust(const MOEventSaleInfo&);
+
+               QList<int> mcategoryids;
+               QStringList mcategoryabbr;
+               int mreserve=0,mordered=0,mused=0,mcancelled=0,mblocked=0,mcapacity=0;
+};
+
+Q_DECLARE_METATYPE(MOSeatPlanVGroup)
+Q_DECLARE_METATYPE(QList<MOSeatPlanVGroup>)
+Q_DECLARE_METATYPE(Nullable<MOSeatPlanVGroup>)
+
+#endif
index fc013be..9fbbe7d 100644 (file)
@@ -10,7 +10,9 @@ HEADERS += \
        wext/event.h \
        wext/customer.h \
        wext/transaction.h \
-       wext/keyvalue.h
+       wext/keyvalue.h \
+       wext/seatplanobj.h \
+       wext/eventsaleinfo.h
 
 SOURCES += \
        wext/customerinfo.cpp \
@@ -19,4 +21,6 @@ SOURCES += \
        wext/address.cpp \
        wext/event.cpp \
        wext/customer.cpp \
-       wext/transaction.cpp
+       wext/transaction.cpp \
+       wext/seatplanobj.cpp \
+       wext/eventsaleinfo.cpp
diff --git a/pack b/pack
index 2802df2..c214783 160000 (submodule)
--- a/pack
+++ b/pack
@@ -1 +1 @@
-Subproject commit 2802df28960dcde2a86a78a154cdcb6590152856
+Subproject commit c214783152ecdbf1cf2b9715ab3328ec81716b2e
index 60307a6..4b29258 100644 (file)
 #include "carttab.h"
 
 #include "MTGetAllShipping"
+#include "MTGetCoupon"
 #include "MTGetEvent"
 #include "MTGetValidVoucherPrices"
+#include "MTGetVoucher"
 #include "MTCreateOrder"
 #include "MTCreateReservation"
 
+#include "MOCoupon"
+#include "MOCouponRule"
+
 #include <QApplication>
 #include <QBoxLayout>
 #include <QComboBox>
 #include <QDebug>
+#include <QFormLayout>
 #include <QFrame>
+#include <QGridLayout>
+#include <QInputDialog>
 #include <QLabel>
 #include <QMenu>
 #include <QMenuBar>
@@ -77,14 +85,25 @@ MCartTab::MCartTab(QString pk)
        connect(p,SIGNAL(clicked()),this,SLOT(cartRemoveItem()));
        vl2->addWidget(frm=new QFrame,0);
        frm->setFrameShape(QFrame::HLine);
-       vl2->addLayout(hl2=new QHBoxLayout,0);
-       hl2->addWidget(lab=new QLabel(tr("Total Price Sum:")));
+       QFormLayout *fl;
+       vl2->addLayout(fl=new QFormLayout,0);
+       fl->addRow(lab=new QLabel(tr("Total Price Sum:")),price=new QLabel("0.0"));
        QFont fnt=lab->font();
        fnt.setPointSizeF(fnt.pointSizeF()*1.5);
        lab->setFont(fnt);
-       hl2->addWidget(price=new QLabel("0.0"));
        price->setFont(fnt);
-       hl2->addStretch(1);
+       fl->addRow(tr("Coupon Code:"),hl2=new QHBoxLayout);
+       hl2->addWidget(coupon=new QLabel(tr("(none)","coupon code label")),1);
+       hl2->addWidget(p=new QPushButton(tr("Add Coupon...")),0);
+       connect(p,&QPushButton::clicked,this,[&](){changeVoucher("vadd:coupon");});
+       connect(coupon,SIGNAL(linkActivated(const QString&)),this,SLOT(changeVoucher(QString)));
+       fl->addRow(tr("Pay with Vouchers:"),hl2=new QHBoxLayout);
+       hl2->addWidget(vouchers=new QLabel("..."),1);
+       hl2->addWidget(p=new QPushButton(tr("Add Voucher...")),0);
+       connect(p,&QPushButton::clicked,this,[&](){changeVoucher("vadd:voucher");});
+       connect(vouchers,SIGNAL(linkActivated(const QString&)),this,SLOT(changeVoucher(QString)));
+       fl->addRow(tr("Remaining Sum:"),priceremain=new QLabel("0.0"));
+       //hl2->addStretch(1);
        
        hl->addWidget(frm=new QFrame,0);
        frm->setFrameShape(QFrame::VLine);
@@ -181,11 +200,17 @@ void MCartTab::updateShipping()
        cartship->setToolTip(QString());
 }
 
+#define COL_AMOUNT 0
+#define COL_TITLE 1
+#define COL_START 2
+#define COL_PRICE 3
+#define COL_NOTES 4
+
 void MCartTab::initCart()
 {
        //clear cart
        cartmodel->clear();
-       cartmodel->setHorizontalHeaderLabels(QStringList()<<tr("Amount")<<tr("Title")<<tr("Start Time")<<tr("Price"));
+       cartmodel->setHorizontalHeaderLabels(QStringList()<<tr("Amount")<<tr("Title")<<tr("Start Time")<<tr("Price")<<tr("Notes"));
        //clear customer
        customer=MOCustomer();
        cartcustomer->setText("");
@@ -199,6 +224,9 @@ void MCartTab::initCart()
        cartship->setCurrentIndex(0);
        //reset colors
        resetColor();
+       //reset coupon and voucher labels
+       couponinfo=MOCoupon();
+       vouchersforpay.clear();
        //update price display
        updatePrice();
 }
@@ -278,9 +306,12 @@ static const int CART_VOUCHER=2;
 
 static const int CART_IDROLE=Qt::UserRole;//ticket id
 static const int CART_PRICEIDROLE=Qt::UserRole+3;//for tickets: price category id
+static const int CART_ORIGPRICEIDROLE=Qt::UserRole+5;//for tickets: original price ID if it has been changed by a coupon
+static const int CART_EVENTDATAROLE=Qt::UserRole+6;//for tickets: the full event
 static const int CART_PRICEROLE=Qt::UserRole+4;//voucher/ticket price
 static const int CART_VALUEROLE=Qt::UserRole+1;//voucher value
 static const int CART_TYPEROLE=Qt::UserRole+2;//which is it? ticket or voucher?
+static const int CART_NOTECOPY=Qt::UserRole+7;//a copy of the original note field, in case it gets overwritten by an error
 
 void MCartTab::cartAddTicket()
 {
@@ -343,7 +374,9 @@ void MCartTab::addTicketForEvent(qint64 id, qint64 prcid)
        cartmodel->setData(cartmodel->index(cr,0),id,CART_IDROLE);
        cartmodel->setData(cartmodel->index(cr,0),CART_TICKET,CART_TYPEROLE);
        cartmodel->setData(cartmodel->index(cr,0),ep[pcidx].pricecategoryid().value(),CART_PRICEIDROLE);
+       cartmodel->setData(cartmodel->index(cr,0),ep[pcidx].pricecategoryid().value(),CART_ORIGPRICEIDROLE);
        cartmodel->setData(cartmodel->index(cr,0),ep[pcidx].price().value(),CART_PRICEROLE);
+       cartmodel->setData(cartmodel->index(cr,0),QVariant::fromValue(ev),CART_EVENTDATAROLE);
        cartmodel->setData(cartmodel->index(cr,1),ev.title().value());
        cartmodel->setData(cartmodel->index(cr,2),ev.startTimeString());
        QString pcn=cent2str(ep[pcidx].price())+" ("+ep[pcidx].pricecategory().value().name().value()+")";
@@ -372,6 +405,19 @@ void MCartTab::eventOrderTicket(qint64 id,qint64 prcid)
 
 void MCartTab::cartAddVoucher()
 {
+       //check privileges
+       if(!req->hasRight(MInterface::PCreateOrder_CanPayVoucherWithVoucher)){
+               bool vfound=false;
+               for(int r=0;r<cartmodel->rowCount();r++)
+                       if(cartmodel->data(cartmodel->index(r,0),CART_TYPEROLE).toInt()==CART_VOUCHER){
+                               vfound=true;
+                               break;
+                       }
+               if(vfound){
+                       QMessageBox::warning(this,tr("Warning"),tr("Cannot pay for vouchers with another voucher!"));
+                       return;
+               }
+       }
        //create voucher selection dialog
        QDialog dlg;
        dlg.setWindowTitle(tr("Select Voucher"));
@@ -554,6 +600,7 @@ void MCartTab::cartTableToOrder(MOCartOrder&cord)
 
 bool MCartTab::orderExecute(MOCartOrder&cord,MOOrder&ord,bool isreserve,bool issale)
 {
+       //TODO: add coupon and related info?
        if(isreserve){
                MTCreateReservation co=req->queryCreateReservation(cord);
                if(co.hasError()){
@@ -563,7 +610,7 @@ bool MCartTab::orderExecute(MOCartOrder&cord,MOOrder&ord,bool isreserve,bool iss
                ord=co.getorder();
                cord=co.getcart();
        }else{
-               MTCreateOrder co=req->queryCreateOrder(cord,issale);
+               MTCreateOrder co=req->queryCreateOrder(cord,issale,QStringList());//TODO: add vouchers
                if(co.hasError()){
                        QMessageBox::warning(this,tr("Warning"),tr("Error while creating order: %1").arg(co.errorString()));
                        return false;
@@ -603,31 +650,53 @@ void MCartTab::verifyOrderCustomer(MOCartOrder&cord)
 
 void MCartTab::verifyOrderTickets(const QList<MOCartTicket>&ticks)
 {
+       //reset model
+       for(int i=0;i<cartmodel->rowCount();i++){
+               if(cartmodel->data(cartmodel->index(i,0),CART_TYPEROLE).toInt() != CART_TICKET)continue;
+               for(int j=0;j<cartmodel->columnCount();j++){
+                       QModelIndex idx=cartmodel->index(i,j);
+                       cartmodel->setData(idx,QVariant(),Qt::BackgroundRole);
+                       cartmodel->setData(idx,QVariant(),Qt::ToolTipRole);
+               }
+               QModelIndex idx=cartmodel->index(i,COL_NOTES);
+               cartmodel->setData(idx,cartmodel->data(idx,CART_NOTECOPY));
+       }
+       //insert
        for(int i=0;i<ticks.size();i++){
                if(ticks[i].status()!=MOCartTicket::Ok){
                        //find it
                        int j=ticks[i].cartlineid();
                        if(j<0 || j>=cartmodel->rowCount())continue;
-                       QModelIndex idx0=cartmodel->index(j,0);
+                       const QModelIndex idx0=cartmodel->index(j,COL_AMOUNT);
                        if(cartmodel->data(idx0,CART_TYPEROLE).toInt() != CART_TICKET)continue;
-                       QModelIndex idx1=cartmodel->index(j,1);
-                       cartmodel->setData(idx1,QColor(Qt::red),Qt::BackgroundRole);
+                       const QModelIndex idx1=cartmodel->index(j,COL_TITLE);
+                       const auto red=QColor(Qt::red);
+                       cartmodel->setData(idx1,red,Qt::BackgroundRole);
+                       const QModelIndex idxn=cartmodel->index(j,COL_NOTES);
                        //check state, color it
+                       QString tt;
                        switch(ticks[i].status().value()){
                                case MOCartTicket::EventOver:
-                                       cartmodel->setData(idx1,tr("The event is already over, please remove this entry."),Qt::ToolTipRole);
+                                       tt=tr("The event is already over, please remove this entry.");
+                                       cartmodel->setData(idx1,tt,Qt::ToolTipRole);
+                                       cartmodel->setData(idxn,tt);
                                        break;
                                case MOCartTicket::TooLate:
-                                       cartmodel->setData(idx1,tr("You cannot order tickets for this event anymore, ask a more privileged user."),Qt::ToolTipRole);
+                                       tt=tr("You cannot order tickets for this event anymore, ask a more privileged user.");
+                                       cartmodel->setData(idx1,tt,Qt::ToolTipRole);
+                                       cartmodel->setData(idxn,tt);
                                        break;
-                               case MOCartTicket::Exhausted:{
-                                       cartmodel->setData(idx0,QColor(Qt::red),Qt::BackgroundRole);
-                                       QString tt=tr("The event is (almost) sold out, there are %1 tickets left.").arg(ticks[i].maxamount());
+                               case MOCartTicket::Exhausted:
+                                       cartmodel->setData(idx0,red,Qt::BackgroundRole);
+                                       tt=tr("The event or category is (almost) sold out, there are %1 tickets left.").arg(ticks[i].maxamount());
                                        cartmodel->setData(idx0,tt,Qt::ToolTipRole);
                                        cartmodel->setData(idx1,tt,Qt::ToolTipRole);
-                                       break;}
+                                       cartmodel->setData(idxn,tt);
+                                       break;
                                case MOCartTicket::Invalid:
-                                       cartmodel->setData(idx1,tr("The event does not exist or there is another serious problem, please remove this entry."),Qt::ToolTipRole);
+                                       tt=tr("The event does not exist or there is another serious problem, please remove this entry.");
+                                       cartmodel->setData(idx1,tt,Qt::ToolTipRole);
+                                       cartmodel->setData(idxn,tt);
                                        break;
                                default:break;//ignore all others
                        }
@@ -637,6 +706,18 @@ void MCartTab::verifyOrderTickets(const QList<MOCartTicket>&ticks)
 
 void MCartTab::verifyOrderVouchers(const QList<MOCartVoucher>&voucs)
 {
+       //reset model
+       for(int i=0;i<cartmodel->rowCount();i++){
+               if(cartmodel->data(cartmodel->index(i,0),CART_TYPEROLE).toInt() != CART_VOUCHER)continue;
+               for(int j=0;j<cartmodel->columnCount();j++){
+                       QModelIndex idx=cartmodel->index(i,j);
+                       cartmodel->setData(idx,QVariant(),Qt::BackgroundRole);
+                       cartmodel->setData(idx,QVariant(),Qt::ToolTipRole);
+               }
+               QModelIndex idx=cartmodel->index(i,COL_NOTES);
+               cartmodel->setData(idx,cartmodel->data(idx,CART_NOTECOPY));
+       }
+       //insert
        for(int i=0;i<voucs.size();i++){
                if(voucs[i].status()!=MOCartVoucher::Ok){
                        //find it
@@ -646,13 +727,19 @@ void MCartTab::verifyOrderVouchers(const QList<MOCartVoucher>&voucs)
                        if(cartmodel->data(idx0,CART_TYPEROLE).toInt() != CART_VOUCHER)continue;
                        QModelIndex idx1=cartmodel->index(j,1);
                        cartmodel->setData(idx1,QColor(Qt::red),Qt::BackgroundRole);
+                       const QModelIndex idxn=cartmodel->index(j,COL_NOTES);
                        //check state, color it
+                       QString tt;
                        switch(voucs[i].status().value()){
                                case MOCartVoucher::InvalidValue:
-                                       cartmodel->setData(idx1,tr("You do not have permission to create vouchers with this value, please remove it."),Qt::ToolTipRole);
+                                       tt=tr("You do not have permission to create vouchers with this value, please remove it.");
+                                       cartmodel->setData(idx1,tt,Qt::ToolTipRole);
+                                       cartmodel->setData(idxn,tt);
                                        break;
                                case MOCartVoucher::InvalidPrice:
-                                       cartmodel->setData(idx1,tr("The price tag of this voucher is not valid, please remove and recreate it."),Qt::ToolTipRole);
+                                       tt=tr("The price tag of this voucher is not valid, please remove and recreate it.");
+                                       cartmodel->setData(idx1,tt,Qt::ToolTipRole);
+                                       cartmodel->setData(idxn,tt);
                                        break;
                                default:break;//ignore all others
                        }
@@ -677,8 +764,194 @@ void MCartTab::updatePrice()
        }
        //get shipping
        prc+=cartship->itemData(cartship->currentIndex(),SHIPPING_PRICEROLE).toInt();
-       //display
+       //display price
        price->setText(cent2str(prc));
+       //format coupon
+       if(couponinfo.couponid().isNull())
+               coupon->setText(tr("(none)","empty coupon code label")+
+                       QString(" <a href=\"vadd:coupon\">%1</a>").arg(tr("[Add Coupon]","link/label")));
+       else
+               coupon->setText(QString("%1 (%2)").arg(couponinfo.couponid()).arg(couponinfo.description()));
+       //format vouchers
+       QString vt;
+       if(vouchersforpay.size()>0){
+               //check for vouchers in cart
+               bool vfound=false;
+               for(int r=0;r<cartmodel->rowCount();r++)
+                       if(cartmodel->data(cartmodel->index(r,0),CART_TYPEROLE).toInt()==CART_VOUCHER){
+                               vfound=true;
+                               break;
+                       }
+               if(vfound)
+                       vt+="<font color=\"red\">"+tr("<b>Warning:</b> Paying for vouchers with older vouchers!")+"</font><br/>";
+               //render vouchers for payment
+               vt+="<html><table>";
+               for(auto vou:vouchersforpay){
+                       vt+=QString("<tr><td>%1 &nbsp;</td><td> -&nbsp; %2</td><td> &nbsp; <a href=\"vremove:%1\">%3</a></td></tr>")
+                               .arg(vou.first).arg(cent2str(vou.second)).arg(tr("[remove]","remove voucher link/label"));
+                       prc-=vou.second;
+               }
+               vt+="</table><br/>";
+       }
+       vt+="<a href=\"vadd:voucher\">"+tr("[Add Voucher]","link/label")+"</a>";
+       vouchers->setText(vt);
+       //format remaining price
+       if(prc<0)prc=0;
+       priceremain->setText(cent2str(prc));
+}
+
+void MCartTab::changeVoucher ( QString url )
+{
+       qDebug()<<url;
+       if(url=="vadd:coupon"){
+               //sanity check
+               if(!couponinfo.couponid().isNull()){
+                       QMessageBox::warning(this,tr("Warning"),tr("Only one coupon can be used on an order."));
+                       return;
+               }
+               //ask for ID
+               const QString cid=QInputDialog::getText(this,tr("Enter Coupon ID"),tr("Please enter the coupon ID:")).trimmed().toUpper();
+               if(cid.isEmpty())return;
+               //get coupon from server
+               MOCoupon cp=req->queryGetCoupon(cid).getcoupon();
+               //verify and use
+               if(cp.couponid().isNull()){
+                       QMessageBox::warning(this,tr("Warning"),tr("'%1' is not a valid coupon.").arg(cid));
+                       return;
+               }
+               if(!req->checkFlags(cp.flags())){
+                       QMessageBox::warning(this,tr("Warning"),tr("Sorry, you are not allowed to use this coupon."));
+                       return;
+               }
+               couponinfo=cp;
+               applyCoupon();
+       }else if(url=="vadd:voucher"){
+               //sanity check
+               bool priv=req->hasRight(MInterface::PCreateOrder_CanPayVoucherWithVoucher);
+               bool vfound=false;
+               for(int r=0;r<cartmodel->rowCount();r++)
+                       if(cartmodel->data(cartmodel->index(r,0),CART_TYPEROLE).toInt()==CART_VOUCHER){
+                               vfound=true;
+                               break;
+                       }
+               if(!priv && vfound){
+                       QMessageBox::warning(this,tr("Warning"),tr("Cannot pay for vouchers with another voucher!"));
+                       return;
+               }
+               //get voucher ID
+               const QString vid=QInputDialog::getText(this,tr("Enter Voucher ID"),tr("Please enter the voucher ID:")).trimmed().toUpper();
+               if(vid.isEmpty())return;
+               for(auto v:vouchersforpay)
+                       if(v.first==vid){
+                               QMessageBox::warning(this,tr("Warning"),tr("Voucher '%1' is already in list.").arg(vid));
+                               return;
+                       }
+               //retrieve it and check
+               MTGetVoucher vou=req->queryGetVoucher(vid);
+               if(vou.getvoucher().isNull() || vou.getvoucher().value().voucherid().isNull()){
+                       QMessageBox::warning(this,tr("Warning"),tr("The voucher '%1' does not exist.").arg(vid));
+                       return;
+               }
+               if(vou.getvoucher().data().value().isNull()||vou.getvoucher().data().value().data()<=0){
+                       QMessageBox::warning(this,tr("Warning"),tr("The voucher '%1' does not have any value left.").arg(vid));
+                       return;
+               }
+               if(!vou.getispaidfor().value()){
+                       QMessageBox::warning(this,tr("Warning"),tr("The voucher '%1' cannot be used: it has not been paid yet.").arg(vid));
+                       return;
+               }
+               //add
+               vouchersforpay.append(QPair<QString,int>(vid,vou.getvoucher().data().value()));
+       }else if(url.startsWith("vremove:")){
+               const QString vid=url.mid(8);
+               for(int i=0;i<vouchersforpay.size();i++)
+                       if(vouchersforpay[i].first==vid){
+                               vouchersforpay.removeAt(i);
+                               break;
+                       }
+       }
+       //TODO: if coupon removal is implemented - change applyCoupon accordingly
+       //update the listing
+       updatePrice();
+}
+
+void MCartTab::applyCoupon()
+{
+       //TODO:
+       // - implement maxtickets (coupon) and maxuse (rule) limits
+       // - implement need* limits (rule)
+       // -> current implementation bails out if it encounters any of the above
+
+       //sanity check
+       if(couponinfo.couponid().isNull())return;
+       //TODO: once removal of coupon gets implemented - do a reset instead of quitting
+
+       //check for not implemented limits - TODO: implement instead
+       if(!couponinfo.amountuse().isNull() || !couponinfo.ticketsperorder().isNull()){
+               coupon->setText(tr("<html><font color=\"red\">Sorry, coupon limits are not implemented yet.</font>"));
+               return;
+       }
+       for(auto rule:couponinfo.rules())
+               if(!rule.maxuse().isNull() || !rule.needcategory().isNull() || !rule.needtickets().isNull()){
+                       coupon->setText(tr("<html><font color=\"red\">Sorry, coupon rule limits are not implemented yet.</font>"));
+                       return;
+               }
+
+       //go through order and reset tickets
+       //TODO: count and accumulate
+       for(int i=0;i<cartmodel->rowCount();i++){
+               QModelIndex idx0=cartmodel->index(i,0);
+               if(cartmodel->data(idx0,CART_TYPEROLE).toInt()!=CART_TICKET)continue;
+               MOEvent ev=cartmodel->data(idx0,CART_EVENTDATAROLE).value<MOEvent>();
+               //QModelIndex idxp=cartmodel->index(i,COL_PRICE);
+               int pid=cartmodel->data(idx0,CART_ORIGPRICEIDROLE).toInt();
+               MOEventPrice ep;
+               for(auto prc:ev.price())
+                       if(prc.pricecategoryid()==pid){
+                               ep=prc;
+                               break;
+                       }
+               cartmodel->setData(idx0,pid,CART_PRICEIDROLE);
+               cartmodel->setData(idx0,ep.price().data(),CART_PRICEROLE);
+               QModelIndex idxp=cartmodel->index(i,COL_PRICE);
+               cartmodel->setData(idxp,QString("%1 (%2)").arg(cent2str(ep.price())).arg(ep.pricecategory().value().name().value()));
+               QModelIndex idxn=cartmodel->index(i,COL_NOTES);
+               cartmodel->setData(idxn,cartmodel->data(idxn,CART_NOTECOPY));
+       }
+
+       //go through rules and adjust tickets
+       //TODO: work on accumulated data
+       for(int i=0;i<cartmodel->rowCount();i++){
+               //get price id
+               QModelIndex idx0=cartmodel->index(i,0);
+               if(cartmodel->data(idx0,CART_TYPEROLE).toInt()!=CART_TICKET)continue;
+               int pid=cartmodel->data(idx0,CART_ORIGPRICEIDROLE).toInt();
+               //do we have a rule?
+               int npid=pid;
+               for(auto rule:couponinfo.rules()){
+                       if(pid==rule.fromcategory().data()){
+                               npid=rule.tocategory();
+                               break;
+                       }
+               }
+               if(pid==npid)continue;
+               //corrections
+               MOEvent ev=cartmodel->data(idx0,CART_EVENTDATAROLE).value<MOEvent>();
+               MOEventPrice ep,epo;
+               for(auto prc:ev.price()){
+                       if(prc.pricecategoryid()==npid)
+                               ep=prc;
+                       if(prc.pricecategoryid()==pid)
+                               epo=prc;
+               }
+
+               cartmodel->setData(idx0,npid,CART_PRICEIDROLE);
+               cartmodel->setData(idx0,ep.price().data(),CART_PRICEROLE);
+               QModelIndex idxp=cartmodel->index(i,COL_PRICE);
+               cartmodel->setData(idxp,QString("%1 (%2)").arg(cent2str(ep.price())).arg(ep.pricecategory().value().name().value()));
+               QModelIndex idxn=cartmodel->index(i,COL_NOTES);
+               cartmodel->setData(idxn,tr("Original price: %1 (%2)").arg(cent2str(epo.price())).arg(epo.pricecategory().value().name().value()));
+       }
 }
 
 /********************************************************************************/
index 9a48a7d..6f3ddb9 100644 (file)
@@ -20,6 +20,7 @@
 #include <QTimer>
 
 #include "MOCustomer"
+#include "MOCoupon"
 
 class QAction;
 class QCheckBox;
@@ -105,6 +106,8 @@ class MCartTab:public QWidget
                void resetColor(bool addronly=false);
                /**update the price shown*/
                void updatePrice();
+               ///remove/add a voucher from the payment area
+               void changeVoucher(QString url);
                
        private:
                //the profile associated with this session
@@ -112,12 +115,14 @@ class MCartTab:public QWidget
                //widgets
                QTableView*carttable;
                QStandardItemModel*cartmodel;
-               QLabel*cartcustomer,*deliveryaddr,*invoiceaddr,*price;
+               QLabel*cartcustomer,*deliveryaddr,*invoiceaddr,*price,*coupon,*vouchers,*priceremain;
                QTextEdit *cartcomment;
                QComboBox*cartship;
                //cart
                MOCustomer customer;
                qint64 deliveryaddrid,invoiceaddrid;
+               MOCoupon couponinfo;
+               QList<QPair<QString,int>> vouchersforpay;
                
                /**helper for order: returns true if an order can be sent*/
                bool canorder(bool isreserve);
@@ -136,6 +141,9 @@ class MCartTab:public QWidget
                
                /**helper for cartAddTicket and eventOrderTicket: choses a price category and then adds the ticket*/
                void addTicketForEvent(qint64,qint64 prcid=-1);
+
+               ///helper to apply coupon rules
+               void applyCoupon();
 };
 
 /**Helper class for shopping cart: allow editing amount, but nothing else*/
diff --git a/tzone b/tzone
index d2852c3..d944922 160000 (submodule)
--- a/tzone
+++ b/tzone
@@ -1 +1 @@
-Subproject commit d2852c3d8f78d0ae39e2a0833e33c3551e89cea0
+Subproject commit d94492246f5b98e0936f5a184e9533496d240a63
index a15dee2..cfc7e99 100644 (file)
@@ -94,6 +94,7 @@
                <Property name="tickets" type="List:CartTicket"/>
                <Property name="vouchers" type="List:CartVoucher"/>
                <Property name="items" type="List:CartItem"/>
+               <Property name="coupon" type="astring">An optional coupon code to be used in this order.</Property>
                
                <Mapping table="cart">
                        <Map property="customerid"/>
index b9371c3..b90414a 100644 (file)
                 </Mapping>
         </Class>
         
-        <Class name="SeatPlanInfo">
-                <Doc>This class is the transport for seat plans when communicating over the wire</Doc>
-                <Property name="roomid" type="string"/>
-                <Property name="name" type="astring"/>
-                <Property name="plan" type="string"/>
-                <Property name="flags" type="astring"/>
-        </Class>
-                
        <Class name="Event">
                <Abstract lang="qt"/>
                <Abstract lang="php"/>
                        </Map>
                </Mapping>
        </Class>
-</Wolf>
\ No newline at end of file
+</Wolf>
index 75a55b7..de18251 100644 (file)
@@ -2,7 +2,7 @@
 <!-- Order MagicSmoke WOLF
   - Tables and Comm Objects for Orders, Tickets, Vouchers, etc.
   -
-  - (c) Konrad Rosenbaum, 2009-2011
+  - (c) Konrad Rosenbaum, 2009-2016
   - this file is protected under the GNU AGPLv3 or at your option any newer
   - see COPYING.AGPL for details
   -->
@@ -54,7 +54,7 @@
                        </Map>
                </Mapping>
        </Class>
-       
+
        <Class name="TicketAudit" base="Ticket">
                <Abstract lang="php"/>
                <Property name="audittime" type="int64"/>
                <Property name="comments" type="string"/>
                
                <Property name="tags" type="string"/>
+
+               <Property name="couponid" type="astring"/>
+               <Property name="coupondescription" type="string"/>
                <!-- etc.pp. -->
                
                <Mapping table="order">
                        <Map column="senttime"/>
                        <Map column="comments"/>
                        <Map column="tags"/>
+                       <Map column="couponid"/>
                        
                        <Map property="tickets"><Call lang="php" method="WOTicket::fromTableArrayticket(WTticket::selectFromDB('orderid='.$GLOBALS['db']->escapeInt($table->orderid)))"/></Map>
                        <Map property="vouchers"><Call lang="php" method="WOVoucher::fromTableArrayvoucher(WTvoucher::selectFromDB('orderid='.$GLOBALS['db']->escapeInt($table->orderid)))"/></Map>
                                
                        <Map property="totalprice"><Call lang="php" method="$data->getTotalPrice()"/></Map>
                        <Map property="amountdue"><Call lang="php" method="$data->prop_totalprice - $data->prop_amountpaid"/></Map>
+                       <Map property="coupondescription"><Call lang="php" method="$GLOBALS['db']->isNull($table->couponid)?null:WTcoupon::getFromDB($table->couponid)->description"/></Map>
                </Mapping>
        </Class>
        
 
                <Property name="ordertime" type="int64"/>
                <Property name="senttime" type="int64"/>
+
+               <Property name="couponid" type="astring"/>
                <!-- etc.pp. -->
                <Mapping table="order">
                        <Map column="orderid"/>
                        <Map property="amountitems">
                                <Call lang="php" method="$data->getAmountItemsFromDB()"/>
                        </Map>
+                       <Map column="couponid"/>
                </Mapping>
        </Class>
        
                        <Map column="visible"/>
                </Mapping>
        </Class>
+
+       <Class name="CouponRule">
+               <Property name="ruleid" type="int64"/>
+               <Property name="couponid" type="astring"/>
+               <Property name="fromcategory" type="int"/>
+               <Property name="tocategory" type="int"/>
+               <Property name="maxuse" type="int"/>
+               <Property name="needcategory" type="int"/>
+               <Property name="needtickets" type="int"/>
+               <Mapping table="couponrule">
+                       <Map column="ruleid"/>
+                       <Map column="couponid"/>
+                       <Map column="fromcategory"/>
+                       <Map column="tocategory"/>
+                       <Map column="maxuse"/>
+                       <Map column="needcategory"/>
+                       <Map column="needtickets"/>
+               </Mapping>
+       </Class>
+
+       <Class name="Coupon">
+               <Doc>This class represents a coupon.</Doc>
+               <Property name="couponid" type="astring"/>
+               <Property name="description" type="string"/>
+               <Property name="validfrom" type="int64"/>
+               <Property name="validtill" type="int64"/>
+               <Property name="amountuse" type="int32"/>
+               <Property name="ticketsperorder" type="int32"/>
+               <Property name="flags" type="string"/>
+               <Property name="rules" type="List:CouponRule"/>
+               <Mapping table="coupon">
+                       <Map column="couponid"/>
+                       <Map column="description"/>
+                       <Map column="validfrom"/>
+                       <Map column="validtill"/>
+                       <Map column="amountuse"/>
+                       <Map column="ticketsperorder"/>
+                       <Map column="flags"/>
+                       <Map property="rules"><Call lang="php" method="WOCouponRule::fromTableArraycouponrule(WTcouponrule::selectFromDB('couponid='.$GLOBALS['db']->escapeString($table->couponid)))"/></Map>
+               </Mapping>
+       </Class>
+       <Class name="CouponInfo">
+               <Doc>This class represents the bare minimum info of a coupon.</Doc>
+               <Property name="couponid" type="astring"/>
+               <Property name="description" type="string"/>
+               <Property name="validfrom" type="int64"/>
+               <Property name="validtill" type="int64"/>
+               <Property name="flags" type="string"/>
+               <Mapping table="coupon">
+                       <Map column="couponid"/>
+                       <Map column="description"/>
+                       <Map column="validfrom"/>
+                       <Map column="validtill"/>
+                       <Map column="flags"/>
+               </Mapping>
+       </Class>
 </Wolf>
index 57fd30d..a582776 100644 (file)
@@ -2,12 +2,15 @@
 <!-- Seat Plan Definition MagicSmoke WOLF
   - declares everything needed to define seat plans
   -
-  - (c) Konrad Rosenbaum, 2013
+  - (c) Konrad Rosenbaum, 2013-16
   - this file is protected under the GNU AGPLv3 or at your option any newer
   - see COPYING.AGPL for details
   -->
 <Wolf>
+       <!-- NON-Wire Helper Classes -->
         <Class name="SeatPlanRow">
+               <!-- Abstract lang="qt"/>
+               <Abstract lang="php"/ -->
                 <Doc>This is a helper for parsing seat plans: it defines a row of seats</Doc>
                 <Property name="id" type="astring">the ID of this group, if multiple row elements with the same ID exist, they refer to different sections of the same row</Property>
                 <Property name="capacity" type="int">amount of seats in this (part of the) row</Property>
                 <Property name="first" type="int">number of the first seat in this row</Property>
         </Class>
         <Class name="SeatPlanGroup">
+               <Abstract lang="qt"/>
+               <Abstract lang="php"/>
                 <Doc>This is a helper for parsing seat plans: it defines a group of rows of seats.</Doc>
                 <Property name="id" type="astring">the ID of this group, if multiple group elements with the same ID exist, they refer to different sections of the same group</Property>
-                <Property name="capacity" type="int">amount of seats in this (part of the) group</Property>
+                <Property name="capacity" type="astring">amount of seats in this (part of the) group</Property>
                 <Property name="name" type="astring">human readable name of the group</Property>
                 <Property name="numbered" type="bool">defines whether the group contains numbered seats</Property>
                 <Property name="geo" type="astring">GUI: geometry information for the group</Property>
                 <Property name="fgcolor" type="astring">GUI: foreground color, the one the group name is rendered in</Property>
                 <Property name="angle" type="int">GUI: rotation of the group rectangle</Property>
                 <Property name="Row" type="List:SeatPlanRow">definition of rows in this group</Property>
+               <Property name="price" type="astring">The price categories in this group.</Property>
         </Class>
-        
+
+        <Class name="SeatPlanVGroup">
+               <Abstract lang="qt"/>
+               <Abstract lang="php"/>
+                <Doc>This is a helper for parsing seat plans: it defines a virtual group of categories.</Doc>
+                <Property name="capacity" type="astring">amount of seats in this (part of the) group</Property>
+                <Property name="name" type="astring">human readable name of the group</Property>
+                <Property name="price" type="astring">definition of categories in this group</Property>
+        </Class>
+
         <Class name="SeatPlanBackground">
+               <!-- Abstract lang="qt"/>
+               <Abstract lang="php"/ -->
+               <Doc>Backgroup objects for seatplan graphics.</Doc>
                 <Property name="type" type="astring">Type of background element, e.g. "circle", "rect", ...</Property>
                 <Property name="geo" type="astring">GUI: geometry information for the group</Property>
                 <Property name="bgcolor" type="astring">GUI: background or fill color</Property>
                 <Property name="fontsize" type="int">GUI: font size for text elements</Property>
                 <Property name="content" type="astring">GUI: textual content</Property>
         </Class>
-        
+
+       <Class name="SeatPlanDefPrice">
+               <Abstract lang="qt"/>
+               <Abstract lang="php"/>
+               <Doc>Default price categories for a seat plan.</Doc>
+                <Property name="capacity" type="astring">amount of seats in this (part of the) group</Property>
+                <Property name="price" type="astring">definition of categories in this group</Property>
+               <Property name="cost" type="astring">default cost definition</Property>
+       </Class>
+
+       <Class name="SeatPlanImage">
+               <Doc>Image data that can be embedded in seat plan backgrounds</Doc>
+               <Property name="name" type="astring"/>
+               <Property name="Data" type="blob"/>
+       </Class>
+
         <Class name="SeatPlan">
-                <Doc>This class is used to parse seat plans on either side.</Doc>
+                <Doc>This class is used to parse seat plans on either side. It is not sent
+               over the wire, since it is just a helper class.</Doc>
+               <Property name="version" type="int"/>
                 <Property name="Group" type="List:SeatPlanGroup"/>
+               <Property name="VGroup" type="List:SeatPlanVGroup"/>
                 <Property name="Background" type="List:SeatPlanBackground"/>
+               <Property name="Image" type="List:SeatPlanImage"/>
+               <Property name="DefPrice" type="List:SeatPlanDefPrice"/>
+        </Class>
+
+
+       <!-- Wire Classes -->
+       <Class name="SeatPlanInfo">
+                <Doc>This class is the transport for seat plans when communicating over the wire</Doc>
+               <Property name="seatplanid" type="int"/>
+                <Property name="roomid" type="string"/>
+                <Property name="name" type="astring"/>
+                <Property name="plan" type="string"/>
+                <Property name="flags" type="astring"/>
+               <Mapping table="seatplan">
+                       <Map column="seatplanid"/>
+                       <Map column="roomid"/>
+                       <Map column="name"/>
+                       <Map column="plan"/>
+                       <Map column="flags"/>
+               </Mapping>
         </Class>
+
+       <Class name="TicketSaleInfo">
+               <Doc>This class represents only the info from a ticket that is required for calculating seating information.</Doc>
+               <Enum name="TicketState" refColumn="ticket:status"/>
+               <Property name="eventid" type="int"/>
+               <Property name="price" type="int"/>
+               <Property name="status" type="TicketState"/>
+               <Property name="pricecategoryid" type="int"/>
+               <Property name="tags" type="string"/>
+                <Property name="seat" type="astring"/>
+
+               <Mapping table="ticket">
+                       <Map column="price"/>
+                       <Map column="eventid"/>
+                       <Map column="status"/>
+                        <Map column="seat"/>
+                       <Map column="pricecategoryid"/>
+               </Mapping>
+       </Class>
+
+       <Class name="EventSaleInfo">
+               <Abstract lang="qt"/>
+               <Doc>This class transports all data necessary to calculate the free seats of an event</Doc>
+               <Property name="eventid" type="int"/>
+               <Property name="iscancelled" type="bool"/>
+               <Property name="capacity" type="int"/>
+               <Property name="room" type="string"/>
+               <Property name="price" type="List:EventPrice"/>
+               <Property name="seatplanid" type="int"/>
+               <Property name="seatplaninfo" type="SeatPlanInfo"/>
+               <Property name="ticketsales" type="List:TicketSaleInfo"/>
+               <Property name="canuse" type="bool"/>
+               <Mapping table="event">
+                       <Map column="eventid" property="id"/>
+                       <Map column="roomid" property="room"/>
+                       <Map column="seatplanid"/>
+                       <Map column="capacity"/>
+                       <Map column="iscancelled"/>
+                       <Map property="price">
+                               <Call lang="php" method="WOEventPrice::fromTableArrayeventprice(WTeventprice::selectFromDB('eventid='.$GLOBALS['db']->escapeInt($table->eventid),'ORDER BY prio'))"/>
+                       </Map>
+                       <Map property="seatplaninfo">
+                               <Call lang="php" method="WOSeatPlanInfo::fromTableseatplan(WTseatplan::getFromDB($table->seatplanid))"/>
+                       </Map>
+                       <Map property="ticketsales">
+                               <Call lang="php" method="WOTicketSalesInfo::fromTableArrayticket(WTticket::selectFromDB('eventid='.$GLOBALS['db']->escapeInt($table->eventid)));"/>
+                       </Map>
+                       <Map property="canuse">
+                               <Call lang="php" method="$GLOBALS['session']->checkFlags($data->getflags())"/>
+                       </Map>
+               </Mapping>
+       </Class>
+
+
 </Wolf>
index 40aa618..accfa25 100644 (file)
@@ -2,7 +2,7 @@
 <!-- Cart MagicSmoke WOLF
    - shopping cart management (web interface)
    -
-   - (c) Konrad Rosenbaum, 2009-2012
+   - (c) Konrad Rosenbaum, 2009-2016
    - this file is protected under the GNU AGPLv3 or at your option any newer
    - see COPYING.AGPL for details
    -->
@@ -63,4 +63,4 @@
                <!--unix timestamp at which to delete this session-->
                <Column name="timeout" type="int64" notnull="yes">the time when the session will be deleted, updated whenever the session is used</Column>
        </Table>
-</Wolf>
\ No newline at end of file
+</Wolf>
index ce017b5..d4f5705 100644 (file)
@@ -9,7 +9,7 @@
        <DataBase
                instance="db" schema="dbScheme" defaultUpdating="yes"
                configTable="config" configKeyColumn="ckey" configValueColumn="cval"
-               version="01.08" versionRow="MagicSmokeVersion">
+               version="01.09" versionRow="MagicSmokeVersion">
                <AuditTables>
                        <Column name="audittime" type="int64">Time at which the change was made.
                                <Call lang="php" method="time()"/>
index a79aebd..3360a41 100644 (file)
                 <Preset><V col="paytype" val="credit card"/><V col="description" val="credit card payment"/><V col="dataname" val="card data"/><V col="datapreset" val="date: %Y-%M-%D, card: ..."/></Preset>
                 <Preset><V col="paytype" val="voucher"/><V col="description" val="payment with voucher"/><V col="dataname" val="voucher ID"/><V col="flags" val="+_admin"/></Preset>
        </Table>
+       <Table name="coupon" backup="yes">
+               <Doc>This table contains coupon definitions</Doc>
+               <Column name="couponid" type="string:32" primarykey="yes"/>
+               <Column name="description" type="string">short description of the coupon for internal use</Column>
+               <Column name="validfrom" type="int64" notnull="yes">Validity period of the coupon.</Column>
+               <Column name="validtill" type="int64" notnull="yes"/>
+               <Column name="amountuse" type="int32" null="yes">How often it can be used, if NULL it is unlimited.</Column>
+               <Column name="ticketsperorder" type="int32" null="yes">How many tickets per order can be changed, NULL for unlimited.</Column>
+               <Column name="flags" type="string">Filter: who can use this coupon.</Column>
+       </Table>
+       <Table name="couponrule" backup="yes">
+               <Doc>This table contains rules of what category of ticket to exchange to what new category by use of a coupon.</Doc>
+               <Column name="ruleid" type="seq64" primarykey="yes">Simple counter key.</Column>
+               <Column name="couponid" type="string:32" foreignkey="coupon:couponid" notnull="yes"/>
+               <Column name="fromcategory" type="int32" foreignkey="pricecategory:pricecategoryid" notnull="yes">from what category of ticket...</Column>
+               <Column name="tocategory" type="int32" foreignkey="pricecategory:pricecategoryid" notnull="yes">..to change to what category of ticket.</Column>
+               <Column name="maxuse" type="int32" null="yes">how often this rule can be used in one order or NULL for unlimited</Column>
+               <Column name="needcategory" type="int32" foreignkey="pricecategory:pricecategoryid" null="yes">
+                       after the change tickets of this category must exist or NULL if there is no requirement
+               </Column>
+               <Column name="needtickets" type="int32" null="yes">
+                       after the change this many tickets of "needcategory" must exist or NULL if there is no requirement;
+                       e.g. a Two-for-One rule would change fromcategory="paid" to tocategory="free" if needcategory="paid" still has needtickets="1" ticket
+               </Column>
+       </Table>
        <Table name="order" backup="yes" audit="yes" backupKey="orderid">
                <Doc>This table contains all orders and sales.</Doc>
                <Column name="orderid" type="seq32" primarykey="yes"/>
@@ -67,6 +92,7 @@
                <Column name="tags" type="string" null="yes">
                additional tags for this order, can be used by scripts
                </Column>
+               <Column name="couponid" type="string:32" null="yes" foreignkey="coupon:couponid">Optional coupon code used on this order.</Column>
                
                <!-- in audit: monitor the type of payment -->
                <AuditColumn name="paytype" type="string:64" foreignkey="paymenttype:paytype"/>
index 1526471..97043ea 100644 (file)
                        <Var name="events" type="List:Event"/>
                </Output>
        </Transaction>
-       
+
+       <Transaction name="GetEventSaleInfo" updating="no">
+               <Doc>returns the information necessary to handle sales with seat plans</Doc>
+               <Input>
+                       <Var name="eventid" type="int"/>
+               </Input>
+               <Call lang="php" method="$this->setevent(WOEventSaleInfo::fromTableevent(WTevent::getFromDB($this->geteventid())));"/>
+               <Output>
+                       <Var name="event" type="EventSaleInfo"/>
+               </Output>
+       </Transaction>
+
        <Transaction name="CreateEvent">
                <Doc>create a new event</Doc>
                <Input>
                        <Var name="orders" type="List:Order"/>
                </Output>
        </Transaction>
-</Wolf>
\ No newline at end of file
+</Wolf>
index 20f3bd1..4d2b5f4 100644 (file)
@@ -2,7 +2,7 @@
 <!-- Order MagicSmoke WOLF
   - Tables and Comm Objects for Orders, Tickets, Vouchers, etc.
   -
-  - (c) Konrad Rosenbaum, 2009-2012
+  - (c) Konrad Rosenbaum, 2009-2016
   - this file is protected under the GNU AGPLv3 or at your option any newer
   - see COPYING.AGPL for details
   -->
                <Input>
                        <Var name="voucherid" type="astring"/>
                </Input>
-               <Call lang="php" method="$this->setvoucher(WOVoucher::fromTablevoucher(WTvoucher::getFromDB($this->getvoucherid())));"/>
+               <Call lang="php" method="$v=WOVoucher::fromTablevoucher(WTvoucher::getFromDB($this->getvoucherid())); $this->setvoucher($v); $this->setispaidfor(WOOrder::fromTableorder(WTorder::getFromDB($v->getorderid()))->isFullyPaid());"/>
                <Output>
                        <Var name="voucher" type="Voucher"/>
+                       <Var name="ispaidfor" type="bool"/>
                </Output>
        </Transaction>
        
                        <Var name="order" type="Order"/>
                </Output>
        </Transaction>
-       
+
+       <Transaction name="GetOrdersByCoupon" updating="no">
+               <Input>
+                       <Var name="oldest" type="int64">unix timestamp for the oldest order to be returned (compared with ordertime)</Var>
+                       <Var name="couponid" type="astring"/>
+               </Input>
+               <Call lang="php" method="WOOrderInfo::GetOrdersByCoupon($this);"/>
+               <Output>
+                       <Var name="orders" type="List:OrderInfo"/>
+                       <Var name="customers" type="List:CustomerInfo"/>
+               </Output>
+       </Transaction>
+
        <Transaction name="CreateOrder">
                <Doc>CreateOrder creates orders that are queued to be executed immediately or sales that are marked executed and paid for immediately; they may contain tickets, vouchers, or shopping items</Doc>
                <Privilege name="AnyVoucherValue">users with this privilege can create vouchers with arbitrary value</Privilege>
                <Privilege name="CanOrderTicket">users with this privilege may put tickets into orders</Privilege>
                <Privilege name="CanOrderVoucher">users with this privilege may put vouchers into orders</Privilege>
                <Privilege name="CanOrderItem">users with this privilege may put shop items (merchandise) into orders</Privilege>
+               <Privilege name="CanPayVoucherWithVoucher">users with this privilege may pay for vouchers with other vouchers</Privilege>
                <Input>
                        <Var name="cart" type="CartOrder">The cart contents</Var>
                        <Var name="issale" type="bool">true if this is a sale, false if this is an order</Var>
+                       <Var name="vouchers" type="List:string">an optional list of vouchers to be used in this order for payment (coupon is in the cart instead)</Var>
                </Input>
                <Call lang="php" method="WOCartOrder::createOrder($this);"/>
                <Output>
                        <Var name="order" type="Order">If successful: the created order</Var>
+                       <Var name="vouchers" type="List:Voucher">If vouchers were used: the current state of the vouchers after creating (and paying) the order</Var>
+                       <Var name="paidcash" type="int">If this is a sale: the amount paid in cash (in cents)</Var>
                        <Var name="cart" type="CartOrder">If unsuccessful: the cart with annotations</Var>
                </Output>
        </Transaction>
                        The transaction will never transfer money from an order back to the voucher.
                        If the voucher is no longer valid the transaction will fail.
                </Doc>
+               <Privilege name="CanPayVoucherWithVoucher">users with this privilege may pay for vouchers with other vouchers</Privilege>
                <Input>
                        <Var name="orderid" type="int">ID of the order that is to be paid for</Var>
                        <Var name="voucherid" type="astring">ID of the voucher that is to be used for payment</Var>
                </Output>
        </Transaction>
        
-       <Transaction name="GetPaymentTypes" update="no">
+       <Transaction name="GetPaymentTypes" updating="no">
                <Doc>returns the valid payment types</Doc>
                <Call lang="php" method="WOOrder::getPaymentTypesTransaction($this);"/>
                <Output>
                <Output/>
        </Transaction>
 
-       <Transaction name="GetOrderDocumentNames" update="no">
+       <Transaction name="GetOrderDocumentNames" updating="no">
                <Doc>Gets the names of all files associated with the Order</Doc>
                <Input>
                        <Var name="orderid" type="int"/>
                        <Var name="documentinfo" type="List:OrderDocumentInfo"/>
                </Output>
        </Transaction>
-       <Transaction name="GetOrderDocument" update="no">
+       <Transaction name="GetOrderDocument" updating="no">
                <Doc>Gets a document associated with an order.</Doc>
                <Input>
                        <Var name="orderid" type="int"/>
                        <Var name="document" type="OrderDocument"/>
                </Output>
        </Transaction>
-       <Transaction name="SetOrderDocument" update="yes">
+       <Transaction name="SetOrderDocument" updating="yes">
                <Doc>Stores a document associated with an order. Overwrites the document if it already exists, creates it otherwise.</Doc>
                <Input>
                        <Var name="document" type="OrderDocument">
                <Call lang="php" method="WOOrder::setOrderDocument($this);"/>
                <Output/>
        </Transaction>
-       <Transaction name="DeleteOrderDocument" update="yes">
+       <Transaction name="DeleteOrderDocument" updating="yes">
                <Doc>Deletes a document associated with an order. Produces an error if the document does not exist.</Doc>
                <Input>
                        <Var name="orderid" type="int"/>
                <Output/>
        </Transaction>
 
-       <Transaction name="SendCustomerMail" update="no">
+       <Transaction name="SendCustomerMail" updating="no">
                <Doc>Sends an eMail to a customer. Usually to update the customer about Order data.</Doc>
                <Input>
                        <Var name="customerid" type="int">ID of the customer. Must be given! The mail address associated with the account is used.</Var>
                <Output/>
        </Transaction>  
        
-       <Transaction name="GetPrintAtHomeSettings" update="no">
+       <Transaction name="GetPrintAtHomeSettings" updating="no">
                <Doc>Gets all settings pertaining to Print@Home</Doc>
                <Input/>
                <Call lang="php" method="WOOrder::getPrintAtHomeSettings($this);"/>
                        <Var name="settings" type="List:KeyValuePair"/>
                </Output>
        </Transaction>
-       <Transaction name="SetPrintAtHomeSettings" update="yes">
+       <Transaction name="SetPrintAtHomeSettings" updating="yes">
                <Doc>Overrides settings for Print@Home, use isnull=true to delete entries.</Doc>
                <Input>
                        <Var name="settings" type="List:KeyValuePair"/>
                <Call lang="php" method="WOOrder::setPrintAtHomeSettings($this);"/>
                <Output/>
        </Transaction>
+
+       <Transaction name="GetCoupon" updating="no">
+               <Doc>Retrieves details about a single coupon.</Doc>
+               <Input>
+                       <Var name="couponid" type="astring"/>
+               </Input>
+               <Call lang="php" method="$this->setcoupon(WOCoupon::fromTablecoupon(WTcoupon::getFromDB($this->getcouponid())));" />
+               <Output>
+                       <Var name="coupon" type="Coupon"/>
+               </Output>
+       </Transaction>
+       <Transaction name="GetCouponList" updating="no">
+               <Doc>Retrieves all Coupon IDs</Doc>
+               <Input>
+                       <Var name="couponid" type="astring"/>
+               </Input>
+               <Call lang="php" method="$this->setcoupons(WOCoupon::fromTableArraycoupon(WTcoupon::selectFromDB('')));" />
+               <Output>
+                       <Var name="coupons" type="List:CouponInfo"/>
+               </Output>
+       </Transaction>
+       <Transaction name="CreateCoupon" updating="yes">
+               <Doc>Creates a new coupon.</Doc>
+               <Input>
+                       <Var name="coupon" type="Coupon">Input data for the coupon, if the ID is empty or null a new one is created</Var>
+               </Input>
+               <Call lang="php" method="WOCoupon::createCoupon($this);"/>
+               <Output>
+                       <Var name="coupon" type="Coupon">actually stored coupon: the ID may be slightly changed by the server</Var>
+               </Output>
+       </Transaction>
+       <Transaction name="ChangeCoupon" updating="yes">
+               <Doc>Changes a coupon.</Doc>
+               <Input>
+                       <Var name="coupon" type="Coupon"/>
+               </Input>
+               <Call lang="php" method="WOCoupon::changeCoupon($this);"/>
+               <Output/>
+       </Transaction>
 </Wolf>
index 36d7027..621c807 100644 (file)
@@ -76,14 +76,17 @@ define("RND_TICKET",0x00);
 //added to a voucher
 define("RND_VOUCHER",0x08);
 //not used yet:
-define("RND_OTHER1",0x10);
+define("RND_COUPON",0x10);
 define("RND_OTHER2",0x18);
 
+//Code 39 chars that are used for barcodes
+define("RND_CODE39_CHARS","123456789ABCDEFGHJKLMNPQRSTUVWXYZ");
+
 /**return a new Code-39 capable ID; length is the amount of characters*/
 function getCode39ID($length,$range=RND_ANYRANGE)
 {
        //we use a subset of the Code-39 characters that is moderately un-ambiguous
-       $c39="123456789ABCDEFGHJKLMNPQRSTUVWXYZ";
+       $c39=RND_CODE39_CHARS;
        //get plenty of random bits (2 hex chars for each Code-39 char)
        $rnd=getRandom($length*8);
        //construct the code
@@ -103,6 +106,24 @@ function getCode39ID($length,$range=RND_ANYRANGE)
        return $ret;
 }
 
+///returns whether a barcode matches a specific range
+function code39IsInRange($code,$range=RND_ANYRANGE,$length=-1)
+{
+       //check length
+       if($length>0){
+               if(strlen($code)!=$length)return false;
+       }
+       //convert first char back to int
+       $c=strtoupper(substr($code,0,1));
+       $i=strpos(RND_CODE39_CHARS,$c);
+       if($i===false)return false;
+       //shortcut
+       if($range==RND_ANYRANGE)return true;
+       //mask and compare
+       $i &= ~RND_MASK;
+       return $i == $range;
+}
+
 //eof
 return;
 ?>
index 822e848..4d3ec30 100644 (file)
@@ -8,6 +8,7 @@ abstract class BarcodeTable extends WobTable
 {
        private static $NumTicketChars=false;
        private static $NumVoucherChars=false;
+       private static $NumCouponChars=false;
        private static function init()
        {
                global $db;
@@ -19,6 +20,10 @@ abstract class BarcodeTable extends WobTable
                        self::$NumVoucherChars=$db->getConfig("VoucherIDChars")+0;
                        if(self::$NumVoucherChars<=5)self::$NumVoucherChars=10;
                }
+               if(self::$NumCouponChars===false){
+                       self::$NumCouponChars=$db->getConfig("CouponIDChars")+0;
+                       if(self::$NumCouponChars<=5)self::$NumCouponChars=10;
+               }
        }
        public static function getNewTicketId()
        {
@@ -40,6 +45,36 @@ abstract class BarcodeTable extends WobTable
                        if(count($res)==0)return $tid;
                }while(true);
        }
+       public static function getNewCouponId()
+       {
+               global $db;
+               self::init();
+               do{
+                       $tid=getCode39ID(self::$NumCouponChars,RND_COUPON);
+                       $res=$db->select("coupon","couponid","couponid=".$db->escapeString($tid));
+                       if(count($res)==0)return $tid;
+               }while(true);
+       }
+       ///checks that the ID is valid as a coupon and could not interfere with tickets or vouchers
+       public static function checkCouponIdValid($couponid)
+       {
+               self::init();
+               //DB limits
+               if(strlen($couponid)<1 || strlen($couponid)>32)return false;
+               //not a ticket or voucher
+               if(code39IsInRange($couponid,RND_TICKET,self::$NumTicketChars) || code39IsInRange($couponid,RND_VOUCHER,self::$NumVoucherChars))
+                       return false;
+               //seems to be ok then...
+               return true;
+       }
+       ///checks whether a coupon ID exists
+       public static function checkCouponIdExists($couponid)
+       {
+               global $db;
+               self::init();
+               $res=$db->select("coupon","couponid","couponid=".$db->escapeString($couponid));
+               return count($res)!=0;
+       }
 };
 
 //eof
index a36117a..504c1d4 100644 (file)
@@ -93,8 +93,13 @@ class WOCartOrder extends WOCartOrderAbstract
                                return;
                        }
                }else{
+                       //verify vouchers and items are valid
                        $very&=$cart->verifyVouchers($trans,$vanyval,$vdiffprice);
                        $very&=$cart->verifyItems($trans);
+                       //verify vouchers are not paid with other vouchers (unless privilege exists)
+                       if(!$trans->havePrivilege(WTrCreateOrder::Priv_CanPayVoucherWithVoucher)){
+                               $very&=count($trans->getvouchers())==0 || count($cart->getvouchers())==0;
+                       }
                }
                //verification successful?
                if(!$very){
index 64fb6d7..6113309 100644 (file)
@@ -135,7 +135,27 @@ class WOOrderInfo extends WOOrderInfoAbstract
                $cust=WTcustomer::selectFromDB("customerid IN ".$db->escapeIntList(array_unique($cst,SORT_NUMERIC)));
                $trans->setcustomers(WOCustomerInfo::fromTableArraycustomer($cust));
        }
-       
+
+       /**called from GetOrdersByUser and getMyOrders transaction*/
+       static public function getOrdersByCoupon($trans)
+       {
+               global $session;
+               $cid=$trans->getcouponid();
+               $old=$trans->getoldest();
+               if($old===false)$old=0;
+               //get primary list: orders of this user
+               global $db;
+               $res=WTorder::selectFromDB("couponid=".$db->escapeString($cid)." AND ordertime>=".$db->escapeInt($old));
+               //get list of customerids
+               $cst=array();
+               foreach($res as $ord)
+                       $cst[]=$ord->customerid;
+               //construct result
+               $trans->setorders(self::fromTableArrayorder(array_values($res)));
+               $cust=WTcustomer::selectFromDB("customerid IN ".$db->escapeIntList(array_unique($cst,SORT_NUMERIC)));
+               $trans->setcustomers(WOCustomerInfo::fromTableArraycustomer($cust));
+       }
+
        /**helper function for several transactions: selects customers by their IDs references by the given order info objects*/
        static protected function getCustomerListByOrders(array $olist)
        {
@@ -181,6 +201,12 @@ class WOOrder extends WOOrderAbstract
                //return
                return $prc;
        }
+
+       ///returns true if the order is fully paid
+       public function isFullyPaid()
+       {
+               return $this->getTotalPrice()<=$this->prop_amountpaid;
+       }
        
        /**called by GetOrderByBarcode transaction*/
        static public function getOrderByBarcode($trans)
@@ -399,7 +425,15 @@ class WOOrder extends WOOrderAbstract
                        $trans->abortWithError(tr("Amount to be paid must be positive."));
                        return;
                }
+               //check that voucher is not used to pay for other vouchers
                $order=WOOrder::fromTableorder($ord);
+               if(!$trans->havePrivilege(WTrUseVoucher::Priv_CanPayVoucherWithVoucher)){
+                       if(count($order->getvouchers())>0){
+                               $trans->abortWithError(tr("Cannot pay for voucher with another voucher."));
+                               return;
+                       }
+               }
+               //make the payment
                $max=$order->prop_amountdue;
                if($max<=0)$pay=0;else
                if($max<$pay)$pay=$max;