mirror of
https://github.com/rickbarrette/redmine_qbo_lineitems.git
synced 2026-04-02 15:11:58 -04:00
Compare commits
8 Commits
2026.3.10
...
4b561ef4e3
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b561ef4e3 | |||
| 1dadcf37b6 | |||
| ffaee10fef | |||
| 58994e3c7d | |||
| 6365fe6679 | |||
| 4d6c16373a | |||
| 20b7564c38 | |||
| 9820646857 |
13
README.md
13
README.md
@@ -10,8 +10,6 @@ This plugin allows **billable line items** to be attached to a Redmine issue. Wh
|
|||||||
|
|
||||||
* **Redmine:** 6.1+
|
* **Redmine:** 6.1+
|
||||||
|
|
||||||
* **Ruby:** 3.2+
|
|
||||||
|
|
||||||
* **Parent Plugin:** [Redmine QuickBooks Online](https://github.com/rickbarrette/redmine_qbo) (must be installed and configured)
|
* **Parent Plugin:** [Redmine QuickBooks Online](https://github.com/rickbarrette/redmine_qbo) (must be installed and configured)
|
||||||
|
|
||||||
|
|
||||||
@@ -19,9 +17,9 @@ This plugin allows **billable line items** to be attached to a Redmine issue. Wh
|
|||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
| Plugin Version | Redmine Version | Ruby Version |
|
| Plugin Version | Redmine Version | Parent Plugin Version |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 2026.3.6+ | 6.1.x | 3.2+ |
|
| 2026.3.8+ | 6.1.x | 2026.3.9+ |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -87,9 +85,10 @@ Before using this plugin:
|
|||||||
1. Install and configure the parent plugin.
|
1. Install and configure the parent plugin.
|
||||||
|
|
||||||
2. Ensure your **QuickBooks Online** company file is connected.
|
2. Ensure your **QuickBooks Online** company file is connected.
|
||||||
|
|
||||||
3. Verify that the products or services referenced in line items exist in QuickBooks.
|
3. Sync Accounts & Items via plugin settings
|
||||||
|
|
||||||
|
4. Set default income account for new items via plugin settings
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
BIN
Screenshots/accounts.png
Normal file
BIN
Screenshots/accounts.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
BIN
Screenshots/edit item.png
Normal file
BIN
Screenshots/edit item.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
BIN
Screenshots/item.png
Normal file
BIN
Screenshots/item.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
BIN
Screenshots/items.png
Normal file
BIN
Screenshots/items.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 368 KiB |
BIN
Screenshots/settings.png
Normal file
BIN
Screenshots/settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
@@ -15,7 +15,10 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<%= f.number_field :unit_price, step: 0.01, size: 10 %>
|
<%= f.text_field :unit_price,
|
||||||
|
class: "price-field",
|
||||||
|
inputmode: "decimal",
|
||||||
|
autocomplete: "off"%>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
let lastKeyWasTab = false;
|
||||||
|
|
||||||
|
document.addEventListener("keydown", function (e) {
|
||||||
|
lastKeyWasTab = (e.key === "Tab");
|
||||||
|
});
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
function initNestedForms() {
|
function initNestedForms() {
|
||||||
document.querySelectorAll("[data-nested-form]").forEach(function (wrapper) {
|
document.querySelectorAll("[data-nested-form]").forEach(function (wrapper) {
|
||||||
@@ -22,11 +28,28 @@
|
|||||||
Date.now().toString()
|
Date.now().toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
//container.insertAdjacentHTML("beforeend", content);
|
|
||||||
container.insertAdjacentHTML("beforeend", content);
|
container.insertAdjacentHTML("beforeend", content);
|
||||||
|
|
||||||
// initialize autocomplete on the new row
|
const newRow = container.lastElementChild;
|
||||||
initLineItemAutocomplete(container.lastElementChild);
|
|
||||||
|
// Ensure clean state
|
||||||
|
newRow.dataset.autoAdded = "false";
|
||||||
|
|
||||||
|
// Reset defaults
|
||||||
|
const qty = newRow.querySelector(".qty-field");
|
||||||
|
if (qty && !qty.value) qty.value = 1;
|
||||||
|
|
||||||
|
const price = newRow.querySelector(".price-field");
|
||||||
|
if (price) price.value = "";
|
||||||
|
|
||||||
|
// initialize autocomplete
|
||||||
|
initLineItemAutocomplete(newRow);
|
||||||
|
|
||||||
|
// Only focus if NOT tabbing
|
||||||
|
if (!lastKeyWasTab) {
|
||||||
|
const desc = newRow.querySelector(".line-item-description");
|
||||||
|
if (desc) desc.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// REMOVE
|
// REMOVE
|
||||||
@@ -56,9 +79,77 @@
|
|||||||
document.addEventListener("turbo:load", initNestedForms);
|
document.addEventListener("turbo:load", initNestedForms);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
$(document).on("input", ".line-item-description", function(){
|
|
||||||
|
|
||||||
|
// Keep your existing behavior
|
||||||
|
$(document).on("input", ".line-item-description", function () {
|
||||||
let row = $(this).closest(".line-item");
|
let row = $(this).closest(".line-item");
|
||||||
row.find(".item-id-field").val("");
|
row.find(".item-id-field").val("");
|
||||||
|
});
|
||||||
|
|
||||||
});
|
|
||||||
|
// -------------------------------
|
||||||
|
// AUTO-ADD NEW ROW LOGIC
|
||||||
|
// -------------------------------
|
||||||
|
|
||||||
|
// Reset autoAdded flag if cleared
|
||||||
|
document.addEventListener("input", function (e) {
|
||||||
|
if (!e.target.classList.contains("line-item-description")) return;
|
||||||
|
|
||||||
|
const row = e.target.closest(".line-item");
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
if (e.target.value.trim() === "") {
|
||||||
|
row.dataset.autoAdded = "false";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Add row when leaving last description (without breaking TAB flow)
|
||||||
|
document.addEventListener("blur", function (e) {
|
||||||
|
if (!e.target.classList.contains("line-item-description")) return;
|
||||||
|
|
||||||
|
const input = e.target;
|
||||||
|
const row = input.closest(".line-item");
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
const wrapper = input.closest("[data-nested-form]");
|
||||||
|
if (!wrapper) return;
|
||||||
|
|
||||||
|
const container = wrapper.querySelector("[data-nested-form-container]");
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// Active (visible + not destroyed) rows only
|
||||||
|
const rows = Array.from(
|
||||||
|
container.querySelectorAll(wrapper.dataset.wrapperSelector)
|
||||||
|
).filter(r => {
|
||||||
|
const destroy = r.querySelector("input[name*='[_destroy]']");
|
||||||
|
const hidden = window.getComputedStyle(r).display === "none";
|
||||||
|
return !(destroy && destroy.value === "1") && !hidden;
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastRow = rows[rows.length - 1];
|
||||||
|
|
||||||
|
// Only last row
|
||||||
|
if (row !== lastRow) return;
|
||||||
|
|
||||||
|
// Must have content
|
||||||
|
if (input.value.trim() === "") return;
|
||||||
|
|
||||||
|
// Prevent duplicate firing
|
||||||
|
if (row.dataset.autoAdded === "true") return;
|
||||||
|
|
||||||
|
// If TAB, ensure user is leaving the row entirely
|
||||||
|
if (lastKeyWasTab) {
|
||||||
|
const next = document.activeElement;
|
||||||
|
|
||||||
|
if (row.contains(next)) {
|
||||||
|
return; // still inside row → allow normal tabbing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
row.dataset.autoAdded = "true";
|
||||||
|
|
||||||
|
const addButton = wrapper.querySelector("[data-nested-form-add]");
|
||||||
|
if (addButton) addButton.click();
|
||||||
|
|
||||||
|
}, true); // capture phase required for blur
|
||||||
10
init.rb
10
init.rb
@@ -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.10'
|
version '2026.3.11'
|
||||||
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'
|
||||||
@@ -31,6 +31,14 @@ Redmine::Plugin.register :redmine_qbo_lineitems do
|
|||||||
Issue.safe_attributes :line_items_attributes
|
Issue.safe_attributes :line_items_attributes
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
# Dynamically load all Hooks & Patches recursively
|
# Dynamically load all Hooks & Patches recursively
|
||||||
base_dir = File.join(File.dirname(__FILE__), 'lib')
|
base_dir = File.join(File.dirname(__FILE__), 'lib')
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ module RedmineQboLineItems
|
|||||||
end
|
end
|
||||||
|
|
||||||
def view_issues_edit_notes_bottom(context = {})
|
def view_issues_edit_notes_bottom(context = {})
|
||||||
|
return if context[:issue].closed?
|
||||||
context[:controller].send(:render_to_string, {
|
context[:controller].send(:render_to_string, {
|
||||||
partial: 'line_items/issue_form',
|
partial: 'line_items/issue_form',
|
||||||
locals: {
|
locals: {
|
||||||
|
|||||||
Reference in New Issue
Block a user