mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-18 12:54:27 +01:00
Merge branch 'dev' for release 4.2.0
This commit is contained in:
commit
7d9f42da8f
@ -5,29 +5,50 @@ vendor/cache
|
|||||||
config/database.yml
|
config/database.yml
|
||||||
config/application.yml
|
config/application.yml
|
||||||
|
|
||||||
# Ignore the default SQLite database.
|
# Ignore database files.
|
||||||
db/*.sqlite3
|
db/*.sqlite3
|
||||||
db/*.sqlite3-journal
|
db/*.sqlite3-journal
|
||||||
|
postgresql
|
||||||
|
elasticsearch
|
||||||
|
redis
|
||||||
|
|
||||||
# Ignore all logfiles and tempfiles.
|
|
||||||
log
|
|
||||||
tmp
|
|
||||||
|
|
||||||
|
# Ignore public assets
|
||||||
public/uploads
|
public/uploads
|
||||||
public/assets
|
public/assets
|
||||||
|
|
||||||
|
# Ignore all logfiles and tempfiles.
|
||||||
|
log
|
||||||
|
*.log
|
||||||
|
tmp
|
||||||
|
|
||||||
|
# Ignore platform dependent files
|
||||||
*.DS_Store
|
*.DS_Store
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
# PDF invoices
|
# PDF invoices
|
||||||
invoices
|
invoices
|
||||||
|
|
||||||
|
# Excel exports
|
||||||
|
exports
|
||||||
|
|
||||||
|
# CSV imports
|
||||||
|
imports
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Development files
|
||||||
.vagrant
|
.vagrant
|
||||||
Vagrantfile
|
Vagrantfile
|
||||||
|
provision
|
||||||
.git*
|
.git*
|
||||||
|
|
||||||
Dockerfile
|
Dockerfile
|
||||||
docker-compose*
|
docker-compose*
|
||||||
|
test
|
||||||
|
|
||||||
|
# Docs
|
||||||
|
*.md
|
||||||
|
doc
|
||||||
|
|
||||||
|
# Modules
|
||||||
|
node_modules
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -34,6 +34,9 @@
|
|||||||
# XLSX exports
|
# XLSX exports
|
||||||
/exports/*
|
/exports/*
|
||||||
|
|
||||||
|
# CSV imports
|
||||||
|
/imports/*
|
||||||
|
|
||||||
# Archives of cLosed accounting periods
|
# Archives of cLosed accounting periods
|
||||||
/accounting/*
|
/accounting/*
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
Metrics/LineLength:
|
Metrics/LineLength:
|
||||||
Max: 140
|
Max: 140
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Max: 30
|
Max: 35
|
||||||
Metrics/CyclomaticComplexity:
|
Metrics/CyclomaticComplexity:
|
||||||
Max: 9
|
Max: 13
|
||||||
Metrics/PerceivedComplexity:
|
Metrics/PerceivedComplexity:
|
||||||
Max: 9
|
Max: 9
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
@ -16,6 +16,8 @@ Metrics/BlockLength:
|
|||||||
- 'lib/tasks/**/*.rake'
|
- 'lib/tasks/**/*.rake'
|
||||||
- 'config/routes.rb'
|
- 'config/routes.rb'
|
||||||
- 'app/pdfs/pdf/*.rb'
|
- 'app/pdfs/pdf/*.rb'
|
||||||
|
Metrics/ParameterLists:
|
||||||
|
CountKeywordArgs: false
|
||||||
Style/BracesAroundHashParameters:
|
Style/BracesAroundHashParameters:
|
||||||
EnforcedStyle: context_dependent
|
EnforcedStyle: context_dependent
|
||||||
Style/RegexpLiteral:
|
Style/RegexpLiteral:
|
||||||
|
47
CHANGELOG.md
47
CHANGELOG.md
@ -1,10 +1,49 @@
|
|||||||
# Changelog Fab Manager
|
# Changelog Fab Manager
|
||||||
|
|
||||||
## v4.1.1 2019 september 20
|
## v4.2.0 2019 October 21
|
||||||
|
|
||||||
- fix a bug: api/reservations#index was using user_id instead of statistic_profile_id
|
- Upgraded PostgreSQL from 9.4 to 9.6
|
||||||
- fix a bug: event_service#date_range method, test on all_day was never truthy
|
- Optional reCaptcha checkbox in sign-up form
|
||||||
- fix a bug: sidekiq 5 does not have delay_for method anymore, uses perform_in instead
|
- Ability to configure and export the accounting data to the ACD accounting software
|
||||||
|
- Compute the VAT per item in each invoices, instead of globally
|
||||||
|
- Use Alpine Linux to build the Docker image (#147)
|
||||||
|
- Updated omniauth & omniauth-oauth2 gems
|
||||||
|
- Ability to set project's CAO attachement maximum upload size
|
||||||
|
- Ability to bulk-import members from a CSV file
|
||||||
|
- Ability to disable invoices generation and interfaces
|
||||||
|
- Added a known issue to the README (#152)
|
||||||
|
- Ability to fully rebuild the projets index in ElasticSearch with rake fablab:es:build_projects_index
|
||||||
|
- Ability to configure SMTP connection to use SMTP/TLS
|
||||||
|
- Updated user's manual for v4.2 (fr)
|
||||||
|
- Fix a bug: invoices with total = 0, are marked as paid on site even if paid by card
|
||||||
|
- Fix a bug: after disabling a group, its associated plans are hidden from the interface
|
||||||
|
- Fix a bug: in case of unexpected server error during stripe payment process, the confirm button is not unlocked
|
||||||
|
- Fix a bug: create a plan does not set its name
|
||||||
|
- Fix a bug: unable to dissociate the last machine from a formation
|
||||||
|
- Fix a bug: in profile_complete form, the user's group is not selected by default
|
||||||
|
- Fix a bug: missing asterisks on some required fields in profile_complete form
|
||||||
|
- Fix a bug: public calendar won't show anything if the current date range include a reserved space availability (#151)
|
||||||
|
- Fix a bug: invoices list is not shown by default in "manage invoices" section
|
||||||
|
- Fix a bug: unable to run rake fablab:es:* tasks due to an issue with gem faraday 0.16.x (was updated to 0.17)
|
||||||
|
- Fix a bug: unauthorized user can see the edit project form
|
||||||
|
- Fix a bug: do not display each days in invoices for multiple days event reservation
|
||||||
|
- Fix a security issue: fixed [CVE-2015-9284](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-9284)
|
||||||
|
- [TODO DEPLOY] **IMPORTANT** Please read [postgres_upgrade.md](doc/postgres_upgrade.md) for instructions on upgrading PostgreSQL.
|
||||||
|
- [TODO DEPLOY] `rake db:migrate`
|
||||||
|
- [TODO DEPLOY] -> (only dev) `yarn install` and `bundle install`
|
||||||
|
- [TODO DEPLOY] -> (only dev) configure `DEFAULT_HOST: 'localhost:5000'` and `DEFAULT_PROTOCOL: http` in [application.yml](config/application.yml.default)
|
||||||
|
- [TODO DEPLOY] add the `RECAPTCHA_SITE_KEY` and `RECAPTCHA_SECRET_KEY` environment variables (see [doc/environment.md](doc/environment.md) for configuration details)
|
||||||
|
- [TODO DEPLOY] add the `MAX_CAO_SIZE` environment variable (see [doc/environment.md](doc/environment.md) for configuration details)
|
||||||
|
- [TODO DEPLOY] add the `MAX_IMPORT_SIZE` environment variable (see [doc/environment.md](doc/environment.md) for configuration details)
|
||||||
|
- [TODO DEPLOY] add `- ${PWD}/imports:/usr/src/app/imports` in the volumes list of your fabmanager service in [docker-compose.yml](docker/docker-compose.yml)
|
||||||
|
- [TODO DEPLOY] add the `FABLAB_WITHOUT_INVOICES` environment variable (see [doc/environment.md](doc/environment.md) for configuration details)
|
||||||
|
- [TODO DEPLOY] add the following environment variables: `SMTP_TLS`
|
||||||
|
|
||||||
|
## v4.1.1 2019 September 20
|
||||||
|
|
||||||
|
- Fix a bug: api/reservations#index was using user_id instead of statistic_profile_id
|
||||||
|
- Fix a bug: event_service#date_range method, test on all_day was never truthy
|
||||||
|
- Fix a bug: sidekiq 5 does not have delay_for method anymore, uses perform_in instead
|
||||||
|
|
||||||
## v4.1.0 2019 September 12
|
## v4.1.0 2019 September 12
|
||||||
|
|
||||||
|
85
Dockerfile
85
Dockerfile
@ -1,46 +1,62 @@
|
|||||||
FROM ruby:2.3
|
FROM ruby:2.3.8-alpine
|
||||||
MAINTAINER peng@sleede.com
|
MAINTAINER peng@sleede.com
|
||||||
|
|
||||||
# First we need to be able to fetch from https repositories
|
# Install upgrade system packages
|
||||||
RUN apt-get update && \
|
RUN apk update && apk upgrade && \
|
||||||
apt-get install -y apt-transport-https \
|
# Install runtime apk dependencies
|
||||||
ca-certificates apt-utils
|
apk add --update \
|
||||||
|
bash \
|
||||||
|
curl \
|
||||||
# Add sources for external tools to APT
|
|
||||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
|
||||||
curl -sSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - && \
|
|
||||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list && \
|
|
||||||
echo "deb https://deb.nodesource.com/node_10.x jessie main" > /etc/apt/sources.list.d/nodesource.list && \
|
|
||||||
echo "deb-src https://deb.nodesource.com/node_10.x jessie main" >> /etc/apt/sources.list.d/nodesource.list
|
|
||||||
|
|
||||||
# Install apt based dependencies required to run Rails as
|
|
||||||
# well as RubyGems. As the Ruby image itself is based on a
|
|
||||||
# Debian image, we use apt-get to install those.
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y \
|
|
||||||
nodejs \
|
nodejs \
|
||||||
|
yarn \
|
||||||
|
imagemagick \
|
||||||
supervisor \
|
supervisor \
|
||||||
yarn
|
tzdata \
|
||||||
|
libc-dev \
|
||||||
|
ruby-dev \
|
||||||
|
zlib-dev \
|
||||||
|
xz-dev \
|
||||||
|
postgresql-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
libxslt-dev \
|
||||||
|
libidn-dev && \
|
||||||
|
# Install buildtime apk dependencies
|
||||||
|
apk add --update --no-cache --virtual .build-deps \
|
||||||
|
alpine-sdk \
|
||||||
|
build-base \
|
||||||
|
linux-headers \
|
||||||
|
git \
|
||||||
|
patch
|
||||||
|
|
||||||
# throw errors if Gemfile has been modified since Gemfile.lock
|
# Throw error if Gemfile has been modified since Gemfile.lock
|
||||||
RUN bundle config --global frozen 1
|
RUN bundle config --global frozen 1
|
||||||
|
|
||||||
# Run Bundle in a cache efficient way
|
# Install gems in a cache efficient way
|
||||||
WORKDIR /tmp
|
WORKDIR /tmp
|
||||||
COPY Gemfile /tmp/
|
COPY Gemfile /tmp/
|
||||||
COPY Gemfile.lock /tmp/
|
COPY Gemfile.lock /tmp/
|
||||||
RUN bundle install --binstubs
|
RUN bundle install --binstubs --without development test doc
|
||||||
|
|
||||||
|
# Install Javascript packages
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
COPY package.json /usr/src/app/package.json
|
||||||
|
COPY yarn.lock /usr/src/app/yarn.lock
|
||||||
|
RUN yarn install
|
||||||
|
|
||||||
# Clean up APT when done.
|
# Clean up build deps, cached packages and temp files
|
||||||
#RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
RUN apk del .build-deps && \
|
||||||
|
yarn cache clean && \
|
||||||
|
rm -rf /tmp/* \
|
||||||
|
/var/tmp/* \
|
||||||
|
/var/cache/apk/* \
|
||||||
|
/usr/lib/ruby/gems/*/cache/*
|
||||||
|
|
||||||
# Web app
|
# Web app
|
||||||
RUN mkdir -p /usr/src/app && \
|
RUN mkdir -p /usr/src/app && \
|
||||||
mkdir -p /usr/src/app/config && \
|
mkdir -p /usr/src/app/config && \
|
||||||
mkdir -p /usr/src/app/invoices && \
|
mkdir -p /usr/src/app/invoices && \
|
||||||
mkdir -p /usr/src/app/exports && \
|
mkdir -p /usr/src/app/exports && \
|
||||||
|
mkdir -p /usr/src/app/imports && \
|
||||||
mkdir -p /usr/src/app/log && \
|
mkdir -p /usr/src/app/log && \
|
||||||
mkdir -p /usr/src/app/public/uploads && \
|
mkdir -p /usr/src/app/public/uploads && \
|
||||||
mkdir -p /usr/src/app/public/assets && \
|
mkdir -p /usr/src/app/public/assets && \
|
||||||
@ -48,32 +64,23 @@ RUN mkdir -p /usr/src/app && \
|
|||||||
mkdir -p /usr/src/app/tmp/sockets && \
|
mkdir -p /usr/src/app/tmp/sockets && \
|
||||||
mkdir -p /usr/src/app/tmp/pids
|
mkdir -p /usr/src/app/tmp/pids
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
COPY docker/database.yml /usr/src/app/config/database.yml
|
COPY docker/database.yml /usr/src/app/config/database.yml
|
||||||
COPY package.json /usr/src/app/package.json
|
|
||||||
COPY yarn.lock /usr/src/app/yarn.lock
|
|
||||||
|
|
||||||
# Run Yarn
|
|
||||||
RUN yarn install
|
|
||||||
|
|
||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
|
|
||||||
# Volumes
|
# Volumes
|
||||||
VOLUME /usr/src/app/invoices
|
VOLUME /usr/src/app/invoices
|
||||||
VOLUME /usr/src/app/exports
|
VOLUME /usr/src/app/exports
|
||||||
|
VOLUME /usr/src/app/imports
|
||||||
VOLUME /usr/src/app/public
|
VOLUME /usr/src/app/public
|
||||||
VOLUME /usr/src/app/public/uploads
|
VOLUME /usr/src/app/public/uploads
|
||||||
VOLUME /usr/src/app/public/assets
|
VOLUME /usr/src/app/public/assets
|
||||||
VOLUME /usr/src/app/accounting
|
VOLUME /usr/src/app/accounting
|
||||||
VOLUME /var/log/supervisor
|
VOLUME /var/log/supervisor
|
||||||
|
|
||||||
# Expose port 3000 to the Docker host, so we can access it
|
# Expose port 3000 to the Docker host, so we can access it from the outside
|
||||||
# from the outside.
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
# The main command to run when the container starts. Also
|
# The main command to run when the container starts. Also tell the Rails server
|
||||||
# tell the Rails dev server to bind to all interfaces by
|
# to bind to all interfaces by default.
|
||||||
# default.
|
|
||||||
COPY docker/supervisor.conf /etc/supervisor/conf.d/fablab.conf
|
COPY docker/supervisor.conf /etc/supervisor/conf.d/fablab.conf
|
||||||
CMD ["/usr/bin/supervisord"]
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/fablab.conf"]
|
||||||
|
12
Gemfile
12
Gemfile
@ -3,6 +3,8 @@ source 'https://rubygems.org'
|
|||||||
gem 'compass-rails', '2.0.4'
|
gem 'compass-rails', '2.0.4'
|
||||||
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
||||||
gem 'rails', '4.2.11.1'
|
gem 'rails', '4.2.11.1'
|
||||||
|
# Use Puma as web server
|
||||||
|
gem 'puma', '3.10.0'
|
||||||
# Use SCSS for stylesheets
|
# Use SCSS for stylesheets
|
||||||
gem 'sass-rails', '5.0.1'
|
gem 'sass-rails', '5.0.1'
|
||||||
|
|
||||||
@ -45,7 +47,6 @@ group :development do
|
|||||||
gem 'foreman'
|
gem 'foreman'
|
||||||
# Preview mail in the browser
|
# Preview mail in the browser
|
||||||
gem 'mailcatcher'
|
gem 'mailcatcher'
|
||||||
gem 'puma'
|
|
||||||
gem 'rb-readline'
|
gem 'rb-readline'
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -68,10 +69,11 @@ gem 'seed_dump'
|
|||||||
|
|
||||||
gem 'pg'
|
gem 'pg'
|
||||||
|
|
||||||
gem 'devise', ">= 4.6.0"
|
gem 'devise', '>= 4.6.0'
|
||||||
|
|
||||||
gem 'omniauth', '~> 1.6.0'
|
gem 'omniauth', '~> 1.9.0'
|
||||||
gem 'omniauth-oauth2'
|
gem 'omniauth-oauth2'
|
||||||
|
gem 'omniauth-rails_csrf_protection', '~> 0.1'
|
||||||
|
|
||||||
gem 'rolify'
|
gem 'rolify'
|
||||||
|
|
||||||
@ -97,8 +99,8 @@ gem 'friendly_id', '~> 5.1.0'
|
|||||||
gem 'aasm'
|
gem 'aasm'
|
||||||
|
|
||||||
# Background job processing
|
# Background job processing
|
||||||
gem 'sidekiq', '>= 3.4.2'
|
|
||||||
gem 'redis-namespace'
|
gem 'redis-namespace'
|
||||||
|
gem 'sidekiq', '>= 3.4.2'
|
||||||
gem 'sinatra', require: false
|
gem 'sinatra', require: false
|
||||||
# Recurring jobs for Sidekiq
|
# Recurring jobs for Sidekiq
|
||||||
gem 'sidekiq-cron'
|
gem 'sidekiq-cron'
|
||||||
@ -148,3 +150,5 @@ gem 'rack-protection', '1.5.5'
|
|||||||
gem 'sys-filesystem'
|
gem 'sys-filesystem'
|
||||||
|
|
||||||
gem 'sha3'
|
gem 'sha3'
|
||||||
|
|
||||||
|
gem 'repost'
|
||||||
|
38
Gemfile.lock
38
Gemfile.lock
@ -159,7 +159,7 @@ GEM
|
|||||||
execjs (2.7.0)
|
execjs (2.7.0)
|
||||||
faker (1.4.3)
|
faker (1.4.3)
|
||||||
i18n (~> 0.5)
|
i18n (~> 0.5)
|
||||||
faraday (0.9.2)
|
faraday (0.17)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
ffi (1.9.24)
|
ffi (1.9.24)
|
||||||
figaro (1.1.0)
|
figaro (1.1.0)
|
||||||
@ -180,7 +180,7 @@ GEM
|
|||||||
activerecord (>= 3.0)
|
activerecord (>= 3.0)
|
||||||
hashdiff (0.3.0)
|
hashdiff (0.3.0)
|
||||||
hashery (2.1.2)
|
hashery (2.1.2)
|
||||||
hashie (3.5.7)
|
hashie (3.6.0)
|
||||||
hike (1.2.3)
|
hike (1.2.3)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
http (3.0.0)
|
http (3.0.0)
|
||||||
@ -209,7 +209,7 @@ GEM
|
|||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
thor (>= 0.14, < 2.0)
|
thor (>= 0.14, < 2.0)
|
||||||
json (1.8.6)
|
json (1.8.6)
|
||||||
jwt (1.5.1)
|
jwt (2.2.1)
|
||||||
kaminari (0.16.3)
|
kaminari (0.16.3)
|
||||||
actionpack (>= 3.0.0)
|
actionpack (>= 3.0.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
@ -243,8 +243,8 @@ GEM
|
|||||||
minitest (>= 5.0)
|
minitest (>= 5.0)
|
||||||
ruby-progressbar
|
ruby-progressbar
|
||||||
multi_json (1.13.1)
|
multi_json (1.13.1)
|
||||||
multi_xml (0.5.5)
|
multi_xml (0.6.0)
|
||||||
multipart-post (2.0.0)
|
multipart-post (2.1.1)
|
||||||
naught (1.1.0)
|
naught (1.1.0)
|
||||||
nokogiri (1.10.4)
|
nokogiri (1.10.4)
|
||||||
mini_portile2 (~> 2.4.0)
|
mini_portile2 (~> 2.4.0)
|
||||||
@ -252,19 +252,22 @@ GEM
|
|||||||
jbuilder (~> 2.0)
|
jbuilder (~> 2.0)
|
||||||
rails (>= 4.2.0)
|
rails (>= 4.2.0)
|
||||||
responders (~> 2.0)
|
responders (~> 2.0)
|
||||||
oauth2 (1.0.0)
|
oauth2 (1.4.2)
|
||||||
faraday (>= 0.8, < 0.10)
|
faraday (>= 0.8, < 2.0)
|
||||||
jwt (~> 1.0)
|
jwt (>= 1.0, < 3.0)
|
||||||
multi_json (~> 1.3)
|
multi_json (~> 1.3)
|
||||||
multi_xml (~> 0.5)
|
multi_xml (~> 0.5)
|
||||||
rack (~> 1.2)
|
rack (>= 1.2, < 3)
|
||||||
oj (2.12.8)
|
oj (2.12.8)
|
||||||
omniauth (1.6.1)
|
omniauth (1.9.0)
|
||||||
hashie (>= 3.4.6, < 3.6.0)
|
hashie (>= 3.4.6, < 3.7.0)
|
||||||
rack (>= 1.6.2, < 3)
|
rack (>= 1.6.2, < 3)
|
||||||
omniauth-oauth2 (1.3.1)
|
omniauth-oauth2 (1.6.0)
|
||||||
oauth2 (~> 1.0)
|
oauth2 (~> 1.1)
|
||||||
omniauth (~> 1.2)
|
omniauth (~> 1.9)
|
||||||
|
omniauth-rails_csrf_protection (0.1.2)
|
||||||
|
actionpack (>= 4.2)
|
||||||
|
omniauth (>= 1.3.1)
|
||||||
openlab_ruby (0.0.4)
|
openlab_ruby (0.0.4)
|
||||||
httparty (~> 0.13)
|
httparty (~> 0.13)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
@ -342,6 +345,7 @@ GEM
|
|||||||
redis-namespace (1.6.0)
|
redis-namespace (1.6.0)
|
||||||
redis (>= 3.0.4)
|
redis (>= 3.0.4)
|
||||||
ref (2.0.0)
|
ref (2.0.0)
|
||||||
|
repost (0.2.9)
|
||||||
responders (2.1.0)
|
responders (2.1.0)
|
||||||
railties (>= 4.2.0, < 5)
|
railties (>= 4.2.0, < 5)
|
||||||
rolify (4.0.0)
|
rolify (4.0.0)
|
||||||
@ -509,15 +513,16 @@ DEPENDENCIES
|
|||||||
minitest-reporters
|
minitest-reporters
|
||||||
notify_with
|
notify_with
|
||||||
oj
|
oj
|
||||||
omniauth (~> 1.6.0)
|
omniauth (~> 1.9.0)
|
||||||
omniauth-oauth2
|
omniauth-oauth2
|
||||||
|
omniauth-rails_csrf_protection (~> 0.1)
|
||||||
openlab_ruby
|
openlab_ruby
|
||||||
pdf-reader
|
pdf-reader
|
||||||
pg
|
pg
|
||||||
prawn
|
prawn
|
||||||
prawn-table
|
prawn-table
|
||||||
protected_attributes
|
protected_attributes
|
||||||
puma
|
puma (= 3.10.0)
|
||||||
pundit
|
pundit
|
||||||
rack-protection (= 1.5.5)
|
rack-protection (= 1.5.5)
|
||||||
railroady
|
railroady
|
||||||
@ -527,6 +532,7 @@ DEPENDENCIES
|
|||||||
rb-readline
|
rb-readline
|
||||||
recurrence
|
recurrence
|
||||||
redis-namespace
|
redis-namespace
|
||||||
|
repost
|
||||||
responders (~> 2.0)
|
responders (~> 2.0)
|
||||||
rolify
|
rolify
|
||||||
rubocop (~> 0.61.1)
|
rubocop (~> 0.61.1)
|
||||||
|
2
Procfile
2
Procfile
@ -1,3 +1,3 @@
|
|||||||
web: bundle exec rails server puma -p $PORT -b0.0.0.0
|
web: bundle exec rails server puma -p $PORT -b0.0.0.0
|
||||||
worker: bundle exec sidekiq -C ./config/sidekiq.yml
|
worker: bundle exec sidekiq -C ./config/sidekiq.yml
|
||||||
mail: bundle exec mailcatcher --foreground
|
mail: bundle exec mailcatcher --foreground --http-ip=0.0.0.0
|
||||||
|
11
README.md
11
README.md
@ -13,7 +13,7 @@ FabManager is the Fab Lab management solution. It provides a comprehensive, web-
|
|||||||
4. [Setup a development environment](#setup-a-development-environment)<br/>
|
4. [Setup a development environment](#setup-a-development-environment)<br/>
|
||||||
4.1. [General Guidelines](#general-guidelines)<br/>
|
4.1. [General Guidelines](#general-guidelines)<br/>
|
||||||
5. [PostgreSQL](#postgresql)<br/>
|
5. [PostgreSQL](#postgresql)<br/>
|
||||||
5.1. [Install PostgreSQL 9.4](#setup-postgresql)
|
5.1. [Install PostgreSQL 9.6](#setup-postgresql)
|
||||||
6. [ElasticSearch](#elasticsearch)<br/>
|
6. [ElasticSearch](#elasticsearch)<br/>
|
||||||
6.1. [Install ElasticSearch](#setup-elasticsearch)<br/>
|
6.1. [Install ElasticSearch](#setup-elasticsearch)<br/>
|
||||||
6.2. [Rebuild statistics](#rebuild-stats)<br/>
|
6.2. [Rebuild statistics](#rebuild-stats)<br/>
|
||||||
@ -43,7 +43,7 @@ FabManager is a Ruby on Rails / AngularJS web application that runs on the follo
|
|||||||
- Redis 2.8.4+
|
- Redis 2.8.4+
|
||||||
- Sidekiq 3.3.4+
|
- Sidekiq 3.3.4+
|
||||||
- Elasticsearch 5.6
|
- Elasticsearch 5.6
|
||||||
- PostgreSQL 9.4
|
- PostgreSQL 9.6
|
||||||
|
|
||||||
<a name="contributing"></a>
|
<a name="contributing"></a>
|
||||||
## Contributing
|
## Contributing
|
||||||
@ -193,7 +193,7 @@ Optionally, you can use a virtual development environment that relies on Vagrant
|
|||||||
## PostgreSQL
|
## PostgreSQL
|
||||||
|
|
||||||
<a name="setup-postgresql"></a>
|
<a name="setup-postgresql"></a>
|
||||||
### Install PostgreSQL 9.4
|
### Install PostgreSQL 9.6
|
||||||
|
|
||||||
We will use docker to easily install the required version of PostgreSQL.
|
We will use docker to easily install the required version of PostgreSQL.
|
||||||
|
|
||||||
@ -208,7 +208,7 @@ We will use docker to easily install the required version of PostgreSQL.
|
|||||||
-v $(pwd)/.docker/postgresql:/var/lib/postgresql/data \
|
-v $(pwd)/.docker/postgresql:/var/lib/postgresql/data \
|
||||||
--network fabmanager --ip 172.18.0.2 \
|
--network fabmanager --ip 172.18.0.2 \
|
||||||
-p 5432:5432 \
|
-p 5432:5432 \
|
||||||
postgres:9.4
|
postgres:9.6
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Configure fab-manager to use it.
|
3. Configure fab-manager to use it.
|
||||||
@ -432,6 +432,9 @@ Developers may find information on how to implement their own authentication pro
|
|||||||
```bash
|
```bash
|
||||||
sudo systemctl restart elasticsearch.service
|
sudo systemctl restart elasticsearch.service
|
||||||
```
|
```
|
||||||
|
- In some cases, the invoices won't be generated. This can be due to the image included in the invoice header not being supported.
|
||||||
|
To fix this issue, change the image in the administrator interface (manage the invoices / invoices settings).
|
||||||
|
See [this thread](https://forum.fab-manager.com/t/resolu-erreur-generation-facture/428) for more info.
|
||||||
|
|
||||||
<a name="related-documentation"></a>
|
<a name="related-documentation"></a>
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
10
Vagrantfile
vendored
10
Vagrantfile
vendored
@ -20,6 +20,9 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
|||||||
config.vm.network "forwarded_port", guest: port, host: port
|
config.vm.network "forwarded_port", guest: port, host: port
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# nginx server
|
||||||
|
config.vm.network "forwarded_port", guest: 80, host: 8080
|
||||||
|
|
||||||
# Configuration to allocate resources fro the virtual machine
|
# Configuration to allocate resources fro the virtual machine
|
||||||
config.vm.provider 'virtualbox' do |vb|
|
config.vm.provider 'virtualbox' do |vb|
|
||||||
vb.customize ['modifyvm', :id, '--memory', '2048']
|
vb.customize ['modifyvm', :id, '--memory', '2048']
|
||||||
@ -33,6 +36,13 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
|||||||
config.vm.provision "file", source: "./config/database.yml.default", destination: "/vagrant/config/database.yml"
|
config.vm.provision "file", source: "./config/database.yml.default", destination: "/vagrant/config/database.yml"
|
||||||
config.vm.provision "file", source: "./config/application.yml.default", destination: "/vagrant/config/application.yml"
|
config.vm.provision "file", source: "./config/application.yml.default", destination: "/vagrant/config/application.yml"
|
||||||
|
|
||||||
|
# Copy default configuration files to allow reviewing the Docker Compose integration
|
||||||
|
config.vm.provision "file", source: "./docker/docker-compose.yml", destination: "/home/vagrant/docker-compose.yml"
|
||||||
|
config.vm.provision "file", source: "./docker/env.example", destination: "/home/vagrant/config/env"
|
||||||
|
config.vm.provision "file", source: "./docker/nginx.conf.example", destination: "/home/vagrant/config/nginx/fabmanager.conf"
|
||||||
|
config.vm.provision "file", source: "./docker/elasticsearch.yml", destination: "/home/vagrant/elasticsearch/config/elasticsearch.yml"
|
||||||
|
config.vm.provision "file", source: "./docker/log4j2.properties", destination: "/home/vagrant/elasticsearch/config/log4j2.properties"
|
||||||
|
|
||||||
## Provision software dependencies
|
## Provision software dependencies
|
||||||
config.vm.provision "shell", privileged: false, run: "once",
|
config.vm.provision "shell", privileged: false, run: "once",
|
||||||
path: "provision/zsh_setup.sh"
|
path: "provision/zsh_setup.sh"
|
||||||
|
@ -20,7 +20,7 @@ angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ui.rout
|
|||||||
'ui.select', 'ui.calendar', 'angularMoment', 'Devise', 'DeviseModal', 'angular-growl', 'xeditable',
|
'ui.select', 'ui.calendar', 'angularMoment', 'Devise', 'DeviseModal', 'angular-growl', 'xeditable',
|
||||||
'checklist-model', 'unsavedChanges', 'angular-loading-bar', 'ngTouch', 'angular-google-analytics',
|
'checklist-model', 'unsavedChanges', 'angular-loading-bar', 'ngTouch', 'angular-google-analytics',
|
||||||
'angularUtils.directives.dirDisqus', 'summernote', 'elasticsearch', 'angular-medium-editor', 'naif.base64',
|
'angularUtils.directives.dirDisqus', 'summernote', 'elasticsearch', 'angular-medium-editor', 'naif.base64',
|
||||||
'minicolors', 'pascalprecht.translate', 'ngFitText', 'ngAside', 'ngCapsLock'])
|
'minicolors', 'pascalprecht.translate', 'ngFitText', 'ngAside', 'ngCapsLock', 'vcRecaptcha'])
|
||||||
.config(['$httpProvider', 'AuthProvider', 'growlProvider', 'unsavedWarningsConfigProvider', 'AnalyticsProvider', 'uibDatepickerPopupConfig', '$provide', '$translateProvider',
|
.config(['$httpProvider', 'AuthProvider', 'growlProvider', 'unsavedWarningsConfigProvider', 'AnalyticsProvider', 'uibDatepickerPopupConfig', '$provide', '$translateProvider',
|
||||||
function ($httpProvider, AuthProvider, growlProvider, unsavedWarningsConfigProvider, AnalyticsProvider, uibDatepickerPopupConfig, $provide, $translateProvider) {
|
function ($httpProvider, AuthProvider, growlProvider, unsavedWarningsConfigProvider, AnalyticsProvider, uibDatepickerPopupConfig, $provide, $translateProvider) {
|
||||||
// Google analytics
|
// Google analytics
|
||||||
@ -84,6 +84,8 @@ angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ui.rout
|
|||||||
$rootScope.fablabWithoutSpaces = Fablab.withoutSpaces;
|
$rootScope.fablabWithoutSpaces = Fablab.withoutSpaces;
|
||||||
// Global config: if true, all payments will be disabled in the application for the members (only admins will be able to proceed reservations)
|
// Global config: if true, all payments will be disabled in the application for the members (only admins will be able to proceed reservations)
|
||||||
$rootScope.fablabWithoutOnlinePayment = Fablab.withoutOnlinePayment;
|
$rootScope.fablabWithoutOnlinePayment = Fablab.withoutOnlinePayment;
|
||||||
|
// Global config: if true, no invoices will be generated
|
||||||
|
$rootScope.fablabWithoutInvoices = Fablab.withoutInvoices;
|
||||||
|
|
||||||
// Global function to allow the user to navigate to the previous screen (ie. $state).
|
// Global function to allow the user to navigate to the previous screen (ie. $state).
|
||||||
// If no previous $state were recorded, navigate to the home page
|
// If no previous $state were recorded, navigate to the home page
|
||||||
|
@ -64,6 +64,7 @@
|
|||||||
//= require ng-fittext/dist/ng-FitText.min
|
//= require ng-fittext/dist/ng-FitText.min
|
||||||
//= require angular-aside/dist/js/angular-aside
|
//= require angular-aside/dist/js/angular-aside
|
||||||
//= require ng-caps-lock/ng-caps-lock
|
//= require ng-caps-lock/ng-caps-lock
|
||||||
|
//= require angular-recaptcha
|
||||||
//= require_tree ./controllers
|
//= require_tree ./controllers
|
||||||
//= require_tree ./services
|
//= require_tree ./services
|
||||||
//= require_tree ./directives
|
//= require_tree ./directives
|
||||||
|
@ -26,6 +26,12 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
|||||||
|
|
||||||
/* PUBLIC SCOPE */
|
/* PUBLIC SCOPE */
|
||||||
|
|
||||||
|
// default active tab
|
||||||
|
$scope.tabs = {
|
||||||
|
listing: { active: !Fablab.withoutInvoices },
|
||||||
|
settings: { active: Fablab.withoutInvoices }
|
||||||
|
};
|
||||||
|
|
||||||
// List of all users invoices
|
// List of all users invoices
|
||||||
$scope.invoices = invoices;
|
$scope.invoices = invoices;
|
||||||
|
|
||||||
@ -76,6 +82,94 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Accounting codes
|
||||||
|
$scope.settings = {
|
||||||
|
journalCode: {
|
||||||
|
name: 'accounting_journal_code',
|
||||||
|
value: settings['accounting_journal_code']
|
||||||
|
},
|
||||||
|
cardClientCode: {
|
||||||
|
name: 'accounting_card_client_code',
|
||||||
|
value: settings['accounting_card_client_code']
|
||||||
|
},
|
||||||
|
cardClientLabel: {
|
||||||
|
name: 'accounting_card_client_label',
|
||||||
|
value: settings['accounting_card_client_label']
|
||||||
|
},
|
||||||
|
walletClientCode: {
|
||||||
|
name: 'accounting_wallet_client_code',
|
||||||
|
value: settings['accounting_wallet_client_code']
|
||||||
|
},
|
||||||
|
walletClientLabel: {
|
||||||
|
name: 'accounting_wallet_client_label',
|
||||||
|
value: settings['accounting_wallet_client_label']
|
||||||
|
},
|
||||||
|
otherClientCode: {
|
||||||
|
name: 'accounting_other_client_code',
|
||||||
|
value: settings['accounting_other_client_code']
|
||||||
|
},
|
||||||
|
otherClientLabel: {
|
||||||
|
name: 'accounting_other_client_label',
|
||||||
|
value: settings['accounting_other_client_label']
|
||||||
|
},
|
||||||
|
walletCode: {
|
||||||
|
name: 'accounting_wallet_code',
|
||||||
|
value: settings['accounting_wallet_code']
|
||||||
|
},
|
||||||
|
walletLabel: {
|
||||||
|
name: 'accounting_wallet_label',
|
||||||
|
value: settings['accounting_wallet_label']
|
||||||
|
},
|
||||||
|
vatCode: {
|
||||||
|
name: 'accounting_VAT_code',
|
||||||
|
value: settings['accounting_VAT_code']
|
||||||
|
},
|
||||||
|
vatLabel: {
|
||||||
|
name: 'accounting_VAT_label',
|
||||||
|
value: settings['accounting_VAT_label']
|
||||||
|
},
|
||||||
|
subscriptionCode: {
|
||||||
|
name: 'accounting_subscription_code',
|
||||||
|
value: settings['accounting_subscription_code']
|
||||||
|
},
|
||||||
|
subscriptionLabel: {
|
||||||
|
name: 'accounting_subscription_label',
|
||||||
|
value: settings['accounting_subscription_label']
|
||||||
|
},
|
||||||
|
machineCode: {
|
||||||
|
name: 'accounting_Machine_code',
|
||||||
|
value: settings['accounting_Machine_code']
|
||||||
|
},
|
||||||
|
machineLabel: {
|
||||||
|
name: 'accounting_Machine_label',
|
||||||
|
value: settings['accounting_Machine_label']
|
||||||
|
},
|
||||||
|
trainingCode: {
|
||||||
|
name: 'accounting_Training_code',
|
||||||
|
value: settings['accounting_Training_code']
|
||||||
|
},
|
||||||
|
trainingLabel: {
|
||||||
|
name: 'accounting_Training_label',
|
||||||
|
value: settings['accounting_Training_label']
|
||||||
|
},
|
||||||
|
eventCode: {
|
||||||
|
name: 'accounting_Event_code',
|
||||||
|
value: settings['accounting_Event_code']
|
||||||
|
},
|
||||||
|
eventLabel: {
|
||||||
|
name: 'accounting_Event_label',
|
||||||
|
value: settings['accounting_Event_label']
|
||||||
|
},
|
||||||
|
spaceCode: {
|
||||||
|
name: 'accounting_Space_code',
|
||||||
|
value: settings['accounting_Space_code']
|
||||||
|
},
|
||||||
|
spaceLabel: {
|
||||||
|
name: 'accounting_Space_label',
|
||||||
|
value: settings['accounting_Space_label']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Placeholding date for the invoice creation
|
// Placeholding date for the invoice creation
|
||||||
$scope.today = moment();
|
$scope.today = moment();
|
||||||
|
|
||||||
@ -325,9 +419,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
|||||||
$scope.history.push({ date: rate.created_at, rate: rate.value, user: rate.user })
|
$scope.history.push({ date: rate.created_at, rate: rate.value, user: rate.user })
|
||||||
});
|
});
|
||||||
activeHistory.setting.history.forEach(function (v) {
|
activeHistory.setting.history.forEach(function (v) {
|
||||||
if (v.value === 'false') {
|
$scope.history.push({ date: v.created_at, enabled: v.value === 'true', user: v.user })
|
||||||
$scope.history.push({ date: v.created_at, rate: 0, user: v.user })
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -432,6 +524,14 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.toggleExportModal = function() {
|
||||||
|
$uibModal.open({
|
||||||
|
templateUrl: '<%= asset_path "admin/invoices/accountingExportModal.html" %>',
|
||||||
|
controller: 'AccountingExportModalController',
|
||||||
|
size: 'xl'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test if the given date is within a closed accounting period
|
* Test if the given date is within a closed accounting period
|
||||||
* @param date {Date} date to test
|
* @param date {Date} date to test
|
||||||
@ -446,6 +546,20 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to bulk save all settings in the page to the database with their values
|
||||||
|
*/
|
||||||
|
$scope.save = function() {
|
||||||
|
Setting.bulkUpdate(
|
||||||
|
{ settings: Object.values($scope.settings) },
|
||||||
|
function () { growl.success(_t('invoices.codes_customization_success')); },
|
||||||
|
function (error) {
|
||||||
|
growl.error('unexpected_error_occurred');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* PRIVATE SCOPE */
|
/* PRIVATE SCOPE */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -791,7 +905,7 @@ Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$ui
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancel the refund, dismiss the modal window
|
* Just dismiss the modal window
|
||||||
*/
|
*/
|
||||||
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||||
|
|
||||||
@ -803,3 +917,136 @@ Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$ui
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Application.Controllers.controller('AccountingExportModalController', ['$scope', '$uibModalInstance', 'Invoice', 'Export', 'CSRF', 'growl', '_t',
|
||||||
|
function ($scope, $uibModalInstance, Invoice, Export, CSRF, growl, _t) {
|
||||||
|
// Retrieve Anti-CSRF tokens from cookies
|
||||||
|
CSRF.setMetaTags();
|
||||||
|
|
||||||
|
const SETTINGS = {
|
||||||
|
acd: {
|
||||||
|
format: 'csv',
|
||||||
|
encoding: 'ISO-8859-1',
|
||||||
|
separator: ';',
|
||||||
|
dateFormat: '%d/%m/%Y',
|
||||||
|
labelMaxLength: 50,
|
||||||
|
decimalSeparator: ',',
|
||||||
|
exportInvoicesAtZero: false,
|
||||||
|
columns: ['journal_code', 'date', 'account_code', 'account_label', 'piece', 'line_label', 'debit_origin', 'credit_origin', 'debit_euro', 'credit_euro', 'lettering']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* PUBLIC SCOPE */
|
||||||
|
|
||||||
|
// API URL where the form will be posted
|
||||||
|
$scope.actionUrl = '/api/accounting/export';
|
||||||
|
|
||||||
|
// Form action on the above URL
|
||||||
|
$scope.method = 'post';
|
||||||
|
|
||||||
|
// Anti-CSRF token to inject into the download form
|
||||||
|
$scope.csrfToken = angular.element('meta[name="csrf-token"]')[0].content;
|
||||||
|
|
||||||
|
// API request body to generate the export
|
||||||
|
$scope.query = null;
|
||||||
|
|
||||||
|
// binding to radio button "export to"
|
||||||
|
$scope.exportTarget = {
|
||||||
|
software: null,
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
settings: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// AngularUI-Bootstrap datepicker parameters to define export dates range
|
||||||
|
$scope.datePicker = {
|
||||||
|
format: Fablab.uibDateFormat,
|
||||||
|
opened: { // default: datePickers are not shown
|
||||||
|
start: false,
|
||||||
|
end: false
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
startingDay: Fablab.weekStartingDay
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Date of the first invoice
|
||||||
|
$scope.firstInvoice = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the export
|
||||||
|
*/
|
||||||
|
$scope.ok = function () {
|
||||||
|
const statusQry = mkQuery();
|
||||||
|
$scope.query = statusQry;
|
||||||
|
|
||||||
|
Export.status(statusQry).then(function (res) {
|
||||||
|
if (!res.data.exists) {
|
||||||
|
growl.success(_t('invoices.export_is_running'));
|
||||||
|
}
|
||||||
|
$uibModalInstance.close(res);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to open/close one of the datepickers
|
||||||
|
* @param event {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
|
* @param picker {string} start | end
|
||||||
|
*/
|
||||||
|
$scope.toggleDatePicker = function(event, picker) {
|
||||||
|
event.preventDefault();
|
||||||
|
$scope.datePicker.opened[picker] = !$scope.datePicker.opened[picker];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will fill the export settings, according to the selected software
|
||||||
|
*/
|
||||||
|
$scope.fillSettings = function() {
|
||||||
|
$scope.exportTarget.settings = SETTINGS[$scope.exportTarget.software];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Just dismiss the modal window
|
||||||
|
*/
|
||||||
|
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||||
|
|
||||||
|
/* PRIVATE SCOPE */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
*/
|
||||||
|
const initialize = function () {
|
||||||
|
// if the invoice was payed with stripe, allow to refund through stripe
|
||||||
|
Invoice.first(function (data) {
|
||||||
|
$scope.firstInvoice = data.date;
|
||||||
|
$scope.exportTarget.startDate = data.date;
|
||||||
|
$scope.exportTarget.endDate = moment().toISOString();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the query for the export API
|
||||||
|
* @returns {{extension: *, query: *, category: string, type: *, key: *}}
|
||||||
|
*/
|
||||||
|
const mkQuery = function() {
|
||||||
|
return {
|
||||||
|
category: 'accounting',
|
||||||
|
type: $scope.exportTarget.software,
|
||||||
|
extension: $scope.exportTarget.settings.format,
|
||||||
|
key: $scope.exportTarget.settings.separator,
|
||||||
|
query: JSON.stringify({
|
||||||
|
columns: $scope.exportTarget.settings.columns,
|
||||||
|
encoding: $scope.exportTarget.settings.encoding,
|
||||||
|
date_format: $scope.exportTarget.settings.dateFormat,
|
||||||
|
start_date: $scope.exportTarget.startDate,
|
||||||
|
end_date: $scope.exportTarget.endDate,
|
||||||
|
label_max_length: $scope.exportTarget.settings.labelMaxLength,
|
||||||
|
decimal_separator: $scope.exportTarget.settings.decimalSeparator,
|
||||||
|
export_invoices_at_zero: $scope.exportTarget.settings.exportInvoicesAtZero
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
return initialize();
|
||||||
|
}]);
|
||||||
|
@ -609,6 +609,81 @@ Application.Controllers.controller('NewMemberController', ['$scope', '$state', '
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller used in the member's import page: import from CSV (admin view)
|
||||||
|
*/
|
||||||
|
Application.Controllers.controller('ImportMembersController', ['$scope', '$state', 'Group', 'Training', 'CSRF', 'tags', 'growl',
|
||||||
|
function($scope, $state, Group, Training, CSRF, tags, growl) {
|
||||||
|
CSRF.setMetaTags();
|
||||||
|
|
||||||
|
/* PUBLIC SCOPE */
|
||||||
|
|
||||||
|
// API URL where the form will be posted
|
||||||
|
$scope.actionUrl = '/api/imports/members';
|
||||||
|
|
||||||
|
// Form action on the above URL
|
||||||
|
$scope.method = 'post';
|
||||||
|
|
||||||
|
// List of all tags
|
||||||
|
$scope.tags = tags
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Callback run after the form was submitted
|
||||||
|
* @param content {*} The result provided by the server, may be an Import object or an error message
|
||||||
|
*/
|
||||||
|
$scope.onImportResult = function(content) {
|
||||||
|
if (content.id) {
|
||||||
|
$state.go('app.admin.members_import_result', { id: content.id });
|
||||||
|
} else {
|
||||||
|
growl.error(JSON.stringify(content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using the MembersController
|
||||||
|
return new MembersController($scope, $state, Group, Training);
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller used in the member's import results page (admin view)
|
||||||
|
*/
|
||||||
|
Application.Controllers.controller('ImportMembersResultController', ['$scope', '$state', 'Import', 'importItem',
|
||||||
|
function ($scope, $state, Import, importItem) {
|
||||||
|
/* PUBLIC SCOPE */
|
||||||
|
|
||||||
|
// Current import as saved in database
|
||||||
|
$scope.import = importItem;
|
||||||
|
|
||||||
|
// Current import results
|
||||||
|
$scope.results = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the admin's view to the members import page
|
||||||
|
*/
|
||||||
|
$scope.cancel = function () { $state.go('app.admin.members_import'); };
|
||||||
|
|
||||||
|
/* PRIVATE SCOPE */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
*/
|
||||||
|
const initialize = function () {
|
||||||
|
$scope.results = JSON.parse($scope.import.results);
|
||||||
|
if (!$scope.results) {
|
||||||
|
setTimeout(function() {
|
||||||
|
Import.get({ id: $scope.import.id }, function(data) {
|
||||||
|
$scope.import = data;
|
||||||
|
initialize();
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
initialize();
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller used in the admin's creation page (admin view)
|
* Controller used in the admin's creation page (admin view)
|
||||||
*/
|
*/
|
||||||
|
@ -82,6 +82,10 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
|
|||||||
*/
|
*/
|
||||||
$scope.signup = function (e) {
|
$scope.signup = function (e) {
|
||||||
if (e) { e.preventDefault(); }
|
if (e) { e.preventDefault(); }
|
||||||
|
<% active_provider = AuthProvider.active %>
|
||||||
|
<% if active_provider.providable_type != DatabaseProvider.name %>
|
||||||
|
$window.location.href = '/sso-redirect';
|
||||||
|
<% else %>
|
||||||
|
|
||||||
return $uibModal.open({
|
return $uibModal.open({
|
||||||
templateUrl: '<%= asset_path "shared/signupModal.html" %>',
|
templateUrl: '<%= asset_path "shared/signupModal.html" %>',
|
||||||
@ -96,6 +100,9 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// reCaptcha v2 site key (or undefined)
|
||||||
|
$scope.recaptchaSiteKey = Fablab.recaptchaSiteKey;
|
||||||
|
|
||||||
// callback to open the date picker (account creation modal)
|
// callback to open the date picker (account creation modal)
|
||||||
$scope.openDatePicker = function ($event) {
|
$scope.openDatePicker = function ($event) {
|
||||||
$event.preventDefault();
|
$event.preventDefault();
|
||||||
@ -117,7 +124,9 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
|
|||||||
// default user's parameters
|
// default user's parameters
|
||||||
$scope.user = {
|
$scope.user = {
|
||||||
is_allow_contact: true,
|
is_allow_contact: true,
|
||||||
is_allow_newsletter: false
|
is_allow_newsletter: false,
|
||||||
|
// reCaptcha response, received from Google (through AJAX) and sent to server for validation
|
||||||
|
recaptcha: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
// Errors display
|
// Errors display
|
||||||
@ -162,6 +171,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
|
|||||||
// when the account was created successfully, set the session to the newly created account
|
// when the account was created successfully, set the session to the newly created account
|
||||||
$scope.setCurrentUser(user);
|
$scope.setCurrentUser(user);
|
||||||
});
|
});
|
||||||
|
<% end %>
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -198,7 +208,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
|
|||||||
return Auth.login().then(function (user) {
|
return Auth.login().then(function (user) {
|
||||||
$scope.setCurrentUser(user);
|
$scope.setCurrentUser(user);
|
||||||
}, function (error) {
|
}, function (error) {
|
||||||
console.error(`Authentication failed: ${error}`);
|
console.error(`Authentication failed: ${JSON.stringify(error)}`);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -346,7 +356,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
|
|||||||
var openLoginModal = function (toState, toParams, callback) {
|
var openLoginModal = function (toState, toParams, callback) {
|
||||||
<% active_provider = AuthProvider.active %>
|
<% active_provider = AuthProvider.active %>
|
||||||
<% if active_provider.providable_type != DatabaseProvider.name %>
|
<% if active_provider.providable_type != DatabaseProvider.name %>
|
||||||
$window.location.href = '<%="/users/auth/#{active_provider.strategy_name}"%>';
|
$window.location.href = '/sso-redirect';
|
||||||
<% else %>
|
<% else %>
|
||||||
return $uibModal.open({
|
return $uibModal.open({
|
||||||
templateUrl: '<%= asset_path "shared/deviseModal.html" %>',
|
templateUrl: '<%= asset_path "shared/deviseModal.html" %>',
|
||||||
@ -362,7 +372,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
, function (error) {
|
, function (error) {
|
||||||
console.error(`Authentication failed: ${error}`);
|
console.error(`Authentication failed: ${JSON.stringify(error)}`);
|
||||||
$scope.alerts = [];
|
$scope.alerts = [];
|
||||||
return $scope.alerts.push({
|
return $scope.alerts.push({
|
||||||
msg: _t('wrong_email_or_password'),
|
msg: _t('wrong_email_or_password'),
|
||||||
|
@ -442,9 +442,9 @@ Application.Controllers.controller('NewProjectController', ['$scope', '$state',
|
|||||||
/**
|
/**
|
||||||
* Controller used in the project edition page
|
* Controller used in the project edition page
|
||||||
*/
|
*/
|
||||||
Application.Controllers.controller('EditProjectController', ['$scope', '$state', '$stateParams', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'projectPromise', 'Diacritics', 'dialogs', 'allowedExtensions', '_t',
|
Application.Controllers.controller('EditProjectController', ['$rootScope', '$scope', '$state', '$stateParams', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'projectPromise', 'Diacritics', 'dialogs', 'allowedExtensions', '_t',
|
||||||
function ($scope, $state, $stateParams, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, projectPromise, Diacritics, dialogs, allowedExtensions, _t) {
|
function ($rootScope, $scope, $state, $stateParams, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, projectPromise, Diacritics, dialogs, allowedExtensions, _t) {
|
||||||
CSRF.setMetaTags();
|
/* PUBLIC SCOPE */
|
||||||
|
|
||||||
// API URL where the form will be posted
|
// API URL where the form will be posted
|
||||||
$scope.actionUrl = `/api/projects/${$stateParams.id}`;
|
$scope.actionUrl = `/api/projects/${$stateParams.id}`;
|
||||||
@ -462,9 +462,26 @@ Application.Controllers.controller('EditProjectController', ['$scope', '$state',
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* PRIVATE SCOPE */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
*/
|
||||||
|
const initialize = function () {
|
||||||
|
CSRF.setMetaTags();
|
||||||
|
|
||||||
|
if ($scope.project.author_id !== $rootScope.currentUser.id && $scope.project.user_ids.indexOf($rootScope.currentUser.id) === -1) {
|
||||||
|
$state.go('app.public.projects_show', { id: $scope.project.slug });
|
||||||
|
console.error('[EditProjectController::initialize] user is not allowed')
|
||||||
|
}
|
||||||
|
|
||||||
// Using the ProjectsController
|
// Using the ProjectsController
|
||||||
return new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t);
|
return new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
return initialize();
|
||||||
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,7 +63,7 @@ Application.Directives.directive('stripeForm', ['Payment', 'growl', '_t',
|
|||||||
Payment.confirm({ payment_method_id: paymentMethod.id, cart_items: $scope.cartItems }, function (response) {
|
Payment.confirm({ payment_method_id: paymentMethod.id, cart_items: $scope.cartItems }, function (response) {
|
||||||
// Handle server response (see Step 3)
|
// Handle server response (see Step 3)
|
||||||
handleServerResponse(response, button);
|
handleServerResponse(response, button);
|
||||||
}, function(error) { handleServerResponse({ error }) });
|
}, function(error) { handleServerResponse({ error }, button) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -89,7 +89,7 @@ Application.Directives.directive('stripeForm', ['Payment', 'growl', '_t',
|
|||||||
// The PaymentIntent can be confirmed again on the server
|
// The PaymentIntent can be confirmed again on the server
|
||||||
Payment.confirm({ payment_intent_id: result.paymentIntent.id, cart_items: $scope.cartItems }, function(confirmResult) {
|
Payment.confirm({ payment_intent_id: result.paymentIntent.id, cart_items: $scope.cartItems }, function(confirmResult) {
|
||||||
handleServerResponse(confirmResult, confirmButton);
|
handleServerResponse(confirmResult, confirmButton);
|
||||||
}, function(error) { handleServerResponse({ error }) });
|
}, function(error) { handleServerResponse({ error }, confirmButton) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -38,7 +38,9 @@ angular.module('application.router', ['ui.router'])
|
|||||||
logoBlackFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-black-file' }).$promise; }],
|
logoBlackFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-black-file' }).$promise; }],
|
||||||
commonTranslations: ['Translations', function (Translations) { return Translations.query(['app.public.common', 'app.shared.buttons', 'app.shared.elements']).$promise; }]
|
commonTranslations: ['Translations', function (Translations) { return Translations.query(['app.public.common', 'app.shared.buttons', 'app.shared.elements']).$promise; }]
|
||||||
},
|
},
|
||||||
onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', function ($rootScope, logoFile, logoBlackFile) {
|
onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', 'CSRF', function ($rootScope, logoFile, logoBlackFile, CSRF) {
|
||||||
|
// Retrieve Anti-CSRF tokens from cookies
|
||||||
|
CSRF.setMetaTags();
|
||||||
// Application logo
|
// Application logo
|
||||||
$rootScope.logo = logoFile.custom_asset;
|
$rootScope.logo = logoFile.custom_asset;
|
||||||
return $rootScope.logoBlack = logoBlackFile.custom_asset;
|
return $rootScope.logoBlack = logoBlackFile.custom_asset;
|
||||||
@ -899,15 +901,13 @@ angular.module('application.router', ['ui.router'])
|
|||||||
resolve: {
|
resolve: {
|
||||||
settings: ['Setting', function (Setting) {
|
settings: ['Setting', function (Setting) {
|
||||||
return Setting.query({
|
return Setting.query({
|
||||||
names: `['invoice_legals', \
|
names: `['invoice_legals', 'invoice_text', 'invoice_VAT-rate', 'invoice_VAT-active', 'invoice_order-nb', 'invoice_code-value', \
|
||||||
'invoice_text', \
|
'invoice_code-active', 'invoice_reference', 'invoice_logo', 'accounting_journal_code', 'accounting_card_client_code', \
|
||||||
'invoice_VAT-rate', \
|
'accounting_card_client_label', 'accounting_wallet_client_code', 'accounting_wallet_client_label', \
|
||||||
'invoice_VAT-active', \
|
'accounting_other_client_code', 'accounting_other_client_label', 'accounting_wallet_code', 'accounting_wallet_label', \
|
||||||
'invoice_order-nb', \
|
'accounting_VAT_code', 'accounting_VAT_label', 'accounting_subscription_code', 'accounting_subscription_label', \
|
||||||
'invoice_code-value', \
|
'accounting_Machine_code', 'accounting_Machine_label', 'accounting_Training_code', 'accounting_Training_label', \
|
||||||
'invoice_code-active', \
|
'accounting_Event_code', 'accounting_Event_label', 'accounting_Space_code', 'accounting_Space_label']` }).$promise;
|
||||||
'invoice_reference', \
|
|
||||||
'invoice_logo']` }).$promise;
|
|
||||||
}],
|
}],
|
||||||
invoices: [ 'Invoice', function (Invoice) {
|
invoices: [ 'Invoice', function (Invoice) {
|
||||||
return Invoice.list({
|
return Invoice.list({
|
||||||
@ -961,6 +961,32 @@ angular.module('application.router', ['ui.router'])
|
|||||||
translations: ['Translations', function (Translations) { return Translations.query(['app.admin.members_new', 'app.shared.user', 'app.shared.user_admin']).$promise; }]
|
translations: ['Translations', function (Translations) { return Translations.query(['app.admin.members_new', 'app.shared.user', 'app.shared.user_admin']).$promise; }]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.state('app.admin.members_import', {
|
||||||
|
url: '/admin/members/import',
|
||||||
|
views: {
|
||||||
|
'main@': {
|
||||||
|
templateUrl: '<%= asset_path "admin/members/import.html" %>',
|
||||||
|
controller: 'ImportMembersController'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
translations: ['Translations', function (Translations) { return Translations.query(['app.admin.members_import', 'app.shared.user', 'app.shared.user_admin']).$promise; }],
|
||||||
|
tags: ['Tag', function(Tag) { return Tag.query().$promise }]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.state('app.admin.members_import_result', {
|
||||||
|
url: '/admin/members/import/:id/results',
|
||||||
|
views: {
|
||||||
|
'main@': {
|
||||||
|
templateUrl: '<%= asset_path "admin/members/import_result.html" %>',
|
||||||
|
controller: 'ImportMembersResultController'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
translations: ['Translations', function (Translations) { return Translations.query(['app.admin.members_import_result', 'app.shared.user', 'app.shared.user_admin']).$promise; }],
|
||||||
|
importItem: ['Import', '$stateParams', function(Import, $stateParams) { return Import.get({ id: $stateParams.id }).$promise }]
|
||||||
|
}
|
||||||
|
})
|
||||||
.state('app.admin.members_edit', {
|
.state('app.admin.members_edit', {
|
||||||
url: '/admin/members/:id/edit',
|
url: '/admin/members/:id/edit',
|
||||||
views: {
|
views: {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
Application.Services.factory('AuthService', ['Session', function (Session) {
|
Application.Services.factory('AuthService', ['Session', 'CSRF', function (Session, CSRF) {
|
||||||
return {
|
return {
|
||||||
isAuthenticated () {
|
isAuthenticated () {
|
||||||
return (Session.currentUser != null) && (Session.currentUser.id != null);
|
return (Session.currentUser != null) && (Session.currentUser.id != null);
|
7
app/assets/javascripts/services/import.js
Normal file
7
app/assets/javascripts/services/import.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Application.Services.factory('Import', ['$resource', function ($resource) {
|
||||||
|
return $resource('/api/imports/:id',
|
||||||
|
{ id: '@id' }
|
||||||
|
);
|
||||||
|
}]);
|
@ -10,6 +10,10 @@ Application.Services.factory('Invoice', ['$resource', function ($resource) {
|
|||||||
url: '/api/invoices/list',
|
url: '/api/invoices/list',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
isArray: true
|
isArray: true
|
||||||
|
},
|
||||||
|
first: {
|
||||||
|
url: '/api/invoices/first',
|
||||||
|
method: 'GET'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -9,6 +9,10 @@ Application.Services.factory('Setting', ['$resource', function ($resource) {
|
|||||||
return angular.toJson({ setting: data });
|
return angular.toJson({ setting: data });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
bulkUpdate: {
|
||||||
|
url: '/api/settings/bulk_update',
|
||||||
|
method: 'PATCH'
|
||||||
|
},
|
||||||
query: {
|
query: {
|
||||||
isArray: false
|
isArray: false
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
.text-blue { color: $blue; }
|
.text-blue { color: $blue; }
|
||||||
.text-muted { color: $text-muted; }
|
.text-muted { color: $text-muted; }
|
||||||
.text-danger, .red { color: $red !important; }
|
.text-danger, .red { color: $red !important; }
|
||||||
|
.text-red-only { color: $red !important; }
|
||||||
.text-purple { color: $violet !important; }
|
.text-purple { color: $violet !important; }
|
||||||
.text-japonica { color: $japonica !important; }
|
.text-japonica { color: $japonica !important; }
|
||||||
.text-beige { color: $beige !important; }
|
.text-beige { color: $beige !important; }
|
||||||
|
@ -65,6 +65,10 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-xl {
|
||||||
|
width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
// component card
|
// component card
|
||||||
.card {
|
.card {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -180,6 +180,11 @@ p, .widget p {
|
|||||||
.p-lg { padding: 30px; }
|
.p-lg { padding: 30px; }
|
||||||
.p-l { padding: 16px; }
|
.p-l { padding: 16px; }
|
||||||
|
|
||||||
|
.p-h-xs { padding-left: 5px; padding-right: 5px; }
|
||||||
|
.p-h-s { padding-left: 10px; padding-right: 10px; }
|
||||||
|
.p-h-l { padding-left: 16px; padding-right: 16px; }
|
||||||
|
.p-h-lg { padding-left: 30px; padding-right: 30px; }
|
||||||
|
|
||||||
.m-xxs{margin: 2px 4px}
|
.m-xxs{margin: 2px 4px}
|
||||||
.m-xs{margin: 5px;}
|
.m-xs{margin: 5px;}
|
||||||
.m-sm{margin: 10px;}
|
.m-sm{margin: 10px;}
|
||||||
@ -256,6 +261,14 @@ p, .widget p {
|
|||||||
.m-b-n-lg{margin-bottom: -30px}
|
.m-b-n-lg{margin-bottom: -30px}
|
||||||
.m-b-n-xl{margin-bottom: -40px}
|
.m-b-n-xl{margin-bottom: -40px}
|
||||||
|
|
||||||
|
.m-h-none{margin-left: 0; margin-right: 0;}
|
||||||
|
.m-h-xs{margin-left: 5px; margin-right: 5px;}
|
||||||
|
.m-h-sm{margin-left: 10px; margin-right: 10px;}
|
||||||
|
.m-h{margin-left: 15px; margin-right: 15px;}
|
||||||
|
.m-h-md{margin-left: 20px; margin-right: 20px;}
|
||||||
|
.m-h-lg{margin-left: 30px; margin-right: 30px;}
|
||||||
|
.m-h-xl{margin-left: 40px; margin-right: 40px;}
|
||||||
|
|
||||||
.media-xs{min-width: 50px}
|
.media-xs{min-width: 50px}
|
||||||
.media-sm{min-width: 80px}
|
.media-sm{min-width: 80px}
|
||||||
.media-md{min-width: 90px}
|
.media-md{min-width: 90px}
|
||||||
@ -306,6 +319,17 @@ p, .widget p {
|
|||||||
width: auto;
|
width: auto;
|
||||||
vertical-align: sub;
|
vertical-align: sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the two classes above are used in import results page for "import failed"
|
||||||
|
.fa-stack-inside {
|
||||||
|
font-size: 0.8em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-stack-outside {
|
||||||
|
font-size: 1.3em !important;
|
||||||
|
line-height: 1.7em !important;
|
||||||
|
}
|
||||||
|
|
||||||
.contrast-250 { -webkit-filter: contrast(250%); filter: contrast(250%); }
|
.contrast-250 { -webkit-filter: contrast(250%); filter: contrast(250%); }
|
||||||
.clear{display:block;overflow: hidden;}
|
.clear{display:block;overflow: hidden;}
|
||||||
|
|
||||||
@ -350,6 +374,11 @@ p, .widget p {
|
|||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: $screen-lg-min) {
|
@media screen and (min-width: $screen-lg-min) {
|
||||||
.b-r-lg {border-right: 1px solid $border-color; }
|
.b-r-lg {border-right: 1px solid $border-color; }
|
||||||
.hide-b-r-lg { border: none !important; }
|
.hide-b-r-lg { border: none !important; }
|
||||||
|
@ -276,3 +276,31 @@ table.scrollable-3-cols {
|
|||||||
input.form-control.as-writable {
|
input.form-control.as-writable {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.accounting-codes .row {
|
||||||
|
margin-top: 2rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table.export-table-template {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
thead td {
|
||||||
|
width: 20px;
|
||||||
|
background-color: #227447;
|
||||||
|
color: white;
|
||||||
|
border-bottom: 2px solid black;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 10px 5px;
|
||||||
|
line-height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody td {
|
||||||
|
border-right: 1px solid #d4d4d4;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="text-center red" translate>{{ 'invoices.export_accounting_data' }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form role="form" name="exportForm">
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label for="start_date" translate>{{ 'invoices.export_form_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_date"
|
||||||
|
id="start_date"
|
||||||
|
ng-model="exportTarget.startDate"
|
||||||
|
uib-datepicker-popup="{{datePicker.format}}"
|
||||||
|
datepicker-options="datePicker.options"
|
||||||
|
is-open="datePicker.opened.start"
|
||||||
|
min-date="firstInvoice"
|
||||||
|
placeholder="{{datePicker.format}}"
|
||||||
|
ng-click="toggleDatePicker($event, 'start')"
|
||||||
|
required/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label for="end_date" translate>{{ 'invoices.export_to_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_date"
|
||||||
|
id="end_date"
|
||||||
|
ng-model="exportTarget.endDate"
|
||||||
|
uib-datepicker-popup="{{datePicker.format}}"
|
||||||
|
datepicker-options="datePicker.options"
|
||||||
|
is-open="datePicker.opened.end"
|
||||||
|
min-date="exportTarget.startDate || firstInvoice"
|
||||||
|
placeholder="{{datePicker.format}}"
|
||||||
|
ng-click="toggleDatePicker($event, 'end')"
|
||||||
|
required/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<h4 class="control-label m-l" translate>{{ 'invoices.export_to' }}</h4>
|
||||||
|
<div class="form-group m-l-lg">
|
||||||
|
<label for="acd">
|
||||||
|
<input type="radio" name="acd" id="acd" ng-model="exportTarget.software" ng-value="'acd'" ng-click="fillSettings()" required/>
|
||||||
|
{{ 'invoices.acd' | translate }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row" ng-show="exportTarget.settings">
|
||||||
|
<div class="col-md-4 font-bold" translate>{{ 'invoices.format' }}</div>
|
||||||
|
<div class="col-md-8">{{ exportTarget.settings.format }}</div>
|
||||||
|
<div class="col-md-4 font-bold" translate>{{ 'invoices.encoding' }}</div>
|
||||||
|
<div class="col-md-8">{{ exportTarget.settings.encoding }}</div>
|
||||||
|
<div class="col-md-4 font-bold" translate>{{ 'invoices.separator' }}</div>
|
||||||
|
<div class="col-md-8">{{ exportTarget.settings.separator }}</div>
|
||||||
|
<div class="col-md-4 font-bold" translate>{{ 'invoices.dateFormat' }}</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<a href="https://apidock.com/ruby/DateTime/strftime" class="help-cursor" target="_blank">{{ exportTarget.settings.dateFormat }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 font-bold" translate>{{ 'invoices.labelMaxLength' }}</div>
|
||||||
|
<div class="col-md-8">{{ exportTarget.settings.labelMaxLength }}</div>
|
||||||
|
<div class="col-md-4 font-bold" translate>{{ 'invoices.decimalSeparator' }}</div>
|
||||||
|
<div class="col-md-8">{{ exportTarget.settings.decimalSeparator }}</div>
|
||||||
|
<div class="col-md-4 font-bold" translate>{{ 'invoices.exportInvoicesAtZero' }}</div>
|
||||||
|
<div class="col-md-8" translate>{{ exportTarget.settings.exportInvoicesAtZero ? 'yes' : 'no' }}</div>
|
||||||
|
<div class="col-md-4 font-bold" translate>{{ 'invoices.columns' }}</div>
|
||||||
|
<table class="col-md-12 export-table-template">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td ng-repeat="column in exportTarget.settings.columns" translate>{{ 'invoices.exportColumns.' + column }}</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td ng-repeat="column in exportTarget.settings.columns"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<form role="form" ng-submit="ok()" name="exportFormParams" method="post" action="{{ actionUrl }}" class="inline">
|
||||||
|
<input name="authenticity_token" type="hidden" ng-value="csrfToken"/>
|
||||||
|
<input name="_method" type="hidden" ng-value="method"/>
|
||||||
|
<input name="extension" type="hidden" ng-value="query.extension"/>
|
||||||
|
<input name="type" type="hidden" ng-value="exportTarget.software"/>
|
||||||
|
<input name="key" type="hidden" ng-value="query.key"/>
|
||||||
|
<input name="query" type="hidden" ng-value="query.query"/>
|
||||||
|
<input type="submit" class="btn btn-warning" value="{{ 'confirm' | translate }}" formtarget="export-frame"/>
|
||||||
|
</form>
|
||||||
|
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
|
||||||
|
</div>
|
@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label translate>{{ 'invoices.refund_mode' }}</label>
|
<label translate>{{ 'invoices.refund_mode' }}</label>
|
||||||
<select class="form-control m-t-sm" name="avoir_mode" ng-model="avoir.avoir_mode" ng-options="mode.value as mode.name for mode in avoirModes" required></select>
|
<select class="form-control m-t-sm" name="payment_method" ng-model="avoir.payment_method" ng-options="mode.value as mode.name for mode in avoirModes" required></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" ng-if="invoice.is_subscription_invoice">
|
<div class="form-group" ng-if="invoice.is_subscription_invoice">
|
||||||
<label translate>{{ 'invoices.do_you_want_to_disable_the_user_s_subscription' }}</label>
|
<label translate>{{ 'invoices.do_you_want_to_disable_the_user_s_subscription' }}</label>
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
|
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
|
||||||
<section class="heading-actions wrapper">
|
<section class="heading-actions wrapper">
|
||||||
|
<a class="btn btn-default rounded m-t-sm" ng-click="toggleExportModal()"><i class="fa fa-book"></i></a>
|
||||||
|
<iframe name="export-frame" height="0" width="0" class="none" id="accounting-export-frame"></iframe>
|
||||||
<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>
|
<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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@ -22,7 +24,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<uib-tabset justified="true">
|
<uib-tabset justified="true">
|
||||||
<uib-tab heading="{{ 'invoices.invoices_list' | translate }}">
|
<uib-tab heading="{{ 'invoices.invoices_list' | translate }}" ng-hide="fablabWithoutInvoices" active="tabs.listing.active">
|
||||||
<h3 class="m-t-xs"><i class="fa fa-filter"></i> {{ 'invoices.filter_invoices' | translate }}</h3>
|
<h3 class="m-t-xs"><i class="fa fa-filter"></i> {{ 'invoices.filter_invoices' | translate }}</h3>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -114,9 +116,13 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<uib-tab heading="{{ 'invoices.invoicing_settings' | translate }}">
|
<uib-tab heading="{{ 'invoices.invoicing_settings' | translate }}" active="tabs.settings.active">
|
||||||
|
<div class="alert alert-warning p-md m-t" role="alert" ng-show="fablabWithoutInvoices">
|
||||||
|
<i class="fa fa-warning m-r"></i>
|
||||||
|
<span translate>{{ 'invoices.warning_invoices_disabled' }}</span>
|
||||||
|
</div>
|
||||||
<form class="invoice-placeholder">
|
<form class="invoice-placeholder">
|
||||||
<div class="invoice-logo" style="background-image: url({{invoice.logo}});">
|
<div class="invoice-logo">
|
||||||
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:/font:FontAwesome/icon" bs-holder ng-if="!invoice.logo" class="img-responsive">
|
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:/font:FontAwesome/icon" bs-holder ng-if="!invoice.logo" class="img-responsive">
|
||||||
<img base-sixty-four-image="invoice.logo" ng-if="invoice.logo && invoice.logo.base64">
|
<img base-sixty-four-image="invoice.logo" ng-if="invoice.logo && invoice.logo.base64">
|
||||||
<div class="tools-box">
|
<div class="tools-box">
|
||||||
@ -201,6 +207,123 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</uib-tab>
|
</uib-tab>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<uib-tab heading="{{ 'invoices.accounting_codes' | translate }}">
|
||||||
|
<div class="panel panel-default m-t-md accounting-codes">
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="journalCode" translate>{{ 'invoices.accounting_journal_code' }}</label>
|
||||||
|
<input type="text" id="journalCode" ng-model="settings.journalCode.value" class="form-control" placeholder="{{ 'invoices.general_journal_code' | translate }}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="cardClientCode" translate>{{ 'invoices.accounting_card_client_code' }}</label>
|
||||||
|
<input type="text" id="cardClientCode" ng-model="settings.cardClientCode.value" class="form-control" placeholder="{{ 'invoices.card_client_code' | translate }}" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="cardClientLabel" translate>{{ 'invoices.accounting_card_client_label' }}</label>
|
||||||
|
<input type="text" id="cardClientLabel" ng-model="settings.cardClientLabel.value" class="form-control" placeholder="{{ 'invoices.card_client_label' | translate }}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="walletClientCode" translate>{{ 'invoices.accounting_wallet_client_code' }}</label>
|
||||||
|
<input type="text" id="walletClientCode" ng-model="settings.walletClientCode.value" class="form-control" placeholder="{{ 'invoices.wallet_client_code' | translate }}" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="walletClientLabel" translate>{{ 'invoices.accounting_wallet_client_label' }}</label>
|
||||||
|
<input type="text" id="walletClientLabel" ng-model="settings.walletClientLabel.value" class="form-control" placeholder="{{ 'invoices.wallet_client_label' | translate }}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="otherClientCode" translate>{{ 'invoices.accounting_other_client_code' }}</label>
|
||||||
|
<input type="text" id="otherClientCode" ng-model="settings.otherClientCode.value" class="form-control" placeholder="{{ 'invoices.other_client_code' | translate }}" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="otherClientLabel" translate>{{ 'invoices.accounting_other_client_label' }}</label>
|
||||||
|
<input type="text" id="otherClientLabel" ng-model="settings.otherClientLabel.value" class="form-control" placeholder="{{ 'invoices.other_client_label' | translate }}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="walletCode" translate>{{ 'invoices.accounting_wallet_code' }}</label>
|
||||||
|
<input type="text" id="walletCode" ng-model="settings.walletCode.value" class="form-control" placeholder="{{ 'invoices.general_wallet_code' | translate }}" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="walletLabel" translate>{{ 'invoices.accounting_wallet_label' }}</label>
|
||||||
|
<input type="text" id="walletLabel" ng-model="settings.walletLabel.value" class="form-control" placeholder="{{ 'invoices.general_wallet_label' | translate }}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="vatCode" translate>{{ 'invoices.accounting_vat_code' }}</label>
|
||||||
|
<input type="text" id="vatCode" ng-model="settings.vatCode.value" class="form-control" placeholder="{{ 'invoices.general_vat_code' | translate }}"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="vatLabel" translate>{{ 'invoices.accounting_vat_label' }}</label>
|
||||||
|
<input type="text" id="vatLabel" ng-model="settings.vatLabel.value" class="form-control" placeholder="{{ 'invoices.general_vat_label' | translate }}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="subscriptionCode" translate>{{ 'invoices.accounting_subscription_code' }}</label>
|
||||||
|
<input type="text" id="subscriptionCode" ng-model="settings.subscriptionCode.value" class="form-control" placeholder="{{ 'invoices.general_subscription_code' | translate }}" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="subscriptionLabel" translate>{{ 'invoices.accounting_subscription_label' }}</label>
|
||||||
|
<input type="text" id="subscriptionLabel" ng-model="settings.subscriptionLabel.value" class="form-control" placeholder="{{ 'invoices.general_subscription_label' | translate }}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="machineCode" translate>{{ 'invoices.accounting_Machine_code' }}</label>
|
||||||
|
<input type="text" id="machineCode" ng-model="settings.machineCode.value" class="form-control" placeholder="{{ 'invoices.general_machine_code' | translate }}"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="machineLabel" translate>{{ 'invoices.accounting_Machine_label' }}</label>
|
||||||
|
<input type="text" id="machineLabel" ng-model="settings.machineLabel.value" class="form-control" placeholder="{{ 'invoices.general_machine_label' | translate }}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="trainingCode" translate>{{ 'invoices.accounting_Training_code' }}</label>
|
||||||
|
<input type="text" id="trainingCode" ng-model="settings.trainingCode.value" class="form-control" placeholder="{{ 'invoices.general_training_code' | translate }}" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="trainingLabel" translate>{{ 'invoices.accounting_Training_label' }}</label>
|
||||||
|
<input type="text" id="trainingLabel" ng-model="settings.trainingLabel.value" class="form-control" placeholder="{{ 'invoices.general_training_label' | translate }}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="eventCode" translate>{{ 'invoices.accounting_Event_code' }}</label>
|
||||||
|
<input type="text" id="eventCode" ng-model="settings.eventCode.value" class="form-control" placeholder="{{ 'invoices.general_event_code' | translate }}"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="eventLabel" translate>{{ 'invoices.accounting_Event_label' }}</label>
|
||||||
|
<input type="text" id="eventLabel" ng-model="settings.eventLabel.value" class="form-control" placeholder="{{ 'invoices.general_event_label' | translate }}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="spaceCode" translate>{{ 'invoices.accounting_Space_code' }}</label>
|
||||||
|
<input type="text" id="spaceCode" ng-model="settings.spaceCode.value" class="form-control" placeholder="{{ 'invoices.general_space_code' | translate }}" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="spaceLabel" translate>{{ 'invoices.accounting_Space_label' }}</label>
|
||||||
|
<input type="text" id="spaceLabel" ng-model="settings.spaceLabel.value" class="form-control" placeholder="{{ 'invoices.general_space_label' | translate }}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button name="button" class="btn btn-warning m-t-lg" ng-click="save()" translate>{{ 'save' }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</uib-tab>
|
||||||
</uib-tabset>
|
</uib-tabset>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -409,8 +532,9 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr ng-repeat="value in history | orderBy:'-date'">
|
<tr ng-repeat="value in history | orderBy:'-date'">
|
||||||
<td>
|
<td>
|
||||||
<span class="no-user-label" ng-show="value.rate === 0" translate>{{'invoices.VAT_disabled'}}</span>
|
<span class="no-user-label" ng-show="value.enabled === false" translate>{{'invoices.VAT_disabled'}}</span>
|
||||||
<span ng-hide="value.rate === 0">{{value.rate}}</span>
|
<span class="no-user-label" ng-show="value.enabled === true" translate>{{'invoices.VAT_enabled'}}</span>
|
||||||
|
<span ng-show="value.rate">{{value.rate}}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{value.date | amDateFormat:'L LT'}}</td>
|
<td>{{value.date | amDateFormat:'L LT'}}</td>
|
||||||
<td>{{value.user.name}}<span class="no-user-label" ng-hide="value.user" translate>{{ 'invoices.deleted_user' }}</span></td>
|
<td>{{value.user.name}}<span class="no-user-label" ng-hide="value.user" translate>{{ 'invoices.deleted_user' }}</span></td>
|
||||||
|
@ -187,7 +187,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</uib-tab>
|
</uib-tab>
|
||||||
|
|
||||||
<uib-tab heading="{{ 'invoices' | translate }}">
|
<uib-tab heading="{{ 'invoices' | translate }}" ng-hide="fablabWithoutInvoices">
|
||||||
<div class="col-md-12 m m-t-lg">
|
<div class="col-md-12 m m-t-lg">
|
||||||
|
|
||||||
|
|
||||||
|
177
app/assets/templates/admin/members/import.html.erb
Normal file
177
app/assets/templates/admin/members/import.html.erb
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
<div>
|
||||||
|
|
||||||
|
<section class="heading b-b">
|
||||||
|
<div class="row no-gutter">
|
||||||
|
|
||||||
|
<div class="col-md-1 hidden-xs">
|
||||||
|
<section class="heading-btn">
|
||||||
|
<a ng-click="cancel()"><i class="fa fa-long-arrow-left"></i></a>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-8 b-l b-r">
|
||||||
|
<section class="heading-title">
|
||||||
|
<h1 translate>{{ 'members_import.import_members' }}</h1>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<section class="heading-actions wrapper">
|
||||||
|
<a class="btn btn-lg btn-block btn-default m-t-xs" target="_blank" href="example.csv" translate>
|
||||||
|
{{ 'members_import.download_example' }}
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="row p-sm">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<p class="alert alert-info" translate>
|
||||||
|
{{ 'members_import.info' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row m-h-sm">
|
||||||
|
|
||||||
|
<div class="col-md-6 p-h-s">
|
||||||
|
<h3 translate>{{ 'members_import.groups' }}</h3>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th translate>{{ 'members_import.group_name' }}</th>
|
||||||
|
<th translate>{{ 'members_import.group_identifier' }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="group in groups">
|
||||||
|
<td>
|
||||||
|
{{ group.name }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ group.slug }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 p-h-s">
|
||||||
|
<h3 translate>{{ 'members_import.trainings' }}</h3>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th translate>{{ 'members_import.training_name' }}</th>
|
||||||
|
<th translate>{{ 'members_import.training_identifier' }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="training in trainings | filterDisabled">
|
||||||
|
<td>
|
||||||
|
{{ training.name }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ training.id }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row m-h-sm">
|
||||||
|
|
||||||
|
<div class="col-md-6 p-h-s" ng-hide="tags.length == 0">
|
||||||
|
<h3 translate>{{ 'members_import.tags' }}</h3>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th translate>{{ 'members_import.tag_name' }}</th>
|
||||||
|
<th translate>{{ 'members_import.tag_identifier' }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="tag in tags">
|
||||||
|
<td>
|
||||||
|
{{ tag.name }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ tag.id }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row no-gutter">
|
||||||
|
<div class="col-sm-12 col-md-12 b-r nopadding">
|
||||||
|
|
||||||
|
<form role="form" name="importForm" class="form-horizontal" novalidate action="{{ actionUrl }}" ng-upload="onImportResult(content)" upload-options-enable-rails-csrf="true">
|
||||||
|
<section class="panel panel-default bg-light m-lg">
|
||||||
|
|
||||||
|
<div class="panel-body m-r">
|
||||||
|
|
||||||
|
<div class="m-t">
|
||||||
|
<p class="alert alert-warning m-h" translate>
|
||||||
|
{{ 'members_import.required_fields' }}
|
||||||
|
</p>
|
||||||
|
<p class="alert alert-warning m-h" translate>
|
||||||
|
{{ 'members_import.about_example' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass()">
|
||||||
|
<div class="form-control" data-trigger="fileinput">
|
||||||
|
<i class="glyphicon glyphicon-file fileinput-exists"></i> <span class="fileinput-filename">{{file.attachment}}</span>
|
||||||
|
</div>
|
||||||
|
<span class="input-group-addon btn btn-default btn-file"><span class="fileinput-new" translate>{{ 'members_import.select_file' }}</span>
|
||||||
|
<span class="fileinput-exists" translate>{{ 'change' }}</span>
|
||||||
|
<input type="file"
|
||||||
|
name="import_members"
|
||||||
|
accept="text/csv"
|
||||||
|
required></span>
|
||||||
|
<a class="input-group-addon btn btn-danger fileinput-exists" data-dismiss="fileinput" ng-click="deleteFile(file)"><i class="fa fa-trash-o"></i></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="m-h">
|
||||||
|
<span translate>{{ 'members_import.update_field' }}</span>
|
||||||
|
<div class="radio m-l-md">
|
||||||
|
<label class="control-label">
|
||||||
|
<input type="radio" id="update_field" name="update_field" value="id" checked>
|
||||||
|
<span translate>{{ 'members_import.update_on_id' }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="radio m-l-md">
|
||||||
|
<label class="control-label">
|
||||||
|
<input type="radio" id="update_field" name="update_field" value="username">
|
||||||
|
<span translate>{{ 'members_import.update_on_username' }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="radio m-l-md">
|
||||||
|
<label class="control-label">
|
||||||
|
<input type="radio" id="update_field" name="update_field" value="email">
|
||||||
|
<span translate>{{ 'members_import.update_on_email' }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div> <!-- ./panel-body -->
|
||||||
|
|
||||||
|
|
||||||
|
<div class="panel-footer no-padder">
|
||||||
|
<input type="submit" value="{{ 'members_import.import' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="importForm.$invalid"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
66
app/assets/templates/admin/members/import_result.html
Normal file
66
app/assets/templates/admin/members/import_result.html
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<div>
|
||||||
|
|
||||||
|
<section class="heading b-b">
|
||||||
|
<div class="row no-gutter">
|
||||||
|
|
||||||
|
<div class="col-md-1 hidden-xs">
|
||||||
|
<section class="heading-btn">
|
||||||
|
<a ng-click="cancel()"><i class="fa fa-long-arrow-left"></i></a>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-8 b-l">
|
||||||
|
<section class="heading-title">
|
||||||
|
<h1 translate>{{ 'members_import_result.import_results' }}</h1>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row no-gutter">
|
||||||
|
<div class="col-sm-12 col-md-12">
|
||||||
|
|
||||||
|
<h2 class="m-l-lg">{{ 'members_import_result.import_details' | translate:{DATE:(import.created_at | amDateFormat:'L'), USER:import.user.full_name, ID:import.id} }}</h2>
|
||||||
|
|
||||||
|
<h3 class="m-l-lg" ng-hide="results"><i class="fa fa-spinner fa-pulse"></i> <span translate>{{ 'members_import_result.pending' }}</span></h3>
|
||||||
|
<div ng-show="results">
|
||||||
|
<h3 class="m-l-lg" translate>{{ 'members_import_result.results' }}</h3>
|
||||||
|
|
||||||
|
<div class="row p-h-lg" ng-repeat="resultRow in results track by $index">
|
||||||
|
<div class="scroll-x">
|
||||||
|
<table class="table table-bordered font-thin text-xs m-t-lg" ng-if="resultRow.row">
|
||||||
|
<tr>
|
||||||
|
<th ng-repeat="(key, value) in resultRow.row">{{key}}</th>
|
||||||
|
</tr>
|
||||||
|
<tr class="text-nowrap">
|
||||||
|
<td ng-repeat="(key, value) in resultRow.row">{{value}}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div ng-if="resultRow.status">
|
||||||
|
<i class="fa fa-arrow-right m-l-lg m-r"></i>
|
||||||
|
<span class="m-r-md">{{ 'members_import_result.status_' + resultRow.status | translate:{ID:resultRow.user} }}</span>
|
||||||
|
<span ng-show="resultRow.result" class="green font-bold">
|
||||||
|
<i class="fa fa-check-square-o fa-stack-outside"></i>
|
||||||
|
<span class="m-l" translate>{{ 'members_import_result.success' }}</span>
|
||||||
|
</span>
|
||||||
|
<span ng-hide="resultRow.result" class="text-red-only font-bold">
|
||||||
|
<span class="fa-stack v-bottom">
|
||||||
|
<i class="fa fa-square-o fa-stack-1x fa-stack-outside"></i>
|
||||||
|
<i class="fa fa-times fa-stack-1x fa-stack-inside"></i>
|
||||||
|
</span>
|
||||||
|
<span class="m-l" translate>{{ 'members_import_result.failed' }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="m-l-lg text-red-only" ng-if="!resultRow.row && !resultRow.status">
|
||||||
|
<span class="m-r" translate>{{ 'members_import_result.error_details' }}</span>{{resultRow}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
@ -5,11 +5,18 @@
|
|||||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
|
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
<div class="col-xs-8 col-sm-8 col-md-8 b-l">
|
||||||
<section class="heading-title">
|
<section class="heading-title">
|
||||||
<h1 translate>{{ 'users_management' }}</h1>
|
<h1 translate>{{ 'users_management' }}</h1>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-xs-1 col-xs-offset-1 col-md-offset-2 b-l">
|
||||||
|
<section class="heading-actions wrapper">
|
||||||
|
<a role="button" class="btn btn-default b-2x rounded m-t-sm" ui-sref="app.admin.members_import">
|
||||||
|
<i class="fa fa-cloud-upload"></i>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -36,8 +36,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr ng-repeat="plan in plans | filterDisabled:planFiltering | orderBy:orderPlans"
|
<tr ng-repeat="plan in plans | filterDisabled:planFiltering | orderBy:orderPlans"
|
||||||
ng-class="{'disabled-line' : plan.disabled && planFiltering === 'all'}"
|
ng-class="{'disabled-line' : plan.disabled && planFiltering === 'all'}"
|
||||||
ng-init="group = getGroupFromId(groups, plan.group_id)"
|
ng-init="group = getGroupFromId(groups, plan.group_id)">
|
||||||
ng-hide="group.disabled">
|
|
||||||
<td>{{getPlanType(plan.type)}}</td>
|
<td>{{getPlanType(plan.type)}}</td>
|
||||||
<td>{{plan.base_name}}</td>
|
<td>{{plan.base_name}}</td>
|
||||||
<td>{{ plan.interval | planIntervalFilter:plan.interval_count }}</td>
|
<td>{{ plan.interval | planIntervalFilter:plan.interval_count }}</td>
|
||||||
|
@ -76,6 +76,9 @@
|
|||||||
<ui-select-choices ui-disable-choice="m.disabled" repeat="m.id as m in (machines | filter: $select.search)">
|
<ui-select-choices ui-disable-choice="m.disabled" repeat="m.id as m in (machines | filter: $select.search)">
|
||||||
<span ng-bind-html="m.name | highlight: $select.search"></span>
|
<span ng-bind-html="m.name | highlight: $select.search"></span>
|
||||||
</ui-select-choices>
|
</ui-select-choices>
|
||||||
|
<ui-select-no-choice>
|
||||||
|
<input type="hidden" name="training[machine_ids][]" value="" />
|
||||||
|
</ui-select-no-choice>
|
||||||
</ui-select>
|
</ui-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<li ui-sref-active="active"><a class="text-black" href="#" ui-sref="app.logged.dashboard.projects" translate>{{ 'my_projects' }}</a></li>
|
<li ui-sref-active="active"><a class="text-black" href="#" ui-sref="app.logged.dashboard.projects" translate>{{ 'my_projects' }}</a></li>
|
||||||
<li ui-sref-active="active"><a class="text-black" href="#" ui-sref="app.logged.dashboard.trainings" translate>{{ 'my_trainings' }}</a></li>
|
<li ui-sref-active="active"><a class="text-black" href="#" ui-sref="app.logged.dashboard.trainings" translate>{{ 'my_trainings' }}</a></li>
|
||||||
<li ui-sref-active="active"><a class="text-black" href="#" ui-sref="app.logged.dashboard.events" translate>{{ 'my_events' }}</a></li>
|
<li ui-sref-active="active"><a class="text-black" href="#" ui-sref="app.logged.dashboard.events" translate>{{ 'my_events' }}</a></li>
|
||||||
<li ui-sref-active="active"><a class="text-black" href="#" ui-sref="app.logged.dashboard.invoices" translate>{{ 'my_invoices' }}</a></li>
|
<li ui-sref-active="active" ng-hide="fablabWithoutInvoices"><a class="text-black" href="#" ui-sref="app.logged.dashboard.invoices" translate>{{ 'my_invoices' }}</a></li>
|
||||||
<li ui-sref-active="active"><a class="text-black" href="#" ui-sref="app.logged.dashboard.wallet" translate>{{ 'my_wallet' }}</a></li>
|
<li ui-sref-active="active"><a class="text-black" href="#" ui-sref="app.logged.dashboard.wallet" translate>{{ 'my_wallet' }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row col-md-2 col-md-offset-5 hidden-sm hidden-xs">
|
<div class="row col-md-2 col-md-offset-5 hidden-sm hidden-xs" ng-hide="user.merged_at">
|
||||||
<p class="font-felt fleche-left text-lg upper text-center">
|
<p class="font-felt fleche-left text-lg upper text-center">
|
||||||
<%= image_tag("fleche-left.png", class: 'fleche-left visible-lg visible-md fleche-left-from-top') %>
|
<%= image_tag("fleche-left.png", class: 'fleche-left visible-lg visible-md fleche-left-from-top') %>
|
||||||
<span class="or" translate>{{ 'or' }}</span>
|
<span class="or" translate>{{ 'or' }}</span>
|
||||||
@ -71,10 +71,15 @@
|
|||||||
<!-- group -->
|
<!-- group -->
|
||||||
<div class="form-group" ng-class="{'has-error': userForm['user[group_id]'].$dirty && userForm['user[group_id]'].$invalid}">
|
<div class="form-group" ng-class="{'has-error': userForm['user[group_id]'].$dirty && userForm['user[group_id]'].$invalid}">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-addon"><i class="fa fa-users"></i></span>
|
<span class="input-group-addon">
|
||||||
|
<i class="fa fa-users"></i>
|
||||||
|
<span class="exponent m-l-xs help-cursor" title="{{ 'used_for_statistics' | translate }}">
|
||||||
|
<i class="fa fa-asterisk" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
<select ng-model="user.group_id" class="form-control" name="user[group_id]" required>
|
<select ng-model="user.group_id" class="form-control" name="user[group_id]" required>
|
||||||
<option value=null translate>{{ 'your_user_s_profile' }}</option>
|
<option value=null translate>{{ 'your_user_s_profile' }}</option>
|
||||||
<option ng-repeat="group in groups" value="{{group.id}}" ng-selected="group.id == user.group_id">{{group.name}}</option>
|
<option ng-repeat="group in groups" ng-value="group.id" ng-selected="group.id == user.group_id">{{group.name}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<span class="help-block" ng-show="userForm['user[group_id]'].$dirty && userForm['user[group_id]'].$error.required" translate>{{ 'user_s_profile_is_required' }}</span>
|
<span class="help-block" ng-show="userForm['user[group_id]'].$dirty && userForm['user[group_id]'].$error.required" translate>{{ 'user_s_profile_is_required' }}</span>
|
||||||
@ -86,7 +91,9 @@
|
|||||||
name="cgu"
|
name="cgu"
|
||||||
ng-model="user.cgu"
|
ng-model="user.cgu"
|
||||||
value="true"
|
value="true"
|
||||||
ng-required="cgu != null"/> {{ 'i_ve_read_and_i_accept_' | translate }} <a href="{{cgu.custom_asset_file_attributes.attachment_url}}" target="_blank" translate>{{ '_the_fablab_policy' }}</a>
|
ng-required="cgu != null"/> {{ 'i_ve_read_and_i_accept_' | translate }}
|
||||||
|
<a href="{{cgu.custom_asset_file_attributes.attachment_url}}" target="_blank" translate>{{ '_the_fablab_policy' }}</a>
|
||||||
|
<span class="exponent m-l-xs"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -39,23 +39,15 @@
|
|||||||
<li><a href="#" ui-sref="app.logged.dashboard.projects" translate>{{ 'my_projects' }}</a></li>
|
<li><a href="#" ui-sref="app.logged.dashboard.projects" translate>{{ 'my_projects' }}</a></li>
|
||||||
<li><a href="#" ui-sref="app.logged.dashboard.trainings" translate>{{ 'my_trainings' }}</a></li>
|
<li><a href="#" ui-sref="app.logged.dashboard.trainings" translate>{{ 'my_trainings' }}</a></li>
|
||||||
<li><a href="#" ui-sref="app.logged.dashboard.events" translate>{{ 'my_events' }}</a></li>
|
<li><a href="#" ui-sref="app.logged.dashboard.events" translate>{{ 'my_events' }}</a></li>
|
||||||
<li><a href="#" ui-sref="app.logged.dashboard.invoices" translate>{{ 'my_invoices' }}</a></li>
|
<li><a href="#" ui-sref="app.logged.dashboard.invoices" ng-hide="fablabWithoutInvoices" translate>{{ 'my_invoices' }}</a></li>
|
||||||
<li><a href="#" ui-sref="app.logged.dashboard.wallet" translate>{{ 'my_wallet' }}</a></li>
|
<li><a href="#" ui-sref="app.logged.dashboard.wallet" translate>{{ 'my_wallet' }}</a></li>
|
||||||
|
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li><a href="#" class="text-black" ng-click="logout($event)"><i class="fa fa-power-off"></i> {{ 'sign_out' | translate }}</a></li>
|
<li><a href="#" class="text-black" ng-click="logout($event)"><i class="fa fa-power-off"></i> {{ 'sign_out' | translate }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<% active_provider = AuthProvider.active %>
|
|
||||||
<% if active_provider.providable_type == DatabaseProvider.name %>
|
|
||||||
<li ng-if="!isAuthenticated()"><a href="#" class="font-sbold label text-md" ng-click="signup($event)"><i class="fa fa-rocket"></i> {{ 'sign_up' | translate }}</a></li>
|
<li ng-if="!isAuthenticated()"><a href="#" class="font-sbold label text-md" ng-click="signup($event)"><i class="fa fa-rocket"></i> {{ 'sign_up' | translate }}</a></li>
|
||||||
<li ng-if="!isAuthenticated()">
|
<li ng-if="!isAuthenticated()">
|
||||||
<a href="#" class="font-sbold label text-md" ng-click="login($event)"><i class="fa fa-sign-in"></i> {{ 'sign_in' | translate }}</a>
|
<a href="#" class="font-sbold label text-md" ng-click="login($event)"><i class="fa fa-sign-in"></i> {{ 'sign_in' | translate }}</a>
|
||||||
</li>
|
</li>
|
||||||
<% else %>
|
|
||||||
<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="<%= "/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>
|
</ul>
|
||||||
|
@ -56,7 +56,7 @@
|
|||||||
<i class="fa fa-calendar-o"></i> <span translate>{{ 'my_events' }}</span>
|
<i class="fa fa-calendar-o"></i> <span translate>{{ 'my_events' }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="hidden-sm hidden-md hidden-lg" ng-if-end>
|
<li class="hidden-sm hidden-md hidden-lg" ng-hide="fablabWithoutInvoices" ng-if-end>
|
||||||
<a href="#" ui-sref="app.logged.dashboard.invoices">
|
<a href="#" ui-sref="app.logged.dashboard.invoices">
|
||||||
<i class="fa fa-file-pdf-o"></i> <span translate>{{ 'my_invoices' }}</span>
|
<i class="fa fa-file-pdf-o"></i> <span translate>{{ 'my_invoices' }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -247,6 +247,12 @@
|
|||||||
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></label>
|
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div vc-recaptcha
|
||||||
|
key="recaptchaSiteKey"
|
||||||
|
class="flex-center"
|
||||||
|
ng-model="user.recaptcha"
|
||||||
|
ng-if="recaptchaSiteKey">
|
||||||
|
</div>
|
||||||
<span class="info-required">
|
<span class="info-required">
|
||||||
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
|
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
|
||||||
<span translate>{{ 'field_required' }}</span>
|
<span translate>{{ 'field_required' }}</span>
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
<div class="text-right m-t">
|
<div class="text-right m-t" ng-hide="fablabWithoutInvoices">
|
||||||
<label for="generate_avoir" translate>{{ 'generate_a_refund_invoice' }}</label>
|
<label for="generate_avoir" translate>{{ 'generate_a_refund_invoice' }}</label>
|
||||||
<div class="inline m-l">
|
<div class="inline m-l">
|
||||||
<input bs-switch
|
<input bs-switch
|
||||||
|
35
app/controllers/api/accounting_exports_controller.rb
Normal file
35
app/controllers/api/accounting_exports_controller.rb
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# API Controller for exporting accounting data to external accounting softwares
|
||||||
|
class API::AccountingExportsController < API::ApiController
|
||||||
|
|
||||||
|
before_action :authenticate_user!
|
||||||
|
|
||||||
|
def export
|
||||||
|
authorize :accounting_export
|
||||||
|
|
||||||
|
export = Export.where(category: 'accounting', export_type: 'accounting-software', key: params[:key])
|
||||||
|
.where(extension: params[:extension], query: params[:query])
|
||||||
|
.where('created_at > ?', Invoice.maximum('updated_at'))
|
||||||
|
.last
|
||||||
|
if export.nil? || !FileTest.exist?(export.file)
|
||||||
|
@export = Export.new(
|
||||||
|
category: 'accounting',
|
||||||
|
export_type: params[:type],
|
||||||
|
user: current_user,
|
||||||
|
extension: params[:extension],
|
||||||
|
query: params[:query],
|
||||||
|
key: params[:key]
|
||||||
|
)
|
||||||
|
if @export.save
|
||||||
|
render json: { export_id: @export.id }, status: :ok
|
||||||
|
else
|
||||||
|
render json: @export.errors, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
else
|
||||||
|
send_file File.join(Rails.root, export.file),
|
||||||
|
type: 'text/csv',
|
||||||
|
disposition: 'attachment'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -8,10 +8,17 @@ class API::ExportsController < API::ApiController
|
|||||||
|
|
||||||
def download
|
def download
|
||||||
authorize @export
|
authorize @export
|
||||||
|
mime_type = if @export.extension == 'xlsx'
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
|
elsif @export.extension == 'csv'
|
||||||
|
'text/csv'
|
||||||
|
else
|
||||||
|
'application/octet-stream'
|
||||||
|
end
|
||||||
|
|
||||||
if FileTest.exist?(@export.file)
|
if FileTest.exist?(@export.file)
|
||||||
send_file File.join(Rails.root, @export.file),
|
send_file File.join(Rails.root, @export.file),
|
||||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
type: mime_type,
|
||||||
disposition: 'attachment'
|
disposition: 'attachment'
|
||||||
else
|
else
|
||||||
render text: I18n.t('errors.messages.export_not_found'), status: :not_found
|
render text: I18n.t('errors.messages.export_not_found'), status: :not_found
|
||||||
@ -21,28 +28,14 @@ class API::ExportsController < API::ApiController
|
|||||||
def status
|
def status
|
||||||
authorize Export
|
authorize Export
|
||||||
|
|
||||||
export = Export.where(category: params[:category], export_type: params[:type], query: params[:query], key: params[:key])
|
exports = Export.where(
|
||||||
|
category: params[:category],
|
||||||
if params[:category] == 'users'
|
export_type: params[:type],
|
||||||
case params[:type]
|
query: params[:query],
|
||||||
when 'subscriptions'
|
key: params[:key],
|
||||||
export = export.where('created_at > ?', Subscription.maximum('updated_at'))
|
extension: params[:extension]
|
||||||
when 'reservations'
|
)
|
||||||
export = export.where('created_at > ?', Reservation.maximum('updated_at'))
|
export = retrieve_last_export(exports, params[:category], params[:type])
|
||||||
when 'members'
|
|
||||||
export = export.where('created_at > ?', User.with_role(:member).maximum('updated_at'))
|
|
||||||
else
|
|
||||||
raise ArgumentError, "Unknown export users/#{params[:type]}"
|
|
||||||
end
|
|
||||||
elsif params[:category] == 'availabilities'
|
|
||||||
case params[:type]
|
|
||||||
when 'index'
|
|
||||||
export = export.where('created_at > ?', [Availability.maximum('updated_at'), Reservation.maximum('updated_at')].max)
|
|
||||||
else
|
|
||||||
raise ArgumentError, "Unknown type availabilities/#{params[:type]}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
export = export.last
|
|
||||||
|
|
||||||
if export.nil? || !FileTest.exist?(export.file)
|
if export.nil? || !FileTest.exist?(export.file)
|
||||||
render json: { exists: false, id: nil }, status: :ok
|
render json: { exists: false, id: nil }, status: :ok
|
||||||
@ -53,6 +46,39 @@ class API::ExportsController < API::ApiController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def retrieve_last_export(export, category, type)
|
||||||
|
case category
|
||||||
|
when 'users'
|
||||||
|
case type
|
||||||
|
when 'subscriptions'
|
||||||
|
export = export.where('created_at > ?', Subscription.maximum('updated_at'))
|
||||||
|
when 'reservations'
|
||||||
|
export = export.where('created_at > ?', Reservation.maximum('updated_at'))
|
||||||
|
when 'members'
|
||||||
|
export = export.where('created_at > ?', User.with_role(:member).maximum('updated_at'))
|
||||||
|
else
|
||||||
|
raise ArgumentError, "Unknown export users/#{type}"
|
||||||
|
end
|
||||||
|
when 'availabilities'
|
||||||
|
case type
|
||||||
|
when 'index'
|
||||||
|
export = export.where('created_at > ?', [Availability.maximum('updated_at'), Reservation.maximum('updated_at')].max)
|
||||||
|
else
|
||||||
|
raise ArgumentError, "Unknown type availabilities/#{type}"
|
||||||
|
end
|
||||||
|
when 'accounting'
|
||||||
|
case type
|
||||||
|
when 'acd'
|
||||||
|
export = export.where('created_at > ?', Invoice.maximum('updated_at'))
|
||||||
|
else
|
||||||
|
raise ArgumentError, "Unknown type accounting/#{type}"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise ArgumentError, "Unknown category #{category}"
|
||||||
|
end
|
||||||
|
export.last
|
||||||
|
end
|
||||||
|
|
||||||
def set_export
|
def set_export
|
||||||
@export = Export.find(params[:id])
|
@export = Export.find(params[:id])
|
||||||
end
|
end
|
||||||
|
34
app/controllers/api/imports_controller.rb
Normal file
34
app/controllers/api/imports_controller.rb
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# API Controller for resources of type Import
|
||||||
|
class API::ImportsController < API::ApiController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize Import
|
||||||
|
|
||||||
|
@import = Import.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def members
|
||||||
|
authorize Import
|
||||||
|
|
||||||
|
@import = Import.new(
|
||||||
|
attachment: import_params,
|
||||||
|
user: current_user,
|
||||||
|
update_field: params[:update_field],
|
||||||
|
category: 'members'
|
||||||
|
)
|
||||||
|
if @import.save
|
||||||
|
render json: { id: @import.id }, status: :created
|
||||||
|
else
|
||||||
|
render json: @import.errors, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def import_params
|
||||||
|
params.require(:import_members)
|
||||||
|
end
|
||||||
|
end
|
@ -51,10 +51,16 @@ class API::InvoicesController < API::ApiController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def first
|
||||||
|
authorize Invoice
|
||||||
|
invoice = Invoice.order(:created_at).first
|
||||||
|
@first = invoice&.created_at
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def avoir_params
|
def avoir_params
|
||||||
params.require(:avoir).permit(:invoice_id, :avoir_date, :avoir_mode, :subscription_to_expire, :description,
|
params.require(:avoir).permit(:invoice_id, :avoir_date, :payment_method, :subscription_to_expire, :description,
|
||||||
invoice_items_ids: [])
|
invoice_items_ids: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -18,6 +18,19 @@ class API::SettingsController < API::ApiController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def bulk_update
|
||||||
|
authorize Setting
|
||||||
|
|
||||||
|
@settings = []
|
||||||
|
params[:settings].each do |setting|
|
||||||
|
next if !setting[:name] || !setting[:value]
|
||||||
|
|
||||||
|
db_setting = Setting.find_or_initialize_by(name: setting[:name])
|
||||||
|
db_setting.save && db_setting.history_values.create(value: setting[:value], invoicing_profile: current_user.invoicing_profile)
|
||||||
|
@settings.push db_setting
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@setting = Setting.find_or_create_by(name: params[:name])
|
@setting = Setting.find_or_create_by(name: params[:name])
|
||||||
@show_history = params[:history] == 'true' && current_user.admin?
|
@show_history = params[:history] == 'true' && current_user.admin?
|
||||||
|
@ -13,7 +13,7 @@ class API::UsersController < API::ApiController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if current_user.admin?
|
authorize User
|
||||||
res = UserService.create_partner(partner_params)
|
res = UserService.create_partner(partner_params)
|
||||||
|
|
||||||
if res[:saved]
|
if res[:saved]
|
||||||
@ -22,9 +22,6 @@ class API::UsersController < API::ApiController
|
|||||||
else
|
else
|
||||||
render json: res[:user].errors.full_messages, status: :unprocessable_entity
|
render json: res[:user].errors.full_messages, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
else
|
|
||||||
head 403
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -19,6 +19,12 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
def index; end
|
def index; end
|
||||||
|
|
||||||
|
def sso_redirect
|
||||||
|
@authorization_token = request.query_parameters[:auth_token]
|
||||||
|
@authentication_token = form_authenticity_token
|
||||||
|
@active_provider = AuthProvider.active
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def set_csrf_cookie
|
def set_csrf_cookie
|
||||||
|
@ -4,6 +4,11 @@
|
|||||||
class RegistrationsController < Devise::RegistrationsController
|
class RegistrationsController < Devise::RegistrationsController
|
||||||
# POST /users.json
|
# POST /users.json
|
||||||
def create
|
def create
|
||||||
|
# first check the recaptcha
|
||||||
|
check = RecaptchaService.verify(params[:user][:recaptcha])
|
||||||
|
render json: check['error-codes'], status: :unprocessable_entity and return unless check['success']
|
||||||
|
|
||||||
|
# then create the user
|
||||||
build_resource(sign_up_params)
|
build_resource(sign_up_params)
|
||||||
|
|
||||||
resource_saved = resource.save
|
resource_saved = resource.save
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# RSS feed about 10 last events
|
||||||
class Rss::EventsController < Rss::RssController
|
class Rss::EventsController < Rss::RssController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -6,7 +6,7 @@ class SessionsController < Devise::SessionsController
|
|||||||
def new
|
def new
|
||||||
active_provider = AuthProvider.active
|
active_provider = AuthProvider.active
|
||||||
if active_provider.providable_type != DatabaseProvider.name
|
if active_provider.providable_type != DatabaseProvider.name
|
||||||
redirect_to "/users/auth/#{active_provider.strategy_name}"
|
redirect_post "/users/auth/#{active_provider.strategy_name}", params: { authenticity_token: form_authenticity_token }
|
||||||
else
|
else
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
@ -17,7 +17,7 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||||||
# unique random string, because:
|
# unique random string, because:
|
||||||
# - if it is the same user, his email will be filled from the SSO when he merge his accounts
|
# - if it is the same user, his email will be filled from the SSO when he merge his accounts
|
||||||
# - if it is not the same user, this will prevent the raise of PG::UniqueViolation
|
# - if it is not the same user, this will prevent the raise of PG::UniqueViolation
|
||||||
if active_provider.sso_fields.include?('user.email') and email_exists?(@user.email)
|
if active_provider.sso_fields.include?('user.email') && email_exists?(@user.email)
|
||||||
old_mail = @user.email
|
old_mail = @user.email
|
||||||
@user.email = "<#{old_mail}>#{Devise.friendly_token}-duplicate"
|
@user.email = "<#{old_mail}>#{Devise.friendly_token}-duplicate"
|
||||||
flash[:alert] = t('omniauth.email_already_linked_to_another_account_please_input_your_authentication_code', OLD_MAIL: old_mail)
|
flash[:alert] = t('omniauth.email_already_linked_to_another_account_please_input_your_authentication_code', OLD_MAIL: old_mail)
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Handle most of the emails sent by the platform. Triggered by notifications
|
||||||
class NotificationsMailer < NotifyWith::NotificationsMailer
|
class NotificationsMailer < NotifyWith::NotificationsMailer
|
||||||
default :from => ENV['DEFAULT_MAIL_FROM']
|
default from: ENV['DEFAULT_MAIL_FROM']
|
||||||
layout 'notifications_mailer'
|
layout 'notifications_mailer'
|
||||||
|
|
||||||
helper :application
|
helper :application
|
||||||
@ -9,15 +12,15 @@ class NotificationsMailer < NotifyWith::NotificationsMailer
|
|||||||
@recipient = notification.receiver
|
@recipient = notification.receiver
|
||||||
@attached_object = notification.attached_object
|
@attached_object = notification.attached_object
|
||||||
|
|
||||||
if !respond_to?(notification.notification_type)
|
unless respond_to?(notification.notification_type)
|
||||||
class_eval %Q{
|
class_eval %{
|
||||||
def #{notification.notification_type}
|
def #{notification.notification_type}
|
||||||
mail to: @recipient.email,
|
mail to: @recipient.email,
|
||||||
subject: t('notifications_mailer.#{notification.notification_type}.subject'),
|
subject: t('notifications_mailer.#{notification.notification_type}.subject'),
|
||||||
template_name: '#{notification.notification_type}',
|
template_name: '#{notification.notification_type}',
|
||||||
content_type: 'text/html'
|
content_type: 'text/html'
|
||||||
end
|
end
|
||||||
}
|
}, __FILE__, __LINE__ - 7
|
||||||
end
|
end
|
||||||
|
|
||||||
send(notification.notification_type)
|
send(notification.notification_type)
|
||||||
@ -29,11 +32,15 @@ class NotificationsMailer < NotifyWith::NotificationsMailer
|
|||||||
|
|
||||||
def notify_user_when_invoice_ready
|
def notify_user_when_invoice_ready
|
||||||
attachments[@attached_object.filename] = File.read(@attached_object.file)
|
attachments[@attached_object.filename] = File.read(@attached_object.file)
|
||||||
mail(to: @recipient.email, subject: t('notifications_mailer.notify_member_invoice_ready.subject'), template_name: 'notify_member_invoice_ready')
|
mail(to: @recipient.email,
|
||||||
|
subject: t('notifications_mailer.notify_member_invoice_ready.subject'),
|
||||||
|
template_name: 'notify_member_invoice_ready')
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_user_when_avoir_ready
|
def notify_user_when_avoir_ready
|
||||||
attachments[@attached_object.filename] = File.read(@attached_object.file)
|
attachments[@attached_object.filename] = File.read(@attached_object.file)
|
||||||
mail(to: @recipient.email, subject: t('notifications_mailer.notify_member_avoir_ready.subject'), template_name: 'notify_member_avoir_ready')
|
mail(to: @recipient.email,
|
||||||
|
subject: t('notifications_mailer.notify_member_avoir_ready.subject'),
|
||||||
|
template_name: 'notify_member_avoir_ready')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Handle emails related to users accounts, at Devise level
|
||||||
class UsersMailer < BaseMailer
|
class UsersMailer < BaseMailer
|
||||||
def notify_user_account_created(user, generated_password)
|
def notify_user_account_created(user, generated_password)
|
||||||
@user = user
|
@user = user
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'file_size_validator'
|
require 'file_size_validator'
|
||||||
|
|
||||||
|
# Generic class, parent of uploadable items
|
||||||
class Asset < ActiveRecord::Base
|
class Asset < ActiveRecord::Base
|
||||||
belongs_to :viewable, polymorphic: true
|
belongs_to :viewable, polymorphic: true
|
||||||
end
|
end
|
||||||
|
@ -5,50 +5,12 @@
|
|||||||
class Avoir < Invoice
|
class Avoir < Invoice
|
||||||
belongs_to :invoice
|
belongs_to :invoice
|
||||||
|
|
||||||
validates :avoir_mode, inclusion: { in: %w[stripe cheque transfer none cash wallet] }
|
validates :payment_method, inclusion: { in: %w[stripe cheque transfer none cash wallet] }
|
||||||
|
|
||||||
attr_accessor :invoice_items_ids
|
attr_accessor :invoice_items_ids
|
||||||
|
|
||||||
def generate_reference
|
def generate_reference
|
||||||
pattern = Setting.find_by(name: 'invoice_reference').value
|
self.reference = InvoiceReferenceService.generate_reference(self, date: created_at, avoir: true)
|
||||||
|
|
||||||
# invoice number per day (dd..dd)
|
|
||||||
reference = pattern.gsub(/d+(?![^\[]*\])/) do |match|
|
|
||||||
pad_and_truncate(number_of_invoices('day'), match.to_s.length)
|
|
||||||
end
|
|
||||||
# invoice number per month (mm..mm)
|
|
||||||
reference.gsub!(/m+(?![^\[]*\])/) do |match|
|
|
||||||
pad_and_truncate(number_of_invoices('month'), match.to_s.length)
|
|
||||||
end
|
|
||||||
# invoice number per year (yy..yy)
|
|
||||||
reference.gsub!(/y+(?![^\[]*\])/) do |match|
|
|
||||||
pad_and_truncate(number_of_invoices('year'), match.to_s.length)
|
|
||||||
end
|
|
||||||
|
|
||||||
# full year (YYYY)
|
|
||||||
reference.gsub!(/YYYY(?![^\[]*\])/, created_at.strftime('%Y'))
|
|
||||||
# year without century (YY)
|
|
||||||
reference.gsub!(/YY(?![^\[]*\])/, created_at.strftime('%y'))
|
|
||||||
|
|
||||||
# abbreviated month name (MMM)
|
|
||||||
reference.gsub!(/MMM(?![^\[]*\])/, created_at.strftime('%^b'))
|
|
||||||
# month of the year, zero-padded (MM)
|
|
||||||
reference.gsub!(/MM(?![^\[]*\])/, created_at.strftime('%m'))
|
|
||||||
# month of the year, non zero-padded (M)
|
|
||||||
reference.gsub!(/M(?![^\[]*\])/, created_at.strftime('%-m'))
|
|
||||||
|
|
||||||
# day of the month, zero-padded (DD)
|
|
||||||
reference.gsub!(/DD(?![^\[]*\])/, created_at.strftime('%d'))
|
|
||||||
# day of the month, non zero-padded (DD)
|
|
||||||
reference.gsub!(/DD(?![^\[]*\])/, created_at.strftime('%-d'))
|
|
||||||
|
|
||||||
# information about refund/avoir (R[text])
|
|
||||||
reference.gsub!(/R\[([^\]]+)\]/, '\1')
|
|
||||||
|
|
||||||
# remove information about online selling (X[text])
|
|
||||||
reference.gsub!(/X\[([^\]]+)\]/, ''.to_s)
|
|
||||||
|
|
||||||
self.reference = reference
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def expire_subscription
|
def expire_subscription
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Validates uploaded images to check that it matches the env parameters
|
||||||
|
# You must `include ImageValidatorConcern` in your class to use it
|
||||||
module ImageValidatorConcern
|
module ImageValidatorConcern
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ class Export < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def filename
|
def filename
|
||||||
"#{export_type}-#{id}_#{created_at.strftime('%d%m%Y')}.xlsx"
|
"#{export_type}-#{id}_#{created_at.strftime('%d%m%Y')}.#{extension}"
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@ -34,6 +34,8 @@ class Export < ActiveRecord::Base
|
|||||||
UsersExportWorker.perform_async(id)
|
UsersExportWorker.perform_async(id)
|
||||||
when 'availabilities'
|
when 'availabilities'
|
||||||
AvailabilitiesExportWorker.perform_async(id)
|
AvailabilitiesExportWorker.perform_async(id)
|
||||||
|
when 'accounting'
|
||||||
|
AccountingExportWorker.perform_async(id)
|
||||||
else
|
else
|
||||||
raise NoMethodError, "Unknown export service for #{category}/#{export_type}"
|
raise NoMethodError, "Unknown export service for #{category}/#{export_type}"
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Group is way to bind users with prices. Different prices can be defined for each plan/reservable, for each group
|
||||||
class Group < ActiveRecord::Base
|
class Group < ActiveRecord::Base
|
||||||
has_many :plans
|
has_many :plans
|
||||||
has_many :users
|
has_many :users
|
||||||
@ -12,15 +15,21 @@ class Group < ActiveRecord::Base
|
|||||||
friendly_id :name, use: :slugged
|
friendly_id :name, use: :slugged
|
||||||
|
|
||||||
validates :name, :slug, presence: true
|
validates :name, :slug, presence: true
|
||||||
|
validates :disabled, inclusion: { in: [false] }, if: :group_has_users?
|
||||||
|
|
||||||
after_create :create_prices
|
after_create :create_prices
|
||||||
after_create :create_statistic_subtype
|
after_create :create_statistic_subtype
|
||||||
after_update :update_statistic_subtype, if: :name_changed?
|
after_update :update_statistic_subtype, if: :name_changed?
|
||||||
|
after_update :disable_plans, if: :disabled_changed?
|
||||||
|
|
||||||
def destroyable?
|
def destroyable?
|
||||||
users.empty? and plans.empty?
|
users.empty? and plans.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def group_has_users?
|
||||||
|
users.count.positive?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def create_prices
|
def create_prices
|
||||||
@ -60,4 +69,10 @@ class Group < ActiveRecord::Base
|
|||||||
subtype.label = name
|
subtype.label = name
|
||||||
subtype.save!
|
subtype.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def disable_plans
|
||||||
|
plans.each do |plan|
|
||||||
|
plan.update_attributes(disabled: disabled)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -25,14 +25,6 @@ class HistoryValue < ActiveRecord::Base
|
|||||||
private
|
private
|
||||||
|
|
||||||
def compute_footprint
|
def compute_footprint
|
||||||
max_date = created_at || Time.current
|
FootprintService.compute_footprint(HistoryValue, self, 'created_at')
|
||||||
previous = HistoryValue.where('created_at < ?', max_date)
|
|
||||||
.order('created_at DESC')
|
|
||||||
.limit(1)
|
|
||||||
|
|
||||||
columns = HistoryValue.columns.map(&:name)
|
|
||||||
.delete_if { |c| %w[footprint updated_at].include? c }
|
|
||||||
|
|
||||||
Checksum.text("#{columns.map { |c| self[c] }.join}#{previous.first ? previous.first.footprint : ''}")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
31
app/models/import.rb
Normal file
31
app/models/import.rb
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'file_size_validator'
|
||||||
|
|
||||||
|
# An Import is a file uploaded by an user that provides some data to the database.
|
||||||
|
# Currently, this is used to import some users from a CSV file
|
||||||
|
class Import < ActiveRecord::Base
|
||||||
|
mount_uploader :attachment, ImportUploader
|
||||||
|
|
||||||
|
belongs_to :user
|
||||||
|
|
||||||
|
validates :attachment, file_size: { maximum: Rails.application.secrets.max_import_size&.to_i || 5.megabytes.to_i }
|
||||||
|
validates :attachment, file_mime_type: { content_type: %w[text/csv text/comma-separated-values application/vnd.ms-excel] }
|
||||||
|
|
||||||
|
after_commit :proceed_import, on: [:create]
|
||||||
|
|
||||||
|
def results_hash
|
||||||
|
YAML.safe_load(results, [Symbol]) if results
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def proceed_import
|
||||||
|
case category
|
||||||
|
when 'members'
|
||||||
|
MembersImportWorker.perform_async(id)
|
||||||
|
else
|
||||||
|
raise NoMethodError, "Unknown import service for #{category}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -48,52 +48,7 @@ class Invoice < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def generate_reference
|
def generate_reference
|
||||||
pattern = Setting.find_by(name: 'invoice_reference').value
|
self.reference = InvoiceReferenceService.generate_reference(self)
|
||||||
|
|
||||||
# invoice number per day (dd..dd)
|
|
||||||
reference = pattern.gsub(/d+(?![^\[]*\])/) do |match|
|
|
||||||
pad_and_truncate(number_of_invoices('day'), match.to_s.length)
|
|
||||||
end
|
|
||||||
# invoice number per month (mm..mm)
|
|
||||||
reference.gsub!(/m+(?![^\[]*\])/) do |match|
|
|
||||||
pad_and_truncate(number_of_invoices('month'), match.to_s.length)
|
|
||||||
end
|
|
||||||
# invoice number per year (yy..yy)
|
|
||||||
reference.gsub!(/y+(?![^\[]*\])/) do |match|
|
|
||||||
pad_and_truncate(number_of_invoices('year'), match.to_s.length)
|
|
||||||
end
|
|
||||||
|
|
||||||
# full year (YYYY)
|
|
||||||
reference.gsub!(/YYYY(?![^\[]*\])/, Time.now.strftime('%Y'))
|
|
||||||
# year without century (YY)
|
|
||||||
reference.gsub!(/YY(?![^\[]*\])/, Time.now.strftime('%y'))
|
|
||||||
|
|
||||||
# abreviated month name (MMM)
|
|
||||||
reference.gsub!(/MMM(?![^\[]*\])/, Time.now.strftime('%^b'))
|
|
||||||
# month of the year, zero-padded (MM)
|
|
||||||
reference.gsub!(/MM(?![^\[]*\])/, Time.now.strftime('%m'))
|
|
||||||
# month of the year, non zero-padded (M)
|
|
||||||
reference.gsub!(/M(?![^\[]*\])/, Time.now.strftime('%-m'))
|
|
||||||
|
|
||||||
# day of the month, zero-padded (DD)
|
|
||||||
reference.gsub!(/DD(?![^\[]*\])/, Time.now.strftime('%d'))
|
|
||||||
# day of the month, non zero-padded (DD)
|
|
||||||
reference.gsub!(/DD(?![^\[]*\])/, Time.now.strftime('%-d'))
|
|
||||||
|
|
||||||
# information about online selling (X[text])
|
|
||||||
if paid_with_stripe?
|
|
||||||
reference.gsub!(/X\[([^\]]+)\]/, '\1')
|
|
||||||
else
|
|
||||||
reference.gsub!(/X\[([^\]]+)\]/, ''.to_s)
|
|
||||||
end
|
|
||||||
|
|
||||||
# information about wallet (W[text])
|
|
||||||
# reference.gsub!(/W\[([^\]]+)\]/, ''.to_s)
|
|
||||||
|
|
||||||
# remove information about refunds (R[text])
|
|
||||||
reference.gsub!(/R\[([^\]]+)\]/, ''.to_s)
|
|
||||||
|
|
||||||
self.reference = reference
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_reference
|
def update_reference
|
||||||
@ -102,43 +57,7 @@ class Invoice < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def order_number
|
def order_number
|
||||||
pattern = Setting.find_by(name: 'invoice_order-nb').value
|
InvoiceReferenceService.generate_order_number(self)
|
||||||
|
|
||||||
# global invoice number (nn..nn)
|
|
||||||
reference = pattern.gsub(/n+(?![^\[]*\])/) do |match|
|
|
||||||
pad_and_truncate(number_of_invoices('global'), match.to_s.length)
|
|
||||||
end
|
|
||||||
# invoice number per year (yy..yy)
|
|
||||||
reference.gsub!(/y+(?![^\[]*\])/) do |match|
|
|
||||||
pad_and_truncate(number_of_invoices('year'), match.to_s.length)
|
|
||||||
end
|
|
||||||
# invoice number per month (mm..mm)
|
|
||||||
reference.gsub!(/m+(?![^\[]*\])/) do |match|
|
|
||||||
pad_and_truncate(number_of_invoices('month'), match.to_s.length)
|
|
||||||
end
|
|
||||||
# invoice number per day (dd..dd)
|
|
||||||
reference.gsub!(/d+(?![^\[]*\])/) do |match|
|
|
||||||
pad_and_truncate(number_of_invoices('day'), match.to_s.length)
|
|
||||||
end
|
|
||||||
|
|
||||||
# full year (YYYY)
|
|
||||||
reference.gsub!(/YYYY(?![^\[]*\])/, created_at.strftime('%Y'))
|
|
||||||
# year without century (YY)
|
|
||||||
reference.gsub!(/YY(?![^\[]*\])/, created_at.strftime('%y'))
|
|
||||||
|
|
||||||
# abbreviated month name (MMM)
|
|
||||||
reference.gsub!(/MMM(?![^\[]*\])/, created_at.strftime('%^b'))
|
|
||||||
# month of the year, zero-padded (MM)
|
|
||||||
reference.gsub!(/MM(?![^\[]*\])/, created_at.strftime('%m'))
|
|
||||||
# month of the year, non zero-padded (M)
|
|
||||||
reference.gsub!(/M(?![^\[]*\])/, created_at.strftime('%-m'))
|
|
||||||
|
|
||||||
# day of the month, zero-padded (DD)
|
|
||||||
reference.gsub!(/DD(?![^\[]*\])/, created_at.strftime('%d'))
|
|
||||||
# day of the month, non zero-padded (DD)
|
|
||||||
reference.gsub!(/DD(?![^\[]*\])/, created_at.strftime('%-d'))
|
|
||||||
|
|
||||||
reference
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# for debug & used by rake task "fablab:maintenance:regenerate_invoices"
|
# for debug & used by rake task "fablab:maintenance:regenerate_invoices"
|
||||||
@ -148,7 +67,7 @@ class Invoice < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_avoir(attrs = {})
|
def build_avoir(attrs = {})
|
||||||
raise Exception if refunded? === true || prevent_refund?
|
raise Exception if refunded? == true || prevent_refund?
|
||||||
|
|
||||||
avoir = Avoir.new(dup.attributes)
|
avoir = Avoir.new(dup.attributes)
|
||||||
avoir.type = 'Avoir'
|
avoir.type = 'Avoir'
|
||||||
@ -189,7 +108,7 @@ class Invoice < ActiveRecord::Base
|
|||||||
|
|
||||||
def subscription_invoice?
|
def subscription_invoice?
|
||||||
invoice_items.each do |ii|
|
invoice_items.each do |ii|
|
||||||
return true if ii.subscription && !ii.subscription.expired?
|
return true if ii.subscription
|
||||||
end
|
end
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
@ -230,6 +149,18 @@ class Invoice < ActiveRecord::Base
|
|||||||
total - (wallet_amount || 0)
|
total - (wallet_amount || 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# return a summary of the payment means used
|
||||||
|
def payment_means
|
||||||
|
res = []
|
||||||
|
res.push(means: :wallet, amount: wallet_amount) if wallet_transaction && wallet_amount.positive?
|
||||||
|
if paid_with_stripe?
|
||||||
|
res.push(means: :card, amount: amount_paid)
|
||||||
|
else
|
||||||
|
res.push(means: :other, amount: amount_paid)
|
||||||
|
end
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
def add_environment
|
def add_environment
|
||||||
self.environment = Rails.env
|
self.environment = Rails.env
|
||||||
end
|
end
|
||||||
@ -244,21 +175,21 @@ class Invoice < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_wallet_transaction(amount, transaction_id)
|
def set_wallet_transaction(amount, transaction_id)
|
||||||
if check_footprint
|
raise InvalidFootprintError unless check_footprint
|
||||||
|
|
||||||
update_columns(wallet_amount: amount, wallet_transaction_id: transaction_id)
|
update_columns(wallet_amount: amount, wallet_transaction_id: transaction_id)
|
||||||
chain_record
|
chain_record
|
||||||
else
|
|
||||||
raise InvalidFootprintError
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def paid_with_stripe?
|
def paid_with_stripe?
|
||||||
stp_payment_intent_id? || stp_invoice_id?
|
stp_payment_intent_id? || stp_invoice_id? || payment_method == 'stripe'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def generate_and_send_invoice
|
def generate_and_send_invoice
|
||||||
|
return if Rails.application.secrets.fablab_without_invoices == 'true'
|
||||||
|
|
||||||
unless Rails.env.test?
|
unless Rails.env.test?
|
||||||
puts "Creating an InvoiceWorker job to generate the following invoice: id(#{id}), invoiced_id(#{invoiced_id}), " \
|
puts "Creating an InvoiceWorker job to generate the following invoice: id(#{id}), invoiced_id(#{invoiced_id}), " \
|
||||||
"invoiced_type(#{invoiced_type}), user_id(#{invoicing_profile.user_id})"
|
"invoiced_type(#{invoiced_type}), user_id(#{invoicing_profile.user_id})"
|
||||||
@ -266,50 +197,8 @@ class Invoice < ActiveRecord::Base
|
|||||||
InvoiceWorker.perform_async(id, user&.subscription&.expired_at)
|
InvoiceWorker.perform_async(id, user&.subscription&.expired_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
|
||||||
# Output the given integer with leading zeros. If the given value is longer than the given
|
|
||||||
# length, it will be truncated.
|
|
||||||
# @param value {Integer} the integer to pad
|
|
||||||
# @param length {Integer} the length of the resulting string.
|
|
||||||
##
|
|
||||||
def pad_and_truncate(value, length)
|
|
||||||
value.to_s.rjust(length, '0').gsub(/^.*(.{#{length},}?)$/m, '\1')
|
|
||||||
end
|
|
||||||
|
|
||||||
##
|
|
||||||
# Returns the number of current invoices in the given range around the current date.
|
|
||||||
# If range is invalid or not specified, the total number of invoices is returned.
|
|
||||||
# @param range {String} 'day', 'month', 'year'
|
|
||||||
# @return {Integer}
|
|
||||||
##
|
|
||||||
def number_of_invoices(range)
|
|
||||||
case range.to_s
|
|
||||||
when 'day'
|
|
||||||
start = DateTime.current.beginning_of_day
|
|
||||||
ending = DateTime.current.end_of_day
|
|
||||||
when 'month'
|
|
||||||
start = DateTime.current.beginning_of_month
|
|
||||||
ending = DateTime.current.end_of_month
|
|
||||||
when 'year'
|
|
||||||
start = DateTime.current.beginning_of_year
|
|
||||||
ending = DateTime.current.end_of_year
|
|
||||||
else
|
|
||||||
return id
|
|
||||||
end
|
|
||||||
return Invoice.count unless defined? start && defined? ending
|
|
||||||
|
|
||||||
Invoice.where('created_at >= :start_date AND created_at < :end_date', start_date: start, end_date: ending).length
|
|
||||||
end
|
|
||||||
|
|
||||||
def compute_footprint
|
def compute_footprint
|
||||||
previous = Invoice.where('id < ?', id)
|
FootprintService.compute_footprint(Invoice, self)
|
||||||
.order('id 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
|
||||||
|
|
||||||
def log_changes
|
def log_changes
|
||||||
|
@ -21,17 +21,29 @@ class InvoiceItem < ActiveRecord::Base
|
|||||||
footprint == compute_footprint
|
footprint == compute_footprint
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def amount_after_coupon
|
||||||
|
# deduct coupon discount
|
||||||
|
coupon_service = CouponService.new
|
||||||
|
coupon_service.ventilate(invoice.total, amount, invoice.coupon)
|
||||||
|
end
|
||||||
|
|
||||||
|
# return the item amount, coupon discount deducted, if any, and VAT excluded, if applicable
|
||||||
|
def net_amount
|
||||||
|
# deduct VAT
|
||||||
|
vat_service = VatHistoryService.new
|
||||||
|
vat_rate = vat_service.invoice_vat(invoice)
|
||||||
|
Rational(amount_after_coupon / (vat_rate / 100.00 + 1)).round.to_f
|
||||||
|
end
|
||||||
|
|
||||||
|
# return the VAT amount for this item
|
||||||
|
def vat
|
||||||
|
amount_after_coupon - net_amount
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def compute_footprint
|
def compute_footprint
|
||||||
previous = InvoiceItem.where('id < ?', id)
|
FootprintService.compute_footprint(InvoiceItem, self)
|
||||||
.order('id 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
|
||||||
|
|
||||||
def log_changes
|
def log_changes
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Machine is an hardware equipment hosted in the fablab that is available for reservation to the members
|
||||||
class Machine < ActiveRecord::Base
|
class Machine < ActiveRecord::Base
|
||||||
extend FriendlyId
|
extend FriendlyId
|
||||||
friendly_id :name, use: :slugged
|
friendly_id :name, use: :slugged
|
||||||
@ -7,12 +10,12 @@ class Machine < ActiveRecord::Base
|
|||||||
has_many :machine_files, as: :viewable, dependent: :destroy
|
has_many :machine_files, as: :viewable, dependent: :destroy
|
||||||
accepts_nested_attributes_for :machine_files, allow_destroy: true, reject_if: :all_blank
|
accepts_nested_attributes_for :machine_files, allow_destroy: true, reject_if: :all_blank
|
||||||
|
|
||||||
has_and_belongs_to_many :projects, join_table: :projects_machines
|
has_and_belongs_to_many :projects, join_table: 'projects_machines'
|
||||||
|
|
||||||
has_many :machines_availabilities, dependent: :destroy
|
has_many :machines_availabilities, dependent: :destroy
|
||||||
has_many :availabilities, through: :machines_availabilities
|
has_many :availabilities, through: :machines_availabilities
|
||||||
|
|
||||||
has_and_belongs_to_many :trainings, join_table: :trainings_machines
|
has_and_belongs_to_many :trainings, join_table: 'trainings_machines'
|
||||||
|
|
||||||
validates :name, presence: true, length: { maximum: 50 }
|
validates :name, presence: true, length: { maximum: 50 }
|
||||||
validates :description, presence: true
|
validates :description, presence: true
|
||||||
@ -39,18 +42,20 @@ class Machine < ActiveRecord::Base
|
|||||||
|
|
||||||
def create_statistic_subtype
|
def create_statistic_subtype
|
||||||
index = StatisticIndex.where(es_type_key: 'machine')
|
index = StatisticIndex.where(es_type_key: 'machine')
|
||||||
StatisticSubType.create!({statistic_types: index.first.statistic_types, key: self.slug, label: self.name})
|
StatisticSubType.create!(statistic_types: index.first.statistic_types, key: slug, label: name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_statistic_subtype
|
def update_statistic_subtype
|
||||||
index = StatisticIndex.where(es_type_key: 'machine')
|
index = StatisticIndex.where(es_type_key: 'machine')
|
||||||
subtype = StatisticSubType.joins(statistic_type_sub_types: :statistic_type).where(key: self.slug, statistic_types: { statistic_index_id: index.first.id }).first
|
subtype = StatisticSubType.joins(statistic_type_sub_types: :statistic_type)
|
||||||
subtype.label = self.name
|
.where(key: slug, statistic_types: { statistic_index_id: index.first.id })
|
||||||
|
.first
|
||||||
|
subtype.label = name
|
||||||
subtype.save!
|
subtype.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_statistic_subtype
|
def remove_statistic_subtype
|
||||||
subtype = StatisticSubType.where(key: self.slug).first
|
subtype = StatisticSubType.where(key: slug).first
|
||||||
subtype.destroy!
|
subtype.destroy!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -46,6 +46,7 @@ class NotificationType
|
|||||||
notify_admin_close_period_reminder
|
notify_admin_close_period_reminder
|
||||||
notify_admin_archive_complete
|
notify_admin_archive_complete
|
||||||
notify_privacy_policy_changed
|
notify_privacy_policy_changed
|
||||||
|
notify_admin_import_complete
|
||||||
]
|
]
|
||||||
# deprecated:
|
# deprecated:
|
||||||
# - notify_member_subscribed_plan_is_changed
|
# - notify_member_subscribed_plan_is_changed
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Plan is a generic description of a subscription plan, which can be subscribed by a member to benefit from advantageous prices.
|
||||||
|
# Subscribers can also get some Credits for some reservable items
|
||||||
class Plan < ActiveRecord::Base
|
class Plan < ActiveRecord::Base
|
||||||
belongs_to :group
|
belongs_to :group
|
||||||
|
|
||||||
@ -19,6 +23,7 @@ class Plan < ActiveRecord::Base
|
|||||||
after_create :create_machines_prices
|
after_create :create_machines_prices
|
||||||
after_create :create_spaces_prices
|
after_create :create_spaces_prices
|
||||||
after_create :create_statistic_type
|
after_create :create_statistic_type
|
||||||
|
after_create :set_name
|
||||||
|
|
||||||
|
|
||||||
validates :amount, :group, :base_name, presence: true
|
validates :amount, :group, :base_name, presence: true
|
||||||
@ -53,13 +58,13 @@ class Plan < ActiveRecord::Base
|
|||||||
|
|
||||||
def create_machines_prices
|
def create_machines_prices
|
||||||
Machine.all.each do |machine|
|
Machine.all.each do |machine|
|
||||||
Price.create(priceable: machine, plan: self, group_id: self.group_id, amount: 0)
|
Price.create(priceable: machine, plan: self, group_id: group_id, amount: 0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_spaces_prices
|
def create_spaces_prices
|
||||||
Space.all.each do |space|
|
Space.all.each do |space|
|
||||||
Price.create(priceable: space, plan: self, group_id: self.group_id, amount: 0)
|
Price.create(priceable: space, plan: self, group_id: group_id, amount: 0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -105,8 +110,12 @@ class Plan < ActiveRecord::Base
|
|||||||
if !stat_type.nil? && !stat_subtype.nil?
|
if !stat_type.nil? && !stat_subtype.nil?
|
||||||
StatisticTypeSubType.create!(statistic_type: stat_type, statistic_sub_type: stat_subtype)
|
StatisticTypeSubType.create!(statistic_type: stat_type, statistic_sub_type: stat_subtype)
|
||||||
else
|
else
|
||||||
puts 'ERROR: Unable to create the statistics association for the new plan. '+
|
puts 'ERROR: Unable to create the statistics association for the new plan. ' \
|
||||||
'Possible causes: the type or the subtype were not created successfully.'
|
'Possible causes: the type or the subtype were not created successfully.'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_name
|
||||||
|
update_columns(name: human_readable_name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# CAO file attached to a project documentation
|
||||||
class ProjectCao < Asset
|
class ProjectCao < Asset
|
||||||
mount_uploader :attachment, ProjectCaoUploader
|
mount_uploader :attachment, ProjectCaoUploader
|
||||||
|
|
||||||
validates :attachment, file_size: { maximum: 20.megabytes.to_i }
|
validates :attachment, file_size: { maximum: Rails.application.secrets.max_cao_size&.to_i || 5.megabytes.to_i }
|
||||||
validates :attachment, :file_mime_type => { :content_type => ENV['ALLOWED_MIME_TYPES'].split(' ') }
|
validates :attachment, file_mime_type: { content_type: ENV['ALLOWED_MIME_TYPES'].split(' ') }
|
||||||
end
|
end
|
||||||
|
@ -46,7 +46,6 @@ class Reservation < ActiveRecord::Base
|
|||||||
raise LockedError if slot.availability.lock
|
raise LockedError if slot.availability.lock
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
case reservable
|
case reservable
|
||||||
|
|
||||||
# === Machine reservation ===
|
# === Machine reservation ===
|
||||||
@ -72,7 +71,6 @@ class Reservation < ActiveRecord::Base
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# === Training reservation ===
|
# === Training reservation ===
|
||||||
when Training
|
when Training
|
||||||
base_amount = reservable.amount_by_group(user.group_id).amount
|
base_amount = reservable.amount_by_group(user.group_id).amount
|
||||||
@ -99,10 +97,16 @@ class Reservation < ActiveRecord::Base
|
|||||||
amount += ticket.booked * ticket.event_price_category.amount
|
amount += ticket.booked * ticket.event_price_category.amount
|
||||||
end
|
end
|
||||||
slots.each do |slot|
|
slots.each do |slot|
|
||||||
description = "#{reservable.name} "
|
description = "#{reservable.name}\n"
|
||||||
(slot.start_at.to_date..slot.end_at.to_date).each do |d|
|
description += if slot.start_at.to_date != slot.end_at.to_date
|
||||||
description += "\n" if slot.start_at.to_date != slot.end_at.to_date
|
I18n.t('events.from_STARTDATE_to_ENDDATE',
|
||||||
description += "#{I18n.l d, format: :long} #{I18n.l slot.start_at, format: :hour_minute}" \
|
STARTDATE: I18n.l(slot.start_at.to_date, format: :long),
|
||||||
|
ENDDATE: I18n.l(slot.end_at.to_date, format: :long)) + ' ' +
|
||||||
|
I18n.t('events.from_STARTTIME_to_ENDTIME',
|
||||||
|
STARTTIME: I18n.l(slot.start_at, format: :hour_minute),
|
||||||
|
ENDTIME: I18n.l(slot.end_at, format: :hour_minute))
|
||||||
|
else
|
||||||
|
"#{I18n.l slot.start_at.to_date, format: :long} #{I18n.l slot.start_at, format: :hour_minute}" \
|
||||||
" - #{I18n.l slot.end_at, format: :hour_minute}"
|
" - #{I18n.l slot.end_at, format: :hour_minute}"
|
||||||
end
|
end
|
||||||
ii_amount = amount
|
ii_amount = amount
|
||||||
@ -202,11 +206,14 @@ class Reservation < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def save_with_payment(operator_profile_id, coupon_code = nil, payment_intent_id = nil)
|
def save_with_payment(operator_profile_id, coupon_code = nil, payment_intent_id = nil)
|
||||||
|
method = InvoicingProfile.find(operator_profile_id)&.user&.admin? ? nil : 'stripe'
|
||||||
|
|
||||||
build_invoice(
|
build_invoice(
|
||||||
invoicing_profile: user.invoicing_profile,
|
invoicing_profile: user.invoicing_profile,
|
||||||
statistic_profile: user.statistic_profile,
|
statistic_profile: user.statistic_profile,
|
||||||
operator_profile_id: operator_profile_id,
|
operator_profile_id: operator_profile_id,
|
||||||
stp_payment_intent_id: payment_intent_id
|
stp_payment_intent_id: payment_intent_id,
|
||||||
|
payment_method: method
|
||||||
)
|
)
|
||||||
generate_invoice_items(true, coupon_code)
|
generate_invoice_items(true, coupon_code)
|
||||||
|
|
||||||
|
@ -40,7 +40,28 @@ class Setting < ActiveRecord::Base
|
|||||||
visibility_yearly
|
visibility_yearly
|
||||||
visibility_others
|
visibility_others
|
||||||
display_name_enable
|
display_name_enable
|
||||||
machines_sort_by] }
|
machines_sort_by
|
||||||
|
accounting_journal_code
|
||||||
|
accounting_card_client_code
|
||||||
|
accounting_card_client_label
|
||||||
|
accounting_wallet_client_code
|
||||||
|
accounting_wallet_client_label
|
||||||
|
accounting_other_client_code
|
||||||
|
accounting_other_client_label
|
||||||
|
accounting_wallet_code
|
||||||
|
accounting_wallet_label
|
||||||
|
accounting_VAT_code
|
||||||
|
accounting_VAT_label
|
||||||
|
accounting_subscription_code
|
||||||
|
accounting_subscription_label
|
||||||
|
accounting_Machine_code
|
||||||
|
accounting_Machine_label
|
||||||
|
accounting_Training_code
|
||||||
|
accounting_Training_label
|
||||||
|
accounting_Event_code
|
||||||
|
accounting_Event_label
|
||||||
|
accounting_Space_code
|
||||||
|
accounting_Space_label] }
|
||||||
|
|
||||||
after_update :update_stylesheet, :notify_privacy_policy_changed if :value_changed?
|
after_update :update_stylesheet, :notify_privacy_policy_changed if :value_changed?
|
||||||
|
|
||||||
|
@ -36,5 +36,4 @@ class StatisticProfile < ActiveRecord::Base
|
|||||||
''
|
''
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -46,6 +46,7 @@ class Subscription < ActiveRecord::Base
|
|||||||
def generate_invoice(operator_profile_id, coupon_code = nil, payment_intent_id = nil)
|
def generate_invoice(operator_profile_id, coupon_code = nil, payment_intent_id = nil)
|
||||||
coupon_id = nil
|
coupon_id = nil
|
||||||
total = plan.amount
|
total = plan.amount
|
||||||
|
method = InvoicingProfile.find(operator_profile_id)&.user&.admin? ? nil : 'stripe'
|
||||||
|
|
||||||
unless coupon_code.nil?
|
unless coupon_code.nil?
|
||||||
@coupon = Coupon.find_by(code: coupon_code)
|
@coupon = Coupon.find_by(code: coupon_code)
|
||||||
@ -64,7 +65,8 @@ class Subscription < ActiveRecord::Base
|
|||||||
total: total,
|
total: total,
|
||||||
coupon_id: coupon_id,
|
coupon_id: coupon_id,
|
||||||
operator_profile_id: operator_profile_id,
|
operator_profile_id: operator_profile_id,
|
||||||
stp_payment_intent_id: payment_intent_id
|
stp_payment_intent_id: payment_intent_id,
|
||||||
|
payment_method: method
|
||||||
)
|
)
|
||||||
invoice.invoice_items.push InvoiceItem.new(
|
invoice.invoice_items.push InvoiceItem.new(
|
||||||
amount: plan.amount,
|
amount: plan.amount,
|
||||||
|
@ -43,6 +43,7 @@ class User < ActiveRecord::Base
|
|||||||
accepts_nested_attributes_for :tags, allow_destroy: true
|
accepts_nested_attributes_for :tags, allow_destroy: true
|
||||||
|
|
||||||
has_many :exports, dependent: :destroy
|
has_many :exports, dependent: :destroy
|
||||||
|
has_many :imports, dependent: :nullify
|
||||||
|
|
||||||
# fix for create admin user
|
# fix for create admin user
|
||||||
before_save do
|
before_save do
|
||||||
|
@ -121,6 +121,8 @@ class PDF::Invoice < Prawn::Document
|
|||||||
data = [[I18n.t('invoices.details'), I18n.t('invoices.amount')]]
|
data = [[I18n.t('invoices.details'), I18n.t('invoices.amount')]]
|
||||||
|
|
||||||
total_calc = 0
|
total_calc = 0
|
||||||
|
total_ht = 0
|
||||||
|
total_vat = 0
|
||||||
# going through invoice_items
|
# going through invoice_items
|
||||||
invoice.invoice_items.each do |item|
|
invoice.invoice_items.each do |item|
|
||||||
|
|
||||||
@ -184,6 +186,8 @@ class PDF::Invoice < Prawn::Document
|
|||||||
|
|
||||||
data += [[details, number_to_currency(price)]]
|
data += [[details, number_to_currency(price)]]
|
||||||
total_calc += price
|
total_calc += price
|
||||||
|
total_ht += item.net_amount
|
||||||
|
total_vat += item.vat
|
||||||
end
|
end
|
||||||
|
|
||||||
## subtract the coupon, if any
|
## subtract the coupon, if any
|
||||||
@ -210,9 +214,7 @@ class PDF::Invoice < Prawn::Document
|
|||||||
|
|
||||||
# discount textual description
|
# discount textual description
|
||||||
literal_discount = cp.percent_off
|
literal_discount = cp.percent_off
|
||||||
if cp.type == 'amount_off'
|
literal_discount = number_to_currency(cp.amount_off / 100.00) if cp.type == 'amount_off'
|
||||||
literal_discount = number_to_currency(cp.amount_off / 100.00)
|
|
||||||
end
|
|
||||||
|
|
||||||
# add a row for the coupon
|
# add a row for the coupon
|
||||||
data += [[_t('invoices.coupon_CODE_discount_of_DISCOUNT',
|
data += [[_t('invoices.coupon_CODE_discount_of_DISCOUNT',
|
||||||
@ -226,20 +228,18 @@ class PDF::Invoice < Prawn::Document
|
|||||||
puts "ERROR: totals are NOT equals => expected: #{total}, computed: #{total_calc}" if total_calc != total
|
puts "ERROR: totals are NOT equals => expected: #{total}, computed: #{total_calc}" if total_calc != total
|
||||||
|
|
||||||
# TVA
|
# TVA
|
||||||
if Setting.find_by(name: 'invoice_VAT-active').value == 'true'
|
|
||||||
data += [[I18n.t('invoices.total_including_all_taxes'), number_to_currency(total)]]
|
|
||||||
|
|
||||||
vat_service = VatHistoryService.new
|
vat_service = VatHistoryService.new
|
||||||
vat_rate = vat_service.invoice_vat(invoice)
|
vat_rate = vat_service.invoice_vat(invoice)
|
||||||
vat = total / (vat_rate / 100.00 + 1)
|
if vat_rate != 0
|
||||||
data += [[I18n.t('invoices.including_VAT_RATE', RATE: vat_rate), number_to_currency(total - vat)]]
|
data += [[I18n.t('invoices.total_including_all_taxes'), number_to_currency(total)]]
|
||||||
data += [[I18n.t('invoices.including_total_excluding_taxes'), number_to_currency(vat)]]
|
data += [[I18n.t('invoices.including_VAT_RATE', RATE: vat_rate), number_to_currency(total_vat / 100.00)]]
|
||||||
|
data += [[I18n.t('invoices.including_total_excluding_taxes'), number_to_currency(total_ht / 100.00)]]
|
||||||
data += [[I18n.t('invoices.including_amount_payed_on_ordering'), number_to_currency(total)]]
|
data += [[I18n.t('invoices.including_amount_payed_on_ordering'), number_to_currency(total)]]
|
||||||
|
|
||||||
# checking the round number
|
# checking the round number
|
||||||
rounded = sprintf('%.2f', vat).to_f + sprintf('%.2f', total - vat).to_f
|
rounded = sprintf('%.2f', total_vat / 100.00).to_f + sprintf('%.2f', total_ht / 100.00).to_f
|
||||||
if rounded != sprintf('%.2f', total_calc).to_f
|
if rounded != sprintf('%.2f', total_calc).to_f
|
||||||
puts 'ERROR: rounding the numbers cause an invoice inconsistency. ' +
|
puts 'ERROR: rounding the numbers cause an invoice inconsistency. ' \
|
||||||
"Total expected: #{sprintf('%.2f', total_calc)}, total computed: #{rounded}"
|
"Total expected: #{sprintf('%.2f', total_calc)}, total computed: #{rounded}"
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@ -279,7 +279,7 @@ class PDF::Invoice < Prawn::Document
|
|||||||
move_down 20
|
move_down 20
|
||||||
if invoice.is_a?(Avoir)
|
if invoice.is_a?(Avoir)
|
||||||
payment_verbose = I18n.t('invoices.refund_on_DATE', DATE:I18n.l(invoice.avoir_date.to_date)) + ' '
|
payment_verbose = I18n.t('invoices.refund_on_DATE', DATE:I18n.l(invoice.avoir_date.to_date)) + ' '
|
||||||
case invoice.avoir_mode
|
case invoice.payment_method
|
||||||
when 'stripe'
|
when 'stripe'
|
||||||
payment_verbose += I18n.t('invoices.by_stripe_online_payment')
|
payment_verbose += I18n.t('invoices.by_stripe_online_payment')
|
||||||
when 'cheque'
|
when 'cheque'
|
||||||
|
8
app/policies/accounting_export_policy.rb
Normal file
8
app/policies/accounting_export_policy.rb
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Check the access policies for API::AccountingExportsController
|
||||||
|
class AccountingExportPolicy < ApplicationPolicy
|
||||||
|
def export?
|
||||||
|
user.admin?
|
||||||
|
end
|
||||||
|
end
|
@ -1,5 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Check the access policies for API::CouponsController
|
||||||
class CouponPolicy < ApplicationPolicy
|
class CouponPolicy < ApplicationPolicy
|
||||||
%w(index show create update destroy send_to).each do |action|
|
%w[index show create update destroy send_to].each do |action|
|
||||||
define_method "#{action}?" do
|
define_method "#{action}?" do
|
||||||
user.admin?
|
user.admin?
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Check the access policies for API::EventsController
|
||||||
class EventPolicy < ApplicationPolicy
|
class EventPolicy < ApplicationPolicy
|
||||||
|
# Defines the scope of the events index, depending on the role of the current user
|
||||||
class Scope < Scope
|
class Scope < Scope
|
||||||
def resolve
|
def resolve
|
||||||
if user.nil? or (user and !user.admin?)
|
if user.nil? || (user && !user.admin?)
|
||||||
scope.includes(:event_image, :event_files, :availability, :category)
|
scope.includes(:event_image, :event_files, :availability, :category)
|
||||||
.where('availabilities.start_at >= ?', Time.now)
|
.where('availabilities.start_at >= ?', Time.now)
|
||||||
.order('availabilities.start_at ASC')
|
.order('availabilities.start_at ASC')
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
class ExportPolicy < Struct.new(:user, :export)
|
# frozen_string_literal: true
|
||||||
%w(export_reservations export_members export_subscriptions export_availabilities download status).each do |action|
|
|
||||||
|
# Check the access policies for API::ExportsController
|
||||||
|
class ExportPolicy < ApplicationPolicy
|
||||||
|
%w[export_reservations export_members export_subscriptions export_availabilities download status].each do |action|
|
||||||
define_method "#{action}?" do
|
define_method "#{action}?" do
|
||||||
user.admin?
|
user.admin?
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Check the access policies for API::GroupsController
|
||||||
class GroupPolicy < ApplicationPolicy
|
class GroupPolicy < ApplicationPolicy
|
||||||
def create?
|
def create?
|
||||||
user.admin?
|
user.admin?
|
||||||
|
12
app/policies/import_policy.rb
Normal file
12
app/policies/import_policy.rb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Check the access policies for API::ImportsController
|
||||||
|
class ImportPolicy < ApplicationPolicy
|
||||||
|
def show?
|
||||||
|
user.admin?
|
||||||
|
end
|
||||||
|
|
||||||
|
def members?
|
||||||
|
user.admin?
|
||||||
|
end
|
||||||
|
end
|
@ -14,4 +14,8 @@ class InvoicePolicy < ApplicationPolicy
|
|||||||
def list?
|
def list?
|
||||||
user.admin?
|
user.admin?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def first?
|
||||||
|
user.admin?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Check the access policies for API::SettingsController
|
||||||
class SettingPolicy < ApplicationPolicy
|
class SettingPolicy < ApplicationPolicy
|
||||||
%w(update).each do |action|
|
%w[update bulk_update].each do |action|
|
||||||
define_method "#{action}?" do
|
define_method "#{action}?" do
|
||||||
user.admin?
|
user.admin?
|
||||||
end
|
end
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Check the access policies for API::MembersController and API::UsersController
|
||||||
class UserPolicy < ApplicationPolicy
|
class UserPolicy < ApplicationPolicy
|
||||||
|
# Defines the scope of the users index, depending on the role of the current user
|
||||||
class Scope < Scope
|
class Scope < Scope
|
||||||
def resolve
|
def resolve
|
||||||
if user.admin?
|
if user.admin?
|
||||||
|
@ -2,22 +2,36 @@
|
|||||||
|
|
||||||
# Provides the routine to export the accounting data to an external accounting software
|
# Provides the routine to export the accounting data to an external accounting software
|
||||||
class AccountingExportService
|
class AccountingExportService
|
||||||
attr_reader :encoding, :format, :separator, :journal_code, :date_format, :columns, :vat_service
|
include ActionView::Helpers::NumberHelper
|
||||||
|
|
||||||
def initialize(columns, encoding = 'UTF-8', format = 'CSV', separator = ';', date_format = '%d/%m/%Y')
|
attr_reader :encoding, :format, :separator, :journal_code, :date_format, :columns, :decimal_separator, :label_max_length,
|
||||||
|
:export_zeros
|
||||||
|
|
||||||
|
def initialize(columns, encoding: 'UTF-8', format: 'CSV', separator: ';')
|
||||||
@encoding = encoding
|
@encoding = encoding
|
||||||
@format = format
|
@format = format
|
||||||
@separator = separator
|
@separator = separator
|
||||||
|
@decimal_separator = ','
|
||||||
|
@date_format = '%d/%m/%Y'
|
||||||
|
@label_max_length = 50
|
||||||
|
@export_zeros = false
|
||||||
@journal_code = Setting.find_by(name: 'accounting_journal_code')&.value || ''
|
@journal_code = Setting.find_by(name: 'accounting_journal_code')&.value || ''
|
||||||
@date_format = date_format
|
@date_format = date_format
|
||||||
@columns = columns
|
@columns = columns
|
||||||
@vat_service = VatHistoryService.new
|
end
|
||||||
|
|
||||||
|
def set_options(decimal_separator: ',', date_format: '%d/%m/%Y', label_max_length: 50, export_zeros: false)
|
||||||
|
@decimal_separator = decimal_separator
|
||||||
|
@date_format = date_format
|
||||||
|
@label_max_length = label_max_length
|
||||||
|
@export_zeros = export_zeros
|
||||||
end
|
end
|
||||||
|
|
||||||
def export(start_date, end_date, file)
|
def export(start_date, end_date, file)
|
||||||
# build CVS content
|
# build CSV content
|
||||||
content = header_row
|
content = header_row
|
||||||
invoices = Invoice.where('created_at >= ? AND created_at <= ?', start_date, end_date).order('created_at ASC')
|
invoices = Invoice.where('created_at >= ? AND created_at <= ?', start_date, end_date).order('created_at ASC')
|
||||||
|
invoices = invoices.where('total > 0') unless export_zeros
|
||||||
invoices.each do |i|
|
invoices.each do |i|
|
||||||
content << generate_rows(i)
|
content << generate_rows(i)
|
||||||
end
|
end
|
||||||
@ -37,134 +51,98 @@ class AccountingExportService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def generate_rows(invoice)
|
def generate_rows(invoice)
|
||||||
"#{client_row(invoice)}\n" \
|
rows = client_rows(invoice) + items_rows(invoice)
|
||||||
"#{items_rows(invoice)}" \
|
|
||||||
"#{vat_row(invoice)}\n"
|
vat = vat_row(invoice)
|
||||||
|
rows += "#{vat}\n" unless vat.nil?
|
||||||
|
|
||||||
|
rows
|
||||||
end
|
end
|
||||||
|
|
||||||
# Generate the "subscription" and "reservation" rows associated with the provided invoice
|
# Generate the "subscription" and "reservation" rows associated with the provided invoice
|
||||||
def items_rows(invoice)
|
def items_rows(invoice)
|
||||||
rows = invoice.subscription_invoice? ? "#{subscription_row(invoice)}\n" : ''
|
rows = invoice.subscription_invoice? ? "#{subscription_row(invoice)}\n" : ''
|
||||||
if invoice.invoiced_type == 'Reservation'
|
if invoice.invoiced_type == 'Reservation'
|
||||||
invoice.invoice_items.each do |item|
|
items = invoice.invoice_items.select { |ii| ii.subscription.nil? }
|
||||||
|
items.each do |item|
|
||||||
rows << "#{reservation_row(invoice, item)}\n"
|
rows << "#{reservation_row(invoice, item)}\n"
|
||||||
end
|
end
|
||||||
|
elsif invoice.invoiced_type == 'WalletTransaction'
|
||||||
|
rows << "#{wallet_row(invoice)}\n"
|
||||||
end
|
end
|
||||||
rows
|
rows
|
||||||
end
|
end
|
||||||
|
|
||||||
# Generate the "client" row, which contains the debit to the client account, all taxes included
|
# Generate the "client" rows, which contains the debit to the client account, all taxes included
|
||||||
def client_row(invoice)
|
def client_rows(invoice)
|
||||||
total = invoice.total / 100.00
|
rows = ''
|
||||||
row = ''
|
invoice.payment_means.each do |details|
|
||||||
columns.each do |column|
|
rows << row(
|
||||||
case column
|
invoice,
|
||||||
when 'journal_code'
|
account(invoice, :client, means: details[:means]),
|
||||||
row << journal_code
|
account(invoice, :client, means: details[:means], type: :label),
|
||||||
when 'date'
|
details[:amount] / 100.00,
|
||||||
row << invoice.created_at&.strftime(date_format)
|
line_label: label(invoice),
|
||||||
when 'account_code'
|
debit_method: :debit_client,
|
||||||
row << account(invoice, :client)
|
credit_method: :credit_client
|
||||||
when 'account_label'
|
)
|
||||||
row << account(invoice, :client, :label)
|
rows << "\n"
|
||||||
when 'piece'
|
|
||||||
row << invoice.reference
|
|
||||||
when 'line_label'
|
|
||||||
row << label(invoice.invoicing_profile.full_name)
|
|
||||||
when 'debit_origin'
|
|
||||||
row << debit_client(invoice, total)
|
|
||||||
when 'credit_origin'
|
|
||||||
row << credit_client(invoice, total)
|
|
||||||
when 'debit_euro'
|
|
||||||
row << debit_client(invoice, total)
|
|
||||||
when 'credit_euro'
|
|
||||||
row << credit_client(invoice, total)
|
|
||||||
when 'lettering'
|
|
||||||
row << ''
|
|
||||||
else
|
|
||||||
puts "Unsupported column: #{column}"
|
|
||||||
end
|
end
|
||||||
row << separator
|
rows
|
||||||
end
|
|
||||||
row
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Generate the "reservation" row, which contains the credit to the reservation account, all taxes excluded
|
# Generate the "reservation" row, which contains the credit to the reservation account, all taxes excluded
|
||||||
def reservation_row(invoice, item)
|
def reservation_row(invoice, item)
|
||||||
wo_taxes = (item.amount / (vat_service.invoice_vat(invoice) / 100.00 + 1)) / 100.00
|
row(
|
||||||
row = ''
|
invoice,
|
||||||
columns.each do |column|
|
account(invoice, :reservation),
|
||||||
case column
|
account(invoice, :reservation, type: :label),
|
||||||
when 'journal_code'
|
item.net_amount / 100.00,
|
||||||
row << journal_code
|
line_label: label(invoice)
|
||||||
when 'date'
|
)
|
||||||
row << invoice.created_at&.strftime(date_format)
|
|
||||||
when 'account_code'
|
|
||||||
row << account(invoice, :reservation)
|
|
||||||
when 'account_label'
|
|
||||||
row << account(invoice, :reservation, :label)
|
|
||||||
when 'piece'
|
|
||||||
row << invoice.reference
|
|
||||||
when 'line_label'
|
|
||||||
row << label(item.description)
|
|
||||||
when 'debit_origin'
|
|
||||||
row << debit(invoice, wo_taxes)
|
|
||||||
when 'credit_origin'
|
|
||||||
row << credit(invoice, wo_taxes)
|
|
||||||
when 'debit_euro'
|
|
||||||
row << debit(invoice, wo_taxes)
|
|
||||||
when 'credit_euro'
|
|
||||||
row << credit(invoice, wo_taxes)
|
|
||||||
when 'lettering'
|
|
||||||
row << ''
|
|
||||||
else
|
|
||||||
puts "Unsupported column: #{column}"
|
|
||||||
end
|
|
||||||
row << separator
|
|
||||||
end
|
|
||||||
row
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Generate the "subscription" row, which contains the credit to the subscription account, all taxes excluded
|
# Generate the "subscription" row, which contains the credit to the subscription account, all taxes excluded
|
||||||
def subscription_row(invoice)
|
def subscription_row(invoice)
|
||||||
subscription_item = invoice.invoice_items.select(&:subscription).first
|
subscription_item = invoice.invoice_items.select(&:subscription).first
|
||||||
wo_taxes = (subscription_item.amount / (vat_service.invoice_vat(invoice) / 100.00 + 1)) / 100.00
|
row(
|
||||||
row = ''
|
invoice,
|
||||||
columns.each do |column|
|
account(invoice, :subscription),
|
||||||
case column
|
account(invoice, :subscription, type: :label),
|
||||||
when 'journal_code'
|
subscription_item.net_amount / 100.00,
|
||||||
row << journal_code
|
line_label: label(invoice)
|
||||||
when 'date'
|
)
|
||||||
row << invoice.created_at&.strftime(date_format)
|
|
||||||
when 'account_code'
|
|
||||||
row << account(invoice, :subscription)
|
|
||||||
when 'account_label'
|
|
||||||
row << account(invoice, :subscription, :label)
|
|
||||||
when 'piece'
|
|
||||||
row << invoice.reference
|
|
||||||
when 'line_label'
|
|
||||||
row << label(subscription_item.description)
|
|
||||||
when 'debit_origin'
|
|
||||||
row << debit(invoice, wo_taxes)
|
|
||||||
when 'credit_origin'
|
|
||||||
row << credit(invoice, wo_taxes)
|
|
||||||
when 'debit_euro'
|
|
||||||
row << debit(invoice, wo_taxes)
|
|
||||||
when 'credit_euro'
|
|
||||||
row << credit(invoice, wo_taxes)
|
|
||||||
when 'lettering'
|
|
||||||
row << ''
|
|
||||||
else
|
|
||||||
puts "Unsupported column: #{column}"
|
|
||||||
end
|
end
|
||||||
row << separator
|
|
||||||
end
|
# Generate the "wallet" row, which contains the credit to the wallet account, all taxes excluded
|
||||||
row
|
# This applies to wallet crediting, when an Avoir is generated at this time
|
||||||
|
def wallet_row(invoice)
|
||||||
|
row(
|
||||||
|
invoice,
|
||||||
|
account(invoice, :wallet),
|
||||||
|
account(invoice, :wallet, type: :label),
|
||||||
|
invoice.invoice_items.first.net_amount / 100.00,
|
||||||
|
line_label: label(invoice)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Generate the "VAT" row, which contains the credit to the VAT account, with VAT amount only
|
# Generate the "VAT" row, which contains the credit to the VAT account, with VAT amount only
|
||||||
def vat_row(invoice)
|
def vat_row(invoice)
|
||||||
vat = (invoice.total - (invoice.total / (vat_service.invoice_vat(invoice) / 100.00 + 1))) / 100.00
|
rate = VatHistoryService.new.invoice_vat(invoice)
|
||||||
|
# we do not render the VAT row if it was disabled for this invoice
|
||||||
|
return nil if rate.zero?
|
||||||
|
|
||||||
|
row(
|
||||||
|
invoice,
|
||||||
|
account(invoice, :vat),
|
||||||
|
account(invoice, :vat, type: :label),
|
||||||
|
invoice.invoice_items.map(&:vat).map(&:to_i).reduce(:+) / 100.00,
|
||||||
|
line_label: label(invoice)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate a row of the export, filling the configured columns with the provided values
|
||||||
|
def row(invoice, account_code, account_label, amount, line_label: '', debit_method: :debit, credit_method: :credit)
|
||||||
row = ''
|
row = ''
|
||||||
columns.each do |column|
|
columns.each do |column|
|
||||||
case column
|
case column
|
||||||
@ -173,21 +151,21 @@ class AccountingExportService
|
|||||||
when 'date'
|
when 'date'
|
||||||
row << invoice.created_at&.strftime(date_format)
|
row << invoice.created_at&.strftime(date_format)
|
||||||
when 'account_code'
|
when 'account_code'
|
||||||
row << account(invoice, :vat)
|
row << account_code
|
||||||
when 'account_label'
|
when 'account_label'
|
||||||
row << account(invoice, :vat, :label)
|
row << account_label
|
||||||
when 'piece'
|
when 'piece'
|
||||||
row << invoice.reference
|
row << invoice.reference
|
||||||
when 'line_label'
|
when 'line_label'
|
||||||
row << I18n.t('accounting_export.VAT')
|
row << line_label
|
||||||
when 'debit_origin'
|
when 'debit_origin'
|
||||||
row << debit(invoice, vat)
|
row << method(debit_method).call(invoice, amount)
|
||||||
when 'credit_origin'
|
when 'credit_origin'
|
||||||
row << credit(invoice, vat)
|
row << method(credit_method).call(invoice, amount)
|
||||||
when 'debit_euro'
|
when 'debit_euro'
|
||||||
row << debit(invoice, vat)
|
row << method(debit_method).call(invoice, amount)
|
||||||
when 'credit_euro'
|
when 'credit_euro'
|
||||||
row << credit(invoice, vat)
|
row << method(credit_method).call(invoice, amount)
|
||||||
when 'lettering'
|
when 'lettering'
|
||||||
row << ''
|
row << ''
|
||||||
else
|
else
|
||||||
@ -199,10 +177,10 @@ class AccountingExportService
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Get the account code (or label) for the given invoice and the specified line type (client, vat, subscription or reservation)
|
# Get the account code (or label) for the given invoice and the specified line type (client, vat, subscription or reservation)
|
||||||
def account(invoice, account, type = :code)
|
def account(invoice, account, type: :code, means: :other)
|
||||||
res = case account
|
case account
|
||||||
when :client
|
when :client
|
||||||
Setting.find_by(name: "accounting_client_#{type}")&.value
|
Setting.find_by(name: "accounting_#{means}_client_#{type}")&.value
|
||||||
when :vat
|
when :vat
|
||||||
Setting.find_by(name: "accounting_VAT_#{type}")&.value
|
Setting.find_by(name: "accounting_VAT_#{type}")&.value
|
||||||
when :subscription
|
when :subscription
|
||||||
@ -217,22 +195,27 @@ class AccountingExportService
|
|||||||
else
|
else
|
||||||
puts "WARN: Invoice #{invoice.id} has no reservation"
|
puts "WARN: Invoice #{invoice.id} has no reservation"
|
||||||
end
|
end
|
||||||
|
when :wallet
|
||||||
|
if invoice.invoiced_type == 'WalletTransaction'
|
||||||
|
Setting.find_by(name: "accounting_wallet_#{type}")&.value
|
||||||
|
else
|
||||||
|
puts "WARN: Invoice #{invoice.id} is not a wallet credit"
|
||||||
|
end
|
||||||
else
|
else
|
||||||
puts "Unsupported account #{account}"
|
puts "Unsupported account #{account}"
|
||||||
end
|
end || ''
|
||||||
res || ''
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Fill the value of the "debit" column: if the invoice is a refund, returns the given amount, returns 0 otherwise
|
# Fill the value of the "debit" column: if the invoice is a refund, returns the given amount, returns 0 otherwise
|
||||||
def debit(invoice, amount)
|
def debit(invoice, amount)
|
||||||
avoir = invoice.is_a? Avoir
|
avoir = invoice.is_a? Avoir
|
||||||
avoir ? amount.to_s : '0'
|
avoir ? format_number(amount) : '0'
|
||||||
end
|
end
|
||||||
|
|
||||||
# Fill the value of the "credit" column: if the invoice is a refund, returns 0, otherwise, returns the given amount
|
# Fill the value of the "credit" column: if the invoice is a refund, returns 0, otherwise, returns the given amount
|
||||||
def credit(invoice, amount)
|
def credit(invoice, amount)
|
||||||
avoir = invoice.is_a? Avoir
|
avoir = invoice.is_a? Avoir
|
||||||
avoir ? '0' : amount.to_s
|
avoir ? '0' : format_number(amount)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Fill the value of the "debit" column for the client row: if the invoice is a refund, returns 0, otherwise, returns the given amount
|
# Fill the value of the "debit" column for the client row: if the invoice is a refund, returns 0, otherwise, returns the given amount
|
||||||
@ -245,9 +228,22 @@ class AccountingExportService
|
|||||||
debit(invoice, amount)
|
debit(invoice, amount)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Format the given text to match the accounting software rules for the labels
|
# Format the given number as a string, using the configured separator
|
||||||
def label(text)
|
def format_number(num)
|
||||||
res = text.tr separator, ''
|
number_to_currency(num, unit: '', separator: decimal_separator, delimiter: '', precision: 2)
|
||||||
res.truncate(50)
|
end
|
||||||
|
|
||||||
|
# Create a text from the given invoice, matching the accounting software rules for the labels
|
||||||
|
def label(invoice)
|
||||||
|
name = "#{invoice.invoicing_profile.last_name} #{invoice.invoicing_profile.first_name}".tr separator, ''
|
||||||
|
reference = invoice.reference
|
||||||
|
|
||||||
|
items = invoice.subscription_invoice? ? [I18n.t('accounting_export.subscription')] : []
|
||||||
|
items.push I18n.t("accounting_export.#{invoice.reservation.reservable_type}_reservation") if invoice.invoiced_type == 'Reservation'
|
||||||
|
items.push I18n.t('accounting_export.wallet') if invoice.invoiced_type == 'WalletTransaction'
|
||||||
|
|
||||||
|
summary = items.join(' + ')
|
||||||
|
res = "#{reference}, #{summary}"
|
||||||
|
"#{name.truncate(label_max_length - res.length)}, #{res}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -82,7 +82,7 @@ class Availabilities::StatusService
|
|||||||
availability.slots.each do |s|
|
availability.slots.each do |s|
|
||||||
reserved_slots << s if s.canceled_at.nil?
|
reserved_slots << s if s.canceled_at.nil?
|
||||||
end
|
end
|
||||||
reserved_slots.map(&:reservations).flatten.map(&:user_id).include? user.id
|
reserved_slots.map(&:reservations).flatten.map(&:statistic_profile_id).include? user.statistic_profile&.id
|
||||||
else
|
else
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# This class provides helper methods to deal with coupons
|
||||||
class CouponService
|
class CouponService
|
||||||
##
|
##
|
||||||
# Apply the provided coupon, if active, to the given price. Usability tests will be run depending on the
|
# Apply the provided coupon, if active, to the given price. Usability tests will be run depending on the
|
||||||
@ -54,4 +57,14 @@ class CouponService
|
|||||||
end
|
end
|
||||||
price
|
price
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Compute the total amount of the given invoice, without the applied coupon
|
||||||
|
# Invoice.total stores the amount payed by the customer, coupon deducted
|
||||||
|
# @param invoice {Invoice} invoice object, its total before discount will be computed
|
||||||
|
##
|
||||||
|
def invoice_total_no_coupon(invoice)
|
||||||
|
total = (invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+) or 0)
|
||||||
|
total / 100.0
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
21
app/services/footprint_service.rb
Normal file
21
app/services/footprint_service.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Provides helper methods to compute footprints
|
||||||
|
class FootprintService
|
||||||
|
# Compute the footprint
|
||||||
|
# @param class_name Invoice|InvoiceItem|HistoryValue
|
||||||
|
# @param item an instance of the provided class
|
||||||
|
# @param sort the items in database by the provided criterion, to find the previous one
|
||||||
|
def self.compute_footprint(klass, item, sort_on = 'id')
|
||||||
|
raise TypeError unless item.is_a? klass
|
||||||
|
|
||||||
|
previous = klass.where("#{sort_on} < ?", item[sort_on])
|
||||||
|
.order("#{sort_on} DESC")
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
columns = klass.columns.map(&:name)
|
||||||
|
.delete_if { |c| %w[footprint updated_at].include? c }
|
||||||
|
|
||||||
|
Checksum.text("#{columns.map { |c| item[c] }.join}#{previous.first ? previous.first.footprint : ''}")
|
||||||
|
end
|
||||||
|
end
|
135
app/services/invoice_reference_service.rb
Normal file
135
app/services/invoice_reference_service.rb
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Provides methods to generate invoice references
|
||||||
|
class InvoiceReferenceService
|
||||||
|
class << self
|
||||||
|
def generate_reference(invoice, date: Time.now, avoir: false)
|
||||||
|
pattern = Setting.find_by(name: 'invoice_reference').value
|
||||||
|
|
||||||
|
reference = replace_invoice_number_pattern(pattern, invoice)
|
||||||
|
reference = replace_date_pattern(reference, date)
|
||||||
|
|
||||||
|
if avoir
|
||||||
|
# information about refund/avoir (R[text])
|
||||||
|
reference.gsub!(/R\[([^\]]+)\]/, '\1')
|
||||||
|
|
||||||
|
# remove information about online selling (X[text])
|
||||||
|
reference.gsub!(/X\[([^\]]+)\]/, ''.to_s)
|
||||||
|
else
|
||||||
|
# information about online selling (X[text])
|
||||||
|
if invoice.paid_with_stripe?
|
||||||
|
reference.gsub!(/X\[([^\]]+)\]/, '\1')
|
||||||
|
else
|
||||||
|
reference.gsub!(/X\[([^\]]+)\]/, ''.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
# remove information about refunds (R[text])
|
||||||
|
reference.gsub!(/R\[([^\]]+)\]/, ''.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
reference
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_order_number(invoice)
|
||||||
|
pattern = Setting.find_by(name: 'invoice_order-nb').value
|
||||||
|
|
||||||
|
# global invoice number (nn..nn)
|
||||||
|
reference = pattern.gsub(/n+(?![^\[]*\])/) do |match|
|
||||||
|
pad_and_truncate(number_of_invoices(invoice, 'global'), match.to_s.length)
|
||||||
|
end
|
||||||
|
|
||||||
|
reference = replace_invoice_number_pattern(reference, invoice)
|
||||||
|
replace_date_pattern(reference, invoice.created_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
##
|
||||||
|
# Output the given integer with leading zeros. If the given value is longer than the given
|
||||||
|
# length, it will be truncated.
|
||||||
|
# @param value {Integer} the integer to pad
|
||||||
|
# @param length {Integer} the length of the resulting string.
|
||||||
|
##
|
||||||
|
def pad_and_truncate(value, length)
|
||||||
|
value.to_s.rjust(length, '0').gsub(/^.*(.{#{length},}?)$/m, '\1')
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Returns the number of current invoices in the given range around the current date.
|
||||||
|
# If range is invalid or not specified, the total number of invoices is returned.
|
||||||
|
# @param invoice {Invoice}
|
||||||
|
# @param range {String} 'day', 'month', 'year'
|
||||||
|
# @return {Integer}
|
||||||
|
##
|
||||||
|
def number_of_invoices(invoice, range)
|
||||||
|
case range.to_s
|
||||||
|
when 'day'
|
||||||
|
start = DateTime.current.beginning_of_day
|
||||||
|
ending = DateTime.current.end_of_day
|
||||||
|
when 'month'
|
||||||
|
start = DateTime.current.beginning_of_month
|
||||||
|
ending = DateTime.current.end_of_month
|
||||||
|
when 'year'
|
||||||
|
start = DateTime.current.beginning_of_year
|
||||||
|
ending = DateTime.current.end_of_year
|
||||||
|
else
|
||||||
|
return invoice.id
|
||||||
|
end
|
||||||
|
return Invoice.count unless defined? start && defined? ending
|
||||||
|
|
||||||
|
Invoice.where('created_at >= :start_date AND created_at < :end_date', start_date: start, end_date: ending).length
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Replace the date elements in the provided pattern with the date values, from the provided date
|
||||||
|
# @param reference {string}
|
||||||
|
# @param date {DateTime}
|
||||||
|
##
|
||||||
|
def replace_date_pattern(reference, date)
|
||||||
|
copy = reference.dup
|
||||||
|
|
||||||
|
# full year (YYYY)
|
||||||
|
copy.gsub!(/YYYY(?![^\[]*\])/, date.strftime('%Y'))
|
||||||
|
# year without century (YY)
|
||||||
|
copy.gsub!(/YY(?![^\[]*\])/, date.strftime('%y'))
|
||||||
|
|
||||||
|
# abbreviated month name (MMM)
|
||||||
|
copy.gsub!(/MMM(?![^\[]*\])/, date.strftime('%^b'))
|
||||||
|
# month of the year, zero-padded (MM)
|
||||||
|
copy.gsub!(/MM(?![^\[]*\])/, date.strftime('%m'))
|
||||||
|
# month of the year, non zero-padded (M)
|
||||||
|
copy.gsub!(/M(?![^\[]*\])/, date.strftime('%-m'))
|
||||||
|
|
||||||
|
# day of the month, zero-padded (DD)
|
||||||
|
copy.gsub!(/DD(?![^\[]*\])/, date.strftime('%d'))
|
||||||
|
# day of the month, non zero-padded (DD)
|
||||||
|
copy.gsub!(/DD(?![^\[]*\])/, date.strftime('%-d'))
|
||||||
|
|
||||||
|
copy
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Replace the invoice number elements in the provided pattern with counts from the database
|
||||||
|
# @param reference {string}
|
||||||
|
# @param invoice {Invoice}
|
||||||
|
##
|
||||||
|
def replace_invoice_number_pattern(reference, invoice)
|
||||||
|
copy = reference.dup
|
||||||
|
|
||||||
|
# invoice number per year (yy..yy)
|
||||||
|
copy.gsub!(/y+(?![^\[]*\])/) do |match|
|
||||||
|
pad_and_truncate(number_of_invoices(invoice, 'year'), match.to_s.length)
|
||||||
|
end
|
||||||
|
# invoice number per month (mm..mm)
|
||||||
|
copy.gsub!(/m+(?![^\[]*\])/) do |match|
|
||||||
|
pad_and_truncate(number_of_invoices(invoice, 'month'), match.to_s.length)
|
||||||
|
end
|
||||||
|
# invoice number per day (dd..dd)
|
||||||
|
copy.gsub!(/d+(?![^\[]*\])/) do |match|
|
||||||
|
pad_and_truncate(number_of_invoices(invoice, 'day'), match.to_s.length)
|
||||||
|
end
|
||||||
|
|
||||||
|
copy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
185
app/services/members/import_service.rb
Normal file
185
app/services/members/import_service.rb
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Provides helper methods to bulk-import some users from a CSV file
|
||||||
|
class Members::ImportService
|
||||||
|
class << self
|
||||||
|
def import(import)
|
||||||
|
require 'csv'
|
||||||
|
log = []
|
||||||
|
CSV.foreach(import.attachment.url, headers: true, col_sep: ';') do |row|
|
||||||
|
begin
|
||||||
|
password = hide_password(row)
|
||||||
|
log << { row: row.to_hash }
|
||||||
|
|
||||||
|
# try to find member based on import.update_field
|
||||||
|
user = User.find_by(import.update_field.to_sym => row[import.update_field])
|
||||||
|
params = row_to_params(row, user, password)
|
||||||
|
if user
|
||||||
|
service = Members::MembersService.new(user)
|
||||||
|
res = service.update(params)
|
||||||
|
log << { user: user.id, status: 'update', result: res }
|
||||||
|
else
|
||||||
|
user = User.new(params)
|
||||||
|
service = Members::MembersService.new(user)
|
||||||
|
res = service.create(import.user, params)
|
||||||
|
log << { user: user.id, status: 'create', result: res }
|
||||||
|
end
|
||||||
|
log << user.errors.to_hash unless user.errors.to_hash.empty?
|
||||||
|
rescue StandardError => e
|
||||||
|
log << e.to_s
|
||||||
|
puts e
|
||||||
|
puts e.backtrace
|
||||||
|
end
|
||||||
|
end
|
||||||
|
log
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def hashify(row, property, value: row[property], key: property.to_sym)
|
||||||
|
res = {}
|
||||||
|
res[key] = value if row[property]
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
def row_to_params(row, user, password)
|
||||||
|
res = {}
|
||||||
|
|
||||||
|
res.merge! hashify(row, 'id')
|
||||||
|
res.merge! hashify(row, 'username')
|
||||||
|
res.merge! hashify(row, 'email')
|
||||||
|
res.merge! hashify(row, 'password', value: password)
|
||||||
|
res.merge! hashify(row, 'password', key: :password_confirmation, value: password)
|
||||||
|
res.merge! hashify(row, 'allow_contact', value: row['allow_contact'] == 'yes', key: :is_allow_contact)
|
||||||
|
res.merge! hashify(row, 'allow_newsletter', value: row['allow_newsletter'] == 'yes', key: :is_allow_newsletter)
|
||||||
|
res.merge! hashify(row, 'group', value: group_id(row), key: :group_id)
|
||||||
|
res.merge! hashify(row, 'tags', value: tag_ids(row), key: :tag_ids)
|
||||||
|
|
||||||
|
profile_attributes = profile(row, user)
|
||||||
|
res[:profile_attributes] = profile_attributes if profile_attributes
|
||||||
|
|
||||||
|
invoicing_profile_attributes = invoicing_profile(row, user)
|
||||||
|
res[:invoicing_profile_attributes] = invoicing_profile_attributes if invoicing_profile_attributes
|
||||||
|
|
||||||
|
statistic_profile_attributes = statistic_profile(row, user)
|
||||||
|
res[:statistic_profile_attributes] = statistic_profile_attributes if statistic_profile_attributes
|
||||||
|
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_id(row)
|
||||||
|
return unless row['group']
|
||||||
|
|
||||||
|
Group.friendly.find(row['group'])&.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def tag_ids(row)
|
||||||
|
return unless row['tags']
|
||||||
|
|
||||||
|
Tag.where(id: row['tags'].split(',')).map(&:id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def profile(row, user)
|
||||||
|
res = {}
|
||||||
|
|
||||||
|
res.merge! hashify(row, 'first_name')
|
||||||
|
res.merge! hashify(row, 'last_name')
|
||||||
|
res.merge! hashify(row, 'phone')
|
||||||
|
res.merge! hashify(row, 'interests', key: :interest)
|
||||||
|
res.merge! hashify(row, 'softwares', key: :software_mastered)
|
||||||
|
res.merge! hashify(row, 'website')
|
||||||
|
res.merge! hashify(row, 'job')
|
||||||
|
res.merge! hashify(row, 'facebook')
|
||||||
|
res.merge! hashify(row, 'twitter')
|
||||||
|
res.merge! hashify(row, 'googleplus', key: :google_plus)
|
||||||
|
res.merge! hashify(row, 'viadeo')
|
||||||
|
res.merge! hashify(row, 'linkedin')
|
||||||
|
res.merge! hashify(row, 'instagram')
|
||||||
|
res.merge! hashify(row, 'youtube')
|
||||||
|
res.merge! hashify(row, 'vimeo')
|
||||||
|
res.merge! hashify(row, 'dailymotion')
|
||||||
|
res.merge! hashify(row, 'github')
|
||||||
|
res.merge! hashify(row, 'echosciences')
|
||||||
|
res.merge! hashify(row, 'pinterest')
|
||||||
|
res.merge! hashify(row, 'lastfm')
|
||||||
|
res.merge! hashify(row, 'flickr')
|
||||||
|
|
||||||
|
res[:id] = user.profile.id if user&.profile
|
||||||
|
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
def invoicing_profile(row, user)
|
||||||
|
res = {}
|
||||||
|
|
||||||
|
res[:id] = user.invoicing_profile.id if user&.invoicing_profile
|
||||||
|
|
||||||
|
address_attributes = address(row, user)
|
||||||
|
res[:address_attributes] = address_attributes if address_attributes
|
||||||
|
|
||||||
|
organization_attributes = organization(row, user)
|
||||||
|
res[:organization_attributes] = organization_attributes if organization_attributes
|
||||||
|
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
def statistic_profile(row, user)
|
||||||
|
res = {}
|
||||||
|
|
||||||
|
res.merge! hashify(row, 'gender', value: row['gender'] == 'male')
|
||||||
|
res.merge! hashify(row, 'birthdate', key: :birthday)
|
||||||
|
|
||||||
|
res[:id] = user.statistic_profile.id if user&.statistic_profile
|
||||||
|
|
||||||
|
training_ids = training_ids(row)
|
||||||
|
res[:training_ids] = training_ids if training_ids
|
||||||
|
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
def address(row, user)
|
||||||
|
return unless row['address']
|
||||||
|
|
||||||
|
res = { address: row['address'] }
|
||||||
|
|
||||||
|
res[:id] = user.invoicing_profile.address.id if user&.invoicing_profile&.address
|
||||||
|
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
def organization(row, user)
|
||||||
|
return unless row['organization_name']
|
||||||
|
|
||||||
|
res = { name: row['organization_name'] }
|
||||||
|
|
||||||
|
res[:id] = user.invoicing_profile.organization.id if user&.invoicing_profile&.organization
|
||||||
|
|
||||||
|
address_attributes = organization_address(row, user)
|
||||||
|
res[:address_attributes] = address_attributes if address_attributes
|
||||||
|
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
def organization_address(row, user)
|
||||||
|
return unless row['organization_address']
|
||||||
|
|
||||||
|
res = { address: row['organization_address'] }
|
||||||
|
|
||||||
|
res[:id] = user.invoicing_profile.organization.address.id if user&.invoicing_profile&.organization&.address
|
||||||
|
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
def training_ids(row)
|
||||||
|
return unless row['trainings']
|
||||||
|
|
||||||
|
Training.where(id: row['trainings'].split(',')).map(&:id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def hide_password(row)
|
||||||
|
password = row['password']
|
||||||
|
row['password'] = '********' if row['password']
|
||||||
|
password
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
31
app/services/recaptcha_service.rb
Normal file
31
app/services/recaptcha_service.rb
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Provides methods to verify the client captcha on Google's services
|
||||||
|
class RecaptchaService
|
||||||
|
class << self
|
||||||
|
def verify(client_response)
|
||||||
|
return { 'success' => true } unless recaptcha_enabled?
|
||||||
|
|
||||||
|
require 'uri'
|
||||||
|
require 'net/http'
|
||||||
|
|
||||||
|
data = { secret: secret_key, response: client_response }
|
||||||
|
url = URI.parse('https://www.google.com/recaptcha/api/siteverify')
|
||||||
|
res = Net::HTTP.post_form(url, data)
|
||||||
|
|
||||||
|
JSON.parse(res&.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
def recaptcha_enabled?
|
||||||
|
secret_key.present? && site_key.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def secret_key
|
||||||
|
Rails.application.secrets.recaptcha_secret_key
|
||||||
|
end
|
||||||
|
|
||||||
|
def site_key
|
||||||
|
Rails.application.secrets.recaptcha_site_key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -131,10 +131,12 @@ class StatisticService
|
|||||||
next if i.invoice.is_a?(Avoir)
|
next if i.invoice.is_a?(Avoir)
|
||||||
|
|
||||||
sub = i.subscription
|
sub = i.subscription
|
||||||
|
|
||||||
next unless sub
|
next unless sub
|
||||||
|
|
||||||
ca = i.amount.to_i / 100.0
|
ca = i.amount.to_i / 100.0
|
||||||
ca = CouponService.new.ventilate(get_invoice_total_no_coupon(i.invoice), ca, i.invoice.coupon) unless i.invoice.coupon_id.nil?
|
cs = CouponService.new
|
||||||
|
ca = cs.ventilate(cs.invoice_total_no_coupon(i.invoice), ca, i.invoice.coupon) unless i.invoice.coupon_id.nil?
|
||||||
profile = sub.statistic_profile
|
profile = sub.statistic_profile
|
||||||
p = sub.plan
|
p = sub.plan
|
||||||
result.push OpenStruct.new({
|
result.push OpenStruct.new({
|
||||||
@ -396,7 +398,8 @@ class StatisticService
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
# subtract coupon discount from invoices and refunds
|
# subtract coupon discount from invoices and refunds
|
||||||
ca = CouponService.new.ventilate(get_invoice_total_no_coupon(invoice), ca, invoice.coupon) unless invoice.coupon_id.nil?
|
cs = CouponService.new
|
||||||
|
ca = cs.ventilate(cs.invoice_total_no_coupon(invoice), ca, invoice.coupon) unless invoice.coupon_id.nil?
|
||||||
# divide the result by 100 to convert from centimes to monetary unit
|
# divide the result by 100 to convert from centimes to monetary unit
|
||||||
ca.zero? ? ca : ca / 100.0
|
ca.zero? ? ca : ca / 100.0
|
||||||
end
|
end
|
||||||
@ -407,7 +410,8 @@ class StatisticService
|
|||||||
ca -= ii.amount.to_i
|
ca -= ii.amount.to_i
|
||||||
end
|
end
|
||||||
# subtract coupon discount from the refund
|
# subtract coupon discount from the refund
|
||||||
ca = CouponService.new.ventilate(get_invoice_total_no_coupon(invoice), ca, invoice.coupon) unless invoice.coupon_id.nil?
|
cs = CouponService.new
|
||||||
|
ca = cs.ventilate(cs.invoice_total_no_coupon(invoice), ca, invoice.coupon) unless invoice.coupon_id.nil?
|
||||||
ca.zero? ? ca : ca / 100.0
|
ca.zero? ? ca : ca / 100.0
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -488,11 +492,6 @@ class StatisticService
|
|||||||
# user_subscription_ca.inject {|sum,x| sum.ca + x.ca } || 0
|
# user_subscription_ca.inject {|sum,x| sum.ca + x.ca } || 0
|
||||||
# end
|
# end
|
||||||
|
|
||||||
def get_invoice_total_no_coupon(invoice)
|
|
||||||
total = (invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+) or 0)
|
|
||||||
total / 100.0
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_or_create_user_info_info_list(profile, list)
|
def find_or_create_user_info_info_list(profile, list)
|
||||||
found = list.select do |l|
|
found = list.select do |l|
|
||||||
l.statistic_profile_id == profile.id
|
l.statistic_profile_id == profile.id
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'abstract_controller'
|
require 'abstract_controller'
|
||||||
require 'action_controller'
|
require 'action_controller'
|
||||||
require 'action_view'
|
require 'action_view'
|
||||||
@ -13,7 +15,7 @@ class StatisticsExportService
|
|||||||
# query all stats with range arguments
|
# query all stats with range arguments
|
||||||
query = MultiJson.load(export.query)
|
query = MultiJson.load(export.query)
|
||||||
|
|
||||||
@results = Elasticsearch::Model.client.search({index: 'stats', scroll: '30s', body: query})
|
@results = Elasticsearch::Model.client.search(index: 'stats', scroll: '30s', body: query)
|
||||||
scroll_id = @results['_scroll_id']
|
scroll_id = @results['_scroll_id']
|
||||||
while @results['hits']['hits'].size != @results['hits']['total']
|
while @results['hits']['hits'].size != @results['hits']['total']
|
||||||
scroll_res = Elasticsearch::Model.client.scroll(scroll: '30s', scroll_id: scroll_id)
|
scroll_res = Elasticsearch::Model.client.scroll(scroll: '30s', scroll_id: scroll_id)
|
||||||
@ -22,9 +24,9 @@ class StatisticsExportService
|
|||||||
end
|
end
|
||||||
|
|
||||||
ids = @results['hits']['hits'].map { |u| u['_source']['userId'] }
|
ids = @results['hits']['hits'].map { |u| u['_source']['userId'] }
|
||||||
@users = User.includes(:profile).where(:id => ids)
|
@users = User.includes(:profile).where(id: ids)
|
||||||
|
|
||||||
@indices = StatisticIndex.all.includes(:statistic_fields, :statistic_types => [:statistic_sub_types])
|
@indices = StatisticIndex.all.includes(:statistic_fields, statistic_types: [:statistic_sub_types])
|
||||||
|
|
||||||
ActionController::Base.prepend_view_path './app/views/'
|
ActionController::Base.prepend_view_path './app/views/'
|
||||||
# place data in view_assigns
|
# place data in view_assigns
|
||||||
@ -37,10 +39,10 @@ class StatisticsExportService
|
|||||||
|
|
||||||
content = av.render template: 'exports/statistics_global.xlsx.axlsx'
|
content = av.render template: 'exports/statistics_global.xlsx.axlsx'
|
||||||
# write content to file
|
# write content to file
|
||||||
File.open(export.file,"w+b") {|f| f.puts content }
|
File.open(export.file, 'w+b') { |f| f.puts content }
|
||||||
end
|
end
|
||||||
|
|
||||||
%w(account event machine project subscription training space).each do |path|
|
%w[account event machine project subscription training space].each do |path|
|
||||||
class_eval %{
|
class_eval %{
|
||||||
def export_#{path}(export)
|
def export_#{path}(export)
|
||||||
|
|
||||||
@ -76,7 +78,7 @@ class StatisticsExportService
|
|||||||
# write content to file
|
# write content to file
|
||||||
File.open(export.file,"w+b") {|f| f.puts content }
|
File.open(export.file,"w+b") {|f| f.puts content }
|
||||||
end
|
end
|
||||||
}
|
}, __FILE__, __LINE__ - 35
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
@ -26,13 +26,21 @@ class VatHistoryService
|
|||||||
private
|
private
|
||||||
|
|
||||||
def vat_history
|
def vat_history
|
||||||
key_dates = []
|
chronology = []
|
||||||
Setting.find_by(name: 'invoice_VAT-rate').history_values.each do |rate|
|
end_date = DateTime.now
|
||||||
key_dates.push(date: rate.created_at, rate: rate.value.to_i)
|
Setting.find_by(name: 'invoice_VAT-active').history_values.order(created_at: 'DESC').each do |v|
|
||||||
|
chronology.push(start: v.created_at, end: end_date, enabled: v.value == 'true')
|
||||||
|
end_date = v.created_at
|
||||||
end
|
end
|
||||||
Setting.find_by(name: 'invoice_VAT-active').history_values.each do |v|
|
date_rates = []
|
||||||
key_dates.push(date: v.created_at, rate: 0) if v.value == 'false'
|
Setting.find_by(name: 'invoice_VAT-rate').history_values.order(created_at: 'ASC').each do |rate|
|
||||||
|
range = chronology.select { |p| rate.created_at.between?(p[:start], p[:end]) }.first
|
||||||
|
date = range[:enabled] ? rate.created_at : range[:end]
|
||||||
|
date_rates.push(date: date, rate: rate.value.to_i)
|
||||||
end
|
end
|
||||||
key_dates.sort_by { |k| k[:date] }
|
chronology.reverse_each do |period|
|
||||||
|
date_rates.push(date: period[:start], rate: 0) unless period[:enabled]
|
||||||
|
end
|
||||||
|
date_rates.sort_by { |k| k[:date] }
|
||||||
end
|
end
|
||||||
end
|
end
|
@ -51,7 +51,7 @@ class WalletService
|
|||||||
avoir.avoir_date = avoir_date
|
avoir.avoir_date = avoir_date
|
||||||
avoir.created_at = avoir_date
|
avoir.created_at = avoir_date
|
||||||
avoir.description = description
|
avoir.description = description
|
||||||
avoir.avoir_mode = 'wallet'
|
avoir.payment_method = 'wallet'
|
||||||
avoir.subscription_to_expire = false
|
avoir.subscription_to_expire = false
|
||||||
avoir.invoicing_profile_id = wallet_transaction.wallet.user.invoicing_profile.id
|
avoir.invoicing_profile_id = wallet_transaction.wallet.user.invoicing_profile.id
|
||||||
avoir.total = wallet_transaction.amount * 100.0
|
avoir.total = wallet_transaction.amount * 100.0
|
||||||
|
28
app/uploaders/import_uploader.rb
Normal file
28
app/uploaders/import_uploader.rb
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# CarrierWave uploader for import files.
|
||||||
|
# This file defines the parameters for these uploads
|
||||||
|
class ImportUploader < CarrierWave::Uploader::Base
|
||||||
|
include UploadHelper
|
||||||
|
|
||||||
|
# Choose what kind of storage to use for this uploader:
|
||||||
|
storage :file
|
||||||
|
after :remove, :delete_empty_dirs
|
||||||
|
|
||||||
|
# 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
|
||||||
|
'../imports'
|
||||||
|
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
|
||||||
|
['csv']
|
||||||
|
end
|
||||||
|
end
|
@ -1,13 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# CarrierWave uploader for project CAO attachments.
|
||||||
|
# This file defines the parameters for these uploads
|
||||||
class ProjectCaoUploader < CarrierWave::Uploader::Base
|
class ProjectCaoUploader < CarrierWave::Uploader::Base
|
||||||
# Include RMagick or MiniMagick support:
|
|
||||||
# include CarrierWave::RMagick
|
|
||||||
#include CarrierWave::MiniMagick
|
|
||||||
include UploadHelper
|
include UploadHelper
|
||||||
|
|
||||||
# Choose what kind of storage to use for this uploader:
|
# Choose what kind of storage to use for this uploader:
|
||||||
storage :file
|
storage :file
|
||||||
after :remove, :delete_empty_dirs
|
after :remove, :delete_empty_dirs
|
||||||
# storage :fog
|
|
||||||
|
|
||||||
# Override the directory where uploaded files will be stored.
|
# Override the directory where uploaded files will be stored.
|
||||||
# This is a sensible default for uploaders that are meant to be mounted:
|
# This is a sensible default for uploaders that are meant to be mounted:
|
||||||
@ -20,31 +20,9 @@ class ProjectCaoUploader < CarrierWave::Uploader::Base
|
|||||||
"uploads/#{model.class.to_s.underscore}"
|
"uploads/#{model.class.to_s.underscore}"
|
||||||
end
|
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.
|
# Add a white list of extensions which are allowed to be uploaded.
|
||||||
# For images you might use something like this:
|
# For images you might use something like this:
|
||||||
def extension_white_list
|
def extension_white_list
|
||||||
ENV['ALLOWED_EXTENSIONS'].split(' ')
|
ENV['ALLOWED_EXTENSIONS'].split(' ')
|
||||||
end
|
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
|
end
|
||||||
|
@ -4,9 +4,7 @@ json.link_to_sso_profile @provider.link_to_sso_profile
|
|||||||
if @provider.providable_type == DatabaseProvider.name
|
if @provider.providable_type == DatabaseProvider.name
|
||||||
json.link_to_sso_connect '/#'
|
json.link_to_sso_connect '/#'
|
||||||
else
|
else
|
||||||
json.link_to_sso_connect "/users/auth/#{@provider.strategy_name}"
|
json.link_to_sso_connect '/sso-redirect'
|
||||||
end
|
end
|
||||||
|
|
||||||
if @provider.providable_type == OAuth2Provider.name
|
json.domain @provider.providable.domain if @provider.providable_type == OAuth2Provider.name
|
||||||
json.domain @provider.providable.domain
|
|
||||||
end
|
|
||||||
|
7
app/views/api/imports/show.json.jbuilder
Normal file
7
app/views/api/imports/show.json.jbuilder
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
json.extract! @import, :id, :category, :user_id, :update_field, :created_at, :updated_at
|
||||||
|
json.results @import.results_hash.to_json
|
||||||
|
json.user do
|
||||||
|
json.full_name @import.user&.profile&.full_name
|
||||||
|
end
|
@ -1,4 +1,4 @@
|
|||||||
json.extract! @avoir, :id, :created_at, :reference, :invoiced_type, :avoir_date, :avoir_mode, :invoice_id
|
json.extract! @avoir, :id, :created_at, :reference, :invoiced_type, :avoir_date, :payment_method, :invoice_id
|
||||||
json.user_id @avoir.invoicing_profile.user_id
|
json.user_id @avoir.invoicing_profile.user_id
|
||||||
json.total @avoir.total / 100.00
|
json.total @avoir.total / 100.00
|
||||||
json.name @avoir.user.profile.full_name
|
json.name @avoir.user.profile.full_name
|
||||||
|
1
app/views/api/invoices/first.json.jbuilder
Normal file
1
app/views/api/invoices/first.json.jbuilder
Normal file
@ -0,0 +1 @@
|
|||||||
|
json.date @first
|
@ -0,0 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
json.title notification.notification_type
|
||||||
|
json.description t('.import_over', CATEGORY: t(".#{notification.attached_object.category}")) +
|
||||||
|
link_to(t('.view_results'), "#!/admin/members/import/#{notification.attached_object.id}/results")
|
||||||
|
json.url notification_url(notification, format: :json)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user