mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-29 18:52:22 +01:00
Merge branch 'dev' for release 3.0.0
This commit is contained in:
commit
d29ae1587b
2
.gitignore
vendored
2
.gitignore
vendored
@ -34,7 +34,7 @@
|
||||
# XLSX exports
|
||||
/exports/*
|
||||
|
||||
# Archives of cLosed accounting periods
|
||||
# Archives of cLosed accounting periods
|
||||
/accounting/*
|
||||
|
||||
.DS_Store
|
||||
|
@ -1,5 +1,5 @@
|
||||
Metrics/LineLength:
|
||||
Max: 130
|
||||
Max: 140
|
||||
Metrics/MethodLength:
|
||||
Max: 30
|
||||
Metrics/CyclomaticComplexity:
|
||||
|
34
3rd-PARTY-LICENSES.md
Normal file
34
3rd-PARTY-LICENSES.md
Normal 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.
|
26
CHANGELOG.md
26
CHANGELOG.md
@ -1,5 +1,29 @@
|
||||
# Changelog Fab Manager
|
||||
|
||||
## v3.0.0 2019 March 28
|
||||
|
||||
- (France) Compliance with Article 88 of Law No. 2015-1785 and BOI-TVA-DECLA-30-10-30-20160803 : Certification of cash systems
|
||||
- Ability for an admin to view and close accounting periods
|
||||
- Secured archives for close accounting periods
|
||||
- Securely chained invoices records with visual control of data integrity
|
||||
- Notify an user if the available disk space reaches a configured threshold
|
||||
- Invoices generated outside of production environment will be watermarked
|
||||
- Keep track of currently logged user on each generated invoice
|
||||
- Fix a bug: unable to add a file attachment to an event
|
||||
- Fix a security issue: updated to devise 4.6.0 to fix [CVE-2019-5421](https://github.com/plataformatec/devise/issues/4981)
|
||||
- Fix a security issue: updated Rails to 4.2.11.1 to fix [CVE-2019-5418](https://groups.google.com/forum/#!topic/rubyonrails-security/pFRKI96Sm8Q) and [CVE-2019-5419](https://groups.google.com/forum/#!topic/rubyonrails-security/GN7w9fFAQeI)
|
||||
- Removed deprecated Capistrano deployment system
|
||||
- Rebranded product from "La Casemate"
|
||||
- Refactored some pieces of Ruby code, according to style guide
|
||||
- Added asterisks on required fields in sign-up form
|
||||
- [TODO DEPLOY] (dev) if applicable, you must first downgrade bundler to v1 `gem uninstall bundler --version=2.0.1 && gem install bundler --version=1.7.3 && bundle install`
|
||||
- [TODO DEPLOY] if you have changed your VAT rate in the past, add its history into database. You can use a rate of "0" to disable VAT. Eg. `rake fablab:setup:add_vat_rate[20,2017-01-01]`
|
||||
- [TODO DEPLOY] `rake fablab:setup:set_environment_to_invoices`
|
||||
- [TODO DEPLOY] `rake fablab:setup:chain_invoices_items_records`
|
||||
- [TODO DEPLOY] `rake fablab:setup:chain_invoices_records`
|
||||
- [TODO DEPLOY] `rake fablab:setup:chain_history_values_records`
|
||||
- [TODO DEPLOY] add `DISK_SPACE_MB_ALERT` and `SUPERADMIN_EMAIL` environment variables (see [doc/environment.md](doc/environment.md) for configuration details)
|
||||
|
||||
## v2.8.4 2019 March 18
|
||||
|
||||
- Limit members search to 50 results to speed up queries
|
||||
@ -515,7 +539,7 @@
|
||||
- Fix a bug: user is not redirected after changing is duplicated e-mail on the SSO provider
|
||||
|
||||
## v2.1.0 2016 May 2
|
||||
- Add search feature on openlab projects : [Openlab-projects](https://github.com/LaCasemate/openlab-projects)
|
||||
- Add search feature on openlab projects : [Openlab-projects](https://github.com/sleede/openlab-projects)
|
||||
- Add integration tests for main features
|
||||
- Credits logic has been extracted into a microservice
|
||||
- Improved UI list of projects
|
||||
|
@ -13,7 +13,7 @@ patches and features.
|
||||
|
||||
## Using the issue tracker
|
||||
|
||||
The [issue tracker](https://github.com/LaCasemate/fab-manager/issues) is the preferred channel for [bug reports](#bugs)
|
||||
The [issue tracker](https://github.com/sleede/fab-manager/issues) is the preferred channel for [bug reports](#bugs)
|
||||
and [submitting pull requests](#pull-requests), but please respect the following restrictions:
|
||||
|
||||
* Please **do not** use the issue tracker for personal support requests (use [the forum](https://forum.fab-manager.com)).
|
||||
@ -96,7 +96,7 @@ Adhering to the following process is the best way to get your work included in t
|
||||
# Navigate to the newly cloned directory
|
||||
cd fab-manager
|
||||
# Assign the original repo to a remote called "upstream"
|
||||
git remote add upstream https://github.com/LaCasemate/fab-manager.git
|
||||
git remote add upstream https://github.com/sleede/fab-manager.git
|
||||
```
|
||||
|
||||
2. If you cloned a while ago, get the latest changes from upstream:
|
||||
@ -131,4 +131,4 @@ Adhering to the following process is the best way to get your work included in t
|
||||
7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title and description.
|
||||
|
||||
**IMPORTANT**: By submitting a patch, you agree to allow the project owners to license your work under the terms of
|
||||
the [GNU Affero General Public License](LICENSE.md).
|
||||
the [GNU Affero General Public License](LICENSE.md).
|
||||
|
@ -4,7 +4,7 @@ MAINTAINER peng@sleede.com
|
||||
# First we need to be able to fetch from https repositories
|
||||
RUN apt-get update && \
|
||||
apt-get install -y apt-transport-https \
|
||||
ca-certificates
|
||||
ca-certificates apt-utils
|
||||
|
||||
|
||||
# Add sources for external tools to APT
|
||||
@ -44,6 +44,7 @@ RUN mkdir -p /usr/src/app/exports
|
||||
RUN mkdir -p /usr/src/app/log
|
||||
RUN mkdir -p /usr/src/app/public/uploads
|
||||
RUN mkdir -p /usr/src/app/public/assets
|
||||
RUN mkdir -p /usr/src/app/accounting
|
||||
RUN mkdir -p /usr/src/app/tmp/sockets
|
||||
RUN mkdir -p /usr/src/app/tmp/pids
|
||||
|
||||
@ -64,6 +65,7 @@ VOLUME /usr/src/app/exports
|
||||
VOLUME /usr/src/app/public
|
||||
VOLUME /usr/src/app/public/uploads
|
||||
VOLUME /usr/src/app/public/assets
|
||||
VOLUME /usr/src/app/accounting
|
||||
VOLUME /var/log/supervisor
|
||||
|
||||
# Expose port 3000 to the Docker host, so we can access it
|
||||
|
17
Gemfile
17
Gemfile
@ -2,7 +2,7 @@ source 'https://rubygems.org'
|
||||
|
||||
gem 'compass-rails', '2.0.4'
|
||||
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
||||
gem 'rails', '4.2.11'
|
||||
gem 'rails', '4.2.11.1'
|
||||
# Use SCSS for stylesheets
|
||||
gem 'sass-rails', '5.0.1'
|
||||
|
||||
@ -17,7 +17,7 @@ gem 'jquery-rails'
|
||||
gem 'jbuilder', '~> 2.5'
|
||||
gem 'jbuilder_cache_multi'
|
||||
# bundle exec rake doc:rails generates the API under doc/api.
|
||||
gem 'sdoc', '~> 0.4.0', group: :doc #TODO remove unused ?
|
||||
gem 'sdoc', '~> 0.4.0', group: :doc # TODO, remove unused ?
|
||||
|
||||
gem 'forgery'
|
||||
gem 'responders', '~> 2.0'
|
||||
@ -41,16 +41,12 @@ end
|
||||
group :development do
|
||||
gem 'active_record_query_trace'
|
||||
gem 'awesome_print'
|
||||
gem 'capistrano'
|
||||
gem 'capistrano-maintenance', '0.0.5', require: false
|
||||
gem 'capistrano-sidekiq', require: false
|
||||
gem 'coveralls', require: false
|
||||
gem 'foreman'
|
||||
# Preview mail in the browser
|
||||
gem 'mailcatcher'
|
||||
gem 'puma'
|
||||
gem 'rb-readline'
|
||||
gem 'rvm-capistrano', require: false
|
||||
end
|
||||
|
||||
group :test do
|
||||
@ -66,15 +62,13 @@ end
|
||||
|
||||
group :production do
|
||||
gem 'rails_12factor'
|
||||
gem 'unicorn'
|
||||
end
|
||||
|
||||
gem 'seed_dump'
|
||||
|
||||
gem 'pg'
|
||||
|
||||
gem 'devise'
|
||||
gem 'devise-async'
|
||||
gem 'devise', ">= 4.6.0"
|
||||
|
||||
gem 'omniauth', '~> 1.6.0'
|
||||
gem 'omniauth-oauth2'
|
||||
@ -148,3 +142,8 @@ gem 'axlsx_rails'
|
||||
gem 'rubyzip', '>= 1.2.2'
|
||||
|
||||
gem 'rack-protection', '1.5.5'
|
||||
|
||||
# get free disk space
|
||||
gem 'sys-filesystem'
|
||||
|
||||
gem 'sha3'
|
||||
|
123
Gemfile.lock
123
Gemfile.lock
@ -14,39 +14,39 @@ GEM
|
||||
specs:
|
||||
Ascii85 (1.0.2)
|
||||
aasm (4.1.0)
|
||||
actionmailer (4.2.11)
|
||||
actionpack (= 4.2.11)
|
||||
actionview (= 4.2.11)
|
||||
activejob (= 4.2.11)
|
||||
actionmailer (4.2.11.1)
|
||||
actionpack (= 4.2.11.1)
|
||||
actionview (= 4.2.11.1)
|
||||
activejob (= 4.2.11.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
actionpack (4.2.11)
|
||||
actionview (= 4.2.11)
|
||||
activesupport (= 4.2.11)
|
||||
actionpack (4.2.11.1)
|
||||
actionview (= 4.2.11.1)
|
||||
activesupport (= 4.2.11.1)
|
||||
rack (~> 1.6)
|
||||
rack-test (~> 0.6.2)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionpack-page_caching (1.0.2)
|
||||
actionpack (>= 4.0.0, < 5)
|
||||
actionview (4.2.11)
|
||||
activesupport (= 4.2.11)
|
||||
actionview (4.2.11.1)
|
||||
activesupport (= 4.2.11.1)
|
||||
builder (~> 3.1)
|
||||
erubis (~> 2.7.0)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
||||
active_record_query_trace (1.4)
|
||||
activejob (4.2.11)
|
||||
activesupport (= 4.2.11)
|
||||
activejob (4.2.11.1)
|
||||
activesupport (= 4.2.11.1)
|
||||
globalid (>= 0.3.0)
|
||||
activemodel (4.2.11)
|
||||
activesupport (= 4.2.11)
|
||||
activemodel (4.2.11.1)
|
||||
activesupport (= 4.2.11.1)
|
||||
builder (~> 3.1)
|
||||
activerecord (4.2.11)
|
||||
activemodel (= 4.2.11)
|
||||
activesupport (= 4.2.11)
|
||||
activerecord (4.2.11.1)
|
||||
activemodel (= 4.2.11.1)
|
||||
activesupport (= 4.2.11.1)
|
||||
arel (~> 6.0)
|
||||
activesupport (4.2.11)
|
||||
activesupport (4.2.11.1)
|
||||
i18n (~> 0.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
@ -70,7 +70,7 @@ GEM
|
||||
axlsx_rails (0.4.0)
|
||||
axlsx (>= 2.0.1)
|
||||
rails (>= 3.1)
|
||||
bcrypt (3.1.10)
|
||||
bcrypt (3.1.12)
|
||||
binding_of_caller (0.7.3)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bootstrap-sass (3.4.1)
|
||||
@ -80,17 +80,6 @@ GEM
|
||||
builder (3.2.3)
|
||||
byebug (8.2.3)
|
||||
camertron-eprun (1.1.0)
|
||||
capistrano (2.15.5)
|
||||
highline
|
||||
net-scp (>= 1.0.0)
|
||||
net-sftp (>= 2.0.0)
|
||||
net-ssh (>= 2.0.14)
|
||||
net-ssh-gateway (>= 1.1.0)
|
||||
capistrano-maintenance (0.0.5)
|
||||
capistrano (~> 2.0)
|
||||
capistrano-sidekiq (0.5.2)
|
||||
capistrano
|
||||
sidekiq
|
||||
carrierwave (0.10.0)
|
||||
activemodel (>= 3.2.0)
|
||||
activesupport (>= 3.2.0)
|
||||
@ -119,7 +108,7 @@ GEM
|
||||
compass (~> 1.0.0)
|
||||
sass-rails (<= 5.0.1)
|
||||
sprockets (< 2.13)
|
||||
concurrent-ruby (1.1.4)
|
||||
concurrent-ruby (1.1.5)
|
||||
connection_pool (2.2.0)
|
||||
coveralls (0.8.16)
|
||||
json (>= 1.8, < 3)
|
||||
@ -135,15 +124,12 @@ GEM
|
||||
debug_inspector (0.0.3)
|
||||
descendants_tracker (0.0.4)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
devise (3.4.1)
|
||||
devise (4.6.1)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 3.2.6, < 5)
|
||||
railties (>= 4.1.0, < 6.0)
|
||||
responders
|
||||
thread_safe (~> 0.1)
|
||||
warden (~> 1.2.3)
|
||||
devise-async (0.9.0)
|
||||
devise (~> 3.2)
|
||||
docile (1.1.5)
|
||||
domain_name (0.5.25)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
@ -185,14 +171,13 @@ GEM
|
||||
forgery (0.6.0)
|
||||
friendly_id (5.1.0)
|
||||
activerecord (>= 4.0.0)
|
||||
globalid (0.4.1)
|
||||
globalid (0.4.2)
|
||||
activesupport (>= 4.2.0)
|
||||
has_secure_token (1.0.0)
|
||||
activerecord (>= 3.0)
|
||||
hashdiff (0.3.0)
|
||||
hashery (2.1.2)
|
||||
hashie (3.5.7)
|
||||
highline (1.7.1)
|
||||
hike (1.2.3)
|
||||
hitimes (1.2.2)
|
||||
htmlentities (4.3.4)
|
||||
@ -226,7 +211,6 @@ GEM
|
||||
kaminari (0.16.3)
|
||||
actionpack (>= 3.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
kgio (2.9.3)
|
||||
libv8 (3.16.14.11)
|
||||
loofah (2.2.3)
|
||||
crass (~> 1.0.2)
|
||||
@ -249,7 +233,7 @@ GEM
|
||||
mimemagic (0.3.2)
|
||||
mini_magick (4.2.0)
|
||||
mini_mime (1.0.1)
|
||||
mini_portile2 (2.3.0)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.11.3)
|
||||
minitest-reporters (1.1.8)
|
||||
ansi
|
||||
@ -260,16 +244,9 @@ GEM
|
||||
multi_xml (0.5.5)
|
||||
multipart-post (2.0.0)
|
||||
naught (1.1.0)
|
||||
net-scp (1.2.1)
|
||||
net-ssh (>= 2.6.5)
|
||||
net-sftp (2.1.2)
|
||||
net-ssh (>= 2.6.5)
|
||||
net-ssh (2.9.2)
|
||||
net-ssh-gateway (1.2.0)
|
||||
net-ssh (>= 2.6.5)
|
||||
netrc (0.10.3)
|
||||
nokogiri (1.8.5)
|
||||
mini_portile2 (~> 2.3.0)
|
||||
nokogiri (1.10.1)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
notify_with (0.0.2)
|
||||
jbuilder (~> 2.0)
|
||||
rails (>= 4.2.0)
|
||||
@ -318,16 +295,16 @@ GEM
|
||||
rack-test (0.6.3)
|
||||
rack (>= 1.0)
|
||||
railroady (1.5.3)
|
||||
rails (4.2.11)
|
||||
actionmailer (= 4.2.11)
|
||||
actionpack (= 4.2.11)
|
||||
actionview (= 4.2.11)
|
||||
activejob (= 4.2.11)
|
||||
activemodel (= 4.2.11)
|
||||
activerecord (= 4.2.11)
|
||||
activesupport (= 4.2.11)
|
||||
rails (4.2.11.1)
|
||||
actionmailer (= 4.2.11.1)
|
||||
actionpack (= 4.2.11.1)
|
||||
actionview (= 4.2.11.1)
|
||||
activejob (= 4.2.11.1)
|
||||
activemodel (= 4.2.11.1)
|
||||
activerecord (= 4.2.11.1)
|
||||
activesupport (= 4.2.11.1)
|
||||
bundler (>= 1.3.0, < 2.0)
|
||||
railties (= 4.2.11)
|
||||
railties (= 4.2.11.1)
|
||||
sprockets-rails
|
||||
rails-deprecated_sanitizer (1.0.3)
|
||||
activesupport (>= 4.2.0.alpha)
|
||||
@ -344,13 +321,12 @@ GEM
|
||||
rails_stdout_logging
|
||||
rails_serve_static_assets (0.0.4)
|
||||
rails_stdout_logging (0.0.3)
|
||||
railties (4.2.11)
|
||||
actionpack (= 4.2.11)
|
||||
activesupport (= 4.2.11)
|
||||
railties (4.2.11.1)
|
||||
actionpack (= 4.2.11.1)
|
||||
activesupport (= 4.2.11.1)
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
rainbow (3.0.0)
|
||||
raindrops (0.13.0)
|
||||
rake (12.3.2)
|
||||
rb-fsevent (0.9.4)
|
||||
rb-inotify (0.9.5)
|
||||
@ -384,8 +360,6 @@ GEM
|
||||
rubyzip (1.2.2)
|
||||
rufus-scheduler (3.0.9)
|
||||
tzinfo
|
||||
rvm-capistrano (1.5.6)
|
||||
capistrano (~> 2.15.4)
|
||||
safe_yaml (1.0.4)
|
||||
sass (3.4.13)
|
||||
sass-rails (5.0.1)
|
||||
@ -403,6 +377,7 @@ GEM
|
||||
seed_dump (3.2.2)
|
||||
activerecord (~> 4)
|
||||
activesupport (~> 4)
|
||||
sha3 (1.0.1)
|
||||
sidekiq (3.3.4)
|
||||
celluloid (>= 0.16.0)
|
||||
connection_pool (>= 2.1.1)
|
||||
@ -440,6 +415,8 @@ GEM
|
||||
stripe (1.30.2)
|
||||
json (~> 1.8.1)
|
||||
rest-client (~> 1.4)
|
||||
sys-filesystem (1.2.0)
|
||||
ffi
|
||||
term-ansicolor (1.3.2)
|
||||
tins (~> 1.0)
|
||||
test_after_commit (1.0.0)
|
||||
@ -484,17 +461,13 @@ GEM
|
||||
unf_ext
|
||||
unf_ext (0.0.6)
|
||||
unicode-display_width (1.4.0)
|
||||
unicorn (4.8.3)
|
||||
kgio (~> 2.6)
|
||||
rack
|
||||
raindrops (~> 0.7)
|
||||
vcr (3.0.1)
|
||||
virtus (1.0.5)
|
||||
axiom-types (~> 0.1)
|
||||
coercible (~> 1.0)
|
||||
descendants_tracker (~> 0.0, >= 0.0.3)
|
||||
equalizer (~> 0.0, >= 0.0.9)
|
||||
warden (1.2.3)
|
||||
warden (1.2.7)
|
||||
rack (>= 1.0)
|
||||
web-console (2.1.3)
|
||||
activemodel (>= 4.0)
|
||||
@ -520,16 +493,12 @@ DEPENDENCIES
|
||||
axlsx_rails
|
||||
bootstrap-sass (>= 3.4.1)
|
||||
byebug
|
||||
capistrano
|
||||
capistrano-maintenance (= 0.0.5)
|
||||
capistrano-sidekiq
|
||||
carrierwave
|
||||
chroma
|
||||
compass-rails (= 2.0.4)
|
||||
coveralls
|
||||
database_cleaner
|
||||
devise
|
||||
devise-async
|
||||
devise (>= 4.6.0)
|
||||
elasticsearch-model (~> 5)
|
||||
elasticsearch-persistence (~> 5)
|
||||
elasticsearch-rails (~> 5)
|
||||
@ -562,7 +531,7 @@ DEPENDENCIES
|
||||
pundit
|
||||
rack-protection (= 1.5.5)
|
||||
railroady
|
||||
rails (= 4.2.11)
|
||||
rails (= 4.2.11.1)
|
||||
rails-observers
|
||||
rails_12factor
|
||||
rb-readline
|
||||
@ -571,24 +540,24 @@ DEPENDENCIES
|
||||
rolify
|
||||
rubocop (~> 0.61.1)
|
||||
rubyzip (>= 1.2.2)
|
||||
rvm-capistrano
|
||||
sass-rails (= 5.0.1)
|
||||
sdoc (~> 0.4.0)
|
||||
seed_dump
|
||||
sha3
|
||||
sidekiq
|
||||
sidekiq-cron
|
||||
sinatra
|
||||
spring
|
||||
stripe (= 1.30.2)
|
||||
sys-filesystem
|
||||
test_after_commit
|
||||
therubyracer (= 0.12.0)
|
||||
twitter
|
||||
twitter-text
|
||||
uglifier (>= 4.1.20)
|
||||
unicorn
|
||||
vcr
|
||||
web-console (~> 2.1.3)
|
||||
webmock
|
||||
|
||||
BUNDLED WITH
|
||||
1.17.2
|
||||
1.17.3
|
||||
|
39
LICENSE.md
39
LICENSE.md
@ -1,4 +1,4 @@
|
||||
Copyright (C) 2015 La Casemate
|
||||
Copyright (C) 2019 Sleede
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
@ -14,43 +14,6 @@ Copyright (C) 2015 La Casemate
|
||||
along with this program. If not, see <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
|
||||
Version 3, 19 November 2007
|
||||
|
||||
|
20
README.md
20
README.md
@ -1,8 +1,8 @@
|
||||
# FabManager
|
||||
|
||||
FabManager is the FabLab management solution. It is web-based, open-source and totally free.
|
||||
FabManager is the Fab Lab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.
|
||||
|
||||
[![Coverage Status](https://coveralls.io/repos/github/LaCasemate/fab-manager/badge.svg)](https://coveralls.io/github/LaCasemate/fab-manager)
|
||||
[![Coverage Status](https://coveralls.io/repos/github/sleede/fab-manager/badge.svg)](https://coveralls.io/github/sleede/fab-manager)
|
||||
[![Docker pulls](https://img.shields.io/docker/pulls/sleede/fab-manager.svg)](https://hub.docker.com/r/sleede/fab-manager/)
|
||||
[![Docker Build Status](https://img.shields.io/docker/build/sleede/fab-manager.svg)](https://hub.docker.com/r/sleede/fab-manager/builds)
|
||||
|
||||
@ -43,7 +43,6 @@ FabManager is a Ruby on Rails / AngularJS web application that runs on the follo
|
||||
|
||||
- Ubuntu LTS 14.04+ / Debian 8+
|
||||
- Ruby 2.3
|
||||
- Git 1.9.1+
|
||||
- Redis 2.8.4+
|
||||
- Sidekiq 3.3.4+
|
||||
- Elasticsearch 5.6
|
||||
@ -102,7 +101,7 @@ This procedure is not easy to follow so if you don't need to write some code for
|
||||
7. Retrieve the project from Git
|
||||
|
||||
```bash
|
||||
git clone https://github.com/LaCasemate/fab-manager.git
|
||||
git clone https://github.com/sleede/fab-manager.git
|
||||
```
|
||||
|
||||
8. Install the software dependencies.
|
||||
@ -136,7 +135,7 @@ This procedure is not easy to follow so if you don't need to write some code for
|
||||
10. Install bundler in the current RVM gemset
|
||||
|
||||
```bash
|
||||
gem install bundler
|
||||
gem install bundler --version=1.17.3
|
||||
```
|
||||
|
||||
11. Install the required ruby gems and javascript plugins
|
||||
@ -205,7 +204,7 @@ environment.
|
||||
2. Retrieve the project from Git
|
||||
|
||||
```bash
|
||||
git clone https://github.com/LaCasemate/fab-manager
|
||||
git clone https://github.com/sleede/fab-manager
|
||||
```
|
||||
|
||||
3. From the project directory, run:
|
||||
@ -339,6 +338,7 @@ This can be achieved doing the following:
|
||||
- `db/migrate/20150604131525_add_meta_data_to_notifications.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html), a PostgreSQL 9.4+ datatype.
|
||||
- `db/migrate/20160915105234_add_transformation_to_o_auth2_mapping.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html), a PostgreSQL 9.4+ datatype.
|
||||
- `db/migrate/20181217103441_migrate_settings_value_to_history_values.rb` is using `SELECT DISTINCT ON`.
|
||||
- `db/migrate/20190107111749_protect_accounting_periods.rb` is using `CREATE RULE` and `DROP RULE`.
|
||||
- If you intend to contribute to the project code, you will need to run the test suite with `rake test`.
|
||||
This also requires your user to have the _SUPERUSER_ role.
|
||||
Please see the [known issues](#known-issues) section for more information about this.
|
||||
@ -445,6 +445,10 @@ In each cases, some inline comments are included in the localisation files.
|
||||
They can be recognized as they start with the sharp character (#).
|
||||
These comments are not required to be translated, they are intended to help the translator to have some context information about the sentence to translate.
|
||||
|
||||
You will also need to translate the invoice watermark, located in `app/pdfs/data/`.
|
||||
You'll find there the [GIMP source of the image](app/pdfs/data/watermark.xcf), which is using [Rubik Mono One](https://fonts.google.com/specimen/Rubik+Mono+One) as font.
|
||||
Use it to generate a similar localised PNG image which keep the default image size, as PDF are not responsive.
|
||||
|
||||
|
||||
<a name="i18n-configuration"></a>
|
||||
### Configuration
|
||||
@ -467,7 +471,7 @@ After modifying any values concerning the localisation, restart the application
|
||||
|
||||
**This configuration is optional.**
|
||||
|
||||
You can configure your fab-manager to synchronize every project with the [Open Projects platform](https://github.com/LaCasemate/openlab-projects).
|
||||
You can configure your fab-manager to synchronize every project with the [Open Projects platform](https://github.com/sleede/openlab-projects).
|
||||
It's very simple and straightforward and in return, your users will be able to search over projects from all fab-manager instances from within your platform.
|
||||
The deal is fair, you share your projects and as reward you benefits from projects of the whole community.
|
||||
|
||||
@ -496,7 +500,7 @@ It enables you to write plugins which can:
|
||||
|
||||
To install a plugin, you just have to copy the plugin folder which contains its code into the folder `plugins` of Fab-manager.
|
||||
|
||||
You can see an example on the [repo of navinum gamification plugin](https://github.com/LaCasemate/navinum-gamification)
|
||||
You can see an example on the [repo of navinum gamification plugin](https://github.com/sleede/navinum-gamification)
|
||||
|
||||
<a name="sso"></a>
|
||||
## Single Sign-On
|
||||
|
@ -17,8 +17,8 @@
|
||||
/**
|
||||
* Controller used in the admin invoices listing page
|
||||
*/
|
||||
Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'Invoice', 'invoices', '$uibModal', 'growl', '$filter', 'Setting', 'settings', '_t',
|
||||
function ($scope, $state, Invoice, invoices, $uibModal, growl, $filter, Setting, settings, _t) {
|
||||
Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'Invoice', 'AccountingPeriod', 'invoices', 'closedPeriods', '$uibModal', 'growl', '$filter', 'Setting', 'settings', '_t',
|
||||
function ($scope, $state, Invoice, AccountingPeriod, invoices, closedPeriods, $uibModal, growl, $filter, Setting, settings, _t) {
|
||||
/* PRIVATE STATIC CONSTANTS */
|
||||
|
||||
// number of invoices loaded each time we click on 'load more...'
|
||||
@ -110,7 +110,9 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
templateUrl: '<%= asset_path "admin/invoices/avoirModal.html" %>',
|
||||
controller: 'AvoirModalController',
|
||||
resolve: {
|
||||
invoice () { return invoice; }
|
||||
invoice () { return invoice; },
|
||||
closedPeriods() { return AccountingPeriod.query().$promise; },
|
||||
lastClosingEnd() { return AccountingPeriod.lastClosingEnd().$promise; }
|
||||
}
|
||||
});
|
||||
|
||||
@ -302,17 +304,34 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
active () {
|
||||
return $scope.invoice.VAT.active;
|
||||
},
|
||||
history () {
|
||||
rateHistory () {
|
||||
return Setting.get({ name: 'invoice_VAT-rate', history: true }).$promise;
|
||||
},
|
||||
activeHistory () {
|
||||
return Setting.get({ name: 'invoice_VAT-active', history: true }).$promise;
|
||||
}
|
||||
},
|
||||
controller: ['$scope', '$uibModalInstance', 'rate', 'active', 'history', function ($scope, $uibModalInstance, rate, active, history) {
|
||||
controller: ['$scope', '$uibModalInstance', 'rate', 'active', 'rateHistory', 'activeHistory', function ($scope, $uibModalInstance, rate, active, rateHistory, activeHistory) {
|
||||
$scope.rate = rate;
|
||||
$scope.isSelected = active;
|
||||
$scope.history = history.setting.history;
|
||||
$scope.history = [];
|
||||
|
||||
$scope.ok = function () { $uibModalInstance.close({ rate: $scope.rate, active: $scope.isSelected }); };
|
||||
return $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||
|
||||
|
||||
const initialize = function() {
|
||||
rateHistory.setting.history.forEach(function (rate) {
|
||||
$scope.history.push({ date: rate.created_at, rate: rate.value, user: rate.user })
|
||||
});
|
||||
activeHistory.setting.history.forEach(function (v) {
|
||||
if (v.value === 'false') {
|
||||
$scope.history.push({ date: v.created_at, rate: 0, user: v.user })
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initialize();
|
||||
}]
|
||||
});
|
||||
|
||||
@ -391,6 +410,37 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
return invoiceSearch(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Open a modal allowing the user to close an accounting period and to
|
||||
* view all periods already closed.
|
||||
*/
|
||||
$scope.closeAnAccountingPeriod = function() {
|
||||
// open modal
|
||||
$uibModal.open({
|
||||
templateUrl: '<%= asset_path "admin/invoices/closePeriodModal.html" %>',
|
||||
controller: 'ClosePeriodModalController',
|
||||
size: 'lg',
|
||||
resolve: {
|
||||
periods() { return AccountingPeriod.query().$promise; },
|
||||
lastClosingEnd() { return AccountingPeriod.lastClosingEnd().$promise; }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the given date is within a closed accounting period
|
||||
* @param date {Date} date to test
|
||||
* @returns {boolean} true if closed, false otherwise
|
||||
*/
|
||||
$scope.isDateClosed = function(date) {
|
||||
for (const period of closedPeriods) {
|
||||
if (moment(date).isBetween(moment.utc(period.start_at).startOf('day'), moment.utc(period.end_at).endOf('day'), null, '[]')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/**
|
||||
@ -500,8 +550,8 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
/**
|
||||
* Controller used in the invoice refunding modal window
|
||||
*/
|
||||
Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModalInstance', 'invoice', 'Invoice', 'growl', '_t',
|
||||
function ($scope, $uibModalInstance, invoice, Invoice, growl, _t) {
|
||||
Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModalInstance', 'invoice', 'closedPeriods', 'lastClosingEnd', 'Invoice', 'growl', '_t',
|
||||
function ($scope, $uibModalInstance, invoice, closedPeriods, lastClosingEnd, Invoice, growl, _t) {
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// invoice linked to the current refund
|
||||
@ -517,6 +567,9 @@ Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModal
|
||||
invoice_items_ids: []
|
||||
};
|
||||
|
||||
// End date of last closed accounting period or date of first invoice
|
||||
$scope.lastClosingEnd = moment.utc(lastClosingEnd.last_end_date).toDate();
|
||||
|
||||
// Possible refunding methods
|
||||
$scope.avoirModes = [
|
||||
{ name: _t('invoices.none'), value: 'none' },
|
||||
@ -580,6 +633,20 @@ Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModal
|
||||
*/
|
||||
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||
|
||||
/**
|
||||
* Test if the given date is within a closed accounting period
|
||||
* @param date {Date} date to test
|
||||
* @returns {boolean} true if closed, false otherwise
|
||||
*/
|
||||
$scope.isDateClosed = function(date) {
|
||||
for (const period of closedPeriods) {
|
||||
if (moment(date).isBetween(moment.utc(period.start_at).startOf('day'), moment.utc(period.end_at).endOf('day'), null, '[]')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/**
|
||||
@ -604,3 +671,115 @@ Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModal
|
||||
return initialize();
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
/**
|
||||
* Controller used in the modal window allowing an admin to close an accounting period
|
||||
*/
|
||||
Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$uibModalInstance', '$window', 'Invoice', 'AccountingPeriod', 'periods', 'lastClosingEnd','dialogs', 'growl', '_t',
|
||||
function ($scope, $uibModalInstance, $window, Invoice, AccountingPeriod, periods, lastClosingEnd, dialogs, growl, _t) {
|
||||
const YESTERDAY = moment.utc({ h: 0, m: 0, s: 0, ms: 0 }).subtract(1, 'day').toDate();
|
||||
const LAST_CLOSING = moment.utc(lastClosingEnd.last_end_date).toDate();
|
||||
const MAX_END = moment.utc(lastClosingEnd.last_end_date).add(1, 'year').subtract(1, 'day').toDate();
|
||||
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// date pickers values are bound to these variables
|
||||
$scope.period = {
|
||||
start_at: LAST_CLOSING,
|
||||
end_at: moment(YESTERDAY).isBefore(MAX_END) ? YESTERDAY : MAX_END
|
||||
};
|
||||
|
||||
// any form errors will come here
|
||||
$scope.errors = {};
|
||||
|
||||
// will match any error about invoices
|
||||
$scope.invoiceErrorRE = /^invoice_(.+)$/;
|
||||
|
||||
// existing closed periods, provided by the API
|
||||
$scope.accountingPeriods = periods;
|
||||
|
||||
// closing a period may take a long time so we need to prevent the user from double-clicking the close button while processing
|
||||
$scope.pendingCreation = false;
|
||||
|
||||
// AngularUI-Bootstrap datepickers parameters to define the period to close
|
||||
$scope.datePicker = {
|
||||
format: Fablab.uibDateFormat,
|
||||
// default: datePicker are not shown
|
||||
startOpened: false,
|
||||
endOpened: false,
|
||||
minDate: LAST_CLOSING,
|
||||
maxDate: moment(YESTERDAY).isBefore(MAX_END) ? YESTERDAY : MAX_END,
|
||||
options: {
|
||||
startingDay: Fablab.weekStartingDay
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback to open the datepicker
|
||||
*/
|
||||
$scope.toggleDatePicker = function ($event) {
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
$scope.datePicker.endOpened = !$scope.datePicker.endOpened;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate the close period creation
|
||||
*/
|
||||
$scope.ok = function () {
|
||||
dialogs.confirm(
|
||||
{
|
||||
resolve: {
|
||||
object () {
|
||||
return {
|
||||
title: _t('invoices.confirmation_required'),
|
||||
msg: _t(
|
||||
'invoices.confirm_close_START_END',
|
||||
{ START: moment.utc($scope.period.start_at).format('LL'), END: moment.utc($scope.period.end_at).format('LL') }
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
function () { // creation confirmed
|
||||
$scope.pendingCreation = true;
|
||||
AccountingPeriod.save(
|
||||
{
|
||||
accounting_period: {
|
||||
start_at: moment.utc($scope.period.start_at).toDate(),
|
||||
end_at: moment.utc($scope.period.end_at).endOf('day').toDate()
|
||||
}
|
||||
},
|
||||
function (resp) {
|
||||
$scope.pendingCreation = false;
|
||||
growl.success(_t(
|
||||
'invoices.period_START_END_closed_success',
|
||||
{ START: moment.utc(resp.start_at).format('LL'), END: moment.utc(resp.end_at).format('LL') }
|
||||
));
|
||||
$uibModalInstance.close(resp);
|
||||
},
|
||||
function(error) {
|
||||
$scope.pendingCreation = false;
|
||||
growl.error(_t('invoices.failed_to_close_period'));
|
||||
$scope.errors = error.data;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel the refund, dismiss the modal window
|
||||
*/
|
||||
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||
|
||||
/**
|
||||
* Trigger the API call to download the JSON archive of the closed accounting period
|
||||
*/
|
||||
$scope.downloadArchive = function(period) {
|
||||
$window.location.href = `/api/accounting_periods/${period.id}/archive`;
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
@ -150,7 +150,7 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
|
||||
};
|
||||
|
||||
// admins list
|
||||
$scope.admins = adminsPromise.admins;
|
||||
$scope.admins = adminsPromise.admins.filter(function(m) { return m.id != Fablab.superadminId; });
|
||||
|
||||
// Admins ordering/sorting. Default: not sorted
|
||||
$scope.orderAdmin = null;
|
||||
|
@ -340,7 +340,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
|
||||
var openLoginModal = function (toState, toParams, callback) {
|
||||
<% active_provider = AuthProvider.active %>
|
||||
<% if active_provider.providable_type != DatabaseProvider.name %>
|
||||
$window.location.href = '<%=user_omniauth_authorize_path(AuthProvider.active.strategy_name.to_sym)%>';
|
||||
$window.location.href = '<%="/users/auth/#{active_provider.strategy_name}"%>';
|
||||
<% else %>
|
||||
return $uibModal.open({
|
||||
templateUrl: '<%= asset_path "shared/deviseModal.html" %>',
|
||||
|
@ -885,6 +885,7 @@ angular.module('application.router', ['ui.router'])
|
||||
query: { number: '', customer: '', date: null, order_by: '-reference', page: 1, size: 20 }
|
||||
}).$promise;
|
||||
}],
|
||||
closedPeriods: [ 'AccountingPeriod', function(AccountingPeriod) { return AccountingPeriod.query().$promise; }],
|
||||
translations: ['Translations', function (Translations) { return Translations.query('app.admin.invoices').$promise; }]
|
||||
}
|
||||
})
|
||||
|
12
app/assets/javascripts/services/accounting_period.js
Normal file
12
app/assets/javascripts/services/accounting_period.js
Normal 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'
|
||||
}
|
||||
}
|
||||
);
|
||||
}]);
|
@ -616,4 +616,8 @@ padding: 10px;
|
||||
& > i.fileinput-exists {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.help-block.error {
|
||||
color: #ff565d;
|
||||
}
|
||||
|
@ -33,6 +33,7 @@
|
||||
@import "app.components";
|
||||
@import "app.plugins";
|
||||
@import "modules/invoice";
|
||||
@import "modules/signup";
|
||||
|
||||
@import "app.responsive";
|
||||
|
||||
|
@ -1,6 +1,14 @@
|
||||
|
||||
// admin invoices
|
||||
|
||||
.chained {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.broken {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.invoice-placeholder {
|
||||
width: 80%;
|
||||
max-width: 800px;
|
||||
@ -184,3 +192,79 @@
|
||||
font-style: italic;
|
||||
color: #5a5a5a;
|
||||
}
|
||||
|
||||
|
||||
table.closings-table {
|
||||
@extend table.scrollable-3-cols;
|
||||
|
||||
tbody .actions {
|
||||
padding-left: 2em;
|
||||
|
||||
& > span {
|
||||
margin-left: 2em;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
tbody .show-more {
|
||||
color: #00b3ee;
|
||||
}
|
||||
|
||||
tbody .download-archive {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
tbody .download-archive:hover {
|
||||
i {
|
||||
display: none;
|
||||
}
|
||||
&:after {
|
||||
content: '\f019';
|
||||
font-family: 'fontawesome';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.scrollable-3-cols {
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
|
||||
thead, tbody, tr, th, td { display: block; }
|
||||
|
||||
thead tr {
|
||||
/* fallback */
|
||||
width: 97%;
|
||||
/* minus scroll bar width */
|
||||
width: -webkit-calc(100% - 16px);
|
||||
width: -moz-calc(100% - 16px);
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
|
||||
thead tr th {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
tr:after { /* clearing float */
|
||||
content: ' ';
|
||||
display: block;
|
||||
visibility: hidden;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
tbody {
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
tbody td, thead th {
|
||||
width: 32%; /* 32% is less than (100% / 3 cols) = 33.33% */
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.period-info-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
31
app/assets/stylesheets/modules/signup.scss
Normal file
31
app/assets/stylesheets/modules/signup.scss
Normal 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;
|
||||
}
|
||||
}
|
11
app/assets/templates/admin/invoices/_period.html.erb
Normal file
11
app/assets/templates/admin/invoices/_period.html.erb
Normal 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>
|
@ -14,6 +14,7 @@
|
||||
uib-datepicker-popup="{{datePicker.format}}"
|
||||
datepicker-options="datePicker.options"
|
||||
is-open="datePicker.opened"
|
||||
min-date="lastClosingEnd"
|
||||
placeholder="{{datePicker.format}}"
|
||||
ng-click="openDatePicker($event)"
|
||||
required/>
|
||||
|
@ -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>
|
@ -10,7 +10,11 @@
|
||||
<h1 translate>{{ 'invoices.invoices' }}</h1>
|
||||
</section>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
@ -57,6 +61,7 @@
|
||||
<table class="table" ng-if="invoices.length > 0">
|
||||
<thead>
|
||||
<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: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>
|
||||
<tbody>
|
||||
<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 ng-if="!invoice.is_avoir">{{ invoice.date | amDateFormat:'L LTS' }}</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">
|
||||
<i class="fa fa-file-pdf-o"></i> {{ 'invoices.download_the_credit_note' | translate }}
|
||||
</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 }}
|
||||
</a>
|
||||
</div>
|
||||
@ -387,18 +396,21 @@
|
||||
|
||||
<div class="m-t-lg">
|
||||
<h4 translate>{{ 'invoices.VAT_history' }}</h4>
|
||||
<table class="table">
|
||||
<head>
|
||||
<table class="table scrollable-3-cols">
|
||||
<thead>
|
||||
<tr>
|
||||
<th translate>{{ 'invoices.VAT_rate' }}</th>
|
||||
<th translate>{{ 'invoices.changed_at' }}</th>
|
||||
<th translate>{{ 'invoices.changed_by' }}</th>
|
||||
</tr>
|
||||
</head>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="value in history">
|
||||
<td>{{value.value}} %</td>
|
||||
<td>{{value.created_at | amDateFormat:'L LT'}}</td>
|
||||
<tr ng-repeat="value in history | orderBy:'-date'">
|
||||
<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>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -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>
|
||||
</li>
|
||||
<% 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()">
|
||||
<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>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
@ -6,7 +6,7 @@
|
||||
<uib-alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)">{{alert.msg}}</uib-alert>
|
||||
|
||||
<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="col-sm-12">
|
||||
<label class="checkbox-inline">
|
||||
@ -22,11 +22,12 @@
|
||||
ng-model="user.profile_attributes.gender"
|
||||
value="false"/> {{ 'woman' | translate }}
|
||||
</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>
|
||||
</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}">
|
||||
<input type="text"
|
||||
@ -35,6 +36,7 @@
|
||||
class="form-control"
|
||||
placeholder="{{ 'your_first_name' | translate }}"
|
||||
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>
|
||||
</div>
|
||||
<div class="m-b visible-xs"></div>
|
||||
@ -45,11 +47,12 @@
|
||||
class="form-control"
|
||||
placeholder="{{ 'your_surname' | translate }}"
|
||||
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>
|
||||
</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="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-user"></i></span>
|
||||
@ -60,11 +63,12 @@
|
||||
placeholder="{{ 'your_pseudonym' | translate }}"
|
||||
required>
|
||||
</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>
|
||||
</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="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-envelope"></i></span>
|
||||
@ -75,11 +79,12 @@
|
||||
placeholder="{{ 'your_email_address' | translate }}"
|
||||
required>
|
||||
</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>
|
||||
</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="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-key"></i></span>
|
||||
@ -91,12 +96,13 @@
|
||||
required
|
||||
ng-minlength="8">
|
||||
</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.minlength" translate>{{ 'password_is_too_short_(minimum_8_characters)' }}</span>
|
||||
</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="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-key"></i></span>
|
||||
@ -108,6 +114,7 @@
|
||||
required ng-minlength="8"
|
||||
match="user.password">
|
||||
</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.$error.match" translate>{{ 'password_does_not_match_with_confirmation' }}</span>
|
||||
</div>
|
||||
@ -124,7 +131,7 @@
|
||||
</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="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-building-o"></i></span>
|
||||
@ -135,11 +142,12 @@
|
||||
placeholder="{{ 'name_of_your_organization' | translate }}"
|
||||
ng-required="user.organization">
|
||||
</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>
|
||||
</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="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-map-marker"></i></span>
|
||||
@ -150,22 +158,24 @@
|
||||
placeholder="{{ 'address_of_your_organization' | translate }}"
|
||||
ng-required="user.organization">
|
||||
</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>
|
||||
</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>
|
||||
<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>
|
||||
</select>
|
||||
<span class="exponent exponent-select"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
|
||||
</div>
|
||||
<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 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="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-calendar-o"></i> </span>
|
||||
@ -180,11 +190,12 @@
|
||||
ng-click="openDatePicker($event)"
|
||||
required/>
|
||||
</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>
|
||||
</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="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-phone"></i> </span>
|
||||
@ -195,6 +206,7 @@
|
||||
placeholder="{{ 'phone_number' | translate }}"
|
||||
required>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@ -229,9 +241,16 @@
|
||||
ng-model="user.cgu"
|
||||
value="true"
|
||||
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>
|
||||
<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">
|
||||
<input type="hidden" name="cgu" ng-model="user.cgu" value="true">
|
||||
</div>
|
||||
@ -240,4 +259,4 @@
|
||||
</div>
|
||||
<div class="modal-footer no-padder">
|
||||
<button class="btn btn-valid btn-warning btn-block p-l btn-lg text-u-c r-b" ng-click="ok()" ng-disabled="signupForm.$invalid" translate>{{ 'confirm' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
50
app/controllers/api/accounting_periods_controller.rb
Normal file
50
app/controllers/api/accounting_periods_controller.rb
Normal 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
|
@ -96,7 +96,7 @@ class API::EventsController < API::ApiController
|
||||
:recurrence_end_at, :category_id, :event_theme_ids, :age_range_id,
|
||||
event_theme_ids: [],
|
||||
event_image_attributes: [:attachment],
|
||||
event_files_attributes: %i[id attachment_destroy],
|
||||
event_files_attributes: %i[id attachment _destroy],
|
||||
event_price_categories_attributes: %i[id price_category_id amount _destroy])
|
||||
EventService.process_params(event_preparams)
|
||||
end
|
||||
|
@ -26,7 +26,7 @@ class API::ReservationsController < API::ApiController
|
||||
user_id = current_user.admin? ? reservation_params[:user_id] : current_user.id
|
||||
|
||||
@reservation = Reservation.new(reservation_params)
|
||||
is_reserve = Reservations::Reserve.new(user_id)
|
||||
is_reserve = Reservations::Reserve.new(user_id, current_user.id)
|
||||
.pay_and_save(@reservation, method, coupon_params[:coupon_code])
|
||||
|
||||
if is_reserve
|
||||
|
@ -19,7 +19,7 @@ class API::SubscriptionsController < API::ApiController
|
||||
user_id = current_user.admin? ? subscription_params[:user_id] : current_user.id
|
||||
|
||||
@subscription = Subscription.new(subscription_params)
|
||||
is_subscribe = Subscriptions::Subscribe.new(user_id)
|
||||
is_subscribe = Subscriptions::Subscribe.new(user_id, current_user.id)
|
||||
.pay_and_save(@subscription, method, coupon_params[:coupon_code], true)
|
||||
|
||||
if is_subscribe
|
||||
@ -35,7 +35,7 @@ class API::SubscriptionsController < API::ApiController
|
||||
|
||||
free_days = params[:subscription][:free] || false
|
||||
|
||||
res = Subscriptions::Subscribe.new(@subscription.user_id)
|
||||
res = Subscriptions::Subscribe.new(@subscription.user_id, current_user.id)
|
||||
.extend_subscription(@subscription, subscription_update_params[:expired_at], free_days)
|
||||
if res.is_a?(Subscription)
|
||||
@subscription = res
|
||||
|
@ -1,4 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
require 'version'
|
||||
|
||||
# API Controller to get the fab-manager version
|
||||
class API::VersionController < API::ApiController
|
||||
@ -6,8 +7,7 @@ class API::VersionController < API::ApiController
|
||||
|
||||
def show
|
||||
authorize :version
|
||||
package = File.read('package.json')
|
||||
version = JSON.parse(package)['version']
|
||||
render json: { version: version }, status: :ok
|
||||
|
||||
render json: { version: Version.current }, status: :ok
|
||||
end
|
||||
end
|
||||
|
@ -30,11 +30,16 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
|
||||
def configure_permitted_parameters
|
||||
devise_parameter_sanitizer.for(:sign_up) <<
|
||||
{ profile_attributes: [:phone, :last_name, :first_name, :gender, :birthday, :interest, :software_mastered,
|
||||
organization_attributes: [:name, address_attributes: [:address]]] }
|
||||
|
||||
devise_parameter_sanitizer.for(:sign_up).concat %i[username is_allow_contact is_allow_newsletter cgu group_id]
|
||||
devise_parameter_sanitizer.permit(:sign_up,
|
||||
keys: [
|
||||
{ profile_attributes: [
|
||||
:phone, :last_name, :first_name, :gender, :birthday,
|
||||
:interest, :software_mastered, organization_attributes: [
|
||||
:name, address_attributes: [:address]
|
||||
]
|
||||
] },
|
||||
:username, :is_allow_contact, :is_allow_newsletter, :cgu, :group_id
|
||||
])
|
||||
end
|
||||
|
||||
def default_url_options
|
||||
|
@ -4,7 +4,7 @@ class SessionsController < Devise::SessionsController
|
||||
def new
|
||||
active_provider = AuthProvider.active
|
||||
if active_provider.providable_type != DatabaseProvider.name
|
||||
redirect_to user_omniauth_authorize_path(active_provider.strategy_name.to_sym)
|
||||
redirect_to "/users/auth/#{active_provider.strategy_name}"
|
||||
else
|
||||
super
|
||||
end
|
||||
|
@ -1,8 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# app/concerns/controllers/api_doc.rb
|
||||
#
|
||||
# Controller extension with common API documentation shortcuts
|
||||
#
|
||||
|
||||
module OpenAPI::ApiDoc
|
||||
# Apipie doesn't allow to append anything to esisting
|
||||
# description. It raises an error on double definition.
|
||||
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# app/docs/application_doc.rb
|
||||
#
|
||||
# A common class for defining API docs
|
||||
@ -16,7 +18,6 @@
|
||||
# end
|
||||
# end
|
||||
#
|
||||
|
||||
class OpenAPI::ApplicationDoc
|
||||
extend OpenAPI::ApiDoc
|
||||
|
||||
|
@ -1,5 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# parent class for openAPI documentation
|
||||
class OpenAPI::V1::BaseDoc < OpenAPI::ApplicationDoc
|
||||
API_VERSION = "v1"
|
||||
FORMATS = ['json']
|
||||
API_VERSION = 'v1'
|
||||
FORMATS = ['json'].freeze
|
||||
PER_PAGE_DEFAULT = 20
|
||||
end
|
||||
|
@ -1,3 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# openAPI documentation for bookable machines endpoint
|
||||
class OpenAPI::V1::BookableMachinesDoc < OpenAPI::V1::BaseDoc
|
||||
resource_description do
|
||||
short 'Bookable machines'
|
||||
@ -7,10 +10,10 @@ class OpenAPI::V1::BookableMachinesDoc < OpenAPI::V1::BaseDoc
|
||||
end
|
||||
|
||||
doc_for :index do
|
||||
api :GET, "/#{API_VERSION}/bookable_machines", "Bookable machines index"
|
||||
description "Machines that a given user is allowed to book."
|
||||
param :user_id, Integer, required: true, desc: "Id of the given user."
|
||||
example <<-EOS
|
||||
api :GET, "/#{API_VERSION}/bookable_machines", 'Bookable machines index'
|
||||
description 'Machines that a given user is allowed to book.'
|
||||
param :user_id, Integer, required: true, desc: 'Id of the given user.'
|
||||
example <<-MACHINES
|
||||
# /open_api/v1/bookable_machines?user_id=522
|
||||
{
|
||||
"machines": [
|
||||
@ -67,6 +70,6 @@ class OpenAPI::V1::BookableMachinesDoc < OpenAPI::V1::BaseDoc
|
||||
# ...
|
||||
]
|
||||
}
|
||||
EOS
|
||||
MACHINES
|
||||
end
|
||||
end
|
||||
|
@ -1,30 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# openAPI pagination
|
||||
module OpenAPI::V1::Concerns::ParamGroups
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
define_param_group :pagination do
|
||||
param :page, Integer, desc: "Page number", optional: true
|
||||
param :page, Integer, desc: 'Page number', optional: true
|
||||
param :per_page, Integer, desc: "Number of objects per page. Default is #{OpenAPI::V1::BaseDoc::PER_PAGE_DEFAULT}.", optional: true
|
||||
end
|
||||
|
||||
# define_param_group :order_type do
|
||||
# param :order_type, ['asc', 'desc'], desc: "order type: descendant or ascendant. Default value is *desc*."
|
||||
# end
|
||||
#
|
||||
# define_param_group :filter_by_tags do
|
||||
# param :tagged_with, [String, Array], desc: 'If multiple tags are given, we use an *OR* function. See parameter *order_by_matching_tag_count* to order the result. It can also be a *comma* *separated* *string*. Example: tagged_with=science,museum'
|
||||
# param :order_by_matching_tag_count, ['t',1,'true'], desc: "You can use this parameter if you are sending a parameter *tagged_with*. Send this parameter to order by number of matching tags (descendant): result will be sort firstly by matching tags and secondly by order given by *order_by* parameter. Default to *false*."
|
||||
# end
|
||||
#
|
||||
# define_param_group :filter_by_blog do
|
||||
# param :blog_slug, String, desc: "Send the blog's *slug* to only return articles belonging to specific blog."
|
||||
# end
|
||||
#
|
||||
# define_param_group :filter_by_geolocation do
|
||||
# param :latitude, Numeric, desc: "Latitude. Example: *45.166670*"
|
||||
# param :longitude, Numeric, desc: "Longitude. Example: *5.7166700*"
|
||||
# param :radius, Numeric, desc: "To be combined with parameters latitude and longitude. Default to *10*."
|
||||
# param :order_by_distance, ['t',1,'true'], desc: "You can use this parameter if you are sending parameters *latitude* and *longitude*. Send this parameter to order by distance (descendant): result will be sort firstly by distance and secondly by order given by *order_by* parameter. Default to *false*."
|
||||
# end
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# openAPI documentation for events endpoint
|
||||
class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc
|
||||
resource_description do
|
||||
short 'Events'
|
||||
@ -9,19 +12,19 @@ class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc
|
||||
include OpenAPI::V1::Concerns::ParamGroups
|
||||
|
||||
doc_for :index do
|
||||
api :GET, "/#{API_VERSION}/events", "Events index"
|
||||
api :GET, "/#{API_VERSION}/events", 'Events index'
|
||||
param_group :pagination
|
||||
param :id, [Integer, Array], optional: true, desc: "Scope the request to one or various events."
|
||||
param :upcoming, [FalseClass, TrueClass], optional: true, desc: "Scope for the upcoming events."
|
||||
description "Events index. Order by *created_at* desc."
|
||||
example <<-EOS
|
||||
param :id, [Integer, Array], optional: true, desc: 'Scope the request to one or various events.'
|
||||
param :upcoming, [FalseClass, TrueClass], optional: true, desc: 'Scope for the upcoming events.'
|
||||
description 'Events index. Order by *created_at* desc.'
|
||||
example <<-EVENTS
|
||||
# /open_api/v1/events?page=1&per_page=2
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"id": 183,
|
||||
"title": "OPEN LAB",
|
||||
"description": "Que vous soyez Fab user, visiteur, curieux ou bricoleur, l’atelier de fabrication numérique vous ouvre ses portes les mercredis soirs pour avancer vos projets ou rencontrer la «communauté» Fab Lab. \r\n\r\nCe soir, venez spécialement découvrir les machines à commandes numérique du Fab Lab de La Casemate, venez comprendre ce lieux ouvert à tous. \r\n\r\n\r\nVenez découvrir un concept, une organisation, des machines, pour stimuler votre sens de la créativité.",
|
||||
"description": "Que vous soyez Fab user, visiteur, curieux ou bricoleur, l’atelier de fabrication numérique vous ouvre ses portes les mercredis soirs pour avancer vos projets ou rencontrer la «communauté» Fab Lab. \r\n\r\nCe soir, venez spécialement découvrir les machines à commandes numérique de la Fabrique de Fab-manager, venez comprendre ce lieux ouvert à tous. \r\n\r\n\r\nVenez découvrir un concept, une organisation, des machines, pour stimuler votre sens de la créativité.",
|
||||
"updated_at": "2016-04-25T10:49:40.055+02:00",
|
||||
"created_at": "2016-04-25T10:49:40.055+02:00",
|
||||
"nb_total_places": 18,
|
||||
@ -54,6 +57,6 @@ class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc
|
||||
}
|
||||
]
|
||||
}
|
||||
EOS
|
||||
EVENTS
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# openAPI documentation for invoices endpoints
|
||||
class OpenAPI::V1::InvoicesDoc < OpenAPI::V1::BaseDoc
|
||||
resource_description do
|
||||
short 'Invoices'
|
||||
@ -9,11 +12,11 @@ class OpenAPI::V1::InvoicesDoc < OpenAPI::V1::BaseDoc
|
||||
include OpenAPI::V1::Concerns::ParamGroups
|
||||
|
||||
doc_for :index do
|
||||
api :GET, "/#{API_VERSION}/invoices", "Invoices index"
|
||||
api :GET, "/#{API_VERSION}/invoices", 'Invoices index'
|
||||
description "Index of users' invoices, with optional pagination. Order by *created_at* descendant."
|
||||
param_group :pagination
|
||||
param :user_id, [Integer, Array], optional: true, desc: "Scope the request to one or various users."
|
||||
example <<-EOS
|
||||
param :user_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various users.'
|
||||
example <<-INVOICES
|
||||
# /open_api/v1/invoices?user_id=211&page=1&per_page=3
|
||||
{
|
||||
"invoices": [
|
||||
@ -64,15 +67,15 @@ class OpenAPI::V1::InvoicesDoc < OpenAPI::V1::BaseDoc
|
||||
}
|
||||
]
|
||||
}
|
||||
EOS
|
||||
INVOICES
|
||||
end
|
||||
|
||||
doc_for :download do
|
||||
api :GET, "/#{API_VERSION}/invoices/:id/download", "Download an invoice"
|
||||
param :id, Integer, desc: "Invoice id", required: true
|
||||
doc_for :download do
|
||||
api :GET, "/#{API_VERSION}/invoices/:id/download", 'Download an invoice'
|
||||
param :id, Integer, desc: 'Invoice id', required: true
|
||||
|
||||
example <<-EOS
|
||||
# /open_api/v1/invoices/2809/download
|
||||
EOS
|
||||
end
|
||||
example <<-URL
|
||||
# /open_api/v1/invoices/2809/download
|
||||
URL
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# openAPI documentation for machines endpoint
|
||||
class OpenAPI::V1::MachinesDoc < OpenAPI::V1::BaseDoc
|
||||
resource_description do
|
||||
short 'Machines'
|
||||
@ -7,9 +10,9 @@ class OpenAPI::V1::MachinesDoc < OpenAPI::V1::BaseDoc
|
||||
end
|
||||
|
||||
doc_for :index do
|
||||
api :GET, "/#{API_VERSION}/machines", "Machines index"
|
||||
description "Machines index. Order by *created_at* ascendant."
|
||||
example <<-EOS
|
||||
api :GET, "/#{API_VERSION}/machines", 'Machines index'
|
||||
description 'Machines index. Order by *created_at* ascendant.'
|
||||
example <<-MACHINES
|
||||
# /open_api/v1/machines
|
||||
{
|
||||
"machines": [
|
||||
@ -63,7 +66,7 @@ class OpenAPI::V1::MachinesDoc < OpenAPI::V1::BaseDoc
|
||||
"description": "La fraiseuse numérique Roland Modela MDX-20\r\n\r\nInformations générales :\r\nCette machine est utilisée pour l'usinage et le scannage 3D de précision. Elle permet principalement d'usiner des circuits imprimés et des moules de petite taille. Le faible diamètre des fraises utilisées (Ø 0,3 mm à Ø 6mm) implique que certains temps d'usinages peuvent êtres long (> 12h), c'est pourquoi cette fraiseuse peut être laissée en autonomie toute une nuit afin d'obtenir le plus précis des usinages au FabLab.\r\n\r\nMatériaux usinables :\r\nLes principaux matériaux usinables sont : bois, plâtre, résine, cire usinable, cuivre.\r\n",
|
||||
"spec": "Taille du plateau X/Y : 220 mm x 160 mm\r\nVolume maximal de travail: 203,2 mm (X), 152,4 mm (Y), 60,5 mm (Z)\r\nPrécision usinage: 0,00625 mm\r\nPrécision scannage: réglable de 0,05 à 5 mm (axes X,Y) et 0,025 mm (axe Z)\r\nVitesse d'analyse (scannage): 4-15 mm/sec\r\n \r\n \r\nLogiciel utilisé pour le fraisage: Roland Modela player 4 \r\nLogiciel utilisé pour l'usinage de circuits imprimés: Cad.py (linux)\r\nFormats acceptés: STL,PNG 3D\r\nFormat d'exportation des données scannées: DXF, VRML, STL, 3DMF, IGES, Grayscale, Point Group et BMP\r\n"
|
||||
},
|
||||
#
|
||||
#
|
||||
# ....
|
||||
#
|
||||
{
|
||||
@ -78,6 +81,6 @@ class OpenAPI::V1::MachinesDoc < OpenAPI::V1::BaseDoc
|
||||
}
|
||||
]
|
||||
}
|
||||
EOS
|
||||
MACHINES
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# openAPI documentation for reservations endpoint
|
||||
class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc
|
||||
resource_description do
|
||||
short 'Reservations'
|
||||
@ -9,14 +12,14 @@ class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc
|
||||
include OpenAPI::V1::Concerns::ParamGroups
|
||||
|
||||
doc_for :index do
|
||||
api :GET, "/#{API_VERSION}/reservations", "Reservations index"
|
||||
description "Index of reservations made by users, with optional pagination. Order by *created_at* descendant."
|
||||
api :GET, "/#{API_VERSION}/reservations", 'Reservations index'
|
||||
description 'Index of reservations made by users, with optional pagination. Order by *created_at* descendant.'
|
||||
param_group :pagination
|
||||
param :user_id, [Integer, Array], optional: true, desc: "Scope the request to one or various users."
|
||||
param :reservable_type, ['Event', 'Machine', 'Training'], optional: true, desc: "Scope the request to a specific type of reservable."
|
||||
param :reservable_id, [Integer, Array], optional: true, desc: "Scope the request to one or various reservables."
|
||||
param :user_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various users.'
|
||||
param :reservable_type, %w[Event Machine Training], optional: true, desc: 'Scope the request to a specific type of reservable.'
|
||||
param :reservable_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various reservables.'
|
||||
|
||||
example <<-EOS
|
||||
example <<-RESERVATIONS
|
||||
# /open_api/v1/reservations?reservable_type=Event&page=1&per_page=3
|
||||
{
|
||||
"reservations": [
|
||||
@ -85,6 +88,6 @@ class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc
|
||||
}
|
||||
]
|
||||
}
|
||||
EOS
|
||||
RESERVATIONS
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# openAPI documentation for trainings endpoint
|
||||
class OpenAPI::V1::TrainingsDoc < OpenAPI::V1::BaseDoc
|
||||
resource_description do
|
||||
short 'Trainings'
|
||||
@ -7,9 +10,9 @@ class OpenAPI::V1::TrainingsDoc < OpenAPI::V1::BaseDoc
|
||||
end
|
||||
|
||||
doc_for :index do
|
||||
api :GET, "/#{API_VERSION}/trainings", "Trainings index"
|
||||
description "Trainings index. Order by *created_at* ascendant."
|
||||
example <<-EOS
|
||||
api :GET, "/#{API_VERSION}/trainings", 'Trainings index'
|
||||
description 'Trainings index. Order by *created_at* ascendant.'
|
||||
example <<-TRAININGS
|
||||
# /open_api/v1/trainings
|
||||
{
|
||||
"trainings": [
|
||||
@ -75,6 +78,6 @@ class OpenAPI::V1::TrainingsDoc < OpenAPI::V1::BaseDoc
|
||||
}
|
||||
]
|
||||
}
|
||||
EOS
|
||||
TRAININGS
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# openAPI documentation for user's trainings endpoint
|
||||
class OpenAPI::V1::UserTrainingsDoc < OpenAPI::V1::BaseDoc
|
||||
resource_description do
|
||||
short 'User trainings'
|
||||
@ -9,12 +12,12 @@ class OpenAPI::V1::UserTrainingsDoc < OpenAPI::V1::BaseDoc
|
||||
include OpenAPI::V1::Concerns::ParamGroups
|
||||
|
||||
doc_for :index do
|
||||
api :GET, "/#{API_VERSION}/user_trainings", "User trainings index"
|
||||
description "Index of trainings accomplished by users, with optional pagination. Order by *created_at* descendant."
|
||||
api :GET, "/#{API_VERSION}/user_trainings", 'User trainings index'
|
||||
description 'Index of trainings accomplished by users, with optional pagination. Order by *created_at* descendant.'
|
||||
param_group :pagination
|
||||
param :training_id, [Integer, Array], optional: true, desc: "Scope the request to one or various trainings."
|
||||
param :user_id, [Integer, Array], optional: true, desc: "Scope the request to one or various users."
|
||||
example <<-EOS
|
||||
param :training_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various trainings.'
|
||||
param :user_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various users.'
|
||||
example <<-TRAININGS
|
||||
# /open_api/v1/user_trainings?training_id[]=3&training_id[]=4&page=1&per_page=2
|
||||
{
|
||||
"user_trainings": [
|
||||
@ -94,6 +97,6 @@ class OpenAPI::V1::UserTrainingsDoc < OpenAPI::V1::BaseDoc
|
||||
}
|
||||
]
|
||||
}
|
||||
EOS
|
||||
TRAININGS
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# openAPI documentation for user endpoint
|
||||
class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc
|
||||
resource_description do
|
||||
short 'Users'
|
||||
@ -9,12 +12,12 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc
|
||||
include OpenAPI::V1::Concerns::ParamGroups
|
||||
|
||||
doc_for :index do
|
||||
api :GET, "/#{API_VERSION}/users", "Users index"
|
||||
description "Users index, with optional pagination. Order by *created_at* descendant."
|
||||
api :GET, "/#{API_VERSION}/users", 'Users index'
|
||||
description 'Users index, with optional pagination. Order by *created_at* descendant.'
|
||||
param_group :pagination
|
||||
param :email, [String, Array], optional: true, desc: "Filter users by *email* using strict matching."
|
||||
param :user_id, [Integer, Array], optional: true, desc: "Filter users by *id* using strict matching."
|
||||
example <<-EOS
|
||||
param :email, [String, Array], optional: true, desc: 'Filter users by *email* using strict matching.'
|
||||
param :user_id, [Integer, Array], optional: true, desc: 'Filter users by *id* using strict matching.'
|
||||
example <<-USERS
|
||||
# /open_api/v1/users?page=1&per_page=4
|
||||
{
|
||||
"users": [
|
||||
@ -92,6 +95,6 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc
|
||||
}
|
||||
]
|
||||
}
|
||||
EOS
|
||||
USERS
|
||||
end
|
||||
end
|
||||
|
146
app/models/accounting_period.rb
Normal file
146
app/models/accounting_period.rb
Normal 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
|
@ -1,5 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Event PDF attachements
|
||||
class EventFile < Asset
|
||||
mount_uploader :attachment, ProjectCaoUploader
|
||||
mount_uploader :attachment, EventFileUploader
|
||||
|
||||
validates :attachment, file_size: { maximum: 20.megabytes.to_i }
|
||||
end
|
||||
|
@ -1,4 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'checksum'
|
||||
|
||||
# Setting values, kept history of modifications
|
||||
class HistoryValue < ActiveRecord::Base
|
||||
belongs_to :setting
|
||||
belongs_to :user
|
||||
|
||||
def chain_record
|
||||
self.footprint = compute_footprint
|
||||
save!
|
||||
end
|
||||
|
||||
def check_footprint
|
||||
footprint == compute_footprint
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def compute_footprint
|
||||
max_date = created_at || Time.current
|
||||
previous = HistoryValue.where('created_at < ?', max_date)
|
||||
.order('created_at DESC')
|
||||
.limit(1)
|
||||
|
||||
columns = HistoryValue.columns.map(&:name)
|
||||
.delete_if { |c| %w[footprint updated_at].include? c }
|
||||
|
||||
Checksum.text("#{columns.map { |c| self[c] }.join}#{previous.first ? previous.first.footprint : ''}")
|
||||
end
|
||||
end
|
||||
|
@ -1,5 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'checksum'
|
||||
|
||||
# Invoice correspond to a single purchase made by an user. This purchase may
|
||||
# include reservation(s) and/or a subscription
|
||||
class Invoice < ActiveRecord::Base
|
||||
@ -15,10 +17,14 @@ class Invoice < ActiveRecord::Base
|
||||
belongs_to :coupon
|
||||
|
||||
has_one :avoir, class_name: 'Invoice', foreign_key: :invoice_id, dependent: :destroy
|
||||
belongs_to :operator, foreign_key: :operator_id, class_name: 'User'
|
||||
|
||||
after_create :update_reference
|
||||
before_create :add_environment
|
||||
after_create :update_reference, :chain_record
|
||||
after_commit :generate_and_send_invoice, on: [:create], if: :persisted?
|
||||
|
||||
validates_with ClosedPeriodValidator
|
||||
|
||||
def file
|
||||
dir = "invoices/#{user.id}"
|
||||
|
||||
@ -211,6 +217,19 @@ class Invoice < ActiveRecord::Base
|
||||
total - (wallet_amount || 0)
|
||||
end
|
||||
|
||||
def add_environment
|
||||
self.environment = Rails.env
|
||||
end
|
||||
|
||||
def chain_record
|
||||
self.footprint = compute_footprint
|
||||
save!
|
||||
end
|
||||
|
||||
def check_footprint
|
||||
invoice_items.map(&:check_footprint).all? && footprint == compute_footprint
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_and_send_invoice
|
||||
@ -256,4 +275,16 @@ class Invoice < ActiveRecord::Base
|
||||
Invoice.where('created_at >= :start_date AND created_at < :end_date', start_date: start, end_date: ending).length
|
||||
end
|
||||
|
||||
def compute_footprint
|
||||
max_date = created_at || DateTime.now
|
||||
previous = Invoice.where('created_at < ?', max_date)
|
||||
.order('created_at DESC')
|
||||
.limit(1)
|
||||
|
||||
columns = Invoice.columns.map(&:name)
|
||||
.delete_if { |c| %w[footprint updated_at].include? c }
|
||||
|
||||
Checksum.text("#{columns.map { |c| self[c] }.join}#{previous.first ? previous.first.footprint : ''}")
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -1,9 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'checksum'
|
||||
|
||||
# A single line inside an invoice. Can be a subscription or a reservation
|
||||
class InvoiceItem < ActiveRecord::Base
|
||||
belongs_to :invoice
|
||||
belongs_to :subscription
|
||||
|
||||
has_one :invoice_item # to associated invoice_items of an invoice to invoice_items of an avoir
|
||||
|
||||
after_create :chain_record
|
||||
|
||||
def chain_record
|
||||
self.footprint = compute_footprint
|
||||
save!
|
||||
end
|
||||
|
||||
def check_footprint
|
||||
footprint == compute_footprint
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def compute_footprint
|
||||
max_date = created_at || Time.current
|
||||
previous = InvoiceItem.where('created_at < ?', max_date)
|
||||
.order('created_at DESC')
|
||||
.limit(1)
|
||||
|
||||
columns = InvoiceItem.columns.map(&:name)
|
||||
.delete_if { |c| %w[footprint updated_at].include? c }
|
||||
|
||||
Checksum.text("#{columns.map { |c| self[c] }.join}#{previous.first ? previous.first.footprint : ''}")
|
||||
end
|
||||
end
|
||||
|
@ -43,6 +43,7 @@ class NotificationType
|
||||
notify_member_about_coupon
|
||||
notify_member_reservation_reminder
|
||||
notify_admin_free_disk_space
|
||||
notify_admin_close_period_reminder
|
||||
]
|
||||
# deprecated:
|
||||
# - notify_member_subscribed_plan_is_changed
|
||||
|
@ -224,10 +224,10 @@ class Reservation < ActiveRecord::Base
|
||||
invoice_items
|
||||
end
|
||||
|
||||
def save_with_payment(coupon_code = nil)
|
||||
def save_with_payment(operator_id, coupon_code = nil)
|
||||
begin
|
||||
clean_pending_strip_invoice_items
|
||||
build_invoice(user: user)
|
||||
build_invoice(user: user, operator_id: operator_id)
|
||||
invoice_items = generate_invoice_items(false, coupon_code)
|
||||
rescue StandardError => e
|
||||
logger.error e
|
||||
@ -242,7 +242,7 @@ class Reservation < ActiveRecord::Base
|
||||
if plan_id
|
||||
self.subscription = Subscription.find_or_initialize_by(user_id: user.id)
|
||||
subscription.attributes = { plan_id: plan_id, user_id: user.id, card_token: card_token, expiration_date: nil }
|
||||
if subscription.save_with_payment(false)
|
||||
if subscription.save_with_payment(operator_id, false)
|
||||
self.stp_invoice_id = invoice_items.first.refresh.invoice
|
||||
invoice.stp_invoice_id = invoice_items.first.refresh.invoice
|
||||
invoice.invoice_items.push InvoiceItem.new(
|
||||
@ -368,8 +368,8 @@ class Reservation < ActiveRecord::Base
|
||||
pending_invoice_items.each(&:delete)
|
||||
end
|
||||
|
||||
def save_with_local_payment(coupon_code = nil)
|
||||
build_invoice(user: user)
|
||||
def save_with_local_payment(operator_id, coupon_code = nil)
|
||||
build_invoice(user: user, operator_id: operator_id)
|
||||
generate_invoice_items(true, coupon_code)
|
||||
|
||||
return false unless valid?
|
||||
@ -377,7 +377,7 @@ class Reservation < ActiveRecord::Base
|
||||
if plan_id
|
||||
self.subscription = Subscription.find_or_initialize_by(user_id: user.id)
|
||||
subscription.attributes = { plan_id: plan_id, user_id: user.id, expiration_date: nil }
|
||||
if subscription.save_with_local_payment(false)
|
||||
if subscription.save_with_local_payment(operator_id, false)
|
||||
invoice.invoice_items.push InvoiceItem.new(
|
||||
amount: subscription.plan.amount,
|
||||
description: subscription.plan.name,
|
||||
|
@ -20,7 +20,7 @@ class Subscription < ActiveRecord::Base
|
||||
# Stripe subscription payment
|
||||
# @params [invoice] if true then subscription pay itself, dont pay with reservation
|
||||
# if false then subscription pay with reservation
|
||||
def save_with_payment(invoice = true, coupon_code = nil)
|
||||
def save_with_payment(operator_id, invoice = true, coupon_code = nil)
|
||||
return unless valid?
|
||||
|
||||
begin
|
||||
@ -75,7 +75,7 @@ class Subscription < ActiveRecord::Base
|
||||
# generate invoice
|
||||
stp_invoice = Stripe::Invoice.all(customer: user.stp_customer_id, limit: 1).data.first
|
||||
if invoice
|
||||
db_invoice = generate_invoice(stp_invoice.id, coupon_code)
|
||||
db_invoice = generate_invoice(operator_id, stp_invoice.id, coupon_code)
|
||||
# debit wallet
|
||||
wallet_transaction = debit_user_wallet
|
||||
if wallet_transaction
|
||||
@ -129,7 +129,7 @@ class Subscription < ActiveRecord::Base
|
||||
|
||||
# @params [invoice] if true then only the subscription is payed, without reservation
|
||||
# if false then the subscription is payed with reservation
|
||||
def save_with_local_payment(invoice = true, coupon_code = nil)
|
||||
def save_with_local_payment(operator_id, invoice = true, coupon_code = nil)
|
||||
return false unless valid?
|
||||
|
||||
set_expiration_date
|
||||
@ -142,7 +142,7 @@ class Subscription < ActiveRecord::Base
|
||||
# debit wallet
|
||||
wallet_transaction = debit_user_wallet
|
||||
|
||||
invoc = generate_invoice(nil, coupon_code)
|
||||
invoc = generate_invoice(operator_id, nil, coupon_code)
|
||||
if wallet_transaction
|
||||
invoc.wallet_amount = @wallet_amount_debit
|
||||
invoc.wallet_transaction_id = wallet_transaction.id
|
||||
@ -152,7 +152,7 @@ class Subscription < ActiveRecord::Base
|
||||
true
|
||||
end
|
||||
|
||||
def generate_invoice(stp_invoice_id = nil, coupon_code = nil)
|
||||
def generate_invoice(operator_id, stp_invoice_id = nil, coupon_code = nil)
|
||||
coupon_id = nil
|
||||
total = plan.amount
|
||||
|
||||
@ -165,13 +165,13 @@ class Subscription < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
invoice = Invoice.new(invoiced_id: id, invoiced_type: 'Subscription', user: user, total: total, stp_invoice_id: stp_invoice_id, coupon_id: coupon_id)
|
||||
invoice = Invoice.new(invoiced_id: id, invoiced_type: 'Subscription', user: user, total: total, stp_invoice_id: stp_invoice_id, coupon_id: coupon_id, operator_id: operator_id)
|
||||
invoice.invoice_items.push InvoiceItem.new(amount: plan.amount, stp_invoice_item_id: stp_subscription_id, description: plan.name, subscription_id: self.id)
|
||||
invoice
|
||||
end
|
||||
|
||||
def generate_and_save_invoice(stp_invoice_id = nil)
|
||||
generate_invoice(stp_invoice_id).save
|
||||
def generate_and_save_invoice(operator_id, stp_invoice_id = nil)
|
||||
generate_invoice(operator_id, stp_invoice_id).save
|
||||
end
|
||||
|
||||
def cancel
|
||||
|
@ -8,7 +8,7 @@ class User < ActiveRecord::Base
|
||||
# Include default devise modules. Others available are:
|
||||
# :lockable, :timeoutable and :omniauthable
|
||||
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable,
|
||||
:confirmable, :async
|
||||
:confirmable
|
||||
rolify
|
||||
|
||||
# enable OmniAuth authentication only if needed
|
||||
@ -44,6 +44,7 @@ class User < ActiveRecord::Base
|
||||
has_many :machine_credits, through: :users_credits, source: :machine_credit
|
||||
|
||||
has_many :invoices, dependent: :destroy
|
||||
has_many :operated_invoices, foreign_key: :operator_id, class_name: 'Invoice', dependent: :nullify
|
||||
|
||||
has_many :user_tags, dependent: :destroy
|
||||
has_many :tags, through: :user_tags
|
||||
@ -92,6 +93,12 @@ class User < ActiveRecord::Base
|
||||
User.with_role(:admin)
|
||||
end
|
||||
|
||||
def self.superadmin
|
||||
return unless Rails.application.secrets.superadmin_email.present?
|
||||
|
||||
User.find_by(email: Rails.application.secrets.superadmin_email)
|
||||
end
|
||||
|
||||
def training_machine?(machine)
|
||||
return true if admin?
|
||||
|
||||
@ -124,10 +131,10 @@ class User < ActiveRecord::Base
|
||||
my_projects.to_a.concat projects
|
||||
end
|
||||
|
||||
def generate_subscription_invoice
|
||||
def generate_subscription_invoice(operator_id)
|
||||
return unless subscription
|
||||
|
||||
subscription.generate_and_save_invoice
|
||||
subscription.generate_and_save_invoice(operator_id)
|
||||
end
|
||||
|
||||
def stripe_customer
|
||||
@ -318,6 +325,10 @@ class User < ActiveRecord::Base
|
||||
create_wallet
|
||||
end
|
||||
|
||||
def send_devise_notification(notification, *args)
|
||||
devise_mailer.send(notification, self, *args).deliver_later
|
||||
end
|
||||
|
||||
def notify_admin_when_user_is_created
|
||||
if need_completion? && !provider.nil?
|
||||
NotificationCenter.call type: 'notify_admin_when_user_is_imported',
|
||||
|
BIN
app/pdfs/data/watermark-en.png
Normal file
BIN
app/pdfs/data/watermark-en.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
app/pdfs/data/watermark-es.png
Normal file
BIN
app/pdfs/data/watermark-es.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
app/pdfs/data/watermark-fr.png
Normal file
BIN
app/pdfs/data/watermark-fr.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
app/pdfs/data/watermark-pt.png
Normal file
BIN
app/pdfs/data/watermark-pt.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
app/pdfs/data/watermark.xcf
Normal file
BIN
app/pdfs/data/watermark.xcf
Normal file
Binary file not shown.
@ -334,6 +334,15 @@ class PDF::Invoice < Prawn::Document
|
||||
text line, align: :right, leading: 4, inline_format: true
|
||||
end
|
||||
end
|
||||
|
||||
# factice watermark
|
||||
return unless %w[staging test development].include?(invoice.environment)
|
||||
|
||||
transparent(0.1) do
|
||||
rotate(45, origin: [0, 0]) do
|
||||
image "#{Rails.root}/app/pdfs/data/watermark-#{I18n.locale}.png", at: [90, 150]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
10
app/policies/accounting_period_policy.rb
Normal file
10
app/policies/accounting_period_policy.rb
Normal 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
|
18
app/services/accounting_period_service.rb
Normal file
18
app/services/accounting_period_service.rb
Normal 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
|
@ -31,7 +31,7 @@ class Members::MembersService
|
||||
@member.generate_auth_migration_token if current_user.admin? && AuthProvider.active.providable_type != DatabaseProvider.name
|
||||
|
||||
if @member.save
|
||||
@member.generate_subscription_invoice
|
||||
@member.generate_subscription_invoice(current_user.id)
|
||||
@member.send_confirmation_instructions
|
||||
UsersMailer.delay.notify_user_account_created(@member, @member.password)
|
||||
true
|
||||
|
@ -1,18 +1,19 @@
|
||||
module Reservations
|
||||
class Reserve
|
||||
attr_accessor :user_id
|
||||
attr_accessor :user_id, :operator_id
|
||||
|
||||
def initialize(user_id)
|
||||
def initialize(user_id, operator_id)
|
||||
@user_id = user_id
|
||||
@operator_id = operator_id
|
||||
end
|
||||
|
||||
def pay_and_save(reservation, payment_method, coupon)
|
||||
reservation.user_id = user_id
|
||||
if payment_method == :local
|
||||
reservation.save_with_local_payment(coupon)
|
||||
reservation.save_with_local_payment(operator_id, coupon)
|
||||
elsif payment_method == :stripe
|
||||
reservation.save_with_payment(coupon)
|
||||
reservation.save_with_payment(operator_id, coupon)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,17 +1,18 @@
|
||||
module Subscriptions
|
||||
class Subscribe
|
||||
attr_accessor :user_id
|
||||
attr_accessor :user_id, :operator_id
|
||||
|
||||
def initialize(user_id)
|
||||
def initialize(user_id, operator_id)
|
||||
@user_id = user_id
|
||||
@operator_id = operator_id
|
||||
end
|
||||
|
||||
def pay_and_save(subscription, payment_method, coupon, invoice)
|
||||
subscription.user_id = user_id
|
||||
if payment_method == :local
|
||||
subscription.save_with_local_payment(invoice, coupon)
|
||||
subscription.save_with_local_payment(operator_id, invoice, coupon)
|
||||
elsif payment_method == :stripe
|
||||
subscription.save_with_payment(invoice, coupon)
|
||||
subscription.save_with_payment(operator_id, invoice, coupon)
|
||||
end
|
||||
end
|
||||
|
||||
@ -24,7 +25,7 @@ module Subscriptions
|
||||
expiration_date: new_expiration_date
|
||||
)
|
||||
if new_sub.save
|
||||
new_sub.user.generate_subscription_invoice
|
||||
new_sub.user.generate_subscription_invoice(operator_id)
|
||||
return new_sub
|
||||
end
|
||||
false
|
||||
|
52
app/uploaders/event_file_uploader.rb
Normal file
52
app/uploaders/event_file_uploader.rb
Normal 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
|
17
app/validators/closed_period_validator.rb
Normal file
17
app/validators/closed_period_validator.rb
Normal 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
|
12
app/validators/date_range_validator.rb
Normal file
12
app/validators/date_range_validator.rb
Normal 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
|
17
app/validators/period_integrity_validator.rb
Normal file
17
app/validators/period_integrity_validator.rb
Normal 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
|
21
app/validators/period_overlap_validator.rb
Normal file
21
app/validators/period_overlap_validator.rb
Normal 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
|
9
app/views/api/accounting_periods/index.json.jbuilder
Normal file
9
app/views/api/accounting_periods/index.json.jbuilder
Normal 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
|
@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.last_end_date @last_end
|
3
app/views/api/accounting_periods/show.json.jbuilder
Normal file
3
app/views/api/accounting_periods/show.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! @accounting_period, :id, :start_at, :end_at, :closed_at, :closed_by, :created_at
|
@ -4,9 +4,9 @@ json.link_to_sso_profile @provider.link_to_sso_profile
|
||||
if @provider.providable_type == DatabaseProvider.name
|
||||
json.link_to_sso_connect '/#'
|
||||
else
|
||||
json.link_to_sso_connect user_omniauth_authorize_path(@provider.strategy_name.to_sym)
|
||||
json.link_to_sso_connect "/users/auth/#{@provider.strategy_name}"
|
||||
end
|
||||
|
||||
if @provider.providable_type == OAuth2Provider.name
|
||||
json.domain @provider.providable.domain
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! event, :id, :title, :description, :age_range_id
|
||||
json.event_image event.event_image.attachment_url if event.event_image
|
||||
json.event_files_attributes event.event_files do |f|
|
||||
@ -6,18 +8,22 @@ json.event_files_attributes event.event_files do |f|
|
||||
json.attachment_url f.attachment_url
|
||||
end
|
||||
json.category_id event.category_id
|
||||
json.category do
|
||||
json.id event.category.id
|
||||
json.name event.category.name
|
||||
end if event.category
|
||||
if event.category
|
||||
json.category do
|
||||
json.id event.category.id
|
||||
json.name event.category.name
|
||||
end
|
||||
end
|
||||
json.event_theme_ids event.event_theme_ids
|
||||
json.event_themes event.event_themes do |e|
|
||||
json.name e.name
|
||||
end
|
||||
json.age_range_id event.age_range_id
|
||||
json.age_range do
|
||||
json.name event.age_range.name
|
||||
end if event.age_range
|
||||
if event.age_range
|
||||
json.age_range do
|
||||
json.name event.age_range.name
|
||||
end
|
||||
end
|
||||
json.start_date event.availability.start_at
|
||||
json.start_time event.availability.start_at
|
||||
json.end_date event.availability.end_at
|
||||
@ -25,7 +31,7 @@ json.end_time event.availability.end_at
|
||||
json.month t('date.month_names')[event.availability.start_at.month]
|
||||
json.month_id event.availability.start_at.month
|
||||
json.year event.availability.start_at.year
|
||||
json.all_day event.availability.start_at.hour == 0 ? 'true' : 'false'
|
||||
json.all_day event.availability.start_at.hour.zero? ? 'true' : 'false'
|
||||
json.availability do
|
||||
json.id event.availability.id
|
||||
json.start_at event.availability.start_at
|
||||
|
@ -12,4 +12,5 @@ json.array!(@invoices) do |invoice|
|
||||
json.stripe invoice.stp_invoice_id?
|
||||
json.date invoice.is_a?(Avoir) ? invoice.avoir_date : invoice.created_at
|
||||
json.prevent_refund invoice.prevent_refund?
|
||||
json.chained_footprint invoice.check_footprint
|
||||
end
|
||||
|
@ -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)
|
@ -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)
|
@ -9,14 +9,9 @@
|
||||
|
||||
<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+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'>
|
||||
<% end %>
|
||||
<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=Loved+by+the+King' rel='stylesheet' type='text/css'>
|
||||
<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
|
||||
<script type="text/javascript">
|
||||
Stripe.setPublishableKey('<%= Rails.application.secrets.stripe_publishable_key %>');
|
||||
@ -28,6 +23,7 @@
|
||||
Fablab.disqusShortname = "<%= Rails.application.secrets.disqus_shortname %>";
|
||||
Fablab.defaultHost = "<%= Rails.application.secrets.default_host %>";
|
||||
Fablab.gaId = "<%= Rails.application.secrets.google_analytics_id %>";
|
||||
Fablab.superadminId = parseInt("<%= User.superadmin&.id %>", 10);
|
||||
|
||||
// i18n stuff
|
||||
Fablab.locale = "<%= Rails.application.secrets.app_locale %>";
|
||||
|
47
app/views/archive/_accounting.json.jbuilder
Normal file
47
app/views/archive/_accounting.json.jbuilder
Normal 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
|
23
app/views/archive/_reservation.json.jbuilder
Normal file
23
app/views/archive/_reservation.json.jbuilder
Normal 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
|
9
app/views/archive/_subscription.json.jbuilder
Normal file
9
app/views/archive/_subscription.json.jbuilder
Normal 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
|
7
app/views/archive/_vat.json.jbuilder
Normal file
7
app/views/archive/_vat.json.jbuilder
Normal 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
|
@ -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 %>
|
||||
|
@ -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>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<% active_provider = AuthProvider.active %>
|
||||
<%= 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] == '/'
|
||||
url_path = root_url + url_path[1..-1]
|
||||
else
|
||||
|
@ -38,7 +38,7 @@
|
||||
|
||||
<p>
|
||||
<%= 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 )%>
|
||||
</a>
|
||||
</p>
|
||||
|
12
app/workers/close_period_reminder_worker.rb
Normal file
12
app/workers/close_period_reminder_worker.rb
Normal 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
|
23
app/workers/free_disk_space_worker.rb
Normal file
23
app/workers/free_disk_space_worker.rb
Normal 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
|
@ -61,6 +61,8 @@ OPENLAB_APP_ID:
|
||||
OPENLAB_BASE_URI: 'https://openprojects.fab-manager.com'
|
||||
|
||||
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_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
|
||||
|
121
config/deploy.rb
121
config/deploy.rb
@ -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
|
@ -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'
|
@ -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
|
@ -24,8 +24,8 @@ Rails.application.configure do
|
||||
|
||||
# Compress JavaScripts and CSS.
|
||||
config.assets.js_compressor = :uglifier
|
||||
# if you want disable variable name mangling
|
||||
config.assets.js_compressor = Uglifier.new(mangle: false)
|
||||
# if you want disable variable name mangling and enable ES6 support
|
||||
config.assets.js_compressor = Uglifier.new(mangle: false, harmony: true)
|
||||
|
||||
# config.assets.css_compressor = :sass
|
||||
|
||||
|
@ -1,5 +0,0 @@
|
||||
Devise::Async.setup do |config|
|
||||
config.enabled = true
|
||||
config.backend = :sidekiq
|
||||
config.queue = :devise_mailer
|
||||
end
|
@ -285,6 +285,7 @@ en:
|
||||
invoices:
|
||||
# list of all invoices & invoicing parameters
|
||||
invoices: "Invoices"
|
||||
accounting_periods: "Accounting periods"
|
||||
invoices_list: "Invoices list"
|
||||
filter_invoices: "Filter invoices"
|
||||
invoice_#_: "Invoice #:"
|
||||
@ -406,6 +407,24 @@ en:
|
||||
logo_successfully_saved: "Logo successfully saved."
|
||||
an_error_occurred_while_saving_the_logo: "An error occurred while saving the logo."
|
||||
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:
|
||||
# management of users, labels, groups, and so on
|
||||
|
@ -285,6 +285,7 @@ es:
|
||||
invoices:
|
||||
# list of all invoices & invoicing parameters
|
||||
invoices: "Facturas"
|
||||
accounting_periods: "Accounting periods" # missing translation
|
||||
invoices_list: "Lista de facturas"
|
||||
filter_invoices: "Filtrar facturas"
|
||||
invoice_#_: "Factura #:"
|
||||
@ -406,6 +407,24 @@ es:
|
||||
logo_successfully_saved: "Logo guardado correctamente."
|
||||
an_error_occurred_while_saving_the_logo: "Se ha producido un error al guardar el logotipo.."
|
||||
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:
|
||||
# management of users, labels, groups, and so on
|
||||
|
@ -285,6 +285,7 @@ fr:
|
||||
invoices:
|
||||
# liste de toutes les factures & paramètres de facturation
|
||||
invoices: "Factures"
|
||||
accounting_periods: "Périodes comptables"
|
||||
invoices_list: "Liste des factures"
|
||||
filter_invoices: "Filtrer les factures"
|
||||
invoice_#_: "Facture n° :"
|
||||
@ -406,6 +407,24 @@ fr:
|
||||
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."
|
||||
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:
|
||||
# gestion des utilisateurs, des groupes, des étiquettes, etc.
|
||||
|
@ -285,6 +285,7 @@ pt:
|
||||
invoices:
|
||||
# list of all invoices & invoicing parameters
|
||||
invoices: "Faturas"
|
||||
accounting_periods: "Accounting periods" # missing translation
|
||||
invoices_list: "Lista de faturas"
|
||||
filter_invoices: "Filtrar faturas"
|
||||
invoice_#_: "Fatura #:"
|
||||
@ -406,6 +407,24 @@ pt:
|
||||
logo_successfully_saved: "Logo salvo com sucesso."
|
||||
an_error_occurred_while_saving_the_logo: "Um erro ocorreu ao salvar o logo."
|
||||
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:
|
||||
# management of users, labels, groups, and so on
|
||||
|
@ -85,6 +85,7 @@ en:
|
||||
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"
|
||||
_the_fablab_policy: "the FabLab policy"
|
||||
field_required: "Field required"
|
||||
|
||||
# password modification modal
|
||||
change_your_password: "Change your password"
|
||||
|
@ -84,6 +84,7 @@ es:
|
||||
i_accept_to_receive_information_from_the_fablab: "Acepto recibir información del FabLab"
|
||||
i_ve_read_and_i_accept_: "He leido y acepto"
|
||||
_the_fablab_policy: "la política de FabLab"
|
||||
field_required: "Field required" #translation_missing
|
||||
|
||||
# password modification modal
|
||||
change_your_password: "Cambiar contraseña"
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user