diff --git a/TODO.md b/TODO.md index 4f1eb6f8..a32ec73c 100644 --- a/TODO.md +++ b/TODO.md @@ -62,3 +62,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im * DOCUMENT: orchestration.middleware: we need to know when an operation starts and ends in order to perform bulk server updates and also to wait for related objects to be saved (base object is saved first and then related) orders.signales: we perform changes right away because data model state can change under monitoring and other periodik task, and we should keep orders consistency under any situation. dependency collector with max_recursion that matches the number of dots on service.match and service.metric + + +* Be consistent with dates: name_on, created ? diff --git a/orchestra/admin/menu.py b/orchestra/admin/menu.py index 04b7a7a5..3fb6bffc 100644 --- a/orchestra/admin/menu.py +++ b/orchestra/admin/menu.py @@ -58,8 +58,6 @@ def get_accounts(): if isinstalled('orchestra.apps.orders'): url = reverse('admin:orders_order_changelist') accounts.append(items.MenuItem(_("Orders"), url)) - url = reverse('admin:orders_service_changelist') - accounts.append(items.MenuItem(_("Services"), url)) return accounts @@ -80,6 +78,8 @@ def get_administration_models(): administration_models.append('orchestra.apps.resources.*') if isinstalled('orchestra.apps.miscellaneous'): administration_models.append('orchestra.apps.miscellaneous.models.MiscService') + if isinstalled('orchestra.apps.orders'): + administration_models.append('orchestra.apps.orders.models.Service') return administration_models diff --git a/orchestra/apps/invoices/__init__.py b/orchestra/apps/bills/__init__.py similarity index 100% rename from orchestra/apps/invoices/__init__.py rename to orchestra/apps/bills/__init__.py diff --git a/orchestra/apps/bills/admin.py b/orchestra/apps/bills/admin.py new file mode 100644 index 00000000..daba9b73 --- /dev/null +++ b/orchestra/apps/bills/admin.py @@ -0,0 +1,50 @@ +from django.contrib import admin +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from orchestra.admin.utils import admin_link, admin_date +from orchestra.apps.accounts.admin import AccountAdminMixin + +from .filters import BillTypeListFilter +from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, Budget, + BillLine, BudgetLine) + + +class BillLineInline(admin.TabularInline): + model = BillLine + +class BudgetLineInline(admin.TabularInline): + model = Budget + + +class BillAdmin(AccountAdminMixin, admin.ModelAdmin): + list_display = ( + 'ident', 'status', 'bill_type_link', 'account_link', 'created_on_display' + ) + list_filter = (BillTypeListFilter, 'status',) + readonly_fields = ('ident',) + inlines = [BillLineInline] + + account_link = admin_link('account') + created_on_display = admin_date('created_on') + + def bill_type_link(self, bill): + bill_type = bill.bill_type.lower() + url = reverse('admin:bills_%s_changelist' % bill_type) + return '%s' % (url, bill.get_bill_type_display()) + bill_type_link.allow_tags = True + bill_type_link.short_description = _("type") + bill_type_link.admin_order_field = 'bill_type' + + def get_inline_instances(self, request, obj=None): + if self.model is Budget: + self.inlines = [BudgetLineInline] + return super(BillAdmin, self).get_inline_instances(request, obj=obj) + + +admin.site.register(Bill, BillAdmin) +admin.site.register(Invoice, BillAdmin) +admin.site.register(AmendmentInvoice, BillAdmin) +admin.site.register(Fee, BillAdmin) +admin.site.register(AmendmentFee, BillAdmin) +admin.site.register(Budget, BillAdmin) diff --git a/orchestra/apps/bills/api.py b/orchestra/apps/bills/api.py new file mode 100644 index 00000000..b96346fc --- /dev/null +++ b/orchestra/apps/bills/api.py @@ -0,0 +1,15 @@ +from rest_framework import viewsets + +from orchestra.api import router +from orchestra.apps.accounts.api import AccountApiMixin + +from .models import Bill +from .serializers import BillSerializer + + +class BillViewSet(AccountApiMixin, viewsets.ModelViewSet): + model = Bill + serializer_class = BillSerializer + + +router.register(r'bills', BillViewSet) diff --git a/orchestra/apps/bills/filters.py b/orchestra/apps/bills/filters.py new file mode 100644 index 00000000..4d52cfa8 --- /dev/null +++ b/orchestra/apps/bills/filters.py @@ -0,0 +1,39 @@ +from django.contrib.admin import SimpleListFilter +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + + +class BillTypeListFilter(SimpleListFilter): + """ Filter tickets by created_by according to request.user """ + title = 'Type' + parameter_name = '' + + def __init__(self, request, *args, **kwargs): + super(BillTypeListFilter, self).__init__(request, *args, **kwargs) + self.request = request + + def lookups(self, request, model_admin): + return ( + ('bill', _("All")), + ('invoice', _("Invoice")), + ('amendmentinvoice', _("Amendment invoice")), + ('fee', _("Fee")), + ('fee', _("Amendment fee")), + ('budget', _("Budget")), + ) + + + def queryset(self, request, queryset): + return queryset + + def value(self): + return self.request.path.split('/')[-2] + + def choices(self, cl): + for lookup, title in self.lookup_choices: + yield { + 'selected': self.value() == lookup, + 'query_string': reverse('admin:bills_%s_changelist' % lookup), + 'display': title, + } + diff --git a/orchestra/apps/bills/models.py b/orchestra/apps/bills/models.py index 453565bd..7c96c558 100644 --- a/orchestra/apps/bills/models.py +++ b/orchestra/apps/bills/models.py @@ -1,13 +1,148 @@ from django.db import models +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ + +from . import settings + + +class BillManager(models.Manager): + def get_queryset(self): + queryset = super(BillManager, self).get_queryset() + if self.model != Bill: + bill_type = self.model.get_type() + queryset = queryset.filter(bill_type=bill_type) + return queryset class Bill(models.Model): + OPEN = 'OPEN' + CLOSED = 'CLOSED' + SEND = 'SEND' + RETURNED = 'RETURNED' + PAID = 'PAID' + BAD_DEBT = 'BAD_DEBT' + STATUSES = ( + (OPEN, _("Open")), + (CLOSED, _("Closed")), + (SEND, _("Sent")), + (RETURNED, _("Returned")), + (PAID, _("Paid")), + (BAD_DEBT, _("Bad debt")), + ) + + TYPES = ( + ('INVOICE', _("Invoice")), + ('AMENDMENTINVOICE', _("Amendment invoice")), + ('FEE', _("Fee")), + ('AMENDMENTFEE', _("Amendment Fee")), + ('BUDGET', _("Budget")), + ) + + ident = models.CharField(_("identifier"), max_length=16, unique=True, + blank=True) + account = models.ForeignKey('accounts.Account', verbose_name=_("account"), + related_name='%(class)s') + bill_type = models.CharField(_("type"), max_length=16, choices=TYPES) + status = models.CharField(_("status"), max_length=16, choices=STATUSES, + default=OPEN) + created_on = models.DateTimeField(_("created on"), auto_now_add=True) + due_on = models.DateTimeField(_("due on"), null=True, blank=True) + last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True) + #base = models.DecimalField(max_digits=12, decimal_places=2) + #tax = models.DecimalField(max_digits=12, decimal_places=2) + comments = models.TextField(_("comments"), blank=True) + # TODO rename to HTML-agnostic term like.. RAW ? + html = models.TextField(_("HTML"), blank=True) + + objects = BillManager() + + def __unicode__(self): + return self.ident + + @classmethod + def get_type(cls): + return cls.__name__.upper() + + def set_ident(self): + cls = type(self) + bill_type = self.bill_type or cls.get_type() + if bill_type == 'BILL': + raise TypeError("get_new_ident() can not be used on a Bill class") + # Bill number resets every natural year + year = timezone.now().strftime("%Y") + bills = cls.objects.filter(created_on__year=year) + number_length = settings.BILLS_IDENT_NUMBER_LENGTH + prefix = getattr(settings, 'BILLS_%s_IDENT_PREFIX' % bill_type) + if self.status == self.OPEN: + prefix = 'O{}'.format(prefix) + bills = bills.filter(status=self.OPEN) + num_bills = bills.order_by('-ident').first() or 0 + if num_bills is not 0: + num_bills = int(num_bills.ident[-number_length:]) + else: + bills = bills.exclude(status=self.OPEN) + num_bills = bills.count() + zeros = (number_length - len(str(num_bills))) * '0' + number = zeros + str(num_bills + 1) + self.ident = '{prefix}{year}{number}'.format( + prefix=prefix, year=year, number=number) + + def save(self, *args, **kwargs): + if not self.bill_type: + self.bill_type = type(self).get_type() + if not self.ident or (self.ident.startswith('O') and self.status != self.OPEN): + self.set_ident() + super(Bill, self).save(*args, **kwargs) + + +class Invoice(Bill): + class Meta: + proxy = True + + +class AmendmentInvoice(Bill): + class Meta: + proxy = True + + +class Fee(Bill): + class Meta: + proxy = True + + +class AmendmentFee(Bill): + class Meta: + proxy = True + + +class Budget(Bill): + class Meta: + proxy = True + + +class BaseBillLine(models.Model): + bill = models.ForeignKey(Bill, verbose_name=_("bill"), + related_name='%(class)ss') + description = models.CharField(max_length=256) + initial_date = models.DateTimeField() + final_date = models.DateTimeField() + price = models.DecimalField(max_digits=12, decimal_places=2) + amount = models.IntegerField() + tax = models.DecimalField(max_digits=12, decimal_places=2) + + class Meta: + abstract = True + + +class BudgetLine(BaseBillLine): pass -class Invoice(models.Model): - pass +class BillLine(BaseBillLine): + order_id = models.PositiveIntegerField(blank=True) + order_last_bill_date = models.DateTimeField(null=True) + order_billed_until = models.DateTimeField(null=True) + auto = models.BooleanField(default=False) + amended_line = models.ForeignKey('self', verbose_name=_("amended line"), + related_name='amendment_lines', null=True, blank=True) - -class Fee(models.Model): - pass diff --git a/orchestra/apps/bills/serializers.py b/orchestra/apps/bills/serializers.py new file mode 100644 index 00000000..6233e7fa --- /dev/null +++ b/orchestra/apps/bills/serializers.py @@ -0,0 +1,15 @@ +from rest_framework import serializers + +from .models import Bill, BillLine + + +class BillLineSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = BillLine + + +class BillSerializer(serializers.HyperlinkedModelSerializer): + lines = BillLineSerializer(source='billlines') + + class Meta: + model = Bill diff --git a/orchestra/apps/bills/settings.py b/orchestra/apps/bills/settings.py new file mode 100644 index 00000000..45f58ffb --- /dev/null +++ b/orchestra/apps/bills/settings.py @@ -0,0 +1,8 @@ +from django.conf import settings + +BILLS_IDENT_NUMBER_LENGTH = getattr(settings, 'BILLS_IDENT_NUMBER_LENGTH', 4) +BILLS_INVOICE_IDENT_PREFIX = getattr(settings, 'BILLS_INVOICE_IDENT_PREFIX', 'I') +BILLS_AMENDMENT_INVOICE_IDENT_PREFIX = getattr(settings, 'BILLS_AMENDMENT_INVOICE_IDENT_PREFIX', 'A') +BILLS_FEE_IDENT_PREFIX = getattr(settings, 'BILLS_FEE_IDENT_PREFIX', 'F') +BILLS_AMENDMENT_FEE_IDENT_PREFIX = getattr(settings, 'BILLS_AMENDMENT_FEE_IDENT_PREFIX', 'B') +BILLS_BUDGET_IDENT_PREFIX = getattr(settings, 'BILLS_BUDGET_IDENT_PREFIX', 'Q') diff --git a/orchestra/apps/miscellaneous/admin.py b/orchestra/apps/miscellaneous/admin.py index 1b506b30..0221a611 100644 --- a/orchestra/apps/miscellaneous/admin.py +++ b/orchestra/apps/miscellaneous/admin.py @@ -32,7 +32,7 @@ class MiscellaneousAdmin(AccountAdminMixin, admin.ModelAdmin): def get_fields(self, request, obj=None): if obj is None: return ('service', 'account', 'description', 'amount', 'is_active') - if not obj.service.has_amount: + elif not obj.service.has_amount: return ('service', 'account_link', 'description', 'is_active') return ('service', 'account_link', 'description', 'amount', 'is_active') diff --git a/orchestra/apps/payments/__init__.py b/orchestra/apps/payments/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestra/apps/payments/admin.py b/orchestra/apps/payments/admin.py new file mode 100644 index 00000000..4c150b72 --- /dev/null +++ b/orchestra/apps/payments/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin + +from .models import PaymentSource, Transaction + + +admin.site.register(PaymentSource) +admin.site.register(Transaction) diff --git a/orchestra/apps/payments/api.py b/orchestra/apps/payments/api.py new file mode 100644 index 00000000..16246b3e --- /dev/null +++ b/orchestra/apps/payments/api.py @@ -0,0 +1,21 @@ +from rest_framework import viewsets + +from orchestra.api import router +from orchestra.apps.accounts.api import AccountApiMixin + +from .models import PaymentSource, Transaction +from .serializers import PaymentSourceSerializer, TransactionSerializer + + +class PaymentSourceViewSet(AccountApiMixin, viewsets.ModelViewSet): + model = PaymentSource + serializer_class = PaymentSourceSerializer + + +class TransactionViewSet(viewsets.ModelViewSet): + model = Transaction + serializer_class = TransactionSerializer + + +router.register(r'payment-sources', PaymentSourceViewSet) +router.register(r'transactions', TransactionViewSet) diff --git a/orchestra/apps/payments/methods.py b/orchestra/apps/payments/methods.py new file mode 100644 index 00000000..ff132bc4 --- /dev/null +++ b/orchestra/apps/payments/methods.py @@ -0,0 +1,15 @@ +from django.utils.translation import ugettext_lazy as _ + +from orchestra.utils import plugins + + +class PaymentMethod(plugins.Plugin): + __metaclass__ = plugins.PluginMount + + +class BankTransfer(PaymentMethod): + verbose_name = _("Bank transfer") + + +class CreditCard(PaymentMethod): + verbose_name = _("Credit card") diff --git a/orchestra/apps/payments/models.py b/orchestra/apps/payments/models.py new file mode 100644 index 00000000..a2f7a79c --- /dev/null +++ b/orchestra/apps/payments/models.py @@ -0,0 +1,44 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from jsonfield import JSONField + +from . import settings +from .methods import PaymentMethod + + +class PaymentSource(models.Model): + account = models.ForeignKey('accounts.Account', verbose_name=_("account"), + related_name='payment_sources') + method = models.CharField(_("method"), max_length=32, + choices=PaymentMethod.get_plugin_choices()) + data = JSONField(_("data")) + + +class Transaction(models.Model): + WAITTING_PROCESSING = 'WAITTING_PROCESSING' + WAITTING_CONFIRMATION = 'WAITTING_CONFIRMATION' + CONFIRMED = 'CONFIRMED' + REJECTED = 'REJECTED' + LOCKED = 'LOCKED' + DISCARTED = 'DISCARTED' + STATES = ( + (WAITTING_PROCESSING, _("Waitting for processing")), + (WAITTING_CONFIRMATION, _("Waitting for confirmation")), + (CONFIRMED, _("Confirmed")), + (REJECTED, _("Rejected")), + (LOCKED, _("Locked")), + (DISCARTED, _("Discarted")), + ) + + bill = models.ForeignKey('bills.bill', verbose_name=_("bill"), + related_name='transactions') + method = models.CharField(_("payment method"), max_length=32, + choices=PaymentMethod.get_plugin_choices()) + state = models.CharField(_("state"), max_length=32, choices=STATES, + default=WAITTING_PROCESSING) + data = JSONField(_("data")) + amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2) + currency = models.CharField(max_length=10, default=settings.PAYMENT_CURRENCY) + created_on = models.DateTimeField(auto_now_add=True) + modified_on = models.DateTimeField(auto_now=True) + related = models.ForeignKey('self', null=True, blank=True) diff --git a/orchestra/apps/payments/serializers.py b/orchestra/apps/payments/serializers.py new file mode 100644 index 00000000..ed164f89 --- /dev/null +++ b/orchestra/apps/payments/serializers.py @@ -0,0 +1,14 @@ +from rest_framework import serializers + +from .models import PaymentSource, PaymentSource + + +class PaymentSourceSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = PaymentSource + + +class TransactionSerializer(serializers.HyperlinkedModelSerializer): + + class Meta: + model = PaymentSource diff --git a/orchestra/apps/payments/settings.py b/orchestra/apps/payments/settings.py new file mode 100644 index 00000000..51c0f344 --- /dev/null +++ b/orchestra/apps/payments/settings.py @@ -0,0 +1,4 @@ +from django.conf import settings + + +PAYMENT_CURRENCY = getattr(settings, 'PAYMENT_CURRENCY', 'Eur') diff --git a/orchestra/bin/orchestra-admin b/orchestra/bin/orchestra-admin index 42638df1..f44d7456 100755 --- a/orchestra/bin/orchestra-admin +++ b/orchestra/bin/orchestra-admin @@ -143,7 +143,8 @@ function install_requirements () { paramiko==1.12.1 \ Pygments==1.6 \ django-filter==0.7 \ - passlib==1.6.2" + passlib==1.6.2 \ + jsonfield==0.9.22" if $testing; then APT="${APT} \ diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py index 941f5680..2672a067 100644 --- a/orchestra/conf/base_settings.py +++ b/orchestra/conf/base_settings.py @@ -79,6 +79,8 @@ INSTALLED_APPS = ( 'orchestra.apps.prices', 'orchestra.apps.orders', 'orchestra.apps.miscellaneous', + 'orchestra.apps.bills', + 'orchestra.apps.payments', # Third-party apps 'django_extensions', @@ -140,8 +142,9 @@ FLUENT_DASHBOARD_APP_GROUPS = ( 'orchestra.apps.contacts.models.Contact', 'orchestra.apps.users.models.User', 'orchestra.apps.orders.models.Order', - 'orchestra.apps.orders.models.Service', 'orchestra.apps.prices.models.Pack', + 'orchestra.apps.bills.models.Bill', + 'orchestra.apps.payments.models.Transaction', ), 'collapsible': True, }), @@ -154,6 +157,7 @@ FLUENT_DASHBOARD_APP_GROUPS = ( 'orchestra.apps.issues.models.Ticket', 'orchestra.apps.resources.models.Resource', 'orchestra.apps.resources.models.Monitor', + 'orchestra.apps.orders.models.Service', ), 'collapsible': True, }), @@ -172,20 +176,22 @@ FLUENT_DASHBOARD_APP_ICONS = { 'databases/database': 'database.png', 'databases/databaseuser': 'postgresql.png', 'vps/vps': 'TuxBox.png', - 'miscellaneous/miscellaneous': 'Misc-Misc-Box-icon.png', + 'miscellaneous/miscellaneous': 'applications-other.png', # Accounts 'accounts/account': 'Face-monkey.png', 'contacts/contact': 'contact.png', 'orders/order': 'basket.png', 'orders/service': 'price.png', - 'prices/pack': 'Dialog-accept.png', + 'prices/pack': 'Pack.png', + 'bills/bill': 'invoice.png', + 'payments/transaction': 'transaction.png', # Administration 'users/user': 'Mr-potato.png', 'djcelery/taskstate': 'taskstate.png', 'orchestration/server': 'vps.png', 'orchestration/route': 'hal.png', 'orchestration/backendlog': 'scriptlog.png', - 'issues/ticket': "Ticket_star.png", + 'issues/ticket': 'Ticket_star.png', 'resources/resource': "gauge.png", 'resources/monitor': "Utilities-system-monitor.png", } diff --git a/orchestra/static/orchestra/icons/gauge.png b/orchestra/static/orchestra/icons/gauge.png index d5d2e57e..686ebd04 100644 Binary files a/orchestra/static/orchestra/icons/gauge.png and b/orchestra/static/orchestra/icons/gauge.png differ diff --git a/orchestra/static/orchestra/icons/gauge.svg b/orchestra/static/orchestra/icons/gauge.svg index 806c0dd8..19e9e95b 100644 --- a/orchestra/static/orchestra/icons/gauge.svg +++ b/orchestra/static/orchestra/icons/gauge.svg @@ -2,6 +2,9 @@ + id="defs4"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/order.svg b/orchestra/static/orchestra/icons/order.svg index 5daa110b..60accb4d 100644 --- a/orchestra/static/orchestra/icons/order.svg +++ b/orchestra/static/orchestra/icons/order.svg @@ -1577,6 +1577,59 @@ style="stop-color:#eeeeec;stop-opacity:0" id="stop2711-8" /> + + + + + image/svg+xml - + @@ -1613,178 +1666,173 @@ id="layer1" inkscape:label="Layer 1" inkscape:groupmode="layer"> + + + + + + + id="g8099" + transform="matrix(0.93883963,0,0,0.93883963,-946.56186,-295.71302)"> - - + + + + + + + + + + + + + + style="fill:#2e5c02;fill-opacity:1;fill-rule:evenodd;stroke:none" + id="rect2325-6" + y="360.97729" + x="1011.6381" + height="1.179504" + width="27.128592" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - + style="fill:#2e5c02;fill-opacity:1;fill-rule:evenodd;stroke:none" + id="rect2327-0" + y="358.61829" + x="1011.6381" + height="1.179504" + width="27.128592" /> + + + + + + + + diff --git a/orchestra/static/orchestra/icons/transaction.png b/orchestra/static/orchestra/icons/transaction.png index 24df7ee9..37c87998 100644 Binary files a/orchestra/static/orchestra/icons/transaction.png and b/orchestra/static/orchestra/icons/transaction.png differ diff --git a/orchestra/static/orchestra/icons/transaction.svg b/orchestra/static/orchestra/icons/transaction.svg index 15156998..39b67d76 100644 --- a/orchestra/static/orchestra/icons/transaction.svg +++ b/orchestra/static/orchestra/icons/transaction.svg @@ -681,17 +681,6 @@ style="stop-color:white;stop-opacity:0" id="stop4226-3-7" /> - - - + + + + + + + + + + inkscape:window-width="1920" + inkscape:window-height="1024" + inkscape:window-x="0" + inkscape:window-y="27" + inkscape:window-maximized="1" /> @@ -920,498 +951,161 @@ inkscape:label="Layer 1" inkscape:groupmode="layer"> + inkscape:label="Layer 1" + id="layer1-1" + transform="matrix(0.93914302,0,0,0.95061485,-0.82457823,-2.3789835)"> + + + + + + + transform="matrix(0.88523756,0,0,0.88523756,-890.27694,-275.44544)" + id="g8099"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + d="m 1057.0679,360.95209 a 5.9014757,2.946741 0 1 1 -11.8029,0 5.9014757,2.946741 0 1 1 11.8029,0 z" + id="path2773-8" + style="fill:#ddca10;fill-opacity:1;fill-rule:evenodd;stroke:#7d6c0f;stroke-width:1.17950475;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + + + + + + + + + + + transform="matrix(0.88523756,0,0,0.88523756,-879.08976,-274.46989)" + id="g8085"> + d="m 1011.048,356.79288 28.3084,0 0,7.10679 -28.3084,0 0,-7.10679 z" + id="path2321-7" + style="fill:#4f7f21;fill-opacity:1;fill-rule:evenodd;stroke:#2e5c02;stroke-width:1.17950368;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1" /> + d="m 1014.3813,349.72956 21.4339,0 3.5411,7.08818 -28.3081,0 3.3331,-7.08818 z" + id="path2323-9" + style="fill:#59af05;fill-opacity:1;fill-rule:evenodd;stroke:#2e5c02;stroke-width:1.17950404;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1" /> + + + d="m 1015.1028,350.96975 -2.3221,4.6443 24.5855,0 -2.4698,-4.6443 -19.7936,0 z" + id="path2329-4" + style="opacity:0.25;fill:none;stroke:#ffffff;stroke-width:1.17950404;stroke-miterlimit:4;stroke-opacity:1" /> + d="m 1031.6895,353.26726 a 5.89755,1.7692638 0 0 1 -11.7951,0 5.89755,1.7692638 0 1 1 11.7951,0 z" + id="path2746-1" + style="fill:#2e5c02;fill-opacity:1;fill-rule:evenodd;stroke:none" /> + + + + + +