You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

439 lines
19 KiB

# -*- coding: utf-8 -*-
###############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Jumana Haseen (odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################
import pytz
from twilio.rest import Client
from odoo import api, fields, models
from datetime import datetime
try:
import qrcode
except ImportError:
qrcode = None
try:
import base64
except ImportError:
base64 = None
class PosOrder(models.Model):
"""Inherited the pos_order class to add filed and function to calculate pos
order details in the dashboard menu"""
_inherit = 'pos.order'
is_exchange = fields.Boolean(string='Exchange',
help='Will enable when create an exchange'
'order')
@api.model
def pos_exchange_order(self, order_id):
"""Mark order a exchanged"""
self.browse(order_id).is_exchange = True
@api.model
def get_invoice(self, id):
"""Retrieve the corresponding invoice details based on the provided ID.
Args:
id (int): The ID of the invoice.
Returns:
dict: A dictionary containing the invoice details.
"""
pos_id = self.search([('pos_reference', '=', id)])
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
invoice_id = self.env['account.move'].search(
[('invoice_origin', '=', pos_id.name)])
return {
'invoice_id': invoice_id.id,
'invoice_name': invoice_id.name,
'base_url': base_url,
'is_qr_code': invoice_id.account_barcode}
@api.model
def get_department(self, option):
"""
Retrieve sales data based on the specified option for POS (Point of Sale) orders.
This method generates a sales report based on the given option, which can be
'pos_hourly_sales', 'pos_monthly_sales', or default to annual sales grouped by month.
The report is generated for the current company and for the current month or year
as appropriate.
Args:
option (str): The type of sales report to generate. Possible values are:
- 'pos_hourly_sales': Hourly sales data for the current month.
- 'pos_monthly_sales': Daily sales data for the current month.
- Any other value: Monthly sales data for the current year.
Returns:
list: A list containing three elements:
- order (list): A list of total sales amounts.
- today (list): A list of time labels corresponding to the sales amounts
(hours, days, or months).
- label (str): A string indicating the type of time label used ('HOURS', 'DAYS', 'MONTHS').
Raises:
Exception: If an error occurs while executing the database query.
"""
company_id = self.env.company.id
if option == 'pos_hourly_sales':
user_tz = self.env.user.tz if self.env.user.tz else pytz.UTC
query = ('''select EXTRACT(hour FROM date_order at time zone
'utc' at time zone '{}') as date_month,sum(amount_total) from
pos_order where EXTRACT(month FROM date_order::date) =
EXTRACT(month FROM CURRENT_DATE) AND pos_order.company_id = '''
+ str(company_id) + ''' group by date_month ''')
query = query.format(user_tz)
label = 'HOURS'
elif option == 'pos_monthly_sales':
query = '''select date_order::date as date_month,sum(amount_total)
from pos_order where EXTRACT(month FROM date_order::date) = EXTRACT
(month FROM CURRENT_DATE) AND pos_order.company_id = ''' + str(
company_id) + ''' group by date_month '''
label = 'DAYS'
else:
query = '''select TO_CHAR(date_order,'MON')date_month,
sum(amount_total) from pos_order where EXTRACT(year FROM
date_order::date) = EXTRACT(year FROM CURRENT_DATE) AND
pos_order.company_id = ''' + str( company_id) + ''' group by
date_month'''
label = 'MONTHS'
self._cr.execute(query)
docs = self._cr.dictfetchall()
order = []
for record in docs:
order.append(record.get('sum'))
today = []
for record in docs:
today.append(record.get('date_month'))
final = [order, today, label]
return final
@api.model
def get_details(self):
"""
Retrieve various details related to POS (Point of Sale) operations for the current company.
This method fetches payment details, salesperson performance, and session statuses
from the POS system. The details include the total amount collected per payment method,
the total sales and order count per salesperson, and the status of active POS sessions.
Returns:
dict: A dictionary containing the following keys:
- 'payment_details': A list of tuples, each containing the name of the payment
method and the total amount collected, formatted with the
company currency symbol.
- 'salesperson': A list of tuples, each containing the name of the salesperson,
the total sales amount formatted with the company currency
symbol, and the number of orders processed.
- 'selling_product': A list of dictionaries, each representing a POS session with
its name and status (e.g., 'Closed', 'Opened').
Raises:
Exception: If an error occurs during database queries or data formatting.
"""
company_id = self.env.company.id
cr = self._cr
cr.execute(
"""select pos_payment_method.name,sum(amount) from pos_payment inner
join pos_payment_method on pos_payment_method.id=pos_payment.
payment_method_id group by pos_payment_method.name ORDER
BY sum(amount) DESC; """)
payment_details = cr.fetchall()
cr.execute(
'''select hr_employee.name,sum(pos_order.amount_paid) as total,
count(pos_order.amount_paid) as orders from pos_order inner join
hr_employee on pos_order.user_id = hr_employee.user_id
where pos_order.company_id =''' + str(
company_id) + '''GROUP BY hr_employee.name order by total
DESC;''')
salesperson = cr.fetchall()
total_sales = []
for rec in salesperson:
rec = list(rec)
sym_id = rec[1]
company = self.env.company
if company.currency_id.position == 'after':
rec[1] = "%s %s" % (sym_id, company.currency_id.symbol)
else:
rec[1] = "%s %s" % (company.currency_id.symbol, sym_id)
rec = tuple(rec)
total_sales.append(rec)
cr.execute(
'''select DISTINCT(product_template.name) as product_name,sum(qty)
as total_quantity from pos_order_line inner join product_product on
product_product.id=pos_order_line.product_id inner join
product_template on product_product.product_tmpl_id = product_template.id
where pos_order_line.company_id =''' + str(
company_id) + ''' group by product_template.id ORDER
BY total_quantity DESC Limit 10 ''')
sessions = self.env['pos.config'].search([])
sessions_list = []
dict = {
'closing_control': 'Closed',
'opened': 'Opened',
'new_session': 'New Session',
'opening_control': "Opening Control"
}
for session in sessions:
sessions_list.append({
'session': session.name,
'status': dict.get(session.pos_session_state)
})
payments = []
for rec in payment_details:
rec = list(rec)
sym_id = rec[1]
company = self.env.company
if company.currency_id.position == 'after':
rec[1] = "%s %s" % (sym_id, company.currency_id.symbol)
else:
rec[1] = "%s %s" % (company.currency_id.symbol, sym_id)
rec = tuple(rec)
payments.append(rec)
return {
'payment_details': payments,
'salesperson': total_sales,
'selling_product': sessions_list,
}
@api.model
def get_refund_details(self):
"""
Retrieve details about sales, refunds, and POS sessions for the current date.
This method calculates various metrics related to POS (Point of Sale) operations,
including the total sales amount, number of orders, refund counts, and session
information. It also formats the total sales with appropriate magnitude suffixes
(e.g., K for thousand, M for million) for easier readability.
Returns:
dict: A dictionary containing the following keys:
- 'total_sale': A formatted string representing the total sales amount with
an appropriate suffix (e.g., '1.2K' for 1200).
- 'total_order_count': An integer representing the total number of orders.
- 'total_refund_count': An integer representing the total number of refund orders.
- 'total_session': An integer representing the total number of POS sessions.
- 'today_refund_total': An integer representing the number of refunds made today.
- 'today_sale': An integer representing the number of sales made today.
Raises:
Exception: If an error occurs while querying the database or calculating the metrics.
"""
default_date = datetime.today().date()
pos_order = self.env['pos.order'].search([])
total = 0
today_refund_total = 0
total_order_count = 0
total_refund_count = 0
today_sale = 0
a = 0
for rec in pos_order:
if rec.amount_total < 0.0 and rec.date_order.date() == default_date:
today_refund_total = today_refund_total + 1
total_sales = rec.amount_total
total = total + total_sales
total_order_count = total_order_count + 1
if rec.date_order.date() == default_date:
today_sale = today_sale + 1
if rec.amount_total < 0.0:
total_refund_count = total_refund_count + 1
magnitude = 0
while abs(total) >= 1000:
magnitude += 1
total /= 1000.0
# add more suffixes if you need them
val = '%.2f%s' % (total, ['', 'K', 'M', 'G', 'T', 'P'][magnitude])
pos_session = self.env['pos.session'].search([])
total_session = 0
for record in pos_session:
total_session = total_session + 1
return {
'total_sale': val,
'total_order_count': total_order_count,
'total_refund_count': total_refund_count,
'total_session': total_session,
'today_refund_total': today_refund_total,
'today_sale': today_sale,
}
@api.model
def get_the_top_customer(self):
"""
Retrieve the top 10 customers based on the total amount spent in POS (Point of Sale) orders.
This method fetches the top customers who have made the highest total payments for
POS orders in the current company. It returns a list of the top 10 customers along
with the corresponding total amounts they have spent.
Returns:
list: A list containing two elements:
- order (list): A list of total amounts spent by the top 10 customers.
- day (list): A list of customer names corresponding to the total amounts.
Raises:
Exception: If an error occurs during the database query execution.
"""
company_id = self.env.company.id
query = '''select res_partner.name as customer,pos_order.partner_id,
sum(pos_order.amount_paid) as amount_total from pos_order inner join
res_partner on res_partner.id = pos_order.partner_id where pos_order
.company_id = ''' + str(
company_id) + ''' GROUP BY pos_order.partner_id,
res_partner.name ORDER BY amount_total DESC LIMIT 10;'''
self._cr.execute(query)
docs = self._cr.dictfetchall()
order = []
for record in docs:
order.append(record.get('amount_total'))
day = []
for record in docs:
day.append(record.get('customer'))
final = [order, day]
return final
@api.model
def get_the_top_products(self):
"""
Retrieve the top 10 products based on the total quantity sold in POS (Point of Sale) orders.
This method fetches the top-selling products in terms of quantity for the current company
and returns a list of the top 10 products along with their corresponding total quantities sold.
Returns:
list: A list containing two elements:
- total_quantity (list): A list of quantities for the top 10 products.
- product_name (list): A list of product names corresponding to the quantities.
Raises:
Exception: If an error occurs during the database query execution.
"""
company_id = self.env.company.id
query = '''select DISTINCT(product_template.name) as product_name,
sum(qty) as total_quantity from
pos_order_line inner join product_product on product_product.
id=pos_order_line.product_id inner join
product_template on product_product.product_tmpl_id =
product_template.id where pos_order_line.company_id = ''' + str(
company_id) + ''' group by product_template.id ORDER
BY total_quantity DESC Limit 10 '''
self._cr.execute(query)
top_product = self._cr.dictfetchall()
total_quantity = []
for record in top_product:
total_quantity.append(record.get('total_quantity'))
product_name = []
for record in top_product:
product_name.append(record.get('product_name'))
final = [total_quantity, product_name]
return final
@api.model
def get_the_top_categories(self):
"""
Retrieve the top product categories based on the total quantity sold in POS (Point of Sale) orders.
This method fetches the top-selling product categories for the current company and returns
a list containing the total quantities sold for each category, sorted in descending order.
Returns:
list: A list containing two elements:
- total_quantity (list): A list of quantities for the top-selling product categories.
- product_categ (list): A list of category names corresponding to the quantities.
Raises:
Exception: If an error occurs during the database query execution.
"""
company_id = self.env.company.id
query = ('''select DISTINCT(product_category.complete_name) as
product_category,sum(qty) as total_quantity from pos_order_line inner
join product_product on product_product.id=pos_order_line.product_id
inner join product_template on product_product.product_tmpl_id =
product_template.id inner join product_category on product_category.id
= product_template.categ_id where pos_order_line.company_id = ''' +
str(company_id) + ''' group by product_category ORDER BY
total_quantity DESC ''')
self._cr.execute(query)
top_product = self._cr.dictfetchall()
total_quantity = []
for record in top_product:
total_quantity.append(record.get('total_quantity'))
product_categ = []
for record in top_product:
product_categ.append(record.get('product_category'))
final = [total_quantity, product_categ]
return final
@api.model
def create_from_ui(self, orders, draft=False):
"""
Create POS orders from the UI and send SMS notifications to customers if configured.
This method extends the default behavior of creating POS orders from the user interface.
After the orders are created, it checks for configurations that allow SMS notifications
and sends an SMS to the customer using Twilio if applicable. The SMS message details
are also logged in the 'pos.greetings' model.
Args:
orders (list): A list of order dictionaries coming from the POS UI.
Returns:
list: The result from the parent 'create_from_ui' method, representing the
created orders.
Raises:
Exception: If an error occurs while sending the SMS, it is caught and suppressed.
"""
res = super(PosOrder, self).create_from_ui(orders, draft)
ids = []
for line in res:
if line['id']:
ids.append(line['id'])
backend_order = self.search([('id', 'in', ids)])
if backend_order:
for pos_order in backend_order:
config_id = pos_order.session_id.config_id
if config_id.customer_msg:
if pos_order.partner_id.phone:
try:
customer_phone = str(pos_order.partner_id.phone)
twilio_number = config_id.twilio_number
auth_token = config_id.auth_token
account_sid = config_id.account_sid
body = config_id.sms_body
client = Client(account_sid, auth_token)
client.messages.create(
body=body,
from_=twilio_number,
to=customer_phone
)
pos_greeting_obj = self.env['pos.greetings']
pos_greeting_obj.create({
'customer_id': pos_order.partner_id.id,
'order_id': pos_order.id,
'auth_token': auth_token,
'twilio_number': twilio_number,
'to_number': customer_phone,
'session_id': pos_order.session_id.id,
'sms_body': body,
'send_sms': True,
})
except:
pass
return res