lucadeal 0.2.7 → 0.2.16
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +135 -6
- data/exe/luca-deal +5 -2
- data/lib/luca_deal.rb +2 -0
- data/lib/luca_deal/contract.rb +56 -9
- data/lib/luca_deal/customer.rb +13 -9
- data/lib/luca_deal/fee.rb +118 -0
- data/lib/luca_deal/invoice.rb +31 -15
- data/lib/luca_deal/product.rb +50 -0
- data/lib/luca_deal/setup.rb +1 -1
- data/lib/luca_deal/version.rb +1 -1
- metadata +20 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5f17d75af6f101c60eced0ecfb770e39ac66b0f35725a6976f135216a0c75099
|
4
|
+
data.tar.gz: d3e60d51908e78ab72e177f29affc26864744afb584d1c5dadd507b18ff888f6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bd534c31a33405478cdeba88777481f5c17af6337cf743e2d16a8e5027209a65d1efb8329cf7f6e72318dfea8c3daa1965d9cf1e833ce93b85d24925c17638f6
|
7
|
+
data.tar.gz: f1d809de08ce94cf7da913c5bc733515f95bba596dc155596880cb4776837113c36f93ee4247c35fe34990f2a45fd758df1bddec3d2e587d182062cdb5893c60
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,15 +1,15 @@
|
|
1
|
-
#
|
1
|
+
# LucaDeal
|
2
2
|
|
3
|
-
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/lucadeal.svg)](https://badge.fury.io/rb/lucadeal)
|
4
4
|
|
5
|
-
|
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 '
|
12
|
+
gem 'lucadeal'
|
13
13
|
```
|
14
14
|
|
15
15
|
And then execute:
|
@@ -18,12 +18,141 @@ And then execute:
|
|
18
18
|
|
19
19
|
Or install it yourself as:
|
20
20
|
|
21
|
-
$ gem install
|
21
|
+
$ gem install lucadeal
|
22
22
|
|
23
23
|
## Usage
|
24
24
|
|
25
25
|
TODO: Write usage instructions here
|
26
26
|
|
27
|
+
|
28
|
+
## Data Structure
|
29
|
+
|
30
|
+
Records are stored in YAML format. On historical records, see [LucaRecord](../lucarecord/README.md#historical-field).
|
31
|
+
|
32
|
+
### Customer
|
33
|
+
|
34
|
+
Customer consists of label information.
|
35
|
+
|
36
|
+
| Top level | Second level | | historical | Description |
|
37
|
+
|-----------|--------------|------|------------|--------------------------------|
|
38
|
+
| id | | auto | | uuid |
|
39
|
+
| name | | must | x | customer's name |
|
40
|
+
| address | | | x | |
|
41
|
+
| address2 | | | x | |
|
42
|
+
| contacts | | | | Array of contact information |
|
43
|
+
| | mail | | | mail address receiving invoice |
|
44
|
+
|
45
|
+
|
46
|
+
### Product
|
47
|
+
|
48
|
+
Product is items template referred by Contract.
|
49
|
+
|
50
|
+
| Top level | Second level | | historical | Description |
|
51
|
+
|-----------|--------------|----------|------------|------------------------------------------------------------------------------------------------------|
|
52
|
+
| id | | auto | | uuid |
|
53
|
+
| name | | | x | Product name. |
|
54
|
+
| items | | | | Array of items. |
|
55
|
+
| | name | | x | Item name. |
|
56
|
+
| | price | | x | Item price. |
|
57
|
+
| | qty | optional | x | quantity. Default: 1. |
|
58
|
+
| | type | optional | | If 'initial', this item is treated as initial cost, applied only on the first month of the contract. |
|
59
|
+
|
60
|
+
|
61
|
+
### Contract
|
62
|
+
|
63
|
+
Contract is core object for calculation. Common fields are as follows:
|
64
|
+
|
65
|
+
| Top level | Second level | | historical | Description |
|
66
|
+
|-------------|---------------|----------|------------|------------------------------------------------------------------------------------------------------|
|
67
|
+
| id | | auto | | uuid |
|
68
|
+
| customer_id | | must | x | customer's uuid |
|
69
|
+
| terms | | | | |
|
70
|
+
| | effective | must | | Start date of the contract. |
|
71
|
+
| | defunct | | | End date of the contract. |
|
72
|
+
|
73
|
+
Fields for subscription customers are as bellows:
|
74
|
+
|
75
|
+
| Top level | Second level | | historical | Description |
|
76
|
+
|-----------|---------------|----------|------------|------------------------------------------------------------------------------------------------------|
|
77
|
+
| terms | | | | |
|
78
|
+
| | billing_cycle | optional | | If 'monthly', invoices are generated on each month. |
|
79
|
+
| | category | optional | | Default: 'subscription' |
|
80
|
+
| products | | | | Array of products. |
|
81
|
+
| | id | | | reference for Product |
|
82
|
+
| items | | | | Array of items. |
|
83
|
+
| | name | | x | Item name. |
|
84
|
+
| | price | | x | Item price. |
|
85
|
+
| | qty | optional | x | quantity. Default: 1. |
|
86
|
+
| | type | optional | | If 'initial', this item is treated as initial cost, applied only on the first month of the contract. |
|
87
|
+
| sales_fee | | optional | | |
|
88
|
+
| | id | | | contract id of fee with sales partner. |
|
89
|
+
|
90
|
+
|
91
|
+
Fields for sales fee are as bellows:
|
92
|
+
|
93
|
+
| Top level | Second level | | historical | Description |
|
94
|
+
|-----------|--------------|----------|------------|-------------------------------------------------------------------------------------|
|
95
|
+
| terms | | | | |
|
96
|
+
| | category | | | If 'sales_fee', contract is treated as selling commission. |
|
97
|
+
| rate | | optional | | |
|
98
|
+
| | default | | | sales fee rate. |
|
99
|
+
| | initial | | | sales fee rate for items of type=initial. |
|
100
|
+
|
101
|
+
|
102
|
+
### Invoice
|
103
|
+
|
104
|
+
Invoice is basically auto generated from Customer and Contract objects.
|
105
|
+
|
106
|
+
| Top level | Second level | Description |
|
107
|
+
|------------|--------------|------------------------------------------|
|
108
|
+
| id | | uuid |
|
109
|
+
| issue_date | | |
|
110
|
+
| due_date | | |
|
111
|
+
| customer | | |
|
112
|
+
| | id | customer's uuid |
|
113
|
+
| | name | customer name |
|
114
|
+
| | address | |
|
115
|
+
| | address2 | |
|
116
|
+
| | to | Array of mail addresses |
|
117
|
+
| items | | Array of items. |
|
118
|
+
| | name | Item name. |
|
119
|
+
| | price | Item price. |
|
120
|
+
| | qty | quantity. Default: 1. |
|
121
|
+
| | type | |
|
122
|
+
| | product_id | refrence for Product |
|
123
|
+
| subtotal | | Array of subtotal by tax category. |
|
124
|
+
| | items | amount of items |
|
125
|
+
| | tax | amount of tax |
|
126
|
+
| | rate | applied tax category. Default: 'default' |
|
127
|
+
| sales_fee | | |
|
128
|
+
| | id | contract id of fee with sales partner. |
|
129
|
+
| status | | Array of status with timestamp. |
|
130
|
+
|
131
|
+
|
132
|
+
### Fee
|
133
|
+
|
134
|
+
Fee is basically auto generated from Contract and Invoice objects.
|
135
|
+
|
136
|
+
| Top level | Second level | Description |
|
137
|
+
|-----------|--------------|----------------------------------------------|
|
138
|
+
| id | | uuid |
|
139
|
+
| sales_fee | | |
|
140
|
+
| | id | contract id with sales partner. |
|
141
|
+
| | default.fee | Amount of fee on dafault rate. |
|
142
|
+
| | default.tax | Amount of tax for default.fee. |
|
143
|
+
| | initial.fee | Amount of fee on initial cost. |
|
144
|
+
| | initial.tax | Amount of tax for initial.fee. |
|
145
|
+
| invoice | | Carbon copy of Invoice attributes. |
|
146
|
+
| | id | |
|
147
|
+
| | contract_id | |
|
148
|
+
| | issue_date | |
|
149
|
+
| | due_date | |
|
150
|
+
| customer | | Carbon copy of Invoice customer except 'to'. |
|
151
|
+
| items | | Carbon copy of Invoice items. |
|
152
|
+
| subtotal | | Carbon copy of Invoice subtotal. |
|
153
|
+
| status | | Array of status with timestamp. |
|
154
|
+
|
155
|
+
|
27
156
|
## Development
|
28
157
|
|
29
158
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -32,4 +161,4 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
32
161
|
|
33
162
|
## Contributing
|
34
163
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
164
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/chumaltd/luca .
|
data/exe/luca-deal
CHANGED
@@ -11,7 +11,7 @@ def customer(args = nil, params = {})
|
|
11
11
|
LucaDeal::Customer.new.list_name
|
12
12
|
when 'create'
|
13
13
|
if params['name']
|
14
|
-
id = LucaDeal::Customer.
|
14
|
+
id = LucaDeal::Customer.create(name: params['name'])
|
15
15
|
puts "Successfully generated Customer #{id}" if id
|
16
16
|
puts 'Edit customer detail.' if id
|
17
17
|
else
|
@@ -28,7 +28,7 @@ def contract(args = nil, params = {})
|
|
28
28
|
case params['mode']
|
29
29
|
when 'create'
|
30
30
|
if params['customer_id']
|
31
|
-
id = LucaDeal::Contract.new.generate!(params['customer_id'])
|
31
|
+
id = LucaDeal::Contract.new.generate!(params['customer_id'], params['category'])
|
32
32
|
puts "Successfully generated Contract #{id}" if id
|
33
33
|
puts 'Conditions are tentative. Edit contract detail.' if id
|
34
34
|
else
|
@@ -87,6 +87,9 @@ when 'contract'
|
|
87
87
|
params['mode'] = 'create'
|
88
88
|
params['customer_id'] = v
|
89
89
|
end
|
90
|
+
opt.on('--salesfee', 'create contract as sales fee definition') do |_v|
|
91
|
+
params['category'] = 'sales_fee'
|
92
|
+
end
|
90
93
|
args = opt.parse(ARGV)
|
91
94
|
contract(args, params)
|
92
95
|
end
|
data/lib/luca_deal.rb
CHANGED
@@ -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
|
data/lib/luca_deal/contract.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'luca_deal/version'
|
2
4
|
|
3
5
|
require 'yaml'
|
@@ -7,40 +9,85 @@ 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
|
-
|
22
|
-
next if !self.class.active_period?(contract)
|
36
|
+
next if !active_period?(data.dig('terms'))
|
23
37
|
|
24
|
-
|
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
|
25
43
|
end
|
26
44
|
end
|
27
45
|
|
28
|
-
def generate!(customer_id)
|
46
|
+
def generate!(customer_id, mode = 'subscription')
|
29
47
|
LucaDeal::Customer.find(customer_id) do |customer|
|
30
48
|
current_customer = parse_current(customer)
|
31
|
-
|
49
|
+
if mode == 'sales_fee'
|
50
|
+
obj = salesfee_template
|
51
|
+
else
|
52
|
+
obj = monthly_template
|
53
|
+
end
|
54
|
+
obj.merge!({ 'customer_id' => current_customer['id'], 'customer_name' => current_customer['name'] })
|
55
|
+
obj['terms'] ||= {}
|
56
|
+
obj['terms']['effective'] = @date
|
57
|
+
self.class.create(obj)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
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
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def monthly_template
|
73
|
+
{}.tap do |obj|
|
32
74
|
obj['items'] = [{
|
33
75
|
'name' => '_ITEM_NAME_FOR_INVOICE_',
|
34
76
|
'qty' => 1,
|
35
77
|
'price' => 0
|
36
78
|
}]
|
37
|
-
obj['terms'] = { 'billing_cycle' => 'monthly'
|
38
|
-
self.class.create(obj)
|
79
|
+
obj['terms'] = { 'billing_cycle' => 'monthly' }
|
39
80
|
end
|
40
81
|
end
|
41
82
|
|
42
|
-
def
|
43
|
-
|
83
|
+
def salesfee_template
|
84
|
+
{}.tap do |obj|
|
85
|
+
obj['rate'] = {
|
86
|
+
'default' => '0.2',
|
87
|
+
'initial' => '0.2'
|
88
|
+
}
|
89
|
+
obj['terms'] = { 'category' => 'sales_fee' }
|
90
|
+
end
|
44
91
|
end
|
45
92
|
end
|
46
93
|
end
|
data/lib/luca_deal/customer.rb
CHANGED
@@ -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
|
25
|
-
|
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
|
-
|
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
|
data/lib/luca_deal/invoice.rb
CHANGED
@@ -12,6 +12,7 @@ require 'luca_record'
|
|
12
12
|
module LucaDeal
|
13
13
|
class Invoice < LucaRecord::Base
|
14
14
|
@dirname = 'invoices'
|
15
|
+
@required = ['issue_date', 'customer', 'items', 'subtotal']
|
15
16
|
|
16
17
|
def initialize(date = nil)
|
17
18
|
@date = issue_date(date)
|
@@ -58,7 +59,6 @@ module LucaDeal
|
|
58
59
|
mail
|
59
60
|
end
|
60
61
|
|
61
|
-
#
|
62
62
|
# Output seriarized invoice data to stdout.
|
63
63
|
# Returns previous N months on multiple count
|
64
64
|
#
|
@@ -97,7 +97,7 @@ module LucaDeal
|
|
97
97
|
collection << stat
|
98
98
|
end
|
99
99
|
end
|
100
|
-
puts YAML.dump(collection)
|
100
|
+
puts YAML.dump(LucaSupport::Code.readable(collection))
|
101
101
|
end
|
102
102
|
end
|
103
103
|
|
@@ -113,12 +113,12 @@ module LucaDeal
|
|
113
113
|
invoice['customer'] = get_customer(contract.dig('customer_id'))
|
114
114
|
invoice['due_date'] = due_date(@date)
|
115
115
|
invoice['issue_date'] = @date
|
116
|
-
invoice['
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
116
|
+
invoice['sales_fee'] = contract['sales_fee'] if contract.dig('sales_fee')
|
117
|
+
invoice['items'] = get_products(contract['products'])
|
118
|
+
.concat(contract['items']&.map { |i| i['qty'] ||= 1; i } || [])
|
119
|
+
.compact
|
120
|
+
invoice['items'].reject! do |item|
|
121
|
+
item.dig('type') == 'initial' && subsequent_month?(contract.dig('terms', 'effective'))
|
122
122
|
end
|
123
123
|
invoice['subtotal'] = subtotal(invoice['items'])
|
124
124
|
.map { |k, v| v.tap { |dat| dat['rate'] = k } }
|
@@ -126,7 +126,6 @@ module LucaDeal
|
|
126
126
|
end
|
127
127
|
end
|
128
128
|
|
129
|
-
#
|
130
129
|
# set variables for ERB template
|
131
130
|
#
|
132
131
|
def invoice_vars(invoice_dat)
|
@@ -151,9 +150,23 @@ module LucaDeal
|
|
151
150
|
end
|
152
151
|
end
|
153
152
|
|
153
|
+
def get_products(products)
|
154
|
+
return [] if products.nil?
|
155
|
+
|
156
|
+
[].tap do |res|
|
157
|
+
products.each do |product|
|
158
|
+
LucaDeal::Product.find(product['id'])['items'].each do |item|
|
159
|
+
item['product_id'] = product['id']
|
160
|
+
item['qty'] ||= 1
|
161
|
+
res << item
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
154
167
|
def gen_invoice!(invoice)
|
155
168
|
id = invoice.dig('contract_id')
|
156
|
-
self.class.
|
169
|
+
self.class.create(invoice, date: @date, codes: Array(id))
|
157
170
|
end
|
158
171
|
|
159
172
|
def issue_date(date)
|
@@ -172,7 +185,6 @@ module LucaDeal
|
|
172
185
|
__dir__
|
173
186
|
end
|
174
187
|
|
175
|
-
#
|
176
188
|
# load user company profile from config.
|
177
189
|
#
|
178
190
|
def set_company
|
@@ -183,23 +195,22 @@ module LucaDeal
|
|
183
195
|
end
|
184
196
|
end
|
185
197
|
|
186
|
-
#
|
187
198
|
# calc items & tax amount by tax category
|
188
199
|
#
|
189
200
|
def subtotal(items)
|
190
201
|
{}.tap do |subtotal|
|
191
202
|
items.each do |i|
|
192
203
|
rate = i.dig('tax') || 'default'
|
204
|
+
qty = i['qty'] || BigDecimal('1')
|
193
205
|
subtotal[rate] = { 'items' => 0, 'tax' => 0 } if subtotal.dig(rate).nil?
|
194
|
-
subtotal[rate]['items'] += i['
|
206
|
+
subtotal[rate]['items'] += i['price'] * qty
|
195
207
|
end
|
196
208
|
subtotal.each do |rate, amount|
|
197
|
-
amount['tax'] = (amount['items'] * load_tax_rate(rate))
|
209
|
+
amount['tax'] = (amount['items'] * load_tax_rate(rate))
|
198
210
|
end
|
199
211
|
end
|
200
212
|
end
|
201
213
|
|
202
|
-
#
|
203
214
|
# load Tax Rate from config.
|
204
215
|
#
|
205
216
|
def load_tax_rate(name)
|
@@ -218,5 +229,10 @@ module LucaDeal
|
|
218
229
|
end
|
219
230
|
false
|
220
231
|
end
|
232
|
+
|
233
|
+
def subsequent_month?(effective_date)
|
234
|
+
effective_date = Date.parse(effective_date) unless effective_date.respond_to? :year
|
235
|
+
effective_date.year != @date.year || effective_date.month != @date.month
|
236
|
+
end
|
221
237
|
end
|
222
238
|
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
|
data/lib/luca_deal/setup.rb
CHANGED
data/lib/luca_deal/version.rb
CHANGED
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.
|
4
|
+
version: 0.2.16
|
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
|
+
date: 2020-11-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: lucarecord
|
@@ -28,14 +28,14 @@ dependencies:
|
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '1.17'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.17'
|
41
41
|
- !ruby/object:Gem::Dependency
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 12.3.3
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 12.3.3
|
55
69
|
description: 'Deal with contracts
|
56
70
|
|
57
71
|
'
|
@@ -69,7 +83,9 @@ files:
|
|
69
83
|
- lib/luca_deal.rb
|
70
84
|
- lib/luca_deal/contract.rb
|
71
85
|
- lib/luca_deal/customer.rb
|
86
|
+
- lib/luca_deal/fee.rb
|
72
87
|
- lib/luca_deal/invoice.rb
|
88
|
+
- lib/luca_deal/product.rb
|
73
89
|
- lib/luca_deal/setup.rb
|
74
90
|
- lib/luca_deal/templates/.keep
|
75
91
|
- lib/luca_deal/templates/config.yml
|