lucadeal 0.2.9 → 0.2.18

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a4958ee224643a3be3408a9b2c4043ac0ca37a1a4c7f3a537d626756a410e117
4
- data.tar.gz: 74d8ead724f3cf89b0d5163565316c9421d1f8a81675e1fa28411b6248674b43
3
+ metadata.gz: d8159099446dcdae1b4c928d4eac5861cd35fe13f8d37155cdf28dafb30fc21e
4
+ data.tar.gz: 3c086a2ea6c4c9ec6bc9014fa9500ab9fde0a07c1778a5817fbb64e7409b5670
5
5
  SHA512:
6
- metadata.gz: 8f91f2b22e69bfa5bbde799ad4664a22b19513e75bcc5bf4a0459e5f0717a945d62abd73850821b320fcd0850c0a77537872f156769e3d3693c8e6c4de24c7c5
7
- data.tar.gz: 4f333744805790545bc8ee27eb771b441a6be18dc2b635c15121a385ba7713880c22295c43e5524d1f593a55018ab506c1f56c79775db08583922594dfcab6c7
6
+ metadata.gz: 0a26cec96f1a0805044470f8710b36b60aacf344bad53527c2a9fd4ff64557a06836e4eefec21f083e0b51bc48a9854dbe2fdd6fed4d4754aba9fe51fa5890ce
7
+ data.tar.gz: 8a611e016833673fb1e7fcab747a24d4bfdb4152a60086791a1377d8576feb181255c8b2a0dfeca2f6807a4d11d4d2f579be6353ca62a7996a5f960d8b4f1311
@@ -0,0 +1,20 @@
1
+ ## LucaDeal 0.2.18
2
+
3
+ * Breaking change: restructure CLI in sub-sub command format.
4
+ * Add 'x-customer', 'x-editor' on export to LucaBook
5
+
6
+ ## LucaDeal 0.2.17
7
+
8
+ * `luca-deal export` export JSON for LucaBook
9
+
10
+ ## LucaDeal 0.2.14
11
+
12
+ * Introduce Product for selling items template.
13
+
14
+ ## LucaDeal 0.2.12
15
+
16
+ * Introduce Sales fee calculation.
17
+
18
+ ## LucaDeal 0.2.10
19
+
20
+ * items can have one time cost at initial month of contract.
data/README.md CHANGED
@@ -1,15 +1,15 @@
1
- # Luca::Deal
1
+ # LucaDeal
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/luca/deal`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ [![Gem Version](https://badge.fury.io/rb/lucadeal.svg)](https://badge.fury.io/rb/lucadeal)
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ LucaDeal is Sales contract management application.
6
6
 
7
7
  ## Installation
8
8
 
9
9
  Add this line to your application's Gemfile:
10
10
 
11
11
  ```ruby
12
- gem 'luca-deal'
12
+ gem 'lucadeal'
13
13
  ```
14
14
 
15
15
  And then execute:
@@ -18,11 +18,179 @@ And then execute:
18
18
 
19
19
  Or install it yourself as:
20
20
 
21
- $ gem install luca-deal
21
+ $ gem install lucadeal
22
22
 
23
23
  ## Usage
24
24
 
25
- TODO: Write usage instructions here
25
+ You can create project skelton with `new` sub command.
26
+
27
+ ```
28
+ $ luca-deal new Dir
29
+ ```
30
+
31
+ Example assumes setup with bundler shim. `bundle exec` prefix may be needed in most cases.
32
+
33
+
34
+ ### Manage Contract
35
+
36
+ Customer object can be created by `customer create` subcommand with name.
37
+
38
+ ```
39
+ $ luca-deal customer create CustomerName
40
+ Successfully generated Customer 5976652cc2d9c0ebf4a8646f7a28aa8d6bd2d606
41
+ Edit customer detail.
42
+ ```
43
+
44
+ Customer is filed under `data/customers` as YAML. Detail need to be editted.
45
+ Then, Contract object is created by `contract create` sub command with customer id.
46
+
47
+ ```
48
+ $ luca-deal contract create 5976652cc2d9c0ebf4a8646f7a28aa8d6bd2d606
49
+ uccessfully generated Contract 814c6fc9fffe5566fe8e7ef683b439b355d612dc
50
+ Conditions are tentative. Edit contract detail.
51
+ ```
52
+
53
+ Contract is filed under `data/contracts` as YAML. Detail need to be editted.
54
+
55
+
56
+ ### Issue invoice
57
+
58
+ Monthly invoices are generated with `invoice create --monthly` sub command. Target month is optional. Without month, this month including today is the target.
59
+
60
+ ```
61
+ $ luca-deal invoice create --monthly [yyyy m]
62
+ ```
63
+
64
+ Invoice conditions are defined by contracts.
65
+
66
+
67
+ ## Data Structure
68
+
69
+ Records are stored in YAML format. On historical records, see [LucaRecord](../lucarecord/README.md#historical-field).
70
+
71
+ ### Customer
72
+
73
+ Customer consists of label information.
74
+
75
+ | Top level | Second level | | historical | Description |
76
+ |-----------|--------------|------|------------|--------------------------------|
77
+ | id | | auto | | uuid |
78
+ | name | | must | x | customer's name |
79
+ | address | | | x | |
80
+ | address2 | | | x | |
81
+ | contacts | | | | Array of contact information |
82
+ | | mail | | | mail address receiving invoice |
83
+
84
+
85
+ ### Product
86
+
87
+ Product is items template referred by Contract.
88
+
89
+ | Top level | Second level | | historical | Description |
90
+ |-----------|--------------|----------|------------|------------------------------------------------------------------------------------------------------|
91
+ | id | | auto | | uuid |
92
+ | name | | | x | Product name. |
93
+ | items | | | | Array of items. |
94
+ | | name | | x | Item name. |
95
+ | | price | | x | Item price. |
96
+ | | qty | optional | x | quantity. Default: 1. |
97
+ | | type | optional | | If 'initial', this item is treated as initial cost, applied only on the first month of the contract. |
98
+
99
+
100
+ ### Contract
101
+
102
+ Contract is core object for calculation. Common fields are as follows:
103
+
104
+ | Top level | Second level | | historical | Description |
105
+ |-------------|---------------|----------|------------|------------------------------------------------------------------------------------------------------|
106
+ | id | | auto | | uuid |
107
+ | customer_id | | must | x | customer's uuid |
108
+ | terms | | | | |
109
+ | | effective | must | | Start date of the contract. |
110
+ | | defunct | | | End date of the contract. |
111
+
112
+ Fields for subscription customers are as bellows:
113
+
114
+ | Top level | Second level | | historical | Description |
115
+ |-----------|---------------|----------|------------|------------------------------------------------------------------------------------------------------|
116
+ | terms | | | | |
117
+ | | billing_cycle | optional | | If 'monthly', invoices are generated on each month. |
118
+ | | category | optional | | Default: 'subscription' |
119
+ | products | | | | Array of products. |
120
+ | | id | | | reference for Product |
121
+ | items | | | | Array of items. |
122
+ | | name | | x | Item name. |
123
+ | | price | | x | Item price. |
124
+ | | qty | optional | x | quantity. Default: 1. |
125
+ | | type | optional | | If 'initial', this item is treated as initial cost, applied only on the first month of the contract. |
126
+ | sales_fee | | optional | | |
127
+ | | id | | | contract id of fee with sales partner. |
128
+
129
+
130
+ Fields for sales fee are as bellows:
131
+
132
+ | Top level | Second level | | historical | Description |
133
+ |-----------|--------------|----------|------------|-------------------------------------------------------------------------------------|
134
+ | terms | | | | |
135
+ | | category | | | If 'sales_fee', contract is treated as selling commission. |
136
+ | rate | | optional | | |
137
+ | | default | | | sales fee rate. |
138
+ | | initial | | | sales fee rate for items of type=initial. |
139
+
140
+
141
+ ### Invoice
142
+
143
+ Invoice is basically auto generated from Customer and Contract objects.
144
+
145
+ | Top level | Second level | Description |
146
+ |------------|--------------|------------------------------------------|
147
+ | id | | uuid |
148
+ | issue_date | | |
149
+ | due_date | | |
150
+ | customer | | |
151
+ | | id | customer's uuid |
152
+ | | name | customer name |
153
+ | | address | |
154
+ | | address2 | |
155
+ | | to | Array of mail addresses |
156
+ | items | | Array of items. |
157
+ | | name | Item name. |
158
+ | | price | Item price. |
159
+ | | qty | quantity. Default: 1. |
160
+ | | type | |
161
+ | | product_id | refrence for Product |
162
+ | subtotal | | Array of subtotal by tax category. |
163
+ | | items | amount of items |
164
+ | | tax | amount of tax |
165
+ | | rate | applied tax category. Default: 'default' |
166
+ | sales_fee | | |
167
+ | | id | contract id of fee with sales partner. |
168
+ | status | | Array of status with timestamp. |
169
+
170
+
171
+ ### Fee
172
+
173
+ Fee is basically auto generated from Contract and Invoice objects.
174
+
175
+ | Top level | Second level | Description |
176
+ |-----------|--------------|----------------------------------------------|
177
+ | id | | uuid |
178
+ | sales_fee | | |
179
+ | | id | contract id with sales partner. |
180
+ | | default.fee | Amount of fee on dafault rate. |
181
+ | | default.tax | Amount of tax for default.fee. |
182
+ | | initial.fee | Amount of fee on initial cost. |
183
+ | | initial.tax | Amount of tax for initial.fee. |
184
+ | invoice | | Carbon copy of Invoice attributes. |
185
+ | | id | |
186
+ | | contract_id | |
187
+ | | issue_date | |
188
+ | | due_date | |
189
+ | customer | | Carbon copy of Invoice customer except 'to'. |
190
+ | items | | Carbon copy of Invoice items. |
191
+ | subtotal | | Carbon copy of Invoice subtotal. |
192
+ | status | | Array of status with timestamp. |
193
+
26
194
 
27
195
  ## Development
28
196
 
@@ -32,4 +200,4 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
200
 
33
201
  ## Contributing
34
202
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/luca-deal.
203
+ Bug reports and pull requests are welcome on GitHub at https://github.com/chumaltd/luca .
@@ -5,57 +5,102 @@ require 'date'
5
5
  require 'optparse'
6
6
  require 'luca_deal'
7
7
 
8
- def customer(args = nil, params = {})
9
- case params['mode']
10
- when 'list'
11
- LucaDeal::Customer.new.list_name
12
- when 'create'
13
- if params['name']
14
- id = LucaDeal::Customer.new.generate!(params['name'])
15
- puts "Successfully generated Customer #{id}" if id
16
- puts 'Edit customer detail.' if id
17
- else
18
- puts 'requires customer\'s name. exit'
19
- exit 1
8
+ module LucaCmd
9
+ class Customer
10
+ def self.create(args = nil, params = {})
11
+ if args
12
+ id = LucaDeal::Customer.create(name: args[0])
13
+ puts "Successfully generated Customer #{id}" if id
14
+ puts 'Edit customer detail.' if id
15
+ else
16
+ puts 'requires customer\'s name. exit'
17
+ exit 1
18
+ end
19
+ end
20
+
21
+ def self.delete(args = nil, params = {})
22
+ if args
23
+ id = LucaDeal::Customer.delete(args[0])
24
+ else
25
+ puts 'requires customer\'s id. exit'
26
+ exit 1
27
+ end
28
+ end
29
+
30
+ def self.list(args = nil, params = {})
31
+ LucaDeal::Customer.new.list_name
20
32
  end
21
- else
22
- puts 'invalid option. --help for usage'
23
- exit 1
24
33
  end
25
- end
26
34
 
27
- def contract(args = nil, params = {})
28
- case params['mode']
29
- when 'create'
30
- if params['customer_id']
31
- id = LucaDeal::Contract.new.generate!(params['customer_id'], params['category'])
32
- puts "Successfully generated Contract #{id}" if id
33
- puts 'Conditions are tentative. Edit contract detail.' if id
34
- else
35
- puts 'requires customer\'s id. exit'
36
- exit 1
35
+ class Contract
36
+ def self.create(args = nil, params = {})
37
+ if args
38
+ id = LucaDeal::Contract.new.generate!(args[0], params['category'])
39
+ puts "Successfully generated Contract #{id}" if id
40
+ puts 'Conditions are tentative. Edit contract detail.' if id
41
+ else
42
+ puts 'requires customer\'s id. exit'
43
+ exit 1
44
+ end
45
+ end
46
+
47
+ def self.delete(args = nil, params = {})
48
+ if args
49
+ id = LucaDeal::Contract.delete(args[0])
50
+ else
51
+ puts 'requires contract id. exit'
52
+ exit 1
53
+ end
37
54
  end
38
- else
39
55
  end
40
- end
41
56
 
42
- def invoice(args = nil, params = {})
43
- date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
44
- case params['mode']
45
- when 'monthly'
46
- LucaDeal::Invoice.new(date).monthly_invoice
47
- when 'mail'
48
- LucaDeal::Invoice.new(date).deliver_mail
49
- when 'preview'
50
- LucaDeal::Invoice.new(date).preview_mail
51
- when 'stats'
52
- if args.empty?
53
- date = "#{Date.today.year}-#{Date.today.month}-1"
54
- count = 3
55
- end
56
- LucaDeal::Invoice.new(date).stats(count || 1)
57
- else
58
- puts 'not implemented mode'
57
+ class Invoice
58
+ def self.create(args = nil, params = {})
59
+ date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
60
+ case params['mode']
61
+ when 'monthly'
62
+ LucaDeal::Invoice.new(date).monthly_invoice
63
+ else
64
+ puts 'not implemented mode'
65
+ end
66
+ end
67
+
68
+ def self.delete(args = nil, params = {})
69
+ if args
70
+ id = LucaDeal::Invoice.delete(args[0])
71
+ else
72
+ puts 'requires contract id. exit'
73
+ exit 1
74
+ end
75
+ end
76
+
77
+ def self.export(args = nil, _params = nil)
78
+ if args
79
+ args << 28 if args.length == 2 # specify safe last day
80
+ LucaDeal::Invoice.new(args.join('-')).export_json
81
+ else
82
+ LucaDeal::Invoice.new.export_json
83
+ end
84
+ end
85
+
86
+ def self.list(args = nil, params = {})
87
+ date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
88
+ if args.empty?
89
+ date = "#{Date.today.year}-#{Date.today.month}-1"
90
+ count = 3
91
+ end
92
+ LucaDeal::Invoice.new(date).stats(count || 1)
93
+ end
94
+
95
+ def self.mail(args = nil, params = {})
96
+ date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
97
+ case params['mode']
98
+ when 'preview'
99
+ LucaDeal::Invoice.new(date).preview_mail
100
+ else
101
+ LucaDeal::Invoice.new(date).deliver_mail
102
+ end
103
+ end
59
104
  end
60
105
  end
61
106
 
@@ -65,44 +110,75 @@ end
65
110
 
66
111
  LucaRecord::Base.valid_project?
67
112
  cmd = ARGV.shift
113
+ params = {}
68
114
 
69
115
  case cmd
70
- when 'customer'
71
- params = {}
72
- OptionParser.new do |opt|
73
- opt.banner = 'Usage: luca-deal customer [options]'
74
- opt.on('--list', 'list all customers') { |v| params['mode'] = 'list' }
75
- opt.on('--create CustomerName', 'register new customer') do |v|
76
- params['mode'] = 'create'
77
- params['name'] = v
116
+ when /customers?/
117
+ subcmd = ARGV.shift
118
+ case subcmd
119
+ when 'list'
120
+ OptionParser.new do |opt|
121
+ opt.banner = 'Usage: luca-deal customers list [options]'
122
+ args = opt.parse(ARGV)
123
+ LucaCmd::Customer.list(args, params)
78
124
  end
79
- args = opt.parse(ARGV)
80
- customer(args, params)
81
- end
82
- when 'contract'
83
- params = {}
84
- OptionParser.new do |opt|
85
- opt.banner = 'Usage: luca-deal contract [options]'
86
- opt.on('--create CustomerId', 'register new contract') do |v|
87
- params['mode'] = 'create'
88
- params['customer_id'] = v
125
+ when 'create'
126
+ OptionParser.new do |opt|
127
+ opt.banner = 'Usage: luca-deal customers create CustomerName'
128
+ args = opt.parse(ARGV)
129
+ LucaCmd::Customer.create(args, params)
89
130
  end
90
- opt.on('--salesfee', 'create contract as sales fee definition') do |_v|
91
- params['category'] = 'sales_fee'
131
+ when 'delete'
132
+ LucaCmd::Customer.delete(ARGV)
133
+ else
134
+ puts 'Usage: luca-deal customers sub-commands'
135
+ end
136
+ when /contracts?/
137
+ subcmd = ARGV.shift
138
+ case subcmd
139
+ when 'create'
140
+ OptionParser.new do |opt|
141
+ opt.banner = 'Usage: luca-deal contracts create [options] CustomerId'
142
+ opt.on('--salesfee', 'create contract as sales fee definition') do |_v|
143
+ params['category'] = 'sales_fee'
144
+ end
145
+ args = opt.parse(ARGV)
146
+ LucaCmd::Contract.create(args, params)
92
147
  end
93
- args = opt.parse(ARGV)
94
- contract(args, params)
148
+ when 'delete'
149
+ LucaCmd::Contract.delete(ARGV)
150
+ else
151
+ puts 'Usage: luca-deal contracts Subcommand'
95
152
  end
96
- when 'invoice'
97
- params = {}
98
- OptionParser.new do |opt|
99
- opt.banner = 'Usage: luca-deal invoice [options] year month [date]'
100
- opt.on('--monthly', 'generate monthly data') { |v| params['mode'] = 'monthly' }
101
- opt.on('--mail', 'send to customers') { |v| params['mode'] = 'mail' }
102
- opt.on('--preview', 'send to preview user') { |v| params['mode'] = 'preview' }
103
- opt.on('--stats', 'list invoices') { |v| params['mode'] = 'stats' }
104
- args = opt.parse(ARGV)
105
- invoice(args, params)
153
+ when 'export'
154
+ LucaCmd::Invoice.export(ARGV)
155
+ when /invoices?/, 'i'
156
+ subcmd = ARGV.shift
157
+ case subcmd
158
+ when 'create'
159
+ OptionParser.new do |opt|
160
+ opt.banner = 'Usage: luca-deal invoices create [options] year month [date]'
161
+ opt.on('--monthly', 'generate monthly data') { |v| params['mode'] = 'monthly' }
162
+ args = opt.parse(ARGV)
163
+ LucaCmd::Invoice.create(args, params)
164
+ end
165
+ when 'delete'
166
+ LucaCmd::Invoice.delete(ARGV)
167
+ when 'list'
168
+ OptionParser.new do |opt|
169
+ opt.banner = 'Usage: luca-deal invoices list [options] year month [date]'
170
+ args = opt.parse(ARGV)
171
+ LucaCmd::Invoice.list(args, params)
172
+ end
173
+ when 'mail'
174
+ OptionParser.new do |opt|
175
+ opt.banner = 'Usage: luca-deal invoices mail [options] year month [date]'
176
+ opt.on('--preview', 'send to preview user') { |v| params['mode'] = 'preview' }
177
+ args = opt.parse(ARGV)
178
+ LucaCmd::Invoice.mail(args, params)
179
+ end
180
+ else
181
+ puts 'Usage: luca-deal invoices SubCommand'
106
182
  end
107
183
  when 'new'
108
184
  params = {}
@@ -112,7 +188,8 @@ when 'new'
112
188
  end
113
189
  else
114
190
  puts 'Usage: luca-deal sub-command [--help|options]'
115
- puts ' customer'
116
- puts ' contract'
117
- puts ' invoice'
191
+ puts ' customers'
192
+ puts ' contracts'
193
+ puts ' invoices'
194
+ puts ' export'
118
195
  end
@@ -6,6 +6,8 @@ require 'luca_deal/version'
6
6
  module LucaDeal
7
7
  autoload :Customer, 'luca_deal/customer'
8
8
  autoload :Contract, 'luca_deal/contract'
9
+ autoload :Fee, 'luca_deal/fee'
9
10
  autoload :Invoice, 'luca_deal/invoice'
11
+ autoload :Product, 'luca_deal/product'
10
12
  autoload :Setup, 'luca_deal/setup'
11
13
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'luca_deal/version'
2
4
 
3
5
  require 'yaml'
@@ -7,41 +9,62 @@ require 'luca_record'
7
9
  module LucaDeal
8
10
  class Contract < LucaRecord::Base
9
11
  @dirname = 'contracts'
12
+ @required = ['customer_id', 'terms']
10
13
 
11
14
  def initialize(date = nil)
12
15
  @date = date ? Date.parse(date) : Date.today
13
16
  @pjdir = Pathname(Dir.pwd)
14
17
  end
15
18
 
19
+ # returns active contracts on specified date.
20
+ #
21
+ def self.asof(year, month, day)
22
+ return enum_for(:asof, year, month, day) unless block_given?
23
+
24
+ new("#{year}-#{month}-#{day}").active do |contract|
25
+ yield contract
26
+ end
27
+ end
28
+
16
29
  #
17
30
  # collect active contracts
18
31
  #
19
32
  def active
33
+ return enum_for(:active) unless block_given?
34
+
20
35
  self.class.all do |data|
21
- contract = parse_current(data)
22
- contract['items'] = contract['items'].map { |item| parse_current(item) }
23
- next if !self.class.active_period?(contract)
36
+ next if !active_period?(data.dig('terms'))
24
37
 
25
- yield contract
38
+ contract = parse_current(data)
39
+ contract['items'] = contract['items']&.map { |item| parse_current(item) }
40
+ # TODO: handle sales_fee rate change
41
+ contract['rate'] = contract['rate']
42
+ yield contract.compact
26
43
  end
27
44
  end
28
45
 
29
- def generate!(customer_id, mode = nil)
46
+ def generate!(customer_id, mode = 'subscription')
30
47
  LucaDeal::Customer.find(customer_id) do |customer|
31
48
  current_customer = parse_current(customer)
32
- obj = { 'customer_id' => current_customer['id'], 'customer_name' => current_customer['name'] }
33
- obj['terms'] = { 'effective' => @date }
34
49
  if mode == 'sales_fee'
35
- obj.merge! salesfee_template
50
+ obj = salesfee_template
36
51
  else
37
- obj.merge! monthly_template
52
+ obj = monthly_template
38
53
  end
54
+ obj.merge!({ 'customer_id' => current_customer['id'], 'customer_name' => current_customer['name'] })
55
+ obj['terms'] ||= {}
56
+ obj['terms']['effective'] = @date
39
57
  self.class.create(obj)
40
58
  end
41
59
  end
42
60
 
43
- def self.active_period?(dat)
44
- !dat.dig('terms').nil?
61
+ def active_period?(dat)
62
+ unless dat.dig('defunct').nil?
63
+ defunct = dat.dig('defunct').respond_to?(:year) ? dat.dig('defunct') : Date.parse(dat.dig('defunct'))
64
+ return false if @date > defunct
65
+ end
66
+ effective = dat.dig('effective').respond_to?(:year) ? dat.dig('effective') : Date.parse(dat.dig('effective'))
67
+ @date >= effective
45
68
  end
46
69
 
47
70
  private
@@ -10,6 +10,7 @@ require 'luca_record'
10
10
  module LucaDeal
11
11
  class Customer < LucaRecord::Base
12
12
  @dirname = 'customers'
13
+ @required = ['name']
13
14
 
14
15
  def initialize(pjdir = nil)
15
16
  @date = Date.today
@@ -21,17 +22,20 @@ module LucaDeal
21
22
  YAML.dump(list).tap { |l| puts l }
22
23
  end
23
24
 
24
- def generate!(name)
25
- contact = {
25
+ def self.create(obj)
26
+ raise ':name is required' if obj[:name].nil?
27
+
28
+ contacts = obj[:contact]&.map { |c| { 'mail' => c[:mail] } }&.compact
29
+ contacts ||= [{
26
30
  'mail' => '_MAIL_ADDRESS_FOR_CONTACT_'
31
+ }]
32
+ h = {
33
+ 'name' => obj[:name],
34
+ 'address' => obj[:address] || '_CUSTOMER_ADDRESS_FOR_INVOICE_',
35
+ 'address2' => obj[:address2] || '_CUSTOMER_ADDRESS_FOR_INVOICE_',
36
+ 'contacts' => contacts
27
37
  }
28
- obj = {
29
- 'name' => name,
30
- 'address' => '_CUSTOMER_ADDRESS_FOR_INVOICE_',
31
- 'address2' => '_CUSTOMER_ADDRESS_FOR_INVOICE_',
32
- 'contacts' => [contact]
33
- }
34
- self.class.create(obj)
38
+ super(h)
35
39
  end
36
40
  end
37
41
  end
@@ -0,0 +1,118 @@
1
+ require 'luca_deal/version'
2
+
3
+ require 'mail'
4
+ require 'yaml'
5
+ require 'pathname'
6
+ require 'bigdecimal'
7
+ require 'luca_support/config'
8
+ require 'luca_support/mail'
9
+ require 'luca_deal'
10
+
11
+ module LucaDeal
12
+ class Fee < LucaRecord::Base
13
+ @dirname = 'fees'
14
+
15
+ def initialize(date = nil)
16
+ @date = issue_date(date)
17
+ @config = load_config('config.yml')
18
+ end
19
+
20
+ # calculate fee, based on invoices
21
+ #
22
+ def monthly_fee
23
+ LucaDeal::Contract.asof(@date.year, @date.month, @date.day) do |contract|
24
+ next if contract.dig('terms', 'category') != 'sales_fee'
25
+
26
+ @rate = { 'default' => BigDecimal(contract.dig('rate', 'default')) }
27
+ @rate['initial'] = contract.dig('rate', 'initial') ? BigDecimal(contract.dig('rate', 'initial')) : @rate['default']
28
+
29
+ LucaDeal::Invoice.asof(@date.year, @date.month) do |invoice|
30
+ next if invoice.dig('sales_fee', 'id') != contract['id']
31
+ next if duplicated_contract? invoice['contract_id']
32
+
33
+ fee = invoice.dup
34
+ fee['invoice'] = {}.tap do |f_invoice|
35
+ %w[id contract_id issue_date due_date].each do |i|
36
+ f_invoice[i] = invoice[i]
37
+ fee.delete i
38
+ end
39
+ end
40
+ fee['id'] = issue_random_id
41
+ fee['customer'].delete('to')
42
+ fee['sales_fee'].merge! subtotal(fee['items'])
43
+ gen_fee!(fee)
44
+ end
45
+ end
46
+ end
47
+
48
+ def get_customer(id)
49
+ {}.tap do |res|
50
+ LucaDeal::Customer.find(id) do |dat|
51
+ customer = parse_current(dat)
52
+ res['id'] = customer['id']
53
+ res['name'] = customer.dig('name')
54
+ res['address'] = customer.dig('address')
55
+ res['address2'] = customer.dig('address2')
56
+ res['to'] = customer.dig('contacts').map { |h| take_current(h, 'mail') }.compact
57
+ end
58
+ end
59
+ end
60
+
61
+ def gen_fee!(fee)
62
+ id = fee.dig('invoice', 'contract_id')
63
+ self.class.create(fee, date: @date, codes: Array(id))
64
+ end
65
+
66
+ private
67
+
68
+ def lib_path
69
+ __dir__
70
+ end
71
+
72
+ # load user company profile from config.
73
+ #
74
+ def set_company
75
+ {}.tap do |h|
76
+ h['name'] = @config.dig('company', 'name')
77
+ h['address'] = @config.dig('company', 'address')
78
+ h['address2'] = @config.dig('company', 'address2')
79
+ end
80
+ end
81
+
82
+ # calc fee & tax amount by tax category
83
+ #
84
+ def subtotal(items)
85
+ {}.tap do |subtotal|
86
+ items.each do |i|
87
+ rate = i.dig('type') || 'default'
88
+ subtotal[rate] = { 'fee' => 0, 'tax' => 0 } if subtotal.dig(rate).nil?
89
+ subtotal[rate]['fee'] += i['qty'] * i['price'] * @rate[rate]
90
+ end
91
+ subtotal.each do |rate, amount|
92
+ amount['tax'] = (amount['fee'] * load_tax_rate(rate)).to_i
93
+ amount['fee'] = amount['fee'].to_i
94
+ end
95
+ end
96
+ end
97
+
98
+ def issue_date(date)
99
+ base = date.nil? ? Date.today : Date.parse(date)
100
+ Date.new(base.year, base.month, -1)
101
+ end
102
+
103
+ # load Tax Rate from config.
104
+ #
105
+ def load_tax_rate(name)
106
+ return 0 if @config.dig('tax_rate', name).nil?
107
+
108
+ BigDecimal(take_current(@config['tax_rate'], name).to_s)
109
+ end
110
+
111
+ def duplicated_contract?(id)
112
+ self.class.asof(@date.year, @date.month, @date.day) do |_f, path|
113
+ return true if path.include?(id)
114
+ end
115
+ false
116
+ end
117
+ end
118
+ end
@@ -1,6 +1,7 @@
1
1
  require 'luca_deal/version'
2
2
 
3
3
  require 'mail'
4
+ require 'json'
4
5
  require 'yaml'
5
6
  require 'pathname'
6
7
  require 'bigdecimal'
@@ -12,6 +13,7 @@ require 'luca_record'
12
13
  module LucaDeal
13
14
  class Invoice < LucaRecord::Base
14
15
  @dirname = 'invoices'
16
+ @required = ['issue_date', 'customer', 'items', 'subtotal']
15
17
 
16
18
  def initialize(date = nil)
17
19
  @date = issue_date(date)
@@ -58,7 +60,6 @@ module LucaDeal
58
60
  mail
59
61
  end
60
62
 
61
- #
62
63
  # Output seriarized invoice data to stdout.
63
64
  # Returns previous N months on multiple count
64
65
  #
@@ -97,7 +98,28 @@ module LucaDeal
97
98
  collection << stat
98
99
  end
99
100
  end
100
- puts YAML.dump(collection)
101
+ puts YAML.dump(LucaSupport::Code.readable(collection))
102
+ end
103
+ end
104
+
105
+ def export_json
106
+ [].tap do |res|
107
+ self.class.asof(@date.year, @date.month) do |dat|
108
+ item = {}
109
+ item['date'] = dat['issue_date']
110
+ item['debit'] = []
111
+ item['credit'] = []
112
+ dat['subtotal'].map do |sub|
113
+ item['debit'] << { 'label' => '売掛金', 'value' => LucaSupport::Code.readable(sub['items']) }
114
+ item['debit'] << { 'label' => '売掛金', 'value' => LucaSupport::Code.readable(sub['tax']) }
115
+ item['credit'] << { 'label' => '売上高', 'value' => LucaSupport::Code.readable(sub['items']) }
116
+ item['credit'] << { 'label' => '売上高', 'value' => LucaSupport::Code.readable(sub['tax']) }
117
+ end
118
+ item['x-customer'] = dat['customer']['name'] if dat.dig('customer', 'name')
119
+ item['x-editor'] = 'LucaDeal'
120
+ res << item
121
+ end
122
+ puts JSON.dump(res)
101
123
  end
102
124
  end
103
125
 
@@ -113,12 +135,12 @@ module LucaDeal
113
135
  invoice['customer'] = get_customer(contract.dig('customer_id'))
114
136
  invoice['due_date'] = due_date(@date)
115
137
  invoice['issue_date'] = @date
116
- invoice['items'] = contract.dig('items').map do |item|
117
- {}.tap do |h|
118
- h['name'] = item.dig('name')
119
- h['price'] = item.dig('price')
120
- h['qty'] = item.dig('qty')
121
- end
138
+ invoice['sales_fee'] = contract['sales_fee'] if contract.dig('sales_fee')
139
+ invoice['items'] = get_products(contract['products'])
140
+ .concat(contract['items']&.map { |i| i['qty'] ||= 1; i } || [])
141
+ .compact
142
+ invoice['items'].reject! do |item|
143
+ item.dig('type') == 'initial' && subsequent_month?(contract.dig('terms', 'effective'))
122
144
  end
123
145
  invoice['subtotal'] = subtotal(invoice['items'])
124
146
  .map { |k, v| v.tap { |dat| dat['rate'] = k } }
@@ -126,7 +148,6 @@ module LucaDeal
126
148
  end
127
149
  end
128
150
 
129
- #
130
151
  # set variables for ERB template
131
152
  #
132
153
  def invoice_vars(invoice_dat)
@@ -151,9 +172,23 @@ module LucaDeal
151
172
  end
152
173
  end
153
174
 
175
+ def get_products(products)
176
+ return [] if products.nil?
177
+
178
+ [].tap do |res|
179
+ products.each do |product|
180
+ LucaDeal::Product.find(product['id'])['items'].each do |item|
181
+ item['product_id'] = product['id']
182
+ item['qty'] ||= 1
183
+ res << item
184
+ end
185
+ end
186
+ end
187
+ end
188
+
154
189
  def gen_invoice!(invoice)
155
190
  id = invoice.dig('contract_id')
156
- self.class.create_record!(invoice, @date, Array(id))
191
+ self.class.create(invoice, date: @date, codes: Array(id))
157
192
  end
158
193
 
159
194
  def issue_date(date)
@@ -172,7 +207,6 @@ module LucaDeal
172
207
  __dir__
173
208
  end
174
209
 
175
- #
176
210
  # load user company profile from config.
177
211
  #
178
212
  def set_company
@@ -183,23 +217,22 @@ module LucaDeal
183
217
  end
184
218
  end
185
219
 
186
- #
187
220
  # calc items & tax amount by tax category
188
221
  #
189
222
  def subtotal(items)
190
223
  {}.tap do |subtotal|
191
224
  items.each do |i|
192
225
  rate = i.dig('tax') || 'default'
226
+ qty = i['qty'] || BigDecimal('1')
193
227
  subtotal[rate] = { 'items' => 0, 'tax' => 0 } if subtotal.dig(rate).nil?
194
- subtotal[rate]['items'] += i['qty'] * i['price']
228
+ subtotal[rate]['items'] += i['price'] * qty
195
229
  end
196
230
  subtotal.each do |rate, amount|
197
- amount['tax'] = (amount['items'] * load_tax_rate(rate)).to_i
231
+ amount['tax'] = (amount['items'] * load_tax_rate(rate))
198
232
  end
199
233
  end
200
234
  end
201
235
 
202
- #
203
236
  # load Tax Rate from config.
204
237
  #
205
238
  def load_tax_rate(name)
@@ -218,5 +251,10 @@ module LucaDeal
218
251
  end
219
252
  false
220
253
  end
254
+
255
+ def subsequent_month?(effective_date)
256
+ effective_date = Date.parse(effective_date) unless effective_date.respond_to? :year
257
+ effective_date.year != @date.year || effective_date.month != @date.month
258
+ end
221
259
  end
222
260
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'luca_deal/version'
4
+
5
+ require 'date'
6
+ require 'yaml'
7
+ require 'pathname'
8
+ require 'luca_record'
9
+
10
+ module LucaDeal
11
+ class Product < LucaRecord::Base
12
+ @dirname = 'products'
13
+ @required = ['items']
14
+
15
+ def list_name
16
+ list = self.class.all.map { |dat| parse_current(dat) }
17
+ YAML.dump(list).tap { |l| puts l }
18
+ end
19
+
20
+ # Save data with hash in Product format. Simple format is also available as bellows:
21
+ # {
22
+ # name: 'item_name(required)', price: 'item_price', qty: 'item_qty',
23
+ # initial: { name: 'item_name', price: 'item_price', qty: 'item_qty' }
24
+ # }
25
+ def self.create(obj)
26
+ if obj[:name].nil?
27
+ h = obj
28
+ else
29
+ items = [{
30
+ 'name' => obj[:name],
31
+ 'price' => obj[:price] || 0,
32
+ 'qty' => obj[:qty] || 1
33
+ }]
34
+ if obj[:initial]
35
+ items << {
36
+ 'name' => obj.dig(:initial, :name),
37
+ 'price' => obj.dig(:initial, :price) || 0,
38
+ 'qty' => obj.dig(:initial, :qty) || 1,
39
+ 'type' => 'initial'
40
+ }
41
+ end
42
+ h = {
43
+ 'name' => obj[:name],
44
+ 'items' => items
45
+ }
46
+ end
47
+ super(h)
48
+ end
49
+ end
50
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LucaDeal
4
- VERSION = "0.2.9"
4
+ VERSION = '0.2.18'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lucadeal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.9
4
+ version: 0.2.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuma Takahiro
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-04 00:00:00.000000000 Z
11
+ date: 2020-11-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lucarecord
@@ -83,7 +83,9 @@ files:
83
83
  - lib/luca_deal.rb
84
84
  - lib/luca_deal/contract.rb
85
85
  - lib/luca_deal/customer.rb
86
+ - lib/luca_deal/fee.rb
86
87
  - lib/luca_deal/invoice.rb
88
+ - lib/luca_deal/product.rb
87
89
  - lib/luca_deal/setup.rb
88
90
  - lib/luca_deal/templates/.keep
89
91
  - lib/luca_deal/templates/config.yml