feat: services excel

This commit is contained in:
2024-10-11 20:02:04 +03:00
parent 1eb85279b5
commit 8075ce04b0
3 changed files with 184 additions and 0 deletions

View File

@@ -0,0 +1 @@
from .core import ServiceExcelExporter

View 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

Binary file not shown.