diff --git a/.gitignore b/.gitignore index db495959f..2e46c2324 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,7 @@ # XLSX exports /exports/* -# Archives of cLosed accounting periods +# Archives of cLosed accounting periods /accounting/* .DS_Store diff --git a/.rubocop.yml b/.rubocop.yml index c4e6a5ab5..8b1d1390e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,5 @@ Metrics/LineLength: - Max: 130 + Max: 140 Metrics/MethodLength: Max: 30 Metrics/CyclomaticComplexity: diff --git a/3rd-PARTY-LICENSES.md b/3rd-PARTY-LICENSES.md new file mode 100644 index 000000000..e112be4c1 --- /dev/null +++ b/3rd-PARTY-LICENSES.md @@ -0,0 +1,34 @@ + +Fab-Manager uses some external components, which are licenced under the +terms of the following licences: + +- [Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0): + - [jasny-bootstrap](https://github.com/jasny/bootstrap/) + - [elasticsearch](https://github.com/elasticsearch/bower-elasticsearch-js) + - [nvd3](https://github.com/novus/nvd3) + - [angular-bootstrap-switch](https://github.com/frapontillo/angular-bootstrap-switch) + - [elasticsearch-rails](https://github.com/elastic/elasticsearch-rails) + - [elasticsearch-model](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-model) + - [elasticsearch-persistence](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-persistence) + - font [Open Sans](http://www.fontsquirrel.com/fonts/open-sans) + +- [General Public License version 2](http://www.gnu.org/licenses/old-licenses/gpl-2.0-faq.en.html): + - [railroady](https://github.com/preston/railroady) + - [unicorn](https://github.com/defunkt/unicorn) + - [prawn](https://github.com/prawnpdf/prawn) + - [prawn-table](https://github.com/prawnpdf/prawn-table) + +- [BSD-2-Clause](https://opensource.org/licenses/BSD-2-Clause) + - [ruby](https://www.ruby-lang.org) + - [rubyzip](https://github.com/rubyzip/rubyzip) + - [byebug](https://github.com/deivid-rodriguez/byebug) + +- [MIT Licence](https://opensource.org/licenses/MIT) + - Errors and omissions excepted, all the other external libraries used + in this project. + +Please refer to the libraries documentation for more information about +their licences. + +Complete lists of used libraries are available in `package.json` for the +JS/EcmaScript libraries and in `Gemfile` for Ruby libraries. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b89ecc0d..02fff6cab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog Fab Manager +## v3.0.0 2019 March 28 + +- (France) Compliance with Article 88 of Law No. 2015-1785 and BOI-TVA-DECLA-30-10-30-20160803 : Certification of cash systems +- Ability for an admin to view and close accounting periods +- Secured archives for close accounting periods +- Securely chained invoices records with visual control of data integrity +- Notify an user if the available disk space reaches a configured threshold +- Invoices generated outside of production environment will be watermarked +- Keep track of currently logged user on each generated invoice +- Fix a bug: unable to add a file attachment to an event +- Fix a security issue: updated to devise 4.6.0 to fix [CVE-2019-5421](https://github.com/plataformatec/devise/issues/4981) +- Fix a security issue: updated Rails to 4.2.11.1 to fix [CVE-2019-5418](https://groups.google.com/forum/#!topic/rubyonrails-security/pFRKI96Sm8Q) and [CVE-2019-5419](https://groups.google.com/forum/#!topic/rubyonrails-security/GN7w9fFAQeI) +- Removed deprecated Capistrano deployment system +- Rebranded product from "La Casemate" +- Refactored some pieces of Ruby code, according to style guide +- Added asterisks on required fields in sign-up form +- [TODO DEPLOY] (dev) if applicable, you must first downgrade bundler to v1 `gem uninstall bundler --version=2.0.1 && gem install bundler --version=1.7.3 && bundle install` +- [TODO DEPLOY] if you have changed your VAT rate in the past, add its history into database. You can use a rate of "0" to disable VAT. Eg. `rake fablab:setup:add_vat_rate[20,2017-01-01]` +- [TODO DEPLOY] `rake fablab:setup:set_environment_to_invoices` +- [TODO DEPLOY] `rake fablab:setup:chain_invoices_items_records` +- [TODO DEPLOY] `rake fablab:setup:chain_invoices_records` +- [TODO DEPLOY] `rake fablab:setup:chain_history_values_records` +- [TODO DEPLOY] add `DISK_SPACE_MB_ALERT` and `SUPERADMIN_EMAIL` environment variables (see [doc/environment.md](doc/environment.md) for configuration details) + ## v2.8.4 2019 March 18 - Limit members search to 50 results to speed up queries @@ -515,7 +539,7 @@ - Fix a bug: user is not redirected after changing is duplicated e-mail on the SSO provider ## v2.1.0 2016 May 2 -- Add search feature on openlab projects : [Openlab-projects](https://github.com/LaCasemate/openlab-projects) +- Add search feature on openlab projects : [Openlab-projects](https://github.com/sleede/openlab-projects) - Add integration tests for main features - Credits logic has been extracted into a microservice - Improved UI list of projects diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b41e3d93..d7ee792fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ patches and features. ## Using the issue tracker -The [issue tracker](https://github.com/LaCasemate/fab-manager/issues) is the preferred channel for [bug reports](#bugs) +The [issue tracker](https://github.com/sleede/fab-manager/issues) is the preferred channel for [bug reports](#bugs) and [submitting pull requests](#pull-requests), but please respect the following restrictions: * Please **do not** use the issue tracker for personal support requests (use [the forum](https://forum.fab-manager.com)). @@ -96,7 +96,7 @@ Adhering to the following process is the best way to get your work included in t # Navigate to the newly cloned directory cd fab-manager # Assign the original repo to a remote called "upstream" - git remote add upstream https://github.com/LaCasemate/fab-manager.git + git remote add upstream https://github.com/sleede/fab-manager.git ``` 2. If you cloned a while ago, get the latest changes from upstream: @@ -131,4 +131,4 @@ Adhering to the following process is the best way to get your work included in t 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title and description. **IMPORTANT**: By submitting a patch, you agree to allow the project owners to license your work under the terms of -the [GNU Affero General Public License](LICENSE.md). \ No newline at end of file +the [GNU Affero General Public License](LICENSE.md). diff --git a/Dockerfile b/Dockerfile index cf2d7db52..f73335484 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ MAINTAINER peng@sleede.com # First we need to be able to fetch from https repositories RUN apt-get update && \ apt-get install -y apt-transport-https \ - ca-certificates + ca-certificates apt-utils # Add sources for external tools to APT @@ -44,6 +44,7 @@ RUN mkdir -p /usr/src/app/exports RUN mkdir -p /usr/src/app/log RUN mkdir -p /usr/src/app/public/uploads RUN mkdir -p /usr/src/app/public/assets +RUN mkdir -p /usr/src/app/accounting RUN mkdir -p /usr/src/app/tmp/sockets RUN mkdir -p /usr/src/app/tmp/pids @@ -64,6 +65,7 @@ VOLUME /usr/src/app/exports VOLUME /usr/src/app/public VOLUME /usr/src/app/public/uploads VOLUME /usr/src/app/public/assets +VOLUME /usr/src/app/accounting VOLUME /var/log/supervisor # Expose port 3000 to the Docker host, so we can access it diff --git a/Gemfile b/Gemfile index 964fbddea..566819a95 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' gem 'compass-rails', '2.0.4' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '4.2.11' +gem 'rails', '4.2.11.1' # Use SCSS for stylesheets gem 'sass-rails', '5.0.1' @@ -17,7 +17,7 @@ gem 'jquery-rails' gem 'jbuilder', '~> 2.5' gem 'jbuilder_cache_multi' # bundle exec rake doc:rails generates the API under doc/api. -gem 'sdoc', '~> 0.4.0', group: :doc #TODO remove unused ? +gem 'sdoc', '~> 0.4.0', group: :doc # TODO, remove unused ? gem 'forgery' gem 'responders', '~> 2.0' @@ -41,16 +41,12 @@ end group :development do gem 'active_record_query_trace' gem 'awesome_print' - gem 'capistrano' - gem 'capistrano-maintenance', '0.0.5', require: false - gem 'capistrano-sidekiq', require: false gem 'coveralls', require: false gem 'foreman' # Preview mail in the browser gem 'mailcatcher' gem 'puma' gem 'rb-readline' - gem 'rvm-capistrano', require: false end group :test do @@ -66,15 +62,13 @@ end group :production do gem 'rails_12factor' - gem 'unicorn' end gem 'seed_dump' gem 'pg' -gem 'devise' -gem 'devise-async' +gem 'devise', ">= 4.6.0" gem 'omniauth', '~> 1.6.0' gem 'omniauth-oauth2' @@ -148,3 +142,8 @@ gem 'axlsx_rails' gem 'rubyzip', '>= 1.2.2' gem 'rack-protection', '1.5.5' + +# get free disk space +gem 'sys-filesystem' + +gem 'sha3' diff --git a/Gemfile.lock b/Gemfile.lock index 22fe64557..0cb5fe659 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,39 +14,39 @@ GEM specs: Ascii85 (1.0.2) aasm (4.1.0) - actionmailer (4.2.11) - actionpack (= 4.2.11) - actionview (= 4.2.11) - activejob (= 4.2.11) + actionmailer (4.2.11.1) + actionpack (= 4.2.11.1) + actionview (= 4.2.11.1) + activejob (= 4.2.11.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.11) - actionview (= 4.2.11) - activesupport (= 4.2.11) + actionpack (4.2.11.1) + actionview (= 4.2.11.1) + activesupport (= 4.2.11.1) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) actionpack-page_caching (1.0.2) actionpack (>= 4.0.0, < 5) - actionview (4.2.11) - activesupport (= 4.2.11) + actionview (4.2.11.1) + activesupport (= 4.2.11.1) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.3) active_record_query_trace (1.4) - activejob (4.2.11) - activesupport (= 4.2.11) + activejob (4.2.11.1) + activesupport (= 4.2.11.1) globalid (>= 0.3.0) - activemodel (4.2.11) - activesupport (= 4.2.11) + activemodel (4.2.11.1) + activesupport (= 4.2.11.1) builder (~> 3.1) - activerecord (4.2.11) - activemodel (= 4.2.11) - activesupport (= 4.2.11) + activerecord (4.2.11.1) + activemodel (= 4.2.11.1) + activesupport (= 4.2.11.1) arel (~> 6.0) - activesupport (4.2.11) + activesupport (4.2.11.1) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) @@ -70,7 +70,7 @@ GEM axlsx_rails (0.4.0) axlsx (>= 2.0.1) rails (>= 3.1) - bcrypt (3.1.10) + bcrypt (3.1.12) binding_of_caller (0.7.3) debug_inspector (>= 0.0.1) bootstrap-sass (3.4.1) @@ -80,17 +80,6 @@ GEM builder (3.2.3) byebug (8.2.3) camertron-eprun (1.1.0) - capistrano (2.15.5) - highline - net-scp (>= 1.0.0) - net-sftp (>= 2.0.0) - net-ssh (>= 2.0.14) - net-ssh-gateway (>= 1.1.0) - capistrano-maintenance (0.0.5) - capistrano (~> 2.0) - capistrano-sidekiq (0.5.2) - capistrano - sidekiq carrierwave (0.10.0) activemodel (>= 3.2.0) activesupport (>= 3.2.0) @@ -119,7 +108,7 @@ GEM compass (~> 1.0.0) sass-rails (<= 5.0.1) sprockets (< 2.13) - concurrent-ruby (1.1.4) + concurrent-ruby (1.1.5) connection_pool (2.2.0) coveralls (0.8.16) json (>= 1.8, < 3) @@ -135,15 +124,12 @@ GEM debug_inspector (0.0.3) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - devise (3.4.1) + devise (4.6.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 3.2.6, < 5) + railties (>= 4.1.0, < 6.0) responders - thread_safe (~> 0.1) warden (~> 1.2.3) - devise-async (0.9.0) - devise (~> 3.2) docile (1.1.5) domain_name (0.5.25) unf (>= 0.0.5, < 1.0.0) @@ -185,14 +171,13 @@ GEM forgery (0.6.0) friendly_id (5.1.0) activerecord (>= 4.0.0) - globalid (0.4.1) + globalid (0.4.2) activesupport (>= 4.2.0) has_secure_token (1.0.0) activerecord (>= 3.0) hashdiff (0.3.0) hashery (2.1.2) hashie (3.5.7) - highline (1.7.1) hike (1.2.3) hitimes (1.2.2) htmlentities (4.3.4) @@ -226,7 +211,6 @@ GEM kaminari (0.16.3) actionpack (>= 3.0.0) activesupport (>= 3.0.0) - kgio (2.9.3) libv8 (3.16.14.11) loofah (2.2.3) crass (~> 1.0.2) @@ -249,7 +233,7 @@ GEM mimemagic (0.3.2) mini_magick (4.2.0) mini_mime (1.0.1) - mini_portile2 (2.3.0) + mini_portile2 (2.4.0) minitest (5.11.3) minitest-reporters (1.1.8) ansi @@ -260,16 +244,9 @@ GEM multi_xml (0.5.5) multipart-post (2.0.0) naught (1.1.0) - net-scp (1.2.1) - net-ssh (>= 2.6.5) - net-sftp (2.1.2) - net-ssh (>= 2.6.5) - net-ssh (2.9.2) - net-ssh-gateway (1.2.0) - net-ssh (>= 2.6.5) netrc (0.10.3) - nokogiri (1.8.5) - mini_portile2 (~> 2.3.0) + nokogiri (1.10.1) + mini_portile2 (~> 2.4.0) notify_with (0.0.2) jbuilder (~> 2.0) rails (>= 4.2.0) @@ -318,16 +295,16 @@ GEM rack-test (0.6.3) rack (>= 1.0) railroady (1.5.3) - rails (4.2.11) - actionmailer (= 4.2.11) - actionpack (= 4.2.11) - actionview (= 4.2.11) - activejob (= 4.2.11) - activemodel (= 4.2.11) - activerecord (= 4.2.11) - activesupport (= 4.2.11) + rails (4.2.11.1) + actionmailer (= 4.2.11.1) + actionpack (= 4.2.11.1) + actionview (= 4.2.11.1) + activejob (= 4.2.11.1) + activemodel (= 4.2.11.1) + activerecord (= 4.2.11.1) + activesupport (= 4.2.11.1) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.11) + railties (= 4.2.11.1) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -344,13 +321,12 @@ GEM rails_stdout_logging rails_serve_static_assets (0.0.4) rails_stdout_logging (0.0.3) - railties (4.2.11) - actionpack (= 4.2.11) - activesupport (= 4.2.11) + railties (4.2.11.1) + actionpack (= 4.2.11.1) + activesupport (= 4.2.11.1) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (3.0.0) - raindrops (0.13.0) rake (12.3.2) rb-fsevent (0.9.4) rb-inotify (0.9.5) @@ -384,8 +360,6 @@ GEM rubyzip (1.2.2) rufus-scheduler (3.0.9) tzinfo - rvm-capistrano (1.5.6) - capistrano (~> 2.15.4) safe_yaml (1.0.4) sass (3.4.13) sass-rails (5.0.1) @@ -403,6 +377,7 @@ GEM seed_dump (3.2.2) activerecord (~> 4) activesupport (~> 4) + sha3 (1.0.1) sidekiq (3.3.4) celluloid (>= 0.16.0) connection_pool (>= 2.1.1) @@ -440,6 +415,8 @@ GEM stripe (1.30.2) json (~> 1.8.1) rest-client (~> 1.4) + sys-filesystem (1.2.0) + ffi term-ansicolor (1.3.2) tins (~> 1.0) test_after_commit (1.0.0) @@ -484,17 +461,13 @@ GEM unf_ext unf_ext (0.0.6) unicode-display_width (1.4.0) - unicorn (4.8.3) - kgio (~> 2.6) - rack - raindrops (~> 0.7) vcr (3.0.1) virtus (1.0.5) axiom-types (~> 0.1) coercible (~> 1.0) descendants_tracker (~> 0.0, >= 0.0.3) equalizer (~> 0.0, >= 0.0.9) - warden (1.2.3) + warden (1.2.7) rack (>= 1.0) web-console (2.1.3) activemodel (>= 4.0) @@ -520,16 +493,12 @@ DEPENDENCIES axlsx_rails bootstrap-sass (>= 3.4.1) byebug - capistrano - capistrano-maintenance (= 0.0.5) - capistrano-sidekiq carrierwave chroma compass-rails (= 2.0.4) coveralls database_cleaner - devise - devise-async + devise (>= 4.6.0) elasticsearch-model (~> 5) elasticsearch-persistence (~> 5) elasticsearch-rails (~> 5) @@ -562,7 +531,7 @@ DEPENDENCIES pundit rack-protection (= 1.5.5) railroady - rails (= 4.2.11) + rails (= 4.2.11.1) rails-observers rails_12factor rb-readline @@ -571,24 +540,24 @@ DEPENDENCIES rolify rubocop (~> 0.61.1) rubyzip (>= 1.2.2) - rvm-capistrano sass-rails (= 5.0.1) sdoc (~> 0.4.0) seed_dump + sha3 sidekiq sidekiq-cron sinatra spring stripe (= 1.30.2) + sys-filesystem test_after_commit therubyracer (= 0.12.0) twitter twitter-text uglifier (>= 4.1.20) - unicorn vcr web-console (~> 2.1.3) webmock BUNDLED WITH - 1.17.2 + 1.17.3 diff --git a/LICENSE.md b/LICENSE.md index 3deb50991..e20c7f8ad 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (C) 2015 La Casemate +Copyright (C) 2019 Sleede This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -14,43 +14,6 @@ Copyright (C) 2015 La Casemate along with this program. If not, see . -Fab-Manager uses some external components, which are licenced under the -terms of the following licences: - -- [Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0): - - [jasny-bootstrap](https://github.com/jasny/bootstrap/) - - [elasticsearch](https://github.com/elasticsearch/bower-elasticsearch-js) - - [nvd3](https://github.com/novus/nvd3) - - [angular-bootstrap-switch](https://github.com/frapontillo/angular-bootstrap-switch) - - [elasticsearch-rails](https://github.com/elastic/elasticsearch-rails) - - [elasticsearch-model](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-model) - - [elasticsearch-persistence](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-persistence) - - font [Open Sans](http://www.fontsquirrel.com/fonts/open-sans) - -- [General Public License version 2](http://www.gnu.org/licenses/old-licenses/gpl-2.0-faq.en.html): - - [railroady](https://github.com/preston/railroady) - - [unicorn](https://github.com/defunkt/unicorn) - - [prawn](https://github.com/prawnpdf/prawn) - - [prawn-table](https://github.com/prawnpdf/prawn-table) - -- [BSD-2-Clause](https://opensource.org/licenses/BSD-2-Clause) - - [ruby](https://www.ruby-lang.org) - - [rubyzip](https://github.com/rubyzip/rubyzip) - - [byebug](https://github.com/deivid-rodriguez/byebug) - -- [MIT Licence](https://opensource.org/licenses/MIT) - - Errors and omissions excepted, all the other external libraries used - in this project. - -Please refer to the libraries documentation for more information about -their licences. - -Complete lists of used libraries are available in `bower.json` for the -JS/EcmaScript libraries and in `Gemfile` for Ruby libraries. - - - - GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 diff --git a/README.md b/README.md index b575911c0..18df6834c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # FabManager -FabManager is the FabLab management solution. It is web-based, open-source and totally free. +FabManager is the Fab Lab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects. -[![Coverage Status](https://coveralls.io/repos/github/LaCasemate/fab-manager/badge.svg)](https://coveralls.io/github/LaCasemate/fab-manager) +[![Coverage Status](https://coveralls.io/repos/github/sleede/fab-manager/badge.svg)](https://coveralls.io/github/sleede/fab-manager) [![Docker pulls](https://img.shields.io/docker/pulls/sleede/fab-manager.svg)](https://hub.docker.com/r/sleede/fab-manager/) [![Docker Build Status](https://img.shields.io/docker/build/sleede/fab-manager.svg)](https://hub.docker.com/r/sleede/fab-manager/builds) @@ -43,7 +43,6 @@ FabManager is a Ruby on Rails / AngularJS web application that runs on the follo - Ubuntu LTS 14.04+ / Debian 8+ - Ruby 2.3 -- Git 1.9.1+ - Redis 2.8.4+ - Sidekiq 3.3.4+ - Elasticsearch 5.6 @@ -102,7 +101,7 @@ This procedure is not easy to follow so if you don't need to write some code for 7. Retrieve the project from Git ```bash - git clone https://github.com/LaCasemate/fab-manager.git + git clone https://github.com/sleede/fab-manager.git ``` 8. Install the software dependencies. @@ -136,7 +135,7 @@ This procedure is not easy to follow so if you don't need to write some code for 10. Install bundler in the current RVM gemset ```bash - gem install bundler + gem install bundler --version=1.17.3 ``` 11. Install the required ruby gems and javascript plugins @@ -205,7 +204,7 @@ environment. 2. Retrieve the project from Git ```bash - git clone https://github.com/LaCasemate/fab-manager + git clone https://github.com/sleede/fab-manager ``` 3. From the project directory, run: @@ -339,6 +338,7 @@ This can be achieved doing the following: - `db/migrate/20150604131525_add_meta_data_to_notifications.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html), a PostgreSQL 9.4+ datatype. - `db/migrate/20160915105234_add_transformation_to_o_auth2_mapping.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html), a PostgreSQL 9.4+ datatype. - `db/migrate/20181217103441_migrate_settings_value_to_history_values.rb` is using `SELECT DISTINCT ON`. + - `db/migrate/20190107111749_protect_accounting_periods.rb` is using `CREATE RULE` and `DROP RULE`. - If you intend to contribute to the project code, you will need to run the test suite with `rake test`. This also requires your user to have the _SUPERUSER_ role. Please see the [known issues](#known-issues) section for more information about this. @@ -445,6 +445,10 @@ In each cases, some inline comments are included in the localisation files. They can be recognized as they start with the sharp character (#). These comments are not required to be translated, they are intended to help the translator to have some context information about the sentence to translate. +You will also need to translate the invoice watermark, located in `app/pdfs/data/`. +You'll find there the [GIMP source of the image](app/pdfs/data/watermark.xcf), which is using [Rubik Mono One](https://fonts.google.com/specimen/Rubik+Mono+One) as font. +Use it to generate a similar localised PNG image which keep the default image size, as PDF are not responsive. + ### Configuration @@ -467,7 +471,7 @@ After modifying any values concerning the localisation, restart the application **This configuration is optional.** -You can configure your fab-manager to synchronize every project with the [Open Projects platform](https://github.com/LaCasemate/openlab-projects). +You can configure your fab-manager to synchronize every project with the [Open Projects platform](https://github.com/sleede/openlab-projects). It's very simple and straightforward and in return, your users will be able to search over projects from all fab-manager instances from within your platform. The deal is fair, you share your projects and as reward you benefits from projects of the whole community. @@ -496,7 +500,7 @@ It enables you to write plugins which can: To install a plugin, you just have to copy the plugin folder which contains its code into the folder `plugins` of Fab-manager. -You can see an example on the [repo of navinum gamification plugin](https://github.com/LaCasemate/navinum-gamification) +You can see an example on the [repo of navinum gamification plugin](https://github.com/sleede/navinum-gamification) ## Single Sign-On diff --git a/app/assets/javascripts/controllers/admin/invoices.js.erb b/app/assets/javascripts/controllers/admin/invoices.js.erb index aab7f9fc0..cd0bd622f 100644 --- a/app/assets/javascripts/controllers/admin/invoices.js.erb +++ b/app/assets/javascripts/controllers/admin/invoices.js.erb @@ -17,8 +17,8 @@ /** * Controller used in the admin invoices listing page */ -Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'Invoice', 'invoices', '$uibModal', 'growl', '$filter', 'Setting', 'settings', '_t', - function ($scope, $state, Invoice, invoices, $uibModal, growl, $filter, Setting, settings, _t) { +Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'Invoice', 'AccountingPeriod', 'invoices', 'closedPeriods', '$uibModal', 'growl', '$filter', 'Setting', 'settings', '_t', + function ($scope, $state, Invoice, AccountingPeriod, invoices, closedPeriods, $uibModal, growl, $filter, Setting, settings, _t) { /* PRIVATE STATIC CONSTANTS */ // number of invoices loaded each time we click on 'load more...' @@ -110,7 +110,9 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I templateUrl: '<%= asset_path "admin/invoices/avoirModal.html" %>', controller: 'AvoirModalController', resolve: { - invoice () { return invoice; } + invoice () { return invoice; }, + closedPeriods() { return AccountingPeriod.query().$promise; }, + lastClosingEnd() { return AccountingPeriod.lastClosingEnd().$promise; } } }); @@ -302,17 +304,34 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I active () { return $scope.invoice.VAT.active; }, - history () { + rateHistory () { return Setting.get({ name: 'invoice_VAT-rate', history: true }).$promise; + }, + activeHistory () { + return Setting.get({ name: 'invoice_VAT-active', history: true }).$promise; } }, - controller: ['$scope', '$uibModalInstance', 'rate', 'active', 'history', function ($scope, $uibModalInstance, rate, active, history) { + controller: ['$scope', '$uibModalInstance', 'rate', 'active', 'rateHistory', 'activeHistory', function ($scope, $uibModalInstance, rate, active, rateHistory, activeHistory) { $scope.rate = rate; $scope.isSelected = active; - $scope.history = history.setting.history; + $scope.history = []; $scope.ok = function () { $uibModalInstance.close({ rate: $scope.rate, active: $scope.isSelected }); }; - return $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; + $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; + + + const initialize = function() { + rateHistory.setting.history.forEach(function (rate) { + $scope.history.push({ date: rate.created_at, rate: rate.value, user: rate.user }) + }); + activeHistory.setting.history.forEach(function (v) { + if (v.value === 'false') { + $scope.history.push({ date: v.created_at, rate: 0, user: v.user }) + } + }); + } + + initialize(); }] }); @@ -391,6 +410,37 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I return invoiceSearch(true); }; + /** + * Open a modal allowing the user to close an accounting period and to + * view all periods already closed. + */ + $scope.closeAnAccountingPeriod = function() { + // open modal + $uibModal.open({ + templateUrl: '<%= asset_path "admin/invoices/closePeriodModal.html" %>', + controller: 'ClosePeriodModalController', + size: 'lg', + resolve: { + periods() { return AccountingPeriod.query().$promise; }, + lastClosingEnd() { return AccountingPeriod.lastClosingEnd().$promise; } + } + }); + } + + /** + * Test if the given date is within a closed accounting period + * @param date {Date} date to test + * @returns {boolean} true if closed, false otherwise + */ + $scope.isDateClosed = function(date) { + for (const period of closedPeriods) { + if (moment(date).isBetween(moment.utc(period.start_at).startOf('day'), moment.utc(period.end_at).endOf('day'), null, '[]')) { + return true; + } + } + return false; + } + /* PRIVATE SCOPE */ /** @@ -500,8 +550,8 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I /** * Controller used in the invoice refunding modal window */ -Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModalInstance', 'invoice', 'Invoice', 'growl', '_t', - function ($scope, $uibModalInstance, invoice, Invoice, growl, _t) { +Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModalInstance', 'invoice', 'closedPeriods', 'lastClosingEnd', 'Invoice', 'growl', '_t', + function ($scope, $uibModalInstance, invoice, closedPeriods, lastClosingEnd, Invoice, growl, _t) { /* PUBLIC SCOPE */ // invoice linked to the current refund @@ -517,6 +567,9 @@ Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModal invoice_items_ids: [] }; + // End date of last closed accounting period or date of first invoice + $scope.lastClosingEnd = moment.utc(lastClosingEnd.last_end_date).toDate(); + // Possible refunding methods $scope.avoirModes = [ { name: _t('invoices.none'), value: 'none' }, @@ -580,6 +633,20 @@ Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModal */ $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; + /** + * Test if the given date is within a closed accounting period + * @param date {Date} date to test + * @returns {boolean} true if closed, false otherwise + */ + $scope.isDateClosed = function(date) { + for (const period of closedPeriods) { + if (moment(date).isBetween(moment.utc(period.start_at).startOf('day'), moment.utc(period.end_at).endOf('day'), null, '[]')) { + return true; + } + } + return false; + } + /* PRIVATE SCOPE */ /** @@ -604,3 +671,115 @@ Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModal return initialize(); } ]); + + +/** + * Controller used in the modal window allowing an admin to close an accounting period + */ +Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$uibModalInstance', '$window', 'Invoice', 'AccountingPeriod', 'periods', 'lastClosingEnd','dialogs', 'growl', '_t', + function ($scope, $uibModalInstance, $window, Invoice, AccountingPeriod, periods, lastClosingEnd, dialogs, growl, _t) { + const YESTERDAY = moment.utc({ h: 0, m: 0, s: 0, ms: 0 }).subtract(1, 'day').toDate(); + const LAST_CLOSING = moment.utc(lastClosingEnd.last_end_date).toDate(); + const MAX_END = moment.utc(lastClosingEnd.last_end_date).add(1, 'year').subtract(1, 'day').toDate(); + + /* PUBLIC SCOPE */ + + // date pickers values are bound to these variables + $scope.period = { + start_at: LAST_CLOSING, + end_at: moment(YESTERDAY).isBefore(MAX_END) ? YESTERDAY : MAX_END + }; + + // any form errors will come here + $scope.errors = {}; + + // will match any error about invoices + $scope.invoiceErrorRE = /^invoice_(.+)$/; + + // existing closed periods, provided by the API + $scope.accountingPeriods = periods; + + // closing a period may take a long time so we need to prevent the user from double-clicking the close button while processing + $scope.pendingCreation = false; + + // AngularUI-Bootstrap datepickers parameters to define the period to close + $scope.datePicker = { + format: Fablab.uibDateFormat, + // default: datePicker are not shown + startOpened: false, + endOpened: false, + minDate: LAST_CLOSING, + maxDate: moment(YESTERDAY).isBefore(MAX_END) ? YESTERDAY : MAX_END, + options: { + startingDay: Fablab.weekStartingDay + } + }; + + /** + * Callback to open the datepicker + */ + $scope.toggleDatePicker = function ($event) { + $event.preventDefault(); + $event.stopPropagation(); + $scope.datePicker.endOpened = !$scope.datePicker.endOpened; + }; + + /** + * Validate the close period creation + */ + $scope.ok = function () { + dialogs.confirm( + { + resolve: { + object () { + return { + title: _t('invoices.confirmation_required'), + msg: _t( + 'invoices.confirm_close_START_END', + { START: moment.utc($scope.period.start_at).format('LL'), END: moment.utc($scope.period.end_at).format('LL') } + ) + }; + } + } + }, + function () { // creation confirmed + $scope.pendingCreation = true; + AccountingPeriod.save( + { + accounting_period: { + start_at: moment.utc($scope.period.start_at).toDate(), + end_at: moment.utc($scope.period.end_at).endOf('day').toDate() + } + }, + function (resp) { + $scope.pendingCreation = false; + growl.success(_t( + 'invoices.period_START_END_closed_success', + { START: moment.utc(resp.start_at).format('LL'), END: moment.utc(resp.end_at).format('LL') } + )); + $uibModalInstance.close(resp); + }, + function(error) { + $scope.pendingCreation = false; + growl.error(_t('invoices.failed_to_close_period')); + $scope.errors = error.data; + } + ); + } + ); + + }; + + /** + * Cancel the refund, dismiss the modal window + */ + $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; + + /** + * Trigger the API call to download the JSON archive of the closed accounting period + */ + $scope.downloadArchive = function(period) { + $window.location.href = `/api/accounting_periods/${period.id}/archive`; + } + } +]); diff --git a/app/assets/javascripts/controllers/admin/members.js.erb b/app/assets/javascripts/controllers/admin/members.js.erb index 3b4d5fd1a..c8c49ed49 100644 --- a/app/assets/javascripts/controllers/admin/members.js.erb +++ b/app/assets/javascripts/controllers/admin/members.js.erb @@ -150,7 +150,7 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', }; // admins list - $scope.admins = adminsPromise.admins; + $scope.admins = adminsPromise.admins.filter(function(m) { return m.id != Fablab.superadminId; }); // Admins ordering/sorting. Default: not sorted $scope.orderAdmin = null; diff --git a/app/assets/javascripts/controllers/application.js.erb b/app/assets/javascripts/controllers/application.js.erb index 7bae15ddb..7e89ca850 100644 --- a/app/assets/javascripts/controllers/application.js.erb +++ b/app/assets/javascripts/controllers/application.js.erb @@ -340,7 +340,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco var openLoginModal = function (toState, toParams, callback) { <% active_provider = AuthProvider.active %> <% if active_provider.providable_type != DatabaseProvider.name %> - $window.location.href = '<%=user_omniauth_authorize_path(AuthProvider.active.strategy_name.to_sym)%>'; + $window.location.href = '<%="/users/auth/#{active_provider.strategy_name}"%>'; <% else %> return $uibModal.open({ templateUrl: '<%= asset_path "shared/deviseModal.html" %>', diff --git a/app/assets/javascripts/router.js.erb b/app/assets/javascripts/router.js.erb index d0a256845..95bd0f3eb 100644 --- a/app/assets/javascripts/router.js.erb +++ b/app/assets/javascripts/router.js.erb @@ -885,6 +885,7 @@ angular.module('application.router', ['ui.router']) query: { number: '', customer: '', date: null, order_by: '-reference', page: 1, size: 20 } }).$promise; }], + closedPeriods: [ 'AccountingPeriod', function(AccountingPeriod) { return AccountingPeriod.query().$promise; }], translations: ['Translations', function (Translations) { return Translations.query('app.admin.invoices').$promise; }] } }) diff --git a/app/assets/javascripts/services/accounting_period.js b/app/assets/javascripts/services/accounting_period.js new file mode 100644 index 000000000..9ffb9e03a --- /dev/null +++ b/app/assets/javascripts/services/accounting_period.js @@ -0,0 +1,12 @@ +'use strict'; + +Application.Services.factory('AccountingPeriod', ['$resource', function ($resource) { + return $resource('/api/accounting_periods/:id', + { id: '@id' }, { + lastClosingEnd: { + method: 'GET', + url: '/api/accounting_periods/last_closing_end' + } + } + ); +}]); diff --git a/app/assets/stylesheets/app.components.scss b/app/assets/stylesheets/app.components.scss index 66fffacab..f04f147b6 100644 --- a/app/assets/stylesheets/app.components.scss +++ b/app/assets/stylesheets/app.components.scss @@ -616,4 +616,8 @@ padding: 10px; & > i.fileinput-exists { margin-right: 5px; } -} \ No newline at end of file +} + +.help-block.error { + color: #ff565d; +} diff --git a/app/assets/stylesheets/application.scss.erb b/app/assets/stylesheets/application.scss.erb index 965ec8d21..f8320961f 100644 --- a/app/assets/stylesheets/application.scss.erb +++ b/app/assets/stylesheets/application.scss.erb @@ -33,6 +33,7 @@ @import "app.components"; @import "app.plugins"; @import "modules/invoice"; +@import "modules/signup"; @import "app.responsive"; diff --git a/app/assets/stylesheets/modules/invoice.scss b/app/assets/stylesheets/modules/invoice.scss index f8c0c230f..9c36c3b1f 100644 --- a/app/assets/stylesheets/modules/invoice.scss +++ b/app/assets/stylesheets/modules/invoice.scss @@ -1,6 +1,14 @@ // admin invoices +.chained { + color: green; +} + +.broken { + color: red; +} + .invoice-placeholder { width: 80%; max-width: 800px; @@ -184,3 +192,79 @@ font-style: italic; color: #5a5a5a; } + + +table.closings-table { + @extend table.scrollable-3-cols; + + tbody .actions { + padding-left: 2em; + + & > span { + margin-left: 2em; + cursor: pointer; + } + } + + tbody .show-more { + color: #00b3ee; + } + + tbody .download-archive { + width: 32px; + height: 32px; + } + + tbody .download-archive:hover { + i { + display: none; + } + &:after { + content: '\f019'; + font-family: 'fontawesome'; + } + } +} + +table.scrollable-3-cols { + width: 100%; + border-spacing: 0; + + thead, tbody, tr, th, td { display: block; } + + thead tr { + /* fallback */ + width: 97%; + /* minus scroll bar width */ + width: -webkit-calc(100% - 16px); + width: -moz-calc(100% - 16px); + width: calc(100% - 16px); + } + + thead tr th { + border-bottom: 0; + } + + tr:after { /* clearing float */ + content: ' '; + display: block; + visibility: hidden; + clear: both; + } + + tbody { + height: 200px; + overflow-y: auto; + overflow-x: hidden; + } + + tbody td, thead th { + width: 32%; /* 32% is less than (100% / 3 cols) = 33.33% */ + float: left; + } +} + + +.period-info-title { + font-weight: bold; +} diff --git a/app/assets/stylesheets/modules/signup.scss b/app/assets/stylesheets/modules/signup.scss new file mode 100644 index 000000000..986bb11e4 --- /dev/null +++ b/app/assets/stylesheets/modules/signup.scss @@ -0,0 +1,31 @@ +.signup-form { + .names-row { + input.form-control { + width: 89%; + display: inline-block; + } + } + .required-row { + div.input-group { + width: 95%; + display: inline-table; + } + select.form-control { + width: 95%; + display: inline-block; + } + .exponent { + position: relative; + top: -14px; + right: -4px; + } + .exponent-select { + top: -1px; + } + } + .info-required { + color: #5a5a5a; + font-size: 8pt; + font-style: italic; + } +} diff --git a/app/assets/templates/admin/invoices/_period.html.erb b/app/assets/templates/admin/invoices/_period.html.erb new file mode 100644 index 000000000..f234c2e1d --- /dev/null +++ b/app/assets/templates/admin/invoices/_period.html.erb @@ -0,0 +1,11 @@ + diff --git a/app/assets/templates/admin/invoices/avoirModal.html.erb b/app/assets/templates/admin/invoices/avoirModal.html.erb index 348c704dc..6d49ebdff 100644 --- a/app/assets/templates/admin/invoices/avoirModal.html.erb +++ b/app/assets/templates/admin/invoices/avoirModal.html.erb @@ -14,6 +14,7 @@ uib-datepicker-popup="{{datePicker.format}}" datepicker-options="datePicker.options" is-open="datePicker.opened" + min-date="lastClosingEnd" placeholder="{{datePicker.format}}" ng-click="openDatePicker($event)" required/> diff --git a/app/assets/templates/admin/invoices/closePeriodModal.html.erb b/app/assets/templates/admin/invoices/closePeriodModal.html.erb new file mode 100644 index 000000000..e75ade394 --- /dev/null +++ b/app/assets/templates/admin/invoices/closePeriodModal.html.erb @@ -0,0 +1,79 @@ + + + diff --git a/app/assets/templates/admin/invoices/index.html.erb b/app/assets/templates/admin/invoices/index.html.erb index 8b2cfe12b..2ff43d5ba 100644 --- a/app/assets/templates/admin/invoices/index.html.erb +++ b/app/assets/templates/admin/invoices/index.html.erb @@ -10,7 +10,11 @@

{{ 'invoices.invoices' }}

- +
+
+ {{ 'invoices.accounting_periods' | translate }} +
+
@@ -57,6 +61,7 @@ + @@ -70,6 +75,10 @@ + @@ -83,7 +92,7 @@ {{ 'invoices.download_the_credit_note' | translate }} - + {{ 'invoices.credit_note' | translate }} @@ -387,18 +396,21 @@

{{ 'invoices.VAT_history' }}

-
{{ 'invoices.invoice_#' | translate }} {{ 'invoices.date' | translate }}
+ + + {{ invoice.reference }} {{ invoice.date | amDateFormat:'L LTS' }} {{ invoice.date | amDateFormat:'L' }}
- +
+ - + - - - + + + diff --git a/app/assets/templates/shared/header.html.erb b/app/assets/templates/shared/header.html.erb index 3a2c59502..9a3fe9cfc 100644 --- a/app/assets/templates/shared/header.html.erb +++ b/app/assets/templates/shared/header.html.erb @@ -53,9 +53,9 @@ {{ 'sign_in' | translate }} <% else %> -
  • {{ 'sign_up' | translate }}
  • +
  • " class="font-sbold label text-md"> {{ 'sign_up' | translate }}
  • - {{ 'sign_in' | translate }} + " class="font-sbold label text-md"> {{ 'sign_in' | translate }}
  • <% end %> diff --git a/app/assets/templates/shared/signupModal.html.erb b/app/assets/templates/shared/signupModal.html.erb index 778a50c72..ba356e4ca 100644 --- a/app/assets/templates/shared/signupModal.html.erb +++ b/app/assets/templates/shared/signupModal.html.erb @@ -6,7 +6,7 @@ {{alert.msg}}
    -
    +
    + {{ 'gender_is_required'}}
    -
    +
    + {{ 'first_name_is_required' }}
    @@ -45,11 +47,12 @@ class="form-control" placeholder="{{ 'your_surname' | translate }}" required> + {{ 'surname_is_required' }}
    -
    +
    @@ -60,11 +63,12 @@ placeholder="{{ 'your_pseudonym' | translate }}" required>
    + {{ 'pseudonym_is_required' }}
    -
    +
    @@ -75,11 +79,12 @@ placeholder="{{ 'your_email_address' | translate }}" required>
    + {{ 'email_is_required' }}
    -
    +
    @@ -91,12 +96,13 @@ required ng-minlength="8">
    + {{ 'password_is_required' }} {{ 'password_is_too_short_(minimum_8_characters)' }}
    -
    +
    @@ -108,6 +114,7 @@ required ng-minlength="8" match="user.password">
    + {{ 'password_confirmation_is_required' }} {{ 'password_does_not_match_with_confirmation' }}
    @@ -124,7 +131,7 @@
    -
    +
    @@ -135,11 +142,12 @@ placeholder="{{ 'name_of_your_organization' | translate }}" ng-required="user.organization">
    + {{ 'organization_name_is_required' }}
    -
    +
    @@ -150,22 +158,24 @@ placeholder="{{ 'address_of_your_organization' | translate }}" ng-required="user.organization">
    + {{ 'organization_address_is_required' }}
    -
    +
    +
    {{ 'user_s_profile_is_required' }}
    -
    +
    @@ -180,11 +190,12 @@ ng-click="openDatePicker($event)" required/>
    + {{ 'birth_date_is_required' }}
    -
    +
    @@ -195,6 +206,7 @@ placeholder="{{ 'phone_number' | translate }}" required>
    + {{ 'phone_number_is_required' }}
    @@ -229,9 +241,16 @@ ng-model="user.cgu" value="true" ng-required="cgu != null"/> - +
    + + + {{ 'field_required' }} +
    @@ -240,4 +259,4 @@
    \ No newline at end of file +
    diff --git a/app/controllers/api/accounting_periods_controller.rb b/app/controllers/api/accounting_periods_controller.rb new file mode 100644 index 000000000..f374e18bd --- /dev/null +++ b/app/controllers/api/accounting_periods_controller.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# API Controller for resources of AccountingPeriod +class API::AccountingPeriodsController < API::ApiController + + before_action :authenticate_user! + before_action :set_period, only: %i[show download_archive] + + def index + @accounting_periods = AccountingPeriodService.all_periods_with_users + end + + def show; end + + def create + authorize AccountingPeriod + @accounting_period = AccountingPeriod.new(period_params.merge(closed_at: DateTime.now, closed_by: current_user.id)) + if @accounting_period.save + render :show, status: :created, location: @accounting_period + else + render json: @accounting_period.errors, status: :unprocessable_entity + end + end + + def last_closing_end + authorize AccountingPeriod + last_period = AccountingPeriodService.find_last_period + if last_period.nil? + invoice = Invoice.order(:created_at).first + @last_end = invoice.created_at if invoice + else + @last_end = last_period.end_at + 1.day + end + end + + def download_archive + authorize AccountingPeriod + send_file File.join(Rails.root, @accounting_period.archive_file), type: 'application/json', disposition: 'attachment' + end + + private + + def set_period + @accounting_period = AccountingPeriod.find(params[:id]) + end + + def period_params + params.require(:accounting_period).permit(:start_at, :end_at) + end +end diff --git a/app/controllers/api/events_controller.rb b/app/controllers/api/events_controller.rb index 127b8fa5d..0791b8de0 100644 --- a/app/controllers/api/events_controller.rb +++ b/app/controllers/api/events_controller.rb @@ -96,7 +96,7 @@ class API::EventsController < API::ApiController :recurrence_end_at, :category_id, :event_theme_ids, :age_range_id, event_theme_ids: [], event_image_attributes: [:attachment], - event_files_attributes: %i[id attachment_destroy], + event_files_attributes: %i[id attachment _destroy], event_price_categories_attributes: %i[id price_category_id amount _destroy]) EventService.process_params(event_preparams) end diff --git a/app/controllers/api/reservations_controller.rb b/app/controllers/api/reservations_controller.rb index ea5e450b6..5ef8e1848 100644 --- a/app/controllers/api/reservations_controller.rb +++ b/app/controllers/api/reservations_controller.rb @@ -26,7 +26,7 @@ class API::ReservationsController < API::ApiController user_id = current_user.admin? ? reservation_params[:user_id] : current_user.id @reservation = Reservation.new(reservation_params) - is_reserve = Reservations::Reserve.new(user_id) + is_reserve = Reservations::Reserve.new(user_id, current_user.id) .pay_and_save(@reservation, method, coupon_params[:coupon_code]) if is_reserve diff --git a/app/controllers/api/subscriptions_controller.rb b/app/controllers/api/subscriptions_controller.rb index 0b11b13ea..9bd1d98bd 100644 --- a/app/controllers/api/subscriptions_controller.rb +++ b/app/controllers/api/subscriptions_controller.rb @@ -19,7 +19,7 @@ class API::SubscriptionsController < API::ApiController user_id = current_user.admin? ? subscription_params[:user_id] : current_user.id @subscription = Subscription.new(subscription_params) - is_subscribe = Subscriptions::Subscribe.new(user_id) + is_subscribe = Subscriptions::Subscribe.new(user_id, current_user.id) .pay_and_save(@subscription, method, coupon_params[:coupon_code], true) if is_subscribe @@ -35,7 +35,7 @@ class API::SubscriptionsController < API::ApiController free_days = params[:subscription][:free] || false - res = Subscriptions::Subscribe.new(@subscription.user_id) + res = Subscriptions::Subscribe.new(@subscription.user_id, current_user.id) .extend_subscription(@subscription, subscription_update_params[:expired_at], free_days) if res.is_a?(Subscription) @subscription = res diff --git a/app/controllers/api/version_controller.rb b/app/controllers/api/version_controller.rb index b87c3077b..b466c27a9 100644 --- a/app/controllers/api/version_controller.rb +++ b/app/controllers/api/version_controller.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +require 'version' # API Controller to get the fab-manager version class API::VersionController < API::ApiController @@ -6,8 +7,7 @@ class API::VersionController < API::ApiController def show authorize :version - package = File.read('package.json') - version = JSON.parse(package)['version'] - render json: { version: version }, status: :ok + + render json: { version: Version.current }, status: :ok end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 90a61671c..f59db1763 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -30,11 +30,16 @@ class ApplicationController < ActionController::Base end def configure_permitted_parameters - devise_parameter_sanitizer.for(:sign_up) << - { profile_attributes: [:phone, :last_name, :first_name, :gender, :birthday, :interest, :software_mastered, - organization_attributes: [:name, address_attributes: [:address]]] } - - devise_parameter_sanitizer.for(:sign_up).concat %i[username is_allow_contact is_allow_newsletter cgu group_id] + devise_parameter_sanitizer.permit(:sign_up, + keys: [ + { profile_attributes: [ + :phone, :last_name, :first_name, :gender, :birthday, + :interest, :software_mastered, organization_attributes: [ + :name, address_attributes: [:address] + ] + ] }, + :username, :is_allow_contact, :is_allow_newsletter, :cgu, :group_id + ]) end def default_url_options diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 5488450ad..e7ee47a0e 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -4,7 +4,7 @@ class SessionsController < Devise::SessionsController def new active_provider = AuthProvider.active if active_provider.providable_type != DatabaseProvider.name - redirect_to user_omniauth_authorize_path(active_provider.strategy_name.to_sym) + redirect_to "/users/auth/#{active_provider.strategy_name}" else super end diff --git a/app/doc/open_api/api_doc.rb b/app/doc/open_api/api_doc.rb index e984db547..cb7dabe97 100644 --- a/app/doc/open_api/api_doc.rb +++ b/app/doc/open_api/api_doc.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # app/concerns/controllers/api_doc.rb # # Controller extension with common API documentation shortcuts # - module OpenAPI::ApiDoc # Apipie doesn't allow to append anything to esisting # description. It raises an error on double definition. diff --git a/app/doc/open_api/application_doc.rb b/app/doc/open_api/application_doc.rb index e677037e5..ff21bd2a5 100644 --- a/app/doc/open_api/application_doc.rb +++ b/app/doc/open_api/application_doc.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # app/docs/application_doc.rb # # A common class for defining API docs @@ -16,7 +18,6 @@ # end # end # - class OpenAPI::ApplicationDoc extend OpenAPI::ApiDoc diff --git a/app/doc/open_api/v1/base_doc.rb b/app/doc/open_api/v1/base_doc.rb index 1772d0b5d..7e04b2110 100644 --- a/app/doc/open_api/v1/base_doc.rb +++ b/app/doc/open_api/v1/base_doc.rb @@ -1,5 +1,8 @@ +# frozen_string_literal: true + +# parent class for openAPI documentation class OpenAPI::V1::BaseDoc < OpenAPI::ApplicationDoc - API_VERSION = "v1" - FORMATS = ['json'] + API_VERSION = 'v1' + FORMATS = ['json'].freeze PER_PAGE_DEFAULT = 20 end diff --git a/app/doc/open_api/v1/bookable_machines_doc.rb b/app/doc/open_api/v1/bookable_machines_doc.rb index f47ae11cd..c80fc6854 100644 --- a/app/doc/open_api/v1/bookable_machines_doc.rb +++ b/app/doc/open_api/v1/bookable_machines_doc.rb @@ -1,3 +1,6 @@ +# frozen_string_literal: true + +# openAPI documentation for bookable machines endpoint class OpenAPI::V1::BookableMachinesDoc < OpenAPI::V1::BaseDoc resource_description do short 'Bookable machines' @@ -7,10 +10,10 @@ class OpenAPI::V1::BookableMachinesDoc < OpenAPI::V1::BaseDoc end doc_for :index do - api :GET, "/#{API_VERSION}/bookable_machines", "Bookable machines index" - description "Machines that a given user is allowed to book." - param :user_id, Integer, required: true, desc: "Id of the given user." - example <<-EOS + api :GET, "/#{API_VERSION}/bookable_machines", 'Bookable machines index' + description 'Machines that a given user is allowed to book.' + param :user_id, Integer, required: true, desc: 'Id of the given user.' + example <<-MACHINES # /open_api/v1/bookable_machines?user_id=522 { "machines": [ @@ -67,6 +70,6 @@ class OpenAPI::V1::BookableMachinesDoc < OpenAPI::V1::BaseDoc # ... ] } - EOS + MACHINES end end diff --git a/app/doc/open_api/v1/concerns/param_groups.rb b/app/doc/open_api/v1/concerns/param_groups.rb index d43efda6c..590d1396a 100644 --- a/app/doc/open_api/v1/concerns/param_groups.rb +++ b/app/doc/open_api/v1/concerns/param_groups.rb @@ -1,30 +1,13 @@ +# frozen_string_literal: true + +# openAPI pagination module OpenAPI::V1::Concerns::ParamGroups extend ActiveSupport::Concern included do define_param_group :pagination do - param :page, Integer, desc: "Page number", optional: true + param :page, Integer, desc: 'Page number', optional: true param :per_page, Integer, desc: "Number of objects per page. Default is #{OpenAPI::V1::BaseDoc::PER_PAGE_DEFAULT}.", optional: true end - - # define_param_group :order_type do - # param :order_type, ['asc', 'desc'], desc: "order type: descendant or ascendant. Default value is *desc*." - # end - # - # define_param_group :filter_by_tags do - # param :tagged_with, [String, Array], desc: 'If multiple tags are given, we use an *OR* function. See parameter *order_by_matching_tag_count* to order the result. It can also be a *comma* *separated* *string*. Example: tagged_with=science,museum' - # param :order_by_matching_tag_count, ['t',1,'true'], desc: "You can use this parameter if you are sending a parameter *tagged_with*. Send this parameter to order by number of matching tags (descendant): result will be sort firstly by matching tags and secondly by order given by *order_by* parameter. Default to *false*." - # end - # - # define_param_group :filter_by_blog do - # param :blog_slug, String, desc: "Send the blog's *slug* to only return articles belonging to specific blog." - # end - # - # define_param_group :filter_by_geolocation do - # param :latitude, Numeric, desc: "Latitude. Example: *45.166670*" - # param :longitude, Numeric, desc: "Longitude. Example: *5.7166700*" - # param :radius, Numeric, desc: "To be combined with parameters latitude and longitude. Default to *10*." - # param :order_by_distance, ['t',1,'true'], desc: "You can use this parameter if you are sending parameters *latitude* and *longitude*. Send this parameter to order by distance (descendant): result will be sort firstly by distance and secondly by order given by *order_by* parameter. Default to *false*." - # end end end diff --git a/app/doc/open_api/v1/events_doc.rb b/app/doc/open_api/v1/events_doc.rb index 4b8f391de..7fd90c778 100644 --- a/app/doc/open_api/v1/events_doc.rb +++ b/app/doc/open_api/v1/events_doc.rb @@ -1,3 +1,6 @@ +# frozen_string_literal: true + +# openAPI documentation for events endpoint class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc resource_description do short 'Events' @@ -9,19 +12,19 @@ class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc include OpenAPI::V1::Concerns::ParamGroups doc_for :index do - api :GET, "/#{API_VERSION}/events", "Events index" + api :GET, "/#{API_VERSION}/events", 'Events index' param_group :pagination - param :id, [Integer, Array], optional: true, desc: "Scope the request to one or various events." - param :upcoming, [FalseClass, TrueClass], optional: true, desc: "Scope for the upcoming events." - description "Events index. Order by *created_at* desc." - example <<-EOS + param :id, [Integer, Array], optional: true, desc: 'Scope the request to one or various events.' + param :upcoming, [FalseClass, TrueClass], optional: true, desc: 'Scope for the upcoming events.' + description 'Events index. Order by *created_at* desc.' + example <<-EVENTS # /open_api/v1/events?page=1&per_page=2 { "events": [ { "id": 183, "title": "OPEN LAB", - "description": "Que vous soyez Fab user, visiteur, curieux ou bricoleur, l’atelier de fabrication numérique vous ouvre ses portes les mercredis soirs pour avancer vos projets ou rencontrer la «communauté» Fab Lab. \r\n\r\nCe soir, venez spécialement découvrir les machines à commandes numérique du Fab Lab de La Casemate, venez comprendre ce lieux ouvert à tous. \r\n\r\n\r\nVenez découvrir un concept, une organisation, des machines, pour stimuler votre sens de la créativité.", + "description": "Que vous soyez Fab user, visiteur, curieux ou bricoleur, l’atelier de fabrication numérique vous ouvre ses portes les mercredis soirs pour avancer vos projets ou rencontrer la «communauté» Fab Lab. \r\n\r\nCe soir, venez spécialement découvrir les machines à commandes numérique de la Fabrique de Fab-manager, venez comprendre ce lieux ouvert à tous. \r\n\r\n\r\nVenez découvrir un concept, une organisation, des machines, pour stimuler votre sens de la créativité.", "updated_at": "2016-04-25T10:49:40.055+02:00", "created_at": "2016-04-25T10:49:40.055+02:00", "nb_total_places": 18, @@ -54,6 +57,6 @@ class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc } ] } - EOS + EVENTS end end diff --git a/app/doc/open_api/v1/invoices_doc.rb b/app/doc/open_api/v1/invoices_doc.rb index abc360596..d6289a599 100644 --- a/app/doc/open_api/v1/invoices_doc.rb +++ b/app/doc/open_api/v1/invoices_doc.rb @@ -1,3 +1,6 @@ +# frozen_string_literal: true + +# openAPI documentation for invoices endpoints class OpenAPI::V1::InvoicesDoc < OpenAPI::V1::BaseDoc resource_description do short 'Invoices' @@ -9,11 +12,11 @@ class OpenAPI::V1::InvoicesDoc < OpenAPI::V1::BaseDoc include OpenAPI::V1::Concerns::ParamGroups doc_for :index do - api :GET, "/#{API_VERSION}/invoices", "Invoices index" + api :GET, "/#{API_VERSION}/invoices", 'Invoices index' description "Index of users' invoices, with optional pagination. Order by *created_at* descendant." param_group :pagination - param :user_id, [Integer, Array], optional: true, desc: "Scope the request to one or various users." - example <<-EOS + param :user_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various users.' + example <<-INVOICES # /open_api/v1/invoices?user_id=211&page=1&per_page=3 { "invoices": [ @@ -64,15 +67,15 @@ class OpenAPI::V1::InvoicesDoc < OpenAPI::V1::BaseDoc } ] } - EOS + INVOICES end - doc_for :download do - api :GET, "/#{API_VERSION}/invoices/:id/download", "Download an invoice" - param :id, Integer, desc: "Invoice id", required: true + doc_for :download do + api :GET, "/#{API_VERSION}/invoices/:id/download", 'Download an invoice' + param :id, Integer, desc: 'Invoice id', required: true - example <<-EOS - # /open_api/v1/invoices/2809/download - EOS - end + example <<-URL + # /open_api/v1/invoices/2809/download + URL + end end diff --git a/app/doc/open_api/v1/machines_doc.rb b/app/doc/open_api/v1/machines_doc.rb index aa1a67e79..42edb050a 100644 --- a/app/doc/open_api/v1/machines_doc.rb +++ b/app/doc/open_api/v1/machines_doc.rb @@ -1,3 +1,6 @@ +# frozen_string_literal: true + +# openAPI documentation for machines endpoint class OpenAPI::V1::MachinesDoc < OpenAPI::V1::BaseDoc resource_description do short 'Machines' @@ -7,9 +10,9 @@ class OpenAPI::V1::MachinesDoc < OpenAPI::V1::BaseDoc end doc_for :index do - api :GET, "/#{API_VERSION}/machines", "Machines index" - description "Machines index. Order by *created_at* ascendant." - example <<-EOS + api :GET, "/#{API_VERSION}/machines", 'Machines index' + description 'Machines index. Order by *created_at* ascendant.' + example <<-MACHINES # /open_api/v1/machines { "machines": [ @@ -63,7 +66,7 @@ class OpenAPI::V1::MachinesDoc < OpenAPI::V1::BaseDoc "description": "La fraiseuse numérique Roland Modela MDX-20\r\n\r\nInformations générales :\r\nCette machine est utilisée pour l'usinage et le scannage 3D de précision. Elle permet principalement d'usiner des circuits imprimés et des moules de petite taille. Le faible diamètre des fraises utilisées (Ø 0,3 mm à Ø 6mm) implique que certains temps d'usinages peuvent êtres long (> 12h), c'est pourquoi cette fraiseuse peut être laissée en autonomie toute une nuit afin d'obtenir le plus précis des usinages au FabLab.\r\n\r\nMatériaux usinables :\r\nLes principaux matériaux usinables sont : bois, plâtre, résine, cire usinable, cuivre.\r\n", "spec": "Taille du plateau X/Y : 220 mm x 160 mm\r\nVolume maximal de travail: 203,2 mm (X), 152,4 mm (Y), 60,5 mm (Z)\r\nPrécision usinage: 0,00625 mm\r\nPrécision scannage: réglable de 0,05 à 5 mm (axes X,Y) et 0,025 mm (axe Z)\r\nVitesse d'analyse (scannage): 4-15 mm/sec\r\n \r\n \r\nLogiciel utilisé pour le fraisage: Roland Modela player 4 \r\nLogiciel utilisé pour l'usinage de circuits imprimés: Cad.py (linux)\r\nFormats acceptés: STL,PNG 3D\r\nFormat d'exportation des données scannées: DXF, VRML, STL, 3DMF, IGES, Grayscale, Point Group et BMP\r\n" }, - # + # # .... # { @@ -78,6 +81,6 @@ class OpenAPI::V1::MachinesDoc < OpenAPI::V1::BaseDoc } ] } - EOS + MACHINES end end diff --git a/app/doc/open_api/v1/reservations_doc.rb b/app/doc/open_api/v1/reservations_doc.rb index 279e70062..f56458ec7 100644 --- a/app/doc/open_api/v1/reservations_doc.rb +++ b/app/doc/open_api/v1/reservations_doc.rb @@ -1,3 +1,6 @@ +# frozen_string_literal: true + +# openAPI documentation for reservations endpoint class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc resource_description do short 'Reservations' @@ -9,14 +12,14 @@ class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc include OpenAPI::V1::Concerns::ParamGroups doc_for :index do - api :GET, "/#{API_VERSION}/reservations", "Reservations index" - description "Index of reservations made by users, with optional pagination. Order by *created_at* descendant." + api :GET, "/#{API_VERSION}/reservations", 'Reservations index' + description 'Index of reservations made by users, with optional pagination. Order by *created_at* descendant.' param_group :pagination - param :user_id, [Integer, Array], optional: true, desc: "Scope the request to one or various users." - param :reservable_type, ['Event', 'Machine', 'Training'], optional: true, desc: "Scope the request to a specific type of reservable." - param :reservable_id, [Integer, Array], optional: true, desc: "Scope the request to one or various reservables." + param :user_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various users.' + param :reservable_type, %w[Event Machine Training], optional: true, desc: 'Scope the request to a specific type of reservable.' + param :reservable_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various reservables.' - example <<-EOS + example <<-RESERVATIONS # /open_api/v1/reservations?reservable_type=Event&page=1&per_page=3 { "reservations": [ @@ -85,6 +88,6 @@ class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc } ] } - EOS + RESERVATIONS end end diff --git a/app/doc/open_api/v1/trainings_doc.rb b/app/doc/open_api/v1/trainings_doc.rb index ade2975b0..9d0a28950 100644 --- a/app/doc/open_api/v1/trainings_doc.rb +++ b/app/doc/open_api/v1/trainings_doc.rb @@ -1,3 +1,6 @@ +# frozen_string_literal: true + +# openAPI documentation for trainings endpoint class OpenAPI::V1::TrainingsDoc < OpenAPI::V1::BaseDoc resource_description do short 'Trainings' @@ -7,9 +10,9 @@ class OpenAPI::V1::TrainingsDoc < OpenAPI::V1::BaseDoc end doc_for :index do - api :GET, "/#{API_VERSION}/trainings", "Trainings index" - description "Trainings index. Order by *created_at* ascendant." - example <<-EOS + api :GET, "/#{API_VERSION}/trainings", 'Trainings index' + description 'Trainings index. Order by *created_at* ascendant.' + example <<-TRAININGS # /open_api/v1/trainings { "trainings": [ @@ -75,6 +78,6 @@ class OpenAPI::V1::TrainingsDoc < OpenAPI::V1::BaseDoc } ] } - EOS + TRAININGS end end diff --git a/app/doc/open_api/v1/user_trainings_doc.rb b/app/doc/open_api/v1/user_trainings_doc.rb index 232505abb..d2de34d17 100644 --- a/app/doc/open_api/v1/user_trainings_doc.rb +++ b/app/doc/open_api/v1/user_trainings_doc.rb @@ -1,3 +1,6 @@ +# frozen_string_literal: true + +# openAPI documentation for user's trainings endpoint class OpenAPI::V1::UserTrainingsDoc < OpenAPI::V1::BaseDoc resource_description do short 'User trainings' @@ -9,12 +12,12 @@ class OpenAPI::V1::UserTrainingsDoc < OpenAPI::V1::BaseDoc include OpenAPI::V1::Concerns::ParamGroups doc_for :index do - api :GET, "/#{API_VERSION}/user_trainings", "User trainings index" - description "Index of trainings accomplished by users, with optional pagination. Order by *created_at* descendant." + api :GET, "/#{API_VERSION}/user_trainings", 'User trainings index' + description 'Index of trainings accomplished by users, with optional pagination. Order by *created_at* descendant.' param_group :pagination - param :training_id, [Integer, Array], optional: true, desc: "Scope the request to one or various trainings." - param :user_id, [Integer, Array], optional: true, desc: "Scope the request to one or various users." - example <<-EOS + param :training_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various trainings.' + param :user_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various users.' + example <<-TRAININGS # /open_api/v1/user_trainings?training_id[]=3&training_id[]=4&page=1&per_page=2 { "user_trainings": [ @@ -94,6 +97,6 @@ class OpenAPI::V1::UserTrainingsDoc < OpenAPI::V1::BaseDoc } ] } - EOS + TRAININGS end end diff --git a/app/doc/open_api/v1/users_doc.rb b/app/doc/open_api/v1/users_doc.rb index 12f4b70f4..3f1b3c6b5 100644 --- a/app/doc/open_api/v1/users_doc.rb +++ b/app/doc/open_api/v1/users_doc.rb @@ -1,3 +1,6 @@ +# frozen_string_literal: true + +# openAPI documentation for user endpoint class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc resource_description do short 'Users' @@ -9,12 +12,12 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc include OpenAPI::V1::Concerns::ParamGroups doc_for :index do - api :GET, "/#{API_VERSION}/users", "Users index" - description "Users index, with optional pagination. Order by *created_at* descendant." + api :GET, "/#{API_VERSION}/users", 'Users index' + description 'Users index, with optional pagination. Order by *created_at* descendant.' param_group :pagination - param :email, [String, Array], optional: true, desc: "Filter users by *email* using strict matching." - param :user_id, [Integer, Array], optional: true, desc: "Filter users by *id* using strict matching." - example <<-EOS + param :email, [String, Array], optional: true, desc: 'Filter users by *email* using strict matching.' + param :user_id, [Integer, Array], optional: true, desc: 'Filter users by *id* using strict matching.' + example <<-USERS # /open_api/v1/users?page=1&per_page=4 { "users": [ @@ -92,6 +95,6 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc } ] } - EOS + USERS end end diff --git a/app/models/accounting_period.rb b/app/models/accounting_period.rb new file mode 100644 index 000000000..b50e106bc --- /dev/null +++ b/app/models/accounting_period.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +require 'checksum' +require 'version' +require 'zip' + +# AccountingPeriod is a period of N days (N > 0) which as been closed by an admin +# to prevent writing new accounting lines (invoices & refunds) during this period of time. +class AccountingPeriod < ActiveRecord::Base + before_destroy { false } + before_update { false } + before_create :compute_totals + after_create :archive_closed_data + + validates :start_at, :end_at, :closed_at, :closed_by, presence: true + validates_with DateRangeValidator + validates_with PeriodOverlapValidator + validates_with PeriodIntegrityValidator + + def delete + false + end + + def invoices + Invoice.where('created_at >= :start_date AND CAST(created_at AS DATE) <= :end_date', start_date: start_at, end_date: end_at) + end + + def invoices_with_vat(invoices) + invoices.map do |i| + if i.type == 'Avoir' + { invoice: i, vat_rate: vat_rate(i.avoir_date) } + else + { invoice: i, vat_rate: vat_rate(i.created_at) } + end + end + end + + def archive_folder + dir = "accounting/#{id}" + + # create directory if it doesn't exists (accounting) + FileUtils.mkdir_p dir + dir + end + + def archive_file + "#{archive_folder}/#{start_at.iso8601}_#{end_at.iso8601}.zip" + end + + def archive_json_file + "#{start_at.iso8601}_#{end_at.iso8601}.json" + end + + def check_footprint + footprint == compute_footprint + end + + def vat_rate(date) + @vat_rates = vat_history if @vat_rates.nil? + + first_rate = @vat_rates.first + return first_rate[:rate] if date < first_rate[:date] + + @vat_rates.each do |h| + return h[:rate] if h[:date] <= date + end + end + + private + + def vat_history + key_dates = [] + Setting.find_by(name: 'invoice_VAT-rate').history_values.each do |rate| + key_dates.push(date: rate.created_at, rate: (rate.value.to_i / 100.0)) + end + Setting.find_by(name: 'invoice_VAT-active').history_values.each do |v| + key_dates.push(date: v.created_at, rate: 0) if v.value == 'false' + end + key_dates.sort_by { |k| k[:date] } + end + + def to_json_archive(invoices, previous_file, last_checksum) + code_checksum = Checksum.code + ApplicationController.new.view_context.render( + partial: 'archive/accounting', + locals: { + invoices: invoices_with_vat(invoices), + period_total: period_total, + perpetual_total: perpetual_total, + period_footprint: footprint, + code_checksum: code_checksum, + last_archive_checksum: last_checksum, + previous_file: previous_file, + software_version: Version.current, + date: Time.now.iso8601 + }, + formats: [:json], + handlers: [:jbuilder] + ) + end + + def previous_period + AccountingPeriod.where('closed_at < ?', closed_at).order(closed_at: :desc).limit(1).last + end + + def archive_closed_data + data = invoices.includes(:invoice_items) + previous_file = previous_period&.archive_file + last_archive_checksum = previous_file ? Checksum.file(previous_file) : nil + json_data = to_json_archive(data, previous_file, last_archive_checksum) + current_archive_checksum = Checksum.text(json_data) + + Zip::OutputStream.open(archive_file) do |io| + io.put_next_entry(archive_json_file) + io.write(json_data) + io.put_next_entry('checksum.sha256') + io.write("#{current_archive_checksum}\t#{archive_json_file}") + io.put_next_entry('chained.sha256') + io.write(Checksum.text("#{current_archive_checksum}#{last_archive_checksum}#{DateTime.iso8601}")) + end + end + + def price_without_taxe(invoice) + invoice[:invoice].total - (invoice[:invoice].total * invoice[:vat_rate]) + end + + def compute_totals + period_invoices = invoices_with_vat(invoices.where(type: nil)) + period_avoirs = invoices_with_vat(invoices.where(type: 'Avoir')) + self.period_total = (period_invoices.map(&method(:price_without_taxe)).reduce(:+) || 0) - + (period_avoirs.map(&method(:price_without_taxe)).reduce(:+) || 0) + + all_invoices = invoices_with_vat(Invoice.where('CAST(created_at AS DATE) <= :end_date AND type IS NULL', end_date: end_at)) + all_avoirs = invoices_with_vat(Invoice.where("CAST(created_at AS DATE) <= :end_date AND type = 'Avoir'", end_date: end_at)) + self.perpetual_total = (all_invoices.map(&method(:price_without_taxe)).reduce(:+) || 0) - + (all_avoirs.map(&method(:price_without_taxe)).reduce(:+) || 0) + self.footprint = compute_footprint + end + + def compute_footprint + columns = AccountingPeriod.columns.map(&:name) + .delete_if { |c| %w[id footprint created_at updated_at].include? c } + + Checksum.text("#{columns.map { |c| self[c] }.join}#{previous_period ? previous_period.footprint : ''}") + end +end diff --git a/app/models/event_file.rb b/app/models/event_file.rb index fb6c7e469..34d788366 100644 --- a/app/models/event_file.rb +++ b/app/models/event_file.rb @@ -1,5 +1,8 @@ +# frozen_string_literal: true + +# Event PDF attachements class EventFile < Asset - mount_uploader :attachment, ProjectCaoUploader + mount_uploader :attachment, EventFileUploader validates :attachment, file_size: { maximum: 20.megabytes.to_i } end diff --git a/app/models/history_value.rb b/app/models/history_value.rb index 902acf022..07a7660be 100644 --- a/app/models/history_value.rb +++ b/app/models/history_value.rb @@ -1,4 +1,32 @@ +# frozen_string_literal: true + +require 'checksum' + +# Setting values, kept history of modifications class HistoryValue < ActiveRecord::Base belongs_to :setting belongs_to :user + + def chain_record + self.footprint = compute_footprint + save! + end + + def check_footprint + footprint == compute_footprint + end + + private + + def compute_footprint + max_date = created_at || Time.current + previous = HistoryValue.where('created_at < ?', max_date) + .order('created_at DESC') + .limit(1) + + columns = HistoryValue.columns.map(&:name) + .delete_if { |c| %w[footprint updated_at].include? c } + + Checksum.text("#{columns.map { |c| self[c] }.join}#{previous.first ? previous.first.footprint : ''}") + end end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 90b437100..33aa18b68 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'checksum' + # Invoice correspond to a single purchase made by an user. This purchase may # include reservation(s) and/or a subscription class Invoice < ActiveRecord::Base @@ -15,10 +17,14 @@ class Invoice < ActiveRecord::Base belongs_to :coupon has_one :avoir, class_name: 'Invoice', foreign_key: :invoice_id, dependent: :destroy + belongs_to :operator, foreign_key: :operator_id, class_name: 'User' - after_create :update_reference + before_create :add_environment + after_create :update_reference, :chain_record after_commit :generate_and_send_invoice, on: [:create], if: :persisted? + validates_with ClosedPeriodValidator + def file dir = "invoices/#{user.id}" @@ -211,6 +217,19 @@ class Invoice < ActiveRecord::Base total - (wallet_amount || 0) end + def add_environment + self.environment = Rails.env + end + + def chain_record + self.footprint = compute_footprint + save! + end + + def check_footprint + invoice_items.map(&:check_footprint).all? && footprint == compute_footprint + end + private def generate_and_send_invoice @@ -256,4 +275,16 @@ class Invoice < ActiveRecord::Base Invoice.where('created_at >= :start_date AND created_at < :end_date', start_date: start, end_date: ending).length end + def compute_footprint + max_date = created_at || DateTime.now + previous = Invoice.where('created_at < ?', max_date) + .order('created_at DESC') + .limit(1) + + columns = Invoice.columns.map(&:name) + .delete_if { |c| %w[footprint updated_at].include? c } + + Checksum.text("#{columns.map { |c| self[c] }.join}#{previous.first ? previous.first.footprint : ''}") + end + end diff --git a/app/models/invoice_item.rb b/app/models/invoice_item.rb index 0477fd45b..7967b4347 100644 --- a/app/models/invoice_item.rb +++ b/app/models/invoice_item.rb @@ -1,9 +1,36 @@ # frozen_string_literal: true +require 'checksum' + # A single line inside an invoice. Can be a subscription or a reservation class InvoiceItem < ActiveRecord::Base belongs_to :invoice belongs_to :subscription has_one :invoice_item # to associated invoice_items of an invoice to invoice_items of an avoir + + after_create :chain_record + + def chain_record + self.footprint = compute_footprint + save! + end + + def check_footprint + footprint == compute_footprint + end + + private + + def compute_footprint + max_date = created_at || Time.current + previous = InvoiceItem.where('created_at < ?', max_date) + .order('created_at DESC') + .limit(1) + + columns = InvoiceItem.columns.map(&:name) + .delete_if { |c| %w[footprint updated_at].include? c } + + Checksum.text("#{columns.map { |c| self[c] }.join}#{previous.first ? previous.first.footprint : ''}") + end end diff --git a/app/models/notification_type.rb b/app/models/notification_type.rb index 54503aa56..200cbf65e 100644 --- a/app/models/notification_type.rb +++ b/app/models/notification_type.rb @@ -43,6 +43,7 @@ class NotificationType notify_member_about_coupon notify_member_reservation_reminder notify_admin_free_disk_space + notify_admin_close_period_reminder ] # deprecated: # - notify_member_subscribed_plan_is_changed diff --git a/app/models/reservation.rb b/app/models/reservation.rb index d110c0b22..9de1fe353 100644 --- a/app/models/reservation.rb +++ b/app/models/reservation.rb @@ -224,10 +224,10 @@ class Reservation < ActiveRecord::Base invoice_items end - def save_with_payment(coupon_code = nil) + def save_with_payment(operator_id, coupon_code = nil) begin clean_pending_strip_invoice_items - build_invoice(user: user) + build_invoice(user: user, operator_id: operator_id) invoice_items = generate_invoice_items(false, coupon_code) rescue StandardError => e logger.error e @@ -242,7 +242,7 @@ class Reservation < ActiveRecord::Base if plan_id self.subscription = Subscription.find_or_initialize_by(user_id: user.id) subscription.attributes = { plan_id: plan_id, user_id: user.id, card_token: card_token, expiration_date: nil } - if subscription.save_with_payment(false) + if subscription.save_with_payment(operator_id, false) self.stp_invoice_id = invoice_items.first.refresh.invoice invoice.stp_invoice_id = invoice_items.first.refresh.invoice invoice.invoice_items.push InvoiceItem.new( @@ -368,8 +368,8 @@ class Reservation < ActiveRecord::Base pending_invoice_items.each(&:delete) end - def save_with_local_payment(coupon_code = nil) - build_invoice(user: user) + def save_with_local_payment(operator_id, coupon_code = nil) + build_invoice(user: user, operator_id: operator_id) generate_invoice_items(true, coupon_code) return false unless valid? @@ -377,7 +377,7 @@ class Reservation < ActiveRecord::Base if plan_id self.subscription = Subscription.find_or_initialize_by(user_id: user.id) subscription.attributes = { plan_id: plan_id, user_id: user.id, expiration_date: nil } - if subscription.save_with_local_payment(false) + if subscription.save_with_local_payment(operator_id, false) invoice.invoice_items.push InvoiceItem.new( amount: subscription.plan.amount, description: subscription.plan.name, diff --git a/app/models/subscription.rb b/app/models/subscription.rb index c9f2de0c4..7d17219e7 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -20,7 +20,7 @@ class Subscription < ActiveRecord::Base # Stripe subscription payment # @params [invoice] if true then subscription pay itself, dont pay with reservation # if false then subscription pay with reservation - def save_with_payment(invoice = true, coupon_code = nil) + def save_with_payment(operator_id, invoice = true, coupon_code = nil) return unless valid? begin @@ -75,7 +75,7 @@ class Subscription < ActiveRecord::Base # generate invoice stp_invoice = Stripe::Invoice.all(customer: user.stp_customer_id, limit: 1).data.first if invoice - db_invoice = generate_invoice(stp_invoice.id, coupon_code) + db_invoice = generate_invoice(operator_id, stp_invoice.id, coupon_code) # debit wallet wallet_transaction = debit_user_wallet if wallet_transaction @@ -129,7 +129,7 @@ class Subscription < ActiveRecord::Base # @params [invoice] if true then only the subscription is payed, without reservation # if false then the subscription is payed with reservation - def save_with_local_payment(invoice = true, coupon_code = nil) + def save_with_local_payment(operator_id, invoice = true, coupon_code = nil) return false unless valid? set_expiration_date @@ -142,7 +142,7 @@ class Subscription < ActiveRecord::Base # debit wallet wallet_transaction = debit_user_wallet - invoc = generate_invoice(nil, coupon_code) + invoc = generate_invoice(operator_id, nil, coupon_code) if wallet_transaction invoc.wallet_amount = @wallet_amount_debit invoc.wallet_transaction_id = wallet_transaction.id @@ -152,7 +152,7 @@ class Subscription < ActiveRecord::Base true end - def generate_invoice(stp_invoice_id = nil, coupon_code = nil) + def generate_invoice(operator_id, stp_invoice_id = nil, coupon_code = nil) coupon_id = nil total = plan.amount @@ -165,13 +165,13 @@ class Subscription < ActiveRecord::Base end end - invoice = Invoice.new(invoiced_id: id, invoiced_type: 'Subscription', user: user, total: total, stp_invoice_id: stp_invoice_id, coupon_id: coupon_id) + invoice = Invoice.new(invoiced_id: id, invoiced_type: 'Subscription', user: user, total: total, stp_invoice_id: stp_invoice_id, coupon_id: coupon_id, operator_id: operator_id) invoice.invoice_items.push InvoiceItem.new(amount: plan.amount, stp_invoice_item_id: stp_subscription_id, description: plan.name, subscription_id: self.id) invoice end - def generate_and_save_invoice(stp_invoice_id = nil) - generate_invoice(stp_invoice_id).save + def generate_and_save_invoice(operator_id, stp_invoice_id = nil) + generate_invoice(operator_id, stp_invoice_id).save end def cancel diff --git a/app/models/user.rb b/app/models/user.rb index ca5dd6963..2c6f618e6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -8,7 +8,7 @@ class User < ActiveRecord::Base # Include default devise modules. Others available are: # :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, - :confirmable, :async + :confirmable rolify # enable OmniAuth authentication only if needed @@ -44,6 +44,7 @@ class User < ActiveRecord::Base has_many :machine_credits, through: :users_credits, source: :machine_credit has_many :invoices, dependent: :destroy + has_many :operated_invoices, foreign_key: :operator_id, class_name: 'Invoice', dependent: :nullify has_many :user_tags, dependent: :destroy has_many :tags, through: :user_tags @@ -92,6 +93,12 @@ class User < ActiveRecord::Base User.with_role(:admin) end + def self.superadmin + return unless Rails.application.secrets.superadmin_email.present? + + User.find_by(email: Rails.application.secrets.superadmin_email) + end + def training_machine?(machine) return true if admin? @@ -124,10 +131,10 @@ class User < ActiveRecord::Base my_projects.to_a.concat projects end - def generate_subscription_invoice + def generate_subscription_invoice(operator_id) return unless subscription - subscription.generate_and_save_invoice + subscription.generate_and_save_invoice(operator_id) end def stripe_customer @@ -318,6 +325,10 @@ class User < ActiveRecord::Base create_wallet end + def send_devise_notification(notification, *args) + devise_mailer.send(notification, self, *args).deliver_later + end + def notify_admin_when_user_is_created if need_completion? && !provider.nil? NotificationCenter.call type: 'notify_admin_when_user_is_imported', diff --git a/app/pdfs/data/watermark-en.png b/app/pdfs/data/watermark-en.png new file mode 100644 index 000000000..ed08817f2 Binary files /dev/null and b/app/pdfs/data/watermark-en.png differ diff --git a/app/pdfs/data/watermark-es.png b/app/pdfs/data/watermark-es.png new file mode 100644 index 000000000..239c451c4 Binary files /dev/null and b/app/pdfs/data/watermark-es.png differ diff --git a/app/pdfs/data/watermark-fr.png b/app/pdfs/data/watermark-fr.png new file mode 100644 index 000000000..d6cdec1e6 Binary files /dev/null and b/app/pdfs/data/watermark-fr.png differ diff --git a/app/pdfs/data/watermark-pt.png b/app/pdfs/data/watermark-pt.png new file mode 100644 index 000000000..dbbf88e54 Binary files /dev/null and b/app/pdfs/data/watermark-pt.png differ diff --git a/app/pdfs/data/watermark.xcf b/app/pdfs/data/watermark.xcf new file mode 100644 index 000000000..c5c17e090 Binary files /dev/null and b/app/pdfs/data/watermark.xcf differ diff --git a/app/pdfs/pdf/invoice.rb b/app/pdfs/pdf/invoice.rb index c958e5cb6..3cd030ea2 100644 --- a/app/pdfs/pdf/invoice.rb +++ b/app/pdfs/pdf/invoice.rb @@ -334,6 +334,15 @@ class PDF::Invoice < Prawn::Document text line, align: :right, leading: 4, inline_format: true end end + + # factice watermark + return unless %w[staging test development].include?(invoice.environment) + + transparent(0.1) do + rotate(45, origin: [0, 0]) do + image "#{Rails.root}/app/pdfs/data/watermark-#{I18n.locale}.png", at: [90, 150] + end + end end private diff --git a/app/policies/accounting_period_policy.rb b/app/policies/accounting_period_policy.rb new file mode 100644 index 000000000..158ee8dc8 --- /dev/null +++ b/app/policies/accounting_period_policy.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Check the access policies for API::AccountingPeriodsController +class AccountingPeriodPolicy < ApplicationPolicy + %w[index show create last_closing_end download_archive].each do |action| + define_method "#{action}?" do + user.admin? + end + end +end diff --git a/app/services/accounting_period_service.rb b/app/services/accounting_period_service.rb new file mode 100644 index 000000000..4f37945ab --- /dev/null +++ b/app/services/accounting_period_service.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Provides methods for accessing AccountingPeriods properties +class AccountingPeriodService + + def self.find_last_period + AccountingPeriod.where(end_at: AccountingPeriod.select('max(end_at)')).first + end + + def self.all_periods_with_users + AccountingPeriod.joins("INNER JOIN #{User.arel_table.name} ON users.id = accounting_periods.closed_by + INNER JOIN #{Profile.arel_table.name} ON profiles.user_id = users.id") + .select("#{AccountingPeriod.arel_table.name}.*, + #{Profile.arel_table.name}.first_name, + #{Profile.arel_table.name}.last_name") + .order('start_at DESC') + end +end diff --git a/app/services/members/members_service.rb b/app/services/members/members_service.rb index 669bceab7..51e0d36fd 100644 --- a/app/services/members/members_service.rb +++ b/app/services/members/members_service.rb @@ -31,7 +31,7 @@ class Members::MembersService @member.generate_auth_migration_token if current_user.admin? && AuthProvider.active.providable_type != DatabaseProvider.name if @member.save - @member.generate_subscription_invoice + @member.generate_subscription_invoice(current_user.id) @member.send_confirmation_instructions UsersMailer.delay.notify_user_account_created(@member, @member.password) true diff --git a/app/services/reservations/reserve.rb b/app/services/reservations/reserve.rb index 053c50b66..9cb8de571 100644 --- a/app/services/reservations/reserve.rb +++ b/app/services/reservations/reserve.rb @@ -1,18 +1,19 @@ module Reservations class Reserve - attr_accessor :user_id + attr_accessor :user_id, :operator_id - def initialize(user_id) + def initialize(user_id, operator_id) @user_id = user_id + @operator_id = operator_id end def pay_and_save(reservation, payment_method, coupon) reservation.user_id = user_id if payment_method == :local - reservation.save_with_local_payment(coupon) + reservation.save_with_local_payment(operator_id, coupon) elsif payment_method == :stripe - reservation.save_with_payment(coupon) + reservation.save_with_payment(operator_id, coupon) end end end -end \ No newline at end of file +end diff --git a/app/services/subscriptions/subscribe.rb b/app/services/subscriptions/subscribe.rb index 58d1981dc..3af97c5f7 100644 --- a/app/services/subscriptions/subscribe.rb +++ b/app/services/subscriptions/subscribe.rb @@ -1,17 +1,18 @@ module Subscriptions class Subscribe - attr_accessor :user_id + attr_accessor :user_id, :operator_id - def initialize(user_id) + def initialize(user_id, operator_id) @user_id = user_id + @operator_id = operator_id end def pay_and_save(subscription, payment_method, coupon, invoice) subscription.user_id = user_id if payment_method == :local - subscription.save_with_local_payment(invoice, coupon) + subscription.save_with_local_payment(operator_id, invoice, coupon) elsif payment_method == :stripe - subscription.save_with_payment(invoice, coupon) + subscription.save_with_payment(operator_id, invoice, coupon) end end @@ -24,7 +25,7 @@ module Subscriptions expiration_date: new_expiration_date ) if new_sub.save - new_sub.user.generate_subscription_invoice + new_sub.user.generate_subscription_invoice(operator_id) return new_sub end false diff --git a/app/uploaders/event_file_uploader.rb b/app/uploaders/event_file_uploader.rb new file mode 100644 index 000000000..d794464e1 --- /dev/null +++ b/app/uploaders/event_file_uploader.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# CarrierWave uploader for event attachments +class EventFileUploader < CarrierWave::Uploader::Base + # Include RMagick or MiniMagick support: + # include CarrierWave::RMagick + # include CarrierWave::MiniMagick + include UploadHelper + + # Choose what kind of storage to use for this uploader: + storage :file + after :remove, :delete_empty_dirs + # storage :fog + + # Override the directory where uploaded files will be stored. + # This is a sensible default for uploaders that are meant to be mounted: + def store_dir + "#{base_store_dir}/#{model.id}" + end + + def base_store_dir + "uploads/#{model.class.to_s.underscore}" + end + + # Provide a default URL as a default if there hasn't been a file uploaded: + # def default_url + # # For Rails 3.1+ asset pipeline compatibility: + # # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_')) + # + # "/images/fallback/" + [version_name, "default.png"].compact.join('_') + # end + + # Process files as they are uploaded: + # process :scale => [200, 300] + # + # def scale(width, height) + # # do something + # end + + + # Add a white list of extensions which are allowed to be uploaded. + # For images you might use something like this: + def extension_white_list + %w[pdf] + end + + # Override the filename of the uploaded files: + # Avoid using model.id or version_name here, see uploader/store.rb for details. + # def filename + # # "avatar.#{file.extension}" if original_filename + # end +end diff --git a/app/validators/closed_period_validator.rb b/app/validators/closed_period_validator.rb new file mode 100644 index 000000000..db6ae34e5 --- /dev/null +++ b/app/validators/closed_period_validator.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Validates the current invoice is not generated within a closed accounting period +class ClosedPeriodValidator < ActiveModel::Validator + def validate(record) + date = if record.is_a?(Avoir) + record.avoir_date + else + DateTime.now + end + + + AccountingPeriod.all.each do |period| + record.errors[:date] << I18n.t('errors.messages.in_closed_period') if date >= period.start_at && date <= period.end_at + end + end +end diff --git a/app/validators/date_range_validator.rb b/app/validators/date_range_validator.rb new file mode 100644 index 000000000..fccd59092 --- /dev/null +++ b/app/validators/date_range_validator.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Validates that start_at is same or before end_at in the given record +class DateRangeValidator < ActiveModel::Validator + def validate(record) + the_end = record.end_at + the_start = record.start_at + return if the_end.present? && the_end >= the_start + + record.errors[:end_at] << I18n.t('errors.messages.end_before_start', START: the_start) + end +end diff --git a/app/validators/period_integrity_validator.rb b/app/validators/period_integrity_validator.rb new file mode 100644 index 000000000..8dc8e6f11 --- /dev/null +++ b/app/validators/period_integrity_validator.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Validates that all invoices in the current accounting period are chained with footprints which ensure their integrity +class PeriodIntegrityValidator < ActiveModel::Validator + def validate(record) + the_end = record.end_at + the_start = record.start_at + + invoices = Invoice.where('created_at >= :start_date AND created_at < :end_date', start_date: the_start, end_date: the_end) + .includes(:invoice_items) + + + invoices.each do |i| + record.errors["invoice_#{i.reference}".to_sym] << I18n.t('errors.messages.invalid_footprint') unless i.check_footprint + end + end +end diff --git a/app/validators/period_overlap_validator.rb b/app/validators/period_overlap_validator.rb new file mode 100644 index 000000000..8d718c9a8 --- /dev/null +++ b/app/validators/period_overlap_validator.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Validates the current accounting period does not overlap an existing one +class PeriodOverlapValidator < ActiveModel::Validator + def validate(record) + the_end = record.end_at + the_start = record.start_at + + AccountingPeriod.all.each do |period| + if the_start >= period.start_at && the_start <= period.end_at + record.errors[:start_at] << I18n.t('errors.messages.cannot_overlap') + end + if the_end >= period.start_at && the_end <= period.end_at + record.errors[:end_at] << I18n.t('errors.messages.cannot_overlap') + end + if period.start_at >= the_start && period.end_at <= the_end + record.errors[:end_at] << I18n.t('errors.messages.cannot_encompass') + end + end + end +end diff --git a/app/views/api/accounting_periods/index.json.jbuilder b/app/views/api/accounting_periods/index.json.jbuilder new file mode 100644 index 000000000..3980cc0dd --- /dev/null +++ b/app/views/api/accounting_periods/index.json.jbuilder @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +json.array!(@accounting_periods) do |ap| + json.extract! ap, :id, :start_at, :end_at, :closed_at, :closed_by, :footprint, :created_at + json.period_total ap.period_total / 100.0 + json.perpetual_total ap.perpetual_total / 100.0 + json.chained_footprint ap.check_footprint + json.user_name "#{ap.first_name} #{ap.last_name}" +end diff --git a/app/views/api/accounting_periods/last_closing_end.json.jbuilder b/app/views/api/accounting_periods/last_closing_end.json.jbuilder new file mode 100644 index 000000000..fe8ebedfa --- /dev/null +++ b/app/views/api/accounting_periods/last_closing_end.json.jbuilder @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +json.last_end_date @last_end diff --git a/app/views/api/accounting_periods/show.json.jbuilder b/app/views/api/accounting_periods/show.json.jbuilder new file mode 100644 index 000000000..a66aad1ab --- /dev/null +++ b/app/views/api/accounting_periods/show.json.jbuilder @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +json.extract! @accounting_period, :id, :start_at, :end_at, :closed_at, :closed_by, :created_at diff --git a/app/views/api/auth_providers/active.json.jbuilder b/app/views/api/auth_providers/active.json.jbuilder index fc0a96351..1f35ece38 100644 --- a/app/views/api/auth_providers/active.json.jbuilder +++ b/app/views/api/auth_providers/active.json.jbuilder @@ -4,9 +4,9 @@ json.link_to_sso_profile @provider.link_to_sso_profile if @provider.providable_type == DatabaseProvider.name json.link_to_sso_connect '/#' else - json.link_to_sso_connect user_omniauth_authorize_path(@provider.strategy_name.to_sym) + json.link_to_sso_connect "/users/auth/#{@provider.strategy_name}" end if @provider.providable_type == OAuth2Provider.name json.domain @provider.providable.domain -end \ No newline at end of file +end diff --git a/app/views/api/events/_event.json.jbuilder b/app/views/api/events/_event.json.jbuilder index d3f0499ee..51098ee40 100644 --- a/app/views/api/events/_event.json.jbuilder +++ b/app/views/api/events/_event.json.jbuilder @@ -1,3 +1,5 @@ +# frozen_string_literal: true + json.extract! event, :id, :title, :description, :age_range_id json.event_image event.event_image.attachment_url if event.event_image json.event_files_attributes event.event_files do |f| @@ -6,18 +8,22 @@ json.event_files_attributes event.event_files do |f| json.attachment_url f.attachment_url end json.category_id event.category_id -json.category do - json.id event.category.id - json.name event.category.name -end if event.category +if event.category + json.category do + json.id event.category.id + json.name event.category.name + end +end json.event_theme_ids event.event_theme_ids json.event_themes event.event_themes do |e| json.name e.name end json.age_range_id event.age_range_id -json.age_range do - json.name event.age_range.name -end if event.age_range +if event.age_range + json.age_range do + json.name event.age_range.name + end +end json.start_date event.availability.start_at json.start_time event.availability.start_at json.end_date event.availability.end_at @@ -25,7 +31,7 @@ json.end_time event.availability.end_at json.month t('date.month_names')[event.availability.start_at.month] json.month_id event.availability.start_at.month json.year event.availability.start_at.year -json.all_day event.availability.start_at.hour == 0 ? 'true' : 'false' +json.all_day event.availability.start_at.hour.zero? ? 'true' : 'false' json.availability do json.id event.availability.id json.start_at event.availability.start_at diff --git a/app/views/api/invoices/list.json.jbuilder b/app/views/api/invoices/list.json.jbuilder index 648900267..629139c1d 100644 --- a/app/views/api/invoices/list.json.jbuilder +++ b/app/views/api/invoices/list.json.jbuilder @@ -12,4 +12,5 @@ json.array!(@invoices) do |invoice| json.stripe invoice.stp_invoice_id? json.date invoice.is_a?(Avoir) ? invoice.avoir_date : invoice.created_at json.prevent_refund invoice.prevent_refund? + json.chained_footprint invoice.check_footprint end diff --git a/app/views/api/notifications/_notify_admin_close_period_reminder.json.jbuilder b/app/views/api/notifications/_notify_admin_close_period_reminder.json.jbuilder new file mode 100644 index 000000000..3f8bde945 --- /dev/null +++ b/app/views/api/notifications/_notify_admin_close_period_reminder.json.jbuilder @@ -0,0 +1,7 @@ +json.title notification.notification_type +if notification.attached_object.class.name == AccountingPeriod.name + json.description t('.warning_last_closed_period_over_1_year', LAST_END: notification.attached_object.end_at) +else + json.description t('.warning_no_closed_periods', FIRST_DATE: notification.attached_object.created_at.to_date) +end +json.url notification_url(notification, format: :json) diff --git a/app/views/api/notifications/_notify_admin_free_disk_space.json.jbuilder b/app/views/api/notifications/_notify_admin_free_disk_space.json.jbuilder new file mode 100644 index 000000000..bf275289d --- /dev/null +++ b/app/views/api/notifications/_notify_admin_free_disk_space.json.jbuilder @@ -0,0 +1,3 @@ +json.title notification.notification_type +json.description t('.warning_free_disk_space', AVAILABLE: number_with_delimiter(notification.meta_data['mb_available'])) +json.url notification_url(notification, format: :json) diff --git a/app/views/application/index.html.erb b/app/views/application/index.html.erb index 84703a1ca..b0810ce43 100644 --- a/app/views/application/index.html.erb +++ b/app/views/application/index.html.erb @@ -9,14 +9,9 @@ <%=Setting.find_by(name: 'fablab_name').value%> - <% if ENV['DEFAULT_HOST'] == 'fablab.lacasemate.fr' %> - - - <% else %> - - - - <% end %> + + +
    {{ 'invoices.VAT_rate' }} {{ 'invoices.changed_at' }} {{ 'invoices.changed_by' }}
    {{value.value}} %{{value.created_at | amDateFormat:'L LT'}}
    + {{'invoices.VAT_disabled'}} + {{value.rate}} + {{value.date | amDateFormat:'L LT'}} {{value.user.name}}{{ 'invoices.deleted_user' }}