[NEW] Addons creation - product_rental_bookings
This commit is contained in:
BIN
product_rental_bookings/.DS_Store
vendored
Normal file
BIN
product_rental_bookings/.DS_Store
vendored
Normal file
Binary file not shown.
4
product_rental_bookings/__init__.py
Executable file
4
product_rental_bookings/__init__.py
Executable file
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
81
product_rental_bookings/__manifest__.py
Executable file
81
product_rental_bookings/__manifest__.py
Executable file
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
"name": "Product Rental Bookings",
|
||||
"category": "Product",
|
||||
"version": "14.0.1.0",
|
||||
"summary": "Book products on several rental periods ",
|
||||
"author": "Elabore",
|
||||
"website": "https://elabore.coop/",
|
||||
"installable": True,
|
||||
"application": True,
|
||||
"auto_install": False,
|
||||
"description": """
|
||||
===========================
|
||||
Product Rental Web Bookings
|
||||
===========================
|
||||
This module allows Odoo Internal users to manage product rental stocks and booking calendar
|
||||
|
||||
Installation
|
||||
============
|
||||
Just install product_rental_web_bookings, all dependencies will be installed by default.
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
Bugs are tracked on `GitHub Issues
|
||||
<https://github.com/elabore-coop/.../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 <https://elabore.coop/web/image/res.company/1/logo?unique=f3db262>`_.
|
||||
|
||||
Contributors
|
||||
------------
|
||||
* Stéphan Sainléger <https://github.com/stephansainleger>
|
||||
|
||||
Funders
|
||||
-------
|
||||
The development of this module has been financially supported by:
|
||||
* Elabore (https://elabore.coop)
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
This module is maintained by ELABORE.
|
||||
|
||||
""",
|
||||
"depends": ["base", "account", "website_sale", "hr", "stock"],
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"security/security.xml",
|
||||
"views/assets.xml",
|
||||
"views/sequence.xml",
|
||||
"views/account_invoice_view.xml",
|
||||
"views/product_book.xml",
|
||||
"views/product_view.xml",
|
||||
"views/product_contract.xml",
|
||||
"wizard/advance_payment_invoice.xml",
|
||||
"views/product_operation.xml",
|
||||
"views/product_order.xml",
|
||||
"views/product_move.xml",
|
||||
"views/res_config_settings_view.xml",
|
||||
"views/stock_picking.xml",
|
||||
"report/rental_order.xml",
|
||||
"report/rental_order_report.xml",
|
||||
"report/rental_contract_report.xml",
|
||||
"report/rental_contract_recurring.xml",
|
||||
"data/data.xml",
|
||||
"views/session_config_settings_view.xml",
|
||||
"views/menus.xml",
|
||||
],
|
||||
"qweb": [
|
||||
"static/src/xml/delivery_sign.xml",
|
||||
"static/src/xml/product_booking_calender.xml",
|
||||
],
|
||||
}
|
84
product_rental_bookings/data/data.xml
Executable file
84
product_rental_bookings/data/data.xml
Executable file
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="extra_charge_product_id" model="product.product">
|
||||
<field name="name">Extra charge</field>
|
||||
<field name="type">service</field>
|
||||
</record>
|
||||
|
||||
<!-- <record id="rental_account_id" model="account.account">-->
|
||||
<!-- <field name="code">708000</field>-->
|
||||
<!-- <field name="name">Rental Order Account</field>-->
|
||||
<!-- <field name="user_type_id" ref="account.data_account_type_direct_costs"/>-->
|
||||
<!-- </record>-->
|
||||
|
||||
<record forcecreate="True" id="ir_cron_rental_contract_costs" model="ir.cron">
|
||||
<field name="name">Generate Rental contracts costs</field>
|
||||
<field name="model_id" ref="model_rental_product_contract" />
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.run_scheduler()</field>
|
||||
<field name="user_id" ref="base.user_root" />
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field eval="False" name="doall" />
|
||||
</record>
|
||||
|
||||
<record id="email_template_product_rental" model="mail.template">
|
||||
<field name="name">Product Order- Send by Email</field>
|
||||
<field name="email_from">
|
||||
${(object.create_uid.email and '%s <%s>' % (object.create_uid.name, object.create_uid.email)or
|
||||
'')|safe}>
|
||||
</field>
|
||||
<field name="subject">Order (Ref ${object.res_number or 'n/a' })</field>
|
||||
<field name="partner_to">${object.customer_name.id}</field>
|
||||
<field name="model_id" ref="product_rental_bookings.model_rental_product_order" />
|
||||
<field name="report_name">${(object.res_number)}</field>
|
||||
<field name="lang">${object.customer_name.lang}</field>
|
||||
<field name="body_html">
|
||||
<![CDATA[ <p>Dear ${object.customer_name.name},<br/>
|
||||
<br/><br/>
|
||||
<p>You can reply to this email if you have any questions.</p>
|
||||
<p>Thank you,</p>
|
||||
|
||||
<p style="color:#888888;">
|
||||
% if object.user_id and object.user_id.signature:
|
||||
${object.user_id.signature | safe}
|
||||
% endif
|
||||
</p>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="email_template_product_contract" model="mail.template">
|
||||
<field name="name">Product Coontract- Send by Email</field>
|
||||
<field name="email_from">
|
||||
${(object.create_uid.email and '%s <%s>' % (object.create_uid.name, object.create_uid.email)or
|
||||
'')|safe}>
|
||||
</field>
|
||||
<field name="subject">Order (Ref ${object.name or 'n/a' })</field>
|
||||
<field name="partner_to">${object.partner_id.id}</field>
|
||||
<field name="model_id" ref="product_rental_bookings.model_rental_product_contract" />
|
||||
<field name="report_template" ref="product_rental_bookings.product_rental_contract_report" />
|
||||
<field name="report_name">${(object.name)}</field>
|
||||
<field name="lang">${object.partner_id.lang}</field>
|
||||
<field name="body_html">
|
||||
<![CDATA[ <p>Dear ${object.partner_id.name},<br/><br/>
|
||||
<p>Your Rental contract has been created,Here we attached contract detail</p>
|
||||
<br/><br/>
|
||||
<p>You can reply to this email if you have any questions.</p>
|
||||
<p>Thank you,</p>
|
||||
|
||||
<p style="color:#888888;">
|
||||
% if object.user_id and object.user_id.signature:
|
||||
${object.user_id.signature | safe}
|
||||
% endif
|
||||
</p>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
|
||||
</odoo>
|
2587
product_rental_bookings/i18n/fr.po
Normal file
2587
product_rental_bookings/i18n/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
13
product_rental_bookings/models/__init__.py
Executable file
13
product_rental_bookings/models/__init__.py
Executable file
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import account_move
|
||||
from . import product
|
||||
from . import product_book
|
||||
from . import product_contract
|
||||
from . import product_operation
|
||||
from . import product_order
|
||||
from . import product_logs
|
||||
from . import res_config_settings
|
||||
from . import stock_move
|
||||
from . import stock_picking
|
||||
from . import session_config_settings
|
106
product_rental_bookings/models/account_move.py
Normal file
106
product_rental_bookings/models/account_move.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import models, fields
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountInvoice(models.Model):
|
||||
_inherit = "account.move"
|
||||
|
||||
rental_order_id = fields.Many2one("rental.product.order", string="Rental ref ")
|
||||
interval_type = fields.Selection(
|
||||
[("days", "Day"), ("weeks", "Week"), ("months", "Month")],
|
||||
string="Interval Type",
|
||||
)
|
||||
interval_number = fields.Integer(string="Interval Number", readonly=1)
|
||||
is_recurring = fields.Boolean(string="Recurring Invoice", default=False)
|
||||
contract_id = fields.Many2one("rental.product.contract", string="Contract")
|
||||
is_hours = fields.Boolean(string="Hours")
|
||||
is_days = fields.Boolean(string="Days")
|
||||
|
||||
def action_invoice_open(self):
|
||||
# lots of duplicate calls to action_invoice_open, so we remove those already open
|
||||
to_open_invoices = self.filtered(lambda inv: inv.state != "open")
|
||||
if to_open_invoices.filtered(lambda inv: inv.state != "draft"):
|
||||
raise UserError(
|
||||
_("Invoice must be in draft state in order to validate it.")
|
||||
)
|
||||
if to_open_invoices.filtered(lambda inv: inv.amount_total < 0):
|
||||
raise UserError(
|
||||
_(
|
||||
"You cannot validate an invoice with a negative total amount. You should create a credit note instead."
|
||||
)
|
||||
)
|
||||
to_open_invoices.action_date_assign()
|
||||
to_open_invoices.action_move_create()
|
||||
if (
|
||||
self.interval_number > 0
|
||||
and self.is_recuuring == False
|
||||
and self.date_due >= datetime.now().strftime("%y-%m-%d-%H-%M")
|
||||
):
|
||||
self.is_recuuring = True
|
||||
sub_name = (
|
||||
str(self.number)
|
||||
+ str("-" + self.interval_type if self.interval_type else "")
|
||||
+ "-"
|
||||
+ datetime.now().strftime("%Y-%m-%d")
|
||||
)
|
||||
model_id = self.env["ir.model"].search([("model", "=", self._name)])
|
||||
|
||||
subscription_document_id = self.env["subscription.document"].search(
|
||||
[("name", "=", "Account Invoice"), ("model", "=", model_id.id)], limit=1
|
||||
)
|
||||
if not subscription_document_id:
|
||||
subscription_document_id = self.env["subscription.document"].create(
|
||||
{"name": "Account Invoice", "model": model_id.id}
|
||||
)
|
||||
subscription_doc_source = (
|
||||
str(subscription_document_id.model.model) + "," + str(self.id)
|
||||
)
|
||||
subscription_id = self.env["subscription.subscription"].create(
|
||||
{
|
||||
"name": sub_name,
|
||||
"partner_id": self.partner_id.id,
|
||||
"interval_number": self.interval_number,
|
||||
"interval_type": self.interval_type,
|
||||
"doc_source": subscription_doc_source,
|
||||
}
|
||||
)
|
||||
subscription_id.set_process()
|
||||
return to_open_invoices.action_invoice_open()
|
||||
|
||||
|
||||
class AccountInvoiceLine(models.Model):
|
||||
_inherit = "account.move.line"
|
||||
|
||||
product_id = fields.Many2one("product.product", string="Product ID")
|
||||
enter_hour = fields.Float(string="Hour")
|
||||
enter_days = fields.Float(string="Days")
|
||||
|
||||
def _get_computed_price_unit(self):
|
||||
self.ensure_one()
|
||||
|
||||
if not self.product_id:
|
||||
return self.price_unit
|
||||
elif self.move_id.is_sale_document(include_receipts=True):
|
||||
# Out invoice.
|
||||
price_unit = self.product_id.lst_price
|
||||
elif self.move_id.is_purchase_document(include_receipts=True):
|
||||
# In invoice.
|
||||
price_unit = self.product_id.standard_price
|
||||
else:
|
||||
return self.price_unit
|
||||
|
||||
if self.product_uom_id != self.product_id.uom_id:
|
||||
price_unit = self.product_id.uom_id._compute_price(
|
||||
price_unit, self.product_uom_id
|
||||
)
|
||||
|
||||
if self.enter_days:
|
||||
price_unit = price_unit * self.enter_days
|
||||
elif self.enter_hour:
|
||||
price_unit = price_unit * self.enter_hour
|
||||
|
||||
return price_unit
|
15
product_rental_bookings/models/product.py
Executable file
15
product_rental_bookings/models/product.py
Executable file
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = "product.product"
|
||||
|
||||
is_rental = fields.Boolean(string="Rental", default=False)
|
||||
selected_product = fields.Boolean(string="Select", default=False)
|
||||
rental_amount = fields.Float(string="Rent Per Day Rate")
|
||||
rental_amount_per_hour = fields.Float(string="Rent Per Hour Rate")
|
||||
rental_amount_per_session = fields.Float(string="Rent Per Session Rate")
|
||||
product_registration_id = fields.Many2one("rental.product.order", string="Order")
|
||||
rental_qyt = fields.Float("Quantity", default=1.0)
|
368
product_rental_bookings/models/product_book.py
Executable file
368
product_rental_bookings/models/product_book.py
Executable file
@@ -0,0 +1,368 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime, date
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError, AccessError
|
||||
|
||||
|
||||
class ProductBooking(models.TransientModel):
|
||||
_name = "product.booking"
|
||||
_rec_name = "book_number"
|
||||
_description = "Book"
|
||||
|
||||
@api.depends(
|
||||
"product_line_ids",
|
||||
"total_days",
|
||||
"extra_charges",
|
||||
"price_based",
|
||||
"from_date",
|
||||
"to_date",
|
||||
)
|
||||
def compute_rate(self):
|
||||
for record in self:
|
||||
record.total = 0
|
||||
record.sub_total = 0
|
||||
record.total_days = 0
|
||||
record.total_hours = 0
|
||||
if record.from_date and record.to_date:
|
||||
date_from = datetime.strptime(
|
||||
str(record.from_date), "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
date_to = datetime.strptime(str(record.to_date), "%Y-%m-%d %H:%M:%S")
|
||||
delta = date_from - date_to
|
||||
record.total_days = abs(delta.days)
|
||||
record.total_hours = abs(delta.total_seconds() / 3600.0)
|
||||
sub_total = 0.0
|
||||
with_tax_total = 0.0
|
||||
for product in record.product_line_ids.filtered(
|
||||
lambda fv: fv.selected_product
|
||||
):
|
||||
if record.price_based == "per_day":
|
||||
taxes = product.taxes_id.compute_all(
|
||||
product.rental_qyt,
|
||||
product.currency_id,
|
||||
product.rental_amount * record.total_days,
|
||||
)
|
||||
sub_total += taxes["total_excluded"]
|
||||
with_tax_total += taxes["total_included"]
|
||||
elif record.price_based == "per_hour":
|
||||
taxes = product.taxes_id.compute_all(
|
||||
product.rental_qyt,
|
||||
product.currency_id,
|
||||
product.rental_amount_per_hour * record.total_hours,
|
||||
)
|
||||
sub_total += taxes["total_excluded"]
|
||||
with_tax_total += taxes["total_included"]
|
||||
else:
|
||||
taxes = product.taxes_id.compute_all(
|
||||
product.rental_qyt,
|
||||
product.currency_id,
|
||||
product.rental_amount_per_session,
|
||||
)
|
||||
sub_total += taxes["total_excluded"]
|
||||
with_tax_total += taxes["total_included"]
|
||||
record.sub_total = sub_total
|
||||
record.total = with_tax_total + record.extra_charges
|
||||
|
||||
@api.model
|
||||
def _get_based_on_selections(self):
|
||||
enabled_day_rent = (
|
||||
self.env["ir.config_parameter"].sudo().get_param("enabled_day_rent")
|
||||
)
|
||||
enabled_hour_rent = (
|
||||
self.env["ir.config_parameter"].sudo().get_param("enabled_hour_rent")
|
||||
)
|
||||
enabled_session_rent = (
|
||||
self.env["ir.config_parameter"].sudo().get_param("enabled_session_rent")
|
||||
)
|
||||
selection = []
|
||||
if enabled_day_rent:
|
||||
selection.append(("per_day", "Day"))
|
||||
if enabled_hour_rent:
|
||||
selection.append(("per_hour", "Hour"))
|
||||
if enabled_session_rent:
|
||||
selection.append(("per_session", "Session"))
|
||||
return selection
|
||||
|
||||
book_number = fields.Char(string="Book Number")
|
||||
from_date = fields.Datetime(string="From Date")
|
||||
to_date = fields.Datetime(string="To Date")
|
||||
product_line_ids = fields.Many2many(
|
||||
"product.product",
|
||||
"product_search_table",
|
||||
"product_search_id",
|
||||
"product_book_id",
|
||||
string="Available Product",
|
||||
)
|
||||
is_search = fields.Boolean(string="Is Search", default=False)
|
||||
total_days = fields.Float(string="Total Days", compute="compute_rate")
|
||||
extra_charges = fields.Monetary(string="Extra Charges")
|
||||
sub_total = fields.Float(string="Sub Total(Exclusive Tax)", compute="compute_rate")
|
||||
total = fields.Float(string="Total", compute="compute_rate")
|
||||
categ_id = fields.Many2one(
|
||||
"product.category", required=True, string="Product Category"
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
"res.company", string="Company", default=lambda self: self.env.user.company_id
|
||||
)
|
||||
currency_id = fields.Many2one("res.currency", related="company_id.currency_id")
|
||||
location_id = fields.Many2one("stock.location", string="Location")
|
||||
price_based = fields.Selection(
|
||||
selection=_get_based_on_selections, default="per_day", string="Based On"
|
||||
)
|
||||
total_hours = fields.Float(string="Total Hours", compute="compute_rate")
|
||||
session_id = fields.Many2one("session.config", string="Session")
|
||||
|
||||
@api.constrains("from_date", "to_date")
|
||||
def check_date(self):
|
||||
for record in self:
|
||||
if (
|
||||
date.strftime(
|
||||
datetime.strptime(str(record.from_date), "%Y-%m-%d %H:%M:%S"),
|
||||
"%Y-%m-%d",
|
||||
)
|
||||
< str(date.today())
|
||||
):
|
||||
raise ValidationError(_("You cannot enter past date"))
|
||||
if date.strftime(
|
||||
datetime.strptime(str(record.to_date), "%Y-%m-%d %H:%M:%S"), "%Y-%m-%d"
|
||||
) < str(date.today()):
|
||||
raise ValidationError(_("You cannot enter past date"))
|
||||
|
||||
@api.model
|
||||
def convert_float_to_hh_mm(self, session_id, from_date):
|
||||
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 = (
|
||||
"%Y-%m-%d "
|
||||
+ start_date.split(":")[0]
|
||||
+ ":"
|
||||
+ start_date.split(":")[1]
|
||||
+ ":"
|
||||
+ "00"
|
||||
)
|
||||
end_date_fmt = (
|
||||
"%Y-%m-%d "
|
||||
+ end_date.split(":")[0]
|
||||
+ ":"
|
||||
+ end_date.split(":")[1]
|
||||
+ ":"
|
||||
+ "00"
|
||||
)
|
||||
start_str_datetime = from_date.strftime(start_date_fmt)
|
||||
end_str_datetime = from_date.strftime(end_date_fmt)
|
||||
start_date = self.env["rental.product.order"].convert_TZ_UTC(start_str_datetime)
|
||||
end_date = self.env["rental.product.order"].convert_TZ_UTC(end_str_datetime)
|
||||
return start_date, end_date
|
||||
|
||||
@api.onchange("price_based", "from_date", "session_id")
|
||||
def onchange_date_from_price_based(self):
|
||||
if self.price_based == "per_session" and self.from_date and self.session_id:
|
||||
start_time, end_time = self.convert_float_to_hh_mm(
|
||||
self.session_id, self.from_date
|
||||
)
|
||||
self.to_date = end_time
|
||||
self.from_date = start_time
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
vals.update(
|
||||
{
|
||||
"book_number": self.env["ir.sequence"].next_by_code("product_booking")
|
||||
or _("Product Booking")
|
||||
}
|
||||
)
|
||||
return super(ProductBooking, self).create(vals)
|
||||
|
||||
def search_product(self):
|
||||
product_id = []
|
||||
if self.from_date and self.to_date:
|
||||
if self.from_date > self.to_date:
|
||||
raise ValidationError("To Date must be greater than From date")
|
||||
elif not self.env.user.has_group("stock.group_stock_multi_locations"):
|
||||
raise AccessError(
|
||||
_("Manage Multiple Stock Locations need to be assign.")
|
||||
)
|
||||
else:
|
||||
categ_ids = self.env["product.category"].search(
|
||||
[("parent_id", "child_of", self.categ_id.id)]
|
||||
)
|
||||
self.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),
|
||||
self.location_id.id,
|
||||
),
|
||||
)
|
||||
product_data = self.env.cr.fetchall()
|
||||
if product_data:
|
||||
for products in product_data:
|
||||
product = (
|
||||
self.env["product.product"]
|
||||
.sudo()
|
||||
.search([("id", "in", products)])
|
||||
)
|
||||
available_product_stock = self.env["stock.quant"].search(
|
||||
[
|
||||
("product_id", "=", product.id),
|
||||
("quantity", ">", 0),
|
||||
("location_id", "=", self.location_id.id),
|
||||
]
|
||||
)
|
||||
available_product_stock.product_id.rental_qyt = 1.0
|
||||
available_product_stock.product_id.selected_product = False
|
||||
product_id.extend(available_product_stock.product_id.ids)
|
||||
self.update(
|
||||
{
|
||||
"product_line_ids": [(6, 0, product_id)],
|
||||
"total": self.total,
|
||||
"is_search": True,
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise ValidationError("Sorry!!! No any Product Available!!!")
|
||||
|
||||
def book_product(self):
|
||||
product = False
|
||||
for product_line in self.product_line_ids:
|
||||
if product_line.selected_product:
|
||||
product = True
|
||||
if product:
|
||||
product_order_id = []
|
||||
for product_id in self.product_line_ids.filtered(
|
||||
lambda fv: fv.selected_product
|
||||
):
|
||||
# available_product_stock = self.env['stock.quant'].search([('product_id', '=', product_id.id),
|
||||
# ('location_id', '=',
|
||||
# self.location_id.id)])
|
||||
# if product_id.rental_qyt > available_product_stock.quantity:
|
||||
# raise ValidationError(_('Available quantity for %s is %d') % (product_id.name,
|
||||
# available_product_stock.quantity))
|
||||
order_line_obj = self.env["product.order.line"]
|
||||
start_date = self.from_date
|
||||
end_date = self.to_date
|
||||
order_line_ids = order_line_obj.sudo().search(
|
||||
[
|
||||
("product_order_id.state", "=", "confirm"),
|
||||
("product_id", "=", product_id.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_id.sudo().qty_available and total_in_order_qty:
|
||||
qty_available = product_id.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_id.sudo().qty_available + sum(
|
||||
total_in_order_qty.mapped("qty_needed")
|
||||
)
|
||||
if (
|
||||
not product_id.rental_qyt
|
||||
or not qty_available >= product_id.rental_qyt
|
||||
):
|
||||
raise ValidationError(
|
||||
_("Available quantity for %s is %d")
|
||||
% (product_id.name, qty_available)
|
||||
)
|
||||
else:
|
||||
if self.price_based == "per_day":
|
||||
rate_total = product_id.rental_amount
|
||||
else:
|
||||
if self.price_based == "per_hour":
|
||||
rate_total = product_id.rental_amount_per_hour
|
||||
else:
|
||||
rate_total = product_id.rental_amount_per_session
|
||||
product_order_id.append(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": product_id.id,
|
||||
"name": product_id.name,
|
||||
"price_based": self.price_based,
|
||||
"enter_days": self.total_days
|
||||
if self.price_based == "per_day"
|
||||
else 0,
|
||||
"enter_hour": self.total_hours
|
||||
if self.price_based in ["per_hour", "per_session"]
|
||||
else 0,
|
||||
"price": rate_total,
|
||||
"qty_needed": product_id.rental_qyt,
|
||||
"tax_id": [(6, 0, product_id.taxes_id.ids)],
|
||||
},
|
||||
)
|
||||
)
|
||||
return {
|
||||
"name": "product order",
|
||||
"view_type": "form",
|
||||
"view_mode": "form",
|
||||
"res_model": "rental.product.order",
|
||||
"type": "ir.actions.act_window",
|
||||
"context": {
|
||||
"default_from_date": self.from_date,
|
||||
"default_to_date": self.to_date,
|
||||
"default_extra_charges": self.extra_charges,
|
||||
"default_is_days": True if self.price_based == "per_day" else False,
|
||||
"default_is_hours": True
|
||||
if self.price_based in ["per_hour", "per_session"]
|
||||
else False,
|
||||
"default_product_order_lines_ids": product_order_id,
|
||||
"default_location_id": self.location_id.id,
|
||||
"default_price_based": self.price_based,
|
||||
},
|
||||
}
|
||||
else:
|
||||
raise ValidationError(_("First Please Select the Product"))
|
851
product_rental_bookings/models/product_contract.py
Executable file
851
product_rental_bookings/models/product_contract.py
Executable file
@@ -0,0 +1,851 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime, date, timedelta
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from odoo import fields, api, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
class RentalProductContract(models.Model):
|
||||
_name = "rental.product.contract"
|
||||
_description = "Product Rental Contract"
|
||||
|
||||
name = fields.Char("Name")
|
||||
partner_id = fields.Many2one("res.partner", string="Customer", required=True)
|
||||
rental_id = fields.Many2one("rental.product.order", string="Rental Order Id")
|
||||
contract_date = fields.Date(
|
||||
string="Contract Date",
|
||||
)
|
||||
contractor_id = fields.Many2one("res.users", string="Contractor Name")
|
||||
from_date = fields.Date(string="From Date")
|
||||
to_date = fields.Date(string="To Date")
|
||||
account_payment_term = fields.Many2one(
|
||||
"account.payment.term", string="Payment Term", required=True
|
||||
)
|
||||
damage_charge = fields.Monetary(string="Damage Charge")
|
||||
additional_charges = fields.Monetary(string="Additional Charges")
|
||||
subtotal = fields.Monetary(string="Sub Total", readonly=True)
|
||||
taxes = fields.Float(string="Taxes", compute="_compute_amount", readonly=True)
|
||||
untaxed_amount = fields.Monetary(
|
||||
string="Untaxed Amount",
|
||||
compute="_compute_amount",
|
||||
)
|
||||
extra_charges = fields.Monetary(string="Extra Charges")
|
||||
invoice_ids = fields.Many2one("account.move", string="Invoice Id")
|
||||
signature = fields.Binary(string="Contractor Signature ")
|
||||
signature_contractor = fields.Binary(string="Contractor Signature")
|
||||
signature_customer = fields.Binary(string="Customer Signature")
|
||||
button_name = fields.Char(string="Button Name")
|
||||
terms_condition = fields.Text(string="Terms and Condition")
|
||||
product_contract_lines_ids = fields.One2many(
|
||||
"product.contract.lines", "product_contract_id", string="Order Line"
|
||||
)
|
||||
pricelist_id = fields.Many2one("product.pricelist", string="Pricelist")
|
||||
total_amount = fields.Monetary(string="Total Amount", compute="_compute_amount")
|
||||
total = fields.Monetary(string="Total", compute="_compute_total")
|
||||
cost_generated = fields.Monetary(
|
||||
string="Recurring Cost",
|
||||
help="Costs paid at regular intervals, depending on the cost frequency",
|
||||
)
|
||||
cost_frequency = fields.Selection(
|
||||
[
|
||||
("no", "No"),
|
||||
("daily", "Daily"),
|
||||
("weekly", "Weekly"),
|
||||
("monthly", "Monthly"),
|
||||
("yearly", "Yearly"),
|
||||
],
|
||||
string="Recurring Cost Frequency",
|
||||
required=True,
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
("futur", "Incoming"),
|
||||
("open", "In Progress"),
|
||||
("expired", "Expired"),
|
||||
("diesoon", "Expiring Soon"),
|
||||
("closed", "Closed"),
|
||||
],
|
||||
"Status",
|
||||
default="open",
|
||||
readonly=True,
|
||||
)
|
||||
cost = fields.Monetary(
|
||||
string="Rent Cost",
|
||||
help="This fields is to determine the cost of rent",
|
||||
required=True,
|
||||
)
|
||||
account_type = fields.Many2one(
|
||||
"account.account",
|
||||
"Account",
|
||||
default=lambda self: self.env["account.account"].search([("id", "=", 17)]),
|
||||
)
|
||||
recurring_line = fields.One2many(
|
||||
"product.rental.line", "rental_number", readonly=True
|
||||
)
|
||||
attachment_ids = fields.Many2many(
|
||||
"ir.attachment",
|
||||
"product_rent_ir_attachments_rel",
|
||||
"rental_id",
|
||||
"attachment_id",
|
||||
string="Attachments",
|
||||
)
|
||||
sum_cost = fields.Float(
|
||||
compute="_compute_sum_cost", string="Indicative Costs Total"
|
||||
)
|
||||
auto_generated = fields.Boolean("Automatically Generated", readonly=True)
|
||||
generated_cost_ids = fields.One2many(
|
||||
"product.rental.line", "rental_number", string="Generated Costs"
|
||||
)
|
||||
invoice_count = fields.Integer(
|
||||
compute="_invoice_count", string="# Invoice", copy=False
|
||||
)
|
||||
first_payment = fields.Float(string="First Payment", compute="_compute_amount")
|
||||
first_invoice_created = fields.Boolean(
|
||||
string="First Invoice Created", default=False
|
||||
)
|
||||
origin = fields.Char(string="Order Reference")
|
||||
picking_id = fields.Many2one("stock.picking", string="Picking")
|
||||
document_ids = fields.One2many(
|
||||
"customer.document", "contract_id", string="Contract"
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
"res.company", string="Company", default=lambda self: self.env.user.company_id
|
||||
)
|
||||
currency_id = fields.Many2one("res.currency", related="company_id.currency_id")
|
||||
cancel_policy_ids = fields.One2many(
|
||||
"rental.policy", "contract_id", string="Cancel Policy"
|
||||
)
|
||||
number_of_slot = fields.Integer(string="Number of Slot")
|
||||
is_hours = fields.Boolean(string="Hours")
|
||||
is_days = fields.Boolean(string="Days")
|
||||
|
||||
def generate_policy(self):
|
||||
if not self.cancel_policy_ids and self.number_of_slot != 0:
|
||||
number_of_days = self.from_date - self.contract_date
|
||||
cancel_policy_list = []
|
||||
if number_of_days.days >= (self.number_of_slot * 2):
|
||||
day_per_slot = int(number_of_days.days / self.number_of_slot - 1)
|
||||
day = 0
|
||||
for i in range(self.number_of_slot - 1):
|
||||
cancel_policy_list.append(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"from_date": self.contract_date + timedelta(day),
|
||||
"to_date": self.contract_date
|
||||
+ timedelta(day_per_slot + day),
|
||||
},
|
||||
)
|
||||
)
|
||||
day += day_per_slot + 1
|
||||
cancel_policy_list.append(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"from_date": self.contract_date + timedelta(day),
|
||||
"to_date": self.from_date - timedelta(days=2),
|
||||
},
|
||||
)
|
||||
)
|
||||
cancel_policy_list.append(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"from_date": self.from_date - timedelta(days=1),
|
||||
"policy_charged": 100,
|
||||
},
|
||||
)
|
||||
)
|
||||
self.cancel_policy_ids = cancel_policy_list
|
||||
else:
|
||||
raise ValidationError(_("Please enter the sufficient Number of Slot"))
|
||||
|
||||
def write(self, vals):
|
||||
if "button_name" in vals.keys():
|
||||
if vals["button_name"] == "signature_contractor":
|
||||
vals["signature_contractor"] = vals["signature"]
|
||||
elif vals["button_name"] == "signature_customer":
|
||||
vals["signature_customer"] = vals["signature"]
|
||||
return super(RentalProductContract, self).write(vals)
|
||||
|
||||
@api.depends("product_contract_lines_ids")
|
||||
def _compute_amount(self):
|
||||
"""
|
||||
Compute the total amounts of the SO.
|
||||
"""
|
||||
for order in self:
|
||||
untaxed_amount = 0.0
|
||||
taxes = 0.0
|
||||
total_amount = 0.0
|
||||
for line in order.product_contract_lines_ids:
|
||||
untaxed_amount += line.sub_total
|
||||
taxes += line.price_tax
|
||||
total_amount += line.sub_total + line.price_tax
|
||||
order.update(
|
||||
{
|
||||
"untaxed_amount": untaxed_amount,
|
||||
"taxes": taxes,
|
||||
"total_amount": untaxed_amount + taxes + order.extra_charges,
|
||||
"first_payment": untaxed_amount + taxes + order.extra_charges,
|
||||
}
|
||||
)
|
||||
|
||||
@api.depends("recurring_line.recurring_amount")
|
||||
def _compute_sum_cost(self):
|
||||
for contract in self:
|
||||
contract.sum_cost = sum(contract.recurring_line.mapped("recurring_amount"))
|
||||
|
||||
def _invoice_count(self):
|
||||
invoice_ids = self.env["account.move"].search(
|
||||
[("invoice_origin", "=", self.name)]
|
||||
)
|
||||
self.invoice_count = len(invoice_ids)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
sequence_no = self.env["ir.sequence"].next_by_code("product_contract") or _(
|
||||
"Product Contract"
|
||||
)
|
||||
vals.update({"name": sequence_no})
|
||||
return super(RentalProductContract, self).create(vals)
|
||||
|
||||
@api.depends("product_contract_lines_ids", "damage_charge")
|
||||
def _compute_total(self):
|
||||
self.total = self.total_amount + self.damage_charge
|
||||
|
||||
def contract_close(self):
|
||||
invoice_ids = self.env["account.move"].search(
|
||||
[("invoice_origin", "=", self.name)]
|
||||
)
|
||||
order_ids = self.env["rental.product.order"].search(
|
||||
[("res_number", "=", self.origin)]
|
||||
)
|
||||
is_paid = 0
|
||||
for each in invoice_ids:
|
||||
if each.state != "posted":
|
||||
is_paid = 1
|
||||
break
|
||||
|
||||
if is_paid == 0:
|
||||
self.state = "closed"
|
||||
order_ids.state = "close"
|
||||
else:
|
||||
raise UserError("Please Check invoices There are Some Invoices are pending")
|
||||
|
||||
def contract_open(self):
|
||||
for record in self:
|
||||
order_ids = self.env["rental.product.order"].search(
|
||||
[("res_number", "=", record.origin)]
|
||||
)
|
||||
record.state = "open"
|
||||
order_ids.state = "draft"
|
||||
|
||||
def act_renew_contract(self):
|
||||
product_list = []
|
||||
for product_line in self.product_contract_lines_ids:
|
||||
product_list.append(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": product_line.product_id.id,
|
||||
"price_based": product_line.price_based,
|
||||
"enter_days": product_line.enter_days,
|
||||
"price": product_line.price,
|
||||
"enter_hour": product_line.enter_hour,
|
||||
},
|
||||
)
|
||||
)
|
||||
assert (
|
||||
len(self.ids) == 1
|
||||
), "This operation should only be done for 1 single contract at a time, as it it suppose to open a window as result"
|
||||
for element in self:
|
||||
# compute end date
|
||||
startdate = fields.Date.from_string(element.from_date)
|
||||
enddate = fields.Date.from_string(element.to_date)
|
||||
diffdate = enddate - startdate
|
||||
default = {
|
||||
"contract_date": fields.Date.context_today(self),
|
||||
"from_date": fields.Date.to_string(
|
||||
fields.Date.from_string(element.to_date) + relativedelta(days=1)
|
||||
),
|
||||
"to_date": fields.Date.to_string(enddate + diffdate),
|
||||
"cost_generated": self.cost_generated,
|
||||
"product_contract_lines_ids": product_list,
|
||||
}
|
||||
newid = element.copy(default).id
|
||||
return {
|
||||
"name": _("Renew Contract"),
|
||||
"view_mode": "form",
|
||||
"view_id": self.env.ref(
|
||||
"product_rental_bookings.rental_product_contract_form"
|
||||
).id,
|
||||
"view_type": "tree,form",
|
||||
"res_model": "rental.product.contract",
|
||||
"type": "ir.actions.act_window",
|
||||
"res_id": newid,
|
||||
}
|
||||
|
||||
def send_product_contract(self):
|
||||
"""
|
||||
This is Email for send contract Detail
|
||||
"""
|
||||
self.ensure_one()
|
||||
ir_model_data = self.env["ir.model.data"]
|
||||
try:
|
||||
template_id = ir_model_data.get_object_reference(
|
||||
"product_rental_bookings", "email_template_product_contract"
|
||||
)[1]
|
||||
except ValueError:
|
||||
template_id = False
|
||||
try:
|
||||
compose_form_id = ir_model_data.get_object_reference(
|
||||
"mail", "email_compose_message_wizard_form"
|
||||
)[1]
|
||||
except ValueError:
|
||||
compose_form_id = False
|
||||
ctx = {
|
||||
"default_model": "rental.product.contract",
|
||||
"default_res_id": self.ids[0],
|
||||
"default_use_template": bool(template_id),
|
||||
"default_template_id": template_id,
|
||||
"mark_so_as_sent": True,
|
||||
}
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"view_type": "form",
|
||||
"view_mode": "form",
|
||||
"res_model": "mail.compose.message",
|
||||
"views": [(compose_form_id, "form")],
|
||||
"view_id": compose_form_id,
|
||||
"target": "new",
|
||||
"context": ctx,
|
||||
}
|
||||
|
||||
def send_email_for_firstpayment(self, inv_id, contracts):
|
||||
"""
|
||||
Send email for payment.
|
||||
"""
|
||||
mail_content = _(
|
||||
"<h3>First Payment!</h3><br/>Hi %s, <br/> This is to notify that You have to pay amount as per mention below.<br/><br/>"
|
||||
"Please find the details below:<br/><br/>"
|
||||
"<table><tr><td>Reference Number<td/><td> %s<td/><tr/>"
|
||||
"<tr><td>Date<td/><td> %s <td/><tr/><tr><td>Amount <td/><td> %s<td/><tr/><table/>"
|
||||
) % (
|
||||
contracts.partner_id.name,
|
||||
inv_id.invoice_origin,
|
||||
date.today(),
|
||||
inv_id.amount_total,
|
||||
)
|
||||
main_content = {
|
||||
"subject": _("You First Payment For: %s") % inv_id.invoice_origin,
|
||||
"author_id": contracts.env.user.partner_id.id,
|
||||
"body_html": mail_content,
|
||||
"email_to": contracts.partner_id.email,
|
||||
}
|
||||
self.env["mail.mail"].create(main_content).send()
|
||||
|
||||
def notification_email_for_expire_contract(self, contracts):
|
||||
mail_content = _(
|
||||
"<h3>Expiration Of Rental Contract</h3>"
|
||||
"<br/>Dear %s, <br/>"
|
||||
"Our record indicate that your rental contract <b>%s,</b> expire soon,<br/>"
|
||||
"If you want to renew this contract Then contact to our agency before last date of contract."
|
||||
"<br/>"
|
||||
"<br/>"
|
||||
"<br/>"
|
||||
"<br/>"
|
||||
"<table>"
|
||||
"<tr>"
|
||||
"<td>Contract Ref<td/>"
|
||||
"<td>%s<td/>"
|
||||
"<tr/>"
|
||||
"<tr>"
|
||||
"<td>Responsible Person <td/>"
|
||||
"<td> %s - %s<td/>"
|
||||
"<tr/>"
|
||||
"<table/>"
|
||||
) % (
|
||||
contracts.partner_id.name,
|
||||
contracts.name,
|
||||
contracts.name,
|
||||
contracts.contractor_id.name,
|
||||
contracts.contractor_id.mobile,
|
||||
)
|
||||
main_content = {
|
||||
"subject": "Expiration Of Rental Contract!",
|
||||
"author_id": contracts.env.user.partner_id.id,
|
||||
"body_html": mail_content,
|
||||
"email_to": contracts.partner_id.email,
|
||||
}
|
||||
self.env["mail.mail"].create(main_content).send()
|
||||
|
||||
def send_email_for_recurring_invoice(self, inv_id, contracts):
|
||||
mail_content = _(
|
||||
"<h3>Reminder Recurrent Payment!</h3>"
|
||||
"<br/>Hi %s, <br/> This is to remind you that the "
|
||||
"recurrent payment for the "
|
||||
"rental contract has to be done."
|
||||
"Please make the payment at the earliest."
|
||||
"<br/>"
|
||||
"<br/>"
|
||||
"Please find the details below:"
|
||||
"<br/>"
|
||||
"<br/>"
|
||||
"<table>"
|
||||
"<tr>"
|
||||
"<td>Amount <td/>"
|
||||
"<td> %s<td/>"
|
||||
"<tr/>"
|
||||
"<tr>"
|
||||
"<td>Due Date <td/>"
|
||||
"<td> %s<td/>"
|
||||
"<tr/>"
|
||||
"<tr>"
|
||||
"<td>Responsible Person <td/>"
|
||||
"<td> %s, %s<td/>"
|
||||
"<tr/>"
|
||||
"<table/>"
|
||||
) % (
|
||||
contracts.partner_id.name,
|
||||
inv_id.amount_total,
|
||||
date.today(),
|
||||
inv_id.user_id.name,
|
||||
inv_id.user_id.mobile,
|
||||
)
|
||||
main_content = {
|
||||
"subject": "Reminder Recurrent Payment!",
|
||||
"author_id": contracts.env.user.partner_id.id,
|
||||
"body_html": mail_content,
|
||||
"email_to": contracts.partner_id.email,
|
||||
}
|
||||
self.env["mail.mail"].create(main_content).send()
|
||||
|
||||
def create_invoice(self):
|
||||
inv_obj = self.env["account.move"]
|
||||
recurring_obj = self.env["product.rental.line"]
|
||||
inv_line = []
|
||||
today = date.today()
|
||||
journal_id = (
|
||||
self.env["account.journal"]
|
||||
.sudo()
|
||||
.search(
|
||||
[("type", "=", "sale"), ("company_id", "=", self.company_id.id)],
|
||||
limit=1,
|
||||
)
|
||||
)
|
||||
for contracts in self.search([]):
|
||||
if not contracts.first_invoice_created:
|
||||
contracts.first_invoice_created = True
|
||||
supplier = contracts.partner_id
|
||||
account_id = self.env["account.account"].search(
|
||||
[
|
||||
("code", "like", "708000"),
|
||||
("company_id", "=", self.company_id.id),
|
||||
]
|
||||
)
|
||||
if not account_id:
|
||||
user_type_id = self.env.ref("account.data_account_type_revenue")
|
||||
account_id = self.env["account.account"].create(
|
||||
{
|
||||
"code": "708000",
|
||||
"name": "Location",
|
||||
"company_id": self.company_id.id,
|
||||
"user_type_id": user_type_id.id,
|
||||
}
|
||||
)
|
||||
for each_product in contracts.product_contract_lines_ids:
|
||||
if each_product.price_based == "per_hour":
|
||||
total_qty = each_product.enter_hour
|
||||
else:
|
||||
total_qty = each_product.enter_days
|
||||
inv_line_data = (
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": each_product.product_id.name or "Deposit",
|
||||
"product_id": each_product.product_id.id or False,
|
||||
"product_id": each_product.product_id.id or False,
|
||||
"account_id": account_id.id,
|
||||
"price_unit": each_product.price * total_qty or 0.0,
|
||||
"quantity": each_product.qty_needed,
|
||||
"enter_hour": each_product.enter_hour,
|
||||
"enter_days": each_product.enter_days,
|
||||
"tax_ids": [(6, 0, each_product.tax_id.ids)],
|
||||
},
|
||||
)
|
||||
inv_line.append(inv_line_data)
|
||||
if self.extra_charges:
|
||||
extra_charge_p_id = self.env.ref(
|
||||
"product_rental_bookings.extra_charge_product_id"
|
||||
)
|
||||
extra_charge_inv_line = (
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": extra_charge_p_id.name,
|
||||
"product_id": extra_charge_p_id.id or False,
|
||||
"price_unit": self.extra_charges,
|
||||
"account_id": account_id.id,
|
||||
"quantity": 1.0,
|
||||
},
|
||||
)
|
||||
inv_line.append(extra_charge_inv_line)
|
||||
inv_data = {
|
||||
"move_type": "out_invoice",
|
||||
"amount_residual": self.total_amount,
|
||||
"currency_id": self.env.company.currency_id.id,
|
||||
"journal_id": journal_id.id,
|
||||
"company_id": self.env.company.id,
|
||||
"partner_id": supplier.id,
|
||||
"invoice_date_due": self.to_date,
|
||||
"invoice_origin": contracts.name,
|
||||
"contract_id": self.id,
|
||||
"is_hours": self.is_hours,
|
||||
"is_days": self.is_days,
|
||||
"rental_order_id": self.rental_id.id,
|
||||
"invoice_line_ids": inv_line,
|
||||
}
|
||||
bokeh = self.env["ir.module.module"].search(
|
||||
[("name", "in", ["l10n_in", "l10n_in_purchase", "l10n_in_sale"])],
|
||||
limit=1,
|
||||
)
|
||||
if bokeh and bokeh.state == "installed":
|
||||
inv_data.update(
|
||||
{
|
||||
"l10n_in_gst_treatment": supplier.l10n_in_gst_treatment
|
||||
or "unregistered",
|
||||
}
|
||||
)
|
||||
inv_id = inv_obj.create(inv_data)
|
||||
inv_id.action_post()
|
||||
|
||||
recurring_data = {
|
||||
"name": "demo",
|
||||
"date_today": today,
|
||||
"rental_number": contracts.id,
|
||||
"recurring_amount": contracts.first_payment,
|
||||
"invoice_number": inv_id.id,
|
||||
"invoice_ref": inv_id.id,
|
||||
}
|
||||
recurring_obj.create(recurring_data)
|
||||
# self.send_email_for_firstpayment(inv_id, contracts)
|
||||
if inv_id:
|
||||
return {
|
||||
"name": _("Account Move"),
|
||||
"view_mode": "form",
|
||||
"view_id": self.env.ref("account.view_move_form").id,
|
||||
"view_type": "tree,form",
|
||||
"res_model": "account.move",
|
||||
"type": "ir.actions.act_window",
|
||||
"res_id": inv_id.id,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def scheduler_manage_invoice(self):
|
||||
journal_id = self.env["account.move"].default_get(["journal_id"])["journal_id"]
|
||||
inv_obj = self.env["account.move"]
|
||||
recurring_obj = self.env["product.rental.line"]
|
||||
_inv_line_data = {}
|
||||
today = date.today()
|
||||
for contracts in self.search([]):
|
||||
account_id = self.env["account.account"].search(
|
||||
[
|
||||
("code", "like", "708000"),
|
||||
("company_id", "=", contracts.company_id.id),
|
||||
]
|
||||
)
|
||||
if not account_id:
|
||||
user_type_id = self.env.ref("account.data_account_type_revenue")
|
||||
account_id = self.env["account.account"].create(
|
||||
{
|
||||
"code": "708000",
|
||||
"name": "Location",
|
||||
"company_id": contracts.company_id.id,
|
||||
"user_type_id": user_type_id.id,
|
||||
}
|
||||
)
|
||||
start_date = datetime.strptime(str(contracts.from_date), "%Y-%m-%d").date()
|
||||
end_date = datetime.strptime(str(contracts.to_date), "%Y-%m-%d").date()
|
||||
if end_date >= date.today():
|
||||
is_recurring = 0
|
||||
if contracts.cost_frequency == "daily":
|
||||
is_recurring = 1
|
||||
elif contracts.cost_frequency == "weekly":
|
||||
week_days = (date.today() - start_date).days
|
||||
if week_days % 7 == 0 and week_days != 0:
|
||||
is_recurring = 1
|
||||
elif contracts.cost_frequency == "monthly":
|
||||
if (
|
||||
start_date.day == date.today().day
|
||||
and start_date != date.today()
|
||||
):
|
||||
is_recurring = 1
|
||||
elif contracts.cost_frequency == "yearly":
|
||||
if (
|
||||
start_date.day == date.today().day
|
||||
and start_date.month == date.today().month
|
||||
and start_date != date.today()
|
||||
):
|
||||
is_recurring = 1
|
||||
if (
|
||||
is_recurring == 1
|
||||
and contracts.cost_frequency != "no"
|
||||
and contracts.state != "expire"
|
||||
and contracts.state != "close"
|
||||
and contracts.state != "futur"
|
||||
and contracts.first_invoice_created == True
|
||||
):
|
||||
inv_line = []
|
||||
supplier = contracts.partner_id
|
||||
line_len = len(contracts.product_contract_lines_ids)
|
||||
for each_product in contracts.product_contract_lines_ids:
|
||||
unit_price = contracts.cost_generated / line_len
|
||||
inv_line_data = (
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": each_product.product_id.id,
|
||||
"name": each_product.product_id.name,
|
||||
"product_id": each_product.product_id.id,
|
||||
"account_id": account_id.id,
|
||||
"price_unit": float(unit_price),
|
||||
"quantity": 1,
|
||||
"exclude_from_invoice_tab": False,
|
||||
},
|
||||
)
|
||||
inv_line.append(inv_line_data)
|
||||
inv_data = {
|
||||
"type": "out_invoice",
|
||||
"currency_id": contracts.account_type.company_id.currency_id.id,
|
||||
"journal_id": journal_id,
|
||||
"company_id": contracts.account_type.company_id.id,
|
||||
"name": supplier.name,
|
||||
"partner_id": supplier.id,
|
||||
"invoice_date_due": contracts.to_date,
|
||||
"invoice_origin": contracts.name,
|
||||
"contract_id": contracts.id,
|
||||
"invoice_line_ids": inv_line,
|
||||
"is_hours": contracts.is_hours,
|
||||
"is_days": contracts.is_days,
|
||||
}
|
||||
|
||||
inv_id = inv_obj.create(inv_data)
|
||||
payment_id = self.env["account.payment"].create(
|
||||
{
|
||||
"payment_type": "inbound",
|
||||
"partner_type": "supplier",
|
||||
"partner_id": supplier.id,
|
||||
"amount": inv_id.amount_total,
|
||||
"journal_id": journal_id,
|
||||
"payment_date": date.today(),
|
||||
"payment_method_id": "1",
|
||||
"communication": inv_id.name,
|
||||
}
|
||||
)
|
||||
recurring_data = {
|
||||
"name": "demo",
|
||||
"date_today": today,
|
||||
"rental_number": contracts.id,
|
||||
"recurring_amount": contracts.cost_generated,
|
||||
"invoice_number": inv_id.id,
|
||||
"invoice_ref": inv_id.id,
|
||||
}
|
||||
recurring_obj.create(recurring_data)
|
||||
# self.send_email_for_recurring_invoice(inv_id, contracts)
|
||||
else:
|
||||
inv_line = []
|
||||
if (
|
||||
not contracts.first_invoice_created
|
||||
and contracts.state != "futur"
|
||||
and contracts.state != "expired"
|
||||
):
|
||||
contracts.first_invoice_created = True
|
||||
supplier = contracts.partner_id
|
||||
for each_product in contracts.product_contract_lines_ids:
|
||||
if each_product.price_based == "per_day":
|
||||
total_qty = each_product.enter_days
|
||||
else:
|
||||
total_qty = each_product.enter_hour
|
||||
inv_line_data = (
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": each_product.product_id.id,
|
||||
"name": each_product.product_id.name,
|
||||
"product_id": each_product.product_id.id,
|
||||
"account_id": supplier.property_account_payable_id.id,
|
||||
"price_unit": each_product.price,
|
||||
"quantity": total_qty,
|
||||
"tax_ids": [(6, 0, each_product.tax_id.ids)],
|
||||
"exclude_from_invoice_tab": False,
|
||||
},
|
||||
)
|
||||
inv_line.append(inv_line_data)
|
||||
inv_data = {
|
||||
"name": supplier.name,
|
||||
"partner_id": supplier.id,
|
||||
"currency_id": contracts.account_type.company_id.currency_id.id,
|
||||
"journal_id": journal_id,
|
||||
"invoice_origin": contracts.name,
|
||||
"company_id": contracts.account_type.company_id.id,
|
||||
"invoice_date_due": self.to_date,
|
||||
"invoice_line_ids": inv_line,
|
||||
}
|
||||
inv_id = inv_obj.create(inv_data)
|
||||
recurring_data = {
|
||||
"name": "demo",
|
||||
"date_today": today,
|
||||
"rental_number": contracts.id,
|
||||
"recurring_amount": contracts.first_payment,
|
||||
"invoice_number": inv_id.id,
|
||||
"invoice_ref": inv_id.id,
|
||||
}
|
||||
recurring_obj.create(recurring_data)
|
||||
# self.send_email_for_firstpayment(inv_id, contracts)
|
||||
|
||||
@api.model
|
||||
def shedule_manage_contract(self):
|
||||
date_today = fields.Date.from_string(fields.Date.today())
|
||||
in_fifteen_days = fields.Date.to_string(date_today + relativedelta(days=+15))
|
||||
nearly_expired_contracts = self.search(
|
||||
[("state", "=", "open"), ("to_date", "<", in_fifteen_days)]
|
||||
)
|
||||
res = {}
|
||||
for contract in nearly_expired_contracts:
|
||||
if contract.partner_id.id in res:
|
||||
res[contract.partner_id.id] += 1
|
||||
else:
|
||||
res[contract.partner_id.id] = 1
|
||||
# contract.notification_email_for_expire_contract(contract)
|
||||
|
||||
nearly_expired_contracts.write({"state": "diesoon"})
|
||||
|
||||
expired_contracts = self.search(
|
||||
[("state", "!=", "expired"), ("to_date", "<", fields.Date.today())]
|
||||
)
|
||||
expired_contracts.write({"state": "expired"})
|
||||
|
||||
futur_contracts = self.search(
|
||||
[
|
||||
("state", "not in", ["futur", "closed"]),
|
||||
("from_date", ">", fields.Date.today()),
|
||||
]
|
||||
)
|
||||
futur_contracts.write({"state": "futur"})
|
||||
|
||||
now_running_contracts = self.search(
|
||||
[("state", "=", "futur"), ("from_date", "<=", fields.Date.today())]
|
||||
)
|
||||
now_running_contracts.write({"state": "open"})
|
||||
|
||||
@api.model
|
||||
def run_scheduler(self):
|
||||
self.shedule_manage_contract()
|
||||
self.scheduler_manage_invoice()
|
||||
|
||||
|
||||
class productConLines(models.Model):
|
||||
_name = "product.contract.lines"
|
||||
_description = "Product Rental Contract Lines"
|
||||
|
||||
product_id = fields.Many2one("product.product", string="product Name")
|
||||
price_based = fields.Selection(
|
||||
[("per_day", "Day"), ("per_hour", "Hour"), ("per_session", "Session")],
|
||||
default="per_day",
|
||||
string="Based On",
|
||||
)
|
||||
enter_hour = fields.Float(string="Hour")
|
||||
enter_days = fields.Float(string="Days")
|
||||
price = fields.Monetary(string="Price")
|
||||
total = fields.Monetary(string="Total")
|
||||
product_contract_id = fields.Many2one("rental.product.contract", string="Contract")
|
||||
tax_id = fields.Many2many("account.tax", "product_contract_tax_rel", string="Tax")
|
||||
sub_total = fields.Monetary(string="Sub Total", compute="_get_subtotal", store=True)
|
||||
price_tax = fields.Float(
|
||||
compute="_get_subtotal", string="Taxes", readonly=True, store=True
|
||||
)
|
||||
price_total = fields.Monetary(
|
||||
compute="_get_subtotal", string="Total ", readonly=True, store=True
|
||||
)
|
||||
description = fields.Char(string="Description")
|
||||
currency_id = fields.Many2one(
|
||||
"res.currency",
|
||||
related="product_contract_id.currency_id",
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
qty_needed = fields.Integer(string="Quantity", default=1)
|
||||
|
||||
@api.depends("product_id", "enter_hour", "enter_days", "price", "tax_id")
|
||||
def _get_subtotal(self):
|
||||
for line in self:
|
||||
if line.price_based == "per_day":
|
||||
qty = line.enter_days * line.qty_needed
|
||||
elif line.price_based == "per_session":
|
||||
qty = line.qty_needed
|
||||
else:
|
||||
qty = line.enter_hour * line.qty_needed
|
||||
print("\n\n\n\n\n\n------>", qty)
|
||||
taxes = line.tax_id.compute_all(
|
||||
qty, line.product_contract_id.currency_id, line.price
|
||||
)
|
||||
line.update(
|
||||
{
|
||||
"price_tax": sum(
|
||||
t.get("amount", 0.0) for t in taxes.get("taxes", [])
|
||||
),
|
||||
"price_total": taxes["total_included"],
|
||||
"sub_total": taxes["total_excluded"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ProductRentalLine(models.Model):
|
||||
_name = "product.rental.line"
|
||||
_description = "Rental Lines"
|
||||
|
||||
name = fields.Char(string="Name")
|
||||
date_today = fields.Date("Date")
|
||||
recurring_amount = fields.Float("Amount")
|
||||
rental_number = fields.Many2one("rental.product.contract", string="Rental Number")
|
||||
payment_info = fields.Char(
|
||||
compute="paid_info", string="Payment Stage", default="draft"
|
||||
)
|
||||
auto_generated = fields.Boolean("Automatically Generated", readonly=True)
|
||||
invoice_number = fields.Integer(string="Invoice ID")
|
||||
invoice_ref = fields.Many2one("account.move", string="Invoice Ref")
|
||||
|
||||
@api.depends("payment_info")
|
||||
def paid_info(self):
|
||||
for record in self:
|
||||
if self.env["account.move"].browse(record.invoice_number):
|
||||
record.payment_info = (
|
||||
self.env["account.move"].browse(record.invoice_number).state
|
||||
)
|
||||
else:
|
||||
record.payment_info = "Record Deleted"
|
||||
|
||||
|
||||
class CustomerDocument(models.Model):
|
||||
_name = "customer.document"
|
||||
_description = "Customer Document"
|
||||
|
||||
name = fields.Binary(string="Document")
|
||||
id_number = fields.Char(string="ID Number")
|
||||
contract_id = fields.Many2one("rental.product.contract", string="Conrtract")
|
||||
|
||||
|
||||
class RentalPolicy(models.Model):
|
||||
_name = "rental.policy"
|
||||
_description = "Rental Policy"
|
||||
|
||||
contract_id = fields.Many2one("rental.product.contract", string="Contract")
|
||||
from_date = fields.Date(string="From Date")
|
||||
to_date = fields.Date(string="To Date")
|
||||
policy_charged = fields.Float(string="Charge(In Percentage)")
|
17
product_rental_bookings/models/product_logs.py
Normal file
17
product_rental_bookings/models/product_logs.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class RentalProductLogs(models.Model):
|
||||
_name = "rental.product.logs"
|
||||
_description = "product Products Logs"
|
||||
_rec_name = "product_id"
|
||||
|
||||
product_id = fields.Many2one("product.product", string="Product")
|
||||
customer_id = fields.Many2one("res.partner", string="Customer")
|
||||
from_date = fields.Date(string="From Date")
|
||||
to_date = fields.Date(string="To Date")
|
||||
company_id = fields.Many2one(
|
||||
"res.company", string="Company", default=lambda self: self.env.user.company_id
|
||||
)
|
91
product_rental_bookings/models/product_operation.py
Executable file
91
product_rental_bookings/models/product_operation.py
Executable file
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class RentalProductOperation(models.Model):
|
||||
_name = "rental.product.operation"
|
||||
_description = "product Products Operation"
|
||||
|
||||
name = fields.Char(string="Operation Types Name", required=True, translate=True)
|
||||
color = fields.Integer(string="Color")
|
||||
rental_move_type = fields.Selection(
|
||||
[("outgoing", "Customers"), ("incoming", "Return"), ("internal", "Internal")],
|
||||
string="Types of Operation",
|
||||
required=True,
|
||||
default="outgoing",
|
||||
)
|
||||
source_location = fields.Many2one("stock.location", string="Source Location")
|
||||
destination_location = fields.Many2one(
|
||||
"stock.location", string="Destination Location"
|
||||
)
|
||||
location_id = fields.Many2one("stock.location", string="Location")
|
||||
state = fields.Selection(
|
||||
[
|
||||
("ready", "Ready"),
|
||||
("on_rent", "On Rent"),
|
||||
("service", "Service"),
|
||||
("done", "Done"),
|
||||
]
|
||||
)
|
||||
count_operation_ready = fields.Integer(compute="_compute_operation_count")
|
||||
count_operation_on_rent = fields.Integer(compute="_compute_operation_count")
|
||||
count_operation_service = fields.Integer(compute="_compute_operation_count")
|
||||
operation_type_id = fields.Many2one(
|
||||
"rental.product.operation", string="Operation Type"
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
"res.company", string="Company", default=lambda self: self.env.user.company_id
|
||||
)
|
||||
|
||||
rental_move_type = fields.Selection(
|
||||
[("outgoing", "Customers"), ("incoming", "Return"), ("internal", "Internal")],
|
||||
string="Types of Operation",
|
||||
required=True,
|
||||
default="internal",
|
||||
)
|
||||
|
||||
def _compute_operation_count(self):
|
||||
for each in self:
|
||||
ready_state = self.env["stock.picking"].search_count(
|
||||
[
|
||||
("state", "in", ["draft"]),
|
||||
("location_id", "=", each.location_id.id),
|
||||
("is_rental", "=", True),
|
||||
]
|
||||
)
|
||||
each.count_operation_ready = ready_state
|
||||
on_rent_state = self.env["stock.picking"].search_count(
|
||||
[
|
||||
("location_id", "=", each.location_id.id),
|
||||
("state", "=", "confirmed"),
|
||||
("is_rental", "=", True),
|
||||
]
|
||||
)
|
||||
each.count_operation_on_rent = on_rent_state
|
||||
service_state = self.env["stock.picking"].search_count(
|
||||
[
|
||||
("state", "=", "assigned"),
|
||||
("location_id", "=", each.location_id.id),
|
||||
("is_rental", "=", True),
|
||||
]
|
||||
)
|
||||
each.count_operation_service = service_state
|
||||
|
||||
def get_action_operation(self):
|
||||
if self.rental_move_type == "outgoing":
|
||||
state = ["draft"]
|
||||
elif self.rental_move_type == "incoming":
|
||||
state = ["confirmed"]
|
||||
elif self.rental_move_type == "internal":
|
||||
state = ["assigned"]
|
||||
action_id = self.env.ref(
|
||||
"product_rental_bookings.action_rental_product_move"
|
||||
).read()[0]
|
||||
action_id["context"] = {"default_rental_move_type": self.rental_move_type}
|
||||
action_id["domain"] = [
|
||||
("state", "in", state),
|
||||
("rental_move_type", "=", self.rental_move_type),
|
||||
("location_id", "in", self.location_id.ids),
|
||||
]
|
||||
return action_id
|
866
product_rental_bookings/models/product_order.py
Executable file
866
product_rental_bookings/models/product_order.py
Executable file
@@ -0,0 +1,866 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import timedelta, date, datetime
|
||||
|
||||
import pytz
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class PaymentTransaction(models.Model):
|
||||
_inherit = "payment.transaction"
|
||||
|
||||
@api.model
|
||||
def _compute_reference_prefix(self, values):
|
||||
prefix = super(PaymentTransaction, self)._compute_reference_prefix(values)
|
||||
if not prefix and values:
|
||||
prefix = "Rental Order"
|
||||
return prefix
|
||||
|
||||
def render_rental_button(self, order, submit_txt=None, render_values=None):
|
||||
values = {
|
||||
"partner_id": order.partner_shipping_id.id or order.partner_invoice_id.id,
|
||||
"billing_partner_id": order.partner_invoice_id.id,
|
||||
"type": "form_save",
|
||||
}
|
||||
if render_values:
|
||||
values.update(render_values)
|
||||
self._log_payment_transaction_sent()
|
||||
return (
|
||||
self.acquirer_id.with_context(
|
||||
submit_class="btn btn-primary", submit_txt=submit_txt or _("Pay Now")
|
||||
)
|
||||
.sudo()
|
||||
.render(
|
||||
self.reference,
|
||||
order.total_amount,
|
||||
order.currency_id.id,
|
||||
values=values,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class RentalProductOrder(models.Model):
|
||||
_name = "rental.product.order"
|
||||
_description = "product Product Order"
|
||||
_rec_name = "res_number"
|
||||
|
||||
def _default_price_list(self):
|
||||
return (
|
||||
self.env["product.pricelist"]
|
||||
.search(
|
||||
[
|
||||
("company_id", "in", (False, self.env.company.id)),
|
||||
("currency_id", "=", self.env.company.currency_id.id),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
.id
|
||||
)
|
||||
|
||||
res_number = fields.Char(string="Order number", readonly=True, default="New")
|
||||
product_name = fields.Many2one("product.product", string="Product Name")
|
||||
from_date = fields.Datetime(string="From Date")
|
||||
date_order = fields.Datetime(string="Date From")
|
||||
to_date = fields.Datetime(string="To Date")
|
||||
start_date = fields.Char(string="start Date")
|
||||
end_date = fields.Char(string="end Date")
|
||||
customer_name = fields.Many2one("res.partner", string="Customer Name")
|
||||
book_date = fields.Datetime(string="Booking Date", default=datetime.now())
|
||||
account_payment_term = fields.Many2one(
|
||||
"account.payment.term", string="Payment Term", required=True, default=1
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
("draft", "Draft"),
|
||||
("confirm", "Confirm"),
|
||||
("cancel", "Cancel"),
|
||||
("close", "Close"),
|
||||
],
|
||||
default="draft",
|
||||
)
|
||||
is_invoice = fields.Boolean(string="invoice")
|
||||
rental_product_ids = fields.One2many(
|
||||
"product.product", "product_registration_id", string="Product"
|
||||
)
|
||||
is_agreement = fields.Boolean(string="Contracts Require?", default=True)
|
||||
product_order_lines_ids = fields.One2many(
|
||||
"product.order.line", "product_order_id", string="Order Line "
|
||||
)
|
||||
invoice_count = fields.Integer(compute="_invoice_total", string="Total Invoiced")
|
||||
contract_count = fields.Integer(compute="_contract_total", string="Total Contract")
|
||||
picking_count = fields.Integer(compute="_picking_total", string="Total Invoiced ")
|
||||
stock_picking_ids = fields.One2many(
|
||||
"stock.picking", "product_order_rel_id", string="Picking Id"
|
||||
)
|
||||
invoice_ids = fields.One2many(
|
||||
"account.move", "rental_order_id", string="Invoice Id"
|
||||
)
|
||||
contract_ids = fields.One2many(
|
||||
"rental.product.contract", "rental_id", string="Contract Id"
|
||||
)
|
||||
pricelist_id = fields.Many2one(
|
||||
"product.pricelist", string="Pricelist", default=_default_price_list
|
||||
) # _default_pricelist
|
||||
extra_charges = fields.Monetary(string="Extra Charges", readonly=True)
|
||||
total_amount = fields.Monetary(
|
||||
string="Total Amount", compute="_compute_amount", store=True
|
||||
)
|
||||
taxes = fields.Float(string="Taxes", compute="_compute_amount", store=True)
|
||||
untaxed_amount = fields.Monetary(
|
||||
string="Untaxed Amount", compute="_compute_amount", store=True
|
||||
)
|
||||
amount_untaxed = fields.Monetary(string="Untaxed Amount 1")
|
||||
|
||||
user_id = fields.Many2one(
|
||||
"res.users", string="Dealer", default=lambda self: self.env.user
|
||||
)
|
||||
terms_condition = fields.Text(string="Terms And Condition")
|
||||
invoice_status = fields.Char(compute="get_invoice_status", string="Invoice Status")
|
||||
company_id = fields.Many2one(
|
||||
"res.company",
|
||||
string="Company",
|
||||
readonly=True,
|
||||
default=lambda self: self.env.user.company_id,
|
||||
)
|
||||
currency_id = fields.Many2one("res.currency", related="company_id.currency_id")
|
||||
count = fields.Integer(
|
||||
string="Count", compute="_compute_count", store=True, invisible=True
|
||||
)
|
||||
is_true = fields.Boolean(string="True")
|
||||
partner_shipping_id = fields.Many2one(
|
||||
"res.partner", related="customer_name", string="Delivery Address"
|
||||
)
|
||||
partner_invoice_id = fields.Many2one(
|
||||
"res.partner", "Invoicing Address", related="customer_name"
|
||||
)
|
||||
return_date = fields.Datetime("Return Date")
|
||||
location_id = fields.Many2one("stock.location", string="Location")
|
||||
is_hours = fields.Boolean(string="Hours")
|
||||
is_days = fields.Boolean(string="Days")
|
||||
|
||||
def _create_payment_transaction(self, vals):
|
||||
"""Similar to self.env['payment.transaction'].create(vals) but the values are filled with the
|
||||
current sales orders fields (e.g. the partner or the currency).
|
||||
:param vals: The values to create a new payment.transaction.
|
||||
:return: The newly created payment.transaction record.
|
||||
"""
|
||||
# Try to retrieve the acquirer. However, fallback to the token's acquirer.
|
||||
acquirer_id = int(vals.get("acquirer_id"))
|
||||
acquirer = False
|
||||
payment_token_id = vals.get("payment_token_id")
|
||||
acquirer = self.env["payment.acquirer"].browse(acquirer_id)
|
||||
partner = self.env["res.partner"].browse(vals.get("partner_id"))
|
||||
if payment_token_id and acquirer_id:
|
||||
payment_token = (
|
||||
self.env["payment.token"].sudo().browse(int(payment_token_id))
|
||||
)
|
||||
if payment_token and payment_token.acquirer_id != acquirer:
|
||||
raise ValidationError(
|
||||
_("Invalid token found! Token acquirer %s != %s")
|
||||
% (payment_token.acquirer_id.name, acquirer.name)
|
||||
)
|
||||
if payment_token and payment_token.partner_id != partner:
|
||||
raise ValidationError(
|
||||
_("Invalid token found! Token partner %s != %s")
|
||||
% (payment_token.partner_id.name, partner.name)
|
||||
)
|
||||
else:
|
||||
acquirer = payment_token.acquirer_id
|
||||
# Check an acquirer is there.
|
||||
if not acquirer_id and not acquirer:
|
||||
raise ValidationError(
|
||||
_("A payment acquirer is required to create a transaction.")
|
||||
)
|
||||
if not acquirer:
|
||||
acquirer = self.env["payment.acquirer"].browse(acquirer_id)
|
||||
# Check a journal is set on acquirer.
|
||||
if not acquirer.journal_id:
|
||||
raise ValidationError(
|
||||
_("A journal must be specified of the acquirer %s." % acquirer.name)
|
||||
)
|
||||
if not acquirer_id and acquirer:
|
||||
vals["acquirer_id"] = acquirer.id
|
||||
vals.update(
|
||||
{
|
||||
"date": datetime.now(),
|
||||
"amount": vals.get("amount"),
|
||||
"currency_id": vals.get("currency_id"),
|
||||
"partner_id": vals.get("partner_id"),
|
||||
}
|
||||
)
|
||||
transaction = self.env["payment.transaction"].create(vals)
|
||||
# Process directly if payment_token
|
||||
if transaction.payment_token_id:
|
||||
transaction.s2s_do_transaction()
|
||||
return transaction
|
||||
|
||||
@api.depends("customer_name")
|
||||
def _compute_count(self):
|
||||
self.ensure_one()
|
||||
self.count = len(self.search([("customer_name", "=", self.customer_name.id)]))
|
||||
|
||||
@api.depends("invoice_status")
|
||||
def get_invoice_status(self):
|
||||
for record in self:
|
||||
record.invoice_status = ""
|
||||
for invoice in record.invoice_ids:
|
||||
record.invoice_status = invoice.state
|
||||
|
||||
@api.onchange("customer_name")
|
||||
def onchange_customer(self):
|
||||
self.account_payment_term = self.customer_name.property_payment_term_id.id
|
||||
|
||||
@api.depends("stock_picking_ids")
|
||||
def _picking_total(self):
|
||||
for order in self:
|
||||
order.picking_count = len(order.stock_picking_ids)
|
||||
|
||||
@api.depends("invoice_ids")
|
||||
def _invoice_total(self):
|
||||
for order in self:
|
||||
order.invoice_count = len(order.invoice_ids)
|
||||
|
||||
def convert_TZ_UTC(self, TZ_datetime):
|
||||
fmt = "%Y-%m-%d %H:%M:%S"
|
||||
# Current time in UTC
|
||||
now_utc = datetime.now(pytz.timezone("UTC"))
|
||||
# Convert to current user time zone
|
||||
now_timezone = now_utc.astimezone(pytz.timezone(self.env.user.tz))
|
||||
UTC_OFFSET_TIMEDELTA = datetime.strptime(
|
||||
now_utc.strftime(fmt), fmt
|
||||
) - datetime.strptime(now_timezone.strftime(fmt), fmt)
|
||||
local_datetime = datetime.strptime(TZ_datetime, fmt)
|
||||
result_utc_datetime = local_datetime + UTC_OFFSET_TIMEDELTA
|
||||
return result_utc_datetime.strftime(fmt)
|
||||
|
||||
def action_view_order_invoices(self):
|
||||
action = self.env.ref("account.action_move_out_invoice_type").read()[0]
|
||||
invoices = self.mapped("invoice_ids")
|
||||
if len(invoices) > 1:
|
||||
action["domain"] = [("id", "in", invoices.ids)]
|
||||
elif invoices:
|
||||
action["views"] = [(self.env.ref("account.view_move_form").id, "form")]
|
||||
action["res_id"] = invoices.id
|
||||
return action
|
||||
|
||||
@api.depends("contract_ids")
|
||||
def _contract_total(self):
|
||||
for contract in self:
|
||||
contract.contract_count = len(contract.contract_ids)
|
||||
|
||||
def action_view_order_contract(self):
|
||||
action = self.env.ref(
|
||||
"product_rental_bookings.action_rental_contract_view_tree"
|
||||
).read()[0]
|
||||
contracts = self.mapped("contract_ids")
|
||||
if len(contracts) > 1:
|
||||
action["domain"] = [("id", "in", contracts.ids)]
|
||||
elif contracts:
|
||||
action["views"] = [
|
||||
(
|
||||
self.env.ref(
|
||||
"product_rental_bookings.rental_product_contract_form"
|
||||
).id,
|
||||
"form",
|
||||
)
|
||||
]
|
||||
action["res_id"] = contracts.id
|
||||
return action
|
||||
|
||||
@api.onchange("customer_name")
|
||||
def customer_pricelist(self):
|
||||
values = {
|
||||
"pricelist_id": self.customer_name.property_product_pricelist
|
||||
and self.customer_name.property_product_pricelist.id
|
||||
or False,
|
||||
}
|
||||
self.update(values)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
product_order_line = []
|
||||
if vals.get("is_true"):
|
||||
for order_line in vals.get("product_order_lines_ids"):
|
||||
if order_line[2] and order_line[0] != 0:
|
||||
product_order_line.append([0, False, order_line[2]])
|
||||
elif order_line[2] and order_line[0] == 0:
|
||||
product_order_line.append(order_line)
|
||||
vals.update({"product_order_lines_ids": product_order_line})
|
||||
sequence = self.env["ir.sequence"].next_by_code("product_registration") or _(
|
||||
"Product Register"
|
||||
)
|
||||
vals.update({"res_number": sequence})
|
||||
res = super(RentalProductOrder, self).create(vals)
|
||||
from_date, to_date = self.start_end_date_global(res.from_date, res.to_date)
|
||||
res.start_date = from_date
|
||||
res.end_date = to_date
|
||||
return res
|
||||
|
||||
@api.depends("product_order_lines_ids", "customer_name")
|
||||
def _compute_amount(self):
|
||||
"""
|
||||
Compute the total amounts of the RO.
|
||||
"""
|
||||
for order in self:
|
||||
untaxed_amount = 0.0
|
||||
taxes = 0.0
|
||||
for line in order.product_order_lines_ids:
|
||||
untaxed_amount += line.sub_total
|
||||
taxes += line.price_tax
|
||||
if order.pricelist_id:
|
||||
order.update(
|
||||
{
|
||||
"untaxed_amount": order.pricelist_id.currency_id.round(
|
||||
untaxed_amount
|
||||
),
|
||||
"taxes": order.pricelist_id.currency_id.round(taxes),
|
||||
"total_amount": untaxed_amount + taxes + order.extra_charges,
|
||||
}
|
||||
)
|
||||
else:
|
||||
order.update(
|
||||
{
|
||||
"untaxed_amount": untaxed_amount,
|
||||
"taxes": taxes,
|
||||
"total_amount": untaxed_amount + taxes + order.extra_charges,
|
||||
}
|
||||
)
|
||||
|
||||
def book(self):
|
||||
self.state = "book"
|
||||
|
||||
@api.model
|
||||
def _get_picking_type(self, company_id):
|
||||
picking_type = self.env["stock.picking.type"].search(
|
||||
[("code", "=", "incoming"), ("warehouse_id.company_id", "=", company_id)]
|
||||
)
|
||||
if not picking_type:
|
||||
picking_type = self.env["stock.picking.type"].search(
|
||||
[("code", "=", "incoming"), ("warehouse_id", "=", False)]
|
||||
)
|
||||
return picking_type[:1].id
|
||||
|
||||
def confirm(self):
|
||||
product_order_id = []
|
||||
move_ids_without_package = []
|
||||
for each in self.product_order_lines_ids:
|
||||
product_order_id.append((0, 0, {"product_id": each.product_id.id}))
|
||||
move_ids_without_package.append(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": each.product_id.id,
|
||||
"product_uom_qty": each.qty_needed,
|
||||
"product_uom": each.product_id.uom_id.id,
|
||||
"location_id": self.location_id.id
|
||||
or each.product_id.location_id.id,
|
||||
"location_dest_id": self.env.ref(
|
||||
"stock.stock_location_customers"
|
||||
).id,
|
||||
"name": each.product_id.name,
|
||||
"company_id": self.company_id.id,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
stock_picking_id = self.env["stock.picking"].create(
|
||||
{
|
||||
"partner_id": self.customer_name.id,
|
||||
"location_id": self.location_id.id,
|
||||
"location_dest_id": self.env.ref("stock.stock_location_customers").id,
|
||||
"rental_move_type": "outgoing",
|
||||
"picking_type_id": self._get_picking_type(self.company_id.id),
|
||||
"product_order_rel_id": self.id,
|
||||
"is_rental": True,
|
||||
"origin": self.res_number,
|
||||
"move_ids_without_package": move_ids_without_package,
|
||||
}
|
||||
)
|
||||
self.state = "confirm"
|
||||
if self.is_agreement:
|
||||
rental_product_contract_obj = self.env["rental.product.contract"]
|
||||
product_order_id = []
|
||||
for each in self.product_order_lines_ids:
|
||||
product_order_id.append(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": each.product_id.id or "",
|
||||
"price_based": each.price_based or "",
|
||||
"enter_days": each.enter_days or "",
|
||||
"enter_hour": each.enter_hour or "",
|
||||
"price": each.price or "",
|
||||
"qty_needed": each.qty_needed,
|
||||
"sub_total": each.sub_total or "",
|
||||
"tax_id": [(6, 0, each.tax_id.ids)],
|
||||
},
|
||||
)
|
||||
)
|
||||
self.state = "confirm"
|
||||
view_id = self.env.ref(
|
||||
"product_rental_bookings.rental_product_contract_form"
|
||||
)
|
||||
contract = rental_product_contract_obj.create(
|
||||
{
|
||||
"partner_id": self.customer_name.id,
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
"total_amount": self.total_amount,
|
||||
"rental_id": self.id,
|
||||
"product_contract_lines_ids": product_order_id,
|
||||
"cost_frequency": "no",
|
||||
"contract_date": self.book_date,
|
||||
"account_payment_term": self.account_payment_term.id,
|
||||
"contractor_id": self.user_id.id,
|
||||
"origin": self.res_number,
|
||||
"cost": 12,
|
||||
"is_hours": self.is_hours,
|
||||
"is_days": self.is_days,
|
||||
"picking_id": stock_picking_id.id,
|
||||
"company_id": self.company_id.id,
|
||||
"name": "New",
|
||||
}
|
||||
)
|
||||
return {
|
||||
"name": _("product Contract"),
|
||||
"type": "ir.actions.act_window",
|
||||
"view_type": "form",
|
||||
"view_mode": "form",
|
||||
"res_model": "rental.product.contract",
|
||||
"res_id": contract.id,
|
||||
"view_id": view_id.id,
|
||||
}
|
||||
self.state = "confirm"
|
||||
|
||||
def action_view_invoice(self):
|
||||
invoices = self.mapped("invoice_ids")
|
||||
action = self.env.ref("account.action_invoice_tree1").read()[0]
|
||||
if len(invoices) > 1:
|
||||
action["domain"] = [("id", "in", invoices.ids)]
|
||||
elif len(invoices) == 1:
|
||||
action["views"] = [(self.env.ref("account.move_form").id, "form")]
|
||||
action["res_id"] = invoices.ids[0]
|
||||
else:
|
||||
action = {"type": "ir.actions.act_window_close"}
|
||||
return action
|
||||
|
||||
def action_view_stock_pickings(self):
|
||||
pickings = self.mapped("stock_picking_ids")
|
||||
return {
|
||||
"name": "Pickings",
|
||||
"view_mode": "tree,form",
|
||||
"res_model": "stock.picking",
|
||||
"domain": [("id", "in", pickings.ids)],
|
||||
"res_id": self.id,
|
||||
"type": "ir.actions.act_window",
|
||||
}
|
||||
|
||||
def cancel(self):
|
||||
inv_obj = self.env["account.move"]
|
||||
order_id = self.env["product.order.line"].browse(int(self.id)).product_order_id
|
||||
self = order_id
|
||||
for contract in order_id.contract_ids:
|
||||
for cancel_policy in contract.cancel_policy_ids:
|
||||
if cancel_policy.from_date and cancel_policy.to_date:
|
||||
if (
|
||||
date.today() >= cancel_policy.from_date
|
||||
and date.today() <= cancel_policy.to_date
|
||||
):
|
||||
invoice_browse = (
|
||||
self.env["account.move"]
|
||||
.sudo()
|
||||
.search(
|
||||
[
|
||||
("contract_id", "=", contract.id),
|
||||
("type", "=", "out_invoice"),
|
||||
]
|
||||
)
|
||||
)
|
||||
for each_invoice in invoice_browse:
|
||||
if each_invoice.state == "draft":
|
||||
invoice_line_data = []
|
||||
invoice_line_data.append(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product_name.id,
|
||||
"name": "Cancel Policy " + self.res_number,
|
||||
"account_id": self.customer_name.property_account_receivable_id.id,
|
||||
"price_unit": (
|
||||
contract.total_amount
|
||||
* cancel_policy.policy_charged
|
||||
)
|
||||
/ 100,
|
||||
"quantity": 1,
|
||||
},
|
||||
)
|
||||
)
|
||||
invoice = inv_obj.create(
|
||||
{
|
||||
"name": self.res_number,
|
||||
"origin": self.res_number,
|
||||
"partner_id": self.customer_name.id,
|
||||
"type": "out_invoice",
|
||||
"date_invoice": date.today(),
|
||||
"reference": False,
|
||||
"account_id": self.customer_name.property_account_receivable_id.id,
|
||||
"invoice_line_ids": invoice_line_data,
|
||||
}
|
||||
)
|
||||
|
||||
elif each_invoice.state == "paid":
|
||||
invoice_line_data = []
|
||||
invoice_line_data.append(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product_name.id,
|
||||
"name": "Cancel Policy " + self.res_number,
|
||||
"account_id": self.customer_name.property_account_receivable_id.id,
|
||||
"price_unit": each_invoice.total_amount
|
||||
- (
|
||||
(
|
||||
contract.total_amount
|
||||
* cancel_policy.policy_charged
|
||||
)
|
||||
/ 100
|
||||
),
|
||||
"quantity": 1,
|
||||
},
|
||||
)
|
||||
)
|
||||
invoice = inv_obj.create(
|
||||
{
|
||||
"name": self.res_number,
|
||||
"origin": self.res_number,
|
||||
"partner_id": self.customer_name.id,
|
||||
"type": "in_refund",
|
||||
"date_invoice": date.today(),
|
||||
"reference": False,
|
||||
"account_id": self.customer_name.property_account_receivable_id.id,
|
||||
"invoice_line_ids": invoice_line_data,
|
||||
}
|
||||
)
|
||||
|
||||
if not cancel_policy.to_date:
|
||||
if date.today() >= cancel_policy.from_date:
|
||||
invoice_browse = self.env["account.move"].search(
|
||||
[
|
||||
("contract_id", "=", contract.id),
|
||||
("type", "=", "out_invoice"),
|
||||
]
|
||||
)
|
||||
for each_invoice in invoice_browse:
|
||||
if each_invoice.state == "draft":
|
||||
each_invoice.state = "paid"
|
||||
self.state = "cancel"
|
||||
|
||||
def send_product_quote(self):
|
||||
"""
|
||||
This is Email for send quotation product order inquiry
|
||||
"""
|
||||
self.ensure_one()
|
||||
ir_model_data = self.env["ir.model.data"]
|
||||
try:
|
||||
template_id = ir_model_data.get_object_reference(
|
||||
"product_rental_bookings", "email_template_product_rental"
|
||||
)[1]
|
||||
except ValueError:
|
||||
template_id = False
|
||||
try:
|
||||
compose_form_id = ir_model_data.get_object_reference(
|
||||
"mail", "email_compose_message_wizard_form"
|
||||
)[1]
|
||||
except ValueError:
|
||||
compose_form_id = False
|
||||
ctx = {
|
||||
"default_model": "rental.product.order",
|
||||
"default_res_id": self.ids[0],
|
||||
"default_use_template": bool(template_id),
|
||||
"default_template_id": template_id,
|
||||
"mark_so_as_sent": True,
|
||||
}
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"view_type": "form",
|
||||
"view_mode": "form",
|
||||
"res_model": "mail.compose.message",
|
||||
"views": [(compose_form_id, "form")],
|
||||
"view_id": compose_form_id,
|
||||
"target": "new",
|
||||
"context": ctx,
|
||||
}
|
||||
|
||||
def close(self):
|
||||
view_id = self.env.ref("rental_product_rental")
|
||||
return {
|
||||
"name": "product Service",
|
||||
"type": "ir.actions.act_window",
|
||||
"view_type": "form",
|
||||
"view_mode": "form",
|
||||
"res_model": "wizard.product.service",
|
||||
"view_id": view_id.id,
|
||||
"target": "new",
|
||||
}
|
||||
|
||||
@api.model
|
||||
def start_end_date_global(self, start, end):
|
||||
tz = pytz.utc
|
||||
current_time = datetime.now(tz)
|
||||
hour_tz = int(str(current_time)[-5:][:2])
|
||||
min_tz = int(str(current_time)[-5:][3:])
|
||||
sign = str(current_time)[-6][:1]
|
||||
sdate = str(start)
|
||||
edate = str(end)
|
||||
|
||||
if sign == "+":
|
||||
start_date = (
|
||||
datetime.strptime(sdate, "%Y-%m-%d %H:%M:%S")
|
||||
+ timedelta(hours=hour_tz, minutes=min_tz)
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
end_date = (
|
||||
datetime.strptime(edate, "%Y-%m-%d %H:%M:%S")
|
||||
+ timedelta(hours=hour_tz, minutes=min_tz)
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
if sign == "-":
|
||||
start_date = (
|
||||
datetime.strptime(sdate, "%Y-%m-%d %H:%M:%S")
|
||||
- timedelta(hours=hour_tz, minutes=min_tz)
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
end_date = (
|
||||
datetime.strptime(edate, "%Y-%m-%d %H:%M:%S")
|
||||
- timedelta(hours=hour_tz, minutes=min_tz)
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
return start_date, end_date
|
||||
|
||||
@api.model
|
||||
def start_and_end_date_global(self, start, end):
|
||||
tz = pytz.timezone(self.env.user.tz) or "UTC"
|
||||
current_time = datetime.now(tz)
|
||||
hour_tz = int(str(current_time)[-5:][:2])
|
||||
min_tz = int(str(current_time)[-5:][3:])
|
||||
sign = str(current_time)[-6][:1]
|
||||
sdate = str(start)
|
||||
edate = str(end)
|
||||
|
||||
if sign == "-":
|
||||
start_date = (
|
||||
datetime.strptime(sdate.split(".")[0], "%Y-%m-%d %H:%M:%S")
|
||||
+ timedelta(hours=hour_tz, minutes=min_tz)
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
end_date = (
|
||||
datetime.strptime(edate.split(".")[0], "%Y-%m-%d %H:%M:%S")
|
||||
+ timedelta(hours=hour_tz, minutes=min_tz)
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
if sign == "+":
|
||||
start_date = (
|
||||
datetime.strptime(sdate.split(".")[0], "%Y-%m-%d %H:%M:%S")
|
||||
- timedelta(hours=hour_tz, minutes=min_tz)
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
end_date = (
|
||||
datetime.strptime(edate.split(".")[0], "%Y-%m-%d %H:%M:%S")
|
||||
- timedelta(hours=hour_tz, minutes=min_tz)
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
return start_date, end_date
|
||||
|
||||
@api.model
|
||||
def utc_to_tz(self, start, end):
|
||||
start_date = pytz.utc.localize(
|
||||
datetime.strptime(start, "%Y-%m-%d %H:%M:%S")
|
||||
).astimezone(pytz.timezone(self.env.user.tz))
|
||||
|
||||
end_date = pytz.utc.localize(
|
||||
datetime.strptime(end, "%Y-%m-%d %H:%M:%S")
|
||||
).astimezone(pytz.timezone(self.env.user.tz))
|
||||
|
||||
return start_date, end_date
|
||||
|
||||
@api.model
|
||||
def get_booking_data(self, model_id):
|
||||
resourcelist = []
|
||||
eventlist = []
|
||||
id_list = []
|
||||
product_booking = self.env["rental.product.order"].search(
|
||||
[("state", "in", ["confirm"])]
|
||||
)
|
||||
categ_ids = self.env["product.category"].search(
|
||||
[("parent_id", "child_of", int(model_id))]
|
||||
)
|
||||
for data in product_booking:
|
||||
if data.product_order_lines_ids and data.from_date.date() >= date.today():
|
||||
for line in data.product_order_lines_ids:
|
||||
if (
|
||||
line.product_id.categ_id.id in categ_ids.ids
|
||||
and line.product_id.id not in id_list
|
||||
):
|
||||
resourcelist.append(
|
||||
{
|
||||
"id": line.product_id.id,
|
||||
"building": line.product_id.categ_id.name,
|
||||
"title": line.product_id.name,
|
||||
"type": line.product_id.categ_id.id or False,
|
||||
"product_id": line.product_id.id,
|
||||
}
|
||||
)
|
||||
id_list.append(line.product_id.id)
|
||||
if line.product_id.categ_id.id in categ_ids.ids:
|
||||
if data.start_date and data.end_date:
|
||||
start_date, end_date = self.utc_to_tz(
|
||||
data.start_date, data.end_date
|
||||
)
|
||||
start = (
|
||||
str(start_date.date()) + "T" + str(start_date.time())
|
||||
)
|
||||
end = str(end_date.date()) + "T" + str(end_date.time())
|
||||
else:
|
||||
start_date, end_date = self.utc_to_tz(
|
||||
data.start_date, data.end_date
|
||||
)
|
||||
start = (
|
||||
str(start_date.date()) + "T" + str(start_date.time())
|
||||
)
|
||||
end = str(end_date.date()) + "T" + str(end_date.time())
|
||||
eventlist.append(
|
||||
{
|
||||
"id": line.id,
|
||||
"line_id": line.id,
|
||||
"resourceId": line.product_id.id,
|
||||
"start": start,
|
||||
"end": end,
|
||||
"title": data.res_number,
|
||||
"type": model_id or False,
|
||||
"product_id": line.product_id.id,
|
||||
}
|
||||
)
|
||||
product_model = self.env["product.product"].search([("is_rental", "=", True)])
|
||||
for product in product_model:
|
||||
if product.categ_id.id in categ_ids.ids and product.id not in id_list:
|
||||
id_list.append(product.id)
|
||||
resourcelist.append(
|
||||
{
|
||||
"id": product.id,
|
||||
"building": product.categ_id.name,
|
||||
"title": product.name,
|
||||
"type": product.categ_id.id or False,
|
||||
"product_id": product.id,
|
||||
}
|
||||
)
|
||||
if not resourcelist:
|
||||
eventlist = []
|
||||
return [resourcelist, eventlist]
|
||||
|
||||
@api.model
|
||||
def remove_event(self, line_id):
|
||||
record_line_id = self.env["product.order.line"].browse(int(line_id))
|
||||
if len(record_line_id.product_order_id.product_order_lines_ids.ids) == 1:
|
||||
record_line_id.product_order_id.state = "cancel"
|
||||
elif len(record_line_id.product_order_id.product_order_lines_ids.ids) > 1:
|
||||
record_line_id.unlink()
|
||||
|
||||
|
||||
class productOrderLine(models.Model):
|
||||
_name = "product.order.line"
|
||||
_description = "product Order Line"
|
||||
|
||||
product_order_id = fields.Many2one("rental.product.order", string="product order")
|
||||
product_id = fields.Many2one("product.product", string="product")
|
||||
price_based = fields.Selection(
|
||||
[("per_day", "Day"), ("per_hour", "Hour"), ("per_session", "Session")],
|
||||
default="per_day",
|
||||
string="Based On",
|
||||
)
|
||||
tax_id = fields.Many2many("account.tax", "product_order_tax_rel", string="Tax")
|
||||
enter_kms = fields.Float(string="KM")
|
||||
enter_hour = fields.Float(string="Hour")
|
||||
enter_days = fields.Float(string="Days")
|
||||
price = fields.Monetary(string="Price")
|
||||
total = fields.Monetary(string="Total")
|
||||
sub_total = fields.Monetary(string="Sub Total", compute="_get_subtotal", store=True)
|
||||
price_tax = fields.Float(compute="_get_subtotal", string="Taxes", store=True)
|
||||
price_total = fields.Monetary(
|
||||
compute="`_get_subtotal`", string="Total Price", store=True
|
||||
)
|
||||
name = fields.Char(string="Description")
|
||||
currency_id = fields.Many2one(
|
||||
"res.currency", related="product_order_id.currency_id"
|
||||
)
|
||||
qty_needed = fields.Integer(string="Quantity", default=1)
|
||||
|
||||
@api.onchange("price_based")
|
||||
def get_price_value(self):
|
||||
self.sub_total = 0.0
|
||||
if self.price_based == "per_day":
|
||||
self.price = self.product_id.rental_amount
|
||||
self.sub_total = (
|
||||
self.enter_days * self.qty_needed * self.product_id.rental_amount
|
||||
)
|
||||
elif self.price_based == "per_hour":
|
||||
self.price = self.product_id.rental_amount_per_hour
|
||||
self.sub_total = (
|
||||
self.enter_hour
|
||||
* self.qty_needed
|
||||
* self.product_id.rental_amount_per_hour
|
||||
)
|
||||
else:
|
||||
self.price = self.product_id.rental_amount_per_session
|
||||
self.sub_total = (
|
||||
self.enter_hour
|
||||
* self.qty_needed
|
||||
* self.product_id.rental_amount_per_session
|
||||
)
|
||||
|
||||
@api.onchange("product_id")
|
||||
def get_line_value(self):
|
||||
if self.product_order_id.from_date and self.product_order_id.to_date:
|
||||
if self.price_based == "per_hour":
|
||||
self.price = self.product_id.rental_amount_per_hour
|
||||
elif self.price_based == "per_day":
|
||||
self.price = self.product_id.rental_amount
|
||||
else:
|
||||
self.price = self.product_id.rental_amount_per_session
|
||||
|
||||
else:
|
||||
raise ValidationError(_("Please Select From date Or to date!!!"))
|
||||
|
||||
@api.depends("product_id", "price", "tax_id")
|
||||
def _get_subtotal(self):
|
||||
for line in self:
|
||||
if line.price_based == "per_day":
|
||||
qty = line.enter_days * line.qty_needed
|
||||
elif line.price_based == "per_session":
|
||||
qty = line.qty_needed
|
||||
else:
|
||||
qty = line.enter_hour * line.qty_needed
|
||||
taxes = line.tax_id.compute_all(
|
||||
qty, line.product_order_id.currency_id, line.price
|
||||
)
|
||||
line.update(
|
||||
{
|
||||
"price_tax": sum(
|
||||
t.get("amount", 0.0) for t in taxes.get("taxes", [])
|
||||
),
|
||||
"price_total": taxes["total_included"],
|
||||
"sub_total": taxes["total_excluded"],
|
||||
}
|
||||
)
|
||||
line.get_line_value()
|
||||
|
||||
|
||||
class WizardProductServiceModel(models.TransientModel):
|
||||
_name = "wizard.product.service"
|
||||
_description = "product Service"
|
||||
_inherit = "rental.product.order"
|
||||
|
||||
is_damaged = fields.Boolean(string="Is Damaged")
|
||||
service_location_id = fields.Many2one("stock.location", string="Service Location")
|
||||
product_location_id = fields.Many2one("stock.location", string="Product Location")
|
||||
|
||||
def confirm_service(self):
|
||||
self.state = "confirm"
|
45
product_rental_bookings/models/res_config_settings.py
Executable file
45
product_rental_bookings/models/res_config_settings.py
Executable file
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
enabled_day_rent = fields.Boolean(string="Day Rent")
|
||||
enabled_hour_rent = fields.Boolean(string="Hour Rent")
|
||||
enabled_session_rent = fields.Boolean(string="Session Rent")
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
res = super(ResConfigSettings, self).get_values()
|
||||
get_param = self.env["ir.config_parameter"].sudo().get_param
|
||||
res.update(
|
||||
enabled_day_rent=get_param("enabled_day_rent"),
|
||||
enabled_hour_rent=get_param("enabled_hour_rent"),
|
||||
enabled_session_rent=get_param("enabled_session_rent"),
|
||||
)
|
||||
return res
|
||||
|
||||
def set_values(self):
|
||||
self.env["ir.config_parameter"].sudo().set_param(
|
||||
"enabled_day_rent", self.enabled_day_rent
|
||||
)
|
||||
self.env["ir.config_parameter"].sudo().set_param(
|
||||
"enabled_hour_rent", self.enabled_hour_rent
|
||||
)
|
||||
self.env["ir.config_parameter"].sudo().set_param(
|
||||
"enabled_session_rent", self.enabled_session_rent
|
||||
)
|
||||
res = super(ResConfigSettings, self).set_values()
|
||||
ICPSudo = self.env["ir.config_parameter"].sudo()
|
||||
ICPSudo.set_param(
|
||||
"product_rental_bookings.enabled_day_rent", self.enabled_day_rent
|
||||
)
|
||||
ICPSudo.set_param(
|
||||
"product_rental_bookings.enabled_day_rent", self.enabled_hour_rent
|
||||
)
|
||||
ICPSudo.set_param(
|
||||
"product_rental_bookings.enabled_day_rent", self.enabled_session_rent
|
||||
)
|
||||
return res
|
27
product_rental_bookings/models/session_config_settings.py
Normal file
27
product_rental_bookings/models/session_config_settings.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class SessionConfigSettings(models.Model):
|
||||
_name = "session.config"
|
||||
_description = "Session configuration settings"
|
||||
|
||||
name = fields.Char("Name")
|
||||
start_time = fields.Float("Start Time")
|
||||
end_time = fields.Float("End Time")
|
||||
|
||||
@api.constrains("start_time", "end_time")
|
||||
def _check_time_constraint(self):
|
||||
for record in self:
|
||||
if (
|
||||
record.start_time < 0.0
|
||||
or 24.0 < record.start_time
|
||||
or record.end_time < 0.0
|
||||
or 24.0 < record.end_time
|
||||
):
|
||||
raise ValidationError(_("Start time or End time is wrong"))
|
||||
elif record.start_time > record.end_time:
|
||||
raise ValidationError(_("Start time must be smaller than End time"))
|
10
product_rental_bookings/models/stock_move.py
Normal file
10
product_rental_bookings/models/stock_move.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = "stock.move"
|
||||
|
||||
product_move_id = fields.Many2one("stock.picking", string="Product Move")
|
||||
products_checked = fields.Boolean(string="Select Products")
|
163
product_rental_bookings/models/stock_picking.py
Normal file
163
product_rental_bookings/models/stock_picking.py
Normal file
@@ -0,0 +1,163 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import Form
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit = "stock.picking"
|
||||
|
||||
product_order_rel_id = fields.Many2one(
|
||||
"rental.product.order", string="product Order"
|
||||
)
|
||||
contract_ids = fields.One2many(
|
||||
"rental.product.contract", "picking_id", string="Contract Id"
|
||||
)
|
||||
is_rental = fields.Boolean(string="Rental Move")
|
||||
product_move_line_id = fields.One2many(
|
||||
"stock.move", "product_move_id", string="Move Lines"
|
||||
)
|
||||
rental_move_type = fields.Selection(
|
||||
[("outgoing", "Customers"), ("incoming", "Return"), ("internal", "Internal")],
|
||||
string="Types of Operation",
|
||||
required=True,
|
||||
default="outgoing",
|
||||
)
|
||||
|
||||
def delivery(self):
|
||||
move_ids_without_package = []
|
||||
if all([each.products_checked for each in self.move_ids_without_package]):
|
||||
self.action_confirm()
|
||||
self.action_assign()
|
||||
res = self.button_validate()
|
||||
if isinstance(res, bool):
|
||||
pass
|
||||
else:
|
||||
Form(
|
||||
self.env["stock.immediate.transfer"].with_context(res["context"])
|
||||
).save().process()
|
||||
|
||||
for each in self.move_ids_without_package.filtered(
|
||||
lambda l: l.products_checked
|
||||
):
|
||||
move_ids_without_package.append(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": each.product_id.id,
|
||||
"products_checked": each.products_checked,
|
||||
"name": each.product_id.name,
|
||||
"product_uom": each.product_uom.id,
|
||||
"product_uom_qty": each.product_uom_qty,
|
||||
"location_id": self.env.ref(
|
||||
"stock.stock_location_customers"
|
||||
).id,
|
||||
"location_dest_id": each.picking_id.location_id.id,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
stock_picking_receipt = self.env["stock.picking"].create(
|
||||
{
|
||||
"partner_id": self.partner_id.id,
|
||||
"location_id": self.env.ref("stock.stock_location_customers").id,
|
||||
"rental_move_type": "incoming",
|
||||
"location_dest_id": self.location_id.id,
|
||||
"product_order_rel_id": self.product_order_rel_id.id,
|
||||
"picking_type_id": self.picking_type_id.id,
|
||||
"is_rental": True,
|
||||
"origin": self.origin,
|
||||
"move_ids_without_package": move_ids_without_package,
|
||||
}
|
||||
)
|
||||
stock_picking_receipt.state = "confirmed"
|
||||
return stock_picking_receipt
|
||||
elif any([each.products_checked for each in self.move_ids_without_package]):
|
||||
deliver_move_id = self.copy()
|
||||
for each in self.move_ids_without_package:
|
||||
if not each.product_id:
|
||||
self.unlink()
|
||||
for each in self.product_move_line_id.filtered(
|
||||
lambda l: l.products_checked
|
||||
):
|
||||
move_ids_without_package.append(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": each.product_id.id,
|
||||
"products_checked": each.products_checked,
|
||||
"name": each.product_id.name,
|
||||
"product_uom_qty": each.product_uom_qty,
|
||||
"product_uom": each.product_uom.id,
|
||||
"location_id": each.picking_id.location_id.id,
|
||||
"location_dest_id": self.env.ref(
|
||||
"stock.stock_location_customers"
|
||||
).id,
|
||||
},
|
||||
)
|
||||
)
|
||||
each.unlink()
|
||||
deliver_move_id.write(
|
||||
{
|
||||
"rental_move_type": "incoming",
|
||||
"state": "confirmed",
|
||||
"move_ids_without_package": move_ids_without_package,
|
||||
}
|
||||
)
|
||||
return deliver_move_id
|
||||
else:
|
||||
raise UserError(_("Please Select Some Product to Move"))
|
||||
|
||||
def incoming(self):
|
||||
self.write({"rental_move_type": "incoming"})
|
||||
product_order_id = []
|
||||
for each in self.move_ids_without_package:
|
||||
product_order_id.append(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": each.product_id.id,
|
||||
"products_checked": each.products_checked,
|
||||
"name": each.product_id.name,
|
||||
"product_uom": each.product_uom.id,
|
||||
"location_id": each.picking_id.location_id.id,
|
||||
"location_dest_id": self.env.ref(
|
||||
"stock.stock_location_customers"
|
||||
).id,
|
||||
},
|
||||
)
|
||||
)
|
||||
self.env["rental.product.logs"].create(
|
||||
{
|
||||
"customer_id": self.partner_id.id,
|
||||
"product_id": each.product_id.id,
|
||||
"from_date": self.scheduled_date,
|
||||
"to_date": self.scheduled_date,
|
||||
}
|
||||
)
|
||||
|
||||
order_id = self.env["rental.product.order"].search(
|
||||
[("res_number", "=", self.origin)]
|
||||
)
|
||||
for each_order in order_id:
|
||||
each_order.state = "close"
|
||||
each_order.return_date = datetime.now()
|
||||
self.state = "assigned"
|
||||
|
||||
def move(self):
|
||||
self.state = "done"
|
||||
self.action_confirm()
|
||||
self.action_assign()
|
||||
res = self.button_validate()
|
||||
if isinstance(res, bool):
|
||||
pass
|
||||
else:
|
||||
Form(
|
||||
self.env["stock.immediate.transfer"].with_context(res["context"])
|
||||
).save().process()
|
130
product_rental_bookings/report/rental_contract_recurring.xml
Executable file
130
product_rental_bookings/report/rental_contract_recurring.xml
Executable file
@@ -0,0 +1,130 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="recurring_report">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context({'lang':doc.partner_id.lang})" />
|
||||
<div class="page">
|
||||
<div class="oe_structure" />
|
||||
<div class="row">
|
||||
<div class="col-xs-6"></div>
|
||||
<div class="col-xs-5 col-xs-offset-1">
|
||||
<strong>Customer Details:</strong>
|
||||
<div t-field="doc.partner_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}' />
|
||||
<p t-if="doc.partner_id.vat">
|
||||
<t t-esc="doc.company_id.country_id.vat_label or 'TIN'" />
|
||||
:
|
||||
<span t-field="doc.partner_id.vat" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<h2>
|
||||
<center>
|
||||
<span>Recurring Invoice Details :</span>
|
||||
</center>
|
||||
</h2>
|
||||
<h4>
|
||||
<center>
|
||||
<span>Contract -</span>
|
||||
<span t-field="doc.name" />
|
||||
</center>
|
||||
</h4>
|
||||
<div class="row mt32 mb32" id="informations">
|
||||
<div t-if="doc.contract_date" class="col-xs-3" style="margin-right: 60px;">
|
||||
<strong>Date Ordered:</strong>
|
||||
<p t-field="doc.contract_date" />
|
||||
</div>
|
||||
<div t-if="doc.contractor_id" class="col-xs-3" style="margin-right: 60px;">
|
||||
<strong>Contractor:</strong>
|
||||
<p t-field="doc.contractor_id" />
|
||||
</div>
|
||||
<div name="account_payment_term" t-if="doc.account_payment_term" class="col-xs-3" style="margin-right: 60px;">
|
||||
<strong>Payment Terms:</strong>
|
||||
<p t-field="doc.account_payment_term" />
|
||||
</div>
|
||||
<div name="from_date" t-if="doc.from_date" class="col-xs-3" style="margin-right: 60px;">
|
||||
<strong>From Date:</strong>
|
||||
<p t-field="doc.from_date" />
|
||||
</div>
|
||||
<div name="to_date" t-if="doc.to_date" class="col-xs-3" style="margin-right: 60px;">
|
||||
<strong>To Date:</strong>
|
||||
<p t-field="doc.to_date" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt32 mb32" id="informations">
|
||||
<div t-if="doc.cost_frequency" class="col-xs-3" style="margin-right: 60px;">
|
||||
<strong>Interval Type:</strong>
|
||||
<p t-field="doc.cost_frequency" />
|
||||
</div>
|
||||
<div t-if="doc.cost_generated" class="col-xs-3" style="margin-right: 60px;">
|
||||
<strong>Recurring Amount:</strong>
|
||||
<p t-field="doc.cost_generated" />
|
||||
</div>
|
||||
<div t-if="doc.security_deposit" class="col-xs-3" style="margin-right: 60px;">
|
||||
<strong>Security Deposite:</strong>
|
||||
<p t-field="doc.security_deposit" />
|
||||
</div>
|
||||
<div t-if="doc.first_payment" class="col-xs-3" style="margin-right: 60px;">
|
||||
<strong>First Payment Amount:</strong>
|
||||
<p t-field="doc.first_payment" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<strong>Date</strong>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<strong>Payment State</strong>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<strong>Amount</strong>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="doc.recurring_line" t-as="line">
|
||||
<td>
|
||||
<t t-if="line.date_today">
|
||||
<span t-field="line.date_today" />
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="line.payment_info">
|
||||
<span t-field="line.payment_info" />
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<t t-if="line.recurring_amount">
|
||||
<span t-field="line.recurring_amount" />
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 pull-right">
|
||||
<table class="table table-condensed">
|
||||
<tr class="border-black">
|
||||
<td>
|
||||
<strong>Total</strong>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-field="doc.sum_cost" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="rental_recurring_report_template">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="product_rental_bookings.recurring_report" t-lang="doc.partner_id.lang" />
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
157
product_rental_bookings/report/rental_contract_report.xml
Executable file
157
product_rental_bookings/report/rental_contract_report.xml
Executable file
@@ -0,0 +1,157 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="rental_contract">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context({'lang':doc.partner_id.lang})" />
|
||||
<div class="page">
|
||||
<div class="oe_structure" />
|
||||
<div class="row">
|
||||
<div class="col-xs-6"></div>
|
||||
<div class="col-xs-5 col-xs-offset-1">
|
||||
<strong>Customer Details:</strong>
|
||||
<div t-field="doc.partner_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}' />
|
||||
<p t-if="doc.partner_id.vat">
|
||||
<t t-esc="doc.company_id.country_id.vat_label or 'TIN'" />
|
||||
:
|
||||
<span t-field="doc.partner_id.vat" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<h2>
|
||||
<center>
|
||||
<span>Contract -</span>
|
||||
<span t-field="doc.name" />
|
||||
</center>
|
||||
</h2>
|
||||
<div class="row mt32 mb32" id="informations">
|
||||
<div t-if="doc.contract_date" class="col-xs-3" style="margin-right: 60px">
|
||||
<strong>Date Ordered:</strong>
|
||||
<p t-field="doc.contract_date" />
|
||||
</div>
|
||||
<div t-if="doc.contractor_id" class="col-xs-3" style="margin-right: 60px">
|
||||
<strong>Contractor:</strong>
|
||||
<p t-field="doc.contractor_id" />
|
||||
</div>
|
||||
<div name="account_payment_term" t-if="doc.account_payment_term" class="col-xs-3" style="margin-right: 60px">
|
||||
<strong>Payment Terms:</strong>
|
||||
<p t-field="doc.account_payment_term" />
|
||||
</div>
|
||||
<div name="from_date" t-if="doc.from_date" class="col-xs-3" style="margin-right: 60px">
|
||||
<strong>From Date:</strong>
|
||||
<p t-field="doc.from_date" />
|
||||
</div>
|
||||
<div name="to_date" t-if="doc.to_date" class="col-xs-3" style="margin-right: 60px">
|
||||
<strong>To Date:</strong>
|
||||
<p t-field="doc.to_date" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<strong>Description</strong>
|
||||
</th>
|
||||
<th>
|
||||
<strong>Based On</strong>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<strong>Days</strong>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<strong>Tax</strong>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<strong>Price</strong>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<strong>subtotal</strong>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="doc.product_contract_lines_ids" t-as="line">
|
||||
<td>
|
||||
<span t-field="line.product_id.name" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="line.price_based" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="line.enter_days" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="','.join(map(lambda x: x.name, line.tax_id))" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="line.price" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
|
||||
<span t-field="line.sub_total" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="row">
|
||||
<div class="col-xs-4 pull-right">
|
||||
<table class="table table-condensed">
|
||||
<tr class="border-black">
|
||||
<td>
|
||||
<strong>Subtotal</strong>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-field="doc.untaxed_amount" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Taxes</td>
|
||||
<td class="text-right">
|
||||
<span t-field="doc.taxes" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Extra Charges</td>
|
||||
<td class="text-right">
|
||||
<span t-field="doc.extra_charges" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-black">
|
||||
<td>
|
||||
<strong>Total</strong>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-field="doc.total_amount" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<p t-field="doc.terms_condition" />
|
||||
<div class="oe_structure" />
|
||||
</div>
|
||||
<div class="row" style="padding-top: 280px">
|
||||
<div class="row">
|
||||
<div style="margin-left: 40px;">
|
||||
<strong>Contractor Signature</strong>
|
||||
<br />
|
||||
<img t-att-src="'data:image/png;base64,%s' % to_text(doc.signature_contractor)" height="70" width="100" />
|
||||
|
||||
</div>
|
||||
<div style="margin-left: 480px;">
|
||||
<strong>Customer Signature</strong>
|
||||
<br />
|
||||
<img t-att-src="'data:image/png;base64,%s' % to_text(doc.signature_customer)" height="70" width="100" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="rental_contract_report_template">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="product_rental_bookings.rental_contract" t-lang="doc.partner_id.lang" />
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
38
product_rental_bookings/report/rental_order.xml
Executable file
38
product_rental_bookings/report/rental_order.xml
Executable file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="product_rental_order_report" model="ir.actions.report">
|
||||
<field name="name">Rental/Order</field>
|
||||
<field name="model">rental.product.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">product_rental_bookings.rental_order_template</field>
|
||||
<field name="report_file">product_rental_bookings.rental_order_report</field>
|
||||
<field name="print_report_name">'Rental/Order'</field>
|
||||
<field name="binding_model_id" ref="model_rental_product_order" />
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
<record id="product_rental_contract_report" model="ir.actions.report">
|
||||
<field name="name">Rental Contract</field>
|
||||
<field name="model">rental.product.contract</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">product_rental_bookings.rental_contract_report_template</field>
|
||||
<field name="report_file">product_rental_bookings.rental_contract_report</field>
|
||||
<field name="print_report_name">'Rental Contract'</field>
|
||||
<field name="binding_model_id" ref="model_rental_product_contract" />
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
<record id="product_contract_recurring_report" model="ir.actions.report">
|
||||
<field name="name">Recurring Invoice</field>
|
||||
<field name="model">rental.product.contract</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">product_rental_bookings.rental_recurring_report_template</field>
|
||||
<field name="report_file">product_rental_bookings.rental_contract_recurring</field>
|
||||
<field name="print_report_name">'Recurring Invoice'</field>
|
||||
<field name="binding_model_id" ref="model_rental_product_contract" />
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
143
product_rental_bookings/report/rental_order_report.xml
Executable file
143
product_rental_bookings/report/rental_order_report.xml
Executable file
@@ -0,0 +1,143 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="report_product_rental_order">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context({'lang':doc.customer_name.lang})" />
|
||||
<div class="page">
|
||||
<div class="oe_structure" />
|
||||
<div class="row">
|
||||
<div class="col-xs-6"></div>
|
||||
<div class="col-xs-5 col-xs-offset-1">
|
||||
<strong>Customer Details:</strong>
|
||||
<div t-field="doc.customer_name" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}' />
|
||||
<p t-if="doc.customer_name.vat">
|
||||
<t t-esc="doc.company_id.country_id.vat_label or 'TIN'" />
|
||||
:
|
||||
<span t-field="doc.customer_name.vat" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<h2>
|
||||
<center>
|
||||
<span>Rental Order -</span>
|
||||
<span t-field="doc.res_number" />
|
||||
</center>
|
||||
</h2>
|
||||
|
||||
<div class="row mt32 mb32" id="informations">
|
||||
<div t-if="doc.book_date" class="col-xs-6" style="margin-right: 60px">
|
||||
<strong>Date Ordered:</strong>
|
||||
<p t-field="doc.book_date" />
|
||||
</div>
|
||||
<div t-if="doc.user_id" class="col-xs-6" style="margin-right: 60px">
|
||||
<strong>Salesperson:</strong>
|
||||
<p t-field="doc.user_id" />
|
||||
</div>
|
||||
<div name="account_payment_term" t-if="doc.account_payment_term" class="col-xs-6" style="margin-right: 60px">
|
||||
<strong>Payment Terms:</strong>
|
||||
<p t-field="doc.account_payment_term" />
|
||||
</div>
|
||||
<div name="from_date" t-if="doc.from_date" class="col-xs-6" style="margin-right: 60px">
|
||||
<strong>From Date:</strong>
|
||||
<p t-field="doc.from_date" />
|
||||
</div>
|
||||
<div name="to_date" t-if="doc.to_date" class="col-xs-6" style="margin-right: 60px">
|
||||
<strong>To Date:</strong>
|
||||
<p t-field="doc.to_date" />
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<strong>Description</strong>
|
||||
</th>
|
||||
<th>
|
||||
<strong>Based On</strong>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<strong>Days</strong>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<strong>Tax</strong>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<strong>Price</strong>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<strong>subtotal</strong>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="doc.product_order_lines_ids" t-as="line">
|
||||
<td>
|
||||
<span t-field="line.product_id.name" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="line.price_based" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="line.enter_days" />
|
||||
</td>
|
||||
|
||||
<td class="text-center">
|
||||
<span t-esc="','.join(map(lambda x: x.name, line.tax_id))" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="line.price" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="line.sub_total" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="row">
|
||||
<div class="col-xs-4 pull-right">
|
||||
<table class="table table-condensed">
|
||||
<tr class="border-black">
|
||||
<td>
|
||||
<strong>Subtotal</strong>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-field="doc.untaxed_amount" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Taxes</td>
|
||||
<td class="text-right">
|
||||
<span t-field="doc.taxes" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Extra Charges</td>
|
||||
<td class="text-right">
|
||||
<span t-field="doc.extra_charges" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-black">
|
||||
<td>
|
||||
<strong>Total</strong>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-field="doc.total_amount" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<p t-field="doc.terms_condition" />
|
||||
<div class="oe_structure" />
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="rental_order_template">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="product_rental_bookings.report_product_rental_order" t-lang="doc.customer_name
|
||||
.lang" />
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
24
product_rental_bookings/security/ir.model.access.csv
Executable file
24
product_rental_bookings/security/ir.model.access.csv
Executable file
@@ -0,0 +1,24 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_rental_product_order,access_rental_product_order,model_rental_product_order,,1,1,1,1
|
||||
access_product_book,access_product_book,model_product_booking,,1,1,1,1
|
||||
access_product_order_line,access_product_order_line,model_product_order_line,,1,1,1,1
|
||||
access_rental_product_contract,access_rental_product_contract,model_rental_product_contract,,1,1,1,1
|
||||
access_product_contract_lines,access_product_contract_lines,model_product_contract_lines,base.group_user,1,1,1,1
|
||||
access_rental_product_operation,access_rental_product_operation,model_rental_product_operation,,1,1,1,1
|
||||
access_product_rental_line,access_product_rental_line,model_product_rental_line,,1,1,1,1
|
||||
access_rental_product_logs,access_rental_product_logs,model_rental_product_logs,,1,1,1,1
|
||||
access_customer_document,access_customer_document,model_customer_document,base.group_user,1,1,1,1
|
||||
access_rental_policy,access_rental_policy,model_rental_policy,base.group_user,1,1,1,1
|
||||
access_wizard_product_service,access_wizard_product_service,model_wizard_product_service,base.group_user,1,1,1,1
|
||||
access_product_advance_payment_invoice,access_product_advance_payment_invoice,model_product_advance_payment_invoice,base.group_user,1,1,1,1
|
||||
access_product_category_portal_user,access_product_category_portal_user,product.model_product_category,base.group_portal,1,1,1,1
|
||||
access_product_category_public,access_product_category_public,product.model_product_category,base.group_public,1,0,1,0
|
||||
access_product_product_portal,access_product_product_portal,product.model_product_product,base.group_portal,1,0,1,0
|
||||
access_product_product_public,access_product_product_public,product.model_product_product,base.group_public,1,0,1,0
|
||||
access_product_template_portal,access_product_template_portal,product.model_product_template,base.group_portal,1,0,1,0
|
||||
access_product_template_public,access_product_template_public,product.model_product_template,base.group_public,1,0,1,0
|
||||
access_ir_sequence_group_public,access_ir_sequence group_public,base.model_ir_sequence,base.group_public,1,0,1,0
|
||||
access_ir_sequence_group_portal,access_ir_sequence_group_portal,base.model_ir_sequence,,base.group_portal,1,0,1,0
|
||||
access_stock_quant_group_public,access_stock_quant group_public,stock.model_stock_quant,base.group_public,1,0,1,0
|
||||
access_stock_quant_group_portal,access_stock_quant_group_portal,stock.model_stock_quant,,base.group_portal,1,0,1,0
|
||||
access_session_config,access_session_config,model_session_config,base.group_user,1,1,1,1
|
Can't render this file because it has a wrong number of fields in line 21.
|
34
product_rental_bookings/security/security.xml
Executable file
34
product_rental_bookings/security/security.xml
Executable file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
|
||||
<record id="product_contract_rule" model="ir.rule">
|
||||
<field name="name">product Contract Rule</field>
|
||||
<field name="model_id" ref="model_rental_product_contract" />
|
||||
<field eval="True" name="global" />
|
||||
<field name="domain_force">['|',('company_id','=',user.company_id.id),('company_id','=',False)]</field>
|
||||
</record>
|
||||
|
||||
<record id="rental_product_rule" model="ir.rule">
|
||||
<field name="name">product product</field>
|
||||
<field name="model_id" ref="model_product_product" />
|
||||
<field eval="True" name="global" />
|
||||
<field name="domain_force">['|',('company_id','=',user.company_id.id),('company_id','=',False)]</field>
|
||||
</record>
|
||||
|
||||
<record id="product_order_rule" model="ir.rule">
|
||||
<field name="name">product Order Rule</field>
|
||||
<field name="model_id" ref="model_rental_product_order" />
|
||||
<field eval="True" name="global" />
|
||||
<field name="domain_force">['|',('company_id','=',user.company_id.id),('company_id','=',False)]</field>
|
||||
</record>
|
||||
|
||||
<record id="product_log_rule" model="ir.rule">
|
||||
<field name="name">product Log Rule</field>
|
||||
<field name="model_id" ref="model_rental_product_logs" />
|
||||
<field eval="True" name="global" />
|
||||
<field name="domain_force">['|',('company_id','=',user.company_id.id),('company_id','=',False)]</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
BIN
product_rental_bookings/static/.DS_Store
vendored
Normal file
BIN
product_rental_bookings/static/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
product_rental_bookings/static/description/icon.png
Normal file
BIN
product_rental_bookings/static/description/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
4
product_rental_bookings/static/src/css/backend.css
Executable file
4
product_rental_bookings/static/src/css/backend.css
Executable file
@@ -0,0 +1,4 @@
|
||||
.model-body-sign .canvas-container{
|
||||
border: 2px dotted #999999;
|
||||
margin: 0 auto;
|
||||
}
|
1403
product_rental_bookings/static/src/css/fullcalendar.css
Executable file
1403
product_rental_bookings/static/src/css/fullcalendar.css
Executable file
File diff suppressed because it is too large
Load Diff
463
product_rental_bookings/static/src/css/scheduler.css
Executable file
463
product_rental_bookings/static/src/css/scheduler.css
Executable file
@@ -0,0 +1,463 @@
|
||||
/*!
|
||||
* FullCalendar Scheduler v1.9.4
|
||||
* Docs & License: https://fullcalendar.io/scheduler/
|
||||
* (c) 2018 Adam Shaw
|
||||
*/
|
||||
/* TODO: break this file up */
|
||||
/* Scroller
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
.fc-scroller-clip {
|
||||
overflow: hidden;
|
||||
/* for clipping scrollbars */
|
||||
position: relative;
|
||||
/* so things like scrollfollowers can attach to this */ }
|
||||
|
||||
/* supresses rendering of native scrollbars */
|
||||
/* on .fc-scroller */
|
||||
.fc-no-scrollbars {
|
||||
background: rgba(255, 255, 255, 0);
|
||||
/* hack for dynamic DOM nodes (in Chrome OSX at least) */ }
|
||||
|
||||
.fc-no-scrollbars::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0; }
|
||||
|
||||
.fc-scroller-canvas {
|
||||
position: relative;
|
||||
/* origin for bg */
|
||||
box-sizing: border-box;
|
||||
/* so that padding (for gutter) will be part of height */
|
||||
min-height: 100%; }
|
||||
|
||||
.fc-scroller-canvas > .fc-bg {
|
||||
z-index: 1;
|
||||
/* make default? */ }
|
||||
|
||||
.fc-scroller-canvas > .fc-content {
|
||||
z-index: 2;
|
||||
/* make default? */
|
||||
position: relative;
|
||||
/* origin for inner content */
|
||||
border-style: solid;
|
||||
border-width: 0; }
|
||||
|
||||
/* for themed, hard to get the border-color, so just forget it (REVISIT) */
|
||||
.ui-widget .fc-scroller-canvas > .fc-content {
|
||||
border-color: transparent; }
|
||||
|
||||
.fc-scroller-canvas.fc-gutter-left > .fc-content {
|
||||
border-left-width: 1px;
|
||||
margin-left: -1px; }
|
||||
|
||||
.fc-scroller-canvas.fc-gutter-right > .fc-content {
|
||||
border-right-width: 1px;
|
||||
margin-right: -1px; }
|
||||
|
||||
.fc-scroller-canvas.fc-gutter-top > .fc-content {
|
||||
border-top-width: 1px;
|
||||
margin-top: -1px; }
|
||||
|
||||
/* content is responsible for bottom border */
|
||||
/* View Structure
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
.fc-rtl .fc-timeline {
|
||||
direction: rtl; }
|
||||
|
||||
.fc-timeline .fc-divider {
|
||||
width: 3px;
|
||||
border-style: double;
|
||||
/* overcome neighboring borders */ }
|
||||
|
||||
.fc-timeline .fc-head > tr > .fc-divider {
|
||||
border-bottom: 0; }
|
||||
|
||||
.fc-timeline .fc-body > tr > .fc-divider {
|
||||
border-top: 0; }
|
||||
|
||||
.fc-timeline .fc-body .fc-divider.ui-widget-header {
|
||||
background-image: none; }
|
||||
|
||||
.fc-scrolled .fc-head .fc-scroller {
|
||||
z-index: 2;
|
||||
/* so drop shadow will go above body panes */ }
|
||||
|
||||
.fc-timeline.fc-scrolled .fc-head .fc-scroller {
|
||||
box-shadow: 0 3px 4px rgba(0, 0, 0, 0.075); }
|
||||
|
||||
.fc-timeline .fc-body .fc-scroller {
|
||||
z-index: 1; }
|
||||
|
||||
/*
|
||||
on most tables that expand to the edges, kill the outer border,
|
||||
because the container elements take care of it.
|
||||
example tables:
|
||||
.fc-scroller-canvas .fc-content table
|
||||
.fc-scroller-canvas .fc-bg .fc-slats table
|
||||
*/
|
||||
.fc-timeline .fc-scroller-canvas > div > table,
|
||||
.fc-timeline .fc-scroller-canvas > div > div > table {
|
||||
border-style: hidden; }
|
||||
|
||||
/*
|
||||
for resource rows (in both the spreadsheet and timeline areas),
|
||||
undo previous rule in order to always show last border.
|
||||
*/
|
||||
.fc-timeline .fc-scroller-canvas > .fc-content > .fc-rows > table {
|
||||
border-bottom-style: none; }
|
||||
|
||||
/* Table Cell Common
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
.fc-timeline th,
|
||||
.fc-timeline td {
|
||||
white-space: nowrap; }
|
||||
|
||||
.fc-timeline .fc-cell-content {
|
||||
overflow: hidden; }
|
||||
|
||||
.fc-timeline .fc-cell-text {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px; }
|
||||
|
||||
.fc-timeline .fc-col-resizer {
|
||||
cursor: col-resize; }
|
||||
|
||||
/*
|
||||
Cells at the start of a week
|
||||
TODO: figure out better styling
|
||||
|
||||
.fc-ltr .fc-timeline .fc-em-cell div {
|
||||
border-left: 3px solid #eee;
|
||||
height: 100%;
|
||||
}
|
||||
.fc-rtl .fc-timeline .fc-em-cell {
|
||||
border-right-width: 3px;
|
||||
}
|
||||
*/
|
||||
/* head */
|
||||
.fc-timeline th {
|
||||
vertical-align: middle; }
|
||||
|
||||
.fc-timeline .fc-head .fc-cell-content {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px; }
|
||||
|
||||
/* body */
|
||||
.fc-timeline .fc-body .ui-widget-content {
|
||||
background-image: none; }
|
||||
|
||||
/* Resource Area
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
.fc-resource-area {
|
||||
width: 30%; }
|
||||
|
||||
.fc-resource-area col {
|
||||
width: 40%;
|
||||
min-width: 70px;
|
||||
/* will be read by JS */ }
|
||||
|
||||
.fc-resource-area col.fc-main-col {
|
||||
width: 60%;
|
||||
/* make the first column in a nested setup bigger */ }
|
||||
|
||||
.fc-flat .fc-expander-space {
|
||||
/* fc-flat is opposite of fc-nested */
|
||||
display: none; }
|
||||
|
||||
.fc-ltr .fc-resource-area tr > * {
|
||||
text-align: left; }
|
||||
|
||||
.fc-rtl .fc-resource-area tr > * {
|
||||
text-align: right; }
|
||||
|
||||
.fc-resource-area .fc-cell-content {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px; }
|
||||
|
||||
/* head */
|
||||
.fc-resource-area .fc-super th {
|
||||
text-align: center; }
|
||||
|
||||
.fc-resource-area th > div {
|
||||
position: relative; }
|
||||
|
||||
.fc-resource-area th .fc-cell-content {
|
||||
position: relative;
|
||||
z-index: 1; }
|
||||
|
||||
.fc-resource-area th .fc-col-resizer {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 5px; }
|
||||
|
||||
.fc-ltr .fc-resource-area th .fc-col-resizer {
|
||||
right: -3px; }
|
||||
|
||||
.fc-rtl .fc-resource-area th .fc-col-resizer {
|
||||
left: -3px; }
|
||||
|
||||
/* body */
|
||||
tr.fc-collapsed > td,
|
||||
tr.fc-transitioning > td {
|
||||
/* during the transition */
|
||||
overflow: hidden;
|
||||
/* prevents absolutely-positioned events from bleeding out */ }
|
||||
|
||||
tr.fc-transitioning > td > div {
|
||||
transition: margin-top 0.2s; }
|
||||
|
||||
tr.fc-collapsed > td > div {
|
||||
margin-top: -10px; }
|
||||
|
||||
.fc-body .fc-resource-area .fc-cell-content {
|
||||
/* might BE the cell */
|
||||
position: relative;
|
||||
/* optimization for ScrollFollower */
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px; }
|
||||
|
||||
.fc-no-overlap .fc-body .fc-resource-area .fc-cell-content {
|
||||
/* might BE the cell */
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px; }
|
||||
|
||||
.fc-resource-area .fc-icon {
|
||||
/* the expander and spacers before the expander */
|
||||
width: 1em;
|
||||
/* ensure constant width, esp for empty icons */
|
||||
font-size: .9em;
|
||||
vertical-align: middle;
|
||||
margin-top: -1%; }
|
||||
|
||||
.fc-resource-area .fc-expander {
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
/* for the icon within */ }
|
||||
|
||||
/* Time Area
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
.fc-time-area col {
|
||||
min-width: 2.2em;
|
||||
/* detected by JS */ }
|
||||
|
||||
/* head */
|
||||
.fc-ltr .fc-time-area .fc-chrono th {
|
||||
text-align: left; }
|
||||
|
||||
.fc-rtl .fc-time-area .fc-chrono th {
|
||||
text-align: right; }
|
||||
|
||||
/* body slats (vertical lines) */
|
||||
.fc-time-area .fc-slats {
|
||||
/* fc-bg is responsible for a lot of this now! */
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0; }
|
||||
|
||||
.fc-time-area .fc-slats table {
|
||||
height: 100%; }
|
||||
|
||||
.fc-time-area .fc-slats .fc-minor {
|
||||
border-style: dotted; }
|
||||
|
||||
.fc-time-area .fc-slats td {
|
||||
border-width: 0 1px;
|
||||
/* need to do this. sometimes -1 margin wouldn't hide the dotted */ }
|
||||
|
||||
.fc-ltr .fc-time-area .fc-slats td {
|
||||
border-right-width: 0; }
|
||||
|
||||
.fc-rtl .fc-time-area .fc-slats td {
|
||||
border-left-width: 0; }
|
||||
|
||||
/* body content containers
|
||||
can be within rows or directly within the pane's content
|
||||
*/
|
||||
.fc-time-area .fc-bgevent-container,
|
||||
.fc-time-area .fc-highlight-container {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
/* only for directly within pane. not for row. overridden later */
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 0; }
|
||||
|
||||
.fc-ltr .fc-time-area .fc-helper-container,
|
||||
.fc-ltr .fc-time-area .fc-bgevent-container,
|
||||
.fc-ltr .fc-time-area .fc-highlight-container {
|
||||
left: 0; }
|
||||
|
||||
.fc-rtl .fc-time-area .fc-helper-container,
|
||||
.fc-rtl .fc-time-area .fc-bgevent-container,
|
||||
.fc-rtl .fc-time-area .fc-highlight-container {
|
||||
right: 0; }
|
||||
|
||||
.fc-time-area .fc-bgevent,
|
||||
.fc-time-area .fc-highlight {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0; }
|
||||
|
||||
/* body resource rows */
|
||||
.fc-time-area .fc-rows {
|
||||
position: relative;
|
||||
z-index: 3; }
|
||||
|
||||
.fc-time-area .fc-rows .ui-widget-content {
|
||||
background: none; }
|
||||
|
||||
.fc-time-area .fc-rows td > div {
|
||||
position: relative; }
|
||||
|
||||
.fc-time-area .fc-rows .fc-bgevent-container,
|
||||
.fc-time-area .fc-rows .fc-highlight-container {
|
||||
z-index: 1; }
|
||||
|
||||
.fc-time-area .fc-event-container {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
/* above bgevent and highlight */
|
||||
width: 0;
|
||||
/* for event positioning. will end up on correct side based on dir */ }
|
||||
|
||||
.fc-time-area .fc-helper-container {
|
||||
/* also an fc-event-container */
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 0; }
|
||||
|
||||
.fc-time-area .fc-event-container {
|
||||
padding-bottom: 8px;
|
||||
top: -1px; }
|
||||
|
||||
.fc-time-area tr:first-child .fc-event-container {
|
||||
top: 0; }
|
||||
|
||||
.fc-no-overlap .fc-time-area .fc-event-container {
|
||||
padding-bottom: 0;
|
||||
top: 0; }
|
||||
|
||||
/* Now Indicator
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
.fc-timeline .fc-now-indicator {
|
||||
/* both the arrow and the line */
|
||||
z-index: 3;
|
||||
/* one above scroller's fc-content */
|
||||
top: 0; }
|
||||
|
||||
.fc-time-area .fc-now-indicator-arrow {
|
||||
margin: 0 -6px;
|
||||
/* 5, then one more to counteract scroller's negative margins */
|
||||
/* triangle pointing down... */
|
||||
border-width: 6px 5px 0 5px;
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent; }
|
||||
|
||||
.fc-time-area .fc-now-indicator-line {
|
||||
margin: 0 -1px;
|
||||
/* counteract scroller's negative margins */
|
||||
bottom: 0;
|
||||
border-left-width: 1px; }
|
||||
|
||||
/* Time Grid Events
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
.fc-timeline-event {
|
||||
position: absolute;
|
||||
border-radius: 0;
|
||||
padding: 2px 0;
|
||||
margin-bottom: 1px; }
|
||||
|
||||
.fc-no-overlap .fc-timeline-event {
|
||||
padding: 5px 0;
|
||||
margin-bottom: 0; }
|
||||
|
||||
/* don't overlap grid lines at the event's end */
|
||||
.fc-ltr .fc-timeline-event {
|
||||
margin-right: 1px; }
|
||||
|
||||
.fc-rtl .fc-timeline-event {
|
||||
margin-left: 1px; }
|
||||
|
||||
.fc-timeline-event .fc-content {
|
||||
padding: 0 1px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden; }
|
||||
|
||||
.fc-timeline-event .fc-time {
|
||||
font-weight: bold;
|
||||
padding: 0 1px; }
|
||||
|
||||
.fc-rtl .fc-timeline-event .fc-time {
|
||||
display: inline-block;
|
||||
/* will force it on the other side */ }
|
||||
|
||||
.fc-timeline-event .fc-title {
|
||||
position: relative;
|
||||
/* optimization for ScrollFollower */
|
||||
padding: 0 1px; }
|
||||
|
||||
.fc-timeline-event.fc-selected .fc-bg {
|
||||
display: none;
|
||||
/* hide semi-white background, to appear darker */ }
|
||||
|
||||
/* follower logic */
|
||||
.fc-ltr .fc-timeline-event .fc-title {
|
||||
padding-left: 10px;
|
||||
margin-left: -8px; }
|
||||
|
||||
.fc-rtl .fc-timeline-event .fc-title {
|
||||
padding-right: 10px;
|
||||
margin-right: -8px; }
|
||||
|
||||
.fc-ltr .fc-timeline-event.fc-not-start .fc-title {
|
||||
margin-left: -2px; }
|
||||
|
||||
.fc-rtl .fc-timeline-event.fc-not-start .fc-title {
|
||||
margin-right: -2px; }
|
||||
|
||||
.fc-timeline-event.fc-not-start .fc-title,
|
||||
.fc-body .fc-time-area .fc-following {
|
||||
position: relative; }
|
||||
|
||||
.fc-timeline-event.fc-not-start .fc-title:before,
|
||||
.fc-body .fc-time-area .fc-following:before {
|
||||
/* generic arrow */
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: -5px;
|
||||
border: 5px solid #000;
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
opacity: .5; }
|
||||
|
||||
.fc-ltr .fc-timeline-event.fc-not-start .fc-title:before,
|
||||
.fc-ltr .fc-body .fc-time-area .fc-following:before {
|
||||
/* LTR. left pointing arrow */
|
||||
border-left: 0;
|
||||
left: 2px; }
|
||||
|
||||
.fc-rtl .fc-timeline-event.fc-not-start .fc-title:before,
|
||||
.fc-rtl .fc-body .fc-time-area .fc-following:before {
|
||||
/* RTL. right pointing arrow */
|
||||
border-right: 0;
|
||||
right: 2px; }
|
||||
|
||||
/* License Message
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
.fc-license-message {
|
||||
position: absolute;
|
||||
z-index: 99999;
|
||||
bottom: 1px;
|
||||
left: 1px;
|
||||
background: #eee;
|
||||
border-color: #ddd;
|
||||
border-style: solid;
|
||||
border-width: 1px 1px 0 0;
|
||||
padding: 2px 4px;
|
||||
font-size: 12px;
|
||||
border-top-right-radius: 3px; }
|
5
product_rental_bookings/static/src/css/scheduler.min.css
vendored
Executable file
5
product_rental_bookings/static/src/css/scheduler.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
9
product_rental_bookings/static/src/js/fabric.min.js
vendored
Executable file
9
product_rental_bookings/static/src/js/fabric.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
23
product_rental_bookings/static/src/js/lib/fullcalendar.js
Executable file
23
product_rental_bookings/static/src/js/lib/fullcalendar.js
Executable file
File diff suppressed because one or more lines are too long
184
product_rental_bookings/static/src/js/lib/popper.min.js
vendored
Executable file
184
product_rental_bookings/static/src/js/lib/popper.min.js
vendored
Executable file
@@ -0,0 +1,184 @@
|
||||
function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global.Popper = factory());
|
||||
}(this, (function () { 'use strict';
|
||||
|
||||
Object.assign||Object.defineProperty(Object,'assign',{enumerable:!1,configurable:!0,writable:!0,value:function value(a){if(a===void 0||null===a)throw new TypeError('Cannot convert first argument to object');var b=Object(a);for(var c=1;c<arguments.length;c++){var d=arguments[c];if(void 0!==d&&null!==d){d=Object(d);var e=Object.keys(d);for(var f=0,g=e.length;f<g;f++){var h=e[f],j=Object.getOwnPropertyDescriptor(d,h);void 0!==j&&j.enumerable&&(b[h]=d[h]);}}}return b}});
|
||||
|
||||
if(!window.requestAnimationFrame){var lastTime=0,vendors=['ms','moz','webkit','o'];for(var x=0;x<vendors.length&&!window.requestAnimationFrame;++x)window.requestAnimationFrame=window[vendors[x]+'RequestAnimationFrame'],window.cancelAnimationFrame=window[vendors[x]+'CancelAnimationFrame']||window[vendors[x]+'CancelRequestAnimationFrame'];window.requestAnimationFrame||(window.requestAnimationFrame=function(a){var b=new Date().getTime(),c=Math.max(0,16-(b-lastTime)),d=window.setTimeout(function(){a(b+c);},c);return lastTime=b+c,d}),window.cancelAnimationFrame||(window.cancelAnimationFrame=function(a){clearTimeout(a);});}
|
||||
|
||||
function findIndex(a,b,c){var d=a.filter(function(e){return e[b]===c})[0];return a.indexOf(d)}
|
||||
|
||||
function getOffsetParent(a){var b=a.offsetParent;return b&&'BODY'!==b.nodeName?b:window.document.documentElement}
|
||||
|
||||
function getStyleComputedProperty(a,b){if(1!==a.nodeType)return[];var c=window.getComputedStyle(a,null);return c[b]}
|
||||
|
||||
function getParentNode(a){return a.parentNode||a.host}
|
||||
|
||||
function getScrollParent(a){return a===window.document?window.document.body.scrollTop?window.document.body:window.document.documentElement:-1!==['scroll','auto'].indexOf(getStyleComputedProperty(a,'overflow'))||-1!==['scroll','auto'].indexOf(getStyleComputedProperty(a,'overflow-x'))||-1!==['scroll','auto'].indexOf(getStyleComputedProperty(a,'overflow-y'))?a===window.document.body?getScrollParent(getParentNode(a)):a:getParentNode(a)?getScrollParent(getParentNode(a)):a}
|
||||
|
||||
function getOffsetRect(a){var b=window.document.documentElement,c=void 0;return c=a===b?{width:Math.max(b.clientWidth,window.innerWidth||0),height:Math.max(b.clientHeight,window.innerHeight||0),left:0,top:0}:{width:a.offsetWidth,height:a.offsetHeight,left:a.offsetLeft,top:a.offsetTop},c.right=c.left+c.width,c.bottom=c.top+c.height,c}
|
||||
|
||||
function isFixed(a){return a!==window.document.body&&(!('fixed'!==getStyleComputedProperty(a,'position'))||(getParentNode(a)?isFixed(getParentNode(a)):a))}
|
||||
|
||||
function getPosition(a){var b=getOffsetParent(a),c=isFixed(b);return c?'fixed':'absolute'}
|
||||
|
||||
function getBoundingClientRect(a){var b=a.getBoundingClientRect();return{left:b.left,top:b.top,right:b.right,bottom:b.bottom,width:b.right-b.left,height:b.bottom-b.top}}
|
||||
|
||||
function getOffsetRectRelativeToCustomParent(a,b){var c=2<arguments.length&&void 0!==arguments[2]&&arguments[2],d=3<arguments.length&&void 0!==arguments[3]&&arguments[3],e=getBoundingClientRect(a),f=getBoundingClientRect(b);if(c&&!d){var j=getScrollParent(b);f.top+=j.scrollTop,f.bottom+=j.scrollTop,f.left+=j.scrollLeft,f.right+=j.scrollLeft;}var g={top:e.top-f.top,left:e.left-f.left,bottom:e.top-f.top+e.height,right:e.left-f.left+e.width,width:e.width,height:e.height},h=b.scrollTop,i=b.scrollLeft;return g.top+=h,g.bottom+=h,g.left+=i,g.right+=i,g}
|
||||
|
||||
function getBoundaries(a,b,c){var d={},e=getOffsetParent(a),f=getScrollParent(a);if('window'===c){var g=window.document.body,h=window.document.documentElement,i=Math.max(g.scrollHeight,g.offsetHeight,h.clientHeight,h.scrollHeight,h.offsetHeight),j=Math.max(g.scrollWidth,g.offsetWidth,h.clientWidth,h.scrollWidth,h.offsetWidth);d={top:0,right:j,bottom:i,left:0};}else if('viewport'===c){var _g=getOffsetRect(e),_h=getPosition(a);d='fixed'===_h?{top:0,right:window.document.documentElement.clientWidth,bottom:window.document.documentElement.clientHeight,left:0}:{top:0-_g.top,right:window.document.documentElement.clientWidth-_g.left,bottom:window.document.documentElement.clientHeight-_g.top,left:0-_g.left};}else d=f===c||'scrollParent'===c?getOffsetRectRelativeToCustomParent(f,e):getOffsetRectRelativeToCustomParent(c,e);if(e.contains(f)){var _g2=f.scrollLeft,_h2=f.scrollTop;d.right+=_g2,d.bottom+=_h2;}return d.left+=b,d.top+=b,d.right-=b,d.bottom-=b,d}
|
||||
|
||||
function getOuterSizes(a){var b=a.style.display,c=a.style.visibility;a.style.display='block',a.style.visibility='hidden';var d=window.getComputedStyle(a),e=parseFloat(d.marginTop)+parseFloat(d.marginBottom),f=parseFloat(d.marginLeft)+parseFloat(d.marginRight),g={width:a.offsetWidth+f,height:a.offsetHeight+e};return a.style.display=b,a.style.visibility=c,g}
|
||||
|
||||
function getPopperClientRect(a){return Object.assign({},a,{right:a.left+a.width,bottom:a.top+a.height})}
|
||||
|
||||
function getSupportedPropertyName(a){var b=['','ms','webkit','moz','o'];for(var c=0;c<b.length;c++){var d=b[c]?b[c]+a.charAt(0).toUpperCase()+a.slice(1):a;if('undefined'!=typeof window.document.body.style[d])return d}return null}
|
||||
|
||||
function isFunction(a){return a&&'[object Function]'==={}.toString.call(a)}
|
||||
|
||||
function isModifierRequired(a,b,c){return!!a.filter(function(d){if(d.name===c)return!0;return d.name!==b&&!1}).length}
|
||||
|
||||
function isNumeric(a){return''!==a&&!isNaN(parseFloat(a))&&isFinite(a)}
|
||||
|
||||
function isTransformed(a){return a!==window.document.body&&('none'!==getStyleComputedProperty(a,'transform')||(getParentNode(a)?isTransformed(getParentNode(a)):a))}
|
||||
|
||||
function runModifiers(a,b,c){var d=void 0===c?a:a.slice(0,findIndex(a,'name',c));return d.forEach(function(e){e.enabled&&isFunction(e.function)&&(b=e.function(b,e));}),b}
|
||||
|
||||
function setStyle(a,b){Object.keys(b).forEach(function(c){var d='';-1!==['width','height','top','right','bottom','left'].indexOf(c)&&isNumeric(b[c])&&(d='px'),a.style[c]=b[c]+d;});}
|
||||
|
||||
var Utils = {findIndex:findIndex,getBoundaries:getBoundaries,getBoundingClientRect:getBoundingClientRect,getOffsetParent:getOffsetParent,getOffsetRectRelativeToCustomParent:getOffsetRectRelativeToCustomParent,getOuterSizes:getOuterSizes,getPopperClientRect:getPopperClientRect,getPosition:getPosition,getScrollParent:getScrollParent,getStyleComputedProperty:getStyleComputedProperty,getSupportedPropertyName:getSupportedPropertyName,isFixed:isFixed,isFunction:isFunction,isModifierRequired:isModifierRequired,isNumeric:isNumeric,isTransformed:isTransformed,runModifiers:runModifiers,setStyle:setStyle};
|
||||
|
||||
var nativeHints=['native code','[object MutationObserverConstructor]'];var isNative = (function(a){return nativeHints.some(function(b){return-1<(a||'').toString().indexOf(b)})});
|
||||
|
||||
var longerTimeoutBrowsers=['Edge','Trident','Firefox']; var timeoutDuration=0;for(var a=0;a<longerTimeoutBrowsers.length;a+=1)if(0<=navigator.userAgent.indexOf(longerTimeoutBrowsers[a])){timeoutDuration=1;break}function microtaskDebounce(a){var b=!1,c=0,d=document.createElement('span'),e=new MutationObserver(function(){a(),b=!1;});return e.observe(d,{childList:!0}),function(){b||(b=!0,d.textContent=''+c,c+=1);}}function taskDebounce(a){var b=!1;return function(){b||(b=!0,setTimeout(function(){b=!1,a();},timeoutDuration));}}var supportsNativeMutationObserver=isNative(window.MutationObserver);var debounce = supportsNativeMutationObserver?microtaskDebounce:taskDebounce;
|
||||
|
||||
function getOffsets(a,b,c,d){d=d.split('-')[0];var e={};e.position=a.position;var f='fixed'===e.position,g=a.isParentTransformed,h=getOffsetParent(f&&g?c:b),i=getOffsetRectRelativeToCustomParent(c,h,f,g),j=getOuterSizes(b);return-1===['right','left'].indexOf(d)?(e.left=i.left+i.width/2-j.width/2,e.top='top'===d?i.top-j.height:i.bottom):(e.top=i.top+i.height/2-j.height/2,e.left='left'===d?i.left-j.width:i.right),e.width=j.width,e.height=j.height,{popper:e,reference:i}}
|
||||
|
||||
function setupEventListeners(a,b,c,d){if(c.updateBound=d,window.addEventListener('resize',c.updateBound,{passive:!0}),'window'!==b.boundariesElement){var e=getScrollParent(a);(e===window.document.body||e===window.document.documentElement)&&(e=window),e.addEventListener('scroll',c.updateBound,{passive:!0}),c.scrollElement=e;}}
|
||||
|
||||
function removeEventListeners(a,b){return window.removeEventListener('resize',b.updateBound),b.scrollElement&&b.scrollElement.removeEventListener('scroll',b.updateBound),b.updateBound=null,b.scrollElement=null,b}
|
||||
|
||||
function sortModifiers(c,d){if(c.order<d.order)return-1;return c.order>d.order?1:0}
|
||||
|
||||
function applyStyle(a){var b={position:a.offsets.popper.position},c=Math.round(a.offsets.popper.left),d=Math.round(a.offsets.popper.top),e=getSupportedPropertyName('transform');return a.instance.options.gpuAcceleration&&e?(b[e]='translate3d('+c+'px, '+d+'px, 0)',b.top=0,b.left=0):(b.left=c,b.top=d),Object.assign(b,a.styles),setStyle(a.instance.popper,b),a.instance.popper.setAttribute('x-placement',a.placement),a.offsets.arrow&&setStyle(a.arrowElement,a.offsets.arrow),a}function applyStyleOnLoad(a,b,c){return b.setAttribute('x-placement',c.placement),c}
|
||||
|
||||
function arrow(a,b){var c=b.element;if('string'==typeof c&&(c=a.instance.popper.querySelector(c)),!c)return a;if(!a.instance.popper.contains(c))return console.warn('WARNING: `arrowElement` must be child of its popper element!'),a;if(!isModifierRequired(a.instance.modifiers,'arrow','keepTogether'))return console.warn('WARNING: keepTogether modifier is required by arrow modifier in order to work, be sure to include it before arrow!'),a;var d={},e=a.placement.split('-')[0],f=getPopperClientRect(a.offsets.popper),g=a.offsets.reference,h=-1!==['left','right'].indexOf(e),i=h?'height':'width',j=h?'top':'left',k=h?'left':'top',l=h?'bottom':'right',m=getOuterSizes(c)[i];g[l]-m<f[j]&&(a.offsets.popper[j]-=f[j]-(g[l]-m)),g[j]+m>f[l]&&(a.offsets.popper[j]+=g[j]+m-f[l]);var n=g[j]+g[i]/2-m/2,o=n-getPopperClientRect(a.offsets.popper)[j];return o=Math.max(Math.min(f[i]-m,o),0),d[j]=o,d[k]='',a.offsets.arrow=d,a.arrowElement=c,a}
|
||||
|
||||
function getOppositePlacement(a){var b={left:'right',right:'left',bottom:'top',top:'bottom'};return a.replace(/left|right|bottom|top/g,function(c){return b[c]})}
|
||||
|
||||
function getOppositeVariation(a){if('end'===a)return'start';return'start'===a?'end':a}
|
||||
|
||||
function flip(a,b){if(a.flipped&&a.placement===a.originalPlacement)return a;var c=getBoundaries(a.instance.popper,b.padding,b.boundariesElement),d=a.placement.split('-')[0],e=getOppositePlacement(d),f=a.placement.split('-')[1]||'',g=[];return g='flip'===b.behavior?[d,e]:b.behavior,g.forEach(function(h,i){if(d!==h||g.length===i+1)return a;d=a.placement.split('-')[0],e=getOppositePlacement(d);var j=getPopperClientRect(a.offsets.popper),k='left'===d&&Math.floor(j.left)<Math.floor(c.left)||'right'===d&&Math.floor(j.right)>Math.floor(c.right)||'top'===d&&Math.floor(j.top)<Math.floor(c.top)||'bottom'===d&&Math.floor(j.bottom)>Math.floor(c.bottom),l=-1!==['top','bottom'].indexOf(d),m=!!b.flipVariations&&(l&&'start'===f&&Math.floor(j.left)<Math.floor(c.left)||l&&'end'===f&&Math.floor(j.right)>Math.floor(c.right)||!l&&'start'===f&&Math.floor(j.top)<Math.floor(c.top)||!l&&'end'===f&&Math.floor(j.bottom)>Math.floor(c.bottom));(k||m)&&(a.flipped=!0,k&&(d=g[i+1]),m&&(f=getOppositeVariation(f)),a.placement=d+(f?'-'+f:''),a.offsets.popper=getOffsets(a.instance.state,a.instance.popper,a.instance.reference,a.placement).popper,a=runModifiers(a.instance.modifiers,a,'flip'));}),a}
|
||||
|
||||
function keepTogether(a){var b=getPopperClientRect(a.offsets.popper),c=a.offsets.reference,d=Math.floor,e=a.placement.split('-')[0];return-1===['top','bottom'].indexOf(e)?(b.bottom<d(c.top)&&(a.offsets.popper.top=d(c.top)-b.height),b.top>d(c.bottom)&&(a.offsets.popper.top=d(c.bottom))):(b.right<d(c.left)&&(a.offsets.popper.left=d(c.left)-b.width),b.left>d(c.right)&&(a.offsets.popper.left=d(c.right))),a}
|
||||
|
||||
function offset(a,b){var c=a.placement,d=a.offsets.popper,e=void 0;return isNumeric(b.offset)?e=[b.offset,0]:(e=b.offset.split(' '),e=e.map(function(f,g){var h=f.match(/(\d*\.?\d*)(.*)/),i=+h[1],j=h[2],k=-1!==c.indexOf('right')||-1!==c.indexOf('left');if(1===g&&(k=!k),'%'===j||'%r'===j){var l=getPopperClientRect(a.offsets.reference),m=void 0;return m=k?l.height:l.width,m/100*i}if('%p'===j){var _l=getPopperClientRect(a.offsets.popper),_m=void 0;return _m=k?_l.height:_l.width,_m/100*i}if('vh'===j||'vw'===j){var _l2=void 0;return _l2='vh'===j?Math.max(document.documentElement.clientHeight,window.innerHeight||0):Math.max(document.documentElement.clientWidth,window.innerWidth||0),_l2/100*i}return'px'===j?+i:+f})),-1===a.placement.indexOf('left')?-1===a.placement.indexOf('right')?-1===a.placement.indexOf('top')?-1!==a.placement.indexOf('bottom')&&(d.left+=e[0],d.top+=e[1]||0):(d.left+=e[0],d.top-=e[1]||0):(d.top+=e[0],d.left+=e[1]||0):(d.top+=e[0],d.left-=e[1]||0),a}
|
||||
|
||||
function preventOverflow(c,d){var e=d.boundariesElement||getOffsetParent(c.instance.popper),f=getBoundaries(c.instance.popper,d.padding,e);d.boundaries=f;var g=d.priority,h=getPopperClientRect(c.offsets.popper),i={left:function left(){var j=h.left;return h.left<f.left&&!shouldOverflowBoundary(c,d,'left')&&(j=Math.max(h.left,f.left)),{left:j}},right:function right(){var j=h.left;return h.right>f.right&&!shouldOverflowBoundary(c,d,'right')&&(j=Math.min(h.left,f.right-h.width)),{left:j}},top:function top(){var j=h.top;return h.top<f.top&&!shouldOverflowBoundary(c,d,'top')&&(j=Math.max(h.top,f.top)),{top:j}},bottom:function bottom(){var j=h.top;return h.bottom>f.bottom&&!shouldOverflowBoundary(c,d,'bottom')&&(j=Math.min(h.top,f.bottom-h.height)),{top:j}}};return g.forEach(function(j){c.offsets.popper=Object.assign(h,i[j]());}),c}function shouldOverflowBoundary(c,d,e){return!!d.escapeWithReference&&(c.flipped&&isSameAxis(c.originalPlacement,e)||!!isSameAxis(c.originalPlacement,e)||!0)}function isSameAxis(c,d){var e=c.split('-')[0],f=d.split('-')[0];return e===f||e===getOppositePlacement(d)}
|
||||
|
||||
function shift(a){var b=a.placement,c=b.split('-')[0],d=b.split('-')[1];if(d){var e=a.offsets.reference,f=getPopperClientRect(a.offsets.popper),g={y:{start:{top:e.top},end:{top:e.top+e.height-f.height}},x:{start:{left:e.left},end:{left:e.left+e.width-f.width}}},h=-1===['bottom','top'].indexOf(c)?'y':'x';a.offsets.popper=Object.assign(f,g[h][d]);}return a}
|
||||
|
||||
function hide(a){if(!isModifierRequired(a.instance.modifiers,'hide','preventOverflow'))return console.warn('WARNING: preventOverflow modifier is required by hide modifier in order to work, be sure to include it before hide!'),a;var b=a.offsets.reference,c=a.instance.modifiers.filter(function(d){return'preventOverflow'===d.name})[0].boundaries;if(b.bottom<c.top||b.left>c.right||b.top>c.bottom||b.right<c.left){if(!0===a.hide)return a;a.hide=!0,a.instance.popper.setAttribute('x-out-of-boundaries','');}else{if(!1===a.hide)return a;a.hide=!1,a.instance.popper.removeAttribute('x-out-of-boundaries');}return a}
|
||||
|
||||
var modifiersFunctions = {applyStyle:applyStyle,arrow:arrow,flip:flip,keepTogether:keepTogether,offset:offset,preventOverflow:preventOverflow,shift:shift,hide:hide};var modifiersOnLoad={applyStyleOnLoad:applyStyleOnLoad};
|
||||
|
||||
var classCallCheck = function (instance, Constructor) {
|
||||
if (!(instance instanceof Constructor)) {
|
||||
throw new TypeError("Cannot call a class as a function");
|
||||
}
|
||||
};
|
||||
|
||||
var createClass = function () {
|
||||
function defineProperties(target, props) {
|
||||
for (var i = 0; i < props.length; i++) {
|
||||
var descriptor = props[i];
|
||||
descriptor.enumerable = descriptor.enumerable || false;
|
||||
descriptor.configurable = true;
|
||||
if ("value" in descriptor) descriptor.writable = true;
|
||||
Object.defineProperty(target, descriptor.key, descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
return function (Constructor, protoProps, staticProps) {
|
||||
if (protoProps) defineProperties(Constructor.prototype, protoProps);
|
||||
if (staticProps) defineProperties(Constructor, staticProps);
|
||||
return Constructor;
|
||||
};
|
||||
}();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var get = function get(object, property, receiver) {
|
||||
if (object === null) object = Function.prototype;
|
||||
var desc = Object.getOwnPropertyDescriptor(object, property);
|
||||
|
||||
if (desc === undefined) {
|
||||
var parent = Object.getPrototypeOf(object);
|
||||
|
||||
if (parent === null) {
|
||||
return undefined;
|
||||
} else {
|
||||
return get(parent, property, receiver);
|
||||
}
|
||||
} else if ("value" in desc) {
|
||||
return desc.value;
|
||||
} else {
|
||||
var getter = desc.get;
|
||||
|
||||
if (getter === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getter.call(receiver);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var set = function set(object, property, value, receiver) {
|
||||
var desc = Object.getOwnPropertyDescriptor(object, property);
|
||||
|
||||
if (desc === undefined) {
|
||||
var parent = Object.getPrototypeOf(object);
|
||||
|
||||
if (parent !== null) {
|
||||
set(parent, property, value, receiver);
|
||||
}
|
||||
} else if ("value" in desc && desc.writable) {
|
||||
desc.value = value;
|
||||
} else {
|
||||
var setter = desc.set;
|
||||
|
||||
if (setter !== undefined) {
|
||||
setter.call(receiver, value);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
var DEFAULTS={placement:'bottom',gpuAcceleration:!0,modifiers:{shift:{order:100,enabled:!0,function:modifiersFunctions.shift},offset:{order:200,enabled:!0,function:modifiersFunctions.offset,offset:0},preventOverflow:{order:300,enabled:!0,function:modifiersFunctions.preventOverflow,priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,function:modifiersFunctions.keepTogether},arrow:{order:500,enabled:!0,function:modifiersFunctions.arrow,element:'[x-arrow]'},flip:{order:600,enabled:!0,function:modifiersFunctions.flip,behavior:'flip',padding:5,boundariesElement:'viewport'},hide:{order:700,enabled:!0,function:modifiersFunctions.hide},applyStyle:{order:800,enabled:!0,function:modifiersFunctions.applyStyle,onLoad:modifiersOnLoad.applyStyleOnLoad}}};var Popper=function(){function Popper(a,b){var _this=this,c=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};return classCallCheck(this,Popper),this.Defaults=DEFAULTS,this.update=debounce(this.update.bind(this)),this.scheduleUpdate=function(){return requestAnimationFrame(_this.update)},this.state={isDestroyed:!1,isCreated:!1},this.reference=a.jquery?a[0]:a,this.popper=b.jquery?b[0]:b,this.options=Object.assign({},DEFAULTS,c),this.modifiers=Object.keys(DEFAULTS.modifiers).map(function(d){return Object.assign({name:d},DEFAULTS.modifiers[d])}),this.modifiers=this.modifiers.map(function(d){var e=c.modifiers&&c.modifiers[d.name]||{},f=Object.assign({},d,e);return f}),c.modifiers&&(this.options.modifiers=Object.assign({},DEFAULTS.modifiers,c.modifiers),Object.keys(c.modifiers).forEach(function(d){if(void 0===DEFAULTS.modifiers[d]){var e=c.modifiers[d];e.name=d,_this.modifiers.push(e);}})),this.modifiers=this.modifiers.sort(sortModifiers),this.modifiers.forEach(function(d){d.enabled&&isFunction(d.onLoad)&&d.onLoad(_this.reference,_this.popper,_this.options,d);}),this.state.position=getPosition(this.reference),this.state.isParentTransformed=isTransformed(this.popper.parentNode),this.update(),setupEventListeners(this.reference,this.options,this.state,this.scheduleUpdate),this}return createClass(Popper,[{key:'update',value:function update(){var a={instance:this,styles:{},flipped:!1};this.state.position=getPosition(this.reference),setStyle(this.popper,{position:this.state.position}),this.state.isDestroyed||(a.placement=this.options.placement,a.originalPlacement=this.options.placement,a.offsets=getOffsets(this.state,this.popper,this.reference,a.placement),a=runModifiers(this.modifiers,a),this.state.isCreated?isFunction(this.state.updateCallback)&&this.state.updateCallback(a):(this.state.isCreated=!0,isFunction(this.state.createCallback)&&this.state.createCallback(a)));}},{key:'onCreate',value:function onCreate(a){return this.state.createCallback=a,this}},{key:'onUpdate',value:function onUpdate(a){return this.state.updateCallback=a,this}},{key:'destroy',value:function destroy(){return this.state.isDestroyed=!0,this.popper.removeAttribute('x-placement'),this.popper.style.left='',this.popper.style.position='',this.popper.style.top='',this.popper.style[getSupportedPropertyName('transform')]='',this.state=removeEventListeners(this.reference,this.state),this.options.removeOnDestroy&&this.popper.parentNode.removeChild(this.popper),this}}]),Popper}();Popper.Utils=Utils;
|
||||
|
||||
return Popper;
|
||||
|
||||
})));
|
9
product_rental_bookings/static/src/js/lib/scheduler.js
Executable file
9
product_rental_bookings/static/src/js/lib/scheduler.js
Executable file
File diff suppressed because one or more lines are too long
287
product_rental_bookings/static/src/js/lib/tooltip.min.js
vendored
Executable file
287
product_rental_bookings/static/src/js/lib/tooltip.min.js
vendored
Executable file
@@ -0,0 +1,287 @@
|
||||
/* ===========================================================
|
||||
* bootstrap-tooltip.js v2.2.2
|
||||
* http://twitter.github.com/bootstrap/javascript.html#tooltips
|
||||
* Inspired by the original jQuery.tipsy by Jason Frame
|
||||
* ===========================================================
|
||||
* Copyright 2012 Twitter, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* ========================================================== */
|
||||
|
||||
|
||||
!function ($) {
|
||||
|
||||
"use strict"; // jshint ;_;
|
||||
|
||||
|
||||
/* TOOLTIP PUBLIC CLASS DEFINITION
|
||||
* =============================== */
|
||||
|
||||
var Tooltip = function (element, options) {
|
||||
this.init('tooltip', element, options)
|
||||
}
|
||||
|
||||
Tooltip.prototype = {
|
||||
|
||||
constructor: Tooltip
|
||||
|
||||
, init: function (type, element, options) {
|
||||
var eventIn
|
||||
, eventOut
|
||||
|
||||
this.type = type
|
||||
this.$element = $(element)
|
||||
this.options = this.getOptions(options)
|
||||
this.enabled = true
|
||||
|
||||
if (this.options.trigger == 'click') {
|
||||
this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
|
||||
} else if (this.options.trigger != 'manual') {
|
||||
eventIn = this.options.trigger == 'hover' ? 'mouseenter' : 'focus'
|
||||
eventOut = this.options.trigger == 'hover' ? 'mouseleave' : 'blur'
|
||||
this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
|
||||
this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
|
||||
}
|
||||
|
||||
this.options.selector ?
|
||||
(this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
|
||||
this.fixTitle()
|
||||
}
|
||||
|
||||
, getOptions: function (options) {
|
||||
options = $.extend({}, $.fn[this.type].defaults, options, this.$element.data())
|
||||
|
||||
if (options.delay && typeof options.delay == 'number') {
|
||||
options.delay = {
|
||||
show: options.delay
|
||||
, hide: options.delay
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
, enter: function (e) {
|
||||
var self = $(e.currentTarget)[this.type](this._options).data(this.type)
|
||||
|
||||
if (!self.options.delay || !self.options.delay.show) return self.show()
|
||||
|
||||
clearTimeout(this.timeout)
|
||||
self.hoverState = 'in'
|
||||
this.timeout = setTimeout(function() {
|
||||
if (self.hoverState == 'in') self.show()
|
||||
}, self.options.delay.show)
|
||||
}
|
||||
|
||||
, leave: function (e) {
|
||||
var self = $(e.currentTarget)[this.type](this._options).data(this.type)
|
||||
|
||||
if (this.timeout) clearTimeout(this.timeout)
|
||||
if (!self.options.delay || !self.options.delay.hide) return self.hide()
|
||||
|
||||
self.hoverState = 'out'
|
||||
this.timeout = setTimeout(function() {
|
||||
if (self.hoverState == 'out') self.hide()
|
||||
}, self.options.delay.hide)
|
||||
}
|
||||
|
||||
, show: function () {
|
||||
var $tip
|
||||
, inside
|
||||
, pos
|
||||
, actualWidth
|
||||
, actualHeight
|
||||
, placement
|
||||
, tp
|
||||
|
||||
if (this.hasContent() && this.enabled) {
|
||||
$tip = this.tip()
|
||||
this.setContent()
|
||||
|
||||
if (this.options.animation) {
|
||||
$tip.addClass('fade')
|
||||
}
|
||||
|
||||
placement = typeof this.options.placement == 'function' ?
|
||||
this.options.placement.call(this, $tip[0], this.$element[0]) :
|
||||
this.options.placement
|
||||
|
||||
inside = /in/.test(placement)
|
||||
|
||||
$tip
|
||||
.detach()
|
||||
.css({ top: 0, left: 0, display: 'block' })
|
||||
.insertAfter(this.$element)
|
||||
|
||||
pos = this.getPosition(inside)
|
||||
|
||||
actualWidth = $tip[0].offsetWidth
|
||||
actualHeight = $tip[0].offsetHeight
|
||||
|
||||
switch (inside ? placement.split(' ')[1] : placement) {
|
||||
case 'bottom':
|
||||
tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}
|
||||
break
|
||||
case 'top':
|
||||
tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2}
|
||||
break
|
||||
case 'left':
|
||||
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}
|
||||
break
|
||||
case 'right':
|
||||
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}
|
||||
break
|
||||
}
|
||||
|
||||
$tip
|
||||
.offset(tp)
|
||||
.addClass(placement)
|
||||
.addClass('in')
|
||||
}
|
||||
}
|
||||
|
||||
, setContent: function () {
|
||||
var $tip = this.tip()
|
||||
, title = this.getTitle()
|
||||
|
||||
$tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
|
||||
$tip.removeClass('fade in top bottom left right')
|
||||
}
|
||||
|
||||
, hide: function () {
|
||||
var that = this
|
||||
, $tip = this.tip()
|
||||
|
||||
$tip.removeClass('in')
|
||||
|
||||
function removeWithAnimation() {
|
||||
var timeout = setTimeout(function () {
|
||||
$tip.off($.support.transition.end).detach()
|
||||
}, 500)
|
||||
|
||||
$tip.one($.support.transition.end, function () {
|
||||
clearTimeout(timeout)
|
||||
$tip.detach()
|
||||
})
|
||||
}
|
||||
|
||||
$.support.transition && this.$tip.hasClass('fade') ?
|
||||
removeWithAnimation() :
|
||||
$tip.detach()
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
, fixTitle: function () {
|
||||
var $e = this.$element
|
||||
if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
|
||||
$e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
|
||||
}
|
||||
}
|
||||
|
||||
, hasContent: function () {
|
||||
return this.getTitle()
|
||||
}
|
||||
|
||||
, getPosition: function (inside) {
|
||||
return $.extend({}, (inside ? {top: 0, left: 0} : this.$element.offset()), {
|
||||
width: this.$element[0].offsetWidth
|
||||
, height: this.$element[0].offsetHeight
|
||||
})
|
||||
}
|
||||
|
||||
, getTitle: function () {
|
||||
var title
|
||||
, $e = this.$element
|
||||
, o = this.options
|
||||
|
||||
title = $e.attr('data-original-title')
|
||||
|| (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
, tip: function () {
|
||||
return this.$tip = this.$tip || $(this.options.template)
|
||||
}
|
||||
|
||||
, validate: function () {
|
||||
if (!this.$element[0].parentNode) {
|
||||
this.hide()
|
||||
this.$element = null
|
||||
this.options = null
|
||||
}
|
||||
}
|
||||
|
||||
, enable: function () {
|
||||
this.enabled = true
|
||||
}
|
||||
|
||||
, disable: function () {
|
||||
this.enabled = false
|
||||
}
|
||||
|
||||
, toggleEnabled: function () {
|
||||
this.enabled = !this.enabled
|
||||
}
|
||||
|
||||
, toggle: function (e) {
|
||||
var self = $(e.currentTarget)[this.type](this._options).data(this.type)
|
||||
self[self.tip().hasClass('in') ? 'hide' : 'show']()
|
||||
}
|
||||
|
||||
, destroy: function () {
|
||||
this.hide().$element.off('.' + this.type).removeData(this.type)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* TOOLTIP PLUGIN DEFINITION
|
||||
* ========================= */
|
||||
|
||||
var old = $.fn.tooltip
|
||||
|
||||
$.fn.tooltip = function ( option ) {
|
||||
return this.each(function () {
|
||||
var $this = $(this)
|
||||
, data = $this.data('tooltip')
|
||||
, options = typeof option == 'object' && option
|
||||
if (!data) $this.data('tooltip', (data = new Tooltip(this, options)))
|
||||
if (typeof option == 'string') data[option]()
|
||||
})
|
||||
}
|
||||
|
||||
$.fn.tooltip.Constructor = Tooltip
|
||||
|
||||
$.fn.tooltip.defaults = {
|
||||
animation: true
|
||||
, placement: 'top'
|
||||
, selector: false
|
||||
, template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
|
||||
, trigger: 'hover'
|
||||
, title: ''
|
||||
, delay: 0
|
||||
, html: false
|
||||
}
|
||||
|
||||
|
||||
/* TOOLTIP NO CONFLICT
|
||||
* =================== */
|
||||
|
||||
$.fn.tooltip.noConflict = function () {
|
||||
$.fn.tooltip = old
|
||||
return this
|
||||
}
|
||||
|
||||
}(window.jQuery);
|
451
product_rental_bookings/static/src/js/product_booking_calender.js
Executable file
451
product_rental_bookings/static/src/js/product_booking_calender.js
Executable file
@@ -0,0 +1,451 @@
|
||||
odoo.define('product_rental_bookings.product_booking_calender', function (require) {
|
||||
"use strict";
|
||||
|
||||
var AbstractAction = require('web.AbstractAction');
|
||||
var Widget = require('web.Widget');
|
||||
var rpc = require('web.rpc');
|
||||
var core = require('web.core');
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
var ajax = require('web.ajax');
|
||||
var session = require('web.session');
|
||||
var dialogs = require('web.view_dialogs');
|
||||
var field_utils = require('web.field_utils');
|
||||
var _lt = core._lt;
|
||||
var datepicker = require('web.datepicker');
|
||||
var config = require('web.config');
|
||||
var Domain = require('web.Domain');
|
||||
var DropdownMenu = require('web.DropdownMenu');
|
||||
|
||||
var ResourceView = AbstractAction.extend({
|
||||
title: core._t('Product Rental Booking'),
|
||||
template: 'ResourceViewTemplate',
|
||||
|
||||
init: function (parent, params) {
|
||||
this._super.apply(this, arguments);
|
||||
var self = this;
|
||||
this.action_manager = parent;
|
||||
this.params = params;
|
||||
this.filtersMapping = [];
|
||||
this.items = [];
|
||||
this.propositions = [];
|
||||
this.fields = _(fields).chain()
|
||||
.map(function (val, key) { return _.extend({}, val, { 'name': key }); })
|
||||
.filter(function (field) { return !field.deprecated && field.searchable; })
|
||||
.sortBy(function (field) { return field.string; })
|
||||
.value();
|
||||
this.attrs = { _: _, fields: this.fields, selected: null };
|
||||
this.value = null;
|
||||
this.items = []
|
||||
self.get_title = false;
|
||||
self.resource_obj = false;
|
||||
self.event_obj = false;
|
||||
self.first_start = false;
|
||||
self.first_end = false;
|
||||
self.second_start = false;
|
||||
self.second_end = false;
|
||||
self.final_date = false,
|
||||
self.cust_list = [];
|
||||
self.color_dict = false;
|
||||
self.calendar_cust_search_string = '';
|
||||
self.highlight_dates = [];
|
||||
self.beautician_lst = [];
|
||||
self.event_date_list = []
|
||||
var fields = {}
|
||||
},
|
||||
|
||||
start: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.set("title", this.title);
|
||||
var self = this;
|
||||
this.items = [];
|
||||
return this._super(); //.done(this.proxy('changed'))
|
||||
},
|
||||
custom_events: {
|
||||
},
|
||||
events: {
|
||||
'change #model_type_ids': 'my_method',
|
||||
},
|
||||
|
||||
my_method: function (e) {
|
||||
var self = this;
|
||||
setTimeout(function () {
|
||||
var model_id = $('#model_type_ids').val()
|
||||
var moment_date = false;
|
||||
rpc.query({
|
||||
model: 'rental.product.order',
|
||||
method: 'get_booking_data',
|
||||
args: [model_id]
|
||||
}).then(function (data) {
|
||||
$('#backend_resource_view').fullCalendar('destroy');
|
||||
self.prepareResourceView(false, data)
|
||||
})
|
||||
}, 200)
|
||||
},
|
||||
|
||||
prepareResourceView: function (moment_date, data) {
|
||||
var self = this;
|
||||
var resourceList = false;
|
||||
var eventList = false;
|
||||
var color_dict = false;
|
||||
var counter = -1;
|
||||
if (data) {
|
||||
resourceList = data[0];
|
||||
if (data[1]) {
|
||||
eventList = data[1]
|
||||
}
|
||||
}
|
||||
self.event_obj = eventList;
|
||||
self.event_date_list = eventList;
|
||||
self.$el.find('#backend_resource_view').fullCalendar({
|
||||
defaultView: 'timelineWeek',
|
||||
defaultDate: moment_date ? moment(moment_date).format('YYYY-MM-DD') : moment(),
|
||||
aspectRatio: 5,
|
||||
editable: true,
|
||||
allDaySlot: false,
|
||||
eventOverlap: false,
|
||||
selectable: true,
|
||||
height: 550,
|
||||
resourceAreaWidth: "17%",
|
||||
slotDuration: '00:00',
|
||||
eventLimit: true, // allow "more" link when too many eventsfullcalendar
|
||||
slotEventOverlap: true,
|
||||
//
|
||||
customButtons: {
|
||||
},
|
||||
header: {
|
||||
left: 'prev, today, next',
|
||||
center: 'title',
|
||||
right: 'month, timelineWeek',
|
||||
},
|
||||
buttonText: {
|
||||
month: 'Month',
|
||||
today: 'Today',
|
||||
},
|
||||
buttonIcons: {
|
||||
prev: 'left-double-arrow',
|
||||
next: 'right-double-arrow',
|
||||
},
|
||||
resourceColumns: [{
|
||||
labelText: 'Products',
|
||||
field: 'title'
|
||||
}],
|
||||
resourceGroupField: 'building',
|
||||
resources: resourceList,
|
||||
|
||||
|
||||
eventRender: function (event, element) {
|
||||
$(element).css({
|
||||
"font-weight": "bold",
|
||||
'font-size': '12px',
|
||||
'color': 'white',
|
||||
});
|
||||
if (event['rendering'] === 'background') { } else {
|
||||
var id = event.resourceId;
|
||||
var line_id = event.line_id;
|
||||
var product_type = event.type
|
||||
var product_id = event.product_id
|
||||
element.prepend("<div data-id=" + id + " data-type=" + product_type + " data-product-id=" + product_id + " class='ibox-tools' style='cursor:pointer;float:right;position:relative;height:20px;width: auto;z-index: 1000;'><a style='position:relative;background-color: transparent; margin-right: 12px;height: auto;' class='testing pull-left'><i class='fa fa-times remove_booking' data-id=" + id + " data-line-id=" + line_id + " style='position:absolute;height:auto;margin-left: 2px;'></i></a></div>");
|
||||
}
|
||||
element.find(".remove_booking").click(function (e) {
|
||||
if (confirm('Are you sure you want to delete ?')) {
|
||||
$('#backend_resource_view').fullCalendar('removeEvents', $(e.currentTarget).attr('data-id'));
|
||||
rpc.query({
|
||||
model: 'rental.product.order',
|
||||
method: 'cancel',
|
||||
args: [$(e.currentTarget).attr('data-line-id')]
|
||||
}, {
|
||||
async: false
|
||||
}).then(function (res) {
|
||||
$('#backend_resource_view').fullCalendar('removeEvents', event._id);
|
||||
return true;
|
||||
});
|
||||
} else { }
|
||||
});
|
||||
},
|
||||
|
||||
events: eventList,
|
||||
|
||||
/*EVENT RESIZE*/
|
||||
|
||||
/*SELECT EVENT ON VIEW CLICK*/
|
||||
select: function (start, end, jsEvent, view, resource) {
|
||||
var current_time = moment().format('YYYY-MM-DD HH:mm:ss')
|
||||
var start_date = moment(start).format('YYYY-MM-DD HH:mm:ss')
|
||||
var end_date = moment(end).format('YYYY-MM-DD HH:mm:ss')
|
||||
var context = false
|
||||
context = rpc.query({
|
||||
model: 'rental.product.order',
|
||||
method: 'start_and_end_date_global',
|
||||
args: [start_date, end_date],
|
||||
}).then(function (sdate) {
|
||||
if (sdate) {
|
||||
context = {
|
||||
'default_from_date': sdate[0],
|
||||
'default_to_date': sdate[1],
|
||||
'default_is_true': true,
|
||||
}
|
||||
}
|
||||
if (resource) {
|
||||
var id = resource.id
|
||||
var product_type = resource.type
|
||||
var product_id = resource.product_id
|
||||
var list = [[0, 0, { 'product_id': product_id }]]
|
||||
context['default_product_type_id'] = 1
|
||||
context['default_product_order_lines_ids'] = list
|
||||
context['default_is_true'] = true
|
||||
}
|
||||
var dialog = new dialogs.FormViewDialog(self, {
|
||||
res_model: 'rental.product.order',
|
||||
res_id: false,
|
||||
title: _t("Rental Order"),
|
||||
readonly: false,
|
||||
context: context,
|
||||
on_saved: function (record, changed) {
|
||||
$('#backend_resource_view').fullCalendar('destroy');
|
||||
self.renderElement()
|
||||
$('.fc-divider').find('.fc-cell-content').addClass('fc-expander');
|
||||
},
|
||||
}).open();
|
||||
});
|
||||
},
|
||||
|
||||
selectAllow: function (selectInfo) {
|
||||
if (selectInfo.start.isBefore(moment().subtract(1, 'days').toDate()))
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
viewRender: function (view, element) {
|
||||
if (view.type && view.type == "customWeek") {
|
||||
$('.fc-divider').find('.fc-cell-content').addClass('fc-expander');
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
next_prev_today_BtnClick: function () {
|
||||
var self = this;
|
||||
var date = moment($('#backend_resource_view').fullCalendar('getDate')).format('YYYY-MM-DD');
|
||||
$('#backend_resource_view').fullCalendar('destroy');
|
||||
self.prepareResourceView(date);
|
||||
$('.fc-divider').find('.fc-cell-content').addClass('fc-expander');
|
||||
},
|
||||
|
||||
changed: function (e) {
|
||||
var nval = this.$(".o_searchview_extended_prop_field").val();
|
||||
if (this.attrs.selected === null || this.attrs.selected === undefined || nval != this.attrs.selected.name) {
|
||||
this.select_field(_.detect(this.fields, function (x) { return x.name == nval; }));
|
||||
}
|
||||
},
|
||||
|
||||
operator_changed: function (e) {
|
||||
this.value.show_inputs($(e.target));
|
||||
},
|
||||
/**
|
||||
* Selects the provided field object
|
||||
*
|
||||
* @param field a field descriptor object (as returned by fields_get, augmented by the field name)
|
||||
*/
|
||||
select_field: function (field) {
|
||||
var self = this;
|
||||
if (this.attrs.selected !== null && this.attrs.selected !== undefined) {
|
||||
this.value.destroy();
|
||||
this.value = null;
|
||||
this.$('.o_searchview_extended_prop_op').html('');
|
||||
}
|
||||
this.attrs.selected = field;
|
||||
if (field === null || field === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
var type = field.type;
|
||||
var Field = core.search_filters_registry.getAny([type, "char"]);
|
||||
|
||||
this.value = new Field(this, field);
|
||||
_.each(this.value.operators, function (operator) {
|
||||
$('<option>', { value: operator.value })
|
||||
.text(String(operator.text))
|
||||
.appendTo(self.$('.o_searchview_extended_prop_op'));
|
||||
});
|
||||
var $value_loc = this.$('.o_searchview_extended_prop_value').show().empty();
|
||||
this.value.appendTo($value_loc);
|
||||
},
|
||||
|
||||
renderElement: function () {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
// var r = new r();
|
||||
var user_ids = [];
|
||||
rpc.query({
|
||||
model: 'product.category',
|
||||
method: 'search_read',
|
||||
fields: ['id', 'name'],
|
||||
}, {
|
||||
async: false
|
||||
}).then(function (model_name) {
|
||||
var model_type_html = QWeb.render('model_template', {
|
||||
model_name: model_name,
|
||||
widget: self,
|
||||
});
|
||||
self.$el.find('#model_selection').empty();
|
||||
self.$el.find('#model_selection').append(model_type_html);
|
||||
});
|
||||
setTimeout(function () {
|
||||
this.$('#cal_cust_search').autocomplete({
|
||||
source: function (request, response) {
|
||||
var query = request.term;
|
||||
var search_timeout = null;
|
||||
self.loaded_partners = [];
|
||||
if (query) {
|
||||
search_timeout = setTimeout(function () {
|
||||
var partners_list = [];
|
||||
self.loaded_partners = self.load_partners(query);
|
||||
_.each(self.loaded_partners, function (partner) {
|
||||
partners_list.push({
|
||||
'id': partner.id,
|
||||
'value': partner.name,
|
||||
'label': partner.name
|
||||
});
|
||||
});
|
||||
response(partners_list);
|
||||
}, 70);
|
||||
}
|
||||
},
|
||||
|
||||
select: function (event, partner) {
|
||||
event.stopImmediatePropagation();
|
||||
// event.preventDefault();
|
||||
if (partner.item && partner.item.id) {
|
||||
var selected_partner = _.find(self.loaded_partners, function (customer) {
|
||||
return customer.id == partner.item.id;
|
||||
});
|
||||
|
||||
var highlight_dates = [];
|
||||
_.find(self.event_obj, function (customer) {
|
||||
if (customer.partner_id === selected_partner.id) {
|
||||
highlight_dates.push(moment(customer.start, 'YYYY-MM_DD').format('D-M-YYYY'));
|
||||
}
|
||||
});
|
||||
self.highlight_dates = highlight_dates;
|
||||
if (highlight_dates && highlight_dates.length > 0) {
|
||||
$(".ui-datepicker-trigger").trigger("click");
|
||||
} else {
|
||||
self.highlight_dates = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
focus: function (event, ui) {
|
||||
event.preventDefault(); // Prevent the default focus behavior.
|
||||
},
|
||||
close: function (event) {
|
||||
// it is necessary to prevent ESC key from propagating to field
|
||||
// root, to prevent unwanted discard operations.
|
||||
if (event.which === $.ui.keyCode.ESCAPE) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
autoFocus: true,
|
||||
html: true,
|
||||
minLength: 1,
|
||||
delay: 200
|
||||
});
|
||||
self.my_method();
|
||||
self.prepareResourceView();
|
||||
|
||||
var input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.name = "";
|
||||
input.setAttribute("id", "select_date");
|
||||
input.setAttribute("class", 'datepicker');
|
||||
input.setAttribute("style", "display:none;");
|
||||
|
||||
var span_tag = document.createElement("SPAN");
|
||||
span_tag.name = "";
|
||||
span_tag.setAttribute('class', 'title-display');
|
||||
$('.fc-left').find('.fc-next-button').after(input);
|
||||
$('.fc-left').find('.fc-next-button').after(span_tag);
|
||||
$('.title-display').html(self.get_title)
|
||||
$("#select_date").datepicker({
|
||||
showOn: "button",
|
||||
buttonText: "<i class='fa fa-calendar'></i>",
|
||||
beforeShowDay: function (date) {
|
||||
if (self.highlight_dates) {
|
||||
var month = date.getMonth() + 1;
|
||||
var year = date.getFullYear();
|
||||
var day = date.getDate();
|
||||
var newdate = day + "-" + month + '-' + year;
|
||||
var tooltip_text = "New event on " + newdate;
|
||||
if ($.inArray(newdate, self.highlight_dates) != -1) {
|
||||
return [true, "highlight", tooltip_text];
|
||||
}
|
||||
return [true];
|
||||
}
|
||||
},
|
||||
onSelect: function (dateText) {
|
||||
$('#backend_resource_view').fullCalendar('gotoDate', moment(dateText).format('YYYY-MM-DD'));
|
||||
$('.title-display').html(self.get_title)
|
||||
}
|
||||
});
|
||||
$('button.o_dropdown_toggler_btn').on('click', function (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation();
|
||||
this.generatorMenuIsOpen = !this.generatorMenuIsOpen;
|
||||
var def;
|
||||
if (!this.generatorMenuIsOpen) {
|
||||
_.invoke(this.propositions, 'destroy');
|
||||
this.propositions = [];
|
||||
}
|
||||
$('.o_filters_menu').toggle()
|
||||
self.changed()
|
||||
});
|
||||
|
||||
$("#room_type_ids").select2({
|
||||
placeholder: "Room Type",
|
||||
allowClear: true,
|
||||
});
|
||||
|
||||
$("#select_filter").select2({
|
||||
placeholder: "Filter",
|
||||
allowClear: true,
|
||||
});
|
||||
|
||||
$('#select_filter').change(function (e) {
|
||||
});
|
||||
|
||||
|
||||
$('.autocomplete_li').mouseover(function (e) {
|
||||
$(e.currentTarget).addClass('o-selection-focus');
|
||||
});
|
||||
$('.autocomplete_li').mouseout(function (e) {
|
||||
$(e.currentTarget).removeClass('o-selection-focus');
|
||||
});
|
||||
}, 0);
|
||||
},
|
||||
|
||||
load_partners: function (query) {
|
||||
var self = this;
|
||||
var loaded_partners = [];
|
||||
rpc.query({
|
||||
model: 'res.partner',
|
||||
method: 'search_read',
|
||||
fields: ['name'],
|
||||
domain: ['|', '|', ['name', 'ilike', query],
|
||||
['mobile', 'ilike', query],
|
||||
['email', 'ilike', query]
|
||||
],
|
||||
limit: 7,
|
||||
}, {
|
||||
async: false
|
||||
}).then(function (partners) {
|
||||
loaded_partners = partners
|
||||
});
|
||||
return loaded_partners;
|
||||
},
|
||||
});
|
||||
|
||||
core.action_registry.add('resource_view_new', ResourceView);
|
||||
|
||||
return {
|
||||
ResourceView: ResourceView,
|
||||
};
|
||||
});
|
124
product_rental_bookings/static/src/js/product_contract_sign.js
Executable file
124
product_rental_bookings/static/src/js/product_contract_sign.js
Executable file
@@ -0,0 +1,124 @@
|
||||
odoo.define('product_rental_bookings.contract_sign', function (require) {
|
||||
"use strict";
|
||||
|
||||
var BasicController = require('web.BasicController');
|
||||
var rpc = require('web.rpc');
|
||||
var FormRenderer = require('web.FormRenderer');
|
||||
var FormController = require('web.FormController');
|
||||
var core = require('web.core');
|
||||
|
||||
var _t = core._t;
|
||||
var sign;
|
||||
|
||||
FormRenderer.include({
|
||||
_addOnClickAction: function ($el, node) {
|
||||
var self = this;
|
||||
$el.click(function () {
|
||||
if (node.attrs.name == 'signature_customer' || node.attrs.name == 'signature_contractor') {
|
||||
self.trigger_up('sign_button_clicked', {
|
||||
attrs: node.attrs,
|
||||
record: self.state,
|
||||
});
|
||||
} else {
|
||||
self.trigger_up('button_clicked', {
|
||||
attrs: node.attrs,
|
||||
record: self.state,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
FormController.include({
|
||||
custom_events: _.extend({}, FormController.prototype.custom_events, {
|
||||
sign_button_clicked: '_onSignButtonClicked',
|
||||
}),
|
||||
_onSignButtonClicked: function (event) {
|
||||
var self = this;
|
||||
self.saveRecord(self.handle, {
|
||||
stayInEdit: true,
|
||||
}).then(function () {
|
||||
var record = self.model.get(event.data.record.id);
|
||||
self._callSignButtonAction(event.data.attrs, record)
|
||||
});
|
||||
},
|
||||
|
||||
_callSignButtonAction: function (attrs, record) {
|
||||
if (attrs.name == 'signature_customer' || attrs.name == 'signature_contractor' && record.model == 'rental.product.contract') {
|
||||
this.open_signature_popup(attrs, record);
|
||||
}
|
||||
},
|
||||
|
||||
open_signature_popup: function (attrs, record) {
|
||||
var self = this;
|
||||
$('.model-body-sign').empty();
|
||||
$('.model-body-sign').html('<canvas id="canvas" width="600px" height="200px"></canvas>');
|
||||
var canvas = new fabric.Canvas('canvas');
|
||||
if (record.res_id) {
|
||||
rpc.query({
|
||||
model: 'rental.product.contract',
|
||||
method: 'search_read',
|
||||
args: [[['id', '=', parseInt(record.res_id)]]],
|
||||
}, {
|
||||
async: false
|
||||
}).then(function (result) {
|
||||
if (result && result[0]) {
|
||||
$('#customerModal').modal('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
canvas.observe('mouse:move', function (o) {
|
||||
canvas.isDrawingMode = 1;
|
||||
canvas.freeDrawingBrush.width = 3;
|
||||
canvas.freeDrawingBrush.color = '#4c4c4c';
|
||||
});
|
||||
canvas.on('mouse:up', function (o) {
|
||||
canvas.isDrawingMode = 0;
|
||||
});
|
||||
sign = attrs.name
|
||||
$('.modal-footer').delegate('.save_sign', 'click', { 'canvas': canvas, 'active_id': record.res_id, 'self': self, 'attrs': sign }, self.save_sign);
|
||||
$('.modal-footer').delegate('.reset_sign', 'click', { 'canvas': canvas }, self.reset_sign);
|
||||
},
|
||||
|
||||
save_sign: function (event) {
|
||||
var self = this;
|
||||
var signature = {}
|
||||
var signature_img = event.data.canvas.toDataURL().split('data:image/png;base64,')[1]
|
||||
event.stopImmediatePropagation()
|
||||
var id = event.data.active_id;
|
||||
var blank = document.createElement('canvas');
|
||||
blank.width = event.data.canvas.width;
|
||||
blank.height = event.data.canvas.height;
|
||||
if (blank.toDataURL() == event.data.canvas.toDataURL()) {
|
||||
event.data.self.do_warn(_t('Please add signature and try again.'));
|
||||
} else {
|
||||
if (id) {
|
||||
rpc.query({
|
||||
model: 'rental.product.contract',
|
||||
method: 'write',
|
||||
args: [parseInt(id),
|
||||
{ 'signature': signature_img, 'button_name': sign, }],
|
||||
}, {
|
||||
async: false
|
||||
}).then(function (result) {
|
||||
if (result) {
|
||||
event.data.self.do_notify(_t('Information'), _t("Signature stored successfully."));
|
||||
event.data.canvas.clear();
|
||||
event.data.canvas.renderAll();
|
||||
$('#customerModal').modal('hide');
|
||||
setTimeout(function () {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
alert("Signature not save.");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alert("Record id not found, Please refresh page and try again.");
|
||||
}
|
||||
}
|
||||
// render form view
|
||||
event.data.self.update({});
|
||||
},
|
||||
});
|
||||
});
|
27
product_rental_bookings/static/src/xml/delivery_sign.xml
Executable file
27
product_rental_bookings/static/src/xml/delivery_sign.xml
Executable file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-extend="FormView.buttons">
|
||||
<t t-jquery="div.o_form_buttons_edit" t-operation="after">
|
||||
<div class="modal fade" id="customerModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
Add Signature
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body model-body-sign"/>
|
||||
<div class="modal-footer">
|
||||
<button type="dummy" name="close" class="btn btn-primary" data-dismiss="modal">Close</button>
|
||||
<button type="dummy" name="close" class="btn btn-primary reset_sign" data-dismiss="modal" style="display:none;">Reset</button>
|
||||
or
|
||||
<button type="dummy" name="close" class="btn btn-primary save_sign">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
68
product_rental_bookings/static/src/xml/product_booking_calender.xml
Executable file
68
product_rental_bookings/static/src/xml/product_booking_calender.xml
Executable file
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="tag_resource_view">
|
||||
<div t-name="ResourceViewTemplate" style="width:100%">
|
||||
<div class="container-fluid" style="margin-top:5px">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div style="width: 50%;float: left;">
|
||||
<div style="width: 28%;float: left;line-height: 30px;">Category:</div>
|
||||
<div id="model_selection" style="width:60%;float: left;line-height: 30px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="btn-group o_dropdown" style="display:none;">
|
||||
<div class="dropdown-menu o_dropdown_menu o_filters_menu" role="menu"
|
||||
x-placement="bottom-start"
|
||||
style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 30px, 0px); width: 170px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 20px;">
|
||||
<div class="col-md-12" style="padding:0">
|
||||
<div id='backend_resource_view'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<t t-name="model_template">
|
||||
<select id="model_type_ids" class="form-control" placeholder="Model">
|
||||
<t t-if="model_name and model_name.length > 0">
|
||||
<t t-foreach="model_name" t-as="each_model">
|
||||
<t t-if="each_model">
|
||||
<option t-att-value="each_model.id">
|
||||
<t t-esc="each_model.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</select>
|
||||
</t>
|
||||
|
||||
<t t-name="fuel_template">
|
||||
<select id="fuel_type_ids" class="form-control" placeholder="Model">
|
||||
<t t-if="model_name and model_name.length > 0">
|
||||
<t t-foreach="model_name" t-as="each_model">
|
||||
<t t-if="each_model">
|
||||
<option t-att-value="each_model.id">
|
||||
<t t-esc="each_model.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</select>
|
||||
</t>
|
||||
</templates>
|
24
product_rental_bookings/views/account_invoice_view.xml
Executable file
24
product_rental_bookings/views/account_invoice_view.xml
Executable file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="inherit_account_move_form">
|
||||
<field name="name">Account Move Inherit</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="model">account.move</field>
|
||||
<field name="arch" type="xml">
|
||||
<field name="team_id" position="after">
|
||||
<field name="contract_id" invisible="1"/>
|
||||
<field name="is_hours" invisible="1"/>
|
||||
<field name="is_days" invisible="1"/>
|
||||
</field>
|
||||
<xpath expr="//page/field[@name='invoice_line_ids']/tree/field[@name='quantity']"
|
||||
position="before">
|
||||
<field name="enter_hour" attrs="{'column_invisible':[('parent.is_hours', '=', False)]}"/>
|
||||
<field name="enter_days" attrs="{'column_invisible':[('parent.is_days', '=', False)]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
24
product_rental_bookings/views/assets.xml
Normal file
24
product_rental_bookings/views/assets.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="assets_backend" name="rental_delivery_sign assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/product_rental_bookings/static/src/js/fabric.min.js" />
|
||||
<script type="text/javascript" src="/product_rental_bookings/static/src/js/product_contract_sign.js" />
|
||||
<link rel="stylesheet" href="/product_rental_bookings/static/src/css/backend.css" id="pos-stylesheet" />
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="fullcalender_assests_backend" name="rental_fullcalender" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<link rel="stylesheet" href="/product_rental_bookings/static/src/css/fullcalendar.css" />
|
||||
<link rel="stylesheet" href="/product_rental_bookings/static/src/css/scheduler.css" />
|
||||
<link rel="stylesheet" href="/product_rental_bookings/static/src/css/scheduler.min.css" />
|
||||
|
||||
<script type="text/javascript" src="/product_rental_bookings/static/src/js/lib/fullcalendar.js" />
|
||||
<script type="text/javascript" src="/product_rental_bookings/static/src/js/lib/scheduler.js" />
|
||||
<script type="text/javascript" src="/product_rental_bookings/static/src/js/product_booking_calender.js" />
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
56
product_rental_bookings/views/menus.xml
Normal file
56
product_rental_bookings/views/menus.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- ROOT -->
|
||||
|
||||
<menuitem id="rental_product_main_menu" name="Rental" web_icon="product_rental_bookings/static/description/book_product.png/" />
|
||||
|
||||
<!-- QUICK SEARCH -->
|
||||
|
||||
<menuitem id="feet_product_quick_booking_menu" name="Quick Search" parent="rental_product_main_menu" sequence="0" action="action_product_quick_product_booking" />
|
||||
|
||||
<!-- PRODUCTS -->
|
||||
|
||||
<menuitem id="product_product_product" name="Product" parent="rental_product_main_menu" sequence="1" action="product.product_normal_action_sell" />
|
||||
|
||||
<!-- BOOKING -->
|
||||
|
||||
<menuitem id="rental_product_booking_menu" name="Rental" parent="rental_product_main_menu" sequence="2" />
|
||||
|
||||
<menuitem id="product_order_menu" name="Rental Orders" parent="rental_product_booking_menu" sequence="0" action="action_product_order_id" />
|
||||
|
||||
<menuitem id="feet_product_contract_sub_menu" name="Rental Contracts" parent="rental_product_booking_menu" sequence="1" action="action_rental_product_contract" />
|
||||
|
||||
<menuitem id="feet_product_calender_booking_menu" name="Rental Bookings" parent="rental_product_booking_menu" sequence="2" action="action_product_calender_product_booking1" />
|
||||
|
||||
<menuitem id="feet_product_logs_menu" name="Rental Logs" parent="rental_product_booking_menu" sequence="3" action="action_rental_product_logs" />
|
||||
|
||||
<!-- INVENTORY -->
|
||||
|
||||
<menuitem id="rental_product_inventory_menu" name="Inventory" parent="rental_product_main_menu" sequence="3" />
|
||||
|
||||
<menuitem id="stock_inventory_sub_menu" name="Stock inventory" parent="rental_product_inventory_menu" sequence="1" action="stock.action_view_quants" />
|
||||
|
||||
<!-- ANALYSIS -->
|
||||
|
||||
<menuitem id="rental_product_analysis" name="Analysis" parent="rental_product_main_menu" sequence="5" />
|
||||
|
||||
<menuitem id="product_rental_order_analysis" name="Rental Orders" parent="rental_product_analysis" action="action_rental_order_analysis" sequence="2" />
|
||||
|
||||
<menuitem id="feet_product_logs_analysis" name="Rental Log" parent="rental_product_analysis" action="action_rental_product_logs_analysis" />
|
||||
|
||||
|
||||
<!-- SETTINGS -->
|
||||
|
||||
<menuitem id="product_configuration_menu" name="Configuration" parent="rental_product_main_menu" sequence="10" />
|
||||
|
||||
<menuitem id="rental_product_branch_menu" name="Operation" parent="product_configuration_menu" />
|
||||
|
||||
<menuitem id="menu_operation_type" name="Operations Types" parent="rental_product_branch_menu" action="action_picking_type_list" sequence="4" />
|
||||
|
||||
<menuitem id="rental_menu_configuration" name="Settings" parent="product_configuration_menu" sequence="0" action="rental_config_settings_action" groups="base.group_system" />
|
||||
|
||||
<menuitem id="menu_session_config" name="Session Configuration" parent="product_configuration_menu" action="action_session_config" sequence="4" />
|
||||
|
||||
</data>
|
||||
</odoo>
|
80
product_rental_bookings/views/product_book.xml
Executable file
80
product_rental_bookings/views/product_book.xml
Executable file
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="action_product_calender_product_booking1" model="ir.actions.client">
|
||||
<field name="name">Product Rental Booking</field>
|
||||
<field name="tag">resource_view_new</field>
|
||||
</record>
|
||||
|
||||
<record id="product_booking_id" model="ir.ui.view">
|
||||
<field name="name">product booking</field>
|
||||
<field name="model">product.booking</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="product product Booking">
|
||||
<header>
|
||||
<button name="search_product" string="Search" type="object" class="oe_highlight" />
|
||||
<button name="book_product" string="Create" type="object" class="oe_highlight" attrs="{'invisible':[('is_search','=',False)]}" />
|
||||
</header>
|
||||
<sheet>
|
||||
<field name="book_number" invisible="True" />
|
||||
<group col="4">
|
||||
<field name="price_based" />
|
||||
<field name="session_id" attrs="{'invisible':[('price_based','!=','per_session')], 'required':[('price_based','=','per_session')]}" />
|
||||
</group>
|
||||
<group col="4" attrs="{'invisible':[('price_based','!=','per_session')]}">
|
||||
<field name="from_date" required="1" string="Date" />
|
||||
<field name="to_date" invisible="1" />
|
||||
</group>
|
||||
<group col="4" attrs="{'invisible':[('price_based','=','per_session')]}">
|
||||
<field name="from_date" required="1" />
|
||||
<field name="to_date" attrs="{'required':[('price_based','!=','per_session')]}" />
|
||||
</group>
|
||||
<group col="4">
|
||||
<field name="location_id" required="1" domain="[('usage', '=', 'internal')]" />
|
||||
<field name="categ_id" required="1" />
|
||||
<field name="is_search" invisible="1" />
|
||||
</group>
|
||||
<notebook attrs="{'invisible':[('is_search','=',False)]}">
|
||||
<page name="product_details" string="Product Details">
|
||||
<field name="product_line_ids">
|
||||
<tree editable="bottom">
|
||||
<field name="name" />
|
||||
<field name="categ_id" />
|
||||
<field name="currency_id" invisible="1" />
|
||||
<field name="rental_qyt" />
|
||||
<field name="rental_amount" widget="monetary" options="{'currency_field': 'currency_id'}" attrs="{'column_invisible':[('parent.price_based','!=','per_day')]}" />
|
||||
<field name="rental_amount_per_hour" widget="monetary" options="{'currency_field': 'currency_id'}" attrs="{'column_invisible':[('parent.price_based','!=','per_hour')]}" />
|
||||
<field name="rental_amount_per_session" widget="monetary" options="{'currency_field': 'currency_id'}" attrs="{'column_invisible':[('parent.price_based','!=','per_session')]}" />
|
||||
<field name="taxes_id" widget="many2many_tags" />
|
||||
<field name="selected_product" />
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
<group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="total_days" attrs="{'invisible':['|',('is_search','=',False),('price_based', '!=', 'per_day')]}" />
|
||||
<field name="total_hours" widget="float_time" attrs="{'invisible':['|',('is_search','=',False),('price_based', '=', 'per_day')]}" />
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="currency_id" invisible="1" />
|
||||
<field name="extra_charges" widget="monetary" options="{'currency_field': 'currency_id'}" attrs="{'invisible':[('is_search','=',False)]}" />
|
||||
<field name="sub_total" widget="monetary" options="{'currency_field': 'currency_id'}" attrs="{'invisible':[('is_search','=',False)]}" readonly="1" />
|
||||
<field name="total" widget="monetary" options="{'currency_field': 'currency_id'}" attrs="{'invisible':[('is_search','=',False)]}" readonly="1" />
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_product_quick_product_booking" model="ir.actions.act_window">
|
||||
<field name="name">Product booking</field>
|
||||
<field name="res_model">product.booking</field>
|
||||
<field name="view_mode">form</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
191
product_rental_bookings/views/product_contract.xml
Executable file
191
product_rental_bookings/views/product_contract.xml
Executable file
@@ -0,0 +1,191 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<odoo>
|
||||
<data>
|
||||
<record id='rental_product_contract_form' model="ir.ui.view">
|
||||
<field name="name">Rental Contracts</field>
|
||||
<field name="model">rental.product.contract</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button name="contract_close" states="open,expired,future" type="object" class="oe_highlight" string="Close Contract" />
|
||||
<button name="send_product_contract" string="Send Contract by Email" type="object" class="oe_highlight" />
|
||||
<button name="contract_open" states="closed" type="object" class="oe_highlight" string="Set
|
||||
Contract In Progress" />
|
||||
<button class="oe_highlight" name="act_renew_contract" type="object" string="Renew
|
||||
Contract" help="Create a new contract automatically with
|
||||
all the same informations except for the date that will
|
||||
start at the end of current contract" attrs="{'invisible':[('state','!=','diesoon')]}" />
|
||||
<button class="oe_highlight" name="create_invoice" type="object" string="Create invoice" attrs="{'invisible':[('first_invoice_created','=',True)]}" />
|
||||
<button name="generate_policy" type="object" string="Generate Policy" invisible="1" />
|
||||
<field name="state" widget="statusbar" />
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="New" />
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group col="2">
|
||||
<field name="currency_id" invisible="1" />
|
||||
<field name="partner_id" invisible="1" />
|
||||
<field name="origin" string="Source Document" invisible="1" />
|
||||
<field name="company_id" readonly="1" />
|
||||
<field name="rental_id" invisible="1" />
|
||||
<field name="picking_id" invisible="1" />
|
||||
<field name="is_hours" invisible="1" />
|
||||
<field name="is_days" invisible="1" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="contract_date" readonly="1" />
|
||||
<field name="from_date" required="1" readonly="1" />
|
||||
<field name="to_date" required="1" readonly="1" />
|
||||
<field name="cost" invisible="1" />
|
||||
<field name="account_type" invisible="1" />
|
||||
<label for="cost_generated" invisible="1" />
|
||||
<div>
|
||||
<field name="cost_frequency" class="oe_inline" invisible="1" />
|
||||
<field name="cost_generated" style="width:142px;" invisible="1" />
|
||||
</div>
|
||||
</group>
|
||||
<group col="2">
|
||||
<separator string="Payment Details" />
|
||||
<field name="account_payment_term" readonly="1" />
|
||||
<field name="first_invoice_created" invisible="1" />
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Product Details" name="product_details">
|
||||
<group>
|
||||
<field name="attachment_ids" widget="many2many_binary" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="product_contract_lines_ids" nolabel="1">
|
||||
<tree editable="bottom">
|
||||
<field name="product_id" string="Product" domain="[('is_rental', '=', True)]" />
|
||||
<field name="description" />
|
||||
<field name="currency_id" invisible="1" />
|
||||
<field name="price_based" />
|
||||
<field name="enter_days" attrs="{'column_invisible':[('parent.is_days', '=', False)],
|
||||
'invisible': [('price_based', '=', 'per_hour')]}" />
|
||||
<field name="enter_hour" attrs="{'column_invisible':[('parent.is_hours', '=', False)],
|
||||
'invisible': [('price_based', '=', 'per_day')]}" />
|
||||
<field name="qty_needed" />
|
||||
<field name="price" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
||||
<field name="tax_id" widget="many2many_tags" />
|
||||
<field name="sub_total" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
||||
<field name="product_contract_id" invisible="1" />
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Recurring Invoice">
|
||||
<field name="recurring_line" mode="tree" sum="recurring_amount">
|
||||
<tree string="product Reccurring Lines" colors="#0b7a35:payment_info=='paid';#f20b07:payment_info!='paid'">
|
||||
<field name="date_today" />
|
||||
<field name="payment_info" />
|
||||
<field name="recurring_amount" sum="recurring_amount" />
|
||||
</tree>
|
||||
</field>
|
||||
<field name="sum_cost" invisible="1" />
|
||||
</page>
|
||||
<page string="Document">
|
||||
<field name="document_ids" nolabel="1">
|
||||
<tree editable="bottom">
|
||||
<field name="name" />
|
||||
<field name="id_number" />
|
||||
<field name="contract_id" invisible="1" />
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Cancellation Policy">
|
||||
<group>
|
||||
<field name="number_of_slot" required="True" />
|
||||
</group>
|
||||
<field name="cancel_policy_ids" mode="tree">
|
||||
<tree string="Cancellation Policy" editable="bottom" create="false" delete="false">
|
||||
<field name="contract_id" invisible="1" />
|
||||
<field name="from_date" />
|
||||
<field name="to_date" />
|
||||
<field name="policy_charged" />
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
<group>
|
||||
<group>
|
||||
<label for="first_payment" />
|
||||
<field name="first_payment" nolabel="1" widget='monetary' />
|
||||
</group>
|
||||
<group class="oe_subtotal_footer oe_right" colspan="2" name="sale_total">
|
||||
<label for="untaxed_amount" />
|
||||
<field name="untaxed_amount" nolabel="1" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
||||
<label for="taxes" />
|
||||
<field name="taxes" nolabel="1" widget='monetary' />
|
||||
<label for="extra_charges" />
|
||||
<field name="extra_charges" readonly="1" nolabel="1" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
||||
<div class="oe_subtotal_footer_separator oe_inline o_td_label">
|
||||
<label for="total_amount" />
|
||||
</div>
|
||||
<field name="total_amount" nolabel="1" class="oe_subtotal_footer_separator" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="terms_condition" />
|
||||
|
||||
</group>
|
||||
<group col="4">
|
||||
<label for="signature_contractor" />
|
||||
<div>
|
||||
<field name="signature_contractor" widget="image" options="{'size': [90, 35]}" />
|
||||
<button name="signature_contractor" string="Contractor Signature" class="oe_highlight" />
|
||||
</div>
|
||||
<label for="signature_customer" />
|
||||
<div>
|
||||
<field name="signature_customer" widget="image" options="{'size': [90, 35]}" />
|
||||
<button name="signature_customer" string="Customer Signature" class="oe_highlight" />
|
||||
</div>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id='rental_product_contract_tree' model='ir.ui.view'>
|
||||
<field name="name">Rental Contracts</field>
|
||||
<field name="model">rental.product.contract</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="partner_id" />
|
||||
<field name="from_date" />
|
||||
<field name="to_date" />
|
||||
<field name="state" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id='action_rental_product_contract' model='ir.actions.act_window'>
|
||||
<field name="name">Rental Contracts</field>
|
||||
<field name="res_model">rental.product.contract</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class='oe_view_nocontent_create'>Click to add Products Contract</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id='action_rental_product_contract_form' model='ir.actions.act_window'>
|
||||
<field name="name">Rental Contracts</field>
|
||||
<field name="res_model">rental.product.contract</field>
|
||||
<field name="view_mode">form,tree</field>
|
||||
<field name="help" type="html">
|
||||
<p class='oe_view_nocontent_create'>Click to add Products Contract</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_rental_contract_view_tree" model="ir.actions.act_window">
|
||||
<field name="name">Contract</field>
|
||||
<field name="res_model">rental.product.contract</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="rental_product_contract_tree" />
|
||||
<field name="domain">[('state', 'not in', ['futur', 'closed'])]</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
80
product_rental_bookings/views/product_move.xml
Executable file
80
product_rental_bookings/views/product_move.xml
Executable file
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="rental_product_logs_form_id" model="ir.ui.view">
|
||||
<field name="name">Product Logs</field>
|
||||
<field name="model">rental.product.logs</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="product_id" />
|
||||
<field name="customer_id" />
|
||||
<field name="from_date" />
|
||||
<field name="to_date" />
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="rental_product_logs_tree_id" model="ir.ui.view">
|
||||
<field name="name">Product Logs</field>
|
||||
<field name="model">rental.product.logs</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="product_id" />
|
||||
<field name="customer_id" />
|
||||
<field name="from_date" />
|
||||
<field name="to_date" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_product_logs_pivot">
|
||||
<field name="name">Products Logs</field>
|
||||
<field name="model">rental.product.logs</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="product Logs">
|
||||
<field name="product_id" type="row" />
|
||||
<field name="from_date" type="col" />
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_product_logs_graph">
|
||||
<field name="name">Products Logs</field>
|
||||
<field name="model">rental.product.logs</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="product Logs">
|
||||
<field name="product_id" type="row" />
|
||||
<field name="from_date" type="col" />
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_rental_product_logs" model="ir.actions.act_window">
|
||||
<field name="name">Rental Logs</field>
|
||||
<field name="res_model">rental.product.logs</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<record id="action_rental_product_logs_analysis" model="ir.actions.act_window">
|
||||
<field name="name">product Logs</field>
|
||||
<field name="res_model">rental.product.logs</field>
|
||||
<field name="view_mode">graph,pivot</field>
|
||||
</record>
|
||||
|
||||
<record id="action_rental_product_move" model="ir.actions.act_window">
|
||||
<field name="name">Rental Move</field>
|
||||
<field name="res_model">stock.picking</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{'search_default_undone': 1}</field>
|
||||
<field name="domain">[('is_rental', '=', True)]</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
134
product_rental_bookings/views/product_operation.xml
Executable file
134
product_rental_bookings/views/product_operation.xml
Executable file
@@ -0,0 +1,134 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="rental_product_operation_form_id" model="ir.ui.view">
|
||||
<field name="name">Operation Type</field>
|
||||
<field name="model">rental.product.operation</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label class="oe_edit_only" for="name" string="Operation Type Name" />
|
||||
<h1>
|
||||
<field name="name" />
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="rental_move_type" />
|
||||
<field name="location_id" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="source_location" />
|
||||
<field name="destination_location" />
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="rental_product_type_kanban" model="ir.ui.view">
|
||||
<field name="name">rental.product.operation.kanban</field>
|
||||
<field name="model">rental.product.operation</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="oe_background_grey o_kanban_dashboard o_emphasize_colors o_stock_kanban" create="0">
|
||||
<field name="color" />
|
||||
<field name="rental_move_type" />
|
||||
<field name="state" />
|
||||
<field name="count_operation_ready" />
|
||||
<field name="count_operation_on_rent" />
|
||||
<field name="count_operation_service" />
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="#{kanban_color(record.color.raw_value)}">
|
||||
<div>
|
||||
<div t-attf-class="o_kanban_card_header">
|
||||
<div class="o_kanban_card_header_title">
|
||||
<div class="o_primary">
|
||||
<field name="name" />
|
||||
</div>
|
||||
<div class="o_secondary">
|
||||
<field class="o_secondary" name="location_id" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_manage_button_section">
|
||||
<a class="o_kanban_manage_toggle_button" href="#">
|
||||
<i class="fa fa-ellipsis-v" role="img" aria-label="Manage" title="Manage" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container o_kanban_card_content">
|
||||
<div class="row">
|
||||
<div class="col-xs-6 o_kanban_primary_left" style="margin-left: 16px;">
|
||||
<button class="btn btn-primary" name="get_action_operation" type="object">
|
||||
<span t-if="record.rental_move_type.raw_value =='outgoing'">
|
||||
<t t-esc="record.count_operation_ready.value" />
|
||||
Deliveries of Product
|
||||
</span>
|
||||
<span t-if="record.rental_move_type.raw_value =='incoming'">
|
||||
<t t-esc="record.count_operation_on_rent.value" />
|
||||
Incoming Products
|
||||
</span>
|
||||
<span t-if="record.rental_move_type.raw_value =='internal'">
|
||||
<t t-esc="record.count_operation_service.value" />
|
||||
Internal Moves
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container o_kanban_card_manage_pane dropdown-menu" role="menu">
|
||||
<div t-if="widget.editable" class="o_kanban_card_manage_settings row">
|
||||
<div class="col-xs-8">
|
||||
<ul class="oe_kanban_colorpicker" data-field="color" />
|
||||
</div>
|
||||
<div class="col-xs-4 text-right">
|
||||
<a type="edit">Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="rental_product_operation_tree_id" model="ir.ui.view">
|
||||
<field name="name">Operation type</field>
|
||||
<field name="model">rental.product.operation</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name" />
|
||||
<field name="location_id" />
|
||||
<field name="rental_move_type" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_picking_type_list" model="ir.actions.act_window">
|
||||
<field name="name">Operation Type</field>
|
||||
<field name="res_model">rental.product.operation</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">Click to define a new transfer.</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_product_operation_type" model="ir.actions.act_window">
|
||||
<field name="name">Operation Type</field>
|
||||
<field name="res_model">rental.product.operation</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="view_mode">kanban,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">Click to create a new operation type.</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
195
product_rental_bookings/views/product_order.xml
Executable file
195
product_rental_bookings/views/product_order.xml
Executable file
@@ -0,0 +1,195 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="rental_product_order_search" model="ir.ui.view">
|
||||
<field name="name">Rental Order</field>
|
||||
<field name="model">rental.product.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="res_number" filter_domain="[('res_number','ilike',self)]" string="Order Number" />
|
||||
<field name="customer_name" filter_domain="[('customer_name','ilike',self)]" string="Customer" />
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="rental_product_order_form_id" model="ir.ui.view">
|
||||
<field name="name">Rental Order</field>
|
||||
<field name="model">rental.product.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="product Order">
|
||||
<header>
|
||||
<field name="state" widget="statusbar" />
|
||||
<button name="send_product_quote" string="Send by Email" type="object" class="oe_highlight" />
|
||||
<button name="confirm" string="Confirm" type="object" class="oe_highlight" states="book,draft" />
|
||||
<button name="cancel" string="Cancel" type="object" class="oe_highlight" attrs="{'invisible': [('state', 'in', ('cancel', 'close'))]}" />
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button type="object" class="oe_stat_button" icon="fa-truck" name="action_view_stock_pickings" invisible="1">
|
||||
<div class="o_form_field o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="picking_count" />
|
||||
</span>
|
||||
<span class="o_stat_text">Move</span>
|
||||
</div>
|
||||
</button>
|
||||
<button type="object" class="oe_stat_button" icon="fa-pencil-square-o" name="action_view_order_invoices" attrs="{'invisible': [('invoice_count','=',0)]}">
|
||||
<div class="o_form_field o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="invoice_count" />
|
||||
</span>
|
||||
<span class="o_stat_text">Invoiced</span>
|
||||
</div>
|
||||
</button>
|
||||
<button type="object" class="oe_stat_button" icon="fa-book" name="action_view_order_contract" attrs="{'invisible': [('contract_count','=',0)]}">
|
||||
<div class="o_form_field o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="contract_count" />
|
||||
</span>
|
||||
<span class="o_stat_text">Contract</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="res_number" placeholder="New" />
|
||||
</h1>
|
||||
</div>
|
||||
<group col="4">
|
||||
<field name="customer_name" required="1" />
|
||||
<field name="book_date" />
|
||||
<field name="from_date" required="1" readonly="1" />
|
||||
<field name="to_date" required="1" readonly="1" />
|
||||
<field name="start_date" invisible="1" />
|
||||
<field name="end_date" invisible="1" />
|
||||
<field name="is_agreement" invisible="1" />
|
||||
<field name="account_payment_term" />
|
||||
<field name="location_id" invisible="1" />
|
||||
<field name="company_id" invisible="1" />
|
||||
<field name="count" invisible="1" />
|
||||
<field name="is_true" invisible="1" />
|
||||
<field name="pricelist_id" invisible="1" />
|
||||
<field name="return_date" invisible="1" />
|
||||
<field name="currency_id" invisible="1" />
|
||||
<field name="is_hours" invisible="1" />
|
||||
<field name="is_days" invisible="1" />
|
||||
</group>
|
||||
<group>
|
||||
<group col="2"></group>
|
||||
<group col="2"></group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="rental_order" string="Rental Order">
|
||||
<field name="product_order_lines_ids" readonly="1">
|
||||
<tree editable="bottom">
|
||||
<field name="product_id" string="Product" context="{'from_date':parent.from_date, 'to_date':parent.to_date, 'from_product_order': True}" domain="[('is_rental', '=', True)]" />
|
||||
<field name="name" />
|
||||
<field name="product_order_id" invisible="1" />
|
||||
<field name="price_based" />
|
||||
<field name="enter_days" attrs="{'column_invisible':[('parent.is_days', '=', False)],'invisible': [('price_based', '=', 'per_hour')]}" />
|
||||
<field name="enter_hour" attrs="{'column_invisible':[('parent.is_hours', '=', False)],'invisible': [('price_based', '=', 'per_day')]}" />
|
||||
<field name="qty_needed" />
|
||||
<field name="currency_id" invisible="1" />
|
||||
<field name="price" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
||||
<field name="tax_id" widget="many2many_tags" />
|
||||
<field name="sub_total" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
||||
</tree>
|
||||
</field>
|
||||
<group class="oe_subtotal_footer oe_right" colspan="2" name="sale_total">
|
||||
<label for="untaxed_amount" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
||||
<field name="untaxed_amount" nolabel="1" widget='monetary' />
|
||||
<label for="taxes" />
|
||||
<field name="taxes" nolabel="1" widget='monetary' />
|
||||
<label for="extra_charges" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
||||
<field name="extra_charges" nolabel="1" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
||||
<div class="oe_subtotal_footer_separator oe_inline o_td_label">
|
||||
<label for="total_amount" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
||||
</div>
|
||||
<field name="total_amount" nolabel="1" class="oe_subtotal_footer_separator" widget='monetary' />
|
||||
</group>
|
||||
<group>
|
||||
<field name="terms_condition" />
|
||||
</group>
|
||||
</page>
|
||||
<page name="other_info" string="Other Information">
|
||||
<group>
|
||||
<group>
|
||||
<field name="user_id" />
|
||||
<field name="invoice_status" />
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="rental_product_driver_schedule" model="ir.ui.view">
|
||||
<field name="name">Rental Order</field>
|
||||
<field name="model">rental.product.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-muted="state=='cancel'" decoration-info="state=='draft'" decoration-success="state=='confirm'">
|
||||
<field name="customer_name" />
|
||||
<field name="res_number" />
|
||||
<field name="from_date" />
|
||||
<field name="to_date" />
|
||||
<field name="state" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_product_rental_order_pivot">
|
||||
<field name="name">rental.order.pivot</field>
|
||||
<field name="model">rental.product.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Rental Orders">
|
||||
<field name="customer_name" type="row" />
|
||||
<field name="count" interval="month" type="col" />
|
||||
<field name="total_amount" type="measure" />
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_product_rental_order_graph">
|
||||
<field name="name">rental.order.graph</field>
|
||||
<field name="model">rental.product.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Rental Orders">
|
||||
<field name="customer_name" type="row" />
|
||||
<field name="total_amount" type="col" />
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_product_order_id" model="ir.actions.act_window">
|
||||
<field name="name">Rental Order</field>
|
||||
<field name="res_model">rental.product.order</field>
|
||||
<field name="view_mode">tree,form,calendar</field>
|
||||
<field name="context">{'search_default_undone': 1}</field>
|
||||
</record>
|
||||
|
||||
<record id="action_product_order_id_new" model="ir.actions.act_window">
|
||||
<field name="name">Rental Order</field>
|
||||
<field name="res_model">rental.product.order</field>
|
||||
<field name="view_mode">form,tree</field>
|
||||
</record>
|
||||
|
||||
<record id="action_rental_order_analysis" model="ir.actions.act_window">
|
||||
<field name="name">Rental Order</field>
|
||||
<field name="res_model">rental.product.order</field>
|
||||
<field name="view_mode">graph,pivot</field>
|
||||
</record>
|
||||
|
||||
<record id="product_booking_calendar" model="ir.ui.view">
|
||||
<field name="name">Rental order</field>
|
||||
<field name="model">rental.product.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<calendar string="product" date_start="from_date" date_stop="to_date" event_open_popup="true" quick_add="false" form_view_id="%(product_rental_bookings.action_product_order_id)s" event_limit="3" color="customer_name" mode="month">
|
||||
<field name="customer_name" />
|
||||
</calendar>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
25
product_rental_bookings/views/product_view.xml
Executable file
25
product_rental_bookings/views/product_view.xml
Executable file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="product_normal_form_view_inherit" model="ir.ui.view">
|
||||
<field name="name">Product Variant</field>
|
||||
<field name="model">product.product</field>
|
||||
<field name="inherit_id" ref="product.product_normal_form_view" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='purchase_ok']/parent::div" position="after">
|
||||
<div>
|
||||
<field name="is_rental" />
|
||||
<label for="is_rental" />
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='uom_po_id']" position="after">
|
||||
<field name="rental_amount" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
||||
<field name="rental_amount_per_hour" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
||||
<field name="rental_amount_per_session" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
60
product_rental_bookings/views/res_config_settings_view.xml
Executable file
60
product_rental_bookings/views/res_config_settings_view.xml
Executable file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="rental_res_config_settings_form_inherit" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.from</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="website.res_config_settings_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[hasclass('settings')]" position="inside">
|
||||
<div class="app_settings_block" data-string="Rental" string="Rental" data-key="product_rental_bookings">
|
||||
<h2>Rental Configuration</h2>
|
||||
<div class="row o_settings_container mt16">
|
||||
<div class="col-xs-12 col-md-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<div class="content-group">
|
||||
<div class="row mt16">
|
||||
<label for="enabled_day_rent" class="col-md-3 o_light_label" style="font-weight:bold;" />
|
||||
<field name="enabled_day_rent" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<div class="content-group">
|
||||
<div class="row mt16">
|
||||
<label for="enabled_hour_rent" class="col-md-3 o_light_label" style="font-weight:bold;" />
|
||||
<field name="enabled_hour_rent" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<div class="content-group">
|
||||
<div class="row mt16">
|
||||
<label for="enabled_session_rent" class="col-md-3 o_light_label" style="font-weight:bold;" />
|
||||
<field name="enabled_session_rent" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="rental_config_settings_action" model="ir.actions.act_window">
|
||||
<field name="name">Settings</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">res.config.settings</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">inline</field>
|
||||
<field name="context">{'module' : 'product_rental_bookings', 'bin_size': False}</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
39
product_rental_bookings/views/sales_config_view.xml
Executable file
39
product_rental_bookings/views/sales_config_view.xml
Executable file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="rental_view_order_form" model="ir.ui.view">
|
||||
<field name="name">sale.order.form</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="partner_id" position="after">
|
||||
<field name="signature" widget="image" options="{'always_reload': True, 'size': [90, 90]}" />
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.delivery</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="sale.res_config_settings_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@class='app_settings_block']" position="inside">
|
||||
<h2>Signature</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-xs-12 col-md-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="signature" />
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="signature" />
|
||||
<div class="text-muted">
|
||||
Required signature before confirm sale order.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
49
product_rental_bookings/views/sequence.xml
Executable file
49
product_rental_bookings/views/sequence.xml
Executable file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="sequence_product_information" model="ir.sequence">
|
||||
<field name="name">product Contract</field>
|
||||
<field name="code">product_contract</field>
|
||||
<field name="prefix">L</field>
|
||||
<field name="padding">3</field>
|
||||
<field name="suffix">/%(month)s/%(day)s</field>
|
||||
<field name="company_id" eval="False" />
|
||||
</record>
|
||||
|
||||
<record id="sequence_product_registration" model="ir.sequence">
|
||||
<field name="name">product Registration</field>
|
||||
<field name="code">product_registration</field>
|
||||
<field name="prefix">REG</field>
|
||||
<field name="padding">3</field>
|
||||
<field name="suffix">/%(month)s/%(day)s</field>
|
||||
<field name="company_id" eval="False" />
|
||||
</record>
|
||||
|
||||
<record id="sequence_product_move" model="ir.sequence">
|
||||
<field name="name">product Move</field>
|
||||
<field name="code">product_move</field>
|
||||
<field name="prefix">PM</field>
|
||||
<field name="padding">3</field>
|
||||
<field name="suffix">/%(month)s/%(day)s</field>
|
||||
<field name="company_id" eval="False" />
|
||||
</record>
|
||||
|
||||
<record id="sequence_quick_search" model="ir.sequence">
|
||||
<field name="name">product Search</field>
|
||||
<field name="code">product_booking</field>
|
||||
<field name="prefix">QS</field>
|
||||
<field name="padding">3</field>
|
||||
<field name="suffix">%(day)s</field>
|
||||
<field name="company_id" eval="False" />
|
||||
</record>
|
||||
|
||||
<record id="sequence_calender_record" model="ir.sequence">
|
||||
<field name="name">Full Calender</field>
|
||||
<field name="code">virtual_</field>
|
||||
<field name="padding">3</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<odoo>
|
||||
<data>
|
||||
<record id='session_config_form' model="ir.ui.view">
|
||||
<field name="name">Session Configuration Form</field>
|
||||
<field name="model">session.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name" required="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group col="4">
|
||||
<group>
|
||||
<field name="start_time" widget="float_time"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="end_time" widget="float_time"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id='session_config_tree' model="ir.ui.view">
|
||||
<field name="name">Session Configuration Tree</field>
|
||||
<field name="model">session.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="start_time" widget="float_time"/>
|
||||
<field name="end_time" widget="float_time"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id='action_session_config' model='ir.actions.act_window'>
|
||||
<field name="name">Session Configuration</field>
|
||||
<field name="res_model">session.config</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
31
product_rental_bookings/views/stock_picking.xml
Normal file
31
product_rental_bookings/views/stock_picking.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="stock_picking_form_inherit" model="ir.ui.view">
|
||||
<field name="name">view.picking.form.inherit</field>
|
||||
<field name="model">stock.picking</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_confirm']" position="after">
|
||||
<button name="incoming" attrs="{'invisible': [('state', 'in', ['draft', 'waiting', 'assigned', 'done', 'cancel'])]}" string="Receive" type="object" class="oe_highlight" states="on_rent" />
|
||||
<button name="delivery" attrs="{'invisible': [('state', 'in', ['waiting', 'confirmed', 'assigned', 'done', 'cancel'])]}" string="Delivery" type="object" class="oe_highlight" groups="base.group_user" />
|
||||
<button name="move" attrs="{'invisible': [('state', 'in', ['draft','waiting', 'confirmed', 'done', 'cancel'])]}" string="Move to Store" type="object" class="oe_highlight" />
|
||||
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='partner_id']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='move_ids_without_package']/tree/field[@name='product_id']" position="after">
|
||||
<field name="products_checked" />
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='origin']" position="after">
|
||||
<field name="rental_move_type" invisible="1" />
|
||||
<field name="is_rental" invisible="1" />
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='origin']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
3
product_rental_bookings/wizard/__init__.py
Executable file
3
product_rental_bookings/wizard/__init__.py
Executable file
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import advance_payment_invoice
|
177
product_rental_bookings/wizard/advance_payment_invoice.py
Executable file
177
product_rental_bookings/wizard/advance_payment_invoice.py
Executable file
@@ -0,0 +1,177 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import time
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ProductAdvancePaymentInvoice(models.TransientModel):
|
||||
_name = "product.advance.payment.invoice"
|
||||
_description = "product Advance Payment Invoice"
|
||||
|
||||
@api.model
|
||||
def _count(self):
|
||||
return len(self._context.get("active_ids", []))
|
||||
|
||||
@api.model
|
||||
def _get_advance_payment_method(self):
|
||||
if self._count() == 1:
|
||||
rental_obj = self.env["rental.product.order"]
|
||||
order = rental_obj.browse(self._context.get("active_ids"))[0]
|
||||
if order.invoice_count:
|
||||
return "all"
|
||||
return "delivered"
|
||||
|
||||
@api.model
|
||||
def _default_product_id(self):
|
||||
product_id = (
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("sale.default_deposit_product_id")
|
||||
)
|
||||
return self.env["product.product"].browse(int(product_id))
|
||||
|
||||
@api.model
|
||||
def _default_deposit_account_id(self):
|
||||
return self._default_product_id().property_account_income_id
|
||||
|
||||
@api.model
|
||||
def _default_deposit_taxes_id(self):
|
||||
return self._default_product_id().taxes_id
|
||||
|
||||
advance_payment_method = fields.Selection(
|
||||
[
|
||||
("delivered", "Invoiceable lines"),
|
||||
("all", "Invoiceable lines (deduct down payments)"),
|
||||
("percentage", "Down payment (percentage)"),
|
||||
("fixed", "Down payment (fixed amount)"),
|
||||
],
|
||||
string="What do you want to invoice?",
|
||||
default=_get_advance_payment_method,
|
||||
required=True,
|
||||
)
|
||||
product_id = fields.Many2one(
|
||||
"product.product",
|
||||
string="Down Payment Product",
|
||||
domain=[("type", "=", "service")],
|
||||
default=_default_product_id,
|
||||
)
|
||||
count = fields.Integer(default=_count, string="# of Orders")
|
||||
amount = fields.Float(
|
||||
"Down Payment Amount",
|
||||
help="The amount to be invoiced in advance, taxes excluded.",
|
||||
)
|
||||
deposit_account_id = fields.Many2one(
|
||||
"account.account",
|
||||
string="Income Account",
|
||||
domain=[("deprecated", "=", False)],
|
||||
help="Account used for deposits",
|
||||
default=_default_deposit_account_id,
|
||||
)
|
||||
deposit_taxes_id = fields.Many2many(
|
||||
"account.tax",
|
||||
string="Customer Taxes",
|
||||
help="Taxes used for deposits",
|
||||
default=_default_deposit_taxes_id,
|
||||
)
|
||||
|
||||
def _create_invoice(self, order, ro_line, amount):
|
||||
inv_obj = self.env["account.move"]
|
||||
|
||||
account_id = False
|
||||
if self.product_id.id:
|
||||
account_id = self.product_id.property_account_income_id.id
|
||||
|
||||
if self.amount <= 0.00:
|
||||
raise UserError(_("The value of the down payment amount must be positive."))
|
||||
|
||||
if self.advance_payment_method == "percentage":
|
||||
amount = order.untaxed_amount * self.amount / 100
|
||||
name = _("Down payment of %s%%") % (self.amount,)
|
||||
else:
|
||||
amount = self.amount
|
||||
name = _("Down Payment")
|
||||
|
||||
taxes = self.product_id.taxes_id
|
||||
tax_ids = taxes.ids
|
||||
|
||||
invoice = inv_obj.create(
|
||||
{
|
||||
"name": order.res_number,
|
||||
"origin": order.res_number,
|
||||
"partner_id": order.customer_name.id,
|
||||
"type": "out_invoice",
|
||||
"reference": False,
|
||||
"account_id": order.customer_name.property_account_receivable_id.id,
|
||||
"invoice_line_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": name,
|
||||
"account_id": account_id,
|
||||
"price_unit": amount,
|
||||
"product_id": self.product_id.id,
|
||||
"invoice_line_tax_ids": [(6, 0, tax_ids)],
|
||||
},
|
||||
)
|
||||
],
|
||||
"currency_id": order.pricelist_id.currency_id.id,
|
||||
"user_id": order.user_id.id,
|
||||
}
|
||||
)
|
||||
invoice.compute_taxes()
|
||||
invoice.message_post_with_view(
|
||||
"mail.message_origin_link",
|
||||
values={"self": invoice, "origin": order},
|
||||
subtype_id=self.env.ref("mail.mt_note").id,
|
||||
)
|
||||
return invoice
|
||||
|
||||
def create_invoices(self):
|
||||
rental_orders = self.env["rental.product.order"].browse(
|
||||
self._context.get("active_ids", [])
|
||||
)
|
||||
if self.advance_payment_method == "delivered":
|
||||
rental_orders.action_invoice_create()
|
||||
elif self.advance_payment_method == "all":
|
||||
rental_orders.action_invoice_create(final=True)
|
||||
else:
|
||||
# Create deposit product if necessary
|
||||
if not self.product_id:
|
||||
vals = self._prepare_deposit_product()
|
||||
self.product_id = self.env["product.product"].create(vals)
|
||||
self.env["ir.config_parameter"].sudo().set_param(
|
||||
"sale.default_deposit_product_id", self.product_id.id
|
||||
)
|
||||
|
||||
rental_line_obj = self.env["product.order.line"]
|
||||
for order in rental_orders:
|
||||
if self.advance_payment_method == "percentage":
|
||||
amount = order.untaxed_amount * self.amount / 100
|
||||
else:
|
||||
amount = self.amount
|
||||
taxes = self.product_id.taxes_id
|
||||
tax_ids = taxes.ids
|
||||
ro_line = rental_line_obj.create(
|
||||
{
|
||||
"name": _("Advance: %s") % (time.strftime("%m %Y"),),
|
||||
"price": amount,
|
||||
"product_order_id": order.id,
|
||||
"tax_id": [(6, 0, tax_ids)],
|
||||
}
|
||||
)
|
||||
self._create_invoice(order, ro_line, amount)
|
||||
if self._context.get("open_invoices", False):
|
||||
return rental_orders.action_view_invoice()
|
||||
return {"type": "ir.actions.act_window_close"}
|
||||
|
||||
def _prepare_deposit_product(self):
|
||||
return {
|
||||
"name": "Down payment",
|
||||
"type": "service",
|
||||
"invoice_policy": "order",
|
||||
"property_account_income_id": self.deposit_account_id.id,
|
||||
"taxes_id": [(6, 0, self.deposit_taxes_id.ids)],
|
||||
}
|
56
product_rental_bookings/wizard/advance_payment_invoice.xml
Executable file
56
product_rental_bookings/wizard/advance_payment_invoice.xml
Executable file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_product_advance_payment_invoice" model="ir.ui.view">
|
||||
<field name="name">Invoice Rental Orders</field>
|
||||
<field name="model">product.advance.payment.invoice</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Invoice Rentals Order">
|
||||
<p class="oe_grey">
|
||||
Invoices will be created in draft so that you can review
|
||||
them before validation.
|
||||
</p>
|
||||
<group>
|
||||
<field name="count" readonly="True"/>
|
||||
<field name="advance_payment_method" class="oe_inline" widget="radio"
|
||||
/>
|
||||
<field name="product_id"
|
||||
context="{'search_default_services': 1, 'default_type': 'service', 'default_invoice_policy': 'order'}"
|
||||
class="oe_inline"
|
||||
invisible="1"/>
|
||||
<label for="amount"
|
||||
attrs="{'invisible': [('advance_payment_method', 'not in', ('fixed','percentage'))]}"/>
|
||||
<div attrs="{'invisible': [('advance_payment_method', 'not in', ('fixed','percentage'))]}">
|
||||
<field name="amount"
|
||||
attrs="{'required': [('advance_payment_method', 'in', ('fixed','percentage'))]}"
|
||||
class="oe_inline" widget="monetary"/>
|
||||
</div>
|
||||
<field name="deposit_account_id" class="oe_inline"
|
||||
attrs="{'invisible': ['|', ('advance_payment_method', 'not in', ('fixed', 'percentage')), ('product_id', '!=', False)]}"
|
||||
groups="account.group_account_manager"/>
|
||||
<field name="deposit_taxes_id" class="oe_inline" widget="many2many_tags"
|
||||
domain="[('type_tax_use','=','sale')]"
|
||||
attrs="{'invisible': ['|', ('advance_payment_method', 'not in', ('fixed', 'percentage')), ('product_id', '!=', False)]}"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="create_invoices" string="Create and View Invoices" type="object"
|
||||
context="{'open_invoices': True}" class="btn-primary"/>
|
||||
<button name="create_invoices" string="Create Invoices" type="object"
|
||||
class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-default" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_view_product_advance_payment_invoice" model="ir.actions.act_window">
|
||||
<field name="name">Invoice Rental Orders</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">product.advance.payment.invoice</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
Reference in New Issue
Block a user