Automatically extracts invoice data and fills payment registration forms on Moneybird
Size
10.6 KB
Version
1.1.2
Created
Jan 14, 2026
Updated
21 days ago
1// ==UserScript==
2// @name Moneybird Auto Payment Registration
3// @description Automatically extracts invoice data and fills payment registration forms on Moneybird
4// @version 1.1.2
5// @match https://*.moneybird.com/*
6// @icon https://assets-app-cdn.moneybird.com/assets/favicon-b1fb00eb89b4530acc973e4c2cd93f58f7fc4095a79f9bb2a7d88f692b994273.ico
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 // Debounce function to prevent multiple rapid executions
12 function debounce(func, wait) {
13 let timeout;
14 return function executedFunction(...args) {
15 const later = () => {
16 clearTimeout(timeout);
17 func(...args);
18 };
19 clearTimeout(timeout);
20 timeout = setTimeout(later, wait);
21 };
22 }
23
24 // Store extracted data in session storage for cross-page access
25 async function storeInvoiceData(invoiceDate, contactName) {
26 await GM.setValue('moneybird_invoice_date', invoiceDate);
27 await GM.setValue('moneybird_contact_name', contactName);
28 console.log('Stored invoice data:', { invoiceDate, contactName });
29 }
30
31 async function getStoredInvoiceData() {
32 const invoiceDate = await GM.getValue('moneybird_invoice_date', null);
33 const contactName = await GM.getValue('moneybird_contact_name', null);
34 return { invoiceDate, contactName };
35 }
36
37 // Extract invoice data from document page
38 function extractInvoiceData() {
39 const dateElement = document.querySelector('p.document-date');
40 const contactElement = document.querySelector('p.document-contact a');
41
42 if (dateElement && contactElement) {
43 const invoiceDate = dateElement.textContent.trim();
44 const contactName = contactElement.textContent.trim();
45
46 console.log('Extracted Invoice Date:', invoiceDate);
47 console.log('Extracted Contact Name:', contactName);
48
49 return { invoiceDate, contactName };
50 }
51 return null;
52 }
53
54 // Add "Auto Register Payment" button to document page
55 function addAutoRegisterButton() {
56 // Check if button already exists
57 if (document.querySelector('#auto-register-payment-btn')) {
58 return;
59 }
60
61 // Try to find the direct register button first
62 let registerButton = document.querySelector('a[title="Registreer betaling"].btn.btn--secondary');
63
64 // If not found, look for the Betaling dropdown button
65 if (!registerButton) {
66 registerButton = document.querySelector('a[title="Betaling"].btn.btn--secondary');
67 }
68
69 if (!registerButton) {
70 return;
71 }
72
73 // Create auto-register button
74 const autoButton = document.createElement('a');
75 autoButton.id = 'auto-register-payment-btn';
76 autoButton.className = 'btn btn--primary';
77 autoButton.style.marginLeft = '10px';
78 autoButton.href = '#';
79 autoButton.innerHTML = '<span class="btn__text">Auto Registreer Betaling</span>';
80
81 autoButton.addEventListener('click', async (e) => {
82 e.preventDefault();
83
84 // Extract and store data
85 const data = extractInvoiceData();
86 if (data) {
87 await storeInvoiceData(data.invoiceDate, data.contactName);
88
89 // Click the register payment button
90 console.log('Clicking Register Payment button...');
91
92 // If it's a dropdown button, we need to open it first and then click the register option
93 if (registerButton.title === 'Betaling') {
94 console.log('Opening Betaling dropdown...');
95 registerButton.click();
96
97 // Wait for dropdown to open
98 await new Promise(resolve => setTimeout(resolve, 300));
99
100 // Find and click "Registreer betaling" in the dropdown
101 const registerOption = document.querySelector('a[href*="/payments/new"]');
102 if (registerOption) {
103 console.log('Clicking Registreer betaling option...');
104 registerOption.click();
105 } else {
106 console.error('Could not find Registreer betaling option in dropdown');
107 }
108 } else {
109 registerButton.click();
110 }
111 } else {
112 console.error('Failed to extract invoice data');
113 alert('Could not extract invoice data. Please check the page structure.');
114 }
115 });
116
117 // Insert button next to the original register button
118 registerButton.parentNode.insertBefore(autoButton, registerButton.nextSibling);
119 console.log('Auto Register Payment button added');
120 }
121
122 // Handle payment method selection page
123 async function handlePaymentMethodPage() {
124 console.log('On payment method selection page');
125
126 // Wait for the page to fully load
127 await new Promise(resolve => setTimeout(resolve, 1000));
128
129 const balanceLink = document.querySelector('div.link-list__item[title="Verrekenen met balanscategorie"] a');
130
131 if (balanceLink) {
132 console.log('Found "Verrekenen met balanscategorie" link, clicking...');
133 balanceLink.click();
134 } else {
135 console.log('Balance category link not found yet, will retry...');
136 }
137 }
138
139 // Fill in the payment form
140 async function fillPaymentForm() {
141 console.log('On payment form page');
142
143 // Wait for form to load
144 await new Promise(resolve => setTimeout(resolve, 1500));
145
146 const { invoiceDate, contactName } = await getStoredInvoiceData();
147
148 if (!invoiceDate || !contactName) {
149 console.log('No stored invoice data found');
150 return;
151 }
152
153 console.log('Filling form with:', { invoiceDate, contactName });
154
155 // Fill ledger account (Tussenrekening) - Handle custom dropdown
156 const ledgerAccountDropdown = document.querySelector('#payment_ledger_account_id_inner');
157 if (ledgerAccountDropdown) {
158 const targetValue = `Tussenrekening ${contactName}`;
159 console.log('Looking for ledger account:', targetValue);
160
161 // Click to open dropdown
162 ledgerAccountDropdown.click();
163 console.log('Clicked dropdown to open');
164
165 // Wait for dropdown to open
166 await new Promise(resolve => setTimeout(resolve, 500));
167
168 // Find the option in the dropdown list
169 const dropdownList = document.querySelector('.select__list--dropdown');
170 if (dropdownList) {
171 const dropdownOptions = dropdownList.querySelectorAll('.select__item');
172 console.log('Found dropdown options:', dropdownOptions.length);
173
174 let foundOption = null;
175 dropdownOptions.forEach(option => {
176 const optionText = option.textContent.trim();
177 console.log('Checking option:', optionText);
178 if (optionText === targetValue) {
179 foundOption = option;
180 }
181 });
182
183 if (foundOption) {
184 console.log('Found matching option, clicking...');
185 foundOption.click();
186 await new Promise(resolve => setTimeout(resolve, 300));
187 console.log('Set ledger account to:', targetValue);
188 } else {
189 console.error('Could not find ledger account option:', targetValue);
190 // Log all available options
191 const allOptions = Array.from(dropdownOptions).map(o => o.textContent.trim());
192 console.log('Available options:', allOptions);
193 }
194 } else {
195 console.error('Dropdown list not found');
196 }
197 } else {
198 console.log('Ledger account dropdown not found');
199 }
200
201 // Fill payment date
202 const dateInput = document.querySelector('input[name="payment[payment_date]"]');
203 if (dateInput) {
204 console.log('Found date input, current value:', dateInput.value);
205
206 // Clear existing value
207 dateInput.value = '';
208 dateInput.focus();
209
210 // Set new value
211 dateInput.value = invoiceDate;
212 console.log('Set payment date to:', invoiceDate);
213
214 // Trigger events to ensure the change is registered
215 dateInput.dispatchEvent(new Event('input', { bubbles: true }));
216 dateInput.dispatchEvent(new Event('change', { bubbles: true }));
217 dateInput.dispatchEvent(new Event('blur', { bubbles: true }));
218
219 // Verify the value was set
220 console.log('Date input value after setting:', dateInput.value);
221 } else {
222 console.log('Date input not found');
223 }
224
225 console.log('Form filling completed');
226 }
227
228 // Detect which page we're on and take appropriate action
229 async function detectAndAct() {
230 const url = window.location.href;
231
232 // Document detail page (where we extract data)
233 if (url.match(/\/documents\/\d+$/) && !url.includes('/payments')) {
234 console.log('On document detail page');
235 addAutoRegisterButton();
236 }
237 // Payment method selection page
238 else if (url.includes('/payments/new')) {
239 console.log('On payment method selection page');
240 await handlePaymentMethodPage();
241 }
242 // Payment form page (balance settlement)
243 else if (url.includes('/payments/balance_settlement')) {
244 console.log('On payment form page');
245 await fillPaymentForm();
246 }
247 }
248
249 // Initialize the script
250 async function init() {
251 console.log('Moneybird Auto Payment Registration initialized');
252
253 // Run initial detection
254 await detectAndAct();
255
256 // Watch for DOM changes (for dynamic content loading)
257 const debouncedDetect = debounce(detectAndAct, 500);
258 const observer = new MutationObserver(debouncedDetect);
259
260 observer.observe(document.body, {
261 childList: true,
262 subtree: true
263 });
264 }
265
266 // Wait for page to be ready
267 if (document.readyState === 'loading') {
268 document.addEventListener('DOMContentLoaded', init);
269 } else {
270 init();
271 }
272})();