mirror of
https://github.com/System-End/theseus.git
synced 2026-04-19 15:28:19 +00:00
POs?
This commit is contained in:
parent
433597368b
commit
b544ee4653
18 changed files with 650 additions and 1 deletions
103
app/controllers/warehouse/purchase_orders_controller.rb
Normal file
103
app/controllers/warehouse/purchase_orders_controller.rb
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Warehouse::PurchaseOrdersController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_purchase_order, except: %i[new create index]
|
||||
|
||||
def index
|
||||
authorize Warehouse::PurchaseOrder
|
||||
@purchase_orders = policy_scope(Warehouse::PurchaseOrder).includes(:user, :line_items).order(created_at: :desc)
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @purchase_order
|
||||
end
|
||||
|
||||
def new
|
||||
authorize Warehouse::PurchaseOrder
|
||||
@purchase_order = Warehouse::PurchaseOrder.new
|
||||
@purchase_order.line_items.build
|
||||
end
|
||||
|
||||
def create
|
||||
@purchase_order = Warehouse::PurchaseOrder.new(
|
||||
purchase_order_params.merge(user: current_user)
|
||||
)
|
||||
authorize @purchase_order
|
||||
|
||||
if @purchase_order.save
|
||||
redirect_to warehouse_purchase_order_path(@purchase_order), notice: "Purchase order was successfully created."
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize @purchase_order
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @purchase_order
|
||||
|
||||
if @purchase_order.update(purchase_order_params)
|
||||
redirect_to warehouse_purchase_order_path(@purchase_order), notice: "Purchase order was successfully updated."
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @purchase_order
|
||||
|
||||
@purchase_order.destroy!
|
||||
redirect_to warehouse_purchase_orders_path, status: :see_other, notice: "Purchase order was deleted."
|
||||
end
|
||||
|
||||
def send_to_zenventory
|
||||
authorize @purchase_order
|
||||
|
||||
begin
|
||||
@purchase_order.dispatch!
|
||||
rescue Zenventory::ZenventoryError => e
|
||||
event_id = Sentry.capture_exception(e)&.event_id
|
||||
redirect_to warehouse_purchase_order_path(@purchase_order), alert: "Zenventory said \"#{e.message}\" (error: #{event_id})"
|
||||
return
|
||||
rescue AASM::InvalidTransition => e
|
||||
event_id = Sentry.capture_exception(e)&.event_id
|
||||
redirect_to warehouse_purchase_order_path(@purchase_order), alert: "Couldn't dispatch purchase order! Wrong state? (error: #{event_id})"
|
||||
return
|
||||
end
|
||||
|
||||
redirect_to warehouse_purchase_order_path(@purchase_order), flash: { success: "Successfully sent to Zenventory!" }
|
||||
end
|
||||
|
||||
def sync
|
||||
authorize @purchase_order
|
||||
|
||||
begin
|
||||
@purchase_order.sync_from_zenventory!
|
||||
rescue Zenventory::ZenventoryError => e
|
||||
event_id = Sentry.capture_exception(e)&.event_id
|
||||
redirect_to warehouse_purchase_order_path(@purchase_order), alert: "Zenventory said \"#{e.message}\" (error: #{event_id})"
|
||||
return
|
||||
end
|
||||
|
||||
redirect_to warehouse_purchase_order_path(@purchase_order), flash: { success: "Synced from Zenventory!" }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_purchase_order
|
||||
@purchase_order = Warehouse::PurchaseOrder.find(params[:id])
|
||||
end
|
||||
|
||||
def purchase_order_params
|
||||
params.require(:warehouse_purchase_order).permit(
|
||||
:supplier_name,
|
||||
:supplier_id,
|
||||
:notes,
|
||||
:required_by_date,
|
||||
line_items_attributes: %i[id sku_id quantity unit_cost _destroy]
|
||||
)
|
||||
end
|
||||
end
|
||||
4
app/helpers/warehouse/purchase_orders_helper.rb
Normal file
4
app/helpers/warehouse/purchase_orders_helper.rb
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Warehouse::PurchaseOrdersHelper
|
||||
end
|
||||
128
app/models/warehouse/purchase_order.rb
Normal file
128
app/models/warehouse/purchase_order.rb
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: warehouse_purchase_orders
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# notes :text
|
||||
# order_number :string
|
||||
# required_by_date :date
|
||||
# status :string default("draft")
|
||||
# supplier_name :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# supplier_id :integer
|
||||
# user_id :bigint not null
|
||||
# zenventory_id :integer
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_warehouse_purchase_orders_on_order_number (order_number)
|
||||
# index_warehouse_purchase_orders_on_user_id (user_id)
|
||||
# index_warehouse_purchase_orders_on_zenventory_id (zenventory_id) UNIQUE
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (user_id => users.id)
|
||||
#
|
||||
class Warehouse::PurchaseOrder < ApplicationRecord
|
||||
include AASM
|
||||
include HasZenventoryUrl
|
||||
|
||||
belongs_to :user
|
||||
has_many :line_items, class_name: "Warehouse::PurchaseOrderLineItem", foreign_key: :purchase_order_id, dependent: :destroy, inverse_of: :purchase_order
|
||||
|
||||
accepts_nested_attributes_for :line_items, allow_destroy: true, reject_if: :all_blank
|
||||
|
||||
validates :supplier_name, presence: true
|
||||
validates :line_items, presence: true
|
||||
|
||||
has_zenventory_url "https://app.zenventory.com/purchasing/purchase-order/%s", :zenventory_id
|
||||
|
||||
HUMANIZED_STATES = {
|
||||
draft: "Draft",
|
||||
open: "Open",
|
||||
completed: "Completed",
|
||||
deleted: "Deleted"
|
||||
}.freeze
|
||||
|
||||
def humanized_state
|
||||
HUMANIZED_STATES[status&.to_sym] || status
|
||||
end
|
||||
|
||||
aasm column: :status, timestamps: true do
|
||||
state :draft, initial: true
|
||||
state :open
|
||||
state :completed
|
||||
state :deleted
|
||||
|
||||
event :mark_open do
|
||||
transitions from: :draft, to: :open
|
||||
end
|
||||
|
||||
event :mark_completed do
|
||||
transitions from: :open, to: :completed
|
||||
end
|
||||
|
||||
event :mark_deleted do
|
||||
transitions from: %i[draft open], to: :deleted
|
||||
end
|
||||
end
|
||||
|
||||
def draft?
|
||||
status == "draft"
|
||||
end
|
||||
|
||||
def open?
|
||||
status == "open"
|
||||
end
|
||||
|
||||
def completed?
|
||||
status == "completed"
|
||||
end
|
||||
|
||||
def dispatch!
|
||||
ActiveRecord::Base.transaction do
|
||||
raise AASM::InvalidTransition, "wrong state" unless may_mark_open?
|
||||
|
||||
po_params = {
|
||||
supplier: { id: supplier_id, name: supplier_name }.compact,
|
||||
requiredByDate: required_by_date&.iso8601,
|
||||
notes: notes,
|
||||
items: line_items.map do |li|
|
||||
{
|
||||
sku: li.sku.sku,
|
||||
quantity: li.quantity,
|
||||
unitCost: li.unit_cost&.to_f
|
||||
}.compact
|
||||
end
|
||||
}.compact
|
||||
|
||||
response = Zenventory.create_purchase_order(po_params)
|
||||
update!(zenventory_id: response[:id], order_number: response[:orderNumber])
|
||||
mark_open!
|
||||
end
|
||||
end
|
||||
|
||||
def sync_from_zenventory!
|
||||
return unless zenventory_id.present?
|
||||
|
||||
zenv_po = Zenventory.get_purchase_order(zenventory_id)
|
||||
|
||||
self.supplier_name = zenv_po.dig(:supplier, :name) || supplier_name
|
||||
self.supplier_id = zenv_po.dig(:supplier, :id) || supplier_id
|
||||
self.order_number = zenv_po[:orderNumber] || order_number
|
||||
self.notes = zenv_po[:notes] if zenv_po[:notes].present?
|
||||
self.required_by_date = Date.parse(zenv_po[:requiredByDate]) if zenv_po[:requiredByDate].present?
|
||||
|
||||
zenv_status = zenv_po[:status]&.downcase
|
||||
self.status = zenv_status if %w[draft open completed deleted].include?(zenv_status)
|
||||
|
||||
save!
|
||||
end
|
||||
|
||||
def total_cost
|
||||
line_items.sum { |li| (li.unit_cost || 0) * li.quantity }
|
||||
end
|
||||
end
|
||||
30
app/models/warehouse/purchase_order_line_item.rb
Normal file
30
app/models/warehouse/purchase_order_line_item.rb
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: warehouse_purchase_order_line_items
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# quantity :integer not null
|
||||
# unit_cost :decimal(10, 2)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# purchase_order_id :bigint not null
|
||||
# sku_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_warehouse_purchase_order_line_items_on_purchase_order_id (purchase_order_id)
|
||||
# index_warehouse_purchase_order_line_items_on_sku_id (sku_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (purchase_order_id => warehouse_purchase_orders.id)
|
||||
# fk_rails_... (sku_id => warehouse_skus.id)
|
||||
#
|
||||
class Warehouse::PurchaseOrderLineItem < ApplicationRecord
|
||||
belongs_to :purchase_order, class_name: "Warehouse::PurchaseOrder", inverse_of: :line_items
|
||||
belongs_to :sku, class_name: "Warehouse::SKU"
|
||||
|
||||
validates :quantity, presence: true, numericality: { greater_than: 0 }
|
||||
end
|
||||
49
app/policies/warehouse/purchase_order_policy.rb
Normal file
49
app/policies/warehouse/purchase_order_policy.rb
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Warehouse::PurchaseOrderPolicy < ApplicationPolicy
|
||||
def index?
|
||||
user_is_admin
|
||||
end
|
||||
|
||||
def show?
|
||||
user_is_admin
|
||||
end
|
||||
|
||||
def new?
|
||||
user_is_admin
|
||||
end
|
||||
|
||||
def create?
|
||||
user_is_admin
|
||||
end
|
||||
|
||||
def edit?
|
||||
user_is_admin
|
||||
end
|
||||
|
||||
def update?
|
||||
user_is_admin
|
||||
end
|
||||
|
||||
def destroy?
|
||||
user_is_admin && record.draft?
|
||||
end
|
||||
|
||||
def send_to_zenventory?
|
||||
user_is_admin && record.draft?
|
||||
end
|
||||
|
||||
def sync?
|
||||
user_is_admin && record.zenventory_id.present?
|
||||
end
|
||||
|
||||
class Scope < ApplicationPolicy::Scope
|
||||
def resolve
|
||||
if user&.admin?
|
||||
scope.all
|
||||
else
|
||||
scope.none
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -82,14 +82,26 @@ class Zenventory
|
|||
conn.post("purchase-orders", **params).body
|
||||
end
|
||||
|
||||
def draft_purchase_order(params = {})
|
||||
create_purchase_order(draft: true, **params)
|
||||
end
|
||||
|
||||
def update_purchase_order(id, params = {})
|
||||
conn.put("purchase-orders/#{id}", **params).body
|
||||
end
|
||||
|
||||
def finalize_purchase_order(id, params = {})
|
||||
update_purchase_order(id, draft: false, **params)
|
||||
end
|
||||
|
||||
def close_purchase_order(id)
|
||||
conn.put("purchase-orders/#{id}/close").body
|
||||
end
|
||||
|
||||
def get_suppliers
|
||||
get_purchase_orders.map { |po| po[:supplier] }.compact.uniq { |s| s[:id] }
|
||||
end
|
||||
|
||||
def run_report(category, report_key, params = {})
|
||||
CSV.parse(
|
||||
conn.get("reports/#{category}/#{report_key}", csv: true, **params).body,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
<%= nav_item(warehouse_batches_path, "Batches") %>
|
||||
<%= nav_item(warehouse_skus_path, "SKUs") %>
|
||||
<%= nav_item(warehouse_orders_path, "Orders") %>
|
||||
<%= nav_item(warehouse_purchase_orders_path, "Purchase Orders") %>
|
||||
<%= nav_item(warehouse_templates_path, "Order templates") %>
|
||||
</ul>
|
||||
</details>
|
||||
|
|
|
|||
38
app/views/warehouse/purchase_orders/_form.html.slim
Normal file
38
app/views/warehouse/purchase_orders/_form.html.slim
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
- content_for :head
|
||||
= vite_javascript_tag 'cocoon'
|
||||
|
||||
= form_with(model: purchase_order, url: purchase_order.persisted? ? warehouse_purchase_order_path(purchase_order) : warehouse_purchase_orders_path) do |form|
|
||||
- if purchase_order.errors.any?
|
||||
= render layout: 'shared/banner', locals: {color: 'alert'} do
|
||||
b hey, slight issue:
|
||||
ul
|
||||
- purchase_order.errors.each do |error|
|
||||
li= error.full_message
|
||||
|
||||
div
|
||||
= form.label :supplier_name, "Supplier Name:"
|
||||
= form.text_field :supplier_name, class: 'form-control', required: true
|
||||
|
||||
div
|
||||
= form.label :supplier_id, "Supplier ID (from Zenventory, optional):"
|
||||
= form.number_field :supplier_id, class: 'form-control'
|
||||
|
||||
div
|
||||
= form.label :required_by_date, "Required By Date:"
|
||||
= form.date_field :required_by_date, class: 'form-control'
|
||||
|
||||
.py-3
|
||||
= form.label :notes, "Notes:"
|
||||
.display-block
|
||||
= form.text_area :notes, class: 'form-control', rows: 4
|
||||
|
||||
h4 Line Items:
|
||||
#line_items
|
||||
= form.fields_for :line_items do |line_item|
|
||||
= render 'line_item_fields', f: line_item
|
||||
|
||||
.links
|
||||
= link_to_add_association 'Add item', form, :line_items
|
||||
|
||||
.pt-3
|
||||
= form.submit "Save", class: 'form-control'
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<div class="nested-fields grid">
|
||||
<% skus_by_category = Warehouse::SKU.order(:sku).group_by(&:category) %>
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-5">
|
||||
<%= f.grouped_collection_select :sku_id,
|
||||
skus_by_category,
|
||||
:last,
|
||||
lambda { |group| group.first.humanize },
|
||||
:id,
|
||||
-> (sku) { "#{sku.sku} - #{sku.name}"},
|
||||
{ include_blank: 'Select a SKU' },
|
||||
{ class: 'needs-select2', disabled: f.object.persisted? } %>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-2">
|
||||
<%= f.number_field :quantity, class: 'form-control', min: 1, placeholder: 'Quantity' %>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-3">
|
||||
<%= f.number_field :unit_cost, class: 'form-control', min: 0, step: 0.01, placeholder: 'Unit Cost' %>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-2">
|
||||
<%= link_to_remove_association "Remove", f, class: 'secondary outline' %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if f.object.persisted? %>
|
||||
<%= f.hidden_field :sku_id %>
|
||||
<% end %>
|
||||
</div>
|
||||
18
app/views/warehouse/purchase_orders/_status_badge.html.erb
Normal file
18
app/views/warehouse/purchase_orders/_status_badge.html.erb
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<%
|
||||
color, text = case purchase_order.status&.to_sym
|
||||
when :draft
|
||||
%w[bg-muted draft]
|
||||
when :open
|
||||
["info", "open"]
|
||||
when :completed
|
||||
%w[success completed]
|
||||
when :deleted
|
||||
%w[warning deleted]
|
||||
else
|
||||
["purple", purchase_order.status || "unknown"]
|
||||
end
|
||||
%>
|
||||
|
||||
<span class="badge <%= color %> float-right">
|
||||
<%= text %>
|
||||
</span>
|
||||
12
app/views/warehouse/purchase_orders/edit.html.erb
Normal file
12
app/views/warehouse/purchase_orders/edit.html.erb
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<% content_for :title, "Edit Purchase Order" %>
|
||||
|
||||
<h1>Edit Purchase Order #<%= @purchase_order.id %></h1>
|
||||
|
||||
<%= render "form", purchase_order: @purchase_order %>
|
||||
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<%= link_to "Show this purchase order", warehouse_purchase_order_path(@purchase_order) %> |
|
||||
<%= link_to "Back to purchase orders", warehouse_purchase_orders_path %>
|
||||
</div>
|
||||
58
app/views/warehouse/purchase_orders/index.html.erb
Normal file
58
app/views/warehouse/purchase_orders/index.html.erb
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<% content_for :title, "Purchase Orders" %>
|
||||
<div class="page-header mb-8 flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold">Purchase Orders</h1>
|
||||
<div class="actions">
|
||||
<%= success_link_to "Create New Purchase Order", new_warehouse_purchase_order_path do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
Create New Purchase Order
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="orders-container">
|
||||
<div class="bg-gray-100 rounded-lg border border-gray-200">
|
||||
<div class="border-b border-gray-200 p-4">
|
||||
<h2 class="text-xl font-medium">All Purchase Orders</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<% if @purchase_orders.any? %>
|
||||
<table class="table-fixed w-full text-left border border-gray-300 border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border border-gray-300 p-2">ID</th>
|
||||
<th class="border border-gray-300 p-2">Supplier</th>
|
||||
<th class="border border-gray-300 p-2">Order #</th>
|
||||
<th class="border border-gray-300 p-2">Required By</th>
|
||||
<th class="border border-gray-300 p-2">Status</th>
|
||||
<th class="border border-gray-300 p-2">Created By</th>
|
||||
<th class="border border-gray-300 p-2">Created At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @purchase_orders.each do |po| %>
|
||||
<tr>
|
||||
<td class="border border-gray-400 p-2">
|
||||
<%= link_to po.id, warehouse_purchase_order_path(po) %>
|
||||
</td>
|
||||
<td class="border border-gray-400 p-2"><%= po.supplier_name %></td>
|
||||
<td class="border border-gray-400 p-2"><%= po.order_number %></td>
|
||||
<td class="border border-gray-400 p-2"><%= po.required_by_date&.strftime("%b %d, %Y") %></td>
|
||||
<td class="border border-gray-400 p-2">
|
||||
<%= render 'status_badge', purchase_order: po %>
|
||||
</td>
|
||||
<td class="border border-gray-400 p-2"><%= po.user&.email %></td>
|
||||
<td class="border border-gray-400 p-2"><%= po.created_at.strftime("%b %d, %Y") %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<div class="text-center py-8">
|
||||
<p class="text-gray-500">No purchase orders found.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
11
app/views/warehouse/purchase_orders/new.html.erb
Normal file
11
app/views/warehouse/purchase_orders/new.html.erb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<% content_for :title, "New Purchase Order" %>
|
||||
|
||||
<h1>New Purchase Order</h1>
|
||||
|
||||
<%= render "form", purchase_order: @purchase_order %>
|
||||
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<%= link_to "Back to purchase orders", warehouse_purchase_orders_path %>
|
||||
</div>
|
||||
86
app/views/warehouse/purchase_orders/show.html.erb
Normal file
86
app/views/warehouse/purchase_orders/show.html.erb
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<% content_for :title, "Purchase Order ##{@purchase_order.id}" %>
|
||||
|
||||
<div>
|
||||
<h2 class="inline">
|
||||
Purchase Order #<%= @purchase_order.id %>
|
||||
</h2>
|
||||
<%= render 'status_badge', purchase_order: @purchase_order %>
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<b>Supplier:</b> <%= @purchase_order.supplier_name %><br>
|
||||
<% if @purchase_order.supplier_id.present? %>
|
||||
<b>Supplier ID:</b> <%= @purchase_order.supplier_id %><br>
|
||||
<% end %>
|
||||
<% if @purchase_order.order_number.present? %>
|
||||
<b>Order Number:</b> <%= @purchase_order.order_number %><br>
|
||||
<% end %>
|
||||
<% if @purchase_order.required_by_date.present? %>
|
||||
<b>Required By:</b> <%= @purchase_order.required_by_date.strftime("%B %d, %Y") %><br>
|
||||
<% end %>
|
||||
<b>Created By:</b> <%= link_to @purchase_order.user&.email, @purchase_order.user %><br>
|
||||
<b>Created At:</b> <%= @purchase_order.created_at.strftime("%B %d, %Y at %I:%M %p") %><br>
|
||||
</div>
|
||||
|
||||
<% if @purchase_order.notes.present? %>
|
||||
<div class="my-4">
|
||||
<b>Notes:</b><br>
|
||||
<%= simple_format(@purchase_order.notes) %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="my-4">
|
||||
<b>Line Items:</b>
|
||||
<table class="table-fixed text-left border border-gray-300 border-collapse mt-2">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border border-gray-300 p-2">SKU</th>
|
||||
<th class="border border-gray-300 p-2">Quantity</th>
|
||||
<th class="border border-gray-300 p-2">Unit Cost</th>
|
||||
<th class="border border-gray-300 p-2">Line Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @purchase_order.line_items.each do |li| %>
|
||||
<tr>
|
||||
<td class="border border-gray-400 p-2">
|
||||
<%= link_to li.sku.name, warehouse_sku_path(li.sku), target: '_blank' %>
|
||||
<span class="text-gray-500">(<%= li.sku.sku %>)</span>
|
||||
</td>
|
||||
<td class="border border-gray-400 p-2"><%= li.quantity %></td>
|
||||
<td class="border border-gray-400 p-2"><%= number_to_currency(li.unit_cost) %></td>
|
||||
<td class="border border-gray-400 p-2"><%= number_to_currency((li.unit_cost || 0) * li.quantity) %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<tr>
|
||||
<td colspan="3" class="border border-gray-400 p-2 text-right"><b>Total:</b></td>
|
||||
<td class="border border-gray-400 p-2"><b><%= number_to_currency(@purchase_order.total_cost) %></b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<% zenv_link @purchase_order %>
|
||||
|
||||
<div class="my-4 space-x-2">
|
||||
<% if policy(@purchase_order).send_to_zenventory? && @purchase_order.may_mark_open? %>
|
||||
<%= button_to "Send to Zenventory", send_to_zenventory_warehouse_purchase_order_path(@purchase_order), method: :post %>
|
||||
<% end %>
|
||||
|
||||
<% if policy(@purchase_order).sync? %>
|
||||
<%= button_to "Sync from Zenventory", sync_warehouse_purchase_order_path(@purchase_order), method: :post %>
|
||||
<% end %>
|
||||
|
||||
<% if policy(@purchase_order).destroy? %>
|
||||
<%= button_to "Delete", warehouse_purchase_order_path(@purchase_order), method: :delete, data: { confirm: "Are you sure?" } %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<% if policy(@purchase_order).edit? %>
|
||||
<%= link_to "Edit this purchase order", edit_warehouse_purchase_order_path(@purchase_order) %><br>
|
||||
<% end %>
|
||||
<%= link_to "Back to purchase orders", warehouse_purchase_orders_path %>
|
||||
</div>
|
||||
|
||||
<%= render partial: "admin_inspector", locals: { record: @purchase_order } %>
|
||||
|
|
@ -550,6 +550,12 @@ Rails.application.routes.draw do
|
|||
post "send_to_warehouse"
|
||||
end
|
||||
end
|
||||
resources :purchase_orders do
|
||||
member do
|
||||
post :send_to_zenventory
|
||||
post :sync
|
||||
end
|
||||
end
|
||||
resources :batches do
|
||||
member do
|
||||
get "/map", to: "batches#map_fields", as: :map_fields
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
class CreateWarehousePurchaseOrders < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :warehouse_purchase_orders do |t|
|
||||
t.string :supplier_name
|
||||
t.integer :supplier_id
|
||||
t.string :order_number
|
||||
t.text :notes
|
||||
t.date :required_by_date
|
||||
t.string :status, default: "draft"
|
||||
t.integer :zenventory_id
|
||||
t.references :user, null: false, foreign_key: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :warehouse_purchase_orders, :zenventory_id, unique: true
|
||||
add_index :warehouse_purchase_orders, :order_number
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
class CreateWarehousePurchaseOrderLineItems < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :warehouse_purchase_order_line_items do |t|
|
||||
t.references :purchase_order, null: false, foreign_key: { to_table: :warehouse_purchase_orders }
|
||||
t.references :sku, null: false, foreign_key: { to_table: :warehouse_skus }
|
||||
t.integer :quantity, null: false
|
||||
t.decimal :unit_cost, precision: 10, scale: 2
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
32
db/schema.rb
generated
32
db/schema.rb
generated
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_12_18_194622) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2026_01_15_180434) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "citext"
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
|
|
@ -566,6 +566,33 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_18_194622) do
|
|||
t.index ["user_id"], name: "index_warehouse_orders_on_user_id"
|
||||
end
|
||||
|
||||
create_table "warehouse_purchase_order_line_items", force: :cascade do |t|
|
||||
t.bigint "purchase_order_id", null: false
|
||||
t.bigint "sku_id", null: false
|
||||
t.integer "quantity", null: false
|
||||
t.decimal "unit_cost", precision: 10, scale: 2
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["purchase_order_id"], name: "index_warehouse_purchase_order_line_items_on_purchase_order_id"
|
||||
t.index ["sku_id"], name: "index_warehouse_purchase_order_line_items_on_sku_id"
|
||||
end
|
||||
|
||||
create_table "warehouse_purchase_orders", force: :cascade do |t|
|
||||
t.string "supplier_name"
|
||||
t.integer "supplier_id"
|
||||
t.string "order_number"
|
||||
t.text "notes"
|
||||
t.date "required_by_date"
|
||||
t.string "status", default: "draft"
|
||||
t.integer "zenventory_id"
|
||||
t.bigint "user_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["order_number"], name: "index_warehouse_purchase_orders_on_order_number"
|
||||
t.index ["user_id"], name: "index_warehouse_purchase_orders_on_user_id"
|
||||
t.index ["zenventory_id"], name: "index_warehouse_purchase_orders_on_zenventory_id", unique: true
|
||||
end
|
||||
|
||||
create_table "warehouse_skus", force: :cascade do |t|
|
||||
t.string "sku"
|
||||
t.text "description"
|
||||
|
|
@ -643,6 +670,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_18_194622) do
|
|||
add_foreign_key "warehouse_orders", "source_tags"
|
||||
add_foreign_key "warehouse_orders", "users"
|
||||
add_foreign_key "warehouse_orders", "warehouse_templates", column: "template_id"
|
||||
add_foreign_key "warehouse_purchase_order_line_items", "warehouse_purchase_orders", column: "purchase_order_id"
|
||||
add_foreign_key "warehouse_purchase_order_line_items", "warehouse_skus", column: "sku_id"
|
||||
add_foreign_key "warehouse_purchase_orders", "users"
|
||||
add_foreign_key "warehouse_templates", "source_tags"
|
||||
add_foreign_key "warehouse_templates", "users"
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue