How to Create an Inline Invoice Table at the Record Level with Edit Options

Hi, Tape community.

I wanted to share an app I created to view invoice tables inside the records. Here is what it looks like:

It also allows you to create new invoice items directly from the invoice record and with a link to edit the desired item. You can find the workspace template here.

Although simple, there are a few things needed to make it work. Here is a video tutorial I recorded about it.

Here is the calculation you will need to make the invoice table work. I explained in detail how it works in the video above.

// PRODUCT DETAILS
var Product = @All of Product Description;
var ListPrice = @All of Item Price;
var Qty = @All of Item Qty;
var ListSubtotal = @All of Subtotal;
var Total = @Sum of Subtotal;

// EDIT URL VARIABLES
var itemid = @All of Record ID;
var a_in = "<a href='";
var a_out = "'>Edit</a>";
var url = "https://tapeapp.com/YOUR-ORGANIZATION-NAME/(workspaces/invoices/apps/invoices//main-modal:record/";

// TABLE HEADER 

var html = "<table>";
html += "<thead><tr><th>Product</th><th>Price</th><th>Qty</th><th>Amount</th><th></th></tr></thead>";

html += "<tbody>";
for (let i = 0; i < Product.length; i++) {

// FORMAT PRICES WITH TWO DECIMAL PLACES AND "$" CURRENCY AT THE BEGINNING.

var Price = ListPrice[i];
var PriceFormatted = "$" + Price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });

var Subtotal = ListSubtotal[i];
var SubtotalFormatted = "$" + Subtotal.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });

var TotalFormatted = "$" + Total.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });

// TABLE BODY

html += "<tr>";
html += "<td width='50%'>" + Product[i] + "</td>";
html += "<td>" + PriceFormatted + "</td>"; 
html += "<td>" + Qty[i] + "</td>";
html += "<td>" + SubtotalFormatted + "</td>";
html += "<td>" + a_in + url + itemid[i] + ')' + a_out + "</td>";
html += "</tr>";

} 

// TABLE TOTALS

html += "<tr>";
html += "<td>" + "" + "</td>";
html += "<td>" + "<strong>TOTAL</strong>" + "</td>";
html += "<td>" + "" + "</td>";
html += "<td>" + "<strong>" + TotalFormatted + "</strong>" + "</td>";
html += "</tr>";

html += "</tbody>";
html += "</table>";

html;

You can add almost anything you want to the table (discount, retainer, taxes, etc) by editing a few things on the calculation.

If you have questions or can add other more advanced things to do with this table, please feel free to share. All the best!

11 Likes

Thanks for sharing @Luis - this looks incredibly useful! :partying_face:

1 Like

Fantastic work, @Luis! Truly impressive. :raised_hands:

1 Like

Great tutorial. Thanks for making and sharing this with the community

1 Like

Well done Luis!! Ty!

1 Like

Hi @Luis,
Thanks a lot for this outstanding post!!! This is sooo informative, absolutely appreciated!!!

This kind of video explanations are pure gold.

This is amazing Luis!! :raised_hands:

1 Like

Hi @luis,

many many thanks for this truly outstanding showcase and template.
Really impressive and extremely useful for the entire community! :100: :rocket:

Again big thanks
Leo

Great example @Luis . Thanks.

This is really great. It gives a lot of flexibility on how to present the invoice lines. Also the Edit link is very handy.

But… it shows the invoice lines, but there is no way to create a new invoice line for the partiucular invoice. That is what I have been searching for quite some time now: a way to create and edit a master/detail set of records in ONE new record layout.

The screenshot shows “Add invoice items” at the bottom. But it isn’t in the code for the calculated field. So, how dit that link show up?

Aha! Claude helped me marvellously. This is how we did it:

The setup

Two apps: Quotes and Quote lines.

  1. On Quotes, a multi-relation field “Lines” pointing to Quote lines. This is the real link. The quote points to its lines, not the other way round.
  2. A calculation field that renders the lines as an HTML table, using outn_<field>_<Lines> tokens for description, quantity, unit, price and line total, plus out_sum_ for the grand total. Each row gets an “Edit” link to the line record (via a calculated Record ID field on Quote lines: @[Record ID](meta_recordId)).
  3. The “Lines” field itself is always hidden. The calculations still use it, but you don’t see the same lines twice on screen.
  4. A second relation field, single relation “Add line”, pointing to Quote lines. This is the button. It’s always empty, so all you see is the little +.

The automation

  • Trigger: When a record is updated, and Add line has changed, in Quotes.
  • Filter: Add line is not empty.
  • Action 1: Update the current record, Lines: Add records, value: Add line.
  • Action 2: Update the current record, Add line: Clear record.

Click +, create the line, close it. The line lands in the table, the total updates and the button is empty again for the next one.

The trap I walked into

In the filter, Tape offers both Add line and Add line / Previous value. I picked the second one without noticing. Previous value is empty by definition the first time you fill the field, so the filter quietly stopped every run. The trigger fired, nothing happened, and I spent a while blaming the trigger. Pick the field itself, not the previous value.

Why not let the lines point to the quote?

That was my first version: a relation “Quote” on Quote lines, plus a reference block on the quote. It works, but the + on a reference block shows all lines again underneath the HTML table, and the relation on the line kept showing as required in my UI even though it wasn’t. Turning the relation around solved both.

Happy to share the calculation script: (Dutch Regels = Lines, Offerte = Quote)

var oms = Regels.Omschrijving.allWithNull;
var aantal = Regels.Aantal.allWithNull;
var eenheid = Regels.Eenheid.allWithNull;
var prijs = Regels.Prijs_per_eenheid.allWithNull;
var totaal = Regels.Regeltotaal.allWithNull;
var ids = Regels.Record_ID.allWithNull;
var som = Regels.Regeltotaal.sum || 0;
var basis = 'https://tapeapp.com/vlotterik-it-speeltuin/record/';
function st(r) { return " style='font-size:15px;line-height:1.2;padding-top:2px;padding-bottom:2px" + (r ? ';text-align:right' : '') + "'"; }
function eur(x) { return '€ ' + (x || 0).toLocaleString('nl-NL', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }
function getal(x) { return (x || 0).toLocaleString('nl-NL', { maximumFractionDigits: 2 }); }
function th(t, r) { return '<th' + st(r) + '>' + t + '</th>'; }
function td(t, r, extra) { return '<td' + st(r) + (extra || '') + '>' + t + '</td>'; }
var html = '<table' + st(false) + '>';
html += '<thead><tr>' + th('Omschrijving') + th('Aantal') + th('Eenheid') + th('Prijs', true) + th('Totaal', true) + th('') + '</tr></thead><tbody>';
if (!oms || oms.length === 0) {
  html += '<tr>' + td('<em>Nog geen regels</em>', false, " colspan='6'") + '</tr>';
} else {
  for (var i = 0; i < oms.length; i++) {
    html += '<tr>';
    html += td(oms[i] || '', false, " width='45%'");
    html += td(getal(aantal[i]));
    html += td(eenheid[i] || '');
    html += td(eur(prijs[i]), true);
    html += td(eur(totaal[i]), true);
    html += td("<a href='" + basis + ids[i] + "'>Bewerk</a>");
    html += '</tr>';
  }
}
html += '<tr>' + td('') + td('') + td('') + td('<strong>Totaal</strong>', true) + td('<strong>' + eur(som) + '</strong>', true) + td('') + '</tr>';
html += '</tbody></table>';
html;
1 Like