Merge pull request #373 from foodcoops/feature/current-orders-plugin

Add current_orders plugin
This commit is contained in:
wvengen 2015-08-07 19:36:05 +02:00
commit 4a9cf862be
47 changed files with 1093 additions and 13 deletions

View File

@ -54,6 +54,7 @@ gem 'foodsoft_wiki', path: 'plugins/wiki'
gem 'foodsoft_messages', path: 'plugins/messages' gem 'foodsoft_messages', path: 'plugins/messages'
# plugins not enabled by default # plugins not enabled by default
#gem 'foodsoft_current_orders', path: 'plugins/current_orders'
#gem 'foodsoft_uservoice', path: 'plugins/uservoice' #gem 'foodsoft_uservoice', path: 'plugins/uservoice'

View File

@ -20,13 +20,13 @@ class OrderByArticles < OrderPdf
rows << [goa.group_order.ordergroup.name, rows << [goa.group_order.ordergroup.name,
"#{goa.quantity} + #{goa.tolerance}", "#{goa.quantity} + #{goa.tolerance}",
goa.result, goa.result,
number_with_precision(order_article.price.fc_price * goa.result, precision: 2)] number_to_currency(goa.total_price(order_article))]
dimrows << rows.length if goa.result == 0 dimrows << rows.length if goa.result == 0
end end
next if rows.length == 0 next if rows.length == 0
rows.unshift I18n.t('documents.order_by_articles.rows') # table header rows.unshift I18n.t('documents.order_by_articles.rows') # table header
text "#{order_article.article.name} (#{order_article.article.unit} | #{order_article.price.unit_quantity.to_s} | #{number_with_precision(order_article.price.fc_price, precision: 2)})", text "#{order_article.article.name} (#{order_article.article.unit} | #{order_article.price.unit_quantity.to_s} | #{number_to_currency(order_article.price.fc_price)})",
style: :bold, size: fontsize(10) style: :bold, size: fontsize(10)
table rows, cell_style: {size: fontsize(8), overflow: :shrink_to_fit} do |table| table rows, cell_style: {size: fontsize(8), overflow: :shrink_to_fit} do |table|
table.column(0).width = 200 table.column(0).width = 200
@ -36,7 +36,10 @@ class OrderByArticles < OrderPdf
table.cells.border_color = '666666' table.cells.border_color = '666666'
table.rows(0).border_bottom_width = 2 table.rows(0).border_bottom_width = 2
# dim rows which were ordered but not received # dim rows which were ordered but not received
dimrows.each { |ri| table.row(ri).text_color = '999999' } dimrows.each do |ri|
table.row(ri).text_color = '999999'
table.row(ri).columns(0..-1).font_style = nil
end
end end
end end
end end

View File

@ -25,26 +25,30 @@ class OrderByGroups < OrderPdf
sub_total = price * goa.result sub_total = price * goa.result
total += sub_total total += sub_total
rows << [goa.order_article.article.name, rows << [goa.order_article.article.name,
"#{goa.quantity} + #{goa.tolerance}", goa.tolerance > 0 ? "#{goa.quantity} + #{goa.tolerance}" : goa.quantity,
goa.result, goa.result,
number_with_precision(price, precision: 2), number_to_currency(price),
goa.order_article.price.unit_quantity, goa.order_article.price.unit_quantity,
goa.order_article.article.unit, goa.order_article.article.unit,
number_with_precision(sub_total, precision: 2)] number_to_currency(sub_total)]
dimrows << rows.length if goa.result == 0 dimrows << rows.length if goa.result == 0
end end
next if rows.length == 0 next if rows.length == 0
rows << [ I18n.t('documents.order_by_groups.sum'), nil, nil, nil, nil, nil, number_with_precision(total, precision: 2)] rows << [ I18n.t('documents.order_by_groups.sum'), nil, nil, nil, nil, nil, number_to_currency(total)]
rows.unshift I18n.t('documents.order_by_groups.rows') # Table Header rows.unshift I18n.t('documents.order_by_groups.rows') # Table Header
text group_order.ordergroup.name, size: fontsize(9), style: :bold text group_order.ordergroup.name, size: fontsize(9), style: :bold
table rows, width: 500, cell_style: {size: fontsize(8), overflow: :shrink_to_fit} do |table| table rows, width: 500, cell_style: {size: fontsize(8), overflow: :shrink_to_fit} do |table|
# borders # borders
table.cells.borders = [] table.cells.borders = [:bottom]
table.row(0).borders = [:bottom] table.cells.border_width = 0.02
table.row(group_order_articles.size).borders = [:bottom] table.cells.border_color = 'dddddd'
table.cells.border_width = 1 table.rows(0).border_width = 1
table.cells.border_color = '666666' table.rows(0).border_color = '666666'
table.rows(0).column(5).font_style = :bold
table.row(rows.length-2).border_width = 1
table.row(rows.length-2).border_color = '666666'
table.row(rows.length-1).borders = []
table.column(0).width = 240 table.column(0).width = 240
table.column(2).font_style = :bold table.column(2).font_style = :bold
@ -53,7 +57,10 @@ class OrderByGroups < OrderPdf
table.column(6).font_style = :bold table.column(6).font_style = :bold
# dim rows which were ordered but not received # dim rows which were ordered but not received
dimrows.each { |ri| table.row(ri).text_color = '999999' } dimrows.each do |ri|
table.row(ri).text_color = '999999'
table.row(ri).columns(0..-1).font_style = nil
end
end end
end end

View File

@ -1680,6 +1680,7 @@ nl:
title_all: Alle groepstaken title_all: Alle groepstaken
ui: ui:
actions: Acties actions: Acties
back: Terug
cancel: Annuleren cancel: Annuleren
close: Sluiten close: Sluiten
copy: Kopiëren copy: Kopiëren

View File

@ -0,0 +1,42 @@
FoodsoftCurrentOrders
=====================
Foodsoft is currently designed to work with one order at a time. In practice,
however there can be multiple orders open at the same time, with one pickup
day. The proper solution to this is to introduce the notion of order cycles,
with each order belonging to a cycle. Until that time, we have this plugin,
with screens for working on all orders that are closed-but-not-finished.
Important: be sure to settle orders from the previous order cycle, before
you close any. If you don't, articles from previous and current dates start
to mix up (if you do, settle the old ones asap).
* `current_orders/orders/receive` for a list of orders that can be received.
* `current_orders/orders.pdf?document=(groups|articles)` for PDFs for all
orders that are closed but not settled.
* `current_orders/articles` to edit an order article's ordergroups in all
orders that are closed but not settled.
* `current_orders/ordergroups` to edit an ordergroup's order articles in all
orders that are closed but not settled.
* `current_orders/group_orders` for all articles in the user's group orders
from orders that are not settled. Can be used as a "shopping-cart overview"
or "checkout" page.
New menu items will be added in the "Orders" menu. Please note that members
with _Orders_ permission will now be able to edit the amounts members received
in some of these screens, something that was previously restricted to the
_Finance_ permission.
This plugin is not enabled by default. To install it, add uncomment the
corresponding line in the `Gemfile`, or add:
```Gemfile
gem 'foodsoft_current_orders', path: 'plugins/current_orders'
```
This plugin introduces the foodcoop config option `use_current_orders`, which
needs to be set to `true` to enable the plugin. This can be done in the
configuration screen or `config/app_config.yml`.
This plugin is part of the foodsoft package and uses the GPL-3 license or later
(see foodsoft's LICENSE for the full license text).

View File

@ -0,0 +1,46 @@
# encoding: utf-8
class CurrentOrders::ArticlesController < ApplicationController
before_filter :authenticate_orders
before_filter :find_order_and_order_article, only: [:index, :show]
def index
# sometimes need to pass id as parameter for forms
show if @order_article
end
def show
respond_to do |format|
format.html { render :show }
format.js { render :show, layout: false }
end
end
def show_on_group_order_article_create
@goa = GroupOrderArticle.find(params[:group_order_article_id])
end
def show_on_group_order_article_update
#@goa = GroupOrderArticle.find(params[:group_order_article_id])
end
protected
def find_order_and_order_article
@current_orders = Order.finished_not_closed
unless params[:order_id].blank?
@order = Order.find(params[:order_id])
@order_articles = @order.order_articles
else
@order_articles = OrderArticle.where(order_id: @current_orders.all.map(&:id))
end
@q = OrderArticle.search(params[:q])
@order_articles = @order_articles.ordered.merge(@q.result).includes(:article, :article_price)
@order_article = @order_articles.where(id: params[:id]).first
end
helper_method \
def ordergroups_for_adding
Ordergroup.undeleted.order(:name)
end
end

View File

@ -0,0 +1,26 @@
class CurrentOrders::GroupOrdersController < ApplicationController
# Security
before_filter :ensure_ordergroup_member
def index
# XXX code duplication lib/foodsoft_current_orders/app/controllers/current_orders/ordergroups_controller.rb
@order_ids = Order.where(state: ['open', 'finished']).all.map(&:id)
@goas = GroupOrderArticle.includes(:group_order => :ordergroup).includes(:order_article).
where(group_orders: {order_id: @order_ids, ordergroup_id: @ordergroup.id}).ordered
@articles_grouped_by_category = @goas.includes(:order_article => {:article => :article_category}).
order('articles.name').
group_by { |a| a.order_article.article.article_category.name }.
sort { |a, b| a[0] <=> b[0] }
end
private
# XXX code duplication from GroupOrdersController
def ensure_ordergroup_member
@ordergroup = @current_user.ordergroup
if @ordergroup.nil?
redirect_to root_url, :alert => I18n.t('group_orders.errors.no_member')
end
end
end

View File

@ -0,0 +1,47 @@
# encoding: utf-8
class CurrentOrders::OrdergroupsController < ApplicationController
before_filter :authenticate_orders
before_filter :find_group_orders, only: [:index, :show]
def index
# sometimes need to pass id as parameter for forms
render 'show' if @ordergroup
end
def show
respond_to do |format|
format.html { render :show }
format.js { render :show, layout: false }
end
end
def show_on_group_order_article_create
@goa = GroupOrderArticle.find(params[:group_order_article_id])
end
def show_on_group_order_article_update
#@goa = GroupOrderArticle.find(params[:group_order_article_id])
@group_order = GroupOrder.find(params[:group_order_id])
@ordergroup = @group_order.ordergroup
end
protected
def find_group_orders
@order_ids = Order.finished_not_closed.map(&:id)
@all_ordergroups = Ordergroup.undeleted.order(:name).to_a
@ordered_group_ids = GroupOrder.where(order_id: @order_ids).pluck('DISTINCT(ordergroup_id)')
@all_ordergroups.sort_by! {|o| @ordered_group_ids.include?(o.id) ? o.name : "ZZZZZ#{o.name}" }
@ordergroup = Ordergroup.find(params[:id]) unless params[:id].nil?
@goas = GroupOrderArticle.includes(:group_order, :order_article => [:article, :article_price]).
where(group_orders: {order_id: @order_ids, ordergroup_id: @ordergroup.id}).ordered.all unless @ordergroup.nil?
end
helper_method \
def articles_for_adding
OrderArticle.includes(:article, :article_price).where(order_id: @order_ids)
end
end

View File

@ -0,0 +1,40 @@
class CurrentOrders::OrdersController < ApplicationController
before_filter :authenticate_orders, except: :my
def show
@doc_options ||= {}
@order_ids = if params[:id]
params[:id].split('+').map(&:to_i)
else
Order.finished_not_closed.all.map(&:id)
end
@view = (params[:view] or 'default').gsub(/[^-_a-zA-Z0-9]/, '')
respond_to do |format|
format.pdf do
pdf = case params[:document]
when 'groups' then MultipleOrdersByGroups.new(@order_ids, @doc_options)
when 'articles' then MultipleOrdersByArticles.new(@order_ids, @doc_options)
end
send_data pdf.to_pdf, filename: pdf.filename, type: 'application/pdf'
end
end
end
def my
@doc_options ||= {}
@doc_options[:ordergroup] = @current_user.ordergroup.id
respond_to do |format|
format.pdf do
params[:document] = 'groups'
show
end
end
end
def receive
@orders = Order.finished_not_closed.includes(:supplier)
end
end

View File

@ -0,0 +1,6 @@
# encoding: utf-8
class CurrentOrdersController < ApplicationController
before_filter :authenticate_orders
end

View File

@ -0,0 +1,73 @@
# encoding: utf-8
class MultipleOrdersByArticles < OrderPdf
include OrdersHelper
def filename
I18n.t('documents.multiple_orders_by_articles.filename', count: @order.count) + '.pdf'
end
def title
I18n.t('documents.multiple_orders_by_articles.title', count: @order.count)
end
def order_articles
@order_articles ||= OrderArticle.joins(:order, :article).where(:orders => {:id => @order}).ordered.reorder('orders.id, articles.name')
end
# @todo refactor to reduce common code with order_by_articles
def body
order_articles.each do |order_article|
down_or_page
rows = []
dimrows = []
has_units_str = ''
for goa in order_article.group_order_articles.ordered
rows << [goa.group_order.ordergroup.name,
goa.tolerance > 0 ? "#{goa.quantity} + #{goa.tolerance}" : goa.quantity,
goa.result,
number_to_currency(goa.total_price(order_article))]
dimrows << rows.length if goa.result == 0
end
next if rows.length == 0
sum = order_article.group_orders_sum
rows.unshift I18n.t('documents.order_by_articles.rows').dup # table header
rows << [I18n.t('documents.order_by_groups.sum'),
order_article.tolerance > 0 ? "#{order_article.quantity} + #{order_article.tolerance}" : order_article.quantity,
sum[:quantity],
nil] #number_to_currency(sum[:price])]
text "<b>#{order_article.article.name}</b> " +
"(#{order_article.article.unit}; #{number_to_currency order_article.price.fc_price}; " +
units_history_line(order_article, plain: true) + ')',
size: fontsize(10), inline_format: true
table rows, cell_style: {size: fontsize(8), overflow: :shrink_to_fit} do |table|
# borders
table.cells.borders = [:bottom]
table.cells.border_width = 0.02
table.cells.border_color = 'dddddd'
table.rows(0).border_width = 1
table.rows(0).border_color = '666666'
table.row(rows.length-2).border_width = 1
table.row(rows.length-2).border_color = '666666'
table.row(rows.length-1).borders = []
table.column(0).width = 200
table.columns(1..2).align = :center
table.column(2).font_style = :bold
table.columns(3).align = :right
# dim rows which were ordered but not received
dimrows.each { |ri| table.row(ri).text_color = '999999' }
end
end
end
protected
def pdf_add_page_breaks?
super 'order_by_articles'
end
end

View File

@ -0,0 +1,106 @@
# encoding: utf-8
class MultipleOrdersByGroups < OrderPdf
include OrdersHelper
def filename
I18n.t('documents.multiple_orders_by_groups.filename', count: @order.count) + '.pdf'
end
def title
I18n.t('documents.multiple_orders_by_groups.title', count: @order.count)
end
def ordergroups
unless @ordergroups
@ordergroups = Ordergroup.joins(:orders).where(orders: {id: @order}).select('distinct(groups.id)').select('groups.*').reorder(:name)
@ordergroups = @ordergroups.where(id: @options[:ordergroup]) if @options[:ordergroup]
end
@ordergroups
end
# @todo refactor to reduce common code with order_by_groups
def body
# Start rendering
ordergroups.each do |ordergroup|
down_or_page 15
total = 0
taxes = Hash.new {0}
rows = []
dimrows = []
group_order_articles = GroupOrderArticle.ordered.joins(:group_order => :order).where(:group_orders =>{:ordergroup_id => ordergroup.id}).where(:orders => {id: @order}).includes(:order_article => :article_price).reorder('orders.id')
has_tolerance = group_order_articles.where('article_prices.unit_quantity > 1').any?
group_order_articles.each do |goa|
price = goa.order_article.price.fc_price
sub_total = goa.total_price
total += sub_total
rows << [goa.order_article.article.name,
goa.group_order.order.name.truncate(10, omission: ''),
number_to_currency(price),
goa.order_article.article.unit,
goa.tolerance > 0 ? "#{goa.quantity} + #{goa.tolerance}" : goa.quantity,
goa.result,
number_to_currency(sub_total),
(goa.order_article.price.unit_quantity if has_tolerance)]
dimrows << rows.length if goa.result == 0
end
next if rows.length == 0
# total
rows << [{content: I18n.t('documents.order_by_groups.sum'), colspan: 6}, number_to_currency(total), nil]
# table header
rows.unshift [
OrderArticle.human_attribute_name(:article),
Article.human_attribute_name(:supplier),
I18n.t('documents.order_by_groups.rows')[3],
Article.human_attribute_name(:unit),
I18n.t('shared.articles.ordered'),
I18n.t('shared.articles.received'),
I18n.t('shared.articles_by.price_sum'),
has_tolerance ? {image: "#{Rails.root}/app/assets/images/package-bg.png", scale: 0.6, position: :center} : nil
]
text ordergroup.name, size: fontsize(13), style: :bold
table rows, width: bounds.width, cell_style: {size: fontsize(8), overflow: :shrink_to_fit} do |table|
# borders
table.cells.borders = [:bottom]
table.cells.border_width = 0.02
table.cells.border_color = 'dddddd'
table.rows(0).border_width = 1
table.rows(0).border_color = '666666'
table.rows(0).column(5).font_style = :bold
table.row(rows.length-2).border_width = 1
table.row(rows.length-2).border_color = '666666'
table.row(rows.length-1).borders = []
table.column(0).width = 180 # @todo would like to set minimum width here
table.column(1).width = 62
table.column(2).align = :right
table.column(5..6).font_style = :bold
table.columns(3..5).align = :center
table.column(6).align = :right
table.column(7).align = :center
# dim rows not relevant for members
table.column(4).text_color = '999999'
table.column(7).text_color = '999999'
# hide unit_quantity if there's no tolerance anyway
table.column(-1).width = has_tolerance ? 20 : 0
# dim rows which were ordered but not received
dimrows.each do |ri|
table.row(ri).text_color = 'aaaaaa'
table.row(ri).columns(0..-1).font_style = nil
end
end
end
end
protected
def pdf_add_page_breaks?
super 'order_by_groups'
end
end

View File

@ -0,0 +1,14 @@
module CurrentOrdersHelper
def to_pay_message(ordergroup)
funds = ordergroup.get_available_funds
if funds > 0
content_tag :b, I18n.t('helpers.current_orders.pay_done'), style: 'color: green'
elsif funds == 0
I18n.t('helpers.current_orders.pay_none')
else
content_tag :b, I18n.t('helpers.current_orders.pay_amount', amount: number_to_currency(-ordergroup.get_available_funds))
end
end
end

View File

@ -0,0 +1,2 @@
/ insert_after 'erb:contains(":use_nick")'
= config_input form, :use_current_orders, as: :boolean

View File

@ -0,0 +1,8 @@
/ insert_before '#articles_table'
- if FoodsoftCurrentOrders.enabled?
- unless @orders.nil? or @orders.empty? or @order_articles.nil? or @order_articles.empty?
- content_for :actionbar do
= link_to url_for(controller: 'current_orders/orders', action: 'my', id: @orders.map(&:id).join('+'), format: 'pdf'), class: 'btn' do
= glyph :download
PDF

View File

@ -0,0 +1,9 @@
/ insert_before 'h2:contains(".orders_finished")'
- if FoodsoftCurrentOrders.enabled?
.btn-group.pull-right#orders_finished_toolbar
= link_to '#', data: {toggle: 'dropdown'}, class: 'btn dropdown-toggle' do
= t 'orders.show.download.title'
%span.caret
%ul.dropdown-menu
%li= link_to t('orders.show.download.group_pdf'), current_orders_orders_path(document: :groups, format: :pdf)
%li= link_to t('orders.show.download.article_pdf'), current_orders_orders_path(document: :articles, format: :pdf)

View File

@ -0,0 +1,7 @@
.btn-group.pull-right
= link_to '#', data: {toggle: 'dropdown'}, class: 'btn dropdown-toggle' do
= t 'orders.show.download.title'
%span.caret
%ul.dropdown-menu
%li= link_to t('orders.show.download.group_pdf'), current_orders_orders_path(document: :groups, format: :pdf)
%li= link_to t('orders.show.download.article_pdf'), current_orders_orders_path(document: :articles, format: :pdf)

View File

@ -0,0 +1,13 @@
#order_article
- if order_article
= render 'article_info', order_article: order_article
= render 'ordergroups', order_article: order_article
- else
%h2= t('current_orders.articles.index.title')
#articles_by_articles
%p
%i= t '.counts', ordergroups: Ordergroup.joins(:orders).where(orders: {state: 'finished'}).count(distinct: true), articles: @order_articles.count
%p
%i= t '.no_selection'

View File

@ -0,0 +1,22 @@
#article_info
%h2{style: 'margin-bottom: 0'}
= t('current_orders.articles.show.title', name: order_article.article.name)
%span.normal= order_article.article.unit
-# @todo unduplicate from group_orders's order_article_info
%p
- if order_article.article.manufacturer.blank?
= raw t '.supplied_by', supplier: content_tag(:em, supplier_link(order_article.article.supplier))
- elsif order_article.article.supplier.name == order_article.article.manufacturer
= raw t '.supplied_and_made_by', manufacturer: content_tag(:em, supplier_link(order_article.article.supplier))
- else
= raw t '.supplied_by_made_by', supplier: content_tag(:em, supplier_link(order_article.article.supplier)), manufacturer: content_tag(:em, order_article.article.manufacturer)
- unless order_article.article.origin.blank?
= raw t '.origin_in', origin: content_tag(:em, order_article.article.origin)
- pkg_info = pkg_helper(order_article.price)
= ", #{pkg_info}".html_safe unless pkg_info.blank?
= ", "
= Article.human_attribute_name(:fc_price_short) + ": "
= number_to_currency(order_article.price.fc_price)
= t '.unit', unit: order_article.article.unit

View File

@ -0,0 +1,4 @@
- @order_articles.includes(:order => :supplier).reorder('suppliers.name, articles.article_category_id, articles.name').group_by {|oa| oa.order.name}.each do |name, articles|
%li.nav-header= name
- articles.each do |oa|
%li= link_to oa.article.name, current_orders_articles_path(order_id: oa.order_id, id: oa.id, anchor: 'order_article'), remote: true

View File

@ -0,0 +1,74 @@
.row-fluid
.span4
.well
%ul.nav.nav-list#article_list
%li.keep
= search_form_for @q, url: current_orders_articles_path, method: 'get', html: {data: {'submit-onchange' => true}, class: 'form-search'}, remote: true do |f|
.input-append
= f.text_field :article_name_cont, placeholder: t('.article_placeholder'), class: 'search-query input-block-level resettable'
%button.add-on.btn.reset-search{type: 'button'}
%i.icon.icon-remove
= render 'articles', orders: @current_orders
.span8
= render 'actions'
= render 'article', order_article: @order_article
= render 'shared/articles_by/common'
- content_for :javascript do
:javascript
// update number of received items - would prefer to do this server-side to
// keep working when showing a partial list, but this avoids an extra ajax call
$(document).on('GroupOrderArticle#update #update_articles_summary', function(e) {
var count_sum = 0;
// number of received items
$('#articles_by_articles input[data-delta]').each(function() {
count_sum += Number($(this).val());
});
$('#single_order_article_total .count_sum').html(count_sum);
// delta
// @todo partial code-duplication with orders/_edit_amounts
var expected = $('#single_order_article_delta .units_delta').data('quantity-expected');
var delta = Math.round((count_sum - expected)*100)/100.0;
if (isNaN(delta)) {
html = '';
color = 'inherit';
} else if (delta == 0) {
html = I18n.t('js.current_orders.articles.equal');
color = 'green';
//$('#single_order_article_total .count_sum').html(count_sum + ' <i class="icon-ok"></i>');
} else if (delta < 0) {
html = I18n.t('js.current_orders.articles.below', {count: -delta});
color = 'inherit';
} else {
html = I18n.t('js.current_orders.articles.above', {count: delta});
color = 'red';
}
$('#single_order_article_delta .units_delta').html(html).attr('style', 'color: '+color);
$('#single_order_article_total .count_sum').attr('style', 'color: '+color);
});
$(document).on('#update_articles_summary', function() {
// initialize add ordergroup control
$('#group_order_article_ordergroup_id').select2().select2('data', null);
});
$(function() {
// initialize when loaded as html
$(document).trigger('#update_articles_summary');
});
$(document).on('GroupOrderArticle#create', function(e) {
var base_unit = $('#articles_by_articles').data('base-unit');
// reset selection
$('#group_order_article_ordergroup_id').select2('data', null);
// update table
$.ajax({
url: '#{show_on_group_order_article_create_current_orders_articles_path}',
type: 'get',
data: {group_order_article_id: e.group_order_article_id, base_unit: base_unit}
});
});

View File

@ -0,0 +1,34 @@
%table.table.table-hover#articles_by_articles
%thead.list-heading
%tr
%th{:style => 'width:70%'}= Ordergroup.model_name.human
%th.center.dimmed
%acronym{:title => t('shared.articles.ordered_desc')}= t 'shared.articles.ordered'
%th.center
%acronym{:title => t('shared.articles.received_desc')}= t 'shared.articles.received'
%td.center
.btn-group
= render 'shared/articles_by/article_single', order_article: order_article, heading: false, delta_column: true, base_unit: params[:base_unit]
%tfoot
%tr
%td
= form_for GroupOrderArticle.new, remote: true, html: {'data-submit-onchange' => true, style: 'margin: 0'} do |f|
= f.hidden_field :order_article_id, value: order_article.id
= f.select :ordergroup_id,
options_for_select(ordergroups_for_adding.map { |g| [ g.name, g.id ] }),
{include_blank: true}, {style: 'width: 100%', 'data-placeholder' => t('.add_new')}
%td{colspan: 3}
%tr#single_order_article_total
%th= t 'shared.articles_by.price_sum'
%td.center.dimmed #{order_article.quantity} + #{order_article.tolerance}
- sum = order_article.group_orders_sum
%th.center.count_sum= sum[:quantity]
%td
%tr.no-top-border#single_order_article_delta
%td
%td
%td.center
%span.units_delta{data: {'quantity-expected' => order_article.units * order_article.price.unit_quantity}}
%td

View File

@ -0,0 +1,3 @@
- title t('.title'), false
= render 'form'

View File

@ -0,0 +1,2 @@
$('#article_list li:not(.keep)').remove();
$('#article_list').append('<%= j(render('articles', orders: @current_orders)) %>');

View File

@ -0,0 +1,3 @@
- title t('.title', name: @order_article.article.name), false
= render 'form'

View File

@ -0,0 +1,3 @@
$('#order_article').replaceWith('<%= j(render('article', order_article: @order_article)) if @order_article %>');
// add javascript improvements (delta)
$(document).trigger('#update_articles_summary');

View File

@ -0,0 +1,7 @@
// Handle more advanced DOM update after AJAX database manipulation.
// See publish/subscribe design pattern in /doc.
(function(w) {
$('#goa_<%= @goa.id %>').remove(); // just to be sure: remove table row which is added below
$('#articles_by_articles tbody').append('<%= j render('shared/articles_by/article_single_goa', goa: @goa, base_unit: params[:base_unit]) %>').addClass('success');
})(window);

View File

@ -0,0 +1,8 @@
.pull-right
= link_to t('ui.back'), root_path(anchor: ''), class: 'btn'
%p.pull-left
= link_to t('current_orders.ordergroups.payment_bar.account_balance'), my_ordergroup_path
= number_to_currency (ordergroup.account_balance)
%p#to_pay_message{style: 'text-align: center'}= to_pay_message(ordergroup)

View File

@ -0,0 +1,55 @@
-# XXX code duplication of foodcoop-adam's app/views/group_orders/show.html.haml
- if @articles_grouped_by_category.count > 0
%table.table.table-hover
%thead
%tr
%th{style: "width:40%"}= heading_helper Article, :name
%th= heading_helper Article, :unit
%th= t 'group_orders.show.articles.unit_price'
%th
%abbr{title: t('group_orders.show.articles.ordered_title')}= t 'group_orders.show.articles.ordered'
%th
%abbr{title: t('group_orders.show.articles.order_nopen_title')}
- if (@order.open? rescue true)
= t 'group_orders.show.articles.order_open'
- else
= t 'group_orders.show.articles.order_not_open'
%th= heading_helper GroupOrderArticle, :total_price
%tbody
- group_order_sum = 0
- for category_name, goas in @articles_grouped_by_category
%tr.article-category.list-heading
%td
= category_name
%i.icon-tag
%td{colspan: "9"}
- goas.each do |goa|
- # get the order-results for the ordergroup
- oa = goa.order_article
- r = {quantity: goa.quantity, tolerance: goa.tolerance, result: goa.result, sub_total: goa.total_price(oa)}
- group_order_sum += r[:sub_total]
%tr{class: cycle('even', 'odd', name: 'articles') + " order-article " + order_article_class_name(r[:quantity], r[:tolerance], r[:result])}
-# article_info is present in foodcoop-adam only
%td.name{style: "width:40%", title: (article_info_title(oa.article) rescue nil)}
= article_info_icon oa.article rescue nil
= oa.article.name
%td
= oa.article.unit
%span{style: 'opacity: 0.4; margin-left: 1em;'}= pkg_helper(oa.price, soft_uq: true)
%td= number_to_currency oa.price.fc_price
%td
= r[:quantity]
= "+ #{r[:tolerance]}" if oa.price.unit_quantity > 1
%td= r[:result] > 0 ? r[:result] : "0"
%td= number_to_currency(r[:sub_total])
- unless oa.article.note.blank?
%tr{id: "note_#{oa.id}", class: "note even", style: "display:none"}
%td{colspan: "6"}=h oa.article.note
%tr{class: cycle('even', 'odd', name: 'articles')}
%th{colspan: "5"}= heading_helper GroupOrder, :price
%th= number_to_currency(group_order_sum)
- elsif @articles_grouped_by_category.count == 0
= t 'group_orders.show.articles.no_articles'
- else
= t 'group_orders.show.articles.order_closed_msg'

View File

@ -0,0 +1,13 @@
- title t('.title')
// Article box
%section
.column_content#result
= render 'result'
.well
= render 'payment_bar', ordergroup: @ordergroup
%br/
= link_to_top

View File

@ -0,0 +1,12 @@
-# output row
%tr{:class => [cycle('even', 'odd', :name => 'articles'), if goa.result == 0 then 'unavailable' end], id: "goa_#{goa.id}"}
%td.name= goa.order_article.article.name
%td{title: goa.order_article.order.name}= link_to goa.order_article.order.name.truncate(15), goa.order_article.order
%td= goa.order_article.article.unit
%td.center= "#{goa.quantity} + #{goa.tolerance}"
%td.center.input-delta= group_order_article_edit_result(goa)
%td.symbol
&times;
%td= number_to_currency goa.order_article.price.fc_price
%td.symbol =
%td.price{data: {value: goa.total_price}}= number_to_currency goa.total_price

View File

@ -0,0 +1,46 @@
- if @ordergroup
%table.table.table-hover#articles_by_groups_table{data: {'base-unit' => params[:base_unit]}}
%thead
%tr
%th{style: 'width: 35%'}= heading_helper Article, :name
%th= Order.model_name.human
%th= heading_helper Article, :unit
%th.center
%acronym{:title => t('shared.articles.ordered_desc')}= t 'shared.articles.ordered'
%th.center{style: 'width: 88px'}
%acronym{:title => t('shared.articles.received_desc')}= t 'shared.articles.received'
%th.symbol
%th= heading_helper Article, :fc_price, short: true
%th.symbol
- total = 0
%tbody.list
- if @goas and @goas.length > 0
- for goa in @goas
- total += goa.total_price
= render 'article', goa: goa, base_unit: params[:base_unit]
- else
%tr
%td{colspan: 9}
%i No articles for #{@ordergroup.name} in the current orders.
%tfoot
%tr
%td{colspan: 9}
- new_article_data = articles_for_select2(articles_for_adding) {|a| "#{a.article.name} (#{a.article.unit}, #{number_to_currency a.price.fc_price})"}
= form_for GroupOrderArticle.new, remote: true, html: {'data-submit-onchange' => true, style: 'margin: 0'} do |f|
= f.select :order_article_id,
options_for_select(new_article_data.map {|a| [a[:text], a[:id]]}),
{}, {style: 'width: 500px', 'data-placeholder' => t('.add_new') }
= f.hidden_field :ordergroup_id, value: @ordergroup.id
%tr#single_ordergroup_total{:class => cycle('even', 'odd', :name => 'articles')}
%th{colspan: 8}= t 'shared.articles_by.price_sum'
%th.price_sum{data: {value: total}}= number_to_currency(total)
.well#payment_bar
= render 'payment_bar', ordergroup: @ordergroup
- else
%i= t '.no_selection'

View File

@ -0,0 +1,71 @@
.well
= form_tag current_orders_ordergroups_path, method: :get, 'data-submit-onchange' => true, style: 'margin: 0' do
= select_tag 'id',
options_for_select(@all_ordergroups.map { |g| [ g.name, g.id, {class: ('muted' unless @ordered_group_ids.include? g.id)}] }, (@ordergroup.id rescue '')),
include_blank: true, id: 'ordergroup_select', style: 'min-width: 300px',
'data-placeholder' => t('.ordergroup_placeholder'), 'data-submit-on-change' => :true
-#.form-search.pull-right # see below why this is disabled
.input-append
= text_field_tag :query, params[:query], class: 'search-query delayed-search resettable', disabled: @ordergroup.nil?,
'placeholder' => t('orders.show.search_placeholder.articles')
%button.add-on.btn.reset-search{:type => :button, :title => t('orders.show.search_reset')}
%i.icon.icon-remove
- if @ordergroup and FoodsoftConfig[:price_markup_list]
%span.price_markup_note{style: 'margin-left: 1em'}= show_price_markup @ordergroup, format: :full_label, optional: true
#articles_by_groups
= render 'articles'
= render 'shared/articles_by/common', order: @order
- content_for :javascript do
:javascript
$(function() {
// TODO group by ordered / not-ordered
$('#ordergroup_select').select2();
// add article
$('#group_order_article_order_article_id').select2({
placeholder: '#{j t('orders.receive.add_article')}',
formatNoMatches: function(term) { return '#{j t('.no_articles_available')}';}
});
$(document).on('GroupOrderArticle#create', function(e) {
var base_unit = $('#articles_by_groups_table').data('base-unit');
// reset selection
$('#group_order_article_order_article_id').select2('data', null);
// update table
$.ajax({
url: '#{show_on_group_order_article_create_current_orders_ordergroups_path}',
type: 'get',
data: {group_order_article_id: e.group_order_article_id, base_unit: base_unit}
});
});
$(document).on('GroupOrderArticle#update', function(e) {
$.ajax({
url: '#{show_on_group_order_article_update_current_orders_ordergroups_path}',
type: 'get',
data: {group_order_id: e.group_order_id, group_order_article_id: e.group_order_article_id}
});
});
// article search
// DO NOT USE because listjs can't handle updates https://github.com/javve/list.js/issues/86
/*
new List(document.body, {
valueNames: ['name'],
engine: 'unlist',
plugins: [
['reset', {highlightClass: 'btn-primary'}],
['delay', {delayedSearchTime: 500}],
],
// make large pages work too (as we don't have paging)
page: 10000,
indexAsync: true
});
*/
});

View File

@ -0,0 +1,14 @@
- if current_user.role_finance?
.pull-right
= t('.payment')
- if current_user.role_finance?
= link_to t('.new_transaction'), new_finance_ordergroup_transaction_path(ordergroup), class: 'btn'
%p.pull-left
- if current_user.role_finance?
= link_to 'Account balance', finance_ordergroup_transactions_path(ordergroup)
- else
= t '.account_balance'
of #{ordergroup.name}: #{number_to_currency ordergroup.account_balance}
%p#to_pay_message{style: 'text-align: center'}= to_pay_message(ordergroup)

View File

@ -0,0 +1,3 @@
- title t('.title')
= render 'form'

View File

@ -0,0 +1,3 @@
- title t('.title', name: @ordergroup.name)
= render 'form'

View File

@ -0,0 +1,3 @@
// untested
$('h1, title').html('<%= j t('.title', name: @ordergroup.name) %>');
$('#articles_by_groups').html('<%= j render('articles') %>');

View File

@ -0,0 +1,7 @@
// Handle more advanced DOM update after AJAX database manipulation.
// See publish/subscribe design pattern in /doc.
(function(w) {
$('#goa_<%= @goa.id %>').remove(); // just to be sure: remove table row which is added below
$('#articles_by_groups tbody').append('<%= j render('article', goa: @goa, base_unit: params[:base_unit]) %>').addClass('success');
})(window);

View File

@ -0,0 +1,7 @@
// Handle more advanced DOM update after AJAX database manipulation.
// See publish/subscribe design pattern in /doc.
(function(w) {
// table update is done by group_order_article hook already
$('#to_pay_message').html('<%= j to_pay_message(@ordergroup) %>');
})(window);

View File

@ -0,0 +1,24 @@
- title t('.title')
.well
- if @orders.empty?
= t '.no_finished_orders'
- else
%table.table.table-striped
%thead
%tr
%th= heading_helper Order, :name
%th= heading_helper Order, :ends
%th= heading_helper Order, :note
%th{colspan: "2"}
%tbody
- for order in @orders
%tr
%td= order.name
%td= format_time(order.ends)
%td= truncate(order.note)
%td= receive_button order, class: 'btn-small' unless order.stockit?
%td
= link_to t('ui.show'), order, class: 'btn btn-small'
= order_pdf order, :fax, t('orders.show.download.fax_pdf'), class: 'btn btn-small'

View File

@ -0,0 +1,73 @@
en:
config:
hints:
use_current_orders: Enable the current_orders plugin. Allows members with the order permission to change member amounts in multiple orders, using three new screens in the Orders menu. Especially useful for pick-up days.
keys:
use_current_orders: Extra distribute screens
current_orders:
articles:
article:
counts: '%{ordergroups} ordergroups ordered %{articles} different articles.'
no_selection: Choose an article to show who ordered it, or download pick lists at the right.
article_info:
origin_in: in %{origin}
supplied_by: from %{supplier}
supplied_and_made_by: made by %{manufacturer}
supplied_by_made_by: from %{supplier} made by %{manufacturer}
unit: per %{unit}
from: from %{supplier}
form:
article_placeholder: Search articles...
current_orders: All current orders
index:
title: Distribute articles
ordergroups:
piece: pc
unit: unit
add_new: Add an ordergroup...
show:
title: ! '%{name}'
navigation:
receive: Receive
articles: Distribute
ordergroups: Member orders
group_orders:
index:
title: Your current orders
ordergroups:
articles:
add_new: Add an article...
no_selection: Choose an ordergroup to show the articles.
form:
ordergroup_placeholder: Choose an ordergroup...
index:
title: Articles for ordergroup
payment_bar:
account_balance: Account balance
new_pin: PIN
new_transaction: New transaction
payment: ! 'Payment:'
show:
title: Articles for %{name}
orders:
receive:
title: Receive orders
no_finished_orders: There are currently no orders to receive.
documents:
multiple_orders_by_articles:
filename: Current orders sorted by article
title: Current orders - by article
multiple_orders_by_groups:
filename: Current orders sorted by group
title: Current orders - by group
helpers:
current_orders:
pay_done: Fully paid
pay_none: Nothing to pay
pay_amount: To pay %{amount}
js:
current_orders:
articles:
above: '%{count} more<br>than available'
below: '%{count} left over'
equal: all distributed

View File

@ -0,0 +1,65 @@
nl:
config:
hints:
use_current_orders: De current_orders plugin aanzetten. Hiermee kunnen leden met bestelling-toegang bestelaantallen van leden aanpassen in meerdere bestellingen tegelijk. Hiervoor zijn drie nieuwe items in het Bestellingen-menu. Bedoeld voor gebruik op een ophaaldag.
keys:
use_current_orders: Extra verdeelschermen
current_orders:
articles:
article:
counts: '%{ordergroups} huishoudens bestelden %{articles} verschillende artikelen.'
no_selection: Kies een artikel om te zien wie het besteld heeft, of download verdeellijsten rechtsboven.
article_info:
from: van %{supplier}
unit: per %{unit}
form:
article_placeholder: Zoek artikelen...
current_orders: Alle huidige bestellingen
index:
title: Artikelen verdelen
ordergroups:
piece: st
add_new: Huishouden toevoegen...
show:
title: ! '%{name} verdelen'
navigation:
receive: Ontvangen
articles: Verdelen
ordergroups: Ledenbestellingen
ordergroups:
articles:
add_new: Artikel toevoegen...
form:
ordergroup_placeholder: Kies een huishouden...
no_selection: Kies een huishouden om de artikelen te tonen.
index:
title: Artikelen voor huishouden
payment_bar:
account_balance: Account balance
new_pin: PIN
new_transaction: Nieuwe transactie
payment: ! 'Betaling:'
show:
title: Artikelen voor %{name}
orders:
receive:
title: Bestellingen ontvangen
no_finished_orders: Er zijn momenteel geen bestellingen die ontvangen hoeven te worden.
documents:
multiple_orders_by_articles:
filename: Huidige bestellingen per artikel
title: Huidige bestellingen - per artikel
multiple_orders_by_groups:
filename: Huidige bestellingen per huishouden
title: Huidige bestellingen - per huishouden
js:
current_orders:
articles:
above: '%{count} meer dan<br>beschikbaar'
below: '%{count} blijft over'
equal: alles verdeeld
helpers:
current_orders:
pay_done: Alles betaald
pay_none: Niets te betalen
pay_amount: Te betalen %{amount}

View File

@ -0,0 +1,27 @@
Rails.application.routes.draw do
scope '/:foodcoop' do
namespace :current_orders do
resources :ordergroups, :only => [:index, :show] do
collection do
get :show_on_group_order_article_create
get :show_on_group_order_article_update
end
end
resources :articles, :only => [:index, :show] do
collection do
get :show_on_group_order_article_create
end
end
resource :orders, :only => [:show] do
collection do
get :my
get :receive
end
end
resources :group_orders, :only => [:index]
end
end
end

View File

@ -0,0 +1,20 @@
$:.push File.expand_path("../lib", __FILE__)
# Maintain your gem's version:
require "foodsoft_current_orders/version"
# Describe your gem and declare its dependencies:
Gem::Specification.new do |s|
s.name = "foodsoft_current_orders"
s.version = FoodsoftCurrentOrders::VERSION
s.authors = ["wvengen"]
s.email = ["dev-voko@willem.engen.nl"]
s.homepage = "https://github.com/foodcoop-adam/foodsoft"
s.summary = "Quick support for working on all currently active orders in foodsoft."
s.description = ""
s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"]
s.add_dependency "rails"
s.add_dependency "deface", "~> 1.0.0"
end

View File

@ -0,0 +1,8 @@
require "deface"
require "foodsoft_current_orders/engine"
module FoodsoftCurrentOrders
def self.enabled?
FoodsoftConfig[:use_current_orders]
end
end

View File

@ -0,0 +1,15 @@
module FoodsoftCurrentOrders
class Engine < ::Rails::Engine
def navigation(primary, context)
return unless FoodsoftCurrentOrders.enabled?
return if primary[:orders].nil?
cond = Proc.new { current_user.role_orders? }
[
SimpleNavigation::Item.new(primary, :stage_divider, nil, nil, class: 'divider', if: cond),
SimpleNavigation::Item.new(primary, :current_orders_receive, I18n.t('current_orders.navigation.receive'), context.receive_current_orders_orders_path, if: cond),
SimpleNavigation::Item.new(primary, :current_orders_articles, I18n.t('current_orders.navigation.articles'), context.current_orders_articles_path, if: cond),
SimpleNavigation::Item.new(primary, :current_orders_ordergroups, I18n.t('current_orders.navigation.ordergroups'), context.current_orders_ordergroups_path, if: cond)
].each {|i| primary[:orders].sub_navigation.items << i }
end
end
end

View File

@ -0,0 +1,3 @@
module FoodsoftCurrentOrders
VERSION = "0.0.1"
end