1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-11 05:54:15 +01:00

Merge branch 'dev' for release 3.0.0

This commit is contained in:
Sylvain 2019-03-28 11:33:17 +01:00
commit d29ae1587b
166 changed files with 10331 additions and 14716 deletions

View File

@ -1,5 +1,5 @@
Metrics/LineLength: Metrics/LineLength:
Max: 130 Max: 140
Metrics/MethodLength: Metrics/MethodLength:
Max: 30 Max: 30
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:

34
3rd-PARTY-LICENSES.md Normal file
View File

@ -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.

View File

@ -1,5 +1,29 @@
# Changelog Fab Manager # 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 ## v2.8.4 2019 March 18
- Limit members search to 50 results to speed up queries - 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 - Fix a bug: user is not redirected after changing is duplicated e-mail on the SSO provider
## v2.1.0 2016 May 2 ## 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 - Add integration tests for main features
- Credits logic has been extracted into a microservice - Credits logic has been extracted into a microservice
- Improved UI list of projects - Improved UI list of projects

View File

@ -13,7 +13,7 @@ patches and features.
## Using the issue tracker ## 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: 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)). * 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 # Navigate to the newly cloned directory
cd fab-manager cd fab-manager
# Assign the original repo to a remote called "upstream" # 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: 2. If you cloned a while ago, get the latest changes from upstream:

View File

@ -4,7 +4,7 @@ MAINTAINER peng@sleede.com
# First we need to be able to fetch from https repositories # First we need to be able to fetch from https repositories
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y apt-transport-https \ apt-get install -y apt-transport-https \
ca-certificates ca-certificates apt-utils
# Add sources for external tools to APT # 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/log
RUN mkdir -p /usr/src/app/public/uploads RUN mkdir -p /usr/src/app/public/uploads
RUN mkdir -p /usr/src/app/public/assets 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/sockets
RUN mkdir -p /usr/src/app/tmp/pids 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
VOLUME /usr/src/app/public/uploads VOLUME /usr/src/app/public/uploads
VOLUME /usr/src/app/public/assets VOLUME /usr/src/app/public/assets
VOLUME /usr/src/app/accounting
VOLUME /var/log/supervisor VOLUME /var/log/supervisor
# Expose port 3000 to the Docker host, so we can access it # Expose port 3000 to the Docker host, so we can access it

17
Gemfile
View File

@ -2,7 +2,7 @@ source 'https://rubygems.org'
gem 'compass-rails', '2.0.4' gem 'compass-rails', '2.0.4'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.2.11' gem 'rails', '4.2.11.1'
# Use SCSS for stylesheets # Use SCSS for stylesheets
gem 'sass-rails', '5.0.1' gem 'sass-rails', '5.0.1'
@ -17,7 +17,7 @@ gem 'jquery-rails'
gem 'jbuilder', '~> 2.5' gem 'jbuilder', '~> 2.5'
gem 'jbuilder_cache_multi' gem 'jbuilder_cache_multi'
# bundle exec rake doc:rails generates the API under doc/api. # 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 'forgery'
gem 'responders', '~> 2.0' gem 'responders', '~> 2.0'
@ -41,16 +41,12 @@ end
group :development do group :development do
gem 'active_record_query_trace' gem 'active_record_query_trace'
gem 'awesome_print' gem 'awesome_print'
gem 'capistrano'
gem 'capistrano-maintenance', '0.0.5', require: false
gem 'capistrano-sidekiq', require: false
gem 'coveralls', require: false gem 'coveralls', require: false
gem 'foreman' gem 'foreman'
# Preview mail in the browser # Preview mail in the browser
gem 'mailcatcher' gem 'mailcatcher'
gem 'puma' gem 'puma'
gem 'rb-readline' gem 'rb-readline'
gem 'rvm-capistrano', require: false
end end
group :test do group :test do
@ -66,15 +62,13 @@ end
group :production do group :production do
gem 'rails_12factor' gem 'rails_12factor'
gem 'unicorn'
end end
gem 'seed_dump' gem 'seed_dump'
gem 'pg' gem 'pg'
gem 'devise' gem 'devise', ">= 4.6.0"
gem 'devise-async'
gem 'omniauth', '~> 1.6.0' gem 'omniauth', '~> 1.6.0'
gem 'omniauth-oauth2' gem 'omniauth-oauth2'
@ -148,3 +142,8 @@ gem 'axlsx_rails'
gem 'rubyzip', '>= 1.2.2' gem 'rubyzip', '>= 1.2.2'
gem 'rack-protection', '1.5.5' gem 'rack-protection', '1.5.5'
# get free disk space
gem 'sys-filesystem'
gem 'sha3'

View File

@ -14,39 +14,39 @@ GEM
specs: specs:
Ascii85 (1.0.2) Ascii85 (1.0.2)
aasm (4.1.0) aasm (4.1.0)
actionmailer (4.2.11) actionmailer (4.2.11.1)
actionpack (= 4.2.11) actionpack (= 4.2.11.1)
actionview (= 4.2.11) actionview (= 4.2.11.1)
activejob (= 4.2.11) activejob (= 4.2.11.1)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
actionpack (4.2.11) actionpack (4.2.11.1)
actionview (= 4.2.11) actionview (= 4.2.11.1)
activesupport (= 4.2.11) activesupport (= 4.2.11.1)
rack (~> 1.6) rack (~> 1.6)
rack-test (~> 0.6.2) rack-test (~> 0.6.2)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionpack-page_caching (1.0.2) actionpack-page_caching (1.0.2)
actionpack (>= 4.0.0, < 5) actionpack (>= 4.0.0, < 5)
actionview (4.2.11) actionview (4.2.11.1)
activesupport (= 4.2.11) activesupport (= 4.2.11.1)
builder (~> 3.1) builder (~> 3.1)
erubis (~> 2.7.0) erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.3) rails-html-sanitizer (~> 1.0, >= 1.0.3)
active_record_query_trace (1.4) active_record_query_trace (1.4)
activejob (4.2.11) activejob (4.2.11.1)
activesupport (= 4.2.11) activesupport (= 4.2.11.1)
globalid (>= 0.3.0) globalid (>= 0.3.0)
activemodel (4.2.11) activemodel (4.2.11.1)
activesupport (= 4.2.11) activesupport (= 4.2.11.1)
builder (~> 3.1) builder (~> 3.1)
activerecord (4.2.11) activerecord (4.2.11.1)
activemodel (= 4.2.11) activemodel (= 4.2.11.1)
activesupport (= 4.2.11) activesupport (= 4.2.11.1)
arel (~> 6.0) arel (~> 6.0)
activesupport (4.2.11) activesupport (4.2.11.1)
i18n (~> 0.7) i18n (~> 0.7)
minitest (~> 5.1) minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4) thread_safe (~> 0.3, >= 0.3.4)
@ -70,7 +70,7 @@ GEM
axlsx_rails (0.4.0) axlsx_rails (0.4.0)
axlsx (>= 2.0.1) axlsx (>= 2.0.1)
rails (>= 3.1) rails (>= 3.1)
bcrypt (3.1.10) bcrypt (3.1.12)
binding_of_caller (0.7.3) binding_of_caller (0.7.3)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootstrap-sass (3.4.1) bootstrap-sass (3.4.1)
@ -80,17 +80,6 @@ GEM
builder (3.2.3) builder (3.2.3)
byebug (8.2.3) byebug (8.2.3)
camertron-eprun (1.1.0) 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) carrierwave (0.10.0)
activemodel (>= 3.2.0) activemodel (>= 3.2.0)
activesupport (>= 3.2.0) activesupport (>= 3.2.0)
@ -119,7 +108,7 @@ GEM
compass (~> 1.0.0) compass (~> 1.0.0)
sass-rails (<= 5.0.1) sass-rails (<= 5.0.1)
sprockets (< 2.13) sprockets (< 2.13)
concurrent-ruby (1.1.4) concurrent-ruby (1.1.5)
connection_pool (2.2.0) connection_pool (2.2.0)
coveralls (0.8.16) coveralls (0.8.16)
json (>= 1.8, < 3) json (>= 1.8, < 3)
@ -135,15 +124,12 @@ GEM
debug_inspector (0.0.3) debug_inspector (0.0.3)
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
devise (3.4.1) devise (4.6.1)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 3.2.6, < 5) railties (>= 4.1.0, < 6.0)
responders responders
thread_safe (~> 0.1)
warden (~> 1.2.3) warden (~> 1.2.3)
devise-async (0.9.0)
devise (~> 3.2)
docile (1.1.5) docile (1.1.5)
domain_name (0.5.25) domain_name (0.5.25)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
@ -185,14 +171,13 @@ GEM
forgery (0.6.0) forgery (0.6.0)
friendly_id (5.1.0) friendly_id (5.1.0)
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
globalid (0.4.1) globalid (0.4.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
has_secure_token (1.0.0) has_secure_token (1.0.0)
activerecord (>= 3.0) activerecord (>= 3.0)
hashdiff (0.3.0) hashdiff (0.3.0)
hashery (2.1.2) hashery (2.1.2)
hashie (3.5.7) hashie (3.5.7)
highline (1.7.1)
hike (1.2.3) hike (1.2.3)
hitimes (1.2.2) hitimes (1.2.2)
htmlentities (4.3.4) htmlentities (4.3.4)
@ -226,7 +211,6 @@ GEM
kaminari (0.16.3) kaminari (0.16.3)
actionpack (>= 3.0.0) actionpack (>= 3.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
kgio (2.9.3)
libv8 (3.16.14.11) libv8 (3.16.14.11)
loofah (2.2.3) loofah (2.2.3)
crass (~> 1.0.2) crass (~> 1.0.2)
@ -249,7 +233,7 @@ GEM
mimemagic (0.3.2) mimemagic (0.3.2)
mini_magick (4.2.0) mini_magick (4.2.0)
mini_mime (1.0.1) mini_mime (1.0.1)
mini_portile2 (2.3.0) mini_portile2 (2.4.0)
minitest (5.11.3) minitest (5.11.3)
minitest-reporters (1.1.8) minitest-reporters (1.1.8)
ansi ansi
@ -260,16 +244,9 @@ GEM
multi_xml (0.5.5) multi_xml (0.5.5)
multipart-post (2.0.0) multipart-post (2.0.0)
naught (1.1.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) netrc (0.10.3)
nokogiri (1.8.5) nokogiri (1.10.1)
mini_portile2 (~> 2.3.0) mini_portile2 (~> 2.4.0)
notify_with (0.0.2) notify_with (0.0.2)
jbuilder (~> 2.0) jbuilder (~> 2.0)
rails (>= 4.2.0) rails (>= 4.2.0)
@ -318,16 +295,16 @@ GEM
rack-test (0.6.3) rack-test (0.6.3)
rack (>= 1.0) rack (>= 1.0)
railroady (1.5.3) railroady (1.5.3)
rails (4.2.11) rails (4.2.11.1)
actionmailer (= 4.2.11) actionmailer (= 4.2.11.1)
actionpack (= 4.2.11) actionpack (= 4.2.11.1)
actionview (= 4.2.11) actionview (= 4.2.11.1)
activejob (= 4.2.11) activejob (= 4.2.11.1)
activemodel (= 4.2.11) activemodel (= 4.2.11.1)
activerecord (= 4.2.11) activerecord (= 4.2.11.1)
activesupport (= 4.2.11) activesupport (= 4.2.11.1)
bundler (>= 1.3.0, < 2.0) bundler (>= 1.3.0, < 2.0)
railties (= 4.2.11) railties (= 4.2.11.1)
sprockets-rails sprockets-rails
rails-deprecated_sanitizer (1.0.3) rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha) activesupport (>= 4.2.0.alpha)
@ -344,13 +321,12 @@ GEM
rails_stdout_logging rails_stdout_logging
rails_serve_static_assets (0.0.4) rails_serve_static_assets (0.0.4)
rails_stdout_logging (0.0.3) rails_stdout_logging (0.0.3)
railties (4.2.11) railties (4.2.11.1)
actionpack (= 4.2.11) actionpack (= 4.2.11.1)
activesupport (= 4.2.11) activesupport (= 4.2.11.1)
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (3.0.0) rainbow (3.0.0)
raindrops (0.13.0)
rake (12.3.2) rake (12.3.2)
rb-fsevent (0.9.4) rb-fsevent (0.9.4)
rb-inotify (0.9.5) rb-inotify (0.9.5)
@ -384,8 +360,6 @@ GEM
rubyzip (1.2.2) rubyzip (1.2.2)
rufus-scheduler (3.0.9) rufus-scheduler (3.0.9)
tzinfo tzinfo
rvm-capistrano (1.5.6)
capistrano (~> 2.15.4)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sass (3.4.13) sass (3.4.13)
sass-rails (5.0.1) sass-rails (5.0.1)
@ -403,6 +377,7 @@ GEM
seed_dump (3.2.2) seed_dump (3.2.2)
activerecord (~> 4) activerecord (~> 4)
activesupport (~> 4) activesupport (~> 4)
sha3 (1.0.1)
sidekiq (3.3.4) sidekiq (3.3.4)
celluloid (>= 0.16.0) celluloid (>= 0.16.0)
connection_pool (>= 2.1.1) connection_pool (>= 2.1.1)
@ -440,6 +415,8 @@ GEM
stripe (1.30.2) stripe (1.30.2)
json (~> 1.8.1) json (~> 1.8.1)
rest-client (~> 1.4) rest-client (~> 1.4)
sys-filesystem (1.2.0)
ffi
term-ansicolor (1.3.2) term-ansicolor (1.3.2)
tins (~> 1.0) tins (~> 1.0)
test_after_commit (1.0.0) test_after_commit (1.0.0)
@ -484,17 +461,13 @@ GEM
unf_ext unf_ext
unf_ext (0.0.6) unf_ext (0.0.6)
unicode-display_width (1.4.0) unicode-display_width (1.4.0)
unicorn (4.8.3)
kgio (~> 2.6)
rack
raindrops (~> 0.7)
vcr (3.0.1) vcr (3.0.1)
virtus (1.0.5) virtus (1.0.5)
axiom-types (~> 0.1) axiom-types (~> 0.1)
coercible (~> 1.0) coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3) descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9) equalizer (~> 0.0, >= 0.0.9)
warden (1.2.3) warden (1.2.7)
rack (>= 1.0) rack (>= 1.0)
web-console (2.1.3) web-console (2.1.3)
activemodel (>= 4.0) activemodel (>= 4.0)
@ -520,16 +493,12 @@ DEPENDENCIES
axlsx_rails axlsx_rails
bootstrap-sass (>= 3.4.1) bootstrap-sass (>= 3.4.1)
byebug byebug
capistrano
capistrano-maintenance (= 0.0.5)
capistrano-sidekiq
carrierwave carrierwave
chroma chroma
compass-rails (= 2.0.4) compass-rails (= 2.0.4)
coveralls coveralls
database_cleaner database_cleaner
devise devise (>= 4.6.0)
devise-async
elasticsearch-model (~> 5) elasticsearch-model (~> 5)
elasticsearch-persistence (~> 5) elasticsearch-persistence (~> 5)
elasticsearch-rails (~> 5) elasticsearch-rails (~> 5)
@ -562,7 +531,7 @@ DEPENDENCIES
pundit pundit
rack-protection (= 1.5.5) rack-protection (= 1.5.5)
railroady railroady
rails (= 4.2.11) rails (= 4.2.11.1)
rails-observers rails-observers
rails_12factor rails_12factor
rb-readline rb-readline
@ -571,24 +540,24 @@ DEPENDENCIES
rolify rolify
rubocop (~> 0.61.1) rubocop (~> 0.61.1)
rubyzip (>= 1.2.2) rubyzip (>= 1.2.2)
rvm-capistrano
sass-rails (= 5.0.1) sass-rails (= 5.0.1)
sdoc (~> 0.4.0) sdoc (~> 0.4.0)
seed_dump seed_dump
sha3
sidekiq sidekiq
sidekiq-cron sidekiq-cron
sinatra sinatra
spring spring
stripe (= 1.30.2) stripe (= 1.30.2)
sys-filesystem
test_after_commit test_after_commit
therubyracer (= 0.12.0) therubyracer (= 0.12.0)
twitter twitter
twitter-text twitter-text
uglifier (>= 4.1.20) uglifier (>= 4.1.20)
unicorn
vcr vcr
web-console (~> 2.1.3) web-console (~> 2.1.3)
webmock webmock
BUNDLED WITH BUNDLED WITH
1.17.2 1.17.3

View File

@ -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 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 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 <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
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 GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007 Version 3, 19 November 2007

View File

@ -1,8 +1,8 @@
# FabManager # 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 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) [![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+ - Ubuntu LTS 14.04+ / Debian 8+
- Ruby 2.3 - Ruby 2.3
- Git 1.9.1+
- Redis 2.8.4+ - Redis 2.8.4+
- Sidekiq 3.3.4+ - Sidekiq 3.3.4+
- Elasticsearch 5.6 - 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 7. Retrieve the project from Git
```bash ```bash
git clone https://github.com/LaCasemate/fab-manager.git git clone https://github.com/sleede/fab-manager.git
``` ```
8. Install the software dependencies. 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 10. Install bundler in the current RVM gemset
```bash ```bash
gem install bundler gem install bundler --version=1.17.3
``` ```
11. Install the required ruby gems and javascript plugins 11. Install the required ruby gems and javascript plugins
@ -205,7 +204,7 @@ environment.
2. Retrieve the project from Git 2. Retrieve the project from Git
```bash ```bash
git clone https://github.com/LaCasemate/fab-manager git clone https://github.com/sleede/fab-manager
``` ```
3. From the project directory, run: 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/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/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/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`. - 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. This also requires your user to have the _SUPERUSER_ role.
Please see the [known issues](#known-issues) section for more information about this. 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 (#). 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. 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.
<a name="i18n-configuration"></a> <a name="i18n-configuration"></a>
### Configuration ### Configuration
@ -467,7 +471,7 @@ After modifying any values concerning the localisation, restart the application
**This configuration is optional.** **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. 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. 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. 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)
<a name="sso"></a> <a name="sso"></a>
## Single Sign-On ## Single Sign-On

View File

@ -17,8 +17,8 @@
/** /**
* Controller used in the admin invoices listing page * Controller used in the admin invoices listing page
*/ */
Application.Controllers.controller('InvoicesController', ['$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, invoices, $uibModal, growl, $filter, Setting, settings, _t) { function ($scope, $state, Invoice, AccountingPeriod, invoices, closedPeriods, $uibModal, growl, $filter, Setting, settings, _t) {
/* PRIVATE STATIC CONSTANTS */ /* PRIVATE STATIC CONSTANTS */
// number of invoices loaded each time we click on 'load more...' // 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" %>', templateUrl: '<%= asset_path "admin/invoices/avoirModal.html" %>',
controller: 'AvoirModalController', controller: 'AvoirModalController',
resolve: { 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 () { active () {
return $scope.invoice.VAT.active; return $scope.invoice.VAT.active;
}, },
history () { rateHistory () {
return Setting.get({ name: 'invoice_VAT-rate', history: true }).$promise; 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.rate = rate;
$scope.isSelected = active; $scope.isSelected = active;
$scope.history = history.setting.history; $scope.history = [];
$scope.ok = function () { $uibModalInstance.close({ rate: $scope.rate, active: $scope.isSelected }); }; $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); 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 */ /* PRIVATE SCOPE */
/** /**
@ -500,8 +550,8 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
/** /**
* Controller used in the invoice refunding modal window * Controller used in the invoice refunding modal window
*/ */
Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModalInstance', 'invoice', 'Invoice', 'growl', '_t', Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModalInstance', 'invoice', 'closedPeriods', 'lastClosingEnd', 'Invoice', 'growl', '_t',
function ($scope, $uibModalInstance, invoice, Invoice, growl, _t) { function ($scope, $uibModalInstance, invoice, closedPeriods, lastClosingEnd, Invoice, growl, _t) {
/* PUBLIC SCOPE */ /* PUBLIC SCOPE */
// invoice linked to the current refund // invoice linked to the current refund
@ -517,6 +567,9 @@ Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModal
invoice_items_ids: [] 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 // Possible refunding methods
$scope.avoirModes = [ $scope.avoirModes = [
{ name: _t('invoices.none'), value: 'none' }, { name: _t('invoices.none'), value: 'none' },
@ -580,6 +633,20 @@ Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModal
*/ */
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; $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 */ /* PRIVATE SCOPE */
/** /**
@ -604,3 +671,115 @@ Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModal
return initialize(); 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`;
}
}
]);

View File

@ -150,7 +150,7 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
}; };
// admins list // admins list
$scope.admins = adminsPromise.admins; $scope.admins = adminsPromise.admins.filter(function(m) { return m.id != Fablab.superadminId; });
// Admins ordering/sorting. Default: not sorted // Admins ordering/sorting. Default: not sorted
$scope.orderAdmin = null; $scope.orderAdmin = null;

View File

@ -340,7 +340,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
var openLoginModal = function (toState, toParams, callback) { var openLoginModal = function (toState, toParams, callback) {
<% active_provider = AuthProvider.active %> <% active_provider = AuthProvider.active %>
<% if active_provider.providable_type != DatabaseProvider.name %> <% 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 %> <% else %>
return $uibModal.open({ return $uibModal.open({
templateUrl: '<%= asset_path "shared/deviseModal.html" %>', templateUrl: '<%= asset_path "shared/deviseModal.html" %>',

View File

@ -885,6 +885,7 @@ angular.module('application.router', ['ui.router'])
query: { number: '', customer: '', date: null, order_by: '-reference', page: 1, size: 20 } query: { number: '', customer: '', date: null, order_by: '-reference', page: 1, size: 20 }
}).$promise; }).$promise;
}], }],
closedPeriods: [ 'AccountingPeriod', function(AccountingPeriod) { return AccountingPeriod.query().$promise; }],
translations: ['Translations', function (Translations) { return Translations.query('app.admin.invoices').$promise; }] translations: ['Translations', function (Translations) { return Translations.query('app.admin.invoices').$promise; }]
} }
}) })

View File

@ -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'
}
}
);
}]);

View File

@ -617,3 +617,7 @@ padding: 10px;
margin-right: 5px; margin-right: 5px;
} }
} }
.help-block.error {
color: #ff565d;
}

View File

@ -33,6 +33,7 @@
@import "app.components"; @import "app.components";
@import "app.plugins"; @import "app.plugins";
@import "modules/invoice"; @import "modules/invoice";
@import "modules/signup";
@import "app.responsive"; @import "app.responsive";

View File

@ -1,6 +1,14 @@
// admin invoices // admin invoices
.chained {
color: green;
}
.broken {
color: red;
}
.invoice-placeholder { .invoice-placeholder {
width: 80%; width: 80%;
max-width: 800px; max-width: 800px;
@ -184,3 +192,79 @@
font-style: italic; font-style: italic;
color: #5a5a5a; 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;
}

View File

@ -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;
}
}

View File

@ -0,0 +1,11 @@
<ul>
<li><span class="period-info-title" translate>{{ 'invoices.closed_at' }}</span> : <span>{{period.closed_at | amDateFormat:'L'}}</span></li>
<li><span class="period-info-title" translate>{{ 'invoices.closed_by' }}</span> : <span>{{period.user_name}}</span></li>
<li><span class="period-info-title" translate>{{ 'invoices.period_total' }}</span> : <span>{{period.period_total | currency}}</span></li>
<li><span class="period-info-title" translate>{{ 'invoices.perpetual_total' }}</span> : <span>{{period.perpetual_total | currency}}</span></li>
<li>
<span class="period-info-title" translate>{{ 'invoices.integrity' }}</span> :
<i class="fa fa-link chained" ng-show="period.chained_footprint"></i>
<i class="fa fa-chain-broken broken" ng-hide="period.chained_footprint"></i>
</li>
</ul>

View File

@ -14,6 +14,7 @@
uib-datepicker-popup="{{datePicker.format}}" uib-datepicker-popup="{{datePicker.format}}"
datepicker-options="datePicker.options" datepicker-options="datePicker.options"
is-open="datePicker.opened" is-open="datePicker.opened"
min-date="lastClosingEnd"
placeholder="{{datePicker.format}}" placeholder="{{datePicker.format}}"
ng-click="openDatePicker($event)" ng-click="openDatePicker($event)"
required/> required/>

View File

@ -0,0 +1,79 @@
<div class="modal-header">
<h3 class="text-center red" translate>{{ 'invoices.close_accounting_period' }}</h3>
</div>
<div class="modal-body">
<form name="closePeriodForm" novalidate="novalidate" class="row">
<div class="form-group col-md-6" ng-class="{'has-error': closePeriodForm.start_at.$dirty && closePeriodForm.start_at.$invalid }">
<label translate>{{ 'invoices.close_from_date' }}</label>
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text"
class="form-control"
name="start_at"
ng-model="period.start_at"
uib-datepicker-popup="{{datePicker.format}}"
datepicker-options="datePicker.options"
is-open="datePicker.startOpened"
min-date="datePicker.minDate"
max-date="datePicker.minDate"
init-date="period.start_at"
placeholder="{{datePicker.format}}"
readonly
required/>
</div>
<span class="help-block" ng-show="closePeriodForm.start_at.$dirty && closePeriodForm.start_at.$error.required" translate>{{ 'invoices.start_date_is_required' }}</span>
<span class="help-block error" ng-show="errors.start_at">{{ errors.start_at[0] }}</span>
</div>
<div class="form-group col-md-6" ng-class="{'has-error': closePeriodForm.end_at.$dirty && closePeriodForm.end_at.$invalid }">
<label translate>{{ 'invoices.close_until_date' }}</label>
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text"
class="form-control"
name="end_at"
ng-model="period.end_at"
uib-datepicker-popup="{{datePicker.format}}"
datepicker-options="datePicker.options"
is-open="datePicker.endOpened"
min-date="datePicker.minDate"
max-date="datePicker.maxDate"
init-date="period.end_at"
placeholder="{{datePicker.format}}"
ng-click="toggleDatePicker($event)"
required/>
</div>
<span class="help-block" ng-show="closePeriodForm.end_at.$dirty && closePeriodForm.end_at.$error.required" translate>{{ 'invoices.end_date_is_required' }}</span>
<span class="help-block error" ng-show="errors.end_at">{{ errors.end_at[0] }}</span>
</div>
</form>
<div ng-repeat="(key, value) in errors" ng-if="invoiceErrorRE.test(key)" class="row col-md-12">
<span class="help-block error">{{ $parent.invoiceErrorRE.exec(key)[1] }} : {{ value[0] }}</span>
</div>
<div>
<h4 translate>{{ 'invoices.previous_closings' }}</h4>
<table class="table closings-table" ng-show="accountingPeriods.length > 0">
<thead>
<tr>
<th translate>{{ 'invoices.start_date' }}</th>
<th translate>{{ 'invoices.end_date' }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="period in accountingPeriods">
<td>{{period.start_at | amDateFormat:'L'}}</td>
<td>{{period.end_at | amDateFormat:'L'}}</td>
<td class="actions">
<span class="show-more" uib-popover-template="'<%= asset_path 'admin/invoices/_period.html' %>'"><i class="fa fa-info-circle"></i></span>
<span class="download-archive" ng-click="downloadArchive(period)"><i class="fa fa-archive"></i></span>
</td>
</tr>
</tbody>
</table>
<div ng-show="accountingPeriods.length === 0" translate>{{ 'invoices.no_periods'}}</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" ng-disabled="closePeriodForm.$invalid || pendingCreation" translate>{{ 'confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" ng-disabled="pendingCreation" translate>{{ 'cancel' }}</button>
</div>

View File

@ -10,7 +10,11 @@
<h1 translate>{{ 'invoices.invoices' }}</h1> <h1 translate>{{ 'invoices.invoices' }}</h1>
</section> </section>
</div> </div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
<section class="heading-actions wrapper">
<a class="btn btn-lg btn-default rounded m-t-sm text-sm" ng-click="closeAnAccountingPeriod()"><i class="fa fa-calendar-check-o"></i> {{ 'invoices.accounting_periods' | translate }}</a>
</section>
</div>
</div> </div>
</section> </section>
@ -57,6 +61,7 @@
<table class="table" ng-if="invoices.length > 0"> <table class="table" ng-if="invoices.length > 0">
<thead> <thead>
<tr> <tr>
<th style="width:5%"></th>
<th style="width:15%"><a href="" ng-click="setOrderInvoice('reference')">{{ 'invoices.invoice_#' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='reference', 'fa fa-sort-numeric-desc': orderInvoice=='-reference', 'fa fa-arrows-v': orderInvoice }"></i></a></th> <th style="width:15%"><a href="" ng-click="setOrderInvoice('reference')">{{ 'invoices.invoice_#' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='reference', 'fa fa-sort-numeric-desc': orderInvoice=='-reference', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
<th style="width:20%"><a href="" ng-click="setOrderInvoice('date')">{{ 'invoices.date' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='date', 'fa fa-sort-numeric-desc': orderInvoice=='-date', 'fa fa-arrows-v': orderInvoice }"></i></a></th> <th style="width:20%"><a href="" ng-click="setOrderInvoice('date')">{{ 'invoices.date' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='date', 'fa fa-sort-numeric-desc': orderInvoice=='-date', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
@ -70,6 +75,10 @@
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="invoice in invoices"> <tr ng-repeat="invoice in invoices">
<td>
<i class="fa fa-link chained" ng-show="invoice.chained_footprint"/>
<i class="fa fa-chain-broken broken" ng-hide="invoice.chained_footprint"/>
</td>
<td>{{ invoice.reference }}</td> <td>{{ invoice.reference }}</td>
<td ng-if="!invoice.is_avoir">{{ invoice.date | amDateFormat:'L LTS' }}</td> <td ng-if="!invoice.is_avoir">{{ invoice.date | amDateFormat:'L LTS' }}</td>
<td ng-if="invoice.is_avoir">{{ invoice.date | amDateFormat:'L' }}</td> <td ng-if="invoice.is_avoir">{{ invoice.date | amDateFormat:'L' }}</td>
@ -83,7 +92,7 @@
<a class="btn btn-default" ng-href="api/invoices/{{invoice.id}}/download" target="_blank" ng-if="invoice.is_avoir"> <a class="btn btn-default" ng-href="api/invoices/{{invoice.id}}/download" target="_blank" ng-if="invoice.is_avoir">
<i class="fa fa-file-pdf-o"></i> {{ 'invoices.download_the_credit_note' | translate }} <i class="fa fa-file-pdf-o"></i> {{ 'invoices.download_the_credit_note' | translate }}
</a> </a>
<a class="btn btn-default" ng-click="generateAvoirForInvoice(invoice)" ng-if="(!invoice.has_avoir || invoice.has_avoir == 'partial') && !invoice.is_avoir && !invoice.prevent_refund"> <a class="btn btn-default" ng-click="generateAvoirForInvoice(invoice)" ng-if="(!invoice.has_avoir || invoice.has_avoir == 'partial') && !invoice.is_avoir && !invoice.prevent_refund && !isDateClosed(invoice.created_at)">
<i class="fa fa-reply"></i> {{ 'invoices.credit_note' | translate }} <i class="fa fa-reply"></i> {{ 'invoices.credit_note' | translate }}
</a> </a>
</div> </div>
@ -387,18 +396,21 @@
<div class="m-t-lg"> <div class="m-t-lg">
<h4 translate>{{ 'invoices.VAT_history' }}</h4> <h4 translate>{{ 'invoices.VAT_history' }}</h4>
<table class="table"> <table class="table scrollable-3-cols">
<head> <thead>
<tr> <tr>
<th translate>{{ 'invoices.VAT_rate' }}</th> <th translate>{{ 'invoices.VAT_rate' }}</th>
<th translate>{{ 'invoices.changed_at' }}</th> <th translate>{{ 'invoices.changed_at' }}</th>
<th translate>{{ 'invoices.changed_by' }}</th> <th translate>{{ 'invoices.changed_by' }}</th>
</tr> </tr>
</head> </thead>
<tbody> <tbody>
<tr ng-repeat="value in history"> <tr ng-repeat="value in history | orderBy:'-date'">
<td>{{value.value}} %</td> <td>
<td>{{value.created_at | amDateFormat:'L LT'}}</td> <span class="no-user-label" ng-show="value.rate === 0" translate>{{'invoices.VAT_disabled'}}</span>
<span ng-hide="value.rate === 0">{{value.rate}}</span>
</td>
<td>{{value.date | amDateFormat:'L LT'}}</td>
<td>{{value.user.name}}<span class="no-user-label" ng-hide="value.user" translate>{{ 'invoices.deleted_user' }}</span></td> <td>{{value.user.name}}<span class="no-user-label" ng-hide="value.user" translate>{{ 'invoices.deleted_user' }}</span></td>
</tr> </tr>
</tbody> </tbody>

View File

@ -53,9 +53,9 @@
<a href="#" class="font-sbold label text-md" ng-click="login($event)"><i class="fa fa-sign-in"></i> {{ 'sign_in' | translate }}</a> <a href="#" class="font-sbold label text-md" ng-click="login($event)"><i class="fa fa-sign-in"></i> {{ 'sign_in' | translate }}</a>
</li> </li>
<% else %> <% else %>
<li ng-if="!isAuthenticated()"><a href="<%= user_omniauth_authorize_path(active_provider.strategy_name.to_sym)%>" class="font-sbold label text-md"><i class="fa fa-rocket"></i> {{ 'sign_up' | translate }}</a></li> <li ng-if="!isAuthenticated()"><a href="<%= "/users/auth/#{active_provider.strategy_name}"%>" class="font-sbold label text-md"><i class="fa fa-rocket"></i> {{ 'sign_up' | translate }}</a></li>
<li ng-if="!isAuthenticated()"> <li ng-if="!isAuthenticated()">
<a href="<%= user_omniauth_authorize_path(active_provider.strategy_name.to_sym)%>" class="font-sbold label text-md"><i class="fa fa-sign-in"></i> {{ 'sign_in' | translate }}</a> <a href="<%= "/users/auth/#{active_provider.strategy_name}"%>" class="font-sbold label text-md"><i class="fa fa-sign-in"></i> {{ 'sign_in' | translate }}</a>
</li> </li>
<% end %> <% end %>
</ul> </ul>

View File

@ -6,7 +6,7 @@
<uib-alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)">{{alert.msg}}</uib-alert> <uib-alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)">{{alert.msg}}</uib-alert>
<div class="well m-b-n"> <div class="well m-b-n">
<form role="form" name="signupForm" class="form-horizontal" novalidate autocomplete="off" ng-keydown="signupForm.$valid && $event.which == 13 && ok()"> <form role="form" name="signupForm" class="form-horizontal signup-form" novalidate autocomplete="off" ng-keydown="signupForm.$valid && $event.which == 13 && ok()">
<div class="form-group" ng-class="{'has-error': signupForm.gender.$dirty && signupForm.gender.$invalid}"> <div class="form-group" ng-class="{'has-error': signupForm.gender.$dirty && signupForm.gender.$invalid}">
<div class="col-sm-12"> <div class="col-sm-12">
<label class="checkbox-inline"> <label class="checkbox-inline">
@ -22,11 +22,12 @@
ng-model="user.profile_attributes.gender" ng-model="user.profile_attributes.gender"
value="false"/> {{ 'woman' | translate }} value="false"/> {{ 'woman' | translate }}
</label> </label>
<span class="exponent m-l-xs"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.gender.$dirty && signupForm.gender.$error.required" translate>{{ 'gender_is_required'}}</span> <span class="help-block" ng-show="signupForm.gender.$dirty && signupForm.gender.$error.required" translate>{{ 'gender_is_required'}}</span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group names-row">
<div class="col-sm-6" ng-class="{'has-error': signupForm.first_name.$dirty && signupForm.first_name.$invalid}"> <div class="col-sm-6" ng-class="{'has-error': signupForm.first_name.$dirty && signupForm.first_name.$invalid}">
<input type="text" <input type="text"
@ -35,6 +36,7 @@
class="form-control" class="form-control"
placeholder="{{ 'your_first_name' | translate }}" placeholder="{{ 'your_first_name' | translate }}"
required> required>
<span class="exponent m-l-xs"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.first_name.$dirty && signupForm.first_name.$error.required" translate>{{ 'first_name_is_required' }}</span> <span class="help-block" ng-show="signupForm.first_name.$dirty && signupForm.first_name.$error.required" translate>{{ 'first_name_is_required' }}</span>
</div> </div>
<div class="m-b visible-xs"></div> <div class="m-b visible-xs"></div>
@ -45,11 +47,12 @@
class="form-control" class="form-control"
placeholder="{{ 'your_surname' | translate }}" placeholder="{{ 'your_surname' | translate }}"
required> required>
<span class="exponent m-l-xs"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.last_name.$dirty && signupForm.last_name.$error.required" translate>{{ 'surname_is_required' }}</span> <span class="help-block" ng-show="signupForm.last_name.$dirty && signupForm.last_name.$error.required" translate>{{ 'surname_is_required' }}</span>
</div> </div>
</div> </div>
<div class="form-group" ng-class="{'has-error': signupForm.username.$dirty && signupForm.username.$invalid}"> <div class="form-group required-row" ng-class="{'has-error': signupForm.username.$dirty && signupForm.username.$invalid}">
<div class="col-sm-12"> <div class="col-sm-12">
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"><i class="fa fa-user"></i></span> <span class="input-group-addon"><i class="fa fa-user"></i></span>
@ -60,11 +63,12 @@
placeholder="{{ 'your_pseudonym' | translate }}" placeholder="{{ 'your_pseudonym' | translate }}"
required> required>
</div> </div>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.username.$dirty && signupForm.username.$error.required" translate>{{ 'pseudonym_is_required' }}</span> <span class="help-block" ng-show="signupForm.username.$dirty && signupForm.username.$error.required" translate>{{ 'pseudonym_is_required' }}</span>
</div> </div>
</div> </div>
<div class="form-group" ng-class="{'has-error': signupForm.email.$dirty && signupForm.email.$invalid}"> <div class="form-group required-row" ng-class="{'has-error': signupForm.email.$dirty && signupForm.email.$invalid}">
<div class="col-sm-12"> <div class="col-sm-12">
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"><i class="fa fa-envelope"></i></span> <span class="input-group-addon"><i class="fa fa-envelope"></i></span>
@ -75,11 +79,12 @@
placeholder="{{ 'your_email_address' | translate }}" placeholder="{{ 'your_email_address' | translate }}"
required> required>
</div> </div>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.email.$dirty && signupForm.email.$error.required" translate>{{ 'email_is_required' }}</span> <span class="help-block" ng-show="signupForm.email.$dirty && signupForm.email.$error.required" translate>{{ 'email_is_required' }}</span>
</div> </div>
</div> </div>
<div class="form-group" ng-class="{'has-error': signupForm.password.$dirty && signupForm.password.$invalid}"> <div class="form-group required-row" ng-class="{'has-error': signupForm.password.$dirty && signupForm.password.$invalid}">
<div class="col-sm-12"> <div class="col-sm-12">
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"><i class="fa fa-key"></i></span> <span class="input-group-addon"><i class="fa fa-key"></i></span>
@ -91,12 +96,13 @@
required required
ng-minlength="8"> ng-minlength="8">
</div> </div>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.password.$dirty && signupForm.password.$error.required" translate>{{ 'password_is_required' }}</span> <span class="help-block" ng-show="signupForm.password.$dirty && signupForm.password.$error.required" translate>{{ 'password_is_required' }}</span>
<span class="help-block" ng-show="signupForm.password.$dirty && signupForm.password.$error.minlength" translate>{{ 'password_is_too_short_(minimum_8_characters)' }}</span> <span class="help-block" ng-show="signupForm.password.$dirty && signupForm.password.$error.minlength" translate>{{ 'password_is_too_short_(minimum_8_characters)' }}</span>
</div> </div>
</div> </div>
<div class="form-group" ng-class="{'has-error': signupForm.password_confirmation.$dirty && signupForm.password_confirmation.$invalid}"> <div class="form-group required-row" ng-class="{'has-error': signupForm.password_confirmation.$dirty && signupForm.password_confirmation.$invalid}">
<div class="col-sm-12"> <div class="col-sm-12">
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"><i class="fa fa-key"></i></span> <span class="input-group-addon"><i class="fa fa-key"></i></span>
@ -108,6 +114,7 @@
required ng-minlength="8" required ng-minlength="8"
match="user.password"> match="user.password">
</div> </div>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.password_confirmation.$dirty && signupForm.password_confirmation.$error.required" translate>{{ 'password_confirmation_is_required' }}</span> <span class="help-block" ng-show="signupForm.password_confirmation.$dirty && signupForm.password_confirmation.$error.required" translate>{{ 'password_confirmation_is_required' }}</span>
<span class="help-block" ng-show="signupForm.password_confirmation.$error.match" translate>{{ 'password_does_not_match_with_confirmation' }}</span> <span class="help-block" ng-show="signupForm.password_confirmation.$error.match" translate>{{ 'password_does_not_match_with_confirmation' }}</span>
</div> </div>
@ -124,7 +131,7 @@
</div> </div>
</div> </div>
<div class="form-group" ng-show="user.organization" ng-class="{'has-error': signupForm.organization_name.$dirty && signupForm.organization_name.$invalid}"> <div class="form-group required-row" ng-show="user.organization" ng-class="{'has-error': signupForm.organization_name.$dirty && signupForm.organization_name.$invalid}">
<div class="col-sm-12"> <div class="col-sm-12">
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"><i class="fa fa-building-o"></i></span> <span class="input-group-addon"><i class="fa fa-building-o"></i></span>
@ -135,11 +142,12 @@
placeholder="{{ 'name_of_your_organization' | translate }}" placeholder="{{ 'name_of_your_organization' | translate }}"
ng-required="user.organization"> ng-required="user.organization">
</div> </div>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.organization_name.$dirty && signupForm.organization_name.$error.required" translate>{{ 'organization_name_is_required' }}</span> <span class="help-block" ng-show="signupForm.organization_name.$dirty && signupForm.organization_name.$error.required" translate>{{ 'organization_name_is_required' }}</span>
</div> </div>
</div> </div>
<div class="form-group" ng-show="user.organization" ng-class="{'has-error': signupForm.organization_address.$dirty && signupForm.organization_address.$invalid}"> <div class="form-group required-row" ng-show="user.organization" ng-class="{'has-error': signupForm.organization_address.$dirty && signupForm.organization_address.$invalid}">
<div class="col-sm-12"> <div class="col-sm-12">
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"><i class="fa fa-map-marker"></i></span> <span class="input-group-addon"><i class="fa fa-map-marker"></i></span>
@ -150,22 +158,24 @@
placeholder="{{ 'address_of_your_organization' | translate }}" placeholder="{{ 'address_of_your_organization' | translate }}"
ng-required="user.organization"> ng-required="user.organization">
</div> </div>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.organization_address.$dirty && signupForm.organization_address.$error.required" translate>{{ 'organization_address_is_required' }}</span> <span class="help-block" ng-show="signupForm.organization_address.$dirty && signupForm.organization_address.$error.required" translate>{{ 'organization_address_is_required' }}</span>
</div> </div>
</div> </div>
<div class="form-group" ng-class="{'has-error': signupForm.group_id.$dirty && signupForm.group_id.$invalid}"> <div class="form-group required-row" ng-class="{'has-error': signupForm.group_id.$dirty && signupForm.group_id.$invalid}">
<div class="col-sm-12"> <div class="col-sm-12">
<div> <div>
<select ng-model="user.group_id" class="form-control" name="group_id" ng-options="g.id as g.name for g in groups" required> <select ng-model="user.group_id" class="form-control" name="group_id" ng-options="g.id as g.name for g in groups" required>
<option value="" translate>{{ 'your_user_s_profile' }}</option> <option value="" translate>{{ 'your_user_s_profile' }}</option>
</select> </select>
<span class="exponent exponent-select"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
</div> </div>
<span class="help-block" ng-show="signupForm.group_id.$dirty && signupForm.group_id.$error.required" translate>{{ 'user_s_profile_is_required' }}</span> <span class="help-block" ng-show="signupForm.group_id.$dirty && signupForm.group_id.$error.required" translate>{{ 'user_s_profile_is_required' }}</span>
</div> </div>
</div> </div>
<div class="form-group" ng-class="{'has-error': signupForm.birthday.$dirty && signupForm.birthday.$invalid}"> <div class="form-group required-row" ng-class="{'has-error': signupForm.birthday.$dirty && signupForm.birthday.$invalid}">
<div class="col-sm-12"> <div class="col-sm-12">
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar-o"></i> </span> <span class="input-group-addon"><i class="fa fa-calendar-o"></i> </span>
@ -180,11 +190,12 @@
ng-click="openDatePicker($event)" ng-click="openDatePicker($event)"
required/> required/>
</div> </div>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.birthday.$dirty && signupForm.birthday.$error.required" translate>{{ 'birth_date_is_required' }}</span> <span class="help-block" ng-show="signupForm.birthday.$dirty && signupForm.birthday.$error.required" translate>{{ 'birth_date_is_required' }}</span>
</div> </div>
</div> </div>
<div class="form-group" ng-class="{'has-error': signupForm.phone.$dirty && signupForm.phone.$invalid}"> <div class="form-group required-row" ng-class="{'has-error': signupForm.phone.$dirty && signupForm.phone.$invalid}">
<div class="col-sm-12"> <div class="col-sm-12">
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"><i class="fa fa-phone"></i> </span> <span class="input-group-addon"><i class="fa fa-phone"></i> </span>
@ -195,6 +206,7 @@
placeholder="{{ 'phone_number' | translate }}" placeholder="{{ 'phone_number' | translate }}"
required> required>
</div> </div>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.phone.$dirty && signupForm.phone.$error.required" translate>{{ 'phone_number_is_required' }}</span> <span class="help-block" ng-show="signupForm.phone.$dirty && signupForm.phone.$error.required" translate>{{ 'phone_number_is_required' }}</span>
</div> </div>
</div> </div>
@ -229,9 +241,16 @@
ng-model="user.cgu" ng-model="user.cgu"
value="true" value="true"
ng-required="cgu != null"/> ng-required="cgu != null"/>
<label for="cgu"><span translate>{{ 'i_ve_read_and_i_accept_' }}</span> <a href="{{cgu.custom_asset_file_attributes.attachment_url}}" target="_blank" translate>{{ '_the_fablab_policy' }}</a></label> <label for="cgu">
<span translate>{{ 'i_ve_read_and_i_accept_' }}</span>
<a href="{{cgu.custom_asset_file_attributes.attachment_url}}" target="_blank" translate>{{ '_the_fablab_policy' }}</a>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></label>
</div> </div>
</div> </div>
<span class="info-required">
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span translate>{{ 'field_required' }}</span>
</span>
<div ng-if="!cgu"> <div ng-if="!cgu">
<input type="hidden" name="cgu" ng-model="user.cgu" value="true"> <input type="hidden" name="cgu" ng-model="user.cgu" value="true">
</div> </div>

View File

@ -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

View File

@ -96,7 +96,7 @@ class API::EventsController < API::ApiController
:recurrence_end_at, :category_id, :event_theme_ids, :age_range_id, :recurrence_end_at, :category_id, :event_theme_ids, :age_range_id,
event_theme_ids: [], event_theme_ids: [],
event_image_attributes: [:attachment], 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]) event_price_categories_attributes: %i[id price_category_id amount _destroy])
EventService.process_params(event_preparams) EventService.process_params(event_preparams)
end end

View File

@ -26,7 +26,7 @@ class API::ReservationsController < API::ApiController
user_id = current_user.admin? ? reservation_params[:user_id] : current_user.id user_id = current_user.admin? ? reservation_params[:user_id] : current_user.id
@reservation = Reservation.new(reservation_params) @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]) .pay_and_save(@reservation, method, coupon_params[:coupon_code])
if is_reserve if is_reserve

View File

@ -19,7 +19,7 @@ class API::SubscriptionsController < API::ApiController
user_id = current_user.admin? ? subscription_params[:user_id] : current_user.id user_id = current_user.admin? ? subscription_params[:user_id] : current_user.id
@subscription = Subscription.new(subscription_params) @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) .pay_and_save(@subscription, method, coupon_params[:coupon_code], true)
if is_subscribe if is_subscribe
@ -35,7 +35,7 @@ class API::SubscriptionsController < API::ApiController
free_days = params[:subscription][:free] || false 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) .extend_subscription(@subscription, subscription_update_params[:expired_at], free_days)
if res.is_a?(Subscription) if res.is_a?(Subscription)
@subscription = res @subscription = res

View File

@ -1,4 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'version'
# API Controller to get the fab-manager version # API Controller to get the fab-manager version
class API::VersionController < API::ApiController class API::VersionController < API::ApiController
@ -6,8 +7,7 @@ class API::VersionController < API::ApiController
def show def show
authorize :version authorize :version
package = File.read('package.json')
version = JSON.parse(package)['version'] render json: { version: Version.current }, status: :ok
render json: { version: version }, status: :ok
end end
end end

View File

@ -30,11 +30,16 @@ class ApplicationController < ActionController::Base
end end
def configure_permitted_parameters def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) << devise_parameter_sanitizer.permit(:sign_up,
{ profile_attributes: [:phone, :last_name, :first_name, :gender, :birthday, :interest, :software_mastered, keys: [
organization_attributes: [:name, address_attributes: [:address]]] } { profile_attributes: [
:phone, :last_name, :first_name, :gender, :birthday,
devise_parameter_sanitizer.for(:sign_up).concat %i[username is_allow_contact is_allow_newsletter cgu group_id] :interest, :software_mastered, organization_attributes: [
:name, address_attributes: [:address]
]
] },
:username, :is_allow_contact, :is_allow_newsletter, :cgu, :group_id
])
end end
def default_url_options def default_url_options

View File

@ -4,7 +4,7 @@ class SessionsController < Devise::SessionsController
def new def new
active_provider = AuthProvider.active active_provider = AuthProvider.active
if active_provider.providable_type != DatabaseProvider.name 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 else
super super
end end

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true
# app/concerns/controllers/api_doc.rb # app/concerns/controllers/api_doc.rb
# #
# Controller extension with common API documentation shortcuts # Controller extension with common API documentation shortcuts
# #
module OpenAPI::ApiDoc module OpenAPI::ApiDoc
# Apipie doesn't allow to append anything to esisting # Apipie doesn't allow to append anything to esisting
# description. It raises an error on double definition. # description. It raises an error on double definition.

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# app/docs/application_doc.rb # app/docs/application_doc.rb
# #
# A common class for defining API docs # A common class for defining API docs
@ -16,7 +18,6 @@
# end # end
# end # end
# #
class OpenAPI::ApplicationDoc class OpenAPI::ApplicationDoc
extend OpenAPI::ApiDoc extend OpenAPI::ApiDoc

View File

@ -1,5 +1,8 @@
# frozen_string_literal: true
# parent class for openAPI documentation
class OpenAPI::V1::BaseDoc < OpenAPI::ApplicationDoc class OpenAPI::V1::BaseDoc < OpenAPI::ApplicationDoc
API_VERSION = "v1" API_VERSION = 'v1'
FORMATS = ['json'] FORMATS = ['json'].freeze
PER_PAGE_DEFAULT = 20 PER_PAGE_DEFAULT = 20
end end

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
# openAPI documentation for bookable machines endpoint
class OpenAPI::V1::BookableMachinesDoc < OpenAPI::V1::BaseDoc class OpenAPI::V1::BookableMachinesDoc < OpenAPI::V1::BaseDoc
resource_description do resource_description do
short 'Bookable machines' short 'Bookable machines'
@ -7,10 +10,10 @@ class OpenAPI::V1::BookableMachinesDoc < OpenAPI::V1::BaseDoc
end end
doc_for :index do doc_for :index do
api :GET, "/#{API_VERSION}/bookable_machines", "Bookable machines index" api :GET, "/#{API_VERSION}/bookable_machines", 'Bookable machines index'
description "Machines that a given user is allowed to book." description 'Machines that a given user is allowed to book.'
param :user_id, Integer, required: true, desc: "Id of the given user." param :user_id, Integer, required: true, desc: 'Id of the given user.'
example <<-EOS example <<-MACHINES
# /open_api/v1/bookable_machines?user_id=522 # /open_api/v1/bookable_machines?user_id=522
{ {
"machines": [ "machines": [
@ -67,6 +70,6 @@ class OpenAPI::V1::BookableMachinesDoc < OpenAPI::V1::BaseDoc
# ... # ...
] ]
} }
EOS MACHINES
end end
end end

View File

@ -1,30 +1,13 @@
# frozen_string_literal: true
# openAPI pagination
module OpenAPI::V1::Concerns::ParamGroups module OpenAPI::V1::Concerns::ParamGroups
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
define_param_group :pagination 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 param :per_page, Integer, desc: "Number of objects per page. Default is #{OpenAPI::V1::BaseDoc::PER_PAGE_DEFAULT}.", optional: true
end 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
end end

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
# openAPI documentation for events endpoint
class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc
resource_description do resource_description do
short 'Events' short 'Events'
@ -9,19 +12,19 @@ class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc
include OpenAPI::V1::Concerns::ParamGroups include OpenAPI::V1::Concerns::ParamGroups
doc_for :index do doc_for :index do
api :GET, "/#{API_VERSION}/events", "Events index" api :GET, "/#{API_VERSION}/events", 'Events index'
param_group :pagination param_group :pagination
param :id, [Integer, Array], optional: true, desc: "Scope the request to one or various events." 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." param :upcoming, [FalseClass, TrueClass], optional: true, desc: 'Scope for the upcoming events.'
description "Events index. Order by *created_at* desc." description 'Events index. Order by *created_at* desc.'
example <<-EOS example <<-EVENTS
# /open_api/v1/events?page=1&per_page=2 # /open_api/v1/events?page=1&per_page=2
{ {
"events": [ "events": [
{ {
"id": 183, "id": 183,
"title": "OPEN LAB", "title": "OPEN LAB",
"description": "Que vous soyez Fab user, visiteur, curieux ou bricoleur, latelier 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, latelier 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", "updated_at": "2016-04-25T10:49:40.055+02:00",
"created_at": "2016-04-25T10:49:40.055+02:00", "created_at": "2016-04-25T10:49:40.055+02:00",
"nb_total_places": 18, "nb_total_places": 18,
@ -54,6 +57,6 @@ class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc
} }
] ]
} }
EOS EVENTS
end end
end end

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
# openAPI documentation for invoices endpoints
class OpenAPI::V1::InvoicesDoc < OpenAPI::V1::BaseDoc class OpenAPI::V1::InvoicesDoc < OpenAPI::V1::BaseDoc
resource_description do resource_description do
short 'Invoices' short 'Invoices'
@ -9,11 +12,11 @@ class OpenAPI::V1::InvoicesDoc < OpenAPI::V1::BaseDoc
include OpenAPI::V1::Concerns::ParamGroups include OpenAPI::V1::Concerns::ParamGroups
doc_for :index do 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." description "Index of users' invoices, with optional pagination. Order by *created_at* descendant."
param_group :pagination param_group :pagination
param :user_id, [Integer, Array], optional: true, desc: "Scope the request to one or various users." param :user_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various users.'
example <<-EOS example <<-INVOICES
# /open_api/v1/invoices?user_id=211&page=1&per_page=3 # /open_api/v1/invoices?user_id=211&page=1&per_page=3
{ {
"invoices": [ "invoices": [
@ -64,15 +67,15 @@ class OpenAPI::V1::InvoicesDoc < OpenAPI::V1::BaseDoc
} }
] ]
} }
EOS INVOICES
end end
doc_for :download do doc_for :download do
api :GET, "/#{API_VERSION}/invoices/:id/download", "Download an invoice" api :GET, "/#{API_VERSION}/invoices/:id/download", 'Download an invoice'
param :id, Integer, desc: "Invoice id", required: true param :id, Integer, desc: 'Invoice id', required: true
example <<-EOS example <<-URL
# /open_api/v1/invoices/2809/download # /open_api/v1/invoices/2809/download
EOS URL
end end
end end

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
# openAPI documentation for machines endpoint
class OpenAPI::V1::MachinesDoc < OpenAPI::V1::BaseDoc class OpenAPI::V1::MachinesDoc < OpenAPI::V1::BaseDoc
resource_description do resource_description do
short 'Machines' short 'Machines'
@ -7,9 +10,9 @@ class OpenAPI::V1::MachinesDoc < OpenAPI::V1::BaseDoc
end end
doc_for :index do doc_for :index do
api :GET, "/#{API_VERSION}/machines", "Machines index" api :GET, "/#{API_VERSION}/machines", 'Machines index'
description "Machines index. Order by *created_at* ascendant." description 'Machines index. Order by *created_at* ascendant.'
example <<-EOS example <<-MACHINES
# /open_api/v1/machines # /open_api/v1/machines
{ {
"machines": [ "machines": [
@ -78,6 +81,6 @@ class OpenAPI::V1::MachinesDoc < OpenAPI::V1::BaseDoc
} }
] ]
} }
EOS MACHINES
end end
end end

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
# openAPI documentation for reservations endpoint
class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc
resource_description do resource_description do
short 'Reservations' short 'Reservations'
@ -9,14 +12,14 @@ class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc
include OpenAPI::V1::Concerns::ParamGroups include OpenAPI::V1::Concerns::ParamGroups
doc_for :index do doc_for :index do
api :GET, "/#{API_VERSION}/reservations", "Reservations index" api :GET, "/#{API_VERSION}/reservations", 'Reservations index'
description "Index of reservations made by users, with optional pagination. Order by *created_at* descendant." description 'Index of reservations made by users, with optional pagination. Order by *created_at* descendant.'
param_group :pagination param_group :pagination
param :user_id, [Integer, Array], optional: true, desc: "Scope the request to one or various users." 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_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." 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 # /open_api/v1/reservations?reservable_type=Event&page=1&per_page=3
{ {
"reservations": [ "reservations": [
@ -85,6 +88,6 @@ class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc
} }
] ]
} }
EOS RESERVATIONS
end end
end end

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
# openAPI documentation for trainings endpoint
class OpenAPI::V1::TrainingsDoc < OpenAPI::V1::BaseDoc class OpenAPI::V1::TrainingsDoc < OpenAPI::V1::BaseDoc
resource_description do resource_description do
short 'Trainings' short 'Trainings'
@ -7,9 +10,9 @@ class OpenAPI::V1::TrainingsDoc < OpenAPI::V1::BaseDoc
end end
doc_for :index do doc_for :index do
api :GET, "/#{API_VERSION}/trainings", "Trainings index" api :GET, "/#{API_VERSION}/trainings", 'Trainings index'
description "Trainings index. Order by *created_at* ascendant." description 'Trainings index. Order by *created_at* ascendant.'
example <<-EOS example <<-TRAININGS
# /open_api/v1/trainings # /open_api/v1/trainings
{ {
"trainings": [ "trainings": [
@ -75,6 +78,6 @@ class OpenAPI::V1::TrainingsDoc < OpenAPI::V1::BaseDoc
} }
] ]
} }
EOS TRAININGS
end end
end end

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
# openAPI documentation for user's trainings endpoint
class OpenAPI::V1::UserTrainingsDoc < OpenAPI::V1::BaseDoc class OpenAPI::V1::UserTrainingsDoc < OpenAPI::V1::BaseDoc
resource_description do resource_description do
short 'User trainings' short 'User trainings'
@ -9,12 +12,12 @@ class OpenAPI::V1::UserTrainingsDoc < OpenAPI::V1::BaseDoc
include OpenAPI::V1::Concerns::ParamGroups include OpenAPI::V1::Concerns::ParamGroups
doc_for :index do doc_for :index do
api :GET, "/#{API_VERSION}/user_trainings", "User trainings index" api :GET, "/#{API_VERSION}/user_trainings", 'User trainings index'
description "Index of trainings accomplished by users, with optional pagination. Order by *created_at* descendant." description 'Index of trainings accomplished by users, with optional pagination. Order by *created_at* descendant.'
param_group :pagination param_group :pagination
param :training_id, [Integer, Array], optional: true, desc: "Scope the request to one or various trainings." 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." param :user_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various users.'
example <<-EOS example <<-TRAININGS
# /open_api/v1/user_trainings?training_id[]=3&training_id[]=4&page=1&per_page=2 # /open_api/v1/user_trainings?training_id[]=3&training_id[]=4&page=1&per_page=2
{ {
"user_trainings": [ "user_trainings": [
@ -94,6 +97,6 @@ class OpenAPI::V1::UserTrainingsDoc < OpenAPI::V1::BaseDoc
} }
] ]
} }
EOS TRAININGS
end end
end end

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
# openAPI documentation for user endpoint
class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc
resource_description do resource_description do
short 'Users' short 'Users'
@ -9,12 +12,12 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc
include OpenAPI::V1::Concerns::ParamGroups include OpenAPI::V1::Concerns::ParamGroups
doc_for :index do doc_for :index do
api :GET, "/#{API_VERSION}/users", "Users index" api :GET, "/#{API_VERSION}/users", 'Users index'
description "Users index, with optional pagination. Order by *created_at* descendant." description 'Users index, with optional pagination. Order by *created_at* descendant.'
param_group :pagination param_group :pagination
param :email, [String, Array], optional: true, desc: "Filter users by *email* using strict matching." 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." param :user_id, [Integer, Array], optional: true, desc: 'Filter users by *id* using strict matching.'
example <<-EOS example <<-USERS
# /open_api/v1/users?page=1&per_page=4 # /open_api/v1/users?page=1&per_page=4
{ {
"users": [ "users": [
@ -92,6 +95,6 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc
} }
] ]
} }
EOS USERS
end end
end end

View File

@ -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

View File

@ -1,5 +1,8 @@
# frozen_string_literal: true
# Event PDF attachements
class EventFile < Asset class EventFile < Asset
mount_uploader :attachment, ProjectCaoUploader mount_uploader :attachment, EventFileUploader
validates :attachment, file_size: { maximum: 20.megabytes.to_i } validates :attachment, file_size: { maximum: 20.megabytes.to_i }
end end

View File

@ -1,4 +1,32 @@
# frozen_string_literal: true
require 'checksum'
# Setting values, kept history of modifications
class HistoryValue < ActiveRecord::Base class HistoryValue < ActiveRecord::Base
belongs_to :setting belongs_to :setting
belongs_to :user 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 end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'checksum'
# Invoice correspond to a single purchase made by an user. This purchase may # Invoice correspond to a single purchase made by an user. This purchase may
# include reservation(s) and/or a subscription # include reservation(s) and/or a subscription
class Invoice < ActiveRecord::Base class Invoice < ActiveRecord::Base
@ -15,10 +17,14 @@ class Invoice < ActiveRecord::Base
belongs_to :coupon belongs_to :coupon
has_one :avoir, class_name: 'Invoice', foreign_key: :invoice_id, dependent: :destroy 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? after_commit :generate_and_send_invoice, on: [:create], if: :persisted?
validates_with ClosedPeriodValidator
def file def file
dir = "invoices/#{user.id}" dir = "invoices/#{user.id}"
@ -211,6 +217,19 @@ class Invoice < ActiveRecord::Base
total - (wallet_amount || 0) total - (wallet_amount || 0)
end 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 private
def generate_and_send_invoice 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 Invoice.where('created_at >= :start_date AND created_at < :end_date', start_date: start, end_date: ending).length
end 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 end

View File

@ -1,9 +1,36 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'checksum'
# A single line inside an invoice. Can be a subscription or a reservation # A single line inside an invoice. Can be a subscription or a reservation
class InvoiceItem < ActiveRecord::Base class InvoiceItem < ActiveRecord::Base
belongs_to :invoice belongs_to :invoice
belongs_to :subscription belongs_to :subscription
has_one :invoice_item # to associated invoice_items of an invoice to invoice_items of an avoir 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 end

View File

@ -43,6 +43,7 @@ class NotificationType
notify_member_about_coupon notify_member_about_coupon
notify_member_reservation_reminder notify_member_reservation_reminder
notify_admin_free_disk_space notify_admin_free_disk_space
notify_admin_close_period_reminder
] ]
# deprecated: # deprecated:
# - notify_member_subscribed_plan_is_changed # - notify_member_subscribed_plan_is_changed

View File

@ -224,10 +224,10 @@ class Reservation < ActiveRecord::Base
invoice_items invoice_items
end end
def save_with_payment(coupon_code = nil) def save_with_payment(operator_id, coupon_code = nil)
begin begin
clean_pending_strip_invoice_items 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) invoice_items = generate_invoice_items(false, coupon_code)
rescue StandardError => e rescue StandardError => e
logger.error e logger.error e
@ -242,7 +242,7 @@ class Reservation < ActiveRecord::Base
if plan_id if plan_id
self.subscription = Subscription.find_or_initialize_by(user_id: user.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 } 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 self.stp_invoice_id = invoice_items.first.refresh.invoice
invoice.stp_invoice_id = invoice_items.first.refresh.invoice invoice.stp_invoice_id = invoice_items.first.refresh.invoice
invoice.invoice_items.push InvoiceItem.new( invoice.invoice_items.push InvoiceItem.new(
@ -368,8 +368,8 @@ class Reservation < ActiveRecord::Base
pending_invoice_items.each(&:delete) pending_invoice_items.each(&:delete)
end end
def save_with_local_payment(coupon_code = nil) def save_with_local_payment(operator_id, coupon_code = nil)
build_invoice(user: user) build_invoice(user: user, operator_id: operator_id)
generate_invoice_items(true, coupon_code) generate_invoice_items(true, coupon_code)
return false unless valid? return false unless valid?
@ -377,7 +377,7 @@ class Reservation < ActiveRecord::Base
if plan_id if plan_id
self.subscription = Subscription.find_or_initialize_by(user_id: user.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 } 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( invoice.invoice_items.push InvoiceItem.new(
amount: subscription.plan.amount, amount: subscription.plan.amount,
description: subscription.plan.name, description: subscription.plan.name,

View File

@ -20,7 +20,7 @@ class Subscription < ActiveRecord::Base
# Stripe subscription payment # Stripe subscription payment
# @params [invoice] if true then subscription pay itself, dont pay with reservation # @params [invoice] if true then subscription pay itself, dont pay with reservation
# if false then subscription 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? return unless valid?
begin begin
@ -75,7 +75,7 @@ class Subscription < ActiveRecord::Base
# generate invoice # generate invoice
stp_invoice = Stripe::Invoice.all(customer: user.stp_customer_id, limit: 1).data.first stp_invoice = Stripe::Invoice.all(customer: user.stp_customer_id, limit: 1).data.first
if invoice if invoice
db_invoice = generate_invoice(stp_invoice.id, coupon_code) db_invoice = generate_invoice(operator_id, stp_invoice.id, coupon_code)
# debit wallet # debit wallet
wallet_transaction = debit_user_wallet wallet_transaction = debit_user_wallet
if wallet_transaction if wallet_transaction
@ -129,7 +129,7 @@ class Subscription < ActiveRecord::Base
# @params [invoice] if true then only the subscription is payed, without reservation # @params [invoice] if true then only the subscription is payed, without reservation
# if false then the subscription is payed with 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? return false unless valid?
set_expiration_date set_expiration_date
@ -142,7 +142,7 @@ class Subscription < ActiveRecord::Base
# debit wallet # debit wallet
wallet_transaction = debit_user_wallet wallet_transaction = debit_user_wallet
invoc = generate_invoice(nil, coupon_code) invoc = generate_invoice(operator_id, nil, coupon_code)
if wallet_transaction if wallet_transaction
invoc.wallet_amount = @wallet_amount_debit invoc.wallet_amount = @wallet_amount_debit
invoc.wallet_transaction_id = wallet_transaction.id invoc.wallet_transaction_id = wallet_transaction.id
@ -152,7 +152,7 @@ class Subscription < ActiveRecord::Base
true true
end 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 coupon_id = nil
total = plan.amount total = plan.amount
@ -165,13 +165,13 @@ class Subscription < ActiveRecord::Base
end end
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.invoice_items.push InvoiceItem.new(amount: plan.amount, stp_invoice_item_id: stp_subscription_id, description: plan.name, subscription_id: self.id)
invoice invoice
end end
def generate_and_save_invoice(stp_invoice_id = nil) def generate_and_save_invoice(operator_id, stp_invoice_id = nil)
generate_invoice(stp_invoice_id).save generate_invoice(operator_id, stp_invoice_id).save
end end
def cancel def cancel

View File

@ -8,7 +8,7 @@ class User < ActiveRecord::Base
# Include default devise modules. Others available are: # Include default devise modules. Others available are:
# :lockable, :timeoutable and :omniauthable # :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable,
:confirmable, :async :confirmable
rolify rolify
# enable OmniAuth authentication only if needed # 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 :machine_credits, through: :users_credits, source: :machine_credit
has_many :invoices, dependent: :destroy 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 :user_tags, dependent: :destroy
has_many :tags, through: :user_tags has_many :tags, through: :user_tags
@ -92,6 +93,12 @@ class User < ActiveRecord::Base
User.with_role(:admin) User.with_role(:admin)
end 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) def training_machine?(machine)
return true if admin? return true if admin?
@ -124,10 +131,10 @@ class User < ActiveRecord::Base
my_projects.to_a.concat projects my_projects.to_a.concat projects
end end
def generate_subscription_invoice def generate_subscription_invoice(operator_id)
return unless subscription return unless subscription
subscription.generate_and_save_invoice subscription.generate_and_save_invoice(operator_id)
end end
def stripe_customer def stripe_customer
@ -318,6 +325,10 @@ class User < ActiveRecord::Base
create_wallet create_wallet
end end
def send_devise_notification(notification, *args)
devise_mailer.send(notification, self, *args).deliver_later
end
def notify_admin_when_user_is_created def notify_admin_when_user_is_created
if need_completion? && !provider.nil? if need_completion? && !provider.nil?
NotificationCenter.call type: 'notify_admin_when_user_is_imported', NotificationCenter.call type: 'notify_admin_when_user_is_imported',

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
app/pdfs/data/watermark.xcf Normal file

Binary file not shown.

View File

@ -334,6 +334,15 @@ class PDF::Invoice < Prawn::Document
text line, align: :right, leading: 4, inline_format: true text line, align: :right, leading: 4, inline_format: true
end end
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 end
private private

View File

@ -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

View File

@ -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

View File

@ -31,7 +31,7 @@ class Members::MembersService
@member.generate_auth_migration_token if current_user.admin? && AuthProvider.active.providable_type != DatabaseProvider.name @member.generate_auth_migration_token if current_user.admin? && AuthProvider.active.providable_type != DatabaseProvider.name
if @member.save if @member.save
@member.generate_subscription_invoice @member.generate_subscription_invoice(current_user.id)
@member.send_confirmation_instructions @member.send_confirmation_instructions
UsersMailer.delay.notify_user_account_created(@member, @member.password) UsersMailer.delay.notify_user_account_created(@member, @member.password)
true true

View File

@ -1,17 +1,18 @@
module Reservations module Reservations
class Reserve 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 @user_id = user_id
@operator_id = operator_id
end end
def pay_and_save(reservation, payment_method, coupon) def pay_and_save(reservation, payment_method, coupon)
reservation.user_id = user_id reservation.user_id = user_id
if payment_method == :local if payment_method == :local
reservation.save_with_local_payment(coupon) reservation.save_with_local_payment(operator_id, coupon)
elsif payment_method == :stripe elsif payment_method == :stripe
reservation.save_with_payment(coupon) reservation.save_with_payment(operator_id, coupon)
end end
end end
end end

View File

@ -1,17 +1,18 @@
module Subscriptions module Subscriptions
class Subscribe 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 @user_id = user_id
@operator_id = operator_id
end end
def pay_and_save(subscription, payment_method, coupon, invoice) def pay_and_save(subscription, payment_method, coupon, invoice)
subscription.user_id = user_id subscription.user_id = user_id
if payment_method == :local if payment_method == :local
subscription.save_with_local_payment(invoice, coupon) subscription.save_with_local_payment(operator_id, invoice, coupon)
elsif payment_method == :stripe elsif payment_method == :stripe
subscription.save_with_payment(invoice, coupon) subscription.save_with_payment(operator_id, invoice, coupon)
end end
end end
@ -24,7 +25,7 @@ module Subscriptions
expiration_date: new_expiration_date expiration_date: new_expiration_date
) )
if new_sub.save if new_sub.save
new_sub.user.generate_subscription_invoice new_sub.user.generate_subscription_invoice(operator_id)
return new_sub return new_sub
end end
false false

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
# frozen_string_literal: true
json.last_end_date @last_end

View File

@ -0,0 +1,3 @@
# frozen_string_literal: true
json.extract! @accounting_period, :id, :start_at, :end_at, :closed_at, :closed_by, :created_at

View File

@ -4,7 +4,7 @@ json.link_to_sso_profile @provider.link_to_sso_profile
if @provider.providable_type == DatabaseProvider.name if @provider.providable_type == DatabaseProvider.name
json.link_to_sso_connect '/#' json.link_to_sso_connect '/#'
else 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 end
if @provider.providable_type == OAuth2Provider.name if @provider.providable_type == OAuth2Provider.name

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
json.extract! event, :id, :title, :description, :age_range_id json.extract! event, :id, :title, :description, :age_range_id
json.event_image event.event_image.attachment_url if event.event_image json.event_image event.event_image.attachment_url if event.event_image
json.event_files_attributes event.event_files do |f| 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 json.attachment_url f.attachment_url
end end
json.category_id event.category_id json.category_id event.category_id
json.category do if event.category
json.category do
json.id event.category.id json.id event.category.id
json.name event.category.name json.name event.category.name
end if event.category end
end
json.event_theme_ids event.event_theme_ids json.event_theme_ids event.event_theme_ids
json.event_themes event.event_themes do |e| json.event_themes event.event_themes do |e|
json.name e.name json.name e.name
end end
json.age_range_id event.age_range_id json.age_range_id event.age_range_id
json.age_range do if event.age_range
json.age_range do
json.name event.age_range.name json.name event.age_range.name
end if event.age_range end
end
json.start_date event.availability.start_at json.start_date event.availability.start_at
json.start_time event.availability.start_at json.start_time event.availability.start_at
json.end_date event.availability.end_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 t('date.month_names')[event.availability.start_at.month]
json.month_id event.availability.start_at.month json.month_id event.availability.start_at.month
json.year event.availability.start_at.year 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.availability do
json.id event.availability.id json.id event.availability.id
json.start_at event.availability.start_at json.start_at event.availability.start_at

View File

@ -12,4 +12,5 @@ json.array!(@invoices) do |invoice|
json.stripe invoice.stp_invoice_id? json.stripe invoice.stp_invoice_id?
json.date invoice.is_a?(Avoir) ? invoice.avoir_date : invoice.created_at json.date invoice.is_a?(Avoir) ? invoice.avoir_date : invoice.created_at
json.prevent_refund invoice.prevent_refund? json.prevent_refund invoice.prevent_refund?
json.chained_footprint invoice.check_footprint
end end

View File

@ -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)

View File

@ -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)

View File

@ -9,14 +9,9 @@
<title><%=Setting.find_by(name: 'fablab_name').value%></title> <title><%=Setting.find_by(name: 'fablab_name').value%></title>
<% if ENV['DEFAULT_HOST'] == 'fablab.lacasemate.fr' %>
<script type="text/javascript" src="//use.typekit.net/rih5zfr.js"></script>
<script type="text/javascript">try{Typekit.load();}catch(e){}</script>
<% else %>
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,400italic,600,600italic,700,800,700italic' rel='stylesheet' type='text/css'> <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,400italic,600,600italic,700,800,700italic' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300,700,300italic' rel='stylesheet' type='text/css'> <link href='https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300,700,300italic' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Loved+by+the+King' rel='stylesheet' type='text/css'> <link href='https://fonts.googleapis.com/css?family=Loved+by+the+King' rel='stylesheet' type='text/css'>
<% end %>
<script type="text/javascript" src="https://js.stripe.com/v2/"></script> <script type="text/javascript" src="https://js.stripe.com/v2/"></script>
<script type="text/javascript"> <script type="text/javascript">
Stripe.setPublishableKey('<%= Rails.application.secrets.stripe_publishable_key %>'); Stripe.setPublishableKey('<%= Rails.application.secrets.stripe_publishable_key %>');
@ -28,6 +23,7 @@
Fablab.disqusShortname = "<%= Rails.application.secrets.disqus_shortname %>"; Fablab.disqusShortname = "<%= Rails.application.secrets.disqus_shortname %>";
Fablab.defaultHost = "<%= Rails.application.secrets.default_host %>"; Fablab.defaultHost = "<%= Rails.application.secrets.default_host %>";
Fablab.gaId = "<%= Rails.application.secrets.google_analytics_id %>"; Fablab.gaId = "<%= Rails.application.secrets.google_analytics_id %>";
Fablab.superadminId = parseInt("<%= User.superadmin&.id %>", 10);
// i18n stuff // i18n stuff
Fablab.locale = "<%= Rails.application.secrets.app_locale %>"; Fablab.locale = "<%= Rails.application.secrets.app_locale %>";

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
json.invoices do
json.array!(invoices) do |invoice|
json.extract! invoice[:invoice], :id, :stp_invoice_id, :created_at, :reference, :footprint
json.total number_to_currency(invoice[:invoice].total / 100.0)
json.invoiced do
json.type invoice[:invoice].invoiced_type
json.id invoice[:invoice].invoiced_id
if invoice[:invoice].invoiced_type == Subscription.name
json.partial! 'archive/subscription', invoiced: invoice[:invoice].invoiced
elsif invoice[:invoice].invoiced_type == Reservation.name
json.partial! 'archive/reservation', invoiced: invoice[:invoice].invoiced, vat_rate: invoice[:vat_rate]
end
end
json.user do
json.extract! invoice[:invoice].user, :id, :email, :created_at
json.profile do
json.extract! invoice[:invoice].user.profile, :id, :first_name, :last_name, :birthday, :phone
json.gender invoice[:invoice].user.profile.gender ? 'male' : 'female'
end
end
json.invoice_items invoice[:invoice].invoice_items do |item|
json.extract! item, :id, :stp_invoice_item_id, :created_at, :description, :footprint
json.partial! 'archive/vat', price: item.amount, vat_rate: invoice[:vat_rate]
end
end
end
json.totals do
json.period_total number_to_currency(period_total / 100.0)
json.perpetual_total number_to_currency(perpetual_total / 100.0)
end
json.software do
json.name 'Fab-Manager'
json.version software_version
json.code_checksum code_checksum
end
json.previous_archive do
json.filename previous_file
json.checksum last_archive_checksum
end
json.period_footprint period_footprint
json.archive_date date

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
json.extract! invoiced, :created_at, :stp_invoice_id
json.reservable do
json.type invoiced.reservable_type
json.id invoiced.reservable_id
if [Training.name, Machine.name, Space.name].include?(invoiced.reservable_type) && !invoiced.reservable.nil?
json.extract! invoiced.reservable, :name, :created_at
elsif invoiced.reservable_type == Event.name && !invoiced.reservable.nil?
json.extract! invoiced.reservable, :title, :created_at
json.prices do
json.standard_price do
json.partial! 'archive/vat', price: invoiced.reservable.amount, vat_rate: vat_rate
end
json.other_prices invoiced.reservable.event_price_categories do |price|
json.partial! 'archive/vat', price: price.amount, vat_rate: vat_rate
json.price_category do
json.extract! price.price_category, :id, :name, :created_at
end
end
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
json.extract! invoiced, :stp_subscription_id, :created_at, :expiration_date, :canceled_at
json.plan do
json.extract! invoiced.plan, :id, :base_name, :interval, :interval_count, :stp_plan_id, :is_rolling
json.group do
json.extract! invoiced.plan.group, :id, :name
end
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
json.amount do
json.without_tax number_to_currency((price - (price * vat_rate)) / 100.0)
json.all_taxes_included number_to_currency(price / 100.0)
json.vat_rate vat_rate.positive? ? number_to_percentage(vat_rate * 100) : 'none'
end

View File

@ -0,0 +1,8 @@
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
<% if @attached_object.class.name == AccountingPeriod.name %>
<p><%= t('.body.warning_last_closed_period_over_1_year', LAST_END: @attached_object.end_at) %></p>
<% else %>
<p><%= t('.body.warning_no_closed_periods', FIRST_DATE: @attached_object.created_at.to_date) %></p>
<% end %>

View File

@ -0,0 +1,4 @@
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
<p><%= t('.body', THRESHOLD: number_with_delimiter(@notification.get_meta_data(:threshold)), AVAILABLE: number_with_delimiter(@notification.get_meta_data(:mb_available))) %></p>

View File

@ -15,7 +15,7 @@
<% active_provider = AuthProvider.active %> <% active_provider = AuthProvider.active %>
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %> <%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
<% <%
url_path = user_omniauth_authorize_path(active_provider.strategy_name.to_sym) url_path = "/users/auth/#{active_provider.strategy_name}"
if url_path[0] == '/' and root_url[-1] == '/' if url_path[0] == '/' and root_url[-1] == '/'
url_path = root_url + url_path[1..-1] url_path = root_url + url_path[1..-1]
else else

View File

@ -38,7 +38,7 @@
<p> <p>
<%= t('.body.thanks_to_') %> <%= t('.body.thanks_to_') %>
<a href="<%= root_url+user_omniauth_authorize_path(active_provider.strategy_name.to_sym)%>?auth_token=<%= @user.auth_token %>" target="_blank"> <a href="<%= "#{root_url}/users/auth/#{active_provider.strategy_name}?auth_token=#{@user.auth_token}"%>" target="_blank">
<%= t('body.logon_or_login', PROVIDER: active_provider.name )%> <%= t('body.logon_or_login', PROVIDER: active_provider.name )%>
</a> </a>
</p> </p>

View File

@ -0,0 +1,12 @@
class ClosePeriodReminderWorker
include Sidekiq::Worker
def perform
last_period = AccountingPeriod.order(closed_at: :desc).limit(1).last
return if Invoice.count == 0 || (last_period && last_period.end_at > (Time.current - 1.year))
NotificationCenter.call type: 'notify_admin_close_period_reminder',
receiver: User.admins,
attached_object: last_period || Invoice.order(:created_at).first
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
# Periodically check if the free disk space available on the host is above the configured limit, otherwise trigger an email alert
class FreeDiskSpaceWorker
include Sidekiq::Worker
def perform
require 'sys/filesystem'
stat = Sys::Filesystem.stat('.')
mb_available = stat.block_size * stat.blocks_available / 1024 / 1024
return if mb_available > Rails.application.secrets.disk_space_mb_alert
NotificationCenter.call type: 'notify_admin_free_disk_space',
receiver: User.superadmin || User.admins,
attached_object: Role.first,
meta_data: {
mb_available: mb_available,
threshold: Rails.application.secrets.disk_space_mb_alert
}
end
end

View File

@ -61,6 +61,8 @@ OPENLAB_APP_ID:
OPENLAB_BASE_URI: 'https://openprojects.fab-manager.com' OPENLAB_BASE_URI: 'https://openprojects.fab-manager.com'
LOG_LEVEL: 'debug' LOG_LEVEL: 'debug'
DISK_SPACE_MB_ALERT: '100'
SUPERADMIN_EMAIL: 'admin@sleede.com'
ALLOWED_EXTENSIONS: pdf ai eps cad math svg stl dxf dwg obj step iges igs 3dm 3dmf doc docx png ino scad fcad skp sldprt sldasm slddrw slddrt tex latex ps ALLOWED_EXTENSIONS: pdf ai eps cad math svg stl dxf dwg obj step iges igs 3dm 3dmf doc docx png ino scad fcad skp sldprt sldasm slddrw slddrt tex latex ps
ALLOWED_MIME_TYPES: application/pdf application/postscript application/illustrator image/x-eps image/svg+xml application/sla application/dxf application/acad application/dwg application/octet-stream application/step application/iges model/iges x-world/x-3dmf application/vnd.openxmlformats-officedocument.wordprocessingml.document image/png text/x-arduino text/plain application/scad application/vnd.sketchup.skp application/x-koan application/vnd-koan koan/x-skm application/vnd.koan application/x-tex application/x-latex ALLOWED_MIME_TYPES: application/pdf application/postscript application/illustrator image/x-eps image/svg+xml application/sla application/dxf application/acad application/dwg application/octet-stream application/step application/iges model/iges x-world/x-3dmf application/vnd.openxmlformats-officedocument.wordprocessingml.document image/png text/x-arduino text/plain application/scad application/vnd.sketchup.skp application/x-koan application/vnd-koan koan/x-skm application/vnd.koan application/x-tex application/x-latex

View File

@ -1,121 +0,0 @@
require "bundler/capistrano"
require "rvm/capistrano"
require 'capistrano/ext/multistage'
require 'capistrano/maintenance'
set :stages, %w(production staging)
set :default_stage, "staging"
default_run_options[:pty] = true
ssh_options[:forward_agent] = true
after "deploy", "deploy:cleanup" # keep only the last 5 releases
# after "deploy:finalize_update", "deploy:assets:precompile"
namespace :deploy do
%w[start stop restart].each do |command|
desc "#{command} unicorn server"
task command, roles: :app, except: {no_release: true} do
run "/etc/init.d/unicorn_#{application} #{command}"
end
end
desc 'Symlink bootstrap glyphicons'
task :symlink, :roles => :web, :except => { :no_release => true } do
#run "rm -R #{shared_path}/assets/bootstrap/glyphicons-halflings-regular-*"
run "ln -nfs #{shared_path}/assets/bootstrap/glyphicons-halflings-regular-*.ttf #{shared_path}/assets/bootstrap/glyphicons-halflings-regular.ttf"
run "ln -nfs #{shared_path}/assets/bootstrap/glyphicons-halflings-regular-*.svg #{shared_path}/assets/bootstrap/glyphicons-halflings-regular.svg"
run "ln -nfs #{shared_path}/assets/bootstrap/glyphicons-halflings-regular-*.woff #{shared_path}/assets/bootstrap/glyphicons-halflings-regular.woff"
run "ln -nfs #{shared_path}/assets/bootstrap/glyphicons-halflings-regular-*.woff2 #{shared_path}/assets/bootstrap/glyphicons-halflings-regular.woff2"
run "ln -nfs #{shared_path}/assets/bootstrap/glyphicons-halflings-regular-*.eot #{shared_path}/assets/bootstrap/glyphicons-halflings-regular.eot"
#run "rm -R #{shared_path}/assets/select2/select2*"
run "ln -nfs #{shared_path}/assets/select2/select2-*.png #{shared_path}/assets/select2.png"
run "ln -nfs #{shared_path}/assets/select2/select2x2-*.png #{shared_path}/assets/select2x2.png"
run "ln -nfs #{shared_path}/assets/select2/select2-spinner-*.gif #{shared_path}/assets/select2-spinner.gif"
end
task :setup_config, roles: :app do
sudo "ln -nfs #{current_path}/config/nginx.conf /etc/nginx/sites-enabled/#{application}"
sudo "ln -nfs #{current_path}/config/unicorn_init.sh /etc/init.d/unicorn_#{application}"
run "mkdir -p #{shared_path}/config"
run "mkdir -p #{shared_path}/uploads"
run "mkdir -p #{shared_path}/invoices"
run "mkdir -p #{shared_path}/exports"
run "mkdir -p #{shared_path}/plugins"
put File.read("config/database.yml"), "#{shared_path}/config/database.yml"
puts "Now edit #{shared_path}/config/database.yml and add your username and password"
put File.read("config/application.yml"), "#{shared_path}/config/application.yml"
puts "Now edit #{shared_path}/config/application.yml and add your ENV vars"
end
after "deploy:setup", "deploy:setup_config"
task :symlink_config, roles: :app do
run "ln -nfs #{shared_path}/config/database.yml #{release_path}/config/database.yml"
run "rm -rf #{release_path}/config/application.yml"
run "ln -nfs #{shared_path}/config/application.yml #{release_path}/config/application.yml"
end
after "deploy:finalize_update", "deploy:symlink_config"
desc "Make sure local git is in sync with remote."
task :check_revision, roles: :web do
unless `git rev-parse HEAD` == `git rev-parse origin/master`
puts "WARNING: HEAD is not the same as origin/master"
puts "Run `git push` to sync changes."
exit
end
end
#before "deploy", "deploy:check_revision"
desc "load seed to bd"
task :load_seed, :roles => :app do
run "cd #{current_path} && bundle exec rake db:seed RAILS_ENV=production"
end
desc "Rake db:migrate"
task :db_migrate, :roles => :app do
run "cd #{current_path} && bundle exec rake db:migrate RAILS_ENV=production"
end
after "deploy:create_symlink", "deploy:db_migrate"
desc "Symlinks the uploads dir"
task :symlink_uploads_dir, :roles => :app do
run "rm -rf #{release_path}/public/uploads"
run "ln -nfs #{shared_path}/uploads/ #{release_path}/public/"
end
after "deploy:finalize_update", 'deploy:symlink_uploads_dir'
desc "Symlinks the invoices dir"
task :symlink_invoices_dir, :roles => :app do
run "rm -rf #{release_path}/invoices"
run "ln -nfs #{shared_path}/invoices/ #{release_path}/"
end
after "deploy:finalize_update", 'deploy:symlink_invoices_dir'
desc "Symlinks the exports dir"
task :symlink_exports_dir, :roles => :app do
run "rm -rf #{release_path}/exports"
run "ln -nfs #{shared_path}/exports/ #{release_path}/"
end
after "deploy:finalize_update", 'deploy:symlink_exports_dir'
desc "Symlinks the plugins dir"
task :symlink_plugins_dir, :roles => :app do
run "rm -rf #{release_path}/plugins"
run "ln -nfs #{shared_path}/plugins/ #{release_path}/"
end
after "deploy:finalize_update", 'deploy:symlink_plugins_dir'
namespace :assets do
desc 'Run the precompile task locally and rsync with shared'
task :precompile, :roles => :web, :except => { :no_release => true } do
%x{bundle exec rake assets:precompile RAILS_ENV=production}
%x{rsync --recursive --times --rsh='ssh -p#{port}' --compress --human-readable --progress public/assets #{user}@#{domain}:#{shared_path}}
%x{bundle exec rake assets:clean}
end
end
end

View File

@ -1,16 +0,0 @@
server "fablab.lacasemate.fr", :web, :app, :db, primary: true
set :domain, "fablab.lacasemate.fr"
set :application, "fablab"
set :user, "sleede"
set :port, 22
set :deploy_to, "/home/#{user}/apps/#{application}"
set :deploy_via, :remote_cache
set :use_sudo, false
set :scm, "git"
set :repository, "git@git.sleede.com:clients/fablab.git"
set :scm_user, "jarod022"
set :branch, "master"
set :rails_env, 'production'

View File

@ -1,66 +0,0 @@
server "test.fab-manager.com", :web, :app, :db, primary: true
set :application, "fablab_staging"
set :user, "admin"
set :port, 22
set :deploy_to, "/home/#{user}/apps/#{application}"
set :deploy_via, :remote_cache
set :use_sudo, false
set :scm, "git"
set :repository, "git@git.sleede.com:clients/fablab.git"
set :scm_user, "jarod022"
set :branch, "dev"
set :rails_env, 'staging'
namespace :deploy do
%w[start stop restart].each do |command|
desc "#{command} unicorn server"
task command, roles: :app, except: {no_release: true} do
run "/etc/init.d/unicorn_#{application} #{command}"
end
end
task :setup_config, roles: :app do
sudo "ln -nfs #{current_path}/config/nginx_staging.conf /etc/nginx/sites-enabled/#{application}"
sudo "ln -nfs #{current_path}/config/unicorn_init_staging.sh /etc/init.d/unicorn_#{application}"
run "mkdir -p #{shared_path}/config"
run "mkdir -p #{shared_path}/uploads"
put File.read("config/database.yml"), "#{shared_path}/config/database.yml"
puts "Now edit #{shared_path}/config/database.yml and add your username and password"
put File.read("config/application.yml"), "#{shared_path}/config/application.yml"
puts "Now edit #{shared_path}/config/application.yml and add your ENV vars"
end
after "deploy:setup", "deploy:setup_config"
task :symlink_robots, roles: :app do
run "rm -rf #{release_path}/public/robots.txt"
run "ln -nfs #{shared_path}/robots.txt #{release_path}/public/robots.txt"
end
after "deploy:finalize_update", "deploy:symlink_robots"
desc "Rake db:migrate"
task :db_migrate, :roles => :app do
run "cd #{current_path} && bundle exec rake db:migrate RAILS_ENV=staging"
end
after "deploy:create_symlink", "deploy:db_migrate"
namespace :assets do
desc 'Run the precompile task locally and rsync with shared'
task :precompile, :only => { :primary => true } do
%x{bundle exec rake assets:precompile RAILS_ENV=staging}
servers = find_servers :roles => [:app], :except => { :no_release => true }
servers.each do |server|
%x{rsync --recursive --times --rsh='ssh -p#{port}' --compress --human-readable --progress public/assets #{user}@#{server}:#{shared_path}}
end
%x{bundle exec rake assets:clean}
end
end
end

View File

@ -24,8 +24,8 @@ Rails.application.configure do
# Compress JavaScripts and CSS. # Compress JavaScripts and CSS.
config.assets.js_compressor = :uglifier config.assets.js_compressor = :uglifier
# if you want disable variable name mangling # if you want disable variable name mangling and enable ES6 support
config.assets.js_compressor = Uglifier.new(mangle: false) config.assets.js_compressor = Uglifier.new(mangle: false, harmony: true)
# config.assets.css_compressor = :sass # config.assets.css_compressor = :sass

View File

@ -1,5 +0,0 @@
Devise::Async.setup do |config|
config.enabled = true
config.backend = :sidekiq
config.queue = :devise_mailer
end

View File

@ -285,6 +285,7 @@ en:
invoices: invoices:
# list of all invoices & invoicing parameters # list of all invoices & invoicing parameters
invoices: "Invoices" invoices: "Invoices"
accounting_periods: "Accounting periods"
invoices_list: "Invoices list" invoices_list: "Invoices list"
filter_invoices: "Filter invoices" filter_invoices: "Filter invoices"
invoice_#_: "Invoice #:" invoice_#_: "Invoice #:"
@ -406,6 +407,24 @@ en:
logo_successfully_saved: "Logo successfully saved." logo_successfully_saved: "Logo successfully saved."
an_error_occurred_while_saving_the_logo: "An error occurred while saving the logo." an_error_occurred_while_saving_the_logo: "An error occurred while saving the logo."
online_payment: "Online payment" online_payment: "Online payment"
close_accounting_period: "Close an accounting period"
close_from_date: "Close from"
start_date_is_required: "Start date is required"
close_until_date: "Close until"
end_date_is_required: "End date is required"
previous_closings: "Previous closings"
start_date: "From"
end_date: "To"
closed_at: "Closed at"
closed_by: "By"
period_total: "Period total"
perpetual_total: "Perpetual total"
integrity: "Integrity check"
confirmation_required: "Confirmation required"
confirm_close_START_END: "Do you really want to close the accounting period between {{START}} and {{END}}? Any subsequent changes will be impossible. This operation will take some time to complete"
period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed"
failed_to_close_period: "An error occurred, unable to close the accounting period"
no_periods: "No closings for now"
members: members:
# management of users, labels, groups, and so on # management of users, labels, groups, and so on

View File

@ -285,6 +285,7 @@ es:
invoices: invoices:
# list of all invoices & invoicing parameters # list of all invoices & invoicing parameters
invoices: "Facturas" invoices: "Facturas"
accounting_periods: "Accounting periods" # missing translation
invoices_list: "Lista de facturas" invoices_list: "Lista de facturas"
filter_invoices: "Filtrar facturas" filter_invoices: "Filtrar facturas"
invoice_#_: "Factura #:" invoice_#_: "Factura #:"
@ -406,6 +407,24 @@ es:
logo_successfully_saved: "Logo guardado correctamente." logo_successfully_saved: "Logo guardado correctamente."
an_error_occurred_while_saving_the_logo: "Se ha producido un error al guardar el logotipo.." an_error_occurred_while_saving_the_logo: "Se ha producido un error al guardar el logotipo.."
online_payment: "Pago online" online_payment: "Pago online"
close_accounting_period: "Close an accounting period" # translation_missing
close_from_date: "Close from" # translation_missing
start_date_is_required: "Start date is required" # translation_missing
close_until_date: "Close until" # translation_missing
end_date_is_required: "End date is required" # translation_missing
previous_closings: "Previous closings" # translation_missing
start_date: "From" # translation_missing
end_date: "To" # translation_missing
closed_at: "Closed at" # translation_missing
closed_by: "By" # translation_missing
period_total: "Period total" # translation_missing
perpetual_total: "Perpetual total" # translation_missing
integrity: "Verificación de integridad"
confirmation_required: "Confirmation required" # translation_missing
confirm_close_START_END: "Do you really want to close the accounting period between {{START}} and {{END}}? Any subsequent changes will be impossible. This operation will take some time to complete" # translation_missing
period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed" # translation_missing
failed_to_close_period: "An error occurred, unable to close the accounting period" # translation_missing
no_periods: "No closings for now" # translation_missing
members: members:
# management of users, labels, groups, and so on # management of users, labels, groups, and so on

View File

@ -285,6 +285,7 @@ fr:
invoices: invoices:
# liste de toutes les factures & paramètres de facturation # liste de toutes les factures & paramètres de facturation
invoices: "Factures" invoices: "Factures"
accounting_periods: "Périodes comptables"
invoices_list: "Liste des factures" invoices_list: "Liste des factures"
filter_invoices: "Filtrer les factures" filter_invoices: "Filtrer les factures"
invoice_#_: "Facture n° :" invoice_#_: "Facture n° :"
@ -406,6 +407,24 @@ fr:
logo_successfully_saved: "Le logo bien été enregistré." logo_successfully_saved: "Le logo bien été enregistré."
an_error_occurred_while_saving_the_logo: "Une erreur est survenue lors de l'enregistrement du logo." an_error_occurred_while_saving_the_logo: "Une erreur est survenue lors de l'enregistrement du logo."
online_payment: "Paiement en ligne" online_payment: "Paiement en ligne"
close_accounting_period: "Clôturer une période comptable"
close_from_date: "Clôturer depuis"
start_date_is_required: "La date de début est requise"
close_until_date: "Clôturer jusqu'au"
end_date_is_required: "La date de fin est requise"
previous_closings: "Fermetures précédentes"
start_date: "Du"
end_date: "Au"
closed_at: "Clôturé le"
closed_by: "Par"
period_total: "Total de la période"
perpetual_total: "Total perpétuel"
integrity: "Contrôle d'intégrité"
confirmation_required: "Confirmation requise"
confirm_close_START_END: "Êtes-vous sur de vouloir clôturer la période comptable du {{START}} au {{END}} ? Toute modification ultérieure sera impossible. Cette opération va prendre un certain temps."
period_START_END_closed_success: "La période comptable du {{START}} au {{END}} a bien été clôturée"
failed_to_close_period: "Une erreur est survenue, impossible de clôturer la période comptable"
no_periods: "Aucune clôture pour le moment"
members: members:
# gestion des utilisateurs, des groupes, des étiquettes, etc. # gestion des utilisateurs, des groupes, des étiquettes, etc.

View File

@ -285,6 +285,7 @@ pt:
invoices: invoices:
# list of all invoices & invoicing parameters # list of all invoices & invoicing parameters
invoices: "Faturas" invoices: "Faturas"
accounting_periods: "Accounting periods" # missing translation
invoices_list: "Lista de faturas" invoices_list: "Lista de faturas"
filter_invoices: "Filtrar faturas" filter_invoices: "Filtrar faturas"
invoice_#_: "Fatura #:" invoice_#_: "Fatura #:"
@ -406,6 +407,24 @@ pt:
logo_successfully_saved: "Logo salvo com sucesso." logo_successfully_saved: "Logo salvo com sucesso."
an_error_occurred_while_saving_the_logo: "Um erro ocorreu ao salvar o logo." an_error_occurred_while_saving_the_logo: "Um erro ocorreu ao salvar o logo."
online_payment: "Pagamento Online" online_payment: "Pagamento Online"
close_accounting_period: "Close an accounting period" # translation_missing
close_from_date: "Close from" # translation_missing
start_date_is_required: "Start date is required" # translation_missing
close_until_date: "Close until" # translation_missing
end_date_is_required: "End date is required" # translation_missing
previous_closings: "Previous closings" # translation_missing
start_date: "From" # translation_missing
end_date: "To" # translation_missing
closed_at: "Closed at" # translation_missing
closed_by: "By" # translation_missing
period_total: "Period total" # translation_missing
perpetual_total: "Perpetual total" # translation_missing
integrity: "Verificação de integridade"
confirmation_required: "Confirmation required" # translation_missing
confirm_close_START_END: "Do you really want to close the accounting period between {{START}} and {{END}}? Any subsequent changes will be impossible. This operation will take some time to complete." # translation_missing
period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed" # translation_missing
failed_to_close_period: "An error occurred, unable to close the accounting period" # translation_missing
no_periods: "No closings for now" # translation_missing
members: members:
# management of users, labels, groups, and so on # management of users, labels, groups, and so on

View File

@ -85,6 +85,7 @@ en:
i_accept_to_receive_information_from_the_fablab: "I accept to receive information from the FabLab" i_accept_to_receive_information_from_the_fablab: "I accept to receive information from the FabLab"
i_ve_read_and_i_accept_: "I've read and I accept" i_ve_read_and_i_accept_: "I've read and I accept"
_the_fablab_policy: "the FabLab policy" _the_fablab_policy: "the FabLab policy"
field_required: "Field required"
# password modification modal # password modification modal
change_your_password: "Change your password" change_your_password: "Change your password"

View File

@ -84,6 +84,7 @@ es:
i_accept_to_receive_information_from_the_fablab: "Acepto recibir información del FabLab" i_accept_to_receive_information_from_the_fablab: "Acepto recibir información del FabLab"
i_ve_read_and_i_accept_: "He leido y acepto" i_ve_read_and_i_accept_: "He leido y acepto"
_the_fablab_policy: "la política de FabLab" _the_fablab_policy: "la política de FabLab"
field_required: "Field required" #translation_missing
# password modification modal # password modification modal
change_your_password: "Cambiar contraseña" change_your_password: "Cambiar contraseña"

View File

@ -85,6 +85,7 @@ fr:
i_accept_to_receive_information_from_the_fablab: "J'accepte de recevoir des informations du Fab Lab" i_accept_to_receive_information_from_the_fablab: "J'accepte de recevoir des informations du Fab Lab"
i_ve_read_and_i_accept_: "J'ai lu et j'accepte" i_ve_read_and_i_accept_: "J'ai lu et j'accepte"
_the_fablab_policy: "la charte d'utilisation du Fab Lab" _the_fablab_policy: "la charte d'utilisation du Fab Lab"
field_required: "Champ requis"
# fenêtre de changement de mot de passe # fenêtre de changement de mot de passe
change_your_password: "Modifier votre mot de passe" change_your_password: "Modifier votre mot de passe"

Some files were not shown because too many files have changed in this diff Show More