mirror of
https://github.com/rickbarrette/redmine_qbo_lineitems.git
synced 2026-10-05 01:30:43 -04:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0d6b08a3d | ||
|
|
9a9efcbaf9 | ||
|
|
0acf5ffca4 | ||
|
|
5b1d98aa07 | ||
|
|
3ca45a457f | ||
|
|
c9d2a47a92 | ||
|
|
b939d834e9 | ||
|
|
9b9a5c3505 | ||
|
|
da49b996da | ||
|
|
50a02cc497 | ||
|
|
b63e8f2a45 |
No files matched your search
@@ -100,8 +100,6 @@ class ItemsController < ApplicationController
|
|||||||
params.require(:item).permit(:name, :description, :sku, :unit_price, :active, :account_id, :type, :taxable)
|
params.require(:item).permit(:name, :description, :sku, :unit_price, :active, :account_id, :type, :taxable)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def log(msg)
|
def log(msg)
|
||||||
Rails.logger.info "[ItemsController] #{msg}"
|
Rails.logger.info "[ItemsController] #{msg}"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -45,11 +45,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
|
<% if User.current.allowed_to?(:add_line_items, nil, global: true) %>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="icon icon-add"
|
class="icon icon-add"
|
||||||
data-nested-form-add>
|
data-nested-form-add>
|
||||||
<%= l(:button_add) %>
|
<%= l(:button_add) %>
|
||||||
</button>
|
</button>
|
||||||
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
<% if @issue.line_items.any? %>
|
<% if @issue.line_items.any? || @issue.descendant_line_items_total > 0%>
|
||||||
<hr/>
|
<hr/>
|
||||||
<div>
|
<div>
|
||||||
<p><strong><%= t :label_line_items %></strong></p>
|
<p><strong><%= t :label_line_items %></strong></p>
|
||||||
|
|
||||||
|
<% total = 0 %>
|
||||||
|
|
||||||
|
<% if @issue.line_items.any?%>
|
||||||
|
|
||||||
<table class="list line-items-table">
|
<table class="list line-items-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -14,7 +18,6 @@
|
|||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<% total = 0 %>
|
|
||||||
|
|
||||||
<% @issue.line_items.each do |item| %>
|
<% @issue.line_items.each do |item| %>
|
||||||
<% line_total = item.quantity.to_f * item.unit_price.to_f %>
|
<% line_total = item.quantity.to_f * item.unit_price.to_f %>
|
||||||
@@ -29,11 +32,26 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<tfoot>
|
<tfoot>
|
||||||
|
<% if @issue.line_items.any?%>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" style="text-align:right;"><strong><%= t :label_total %></strong></td>
|
<td colspan="3" style="text-align:right;"><strong><%= t :label_total %></strong></td>
|
||||||
<td><strong><%= number_to_currency(total) %></strong></td>
|
<td>
|
||||||
|
<strong><%= number_to_currency(total) %></strong>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if @issue.descendant_line_items_total > 0 %>
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" style="text-align:right;"><strong><%= t :label_running_total %></strong></td>
|
||||||
|
<td>
|
||||||
|
<strong>(<%= number_to_currency(@issue.descendant_line_items_total + total) %>)</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
0.00
|
0.00
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<% if User.current.allowed_to?(:delete_line_items, nil, global: true) %>
|
||||||
<% unless readonly %>
|
<% unless readonly %>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
@@ -44,4 +45,5 @@
|
|||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -33,6 +33,7 @@ en:
|
|||||||
label_no: "No"
|
label_no: "No"
|
||||||
label_qty: "Quantity"
|
label_qty: "Quantity"
|
||||||
label_remove: "Remove"
|
label_remove: "Remove"
|
||||||
|
label_running_total: "Running Total"
|
||||||
label_sync_now_accounts: "Sync Accounts"
|
label_sync_now_accounts: "Sync Accounts"
|
||||||
label_sync_now_items: "Sync Items"
|
label_sync_now_items: "Sync Items"
|
||||||
label_type: "Type"
|
label_type: "Type"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Redmine::Plugin.register :redmine_qbo_lineitems do
|
|||||||
name 'Redmine QBO Line Items plugin'
|
name 'Redmine QBO Line Items plugin'
|
||||||
author 'Rick Barrette'
|
author 'Rick Barrette'
|
||||||
description 'A plugin for Redmine to extend the capabilitys of the Redmine QuickBooks Online plugin to attach billable line items to an isuue'
|
description 'A plugin for Redmine to extend the capabilitys of the Redmine QuickBooks Online plugin to attach billable line items to an isuue'
|
||||||
version '2026.3.11'
|
version '2026.9.0'
|
||||||
url 'https://github.com/rickbarrette/redmine_qbo_lineitems'
|
url 'https://github.com/rickbarrette/redmine_qbo_lineitems'
|
||||||
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'
|
||||||
@@ -29,20 +29,12 @@ Redmine::Plugin.register :redmine_qbo_lineitems do
|
|||||||
|
|
||||||
# Add safe attributes for core models
|
# Add safe attributes for core models
|
||||||
Issue.safe_attributes :line_items_attributes
|
Issue.safe_attributes :line_items_attributes
|
||||||
|
|
||||||
|
# Global Permissions
|
||||||
|
permission :view_line_items, { line_items: [:index, :show] }, global: true
|
||||||
|
permission :add_line_items, { line_items: [:new, :create] }, global: true
|
||||||
|
permission :edit_line_items, { line_items: [:edit, :update] }, global: true
|
||||||
|
permission :delete_line_items, { line_items: [:destroy] }, global: true
|
||||||
end
|
end
|
||||||
|
|
||||||
# Administration menu extension
|
RedmineQboLineItems.setup
|
||||||
Redmine::MenuManager.map :admin_menu do |menu|
|
|
||||||
menu.push :redmine_qbo_lineitems, { controller: 'items', action: 'index' },
|
|
||||||
icon: 'list',
|
|
||||||
caption: :label_items,
|
|
||||||
html: { class: 'icon icon-list' }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Dynamically load all Hooks & Patches recursively
|
|
||||||
base_dir = File.join(File.dirname(__FILE__), 'lib')
|
|
||||||
|
|
||||||
# '**' looks inside subdirectories, '*.rb' matches Ruby files
|
|
||||||
Dir.glob(File.join(base_dir, '**', '*.rb')).sort.each do |file|
|
|
||||||
require file
|
|
||||||
end
|
|
||||||
+2
-2
@@ -8,10 +8,10 @@
|
|||||||
#
|
#
|
||||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
module RedmineQboLineItems
|
module LineItems
|
||||||
module Hooks
|
module Hooks
|
||||||
|
|
||||||
class IssuesSaveHookListener < Redmine::Hook::ViewListener
|
class IssuesSaveHookListener < Redmine::Hook::Listener
|
||||||
|
|
||||||
# Called After Issue Saved
|
# Called After Issue Saved
|
||||||
def controller_issues_edit_after_save(context={})
|
def controller_issues_edit_after_save(context={})
|
||||||
+2
-2
@@ -8,10 +8,10 @@
|
|||||||
#
|
#
|
||||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
module RedmineQboLineItems
|
module LineItems
|
||||||
module Hooks
|
module Hooks
|
||||||
|
|
||||||
class QboHookListener < Redmine::Hook::ViewListener
|
class QboHookListener < Redmine::Hook::Listener
|
||||||
|
|
||||||
# Called by WebhookProcessJob
|
# Called by WebhookProcessJob
|
||||||
def qbo_additional_entities(context={})
|
def qbo_additional_entities(context={})
|
||||||
+9
-9
@@ -8,7 +8,7 @@
|
|||||||
#
|
#
|
||||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
module RedmineQboLineItems
|
module LineItems
|
||||||
module Hooks
|
module Hooks
|
||||||
|
|
||||||
class ViewHookListener < Redmine::Hook::ViewListener
|
class ViewHookListener < Redmine::Hook::ViewListener
|
||||||
@@ -24,18 +24,18 @@ module RedmineQboLineItems
|
|||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Add the line items form to the issue edit page
|
||||||
def view_issues_edit_notes_bottom(context = {})
|
def view_issues_edit_notes_bottom(context = {})
|
||||||
return if context[:issue].closed?
|
return if context[:issue].closed?
|
||||||
context[:controller].send(:render_to_string, {
|
return unless User.current.allowed_to?(:edit_line_items, nil, global: true)
|
||||||
partial: 'line_items/issue_form',
|
context[:controller].send(:render_to_string, { partial: 'line_items/issue_form', locals: { f: context[:form] } })
|
||||||
locals: {
|
|
||||||
f: context[:form]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
render_on :view_issues_show_description_bottom , partial: 'line_items/issue_line_items'
|
# Add the line items to the issue view page
|
||||||
|
def view_issues_show_description_bottom (context = {})
|
||||||
|
return unless User.current.allowed_to?(:view_line_items, nil, global: true)
|
||||||
|
context[:controller].send(:render_to_string, { partial: 'line_items/issue_line_items', locals: { issue: context[:issue] } })
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#The MIT License (MIT)
|
||||||
|
#
|
||||||
|
#Copyright (c) 2026 rick barrette
|
||||||
|
#
|
||||||
|
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
module LineItems
|
||||||
|
module Patches
|
||||||
|
module IssuePatch extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
prepended do
|
||||||
|
has_many :line_items, dependent: :destroy
|
||||||
|
accepts_nested_attributes_for :line_items, allow_destroy: true, reject_if: proc { |attrs| attrs['description'].blank? }
|
||||||
|
|
||||||
|
# Returns line items for immediate children
|
||||||
|
def children_line_items
|
||||||
|
LineItem.where(issue_id: self.children.pluck(:id))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Calculates the total value of all child line items
|
||||||
|
def children_line_items_total
|
||||||
|
children_line_items.sum(:line_total)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns line items for the entire tree below this issue
|
||||||
|
def descendant_line_items
|
||||||
|
LineItem.where(issue_id: self.descendants.pluck(:id))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Calculates the total value of entire tree below this issue
|
||||||
|
def descendant_line_items_total
|
||||||
|
descendant_line_items.sum(:line_total)
|
||||||
|
end
|
||||||
|
|
||||||
|
def line_items_total
|
||||||
|
line_items.sum(:line_total)
|
||||||
|
end
|
||||||
|
|
||||||
|
def line_items_attributes=(attrs)
|
||||||
|
attrs = attrs.stringify_keys
|
||||||
|
|
||||||
|
# IDs submitted in the form
|
||||||
|
submitted_ids = attrs.values.map { |a| a['id'] }.compact.map(&:to_s)
|
||||||
|
|
||||||
|
# Existing IDs in DB
|
||||||
|
existing_ids = line_items.pluck(:id).map(&:to_s)
|
||||||
|
|
||||||
|
# Find missing ones (these would be implicitly deleted by Rails)
|
||||||
|
missing_ids = existing_ids - submitted_ids
|
||||||
|
|
||||||
|
# Re-add missing records so Rails doesn't delete them
|
||||||
|
missing_ids.each do |id|
|
||||||
|
attrs["preserve_#{id}"] = { 'id' => id }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Only allow explicit deletes or valid updates/creates
|
||||||
|
filtered = attrs.select do |_, item_attrs|
|
||||||
|
item_attrs['_destroy'] == '1' ||
|
||||||
|
item_attrs['id'].present? ||
|
||||||
|
item_attrs['description'].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
super(filtered)
|
||||||
|
rescue => e
|
||||||
|
logger.error "Error processing line items attributes: #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,35 +8,22 @@
|
|||||||
#
|
#
|
||||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
require_dependency 'issue'
|
|
||||||
|
|
||||||
module RedmineQboLineItems
|
module RedmineQboLineItems
|
||||||
module Patches
|
|
||||||
module IssuePatch
|
|
||||||
|
|
||||||
def self.included(base)
|
def self.setup
|
||||||
base.extend(ClassMethods)
|
unless Issue.ancestors.include?(LineItems::Patches::IssuePatch)
|
||||||
base.send(:include, InstanceMethods)
|
# Patches
|
||||||
|
Issue.prepend LineItems::Patches::IssuePatch
|
||||||
|
|
||||||
base.class_eval do
|
# Hooks
|
||||||
has_many :line_items, dependent: :destroy
|
LineItems::Hooks::IssuesSaveHookListener
|
||||||
accepts_nested_attributes_for :line_items,
|
LineItems::Hooks::QboHookListener
|
||||||
allow_destroy: true,
|
LineItems::Hooks::ViewHookListener
|
||||||
reject_if: proc { |attrs| attrs['description'].blank? }
|
|
||||||
|
# Administration menu extension
|
||||||
|
Redmine::MenuManager.map :admin_menu do |menu|
|
||||||
|
menu.push :redmine_qbo_lineitems, { controller: 'items', action: 'index' }, icon: 'list', caption: :label_items, html: { class: 'icon icon-list' }
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
module ClassMethods
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
module InstanceMethods
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
Issue.send(:include, IssuePatch)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
Reference in new issue
Block a user