Compare commits

...

222 commits

Author SHA1 Message Date
Aurélien Delobelle 5aa2d3cbac kfet.kpsul -- Make the low-stock indicator appear…
…if ordered quantity is greater than the current stock.

Also fix the focus after a reset of the selected account.
2018-01-13 02:04:42 +01:00
Aurélien Delobelle 367b5899fc Merge branch 'aureplop/kpsul_js_refactor' into aureplop/js_basket 2018-01-12 18:49:13 +01:00
Aurélien Delobelle 5ef219c88e kfet -- Update tests to reflect last changes + fix
- Remove duplicate categories returned by the view 'kpsul_articles_data'.
- Fix in account filtering of the view 'history_json'.

- Tests of the view 'kpsul_articles_data' are updated to reflect changes of the
  data format.
- The views returning Account and Checkout data as JSON no longer
  exists. The related tests are moved.
- The view canceling transfers has been merged with the one canceling
  the other operations. The related tests are moved.
2018-01-12 18:48:42 +01:00
Ludovic Stephan f93cadc12c Merge branch 'aureplop/kpsul_js_refactor' into aureplop/js_basket 2018-01-10 19:12:09 +01:00
Ludovic Stephan f03ce35126 Merge branch 'master' into aureplop/kpsul_js_refactor 2018-01-10 19:09:41 +01:00
Ludovic Stephan b62f0293dd Merge branch 'master' into aureplop/kpsul_js_refactor 2018-01-10 18:55:33 +01:00
Aurélien Delobelle ba1eabe240 "Fix" inconsistents amounts/balances 2017-05-31 21:29:53 +02:00
Aurélien Delobelle 5ab7519c8d ForestDisplay update node.
- ForestDisplay update method update the container without re-rendering
from scratch the object. This allows persistance of properties set by
other components.
- Fix unselect on basket updates from keyboard.
2017-05-31 17:14:31 +02:00
Aurélien Delobelle eff1b7ff19 Cleaning - Article autocomplete, ForestDisplay and more
K-Psul
- Improve article autocompletion.

ArticleManager
- "selected" property becomes a reference to an article in the data properties.

ForestDisplay
- Add data property "object" linked to object being represented
- Use class to identify objects instead of id. Allow multiple displays of same ModelForest.
- New get_class method returns the class selector to find an object container in the DOM.
- New get_dom method returns the DOM element from an object in the ModelForest.

Cancellation view
- Fix 500 on cancel with already canceled opes/transfers
2017-05-21 20:03:37 +02:00
Aurélien Delobelle 51083f9195 Merge branch 'aureplop/kpsul_js_refactor' into aureplop/js_basket 2017-05-21 13:01:51 +02:00
Ludovic Stephan c76b729320 Merge branch 'aureplop/ws_checkout' into 'aureplop/kpsul_js_refactor'
Add last statement update support.

See merge request !232
2017-05-19 21:50:29 +02:00
Aurélien Delobelle 8a9d6712ee Merge branch 'aureplop/kpsul_js_refactor' into aureplop/js_basket 2017-05-18 13:41:56 +02:00
Aurélien Delobelle 914d2ca870 Add last statement update support.
K-Psul
- Add handler for checkout data (balance update)
- New statements are sent through ws, and display live on K-Psul.
2017-05-18 13:33:53 +02:00
Aurélien Delobelle 86a3ae3c2f clean
- basket.add_withdraw takes a positive amount
- move models and formatters definitions with their siblings
2017-05-18 12:49:39 +02:00
Ludovic Stephan ef1f00c15b Merge branch 'aureplop/fix_ws' into 'aureplop/kpsul_js_refactor'
Fix some ws updates on khistory

See merge request !230
2017-05-18 03:39:53 +02:00
Aurélien Delobelle a0503d0c53 Refactor K-Psul basket (big part)
K-Psul - Basket refactor
- Almost done.

ModelForest
- Add create method (based on previous get_or_create). Direction defaults to 0.
- Add delete method.
- Methods find, traverse, update, delete can also take a model (class) as first
  argument. String representation of model still works.
- Fix child linking to parent in create method.

ModelForest -> ForestDisplay
- (One-way data binding) Changes on a ModelForest are directly reflected on
  listening ForestDisplay(s).
- ArticleManager and KHistory become simpler.

Config
- Add addcost key, shorthand for double addcost keys check.

K-Psul
- Improve display for basket summary and previous operation.
- Clean js code / duplicates.
- Some components gains chance to trigger/handle events. They are really happy.
  Eg basket amounts and summary are updated thanks to these events if the
  selected account is changed.

Formatters
- Fixes addcost and amount display.

History
- Fix options management (api_options were overrided and K-Psul displayed more
  than the last day history).
- Fix data display, thanks to formatters fixes and modelforest fixes.
2017-05-18 02:13:42 +02:00
Aurélien Delobelle 24b9aaae39 Revert template literals use
They were adding spaces at some places.
2017-05-16 18:47:26 +02:00
Aurélien Delobelle cb0c0be8a2 filters on ws for khistory
- fix filters on ws khistory updates
- add filters for transfers against accounts
2017-05-16 18:08:40 +02:00
Aurélien Delobelle 02a015e633 fix data sent to ws by perform_transfers 2017-05-16 17:08:51 +02:00
Aurélien Delobelle bacc079778 Merge branch 'master' into aureplop/kpsul_js_refactor 2017-05-16 16:48:12 +02:00
Aurélien Delobelle bc71e1628a keep eslint quiet 2017-05-16 12:27:39 +02:00
Aurélien Delobelle 311e0c48bd Merge branch 'Aufinal/refactor_history' into 'aureplop/kpsul_js_refactor'
Aufinal/refactor history

See merge request !192
2017-05-16 10:48:07 +02:00
Ludovic Stephan 6a6fc38ead Add selection reset to cancel_opes 2017-05-15 21:10:39 -03:00
Ludovic Stephan 43e772363e Extend history options 2017-05-15 19:29:12 -03:00
Ludovic Stephan 93c8844b3f typo 2017-05-15 19:28:24 -03:00
Ludovic Stephan c2da055b60 Remove duplicate ws updates 2017-05-15 18:59:18 -03:00
Ludovic Stephan ac33e6302e Fix addExistingPurchase + few other bugs 2017-05-15 17:39:33 -03:00
Ludovic Stephan ad42687293 Fix tranfers page 2017-05-15 15:27:46 -03:00
Ludovic Stephan f4cb1e2e83 Add opesonly option 2017-05-15 14:17:58 -03:00
Ludovic Stephan c12c705f8b Bind ForestDisplay to initial data 2017-05-14 20:18:31 -03:00
Ludovic Stephan 31b742fdb7 Move ws update to respective classes 2017-05-14 17:19:09 -03:00
Ludovic Stephan 8b8a3f8a25 Update button in history 2017-04-24 14:46:16 -03:00
Ludovic Stephan 16dbfed977 Add chidren div in display 2017-04-24 14:12:03 -03:00
Ludovic Stephan 5096e5f129 Adapt ArticleManager 2017-04-24 13:25:18 -03:00
Ludovic Stephan 46ac82fd27 Adapt history to changes 2017-04-24 13:25:07 -03:00
Ludovic Stephan e283439ebc Create ForestDisplay class
- Store templates, container and data
- All display functions removed from ModelForest
2017-04-24 13:21:56 -03:00
Ludovic Stephan 20d635137c Merge remote-tracking branch 'origin/aureplop/kpsul_js_refactor' into Aufinal/refactor_history 2017-04-24 11:30:01 -03:00
Ludovic Stephan b2a5dfd682 Move permission check 2017-04-14 12:51:58 -03:00
Ludovic Stephan 100686457b Change select_related for future compatibility 2017-04-14 12:47:15 -03:00
Ludovic Stephan b544d6c5b3 Better alignment 2017-04-14 12:45:38 -03:00
Ludovic Stephan 659b20891e Add filter to cancel_opes 2017-04-14 12:43:59 -03:00
Ludovic Stephan 034a661444 Tweaks on kfet.js 2017-04-14 12:12:43 -03:00
Ludovic Stephan de865c61aa Move container clearing 2017-04-14 11:36:52 -03:00
Aurélien Delobelle b0d35667b3 Merge branch 'master' into aureplop/kpsul_js_refactor 2017-04-14 15:56:44 +02:00
Ludovic Stephan 8c02e5da0c Trigger event on history itself 2017-04-10 15:40:29 -03:00
Ludovic Stephan 2eba6892a2 Revert "Change event triggered when canceling opes"
This reverts commit ce3d8aa6f7.
2017-04-10 15:34:57 -03:00
Ludovic Stephan ce3d8aa6f7 Change event triggered when canceling opes 2017-04-10 15:08:17 -03:00
Ludovic Stephan 84d478271b Compatibility changes on history.js 2017-04-10 12:34:43 -03:00
Ludovic Stephan cd0e4c6f3e Allow (basic) chaining on api_with_auth 2017-04-10 12:33:24 -03:00
Ludovic Stephan 983a55780f Add cancel_history event 2017-04-10 12:32:35 -03:00
Ludovic Stephan cfb39b1050 Better default options 2017-04-10 11:30:00 -03:00
Aurélien Delobelle c75b2946e3 Merge branch 'master' into aureplop/kpsul_js_refactor 2017-04-10 13:08:23 +02:00
Ludovic Stephan 47da80f21c Add related objects 2017-04-09 23:24:50 -03:00
Ludovic Stephan 5e8752632c Add index for Day objects 2017-04-09 21:50:47 -03:00
Ludovic Stephan 688d5bba29 Adapt history to new structure 2017-04-09 17:54:37 -03:00
Ludovic Stephan 051231a031 Merge remote-tracking branch 'origin/aureplop/kpsul_js_refactor' into Aufinal/refactor_history 2017-04-09 16:47:05 -03:00
Aurélien Delobelle 7fc07ac603 Merge branch 'Aufinal/clean_modelforest' into 'aureplop/kpsul_js_refactor'
Change ModelForest inner structure
- Intermediary model for nodes removed
- More descriptive ModelForest structure in constructor.structure, child_sort not defined in Python function anymore.
- More intuitive access to parent and children of nodes (e.g. article.category, category.articles...)

See merge request !215
2017-04-09 21:28:16 +02:00
Ludovic Stephan 323f019c0d Check if children is non empty 2017-04-09 13:35:01 -03:00
Ludovic Stephan 9ad208a171 Change child sort + bugfix grom prev commit 2017-04-09 12:30:15 -03:00
Ludovic Stephan 73fb3c419e Add stop check in traverse 2017-04-09 11:43:51 -03:00
Ludovic Stephan 9ba13a81ee Adapt add_to_container + small improvements 2017-04-06 00:10:39 -03:00
Ludovic Stephan 23d19545a7 Add back root_sort 2017-04-05 22:23:56 -03:00
Ludovic Stephan df47bedae1 Change ModelForest inner structure 2017-04-05 22:10:21 -03:00
Aurélien Delobelle e4dd434608 no longer use model_to_dict
- fix cof status on k-psul
2017-04-05 23:33:45 +02:00
Aurélien Delobelle e4ccd88dfd Merge branch 'master' into aureplop/kpsul_js_refactor 2017-04-05 23:18:33 +02:00
Ludovic Stephan ed0a82ed5d Add no_trigramme option 2017-04-05 16:51:17 -03:00
Aurélien Delobelle 6a8f41849b Merge branch 'Aufinal/refactor_articles' into 'aureplop/kpsul_js_refactor'
Aufinal/refactor articles

See merge request !173
2017-04-05 17:56:07 +02:00
Ludovic Stephan 5c422e892a Add children fo traverse callback 2017-04-05 12:31:19 -03:00
Ludovic Stephan f57c292184 Rename history var 2017-04-05 12:13:24 -03:00
Ludovic Stephan 508e7ec23f Change traverse and find behavior 2017-04-05 12:00:39 -03:00
Ludovic Stephan 88f7ea941d Move selection logic to another class 2017-04-05 11:26:33 -03:00
Ludovic Stephan 8d13c0a4bb Add fetch method 2017-04-05 10:59:59 -03:00
Ludovic Stephan 290d4ecb6e Merge branch 'Aufinal/refactor_articles' into Aufinal/refactor_history 2017-04-05 10:03:30 -03:00
Ludovic Stephan 7ec7ed2696 Rename History class 2017-04-05 09:53:12 -03:00
Ludovic Stephan e051631a34 Use WebSocket classes 2017-04-05 09:28:32 -03:00
Ludovic Stephan 360c442a4e Remove useless class 2017-04-05 09:24:27 -03:00
Ludovic Stephan 1761c5f1bd Change fromAPI logic 2017-04-05 09:13:00 -03:00
Ludovic Stephan 840010b63f Merge remote-tracking branch 'origin/aureplop/kpsul_js_refactor' into Aufinal/refactor_articles 2017-04-05 09:03:59 -03:00
Ludovic Stephan a29de134f1 Move focus ; move is_low_stock to method 2017-04-05 08:58:46 -03:00
Aurélien Delobelle 2e0de75471 kpsul - fix account balance ukf 2017-04-05 13:18:01 +02:00
Aurélien Delobelle 6bb9280b0d Merge branch 'aureplop/clean_js' into 'aureplop/kpsul_js_refactor'
Enhance APIModelObject

See merge request !213
2017-04-05 04:30:34 +02:00
Aurélien Delobelle 6be6202b3f few cleans 2017-04-05 04:26:50 +02:00
Aurélien Delobelle f1aaad7317 Better jquery ajax calls management
It becomes the same as the original jQuery ajax object.
For example, callbacks can be queued.
get_by_apipk and from_API of ModelObject returns the ajax object.

Example (js):
Account.get_by_apipk('AAA')
    .done(function (data) {
        console.log(data)
    })
    .fail( () => console.log('cool') )
    ...
    .done( ...
2017-04-05 04:05:31 +02:00
Aurélien Delobelle efbcde163b clean some js
- clean buttons code on account and checkout
- merge CheckoutRead and kpsul_checkout_data views (the first won)

APIModelObject interface
- add url_create, url_update, url_update_for
- rename url_object_for and url_object to url_read_for and url_read
2017-04-05 03:43:51 +02:00
Ludovic Stephan 3b9affb3f3 Add focus methods 2017-04-04 20:18:53 -03:00
Ludovic Stephan 9c559d9ec3 Add articles reset to kpsul.reset 2017-04-04 19:54:03 -03:00
Ludovic Stephan 021937a38e Small bugfixes 2017-04-04 19:52:12 -03:00
Ludovic Stephan cb28b928c4 Remove articleSelect from _env 2017-04-04 19:42:30 -03:00
Ludovic Stephan e5791efe4d Remove last traces of old articles 2017-04-04 19:41:15 -03:00
Ludovic Stephan 05156f37c6 Update addExistingPurchase 2017-04-04 19:34:22 -03:00
Ludovic Stephan 514f1da6df Fix SpecialOpeFormatter 2017-04-03 21:26:33 -03:00
Ludovic Stephan ec9f47274a Fix WS update functions 2017-04-03 21:24:07 -03:00
Ludovic Stephan 20b7156e1f Improve type display 2017-04-03 21:18:07 -03:00
Ludovic Stephan a173be4f7d Use api_with_auth in history 2017-04-03 21:14:06 -03:00
Ludovic Stephan 07290f6fec Merge branch 'Aufinal/refactor_articles' into Aufinal/refactor_history 2017-04-03 21:00:12 -03:00
Ludovic Stephan 3ce4dc5c85 Add article stock management 2017-04-03 20:14:45 -03:00
Ludovic Stephan b91edc9c7d Merge remote-tracking branch 'origin/aureplop/kpsul_js_refactor' into Aufinal/refactor_articles 2017-04-03 20:03:58 -03:00
Aurélien Delobelle 9d2298a089 Merge branch 'master' into aureplop/kpsul_js_refactor
Not chill.
2017-04-04 00:51:49 +02:00
Aurélien Delobelle dcda67aaf7 Merge branch 'Aufinal/dialog_utils' into 'aureplop/kpsul_js_refactor'
Utilitaires de dialogue

Ajoute deux type de dialogue avec l'utilisateur
- une classe UserDialog pour ouvrir un simple dialogue jconfirm
- une fonction api_with_auth pour gérer toutes les requêtes API pouvant 
nécessiter un mot de passe

See merge request !199
2017-04-03 23:25:55 +02:00
Ludovic Stephan 5020037103 api_lock inside kfet.js 2017-04-03 16:08:40 -03:00
Ludovic Stephan 4af2562121 More clarity in argument names 2017-04-01 09:34:02 -03:00
Ludovic Stephan 29836fd15c Remove deprecated option 2017-04-01 09:24:18 -03:00
Ludovic Stephan 2774dbb5de Merge branch 'Aufinal/refactor_articles' into Aufinal/refactor_history 2017-03-31 23:51:13 -03:00
Ludovic Stephan 7d93d91af9 Merge remote-tracking branch 'origin/aureplop/kpsul_js_refactor' into Aufinal/refactor_articles 2017-03-31 23:50:33 -03:00
Ludovic Stephan 236dcb4644 Tweaks to UserDialog 2017-03-31 18:10:06 -03:00
Ludovic Stephan 582cdebaa1 Better callback management 2017-03-31 17:49:22 -03:00
Ludovic Stephan 08c752f1b3 Simplify addcost management 2017-03-26 18:10:26 -03:00
Ludovic Stephan 5101400f64 Use callback_as_dict for addcost 2017-03-26 17:57:51 -03:00
Ludovic Stephan c1f70d9d0a Add capslock support for inputs 2017-03-26 17:51:55 -03:00
Ludovic Stephan 6d92df4155 Merge branch 'aureplop/kpsul_js_refactor' of git.eleves.ens.fr:cof-geek/gestioCOF into Aufinal/dialog_utils 2017-03-26 15:21:33 -03:00
Aurélien Delobelle 842f2cecc1 fix import, fix ope with addcost enabled, move Config location 2017-03-26 15:12:03 +02:00
Aurélien Delobelle bc6ecda0c8 fix addcost kspul 2017-03-26 14:59:21 +02:00
Aurélien Delobelle 24f72ae7d4 add missing ; 2017-03-26 14:52:23 +02:00
Aurélien Delobelle b81b33c056 allow chaining on container in display method 2017-03-26 14:51:16 +02:00
Aurélien Delobelle 485ae86a42 add update method to ModelObject 2017-03-26 14:46:46 +02:00
Ludovic Stephan 3bf7a066a2 Adapt history.html to new functions 2017-03-26 00:33:33 -03:00
Ludovic Stephan 3f07bf56fa Remove console log 2017-03-26 00:25:51 -03:00
Ludovic Stephan dbcfc6df46 Remove duplicate kfet.js import 2017-03-26 00:21:17 -03:00
Ludovic Stephan 87943ea7b9 Adapt kpsul to new functions 2017-03-26 00:20:47 -03:00
Ludovic Stephan 6be2f086df Add generic functions for confirm dialogs 2017-03-26 00:20:28 -03:00
Ludovic Stephan a1c976185c Fix transfer sort 2017-03-25 13:44:35 -03:00
Ludovic Stephan e6735d44ba Merge branch 'Aufinal/refactor_articles' into Aufinal/refactor_history 2017-03-25 12:42:26 -03:00
Ludovic Stephan 0997d85083 Merge branch 'aureplop/kpsul_js_refactor' of git.eleves.ens.fr:cof-geek/gestioCOF into Aufinal/refactor_history 2017-03-25 12:39:30 -03:00
Ludovic Stephan 5ff8f69bfa Merge branch 'aureplop/kpsul_js_refactor' of git.eleves.ens.fr:cof-geek/gestioCOF into Aufinal/refactor_articles 2017-03-25 12:35:04 -03:00
Aurélien Delobelle 6afbcb44a1 delete array comprehesion 2017-03-25 10:26:45 +01:00
Aurélien Delobelle 967748ded3 details link to read instead of update
- It was the old behaviour.
2017-03-25 09:58:20 +01:00
Aurélien Delobelle 72970c6be7 Fix api call on new transfers
- Use django-js-reverse
- Replace old url (deleted) with standard url
2017-03-25 09:44:55 +01:00
Ludovic Stephan aa6a50a6e7 Simplify JS-Python interface for cancel_ops 2017-03-24 23:57:27 -03:00
Aurélien Delobelle ac18cbd9d9 fix by on last statement 2017-03-24 22:10:34 +01:00
Aurélien Delobelle 0d02d47d33 move utils functions 2017-03-24 22:05:04 +01:00
Aurélien Delobelle 2e3bd5bd7a fix search box 2017-03-24 21:41:34 +01:00
Aurélien Delobelle abce961d91 use django-js-reverse 2017-03-24 21:28:33 +01:00
Aurélien Delobelle a9d1a6aae9 clean array iteration 2017-03-24 21:24:06 +01:00
Ludovic Stephan 1fcd53d780 Continue renaming node.type to node.modelname 2017-03-20 00:42:12 -03:00
Ludovic Stephan fe965875f7 Merge branch 'Aufinal/refactor_articles' into Aufinal/refactor_history 2017-03-20 00:33:30 -03:00
Ludovic Stephan 3465dd7045 Change node.type to node.modelname for clarity 2017-03-20 00:26:11 -03:00
Ludovic Stephan c99e4f26d0 Move history initialisation as Config.reset callback 2017-03-19 04:59:55 +01:00
Ludovic Stephan 66c5a6953c Improve websocket filter for special history pages 2017-03-19 04:29:54 +01:00
Ludovic Stephan 7a00096170 Add support for account_read history 2017-03-18 22:48:50 -03:00
Ludovic Stephan 951932a6c8 Add support and websocket to transfers.html 2017-03-18 22:35:47 -03:00
Ludovic Stephan fa64a68378 Add strict mode to history.html script 2017-03-18 22:15:09 -03:00
Ludovic Stephan fc3e86aea6 Add websocket support to ArticleManager 2017-03-18 22:06:30 -03:00
Ludovic Stephan b8a307b4a6 Add support for kfet/history page 2017-03-18 14:05:11 -03:00
Ludovic Stephan 1d532616b7 Fix bugs introduced by previous commit 2017-03-18 02:47:30 -03:00
Ludovic Stephan 9e905b0f8b Remove kpsul dependence from history (oops) 2017-03-18 02:40:34 -03:00
Ludovic Stephan 644b08973a Add websocket support for history 2017-03-18 02:32:27 -03:00
Ludovic Stephan 34bb680570 Add History equivalent functions 2017-03-18 02:31:52 -03:00
Ludovic Stephan b655907bd4 Add history to KPsul manager 2017-03-18 02:29:02 -03:00
Ludovic Stephan 14b922634d Remove deprecated history functions 2017-03-18 02:28:01 -03:00
Ludovic Stephan 5c9c206f68 Adapt css to new canceled syntax 2017-03-18 02:25:44 -03:00
Ludovic Stephan b0b1fdf936 Add jsdoc comments to history models 2017-03-17 21:00:58 -03:00
Ludovic Stephan 565a054323 Add support for low stock css 2017-03-17 17:30:22 -03:00
Ludovic Stephan 58c57c6f89 Add hierarchy of needed models, w/ formatters 2017-03-17 16:00:49 -03:00
Ludovic Stephan 47fe74fbb0 template specification 2017-03-17 15:59:52 -03:00
Ludovic Stephan df0ea96b41 Adapt history_json return value to ModelForest standards 2017-03-17 15:59:21 -03:00
Ludovic Stephan 53f89f53e0 Merge branch 'Aufinal/refactor_articles' into Aufinal/refactor_history 2017-03-17 12:34:48 -03:00
Ludovic Stephan a7de396aa3 Better comparison control 2017-03-17 12:33:43 -03:00
Ludovic Stephan 11603cee69 Merge branch 'Aufinal/refactor_articles' into Aufinal/refactor_history 2017-03-17 00:19:45 -03:00
Ludovic Stephan 9ab2a11432 Finish adapting ArticleManager and Autocomplete 2017-03-16 22:26:59 -03:00
Ludovic Stephan 91f14deda1 last tweaks and doc 2017-03-16 22:26:30 -03:00
Ludovic Stephan 1c5ac561a3 Change article table into divs 2017-03-16 22:26:08 -03:00
Ludovic Stephan f0a80561ed Add article display to Config callback 2017-03-16 22:24:57 -03:00
Ludovic Stephan 01295d464d Adapt ArticleAutocomplete to new format 2017-03-16 01:22:46 -03:00
Ludovic Stephan 2ce96bce1b Add traverse function to ModelTree 2017-03-16 01:21:50 -03:00
Ludovic Stephan 770c185bd0 Modify sort in ModelTree 2017-03-16 01:21:18 -03:00
Ludovic Stephan 3d76079439 Add correct syntax to category data 2017-03-16 01:20:06 -03:00
Ludovic Stephan 08d1521d81 Adapt ArticleManagerto new API 2017-03-15 22:40:06 -03:00
Ludovic Stephan fe6823fc7b Adapt article_data return value to ModelTree standards 2017-03-15 22:39:30 -03:00
Ludovic Stephan 1570d9f494 Polish ModelForest class 2017-03-15 22:10:56 -03:00
Ludovic Stephan 0219d998ac model tree struct draft 2017-03-15 02:45:13 -03:00
Ludovic Stephan 66beeb5bd0 transfer history added 2017-03-11 01:41:21 -03:00
Ludovic Stephan ab6b0d52f2 Merge branch 'Aufinal/transferts_historique' into Aufinal/refactor_history 2017-03-11 00:45:54 -03:00
Ludovic Stephan ac2e773f9e opelist class 2017-03-10 22:09:23 -03:00
Ludovic Stephan 741bac880b dummy History test class 2017-03-10 22:09:02 -03:00
Ludovic Stephan 8eae3cee7f adapt history view 2017-03-10 22:08:44 -03:00
Ludovic Stephan 02485afd9b doc and compare function 2017-03-10 19:59:15 -03:00
Ludovic Stephan eac6c42041 day, opegroup and ope model drafts 2017-03-10 19:57:36 -03:00
Ludovic Stephan e1abff2242 last tweaks 2017-03-09 22:31:59 -03:00
Ludovic Stephan 8aa4fa2dce modify article API return 2017-03-09 09:21:07 -03:00
Ludovic Stephan c9b7683238 articlelist and modellist finished 2017-03-09 09:20:53 -03:00
Ludovic Stephan fe8e5d7e46 move and adapt manager and completion 2017-03-09 09:20:23 -03:00
Ludovic Stephan a05a075962 apimodellist & articlelist 2017-03-08 10:47:51 -03:00
Ludovic Stephan d5dfd5fa93 move ModelList def 2017-03-07 18:36:56 -03:00
Ludovic Stephan c9cce5b125 remove temp file 2017-03-07 17:58:10 -03:00
Ludovic Stephan 2cc0e0cffe modellist suite et fin 2017-03-07 17:57:40 -03:00
Ludovic Stephan db9c14f768 articlelist wip 2017-03-06 02:43:48 -03:00
Ludovic Stephan 643503269e add Article and Category models 2017-03-02 06:50:47 -03:00
Aurélien Delobelle a9cb50b38d Better k-fet js and more
JavaScript
----------
- Basic classes that can be inherited to define a new class for a
  django model in javascript.
- Formatters classes are used to render properties and attributes of
  the instances of models classes.
- New classes to handle Account, Checkout, Statement models.
- Refactor K-Psul JS (part n/m).
- Better file organization.

Views
-----
- 'kpsul.checkout_data' is cleaner. Last statement is added to the JSON
  response with GET paramater 'last_statement'.
- 'account.read.json' is merged in account.read. JSON response is sent if
  GET parametter 'format' is set to 'json'.
- Fix PEP8 of concerned views.

New requirement: django-js-reverse
----------------------------------
Used to resolve the URLs defined in the project in JavaScript.
See https://github.com/ierror/django-js-reverse
2017-02-23 22:07:38 +01:00
Aurélien Delobelle ee848d210e refactor account k-psul js - part 2/?
- les événements de AccountSearch sont enregistrés depuis ces classes (plus depuis le manager)
- ajout d'une classe AccountSelection s'occupant de la sélection d'un
  compte par l'utilisateur
- la méthode update de AccountManager peut maintenant prendre un
  trigramme et le set correctement, à défaut elle récupère le trigramme
  via AccountSelection
2017-02-15 14:09:20 +01:00
Aurélien Delobelle fdcf4c3ab0 fix property location 2017-02-13 13:48:57 +01:00
Aurélien Delobelle d9fc683525 update account_data[] to account_manager.account. 2017-02-13 00:41:41 +01:00
Aurélien Delobelle 890be9b343 refactor account k-psul js - part 1
- nouvelle classe - Account: stocke, sert et récupère les données
  associées à un compte
- nouvelle classe - AccountManager: interface pour le management de la
  partie Account de K-Psul
- nouvelle classe - AccountSearch: module de recherche d'un compte
- nouvelles classes - AccountFormatter, StandardAccountFormatter,
  LIQAccountFormatter: styles de formattage des données d'un compte

- désactive l'autocomplétion dans la recherche d'un compte
- fix #89: "Entrée" dans le champ de trigramme met le compte LIQ
2017-02-13 00:23:32 +01:00
Aurélien Delobelle 5c7a1d6874 Refactor JS Settings K-Psul
- change name: Settings -> Config
- provide interface `Config` to get/set parameters
- `Config` uses global object `window.config` to store key/value
- `Config` setters handle types
2017-02-12 13:26:02 +01:00
Aurélien Delobelle 8279bddf4e clean js k-psul
- K-Psul JavaScript uses strict-mode (when JS try to do better things,
  we should follow)
2017-02-12 06:10:17 +01:00
Ludovic Stephan 481409253b simpler string pluralizing 2017-02-11 21:44:32 -02:00
Ludovic Stephan 02735642f1 better pluralize 2017-02-11 21:41:09 -02:00
Ludovic Stephan fc2de20ab8 use switch + move text 2017-02-11 21:38:07 -02:00
Ludovic Stephan db94a8904c remove addition of new opegroups 2017-02-07 18:45:57 -02:00
Ludovic Stephan b404c989ff pep8 2017-02-05 03:42:02 -02:00
Ludovic Stephan 1d5e693045 ws for transfers 2017-02-05 03:26:15 -02:00
Ludovic Stephan 1ea334341b ws update for history and transfer pages 2017-02-05 03:04:41 -02:00
Ludovic Stephan f8aa67721c fix socket update 2017-02-05 02:19:20 -02:00
Ludovic Stephan 8895daff6a Merge branch 'Aufinal/transferts_historique' of git.eleves.ens.fr:cof-geek/gestioCOF into Aufinal/transferts_historique 2017-02-04 23:22:54 -02:00
Ludovic Stephan f06a732da5 remove unnecessary function 2016-12-11 23:29:05 -02:00
Ludovic Stephan e52c44580f pluralize function 2016-12-11 23:23:12 -02:00
Ludovic Stephan ee54b36696 minor imprevements to history 2016-12-11 23:22:59 -02:00
Ludovic Stephan a9e1cd01db add transfersonly option 2016-12-11 23:22:14 -02:00
Ludovic Stephan 3f35dc2c06 unite transfer history 2016-12-11 23:21:36 -02:00
Ludovic Stephan 1dbbad38b9 transfer cancellation html 2016-12-11 21:00:42 -02:00
Ludovic Stephan 66304359c0 unite cancel_ope and cancel_transfer 2016-12-11 16:22:55 -02:00
Ludovic Stephan 49bef61e53 filter transfers frop opes 2016-12-11 14:45:52 -02:00
Ludovic Stephan 2c2da60e54 send data for cancel 2016-12-10 23:52:26 -02:00
Ludovic Stephan 0b61a48c65 fix selection 2016-12-10 23:13:43 -02:00
Ludovic Stephan 85af7fe485 filter on id 2016-12-09 17:29:40 -02:00
Ludovic Stephan b3b49d5768 Merge branch 'k-fet' of git.eleves.ens.fr:cof-geek/gestioCOF into Aufinal/transferts_historique 2016-12-09 16:48:18 -02:00
Ludovic Stephan b0a21119fa Merge branch 'k-fet' of git.eleves.ens.fr:cof-geek/gestioCOF into Aufinal/transferts_historique 2016-12-09 01:25:40 -02:00
Ludovic Stephan 36edc334d4 add transfer information 2016-12-09 00:26:25 -02:00
Ludovic Stephan ac0356386a add css for transfers 2016-12-09 00:26:07 -02:00
Ludovic Stephan 027bc2e94b Merge branch 'k-fet' of git.eleves.ens.fr:cof-geek/gestioCOF into Aufinal/transferts_historique 2016-12-04 00:22:46 -02:00
Ludovic Stephan f747c0c389 print transfers (BROKEN) 2016-12-02 00:17:03 -02:00
Ludovic Stephan 5a354c61a0 fetch transfers as well 2016-12-02 00:16:40 -02:00
Ludovic Stephan 971848cb1b database lookups 2016-12-01 01:29:28 -02:00
19 changed files with 4412 additions and 2161 deletions

View file

@ -80,6 +80,7 @@ INSTALLED_APPS = [
'kfet.open',
'channels',
'widget_tweaks',
'django_js_reverse',
'custommail',
'djconfig',
'wagtail.wagtailforms',

View file

@ -8,9 +8,11 @@ from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls.static import static
from django.contrib import admin
from django.views.decorators.cache import cache_page
from django.views.generic.base import TemplateView
from django.contrib.auth import views as django_views
from django_cas_ng import views as django_cas_views
from django_js_reverse.views import urls_js
from wagtail.wagtailadmin import urls as wagtailadmin_urls
from wagtail.wagtailcore import urls as wagtail_urls
@ -89,6 +91,7 @@ urlpatterns = [
url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof),
url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente),
url(r'^k-fet/', include('kfet.urls')),
url(r'^jsreverse/$', cache_page(3600)(urls_js), name='js_reverse'),
url(r'^cms/', include(wagtailadmin_urls)),
url(r'^documents/', include(wagtaildocs_urls)),
# djconfig

View file

@ -44,6 +44,11 @@
width:90px;
}
#history .opegroup .infos {
text-align:center;
width:145px;
}
#history .opegroup .valid_by {
padding-left:20px
}
@ -71,6 +76,10 @@
text-align:right;
}
#history .ope .glyphicon {
padding-left:15px;
}
#history .ope .infos2 {
padding-left:15px;
}
@ -88,11 +97,11 @@
color:#FFF;
}
#history .ope.canceled, #history .transfer.canceled {
#history [canceled="true"] {
color:#444;
}
#history .ope.canceled::before, #history.transfer.canceled::before {
#history [canceled="true"]::before {
position: absolute;
content: ' ';
width:100%;

View file

@ -49,10 +49,10 @@ input[type=number]::-webkit-outer-spin-button {
height:120px;
}
#account[data-balance="ok"] #account_form input { background:#009011; color:#FFF;}
#account[data-balance="low"] #account_form input { background:#EC6400; color:#FFF; }
#account[data-balance="neg"] #account_form input { background:#C8102E; color:#FFF; }
#account[data-balance="frozen"] #account_form input { background:#000FBA; color:#FFF; }
#account[data_balance="ok"] #account_form input { background:#009011; color:#FFF;}
#account[data_balance="low"] #account_form input { background:#EC6400; color:#FFF; }
#account[data_balance="neg"] #account_form input { background:#C8102E; color:#FFF; }
#account[data_balance="frozen"] #account_form input { background:#000FBA; color:#FFF; }
#account_form {
padding:0;
@ -90,7 +90,7 @@ input[type=number]::-webkit-outer-spin-button {
font-size:12px;
}
#account_data #account-balance {
#account_data #account-balance_ukf {
height:40px;
line-height:40px;
@ -102,6 +102,10 @@ input[type=number]::-webkit-outer-spin-button {
font-weight:bold;
}
#account-is_cof {
font-weight:bold;
}
#account .buttons {
position:absolute;
bottom:0;
@ -119,7 +123,7 @@ input[type=number]::-webkit-outer-spin-button {
font-size:14px;
line-height:24px;
}
#account_data #account-balance {
#account_data #account-balance_ukf {
font-size:50px;
line-height:60px;
height:60px;
@ -255,6 +259,8 @@ input[type=number]::-webkit-outer-spin-button {
#article_selection {
height:40px;
width:100%;
border-bottom: 1px solid #c8102e;
line-height: 39px;
}
#article_selection input, #article_selection span {
@ -276,27 +282,17 @@ input[type=number]::-webkit-outer-spin-button {
padding-left:10px;
}
#article_number {
#article_number, #article_stock {
width:10%;
text-align:center;
}
#article_stock {
width:10%;
line-height:38px;
text-align:center;
}
@media (min-width:1200px) {
#article_autocomplete {
width:84%
}
#article_number {
width:8%;
}
#article_stock {
#article_number, #article_stock {
width:8%;
}
}
@ -306,28 +302,40 @@ input[type=number]::-webkit-outer-spin-button {
#articles_data {
overflow:auto;
max-height:500px;
}
#articles_data table {
width: 100%;
}
#articles_data table tr.article {
#articles_data div.article {
height:25px;
font-size:14px;
}
#articles_data table tr.article td:first-child {
#articles_data .article[data_stock="low"] {
background:rgba(236,100,0,0.3);
}
#articles_data span {
height:25px;
line-height:25px;
display: inline-block;
}
#articles_data span.name {
padding-left:10px;
width:78%;
}
#articles_data table tr.article td + td {
padding-right:10px;
text-align:right;
#articles_data span.price {
width:8%;
}
#articles_data table tr.category {
#articles_data span.stock {
width:14%;
}
#articles_data div.category {
height:35px;
line-height:35px;
background-color:#c8102e;
font-family:"Roboto Slab";
font-size:16px;
@ -335,7 +343,7 @@ input[type=number]::-webkit-outer-spin-button {
color:#FFF;
}
#articles_data table tr.category>td:first-child {
#articles_data div.category>span:first-child {
padding-left:20px;
}
@ -365,7 +373,10 @@ input[type=number]::-webkit-outer-spin-button {
#basket_rel, #previous_op {
border-top:1px solid #C8102E;
padding-left: 3px;
}
#basket_rel {
padding-top: 35px;
}
#basket {
@ -373,61 +384,81 @@ input[type=number]::-webkit-outer-spin-button {
}
@media (min-width:768px) {
#basket {
margin-right:7px;
}
#basket_rel {
#basket_rel, #previous_op {
border-top:0;
margin-left:7px;
margin-right:7px;
}
#previous_op {
border-top:0;
margin-left:7px;
margin-left:15px;
}
}
#basket table {
#basket > .items {
width:100%;
}
#basket table tr {
#basket .basket-item {
width: 100%;
height:25px;
font-size:14px;
}
#basket tr .amount {
#basket .basket-item > span {
display: inline-block;
}
#basket .basket-item .amount {
width:70px;
padding-right:15px;
text-align:right;
}
#basket tr .number {
width:50px;
#basket .basket-item .number {
width:75px;
padding-right:15px;
text-align:right;
}
#basket tr .lowstock {
display:none;
padding-right:15px;
#basket .basket-item > .lowstock {
width: 30px;
padding-right: 15px;
}
#basket tr.ui-selected, #basket tr.ui-selecting {
#basket .basket-item .glyphicon.lowstock {
display: none;
}
#basket .basket-item[low_stock=true] .glyphicon.lowstock {
display: inline-block;
}
#basket .basket-item.ui-selected,
#basket .basket-item.ui-selecting {
background-color:rgba(200,16,46,0.6);
color:#FFF;
}
.basket_summary {
margin: 0 auto;
text-align: right;
}
.basket_summary .name {
padding-right: 15px;
font-weight: bold;
}
/* History */
#previous_op .trigramme {
width:100%;
height: 30px;
background-color:rgba(200,16,46,0.85);
color:#FFF;
font-weight:bold;
padding:3px;
margin-left: -3px;
margin-bottom: 3px;
padding:5px;
margin-bottom: 5px;
text-align:center;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.kpsul_middle_right_col {

View file

@ -73,14 +73,19 @@
.jconfirm .capslock .glyphicon {
position: absolute;
display:none;
padding: 10px;
right: 0px;
top: 15px;
font-size: 30px;
display: none ;
margin-left: 60px !important;
}
.capslock_on .capslock .glyphicon{
display: inline-block !important;
}
.jconfirm .capslock input {
padding-right: 50px;
padding-left: 50px;

View file

@ -1,148 +1,220 @@
function KHistory(options={}) {
$.extend(this, KHistory.default_options, options);
var cancelHistory = new Event("cancel_done");
this.$container = $(this.container);
class KHistory {
this.reset = function() {
this.$container.html('');
};
static get default_options() {
return {
'templates': {
'purchase': '<div class="ope"><span class="amount"></span><span class="infos1"></span><span class="infos2"></span><span class="addcost"></span><span class="canceled"></span></div>',
'specialope': '<div class="ope"><span class="amount"></span><span class="infos1"></span><span class="infos2"></span><span class="addcost"></span><span class="canceled"></span></div>',
'opegroup': '<div class="opegroup"><span class="time"></span><span class="trigramme"></span><span class="amount"></span><span class="valid_by"></span><span class="comment"></span></div>',
'transfergroup': '<div class="opegroup"><span class="time"></span><span class="infos"></span><span class="valid_by"></span><span class="comment"></span></div>',
'day': '<div class="day"><span class="date"></span></div>',
'transfer': '<div class="ope"><span class="amount"></span><span class="infos1"></span><span class="glyphicon glyphicon-arrow-right"></span><span class="infos2"></span><span class="canceled"></span></div>',
},
this.addOpeGroup = function(opegroup) {
var $day = this._getOrCreateDay(opegroup['at']);
var $opegroup = this._opeGroupHtml(opegroup);
'api_options': {
from: moment().subtract(1, 'days').format('YYYY-MM-DD HH:mm:ss'),
},
$day.after($opegroup);
var trigramme = opegroup['on_acc_trigramme'];
var is_cof = opegroup['is_cof'];
for (var i=0; i<opegroup['opes'].length; i++) {
var $ope = this._opeHtml(opegroup['opes'][i], is_cof, trigramme);
$ope.data('opegroup', opegroup['id']);
$opegroup.after($ope);
}
};
}
this._opeHtml = function(ope, is_cof, trigramme) {
var $ope_html = $(this.template_ope);
var parsed_amount = parseFloat(ope['amount']);
var amount = amountDisplay(parsed_amount, is_cof, trigramme);
var infos1 = '', infos2 = '';
constructor(options) {
var all_options = $.extend(true, {}, this.constructor.default_options, options);
this.api_options = all_options.api_options;
if (ope['type'] == 'purchase') {
infos1 = ope['article_nb'];
infos2 = ope['article__name'];
} else {
infos1 = parsed_amount.toFixed(2)+'€';
switch (ope['type']) {
case 'initial':
infos2 = 'Initial';
break;
case 'withdraw':
infos2 = 'Retrait';
break;
case 'deposit':
infos2 = 'Charge';
break;
case 'edit':
infos2 = 'Édition';
break;
this._$container = $('#history');
this._$nb_opes = $('#nb_opes');
this.data = new OperationList();
if (!all_options.no_select)
this.selection = new KHistorySelection(this);
if (!all_options.static)
OperationWebSocket.add_handler(data => this.update_data(data));
var templates = all_options.templates;
if (all_options.no_trigramme)
templates['opegroup'] = '<div class="opegroup"><span class="time"></span><span class="amount"></span><span class="valid_by"></span><span class="comment"></span></div>';
this.display = new ForestDisplay(this._$container, templates, this.data);
this._init_events();
}
fetch(api_options) {
$.extend(this.api_options, api_options);
this.data.fromAPI(this.api_options);
}
_init_events() {
var that = this;
$(document).on('keydown', function(e) {
if (e.keyCode == 46 && that.selection) {
//DEL key ; we delete the selected operations (if any)
var to_cancel = that.selection.get_selected();
if (to_cancel['opes'].length > 0 || to_cancel['transfers'].length > 0)
that.cancel_operations(to_cancel);
}
});
$(this.data).on("changed", function() {
let nb_opes = 0;
that.data.traverse(Operation, function(o) {
if (!o.canceled_at)
nb_opes++;
});
that._$nb_opes.text(nb_opes);
});
}
cancel_operations(to_cancel) {
var that = this;
var on_success = function() {
if (that.selection)
that.selection.reset();
$(that).trigger("cancel_done");
};
api_with_auth({
url: Urls['kfet.kpsul.cancel_operations'](),
data: to_cancel,
on_success: on_success,
});
}
is_valid(opegroup) {
var options = this.api_options;
if (options.from && dateUTCToParis(opegroup.content.at).isBefore(moment(options.from)))
return false;
if (options.to && dateUTCToParis(opegroup.content.at).isAfter(moment(options.to)))
return false;
var accounts_filter = options.accounts && options.accounts.length;
var checkouts_filter = options.checkouts && options.checkouts.length;
if (opegroup.modelname == 'opegroup') {
if (options.transfersonly)
return false;
if (accounts_filter && options.accounts.indexOf(opegroup.content.account_id) < 0)
return false;
if (checkouts_filter && options.checkouts.indexOf(opegroup.content.checkout_id) < 0)
return false;
} else if (opegroup.modelname == 'transfergroup') {
if (options.opesonly)
return false;
if (checkouts_filter)
return false;
if (accounts_filter) {
opegroup.content.children =
opegroup.content.children.filter( function(transfer) {
var is_from_in =
options.accounts.indexOf(transfer.content.from_acc_id) >= 0;
var is_to_in =
options.accounts.indexOf(transfer.content.to_acc_id) >= 0;
return is_from_in || is_to_in;
});
if (opegroup.content.children.length == 0)
return false;
}
}
$ope_html
.data('ope', ope['id'])
.find('.amount').text(amount).end()
.find('.infos1').text(infos1).end()
.find('.infos2').text(infos2).end();
return true;
}
var addcost_for = ope['addcost_for__trigramme'];
if (addcost_for) {
var addcost_amount = parseFloat(ope['addcost_amount']);
$ope_html.find('.addcost').text('('+amountDisplay(addcost_amount, is_cof)+'UKF pour '+addcost_for+')');
update_data(data) {
var opegroups = data['opegroups'];
var opes = data['opes'];
for (let ope of opes) {
if (ope['cancellation']) {
let update_data = {
'canceled_at': ope.canceled_at,
'canceled_by': ope.canceled_by,
};
let model;
if (ope.modelname === 'ope')
model = Operation;
else if (ope.modelname === 'transfer')
model = Transfer;
this.data.update(model, ope.id, update_data);
}
}
if (ope['canceled_at'])
this.cancelOpe(ope, $ope_html);
for (let opegroup of opegroups) {
if (opegroup['cancellation']) {
let update_data = { 'amount': opegroup.amount };
this.data.update('opegroup', opegroup.id, update_data);
}
if (opegroup['add'] && this.is_valid(opegroup)) {
this.data.create(opegroup.modelname, opegroup.content);
}
}
return $ope_html;
}
this.cancelOpe = function(ope, $ope = null) {
if (!$ope)
$ope = this.findOpe(ope['id']);
var cancel = 'Annulé';
var canceled_at = dateUTCToParis(ope['canceled_at']);
if (ope['canceled_by__trigramme'])
cancel += ' par '+ope['canceled_by__trigramme'];
cancel += ' le '+canceled_at.format('DD/MM/YY à HH:mm:ss');
$ope.addClass('canceled').find('.canceled').text(cancel);
}
this._opeGroupHtml = function(opegroup) {
var $opegroup_html = $(this.template_opegroup);
var at = dateUTCToParis(opegroup['at']).format('HH:mm:ss');
var trigramme = opegroup['on_acc__trigramme'];
var amount = amountDisplay(
parseFloat(opegroup['amount']), opegroup['is_cof'], trigramme);
var comment = opegroup['comment'] || '';
$opegroup_html
.data('opegroup', opegroup['id'])
.find('.time').text(at).end()
.find('.amount').text(amount).end()
.find('.comment').text(comment).end()
.find('.trigramme').text(trigramme).end();
if (!this.display_trigramme)
$opegroup_html.find('.trigramme').remove();
if (opegroup['valid_by__trigramme'])
$opegroup_html.find('.valid_by').text('Par '+opegroup['valid_by__trigramme']);
return $opegroup_html;
}
this._getOrCreateDay = function(date) {
var at = dateUTCToParis(date);
var at_ser = at.format('YYYY-MM-DD');
var $day = this.$container.find('.day').filter(function() {
return $(this).data('date') == at_ser
});
if ($day.length == 1)
return $day;
var $day = $(this.template_day).prependTo(this.$container);
return $day.data('date', at_ser).text(at.format('D MMMM'));
}
this.findOpeGroup = function(id) {
return this.$container.find('.opegroup').filter(function() {
return $(this).data('opegroup') == id
});
}
this.findOpe = function(id) {
return this.$container.find('.ope').filter(function() {
return $(this).data('ope') == id
});
}
this.cancelOpeGroup = function(opegroup) {
var $opegroup = this.findOpeGroup(opegroup['id']);
var trigramme = $opegroup.find('.trigramme').text();
var amount = amountDisplay(
parseFloat(opegroup['amount'], opegroup['is_cof'], trigramme));
$opegroup.find('.amount').text(amount);
}
}
KHistory.default_options = {
container: '#history',
template_day: '<div class="day"></div>',
template_opegroup: '<div class="opegroup"><span class="time"></span><span class="trigramme"></span><span class="amount"></span><span class="valid_by"></span><span class="comment"></span></div>',
template_ope: '<div class="ope"><span class="amount"></span><span class="infos1"></span><span class="infos2"></span><span class="addcost"></span><span class="canceled"></span></div>',
display_trigramme: true,
class KHistorySelection {
constructor(history) {
this._$container = history._$container;
this._init();
}
_init() {
this._$container.selectable({
filter: 'div.opegroup, div.ope',
selected: function(e, ui) {
$(ui.selected).each(function() {
if ($(this).hasClass('opegroup')) {
$(this).parent().find('.ope').addClass('ui-selected');
}
});
},
unselected: function(e, ui) {
$(ui.unselected).each(function() {
if ($(this).hasClass('opegroup')) {
$(this).parent().find('.ope').removeClass('ui-selected');
}
});
},
});
}
get_selected() {
var selected = {
transfers: [],
opes: []
};
this._$container.find('.ope.ui-selected').each(function() {
let object = $(this).parent().data("object");
if (object instanceof Transfer)
selected.transfers.push(object.id);
else
selected.opes.push(object.id);
});
return selected;
}
reset() {
this._$container.find('.ui-selected')
.removeClass('.ui-selected');
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,141 @@
/**
* @file Miscellaneous JS definitions for <tt>k-fet</tt> app.
* @copyright 2017 cof-geek
* @license MIT
*/
/**
* String method
* @memberof String
* @return {String} String formatted as trigramme
*/
String.prototype.formatTri = function() {
return this.toUpperCase().substr(0, 3);
}
/**
* String method
* @global
* @return {Boolean} true iff String follows trigramme pattern
*/
String.prototype.isValidTri = function() {
var pattern = /^[^a-z]{3}$/;
return pattern.test(this);
}
/**
* Checks if given argument is float ;
* if not, parses given argument to float value.
* @global
* @return {float}
*/
function floatCheck(v) {
if (typeof v === 'number')
return v;
return Number.parseFloat(v);
}
function intCheck(v) {
return Number.parseInt(v);
}
function floatCheck(v) {
if (typeof v === 'number')
return v;
return Number.parseFloat(v);
}
function booleanCheck(v) {
return v == true;
}
/**
* Short: Equivalent to python str format.
* Source: [MDN]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals}.
* @example
* var t1Closure = template`${0}${1}${0}!`;
* t1Closure('Y', 'A'); // "YAY!"
* @example
* var t2Closure = template`${0} ${'foo'}!`;
* t2Closure('Hello', {foo: 'World'}); // "Hello World!"
*/
function template(strings, ...keys) {
return (function(...values) {
var dict = values[values.length - 1] || {};
var result = [strings[0]];
keys.forEach(function(key, i) {
var value = Number.isInteger(key) ? values[key] : dict[key];
result.push(value, strings[i + 1]);
});
return result.join('');
});
}
/**
* Get and store K-Psul config from API.
* <br><br>
*
* Config should be accessed statically only.
*/
class Config {
/**
* Get or create config object.
* @private
* @return {object} object - config keys/values
*/
static _get_or_create_config() {
if (window.config === undefined)
window.config = {};
return window.config;
}
/**
* Get config from API.
* @param {jQueryAjaxComplete} [callback] - A function to be called when
* the request finishes.
*/
static reset(callback) {
$.getJSON(Urls['kfet.kpsul.get_settings']())
.done(function(data) {
for (var key in data) {
Config.set(key, data[key]);
}
})
.always(callback);
}
/**
* Get value for key in config.
* @param {string} key
*/
static get(key) {
if (key == "addcost")
return this.get("addcost_for") && this.get("addcost_amount");
return this._get_or_create_config()[key];
}
/**
* Set value for key in config.
* @param {string} key
* @param {*} value
*/
static set(key, value) {
// API currently returns string for Decimal type
if (['addcost_amount', 'subvention_cof'].indexOf(key) > -1)
value = floatCheck(value);
this._get_or_create_config()[key] = value;
}
}
/*
* CSRF Token
*/
@ -19,6 +156,7 @@ $.ajaxSetup({
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
function add_csrf_form($form) {
@ -28,6 +166,29 @@ function add_csrf_form($form) {
}
/*
* Capslock management
*/
window.capslock = -1;
$(document).on('keypress', function(e) {
var s = String.fromCharCode(e.which);
if ((s.toUpperCase() === s && s.toLowerCase() !== s && !e.shiftKey)|| //caps on, shift off
(s.toUpperCase() !== s && s.toLowerCase() === s && e.shiftKey)) { //caps on, shift on
$('body').addClass('capslock_on')
} else if ((s.toLowerCase() === s && s.toUpperCase() !== s && !e.shiftKey)|| //caps off, shift off
(s.toLowerCase() !== s && s.toUpperCase() === s && e.shiftKey)) { //caps off, shift on
$('body').removeClass('capslock_on')
}
});
$(document).on('keydown', function(e) {
if (e.which == 20) {
$('body').toggleClass('capslock_on')
}
});
/*
* Generic Websocket class and k-psul ws instanciation
*/
@ -84,15 +245,15 @@ function dateUTCToParis(date) {
return moment.tz(date, 'UTC').tz('Europe/Paris');
}
function amountDisplay(amount, is_cof=false, tri='') {
function amountDisplay(amount, is_cof=false, tri='', account=true) {
if (tri == 'LIQ')
return (- amount).toFixed(2) +'€';
return amountToUKF(amount, is_cof);
return amountToUKF(amount, is_cof, account).toString();
}
function amountToUKF(amount, is_cof=false, account=false) {
var rounding = account ? Math.floor : Math.round ;
var coef_cof = is_cof ? 1 + settings['subvention_cof'] / 100 : 1;
var coef_cof = is_cof ? 1 + Config.get('subvention_cof') / 100 : 1;
return rounding(amount * coef_cof * 10);
}
@ -101,6 +262,58 @@ function isValidTrigramme(trigramme) {
return trigramme.match(pattern);
}
/**
* Dialogs with user via jconfirm
*/
class UserDialog {
static get defaults() {
return {'title': '', 'content': ''};
}
constructor(data) {
$.extend(this, this.constructor.defaults, data);
}
open(settings) {
// Arg management
var pre_content = settings.pre_content || '';
var post_content = settings.post_content || '';
var callback = settings.callback || $.noop;
var that = this;
$.confirm({
title: this.title,
content: pre_content + this.content + post_content,
backgroundDismiss: true,
animation:'top',
closeAnimation:'bottom',
keyboardEnabled: true,
confirm: function() {
var inputs = {};
this.$content.find('input').each(function () {
inputs[$(this).attr('name')] = $(this).val();
});
if (Object.keys(inputs).length > 1)
return callback(inputs);
else
return callback(inputs[Object.keys(inputs)[0]]);
},
onOpen: function() {
var that = this
this.$content.find('input').on('keydown', function(e) {
if (e.keyCode == 13) {
e.preventDefault();
that.$confirmButton.click();
}
});
},
onClose: function() { if (settings.next_focus) { this._lastFocused = settings.next_focus; } }
});
}
}
function getErrorsHtml(data) {
var content = '';
if (!data)
@ -148,59 +361,77 @@ function getErrorsHtml(data) {
return content;
}
function requestAuth(data, callback, focus_next = null) {
var content = getErrorsHtml(data);
content += '<div class="capslock"><span class="glyphicon glyphicon-lock"></span><input type="password" name="password" autofocus><div>',
$.confirm({
title: 'Authentification requise',
content: content,
function displayErrors(html) {
$.alert({
title: 'Erreurs',
content: html,
backgroundDismiss: true,
animation:'top',
closeAnimation:'bottom',
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
confirm: function() {
var password = this.$content.find('input').val();
callback(password);
},
onOpen: function() {
var that = this;
var capslock = -1 ; // 1 -> caps on ; 0 -> caps off ; -1 or 2 -> unknown
this.$content.find('input').on('keypress', function(e) {
if (e.keyCode == 13)
that.$confirmButton.click();
var s = String.fromCharCode(e.which);
if ((s.toUpperCase() === s && s.toLowerCase() !== s && !e.shiftKey)|| //caps on, shift off
(s.toUpperCase() !== s && s.toLowerCase() === s && e.shiftKey)) { //caps on, shift on
capslock = 1 ;
} else if ((s.toLowerCase() === s && s.toUpperCase() !== s && !e.shiftKey)|| //caps off, shift off
(s.toLowerCase() !== s && s.toUpperCase() === s && e.shiftKey)) { //caps off, shift on
capslock = 0 ;
}
if (capslock == 1)
$('.capslock .glyphicon').show() ;
else if (capslock == 0)
$('.capslock .glyphicon').hide() ;
});
// Capslock key is not detected by keypress
this.$content.find('input').on('keydown', function(e) {
if (e.which == 20) {
capslock = 1-capslock ;
}
if (capslock == 1)
$('.capslock .glyphicon').show() ;
else if (capslock == 0)
$('.capslock .glyphicon').hide() ;
});
},
onClose: function() {
if (focus_next)
this._lastFocused = focus_next;
}
});
}
var authDialog = new UserDialog({
'title': 'Authentification requise',
'content': '<div class="capslock"><span class="glyphicon glyphicon-lock"></span><input type="password" name="password" autofocus><div>',
});
//Note/TODO: the returned ajax object can be improved by allowing chaining on errors 403/400
function api_with_auth(settings, password) {
if (window.api_lock == 1)
return false;
window.api_lock = 1;
var url = settings.url;
if (!url)
return false;
var data = settings.data || {} ;
var on_success = settings.on_success || $.noop ;
var on_400 = settings.on_400 || $.noop ;
return $.ajax({
dataType: "json",
url: url,
method: "POST",
data: data,
beforeSend: function ($xhr) {
$xhr.setRequestHeader("X-CSRFToken", csrftoken);
if (password)
$xhr.setRequestHeader("KFetPassword", password);
},
})
.done(function(data) {
on_success(data);
})
.fail(function($xhr) {
var response = $xhr.responseJSON;
switch ($xhr.status) {
case 403:
authDialog.open({
callback: function(password) {
api_with_auth(settings, password)
},
pre_content: getErrorsHtml(response),
next_focus: settings.next_focus,
});
break;
case 400:
on_400(response);
break;
}
})
.always(function() {
window.api_lock = 0;
});
}
String.prototype.pluralize = function(count, irreg_plural) {
if (Math.abs(count) >= 2)
return irreg_plural ? irreg_plural : this+'s' ;
return this ;
}
/**
* Setup jquery-confirm

View file

@ -0,0 +1,994 @@
/**
* @file K-Psul JS
* @copyright 2017 cof-geek
* @license MIT
*/
class KPsulManager {
constructor(env) {
this._env = env;
this.account_manager = new AccountManager(this);
this.checkout_manager = new CheckoutManager(this);
this.article_manager = new ArticleManager(this);
this.basket = new BasketManager(this);
this.history = new KHistory({
api_options: {'opesonly': true},
});
this.previous_basket = new PreviousBasket(this);
this._init_events();
}
reset(soft) {
soft = soft || false;
this.basket.reset();
this.account_manager.reset();
this.article_manager.reset();
if (!soft) {
this.checkout_manager.reset();
this.previous_basket.reset();
Config.reset( () => {
this.article_manager.fetch_data();
this.history.fetch();
});
}
return this;
}
focus() {
if (this.checkout_manager.is_empty())
this.checkout_manager.focus();
else if (this.account_manager.is_empty())
this.account_manager.focus();
else
this.article_manager.focus();
return this;
}
_init_events() {
$(this.history).on("cancel_done", () => this.reset(true).focus());
}
}
class AccountManager {
constructor() {
this._$container = $('#account');
this.account = new Account();
this.selection = new AccountSelection(this);
this.search = new AccountSearch(this);
// buttons: search, read or create
this._$buttons_container = this._$container.find('.buttons');
this._buttons_templates = {
create: template`<a href="${'url'}" class="btn btn-primary" target="_blank" title="Créer ce compte"><span class="glyphicon glyphicon-plus"></span></a>`,
read: template`<a href="${'url'}" class="btn btn-primary" target="_blank" title="Détails du compte"><span class="glyphicon glyphicon-info-sign"></span></a>`,
search: template`<button class="btn btn-primary search" title="Rechercher"><span class="glyphicon glyphicon-search"></span></button>`,
};
}
is_empty() { return this.account.is_empty(); }
display() {
this._display_data();
this._display_buttons();
}
_display_data() {
this.account.display(this._$container, {
'prefix_prop': '#account-',
});
}
_display_buttons() {
var buttons;
if (this.is_empty()) {
var trigramme = this.selection.get();
if (trigramme.isValidTri()) {
let url = Account.url_create(trigramme);
buttons = this._buttons_templates['create']({url: url});
} else { /* trigramme input is empty or invalid */
buttons = this._buttons_templates['search']();
}
} else { /* an account is loaded */
let url = this.account.url_read;
buttons = this._buttons_templates['read']({url: url});
}
this._$buttons_container.html(buttons);
}
update(trigramme) {
if (trigramme !== undefined)
this.selection.set(trigramme);
trigramme = trigramme || this.selection.get();
if (trigramme.isValidTri()) {
this.account.get_by_apipk(trigramme)
.done( () => this._update_on_success() )
.fail( () => this.reset_data() );
} else {
this.reset_data();
}
}
_update_on_success() {
$('#id_on_acc').val(this.account.id);
this.display();
kpsul.focus();
$(this).trigger("changed", [this.account]);
}
reset() {
$('#id_on_acc').val(0);
this.selection.reset();
this.search.reset();
this.reset_data();
}
reset_data() {
this.account.clear();
this.display();
}
focus() {
this.selection.focus();
return this;
}
}
class AccountSelection {
constructor(manager) {
this.manager = manager;
this._$input = $('#id_trigramme');
this._init_events();
}
_init_events() {
var that = this;
// user change trigramme
this._$input
.on('input', () => this.manager.update());
// LIQ shortcuts
this._$input
.on('keydown', function(e) {
// keys: 13:Enter|40:Arrow-Down
if (e.keyCode == 13 || e.keyCode == 40)
that.manager.update('LIQ');
});
}
get() {
return this._$input.val().formatTri();
}
set(v) {
this._$input.val(v);
}
focus() {
this._$input.focus();
}
reset() {
this.set('');
}
}
class AccountSearch {
constructor(manager) {
this.manager = manager;
this._content = '<input type="text" name="q" id="search_autocomplete" autocomplete="off" spellcheck="false" autofocus><div id="account_results"></div>';
this._input = '#search_autocomplete';
this._results_container = '#account_results';
this._init_outer_events();
}
open() {
var that = this;
this._$dialog = $.dialog({
title: 'Recherche de compte',
content: this._content,
backgroundDismiss: true,
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
onOpen: function() {
that._$input = $(that._input);
that._$results_container = $(that._results_container);
that._init_form()
._init_inner_events();
},
});
}
_init_form() {
var that = this;
this._$input.yourlabsAutocomplete({
url: Urls['kfet.account.search.autocomplete'](),
minimumCharacters: 2,
id: 'search_autocomplete',
choiceSelector: '.choice',
placeholder: "Chercher un utilisateur K-Fêt",
container: that._$results_container,
box: that._$results_container,
fixPosition: function() {},
});
return this;
}
_init_outer_events() {
var that = this;
/* open search on button click */
this.manager._$container
.on('click', '.search', () => this.open());
/* open search on Ctrl-F */
this.manager._$container
.on('keydown', function(e) {
if (e.which == 70 && e.ctrlKey) {
that.open();
e.preventDefault();
}
});
}
_init_inner_events() {
this._$input.bind('selectChoice',
(e, choice, autocomplete) => this._on_select(e, choice, autocomplete)
);
return this;
}
_on_select(e, choice, autocomplete) {
this.manager.update(choice.find('.trigramme').text());
this.reset();
}
reset() {
if (this._$dialog !== undefined) {
this._$dialog.close();
}
}
}
class CheckoutManager {
constructor(kpsul) {
this.kpsul = kpsul;
this._$container = $('#checkout');
this.display_prefix = '#checkout-';
this.checkout = new Checkout();
this.selection = new CheckoutSelection(this);
this._$laststatement_container = $('#last_statement');
this.laststatement = new Statement();
this.laststatement_display_prefix = '#checkout-last_statement_';
this._$buttons_container = this._$container.find('.buttons');
this._buttons_templates = {
read: template`<a class="btn btn-primary" href="${'url'}" title="En savoir plus" target="_blank"><span class="glyphicon glyphicon-info-sign"></span></a>`,
statement_create: template`<a href="${'url'}" title="Effectuer un relevé" class="btn btn-primary" target="_blank"><span class="glyphicon glyphicon-euro"></span></a>`,
};
OperationWebSocket.add_handler(data => this.update_data(data));
}
update(id) {
if (id !== undefined)
this.selection.set(id);
id = id || this.selection.get();
var api_options = {
'last_statement': true,
};
this.checkout.get_by_apipk(id, api_options)
.done( (data) => this._update_on_success(data) )
.fail( () => this.reset_data() )
.always( () => this.kpsul.focus() );
}
_update_on_success(data) {
if (data['laststatement'] !== undefined)
this.laststatement.from(data['laststatement']);
$('#id_checkout').val(this.checkout.id);
this.display();
}
update_data(ws_data) {
let data = ws_data["checkouts"].find(o => o.id === this.checkout.id);
if (!data)
return;
this.checkout.update(data);
this._update_on_success(data);
}
is_empty() { return this.checkout.is_empty(); }
display() {
this._display_data();
this._display_laststatement();
this._display_buttons();
}
_display_data() {
this.checkout.display(this._$container, {
'prefix_prop': this.display_prefix,
});
}
_display_laststatement() {
if (this.laststatement.is_empty()) {
this._$laststatement_container.hide();
} else {
this.laststatement.display(this._$laststatement_container, {
'prefix_prop': this.laststatement_display_prefix
});
this._$laststatement_container.show();
}
}
_display_buttons() {
var buttons = '';
if (!this.is_empty()) {
var url_newcheckout = Statement.url_create(this.checkout.id);
buttons += this._buttons_templates['statement_create']({
url: url_newcheckout});
var url_read = this.checkout.url_read;
buttons += this._buttons_templates['read']({url: url_read});
}
this._$buttons_container.html(buttons);
}
reset() {
$('#id_checkout').val(0);
this.selection.reset();
this.reset_data();
if (this.selection.choices.length == 1)
this.update(this.selection.choices[0]);
}
reset_data() {
this.checkout.clear();
this.laststatement.clear();
this.display();
}
focus() {
this.selection.focus();
return this;
}
}
class CheckoutSelection {
constructor(manager) {
this.manager = manager;
this._$input = $('#id_checkout_select');
this._init_events();
this.choices =
this._$input.find('option[value!=""]')
.toArray()
.map(function(opt) {
return parseInt($(opt).attr('value'));
});
}
_init_events() {
this._$input.on('change', () => this.manager.update());
}
get() {
return this._$input.val() || 0;
}
set(v) {
this._$input.find('option[value='+ v +']').prop('selected', true);
}
reset() {
this._$input.find('option:first').prop('selected', true);
}
focus() {
this._$input.focus();
return this;
}
}
class ArticleManager {
constructor(kpsul) {
this.kpsul = kpsul; // Global K-Psul Manager
this._$container = $('#articles_data');
this._$input = $('#article_autocomplete');
this._$nb = $('#article_number');
this._$stock = $('#article_stock');
this.selected = null;
this.data = new ArticleList();
var $container = $('#articles_data');
var templates = {
category: '<div class="category"><span class="name"></span></div>',
article: '<div class="article"><span class="name"></span><span class="price"></span><span class="stock"></span></div>',
};
this.display = new ForestDisplay($container, templates, this.data);
this.autocomplete = new ArticleAutocomplete(this, $container);
this._init_events();
OperationWebSocket.add_handler(data => this.update_data(data));
}
get nb() {
return this._$nb.val();
}
validate(article) {
this.selected = article;
this._$input.val(article.name);
this._$nb.val('1');
this._$stock.text('/'+article.stock);
this._$nb.focus().select();
}
unset() {
this.selected = null;
}
is_empty() {
return !this.selected || this.selected.is_empty();
}
fetch_data() {
this.data.fromAPI();
}
update_data(data) {
for (let article_data of data.articles)
this.data.update('article', article_data.id, article_data);
}
reset() {
this.unset();
this._$stock.text('');
this._$nb.val('');
this._$input.val('');
this.autocomplete.showAll();
return this;
}
_init_events() {
var that = this;
// 8:Backspace|9:Tab|13:Enter|46:DEL|112-117:F1-6|119-123:F8-F12
var normalKeys = /^(8|9|13|46|112|113|114|115|116|117|119|120|121|122|123)$/;
var arrowKeys = /^(37|38|39|40)$/;
//Global input event (to merge ?)
this._$input.on('keydown', function(e) {
if (e.keyCode == 13 && that._$input.val() == '') {
that.kpsul._env.performOperations();
}
});
this._$container.on('click', '.article', function() {
let article = $(this).parent().data("object");
that.validate(article);
});
this._$nb.on('keydown', function(e) {
if (e.keyCode == 13 && that.constructor.check_nb(that.nb) && !that.is_empty()) {
that.kpsul.basket.add_purchase(that.selected, parseInt(that.nb));
that.reset().focus();
}
if (normalKeys.test(e.keyCode) || arrowKeys.test(e.keyCode) || e.ctrlKey) {
if (e.ctrlKey && e.charCode == 65)
that._$nb.val('');
return true;
}
if (that.constructor.check_nb(that.nb+e.key))
return true;
return false;
});
}
//Note : this function may not be needed after the whole rework
get_article(id) {
return this.data.find('article', id);
}
focus() {
if (this.is_empty())
this._$input.focus();
else
this._$nb.focus();
return this;
}
static check_nb(nb) {
return /^[0-9]+$/.test(nb) && nb > 0 && nb <= 24;
}
}
class ArticleAutocomplete {
constructor(article_manager, $container) {
this.manager = article_manager;
this._$container = $container;
this._$input = $('#article_autocomplete');
this.showAll();
this._init_events();
}
_init_events() {
var that = this;
// 8:Backspace|9:Tab|13:Enter|35:End|36:Home|37-40:Arrows|46:DEL|112-123:F1-F12
var normalKeys = /^(8|9|13|35|36|37|38|39|40|46|112|113|114|115|116|117|119|120|121|122|123)$/;
this._$input
.on('keydown', function(e) {
if (normalKeys.test(e.keyCode) || e.ctrlKey)
return true;
let initial = that._$input.val();
let future = initial.substr(0, this.selectionStart)
+ e.key
+ initial.substr(this.selectionEnd);
that.update(future);
return false;
});
this._$input
.on('input', () => this.update(this._$input.val(), false));
}
update(prefix, autofill) {
if (autofill === undefined)
autofill = true;
this.matching = this.find_matching(prefix);
if (this.matching.length === 1 && autofill) {
this.manager.validate(this.matching[0]);
this.showAll();
} else {
this.manager.unset();
if (this.matching.length >= 1) {
this.updateDisplay();
if (autofill)
this.updateInput();
}
}
}
updateDisplay() {
let that = this;
let display = this.manager.display;
this.manager.data.traverse('category', function(category) {
var is_active = false;
for (let article of category.articles) {
let $article = display.get_dom(article);
if (that.matching.indexOf(article) != -1) {
is_active = true;
$article.show();
} else {
$article.hide();
}
}
let $category = display.get_dom(category);
is_active ? $category.show() : $category.hide();
});
}
updateInput() {
if (!this.matching.length)
return;
let names = this.matching.map( article => article.name.toLowerCase() ).sort();
let first = names[0], last = names[names.length-1],
length = first.length, i = 0;
while (i < length && first.charAt(i) === last.charAt(i)) i++;
this._$input.val(first.substring(0,i));
}
showAll() {
this.matching = this.find_matching("");
this.updateDisplay();
}
find_matching(start) {
let lower = start.toLowerCase();
let matching = [];
this.manager.data.traverse('article', function(article) {
if (article.name.toLowerCase().startsWith(lower))
matching.push(article);
});
return matching;
}
}
class BasketManager {
constructor(kpsul) {
this.kpsul = kpsul;
this._$container = $('#basket');
let item_template = '<div class="basket-item"><span class="amount"></span><span class="number"></span><span class="lowstock"><span class="lowstock glyphicon glyphicon-alert"></span></span><span class="name"></span></div>';
let templates = {
purchase: item_template,
specialope: item_template,
};
this.data = new BasketData();
this.display = new ForestDisplay(this._$container, templates, this.data);
this.summary = new BasketSummary(this);
this.formset = new BasketFormset(this);
this.selection = new BasketSelection(this);
this._init_events();
}
_init_events() {
$(this.data).on("changed", (e) => $(this).trigger("changed", [e]));
$(this.kpsul.account_manager).on("changed", (e) => $(this.data).trigger("update_all", [e]));
}
is_empty() {
return this.data.is_empty();
}
reset() {
this.data.clear();
this.formset.reset();
this.selection.reset();
}
total_amount() {
let total = 0;
for (let ope of this.data.roots)
total += ope.amount;
return Number(Math.round(total*100)/100);
}
add_purchase(article, nb) {
let found = this.find_purchase(article);
if (found) {
let new_nb = found.article_nb + nb;
if (new_nb > 0) {
found.update({
article_nb: found.article_nb + nb
});
$(this.data).trigger("updated", [found]);
this.formset.update(found.id, found.for_formset());
} else {
this.delete(found.id);
}
} else {
let created = this.data.create("purchase", {
id: this.formset.new_index(),
article: article,
article_nb: nb
});
this.formset.create(created.for_formset());
}
}
find_purchase(article) {
return this.data.find("purchase", (purchase) => purchase.article.id === article.id);
}
add_deposit(amount) {
this._add_special("deposit", amount);
}
add_withdraw(amount) {
this._add_special("withdraw", - amount);
}
add_edit(amount) {
this._add_special("edit", amount);
}
_add_special(type, amount) {
let created = this.data.create("specialope", {
id: this.formset.new_index(),
type: type,
amount: amount
});
this.formset.create(created.for_formset());
}
delete(id) {
this.data.delete(Operation, id);
this.formset.delete(id);
}
}
class BasketSummary {
constructor(basket) {
this.basket = basket;
this._$container = $("#basket_rel");
this._init_events();
}
_init_events() {
$(this.basket).on("changed", () => this.update_infos());
}
update_infos() {
let items = [];
if (!this.basket.is_empty() && !kpsul.account_manager.is_empty()) {
let account = kpsul.account_manager.account;
let amount = this.basket.total_amount();
let total_str = amountDisplay(amount, account.is_cof, account.trigramme, false);
items.push(['Total', total_str]);
if (account.trigramme == "LIQ") {
let abs_amount = Math.abs(amount);
for (let given of [5, 10, 20])
if (abs_amount < given)
items.push(this.rendu(abs_amount, given));
} else {
let new_balance = account.balance + amount;
let new_balance_str = amountDisplay(
new_balance, account.is_cof, account.trigramme);
items.push(['Nouveau solde', new_balance_str]);
if (new_balance < 0)
items.push(['Manque', (- new_balance).toFixed(2)+'€']);
}
}
this._$container.html(this._$render(items));
}
rendu(amount, given) {
return [given.toString()+'€', (given - amount).toFixed(2)+'€'];
}
_$render(items) {
let $elt = $('<table>');
for (let item of items)
this._$render_item(item[0], item[1]).appendTo($elt);
return $elt;
}
_$render_item(name, value) {
return $('<tr><td class="name">'+name+'</td><td class="value">'+value+'</td></tr>');
}
}
class BasketFormset {
constructor(basket) {
this.basket = basket;
this._$container = $('#operation_formset');
this._$mngmt_total_forms_input = $('#id_form-TOTAL_FORMS');
this._mngmt_total_forms = 1;
this._prefix_regex = /__prefix__/;
this._$empty_html =
$('#operation_empty_html')
.removeAttr('id')
.find('label').remove().end()
.find('#id_form-__prefix__-DELETE').css('display', 'none').end();
$('#id_form-0-DELETE').prop('checked', true);
}
reset() {
this._mngmt_total_forms = 1;
this._$mngmt_total_forms_input.val(1);
this._$container.find("[data-opeindex]").remove();
}
new_index() {
let index = this._mngmt_total_forms++;
this._$mngmt_total_forms_input.val(index + 1);
return index;
}
create(data) {
let that = this;
let $ope = this._$empty_html.clone();
let index = data.id;
$ope.attr('data-opeindex', index);
$ope.find(':input').each( function() {
let name = $(this).attr('name').replace(that._prefix_regex, index);
let id = 'id_' + name;
$(this).attr({
name: name,
id: id
});
});
this._$container.append($ope);
this._update(index, data);
return index;
}
update(index, data) {
this._update(index, data);
}
delete(index) {
this.update(index, {
delete: true,
});
}
_update(index, data) {
let $ope = this._$container.find(`[data-opeindex=${index}]`);
let selector_prefix = `#id_form-${index}-`;
$ope.find(selector_prefix + 'type').val(data.type);
if (data.amount !== undefined)
$ope.find(selector_prefix + 'amount').val(data.amount.toFixed(2));
if (data.article !== undefined)
$ope.find(selector_prefix + 'article').val(data.article.id);
if (data.article_nb !== undefined)
$ope.find(selector_prefix + 'article_nb').val(data.article_nb);
if (data.delete !== undefined)
$ope.find(selector_prefix + 'DELETE').prop('checked', true);
}
}
class PreviousBasket {
constructor(kpsul) {
this.kpsul = kpsul;
this._$container = $("#previous_op");
}
update() {
let account = this.kpsul.account_manager.account;
let html = ``;
html += `<div class="trigramme">${account.trigramme} - ${account.name}</div>`;
html += this.kpsul.basket.summary._$container.html();
this._$container.html(html);
}
reset() {
this._$container.html("");
}
}
class BasketSelection {
constructor(basket) {
this.basket = basket;
this._$container = basket._$container;
this._init();
}
_init() {
this._$container.selectable({
filter: ".basket-item"
});
this._init_events();
}
_init_events() {
let that = this;
let basket = this.basket;
$(document).on('keydown', function (e) {
switch(e.which) {
case 46:
// DEL (Suppr)
that.get().each( (_, item) => basket.delete(item.id) );
break;
case 38:
// Arrow up
that.get().each( (_, item) => {
if (item instanceof PurchaseBasket)
basket.add_purchase(item.article, 1);
});
break;
case 40:
// Arrow down
that.get().each( (_, item) => {
if (item instanceof PurchaseBasket)
basket.add_purchase(item.article, -1);
});
break;
}
});
}
get() {
return this.$get().map( function() {
return $(this).parent().data("object");
});
}
reset() {
this.$get().removeClass('ui-selected');
}
$get() {
let filter = this._$container.selectable('option', 'filter');
return this._$container.find(filter+'.ui-selected');
}
}

View file

@ -4,6 +4,7 @@
{% load l10n %}
{% block extra_head %}
<script type="text/javascript" src="{% static 'kfet/js/kfet.api.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
{% if account.user == request.user %}
<script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script>
@ -85,33 +86,11 @@ $(document).ready(function() {
<script type="text/javascript">
$(document).ready(function() {
settings = { 'subvention_cof': parseFloat({{ kfet_config.subvention_cof|unlocalize }})}
'use strict';
khistory = new KHistory({
display_trigramme: false,
});
var khistory = new KHistory({'no_trigramme': true});
function getHistory() {
var data = {
'accounts': [{{ account.pk }}],
}
$.ajax({
dataType: "json",
url : "{% url 'kfet.history.json' %}",
method : "POST",
data : data,
})
.done(function(data) {
for (var i=0; i<data['opegroups'].length; i++) {
khistory.addOpeGroup(data['opegroups'][i]);
}
var nb_opes = khistory.$container.find('.ope:not(.canceled)').length;
$('#nb_opes').text(nb_opes);
});
}
getHistory();
Config.reset(() => khistory.fetch({'accounts': [{{account.pk}}]}));
});
</script>

View file

@ -29,6 +29,7 @@
<script type="text/javascript" src="{% static 'kfet/js/moment-fr.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-timezone-with-data-2010-2020.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/kfet.js' %}"></script>
<script type="text/javascript" src="{% url 'js_reverse' %}"></script>
{% include "kfetopen/init.html" %}

View file

@ -3,6 +3,7 @@
{% block extra_head %}
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/multiple-select.css' %}">
<script type="text/javascript" src="{% static 'kfet/js/kfet.api.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/multiple-select.js' %}"></script>
{{ filter_form.media }}
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
@ -32,14 +33,14 @@
{% block main %}
<table id="history" class="table">
</table>
<div id="history">
</div>
<script type="text/javascript">
$(document).ready(function() {
settings = { 'subvention_cof': parseFloat({{ kfet_config.subvention_cof|unlocalize }})}
'use strict';
khistory = new KHistory();
var khistory = new KHistory();
var $from_date = $('#id_from_date');
var $to_date = $('#id_to_date');
@ -54,7 +55,9 @@ $(document).ready(function() {
return selected;
}
function getHistory() {
function updateHistory() {
// Get API options
var data = {};
if ($from_date.val())
data['from'] = moment($from_date.val()).format('YYYY-MM-DD HH:mm:ss');
@ -64,21 +67,11 @@ $(document).ready(function() {
if ($checkouts)
data['checkouts'] = checkouts;
var accounts = getSelectedMultiple($accounts);
data['accounts'] = accounts;
if (accounts)
data['accounts'] = accounts.map(id => parseInt(id));
$.ajax({
dataType: "json",
url : "{% url 'kfet.history.json' %}",
method : "POST",
data : data,
})
.done(function(data) {
for (var i=0; i<data['opegroups'].length; i++) {
khistory.addOpeGroup(data['opegroups'][i]);
}
var nb_opes = khistory.$container.find('.ope:not(.canceled)').length;
$('#nb_opes').text(nb_opes);
});
// Update history
khistory.fetch(data);
}
let defaults_datetimepicker = {
@ -92,8 +85,9 @@ $(document).ready(function() {
$from_date.datetimepicker($.extend({}, defaults_datetimepicker, {
defaultDate: moment().subtract(24, 'hours'),
}));
$to_date.datetimepicker($.extend({}, defaults_datetimepicker, {
defaultDate: moment(),
defaultDate: moment().add(5, 'minutes') // workaround for 'stepping' rounding
}));
$("#from_date").on("dp.change", function (e) {
@ -111,131 +105,11 @@ $(document).ready(function() {
countSelected: "# sur %"
});
$("input").on('dp.change change', function() {
khistory.reset();
getHistory();
$("#update_history").on('click', function() {
updateHistory();
});
khistory.$container.selectable({
filter: 'div.opegroup, div.ope',
selected: function(e, ui) {
$(ui.selected).each(function() {
if ($(this).hasClass('opegroup')) {
var opegroup = $(this).data('opegroup');
$(this).siblings('.ope').filter(function() {
return $(this).data('opegroup') == opegroup
}).addClass('ui-selected');
}
});
},
});
$(document).on('keydown', function (e) {
if (e.keyCode == 46) {
// DEL (Suppr)
var opes_to_cancel = [];
khistory.$container.find('.ope.ui-selected').each(function () {
opes_to_cancel.push($(this).data('ope'));
});
if (opes_to_cancel.length > 0)
confirmCancel(opes_to_cancel);
}
});
function confirmCancel(opes_to_cancel) {
var nb = opes_to_cancel.length;
var content = nb+" opérations vont être annulées";
$.confirm({
title: 'Confirmation',
content: content,
backgroundDismiss: true,
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
confirm: function() {
cancelOperations(opes_to_cancel);
}
});
}
function requestAuth(data, callback) {
var content = getErrorsHtml(data);
content += '<input type="password" name="password" autofocus>',
$.confirm({
title: 'Authentification requise',
content: content,
backgroundDismiss: true,
animation:'top',
closeAnimation:'bottom',
keyboardEnabled: true,
confirm: function() {
var password = this.$content.find('input').val();
callback(password);
},
onOpen: function() {
var that = this;
this.$content.find('input').on('keypress', function(e) {
if (e.keyCode == 13)
that.$confirmButton.click();
});
},
});
}
function getErrorsHtml(data) {
var content = '';
if ('missing_perms' in data['errors']) {
content += 'Permissions manquantes';
content += '<ul>';
for (var i=0; i<data['errors']['missing_perms'].length; i++)
content += '<li>'+data['errors']['missing_perms'][i]+'</li>';
content += '</ul>';
}
if ('negative' in data['errors']) {
var url_base = "{% url 'kfet.account.update' LIQ}";
url_base = base_url(0, url_base.length-8);
for (var i=0; i<data['errors']['negative'].length; i++) {
content += '<a class="btn btn-primary" href="'+url_base+data['errors']['negative'][i]+'/edit" target="_blank">Autorisation de négatif requise pour '+data['errors']['negative'][i]+'</a>';
}
}
return content;
}
function cancelOperations(opes_array, password = '') {
var data = { 'operations' : opes_array }
$.ajax({
dataType: "json",
url : "{% url 'kfet.kpsul.cancel_operations' %}",
method : "POST",
data : data,
beforeSend: function ($xhr) {
$xhr.setRequestHeader("X-CSRFToken", csrftoken);
if (password != '')
$xhr.setRequestHeader("KFetPassword", password);
},
})
.done(function(data) {
khistory.$container.find('.ui-selected').removeClass('ui-selected');
})
.fail(function($xhr) {
var data = $xhr.responseJSON;
switch ($xhr.status) {
case 403:
requestAuth(data, function(password) {
cancelOperations(opes_array, password);
});
break;
case 400:
displayErrors(getErrorsHtml(data));
break;
}
});
}
getHistory();
Config.reset(updateHistory);
});
</script>

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,22 @@
{% extends 'kfet/base_col_2.html' %}
{% load staticfiles %}
{% block extra_head %}
<script type="text/javascript" src="{% static 'kfet/js/kfet.api.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
{% endblock %}
{% block title %}Transferts{% endblock %}
{% block header-title %}Transferts{% endblock %}
{% block fixed %}
<aside>
<div class="heading">
<div id="nb_opes"></div>
<div class="sub">transferts</div>
</div>
</aside>
<div class="buttons">
<a class="btn btn-primary" href="{% url 'kfet.transfers.create' %}">
Nouveaux
@ -16,109 +27,17 @@
{% block main %}
<div id="history">
{% for transfergroup in transfergroups %}
<div class="opegroup transfergroup" data-transfergroup="{{ transfergroup.pk }}">
<span>{{ transfergroup.at }}</span>
<span>{{ transfergroup.valid_by.trigramme }}</span>
<span>{{ transfergroup.comment }}</span>
</div>
{% for transfer in transfergroup.transfers.all %}
<div class="ope transfer{% if transfer.canceled_at %} canceled{% endif %}" data-transfer="{{ transfer.pk }}" data-transfergroup="{{ transfergroup.pk }}">
<span class="amount">{{ transfer.amount }} €</span>
<span class="from_acc">{{ transfer.from_acc.trigramme }}</span>
<span class="glyphicon glyphicon-arrow-right"></span>
<span class="to_acc">{{ transfer.to_acc.trigramme }}</span>
</div>
{% endfor %}
{% endfor %}
<div id="history" class="table">
</div>
<script type="text/javascript">
$(document).ready(function() {
'use strict';
lock = 0;
function displayErrors(html) {
$.alert({
title: 'Erreurs',
content: html,
backgroundDismiss: true,
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
});
}
function cancelTransfers(transfers_array, password = '') {
if (lock == 1)
return false
lock = 1;
var data = { 'transfers' : transfers_array }
$.ajax({
dataType: "json",
url : "{% url 'kfet.transfers.cancel' %}",
method : "POST",
data : data,
beforeSend: function ($xhr) {
$xhr.setRequestHeader("X-CSRFToken", csrftoken);
if (password != '')
$xhr.setRequestHeader("KFetPassword", password);
},
})
.done(function(data) {
for (var i=0; i<data['canceled'].length; i++) {
$('#history').find('.transfer[data-transfer='+data['canceled'][i]+']')
.addClass('canceled');
}
$('#history').find('.ui-selected').removeClass('ui-selected');
lock = 0;
})
.fail(function($xhr) {
var data = $xhr.responseJSON;
switch ($xhr.status) {
case 403:
requestAuth(data, function(password) {
cancelTransfers(transfers_array, password);
});
break;
case 400:
displayErrors(getErrorsHtml(data));
break;
}
lock = 0;
});
}
$('#history').selectable({
filter: 'div.transfergroup, div.transfer',
selected: function(e, ui) {
$(ui.selected).each(function() {
if ($(this).hasClass('transfergroup')) {
var transfergroup = $(this).attr('data-transfergroup');
$(this).siblings('.ope').filter(function() {
return $(this).attr('data-transfergroup') == transfergroup
}).addClass('ui-selected');
}
});
},
});
$(document).on('keydown', function (e) {
if (e.keyCode == 46) {
// DEL (Suppr)
var transfers_to_cancel = [];
$('#history').find('.transfer.ui-selected').each(function () {
transfers_to_cancel.push($(this).attr('data-transfer'));
});
if (transfers_to_cancel.length > 0)
cancelTransfers(transfers_to_cancel);
}
});
var khistory = new KHistory();
Config.reset(() => khistory.fetch({'transfersonly': true}));
});
</script>

View file

@ -3,6 +3,7 @@
{% block extra_head %}
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/transfers_form.css' %}">
<script type="text/javascript" src="{% url 'js_reverse' %}"></script>
{% endblock %}
{% block title %}Nouveaux transferts{% endblock %}
@ -51,14 +52,12 @@
<script type="text/javascript">
$(document).ready(function () {
function getAccountData(trigramme, callback = function() {}) {
$.ajax({
dataType: "json",
url : "{% url 'kfet.account.read.json' %}",
method : "POST",
data : { trigramme: trigramme },
success : callback,
});
function getAccountData(trigramme, callback) {
callback = callback || $.noop;
$.getJSON(Urls['kfet.account.read'](trigramme), {
'format': 'json',
})
.done(callback);
}
function updateAccountData(trigramme, $input) {

View file

@ -12,7 +12,7 @@ from ..config import kfet_config
from ..models import (
Account, Article, ArticleCategory, Checkout, CheckoutStatement, Inventory,
InventoryArticle, Operation, OperationGroup, Order, OrderArticle, Supplier,
SupplierArticle, Transfer, TransferGroup,
SupplierArticle, TransferGroup,
)
from .testcases import ViewTestCaseMixin
from .utils import create_team, create_user, get_perms
@ -248,6 +248,26 @@ class AccountReadViewTests(ViewTestCaseMixin, TestCase):
r = client.get(self.url)
self.assertEqual(r.status_code, 200)
def test_json(self):
r = self.client.get(self.url, {'format': 'json'})
self.assertEqual(r.status_code, 200)
content = json.loads(r.content.decode('utf-8'))
expected = {
'name': 'first last',
'trigramme': '001',
'balance': '0.00',
}
self.assertDictContainsSubset(expected, content)
self.assertSetEqual(set(content.keys()), set([
'id', 'trigramme', 'first_name', 'last_name', 'name', 'email',
'is_cof', 'promo', 'balance', 'is_frozen', 'departement',
'nickname',
]))
class AccountUpdateViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.account.update'
@ -746,18 +766,53 @@ class CheckoutReadViewTests(ViewTestCaseMixin, TestCase):
def setUp(self):
super().setUp()
self.checkout = Checkout.objects.create(
name='Checkout',
created_by=self.accounts['team'],
valid_from=self.now,
valid_to=self.now + timedelta(days=5),
valid_from=datetime(2018, 1, 12, 16, 20, tzinfo=timezone.utc),
valid_to=datetime(2018, 1, 17, 16, 20, tzinfo=timezone.utc),
)
# TODO: This statement is currently created by CheckoutCreate view, it
# should rather be done in Checkout.save().
with mock.patch('django.utils.timezone.now') as mock_now:
mock_now.return_value = (
datetime(2018, 1, 12, 16, 20, tzinfo=timezone.utc))
self.checkout.statements.create(
balance_new=0, balance_old=0, amount_taken=0,
by=self.accounts['team'],
)
def test_ok(self):
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.context['checkout'], self.checkout)
def test_json(self):
r = self.client.get(self.url, {
'format': 'json',
'last_statement': '1',
})
self.assertEqual(r.status_code, 200)
content = json.loads(r.content.decode('utf-8'))
self.assertEqual(content, {
'id': self.checkout.pk,
'name': 'Checkout',
'balance': '0.00',
'valid_from': '2018-01-12T16:20:00Z',
'valid_to': '2018-01-17T16:20:00Z',
'laststatement': {
'id': self.checkout.statements.all()[0].pk,
'at': '2018-01-12T16:20:00Z',
'balance_new': '0.00',
'balance_old': '0.00',
'by': str(self.accounts['team']),
},
})
class CheckoutUpdateViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.checkout.update'
@ -1406,46 +1461,6 @@ class KPsulViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(r.status_code, 200)
class KPsulCheckoutDataViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.kpsul.checkout_data'
url_expected = '/k-fet/k-psul/checkout_data'
http_methods = ['POST']
auth_user = 'team'
auth_forbidden = [None, 'user']
def setUp(self):
super().setUp()
self.checkout = Checkout.objects.create(
name='Checkout',
balance=Decimal('10'),
created_by=self.accounts['team'],
valid_from=self.now,
valid_to=self.now + timedelta(days=5),
)
def test_ok(self):
r = self.client.post(self.url, {'pk': self.checkout.pk})
self.assertEqual(r.status_code, 200)
content = json.loads(r.content.decode('utf-8'))
expected = {
'name': 'Checkout',
'balance': '10.00',
}
self.assertDictContainsSubset(expected, content)
self.assertSetEqual(set(content.keys()), set([
'balance', 'id', 'name', 'valid_from', 'valid_to',
'last_statement_at', 'last_statement_balance',
'last_statement_by_first_name', 'last_statement_by_last_name',
'last_statement_by_trigramme',
]))
class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.kpsul.perform_operations'
url_expected = '/k-fet/k-psul/perform_operations'
@ -1465,11 +1480,82 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
http_methods = ['POST']
auth_user = 'team'
auth_user = 'team1'
auth_forbidden = [None, 'user']
def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
'kfet.perform_negative_operations',
]),
'u1': create_user('user1', '001'),
'u2': create_user('user2', '002'),
'u3': create_user('user3', '003'),
}
def setUp(self):
super().setUp()
trg1 = TransferGroup.objects.create()
self.trg1_1 = trg1.transfers.create(
from_acc=self.accounts['u1'],
to_acc=self.accounts['u2'],
amount='3.5',
)
self.trg1_2 = trg1.transfers.create(
from_acc=self.accounts['u2'],
to_acc=self.accounts['u3'],
amount='2.4',
)
trg2 = TransferGroup.objects.create(
at=(
timezone.now() -
(kfet_config.cancel_duration + timedelta(seconds=1))
),
)
self.trg2_1 = trg2.transfers.create(
from_acc=self.accounts['u1'],
to_acc=self.accounts['u2'],
amount='5',
)
def test_ok(self):
pass
data = {
'transfers[]': [str(self.trg1_1.pk), str(self.trg1_2.pk)],
}
r = self.client.post(self.url, data)
self.assertEqual(r.status_code, 200)
u1 = self.accounts['u1']
u1.refresh_from_db()
self.assertEqual(u1.balance, Decimal('3.5'))
u2 = self.accounts['u2']
u2.refresh_from_db()
self.assertEqual(u2.balance, Decimal('-1.1'))
u3 = self.accounts['u3']
u3.refresh_from_db()
self.assertEqual(u3.balance, Decimal('-2.4'))
def test_error_tooold(self):
data = {
'transfers[]': [str(self.trg2_1.pk)],
}
r = self.client.post(self.url, data)
self.assertEqual(r.status_code, 403)
self.assertDictEqual(json.loads(r.content.decode('utf-8')), {
'canceled': {},
'warnings': {},
'errors': {
'missing_perms': ['Annuler des commandes non récentes'],
},
})
class KPsulArticlesData(ViewTestCaseMixin, TestCase):
@ -1481,16 +1567,10 @@ class KPsulArticlesData(ViewTestCaseMixin, TestCase):
def setUp(self):
super().setUp()
category = ArticleCategory.objects.create(name='Catégorie')
self.article1 = Article.objects.create(
category=category,
name='Article 1',
)
self.article2 = Article.objects.create(
category=category,
name='Article 2',
price=Decimal('2.5'),
)
self.ac = ArticleCategory.objects.create(name='Catégorie')
self.a1 = self.ac.articles.create(name='Article 1')
self.a2 = self.ac.articles.create(name='Article 2', price='2.5')
def test_ok(self):
r = self.client.get(self.url)
@ -1498,24 +1578,35 @@ class KPsulArticlesData(ViewTestCaseMixin, TestCase):
content = json.loads(r.content.decode('utf-8'))
articles = content['articles']
expected_list = [{
'category__name': 'Catégorie',
'name': 'Article 1',
'price': '0.00',
}, {
'category__name': 'Catégorie',
'name': 'Article 2',
'price': '2.50',
}]
for expected, article in zip(expected_list, articles):
self.assertDictContainsSubset(expected, article)
self.assertSetEqual(set(article.keys()), set([
'id', 'name', 'price', 'stock',
'category_id', 'category__name', 'category__has_addcost',
]))
self.assertEqual(content, {
'objects': {
'article': [
{
'id': self.a1.pk,
'name': 'Article 1',
'price': '0.00',
'stock': 0,
'category__id': self.a1.category.pk,
},
{
'id': self.a2.pk,
'name': 'Article 2',
'price': '2.50',
'stock': 0,
'category__id': self.a2.category.pk,
},
],
},
'related': {
'category': [
{
'id': self.ac.pk,
'name': 'Catégorie',
'has_addcost': True,
},
],
},
})
class KPsulUpdateAddcost(ViewTestCaseMixin, TestCase):
@ -1581,34 +1672,6 @@ class HistoryJSONViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(r.status_code, 200)
class AccountReadJSONViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.account.read.json'
url_expected = '/k-fet/accounts/read.json'
http_methods = ['POST']
auth_user = 'team'
auth_forbidden = [None, 'user']
def test_ok(self):
r = self.client.post(self.url, {'trigramme': '000'})
self.assertEqual(r.status_code, 200)
content = json.loads(r.content.decode('utf-8'))
expected = {
'name': 'first last',
'trigramme': '000',
'balance': '0.00',
}
self.assertDictContainsSubset(expected, content)
self.assertSetEqual(set(content.keys()), set([
'balance', 'departement', 'email', 'id', 'is_cof', 'is_frozen',
'name', 'nickname', 'promo', 'trigramme',
]))
class SettingsListViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.settings'
url_expected = '/k-fet/settings/'
@ -1761,62 +1824,6 @@ class TransferPerformViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(team1.balance, Decimal('2.4'))
class TransferCancelViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.transfers.cancel'
url_expected = '/k-fet/transfers/cancel'
http_methods = ['POST']
auth_user = 'team1'
auth_forbidden = [None, 'user', 'team']
def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
# Convenience
'kfet.perform_negative_operations',
]),
}
@property
def post_data(self):
return {
'transfers[]': [self.transfer1.pk, self.transfer2.pk],
}
def setUp(self):
super().setUp()
group = TransferGroup.objects.create()
self.transfer1 = Transfer.objects.create(
group=group,
from_acc=self.accounts['user'],
to_acc=self.accounts['team'],
amount='3.5',
)
self.transfer2 = Transfer.objects.create(
group=group,
from_acc=self.accounts['team'],
to_acc=self.accounts['root'],
amount='2.4',
)
def test_ok(self):
r = self.client.post(self.url, self.post_data)
self.assertEqual(r.status_code, 200)
user = self.accounts['user']
user.refresh_from_db()
self.assertEqual(user.balance, Decimal('3.5'))
team = self.accounts['team']
team.refresh_from_db()
self.assertEqual(team.balance, Decimal('-1.1'))
root = self.accounts['root']
root.refresh_from_db()
self.assertEqual(root.balance, Decimal('-2.4'))
class InventoryListViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.inventory'
url_expected = '/k-fet/inventaires/'

View file

@ -161,8 +161,6 @@ urlpatterns = [
# -----
url('^k-psul/$', views.kpsul, name='kfet.kpsul'),
url('^k-psul/checkout_data$', views.kpsul_checkout_data,
name='kfet.kpsul.checkout_data'),
url('^k-psul/perform_operations$', views.kpsul_perform_operations,
name='kfet.kpsul.perform_operations'),
url('^k-psul/cancel_operations$', views.kpsul_cancel_operations,
@ -180,9 +178,6 @@ urlpatterns = [
url(r'^history.json$', views.history_json,
name='kfet.history.json'),
url(r'^accounts/read.json$', views.account_read_json,
name='kfet.account.read.json'),
# -----
# Settings urls
@ -204,8 +199,6 @@ urlpatterns = [
name='kfet.transfers.create'),
url(r'^transfers/perform$', views.perform_transfers,
name='kfet.transfers.perform'),
url(r'^transfers/cancel$', views.cancel_transfers,
name='kfet.transfers.cancel'),
# -----
# Inventories urls

View file

@ -17,7 +17,7 @@ from django.contrib.auth.models import User, Permission
from django.http import JsonResponse, Http404
from django.forms import formset_factory
from django.db import transaction
from django.db.models import F, Sum, Prefetch, Count
from django.db.models import Q, F, Sum, Prefetch, Count
from django.db.models.functions import Coalesce
from django.utils import timezone
from django.utils.decorators import method_decorator
@ -49,12 +49,36 @@ from decimal import Decimal
import heapq
import statistics
from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes, WeekScale
from .auth.views import ( # noqa
account_group, login_generic, AccountGroupCreate, AccountGroupUpdate,
)
# source : docs.djangoproject.com/fr/1.10/topics/class-based-views/mixins/
class JSONResponseMixin(object):
"""
A mixin that can be used to render a JSON response.
"""
def render_to_json_response(self, context, **response_kwargs):
"""
Returns a JSON response, transforming 'context' to make the payload.
"""
return JsonResponse(
self.get_data(context),
**response_kwargs
)
def get_data(self, context):
"""
Returns an object that will be serialized as JSON by json.dumps().
"""
# Note: This is *EXTREMELY* naive; in reality, you'll need
# to do much more complex handling to ensure that arbitrary
# objects -- such as Django model instances or querysets
# -- can be serialized as JSON.
return context
def put_cleaned_data_in_dict(dict, form):
for field in form.cleaned_data:
dict[field] = form.cleaned_data[field]
@ -325,6 +349,13 @@ def account_read(request, trigramme):
request.user != account.user):
raise PermissionDenied
if request.GET.get('format') == 'json':
export_keys = ['id', 'trigramme', 'first_name', 'last_name', 'name',
'email', 'is_cof', 'promo', 'balance', 'is_frozen',
'departement', 'nickname']
data = {k: getattr(account, k) for k in export_keys}
return JsonResponse(data)
addcosts = (
OperationGroup.objects
.filter(opes__addcost_for=account,
@ -340,6 +371,7 @@ def account_read(request, trigramme):
'addcosts': addcosts,
})
# Account - Update
@ -540,18 +572,38 @@ class CheckoutCreate(SuccessMessageMixin, CreateView):
return super(CheckoutCreate, self).form_valid(form)
# Checkout - Read
class CheckoutRead(DetailView):
class CheckoutRead(JSONResponseMixin, DetailView):
model = Checkout
template_name = 'kfet/checkout_read.html'
context_object_name = 'checkout'
def get_context_data(self, **kwargs):
context = super(CheckoutRead, self).get_context_data(**kwargs)
context['statements'] = context['checkout'].statements.order_by('-at')
context = super().get_context_data(**kwargs)
checkout = self.object
if self.request.GET.get('last_statement'):
context['laststatement'] = checkout.statements.latest('at')
else:
context['statements'] = checkout.statements.order_by('-at')
return context
def render_to_response(self, context, **kwargs):
if self.request.GET.get('format') == 'json':
export_keys = ['id', 'name', 'balance', 'valid_from', 'valid_to']
data = {k: getattr(self.object, k) for k in export_keys}
if 'laststatement' in context:
last_st = context['laststatement']
export_keys = ['id', 'at', 'balance_new', 'balance_old']
last_st_data = {k: getattr(last_st, k) for k in export_keys}
last_st_data['by'] = str(last_st.by)
data['laststatement'] = last_st_data
return self.render_to_json_response(data)
else:
return super().render_to_response(context, **kwargs)
# Checkout - Update
class CheckoutUpdate(SuccessMessageMixin, UpdateView):
@ -635,7 +687,24 @@ class CheckoutStatementCreate(SuccessMessageMixin, CreateView):
form.instance.balance_new = getAmountBalance(form.cleaned_data)
form.instance.checkout_id = self.kwargs['pk_checkout']
form.instance.by = self.request.user.profile.account_kfet
return super(CheckoutStatementCreate, self).form_valid(form)
res = super(CheckoutStatementCreate, self).form_valid(form)
ws_data = {
'id': self.object.id,
'at': self.object.at,
'balance_new': self.object.balance_new,
'balance_old': self.object.balance_old,
'by': str(self.object.by),
}
consumers.KPsul.group_send('kfet.kpsul', {
'checkouts': [{
'id': self.object.checkout.id,
'laststatement': ws_data,
}],
})
return res
class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView):
model = CheckoutStatement
@ -850,50 +919,6 @@ def kpsul_get_settings(request):
return JsonResponse(data)
@teamkfet_required
def account_read_json(request):
trigramme = request.POST.get('trigramme', '')
account = get_object_or_404(Account, trigramme=trigramme)
data = { 'id': account.pk, 'name': account.name, 'email': account.email,
'is_cof': account.is_cof, 'promo': account.promo,
'balance': account.balance, 'is_frozen': account.is_frozen,
'departement': account.departement, 'nickname': account.nickname,
'trigramme': account.trigramme }
return JsonResponse(data)
@teamkfet_required
def kpsul_checkout_data(request):
pk = request.POST.get('pk', 0)
if not pk:
pk = 0
data = (
Checkout.objects
.annotate(
last_statement_by_first_name=F('statements__by__cofprofile__user__first_name'),
last_statement_by_last_name=F('statements__by__cofprofile__user__last_name'),
last_statement_by_trigramme=F('statements__by__trigramme'),
last_statement_balance=F('statements__balance_new'),
last_statement_at=F('statements__at'))
.select_related(
'statements'
'statements__by',
'statements__by__cofprofile__user')
.filter(pk=pk)
.order_by('statements__at')
.values(
'id', 'name', 'balance', 'valid_from', 'valid_to',
'last_statement_balance', 'last_statement_at',
'last_statement_by_trigramme', 'last_statement_by_last_name',
'last_statement_by_first_name')
.last()
)
if data is None:
raise Http404
return JsonResponse(data)
@teamkfet_required
def kpsul_update_addcost(request):
addcost_form = AddcostForm(request.POST)
@ -1083,31 +1108,47 @@ def kpsul_perform_operations(request):
websocket_data = {}
websocket_data['opegroups'] = [{
'add': True,
'id': operationgroup.pk,
'amount': operationgroup.amount,
'checkout__name': operationgroup.checkout.name,
'at': operationgroup.at,
'is_cof': operationgroup.is_cof,
'comment': operationgroup.comment,
'valid_by__trigramme': (operationgroup.valid_by and
operationgroup.valid_by.trigramme or None),
'on_acc__trigramme': operationgroup.on_acc.trigramme,
'opes': [],
'modelname': 'opegroup',
'content': {
'id': operationgroup.pk,
'amount': operationgroup.amount,
'at': operationgroup.at,
'is_cof': operationgroup.is_cof,
'comment': operationgroup.comment,
'valid_by': (operationgroup.valid_by and
operationgroup.valid_by.trigramme or None),
'trigramme': operationgroup.on_acc.trigramme,
# Used to filter websocket updates
'account_id': operationgroup.on_acc.pk,
'checkout_id': operationgroup.checkout.pk,
'children': [],
},
}]
for operation in operations:
for ope in operations:
ope_data = {
'id': operation.pk, 'type': operation.type,
'amount': operation.amount,
'addcost_amount': operation.addcost_amount,
'addcost_for__trigramme': (
operation.addcost_for and addcost_for.trigramme or None),
'article__name': (
operation.article and operation.article.name or None),
'article_nb': operation.article_nb,
'group_id': operationgroup.pk,
'canceled_by__trigramme': None, 'canceled_at': None,
'content': {
'id': ope.id,
'amount': ope.amount,
'canceled_at': None,
'canceled_by': None,
},
}
websocket_data['opegroups'][0]['opes'].append(ope_data)
if ope.type == Operation.PURCHASE:
ope_data['modelname'] = 'purchase'
ope_data['content'].update({
'article_name': ope.article.name,
'article_nb': ope.article_nb,
'addcost_amount': ope.addcost_amount,
'addcost_for':
ope.addcost_for and ope.addcost_for.trigramme or None,
})
else:
ope_data['modelname'] = 'specialope'
ope_data['content'].update({
'type': ope.type,
})
websocket_data['opegroups'][0]['content']['children'].append(ope_data)
# Need refresh from db cause we used update on queryset
operationgroup.checkout.refresh_from_db()
websocket_data['checkouts'] = [{
@ -1130,37 +1171,63 @@ def kpsul_perform_operations(request):
@teamkfet_required
def kpsul_cancel_operations(request):
# Pour la réponse
data = { 'canceled': [], 'warnings': {}, 'errors': {}}
data = {'canceled': {}, 'warnings': {}, 'errors': {}}
# Checking if BAD REQUEST (opes_pk not int or not existing)
try:
# Set pour virer les doublons
opes_post = set(map(int, filter(None, request.POST.getlist('operations[]', []))))
opes_post = (
set(map(int, filter(None, request.POST.getlist('opes[]', []))))
)
transfers_post = (
set(map(int, filter(None, request.POST.getlist('transfers[]', []))))
)
except ValueError:
return JsonResponse(data, status=400)
opes_all = (
Operation.objects
.select_related('group', 'group__on_acc', 'group__on_acc__negative')
.filter(pk__in=opes_post))
opes_pk = [ ope.pk for ope in opes_all ]
opes_notexisting = [ ope for ope in opes_post if ope not in opes_pk ]
if opes_notexisting:
data['errors']['opes_notexisting'] = opes_notexisting
opes_pk = [ope.pk for ope in opes_all]
opes_notexisting = [ope for ope in opes_post if ope not in opes_pk]
transfers_all = (
Transfer.objects
.select_related('group', 'from_acc', 'from_acc__negative',
'to_acc', 'to_acc__negative')
.filter(pk__in=transfers_post))
transfers_pk = [transfer.pk for transfer in transfers_all]
transfers_notexisting = [transfer for transfer in transfers_post
if transfer not in transfers_pk]
if transfers_notexisting or opes_notexisting:
if transfers_notexisting:
data['errors']['transfers_notexisting'] = transfers_notexisting
if opes_notexisting:
data['errors']['opes_notexisting'] = opes_notexisting
return JsonResponse(data, status=400)
opes_already_canceled = [] # Déjà annulée
opes = [] # Pas déjà annulée
already_canceled = defaultdict(list)
opes = [] # Pas déjà annulée
transfers = []
required_perms = set()
stop_all = False
stop_all = False
cancel_duration = kfet_config.cancel_duration
to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes
to_groups_amounts = defaultdict(lambda:0) # ------ sur les montants des groupes d'opé
to_checkouts_balances = defaultdict(lambda:0) # ------ sur les balances de caisses
to_articles_stocks = defaultdict(lambda:0) # ------ sur les stocks d'articles
# Modifs à faire sur les balances des comptes
to_accounts_balances = defaultdict(lambda: 0)
# ------ sur les montants des groupes d'opé
to_groups_amounts = defaultdict(lambda: 0)
# ------ sur les balances de caisses
to_checkouts_balances = defaultdict(lambda: 0)
# ------ sur les stocks d'articles
to_articles_stocks = defaultdict(lambda: 0)
for ope in opes_all:
if ope.canceled_at:
# Opération déjà annulée, va pour un warning en Response
opes_already_canceled.append(ope.pk)
already_canceled['opes'].append(ope.pk)
else:
opes.append(ope.pk)
# Si opé il y a plus de CANCEL_DURATION, permission requise
@ -1187,10 +1254,11 @@ def kpsul_cancel_operations(request):
# par `.save()`, amount_error est recalculé automatiquement,
# ce qui n'est pas le cas en faisant un update sur queryset
# TODO ? : Maj les balance_old de relevés pour modifier l'erreur
last_statement = (CheckoutStatement.objects
.filter(checkout=ope.group.checkout)
.order_by('at')
.last())
last_statement = \
(CheckoutStatement.objects
.filter(checkout=ope.group.checkout)
.order_by('at')
.last())
if not last_statement or last_statement.at < ope.group.at:
if ope.is_checkout:
if ope.group.on_acc.is_cash:
@ -1206,23 +1274,41 @@ def kpsul_cancel_operations(request):
# Note : si InventoryArticle est maj par .save(), stock_error
# est recalculé automatiquement
if ope.article and ope.article_nb:
last_stock = (InventoryArticle.objects
last_stock = (
InventoryArticle.objects
.select_related('inventory')
.filter(article=ope.article)
.order_by('inventory__at')
.last())
.last()
)
if not last_stock or last_stock.inventory.at < ope.group.at:
to_articles_stocks[ope.article] += ope.article_nb
if not opes:
data['warnings']['already_canceled'] = opes_already_canceled
for transfer in transfers_all:
if transfer.canceled_at:
# Transfert déjà annulé, va pour un warning en Response
already_canceled['transfers'].append(transfer.pk)
else:
transfers.append(transfer.pk)
# Si transfer il y a plus de CANCEL_DURATION, permission requise
if transfer.group.at + cancel_duration < timezone.now():
required_perms.add('kfet.cancel_old_operations')
# Calcul de toutes modifs à faire en cas de validation
# Pour les balances de comptes
to_accounts_balances[transfer.from_acc] += transfer.amount
to_accounts_balances[transfer.to_acc] += -transfer.amount
if not opes and not transfers:
data['warnings']['already_canceled'] = already_canceled
return JsonResponse(data)
negative_accounts = []
# Checking permissions or stop
for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation(
amount = to_accounts_balances[account])
amount=to_accounts_balances[account])
required_perms |= perms
stop_all = stop_all or stop
if stop:
@ -1242,6 +1328,10 @@ def kpsul_cancel_operations(request):
with transaction.atomic():
(Operation.objects.filter(pk__in=opes)
.update(canceled_by=canceled_by, canceled_at=canceled_at))
(Transfer.objects.filter(pk__in=transfers)
.update(canceled_by=canceled_by, canceled_at=canceled_at))
for account in to_accounts_balances:
(
Account.objects
@ -1254,20 +1344,22 @@ def kpsul_cancel_operations(request):
account.update_negative()
for checkout in to_checkouts_balances:
Checkout.objects.filter(pk=checkout.pk).update(
balance = F('balance') + to_checkouts_balances[checkout])
balance=F('balance') + to_checkouts_balances[checkout])
for group in to_groups_amounts:
OperationGroup.objects.filter(pk=group.pk).update(
amount = F('amount') + to_groups_amounts[group])
amount=F('amount') + to_groups_amounts[group])
for article in to_articles_stocks:
Article.objects.filter(pk=article.pk).update(
stock = F('stock') + to_articles_stocks[article])
stock=F('stock') + to_articles_stocks[article])
# Websocket data
websocket_data = { 'opegroups': [], 'opes': [], 'checkouts': [], 'articles': [] }
websocket_data = {'opegroups': [], 'opes': [],
'checkouts': [], 'articles': []}
# Need refresh from db cause we used update on querysets
opegroups_pk = [ opegroup.pk for opegroup in to_groups_amounts ]
opegroups_pk = [opegroup.pk for opegroup in to_groups_amounts]
opegroups = (OperationGroup.objects
.values('id','amount','is_cof').filter(pk__in=opegroups_pk))
.values('id', 'amount', 'is_cof')
.filter(pk__in=opegroups_pk))
for opegroup in opegroups:
websocket_data['opegroups'].append({
'cancellation': True,
@ -1275,24 +1367,35 @@ def kpsul_cancel_operations(request):
'amount': opegroup['amount'],
'is_cof': opegroup['is_cof'],
})
canceled_by__trigramme = canceled_by and canceled_by.trigramme or None
canceled_by = canceled_by and canceled_by.trigramme or None
for ope in opes:
websocket_data['opes'].append({
'cancellation': True,
'modelname': 'ope',
'id': ope,
'canceled_by__trigramme': canceled_by__trigramme,
'canceled_by': canceled_by,
'canceled_at': canceled_at,
})
for ope in transfers:
websocket_data['opes'].append({
'cancellation': True,
'modelname': 'transfer',
'id': ope,
'canceled_by': canceled_by,
'canceled_at': canceled_at,
})
# Need refresh from db cause we used update on querysets
checkouts_pk = [ checkout.pk for checkout in to_checkouts_balances]
checkouts_pk = [checkout.pk for checkout in to_checkouts_balances]
checkouts = (Checkout.objects
.values('id', 'balance').filter(pk__in=checkouts_pk))
.values('id', 'balance')
.filter(pk__in=checkouts_pk))
for checkout in checkouts:
websocket_data['checkouts'].append({
'id': checkout['id'],
'balance': checkout['balance']})
# Need refresh from db cause we used update on querysets
articles_pk = [ article.pk for articles in to_articles_stocks]
articles_pk = [article.pk for articles in to_articles_stocks]
articles = Article.objects.values('id', 'stock').filter(pk__in=articles_pk)
for article in articles:
websocket_data['articles'].append({
@ -1300,92 +1403,194 @@ def kpsul_cancel_operations(request):
'stock': article['stock']})
consumers.KPsul.group_send('kfet.kpsul', websocket_data)
data['canceled'] = opes
if opes_already_canceled:
data['warnings']['already_canceled'] = opes_already_canceled
data['canceled']['opes'] = opes
data['canceled']['transfers'] = transfers
if already_canceled:
data['warnings']['already_canceled'] = already_canceled
return JsonResponse(data)
@login_required
def history_json(request):
# Récupération des paramètres
from_date = request.POST.get('from', None)
to_date = request.POST.get('to', None)
limit = request.POST.get('limit', None);
checkouts = request.POST.getlist('checkouts[]', None)
accounts = request.POST.getlist('accounts[]', None)
from_date = request.GET.get('from', None)
to_date = request.GET.get('to', None)
checkouts = request.GET.getlist('checkouts[]', None)
accounts = request.GET.getlist('accounts[]', None)
transfers_only = request.GET.get('transfersonly', None)
opes_only = request.GET.get('opesonly', None)
# Un non-membre de l'équipe n'a que accès à son historique
if not request.user.has_perm('kfet.is_team'):
accounts = [request.user.profile.account_kfet]
# Construction de la requête (sur les opérations) pour le prefetch
queryset_prefetch = Operation.objects.select_related(
'article', 'canceled_by', 'addcost_for')
ope_queryset_prefetch = Operation.objects.select_related(
'canceled_by', 'addcost_for', 'article')
ope_prefetch = Prefetch('opes',
queryset=ope_queryset_prefetch)
transfer_queryset_prefetch = Transfer.objects.select_related(
'from_acc', 'to_acc', 'canceled_by')
if accounts:
transfer_queryset_prefetch = transfer_queryset_prefetch.filter(
Q(from_acc__in=accounts) |
Q(to_acc__in=accounts))
if not request.user.has_perm('kfet.is_team'):
acc = request.user.profile.account_kfet
transfer_queryset_prefetch = transfer_queryset_prefetch.filter(
Q(from_acc=acc) | Q(to_acc=acc))
transfer_prefetch = Prefetch('transfers',
queryset=transfer_queryset_prefetch,
to_attr='filtered_transfers')
# Construction de la requête principale
opegroups = (
OperationGroup.objects
.prefetch_related(Prefetch('opes', queryset=queryset_prefetch))
.select_related('on_acc', 'valid_by')
.order_by('at')
.prefetch_related(ope_prefetch)
.select_related('on_acc',
'valid_by')
.order_by('at')
)
transfergroups = (
TransferGroup.objects
.prefetch_related(transfer_prefetch)
.select_related('valid_by')
.order_by('at')
)
# Application des filtres
if from_date:
opegroups = opegroups.filter(at__gte=from_date)
transfergroups = transfergroups.filter(at__gte=from_date)
if to_date:
opegroups = opegroups.filter(at__lt=to_date)
transfergroups = transfergroups.filter(at__lt=to_date)
if checkouts:
opegroups = opegroups.filter(checkout_id__in=checkouts)
transfergroups = TransferGroup.objects.none()
if transfers_only:
opegroups = OperationGroup.objects.none()
if opes_only:
transfergroups = TransferGroup.objects.none()
if accounts:
opegroups = opegroups.filter(on_acc_id__in=accounts)
# Un non-membre de l'équipe n'a que accès à son historique
if not request.user.has_perm('kfet.is_team'):
opegroups = opegroups.filter(on_acc=request.user.profile.account_kfet)
if limit:
opegroups = opegroups[:limit]
opegroups = opegroups.filter(on_acc__in=accounts)
# Construction de la réponse
opegroups_list = []
related_data = defaultdict(list)
objects_data = defaultdict(list)
for opegroup in opegroups:
opegroup_dict = {
'id' : opegroup.id,
'amount' : opegroup.amount,
'at' : opegroup.at,
'checkout_id': opegroup.checkout_id,
'is_cof' : opegroup.is_cof,
'comment' : opegroup.comment,
'opes' : [],
'on_acc__trigramme':
opegroup.on_acc and opegroup.on_acc.trigramme or None,
'id': opegroup.id,
'amount': opegroup.amount,
'at': opegroup.at,
'is_cof': opegroup.is_cof,
'comment': opegroup.comment,
'trigramme':
opegroup.on_acc and opegroup.on_acc.trigramme or None,
}
if request.user.has_perm('kfet.is_team'):
opegroup_dict['valid_by__trigramme'] = (
opegroup_dict['valid_by'] = (
opegroup.valid_by and opegroup.valid_by.trigramme or None)
for ope in opegroup.opes.all():
ope_dict = {
'id' : ope.id,
'type' : ope.type,
'amount' : ope.amount,
'article_nb' : ope.article_nb,
'addcost_amount': ope.addcost_amount,
'canceled_at' : ope.canceled_at,
'article__name':
ope.article and ope.article.name or None,
'addcost_for__trigramme':
ope.addcost_for and ope.addcost_for.trigramme or None,
'id': ope.id,
'amount': ope.amount,
'canceled_at': ope.canceled_at,
'trigramme':
opegroup.on_acc and opegroup.on_acc.trigramme or None,
'opegroup__id': opegroup.id,
}
if request.user.has_perm('kfet.is_team'):
ope_dict['canceled_by__trigramme'] = (
ope_dict['canceled_by'] = (
ope.canceled_by and ope.canceled_by.trigramme or None)
opegroup_dict['opes'].append(ope_dict)
opegroups_list.append(opegroup_dict)
return JsonResponse({ 'opegroups': opegroups_list })
if ope.type == Operation.PURCHASE:
ope_dict.update({
'article_name': ope.article.name,
'article_nb': ope.article_nb,
'addcost_amount': ope.addcost_amount,
'addcost_for':
ope.addcost_for and ope.addcost_for.trigramme or None,
})
objects_data['purchase'].append(ope_dict)
else:
ope_dict.update({
'type': ope.type,
})
objects_data['specialope'].append(ope_dict)
related_data['opegroup'].append(opegroup_dict)
for transfergroup in transfergroups:
if transfergroup.filtered_transfers:
transfergroup_dict = {
'id': transfergroup.id,
'at': transfergroup.at,
'comment': transfergroup.comment,
}
if request.user.has_perm('kfet.is_team'):
transfergroup_dict['valid_by'] = (
transfergroup.valid_by and
transfergroup.valid_by.trigramme or
None)
for transfer in transfergroup.filtered_transfers:
transfer_dict = {
'id': transfer.id,
'amount': transfer.amount,
'canceled_at': transfer.canceled_at,
'from_acc': transfer.from_acc.trigramme,
'to_acc': transfer.to_acc.trigramme,
'transfergroup__id': transfergroup.id,
}
if request.user.has_perm('kfet.is_team'):
transfer_dict['canceled_by'] = (
transfer.canceled_by and
transfer.canceled_by.trigramme or
None)
objects_data['transfer'].append(transfer_dict)
related_data['transfergroup'].append(transfergroup_dict)
data = {
'objects': objects_data,
'related': related_data,
}
return JsonResponse(data)
@teamkfet_required
def kpsul_articles_data(request):
articles = (
Article.objects
.values('id', 'name', 'price', 'stock', 'category_id',
'category__name', 'category__has_addcost')
.filter(is_sold=True))
return JsonResponse({ 'articles': list(articles) })
data = {'objects': {}, 'related': {}}
data['objects']['article'] = [
{
'id': article.id,
'name': article.name,
'price': article.price,
'stock': article.stock,
'category__id': article.category_id,
}
for article in Article.objects.filter(is_sold=True)
]
data['related']['category'] = [
{
'id': category.id,
'name': category.name,
'has_addcost': category.has_addcost,
}
for category in ArticleCategory.objects.all()
]
return JsonResponse(data)
@teamkfet_required
@ -1432,25 +1637,10 @@ config_update = (
# Transfer views
# -----
@teamkfet_required
def transfers(request):
transfers_pre = Prefetch(
'transfers',
queryset=(
Transfer.objects
.select_related('from_acc', 'to_acc')
),
)
transfergroups = (
TransferGroup.objects
.select_related('valid_by')
.prefetch_related(transfers_pre)
.order_by('-at')
)
return render(request, 'kfet/transfers.html', {
'transfergroups': transfergroups,
})
return render(request, 'kfet/transfers.html')
@teamkfet_required
@ -1459,20 +1649,24 @@ def transfers_create(request):
return render(request, 'kfet/transfers_create.html',
{ 'transfer_formset': transfer_formset })
@teamkfet_required
def perform_transfers(request):
data = { 'errors': {}, 'transfers': [], 'transfergroup': 0 }
data = {'errors': {}, 'transfers': [], 'transfergroup': 0}
# Checking transfer_formset
transfer_formset = TransferFormSet(request.POST)
if not transfer_formset.is_valid():
return JsonResponse({ 'errors': list(transfer_formset.errors)}, status=400)
return JsonResponse({'errors': list(transfer_formset.errors)},
status=400)
transfers = transfer_formset.save(commit = False)
transfers = transfer_formset.save(commit=False)
# Initializing vars
required_perms = set(['kfet.add_transfer']) # Required perms to perform all transfers
to_accounts_balances = defaultdict(lambda:0) # For balances of accounts
# Required perms to perform all transfers
required_perms = set(['kfet.add_transfer'])
# For balances of accounts
to_accounts_balances = defaultdict(lambda: 0)
for transfer in transfers:
to_accounts_balances[transfer.from_acc] -= transfer.amount
@ -1484,7 +1678,7 @@ def perform_transfers(request):
# Checking if ok on all accounts
for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation(
amount = to_accounts_balances[account])
amount=to_accounts_balances[account])
required_perms |= perms
stop_all = stop_all or stop
if stop:
@ -1510,7 +1704,7 @@ def perform_transfers(request):
# Updating balances accounts
for account in to_accounts_balances:
Account.objects.filter(pk=account.pk).update(
balance = F('balance') + to_accounts_balances[account])
balance=F('balance') + to_accounts_balances[account])
account.refresh_from_db()
if account.balance < 0:
if hasattr(account, 'negative'):
@ -1519,10 +1713,10 @@ def perform_transfers(request):
account.negative.save()
else:
negative = AccountNegative(
account = account, start = timezone.now())
account=account, start=timezone.now())
negative.save()
elif (hasattr(account, 'negative')
and not account.negative.balance_offset):
elif (hasattr(account, 'negative') and
not account.negative.balance_offset):
account.negative.delete()
# Saving transfer group
@ -1535,103 +1729,38 @@ def perform_transfers(request):
transfer.save()
data['transfers'].append(transfer.pk)
# Websocket data
websocket_data = {}
websocket_data['opegroups'] = [{
'add': True,
'modelname': 'transfergroup',
'content': {
'id': transfergroup.pk,
'at': transfergroup.at,
'comment': transfergroup.comment,
'valid_by__trigramme': (transfergroup.valid_by and
transfergroup.valid_by.trigramme or None),
'children': []
},
}]
for transfer in transfers:
ope_data = {
'modelname': 'transfer',
'content': {
'id': transfer.pk,
'amount': transfer.amount,
'from_acc': transfer.from_acc.trigramme,
'to_acc': transfer.to_acc.trigramme,
'canceled_by__trigramme': None, 'canceled_at': None,
'from_acc_id': transfer.from_acc.id,
'to_acc_id': transfer.to_acc.id,
},
}
websocket_data['opegroups'][0]['content']['children'].append(ope_data)
consumers.KPsul.group_send('kfet.kpsul', websocket_data)
return JsonResponse(data)
@teamkfet_required
def cancel_transfers(request):
# Pour la réponse
data = { 'canceled': [], 'warnings': {}, 'errors': {}}
# Checking if BAD REQUEST (transfers_pk not int or not existing)
try:
# Set pour virer les doublons
transfers_post = set(map(int, filter(None, request.POST.getlist('transfers[]', []))))
except ValueError:
return JsonResponse(data, status=400)
transfers_all = (
Transfer.objects
.select_related('group', 'from_acc', 'from_acc__negative',
'to_acc', 'to_acc__negative')
.filter(pk__in=transfers_post))
transfers_pk = [ transfer.pk for transfer in transfers_all ]
transfers_notexisting = [ transfer for transfer in transfers_post
if transfer not in transfers_pk ]
if transfers_notexisting:
data['errors']['transfers_notexisting'] = transfers_notexisting
return JsonResponse(data, status=400)
transfers_already_canceled = [] # Déjà annulée
transfers = [] # Pas déjà annulée
required_perms = set()
stop_all = False
cancel_duration = kfet_config.cancel_duration
to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes
for transfer in transfers_all:
if transfer.canceled_at:
# Transfert déjà annulé, va pour un warning en Response
transfers_already_canceled.append(transfer.pk)
else:
transfers.append(transfer.pk)
# Si transfer il y a plus de CANCEL_DURATION, permission requise
if transfer.group.at + cancel_duration < timezone.now():
required_perms.add('kfet.cancel_old_operations')
# Calcul de toutes modifs à faire en cas de validation
# Pour les balances de comptes
to_accounts_balances[transfer.from_acc] += transfer.amount
to_accounts_balances[transfer.to_acc] += -transfer.amount
if not transfers:
data['warnings']['already_canceled'] = transfers_already_canceled
return JsonResponse(data)
negative_accounts = []
# Checking permissions or stop
for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation(
amount = to_accounts_balances[account])
required_perms |= perms
stop_all = stop_all or stop
if stop:
negative_accounts.append(account.trigramme)
if stop_all or not request.user.has_perms(required_perms):
missing_perms = get_missing_perms(required_perms, request.user)
if missing_perms:
data['errors']['missing_perms'] = missing_perms
if stop_all:
data['errors']['negative'] = negative_accounts
return JsonResponse(data, status=403)
canceled_by = required_perms and request.user.profile.account_kfet or None
canceled_at = timezone.now()
with transaction.atomic():
(Transfer.objects.filter(pk__in=transfers)
.update(canceled_by=canceled_by, canceled_at=canceled_at))
for account in to_accounts_balances:
Account.objects.filter(pk=account.pk).update(
balance = F('balance') + to_accounts_balances[account])
account.refresh_from_db()
if account.balance < 0:
if hasattr(account, 'negative'):
if not account.negative.start:
account.negative.start = timezone.now()
account.negative.save()
else:
negative = AccountNegative(
account = account, start = timezone.now())
negative.save()
elif (hasattr(account, 'negative')
and not account.negative.balance_offset):
account.negative.delete()
data['canceled'] = transfers
if transfers_already_canceled:
data['warnings']['already_canceled'] = transfers_already_canceled
return JsonResponse(data)
class InventoryList(ListView):
queryset = (Inventory.objects
@ -2022,29 +2151,6 @@ class SupplierUpdate(SuccessMessageMixin, UpdateView):
# ---------------
# Vues génériques
# ---------------
# source : docs.djangoproject.com/fr/1.10/topics/class-based-views/mixins/
class JSONResponseMixin(object):
"""
A mixin that can be used to render a JSON response.
"""
def render_to_json_response(self, context, **response_kwargs):
"""
Returns a JSON response, transforming 'context' to make the payload.
"""
return JsonResponse(
self.get_data(context),
**response_kwargs
)
def get_data(self, context):
"""
Returns an object that will be serialized as JSON by json.dumps().
"""
# Note: This is *EXTREMELY* naive; in reality, you'll need
# to do much more complex handling to ensure that arbitrary
# objects -- such as Django model instances or querysets
# -- can be serialized as JSON.
return context
class JSONDetailView(JSONResponseMixin, BaseDetailView):

View file

@ -20,6 +20,7 @@ future==0.15.2
django-widget-tweaks==1.4.1
git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail
ldap3
django-js-reverse==0.7.3
channels==1.1.5
python-dateutil
wagtail==1.10.*