Enhancing User Experience with Custom Buttons on DocType in ERPNext

In this blog we are trying to create buttons on our Doctype Sales Opportunity behind which the filtered list view of a specific link, here Quotation, can be found.

 · 27 min read

Introduction :


In ERPNext, a popular open-source enterprise resource planning (ERP) system, the user interface plays a crucial role in improving productivity and user satisfaction. One powerful feature that can greatly enhance the user experience is the ability to add custom buttons on a DocType, enabling users to access different lists with a single click. In this blog post, we will explore how to leverage this functionality to improve navigation and streamline workflows within ERPNext.


In this example we are trying to create buttons on our DocType Sales Opportunity behind which the filtered list view of a specific link, here Quoation, can be found. The buttons should be appealing and understandable for the user of the system. Here are a few examples that were created with the help of ChatGPT .


Table with blue buttons :



the script :


frappe.ui.form.on('Sales Opportunity', {                                                                                                                                                                                                                                                                                                                                                    

refresh: function(frm) {

// Get the linked Quotations

frappe.call({

method: 'frappe.client.get_list',

args: {

doctype: 'Quotation',

filters: {

sales_opportunity: frm.doc.name

},

fields: ['status']

},

callback: function(response) {

var data = response && response.message;

// Count the Quotations by status

var counts = {};

if (data && data.length > 0) {

data.forEach(function(row) {

if (row.status) {

if (counts[row.status]) {

counts[row.status]++;

} else {

counts[row.status] = 1;

}

}

});

}

// Create or update the visual section with the table

var section = frm.dashboard.add_section(__('Quotations'));

var html = `<style>

.sales-opportunity-table {

font-size: 70%;

}

</style>

<table class="table table-bordered sales-opportunity-table">

<thead>

<tr>

<th>Type</th>

<th>Status</th>

<th>Amount</th>

<th>Action</th>

</tr>

</thead>

<tbody>`;

if (Object.keys(counts).length > 0) {

Object.keys(counts).forEach(function(status) {

html += `<tr>

<td>Quotation</td>

<td>${status}</td>

<td>${counts[status]}</td>

<td><button class="btn btn-primary btn-sm view-opportunity-btn" data-status="${status}">Open in List View</button></td>

</tr>`;

});

} else {

html += `<tr>

<td>Opportunity</td>

<td colspan="3" align="center">No Quotations</td>

</tr>`;

}

html += `</tbody>

</table>`;

section.html(html);

// Add click event to the button

section.find('.view-opportunity-btn').on('click', function() {

var status = $(this).data('status');

viewQuotationList(status, frm.doc.name);

});

}

});

}

});

function viewQuotationList(status, salesOpportunity) {

// Redirect to the Quotation List view with the status filter applied

frappe.set_route('List', 'Quotation', { 'status': status, 'sales_opportunity': salesOpportunity });

}


Table with button "List View" from the list view :



the script :


frappe.ui.form.on('Sales Opportunity', {

  refresh: function(frm) {

      // Get the linked Quotations

      frappe.call({

          method: 'frappe.client.get_list',

          args: {

              doctype: 'Quotation',

              filters: {

                  sales_opportunity: frm.doc.name

              },

              fields: ['status']

          },

          callback: function(response) {

              var data = response && response.message;

              // Count the Quotations by status

              var counts = {};

              if (data && data.length > 0) {

                  data.forEach(function(row) {

                      if (row.status) {

                          if (counts[row.status]) {

                              counts[row.status]++;

                          } else {

                              counts[row.status] = 1;

                          }

                      }

                  });

              }

              // Create or update the visual section with the table

              var section = frm.dashboard.add_section(__('Quotations'));

              var html = `<style>

                              .sales-opportunity-table {

                                  font-size: 70%;

                              }

                          </style>

                          <table class="table table-bordered sales-opportunity-table">

                              <thead>

                                  <tr>

                                      <th>Type</th>

                                      <th>Status</th>

                                      <th>Amount</th>

                                      <th>Action</th>

                                  </tr>

                              </thead>

                              <tbody>`;

              if (Object.keys(counts).length > 0) {

                  Object.keys(counts).forEach(function(status) {

                      html += `<tr>

                                  <td>Quotation</td>

                                  <td>${status}</td>

                                  <td>${counts[status]}</td>

                                  <td><button class="btn btn-secondary btn-sm view-opportunity-btn" data-status="${status}" data-doctype="Quotation"><i class="fa fa-list"></i> View Quotation List</button></td>

                              </tr>`;

                  });

              } else {

                  html += `<tr>

                              <td>Opportunity</td>

                              <td colspan="3" align="center">No Quotations</td>

                          </tr>`;

              }

              html += `</tbody>

                      </table>`;

              section.html(html);

              // Add click event to the button

              section.find('.view-opportunity-btn').on('click', function() {

                  var status = $(this).data('status');

                  var doctype = $(this).data('doctype');

                  viewQuotationList(status, doctype, frm.doc.name);

              });

          }

      });

  }

});

function viewQuotationList(status, doctype, salesOpportunity) {

  // Redirect to the Quotation List view with the status and sales opportunity filter applied

  frappe.set_route('List', doctype, { 'status': status, 'sales_opportunity': salesOpportunity });

}


Table with coloured buttons :



the script :


frappe.ui.form.on('Sales Opportunity', {

refresh: function(frm) {

// Get the linked Quotations

frappe.call({

method: 'frappe.client.get_list',

args: {

doctype: 'Quotation',

filters: {

sales_opportunity: frm.doc.name

},

fields: ['status']

},

callback: function(response) {

var data = response && response.message;

// Count the Quotations by status

var counts = {};

if (data && data.length > 0) {

data.forEach(function(row) {

if (row.status) {

if (counts[row.status]) {

counts[row.status]++;

} else {

counts[row.status] = 1;

}

}

});

}

// Create or update the visual section with the table

var section = frm.dashboard.add_section(__('Quotations'));

var html = `<style>

.sales-opportunity-table {

font-size: 100%;

}

.status-draft {

background-color: #f2f2f2;

color: #495057;

}

.status-open {

background-color: #28a745;

color: #fff;

}

.status-submitted {

background-color: #ffc107;

color: #fff;

}

.status-accepted {

background-color: #17a2b8;

color: #fff;

}

.status-rejected {

background-color: #dc3545;

color: #fff;

}

</style>

<table class="table table-bordered sales-opportunity-table">

<thead>

<tr>

<th>Quotations</th>

</tr>

</thead>

<tbody>

<tr>`;

// Add buttons for each status

var statusOrder = ['Draft', 'Open', 'Submitted', 'Accepted', 'Rejected'];

statusOrder.forEach(function(status) {

var statusClass = getStatusClass(status);

var amount = counts[status] || 0;

var buttonText = `Open List View with <b>${amount}</b> Quotations in <i>${status}</i>`;

html += `<td><button class="btn btn-sm view-opportunity-btn ${statusClass}" data-status="${status}" data-doctype="Quotation">${buttonText}</button></td>`;

});

html += `</tr>

</tbody>

</table>`;

section.html(html);

// Add click event to the buttons

section.find('.view-opportunity-btn').on('click', function() {

var status = $(this).data('status');

var doctype = $(this).data('doctype');

viewQuotationList(status, doctype, frm.doc.name);

});

}

});

}

});

function viewQuotationList(status, doctype, salesOpportunity) {

// Redirect to the Quotation List view with the status and sales opportunity filter applied

frappe.set_route('List', doctype, { 'status': status, 'sales_opportunity': salesOpportunity });

}

function getStatusClass(status) {

// Define the class for each status

var statusClass = {

'Draft': 'status-draft',

'Open': 'status-open',

'Submitted': 'status-submitted',

'Accepted': 'status-accepted',

'Rejected': 'status-rejected'

};

return statusClass[status] || '';

}


Using badges instead of buttons to save space!  :



the script :


frappe.ui.form.on('Sales Opportunity', {

  refresh: function(frm) {

      // Get the linked Quotations

      frappe.call({

          method: 'frappe.client.get_list',

          args: {

              doctype: 'Quotation',

              filters: {

                  sales_opportunity: frm.doc.name

              },

              fields: ['status']

          },

          callback: function(response) {

              var data = response && response.message;

              // Count the Quotations by status

              var quotationCounts = {};

              if (data && data.length > 0) {

                  data.forEach(function(row) {

                      if (row.status) {

                          if (quotationCounts[row.status]) {

                              quotationCounts[row.status]++;

                          } else {

                              quotationCounts[row.status] = 1;

                          }

                      }

                  });

              }

              // Get the linked Sales Orders

              frappe.call({

                  method: 'frappe.client.get_list',

                  args: {

                      doctype: 'Sales Order',

                      filters: {

                          sales_opportunity: frm.doc.name

                      },

                      fields: ['status']

                  },

                  callback: function(response) {

                      var data = response && response.message;

                      // Count the Sales Orders by status

                      var orderCounts = {};

                      if (data && data.length > 0) {

                          data.forEach(function(row) {

                              if (row.status) {

                                  if (orderCounts[row.status]) {

                                      orderCounts[row.status]++;

                                  } else {

                                      orderCounts[row.status] = 1;

                                  }

                              }

                          });

                      }

                      // Create or update the visual section with the table

                      var section = frm.dashboard.add_section(__('Sales Documents'));

                      var html = `<style>

                                      .sales-documents-table {

                                          font-size: 100%;

                                      }

                                      .badge-closed {

                                          background-color: green;

                                          color: #fff;

                                      }

                                      .badge-draft {

                                          background-color: red;

                                          color: #495057;

                                      }

                                      .badge-overdue {

                                          background-color: red;

                                          color: #495057;

                                      }

                                      .badge-open {

                                          background-color: orange;

                                          color: #fff;

                                      }

                                      .status-submitted {

                                          background-color: #ffc107;

                                          color: #fff;

                                      }

                                      .status-accepted {

                                          background-color: #17a2b8;

                                          color: #fff;

                                      }

                                      .status-rejected {

                                          background-color: #dc3545;

                                          color: #fff;

                                      }

                                      .badge-to_deliver_and_bill {

                                          background-color: orange;

                                          color: #fff;

                                      }

                                  </style>

                                  <table class="table table-bordered sales-documents-table">

                                      <thead>

                                          <tr>

                                              <th>Document Type</th>

                                              <th>Documents</th>

                                          </tr>

                                      </thead>

                                      <tbody>

                                          <tr>

                                              <td>Quotation</td>

                                              <td>`;

                      // Add badges for Quotations

                      var quotationStatusOrder = ['Draft', 'Open', 'Submitted', 'Accepted', 'Rejected'];

                      quotationStatusOrder.forEach(function(status) {

                          var amount = quotationCounts[status] || 0;

                          if (amount > 0) {

                              var badgeClass = getStatusBadgeClass(status);

                              var badgeText = `Open (${amount}) Quotations in ${status}`;

                              var linkUrl = getQuotationListUrl(status, frm.doc.name);

                              html += `<a class="badge ${badgeClass}" href="${linkUrl}">${badgeText}</a> `;

                          }

                      });

                      html += `</td>

                              </tr>

                              <tr>

                                  <td>Sales Order</td>

                                  <td>`;

                      // Add badges for Sales Orders

                      var orderStatusOrder = ['Draft', 'Submitted', 'Completed', 'To Deliver and Bill', 'Overdue', 'Closed'];

                      orderStatusOrder.forEach(function(status) {

                          var amount = orderCounts[status] || 0;

                          if (amount > 0) {

                              var badgeClass = getStatusBadgeClass(status);

                              var badgeText = `Open (${amount}) Sales Orders in ${status}`;

                              var linkUrl = getSalesOrderListUrl(status, frm.doc.name);

                              html += `<a class="badge ${badgeClass}" href="${linkUrl}">${badgeText}</a> `;

                          }

                      });

                      html += `</td>

                              </tr>

                          </tbody>

                      </table>`;

                      section.html(html);

                  }

              });

          }

      });

  }

});

function getStatusBadgeClass(status) {

  // Define the class for each status badge

  var statusBadgeClass = {

      'Closed': 'badge-closed',

      'Draft': 'badge-draft',

      'Open': 'badge-open',

      'Overdue': 'badge-overdue',

      'Submitted': 'badge-warning',

      'Accepted': 'badge-info',

      'Rejected': 'badge-danger',

      'Completed': 'badge-success',

      'To Deliver and Bill': 'badge-to_deliver_and_bill'

  };

  return statusBadgeClass[status] || 'badge-secondary';

}

function getQuotationListUrl(status, salesOpportunity) {

  // Generate the Quotation List URL with the status and sales opportunity filter

  var url = frappe.urllib.get_base_url();

  url += `/app/list/Quotation?status=${encodeURIComponent(status)}&sales_opportunity=${encodeURIComponent(salesOpportunity)}`;

  return url;

}

function getSalesOrderListUrl(status, salesOpportunity) {

  // Generate the Sales Order List URL with the status and sales opportunity filter

  var url = frappe.urllib.get_base_url();

  url += `/app/list/Sales%20Order?status=${encodeURIComponent(status)}&sales_opportunity=${encodeURIComponent(salesOpportunity)}`;

  return url;

}


accordion :



the script:


frappe.ui.form.on('Sales Opportunity', {

  refresh: function(frm) {

      // Get the linked Quotations

      frappe.call({

          method: 'frappe.client.get_list',

          args: {

              doctype: 'Quotation',

              filters: {

                  sales_opportunity: frm.doc.name

              },

              fields: ['status']

          },

          callback: function(response) {

              var data = response && response.message;

              // Count the Quotations by status

              var quotationCounts = {};

              if (data && data.length > 0) {

                  data.forEach(function(row) {

                      if (row.status) {

                          if (quotationCounts[row.status]) {

                              quotationCounts[row.status]++;

                          } else {

                              quotationCounts[row.status] = 1;

                          }

                      }

                  });

              }

              // Get the linked Sales Orders

              frappe.call({

                  method: 'frappe.client.get_list',

                  args: {

                      doctype: 'Sales Order',

                      filters: {

                          sales_opportunity: frm.doc.name

                      },

                      fields: ['status']

                  },

                  callback: function(response) {

                      var data = response && response.message;

                      // Count the Sales Orders by status

                      var orderCounts = {};

                      if (data && data.length > 0) {

                          data.forEach(function(row) {

                              if (row.status) {

                                  if (orderCounts[row.status]) {

                                      orderCounts[row.status]++;

                                  } else {

                                      orderCounts[row.status] = 1;

                                  }

                              }

                          });

                      }

                      // Create or update the visual section with the accordion

                      var section = frm.dashboard.add_section(__('Sales Documents'));

                      var accordionHtml = `<style>

                                              .sales-accordion {

                                                  font-size: 100%;

                                              }

                                              .sales-accordion .accordion-title {

                                                  background-color: #f5f5f5;

                                                  color: #333;

                                                  cursor: pointer;

                                                  padding: 10px;

                                                  border: none;

                                                  text-align: left;

                                                  outline: none;

                                                  font-weight: bold;

                                                  transition: background-color 0.3s;

                                              }

                                              .sales-accordion .accordion-content {

                                                  padding: 10px;

                                                  display: none;

                                                  overflow: hidden;

                                                  background-color: #fff;

                                                  border: 1px solid #e7e7e7;

                                              }

                                              .sales-accordion .accordion-content a {

                                                  display: block;

                                                  margin-bottom: 5px;

                                              }

                                              .sales-accordion .accordion-content a:hover {

                                                  text-decoration: underline;

                                              }

                                              .sales-accordion .accordion-title.active {

                                                  background-color: #ccc;

                                              }

                                              .sales-accordion .accordion-content.active {

                                                  display: block;

                                              }

                                          </style>

                                          <div class="sales-accordion">`;

                      // Add accordion for Quotations

                      var quotationStatusOrder = ['Draft', 'Open', 'Submitted', 'Accepted', 'Rejected'];

                      accordionHtml += `<button class="accordion-title">Quotation</button>

                                        <div class="accordion-content">`;

                      quotationStatusOrder.forEach(function(status) {

                          var amount = quotationCounts[status] || 0;

                          if (amount > 0) {

                              var badgeClass = getStatusBadgeClass(status);

                              var badgeText = `Open (${amount}) Quotations in ${status}`;

                              var linkUrl = getQuotationListUrl(status, frm.doc.name);

                              accordionHtml += `<a class="badge ${badgeClass}" href="${linkUrl}">${badgeText}</a>`;

                          }

                      });

                      accordionHtml += `</div>`;

                      // Add accordion for Sales Orders

                      var orderStatusOrder = ['Draft', 'Submitted', 'Completed', 'To Deliver and Bill', 'Overdue', 'Closed'];

                      accordionHtml += `<button class="accordion-title">Sales Order</button>

                                        <div class="accordion-content">`;

                      orderStatusOrder.forEach(function(status) {

                          var amount = orderCounts[status] || 0;

                          if (amount > 0) {

                              var badgeClass = getStatusBadgeClass(status);

                              var badgeText = `Open (${amount}) Sales Orders in ${status}`;

                              var linkUrl = getSalesOrderListUrl(status, frm.doc.name);

                              accordionHtml += `<a class="badge ${badgeClass}" href="${linkUrl}">${badgeText}</a>`;

                          }

                      });

                      accordionHtml += `</div></div>`;

                      section.html(accordionHtml);

                      // Add event listener to toggle accordion content

                      var accordionTitles = section.find('.accordion-title');

                      accordionTitles.on('click', function() {

                          var accordionContent = $(this).next('.accordion-content');

                          accordionContent.slideToggle();

                          $(this).toggleClass('active');

                          accordionContent.toggleClass('active');

                      });

                  }

              });

          }

      });

  }

});

function getStatusBadgeClass(status) {

  // Define the class for each status badge

  var statusBadgeClass = {

      'Closed': 'badge-closed',

      'Draft': 'badge-draft',

      'Open': 'badge-open',

      'Overdue': 'badge-overdue',

      'Submitted': 'badge-warning',

      'Accepted': 'badge-info',

      'Rejected': 'badge-danger',

      'Completed': 'badge-success',

      'To Deliver and Bill': 'badge-to_deliver_and_bill'

  };

  return statusBadgeClass[status] || 'badge-secondary';

}

function getQuotationListUrl(status, salesOpportunity) {

  // Generate the Quotation List URL with the status and sales opportunity filter

  var url = frappe.urllib.get_base_url();

  url += `/app/list/Quotation?status=${encodeURIComponent(status)}&sales_opportunity=${encodeURIComponent(salesOpportunity)}`;

  return url;

}

function getSalesOrderListUrl(status, salesOpportunity) {

  // Generate the Sales Order List URL with the status and sales opportunity filter

  var url = frappe.urllib.get_base_url();

  url += `/app/list/Sales%20Order?status=${encodeURIComponent(status)}&sales_opportunity=${encodeURIComponent(salesOpportunity)}`;

  return url;

}



Why Custom Buttons?


Custom buttons provide a convenient way to access specific lists or perform frequently used actions within a DocType. By placing these buttons strategically, users can reduce the time and effort spent on navigating through various modules and menus. It allows for a more intuitive and tailored user experience, ensuring quick access to relevant information and actions.


Conclusion:

 By adding custom buttons on DocTypes in ERPNext, organizations can significantly enhance the user experience, leading to increased productivity and user satisfaction. The ability to access different lists and reports with a single click streamlines workflows, reduces navigation efforts, and enables users to focus on their core tasks. With ERPNext's customization capabilities, it's possible to create a tailored user interface that aligns with specific business needs and improves overall efficiency. So why not leverage this powerful feature and empower your ERPNext users with custom buttons today?





Book a free 30min tailored consultation

Free first 30min of enterprise consultation to manage your business better using digital processes.


Sharvari Rathod

ERP enthusiast. Likes to write blogs that help people adopt technology for their benefit.

No comments yet.

Add a comment
Ctrl+Enter to add comment