From e1a84ff327bf4ea5923e3ef7f28c95e7129545cb Mon Sep 17 00:00:00 2001 From: Alaguraj0361 Date: Fri, 6 Feb 2026 16:40:33 +0530 Subject: [PATCH] implement a comprehensive restaurant reservation management system with scheduling, holiday management, and table booking. --- addons/dine360_reservation/__manifest__.py | 4 + .../__pycache__/main.cpython-310.pyc | Bin 2075 -> 8425 bytes .../dine360_reservation/controllers/main.py | 255 ++++++++++++++++-- .../data/reservation_schedule_data.xml | 40 +++ addons/dine360_reservation/models/__init__.py | 4 + .../__pycache__/__init__.cpython-310.pyc | Bin 196 -> 364 bytes .../reservation_holiday.cpython-310.pyc | Bin 0 -> 1411 bytes .../reservation_peak_hour.cpython-310.pyc | Bin 0 -> 1233 bytes .../reservation_schedule.cpython-310.pyc | Bin 0 -> 3017 bytes .../restaurant_reservation.cpython-310.pyc | Bin 5152 -> 6934 bytes .../restaurant_table.cpython-310.pyc | Bin 0 -> 1268 bytes .../models/reservation_holiday.py | 26 ++ .../models/reservation_peak_hour.py | 23 ++ .../models/reservation_schedule.py | 78 ++++++ .../models/restaurant_reservation.py | 96 +++++-- .../models/restaurant_table.py | 29 ++ .../security/ir.model.access.csv | 3 + .../dine360_reservation/views/menu_items.xml | 18 ++ .../views/reservation_holiday_views.xml | 30 +++ .../views/reservation_schedule_views.xml | 67 +++++ .../views/reservation_templates.xml | 178 ++++++++---- .../views/reservation_views.xml | 10 +- .../views/restaurant_table_views.xml | 51 ++++ 23 files changed, 825 insertions(+), 87 deletions(-) create mode 100644 addons/dine360_reservation/data/reservation_schedule_data.xml create mode 100644 addons/dine360_reservation/models/__pycache__/reservation_holiday.cpython-310.pyc create mode 100644 addons/dine360_reservation/models/__pycache__/reservation_peak_hour.cpython-310.pyc create mode 100644 addons/dine360_reservation/models/__pycache__/reservation_schedule.cpython-310.pyc create mode 100644 addons/dine360_reservation/models/__pycache__/restaurant_table.cpython-310.pyc create mode 100644 addons/dine360_reservation/models/reservation_holiday.py create mode 100644 addons/dine360_reservation/models/reservation_peak_hour.py create mode 100644 addons/dine360_reservation/models/reservation_schedule.py create mode 100644 addons/dine360_reservation/models/restaurant_table.py create mode 100644 addons/dine360_reservation/views/reservation_holiday_views.xml create mode 100644 addons/dine360_reservation/views/reservation_schedule_views.xml create mode 100644 addons/dine360_reservation/views/restaurant_table_views.xml diff --git a/addons/dine360_reservation/__manifest__.py b/addons/dine360_reservation/__manifest__.py index 81ef4f0..72ea698 100644 --- a/addons/dine360_reservation/__manifest__.py +++ b/addons/dine360_reservation/__manifest__.py @@ -16,8 +16,12 @@ 'security/ir.model.access.csv', 'data/reservation_sequence.xml', 'data/reservation_cron.xml', + 'data/reservation_schedule_data.xml', 'data/website_menu.xml', 'views/reservation_views.xml', + 'views/restaurant_table_views.xml', + 'views/reservation_schedule_views.xml', + 'views/reservation_holiday_views.xml', 'views/reservation_templates.xml', 'views/menu_items.xml', ], diff --git a/addons/dine360_reservation/controllers/__pycache__/main.cpython-310.pyc b/addons/dine360_reservation/controllers/__pycache__/main.cpython-310.pyc index 6522f029f19c6309e3261dee070f27c408723960..b4c5937f5cb23c0d76b3b6f7490d59180a818c40 100644 GIT binary patch literal 8425 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 diff --git a/addons/dine360_reservation/controllers/main.py b/addons/dine360_reservation/controllers/main.py index 8d5888d..70ce2de 100644 --- a/addons/dine360_reservation/controllers/main.py +++ b/addons/dine360_reservation/controllers/main.py @@ -7,47 +7,260 @@ class TableReservationController(http.Controller): @http.route(['/reservation'], type='http', auth="public", website=True) def reservation_form(self, **post): - floors = request.env['restaurant.floor'].sudo().search([]) - tables = request.env['restaurant.table'].sudo().search([]) + schedule = request.env['reservation.schedule'].sudo().search([]) return request.render("dine360_reservation.reservation_page_template", { - 'floors': floors, - 'tables': tables, + 'schedule': schedule }) + def _get_slot_duration(self, schedule, time_float): + """ Returns the slot duration for a given time based on peak hour settings """ + if not schedule: + return 1.0 + for peak in schedule.peak_hour_ids: + if peak.start_time <= time_float < peak.end_time: + return float(peak.slot_duration) + return float(schedule.default_slot_duration or 1.0) + + def _find_best_tables(self, floor, available_floor_tables, num_people): + """ Find the best combination of nearby tables on a floor, preferring same zone """ + # Case 1: Single table fits + singles = [t for t in available_floor_tables if t.min_party_size <= num_people <= t.max_party_size] + if singles: + best_single = min(singles, key=lambda t: t.max_party_size) + return [best_single] + + # Case 2: Merge needed + # Group tables by zone + zones = {} + for t in available_floor_tables: + z = t.zone or 'Default' + if z not in zones: zones[z] = [] + zones[z].append(t) + + best_overall_combo = [] + min_overall_dist = float('inf') + + # Try merging within each zone first + for zone_name, zone_tables in zones.items(): + if sum(t.max_party_size for t in zone_tables) < num_people: + continue + + for start_table in zone_tables: + current_combo = [start_table] + current_cap = start_table.max_party_size + others = [t for t in zone_tables if t.id != start_table.id] + + while current_cap < num_people and others: + nearest = min(others, key=lambda o: min( + (((o.position_h or 0) - (c.position_h or 0))**2 + ((o.position_v or 0) - (c.position_v or 0))**2)**0.5 + for c in current_combo + )) + current_combo.append(nearest) + current_cap += nearest.max_party_size + others.remove(nearest) + + if current_cap >= num_people: + dist_sum = 0 + for i in range(len(current_combo)): + for j in range(i + 1, len(current_combo)): + t1, t2 = current_combo[i], current_combo[j] + dist_sum += (((t1.position_h or 0) - (t2.position_h or 0))**2 + ((t1.position_v or 0) - (t2.position_v or 0))**2)**0.5 + + if dist_sum < min_overall_dist: + min_overall_dist = dist_sum + best_overall_combo = current_combo + + # If no same-zone combo found, use the best combo across the whole floor + if not best_overall_combo: + for start_table in available_floor_tables: + current_combo = [start_table] + current_cap = start_table.max_party_size + others = [t for t in available_floor_tables if t.id != start_table.id] + while current_cap < num_people and others: + nearest = min(others, key=lambda o: min( + (((o.position_h or 0) - (c.position_h or 0))**2 + ((o.position_v or 0) - (c.position_v or 0))**2)**0.5 + for c in current_combo + )) + current_combo.append(nearest) + current_cap += nearest.max_party_size + others.remove(nearest) + if current_cap >= num_people: + dist_sum = 0 + for i in range(len(current_combo)): + for j in range(i + 1, len(current_combo)): + t1, t2 = current_combo[i], current_combo[j] + dist_sum += (((t1.position_h or 0) - (t2.position_h or 0))**2 + ((t1.position_v or 0) - (t2.position_v or 0))**2)**0.5 + if dist_sum < min_overall_dist: + min_overall_dist = dist_sum + best_overall_combo = current_combo + + return best_overall_combo + + @http.route(['/reservation/get_slots'], type='json', auth="public", website=True) + def get_available_slots(self, date_str, num_people): + try: + res_date = datetime.datetime.strptime(date_str, '%Y-%m-%d').date() + num_people = int(num_people) + except: + return {'error': 'Invalid date or guest count'} + + # 1. Check for Holiday + holiday = request.env['reservation.holiday'].sudo().search([('date', '=', res_date)], limit=1) + if holiday and holiday.is_closed: + return {'error': 'The restaurant is closed on this day for %s.' % holiday.name} + + # 2. Get Schedule + day = str(res_date.weekday()) + schedule = request.env['reservation.schedule'].sudo().search([('day', '=', day)], limit=1) + if not schedule or schedule.is_closed: + return {'error': 'Reservations are not available on this day.'} + + # Determine operating hours + opening = holiday.opening_time if (holiday and holiday.opening_time) else schedule.opening_time + closing = holiday.closing_time if (holiday and holiday.closing_time) else schedule.closing_time + + # 3. Generate Slots (intervals based on table duration) + slots = [] + current_time = opening + restaurant_tz = pytz.timezone('America/Toronto') + now_local = datetime.datetime.now(restaurant_tz).replace(tzinfo=None) + + all_tables = request.env['restaurant.table'].sudo().search([('is_reservation_enabled', '=', True)]) + floors = request.env['restaurant.floor'].sudo().search([]) + + while current_time < closing: + duration = self._get_slot_duration(schedule, current_time) + slot_start_dt = datetime.datetime.combine(res_date, datetime.time(int(current_time), int((current_time % 1) * 60))) + slot_end_dt = slot_start_dt + datetime.timedelta(hours=duration) + + if slot_start_dt > now_local: + # Check for Break + is_in_break = False + if schedule.has_break: + if current_time < schedule.break_end_time and (current_time + duration) > schedule.break_start_time: + is_in_break = True + + if not is_in_break: + best_available_combo = [] + + for floor in floors: + floor_tables = all_tables.filtered(lambda t: t.floor_id == floor) + available_on_floor = [] + for table in floor_tables: + overlap = request.env['restaurant.reservation'].sudo().search([ + ('table_ids', 'in', [table.id]), + ('state', 'in', ['confirmed', 'draft']), + ('start_time', '<', slot_end_dt), + ('end_time', '>', slot_start_dt), + ]) + if not overlap: + available_on_floor.append(table) + + combo = self._find_best_tables(floor, available_on_floor, num_people) + if combo and (not best_available_combo or len(combo) < len(best_available_combo)): + best_available_combo = combo + if len(combo) == 1: break # Perfect match found + + if best_available_combo: + hours_int = int(current_time) + mins_int = int((current_time % 1) * 60) + ampm = "AM" if hours_int < 12 else "PM" + disp_h = hours_int if hours_int <= 12 else hours_int - 12 + if disp_h == 0: disp_h = 12 + + table_names = ", ".join([t.name for t in best_available_combo]) + floor_name = best_available_combo[0].floor_id.name + + slots.append({ + 'time': f"{hours_int:02d}:{mins_int:02d}", + 'display': f"{disp_h}:{mins_int:02d} {ampm}", + 'full_dt': slot_start_dt.strftime('%Y-%m-%dT%H:%M'), + 'tables': f"{floor_name} - {table_names}" + }) + + # Use the calculated duration as the interval to respect user configuration + current_time += duration + + return {'slots': slots} + @http.route(['/reservation/submit'], type='http', auth="public", website=True, methods=['POST'], csrf=True) def reservation_submit(self, **post): - # Extract data from post + # Extract data customer_name = post.get('customer_name') phone = post.get('phone') email = post.get('email') - floor_id = int(post.get('floor_id')) - table_id = int(post.get('table_id')) - start_time_str = post.get('start_time') # Format: 2024-05-20T18:00 num_people = int(post.get('num_people', 1)) + start_time_str = post.get('start_time') + + if not start_time_str: + return request.render("dine360_reservation.reservation_page_template", {'error': 'Please select a time slot.'}) - # Convert start_time to datetime object and localize to restaurant timezone (Brampton) + # Convert start_time to datetime object and localize to restaurant timezone (America/Toronto) restaurant_tz = pytz.timezone('America/Toronto') - local_start = datetime.datetime.strptime(start_time_str, '%Y-%m-%dT%H:%M') - local_start = restaurant_tz.localize(local_start) + try: + local_start = datetime.datetime.strptime(start_time_str, '%Y-%m-%dT%H:%M') + local_start = restaurant_tz.localize(local_start) + # Convert to UTC for Odoo + start_time = local_start.astimezone(pytz.utc).replace(tzinfo=None) + except Exception: + return request.render("dine360_reservation.reservation_page_template", { + 'error': _("Invalid date or time format."), + 'post': post, + }) - # Convert to UTC for Odoo - start_time = local_start.astimezone(pytz.utc).replace(tzinfo=None) + res_date = local_start.date() # Use local_start for date to get correct weekday - # Standard duration of 1 hour for now - end_time = start_time + datetime.timedelta(hours=1) + # Determine Duration + day = str(res_date.weekday()) + schedule = request.env['reservation.schedule'].sudo().search([('day', '=', day)], limit=1) + time_float = local_start.hour + local_start.minute / 60.0 + duration = self._get_slot_duration(schedule, time_float) + end_time = start_time + datetime.timedelta(hours=duration) - # Create reservation + # FIND TABLES (Nearest Merge) + assigned_tables = [] + target_floor = False + + all_floors = request.env['restaurant.floor'].sudo().search([]) + for floor in all_floors: + floor_tables = request.env['restaurant.table'].sudo().search([ + ('floor_id', '=', floor.id), + ('is_reservation_enabled', '=', True) + ]) + + available_tables = [] + for table in floor_tables: + overlap = request.env['restaurant.reservation'].sudo().search([ + ('table_ids', 'in', [table.id]), + ('state', 'in', ['confirmed', 'draft']), + ('start_time', '<', end_time), + ('end_time', '>', start_time), + ]) + if not overlap: + available_tables.append(table) + + combo = self._find_best_tables(floor, available_tables, num_people) + if combo and (not assigned_tables or len(combo) < len(assigned_tables)): + assigned_tables = combo + target_floor = floor + if len(combo) == 1: break + + if not assigned_tables: + return request.render("dine360_reservation.reservation_page_template", {'error': 'Sorry, no tables available for this time/group size.'}) + + # Create Reservation try: reservation = request.env['restaurant.reservation'].sudo().create({ 'customer_name': customer_name, 'phone': phone, 'email': email, - 'floor_id': floor_id, - 'table_id': table_id, + 'num_people': num_people, + 'floor_id': target_floor.id, + 'table_ids': [(6, 0, [t.id for t in assigned_tables])], + 'table_id': assigned_tables[0].id, 'start_time': start_time, 'end_time': end_time, - 'num_people': num_people, - 'state': 'draft' + 'state': 'confirmed' # Direct confirmation from website }) return request.render("dine360_reservation.reservation_success_template", { 'reservation': reservation, @@ -55,7 +268,5 @@ class TableReservationController(http.Controller): except Exception as e: return request.render("dine360_reservation.reservation_page_template", { 'error': str(e), - 'floors': request.env['restaurant.floor'].sudo().search([]), - 'tables': request.env['restaurant.table'].sudo().search([]), 'post': post, }) diff --git a/addons/dine360_reservation/data/reservation_schedule_data.xml b/addons/dine360_reservation/data/reservation_schedule_data.xml new file mode 100644 index 0000000..4522cde --- /dev/null +++ b/addons/dine360_reservation/data/reservation_schedule_data.xml @@ -0,0 +1,40 @@ + + + + + 0 + 12.0 + 21.0 + + + 1 + 12.0 + 21.0 + + + 2 + 12.0 + 21.0 + + + 3 + 12.0 + 21.0 + + + 4 + 12.0 + 23.0 + + + 5 + 12.0 + 23.0 + + + 6 + 12.0 + 21.0 + + + diff --git a/addons/dine360_reservation/models/__init__.py b/addons/dine360_reservation/models/__init__.py index 7c07528..efba3da 100644 --- a/addons/dine360_reservation/models/__init__.py +++ b/addons/dine360_reservation/models/__init__.py @@ -1 +1,5 @@ from . import restaurant_reservation +from . import restaurant_table +from . import reservation_schedule +from . import reservation_holiday +from . import reservation_peak_hour diff --git a/addons/dine360_reservation/models/__pycache__/__init__.cpython-310.pyc b/addons/dine360_reservation/models/__pycache__/__init__.cpython-310.pyc index a7b05deb6fd363036308e1dcf3eb3cb5adcdad32..12bdb0ded5e3e6101a471f83e3b7d40682cb01d2 100644 GIT binary patch literal 364 zcmYk2J!%6n5QU}vi!nAB68jEY#4;F63KMdIG$D-|AxDB0*_Di>HSq!5T~d$j#?d0`dNRF*i7WFi9hWQAE*}bd*zyoB?M%8#xEgc|LLh zT<~J#61e0gaP>y&yI%^|)O1*R9ir^Lv{3+Rf0EHS3zbDuiSp2D@E(#D5k4BN`q95n z{l>LM$uqPsQ~Rjpr)XU7U+Zch9_R5nN`X?MR3KO4C&(y&Ppq?%>96SJqg2Y-z?89i myW6CXWt~&H4NM4QO%!5%JP(sSnDDq~oHO9p&d7}7TF`$BEMK$$ delta 78 zcmaFEbcB&NpO=@50SFZPS~Arp^2&+{068fPDU3M`xr|Yaj0`DE!3>(r6AR>p{WO_w aF%&TYh$emdQelK5QTn0R{k5O$~Ga diff --git a/addons/dine360_reservation/models/__pycache__/reservation_holiday.cpython-310.pyc b/addons/dine360_reservation/models/__pycache__/reservation_holiday.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d99c1a5a46989dc9349658f1f4a4e50604cdbc6 GIT binary patch literal 1411 zcmZWpOONA35bpLXFK3d;ymk>1%?%$&ct{8#gcdS94lpahii0I&8CPZESwE6)XEd=E zX~hi3`3ne+{H4Bf;x8Z}sCF`7iSG25u5wk^qd&VG4*QJ8&v!qiFCE5yH$r=mjy<8J zJ|F-FJYzWzd2WSPZija6gbp`)D|2%%^f>#Rfep?b0|)t4=hhCp;C{mb?+@~4fup^= z1k9B77Aa<+v>mTf&7*+p(YJAyLR_U~@u`%hOpHf+xFkHGrD_6XAqN&(z(X6X&;dJi z(Yxw^11@;bxu?h^_kiL<@7@agBn_ZX(tsqNq#^h)gaeWe8fgUMdmfH({L3L6!UT?f zwc!{}?$|94CvbYn!y`DOS{~`i*QiilH@SXMnyk0=k^E0LUG9~ywhYDIa`yBZrAz_q z9#xA1P$hbBPAJorss33#!Rh5x%$G7%RURvG9&ZC%d#aLYajCl!*PB!#=)o$B6Qp2( zQak5#T42c@bw`?8tVUt)ZX z0`pb16;&xV3WdbWO%_X`k|n|>Ly;~-QC1>pLOq=9EHPWPjx~uq92@W^MeUcnSW!i> zv~La|lbzH{RkS-X;Z(`PuxMi#Ij5!GCwRfm*>`6@@E81wsmFv|4GAsT?9J}ED<4;O z`6FLjz~~OGi~61a6>Sm``9@XZd!!$=4V$`5=H}TnaOELIl_LXV1D*jx%)ru) z!fc^!iOFwF9+U3R`7AH08QxSfelrFr3pInZz<1w!D{8T2wtFRJEmgE9&~&xcenhQK zo;QtACcN?qy4HmIG_9A;U-fIP+ugHl+V&#y3=G$zO_8oQSo^#GH;gW*gO3YQ?m5|T zrCO#+G~A3ci7{*i-VhVj>leYX?nY4&=NLu0A4QZoae(%I6sN_Gg0zj}qB%N%az*gD_`)`3NxwEEoYbpLMbE`pwRN+?Td7}sPh yjc;?)dSsqV@g~9MbyW=swELC$ur=PdpeWbR7U{x-( zT#))7fE$0wS5Eu|IKbFxA;O7Ap3h^?c)l-b`u&jLc>L*O@zo*ZyKxqWi_Qj4c?S&; zpe3njLMxWA%1NBcO?>(;DCEVz~ywmbL=Dmc)KL_f8co%yV|cB;HA|5 zUcpO{+R3JcrfE#|@Of4ikg1}nw}fa!ZcZ!?ADs=Havu#R2?dfcpos%4ak;neo6O*W zf9fP1^aJQX0NqoTbgdVjc_DgBuLvxCrO<;uqJvX6>GA&O0SsaEg~0{5ctVb8GK6KU zeMK)n<&ujxwzelcJ9yO0M0-j61IJTD$FB{16=PL46Irby6C0V}XcGbRvUX+hl+0gq zn3Y@yyXUJ;p2<7|9y9GrC5rl$4w@QU#uaae_QGA22^BpnD*h~H!tkd9!4GGJ;L!GM z$W{$DAa-NcI;-PbZFqAv+Aivu;xcAGowf05>#y0`OvD;SbkSG<16pSt13K$-zr8rm zTQ=(NLqs3*#?Cx_2l!q#E0yS(VnTesP^rQKb--(|tDNH2`Czy(%DRtJUPE(4Hpz>@ zdwN9o2~K+8iIHN;0eXy|5A=`$K_=Kvd)<~|GejmsRL!J{UUK~IHG)T&&*J0Q6+^sP zjEphT5grdVI!Mzxt9Y8~FikNf z$^+NEG(DVU<=KvBLl6Wmb5Try~E+Jt1v|v^Y)0V1VUI7QHBA*Jw+`;kTXWCIN*a2v JkRH9v{s1j+P7nY9 literal 0 HcmV?d00001 diff --git a/addons/dine360_reservation/models/__pycache__/reservation_schedule.cpython-310.pyc b/addons/dine360_reservation/models/__pycache__/reservation_schedule.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e5535ec76e10b2caeb445537337cda16e1127e6 GIT binary patch literal 3017 zcmZuzOLH5?5#E`7U;%;;QzQk+vbB=+;K~yCvUE^fiAtgr=OD@zs^ZjEQd?{EkX#bj z1!oqHxCK_FBIT${uC7#xx%n^6H7A{ON%4|&&jKVYYk}#V?&+RKf89NsPfs^2e767b z_w+wa%lbDllaGVUBW&s+60m@EtsaY5k4L;`M|RJN97b~9b$edqG3&P$*x3=xDE(gXk*l?WCd$O4~_4)hrH~p8Z48O(7}LJbNl-E<0o~`P7hk zgiTq@vV;{eup$mDvVlhq*pVx|{n{?B{4WMBcu;$btD~$AKGdNBQ^+?A--KzHdCQ|I zP|pu&u_@tEgHe1DRw+Fkuy(nbjaU z3KG=`Ya!QcReR6#3`NAO>DtSIP$O<#*S`~xo$(vmf4Mi1&+#`3=Qx07NZ zN9+UbZ4YSHa%vM-%XfnBggAJKYj~=VX|}6_*$&qG@-p_Zy1Dvbng@?UZj@yU zEy|g15*0j_A~`T?JuBxbX1hpaVMHM_>g7x|@H7J>E-%vL0?alhoT@@MYzjSwptZPS}vee8ky? zb5QQeTO*lw!hXc!GU95){nJJp|BwS8y)6uSd#kjAdx5sXx}J}Bx_MH>MQ(N%V+`qL zUWJX3=-Kh^M^fGFRN?T!@JOobdx?s_r%=#y=7n=&5LDVTl|*Dv@zdkUjr=FB9VNOu z+Log8lO?}IFPv_!^s-__yeedJF9FCiwE}4-HXf|T6Y#856>()k6pvwQ_mAWioazpF zb_R*hX7J~+>&)lQX?bTN?#8*?{y2CYLTUUET2$H%Vt>L;fjGTp$DH0n-a;%Eu;d)u zL-hZFCFjA;7&ym{;hDTVUeWe>Hzx(*mvcA6ETCqq(cP_$L(YM0_x=~}N!Bkd%;eovn{-;5e> zSL80*%d1E>uqj&5XLI;lU`=f0t&jTa7jYEYx`A~n9%Sj^K$KI}dztE3d2{S%llR45 zssfY;Nf+Y=jso#2RfW1H16*2OCvua>Eh0CN{33DAVA#P?lQ?Zm9JX(%RZ1Of{J@nL z3cGLsA$raJMlx59M?J@~ErcjUX>Fp9u&LFIG0Rx;$`aFQlt$sWz>=?n-ar+>_X7V| zQ$;XJh=9i0<fQT+zT)I1WOdx&!r^J5t)|0`z5CwvmdIM44;P0`n!!+h3a^UPu8m5;^y*kCKXs{PnZ8ppa3$5@E)69W6qI6fRC zU85Ll*QlTnuZE6H`)21}oJ+v_!lupDx5p={4&y$f%6yKku0784u1GSI0u#`Zi}m8@ z&W;scY%z>3HtoO2#Ck8uj`TELU@^rNURz2#PxcZiuc4{@g2-(mR3%N~>$zARcCiL# zcp)TMRg@%Db>mE;co5_c?tY Y8}pCtP5WP0ApM%kry`S<)nrTj|5*Dsp#T5? literal 0 HcmV?d00001 diff --git a/addons/dine360_reservation/models/__pycache__/restaurant_reservation.cpython-310.pyc b/addons/dine360_reservation/models/__pycache__/restaurant_reservation.cpython-310.pyc index 628bbd56c09ceb010d4688263013ea35b44ccf82..b2bf0a2a1f7381b1e29a229634a1c9f3dca685ca 100644 GIT binary patch literal 6934 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 TfsY?<+VTY{)KSPCwk!Sx*pE~A literal 0 HcmV?d00001 diff --git a/addons/dine360_reservation/models/reservation_holiday.py b/addons/dine360_reservation/models/reservation_holiday.py new file mode 100644 index 0000000..238c358 --- /dev/null +++ b/addons/dine360_reservation/models/reservation_holiday.py @@ -0,0 +1,26 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + +class ReservationHoliday(models.Model): + _name = 'reservation.holiday' + _description = 'Reservation Holiday/Override' + _order = 'date desc' + + name = fields.Char(string='Description', required=True, placeholder="e.g. Christmas Day") + date = fields.Date(string='Date', required=True) + + is_closed = fields.Boolean(string='Closed for Reservations', default=True) + + opening_time = fields.Float(string='Override Opening Time', help="Leave empty to use regular schedule if not closed") + closing_time = fields.Float(string='Override Closing Time') + + @api.constrains('opening_time', 'closing_time', 'is_closed') + def _check_times(self): + for rec in self: + if not rec.is_closed and (rec.opening_time or rec.closing_time): + if rec.opening_time >= rec.closing_time: + raise ValidationError(_("Override Opening time must be before closing time.")) + + _sql_constraints = [ + ('date_unique', 'unique(date)', 'An override for this date already exists!') + ] diff --git a/addons/dine360_reservation/models/reservation_peak_hour.py b/addons/dine360_reservation/models/reservation_peak_hour.py new file mode 100644 index 0000000..6e397c2 --- /dev/null +++ b/addons/dine360_reservation/models/reservation_peak_hour.py @@ -0,0 +1,23 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + +class ReservationPeakHour(models.Model): + _name = 'reservation.peak.hour' + _description = 'Restaurant Peak Hour Duration' + + schedule_id = fields.Many2one('reservation.schedule', string='Schedule', ondelete='cascade') + start_time = fields.Float(string='Peak Start Time', required=True) + end_time = fields.Float(string='Peak End Time', required=True) + + slot_duration = fields.Selection([ + ('0.5', '30 Minutes'), + ('1.0', '1 Hour'), + ('1.5', '1.5 Hours'), + ('2.0', '2 Hours'), + ], string='Peak Slot Duration', default='1.0', required=True) + + @api.constrains('start_time', 'end_time') + def _check_times(self): + for rec in self: + if rec.start_time >= rec.end_time: + raise ValidationError(_("Peak start time must be before end time.")) diff --git a/addons/dine360_reservation/models/reservation_schedule.py b/addons/dine360_reservation/models/reservation_schedule.py new file mode 100644 index 0000000..662eb67 --- /dev/null +++ b/addons/dine360_reservation/models/reservation_schedule.py @@ -0,0 +1,78 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + +class ReservationSchedule(models.Model): + _name = 'reservation.schedule' + _description = 'Restaurant Reservation Schedule' + _order = 'day asc' + + day = fields.Selection([ + ('0', 'Monday'), + ('1', 'Tuesday'), + ('2', 'Wednesday'), + ('3', 'Thursday'), + ('4', 'Friday'), + ('5', 'Saturday'), + ('6', 'Sunday') + ], string='Day of Week', required=True) + + opening_time = fields.Float(string='Opening Time', default=12.0, help="Format: 14.5 for 2:30 PM") + closing_time = fields.Float(string='Closing Time', default=22.0) + + has_break = fields.Boolean(string='Has Break Time', default=False) + break_start_time = fields.Float(string='Break Start Time') + break_end_time = fields.Float(string='Break End Time') + + is_closed = fields.Boolean(string='Closed for Reservations', default=False) + + default_slot_duration = fields.Selection([ + ('0.5', '30 Minutes'), + ('1.0', '1 Hour'), + ('1.5', '1.5 Hours'), + ('2.0', '2 Hours'), + ('3.0', '3 Hours'), + ], string='Default Slot Duration', default='1.0', required=True) + + peak_hour_ids = fields.One2many('reservation.peak.hour', 'schedule_id', string='Peak Hour Slot Overrides') + + display_opening = fields.Char(compute='_compute_display_times') + display_closing = fields.Char(compute='_compute_display_times') + display_break = fields.Char(compute='_compute_display_times') + + def _compute_display_times(self): + for rec in self: + rec.display_opening = self._float_to_time_str(rec.opening_time) + rec.display_closing = self._float_to_time_str(rec.closing_time) + if rec.has_break: + rec.display_break = f"{self._float_to_time_str(rec.break_start_time)} - {self._float_to_time_str(rec.break_end_time)}" + else: + rec.display_break = "" + + def _float_to_time_str(self, time_float): + hours = int(time_float) + minutes = int((time_float - hours) * 60) + ampm = "am" if hours < 12 else "pm" + display_hours = hours if hours <= 12 else hours - 12 + if display_hours == 0: display_hours = 12 + return f"{display_hours}:{minutes:02d}{ampm}" + + _sql_constraints = [ + ('day_unique', 'unique(day)', 'Schedule for this day already exists!') + ] + + @api.constrains('opening_time', 'closing_time', 'break_start_time', 'break_end_time', 'has_break') + def _check_times(self): + for rec in self: + if not rec.is_closed: + if rec.opening_time >= rec.closing_time: + raise ValidationError(_("Opening time must be before closing time.")) + if rec.has_break: + if not (rec.opening_time < rec.break_start_time < rec.break_end_time < rec.closing_time): + raise ValidationError(_("Break time must be within opening and closing hours, and start before it ends.")) + + def name_get(self): + result = [] + for rec in self: + day_name = dict(self._fields['day'].selection).get(rec.day) + result.append((rec.id, day_name)) + return result diff --git a/addons/dine360_reservation/models/restaurant_reservation.py b/addons/dine360_reservation/models/restaurant_reservation.py index e09df98..1731b4a 100644 --- a/addons/dine360_reservation/models/restaurant_reservation.py +++ b/addons/dine360_reservation/models/restaurant_reservation.py @@ -14,8 +14,13 @@ class RestaurantReservation(models.Model): email = fields.Char(string='Email') num_people = fields.Integer(string='Number of People', default=1) - floor_id = fields.Many2one('restaurant.floor', string='Floor', required=True) - table_id = fields.Many2one('restaurant.table', string='Table', required=True) + floor_id = fields.Many2one('restaurant.floor', string='Floor') + table_id = fields.Many2one('restaurant.table', string='Primary Table') + table_ids = fields.Many2many('restaurant.table', string='Tables') + + is_admin_override = fields.Boolean(string='Admin Override', default=False, help="Skip all validation and availability checks") + override_reason = fields.Text(string='Override Reason') + overridden_by_id = fields.Many2one('res.users', string='Overridden By', readonly=True) start_time = fields.Datetime(string='Start Time', required=True) end_time = fields.Datetime(string='End Time', required=True) @@ -51,43 +56,104 @@ class RestaurantReservation(models.Model): def create(self, vals): if vals.get('name', _('New')) == _('New'): vals['name'] = self.env['ir.sequence'].next_by_code('restaurant.reservation') or _('New') + if vals.get('is_admin_override'): + vals['overridden_by_id'] = self.env.user.id return super(RestaurantReservation, self).create(vals) - @api.constrains('table_id', 'start_time', 'end_time', 'state') + def write(self, vals): + if vals.get('is_admin_override'): + vals['overridden_by_id'] = self.env.user.id + return super(RestaurantReservation, self).write(vals) + + @api.constrains('is_admin_override', 'override_reason') + def _check_override_reason(self): + for rec in self: + if rec.is_admin_override and not rec.override_reason: + raise ValidationError(_("Please provide a reason for the admin override.")) + + @api.constrains('table_ids', 'start_time', 'end_time', 'state', 'is_admin_override') def _check_overlap(self): for rec in self: + if rec.is_admin_override: + continue if rec.state in ['confirmed', 'completed']: + tables = rec.table_ids or rec.table_id + if not tables: + continue overlap = self.search([ ('id', '!=', rec.id), - ('table_id', '=', rec.table_id.id), + ('table_ids', 'in', tables.ids), ('state', '=', 'confirmed'), ('start_time', '<', rec.end_time), ('end_time', '>', rec.start_time), ]) if overlap: - raise ValidationError(_('This table is already reserved for the selected time slot.')) + raise ValidationError(_('One or more tables are already reserved for the selected time slot.')) - @api.constrains('start_time', 'end_time') + @api.constrains('start_time', 'end_time', 'is_admin_override') def _check_opening_hours(self): restaurant_tz = pytz.timezone('America/Toronto') for rec in self: + if rec.is_admin_override: + continue local_start = pytz.utc.localize(rec.start_time).astimezone(restaurant_tz) local_end = pytz.utc.localize(rec.end_time).astimezone(restaurant_tz) - day = local_start.weekday() # 0=Mon, 6=Sun + res_date = local_start.date() + day = str(local_start.weekday()) # 0=Mon, 6=Sun time_start = local_start.hour + local_start.minute / 60.0 time_end = local_end.hour + local_end.minute / 60.0 - if day in [6, 0, 1, 2, 3]: # Sun-Thu - if time_start < 12.0 or time_end > 21.0: - raise ValidationError(_('Reservations for Sunday - Thursday must be between 12:00 PM and 9:00 PM (Local Time).')) - else: # Fri-Sat - if time_start < 12.0 or time_end > 23.0: - raise ValidationError(_('Reservations for Friday & Saturday must be between 12:00 PM and 11:00 PM (Local Time).')) + # 1. Check for Holiday/Override + holiday = self.env['reservation.holiday'].sudo().search([('date', '=', res_date)], limit=1) + + if holiday: + if holiday.is_closed: + raise ValidationError(_("Reservations are closed on %s for %s.") % (res_date.strftime('%B %d, %Y'), holiday.name)) + + # Check holiday override hours if specified + if holiday.opening_time or holiday.closing_time: + opening = holiday.opening_time or 0.0 + closing = holiday.closing_time or 24.0 + if time_start < opening or time_end > closing: + raise ValidationError(_("Special hours for %s (%s): %s to %s.") % ( + holiday.name, + res_date.strftime('%B %d'), + self._float_to_time_str(opening), + self._float_to_time_str(closing) + )) + return # If holiday found and validated, skip regular schedule - @api.onchange('start_time') + # 2. Check Regular Schedule + schedule = self.env['reservation.schedule'].sudo().search([('day', '=', day)], limit=1) + + if not schedule: + continue + + if schedule.is_closed: + raise ValidationError(_("Reservations are closed for %s.") % dict(schedule._fields['day'].selection).get(day)) + + if time_start < schedule.opening_time or time_end > schedule.closing_time: + raise ValidationError(_("Reservations for %s must be between %s and %s.") % ( + dict(schedule._fields['day'].selection).get(day), + self._float_to_time_str(schedule.opening_time), + self._float_to_time_str(schedule.closing_time) + )) + + if schedule.has_break: + if time_start < schedule.break_end_time and time_end > schedule.break_start_time: + raise ValidationError(_("The restaurant has a break between %s and %s on %s.") % ( + self._float_to_time_str(schedule.break_start_time), + self._float_to_time_str(schedule.break_end_time), + dict(schedule._fields['day'].selection).get(day) + )) + + @api.onchange('start_time', 'table_id') def _onchange_start_time(self): - if self.start_time: + if self.start_time and self.table_id: + duration = float(self.table_id.reservation_slot_duration or 1.0) + self.end_time = self.start_time + timedelta(hours=duration) + elif self.start_time: self.end_time = self.start_time + timedelta(hours=1) def action_confirm(self): diff --git a/addons/dine360_reservation/models/restaurant_table.py b/addons/dine360_reservation/models/restaurant_table.py new file mode 100644 index 0000000..4a6539c --- /dev/null +++ b/addons/dine360_reservation/models/restaurant_table.py @@ -0,0 +1,29 @@ +from odoo import models, fields, api + +class RestaurantTable(models.Model): + _inherit = 'restaurant.table' + + is_reservation_enabled = fields.Boolean(string='Enabled for Reservation', default=True) + min_party_size = fields.Integer(string='Min Party Size', default=1) + max_party_size = fields.Integer(string='Max Party Size', default=1) + zone = fields.Char(string='Zone/Section', help="e.g. Window Side, Garden, VIP Section") + reservation_slot_duration = fields.Selection([ + ('0.5', '30 Minutes'), + ('1.0', '1 Hour'), + ('1.5', '1.5 Hours'), + ('2.0', '2 Hours'), + ('3.0', '3 Hours'), + ], string='Slot Duration', default='1.0', required=True) + + @api.onchange('seats') + def _onchange_seats_for_reservation(self): + for record in self: + if record.seats == 6: + record.min_party_size = 4 + record.max_party_size = 6 + elif record.seats == 4: + record.min_party_size = 2 + record.max_party_size = 4 + else: + record.min_party_size = 1 + record.max_party_size = record.seats or 1 diff --git a/addons/dine360_reservation/security/ir.model.access.csv b/addons/dine360_reservation/security/ir.model.access.csv index 6191133..aab549d 100644 --- a/addons/dine360_reservation/security/ir.model.access.csv +++ b/addons/dine360_reservation/security/ir.model.access.csv @@ -1,2 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_restaurant_reservation_user,restaurant.reservation,model_restaurant_reservation,base.group_user,1,1,1,1 +access_reservation_schedule_user,reservation.schedule,model_reservation_schedule,base.group_user,1,1,1,1 +access_reservation_holiday_user,reservation.holiday,model_reservation_holiday,base.group_user,1,1,1,1 +access_reservation_peak_hour_user,reservation.peak_hour,model_reservation_peak_hour,base.group_user,1,1,1,1 diff --git a/addons/dine360_reservation/views/menu_items.xml b/addons/dine360_reservation/views/menu_items.xml index 0106528..3c2a6fb 100644 --- a/addons/dine360_reservation/views/menu_items.xml +++ b/addons/dine360_reservation/views/menu_items.xml @@ -12,4 +12,22 @@ parent="menu_restaurant_reservation_root" action="action_restaurant_reservation" sequence="10"/> + + + + + + diff --git a/addons/dine360_reservation/views/reservation_holiday_views.xml b/addons/dine360_reservation/views/reservation_holiday_views.xml new file mode 100644 index 0000000..abc6612 --- /dev/null +++ b/addons/dine360_reservation/views/reservation_holiday_views.xml @@ -0,0 +1,30 @@ + + + + reservation.holiday.tree + reservation.holiday + + + + + + + + + + + + + Holiday / Special Overrides + reservation.holiday + tree + +

+ Add holiday or festival overrides. +

+

+ You can mark a specific date as closed or set special opening/closing hours that override the regular schedule. +

+
+
+
diff --git a/addons/dine360_reservation/views/reservation_schedule_views.xml b/addons/dine360_reservation/views/reservation_schedule_views.xml new file mode 100644 index 0000000..2ced474 --- /dev/null +++ b/addons/dine360_reservation/views/reservation_schedule_views.xml @@ -0,0 +1,67 @@ + + + + reservation.schedule.tree + reservation.schedule + + + + + + + + + + + + + + + reservation.schedule.form + reservation.schedule + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + Reservation Schedule + reservation.schedule + tree,form + +

+ Manage your daily reservation hours and break times. +

+
+
+
diff --git a/addons/dine360_reservation/views/reservation_templates.xml b/addons/dine360_reservation/views/reservation_templates.xml index d0ce5f6..dfb94e0 100644 --- a/addons/dine360_reservation/views/reservation_templates.xml +++ b/addons/dine360_reservation/views/reservation_templates.xml @@ -12,9 +12,35 @@

Table Reservation

Book your spot for an authentic South Indian dining experience.

-
- Sun - Thu: 12pm - 9pm | Fri & Sat: 12pm - 11pm -
+
+ +
+
+
+ +
+
+ +
+
+ + Closed + + + - + +
Break: +
+
+
+
+
+
+
+
+
@@ -39,32 +65,28 @@ -
- - -
-
- - -
-
+
-
- - -
+
+ + +
+
+ +
+
+ + Select a date and guest count to see available slots +
+
+ + +
@@ -72,26 +94,83 @@ CONFIRM RESERVATION
- + + + function fetchSlots() { + const date = dateInput.value; + const guests = guestsInput.value; + + if (!date || !guests) return; + + container.innerHTML = '
Loading slots...
'; + hiddenInput.value = ""; + + fetch('/reservation/get_slots', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: "2.0", + params: { date_str: date, num_people: guests } + }) + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + container.innerHTML = `
${data.error.message}
`; + return; + } + const result = data.result; + container.innerHTML = ""; + + if (result.error) { + container.innerHTML = `
${result.error}
`; + return; + } + + if (result.slots.length === 0) { + container.innerHTML = '
No available slots for this selection. Try another date or party size.
'; + return; + } + + result.slots.forEach(slot => { + const btn = document.createElement('button'); + btn.type = "button"; + btn.className = "btn btn-outline-dark slot-btn transition-all m-1"; + btn.style.cssText = "border-radius: 8px; font-weight: 600; padding: 10px 15px; border: 1px solid #fecd4f; min-width: 100px;"; + btn.innerHTML = slot.display; + btn.onclick = function() { + document.querySelectorAll('.slot-btn').forEach(b => { + b.classList.remove('active'); + b.style.backgroundColor = ""; + b.style.color = ""; + }); + this.classList.add('active'); + this.style.backgroundColor = "#fecd4f"; + this.style.color = "#171422"; + hiddenInput.value = slot.full_dt; + + const infoDiv = document.getElementById('selection_info'); + const infoText = document.getElementById('assigned_table_text'); + infoDiv.style.display = 'block'; + infoText.innerHTML = "Assignment: " + slot.tables + ""; + }; + container.appendChild(btn); + }); + }); + } + + dateInput.addEventListener('change', fetchSlots); + guestsInput.addEventListener('change', fetchSlots); + + if(dateInput.value) fetchSlots(); + }); + @@ -117,7 +196,14 @@
Floor:
-
Table:
+
Table(s): + + + + + + +
Guests:
Time:
diff --git a/addons/dine360_reservation/views/reservation_views.xml b/addons/dine360_reservation/views/reservation_views.xml index 175cad5..ed315dd 100644 --- a/addons/dine360_reservation/views/reservation_views.xml +++ b/addons/dine360_reservation/views/reservation_views.xml @@ -10,7 +10,7 @@ - + @@ -47,10 +47,13 @@ + + + - + @@ -69,12 +72,11 @@ - + - diff --git a/addons/dine360_reservation/views/restaurant_table_views.xml b/addons/dine360_reservation/views/restaurant_table_views.xml new file mode 100644 index 0000000..9505ee7 --- /dev/null +++ b/addons/dine360_reservation/views/restaurant_table_views.xml @@ -0,0 +1,51 @@ + + + + + restaurant.table.form.inherit + restaurant.table + + + + + + + + + + + + + + + + + restaurant.table.tree.reservation.settings + restaurant.table + + + + + + + + + + + + + + + + + Reservation Settings + restaurant.table + tree,form + + +

+ Configure your tables for reservation. +

+
+
+