├── log └── .keep ├── tmp ├── .keep └── pdfs │ ├── .keep │ ├── source │ └── .keep │ └── encrypted │ └── .keep ├── vendor └── .keep ├── lib ├── assets │ └── .keep ├── tasks │ ├── .keep │ ├── sample.csv │ ├── import.rake │ └── dev.rake └── templates │ └── haml │ └── scaffold │ └── _form.html.haml ├── public ├── favicon.ico ├── apple-touch-icon.png ├── apple-touch-icon-precomposed.png └── robots.txt ├── test ├── helpers │ ├── .keep │ └── payrolls_test.rb ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── overtime_test.rb │ ├── term_test.rb │ ├── extra_entry_test.rb │ └── statement_test.rb ├── system │ └── .keep ├── integration │ └── .keep ├── factories │ ├── terms.rb │ ├── overtime.rb │ ├── statement.rb │ ├── correction.rb │ ├── extra_entry.rb │ ├── salary.rb │ ├── payroll.rb │ └── employee.rb ├── application_system_test_case.rb ├── services │ ├── payroll_init_service_test.rb │ ├── income_tax_service │ │ ├── insuranced_salary_test.rb │ │ ├── regular_employee_test.rb │ │ ├── exemption_test.rb │ │ ├── irregular_income_test.rb │ │ ├── professional_service_test.rb │ │ ├── uninsuranced_salary_test.rb │ │ └── dispatcher_test.rb │ ├── health_insurance_service │ │ ├── supplement_premium_rate_test.rb │ │ ├── company_withhold_bonus_test.rb │ │ ├── parttime_income_test.rb │ │ ├── company_coverage_test.rb │ │ ├── professional_service_test.rb │ │ └── dispatcher_test.rb │ ├── business_calendar_days_serivce_test.rb │ ├── calculation_service │ │ ├── sicktime_test.rb │ │ ├── vacation_refund_test.rb │ │ └── leavetime_test.rb │ ├── payroll_init_regulars_service_test.rb │ ├── salary_service │ │ └── sync_payroll_relations_test.rb │ ├── minimun_wage_service_test.rb │ └── statement_service │ │ ├── splitter_test.rb │ │ └── builder_test.rb ├── test_helper.rb ├── decorators │ ├── employee_decorator_test.rb │ ├── payroll_decorator_test.rb │ ├── salary_decorator_test.rb │ └── statement_decorator_test.rb └── lib │ └── collection_translatable_test.rb ├── .ruby-version ├── app ├── assets │ ├── images │ │ └── .keep │ ├── stylesheets │ │ ├── terms.scss │ │ ├── reports.scss │ │ ├── employees.scss │ │ ├── statements.scss │ │ ├── salaries.scss │ │ ├── application.scss │ │ └── payrolls.scss │ ├── javascripts │ │ ├── channels │ │ │ └── .keep │ │ ├── employees.js │ │ ├── cable.js │ │ └── application.js │ └── config │ │ └── manifest.js ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ ├── correction.rb │ ├── term.rb │ ├── extra_entry.rb │ ├── overtime.rb │ ├── payroll_detail.rb │ ├── statement.rb │ ├── employee.rb │ ├── salary_tracker.rb │ └── salary.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── application_controller.rb │ ├── terms_controller.rb │ ├── statements_controller.rb │ ├── salaries_controller.rb │ └── reports_controller.rb ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── mailer.html.erb │ │ ├── pdf.pdf.haml │ │ └── application.html.erb │ ├── reports │ │ ├── index.haml │ │ ├── _title.haml │ │ ├── _cell.haml │ │ ├── _header.haml │ │ ├── irregular.haml │ │ ├── monthly.haml │ │ ├── service.haml │ │ └── salary.haml │ ├── employees │ │ ├── edit.haml │ │ ├── new.haml │ │ ├── show.haml │ │ ├── _info.haml │ │ ├── _terms.haml │ │ ├── _payrolls.haml │ │ ├── _salaries.haml │ │ ├── index.haml │ │ └── _form.haml │ ├── kaminari │ │ ├── _gap.html.haml │ │ ├── _first_page.html.haml │ │ ├── _last_page.html.haml │ │ ├── _next_page.html.haml │ │ ├── _prev_page.html.haml │ │ ├── _page.html.haml │ │ └── _paginator.html.haml │ ├── statements │ │ ├── index.json.jbuilder │ │ ├── _notes.haml │ │ ├── _correction_fields.haml │ │ ├── _details.haml │ │ ├── index.haml │ │ ├── edit.haml │ │ ├── _correction_form.haml │ │ ├── _list.haml │ │ ├── show.haml │ │ └── split.haml │ ├── terms │ │ ├── _info.haml │ │ ├── _form.haml │ │ ├── new.haml │ │ ├── edit.haml │ │ └── show.haml │ ├── statement_mailer │ │ └── notify_email.text.erb │ ├── payrolls │ │ ├── _festival_bonus_form.haml │ │ ├── _initiate_link.haml │ │ ├── _extra_entry_fields.haml │ │ ├── index.haml │ │ ├── _overtime_fields.haml │ │ ├── _basic_form.haml │ │ ├── _list.haml │ │ ├── _statements.haml │ │ ├── _overtime_form.haml │ │ ├── parttimers.haml │ │ ├── edit.haml │ │ └── _extra_entry_form.haml │ └── salaries │ │ ├── new.haml │ │ ├── edit.haml │ │ ├── show.haml │ │ ├── _info.haml │ │ ├── recent.js.erb │ │ └── _form.haml ├── helpers │ ├── application_helper.rb │ └── payrolls_helper.rb ├── decorators │ ├── employee_decorator.rb │ ├── payroll_decorator.rb │ ├── salary_decorator.rb │ └── statement_decorator.rb ├── mailers │ ├── application_mailer.rb │ └── statement_mailer.rb ├── services │ ├── callable.rb │ ├── salary_service │ │ ├── destroy.rb │ │ ├── update.rb │ │ ├── create.rb │ │ ├── sync_payroll_relations.rb │ │ ├── term_finder.rb │ │ └── base.rb │ ├── income_tax_service │ │ ├── regular_employee.rb │ │ ├── insuranced_salary.rb │ │ ├── professional_service.rb │ │ ├── exemption.rb │ │ ├── dispatcher.rb │ │ ├── irregular_income.rb │ │ └── uninsuranced_salary.rb │ ├── format_service │ │ ├── extra_income.rb │ │ ├── extra_deductions.rb │ │ ├── festival_bonus.rb │ │ ├── statement_gain_loss_columns.rb │ │ ├── statement_notes.rb │ │ └── deductions.rb │ ├── calculation_service │ │ ├── sicktime.rb │ │ ├── vacation_refund.rb │ │ ├── leavetime.rb │ │ ├── total_income.rb │ │ ├── total_deduction.rb │ │ └── overtime.rb │ ├── health_insurance_service │ │ ├── supplement_premium_rate.rb │ │ ├── company_withhold_bonus.rb │ │ ├── dispatcher.rb │ │ ├── professional_service.rb │ │ ├── parttime_income.rb │ │ ├── company_coverage.rb │ │ └── bonus_income.rb │ ├── payroll_init_regulars_service.rb │ ├── statement_service │ │ ├── update.rb │ │ ├── splitter.rb │ │ └── builder.rb │ ├── report_service │ │ ├── vacation_refund_cell.rb │ │ ├── overtime_cell.rb │ │ ├── service_income_cell.rb │ │ ├── extra_entry_cell.rb │ │ ├── service_income.rb │ │ ├── irregular_income.rb │ │ ├── monthly_statements.rb │ │ └── salary_income.rb │ ├── payroll_init_service.rb │ ├── minimum_wage_service.rb │ └── business_calendar_days_service.rb └── lib │ ├── collection_translatable.rb │ └── statement_pdf_builder.rb ├── package.json ├── config ├── initializers │ ├── ransack.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── generators.rb │ ├── filter_parameter_logging.rb │ ├── application_controller_renderer.rb │ ├── cookies_serializer.rb │ ├── kaminari_config.rb │ ├── backtrace_silencers.rb │ ├── wrap_parameters.rb │ ├── assets.rb │ └── wicked_pdf.rb ├── spring.rb ├── boot.rb ├── locales │ ├── reports.zh-TW.yml │ ├── simple_form.en.yml │ └── en.yml ├── environment.rb ├── cable.yml ├── application.yml.sample ├── holidays │ ├── 2015.yml │ ├── 2025.yml │ ├── 2022.yml │ ├── 2024.yml │ ├── 2018.yml │ ├── 2017.yml │ ├── 2020.yml │ ├── 2021.yml │ ├── 2016.yml │ ├── 2019.yml │ └── 2023.yml ├── application.rb ├── minimum_wage.yml ├── secrets.yml.sample └── routes.rb ├── bin ├── bundle ├── rake ├── rails ├── yarn ├── spring ├── update └── setup ├── config.ru ├── db ├── migrate │ ├── 20181009033016_create_reports.rb │ ├── 20181026081745_drop_reports.rb │ ├── 20181027094809_create_payroll_details.rb │ ├── 20200714082821_add_term_to_salary.rb │ ├── 20200716024001_create_salary_trackers.rb │ ├── 20181008070814_add_salary_to_payrolls.rb │ ├── 20181026081854_update_reports_to_version_7.rb │ ├── 20181017081148_add_b2b_to_employee.rb │ ├── 20190303142812_rename_income_tax.rb │ ├── 20200103074054_add_split_to_salary.rb │ ├── 20180413081120_add_cycle_to_salaries.rb │ ├── 20181027082411_add_owner_to_employees.rb │ ├── 20181010173602_add_index_to_payrolls.rb │ ├── 20190225063154_add_tax_income_to_salary.rb │ ├── 20181010080608_update_reports_to_version_2.rb │ ├── 20181011102527_update_reports_to_version_3.rb │ ├── 20181013142045_update_reports_to_version_4.rb │ ├── 20181013163043_update_reports_to_version_5.rb │ ├── 20181017081241_update_reports_to_version_6.rb │ ├── 20200703080637_update_reports_to_version_8.rb │ ├── 20200721073317_add_correct_to_statements.rb │ ├── 20200721093758_update_reports_to_version_9.rb │ ├── 20181026080817_change_irregular_to_subsidy.rb │ ├── 20181027154228_remove_taxable_from_extra_entries.rb │ ├── 20181018072141_add_bonus_income_to_statements.rb │ ├── 20181028134613_add_excess_income_to_statements.rb │ ├── 20181028142154_remove_bonus_income_from_statements.rb │ ├── 20181010071005_add_irregular_income_to_statements.rb │ ├── 20181026041233_add_income_type_to_extra_entries.rb │ ├── 20181028141152_update_payroll_details_to_version_2.rb │ ├── 20200703065915_add_detail_to_statement.rb │ ├── 20180412032053_add_bank_transfer_type_to_employees.rb │ ├── 20180604064006_add_monthly_wage_adjustment_to_salary.rb │ ├── 20181108055027_remove_dates_from_employee.rb │ ├── 20181013135559_add_festival_to_payrolls.rb │ ├── 20180413054837_add_insurance_to_salaries.rb │ ├── 20181011084858_create_corrections.rb │ ├── 20180106155844_create_overtimes.rb │ ├── 20181106120752_create_terms.rb │ ├── 20180114122028_create_statements.rb │ ├── 20180107080943_create_extra_entries.rb │ ├── 20180105170858_create_payrolls.rb │ ├── 20180103063747_create_employees.rb │ ├── 20181014044335_add_indexes.rb │ ├── 20180105160319_create_salaries.rb │ └── 20181119082055_change_float_to_decimal.rb ├── views │ ├── reports_v01.sql │ ├── salary_trackers_v01.sql │ ├── reports_v02.sql │ ├── payroll_details_v01.sql │ ├── payroll_details_v02.sql │ ├── reports_v03.sql │ ├── reports_v04.sql │ ├── reports_v05.sql │ ├── reports_v07.sql │ ├── reports_v06.sql │ ├── reports_v09.sql │ └── reports_v08.sql └── seeds.rb ├── Rakefile ├── .reek.yml ├── .gitignore ├── LICENSE.txt ├── Gemfile └── .github └── workflows └── rails.yml /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/system/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/pdfs/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.6 2 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/pdfs/source/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/pdfs/encrypted/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/stylesheets/terms.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/employees.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/stylesheets/reports.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/views/reports/index.haml: -------------------------------------------------------------------------------- 1 | = render "header" 2 | -------------------------------------------------------------------------------- /app/assets/stylesheets/employees.scss: -------------------------------------------------------------------------------- 1 | .employees { 2 | } 3 | -------------------------------------------------------------------------------- /app/views/employees/edit.haml: -------------------------------------------------------------------------------- 1 | %h1 編輯員工資料 2 | = render "form", employee: @employee 3 | -------------------------------------------------------------------------------- /app/views/employees/new.haml: -------------------------------------------------------------------------------- 1 | %h1 新增員工資料 2 | = render "form", employee: @employee 3 | -------------------------------------------------------------------------------- /app/views/reports/_title.haml: -------------------------------------------------------------------------------- 1 | %h3 #{params[:year]} 年#{t(action_name, scope: :reports)} 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fortuna", 3 | "private": true, 4 | "dependencies": {} 5 | } 6 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module ApplicationHelper 3 | end 4 | -------------------------------------------------------------------------------- /app/views/kaminari/_gap.html.haml: -------------------------------------------------------------------------------- 1 | %li.disabled 2 | = content_tag :a, raw(t 'views.pagination.truncate') 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /test/factories/terms.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | FactoryBot.define do 3 | factory :term do 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/statements.scss: -------------------------------------------------------------------------------- 1 | .statements#index { 2 | #statement_search { 3 | margin-bottom: 20px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/factories/overtime.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :overtime do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/factories/statement.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :statement do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/salaries.scss: -------------------------------------------------------------------------------- 1 | .salaries { 2 | .tip { 3 | border-bottom: 1px dashed #ccc; 4 | cursor: help; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /config/initializers/ransack.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | Ransack.configure do |config| 3 | config.search_key = :query 4 | end 5 | -------------------------------------------------------------------------------- /test/factories/correction.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :correction do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/factories/extra_entry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :extra_entry do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/views/kaminari/_first_page.html.haml: -------------------------------------------------------------------------------- 1 | %li 2 | = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote 3 | -------------------------------------------------------------------------------- /app/views/kaminari/_last_page.html.haml: -------------------------------------------------------------------------------- 1 | %li 2 | = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {:remote => remote} 3 | -------------------------------------------------------------------------------- /app/views/statements/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | json.array! @statements, :id, :amount, :year, :month, :employee_id 3 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class ApplicationRecord < ActiveRecord::Base 3 | self.abstract_class = true 4 | end 5 | -------------------------------------------------------------------------------- /app/views/statements/_notes.haml: -------------------------------------------------------------------------------- 1 | %tr 2 | %th.text-center 備註 3 | %td{colspan: 3} 4 | %ul 5 | - notes.each do |note| 6 | %li= note 7 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /test/factories/salary.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :salary do 5 | role { "regular" } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/kaminari/_next_page.html.haml: -------------------------------------------------------------------------------- 1 | %li 2 | = link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, :rel => 'next', :remote => remote 3 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /db/migrate/20181009033016_create_reports.rb: -------------------------------------------------------------------------------- 1 | class CreateReports < ActiveRecord::Migration[5.1] 2 | def change 3 | create_view :reports 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/kaminari/_prev_page.html.haml: -------------------------------------------------------------------------------- 1 | %li 2 | = link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, :rel => 'prev', :remote => remote 3 | -------------------------------------------------------------------------------- /config/locales/reports.zh-TW.yml: -------------------------------------------------------------------------------- 1 | zh-TW: 2 | reports: 3 | salary: "薪資支出表" 4 | service: "勞務報酬支出表" 5 | irregular: "非經常性給予支出表" 6 | monthly: "每月薪資明細" 7 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /app/decorators/employee_decorator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module EmployeeDecorator 3 | def role 4 | salaries.ordered.first.given_role 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20181026081745_drop_reports.rb: -------------------------------------------------------------------------------- 1 | class DropReports < ActiveRecord::Migration[5.2] 2 | def change 3 | drop_view :reports, revert_to_version: 6 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181027094809_create_payroll_details.rb: -------------------------------------------------------------------------------- 1 | class CreatePayrollDetails < ActiveRecord::Migration[5.2] 2 | def change 3 | create_view :payroll_details 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20200714082821_add_term_to_salary.rb: -------------------------------------------------------------------------------- 1 | class AddTermToSalary < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :salaries, :term_id, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20200716024001_create_salary_trackers.rb: -------------------------------------------------------------------------------- 1 | class CreateSalaryTrackers < ActiveRecord::Migration[5.2] 2 | def change 3 | create_view :salary_trackers 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class ApplicationMailer < ActionMailer::Base 3 | default from: ENV.fetch("email_sender") 4 | layout "mailer" 5 | end 6 | -------------------------------------------------------------------------------- /app/views/terms/_info.haml: -------------------------------------------------------------------------------- 1 | %h3 合作期間 2 | %table.table 3 | %tr 4 | %th.col-xs-1 起始日期 5 | %td.col-xs-2= term.start_date 6 | %th.col-xs-1 結束日期 7 | %td.col-xs-2= term.end_date 8 | -------------------------------------------------------------------------------- /db/migrate/20181008070814_add_salary_to_payrolls.rb: -------------------------------------------------------------------------------- 1 | class AddSalaryToPayrolls < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :payrolls, :salary_id, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181026081854_update_reports_to_version_7.rb: -------------------------------------------------------------------------------- 1 | class UpdateReportsToVersion7 < ActiveRecord::Migration[5.2] 2 | def change 3 | create_view :reports, version: 7 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | ActiveSupport::Inflector.inflections(:en) do |inflect| 2 | # inflect.singular /^(bonus)(es)?$/i, '\1' 3 | # inflect.plural /^(bonus)$/i, '\1us' 4 | end 5 | 6 | -------------------------------------------------------------------------------- /db/migrate/20181017081148_add_b2b_to_employee.rb: -------------------------------------------------------------------------------- 1 | class AddB2bToEmployee < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :employees, :b2b, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20190303142812_rename_income_tax.rb: -------------------------------------------------------------------------------- 1 | class RenameIncomeTax < ActiveRecord::Migration[5.2] 2 | def change 3 | rename_column :salaries, :income_tax, :fixed_income_tax 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /db/migrate/20200103074054_add_split_to_salary.rb: -------------------------------------------------------------------------------- 1 | class AddSplitToSalary < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :salaries, :split, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180413081120_add_cycle_to_salaries.rb: -------------------------------------------------------------------------------- 1 | class AddCycleToSalaries < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :salaries, :cycle, :string, default: "normal" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181027082411_add_owner_to_employees.rb: -------------------------------------------------------------------------------- 1 | class AddOwnerToEmployees < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :employees, :owner, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181010173602_add_index_to_payrolls.rb: -------------------------------------------------------------------------------- 1 | class AddIndexToPayrolls < ActiveRecord::Migration[5.1] 2 | def change 3 | add_index :payrolls, :year 4 | add_index :payrolls, :month 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20190225063154_add_tax_income_to_salary.rb: -------------------------------------------------------------------------------- 1 | class AddTaxIncomeToSalary < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :salaries, :income_tax, :integer, :default => 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://localhost:6379/1 10 | channel_prefix: fortuna_production 11 | -------------------------------------------------------------------------------- /db/migrate/20181010080608_update_reports_to_version_2.rb: -------------------------------------------------------------------------------- 1 | class UpdateReportsToVersion2 < ActiveRecord::Migration[5.1] 2 | def change 3 | update_view :reports, version: 2, revert_to_version: 1 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181011102527_update_reports_to_version_3.rb: -------------------------------------------------------------------------------- 1 | class UpdateReportsToVersion3 < ActiveRecord::Migration[5.1] 2 | def change 3 | update_view :reports, version: 3, revert_to_version: 2 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181013142045_update_reports_to_version_4.rb: -------------------------------------------------------------------------------- 1 | class UpdateReportsToVersion4 < ActiveRecord::Migration[5.1] 2 | def change 3 | update_view :reports, version: 4, revert_to_version: 3 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181013163043_update_reports_to_version_5.rb: -------------------------------------------------------------------------------- 1 | class UpdateReportsToVersion5 < ActiveRecord::Migration[5.1] 2 | def change 3 | update_view :reports, version: 5, revert_to_version: 4 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181017081241_update_reports_to_version_6.rb: -------------------------------------------------------------------------------- 1 | class UpdateReportsToVersion6 < ActiveRecord::Migration[5.2] 2 | def change 3 | update_view :reports, version: 6, revert_to_version: 5 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20200703080637_update_reports_to_version_8.rb: -------------------------------------------------------------------------------- 1 | class UpdateReportsToVersion8 < ActiveRecord::Migration[5.2] 2 | def change 3 | update_view :reports, version: 8, revert_to_version: 7 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20200721073317_add_correct_to_statements.rb: -------------------------------------------------------------------------------- 1 | class AddCorrectToStatements < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :statements, :correction, :integer, default: 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20200721093758_update_reports_to_version_9.rb: -------------------------------------------------------------------------------- 1 | class UpdateReportsToVersion9 < ActiveRecord::Migration[5.2] 2 | def change 3 | update_view :reports, version: 9, revert_to_version: 8 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/statements/_correction_fields.haml: -------------------------------------------------------------------------------- 1 | %tr.nested-fields 2 | %td= f.input_field :amount 3 | %td= f.input_field :description, size: 50 4 | %td= link_to_remove_association "移除", f, class: "btn btn-xs btn-danger" 5 | -------------------------------------------------------------------------------- /db/migrate/20181026080817_change_irregular_to_subsidy.rb: -------------------------------------------------------------------------------- 1 | class ChangeIrregularToSubsidy < ActiveRecord::Migration[5.2] 2 | def change 3 | rename_column :statements, :irregular_income, :subsidy_income 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181027154228_remove_taxable_from_extra_entries.rb: -------------------------------------------------------------------------------- 1 | class RemoveTaxableFromExtraEntries < ActiveRecord::Migration[5.2] 2 | def change 3 | remove_column :extra_entries, :taxable, :boolean 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/services/callable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Callable 3 | extend ActiveSupport::Concern 4 | 5 | class_methods do 6 | def call(*args) 7 | new(*args).call 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /config/initializers/generators.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.generators do |g| 2 | g.template_engine :haml 3 | g.test_framework :test_unit, fixture: false 4 | g.fixture_replacement :factory_girl, dir: 'spec/factories' 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181018072141_add_bonus_income_to_statements.rb: -------------------------------------------------------------------------------- 1 | class AddBonusIncomeToStatements < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :statements, :bonus_income, :integer, default: 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181028134613_add_excess_income_to_statements.rb: -------------------------------------------------------------------------------- 1 | class AddExcessIncomeToStatements < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :statements, :excess_income, :integer, default: 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181028142154_remove_bonus_income_from_statements.rb: -------------------------------------------------------------------------------- 1 | class RemoveBonusIncomeFromStatements < ActiveRecord::Migration[5.2] 2 | def change 3 | remove_column :statements, :bonus_income, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/correction.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 由於本系統為動態計算,當計算公式因故有所調整時, 3 | # 會與既有會計紀錄(且會計端已無法修改)產生誤差, 4 | # 為讓系統報表與會計紀錄維持一致,可透過此 model 做手動校正 5 | class Correction < ApplicationRecord 6 | belongs_to :statement 7 | end 8 | -------------------------------------------------------------------------------- /app/views/statement_mailer/notify_email.text.erb: -------------------------------------------------------------------------------- 1 | Hi, 2 | 3 | 感謝大家這個月來的幫忙, 4 | 附件是本月份的薪資明細表, 5 | 預設密碼為您的身份證字號。 6 | 若有任何問題歡迎隨時向公司提出 :) 7 | 8 | ------------------------------------------------------------ 9 | 本信件為自動產生,請勿直接回覆 10 | -------------------------------------------------------------------------------- /app/models/term.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class Term < ApplicationRecord 3 | belongs_to :employee 4 | has_many :salaries, dependent: :restrict_with_error 5 | 6 | scope :ordered, -> { order(start_date: :desc) } 7 | end 8 | -------------------------------------------------------------------------------- /app/views/reports/_cell.haml: -------------------------------------------------------------------------------- 1 | - if cell.present? 2 | - if cell.adjusted_amount.zero? 3 | %td 0 4 | - else 5 | %td= link_to number_with_delimiter(cell.adjusted_amount), edit_payroll_path(cell.payroll_id) 6 | - else 7 | %td 0 8 | -------------------------------------------------------------------------------- /db/migrate/20181010071005_add_irregular_income_to_statements.rb: -------------------------------------------------------------------------------- 1 | class AddIrregularIncomeToStatements < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :statements, :irregular_income, :integer, default: 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181026041233_add_income_type_to_extra_entries.rb: -------------------------------------------------------------------------------- 1 | class AddIncomeTypeToExtraEntries < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :extra_entries, :income_type, :string, default: "salary" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181028141152_update_payroll_details_to_version_2.rb: -------------------------------------------------------------------------------- 1 | class UpdatePayrollDetailsToVersion2 < ActiveRecord::Migration[5.2] 2 | def change 3 | update_view :payroll_details, version: 2, revert_to_version: 1 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20200703065915_add_detail_to_statement.rb: -------------------------------------------------------------------------------- 1 | class AddDetailToStatement < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :statements, :gain, :jsonb 4 | add_column :statements, :loss, :jsonb 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "test_helper" 3 | 4 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 5 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400] 6 | end 7 | -------------------------------------------------------------------------------- /app/decorators/payroll_decorator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module PayrollDecorator 3 | def payment_period 4 | "#{year}-#{sprintf('%02d', month)}" 5 | end 6 | 7 | def role 8 | salary.given_role 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20180412032053_add_bank_transfer_type_to_employees.rb: -------------------------------------------------------------------------------- 1 | class AddBankTransferTypeToEmployees < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :employees, :bank_transfer_type, :string, default: "salary" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180604064006_add_monthly_wage_adjustment_to_salary.rb: -------------------------------------------------------------------------------- 1 | class AddMonthlyWageAdjustmentToSalary < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :salaries, :monthly_wage_adjustment, :float, default: 1.0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/application.yml.sample: -------------------------------------------------------------------------------- 1 | company_bank_account: "1234567890123" 2 | email_sender: "test@example.com" 3 | statement_bcc: "bob@example.com, alice@example.com" 4 | company_tax_id: "" 5 | company_labor_insurance_id: "" 6 | company_health_insurance_id: "" 7 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /db/migrate/20181108055027_remove_dates_from_employee.rb: -------------------------------------------------------------------------------- 1 | class RemoveDatesFromEmployee < ActiveRecord::Migration[5.2] 2 | def change 3 | remove_column :employees, :start_date, :date 4 | remove_column :employees, :end_date, :date 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/views/terms/_form.haml: -------------------------------------------------------------------------------- 1 | %table.table 2 | %tr 3 | %th.col-xs-1= f.label "起始日期" 4 | %td.col-xs-2= f.input_field :start_date, type: :date 5 | %th.col-xs-1= f.label "結束日期" 6 | %td.col-xs-2= f.input_field :end_date, type: :date, include_blank: true 7 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /app/views/terms/new.haml: -------------------------------------------------------------------------------- 1 | %h1 建立合作期間資料 2 | 3 | = simple_form_for @term, url: employee_terms_path, method: :post do |f| 4 | = render "/employees/info", employee: @employee 5 | = render "form", f: f, employee: @employee 6 | 7 | .text-center 8 | = f.submit "送出" 9 | -------------------------------------------------------------------------------- /app/views/statements/_details.haml: -------------------------------------------------------------------------------- 1 | - details.each do |i| 2 | %tr 3 | %th.text-center= i[0].keys.first 4 | %td.text-right= number_with_delimiter i[0].values.first 5 | %th.text-center= i[1].keys.first 6 | %td.text-right= number_with_delimiter i[1].values.first 7 | -------------------------------------------------------------------------------- /app/views/terms/edit.haml: -------------------------------------------------------------------------------- 1 | %h1 修改合作期間資料 2 | 3 | = simple_form_for @term, url: employee_term_path(@employee), method: :put do |f| 4 | = render "/employees/info", employee: @employee 5 | = render "form", f: f, employee: @employee 6 | 7 | .text-center 8 | = f.submit "送出" 9 | -------------------------------------------------------------------------------- /app/views/payrolls/_festival_bonus_form.haml: -------------------------------------------------------------------------------- 1 | %h2 三節禮金 2 | %table.table 3 | %tr 4 | %th.col-sm-1= f.label "名目" 5 | %td= f.input_field :festival_type, collection: Payroll::FESTIVAL_BONUS 6 | %tr 7 | %th.col-sm-1= f.label "金額" 8 | %td= f.input_field :festival_bonus 9 | -------------------------------------------------------------------------------- /db/migrate/20181013135559_add_festival_to_payrolls.rb: -------------------------------------------------------------------------------- 1 | class AddFestivalToPayrolls < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :payrolls, :festival_bonus, :integer, default: 0 4 | add_column :payrolls, :festival_type, :string, default: nil 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.scss: -------------------------------------------------------------------------------- 1 | /* 2 | *= require_self 3 | */ 4 | @import "bootstrap-sprockets"; 5 | @import "bootstrap"; 6 | 7 | body { 8 | padding: 5px; 9 | } 10 | 11 | .navbar { 12 | margin-bottom: 15px; 13 | } 14 | 15 | .highlight { 16 | color: #f00; 17 | } 18 | -------------------------------------------------------------------------------- /db/migrate/20180413054837_add_insurance_to_salaries.rb: -------------------------------------------------------------------------------- 1 | class AddInsuranceToSalaries < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :salaries, :insured_for_health, :integer, default: 0 4 | add_column :salaries, :insured_for_labor, :integer, default: 0 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/views/kaminari/_page.html.haml: -------------------------------------------------------------------------------- 1 | - if page.current? 2 | %li.active 3 | = content_tag :a, page, remote: remote, rel: (page.next? ? 'next' : (page.prev? ? 'prev' : nil)) 4 | - else 5 | %li 6 | = link_to page, url, remote: remote, rel: (page.next? ? 'next' : (page.prev? ? 'prev' : nil)) 7 | -------------------------------------------------------------------------------- /app/views/payrolls/_initiate_link.haml: -------------------------------------------------------------------------------- 1 | %h3= init_button_text 2 | .initiate_link 3 | = link_to "月薪制", init_regulars_path(year: init_year, month: init_month), class: "btn btn-ms btn-primary" 4 | = link_to "兼職外包", parttimers_path(year: init_year, month: init_month), class: "btn btn-ms btn-primary" 5 | -------------------------------------------------------------------------------- /app/views/statements/index.haml: -------------------------------------------------------------------------------- 1 | %h1 發薪紀錄 2 | 3 | = search_form_for @query do |f| 4 | = f.search_field :year_eq 5 | = f.label "年" 6 | = f.search_field :month_eq 7 | = f.label "月" 8 | = f.submit "查詢" 9 | 10 | - if params[:query] 11 | = render "list", statements: @statements 12 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /app/views/payrolls/_extra_entry_fields.haml: -------------------------------------------------------------------------------- 1 | %tr.nested-fields 2 | %td= f.input_field :title 3 | %td= f.input_field :amount 4 | %td= f.input_field :income_type, collection: ExtraEntry::INCOME_TYPE 5 | %td= f.input_field :note 6 | %td= link_to_remove_association "移除", f, class: "btn btn-xs btn-danger" 7 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/views/layouts/pdf.pdf.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html 3 | %head 4 | %title Fortuna 5 | = stylesheet_link_tag "http://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css", media: "all" 6 | = wicked_pdf_stylesheet_link_tag controller_name 7 | %body{id: action_name, class: controller_name} 8 | = yield 9 | -------------------------------------------------------------------------------- /app/views/payrolls/index.haml: -------------------------------------------------------------------------------- 1 | %h1 薪資單產生器 2 | 3 | = search_form_for @query do |f| 4 | = f.search_field :year_eq 5 | = f.label "年" 6 | = f.search_field :month_eq 7 | = f.label "月" 8 | = f.submit "查詢" 9 | 10 | = render "initiate_link" 11 | 12 | - if params[:query] 13 | = render "list", payrolls: @result 14 | -------------------------------------------------------------------------------- /app/views/reports/_header.haml: -------------------------------------------------------------------------------- 1 | %h1 年度報表 2 | = form_tag(reports_path, method: :get) do |f| 3 | = select_tag :year, options_for_select(Report.years, (params[:year] || Date.today.year)) 4 | = label_tag "年度" 5 | = select_tag :report_type, options_for_select(Report::INCOME_TYPE, params[:report_type]) 6 | = submit_tag "送出" 7 | -------------------------------------------------------------------------------- /app/views/payrolls/_overtime_fields.haml: -------------------------------------------------------------------------------- 1 | %tr.nested-fields 2 | %td= f.input_field :date, default: Date.new(@payroll.year, @payroll.month, 1) 3 | %td= f.input_field :hours 4 | %td= f.input_field :rate, collection: Overtime::RATE, default: "weekday" 5 | %td= link_to_remove_association "移除", f, class: "btn btn-xs btn-danger" 6 | -------------------------------------------------------------------------------- /db/migrate/20181011084858_create_corrections.rb: -------------------------------------------------------------------------------- 1 | class CreateCorrections < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :corrections do |t| 4 | t.integer :statement_id 5 | t.integer :amount, default: 0 6 | t.string :description 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/views/salaries/new.haml: -------------------------------------------------------------------------------- 1 | %h1 建立薪資資料 2 | 3 | = simple_form_for @salary, url: employee_salaries_path, method: :post do |f| 4 | = render "/employees/info", employee: @employee 5 | = render "/terms/info", term: @term 6 | = render "form", f: f, employee: @employee, term: @term 7 | 8 | .text-center 9 | = f.submit "送出" 10 | -------------------------------------------------------------------------------- /app/views/statements/edit.haml: -------------------------------------------------------------------------------- 1 | %h1 報表校正 2 | 3 | = render template: @details[:template], details: @details 4 | 5 | = link_to "薪資明細設定", edit_payroll_path(@statement.payroll), class: "btn btn-sm btn-default" 6 | 7 | = simple_form_for @statement do |f| 8 | = render "correction_form", f: f 9 | .text-center 10 | = f.submit "送出" 11 | -------------------------------------------------------------------------------- /db/migrate/20180106155844_create_overtimes.rb: -------------------------------------------------------------------------------- 1 | class CreateOvertimes < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :overtimes do |t| 4 | t.date :date 5 | t.string :rate 6 | t.float :hours, default: 0 7 | 8 | t.integer :payroll_id 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/decorators/salary_decorator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module SalaryDecorator 3 | def wage_type 4 | monthly_wage.positive? ? "月薪" : "時薪" 5 | end 6 | 7 | def wage 8 | monthly_wage.positive? ? monthly_wage : hourly_wage 9 | end 10 | 11 | def split_status 12 | split? ? "拆單" : "不拆單" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/services/salary_service/destroy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module SalaryService 3 | class Destroy < Base 4 | def call 5 | salary.destroy 6 | sync_payroll_relations 7 | sync_statements 8 | end 9 | 10 | private 11 | 12 | def effective_date 13 | nil 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/views/terms/show.haml: -------------------------------------------------------------------------------- 1 | %h1 員工資料 2 | = render "/employees/info", employee: @term.employee 3 | = render "info", term: @term 4 | 5 | = link_to "修改合作期間資料", edit_employee_term_path(@employee), class: "btn btn-sm btn-default" 6 | = link_to "刪除合作期間資料", employee_term_path(@employee), method: :delete, data: { confirm: "確認刪除?" }, class: "btn btn-sm btn-danger" 7 | -------------------------------------------------------------------------------- /app/views/salaries/edit.haml: -------------------------------------------------------------------------------- 1 | %h1 修改薪資資料 2 | 3 | = simple_form_for @salary, url: employee_salary_path(@employee), method: :put do |f| 4 | = render "/employees/info", employee: @employee 5 | = render "terms/info", term: @salary.term 6 | = render "form", f: f, employee: @employee, term: @salary.term 7 | 8 | .text-center 9 | = f.submit "送出" 10 | -------------------------------------------------------------------------------- /lib/templates/haml/scaffold/_form.html.haml: -------------------------------------------------------------------------------- 1 | = simple_form_for(@<%= singular_table_name %>) do |f| 2 | = f.error_notification 3 | 4 | .form-inputs 5 | <%- attributes.each do |attribute| -%> 6 | = f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> 7 | <%- end -%> 8 | 9 | .form-actions 10 | = f.button :submit 11 | -------------------------------------------------------------------------------- /app/views/salaries/show.haml: -------------------------------------------------------------------------------- 1 | %h1 員工資料 2 | = render "/employees/info", employee: @salary.employee 3 | = render "info", salary: @salary 4 | 5 | = link_to "修改薪資資料", edit_employee_salary_path(@employee), class: "btn btn-sm btn-default" 6 | = link_to "刪除薪資資料", employee_salary_path(@employee), method: :delete, data: { confirm: "確認刪除?" }, class: "btn btn-sm btn-danger" 7 | -------------------------------------------------------------------------------- /db/migrate/20181106120752_create_terms.rb: -------------------------------------------------------------------------------- 1 | class CreateTerms < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :terms do |t| 4 | t.date :start_date, default: nil, index: true 5 | t.date :end_date, default: nil, index: true 6 | 7 | t.integer :employee_id, index: true 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/factories/payroll.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :payroll do 5 | transient do 6 | build_statement_immediatedly { false } 7 | end 8 | 9 | after :create do |payroll, ev| 10 | StatementService::Builder.call(payroll) if ev.build_statement_immediatedly 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | VENDOR_PATH = File.expand_path('..', __dir__) 3 | Dir.chdir(VENDOR_PATH) do 4 | begin 5 | exec "yarnpkg #{ARGV.join(" ")}" 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/payrolls/_basic_form.haml: -------------------------------------------------------------------------------- 1 | %h2 基本項目 2 | %table.table 3 | %tr 4 | %th.col-sm-1= f.label "工作時數" 5 | %td= f.input_field :parttime_hours 6 | %th= f.label "扣薪事假時數" 7 | %td= f.input_field :leavetime_hours 8 | %tr 9 | %th= f.label "扣薪病假時數" 10 | %td= f.input_field :sicktime_hours 11 | %th= f.label "特休折換時數" 12 | %td= f.input_field :vacation_refund_hours 13 | -------------------------------------------------------------------------------- /app/views/payrolls/_list.haml: -------------------------------------------------------------------------------- 1 | %table.table 2 | %tr 3 | %th # 4 | %th 員工 5 | %th 身份 6 | %th 計算月份 7 | %th 操作 8 | - payrolls.each do |payroll| 9 | %tr 10 | %td= payroll.id 11 | %td= payroll.employee_name 12 | %td= payroll.role 13 | %td= payroll.payment_period 14 | %td= render "statements", payroll: payroll 15 | 16 | = paginate payrolls 17 | -------------------------------------------------------------------------------- /config/holidays/2015.yml: -------------------------------------------------------------------------------- 1 | holidays: 2 | 1: 3 | - 1 # 元旦 4 | - 2 # 元旦彈性 5 | 2: 6 | - 18 # 除夕 7 | - 19 # 春節 8 | - 20 # 春節 9 | - 23 # 春節 10 | - 27 # 228 補假 11 | 4: 12 | - 3 # 兒童節補假 13 | - 6 # 清明補假 14 | 5: 15 | - 1 # 勞動節 16 | 6: 17 | - 19 # 端午補假 18 | 9: 19 | - 28 # 中秋補假 20 | 10: 21 | - 9 # 國慶補假 22 | makeup_days: 23 | -------------------------------------------------------------------------------- /config/holidays/2025.yml: -------------------------------------------------------------------------------- 1 | holidays: 2 | 1: 3 | - 1 # 元旦 4 | - 27 # 春節 5 | - 28 # 春節 6 | - 29 # 春節 7 | - 30 # 春節 8 | - 31 # 春節 9 | 10 | 2: 11 | - 28 # 228 12 | 4: 13 | - 3 # 清明 14 | - 4 # 清明 15 | 5: 16 | - 1 # 勞動節 17 | - 30 # 端午節 18 | 10: 19 | - 6 # 中秋節 20 | - 10 # 國慶日 21 | makeup_days: 22 | 2: 23 | - 8 # 春節補班 24 | -------------------------------------------------------------------------------- /db/migrate/20180114122028_create_statements.rb: -------------------------------------------------------------------------------- 1 | class CreateStatements < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :statements do |t| 4 | t.integer :amount, default: 0 5 | t.integer :year 6 | t.integer :month 7 | t.integer :splits, default: nil, array: true 8 | 9 | t.integer :payroll_id 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class ApplicationController < ActionController::Base 3 | protect_from_forgery with: :exception 4 | 5 | def store_location 6 | session[:return_to] = request.fullpath if request.get? 7 | end 8 | 9 | private 10 | 11 | def not_found 12 | raise ActionController::RoutingError, "Not Found" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /config/holidays/2022.yml: -------------------------------------------------------------------------------- 1 | holidays: 2 | 1: 3 | - 31 # 除夕 4 | 5 | 2: 6 | - 1 # 春節 7 | - 2 # 春節 8 | - 3 # 春節 9 | - 4 # 春節 10 | - 28 # 228 11 | 4: 12 | - 4 # 兒童節 13 | - 5 # 清明 14 | 5: 15 | - 2 # 勞動節補假 16 | 6: 17 | - 3 # 端午節 18 | 9: 19 | - 9 # 中秋節補假 20 | 10: 21 | - 10 # 國慶日 22 | makeup_days: 23 | 1: 24 | - 22 # 春節補班 25 | -------------------------------------------------------------------------------- /config/initializers/kaminari_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | Kaminari.configure do |config| 3 | config.default_per_page = 50 4 | # config.max_per_page = nil 5 | # config.window = 4 6 | # config.outer_window = 0 7 | # config.left = 0 8 | # config.right = 0 9 | # config.page_method_name = :page 10 | # config.param_name = :page 11 | # config.params_on_first_page = false 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20180107080943_create_extra_entries.rb: -------------------------------------------------------------------------------- 1 | class CreateExtraEntries < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :extra_entries do |t| 4 | t.string :title 5 | t.integer :amount, default: 0 6 | t.boolean :taxable, default: false 7 | t.string :note, null: true 8 | 9 | t.integer :payroll_id 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/payrolls/_statements.haml: -------------------------------------------------------------------------------- 1 | = link_to "員工資料", employee_path(payroll.employee), class: "btn btn-xs btn-default" 2 | - if payroll.statement 3 | = link_to "修改明細", edit_payroll_path(payroll), class: "btn btn-xs btn-default" 4 | = link_to "報表校正", edit_statement_path(payroll.statement), class: "btn btn-xs btn-warning" 5 | - else 6 | = link_to "設定明細", edit_payroll_path(payroll), class: "btn btn-xs btn-warning" 7 | -------------------------------------------------------------------------------- /config/holidays/2024.yml: -------------------------------------------------------------------------------- 1 | holidays: 2 | 1: 3 | - 1 # 元旦 4 | 5 | 2: 6 | - 8 # 春節 7 | - 9 # 春節 8 | - 12 # 春節 9 | - 13 # 春節 10 | - 14 # 春節 11 | - 28 # 228 12 | 4: 13 | - 4 # 清明 14 | - 5 # 清明 15 | 5: 16 | - 1 # 勞動節 17 | 6: 18 | - 10 # 端午節 19 | 9: 20 | - 17 # 中秋節 21 | 10: 22 | - 10 # 國慶日 23 | makeup_days: 24 | 2: 25 | - 17 # 春節補班 26 | -------------------------------------------------------------------------------- /app/services/salary_service/update.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module SalaryService 3 | class Update < Base 4 | def call 5 | salary.update(params) 6 | sync_payroll_relations if effective_date_change? 7 | sync_statements 8 | end 9 | 10 | private 11 | 12 | def effective_date_change? 13 | salary.effective_date_before_last_save != effective_date 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/assets/javascripts/cable.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command. 3 | // 4 | //= require action_cable 5 | //= require_self 6 | //= require_tree ./channels 7 | 8 | (function() { 9 | this.App || (this.App = {}); 10 | 11 | App.cable = ActionCable.createConsumer(); 12 | 13 | }).call(this); 14 | -------------------------------------------------------------------------------- /app/services/income_tax_service/regular_employee.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module IncomeTaxService 3 | class RegularEmployee 4 | include Callable 5 | 6 | attr_reader :payroll 7 | 8 | def initialize(payroll) 9 | @payroll = payroll 10 | end 11 | 12 | def call 13 | IncomeTaxService::InsurancedSalary.call(payroll) + IncomeTaxService::IrregularIncome.call(payroll) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/extra_entry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class ExtraEntry < ApplicationRecord 3 | belongs_to :payroll 4 | 5 | INCOME_TYPE = { "薪資": "salary", "補助": "subsidy", "獎金": "bonus" }.freeze 6 | 7 | scope :yearly_subsidy_report, ->(year) { 8 | includes(:payroll, payroll: :employee) 9 | .where("amount > 0 AND income_type = ?", :subsidy) 10 | .joins(:payroll) 11 | .where(payrolls: { year: year }) 12 | } 13 | end 14 | -------------------------------------------------------------------------------- /test/factories/employee.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "taiwanese_id_validator/twid_generator" 4 | 5 | FactoryBot.define do 6 | factory :employee do 7 | name do 8 | Faker::Name.name 9 | end 10 | company_email do 11 | Faker::Internet.email 12 | end 13 | personal_email do 14 | Faker::Internet.email 15 | end 16 | id_number do 17 | TwidGenerator.generate 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/assets/stylesheets/payrolls.scss: -------------------------------------------------------------------------------- 1 | .payrolls#show { 2 | .table { 3 | margin-bottom: 0; 4 | } 5 | 6 | .row { 7 | margin-right: 0; 8 | margin-left: 0; 9 | 10 | div.col-xs-3, div.col-xs-6 { 11 | padding-right: 0; 12 | padding-left: 0; 13 | } 14 | } 15 | } 16 | .payrolls#index { 17 | #payroll_search { 18 | margin-bottom: 20px; 19 | } 20 | 21 | .initiate_link { 22 | margin: 10px 0 20px 0; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /test/services/payroll_init_service_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "test_helper" 3 | 4 | class PayrollInitServiceTest < ActiveSupport::TestCase 5 | def test_initiate_payroll 6 | employee = build(:employee) 7 | salary = build(:salary, employee: employee) 8 | 9 | StatementService::Builder.any_instance.expects(:call).returns(true) 10 | 11 | assert PayrollInitService.call(2018, 1, employee.id, salary.id) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/mailers/statement_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class StatementMailer < ApplicationMailer 4 | def notify_email(statement) 5 | builder = StatementPDFBuilder.new(statement) 6 | attachments[builder.filename] = builder.encrypted_pdf 7 | mail( 8 | to: statement.employee.email, 9 | bcc: ENV["statement_bcc"].split(","), 10 | subject: builder.email_subject 11 | ) 12 | builder.delete_files 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/services/format_service/extra_income.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module FormatService 3 | class ExtraIncome 4 | include Callable 5 | 6 | attr_reader :payroll, :hash 7 | 8 | def initialize(payroll) 9 | @payroll = payroll 10 | @hash = {} 11 | end 12 | 13 | def call 14 | payroll.extra_entries.map { |entry| hash[entry.title] = entry.amount if entry.amount.positive? } 15 | hash 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /db/views/reports_v01.sql: -------------------------------------------------------------------------------- 1 | SELECT DISTINCT 2 | employees.id AS employee_id, 3 | payrolls.id AS payroll_id, 4 | statements.id AS statement_id, 5 | employees.name, 6 | payrolls.year, 7 | payrolls.month, 8 | salaries.tax_code, 9 | statements.amount 10 | FROM employees 11 | INNER JOIN payrolls ON employees.id = payrolls.employee_id 12 | INNER JOIN salaries ON salaries.id = payrolls.salary_id 13 | INNER JOIN statements ON payrolls.id = statements.payroll_id 14 | -------------------------------------------------------------------------------- /app/services/format_service/extra_deductions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module FormatService 3 | class ExtraDeductions 4 | include Callable 5 | 6 | attr_reader :payroll, :hash 7 | 8 | def initialize(payroll) 9 | @payroll = payroll 10 | @hash = {} 11 | end 12 | 13 | def call 14 | payroll.extra_entries.map { |entry| hash[entry.title] = entry.amount.abs if entry.amount.negative? } 15 | hash 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/views/kaminari/_paginator.html.haml: -------------------------------------------------------------------------------- 1 | = paginator.render do 2 | %ul.pagination 3 | = first_page_tag unless current_page.first? 4 | = prev_page_tag unless current_page.first? 5 | - each_page do |page| 6 | - if page.left_outer? || page.right_outer? || page.inside_window? 7 | = page_tag page 8 | - elsif !page.was_truncated? 9 | = gap_tag 10 | = next_page_tag unless current_page.last? 11 | = last_page_tag unless current_page.last? 12 | -------------------------------------------------------------------------------- /config/holidays/2018.yml: -------------------------------------------------------------------------------- 1 | holidays: 2 | 1: 3 | - 1 # 元旦 4 | 2: 5 | - 15 # 除夕 6 | - 16 # 春節 7 | - 19 # 春節 8 | - 20 # 春節 9 | - 28 # 228 10 | 4: 11 | - 4 # 兒童節 12 | - 5 # 清明 13 | - 6 # 清明彈性 14 | 5: 15 | - 1 # 勞動節 16 | 6: 17 | - 18 # 端午節 18 | 9: 19 | - 24 # 中秋節 20 | 10: 21 | - 10 # 國慶日 22 | 12: 23 | - 31 # 元旦彈性 24 | makeup_days: 25 | 3: 26 | - 31 # 清明補班 27 | 12: 28 | - 22 # 元旦補班 29 | -------------------------------------------------------------------------------- /app/services/income_tax_service/insuranced_salary.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module IncomeTaxService 3 | class InsurancedSalary 4 | include Callable 5 | 6 | attr_reader :payroll 7 | 8 | def initialize(payroll) 9 | @payroll = payroll 10 | end 11 | 12 | def call 13 | insuranced_salary_tax 14 | end 15 | 16 | private 17 | 18 | def insuranced_salary_tax 19 | payroll.salary.fixed_income_tax 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /db/migrate/20180105170858_create_payrolls.rb: -------------------------------------------------------------------------------- 1 | class CreatePayrolls < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :payrolls do |t| 4 | t.integer :year 5 | t.integer :month 6 | t.float :parttime_hours, default: 0 7 | t.float :leavetime_hours, default: 0 8 | t.float :sicktime_hours, default: 0 9 | t.float :vacation_refund_hours, default: 0 10 | 11 | t.integer :employee_id 12 | t.timestamps 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/views/salary_trackers_v01.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | employees.id AS employee_id, 3 | terms.id AS term_id, 4 | salaries.id AS salary_id, 5 | employees.name AS name, 6 | terms.start_date AS term_start, 7 | terms.end_date AS term_end, 8 | salaries.role AS role, 9 | salaries.effective_date AS salary_start 10 | FROM employees 11 | INNER JOIN terms ON employees.id = terms.employee_id 12 | INNER JOIN salaries ON employees.id = salaries.employee_id 13 | WHERE salaries.term_id = terms.id 14 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "simplecov" 3 | SimpleCov.start "rails" do 4 | add_filter "app/controllers" 5 | end 6 | 7 | require File.expand_path("../config/environment", __dir__) 8 | require "rails/test_help" 9 | require "minitest/unit" 10 | require "mocha/minitest" 11 | 12 | class ActiveSupport::TestCase 13 | include FactoryBot::Syntax::Methods 14 | end 15 | 16 | DatabaseRewinder.strategy = :transaction 17 | DatabaseRewinder.clean_with(:truncation) 18 | -------------------------------------------------------------------------------- /app/views/statements/_correction_form.haml: -------------------------------------------------------------------------------- 1 | %h2 校正項目 2 | %table.table 3 | %tr 4 | %th.col-sm-2 金額 5 | %th.col-sm-4 說明 6 | %th.col-sm-4 操作 7 | %tbody#correction_items 8 | = f.simple_fields_for :corrections do |correction| 9 | = render "correction_fields", f: correction 10 | 11 | = link_to_add_association "新增校正項目", f, :corrections, class: "btn btn-sm btn-primary", data: { "association-insertion-node": "tbody#correction_items", "association-insertion-method": "append" } 12 | -------------------------------------------------------------------------------- /db/migrate/20180103063747_create_employees.rb: -------------------------------------------------------------------------------- 1 | class CreateEmployees < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :employees do |t| 4 | t.string :name 5 | t.string :company_email 6 | t.string :personal_email 7 | t.string :id_number 8 | t.string :residence_address 9 | t.date :birthday 10 | t.string :bank_account 11 | t.date :start_date 12 | t.date :end_date 13 | 14 | t.timestamps 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/decorators/employee_decorator_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "test_helper" 3 | 4 | class EmployeeDecoratorTest < ActiveSupport::TestCase 5 | def test_role 6 | assert_equal "正職", decorated_subject.role 7 | end 8 | 9 | private 10 | 11 | def decorated_subject 12 | ActiveDecorator::Decorator.instance.decorate( 13 | build(:employee) { |employee| create(:salary, role: "regular", employee: employee, term: build(:term)) } 14 | ) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/services/salary_service/create.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module SalaryService 3 | class Create < Base 4 | def call 5 | salary.save 6 | sync_payroll_relations if effective_date_backtrack? 7 | sync_statements 8 | end 9 | 10 | private 11 | 12 | def effective_date_backtrack? 13 | recent_salary.effective_date > effective_date 14 | end 15 | 16 | def recent_salary 17 | Salary.recent_for(salary.employee) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /db/views/reports_v02.sql: -------------------------------------------------------------------------------- 1 | SELECT DISTINCT 2 | employees.id AS employee_id, 3 | payrolls.id AS payroll_id, 4 | statements.id AS statement_id, 5 | employees.name, 6 | payrolls.year, 7 | payrolls.month, 8 | salaries.tax_code, 9 | statements.amount, 10 | statements.irregular_income 11 | FROM employees 12 | INNER JOIN payrolls ON employees.id = payrolls.employee_id 13 | INNER JOIN salaries ON salaries.id = payrolls.salary_id 14 | INNER JOIN statements ON payrolls.id = statements.payroll_id 15 | -------------------------------------------------------------------------------- /app/views/payrolls/_overtime_form.haml: -------------------------------------------------------------------------------- 1 | %h2 加班紀錄 2 | %table.table 3 | %tr 4 | %th.col-sm-2 加班日期 5 | %th.col-sm-2 加班時數 6 | %th.col-sm-2 適用費率 7 | %th.col-sm-4 操作 8 | %tbody#overtime_items 9 | = f.simple_fields_for :overtimes do |overtime| 10 | = render "overtime_fields", f: overtime 11 | 12 | = link_to_add_association "新增加班紀錄", f, :overtimes, class: "btn btn-sm btn-primary", data: { "association-insertion-node": "tbody#overtime_items", "association-insertion-method": "append" } 13 | -------------------------------------------------------------------------------- /app/views/payrolls/parttimers.haml: -------------------------------------------------------------------------------- 1 | %h1 薪資單產生器 2 | 3 | = form_for :parttimers do |f| 4 | %table.table 5 | %tr 6 | %th.col-sm-1 # 7 | %th.col-sm-1 姓名 8 | %th.col-sm-1 選取 9 | %th{width: "100%"} 身份 10 | - @result.each do |row| 11 | %tr 12 | %td= row.employee_id 13 | %td= row.name 14 | %td 15 | %input{type: "checkbox", name: "parttimers[]", value: "#{row.employee_id}, #{row.salary_id}"} 16 | %td= row.given_role 17 | %p= f.submit 18 | -------------------------------------------------------------------------------- /config/holidays/2017.yml: -------------------------------------------------------------------------------- 1 | holidays: 2 | 1: 3 | - 2 # 元旦補假 4 | - 27 # 除夕 5 | - 30 # 春節 6 | - 31 # 春節補假 7 | 2: 8 | - 1 # 春節補假 9 | - 27 # 228 彈性 10 | - 28 # 228 11 | 4: 12 | - 3 # 兒童節 13 | - 4 # 清明 14 | 5: 15 | - 1 # 勞動節 16 | - 28 # 端午彈性 17 | - 29 # 端午節 18 | 10: 19 | - 4 # 中秋節 20 | - 9 # 國慶彈性 21 | - 10 # 國慶日 22 | makeup_days: 23 | 2: 24 | - 18 # 228 補班 25 | 6: 26 | - 3 # 端午補班 27 | 9: 28 | - 30 # 國慶補班 29 | -------------------------------------------------------------------------------- /config/holidays/2020.yml: -------------------------------------------------------------------------------- 1 | holidays: 2 | 1: 3 | - 1 # 元旦 4 | - 23 # 除夕彈性 5 | - 24 # 除夕 6 | - 27 # 春節 7 | - 28 # 春節 8 | - 29 # 春節 9 | 2: 10 | - 28 # 228 11 | 4: 12 | - 2 # 兒童節 13 | - 3 # 清明 14 | 5: 15 | - 1 # 勞動節 16 | 6: 17 | - 25 # 端午節 18 | - 26 # 端午彈性 19 | 10: 20 | - 1 # 中秋節 21 | - 2 # 中秋彈性 22 | - 9 # 國慶日 23 | makeup_days: 24 | 2: 25 | - 15 # 春節補班 26 | 6: 27 | - 20 # 端午補班 28 | 9: 29 | - 26 # 中秋補班 30 | -------------------------------------------------------------------------------- /app/lib/collection_translatable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 使用方式參考 Salary::ROLE 3 | module CollectionTranslatable 4 | def method_missing(method_name) 5 | if (match = method_name.id2name.match(%r{^given_(\w+)})) 6 | "#{model_name}::#{match[1].upcase}".constantize.key(self.send(match[1])).to_s 7 | else 8 | super 9 | end 10 | end 11 | 12 | def respond_to_missing?(method_name, include_private = false) 13 | method_name.to_s.start_with?("given_") || super 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config/holidays/2021.yml: -------------------------------------------------------------------------------- 1 | holidays: 2 | 1: 3 | - 1 # 元旦 4 | 5 | 2: 6 | - 10 # 除夕彈性 7 | - 11 # 除夕 8 | - 12 # 春節 9 | - 15 # 春節 10 | - 16 # 春節 11 | 3: 12 | - 1 # 228 補假 13 | 4: 14 | - 2 # 兒童節補假 15 | - 5 # 清明補假 16 | 5: 17 | - 1 # 勞動節 18 | 6: 19 | - 14 # 端午節 20 | 9: 21 | - 20 # 中秋節彈性 22 | - 21 # 中秋節 23 | 10: 24 | - 11 # 國慶日補假 25 | 12: 26 | - 31 # 元旦補假 27 | makeup_days: 28 | 2: 29 | - 20 # 春節補班 30 | 9: 31 | - 11 # 中秋補班 32 | -------------------------------------------------------------------------------- /config/holidays/2016.yml: -------------------------------------------------------------------------------- 1 | holidays: 2 | 1: 3 | - 1 # 元旦 4 | 2: 5 | - 8 # 春節 6 | - 9 # 春節 7 | - 10 # 春節 8 | - 11 # 春節 9 | - 12 # 春節彈性 10 | - 29 # 228 補假 11 | 4: 12 | - 4 # 兒童節 13 | - 5 # 清明補假 14 | 5: 15 | - 2 # 勞動節補假 16 | 6: 17 | - 9 # 端午節 18 | - 10 # 端午彈性 19 | 9: 20 | - 15 # 中秋節 21 | - 16 # 中秋彈性 22 | 10: 23 | - 10 # 國慶日 24 | makeup_days: 25 | 1: 26 | - 30 # 春節補班 27 | 6: 28 | - 4 # 端午補班 29 | 9: 30 | - 10 # 中秋補班 31 | -------------------------------------------------------------------------------- /app/views/payrolls/edit.haml: -------------------------------------------------------------------------------- 1 | %h1 #{@payroll.payment_period} 薪資單明細設定 2 | 3 | = render "/employees/info", employee: @payroll.employee 4 | = render "/salaries/info", salary: @payroll.salary 5 | 6 | = link_to "報表校正", edit_statement_path(@payroll.statement), class: "btn btn-sm btn-warning" 7 | 8 | = simple_form_for @payroll do |f| 9 | = render "basic_form", f: f 10 | = render "festival_bonus_form", f: f 11 | = render "overtime_form", f: f 12 | = render "extra_entry_form", f: f 13 | 14 | .text-center 15 | = f.submit "送出" 16 | -------------------------------------------------------------------------------- /config/holidays/2019.yml: -------------------------------------------------------------------------------- 1 | holidays: 2 | 1: 3 | - 1 # 元旦 4 | 2: 5 | - 4 # 除夕 6 | - 5 # 春節 7 | - 6 # 春節 8 | - 7 # 春節 9 | - 8 # 春節 10 | - 28 # 228 11 | 3: 12 | - 1 # 228彈性 13 | 4: 14 | - 4 # 兒童節 15 | - 5 # 清明 16 | 5: 17 | - 1 # 勞動節 18 | 6: 19 | - 7 # 端午節 20 | 9: 21 | - 13 # 中秋節 22 | 10: 23 | - 10 # 國慶日 24 | - 11 # 國慶日彈性 25 | makeup_days: 26 | 1: 27 | - 19 # 春節補班 28 | 2: 29 | - 23 # 228補班 30 | 10: 31 | - 5 # 國慶日補班 32 | -------------------------------------------------------------------------------- /app/services/calculation_service/sicktime.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CalculationService 3 | class Sicktime 4 | include Callable 5 | 6 | attr_reader :hours, :salary 7 | 8 | def initialize(payroll) 9 | @hours = payroll.sicktime_hours 10 | @salary = payroll.salary 11 | end 12 | 13 | def call 14 | (hours * hourly_rate * 0.5).to_i 15 | end 16 | 17 | private 18 | 19 | def hourly_rate 20 | (salary.income_with_subsidies / 30 / 8.0).ceil 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /.reek.yml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - config 4 | - lib 5 | - test 6 | - db 7 | 8 | detectors: 9 | IrresponsibleModule: 10 | enabled: false 11 | NestedIterators: 12 | max_allowed_nesting: 2 13 | DuplicateMethodCall: 14 | max_calls: 2 15 | UtilityFunction: 16 | enabled: false 17 | BooleanParameter: 18 | exclude: 19 | - "CollectionTranslatable#respond_to_missing?" # monkey patch, can't fix 20 | 21 | directories: 22 | "app/controllers": 23 | InstanceVariableAssumption: 24 | enabled: false 25 | -------------------------------------------------------------------------------- /app/services/health_insurance_service/supplement_premium_rate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module HealthInsuranceService 3 | class SupplementPremiumRate 4 | include Callable 5 | 6 | attr_reader :date 7 | 8 | def initialize(year, month) 9 | @date = Date.new(year, month, 1) 10 | end 11 | 12 | def call 13 | if date >= Date.new(2021, 1, 1) 14 | 0.0211 15 | elsif date >= Date.new(2016, 1, 1) 16 | 0.0191 17 | else 18 | 0.02 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/services/calculation_service/vacation_refund.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CalculationService 3 | class VacationRefund 4 | include Callable 5 | 6 | attr_reader :hours, :salary 7 | 8 | def initialize(payroll) 9 | @hours = payroll.vacation_refund_hours 10 | @salary = payroll.salary 11 | end 12 | 13 | def call 14 | (hourly_rate * hours).to_i 15 | end 16 | 17 | private 18 | 19 | def hourly_rate 20 | (salary.income_with_subsidies / 30 / 8.0).ceil 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/views/payrolls/_extra_entry_form.haml: -------------------------------------------------------------------------------- 1 | %h2 自訂項目 2 | %table.table 3 | %tr 4 | %th.col-sm-1 項目名稱 5 | %th.col-sm-1 金額 6 | %th.col-sm-1 稅別 7 | %th.col-sm-2 備註 8 | %th.col-sm-4 操作 9 | %tbody#extra_entry_items 10 | = f.simple_fields_for :extra_entries do |extra_entry| 11 | = render "extra_entry_fields", f: extra_entry 12 | 13 | = link_to_add_association "新增自訂項目", f, :extra_entries, class: "btn btn-sm btn-primary", data: { "association-insertion-node": "tbody#extra_entry_items", "association-insertion-method": "append" } 14 | -------------------------------------------------------------------------------- /db/migrate/20181014044335_add_indexes.rb: -------------------------------------------------------------------------------- 1 | class AddIndexes < ActiveRecord::Migration[5.2] 2 | def change 3 | add_index :employees, :start_date 4 | add_index :employees, :end_date 5 | add_index :payrolls, :salary_id 6 | add_index :payrolls, :employee_id 7 | add_index :salaries, :employee_id 8 | add_index :salaries, :effective_date 9 | add_index :statements, :payroll_id 10 | add_index :overtimes, :payroll_id 11 | add_index :extra_entries, :payroll_id 12 | add_index :corrections, :statement_id 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/models/overtime_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "test_helper" 3 | 4 | class OvertimeTest < ActiveSupport::TestCase 5 | setup do 6 | create(:overtime, date: "2017-02-01", payroll: build(:payroll)) 7 | create(:overtime, date: "2018-03-15", payroll: build(:payroll)) 8 | create(:overtime, date: "2018-04-15", payroll: build(:payroll)) 9 | end 10 | 11 | def test_yearly_report 12 | assert_equal 1, Overtime.yearly_report(2017).count 13 | assert_equal 2, Overtime.yearly_report(2018).count 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/services/salary_service/sync_payroll_relations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module SalaryService 3 | class SyncPayrollRelations 4 | include Callable 5 | 6 | attr_reader :payroll 7 | 8 | def initialize(payroll) 9 | @payroll = payroll 10 | end 11 | 12 | def call 13 | payroll.update(salary: matched_salary) if matched_salary != payroll.salary 14 | end 15 | 16 | private 17 | 18 | def matched_salary 19 | Salary.find(SalaryTracker.salary_by_payroll(payroll: payroll)) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/services/format_service/festival_bonus.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module FormatService 3 | class FestivalBonus 4 | include Callable 5 | 6 | attr_reader :payroll, :hash 7 | 8 | def initialize(payroll) 9 | @payroll = payroll 10 | @hash = {} 11 | end 12 | 13 | def call 14 | hash[title] = payroll.festival_bonus if payroll.festival_bonus.positive? 15 | hash 16 | end 17 | 18 | private 19 | 20 | def title 21 | Payroll::FESTIVAL_BONUS.key(payroll.festival_type) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/models/overtime.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class Overtime < ApplicationRecord 3 | belongs_to :payroll 4 | delegate :year, :month, to: :payroll, prefix: true 5 | 6 | has_one :employee, through: :payroll 7 | delegate :id, :name, to: :employee, prefix: true 8 | 9 | RATE = { "平日": "weekday", "休息日": "weekend", "休假日": "holiday", "例假日": "offday" }.freeze 10 | 11 | scope :yearly_report, ->(year) { 12 | includes(:payroll, :employee, payroll: :salary) 13 | .where("date BETWEEN ? AND ?", Date.new(year, 1, 1), Date.new(year, 12, -1)) 14 | } 15 | end 16 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/services/payroll_init_regulars_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class PayrollInitRegularsService 3 | include Callable 4 | 5 | attr_reader :year, :month 6 | 7 | def initialize(year, month) 8 | @year = year.to_i 9 | @month = month.to_i 10 | end 11 | 12 | def call 13 | regulars.map { |tracker| PayrollInitService.call(year, month, tracker.employee_id, tracker.salary_id) } 14 | end 15 | 16 | private 17 | 18 | def regulars 19 | SalaryTracker.on_payroll(year: year, month: month).by_role(role: %w[boss regular contractor]) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/services/income_tax_service/insuranced_salary_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "test_helper" 3 | 4 | module IncomeTaxService 5 | class InsurancedSalaryTest 6 | def test_has_insuranced_salary_tax 7 | assert_equal 3000, IncomeTaxService::InsurancedSalary.call(subject) 8 | end 9 | 10 | private 11 | 12 | def subject 13 | build( 14 | :payroll, 15 | year: 2018, 16 | month: 5, 17 | salary: build(:salary, fixed_income_tax: 3000), 18 | employee: build(:employee) 19 | ) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/views/reports/irregular.haml: -------------------------------------------------------------------------------- 1 | = render "header" 2 | = render "title" 3 | 4 | %p= link_to "匯出 CSV", irregular_reports_path(params[:year], format: :csv), class: "btn btn-m btn-primary" 5 | 6 | - if @report.any? 7 | %table.table 8 | %tr 9 | %th.col-sm-2 姓名 10 | %th.col-sm-2 計算月份 11 | %th.col-sm-2 項目 12 | %th 金額 13 | - @report.each do |row| 14 | %tr 15 | %td= link_to row[:name], employee_path(row[:employee_id]) 16 | %td= link_to row[:period], edit_payroll_path(row[:payroll_id]) 17 | %td= row[:description] 18 | %td= number_with_delimiter(row[:amount]) 19 | -------------------------------------------------------------------------------- /db/views/payroll_details_v01.sql: -------------------------------------------------------------------------------- 1 | SELECT DISTINCT 2 | employees.id AS employee_id, 3 | payrolls.id AS payroll_id, 4 | payrolls.year, 5 | payrolls.month, 6 | salaries.tax_code, 7 | salaries.monthly_wage, 8 | salaries.insured_for_health, 9 | statements.splits, 10 | statements.amount, 11 | statements.subsidy_income, 12 | statements.bonus_income, 13 | employees.owner 14 | FROM employees 15 | INNER JOIN payrolls ON employees.id = payrolls.employee_id 16 | INNER JOIN salaries ON salaries.id = payrolls.salary_id 17 | INNER JOIN statements ON payrolls.id = statements.payroll_id 18 | WHERE employees.b2b = false 19 | -------------------------------------------------------------------------------- /db/views/payroll_details_v02.sql: -------------------------------------------------------------------------------- 1 | SELECT DISTINCT 2 | employees.id AS employee_id, 3 | payrolls.id AS payroll_id, 4 | payrolls.year, 5 | payrolls.month, 6 | salaries.tax_code, 7 | salaries.monthly_wage, 8 | salaries.insured_for_health, 9 | statements.splits, 10 | statements.amount, 11 | statements.subsidy_income, 12 | statements.excess_income, 13 | employees.owner 14 | FROM employees 15 | INNER JOIN payrolls ON employees.id = payrolls.employee_id 16 | INNER JOIN salaries ON salaries.id = payrolls.salary_id 17 | INNER JOIN statements ON payrolls.id = statements.payroll_id 18 | WHERE employees.b2b = false 19 | -------------------------------------------------------------------------------- /test/models/term_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "test_helper" 3 | 4 | class TermTest < ActiveSupport::TestCase 5 | def test_scope_ordered 6 | prepare_scope_ordered 7 | 8 | assert Term.ordered.each_cons(2).all? do |first, second| 9 | first.start_date >= second.start_date 10 | end 11 | end 12 | 13 | private 14 | 15 | def prepare_scope_ordered 16 | create(:term, start_date: "2018-05-05", employee: build(:employee)) 17 | create(:term, start_date: "2018-10-01", employee: build(:employee)) 18 | create(:term, start_date: "2018-01-23", employee: build(:employee)) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/views/employees/show.haml: -------------------------------------------------------------------------------- 1 | %h1 員工資料 2 | = render "info", employee: @employee 3 | = link_to "修改員工資料", edit_employee_path(@employee), class: "btn btn-default btn-sm" 4 | = link_to "刪除員工資料", employee_path(@employee), method: :delete, data: { confirm: "確定刪除?" }, class: "btn btn-danger btn-sm" 5 | 6 | %h3 合作期間 7 | = render "terms", terms: @employee.terms.ordered 8 | %p= link_to "新增合作期間", new_employee_term_path(@employee), class: "btn btn-sm btn-primary" 9 | 10 | %h2 薪資紀錄 11 | - if @employee.salaries.any? 12 | = render "salaries", salaries: @employee.salaries.ordered 13 | 14 | %h2 薪資單 15 | = render "payrolls", payrolls: @payrolls.ordered 16 | -------------------------------------------------------------------------------- /test/services/health_insurance_service/supplement_premium_rate_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "test_helper" 3 | 4 | module HealthInsuranceService 5 | class SupplementPremiumRateTest < ActiveSupport::TestCase 6 | def test_after_202101 7 | assert_equal 0.0211, HealthInsuranceService::SupplementPremiumRate.call(2021, 1) 8 | end 9 | 10 | def test_after_201601 11 | assert_equal 0.0191, HealthInsuranceService::SupplementPremiumRate.call(2016, 1) 12 | end 13 | 14 | def test_after_203101 15 | assert_equal 0.02, HealthInsuranceService::SupplementPremiumRate.call(2013, 1) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/models/payroll_detail.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class PayrollDetail < ApplicationRecord 3 | scope :owner_income, ->(year:, month:) { find_by(year: year, month: month, owner: true) } 4 | 5 | scope :yearly_bonus_total_until, ->(year:, month:, employee_id:) { 6 | where(employee_id: employee_id, year: year, splits: nil, tax_code: 50, owner: false) 7 | .where("excess_income > 0 AND month <= ?", month) 8 | .sum(:excess_income) 9 | } 10 | 11 | scope :monthly_excess_payment, ->(year:, month:) { 12 | where(year: year, month: month, splits: nil, tax_code: 50, owner: false) 13 | .where("excess_income > 0") 14 | } 15 | end 16 | -------------------------------------------------------------------------------- /app/views/employees/_info.haml: -------------------------------------------------------------------------------- 1 | %h3 員工資料 2 | %table.table 3 | %tr 4 | %th.col-xs-1 姓名 5 | %td.col-xs-2= employee.name 6 | %th.col-xs-1 戶籍地址 7 | %td.col-xs-2= employee.residence_address 8 | %tr 9 | %th 身分證字號 10 | %td= employee.id_number 11 | %th 出生年月日 12 | %td= employee.birthday 13 | %tr 14 | %th 公司信箱 15 | %td= employee.company_email 16 | %th 私人信箱 17 | %td= employee.personal_email 18 | %tr 19 | %th 銀行帳戶 20 | %td= employee.bank_account 21 | %th 轉帳方式 22 | %td= employee.given_bank_transfer_type 23 | -if employee.b2b? 24 | %tr 25 | %th 非個人所得 26 | %td{colspan: 3} 付款對象可開立發票,不列入勞務報酬 27 | -------------------------------------------------------------------------------- /app/services/statement_service/update.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module StatementService 3 | class Update 4 | include Callable 5 | attr_reader :statement, :params 6 | 7 | def initialize(statement, params) 8 | @statement = statement 9 | @params = params 10 | end 11 | 12 | def call 13 | params["correction"] = correction 14 | statement.update(params) 15 | end 16 | 17 | private 18 | 19 | def correction 20 | sum = 0 21 | params["corrections_attributes"].each do |row| 22 | sum += row[1][:amount].to_i if row[1][:_destroy] == "false" 23 | end 24 | sum 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /config/holidays/2023.yml: -------------------------------------------------------------------------------- 1 | holidays: 2 | 1: 3 | - 2 # 元旦 4 | - 20 # 春節彈性 5 | - 23 # 春節 6 | - 24 # 春節 7 | - 25 # 春節 8 | - 26 # 春節 9 | - 27 # 春節彈性 10 | 11 | 2: 12 | - 27 # 228 彈性 13 | - 28 # 228 14 | 4: 15 | - 3 # 清明彈性 16 | - 4 # 清明 17 | - 5 # 清明 18 | 5: 19 | - 1 # 勞動節 20 | 6: 21 | - 22 # 端午節 22 | - 23 # 端午節彈性 23 | 9: 24 | - 29 # 中秋節 25 | 10: 26 | - 9 # 國慶日彈性 27 | - 10 # 國慶日 28 | makeup_days: 29 | 1: 30 | - 7 # 春節補班 31 | 2: 32 | - 4 # 春節補班 33 | - 18 # 228 補班 34 | 3: 35 | - 25 # 清明補班 36 | 6: 37 | - 17 # 端午補班 38 | 9: 39 | - 23 # 雙十補班 40 | -------------------------------------------------------------------------------- /app/services/health_insurance_service/company_withhold_bonus.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 二代健保:公司代扣總額(獎金) 3 | module HealthInsuranceService 4 | class CompanyWithholdBonus 5 | include Callable 6 | 7 | attr_reader :year, :month 8 | 9 | def initialize(year, month) 10 | @year = year 11 | @month = month 12 | end 13 | 14 | def call 15 | premium 16 | end 17 | 18 | private 19 | 20 | def premium 21 | PayrollDetail.monthly_excess_payment(year: year, month: month).reduce(0) do |sum, row| 22 | sum + HealthInsuranceService::BonusIncome.call(Payroll.find(row.payroll_id)) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /db/migrate/20180105160319_create_salaries.rb: -------------------------------------------------------------------------------- 1 | class CreateSalaries < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :salaries do |t| 4 | t.string :role 5 | t.string :tax_code, default: 50 6 | t.integer :monthly_wage, default: 0 7 | t.integer :hourly_wage, default: 0 8 | t.date :effective_date 9 | t.integer :equipment_subsidy, default: 0 10 | t.integer :commuting_subsidy, default: 0 11 | t.integer :supervisor_allowance, default: 0 12 | t.integer :labor_insurance, default: 0 13 | t.integer :health_insurance, default: 0 14 | 15 | t.integer :employee_id 16 | t.timestamps 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /db/views/reports_v03.sql: -------------------------------------------------------------------------------- 1 | SELECT DISTINCT 2 | employees.id AS employee_id, 3 | payrolls.id AS payroll_id, 4 | statements.id AS statement_id, 5 | employees.name, 6 | payrolls.year, 7 | payrolls.month, 8 | salaries.tax_code, 9 | statements.amount, 10 | statements.irregular_income, 11 | SUM(corrections.amount) AS correction 12 | FROM employees 13 | INNER JOIN payrolls ON employees.id = payrolls.employee_id 14 | INNER JOIN salaries ON salaries.id = payrolls.salary_id 15 | INNER JOIN statements ON payrolls.id = statements.payroll_id 16 | LEFT OUTER JOIN corrections ON statements.id = corrections.statement_id 17 | GROUP BY employees.id, payrolls.id, statements.id, salaries.tax_code 18 | -------------------------------------------------------------------------------- /app/helpers/payrolls_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module PayrollsHelper 3 | def init_year 4 | q_year.between?(2014, this_year + 1) ? q_year : this_year 5 | end 6 | 7 | def init_month 8 | q_month.between?(1, 12) ? q_month : this_month 9 | end 10 | 11 | def init_button_text 12 | "產生薪資單:#{init_year}-#{sprintf('%02d', init_month)}" 13 | end 14 | 15 | private 16 | 17 | def this_year 18 | Time.zone.today.year 19 | end 20 | 21 | def this_month 22 | Time.zone.today.month 23 | end 24 | 25 | def q_year 26 | params.dig(:query, :year_eq).to_i 27 | end 28 | 29 | def q_month 30 | params.dig(:query, :month_eq).to_i 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /db/views/reports_v04.sql: -------------------------------------------------------------------------------- 1 | SELECT DISTINCT 2 | employees.id AS employee_id, 3 | payrolls.id AS payroll_id, 4 | employees.name, 5 | payrolls.year, 6 | payrolls.month, 7 | salaries.tax_code, 8 | statements.amount, 9 | statements.irregular_income, 10 | payrolls.festival_bonus, 11 | payrolls.festival_type, 12 | SUM(corrections.amount) AS correction 13 | FROM employees 14 | INNER JOIN payrolls ON employees.id = payrolls.employee_id 15 | INNER JOIN salaries ON salaries.id = payrolls.salary_id 16 | INNER JOIN statements ON payrolls.id = statements.payroll_id 17 | LEFT OUTER JOIN corrections ON statements.id = corrections.statement_id 18 | GROUP BY employees.id, payrolls.id, statements.id, salaries.tax_code 19 | -------------------------------------------------------------------------------- /app/views/employees/_terms.haml: -------------------------------------------------------------------------------- 1 | %table.table 2 | %tr 3 | %th.col-xs-1 # 4 | %th.col-xs-2 起始日期 5 | %th.col-xs-2 結束日期 6 | %th.col-xs-6 操作 7 | - terms.each do |term| 8 | %tr 9 | %td= term.id 10 | %td= term.start_date 11 | %td= term.end_date 12 | %td 13 | = link_to "新增薪資", new_employee_salary_path(employee_id: term.employee.id, term_id: term.id), class: "btn btn-xs btn-primary" 14 | = link_to "修改", edit_employee_term_path(employee_id: term.employee.id, id: term.id), class: "btn btn-xs btn-default" 15 | = link_to "刪除", employee_term_path(employee_id: term.employee.id, id: term.id), method: :delete, data: { confirm: "確定刪除?" }, class: "btn btn-xs btn-danger" 16 | -------------------------------------------------------------------------------- /app/services/income_tax_service/professional_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module IncomeTaxService 3 | class ProfessionalService 4 | include Callable 5 | include Calculatable 6 | 7 | attr_reader :payroll 8 | 9 | def initialize(payroll) 10 | @payroll = payroll 11 | end 12 | 13 | # 執行業務所得超過兩萬元 需代扣 10% 所得稅 14 | def call 15 | return 0 if payroll.salary.split? 16 | return 0 unless taxable? 17 | (taxable_income * rate).ceil 18 | end 19 | 20 | private 21 | 22 | def taxable? 23 | taxable_income > exemption 24 | end 25 | 26 | def exemption 27 | 20000 28 | end 29 | 30 | def rate 31 | 0.1 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/decorators/payroll_decorator_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "test_helper" 3 | 4 | class PayrollDecoratorTest < ActiveSupport::TestCase 5 | def test_role 6 | assert_equal "正職", decorated_subject.role 7 | end 8 | 9 | def test_payment_period 10 | assert_equal "2018-09", decorated_subject.payment_period 11 | end 12 | 13 | private 14 | 15 | def decorated_subject 16 | ActiveDecorator::Decorator.instance.decorate( 17 | build( 18 | :payroll, 19 | year: 2018, 20 | month: 9, 21 | salary: create(:salary, role: "regular", employee: build(:employee), term: build(:term)), 22 | employee: build(:employee) 23 | ) 24 | ) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/services/report_service/vacation_refund_cell.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module ReportService 3 | class VacationRefundCell 4 | include Callable 5 | 6 | attr_reader :payroll 7 | 8 | def initialize(payroll) 9 | @payroll = payroll 10 | end 11 | 12 | def call 13 | { 14 | employee_id: payroll.employee_id, 15 | name: payroll.employee_name, 16 | period: period, 17 | description: "特休折現", 18 | amount: CalculationService::VacationRefund.call(payroll), 19 | payroll_id: payroll.id, 20 | } 21 | end 22 | 23 | private 24 | 25 | def period 26 | "#{payroll.year}-#{sprintf('%02d', payroll.month)}" 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/services/salary_service/term_finder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module SalaryService 3 | class TermFinder 4 | include Callable 5 | 6 | attr_reader :salary, :terms 7 | 8 | def initialize(salary) 9 | @salary = salary 10 | @terms = salary.employee.terms 11 | end 12 | 13 | def call 14 | find_term 15 | end 16 | 17 | private 18 | 19 | def find_term 20 | terms.map do |term| 21 | return term if salary_match?(term) 22 | end 23 | end 24 | 25 | def salary_match?(term) 26 | salary.effective_date.between? term.start_date, end_date(term) 27 | end 28 | 29 | def end_date(term) 30 | term.end_date || Date.today 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/services/income_tax_service/regular_employee_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "test_helper" 3 | 4 | module IncomeTaxService 5 | class RegularEmployeeTest < ActiveSupport::TestCase 6 | def test_has_insuranced_salary_and_irregular_income 7 | IncomeTaxService::InsurancedSalary.expects(:call).returns(3000) 8 | IncomeTaxService::IrregularIncome.expects(:call).returns(5500) 9 | assert_equal 8500, IncomeTaxService::RegularEmployee.call(subject) 10 | end 11 | 12 | private 13 | 14 | def subject 15 | build( 16 | :payroll, 17 | year: 2018, 18 | month: 5, 19 | salary: build(:salary), 20 | employee: build(:employee) 21 | ) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Fortuna 10 | class Application < Rails::Application 11 | # Initialize configuration defaults for originally generated Rails version. 12 | config.load_defaults 5.1 13 | 14 | # Settings in config/environments/* take precedence over those specified here. 15 | # Application configuration should go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded. 17 | config.time_zone = "Taipei" 18 | config.i18n.default_locale = :"zh-TW" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/services/business_calendar_days_serivce_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "test_helper" 3 | 4 | class BusinessCalendarDaysServiceTest < ActiveSupport::TestCase 5 | def test_2018_02 6 | assert_equal 15, BusinessCalendarDaysService.call(Date.new(2018, 2, 1), Date.new(2018, 2, 28)) 7 | end 8 | 9 | def test_2018_07 10 | assert_equal 22, BusinessCalendarDaysService.call(Date.new(2018, 7, 1), Date.new(2018, 7, 31)) 11 | end 12 | 13 | def test_2018_02_range 14 | assert_equal 5, BusinessCalendarDaysService.call(Date.new(2018, 2, 21), Date.new(2018, 2, 28)) 15 | end 16 | 17 | # 無補班日 18 | def test_2015_05 19 | assert_equal 20, BusinessCalendarDaysService.call(Date.new(2015, 5, 1), Date.new(2015, 5, 31)) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/employees/_payrolls.haml: -------------------------------------------------------------------------------- 1 | %table.table 2 | %tr 3 | %th # 4 | %th 期間 5 | %th 身份 6 | %th 實發薪資 7 | %th 申報所得 8 | %th 操作 9 | - payrolls.each do |payroll| 10 | - if payroll.statement 11 | %tr 12 | %td= payroll.id 13 | %td= payroll.payment_period 14 | %td= payroll.role 15 | = payroll.statement.paid_amount_cell 16 | = payroll.statement.declared_income_cell 17 | %td 18 | = link_to "修改明細", edit_payroll_path(payroll), class: "btn btn-xs btn-default" 19 | = link_to "報表校正", edit_statement_path(payroll.statement), class: "btn btn-xs btn-warning" 20 | = link_to "刪除", payroll_path(payroll), method: :delete, data: { confirm: "確定刪除?" }, class: "btn btn-xs btn-danger" 21 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join('node_modules') 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | Rails.application.config.assets.precompile += %w( 15 | payrolls.css employees.css salaries.css statements.css reports.css terms.css 16 | ) 17 | -------------------------------------------------------------------------------- /app/services/payroll_init_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class PayrollInitService 3 | include Callable 4 | 5 | attr_reader :year, :month, :employee_id, :salary_id 6 | 7 | def initialize(year, month, employee_id, salary_id) 8 | @year = year.to_i 9 | @month = month.to_i 10 | @employee_id = employee_id 11 | @salary_id = salary_id 12 | end 13 | 14 | def call 15 | payroll 16 | sync_statement 17 | end 18 | 19 | private 20 | 21 | def payroll 22 | @payroll ||= Payroll.find_or_create_by( 23 | year: year, 24 | month: month, 25 | employee_id: employee_id, 26 | salary_id: salary_id 27 | ) 28 | end 29 | 30 | def sync_statement 31 | StatementService::Builder.call(payroll) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/services/calculation_service/sicktime_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "test_helper" 3 | 4 | module CalculationService 5 | class SicktimeTest < ActiveSupport::TestCase 6 | def test_sicktime_with_fulltime_salary 7 | salary = build(:salary, monthly_wage: 36000) 8 | payroll = build(:payroll, salary: salary, sicktime_hours: 1) 9 | assert_equal 75, CalculationService::Sicktime.call(payroll) 10 | end 11 | 12 | def test_sicktime_with_parttime_salary 13 | salary = build(:salary, monthly_wage: 24000, equipment_subsidy: 800, monthly_wage_adjustment: 0.6) 14 | payroll = build(:payroll, salary: salary, sicktime_hours: 1) 15 | assert_equal 85, CalculationService::Sicktime.call(payroll) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/services/income_tax_service/exemption.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module IncomeTaxService 3 | class Exemption 4 | include Callable 5 | 6 | attr_reader :date 7 | 8 | # 薪資所得扣繳稅額表無配偶及受扶養親屬者之起扣標準(也太長) 9 | def initialize(year, month) 10 | @date = Date.new(year, month, 1) 11 | end 12 | 13 | def call 14 | exemption_by_date 15 | end 16 | 17 | private 18 | 19 | def exemption_by_date 20 | if date >= Date.new(2024, 1, 1) 21 | 88500 22 | elsif date >= Date.new(2022, 1, 1) 23 | 86000 24 | elsif date >= Date.new(2018, 1, 1) 25 | 84500 26 | elsif date >= Date.new(2017, 1, 1) 27 | 73500 28 | else 29 | 73000 # 再往下寫沒意義 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/services/report_service/overtime_cell.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module ReportService 3 | class OvertimeCell 4 | include Callable 5 | 6 | attr_reader :overtime, :employee 7 | 8 | def initialize(overtime) 9 | @overtime = overtime 10 | @employee = overtime.employee 11 | end 12 | 13 | def call 14 | { 15 | employee_id: employee.id, 16 | name: employee.name, 17 | period: period, 18 | description: "加班費", 19 | amount: CalculationService::Overtime.call(overtime), 20 | payroll_id: overtime.payroll_id, 21 | } 22 | end 23 | 24 | private 25 | 26 | def period 27 | "#{overtime.payroll_year}-#{sprintf('%02d', overtime.payroll_month)}" 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /db/views/reports_v05.sql: -------------------------------------------------------------------------------- 1 | SELECT DISTINCT 2 | employees.id AS employee_id, 3 | payrolls.id AS payroll_id, 4 | employees.name, 5 | employees.id_number, 6 | employees.residence_address, 7 | payrolls.year, 8 | payrolls.month, 9 | salaries.tax_code, 10 | statements.amount, 11 | statements.irregular_income, 12 | payrolls.festival_bonus, 13 | payrolls.festival_type, 14 | SUM(corrections.amount) AS correction 15 | FROM employees 16 | INNER JOIN payrolls ON employees.id = payrolls.employee_id 17 | INNER JOIN salaries ON salaries.id = payrolls.salary_id 18 | INNER JOIN statements ON payrolls.id = statements.payroll_id 19 | LEFT OUTER JOIN corrections ON statements.id = corrections.statement_id 20 | GROUP BY employees.id, payrolls.id, statements.id, salaries.tax_code 21 | -------------------------------------------------------------------------------- /test/models/extra_entry_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "test_helper" 3 | 4 | class ExtraEntryTest < ActiveSupport::TestCase 5 | def test_scope_yearly_subsidy_report 6 | prepare_scope_yearly_subsidy_report(year: 2017, subsidies: 5) 7 | prepare_scope_yearly_subsidy_report(year: 2018, subsidies: 3) 8 | 9 | assert ExtraEntry.yearly_subsidy_report(2017).count, 5 10 | assert ExtraEntry.yearly_subsidy_report(2018).count, 3 11 | end 12 | 13 | private 14 | 15 | def prepare_scope_yearly_subsidy_report(year:, subsidies:) 16 | create(:payroll, year: year, salary: build(:salary), employee: build(:employee)) do |payroll| 17 | subsidies.times { create(:extra_entry, amount: 1, income_type: "subsidy", payroll: payroll) } 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/services/report_service/service_income_cell.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module ReportService 3 | class ServiceIncomeCell 4 | include Callable 5 | 6 | attr_reader :data, :year, :month 7 | 8 | def initialize(data, year, month) 9 | @data = data 10 | @year = year 11 | @month = month 12 | end 13 | 14 | def call 15 | if month.zero? 16 | shift_to_previous_year 17 | else 18 | shift_to_previous_month 19 | end 20 | end 21 | 22 | private 23 | 24 | def shift_to_previous_year 25 | data.select { |cell| (cell.month == 12) && (cell.year == year - 1) }.first 26 | end 27 | 28 | def shift_to_previous_month 29 | data.select { |cell| cell.month == month }.first 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/services/income_tax_service/exemption_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "test_helper" 3 | 4 | module IncomeTaxService 5 | class ExemptionTest < ActiveSupport::TestCase 6 | def test_after_2024 7 | assert_equal 88500, IncomeTaxService::Exemption.call(2024, 1) 8 | end 9 | 10 | def test_after_2022 11 | assert_equal 86000, IncomeTaxService::Exemption.call(2022, 1) 12 | end 13 | 14 | def test_after_2018 15 | assert_equal 84500, IncomeTaxService::Exemption.call(2018, 1) 16 | end 17 | 18 | def test_after_2017 19 | assert_equal 73500, IncomeTaxService::Exemption.call(2017, 1) 20 | end 21 | 22 | def test_before_2017 23 | assert_equal 73000, IncomeTaxService::Exemption.call(2016, 12) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/helpers/payrolls_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "test_helper" 3 | 4 | class PayrollsHelperTest < ActionView::TestCase 5 | def test_with_params 6 | prepare_params(year: 2017, month: 7) 7 | assert_equal 2017, init_year 8 | assert_equal 7, init_month 9 | assert_equal "產生薪資單:2017-07", init_button_text 10 | end 11 | 12 | def test_without_params 13 | Timecop.freeze(Date.new(2015, 3)) do 14 | assert_equal 2015, init_year 15 | assert_equal 3, init_month 16 | assert_equal "產生薪資單:2015-03", init_button_text 17 | end 18 | end 19 | 20 | private 21 | 22 | def prepare_params(year:, month:) 23 | @controller = ApplicationController.new 24 | @controller.send(:params=, query: { year_eq: year, month_eq: month }) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/services/income_tax_service/dispatcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module IncomeTaxService 3 | class Dispatcher 4 | include Callable 5 | 6 | attr_reader :payroll, :salary 7 | 8 | def initialize(payroll) 9 | @payroll = payroll 10 | @salary = payroll.salary 11 | end 12 | 13 | def call 14 | return 0 if payroll.employee.b2b? 15 | dispatch 16 | end 17 | 18 | private 19 | 20 | def dispatch 21 | if salary.professional_service? 22 | IncomeTaxService::ProfessionalService.call(payroll) 23 | elsif salary.parttime_income_uninsured_for_labor? 24 | IncomeTaxService::UninsurancedSalary.call(payroll) 25 | else 26 | IncomeTaxService::RegularEmployee.call(payroll) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /db/views/reports_v07.sql: -------------------------------------------------------------------------------- 1 | SELECT DISTINCT 2 | employees.id AS employee_id, 3 | payrolls.id AS payroll_id, 4 | employees.name, 5 | employees.id_number, 6 | employees.residence_address, 7 | payrolls.year, 8 | payrolls.month, 9 | salaries.tax_code, 10 | statements.amount, 11 | statements.subsidy_income, 12 | payrolls.festival_bonus, 13 | payrolls.festival_type, 14 | SUM(corrections.amount) AS correction 15 | FROM employees 16 | INNER JOIN payrolls ON employees.id = payrolls.employee_id 17 | INNER JOIN salaries ON salaries.id = payrolls.salary_id 18 | INNER JOIN statements ON payrolls.id = statements.payroll_id 19 | LEFT OUTER JOIN corrections ON statements.id = corrections.statement_id 20 | WHERE employees.b2b = false 21 | GROUP BY employees.id, payrolls.id, statements.id, salaries.tax_code 22 | -------------------------------------------------------------------------------- /db/views/reports_v06.sql: -------------------------------------------------------------------------------- 1 | SELECT DISTINCT 2 | employees.id AS employee_id, 3 | payrolls.id AS payroll_id, 4 | employees.name, 5 | employees.id_number, 6 | employees.residence_address, 7 | payrolls.year, 8 | payrolls.month, 9 | salaries.tax_code, 10 | statements.amount, 11 | statements.irregular_income, 12 | payrolls.festival_bonus, 13 | payrolls.festival_type, 14 | SUM(corrections.amount) AS correction 15 | FROM employees 16 | INNER JOIN payrolls ON employees.id = payrolls.employee_id 17 | INNER JOIN salaries ON salaries.id = payrolls.salary_id 18 | INNER JOIN statements ON payrolls.id = statements.payroll_id 19 | LEFT OUTER JOIN corrections ON statements.id = corrections.statement_id 20 | WHERE employees.b2b = false 21 | GROUP BY employees.id, payrolls.id, statements.id, salaries.tax_code 22 | -------------------------------------------------------------------------------- /test/services/calculation_service/vacation_refund_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "test_helper" 3 | 4 | module CalculationService 5 | class VacationRefundTest < ActiveSupport::TestCase 6 | def test_refund_with_fulltime_salary 7 | salary = build(:salary, monthly_wage: 36000) 8 | payroll = build(:payroll, salary: salary, vacation_refund_hours: 1) 9 | assert_equal 150, CalculationService::VacationRefund.call(payroll) 10 | end 11 | 12 | def test_refund_with_parttime_salary 13 | salary = build(:salary, monthly_wage: 24000, equipment_subsidy: 800, monthly_wage_adjustment: 0.6) 14 | payroll = build(:payroll, salary: salary, vacation_refund_hours: 1) 15 | assert_equal 170, CalculationService::VacationRefund.call(payroll) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's 5 | // vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery3 14 | //= require jquery_ujs 15 | //= require turbolinks 16 | //= require cocoon 17 | //= require_tree . 18 | -------------------------------------------------------------------------------- /app/services/health_insurance_service/dispatcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module HealthInsuranceService 3 | class Dispatcher 4 | include Callable 5 | 6 | attr_reader :payroll, :salary 7 | 8 | def initialize(payroll) 9 | @payroll = payroll 10 | @salary = payroll.salary 11 | end 12 | 13 | def call 14 | return 0 if payroll.employee.b2b? 15 | dispatch 16 | end 17 | 18 | private 19 | 20 | def dispatch 21 | if salary.professional_service? 22 | HealthInsuranceService::ProfessionalService.call(payroll) 23 | elsif salary.parttime_income_uninsured_for_health? 24 | HealthInsuranceService::ParttimeIncome.call(payroll) 25 | else 26 | HealthInsuranceService::BonusIncome.call(payroll) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/views/statements/_list.haml: -------------------------------------------------------------------------------- 1 | %table.table 2 | %tr.warning 3 | %th 姓名 4 | %th 身份 5 | %th 計算月份 6 | %th 實發薪資 7 | %th 申報所得 8 | %th 操作 9 | - statements.each do |statement| 10 | %tr 11 | %td= statement.employee_name 12 | %td= statement.payroll.role 13 | %td= statement.payroll.payment_period 14 | = statement.paid_amount_cell 15 | = statement.declared_income_cell 16 | %td 17 | = link_to "員工資料", employee_path(statement.payroll.employee), class: "btn btn-xs btn-default" 18 | = link_to "薪資單", statement_path(statement), class: "btn btn-xs btn-info" 19 | 20 | %tr.info 21 | %th 總額 22 | %td 23 | %td 24 | %td= number_with_delimiter statements.map { |statement| statement.amount }.reduce(:+) 25 | %td 26 | %td 27 | 28 | = paginate statements 29 | -------------------------------------------------------------------------------- /app/services/income_tax_service/irregular_income.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module IncomeTaxService 3 | class IrregularIncome 4 | include Callable 5 | include Calculatable 6 | 7 | attr_reader :payroll 8 | 9 | def initialize(payroll) 10 | @payroll = payroll 11 | end 12 | 13 | # 非經常性給予超過免稅額 需代扣 5% 所得稅 14 | def call 15 | return 0 unless taxable? 16 | (irregular_income * rate).ceil 17 | end 18 | 19 | private 20 | 21 | def taxable? 22 | irregular_income > exemption 23 | end 24 | 25 | def irregular_income 26 | bonus_income + payroll.extra_income_of(:salary) 27 | end 28 | 29 | def exemption 30 | IncomeTaxService::Exemption.call(payroll.year, payroll.month) 31 | end 32 | 33 | def rate 34 | 0.05 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /config/initializers/wicked_pdf.rb: -------------------------------------------------------------------------------- 1 | # WickedPDF Global Configuration 2 | # 3 | # Use this to set up shared configuration options for your entire application. 4 | # Any of the configuration options shown here can also be applied to single 5 | # models by passing arguments to the `render :pdf` call. 6 | # 7 | # To learn more, check out the README: 8 | # 9 | # https://github.com/mileszs/wicked_pdf/blob/master/README.md 10 | 11 | WickedPdf.config = { 12 | # Path to the wkhtmltopdf executable: This usually isn't needed if using 13 | # one of the wkhtmltopdf-binary family of gems. 14 | # exe_path: '/usr/local/bin/wkhtmltopdf', 15 | # or 16 | # exe_path: Gem.bin_path('wkhtmltopdf-binary', 'wkhtmltopdf') 17 | 18 | # Layout file to be used for all PDFs 19 | # (but can be overridden in `render :pdf` calls) 20 | # layout: 'pdf.html', 21 | } 22 | -------------------------------------------------------------------------------- /db/views/reports_v09.sql: -------------------------------------------------------------------------------- 1 | SELECT DISTINCT 2 | employees.id AS employee_id, 3 | payrolls.id AS payroll_id, 4 | employees.name, 5 | employees.id_number, 6 | employees.residence_address, 7 | payrolls.year, 8 | payrolls.month, 9 | salaries.tax_code, 10 | statements.amount, 11 | statements.subsidy_income, 12 | statements.gain, 13 | statements.loss, 14 | statements.correction, 15 | payrolls.festival_bonus, 16 | payrolls.festival_type 17 | FROM employees 18 | INNER JOIN payrolls ON employees.id = payrolls.employee_id 19 | INNER JOIN salaries ON salaries.id = payrolls.salary_id 20 | INNER JOIN statements ON payrolls.id = statements.payroll_id 21 | LEFT OUTER JOIN corrections ON statements.id = corrections.statement_id 22 | WHERE employees.b2b = false 23 | GROUP BY employees.id, payrolls.id, statements.id, salaries.tax_code 24 | -------------------------------------------------------------------------------- /app/services/report_service/extra_entry_cell.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module ReportService 3 | class ExtraEntryCell 4 | include Callable 5 | 6 | attr_reader :entry, :payroll 7 | 8 | def initialize(entry) 9 | @entry = entry 10 | @payroll = entry.payroll 11 | end 12 | 13 | def call 14 | { 15 | employee_id: employee_id, 16 | name: name, 17 | period: period, 18 | description: entry.title, 19 | amount: entry.amount, 20 | payroll_id: payroll.id, 21 | } 22 | end 23 | 24 | private 25 | 26 | def employee_id 27 | payroll.employee_id 28 | end 29 | 30 | def name 31 | payroll.employee_name 32 | end 33 | 34 | def period 35 | "#{payroll.year}-#{sprintf('%02d', payroll.month)}" 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |