24 Commits

Author SHA1 Message Date
f649d4e902 added checkbox for appointment link javascript 2026-02-09 21:44:23 -05:00
2db17f3675 Fixed display of vehcile notes 2026-02-05 14:07:24 -05:00
d37933fe82 2026.1.7
Added vehicle estimates & invoices to vehicle page
2026-01-31 13:00:19 -05:00
3efc545f0a Compact to remove nil elements from the array 2026-01-31 12:55:47 -05:00
adcc116841 Add estimates and invoices sections to vehicle details view 2026-01-31 12:46:41 -05:00
8bb98d2408 Add methods to retrieve invoices and estimates for vehicles 2026-01-31 12:46:30 -05:00
88b0ffcd6b Updaed readme 2026-01-31 08:00:19 -05:00
48deb3b7c8 2026.1.6 2026-01-30 21:32:56 -05:00
e2ea22afdf Removed unused folders 2026-01-30 20:53:46 -05:00
717a61b024 Updated flash notices to use locale 2026-01-30 19:49:48 -05:00
a932551b96 2026.1.5 2026-01-30 17:08:39 -05:00
e09990cd42 added nil check 2026-01-30 17:04:23 -05:00
ad8aa8e2e8 use locale 2026-01-30 08:07:11 -05:00
707abc00a9 Use symbols 2026-01-30 08:05:57 -05:00
1199af0886 2026.1.4 2026-01-30 07:50:26 -05:00
5a2832b751 need to supply selcted id 2026-01-30 07:49:51 -05:00
6fc2db5d73 2026.1.3 2026-01-30 07:21:11 -05:00
634fd96e82 Cleaned up pdf hook 2026-01-29 23:38:55 -05:00
ce624f178f Updated comment 2026-01-29 23:33:42 -05:00
808b84a346 Cleaned up issue show hook 2026-01-29 23:32:12 -05:00
e4d2df889d Cleaned up issue form hook 2026-01-29 23:22:09 -05:00
715c412668 removed unused invocie variable 2026-01-29 22:55:14 -05:00
e0a23bac20 Lose the hash rocket, use symbols 2026-01-29 22:52:17 -05:00
fa72b1c837 Updated README 2026-01-28 08:22:00 -05:00
23 changed files with 121 additions and 125 deletions

View File

@@ -1,53 +1,55 @@
# Redmine QuickBooks Online Vehicles # Redmine QuickBooks Online Vehicles
A redmine plugin to compliment the Redmine QuickBooks Online Vehicles plug in. A Redmine plugin to complement the [Redmine QuickBooks Online](https://github.com/rickbarrette/redmine_qbo) plugin.
The goal of this project is to allow add vehicle tracking for customer vehicles. The goal of this project is to enable vehicle tracking for customer vehicles within Redmine.
## Requirements
* **Redmine:** 6.1+
* **Parent Plugin:** [Redmine QuickBooks Online](https://github.com/rickbarrette/redmine_qbo)
## Compatibility ## Compatibility
| Redmine QBO Plugin Version | Redmine Version | | Plugin Version | Redmine Version | Ruby Version |
| :--- | :--- | | :--- | :--- | :--- |
| Version 2026.1.2+ | Redmine 6.1 | | 2026.1.2+ | Redmine 6.1 | 3.2+ |
## Features ## Features
Adds vehicles that are owned by customers that can be attached to issues. * **Asset Tracking:** Adds vehicles owned by customers to the system.
* **Issue Association:** Allows these vehicles to be attached directly to Redmine issues for better service tracking.
## Installation ## Installation
1. **Clone the plugin:** 1. **Clone the plugin:**
Clone this repo into your plugin folder and checkout a tagged version. Navigate to your Redmine plugins directory and clone the repository.
```bash ```bash
cd path/to/redmine/plugins cd path/to/redmine/plugins
git clone git@github.com:rickbarrette/redmine_qbo_vehicles.git git clone git@github.com:rickbarrette/redmine_qbo_vehicles.git
cd redmine_qbo cd redmine_qbo_vehicles
git checkout <tag> git checkout <tag>
``` ```
*(Note: Replace `<tag>` with the specific release version you wish to use, or omit the last line to use the main branch.)*
2. **Install dependencies:** *Crucial for Redmine 6 / Rails 7 compatibility.* 2. **Install dependencies:**
*Crucial for Redmine 6 / Rails 7 compatibility.*
Bash ```bash
```
bundle install bundle install
``` ```
3. **Migrate your database:** 3. **Migrate your database:**
```bash
Bash
```
bundle exec rake redmine:plugins:migrate RAILS_ENV=production bundle exec rake redmine:plugins:migrate RAILS_ENV=production
``` ```
4. **Restart Redmine:** You must restart your Redmine server instance for the plugin and hooks to load. 4. **Restart Redmine:**
You must restart your Redmine server instance (e.g., Puma, Passenger, Unicorn) for the plugin and hooks to load correctly.
## Usage ## Usage
Simply add vehicles to customers via Customer view 1. **Add a Vehicle:** Navigate to a Customer Profile. You will see a new option to add vehicles to that customer.
2. **Link to Issue:** Once a vehicle is added to a customer, it can be selected and attached to an Issue relevant to that customer.
Once a customer is attached to the customer, they can be attached to an issue.
## License ## License

View File

@@ -23,15 +23,16 @@ class VehiclesController < ApplicationController
def index def index
if params[:customer_id] if params[:customer_id]
begin begin
@vehicles = Customer.find_by_id(params[:customer_id]).vehicles.paginate(:page => params[:page]) @vehicles = Customer.find_by_id(params[:customer_id]).vehicles.paginate(page: params[:page])
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
flash[:error] = t :alert_vehicle_not_found
render_404 render_404
end end
end end
# search for a vehicle by vin # search for a vehicle by vin
if params[:search] if params[:search]
@vehicles = Vehicle.search(params[:search]).paginate(:page => params[:page]) @vehicles = Vehicle.search(params[:search]).paginate(page: params[:page])
if only_one_non_zero?(@vehicles) if only_one_non_zero?(@vehicles)
redirect_to @vehicles.first redirect_to @vehicles.first
end end
@@ -48,7 +49,7 @@ class VehiclesController < ApplicationController
def create def create
@vehicle = Vehicle.new(allowed_params) @vehicle = Vehicle.new(allowed_params)
if @vehicle.save if @vehicle.save
flash[:notice] = "New Vehicle Created" flash[:notice] = t :notice_vehicle_created
redirect_to @vehicle redirect_to @vehicle
else else
flash[:error] = @vehicle.errors.full_messages.to_sentence flash[:error] = @vehicle.errors.full_messages.to_sentence
@@ -64,6 +65,7 @@ class VehiclesController < ApplicationController
@issues = @vehicle.issues.order(id: :desc) @issues = @vehicle.issues.order(id: :desc)
@closed_issues = (@issues - @issues.open) @closed_issues = (@issues - @issues.open)
rescue rescue
flash[:error] = t :alert_vehicle_not_found
render_404 render_404
end end
end end
@@ -74,6 +76,7 @@ class VehiclesController < ApplicationController
@vehicle = Vehicle.find_by_id(params[:id]) @vehicle = Vehicle.find_by_id(params[:id])
@customer = @vehicle.customer @customer = @vehicle.customer
rescue rescue
flash[:error] = t :alert_vehicle_not_found
render_404 render_404
end end
end end
@@ -84,7 +87,7 @@ class VehiclesController < ApplicationController
begin begin
@vehicle = Vehicle.find_by_id(params[:id]) @vehicle = Vehicle.find_by_id(params[:id])
if @vehicle.update(allowed_params) if @vehicle.update(allowed_params)
flash[:notice] = "Vehicle updated" flash[:notice] = t :notice_vehicle_updated
redirect_to @vehicle redirect_to @vehicle
else else
redirect_to edit_vehicle_path redirect_to edit_vehicle_path
@@ -92,6 +95,7 @@ class VehiclesController < ApplicationController
#show any errors anyways #show any errors anyways
flash[:error] = @vehicle.errors.full_messages.to_sentence unless @vehicle.errors.empty? flash[:error] = @vehicle.errors.full_messages.to_sentence unless @vehicle.errors.empty?
rescue rescue
flash[:error] = t :alert_vehicle_not_updated
render_404 render_404
end end
end end
@@ -100,9 +104,10 @@ class VehiclesController < ApplicationController
def destroy def destroy
begin begin
Vehicle.find_by_id(params[:id]).destroy Vehicle.find_by_id(params[:id]).destroy
flash[:notice] = "Vehicle deleted successfully" flash[:notice] = t :notice_vehicle_deleted
redirect_to action: :index redirect_to action: :index
rescue rescue
flash[:error] = t :alert_vehicle_not_deleted
render_404 render_404
end end
end end

View File

@@ -72,6 +72,16 @@ class Vehicle < ActiveRecord::Base
self.name = to_s self.name = to_s
end end
# reurns all invoices for this vehicle
def invoices
self.issues.flat_map(&:invoices).uniq.compact
end
# returns all estimates for this vehicle
def estimates
self.issues.flat_map(&:estimate).uniq.compact
end
private private
# init method to pull JSON details from NHTSA # init method to pull JSON details from NHTSA

View File

@@ -1,5 +1,5 @@
<h4><%=t(:field_vehicles)%>:</h4> <h4><%=t(:field_vehicles)%>:</h4>
<%= render :partial => 'vehicles/list', :locals => { :vehicles => customer.vehicles.paginate(:page => params[:page]) } %> <%= render partial: 'vehicles/list', locals: { vehicles: customer.vehicles.paginate(page: params[:page]) } %>
<div style="float: right;"> <div style="float: right;">
<%= button_to t(:button_new_vehice), new_customer_vehicle_path(customer), method: :get %> <%= button_to t(:button_new_vehice), new_customer_vehicle_path(customer), method: :get %>
</div> </div>

View File

@@ -10,9 +10,5 @@
<div class="vehicle_notes attribute"> <div class="vehicle_notes attribute">
<div class="label"><span><%=t(:field_notes)%></span>:</div> <div class="label"><span><%=t(:field_notes)%></span>:</div>
<div class="value"> <pre class="value" id="note-display" style="text-align: left; white-space: pre-wrap; font-family: inherit; "><%=notes%></pre>
<pre id="note-display" style="text-align: left; white-space: pre-wrap; font-family: inherit; ">
<%=notes%>
</pre>
</div>
</div> </div>

View File

@@ -6,43 +6,43 @@
<div class="clearfix"> <div class="clearfix">
<%=t(:field_customer)%>: <%=t(:field_customer)%>:
<div class="input"> <div class="input">
<%= f.autocomplete_field :customer, autocomplete_customer_name_customers_path, :value => @customer.name, :update_elements => {:id => '#customer_id', :value => '#issue_customer'}, :required => true %> <%= f.autocomplete_field :customer, autocomplete_customer_name_customers_path, value: @customer.name, update_elements: {id: '#customer_id', value: '#issue_customer'}, required: true %>
<%= f.hidden_field :customer_id, :id => "customer_id", :value => @customer.id %> <%= f.hidden_field :customer_id, id: "customer_id", value: @customer.id %>
</div> </div>
</div> </div>
<div class="clearfix"> <div class="clearfix">
<%=t(:label_year)%>: <%=t(:label_year)%>:
<div class="input"> <div class="input">
<%= f.number_field :year, :autocomplete => "off" %> <%= f.number_field :year, autocomplete: "off" %>
</div> </div>
</div> </div>
<div class="clearfix"> <div class="clearfix">
<%=t(:label_make)%>: <%=t(:label_make)%>:
<div class="input"> <div class="input">
<%= f.text_field :make, :autocomplete => "off" %> <%= f.text_field :make, autocomplete: "off" %>
</div> </div>
</div> </div>
<div class="clearfix"> <div class="clearfix">
<%=t(:label_model)%>: <%=t(:label_model)%>:
<div class="input"> <div class="input">
<%= f.text_field :model, :autocomplete => "off" %> <%= f.text_field :model, autocomplete: "off" %>
</div> </div>
</div> </div>
<div class="clearfix"> <div class="clearfix">
<%=t(:field_vin)%>: <%=t(:field_vin)%>:
<div class="input"> <div class="input">
<%= f.text_field :vin , :autofocus => true %> <%= f.text_field :vin , autofocus: true %>
</div> </div>
</div> </div>
<div class="clearfix"> <div class="clearfix">
<%=t(:field_notes)%>: <%=t(:field_notes)%>:
<div class="input"> <div class="input">
<%= f.text_area :notes, :cols => 60, :rows => 10, :no_label => true %> <%= f.text_area :notes, cols: 60, rows: 10, no_label: true %>
</div> </div>
</div> </div>

View File

@@ -3,6 +3,7 @@
<% vehicles.each do |vehicle| %> <% vehicles.each do |vehicle| %>
<div class="row"> <div class="row">
<div> <div>
<%= check_box_tag "vehicle_ids[]", vehicle.id, false, onchange: "updateLink()", data: { url: vehicle_path(vehicle).html_safe, text: vehicle.to_s }, class: "appointment" %>
<b><%= link_to "##{vehicle.id}", vehicle_path(vehicle) %> </b> <b><%= link_to "##{vehicle.id}", vehicle_path(vehicle) %> </b>
</div> </div>

View File

@@ -1,4 +1,4 @@
<%= form_tag(vehicles_path, :method => "get", id: "search-form") do %> <%= form_tag(vehicles_path, method: "get", id: "search-form") do %>
<%= text_field_tag :search, params[:search], placeholder: t(:label_search_vin), :autocomplete => "off" %> <%= text_field_tag :search, params[:search], placeholder: t(:label_search_vin), autocomplete: "off" %>
<%= submit_tag t(:label_search) %> <%= submit_tag t(:label_search) %>
<% end %> <% end %>

View File

@@ -1,3 +1,3 @@
<h1><%=t(:label_edit_customer_vehicle)%></h1> <h1><%=t(:label_edit_customer_vehicle)%></h1>
<br/> <br/>
<%= render :partial => 'vehicles/form' %> <%= render partial: 'vehicles/form' %>

View File

@@ -1,4 +1,4 @@
<h2><%=t(:label_cusomer_vehicles)%> <span style="float:right"> <%= render :partial => 'vehicles/search' %> </span> </h2> <h2><%=t(:label_cusomer_vehicles)%> <span style="float:right"> <%= render partial: 'vehicles/search' %> </span> </h2>
<br/> <br/>
<%= render :partial => 'vehicles/list', locals: {vehicles: @vehicles} %> <%= render partial: 'vehicles/list', locals: {vehicles: @vehicles} %>

View File

@@ -1,3 +1,3 @@
<h2><%=t(:label_new_vehicle)%></h2> <h2><%=t(:label_new_vehicle)%></h2>
<br/> <br/>
<%= render :partial => 'vehicles/form' %> <%= render partial: 'vehicles/form' %>

View File

@@ -1,11 +1,23 @@
<h2><%=t(:field_vehicle)%> #<%=@vehicle.id%></h2> <h2><%=t(:field_vehicle)%> #<%=@vehicle.id%></h2>
<%= render :partial => 'vehicles/details', locals: {vehicle: @vehicle} %> <%= render partial: 'vehicles/details', locals: {vehicle: @vehicle} %>
<div class="splitcontent">
<div class="splitcontentleft">
<h4><%=t(:estimates)%>:</h4>
<%= render partial: 'estimates/list', locals: {estimates: @vehicle.estimates} %>
</div>
<div class="splitcontentleft">
<h4><%=t(:label_invoices)%>:</h4>
<%= render partial: 'invoices/list', locals: {invoices: @vehicle.invoices} %>
</div>
</div>
<h3><%=@issues.open.count%> <%=t(:label_open_issues)%></h3> <h3><%=@issues.open.count%> <%=t(:label_open_issues)%></h3>
<%= render :partial => 'issues/list_simple', locals: {issues: @issues.open} %> <%= render partial: 'issues/list_simple', locals: {issues: @issues.open} %>
<h3><%=@closed_issues.count%> <%=t(:label_closed_issues)%></h3> <h3><%=@closed_issues.count%> <%=t(:label_closed_issues)%></h3>
<%= render :partial => 'issues/list_simple', locals: {issues: (@closed_issues)} %> <%= render partial: 'issues/list_simple', locals: {issues: (@closed_issues)} %>

View File

View File

@@ -24,3 +24,10 @@ en:
label_cusomer_vehicles: "Customer Vehicles" label_cusomer_vehicles: "Customer Vehicles"
label_new_vehicle: "New Customer Vehicle" label_new_vehicle: "New Customer Vehicle"
button_new_vehice: "New Vehicle" button_new_vehice: "New Vehicle"
notice_vehicle_created: "Vehicle was successfully created."
notice_vehicle_updated: "Vehicle was successfully updated."
notice_vehicle_deleted: "Vehicle was successfully deleted."
alert_vehicle_not_found: "Vehicle not found."
alert_vehicle_not_deleted: "Vehicle could not be deleted."
alert_vehicle_not_created: "Vehicle could not be created."
alert_vehicle_not_updated: "Vehicle could not be updated."

View File

@@ -11,7 +11,7 @@
# Nest Vehicles under customers # Nest Vehicles under customers
resources :customers do resources :customers do
resources :vehicles resources :vehicles
get :autocomplete_customer_name, :on => :collection get :autocomplete_customer_name, on: :collection
end end
#allow for just vehicles too #allow for just vehicles too

10
init.rb
View File

@@ -14,10 +14,10 @@ Redmine::Plugin.register :redmine_qbo_vehicles do
name 'Redmine QBO Vehicles plugin' name 'Redmine QBO Vehicles plugin'
author 'Rick Barrette' author 'Rick Barrette'
description 'This is a plugin for Redmine to intergrate with the redmine_qbo plugin to provide vehicle data tracking' description 'This is a plugin for Redmine to intergrate with the redmine_qbo plugin to provide vehicle data tracking'
version '2026.1.1' version '2026.1.7'
url 'https://github.com/rickbarrette/redmine_qbo_vehicles' url 'https://github.com/rickbarrette/redmine_qbo_vehicles'
author_url 'https://barrettefabrication.com' author_url 'https://barrettefabrication.com'
requires_redmine :version_or_higher => '6.1.0' requires_redmine version_or_higher: '6.1.0'
# Ensure redmine_qbo is installed # Ensure redmine_qbo is installed
begin begin
@@ -27,13 +27,13 @@ Redmine::Plugin.register :redmine_qbo_vehicles do
end end
# Add safe attributes for core models # Add safe attributes for core models
Issue.safe_attributes 'vehicle_id' Issue.safe_attributes :vehicle_id
# Permissions for security # Permissions for security
permission :view_vehicles, :vehicles => :new, :public => false permission :view_vehicles, vehicles: :new, public: false
# Register top menu items # Register top menu items
menu :top_menu, :vehicles, { :controller => :vehicles, :action => :index }, :caption => 'Vehicles', :if => Proc.new { User.current.logged? } menu :top_menu, :vehicles, { controller: :vehicles, action: :index }, caption: :field_vehicles, if: Proc.new { User.current.logged? }
end end

View File

@@ -15,11 +15,11 @@ module Vehicles
include IssuesHelper include IssuesHelper
# Display vehicle information on the customer show view (right side)
def show_customer_view_right(context={}) def show_customer_view_right(context={})
# Pass all prebuilt form components to our partial
context[:controller].send(:render_to_string, { context[:controller].send(:render_to_string, {
:partial => 'customers/show_hook', locals: { customer: context[:customer] } partial: 'customers/show_hook', locals: { customer: context[:customer] }
}) })
end end

View File

@@ -19,7 +19,6 @@ module Vehicles
def process_invoice_custom_fields(context={}) def process_invoice_custom_fields(context={})
Rails.logger.info "redmine_qbo_vehicles.process_invoice_custom_fields" Rails.logger.info "redmine_qbo_vehicles.process_invoice_custom_fields"
issue = context[:issue] issue = context[:issue]
invoice = context[:invoice]
# update the invoive custom fields with infomation from the issue if available # update the invoive custom fields with infomation from the issue if available
context[:invoice].custom_fields.each do |cf| context[:invoice].custom_fields.each do |cf|

View File

@@ -15,36 +15,19 @@ module Vehicles
include IssuesHelper include IssuesHelper
# Edit Issue Form
# Here we build the required form components before passing them to a partial view formatting.
def view_issues_form_details_bottom(context={}) def view_issues_form_details_bottom(context={})
f = context[:form]
issue = context[:issue]
# Check to see if the issue already belongs to a customer # Load the customer's vehicles for selection in the issue form.
selected_vehicle = issue.vehicle.id unless issue.vehicle.nil?
# Load customer's vehicles
if issue.customer
if issue.customer.vehicles
vehicles = issue.customer.vehicles.pluck(:name, :id)
else
vehicles = [nil].compact
end
else
vehicles = [nil].compact
end
# Generate the drop down list of vehicles
vehicle = f.select :vehicle_id, vehicles, :selected => selected_vehicle, include_blank: true
# Pass all prebuilt form components to our partial
context[:controller].send(:render_to_string, { context[:controller].send(:render_to_string, {
:partial => 'issues/form_hook_vehicles', partial: 'issues/form_hook_vehicles',
locals: { locals: {
vehicle: vehicle vehicle: context[:form].select( :vehicle_id,
context[:issue].customer ? context[:issue].customer.vehicles.pluck(:name, :id) : [],
selected: context[:issue].vehicle ? context[:issue].vehicle.id : nil,
include_blank: true )
} }
}) })
end end
end end

View File

@@ -13,28 +13,15 @@ module Vehicles
class IssuesShowHookListener < Redmine::Hook::ViewListener class IssuesShowHookListener < Redmine::Hook::ViewListener
# View Issue # Display vehicle information on the issue view page.
# Display the quickbooks contact in the issue
def show_issue_view_right(context={}) def show_issue_view_right(context={})
issue = context[:issue]
begin
v = issue.vehicle
vehicle = link_to v.to_s, vehicle_path( v.id )
vin = v.vin
notes = v.notes
rescue
#do nothing
end
split_vin = vin.scan(/.{1,9}/) if vin
context[:controller].send(:render_to_string, { context[:controller].send(:render_to_string, {
:partial => 'issues/show_issue_view_right', partial: 'issues/show_issue_view_right',
locals: { locals: {
vehicle: vehicle, vehicle: context[:issue].vehicle ? link_to(context[:issue].vehicle) : nil,
split_vin: split_vin, split_vin: context[:issue].vehicle ? context[:issue].vehicle.vin.to_s.scan(/.{1,9}/) : nil,
notes: notes notes: context[:issue].vehicle ? context[:issue].vehicle.notes : nil
} }
}) })
end end

View File

@@ -16,18 +16,12 @@ module Vehicles
include IssuesHelper include IssuesHelper
# Edit Issue Form # Add vehicle information to the left column of the issue PDF
# Here we build the required form components before passing them to a partial view formatting.
def pdf_left(context={}) def pdf_left(context={})
issue = context[:issue]
output = [] output = []
v = issue.vehicle output << [l(:field_vehicles), context[:issue].vehicle ? context[:issue].vehicle.to_s : nil]
vehicle = v ? v.to_s : nil output << [l(:field_vin), context[:issue].vehicle ? context[:issue].vehicle.vin.gsub(/(.{9})/, '\1 ') : nil]
vin = v ? v.vin : nil output << [l(:field_notes), context[:issue].vehicle ? context[:issue].vehicle.notes : nil]
notes = v ? v.notes : nil
output << [l(:field_vehicles), vehicle]
output << [l(:field_vin), vin ? vin.gsub(/(.{9})/, '\1 ') : nil]
output << [l(:field_notes), notes]
return output return output
end end