diff options
| -rw-r--r-- | archaeological_files/admin.py | 41 | ||||
| -rw-r--r-- | archaeological_files/models.py | 102 | ||||
| -rw-r--r-- | ishtar_common/libreoffice.py | 16 |
3 files changed, 157 insertions, 2 deletions
diff --git a/archaeological_files/admin.py b/archaeological_files/admin.py index e74dbbbff..97573265d 100644 --- a/archaeological_files/admin.py +++ b/archaeological_files/admin.py @@ -17,10 +17,15 @@ # See the file COPYING for details. +from io import BytesIO +import os +import tempfile + from django import forms from django.conf import settings from django.conf.urls import url -from django.http import HttpResponseRedirect +from django.contrib import messages +from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.urls import reverse from django.utils.translation import ugettext_lazy as _ @@ -256,13 +261,45 @@ class CopyPriceAgreementAdmin(GeneralTypeAdmin): ) +def export_prices(modeladmin, request, queryset): + if len(queryset) != 1: + error = str(_("Select only one price agreement.")) + modeladmin.message_user(request, error, level=messages.WARNING) + return + price_agreement = queryset[0] + with tempfile.TemporaryDirectory(prefix="ishtar-prices") as tmpdir: + pa_doc = price_agreement.generate_summary_document(tmpdir) + if not pa_doc: + modeladmin.message_user( + request, + str(_("Document not generated: is the LibreOffice daemon configured and running?")), + level=messages.ERROR + ) + return + + in_memory = BytesIO() + with open(pa_doc, "rb") as fle: + in_memory.write(fle.read()) + filename = pa_doc.split(os.sep)[-1].replace(" ", "_") + response = HttpResponse( + content_type="application/vnd.oasis.opendocument.spreadsheet" + ) + response["Content-Disposition"] = f"attachment; filename={filename}" + in_memory.seek(0) + response.write(in_memory.read()) + return response + + +export_prices.short_description = _("Export prices") + + class PriceAgreementAdmin(CopyPriceAgreementAdmin): list_filter = ("available",) extra_list_display = [ "start_date", "end_date", ] - actions = [] + actions = [export_prices] model = models.PriceAgreement diff --git a/archaeological_files/models.py b/archaeological_files/models.py index 33af742ac..b41836f6a 100644 --- a/archaeological_files/models.py +++ b/archaeological_files/models.py @@ -20,6 +20,8 @@ import datetime from collections import OrderedDict import json +import os +import tempfile from django.apps import apps from django.conf import settings @@ -30,6 +32,7 @@ from django.core.validators import MinValueValidator, MaxValueValidator from django.db.models import Q, Max from django.db.models.signals import post_save, m2m_changed, post_delete from django.urls import reverse, reverse_lazy +from django.utils.text import slugify from ishtar_common.models_common import OrderedHierarchicalType from ishtar_common.utils import ugettext_lazy as _, pgettext_lazy, get_current_profile, \ @@ -74,6 +77,13 @@ from archaeological_operations.models import ( ParcelItem, ) +UnoCalc = None +if settings.USE_LIBREOFFICE: + try: + from ishtar_common.libreoffice import UnoCalc + except ImportError: + pass + class PriceAgreement(GeneralType): order = models.IntegerField(_("Order"), default=10) @@ -91,6 +101,98 @@ class PriceAgreement(GeneralType): ) ADMIN_SECTION = _("Preventive") + def generate_summary_document(self, tmpdir=None): + if not UnoCalc: + return + uno = UnoCalc() + calc = uno.create_calc() + if not calc: + return + costs = {} + for cost in self.equipment_service_costs.filter( + parent__isnull=True).order_by("equipment_service_type__label", "id").all(): + if cost.equipment_service_type.txt_idx not in costs: + costs[cost.equipment_service_type.txt_idx] = [cost.equipment_service_type] + costs[cost.equipment_service_type.txt_idx].append(cost) + for cost in self.equipment_service_costs.filter( + parent__isnull=False).order_by("equipment_service_type__label", "id").all(): + if cost.parent.txt_idx not in costs: + costs[cost.parent.txt_idx] = [cost.equipment_service_type] + costs[cost.parent.txt_idx].append(cost) + + lst = [] + for type_slug in costs: + clst = [] + for cost in costs[type_slug][1:]: + clst.append(( + str(cost), + DCT_ES_UNITS[cost.unit] if cost.unit in DCT_ES_UNITS else "-", + cost.unitary_cost + )) + tpe = costs[type_slug][0] + lst.append([str(tpe), clst]) + + self._generate_page( + 0, _("Equipment - service costs"), + (_("Label"), _("Unit"), _("Price")), + lst, uno, calc) + + lst = [] + for job in self.jobs.order_by("order").all(): + lst.append(( + job.label, + _("Yes") if job.permanent_contract else _("No"), + job.ground_daily_cost, + job.daily_cost, + )) + header = [_("Label"), _("Permanent contract"), + _("Ground daily cost"), _("Daily cost")] + self._generate_page(1, _("Job cost"), header, lst, uno, calc, flat=True) + + if not tmpdir: + tmpdir = tempfile.mkdtemp(prefix="ishtar-prices") + base = "{}-{}.ods".format( + _("ishtar-price-agreement"), slugify(self.label)) + dest_filename = tmpdir + os.sep + base + uno.save_calc(calc, dest_filename) + return dest_filename + + def __set_cost_cell(self, sheet, uno, line, cost): + for idx, c in enumerate(cost): + cell = sheet.getCellByPosition(idx, line) + if isinstance(c, (int, float)): + cell.setValue(c) + else: + cell.setString(str(c)) + uno.format_cell_border(cell) + + def _generate_page(self, page_number, name, header, costs_lst, uno, calc, flat=False): + sheet = uno.get_or_create_sheet(calc, page_number) + sheet.Name = str(name) + for col_number, column in enumerate(header): + # header + cell = sheet.getCellByPosition(col_number, 0) + cell.CharWeight = 150 + cell.setString(str(column)) + uno.format_cell_border(cell) + + if flat: + line = 0 + for cost in costs_lst: + line += 1 + self.__set_cost_cell(sheet, uno, line, cost) + return + line = 1 + for label, costs in costs_lst: + line += 1 + cell = sheet.getCellByPosition(0, line) + cell.CharWeight = 150 + cell.setString(label) + uno.format_cell_border(cell) + for cost in costs: + line += 1 + self.__set_cost_cell(sheet, uno, line, cost) + class Job(GeneralType): price_agreement = models.ForeignKey( diff --git a/ishtar_common/libreoffice.py b/ishtar_common/libreoffice.py index 573d99361..27b31c849 100644 --- a/ishtar_common/libreoffice.py +++ b/ishtar_common/libreoffice.py @@ -7,6 +7,7 @@ from com.sun.star.awt import Size from com.sun.star.beans import PropertyValue from com.sun.star.connection import NoConnectException from com.sun.star.sheet.ValidationType import LIST +from com.sun.star.table import BorderLineStyle # nosec: filename used is generated and sanitized import subprocess # nosec @@ -45,6 +46,7 @@ class UnoClient: self.service_manager = None self.remote_context = None self.desktop = None + self.__line_format = None self.connect() def connect(self): @@ -92,6 +94,10 @@ class UnoClient: class UnoCalc(UnoClient): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__line_format = None + def create_calc(self): return self.create_document('scalc') @@ -192,6 +198,16 @@ class UnoCalc(UnoClient): # bitmap_component.removeByName(oo_id) return img_temp + def format_cell_border(self, cell): + if not self.__line_format: + self.__line_format = uno.createUnoStruct("com.sun.star.table.BorderLine2") + self.__line_format.LineStyle = BorderLineStyle.SOLID + self.__line_format.LineWidth = 20 + cell.TopBorder = self.__line_format + cell.RightBorder = self.__line_format + cell.LeftBorder = self.__line_format + cell.BottomBorder = self.__line_format + def test(self): self.test_2() |
