summaryrefslogtreecommitdiff
path: root/archaeological_files
diff options
context:
space:
mode:
authorÉtienne Loks <etienne.loks@iggdrasil.net>2026-02-18 15:48:09 +0100
committerÉtienne Loks <etienne.loks@iggdrasil.net>2026-02-19 12:15:58 +0100
commit976248379d5866d8d46f9ec7974d3766f5b8c209 (patch)
tree2da0a01164f6a47c571082b9464b6d8dc59ff96c /archaeological_files
parent420f00dec6d2459d62855e3a891c49f58aadf01f (diff)
downloadIshtar-976248379d5866d8d46f9ec7974d3766f5b8c209.tar.bz2
Ishtar-976248379d5866d8d46f9ec7974d3766f5b8c209.zip
✨ admin - price agreement: export all prices in a readable LO Calc document
Diffstat (limited to 'archaeological_files')
-rw-r--r--archaeological_files/admin.py41
-rw-r--r--archaeological_files/models.py102
2 files changed, 141 insertions, 2 deletions
diff --git a/archaeological_files/admin.py b/archaeological_files/admin.py
index 3313c6b02..d7b0a763a 100644
--- a/archaeological_files/admin.py
+++ b/archaeological_files/admin.py
@@ -17,9 +17,14 @@
# 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.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 re_path, reverse
from django.utils.translation import gettext_lazy as _
@@ -255,13 +260,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 56de978c3..fec8625cd 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 gettext_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(