MyInvois Extension for Manager.io

Hello everyone,

I am pleased to announce the release of the MyInvois Extension for Manager.io. :tada:
This extension is now available for testing and feedback.

What is MyInvois Extension?

The MyInvois Extension is an add-on for Manager.io that enables businesses in Malaysia to comply with LHDNM e-Invoicing requirements.
With this extension, you can:

  • Integrate Manager.io with the MyInvois system.
  • Submit Sales Invoices and Credit Notes directly to LHDNM.
  • Track document statuses (Submitted, Valid, Invalid, Cancelled).
  • Perform taxpayer lookups using official LHDNM QR codes.

:warning: Important Notice

  • Ensure credentials match the selected environment (PreProduction or Production).
  • Do not use real business data in PreProduction mode.
  • After successful testing, switch to Production with real data.

How to Use

:one: Registering the Extension

  • Go to Manager β†’ Settings β†’ Extensions.
  • Create a New Extension with the following details:
    • Name: MyInvois-Extension
    • Source: Url
    • Endpoint: https://myinvoisextension.azurewebsites.net
    • Placement: customers, sales-invoice-view, or credit-note-view
  • Since multi-placement is not yet supported, repeat the process for each placement.

:two: Integration Wizard

  • Fill in your business details, credentials, and select the environment (PreProduction/Production).
  • Verify credentials with the LHDNM system.
  • Document v1.0 β†’ no certificate required.
  • Document v1.1 β†’ a valid certificate from an LHDNM-recognized provider is required.

:three: Submit Invoice / Credit Note

  • Create a Sales Invoice or Credit Note in Manager.
  • Open the document and click the MyInvois Extension button.
  • The document will be converted into an e-Invoice compliant with LHDNM.
  • Click Submit to send it to LHDNM.
  • The status will be updated in Manager:
    • Success β†’ Submitted
    • Errors β†’ displayed for correction
  • Use Check Status to retrieve the latest status from LHDNM.
  • When the document is Valid or Cancelled, a QR Code will be generated and saved into the Manager document.

:four: Taxpayer Lookup

  • Verify taxpayer details using QR codes. Supported methods:
    • Scan with camera
    • Drag & drop a QR image
    • Upload a QR file from device
    • Manually type QR content
  • Retrieved taxpayer details can be saved as Customer Data in Manager, ensuring consistency with the official LHDNM database.

Feedback

I kindly invite everyone to try out the MyInvois Extension in PreProduction and share your feedback. Please report any issues or bugs you encounter and feel free to suggest ideas for improvements. Your input will help shape the future development of this extension and ensure it becomes more useful for the community.

@wazhanudin, @neobks91, @zmm

8 Likes

Thank you so much for your kind efforts @Mabaega on this project.

I will try out the extension and give the feedback as soon as possible.

:star_struck:

Hi, thank you for your effort in MyInvois integration with Manager.

For the business details that filled in at Integration Wizard, it’s only 1 time update, then next time will skip this stage to submit the MyInvois direct or we have to fill in own business details every time we do submission of an invoice ?

1 Like

Hi, where to find the integration Wizard?

@Amirah_Mahirah_Abdul Welcome to forum, on top :outbox_tray:of this post.

Hi Everyone,

Great to hear Manager can work with e-invoice. Does it also work for Self Billed and Consolidate Invoices?

At the moment, the integration only supports two invoice types:

Code –  Description 
01 – Invoice 
02 – Credit Note

For the invoice type you mentioned, could you please share an example of how you normally create it in Manager? This will help me better understand the differences between that invoice type and a standard invoice or credit note.

1 Like

There is 1 small error during the recurring sales invoice, the Document Issue Date doesn’t appear. Need to reopen a new invoice for it to appear.

1 Like

@neobks91

I have not yet touched the Recurring Sales Invoice section.

Could you try updating the script in the Document Status custom field description?

<div id='MainScriptContainer'>
  <script>
    document.addEventListener('DOMContentLoaded', () => {

      const vModelForm = document.getElementById('v-model-form');
      if (!vModelForm) return;

      const documentIssueDateGuid = '65bf01ec-9d33-45c5-b441-4b29b1bfdfba';

      // ------------------------------------------------------------
      // Find Document Status input
      // ------------------------------------------------------------
      function findDocumentStatusInput() {
        const labels = vModelForm.querySelectorAll('label');
        for (const label of labels) {
          if (label.textContent.trim().toLowerCase() === 'document status') {
            const grp = label.closest('.form-group') || label.parentElement;
            if (!grp) continue;

            const inp = grp.querySelector('input, textarea, select, input[type=hidden]');
            if (inp && inp.value && inp.value.toString().trim() !== '') {
              return inp;
            }
          }
        }
        return null;
      }

      const documentStatusInput = findDocumentStatusInput();
      let documentStatusHasValue = !!documentStatusInput;

      // ------------------------------------------------------------
      // Detect Update Button
      // ------------------------------------------------------------
      const updateButton = document.querySelector('button.btn.btn-success');

      // Jika status ada tapi tidak ada update button β†’ reset
      if (documentStatusHasValue && !updateButton && documentStatusInput) {
        documentStatusInput.value = '';
        documentStatusHasValue = false;
      }

      // ------------------------------------------------------------
      // Lock Date Picker
      // ------------------------------------------------------------
      const dateInput = document.querySelector('.mx-input');
      if (dateInput) {
        dateInput.readOnly = true;
        dateInput.setAttribute('readonly', 'readonly');
        dateInput.style.opacity = '0.8';

        const cal = document.querySelector('.mx-icon-calendar');
        if (cal) {
          cal.style.pointerEvents = 'none';
          cal.style.opacity = '0.5';
        }
      }

      // ------------------------------------------------------------
      // Lock form if DocumentStatus has value
      // ------------------------------------------------------------
      if (documentStatusHasValue) {

        const allowedButtons = [
          ...document.querySelectorAll('button.btn.btn-success, button.btn.btn-primary, button.btn.btn-default')
        ];

        vModelForm.querySelectorAll('input, textarea, select, button').forEach(control => {
          const isCheckbox = control.tagName === 'INPUT' && control.type === 'checkbox';
          const isAllowedButton = allowedButtons.includes(control);

          if (!isCheckbox && !isAllowedButton) {

            if (control.tagName === 'INPUT' || control.tagName === 'TEXTAREA') {
              control.readOnly = true;
              control.setAttribute('readonly', 'readonly');

            } else if (control.tagName === 'SELECT') {
              control.style.pointerEvents = 'none';
              control.style.opacity = '0.6';

            } else if (control.tagName === 'BUTTON') {
              control.style.pointerEvents = 'none';
              control.style.opacity = '0.6';
            }

          } else if (isCheckbox) {
            control.style.pointerEvents = 'none';
            control.style.opacity = '0.8';
          }
        });

        // v-select
        vModelForm.querySelectorAll('.v-select').forEach(vs => {
          vs.style.pointerEvents = 'none';
          vs.style.opacity = '0.6';
          vs.querySelectorAll('input.vs__search, .vs__clear').forEach(el => {
            try {
              el.readOnly = true;
              el.setAttribute('readonly', 'readonly');
              el.style.pointerEvents = 'none';
            } catch (e) {}
          });
        });

        // select2
        vModelForm.querySelectorAll('.select2-container').forEach(s2 => {
          s2.style.pointerEvents = 'none';
          s2.style.opacity = '0.6';
          s2.querySelectorAll('input').forEach(el => {
            try {
              el.readOnly = true;
              el.setAttribute('readonly','readonly');
            } catch(e) {}
          });
        });

        // Remove handle delete
        vModelForm.querySelectorAll('td.handle').forEach(td => {
          td.style.pointerEvents = 'none';
          td.style.opacity = '0.5';
        });

        vModelForm.querySelectorAll('a.btn.btn-danger').forEach(link => {
          link.style.pointerEvents = 'none';
          link.style.opacity = '0.6';
          link.setAttribute('aria-disabled', 'true');
          link.removeAttribute('href');
        });
      }

      // ------------------------------------------------------------
      // Reference checkbox & text
      // ------------------------------------------------------------
      const refGroup = Array.from(vModelForm.querySelectorAll('.input-group'))
        .find(g => g.querySelector('input[type=checkbox]') && g.querySelector('input[type=text]'));

      if (refGroup) {
        const refCheckbox = refGroup.querySelector('input[type=checkbox]');
        const refText = refGroup.querySelector('input[type=text]');

        if (refCheckbox && refText) {
          if (documentStatusHasValue) {
            app.AutomaticReference = false;
            refCheckbox.checked = false;
            refCheckbox.style.pointerEvents = 'none';
            refCheckbox.style.opacity = '0.8';
            refText.readOnly = true;
            refText.setAttribute('readonly', 'readonly');
          } else {
            app.AutomaticReference = true;
            refCheckbox.checked = true;
            refCheckbox.style.pointerEvents = 'auto';
            refCheckbox.style.opacity = '1';
            refText.readOnly = false;
            refText.removeAttribute('readonly');
          }
        }
      }

      // ------------------------------------------------------------
      // CREATE BUTTON β†’ Set Document Issue Date
      // (Mode Create)
      // ------------------------------------------------------------
      const createButton = document.querySelector('button.btn.btn-primary');

      if (createButton && typeof app !== 'undefined' && app?.CustomFields2?.Strings) {
        const now = new Date();
        const currentDate = now.toISOString().slice(0,19).replace('T',' ');
        const currentDateOnly = now.toISOString().slice(0,10);

        app.CustomFields2.Strings[documentIssueDateGuid] = currentDate;
        app.IssueDate = currentDateOnly;
      }

    // ------------------------------------------------------------
	// NEW FEATURE REQUESTED BY USER
	// If UpdateButton exists AND documentIssueDate empty β†’ create new timestamp
	// ------------------------------------------------------------
	if (updateButton &&
		typeof app !== 'undefined' &&
		app?.CustomFields2?.Strings &&
		!app.CustomFields2.Strings[documentIssueDateGuid]) {

		// Use existing app.IssueDate for the date
		const issueDate = app.IssueDate; // expected format: yyyy-MM-dd

		// Current time
		const now = new Date();
		const hh = String(now.getHours()).padStart(2, '0');
		const mm = String(now.getMinutes()).padStart(2, '0');
		const ss = String(now.getSeconds()).padStart(2, '0');

		// Combine: existing date + current time
		const finalDateTime = `${issueDate} ${hh}:${mm}:${ss}`;

		// Apply
		app.CustomFields2.Strings[documentIssueDateGuid] = finalDateTime;

		// Do NOT change app.IssueDate
	}


      // ------------------------------------------------------------
      // Remove script block
      // ------------------------------------------------------------
      document.getElementById('MainScriptContainer')?.remove();

    });
  </script>
</div>

The new script should add the Document Issue Date in Edit mode after batch creation from a recurring sales invoice. Document Issue Date should be generated based on the invoice date and the time when you perform the first update.

Please make sure to back up your data before trying this and let me know if this approach works as expected.