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
 | 
			
		||||
		Reference in New Issue
	
	Block a user