Compare commits

..

6 Commits

Author SHA1 Message Date
Christian Merten aae75ce291
perf(finance/admin): remove `is_valid` from list view (#9)
The new unified statement view is very slow in production. This is
caused by the computation of `is_valid` for every entry of the list,
which entails at least one database query per row. Instead of removing
the `is_valid` field from the list view, we could prefetch the related
fields, but I don't think the `is_valid` provides any noticeable benefit
anyway.

We also add pagination to the statement view.
3 months ago
Christian Merten efe20bc721
chore(finance/models): add documentation and test for `validity` (#11)
Currently, the `INVALID_TOTAL` case of the `Statement.validity` method
is tested indirectly via the admin tests and the behavior is hard to
trace back. We therefore add documentation and a targeted test.
3 months ago
Christian Merten 5d4f29be89
feat(finance/admin): unified statement view (#6)
This PR replaces the separate admin views for unsubmitted, submitted and
confirmed statements by one common view. To distinguish the state, we
now display a colored badge in the changelist.

The default permissions for the `Standard` group are changed so that
normal users can continue to view statements they are related to when
these are submitted or confirmed.
3 months ago
Christian Merten 3b8964fbb0
chore(deploy): update urls to github repository (#8) 3 months ago
Christian Merten 3e5f6f0d27
chore: update favicon 3 months ago
Christian Merten 461cb098f0
chore(ci): build and release docker image, build documentation (#7)
We expand the tests CI run to first build the main image, build and
deploy the documentation pages, run the test suite and publish the
docker image in the github docker registry.
4 months ago

@ -0,0 +1,147 @@
name: Build and test
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
APP_IMAGE_NAME: ${{ github.repository }}
NGINX_IMAGE_NAME: ${{ github.repository }}-nginx
jobs:
build-test-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for application image
id: meta-app
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.APP_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Extract metadata for nginx image
id: meta-nginx
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.NGINX_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build application image
uses: docker/build-push-action@v5
with:
context: .
file: docker/production/Dockerfile
load: true
tags: kompass:test
cache-from: |
type=gha,scope=app-${{ github.ref_name }}
type=gha,scope=app-master
type=gha,scope=app-main
type=registry,ref=ghcr.io/${{ github.repository }}:latest
cache-to: type=gha,mode=max,scope=app-${{ github.ref_name }}
build-args: |
BUILDKIT_INLINE_CACHE=1
- name: Build documentation
run: |
# Create output directory with proper permissions
mkdir -p docs-output
chmod 777 docs-output
# Run sphinx-build inside the container
docker run --rm \
-v ${{ github.workspace }}/docs:/app/docs:ro \
-v ${{ github.workspace }}/docs-output:/app/docs-output \
kompass:test \
bash -c "cd /app/docs && sphinx-build -b html source /app/docs-output"
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs-output
destination_dir: ${{ github.ref == 'refs/heads/main' && '.' || github.ref_name }}
keep_files: true
- name: Run tests
run: make test-only
- name: Check coverage
run: |
COVERAGE=$(python3 -c "import json; data=json.load(open('docker/test/htmlcov/coverage.json')); print(data['totals']['percent_covered'])")
echo "Coverage: ${COVERAGE}%"
if (( $(echo "$COVERAGE < 100" | bc -l) )); then
echo "Error: Coverage is ${COVERAGE}%, must be 100%"
exit 1
fi
- name: Tag and push application image
if: github.event_name != 'pull_request'
run: |
# Tag the built image with all required tags
echo "${{ steps.meta-app.outputs.tags }}" | while read -r tag; do
docker tag kompass:test "$tag"
docker push "$tag"
done
- name: Build and push nginx image
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v5
with:
context: docker/production/nginx
file: docker/production/nginx/Dockerfile
push: true
tags: ${{ steps.meta-nginx.outputs.tags }}
labels: ${{ steps.meta-nginx.outputs.labels }}
cache-from: |
type=gha,scope=nginx-${{ github.ref_name }}
type=gha,scope=nginx-master
type=gha,scope=nginx-main
type=registry,ref=ghcr.io/${{ github.repository }}-nginx:latest
cache-to: type=gha,mode=max,scope=nginx-${{ github.ref_name }}
build-args: |
BUILDKIT_INLINE_CACHE=1
- name: Output image tags
if: github.event_name != 'pull_request'
run: |
echo "Application image tags:"
echo "${{ steps.meta-app.outputs.tags }}"
echo ""
echo "Nginx image tags:"
echo "${{ steps.meta-nginx.outputs.tags }}"

@ -1,60 +0,0 @@
name: Tests
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ hashFiles('requirements.txt', 'docker/test/Dockerfile') }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Build Docker image with cache
run: |
cd docker/test
docker buildx build \
--cache-from type=local,src=/tmp/.buildx-cache \
--cache-to type=local,dest=/tmp/.buildx-cache-new,mode=max \
--load \
-t kompass:test \
-f Dockerfile \
../../
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
- name: Run tests
run: make test-only
- name: Check coverage
run: |
COVERAGE=$(python3 -c "import json; data=json.load(open('docker/test/htmlcov/coverage.json')); print(data['totals']['percent_covered'])")
echo "Coverage: ${COVERAGE}%"
if (( $(echo "$COVERAGE < 100" | bc -l) )); then
echo "Error: Coverage is ${COVERAGE}%, must be 100%"
exit 1
fi

@ -1,6 +1,6 @@
# jdav Kompass # jdav Kompass
![Build Status](https://github.com/chrisflav/kompass/actions/workflows/test.yml/badge.svg?branch=main) ![Build Status](https://github.com/chrisflav/kompass/actions/workflows/build-docker.yml/badge.svg?branch=main)
Kompass is an administration platform designed for local sections of the Young German Alpine Club. It provides Kompass is an administration platform designed for local sections of the Young German Alpine Club. It provides
tools to contact and (automatically) manage members, groups, material, excursions and statements. tools to contact and (automatically) manage members, groups, material, excursions and statements.

@ -13,7 +13,7 @@ services:
master: master:
<<: *kompass <<: *kompass
build: build:
context: git@git.jdav-hd.merten.dev:digitales/kompass#main context: https://github.com/chrisflav/kompass.git#main
dockerfile: docker/production/Dockerfile dockerfile: docker/production/Dockerfile
entrypoint: /app/docker/production/entrypoint-master.sh entrypoint: /app/docker/production/entrypoint-master.sh
volumes: volumes:
@ -28,7 +28,7 @@ services:
- "host:10.26.42.1" - "host:10.26.42.1"
nginx: nginx:
build: git@git.jdav-hd.merten.dev:digitales/kompass#main:docker/production/nginx build: https://github.com/chrisflav/kompass.git#main:docker/production/nginx
restart: always restart: always
volumes: volumes:
- uwsgi_data:/tmp/uwsgi/ - uwsgi_data:/tmp/uwsgi/

@ -22,8 +22,6 @@ ENV PATH="/app/.local/bin:$PATH"
# install requirements # install requirements
COPY --chown=app:app ./requirements.txt /app/requirements.txt COPY --chown=app:app ./requirements.txt /app/requirements.txt
# we install uwsgi here to check if packages dependencies are resolved, but we don't actually RUN pip install uwsgi -r requirements.txt
# need uwsgi in test
RUN pip install coverage uwsgi -r requirements.txt
COPY --chown=app:app . /app COPY --chown=app:app . /app

@ -1 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.c{fill:#9cc;}.d{fill:#2d5955;}.e{stroke:#2d5955;stroke-width:3.2px;}.e,.f{fill:none;stroke-miterlimit:10;}.g{fill:#666;mix-blend-mode:multiply;}.f{stroke:#666;stroke-width:3.3px;}.h{opacity:.36;}.i{isolation:isolate;}.j{fill:#fff;opacity:.24;}</style></defs><g class="i"><g id="a"><circle class="f" cx="24.31" cy="24.31" r="21.69"/></g><g id="b"><circle class="e" cx="23.69" cy="23.69" r="21.69"/><polygon class="g" points="21.2 22.96 15.21 43.89 27.68 25.98 33.66 5.05 21.2 22.96"/><polygon class="c" points="14.34 43.15 26.8 25.24 20.32 22.23 14.34 43.15"/><polygon class="d" points="32.79 4.32 20.32 22.23 26.8 25.24 32.79 4.32"/><polyline class="h" points="14.34 43.15 26.8 25.24 32.79 4.32"/><circle class="j" cx="23.61" cy="23.64" r="1.55"/></g></g></svg> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{display:none;}
.st1{display:inline;fill:#254D49;}
.st2{display:inline;opacity:0.27;fill:url(#SVGID_1_);}
.st3{fill:none;stroke:#1B2E2C;stroke-width:3;stroke-miterlimit:10;}
.st4{fill:#1B2E2C;}
.st5{fill:none;stroke:#508480;stroke-width:3.2;stroke-miterlimit:10;}
.st6{fill:#BADDD9;}
.st7{fill:#508480;}
.st8{opacity:0.36;}
.st9{opacity:0.24;fill:#FFFFFF;}
</style>
<g id="Hintergrund_x5F_Uni" class="st0">
<rect class="st1" width="48" height="48"/>
</g>
<g id="Verlauf" class="st0">
<radialGradient id="SVGID_1_" cx="23.348" cy="21.0566" r="25.4002" fx="3.9002" fy="4.7179" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#000000;stop-opacity:0"/>
<stop offset="1" style="stop-color:#000000"/>
</radialGradient>
<circle class="st2" cx="23.6" cy="23.6" r="22.9"/>
</g>
<g id="Logo_x5F_Schatten">
<circle class="st3" cx="24.3" cy="24.3" r="21.7"/>
<polygon class="st4" points="21.4,22.9 15.8,42.4 27.5,25.7 33.2,6.2 "/>
</g>
<g id="LogooVordergrund">
<circle class="st5" cx="23.7" cy="23.7" r="21.7"/>
<g>
<polygon class="st6" points="14.9,41.8 26.6,25.1 20.5,22.2 "/>
<polygon class="st7" points="32.3,5.5 20.5,22.2 26.6,25.1 "/>
<polyline class="st8" points="14.9,41.8 26.6,25.1 32.3,5.5 "/>
<circle class="st9" cx="23.6" cy="23.6" r="1.6"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 873 B

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -65,11 +65,12 @@ def decorate_statement_view(model, perm=None):
@admin.register(Statement) @admin.register(Statement)
class StatementAdmin(CommonAdminMixin, admin.ModelAdmin): class StatementAdmin(CommonAdminMixin, admin.ModelAdmin):
fields = ['short_description', 'explanation', 'excursion', 'status'] fields = ['short_description', 'explanation', 'excursion', 'status']
list_display = ['__str__', 'total_pretty', 'created_by', 'submitted_date', 'is_valid', 'status_badge'] list_display = ['__str__', 'total_pretty', 'created_by', 'submitted_date', 'status_badge']
list_filter = ['status'] list_filter = ['status']
search_fields = ('excursion__name', 'short_description') search_fields = ('excursion__name', 'short_description')
ordering = ['-submitted_date'] ordering = ['-submitted_date']
inlines = [BillOnStatementInline] inlines = [BillOnStatementInline]
list_per_page = 25
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
if obj is None: if obj is None:

@ -206,7 +206,7 @@ msgid "Submitted"
msgstr "Eingereicht" msgstr "Eingereicht"
#: finance/models.py #: finance/models.py
msgid "Confirmed" msgid "Completed"
msgstr "Abgewickelt" msgstr "Abgewickelt"
#: finance/models.py #: finance/models.py

@ -55,7 +55,7 @@ class Statement(CommonModel):
UNSUBMITTED, SUBMITTED, CONFIRMED = 0, 1, 2 UNSUBMITTED, SUBMITTED, CONFIRMED = 0, 1, 2
STATUS_CHOICES = [(UNSUBMITTED, _('In preparation')), STATUS_CHOICES = [(UNSUBMITTED, _('In preparation')),
(SUBMITTED, _('Submitted')), (SUBMITTED, _('Submitted')),
(CONFIRMED, _('Confirmed'))] (CONFIRMED, _('Completed'))]
STATUS_CSS_CLASS = { SUBMITTED: 'submitted', STATUS_CSS_CLASS = { SUBMITTED: 'submitted',
CONFIRMED: 'confirmed', CONFIRMED: 'confirmed',
UNSUBMITTED: 'unsubmitted' } UNSUBMITTED: 'unsubmitted' }
@ -162,6 +162,18 @@ class Statement(CommonModel):
@property @property
def transaction_issues(self): def transaction_issues(self):
"""
Returns a list of critical problems with the currently configured transactions. This is done
by calculating a list of required paiments. From this list, we deduce the total amount
every member should receive (this amount can be negative, due to org fees).
Finally, the amounts are compared to the total amounts paid out by currently setup transactions.
The list of required paiments is generated from:
- All covered bills that have a configured payer.
(Note: This means that `transaction_issues` might return an empty list, but the calculated
total still differs from the transaction total.)
- If the statement is associated with an excursion: allowances, subsidies, LJP paiment and org fee.
"""
needed_paiments = [(b.paid_by, b.amount) for b in self.bill_set.all() if b.costs_covered and b.paid_by] needed_paiments = [(b.paid_by, b.amount) for b in self.bill_set.all() if b.costs_covered and b.paid_by]
if self.excursion is not None: if self.excursion is not None:
@ -206,6 +218,7 @@ class Statement(CommonModel):
@property @property
def transactions_match_expenses(self): def transactions_match_expenses(self):
"""Returns true iff there are no transaction issues."""
return len(self.transaction_issues) == 0 return len(self.transaction_issues) == 0
@property @property
@ -223,7 +236,11 @@ class Statement(CommonModel):
@property @property
def total_valid(self): def total_valid(self):
"""Checks if the calculated total agrees with the total amount of all transactions.""" """
Checks if the calculated total agrees with the total amount of all transactions.
Note: This is not the same as `transactions_match_expenses`. For details see the
docstring of `transaction_issues`.
"""
total_transactions = 0 total_transactions = 0
for transaction in self.transaction_set.all(): for transaction in self.transaction_set.all():
total_transactions += transaction.amount total_transactions += transaction.amount
@ -231,6 +248,19 @@ class Statement(CommonModel):
@property @property
def validity(self): def validity(self):
"""
Returns the validity status of the statement. This is one of:
- `Statement.VALID`:
Everything is correct.
- `Statement.NON_MATCHING_TRANSACTIONS`:
There is a transaction issue (in the sense of `transaction_issues`).
- `Statement.MISSING_LEDGER`:
At least one transaction has no ledger configured.
- `Statement.INVALID_ALLOWANCE_TO`:
The members receiving allowance don't match the regulations.
- `Statement.INVALID_TOTAL`:
The total amount of transactions differs from the calculated total payout.
"""
if not self.transactions_match_expenses: if not self.transactions_match_expenses:
return Statement.NON_MATCHING_TRANSACTIONS return Statement.NON_MATCHING_TRANSACTIONS
if not self.ledgers_configured: if not self.ledgers_configured:

@ -147,6 +147,9 @@ class StatementTestCase(TestCase):
refunded=False refunded=False
) )
self.st6 = Statement.objects.create(night_cost=self.night_cost)
Bill.objects.create(statement=self.st6, amount='42', costs_covered=True)
def test_org_fee(self): def test_org_fee(self):
# org fee should be collected if participants are older than 26 # org fee should be collected if participants are older than 26
self.assertEqual(self.st5.excursion.old_participant_count, 3, 'Calculation of number of old people in excursion is incorrect.') self.assertEqual(self.st5.excursion.old_participant_count, 3, 'Calculation of number of old people in excursion is incorrect.')
@ -488,6 +491,11 @@ class StatementTestCase(TestCase):
ljp_contrib = self.st_small.paid_ljp_contributions ljp_contrib = self.st_small.paid_ljp_contributions
self.assertEqual(ljp_contrib, 0) self.assertEqual(ljp_contrib, 0)
def test_validity_paid_by_none(self):
# st6 has one covered bill with no payer, so no transaction issues,
# but total transaction amount (= 0) differs from actual total (> 0).
self.assertEqual(self.st6.validity, Statement.INVALID_TOTAL)
class LedgerTestCase(TestCase): class LedgerTestCase(TestCase):
def setUp(self): def setUp(self):

@ -1 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.c{fill:#9cc;}.d{fill:#2d5955;}.e{stroke:#2d5955;stroke-width:3.2px;}.e,.f{fill:none;stroke-miterlimit:10;}.g{fill:#666;mix-blend-mode:multiply;}.f{stroke:#666;stroke-width:3.3px;}.h{opacity:.36;}.i{isolation:isolate;}.j{fill:#fff;opacity:.24;}</style></defs><g class="i"><g id="a"><circle class="f" cx="24.31" cy="24.31" r="21.69"/></g><g id="b"><circle class="e" cx="23.69" cy="23.69" r="21.69"/><polygon class="g" points="21.2 22.96 15.21 43.89 27.68 25.98 33.66 5.05 21.2 22.96"/><polygon class="c" points="14.34 43.15 26.8 25.24 20.32 22.23 14.34 43.15"/><polygon class="d" points="32.79 4.32 20.32 22.23 26.8 25.24 32.79 4.32"/><polyline class="h" points="14.34 43.15 26.8 25.24 32.79 4.32"/><circle class="j" cx="23.61" cy="23.64" r="1.55"/></g></g></svg> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{display:none;}
.st1{display:inline;fill:#254D49;}
.st2{display:inline;opacity:0.27;fill:url(#SVGID_1_);}
.st3{fill:none;stroke:#1B2E2C;stroke-width:3;stroke-miterlimit:10;}
.st4{fill:#1B2E2C;}
.st5{fill:none;stroke:#508480;stroke-width:3.2;stroke-miterlimit:10;}
.st6{fill:#BADDD9;}
.st7{fill:#508480;}
.st8{opacity:0.36;}
.st9{opacity:0.24;fill:#FFFFFF;}
</style>
<g id="Hintergrund_x5F_Uni" class="st0">
<rect class="st1" width="48" height="48"/>
</g>
<g id="Verlauf" class="st0">
<radialGradient id="SVGID_1_" cx="23.348" cy="21.0566" r="25.4002" fx="3.9002" fy="4.7179" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#000000;stop-opacity:0"/>
<stop offset="1" style="stop-color:#000000"/>
</radialGradient>
<circle class="st2" cx="23.6" cy="23.6" r="22.9"/>
</g>
<g id="Logo_x5F_Schatten">
<circle class="st3" cx="24.3" cy="24.3" r="21.7"/>
<polygon class="st4" points="21.4,22.9 15.8,42.4 27.5,25.7 33.2,6.2 "/>
</g>
<g id="LogooVordergrund">
<circle class="st5" cx="23.7" cy="23.7" r="21.7"/>
<g>
<polygon class="st6" points="14.9,41.8 26.6,25.1 20.5,22.2 "/>
<polygon class="st7" points="32.3,5.5 20.5,22.2 26.6,25.1 "/>
<polyline class="st8" points="14.9,41.8 26.6,25.1 32.3,5.5 "/>
<circle class="st9" cx="23.6" cy="23.6" r="1.6"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 873 B

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -3,37 +3,38 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve"> viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css"> <style type="text/css">
.st0{opacity:0.27;fill:url(#SVGID_1_);} .st0{display:none;}
.st1{fill:none;stroke:#999999;stroke-width:3.3;stroke-miterlimit:10;} .st1{display:inline;fill:#254D49;}
.st2{fill:none;stroke:#508480;stroke-width:3.2;stroke-miterlimit:10;} .st2{opacity:0.27;fill:url(#SVGID_1_);}
.st3{fill:none;stroke:url(#SVGID_00000121983970493329986990000017723393330248815746_);stroke-width:3.2;stroke-miterlimit:10;} .st3{fill:none;stroke:#1B2E2C;stroke-width:3;stroke-miterlimit:10;}
.st4{fill:#999999;} .st4{fill:#1B2E2C;}
.st5{fill:#BADDD9;} .st5{fill:none;stroke:#508480;stroke-width:3.2;stroke-miterlimit:10;}
.st6{fill:#508480;} .st6{fill:#BADDD9;}
.st7{opacity:0.36;} .st7{fill:#508480;}
.st8{opacity:0.24;fill:#FFFFFF;} .st8{opacity:0.36;}
.st9{opacity:0.24;fill:#FFFFFF;}
</style> </style>
<g id="Logo_x5F_Schatten"> <g id="Hintergrund_x5F_Uni" class="st0">
<radialGradient id="SVGID_1_" cx="20.8612" cy="20.647" r="22.9444" gradientUnits="userSpaceOnUse"> <rect class="st1" width="48" height="48"/>
<stop offset="0" style="stop-color:#FFFFFF"/> </g>
<g id="Verlauf">
<radialGradient id="SVGID_1_" cx="23.348" cy="21.0566" r="25.4002" fx="3.9002" fy="4.7179" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#000000;stop-opacity:0"/>
<stop offset="1" style="stop-color:#000000"/> <stop offset="1" style="stop-color:#000000"/>
</radialGradient> </radialGradient>
<circle class="st0" cx="23.6" cy="23.6" r="22.9"/> <circle class="st2" cx="23.6" cy="23.6" r="22.9"/>
<circle class="st1" cx="24.3" cy="24.3" r="21.7"/> </g>
<g id="Logo_x5F_Schatten">
<circle class="st3" cx="24.3" cy="24.3" r="21.7"/>
<polygon class="st4" points="21.4,22.9 15.8,42.4 27.5,25.7 33.2,6.2 "/>
</g> </g>
<g id="LogooVordergrund"> <g id="LogooVordergrund">
<circle class="st2" cx="23.7" cy="23.7" r="21.7"/> <circle class="st5" cx="23.7" cy="23.7" r="21.7"/>
<g>
<linearGradient id="SVGID_00000165954078947660943750000008230134672540192957_" gradientUnits="userSpaceOnUse" x1="3.5175" y1="12.0447" x2="43.8685" y2="35.3413"> <polygon class="st6" points="14.9,41.8 26.6,25.1 20.5,22.2 "/>
<stop offset="0" style="stop-color:#BADDD9;stop-opacity:0.1"/> <polygon class="st7" points="32.3,5.5 20.5,22.2 26.6,25.1 "/>
<stop offset="1" style="stop-color:#508480"/> <polyline class="st8" points="14.9,41.8 26.6,25.1 32.3,5.5 "/>
</linearGradient> <circle class="st9" cx="23.6" cy="23.6" r="1.6"/>
</g>
<circle style="fill:none;stroke:url(#SVGID_00000165954078947660943750000008230134672540192957_);stroke-width:3.2;stroke-miterlimit:10;" cx="23.7" cy="23.7" r="21.7"/>
<polygon class="st4" points="21.2,23 15.2,43.9 27.7,26 33.7,5 "/>
<polygon class="st5" points="14.3,43.2 26.8,25.2 20.3,22.2 "/>
<polygon class="st6" points="32.8,4.3 20.3,22.2 26.8,25.2 "/>
<polyline class="st7" points="14.3,43.2 26.8,25.2 32.8,4.3 "/>
<circle class="st8" cx="23.6" cy="23.6" r="1.5"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Loading…
Cancel
Save