From a8585ae0355c561b5737b9014b2db9f54372d677 Mon Sep 17 00:00:00 2001 From: SHIMADA Koji Date: Fri, 6 Nov 2015 18:16:15 +0900 Subject: [PATCH] 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..ccd6611 --- /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 { + return config + } + + 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 +}