Parcourir la source

Added promotions

Jacqueline Maldonado il y a 7 ans
Parent
commit
f2d219ae42

+ 3 - 0
app/assets/javascripts/promotions.coffee

@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://coffeescript.org/

+ 3 - 0
app/assets/stylesheets/promotions.scss

@@ -0,0 +1,3 @@
+// Place all the styles related to the promotions controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/

+ 41 - 0
app/controllers/application_controller.rb

@@ -35,6 +35,19 @@ class ApplicationController < ActionController::Base
     render json: SpmxCounty.where("state_id = ?", params[:state_id])
   end
 
+  def find_products
+    products =
+      if params[:variants] == "true" && params[:query].include?(":")
+        attributes = query_for_variants(params[:query])
+        Product.name_sku_barcode_attribute_like(@product_name, attributes).limit(30).to_json(methods: [:small_img, :display_attributes, :display_sku_name_attributes])
+      elsif params[:variants] == "true"
+        Product.name_sku_barcode_like(params[:query]).limit(30).to_json(methods: [:small_img, :display_attributes, :display_sku_name_attributes])
+      else
+        Product.name_sku_barcode_like_sp(params[:query]).limit(30).to_json(methods: [:small_img, :display_attributes, :display_sku_name_attributes])
+      end
+    render json: products
+  end
+
   def find
     query = params[:query]
     if query.include? ':' # search with attributes
@@ -65,6 +78,34 @@ class ApplicationController < ActionController::Base
     render json: query.include?(":") ? Product.name_sku_barcode_attribute_like(product_name, attrs_query_string).limit(30).to_json(methods: [:small_img, :display_attributes]) : Product.name_sku_barcode_like(params[:query]).limit(30).to_json(methods: [:small_img, :display_attributes])
   end
 
+  def query_for_variants(query)
+    product_query =
+      if query.include? ':' # search with attributes
+        query_array = query.split(':')
+        @product_name = query_array[0]
+        query_array.shift # delete the name of the product from the array to iterate the attributes
+        attrs_query_string = ''
+        query_array.each do |attribute|
+          next unless attribute.present?
+          attr_type =
+            case attribute[0]
+            when 'c'
+              'colors'
+            when 't'
+              'sizes'
+            when 'e'
+              'styles'
+            end
+          attribute[0] = "" # delete the attribute type character
+          attrs_query_string.concat(" AND attributes_json::json->>'#{attr_type}' ilike '%#{attribute}%'")
+        end
+        attrs_query_string
+      else
+        query
+      end
+    product_query
+  end
+
   # para special_prices
   def find_sp
     query = params[:query]

+ 74 - 0
app/controllers/promotions_controller.rb

@@ -0,0 +1,74 @@
+class PromotionsController < ApplicationController
+  ##--- Abilities
+  load_and_authorize_resource
+
+  add_breadcrumb I18n.t("breadcrumbs." + controller_name), :promotions_path
+  add_breadcrumb "Nueva " + I18n.t("breadcrumbs." + controller_name).singularize, :new_promotion_path, only: :new
+
+  before_action :set_promotion, only: [:show, :destroy]
+
+  # GET /promotions
+  # GET /promotions.json
+  def index
+    @promotions = Promotion.all.order('id desc')
+    @promotions.map(&:verify_promotion)
+  end
+
+  # GET /promotions/1
+  # GET /promotions/1.json
+  def show; end
+
+  # GET /promotions/new
+  def new
+    @promotion = Promotion.new
+    @promotion.percent = nil
+  end
+
+  # POST /promotions
+  # POST /promotions.json
+  def create
+    @promotion = Promotion.new(promotion_params)
+    source =
+      if @promotion.category_id.present? || @promotion.subcategory_id.to_i != 0
+        "a la #{@promotion.subcategory_id.to_i != 0 ? 'sublínea' : 'línea' } #{@promotion.subcategory_id.to_i != 0 ? Category.find(@promotion.subcategory_id).category : @promotion.category.category}"
+      elsif @promotion.product.present?
+        "al producto #{@promotion.product.name}"
+      end
+    respond_to do |format|
+      if @promotion.save
+        message = "Promoción de descuento del #{@promotion.percent}% agregado #{source} del #{@promotion.start_date.strftime("%d/%m/%Y")} al #{@promotion.end_date.strftime("%d/%m/%Y")}"
+        @promotion.audit_comment = message
+        format.html { redirect_to promotions_path, notice: message }
+        format.json { render :show, status: :created, location: @promotion }
+      else
+        @filter = params[:filter]
+        format.html { render :new }
+        format.json { render json: @promotion.errors, status: :unprocessable_entity }
+      end
+    end
+  end
+
+  # DELETE /promotions/1
+  # DELETE /promotions/1.json
+  def destroy
+    message = "Descuento eliminado correctamente"
+    @promotion.audit_comment = message
+    @promotion.destroy
+    respond_to do |format|
+      format.html { redirect_to promotions_url, notice: message }
+      format.json { head :no_content }
+    end
+  end
+
+  private
+
+  # Use callbacks to share common setup or constraints between actions.
+  def set_promotion
+    @promotion = Promotion.find(params[:id])
+  end
+
+  # Never trust parameters from the scary internet, only allow the white list through.
+  def promotion_params
+    params.require(:promotion).permit(:start_date, :end_date, :percent, :product_id, :category_id, :subcategory_id, :status)
+  end
+end

+ 12 - 11
app/controllers/sales_controller.rb

@@ -328,21 +328,22 @@ class SalesController < ApplicationController
   def add_haggle
     @pre_sale = PreSale.find(params[:pre_sale])
     @haggle_percent = !current_user.pointsale.haggle_percent.blank? ? current_user.pointsale.haggle_percent : @pos_config.haggle_in_sale_percent
-    @suggested_haggle = (@haggle_percent.to_f / 100) * @pre_sale.unit_price
+    @suggested_haggle = (@haggle_percent.to_f / 100) * @pre_sale.unit_price_w_discount
   end
 
   def create_haggle
+    @pre_sale = PreSale.find(params[:pre_sale])
+    @haggle_percent = params[:haggle_percent].present? ? params[:haggle_percent].to_f : nil
+    @haggle_quantity = params[:haggle_quantity].present? ? params[:haggle_quantity].to_f : nil
+
+    if @haggle_percent.present?
+      @pre_sale.haggle_percent = @haggle_percent
+    elsif @haggle_quantity.present?
+      @pre_sale.haggle = @haggle_quantity
+    end
+    @pre_sale.get_totals
+
     respond_to do |format|
-      @pre_sale = PreSale.find(params[:pre_sale])
-      @haggle_percent = params[:haggle_percent].present? ? params[:haggle_percent].to_f : nil
-      @haggle_quantity = params[:haggle_quantity].present? ? params[:haggle_quantity].to_f : nil
-
-      if @haggle_percent.present?
-        @pre_sale.haggle = (@haggle_percent / 100) * @pre_sale.total
-      elsif @haggle_quantity.present?
-        @pre_sale.haggle = @haggle_quantity
-      end
-      @pre_sale.get_totals
       if @pre_sale.save
         format.js
       end

+ 2 - 0
app/helpers/promotions_helper.rb

@@ -0,0 +1,2 @@
+module PromotionsHelper
+end

+ 17 - 11
app/models/ability.rb

@@ -34,36 +34,42 @@ class Ability
 
     if user.usertype == "A" || user.usertype == "SS"
       # Cajas registradoras
-      can :read, [CashRegister, Purchase, PaymentMethod, ProductsReturn, CashOut]
+      can :read, [AvailableProduct, CashOut, CashRegister, PaymentMethod, ProductsReturn]
       # Categorias
-      can :manage, [Category, Customer, BillingInformation, Expensesconcept, Pointsale, Product, Supplier, Unit, Sale, PosConfig, Purchase, SpecialPrice, ProductWaste, Seller, Transfer, Expense, User, Warehouse, Commission, Sellerscommission]
+      can :manage, [BillingInformation, Category, Commission, Customer, Expense, Expensesconcept, Pointsale, PosConfig, Product, Promotion, ProductWaste, Purchase, Sale, Seller, Sellerscommission, SpecialPrice, Supplier, Transfer, Unit, User, Warehouse]
       can [:opened_cash_registers, :find_cash_outs_by_date], CashOut
-      cannot [:create, :delete, :liquidate_reserve], Sale
-      cannot :create, ProductWaste
+      can :sales_reserved, Sale
+      cannot [:delete, :liquidate_reserve], Sale
+      cannot :create, [ProductWaste, Purchase, Sale]
+      cannot [:debtors, :customer_sales], Customer
     elsif user.usertype == "G"
       # Cajas registradoras
-      can :manage, [CashRegister, Purchase, Product, PrePurchase, Seller, Sale, Expense, ProductWaste, Transfer, OpenCashRegister, CashOut, Supplier, Customer, Credit, CreditPayment, Commission, Sellerscommission, ProductsReturn, Category]
+      can :manage, [CashOut, CashRegister, Category, Credit, CreditPayment, Commission, Customer, Expense, OpenCashRegister, PrePurchase, Product, Promotion, ProductsReturn, ProductWaste, Purchase, Sale, Seller, Sellerscommission, Supplier, Transfer]
       # Categorias
-      can :read, [SpecialPrice, Expensesconcept, Credit, CreditPayment, Unit]
+      can :read, [AvailableProduct, Credit, CreditPayment, Expensesconcept, SpecialPrice, Unit]
       # Clientes
-      can :cru, [Customer, BillingInformation, Pointsale, User, Warehouse, Credit, CreditPayment, Commission, Sellerscommission]
+      can :cru, [BillingInformation, Commission, Credit, CreditPayment, Customer, Sellerscommission, User, Warehouse]
 
       cannot :opened_cash_registers, CashOut
+      cannot :sales_per_month_report, Sale
+      cannot :product_track, Product
 
     elsif user.usertype == "C"
       # Cajas registradoras
-      can :read, [Product, Pointsale, Customer, BillingInformation, Seller, SpecialPrice, Expensesconcept, Credit, CreditPayment]
+      can :read, [BillingInformation, Credit, CreditPayment, Customer, Expensesconcept, Seller, SpecialPrice, Product, Promotion]
       # ventas
       can :cru, [Credit, CreditPayment]
 
-      can :manage, [CashRegister, PreSale, OpenCashRegister, Sale, Customer, Credit, CreditPayment, CashOut, Expense, Transfer, ProductsReturn, ProductWaste]
+      can :manage, [CashOut, CashRegister, Credit, CreditPayment, Customer, Expense, OpenCashRegister, PreSale, Sale, Transfer, ProductsReturn, ProductWaste]
 
       cannot :opened_cash_registers, CashOut
+      cannot :sales_per_month_report, Sale
 
     elsif user.usertype == "S"
-      can :read, [CashRegister, Product, Pointsale, Customer, BillingInformation, Seller, SpecialPrice, Expensesconcept]
+      can :read, [BillingInformation, CashRegister, Customer, Expensesconcept, Seller, SpecialPrice, Product, Promotion]
+      can :manage, [ProductWaste, Transfer]
 
-      can :manage, [Transfer, ProductWaste]
+      cannot [:debtors, :customer_sales], Customer
     end
   end
 end

+ 26 - 11
app/models/pre_sale.rb

@@ -17,20 +17,35 @@ class PreSale < ActiveRecord::Base
   # before_save :get_totals
 
   def get_totals
+    promotion = product.get_promotion
     special_price = SpecialPrice.find_by(customer_id: customer_id, product_id: product_id)
-    unless special_price.blank?
-      self.special_price_id = special_price.id
-      discount_per_unit = special_price.get_unit_discount(unit_price)
-      self.discount = discount_per_unit * quantity
-    end
-
-    self.discount += haggle
-    self.amount = quantity * unit_price
+    discount_per_unit =
+      if promotion.present?
+        self.promotion_id = promotion.id
+        (promotion.percent / 100) * unit_price
+      elsif special_price.present?
+        self.special_price_id = special_price.id
+        special_price.get_unit_discount(unit_price)
+      else
+        0
+      end
+    self.unit_price_w_discount = (unit_price - discount_per_unit).round(2)
+    self.discount = (discount_per_unit * quantity).round(2)
+    self.amount = (quantity * unit_price_w_discount)
     self.tax = 0.0
-    if product.include_sale_tax == 1
+    if product.include_sale_tax?
       self.tax = ((PosConfig.first.tax_percent / 100) * amount).round(2)
     end
-    self.total = (amount - discount) + tax
-    return true
+    haggle_discount =
+      if haggle > 0
+        haggle.round(2)
+      elsif haggle_percent > 0
+        ((haggle_percent.to_f / 100) * amount).round(2)
+      else
+        0
+      end
+    self.amount -= haggle_discount if haggle_discount.present?
+    self.total = amount + tax
+    true
   end
 end

+ 11 - 3
app/models/product.rb

@@ -13,6 +13,7 @@ class Product < ActiveRecord::Base
   has_many :special_prices
   has_many :pre_transfers
   has_many :available_products
+  has_many :promotions
 
   enum status: [:erased, :active, :inactive]
 
@@ -27,13 +28,13 @@ class Product < ActiveRecord::Base
   ##--- Validaciones previas de guardar
   validates :sku,
             presence: { message: "Debe capturar el SKU del producto." },
-            length: { maximum: 30, too_long: "El maximo de caracteres debe ser %{count}." },
+            length: { maximum: 30, too_long: "El máximo de caracteres para el SKU debe ser %{count}." },
             uniqueness: { message: "El SKU ya fue utilizado, favor de especificar otro." },
             unless: :skip_sku_validation
   validates_presence_of :name, message: "Debe capturar el nombre del producto."
   validates_presence_of :unit_id, message: "Debe seleccionar la unidad de medida correspondiente al producto."
   validates_presence_of :category_ids, message: "Debe elegir por lo menos una línea de producto relacionada al producto."
-  validates :barcode, uniqueness: { message: "El codigo de barras ya fue utilizado, favor de especificar otro." }, allow_blank: true
+  validates :barcode, uniqueness: { message: "El código de barras ya fue utilizado, favor de especificar otro." }, allow_blank: true
   validates_presence_of :name, message: "Debe capturar el nombre del producto."
   validates_presence_of :price_sale, message: "Debe capturar el precio de venta del producto."
 
@@ -63,6 +64,13 @@ class Product < ActiveRecord::Base
     sku.to_s + " - " + name.to_s
   end
 
+  def get_promotion
+    category_id = categories[0].parent.id
+    category_array = [category_id, categories.ids].flatten
+    product_ids = [id, parent_id]
+    promos = Promotion.where("product_id IN (?) OR category_id IN (?)", product_ids, category_array).order("percent, id DESC").vigentes.first
+  end
+
   def display_sku_name_attributes
     sku.to_s + " | " + name.to_s + " | " + display_attributes.to_s
   end
@@ -350,7 +358,7 @@ class Product < ActiveRecord::Base
   # rubocop:enable Metrics/BlockLength
 
   def children
-    Product.where("parent_id = ? and status != 0 ", id)
+    Product.where("parent_id = ? and status != ? ", id, 0)
   end
 
   def attributes_to_hash

+ 63 - 0
app/models/promotion.rb

@@ -0,0 +1,63 @@
+class Promotion < ActiveRecord::Base
+  ##--- Llevar registro de Actividad del usuario
+  audited
+
+  belongs_to :product
+  belongs_to :category
+  # belongs_to :subcategory_id, class_name: "Category"
+
+  enum status: [:inactive, :active]
+
+  validates_presence_of :start_date, message: "Debe indicar fecha de inicio de la promoción."
+  validates_presence_of :end_date, message: "Debe indicar fecha de término de la promoción."
+  validates_presence_of :percent, message: "Debe indicar el porcentaje de descuento de la promoción."
+  validates :percent, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 100 }
+  validate :start_end_date, on: [:create]
+
+  validate :start_date_greater_than_end_date, if: proc { |p| p.start_date.present? && p.end_date.present? }
+  validate :promotion_source
+  validate :check_for_duplicates
+
+  scope :activas, -> { where(status: 1) }
+  scope :vigentes, -> { where("? between start_date and end_date", Date.today) }
+
+  def verify_promotion
+    is_active = end_date >= Date.today
+    unless is_active
+      update_attribute(:status, :inactive)
+    end
+  end
+
+  def start_end_date
+    if end_date.present? && end_date < Date.today
+      errors.add(:end_date, "La fecha de fin debe ser mayor a hoy.")
+    end
+  end
+
+  def start_date_greater_than_end_date
+    if end_date < start_date
+      errors.add(:start_date, 'La fecha de inicio debe ser mayor a la fecha de término de la promoción.')
+    end
+  end
+
+  def promotion_source
+    if product_id.blank? && category_id.blank?
+      errors.add(:base, 'Seleccione una opción entre línea o producto.')
+    end
+  end
+
+  def check_for_duplicates
+    source =
+      if category_id.present?
+        "category_id = #{category_id}"
+      elsif subcategory_id.present?
+        "subcategory_id = #{subcategory_id}"
+      elsif product_id.present?
+        "product_id = #{product_id}"
+      end
+    if source
+      exists_already = Promotion.where(source).where("start_date < ? and end_date > ?", end_date, start_date).any?
+      errors.add(:base, 'Ya existe una promoción activa con los parámetros seleccionados') if exists_already
+    end
+  end
+end

+ 1 - 1
app/views/pre_sales/_pre_sale.html.erb

@@ -19,7 +19,7 @@
   <td><%= pre_sale.discount %></td>
   <td><%= pre_sale.total %></td>
   <td style="width:5%">
-    <% if @pos_config.enable_haggle? && pre_sale.haggle == 0 %>
+    <% if @pos_config.enable_haggle? && pre_sale.haggle.zero? && pre_sale.haggle_percent.zero? %>
       <%= link_to add_sale_haggle_path(pre_sale), :remote => true, :class => "btn btn-icon-only btn-primary hagglebutton", :title=>"Agregar regate" do %>
         <i class="fa fa-tag"></i>
       <% end %>

+ 262 - 0
app/views/promotions/_form.html.erb

@@ -0,0 +1,262 @@
+<%= form_for(@promotion, html: { class: "form-horizontal", id: "promotion_form" }) do |f| %>
+  <div class="portlet-body form">
+    <% if @promotion.errors.any? %>
+      <div class="alert alert-danger">
+        <strong>Tiene <%= pluralize(@promotion.errors.count, "error") %> no se puede guardar la promoción</strong><br>
+        <% @promotion.errors.values.each do |message| %>
+          <%= message.first.to_s %><br>
+           <!--  $("#error_explanation_move").append($("<li />").html("< %= message.first.to_s %>")); -->
+        <% end %>
+      </div>
+    <% end %>
+    <div class="form-body">
+      <div class="alert alert-danger hidden" id="error_explanation_for_promotion"></div>
+      <div class="row">
+        <div class="col-md-12">
+          <!-- fechas -->
+          <div class="form-group">
+            <%= label_tag :begin_date, "Fecha", { class: "col-md-1 control-label" } do %> Del
+              <span class="required">*</span>
+            <% end %>
+            <div class="col-sm-2">
+              <div class='input-group date' id='begin_date'>
+                <!-- <input id="start" type='text' class="form-control"/> -->
+                <%= f.text_field :start_date, { id: 'start', class: 'form-control' } %>
+                <span class="input-group-addon">
+                  <span class="glyphicon glyphicon-calendar"></span>
+                </span>
+                <!-- < %= f.hidden_field :start_date, {id: 'start_date_input'} %> -->
+              </div>
+            </div>
+            <%= label_tag :end_date, "Fecha", { class: "col-md-1 control-label" } do %> Al
+              <span class="required">*</span>
+            <% end %>
+            <div class="col-sm-2">
+              <div class='input-group date' id='end_date'>
+                <!-- <input id="end" type='text' class="form-control"/> -->
+                <%= f.text_field :end_date, { id: 'end', class: 'form-control' } %>
+                <span class="input-group-addon">
+                  <span class="glyphicon glyphicon-calendar"></span>
+                </span>
+                <!-- < %= f.hidden_field :end_date, {id: 'end_date_input'} %> -->
+              </div>
+            </div>
+          </div>
+          <!-- filtro -->
+          <div class="form-group">
+            <div class="col-md-8" style="padding-top:5px">
+              <div class="radio-list">
+                <label class="radio-inline" style="margin-left:10px">
+                  <input type="radio" name="filter" id="category" value="category">Todos los productos de una línea
+                </label>
+                <label class="radio-inline" style="margin-left:37px">
+                  <input type="radio" name="filter" id="products" value="products" checked>Producto específico
+                </label>
+              </div>
+            </div>
+          </div>
+          <div class="form-group">
+            <%= f.label :percent, "", { class: "col-md-2 control-label" } do %> Porcentaje de descuento <span class="required">*</span> <% end %>
+            <div class="col-md-1">
+              <div class="input-group">
+                <%= f.text_field :percent, { class: "form-control mask_decimal input-xsmall", value: 1 } %>
+                <span class="input-group-addon"> % </span>
+              </div>
+            </div>
+          </div>
+          <div class="form-group" id="variants_div">
+            <%= label_tag :variants, "", { class: "col-md-3 control-label" } do %> ¿Buscar por variantes? <span class="required">*</span> <% end %>
+            <div class="col-md-3">
+              <%= check_box_tag(:include_variants, true, true, {
+                class: "make-switch",
+                  data: {
+                    on_color: "success",
+                    off_color: "danger",
+                    on_text: "Si",
+                    off_text: "No"
+                  }
+                }) %>
+            </div>
+          </div>
+          <div class="form-group">
+            <div class="well col-md-6">
+              <!-- linea -->
+              <div class="form-group hidden" id="category_div">
+                <%= f.label :category_id, "", { class: "col-md-3 control-label" } do %> Línea de productos
+                  <span class="required">*</span>
+                <% end %>
+                <div class="col-md-8 select2-bootstrap-prepend">
+                  <%= f.collection_select(:category_id, Category.activos_padre, :id, :category, {}, class: "form-control select2") %>
+                  <span class="help-block">Se agregarán todos los productos de la línea a la tabla.</span>
+                </div>
+              </div>
+              <div class="form-group hidden" id="subcategory_div">
+                <%= f.label :subcategory_id, "Sublínea", { class: "col-md-3 col-sm-3 control-label" } %>
+                <div class="col-md-8 select2-bootstrap-prepend col-sm-4">
+                  <%= f.collection_select(:subcategory_id, {}, :id, :category, {}, class: "form-control select2") %>
+                </div>
+              </div>
+              <!-- producto -->
+              <div class="form-group" id="products_div">
+                <label class="control-label col-md-3" for="typeahead"> Producto
+                  <span class="required">*</span>
+                </label>
+                <div class="input-groupll col-md-8">
+                  <input class="form-control" type="text" id="typeahead" data-option-url="/find_products/%QUERY/VAR">
+                  <span class="help-block">Se puede buscar por No. de parte o Nombre de producto </span>
+                  <%= f.hidden_field :product_id, { id: 'product_input' } %>
+                  <br>
+                  <span id="product_name"><% if @promotion.product.present? %><i>Producto seleccionado: <%= @promotion.product.display_sku_name_attributes %></i><% end %></span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  <!-- acciones del form -->
+    <div class="form-actions">
+      <div class="row">
+        <div class="col-md-9">
+          <!-- < %= f.submit 'Guardar', { class: "btn green" } %> -->
+          <button type="button" class="btn green" onclick="addPromotion()" id="submit_promotion">Guardar</button>
+          <%= link_to 'Cancelar', promotions_path, { class: "btn default" } %>
+        </div>
+      </div>
+    </div>
+  </div>
+<% end %>
+
+<script type="text/javascript">
+  var selectedProduct;
+  // inicialmente dejar en blanco los selects
+  $(document).on("page:change", function() {
+    <% unless @filter.present? && @filter == "category" %>
+      $("#promotion_category_id").select2("val", "");
+      $("#promotion_subcategory_id").select2("val", "");
+    <% end %>
+  });
+
+  $('input[name="include_variants"]').on('switchChange.bootstrapSwitch', function(event, state) {
+    $(this).val(state);
+  });
+
+  $('#begin_date').datetimepicker({
+    icons: {
+      date: "fa fa-calendar"
+    },
+    format: "DD/MM/YYYY"
+  });
+
+  $('#end_date').datetimepicker({
+    icons: {
+      date: "fa fa-calendar"
+    },
+    format: "DD/MM/YYYY"
+  });
+
+  var bloodhound = new Bloodhound({
+    datumTokenizer: function (d) {
+      return Bloodhound.tokenizers.whitespace(d.value);
+    },
+    queryTokenizer: Bloodhound.tokenizers.whitespace,
+    remote: {
+      url: $('#typeahead').data('option-url'),
+      wildcard: '%QUERY',
+      replace: function(url, uriEncodedQuery) {
+        variants = $("#include_variants").val();
+        return url.replace('VAR', variants).replace('%QUERY', uriEncodedQuery)
+      }
+    }
+  });
+  bloodhound.initialize();
+
+  $('#typeahead').typeahead(
+    {
+      minLength: 3
+    },
+    {
+      displayKey: 'name',
+      source: bloodhound.ttAdapter(),
+      limit: Infinity,
+      templates: {
+        empty: [
+          '<div class="empty-message">',
+            'No se encontró ningún producto. Favor de verificar',
+          '</div>'
+        ].join('\n'),
+        suggestion: function(data) {
+          return '<div class="media">' +
+                  '<div class="pull-left">' +
+                    '<div class="media-object">' +
+                      '<img src="'+ data.small_img +'" width="50" height="50"/>' +
+                    '</div>' +
+                  '</div>' +
+                  '<div class="media-body">' +
+                    '<h4 class="media-heading"><strong>'+ data.sku +'</strong> | '+ data.name +'</h4>' +
+                    '<p>'+ data.barcode +'</p>' +
+                    '<p>'+ data.description +'</p>' +
+                    '<p>'+ data.display_attributes +'</p>' +
+                  '</div>' +
+                '</div>'
+        }
+      }
+    }
+  );
+
+  // this is the event that is fired when a user clicks on a suggestion
+  $('#typeahead').bind('typeahead:selected', function(event, datum, name) {
+    console.log(selectedProduct)
+    selectedProduct = datum;
+    $('#product_input').val(selectedProduct.id);
+    $("#product_name").html("<i>Producto seleccionado: "+selectedProduct.display_sku_name_attributes+"</i>")
+  });
+
+  $('input[type=radio][name=filter]').change(function() {
+    switch(this.value) {
+      case 'category':
+        $('#category_div').removeClass('hidden');
+        $("#subcategory_div").removeClass("hidden");
+        $('#products_div').addClass('hidden');
+        $("#variants_div").addClass('hidden');
+
+        $('#product_input').val('');
+        break;
+      case 'products':
+        $('#category_div').addClass('hidden');
+        $("#subcategory_div").addClass("hidden");
+        $('#products_div').removeClass('hidden');
+        $("#variants_div").removeClass('hidden');
+
+        $("#promotion_category_id").select2("val", "");
+        break;
+    }
+    $('.select2').select2();
+  });
+
+  $('#promotion_category_id').on('change', function() {
+    if($(this).val()) {
+      $.ajax({
+        type: "get",
+        url: '/getcategories/' + $(this).val(),
+        dataType: 'json',
+        success: function(data) {
+          $('#promotion_subcategory_id').empty();
+          if(data.length > 0) {
+            $('#promotion_subcategory_id').attr('disabled', false);
+            $('#promotion_subcategory_id').append("<option selected value='0'>Seleccione</option>")
+          } else {
+            $('#promotion_subcategory_id').attr('disabled', true);
+          }
+          for(i in data){
+            $('#promotion_subcategory_id').append("<option value='" + data[i].id +"'>" + data[i].category + "</option>")
+          }
+        }
+      });
+    }
+  });
+
+  function addPromotion() {
+    $('#promotion_form').submit();
+  }
+</script>

+ 116 - 0
app/views/promotions/index.html.erb

@@ -0,0 +1,116 @@
+<!-- BEGIN CONTAINER -->
+<div class="page-container">
+  <!-- BEGIN CONTENT -->
+  <div class="page-content-wrapper">
+    <!-- BEGIN CONTENT BODY -->
+    <!-- BEGIN PAGE HEAD-->
+    <div class="page-head">
+      <div class="container-fluid">
+        <!-- BEGIN PAGE TITLE -->
+        <div class="page-title">
+          <h1>Promociones</h1>
+        </div>
+        <!-- END PAGE TITLE -->
+      </div>
+    </div>
+    <!-- END PAGE HEAD-->
+    <!-- BEGIN PAGE CONTENT BODY -->
+    <div class="page-content">
+      <div class="container-fluid">
+        <!-- BEGIN PAGE BREADCRUMBS -->
+        <ul class="page-breadcrumb breadcrumb">
+          <%= render_breadcrumbs :tag => :li, :separator => ' <i class="fa fa-circle"></i> ' %>
+        </ul>
+        <!-- END PAGE BREADCRUMBS -->
+        <!-- BEGIN PAGE CONTENT INNER -->
+        <div class="page-content-inner">
+          <div id="notice">
+          <% if success %>
+            <div class="alert alert-success">
+              <p><%= success %></p>
+            </div>
+          <% elsif warning %>
+            <div class="alert alert-warning">
+              <p><%= warning %></p>
+            </div>
+          <% end %>
+          </div>
+          <div class="row">
+            <div class="col-md-12">
+              <div class="portlet light">
+                <div class="portlet-title">
+                  <div class="caption">
+                    <i class="fa fa-list"></i>
+                    <span class="caption-subject bold uppercase">Lista de promociones</span>
+                  </div>
+                  <div class="actions">
+                    <% if can? :create, Promotion %>
+                      <%= link_to new_promotion_path, { class: "btn bold green pull-right" } do %>
+                        Nueva promoción <i class="fa fa-plus"></i>
+                      <% end %>
+                    <% end %>
+                  </div>
+                </div>
+                <div class="portlet-body">
+                  <table class="table table-striped table-bordered table-hover tableadvanced">
+                    <thead>
+                      <tr>
+                        <th>#</th>
+                        <th>Periodo</th>
+                        <th>Descuento</th>
+                        <th>Línea / Sublínea</th>
+                        <th>Producto</th>
+                        <th>Status</th>
+                        <th>Acciones</th>
+                      </tr>
+                    </thead>
+                    <tbody>
+                      <% @promotions.each_with_index do |promotion, key| %>
+                        <tr>
+                          <td><%= key + 1 %></td>
+                          <td>
+                            Del <strong><%= l(promotion.start_date, format: '%d/%B/%Y') %></strong> al <strong><%= l(promotion.end_date, format: '%d/%B/%Y') %></strong>
+                          </td>
+                          <td> <%= promotion.percent %>%</td>
+                          <td>
+                            <%=
+                              if promotion.subcategory_id.present?
+                                Category.find(promotion.subcategory_id).category
+                              elsif promotion.category_id.present?
+                                promotion.category.category
+                              else
+                                ""
+                              end %>
+                          </td>
+                          <td> <%= promotion.product.present? ? promotion.product.full_display : '' %> </td>
+                          <td class="text-center">
+                            <% if promotion.active? %>
+                              <span class="label label-success">Vigente</span>
+                            <% else %>
+                              <span class="label label-danger">NO vigente</span>
+                            <% end %>
+                          </td>
+                          <td>
+                            <% if can? :destroy, Promotion %>
+                              <%= link_to promotion, method: :delete, class: "btn btn-icon-only btn-danger", title: "Eliminar promoción", data: { confirm: '¿Está seguro de eliminar la promoción?' } do %>
+                                <i class="fa fa-trash-o"></i>
+                              <% end %>
+                            <% end %>
+                          </td>
+                        </tr>
+                      <% end %>
+                    </tbody>
+                  </table>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+        <!-- END PAGE CONTENT INNER -->
+      </div>
+    </div>
+    <!-- END PAGE CONTENT BODY -->
+    <!-- END CONTENT BODY -->
+  </div>
+  <!-- END CONTENT -->
+</div>

+ 54 - 0
app/views/promotions/new.html.erb

@@ -0,0 +1,54 @@
+<!-- BEGIN CONTAINER -->
+<div class="page-container">
+  <!-- BEGIN CONTENT -->
+  <div class="page-content-wrapper">
+    <!-- BEGIN CONTENT BODY -->
+    <!-- BEGIN PAGE HEAD-->
+    <div class="page-head">
+      <div class="container-fluid">
+        <!-- BEGIN PAGE TITLE -->
+        <div class="page-title">
+          <h1> Promociones</h1>
+        </div>
+        <!-- END PAGE TITLE -->
+      </div>
+    </div>
+    <!-- END PAGE HEAD-->
+    <!-- BEGIN PAGE CONTENT BODY -->
+    <div class="page-content">
+      <div class="container-fluid">
+        <%= link_to promotions_path, { class: "btn blue-hoki pull-right margin-bottom-10" } do %>
+          <i class="fa fa-angle-left "></i>
+          Regresar
+        <% end %>
+        <!-- BEGIN PAGE BREADCRUMBS -->
+        <ul class="page-breadcrumb breadcrumb">
+          <%= render_breadcrumbs :tag => :li, :separator => ' <i class="fa fa-circle"></i> ' %>
+        </ul>
+        <!-- END PAGE BREADCRUMBS -->
+        <!-- BEGIN PAGE CONTENT INNER -->
+        <div class="page-content-inner">
+          <div id="notice"><%= notice %></div>
+          <div class="row">
+            <div class="col-md-12">
+              <div class="portlet light ">
+                <div class="portlet-title">
+                  <div class="caption">
+                    <i class="fa fa-plus font-green"></i>
+                    <span class="caption-subject font-green bold uppercase">Nueva promoción</span>
+                    <span class="caption-helper"></span>
+                  </div>
+                </div>
+                <%= render 'form' %>
+              </div>
+            </div>
+          </div>
+        </div>
+        <!-- END PAGE CONTENT INNER -->
+      </div>
+    </div>
+    <!-- END PAGE CONTENT BODY -->
+    <!-- END CONTENT BODY -->
+  </div>
+  <!-- END CONTENT -->
+</div>

+ 1 - 0
config/initializers/inflections.rb

@@ -14,5 +14,6 @@
 ActiveSupport::Inflector.inflections do |inflect|
   # inflect.acronym 'RESTful'
   inflect.irregular "error", "errores"
+  inflect.irregular "promoción", "promociones"
 end
 

+ 2 - 1
config/locales/en.yml

@@ -279,9 +279,10 @@ en:
     warehouses: Almacenes
     commissions: Comisiones
     products_returns: Devoluciones
+    promotions: Promociones
     # user:
     #   pwd: Cambio de contraseña
   dictionary:
     sizes: Talla
-    colors: Color 
+    colors: Color
     styles: Estilo

+ 3 - 0
config/routes.rb

@@ -101,6 +101,7 @@ Rails.application.routes.draw do
   end
   get 'validate_unique_barcode/:barcode' => 'products#validate_unique_barcode', defaults: { format: 'js' }
   get 'product_track' => 'products#product_track', :as => "product_track"
+  get 'find_products/:query/:variants' => 'application#find_products', as: "find_products", format: :json
 
   resources :available_products, except: [:edit, :update] do
     get 'edit_price' => 'available_products#edit_price'
@@ -255,5 +256,7 @@ Rails.application.routes.draw do
   ## soporte
   get "contact_support" => "supports#contact_support"
   get "system_updates" => "supports#system_updates"
+
+  resources :promotions, except: [:show, :update, :edit]
 end
 # rubocop:enable Metrics/BlockLength

+ 14 - 0
db/migrate/20180816020521_create_promotions.rb

@@ -0,0 +1,14 @@
+class CreatePromotions < ActiveRecord::Migration
+  def change
+    create_table :promotions do |t|
+      t.date :start_date, null: false
+      t.date :end_date, null: false
+      t.decimal :percent, default: 0
+      t.belongs_to :product, index: true
+      t.belongs_to :category, index: true
+      t.belongs_to :user, index: true
+      t.integer :status, null: false, default: 1
+      t.timestamps null: false
+    end
+  end
+end

+ 5 - 0
db/migrate/20180816022444_add_promotion_id_to_pre_sale.rb

@@ -0,0 +1,5 @@
+class AddPromotionIdToPreSale < ActiveRecord::Migration
+  def change
+    add_column :pre_sales, :promotion_id, :integer, index: true, null: true unless column_exists? :pre_sales, :promotion_id
+  end
+end

+ 6 - 0
db/migrate/20180816171401_add_unit_price_w_discount_to_pre_sale.rb

@@ -0,0 +1,6 @@
+class AddUnitPriceWDiscountToPreSale < ActiveRecord::Migration
+  def change
+    add_column :pre_sales, :unit_price_w_discount, :decimal, precision: 10, scale: 2, null: true
+    add_column :sales_details, :unit_price_w_discount, :decimal, precision: 10, scale: 2, null: true
+  end
+end

+ 5 - 0
db/migrate/20180816181027_add_sub_category_to_promotion.rb

@@ -0,0 +1,5 @@
+class AddSubCategoryToPromotion < ActiveRecord::Migration
+  def change
+    add_column :promotions, :subcategory_id, :integer, null: true
+  end
+end

+ 5 - 0
db/migrate/20180817233420_add_haggle_percent_to_pre_sale.rb

@@ -0,0 +1,5 @@
+class AddHagglePercentToPreSale < ActiveRecord::Migration
+  def change
+    add_column :pre_sales, :haggle_percent, :integer, default: 0, null: true
+  end
+end

+ 31 - 10
db/schema.rb

@@ -11,7 +11,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20180727152745) do
+ActiveRecord::Schema.define(version: 20180817233420) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -383,6 +383,9 @@ ActiveRecord::Schema.define(version: 20180727152745) do
     t.integer  "special_price_id"
     t.decimal  "unit_price",                      precision: 10, scale: 2
     t.decimal  "haggle",                          precision: 10, scale: 2, default: 0.0
+    t.integer  "promotion_id"
+    t.decimal  "unit_price_w_discount",           precision: 10, scale: 2
+    t.integer  "haggle_percent",                                           default: 0
   end
 
   create_table "pre_transfers", force: :cascade do |t|
@@ -502,6 +505,23 @@ ActiveRecord::Schema.define(version: 20180727152745) do
 
   add_index "products_variants", ["product_id"], name: "index_products_variants_on_product_id", using: :btree
 
+  create_table "promotions", force: :cascade do |t|
+    t.date     "start_date",                   null: false
+    t.date     "end_date",                     null: false
+    t.decimal  "percent",        default: 0.0
+    t.integer  "product_id"
+    t.integer  "category_id"
+    t.integer  "user_id"
+    t.integer  "status",         default: 1,   null: false
+    t.datetime "created_at",                   null: false
+    t.datetime "updated_at",                   null: false
+    t.integer  "subcategory_id"
+  end
+
+  add_index "promotions", ["category_id"], name: "index_promotions_on_category_id", using: :btree
+  add_index "promotions", ["product_id"], name: "index_promotions_on_product_id", using: :btree
+  add_index "promotions", ["user_id"], name: "index_promotions_on_user_id", using: :btree
+
   create_table "purchase_details", force: :cascade do |t|
     t.integer  "purchase_id"
     t.integer  "product_id"
@@ -561,16 +581,17 @@ ActiveRecord::Schema.define(version: 20180727152745) do
   create_table "sales_details", force: :cascade do |t|
     t.integer  "sale_id"
     t.integer  "product_id"
-    t.decimal  "quantity",                                                null: false
-    t.decimal  "amount",           precision: 10, scale: 2, default: 0.0, null: false
-    t.decimal  "tax",              precision: 10, scale: 2, default: 0.0, null: false
-    t.decimal  "discount",         precision: 10, scale: 2, default: 0.0, null: false
-    t.decimal  "total",            precision: 10, scale: 2, default: 0.0, null: false
-    t.integer  "status",                                    default: 1,   null: false
-    t.datetime "created_at",                                              null: false
-    t.datetime "updated_at",                                              null: false
+    t.decimal  "quantity",                                                     null: false
+    t.decimal  "amount",                precision: 10, scale: 2, default: 0.0, null: false
+    t.decimal  "tax",                   precision: 10, scale: 2, default: 0.0, null: false
+    t.decimal  "discount",              precision: 10, scale: 2, default: 0.0, null: false
+    t.decimal  "total",                 precision: 10, scale: 2, default: 0.0, null: false
+    t.integer  "status",                                         default: 1,   null: false
+    t.datetime "created_at",                                                   null: false
+    t.datetime "updated_at",                                                   null: false
     t.integer  "special_price_id"
-    t.decimal  "unit_price",       precision: 10, scale: 2
+    t.decimal  "unit_price",            precision: 10, scale: 2
+    t.decimal  "unit_price_w_discount", precision: 10, scale: 2
   end
 
   add_index "sales_details", ["product_id"], name: "index_sales_details_on_product_id", using: :btree