feat: services excel
This commit is contained in:
1
generators/services_excel_generator/__init__.py
Normal file
1
generators/services_excel_generator/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .core import ServiceExcelExporter
|
||||||
183
generators/services_excel_generator/core.py
Normal file
183
generators/services_excel_generator/core.py
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import openpyxl
|
||||||
|
import openpyxl.styles
|
||||||
|
from six import BytesIO
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
|
from constants import APP_PATH
|
||||||
|
from enums.service import ServiceType
|
||||||
|
from models import Service, ServiceCategory
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceExcelExporter:
|
||||||
|
SERVICE_TYPE_FONT = openpyxl.styles.Font(bold=True, size=14, name='Calibri')
|
||||||
|
SERVICE_TYPE_FILL = openpyxl.styles.PatternFill(start_color='0000FF', end_color='0000FF', fill_type='solid')
|
||||||
|
SERVICE_TYPE_ALIGNMENT = openpyxl.styles.Alignment(horizontal='center')
|
||||||
|
SERVICE_TYPE_BORDER = openpyxl.styles.Border(
|
||||||
|
left=openpyxl.styles.Side(style='medium'),
|
||||||
|
right=openpyxl.styles.Side(style='medium'),
|
||||||
|
top=openpyxl.styles.Side(style='medium'),
|
||||||
|
bottom=openpyxl.styles.Side(style='medium')
|
||||||
|
)
|
||||||
|
|
||||||
|
CATEGORY_FONT = openpyxl.styles.Font(bold=True, size=12, name='Calibri')
|
||||||
|
CATEGORY_FILL = openpyxl.styles.PatternFill(start_color='DBEEF4', end_color='DBEEF4', fill_type='solid')
|
||||||
|
CATEGORY_ALIGNMENT = openpyxl.styles.Alignment(horizontal='center')
|
||||||
|
CATEGORY_BORDER = SERVICE_TYPE_BORDER # Same as service type border
|
||||||
|
|
||||||
|
EVEN_ROW_FILL = openpyxl.styles.PatternFill(start_color='EBF1DE', end_color='EBF1DE', fill_type='solid')
|
||||||
|
|
||||||
|
def __init__(self, session):
|
||||||
|
self.session = session
|
||||||
|
self.template_path = Path(APP_PATH) / 'static' / 'excel' / 'services.xlsx'
|
||||||
|
self.workbook = None
|
||||||
|
self.worksheet = None
|
||||||
|
self.start_row = 12
|
||||||
|
self.name_column = 'A'
|
||||||
|
self.price_column = 'B'
|
||||||
|
self.categories_dict = {}
|
||||||
|
self.service_type_dict = {
|
||||||
|
ServiceType.DEAL_SERVICE: 'Общие услуги',
|
||||||
|
ServiceType.PRODUCT_SERVICE: 'Услуги фулфилмента'
|
||||||
|
}
|
||||||
|
|
||||||
|
async def get_services(self):
|
||||||
|
"""Fetch and organize services from the database."""
|
||||||
|
services = (await self.session.scalars(
|
||||||
|
select(Service)
|
||||||
|
.options(joinedload(Service.category))
|
||||||
|
.filter(Service.is_deleted == False)
|
||||||
|
.order_by(Service.rank)
|
||||||
|
)).all()
|
||||||
|
|
||||||
|
intermediate_result = defaultdict(lambda: defaultdict(list))
|
||||||
|
for service in services:
|
||||||
|
intermediate_result[service.service_type][service.category_id].append(service)
|
||||||
|
|
||||||
|
final_result = defaultdict(dict)
|
||||||
|
for service_type, categories_dict in intermediate_result.items():
|
||||||
|
categories = {service.category for services in categories_dict.values() for service in services}
|
||||||
|
|
||||||
|
def category_sort_key(category):
|
||||||
|
return category.deal_service_rank if service_type == ServiceType.DEAL_SERVICE else category.product_service_rank
|
||||||
|
|
||||||
|
sorted_categories = sorted(categories, key=category_sort_key)
|
||||||
|
sorted_categories_dict = {category.id: categories_dict[category.id] for category in sorted_categories}
|
||||||
|
final_result[service_type] = sorted_categories_dict
|
||||||
|
final_final_result = {}
|
||||||
|
for service_type in [ServiceType.DEAL_SERVICE, ServiceType.PRODUCT_SERVICE]:
|
||||||
|
final_final_result[service_type] = final_result[service_type]
|
||||||
|
return dict(final_final_result)
|
||||||
|
|
||||||
|
async def get_categories(self):
|
||||||
|
"""Fetch categories from the database."""
|
||||||
|
categories = (await self.session.scalars(
|
||||||
|
select(ServiceCategory)
|
||||||
|
.filter(ServiceCategory.is_deleted == False)
|
||||||
|
)).all()
|
||||||
|
self.categories_dict = {category.id: category for category in categories}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_ruble_number(number: int):
|
||||||
|
"""Format a number with spaces as thousand separators."""
|
||||||
|
return f'{number:,}'.replace(',', ' ')
|
||||||
|
|
||||||
|
def get_price_value(self, service: Service):
|
||||||
|
"""Get the price value string for a service."""
|
||||||
|
if service.price_ranges:
|
||||||
|
price_ranges_length = len(service.price_ranges)
|
||||||
|
if price_ranges_length == 1:
|
||||||
|
price = self.format_ruble_number(int(service.price_ranges[0].price))
|
||||||
|
return f'{price} ₽'
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for idx, price_range in enumerate(service.price_ranges):
|
||||||
|
price = self.format_ruble_number(int(price_range.price))
|
||||||
|
if idx == price_ranges_length - 1:
|
||||||
|
result.append(f'от {price_range.from_quantity} шт: {price} ₽')
|
||||||
|
else:
|
||||||
|
result.append(f'{price_range.from_quantity} шт - {price_range.to_quantity} шт: {price} ₽')
|
||||||
|
return '\n'.join(result)
|
||||||
|
else:
|
||||||
|
price = self.format_ruble_number(int(service.price))
|
||||||
|
return f'{price} ₽'
|
||||||
|
|
||||||
|
def load_template(self):
|
||||||
|
"""Load the Excel template."""
|
||||||
|
self.workbook = openpyxl.load_workbook(self.template_path)
|
||||||
|
self.worksheet = self.workbook.active
|
||||||
|
|
||||||
|
def format_service_type_cell(self, cell):
|
||||||
|
"""Apply formatting to a service type cell."""
|
||||||
|
cell.font = self.SERVICE_TYPE_FONT
|
||||||
|
cell.fill = self.SERVICE_TYPE_FILL
|
||||||
|
cell.alignment = self.SERVICE_TYPE_ALIGNMENT
|
||||||
|
cell.border = self.SERVICE_TYPE_BORDER
|
||||||
|
|
||||||
|
def format_category_cell(self, cell):
|
||||||
|
"""Apply formatting to a category cell."""
|
||||||
|
cell.font = self.CATEGORY_FONT
|
||||||
|
cell.fill = self.CATEGORY_FILL
|
||||||
|
cell.alignment = self.CATEGORY_ALIGNMENT
|
||||||
|
cell.border = self.CATEGORY_BORDER
|
||||||
|
|
||||||
|
def format_service_row(self, name_cell, price_cell, is_even_row):
|
||||||
|
"""Apply formatting to a service row."""
|
||||||
|
name_cell.alignment = openpyxl.styles.Alignment(wrap_text=True, vertical='center')
|
||||||
|
price_cell.alignment = openpyxl.styles.Alignment(wrap_text=True, horizontal='right', vertical="center")
|
||||||
|
if is_even_row:
|
||||||
|
name_cell.fill = self.EVEN_ROW_FILL
|
||||||
|
price_cell.fill = self.EVEN_ROW_FILL
|
||||||
|
|
||||||
|
def write_service_type_row(self, service_type):
|
||||||
|
"""Write a service type row to the worksheet."""
|
||||||
|
row = self.start_row
|
||||||
|
cell = self.worksheet[f'{self.name_column}{row}']
|
||||||
|
cell.value = self.service_type_dict[service_type]
|
||||||
|
self.worksheet.merge_cells(f'{self.name_column}{row}:{self.price_column}{row}')
|
||||||
|
self.format_service_type_cell(cell)
|
||||||
|
self.start_row += 1
|
||||||
|
|
||||||
|
def write_category_row(self, category_name):
|
||||||
|
"""Write a category row to the worksheet."""
|
||||||
|
row = self.start_row
|
||||||
|
cell = self.worksheet[f'{self.name_column}{row}']
|
||||||
|
cell.value = category_name
|
||||||
|
self.worksheet.merge_cells(f'{self.name_column}{row}:{self.price_column}{row}')
|
||||||
|
self.format_category_cell(cell)
|
||||||
|
self.start_row += 1
|
||||||
|
|
||||||
|
def write_service_row(self, service, counter):
|
||||||
|
"""Write a service row to the worksheet."""
|
||||||
|
row = self.start_row
|
||||||
|
name_cell = self.worksheet[f'{self.name_column}{row}']
|
||||||
|
price_cell = self.worksheet[f'{self.price_column}{row}']
|
||||||
|
name_cell.value = service.name
|
||||||
|
price_cell.value = self.get_price_value(service)
|
||||||
|
self.format_service_row(name_cell, price_cell, counter % 2 == 0)
|
||||||
|
self.start_row += 1
|
||||||
|
|
||||||
|
async def generate_excel(self):
|
||||||
|
"""Generate the Excel file with services and categories."""
|
||||||
|
start = time.time()
|
||||||
|
await self.get_categories()
|
||||||
|
services = await self.get_services()
|
||||||
|
self.load_template()
|
||||||
|
|
||||||
|
for service_type, categories in services.items():
|
||||||
|
self.write_service_type_row(service_type)
|
||||||
|
for category_id, services_list in categories.items():
|
||||||
|
category_name = self.categories_dict[category_id].name
|
||||||
|
self.write_category_row(category_name)
|
||||||
|
for idx, service in enumerate(services_list):
|
||||||
|
self.write_service_row(service, idx)
|
||||||
|
|
||||||
|
result = BytesIO()
|
||||||
|
self.workbook.save(result)
|
||||||
|
result.seek(0)
|
||||||
|
print('Elapsed time:', time.time() - start)
|
||||||
|
return result
|
||||||
BIN
static/excel/services.xlsx
Normal file
BIN
static/excel/services.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user