The aim of this coursework is to code a Python 3 module that simulates a supermarket cashier system (also known as POS system). The module allows a self-checkout customer to list the products on stock, add products from the stock to a shopping basket or remove products from it, list all the items in the basket, and produce a bill of the basket (applying two types of promotions). While there are several ways of solving this problem, in this project you are required to use the data structures described below and follow the tasks as outlined. First read and understand the whole description in this document before starting with the actual work.
The best way to approach this coursework is, as usual, to first sit down with pen and paper and think about what data structures and functions should be required to represent and work on a supermarket shopping basket. Clearly, we will have a stock of products each of which has an associated price and amount of availability (for example, we may have 2.210 kg of bananas priced at £0.68 per kg available on stock). We want to be able to take a certain amount of product (bananas) from the stock and add it to a shopping basket. Likewise, we want to be able to take products out of the basket and put them back on stock. Finally, when we are done with our shopping we want to check out the basket and get a bill. This is the essence of this coursework.
In order to make life easier for you, below you will find nine concrete tasks to follow (Tasks 0-8). These tasks are ordered logically and in (approximately) increasing level of difficulty. The only exception is Task 7, which explains the main functionality of the module. You may look at Task 7 earlier.
The Python knowledge contained in the course notes is sufficient to complete this project. While you are allowed to consult the web for getting ideas about how to solve a specific problem, the most straightforward solution may already be in the course notes. It is not allowed to copy-and-paste several lines of computer code from a (online) source without clearly indicating their origin. If you really need to use an online source, you must indicate this for example like so:
# the following three lines follow a similar code on
# http://webaddress.org/somecode.htm as retrieved on 24/11/2020
Let's be very clear about what is not allowed:
You are NOT allowed to send/give/receive Python code to/from classmates and others.
This project is the equivalent of a standard exam and it counts 70% towards your final mark.
Consequently, the standard examination rules apply to this project.
Once your Python module is submitted via the Blackboard system, it will undergo plagiarism tests:
1.) The turn-it-in system checks for similarities in the codes among all students.
2.) Another Python program compares the syntax of all submitted codes.
Note that even if you are the originator (and not the one who copied), the University Guidelines require that you will be equally responsible for this case of academic malpractice and may lose all marks on the coursework (or even be assigned 0 marks for the overall course).
There are several factors that go into the assessment.
First of all, it will be checked whether you have followed the tasks and format specified below (using the prescribed function names, variable names, etc.).
All functions of your code will be tested automatically by another computer program. The outputs are then compared to verified outputs of another implementation. (This also means that if you do not strictly follow the format specified below then some of these automatic tests will fail and you may lose marks.)
Each function/line of code should be bug free. Functionality will be the main factor in the assessment.
Your code should be robust to exceptional user inputs (using Exceptions). It should not be possible to crash the code even when a user provides an unexpected input.
It is required that your module is properly documented, and that the module and every of its functions has a meaningful docstring. Each function must explain its own input arguments and returned values and their types. There is no room for misinterpretation. Check that print(functionname.__doc__)
returns a proper docstring. Essentially, each function should be useable from the docstring alone, without reading the code.
Further, marks will be given on code effciency. Have you solved a problem using 100 lines of code, although we have learned a very straightforward way which would only require 2 lines? You may lose marks in this case.
The rough split (subject to scaling) between the total marks is 65% for testing and manual inspection, 15% for documentation, and 20% for code efficiency.
The complete coursework can be completed and submitted as a single Python file supermarket.py
.
Click here to download a template module: supermarket.py
.
The submission will be via Blackboard and the deadline is Friday, December 18, at 1pm.
Important: You have almost four weeks for completing this coursework, but please do not wait until the end of the semester for getting started. The deadline of Friday, December 18, at 1pm is strict and no exceptions can be made for fairness. Aim to submit your coursework earlier than this so there is enough time to sort out eventualities. You can resubmit as often as you like, but only the last submission counts.
stock
and basket
¶Before you start with the actual coding, understand how to keep track of the products on stock and the products in the shopping basket. The fundamental data structures of your module will be two dictionaries stock
and products
. The stock
dictionary has the following form (only showing three representative items):
stock = {
'10005' : {
'name' : 'Conference Pears Loose',
'price' : 2.00,
'unit' : 'kg',
'promotion' : None,
'group' : None,
'amount' : 1.550
},
'10013' : {
'name' : 'Emmental Slices 250G',
'price' : 1.75,
'unit' : 'pieces',
'promotion' : 'get2pay1',
'group' : None,
'amount' : 9
},
'10015' : {
'name' : 'Diced Beef 400G',
'price' : 4.50,
'unit' : 'pieces',
'promotion': 'get4pay3',
'group' : 4,
'amount' : 14
},
# ...more products on stock...
}
Each key in the dictionary is a string referred to as ident
(in the above example, the ident
strings are 10005
, 10013
, and 10015
), which serves as a unique identifier for each product. Each product is again represented as a dictionary, with the keys name
, price
, unit
, promotion
, group
, and amount
. Here is a more detailed description of the corresponding values:
name
: a string representing the name of a product (e.g., Granny Smith Apples Loose
)price
: a floating point number representing the price (in Great Britain Pounds)unit
: a string which is either pieces
or kg
, indicating whether the product is costed in pieces or per kilogram weightpromotion
: the value can either be None
or a string get2pay1
or get4pay3
group
: refers to the group of promotion and it can either be None
or an integer (the latter only when promotion
is get4pay3
)amount
: refers to the amount of a product being available on stock; it can either be an integer (in case unit
takes the value pieces
) or a floating point number (in case unit
takes the value kg
).Example: The stock
dictionary above indicates that we have 1.55 kg of pears on stock, with each kg priced at £2. There are no promotions on pears.
Now assume the customer adds 0.55 kg of pears to the shopping basket. Then we can simply represent the content of the basket using another dictionary of the form
basket = {
'10005' : {
'name' : 'Conference Pears Loose',
'price' : 2.00,
'unit' : 'kg',
'promotion' : None,
'group' : None,
'amount' : 0.550
},
}
This means we have essentially copied the value of stock['10005']
to the basket dictionary and set the amount to the demanded value. At the same time, the amount of pears available on stock has been reduced to 1 kg. We can also add negative amounts of pears to the basket, in which case we are taking them out and adding them back to the stock.
loadStockFromFile(filename)
¶Write a function loadStockFromFile
which reads the products from a file and returns the dictionary stock
in the form described above. The function accepts an optional string parameter filename
corresponding to a file in the same directory as your Python module (i.e., there is no need to specify a path). If filename
is not provided, the value stock.csv
should be assumed. If a file cannot be found or opened, for whatever reason, the function should throw an appropriate exception.
Example file: To download the example stock.csv
used here, right-click on the link and choose "Save target as..." or "Save as..." (or similarly, depending on your operating system). Save the file to the same directory as your Python module supermarket.py
.
Each line in stock.csv
is of the form
name|price|unit|promotion|group|amount
Ensure that the values in the sub-dictionaries representing each product have the correct type. For example, if unit
takes the value pieces
, then amount
is an integer, otherwise it is a floating point number.
The loadStockFromFile()
function should ignore "corrupted" lines in stock.csv
which are not of the correct format. For example, the following line would not be of the correct format:
10014|Beef Steak Mince 600G|4.00|pieces|get4pay3|4|3.21
as the unit is pieces
and the amount (the last value) is not an integer (3.21). This item should be skipped.
Also ensure that None
values in stock.csv
become proper None
's in the dictionary (of type NoneType
, not str
).
Note: If for some reason you fail to implement the function loadStockFromFile()
, or if you prefer to start working on other parts of the module without loading data from a file first, you may copy-and-paste the above stock
dictionary with only three items to your module.
listItems(dct)
¶As both the stock
and the basket
dictionaries are of the same form, we only need to write one function which displays their items. Write a function listItems(dct)
which takes as input a dictionary dct
(which will be either stock
or basket
) and returns (not prints) a string corresponding to a nicely formatted table.
The products need to be sorted by increasing ident
identifier (lexicographically). The prices need to be right-aligned and displayed with two decimal places. The kg amounts are displayed with one decimal place.
Example: If s = listItems(dct); print(s)
is called, the returned string should be formatted similar to the following (your precise formatting and the items might differ):
Ident | Product | Price | Amount
-------+-------------------------------------+-----------+-------------
10000 | Granny Smith Apples Loose | 0.32 £ | 6 pieces
10001 | Watermelon Fingers 90G | 0.50 £ | 17 pieces
10002 | Mango And Pineapple Fingers 80G | 0.50 £ | 2 pieces
10003 | Melon Finger Tray 80G | 0.50 £ | 10 pieces
10004 | Bananas Loose | 0.68 £ | 2.2 kg
10005 | Conference Pears Loose | 2.00 £ | 1.6 kg
searchStock(stock, s)
¶Write a function searchStock(stock, str)
which returns a dictionary substock
of the same form as stock
, but containing only those products whose name
contains the string s
as a substring. The search should be case-insensitive.
Example: A call to searchStock(stock, 'beef')
should return a dictionary
substock = {
'10015' : {
'name' : 'Diced Beef 400G',
'price' : 4.50,
'unit' : 'pieces',
'promotion' : 'get4pay3',
'group' : 4,
'amount' : 14
},
'10017' : {
'name' : 'Brisket Beef 400G',
'price' : 3.50,
'unit' : 'pieces',
'promotion' : 'get4pay3',
'group' : 4,
'amount' : 15
}
}
Conveniently, because substock
and stock
are dictionaries of the same format, in your program you can use the same function listItems(substock)
from Task 2 to display the search results.
addToBasket(stock, basket, ident, amount)
¶Write a function addToBasket(stock, basket, ident, amount)
which adds amount
units of the product with identifier ident
from the stock to the basket. The function should return a message msg
, which can either be None
or a string. There are four possible scenarios this function must handle:
1) If amount
is positive and there are sufficiently many units of the product on stock, then amount
units will be taken out and added to the basket. See the above example in Task 0, where we have added 0.550 kg of pears from the stock to the basket. In this case the function will return msg = None
, i.e., no message.
2) If amount
is positive but there are less units of the product on stock, then only this many units will be added to the basket (hence leaving 0 units of the product on stock). The function will return a message of the form msg = 'Cannot add this many {unit} to the basket, only added {amount} {unit}.'
, where the appropriate replacements are performed on the string.
3) If amount
is negative and there are sufficiently many units of the product in the basket, then amount
units will be taken out and added back onto the stock. If it happens that the amount of product remaining in the basket is zero, then the corresponding product should be deleted from the basket
dictionary. In this case the function will return msg = None
, i.e., no message.
4) If amount
is negative but there are less units of the product in the basket, then the product will be removed from the basket and added back to the stock. The function will return a message of the form msg = 'Cannot remove this many {unit} from the basket, only removed {amount} {unit}.'
, where the appropriate replacements are performed on the string.
Hint: Recall that dictionaries behave similarly to lists and you can simply modify stock
and basket
inside a function. The changes will apply to the dictionaries available outside module. Therefore your addToBasket
function does not need to return updated stock
and basket
variables, only the message msg
. The function should update the values of stock
and basket
, which are provided as arguments to it, directly.
prepareCheckout(basket)
¶This is a very quick task (it can be done in two lines of code): Write a function prepareCheckout(basket)
that loops through each item in the basket
and adds a key amountPayable
taking the same value as the corresponding amount
value. This will be required later (in Task 8) because with promotions you may pay for an amount of a product that is less than the actual amount in the basket.
Example: Assume that our basket
dictionary is of the following form:
basket = {
'10013' : {
'name' : 'Emmental Slices 250G',
'price' : 1.75,
'unit' : 'pieces',
'promotion' : 'get2pay1',
'group' : None,
'amount' : 7
},
'10015' : {
'name' : 'Diced Beef 400G',
'price' : 4.50,
'unit' : 'pieces',
'promotion' : 'get4pay3',
'group' : 4,
'amount' : 5
},
'10017' : {
'name' : 'Brisket Beef 400G',
'price' : 3.50,
'unit' : 'pieces',
'promotion' : 'get4pay3',
'group' : 4,
'amount' : 3
},
}
Then after calling prepareCheckout(basket)
we want basket
to be the following:
basket = {
'10013' : {
'name' : 'Emmental Slices 250G',
'price' : 1.75,
'unit' : 'pieces',
'promotion' : 'get2pay1',
'group' : None,
'amount' : 7,
'amountPayable' : 7
},
'10015' : {
'name' : 'Diced Beef 400G',
'price' : 4.50,
'unit' : 'pieces',
'promotion' : 'get4pay3',
'group' : 4,
'amount' : 5,
'amountPayable' : 5
},
'10017' : {
'name' : 'Brisket Beef 400G',
'price' : 3.50,
'unit' : 'pieces',
'promotion' : 'get4pay3',
'group' : 4,
'amount' : 3,
'amountPayable' : 3
},
}
That's it. At this stage we will not worry about applying promotions, and only come back to them later. The next two steps are easier and should be done first.
getBill(basket)
(without promotions)¶Write a function that returns, as a string, a nicely formatted bill for the basket
. The total amount to be paid is given by the sum of amountPayable*price
for each product in the basket. The returned string should also include the total amount to be paid.
Example: For the basket in Task 5 the returned string from getBill(basket)
should look similar to
Here is your shopping bill:
Product | Price | Amount | Payable
------------------------------------+-----------+------------+----------
Emmental Slices 250G | 1.75 £ | 7 pieces | 12.25 £
Diced Beef 400G | 4.50 £ | 5 pieces | 22.50 £
Brisket Beef 400G | 3.50 £ | 3 pieces | 10.50 £
------------------------------------+-----------+------------+-----------
TOTAL: 45.25 £
main()
function¶The main()
function uses all the above functions to mimic a self-checkout system which allows the customer to list the products on stock, add (remove) products from the stock to their shopping basket, list the items in the basket, and display a bill for the basket.
You may want to use the following general form of the module (also available for download: supermarket.py
):
"""
Add a short description of the module.
"""
import csv # you may use this for loadStockFromFile()
# feel free to import other modules or define your own functions
# Task 1
def loadStockFromFile(filename):
"""
TODO: include a docstring for each function
"""
stock = {}
# TODO: load the stock items from file
return stock
# TODO: functions for the other tasks can go here
# Task 7
def main():
"""
TODO: include a docstring
"""
stock = loadStockFromFile('stock.csv')
basket = { }
print("*"*75)
print("*"*15+" "*10+"WELCOME TO STEFAN EXPRESS"+" "*10+"*"*15)
print("*"*75,"\n")
while True:
s = input("Input product-Ident, search string, 0 to display basket, 1 to check out: ")
# TODO: complete the code
if __name__ == '__main__':
main()
As you can see, in the main function we will ask the user for a string s
. Depending on this input, the following things can happen:
If s
is "0"
, then your function should output a string like "Your current shopping basket:
", followed by a use of the listItems(basket)
function to display the contents of the shopping basket. If the basket is empty, an informative message should be displayed. After the basket content has been listed, the function asks for user input again.
If s
is "1"
, then prepareCheckout(basket)
and getBill(basket)
are used to display the bill for the shopping basket. Following this, a goodbye message will be displayed (e.g., "Thank you for shopping with us!
") and the function stops.
If s
corresponds to an ident
key in the dictionary stock
, then the user wants to add/remove the corresponding product to/from the basket. The function hence asks "How many units ({}) do you want to add to your basket?
" (where {}
can be either pieces
or kg
), and inputs the amount (say nr
). If the amount nr
is not an integer or a floating point number (depending on what unit
the product has), the function we will ask for a corrected input. The function then calls msg = addToBasket(stock, basket, s, nr)
which will invoke the function we have defined in Task 4. If the returned msg
is not None
, the program displays msg
to the user (recall, msg
is a string representing a warning that nr
was to large positive or negative). Afterwards the program asks for user input again.
If s
is anything else, then we assume the user wants to search the stock and call substock = searchStock(stock, s)
and use listItems(substock)
to display the search results. Before listing the results, an appropriate description is given, and following it we again ask for user input. For example:
Input product-Ident, search string, 0 to display basket, 1 to check out: beef
There were 2 search results for 'beef':
Ident | Product | Price | Amount
-------+-------------------------------------+-----------+-------------
10015 | Diced Beef 400G | 4.50 £ | 14 pieces
10017 | Brisket Beef 400G | 3.50 £ | 15 pieces
Input product-Ident, search string, 0 to display basket, 1 to check out:
applyPromotions(basket)
¶Once you have completed all previous tasks and your program is working flawlessly you can attempt this final task!
Write a function applyPromotions(basket)
that adjusts the value of amountPayable
for each item in the basket. There are two types of promotions, indicated by the value of the promotion
key associated with each product.
Get 2 Pay 1: If a product has a promotion
string equal to get2pay1
, then every second instance of this product in the basket is free. For example, if amount
is equal to 3, then with this promotion type we will set amountPayable
to 2.
Get 4 Pay 3: If a number of products has a promotion
string equal to get4pay3
and the same integer value for group
(i.e., they are in the same promotions group), then out of 4 instances only 3 items have to be payed for. Our shop tries to maximize its profit by only giving away the cheapest products for free.
To illustrate, here is the result of applyPromotions
being applied to the basket
in Task 5:
basket = {
'10013' : {
'name' : 'Emmental Slices 250G',
'price' : 1.75,
'unit' : 'pieces',
'promotion' : 'get2pay1',
'group' : None,
'amount' : 7,
'amountPayable' : 4
},
'10015' : {
'name' : 'Diced Beef 400G',
'price' : 4.50,
'unit' : 'pieces',
'promotion' : 'get4pay3',
'group' : 4,
'amount' : 5,
'amountPayable' : 5
},
'10017' : {
'name' : 'Brisket Beef 400G',
'price' : 3.50,
'unit' : 'pieces',
'promotion' : 'get4pay3',
'group' : 4,
'amount' : 3,
'amountPayable' : 1
},
}
Note that we only pay for 4 Emmentals, because 3 out of 7 Emmentals in our basket qualify for the "Get 2 Pay 1" promotion. Likewise, we have a total number of 8 items qualifying for the "Get 4 Pay 3" promotion and sharing the same promotions group 4
, hence 2 of these items should go for free. As our shop is greedy, these free items will be "Brisket Beef
" (which is cheaper than "Diced Beef
"), hence amountPayable
has been reduced from 3
to 1
.
Finally, adjust the getBill
function from Task 6 to return a string displaying the applied promotions:
Here is your shopping bill:
Product | Price | Amount | Payable
------------------------------------+-----------+------------+----------
Emmental Slices 250G | 1.75 £ | 7 pieces | 12.25 £
Promotion get2pay1 | -1.75 £ | 3 pieces | -5.25 £
Diced Beef 400G | 4.50 £ | 5 pieces | 22.50 £
Brisket Beef 400G | 3.50 £ | 3 pieces | 10.50 £
Promotion get4pay3 | -3.50 £ | 2 pieces | -7.00 £
------------------------------------+-----------+------------+----------
TOTAL: 33.00 £
********** THANK YOU FOR SHOPPING AT STEFAN EXPRESS! ***********
Hint: You can easily infer which products have been subject to promotions by comparing amount
and amountPayable
.