diff --git a/product_rental_web_bookings/__init__.py b/product_rental_web_bookings/__init__.py new file mode 100755 index 0000000..b0f26a9 --- /dev/null +++ b/product_rental_web_bookings/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import controllers diff --git a/product_rental_web_bookings/__manifest__.py b/product_rental_web_bookings/__manifest__.py new file mode 100755 index 0000000..7797d15 --- /dev/null +++ b/product_rental_web_bookings/__manifest__.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +{ + "name": "Product Rental Web Bookings", + "category": "Product", + "version": "14.0.1.0", + "summary": "Book products on several rental periods through website", + "author": "Elabore", + "website": "https://elabore.coop/", + "installable": True, + "application": True, + "auto_install": False, + "description": """ +======================= +Product Rental Bookings +======================= +This module allows Odoo website visitors to rent products on several periods. + +Installation +============ +Just install product_rental_bookings, all dependencies will be installed by default. + +Known issues / Roadmap +====================== + +Bug Tracker +=========== +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Images +------ +* Elabore: `Icon `_. + +Contributors +------------ +* Stéphan Sainléger + +Funders +------- +The development of this module has been financially supported by: +* Elabore (https://elabore.coop) + +Maintainer +---------- +This module is maintained by ELABORE. + +""", + "depends": ["base", "product_rental_bookings"], + "data": [ + "views/product_template.xml", + "views/website_templates.xml", + ], + "qweb": [], +} diff --git a/product_rental_web_bookings/controllers/__init__.py b/product_rental_web_bookings/controllers/__init__.py new file mode 100755 index 0000000..f64f2ce --- /dev/null +++ b/product_rental_web_bookings/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import website_rental +from . import customer_portal diff --git a/product_rental_web_bookings/controllers/customer_portal.py b/product_rental_web_bookings/controllers/customer_portal.py new file mode 100644 index 0000000..43c1c0a --- /dev/null +++ b/product_rental_web_bookings/controllers/customer_portal.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +from odoo import http +from odoo.addons.portal.controllers.portal import CustomerPortal +from odoo.http import request + + +class CustomerPortal(CustomerPortal): + def _prepare_portal_layout_values(self): + values = super(CustomerPortal, self)._prepare_portal_layout_values() + rental_order_detail = request.env["rental.product.order"].sudo() + rental_order_count = rental_order_detail.search_count( + [("customer_name", "=", request.env.user.partner_id.id)] + ) + values.update( + { + "rental_order_count": rental_order_count, + } + ) + return values + + @http.route("/order", type="http", auth="public", website=True) + def order(self, page=1, date_begin=None, date_end=None, sortby=None): + values = self._prepare_portal_layout_values() + order_id = ( + request.env["rental.product.order"] + .sudo() + .search([("customer_name", "=", request.env.user.partner_id.id)]) + ) + searchbar_sortings = { + "date": {"label": ("Start Date"), "order": "from_date desc"}, + "date1": {"label": ("End Date"), "order": "to_date desc"}, + "stage": {"label": ("Stage"), "order": "state"}, + } + # default sortby order + if not sortby: + sortby = "date" + sort_order = searchbar_sortings[sortby]["order"] + values.update( + { + "order_id": order_id, + "page_name": "rental_order", + "searchbar_sortings": searchbar_sortings, + "sortby": sortby, + } + ) + return request.render("product_rental_bookings.rental_order", values) diff --git a/product_rental_web_bookings/controllers/website_rental.py b/product_rental_web_bookings/controllers/website_rental.py new file mode 100755 index 0000000..5321c88 --- /dev/null +++ b/product_rental_web_bookings/controllers/website_rental.py @@ -0,0 +1,566 @@ +# -*- coding: utf-8 -*- + +import json +from datetime import datetime + +from odoo import http +from odoo.addons.payment.controllers.portal import PaymentProcessing +from odoo.http import request +from odoo.osv import expression + + +class WebsiteRental(http.Controller): + @http.route("/search-product", type="http", auth="public", website=True) + def search_product(self, **post): + return request.render( + "product_rental_bookings.product_search", {"post_data": post} + ) + + def convert_float_to_hh_mm(self, session_id): + start_date = "{0:02.0f}:{1:02.0f}".format( + *divmod(session_id.start_time * 60, 60) + ) + end_date = "{0:02.0f}:{1:02.0f}".format(*divmod(session_id.end_time * 60, 60)) + start_date_fmt = ( + start_date.split(":")[0] + ":" + start_date.split(":")[1] + ":" + "00" + ) + end_date_fmt = ( + end_date.split(":")[0] + ":" + end_date.split(":")[1] + ":" + "00" + ) + return start_date_fmt, end_date_fmt + + @http.route("/booking_form", type="http", auth="public", website=True) + def booking_form(self, **post): + product_list = [] + if ( + post + and post.get("based_on") + and post.get("class_type") + and post["location"] + ): + if post.get("based_on") == "per_session": + rent_session_id = ( + request.env["session.config"] + .sudo() + .browse(int(post.get("session_type"))) + ) + request.session["session_type"] = rent_session_id.name + request.session["session_type_id"] = rent_session_id.id + start_date, end_date = self.convert_float_to_hh_mm(rent_session_id) + date_from = datetime.strptime( + post["date_from"] + " " + start_date, "%d-%m-%Y %H:%M:%S" + ) + date_to = datetime.strptime( + post["date_from"] + " " + end_date, "%d-%m-%Y %H:%M:%S" + ) + else: + date_format = ( + "%d-%m-%Y" + if post["date_from"] and post["date_to"] + else "%d-%m-%Y %H:%M" + ) + date_from = datetime.strptime( + post["date_from"] if post["date_from"] else post["datetime_from"], + date_format, + ) + date_to = datetime.strptime( + post["date_to"] + " 23:59:59" + if post["date_to"] + else post["datetime_to"] + ":00", + "%d-%m-%Y %H:%M:%S", + ) + request.session["date_from"] = date_from + request.session["date_to"] = date_to + request.session["location"] = post["location"] + request.session["class_type"] = post["class_type"] + request.session["based_on"] = post["based_on"] + categ_ids = ( + request.env["product.category"] + .sudo() + .search([("parent_id", "child_of", int(post["class_type"]))]) + ) + request.env.cr.execute( + """ + SELECT + propro.id + FROM product_product propro + JOIN product_template protmp ON protmp.id = propro.product_tmpl_id + WHERE + protmp.categ_id IN %s AND + propro.is_rental='t' AND + propro.id IN ( + Select product_id from stock_quant where location_id = %s); + """, + ( + tuple(categ_ids.ids), + post["location"], + ), + ) + product_data = request.env.cr.fetchall() + if product_data: + for products in product_data: + product = ( + request.env["product.product"] + .sudo() + .search([("id", "in", products)]) + ) + available_products = ( + request.env["stock.quant"] + .sudo() + .search( + [ + ("product_id", "=", product.id), + ("quantity", ">", 0), + ("location_id", "=", int(post.get("location"))), + ] + ) + ) + if available_products.product_id and product.is_published: + product_list.append(available_products.product_id) + values = { + "product_id": product_list, + "location": post["location"], + "post_data": post, + } + return request.render("product_rental_bookings.product_search", values) + else: + return request.render( + "product_rental_bookings.product_search", + {"post_data": post, "error": True}, + ) + else: + return request.render( + "product_rental_bookings.product_search", {"post_data": post} + ) + + @http.route( + '/booking_form/create_quotation/', + type="http", + auth="public", + website=True, + ) + def create_qutation(self, product_id, **post): + product_order = { + "product_id": product_id, + "post_data": request.session["location"], + } + if request.session.get("based_on") in ["per_hour", "per_session"]: + delta = request.session.get("date_from") - request.session.get("date_to") + hours = abs(delta.total_seconds() / 3600.0) + product_order.update({"total_hours": hours}) + return request.render("product_rental_bookings.rental_product", product_order) + + @http.route("/product_cart_update", type="http", auth="public", website=True) + def product_cart_update(self, **post): + product_order_list = [] + rental_order_obj = request.env["rental.product.order"] + order_id = rental_order_obj.search( + [ + ("customer_name", "=", request.env.user.partner_id.id), + ("state", "=", "draft"), + ], + limit=1, + ) + if post and post["product_id"]: + product_id = ( + request.env["product.product"] + .sudo() + .search([("id", "=", post["product_id"])]) + ) + enter_hour = post.get("enter_hour") + if not enter_hour: + total_day = ( + request.session.get("date_to") - request.session.get("date_from") + ).days + product_order_list.append( + ( + 0, + 0, + { + "product_id": product_id.id, + "price_based": "per_day", + "enter_days": total_day + 1, + "enter_hour": 0.0, + "qty_needed": int(post["qty_needed"]), + "price": product_id.rental_amount, + }, + ) + ) + elif enter_hour and request.session["based_on"] == "per_session": + product_order_list.append( + ( + 0, + 0, + { + "product_id": product_id.id, + "price_based": request.session.get("based_on"), + "enter_days": 0.0, + "enter_hour": float(post["enter_hour"]), + "qty_needed": int(post["qty_needed"]), + "price": product_id.rental_amount_per_session, + }, + ) + ) + else: + product_order_list.append( + ( + 0, + 0, + { + "product_id": product_id.id, + "price_based": request.session.get("based_on"), + "enter_days": 0.0, + "enter_hour": float(post["enter_hour"]), + "qty_needed": int(post["qty_needed"]), + "price": product_id.rental_amount_per_hour, + }, + ) + ) + + if order_id: + order_lines_product = [] + for order_line in order_id.product_order_lines_ids: + order_lines_product.append(order_line.product_id.id) + order_id.write( + ( + { + "product_order_lines_ids": product_order_list, + "is_hours": True + if request.session.get("based_on") + in ["per_session", "per_hour"] + else order_id.is_hours, + "is_days": True + if request.session.get("based_on") == "per_day" + else order_id.is_days, + } + ) + ) + return request.render( + "product_rental_bookings.rental_product_cart_update", + { + "product_order_id": order_id, + "location_id": request.session["location"], + }, + ) + else: + from_date, to_date = rental_order_obj.sudo().start_end_date_global( + request.session.get("date_from"), request.session.get("date_to") + ) + order_id = rental_order_obj.sudo().create( + { + "customer_name": request.env.user.partner_id.id, + "from_date": rental_order_obj.sudo().convert_TZ_UTC(from_date), + "to_date": rental_order_obj.sudo().convert_TZ_UTC(to_date), + "state": "draft", + "is_true": True, + "product_order_lines_ids": product_order_list, + "location_id": request.session["location"], + "is_hours": True + if request.session.get("based_on") + in ["per_session", "per_hour"] + else order_id.is_hours, + "is_days": True + if request.session.get("based_on") == "per_day" + else order_id.is_days, + } + ) + order_id.product_order_lines_ids._get_subtotal() + request.session["order_id"] = order_id.id + return request.render( + "product_rental_bookings.rental_product_cart_update", + {"product_order_id": order_id, "location_id": request.session["location"]}, + ) + + @http.route("/view_cart", type="http", auth="public", website=True) + def view_cart(self, **post): + rental_product_order_obj = request.env["rental.product.order"] + if post: + order_id = rental_product_order_obj.sudo().search( + [("id", "=", post["rental_order"])] + ) + else: + order_id = rental_product_order_obj.sudo().search( + [("id", "=", request.session.get("order_id"))] + ) + request.session["order_id"] = order_id.id + return request.render( + "product_rental_bookings.rental_product_cart_update", + {"product_order_id": order_id}, + ) + + def _get_rental_payment_values(self, order, **kwargs): + values = dict( + website_rental_order=order, + errors=[], + partner=order.customer_name.id, + order=order, + payment_action_id=request.env.ref("payment.action_payment_acquirer").id, + return_url="/rental/payment/validate", + bootstrap_formatting=True, + ) + + domain = expression.AND( + [ + [ + "&", + ("state", "in", ["enabled", "test"]), + ("company_id", "=", order.company_id.id), + ], + [ + "|", + ("country_ids", "=", False), + ("country_ids", "in", [order.customer_name.country_id.id]), + ], + ] + ) + acquirers = request.env["payment.acquirer"].search(domain) + values["acquirers"] = [ + acq + for acq in acquirers + if (acq.payment_flow == "form" and acq.view_template_id) + or (acq.payment_flow == "s2s" and acq.registration_view_template_id) + ] + values["tokens"] = request.env["payment.token"].search( + [ + ("partner_id", "=", order.customer_name.id), + ("acquirer_id", "in", acquirers.ids), + ] + ) + + return values + + @http.route( + "/rental/payment/validate", type="http", auth="public", website=True, csrf=False + ) + def payment_validate(self, transaction_id=None, rental_order_id=None, **post): + """Method that should be called by the server when receiving an update + for a transaction. State at this point : + - UDPATE ME + """ + # vals = {} + outstanding_info = False + order = ( + request.env["rental.product.order"] + .sudo() + .browse(request.session.get("order_id")) + ) + if order: + if not order.invoice_ids: + order.confirm() + invoice_id = order.contract_ids[0].create_invoice() + order.contract_ids[0].create_invoice() + invoices = order.mapped("invoice_ids").filtered( + lambda inv: inv.state == "posted" + ) + for inv in invoices: + if inv.invoice_has_outstanding: + outstanding_info = json.loads( + inv.invoice_outstanding_credits_debits_widget + ) + transaction_id = request.env["payment.transaction"].browse( + request.session.get("__website_sale_last_tx_id") + ) + if outstanding_info: + credit_aml_id = False + if "content" in outstanding_info: + for item in outstanding_info["content"]: + credit_aml_id = outstanding_info["content"][0]["id"] + if credit_aml_id and inv.state == "posted": + inv.js_assign_outstanding_line(credit_aml_id) + assert order.id == request.session.get("order_id") + return request.render( + "product_rental_bookings.rental_order_comfirmation", {"order": order} + ) + + @http.route( + [ + "/rental/payment/transaction/", + "/rental/payment/transaction/", + "/rental/payment/transaction//", + ], + type="json", + auth="public", + website=True, + ) + def payment_transaction( + self, + acquirer_id, + save_token=False, + so_id=None, + access_token=None, + token=None, + **kwargs + ): + """Json method that creates a payment.transaction, used to create a + transaction when the user clicks on 'pay now' button. After having + created the transaction, the event continues and the user is redirected + to the acquirer website. + + :param int acquirer_id: id of a payment.acquirer record. If not set the + user is redirected to the checkout page + """ + # Ensure a payment acquirer is selected + if not acquirer_id: + return False + try: + acquirer_id = int(acquirer_id) + except: + return False + # Retrieve the sale order + if so_id: + env = request.env["rental.product.order"] + domain = [("id", "=", so_id)] + if access_token: + env = env.sudo() + domain.append(("access_token", "=", access_token)) + order = env.search(domain, limit=1) + else: + order = ( + request.env["rental.product.order"] + .sudo() + .browse(request.session.get("order_id")) + ) + # Ensure there is something to proceed + assert order.customer_name.id != request.website.partner_id.id + # Create transaction + vals = { + "acquirer_id": acquirer_id, + "return_url": "/rental/payment/validate", + "currency_id": request.env.user.company_id.currency_id.id, + "amount": order.total_amount, + "partner_id": order.customer_name.id, + } + if save_token: + vals["type"] = "form_save" + if token: + vals["payment_token_id"] = int(token) + transaction = order._create_payment_transaction(vals) + last_tx_id = request.session.get("__website_sale_last_tx_id") + PaymentProcessing.add_payment_transaction(transaction) + request.session["__website_sale_last_tx_id"] = transaction.id + return transaction.render_rental_button(order) + + @http.route("/checkout", type="http", auth="public", website=True, csrf=False) + def checkout(self, **post): + rental_order_id = ( + request.env["rental.product.order"] + .sudo() + .search([("id", "=", request.session.get("order_id"))]) + ) + render_values = self._get_rental_payment_values(rental_order_id, **post) + if render_values["errors"]: + render_values.pop("acquirers", "") + render_values.pop("tokens", "") + return request.render("product_rental_bookings.checkout_process", render_values) + + @http.route("/get_rate_details", type="json", auth="public", website=True) + def get_rate_details(self, units, product_id): + + total_day = ( + request.session.get("date_to").date() + - request.session.get("date_from").date() + ).days + product_id = ( + request.env["product.product"].sudo().search([("id", "=", product_id)]) + ) + if units == "per_day": + product_details = { + "rate": product_id.rental_amount, + "total_days": total_day + 1, + "from_date": request.session.get("date_from"), + "to_date": request.session.get("date_to"), + } + return product_details + else: + product_details = { + "rate": product_id.rental_amount_per_hour + if units == "per_hour" + else product_id.rental_amount_per_session, + "from_date": request.session.get("date_from"), + "to_date": request.session.get("date_to"), + } + return product_details + + @http.route("/product_ordel_line/remove", type="json", auth="user") + def remove_line(self, order_line_id, **kwrgs): + line_id = request.env["product.order.line"].browse(int(order_line_id)) + order_id = line_id.product_order_id + line_id.sudo().unlink() + if not any(order_id.product_order_lines_ids): + order_id.sudo().unlink() + return {"order_id": order_id} + + @http.route("/check/quantity", type="json", auth="public") + def quantity_check(self, quantity, product_id): + Product_obj = request.env["product.product"] + order_line_obj = request.env["product.order.line"] + product = Product_obj.sudo().search([("id", "=", product_id)]) + rental_order_obj = request.env["rental.product.order"] + from_date, to_date = rental_order_obj.sudo().start_end_date_global( + request.session.get("date_from"), request.session.get("date_to") + ) + start_date = datetime.strptime( + rental_order_obj.sudo().convert_TZ_UTC(from_date) + if request.env.user.tz + else from_date, + "%Y-%m-%d %H:%M:%S", + ) + end_date = datetime.strptime( + rental_order_obj.sudo().convert_TZ_UTC(to_date) + if request.env.user.tz + else to_date, + "%Y-%m-%d %H:%M:%S", + ) + order_line_ids = order_line_obj.sudo().search( + [ + ("product_order_id.state", "=", "confirm"), + ("product_id", "=", product_id), + ] + ) + total_in_order_qty = 0 + for order_line in order_line_ids: + if ( + (start_date <= order_line.product_order_id.from_date <= end_date) + or (start_date <= order_line.product_order_id.to_date <= end_date) + and order_line.product_order_id.picking_count == 1 + ): + total_in_order_qty += ( + order_line.qty_needed + if order_line.product_order_id.picking_count == 1 + else 0 + ) + elif ( + ( + order_line.product_order_id.from_date + <= start_date + <= order_line.product_order_id.to_date + ) + or ( + order_line.product_order_id.from_date + <= end_date + <= order_line.product_order_id.to_date + ) + and order_line.product_order_id.picking_count == 1 + ): + total_in_order_qty += ( + order_line.qty_needed + if order_line.product_order_id.picking_count == 1 + else 0 + ) + if product.sudo().qty_available and total_in_order_qty: + qty_available = product.sudo().qty_available - total_in_order_qty + else: + total_in_order_qty = order_line_ids.filtered( + lambda x: x.qty_needed if x.product_order_id.picking_count > 1 else 0 + ) + qty_available = product.sudo().qty_available + sum( + total_in_order_qty.mapped("qty_needed") + ) + total_price_days = product.rental_amount + total_price_hour = product.rental_amount_per_hour + if not quantity or not qty_available >= quantity: + return False + return { + "product_price_days": total_price_days, + "product_price_hour": total_price_hour, + } diff --git a/product_rental_web_bookings/static/src/css/index.css b/product_rental_web_bookings/static/src/css/index.css new file mode 100755 index 0000000..6d0a505 --- /dev/null +++ b/product_rental_web_bookings/static/src/css/index.css @@ -0,0 +1,55 @@ +.search-box{width:50%;background:#000000a3;border-radius:30px;margin:0 auto;margin-top:50px;margin-bottom:50px;padding:10px;font-size:16px;color:#fff;border: 3px solid; +border-color: black;} +.top-deals{padding:40px 0;background:#ededed} +.top-deals .col-xs-12{font-family:'Roboto',sans-serif;font-weight:700;font-size:40px;line-height:60px;color:#191919} +.top-deals .rline{width:100px;display:block;height:5px;background:#d42316;margin:0 auto;margin-top:20px} +.top-deals .box{background:#4ebab1;padding-bottom:150px;margin:0 auto;max-width:450px;margin-top:40px;-moz-box-shadow:1px 1px 5px #ccc;-webkit-box-shadow:1px 1px 5px #ccc;box-shadow:1px 1px 5px #ccc} +.top-deals .box .box-town-image{background:#4ebab1;width:50%;height:20px;} +.top-deals .box img{height:50%;width:50%;object-fit:cover} +.image{overflow:hidden;position:relative} +.heading-font{color:#fff;font-size:12px;line-height:18px;text-transform:uppercase;margin-top:65px;} +.heading-font{letter-spacing:0 !important}.text-transform{text-transform:uppercase} +.heading-font{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}ul{padding-left:20px}ul li{margin-bottom:8px}ol{padding-left:20px}ol li{margin-bottom:8px}ul.header-menu ul{margin-top:0}ul ol,ol ol,ol ul,ul ul{margin-top:8px}img{max-width:100%;height:auto}a{color:#6c98e1}a:hover{color:#6c98e1}a:focus{color:#6c98e1}a:active{color:#6c98e1}} +.image{overflow:hidden;position:relative}.rmv_txt_drctn:hover .image:before{opacity:1} +.special-offers .title{position:relative;padding:0 130px;margin-bottom:16px;text-align:center;font-weight:700;font-size:36px;line-height:normal;color:#fff} +.listing-car-items-units{margin:0 -15px}.listing-car-items .listing-car-item{padding:10px 15px} +.listing-car-items .listing-car-item .listing-car-item-inner{max-width:350px;width:auto;margin-bottom:30px;} +.listing-car-items .listing-car-item .car-title{margin-top:40px;color:#fff;overflow:hidden;text-transform:uppercase;font-size:14px;line-height:18px;font-weight:700;text-decoration:none;text-align:left} +.listing-car-items .listing-car-item .car-title .colored{color:#cc6119} +.listing-car-items .listing-car-item .price{position:relative;float:right;padding:6px 15px 5px 18px;margin-left:23px;background-color:#6c98e1;text-align:right} +.listing-car-items .listing-car-item .price .regular-price,.listing-car-items .listing-car-item .price .sale-price, +.listing-car-items .listing-car-item .price .normal-price{position:relative;z-index:6}.listing-car-items .listing-car-item .price .regular-price{font-size:13px;line-height:normal;text-decoration:line-through} +.listing-car-items .listing-car-item .price .normal-price,.listing-car-items .listing-car-item .price .sale-price{color:#fff;font-size:16px;line-height:20px;font-weight:700}.car-listing-row +.listing-car-item-meta .price .normal-price,.car-listing-row +.listing-car-item-meta .price .sale-price{color:#fff;font-size:16px;line-height:20px;font-weight:700} +.listing-car-items .listing-car-item .price .normal-price{line-height:36px}.listing-car-items .listing-car-item .price:before{position:absolute;content:'';display:block;top:0;left:-14px;height:100%;width:32px;background-color:#6c98e1;-webkit-transform:skew(-18deg,0deg);transform:skew(-18deg,0deg);z-index:5} +.listing-car-items .listing-car-item .listing-car-item-meta{border-radius: 15px;padding:3px 0px 19px 1px;width: 900px;background-color:#373c3f;text-align:left;vertical-align: middle;} +.title{margin-bottom:10px;font-weight:700;text-transform:uppercase;font-size:14px;line-height:10px} +.title{text-transform:uppercase;border-bottom:1px solid #ddd} +.title{font-size:16px;font-weight:400;color:#232628} +.title{position:relative;min-height:42px;text-transform:uppercase} +.car-meta-top{min-height:9px;padding-bottom:9px;border-bottom:1px solid #4e5254}.listing-car-items .listing-car-item +.listing-car-item-meta .car-meta-bottom ul{list-style:none;padding:0;margin:0;border-radius:37px} +.listing-car-items .listing-car-item .listing-car-item-meta .car-meta-bottom ul li{display:inline-block;margin-top:18px;margin-right:16px;margin-bottom:0;color:#c9c9c9;vertical-align:middle;line-height:13px} +.dp-in{display:inline-block;vertical-align:top;max-width:100%}.car-listing-tabs-unit .stm-listing-tabs{margin-top:20px}.car-listing-tabs-unit .car-listing-top-part{position:relative;}.car-listing-tabs-unit .car-listing-top-part:before{position:absolute;content:'';display:block;top:0;bottom:0;left:-2000px;right:-2000px;z-index:-1} +.stm-base-background-color{background-color:#cc6119 !important}.stm-base-color{color:#cc6119 !important} +.stm-image-links .stm-image-link .inner img{filter:grayscale(0%) !important;-webkit-filter:grayscale(0%) !important}img{-webkit-transform:translateZ(0)}.testimonials-carousel .owl-controls{display:none}.testimonial-unit +.image{margin-right:20px !important}.wpb_column . +.widget_car_location #stm-dealer-gmap{width:100%;height:253px}@media(max-width:440px){} +.vc_custom_1448532088457{margin-bottom: 0px !important;padding-top: 51px !important;padding-bottom: 40px !important;background-position: 0 0 !important;background-repeat: repeat !important;} +.car-listing-tabs-unit .car-listing-top-part:before {background-color: #232628} +.font-color{font-size:16px;font-weight:400;color:#000000} +.select{max-width:100px;} +.css_quantity { + max-width: 125px; +}.css_quantity input[name="add_qty"] { + text-align: center; +} +.kilometer{max-width:70px;} +.search_product{margin-top:8px; padding: 0 7px;} +.scroll_down { + width: 100%; + height: 300px; +} + + diff --git a/product_rental_web_bookings/static/src/js/main.js b/product_rental_web_bookings/static/src/js/main.js new file mode 100755 index 0000000..91625b9 --- /dev/null +++ b/product_rental_web_bookings/static/src/js/main.js @@ -0,0 +1,228 @@ +odoo.define('product_rental_bookings.main', function (require) { + "use strict"; + var ajax = require('web.ajax'); + + $(document).ready(function () { + + $('.total_days').hide(); + var error = $('#error').val(); + if (error == "True") { + $("#myModal").modal(); + } + + $('#date_from').datepicker({ + minDate: new Date(), + dateFormat: "dd-mm-yy", + yearRange: new Date().getFullYear().toString() + ':' + new Date().getFullYear().toString(), + onClose: function (selectedDate) { $("#date_to").datepicker("option", "minDate", selectedDate); } + }); + + $('#date_to').datepicker({ + dateFormat: "dd-mm-yy", + yearRange: new Date().getFullYear().toString() + ':' + new Date().getFullYear().toString(), + onClose: function (selectedDate) { $("#date_from").datepicker("option", "maxDate", selectedDate); } + }); + + $('#datetime_from').datetimepicker({ + minDate: new Date(), + format: 'd-m-Y H:i', + onClose: function (selectedDate) { + $("#datetime_to").datetimepicker("option", "minDateTime", selectedDate); + } + }); + + $('#datetime_to').datetimepicker({ + minDate: new Date(), + format: 'd-m-Y H:i', + onClose: function (selectedDate) { + $("#datetime_from").datetimepicker("option", "maxDateTime", selectedDate); + } + }); + + $('#datetime_from').on('change', function () { + var date_from = $('#datetime_from').val() + var date_to = $('#datetime_to').val() + if (date_from && date_to && date_from > date_to) { + alert('From Datetime is after To Datetime') + $('#datetime_from').val('') + } + }); + + $('#datetime_to').on('change', function () { + var date_from = $('#datetime_from').val() + var date_to = $('#datetime_to').val() + if (date_from && date_to && date_from > date_to) { + alert('To Datetime is before From Datetime') + $('#datetime_to').val('') + } + }); + + $('.rate_option').click(function () { + var select_value = $(this).val(); + var qty = $(this).parent().parent().find('.qty').val() + var hour = $(this).parent().parent().find('.enter_hour').val() + var day = $(this).parent().parent().find('.total_day').val() + ajax.jsonRpc('/get_rate_details', 'call', { + 'product_id': $(document).find('.product_id').val(), + 'units': $(this).val(), + }).then(function (product_details) { + var rate = parseInt(product_details['rate']) + var total_day = parseInt(product_details['total_days']) + var from_date = (product_details['from_date']) + var to_date = (product_details['to_date']) + if (select_value == 'per_day') { + var html = ''; + var day = ''; + $('.total_hour').hide(); + $('.enter_hour').hide(); + $('.total_hours').hide(); + $('.total_day').show(); + $('.total_days').show(); + html += "" + rate + ""; + day += "" + total_day + "" + from_date += "" + from_date + "" + to_date += "" + to_date + "" + $('.rate_value').html(html); + $('.total_day').html(day) + $('.rate_value').addClass('rate_cls'); + $('.total_day').addClass('day_cls'); + $('.date_from').html(from_date) + $('.date_to').html(to_date) + calculate_total(rate * total_day); + $($('.calculate_total')).show() + $("input[name='qty_needed']").val(1) + } + else if (select_value == 'per_hour') { + $('.total_hour').show(); + $('.enter_hour').show(); + $('.total_hours').show(); + $('.total_day').hide(); + $('.total_days').hide(); + calculate_total(rate * hour); + $($('.calculate_total')).show() + $("input[name='qty_needed']").val(1) + } + else if (select_value == 'per_session') { + $('.total_hour').show(); + $('.enter_hour').show(); + $('.total_hours').show(); + $('.total_day').hide(); + $('.total_days').hide(); + calculate_total(rate); + $($('.calculate_total')).show() + $("input[name='qty_needed']").val(1) + } + + }); + }); + $(".rate_option").trigger("click"); + + $('.quantity').on('change', function () { + var numberPattern = /\d+/g; + var rate_value = parseInt($('.rate_cls').html().match(numberPattern)); + var hour = parseInt($('.enter_hour').val()); + var total_day = parseInt($('.day_cls').html().match(numberPattern)); + if ($(this).parent().parent().parent().find('.rate_option').val() == 'per_hour') { + calculate_total(rate_value * $(this).val() * hour); + } + + }); + + + // const total_amt = $('.calculate_total').html(); + $("input[name='qty_needed']").on('change', function (e) { + var quantity = e.currentTarget.value; + var product_id = $("input[name='product_id']").val(); + ajax.jsonRpc('/check/quantity', 'call', { + 'product_id': parseInt(product_id), + 'quantity': parseInt(quantity) + }).then(function (response) { + if (response == false) { + alert("Quantity Not Available.") + e.currentTarget.value = 1; + } + else { + if ($('.rate_option').val() == 'per_day') { + var total_day = ($('.total_day').html().replace('', '')).replace('', ''); + $('.calculate_total').html(total_day * response.product_price_days * parseInt(quantity)) + } + else { + var hour = $("input[name='enter_hour']").val() + $('.calculate_total').html(parseInt(hour) * response.product_price_hour * parseInt(quantity)) + } + } + if (e.currentTarget.value == 0) { + $(".js_check_product").attr("disabled", true); + } + else { + $(".js_check_product").attr("disabled", false); + } + }); + }); + + $('.js_delete_product').click(function () { + var ele = $(this) + var id = $(this).attr("id"); + ajax.jsonRpc('/product_ordel_line/remove', 'call', { + 'order_line_id': id, + }).then(function () { + ele.closest('tr').remove() + window.location.reload(); + }); + }); + + $('#based_on').click(function () { + var select_value = $(this).val(); + if (select_value == 'per_hour') { + $('#session_selection').hide(); + $('#for_hour_selection').show(); + $('#for_day_selection').hide(); + $("#datetime_from").prop("required", true); + $("#datetime_to").prop("required", true); + $("#date_from").prop("required", false); + $("#date_to").prop("required", false); + $("#session_type").prop("required", false); + $("#date_from").val(''); + $("#date_to").val(''); + } + else if (select_value == 'per_day') { + $('#session_selection').hide(); + $('#for_hour_selection').hide(); + $('#for_day_selection').show(); + $("#to_date").show(); + $("#date_from").prop("required", true); + $("#date_to").prop("required", true); + $("#datetime_from").prop("required", false); + $("#datetime_to").prop("required", false); + $("#session_type").prop("required", false); + $("#session_type").val(''); + $("#datetime_from").val(''); + $("#datetime_to").val(''); + } + else { + $('#session_selection').show(); + $('#for_hour_selection').hide(); + $('#for_day_selection').show(); + $("#date_from").prop("required", true); + $("#to_date").hide(); + $("#date_to").prop("required", false); + $("#datetime_from").prop("required", false); + $("#datetime_to").prop("required", false); + $("#datetime_from").val(''); + $("#datetime_to").val(''); + } + }); + }); + + function calculate_total(calculated_value) { + $('.calculate_total').html("" + calculated_value + ""); + } + +}); + + + + + + + diff --git a/product_rental_web_bookings/static/src/scss/website_sale_frontend.scss b/product_rental_web_bookings/static/src/scss/website_sale_frontend.scss new file mode 100644 index 0000000..8b13a0d --- /dev/null +++ b/product_rental_web_bookings/static/src/scss/website_sale_frontend.scss @@ -0,0 +1,140 @@ +//## Website Sale frontent design +//## ---------------------------- + +// Theming variables +$o-rental-wizard-thickness: 0.125rem; +$o-rental-wizard-dot-size: 0.625rem; +$o-rental-wizard-dot-active-glow: 0.25rem; + +$o-rental-wizard-color-inner: white; +$o-rental-wizard-color-default: gray('200'); + +$o-rental-wizard-dot-active: theme-color('primary'); +$o-rental-wizard-dot-completed: theme-color('success'); + +$o-rental-wizard-label-default: $text-muted; +$o-rental-wizard-label-active: $body-color; +$o-rental-wizard-label-completed: $success; + +.progress-rental-wizard { + // Scoped variables + $tmp-dot-radius: ($o-rental-wizard-dot-size + $o-rental-wizard-thickness)*0.5; + $tmp-check-size: max($font-size-base, $o-rental-wizard-dot-size + $o-rental-wizard-thickness + $o-rental-wizard-dot-active-glow*2); + $tmp-check-pos: $o-rental-wizard-dot-size*0.5 - $tmp-check-size*0.5; + + margin-top: $grid-gutter-width*0.5; + padding: 0 $grid-gutter-width*0.5; + + @include media-breakpoint-up(md) { + padding: 0; + } + + .progress-rental-wizard-step { + position: relative; + + @include media-breakpoint-up(md) { + margin-top: $tmp-dot-radius + $o-rental-wizard-thickness*3.5; + float: left; + width: percentage(1/3); + + .o_wizard_has_extra_step + & { + width: percentage(1/4); + } + } + @include media-breakpoint-down(sm) { + &.disabled, &.complete { + display:none; + } + } + .progress-rental-wizard-dot { + width: $o-rental-wizard-dot-size; + height: $o-rental-wizard-dot-size; + position: relative; + display: inline-block; + background-color: $o-rental-wizard-color-inner; + border-radius: 50%; + box-shadow: 0 0 0 $o-rental-wizard-thickness $o-rental-wizard-color-default; + + @include media-breakpoint-up(md) { + @include o-position-absolute($left: 50%); + margin: (-$tmp-dot-radius) 0 0 (-$o-rental-wizard-dot-size*0.5); + } + } + + .progress-rental-wizard-steplabel { + color: $o-rental-wizard-label-default; + margin: 5px 0 5px 5px; + font-size: $font-size-base; + display: inline-block; + + @include media-breakpoint-up(md) { + display: block; + margin: (0.625rem + $tmp-dot-radius) 0 20px 0; + } + @include media-breakpoint-down(sm) { + margin-left: -15px; + font-size: 24px; + } + } + + .progress-rental-wizard-bar { + height: $o-rental-wizard-thickness; + background-color: $o-rental-wizard-color-default; + } + + &.active { + .progress-rental-wizard-dot { + animation: fadeIn 1s ease 0s 1 normal none running; + background: $o-rental-wizard-dot-active; + box-shadow: 0 0 0 ($o-rental-wizard-dot-active-glow - 0.0625rem) $o-rental-wizard-color-inner, + 0 0 0 $o-rental-wizard-dot-active-glow rgba($o-rental-wizard-dot-active, 0.5); + } + + .progress-rental-wizard-steplabel { + color: $o-rental-wizard-label-active; + font-weight: bolder; + } + } + + &.complete { + .progress-rental-wizard-dot { + background: none; + box-shadow: none; + + &:after { + @include o-position-absolute($tmp-check-pos, $left: $tmp-check-pos); + width: $tmp-check-size; + height: $tmp-check-size; + border-radius: 100%; + + background: $o-rental-wizard-color-inner; + color: $o-rental-wizard-dot-completed; + text-align: center; + line-height: 1; + font-size: $tmp-check-size; + font-family: FontAwesome; + + content: "\f058"; + } + } + + .progress-rental-wizard-steplabel { + color: $o-rental-wizard-label-completed; + } + + &:hover:not(.disabled) { + .progress-rental-wizard-dot:after { + color: $o-rental-wizard-label-completed; + } + + .progress-rental-wizard-steplabel { + color: $o-rental-wizard-label-active; + } + } + } + + &.disabled { + cursor: default; + } + } +} diff --git a/product_rental_web_bookings/views/product_template.xml b/product_rental_web_bookings/views/product_template.xml new file mode 100644 index 0000000..ae560a2 --- /dev/null +++ b/product_rental_web_bookings/views/product_template.xml @@ -0,0 +1,800 @@ + + + +