Download 4. Programming the Access application

Survey
yes no Was this document useful for you?
   Thank you for your participation!

* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project

Document related concepts

Microsoft Jet Database Engine wikipedia , lookup

Relational model wikipedia , lookup

Database model wikipedia , lookup

Transcript
Access Programming: Week 4
4. Programming the Access application
Access Object Model (revisited)
Application
Forms
Reports
Controls
Controls
Modules
References
DoCmd
Screen
So far on the course we have created programs to manipulate Form and Report objects, often using the DoCmd object,
which is an object whose methods correspond to most of the macro actions. The Modules collection contains the Module
object which refers to a specific open module. In general you use the Module object interactively to write code, rather than
write code about modules. The Forms, Reports and Modules collections have no methods; Access manages them for you.
We have seen the exclamation point syntax when referring to objects in collections.
?Forms!frmCustomers.RecordSource
There are three additional ways. If you open the form frmCustomers you can test them in the Immediate window.
1. Refer by name
?Forms(“frmCustomers”). RecordSource
2. Refer by variable
strName = “frmCustomers”
?Forms(strName).RecordSource
3. Refer by position
?Forms(0). RecordSource
The last of these is trickiest to use because 0 refers to the first open form, 1 to the next open form and so on. But if users
can open and close forms at will, problems would be inevitable.
The Screen object
The Screen object can be used in procedures if you are concerned with writing reusable code. The Screen object refers to
the active object, the object that has the focus (or it could refer to an object that previously had the focus). It does not
make the form, report or control the active object. Code has potential to be reusable if specific object names are not
referred to. The screen object’s properties include:
Screen.ActiveForm
Screen.ActiveControl
Screen.ActiveReport
Screen.PreviousControl
For example, in a program the line
Screen.ActiveForm.Visible = False
will hide the form that is the active form
References
The References collection object refers to the libraries that are available to the programmer. Libraries contain the
resources a programmer needs, i.e. the inbuilt objects and functions that one needs to manipulate. For example if you
wanted to program an Excel workbook from an Access database you would need to add a reference to the Excel
Object library. We will be adding references in next week’s course.
1
Access Programming: Week 4
Referring to a subform
A form can display a subform, i.e. you can display two forms on the screen. (It is necessary for the tables on which the
form is based to participate in a one-to-many relationship, and for the table on the many side of the relationship to
have a primary key, or a field with its Indexed property set to Yes, no duplicates). The form containing the subform
control is called the main form and the form displayed within the subform control is called the subform. Open
frmSalesOrders to see an example, and go to Design view. Referring to a subform can seem a little tricky at first. In
the first illustration the subform control is selected:
the SourceObject
property is
subfrmOrderDetails
Link Child Fields and
Link Master Fields
are what
synchronize the
subform to the main
form.
The subform
control is
selected
Test the reference to the subform control in the Immediate window as follows:
? Forms!frmSalesOrders.subfrmOrderDetails.SourceObject
It should show the SourceObject property of the subform control
In the next illustration the subform is selected – the properties are different to those of the subform control:
The window
displays the
properties of
the Form
object.
The
RecordSource
property is
qryOrderDetails
Here the
subform is
selected
To reference the subform in the Immediate window:
? Forms!frmSalesOrders.subfrmOrderDetails.Form.RecordSource
2
Access Programming: Week 4
A procedure to hide a Subform
You may want to hide a subform under certain circumstances.
Open frmSalesOrders in Design view and add a toggle button just above the right corner of the subform. Name the
button tglHide, and set its caption property to Hide.
Write the following procedure for its After Update event.
Listing 4.1
Private Sub tglHide_AfterUpdate()
If Me.tglHide Then
Me!subfrmOrderDetails.Visible = False
Me!tglHide.Caption = "Show"
Else
Me!subfrmOrderDetails.Visible = True
Me!tglHide.Caption = "Hide"
End If
End Sub
If you wanted the subform to be hidden when the form opens go to the Properties window:
- change the Visible property of the subform control to No
- change the caption of the button to Show.
- in the code reverse the settings of the Visible and Caption properties in the code.
The program in listing 4.5 uses simple syntax, taking advantage of the Me property of frmSalesOrders.
However you have to use semi-qualified syntax if referring to a subform from another form, a query or the Immediate
window. For example this is how to refer to the OrderNo control
Forms!frmSalesOrders.subfrmOrderDetails.Form.OrderNo
Test the reference in the Immediate window (you would need the form to be open in Form view)
? Forms!frmSalesOrders.subfrmOrderDetails.Form.OrderNo
The general syntax for referring to a control on a subform has an extra element, Form:
Forms!formname!subformcontrolname.Form.controlname
The Form property of the subform control represents its subform.
Similarly the Report property of a subreport control refers to its subreport.
3
Access Programming: Week 4
Active X controls1
You can add Active X controls to Access to provide capabilities that Access does not possess e.g. to display an
interactive calendar, or play a sound or video file. These are self-contained objects with properties, methods and
In Access 2007, click the Design tab, and then
events.
click Insert ActiveX Control in the Controls group.
There are thousands of Active X controls (formerly known as ocx’s) commercially available and some that are free.
In the Insert
ActiveX
Control dialog
box, click to
We will look the interactive Calendar control, which is free with the retail version
of Access.
Depending
on your
select Calendar Control 10.0 or a later version
installation of Access there may be other controls.
from the Select an ActiveX Control list box, and
clickActiveX
OK. control. (You might not
To insert a Calendar, in the form frmSalesOrders, click the Insert menu andthen
choose
wish to save the changes after you have finished)
Choose Calendar control and click OK.
Click the form and the control will be inserted on the form.
When you add a control Access automatically adds a Reference to the object’s type library – you could view its
methods and properties in the Object Browser.
Make some room for the
control to display it; you can
resize the control.
You can select the control and view its properties:
1
Calendar control no longer exists in Access 2010. Alternative solutions are discussed on
http://social.technet.microsoft.com/Forums/en/office2010/thread/662ac883-0f5c-4b89-8a6f-0a3efc4ec259
4
Access Programming: Week 4
Its name is Calendar 1
You can view and modify more
properties by clicking the Build button
at the Month property
With frmSalesOrders in Form view you can type
?Forms!frmSalesOrders!Calendar1.Value
in the Immediate window, and the currently
selected date should be returned.
To bind the calendar to a control on the form go to the Calendar’s Control Source property, and choose OrderDate; the
calendar will now display the value in the OrderDate field. To change the value of the OrderDate field for a particular
order you can click a day and enter the record.
To automatically refresh the form on changing the date, go to the form’s module and type the following:
Listing 4.2
Private Sub Calendar1_AfterUpdate()
Me.Refresh
End Sub
Note that Access doesn’t provide an AfterUpdate event for a calendar control in the Properties window, because the
event is control specific, Access can only provide generic events. However the control does recognise an AfterUpdate
event and you can use it.
5
Access Programming: Week 4
Calendar Example
Another way to use a Calendar control is to link it to a subform, so the subform displays records for the date value of
the calendar. We will use a Calendar to display sales orders on a particular date. The finished form should look as
follows:
First we make the subform
Click Forms in the database
window, click New, and choose
Form Wizard
Select SalesOrders as the data
source and click OK
Select the fields as illustrated
Click Next
6
Access Programming: Week 4
Select Datasheet for the
layout
Click Next
The style doesn’t matter click
Next again, and finally type
subfrmOrders as the form’s
title. Click Finish.
Close the subform.
Now make the main form. In the database window, click Forms, New and choose Design view without specifying any
data.
Insert a Calendar control, go to its properties and name it ocxCal.
Save the form as frmDates. Then arrange your screen so you can see both the Database window and frmDates in
Design view behind it. A neat way of inserting a subform is to drag the subform from the Database window and drop
it into the main form.


7
Access Programming: Week 4
Next we must take the important step of setting the Link Child Fields and Link Master Fields properties. Set the Child field
to be OrderDate, and set the Master field to be ocxCal. This is necessary to synchronize the two forms.
Go to form view to try it out. Select May 2001, and click the 13th. Click the subform and you should see the orders on
that day.
To make it operate more smoothly write this short procedure for the Calendar control’s AfterUpdate event.
Listing 4.3
Private Sub ocxCal_AfterUpdate()
Me!subfrmOrders.Requery
End Sub
Save the form when you have finished.
8
Access Programming: Week 4
Connectivity Using DoCmd
The DoCmd object provides TransferDatabase and TransferSpreadsheet methods (you can even use the corresponding
macro actions if you don’t want to program). The transfer could be an import or an export. DoCmd routines can be
event handlers attached to buttons on forms, or run from VB Editor if one-offs, or called as a function if you want to
reuse the code on more than one form.
In the first example we are in an mdb file which contains a table Invoices, and the ExportTable routine exports it to the
specified pathname as Invoices. Open Accounts.mdb create a module and try this code.
Listing 4.4
(Exporting from current database)
Sub ExportTable()
On Error GoTo ExportTable_Err
DoCmd.TransferDatabase acExport, "Microsoft Access", _
"U:\Company2004.mdb", acTable, "Invoices", "Invoices", False
ExportTable_Exit:
Exit Sub
Modify path as appropriate
ExportTable_Err:
MsgBox Error$
Resume ExportTable_Exit
End Sub
Next we create a link named Invoices to a table Invoices, which is in an external file, so this code would work in any
database. If you want to reuse the code, make it a function and call from the On Click property of a button on a form.
Listing 4.5 (Making a link in the current database to a table in another database)
Sub Linktable()
On Error GoTo Linktable_Err
DoCmd.TransferDatabase acLink, "Microsoft Access", _
"U:\Accounts.mdb", acTable, "Invoices", "Invoices", False
Linktable_Exit:
Exit Sub
Linktable_Err:
MsgBox Error$
Resume Linktable_Exit
End Sub
Here’s a reference for the TransferDatabase method:
can be one of these constants:
TransferType
acExport
(optional)
acImport default
The acLink transfer type is
not supported for Microsoft
Access projects (.adp files)
acLink
Database Type
(optional)
DatabaseName
(optional)
ObjectType
(optional)
Source
(optional)
Destination
(optional)
StructureOnly
(optional)
one of the types of databases you can use to import,
export, or link data. Microsoft Access is default
the full name, including the path, of the database you
want to use to import, export, or link data
the type of object whose data you want to import,
export, or link. You can specify an object other than
acTable only if you are importing or exporting data
between two Access databases. Options include
acTable default ,acQuery,acReport
the name of the object whose data you want to import,
export, or link
the name of the imported, exported, or linked object in
the destination database
True to to import or export only the structure of a
database; False (default) to import/export data.
9
Access Programming: Week 4
StoreLogIn
(optional)
to store the login ID and password; False (default) if
you don't want include the details
Importing an Excel file
The DoCmd object can be used to connect to Excel, using its TransferSpreadsheet method (naturally you can use the
corresponding macro actions if you don’t want to program).
The following code imports an Excel file called NewPrices.xls from the specified path as a table NewPrices
Listing 4.6
Sub ImportPrices()
On Error GoTo ImportSheet_Err
DoCmd.TransferSpreadsheet acImport, 8, "NewPrices", _
"D:\Spreadsheets\NewPrices.xls", True
‘change path
ImportSheet_Exit:
Exit Function
ImportSheet_Err:
MsgBox Error$
Resume ImportSheet_Exit
End Sub
8 refers to the
version of Excel
True refers to the
fact that the first
row of the
spreadsheet is a
header row; use
False if the first
row contains data.
Action Queries for the New Prices example
We will extend the example as follows:
 Import the NewPrices spreadsheet
 Update the prices in the Products table to those in the imported table
 Add any new rows in the imported table to the Products table (append)
A snag is that the ProductId field in the Products table is of the data type Text, but Access imports it as a number (a
Double) in the NewPrices table. As things stand we need to change the data type in the Products table to a Double
(even worse we have to delete the relationships the Products table is in before we can change the design, then recreate
the relationships after).
1. Updating Prices using an Update Query
The task is to update the Unit Prices in the Products table to the amount in a spreadsheet file NewPrices.
Import the spreadsheet NewPrices as a table, call the new table NewPrices. Before you create the query ensure the
ProductID field in the NewPrices table is Text (to match the data type of ProductID in the Products table).
Create the illustrated query and run it using the Run button.
10
Access Programming: Week 4
2. Appending records from a table where there is no record in the original table
The idea here is to append products to the Products table from the NewPrices table where the Product is new, i.e.
there is not yet a record of it in the Products table.
Create the following Append query. Note that the Criteria contains an SQL statement (i.e. a subquery)
Check the query result, then turn it into an Append query and run it.
Save the query as qryAppendPrices
Putting it all together
Note that the code calls the ImportPrices program. Also not e that it has been made a function – this is not necessary
but it makes it possible to attach the code to the click event of as many buttons as you like. (Put a button on the Menu
form and type =ImportAndModify() into its Click Event in the Properties window.
Function ImportAndModify()
ImportPrices
DoCmd.OpenQuery "qryUpdatePrices"
DoCmd.OpenQuery "qryAppendPrices"
End Function
Importing Text
The TransferText method is available to import text files.
Listing4.7
Sub ImportText()
On Error GoTo ImportText_Err
DoCmd.TransferText acImportDelim, "", "New", "U:\sample.txt", False, ""
ImportText_Exit:
Exit Function
ImportText_Err:
MsgBox Error$
Resume importText_Exit
End Sub
11
Access Programming: Week 4
Appendix: Built-in functions
Both Access and VBA provide functions that you can use in procedures. Functions are classified into categories: some
work with Date and Time fields; amongst others there are also Text Manipulation functions; Data Type Conversion
functions; Domain Aggregate functions; Mathematical functions; Financial functions; General purpose functions.
One way to see the categories of
functions is to open the
Expression Builder, expand
Functions, then expand Built-In
Functions. The middle column
shows the categories.
If you choose a function from
the right hand column you can
click the Help button to access
further information.
Functions are diverse in the tasks they perform and there is no one way to use them all. Each function must be looked
at on the basis of its own characteristics. The majority take one or more arguments, some like Date take no arguments
– some like the financial functions might need some research on your part before you could use them correctly.
You can also look for help on specific functions is the Contents page of VB Help; expand Visual Basic Language
Reference, expand Functions, then expand the appropriate alphabetical category
The following tables list selected functions. The examples can be tested in the Immediate Window
Date and Time functions
Function
Date() or Now()
Day()
Month()
Year()
Weekday()
Description
returns the current system date
returns an Integer representing the day
from a Date value
returns an Integer that represents the
month of a Date value
returns an Integer that represents the year
of a Date value
returns day of the week (Sunday = 1)
Example (You can use # instead of “ in examples)
Date()
Day(“31-Jan-2004”)
returns 31
Month(“31-Jan-2004”)
returns 1
Year(“31-Jan-2004”)
returns 2004
Weekday(“31/01/2004”)
returns 7
DateAdd()
DateDiff()
DateSerial()
returns a data to which a specified time
interval has been added; all three
arguments are required
DateAdd("yyyy", 2, date())
returns an Integer representing the
difference between two dates
DateDiff(“d”, Date(), ”31-Jan-2004”)
returns a date given three integers
that specify year, month, day in that order
DateSerial(2004, 1,31)
returns the date 2 years from today
DateAdd("ww", -3, ”4 Mar 2004”)
returns the date 3 weeks earlier than 4th March
2004
returns the number of days between 31-Jan-2004
and the current date
returns 31/01/2004
12
Access Programming: Week 4
Nested functions
You can have a function within another, e.g.,
Month(Date())
will return the current month as an Integer
To evaluate a nested function start with the innermost brackets, i.e. Date() – this evaluates to the current date, then the
Month function extracts the month part of the date.
Example
You can use inbuilt functions within in your own functions. For example, here is a function to find the start of the next
financial year, given a date – it uses the DateSerial() function
Listing 4.9
Public Function NextFinYear(dt As Date) As Date
If Month(dt) > 3 And Month(dt) <= 12 Then
NextFinYear = DateSerial(Year(dt) + 1, 4, 1)
Else
NextFinYear = DateSerial(Year(dt), 4, 1)
End If
End Function
Explanation
The crux of the problem is that the date supplied to the function might be before or after the end of March.
If it is after March, the next financial year begins in the year after the current one. DateSerial adds one to the year part,
and returns 4 for the month and 1 for the day.
If the month part of the supplied date is January, February or March, the next financial year begins in the current year.
DateSerial composes a date out of the current year and supplies a 4 and a 1 for April 1 st.
How to test for a leap year
The rule for testing for a leap year is that a year is a leap year if it is exactly divisible by 4 but not by 100, except that
years divisible by 400 are leap years – this is the Gregorian calendar.
Listing 4.10
Function IsLeap(dt As Date) As Boolean
If (Year(dt) Mod 4 = 0 And Year(dt) Mod 100 <> 0) Or Year(dt) Mod 400 = 0 Then
IsLeap = True
Else
IsLeap = False
End If
End Function
Text manipulation functions
LCase()
UCase()
Len()
Trim()
LTrim()
RTrim()
Left()
Right()
Mid()
returns lower case version of a string
returns upper case version of a string
returns number of characters in a string
removes leading and trailing spaces
from a string
removes leading spaces from a string
removes trailing spaces from a string
returns leftmost characters of a string
returns rightmost characters of a string
returns specified number of characters
from specified starting point in a string
LCase(“ABCD”)
UCase(“abcd”)
Len(“ABCDE”)
Trim(“ ABC “)
LTrim(“ ABC”)
RTrim(“ABC “)
Left(“ABCDEF”,3)
Right(“ABCDEF”,3)
Mid("Lindsay Davenport",9,4)
returns Dave.
The function returns the string starting at the ninth
character which is four characters long
StrComp()
compares two strings for equivalence
and returns the integer result of
comparison: 0 if strings are equal, -1 if
strings are different
StrComp(“ABC”,”abc”)
returns 0. The strings are equal.
13
Access Programming: Week 4
Val()
Chr() or Chr$()
returns numeric value of a string in
data type appropriate to the format of
the argument.
returns a character associated with a
character code e.g. ANSI
(you sometimes see a character code
used in a program instead of the
character, but its not necessary)
Val(“123.45”)
returns 123.45 (i.e. a number)
chr(34) returns " (double quote)
chr(42) returns * (asterisk)
Mathematical functions
Abs()
returns absolute value of numeric field
Int()
returns numeric value with the decimal
fraction truncated
returns a number rounded to a specified
number of decimal places.
syntax is:
Round()
Round(expression, numdecimalplaces)
where the numdecimalplaces argument
Rnd()
is
optional; the function returns an integer
if it is omitted.
creates random number (single)
between 0 and 1
Abs(-1234.5)
returns 1234.5
Int(13.9)
returns 13
round(12.459, 2)
returns 12.46
round(12.9)
returns 13
to generate a random whole number between 0 and
9 you can use:
Int(Rnd()*10)
Explanation – the Rnd() function generates a single
between 0 and 1; this value is multiplied by 10, so
we have a number between 0 and 9; finally the Int()
function removes the fractional part leaving a
whole number.
Other maths functions are: Atn , Cos , Exp, Fix, Log, Sgn, Sin, Sqr, Tan
General-Purpose functions
Iif()
IsNumeric()
IsNull()
IsDate()
IsEmpty
VarType
Format
returns one value if the result of an expression
is True, another if the result is False
returns True if argument is one of the number
field data types, otherwise returns False
returns True if argument is Null, otherwise
returns False
indicates whether an expression can be
converted to a date
indicates whether a variable (of type variant)
has been assigned a value
Returns an Integer indicating the subtype of a
variable. Refer to Help for the list of constants
with which the function is used.
This function can be used to format numbers,
strings and dates.
Iif([Exam mark] >=50, “Pass”,”Fail”)
IsNumeric([Field Name])
IsNull([Field Name])
IsDate("21 feb 98")
returns True
IsEmpty(variable name)
e.g. you could test if a variable is an integer
as follows:
If VarType(varname) <> vbInteger Then
Format(5459.4, "£##,##0.00")
returns £5,459.40
Format("HELLO", "<")
returns "hello"
Format(now, "dd/mm/yy hh:mm:ss AMPM")
returns 28/10/04 01:00:16 PM
Format(date,"Long Date")
returns 28 October 2004
14
Access Programming: Week 4
IIf() (Immediate If)
An Immediate If is like a one line If ... Then ... Else statement; it can be used in places that an If ... Then ... Else statement
can’t, e.g., in a query which is Control Source for an object on a form or report.
The way it works is that it takes three arguments:
the first argument is a condition to be evaluated
the second argument is the action if the condition evaluates to True
the third argument is the action if the condition evaluates to False
As an example, if the value of a field on a report is 0 you might not want to print a zero. You could use the Iif
statement in such a situation:
Expr1: IIf([DiscountValue]=0," ","Discount: " & [DiscountValue])
IIf statements can often be hard to read – it helps to split the argument into three components
[DiscountValue]=0
the condition is True if DiscountValue evaluates to zero
""
action to take if True is to print a space, i.e. nothing shows up on report
"Discount: " & [DiscountValue])
action to take if condition is false is to print the string “Discount “ and
concatenate DiscountValue
Data Type Conversion functions
CInt()
converts a string to a number
CInt(“99”)
returns 99
rounds a fraction to an Integer
CInt(123.5)
returns 124
CLng()
rounds a fraction to a Long Integer
CCur()
CDate()
converts numeric value to Currency data type
converts string expression to Date
CStr
converts its argument to a string
CLng(100000.1)
returns 100000
CCur(123.5)
CDate("21Feb 98")
returns 21/02/1998
CStr(99)
returns “99”
Other conversion functions are: CBool, CByte, CDbl, CDec, CSng, and CVar. You may come across Str and Val functions:
these are older conversion functions that can be replaced with the more modern versions.
Timer function
If you are concerned with testing performance issues in your database, you can use the Timer function to time how
long something takes. For example, here is an example that times how long a query takes to run.
Public Function QueryRunTime(strQueryName As String) As Double
Dim Start As Double, Finish As Double
Start = Timer
DoCmd.OpenQuery strQueryName
Finish = Timer
QueryRunTime = Finish - Start
MsgBox strQueryName & " run time is " & QueryRunTime
End Function
Call the function like this, say in the Immediate window
?queryruntime("qryRegionalCustomers")
15
Access Programming: Week 4
Domain Aggregate functions
Domain Aggregate functions provide statistical information about sets of records (known as domains). In this respect
they are similar to the functions in Totals queries, which are known as SQL Aggregate functions. The syntax of
Domain Aggregate functions takes a little getting used to. The following example can be taken as representative:
DCount(expr, domain, [criteria])
where
expr
identifies the field for which you want to count records; it can be a field in a table, a control on a form
domain is a string expression identifying the set of records. It can be a table name or a query name
criteria is an optional string expression to restrict the range of data on which the function is performed
DCount()
DSum()
DAvg()
determines the number of records that
are in a specified set of records (a
domain).
calculates the sum of a set of values
in a domain
calculates the average of a set of
values in a domain
DCount("OrderNo", "SalesOrders")
DSum("ItemCost", "qryOrderDetails")
Tthe third optional argument will take a string which is
an SQL Where clause without the word WHERE e.g.
DSum("ItemCost", "qryOrderDetails", "ProductID = '103'")
DAvg("PayRate", "Employees"
You can use domain aggregate functions to supply summary information in a text box on an unbound form as follows:
Create a new form in design view without specifying any data source
Click the text box tool
Then click on the form; you will get
an unbound text box
In the text box type the expression:
=DAvg("PayRate", "Employees")
Go to form view; the textbox should show the average pay rate of all employees in the Employees table;
all you need to do is format it as currency and type a meaningful caption.
The function gives a result equivalent to the query illustrated
right – domain aggregate functions are available however where
SQL aggregate functions are not, like on a form.
Add some more text boxes and try the other examples from the table.
16
Access Programming: Week 4
Getting the Discount value into the Invoice report
Working with reports can be incredibly tricky, especially where subreports are concerned – you don’t have to read this
section if you don’t want to. On the other hand it might shed some light on problems you have experienced in you own
work.
This is the way I got the discount value into the invoice. In query builder view of qryOrderDetails I have amended the
way DiscountValue is organised. The basic calculation is now in a column captioned DValue. The DiscountValue field,
which is to appear on forms / reports takes DValue and applies the Format function to it as illustrated.
(I suppose it could have been done all in one giant expression, but it would be almost impossible to read).
Close and save the query in the Query Builder.
Next open the report rptInvoice in design view, select the subreport control, then the subreport and click the Field List
button. DiscountValue should be present. Drag it to the subreport, and delete its label. Preview the report
the DiscountValue
field should be
visible
The field is beautifully formatted, but leaves a problem if you don’t want zeros to appear on the invoice. Go back to
qryOrderDetails in query builder view and create an expression as illustrated (you can leave its caption as Expr1)
Close the Query Builder and save. Now in design view of the subreport use Expr1 instead of DiscountValue.
Here’s the evidence:
Expr1
field
Obviously there’s more work to be done on the report e.g. subtract discounts from subtotal, charge VAT and so on.
17