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/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/Rakefile b/Rakefile deleted file mode 100644 index 2995527..0000000 --- a/Rakefile +++ /dev/null @@ -1 +0,0 @@ -require "bundler/gem_tasks" diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..0885604 --- /dev/null +++ b/config/config.go @@ -0,0 +1,35 @@ +package config + +import ( + "io/ioutil" + "os" + + "gopkg.in/yaml.v2" +) + +type Config struct { + Organization +} + +type Organization struct { + Name string +} + +func Load(configPath string) (*Config, error) { + var config Config + + _, err := os.Stat(configPath) + if err != nil { + return nil, err + } + + buf, err := ioutil.ReadFile(configPath) + if err != nil { + return nil, err + } + err = yaml.Unmarshal(buf, &config) + if err != nil { + return nil, err + } + return &config, nil +} 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/expense/expense.go b/expense/expense.go new file mode 100644 index 0000000..b1b8773 --- /dev/null +++ b/expense/expense.go @@ -0,0 +1,8 @@ +package expense + +type Entry struct { + Date string + Applicant string + Amount int + Remarks string +} 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/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/main.go b/main.go new file mode 100644 index 0000000..13a4df0 --- /dev/null +++ b/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/codegangsta/cli" + + "github.com/enishitech/seisan/expense" + "github.com/enishitech/seisan/reporter" +) + +func main() { + app := cli.NewApp() + app.Name = "seisan" + app.Usage = "Generate seisan report" + + sr := reporter.New(*expense.NewReporter()) + + app.Action = func(c *cli.Context) { + 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) +} diff --git a/reporter/reporter.go b/reporter/reporter.go new file mode 100644 index 0000000..6a4b580 --- /dev/null +++ b/reporter/reporter.go @@ -0,0 +1,93 @@ +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(baseDir string, target string) error { + 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(targetDir) + if err != nil { + return err + } + + targetName := strings.Replace(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, reqs) + if err != nil { + return err + } + } + + destPath := filepath.Join(outputDir, targetName+".xlsx") + if _, err := os.Stat(outputDir); os.IsNotExist(err) { + if err := os.Mkdir(outputDir, 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/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.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