from datetime import timedelta, datetime
class CustOrder:
def __init__(self, ref_no: int, recipient: str, address: str, date_ordered:
datetime, date_delivered: datetime):
self._ref_no = ref_no
self.recipient = recipient
self.address = address
self._date_ordered = date_ordered
self._date_delivered = date_delivered
self.item_list = []
@property
def ref_no(self):
return self._ref_no
@ref_no.setter
def ref_no(self, value):
if value == "": # Prevent from entering an empty value
raise ValueError("Reference number cannot be empty, must be a positive
integer")
try:
value = int(value)
if value > 0:
self._ref_no = value
else:
raise ValueError("Reference number must be a positive integer")
except (ValueError, TypeError): # Prevent from entering a value that cannot
be converted into an integer (list, dictionary ...)
raise ValueError("Reference number must be a valid positive integer")
@property
def date_ordered(self):
return self._date_ordered
@date_ordered.setter
def date_ordered(self, value):
if isinstance(value, datetime): # Value wil be an instance of the datetime
class, not the class itself (Cannot use if value == datetime:)
self._date_ordered = value
else:
raise ValueError("Date ordered must be a datetime object")
@property
def date_delivered(self):
return self._date_delivered
@date_delivered.setter
def date_delivered(self, value):
if isinstance(value, datetime): # check if value is an instance of the
datetime class
self._date_delivered = value
else:
raise ValueError("Date delivered must be a datetime object")
def add_item(self, item: 'OrderItem'): # Because class OrderItem is defined
later, using '' for OrderItem allow to reference class name as string to avoid
circular reference issue
for existing_item in self.item_list:
if existing_item.prod == item.prod:
existing_item.qty = existing_item.qty + item.qty
return
self.item_list.append(item)
def remove_item(self, item: 'OrderItem'):
for existing_item in self.item_list:
if existing_item.prod == item.prod:
if existing_item.qty >= item.qty: # Check for multiple condition
(True or False)
item_new_qty = existing_item.qty - item.qty
if item_new_qty > 0:
existing_item.qty = item_new_qty
else:
self.item_list.remove(existing_item)
return True
else:
return False
return False
def get_total(self):
return sum(item.get_total_amount() for item in self.item_list) # Without
special mention, sum is +
def __str__(self):
order_detail = "=" * 70 + "\n" # Horizontal Line
order_detail += f"Reference Number: {self.ref_no}\n"
order_detail += f"Name: {self.recipient}\n"
order_detail += f"Address: {self.address}\n"
order_detail += f"Date ordered/delivered: {self.date_ordered.strftime('%d-
%b-%Y %I:%M%p')} / {self.date_delivered.strftime('%d-%b-%Y %I:%M%p')}\n" # strftime
is a method in datetime, format date and time into strings based on specific format
order_detail += "-" * 70 + "\n"
order_detail += f"{'S/N':<4}{'Product':<20}{'Price':>10}{'Qty':>8}
{'SubTotal':>16}\n" # <4 will align the 'S/N' to the left in width of 4, >10 will
align the 'Price' to the right within a total width of 10 character
i = 1
for item in self.item_list:
order_detail += f"{i:<4}{str(item)}\n"
i += 1
order_detail += "-" * 70 + "\n"
order_detail += f"{'Total':>48.5} ${self.get_total():,.2f}\n"
order_detail += "* - discounted price\n"
order_detail += "=" * 70
return order_detail
class OrderItem:
def __init__(self, prod: str, unit_price: float, discount: float, qty: int):
self.prod = prod
self.unit_price = unit_price #Calls the setter
self.discount = discount # Using self._discount = discount will bypass
validation
self.qty = qty # Using self.qty = qty will go through validation
@property
def unit_price(self):
return self._unit_price # Getter uses _unit_price, this needs to return the
internal unit_price
@unit_price.setter
def unit_price(self, value): # When self.unit_price = value is called
if value == "":
raise ValueError("Unit price cannot be empty, must be a positive
number")
try:
value = float(value)
if value > 0:
self._unit_price = value #Setter stores in unit_price after
validation
else:
raise ValueError("Unit price must be a positive number")
except (ValueError, TypeError):
raise ValueError("Unit price must be a valid positive number")
@property
def discount(self):
return self._discount
@discount.setter
def discount(self, value):
if value == "":
raise ValueError("Discount amount cannot be empty, must be a float
between 0 and 1")
try:
value = float(value)
if 0 <= value <= 1: #Using decimal point to represent percentage (0.20
as 20% discount)
self._discount = value
else:
raise ValueError("Discount must be a float between 0 and 1")
except (ValueError, TypeError):
raise ValueError("Discount must be a valid float between 0 and 1")
@property
def qty(self):
return self._qty
@qty.setter
def qty(self, value):
if value == "":
raise ValueError("Quantity cannot be empty")
try:
value = int(value)
if value > 0:
self._qty = value
else:
raise ValueError("Quantity must be a positive integer greater than
0")
except (ValueError, TypeError):
raise ValueError("Quantity must be a valid positive integer greater
than 0")
def get_total_amount(self):
return self._unit_price * self._qty * (1 - self._discount)
def get_discount_amount(self):
return (self._unit_price * self._qty) * self._discount
def get_actual_amount(self):
return self.get_total_amount()
def __str__(self):
price = self._unit_price * (1 - self._discount)
discount_indicator = "*" if self._discount > 0 else "" # Display a * beside
subtotal for the amount of product which has been given a discount
return f"{self.prod:<20}{price:>10.2f}{self._qty:>8} {'$':>5}
{self.get_total_amount():>10.2f}{discount_indicator}" # Display 2 digit after
decimal point (Floating-point number)
def main():
current_time = datetime.now()
delivery_time = current_time + timedelta(days=2) # Set delivery date is 2 day
later than date ordered (Based on the current time)
order = CustOrder(
ref_no=170772,
recipient="Erick Aspas Santos",
address="12, Saint George Lane, Singapore 543210",
date_ordered=current_time,
date_delivered=delivery_time
)
order.add_item(OrderItem(prod="Laptop", unit_price=2000, qty=1, discount=0))
order.add_item(OrderItem(prod="Phone", unit_price=3000, qty=2, discount=0))
order.add_item(OrderItem(prod="Keyboard", unit_price=100, qty=1, discount=0))
order.add_item(OrderItem(prod="Mouse", unit_price=35.50, qty=3, discount=0))
print(order)
print("\n\nAdd Earphone as a new item: ")
order.add_item(OrderItem(prod="Earphone", unit_price=55.50, qty=1, discount=0))
print(order)
print("\n\nAdd quantity for Keyboard: ")
order.add_item(OrderItem(prod="Keyboard", unit_price=100, qty=5, discount=0))
print(order)
print("\n\nRemove Phone from order: ")
order.remove_item(OrderItem(prod="Phone", unit_price=3000, qty=2, discount=0))
print(order)
print("\n\nDeduct Keyboard quantity from order: ")
order.remove_item(OrderItem(prod="Keyboard", unit_price=100, qty=4,
discount=0))
print(order)
print("\n\nRemove non-existing item(Tablet): ")
order.remove_item(OrderItem(prod="Tablet", unit_price=1500, qty=1, discount=0))
print(order)
if __name__ == "__main__":
main()