A webapp/form for people to join pEp coop. Fork of Cultural Commons Collecting Society (C3S) SCE
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

models.py 78KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382
  1. # -*- coding: utf-8 -*-
  2. """
  3. This module holds the database models for c3sMembership.
  4. Unit and functional tests for the code are in tests/test_models.py.
  5. Other functional and integration tests can be found throughout the
  6. other bits of tests and code relying on or using these models.
  7. The classes below represent the database tables.
  8. It is actually a SQLAlchemy model.
  9. Classes / Data Objects:
  10. * **Groups** (Roles for Users)
  11. * **C3sStaff** (backend login people, administration)
  12. * **Shares** (packages -- members can hold packages of shares)
  13. * **C3sMember** (members .. or applications to become members)
  14. * **Dues15Invoice** (membership dues, 2015 edition)
  15. """
  16. from datetime import (
  17. date,
  18. datetime,
  19. )
  20. from decimal import Decimal
  21. import math
  22. import re
  23. from sqlalchemy import (
  24. and_,
  25. Boolean,
  26. Column,
  27. Date,
  28. DateTime,
  29. distinct,
  30. ForeignKey,
  31. Integer,
  32. or_,
  33. not_,
  34. Table,
  35. Unicode,
  36. )
  37. from sqlalchemy.ext.hybrid import hybrid_property
  38. from sqlalchemy.sql import func
  39. from sqlalchemy.sql import expression
  40. from sqlalchemy.orm import (
  41. relationship,
  42. synonym
  43. )
  44. import sqlalchemy.types as types
  45. import cryptacular.bcrypt
  46. from c3smembership.data.model.base import (
  47. Base,
  48. DBSession,
  49. )
  50. # pylint: disable=no-member
  51. CRYPT = cryptacular.bcrypt.BCRYPTPasswordManager()
  52. def hash_password(password):
  53. """
  54. Calculates the password hash.
  55. """
  56. return unicode(CRYPT.encode(password))
  57. # TODO: Use standard SQLAlchemy Decimal when a database is used which supports
  58. # it.
  59. class SqliteDecimal(types.TypeDecorator):
  60. """
  61. Type decorator for persisting Decimal (currency values)
  62. TODO: Use standard SQLAlchemy Decimal
  63. when a database is used which supports it.
  64. """
  65. impl = types.String
  66. def load_dialect_impl(self, dialect):
  67. return dialect.type_descriptor(types.VARCHAR(100))
  68. def process_bind_param(self, value, dialect):
  69. if value is not None:
  70. return str(value)
  71. else:
  72. return None
  73. def process_result_value(self, value, dialect):
  74. if value is not None and value != '':
  75. return Decimal(value)
  76. else:
  77. return None
  78. DatabaseDecimal = SqliteDecimal
  79. class InvalidPropertyException(Exception):
  80. """
  81. Exception indicating an invalid property value.
  82. """
  83. pass
  84. class InvalidSortDirection(Exception):
  85. """
  86. Exception indicating an invalid sort direction.
  87. """
  88. pass
  89. class Group(Base):
  90. """
  91. The table of Groups.
  92. aka roles for users.
  93. Users in group 'staff' may do things others may not.
  94. """
  95. __tablename__ = 'groups'
  96. # pylint: disable=invalid-name
  97. id = Column(Integer, primary_key=True, nullable=False)
  98. """technical id. / number in table (Integer, Primary Key)"""
  99. name = Column(Unicode(30), unique=True, nullable=False)
  100. """name of the group (Unicode)"""
  101. def __str__(self):
  102. return 'group:%s' % self.name
  103. def __init__(self, name):
  104. self.name = name
  105. @classmethod
  106. def get_staffers_group(cls, groupname=u'staff'):
  107. """
  108. Get the "staff" group.
  109. Returns:
  110. object: staff group.
  111. """
  112. dbsession = DBSession()
  113. staff_group = dbsession.query(
  114. cls).filter(cls.name == groupname).first()
  115. return staff_group
  116. # table for relation between staffers and groups
  117. # pylint: disable=invalid-name
  118. staff_groups = Table(
  119. 'staff_groups', Base.metadata,
  120. Column(
  121. 'staff_id', Integer, ForeignKey('staff.id'),
  122. primary_key=True, nullable=False),
  123. Column(
  124. 'group_id', Integer, ForeignKey('groups.id'),
  125. primary_key=True, nullable=False)
  126. )
  127. # """
  128. # Table for relation of staff to groups
  129. # """
  130. class C3sStaff(Base):
  131. """
  132. C3S staff may login and do things.
  133. """
  134. __tablename__ = 'staff'
  135. # pylint: disable=invalid-name
  136. id = Column(Integer, primary_key=True)
  137. """technical id. / number in table (integer, primary key)"""
  138. login = Column(Unicode(255), unique=True)
  139. """every user has a login name. (unicode)"""
  140. _password = Column('password', Unicode(60))
  141. """a hash"""
  142. last_password_change = Column(
  143. DateTime,
  144. default=func.current_timestamp())
  145. """timestamp of last password change/form submission (Datetime)"""
  146. email = Column(Unicode(255))
  147. """email address (Unicode)"""
  148. groups = relationship(
  149. Group,
  150. secondary=staff_groups,
  151. backref="staff")
  152. """list of group objects (users groups) (relation)"""
  153. def _init_(self, login, password, email): # pragma: no cover
  154. """
  155. make new group
  156. """
  157. self.login = login
  158. self.password = password
  159. self.last_password_change = datetime.now()
  160. self.email = email
  161. def _get_password(self):
  162. return self._password
  163. def _set_password(self, password):
  164. self._password = hash_password(password)
  165. password = property(_get_password, _set_password)
  166. password = synonym('_password', descriptor=password)
  167. @classmethod
  168. def get_by_id(cls, staff_id):
  169. """
  170. Get C3sStaff object by id.
  171. Args:
  172. id: the id of the C3sStaff object to be returned.
  173. Returns:
  174. * **object**: C3sStaff object with relevant id, if exists.
  175. * **None**: if id can't be found.
  176. """
  177. return DBSession.query(cls).filter(cls.id == staff_id).first()
  178. @classmethod
  179. def get_by_login(cls, login):
  180. """
  181. Get C3sStaff object by login.
  182. Args:
  183. login: the login of the C3sStaff object to be returned.
  184. Returns:
  185. * **object**: C3sStaff object with relevant login, if exists.
  186. * **None**: if login can't be found.
  187. """
  188. return DBSession.query(cls).filter(cls.login == login).first()
  189. @classmethod
  190. def check_password(cls, login, password):
  191. """
  192. Check staff password.
  193. Args:
  194. login: staff login.
  195. password: staff password as supplied.
  196. Returns:
  197. the answer of bcrypt.crypt, comparing the password supplied
  198. and the hash from the database
  199. """
  200. staffer = cls.get_by_login(login)
  201. return CRYPT.check(staffer.password, password)
  202. # this one is used by RequestWithUserAttribute
  203. @classmethod
  204. def check_user_or_none(cls, login):
  205. """
  206. Check whether a user by that login exists in the database.
  207. Args:
  208. login: the name to log in with
  209. Returns:
  210. * **C3sStaff object**, if login exists.
  211. * **None**, if login does not exist.
  212. """
  213. login = cls.get_by_login(login) # is None if user not exists
  214. return login
  215. @classmethod
  216. def delete_by_id(cls, staff_id):
  217. """
  218. Delete one C3sStaff object by id.
  219. """
  220. row = DBSession.query(cls).filter(cls.id == staff_id).first()
  221. row.groups = []
  222. DBSession.query(cls).filter(cls.id == staff_id).delete()
  223. @classmethod
  224. def get_all(cls):
  225. """
  226. Get all C3sStaff objects from the database.
  227. Returns:
  228. list: list of C3sStaff objects.
  229. """
  230. return DBSession.query(cls).all()
  231. class Shares(Base):
  232. """
  233. A package of shares which a member acquires.
  234. Each shares package consists of one to sixty shares. One member may own
  235. several packages, e.g. from membership application, crowdfunding and
  236. requesting the acquisition of additional shares.
  237. Shares packages only come to existence once the business process is finished
  238. and the transaction is completed. Information about ongoing processes cannot
  239. here.
  240. """
  241. __tablename__ = 'shares'
  242. # pylint: disable=invalid-name
  243. id = Column(Integer, primary_key=True)
  244. """Technical primary key of the shares package."""
  245. number = Column(Integer())
  246. """Number of shares of the shares package."""
  247. date_of_acquisition = Column(Date(), nullable=False)
  248. """Date of acquisition of the shares package, i.e. the date of approval of
  249. the administrative board."""
  250. reference_code = Column(Unicode(255), unique=True)
  251. """A reference code used for email confirmation and as bank transfer
  252. purpose."""
  253. signature_received = Column(Boolean, default=False)
  254. """Flag indicating whether the signed application for becoming a member or
  255. acquiring additional shares was received."""
  256. signature_received_date = Column(
  257. Date(), default=date(1970, 1, 1))
  258. """Date on which the signed application for becoming a member or acquiring
  259. additional sharess was received."""
  260. signature_confirmed = Column(Boolean, default=False)
  261. """Flag indicating whether the confirmation email about the arrival of the
  262. signed application was sent."""
  263. signature_confirmed_date = Column(
  264. Date(), default=date(1970, 1, 1))
  265. """Date on which the confirmation email about the arrival of the signed
  266. application was sent."""
  267. payment_received = Column(Boolean, default=False)
  268. """Flag indicating whether the payment for the shares package was
  269. received."""
  270. payment_received_date = Column(
  271. Date(), default=date(1970, 1, 1))
  272. """Date on which the payment for the shares package was received."""
  273. payment_confirmed = Column(Boolean, default=False)
  274. """Flag indicating whether the confirmation email about the arrival of the
  275. payment was sent."""
  276. payment_confirmed_date = Column(
  277. Date(), default=date(1970, 1, 1))
  278. """Date on which the confirmation email about the arrival of the payment was
  279. sent."""
  280. accountant_comment = Column(Unicode(255))
  281. """A free text comment for accounting purposes."""
  282. # table for relation between membership and shares
  283. # pylint: disable=invalid-name
  284. members_shares = Table(
  285. 'members_shares', Base.metadata,
  286. Column(
  287. 'members_id', Integer, ForeignKey('members.id'),
  288. primary_key=True, nullable=False),
  289. Column(
  290. 'shares_id', Integer, ForeignKey('shares.id'),
  291. primary_key=True, nullable=False)
  292. )
  293. class C3sMember(Base):
  294. """
  295. This table holds datasets from submissions to the C3S AFM form
  296. (AFM = application for membership),
  297. as well as members who have completed the process
  298. of becoming a member.
  299. Apart from datasets from the original form,
  300. other datasets have found their way into the database
  301. through imports: crowdfunders and founding members, for example.
  302. * The crowdfunders were gathered through a CF platform.
  303. * The founders from the initial assembly (RL! Hamburg!)
  304. * legal entities (we had a form on dead wood)
  305. Some attributes have been added over time to cater for different needs.
  306. """
  307. __tablename__ = 'members'
  308. # pylint: disable=invalid-name
  309. id = Column(Integer, primary_key=True)
  310. """technical id. / number in table (integer, primary key)"""
  311. # personal information
  312. """
  313. **personal information**
  314. """
  315. firstname = Column(Unicode(255))
  316. """given name(s) of person"""
  317. lastname = Column(Unicode(255))
  318. """last name of person"""
  319. email = Column(Unicode(255))
  320. """email address of person
  321. """
  322. _password = Column('password', Unicode(60))
  323. """password hash of person
  324. """
  325. last_password_change = Column(
  326. DateTime,
  327. default=func.current_timestamp())
  328. """timestamp of password persistence (time of submission)
  329. """
  330. # pass_reset_token = Column(Unicode(255))
  331. address1 = Column(Unicode(255))
  332. """Street & Number"""
  333. address2 = Column(Unicode(255))
  334. """Address continued"""
  335. postcode = Column(Unicode(255))
  336. """Postal Code"""
  337. city = Column(Unicode(255))
  338. """City or Place"""
  339. country = Column(Unicode(255))
  340. """Country"""
  341. locale = Column(Unicode(255))
  342. """The Language chosen by a member when filling out the form.
  343. We remember this so we know which language to address her with.
  344. """
  345. date_of_birth = Column(Date(), nullable=False)
  346. email_is_confirmed = Column(Boolean, default=False)
  347. email_confirm_code = Column(Unicode(255), unique=True) # reference code
  348. """The Code used as reference when registering for membership:
  349. * contained in URL of link applicants have to use to get their PDF.
  350. * used for bank transfer reference
  351. """
  352. email_confirm_token = Column(Unicode(255), unique=True) # token
  353. '''Unicode'''
  354. email_confirm_mail_date = Column(
  355. DateTime(), default=datetime(1970, 1, 1))
  356. # duplicate entries // people submitting at different times
  357. is_duplicate = Column(Boolean, default=False)
  358. """boolean
  359. * flag for entries that are known duplicates of other applications
  360. if person has already applied before.
  361. """
  362. is_duplicate_of = Column(Integer, nullable=True)
  363. """Integer
  364. * id of entry considered as original or relevant for membership
  365. """
  366. # shares
  367. num_shares = Column(Integer())
  368. """Integer
  369. * The number of shares from the time of afm submission
  370. * Then application is approved, this number of shares is turned into
  371. an entry in the shares list below, referencing a shares package
  372. which is persisted in the Shares table.
  373. .. note:: For accepted members, this is not necessarily the total
  374. number of shares, as members can hold several packages,
  375. from different times of acquisition.
  376. """
  377. date_of_submission = Column(DateTime(), nullable=False)
  378. """datetime
  379. * the date and time this application was submitted
  380. """
  381. signature_received = Column(Boolean, default=False)
  382. """Boolean
  383. * Has the signature been received?
  384. """
  385. signature_received_date = Column(
  386. DateTime(), default=datetime(1970, 1, 1))
  387. """datetime
  388. * the date and time this application was submitted
  389. """
  390. signature_confirmed = Column(Boolean, default=False)
  391. """Boolean
  392. * Has reception of signed form been confirmed?
  393. """
  394. signature_confirmed_date = Column(
  395. DateTime(), default=datetime(1970, 1, 1))
  396. """datetime
  397. * the date and time arrival of signed form was confirmed per email
  398. """
  399. payment_received = Column(Boolean, default=False)
  400. """Boolean
  401. * Has the payment been received?
  402. """
  403. payment_received_date = Column(
  404. DateTime(), default=datetime(1970, 1, 1))
  405. """datetime
  406. * the date and time payment for this application was received
  407. """
  408. payment_confirmed = Column(Boolean, default=False)
  409. """Boolean
  410. * Has the payment been confirmed?
  411. """
  412. payment_confirmed_date = Column(
  413. DateTime(), default=datetime(1970, 1, 1))
  414. """datetime
  415. * the date and time this application was confirmed per email
  416. """
  417. # shares in other table
  418. shares = relationship(
  419. Shares,
  420. secondary=members_shares,
  421. backref="members"
  422. )
  423. """relation
  424. * list of shares packages a member has acquired.
  425. * has entries as soon as an application for membership has been approved
  426. by the board of directors -- and the relevant date of approval
  427. has been entered into the system by staff.
  428. """
  429. # reminders
  430. sent_signature_reminder = Column(Integer, default=0)
  431. """Integer
  432. * stores how many signature reminders have been sent out
  433. """
  434. sent_signature_reminder_date = Column(
  435. DateTime(), default=datetime(1970, 1, 1))
  436. """DateTime
  437. * stores *when* the last signature reminder was sent out
  438. """
  439. sent_payment_reminder = Column(Integer, default=0)
  440. """Integer
  441. * stores how many payment reminders have been sent out
  442. """
  443. sent_payment_reminder_date = Column(
  444. DateTime(), default=datetime(1970, 1, 1))
  445. """DateTime
  446. * stores *when* the last payment reminder was sent out
  447. """
  448. # comment
  449. accountant_comment = Column(Unicode(255))
  450. # membership information
  451. membership_type = Column(Unicode(255))
  452. """Unicode
  453. * Type of membership. either one of
  454. * normal (persons, artists)
  455. * investing (non-artist persons or legal entities)
  456. """
  457. member_of_colsoc = Column(Boolean, default=False)
  458. """Boolean
  459. * is member of other collecting society
  460. """
  461. name_of_colsoc = Column(Unicode(255))
  462. """Unicode
  463. * name(s) of other collecting societies
  464. """
  465. # acquisition of membership
  466. membership_accepted = Column(Boolean, default=False)
  467. """Boolean
  468. * has the membersip been accepted by the board of directors?
  469. """
  470. membership_date = Column(Date(), default=date(1970, 1, 1))
  471. """Date
  472. Date of membership approval by the board.
  473. """
  474. membership_number = Column(Integer())
  475. """Integer
  476. * Membership Number given upon approval.
  477. """
  478. # ## loss of membership
  479. # the date on which the membership terminates, i.e. the date of
  480. # membership and the day after which the membership does no longer exist
  481. membership_loss_date = Column(Date())
  482. # the membership can be lost upon:
  483. # - resignation
  484. # - expulsion
  485. # - death
  486. # - bankruptcy
  487. # - transfer of remaining shares
  488. membership_loss_type = Column(Unicode(255))
  489. # startnex repair operations
  490. mtype_confirm_token = Column(Unicode(255))
  491. mtype_email_date = Column(DateTime(), default=datetime(1970, 1, 1))
  492. # invitations
  493. email_invite_flag_bcgv14 = Column(Boolean, default=False)
  494. email_invite_date_bcgv14 = Column(DateTime(), default=datetime(1970, 1, 1))
  495. email_invite_flag_bcgv15 = Column(Boolean, default=False)
  496. email_invite_date_bcgv15 = Column(DateTime(), default=datetime(1970, 1, 1))
  497. email_invite_token_bcgv15 = Column(Unicode(255))
  498. email_invite_flag_bcgv16 = Column(Boolean, default=False)
  499. email_invite_date_bcgv16 = Column(DateTime(), default=datetime(1970, 1, 1))
  500. email_invite_token_bcgv16 = Column(Unicode(255))
  501. email_invite_flag_bcgv17 = Column(Boolean, default=False)
  502. email_invite_date_bcgv17 = Column(DateTime(), default=datetime(1970, 1, 1))
  503. email_invite_token_bcgv17 = Column(Unicode(255))
  504. email_invite_flag_bcgv18 = Column(Boolean, default=False)
  505. email_invite_date_bcgv18 = Column(DateTime(), default=datetime(1970, 1, 1))
  506. email_invite_token_bcgv18 = Column(Unicode(255))
  507. # legal entities
  508. is_legalentity = Column(Boolean, default=False)
  509. court_of_law = Column(Unicode(255))
  510. registration_number = Column(Unicode(255))
  511. # membership certificate
  512. certificate_email = Column(Boolean, default=False)
  513. certificate_token = Column(Unicode(10))
  514. certificate_email_date = Column(DateTime())
  515. # membership dues for 2015
  516. dues15_invoice = Column(Boolean, default=False) # sent?
  517. dues15_invoice_date = Column(DateTime()) # when?
  518. dues15_invoice_no = Column(Integer()) # lfd. nummer
  519. dues15_token = Column(Unicode(10)) # access token
  520. dues15_start = Column(Unicode(255)) # a string, 2015 quarter of membership
  521. dues15_amount = Column( # calculated amount member has to pay by default
  522. DatabaseDecimal(12, 2), default=Decimal('NaN'))
  523. dues15_reduced = Column(Boolean, default=False) # was reduced?
  524. _dues15_amount_reduced = Column(
  525. 'dues15_amount_reduced', # the amount reduced to
  526. DatabaseDecimal(12, 2), default=Decimal('NaN')) # ..to xs
  527. # balance
  528. _dues15_balance = Column(
  529. 'dues15_balance', # the amount to be settled
  530. DatabaseDecimal(12, 2), default=Decimal('0'))
  531. dues15_balanced = Column(Boolean, default=True) # was balanced?
  532. # payment
  533. dues15_paid = Column(Boolean, default=False) # payment flag
  534. dues15_amount_paid = Column( # how much paid?
  535. DatabaseDecimal(12, 2), default=Decimal('0'))
  536. dues15_paid_date = Column(DateTime()) # paid when?
  537. # membership dues for 2016
  538. dues16_invoice = Column(Boolean, default=False) # sent?
  539. dues16_invoice_date = Column(DateTime()) # when?
  540. dues16_invoice_no = Column(Integer()) # lfd. nummer
  541. dues16_token = Column(Unicode(10)) # access token
  542. dues16_start = Column(Unicode(255)) # a string, 2016 quarter of membership
  543. dues16_amount = Column( # calculated amount member has to pay by default
  544. DatabaseDecimal(12, 2), default=Decimal('NaN'))
  545. dues16_reduced = Column(Boolean, default=False) # was reduced?
  546. _dues16_amount_reduced = Column(
  547. 'dues16_amount_reduced', # the amount reduced to
  548. DatabaseDecimal(12, 2), default=Decimal('NaN')) # ..to xs
  549. # balance
  550. _dues16_balance = Column(
  551. 'dues16_balance', # the amount to be settled
  552. DatabaseDecimal(12, 2), default=Decimal('0'))
  553. dues16_balanced = Column(Boolean, default=True) # was balanced?
  554. # payment
  555. dues16_paid = Column(Boolean, default=False) # payment flag
  556. dues16_amount_paid = Column( # how much paid?
  557. DatabaseDecimal(12, 2), default=Decimal('0'))
  558. dues16_paid_date = Column(DateTime()) # paid when?
  559. # membership dues for 2017
  560. dues17_invoice = Column(Boolean, default=False) # sent?
  561. dues17_invoice_date = Column(DateTime()) # when?
  562. dues17_invoice_no = Column(Integer()) # lfd. nummer
  563. dues17_token = Column(Unicode(10)) # access token
  564. dues17_start = Column(Unicode(255)) # a string, 2017 quarter of membership
  565. dues17_amount = Column( # calculated amount member has to pay by default
  566. DatabaseDecimal(12, 2), default=Decimal('NaN'))
  567. dues17_reduced = Column(Boolean, default=False) # was reduced?
  568. _dues17_amount_reduced = Column(
  569. 'dues17_amount_reduced', # the amount reduced to
  570. DatabaseDecimal(12, 2), default=Decimal('NaN')) # ..to xs
  571. # balance
  572. _dues17_balance = Column(
  573. 'dues17_balance', # the amount to be settled
  574. DatabaseDecimal(12, 2), default=Decimal('0'))
  575. dues17_balanced = Column(Boolean, default=True) # was balanced?
  576. # payment
  577. dues17_paid = Column(Boolean, default=False) # payment flag
  578. dues17_amount_paid = Column( # how much paid?
  579. DatabaseDecimal(12, 2), default=Decimal('0'))
  580. dues17_paid_date = Column(DateTime()) # paid when?
  581. member_type = Column(Unicode(255))
  582. """type of the member, like individual, commercial entity"""
  583. fee = Column(DatabaseDecimal(12, 2), default=Decimal('NaN'))
  584. """membership fee"""
  585. def __init__(self, **kwargs):
  586. print(kwargs)
  587. self.firstname = kwargs.pop('firstname')
  588. self.lastname = kwargs.pop('lastname')
  589. self.email = kwargs.pop('email')
  590. self.password = kwargs.pop('password')
  591. self.last_password_change = datetime.now()
  592. self.address1 = kwargs.pop('address1')
  593. self.address2 = kwargs.pop('address2')
  594. self.postcode = kwargs.pop('postcode')
  595. self.city = kwargs.pop('city')
  596. self.country = kwargs.pop('country')
  597. self.locale = kwargs.pop('locale')
  598. self.date_of_birth = kwargs.pop('date_of_birth')
  599. self.email_is_confirmed = kwargs.pop('email_is_confirmed')
  600. self.email_confirm_code = kwargs.pop('email_confirm_code')
  601. self.num_shares = kwargs.pop('num_shares')
  602. self.date_of_submission = kwargs.pop('date_of_submission')
  603. self.signature_received = False
  604. self.payment_received = False
  605. if customization.membership_types and len(customization.membership_types) > 1:
  606. self.membership_type = kwargs.pop('membership_type')
  607. if self.member_of_colsoc is True:
  608. self.name_of_colsoc = kwargs.pop('name_of_colsoc')
  609. else:
  610. self.name_of_colsoc = u''
  611. try:
  612. if len(customization.membership_types) <= 1:
  613. raise ValueError
  614. except NameError, ValueError:
  615. pass
  616. else:
  617. self.membership_type = kwargs.pop('membership_type')
  618. if len(customization.membership_fees)>1:
  619. self.fee = kwargs.pop('fee')
  620. self.member_type = kwargs.pop('member_type')
  621. if len(kwargs)!=0:
  622. raise TypeError('__init__ did not consume all arguments')
  623. def _get_password(self):
  624. return self._password
  625. def _set_password(self, password):
  626. self._password = hash_password(password)
  627. password = property(_get_password, _set_password)
  628. password = synonym('_password', descriptor=password)
  629. @hybrid_property
  630. def dues15_balance(self):
  631. """
  632. TODO: write this docstring
  633. TODO: write testcase in test_models.py
  634. """
  635. return self._dues15_balance
  636. @dues15_balance.setter
  637. def dues15_balance(self, dues15_balance):
  638. """
  639. TODO: write this docstring
  640. TODO: write testcase in test_models.py
  641. """
  642. self._dues15_balance = dues15_balance
  643. self.dues15_balanced = self._dues15_balance == Decimal('0')
  644. @hybrid_property
  645. def dues15_amount_reduced(self):
  646. """
  647. TODO: write this docstring
  648. TODO: write testcase in test_models.py
  649. """
  650. return self._dues15_amount_reduced
  651. @dues15_amount_reduced.setter
  652. def dues15_amount_reduced(self, dues15_amount_reduced):
  653. """
  654. TODO: write this docstring
  655. TODO: write testcase in test_models.py
  656. """
  657. self._dues15_amount_reduced = dues15_amount_reduced
  658. self.dues15_reduced = (
  659. not math.isnan(self.dues15_amount_reduced)
  660. and
  661. self.dues15_amount_reduced != self.dues15_amount)
  662. @hybrid_property
  663. def dues16_balance(self):
  664. """
  665. TODO: write this docstring
  666. TODO: write testcase in test_models.py
  667. """
  668. return self._dues16_balance
  669. @dues16_balance.setter
  670. def dues16_balance(self, dues16_balance):
  671. """
  672. TODO: write this docstring
  673. TODO: write testcase in test_models.py
  674. """
  675. self._dues16_balance = dues16_balance
  676. self.dues16_balanced = self._dues16_balance == Decimal('0')
  677. @hybrid_property
  678. def dues16_amount_reduced(self):
  679. """
  680. TODO: write this docstring
  681. TODO: write testcase in test_models.py
  682. """
  683. return self._dues16_amount_reduced
  684. @dues16_amount_reduced.setter
  685. def dues16_amount_reduced(self, dues16_amount_reduced):
  686. """
  687. TODO: write this docstring
  688. TODO: write testcase in test_models.py
  689. """
  690. self._dues16_amount_reduced = dues16_amount_reduced
  691. self.dues16_reduced = \
  692. not math.isnan(self.dues16_amount_reduced) \
  693. and \
  694. self.dues16_amount_reduced != self.dues16_amount
  695. @hybrid_property
  696. def dues17_balance(self):
  697. """
  698. TODO: write this docstring
  699. TODO: write testcase in test_models.py
  700. """
  701. return self._dues17_balance
  702. @dues17_balance.setter
  703. def dues17_balance(self, dues17_balance):
  704. """
  705. TODO: write this docstring
  706. TODO: write testcase in test_models.py
  707. """
  708. self._dues17_balance = dues17_balance
  709. self.dues17_balanced = self._dues17_balance == Decimal('0')
  710. @hybrid_property
  711. def dues17_amount_reduced(self):
  712. """
  713. TODO: write this docstring
  714. TODO: write testcase in test_models.py
  715. """
  716. return self._dues17_amount_reduced
  717. @dues17_amount_reduced.setter
  718. def dues17_amount_reduced(self, dues17_amount_reduced):
  719. """
  720. TODO: write this docstring
  721. TODO: write testcase in test_models.py
  722. """
  723. self._dues17_amount_reduced = dues17_amount_reduced
  724. self.dues17_reduced = \
  725. not math.isnan(self.dues17_amount_reduced) \
  726. and \
  727. self.dues17_amount_reduced != self.dues17_amount
  728. @classmethod
  729. def get_by_code(cls, email_confirm_code):
  730. """
  731. Find a member by confirmation code
  732. This is needed when a user returns from reading her email
  733. and clicking on a link containing the confirmation code.
  734. As the code is unique, one record is returned.
  735. Returns:
  736. object: C3sMember object
  737. """
  738. return DBSession.query(cls).filter(
  739. cls.email_confirm_code == email_confirm_code).first()
  740. # used got barcamp & general assembly invitations
  741. @classmethod
  742. def get_by_bcgvtoken(cls, token):
  743. """
  744. Find a member by token used for GA and BarCamp.
  745. This is needed when a user returns from reading her email
  746. and clicking on a link containing the token.
  747. Returns:
  748. object: C3sMember object
  749. """
  750. return DBSession.query(cls).filter(
  751. cls.email_invite_token_bcgv18 == token).first()
  752. @classmethod
  753. def check_for_existing_confirm_code(cls, email_confirm_code):
  754. """
  755. check if a code is already present
  756. """
  757. check = DBSession.query(cls).filter(
  758. cls.email_confirm_code == email_confirm_code).first()
  759. return check is not None
  760. @classmethod
  761. def get_by_id(cls, member_id):
  762. """
  763. Get one C3sMember object by id.
  764. Returns:
  765. * **C3sMember object**, if id exists.
  766. * **None**, if id does not exist.
  767. """
  768. return DBSession.query(cls).filter(cls.id == member_id).first()
  769. @classmethod
  770. def get_by_email(cls, email):
  771. """return one or more members by email (a list!)"""
  772. return DBSession.query(cls).filter(cls.email == email).all()
  773. @classmethod
  774. def get_by_dues15_token(cls, code):
  775. """return one member by fee token"""
  776. return DBSession.query(cls).filter(cls.dues15_token == code).first()
  777. @classmethod
  778. def get_by_dues16_token(cls, code):
  779. """return one member by fee token"""
  780. return DBSession.query(cls).filter(cls.dues16_token == code).first()
  781. @classmethod
  782. def get_by_dues17_token(cls, code):
  783. """return one member by fee token"""
  784. return DBSession.query(cls).filter(cls.dues17_token == code).first()
  785. @classmethod
  786. def get_all(cls):
  787. """return all afms and members"""
  788. return DBSession.query(cls).all()
  789. # needed for invitation to barcam & general assembly
  790. @classmethod
  791. def get_invitees(cls, num):
  792. """
  793. Get a given number *n* of members to invite for barcamp and GV.
  794. Queries the database for members, where
  795. * members are accepted
  796. * members have not received their invitation email yet
  797. Args:
  798. num is the number *n* of invitees to return
  799. Returns:
  800. a list of *n* member objects
  801. """
  802. return DBSession.query(cls).filter(
  803. and_(
  804. cls.is_member_filter(),
  805. or_(
  806. (cls.email_invite_flag_bcgv18 == 0),
  807. (cls.email_invite_flag_bcgv18 == ''),
  808. # pylint: disable=singleton-comparison
  809. (cls.email_invite_flag_bcgv18 == None),
  810. )
  811. )
  812. ).slice(0, num).all()
  813. @classmethod
  814. def get_dues15_invoicees(cls, num):
  815. """
  816. Get a given number *n* of members to send dues invoices to.
  817. Queries the database for members, where
  818. * members are accepted
  819. * members have not received their dues invoice email yet
  820. Args:
  821. num is the number *n* of C3sMembers to return
  822. Returns:
  823. a list of *n* member objects
  824. """
  825. return DBSession.query(cls).filter(
  826. and_(
  827. cls.membership_accepted == 1,
  828. cls.dues15_invoice == 0
  829. )).slice(0, num).all()
  830. @classmethod
  831. def get_dues16_invoicees(cls, num):
  832. """
  833. Get a given number *n* of members to send dues invoices to.
  834. Queries the database for members, where
  835. * members are accepted
  836. * members have not received their dues invoice email yet
  837. Args:
  838. num is the number *n* of C3sMembers to return
  839. Returns:
  840. a list of *n* member objects
  841. """
  842. return DBSession.query(cls).filter(
  843. and_(
  844. cls.membership_accepted == 1,
  845. cls.dues16_invoice == 0,
  846. cls.membership_date < date(2017, 1, 1),
  847. cls.membership_type.in_([u'normal', u'investing'])
  848. )).slice(0, num).all()
  849. @classmethod
  850. def get_dues17_invoicees(cls, num):
  851. """
  852. Get a given number *n* of members to send dues invoices to.
  853. Queries the database for members, where
  854. * members are accepted
  855. * members have not received their dues invoice email yet
  856. Args:
  857. num is the number *n* of C3sMembers to return
  858. Returns:
  859. a list of *n* member objects
  860. """
  861. # In SqlAlchemy the True comparison must be done as "a == True" and not
  862. # in the python default way "a is True". Therefore:
  863. # pylint: disable=singleton-comparison
  864. return DBSession.query(cls).filter(
  865. and_(
  866. cls.membership_accepted == True,
  867. cls.dues17_invoice == False,
  868. cls.membership_date < date(2018, 1, 1),
  869. cls.membership_type.in_([u'normal', u'investing']),
  870. or_(
  871. cls.membership_loss_date == None,
  872. cls.membership_loss_date >= date(2017, 1, 1),
  873. ),
  874. )).slice(0, num).all()
  875. @classmethod
  876. def delete_by_id(cls, member_id):
  877. """
  878. Delete one C3sMember entry by id.
  879. Args:
  880. _id: the id to delete
  881. Returns:
  882. * **1** on success
  883. * **0** else
  884. """
  885. return DBSession.query(cls).filter(cls.id == member_id).delete()
  886. # listings
  887. @classmethod
  888. def get_duplicates(cls):
  889. """
  890. Get all duplicates: C3sMember entries tagged as duplicates.
  891. Used in:
  892. membership_list, statistics_view
  893. Returns:
  894. list: list of C3sMember entries flagged as duplicates.
  895. """
  896. return DBSession.query(cls).filter(
  897. cls.is_duplicate == 1).all()
  898. @classmethod
  899. def get_members(cls, order_by, how_many=10, offset=0, order="asc"):
  900. """
  901. Compute a list of C3sMember items with membership accepted (Query!).
  902. Args:
  903. order_by: which column to sort on, e.g. "id"
  904. how_many: number of entries (Integer)
  905. offset: how many to omit (leave out first n; default is 0)
  906. order: either "asc" (ascending, **default**) or "desc" (descending)
  907. Raises:
  908. Exception: invalid value for "order_by" or "order".
  909. Returns:
  910. query: C3sMember database query
  911. """
  912. try:
  913. attr = getattr(cls, order_by)
  914. order_function = getattr(attr, order)
  915. except:
  916. raise Exception("Invalid order_by ({0}) or order value "
  917. "({1})".format(order_by, order))
  918. count = int(offset) + int(how_many)
  919. offset = int(offset)
  920. query = DBSession.query(cls).filter(
  921. cls.membership_accepted == 1
  922. ).order_by(order_function()).slice(offset, count)
  923. return query
  924. # statistical stuff
  925. @classmethod
  926. def get_postal_codes_de(cls):
  927. """
  928. Get postal codes of C3sMember entries from germany
  929. Returns:
  930. bag (list containing duplicates): postal codes in DE"""
  931. rows = DBSession.query(cls).filter(
  932. cls.country == 'DE'
  933. ).all()
  934. postal_codes_de = []
  935. for row in rows:
  936. try:
  937. int(row.postcode)
  938. if len(row.postcode) == 5:
  939. postal_codes_de.append(row.postcode)
  940. except ValueError:
  941. print("exception at id {}: {}".format(
  942. row.id,
  943. row.postcode))
  944. return postal_codes_de
  945. # statistical stuff
  946. @classmethod
  947. def get_number(cls):
  948. """
  949. Count number of entries in C3sMember table (by counting rows)
  950. Used in:
  951. statistics_view, membership_list, import_export, some tests...
  952. Returns:
  953. Integer: number
  954. """
  955. return DBSession.query(cls).count()
  956. @classmethod
  957. def get_num_members_accepted(cls):
  958. """
  959. Count the entries that have actually been accepted as members.
  960. Used in:
  961. statistics_view, membership_list
  962. Returns:
  963. Integer: number
  964. """
  965. return DBSession.query(
  966. cls).filter(cls.is_member_filter()).count()
  967. @classmethod
  968. def get_num_non_accepted(cls):
  969. """
  970. Count the applications that have **not** been accepted as members.
  971. TODO: how about duplicates!?
  972. Returns:
  973. Integer: number of C3sMember entries.
  974. """
  975. return DBSession.query(cls).filter(
  976. not_(cls.membership_accepted_filter())).count()
  977. @classmethod
  978. def get_num_mem_nat_acc(cls):
  979. """
  980. Count the *persons* that have actually been accepted as members.
  981. Used in:
  982. statistics_view
  983. Returns:
  984. Integer: number
  985. """
  986. return DBSession.query(cls).filter(
  987. cls.is_legalentity == 0,
  988. cls.is_member_filter(),
  989. ).count()
  990. @classmethod
  991. def get_num_mem_jur_acc(cls):
  992. """
  993. Count the *legal entities* that have actually been accepted as members.
  994. Used in:
  995. statistics_view
  996. Returns:
  997. Integer: number
  998. """
  999. return DBSession.query(cls).filter(
  1000. cls.is_legalentity == 1,
  1001. cls.is_member_filter(),
  1002. ).count()
  1003. @classmethod
  1004. def get_num_mem_norm(cls):
  1005. """
  1006. Count the memberships that are normal members.
  1007. Used in:
  1008. statistics_view
  1009. Returns:
  1010. Integer: number
  1011. """
  1012. return DBSession.query(cls).filter(
  1013. cls.is_member_filter(),
  1014. cls.membership_type == u'normal'
  1015. ).count()
  1016. @classmethod
  1017. def get_num_mem_invest(cls):
  1018. """
  1019. Count the memberships that are investing members.
  1020. Used in:
  1021. statistics_view
  1022. Returns:
  1023. Integer: number
  1024. """
  1025. return DBSession.query(cls).filter(
  1026. cls.is_member_filter(),
  1027. cls.membership_type == u'investing'
  1028. ).count()
  1029. @classmethod
  1030. def get_num_mem_other_features(cls):
  1031. """
  1032. Count the memberships that are neither normal nor investing members.
  1033. Used in:
  1034. statistics_view
  1035. Returns:
  1036. Integer: number
  1037. """
  1038. return DBSession.query(cls).filter(
  1039. cls.is_member_filter(),
  1040. cls.membership_type != u'normal',
  1041. cls.membership_type != u'investing'
  1042. ).count()
  1043. @classmethod
  1044. def get_num_membership_lost(cls):
  1045. """
  1046. Gets the number of members which lost membership before today.
  1047. Returns:
  1048. Integer: number
  1049. """
  1050. return DBSession.query(cls).filter(cls.membership_lost_filter()).count()
  1051. # listings
  1052. @classmethod
  1053. def member_listing(cls, order_by, how_many=10, offset=0, order="asc"):
  1054. """
  1055. Compute a list of C3sMember items (Query!).
  1056. Note:
  1057. these are not necessarily accepted members!
  1058. Used in:
  1059. membership_list, import_export
  1060. Args:
  1061. order_by: which column to sort on, e.g. "id"
  1062. how_many: number of entries (Integer)
  1063. offset: how many to omit (leave out first n; default is 0)
  1064. order: either "asc" (ascending, **default**) or "desc" (descending)
  1065. Raises:
  1066. Exception: invalid value for "order_by" or "order".
  1067. Returns:
  1068. query: C3sMember database query
  1069. """
  1070. try:
  1071. attr = getattr(cls, order_by)
  1072. order_function = getattr(attr, order)
  1073. except:
  1074. raise Exception("Invalid order_by ({0}) or order value "
  1075. "({1})".format(order_by, order))
  1076. count = int(offset) + int(how_many)
  1077. offset = int(offset)
  1078. query = DBSession.query(cls).order_by(order_function())\
  1079. .slice(offset, count)
  1080. return query
  1081. @classmethod
  1082. def get_range_ids(cls, order_by, first_id, last_id, order="asc"):
  1083. """
  1084. Get a list of C3sMember items by range of ids.
  1085. Used in:
  1086. membership_list
  1087. Args:
  1088. order_by: which column to sort on, e.g. "id"
  1089. first_id: id of first entry (Integer)
  1090. last_id: id of last entry (Integer)
  1091. order: either "asc" (ascending, **default**) or "desc" (descending)
  1092. Raises:
  1093. Exception: invalid value for "order_by" or "order".
  1094. Returns:
  1095. list: C3sMembership objects
  1096. """
  1097. try:
  1098. attr = getattr(cls, order_by)
  1099. order_function = getattr(attr, order)
  1100. except:
  1101. raise Exception("Invalid order_by ({0}) or order value "
  1102. "({1})".format(order_by, order))
  1103. query = DBSession.query(cls).filter(
  1104. and_(
  1105. cls.id >= first_id,
  1106. cls.id <= last_id,
  1107. )
  1108. ).order_by(order_function()).all()
  1109. return query
  1110. @classmethod
  1111. def nonmember_listing(cls, offset, page_size, sort_property,
  1112. sort_direction='asc'):
  1113. """
  1114. Retrieve a list of members which are **not** accepted.
  1115. Note:
  1116. These are membership applicants which have not been accepted, yet.
  1117. Used in:
  1118. accountants_views
  1119. Args:
  1120. offset: How many to omit (leave out first n; default is 0)
  1121. page_size: Number of entries per page (Integer)
  1122. sort_property: Which column to sort on, e.g. "id"
  1123. sort_direction: Either "asc" (ascending, **default**) or "desc"
  1124. (descending)
  1125. Raises:
  1126. InvalidPropertyException: The sort property does not exist.
  1127. InvalidSortDirection: The sort direction is invalid.
  1128. Returns:
  1129. List of C3sMember objects.
  1130. """
  1131. try:
  1132. sort_attribute = getattr(cls, sort_property)
  1133. except AttributeError:
  1134. raise InvalidPropertyException(
  1135. 'C3sMember does not have a property named "{0}".'.format(
  1136. sort_property))
  1137. try:
  1138. order_function = getattr(sort_attribute, sort_direction)
  1139. except AttributeError:
  1140. raise InvalidSortDirection(
  1141. 'Invalid sort direction: {0}'.format(sort_direction))
  1142. query = DBSession.query(cls).filter(
  1143. or_(
  1144. cls.membership_accepted == 0,
  1145. cls.membership_accepted == '',
  1146. # pylint: disable=singleton-comparison
  1147. # noqa
  1148. cls.membership_accepted == None,
  1149. )
  1150. ).order_by(
  1151. order_function()
  1152. ).slice(offset, offset + page_size)
  1153. return query.all()
  1154. @classmethod
  1155. def nonmember_listing_count(cls):
  1156. """
  1157. Gets the number of applicants which have not been accepted as members
  1158. yet.
  1159. """
  1160. query = DBSession.query(cls).filter(
  1161. or_(
  1162. cls.membership_accepted == 0,
  1163. cls.membership_accepted == '',
  1164. # pylint: disable=singleton-comparison
  1165. # noqa
  1166. cls.membership_accepted == None,
  1167. )
  1168. ).count()
  1169. return query
  1170. # count for statistics
  1171. @classmethod
  1172. def afm_num_shares_unpaid(cls):
  1173. """
  1174. Gets the number of shares for which membership applicant has not yet
  1175. paid the price.
  1176. """
  1177. rows = DBSession.query(cls).all()
  1178. num_shares_unpaid = 0
  1179. for row in rows:
  1180. if not row.payment_received:
  1181. num_shares_unpaid += row.num_shares
  1182. return num_shares_unpaid
  1183. @classmethod
  1184. def afm_num_shares_paid(cls):
  1185. """
  1186. Gets the number of shares for which membership applicant has already
  1187. paid the price.
  1188. """
  1189. rows = DBSession.query(cls).all()
  1190. num_shares_paid = 0
  1191. for row in rows:
  1192. if row.payment_received:
  1193. num_shares_paid += row.num_shares
  1194. return num_shares_paid
  1195. # workflow: need approval by the board
  1196. @classmethod
  1197. def afms_ready_for_approval(cls):
  1198. """
  1199. Gets the list of membership applicants who can be granted membership by
  1200. the board of directors because they have fulfilled their duty of
  1201. sending in a signed membership application as well as paying the
  1202. share's price.
  1203. """
  1204. return DBSession.query(cls).filter(
  1205. and_(
  1206. (cls.membership_accepted == 0),
  1207. (cls.signature_received),
  1208. (cls.payment_received),
  1209. )).all()
  1210. # autocomplete
  1211. @classmethod
  1212. def get_matching_codes(cls, prefix):
  1213. """
  1214. Return only codes matching the prefix.
  1215. This is used in the autocomplete form to search for C3sMember entries.
  1216. Returns:
  1217. list of strings
  1218. """
  1219. rows = DBSession.query(cls).all()
  1220. codes = []
  1221. for row in rows:
  1222. if row.email_confirm_code.startswith(prefix):
  1223. codes.append(row.email_confirm_code)
  1224. return codes
  1225. @classmethod
  1226. def check_password(cls, member_id, password):
  1227. """
  1228. Check a password against the database.
  1229. Args:
  1230. member_id: C3sMember entry id.
  1231. password: a password supplied
  1232. Returns:
  1233. the answer of bcrypt.crypt, comparing the password supplied
  1234. and the hash from the database
  1235. """
  1236. member = cls.get_by_id(member_id)
  1237. return CRYPT.check(member.password, password)
  1238. # this one is used by RequestWithUserAttribute
  1239. @classmethod
  1240. def check_user_or_none(cls, member_id):
  1241. """
  1242. Check whether a user by that username exists in the database.
  1243. Used in:
  1244. security.request.RequestWithUserAttribute
  1245. Args:
  1246. member_id: id of C3sMember entry.
  1247. Returns:
  1248. object, if id exists, else None.
  1249. None: if id
  1250. """
  1251. login = cls.get_by_id(member_id) # is None if user not exists
  1252. return login
  1253. # for merge comparisons
  1254. @classmethod
  1255. def get_same_lastnames(cls, lastname):
  1256. """return list of accepted members with same lastnames"""
  1257. return DBSession.query(cls).filter(
  1258. and_(
  1259. cls.membership_accepted == 1,
  1260. cls.lastname == lastname
  1261. )).all()
  1262. @classmethod
  1263. def get_same_firstnames(cls, firstname):
  1264. """return list of accepted members with same fistnames"""
  1265. return DBSession.query(cls).filter(
  1266. and_(
  1267. cls.membership_accepted == 1,
  1268. cls.firstname == firstname
  1269. )).all()
  1270. @classmethod
  1271. def get_same_email(cls, email):
  1272. """return list of accepted members with same email"""
  1273. return DBSession.query(cls).filter(
  1274. and_(
  1275. cls.membership_accepted == 1,
  1276. cls.email == email,
  1277. )).all()
  1278. @classmethod
  1279. def get_same_date_of_birth(cls, date_of_birth):
  1280. """return list of accepted members with same date of birth"""
  1281. return DBSession.query(cls).filter(
  1282. and_(
  1283. cls.membership_accepted == 1,
  1284. cls.date_of_birth == date_of_birth,
  1285. )).all()
  1286. # membership numbers etc.
  1287. @classmethod
  1288. def get_num_membership_numbers(cls):
  1289. """
  1290. count the number of membership numbers
  1291. """
  1292. return DBSession.query(cls).filter(cls.membership_number).count()
  1293. @classmethod
  1294. def get_next_free_membership_number(cls):
  1295. """
  1296. returns the next free membership number
  1297. """
  1298. return C3sMember.get_highest_membership_number() + 1
  1299. @classmethod
  1300. def get_highest_membership_number(cls):
  1301. """
  1302. get the highest membership number
  1303. """
  1304. rows = DBSession.query(cls.membership_number).filter(
  1305. cls.membership_number != None).all() # noqa
  1306. membership_numbers = []
  1307. for row in rows:
  1308. membership_numbers.append(int(row[0]))
  1309. try:
  1310. max_number = max(membership_numbers)
  1311. except ValueError:
  1312. membership_numbers = [0, 999999999]
  1313. max_number = 999999999
  1314. try:
  1315. assert(max_number == 999999999)
  1316. membership_numbers.remove(max(membership_numbers)) # remove known maximum
  1317. except AssertionError:
  1318. pass
  1319. return max(membership_numbers)
  1320. # countries
  1321. @classmethod
  1322. def get_num_countries(cls):
  1323. """return number of countries in DB"""
  1324. return DBSession.query(func.count(distinct(cls.country))).scalar()
  1325. @classmethod
  1326. def get_countries_list(cls):
  1327. """return dict of countries and number of occurrences"""
  1328. countries = {}
  1329. rows = DBSession.query(cls)
  1330. for row in rows:
  1331. if row.country not in countries.keys():
  1332. countries[row.country] = 1
  1333. else:
  1334. countries[row.country] += 1
  1335. return countries
  1336. # autocomplete
  1337. @classmethod
  1338. def get_matching_people(cls, prefix):
  1339. """
  1340. return only entries matchint the prefix
  1341. """
  1342. rows = DBSession.query(cls).all()
  1343. names = {}
  1344. for row in rows:
  1345. if row.lastname.startswith(prefix):
  1346. key = (
  1347. row.email_confirm_code + ' ' +
  1348. row.lastname + ', ' + row.firstname)
  1349. names[key] = key
  1350. return names
  1351. def set_dues15_payment(self, paid_amount, paid_date):
  1352. if math.isnan(self.dues15_amount_paid):
  1353. dues15_amount_paid = Decimal('0')
  1354. else:
  1355. dues15_amount_paid = self.dues15_amount_paid
  1356. self.dues15_paid = True
  1357. self.dues15_amount_paid = dues15_amount_paid + paid_amount
  1358. self.dues15_paid_date = paid_date
  1359. self.dues15_balance = self.dues15_balance - paid_amount
  1360. def set_dues15_amount(self, dues_amount):
  1361. if math.isnan(self.dues15_amount):
  1362. dues15_amount = Decimal('0')
  1363. else:
  1364. dues15_amount = self.dues15_amount
  1365. self.dues15_balance = self.dues15_balance - dues15_amount + Decimal(
  1366. dues_amount) # what they actually have to pay
  1367. self.dues15_amount = dues_amount # what they have to pay (calc'ed)
  1368. def set_dues15_reduced_amount(self, reduced_amount):
  1369. if reduced_amount != self.dues15_amount:
  1370. previous_amount_in_balance = (
  1371. self.dues15_amount_reduced
  1372. if self.dues15_reduced
  1373. else self.dues15_amount)
  1374. self.dues15_balance = self.dues15_balance - \
  1375. previous_amount_in_balance + reduced_amount
  1376. self.dues15_amount_reduced = reduced_amount
  1377. else:
  1378. self.dues15_amount_reduced = Decimal('NaN')
  1379. def set_dues16_payment(self, paid_amount, paid_date):
  1380. if math.isnan(self.dues16_amount_paid):
  1381. dues16_amount_paid = Decimal('0')
  1382. else:
  1383. dues16_amount_paid = self.dues16_amount_paid
  1384. self.dues16_paid = True
  1385. self.dues16_amount_paid = dues16_amount_paid + paid_amount
  1386. self.dues16_paid_date = paid_date
  1387. self.dues16_balance = self.dues16_balance - paid_amount
  1388. def set_dues16_amount(self, dues_amount):
  1389. if math.isnan(self.dues16_amount):
  1390. dues16_amount = Decimal('0')
  1391. else:
  1392. dues16_amount = self.dues16_amount
  1393. self.dues16_balance = self.dues16_balance - dues16_amount + Decimal(
  1394. dues_amount) # what they actually have to pay
  1395. self.dues16_amount = dues_amount # what they have to pay (calc'ed)
  1396. def set_dues16_reduced_amount(self, reduced_amount):
  1397. if reduced_amount != self.dues16_amount:
  1398. previous_amount_in_balance = (
  1399. self.dues16_amount_reduced
  1400. if self.dues16_reduced
  1401. else self.dues16_amount)
  1402. self.dues16_balance = self.dues16_balance - \
  1403. previous_amount_in_balance + reduced_amount
  1404. self.dues16_amount_reduced = reduced_amount
  1405. else:
  1406. self.dues16_amount_reduced = Decimal('NaN')
  1407. def set_dues17_payment(self, paid_amount, paid_date):
  1408. if math.isnan(self.dues17_amount_paid):
  1409. dues17_amount_paid = Decimal('0')
  1410. else:
  1411. dues17_amount_paid = self.dues17_amount_paid
  1412. self.dues17_paid = True
  1413. self.dues17_amount_paid = dues17_amount_paid + paid_amount
  1414. self.dues17_paid_date = paid_date
  1415. self.dues17_balance = self.dues17_balance - paid_amount
  1416. def set_dues17_amount(self, dues_amount):
  1417. if math.isnan(self.dues17_amount):
  1418. dues17_amount = Decimal('0')
  1419. else:
  1420. dues17_amount = self.dues17_amount
  1421. self.dues17_balance = self.dues17_balance - dues17_amount + Decimal(
  1422. dues_amount) # what they actually have to pay
  1423. self.dues17_amount = dues_amount # what they have to pay (calc'ed)
  1424. def set_dues17_reduced_amount(self, reduced_amount):
  1425. if reduced_amount != self.dues17_amount:
  1426. previous_amount_in_balance = (
  1427. self.dues17_amount_reduced
  1428. if self.dues17_reduced
  1429. else self.dues17_amount)
  1430. self.dues17_balance = self.dues17_balance - \
  1431. previous_amount_in_balance + reduced_amount
  1432. self.dues17_amount_reduced = reduced_amount
  1433. else:
  1434. self.dues17_amount_reduced = Decimal('NaN')
  1435. def get_url_safe_name(self):
  1436. """
  1437. Gets a url-safe version of the member's name in which all characters
  1438. except 0-9, a-z and A-Z are replaced by a dash.
  1439. """
  1440. return re.sub( # # replace characters
  1441. '[^0-9a-zA-Z]', # other than these
  1442. '-', # with a -
  1443. self.lastname if self.is_legalentity else (
  1444. self.lastname + self.firstname))
  1445. def is_member(self, effective_date=None):
  1446. """
  1447. Indicates whether the entity is still a member.
  1448. For being a member the membership must have been accepted and the
  1449. membership must not have been lost.
  1450. """
  1451. if effective_date is None:
  1452. effective_date = date.today()
  1453. membership_lost = (
  1454. self.membership_loss_date is not None
  1455. and
  1456. self.membership_loss_date < effective_date)
  1457. membership_accepted = (
  1458. self.membership_accepted
  1459. and
  1460. self.membership_date is not None
  1461. and
  1462. self.membership_date <= effective_date)
  1463. return membership_accepted and not membership_lost
  1464. @classmethod
  1465. def membership_lost_filter(cls, effective_date=None):
  1466. """
  1467. Provides a SqlAlchemy filter only matching entities which lost
  1468. membership before the specified effective date.
  1469. Args:
  1470. effective_date: Optional. The date before which the membership was
  1471. lost. If not specified then the current date of is used.
  1472. Returns:
  1473. A filter only matching entities which lost membership before the
  1474. specified effective date.
  1475. """
  1476. if effective_date is None:
  1477. effective_date = date.today()
  1478. return and_(
  1479. cls.membership_loss_date != None,
  1480. cls.membership_loss_date < effective_date)
  1481. @classmethod
  1482. def membership_accepted_filter(cls, effective_date=None):
  1483. """
  1484. Provides a SqlAlchemy filter only matching entities which had their
  1485. membership accepted before or on the specified effective date.
  1486. Args:
  1487. effective_date: Optional. The date before or on which the membership
  1488. was accepted. If not specified then the current date of is used.
  1489. Returns:
  1490. A filter only matching entities which had their membership accepted
  1491. before or on the specified effective date.
  1492. """
  1493. if effective_date is None:
  1494. effective_date = date.today()
  1495. return and_(
  1496. cls.membership_accepted,
  1497. cls.membership_date != None,
  1498. cls.membership_date <= effective_date)
  1499. @classmethod
  1500. def is_member_filter(cls, effective_date=None):
  1501. """
  1502. Provides a SqlAlchemy filter only matching entities which are members at
  1503. the specified effective date.
  1504. For being a member the membership must have been accepted and the
  1505. membership must not have been lost.
  1506. Args:
  1507. effective_date: Optional. The date for which the membership status
  1508. is checked. If not specified then the current date of is used.
  1509. Returns:
  1510. A filter matching all entities which are members as the specified
  1511. effective date.
  1512. """
  1513. if effective_date is None:
  1514. effective_date = date.today()
  1515. return and_(
  1516. cls.membership_accepted_filter(effective_date),
  1517. not_(cls.membership_lost_filter(effective_date)))
  1518. class Dues15Invoice(Base):
  1519. """
  1520. This table stores the invoices for the 2015 version of dues.
  1521. We need this for bookkeeping,
  1522. because whenever a member is granted a reduction of her dues,
  1523. the old invoice is canceled by a reversal invoice
  1524. and a new invoice must be issued.
  1525. Edge case: if reduced to 0, no new invoice needed.
  1526. """
  1527. __tablename__ = 'dues15invoices'
  1528. # pylint: disable=invalid-name
  1529. id = Column(Integer, primary_key=True)
  1530. """tech. id. / no. in table (integer, primary key)"""
  1531. # this invoice
  1532. invoice_no = Column(Integer(), unique=True)
  1533. """invoice number (Integer, unique)"""
  1534. invoice_no_string = Column(Unicode(255), unique=True)
  1535. """invoice number string (unique)"""
  1536. invoice_date = Column(DateTime())
  1537. """timestamp of invoice creation (DateTime)"""
  1538. invoice_amount = Column(DatabaseDecimal(12, 2), default=Decimal('NaN'))
  1539. """amount (DatabaseDecimal(12,2))"""
  1540. # has it been superseeded by reversal?
  1541. is_cancelled = Column(Boolean, default=False)
  1542. """flag: invoice has been superseeded by reversal or cancellation"""
  1543. cancelled_date = Column(DateTime())
  1544. """timestamp of cancellation/reversal"""
  1545. # is it a reversal?
  1546. is_reversal = Column(Boolean, default=False)
  1547. """flag: is this a reversal invoice?"""
  1548. # is it a reduction (or even more than default)?
  1549. is_altered = Column(Boolean, default=False)
  1550. """flag: has the amount been reduced or increased?"""
  1551. # person reference
  1552. member_id = Column(Integer())
  1553. """reference to C3sMember id"""
  1554. membership_no = Column(Integer())
  1555. """reference to C3sMember membership_number"""
  1556. email = Column(Unicode(255))
  1557. """C3sMembers email we sent this invoice to"""
  1558. token = Column(Unicode(255))
  1559. """used to limit access to this invoice"""
  1560. # referrals
  1561. preceding_invoice_no = Column(Integer(), default=None)
  1562. """the invoice number preceeding this one, if applicable"""
  1563. succeeding_invoice_no = Column(Integer(), default=None)
  1564. """the invoice number succeeding this one, if applicable"""
  1565. def __init__(
  1566. self,
  1567. invoice_no,
  1568. invoice_no_string,
  1569. invoice_date,
  1570. invoice_amount,
  1571. member_id,
  1572. membership_no,
  1573. email,
  1574. token):
  1575. """
  1576. Make a new invoice object
  1577. Args:
  1578. invoice_no: invoice number
  1579. invoice_no_string: invoice number string
  1580. invoice_date: timestamp of creation
  1581. invoice_amount: amount of money
  1582. member_id: references C3sMember
  1583. membership_no: references C3sMember
  1584. email: email to send it to
  1585. token: a token to limit access
  1586. """
  1587. self.invoice_no = invoice_no
  1588. self.invoice_no_string = invoice_no_string
  1589. self.invoice_date = invoice_date
  1590. self.invoice_amount = invoice_amount
  1591. self.member_id = member_id
  1592. self.membership_no = membership_no
  1593. self.email = email
  1594. self.token = token
  1595. @classmethod
  1596. def get_all(cls):
  1597. """
  1598. Return all dues15 invoices
  1599. """
  1600. return DBSession.query(cls).all()
  1601. @classmethod
  1602. def get_by_invoice_no(cls, number):
  1603. """return one invoice by invoice number"""
  1604. return DBSession.query(cls).filter(cls.invoice_no == number).first()
  1605. @classmethod
  1606. def get_by_membership_no(cls, number):
  1607. """return all invoices of one member by membership number"""
  1608. return DBSession.query(cls).filter(cls.membership_no == number).all()
  1609. @classmethod
  1610. def get_max_invoice_no(cls):
  1611. """
  1612. Get the maximum invoice number.
  1613. Returns:
  1614. * Integer: maximum of given invoice numbers or 0"""
  1615. res, = DBSession.query(func.max(cls.id)).first()
  1616. if res is None:
  1617. return 0
  1618. return res
  1619. @classmethod
  1620. def check_for_existing_dues15_token(cls, dues_token):
  1621. """
  1622. Check if a dues token is already present.
  1623. Args:
  1624. dues_token: a given string
  1625. Returns:
  1626. * **True**, if token already in table
  1627. * **False** else
  1628. """
  1629. check = DBSession.query(cls).filter(
  1630. cls.token == dues_token).first()
  1631. return check is not None
  1632. @classmethod
  1633. def get_monthly_stats(cls):
  1634. """
  1635. Gets the monthly statistics.
  1636. Provides sums of the normale as well as reversal invoices per
  1637. calendar month based on the invoice date.
  1638. """
  1639. result = []
  1640. # SQLite specific: substring for SQLite as it does not support
  1641. # date_trunc.
  1642. # invoice_date_month = func.date_trunc(
  1643. # 'month',
  1644. # Dues15Invoice.invoice_date)
  1645. invoice_date_month = func.substr(Dues15Invoice.invoice_date, 1, 7)
  1646. payment_date_month = func.substr(C3sMember.dues15_paid_date, 1, 7)
  1647. # collect the invoice amounts per month
  1648. invoice_amounts_query = DBSession.query(
  1649. invoice_date_month.label('month'),
  1650. func.sum(expression.case(
  1651. [(
  1652. expression.not_(Dues15Invoice.is_reversal),
  1653. Dues15Invoice.invoice_amount)],
  1654. else_=Decimal('0.0'))).label('amount_invoiced_normal'),
  1655. func.sum(expression.case(
  1656. [(
  1657. Dues15Invoice.is_reversal,
  1658. Dues15Invoice.invoice_amount)],
  1659. else_=Decimal('0.0'))).label('amount_invoiced_reversal'),
  1660. expression.literal_column(
  1661. '\'0.0\'', SqliteDecimal).label('amount_paid')
  1662. ).group_by(invoice_date_month)
  1663. # collect the payments per month
  1664. member_payments_query = DBSession.query(
  1665. payment_date_month.label('month'),
  1666. expression.literal_column(
  1667. '\'0.0\'', SqliteDecimal).label('amount_invoiced_normal'),
  1668. expression.literal_column(
  1669. '\'0.0\'', SqliteDecimal
  1670. ).label('amount_invoiced_reversal'),
  1671. func.sum(C3sMember.dues15_amount_paid).label('amount_paid')
  1672. ).filter(C3sMember.dues15_paid_date.isnot(None)) \
  1673. .group_by(payment_date_month)
  1674. # union invoice amounts and payments
  1675. union_all_query = expression.union_all(
  1676. member_payments_query, invoice_amounts_query)
  1677. # aggregate invoice amounts and payments by month
  1678. result_query = DBSession.query(
  1679. union_all_query.c.month.label('month'),
  1680. func.sum(union_all_query.c.amount_invoiced_normal).label(
  1681. 'amount_invoiced_normal'),
  1682. func.sum(union_all_query.c.amount_invoiced_reversal).label(
  1683. 'amount_invoiced_reversal'),
  1684. func.sum(union_all_query.c.amount_paid).label('amount_paid')
  1685. ) \
  1686. .group_by(union_all_query.c.month) \
  1687. .order_by(union_all_query.c.month)
  1688. for month_stat in result_query.all():
  1689. result.append(
  1690. {
  1691. 'month': datetime(
  1692. int(month_stat[0][0:4]),
  1693. int(month_stat[0][5:7]),
  1694. 1),
  1695. 'amount_invoiced_normal': month_stat[1],
  1696. 'amount_invoiced_reversal': month_stat[2],
  1697. 'amount_paid': month_stat[3]
  1698. })
  1699. return result
  1700. class Dues16Invoice(Base):
  1701. """
  1702. This table stores the invoices for the 2015 version of dues.
  1703. We need this for bookkeeping,
  1704. because whenever a member is granted a reduction of her dues,
  1705. the old invoice is canceled by a reversal invoice
  1706. and a new invoice must be issued.
  1707. Edge case: if reduced to 0, no new invoice needed.
  1708. """
  1709. __tablename__ = 'dues16invoices'
  1710. # pylint: disable=invalid-name
  1711. id = Column(Integer, primary_key=True)
  1712. """tech. id. / no. in table (integer, primary key)"""
  1713. # this invoice
  1714. invoice_no = Column(Integer(), unique=True)
  1715. """invoice number (Integer, unique)"""
  1716. invoice_no_string = Column(Unicode(255), unique=True)
  1717. """invoice number string (unique)"""
  1718. invoice_date = Column(DateTime())
  1719. """timestamp of invoice creation (DateTime)"""
  1720. invoice_amount = Column(DatabaseDecimal(12, 2), default=Decimal('NaN'))
  1721. """amount (DatabaseDecimal(12,2))"""
  1722. # has it been superseeded by reversal?
  1723. is_cancelled = Column(Boolean, default=False)
  1724. """flag: invoice has been superseeded by reversal or cancellation"""
  1725. cancelled_date = Column(DateTime())
  1726. """timestamp of cancellation/reversal"""
  1727. # is it a reversal?
  1728. is_reversal = Column(Boolean, default=False)
  1729. """flag: is this a reversal invoice?"""
  1730. # is it a reduction (or even more than default)?
  1731. is_altered = Column(Boolean, default=False)
  1732. """flag: has the amount been reduced or increased?"""
  1733. # person reference
  1734. member_id = Column(Integer())
  1735. """reference to C3sMember id"""
  1736. membership_no = Column(Integer())
  1737. """reference to C3sMember membership_number"""
  1738. email = Column(Unicode(255))
  1739. """C3sMembers email we sent this invoice to"""
  1740. token = Column(Unicode(255))
  1741. """used to limit access to this invoice"""
  1742. # referrals
  1743. preceding_invoice_no = Column(Integer(), default=None)
  1744. """the invoice number preceeding this one, if applicable"""
  1745. succeeding_invoice_no = Column(Integer(), default=None)
  1746. """the invoice number succeeding this one, if applicable"""
  1747. def __init__(
  1748. self,
  1749. invoice_no,
  1750. invoice_no_string,
  1751. invoice_date,
  1752. invoice_amount,
  1753. member_id,
  1754. membership_no,
  1755. email,
  1756. token):
  1757. """
  1758. Make a new invoice object
  1759. Args:
  1760. invoice_no: invoice number
  1761. invoice_no_string: invoice number string
  1762. invoice_date: timestamp of creation
  1763. invoice_amount: amount of money
  1764. member_id: references C3sMember
  1765. membership_no: references C3sMember
  1766. email: email to send it to
  1767. token: a token to limit access
  1768. """
  1769. self.invoice_no = invoice_no
  1770. self.invoice_no_string = invoice_no_string
  1771. self.invoice_date = invoice_date
  1772. self.invoice_amount = invoice_amount
  1773. self.member_id = member_id
  1774. self.membership_no = membership_no
  1775. self.email = email
  1776. self.token = token
  1777. @classmethod
  1778. def get_all(cls):
  1779. """
  1780. Return all dues16 invoices
  1781. """
  1782. return DBSession.query(cls).all()
  1783. @classmethod
  1784. def get_by_invoice_no(cls, _no):
  1785. """return one invoice by invoice number"""
  1786. return DBSession.query(cls).filter(cls.invoice_no == _no).first()
  1787. @classmethod
  1788. def get_by_membership_no(cls, _no):
  1789. """return all invoices of one member by membership number"""
  1790. return DBSession.query(cls).filter(cls.membership_no == _no).all()
  1791. @classmethod
  1792. def get_max_invoice_no(cls):
  1793. """
  1794. Get the maximum invoice number.
  1795. Returns:
  1796. * Integer: maximum of given invoice numbers or 0"""
  1797. res, = DBSession.query(func.max(cls.id)).first()
  1798. if res is None:
  1799. return 0
  1800. return res
  1801. @classmethod
  1802. def check_for_existing_dues16_token(cls, dues_token):
  1803. """
  1804. Check if a dues token is already present.
  1805. Args:
  1806. dues_token: a given string
  1807. Returns:
  1808. * **True**, if token already in table
  1809. * **False** else
  1810. """
  1811. check = DBSession.query(cls).filter(
  1812. cls.token == dues_token).first()
  1813. return check is not None
  1814. @classmethod
  1815. def get_monthly_stats(cls):
  1816. """
  1817. Gets the monthly statistics.
  1818. Provides sums of the normale as well as reversal invoices per
  1819. calendar month based on the invoice date.
  1820. """
  1821. result = []
  1822. # SQLite specific: substring for SQLite as it does not support
  1823. # date_trunc.
  1824. # invoice_date_month = func.date_trunc(
  1825. # 'month',
  1826. # Dues16Invoice.invoice_date)
  1827. invoice_date_month = func.substr(Dues16Invoice.invoice_date, 1, 7)
  1828. payment_date_month = func.substr(C3sMember.dues16_paid_date, 1, 7)
  1829. # collect the invoice amounts per month
  1830. invoice_amounts_query = DBSession.query(
  1831. invoice_date_month.label('month'),
  1832. func.sum(expression.case(
  1833. [(
  1834. expression.not_(Dues16Invoice.is_reversal),
  1835. Dues16Invoice.invoice_amount)],
  1836. else_=Decimal('0.0'))).label('amount_invoiced_normal'),
  1837. func.sum(expression.case(
  1838. [(
  1839. Dues16Invoice.is_reversal,
  1840. Dues16Invoice.invoice_amount)],
  1841. else_=Decimal('0.0'))).label('amount_invoiced_reversal'),
  1842. expression.literal_column(
  1843. '\'0.0\'', SqliteDecimal).label('amount_paid')
  1844. ).group_by(invoice_date_month)
  1845. # collect the payments per month
  1846. member_payments_query = DBSession.query(
  1847. payment_date_month.label('month'),
  1848. expression.literal_column(
  1849. '\'0.0\'', SqliteDecimal).label('amount_invoiced_normal'),
  1850. expression.literal_column(
  1851. '\'0.0\'', SqliteDecimal
  1852. ).label('amount_invoiced_reversal'),
  1853. func.sum(C3sMember.dues16_amount_paid).label('amount_paid')
  1854. ).filter(C3sMember.dues16_paid_date.isnot(None)) \
  1855. .group_by(payment_date_month)
  1856. # union invoice amounts and payments
  1857. union_all_query = expression.union_all(
  1858. member_payments_query, invoice_amounts_query)
  1859. # aggregate invoice amounts and payments by month
  1860. result_query = DBSession.query(
  1861. union_all_query.c.month.label('month'),
  1862. func.sum(union_all_query.c.amount_invoiced_normal).label(
  1863. 'amount_invoiced_normal'),
  1864. func.sum(union_all_query.c.amount_invoiced_reversal).label(
  1865. 'amount_invoiced_reversal'),
  1866. func.sum(union_all_query.c.amount_paid).label('amount_paid')
  1867. ) \
  1868. .group_by(union_all_query.c.month) \
  1869. .order_by(union_all_query.c.month)
  1870. for month_stat in result_query.all():
  1871. result.append(
  1872. {
  1873. 'month': datetime(
  1874. int(month_stat[0][0:4]),
  1875. int(month_stat[0][5:7]),
  1876. 1),
  1877. 'amount_invoiced_normal': month_stat[1],
  1878. 'amount_invoiced_reversal': month_stat[2],
  1879. 'amount_paid': month_stat[3]
  1880. })
  1881. return result
  1882. class Dues17Invoice(Base):
  1883. """
  1884. This table stores the invoices for the 2015 version of dues.
  1885. We need this for bookkeeping,
  1886. because whenever a member is granted a reduction of her dues,
  1887. the old invoice is canceled by a reversal invoice
  1888. and a new invoice must be issued.
  1889. Edge case: if reduced to 0, no new invoice needed.
  1890. """
  1891. __tablename__ = 'dues17invoices'
  1892. # pylint: disable=invalid-name
  1893. id = Column(Integer, primary_key=True)
  1894. """tech. id. / no. in table (integer, primary key)"""
  1895. # this invoice
  1896. invoice_no = Column(Integer(), unique=True)
  1897. """invoice number (Integer, unique)"""
  1898. invoice_no_string = Column(Unicode(255), unique=True)
  1899. """invoice number string (unique)"""
  1900. invoice_date = Column(DateTime())
  1901. """timestamp of invoice creation (DateTime)"""
  1902. invoice_amount = Column(DatabaseDecimal(12, 2), default=Decimal('NaN'))
  1903. """amount (DatabaseDecimal(12,2))"""
  1904. # has it been superseeded by reversal?
  1905. is_cancelled = Column(Boolean, default=False)
  1906. """flag: invoice has been superseeded by reversal or cancellation"""
  1907. cancelled_date = Column(DateTime())
  1908. """timestamp of cancellation/reversal"""
  1909. # is it a reversal?
  1910. is_reversal = Column(Boolean, default=False)
  1911. """flag: is this a reversal invoice?"""
  1912. # is it a reduction (or even more than default)?
  1913. is_altered = Column(Boolean, default=False)
  1914. """flag: has the amount been reduced or increased?"""
  1915. # person reference
  1916. member_id = Column(Integer())
  1917. """reference to C3sMember id"""
  1918. membership_no = Column(Integer())
  1919. """reference to C3sMember membership_number"""
  1920. email = Column(Unicode(255))
  1921. """C3sMembers email we sent this invoice to"""
  1922. token = Column(Unicode(255))
  1923. """used to limit access to this invoice"""
  1924. # referrals
  1925. preceding_invoice_no = Column(Integer(), default=None)
  1926. """the invoice number preceeding this one, if applicable"""
  1927. succeeding_invoice_no = Column(Integer(), default=None)
  1928. """the invoice number succeeding this one, if applicable"""
  1929. def __init__(
  1930. self,
  1931. invoice_no,
  1932. invoice_no_string,
  1933. invoice_date,
  1934. invoice_amount,
  1935. member_id,
  1936. membership_no,
  1937. email,
  1938. token):
  1939. """
  1940. Make a new invoice object
  1941. Args:
  1942. invoice_no: invoice number
  1943. invoice_no_string: invoice number string
  1944. invoice_date: timestamp of creation
  1945. invoice_amount: amount of money
  1946. member_id: references C3sMember
  1947. membership_no: references C3sMember
  1948. email: email to send it to
  1949. token: a token to limit access
  1950. """
  1951. self.invoice_no = invoice_no
  1952. self.invoice_no_string = invoice_no_string
  1953. self.invoice_date = invoice_date
  1954. self.invoice_amount = invoice_amount
  1955. self.member_id = member_id
  1956. self.membership_no = membership_no
  1957. self.email = email
  1958. self.token = token
  1959. @classmethod
  1960. def get_all(cls):
  1961. """
  1962. Return all dues17 invoices
  1963. """
  1964. return DBSession.query(cls).all()
  1965. @classmethod
  1966. def get_by_invoice_no(cls, _no):
  1967. """return one invoice by invoice number"""
  1968. return DBSession.query(cls).filter(cls.invoice_no == _no).first()
  1969. @classmethod
  1970. def get_by_membership_no(cls, _no):
  1971. """return all invoices of one member by membership number"""
  1972. return DBSession.query(cls).filter(cls.membership_no == _no).all()
  1973. @classmethod
  1974. def get_max_invoice_no(cls):
  1975. """
  1976. Get the maximum invoice number.
  1977. Returns:
  1978. * Integer: maximum of given invoice numbers or 0"""
  1979. res, = DBSession.query(func.max(cls.id)).first()
  1980. if res is None:
  1981. return 0
  1982. return res
  1983. @classmethod
  1984. def check_for_existing_dues17_token(cls, dues_token):
  1985. """
  1986. Check if a dues token is already present.
  1987. Args:
  1988. dues_token: a given string
  1989. Returns:
  1990. * **True**, if token already in table
  1991. * **False** else
  1992. """
  1993. check = DBSession.query(cls).filter(
  1994. cls.token == dues_token).first()
  1995. return check is not None
  1996. @classmethod
  1997. def get_monthly_stats(cls):
  1998. """
  1999. Gets the monthly statistics.
  2000. Provides sums of the normale as well as reversal invoices per
  2001. calendar month based on the invoice date.
  2002. """
  2003. result = []
  2004. # SQLite specific: substring for SQLite as it does not support
  2005. # date_trunc.
  2006. # invoice_date_month = func.date_trunc(
  2007. # 'month',
  2008. # Dues17Invoice.invoice_date)
  2009. invoice_date_month = func.substr(Dues17Invoice.invoice_date, 1, 7)
  2010. payment_date_month = func.substr(C3sMember.dues17_paid_date, 1, 7)
  2011. # collect the invoice amounts per month
  2012. invoice_amounts_query = DBSession.query(
  2013. invoice_date_month.label('month'),
  2014. func.sum(expression.case(
  2015. [(
  2016. expression.not_(Dues17Invoice.is_reversal),
  2017. Dues17Invoice.invoice_amount)],
  2018. else_=Decimal('0.0'))).label('amount_invoiced_normal'),
  2019. func.sum(expression.case(
  2020. [(
  2021. Dues17Invoice.is_reversal,
  2022. Dues17Invoice.invoice_amount)],
  2023. else_=Decimal('0.0'))).label('amount_invoiced_reversal'),
  2024. expression.literal_column(
  2025. '\'0.0\'', SqliteDecimal).label('amount_paid')
  2026. ).group_by(invoice_date_month)
  2027. # collect the payments per month
  2028. member_payments_query = DBSession.query(
  2029. payment_date_month.label('month'),
  2030. expression.literal_column(
  2031. '\'0.0\'', SqliteDecimal).label('amount_invoiced_normal'),
  2032. expression.literal_column(
  2033. '\'0.0\'', SqliteDecimal
  2034. ).label('amount_invoiced_reversal'),
  2035. func.sum(C3sMember.dues17_amount_paid).label('amount_paid')
  2036. ).filter(C3sMember.dues17_paid_date.isnot(None)) \
  2037. .group_by(payment_date_month)
  2038. # union invoice amounts and payments
  2039. union_all_query = expression.union_all(
  2040. member_payments_query, invoice_amounts_query)
  2041. # aggregate invoice amounts and payments by month
  2042. result_query = DBSession.query(
  2043. union_all_query.c.month.label('month'),
  2044. func.sum(union_all_query.c.amount_invoiced_normal).label(
  2045. 'amount_invoiced_normal'),
  2046. func.sum(union_all_query.c.amount_invoiced_reversal).label(
  2047. 'amount_invoiced_reversal'),
  2048. func.sum(union_all_query.c.amount_paid).label('amount_paid')
  2049. ) \
  2050. .group_by(union_all_query.c.month) \
  2051. .order_by(union_all_query.c.month)
  2052. for month_stat in result_query.all():
  2053. result.append(
  2054. {
  2055. 'month': datetime(
  2056. int(month_stat[0][0:4]),
  2057. int(month_stat[0][5:7]),
  2058. 1),
  2059. 'amount_invoiced_normal': month_stat[1],
  2060. 'amount_invoiced_reversal': month_stat[2],
  2061. 'amount_paid': month_stat[3]
  2062. })
  2063. return result