Build an expense approval system with Anvil
Anvil is a powerful and versatile tool for building full-stack web applications. With Anvil, you can quickly and easily create your UI and write Python code to control the front-end and back-end of your app. Creating apps with Anvil is fast and flexible, making it well-suited for building internal tools. This expense approval system app showcases some of the features that Anvil has on offer.
Open in Anvil: https://anvil.works/build#clone:ODKPA5XG5ZBEYSLZ%3dT6QRGCPIIRYAZ5FKKXALBEQI
I built the app’s UI using Anvil’s drag-and-drop editor and writing front-end Python code. I also wrote the backend in Python, on a secure Server Module. This app also uses Anvil’s built-in database, user authentication and email sending. Take a look below at how I built this expense approval system, with only Python and Anvil.
The Expense Approval app
This app is a simple expense approval system, where users can submit expenses for an admin user to approve or reject. The main page is a view of the expenses, and users can add expenses by clicking on the + New Expense button, and filling out the pop-up form that it triggers. Clicking on the expense description brings out a detailed view of the expense.
An admin will be able to see all expenses and reject, approve and reimburse them, whereas a regular user will only have access to their own submissions
Whenever an admin takes action on an expense, the user that submitted it receives an email notifying them of the change. Finally, admins also have access to a dashboard summary of the expenses, with visualisations, which they can download as a PDF.
How it’s built
Creating the UI
Anvil makes it simple to create a UI. You can use drag-and-drop components and then write Python to make them interact with each other. For example, I used a Data Grid to display the expenses in my app.
Once the layout is set, you can make your components interact with each other by writing your browser code, all in Python. The function below, which runs whenever we change to the Pending Approval view, is running entirely in the browser without making any round trips to the server.
def pendingappr_btn_click(self, **event_args):
# When pendingappr_btn is clicked, the page contents are changed
self.content_panel.clear()
self.content_panel.add_component(
ExpenseDashboard(status='pending'))
Data Tables and Server Side
Anvil has a built-in database system built on top of PostgreSQL, with a GUI for creating, editing and deleting tables. This makes it simple to define your schema. By default, Data Tables are only accessible from the server side.
Anvil’s Server Modules are full server-side Python environments, which cannot be edited or seen by the user. Using server functions to interact with our database ensures our data stays secure. These functions can then be called from the client-side with just one line of code. For example, the function below adds a row to my expenses data table.
@anvil.server.callable
def add_expense(expense_dict):
app_tables.expenses.add_row(created=datetime.now(),
status="pending",
submitted_by=anvil.users.get_user(),
**expense_dict)
The @anvil.server.callable
decorator means I can call this function from client-side code.
anvil.server.call('add_expense', expense)
User Management
Anvil has a built-in Users Service, which handles signup, login and user permissions, as well as supporting corporate single-sign-on, two-factor authentication, and more. I simply added the Users Service to my app and chose the authentication methods I wanted to use - Email + Password and Google sign-in. I then added anvil.users.login_with_form()
to my client side to display a login form on startup.
With Anvil, it’s also very simple to restrict functionalities based on user roles. For example, I wanted certain components to be visible and enabled only if the logged in user is an admin, and I was able to implement this with only a couple lines of code:
if State.user['role'] == 'admin':
self.summary_btn.visible = True
self.summary_btn.enabled = True
Email service and background tasks
Anvil makes it easy to send and receive emails from your app with a few lines of Python using the built-in email service. I wanted to notify users via email of a status change in their expense request, so first I wrote a function to do just that:
@anvil.server.background_task
def send_email(user, message):
anvil.email.send(to=user, from_name="Expenses App", subject="Your expense has been updated", html=message)
The @anvil.server.background_task
decorator indicates that this task can be run in the background. Background Tasks allow you to fire off a function to run in the background, leaving your main program to continue executing while the function is running.
With this, I could launch my background task every time an expense’s status is changed, without blocking the control flow of my app, by adding anvil.server.launch_background_task
to my change_status
server function.
@anvil.server.callable(require_user=is_admin)
def change_status(row, status):
old_status = row['status']
user = row['submitted_by']['email']
message = f"<p>Hi, {user},</p><p>The status of your expense ('{row['description']}') changed from <b>{old_status}</b> to <b>{status}</b>.</p><p>Visit the <a href={anvil.server.get_app_origin()}>app</a> to learn more details.</p>"
row.update(status=status)
anvil.server.launch_background_task('send_email', user=user, message=html)
Data Visualisations and PDF rendering
Anvil comes with a built in Plotly integration, so you can import, process and visualize data, all within Anvil, using Python. Because they’re Plotly plots under the hood, Anvil’s plots are interactive — you can move the mouse over a plot to see the available tools.
I decided to plot some data from the app’s database, so I first created some functions on the server to get the data into shape, like in the example below:
@anvil.server.callable(require_user=is_admin)
def get_status_data():
status_data = [x['status'] for x in app_tables.expenses.search()]
labels = list(set(status_data))
values = []
for l in labels:
values.append(status_data.count(l))
return labels, values
Then, I just fed it into one of my plots from my Summary Form:
labels, values = anvil.server.call('get_status_data')
self.plot_1.data = go.Pie(labels=labels, values=values, hole=.4)
self.plot_1.layout.title = "Expense requests by status"
I also decided to make the Form downloadable as a PDF. First, I rendered it into a PDF in a server function:
@anvil.server.callable(require_user=is_admin)
def create_summary_pdf():
return anvil.pdf.render_form('Plots')
Then, I call this function from the client to download the file whenever the download button is clicked:
def download_summ_btn_click(self, **event_args):
"""This method is called when the button is clicked"""
media_object = anvil.server.call('create_summary_pdf')
anvil.media.download(media_object)
Build your internal tools with Anvil
Anvil makes building powerful internal tools simple, straightforward and fast. You can use all your favourite Python libraries to build your code, and the drag-and-drop designer makes building UIs a breeze.
This expense approval system makes use of Anvil’s built-in database system, the easy-to-use user management service, the email service, and more. It’s powered by Python running in the browser, as well as on the server, meaning that it’s extremely customisable. Clone the app to check out the source code and play with it yourself!
Open in Anvil: https://anvil.works/build#clone:ODKPA5XG5ZBEYSLZ%3dT6QRGCPIIRYAZ5FKKXALBEQI
Top comments (0)