1
0
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:
Sylvain 2019-03-28 11:33:17 +01:00
commit d29ae1587b
166 changed files with 10331 additions and 14716 deletions

2
.gitignore vendored
View File

@ -34,7 +34,7 @@
# XLSX exports
/exports/*
# Archives of cLosed accounting periods
# Archives of cLosed accounting periods
/accounting/*
.DS_Store

View File

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

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

@ -0,0 +1,34 @@
Fab-Manager uses some external components, which are licenced under the
terms of the following licences:
- [Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0):
- [jasny-bootstrap](https://github.com/jasny/bootstrap/)
- [elasticsearch](https://github.com/elasticsearch/bower-elasticsearch-js)
- [nvd3](https://github.com/novus/nvd3)
- [angular-bootstrap-switch](https://github.com/frapontillo/angular-bootstrap-switch)
- [elasticsearch-rails](https://github.com/elastic/elasticsearch-rails)
- [elasticsearch-model](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-model)
- [elasticsearch-persistence](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-persistence)
- font [Open Sans](http://www.fontsquirrel.com/fonts/open-sans)
- [General Public License version 2](http://www.gnu.org/licenses/old-licenses/gpl-2.0-faq.en.html):
- [railroady](https://github.com/preston/railroady)
- [unicorn](https://github.com/defunkt/unicorn)
- [prawn](https://github.com/prawnpdf/prawn)
- [prawn-table](https://github.com/prawnpdf/prawn-table)
- [BSD-2-Clause](https://opensource.org/licenses/BSD-2-Clause)
- [ruby](https://www.ruby-lang.org)
- [rubyzip](https://github.com/rubyzip/rubyzip)
- [byebug](https://github.com/deivid-rodriguez/byebug)
- [MIT Licence](https://opensource.org/licenses/MIT)
- Errors and omissions excepted, all the other external libraries used
in this project.
Please refer to the libraries documentation for more information about
their licences.
Complete lists of used libraries are available in `package.json` for the
JS/EcmaScript libraries and in `Gemfile` for Ruby libraries.

View File

@ -1,5 +1,29 @@
# Changelog Fab Manager
## 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

View File

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

View File

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

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

View File

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

View File

@ -1,4 +1,4 @@
Copyright (C) 2015 La Casemate
Copyright (C) 2019 Sleede
This program is free software: you can redistribute it and/or modify
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

View File

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

View File

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

View File

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

View File

@ -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" %>',

View File

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

View File

@ -0,0 +1,12 @@
'use strict';
Application.Services.factory('AccountingPeriod', ['$resource', function ($resource) {
return $resource('/api/accounting_periods/:id',
{ id: '@id' }, {
lastClosingEnd: {
method: 'GET',
url: '/api/accounting_periods/last_closing_end'
}
}
);
}]);

View File

@ -616,4 +616,8 @@ padding: 10px;
& > i.fileinput-exists {
margin-right: 5px;
}
}
}
.help-block.error {
color: #ff565d;
}

View File

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

View File

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

View File

@ -0,0 +1,31 @@
.signup-form {
.names-row {
input.form-control {
width: 89%;
display: inline-block;
}
}
.required-row {
div.input-group {
width: 95%;
display: inline-table;
}
select.form-control {
width: 95%;
display: inline-block;
}
.exponent {
position: relative;
top: -14px;
right: -4px;
}
.exponent-select {
top: -1px;
}
}
.info-required {
color: #5a5a5a;
font-size: 8pt;
font-style: italic;
}
}

View File

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

View File

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

View File

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

View File

@ -10,7 +10,11 @@
<h1 translate>{{ 'invoices.invoices' }}</h1>
</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>

View File

@ -53,9 +53,9 @@
<a href="#" class="font-sbold label text-md" ng-click="login($event)"><i class="fa fa-sign-in"></i> {{ 'sign_in' | translate }}</a>
</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>

View File

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

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
# API Controller for resources of AccountingPeriod
class API::AccountingPeriodsController < API::ApiController
before_action :authenticate_user!
before_action :set_period, only: %i[show download_archive]
def index
@accounting_periods = AccountingPeriodService.all_periods_with_users
end
def show; end
def create
authorize AccountingPeriod
@accounting_period = AccountingPeriod.new(period_params.merge(closed_at: DateTime.now, closed_by: current_user.id))
if @accounting_period.save
render :show, status: :created, location: @accounting_period
else
render json: @accounting_period.errors, status: :unprocessable_entity
end
end
def last_closing_end
authorize AccountingPeriod
last_period = AccountingPeriodService.find_last_period
if last_period.nil?
invoice = Invoice.order(:created_at).first
@last_end = invoice.created_at if invoice
else
@last_end = last_period.end_at + 1.day
end
end
def download_archive
authorize AccountingPeriod
send_file File.join(Rails.root, @accounting_period.archive_file), type: 'application/json', disposition: 'attachment'
end
private
def set_period
@accounting_period = AccountingPeriod.find(params[:id])
end
def period_params
params.require(:accounting_period).permit(:start_at, :end_at)
end
end

View File

@ -96,7 +96,7 @@ class API::EventsController < API::ApiController
:recurrence_end_at, :category_id, :event_theme_ids, :age_range_id,
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, latelier de fabrication numérique vous ouvre ses portes les mercredis soirs pour avancer vos projets ou rencontrer la «communauté» Fab Lab. \r\n\r\nCe soir, venez spécialement découvrir les machines à commandes numérique du Fab Lab de La Casemate, venez comprendre ce lieux ouvert à tous. \r\n\r\n\r\nVenez découvrir un concept, une organisation, des machines, pour stimuler votre sens de la créativité.",
"description": "Que vous soyez Fab user, visiteur, curieux ou bricoleur, latelier de fabrication numérique vous ouvre ses portes les mercredis soirs pour avancer vos projets ou rencontrer la «communauté» Fab Lab. \r\n\r\nCe soir, venez spécialement découvrir les machines à commandes numérique de la Fabrique de Fab-manager, venez comprendre ce lieux ouvert à tous. \r\n\r\n\r\nVenez découvrir un concept, une organisation, des machines, pour stimuler votre sens de la créativité.",
"updated_at": "2016-04-25T10:49:40.055+02:00",
"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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,146 @@
# frozen_string_literal: true
require 'checksum'
require 'version'
require 'zip'
# AccountingPeriod is a period of N days (N > 0) which as been closed by an admin
# to prevent writing new accounting lines (invoices & refunds) during this period of time.
class AccountingPeriod < ActiveRecord::Base
before_destroy { false }
before_update { false }
before_create :compute_totals
after_create :archive_closed_data
validates :start_at, :end_at, :closed_at, :closed_by, presence: true
validates_with DateRangeValidator
validates_with PeriodOverlapValidator
validates_with PeriodIntegrityValidator
def delete
false
end
def invoices
Invoice.where('created_at >= :start_date AND CAST(created_at AS DATE) <= :end_date', start_date: start_at, end_date: end_at)
end
def invoices_with_vat(invoices)
invoices.map do |i|
if i.type == 'Avoir'
{ invoice: i, vat_rate: vat_rate(i.avoir_date) }
else
{ invoice: i, vat_rate: vat_rate(i.created_at) }
end
end
end
def archive_folder
dir = "accounting/#{id}"
# create directory if it doesn't exists (accounting)
FileUtils.mkdir_p dir
dir
end
def archive_file
"#{archive_folder}/#{start_at.iso8601}_#{end_at.iso8601}.zip"
end
def archive_json_file
"#{start_at.iso8601}_#{end_at.iso8601}.json"
end
def check_footprint
footprint == compute_footprint
end
def vat_rate(date)
@vat_rates = vat_history if @vat_rates.nil?
first_rate = @vat_rates.first
return first_rate[:rate] if date < first_rate[:date]
@vat_rates.each do |h|
return h[:rate] if h[:date] <= date
end
end
private
def vat_history
key_dates = []
Setting.find_by(name: 'invoice_VAT-rate').history_values.each do |rate|
key_dates.push(date: rate.created_at, rate: (rate.value.to_i / 100.0))
end
Setting.find_by(name: 'invoice_VAT-active').history_values.each do |v|
key_dates.push(date: v.created_at, rate: 0) if v.value == 'false'
end
key_dates.sort_by { |k| k[:date] }
end
def to_json_archive(invoices, previous_file, last_checksum)
code_checksum = Checksum.code
ApplicationController.new.view_context.render(
partial: 'archive/accounting',
locals: {
invoices: invoices_with_vat(invoices),
period_total: period_total,
perpetual_total: perpetual_total,
period_footprint: footprint,
code_checksum: code_checksum,
last_archive_checksum: last_checksum,
previous_file: previous_file,
software_version: Version.current,
date: Time.now.iso8601
},
formats: [:json],
handlers: [:jbuilder]
)
end
def previous_period
AccountingPeriod.where('closed_at < ?', closed_at).order(closed_at: :desc).limit(1).last
end
def archive_closed_data
data = invoices.includes(:invoice_items)
previous_file = previous_period&.archive_file
last_archive_checksum = previous_file ? Checksum.file(previous_file) : nil
json_data = to_json_archive(data, previous_file, last_archive_checksum)
current_archive_checksum = Checksum.text(json_data)
Zip::OutputStream.open(archive_file) do |io|
io.put_next_entry(archive_json_file)
io.write(json_data)
io.put_next_entry('checksum.sha256')
io.write("#{current_archive_checksum}\t#{archive_json_file}")
io.put_next_entry('chained.sha256')
io.write(Checksum.text("#{current_archive_checksum}#{last_archive_checksum}#{DateTime.iso8601}"))
end
end
def price_without_taxe(invoice)
invoice[:invoice].total - (invoice[:invoice].total * invoice[:vat_rate])
end
def compute_totals
period_invoices = invoices_with_vat(invoices.where(type: nil))
period_avoirs = invoices_with_vat(invoices.where(type: 'Avoir'))
self.period_total = (period_invoices.map(&method(:price_without_taxe)).reduce(:+) || 0) -
(period_avoirs.map(&method(:price_without_taxe)).reduce(:+) || 0)
all_invoices = invoices_with_vat(Invoice.where('CAST(created_at AS DATE) <= :end_date AND type IS NULL', end_date: end_at))
all_avoirs = invoices_with_vat(Invoice.where("CAST(created_at AS DATE) <= :end_date AND type = 'Avoir'", end_date: end_at))
self.perpetual_total = (all_invoices.map(&method(:price_without_taxe)).reduce(:+) || 0) -
(all_avoirs.map(&method(:price_without_taxe)).reduce(:+) || 0)
self.footprint = compute_footprint
end
def compute_footprint
columns = AccountingPeriod.columns.map(&:name)
.delete_if { |c| %w[id footprint created_at updated_at].include? c }
Checksum.text("#{columns.map { |c| self[c] }.join}#{previous_period ? previous_period.footprint : ''}")
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

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

Binary file not shown.

View File

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

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
# Check the access policies for API::AccountingPeriodsController
class AccountingPeriodPolicy < ApplicationPolicy
%w[index show create last_closing_end download_archive].each do |action|
define_method "#{action}?" do
user.admin?
end
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
# Provides methods for accessing AccountingPeriods properties
class AccountingPeriodService
def self.find_last_period
AccountingPeriod.where(end_at: AccountingPeriod.select('max(end_at)')).first
end
def self.all_periods_with_users
AccountingPeriod.joins("INNER JOIN #{User.arel_table.name} ON users.id = accounting_periods.closed_by
INNER JOIN #{Profile.arel_table.name} ON profiles.user_id = users.id")
.select("#{AccountingPeriod.arel_table.name}.*,
#{Profile.arel_table.name}.first_name,
#{Profile.arel_table.name}.last_name")
.order('start_at DESC')
end
end

View File

@ -31,7 +31,7 @@ class Members::MembersService
@member.generate_auth_migration_token if current_user.admin? && AuthProvider.active.providable_type != DatabaseProvider.name
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

View File

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

View File

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

View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
# CarrierWave uploader for event attachments
class EventFileUploader < CarrierWave::Uploader::Base
# Include RMagick or MiniMagick support:
# include CarrierWave::RMagick
# include CarrierWave::MiniMagick
include UploadHelper
# Choose what kind of storage to use for this uploader:
storage :file
after :remove, :delete_empty_dirs
# storage :fog
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:
def store_dir
"#{base_store_dir}/#{model.id}"
end
def base_store_dir
"uploads/#{model.class.to_s.underscore}"
end
# Provide a default URL as a default if there hasn't been a file uploaded:
# def default_url
# # For Rails 3.1+ asset pipeline compatibility:
# # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
#
# "/images/fallback/" + [version_name, "default.png"].compact.join('_')
# end
# Process files as they are uploaded:
# process :scale => [200, 300]
#
# def scale(width, height)
# # do something
# end
# Add a white list of extensions which are allowed to be uploaded.
# For images you might use something like this:
def extension_white_list
%w[pdf]
end
# Override the filename of the uploaded files:
# Avoid using model.id or version_name here, see uploader/store.rb for details.
# def filename
# # "avatar.#{file.extension}" if original_filename
# end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
# Validates the current invoice is not generated within a closed accounting period
class ClosedPeriodValidator < ActiveModel::Validator
def validate(record)
date = if record.is_a?(Avoir)
record.avoir_date
else
DateTime.now
end
AccountingPeriod.all.each do |period|
record.errors[:date] << I18n.t('errors.messages.in_closed_period') if date >= period.start_at && date <= period.end_at
end
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
# Validates that start_at is same or before end_at in the given record
class DateRangeValidator < ActiveModel::Validator
def validate(record)
the_end = record.end_at
the_start = record.start_at
return if the_end.present? && the_end >= the_start
record.errors[:end_at] << I18n.t('errors.messages.end_before_start', START: the_start)
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
# Validates that all invoices in the current accounting period are chained with footprints which ensure their integrity
class PeriodIntegrityValidator < ActiveModel::Validator
def validate(record)
the_end = record.end_at
the_start = record.start_at
invoices = Invoice.where('created_at >= :start_date AND created_at < :end_date', start_date: the_start, end_date: the_end)
.includes(:invoice_items)
invoices.each do |i|
record.errors["invoice_#{i.reference}".to_sym] << I18n.t('errors.messages.invalid_footprint') unless i.check_footprint
end
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
# Validates the current accounting period does not overlap an existing one
class PeriodOverlapValidator < ActiveModel::Validator
def validate(record)
the_end = record.end_at
the_start = record.start_at
AccountingPeriod.all.each do |period|
if the_start >= period.start_at && the_start <= period.end_at
record.errors[:start_at] << I18n.t('errors.messages.cannot_overlap')
end
if the_end >= period.start_at && the_end <= period.end_at
record.errors[:end_at] << I18n.t('errors.messages.cannot_overlap')
end
if period.start_at >= the_start && period.end_at <= the_end
record.errors[:end_at] << I18n.t('errors.messages.cannot_encompass')
end
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
json.array!(@accounting_periods) do |ap|
json.extract! ap, :id, :start_at, :end_at, :closed_at, :closed_by, :footprint, :created_at
json.period_total ap.period_total / 100.0
json.perpetual_total ap.perpetual_total / 100.0
json.chained_footprint ap.check_footprint
json.user_name "#{ap.first_name} #{ap.last_name}"
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
json.title notification.notification_type
if notification.attached_object.class.name == AccountingPeriod.name
json.description t('.warning_last_closed_period_over_1_year', LAST_END: notification.attached_object.end_at)
else
json.description t('.warning_no_closed_periods', FIRST_DATE: notification.attached_object.created_at.to_date)
end
json.url notification_url(notification, format: :json)

View File

@ -0,0 +1,3 @@
json.title notification.notification_type
json.description t('.warning_free_disk_space', AVAILABLE: number_with_delimiter(notification.meta_data['mb_available']))
json.url notification_url(notification, format: :json)

View File

@ -9,14 +9,9 @@
<title><%=Setting.find_by(name: 'fablab_name').value%></title>
<% 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 %>";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@
<% active_provider = AuthProvider.active %>
<%= 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

View File

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

View File

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

View File

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

View File

@ -61,6 +61,8 @@ OPENLAB_APP_ID:
OPENLAB_BASE_URI: 'https://openprojects.fab-manager.com'
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

View File

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

View File

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

View File

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

View File

@ -24,8 +24,8 @@ Rails.application.configure do
# Compress JavaScripts and CSS.
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

View File

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

View File

@ -285,6 +285,7 @@ en:
invoices:
# 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

View File

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

View File

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

View File

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

View File

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

View File

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