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