From db1c480905a2dfacea4d4cb8a597648178bf5c68 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 5 Jul 2022 12:22:24 +0200 Subject: [PATCH] Add exporter for list of products --- src/pretix/base/exporters/__init__.py | 1 + src/pretix/base/exporters/items.py | 218 ++++++++++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 src/pretix/base/exporters/items.py diff --git a/src/pretix/base/exporters/__init__.py b/src/pretix/base/exporters/__init__.py index b970dbe384..43cf23a37e 100644 --- a/src/pretix/base/exporters/__init__.py +++ b/src/pretix/base/exporters/__init__.py @@ -23,6 +23,7 @@ from .answers import * # noqa from .dekodi import * # noqa from .events import * # noqa from .invoices import * # noqa +from .items import * # noqa from .json import * # noqa from .mail import * # noqa from .orderlist import * # noqa diff --git a/src/pretix/base/exporters/items.py b/src/pretix/base/exporters/items.py new file mode 100644 index 0000000000..733d57f7a7 --- /dev/null +++ b/src/pretix/base/exporters/items.py @@ -0,0 +1,218 @@ +# +# This file is part of pretix (Community Edition). +# +# Copyright (C) 2014-2020 Raphael Michel and contributors +# Copyright (C) 2020-2021 rami.io GmbH and contributors +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General +# Public License as published by the Free Software Foundation in version 3 of the License. +# +# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are +# applicable granting you additional permissions and placing additional restrictions on your usage of this software. +# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive +# this file, see . +# +# 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 for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along with this program. If not, see +# . +# +from django.db.models import Prefetch +from django.dispatch import receiver +from django.utils.formats import date_format +from django.utils.translation import gettext_lazy as _ +from openpyxl.styles import Alignment +from openpyxl.utils import get_column_letter + +from ..channels import get_all_sales_channels +from ..exporter import ListExporter +from ..models import ItemMetaValue +from ..signals import register_data_exporters +from ...helpers.safe_openpyxl import SafeCell + + +def _max(a1, a2): + if a1 and a2: + return max(a1, a2) + return a1 or a2 + + +def _min(a1, a2): + if a1 and a2: + return min(a1, a2) + return a1 or a2 + + +class ItemDataExporter(ListExporter): + identifier = 'itemdata' + verbose_name = _('Product data') + + def iterate_list(self, form_data): + locales = self.event.settings.locales + scs = get_all_sales_channels() + header = [ + _("Product ID"), + _("Variation ID"), + _("Product category"), + _("Internal name"), + ] + for l in locales: + header.append( + _("Item name") + f" ({l})" + ) + for l in locales: + header.append( + _("Variation") + f" ({l})" + ) + header += [ + _("Active"), + _("Sales channels"), + _("Default price"), + _("Free price input"), + _("Sales tax"), + _("Is an admission ticket"), + _("Generate tickets"), + _("Waiting list"), + _("Available from"), + _("Available until"), + _("This product can only be bought using a voucher."), + _("This product will only be shown if a voucher matching the product is redeemed."), + _("Buying this product requires approval"), + _("Only sell this product as part of a bundle"), + _("Allow product to be canceled or changed"), + _("Minimum amount per order"), + _("Maximum amount per order"), + _("Requires special attention"), + _("Original price"), + _("This product is a gift card"), + _("Require a valid membership"), + _("Hide without a valid membership"), + ] + props = list(self.event.item_meta_properties.all()) + for p in props: + header.append(p.name) + + if form_data["_format"] == "xlsx": + row = [] + for h in header: + c = SafeCell(self.__ws, value=h) + c.alignment = Alignment(wrap_text=True, vertical='top') + row.append(c) + else: + row = header + + yield row + + for i in self.event.items.prefetch_related( + 'variations', + Prefetch( + 'meta_values', + ItemMetaValue.objects.select_related('property'), + to_attr='meta_values_cached' + ) + ).select_related('category', 'tax_rule'): + m = i.meta_data + vars = list(i.variations.all()) + + if vars: + for v in vars: + row = [ + i.pk, + v.pk, + str(i.category) if i.category else "", + i.internal_name or "", + ] + for l in locales: + row.append(i.name.localize(l)) + for l in locales: + row.append(v.value.localize(l)) + row += [ + _("Yes") if i.active and v.active else "", + ", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels and s in v.sales_channels]), + v.default_price or i.default_price, + _("Yes") if i.free_price else "", + str(i.tax_rule) if i.tax_rule else "", + _("Yes") if i.admission else "", + _("Yes") if i.generate_tickets else "", + _("Yes") if i.allow_waitinglist else "", + date_format(_max(i.available_from, v.available_from).astimezone(self.timezone), + "SHORT_DATETIME_FORMAT") if i.available_from or v.available_from else "", + date_format(_min(i.available_until, v.available_until).astimezone(self.timezone), + "SHORT_DATETIME_FORMAT") if i.available_until or v.available_until else "", + _("Yes") if i.require_voucher else "", + _("Yes") if i.hide_without_voucher or v.hide_without_voucher else "", + _("Yes") if i.require_approval or v.require_approval else "", + _("Yes") if i.require_bundling else "", + _("Yes") if i.allow_cancel else "", + i.min_per_order if i.min_per_order is not None else "", + i.max_per_order if i.max_per_order is not None else "", + _("Yes") if i.checkin_attention else "", + v.original_price or i.original_price or "", + _("Yes") if i.issue_giftcard else "", + _("Yes") if i.require_membership or v.require_membership else "", + _("Yes") if i.require_membership_hidden or v.require_membership_hidden else "", + ] + + else: + row = [ + i.pk, + "", + str(i.category) if i.category else "", + i.internal_name or "", + ] + for l in locales: + row.append(i.name.localize(l)) + for l in locales: + row.append("") + row += [ + _("Yes") if i.active else "", + ", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels]), + i.default_price, + _("Yes") if i.free_price else "", + str(i.tax_rule) if i.tax_rule else "", + _("Yes") if i.admission else "", + _("Yes") if i.generate_tickets else "", + _("Yes") if i.allow_waitinglist else "", + date_format(i.available_from.astimezone(self.timezone), + "SHORT_DATETIME_FORMAT") if i.available_from else "", + date_format(i.available_until.astimezone(self.timezone), + "SHORT_DATETIME_FORMAT") if i.available_until else "", + _("Yes") if i.require_voucher else "", + _("Yes") if i.hide_without_voucher else "", + _("Yes") if i.require_approval else "", + _("Yes") if i.require_bundling else "", + _("Yes") if i.allow_cancel else "", + i.min_per_order if i.min_per_order is not None else "", + i.max_per_order if i.max_per_order is not None else "", + _("Yes") if i.checkin_attention else "", + i.original_price or "", + _("Yes") if i.issue_giftcard else "", + _("Yes") if i.require_membership else "", + _("Yes") if i.require_membership_hidden else "", + ] + + row += [ + m.get(p.name, '') for p in props + ] + yield row + + def get_filename(self): + return '{}_products'.format(self.events.first().organizer.slug) + + def prepare_xlsx_sheet(self, ws): + self.__ws = ws + ws.freeze_panes = 'A1' + ws.column_dimensions['C'].width = 25 + ws.column_dimensions['D'].width = 25 + for i in range(len(self.event.settings.locales)): + ws.column_dimensions[get_column_letter(5 + 2 * i + 0)].width = 25 + ws.column_dimensions[get_column_letter(5 + 2 * i + 1)].width = 25 + ws.column_dimensions[get_column_letter(5 + 2 * len(self.event.settings.locales) + 1)].width = 25 + ws.row_dimensions[1].height = 40 + + +@receiver(register_data_exporters, dispatch_uid="exporter_itemdata") +def register_itemdata_exporter(sender, **kwargs): + return ItemDataExporter