From 379a214ab4cc27dfd6be3dc90dc1ab1b6fbe6957 Mon Sep 17 00:00:00 2001 From: SHIMADA Koji Date: Fri, 6 Nov 2015 17:48:10 +0900 Subject: [PATCH 01/18] Clean up Ruby implementation --- Gemfile | 4 --- Rakefile | 1 - example/Gemfile | 9 ------ example/Rakefile | 1 - lib/seisan.rb | 24 -------------- lib/seisan/base_renderer.rb | 18 ----------- lib/seisan/expense_renderer.rb | 51 ----------------------------- lib/seisan/header_renderer.rb | 20 ------------ lib/seisan/report.rb | 59 ---------------------------------- lib/seisan/task.rb | 58 --------------------------------- lib/seisan/version.rb | 3 -- seisan.gemspec | 26 --------------- 12 files changed, 274 deletions(-) delete mode 100644 Gemfile delete mode 100644 Rakefile delete mode 100644 example/Gemfile delete mode 100644 example/Rakefile delete mode 100644 lib/seisan.rb delete mode 100644 lib/seisan/base_renderer.rb delete mode 100644 lib/seisan/expense_renderer.rb delete mode 100644 lib/seisan/header_renderer.rb delete mode 100644 lib/seisan/report.rb delete mode 100644 lib/seisan/task.rb delete mode 100644 lib/seisan/version.rb delete mode 100644 seisan.gemspec diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 6592888..0000000 --- a/Gemfile +++ /dev/null @@ -1,4 +0,0 @@ -source 'https://rubygems.org' - -# Specify your gem's dependencies in seisan.gemspec -gemspec diff --git a/Rakefile b/Rakefile deleted file mode 100644 index 2995527..0000000 --- a/Rakefile +++ /dev/null @@ -1 +0,0 @@ -require "bundler/gem_tasks" diff --git a/example/Gemfile b/example/Gemfile deleted file mode 100644 index 1d2db71..0000000 --- a/example/Gemfile +++ /dev/null @@ -1,9 +0,0 @@ -source 'https://rubygems.org' - -gem 'rake' - -# Use development version. Usually you should want to use released version: -# -# gem 'seisan' -# -gem 'seisan', path: '..' diff --git a/example/Rakefile b/example/Rakefile deleted file mode 100644 index 1f0a334..0000000 --- a/example/Rakefile +++ /dev/null @@ -1 +0,0 @@ -require 'seisan/task' diff --git a/lib/seisan.rb b/lib/seisan.rb deleted file mode 100644 index 6e2188b..0000000 --- a/lib/seisan.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'seisan/version' -require 'seisan/report' -require 'seisan/base_renderer' -require 'seisan/header_renderer' -require 'seisan/expense_renderer' -require 'logger' - -module Seisan - def self.logger - @@logger ||= initialize_default_logger - end - - def self.logger=(logger) - @@logger = logger - end - - def self.initialize_default_logger - logger = Logger.new(STDOUT) - logger.formatter = proc{|severity, datetime, progname, message| - "#{message}\n" - } - logger - end -end diff --git a/lib/seisan/base_renderer.rb b/lib/seisan/base_renderer.rb deleted file mode 100644 index 002cf13..0000000 --- a/lib/seisan/base_renderer.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Seisan - class BaseRenderer - def initialize(requests, sheet, font, config) - @requests = requests - @sheet = sheet - @font = font - @config = config - end - - def requests - @requests - end - - def row(columns=[]) - @sheet.add_row columns, :style => @font - end - end -end diff --git a/lib/seisan/expense_renderer.rb b/lib/seisan/expense_renderer.rb deleted file mode 100644 index 5bad39f..0000000 --- a/lib/seisan/expense_renderer.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'seisan/base_renderer' -require 'date' - -module Seisan - class ExpenseRenderer < BaseRenderer - def render - row ['立替払サマリー'] - row summary_headings - summary.each do |person, amount| - row [person, amount] - end - row - - row ['立替払明細'] - row headings - lines.each do |line| - row line - end - row - - Seisan.logger.info 'Processed %d expenses' % lines.size - end - - private - def summary_headings - %w(氏名 金額) - end - - def summary - summary = Hash.new(0) - requests.each do |entry| - summary[entry['applicant']] += entry['expense'].inject(0){|r, e| r += e['amount'].to_i } - end - summary - end - - def headings - %w(日付 立替者 金額 摘要 備考) - end - - def lines - lines = [] - requests.each do |entry| - entry['expense'].each do |expense| - lines << [expense['date'].to_s, entry['applicant'], expense['amount'], expense['remarks'], expense['notes']] - end - end - lines.sort_by {|line| [Date.parse(line[0]), line[1]] } - end - end -end diff --git a/lib/seisan/header_renderer.rb b/lib/seisan/header_renderer.rb deleted file mode 100644 index d69c988..0000000 --- a/lib/seisan/header_renderer.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'seisan/base_renderer' - -module Seisan - class HeaderRenderer < BaseRenderer - def render - row ["#{organization_name} 精算シート #{target_name}"] - row ['作成時刻', Time.now.strftime('%Y-%m-%d %X')] - row - end - - private - def target_name - @config['target'] - end - - def organization_name - @config['organization'] ? @config['organization']['name'] : '' - end - end -end diff --git a/lib/seisan/report.rb b/lib/seisan/report.rb deleted file mode 100644 index 70bd8ed..0000000 --- a/lib/seisan/report.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'axlsx' -require 'fileutils' -require 'seisan/header_renderer' -require 'seisan/expense_renderer' - -module Seisan - class Report - DEFAULT_RENDERERS = [ - Seisan::HeaderRenderer, - Seisan::ExpenseRenderer - ] - @@renderers = DEFAULT_RENDERERS - - class << self - def renderer_chain(&block) - @@renderers = [] - block.call(self) if block - end - - def add(renderer) - @@renderers << renderer - end - end - - def initialize(requests, config) - @requests = requests - @config = config - end - - def export(dest_path) - prepare_sheet - - renderers.each do |renderer| - renderer.render - end - - write_to_file(dest_path) - end - - private - def renderers - @@renderers.map {|r| r.new(@requests, @sheet, @font, @config) } - end - - def prepare_sheet - @package = Axlsx::Package.new - @workbook = @package.workbook - @package.use_shared_strings = true - @font = @workbook.styles.add_style :font_name => 'MS Pゴシック' - @sheet = @workbook.add_worksheet(:name => '精算シート') - end - - def write_to_file(dest_path) - FileUtils.mkdir_p(File.dirname(dest_path)) - @package.serialize(dest_path) - Seisan.logger.info 'Wrote to %s' % dest_path - end - end -end diff --git a/lib/seisan/task.rb b/lib/seisan/task.rb deleted file mode 100644 index 0ebfb18..0000000 --- a/lib/seisan/task.rb +++ /dev/null @@ -1,58 +0,0 @@ -require 'rake' -require 'seisan' -require 'gimlet' - -module Seisan - class Task - include Rake::DSL if defined? Rake::DSL - def self.install_tasks - new.install - end - - def install - desc "Generate seisan report" - task :seisan do - src_dir, dest_dir = 'data', 'output' - config = user_config.merge('target' => ENV['TARGET']) - report(src_dir, dest_dir, config) - end - task :default => :seisan - end - - private - def report(src_dir, dest_dir, config) - if config['target'].nil? - Seisan.logger.error "You must specify the 'TARGET'.\nExample:\n % bundle exec rake TARGET=2013/07" - exit - end - - Seisan.logger.info 'Processing %s ...' % config['target'] - requests = load_seisan_requests(src_dir, config['target']) - Seisan.logger.info 'Loaded %d files' % requests.size - report = Seisan::Report.new(requests, config) - - dest_path = File.join(dest_dir, '%s.xlsx' % convert_target_to_file_name(config['target'])) - report.export(dest_path) - end - - def user_config - Gimlet::DataStore.new('config.yaml').to_h - rescue Gimlet::DataStore::SourceNotFound - {} - end - - def load_seisan_requests(src_dir, target) - source = Gimlet::DataStore.new(File.join(src_dir, target)) - source.to_h.values - rescue Gimlet::DataStore::SourceNotFound - [] - end - - def convert_target_to_file_name(target) - target.gsub('/', '-') - end - end -end - -# Install tasks -Seisan::Task.install_tasks diff --git a/lib/seisan/version.rb b/lib/seisan/version.rb deleted file mode 100644 index 686865f..0000000 --- a/lib/seisan/version.rb +++ /dev/null @@ -1,3 +0,0 @@ -module Seisan - VERSION = "0.0.1" -end diff --git a/seisan.gemspec b/seisan.gemspec deleted file mode 100644 index d98f584..0000000 --- a/seisan.gemspec +++ /dev/null @@ -1,26 +0,0 @@ -# coding: utf-8 -lib = File.expand_path('../lib', __FILE__) -$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require 'seisan/version' - -Gem::Specification.new do |spec| - spec.name = "seisan" - spec.version = Seisan::VERSION - spec.authors = ["SHIMADA Koji"] - spec.email = ["koji.shimada@enishi-tech.com"] - spec.description = %q{seisan solution for small team} - spec.summary = %q{seisan solution for small team} - spec.homepage = "https://github.com/enishitech/seisan" - spec.license = "MIT" - - spec.files = `git ls-files`.split($/) - spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } - spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) - spec.require_paths = ["lib"] - - spec.add_runtime_dependency "axlsx", "~> 2.0.1" - spec.add_runtime_dependency "gimlet", "~> 0.0.3" - - spec.add_development_dependency "bundler", "~> 1.3" - spec.add_development_dependency "rake" -end From 21d62253a4d5c02d25f6e266d790d77d3020a707 Mon Sep 17 00:00:00 2001 From: SHIMADA Koji Date: Fri, 6 Nov 2015 18:16:15 +0900 Subject: [PATCH 02/18] Go powered seisan --- README.md | 29 +++---------- config.go | 39 +++++++++++++++++ expense.go | 8 ++++ expense_report.go | 105 ++++++++++++++++++++++++++++++++++++++++++++++ main.go | 29 +++++++++++++ seisan_report.go | 68 ++++++++++++++++++++++++++++++ seisan_request.go | 40 ++++++++++++++++++ 7 files changed, 295 insertions(+), 23 deletions(-) create mode 100644 config.go create mode 100644 expense.go create mode 100644 expense_report.go create mode 100644 main.go create mode 100644 seisan_report.go create mode 100644 seisan_request.go diff --git a/README.md b/README.md index 2e0017d..01fdd50 100644 --- a/README.md +++ b/README.md @@ -4,28 +4,14 @@ Seisan solution for small team. ## Installation -You need a few steps to setup seisan data repository. `example` directory in `enishitech/seisan` will be a good reference for you. - -Put `Gemfile`: - -``` -source 'https://rubygems.org' +Compile from Source. -gem 'rake' -gem 'seisan' ``` - -Run bundler: - -```shell -% bundle +% go get -u github.com/enishitech/seisan ``` -Put `Rakefile`: +You need a few steps to setup seisan data repository. `example` directory in `enishitech/seisan` will be a good reference for you. -``` -require 'seisan/task' -``` Create `data` directory to store data. ```shell @@ -35,7 +21,7 @@ Create `data` directory to store data. OK, now everything is set up. Run ```shell -% bundle exec rake seisan TARGET=2013/07 +% seisan 2013/07 ``` Then you will have an empty monthly report (because you have no record in seisan data) at `output/2013-07.xlsx`. @@ -83,12 +69,10 @@ data └── 08-shidara.yaml ``` -Put `Rakefile` to your seisan data repository, - Then you can generate seisan report. ```shell -% bundle exec rake seisan TARGET=2013/07 +% seisan 2013/07 ``` ## Contributing @@ -101,8 +85,7 @@ Then you can generate seisan report. ----- -© 2013 Enishi Tech Inc. - +© 2015 Enishi Tech Inc. [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/enishitech/seisan/trend.png)](https://bitdeli.com/free "Bitdeli Badge") diff --git a/config.go b/config.go new file mode 100644 index 0000000..4ed3cfb --- /dev/null +++ b/config.go @@ -0,0 +1,39 @@ +package main + +import ( + "io/ioutil" + "log" + "os" + + "github.com/codegangsta/cli" + + "gopkg.in/yaml.v2" +) + +type Config struct { + Target string + Organization map[string]string +} + +func (self *Config) mergeCliArgs(args cli.Args) { + self.Target = args.First() +} + +func loadConfig(configPath string) Config { + var config Config + + _, err := os.Stat(configPath) + if err != nil { + log.Fatal("ERROR: ", err.Error()) + } + + buf, err := ioutil.ReadFile(configPath) + if err != nil { + log.Fatal("ERROR: ", err.Error()) + } + err = yaml.Unmarshal(buf, &config) + if err != nil { + log.Fatal("ERROR: ", err.Error()) + } + return config +} diff --git a/expense.go b/expense.go new file mode 100644 index 0000000..7ba41de --- /dev/null +++ b/expense.go @@ -0,0 +1,8 @@ +package main + +type Expense struct { + Date string + Applicant string + Amount int + Remarks string +} diff --git a/expense_report.go b/expense_report.go new file mode 100644 index 0000000..ccc6247 --- /dev/null +++ b/expense_report.go @@ -0,0 +1,105 @@ +package main + +import ( + "fmt" + "sort" + + "github.com/tealeg/xlsx" +) + +type ExpenseReport struct { + summary map[string]int + lines []Expense +} + +func newExpenseReport(seisanRequests []SeisanRequest) *ExpenseReport { + report := &ExpenseReport{} + report.makeSummary(seisanRequests) + report.makeLines(seisanRequests) + return report +} + +func (self *ExpenseReport) makeSummary(seisanRequests []SeisanRequest) { + self.summary = map[string]int{} + for _, seisanRequest := range seisanRequests { + for _, expense := range seisanRequest.Expenses { + if _, exists := self.summary[seisanRequest.Applicant]; exists { + self.summary[seisanRequest.Applicant] = expense.Amount + } else { + self.summary[seisanRequest.Applicant] += expense.Amount + } + } + } +} + +type ByDate []Expense + +func (a ByDate) Len() int { return len(a) } +func (a ByDate) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByDate) Less(i, j int) bool { return a[i].Date < a[j].Date } + +func (self *ExpenseReport) makeLines(in []SeisanRequest) { + self.lines = []Expense{} + for _, seisanRequest := range in { + for _, expense := range seisanRequest.Expenses { + expense.Applicant = seisanRequest.Applicant + self.lines = append(self.lines, expense) + } + } + sort.Sort(ByDate(self.lines)) + fmt.Printf("Processed %d expenses\n", len(self.lines)) +} + +func (self *ExpenseReport) renderSummary(sheet *xlsx.Sheet) { + var row *xlsx.Row + var cell *xlsx.Cell + + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue("立替払サマリー") + row = sheet.AddRow() + for _, heading := range []string{"氏名", "金額"} { + cell = row.AddCell() + cell.SetValue(heading) + } + for key, value := range self.summary { + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue(key) + cell = row.AddCell() + cell.SetValue(value) + } + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue("") +} + +func (self *ExpenseReport) renderLines(sheet *xlsx.Sheet) { + var row *xlsx.Row + var cell *xlsx.Cell + + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue("立替払明細") + row = sheet.AddRow() + for _, heading := range []string{"日付", "立替者", "金額", "摘要", "備考"} { + cell = row.AddCell() + cell.SetValue(heading) + } + for _, detail := range self.lines { + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue(detail.Date) + cell = row.AddCell() + cell.SetValue(detail.Applicant) + cell = row.AddCell() + cell.SetValue(detail.Amount) + cell = row.AddCell() + cell.SetValue(detail.Remarks) + } +} + +func (self *ExpenseReport) render(sheet *xlsx.Sheet) { + self.renderSummary(sheet) + self.renderLines(sheet) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..3458111 --- /dev/null +++ b/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/codegangsta/cli" +) + +func main() { + app := cli.NewApp() + app.Name = "seisan" + app.Usage = "Generate seisan report" + app.Action = func(c *cli.Context) { + if args := c.Args(); args.Present() { + config := loadConfig("config.yaml") + config.mergeCliArgs(args) + + fmt.Printf("Processing %s ...\n", config.Target) + seisanRequests := loadSeisanRequests(filepath.Join("data", config.Target)) + seisanReport := newSeisanReport(seisanRequests, config) + seisanReport.export() + } else { + fmt.Println("You must specify the 'TARGET'.\nExample:\n % seisan 2015/10") + } + } + app.Run(os.Args) +} diff --git a/seisan_report.go b/seisan_report.go new file mode 100644 index 0000000..13520ee --- /dev/null +++ b/seisan_report.go @@ -0,0 +1,68 @@ +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + "time" + + "github.com/tealeg/xlsx" +) + +type SeisanReport struct { + config Config + expenseReport *ExpenseReport +} + +func newSeisanReport(seisanRequests []SeisanRequest, config Config) *SeisanReport { + report := &SeisanReport{} + report.config = config + report.expenseReport = newExpenseReport(seisanRequests) + return report +} + +func (self *SeisanReport) renderReportHeader(sheet *xlsx.Sheet, name string) { + var row *xlsx.Row + var cell *xlsx.Cell + + row = sheet.AddRow() + cell = row.AddCell() + orgName := fmt.Sprint(self.config.Organization["name"]) + cell.SetValue(orgName + " 精算シート " + name) + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue("作成時刻") + cell = row.AddCell() + cell.SetValue(time.Now()) + row = sheet.AddRow() + cell = row.AddCell() +} + +func (self *SeisanReport) export() { + targetName := strings.Replace(self.config.Target, "/", "-", -1) + + xlsx.SetDefaultFont(11, "MS Pゴシック") + + file := xlsx.NewFile() + sheet, err := file.AddSheet("精算シート") + if err != nil { + log.Fatal("ERROR: ", err.Error()) + } + + self.renderReportHeader(sheet, targetName) + self.expenseReport.render(sheet) + + destPath := filepath.Join("output", targetName+".xlsx") + if _, err := os.Stat("output"); os.IsNotExist(err) { + if err := os.Mkdir("output", 0777); err != nil { + log.Fatal("ERROR: ", err.Error()) + } + } + err = file.Save(destPath) + if err != nil { + log.Fatal("ERROR: ", err.Error()) + } + fmt.Printf("Wrote to %s\n", destPath) +} diff --git a/seisan_request.go b/seisan_request.go new file mode 100644 index 0000000..9723eb8 --- /dev/null +++ b/seisan_request.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +type SeisanRequest struct { + Applicant string + Expenses []Expense `yaml:"expense"` +} + +func loadSeisanRequests(targetPath string) []SeisanRequest { + entries, err := ioutil.ReadDir(targetPath) + if err != nil { + log.Fatal("ERROR: ", err.Error()) + } + requests := []SeisanRequest{} + + for _, entry := range entries { + entryPath := filepath.Join(targetPath, entry.Name()) + buf, err := ioutil.ReadFile(entryPath) + if err != nil { + log.Fatal("ERROR: ", err.Error()) + } + var req SeisanRequest + err = yaml.Unmarshal(buf, &req) + if err != nil { + log.Fatal("ERROR: ", err.Error()) + } + requests = append(requests, req) + } + fmt.Printf("Loaded %d files\n", len(entries)) + + return requests +} From 71097220a9bd331ef75599413b535d26def21504 Mon Sep 17 00:00:00 2001 From: Yoji Shidara Date: Wed, 11 Nov 2015 10:23:22 +0900 Subject: [PATCH 03/18] Extract loadSeisanRequest() --- seisan_request.go | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/seisan_request.go b/seisan_request.go index 9723eb8..fa52466 100644 --- a/seisan_request.go +++ b/seisan_request.go @@ -14,6 +14,19 @@ type SeisanRequest struct { Expenses []Expense `yaml:"expense"` } +func loadSeisanRequest(srcPath string) (*SeisanRequest, error) { + buf, err := ioutil.ReadFile(srcPath) + if err != nil { + return nil, err + } + var req SeisanRequest + err = yaml.Unmarshal(buf, &req) + if err != nil { + return nil, err + } + return &req, nil +} + func loadSeisanRequests(targetPath string) []SeisanRequest { entries, err := ioutil.ReadDir(targetPath) if err != nil { @@ -22,17 +35,12 @@ func loadSeisanRequests(targetPath string) []SeisanRequest { requests := []SeisanRequest{} for _, entry := range entries { - entryPath := filepath.Join(targetPath, entry.Name()) - buf, err := ioutil.ReadFile(entryPath) - if err != nil { - log.Fatal("ERROR: ", err.Error()) - } - var req SeisanRequest - err = yaml.Unmarshal(buf, &req) + path := filepath.Join(targetPath, entry.Name()) + request, err := loadSeisanRequest(path) if err != nil { log.Fatal("ERROR: ", err.Error()) } - requests = append(requests, req) + requests = append(requests, *request) } fmt.Printf("Loaded %d files\n", len(entries)) From 7c2eeac268e73c441ed874e2b03d8a2bbaab6907 Mon Sep 17 00:00:00 2001 From: Yoji Shidara Date: Wed, 11 Nov 2015 10:24:28 +0900 Subject: [PATCH 04/18] Do not call log.Fatal() in loadSeisanRequests() --- main.go | 6 +++++- seisan_request.go | 9 ++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index 3458111..1d68067 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "log" "os" "path/filepath" @@ -18,7 +19,10 @@ func main() { config.mergeCliArgs(args) fmt.Printf("Processing %s ...\n", config.Target) - seisanRequests := loadSeisanRequests(filepath.Join("data", config.Target)) + seisanRequests, err := loadSeisanRequests(filepath.Join("data", config.Target)) + if err != nil { + log.Fatal(err) + } seisanReport := newSeisanReport(seisanRequests, config) seisanReport.export() } else { diff --git a/seisan_request.go b/seisan_request.go index fa52466..cc212c8 100644 --- a/seisan_request.go +++ b/seisan_request.go @@ -3,7 +3,6 @@ package main import ( "fmt" "io/ioutil" - "log" "path/filepath" "gopkg.in/yaml.v2" @@ -27,10 +26,10 @@ func loadSeisanRequest(srcPath string) (*SeisanRequest, error) { return &req, nil } -func loadSeisanRequests(targetPath string) []SeisanRequest { +func loadSeisanRequests(targetPath string) ([]SeisanRequest, error) { entries, err := ioutil.ReadDir(targetPath) if err != nil { - log.Fatal("ERROR: ", err.Error()) + return nil, err } requests := []SeisanRequest{} @@ -38,11 +37,11 @@ func loadSeisanRequests(targetPath string) []SeisanRequest { path := filepath.Join(targetPath, entry.Name()) request, err := loadSeisanRequest(path) if err != nil { - log.Fatal("ERROR: ", err.Error()) + return nil, err } requests = append(requests, *request) } fmt.Printf("Loaded %d files\n", len(entries)) - return requests + return requests, nil } From 45896fa8a2f3b0a5aa91950e4b37b809f70a2ca2 Mon Sep 17 00:00:00 2001 From: Yoji Shidara Date: Wed, 11 Nov 2015 10:26:43 +0900 Subject: [PATCH 05/18] Do not call log.Fatal() in loadConfig() --- config.go | 11 +++++------ main.go | 7 +++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/config.go b/config.go index 4ed3cfb..f1f5aeb 100644 --- a/config.go +++ b/config.go @@ -2,7 +2,6 @@ package main import ( "io/ioutil" - "log" "os" "github.com/codegangsta/cli" @@ -19,21 +18,21 @@ func (self *Config) mergeCliArgs(args cli.Args) { self.Target = args.First() } -func loadConfig(configPath string) Config { +func loadConfig(configPath string) (*Config, error) { var config Config _, err := os.Stat(configPath) if err != nil { - log.Fatal("ERROR: ", err.Error()) + return nil, err } buf, err := ioutil.ReadFile(configPath) if err != nil { - log.Fatal("ERROR: ", err.Error()) + return nil, err } err = yaml.Unmarshal(buf, &config) if err != nil { - log.Fatal("ERROR: ", err.Error()) + return nil, err } - return config + return &config, nil } diff --git a/main.go b/main.go index 1d68067..0a7041e 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,10 @@ func main() { app.Usage = "Generate seisan report" app.Action = func(c *cli.Context) { if args := c.Args(); args.Present() { - config := loadConfig("config.yaml") + config, err := loadConfig("config.yaml") + if err != nil { + log.Fatal(err) + } config.mergeCliArgs(args) fmt.Printf("Processing %s ...\n", config.Target) @@ -23,7 +26,7 @@ func main() { if err != nil { log.Fatal(err) } - seisanReport := newSeisanReport(seisanRequests, config) + seisanReport := newSeisanReport(seisanRequests, *config) seisanReport.export() } else { fmt.Println("You must specify the 'TARGET'.\nExample:\n % seisan 2015/10") From 0fae7c72db9310c2bc6273eaeb6ec937b47cff4e Mon Sep 17 00:00:00 2001 From: Yoji Shidara Date: Wed, 11 Nov 2015 10:28:08 +0900 Subject: [PATCH 06/18] Do not call log.Fatal() in export() --- main.go | 4 +++- seisan_report.go | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index 0a7041e..948a743 100644 --- a/main.go +++ b/main.go @@ -27,7 +27,9 @@ func main() { log.Fatal(err) } seisanReport := newSeisanReport(seisanRequests, *config) - seisanReport.export() + if err := seisanReport.export(); err != nil { + log.Fatal(err) + } } else { fmt.Println("You must specify the 'TARGET'.\nExample:\n % seisan 2015/10") } diff --git a/seisan_report.go b/seisan_report.go index 13520ee..262a93f 100644 --- a/seisan_report.go +++ b/seisan_report.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "log" "os" "path/filepath" "strings" @@ -40,7 +39,7 @@ func (self *SeisanReport) renderReportHeader(sheet *xlsx.Sheet, name string) { cell = row.AddCell() } -func (self *SeisanReport) export() { +func (self *SeisanReport) export() error { targetName := strings.Replace(self.config.Target, "/", "-", -1) xlsx.SetDefaultFont(11, "MS Pゴシック") @@ -48,7 +47,7 @@ func (self *SeisanReport) export() { file := xlsx.NewFile() sheet, err := file.AddSheet("精算シート") if err != nil { - log.Fatal("ERROR: ", err.Error()) + return err } self.renderReportHeader(sheet, targetName) @@ -57,12 +56,13 @@ func (self *SeisanReport) export() { destPath := filepath.Join("output", targetName+".xlsx") if _, err := os.Stat("output"); os.IsNotExist(err) { if err := os.Mkdir("output", 0777); err != nil { - log.Fatal("ERROR: ", err.Error()) + return err } } err = file.Save(destPath) if err != nil { - log.Fatal("ERROR: ", err.Error()) + return err } fmt.Printf("Wrote to %s\n", destPath) + return nil } From c070ac8c1aa51f25f7510e14830b814fa05afd70 Mon Sep 17 00:00:00 2001 From: Yoji Shidara Date: Wed, 11 Nov 2015 10:39:33 +0900 Subject: [PATCH 07/18] Decouple config and cli --- config.go | 6 ++---- main.go | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/config.go b/config.go index f1f5aeb..31053b0 100644 --- a/config.go +++ b/config.go @@ -4,8 +4,6 @@ import ( "io/ioutil" "os" - "github.com/codegangsta/cli" - "gopkg.in/yaml.v2" ) @@ -14,8 +12,8 @@ type Config struct { Organization map[string]string } -func (self *Config) mergeCliArgs(args cli.Args) { - self.Target = args.First() +func (self *Config) SetTarget(target string) { + self.Target = target } func loadConfig(configPath string) (*Config, error) { diff --git a/main.go b/main.go index 948a743..389a7da 100644 --- a/main.go +++ b/main.go @@ -19,7 +19,7 @@ func main() { if err != nil { log.Fatal(err) } - config.mergeCliArgs(args) + config.SetTarget(args.First()) fmt.Printf("Processing %s ...\n", config.Target) seisanRequests, err := loadSeisanRequests(filepath.Join("data", config.Target)) From 53f0bc15c9bcf240778bdd9eda224b90be05af03 Mon Sep 17 00:00:00 2001 From: Yoji Shidara Date: Wed, 11 Nov 2015 10:42:26 +0900 Subject: [PATCH 08/18] Extract config package --- config.go => config/config.go | 4 ++-- main.go | 12 +++++++----- seisan_report.go | 8 +++++--- 3 files changed, 14 insertions(+), 10 deletions(-) rename config.go => config/config.go (87%) diff --git a/config.go b/config/config.go similarity index 87% rename from config.go rename to config/config.go index 31053b0..7ee1498 100644 --- a/config.go +++ b/config/config.go @@ -1,4 +1,4 @@ -package main +package config import ( "io/ioutil" @@ -16,7 +16,7 @@ func (self *Config) SetTarget(target string) { self.Target = target } -func loadConfig(configPath string) (*Config, error) { +func Load(configPath string) (*Config, error) { var config Config _, err := os.Stat(configPath) diff --git a/main.go b/main.go index 389a7da..6ef210b 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,8 @@ import ( "path/filepath" "github.com/codegangsta/cli" + + "github.com/enishitech/seisan/config" ) func main() { @@ -15,18 +17,18 @@ func main() { app.Usage = "Generate seisan report" app.Action = func(c *cli.Context) { if args := c.Args(); args.Present() { - config, err := loadConfig("config.yaml") + conf, err := config.Load("config.yaml") if err != nil { log.Fatal(err) } - config.SetTarget(args.First()) + conf.SetTarget(args.First()) - fmt.Printf("Processing %s ...\n", config.Target) - seisanRequests, err := loadSeisanRequests(filepath.Join("data", config.Target)) + fmt.Printf("Processing %s ...\n", conf.Target) + seisanRequests, err := loadSeisanRequests(filepath.Join("data", conf.Target)) if err != nil { log.Fatal(err) } - seisanReport := newSeisanReport(seisanRequests, *config) + seisanReport := newSeisanReport(seisanRequests, *conf) if err := seisanReport.export(); err != nil { log.Fatal(err) } diff --git a/seisan_report.go b/seisan_report.go index 262a93f..aa72e29 100644 --- a/seisan_report.go +++ b/seisan_report.go @@ -8,16 +8,18 @@ import ( "time" "github.com/tealeg/xlsx" + + "github.com/enishitech/seisan/config" ) type SeisanReport struct { - config Config + config config.Config expenseReport *ExpenseReport } -func newSeisanReport(seisanRequests []SeisanRequest, config Config) *SeisanReport { +func newSeisanReport(seisanRequests []SeisanRequest, conf config.Config) *SeisanReport { report := &SeisanReport{} - report.config = config + report.config = conf report.expenseReport = newExpenseReport(seisanRequests) return report } From 153a7706dc7ce10458e69da4fa0136b12b249a86 Mon Sep 17 00:00:00 2001 From: Yoji Shidara Date: Wed, 11 Nov 2015 10:53:28 +0900 Subject: [PATCH 09/18] Extract expense package and expense.Entry --- expense.go => expense/expense.go | 4 ++-- expense_report.go | 8 +++++--- seisan_request.go | 4 +++- 3 files changed, 10 insertions(+), 6 deletions(-) rename expense.go => expense/expense.go (66%) diff --git a/expense.go b/expense/expense.go similarity index 66% rename from expense.go rename to expense/expense.go index 7ba41de..b1b8773 100644 --- a/expense.go +++ b/expense/expense.go @@ -1,6 +1,6 @@ -package main +package expense -type Expense struct { +type Entry struct { Date string Applicant string Amount int diff --git a/expense_report.go b/expense_report.go index ccc6247..51394a6 100644 --- a/expense_report.go +++ b/expense_report.go @@ -5,11 +5,13 @@ import ( "sort" "github.com/tealeg/xlsx" + + "github.com/enishitech/seisan/expense" ) type ExpenseReport struct { summary map[string]int - lines []Expense + lines []expense.Entry } func newExpenseReport(seisanRequests []SeisanRequest) *ExpenseReport { @@ -32,14 +34,14 @@ func (self *ExpenseReport) makeSummary(seisanRequests []SeisanRequest) { } } -type ByDate []Expense +type ByDate []expense.Entry func (a ByDate) Len() int { return len(a) } func (a ByDate) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByDate) Less(i, j int) bool { return a[i].Date < a[j].Date } func (self *ExpenseReport) makeLines(in []SeisanRequest) { - self.lines = []Expense{} + self.lines = []expense.Entry{} for _, seisanRequest := range in { for _, expense := range seisanRequest.Expenses { expense.Applicant = seisanRequest.Applicant diff --git a/seisan_request.go b/seisan_request.go index cc212c8..01ff8f1 100644 --- a/seisan_request.go +++ b/seisan_request.go @@ -6,11 +6,13 @@ import ( "path/filepath" "gopkg.in/yaml.v2" + + "github.com/enishitech/seisan/expense" ) type SeisanRequest struct { Applicant string - Expenses []Expense `yaml:"expense"` + Expenses []expense.Entry `yaml:"expense"` } func loadSeisanRequest(srcPath string) (*SeisanRequest, error) { From feb327683b2b3268f2d9ddb950a8878a86de11fb Mon Sep 17 00:00:00 2001 From: Yoji Shidara Date: Wed, 11 Nov 2015 11:37:50 +0900 Subject: [PATCH 10/18] Introduce reporter --- main.go | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 6ef210b..ce62e73 100644 --- a/main.go +++ b/main.go @@ -11,10 +11,48 @@ import ( "github.com/enishitech/seisan/config" ) +type Reporter interface { + Report(*config.Config, []SeisanRequest) error +} + +type ExpenseReporter struct{} + +func NewExpenseReporter() *ExpenseReporter { + return &ExpenseReporter{} +} + +func (reporter ExpenseReporter) Report(conf *config.Config, requests []SeisanRequest) error { + seisanReport := newSeisanReport(requests, *conf) + return seisanReport.export() +} + +type SeisanReporter struct { + reporters []Reporter +} + +func NewSeisanReporter(reporters ...Reporter) *SeisanReporter { + sr := &SeisanReporter{} + sr.reporters = reporters + return sr +} + +func (sr SeisanReporter) Report(conf *config.Config, requests []SeisanRequest) error { + for _, r := range sr.reporters { + err := r.Report(conf, requests) + if err != nil { + return err + } + } + return nil +} + func main() { app := cli.NewApp() app.Name = "seisan" app.Usage = "Generate seisan report" + + sr := NewSeisanReporter(*NewExpenseReporter()) + app.Action = func(c *cli.Context) { if args := c.Args(); args.Present() { conf, err := config.Load("config.yaml") @@ -28,8 +66,7 @@ func main() { if err != nil { log.Fatal(err) } - seisanReport := newSeisanReport(seisanRequests, *conf) - if err := seisanReport.export(); err != nil { + if err := sr.Report(conf, seisanRequests); err != nil { log.Fatal(err) } } else { From 2729f72df07dbc73e3014db04c1e2096dea1c50a Mon Sep 17 00:00:00 2001 From: Yoji Shidara Date: Wed, 11 Nov 2015 12:19:29 +0900 Subject: [PATCH 11/18] Pluggable reporter --- expense_report.go | 107 ---------------------------------- main.go | 142 ++++++++++++++++++++++++++++++++++++++++++--- request/request.go | 39 +++++++++++++ seisan_report.go | 70 ---------------------- seisan_request.go | 49 ---------------- 5 files changed, 173 insertions(+), 234 deletions(-) delete mode 100644 expense_report.go create mode 100644 request/request.go delete mode 100644 seisan_report.go delete mode 100644 seisan_request.go diff --git a/expense_report.go b/expense_report.go deleted file mode 100644 index 51394a6..0000000 --- a/expense_report.go +++ /dev/null @@ -1,107 +0,0 @@ -package main - -import ( - "fmt" - "sort" - - "github.com/tealeg/xlsx" - - "github.com/enishitech/seisan/expense" -) - -type ExpenseReport struct { - summary map[string]int - lines []expense.Entry -} - -func newExpenseReport(seisanRequests []SeisanRequest) *ExpenseReport { - report := &ExpenseReport{} - report.makeSummary(seisanRequests) - report.makeLines(seisanRequests) - return report -} - -func (self *ExpenseReport) makeSummary(seisanRequests []SeisanRequest) { - self.summary = map[string]int{} - for _, seisanRequest := range seisanRequests { - for _, expense := range seisanRequest.Expenses { - if _, exists := self.summary[seisanRequest.Applicant]; exists { - self.summary[seisanRequest.Applicant] = expense.Amount - } else { - self.summary[seisanRequest.Applicant] += expense.Amount - } - } - } -} - -type ByDate []expense.Entry - -func (a ByDate) Len() int { return len(a) } -func (a ByDate) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ByDate) Less(i, j int) bool { return a[i].Date < a[j].Date } - -func (self *ExpenseReport) makeLines(in []SeisanRequest) { - self.lines = []expense.Entry{} - for _, seisanRequest := range in { - for _, expense := range seisanRequest.Expenses { - expense.Applicant = seisanRequest.Applicant - self.lines = append(self.lines, expense) - } - } - sort.Sort(ByDate(self.lines)) - fmt.Printf("Processed %d expenses\n", len(self.lines)) -} - -func (self *ExpenseReport) renderSummary(sheet *xlsx.Sheet) { - var row *xlsx.Row - var cell *xlsx.Cell - - row = sheet.AddRow() - cell = row.AddCell() - cell.SetValue("立替払サマリー") - row = sheet.AddRow() - for _, heading := range []string{"氏名", "金額"} { - cell = row.AddCell() - cell.SetValue(heading) - } - for key, value := range self.summary { - row = sheet.AddRow() - cell = row.AddCell() - cell.SetValue(key) - cell = row.AddCell() - cell.SetValue(value) - } - row = sheet.AddRow() - cell = row.AddCell() - cell.SetValue("") -} - -func (self *ExpenseReport) renderLines(sheet *xlsx.Sheet) { - var row *xlsx.Row - var cell *xlsx.Cell - - row = sheet.AddRow() - cell = row.AddCell() - cell.SetValue("立替払明細") - row = sheet.AddRow() - for _, heading := range []string{"日付", "立替者", "金額", "摘要", "備考"} { - cell = row.AddCell() - cell.SetValue(heading) - } - for _, detail := range self.lines { - row = sheet.AddRow() - cell = row.AddCell() - cell.SetValue(detail.Date) - cell = row.AddCell() - cell.SetValue(detail.Applicant) - cell = row.AddCell() - cell.SetValue(detail.Amount) - cell = row.AddCell() - cell.SetValue(detail.Remarks) - } -} - -func (self *ExpenseReport) render(sheet *xlsx.Sheet) { - self.renderSummary(sheet) - self.renderLines(sheet) -} diff --git a/main.go b/main.go index ce62e73..d273def 100644 --- a/main.go +++ b/main.go @@ -5,14 +5,20 @@ import ( "log" "os" "path/filepath" + "sort" + "strings" + "time" "github.com/codegangsta/cli" + "github.com/tealeg/xlsx" "github.com/enishitech/seisan/config" + "github.com/enishitech/seisan/expense" + "github.com/enishitech/seisan/request" ) type Reporter interface { - Report(*config.Config, []SeisanRequest) error + Report(*xlsx.Sheet, *config.Config, []request.Request) error } type ExpenseReporter struct{} @@ -21,9 +27,89 @@ func NewExpenseReporter() *ExpenseReporter { return &ExpenseReporter{} } -func (reporter ExpenseReporter) Report(conf *config.Config, requests []SeisanRequest) error { - seisanReport := newSeisanReport(requests, *conf) - return seisanReport.export() +type ExpenseRequest struct { + Applicant string `yaml:"applicant"` + ExpeneseEntries []expense.Entry `yaml:"expense"` +} + +func (reporter ExpenseReporter) renderSummary(sheet *xlsx.Sheet, sumByApplicant map[string]int) { + var row *xlsx.Row + var cell *xlsx.Cell + + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue("立替払サマリー") + row = sheet.AddRow() + for _, heading := range []string{"氏名", "金額"} { + cell = row.AddCell() + cell.SetValue(heading) + } + for key, value := range sumByApplicant { + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue(key) + cell = row.AddCell() + cell.SetValue(value) + } + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue("") +} + +type ByDate []expense.Entry + +func (a ByDate) Len() int { return len(a) } +func (a ByDate) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByDate) Less(i, j int) bool { return a[i].Date < a[j].Date } + +func (reporter ExpenseReporter) renderEntries(sheet *xlsx.Sheet, entries []expense.Entry) { + var row *xlsx.Row + var cell *xlsx.Cell + + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue("立替払明細") + row = sheet.AddRow() + for _, heading := range []string{"日付", "立替者", "金額", "摘要", "備考"} { + cell = row.AddCell() + cell.SetValue(heading) + } + for _, detail := range entries { + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue(detail.Date) + cell = row.AddCell() + cell.SetValue(detail.Applicant) + cell = row.AddCell() + cell.SetValue(detail.Amount) + cell = row.AddCell() + cell.SetValue(detail.Remarks) + } +} + +func (reporter ExpenseReporter) Report(sheet *xlsx.Sheet, conf *config.Config, requests []request.Request) error { + entries := make([]expense.Entry, 0) + sumByApplicant := make(map[string]int) + for _, req := range requests { + var er ExpenseRequest + if err := req.Unmarshal(&er); err != nil { + return err + } + if _, ok := sumByApplicant[er.Applicant]; !ok { + sumByApplicant[er.Applicant] = 0 + } + for _, entry := range er.ExpeneseEntries { + entry.Applicant = er.Applicant + sumByApplicant[er.Applicant] += entry.Amount + entries = append(entries, entry) + } + } + sort.Sort(ByDate(entries)) + + reporter.renderSummary(sheet, sumByApplicant) + reporter.renderEntries(sheet, entries) + + return nil } type SeisanReporter struct { @@ -36,13 +122,53 @@ func NewSeisanReporter(reporters ...Reporter) *SeisanReporter { return sr } -func (sr SeisanReporter) Report(conf *config.Config, requests []SeisanRequest) error { +func renderReportHeader(sheet *xlsx.Sheet, orgName, name string) { + var row *xlsx.Row + var cell *xlsx.Cell + + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue(orgName + " 精算シート " + name) + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue("作成時刻") + cell = row.AddCell() + cell.SetValue(time.Now()) + row = sheet.AddRow() + cell = row.AddCell() +} + +func (sr SeisanReporter) Report(conf *config.Config, requests []request.Request) error { + targetName := strings.Replace(conf.Target, "/", "-", -1) + xlsx.SetDefaultFont(11, "MS Pゴシック") + + file := xlsx.NewFile() + sheet, err := file.AddSheet("精算シート") + if err != nil { + return err + } + + renderReportHeader(sheet, targetName, conf.Organization["name"]) + for _, r := range sr.reporters { - err := r.Report(conf, requests) + err := r.Report(sheet, conf, requests) if err != nil { return err } } + + destPath := filepath.Join("output", targetName+".xlsx") + if _, err := os.Stat("output"); os.IsNotExist(err) { + if err := os.Mkdir("output", 0777); err != nil { + return err + } + } + err = file.Save(destPath) + if err != nil { + return err + } + fmt.Printf("Wrote to %s\n", destPath) + return nil } @@ -62,11 +188,11 @@ func main() { conf.SetTarget(args.First()) fmt.Printf("Processing %s ...\n", conf.Target) - seisanRequests, err := loadSeisanRequests(filepath.Join("data", conf.Target)) + reqs, err := request.LoadDir(filepath.Join("data", conf.Target)) if err != nil { log.Fatal(err) } - if err := sr.Report(conf, seisanRequests); err != nil { + if err := sr.Report(conf, reqs); err != nil { log.Fatal(err) } } else { diff --git a/request/request.go b/request/request.go new file mode 100644 index 0000000..700e113 --- /dev/null +++ b/request/request.go @@ -0,0 +1,39 @@ +package request + +import ( + "fmt" + "io/ioutil" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +type Request struct { + Data []byte +} + +func (req *Request) Unmarshal(v interface{}) error { + return yaml.Unmarshal(req.Data, v) +} + +func LoadDir(dir string) ([]Request, error) { + entries, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + requests := []Request{} + + for _, entry := range entries { + path := filepath.Join(dir, entry.Name()) + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + request := Request{Data: data} + requests = append(requests, request) + } + fmt.Printf("Loaded %d files\n", len(entries)) + + return requests, nil +} diff --git a/seisan_report.go b/seisan_report.go deleted file mode 100644 index aa72e29..0000000 --- a/seisan_report.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "time" - - "github.com/tealeg/xlsx" - - "github.com/enishitech/seisan/config" -) - -type SeisanReport struct { - config config.Config - expenseReport *ExpenseReport -} - -func newSeisanReport(seisanRequests []SeisanRequest, conf config.Config) *SeisanReport { - report := &SeisanReport{} - report.config = conf - report.expenseReport = newExpenseReport(seisanRequests) - return report -} - -func (self *SeisanReport) renderReportHeader(sheet *xlsx.Sheet, name string) { - var row *xlsx.Row - var cell *xlsx.Cell - - row = sheet.AddRow() - cell = row.AddCell() - orgName := fmt.Sprint(self.config.Organization["name"]) - cell.SetValue(orgName + " 精算シート " + name) - row = sheet.AddRow() - cell = row.AddCell() - cell.SetValue("作成時刻") - cell = row.AddCell() - cell.SetValue(time.Now()) - row = sheet.AddRow() - cell = row.AddCell() -} - -func (self *SeisanReport) export() error { - targetName := strings.Replace(self.config.Target, "/", "-", -1) - - xlsx.SetDefaultFont(11, "MS Pゴシック") - - file := xlsx.NewFile() - sheet, err := file.AddSheet("精算シート") - if err != nil { - return err - } - - self.renderReportHeader(sheet, targetName) - self.expenseReport.render(sheet) - - destPath := filepath.Join("output", targetName+".xlsx") - if _, err := os.Stat("output"); os.IsNotExist(err) { - if err := os.Mkdir("output", 0777); err != nil { - return err - } - } - err = file.Save(destPath) - if err != nil { - return err - } - fmt.Printf("Wrote to %s\n", destPath) - return nil -} diff --git a/seisan_request.go b/seisan_request.go deleted file mode 100644 index 01ff8f1..0000000 --- a/seisan_request.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "fmt" - "io/ioutil" - "path/filepath" - - "gopkg.in/yaml.v2" - - "github.com/enishitech/seisan/expense" -) - -type SeisanRequest struct { - Applicant string - Expenses []expense.Entry `yaml:"expense"` -} - -func loadSeisanRequest(srcPath string) (*SeisanRequest, error) { - buf, err := ioutil.ReadFile(srcPath) - if err != nil { - return nil, err - } - var req SeisanRequest - err = yaml.Unmarshal(buf, &req) - if err != nil { - return nil, err - } - return &req, nil -} - -func loadSeisanRequests(targetPath string) ([]SeisanRequest, error) { - entries, err := ioutil.ReadDir(targetPath) - if err != nil { - return nil, err - } - requests := []SeisanRequest{} - - for _, entry := range entries { - path := filepath.Join(targetPath, entry.Name()) - request, err := loadSeisanRequest(path) - if err != nil { - return nil, err - } - requests = append(requests, *request) - } - fmt.Printf("Loaded %d files\n", len(entries)) - - return requests, nil -} From 7a0f6334c38dffcfb2f10222fee3205f8291eae0 Mon Sep 17 00:00:00 2001 From: Yoji Shidara Date: Wed, 11 Nov 2015 12:24:03 +0900 Subject: [PATCH 12/18] Move ExpenseReporter under expense --- expense/reporter.go | 101 ++++++++++++++++++++++++++++++++++++++++++++ main.go | 94 +---------------------------------------- 2 files changed, 102 insertions(+), 93 deletions(-) create mode 100644 expense/reporter.go diff --git a/expense/reporter.go b/expense/reporter.go new file mode 100644 index 0000000..2177a0d --- /dev/null +++ b/expense/reporter.go @@ -0,0 +1,101 @@ +package expense + +import ( + "sort" + + "github.com/tealeg/xlsx" + + "github.com/enishitech/seisan/config" + "github.com/enishitech/seisan/request" +) + +type ExpenseReporter struct{} + +func NewReporter() *ExpenseReporter { + return &ExpenseReporter{} +} + +type ExpenseRequest struct { + Applicant string `yaml:"applicant"` + ExpeneseEntries []Entry `yaml:"expense"` +} + +func (reporter ExpenseReporter) renderSummary(sheet *xlsx.Sheet, sumByApplicant map[string]int) { + var row *xlsx.Row + var cell *xlsx.Cell + + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue("立替払サマリー") + row = sheet.AddRow() + for _, heading := range []string{"氏名", "金額"} { + cell = row.AddCell() + cell.SetValue(heading) + } + for key, value := range sumByApplicant { + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue(key) + cell = row.AddCell() + cell.SetValue(value) + } + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue("") +} + +type ByDate []Entry + +func (a ByDate) Len() int { return len(a) } +func (a ByDate) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByDate) Less(i, j int) bool { return a[i].Date < a[j].Date } + +func (reporter ExpenseReporter) renderEntries(sheet *xlsx.Sheet, entries []Entry) { + var row *xlsx.Row + var cell *xlsx.Cell + + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue("立替払明細") + row = sheet.AddRow() + for _, heading := range []string{"日付", "立替者", "金額", "摘要", "備考"} { + cell = row.AddCell() + cell.SetValue(heading) + } + for _, detail := range entries { + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue(detail.Date) + cell = row.AddCell() + cell.SetValue(detail.Applicant) + cell = row.AddCell() + cell.SetValue(detail.Amount) + cell = row.AddCell() + cell.SetValue(detail.Remarks) + } +} + +func (reporter ExpenseReporter) Report(sheet *xlsx.Sheet, conf *config.Config, requests []request.Request) error { + entries := make([]Entry, 0) + sumByApplicant := make(map[string]int) + for _, req := range requests { + var er ExpenseRequest + if err := req.Unmarshal(&er); err != nil { + return err + } + if _, ok := sumByApplicant[er.Applicant]; !ok { + sumByApplicant[er.Applicant] = 0 + } + for _, entry := range er.ExpeneseEntries { + entry.Applicant = er.Applicant + sumByApplicant[er.Applicant] += entry.Amount + entries = append(entries, entry) + } + } + sort.Sort(ByDate(entries)) + + reporter.renderSummary(sheet, sumByApplicant) + reporter.renderEntries(sheet, entries) + + return nil +} diff --git a/main.go b/main.go index d273def..0a48943 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,6 @@ import ( "log" "os" "path/filepath" - "sort" "strings" "time" @@ -21,97 +20,6 @@ type Reporter interface { Report(*xlsx.Sheet, *config.Config, []request.Request) error } -type ExpenseReporter struct{} - -func NewExpenseReporter() *ExpenseReporter { - return &ExpenseReporter{} -} - -type ExpenseRequest struct { - Applicant string `yaml:"applicant"` - ExpeneseEntries []expense.Entry `yaml:"expense"` -} - -func (reporter ExpenseReporter) renderSummary(sheet *xlsx.Sheet, sumByApplicant map[string]int) { - var row *xlsx.Row - var cell *xlsx.Cell - - row = sheet.AddRow() - cell = row.AddCell() - cell.SetValue("立替払サマリー") - row = sheet.AddRow() - for _, heading := range []string{"氏名", "金額"} { - cell = row.AddCell() - cell.SetValue(heading) - } - for key, value := range sumByApplicant { - row = sheet.AddRow() - cell = row.AddCell() - cell.SetValue(key) - cell = row.AddCell() - cell.SetValue(value) - } - row = sheet.AddRow() - cell = row.AddCell() - cell.SetValue("") -} - -type ByDate []expense.Entry - -func (a ByDate) Len() int { return len(a) } -func (a ByDate) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ByDate) Less(i, j int) bool { return a[i].Date < a[j].Date } - -func (reporter ExpenseReporter) renderEntries(sheet *xlsx.Sheet, entries []expense.Entry) { - var row *xlsx.Row - var cell *xlsx.Cell - - row = sheet.AddRow() - cell = row.AddCell() - cell.SetValue("立替払明細") - row = sheet.AddRow() - for _, heading := range []string{"日付", "立替者", "金額", "摘要", "備考"} { - cell = row.AddCell() - cell.SetValue(heading) - } - for _, detail := range entries { - row = sheet.AddRow() - cell = row.AddCell() - cell.SetValue(detail.Date) - cell = row.AddCell() - cell.SetValue(detail.Applicant) - cell = row.AddCell() - cell.SetValue(detail.Amount) - cell = row.AddCell() - cell.SetValue(detail.Remarks) - } -} - -func (reporter ExpenseReporter) Report(sheet *xlsx.Sheet, conf *config.Config, requests []request.Request) error { - entries := make([]expense.Entry, 0) - sumByApplicant := make(map[string]int) - for _, req := range requests { - var er ExpenseRequest - if err := req.Unmarshal(&er); err != nil { - return err - } - if _, ok := sumByApplicant[er.Applicant]; !ok { - sumByApplicant[er.Applicant] = 0 - } - for _, entry := range er.ExpeneseEntries { - entry.Applicant = er.Applicant - sumByApplicant[er.Applicant] += entry.Amount - entries = append(entries, entry) - } - } - sort.Sort(ByDate(entries)) - - reporter.renderSummary(sheet, sumByApplicant) - reporter.renderEntries(sheet, entries) - - return nil -} - type SeisanReporter struct { reporters []Reporter } @@ -177,7 +85,7 @@ func main() { app.Name = "seisan" app.Usage = "Generate seisan report" - sr := NewSeisanReporter(*NewExpenseReporter()) + sr := NewSeisanReporter(*expense.NewReporter()) app.Action = func(c *cli.Context) { if args := c.Args(); args.Present() { From 5f7e9bd7825699b110f1cae1756c3d9dca3eaae0 Mon Sep 17 00:00:00 2001 From: Yoji Shidara Date: Wed, 11 Nov 2015 12:26:47 +0900 Subject: [PATCH 13/18] Extract reporter package --- main.go | 70 ++------------------------------------- reporter/reporter.go | 78 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 68 deletions(-) create mode 100644 reporter/reporter.go diff --git a/main.go b/main.go index 0a48943..7dab527 100644 --- a/main.go +++ b/main.go @@ -5,87 +5,21 @@ import ( "log" "os" "path/filepath" - "strings" - "time" "github.com/codegangsta/cli" - "github.com/tealeg/xlsx" "github.com/enishitech/seisan/config" "github.com/enishitech/seisan/expense" + "github.com/enishitech/seisan/reporter" "github.com/enishitech/seisan/request" ) -type Reporter interface { - Report(*xlsx.Sheet, *config.Config, []request.Request) error -} - -type SeisanReporter struct { - reporters []Reporter -} - -func NewSeisanReporter(reporters ...Reporter) *SeisanReporter { - sr := &SeisanReporter{} - sr.reporters = reporters - return sr -} - -func renderReportHeader(sheet *xlsx.Sheet, orgName, name string) { - var row *xlsx.Row - var cell *xlsx.Cell - - row = sheet.AddRow() - cell = row.AddCell() - cell.SetValue(orgName + " 精算シート " + name) - row = sheet.AddRow() - cell = row.AddCell() - cell.SetValue("作成時刻") - cell = row.AddCell() - cell.SetValue(time.Now()) - row = sheet.AddRow() - cell = row.AddCell() -} - -func (sr SeisanReporter) Report(conf *config.Config, requests []request.Request) error { - targetName := strings.Replace(conf.Target, "/", "-", -1) - xlsx.SetDefaultFont(11, "MS Pゴシック") - - file := xlsx.NewFile() - sheet, err := file.AddSheet("精算シート") - if err != nil { - return err - } - - renderReportHeader(sheet, targetName, conf.Organization["name"]) - - for _, r := range sr.reporters { - err := r.Report(sheet, conf, requests) - if err != nil { - return err - } - } - - destPath := filepath.Join("output", targetName+".xlsx") - if _, err := os.Stat("output"); os.IsNotExist(err) { - if err := os.Mkdir("output", 0777); err != nil { - return err - } - } - err = file.Save(destPath) - if err != nil { - return err - } - fmt.Printf("Wrote to %s\n", destPath) - - return nil -} - func main() { app := cli.NewApp() app.Name = "seisan" app.Usage = "Generate seisan report" - sr := NewSeisanReporter(*expense.NewReporter()) + sr := reporter.New(*expense.NewReporter()) app.Action = func(c *cli.Context) { if args := c.Args(); args.Present() { diff --git a/reporter/reporter.go b/reporter/reporter.go new file mode 100644 index 0000000..13fd26b --- /dev/null +++ b/reporter/reporter.go @@ -0,0 +1,78 @@ +package reporter + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/tealeg/xlsx" + + "github.com/enishitech/seisan/config" + "github.com/enishitech/seisan/request" +) + +type Reporter interface { + Report(*xlsx.Sheet, *config.Config, []request.Request) error +} + +type SeisanReporter struct { + reporters []Reporter +} + +func New(reporters ...Reporter) *SeisanReporter { + sr := &SeisanReporter{} + sr.reporters = reporters + return sr +} + +func renderReportHeader(sheet *xlsx.Sheet, orgName, name string) { + var row *xlsx.Row + var cell *xlsx.Cell + + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue(orgName + " 精算シート " + name) + row = sheet.AddRow() + cell = row.AddCell() + cell.SetValue("作成時刻") + cell = row.AddCell() + cell.SetValue(time.Now()) + row = sheet.AddRow() + cell = row.AddCell() +} + +func (sr SeisanReporter) Report(conf *config.Config, requests []request.Request) error { + targetName := strings.Replace(conf.Target, "/", "-", -1) + xlsx.SetDefaultFont(11, "MS Pゴシック") + + file := xlsx.NewFile() + sheet, err := file.AddSheet("精算シート") + if err != nil { + return err + } + + renderReportHeader(sheet, targetName, conf.Organization["name"]) + + for _, r := range sr.reporters { + err := r.Report(sheet, conf, requests) + if err != nil { + return err + } + } + + destPath := filepath.Join("output", targetName+".xlsx") + if _, err := os.Stat("output"); os.IsNotExist(err) { + if err := os.Mkdir("output", 0777); err != nil { + return err + } + } + err = file.Save(destPath) + if err != nil { + return err + } + fmt.Printf("Wrote to %s\n", destPath) + + return nil +} From 3e8f9b73ea75883bececdb168d5494916486e875 Mon Sep 17 00:00:00 2001 From: Yoji Shidara Date: Wed, 11 Nov 2015 13:14:26 +0900 Subject: [PATCH 14/18] Do reporting things in reporter --- config/config.go | 5 ----- main.go | 17 +++-------------- reporter/reporter.go | 16 +++++++++++++--- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/config/config.go b/config/config.go index 7ee1498..fd063a1 100644 --- a/config/config.go +++ b/config/config.go @@ -8,14 +8,9 @@ import ( ) type Config struct { - Target string Organization map[string]string } -func (self *Config) SetTarget(target string) { - self.Target = target -} - func Load(configPath string) (*Config, error) { var config Config diff --git a/main.go b/main.go index 7dab527..987e891 100644 --- a/main.go +++ b/main.go @@ -4,14 +4,11 @@ import ( "fmt" "log" "os" - "path/filepath" "github.com/codegangsta/cli" - "github.com/enishitech/seisan/config" "github.com/enishitech/seisan/expense" "github.com/enishitech/seisan/reporter" - "github.com/enishitech/seisan/request" ) func main() { @@ -23,18 +20,10 @@ func main() { app.Action = func(c *cli.Context) { if args := c.Args(); args.Present() { - conf, err := config.Load("config.yaml") - if err != nil { - log.Fatal(err) - } - conf.SetTarget(args.First()) + target := args.First() - fmt.Printf("Processing %s ...\n", conf.Target) - reqs, err := request.LoadDir(filepath.Join("data", conf.Target)) - if err != nil { - log.Fatal(err) - } - if err := sr.Report(conf, reqs); err != nil { + fmt.Printf("Processing %s ...\n", target) + if err := sr.Report(".", target); err != nil { log.Fatal(err) } } else { diff --git a/reporter/reporter.go b/reporter/reporter.go index 13fd26b..12d89b2 100644 --- a/reporter/reporter.go +++ b/reporter/reporter.go @@ -43,8 +43,18 @@ func renderReportHeader(sheet *xlsx.Sheet, orgName, name string) { cell = row.AddCell() } -func (sr SeisanReporter) Report(conf *config.Config, requests []request.Request) error { - targetName := strings.Replace(conf.Target, "/", "-", -1) +func (sr SeisanReporter) Report(baseDir string, target string) error { + conf, err := config.Load(filepath.Join(baseDir, "config.yaml")) + if err != nil { + return err + } + + reqs, err := request.LoadDir(filepath.Join(baseDir, "data", target)) + if err != nil { + return err + } + + targetName := strings.Replace(target, "/", "-", -1) xlsx.SetDefaultFont(11, "MS Pゴシック") file := xlsx.NewFile() @@ -56,7 +66,7 @@ func (sr SeisanReporter) Report(conf *config.Config, requests []request.Request) renderReportHeader(sheet, targetName, conf.Organization["name"]) for _, r := range sr.reporters { - err := r.Report(sheet, conf, requests) + err := r.Report(sheet, conf, reqs) if err != nil { return err } From 6e46a72b069d0734caa6c0f8aa831ec82615a267 Mon Sep 17 00:00:00 2001 From: Yoji Shidara Date: Wed, 11 Nov 2015 13:15:45 +0900 Subject: [PATCH 15/18] Fail first --- main.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/main.go b/main.go index 987e891..13a4df0 100644 --- a/main.go +++ b/main.go @@ -19,15 +19,16 @@ func main() { sr := reporter.New(*expense.NewReporter()) app.Action = func(c *cli.Context) { - if args := c.Args(); args.Present() { - target := args.First() - - fmt.Printf("Processing %s ...\n", target) - if err := sr.Report(".", target); err != nil { - log.Fatal(err) - } - } else { + args := c.Args() + if !args.Present() { fmt.Println("You must specify the 'TARGET'.\nExample:\n % seisan 2015/10") + return + } + + target := args.First() + fmt.Printf("Processing %s ...\n", target) + if err := sr.Report(".", target); err != nil { + log.Fatal(err) } } app.Run(os.Args) From a50dbeaefcc95afbf246fdd32d575831cc72727e Mon Sep 17 00:00:00 2001 From: Yoji Shidara Date: Wed, 11 Nov 2015 13:26:32 +0900 Subject: [PATCH 16/18] Extract organization name with Unmarshal --- config/config.go | 6 +++++- reporter/reporter.go | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/config/config.go b/config/config.go index fd063a1..0885604 100644 --- a/config/config.go +++ b/config/config.go @@ -8,7 +8,11 @@ import ( ) type Config struct { - Organization map[string]string + Organization +} + +type Organization struct { + Name string } func Load(configPath string) (*Config, error) { diff --git a/reporter/reporter.go b/reporter/reporter.go index 12d89b2..eb52d2c 100644 --- a/reporter/reporter.go +++ b/reporter/reporter.go @@ -63,7 +63,7 @@ func (sr SeisanReporter) Report(baseDir string, target string) error { return err } - renderReportHeader(sheet, targetName, conf.Organization["name"]) + renderReportHeader(sheet, targetName, conf.Organization.Name) for _, r := range sr.reporters { err := r.Report(sheet, conf, reqs) From f711fe4d0352513d1b96544a91249586a4d1d0a6 Mon Sep 17 00:00:00 2001 From: Yoji Shidara Date: Wed, 11 Nov 2015 13:30:11 +0900 Subject: [PATCH 17/18] Do not rely on cwd --- reporter/reporter.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/reporter/reporter.go b/reporter/reporter.go index eb52d2c..118b262 100644 --- a/reporter/reporter.go +++ b/reporter/reporter.go @@ -72,9 +72,10 @@ func (sr SeisanReporter) Report(baseDir string, target string) error { } } - destPath := filepath.Join("output", targetName+".xlsx") - if _, err := os.Stat("output"); os.IsNotExist(err) { - if err := os.Mkdir("output", 0777); err != nil { + outputDir := filepath.Join(baseDir, "output") + destPath := filepath.Join(outputDir, targetName+".xlsx") + if _, err := os.Stat(outputDir); os.IsNotExist(err) { + if err := os.Mkdir(outputDir, 0777); err != nil { return err } } From 31b32adcec1313a2a3a4c86ff849618382ab20b7 Mon Sep 17 00:00:00 2001 From: Yoji Shidara Date: Wed, 11 Nov 2015 13:31:46 +0900 Subject: [PATCH 18/18] Prepare paths first --- reporter/reporter.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/reporter/reporter.go b/reporter/reporter.go index 118b262..6a4b580 100644 --- a/reporter/reporter.go +++ b/reporter/reporter.go @@ -44,12 +44,17 @@ func renderReportHeader(sheet *xlsx.Sheet, orgName, name string) { } func (sr SeisanReporter) Report(baseDir string, target string) error { - conf, err := config.Load(filepath.Join(baseDir, "config.yaml")) + dataDir := filepath.Join(baseDir, "data") + outputDir := filepath.Join(baseDir, "output") + configPath := filepath.Join(baseDir, "config.yaml") + targetDir := filepath.Join(dataDir, target) + + conf, err := config.Load(configPath) if err != nil { return err } - reqs, err := request.LoadDir(filepath.Join(baseDir, "data", target)) + reqs, err := request.LoadDir(targetDir) if err != nil { return err } @@ -72,7 +77,6 @@ func (sr SeisanReporter) Report(baseDir string, target string) error { } } - outputDir := filepath.Join(baseDir, "output") destPath := filepath.Join(outputDir, targetName+".xlsx") if _, err := os.Stat(outputDir); os.IsNotExist(err) { if err := os.Mkdir(outputDir, 0777); err != nil {