spree_price 3.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +83 -0
- data/app/assets/javascripts/spree/backend/price.js.coffee +13 -0
- data/app/assets/javascripts/spree/backend/price_book.js.coffee +104 -0
- data/app/assets/javascripts/spree/backend/spree_price.js +2 -0
- data/app/assets/javascripts/spree/frontend/spree_price.js +2 -0
- data/app/assets/stylesheets/spree/backend/spree_price.css +4 -0
- data/app/assets/stylesheets/spree/frontend/spree_price.css +4 -0
- data/app/controllers/spree/admin/currency_rates_controller.rb +11 -0
- data/app/controllers/spree/admin/price_books_controller.rb +41 -0
- data/app/controllers/spree/admin/price_types_controller.rb +6 -0
- data/app/controllers/spree/admin/prices_controller_decorator.rb +54 -0
- data/app/controllers/spree/admin/stores_controller_decorator.rb +16 -0
- data/app/helpers/spree/base_helper_decorator.rb +32 -0
- data/app/models/spree/currency_rate.rb +56 -0
- data/app/models/spree/line_item_decorator.rb +79 -0
- data/app/models/spree/line_item_price.rb +4 -0
- data/app/models/spree/order/currency_updater_decorator.rb +9 -0
- data/app/models/spree/order_decorator.rb +15 -0
- data/app/models/spree/order_price.rb +4 -0
- data/app/models/spree/order_updater_decorator.rb +30 -0
- data/app/models/spree/price_book.rb +88 -0
- data/app/models/spree/price_decorator.rb +39 -0
- data/app/models/spree/price_type.rb +15 -0
- data/app/models/spree/role_decorator.rb +15 -0
- data/app/models/spree/role_price_book.rb +5 -0
- data/app/models/spree/store_decorator.rb +4 -0
- data/app/models/spree/store_price_book.rb +7 -0
- data/app/models/spree/variant_decorator.rb +43 -0
- data/app/overrides/spree/admin/roles/_form/add_default_checkout_to_role.html.erb.deface +9 -0
- data/app/overrides/spree/admin/shared/sub_menu/_configuration/current_rates_link.html.erb.deface +6 -0
- data/app/overrides/spree/admin/shared/sub_menu/_product/add_price_books_tab.html.erb.deface +4 -0
- data/app/overrides/spree/admin/stores/_form/add_price_book_to_store.html.erb.deface +49 -0
- data/app/views/spree/admin/currency_rates/_form.html.erb +29 -0
- data/app/views/spree/admin/currency_rates/edit.html.erb +19 -0
- data/app/views/spree/admin/currency_rates/index.html.erb +50 -0
- data/app/views/spree/admin/currency_rates/new.html.erb +21 -0
- data/app/views/spree/admin/price_books/_add_price.html.erb +82 -0
- data/app/views/spree/admin/price_books/_form.html.erb +81 -0
- data/app/views/spree/admin/price_books/_price.html.erb +12 -0
- data/app/views/spree/admin/price_books/_price_book.html.erb +16 -0
- data/app/views/spree/admin/price_books/_price_list.html.erb +37 -0
- data/app/views/spree/admin/price_books/edit.html.erb +21 -0
- data/app/views/spree/admin/price_books/index.html.erb +65 -0
- data/app/views/spree/admin/price_books/new.html.erb +14 -0
- data/app/views/spree/admin/price_books/show.html.erb +39 -0
- data/app/views/spree/admin/price_types/_form.html.erb +25 -0
- data/app/views/spree/admin/price_types/edit.html.erb +18 -0
- data/app/views/spree/admin/price_types/index.html.erb +51 -0
- data/app/views/spree/admin/price_types/new.html.erb +14 -0
- data/app/views/spree/admin/prices/_variant_prices.html.erb +57 -0
- data/app/views/spree/admin/prices/index.html.erb +39 -0
- data/config/locales/en.yml +34 -0
- data/config/locales/validates_timeliness.en.yml +16 -0
- data/config/routes.rb +33 -0
- data/db/migrate/20180828044728_add_spree_price_book.rb +52 -0
- data/db/migrate/20180830053207_add_price_to_order_and_line_item.rb +19 -0
- data/db/migrate/20180830055224_add_default_for_role.rb +7 -0
- data/lib/generators/spree_price/install/install_generator.rb +31 -0
- data/lib/spree_price.rb +7 -0
- data/lib/spree_price/engine.rb +20 -0
- data/lib/spree_price/factories.rb +5 -0
- data/lib/spree_price/factories/currency_rate_factory.rb +15 -0
- data/lib/spree_price/factories/price_book_factory.rb +34 -0
- data/lib/spree_price/factories/price_type_factory.rb +21 -0
- data/lib/spree_price/version.rb +17 -0
- data/lib/tasks/price.rake +58 -0
- metadata +362 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 48cb223d2b3c405236743e13146782f898967245dd2ac3d25cf6e0869cfdfc66
|
4
|
+
data.tar.gz: 99334096c7a58e696e3188098f5506c74aeabcfc2e47ade2ae1c01d8561f48e7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 66027887b1514bb4658a575ad4e717233fd76f2ad28f22911164c35112cc6c866889f4fbee122955aa828e00eb6b13d1d99a5e62f6febd522e6fc9374cec0736
|
7
|
+
data.tar.gz: 24d3611554d5c320265685fcaff8d9de5697e1acb48e5b78d7a3c65fb32d0e41ad0386ae8c8706151e5fe6feddbf68cddc49347c2e952d644d13262f5f34ad00
|
data/README.md
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# Spree Price
|
2
|
+
|
3
|
+
Heavily inspired by [spree-contrib/spree_price_book](https://github.com/spree-contrib/spree_price_books).
|
4
|
+
1. Support multiple store price
|
5
|
+
2. Support multiple type of prices (e.g. sales price, marked price, manufacturer's suggested retail price
|
6
|
+
3. Support price book. Price book can be prioritized at store.
|
7
|
+
4. Auto adjust prices according to exchange rate.
|
8
|
+
5. Price is found by the following order.
|
9
|
+
- Manual set price
|
10
|
+
- Price book with higher priority in the same store
|
11
|
+
- First price matching the currency and price type
|
12
|
+
- First price matching the currency
|
13
|
+
TODO: Find the price from parent price type without sacrificing the performance too much
|
14
|
+
|
15
|
+
6. The price will not be populated from parent price type by default. You can either fill nil prices from parent price book with adjustment factor or refresh all prices from parent price book in admin panel.
|
16
|
+
|
17
|
+
7. Line item and order will save all prices for each price type.
|
18
|
+
|
19
|
+
### Usage
|
20
|
+
Create your own price type
|
21
|
+
![Price Type](/docs/price-type-1.png?raw=true "Price Type")
|
22
|
+
|
23
|
+
Create your own price book for each currency, price type, store, user roles
|
24
|
+
![Price Book](/docs/price-book-1.png?raw=true "Price Book")
|
25
|
+
|
26
|
+
You can update the prices in price book from parent price book, either filling nil prices from parent price book with adjustment factor or refreshing all prices in price book.
|
27
|
+
![Price Book](/docs/price-book-details-1.png?raw=true "Price Book")
|
28
|
+
|
29
|
+
Update the price from product page with store, price type and role filter. If the price is not set manually, it will show you the reference price.
|
30
|
+
![Variant Prices](/docs/variant-prices.png?raw=true "Variant Prices")
|
31
|
+
|
32
|
+
### Installation
|
33
|
+
Add spree_price_books to your Gemfile:
|
34
|
+
|
35
|
+
```shell
|
36
|
+
gem 'spree_price', github: 'EONIQ/spree_price'
|
37
|
+
```
|
38
|
+
|
39
|
+
Bundle your dependencies and run the installation generator:
|
40
|
+
```shell
|
41
|
+
bundle
|
42
|
+
bundle exec rails g spree_price:install
|
43
|
+
```
|
44
|
+
|
45
|
+
### Configuration
|
46
|
+
Once installed you can seed default currency exchange rates via open exchange rate
|
47
|
+
|
48
|
+
Get your app id from https://openexchangerates.org/signup
|
49
|
+
|
50
|
+
Add open_exchange_rate.rb to config/initializers
|
51
|
+
```ruby
|
52
|
+
Rails.application.config.openExchangeRate = {
|
53
|
+
appId: 'YOUR APP ID HERE',
|
54
|
+
}
|
55
|
+
```
|
56
|
+
|
57
|
+
```shell
|
58
|
+
bundle exec rake spree_price:currency_rates
|
59
|
+
```
|
60
|
+
|
61
|
+
### Testing
|
62
|
+
TODO: Need to work on rspec
|
63
|
+
|
64
|
+
First bundle your dependencies, then run rake. rake will default to building the dummy app if it does not exist, then it will run specs. The dummy app can be regenerated by using rake test_app.
|
65
|
+
|
66
|
+
On the first run you may need to create the Postgresql role (ex. createuser -d postgres)
|
67
|
+
|
68
|
+
```
|
69
|
+
bundle
|
70
|
+
bundle exec rake
|
71
|
+
```
|
72
|
+
|
73
|
+
When testing your applications integration with this extension you may use it's factories. Simply add this require statement to your spec_helper:
|
74
|
+
|
75
|
+
```
|
76
|
+
require 'spree_price/factories'
|
77
|
+
```
|
78
|
+
|
79
|
+
### Credit
|
80
|
+
[spree/spree](https://github.com/spree/spree)
|
81
|
+
[spree-contrib/spree_price_book](https://github.com/spree-contrib/spree_price_books)
|
82
|
+
|
83
|
+
Copyright (c) 2018 EONIQ (HK) LIMITED, released under the New BSD License
|
@@ -0,0 +1,13 @@
|
|
1
|
+
$ ->
|
2
|
+
appendParams = (key, value) ->
|
3
|
+
path = window.location.pathname;
|
4
|
+
url = Spree.url(path + window.location.search).deleteQueryParam(key).addQueryParam(key, value)
|
5
|
+
|
6
|
+
window.location.href = url.toString();
|
7
|
+
|
8
|
+
$('#select_variant_prices_store').on 'change', (event) ->
|
9
|
+
appendParams('store_id', event.val)
|
10
|
+
$('#select_variant_prices_price_type').on 'change', (event) ->
|
11
|
+
appendParams('price_type_id', event.val)
|
12
|
+
$('#select_variant_prices_role').on 'change', (event) ->
|
13
|
+
appendParams('role_id', event.val)
|
@@ -0,0 +1,104 @@
|
|
1
|
+
$ ->
|
2
|
+
class PriceBookVariant
|
3
|
+
constructor: (@variant) ->
|
4
|
+
@id = @variant.id
|
5
|
+
@name = "#{@variant.name} - #{@variant.sku}"
|
6
|
+
@price = 0.0
|
7
|
+
|
8
|
+
update: (price) ->
|
9
|
+
@price = price
|
10
|
+
|
11
|
+
class PriceBookVariants
|
12
|
+
constructor: ->
|
13
|
+
@build_select(Spree.url(Spree.routes.variants_api), 'product_name_or_sku_cont')
|
14
|
+
|
15
|
+
format_variant_result: (result) ->
|
16
|
+
"#{result.name} - #{result.sku}"
|
17
|
+
|
18
|
+
build_select: (url, query) ->
|
19
|
+
$('#price_book_variant').select2
|
20
|
+
minimumInputLength: 3
|
21
|
+
ajax:
|
22
|
+
url: url
|
23
|
+
datatype: "json"
|
24
|
+
data: (term, page) ->
|
25
|
+
query_object = {}
|
26
|
+
query_object[query] = term
|
27
|
+
q: query_object
|
28
|
+
token: Spree.api_key
|
29
|
+
|
30
|
+
results: (data, page) ->
|
31
|
+
result = data["variants"]
|
32
|
+
window.variants = result
|
33
|
+
results: result
|
34
|
+
|
35
|
+
formatResult: @format_variant_result
|
36
|
+
formatSelection: (variant) ->
|
37
|
+
if !!variant.options_text
|
38
|
+
variant.name + " (#{variant.options_text})" + " - #{variant.sku}"
|
39
|
+
else
|
40
|
+
variant.name + " - #{variant.sku}"
|
41
|
+
|
42
|
+
class PriceBookAddVariants
|
43
|
+
constructor: ->
|
44
|
+
@variants = []
|
45
|
+
if $('#price_book_variant_template').length > 0
|
46
|
+
@template = Handlebars.compile $('#price_book_variant_template').html()
|
47
|
+
|
48
|
+
$('button.price_book_add_variant').click (event) =>
|
49
|
+
event.preventDefault()
|
50
|
+
if $('#price_book_variant').select2('data')?
|
51
|
+
@add_variant()
|
52
|
+
else
|
53
|
+
alert('Please select a variant first')
|
54
|
+
|
55
|
+
$('#price_book-variants-table').on 'click', '.price_book_remove_variant', (event) =>
|
56
|
+
event.preventDefault()
|
57
|
+
@remove_variant $(event.target)
|
58
|
+
|
59
|
+
$('button.price_book_new_push').click =>
|
60
|
+
unless @variants.length > 0
|
61
|
+
alert('no variants to transfer')
|
62
|
+
false
|
63
|
+
|
64
|
+
add_variant: ->
|
65
|
+
variant = $('#price_book_variant').select2('data')
|
66
|
+
price = $('#price_book_variant_price').val()
|
67
|
+
|
68
|
+
variant = @find_or_add(variant)
|
69
|
+
variant.update(price)
|
70
|
+
@render()
|
71
|
+
|
72
|
+
find_or_add: (variant) ->
|
73
|
+
if existing = _.find(@variants, (v) -> v.id == variant.id)
|
74
|
+
return existing
|
75
|
+
else
|
76
|
+
variant = new PriceBookVariant($.extend({}, variant))
|
77
|
+
@variants.push variant
|
78
|
+
return variant
|
79
|
+
|
80
|
+
remove_variant: (target) ->
|
81
|
+
variant_id = parseInt(target.data('variantId'))
|
82
|
+
@variants = (v for v in @variants when v.id isnt variant_id)
|
83
|
+
@render()
|
84
|
+
|
85
|
+
clear_variants: ->
|
86
|
+
@variants = []
|
87
|
+
@render()
|
88
|
+
|
89
|
+
contains: (id) ->
|
90
|
+
_.contains(_.pluck(@variants, 'id'), id)
|
91
|
+
|
92
|
+
render: ->
|
93
|
+
if @variants.length is 0
|
94
|
+
$('#price_book-variants-table').hide()
|
95
|
+
$('.no-objects-found').show()
|
96
|
+
else
|
97
|
+
$('#price_book-variants-table').show()
|
98
|
+
$('.no-objects-found').hide()
|
99
|
+
|
100
|
+
rendered = @template { variants: @variants }
|
101
|
+
$('#price_book_variants_tbody').html(rendered)
|
102
|
+
|
103
|
+
price_book_add_variants = new PriceBookAddVariants
|
104
|
+
price_book_variants = new PriceBookVariants
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Spree
|
2
|
+
module Admin
|
3
|
+
class CurrencyRatesController < Spree::Admin::ResourceController
|
4
|
+
def fetch
|
5
|
+
Spree::CurrencyRate.update_from_open_exchange
|
6
|
+
flash[:success] = Spree.t('notice_messages.currency_rate_fetched')
|
7
|
+
redirect_to admin_currency_rates_path
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Spree
|
2
|
+
module Admin
|
3
|
+
class PriceBooksController < Spree::Admin::ResourceController
|
4
|
+
def show
|
5
|
+
@prices = @price_book
|
6
|
+
.prices
|
7
|
+
.includes(variant: [{ option_values: :option_type }, :product])
|
8
|
+
.page(params[:page])
|
9
|
+
end
|
10
|
+
|
11
|
+
def update_price
|
12
|
+
@price_book.update_attributes(prices_params)
|
13
|
+
redirect_to admin_price_book_path(@price_book)
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_price
|
17
|
+
prices_params[:prices_attributes].each do |price_param|
|
18
|
+
price = @price_book.prices.find_or_initialize_by(
|
19
|
+
currency: @price_book.currency,
|
20
|
+
variant_id: price_param[:variant_id].to_i,
|
21
|
+
)
|
22
|
+
price.amount = price_param[:amount].to_f
|
23
|
+
price.save!
|
24
|
+
end
|
25
|
+
redirect_to admin_price_book_path(@price_book)
|
26
|
+
end
|
27
|
+
|
28
|
+
def load_from_parent
|
29
|
+
@price_book.load_prices_from_parent(!params[:force_update].nil?)
|
30
|
+
|
31
|
+
flash[:success] = Spree.t('notice_messages.price_book_loaded_from_parent')
|
32
|
+
redirect_to admin_price_book_path(@price_book)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def prices_params
|
37
|
+
params.require(:price_book).permit(prices_attributes: [:id, :amount, :variant_id])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
Spree::Admin::PricesController.class_eval do
|
2
|
+
before_action :load_extra, only: [:index, :variant_prices]
|
3
|
+
|
4
|
+
def load_extra
|
5
|
+
@store = params[:store_id] ? Spree::Store.find(params[:store_id]) : Spree::Store.first
|
6
|
+
@price_type = params[:price_type_id] ? Spree::PriceType.find(params[:price_type_id]) : Spree::PriceType.first
|
7
|
+
@role = params[:role_id] ? Spree::Role.find(params[:role_id]) : Spree::Role.first
|
8
|
+
end
|
9
|
+
|
10
|
+
def variant_prices
|
11
|
+
@product = Spree::Product.friendly.find(params[:product_id])
|
12
|
+
|
13
|
+
variant_prices_params.each do |variant_id, price_params|
|
14
|
+
price_params.each do |currency, amount|
|
15
|
+
if amount.present?
|
16
|
+
price_book = @store
|
17
|
+
.price_books
|
18
|
+
.by_currency(currency.upcase)
|
19
|
+
.by_price_type(@price_type.try(:id))
|
20
|
+
.by_roles([@role.try(:id)])
|
21
|
+
.first
|
22
|
+
price_book = price_book || Spree::PriceBook
|
23
|
+
.by_currency(currency)
|
24
|
+
.by_price_type(@price_type.try(:id))
|
25
|
+
.first
|
26
|
+
|
27
|
+
price = Spree::Price.find_or_initialize_by(
|
28
|
+
currency: currency.upcase,
|
29
|
+
variant_id: variant_id,
|
30
|
+
price_book_id: price_book.try(:id)
|
31
|
+
)
|
32
|
+
|
33
|
+
price.amount = amount.to_f
|
34
|
+
price.save!
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
flash[:success] = Spree.t('notice_messages.variant_price_updated')
|
40
|
+
redirect_to admin_product_prices_path(
|
41
|
+
@product,
|
42
|
+
store_id: @store.try(:id),
|
43
|
+
role_id: @role.try(:id),
|
44
|
+
price_type_id: @price_type.try(:id)
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def variant_prices_params
|
50
|
+
params.require(:vp).tap do |whitelisted|
|
51
|
+
whitelisted = params[:vp]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
Spree::Admin::StoresController.class_eval do
|
2
|
+
def update_price_book_positions
|
3
|
+
Spree::PriceBook.transaction do
|
4
|
+
params[:positions].each do |id, index|
|
5
|
+
Spree::StorePriceBook
|
6
|
+
.where(price_book_id: id, store_id: params[:id])
|
7
|
+
.update_all(priority: index)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
respond_to do |format|
|
12
|
+
format.html { redirect_to admin_stores_url(params[:id]) }
|
13
|
+
format.js { render plain: 'Ok' }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
Spree::BaseHelper.class_eval do
|
2
|
+
def supported_currencies_options
|
3
|
+
supported_currencies.map do |currency|
|
4
|
+
iso = currency.iso_code
|
5
|
+
["#{currency.name} (#{iso})", iso]
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def price_types_options
|
10
|
+
Spree::PriceType.all.map do |price_type|
|
11
|
+
["#{price_type.name} (#{price_type.code})", price_type.id]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def stores_options
|
16
|
+
Spree::Store.all.map do |store|
|
17
|
+
["#{store.name} (#{store.code})", store.id]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def roles_options
|
22
|
+
Spree::Role.all.map do |role|
|
23
|
+
[role.name, role.id]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def roles_to_s(roles)
|
28
|
+
if roles && !roles.empty?
|
29
|
+
roles.map(&:name).join(', ')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
class Spree::CurrencyRate < Spree::Base
|
2
|
+
validates :base_currency, presence: true
|
3
|
+
validates :currency, presence: true, uniqueness: { scope: :base_currency }
|
4
|
+
validates :exchange_rate, presence: true
|
5
|
+
validate :validate_single_default
|
6
|
+
|
7
|
+
def self.create_default
|
8
|
+
create(base_currency: Spree::Config[:currency], currency: Spree::Config[:currency], default: true)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.default
|
12
|
+
if default = where(default: true).first
|
13
|
+
default
|
14
|
+
else
|
15
|
+
create_default
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.update_from_open_exchange
|
20
|
+
oxr = Money::Bank::OpenExchangeRatesBank.new
|
21
|
+
oxr.app_id = Rails.application.config.openExchangeRate[:appId]
|
22
|
+
oxr.update_rates
|
23
|
+
oxr.cache = 'tmp/cache.json'
|
24
|
+
oxr.ttl_in_seconds = 86400
|
25
|
+
oxr.source = Spree::CurrencyRate.default.currency
|
26
|
+
Money.default_bank = oxr
|
27
|
+
|
28
|
+
Spree::Config[:supported_currencies].split(',').each do |currencyCode|
|
29
|
+
logger.debug "Fetching currency #{currencyCode} from OpenExchange"
|
30
|
+
currency = Money::Currency.new(currencyCode)
|
31
|
+
rate = Money.default_bank.get_rate(Spree::CurrencyRate.default.currency, currency)
|
32
|
+
currencyRate = Spree::CurrencyRate.find_or_create_by(
|
33
|
+
base_currency: Spree::CurrencyRate.default.currency,
|
34
|
+
currency: currency.iso_code,
|
35
|
+
default: (Spree::Config[:currency] == currency.iso_code)
|
36
|
+
)
|
37
|
+
currencyRate.update_attribute(:exchange_rate, rate) if currencyRate
|
38
|
+
logger.debug "Currency #{currencyCode} rate updated: #{rate}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def validate_single_default
|
44
|
+
return unless default?
|
45
|
+
|
46
|
+
matches = self.class.where(default: true)
|
47
|
+
|
48
|
+
if persisted?
|
49
|
+
matches = matches.where('id != ?', id)
|
50
|
+
end
|
51
|
+
|
52
|
+
if matches.exists?
|
53
|
+
errors.add(:default, 'cannot have multiple defaults.')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|