diff --git a/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_admin_item_controller.rb b/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_admin_item_controller.rb new file mode 100644 index 0000000..5149e38 --- /dev/null +++ b/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_admin_item_controller.rb @@ -0,0 +1,17 @@ +class OrdersCyclePluginAdminItemController < OrdersPluginAdminItemController + + no_design_blocks + + # FIXME: remove me when styles move from consumers_coop plugin + include ConsumersCoopPlugin::ControllerHelper + include OrdersCyclePlugin::TranslationHelper + + helper OrdersCyclePlugin::TranslationHelper + helper OrdersCyclePlugin::DisplayHelper + + protected + + extend HMVC::ClassMethods + hmvc OrdersCyclePlugin, orders_context: OrdersCyclePlugin + +end diff --git a/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_cycle_controller.rb b/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_cycle_controller.rb new file mode 100644 index 0000000..a9f94d8 --- /dev/null +++ b/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_cycle_controller.rb @@ -0,0 +1,156 @@ +class OrdersCyclePluginCycleController < OrdersPluginAdminController + + no_design_blocks + + # FIXME: remove me when styles move from consumers_coop plugin + include ConsumersCoopPlugin::ControllerHelper + include OrdersCyclePlugin::TranslationHelper + + protect 'edit_profile', :profile + before_filter :set_admin + + helper OrdersCyclePlugin::TranslationHelper + helper OrdersCyclePlugin::DisplayHelper + + def index + @closed_cycles = search_scope(profile.orders_cycles.closing).all + if request.xhr? + render partial: 'results' + else + @open_cycles = profile.orders_cycles.opened + end + end + + def new + if request.put? + @cycle = OrdersCyclePlugin::Cycle.find params[:id] + + params[:cycle][:status] = 'orders' if @open = params[:open] == '1' + @success = @cycle.update_attributes params[:cycle] + + if @success + session[:notice] = t('controllers.myprofile.cycle_controller.cycle_created') + if params[:sendmail] + OrdersCyclePlugin::Mailer.delay(run_at: @cycle.start).open_cycle( + @cycle.profile, @cycle ,t('controllers.myprofile.cycle_controller.new_open_cycle')+": "+@cycle.name, @cycle.opening_message) + end + else + render action: :edit + end + else + count = OrdersCyclePlugin::Cycle.count conditions: {profile_id: profile} + @cycle = OrdersCyclePlugin::Cycle.create! profile: profile, status: 'new', + name: t('controllers.myprofile.cycle_controller.cycle_n_n') % {n: count+1} + end + end + + def edit + # editing an order + return super if params[:actor_name] + + @cycle = OrdersCyclePlugin::Cycle.find params[:id] + @products = products + + if request.xhr? + if params[:commit] + params[:cycle][:status] = 'orders' if @open = params[:open] == '1' + @success = @cycle.update_attributes params[:cycle] + + if params[:sendmail] + OrdersCyclePlugin::Mailer.delay(run_at: @cycle.start).open_cycle(@cycle.profile, + @cycle, t('controllers.myprofile.cycle_controller.new_open_cycle')+": "+@cycle.name, @cycle.opening_message) + end + end + end + end + + def products_load + @cycle = OrdersCyclePlugin::Cycle.find params[:id] + @products = products + + if @cycle.add_products_job + render nothing: true + else + render partial: 'product_lines' + end + end + + def destroy + @cycle = OrdersCyclePlugin::Cycle.find params[:id] + @cycle.destroy + redirect_to action: :index + end + + def step + @cycle = OrdersCyclePlugin::Cycle.find params[:id] + @cycle.step + @cycle.save! + redirect_to action: :edit, id: @cycle.id + end + + def step_back + @cycle = OrdersCyclePlugin::Cycle.find params[:id] + @cycle.step_back + @cycle.save! + redirect_to action: :edit, id: @cycle.id + end + + def add_missing_products + @cycle = OrdersCyclePlugin::Cycle.find params[:id] + @cycle.add_products + render partial: 'suppliers_plugin/shared/pagereload' + end + + def report_products + return super if params[:ids].present? + @cycle = OrdersCyclePlugin::Cycle.find params[:id] + report_file = report_products_by_supplier @cycle.supplier_products_by_suppliers(@cycle.sales.ordered) + + send_file report_file, type: 'application/xlsx', + disposition: 'attachment', + filename: t('controllers.myprofile.admin.products_report') % { + date: DateTime.now.strftime("%Y-%m-%d"), profile_identifier: profile.identifier, name: @cycle.name_with_code} + end + + def report_orders + return super if params[:ids].present? + @cycle = OrdersCyclePlugin::Cycle.find params[:id] + report_file = report_orders_by_consumer @cycle.sales.ordered + + send_file report_file, type: 'application/xlsx', + disposition: 'attachment', + filename: t('controllers.myprofile.admin.orders_report') % {date: DateTime.now.strftime("%Y-%m-%d"), profile_identifier: profile.identifier, name: @cycle.name_with_code} + end + + def filter + @cycle = profile.orders_cycles.find params[:owner_id] + @scope = @cycle + + params[:code].gsub!(/^#{@cycle.code}\./, '') if params[:code].present? + super + end + + protected + + attr_accessor :cycle + + extend HMVC::ClassMethods + hmvc OrdersCyclePlugin, orders_context: OrdersCyclePlugin + + def search_scope scope + params[:date] ||= {} + scope = scope.by_year params[:date][:year] if params[:date][:year].present? + scope = scope.by_month params[:date][:month] if params[:date][:month].present? + scope = scope.by_status params[:status] if params[:status].present? + scope + end + + def set_admin + @admin = true + end + + def products + @cycle.products.unarchived.paginate per_page: 15, page: params["page"] + end + +end diff --git a/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_delivery_option_controller.rb b/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_delivery_option_controller.rb new file mode 100644 index 0000000..edb8772 --- /dev/null +++ b/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_delivery_option_controller.rb @@ -0,0 +1,17 @@ +class OrdersCyclePluginDeliveryOptionController < DeliveryPlugin::AdminOptionsController + + no_design_blocks + + # FIXME: remove me when styles move from consumers_coop plugin + include ConsumersCoopPlugin::ControllerHelper + include OrdersCyclePlugin::TranslationHelper + + helper OrdersCyclePlugin::TranslationHelper + helper OrdersCyclePlugin::DisplayHelper + + protected + + extend HMVC::ClassMethods + hmvc OrdersCyclePlugin, orders_context: OrdersCyclePlugin + +end diff --git a/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_item_controller.rb b/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_item_controller.rb new file mode 100644 index 0000000..0599094 --- /dev/null +++ b/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_item_controller.rb @@ -0,0 +1,67 @@ +class OrdersCyclePluginItemController < OrdersPluginItemController + + no_design_blocks + + # FIXME: remove me when styles move from consumers_coop plugin + include ConsumersCoopPlugin::ControllerHelper + include OrdersCyclePlugin::TranslationHelper + + helper OrdersCyclePlugin::TranslationHelper + helper OrdersCyclePlugin::DisplayHelper + + def new + @offered_product = Product.find params[:offered_product_id] + @consumer = user + return render_not_found unless @offered_product + raise 'Please login to place an order' if @consumer.blank? + + if params[:order_id] == 'new' + @cycle = @offered_product.cycle + raise 'Cycle closed for orders' unless @cycle.may_order? @consumer + @order = OrdersCyclePlugin::Sale.create! cycle: @cycle, profile: profile, consumer: @consumer + else + @order = OrdersCyclePlugin::Sale.find params[:order_id] + @cycle = @order.cycle + raise 'Order confirmed or cycle is closed for orders' unless @order.open? + raise 'You are not the owner of this order' unless @order.may_edit? @consumer, @admin + end + + @item = OrdersCyclePlugin::Item.where(order_id: @order.id, product_id: @offered_product.id).first + @item ||= OrdersCyclePlugin::Item.new + @item.sale = @order + @item.product = @offered_product + if set_quantity_consumer_ordered(params[:quantity_consumer_ordered] || 1) + @item.update_attributes! quantity_consumer_ordered: @quantity_consumer_ordered + end + end + + def edit + return redirect_to params.merge(action: :admin_edit) if @admin_edit + super + @offered_product = @item.product + @cycle = @order.cycle + end + + def admin_edit + @item = OrdersCyclePlugin::Item.find params[:id] + @order = @item.order + @cycle = @order.cycle + + #update on association for total + @order.items.each{ |i| i.attributes = params[:item] if i.id == @item.id } + + @item.update_attributes = params[:item] + end + + def destroy + super + @offered_product = @product + @cycle = @order.cycle + end + + protected + + extend HMVC::ClassMethods + hmvc OrdersCyclePlugin, orders_context: OrdersCyclePlugin + +end diff --git a/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_message_controller.rb b/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_message_controller.rb new file mode 100644 index 0000000..c80629b --- /dev/null +++ b/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_message_controller.rb @@ -0,0 +1,14 @@ +class OrdersCyclePluginMessageController < OrdersPluginMessageController + + no_design_blocks + + # FIXME: remove me when styles move from consumers_coop plugin + include ConsumersCoopPlugin::ControllerHelper + include OrdersCyclePlugin::TranslationHelper + + helper OrdersCyclePlugin::TranslationHelper + helper OrdersPlugin::FieldHelper + + protected + +end diff --git a/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_product_controller.rb b/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_product_controller.rb new file mode 100644 index 0000000..8769e80 --- /dev/null +++ b/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_product_controller.rb @@ -0,0 +1,46 @@ +class OrdersCyclePluginProductController < SuppliersPlugin::ProductController + + no_design_blocks + + # FIXME: remove me when styles move from consumers_coop plugin + include ConsumersCoopPlugin::ControllerHelper + include OrdersCyclePlugin::TranslationHelper + + helper OrdersCyclePlugin::TranslationHelper + helper OrdersCyclePlugin::DisplayHelper + + def edit + super + @units = environment.units.all + end + + def remove_from_order + @offered_product = OrdersCyclePlugin::OfferedProduct.find params[:id] + @order = OrdersCyclePlugin::Sale.find params[:order_id] + raise 'Order confirmed or cycle is closed for orders' unless @order.open? + @item = @order.items.find_by_product_id @offered_product.id + @item.destroy rescue render nothing: true + end + + def cycle_edit + @product = OrdersCyclePlugin::OfferedProduct.find params[:id] + if request.xhr? + @product.update_attributes! params[:product] + respond_to do |format| + format.js + end + end + end + + def cycle_destroy + @product = OrdersCyclePlugin::OfferedProduct.find params[:id] + @product.destroy + flash[:notice] = t('controllers.myprofile.product_controller.product_removed_from_') + end + + protected + + extend HMVC::ClassMethods + hmvc OrdersCyclePlugin, orders_context: OrdersCyclePlugin + +end diff --git a/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_supplier_controller.rb b/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_supplier_controller.rb new file mode 100644 index 0000000..e048c76 --- /dev/null +++ b/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_supplier_controller.rb @@ -0,0 +1,24 @@ +class OrdersCyclePluginSupplierController < SuppliersPluginMyprofileController + + no_design_blocks + + # FIXME: remove me when styles move from consumers_coop plugin + include ConsumersCoopPlugin::ControllerHelper + include OrdersCyclePlugin::TranslationHelper + + protect 'edit_profile', :profile + + helper OrdersCyclePlugin::TranslationHelper + helper OrdersCyclePlugin::DisplayHelper + + def margin_change + super + profile.orders_cycles_products_default_margins if params[:apply_to_open_cycles] + end + + protected + + extend HMVC::ClassMethods + hmvc OrdersCyclePlugin, orders_context: OrdersCyclePlugin + +end diff --git a/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_volunteers_controller.rb b/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_volunteers_controller.rb new file mode 100644 index 0000000..80c27af --- /dev/null +++ b/plugins/orders_cycle/controllers/myprofile/orders_cycle_plugin_volunteers_controller.rb @@ -0,0 +1,13 @@ +class OrdersCyclePluginVolunteersController < VolunteersPluginMyprofileController + + no_design_blocks + include OrdersCyclePlugin::TranslationHelper + + helper OrdersCyclePlugin::TranslationHelper + + protected + + extend HMVC::ClassMethods + hmvc OrdersCyclePlugin, orders_context: OrdersCyclePlugin + +end diff --git a/plugins/orders_cycle/controllers/profile/orders_cycle_plugin_order_controller.rb b/plugins/orders_cycle/controllers/profile/orders_cycle_plugin_order_controller.rb new file mode 100644 index 0000000..b4974b6 --- /dev/null +++ b/plugins/orders_cycle/controllers/profile/orders_cycle_plugin_order_controller.rb @@ -0,0 +1,183 @@ +class OrdersCyclePluginOrderController < OrdersPluginOrderController + + # FIXME: remove me when styles move from consumers_coop plugin + include ConsumersCoopPlugin::ControllerHelper + include OrdersCyclePlugin::TranslationHelper + + no_design_blocks + before_filter :login_required, except: [:index] + + helper OrdersCyclePlugin::TranslationHelper + helper OrdersCyclePlugin::DisplayHelper + + def index + @current_year = DateTime.now.year.to_s + @year = (params[:year] || @current_year).to_s + + @years_with_cycles = profile.orders_cycles_without_order.years.collect &:year + @years_with_cycles.unshift @current_year unless @years_with_cycles.include? @current_year + + @cycles = profile.orders_cycles.by_year @year + @consumer = user + end + + def new + if user.blank? + session[:notice] = t('orders_plugin.controllers.profile.consumer.please_login_first') + redirect_to action: :index + return + end + + if not profile.members.include? user + render_access_denied + else + @consumer = user + @cycle = profile.orders_cycles.find params[:cycle_id] + @order = OrdersCyclePlugin::Sale.new + @order.profile = profile + @order.consumer = @consumer + @order.cycle = @cycle + @order.save! + redirect_to params.merge(action: :edit, id: @order.id) + end + end + + def repeat + @consumer = user + @order = profile.orders_cycles_sales.where(id: params[:order_id], consumer_id: @consumer.id).first + @cycle = profile.orders_cycles.find params[:cycle_id] + if @order + @order.repeat_cycle = @cycle + @repeat_order = OrdersCyclePlugin::Sale.new profile: profile, consumer: @consumer, cycle: @cycle + @order.items.each do |item| + next unless item.repeat_product and item.repeat_product.available + @repeat_order.items.build sale: @repeat_order, product: item.repeat_product, quantity_consumer_ordered: item.quantity_consumer_ordered + end + @repeat_order.supplier_delivery = @order.supplier_delivery + @repeat_order.save! + redirect_to params.merge(action: :edit, id: @repeat_order.id) + else + @orders = @cycle.consumer_previous_orders(@consumer).last(5).reverse + @orders.each{ |o| o.enable_product_diff } + @orders.each{ |o| o.repeat_cycle = @cycle } + render template: 'orders_plugin/repeat' + end + end + + def edit + return show_more if params[:page].present? + + if request.xhr? and params[:order].present? + status = params[:order][:status] + if status == 'ordered' + if @order.items.size > 0 + @order.to_yaml # most strange workaround to avoid a crash in the next line + @order.update_attributes! params[:order] + session[:notice] = t('orders_plugin.controllers.profile.consumer.order_confirmed') + else + session[:notice] = t('orders_plugin.controllers.profile.consumer.can_not_confirm_your_') + end + end + return + end + + if cycle_id = params[:cycle_id] + @cycle = profile.orders_cycles.where(id: cycle_id).first + return render_not_found unless @cycle + @consumer = user + + # load the first order + unless @order + @consumer_orders = @cycle.sales.for_consumer @consumer + if @consumer_orders.size == 1 + @order = @consumer_orders.first + redirect_to action: :edit, id: @order.id + elsif @consumer_orders.size > 1 + # get the first open + @order = @consumer_orders.find{ |o| o.open? } + redirect_to action: :edit, id: @order.id if @order + end + end + else + return render_not_found unless @order + # an order was loaded on load_order + + @cycle = @order.cycle + + @consumer = @order.consumer + @admin_edit = (user and user.in?(profile.admins) and user != @consumer) + return render_access_denied unless @user_is_admin or @admin_edit or user == @consumer + + @consumer_orders = @cycle.sales.for_consumer @consumer + end + + load_products_for_order + @product_categories = @cycle.product_categories + @consumer_orders = @cycle.sales.for_consumer @consumer + end + + def reopen + @order.update_attributes! status: 'draft' + render 'edit' + end + + def cancel + @order.update_attributes! status: 'cancelled' + session[:notice] = t('orders_plugin.controllers.profile.consumer.order_cancelled') + render 'edit' + end + + def remove + super + redirect_to action: :index, cycle_id: @order.cycle.id + end + + def admin_new + return redirect_to action: :index unless profile.has_admin? user + + @consumer = user + @cycle = profile.orders_cycles.find params[:cycle_id] + @order = OrdersCyclePlugin::Sale.create! cycle: @cycle, consumer: @consumer + redirect_to action: :edit, id: @order.id, profile: profile.identifier + end + + def filter + if id = params[:id] + @order = OrdersCyclePlugin::Sale.find id rescue nil + @cycle = @order.cycle + else + @cycle = profile.orders_cycles.find params[:cycle_id] + @order = OrdersCyclePlugin::Sale.find params[:order_id] rescue nil + end + load_products_for_order + + render partial: 'filter', locals: { + order: @order, cycle: @cycle, + products_for_order: @products, + } + end + + def show_more + filter + end + + def supplier_balloon + @supplier = SuppliersPlugin::Supplier.find params[:id] + end + def product_balloon + @product = OrdersCyclePlugin::OfferedProduct.find params[:id] + end + + protected + + def load_products_for_order + scope = @cycle.products_for_order + page, per_page = params[:page].to_i, 20 + page = 1 if page < 1 + @products = OrdersCyclePlugin::OfferedProduct.search_scope(scope, params).paginate page: page, per_page: per_page + end + + extend HMVC::ClassMethods + hmvc OrdersCyclePlugin, orders_context: OrdersCyclePlugin + +end diff --git a/plugins/orders_cycle/db/migrate/20130909175738_create_orders_cycle_plugin_tables.rb b/plugins/orders_cycle/db/migrate/20130909175738_create_orders_cycle_plugin_tables.rb new file mode 100644 index 0000000..46b7226 --- /dev/null +++ b/plugins/orders_cycle/db/migrate/20130909175738_create_orders_cycle_plugin_tables.rb @@ -0,0 +1,39 @@ +class CreateOrdersCyclePluginTables < ActiveRecord::Migration + + def change + # check if distribution plugin already moved the table + return if ActiveRecord::Base.connection.table_exists? :orders_cycle_plugin_cycles + + create_table :orders_cycle_plugin_cycle_orders do |t| + t.integer :cycle_id + t.integer :order_id + t.datetime :created_at + t.datetime :updated_at + end + + create_table :orders_cycle_plugin_cycle_products do |t| + t.integer :cycle_id + t.integer :product_id + end + + create_table :orders_cycle_plugin_cycles do |t| + t.integer :profile_id + t.integer :code + t.string :name + t.text :description + t.datetime :start + t.datetime :finish + t.string :status + t.text :opening_message + t.datetime :delivery_start + t.datetime :delivery_finish + t.decimal :margin_percentage + t.datetime :created_at + t.datetime :updated_at + end + + add_index :orders_cycle_plugin_cycles, [:profile_id] + add_index :orders_cycle_plugin_cycles, [:status] + end + +end diff --git a/plugins/orders_cycle/db/migrate/20131001162741_orders_cycle_plugin_index_filtered_fields.rb b/plugins/orders_cycle/db/migrate/20131001162741_orders_cycle_plugin_index_filtered_fields.rb new file mode 100644 index 0000000..4350e97 --- /dev/null +++ b/plugins/orders_cycle/db/migrate/20131001162741_orders_cycle_plugin_index_filtered_fields.rb @@ -0,0 +1,19 @@ +class OrdersCyclePluginIndexFilteredFields < ActiveRecord::Migration + + def up + add_index :orders_cycle_plugin_cycle_orders, [:cycle_id] + add_index :orders_cycle_plugin_cycle_orders, [:order_id] + add_index :orders_cycle_plugin_cycle_orders, [:cycle_id, :order_id] + + add_index :orders_cycle_plugin_cycle_products, [:cycle_id], name: :orders_cycle_plugin_index_dqaEe7Hf + add_index :orders_cycle_plugin_cycle_products, [:product_id], name: :orders_cycle_plugin_index_f5DmQ6w5Y + add_index :orders_cycle_plugin_cycle_products, [:cycle_id, :product_id], name: :orders_cycle_plugin_index_PhBVTRFB + + add_index :orders_cycle_plugin_cycles, [:code] + end + + def down + say "this migration can't be reverted" + end + +end diff --git a/plugins/orders_cycle/db/migrate/20140406155248_refactor_orders_cycle_plugin_cycle_order.rb b/plugins/orders_cycle/db/migrate/20140406155248_refactor_orders_cycle_plugin_cycle_order.rb new file mode 100644 index 0000000..e63742f --- /dev/null +++ b/plugins/orders_cycle/db/migrate/20140406155248_refactor_orders_cycle_plugin_cycle_order.rb @@ -0,0 +1,17 @@ +class RefactorOrdersCyclePluginCycleOrder < ActiveRecord::Migration + + def up + rename_column :orders_cycle_plugin_cycle_orders, :order_id, :sale_id + add_column :orders_cycle_plugin_cycle_orders, :purchase_id, :integer + + add_index :orders_cycle_plugin_cycle_orders, :sale_id + add_index :orders_cycle_plugin_cycle_orders, :purchase_id + add_index :orders_cycle_plugin_cycle_orders, [:cycle_id, :sale_id] + add_index :orders_cycle_plugin_cycle_orders, [:cycle_id, :purchase_id], name: :index_orders_cycle_plugin_cycle_orders_cycle_purchase + end + + def down + say "this migration can't be reverted" + end + +end diff --git a/plugins/orders_cycle/db/migrate/20140911210514_add_serialized_data_to_orders_cycle_plugin_cycle.rb b/plugins/orders_cycle/db/migrate/20140911210514_add_serialized_data_to_orders_cycle_plugin_cycle.rb new file mode 100644 index 0000000..7013356 --- /dev/null +++ b/plugins/orders_cycle/db/migrate/20140911210514_add_serialized_data_to_orders_cycle_plugin_cycle.rb @@ -0,0 +1,10 @@ +class AddSerializedDataToOrdersCyclePluginCycle < ActiveRecord::Migration + def self.up + add_column :orders_cycle_plugin_cycles, :data, :text, :default => {}.to_yaml + execute "update orders_cycle_plugin_cycles set data = '#{{}.to_yaml}'" + end + + def self.down + remove_column :orders_cycle_plugin_cycles, :data + end +end diff --git a/plugins/orders_cycle/db/migrate/20150119173244_move_items_to_orders_cycle_plugin_item.rb b/plugins/orders_cycle/db/migrate/20150119173244_move_items_to_orders_cycle_plugin_item.rb new file mode 100644 index 0000000..847285e --- /dev/null +++ b/plugins/orders_cycle/db/migrate/20150119173244_move_items_to_orders_cycle_plugin_item.rb @@ -0,0 +1,12 @@ +class MoveItemsToOrdersCyclePluginItem < ActiveRecord::Migration + def up + OrdersCyclePlugin::Cycle.find_each batch_size: 5 do |cycle| + cycle.items_selled.update_all type: 'OrdersCyclePlugin::Item' + cycle.items_purchased.update_all type: 'OrdersCyclePlugin::Item' + end + end + + def down + say "this migration can't be reverted" + end +end diff --git a/plugins/orders_cycle/db/migrate/20150506220607_fill_default_delivery_method_to_orders_cycle_sales.rb b/plugins/orders_cycle/db/migrate/20150506220607_fill_default_delivery_method_to_orders_cycle_sales.rb new file mode 100644 index 0000000..2e541fa --- /dev/null +++ b/plugins/orders_cycle/db/migrate/20150506220607_fill_default_delivery_method_to_orders_cycle_sales.rb @@ -0,0 +1,12 @@ +class FillDefaultDeliveryMethodToOrdersCycleSales < ActiveRecord::Migration + def up + OrdersCyclePlugin::Sale.find_each batch_size: 50 do |sale| + next unless sale.cycle.present? + sale.update_column :supplier_delivery_id, sale.supplier_delivery_id + end + end + + def down + say "this migration can't be reverted" + end +end diff --git a/plugins/orders_cycle/lib/ext/delivery_plugin/option.rb b/plugins/orders_cycle/lib/ext/delivery_plugin/option.rb new file mode 100644 index 0000000..33787fc --- /dev/null +++ b/plugins/orders_cycle/lib/ext/delivery_plugin/option.rb @@ -0,0 +1,8 @@ +require_dependency 'delivery_plugin/option' + +class DeliveryPlugin::Option + + belongs_to :cycle, class_name: 'OrdersCyclePlugin::Cycle', + foreign_key: :owner_id, conditions: ["delivery_plugin_options.owner_type = 'OrdersCyclePlugin::Cycle'"] + +end diff --git a/plugins/orders_cycle/lib/ext/product.rb b/plugins/orders_cycle/lib/ext/product.rb new file mode 100644 index 0000000..67c1a4e --- /dev/null +++ b/plugins/orders_cycle/lib/ext/product.rb @@ -0,0 +1,12 @@ +require_dependency 'product' + +# based on orders/lib/ext/product.rb +class Product + + has_many :orders_cycles_items, class_name: 'OrdersCyclePlugin::Item', foreign_key: :product_id + + has_many :orders_cycles_orders, through: :orders_cycles_items, source: :order + has_many :orders_cycles_sales, through: :orders_cycles_items, source: :sale + has_many :orders_cycles_purchases, through: :orders_cycles_items, source: :purchase + +end diff --git a/plugins/orders_cycle/lib/ext/profile.rb b/plugins/orders_cycle/lib/ext/profile.rb new file mode 100644 index 0000000..953dcc7 --- /dev/null +++ b/plugins/orders_cycle/lib/ext/profile.rb @@ -0,0 +1,32 @@ +require_dependency 'profile' + +class Profile + + has_many :orders_cycles, class_name: 'OrdersCyclePlugin::Cycle', dependent: :destroy, order: 'created_at DESC', + conditions: ["orders_cycle_plugin_cycles.status <> 'new'"] + has_many :orders_cycles_without_order, class_name: 'OrdersCyclePlugin::Cycle', + conditions: ["orders_cycle_plugin_cycles.status <> 'new'"] + + has_many :orders_cycles_sales, through: :orders_cycles, source: :sales + has_many :orders_cycles_purchases, through: :orders_cycles, source: :purchases + + has_many :offered_products, class_name: 'OrdersCyclePlugin::OfferedProduct', order: 'products.name ASC' + + def orders_cycles_closed_date_range + list = self.orders_cycles.closing.all order: 'start ASC' + return DateTime.now..DateTime.now if list.blank? + list.first.start.to_date..list.last.finish.to_date + end + + def orders_cycles_products_default_margins + self.class.transaction do + self.orders_cycles.opened.each do |cycle| + cycle.products.each do |product| + product.margin_percentage = margin_percentage + product.save! + end + end + end + end + +end diff --git a/plugins/orders_cycle/lib/ext/suppliers_plugin/base_product.rb b/plugins/orders_cycle/lib/ext/suppliers_plugin/base_product.rb new file mode 100644 index 0000000..ccc377e --- /dev/null +++ b/plugins/orders_cycle/lib/ext/suppliers_plugin/base_product.rb @@ -0,0 +1,7 @@ +require_dependency 'product' + +class Product + + scope :in_cycle, -> { where type: 'OrdersCyclePlugin::OfferedProduct' } + +end diff --git a/plugins/orders_cycle/lib/orders_cycle_plugin.rb b/plugins/orders_cycle/lib/orders_cycle_plugin.rb new file mode 100644 index 0000000..64f3977 --- /dev/null +++ b/plugins/orders_cycle/lib/orders_cycle_plugin.rb @@ -0,0 +1,13 @@ +module OrdersCyclePlugin + + extend Noosfero::Plugin::ParentMethods + + def self.plugin_name + I18n.t('orders_cycle_plugin.lib.plugin.name') + end + + def self.plugin_description + I18n.t('orders_cycle_plugin.lib.plugin.description') + end + +end diff --git a/plugins/orders_cycle/lib/orders_cycle_plugin/base.rb b/plugins/orders_cycle/lib/orders_cycle_plugin/base.rb new file mode 100644 index 0000000..8aac84e --- /dev/null +++ b/plugins/orders_cycle/lib/orders_cycle_plugin/base.rb @@ -0,0 +1,14 @@ +require_dependency "#{File.dirname __FILE__}/../ext/delivery_plugin/option" + +class OrdersCyclePlugin::Base < Noosfero::Plugin + + def stylesheet? + true + end + + def js_files + ['orders_cycle'].map{ |j| "javascripts/#{j}" } + end + +end + diff --git a/plugins/orders_cycle/lib/orders_cycle_plugin/cycle_helper.rb b/plugins/orders_cycle/lib/orders_cycle_plugin/cycle_helper.rb new file mode 100644 index 0000000..ec42d45 --- /dev/null +++ b/plugins/orders_cycle/lib/orders_cycle_plugin/cycle_helper.rb @@ -0,0 +1,18 @@ +module OrdersCyclePlugin::CycleHelper + + protected + + def timeline_class cycle, status, selected + klass = "" + if cycle.status == status + klass += " cycle-timeline-current-item" + elsif cycle.passed_by? status + klass += " cycle-timeline-passed-item" + else + klass += " cycle-timeline-next-item" + end + klass += " cycle-timeline-selected-item" if selected == status + klass + end + +end diff --git a/plugins/orders_cycle/lib/orders_cycle_plugin/display_helper.rb b/plugins/orders_cycle/lib/orders_cycle_plugin/display_helper.rb new file mode 100644 index 0000000..8718adb --- /dev/null +++ b/plugins/orders_cycle/lib/orders_cycle_plugin/display_helper.rb @@ -0,0 +1,17 @@ +module OrdersCyclePlugin::DisplayHelper + + protected + + include ::ActionView::Helpers::JavaScriptHelper # we want the original button_to_function! + + include OrdersPlugin::DisplayHelper + + include OrdersCyclePlugin::RepeatHelper + include OrdersCyclePlugin::CycleHelper + + include SuppliersPlugin::DisplayHelper + include SuppliersPlugin::ProductHelper + + include DeliveryPlugin::DisplayHelper + +end diff --git a/plugins/orders_cycle/lib/orders_cycle_plugin/mailer.rb b/plugins/orders_cycle/lib/orders_cycle_plugin/mailer.rb new file mode 100644 index 0000000..719ae5c --- /dev/null +++ b/plugins/orders_cycle/lib/orders_cycle_plugin/mailer.rb @@ -0,0 +1,39 @@ +class OrdersCyclePlugin::Mailer < Noosfero::Plugin::MailerBase + + include OrdersCyclePlugin::TranslationHelper + + helper ApplicationHelper + helper OrdersCyclePlugin::TranslationHelper + + attr_accessor :environment + attr_accessor :profile + + def open_cycle profile, cycle, subject, message + self.environment = profile.environment + @profile = profile + @cycle = cycle + @message = message + + mail bcc: organization_members(@profile), + from: environment.noreply_email, + reply_to: profile_recipients(@profile), + subject: t('lib.mailer.profile_subject') % {profile: profile.name, subject: subject} + end + + protected + + def profile_recipients profile + if profile.person? + profile.contact_email + else + profile.admins.map{ |p| p.contact_email } + end + end + + def organization_members profile + if profile.organization? + profile.members.map{ |p| p.contact_email } + end + end + +end diff --git a/plugins/orders_cycle/lib/orders_cycle_plugin/order_helper.rb b/plugins/orders_cycle/lib/orders_cycle_plugin/order_helper.rb new file mode 100644 index 0000000..2f49468 --- /dev/null +++ b/plugins/orders_cycle/lib/orders_cycle_plugin/order_helper.rb @@ -0,0 +1,7 @@ +module OrdersCyclePlugin::OrderHelper + + protected + + include OrdersCyclePlugin::DisplayHelper + +end diff --git a/plugins/orders_cycle/lib/orders_cycle_plugin/repeat_helper.rb b/plugins/orders_cycle/lib/orders_cycle_plugin/repeat_helper.rb new file mode 100644 index 0000000..e70a698 --- /dev/null +++ b/plugins/orders_cycle/lib/orders_cycle_plugin/repeat_helper.rb @@ -0,0 +1,13 @@ +module OrdersCyclePlugin::RepeatHelper + + def repeat_checkout_order_button order + button :check, t('views.public.repeat.checkout'), {controller: :orders_cycle_plugin_order, action: :repeat, order_id: order.id, cycle_id: @cycle.id}, + class: 'repeat-checkout-order' + end + + def repeat_choose_order_button order + nil + end + +end + diff --git a/plugins/orders_cycle/lib/orders_cycle_plugin/translation_helper.rb b/plugins/orders_cycle/lib/orders_cycle_plugin/translation_helper.rb new file mode 100644 index 0000000..a9a7071 --- /dev/null +++ b/plugins/orders_cycle/lib/orders_cycle_plugin/translation_helper.rb @@ -0,0 +1,12 @@ +module OrdersCyclePlugin::TranslationHelper + + protected + + # included here to be used on controller's t calls + include TermsHelper + + def i18n_scope + ['orders_cycle_plugin', 'orders_plugin', 'suppliers_plugin', 'volunteers_plugin'] + end + +end diff --git a/plugins/orders_cycle/locales/en-US.yml b/plugins/orders_cycle/locales/en-US.yml new file mode 100644 index 0000000..7f3dae8 --- /dev/null +++ b/plugins/orders_cycle/locales/en-US.yml @@ -0,0 +1,259 @@ +en-US: &en-US + + orders_cycle_plugin: + lib: + plugin: + name: "Orders' Cycle" + description: "Create and manage orders' cycle" + ext: + orders_plugin: + order: + cyclecode_ordercode: "%{cyclecode}.%{ordercode}" + mailer: + profile_subject: "[%{profile}] %{subject}" + order_was_changed: "[%{profile}] Your order was modificado" + order_block: + distribution_orders_c: "Distribution orders' cycles for consumers" + offer_cycles_for_you_: "Offer cycles for you consumers to make orders" + orders_cycles: "Orders' cycles" + controllers: + myprofile: + message_controller: + message_sent: "Message sent" + product_controller: + product_removed_from_: "Product removed from cycle" + product_removed_succe: "Product removed successfully" + the_product_was_not_r: "The product was not removed" + cycle_controller: + cycle_created: "Cycle created" + cycle_n_n: "Cycle n.%{n}" + new_open_cycle: "New open cycle: " + + models: + cycle: + code_name: "%{code}. %{name}" + delivery_period_befor: "Delivery' period before orders' period" + invalid_delivery_peri: "Invalid delivery' period" + invalid_orders_period: "Invalid orders' period" + statuses: + edition: Edition + orders: Orders + purchases: Purchases + receipts: Receipts + separation: Separation + delivery: Delivery + closing: Closing + views: + gadgets: + _cycle: + happening: Happening + orders_open_b_cycle: "Orders open: %{cycle}" + place_an_order: "place an order" + see_orders_cycle: "see orders' cycle" + cycles: + all_cycles: "all cycles" + mailer: + open_cycle: + a_new_cycle_is_open_c: "A new cycle is open called " + hello_member_of_name: "Hello consumer of %{name}," + the_administrator_let: "The administrator let a message about this cycle" + the_cycle_description: "The cycle description is.." + profile: + order: + _consumer_orders: + caution: "Caution, you are editing the orders of \"%{consumer}\". It is preferable to make small editions through the cycle's administration, this way the person will be properly warned of the updates. We recommend using this page only if you're doing the order for another person." + show_cancelled_orders: "show cancelled orders" + hide_cancelled_orders: "hide cancelled orders" + administration_of_thi: "Administration of this cycle" + before_the_closing: "(before the closing)" + change_order: "reopen order" + edit_your_orders: "Edit your orders" + login: login + new_order: "New order" + repeat_order: "Repetir order" + orders_from_another_m: "Orders from another consumer" + orders_from_consumer_: "Orders from \"%{consumer}\" on this cycle" + send_message_to_the_m: "send message to the managers" + sign_up: "sign up" + this_cycle_is_already: "This cycle is already closed." + this_cycle_is_not_ope: "This cycle is not open yet." + the_time_for_orders_is: "The orders' period for this cycle is from %{start} to %{finish}" + to_place_an_order_you: "To place an order you need to be logged in and registered %{terms.profile.at_article.singular}. Please %{login} or %{signup}." + you_haven_t_placed_an: "You haven't placed any order on this cycle yet." + you_still_can: "You still can:" + your_order_is_confirm: "Your order is confirmed and registered. Please follow the guidelines of the delivery method below, so that it happens without problems." + your_order_was_cancel: "Your order was cancelled." + your_order_wasn_t_con: "Your order wasn't confirmed and the cycle orders period already ended." + your_orders_on_this_c: "Your orders on this cycle" + associate_to_order: "Associate to make orders" + _filter_products: + active: active + all_the_categories: "all the categories" + all_the_suppliers: "all %{terms.supplier.article.plural}" + and_being: "and being" + anyone: anyone + bigger_than_the_stock: "bigger than the stock" + filter: Filter + in_any_state: "In any state" + inactive: inactive + product_name: "Product Name" + supplier: "%{terms.supplier.singular.capitalize}" + whose_qty_available_i: "whose qty. available is" + _status: + code_status_message: "%{code} %{status_message}" + open_it: "open it" + index: + code: "%{code}." + orders_cycles: "Orders' cycles" + place_an_order: "Place an order" + place_another_order: "Place another order" + there_s_no_open_sessi: "There's no open cycle" + your_orders: "Your orders:" + product: + _order_edit: + add: Add + cancel: cancel + change: Change + city: City + city_state: "%{city}/%{state}" + include: include + more_about_the_produc: "More about the producer \"%{supplier}\"" + no_description: "No description" + opening_new_order_for: "Opening new order for your product inclusion" + opening_order_code_fo: "Opening order %{code} for your product inclusion" + price_percent_price_w: "%{price} + %{percent}% = %{price_with_margin}" + price_s_descriptive: "price's descriptive" + product_image: "Product Image" + _order_search: + order_qty: "Order qty" + category: Category + producer: Producer + price: Price + product: Product + this_search_hasn_t_re: "This search hasn't returned any product" + _cycle_edit: + all_ordered_products: "All ordered products from this product will also be removed; you should first warn consumers that ordered this products" + buy_price: "Buy price" + buy_unit: "Buy unit" + cancel_updates: "cancel updates" + default_margin: "Default margin" + default_sell_price: "Default sell price" + edit_product: "edit product" + margin: Margin + qty_in_stock: "Qty. in stock" + qty_offered: "Qty. offered" + remove_from_cycle: "remove from cycle" + save: Save + sell_price: "Sell price" + sell_unit: "Sell unit" + cycle: + _brief: + confirmed_orders: + zero: "(no confirmed orders)" + one: "(1 confirmed order)" + other: "(%{count} confirmed orders)" + code: "%{code}." + delivery: Delivery + orders: Orders + _closed: + cycle_already_finishe: "Cycle already finished" + _edit_fields: + add_method: "Add method" + add_all_methods: "(add all)" + available_delivery_me: "Available delivery methods" + cancel_changes: "cancel changes" + remove: "remove" + confirm_remove: "Are you sure you want to remove this cycle?" + create_new_cycle: "Create new cycle" + deliveries_interval: "Deliveries Interval" + description: Description + general_settings: "General settings" + name: Name + notify_members_of_ope: "Notify consumer of open orders" + opening_message: "Opening Message" + orders_interval: "Orders Interval" + save: Save + save_and_open_orders: Save and open orders + create_and_open_orders: Create and open orders + this_message_will_be_: "This message will be sent by mail for the consumers %{terms.profile.from_article.singular} " + _edit_popin: + close: Close + cycle_editing: "Cycle editing" + cycle_saved: "Cycle saved." + _edition: + info: 'The edition time is gone and the cycle is already public. Actually, the cycle is in a supply call period.

It is still possible to edit some Cycle parameters through this page, however, beware of the risk. Some operations have different implications depending on the fase you are. When needed, you will be notified by a notification window of the consequences of the changes made.' + add_product: "Add product" + it_was_automatically_: "It was automatically created from the active products. See the list below and check for needed changes." + the_following_list_of: "The following list of products are available in this cycle." + the_products: "The products" + _products_loading: "The products are being loaded into the cycle." + _orders: + header_help: "In this phase the orders %{terms.consumer.from_article.plural} are received, and it is possible supervise them if by chance there is some more severe error." + the_orders_period_is_: "The orders period is still on, take care to edit the orders already open, it may confuse the users" + already_closed: "The orders period was already closed, It's not possible to edit the originals orders. In the redistribution phase it is possible to edit the order, before the delivery it also possible to edit this order." + _purchases: + header_help: "In this phase, using the received orders, the purchases for each supplier are done to supply the demand of all orders." + _receipts: + header_help: "In this phase are registered the receipts of the purchases e are edited any errors that may exist." + _separation: + header_help: "In this phase each order é separated acording to the availability of the products purchased that arrived." + _delivery: + header_help: "In this phase are registered the deliveries %{terms.consumer.to_article.plural}. This is the moment to register the changes in relation to what was separated." + _product_lines: + category: Category + price: Price + product: Product + qty_avail: "Qty. avail." + showing_pcount_produc: "Showing %{pcount} products of %{allpcount}" + supplier: "%{terms.supplier.singular.capitalize}" + _results: + no_cycles_to_show: "No cycles to show" + _timeline: + are_you_sure_you_want_to_reopen: "Are you sure you want to reopen the orders cycle?" + are_you_sure_you_want_to_close: "Are you sure you want to close the orders cycle?" + call: Call + close: Close + close_status: "Close %{status}" + finish_cycle_editing: "Open orders for cycle" + reopen_orders_period: "Reopen orders period" + _title: + new_cycle: "New cycle" + order_cycle: "Order Cycle: " + _view_dates: + delivery: "Delivery: " + happening: Happening + orders: "Orders: " + _view_header: + ? ", " + : ", " + all_orders_cycles: "all orders cycles" + orders_cycle_cycle: "Orders' cycle: %{cycle}" + other_open_cycles_lis: "Other open cycles: %{list}. See also %{all}" + see_also_all: "See also %{all}" + _view_products: + the_products: "The products" + add_products: + add_all_missing_produ: "add all missing products %{terms.profile.to_article.singular}" + add_product_to_cycle_: "Add product to cycle's products" + cancel: cancel + close: close + or: or + search_for_a_product_: "Search for a product in our products" + send: Send + type_in_a_name: "Type in a name" + you_already_have_all_: "You already have all your distributed products added" + index: + and_are_from_the_mont: "and are from the month of" + closed_cycles: "Closed Cycles" + filter: Filter + new_cycle: "New cycle" + no_cycles_to_show: "No cycles to show" + open_cycles: "Open Cycles" + orders_cycles: "Orders' Cycles" + show_cycles_from_year: "Show cycles from year" + +en_US: + <<: *en-US +en: + <<: *en-US + diff --git a/plugins/orders_cycle/locales/pt-BR.yml b/plugins/orders_cycle/locales/pt-BR.yml new file mode 100644 index 0000000..23b77fe --- /dev/null +++ b/plugins/orders_cycle/locales/pt-BR.yml @@ -0,0 +1,257 @@ +pt-BR: &pt-BR + + orders_cycle_plugin: + lib: + plugin: + name: "Ciclo de pedidos" + description: "Criar e administrar ciclos de pedidos" + ext: + orders_plugin: + order: + cyclecode_ordercode: "%{cyclecode}.%{ordercode}" + mailer: + order_was_changed: "[%{profile}] Seu pedido foi modificado" + order_block: + distribution_orders_c: "Ciclos de pedidos para os(as) consumidores(as)" + offer_cycles_for_you_: "Ciclo de oferta para os(as) consumidores(as) fazerem pedidos" + orders_cycles: "Ciclos de pedidos" + controllers: + myprofile: + message_controller: + message_sent: "A mensagem foi enviada" + product_controller: + product_removed_from_: "Produto removido do ciclo" + product_removed_succe: "Produto removido com sucesso" + the_product_was_not_r: "O produto não foi removido" + cycle_controller: + cycle_created: "Ciclo criado" + cycle_n_n: "Ciclo n.%{n}" + new_open_cycle: "Novo ciclo aberto" + + models: + cycle: + code_name: "%{code}. %{name}" + delivery_period_befor: "Período de entrega antes do período de pedidos" + invalid_delivery_peri: "Período de entrega inválido" + invalid_orders_period: "Período de pedidos inválido" + statuses: + edition: Edição + orders: Pedidos + purchases: Compras + receipts: Recebimentos + separation: Separação + delivery: Entrega + closing: Fechamento + views: + gadgets: + _cycle: + happening: Acontecendo + orders_open_b_cycle: "Pedidos abertos: %{cycle}" + place_an_order: "place an order" + see_orders_cycle: "Ver ciclo de pedidos" + cycles: + all_cycles: "todos ciclos" + mailer: + open_cycle: + a_new_cycle_is_open_c: "Um novo ciclo de pedidos está aberto chamado " + hello_member_of_name: "Olá membros da comunidade %{name}!" + the_administrator_let: "A/O administrador(a) deixou uma mensagem sobre este ciclo" + the_cycle_description: "A descrição do ciclo é.." + order: + _consumer_orders: + caution: "Cuidado, voce está editando os pedidos de \"%{consumer}\". É preferível fazer pequenas edições pela administração do ciclo. Desta forma, a pessoa será propriamente avisada das mudanças. Nós recomendamos que esta página seja usada somente se você está fazendo o pedido para outra pessoa." + show_cancelled_orders: "mostrar pedidos cancelados" + hide_cancelled_orders: "esconder pedidos cancelados" + administration_of_thi: "Administração deste ciclo" + before_the_closing: "(antes do fechamento)" + change_order: "Reabrir o pedido" + edit_your_orders: "Edite seus pedidos" + login: login + new_order: "Novo pedido" + repeat_order: "Repetir pedido" + orders_from_another_m: "Orders from another member" + orders_from_consumer_: "Pedidos de \"%{consumer}\" neste ciclo" + send_message_to_the_m: "Enviar mensagem para gestores" + sign_up: registre-se + this_cycle_is_already: "Este ciclo já foi fechado." + this_cycle_is_not_ope: "Este ciclo ainda não está aberto." + the_time_for_orders_is: "O período de pedidos para esse ciclo é de %{start} até %{finish}" + to_place_an_order_you: "Para fazer um pedido você precisa estar logado e registrado %{terms.profile.at_article.singular}. Por favor faça %{login} ou %{signup}." + you_haven_t_placed_an: "Você ainda não fez pedidos neste ciclo." + you_still_can: "Você ainda pode:" + your_order_is_confirm: "Seu pedido está confirmado e registrado. Por favor, siga as diretrizes do método de entrega abaixo para que isso aconteça sem problemas." + your_order_was_cancel: "Seu pedido foi cancelado" + your_order_wasn_t_con: "Seu pedido não foi confirmado e o período de pedidos terminou." + your_orders_on_this_c: "Seus pedidos neste ciclo" + associate_to_order: "Associe-se para realizar pedidos" + _filter_products: + active: Ativo + all_the_categories: "todas as categorias" + all_the_suppliers: "todos %{terms.supplier.article.plural}" + and_being: "e que estejam" + anyone: anyone + bigger_than_the_stock: "maior do que o estoque" + filter: Filtro + in_any_state: "Em qualquer estado" + inactive: Inativo + product_name: "Nome do produto" + supplier: "%{terms.supplier.singular.capitalize}" + whose_qty_available_i: "cuja quantidade disponível é" + _status: + code_status_message: "%{code} %{status_message}" + open_it: abrir + index: + code: "%{code}." + orders_cycles: "Ciclos de pedidos" + place_an_order: "Faça um pedido" + place_another_order: "Faça um outro pedido" + there_s_no_open_sessi: "Nã há ciclos abertos" + your_orders: "Seus pedidos:" + product: + _order_edit: + add: Adicionar + cancel: Cancelar + change: Mudar + city: Cidade + city_state: "%{city}/%{state}" + include: Incluir + more_about_the_produc: "Mais sobre o produtor \"%{supplier}\"" + no_description: "Sem Descrição" + opening_new_order_for: "Abrindo novo pedido para a inclusão do produto." + opening_order_code_fo: "Abrindo pedido %{code} para a inclusão do produto" + price_percent_price_w: "%{price} + %{percent}% = %{price_with_margin}" + price_s_descriptive: "descrictivo do preço" + product_image: "Imagem do produto" + _order_search: + order_qty: "Qtd pedida" + category: Categoria + producer: Produtor + price: Preço + product: Produto + this_search_hasn_t_re: "Esta busca não retornou produtos" + _cycle_edit: + all_ordered_products: "Todos pedidos deste produto serão removidos também; você deveria primeiro avisar os(as) consumidores(as) que pediram esse produto." + buy_price: "Preço de compra" + buy_unit: "Unidade de compra" + cancel_updates: "Cancelar atualizações" + default_margin: "Margem padrão" + default_sell_price: "Preço de venda padrão" + edit_product: "Editar produto" + margin: Margem + qty_in_stock: "Qtd em estoque" + qty_offered: "Qtd oferecida" + remove_from_cycle: "Remover do ciclo" + save: Salvar + sell_price: "Preço de venda:" + sell_unit: "Unidade de venda" + cycle: + _brief: + confirmed_orders: + zero: "(nenhum pedido confirmado)" + one: "(1 pedido confirmado)" + other: "(%{count} pedidos confirmados)" + code: "%{code}." + delivery: Entrega + orders: Pedidos + _closed: + cycle_already_finishe: "Ciclo já fechado" + _edit_fields: + add_method: "Adicionar método" + add_all_methods: "(adicionar todos)" + available_delivery_me: "Métodos de entrega disponíveis" + cancel_changes: "cancelar alterações" + remove: "remover" + confirm_remove: "Tem certeza de que quer remover este ciclo?" + create_new_cycle: "Criar novo ciclo de pedidos" + deliveries_interval: "Intervalo das Entregas" + description: Descrição + general_settings: "Configurações gerais" + name: Nome + notify_members_of_ope: "Avisar os(as) consumidores(as) da abertura de pedidos" + opening_message: "Mensagem de abertura" + orders_interval: "Intervalo dos Pedidos" + save: Salvar + save_and_open_orders: Salvar e abrir pedidos + create_and_open_orders: Criar e abrir pedidos + this_message_will_be_: "Esta mensagem será mandada por email aos(às) consumidores(as) %{terms.profile.from_article.singular}" + _edit_popin: + close: Fechar + cycle_editing: "Edição de ciclo" + cycle_saved: "Ciclo salvo" + _edition: + info: "O tempo de edição deste ciclo acabou e ele ainda está público.

Ainda é possível editar alguns parametros do ciclo através desta página, no entanto, esteja ciente do risco. Algumas operações têm diferentes implicações dependendo da fase em que se está. Quando assim for, você será avisado por uma janelinha de aviso das consequências das mudanças efetuadas." + add_product: "Adicionar produto" + it_was_automatically_: "Ela foi automaticamente gerada a partir dos produtos ativos e suas respectivas margens. Verifique a lista de produtos e edite-a conforme as necessidades e particularidades deste ciclo de pedidos." + the_following_list_of: "A seguinte lista de produtos está disponível neste ciclo." + the_products: "Os produtos" + _products_loading: "Os produtos estão sendo carregados no ciclo." + _orders: + header_help: "Nesta etapa são recebidos os pedidos %{terms.consumer.from_article.plural}, e é possível supervisioná-los se por acaso existe algum erro mais grave." + the_orders_period_is_: "O período de pedidos está ainda aberto, tome cuidado ao editar os pedidos abertos. Isso pode confundir os usuários." + already_closed: "O período de pedidos já foi fechado, Não é possível editar os pedidos originais. Na fase de redistribuição é possível editar o pedido, e também antes da entrega." + _purchases: + header_help: "Nesta etapa, com base nos pedidos recebidos, são feitas as compras de cada um %{terms.supplier.of_article.plural} para suprir a demanda de todos os pedidos." + _receipts: + header_help: "Nesta etapa são registradas os recebimentos das compras e são editadas quaisquer discrepâncias que possam existir." + _separation: + header_help: "Nesta etapa cada pedido é separado de acordo com a disponibilidade dos produtos encomendados que chegaram." + _delivery: + header_help: "Nesta etapa são registradas as entregas %{terms.consumer.to_article.plural}. Este é o momento de registrar as mudanças em relação ao que foi separado." + _product_lines: + category: Categoria + price: Preço + product: Produto + qty_avail: "Qtd. dispon." + showing_pcount_produc: "Mostrando %{pcount} produtos de %{allpcount}" + supplier: "%{terms.supplier.singular.capitalize}" + _results: + no_cycles_to_show: "Sem ciclos a mostrar" + _timeline: + are_you_sure_you_want_to_reopen: "Tem certeza de que deseja reabrir o ciclo de pedidos?" + are_you_sure_you_want_to_close: "Tem certeza de que deseja encerrar a etapa %{status}?" + call: Call + close: Fechar + close_status: "Encerrar %{status}" + finish_cycle_editing: "Abrir pedidos do ciclo" + reopen_orders_period: "Reabrir ciclo de pedidos" + _title: + new_cycle: "Novo ciclo" + order_cycle: "Ciclo de pedidos: " + _view_dates: + delivery: "Entrega: " + happening: Acontecendo + orders: "Pedidos: " + _view_header: + ? ", " + : ", " + all_orders_cycles: "Todos ciclos abertos" + orders_cycle_cycle: "Ciclo de pedidos: %{cycle}" + other_open_cycles_lis: "Outros ciclos abertos: %{list}. Veja também %{all}" + see_also_all: "Veja também %{all}" + _view_products: + the_products: "Os produtos" + add_products: + add_all_missing_produ: "adicionar todos produtos que faltam %{terms.profile.to_article.singular}" + add_product_to_cycle_: "Adicionar produto à lista de produtos distribuídos" + cancel: Cancelar + close: Fechar + or: ou + search_for_a_product_: "Busca por um produto em nossos produtos" + send: Enviar + type_in_a_name: "Escreva um nome" + you_already_have_all_: "Todos seus produtos distribuídos já foram adicionados" + index: + and_are_from_the_mont: "e que sejam do mês de" + closed_cycles: "Ciclos de pedidos fechados" + filter: Filtro + new_cycle: "Novo ciclo" + no_cycles_to_show: "Sem ciclos a mostrar" + open_cycles: "Ciclos de pedidos Abertos" + orders_cycles: "Ciclo de pedidos" + show_cycles_from_year: "Mostre os ciclos do ano" + +pt_BR: + <<: *pt-BR +pt: + <<: *pt-BR + diff --git a/plugins/orders_cycle/models/orders_cycle_plugin/cycle.rb b/plugins/orders_cycle/models/orders_cycle_plugin/cycle.rb new file mode 100644 index 0000000..5d3f178 --- /dev/null +++ b/plugins/orders_cycle/models/orders_cycle_plugin/cycle.rb @@ -0,0 +1,320 @@ +class OrdersCyclePlugin::Cycle < ActiveRecord::Base + + attr_accessible :profile, :status, :name, :description, :opening_message + + attr_accessible :start, :finish, :delivery_start, :delivery_finish + attr_accessible :start_date, :start_time, :finish_date, :finish_time, :delivery_start_date, :delivery_start_time, :delivery_finish_date, :delivery_finish_time, + + Statuses = %w[edition orders purchases receipts separation delivery closing] + DbStatuses = %w[new] + Statuses + UserStatuses = Statuses + + # which status the sales are on each cycle status + SaleStatusMap = { + 'edition' => nil, + 'orders' => :ordered, + 'purchases' => :accepted, + 'receipts' => :accepted, + 'separation' => :accepted, + 'delivery' => :separated, + 'closing' => :delivered, + } + # which status the purchases are on each cycle status + PurchaseStatusMap = { + 'edition' => nil, + 'orders' => nil, + 'purchases' => :draft, + 'receipts' => :ordered, + 'separation' => :received, + 'delivery' => :received, + 'closing' => :received, + } + + belongs_to :profile + + has_many :delivery_options, class_name: 'DeliveryPlugin::Option', dependent: :destroy, + as: :owner, conditions: ["delivery_plugin_options.owner_type = 'OrdersCyclePlugin::Cycle'"] + has_many :delivery_methods, through: :delivery_options, source: :delivery_method + + has_many :cycle_orders, class_name: 'OrdersCyclePlugin::CycleOrder', foreign_key: :cycle_id, dependent: :destroy, order: 'id ASC' + + # cannot use :order because of months/years named_scope + has_many :sales, through: :cycle_orders, source: :sale + has_many :purchases, through: :cycle_orders, source: :purchase + + has_many :cycle_products, foreign_key: :cycle_id, class_name: 'OrdersCyclePlugin::CycleProduct', dependent: :destroy + has_many :products, through: :cycle_products, source: :product, order: 'products.name ASC', + include: [ :from_2x_products, :from_products, {profile: :domains}, ] + + has_many :consumers, through: :sales, source: :consumer, order: 'name ASC', uniq: true + has_many :suppliers, through: :products, source: :suppliers, order: 'suppliers_plugin_suppliers.name ASC', uniq: true + has_many :orders_suppliers, through: :sales, source: :profile, order: 'name ASC' + + has_many :from_products, through: :products, order: 'name ASC', uniq: true + has_many :supplier_products, through: :products, order: 'name ASC', uniq: true + has_many :product_categories, through: :products, order: 'name ASC', uniq: true + + has_many :orders_confirmed, through: :cycle_orders, source: :sale, order: 'id ASC', + conditions: ['orders_plugin_orders.ordered_at IS NOT NULL'] + + has_many :items_selled, through: :sales, source: :items + has_many :items_purchased, through: :purchases, source: :items + # DEPRECATED + has_many :items, through: :orders_confirmed + + has_many :ordered_suppliers, through: :orders_confirmed, source: :suppliers + + has_many :ordered_offered_products, through: :orders_confirmed, source: :offered_products, uniq: true, include: [:suppliers] + has_many :ordered_distributed_products, through: :orders_confirmed, source: :distributed_products, uniq: true, include: [:suppliers] + has_many :ordered_supplier_products, through: :orders_confirmed, source: :supplier_products, uniq: true, include: [:suppliers] + + has_many :volunteers_periods, class_name: 'VolunteersPlugin::Period', as: :owner + has_many :volunteers, through: :volunteers_periods, source: :profile + attr_accessible :volunteers_periods_attributes + accepts_nested_attributes_for :volunteers_periods, allow_destroy: true + + scope :has_volunteers_periods, -> {uniq.joins [:volunteers_periods]} + + # status scopes + scope :on_edition, -> { where status: 'edition' } + scope :on_orders, -> { where status: 'orders' } + scope :on_purchases, -> { where status: 'purchases' } + scope :on_separation, -> { where status: 'separation' } + scope :on_delivery, -> { where status: 'delivery' } + scope :on_closing, -> { where status: 'closing' } + + scope :defuncts, conditions: ["status = 'new' AND created_at < ?", 2.days.ago] + scope :not_new, conditions: ["status <> 'new'"] + scope :on_orders, lambda { + {conditions: ["status = 'orders' AND ( (start <= :now AND finish IS NULL) OR (start <= :now AND finish >= :now) )", + {now: DateTime.now}]} + } + scope :not_on_orders, lambda { + {conditions: ["NOT (status = 'orders' AND ( (start <= :now AND finish IS NULL) OR (start <= :now AND finish >= :now) ) )", + {now: DateTime.now}]} + } + scope :opened, conditions: ["status <> 'new' AND status <> 'closing'"] + scope :closing, conditions: ["status = 'closing'"] + scope :by_status, lambda { |status| { conditions: {status: status} } } + + scope :months, select: 'DISTINCT(EXTRACT(months FROM start)) as month', order: 'month DESC' + scope :years, select: 'DISTINCT(EXTRACT(YEAR FROM start)) as year', order: 'year DESC' + + scope :by_month, lambda { |month| { + conditions: [ 'EXTRACT(month FROM start) <= :month AND EXTRACT(month FROM finish) >= :month', { month: month } ]} + } + scope :by_year, lambda { |year| { + conditions: [ 'EXTRACT(year FROM start) <= :year AND EXTRACT(year FROM finish) >= :year', { year: year } ]} + } + scope :by_range, lambda { |range| { + conditions: [ 'start BETWEEN :start AND :finish OR finish BETWEEN :start AND :finish', + { start: range.first, finish: range.last } + ]} + } + + validates_presence_of :profile + validates_presence_of :name, if: :not_new? + validates_presence_of :start, if: :not_new? + # FIXME: The user frequenqly forget about this, and this will crash the app in some places, so don't enable this + #validates_presence_of :delivery_options, unless: :new_or_edition? + validates_inclusion_of :status, in: DbStatuses, if: :not_new? + validates_numericality_of :margin_percentage, allow_nil: true, if: :not_new? + validate :validate_orders_dates, if: :not_new? + validate :validate_delivery_dates, if: :not_new? + + before_validation :step_new + before_validation :update_orders_status + before_save :add_products_on_edition_state + after_create :delay_purge_profile_defuncts + + extend CodeNumbering::ClassMethods + code_numbering :code, scope: Proc.new { self.profile.orders_cycles } + + extend OrdersPlugin::DateRangeAttr::ClassMethods + date_range_attr :start, :finish + date_range_attr :delivery_start, :delivery_finish + + extend SplitDatetime::SplitMethods + split_datetime :start + split_datetime :finish + split_datetime :delivery_start + split_datetime :delivery_finish + + serialize :data, Hash + + def name_with_code + I18n.t('orders_cycle_plugin.models.cycle.code_name') % { + code: code, name: name + } + end + def total_price_consumer_ordered + self.items.sum :price_consumer_ordered + end + + def status + self['status'] = 'closing' if self['status'] == 'closed' + self['status'] + end + + def step + self.status = DbStatuses[DbStatuses.index(self.status)+1] + end + def step_back + self.status = DbStatuses[DbStatuses.index(self.status)-1] + end + + def passed_by? status + DbStatuses.index(self.status) > DbStatuses.index(status) rescue false + end + + def new? + self.status == 'new' + end + def not_new? + self.status != 'new' + end + def open? + !self.closing? + end + def closing? + self.status == 'closing' + end + def edition? + self.status == 'edition' + end + def new_or_edition? + self.status == 'new' or self.status == 'edition' + end + def after_orders? + now = DateTime.now + status == 'orders' && self.finish < now + end + def before_orders? + now = DateTime.now + status == 'orders' && self.start >= now + end + def orders? + now = DateTime.now + status == 'orders' && ( (self.start <= now && self.finish.nil?) || (self.start <= now && self.finish >= now) ) + end + def delivery? + now = DateTime.now + status == 'delivery' && ( (self.delivery_start <= now && self.delivery_finish.nil?) || (self.delivery_start <= now && self.delivery_finish >= now) ) + end + + def may_order? consumer + self.orders? and consumer.present? and consumer.in? profile.members + end + + def consumer_previous_orders consumer + self.profile.orders_cycles_sales.where(consumer_id: consumer.id). + where('orders_cycle_plugin_cycle_orders.cycle_id <> ?', self.id) + end + + def products_for_order + # FIXME name alias conflict + #self.products.unarchived.with_price.order('products.name ASC') + self.products.unarchived.with_price + end + + def supplier_products_by_suppliers orders = self.sales.ordered + OrdersCyclePlugin::Order.supplier_products_by_suppliers orders + end + + def generate_purchases sales = self.sales.ordered + return self.purchases if self.purchases.present? + + sales.each do |sale| + sale.add_purchases_items_without_delay + end + + self.purchases true + end + def regenerate_purchases sales = self.sales.ordered + self.purchases.destroy_all + self.generate_purchases sales + end + + def add_products + return if self.products.count > 0 + ActiveRecord::Base.transaction do + self.profile.products.supplied.unarchived.available.find_each batch_size: 20 do |product| + self.add_product product + end + end + end + + def add_product product + OrdersCyclePlugin::OfferedProduct.create_from product, self + end + + def add_products_job + @add_products_job ||= Delayed::Job.find_by_id self.data[:add_products_job_id] + end + + protected + + def add_products_on_edition_state + return unless self.status_was == 'new' + job = self.delay.add_products + self.data[:add_products_job_id] = job.id + end + + def step_new + return if new_record? + self.step if self.new? + end + + def update_sales_status from, to + sales = self.sales.where(status: from.to_s) + sales.each do |sale| + sale.update_attributes status: to.to_s + end + end + + def update_purchases_status from, to + purchases = self.purchases.where(status: from.to_s) + purchases.each do |purchase| + purchase.update_attributes status: to.to_s + end + end + + def update_orders_status + # step orders to next_status on status change + return if self.new? or self.status_was == "new" or self.status_was == self.status + + # Don't rewind confirmed sales + unless self.status_was == 'orders' and self.status == 'edition' + sale_status_was = SaleStatusMap[self.status_was] + new_sale_status = SaleStatusMap[self.status] + self.delay.update_sales_status sale_status_was, new_sale_status unless sale_status_was == new_sale_status + end + + # Don't rewind confirmed purchases + unless self.status_was == 'receipts' and self.status == 'purchases' + purchase_status_was = PurchaseStatusMap[self.status_was] + new_purchase_status = PurchaseStatusMap[self.status] + self.delay.update_purchases_status purchase_status_was, new_purchase_status unless purchase_status_was == new_purchase_status + end + end + + def validate_orders_dates + return if self.new? or self.finish.nil? + errors.add :base, (I18n.t('orders_cycle_plugin.models.cycle.invalid_orders_period')) unless self.start < self.finish + end + + def validate_delivery_dates + return if self.new? or delivery_start.nil? or delivery_finish.nil? + errors.add :base, I18n.t('orders_cycle_plugin.models.cycle.invalid_delivery_peri') unless delivery_start < delivery_finish + errors.add :base, I18n.t('orders_cycle_plugin.models.cycle.delivery_period_befor') unless finish <= delivery_start + end + + def purge_profile_defuncts + self.class.where(profile_id: self.profile_id).defuncts.destroy_all + end + + def delay_purge_profile_defuncts + self.delay.purge_profile_defuncts + end + +end diff --git a/plugins/orders_cycle/models/orders_cycle_plugin/cycle_order.rb b/plugins/orders_cycle/models/orders_cycle_plugin/cycle_order.rb new file mode 100644 index 0000000..61ccd6e --- /dev/null +++ b/plugins/orders_cycle/models/orders_cycle_plugin/cycle_order.rb @@ -0,0 +1,16 @@ +class OrdersCyclePlugin::CycleOrder < ActiveRecord::Base + + belongs_to :cycle, class_name: 'OrdersCyclePlugin::Cycle' + belongs_to :sale, class_name: 'OrdersCyclePlugin::Sale', foreign_key: :sale_id, dependent: :destroy + belongs_to :purchase, class_name: 'OrdersCyclePlugin::Purchase', foreign_key: :purchase_id, dependent: :destroy + + validates_presence_of :cycle + validate :sale_or_purchase + + protected + + def sale_or_purchase + errors.add :base, "Specify a sale of purchase" unless self.sale_id or self.purchase_id + end + +end diff --git a/plugins/orders_cycle/models/orders_cycle_plugin/cycle_product.rb b/plugins/orders_cycle/models/orders_cycle_plugin/cycle_product.rb new file mode 100644 index 0000000..ffaf905 --- /dev/null +++ b/plugins/orders_cycle/models/orders_cycle_plugin/cycle_product.rb @@ -0,0 +1,11 @@ +class OrdersCyclePlugin::CycleProduct < ActiveRecord::Base + + self.table_name = :orders_cycle_plugin_cycle_products + + belongs_to :cycle, :class_name => 'OrdersCyclePlugin::Cycle' + belongs_to :product, :class_name => 'OrdersCyclePlugin::OfferedProduct', :dependent => :destroy # a product only belongs to one cycle + + validates_presence_of :cycle + validates_presence_of :product + +end diff --git a/plugins/orders_cycle/models/orders_cycle_plugin/item.rb b/plugins/orders_cycle/models/orders_cycle_plugin/item.rb new file mode 100644 index 0000000..74e4b2a --- /dev/null +++ b/plugins/orders_cycle/models/orders_cycle_plugin/item.rb @@ -0,0 +1,44 @@ +class OrdersCyclePlugin::Item < OrdersPlugin::Item + + has_one :supplier, through: :product + + # see also: repeat_product + attr_accessor :repeat_cycle + + delegate :cycle, to: :order + + # OVERRIDE OrdersPlugin::Item + belongs_to :order, class_name: '::OrdersCyclePlugin::Order', foreign_key: :order_id, touch: true + belongs_to :sale, class_name: '::OrdersCyclePlugin::Sale', foreign_key: :order_id, touch: true + belongs_to :purchase, class_name: '::OrdersCyclePlugin::Purchase', foreign_key: :order_id, touch: true + + # WORKAROUND for direct relationship + belongs_to :offered_product, foreign_key: :product_id, class_name: 'OrdersCyclePlugin::OfferedProduct' + has_many :from_products, through: :offered_product + has_one :from_product, through: :offered_product + has_many :to_products, through: :offered_product + has_one :to_product, through: :offered_product + has_many :sources_supplier_products, through: :offered_product + has_one :sources_supplier_product, through: :offered_product + has_many :supplier_products, through: :offered_product + has_one :supplier_product, through: :offered_product + has_many :suppliers, through: :offered_product + has_one :supplier, through: :offered_product + + # what items were selled from this item + def selled_items + self.order.cycle.selled_items.where(profile_id: self.consumer.id, orders_plugin_item: {product_id: self.product_id}) + end + # what items were purchased from this item + def purchased_items + self.order.cycle.purchases.where(consumer_id: self.profile.id) + end + + # override + def repeat_product + distributed_product = self.from_product + return unless self.repeat_cycle and distributed_product + self.repeat_cycle.products.where(from_products_products: {id: distributed_product.id}).first + end + +end diff --git a/plugins/orders_cycle/models/orders_cycle_plugin/offered_product.rb b/plugins/orders_cycle/models/orders_cycle_plugin/offered_product.rb new file mode 100644 index 0000000..3affbcc --- /dev/null +++ b/plugins/orders_cycle/models/orders_cycle_plugin/offered_product.rb @@ -0,0 +1,108 @@ +class OrdersCyclePlugin::OfferedProduct < SuppliersPlugin::BaseProduct + + # FIXME: WORKAROUND for https://github.com/rails/rails/issues/6663 + # OrdersCyclePlugin::Sale.find(3697).cycle.suppliers returns empty without this + def self.finder_needs_type_condition? + false + end + + has_many :cycle_products, foreign_key: :product_id, class_name: 'OrdersCyclePlugin::CycleProduct' + has_one :cycle_product, foreign_key: :product_id, class_name: 'OrdersCyclePlugin::CycleProduct' + has_many :cycles, through: :cycle_products + has_one :cycle, through: :cycle_product + + # OVERRIDE suppliers/lib/ext/product.rb + # for products in cycle, these are the products of the suppliers: + # p in cycle -> p distributed -> p from supplier + # So, sources_supplier_products is the same as sources_from_2x_products + has_many :sources_supplier_products, through: :from_products, source: :sources_from_products + has_one :sources_supplier_product, through: :from_product, source: :sources_from_product + # necessary only due to the override of sources_supplier_products, as rails somehow caches the old reference + # copied from suppliers/lib/ext/product + has_many :supplier_products, through: :sources_supplier_products, source: :from_product, order: 'id ASC' + has_one :supplier_product, through: :sources_supplier_product, source: :from_product, order: 'id ASC', autosave: true + has_many :suppliers, through: :sources_supplier_products, uniq: true, order: 'id ASC' + has_one :supplier, through: :sources_supplier_product, order: 'id ASC' + + instance_exec &OrdersPlugin::Item::DefineTotals + extend CurrencyHelper::ClassMethods + has_currency :buy_price + + # test this before use! + #validates_presence_of :cycle + + # remove on rails4 + scope :with_price, conditions: 'products.price > 0' + scope :with_product_category_id, lambda { |id| { conditions: {product_category_id: id} } } + def self.search_scope scope, params + scope = scope.from_supplier_id params[:supplier_id] if params[:supplier_id].present? + scope = scope.with_available(if params[:available] == 'true' then true else false end) if params[:available].present? + scope = scope.name_like params[:name] if params[:name].present? + scope = scope.with_product_category_id params[:category_id] if params[:category_id].present? + scope + end + + def self.create_from product, cycle + op = self.new + + product.attributes.except('id').each{ |a,v| op.send "#{a}=", v } + op.freeze_default_attributes product + op.profile = product.profile + op.type = self.name + + op.from_products << product + cycle.products << op if cycle + + op + end + + # always recalculate in case something has changed + def margin_percentage + return self['margin_percentage'] if price.nil? or buy_price.nil? or price.zero? or buy_price.zero? + ((price / buy_price) - 1) * 100 + end + def margin_percentage= value + self['margin_percentage'] = value + self.price = self.price_with_margins buy_price + end + + def sell_unit + self.unit || self.class.default_unit + end + + # reimplement to don't destroy this, keeping history in cycles + # offered products copy attributes + def dependent? + false + end + + # cycle products freezes properties and don't use the original + DEFAULT_ATTRIBUTES.each do |a| + define_method "default_#{a}" do + nil + end + end + + FROOZEN_DEFAULT_ATTRIBUTES = DEFAULT_ATTRIBUTES + def freeze_default_attributes from_product + FROOZEN_DEFAULT_ATTRIBUTES.each do |attr| + self[attr] = from_product.send(attr) if from_product[attr] or from_product.respond_to? attr + end + end + + def solr_index? + false + end + + protected + + after_update :sync_ordered + def sync_ordered + return unless self.price_changed? + self.items.each do |item| + item.calculate_prices self.price + item.save! + end + end + +end diff --git a/plugins/orders_cycle/models/orders_cycle_plugin/order.rb b/plugins/orders_cycle/models/orders_cycle_plugin/order.rb new file mode 100644 index 0000000..66fd9c8 --- /dev/null +++ b/plugins/orders_cycle/models/orders_cycle_plugin/order.rb @@ -0,0 +1,6 @@ +class OrdersCyclePlugin::Order < OrdersPlugin::Order + + # nothing here, see OrderBase + include OrdersCyclePlugin::OrderBase + +end diff --git a/plugins/orders_cycle/models/orders_cycle_plugin/order_base.rb b/plugins/orders_cycle/models/orders_cycle_plugin/order_base.rb new file mode 100644 index 0000000..5446b18 --- /dev/null +++ b/plugins/orders_cycle/models/orders_cycle_plugin/order_base.rb @@ -0,0 +1,64 @@ +# This module is needed to pretend a multiple inheritance for Sale and Purchase +module OrdersCyclePlugin::OrderBase + + extend ActiveSupport::Concern + included do + + attr_accessible :cycle + + has_many :cycle_sales, class_name: 'OrdersCyclePlugin::CycleOrder', foreign_key: :sale_id, dependent: :destroy + has_one :cycle_sale, class_name: 'OrdersCyclePlugin::CycleOrder', foreign_key: :sale_id + has_many :cycle_purchases, class_name: 'OrdersCyclePlugin::CycleOrder', foreign_key: :purchase_id, dependent: :destroy + has_one :cycle_purchase, class_name: 'OrdersCyclePlugin::CycleOrder', foreign_key: :purchase_id + def all_cycles + self.cycle_sales.includes(:cycle).map(&:cycle) + self.cycle_purchases.includes(:cycle).map(&:cycle) + end + + # TODO: test if the has_one defined on Sale/Purchase works and these are not needed + def cycle + self.cycles.first + end + def cycle= cycle + self.cycles = [cycle] + end + + scope :for_cycle, -> (cycle) { + where('orders_cycle_plugin_cycles.id = ?', cycle.id). + joins(:cycles) + } + + has_many :items, class_name: 'OrdersCyclePlugin::Item', foreign_key: :order_id, dependent: :destroy, order: 'name ASC' + + has_many :offered_products, through: :items, source: :offered_product, uniq: true + has_many :distributed_products, through: :offered_products, source: :from_products, uniq: true + has_many :supplier_products, through: :distributed_products, source: :from_products, uniq: true + + has_many :suppliers, through: :supplier_products, uniq: true + + extend CodeNumbering::ClassMethods + code_numbering :code, scope: (proc do + if self.cycle then self.cycle.send(self.orders_name) else self.profile.orders end + end) + + def code + I18n.t('orders_cycle_plugin.lib.ext.orders_plugin.order.cyclecode_ordercode') % { + cyclecode: self.cycle.code, ordercode: self['code'] + } + end + + def delivery_methods + self.cycle.delivery_methods + end + + def repeat_cycle= cycle + self.items.each{ |i| i.repeat_cycle = cycle } + end + + def available_products + self.cycle.products + end + + protected + end + +end diff --git a/plugins/orders_cycle/models/orders_cycle_plugin/purchase.rb b/plugins/orders_cycle/models/orders_cycle_plugin/purchase.rb new file mode 100644 index 0000000..c5137be --- /dev/null +++ b/plugins/orders_cycle/models/orders_cycle_plugin/purchase.rb @@ -0,0 +1,8 @@ +class OrdersCyclePlugin::Purchase < OrdersPlugin::Purchase + + include OrdersCyclePlugin::OrderBase + + has_many :cycles, through: :cycle_purchases, source: :cycle + has_one :cycle, through: :cycle_purchase, source: :cycle + +end diff --git a/plugins/orders_cycle/models/orders_cycle_plugin/sale.rb b/plugins/orders_cycle/models/orders_cycle_plugin/sale.rb new file mode 100644 index 0000000..1b1ccc2 --- /dev/null +++ b/plugins/orders_cycle/models/orders_cycle_plugin/sale.rb @@ -0,0 +1,80 @@ +class OrdersCyclePlugin::Sale < OrdersPlugin::Sale + + include OrdersCyclePlugin::OrderBase + + has_many :cycles, through: :cycle_sales, source: :cycle + has_one :cycle, through: :cycle_sale, source: :cycle + + after_save :change_purchases, if: :cycle + before_destroy :remove_purchases_items, if: :cycle + + def current_status + return 'forgotten' if self.forgotten? + super + end + + def delivery? + self.cycle.delivery? + end + def forgotten? + self.draft? and !self.cycle.orders? + end + + def open? + super and self.cycle.orders? + end + + def supplier_delivery + super || (self.cycle.delivery_methods.first rescue nil) + end + + def change_purchases + return unless self.status_was.present? + if self.ordered_at_was.nil? and self.ordered_at.present? + self.add_purchases_items + elsif self.ordered_at_was.present? and self.ordered_at.nil? + self.remove_purchases_items + end + end + + def add_purchases_items + ActiveRecord::Base.transaction do + self.items.each do |item| + next unless supplier_product = item.product.supplier_product + next unless supplier = supplier_product.profile + + purchase = self.cycle.purchases.for_profile(supplier).first + purchase ||= OrdersCyclePlugin::Purchase.create! cycle: self.cycle, consumer: self.profile, profile: supplier + + purchased_item = purchase.items.for_product(supplier_product).first + purchased_item ||= purchase.items.build purchase: purchase, product: supplier_product + purchased_item.quantity_consumer_ordered ||= 0 + purchased_item.quantity_consumer_ordered += item.status_quantity + purchased_item.price_consumer_ordered ||= 0 + purchased_item.price_consumer_ordered += item.status_quantity * supplier_product.price + purchased_item.save! + end + end + end + + def remove_purchases_items + ActiveRecord::Base.transaction do + self.items.each do |item| + next unless supplier_product = item.product.supplier_product + next unless purchase = supplier_product.orders_cycles_purchases.for_cycle(self.cycle).first + + purchased_item = purchase.items.for_product(supplier_product).first + purchased_item.quantity_consumer_ordered -= item.status_quantity + purchased_item.price_consumer_ordered -= item.status_quantity * supplier_product.price + purchased_item.save! + + purchased_item.destroy if purchased_item.quantity_consumer_ordered.zero? + purchase.destroy if purchase.items(true).blank? + end + end + end + + handle_asynchronously :add_purchases_items + handle_asynchronously :remove_purchases_items + +end diff --git a/plugins/orders_cycle/plugin.yml b/plugins/orders_cycle/plugin.yml new file mode 100644 index 0000000..9f8461d --- /dev/null +++ b/plugins/orders_cycle/plugin.yml @@ -0,0 +1,5 @@ +name: orders_cycle +dependencies: + - orders + - suppliers + - volunteers diff --git a/plugins/orders_cycle/public/images/order-statuses.png b/plugins/orders_cycle/public/images/order-statuses.png new file mode 100644 index 0000000..f3f8f9a Binary files /dev/null and b/plugins/orders_cycle/public/images/order-statuses.png differ diff --git a/plugins/orders_cycle/public/images/progressbar.png b/plugins/orders_cycle/public/images/progressbar.png new file mode 100644 index 0000000..a61b3df Binary files /dev/null and b/plugins/orders_cycle/public/images/progressbar.png differ diff --git a/plugins/orders_cycle/public/javascripts/orders_cycle.js b/plugins/orders_cycle/public/javascripts/orders_cycle.js new file mode 100644 index 0000000..61bef88 --- /dev/null +++ b/plugins/orders_cycle/public/javascripts/orders_cycle.js @@ -0,0 +1,211 @@ + +orders_cycle = { + + cycle: { + + edit: { + openingMessage: { + onKeyup: function(textArea) { + textArea = $(textArea) + var checked = textArea.val() ? true : false; + var checkBox = textArea.parents('#cycle-new-mail').find('input[type=checkbox]') + checkBox.prop('checked', checked) + }, + }, + }, + + products: { + load_url: null, + + load: function () { + $.get(orders_cycle.cycle.products.load_url, function(data) { + if (data.length > 10) + $('#cycle-products .table').html(data) + else + setTimeout(orders_cycle.cycle.products.load, 5*1000); + }); + + }, + }, + }, + + /* ----- cycle ----- */ + + in_cycle_order_toggle: function (context) { + container = $(context).hasClass('cycle-orders') ? $(context) : $(context).parents('.cycle-orders'); + container.toggleClass('show'); + container.find('.order-content').toggle(); + sortable_table.edit_arrow_toggle(container); + }, + + /* ----- order ----- */ + + order: { + + load: function() { + $('html').click(function(e) { + $('.popover').remove() + }) + }, + + product: { + include_message: '', + order_id: 0, + redirect_after_include: '', + add_url: '', + remove_url: '', + balloon_url: '', + + load: function (id, state) { + var product = $('#cycle-product-'+id); + product.toggleClass('in-order', state); + product.find('input').get(0).checked = state; + toggle_edit.value_row.reload(); + return product; + }, + + showMore: function (url) { + $.get(url, function (data) { + var newProducts = $(data).filter('#cycle-products').find('.table-content').children() + $('.pagination').replaceWith(newProducts) + pagination.loading = false + }) + }, + + click: function (event, id) { + // was this a child click? + if (event != null && event.target != this && event.target.onclick) + return; + + var product = $('#cycle-product-'+id); + if (! product.hasClass('editable')) + return; + + var state = !product.hasClass('in-order'); + if (state == true) + this.add(id); + else + this.remove(id); + product.find('input').get(0).checked = state; + }, + + setEditable: function (editable) { + $('.order-cycle-product').toggleClass('editable', editable) + if (editable) + $('.order-cycle-product #product_ids_').removeAttr('disabled') + else + $('.order-cycle-product #product_ids_').attr('disabled', 'disabled') + }, + + add: function (id) { + var product = this.load(id, true); + + if (this.include_message) + alert(this.include_message); + + loading_overlay.show(product); + $.post(this.add_url, {order_id: this.order_id, redirect: this.redirect_after_include, offered_product_id: id}, function () { + loading_overlay.hide(product); + }, 'script'); + }, + remove: function (id) { + var product = this.load(id, false); + + loading_overlay.show(product); + $.post(this.remove_url, {id: id, order_id: this.order_id}, function () { + loading_overlay.hide(product); + }, 'script'); + }, + + supplier: { + balloon_url: '', + + balloon: function (id) { + var product = $('#cycle-product-'+id) + var target = product.find('.supplier') + var supplier_id = product.attr('supplier-id') + $.get(this.balloon_url+'/'+supplier_id, function(data) { + var html = $(data) + var title = orders_cycle.order.product.balloon_title(html) + // use container to avoid conflict with row click + var options = {html: true, content: html, container: 'body', title: title} + target.popover(options).popover('show') + }) + }, + }, + + balloon: function (id) { + var product = $('#cycle-product-'+id); + var target = product.find('.product'); + $.get(this.balloon_url+'/'+id, function(data) { + var html = $(data) + var title = orders_cycle.order.product.balloon_title(html) + // use container to avoid conflict with row click + var options = {html: true, content: html, container: 'body', title: title} + target.popover(options).popover('show') + }) + }, + + balloon_title: function(content) { + var titleElement = $(content).find('.popover-title') + var title = titleElement.html() + titleElement.hide() + return title + }, + }, // product + }, // order + + /* ----- cycle editions ----- */ + + offered_product: { + + pmsync: function (context, to_price) { + p = $(context).parents('.cycle-product .box-edit'); + margin = p.find('#product_margin_percentage'); + price = p.find('#product_price'); + buy_price = p.find('#product_buy_price'); + original_price = p.find('#product_original_price'); + base_price = unlocalize_currency(buy_price.val()) ? buy_price : original_price; + + if (to_price) + suppliers.price.calculate(price, margin, base_price); + else + suppliers.margin.calculate(margin, price, base_price); + }, + + edit: function () { + toggle_edit.editing().find('.box-edit').toggle(toggle_edit.isEditing()); + }, + }, + + /* ----- toggle edit ----- */ + + cycle_mail_message_toggle: function () { + if ($('#cycle-new-mail-send').prop('checked')) { + $('#cycle-new-mail').removeClass('disabled'); + $('#cycle-new-mail textarea').removeAttr('disabled'); + } else { + $('#cycle-new-mail').addClass('disabled'); + $('#cycle-new-mail textarea').attr('disabled', true); + } + }, + + ajaxifyPagination: function(selector) { + $(selector).find(".pagination a").click(function() { + loading_overlay.show(selector); + $.ajax({ + type: "GET", + url: $(this).attr("href"), + dataType: "script" + }); + return false; + }); + }, + + toggleCancelledOrders: function () { + $('.consumers-coop #show-cancelled-orders a').toggle(); + $('.consumers-coop #hide-cancelled-orders a').toggle(); + $('.consumers-coop .consumer-order.cancelled').not('.comsumer-order.active-order').toggle(); + }, + +}; diff --git a/plugins/orders_cycle/public/style.scss b/plugins/orders_cycle/public/style.scss new file mode 100644 index 0000000..803b97d --- /dev/null +++ b/plugins/orders_cycle/public/style.scss @@ -0,0 +1,2 @@ +@import 'stylesheets/orders_cycle' + diff --git a/plugins/orders_cycle/public/stylesheets/_base.scss b/plugins/orders_cycle/public/stylesheets/_base.scss new file mode 120000 index 0000000..a956ddb --- /dev/null +++ b/plugins/orders_cycle/public/stylesheets/_base.scss @@ -0,0 +1 @@ +../../../orders/public/stylesheets/_base.scss \ No newline at end of file diff --git a/plugins/orders_cycle/public/stylesheets/_field.scss b/plugins/orders_cycle/public/stylesheets/_field.scss new file mode 120000 index 0000000..46300a3 --- /dev/null +++ b/plugins/orders_cycle/public/stylesheets/_field.scss @@ -0,0 +1 @@ +../../../suppliers/public/stylesheets/_field.scss \ No newline at end of file diff --git a/plugins/orders_cycle/public/stylesheets/_orders-variables.scss b/plugins/orders_cycle/public/stylesheets/_orders-variables.scss new file mode 120000 index 0000000..e445e66 --- /dev/null +++ b/plugins/orders_cycle/public/stylesheets/_orders-variables.scss @@ -0,0 +1 @@ +../../../orders/public/stylesheets/_orders-variables.scss \ No newline at end of file diff --git a/plugins/orders_cycle/public/stylesheets/_sortable-table.scss b/plugins/orders_cycle/public/stylesheets/_sortable-table.scss new file mode 120000 index 0000000..1b3c561 --- /dev/null +++ b/plugins/orders_cycle/public/stylesheets/_sortable-table.scss @@ -0,0 +1 @@ +../../../orders/public/stylesheets/_sortable-table.scss \ No newline at end of file diff --git a/plugins/orders_cycle/public/stylesheets/orders_cycle.scss b/plugins/orders_cycle/public/stylesheets/orders_cycle.scss new file mode 100644 index 0000000..9515bc3 --- /dev/null +++ b/plugins/orders_cycle/public/stylesheets/orders_cycle.scss @@ -0,0 +1,620 @@ +@import 'base'; +@import 'orders-variables'; + +@import 'sortable-table'; + +.balloon-content.orders-cycle { + @extend .container-clean; + // the size is defined on .balloon .content (see above) + + .left-column { + width: $module01; + margin-right: $half-margin; + float: left; + } + .right-column { + width: $module02; + float: right; + } + +} + +.cycle-view { + + #cycle-others, + #cycle-header { + padding-bottom: $padding; + margin-bottom: $half-margin; + margin-left: -2*$border; + padding-right: 2*$border; + } + #cycle-header { + border-bottom: 2*$border solid #ddd; + margin-bottom: 0; + + .subtitle, + #cycle-dates { + width: 520px; + + th { + border-bottom: 0; + } + } + + .subtitle { + display: block; + margin-top: 20px; + font-style: italic; + } + } + + #cycle-products-for-order { + width: $module06 + $intercolumn; + float: left; + padding-left: 0; + + .title { + } + + .balloon { + + .content { + width: $module03 + $intercolumn; //$intercolumn for scrollbars + max-height: 10*$height; + } + } + + #filter { + margin-left: -$sortable-table-negative-area; + + .title, .filter-box, .submit { + padding-left: $sortable-table-negative-area + $padding !important; + } + + .filter-box { + padding-left: $padding; + + &.fields { + + .supplier { + padding-left: 0; + } + .name { + padding-right: 0; + } + } + } + } + + #cycle-products { + + .table-header, .table-content { + margin-left: -$sortable-table-negative-area; + } + .table-header { + padding-left: $sortable-table-negative-area; + } + + + .table-content { + //override .product-pic from application.css + .product-pic { + max-width: 70px; + max-height: 70px; + } + } + + .box-field { + &, * { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + &.category { + width: $module02 - 3*$base; + } + &.supplier { + width: $module02; + } + &.product { + width: $module02; + } + &.price-with-unit { + width: $module01; + } + } + + .value-row { + border: none; + + #product_ids_[disabled] { + display: none; + } + + &.in-order { + + .box-view { + background-color: $sortable-table-bg-yellow; + } + + } + } + } + } + + #order-column { + float: left; + width: $order-items-width; + margin-left: $margin; + padding: 0; + + > h3 { + margin-bottom: 2*$margin; + } + > h3 a { + text-transform: none; + float: right; + margin-left: $half-margin; + &:before { + padding-right: $half-padding; + } + } + + .consumer-order, + #order-page .in-order { + background-color: $sortable-table-bg-yellow; + } + .consumer-order { + margin-bottom: 10px; + width: $order-items-width; + + &.unactive { + .order-message-title { + float: left; + } + .actions { + float: right; + padding: $half-padding $padding; + } + &.cancelled { + display: none; + } + } + } + + #order-admin-warning { + background-color: #FECBCA; + } + + #order-admin-warning { + margin-bottom: $margin; + } + + .order-message { + padding: $padding; + } + + .order-message-text { + font-size: 10px; + font-style: italic; + } + .order-message-actions { + margin-top: $half-margin; + font-size: 10px; + } + + .order-message-actions a { + font-weight: bold; + } + + #close-order, + #desist-order { + margin-top: 30px; + margin-bottom: 8px; + } + + .quantity-entry input { + width: 76px; + } + } +} + +#cycle-admin-page { + @import 'field'; + + a.action-button { + margin-top: 5px; + } + #cycle-header-warning { + background-color: #FECBCA; + padding: 8px; + + #cycle-header-warning-wrap { + width: 70%; + + } + } + + #cycle-product-line .field { + width: 120px; + } + + #cycle-products { + margin-top: $margin; + + .header { + div { + width: 400px; + } + a { + line-height: 30px; + } + } + + .table { + .small-loading { + background-position: 0; + padding-left: 20px; + } + } + + .cycle-products-table { + margin-top: 30px; + } + .value-row.edit .price, + .value-row.edit .quantity-available, + .value-row.edit .actions { + visibility: hidden; + } + + .box-edit .field { + float: left; + clear: none; + margin-right: 40px; + width: 120px; + } + + .box-view .box-field { + } + .box-field.category { + width: 90px; + } + .box-field.supplier { + width: 120px; + } + .box-field.name { + width: 190px; + } + .box-field.price { + width: 160px; + } + .box-field.quantity-available { + width: 80px; + } + .quantity-available input { + width: 60px; + } + .box-edit input[type=text] { + width: 120px; + } + } +} + +#cycle-closed-listing { + padding-left: 0; +} + +#cycle-new-mail-send { + margin-bottom: 0; +} + +#cycle-new-mail label { + font-weight: bold; +} + +#cycle-new-mail .mail-message { + margin-top: 10px; +} + + +.cycle { + + .h2 { + margin-bottom: 0; + } + + .cycle-timeline { + border: 2*$border solid #CBCBCB; + margin-bottom: $margin; + @extend .container-clean; + + //wireframe size + width: $wireframe + 2*$wireframe-padding; + margin-left: -$wireframe-padding; + padding: 0 $wireframe-padding; + + &, + .cycle-timeline-passed-item, + .cycle-timeline-current-item { + background: url(/plugins/orders_cycle/images/progressbar.png) left top no-repeat; + } + & { + background-size: $wireframe-padding $wireframe-padding*2; //height is arbitrary big to fit + } + + .cycle-timeline-item { + float: left; + padding: $padding; + font-weight: bold; + + &:first-child { + padding-left: 0; + } + } + + .cycle-timeline-current-item { + background-position: 105% 0; + } + .cycle-timeline-next-item { + color: #BABABA; + } + .cycle-timeline-selected-item { + color: black; + text-decoration: none; + font-weight: bold; + } + + } + + .dates-brief { + margin-bottom: 2*$margin; + } + + .actions-bar { + clear: both; + } +} + + +.cycle-product-line *[disabled] { + background-color: transparent; +} + +#cycle-dates th { + width: 80px; + vertical-align: top; + border-bottom: 0; + + .cycle-list-item { + border-top: 1px dotted #ccc; + padding: 8px; + } + .cycle-list-item:last-child { + border-bottom: 1px dotted #ccc; + } + .cycle-name-and-code { + font-weight: bold; + } +} + +#orders-view { + + h2 { + margin-top: 20px; + } + + .consumer-orders { + + #cycle-others { + border-bottom: 2px dotted #aaa; + } + #consumer-new-order { + font-weight: bold; + } + + #order-place-new { + font-size: 10px; + border: $border solid #FFF0AD; + padding: $padding; + margin-bottom: 10px; + + &.admin { + font-size: 12px; + border: none; + float: right; + padding: $padding 0; + text-transform: lower; + } + } + + #years-filter { + margin-bottom: $margin; + + a { + font-weight: bold; + + &.current { + text-decoration: none; + color: black; + } + } + } + + .order-message-title { + float: left; + margin-right: 5px; + } + } + + .cycle-with-consumer-orders { + padding-bottom: 10px; + border-top: 2px dotted #aaa; + + &:last-child { + border-bottom: 2px dotted #aaa; + } + } + + .consumer-orders { + + > a { + float: left; + } + a { + text-decoration: none; + } + a div { + font-size: 10px; + color: black; + } + } +} + +.order-data .order-message-title.cancelled { + display: block; +} +.order-message-title { + padding: $half-padding $padding; + font-size: 13px; + font-weight: bold; + @extend .container-clean; + + div { + float: left; + background-size: 11px 30px; + } + + a.open { + display: none; + float: right; + font-weight: normal; + } + + &.open { + background-color: #FFF2A3; + div { + padding-left: 20px; + background: url(/plugins/orders_cycle/images/order-statuses.png) 0px -25px no-repeat; + padding-left: 24px; + margin-bottom: 0; + } + } + + &.ordered { + background-color: #E1F5C4; + div { + background: url(/plugins/orders_cycle/images/order-statuses.png) left -2px no-repeat; + padding-left: 24px; + } + } + + &.cancelled { + } + + &.cancelled, + &.forgotten { + background-color: #FECBCA; + } +} + +#cycles-gadget { + margin-bottom: 30px; + + #all-cycles { + float: right; + } + h2 { + color: #036475; + } + + td { + padding-right: 8px; + vertical-align: top; + } + td.first-column { + width: 300px; + } + td.second-column { + width: 400px; + } + td.third-column { + vertical-align: bottom; + } + td.third-column form { + float: right; + clear: both; + } + .cycle .happening { + background-color: #ffe546; + line-height: 20px; + text-transform: uppercase; + padding: 0 8px; + } + .cycle .info { + background: #DDF2C5; + padding: 8px; + padding-right: 0; + } +} + +#cycle_opening_message { + margin-bottom: $intercolumn; + width: 420px; +} + +#cycle-fields input { + margin-right: 7px; +} +.cycle-field-name input { + width: 195px; +} +.cycle-fields-block { + margin: 10px 0; +} +.cycle-field-description textarea { + width: 412px; + height: 100px; +} + +.action-orders_cycle_plugin_cycle-new .cycle-edit-link, +.action-orders_cycle_plugin_cycle-edit .cycle-edit-link { + height: 44px; + display: block; +} + +#cycle-delivery-options { + clear: both; +} +#cycle-delivery label { + font-weight: bold; +} + +.cycle-new { + margin-bottom: 29px; + display: block; +} + +.cycle-delivery-option { + float: left; + display: inline-block; + padding: 2px 5px; + margin-right: 10px; + background-color: #E5E0EC; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -o-border-radius: 4px; +} + +.action-orders_cycle_plugin_cycle-new #colorbox, .action-orders_cycle_plugin_cycle-edit #colorbox { + top: 80%; +} + +#cycle-orders { + margin-top: $margin; + +} + +.distribution-plugin-popin { + padding: 10px; +} diff --git a/plugins/orders_cycle/test/factories.rb b/plugins/orders_cycle/test/factories.rb new file mode 100644 index 0000000..021fdaf --- /dev/null +++ b/plugins/orders_cycle/test/factories.rb @@ -0,0 +1,57 @@ +module OrdersCyclePlugin::Factory + + def defaults_for_suppliers_plugin_supplier + {:profile => build(Profile), + :consumer => build(Profile)} + end + + def defaults_for_suppliers_plugin_distributed_product attrs = {} + profile = attrs[:profile] || build(Profile) + {:profile => profile, :name => "product-#{factory_num_seq}", :price => 2.0, + :product => build(Product, :enterprise => profile.profile, :price => 2.0), + :supplier => build(SuppliersPlugin::Supplier, :profile => profile, :consumer => profile)} + end + + def defaults_for_orders_cycle_plugin_offered_product attrs = {} + hash = defaults_for_orders_cycle_plugin_product(attrs) + profile = hash[:profile] + hash.merge({ + :from_products => [build(SuppliersPlugin::DistributedProduct, :profile => profile)]}) + end + + def defaults_for_delivery_plugin_method + {:profile => build(OrdersCyclePlugin::Profile), + :name => "My delivery #{factory_num_seq.to_s}", + :delivery_type => 'deliver'} + end + + def defaults_for_delivery_plugin_option + {:cycle => build(OrdersCyclePlugin::Cycle), + :delivery_method => build(DeliveryPlugin::Method)} + end + + def defaults_for_orders_plugin_order attrs = {} + profile = attrs[:profile] || build(OrdersCyclePlugin::Profile) + {:status => 'ordered', + :cycle => build(OrdersCyclePlugin::Cycle, :profile => profile), + :consumer => build(OrdersCyclePlugin::Profile), + :supplier_delivery => build(DeliveryPlugin::Method, :profile => profile), + :consumer_delivery => build(DeliveryPlugin::Method, :profile => profile)} + end + + def defaults_for_orders_plugin_items + {:order => build(OrdersPlugin::Order), + :product => build(OrdersCyclePlugin::OfferedProduct), + :quantity_shipped => 1.0, :quantity_ordered => 2.0, :quantity_accepted => 3.0, + :price_shipped => 10.0, :price_ordered => 20.0, :price_accepted => 30.0} + end + + def defaults_for_orders_cycle_plugin_cycle + {:profile => build(OrdersCyclePlugin::Profile), :status => 'orders', + :name => 'weekly', :start => Time.now, :finish => Time.now+1.days} + end + +end + +Noosfero::Factory.register_extension OrdersCyclePlugin::Factory + diff --git a/plugins/orders_cycle/test/functional/orders_cycle_plugin/order_controller_test.rb b/plugins/orders_cycle/test/functional/orders_cycle_plugin/order_controller_test.rb new file mode 100644 index 0000000..9a93fce --- /dev/null +++ b/plugins/orders_cycle/test/functional/orders_cycle_plugin/order_controller_test.rb @@ -0,0 +1,12 @@ +require "#{File.dirname(__FILE__)}/../../test_helper" + +class OrdersCyclePlugin::OrderControllerTest < Test::Unit::TestCase + + def setup + @controller = OrdersCyclePluginOrderController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + +end diff --git a/plugins/orders_cycle/test/functional/orders_cycle_plugin/session_controller_test.rb b/plugins/orders_cycle/test/functional/orders_cycle_plugin/session_controller_test.rb new file mode 100644 index 0000000..7ab2402 --- /dev/null +++ b/plugins/orders_cycle/test/functional/orders_cycle_plugin/session_controller_test.rb @@ -0,0 +1,14 @@ +require "#{File.dirname(__FILE__)}/../../test_helper" + +class OrdersCyclePlugin::CycleControllerTest < Test::Unit::TestCase + + def setup + @controller = OrdersCyclePluginCycleController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + should 'create a new cycle' do + end + +end diff --git a/plugins/orders_cycle/test/test_helper.rb b/plugins/orders_cycle/test/test_helper.rb new file mode 100644 index 0000000..3f0b088 --- /dev/null +++ b/plugins/orders_cycle/test/test_helper.rb @@ -0,0 +1,6 @@ +require File.dirname(__FILE__) + '/../../../test/test_helper' +require 'spec' + +class ActiveRecord::TestCase < ActiveSupport::TestCase + include OrdersCyclePluginFactory +end diff --git a/plugins/orders_cycle/test/unit/orders_cycle_plugin/cycle_test.rb b/plugins/orders_cycle/test/unit/orders_cycle_plugin/cycle_test.rb new file mode 100644 index 0000000..7c09b02 --- /dev/null +++ b/plugins/orders_cycle/test/unit/orders_cycle_plugin/cycle_test.rb @@ -0,0 +1,27 @@ +require "#{File.dirname(__FILE__)}/../../test_helper" + +class OrdersCyclePlugin::CycleTest < ActiveSupport::TestCase + + def setup + @profile = Enterprise.create!(:name => "trocas verdes", :identifier => "trocas-verdes") + @pc = ProductCategory.create!(:name => 'frutas', :environment_id => 1) + @profile.products = [Product.create!(:name => 'banana', :product_category => @pc), + Product.new(:name => 'mandioca', :product_category => @pc), Product.new(:name => 'alface', :product_category => @pc)] + + profile.offered_products = @profile.products.map{ |p| OrdersCyclePlugin::OfferedProduct.create!(:product => p) } + DeliveryPlugin::Method.create! :name => 'at home', :delivery_type => 'pickup', :profile => @profile + @cycle = OrdersCyclePlugin::Cycle.create!(:profile => @profile) + end + + should 'add products from profile after create' do + assert_equal @cycle.products.collect(&:product_id), @profile.products.collect(&:id) + end + + should 'have at least one delivery method unless in edition status' do + cycle = OrdersCyclePlugin::Cycle.create! :profile => @profile, :name => "Testes batidos", :start => DateTime.now, :status => 'edition' + assert cycle + cycle.status = 'orders' + assert_nil cycle.save! + end + +end diff --git a/plugins/orders_cycle/test/unit/orders_cycle_plugin/offered_product_test.rb b/plugins/orders_cycle/test/unit/orders_cycle_plugin/offered_product_test.rb new file mode 100644 index 0000000..7a92fde --- /dev/null +++ b/plugins/orders_cycle/test/unit/orders_cycle_plugin/offered_product_test.rb @@ -0,0 +1,6 @@ +require "#{File.dirname(__FILE__)}/../../test_helper" + +class OrdersCyclePlugin::OfferedProductTest < ActiveSupport::TestCase + + +end diff --git a/plugins/orders_cycle/test/unit/profile_test.rb b/plugins/orders_cycle/test/unit/profile_test.rb new file mode 100644 index 0000000..4b2e21f --- /dev/null +++ b/plugins/orders_cycle/test/unit/profile_test.rb @@ -0,0 +1,127 @@ +require "#{File.dirname(__FILE__)}/../../test_helper" + +class OrdersCyclePlugin::ProfileTest < ActiveRecord::TestCase + + def setup + @profile = build(Profile) + @invisible_profile = build(Enterprise, :visible => false) + @other_profile = build(Enterprise) + @profile = build(OrdersCyclePlugin::profile, :profile => @profile) + @self_supplier = build(OrdersCyclePlugin::Supplier, :consumer => @profile, :profile => @profile) + @dummy_supplier = build(OrdersCyclePlugin::Supplier, :consumer => @profile, :profile => @dummy_profile) + @other_supplier = build(OrdersCyclePlugin::Supplier, :consumer => @profile, :profile => @other_profile) + end + + attr_accessor :profile, :invisible_profile, :other_profile, + :self_supplier, :dummy_supplier, :other_supplier + + should 'respond to name methods' do + profile.expects(:name).returns('name') + assert_equal 'name', profile.name + end + + should 'respond to dummy methods' do + profile.dummy = true + assert_equal true, profile.dummy? + profile.dummy = false + assert_equal false, profile.dummy + end + + should "return closed cycles' date range" do + DateTime.expects(:now).returns(1).at_least_once + assert_equal 1..1, profile.orders_cycles_closed_date_range + s1 = create(OrdersCyclePlugin::Cycle, :profile => profile, :start => Time.now-1.days, :finish => nil) + s2 = create(OrdersCyclePlugin::Cycle, :profile => profile, :finish => Time.now+1.days, :start => Time.now) + assert_equal (s1.start.to_date..s2.finish.to_date), profile.orders_cycles_closed_date_range + end + + should 'return abbreviation or the name' do + profile.name_abbreviation = 'coll.' + profile.profile.name = 'collective' + assert_equal 'coll.', profile.abbreviation_or_name + profile.name_abbreviation = nil + assert_equal 'collective', profile.abbreviation_or_name + end + + ### + # Products + ### + + should "default products's margins when asked" do + profile.update_attributes! :margin_percentage => 10 + product = create(SuppliersPlugin::DistributedProduct, :profile => profile, :supplier => profile.self_supplier, + :price => 10, :default_margin_percentage => false) + cycle = create(OrdersCyclePlugin::Cycle, :profile => profile) + sproduct = cycle.products.first + sproduct.update_attributes! :margin_percentage => 5 + cycleclosed = create(OrdersCyclePlugin::Cycle, :profile => profile, :status => 'closed') + + profile.orders_cycles_products_default_margins + product.reload + sproduct.reload + assert_equal true, product.default_margin_percentage + assert_equal sproduct.margin_percentage, profile.margin_percentage + end + + should 'return not yet distributed products' do + profile.save! + other_profile.save! + other_supplier.save! + product = create(SuppliersPlugin::DistributedProduct, :profile => other_profile, :supplier => other_profile.self_supplier) + profile.add_supplier_products other_supplier + product2 = create(SuppliersPlugin::DistributedProduct, :profile => other_profile, :supplier => other_profile.self_supplier) + assert_equal [product2], profile.not_distributed_products(other_supplier) + end + + ### + # Suppliers + ### + + should 'add supplier' do + @profile.save! + @other_profile.save! + + assert_difference OrdersCyclePlugin::Supplier, :count do + @profile.add_supplier @other_profile + end + assert @profile.suppliers_profiles.include?(@other_profile) + assert @other_profile.consumers_profiles.include?(@profile) + end + + should "add all supplier's products when supplier is added" do + @profile.save! + @other_profile.save! + product = create(SuppliersPlugin::DistributedProduct, :profile => @other_profile) + @profile.add_supplier @other_profile + assert_equal [product], @profile.from_products + end + + should 'remove supplier' do + @profile.save! + @other_profile.save! + + @profile.add_supplier @other_profile + assert_difference OrdersCyclePlugin::Supplier, :count, -1 do + assert_difference RoleAssignment, :count, -1 do + @profile.remove_supplier @other_profile + end + end + assert !@profile.suppliers_profiles.include?(@other_profile) + end + + should "archive supplier's products when supplier is removed" do + @profile.save! + @other_profile.save! + product = create(SuppliersPlugin::DistributedProduct, :profile => @other_profile) + @profile.add_supplier @other_profile + @profile.remove_supplier @other_profile + assert_equal [product], @profile.from_products + assert_equal 1, @profile.distributed_products.archived.count + end + + should 'create self supplier automatically' do + profile = create(OrdersCyclePlugin::profile, :profile => @profile) + assert_equal 1, profile.suppliers.count + end + +end diff --git a/plugins/orders_cycle/views/orders_cycle_plugin/mailer/open_cycle.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin/mailer/open_cycle.html.erb new file mode 100644 index 0000000..f2dd809 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin/mailer/open_cycle.html.erb @@ -0,0 +1,28 @@ + + + + + + +

+ <%= t('views.mailer.open_cycle.hello_member_of_name') % {:name => @profile.name} %> +

+ +

<%= t('views.mailer.open_cycle.a_new_cycle_is_open_c') + @cycle.name %>

+
+

<%= @cycle.description %>

+
+

<%= t('views.mailer.open_cycle.the_administrator_let') %>

+

<%= @message %>

+ +

<%= link_to t('views.order.index.place_an_order'), {:controller => :orders_cycle_plugin_order, :action => :edit, :cycle_id => @cycle.id, :profile => @profile.identifier, :only_path => false, :host => @profile.hostname || @environment.default_hostname }, :id => 'consumer-new-order' %>

+ +

<%= @domain %> +

+ --
+

+ + <%= @environment.name %> + + + diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_closed.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_closed.html.erb new file mode 100644 index 0000000..4c3a950 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_closed.html.erb @@ -0,0 +1,2 @@ +<%= render 'title', :title => t('views.cycle._closed.cycle_already_finishe') %> + diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_closing.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_closing.html.erb new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_closing.html.erb diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_cycle_purchases.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_cycle_purchases.html.slim new file mode 100644 index 0000000..c64a4c4 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_cycle_purchases.html.slim @@ -0,0 +1,2 @@ += render 'orders_plugin_admin/purchases', actors: @cycle.suppliers, orders_owner: @cycle, owner_id: @cycle.id, + orders: @cycle.purchases.default_order.paginate(per_page: 30, page: params[:page]) diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_cycle_sales.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_cycle_sales.html.slim new file mode 100644 index 0000000..052c676 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_cycle_sales.html.slim @@ -0,0 +1,2 @@ += render 'orders_plugin_admin/sales', actors: @cycle.consumers, orders_owner: @cycle, owner_id: @cycle.id, + orders: @cycle.sales.default_order.paginate(per_page: 30, page: params[:page]) diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_delivery.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_delivery.html.erb new file mode 100644 index 0000000..67f7d90 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_delivery.html.erb @@ -0,0 +1,6 @@ +
+ <%= hideable_help_text t('views.cycle._delivery.header_help') %> +
+ +<%= render 'orders_cycle_plugin_cycle/cycle_sales' %> + diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_edit_fields.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_edit_fields.html.slim new file mode 100644 index 0000000..2e10b03 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_edit_fields.html.slim @@ -0,0 +1,58 @@ += render 'orders_plugin/shared/daterangepicker/init' + +h3= t('views.cycle._edit_fields.general_settings') + += form_for @cycle, as: :cycle , remote: true, url: {action: @cycle.new? ? :new : :edit, id: @cycle.id }, html: {data: {loading: '#cycle-fields form'}} do |f| + + = labelled_field f, :name, t('views.cycle._edit_fields.name'), f.text_field(:name), class: 'cycle-field-name' + = labelled_field f, :description, t('views.cycle._edit_fields.description'), f.text_area(:description, class: 'mceEditor'), class: 'cycle-field-description' + = render file: 'shared/tiny_mce', locals: {mode: 'simple'} + + .cycle-fields-block + = labelled_datetime_range_field f, :start, :finish, t('views.cycle._edit_fields.orders_interval'), class: 'cycle-orders-period' + .cycle-fields-block + = labelled_datetime_range_field f, :delivery_start, :delivery_finish, t('views.cycle._edit_fields.deliveries_interval'), class: 'cycle-orders-period' + + .cycle-fields-block + #cycle-delivery.field + = f.label :delivery_methods, t('views.cycle._edit_fields.available_delivery_me') + div + #cycle-delivery-options.subtitle + = render 'delivery_plugin/admin_options/index', owner: @cycle + = modal_link_to t('views.cycle._edit_fields.add_method'), + {controller: :orders_cycle_plugin_delivery_option, action: :select, owner_id: @cycle.id, owner_type: @cycle.class.name}, + class: 'subtitle' + |  + = link_to_function t('views.cycle._edit_fields.add_all_methods'), + "$.getScript('#{url_for controller: :orders_cycle_plugin_delivery_option, action: :select_all, owner_id: @cycle.id, owner_type: @cycle.class.name}')", + class: 'subtitle' + .clean + + - if profile.volunteers_settings.cycle_volunteers_enabled + = render 'volunteers_plugin_myprofile/edit_periods', owner: @cycle, f: f + + #cycle-new-mail + = check_box_tag('sendmail', 'yes', false, id: 'cycle-new-mail-send') + = content_tag('label', t('views.cycle._edit_fields.notify_members_of_ope'), for: 'sendmail') + .mail-message + = f.label :sendmail, t('views.cycle._edit_fields.opening_message') + div= t('views.cycle._edit_fields.this_message_will_be_') + = f.text_area(:opening_message, onkeyup: 'orders_cycle.cycle.edit.openingMessage.onKeyup(this)') + javascript: + $('#cycle-new-mail-send').on('click', orders_cycle.cycle_mail_message_toggle) + + - submit_text = if @cycle.new? then t('views.cycle._edit_fields.create_new_cycle') else t('views.cycle._edit_fields.save') end + = f.submit submit_text + - unless @cycle.passed_by? 'edition' + |  + = hidden_field_tag 'open', nil + - text = t("views.cycle._edit_fields.#{if @cycle.new? then 'create' else 'save' end}_and_open_orders") + = f.submit text, onclick: "this.form.elements['open'].value = '1'" + + |  + = link_to t('views.cycle._edit_fields.cancel_changes'), @cycle.new? ? {action: :index} : params + - unless @cycle.new? + |  + = link_to t('views.cycle._edit_fields.remove'), {action: :destroy, id: @cycle.id}, confirm: t('views.cycle._edit_fields.confirm_remove') + + diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_edit_popin.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_edit_popin.html.erb new file mode 100644 index 0000000..da2351a --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_edit_popin.html.erb @@ -0,0 +1,18 @@ +
+

<%= t('views.cycle._edit_popin.cycle_editing') %>

+ +
+ <% if cycle.valid? %> + <%= t('views.cycle._edit_popin.cycle_saved') %> + <% else %> + <% cycle.errors.full_messages.each do |msg| %> +
<%= msg %>
+ <% end %> + <% end %> +
+ +
+ <%= modal_close_button t('views.cycle._edit_popin.close') %> +
+ +
diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_edition.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_edition.html.erb new file mode 100644 index 0000000..6f31c5d --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_edition.html.erb @@ -0,0 +1,26 @@ +<% if @cycle.passed_by? 'edition' %> + <%= render 'title', :title => t('views.cycle._edition.info') %> +<% end %> + +
+ <%= render 'edit_fields' %> +
+ +
+
+

<%= t('views.cycle._edition.the_products') %>

+ +
<%= t('views.cycle._edition.the_following_list_of') %>
+
<%= t('views.cycle._edition.it_was_automatically_') %>
+
+ +
+ <% if @cycle.add_products_job %> + <%= render 'products_loading' %> + <% else %> + <%= render 'product_lines' %> + <% end %> +
+ +
+ diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_filter_fields.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_filter_fields.html.erb new file mode 100644 index 0000000..5c5d8d2 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_filter_fields.html.erb @@ -0,0 +1,15 @@ +
+ +
+ <% range = @profile.orders_cycles_closed_date_range %> + <%= select_year @year_date, {:prompt => '', :start_year => range.first.year, :end_year => range.last.year} %> +
+
+ +
+ +
+ <%= select_month @month_date, {:prompt => '' } %> +
+
+ diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_header.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_header.html.erb new file mode 100644 index 0000000..249dd98 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_header.html.erb @@ -0,0 +1,18 @@ +<% + actions = true if actions.nil? and @admin + timeline = true if actions.nil? and @admin + listing = false if listing.nil? +%> + +

+ <% if cycle.new? %> + + <% else %> + <%= link_to "#{t('views.cycle._title.order_cycle') if not listing}#{cycle.name_with_code}", {:action => 'edit', :id => cycle.id }, :class => "cycle-edit-link" %> + <% end %> + + <%= t('views.cycle._brief.confirmed_orders', :count => cycle.sales.ordered.count) if listing and not cycle.new? %> +

+ +<%= render 'timeline', :cycle => cycle, :actions => actions, :listing => listing if timeline %> + diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_orders.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_orders.html.erb new file mode 100644 index 0000000..de8b644 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_orders.html.erb @@ -0,0 +1,14 @@ +
+ <%= hideable_help_text t('views.cycle._orders.header_help') %> +
+ +<% if @cycle.orders? or @cycle.passed_by? 'orders' %> + <% if @cycle.orders? %> + <% header = t('views.cycle._orders.the_orders_period_is_') %> + <% elsif @cycle.passed_by? 'orders' %> + <% header = t('views.cycle._orders.already_closed') %> + <% end %> +<% end %> + +<%= render 'orders_cycle_plugin_cycle/cycle_sales' %> + diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_product_lines.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_product_lines.html.erb new file mode 100644 index 0000000..36591fb --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_product_lines.html.erb @@ -0,0 +1,30 @@ +
+ <%= pagination_links @products %> +
+ <%= t('views.cycle._product_lines.showing_pcount_produc') % {:pcount => @products.length, :allpcount => @cycle.products.count} %> +
+
+ <%= t('views.cycle._product_lines.category') %> + <%= t('views.cycle._product_lines.supplier') %> + <%= t('views.cycle._product_lines.product') %> + <%= t('views.cycle._product_lines.price') %> + <%= t('views.cycle._product_lines.qty_avail') %> + + +
+
+ +
+ <% @products.each do |p| %> +
+ <%= render 'orders_cycle_plugin_product/cycle_edit', :p => p %> +
+ <% end %> + <%= javascript_tag do %> + jQuery(document).ready(function() { + orders_cycle.ajaxifyPagination("#cycle-products .table") + }) + <% end %> +
+
+ diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_products_loading.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_products_loading.html.erb new file mode 100644 index 0000000..36b3933 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_products_loading.html.erb @@ -0,0 +1,9 @@ + +
+ <%= t('views.cycle._products_loading') %> + + <%= javascript_tag do %> + orders_cycle.cycle.products.load_url = <%= url_for(:action => :products_load, :id => @cycle.id).to_json %> + orders_cycle.cycle.products.load() + <% end %> +
diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_purchases.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_purchases.html.slim new file mode 100644 index 0000000..e06e0d3 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_purchases.html.slim @@ -0,0 +1,2 @@ +.subtitle= hideable_help_text t('views.cycle._purchases.header_help') += render 'orders_cycle_plugin_cycle/cycle_purchases' diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_receipts.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_receipts.html.erb new file mode 100644 index 0000000..aab49d2 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_receipts.html.erb @@ -0,0 +1,6 @@ +
+ <%= hideable_help_text t('views.cycle._receipts.header_help') %> +
+ +<%= render 'orders_cycle_plugin_cycle/cycle_purchases' %> + diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_results.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_results.html.erb new file mode 100644 index 0000000..919b3b8 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_results.html.erb @@ -0,0 +1,9 @@ +<% if @closed_cycles.blank? %> +
<%= t('views.cycle._results.no_cycles_to_show') %>
+<% else %> + <% @closed_cycles.each do |cycle| %> +
+ <%= render 'header', :cycle => cycle, :timeline => true, :actions => false, :listing => true %> +
+ <% end %> +<% end %> diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_separation.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_separation.html.erb new file mode 100644 index 0000000..e1478da --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_separation.html.erb @@ -0,0 +1,6 @@ +
+ <%= hideable_help_text t('views.cycle._separation.header_help') %> +
+ +<%= render 'orders_cycle_plugin_cycle/cycle_sales' %> + diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_timeline.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_timeline.html.erb new file mode 100644 index 0000000..0c1db1d --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_timeline.html.erb @@ -0,0 +1,43 @@ +<% + actions = true if actions.nil? and @admin + listing = false if listing.nil? + view_status = if OrdersCyclePlugin::Cycle::Statuses.index(params[:view]) then params[:view] else cycle.status end + status_text = t("models.cycle.statuses.#{view_status}") +%> + +
+ <% OrdersCyclePlugin::Cycle::UserStatuses.each do |status| %> + <% + klass = "cycle-timeline-item #{timeline_class cycle, status, view_status}" + name = t("models.cycle.statuses.#{status}") + %> + + <%= link_to name, params.merge(:action => :edit, :id => cycle.id, :view => status), :class => klass %> + <% end %> +
+ +<% if listing %> +
+
+ <%= t('views.cycle._brief.orders') %>  + <%= datetime_period_short cycle.start, cycle.finish %> +
+
+ <%= t('views.cycle._brief.delivery') %>  + <%= datetime_period_short cycle.delivery_start, cycle.delivery_finish %> +
+
+<% end %> + +<% if actions %> +
+ <% if cycle.status == view_status and cycle.status != 'closing' %> + <%= link_to t('views.cycle._timeline.close_status') % {:status => status_text}, + {:action => :step, :id => cycle.id, :method => :post}, + {:confirm => t('views.cycle._timeline.are_you_sure_you_want_to_close') % {:status => status_text}, :class => 'action-button'} %> + <% end %> + + <%= render "orders_cycle_plugin_cycle/actions/#{view_status}", :cycle => cycle rescue nil %> +
+<% end %> + diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_title.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_title.html.erb new file mode 100644 index 0000000..b9fc648 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_title.html.erb @@ -0,0 +1,5 @@ +
+
+ <%= title %> +
+
diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_view_dates.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_view_dates.html.slim new file mode 100644 index 0000000..60e266c --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_view_dates.html.slim @@ -0,0 +1,13 @@ +- happening = true if !defined?(happening) or happening.nil? + +table#cycle-dates + tr class="orders-date #{"happening" if happening and cycle.orders?}" + th + strong= t'views.cycle._view_dates.orders' + td= datetime_period_with_day cycle.start, cycle.finish + - if happening and cycle.orders? + td.happening= t'views.cycle._view_dates.happening' + tr.delivery-date + th + strong= t'views.cycle._view_dates.delivery' + td= datetime_period_with_day cycle.delivery_start, cycle.delivery_finish diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_view_header.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_view_header.html.erb new file mode 100644 index 0000000..42a63f9 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_view_header.html.erb @@ -0,0 +1,14 @@ +
+ <% others = (profile.orders_cycles.on_orders - [cycle]) %> + <%= ( others.blank? ? t('views.cycle._view_header.see_also_all') : t('views.cycle._view_header.other_open_cycles_lis') ) % { + list: others.map{ |s| link_to(s.name, '') }.join(t('views.cycle._view_header., ')), + all: link_to(t('views.cycle._view_header.all_orders_cycles'), {controller: :orders_cycle_plugin_order, action: :index}), } %> +
+ +
+

<%=t('views.cycle._view_header.orders_cycle_cycle') % {cycle: content_tag(:strong, cycle.name_with_code)} %>

+ + <%= render partial: 'orders_cycle_plugin_cycle/view_dates', locals: {cycle: cycle} %> + +
<%= cycle.description %>
+
diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_view_products.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_view_products.html.erb new file mode 100644 index 0000000..053fea7 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_view_products.html.erb @@ -0,0 +1,14 @@ +
+
+

<%=t('views.cycle._view_products.the_products')%>

+
+ + <%= render partial: "orders_cycle_plugin_shared/filter", locals: {type: :order, cycle: cycle, order: order} %> + +
+ <%= render partial: "orders_cycle_plugin_order/filter", locals: { + cycle: cycle, order: order, + products_for_order: @products, + } %> +
+
diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_with_selection_actions.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_with_selection_actions.html.slim new file mode 100644 index 0000000..29938c9 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/_with_selection_actions.html.slim @@ -0,0 +1,8 @@ +.btn-group + button.btn.btn-default.btn-xs.dropdown-toggle aria-expanded="false" data-toggle="dropdown" type="button" + = t('views.admin.reports.generate') + span.caret + + ul.dropdown-menu role="menu" + li= link_to_function t('views.admin.reports.orders_spreadsheet'), "orders.admin.select.report('#{url_for action: :report_orders, id: @cycle.id, orders_method: orders_method}')" + li= link_to_function t('views.admin.reports.products_spreadsheet'), "orders.admin.select.report('#{url_for action: :report_products, id: @cycle.id, orders_method: orders_method}')" diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/actions/_closed.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/actions/_closed.html.erb new file mode 100644 index 0000000..fa142f9 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/actions/_closed.html.erb @@ -0,0 +1,4 @@ +<% if cycle.status == 'closing' %> + <%= link_to t('views.cycle._timeline.reopen_orders_period'), {action: :step_back, id: cycle.id, method: :post}, + {id: 'cycle-open-cycle' , confirm: t('views.cycle._timeline.are_you_sure_you_want_to_reopen'), class: 'action-button'} %> +<% end %> diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/actions/_orders.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/actions/_orders.html.slim new file mode 100644 index 0000000..831f691 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/actions/_orders.html.slim @@ -0,0 +1,8 @@ +.btn-group + button.btn.btn-default.btn-xs.dropdown-toggle aria-expanded="false" data-toggle="dropdown" type="button" + = t('views.admin.reports.generate') + span.caret + + ul.dropdown-menu role="menu" + li= link_to t('views.admin.reports.orders_spreadsheet'), {action: :report_orders, id: cycle.id} + li= link_to t('views.admin.reports.products_spreadsheet'), {action: :report_products, id: cycle.id} diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/edit.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/edit.html.erb new file mode 100644 index 0000000..4ed3219 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/edit.html.erb @@ -0,0 +1,11 @@ +
+ +
+ <%= render 'header', :cycle => @cycle, :timeline => true, :actions => true, :listing => false %> + + <% view = if params[:view] then params[:view] else @cycle.status end %> +
+ <%= render view %> +
+
+
diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/edit.js.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/edit.js.erb new file mode 100644 index 0000000..f208e88 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/edit.js.erb @@ -0,0 +1,9 @@ +<% if params[:commit] %> + <% if @success and @open %> + window.location = <%= url_for(:action => :edit, :id => @cycle.id).to_json %> + <% else %> + noosfero.modal.html(<%= render('edit_popin', :cycle => @cycle).to_json %>) + <% end %> +<% else %> + jQuery("#cycle-products .table").replaceWith('<%= escape_javascript render('product_lines') %>') +<% end %> diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/index.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/index.html.erb new file mode 100644 index 0000000..ef4e4a2 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/index.html.erb @@ -0,0 +1,32 @@ +<% cycles = @open_cycles %> + +
+

<%= t('views.cycle.index.orders_cycles') %>

+ + <%= link_to t('views.cycle.index.new_cycle'), {:action => :new}, :class => 'cycle-new' %> + +
+

<%= t('views.cycle.index.open_cycles') %>

+ + <% if cycles.blank? %> +
<%= t('views.cycle.index.no_cycles_to_show') %>
+ <% else %> + <% cycles.each do |cycle| %> +
+ <%= render 'header', :cycle => cycle, :timeline => true, :actions => false, :listing => true %> +
+ <% end %> + <% end %> +
+ +
+

<%= t('views.cycle.index.closed_cycles') %>

+ + <%= render "orders_cycle_plugin_shared/filter", :type => :orders_cycle, :wireframe_size => true %> + +
+ <%= render 'results' %> +
+
+ +
diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/new.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/new.html.erb new file mode 100644 index 0000000..4f810d3 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/new.html.erb @@ -0,0 +1,7 @@ +
+ <%= render 'header', cycle: @cycle, timeline: false, actions: false %> + +
+ <%= render 'edit_fields' %> +
+
diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/new.js.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/new.js.erb new file mode 100644 index 0000000..5d536cb --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/new.js.erb @@ -0,0 +1,2 @@ +window.location.href = <%= url_for(action: :edit, id: @cycle.id).to_json %> + diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/orders_filter.js.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/orders_filter.js.erb new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/orders_filter.js.erb diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_cycle/report_products.erb b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/report_products.erb new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_cycle/report_products.erb diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_gadgets/_cycle.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_gadgets/_cycle.html.erb new file mode 100644 index 0000000..00f82b7 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_gadgets/_cycle.html.erb @@ -0,0 +1,25 @@ +
+
<%= t('views.gadgets._cycle.happening') %>
+
+

+ <%= t('views.gadgets._cycle.orders_open_b_cycle') % {:cycle => cycle.name_with_code} %> +

+ + + + + + + +
+ <%= render :partial => 'orders_cycle_plugin_cycle/view_dates', + :locals => {:cycle => cycle, :happening => false} %> + +
<%= cycle.description %>
+
+ <%= button_to t('views.gadgets._cycle.see_orders_cycle'), {:controller => :orders_cycle_plugin_order, :action => :edit, :cycle_id => cycle.id}, :class => "action-button" %> + <%# button_to t('views.gadgets._cycle.place_an_order'), {:controller => :orders_cycle_plugin_order, :action => :new, :cycle_id => cycle.id} %> +
+
+
+
diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_gadgets/cycles.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_gadgets/cycles.html.erb new file mode 100644 index 0000000..7fb4f12 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_gadgets/cycles.html.erb @@ -0,0 +1,12 @@ +<% extend OrdersCyclePlugin::TranslationHelper %> + +
+ <% profile.orders_cycles.on_orders.each do |cycle| %> + <%= render :partial => 'orders_cycle_plugin_gadgets/cycle', :locals => {:cycle => cycle} %> + <% end %> + +
+ <%= link_to t('views.gadgets.cycles.all_cycles'), {:controller => :orders_cycle_plugin_order, :profile => profile.identifier, :action => :index}, :id => "all-cycles" %> +
+
+
diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_item/_edit.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_item/_edit.html.erb new file mode 120000 index 0000000..3d7db11 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_item/_edit.html.erb @@ -0,0 +1 @@ +../../../orders/views/orders_plugin_item/_edit.html.erb \ No newline at end of file diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_item/_index.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_item/_index.html.slim new file mode 120000 index 0000000..071b4eb --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_item/_index.html.slim @@ -0,0 +1 @@ +../../../orders/views/orders_plugin_item/_index.html.slim \ No newline at end of file diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_item/_price_total.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_item/_price_total.html.erb new file mode 120000 index 0000000..9dc5bd1 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_item/_price_total.html.erb @@ -0,0 +1 @@ +../../../orders/views/orders_plugin_item/_price_total.html.erb \ No newline at end of file diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_item/_quantity_price.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_item/_quantity_price.html.slim new file mode 120000 index 0000000..3bfbeef --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_item/_quantity_price.html.slim @@ -0,0 +1 @@ +../../../orders/views/orders_plugin_item/_quantity_price.html.slim \ No newline at end of file diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_item/destroy.js.erb b/plugins/orders_cycle/views/orders_cycle_plugin_item/destroy.js.erb new file mode 100644 index 0000000..eb82b2f --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_item/destroy.js.erb @@ -0,0 +1,4 @@ +jQuery('#consumer-order-<%=@order.id%>').html(<%= render('orders_cycle_plugin_order/active_order', order: @order, actor_name: @actor_name).to_json %>); + +orders_cycle.order.product.load(<%= @offered_product.id.to_json %>, false); + diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_item/new.js.erb b/plugins/orders_cycle/views/orders_cycle_plugin_item/new.js.erb new file mode 100644 index 0000000..d5acbf2 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_item/new.js.erb @@ -0,0 +1,10 @@ +<% unless params[:redirect] == '1' %> + orders_cycle.order.product.load(<%= @offered_product.id %>, true); + + $('#consumer-order-<%=@order.id%>').html(<%= render('orders_cycle_plugin_order/active_order', order: @order, actor_name: @actor_name).to_json %>); + + var item = $('#item-<%=@item.id%>'); + orders.item.edit_quantity(item); +<% else %> + window.location = <%= url_for(controller: :orders_cycle_plugin_order, action: :edit, id: @order.id).to_json %> +<% end %> diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_order/_active_order.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_order/_active_order.html.erb new file mode 100644 index 0000000..52c7a10 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_order/_active_order.html.erb @@ -0,0 +1,35 @@ +<% content_for :order_header do %> + <%= render 'orders_cycle_plugin_order/status', order: @order %> + + <% unless @order.open? %> +
+
+ <% if @order.confirmed? %> + <%= t('views.order._consumer_orders.your_order_is_confirm') %> + <% elsif @order.cancelled? %> + <%= t('views.order._consumer_orders.your_order_was_cancel') %> + <% else %> + <%= t('views.order._consumer_orders.your_order_wasn_t_con') %> + <% end %> +
+
+
<%= t('views.order._consumer_orders.you_still_can') %>
+ + <% if @order.cycle.orders? %> + <%= link_to_function t('views.order._consumer_orders.change_order'), "orders.order.reload(this, '#{url_for controller: :orders_cycle_plugin_order, action: :reopen, id: @order.id}')" %> + <%= t('views.order._consumer_orders.before_the_closing') %>  + <% end %> +
+ + <% unless @order.cancelled? %> + <%= link_to_function t('views.order._show.cancel_order'), "orders.order.reload(this, '#{url_for controller: :orders_cycle_plugin_order, action: :cancel, id: @order.id}')" %> +
+ <% end %> + + <%= modal_link_to t('views.order._consumer_orders.send_message_to_the_m'), {controller: :orders_plugin_message, action: :new_to_admins} %> +
+
+ <% end %> +<% end %> + +<%= render 'orders_plugin_order/show', order: @order, actor_name: :consumer %> diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_order/_consumer_orders.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_order/_consumer_orders.html.slim new file mode 100644 index 0000000..3b4efea --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_order/_consumer_orders.html.slim @@ -0,0 +1,69 @@ += content_for :head do + / while we are not responsive + = stylesheet_link_tag '/assets/designs/icons/awesome/scss/font-awesome.css' + +h3 + - if @admin_edit + = t('views.order._consumer_orders.orders_from_consumer_') % {consumer: @consumer.name} + - elsif @consumer and @cycle.orders? + = t('views.order._consumer_orders.your_orders_on_this_c') + - if @consumer.in? @cycle.profile.members + = link_to t('views.order._consumer_orders.new_order'), {action: :new, cycle_id: @cycle.id}, + class: 'btn btn-default btn-danger fa fa-plus', onclick: "$(this).attr('disabled', '')" + - if not @admin_edit and @cycle.consumer_previous_orders(@consumer).present? + = link_to t('views.order._consumer_orders.repeat_order'), {action: :repeat, cycle_id: @cycle.id}, + class: 'modal-toggle btn btn-default btn-danger fa fa-repeat' + +- if @order and @order.consumer != @consumer and @admin_edit + #order-admin-warning.order-message + .order-message-text + = t'views.order._consumer_orders.caution', consumer: @consumer.name + .order-message-actions + = link_to t('views.order._consumer_orders.edit_your_orders'), {action: :edit, cycle_id: @cycle.id} + |  + = link_to t('views.order._consumer_orders.administration_of_thi'), {controller: :orders_cycle_plugin_cycle, action: :edit, id: @cycle.id} + |  + #order-place-new.admin + = link_to t('views.order._consumer_orders.new_order'), {action: :admin_new, consumer_id: @order.consumer.id, cycle_id: @cycle.id} if @cycle.orders? +- else + - if @consumer.nil? + - message = t'views.order._consumer_orders.to_place_an_order_you', + login: modal_link_to(t('views.order._consumer_orders.login'), login_url, id: 'link_login'), + signup: link_to(t('views.order._consumer_orders.sign_up'), controller: 'account', action: 'signup') + - else + - if @cycle.orders? + - if not @admin and not @consumer.in? @cycle.profile.members and @cycle.profile.community? + = t'views.order._consumer_orders.associate_to_order' + = render 'blocks/profile_info_actions/join_leave_community' + - elsif @consumer_orders.count == 0 + - message = t'views.order._consumer_orders.you_haven_t_placed_an' + - elsif @cycle.edition? + - message = t'views.order._consumer_orders.this_cycle_is_not_ope' + - elsif @cycle.before_orders? + - message = t'views.order._consumer_orders.the_time_for_orders_is', start: l(@cycle.start, format: :short), finish: l(@cycle.finish, format: :short) + - elsif @cycle.after_orders? + - message = t'views.order._consumer_orders.this_cycle_is_already' + - if message + #order-place-new + = message +.clean + +- @consumer_orders.each do |order| + - if @order != order and order.current_status == 'cancelled' + #show-cancelled-orders + = link_to_function t('views.order._consumer_orders.show_cancelled_orders'), "orders_cycle.toggleCancelledOrders()" + #hide-cancelled-orders + = link_to_function t('views.order._consumer_orders.hide_cancelled_orders'), "orders_cycle.toggleCancelledOrders()", style: 'display:none' + - break +- @consumer_orders.each do |order| + - next if @order == order + div class=("consumer-order unactive #{order.current_status}") id="consumer-order-#{order.id}" + = render 'status', order: order + .actions + = link_to t('views.order._show.open'), {action: :edit, id: order.id} + .clean + +/ prints out the referenced order +- if @order + .consumer-order.edit.active-order id="consumer-order-#{@order.id}" + = render 'orders_cycle_plugin_order/active_order' diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_order/_filter.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_order/_filter.html.slim new file mode 100644 index 0000000..dacf71b --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_order/_filter.html.slim @@ -0,0 +1,43 @@ +- draft_order = cycle.sales.draft.for_consumer(user).first +- include_message = '' +- if order.nil? + - if draft_order + - order_id = draft_order.id + - include_message = t'orders_cycle_plugin.views.product._order_edit.opening_order_code_fo', code: draft_order.code + - elsif cycle.may_order? user + - order_id = 'new' + - include_message = t'orders_cycle_plugin.views.product._order_edit.opening_new_order_for' +- else + - order_id = order.id +- editable = (order.present? && order.may_edit?(user, @admin)) || order_id == 'new' + +- if products_for_order.empty? + strong= t'orders_cycle_plugin.views.product._order_search.this_search_hasn_t_re' +- else + #cycle-products.sortable-table + .table-header + .box-field.category= t'orders_cycle_plugin.views.product._order_search.category' + .box-field.supplier= t'orders_cycle_plugin.views.product._order_search.producer' + .box-field.product= t'orders_cycle_plugin.views.product._order_search.product' + .box-field.price-with-unit= t'orders_cycle_plugin.views.product._order_search.price' + .table-content + - products_for_order.each do |offered_product| + - next if offered_product.supplier.nil? + div class=("order-cycle-product value-row #{'editable' if editable}") id="cycle-product-#{offered_product.id}" onclick="orders_cycle.order.product.click(event, #{offered_product.id.to_json});" supplier-id="#{offered_product.supplier.id}" toggle-ignore="" + - item = if order.blank? then nil else order.items.find{ |op| offered_product.id == op.product_id } end + = render 'orders_cycle_plugin_product/order_edit', editable: editable, + product: offered_product, order: order, cycle: cycle, item: item + + = pagination_links products_for_order, params: {action: :edit, controller: :orders_cycle_plugin_order, cycle_id: cycle.id, order_id: order_id}, + class: 'pagination infinite-scroll' + +javascript: + orders_cycle.order.load() + orders_cycle.order.product.include_message = '#{include_message}' + orders_cycle.order.product.order_id = #{order_id.to_json} + orders_cycle.order.product.redirect_after_include = '#{order.nil? ? 1 : ''}' + orders_cycle.order.product.add_url = '#{url_for controller: :orders_cycle_plugin_item, action: :new}' + orders_cycle.order.product.remove_url = '#{url_for controller: :orders_cycle_plugin_product, action: :remove_from_order}' + orders_cycle.order.product.balloon_url = '#{url_for controller: :orders_cycle_plugin_order, action: :product_balloon}' + orders_cycle.order.product.supplier.balloon_url = '#{url_for controller: :orders_cycle_plugin_order, action: :supplier_balloon}' + pagination.infiniteScroll(#{_('loading...').to_json}, {load: orders_cycle.order.product.showMore}) diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_order/_filter_fields.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_order/_filter_fields.html.slim new file mode 100644 index 0000000..6e598e8 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_order/_filter_fields.html.slim @@ -0,0 +1,23 @@ += hidden_field_tag :cycle_id, cycle.id += hidden_field_tag :order_id, order.id unless order.nil? + +.field.supplier + label= t'orders_cycle_plugin.views.order._filter_products.supplier' + div= select_tag :supplier_id, + options_for_select([[t('orders_cycle_plugin.views.order._filter_products.all_the_suppliers'), ""]] + supplier_choices(cycle.suppliers), params[:supplier_profile_id].to_i) + +.field.category + label= t'suppliers_plugin.views.product.index.category' + div + = select_tag :category_id, options_for_select(@product_categories.map{ |pc| [pc.name, pc.id]}.insert(0,[t('suppliers_plugin.views.product.index.all_the_categories'), ""])) + +/.field.qualifier + div= select_tag :qualifier_id, options_for_select([t('orders_cycle_plugin.views.order._filter_products.anyone'), '']) + +/.field.stock + div= t'orders_cycle_plugin.views.order._filter_products.whose_qty_available_i') + div= select_tag :stock, options_for_select([t('orders_cycle_plugin.views.order._filter_products.bigger_than_the_stock'), '']) + +.field.name + label= t'orders_cycle_plugin.views.order._filter_products.product_name' + div= text_field_tag :name, params[:name] diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_order/_session_edit.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_order/_session_edit.html.erb new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_order/_session_edit.html.erb diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_order/_status.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_order/_status.html.erb new file mode 100644 index 0000000..a172166 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_order/_status.html.erb @@ -0,0 +1,8 @@ +
+
+ <%= t('views.order._status.code_status_message') % { :code => order.code, :status_message => order.status_message } %> +
+ + <%= link_to t('views.order._status.open_it'), {:action => :edit, :id => order.id}, :class => 'open' if order != @order %> + +
diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_order/cycle_edit.rjs b/plugins/orders_cycle/views/orders_cycle_plugin_order/cycle_edit.rjs new file mode 100644 index 0000000..824a49c --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_order/cycle_edit.rjs @@ -0,0 +1,3 @@ +page.replace_html "order-#{@order.id}", :partial => 'cycle_edit', :locals => {:order => @order} +page.replace_html "cycle-products-sums", :partial => 'orders_cycle_plugin_cycle/orders_suppliers', :locals => {:cycle => @order.cycle} +page << "toggle_edit.value_row.reload();" diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_order/edit.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_order/edit.html.erb new file mode 100644 index 0000000..d0230b3 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_order/edit.html.erb @@ -0,0 +1,22 @@ +
+ + <%= render 'orders_cycle_plugin_cycle/view_header', cycle: @cycle %> + + <%= render 'orders_cycle_plugin_cycle/view_products', cycle: @cycle, order: @order %> + +
+ <%= render 'consumer_orders' %> +
+ + <%= javascript_include_tag '/assets/plugins/orders/javascripts/jquery.stickyPanel.js' %> + <%= javascript_tag do %> + jQuery(document).ready(function() { + jQuery('.consumer-order.edit').stickyPanel({ + topPadding: jQuery('#cirandas-top-bar').outerHeight(), + onReAttached: orders.set_orders_container_max_height + }) + }); + <% end %> + +
+ diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_order/edit.js.erb b/plugins/orders_cycle/views/orders_cycle_plugin_order/edit.js.erb new file mode 100644 index 0000000..911be16 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_order/edit.js.erb @@ -0,0 +1,8 @@ +jQuery('#order-<%=@order.id%>').replaceWith("<%= j render('orders_cycle_plugin_order/active_order', :order => @order, :actor_name => @actor_name) %>"); + +orders_cycle.order.product.setEditable(<%=@order.may_edit?(user, @admin).to_json%>) + +<% if notice = session.delete(:notice) %> + display_notice(<%= notice.to_json %>) +<% end %> + diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_order/index.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_order/index.html.slim new file mode 100644 index 0000000..a8f9809 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_order/index.html.slim @@ -0,0 +1,35 @@ +#orders-view.orders.page + h2= t('views.order.index.orders_cycles') + + #years-filter + - @years_with_cycles.each do |year| + = link_to year, {action: :index, year: year}, class: (@year == year ? "current" : nil) + |  + + - if @cycles.blank? + div + = t('views.order.index.there_s_no_open_sessi') + + - @cycles.each do |cycle| + .cycle-with-consumer-orders + h3 + span= t'views.order.index.code', code: cycle.code + = link_to cycle.name, {action: :edit, cycle_id: cycle.id} + + = render 'orders_cycle_plugin_cycle/view_dates', cycle: cycle + + - if cycle.may_order? user + - orders = cycle.sales.not_cancelled.for_consumer @consumer + - if orders.count > 0 + div + strong= t('views.order.index.your_orders') + .consumer-orders + - orders.each do |order| + a href="#{url_for action: :edit, id: order.id}" + = render 'status', order: order + .clean + - if cycle.may_order? user + - if orders.empty? + = link_to t('views.order.index.place_an_order'), {action: :new, cycle_id: cycle.id}, id: 'consumer-new-order' + - else + = link_to t('views.order.index.place_another_order'), {action: :new, cycle_id: cycle.id}, id: 'consumer-new-order' diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_order/product_balloon.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_order/product_balloon.html.slim new file mode 100644 index 0000000..8705013 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_order/product_balloon.html.slim @@ -0,0 +1,6 @@ +#product-ballon.balloon-content.orders-cycle + h3.popover-title= link_to @product.name, @product.url, target: '_blank' + .left-column + = link_to image_tag(@product.default_image(:thumb), class: 'product-pic'), @product.url, target: '_blank' + .right-column + div= @product.description diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_order/supplier_balloon.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_order/supplier_balloon.html.slim new file mode 100644 index 0000000..fc442c0 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_order/supplier_balloon.html.slim @@ -0,0 +1,6 @@ +#supplier-ballon.balloon-content.orders-cycle + h3.popover-title= link_to @supplier.abbreviation_or_name, @supplier.profile.url, target: '_blank' + .left-column + = link_to profile_image(@supplier.profile, :portrait), @supplier.profile.url, target: '_blank' + .right-column + div= @supplier.description diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_product/_cycle_edit.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_product/_cycle_edit.html.slim new file mode 100644 index 0000000..b08a302 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_product/_cycle_edit.html.slim @@ -0,0 +1,36 @@ +.box-view + span.box-field.category= nil + span.box-field.supplier= p.supplier.abbreviation_or_name + span.box-field.name.box-edit-link= p.name + span.box-field.price= price_span p.price_as_currency_number + span.box-field.quantity-available= p.quantity_localized || '∞' + = edit_arrow "", false, onclick: 'return false;', class: "box-field cycle-product-actions" + +.box-edit style=("display: none") + = form_for p, as: :product, remote: true, url: {controller: :orders_cycle_plugin_product, action: :cycle_edit, id: p.id } do |f| + .cycle-product-line + = labelled_field f, :price, t('views.product._cycle_edit.sell_price'), f.number_field(:price, step: 'any', onkeyup: 'orders_cycle.offered_product.pmsync(this, false)', oninput: 'this.onkeyup()') + = labelled_field f, :margin_percentage, t('views.product._cycle_edit.margin'), f.number_field(:margin_percentage, step: 'any', onkeyup: "orders_cycle.offered_product.pmsync(this, true);", oninput: 'this.onkeyup()') + .clean + + .cycle-product-line + = labelled_field f, :original_price, t('views.product._cycle_edit.default_sell_price'), f.number_field(:original_price, step: 'any', disabled: '') + = labelled_field f, :original_margin, t('views.product._cycle_edit.default_margin'), f.number_field(:original_margin_percentage, step: 'any', disabled: '') + .clean + + .cycle-product-line + = labelled_field f, :buy_price, t('views.product._cycle_edit.buy_price'), f.number_field(:buy_price, step: 'any', disabled: '') + = labelled_field f, :buy_unit, t('views.product._cycle_edit.buy_unit'), text_field_tag(:buy_unit, p.buy_unit.singular, disabled: '') + = labelled_field f, :sell_unit, t('views.product._cycle_edit.sell_unit'), text_field_tag(:sell_unit, p.sell_unit.singular, disabled: '') + .clean + + = f.submit t('views.product._cycle_edit.save') + |  + = link_to_function t('views.product._cycle_edit.cancel_updates'), 'toggle-edit' => '' + |  + = link_to t('views.product._cycle_edit.remove_from_cycle'), {controller: :orders_cycle_plugin_product, action: :cycle_destroy, id: p.id }, remote: true, data: {update: "#cycle-product-#{p.id}", confirm: if p.items.blank? then nil else t('views.product._cycle_edit.all_ordered_products') end} + |  + = link_to t('views.product._cycle_edit.edit_product'), {controller: :manage_products, action: :show, id: p.id}, target: '_blank' + |  + .clean +.clean diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_product/_order_edit.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_product/_order_edit.html.slim new file mode 100644 index 0000000..5331562 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_product/_order_edit.html.slim @@ -0,0 +1,17 @@ +.box-view.with-inner + .box-field.select + - options = if editable then {} else {disabled: "disabled"} end + = check_box_tag "product_ids[]", product.id, item.present?, options + + .box-view-inner + .box-field.category title="#{product.category_name}" + = product.category_name + .box-field.supplier + = link_to_function product.supplier.abbreviation_or_name, "orders_cycle.order.product.supplier.balloon(#{product.id})" + .box-field.product + = link_to_function product.name, "orders_cycle.order.product.balloon(#{product.id})" + = price_with_unit_span product.price_as_currency_number, product.unit, product.unit_detail, class: 'box-field' + +javascript: + orders_cycle.order.product.load(#{product.id}, #{item.present?}) + diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_product/_session_destroy.rjs b/plugins/orders_cycle/views/orders_cycle_plugin_product/_session_destroy.rjs new file mode 100644 index 0000000..05cc1a2 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_product/_session_destroy.rjs @@ -0,0 +1 @@ +page.remove 'cycle-product-'+@product_id.to_s diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_product/cycle_destroy.rjs b/plugins/orders_cycle/views/orders_cycle_plugin_product/cycle_destroy.rjs new file mode 100644 index 0000000..f3d2b51 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_product/cycle_destroy.rjs @@ -0,0 +1 @@ +page.remove "cycle-product-#{@product.id}" diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_product/cycle_edit.rjs b/plugins/orders_cycle/views/orders_cycle_plugin_product/cycle_edit.rjs new file mode 100644 index 0000000..945ddae --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_product/cycle_edit.rjs @@ -0,0 +1,2 @@ +page.replace_html "cycle-product-#{@product.id}", :partial => 'cycle_edit', :locals => {:p => @product} +page << "toggle_edit.value_row.click();" diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_product/destroy.rjs b/plugins/orders_cycle/views/orders_cycle_plugin_product/destroy.rjs new file mode 120000 index 0000000..3f4cb0e --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_product/destroy.rjs @@ -0,0 +1 @@ +../../../suppliers/views/suppliers_plugin/product/destroy.rjs \ No newline at end of file diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_product/edit.rjs b/plugins/orders_cycle/views/orders_cycle_plugin_product/edit.rjs new file mode 120000 index 0000000..1399437 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_product/edit.rjs @@ -0,0 +1 @@ +../../../suppliers/views/suppliers_plugin/product/edit.rjs \ No newline at end of file diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_product/index.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_product/index.html.slim new file mode 120000 index 0000000..b537167 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_product/index.html.slim @@ -0,0 +1 @@ +../../../suppliers/views/suppliers_plugin/product/index.html.slim \ No newline at end of file diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_product/remove_from_order.js.erb b/plugins/orders_cycle/views/orders_cycle_plugin_product/remove_from_order.js.erb new file mode 120000 index 0000000..0450782 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_product/remove_from_order.js.erb @@ -0,0 +1 @@ +../orders_cycle_plugin_item/destroy.js.erb \ No newline at end of file diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_shared/_filter.html.slim b/plugins/orders_cycle/views/orders_cycle_plugin_shared/_filter.html.slim new file mode 100644 index 0000000..d3a74b3 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_shared/_filter.html.slim @@ -0,0 +1,19 @@ +- wireframe_size = false unless defined? wireframe_size + +#filter class="#{'wireframe-size' if wireframe_size}" + .title.filter-box + = t'views.filter.filter' + + - url = {controller: :orders_cycle_plugin_order, action: :filter} if type == :order + = form_tag url, remote: true, id: 'filter-form', data: {loading: "#filter", update: '#search-results', type: 'html'}, method: :get do + .fields.filter-box + - if type == :supplier + = render 'suppliers_plugin_myprofile/filter_fields' + - elsif type == :product + = render 'suppliers_plugin/product/filter_fields' + - elsif type == :order + = render 'filter_fields', actor: cycle.suppliers, cycle: cycle, order: order + - else + = render 'filter_fields' + .submit + = submit_tag t('views.filter.filter_it') diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_supplier/margin_change.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_supplier/margin_change.html.erb new file mode 100644 index 0000000..fdf8e38 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_supplier/margin_change.html.erb @@ -0,0 +1,29 @@ +
+

<%= t('views.myprofile.margin_change.change_default_margin') %>

+ + <%= form_for @profile, as: :profile_data, remote: true, url: {controller: :suppliers_plugin_myprofile, action: :margin_change}, + html: {class: 'disable-on-submit'} do |f| %> + +
+ <%= t('views.myprofile.margin_change.notice') %> +
+ + <%= labelled_field f, :margin_percentage, t('views.myprofile.margin_change.new_margin'), f.number_field(:margin_percentage, step: 'any') + ' ' + t('views.myprofile.margin_change.%') %> + +
+ <%= check_box_tag :apply_to_all, 1, false, style: 'float: left' %> + <%= label_tag :apply_to_all, t('views.myprofile.margin_change.apply_to_all'), class: 'line-label' %> +
+ +
+ <%= check_box_tag :apply_to_open_cycles, 1, false, style: 'float: left' %> + <%= label_tag :apply_to_open_cycles, t('views.myprofile.margin_change.apply_to_open_cycles'), class: 'line-label' %> +
+ +
+ + <%= submit_tag t('views.myprofile.margin_change.confirm') %> + <%= modal_close_link t('views.myprofile.margin_change.cancel') %> + <% end %> +
+ diff --git a/plugins/orders_cycle/views/orders_cycle_plugin_volunteers/index.html.erb b/plugins/orders_cycle/views/orders_cycle_plugin_volunteers/index.html.erb new file mode 100644 index 0000000..65ae3b3 --- /dev/null +++ b/plugins/orders_cycle/views/orders_cycle_plugin_volunteers/index.html.erb @@ -0,0 +1,9 @@ +<% profile.orders_cycles.has_volunteers_periods.each do |cycle| %> +

+ <%= cycle.name_with_code %> +

+ + <% cycle.volunteers_periods.each do |period| %> + <%= render 'volunteering', period: period %> + <% end %> +<% end %> diff --git a/plugins/suppliers/Gemfile b/plugins/suppliers/Gemfile new file mode 100644 index 0000000..6dc5d73 --- /dev/null +++ b/plugins/suppliers/Gemfile @@ -0,0 +1,3 @@ +gem 'charlock_holmes', platform: :ruby +gem 'charlock_holmes-jruby', platform: :jruby + diff --git a/plugins/suppliers/controllers/myprofile/suppliers_plugin/basket_controller.rb b/plugins/suppliers/controllers/myprofile/suppliers_plugin/basket_controller.rb new file mode 100644 index 0000000..d403709 --- /dev/null +++ b/plugins/suppliers/controllers/myprofile/suppliers_plugin/basket_controller.rb @@ -0,0 +1,64 @@ +class SuppliersPlugin::BasketController < MyProfileController + + include SuppliersPlugin::TranslationHelper + + no_design_blocks + + protect 'edit_profile', :profile + before_filter :set_allowed_user + + helper SuppliersPlugin::TranslationHelper + helper SuppliersPlugin::DisplayHelper + + def search + @product = profile.products.supplied.find params[:id] + @query = params[:query].to_s + @scope = profile.products.supplied.limit(10) + @scope = @scope.where('id NOT IN (?)', @product.id) + # not a good option as need to search on from_products to, solr is a perfect match + #@products = @scope.where('name ILIKE ? OR name ILIKE ?', "#{@query}%", "% #{@query}%") + @products = autocomplete(:catalog, @scope, @query, {per_page: 10, page: 1}, {})[:results] + + render json: @products.map{ |p| + {value: p.id, label: "#{p.name} (#{if p.respond_to? :supplier then p.supplier.name else p.profile.short_name end})"} + } + end + + def add + @product = profile.products.supplied.find params[:id] + @aggregate = profile.products.supplied.find params[:aggregate_id] + + @sp = @product.sources_from_products.where(from_product_id: @aggregate.id).first + if @sp + @sp.update_column :quantity, @sp.quantity + 1 + else + @sp = @product.sources_from_products.create! from_product: @aggregate, to_product: @product + end + + render partial: 'suppliers_plugin/manage_products/basket_tab' + end + + def remove + @product = profile.products.supplied.find params[:id] + @aggregate = profile.products.supplied.find params[:aggregate_id] + @sp = @product.sources_from_products.where(from_product_id: @aggregate.id).first + @sp.destroy + + render partial: 'suppliers_plugin/manage_products/basket_tab' + end + + protected + + extend HMVC::ClassMethods + hmvc SuppliersPlugin + + def default_url_options + # avoid rails' use_relative_controller! + {use_route: '/'} + end + + def set_allowed_user + @allowed_user = true + end + +end diff --git a/plugins/suppliers/controllers/myprofile/suppliers_plugin/product_controller.rb b/plugins/suppliers/controllers/myprofile/suppliers_plugin/product_controller.rb new file mode 100644 index 0000000..3fef0de --- /dev/null +++ b/plugins/suppliers/controllers/myprofile/suppliers_plugin/product_controller.rb @@ -0,0 +1,103 @@ +class SuppliersPlugin::ProductController < MyProfileController + + include SuppliersPlugin::TranslationHelper + + no_design_blocks + + protect 'edit_profile', :profile + + helper SuppliersPlugin::TranslationHelper + helper SuppliersPlugin::DisplayHelper + + def index + filter + respond_to do |format| + format.html{ render template: 'suppliers_plugin/product/index' } + format.js{ render partial: 'suppliers_plugin/product/search' } + end + end + + def search + filter + if params[:page].present? + render partial: 'suppliers_plugin/product/results' + else + render partial: 'suppliers_plugin/product/search' + end + end + + def add + + end + + def edit + @product = profile.products.supplied.find params[:id] + @product.update_attributes params["product_#{@product.id}"] + end + + def import + if params[:csv].present? + if params[:remove_all_suppliers] == 'true' + profile.suppliers.except_self.find_each(batch_size: 20){ |s| s.delay.destroy } + end + SuppliersPlugin::Import.delay.products profile, params[:csv].read + + @notice = t'controllers.product.import_in_progress' + respond_to{ |format| format.js{ render layout: false } } + else + respond_to{ |format| format.html{ render layout: false } } + end + end + + def destroy + @product = SuppliersPlugin::DistributedProduct.find params[:id] + @product.destroy + flash[:notice] = t('controllers.myprofile.product_controller.product_removed_succe') + end + + def distribute_to_consumers + params[:consumers] ||= {} + + @product = profile.products.find params[:id] + @consumers = profile.consumers.find(params[:consumers].keys.to_a).collect &:profile + to_add = @consumers - @product.consumers + to_remove = @product.consumers - @consumers + + to_add.each{ |c| @product.distribute_to_consumer c } + + to_remove = to_remove.to_set + @product.to_products.each{ |p| p.destroy if to_remove.include? p.profile } + + @product.reload + end + + protected + + def filter + page = params[:page] + page = 1 if page.blank? + + @supplier = SuppliersPlugin::Supplier.where(id: params[:supplier_id]).first if params[:supplier_id].present? + + @scope = profile.products.unarchived.joins :from_products, :suppliers + @scope = SuppliersPlugin::BaseProduct.search_scope @scope, params + @products_count = @scope.supplied_for_count.count + @scope = @scope.supplied.select('products.*, MIN(from_products_products.name) as from_products_name').order('from_products_name ASC') + @products = @scope.paginate per_page: 20, page: page + + @product_categories = Product.product_categories_of @products + @new_product = SuppliersPlugin::DistributedProduct.new + @new_product.profile = profile + @new_product.supplier = @supplier + @units = Unit.all + end + + extend HMVC::ClassMethods + hmvc SuppliersPlugin + + def default_url_options + # avoid rails' use_relative_controller! + {use_route: '/'} + end + +end diff --git a/plugins/suppliers/controllers/myprofile/suppliers_plugin_myprofile_controller.rb b/plugins/suppliers/controllers/myprofile/suppliers_plugin_myprofile_controller.rb new file mode 100644 index 0000000..3266894 --- /dev/null +++ b/plugins/suppliers/controllers/myprofile/suppliers_plugin_myprofile_controller.rb @@ -0,0 +1,82 @@ +class SuppliersPluginMyprofileController < MyProfileController + + include SuppliersPlugin::TranslationHelper + + no_design_blocks + + protect 'edit_profile', :profile + + before_filter :load_new, only: [:index, :new] + + helper SuppliersPlugin::TranslationHelper + helper SuppliersPlugin::DisplayHelper + + def index + @suppliers = search_scope(profile.suppliers.except_self).paginate(per_page: 30, page: params[:page]) + @is_search = params[:name] or params[:active] + + if request.xhr? + render partial: 'suppliers_plugin_myprofile/suppliers_list', locals: {suppliers: @suppliers} + end + end + + def new + @new_supplier.update_attributes! params[:supplier] + @supplier = @new_supplier + session[:notice] = t('controllers.myprofile.supplier_created') + end + + def add + @enterprise = environment.enterprises.find params[:id] + @new_supplier = profile.suppliers.create! profile: @enterprise + end + + def edit + @supplier = profile.suppliers.find params[:id] + @supplier.update_attributes params[:supplier] + end + + def margin_change + if params[:commit] + profile.margin_percentage = params[:profile_data][:margin_percentage] + profile.save + profile.supplier_products_default_margins if params[:apply_to_all] + + render partial: 'suppliers_plugin/shared/pagereload' + end + end + + def toggle_active + @supplier = profile.suppliers.find params[:id] + @supplier.toggle! :active + end + + def destroy + @supplier = profile.suppliers.find params[:id] + @supplier.destroy + end + + def search + @query = params[:query].downcase + @enterprises = environment.enterprises.enabled.public.all limit: 12, order: 'name ASC', + conditions: ['LOWER(name) LIKE ? OR LOWER(name) LIKE ? OR identifier LIKE ?', "#{@query}%", "% #{@query}%", "#{@query}%"] + @enterprises -= profile.suppliers.collect(&:profile) + end + + protected + + def load_new + @new_supplier = SuppliersPlugin::Supplier.new_dummy consumer: profile + @new_profile = @new_supplier.profile + end + + def search_scope scope + scope = scope.by_active params[:active] if params[:active].present? + scope = scope.with_name params[:name] if params[:name].present? + scope + end + + extend HMVC::ClassMethods + hmvc OrdersPlugin + +end diff --git a/plugins/suppliers/db/migrate/20130704000000_create_suppliers_plugin_tables.rb b/plugins/suppliers/db/migrate/20130704000000_create_suppliers_plugin_tables.rb new file mode 100644 index 0000000..ccd3803 --- /dev/null +++ b/plugins/suppliers/db/migrate/20130704000000_create_suppliers_plugin_tables.rb @@ -0,0 +1,21 @@ +class CreateSuppliersPluginTables < ActiveRecord::Migration + def self.up + # check if distribution plugin already moved the table + return if ActiveRecord::Base.connection.table_exists? :suppliers_plugin_suppliers + + create_table :suppliers_plugin_suppliers do |t| + t.integer :profile_id + t.integer :consumer_id + t.string :name + t.string :name_abbreviation + t.text :description + t.timestamps + end + + add_index :suppliers_plugin_suppliers, [:consumer_id] + end + + def self.down + drop_table :suppliers_plugin_suppliers + end +end diff --git a/plugins/suppliers/db/migrate/20130704202336_create_suppliers_plugin_source_product.rb b/plugins/suppliers/db/migrate/20130704202336_create_suppliers_plugin_source_product.rb new file mode 100644 index 0000000..ed1ea69 --- /dev/null +++ b/plugins/suppliers/db/migrate/20130704202336_create_suppliers_plugin_source_product.rb @@ -0,0 +1,22 @@ +class CreateSuppliersPluginSourceProduct < ActiveRecord::Migration + def self.up + # check if distribution plugin already moved the table + return if ActiveRecord::Base.connection.table_exists? "suppliers_plugin_source_products" + + create_table :suppliers_plugin_source_products do |t| + t.integer "from_product_id" + t.integer "to_product_id" + t.integer "supplier_id" + t.decimal "quantity", :default => 0.0 + t.timestamps + end + + add_index :suppliers_plugin_source_products, [:from_product_id] + add_index :suppliers_plugin_source_products, [:to_product_id] + + end + + def self.down + drop_table :suppliers_plugin_source_products + end +end diff --git a/plugins/suppliers/db/migrate/20130902115916_add_active_to_suppliers_plugin_supplier.rb b/plugins/suppliers/db/migrate/20130902115916_add_active_to_suppliers_plugin_supplier.rb new file mode 100644 index 0000000..08af3e1 --- /dev/null +++ b/plugins/suppliers/db/migrate/20130902115916_add_active_to_suppliers_plugin_supplier.rb @@ -0,0 +1,13 @@ +class SuppliersPlugin::Supplier < ActiveRecord::Base +end + +class AddActiveToSuppliersPluginSupplier < ActiveRecord::Migration + def self.up + add_column :suppliers_plugin_suppliers, :active, :boolean, :default => true + SuppliersPlugin::Supplier.update_all ['active = ?', true] + end + + def self.down + remove_column :suppliers_plugin_suppliers, :active + end +end diff --git a/plugins/suppliers/db/migrate/20131001180248_suppliers_plugin_index_filtered_fields.rb b/plugins/suppliers/db/migrate/20131001180248_suppliers_plugin_index_filtered_fields.rb new file mode 100644 index 0000000..d678667 --- /dev/null +++ b/plugins/suppliers/db/migrate/20131001180248_suppliers_plugin_index_filtered_fields.rb @@ -0,0 +1,16 @@ +class SuppliersPluginIndexFilteredFields < ActiveRecord::Migration + def self.up + add_index :suppliers_plugin_suppliers, [:profile_id] + add_index :suppliers_plugin_suppliers, [:profile_id, :consumer_id] + + add_index :suppliers_plugin_source_products, [:from_product_id, :to_product_id], :name => 'suppliers_plugin_index_dtBULzU3' + add_index :suppliers_plugin_source_products, [:supplier_id], :name => 'suppliers_plugin_index_Lm5QPpV8' + add_index :suppliers_plugin_source_products, [:supplier_id, :from_product_id], :name => 'suppliers_plugin_index_naHsVLS6cH' + add_index :suppliers_plugin_source_products, [:supplier_id, :to_product_id], :name => 'suppliers_plugin_index_LgsgYqCQI' + add_index :suppliers_plugin_source_products, [:supplier_id, :from_product_id, :to_product_id], :name => 'suppliers_plugin_index_VBNqyeCP' + end + + def self.down + say "this migration can't be reverted" + end +end diff --git a/plugins/suppliers/db/migrate/20150119124030_suppliers_plugin_update_quantity_of_source_products.rb b/plugins/suppliers/db/migrate/20150119124030_suppliers_plugin_update_quantity_of_source_products.rb new file mode 100644 index 0000000..f3d0aff --- /dev/null +++ b/plugins/suppliers/db/migrate/20150119124030_suppliers_plugin_update_quantity_of_source_products.rb @@ -0,0 +1,11 @@ +class SuppliersPluginUpdateQuantityOfSourceProducts < ActiveRecord::Migration + def up + change_column_default :suppliers_plugin_source_products, :quantity, 1.0 + SuppliersPlugin::SourceProduct.update_all quantity: 1.0 + end + + def down + change_column_default :suppliers_plugin_source_products, :quantity, 0 + SuppliersPlugin::SourceProduct.update_all quantity: 0 + end +end diff --git a/plugins/suppliers/lib/currency_helper.rb b/plugins/suppliers/lib/currency_helper.rb new file mode 120000 index 0000000..a8c13be --- /dev/null +++ b/plugins/suppliers/lib/currency_helper.rb @@ -0,0 +1 @@ +../../orders/lib/currency_helper.rb \ No newline at end of file diff --git a/plugins/suppliers/lib/default_delegate.rb b/plugins/suppliers/lib/default_delegate.rb new file mode 100644 index 0000000..02bdfbe --- /dev/null +++ b/plugins/suppliers/lib/default_delegate.rb @@ -0,0 +1,122 @@ +module DefaultDelegate + + module ClassMethods + + def default_delegate_setting field, options, &block + extend ActsAsHavingSettings::ClassMethods + + prefix = options[:prefix] || :default + default_setting = "#{prefix}_#{field}" + settings_items default_setting, default: options[:default], type: :boolean + + options[:default_setting] = default_setting + default_delegate field, options + end + + # TODO: add some documentation about the methods being added + def default_delegate field, options = {} + # rake db:migrate run? + return unless self.table_exists? + + # Rails doesn't define getters for attributes + define_method field do + self[field] + end if field.to_s.in? self.column_names and not self.method_defined? field + define_method "#{field}=" do |value| + self[field] = value + end if field.to_s.in? self.column_names and not self.method_defined? "#{field}=" + + original_field_method = "original_own_#{field}".freeze + alias_method original_field_method, field + own_field = "own_#{field}".freeze + define_method own_field do + # we prefer the value from dabatase here, and the getter may give a default value + # e.g. Product#name defaults to Product#product_category.name + if field.to_s.in? self.class.column_names then self[field] else self.send original_field_method end + end + alias_method "#{own_field}=", "#{field}=" + + delegated_field = "delegated_#{field}".freeze + to = options[:to].freeze + define_method delegated_field do + case to + when Symbol + object = self.send to + object.send field if object and object.respond_to? field + when Proc then instance_exec &to + end + end + alias_method "original_#{field}", delegated_field + + own_field_blank = "own_#{field}_blank?".freeze + define_method own_field_blank do + own = self.send own_field + # blank? also covers false, use nil? and empty? instead + own.nil? or (own.respond_to? :empty? and own.empty?) + end + own_field_present = "own_#{field}_present?".freeze + define_method own_field_present do + not self.send own_field_blank + end + default_if = options[:default_if].freeze + own_field_is_default = "own_#{field}_default?".freeze + define_method own_field_is_default do + default = self.send own_field_blank + default ||= case default_if + when Proc then instance_exec &default_if + when :equal? + self.send(own_field).equal? self.send(delegated_field) + when Symbol then self.send default_if + else false + end + end + + default_setting = options[:default_setting] || "#{options[:prefix] || :default}_#{field}" + # as a field may use other field's default_setting, check for definition + default_setting_with_presence = "#{default_setting}_with_presence".freeze + unless self.method_defined? default_setting_with_presence + define_method default_setting_with_presence do + original_setting = self.send "#{default_setting}_without_presence" + # if the setting is false, see if it should be true; if it is true, respect it. + original_setting = self.send own_field_is_default unless original_setting + original_setting + end + define_method "#{default_setting_with_presence}=" do |value| + # this ensures latter the getter won't get a different + self.send "#{own_field}=", nil if value + self.send "#{default_setting}_without_presence=", value + end + alias_method_chain default_setting, :presence + alias_method_chain "#{default_setting}=", :presence + end + + define_method "#{field}_with_default" do + if self.send default_setting + # delegated_field may return nil, so use own instead + # this is the case with some associations (e.g. Product#product_qualifiers) + # FIXME: this shouldn't be necessary, it seems to happens only in certain cases + # (product creation, product global search, etc) + self.send(delegated_field) || self.send(own_field) + else self.send(own_field) + end + end + define_method "#{field}_with_default=" do |*args| + own = self.send "#{own_field}=", *args + # break/set the default setting automatically, used for interfaces + # that don't have the default setting (e.g. manage_products) + self.send "#{default_setting}=", self.send(own_field_is_default) + own + end + alias_method_chain field, :default + alias_method_chain "#{field}=", :default + + include DefaultDelegate::InstanceMethods + end + end + + module InstanceMethods + end + +end + +ActiveRecord::Base.extend DefaultDelegate::ClassMethods diff --git a/plugins/suppliers/lib/ext/orders_plugin/item.rb b/plugins/suppliers/lib/ext/orders_plugin/item.rb new file mode 100644 index 0000000..d27dc33 --- /dev/null +++ b/plugins/suppliers/lib/ext/orders_plugin/item.rb @@ -0,0 +1,7 @@ +require_dependency "orders_plugin/item" + +class OrdersPlugin::Item + + delegate :supplier, to: :product + +end diff --git a/plugins/suppliers/lib/ext/price_detail.rb b/plugins/suppliers/lib/ext/price_detail.rb new file mode 100644 index 0000000..8983a8e --- /dev/null +++ b/plugins/suppliers/lib/ext/price_detail.rb @@ -0,0 +1,8 @@ +require_dependency 'price_detail' + +class PriceDetail + + # should be on core, used by SuppliersPlugin::Import + attr_accessible :production_cost + +end diff --git a/plugins/suppliers/lib/ext/product.rb b/plugins/suppliers/lib/ext/product.rb new file mode 100644 index 0000000..4207159 --- /dev/null +++ b/plugins/suppliers/lib/ext/product.rb @@ -0,0 +1,175 @@ +require_dependency 'product' + +# FIXME: The lines bellow should be on the core +class Product + + extend CurrencyHelper::ClassMethods + has_currency :price + has_currency :discount + + scope :available, conditions: {available: true} + scope :unavailable, conditions: ['products.available <> true'] + scope :archived, conditions: {archived: true} + scope :unarchived, conditions: ['products.archived <> true'] + + scope :with_available, lambda { |available| where available: available } + scope :with_price, conditions: 'products.price > 0' + scope :with_product_category_id, lambda { |id| { conditions: {product_category_id: id} } } + + # FIXME: transliterate input and name column + scope :name_like, lambda { |name| { conditions: ["LOWER(products.name) LIKE ?", "%#{name}%"] } } + + scope :by_profile, lambda { |profile| { conditions: {profile_id: profile.id} } } + scope :by_profile_id, lambda { |profile_id| { conditions: {profile_id: profile_id} } } + + def self.product_categories_of products + ProductCategory.find products.collect(&:product_category_id).compact.select{ |id| not id.zero? } + end + + attr_accessible :external_id + settings_items :external_id, type: String, default: nil + + # should be on core, used by SuppliersPlugin::Import + attr_accessible :price_details + +end + +class Product + + attr_accessible :from_products, :supplier_id, :supplier + + has_many :sources_from_products, foreign_key: :to_product_id, class_name: 'SuppliersPlugin::SourceProduct', dependent: :destroy + has_one :sources_from_product, foreign_key: :to_product_id, class_name: 'SuppliersPlugin::SourceProduct' + has_many :sources_to_products, foreign_key: :from_product_id, class_name: 'SuppliersPlugin::SourceProduct', dependent: :destroy + has_one :sources_to_product, foreign_key: :from_product_id, class_name: 'SuppliersPlugin::SourceProduct' + has_many :to_products, through: :sources_to_products, order: 'id ASC' + has_one :to_product, through: :sources_to_product, order: 'id ASC', autosave: true + has_many :from_products, through: :sources_from_products, order: 'id ASC' + has_one :from_product, through: :sources_from_product, order: 'id ASC', autosave: true + + has_many :sources_from_2x_products, through: :from_products, source: :sources_from_products + has_one :sources_from_2x_product, through: :from_product, source: :sources_from_product + has_many :sources_to_2x_products, through: :to_products, source: :sources_to_products + has_one :sources_to_2x_product, through: :to_product, source: :sources_to_product + has_many :from_2x_products, through: :sources_from_2x_products, source: :from_product + has_one :from_2x_product, through: :sources_from_2x_product, source: :from_product + has_many :to_2x_products, through: :sources_to_2x_products, source: :to_product + has_one :to_2x_product, through: :sources_to_2x_product, source: :to_product + + # semantic alias for supplier_from_product(s) + has_many :sources_supplier_products, foreign_key: :to_product_id, class_name: 'SuppliersPlugin::SourceProduct' + has_one :sources_supplier_product, foreign_key: :to_product_id, class_name: 'SuppliersPlugin::SourceProduct' + has_many :supplier_products, through: :sources_supplier_products, source: :from_product, order: 'id ASC' + has_one :supplier_product, through: :sources_supplier_product, source: :from_product, order: 'id ASC', autosave: true + has_many :suppliers, through: :sources_supplier_products, uniq: true, order: 'id ASC' + has_one :supplier, through: :sources_supplier_product, order: 'id ASC' + + has_many :consumers, through: :to_products, source: :profile, uniq: true, order: 'id ASC' + has_one :consumer, through: :to_product, source: :profile, order: 'id ASC' + + # overhide original + scope :available, -> { + joins(:suppliers). + where 'products.available = ? AND suppliers_plugin_suppliers.active = ?', true, true + } + scope :unavailable, -> { + where 'products.available <> ? OR suppliers_plugin_suppliers.active <> ?', true, true + } + scope :with_available, -> (available) { + op = if available then '=' else '<>' end + cond = if available then 'AND' else 'OR' end + where "products.available #{op} ? #{cond} suppliers_plugin_suppliers.active #{op} ?", true, true + } + + scope :name_like, lambda { |name| where "from_products_products.name ILIKE ?", "%#{name}%" } + scope :with_product_category_id, lambda { |id| where 'from_products_products.product_category_id = ?', id } + + # prefer distributed_products has_many to use DistributedProduct scopes and eager loading + scope :distributed, -> { where type: 'SuppliersPlugin::DistributedProduct'} + scope :own, -> { where type: nil } + scope :supplied, -> { + where(type: [nil, 'SuppliersPlugin::DistributedProduct']). + # this allow duplicates and sorting on the fields + group('products.id') + } + scope :supplied_for_count, -> { + where(type: [nil, 'SuppliersPlugin::DistributedProduct']).uniq + } + + scope :from_supplier, lambda { |supplier| { conditions: ['suppliers_plugin_suppliers.id = ?', supplier.id] } } + scope :from_supplier_id, lambda { |supplier_id| { conditions: ['suppliers_plugin_suppliers.id = ?', supplier_id] } } + + after_create :distribute_to_consumers + + def own? + self.class == Product + end + def distributed? + self.class == SuppliersPlugin::DistributedProduct + end + def supplied? + self.own? or self.distributed? + end + + def supplier + # FIXME: use self.suppliers when rails support for nested preload comes + @supplier ||= self.sources_supplier_product.supplier rescue nil + @supplier ||= self.profile.self_supplier rescue nil + end + def supplier= value + @supplier = value + end + def supplier_id + self.supplier.id + end + def supplier_id= id + @supplier = profile.environment.profiles.find id + end + + def supplier_dummy? + self.supplier ? self.supplier.dummy? : self.profile.dummy? + end + + def distribute_to_consumer consumer, attrs = {} + distributed_product = consumer.distributed_products.where(profile_id: consumer.id, from_products_products: {id: self.id}).first + distributed_product ||= SuppliersPlugin::DistributedProduct.create! profile: consumer, from_products: [self] + distributed_product.update_attributes! attrs if attrs.present? + distributed_product + end + + def destroy_dependent + self.to_products.each do |to_product| + to_product.destroy if to_product.respond_to? :dependent? and to_product.dependent? + end + end + + # before_destroy and after_destroy don't work, + # see http://stackoverflow.com/questions/14175330/associations-not-loaded-in-before-destroy-callback + def destroy + self.class.transaction do + self.destroy_dependent + super + end + end + + def diff from = self.from_product + return @changed_attrs if @changed_attrs + @changed_attrs = [] + SuppliersPlugin::BaseProduct::CORE_DEFAULT_ATTRIBUTES.each do |attr| + @changed_attrs << attr if self[attr].present? and self[attr] != from[attr] + end + @changed_attrs + end + + protected + + def distribute_to_consumers + # shopping_cart creates products without a profile... + return unless self.profile + + self.profile.consumers.except_people.except_self.each do |consumer| + self.distribute_to_consumer consumer.profile + end + end + +end diff --git a/plugins/suppliers/lib/ext/profile.rb b/plugins/suppliers/lib/ext/profile.rb new file mode 100644 index 0000000..be25627 --- /dev/null +++ b/plugins/suppliers/lib/ext/profile.rb @@ -0,0 +1,89 @@ +require_dependency 'profile' + +# FIXME: The lines bellow should be on the core +class Profile + + has_many :products + + def create_product? + true + end + +end + +class Profile + + # use profile.products.supplied to include own products + has_many :distributed_products, class_name: 'SuppliersPlugin::DistributedProduct' + + has_many :from_products, through: :products + has_many :to_products, through: :products + + has_many :suppliers, class_name: 'SuppliersPlugin::Supplier', foreign_key: :consumer_id, dependent: :destroy, + include: [{profile: [:domains], consumer: [:domains]}], order: 'name ASC' + has_many :consumers, class_name: 'SuppliersPlugin::Consumer', foreign_key: :profile_id, dependent: :destroy, + include: [{profile: [:domains], consumer: [:domains]}], order: 'name ASC' + + def supplier_settings + @supplier_settings ||= Noosfero::Plugin::Settings.new self, SuppliersPlugin + end + + def dummy? + !self.visible + end + + def orgs_consumers + @orgs_consumers ||= self.consumers.except_people.except_self + end + + def self_supplier + @self_supplier ||= if new_record? + self.suppliers_without_self_supplier.build profile: self + else + suppliers_without_self_supplier.select{ |s| s.profile_id == s.consumer_id }.first || self.suppliers_without_self_supplier.create(profile: self) + end + end + def suppliers_with_self_supplier + self_supplier # guarantee that the self_supplier is created + suppliers_without_self_supplier + end + alias_method_chain :suppliers, :self_supplier + + def add_consumer consumer_profile + consumer = self.consumers.where(consumer_id: consumer_profile.id).first + consumer ||= self.consumers.create! profile: self, consumer: consumer_profile + end + def remove_consumer consumer_profile + consumer = self.consumers.of_consumer(consumer_profile).first + consumer.destroy if supplier + end + + def add_supplier supplier_profile, attrs={} + supplier = self.suppliers.where(profile_id: supplier_profile.id).first + supplier ||= self.suppliers.create! attrs.merge(profile: supplier_profile, consumer: self) + end + def remove_supplier supplier_profile + supplier_profile.remove_consumer self + end + + def not_distributed_products supplier + raise "'#{supplier.name}' is not a supplier of #{self.name}" if self.suppliers.of_profile(supplier).blank? + + # FIXME: only select all products if supplier is dummy + supplier.profile.products.unarchived.own - self.from_products.unarchived.by_profile(supplier.profile) + end + + delegate :margin_percentage, :margin_percentage=, to: :supplier_settings + extend CurrencyHelper::ClassMethods + has_number_with_locale :margin_percentage + + def supplier_products_default_margins + self.class.transaction do + self.distributed_products.unarchived.each do |product| + product.default_margin_percentage = true + product.save! + end + end + end + +end diff --git a/plugins/suppliers/lib/suppliers_plugin.rb b/plugins/suppliers/lib/suppliers_plugin.rb new file mode 100644 index 0000000..6c4ba45 --- /dev/null +++ b/plugins/suppliers/lib/suppliers_plugin.rb @@ -0,0 +1,14 @@ +module SuppliersPlugin + + extend Noosfero::Plugin::ParentMethods + + def self.plugin_name + I18n.t'suppliers_plugin.lib.plugin.name' + end + + def self.plugin_description + I18n.t'suppliers_plugin.lib.plugin.description' + end + +end + diff --git a/plugins/suppliers/lib/suppliers_plugin/base.rb b/plugins/suppliers/lib/suppliers_plugin/base.rb new file mode 100644 index 0000000..b55a012 --- /dev/null +++ b/plugins/suppliers/lib/suppliers_plugin/base.rb @@ -0,0 +1,70 @@ +if defined? OrdersPlugin + require_dependency "#{File.dirname __FILE__}/../ext/orders_plugin/item" +end + +class SuppliersPlugin::Base < Noosfero::Plugin + + def stylesheet? + true + end + + def js_files + ['locale', 'toggle_edit', 'sortable-table', 'suppliers'].map{ |j| "javascripts/#{j}" } + end + + ProductTabs = { + distribution: { + id: 'product-distribution', + content: lambda do + render 'suppliers_plugin/manage_products/distribution_tab' + end + }, + compare: { + id: 'product-compare-origin', + content: lambda do + render 'suppliers_plugin/manage_products/compare_tab' + end + }, + basket: { + id: 'product-basket', + content: lambda do + render 'suppliers_plugin/manage_products/basket_tab' + end + }, + } + + def product_tabs product + allowed_user = context.instance_variable_get :@allowed_user + + tabs = ProductTabs.dup + tabs.delete :distribution unless allowed_user and product.profile.orgs_consumers.present? + tabs.delete :compare unless allowed_user and product.from_products.size == 1 + # for now, only support basket as a product of the profile + tabs.delete :basket unless product.own? and (allowed_user or product.from_products.size > 1) + tabs.each{ |t, op| op[:title] = I18n.t "suppliers_plugin.lib.plugin.#{t}_tab" } + tabs.values + end + + def control_panel_buttons + # FIXME: disable for now + return + + profile = context.profile + return unless profile.enterprise? + [ + {title: I18n.t('suppliers_plugin.views.control_panel.suppliers'), icon: 'suppliers-manage-suppliers', url: {controller: :suppliers_plugin_myprofile, action: :index}}, + {title: I18n.t('suppliers_plugin.views.control_panel.products'), icon: 'suppliers-manage-suppliers', url: {controller: 'suppliers_plugin/product', action: :index}}, + ] + end + +end + +ActiveSupport.on_load :solr_product do + ::Product.class_eval do + def solr_supplied + self.supplied? + end + self.solr_extra_fields << :solr_supplied + end +end + diff --git a/plugins/suppliers/lib/suppliers_plugin/display_helper.rb b/plugins/suppliers/lib/suppliers_plugin/display_helper.rb new file mode 100644 index 0000000..4bcbcf5 --- /dev/null +++ b/plugins/suppliers/lib/suppliers_plugin/display_helper.rb @@ -0,0 +1,13 @@ +module SuppliersPlugin::DisplayHelper + + protected + + include SearchHelper + + include SuppliersPlugin::TableHelper + include SuppliersPlugin::FieldHelper + include SuppliersPlugin::ProductHelper + include SuppliersPlugin::ImageHelper + include SuppliersPlugin::JavascriptHelper + +end diff --git a/plugins/suppliers/lib/suppliers_plugin/field_helper.rb b/plugins/suppliers/lib/suppliers_plugin/field_helper.rb new file mode 100644 index 0000000..b095be8 --- /dev/null +++ b/plugins/suppliers/lib/suppliers_plugin/field_helper.rb @@ -0,0 +1,16 @@ +module SuppliersPlugin::FieldHelper + + protected + + def labelled_field form, field, label, field_html, options = {} + help = options.delete(:help) + + field_label = (form ? form.label(field, label) : label_tag(field, label)) + field_help = help.blank? ? '' : content_tag('div', help, class: 'field-help') + field_box = content_tag('div', field_html, class: 'field-box') + + content_tag('div', field_label + field_help + field_box, + options.merge(class: options[:class].to_s + ' field')) + end + +end diff --git a/plugins/suppliers/lib/suppliers_plugin/image_helper.rb b/plugins/suppliers/lib/suppliers_plugin/image_helper.rb new file mode 100644 index 0000000..055e8fd --- /dev/null +++ b/plugins/suppliers/lib/suppliers_plugin/image_helper.rb @@ -0,0 +1,12 @@ +module SuppliersPlugin::ImageHelper + + # image that can be aligned, centered, and resized with aspect ratio + def profile_link_with_image profile, size=:portrait, options={} + options[:class] = "#{options[:class] || ''} inner" + options[:style] = "#{options[:style] || ''}; background-image: url(#{profile_icon profile, size})" + + link = link_to '', profile.url, options + content_tag 'div', link, :class => "profile-image #{size}" + end + +end diff --git a/plugins/suppliers/lib/suppliers_plugin/import.rb b/plugins/suppliers/lib/suppliers_plugin/import.rb new file mode 100644 index 0000000..0b57ee8 --- /dev/null +++ b/plugins/suppliers/lib/suppliers_plugin/import.rb @@ -0,0 +1,138 @@ +require 'csv' +require 'charlock_holmes' + +class SuppliersPlugin::Import + + def self.product_columns header + keys = I18n.t'suppliers_plugin.lib.import.keys' + columns = [] + header.each do |name| + c = nil; keys.each do |key, regex| + if /#{regex}/i =~ name + c = key + break + end + end + raise "duplicate column match '#{name}' already added as :#{c}" if c and c.in? columns + columns << c + end + # check required fields + return if ([:supplier_name, :product_name, :price] - columns).present? + columns + end + + def self.products consumer, csv + default_product_category = consumer.environment.product_categories.find_by_name 'Produtos' + + detection = CharlockHolmes::EncodingDetector.detect csv + csv = CharlockHolmes::Converter.convert csv, detection[:encoding], 'UTF-8' + data = {} + rows = [] + columns = [] + quote_chars = %w[" | ~ ^ & *] + [",", ";", "\t"].each do |sep| + begin + rows = CSV.parse csv, quote_char: quote_chars.shift, col_sep: sep + columns = self.product_columns rows.first + rescue + if quote_chars.empty? then raise else retry end + ensure + break if columns.present? + end + end + rows.shift + raise "can't find required columns" if columns.blank? + + # extract and treat attributes + rows.each do |row| + attrs = {}; row.each.with_index do |value, i| + next unless c = columns[i] + value = value.to_s.squish + attrs[c] = if value.present? then value else nil end + end + + distributed = attrs[:distributed] = {} + distributed[:external_id] = attrs.delete :external_id + if supplier_price = attrs.delete(:supplier_price) + distributed[:price] = attrs[:price] + attrs[:price] = supplier_price + end + + attrs[:name] = attrs.delete :product_name + if product_category = attrs[:product_category] + attrs[:product_category] = ProductCategory.find_by_solr(product_category, query_fields: ['name']).first + end + attrs[:product_category] ||= default_product_category + if qualifiers = attrs[:qualifiers] + qualifiers = JSON.parse qualifiers + qualifiers.map! do |q| + next if q.blank? + Qualifier.find_by_solr(q, query_fields: ['name']).first + end.compact! + attrs[:qualifiers] = qualifiers + end + attrs[:unit] = consumer.environment.units.where(singular: attrs[:unit]).first || SuppliersPlugin::BaseProduct.default_unit + # FIXME + attrs.delete :stock + + if composition = attrs.delete(:composition) + composition = JSON.parse composition rescue nil + distributed[:price_details] = composition.map do |name, price| + production_cost = consumer.environment.production_costs.where(name: name).first + production_cost ||= consumer.production_costs.where(name: name).first + production_cost ||= consumer.production_costs.create! name: name, owner: profile + PriceDetail.new production_cost: production_cost, price: price + end + end + + # treat URLs + profile = nil + if product_url = attrs.delete(:product_url) and /manage_products\/show\/(\d+)/ =~ product_url + product = Product.where(id: $1).first + next if product.blank? + attrs[:record] = product + profile = product.profile + end + if supplier_url = attrs.delete(:supplier_url) + uri = URI.parse supplier_url + profile = Domain.where(name: uri.host).first.profile rescue nil + profile ||= Profile.where(identifier: Rails.application.routes.recognize_path(uri.path)[:profile]).first + next if profile.blank? + end + supplier_name = attrs.delete :supplier_name + supplier = profile || supplier_name + + data[supplier] ||= [] + data[supplier] << attrs + end + + data.each do |supplier, products| + if supplier.is_a? Profile + supplier = consumer.add_supplier supplier, distribute_products_on_create: false + else + supplier_name = supplier + supplier = consumer.suppliers.where(name: supplier_name).first + supplier ||= SuppliersPlugin::Supplier.create_dummy consumer: consumer, name: supplier_name + end + + products.each do |attrs| + distributed_attrs = attrs.delete :distributed + + product = attrs.delete :record + product ||= supplier.profile.products.where(name: attrs[:name]).first + product ||= supplier.profile.products.build attrs + # let update happen only on dummy suppliers + if product.persisted? and supplier.dummy? + product.update_attributes! attrs + elsif product.new_record? + # create products as not available + attrs[:available] = false if not supplier.dummy? + product.update_attributes! attrs + end + + distributed_product = product.distribute_to_consumer consumer, distributed_attrs + end + end + end + +end diff --git a/plugins/suppliers/lib/suppliers_plugin/javascript_helper.rb b/plugins/suppliers/lib/suppliers_plugin/javascript_helper.rb new file mode 100644 index 0000000..ac19ff5 --- /dev/null +++ b/plugins/suppliers/lib/suppliers_plugin/javascript_helper.rb @@ -0,0 +1,7 @@ +module SuppliersPlugin::JavascriptHelper + + def j *args + escape_javascript *args + end + +end diff --git a/plugins/suppliers/lib/suppliers_plugin/product_helper.rb b/plugins/suppliers/lib/suppliers_plugin/product_helper.rb new file mode 100644 index 0000000..adfba75 --- /dev/null +++ b/plugins/suppliers/lib/suppliers_plugin/product_helper.rb @@ -0,0 +1,11 @@ +module SuppliersPlugin::ProductHelper + + protected + + def supplier_choices suppliers + @supplier_choices ||= suppliers.map do |s| + [s.abbreviation_or_name, s.id] + end.sort{ |a,b| a[0].downcase <=> b[0].downcase } + end + +end diff --git a/plugins/suppliers/lib/suppliers_plugin/table_helper.rb b/plugins/suppliers/lib/suppliers_plugin/table_helper.rb new file mode 100644 index 0000000..ae4ea33 --- /dev/null +++ b/plugins/suppliers/lib/suppliers_plugin/table_helper.rb @@ -0,0 +1,7 @@ +module SuppliersPlugin::TableHelper + + protected + + include ToggleEdit::TableHelper + +end diff --git a/plugins/suppliers/lib/suppliers_plugin/translation_helper.rb b/plugins/suppliers/lib/suppliers_plugin/translation_helper.rb new file mode 100644 index 0000000..38302ea --- /dev/null +++ b/plugins/suppliers/lib/suppliers_plugin/translation_helper.rb @@ -0,0 +1,12 @@ +module SuppliersPlugin::TranslationHelper + + protected + + # included here to be used on controller's t calls + include TermsHelper + + def i18n_scope + ['suppliers_plugin'] + end + +end diff --git a/plugins/suppliers/lib/toggle_edit b/plugins/suppliers/lib/toggle_edit new file mode 120000 index 0000000..e068523 --- /dev/null +++ b/plugins/suppliers/lib/toggle_edit @@ -0,0 +1 @@ +../../orders/lib/toggle_edit \ No newline at end of file diff --git a/plugins/suppliers/locales/en-US.yml b/plugins/suppliers/locales/en-US.yml new file mode 100644 index 0000000..e73ffaa --- /dev/null +++ b/plugins/suppliers/locales/en-US.yml @@ -0,0 +1,491 @@ +en-US: &en-US + + suppliers_plugin: + lib: + plugin: + name: "Suppliers" + description: "Suppliers and its distributed products registry" + distribution_tab: "Distribuição" + compare_tab: "Compare with the supplier" + basket_tab: "Cesta" + import: + keys: + # ordered by most specific + supplier_url: '(supplier|producer).+url' + product_url: 'product.+url' + supplier_price: '(supplier|producer).+price' + supplier: '(supplier|producer)' + product: 'product' + qualifiers: 'qualifier' + product_category: 'category' + price: 'price' + unit: 'unit' + composition: 'composition' + external_id: 'external' + stock: 'stock' + + terms: &suppliers_terms + profile: &suppliers_term_profile + singular: "organization" + plural: "organizations" + it: + singular: "it" + plural: "them" + article: + singular: "the organization" + plural: "the organizations" + undefined_article: + singular: "a organization" + by_article: + singular: "by the organization" + tO_it: + singular: "to it" + plural: "to them" + to_article: + singular: "to the organization" + plural: "to the organizations" + at_article: + singular: "at the organization" + from: + singular: "from an organization" + from_article: + singular: "from the organization" + from_which_article: + singular: "from which the organização" + of: + singular: "of organization" + plural: "of organizations" + of_article: + singular: "of the organization" + plural: "of the organizations" + of_this: + singular: "of this organization" + of_another: + singular: "of another organization" + this: + singular: "this organization" + with_which: + singular: "with which the organization" + consumer: + singular: "consumer" + plural: "consumers" + to_article: + singular: "to the consumer" + plural: "to the consumers" + from_article: + singular: "from consumer" + plural: "from consumers" + supplier: + singular: "supplier" + plural: "suppliers" + one: "one" + it: + singular: "it" + plural: "them" + article: + singular: "the supplier" + plural: "the suppliers" + undefined_article: + singular: "a supplier" + by_article: + singular: "by the supplier" + new_undefined_article: + singular: "a new supplier" + to: + singular: "to a supplier" + plural: "to suppliers" + tO_it: + singular: "to it" + plural: "to them" + to_article: + singular: "to the supplier" + plural: "to the suppliers" + none: + singular: "any supplier" + this: + singular: "this supplier" + from: + singular: "from supplier" + from_article: + singular: "from the supplier" + from_which_article: + singular: "from which the supplier" + of: + singular: "of supplier" + plural: "of suppliers" + of_article: + singular: "of the supplier" + plural: "of the suppliers" + of_this: + singular: "of this supplier" + of_another: + singular: "of another supplier" + your: + singular: "your supplier" + plural: "your suppliers" + on_your: + singular: "on your organization" + on_article: + singular: "on your organization" + on_undefined_article: + singular: "on an organization" + controllers: + myprofile: + supplier_created: "%{terms.supplier.singular.capitalize} created" + product: + import_in_progress: "The products are being imported. Wait a while and reload the page to view them." + + models: + base_product: + default_attributes: + name: 'Name' + description: 'Description' + price: 'Price' + unit_id: 'Unit' + product_category_id: 'Category' + image_id: 'Image' + distributed_product: + greater: " > " + product: + unit: unit + units: units + profile: + consumer: Consumer + views: + filter: + filter: Filter + filter_it: Filter + actions: + + manage_products: + compare_tab: + see_supplier: 'See original product at the page of the supplier "%{supplier}".' + see: 'See original product.' + no_diff: 'Este produto não tem nenhuma diferença com o que está na página do fornecedor".' + diff_count: "This product has %{n} differences compared to the same product on the supplier's page:" + distribution_tab: + title: "This product is distributed for the following consumers:" + no_consumers: "This profile isn't a suppliers of the network." + save: "Save" + basket_tab: + make_a_basket: 'Make this product a basket by adding another product!' + search: 'find and add your products' + component: 'Component' + quantity: 'Quantity' + + control_panel: + suppliers: "Manage suppliers" + products: "Manage distributed products" + myprofile: + margin_change: + ? "%" + : "%" + notice: "By changing the default margin of commercialization, the products that are \"use the default margin\" marked will automatically have their margin updated." + cancel: cancel + change_default_margin: "Change default margin of commercialization" + confirm: Confirm + apply_to_all: "Apply to the products with custom margins" + apply_to_open_cycles: "Apply to the products in open cycles" + new_margin: "New margin" + index: + redistribution_situation: "Product distribution" + name: "Name" + + product: + import: + action: "Import spreadsheet" + help: "Format you spreadsheet to have exactly the fields bellow. Then, save it choosing the CSV file type (.csv extension).
Products with equal names have their data (price/unit) updated." + field_supplier_name: "Supplier's name" + field_product_name: "Product's name" + field_product_unit: "Unit" + field_product_price: "Price per unit" + remove_all: 'Also remove all registered suppliers and their products (start a new list of products).' + confirm_remove_all: "CAUTION! This option will remove all registered suppliers and their products!" + unit_help: "Use as unit one of the following (put the name equal): %{units}" + send: "Send" + _edit: + distribution_setts: "Product's distribution settings" + margin_price: "Margin / price" + default_margin: "Use default" + stock: "Stock configuration" + default_stock: "Keep in stock" + current_stock: "Current stock" + distribution_state: "Distribution state" + available: Active + are_you_sure_you_want: "Are you sure you want to delete this product?" + remove_product: remove + cancel: cancel + save: save changes + view_on_supplier_page: "See product %{terms.supplier.at_article.singular}'s page" + product_registry: "Product's registry" + selling_unit: "Selling unit" + same_from_purchase: "Same from purchase" + unit: "Unit" + unit_detail: "Unit detail" + _edit_line: + active: Active + inactive: Inactive + _form_errors: + errors_found: "Errors found:" + _price_details: + commercialization_act: "Commercialization active" + minimum_order: "Minimum order" + price: Price + unit: Unit + unit_specification: "Unit specification" + _search: + status: Status + supplier: "%{terms.supplier.singular.capitalize}" + category: Category + product: Product + margin: Margin + price: Price + stock: Stock + unit: Unit + showing_pcount_produc: "Total: %{count} products" + we_don_t_have_product: "We don't have products to show." + add: + title: "Create product" + own_product: "Create own product" + or_from_a_supplier: 'or from a managed supplier:' + index: + active: active + add_product: "Create product" + all_the_categories: "all the categories" + all_the_suppliers: "all %{terms.supplier.article.plural}" + situation: "distribution situation" + anyone: anyone + bigger_than_the_stock: "bigger than the stock" + change: change + default_margin_of_com: "Default margin of commercialization" + default_margin_info: "This margin is automatically applied in all the products, but it is possible to stabilish specific margins for certain products." + in_any_state: "In any state" + inactive: inactive + margin: "%{margin} %" + no_margin_set: "no margin set" + products: Products + supplier: "%{terms.supplier.singular.capitalize}" + info: "This is list of all the products %{terms.profile.with_which.singular} distribute. The products are divided between active and inactive." + the_products: "The products" + name: "Nome" + whose_qty_available_i: "whose qty. available is" + category: "Category" + with_the_qualifiers: "with the qualifiers" + supplier: + _new: + title: "Add %{terms.supplier.singular} to %{profile}" + intro_help: "Begin to type the name %{terms.supplier.from_article.singular} that you wnat to add. We'll look if %{terms.supplier.article.singular} already exists at %{environment}. In case it doesn't exists, you'll be able to createa type of enterprise that will be visible ONLY %{terms.profile.on_your.singular}, and not at the %{environment}. If %{terms.supplier.this.singular} really wants to join the %{environment}, then it should send a message to %{contact_email} making the order." + query_placeholder: "find %{terms.supplier.undefined_article.singular}" + search: "Search" + none_found: "%{terms.supplier.none.singular} found. Please revise the search text." + add: "Add" + suggest_creation: "If you don't find %{terms.supplier.article.singular} you can create %{terms.supplier.new_undefined_article.singular} that will be managed %{terms.profile.by_article.singular}. But please find carefully to avoid duplicates!" + create_dummy: "Create %{terms.supplier.singular}" + _edit: + registry_help: "The registry %{terms.supplier.from_article.singular} will be managed by the %{terms.profile.by_article.singular} itself and will be private %{terms.profile.to_it.singular}." + abbreviated_name: "Abbreviated name" + add_supplier_managed_: "Add %{terms.supplier.singular} managed %{terms.profile.by_article.singular}" + additional_fields: "Additional fields" + basic_data: "Basic data" + cancel: cancel + description: Description + edit_supplier: "Edit %{terms.supplier.singular}" + estrategic_informatio: "Estrategic informations" + full_registration: "Full registration" + name: Name + register_new_supplier: "Register %{terms.supplier.new_undefined_article.singular}" + registry_form: "Registry form" + save: Save + _actions: + all: "all" + none: "none" + select: "select" + with_selection: "with selected" + activate: "activate" + deactivate: "deactivate" + _supplier: + edit: "Edit" + see_page: "See page" + see_products: "See products" + message: "Message" + abreviated_name: "Abreviated Name" + actions: Actions + among_them: ", among them" + any_registered_produc: "any registered product" + by_removing_this_supp: "By removing %{terms.supplier.article.singular} all its products won't be available for future cycles. Please confirm you choice" + deactivate: "hide %{terms.profile.from_article.singular}" + activate: activate + edit: edit + products_are_active: " produtos estão ativos" + products_distributed: " distributed products %{terms.profile.by_article.singular}" + products: + zero: "no products" + one: "1 product" + other: "%{count} products" + manage_products: "manage products" + add_product: "add product" + registered_supplier_i: "Registered %{terms.supplier.singular} in the network" + disassociate: "disassociate %{terms.profile.from_article.singular}" + supplier_profile_product: " registered products" + supplier_with_registr: "%{terms.supplier.singular.capitalize} with registry managed by the %{terms.profile.by_article.singular}" + this_supplier_has: "This %{terms.supplier.singular} has" + _suppliers_list: + this_search_didn_t_re: "This search didn't return %{terms.supplier.plural}." + any_registered: "%{terms.supplier.none.singular.capitalize} registered." + index: + add_supplier: "Add %{terms.supplier.singular}" + filter: Filter + filter_it: Filter + name: Name + suppliers: "%{terms.supplier.plural.capitalize}" + this_is_the_list_of_s: "This is the list of %{terms.supplier.plural} associated %{terms.profile.to_article.singular}. It is possible to associate with existent %{terms.supplier.plural} or register %{terms.supplier.one}." + + orders_cycle_plugin: + terms: + <<: *suppliers_terms + + consumers_coop_plugin: + terms: + <<: *suppliers_terms + profile: &consumers_coop_term_profile + singular: "consumers' coop" + plural: "consumers' coop" + it: + singular: "it" + plural: "them" + article: + singular: "the consumers' coop" + plural: "the consumers' coops" + undefined_article: + singular: "a consumers' coop" + by_article: + singular: "by the consumers' coop" + tO_it: + singular: "to it" + plural: "to them" + to_article: + singular: "to the consumers' coop" + at_article: + singular: "at the consumers' coop" + from: + singular: "from a consumers' coop" + from_article: + singular: "from the consumers' coop" + from_which_article: + singular: "from which the consumers' coop" + of: + singular: "of consumers' coop" + plural: "of consumers' coops" + of_article: + singular: "of the consumers' coop" + plural: "of the consumers' coops" + of_this: + singular: "of this consumers' coop" + of_another: + singular: "of another consumers' coop" + on_your: + singular: "on your consumers' coop" + this: + singular: "this consumers' coop" + with_which: + singular: "with which the consumers' coop" + + networks_plugin: + terms: + <<: *suppliers_terms + profile: &networks_term_profile + singular: "network" + plural: "networks" + it: + singular: "it" + plural: "them" + article: + singular: "the network" + plural: "the networks" + undefined_article: + singular: "a network" + by_article: + singular: "by the network" + tO_it: + singular: "to it" + plural: "to them" + to_article: + singular: "to the network" + at_article: + singular: "at the network" + from: + singular: "from a network" + from_article: + singular: "from the network" + from_which_article: + singular: "from which the rede" + of: + singular: "of the network" + plural: "of the networks" + of_article: + singular: "of the network" + plural: "of the networks" + of_this: + singular: "of this network" + of_another: + singular: "of another network" + this: + singular: "this network" + on_your: + singular: "on your network" + with_which: + singular: "with which the network" + supplier: + singular: "enterprise" + plural: "enterprises" + one: "one" + it: + singular: "it" + plural: "them" + article: + singular: "the enterprise" + plural: "the enterprises" + undefined_article: + singular: "a enterprise" + by_article: + singular: "by the enterprise" + new_undefined_article: + singular: "a new enterprise" + to: + singular: "to a enterprise" + plural: "to enterprises" + tO_it: + singular: "to it" + plural: "to them" + to_article: + singular: "to the enterprise" + plural: "to the enterprises" + your: + singular: "your enterprise" + plural: "your enterprises" + from: + singular: "from enterprise" + from_article: + singular: "from the enterprise" + from_which_article: + singular: "from which the enterprise" + of: + singular: "of enterprise" + plural: "of enterprises" + of_article: + singular: "of the enterprise" + plural: "of the enterprises" + of_this: + singular: "of this enterprise" + of_another: + singular: "of another enterprise" + none: + singular: "any enterprise" + this: + singular: "this enterprise" + +en_US: + <<: *en-US +en: + <<: *en-US + diff --git a/plugins/suppliers/locales/pt-BR.yml b/plugins/suppliers/locales/pt-BR.yml new file mode 100644 index 0000000..52a61cc --- /dev/null +++ b/plugins/suppliers/locales/pt-BR.yml @@ -0,0 +1,536 @@ +pt-BR: &pt-BR + + suppliers_plugin: + lib: + plugin: + name: "Fornecedores" + description: "Registro de fornecedores e produtos distribuídos" + distribution_tab: "Distribuição" + compare_tab: "Compare com o fornecedor" + basket_tab: "Cesta" + import: + keys: + # ordered by most specific + supplier_url: 'url.+(fornecedor|produtor)' + product_url: 'url.+produto' + supplier_price: 'pre[[:alpha:]]o.+(fornecedor|produtor)' + supplier_name: '(fornecedor|produtor)' + product_name: 'produto' + qualifiers: 'qualificador' + product_category: 'categoria' + price: 'pre[[:alpha:]]o' + unit: 'unidade' + composition: 'composi[[:alpha:]]{2,2}o' + external_id: 'external' + stock: 'estoque' + + terms: &suppliers_terms + profile: &suppliers_term_profile + singular: "organização" + plural: "organizações" + it: + singular: "ela" + plural: "elas" + article: + singular: "a organização" + plural: "as organizações" + undefined_article: + singular: "uma organização" + by_article: + singular: "pela organização" + tO_it: + singular: "a ela" + plural: "a elas" + to_article: + singular: "a organização" + plural: "às organizações" + at_article: + singular: "na organização" + from: + singular: "de uma organização" + from_article: + singular: "da organização" + from_which_article: + singular: "com que a organização" + of: + singular: "de organização" + plural: "de organizações" + of_article: + singular: "da organização" + plural: "das organizações" + of_this: + singular: "desta organização" + of_another: + singular: "de outra organização" + with: + singular: "com organização" + plural: "com organizações" + with_article: + singular: "com a organização" + plural: "com as organizações" + your: + singular: "seu fornecedor" + plural: "seus fornecedores" + on_your: + singular: "na sua organização" + on_undefined_article: + singular: "em uma organização" + this: + singular: "esta organização" + consumer: + singular: "consumidor(a)" + plural: "consumidores(as)" + to_article: + singular: "ao(à) consumidor(a)" + plural: "aos(às) consumidores(as)" + from_article: + singular: "do(a) consumidor(a)" + plural: "dos(as) consumidores(as)" + supplier: + singular: "fornecedor" + plural: "fornecedores" + one: "um" + it: + singular: "ele" + plural: "eles" + to: + singular: "a um fornecedor" + plural: "a fornecedores" + to_it: + singular: "a ele" + plural: "a eles" + to_article: + singular: "ao fornecedor" + plural: "aos fornecedores" + undefined_article: + singular: "um fornecedor" + none: + singular: "nenhum fornecedor" + this: + singular: "este fornecedor" + from: + singular: "de fornecedor" + from_article: + singular: "do fornecedor" + from_which_article: + singular: "com que o fornecedor" + of: + singular: "de fornecedor" + plural: "de fornecedores" + of_article: + singular: "do fornecedor" + plural: "dos fornecedores" + of_this: + singular: "deste fornecedor" + of_another: + singular: "de outro fornecedor" + with: + singular: "com fornecedor" + plural: "com fornecedores" + with_article: + singular: "com o fornecedor" + plural: "com o fornecedores" + article: + singular: "o fornecedor" + plural: "os fornecedores" + by_article: + singular: "pelo fornecedor" + new_undefined_article: + singular: "um novo fornecedor" + controllers: + myprofile: + supplier_created: "%{terms.supplier.singular.capitalize} criado" + product: + import_in_progress: "Os produtos estão sendo importados. Aguarde um pouco e recarregue a página para visualizá-los." + + models: + base_product: + default_attributes: + name: 'Nome' + description: 'Descrição' + price: 'Preço' + unit_id: 'Unidade' + product_category_id: 'Categoria' + image_id: 'Imagem' + distributed_product: + greater: " > " + product: + unit: unidade + units: unidades + profile: + consumer: Consumidor(a) + + views: + filter: + filter: Filtro + filter_it: Filtrar + actions: + + manage_products: + compare_tab: + see_supplier: 'Ver produto original na página do fornecedor "%{supplier}".' + see: 'Ver produto original.' + no_diff: 'Este produto não tem nenhuma diferença com o que está na página do fornecedor.' + diff_count: 'Este produto está com %{n} diferenças com relação ao mesmo produto na página do fornecedor:' + distribution_tab: + title: "Esse produto é distribuído para os seguintes consumidores(as):" + save: "Salvar" + basket_tab: + make_a_basket: 'Faça deste produto uma cesta adicionando mais um produto!' + search: 'encontre e adicione seus produtos' + component: 'Componente' + quantity: 'Quantidade' + + control_panel: + suppliers: "Gerenciar fornecedores" + products: "Gerenciar produtos distribuídos" + + myprofile: + margin_change: + ? "%" + : "%" + notice: "Ao mudar a margem padrão de comercialização, os produtos que são marcados como \"use a margem padrão\" automaticamente terão sua margem atualizada." + cancel: Cancelar + change_default_margin: "Mudar a margem padrão de comercialização" + confirm: Confirmar + apply_to_all: "Aplicar para os produtos com margens personalizadas" + apply_to_open_cycles: "Aplicar para os produtos nos ciclos abertos" + new_margin: "Nova margem" + index: + redistribution_situation: "Distribuição dos produtos" + name: "Nome" + + product: + import: + action: "Importar planilha" + help: "Formate sua planilha para que tenha exatamente os campos abaixo. Depois, salve-a escolhendo o tipo de arquivo CSV (extensão .csv).
Produtos com nomes iguais tem seus dados (preço/unidade) atualizados." + field_supplier_name: "Nome do fornecedor" + field_product_name: "Nome do produto" + field_product_unit: "Unidade" + field_product_price: "Preço por unidade" + remove_all: 'Remover todos os atuais fornecedores cadastrados e todos os seus produtos (começar uma nova lista de produtos).' + confirm_remove_all: "ATENÇÃO! Essa opção removerá todos os fornecedores cadastrados e todos os seus produtos!" + unit_help: "Use como unidade uma entre (coloque o nome igual): %{units}" + send: "Enviar" + _edit: + distribution_setts: "Configurações de distribuição do produto" + margin_price: "Margem / preço" + default_margin: "Usar padrão" + stock: "Configuração de estoque" + default_stock: "Manter estoque" + current_stock: "Estoque atual" + distribution_state: "Estado da distribuição" + available: Ativa + are_you_sure_you_want: "Tem certeza de que deseja remover este produto?" + remove_product: remover + cancel: cancelar + save: salvar mudanças + view_on_supplier_page: "Ver produto na página %{terms.supplier.from_article.singular}" + product_registry: "Cadastro do produto" + selling_unit: "Unidade de venda" + same_from_purchase: "Mesma da compra" + unit: "Unidade" + unit_detail: "Especificação da unidade" + _edit_line: + active: Ativo + inactive: Inativo + _form_errors: + errors_found: "Erros encontrados:" + _price_details: + commercialization_act: "Comercialização ativa" + minimum_order: "Pedido mínimo" + price: Preço + unit: Unidade + unit_specification: "Especificações de unidade" + _search: + status: Estado + category: Categoria + supplier: "%{terms.supplier.singular.capitalize}" + product: Produto + margin: Margem + price: Preço + stock: Estoque + unit: Unidade + showing_pcount_produc: "Total: %{count} produtos" + we_don_t_have_product: "Não temos produtos a mostrar." + add: + title: "Criar produto" + own_product: "Criar produto próprio" + or_from_a_supplier: 'ou de um fornecedor gerido pelo coletivo:' + index: + active: ativa + add_product: "Criar produto" + all_the_categories: "todas as categorias" + all_the_suppliers: "todos %{terms.supplier.article.plural}" + situation: "Situação de distribuição" + anyone: anyone + bigger_than_the_stock: "maior do que o estoque" + change: mudar + default_margin_of_com: "Margem padrão de comercialização" + default_margin_info: "Esta margem é automaticamente aplicada em todos os produtos, mas é possível estabelecer margens específicas para certos produtos." + in_any_state: "Em qualquer estado" + inactive: inativa + margin: "%{margin} %" + no_margin_set: "Sem margem definida" + products: Produtos + supplier: "%{terms.supplier.singular.capitalize}" + info: "Esta é a lista de produtos %{terms.profile.from_which_article.singular} distribuí. Os produtos se dividem entre ativos e inativos." + the_products: "Os produtos" + name: "Nome" + whose_qty_available_i: "cuja qtd disponível é" + category: "Categoria" + with_the_qualifiers: "com os qualificadores" + + supplier: + _new: + title: "Adicionar %{terms.supplier.singular} a %{profile}" + intro_help: "Comece a digitar o nome %{terms.supplier.from_article.singular} que deseja adicionar. Procuraremos para ver se %{terms.supplier.article.singular} já existe no %{environment}. Caso ele não exista, você poderá criar um tipo de empreendimento que ficará visível APENAS %{terms.profile.on_your.singular}, e não no %{environment} como um todo. Se %{terms.supplier.this.singular} quiser entrar realmente no %{environment}, deve enviar uma mensagem para %{contact_email} fazendo o pedido." + query_placeholder: "busque %{terms.supplier.undefined_article.singular}" + search: "Buscar" + none_found: "%{terms.supplier.none.singular} foi encontrado. Por favor revise o texto da busca." + add: "Adicionar" + suggest_creation: "Se você não encontrar %{terms.supplier.article.singular}, pode criar %{terms.supplier.new_undefined_article.singular} que será gerenciado %{terms.profile.by_article.singular}. Mas procure bem para evitar duplicatas!" + create_dummy: "Criar %{terms.supplier.singular}" + _edit: + registry_help: "O registro %{terms.supplier.from_article.singular} será administrado %{terms.profile.by_article.singular} e será privado %{terms.profile.to_it.singular}." + abbreviated_name: "Nome Abreviado" + add_supplier_managed_: "Adicionar %{terms.supplier.singular} com registro administrado %{terms.profile.by_article.singular}" + additional_fields: "Campos adicionais" + basic_data: "Informações básicas" + cancel: Cancelar + description: Descrição + edit_supplier: "Editar %{terms.supplier.singular}" + estrategic_informatio: "Informações estratégicas" + full_registration: "Registro completo" + name: Nome + register_new_supplier: "Registrar %{terms.supplier.new_undefined_article.singular}" + registry_form: "Formulário de registro" + save: Salvar + _actions: + all: "todos" + none: "nenhum" + select: "selecionar" + with_selection: "com selecionados" + activate: "activar" + deactivate: "desativar" + _supplier: + edit: "Editar" + cancel: "Cancelar" + see_page: "Ver página" + see_products: "Ver produtos" + message: "Messagem" + abreviated_name: "Nome Abreviado" + actions: Ações + among_them: ", entre eles" + any_registered_produc: "nenhum produto registrado" + by_removing_this_supp: "Ao remover %{terms.supplier.article.singular}, todos os produtos ficarão indisponíveis em futuros ciclos. Por favor, confirme sua opção" + indirect: "indireta" + direct: "direta" + active: "ativa" + inactive: "inativa" + deactivate: "desativar" + activate: ativar + edit: editar + contact_status: "contato %{status}" + contact: "contato" + qualifiers: qualificadores + tags: "Marcadores" + minidescription: "Mini descrição" + cycles: "Ciclos abertos" + register: "Ficha cadastral" + geolocalization: "Geolocalização" + strategic_info: "Informações Estratégicas" + register: "Ficha cadastral" + distribution_status: "redistribuição %{status}" + management_type: "gerência %{type}" + collective: coletivo + active_products: "produtos %{count} ativos" + situation: "Situação geral" + products: + zero: "nenhum produto" + one: "1 produto" + other: "%{count} produtos" + manage_products: "gerenciar produtos" + add_product: "adicionar produto" + registered_supplier_i: "%{terms.supplier.singular.capitalize} registrado na rede" + disassociate: "disassociar" + supplier_profile_product: "produtos registrados" + supplier_with_registr: "%{terms.supplier.singular.capitalize} com registro administrado %{terms.profile.by_article.singular}" + this_supplier_has: "Este %{terms.supplier.singular} tem" + _suppliers_list: + this_search_didn_t_re: "Esta busca não retornou %{terms.supplier.plural}." + any_registered: "%{terms.supplier.none.singular.capitalize} registrado." + index: + add_supplier: "Adicionar %{terms.supplier.singular}" + filter: Filtro + name: Nome + suppliers: "%{terms.supplier.plural.capitalize}" + this_is_the_list_of_s: "Esta é a lista de %{terms.supplier.plural} associados %{terms.profile.to_article.singular}. É possível associar %{terms.supplier.to.plural} existentes ou registrar %{terms.supplier.one}." + + orders_cycle_plugin: + terms: + <<: *suppliers_terms + + consumers_coop_plugin: + terms: + <<: *suppliers_terms + profile: &consumers_coop_term_profile + singular: "coletivo de consumo" + plural: "coletivos de consumo" + it: + singular: "ele" + plural: "eles" + article: + singular: "o coletivo de consumo" + plural: "os coletivos de consumo" + undefined_article: + singular: "um coletivo de consumo" + by_article: + singular: "pelo coletivo de consumo" + to_it: + singular: "a ele" + plural: "a eles" + to_article: + singular: "ao coletivo de consumo" + at_article: + singular: "no coletivo de consumo" + from: + singular: "de um coletivo de consumo" + from_article: + singular: "do coletivo de consumo" + from_which_article: + singular: "com que o coletivo de consumo" + of: + singular: "de coletivo de consumo" + plural: "de coletivos de consumo" + of_article: + singular: "do coletivo de consumo" + plural: "dos coletivos de consumo" + of_this: + singular: "deste coletivo de consumo" + of_another: + singular: "de outro coletivo de consumo" + with: + singular: "com coletivo de consumo" + plural: "com coletivos de consumo" + with_article: + singular: "com o coletivo de consumo" + plural: "com os coletivos de consumo" + on_your: + singular: "em seu coletivo de consumo" + this: + singular: "este coletivo de consumo" + + networks_plugin: + terms: + <<: *suppliers_terms + profile: &networks_term_profile + singular: "rede" + plural: "redes" + it: + singular: "ela" + plural: "elas" + to_it: + singular: "a ela" + plural: "a elas" + article: + singular: "a rede" + plural: "as redes" + undefined_article: + singular: "uma rede" + to_article: + singular: "a rede" + at_article: + singular: "na rede" + from: + singular: "de uma rede" + from_article: + singular: "da rede" + from_which_article: + singular: "com que a rede" + of: + singular: "de rede" + plural: "de redes" + of_article: + singular: "da rede" + plural: "das redes" + of_this: + singular: "desta rede" + of_another: + singular: "de outra rede" + with: + singular: "com rede" + plural: "com redes" + with_article: + singular: "com a rede" + plural: "com a organizações" + this: + singular: "esta rede" + on_your: + singular: "na sua rede" + supplier: + singular: "empreendimento" + plural: "empreendimentos" + one: "um" + it: + singular: "ele" + plural: "eles" + to: + singular: "a um empreendimento" + plural: "a empreendimentos" + to_it: + singular: "a ele" + plural: "a eles" + to_article: + singular: "ao empreendimento" + plural: "aos empreendimentos" + article: + singular: "o empreendimento" + plural: "os empreendimentos" + by_article: + singular: "pelo empreendimento" + undefined_article: + singular: "um empreendimento" + new_undefined_article: + singular: "um novo empreendimento" + to_it: + singular: "a ele" + plural: "a eles" + to_article: + singular: "ao empreendimento" + plural: "aos empreendimentos" + none: + singular: "nenhum empreendimento" + this: + singular: "este empreendimento" + your: + singular: "seu empreendimento" + plural: "seus empreendimentos" + from: + singular: "de empreendimento" + from_article: + singular: "do empreendimento" + from_which_article: + singular: "com que o empreendimento" + of: + singular: "de empreendimento" + plural: "de empreendimentos" + of_article: + singular: "do empreendimento" + plural: "dos empreendimentos" + of_this: + singular: "deste empreendimento" + of_another: + singular: "de outro empreendimento" + with: + singular: "com empreendimento" + plural: "com empreendimentos" + with_article: + singular: "com o empreendimento" + plural: "com os empreendimentos" + +pt_BR: + <<: *pt-BR +pt: + <<: *pt-BR + diff --git a/plugins/suppliers/models/suppliers_plugin/base_product.rb b/plugins/suppliers/models/suppliers_plugin/base_product.rb new file mode 100644 index 0000000..842b76d --- /dev/null +++ b/plugins/suppliers/models/suppliers_plugin/base_product.rb @@ -0,0 +1,192 @@ +# for some unknown reason, if this is named SuppliersPlugin::Product then +# cycle.products will go to an infinite loop +class SuppliersPlugin::BaseProduct < Product + + attr_accessible :default_margin_percentage, :margin_percentage, :default_unit, :unit_detail, + :supplier_product_attributes + + accepts_nested_attributes_for :supplier_product + + default_scope include: [ + # from_products is required for products.available + :from_products, + # FIXME: move use cases to a scope called 'includes_for_links' + { + suppliers: [{ profile: [:domains, {environment: :domains}] }] + }, + { + profile: [:domains, {environment: :domains}] + } + ] + + self.abstract_class = true + + settings_items :minimum_selleable, type: Float, default: nil + settings_items :margin_percentage, type: Float, default: nil + settings_items :quantity, type: Float, default: nil + settings_items :unit_detail, type: String, default: nil + + CORE_DEFAULT_ATTRIBUTES = [ + :name, :description, :price, :unit_id, :product_category_id, :image_id, + ] + DEFAULT_ATTRIBUTES = CORE_DEFAULT_ATTRIBUTES + [ + :margin_percentage, :stored, :minimum_selleable, :unit_detail, + ] + + extend DefaultDelegate::ClassMethods + default_delegate_setting :name, to: :supplier_product + default_delegate_setting :description, to: :supplier_product + + default_delegate_setting :qualifiers, to: :supplier_product + default_delegate :product_qualifiers, default_setting: :default_qualifiers, to: :supplier_product + + default_delegate_setting :product_category, to: :supplier_product + default_delegate :product_category_id, default_setting: :default_product_category, to: :supplier_product + + default_delegate_setting :image, to: :supplier_product, prefix: :_default + default_delegate :image_id, default_setting: :_default_image, to: :supplier_product + + default_delegate_setting :unit, to: :supplier_product + default_delegate :unit_id, default_setting: :default_unit, to: :supplier_product + + default_delegate_setting :margin_percentage, to: :profile, + default_if: -> { self.own_margin_percentage.blank? or self.own_margin_percentage.zero? } + default_delegate :price, default_setting: :default_margin_percentage, default_if: :equal?, + to: -> { self.supplier_product.price_with_discount if self.supplier_product } + + default_delegate :unit_detail, default_setting: :default_unit, to: :supplier_product + default_delegate_setting :minimum_selleable, to: :supplier_product + + extend CurrencyHelper::ClassMethods + has_currency :own_price + has_currency :original_price + has_number_with_locale :minimum_selleable + has_number_with_locale :own_minimum_selleable + has_number_with_locale :original_minimum_selleable + has_number_with_locale :quantity + has_number_with_locale :margin_percentage + has_number_with_locale :own_margin_percentage + has_number_with_locale :original_margin_percentage + + def self.default_product_category environment + ProductCategory.top_level_for(environment).order('name ASC').first + end + def self.default_unit + Unit.new(singular: I18n.t('suppliers_plugin.models.product.unit'), plural: I18n.t('suppliers_plugin.models.product.units')) + end + + def self.search_scope scope, params + scope = scope.from_supplier_id params[:supplier_id] if params[:supplier_id].present? + scope = scope.with_available(if params[:available] == 'true' then true else false end) if params[:available].present? + scope = scope.name_like params[:name] if params[:name].present? + scope = scope.with_product_category_id params[:category_id] if params[:category_id].present? + scope + end + + def self.orphans_ids + # FIXME: need references from rails4 to do it without raw query + result = self.connection.execute <<-SQL +SELECT products.id FROM products +LEFT OUTER JOIN suppliers_plugin_source_products ON suppliers_plugin_source_products.to_product_id = products.id +LEFT OUTER JOIN products from_products_products ON from_products_products.id = suppliers_plugin_source_products.from_product_id +WHERE products.type IN (#{(self.descendants << self).map{ |d| "'#{d}'" }.join(',')}) +GROUP BY products.id HAVING count(from_products_products.id) = 0; +SQL + result.values + end + + def self.archive_orphans + self.where(id: self.orphans_ids).find_each batch_size: 50 do |product| + # need full save to trigger search index + product.update_attributes archived: true + end + end + + def buy_price + self.supplier_products.inject(0){ |sum, p| sum += p.price || 0 } + end + def buy_unit + #TODO: handle multiple products + (self.supplier_product.unit rescue nil) || self.class.default_unit + end + + def available + self[:available] + end + + def available_with_supplier + return self.available_without_supplier unless self.supplier + self.available_without_supplier and self.supplier.active rescue false + end + def chained_available + return self.available_without_supplier unless self.supplier_product + self.available_without_supplier and self.supplier_product.available and self.supplier.active rescue false + end + alias_method_chain :available, :supplier + + def dependent? + self.from_products.length >= 1 + end + def orphan? + !self.dependent? + end + + def minimum_selleable + self[:minimum_selleable] || 0.1 + end + + def price_with_margins base_price = nil, margin_source = nil + margin_source ||= self + margin_percentage = margin_source.margin_percentage + margin_percentage ||= self.profile.margin_percentage if self.profile + + base_price ||= 0 + price = if margin_percentage and not base_price.zero? + base_price.to_f + (margin_percentage.to_f / 100) * base_price.to_f + else + self.price_with_default + end + + price + end + + def price_without_margins + self[:price] / (1 + self.margin_percentage/100) + end + + # FIXME: move to core + # just in case the from_products is nil + def product_category_with_default + self.product_category_without_default or self.class.default_product_category(self.environment) + end + def product_category_id_with_default + self.product_category_id_without_default or self.product_category_with_default.id + end + alias_method_chain :product_category, :default + alias_method_chain :product_category_id, :default + + # FIXME: move to core + def unit_with_default + self.unit_without_default or self.class.default_unit + end + alias_method_chain :unit, :default + + # FIXME: move to core + def archive + self.update_attributes! archived: true + end + def unarchive + self.update_attributes! archived: false + end + + protected + + def validate_uniqueness_of_column_name? + false + end + + # overhide Product's after_create callback to avoid infinite loop + def distribute_to_consumers + end + +end diff --git a/plugins/suppliers/models/suppliers_plugin/consumer.rb b/plugins/suppliers/models/suppliers_plugin/consumer.rb new file mode 100644 index 0000000..92e4125 --- /dev/null +++ b/plugins/suppliers/models/suppliers_plugin/consumer.rb @@ -0,0 +1,13 @@ +class SuppliersPlugin::Consumer < SuppliersPlugin::Supplier + + self.table_name = :suppliers_plugin_suppliers + + belongs_to :profile, foreign_key: :consumer_id + belongs_to :supplier, foreign_key: :profile_id + alias_method :consumer, :profile + + def name + self.profile.name + end + +end diff --git a/plugins/suppliers/models/suppliers_plugin/distributed_product.rb b/plugins/suppliers/models/suppliers_plugin/distributed_product.rb new file mode 100644 index 0000000..afca18c --- /dev/null +++ b/plugins/suppliers/models/suppliers_plugin/distributed_product.rb @@ -0,0 +1,34 @@ +class SuppliersPlugin::DistributedProduct < SuppliersPlugin::BaseProduct + + attr_accessible :from_products + + # missed from lib/ext/product.rb because of STI + attr_accessible :external_id, :price_details + + validates_presence_of :supplier + + def supplier_price + self.supplier_product.price if self.supplier_product + end + + # Automatic set/get price chaging/applying margins + # FIXME: this won't work if we have other params, like fixed margin, delivery cost, etc + def price + base_price = self.supplier_price + return super if base_price.blank? + + self.price_with_margins base_price + end + def price= value + return super if value.blank? + value = value.to_f + base_price = self.supplier_price + return super if base_price.blank? + + self.margin_percentage = 100 * (value - base_price) / base_price + super + end + + protected + +end diff --git a/plugins/suppliers/models/suppliers_plugin/source_product.rb b/plugins/suppliers/models/suppliers_plugin/source_product.rb new file mode 100644 index 0000000..be90f1a --- /dev/null +++ b/plugins/suppliers/models/suppliers_plugin/source_product.rb @@ -0,0 +1,31 @@ +class SuppliersPlugin::SourceProduct < ActiveRecord::Base + + attr_accessible :from_product, :to_product, :quantity + + default_scope include: [:from_product, :to_product] + + belongs_to :from_product, class_name: 'Product' + belongs_to :to_product, class_name: 'Product' + belongs_to :supplier, class_name: 'SuppliersPlugin::Supplier' + + has_many :sources_from_products, through: :from_product + has_many :sources_to_products, through: :to_product + + has_one :supplier_profile, through: :supplier, source: :profile + + before_validation :find_supplier + + validates_presence_of :from_product + validates_presence_of :to_product + validates_presence_of :supplier + validates_numericality_of :quantity, allow_nil: true + + protected + + def find_supplier + self.supplier = SuppliersPlugin::Supplier.where(profile_id: self.from_product.profile_id, consumer_id: self.to_product.profile_id).first + raise "Can't find supplier" unless self.supplier + self.supplier + end + +end diff --git a/plugins/suppliers/models/suppliers_plugin/supplier.rb b/plugins/suppliers/models/suppliers_plugin/supplier.rb new file mode 100644 index 0000000..a601012 --- /dev/null +++ b/plugins/suppliers/models/suppliers_plugin/supplier.rb @@ -0,0 +1,158 @@ +class SuppliersPlugin::Supplier < ActiveRecord::Base + + attr_accessor :distribute_products_on_create, :dont_destroy_dummy, :identifier_from_name + + attr_accessible :profile_id, :profile, :consumer, :consumer_id, :name, :name_abbreviation, :description + + belongs_to :profile + belongs_to :consumer, class_name: 'Profile' + alias_method :supplier, :profile + + validates_presence_of :name, if: :dummy? + validates_associated :profile, if: :dummy? + validates_presence_of :profile + validates_presence_of :consumer + validates_uniqueness_of :consumer_id, scope: :profile_id, if: :profile_id + + scope :alphabetical, -> { order 'name ASC' } + + scope :active, conditions: {active: true} + scope :dummy, -> { joins(:profile).where profiles: {visible: false} } + + scope :of_profile, lambda { |p| { conditions: {profile_id: p.id} } } + scope :of_profile_id, lambda { |id| { conditions: {profile_id: id} } } + scope :of_consumer, lambda { |c| { conditions: {consumer_id: c.id} } } + scope :of_consumer_id, lambda { |id| { conditions: {consumer_id: id} } } + + scope :from_supplier_id, lambda { |supplier_id| { conditions: ['suppliers_plugin_suppliers.id = ?', supplier_id] } } + + scope :with_name, lambda { |name| if name then {conditions: ["LOWER(suppliers_plugin_suppliers.name) LIKE ?","%#{name.downcase}%"]} else {} end } + scope :by_active, lambda { |active| if active then {conditions: {active: active}} else {} end } + + scope :except_people, { conditions: ['profiles.type <> ?', Person.name], joins: [:consumer] } + scope :except_self, { conditions: 'profile_id <> consumer_id' } + + after_create :add_admins, if: :dummy? + after_create :save_profile, if: :dummy? + after_create :distribute_products_to_consumer + before_validation :fill_identifier, if: :dummy? + before_destroy :destroy_consumer_products + + def self.new_dummy attributes + environment = attributes[:consumer].environment + profile = environment.enterprises.build + profile.enabled = false + profile.visible = false + profile.public_profile = false + + supplier = self.new + supplier.profile = profile + supplier.consumer = attributes.delete :consumer + supplier.attributes = attributes + supplier + end + def self.create_dummy attributes + s = new_dummy attributes + s.save! + s + end + + def self? + self.profile_id == self.consumer_id + end + def person? + self.consumer.person? + end + def dummy? + !self.supplier.visible rescue false + end + def active? + self.active + end + + def name + self.attributes['name'] || self.profile.name + end + def name= value + self['name'] = value + self.supplier.name = value if self.dummy? and not self.supplier.frozen? + end + def description + self.attributes['description'] || self.profile.description + end + def description= value + self['description'] = value + self.supplier.description = value if self.dummy? and not self.supplier.frozen? + end + + def abbreviation_or_name + return self.profile.nickname || self.name if self.self? + self.name_abbreviation.blank? ? self.name : self.name_abbreviation + end + + def destroy_with_dummy + if not self.self? and not self.dont_destroy_dummy and self.supplier and self.supplier.dummy? + self.supplier.destroy + end + self.destroy_without_dummy + end + alias_method_chain :destroy, :dummy + + protected + + def set_identifier + if self.identifier_from_name + identifier = self.profile.identifier = self.profile.name.to_slug + i = 0 + self.profile.identifier = "#{identifier}#{i += 1}" while Profile[self.profile.identifier].present? + else + self.profile.identifier = Digest::MD5.hexdigest rand.to_s + end + end + + def fill_identifier + return if self.profile.identifier.present? + set_identifier + end + + def add_admins + self.consumer.admins.to_a.each{ |a| self.supplier.add_admin a } + end + + # sync name, description, etc + def save_profile + self.supplier.save + end + + def distribute_products_to_consumer + self.distribute_products_on_create = true if self.distribute_products_on_create.nil? + return if self.self? or self.consumer.person? or not self.distribute_products_on_create + + already_supplied = self.consumer.distributed_products.unarchived.from_supplier_id(self.id).all + + self.profile.products.unarchived.each do |source_product| + next if already_supplied.find{ |f| f.supplier_product == source_product } + + source_product.distribute_to_consumer self.consumer + end + end + handle_asynchronously :distribute_products_to_consumer + + def destroy_consumer_products + self.consumer.products.joins(:suppliers).from_supplier(self).destroy_all + end + + # delegate missing methods to profile + def method_missing method, *args, &block + if self.profile.respond_to? method + self.profile.send method, *args, &block + else + super method, *args, &block + end + end + def respond_to_with_profile? method, include_private=false + respond_to_without_profile? method, include_private or Profile.new.respond_to? method, include_private + end + alias_method_chain :respond_to?, :profile + +end diff --git a/plugins/suppliers/public/images/control-panel/manage-suppliers.jpg b/plugins/suppliers/public/images/control-panel/manage-suppliers.jpg new file mode 100644 index 0000000..71695cb Binary files /dev/null and b/plugins/suppliers/public/images/control-panel/manage-suppliers.jpg differ diff --git a/plugins/suppliers/public/images/control-panel/manage-suppliers.png b/plugins/suppliers/public/images/control-panel/manage-suppliers.png new file mode 100644 index 0000000..ef51bb7 Binary files /dev/null and b/plugins/suppliers/public/images/control-panel/manage-suppliers.png differ diff --git a/plugins/suppliers/public/images/minus.png b/plugins/suppliers/public/images/minus.png new file mode 120000 index 0000000..5e9b801 --- /dev/null +++ b/plugins/suppliers/public/images/minus.png @@ -0,0 +1 @@ +../../../orders/public/images/minus.png \ No newline at end of file diff --git a/plugins/suppliers/public/images/plus.png b/plugins/suppliers/public/images/plus.png new file mode 120000 index 0000000..6db118c --- /dev/null +++ b/plugins/suppliers/public/images/plus.png @@ -0,0 +1 @@ +../../../orders/public/images/plus.png \ No newline at end of file diff --git a/plugins/suppliers/public/javascripts/locale.js b/plugins/suppliers/public/javascripts/locale.js new file mode 120000 index 0000000..19d5817 --- /dev/null +++ b/plugins/suppliers/public/javascripts/locale.js @@ -0,0 +1 @@ +../../../orders/public/javascripts/locale.js \ No newline at end of file diff --git a/plugins/suppliers/public/javascripts/sortable-table.js b/plugins/suppliers/public/javascripts/sortable-table.js new file mode 120000 index 0000000..4c3a4d0 --- /dev/null +++ b/plugins/suppliers/public/javascripts/sortable-table.js @@ -0,0 +1 @@ +../../../orders/public/javascripts/sortable-table.js \ No newline at end of file diff --git a/plugins/suppliers/public/javascripts/suppliers.js b/plugins/suppliers/public/javascripts/suppliers.js new file mode 100644 index 0000000..f286a19 --- /dev/null +++ b/plugins/suppliers/public/javascripts/suppliers.js @@ -0,0 +1,194 @@ + +suppliers = { + + filter: { + form: function() { + return table_filter.form() + }, + + apply: function() { + this.form().submit() + }, + + }, + + add_link: function () { + if (toggle_edit.isEditing()) + toggle_edit.value_row.toggle_edit(); + + var supplier_add = $('#supplier-add'); + toggle_edit.setEditing(supplier_add); + toggle_edit.value_row.toggle_edit(); + }, + + toggle_edit: function () { + if (toggle_edit.editing().is('#supplier-add')) + toggle_edit.editing().toggle(toggle_edit.isEditing()); + toggle_edit.editing().find('.box-view').toggle(!toggle_edit.isEditing()); + toggle_edit.editing().find('.box-edit').toggle(toggle_edit.isEditing()); + }, + + add: { + supplier_added: function () { + $('#results').html(''); + }, + create_dummy: function () { + $('#find-enterprise, #create-dummy, #create-dummy .box-edit').toggle(); + }, + + search: function (input) { + query = $(input).val(); + if (query.length < 3) + return; + input.form.onsubmit(); + }, + }, + + // core uses (future product plugin) + product: { + + pmsync: function (to_price) { + var margin_input = $('#product_margin_percentage') + var price_input = $('#product_price') + var base_price_input = $('#product_supplier_product_attributes_price') + + if (to_price) + suppliers.price.calculate(price_input, margin_input, base_price_input) + else + suppliers.margin.calculate(margin_input, price_input, base_price_input) + }, + + updateBasePrice: function () { + this.pmsync(this) + }, + }, + + our_product: { + + toggle_edit: function () { + toggle_edit.editing().find('.box-edit').toggle(toggle_edit.isEditing()) + }, + + default_change: function (event) { + var block = $(this).parents('.block') + var nonDefaults = block.find('div[data-non-defaults]') + nonDefaults.toggle(!this.checked) + nonDefaults.find('input,select,textarea').prop('disabled', this.checked) + }, + + load: function(id) { + $('#our-product-'+id+' div[data-default-toggle] input').change(suppliers.our_product.default_change).change(); + }, + + pmsync: function (context, to_price) { + var p = $(context).parents('.our-product') + var margin_input = p.find('.product-margin-percentage') + var price_input = p.find('.product-price') + var buy_price_input = p.find('.product-base-price') + + if (to_price) + suppliers.price.calculate(price_input, margin_input, buy_price_input) + else + suppliers.margin.calculate(margin_input, price_input, buy_price_input) + }, + + select: { + all: function() { + $('.our-product #product_ids_').attr('checked', true) + }, + none: function() { + $('.our-product #product_ids_').attr('checked', false) + }, + + activate: function(state) { + var selection = $('.our-product #product_ids_:checked').parents('.our-product') + selection.find('.available input[type=checkbox]').each(function() { + this.checked = state + $(this.form).submit() + }); + }, + + }, + + import: { + confirmRemoveAll: function(checkbox, text) { + if (checkbox.checked && !confirm(text)) + checkbox.checked = false + }, + }, + }, + + basket: { + searchUrl: null, + addUrl: null, + removeUrl: null, + + remove: function (id) { + var self = this + $.ajax(self.removeUrl, {method: 'DELETE', + data: {aggregate_id: id}, + success: function(data) { + self.reload(data) + }, + }) + }, + + reload: function (data) { + $('#product-basket').html(data) + $('#basket-add').focus() + }, + + load: function () { + var self = this + var input = $('#basket-add') + + self.source = new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: this.searchUrl+'?query=%QUERY', + }) + self.source.initialize() + + input.typeahead({ + minLength: 2, highlight: true, + }, { + displayKey: 'label', + source: this.source.ttAdapter(), + }).on('typeahead:selected', function(e, item) { + input.val('') + $.post(self.addUrl, {aggregate_id: item.value}, function(data) { + self.reload(data) + }) + }) + }, + }, + + price: { + + calculate: function (price_input, margin_input, base_price_input) { + var price = unlocalize_currency($(price_input).val()); + var base_price = unlocalize_currency($(base_price_input).val()); + var margin = unlocalize_currency($(margin_input).val()); + + var value = base_price + (margin / 100) * base_price; + if (isNaN(value)) + value = unlocalize_currency(base_price_input.val()); + $(price_input).val(value); + }, + }, + + margin: { + + calculate: function (margin_input, price_input, base_price_input) { + var price = unlocalize_currency($(price_input).val()); + var base_price = unlocalize_currency($(base_price_input).val()); + var margin = unlocalize_currency($(margin_input).val()); + + var value = ((price - base_price) / base_price ) * 100; + value = !isFinite(value) ? 0.0 : value; + $(margin_input).val(value); + }, + }, + +}; + diff --git a/plugins/suppliers/public/javascripts/table_filter.js b/plugins/suppliers/public/javascripts/table_filter.js new file mode 120000 index 0000000..adcddc2 --- /dev/null +++ b/plugins/suppliers/public/javascripts/table_filter.js @@ -0,0 +1 @@ +../../../orders/public/javascripts/table_filter.js \ No newline at end of file diff --git a/plugins/suppliers/public/javascripts/toggle_edit.js b/plugins/suppliers/public/javascripts/toggle_edit.js new file mode 120000 index 0000000..f740683 --- /dev/null +++ b/plugins/suppliers/public/javascripts/toggle_edit.js @@ -0,0 +1 @@ +../../../orders/public/javascripts/toggle_edit.js \ No newline at end of file diff --git a/plugins/suppliers/public/style.scss b/plugins/suppliers/public/style.scss new file mode 100644 index 0000000..a480cf3 --- /dev/null +++ b/plugins/suppliers/public/style.scss @@ -0,0 +1,2 @@ +@import 'stylesheets/suppliers' + diff --git a/plugins/suppliers/public/stylesheets/_actions.scss b/plugins/suppliers/public/stylesheets/_actions.scss new file mode 120000 index 0000000..f4eeafa --- /dev/null +++ b/plugins/suppliers/public/stylesheets/_actions.scss @@ -0,0 +1 @@ +../../../orders/public/stylesheets/_actions.scss \ No newline at end of file diff --git a/plugins/suppliers/public/stylesheets/_base.scss b/plugins/suppliers/public/stylesheets/_base.scss new file mode 120000 index 0000000..a956ddb --- /dev/null +++ b/plugins/suppliers/public/stylesheets/_base.scss @@ -0,0 +1 @@ +../../../orders/public/stylesheets/_base.scss \ No newline at end of file diff --git a/plugins/suppliers/public/stylesheets/_field.scss b/plugins/suppliers/public/stylesheets/_field.scss new file mode 120000 index 0000000..0a8189c --- /dev/null +++ b/plugins/suppliers/public/stylesheets/_field.scss @@ -0,0 +1 @@ +../../../orders/public/stylesheets/_field.scss \ No newline at end of file diff --git a/plugins/suppliers/public/stylesheets/_filter.scss b/plugins/suppliers/public/stylesheets/_filter.scss new file mode 120000 index 0000000..7cd8656 --- /dev/null +++ b/plugins/suppliers/public/stylesheets/_filter.scss @@ -0,0 +1 @@ +../../../orders/public/stylesheets/_filter.scss \ No newline at end of file diff --git a/plugins/suppliers/public/stylesheets/_popin.scss b/plugins/suppliers/public/stylesheets/_popin.scss new file mode 120000 index 0000000..ac6bc20 --- /dev/null +++ b/plugins/suppliers/public/stylesheets/_popin.scss @@ -0,0 +1 @@ +../../../orders/public/stylesheets/_popin.scss \ No newline at end of file diff --git a/plugins/suppliers/public/stylesheets/_product.scss b/plugins/suppliers/public/stylesheets/_product.scss new file mode 100644 index 0000000..5e428b5 --- /dev/null +++ b/plugins/suppliers/public/stylesheets/_product.scss @@ -0,0 +1,129 @@ +@import 'base'; + +#supplier-our-products { + + .sortable-table { + + .box-field { + &.status { + width: $module01 - $base; + } + &.category { + width: $module02 - $base; + } + &.supplier { + width: $module02; + } + &.product { + width: $module04; + } + &.margin { + width: $module01; + } + &.price { + width: $module01; + } + &.stock { + width: $module01 - $base; + } + &.unit { + width: $module01 + 2*$base; + } + } + + form { + width: $module10 + $intercolumn; + float: left; + @extend .container-clean; + + .field-secondary { + float: left; + display: block; + + &, input, select { + width: $module02 - $intercolumn; + } + + &.with-unit { + @extend .container-clean; + + .input-group { + float: left; + width: $module02 - 2*$intercolumn; + &, input, select { + width: 100%; + } + } + } + } + + .internal-table { + + .row { + @extend .container-clean; + + &.first { + .block { + //height: 12*$height; + } + } + + .block { + float: left; + border-top: 2*$border solid rgba(255,255,255,0.5); + border-right: 2*$border solid rgba(255,255,255,0.5); + padding: $padding; + padding-top: $padding - 2*$border; + padding-right: $padding - 2*$border; + margin: 0; + + &:last-child { + border-right: none; + } + + &.margin-price { + width: $module04; + } + &.stock { + width: $module04; + } + &.available { + width: $module02; + color: black; //overwrite stupid application.css style + } + + .field-secondary { + margin-right: $margin; + } + } + } + } + + .action-button-container { + float: left; + width: $module10 - 2*$intercolumn; + margin-top: $half-margin; + + .action-button { + + &.save { + float: right; + } + &.remove, &.cancel { + margin-right: $margin; + float: left; + } + } + } + } + } + + .links { + float: right; + width: $module02; + + .action-button { + margin-bottom: $half-margin; + } + } +} diff --git a/plugins/suppliers/public/stylesheets/_sortable-table.scss b/plugins/suppliers/public/stylesheets/_sortable-table.scss new file mode 120000 index 0000000..1b3c561 --- /dev/null +++ b/plugins/suppliers/public/stylesheets/_sortable-table.scss @@ -0,0 +1 @@ +../../../orders/public/stylesheets/_sortable-table.scss \ No newline at end of file diff --git a/plugins/suppliers/public/stylesheets/suppliers.scss b/plugins/suppliers/public/stylesheets/suppliers.scss new file mode 100644 index 0000000..142f44c --- /dev/null +++ b/plugins/suppliers/public/stylesheets/suppliers.scss @@ -0,0 +1,160 @@ +@import 'base'; +@import 'popin'; +@import 'sortable-table'; + +@import 'filter'; +@import 'actions'; + +@import 'product'; + +.controller-profile_editor a.control-panel-suppliers-manage-suppliers { + background-image: url("/plugins/suppliers/images/control-panel/manage-suppliers.png") +} + +#suppliers-page { + + /* should be moved to core */ + input.small-loading { + background: transparent url(/images/loading-small.gif) no-repeat scroll right center; + } + + #search-results { + + > strong { + display: block; + margin-top: 3*$margin; + border-top: $border solid black; + } + } + + .supplier { + border-top: $border solid black; + border-bottom: none; + + &.supplier-inactive { + background-color: #faeeee; + } + + &#supplier-add { + display: none; + &.edit { + display: block; + } + } + &:last-child { + border-bottom: $border solid black; + } + + .supplier-add-link { + margin: 20px 0; + } + + #create-dummy { + display: none; //default + } + + .box-view { + padding: $padding; + padding-right: 0; + @extend .container-clean; + + .supplier-type { + font-style: italic; + } + + .supplier-logo { + float: left; + width: 60px; + min-height: 60px; + } + + .supplier-body { + width: $module04; + float: left; + } + + .supplier-name { + text-transform: uppercase; + } + + .supplier-actions { + float: right; + width: $module03; + text-align: right; + .hidden { + display: none; + } + } + } + + .box-edit { + display: none; //default + padding: $padding; + background-color: #FDF2A3; + + form { + width: 640px; + @import 'field'; + + // disable them until they are ready + .full-registration-check, + .full-data { + display: none !important; + } + + .data { + + textarea, + input { + width: 100%; + } + + .basic-data, + .full-data { + float: left; + @extend .container-clean; + width: 290px; + margin-right: $wireframe-padding; + } + .full-data { + &.estrategic-info { + float: right; + } + &.additional-fields { + width: 100%; + } + } + } + + input[type=submit] { + margin-top: $half-margin; + margin-right: $half-margin; + } + } + } + } + + .filter-box { + + .box-field { + + &, input { + width: 100%; + margin-left: 0; + } + } + } +} + +#suppliers-products-import { + padding: + + table, th, td { + border: $border solid black; + } + table { + th { + font-weight: bold; + } + } +} diff --git a/plugins/suppliers/test/test_helper.rb b/plugins/suppliers/test/test_helper.rb new file mode 100644 index 0000000..cca1fd3 --- /dev/null +++ b/plugins/suppliers/test/test_helper.rb @@ -0,0 +1 @@ +require File.dirname(__FILE__) + '/../../../test/test_helper' diff --git a/plugins/suppliers/test/unit/code_numbering_test.rb b/plugins/suppliers/test/unit/code_numbering_test.rb new file mode 100644 index 0000000..0fcb79a --- /dev/null +++ b/plugins/suppliers/test/unit/code_numbering_test.rb @@ -0,0 +1,7 @@ +require File.dirname(__FILE__) + '/../../../../test/test_helper' + +class CodeNumberingTest < ActiveSupport::TestCase + + +end + diff --git a/plugins/suppliers/test/unit/default_delegate_test.rb b/plugins/suppliers/test/unit/default_delegate_test.rb new file mode 100644 index 0000000..525796d --- /dev/null +++ b/plugins/suppliers/test/unit/default_delegate_test.rb @@ -0,0 +1,6 @@ +require 'test_helper' + +class DefaultDelegateTest < ActiveSupport::TestCase + + +end diff --git a/plugins/suppliers/test/unit/suppliers_plugin/distributed_product_test.rb b/plugins/suppliers/test/unit/suppliers_plugin/distributed_product_test.rb new file mode 100644 index 0000000..ddcd396 --- /dev/null +++ b/plugins/suppliers/test/unit/suppliers_plugin/distributed_product_test.rb @@ -0,0 +1,108 @@ +require "#{File.dirname(__FILE__)}/../../test_helper" + +class SuppliersPlugin::DistributedProductTest < ActiveSupport::TestCase + + def setup + @product_category = create(ProductCategory, :name => 'parent') + @profile = build(Enterprise) + @invisible_profile = build(Enterprise, :visible => false) + @other_profile = build(Enterprise) + @self_supplier = build(SuppliersPlugin::Supplier, :consumer => @profile, :profile => @profile) + @dummy_supplier = build(SuppliersPlugin::Supplier, :consumer => @profile, :profile => @dummy_profile) + @other_supplier = build(SuppliersPlugin::Supplier, :consumer => @profile, :profile => @other_profile) + end + + attr_accessor :product_category, + :profile, :invisible_profile, :other_profile, + :profile, :dummy_profile, :other_profile, :self_supplier, :dummy_supplier, :other_supplier + + should 'return default settings considering dummy supplier' do + product = build(SuppliersPlugin::DistributedProduct, :profile => @profile, :supplier => @dummy_supplier) + assert_equal nil, product.default_name + assert_equal nil, product.default_description + product = build(SuppliersPlugin::DistributedProduct, :profile => @profile, :supplier => @other_supplier) + assert_equal true, product.default_name + assert_equal true, product.default_description + end + + should 'return price without margins if it is own product' do + product = build(SuppliersPlugin::DistributedProduct, :price => 10, :margin_percentage => 10, :profile => @profile, :supplier => @self_supplier) + assert_equal 10.0, product.price.to_f + end + + should 'return price without margins if supplier product has no price' do + supplier_product = build(SuppliersPlugin::DistributedProduct, :profile => @other_profile, :supplier => @other_profile.self_supplier) + product = build(SuppliersPlugin::DistributedProduct, :price => 10, :margin_percentage => 10, :profile => @profile, :supplier => @other_supplier) + assert_equal 10.0, product.price.to_f + end + + should 'return price with margins' do + supplier_product = build(SuppliersPlugin::DistributedProduct, :price => 10, :margin_percentage => 10, :profile => @other_profile, :supplier => @other_profile.self_supplier) + product = build(SuppliersPlugin::DistributedProduct, :price => 10, :margin_percentage => 10, :supplier_product => supplier_product, :profile => @profile, :supplier => @other_supplier) + + product.default_margin_percentage = false + assert_equal 11.0, product.price.to_f + profile.margin_percentage = 20 + product.default_margin_percentage = true + assert_equal 12.0, product.price.to_f + end + + should 'allow set of supplier product' do + product = build(SuppliersPlugin::DistributedProduct, :price => 10, :margin_percentage => 10, :profile => @profile, :supplier => @self_supplier) + + product.from_product = build(SuppliersPlugin::DistributedProduct, :profile => @profile, :supplier => @self_supplier) + assert_nothing_raised do + product.supplier_product = {:price => 10, :margin_percentage => 10} + product.supplier_product = SuppliersPlugin::DistributedProduct.new :price => 5 + end + end + + should 'create a supplier product for a dummy supplier' do + product = build(SuppliersPlugin::DistributedProduct, :profile => @profile, :supplier => @dummy_supplier) + assert product.supplier_product + # negative assertion + product = build(SuppliersPlugin::DistributedProduct, :profile => @profile, :supplier => @other_supplier) + assert !product.supplier_product + end + + should 'respond to supplier_product_id setter and getter' do + product = create(SuppliersPlugin::DistributedProduct, :profile => @profile, :supplier => @dummy_supplier) + assert_equal product.supplier_product.id, product.supplier_product_id + product.expects(:distribute_from) + product.supplier_product_id = 1 + end + + should 'respond to distribute_from' do + product = create(SuppliersPlugin::DistributedProduct, :profile => @profile, :supplier => @profile.self_supplier) + + assert_raise RuntimeError do + supplier_product = build(SuppliersPlugin::DistributedProduct, :profile => @other_profile) + product.distribute_from(supplier_product) + end + + supplier_product = create(SuppliersPlugin::DistributedProduct, :profile => @other_profile) + product.profile.add_supplier @other_profile + product.distribute_from supplier_product + assert_equal product.supplier.profile, supplier_product.profile + assert_equal product.supplier.profile, supplier_product.profile + end + + should 'return json for category hierarchy' do + grandparent = create(ProductCategory, :name => 'grand parent') + parent = create(ProductCategory, :name => 'parent') + child = product_category + + product = SuppliersPlugin::DistributedProduct.new :category => parent + hash = {:own_name => "parent", :id => "2", :subcats => [], :name => "parent", + :hierarchy => [{:own_name => "parent", :subcats => [], :name => "parent", :id => "2"}]} + assert_equal hash, product.json_for_category + end + + should 'block own product distribution' do + product = Product.create :enterprise => @profile, :product_category => product_category + distributed = SuppliersPlugin::DistributedProduct.new :enterprise => @profile, :from_products => [product] + + assert distributed.invalid? + end + +end diff --git a/plugins/suppliers/test/unit/suppliers_plugin/product_test.rb b/plugins/suppliers/test/unit/suppliers_plugin/product_test.rb new file mode 100644 index 0000000..947ba74 --- /dev/null +++ b/plugins/suppliers/test/unit/suppliers_plugin/product_test.rb @@ -0,0 +1,49 @@ +require "test_helper" + +class SuppliersPlugin::ProductTest < ActiveSupport::TestCase + + def setup + @product = build(SuppliersPlugin::BaseProduct) + end + + should 'return first from product as supplier product' do + fp = build(SuppliersPlugin::BaseProduct, :profile => @product.profile) + @product.from_products = [fp] + assert_equal fp, @product.from_product + assert_equal fp, @product.supplier_product + end + + should 'respond to dummy and own' do + assert !@product.dummy? + assert @product.own? + end + + should 'return price with margins' do + supplier_product = build(SuppliersPlugin::DistributedProduct, :price => 10, :margin_percentage => 10, :profile => @product.profile, :supplier => @product.profile.self_supplier) + product = build(SuppliersPlugin::DistributedProduct, :price => 10, :margin_percentage => 10, :supplier_product => supplier_product, :profile => @product.profile, :supplier => @product.profile.self_supplier) + + product.default_margin_percentage = false + assert_equal 11.0, product.price_with_margins + @product.profile.margin_percentage = 20 + product.default_margin_percentage = true + assert_equal 12.0, product.price_with_margins + end + + should 'build default unit if none exists' do + assert_equal 0, Unit.count + assert 'unit', @product.unit.singular + end + + should 'avoid destroy by raising an exception' do + assert_raise RuntimeError do + @product.destroy + end + end + + should 'accept price in different formats' do + @product.price = '2,45' + assert_equal 2.45, @product.price + end + + +end diff --git a/plugins/suppliers/test/unit/suppliers_plugin/source_product_test.rb b/plugins/suppliers/test/unit/suppliers_plugin/source_product_test.rb new file mode 100644 index 0000000..a043536 --- /dev/null +++ b/plugins/suppliers/test/unit/suppliers_plugin/source_product_test.rb @@ -0,0 +1,8 @@ +require "#{File.dirname(__FILE__)}/../../test_helper" + +class SuppliersPlugin::SourceProductTest < ActiveSupport::TestCase + + def setup + end + +end diff --git a/plugins/suppliers/test/unit/suppliers_plugin/supplier_test.rb b/plugins/suppliers/test/unit/suppliers_plugin/supplier_test.rb new file mode 100644 index 0000000..f3d53c9 --- /dev/null +++ b/plugins/suppliers/test/unit/suppliers_plugin/supplier_test.rb @@ -0,0 +1,8 @@ +require "#{File.dirname(__FILE__)}/../../test_helper" + +class SuppliersPlugin::SupplierTest < ActiveSupport::TestCase + + def setup + end + +end diff --git a/plugins/suppliers/views/suppliers_plugin/manage_products/_basket_add.html.slim b/plugins/suppliers/views/suppliers_plugin/manage_products/_basket_add.html.slim new file mode 100644 index 0000000..c915f84 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/manage_products/_basket_add.html.slim @@ -0,0 +1,6 @@ += content_for :head do + = javascript_include_tag 'typeahead.bundle.js' + = stylesheet_link_tag 'typeahead' + += text_field_tag :query, nil, id: 'basket-add', placeholder: t('suppliers_plugin.views.manage_products.basket_tab.search') + diff --git a/plugins/suppliers/views/suppliers_plugin/manage_products/_basket_tab.html.slim b/plugins/suppliers/views/suppliers_plugin/manage_products/_basket_tab.html.slim new file mode 100644 index 0000000..0630554 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/manage_products/_basket_tab.html.slim @@ -0,0 +1,26 @@ += t'suppliers_plugin.views.manage_products.basket_tab.make_a_basket' if @product.from_products.size <= 1 + += render 'suppliers_plugin/manage_products/basket_add' + +table + thead + th= t'suppliers_plugin.views.manage_products.basket_tab.component' + th= t'suppliers_plugin.views.manage_products.basket_tab.quantity' + th + + tbody + - @product.sources_from_products.each do |sp| + tr + td + = link_to_product sp.from_product + |  + = "(#{sp.from_product.unit.singular})" if sp.from_product.unit + td= sp.quantity + td= button_to_function :remove, _('Remove'), "suppliers.basket.remove(#{sp.from_product.id.to_json})" if @allowed_user + +javascript: + suppliers.basket.removeUrl = #{url_for(controller: 'suppliers_plugin/basket', action: :remove, id: @product.id).to_json} + suppliers.basket.searchUrl = #{url_for(controller: 'suppliers_plugin/basket', action: :search, id: @product.id).to_json} + suppliers.basket.addUrl = #{url_for(controller: 'suppliers_plugin/basket', action: :add, id: @product.id).to_json} + suppliers.basket.load() + diff --git a/plugins/suppliers/views/suppliers_plugin/manage_products/_compare_tab.html.slim b/plugins/suppliers/views/suppliers_plugin/manage_products/_compare_tab.html.slim new file mode 100644 index 0000000..f3f8b1c --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/manage_products/_compare_tab.html.slim @@ -0,0 +1,18 @@ +- if @product.diff.empty? + = t'suppliers_plugin.views.manage_products.compare_tab.no_diff' +- else + = t'suppliers_plugin.views.manage_products.compare_tab.diff_count', n: @product.diff.size + ul + - @product.diff.each do |attr| + li= t"suppliers_plugin.models.base_product.default_attributes.#{attr}" + +div + - if @product.profile != @product.from_product.profile + = button nil, t('suppliers_plugin.views.manage_products.compare_tab.see_supplier', supplier: @product.from_product.profile.name), + {controller: :manage_products, profile: @product.from_product.profile.identifier, action: :show, id: @product.from_product.id}, + target: '_blank' + - else + = button nil, t('suppliers_plugin.views.manage_products.compare_tab.see'), + {controller: :manage_products, action: :show, id: @product.from_product.id}, + target: '_blank' + diff --git a/plugins/suppliers/views/suppliers_plugin/manage_products/_distribution_tab.html.slim b/plugins/suppliers/views/suppliers_plugin/manage_products/_distribution_tab.html.slim new file mode 100644 index 0000000..bcc3037 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/manage_products/_distribution_tab.html.slim @@ -0,0 +1,14 @@ + += t'suppliers_plugin.views.manage_products.distribution_tab.title' + += form_for :consumers, remote: true, url: {controller: 'suppliers_plugin/product', action: :distribute_to_consumers, id: @product.id}, + html: {data: {loading: "#product-distribution"}} do |f| + + - distributed_consumers = @product.consumers.all.to_set + - @product.profile.orgs_consumers.each do |consumer| + div + = check_box_tag "consumers[#{consumer.id}]", '1', distributed_consumers.include?(consumer.profile) + = label_tag "consumers[#{consumer.id}]", consumer.name + + - button_bar do + = submit_button :save, _('Save') diff --git a/plugins/suppliers/views/suppliers_plugin/product/_actions.html.slim b/plugins/suppliers/views/suppliers_plugin/product/_actions.html.slim new file mode 100644 index 0000000..29f7bea --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/product/_actions.html.slim @@ -0,0 +1,26 @@ +#actions-box.wireframe-size + .labels + span.select.col-lg-4.col-md-4.col-sm-4 + span.with-selection.col-lg-4.col-md-4.col-sm-4 + = t'views.actions.with_selection' + .clean + + .buttons + .select.btn-group.col-lg-4.col-md-4.col-sm-4 + button.btn.btn-default.btn-xs.dropdown-toggle aria-expanded="false" data-toggle="dropdown" type="button" + = t'views.actions.select' + span.caret + + ul.dropdown-menu role="menu" + li= link_to_function t('views.actions.all'), 'suppliers.our_product.select.all()' + li= link_to_function t('views.actions.none'), 'suppliers.our_product.select.none()' + + .with-selection.col-lg-4.col-md-4.col-sm-4 + = link_to_function t('views.actions.activate'), 'suppliers.our_product.select.activate(true)', class: 'btn btn-default btn-xs' + |  + = link_to_function t('views.actions.deactivate'), 'suppliers.our_product.select.activate(false)', class: 'btn btn-default btn-xs' + + .other.col-lg-4.col-md-4.col-sm-4 + = modal_link_to t('views.product.import.action'), {controller: 'suppliers_plugin/product', action: :import}, class: 'btn btn-default btn-xs' + + .clean diff --git a/plugins/suppliers/views/suppliers_plugin/product/_edit.html.slim b/plugins/suppliers/views/suppliers_plugin/product/_edit.html.slim new file mode 100644 index 0000000..1b8d10b --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/product/_edit.html.slim @@ -0,0 +1,79 @@ +.box-edit + = form_for product, as: "product_#{product.id}", remote: true, html: {data: {loading: true}}, + url: {controller: 'suppliers_plugin/product', action: :edit, id: product ? product.id : nil} do |f| + .internal-table + .title + = t'views.product._edit.distribution_setts' + + .row.first + + - unless product.own? + .block.margin-price + .field + = f.label :margin_percentage, t('views.product._edit.margin_price') + div.checkbox data-default-toggle="" + label name='default_margin_percentage' + = f.check_box :default_margin_percentage + = t'views.product._edit.default_margin' + div data-non-defaults="" + span.field-secondary.with-unit + = input_group_addon('%'){ f.number_field :margin_percentage, class: 'product-margin-percentage', step: 'any', onkeyup: 'suppliers.our_product.pmsync(this, true);', oninput: 'this.onkeyup()' } + + span.field-secondary.with-unit + = hidden_field_tag :base_price, product.from_product.price, class: 'product-base-price', id: 'product_base_price', + value: product.from_product.price + = input_group_addon(environment.currency_unit){ f.number_field :price, value: product.price, step: 'any', class: 'product-price', onkeyup: 'suppliers.our_product.pmsync(this, false);', oninput: 'this.onkeyup()' } + + - if defined? StockPlugin + .block.stock + .field + = f.label :stock, t('views.product._edit.stock') + div.checkbox data-default-toggle="" + label name='default_stored' + = f.check_box :default_stored + = t'views.product._edit.default_stock' + div data-non-defaults="" + span.field-secondary + = f.label :stored, t('views.product._edit.current_stock'), class: 'field-bellow' + = f.number_field :stored, step: 'any' + + .block.available + .field + = f.label :available, t('views.product._edit.distribution_state') + div.checkbox + label name='available' + = f.check_box :available + = t'views.product._edit.available' + + .row.second + - unless product.own? + .block.unit + .field + = f.label :unit_id, t('views.product._edit.selling_unit') + div.checkbox data-default-toggle="" + label name='default_unit' + = f.check_box :default_unit + = t'views.product._edit.same_from_purchase' + div data-non-defaults="" + span.field-secondary + = f.label :unit_id, t('views.product._edit.unit'), class: 'field-bellow' + = f.select :unit_id, options_for_select(@units.map{ |u| [u.name, u.id]}) + span.field-secondary + = f.label :unit_detail, t('views.product._edit.unit_detail'), class: 'field-bellow' + = f.text_field :unit_detail + + .action-button-container + = f.submit t('views.product._edit.save'), class: 'save action-button' + = link_to_function t('views.product._edit.cancel'), '', class: 'action-button cancel', 'toggle-edit' => '' + = link_to_remote t('views.product._edit.remove_product'), remote: true, confirm: t('views.product._edit.are_you_sure_you_want'), + url: {controller: 'suppliers_plugin/product', action: 'destroy', id: product.id }, class: 'action-button remove', + html: {data: {loading: "#our-product-#{product.id}"}} + + .links + = link_to t('views.product._edit.product_registry'), + {controller: :manage_products, action: :show, id: product.id}, + target: '_blank', class: 'action-button registry' + +javascript: + suppliers.our_product.load(#{product.id}); + diff --git a/plugins/suppliers/views/suppliers_plugin/product/_edit_line.html.slim b/plugins/suppliers/views/suppliers_plugin/product/_edit_line.html.slim new file mode 100644 index 0000000..cb49377 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/product/_edit_line.html.slim @@ -0,0 +1,19 @@ +.box-view.with-inner + .box-field.select toggle-ignore="" = check_box_tag "product_ids[]", product.id + + .box-view-inner + .box-field.status= t"views.product._edit_line.#{if product.available then :active else :inactive end}" + .box-field.category= product.category_name + .box-field.supplier= product.supplier.abbreviation_or_name + .box-field.product.box-edit-link= product.name + / own products doesn't have these fields, rescue + .box-field.margin= "#{product.margin_percentage_localized}%" unless product.own? + .box-field.price= price_span(product.price_as_currency_number || '-') unless product.own? + - if defined? StockPlugin + .box-field.stock= product.stored_localized || '∞' unless product.own? + .box-field.unit= product.unit.name if product.unit + + = edit_arrow "#our-product-#{product.id}", false + .clean + += render 'suppliers_plugin/product/edit', product: product diff --git a/plugins/suppliers/views/suppliers_plugin/product/_filter_fields.html.slim b/plugins/suppliers/views/suppliers_plugin/product/_filter_fields.html.slim new file mode 100644 index 0000000..2426003 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/product/_filter_fields.html.slim @@ -0,0 +1,24 @@ += hidden_field_tag :page + +.field.state + label= t'suppliers_plugin.views.product.index.situation' + div= select_tag :available, options_for_select([[t('suppliers_plugin.views.product.index.in_any_state'), ""], [ t('suppliers_plugin.views.product.index.active'), 'true'], [ t('suppliers_plugin.views.product.index.inactive'), 'false']], params[:available]) + +.field.category + label= t'suppliers_plugin.views.product.index.category' + div= select_tag :category_id, options_for_select(@product_categories.map{ |pc| [pc.name, pc.id]}.insert(0,[t('suppliers_plugin.views.product.index.all_the_categories'), ""])) + +.field.supplier + label= t'suppliers_plugin.views.product.index.supplier' + div= select_tag :supplier_id, options_for_select([[t('suppliers_plugin.views.product.index.all_the_suppliers'), ""]] + supplier_choices(profile.suppliers), params[:supplier_id].to_i) + +/ .field.qualifier + label= t'suppliers_plugin.views.product.index.with_the_qualifiers' + div= select_tag :qualifier_id, options_for_select([t('suppliers_plugin.views.product.index.anyone'), '']) +/ .field.stock + label= t'suppliers_plugin.views.product.index.whose_qty_available_i' + div= select_tag :stock, options_for_select([t('suppliers_plugin.views.product.index.bigger_than_the_stock'), '']) + +.field.name + label= t'suppliers_plugin.views.product.index.name' + div= text_field_tag :name, params[:name] diff --git a/plugins/suppliers/views/suppliers_plugin/product/_form_errors.html.erb b/plugins/suppliers/views/suppliers_plugin/product/_form_errors.html.erb new file mode 100644 index 0000000..decf479 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/product/_form_errors.html.erb @@ -0,0 +1,11 @@ +<% if form_errors.errors.any? %> +
+ <%= t('suppliers_plugin.views.product._form_errors.errors_found') %> + +
+<% end %> + diff --git a/plugins/suppliers/views/suppliers_plugin/product/_price_details.html.erb b/plugins/suppliers/views/suppliers_plugin/product/_price_details.html.erb new file mode 100644 index 0000000..03e804b --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/product/_price_details.html.erb @@ -0,0 +1,27 @@ +
"> + <% options = {class: "small-input"}.merge((product.supplier_dummy? or not supplier_product) ? {} : {disabled: 'disabled'}) %> + <% if supplier_product %> + <% options = options.merge(onkeyup: 'suppliers.our_product.sync_referred(this)', oninput: 'this.onkeyup()') %> + <% else %> + <% options = options.merge(onkeyup: 'suppliers.our_product.pmsync(this, false)', oninput: 'this.onkeyup()') %> + <% end %> + + <%= labelled_field f, :price, t('views.product._price_details.price'), + f.number_field(:price, options.merge(step: 'any', oldvalue: product.own_price)), class: 'left-column' %> + <%= labelled_field f, :unit_id, t('views.product._price_details.unit'), + f.select(:unit_id, Unit.all.map{ |u| [u.singular, u.id] }, {}, options.merge(oldvalue: product.own_unit_id)) %> + <%= labelled_field f, :minimum_selleable, t('views.product._price_details.minimum_order'), + f.number_field(:minimum_selleable, options.merge(step: 'any', oldvalue: product.own_minimum_selleable)), class: 'left-column' %> + <%= labelled_field f, :unit_detail, t('views.product._price_details.unit_specification'), + f.number_field(:unit_detail, options.merge(step: 'any', oldvalue: product.own_unit_detail)) %> + + <%# put here due to css align, see distribution.js %> + <% if supplier_product and not product.supplier_dummy? %> +
+ <%= check_box_tag :product_supplier_product_available, 1, false, style: 'float: left', disabled: 'disabled' %> + <%= f.label :available, t('views.product._price_details.commercialization_act'), class: 'line-label' %> +
+ <% end %> + +
+
diff --git a/plugins/suppliers/views/suppliers_plugin/product/_results.html.slim b/plugins/suppliers/views/suppliers_plugin/product/_results.html.slim new file mode 100644 index 0000000..d020cc5 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/product/_results.html.slim @@ -0,0 +1,8 @@ +- @products.each do |product| + .our-product.value-row id="our-product-#{product.id}" toggle-edit="suppliers.our_product.toggle_edit()" + = render "suppliers_plugin/product/edit_line", product: product + += pagination_links @products +javascript: + table_filter.pagination.init(#{_('loading...').to_json}) + diff --git a/plugins/suppliers/views/suppliers_plugin/product/_search.html.slim b/plugins/suppliers/views/suppliers_plugin/product/_search.html.slim new file mode 100644 index 0000000..373afd0 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/product/_search.html.slim @@ -0,0 +1,21 @@ +#result-count + small= t'views.product._search.showing_pcount_produc', count: @products_count + +#results.sortable-table.wireframe-size.checkable + .table-header style="#{"display: none" if @products.empty?}" + .box-field.status= t'views.product._search.status' + .box-field.category= t'views.product._search.category' + .box-field.supplier= t'views.product._search.supplier' + .box-field.product= t'views.product._search.product' + .box-field.margin= t'views.product._search.margin' + .box-field.price= t'views.product._search.price' + - if defined? StockPlugin + .box-field.stock= t'views.product._search.stock' + .box-field.unit= t'views.product._search.unit' + + .table-content + - if @products.empty? + = t'views.product._search.we_don_t_have_product' + - else + = render 'results' + .clean diff --git a/plugins/suppliers/views/suppliers_plugin/product/add.html.slim b/plugins/suppliers/views/suppliers_plugin/product/add.html.slim new file mode 100644 index 0000000..bf7737c --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/product/add.html.slim @@ -0,0 +1,18 @@ +div.popin + h1= t'views.product.add.title' + + div + = button :new, t('views.product.add.own_product'), {controller: :manage_products, profile: profile.identifier, action: :new}, + target: '_blank', onclick: 'noosfero.modal.close()' + |  + = t'views.product.add.or_from_a_supplier' + br + + div + - profile.suppliers.dummy.except_self.alphabetical.each do |supplier| + = link_to({controller: :manage_products, profile: supplier.profile.identifier, action: :new}, + target: '_blank', onclick: 'noosfero.modal.close()') do + = profile_image supplier, :icon + = supplier.abbreviation_or_name + |  + diff --git a/plugins/suppliers/views/suppliers_plugin/product/destroy.rjs b/plugins/suppliers/views/suppliers_plugin/product/destroy.rjs new file mode 100644 index 0000000..efc4f33 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/product/destroy.rjs @@ -0,0 +1,2 @@ +page.remove 'our-product-'+@product.id.to_s +page << "jQuery('#filter-form').submit();" diff --git a/plugins/suppliers/views/suppliers_plugin/product/distribute_to_consumers.js.erb b/plugins/suppliers/views/suppliers_plugin/product/distribute_to_consumers.js.erb new file mode 100644 index 0000000..5381728 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/product/distribute_to_consumers.js.erb @@ -0,0 +1,2 @@ +$('#product-distribution').html('<%= j render('suppliers_plugin/manage_products/distribution_tab') %>'); + diff --git a/plugins/suppliers/views/suppliers_plugin/product/edit.rjs b/plugins/suppliers/views/suppliers_plugin/product/edit.rjs new file mode 100644 index 0000000..9df819e --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/product/edit.rjs @@ -0,0 +1,3 @@ +page.replace_html "our-product-#{@product.id}", partial: 'suppliers_plugin/product/edit_line', locals: {product: @product} +page << "toggle_edit.value_row.reload()" + diff --git a/plugins/suppliers/views/suppliers_plugin/product/import.html.slim b/plugins/suppliers/views/suppliers_plugin/product/import.html.slim new file mode 100644 index 0000000..88809b0 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/product/import.html.slim @@ -0,0 +1,29 @@ +#suppliers-products-import.popin + h1= t'views.product.import.action' + + = form_tag({controller: 'suppliers_plugin/product', action: :import}, onsubmit: '$(this).ajaxSubmit(); return false') do + p= t'views.product.import.help' + table + thead + th= t'views.product.import.field_supplier_name' + th= t'views.product.import.field_product_name' + th= t'views.product.import.field_product_unit' + th= t'views.product.import.field_product_price' + tbody + tr + td + td + td + td + p= t'views.product.import.unit_help', units: environment.units.reorder('singular ASC').map(&:singular).join(' - ') + + .checkbox + label name='remove_all_suppliers' + = check_box_tag :remove_all_suppliers, 'true', false, onchange: "suppliers.our_product.import.confirmRemoveAll(this, '#{j t'views.product.import.confirm_remove_all'}')" + = t'views.product.import.remove_all' + + = file_field_tag :csv, accept: 'text/csv' + + = submit_button :save, t('views.product.import.send') + = modal_close_button _'Cancel' + diff --git a/plugins/suppliers/views/suppliers_plugin/product/import.js.erb b/plugins/suppliers/views/suppliers_plugin/product/import.js.erb new file mode 100644 index 0000000..86b6a7a --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/product/import.js.erb @@ -0,0 +1,4 @@ +noosfero.modal.close() + +display_notice(<%= @notice.to_json %>) + diff --git a/plugins/suppliers/views/suppliers_plugin/product/index.html.slim b/plugins/suppliers/views/suppliers_plugin/product/index.html.slim new file mode 100644 index 0000000..cc1bb02 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/product/index.html.slim @@ -0,0 +1,22 @@ +#supplier-our-products + h2= t'views.product.index.products' + div + .subtitle= t'views.product.index.info' + + h3= t'views.product.index.default_margin_of_com' + .subtitle= t'views.product.index.default_margin_info' + span#profile-margin-percentage + span class="#{profile.margin_percentage.blank? ? 'none' : 'set'}" + = (if profile.margin_percentage_localized then t'views.product.index.margin', margin: profile.margin_percentage_localized else t'views.product.index.no_margin_set' end) + |  + = modal_link_to t('views.product.index.change'), {controller: :suppliers_plugin_myprofile, action: :margin_change} + + br + = link_to t('views.product.index.add_product'), {controller: 'suppliers_plugin/product', action: :add}, class: 'btn btn-default btn-xs modal-toggle' + + = render 'suppliers_plugin/shared/filter', type: :product + = render 'suppliers_plugin/product/actions' + + #search-results + = render 'suppliers_plugin/product/search' + diff --git a/plugins/suppliers/views/suppliers_plugin/product/search.rjs b/plugins/suppliers/views/suppliers_plugin/product/search.rjs new file mode 100644 index 0000000..9649dfe --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/product/search.rjs @@ -0,0 +1 @@ +page.replace_html 'search-results', partial: 'suppliers_plugin/product/search' diff --git a/plugins/suppliers/views/suppliers_plugin/shared/_filter.html.slim b/plugins/suppliers/views/suppliers_plugin/shared/_filter.html.slim new file mode 100644 index 0000000..da44b12 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/shared/_filter.html.slim @@ -0,0 +1,15 @@ +#filter class="wireframe-size #{'with-actions' if type == :product}" + .title.filter-box= t'views.filter.filter' + - action = if type == :product then {action: :search} else {action: :index} end + = form_tag action, remote: true, id: 'filter-form', data: {loading: '#filter', update: '#search-results', type: "html"}, method: :get do + .fields.filter-box + - if type == :supplier + = render partial: 'suppliers_plugin_myprofile/filter_fields' + - elsif type == :product + = render 'suppliers_plugin/product/filter_fields' + .submit + = submit_tag t('views.filter.filter_it'), class: 'filter-submit' + +- if type == :product + = javascript_include_tag '/assets/plugins/suppliers/javascripts/table_filter' + diff --git a/plugins/suppliers/views/suppliers_plugin/shared/_pagereload.js.erb b/plugins/suppliers/views/suppliers_plugin/shared/_pagereload.js.erb new file mode 120000 index 0000000..7a064e9 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/shared/_pagereload.js.erb @@ -0,0 +1 @@ +../../../../orders/views/orders_plugin_shared/_pagereload.js.erb \ No newline at end of file diff --git a/plugins/suppliers/views/suppliers_plugin/shared/_pagereload.rjs b/plugins/suppliers/views/suppliers_plugin/shared/_pagereload.rjs new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin/shared/_pagereload.rjs diff --git a/plugins/suppliers/views/suppliers_plugin_myprofile/_edit.html.erb b/plugins/suppliers/views/suppliers_plugin_myprofile/_edit.html.erb new file mode 100644 index 0000000..d19c243 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin_myprofile/_edit.html.erb @@ -0,0 +1,62 @@ +<% supplier_label = (supplier.new_record? ? "add" : supplier.id) %> + +
+ +
+ <% if supplier.new_record? %> + <% if supplier.dummy? %> + <%= t('views.supplier._edit.add_supplier_managed_') %> + <% else %> + <%= t('views.supplier._edit.register_new_supplier') %> + <% end %> + <% else %> + <%= t('views.supplier._edit.edit_supplier') %> + <% end %> +
+ + <% if supplier.dummy? %> +
+ <%= t('views.supplier._edit.registry_help') %> +
+ + <% end %> + + <%= form_for supplier, as: :supplier, url: {action: supplier.new_record? ? :new : :edit, id: supplier.id}, remote: true, + html: {data: {loading: "#supplier-#{supplier_label}"}, class: 'full-registration'} do |f| %> + + <%= error_messages_for :supplier %> + +
+ <%= check_box_tag :full_registration, '1', false, + onchange: "jQuery(this).parents('.supplier').find('.data').toggleClass('full-registration', this.checked)" %> + <%= label_tag :full_registration, t('views.supplier._edit.full_registration') %> +
+ +
+
+

<%= t('views.supplier._edit.basic_data') %>

+ + <%= labelled_field f, :name, t('views.supplier._edit.name'), f.text_field(:name) %> + <%= labelled_field f, :name_abbreviation, t('views.supplier._edit.abbreviated_name'), f.text_field(:name_abbreviation) %> + <%= labelled_field f, :description, t('views.supplier._edit.description'), f.text_area(:description, maxlength: '550') %> + +
+
+

<%= t('views.supplier._edit.estrategic_informatio') %>

+
+
+

<%= t('views.supplier._edit.registry_form') %>

+
+
+

<%= t('views.supplier._edit.additional_fields') %>

+
+ +
+
+ + <%= f.submit t('views.supplier._edit.save') %> + <%= link_to_function t('views.supplier._edit.cancel'), '', 'toggle-edit' => '' %> + + <% end %> +
+ diff --git a/plugins/suppliers/views/suppliers_plugin_myprofile/_filter_fields.html.erb b/plugins/suppliers/views/suppliers_plugin_myprofile/_filter_fields.html.erb new file mode 100644 index 0000000..3990539 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin_myprofile/_filter_fields.html.erb @@ -0,0 +1,13 @@ +
+ +
+ <%= select_tag :active, options_for_select([[t('suppliers_plugin.views.product.index.in_any_state'), ""], [ t('suppliers_plugin.views.product.index.active'), 'true'], [ t('suppliers_plugin.views.product.index.inactive'), 'false']], params[:active]) %> +
+
+ +
+ +
<%= text_field_tag :name, params[:name] %>
+
+ + diff --git a/plugins/suppliers/views/suppliers_plugin_myprofile/_index.html.erb b/plugins/suppliers/views/suppliers_plugin_myprofile/_index.html.erb new file mode 120000 index 0000000..81a0589 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin_myprofile/_index.html.erb @@ -0,0 +1 @@ +index.html.erb \ No newline at end of file diff --git a/plugins/suppliers/views/suppliers_plugin_myprofile/_new.html.erb b/plugins/suppliers/views/suppliers_plugin_myprofile/_new.html.erb new file mode 100644 index 0000000..24cb717 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin_myprofile/_new.html.erb @@ -0,0 +1,7 @@ +
+ <%= render :partial => 'suppliers_plugin_myprofile/search', :locals => {:profile => profile} %> +
+ +
+ <%= render :partial => 'suppliers_plugin_myprofile/edit', :locals => {:supplier => supplier, :profile => profile} %> +
diff --git a/plugins/suppliers/views/suppliers_plugin_myprofile/_result.html.erb b/plugins/suppliers/views/suppliers_plugin_myprofile/_result.html.erb new file mode 100644 index 0000000..ec253d5 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin_myprofile/_result.html.erb @@ -0,0 +1,18 @@ + + + <%= profile_link_with_image enterprise, :thumb, :target => '_blank' %> + + +
+ <%= link_to enterprise.name, enterprise.url, :target => '_blank' %> +
+
+ <%= city_with_state enterprise.region %> +
+ + + <%= link_to_remote t('views.supplier._new.add'), + :url => {:action => :add, :profile => profile.identifier, :id => enterprise.id}, + :update => "search-results", :html => {:class => 'action-button', data: {loading: '#supplier-add'}} %> + + diff --git a/plugins/suppliers/views/suppliers_plugin_myprofile/_results.html.erb b/plugins/suppliers/views/suppliers_plugin_myprofile/_results.html.erb new file mode 100644 index 0000000..e790299 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin_myprofile/_results.html.erb @@ -0,0 +1,17 @@ +<% if @enterprises.empty? %> + <%= t('views.supplier._new.none_found') %> +<% else %> + + <% @enterprises.each do |enterprise| %> + <%= render 'result', :enterprise => enterprise %> + <% end %> +
+ +
+
+ <%= t('views.supplier._new.suggest_creation') %> +
+ + <%= link_to_function t('views.supplier._new.create_dummy'), 'suppliers.add.create_dummy();' %> +
+<% end %> diff --git a/plugins/suppliers/views/suppliers_plugin_myprofile/_search.html.erb b/plugins/suppliers/views/suppliers_plugin_myprofile/_search.html.erb new file mode 100644 index 0000000..8b78835 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin_myprofile/_search.html.erb @@ -0,0 +1,14 @@ +

<%= t('views.supplier._new.title') % {profile: profile.name} %>

+ +

<%= t('views.supplier._new.intro_help') % {environment: environment.name, contact_email: environment.contact_email} %>

+ +<%= form_tag({action: :search, profile: profile.identifier}, remote: true, method: :get, id: 'enterprise-search-form', + data: {loading: '#results'}) do %> + + <%= text_field_tag 'query', nil, placeholder: t('views.supplier._new.query_placeholder'), + onkeyup: 'suppliers.add.search(this)', autocomplete: 'off', autofill: 'off' %> + +
+
+ +<% end %> diff --git a/plugins/suppliers/views/suppliers_plugin_myprofile/_supplier.html.erb b/plugins/suppliers/views/suppliers_plugin_myprofile/_supplier.html.erb new file mode 100644 index 0000000..8a39127 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin_myprofile/_supplier.html.erb @@ -0,0 +1,58 @@ +
+ + +
+
+ <%= if supplier.dummy? then t('views.supplier._supplier.supplier_with_registr') else t('views.supplier._supplier.registered_supplier_i') end %> +
+ +
+ <%= link_to supplier.name, supplier.profile.url, :class => 'supplier-name', :target => '_blank' %> +
+ <% if supplier.name_abbreviation %> +
<%= t('views.supplier._supplier.abreviated_name') + ' ' %><%= supplier.name_abbreviation %>
+ <% end %> +

<%= supplier.description %>

+ +
<%= t('views.supplier._supplier.this_supplier_has') %>
+
+ <% products = supplier.consumer.distributed_products.unarchived.from_supplier_id(supplier.id) %> + <% if products.size > 0 %> + <%= link_to t('views.supplier._supplier.products', count: products.size), + {controller: 'suppliers_plugin/product', action: :index, supplier_id: supplier.id}, target: '_blank' %> + + <%= t('views.supplier._supplier.among_them') %> + + <% if supplier.dummy? %> + <% msg = products.available.size.to_s + + t('views.supplier._supplier.products_are_active') %> + <% else %> + <% msg = products.size.to_s + t('views.supplier._supplier.products_distributed') %> + <% end %> + <%= link_to msg, {controller: 'suppliers_plugin/product', action: :index, supplier_id: supplier.id}, target: '_blank', class: 'supplier-distributed-products-count' %> + <% else %> + <%= t('views.supplier._supplier.any_registered_produc') %> + <% end %> +
+
+ +
+
<%= t('views.supplier._supplier.actions') %>
+
<%= link_to_function t('views.supplier._supplier.edit'), '', 'toggle-edit' => '' %>
+ +
'><%= link_to_remote t('views.supplier._supplier.deactivate'), :url => {:action => :toggle_active, :id => supplier.id} %>
+
'><%= link_to_remote t('views.supplier._supplier.activate'), :url => {:action => :toggle_active, :id => supplier.id} %>
+ +
<%= link_to_remote t('views.supplier._supplier.disassociate'), :url => {:action => :destroy, :id => supplier.id}, + :confirm => t('views.supplier._supplier.by_removing_this_supp'), :class => 'supplier-remove' %>
+ +
<%= link_to t('views.supplier._supplier.manage_products'), {controller: 'suppliers_plugin/product', action: :index, supplier_id: supplier.id}, target: '_blank' %>
+
<%= link_to t('views.supplier._supplier.add_product'), {controller: :manage_products, profile: supplier.profile.identifier, action: :new}, target: '_blank' %>
+
+
+ +<%= render :partial => 'suppliers_plugin_myprofile/edit', :locals => {:supplier => supplier} %> + +
diff --git a/plugins/suppliers/views/suppliers_plugin_myprofile/_suppliers_list.html.erb b/plugins/suppliers/views/suppliers_plugin_myprofile/_suppliers_list.html.erb new file mode 100644 index 0000000..8e953e4 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin_myprofile/_suppliers_list.html.erb @@ -0,0 +1,15 @@ +<%= pagination_links suppliers %> + +<% if suppliers.empty? %> + <% if @is_search %> + <%= t('views.supplier._suppliers_list.this_search_didn_t_re') %> + <% else %> + <%= t('views.supplier._suppliers_list.any_registered') %> + <% end %> +<% else %> + <% suppliers.each do |supplier| %> +
+ <%= render :partial => 'suppliers_plugin_myprofile/supplier', :object => supplier %> +
+ <% end %> +<% end %> diff --git a/plugins/suppliers/views/suppliers_plugin_myprofile/add.js.erb b/plugins/suppliers/views/suppliers_plugin_myprofile/add.js.erb new file mode 100644 index 0000000..36b7770 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin_myprofile/add.js.erb @@ -0,0 +1,2 @@ +<%= render 'suppliers_plugin/shared/pagereload' %> + diff --git a/plugins/suppliers/views/suppliers_plugin_myprofile/destroy.js.erb b/plugins/suppliers/views/suppliers_plugin_myprofile/destroy.js.erb new file mode 100644 index 0000000..6db86af --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin_myprofile/destroy.js.erb @@ -0,0 +1 @@ +jQuery("#supplier-<%= @supplier.id %>").remove(); diff --git a/plugins/suppliers/views/suppliers_plugin_myprofile/edit.js.erb b/plugins/suppliers/views/suppliers_plugin_myprofile/edit.js.erb new file mode 100644 index 0000000..1bf5d4a --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin_myprofile/edit.js.erb @@ -0,0 +1,5 @@ +jQuery("#supplier-<%= @supplier.id %>").html("<%= j render(:partial => 'suppliers_plugin_myprofile/supplier', :object => @supplier) %>"); +<% unless @supplier.valid? %> + toggle_edit.value_row.reload(); +<% end %> + diff --git a/plugins/suppliers/views/suppliers_plugin_myprofile/index.html.erb b/plugins/suppliers/views/suppliers_plugin_myprofile/index.html.erb new file mode 100644 index 0000000..b745a5b --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin_myprofile/index.html.erb @@ -0,0 +1,21 @@ +
+

<%= t('views.supplier.index.suppliers') %>

+ +
+ <%= t('views.supplier.index.this_is_the_list_of_s') %> +
+ + + + <%= render 'suppliers_plugin/shared/filter', type: :supplier %> + +
+
+ <%= render 'suppliers_plugin_myprofile/new', supplier: @new_supplier %> +
+ +
+ <%= render 'suppliers_plugin_myprofile/suppliers_list', suppliers: @suppliers %> +
+
+
diff --git a/plugins/suppliers/views/suppliers_plugin_myprofile/margin_change.html.erb b/plugins/suppliers/views/suppliers_plugin_myprofile/margin_change.html.erb new file mode 100644 index 0000000..b88f646 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin_myprofile/margin_change.html.erb @@ -0,0 +1,24 @@ +
+

<%= t('views.myprofile.margin_change.change_default_margin') %>

+ + <%= form_for @profile, as: :profile_data, remote: true, url: {controller: :suppliers_plugin_myprofile, action: :margin_change} do |f| %> + +
+ <%= t('views.myprofile.margin_change.notice') %> +
+ + <%= labelled_field f, :margin_percentage, t('views.myprofile.margin_change.new_margin'), + f.number_field(:margin_percentage, step: 'any') + ' ' + t('views.myprofile.margin_change.%') %> + +
+ <%= check_box_tag :apply_to_all, 1, false, style: 'float: left' %> + <%= label_tag :apply_to_all, t('views.myprofile.margin_change.apply_to_all'), class: 'line-label' %> +
+ +
+ + <%= submit_tag t('views.myprofile.margin_change.confirm') %> + <%= modal_close_link t('views.myprofile.margin_change.cancel') %> + <% end %> +
+ diff --git a/plugins/suppliers/views/suppliers_plugin_myprofile/new.js.erb b/plugins/suppliers/views/suppliers_plugin_myprofile/new.js.erb new file mode 100644 index 0000000..7dcc1ad --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin_myprofile/new.js.erb @@ -0,0 +1,7 @@ +<% if @supplier.valid? %> + window.location = '<%= url_for :action => :index %>'; +<% else %> + jQuery("#supplier-add").html("<%= j render(:partial => 'suppliers_plugin_myprofile/supplier', :object => @supplier) %>"); + toggle_edit.value_row.reload(); +<% end %> + diff --git a/plugins/suppliers/views/suppliers_plugin_myprofile/search.js.erb b/plugins/suppliers/views/suppliers_plugin_myprofile/search.js.erb new file mode 100644 index 0000000..36e1828 --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin_myprofile/search.js.erb @@ -0,0 +1 @@ +jQuery('#results').html("<%= j render('results') %>"); diff --git a/plugins/suppliers/views/suppliers_plugin_myprofile/toggle_active.js.erb b/plugins/suppliers/views/suppliers_plugin_myprofile/toggle_active.js.erb new file mode 100644 index 0000000..d33663d --- /dev/null +++ b/plugins/suppliers/views/suppliers_plugin_myprofile/toggle_active.js.erb @@ -0,0 +1,3 @@ +jQuery('#supplier-<%=@supplier.id%>').toggleClass('supplier-inactive'); +jQuery('#supplier-<%=@supplier.id%> .supplier-actions .deactivate').toggleClass('hidden'); +jQuery('#supplier-<%=@supplier.id%> .supplier-actions .activate').toggleClass('hidden'); diff --git a/plugins/volunteers/controllers/myprofile/volunteers_plugin_myprofile_controller.rb b/plugins/volunteers/controllers/myprofile/volunteers_plugin_myprofile_controller.rb new file mode 100644 index 0000000..45170e8 --- /dev/null +++ b/plugins/volunteers/controllers/myprofile/volunteers_plugin_myprofile_controller.rb @@ -0,0 +1,33 @@ +class VolunteersPluginMyprofileController < MyProfileController + + no_design_blocks + + # remove fake dependency + helper OrdersPlugin::DateHelper + + def index + + end + + def toggle_assign + @owner_id = params[:owner_id] + @owner_type = params[:owner_type] + @owner = @owner_type.constantize.find @owner_id + @period = @owner.volunteers_periods.find params[:id] + + if profile.members.include? user + @assignment = @period.assignments.where(profile_id: user.id).first + if @assignment + @assignment.destroy + else + @period.assignments.create! profile_id: user.id + end + @period.assignments.reload + end + + render partial: 'volunteering', locals: {period: @period} + end + + protected + +end diff --git a/plugins/volunteers/db/migrate/20141012005319_create_volunteers_plugin_periods.rb b/plugins/volunteers/db/migrate/20141012005319_create_volunteers_plugin_periods.rb new file mode 100644 index 0000000..2d72d48 --- /dev/null +++ b/plugins/volunteers/db/migrate/20141012005319_create_volunteers_plugin_periods.rb @@ -0,0 +1,21 @@ +class CreateVolunteersPluginPeriods < ActiveRecord::Migration + def up + create_table :volunteers_plugin_periods do |t| + t.integer :owner_id + t.string :owner_type + t.text :name + t.datetime :start + t.datetime :end + t.integer :minimum_assigments + t.integer :maximum_assigments + + t.timestamps + end + add_index :volunteers_plugin_periods, [:owner_type] + add_index :volunteers_plugin_periods, [:owner_id, :owner_type] + end + + def down + drop_table :volunteers_plugin_periods + end +end diff --git a/plugins/volunteers/db/migrate/20141012005354_create_volunteers_plugin_assignments.rb b/plugins/volunteers/db/migrate/20141012005354_create_volunteers_plugin_assignments.rb new file mode 100644 index 0000000..f2a0f86 --- /dev/null +++ b/plugins/volunteers/db/migrate/20141012005354_create_volunteers_plugin_assignments.rb @@ -0,0 +1,17 @@ +class CreateVolunteersPluginAssignments < ActiveRecord::Migration + def up + create_table :volunteers_plugin_assignments do |t| + t.integer :profile_id + t.integer :period_id + + t.timestamps + end + add_index :volunteers_plugin_assignments, [:period_id] + add_index :volunteers_plugin_assignments, [:profile_id] + add_index :volunteers_plugin_assignments, [:profile_id, :period_id] + end + + def down + drop_table :volunteers_plugin_assignments + end +end diff --git a/plugins/volunteers/lib/ext/profile.rb b/plugins/volunteers/lib/ext/profile.rb new file mode 100644 index 0000000..068594c --- /dev/null +++ b/plugins/volunteers/lib/ext/profile.rb @@ -0,0 +1,19 @@ +require_dependency 'profile' + +# attr_accessible must be defined on subclasses +Profile.descendants.each do |subclass| + subclass.class_eval do + attr_accessible :volunteers_settings + end +end + +class Profile + + def volunteers_settings attrs = {} + @volunteers_settings ||= Noosfero::Plugin::Settings.new self, VolunteersPlugin, attrs + attrs.each{ |a, v| @volunteers_settings.send "#{a}=", v } + @volunteers_settings + end + alias_method :volunteers_settings=, :volunteers_settings + +end diff --git a/plugins/volunteers/lib/split_datetime.rb b/plugins/volunteers/lib/split_datetime.rb new file mode 100644 index 0000000..be184bf --- /dev/null +++ b/plugins/volunteers/lib/split_datetime.rb @@ -0,0 +1,72 @@ + +module SplitDatetime + + class << self + def nil_time + Time.parse "#{Time.now.hour}:0:0" + end + def nil_date + Date.today + end + + def to_time datetime + datetime = self.nil_time if datetime.blank? + datetime.to_formatted_s :time + end + def to_date datetime + datetime = self.nil_date if datetime.blank? + datetime.strftime '%d/%m/%Y' + end + def set_time datetime, value + value = if value.blank? + self.nil_time + elsif value.kind_of? String + Time.parse value + else + value.to_time + end + datetime = self.nil_date if datetime.blank? + + Time.mktime(datetime.year, datetime.month, datetime.day, value.hour, value.min, value.sec).to_datetime + end + def set_date datetime, value + value = if value.blank? + self.nil_date + elsif value.kind_of? String + DateTime.strptime value, '%d/%m/%Y' + else + value.to_time + end + datetime = nil_time if datetime.blank? + + Time.mktime(value.year, value.month, value.day, datetime.hour, datetime.min, datetime.sec).to_datetime + end + end + + module SplitMethods + + def split_datetime attr + define_method "#{attr}_time" do + datetime = send attr + SplitDatetime.to_time datetime + end + define_method "#{attr}_date" do + datetime = send attr + SplitDatetime.to_date datetime + end + define_method "#{attr}_time=" do |value| + datetime = send attr + send "#{attr}=", SplitDatetime.set_time(datetime, value) + end + define_method "#{attr}_date=" do |value| + datetime = send attr + send "#{attr}=", SplitDatetime.set_date(datetime, value) + end + end + + end + +end + +Class.extend SplitDatetime::SplitMethods +ActiveRecord::Base.extend SplitDatetime::SplitMethods diff --git a/plugins/volunteers/lib/volunteers_plugin.rb b/plugins/volunteers/lib/volunteers_plugin.rb new file mode 100644 index 0000000..6af0dce --- /dev/null +++ b/plugins/volunteers/lib/volunteers_plugin.rb @@ -0,0 +1,20 @@ + +class VolunteersPlugin < Noosfero::Plugin + + def self.plugin_name + I18n.t('volunteers_plugin.lib.plugin.name') + end + + def self.plugin_description + I18n.t('volunteers_plugin.lib.plugin.description') + end + + def stylesheet? + true + end + + def js_files + ['volunteers.js'].map{ |j| "javascripts/#{j}" } + end + +end diff --git a/plugins/volunteers/locales/en-US.yml b/plugins/volunteers/locales/en-US.yml new file mode 100644 index 0000000..0279af3 --- /dev/null +++ b/plugins/volunteers/locales/en-US.yml @@ -0,0 +1,35 @@ +# encoding: UTF-8 + +"en-US": &en-US + + volunteers_plugin: + lib: + plugin: + name: "Work periods and volunteers" + description: "Do management of periods of voluteers work" + models: + period: + name: 'Nome' + start_date: 'Start day' + start_time: 'Time of start' + end_date: 'End day' + end_time: 'Time of end' + minimum_assigments: 'Minimum number required' + views: + index: + volunteer: "volunteer" + myprofile: + volunteers_periods: "Volunteers' period" + add_period: "Add" + period: + label: 'Turno' + remove: 'Remove' + minimum: 'have minimum?' + maximum: 'have maximum?' + no: 'No' + +'en_US': + <<: *en-US +'en': + <<: *en-US + diff --git a/plugins/volunteers/locales/pt-BR.yml b/plugins/volunteers/locales/pt-BR.yml new file mode 100644 index 0000000..6831b37 --- /dev/null +++ b/plugins/volunteers/locales/pt-BR.yml @@ -0,0 +1,35 @@ +# encoding: UTF-8 + +"pt-BR": &pt-BR + + volunteers_plugin: + lib: + plugin: + name: "Períodos de trabalhos e voluntários" + description: "Faz a gestão dos períodos de trabalho voluntário" + attributes: + period: + name: 'Nome' + start_date: 'Dia de início' + start_time: 'Horário de início' + end_date: 'Dia de fim' + end_time: 'Horário de fim' + minimum_assigments: 'Número mínimo requerido' + views: + index: + volunteer: "voluntariar" + myprofile: + volunteers_periods: "Turnos de voluntários" + add_period: "Adicionar turno" + period: + label: 'Turno' + remove: 'Remover' + minimum: 'tem mínimo?' + noo: 'Não' + maximum: 'tem maximo?' + +'pt_BR': + <<: *pt-BR +'pt': + <<: *pt-BR + diff --git a/plugins/volunteers/models/volunteers_plugin/assignment.rb b/plugins/volunteers/models/volunteers_plugin/assignment.rb new file mode 100644 index 0000000..2542ced --- /dev/null +++ b/plugins/volunteers/models/volunteers_plugin/assignment.rb @@ -0,0 +1,12 @@ +class VolunteersPlugin::Assignment < ActiveRecord::Base + + attr_accessible :profile_id + + belongs_to :profile + belongs_to :period, class_name: 'VolunteersPlugin::Period' + + validates_presence_of :profile + validates_presence_of :period + validates_uniqueness_of :profile_id, scope: :period_id + +end diff --git a/plugins/volunteers/models/volunteers_plugin/period.rb b/plugins/volunteers/models/volunteers_plugin/period.rb new file mode 100644 index 0000000..ff3091c --- /dev/null +++ b/plugins/volunteers/models/volunteers_plugin/period.rb @@ -0,0 +1,24 @@ +class VolunteersPlugin::Period < ActiveRecord::Base + + attr_accessible :name + attr_accessible :start, :end + attr_accessible :owner_type + attr_accessible :minimum_assigments + attr_accessible :maximum_assigments + + belongs_to :owner, polymorphic: true + + has_many :assignments, class_name: 'VolunteersPlugin::Assignment', foreign_key: :period_id, include: [:profile], dependent: :destroy + + validates_presence_of :owner + validates_presence_of :name + validates_presence_of :start, :end + + extend OrdersPlugin::DateRangeAttr::ClassMethods + date_range_attr :start, :end + + extend SplitDatetime::SplitMethods + split_datetime :start + split_datetime :end + +end diff --git a/plugins/volunteers/public/javascripts/volunteers.js b/plugins/volunteers/public/javascripts/volunteers.js new file mode 100644 index 0000000..48130f2 --- /dev/null +++ b/plugins/volunteers/public/javascripts/volunteers.js @@ -0,0 +1,47 @@ +volunteers = { + + periods: { + load: function() { + $('#volunteers-periods .period').each(function() { + volunteers.periods.applyDaterangepicker(this) + }) + $('#period-new input').prop('disabled', true) + }, + + new: function() { + var period = $('#volunteers-periods-template').html() + period = period.replace(/_new_/g, new Date().getTime()) + period = $(period) + period.find('input').prop('disabled', false) + this.applyDaterangepicker(period) + return period + }, + + add: function() { + $('.periods').append(this.new()) + }, + + remove: function(link) { + link = $(link) + var period = link.parents('.period') + period.find('input[name*=_destroy]').prop('value', '1') + period.hide() + }, + + applyDaterangepicker: function(period) { + orders.daterangepicker.init($(period).find('.daterangepicker-field')) + }, + + }, + + assignments: { + toggle: function(period) { + period = $(period) + $.get(period.attr('data-toggle-url'), function(data) { + $(period).replaceWith(data) + }) + }, + + }, + +}; diff --git a/plugins/volunteers/public/style.scss b/plugins/volunteers/public/style.scss new file mode 100644 index 0000000..e7b9aab --- /dev/null +++ b/plugins/volunteers/public/style.scss @@ -0,0 +1,2 @@ +@import 'stylesheets/style' + diff --git a/plugins/volunteers/public/stylesheets/_base.scss b/plugins/volunteers/public/stylesheets/_base.scss new file mode 120000 index 0000000..26345f3 --- /dev/null +++ b/plugins/volunteers/public/stylesheets/_base.scss @@ -0,0 +1 @@ +../../../suppliers/public/stylesheets/_base.scss \ No newline at end of file diff --git a/plugins/volunteers/public/stylesheets/style.scss b/plugins/volunteers/public/stylesheets/style.scss new file mode 100644 index 0000000..47bbe88 --- /dev/null +++ b/plugins/volunteers/public/stylesheets/style.scss @@ -0,0 +1,58 @@ +@import 'base'; + +.volunteer-to-period { + width: $module03; + margin-right: $margin; + float: left; + cursor: pointer; + + background: #F8FA83; + text-align: left; + + &:hover { + background: lighten(yellow, 20%); + } + + .name { + background-color: #E1F5C4; + width: 100%; + line-height: 20px; + font-size: 15px; + font-weight: bold; + text-transform: capitalize; + padding: $padding; + box-sizing: border-box; + } + .body { + padding: $padding; + + .period { + margin-bottom: $margin; + } + } +} + +#cycle-admin-page #volunteers-periods { + margin-bottom: $margin*2; + + .body { + margin-bottom: $half-margin; + } + input.minimum { + display: block; + } + input.time-select { + width: 50px; + } + select.minimum { + float: left; + margin-right: 20px; + } + select.maximum { + } + a.remove { + clear: both; + display: block; + margin-bottom: $margin; + } +} diff --git a/plugins/volunteers/test/unit/split_datetime_test.rb b/plugins/volunteers/test/unit/split_datetime_test.rb new file mode 100644 index 0000000..9bdeaec --- /dev/null +++ b/plugins/volunteers/test/unit/split_datetime_test.rb @@ -0,0 +1,53 @@ +require File.dirname(__FILE__) + '/../../../../test/test_helper' + +class ModelWithDate + + def initialize + @delivery = DateTime.now + end + + attr_accessor :delivery + + extend SplitDatetime::SplitMethods + split_datetime :delivery + +end + +class SplitDatetimeTest < ActiveSupport::TestCase + + def setup + @m = ModelWithDate.new + @m.delivery = (Time.mktime(2011) + 2.hours + 2.minutes + 2.seconds).to_datetime + end + + should 'return get splitted times' do + assert_equal @m.delivery_date, '2011-01-01' + assert_equal @m.delivery_time, '02:02' + end + + should 'return set splitted times by Date' do + @m.delivery_date = (Time.mktime(2011, 3, 5) + 3.hours + 3.minutes + 3.seconds).to_datetime + assert_equal @m.delivery_date, '2011-03-05' + assert_equal @m.delivery_time, '02:02' + end + + should 'return set splitted times by Time' do + @m.delivery_time = (Time.mktime(2011, 3, 5) + 3.hours + 3.minutes + 3.seconds).to_datetime + assert_equal @m.delivery_date, '2011-01-01' + assert_equal @m.delivery_time, '03:03' + end + + should 'return set splitted times by Date String' do + @m.delivery_date = "2011-11-11" + assert_equal @m.delivery_date, '2011-11-11' + assert_equal @m.delivery_time, '02:02' + end + + should 'return set splitted times by Time String' do + @m.delivery_time = "15:43" + assert_equal @m.delivery_date, '2011-01-01' + assert_equal @m.delivery_time, '15:43' + end + +end + diff --git a/plugins/volunteers/views/volunteers_plugin_myprofile/_edit_periods.html.erb b/plugins/volunteers/views/volunteers_plugin_myprofile/_edit_periods.html.erb new file mode 100644 index 0000000..ff7423b --- /dev/null +++ b/plugins/volunteers/views/volunteers_plugin_myprofile/_edit_periods.html.erb @@ -0,0 +1,30 @@ +
+ + <%= f.label :volunteers_periods, t('volunteers_plugin.views.myprofile.volunteers_periods') %> +
+ +
+ <% owner.volunteers_periods.each do |period| %> + <%= f.fields_for :volunteers_periods, period do |ff| %> +
+ <%= render 'volunteers_plugin_myprofile/period', f: ff, period: period, owner: owner %> +
+ <% end %> + <% end %> +
+ + + <%= link_to_function t('volunteers_plugin.views.myprofile.add_period'), 'volunteers.periods.add()', class: 'action-button' %> + + <%= javascript_tag do %> + volunteers.periods.load(); + <% end %> + +
diff --git a/plugins/volunteers/views/volunteers_plugin_myprofile/_period.html.erb b/plugins/volunteers/views/volunteers_plugin_myprofile/_period.html.erb new file mode 100644 index 0000000..0b5ce3a --- /dev/null +++ b/plugins/volunteers/views/volunteers_plugin_myprofile/_period.html.erb @@ -0,0 +1,21 @@ +<%= f.hidden_field :id %> +<%= f.hidden_field :_destroy %> + +<% here = 'volunteers_plugin.views.myprofile.period.' %> + +
+ <%= f.text_field :name, placeholder: t('volunteers_plugin.attributes.period.name') %> +
+ <%= datetime_range_field f, :start, :end %> +
+ + <%# f.label :minimum_assigments, t(here+'minimum') %> + <%# f.select :minimum_assigments, options_for_select((0..10).to_a), {prompt: t(here+'noo')}, class: "minimum" %> + + <%# f.label :maximum_assigments, t(here+'maximum') %> + <%# f.select :maximum_assigments, options_for_select((1..10).to_a), {prompt: t(here+'noo')}, class: "maximum" %> + + <%= link_to_function t(here+'remove'), 'volunteers.periods.remove(this)', class: 'remove' %> +
+
+ diff --git a/plugins/volunteers/views/volunteers_plugin_myprofile/_volunteering.html.erb b/plugins/volunteers/views/volunteers_plugin_myprofile/_volunteering.html.erb new file mode 100644 index 0000000..832a273 --- /dev/null +++ b/plugins/volunteers/views/volunteers_plugin_myprofile/_volunteering.html.erb @@ -0,0 +1,20 @@ +
+
+ <%= period.name %> +
+
+
+ <%= day_time_period period.start, period.end %> +
+
+ <% period.assignments.each do |assig| %> +
+ <%= profile_image assig.profile, :icon %> + <%= assig.profile.short_name %> +
+ <% end %> + <%# link_to t('volunteers_plugin.views.index.volunteer'), url: '' %> +
+
+ +
diff --git a/plugins/volunteers/views/volunteers_plugin_myprofile/index.html.erb b/plugins/volunteers/views/volunteers_plugin_myprofile/index.html.erb new file mode 100644 index 0000000..ef93c36 --- /dev/null +++ b/plugins/volunteers/views/volunteers_plugin_myprofile/index.html.erb @@ -0,0 +1,5 @@ +<% profile.orders_cycle.has_volunteers_periods.each do |cycle| %> + <% cycle %> + <% cycle.volunteers-periods.each do |period| %> + <% end %> +<% end %> -- libgit2 0.21.2