Compare commits
128 Commits
dev/api-do
...
master
Author | SHA1 | Date |
---|---|---|
Santiago L | 5ab4779e1a | |
Santiago L | 5e6cd2f147 | |
Santiago L | 03666d8ed0 | |
Santiago L | e88e27a56e | |
Santiago L | 9a4f4ee17c | |
Santiago L | 008f49100f | |
Santiago L | b0f77ad591 | |
Santiago L | 639ecdde58 | |
Santiago L | 361b4b41a8 | |
Santiago L | 6720df314b | |
Santiago L | 9f80c75da7 | |
Santiago L | 1258a27688 | |
Santiago L | a400c25de9 | |
Santiago L | e3ec82a182 | |
Santiago L | cda47e2fb6 | |
Santiago L | d3e5ea59a9 | |
Santiago L | b37d9cc515 | |
Santiago L | 1faab905d6 | |
Santiago L | de26baf75a | |
Santiago L | 50f916fa4d | |
Santiago L | c21a52a756 | |
Santiago L | a90e500186 | |
Santiago L | 7d6a2474ab | |
Santiago L | b365580165 | |
Santiago L | bcfed9cb79 | |
Santiago L | 867d9afe65 | |
Santiago L | e1d71fa620 | |
Santiago L | 70f7551e7d | |
Santiago L | 81c67778e5 | |
Santiago L | 9a3b6dcbc3 | |
Santiago L | 5e7a823205 | |
Santiago L | e1224ddd57 | |
Santiago L | 7b59931bcf | |
Santiago L | 0e10d2b142 | |
Santiago L | 47eb0f1efe | |
Santiago L | 28c03ac6c8 | |
Santiago L | 9953124a95 | |
Santiago L | 06c226d302 | |
Santiago L | 4f695c2e6e | |
Santiago L | e6495a967b | |
Santiago L | 6d8a2ced53 | |
Santiago L | a2927f7616 | |
Santiago L | f13fea5030 | |
Santiago L | f0683660ae | |
Santiago L | b24ddf7546 | |
Santiago L | 3b4bb51925 | |
Santiago L | a6c5aa32df | |
Santiago L | 13b4ac5eee | |
Santiago L | 8dc792b851 | |
Santiago L | 5a21f766b4 | |
Santiago L | 7183174f4c | |
Santiago L | 48ef1f21e3 | |
Santiago L | aebbd424fc | |
Santiago L | 5389f425ce | |
Santiago L | ed9bfc0eb7 | |
Santiago L | 0095da61ea | |
Santiago L | 58be94bde2 | |
Santiago L | be5e06129a | |
Santiago L | 69df9780bf | |
Santiago L | 18a41d507b | |
Santiago L | f7627926cb | |
Santiago L | ffd08459c4 | |
Santiago L | 085b8f85bd | |
Santiago L | d5fce3b6e2 | |
Santiago L | 777a7f6de5 | |
Santiago L | 422305a636 | |
Santiago L | d6cebf66a2 | |
Santiago L | 0338b927cf | |
Santiago L | 97f1c7ef2b | |
Santiago L | b6cf0c34f5 | |
Santiago L | 7fa7106d72 | |
Santiago L | 6ef7f921e9 | |
Santiago L | a8b17da992 | |
Santiago L | c689a6e44c | |
Santiago L | de979011f9 | |
Santiago L | 7d975637d5 | |
Santiago L | d863598d81 | |
Santiago L | eadc06d4c5 | |
Santiago L | 2b06652a5b | |
Santiago L | dc722ec17a | |
Santiago L | e7aabf4799 | |
Cayo Puigdefabregas | fa8a895299 | |
Cayo Puigdefabregas | 091120d3c2 | |
Cayo Puigdefabregas | c952d782cd | |
Cayo Puigdefabregas | 226327cacf | |
Cayo Puigdefabregas | 6f043cd272 | |
Cayo Puigdefabregas | 0633df114e | |
Cayo Puigdefabregas | a53b71bab1 | |
Cayo Puigdefabregas | c010c10157 | |
Santiago L | acac7727c2 | |
Cayo Puigdefabregas | 48ef6d63bc | |
Santiago L | 45bf31c9da | |
Santiago L | 08a76a8de4 | |
Santiago L | 14fbd98e33 | |
Santiago L | 58395147c9 | |
Santiago L | c505f9a3c6 | |
Santiago L | f4c0a7413c | |
Santiago L | 9d2d0befc4 | |
cayop | 350d93f820 | |
Cayo Puigdefabregas | cedb8d690b | |
Cayo Puigdefabregas | 883cf631e2 | |
Cayo Puigdefabregas | 898c6882c8 | |
Cayo Puigdefabregas | a236bbdf5d | |
Cayo Puigdefabregas | 6ce4d6b877 | |
Cayo Puigdefabregas | 8da89ae22a | |
Cayo Puigdefabregas | 78db4fb8d5 | |
Cayo Puigdefabregas | 7c62092faa | |
Cayo Puigdefabregas | d0050f81b7 | |
Cayo Puigdefabregas | 6450d0d749 | |
Cayo Puigdefabregas | 2619a50410 | |
Cayo Puigdefabregas | 24e75bc07f | |
Cayo Puigdefabregas | 0cde41042f | |
Cayo Puigdefabregas | 5b4b7310e6 | |
Cayo Puigdefabregas | 38275847d9 | |
Cayo Puigdefabregas | a4c3b00205 | |
Cayo Puigdefabregas | c386b10bc8 | |
Cayo Puigdefabregas | 0b937bfb4f | |
Cayo Puigdefabregas | 30bd1ad816 | |
Cayo Puigdefabregas | 44ebd42942 | |
Cayo Puigdefabregas | e2ef8823f8 | |
Cayo Puigdefabregas | f0fadf8bba | |
Santiago L | e6e434f525 | |
Santiago L | 43d8c9471b | |
Marc Aymerich | ea9c398de4 | |
Marc Aymerich | 6fadf0c631 | |
Marc Aymerich | a1f73d883a | |
Marc Aymerich | 0c1b4c7f4a | |
Marc Aymerich | 25fbc6a088 |
|
@ -11,7 +11,7 @@ If you are planing to do some development you may want to consider doing it unde
|
||||||
|
|
||||||
2. Build a new image, create and start a container
|
2. Build a new image, create and start a container
|
||||||
```bash
|
```bash
|
||||||
curl -L http://git.io/orchestra-Dockerfile > /tmp/Dockerfile
|
curl -L https://raw.githubusercontent.com/ribaguifi/django-orchestra/master/scripts/containers/Dockerfile > /tmp/Dockerfile
|
||||||
docker build -t orchestra /tmp/
|
docker build -t orchestra /tmp/
|
||||||
docker create --name orchestra -i -t -u orchestra -w /home/orchestra orchestra bash
|
docker create --name orchestra -i -t -u orchestra -w /home/orchestra orchestra bash
|
||||||
docker start orchestra
|
docker start orchestra
|
||||||
|
@ -21,12 +21,13 @@ If you are planing to do some development you may want to consider doing it unde
|
||||||
|
|
||||||
3. Deploy django-orchestra development environment, inside the container
|
3. Deploy django-orchestra development environment, inside the container
|
||||||
```bash
|
```bash
|
||||||
bash <( curl -L http://git.io/orchestra-deploy ) --dev
|
bash <( curl -L https://raw.githubusercontent.com/ribaguifi/django-orchestra/master/scripts/containers/deploy.sh ) --dev
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Nginx should be serving on port 80, but Django's development server can be used as well:
|
3. Nginx should be serving on port 80, but Django's development server can be used as well:
|
||||||
```bash
|
```bash
|
||||||
cd panel
|
cd panel
|
||||||
|
python3 manage.py migrate
|
||||||
python3 manage.py runserver 0.0.0.0:8888
|
python3 manage.py runserver 0.0.0.0:8888
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -34,5 +35,5 @@ If you are planing to do some development you may want to consider doing it unde
|
||||||
5. To upgrade to current master just re-run the deploy script
|
5. To upgrade to current master just re-run the deploy script
|
||||||
```bash
|
```bash
|
||||||
git pull origin master
|
git pull origin master
|
||||||
bash <( curl -L http://git.io/orchestra-deploy ) --dev
|
bash <( curl -L https://raw.githubusercontent.com/ribaguifi/django-orchestra/master/scripts/containers/deploy.sh ) --dev
|
||||||
```
|
```
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
# System requirements:
|
||||||
|
The most important requirement is use python3.6
|
||||||
|
we need install this packages:
|
||||||
|
```
|
||||||
|
bind9utils
|
||||||
|
ca-certificates
|
||||||
|
gettext
|
||||||
|
libcrack2-dev
|
||||||
|
libxml2-dev
|
||||||
|
libxslt1-dev
|
||||||
|
python3
|
||||||
|
python3-pip
|
||||||
|
python3-dev
|
||||||
|
ssh-client
|
||||||
|
wget
|
||||||
|
xvfb
|
||||||
|
zlib1g-dev
|
||||||
|
git
|
||||||
|
iceweasel
|
||||||
|
dnsutils
|
||||||
|
```
|
||||||
|
We need install too a *wkhtmltopdf* package
|
||||||
|
You can use one of your OS or get it from original.
|
||||||
|
This it is in https://wkhtmltopdf.org/downloads.html
|
||||||
|
|
||||||
|
# pip installations
|
||||||
|
We need install this packages:
|
||||||
|
```
|
||||||
|
Django==1.10.5
|
||||||
|
django-fluent-dashboard==0.6.1
|
||||||
|
django-admin-tools==0.8.0
|
||||||
|
django-extensions==1.7.4
|
||||||
|
django-celery==3.1.17
|
||||||
|
celery==3.1.23
|
||||||
|
kombu==3.0.35
|
||||||
|
billiard==3.3.0.23
|
||||||
|
Markdown==2.4
|
||||||
|
djangorestframework==3.4.7
|
||||||
|
ecdsa==0.11
|
||||||
|
Pygments==1.6
|
||||||
|
django-filter==0.15.2
|
||||||
|
jsonfield==0.9.22
|
||||||
|
python-dateutil==2.2
|
||||||
|
https://github.com/glic3rinu/passlib/archive/master.zip
|
||||||
|
django-iban==0.3.0
|
||||||
|
requests
|
||||||
|
phonenumbers
|
||||||
|
django-countries
|
||||||
|
django-localflavor
|
||||||
|
amqp
|
||||||
|
anyjson
|
||||||
|
pytz
|
||||||
|
cracklib
|
||||||
|
lxml==3.3.5
|
||||||
|
selenium
|
||||||
|
xvfbwrapper
|
||||||
|
freezegun
|
||||||
|
coverage
|
||||||
|
flake8
|
||||||
|
django-debug-toolbar==1.3.0
|
||||||
|
django-nose==1.4.4
|
||||||
|
sqlparse
|
||||||
|
pyinotify
|
||||||
|
PyMySQL
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to use Orchestra you need to install from pip like this:
|
||||||
|
```
|
||||||
|
pip3 install http://git.io/django-orchestra-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
But if you want develop orquestra you need to do this:
|
||||||
|
```
|
||||||
|
git clone https://github.com/ribaguifi/django-orchestra
|
||||||
|
pip install -e django-orchestra
|
||||||
|
```
|
||||||
|
|
||||||
|
# Database
|
||||||
|
For default use sqlite3 if you want to use postgresql you need install this packages:
|
||||||
|
|
||||||
|
```
|
||||||
|
psycopg2 postgresql
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use it for debian or ubuntu:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt-get install python3-psycopg2 postgresql-contrib
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember create a database for your project and give permitions for the correct user like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
psql -U postgres
|
||||||
|
psql (12.4)
|
||||||
|
Digite «help» para obtener ayuda.
|
||||||
|
|
||||||
|
postgres=# CREATE database orchesta;
|
||||||
|
postgres=# CREATE USER orchesta WITH PASSWORD 'orquesta';
|
||||||
|
postgres=# GRANT ALL PRIVILEGES ON DATABASE orchesta TO orchesta;
|
||||||
|
```
|
||||||
|
|
||||||
|
# Create new project
|
||||||
|
You can use orchestra-admin for create your new project
|
||||||
|
```
|
||||||
|
orchestra-admin startproject <project_name> # e.g. panel
|
||||||
|
cd <project_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Next we need change the settings.py for configure the correct database
|
||||||
|
|
||||||
|
In settings.py we need change the DATABASE section like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
|
'NAME': 'orchestra'
|
||||||
|
'USER': 'orchestra',
|
||||||
|
'PASSWORD': 'orchestra',
|
||||||
|
'HOST': 'localhost',
|
||||||
|
'PORT': '5432',
|
||||||
|
'CONN_MAX_AGE': 60*10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For end you need to do the migrations:
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 manage.py migrate
|
||||||
|
```
|
|
@ -3,7 +3,7 @@ from collections import OrderedDict
|
||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from fluent_dashboard import dashboard, appsettings
|
from fluent_dashboard import dashboard, appsettings
|
||||||
from fluent_dashboard.modules import CmsAppIconList
|
from fluent_dashboard.modules import CmsAppIconList
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django import forms
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.forms.models import modelformset_factory, BaseModelFormSet
|
from django.forms.models import modelformset_factory, BaseModelFormSet
|
||||||
from django.template import Template, Context
|
from django.template import Template
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.forms.widgets import SpanWidget
|
from orchestra.forms.widgets import SpanWidget
|
||||||
|
@ -28,9 +28,9 @@ class AdminFormMixin(object):
|
||||||
' {% include "admin/includes/fieldset.html" %}'
|
' {% include "admin/includes/fieldset.html" %}'
|
||||||
'{% endfor %}'
|
'{% endfor %}'
|
||||||
)
|
)
|
||||||
context = Context({
|
context = {
|
||||||
'adminform': adminform
|
'adminform': adminform
|
||||||
})
|
}
|
||||||
return template.render(context)
|
return template.render(context)
|
||||||
|
|
||||||
|
|
||||||
|
@ -71,9 +71,9 @@ class AdminFormSet(BaseModelFormSet):
|
||||||
</div>
|
</div>
|
||||||
</div>""")
|
</div>""")
|
||||||
)
|
)
|
||||||
context = Context({
|
context = {
|
||||||
'formset': self
|
'formset': self
|
||||||
})
|
}
|
||||||
return template.render(context)
|
return template.render(context)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from admin_tools.menu import items, Menu
|
from admin_tools.menu import items, Menu
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,11 @@ from functools import wraps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
from django.urls import reverse, NoReverseMatch
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape, format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from orchestra.models.utils import get_field_value
|
from orchestra.models.utils import get_field_value
|
||||||
|
@ -113,21 +113,21 @@ def admin_link(*args, **kwargs):
|
||||||
return '---'
|
return '---'
|
||||||
if not getattr(obj, 'pk', None):
|
if not getattr(obj, 'pk', None):
|
||||||
return '---'
|
return '---'
|
||||||
display = kwargs.get('display')
|
display_ = kwargs.get('display')
|
||||||
if display:
|
if display_:
|
||||||
display = getattr(obj, display, display)
|
display_ = getattr(obj, display_, display_)
|
||||||
else:
|
else:
|
||||||
display = obj
|
display_ = obj
|
||||||
try:
|
try:
|
||||||
url = change_url(obj)
|
url = change_url(obj)
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
# Does not has admin
|
# Does not has admin
|
||||||
return str(display)
|
return str(display_)
|
||||||
extra = ''
|
extra = ''
|
||||||
if kwargs['popup']:
|
if kwargs['popup']:
|
||||||
extra = 'onclick="return showAddAnotherPopup(this);"'
|
extra = mark_safe('onclick="return showAddAnotherPopup(this);"')
|
||||||
title = "Change %s" % obj._meta.verbose_name
|
title = "Change %s" % obj._meta.verbose_name
|
||||||
return mark_safe('<a href="%s" title="%s" %s>%s</a>' % (url, title, extra, display))
|
return format_html('<a href="{}" title="{}" {}>{}</a>', url, title, extra, display_)
|
||||||
|
|
||||||
|
|
||||||
@admin_field
|
@admin_field
|
||||||
|
@ -158,7 +158,7 @@ def admin_date(*args, **kwargs):
|
||||||
date = date.strftime("%Y-%m-%d %H:%M:%S %Z")
|
date = date.strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||||
else:
|
else:
|
||||||
date = date.strftime("%Y-%m-%d")
|
date = date.strftime("%Y-%m-%d")
|
||||||
return '<span title="{0}">{1}</span>'.format(date, escape(natural))
|
return format_html('<span title="{0}">{1}</span>', date, natural)
|
||||||
|
|
||||||
|
|
||||||
def get_object_from_url(modeladmin, request):
|
def get_object_from_url(modeladmin, request):
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from .serializers import SetPasswordSerializer
|
from .serializers import SetPasswordSerializer
|
||||||
|
|
||||||
|
|
||||||
class SetPasswordApiMixin(object):
|
class SetPasswordApiMixin(object):
|
||||||
@detail_route(methods=['post'], serializer_class=SetPasswordSerializer)
|
@action(detail=True, methods=['post'], serializer_class=SetPasswordSerializer)
|
||||||
def set_password(self, request, pk):
|
def set_password(self, request, pk):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
data = request.DATA
|
data = request.data
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
data = {
|
data = {
|
||||||
'password': data
|
'password': data
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from django.core.urlresolvers import NoReverseMatch
|
from django.urls import NoReverseMatch
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,16 +23,16 @@ def link_wrap(view, view_names):
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def insert_links(viewset, base_name):
|
def insert_links(viewset, basename):
|
||||||
collection_links = ['api-root', '%s-list' % base_name]
|
collection_links = ['api-root', '%s-list' % basename]
|
||||||
object_links = ['api-root', '%s-list' % base_name, '%s-detail' % base_name]
|
object_links = ['api-root', '%s-list' % basename, '%s-detail' % basename]
|
||||||
exception_links = ['api-root']
|
exception_links = ['api-root']
|
||||||
list_links = ['api-root']
|
list_links = ['api-root']
|
||||||
retrieve_links = ['api-root', '%s-list' % base_name]
|
retrieve_links = ['api-root', '%s-list' % basename]
|
||||||
# Determine any `@action` or `@link` decorated methods on the viewset
|
# Determine any `@action` or `@link` decorated methods on the viewset
|
||||||
for methodname in dir(viewset):
|
for methodname in dir(viewset):
|
||||||
method = getattr(viewset, methodname)
|
method = getattr(viewset, methodname)
|
||||||
view_name = '%s-%s' % (base_name, methodname.replace('_', '-'))
|
view_name = '%s-%s' % (basename, methodname.replace('_', '-'))
|
||||||
if hasattr(method, 'collection_bind_to_methods'):
|
if hasattr(method, 'collection_bind_to_methods'):
|
||||||
list_links.append(view_name)
|
list_links.append(view_name)
|
||||||
retrieve_links.append(view_name)
|
retrieve_links.append(view_name)
|
||||||
|
|
|
@ -65,12 +65,12 @@ class LinkHeaderRouter(DefaultRouter):
|
||||||
APIRoot.router = self
|
APIRoot.router = self
|
||||||
return APIRoot.as_view()
|
return APIRoot.as_view()
|
||||||
|
|
||||||
def register(self, prefix, viewset, base_name=None):
|
def register(self, prefix, viewset, basename=None):
|
||||||
""" inserts link headers on every viewset """
|
""" inserts link headers on every viewset """
|
||||||
if base_name is None:
|
if basename is None:
|
||||||
base_name = self.get_default_base_name(viewset)
|
basename = self.get_default_basename(viewset)
|
||||||
insert_links(viewset, base_name)
|
insert_links(viewset, basename)
|
||||||
self.registry.append((prefix, viewset, base_name))
|
self.registry.append((prefix, viewset, basename))
|
||||||
|
|
||||||
def get_viewset(self, prefix_or_model):
|
def get_viewset(self, prefix_or_model):
|
||||||
for _prefix, viewset, __ in self.registry:
|
for _prefix, viewset, __ in self.registry:
|
||||||
|
|
|
@ -23,7 +23,7 @@ class APIRoot(views.APIView):
|
||||||
'accountancy': {},
|
'accountancy': {},
|
||||||
'services': {},
|
'services': {},
|
||||||
}
|
}
|
||||||
if not request.user.is_anonymous():
|
if not request.user.is_anonymous:
|
||||||
list_name = '{basename}-list'
|
list_name = '{basename}-list'
|
||||||
detail_name = '{basename}-detail'
|
detail_name = '{basename}-detail'
|
||||||
for prefix, viewset, basename in self.router.registry:
|
for prefix, viewset, basename in self.router.registry:
|
||||||
|
|
|
@ -64,7 +64,10 @@ class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class RelatedHyperlinkedModelSerializer(HyperlinkedModelSerializer):
|
class RelatedHyperlinkedModelSerializer(HyperlinkedModelSerializer):
|
||||||
""" returns object on to_internal_value based on URL """
|
""" returns object on to_internal_value based on URL """
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
|
try:
|
||||||
url = data.get('url')
|
url = data.get('url')
|
||||||
|
except AttributeError:
|
||||||
|
url = None
|
||||||
if not url:
|
if not url:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'url': "URL is required."
|
'url': "URL is required."
|
||||||
|
@ -81,14 +84,14 @@ class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer):
|
||||||
validators=[validate_password], write_only=True, required=False,
|
validators=[validate_password], write_only=True, required=False,
|
||||||
style={'widget': widgets.PasswordInput})
|
style={'widget': widgets.PasswordInput})
|
||||||
|
|
||||||
def validate_password(self, attrs, source):
|
def validate_password(self, value):
|
||||||
""" POST only password """
|
""" POST only password """
|
||||||
if self.instance:
|
if self.instance:
|
||||||
if 'password' in attrs:
|
if value:
|
||||||
raise serializers.ValidationError(_("Can not set password"))
|
raise serializers.ValidationError(_("Can not set password"))
|
||||||
elif 'password' not in attrs:
|
elif not value:
|
||||||
raise serializers.ValidationError(_("Password required"))
|
raise serializers.ValidationError(_("Password required"))
|
||||||
return attrs
|
return value
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
""" remove password in case is not a real model field """
|
""" remove password in case is not a real model field """
|
||||||
|
@ -98,7 +101,7 @@ class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
password = attrs.pop('password', None)
|
password = attrs.pop('password', None)
|
||||||
attrs = super(SetPasswordSerializer, self).validate()
|
attrs = super().validate(attrs)
|
||||||
if password is not None:
|
if password is not None:
|
||||||
attrs['password'] = password
|
attrs['password'] = password
|
||||||
return attrs
|
return attrs
|
||||||
|
|
|
@ -157,7 +157,7 @@ function install_requirements () {
|
||||||
PIP="${PIP} \
|
PIP="${PIP} \
|
||||||
selenium \
|
selenium \
|
||||||
xvfbwrapper \
|
xvfbwrapper \
|
||||||
freezegun \
|
freezegun==0.3.14 \
|
||||||
coverage \
|
coverage \
|
||||||
flake8 \
|
flake8 \
|
||||||
django-debug-toolbar==1.3.0 \
|
django-debug-toolbar==1.3.0 \
|
||||||
|
@ -174,7 +174,7 @@ function install_requirements () {
|
||||||
minor=$(echo -e "$wkhtmltox_version\n0.12.2.1" | sort -V | head -n 1)
|
minor=$(echo -e "$wkhtmltox_version\n0.12.2.1" | sort -V | head -n 1)
|
||||||
if [[ ! $wkhtmltox_version ]] || [[ $wkhtmltox_version != 0.12.2.1 && $minor == ${wkhtmltox_version} ]]; then
|
if [[ ! $wkhtmltox_version ]] || [[ $wkhtmltox_version != 0.12.2.1 && $minor == ${wkhtmltox_version} ]]; then
|
||||||
wkhtmltox=$(mktemp)
|
wkhtmltox=$(mktemp)
|
||||||
wget http://download.gna.org/wkhtmltopdf/0.12/0.12.2.1/wkhtmltox-0.12.2.1_linux-jessie-amd64.deb -O ${wkhtmltox}
|
wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.buster_amd64.deb -O ${wkhtmltox}
|
||||||
dpkg -i ${wkhtmltox} || { echo "Installing missing dependencies for wkhtmltox..." && apt-get -f -y install; }
|
dpkg -i ${wkhtmltox} || { echo "Installing missing dependencies for wkhtmltox..." && apt-get -f -y install; }
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
|
@ -178,7 +178,7 @@ def fire_pending_tasks(manage, db):
|
||||||
if is_due(now, minute, hour, day_of_week, day_of_month, month_of_year):
|
if is_due(now, minute, hour, day_of_week, day_of_month, month_of_year):
|
||||||
command = 'python3 -W ignore::DeprecationWarning {manage} runtask {task_id}'.format(
|
command = 'python3 -W ignore::DeprecationWarning {manage} runtask {task_id}'.format(
|
||||||
manage=manage, task_id=task_id)
|
manage=manage, task_id=task_id)
|
||||||
proc = run(command, async=True)
|
proc = run(command, run_async=True)
|
||||||
yield proc
|
yield proc
|
||||||
|
|
||||||
|
|
||||||
|
@ -201,7 +201,7 @@ def fire_pending_messages(settings, db):
|
||||||
|
|
||||||
if has_pending_messages(settings, db):
|
if has_pending_messages(settings, db):
|
||||||
command = 'python3 -W ignore::DeprecationWarning {manage} sendpendingmessages'.format(manage=manage)
|
command = 'python3 -W ignore::DeprecationWarning {manage} sendpendingmessages'.format(manage=manage)
|
||||||
proc = run(command, async=True)
|
proc = run(command, run_async=True)
|
||||||
yield proc
|
yield proc
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ SECRET_KEY = '{{ secret_key }}'
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
|
@ -65,6 +66,7 @@ INSTALLED_APPS = [
|
||||||
'admin_tools.dashboard',
|
'admin_tools.dashboard',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework.authtoken',
|
'rest_framework.authtoken',
|
||||||
|
'django_filters',
|
||||||
'passlib.ext.django',
|
'passlib.ext.django',
|
||||||
'django_countries',
|
'django_countries',
|
||||||
# 'debug_toolbar',
|
# 'debug_toolbar',
|
||||||
|
@ -84,6 +86,21 @@ INSTALLED_APPS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
|
||||||
|
'orchestra.core.caches.RequestCacheMiddleware',
|
||||||
|
# also handles transations, ATOMIC_REQUESTS does not wrap middlewares
|
||||||
|
'orchestra.contrib.orchestration.middlewares.OperationsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
ROOT_URLCONF = '{{ project_name }}.urls'
|
ROOT_URLCONF = '{{ project_name }}.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
|
@ -127,6 +144,24 @@ DATABASES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
|
# https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
|
||||||
|
|
||||||
|
@ -168,22 +203,6 @@ LOCALE_PATHS = (
|
||||||
ORCHESTRA_SITE_NAME = '{{ project_name }}'
|
ORCHESTRA_SITE_NAME = '{{ project_name }}'
|
||||||
|
|
||||||
|
|
||||||
MIDDLEWARE_CLASSES = (
|
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
||||||
'django.middleware.common.CommonMiddleware',
|
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
||||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
|
||||||
# 'django.middleware.locale.LocaleMiddleware'
|
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
||||||
'django.middleware.security.SecurityMiddleware',
|
|
||||||
'orchestra.core.caches.RequestCacheMiddleware',
|
|
||||||
# also handles transations, ATOMIC_REQUESTS does not wrap middlewares
|
|
||||||
'orchestra.contrib.orchestration.middlewares.OperationsMiddleware',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
AUTH_USER_MODEL = 'accounts.Account'
|
AUTH_USER_MODEL = 'accounts.Account'
|
||||||
|
|
||||||
|
|
||||||
|
@ -228,7 +247,7 @@ REST_FRAMEWORK = {
|
||||||
'rest_framework.authentication.TokenAuthentication',
|
'rest_framework.authentication.TokenAuthentication',
|
||||||
),
|
),
|
||||||
'DEFAULT_FILTER_BACKENDS': (
|
'DEFAULT_FILTER_BACKENDS': (
|
||||||
('rest_framework.filters.DjangoFilterBackend',)
|
('django_filters.rest_framework.DjangoFilterBackend',)
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,7 +261,6 @@ PASSLIB_CONFIG = (
|
||||||
"default = sha512_crypt\n"
|
"default = sha512_crypt\n"
|
||||||
"deprecated = django_pbkdf2_sha1, django_salted_sha1, django_salted_md5, "
|
"deprecated = django_pbkdf2_sha1, django_salted_sha1, django_salted_md5, "
|
||||||
" django_des_crypt, des_crypt, hex_md5\n"
|
" django_des_crypt, des_crypt, hex_md5\n"
|
||||||
"all__vary_rounds = 0.05\n"
|
|
||||||
"django_pbkdf2_sha256__min_rounds = 10000\n"
|
"django_pbkdf2_sha256__min_rounds = 10000\n"
|
||||||
"sha512_crypt__min_rounds = 80000\n"
|
"sha512_crypt__min_rounds = 80000\n"
|
||||||
"staff__django_pbkdf2_sha256__default_rounds = 12500\n"
|
"staff__django_pbkdf2_sha256__default_rounds = 12500\n"
|
||||||
|
|
|
@ -4,7 +4,7 @@ from django.contrib import messages
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
from django.contrib.admin.utils import NestedObjects, quote
|
from django.contrib.admin.utils import NestedObjects, quote
|
||||||
from django.contrib.auth import get_permission_codename
|
from django.contrib.auth import get_permission_codename
|
||||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
from django.urls import reverse, NoReverseMatch
|
||||||
from django.db import router
|
from django.db import router
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
|
@ -175,7 +175,7 @@ def delete_related_services(modeladmin, request, queryset):
|
||||||
for model, objs in collector.model_objs.items():
|
for model, objs in collector.model_objs.items():
|
||||||
count = 0
|
count = 0
|
||||||
# discount main systemuser
|
# discount main systemuser
|
||||||
if model is modeladmin.model.main_systemuser.field.rel.to:
|
if model is modeladmin.model.main_systemuser.field.related_model:
|
||||||
count = len(objs) - 1
|
count = len(objs) - 1
|
||||||
# Discount account
|
# Discount account
|
||||||
elif model is not modeladmin.model and model in registered_services:
|
elif model is not modeladmin.model and model in registered_services:
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.conf.urls import url
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.contrib.admin.utils import unquote
|
from django.contrib.admin.utils import unquote
|
||||||
from django.contrib.auth import admin as auth
|
from django.contrib.auth import admin as auth
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
@ -158,6 +158,7 @@ class AccountListAdmin(AccountAdmin):
|
||||||
actions = None
|
actions = None
|
||||||
change_list_template = 'admin/accounts/account/select_account_list.html'
|
change_list_template = 'admin/accounts/account/select_account_list.html'
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def select_account(self, instance):
|
def select_account(self, instance):
|
||||||
# TODO get query string from request.META['QUERY_STRING'] to preserve filters
|
# TODO get query string from request.META['QUERY_STRING'] to preserve filters
|
||||||
context = {
|
context = {
|
||||||
|
@ -167,7 +168,6 @@ class AccountListAdmin(AccountAdmin):
|
||||||
}
|
}
|
||||||
return _('<a href="%(url)s">%(plus)s Add to %(name)s</a>') % context
|
return _('<a href="%(url)s">%(plus)s Add to %(name)s</a>') % context
|
||||||
select_account.short_description = _("account")
|
select_account.short_description = _("account")
|
||||||
select_account.allow_tags = True
|
|
||||||
select_account.admin_order_field = 'username'
|
select_account.admin_order_field = 'username'
|
||||||
|
|
||||||
def changelist_view(self, request, extra_context=None):
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
@ -207,6 +207,7 @@ class AccountAdminMixin(object):
|
||||||
account = None
|
account = None
|
||||||
list_select_related = ('account',)
|
list_select_related = ('account',)
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_active(self, instance):
|
def display_active(self, instance):
|
||||||
if not instance.is_active:
|
if not instance.is_active:
|
||||||
return '<img src="%s" alt="False">' % static('admin/img/icon-no.svg')
|
return '<img src="%s" alt="False">' % static('admin/img/icon-no.svg')
|
||||||
|
@ -215,14 +216,12 @@ class AccountAdminMixin(object):
|
||||||
return '<img style="width:13px" src="%s" alt="False" title="%s">' % (static('admin/img/inline-delete.svg'), msg)
|
return '<img style="width:13px" src="%s" alt="False" title="%s">' % (static('admin/img/inline-delete.svg'), msg)
|
||||||
return '<img src="%s" alt="False">' % static('admin/img/icon-yes.svg')
|
return '<img src="%s" alt="False">' % static('admin/img/icon-yes.svg')
|
||||||
display_active.short_description = _("active")
|
display_active.short_description = _("active")
|
||||||
display_active.allow_tags = True
|
|
||||||
display_active.admin_order_field = 'is_active'
|
display_active.admin_order_field = 'is_active'
|
||||||
|
|
||||||
def account_link(self, instance):
|
def account_link(self, instance):
|
||||||
account = instance.account if instance.pk else self.account
|
account = instance.account if instance.pk else self.account
|
||||||
return admin_link()(account)
|
return admin_link()(account)
|
||||||
account_link.short_description = _("account")
|
account_link.short_description = _("account")
|
||||||
account_link.allow_tags = True
|
|
||||||
account_link.admin_order_field = 'account__username'
|
account_link.admin_order_field = 'account__username'
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
|
|
@ -47,7 +47,7 @@ def create_account_creation_form():
|
||||||
# Previous validation error
|
# Previous validation error
|
||||||
return
|
return
|
||||||
errors = {}
|
errors = {}
|
||||||
systemuser_model = Account.main_systemuser.field.rel.to
|
systemuser_model = Account.main_systemuser.field.related_model
|
||||||
if systemuser_model.objects.filter(username=account.username).exists():
|
if systemuser_model.objects.filter(username=account.username).exists():
|
||||||
errors['username'] = _("A system user with this name already exists.")
|
errors['username'] = _("A system user with this name already exists.")
|
||||||
for model, key, related_kwargs, __ in create_related:
|
for model, key, related_kwargs, __ in create_related:
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ class Migration(migrations.Migration):
|
||||||
('is_superuser', models.BooleanField(help_text='Designates that this user has all permissions without explicitly assigning them.', default=False, verbose_name='superuser status')),
|
('is_superuser', models.BooleanField(help_text='Designates that this user has all permissions without explicitly assigning them.', default=False, verbose_name='superuser status')),
|
||||||
('is_active', models.BooleanField(help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', default=True, verbose_name='active')),
|
('is_active', models.BooleanField(help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', default=True, verbose_name='active')),
|
||||||
('date_joined', models.DateTimeField(verbose_name='date joined', default=django.utils.timezone.now)),
|
('date_joined', models.DateTimeField(verbose_name='date joined', default=django.utils.timezone.now)),
|
||||||
('main_systemuser', models.ForeignKey(to='systemusers.SystemUser', editable=False, null=True, related_name='accounts_main')),
|
('main_systemuser', models.ForeignKey(to='systemusers.SystemUser', editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='accounts_main')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:08
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import orchestra.contrib.accounts.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('accounts', '0001_initial'), ('accounts', '0002_auto_20170528_2005'), ('accounts', '0003_auto_20210330_1049'), ('accounts', '0004_auto_20210422_1108')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('systemusers', '0001_initial'),
|
||||||
|
('auth', '0006_require_contenttypes_0002'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Account',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('username', models.CharField(help_text='Required. 64 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Enter a valid username.', 'invalid')], verbose_name='username')),
|
||||||
|
('short_name', models.CharField(blank=True, max_length=64, verbose_name='short name')),
|
||||||
|
('full_name', models.CharField(max_length=256, verbose_name='full name')),
|
||||||
|
('email', models.EmailField(help_text='Used for password recovery', max_length=254, verbose_name='email address')),
|
||||||
|
('type', models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('COMPANY', 'Company'), ('PUBLICBODY', 'Public body'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type')),
|
||||||
|
('language', models.CharField(choices=[('EN', 'English')], default='EN', max_length=2, verbose_name='language')),
|
||||||
|
('comments', models.TextField(blank=True, max_length=256, verbose_name='comments')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('main_systemuser', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='accounts_main', to='systemusers.SystemUser')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterModelManagers(
|
||||||
|
name='account',
|
||||||
|
managers=[
|
||||||
|
('objects', orchestra.contrib.accounts.models.AccountManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='language',
|
||||||
|
field=models.CharField(choices=[('CA', 'Catalan'), ('ES', 'Spanish'), ('EN', 'English')], default='CA', max_length=2, verbose_name='language'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='username',
|
||||||
|
field=models.CharField(help_text='Required. 32 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Enter a valid username.', 'invalid')], verbose_name='username'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='language',
|
||||||
|
field=models.CharField(choices=[('EN', 'English')], default='EN', max_length=2, verbose_name='language'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('COMPANY', 'Company'), ('PUBLICBODY', 'Public body'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='main_systemuser',
|
||||||
|
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='accounts_main', to='systemusers.SystemUser'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,38 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2017-05-28 18:05
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import orchestra.contrib.accounts.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelManagers(
|
||||||
|
name='account',
|
||||||
|
managers=[
|
||||||
|
('objects', orchestra.contrib.accounts.models.AccountManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='language',
|
||||||
|
field=models.CharField(choices=[('CA', 'Catalan'), ('ES', 'Spanish'), ('EN', 'English')], default='CA', max_length=2, verbose_name='language'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='username',
|
||||||
|
field=models.CharField(help_text='Required. 32 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Enter a valid username.', 'invalid')], verbose_name='username'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-03-30 10:49
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0002_auto_20170528_2005'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='language',
|
||||||
|
field=models.CharField(choices=[('EN', 'English')], default='EN', max_length=2, verbose_name='language'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('COMPANY', 'Company'), ('PUBLICBODY', 'Public body'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
#from orchestra.contrib.orchestration.middlewares import OperationsMiddleware
|
#from orchestra.contrib.orchestration.middlewares import OperationsMiddleware
|
||||||
#from orchestra.contrib.orchestration import Operation
|
#from orchestra.contrib.orchestration import Operation
|
||||||
from orchestra.core import services
|
from orchestra import core
|
||||||
from orchestra.models.utils import has_db_field
|
from orchestra.models.utils import has_db_field
|
||||||
from orchestra.utils.mail import send_email_template
|
from orchestra.utils.mail import send_email_template
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ class Account(auth.AbstractBaseUser):
|
||||||
validators.RegexValidator(r'^[\w.-]+$', _("Enter a valid username."), 'invalid')
|
validators.RegexValidator(r'^[\w.-]+$', _("Enter a valid username."), 'invalid')
|
||||||
])
|
])
|
||||||
main_systemuser = models.ForeignKey(settings.ACCOUNTS_SYSTEMUSER_MODEL, null=True,
|
main_systemuser = models.ForeignKey(settings.ACCOUNTS_SYSTEMUSER_MODEL, null=True,
|
||||||
related_name='accounts_main', editable=False)
|
related_name='accounts_main', editable=False, on_delete=models.SET_NULL)
|
||||||
short_name = models.CharField(_("short name"), max_length=64, blank=True)
|
short_name = models.CharField(_("short name"), max_length=64, blank=True)
|
||||||
full_name = models.CharField(_("full name"), max_length=256)
|
full_name = models.CharField(_("full name"), max_length=256)
|
||||||
email = models.EmailField(_('email address'), help_text=_("Used for password recovery"))
|
email = models.EmailField(_('email address'), help_text=_("Used for password recovery"))
|
||||||
|
@ -52,6 +52,11 @@ class Account(auth.AbstractBaseUser):
|
||||||
USERNAME_FIELD = 'username'
|
USERNAME_FIELD = 'username'
|
||||||
REQUIRED_FIELDS = ['email']
|
REQUIRED_FIELDS = ['email']
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# ignore `is_staff` kwarg because is handled with `is_superuser`
|
||||||
|
kwargs.pop('is_staff', None)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@ -98,7 +103,7 @@ class Account(auth.AbstractBaseUser):
|
||||||
]
|
]
|
||||||
for rel in related_fields:
|
for rel in related_fields:
|
||||||
source = getattr(rel, 'related_model', rel.model)
|
source = getattr(rel, 'related_model', rel.model)
|
||||||
if source in services and hasattr(source, 'active'):
|
if source in core.services and hasattr(source, 'active'):
|
||||||
for obj in getattr(self, rel.get_accessor_name()).all():
|
for obj in getattr(self, rel.get_accessor_name()).all():
|
||||||
yield obj
|
yield obj
|
||||||
|
|
||||||
|
@ -141,12 +146,25 @@ class Account(auth.AbstractBaseUser):
|
||||||
backend returns True. Thus, a user who has permission from a single
|
backend returns True. Thus, a user who has permission from a single
|
||||||
auth backend is assumed to have permission in general. If an object is
|
auth backend is assumed to have permission in general. If an object is
|
||||||
provided, permissions for this specific object are checked.
|
provided, permissions for this specific object are checked.
|
||||||
|
applabel.action_modelname
|
||||||
"""
|
"""
|
||||||
|
if not self.is_active:
|
||||||
|
return False
|
||||||
# Active superusers have all permissions.
|
# Active superusers have all permissions.
|
||||||
if self.is_active and self.is_superuser:
|
if self.is_superuser:
|
||||||
return True
|
return True
|
||||||
# Otherwise we need to check the backends.
|
app, action_model = perm.split('.')
|
||||||
return auth._user_has_perm(self, perm, obj)
|
action, model = action_model.split('_', 1)
|
||||||
|
service_apps = set(model._meta.app_label for model in core.services.get().keys())
|
||||||
|
accounting_apps = set(model._meta.app_label for model in core.accounts.get().keys())
|
||||||
|
import inspect
|
||||||
|
if ((app in service_apps or (action == 'view' and app in accounting_apps))):
|
||||||
|
# class-level permissions
|
||||||
|
if inspect.isclass(obj):
|
||||||
|
return True
|
||||||
|
elif obj and getattr(obj, 'account', None) == self:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def has_perms(self, perm_list, obj=None):
|
def has_perms(self, perm_list, obj=None):
|
||||||
"""
|
"""
|
||||||
|
@ -167,7 +185,6 @@ class Account(auth.AbstractBaseUser):
|
||||||
# Active superusers have all permissions.
|
# Active superusers have all permissions.
|
||||||
if self.is_active and self.is_superuser:
|
if self.is_active and self.is_superuser:
|
||||||
return True
|
return True
|
||||||
return auth._user_has_module_perms(self, app_label)
|
|
||||||
|
|
||||||
def get_related_passwords(self, db_field=False):
|
def get_related_passwords(self, db_field=False):
|
||||||
related = [
|
related = [
|
||||||
|
|
|
@ -7,7 +7,7 @@ class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Account
|
model = Account
|
||||||
fields = (
|
fields = (
|
||||||
'url', 'id', 'username', 'type', 'language', 'short_name', 'full_name', 'date_joined',
|
'url', 'id', 'username', 'type', 'language', 'short_name', 'full_name', 'date_joined', 'last_login',
|
||||||
'is_active'
|
'is_active'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from datetime import date
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.forms.models import modelformset_factory
|
from django.forms.models import modelformset_factory
|
||||||
from django.http import HttpResponse, HttpResponseRedirect
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
|
|
|
@ -2,11 +2,12 @@ from django import forms
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.contrib.admin.utils import unquote
|
from django.contrib.admin.utils import unquote
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Sum, Prefetch
|
from django.db.models import F, Sum, Prefetch
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
@ -15,12 +16,12 @@ from orchestra.admin import ExtendedModelAdmin
|
||||||
from orchestra.admin.utils import admin_date, insertattr, admin_link, change_url
|
from orchestra.admin.utils import admin_date, insertattr, admin_link, change_url
|
||||||
from orchestra.contrib.accounts.actions import list_accounts
|
from orchestra.contrib.accounts.actions import list_accounts
|
||||||
from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin
|
from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin
|
||||||
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
|
from orchestra.forms.widgets import PaddingCheckboxSelectMultiple
|
||||||
|
|
||||||
from . import settings, actions
|
from . import settings, actions
|
||||||
from .filters import (BillTypeListFilter, HasBillContactListFilter, TotalListFilter,
|
from .filters import (BillTypeListFilter, HasBillContactListFilter, TotalListFilter,
|
||||||
PaymentStateListFilter, AmendedListFilter)
|
PaymentStateListFilter, AmendedListFilter)
|
||||||
from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine,
|
from .models import (Bill, Invoice, AmendmentInvoice, AbonoInvoice, Fee, AmendmentFee, ProForma, BillLine,
|
||||||
BillSubline, BillContact)
|
BillSubline, BillContact)
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,6 +68,7 @@ class BillLineInline(admin.TabularInline):
|
||||||
|
|
||||||
order_link = admin_link('order', display='pk')
|
order_link = admin_link('order', display='pk')
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_total(self, line):
|
def display_total(self, line):
|
||||||
if line.pk:
|
if line.pk:
|
||||||
total = line.compute_total()
|
total = line.compute_total()
|
||||||
|
@ -78,7 +80,6 @@ class BillLineInline(admin.TabularInline):
|
||||||
return '<a href="%s" title="%s">%s <img src="%s"></img></a>' % (url, content, total, img)
|
return '<a href="%s" title="%s">%s <img src="%s"></img></a>' % (url, content, total, img)
|
||||||
return '<a href="%s">%s</a>' % (url, total)
|
return '<a href="%s">%s</a>' % (url, total)
|
||||||
display_total.short_description = _("Total")
|
display_total.short_description = _("Total")
|
||||||
display_total.allow_tags = True
|
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Make value input widget bigger """
|
""" Make value input widget bigger """
|
||||||
|
@ -104,27 +105,26 @@ class ClosedBillLineInline(BillLineInline):
|
||||||
readonly_fields = fields
|
readonly_fields = fields
|
||||||
can_delete = False
|
can_delete = False
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_description(self, line):
|
def display_description(self, line):
|
||||||
descriptions = [line.description]
|
descriptions = [line.description]
|
||||||
for subline in line.sublines.all():
|
for subline in line.sublines.all():
|
||||||
descriptions.append(' '*4+subline.description)
|
descriptions.append(' ' * 4 + subline.description)
|
||||||
return '<br>'.join(descriptions)
|
return '<br>'.join(descriptions)
|
||||||
display_description.short_description = _("Description")
|
display_description.short_description = _("Description")
|
||||||
display_description.allow_tags = True
|
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_subtotal(self, line):
|
def display_subtotal(self, line):
|
||||||
subtotals = [' ' + str(line.subtotal)]
|
subtotals = [' ' + str(line.subtotal)]
|
||||||
for subline in line.sublines.all():
|
for subline in line.sublines.all():
|
||||||
subtotals.append(str(subline.total))
|
subtotals.append(str(subline.total))
|
||||||
return '<br>'.join(subtotals)
|
return '<br>'.join(subtotals)
|
||||||
display_subtotal.short_description = _("Subtotal")
|
display_subtotal.short_description = _("Subtotal")
|
||||||
display_subtotal.allow_tags = True
|
|
||||||
|
|
||||||
def display_total(self, line):
|
def display_total(self, line):
|
||||||
if line.pk:
|
if line.pk:
|
||||||
return line.compute_total()
|
return line.compute_total()
|
||||||
display_total.short_description = _("Total")
|
display_total.short_description = _("Total")
|
||||||
display_total.allow_tags = True
|
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
return False
|
return False
|
||||||
|
@ -242,6 +242,7 @@ class BillLineManagerAdmin(BillLineAdmin):
|
||||||
|
|
||||||
|
|
||||||
class BillAdminMixin(AccountAdminMixin):
|
class BillAdminMixin(AccountAdminMixin):
|
||||||
|
@mark_safe
|
||||||
def display_total_with_subtotals(self, bill):
|
def display_total_with_subtotals(self, bill):
|
||||||
if bill.pk:
|
if bill.pk:
|
||||||
currency = settings.BILLS_CURRENCY.lower()
|
currency = settings.BILLS_CURRENCY.lower()
|
||||||
|
@ -251,10 +252,10 @@ class BillAdminMixin(AccountAdminMixin):
|
||||||
subtotals.append(_("Taxes %s%% VAT %s &%s;") % (tax, subtotal[1], currency))
|
subtotals.append(_("Taxes %s%% VAT %s &%s;") % (tax, subtotal[1], currency))
|
||||||
subtotals = '\n'.join(subtotals)
|
subtotals = '\n'.join(subtotals)
|
||||||
return '<span title="%s">%s &%s;</span>' % (subtotals, bill.compute_total(), currency)
|
return '<span title="%s">%s &%s;</span>' % (subtotals, bill.compute_total(), currency)
|
||||||
display_total_with_subtotals.allow_tags = True
|
|
||||||
display_total_with_subtotals.short_description = _("total")
|
display_total_with_subtotals.short_description = _("total")
|
||||||
display_total_with_subtotals.admin_order_field = 'approx_total'
|
display_total_with_subtotals.admin_order_field = 'approx_total'
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_payment_state(self, bill):
|
def display_payment_state(self, bill):
|
||||||
if bill.pk:
|
if bill.pk:
|
||||||
t_opts = bill.transactions.model._meta
|
t_opts = bill.transactions.model._meta
|
||||||
|
@ -276,7 +277,6 @@ class BillAdminMixin(AccountAdminMixin):
|
||||||
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
|
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
|
||||||
return '<a href="{url}" style="color:{color}" title="{title}">{name}</a>'.format(
|
return '<a href="{url}" style="color:{color}" title="{title}">{name}</a>'.format(
|
||||||
url=url, color=color, name=state, title=title)
|
url=url, color=color, name=state, title=title)
|
||||||
display_payment_state.allow_tags = True
|
|
||||||
display_payment_state.short_description = _("Payment")
|
display_payment_state.short_description = _("Payment")
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
|
@ -376,16 +376,14 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin):
|
||||||
|
|
||||||
def display_total(self, bill):
|
def display_total(self, bill):
|
||||||
currency = settings.BILLS_CURRENCY.lower()
|
currency = settings.BILLS_CURRENCY.lower()
|
||||||
return '%s &%s;' % (bill.compute_total(), currency)
|
return format_html('{} &{};', bill.compute_total(), currency)
|
||||||
display_total.allow_tags = True
|
|
||||||
display_total.short_description = _("total")
|
display_total.short_description = _("total")
|
||||||
display_total.admin_order_field = 'approx_total'
|
display_total.admin_order_field = 'approx_total'
|
||||||
|
|
||||||
def type_link(self, bill):
|
def type_link(self, bill):
|
||||||
bill_type = bill.type.lower()
|
bill_type = bill.type.lower()
|
||||||
url = reverse('admin:bills_%s_changelist' % bill_type)
|
url = reverse('admin:bills_%s_changelist' % bill_type)
|
||||||
return '<a href="%s">%s</a>' % (url, bill.get_type_display())
|
return format_html('<a href="{}">{}</a>', url, bill.get_type_display())
|
||||||
type_link.allow_tags = True
|
|
||||||
type_link.short_description = _("type")
|
type_link.short_description = _("type")
|
||||||
type_link.admin_order_field = 'type'
|
type_link.admin_order_field = 'type'
|
||||||
|
|
||||||
|
@ -461,6 +459,7 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin):
|
||||||
admin.site.register(Bill, BillAdmin)
|
admin.site.register(Bill, BillAdmin)
|
||||||
admin.site.register(Invoice, BillAdmin)
|
admin.site.register(Invoice, BillAdmin)
|
||||||
admin.site.register(AmendmentInvoice, BillAdmin)
|
admin.site.register(AmendmentInvoice, BillAdmin)
|
||||||
|
admin.site.register(AbonoInvoice, BillAdmin)
|
||||||
admin.site.register(Fee, BillAdmin)
|
admin.site.register(Fee, BillAdmin)
|
||||||
admin.site.register(AmendmentFee, BillAdmin)
|
admin.site.register(AmendmentFee, BillAdmin)
|
||||||
admin.site.register(ProForma, BillAdmin)
|
admin.site.register(ProForma, BillAdmin)
|
||||||
|
@ -478,7 +477,7 @@ class BillContactInline(admin.StackedInline):
|
||||||
if db_field.name == 'address':
|
if db_field.name == 'address':
|
||||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||||
if db_field.name == 'email_usage':
|
if db_field.name == 'email_usage':
|
||||||
kwargs['widget'] = paddingCheckboxSelectMultiple(45)
|
kwargs['widget'] = PaddingCheckboxSelectMultiple(45)
|
||||||
return super().formfield_for_dbfield(db_field, **kwargs)
|
return super().formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import action
|
||||||
|
|
||||||
from orchestra.api import router, LogApiMixin
|
from orchestra.api import router, LogApiMixin
|
||||||
from orchestra.contrib.accounts.api import AccountApiMixin
|
from orchestra.contrib.accounts.api import AccountApiMixin
|
||||||
|
@ -15,7 +15,7 @@ class BillViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
||||||
queryset = Bill.objects.all()
|
queryset = Bill.objects.all()
|
||||||
serializer_class = BillSerializer
|
serializer_class = BillSerializer
|
||||||
|
|
||||||
@detail_route(methods=['get'])
|
@action(detail=True, methods=['get'])
|
||||||
def document(self, request, pk):
|
def document(self, request, pk):
|
||||||
bill = self.get_object()
|
bill = self.get_object()
|
||||||
content_type = request.META.get('HTTP_ACCEPT')
|
content_type = request.META.get('HTTP_ACCEPT')
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.contrib.admin import SimpleListFilter
|
from django.contrib.admin import SimpleListFilter
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
@ -21,7 +21,7 @@ def validate_contact(request, bill, error=True):
|
||||||
message = msg.format(relation=_("Related"), account=account, url=url)
|
message = msg.format(relation=_("Related"), account=account, url=url)
|
||||||
send(request, mark_safe(message))
|
send(request, mark_safe(message))
|
||||||
valid = False
|
valid = False
|
||||||
main = type(bill).account.field.rel.to.objects.get_main()
|
main = type(bill).account.field.related_model.objects.get_main()
|
||||||
if not hasattr(main, 'billcontact'):
|
if not hasattr(main, 'billcontact'):
|
||||||
account = force_text(main)
|
account = force_text(main)
|
||||||
url = reverse('admin:accounts_account_change', args=(main.id,))
|
url = reverse('admin:accounts_account_change', args=(main.id,))
|
||||||
|
|
Binary file not shown.
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2015-10-29 10:51+0000\n"
|
"POT-Creation-Date: 2019-12-20 11:56+0100\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
@ -18,33 +18,33 @@ msgstr ""
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
#: actions.py:31
|
#: actions.py:33
|
||||||
msgid "View"
|
msgid "View"
|
||||||
msgstr "Vista"
|
msgstr "Vista"
|
||||||
|
|
||||||
#: actions.py:42
|
#: actions.py:45
|
||||||
msgid "Selected bills should be in open state"
|
msgid "Selected bills should be in open state"
|
||||||
msgstr "Les factures seleccionades han d'estar en estat obert"
|
msgstr "Les factures seleccionades han d'estar en estat obert"
|
||||||
|
|
||||||
#: actions.py:57
|
#: actions.py:60
|
||||||
msgid "Selected bills have been closed"
|
msgid "Selected bills have been closed"
|
||||||
msgstr "Les factures seleccionades han estat tancades"
|
msgstr "Les factures seleccionades han estat tancades"
|
||||||
|
|
||||||
#: actions.py:70
|
#: actions.py:73
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "<a href=\"%(url)s\">One related transaction</a> has been created"
|
msgid "<a href=\"%(url)s\">One related transaction</a> has been created"
|
||||||
msgstr "S'ha creat una <a href=\"%(url)s\">transacció</a>"
|
msgstr "S'ha creat una <a href=\"%(url)s\">transacció</a>"
|
||||||
|
|
||||||
#: actions.py:71
|
#: actions.py:74
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "<a href=\"%(url)s\">%(num)i related transactions</a> have been created"
|
msgid "<a href=\"%(url)s\">%(num)i related transactions</a> have been created"
|
||||||
msgstr "S'han creat les <a href=\"%(url)s\">%(num)i següents transaccions</a>"
|
msgstr "S'han creat les <a href=\"%(url)s\">%(num)i següents transaccions</a>"
|
||||||
|
|
||||||
#: actions.py:77
|
#: actions.py:80
|
||||||
msgid "Are you sure about closing the following bills?"
|
msgid "Are you sure about closing the following bills?"
|
||||||
msgstr "Estàs a punt de tancar les següents factures, estàs segur?"
|
msgstr "Estàs a punt de tancar les següents factures, estàs segur?"
|
||||||
|
|
||||||
#: actions.py:78
|
#: actions.py:81
|
||||||
msgid ""
|
msgid ""
|
||||||
"Once a bill is closed it can not be further modified.</p><p>Please select a "
|
"Once a bill is closed it can not be further modified.</p><p>Please select a "
|
||||||
"payment source for the selected bills"
|
"payment source for the selected bills"
|
||||||
|
@ -52,174 +52,205 @@ msgstr ""
|
||||||
"Una vegada la factura estigui tancada no podrà ser modificada.</p><p>Si us "
|
"Una vegada la factura estigui tancada no podrà ser modificada.</p><p>Si us "
|
||||||
"plau selecciona un mètode de pagament per les factures seleccionades"
|
"plau selecciona un mètode de pagament per les factures seleccionades"
|
||||||
|
|
||||||
#: actions.py:91
|
#: actions.py:97
|
||||||
msgid "Close"
|
msgid "Close"
|
||||||
msgstr "Tanca"
|
msgstr "Tanca"
|
||||||
|
|
||||||
#: actions.py:109
|
#: actions.py:115
|
||||||
msgid "One bill has been sent."
|
msgid "One bill has been sent."
|
||||||
msgstr "S'ha creat una factura"
|
msgstr "S'ha creat una factura"
|
||||||
|
|
||||||
#: actions.py:110
|
#: actions.py:116
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%i bills have been sent."
|
msgid "%i bills have been sent."
|
||||||
msgstr "S'han enviat %i factures."
|
msgstr "S'han enviat %i factures."
|
||||||
|
|
||||||
#: actions.py:117
|
#: actions.py:123
|
||||||
msgid "Resend"
|
msgid "Resend"
|
||||||
msgstr "Reenviat"
|
msgstr "Reenviat"
|
||||||
|
|
||||||
#: actions.py:137
|
#: actions.py:146
|
||||||
msgid "Download"
|
msgid "Download"
|
||||||
msgstr "Descarrega"
|
msgstr "Descarrega"
|
||||||
|
|
||||||
#: actions.py:153
|
#: actions.py:162
|
||||||
msgid "C.S.D."
|
msgid "C.S.D."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:155
|
#: actions.py:164
|
||||||
msgid "Close, send and download bills in one shot."
|
msgid "Close, send and download bills in one shot."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:216
|
#: actions.py:225
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(norders)s orders and %(nlines)s lines undoed."
|
msgid "%(norders)s orders and %(nlines)s lines undoed."
|
||||||
msgstr "%(norders)s ordres i %(nlines)s línies desfetes."
|
msgstr "%(norders)s ordres i %(nlines)s línies desfetes."
|
||||||
|
|
||||||
#: actions.py:235
|
#: actions.py:244
|
||||||
msgid "Lines moved"
|
msgid "Lines moved"
|
||||||
msgstr "Línies mogudes"
|
msgstr "Línies mogudes"
|
||||||
|
|
||||||
#: actions.py:248
|
#: actions.py:257
|
||||||
msgid "Selected bills should be in closed state"
|
msgid "Selected bills should be in closed state"
|
||||||
msgstr "Les factures seleccionades han d'estar en estat obert"
|
msgstr "Les factures seleccionades han d'estar en estat obert"
|
||||||
|
|
||||||
#: actions.py:265
|
#: actions.py:259
|
||||||
|
#, python-format
|
||||||
|
msgid "%s can not be amended."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: actions.py:279
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(type)s of %(related_type)s %(number)s and creation date %(date)s"
|
msgid "%(type)s of %(related_type)s %(number)s and creation date %(date)s"
|
||||||
msgstr "%(type)s de %(related_type)s %(number)s amb data de creació %(date)s"
|
msgstr "%(type)s de %(related_type)s %(number)s amb data de creació %(date)s"
|
||||||
|
|
||||||
#: actions.py:272
|
#: actions.py:286
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(related_type)s %(number)s subtotal for tax %(tax)s%%"
|
msgid "%(related_type)s %(number)s subtotal for tax %(tax)s%%"
|
||||||
msgstr "%(related_type)s %(number)s subtotal %(tax)s%%"
|
msgstr "%(related_type)s %(number)s subtotal %(tax)s%%"
|
||||||
|
|
||||||
#: actions.py:288
|
#: actions.py:303
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "<a href=\"%(url)s\">One amendment bill</a> have been generated."
|
msgid "<a href=\"%(url)s\">One amendment bill</a> have been generated."
|
||||||
msgstr "S'ha creat una <a href=\"%(url)s\">transacció</a>"
|
msgstr "S'ha creat una <a href=\"%(url)s\">transacció</a>"
|
||||||
|
|
||||||
#: actions.py:289
|
#: actions.py:304
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "<a href=\"%(url)s\">%(num)i amendment bills</a> have been generated."
|
msgid "<a href=\"%(url)s\">%(num)i amendment bills</a> have been generated."
|
||||||
msgstr "S'han creat les <a href=\"%(url)s\">%(num)i següents transaccions</a>"
|
msgstr "S'han creat les <a href=\"%(url)s\">%(num)i següents transaccions</a>"
|
||||||
|
|
||||||
#: actions.py:292
|
#: actions.py:307
|
||||||
msgid "Amend"
|
msgid "Amend"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:58 admin.py:103 admin.py:140 forms.py:11
|
#: admin.py:80 admin.py:126 admin.py:180 forms.py:11
|
||||||
#: templates/admin/bills/bill/report.html:43
|
#: templates/admin/bills/bill/report.html:43
|
||||||
#: templates/admin/bills/bill/report.html:70
|
#: templates/admin/bills/bill/report.html:70
|
||||||
msgid "Total"
|
msgid "Total"
|
||||||
msgstr "Total"
|
msgstr "Total"
|
||||||
|
|
||||||
#: admin.py:89
|
#: admin.py:112
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr "Descripció"
|
msgstr "Descripció"
|
||||||
|
|
||||||
#: admin.py:97
|
#: admin.py:120
|
||||||
msgid "Subtotal"
|
msgid "Subtotal"
|
||||||
msgstr "Subtotal"
|
msgstr "Subtotal"
|
||||||
|
|
||||||
#: admin.py:130
|
#: admin.py:146
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Total"
|
||||||
|
msgid "Totals"
|
||||||
|
msgstr "Total"
|
||||||
|
|
||||||
|
#: admin.py:150
|
||||||
|
msgid "Order"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin.py:169
|
||||||
msgid "Is open"
|
msgid "Is open"
|
||||||
msgstr "És oberta"
|
msgstr "És oberta"
|
||||||
|
|
||||||
#: admin.py:135
|
#: admin.py:175
|
||||||
msgid "Subline"
|
#, fuzzy
|
||||||
|
#| msgid "Subline"
|
||||||
|
msgid "Sublines"
|
||||||
msgstr "Sublínia"
|
msgstr "Sublínia"
|
||||||
|
|
||||||
#: admin.py:167
|
#: admin.py:221
|
||||||
msgid "No bills selected."
|
msgid "No bills selected."
|
||||||
msgstr "No hi ha factures seleccionades"
|
msgstr "No hi ha factures seleccionades"
|
||||||
|
|
||||||
#: admin.py:174
|
#: admin.py:229
|
||||||
#, python-format
|
#, fuzzy, python-format
|
||||||
msgid "Manage %s bill lines."
|
#| msgid "Manage %s bill lines."
|
||||||
|
msgid "Manage %s bill lines"
|
||||||
msgstr "Gestiona %s línies de factura."
|
msgstr "Gestiona %s línies de factura."
|
||||||
|
|
||||||
#: admin.py:176
|
#: admin.py:231
|
||||||
msgid "Bill not in open state."
|
msgid "Bill not in open state."
|
||||||
msgstr "La factura no està en estat obert"
|
msgstr "La factura no està en estat obert"
|
||||||
|
|
||||||
#: admin.py:179
|
#: admin.py:234
|
||||||
msgid "Not all bills are in open state."
|
msgid "Not all bills are in open state."
|
||||||
msgstr "No totes les factures estan en estat obert"
|
msgstr "No totes les factures estan en estat obert"
|
||||||
|
|
||||||
#: admin.py:180
|
#: admin.py:235
|
||||||
msgid "Manage bill lines of multiple bills."
|
#, fuzzy
|
||||||
|
#| msgid "Manage bill lines of multiple bills."
|
||||||
|
msgid "Manage bill lines of multiple bills"
|
||||||
msgstr "Gestiona línies de factura de multiples factures."
|
msgstr "Gestiona línies de factura de multiples factures."
|
||||||
|
|
||||||
#: admin.py:204
|
#: admin.py:250
|
||||||
msgid "Dates"
|
#, python-format
|
||||||
|
msgid "Subtotal %s%% VAT %s &%s;"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:209
|
#: admin.py:251
|
||||||
msgid "Raw"
|
#, python-format
|
||||||
msgstr "Raw"
|
msgid "Taxes %s%% VAT %s &%s;"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:235 models.py:73
|
#: admin.py:255 admin.py:381 filters.py:46
|
||||||
msgid "Created"
|
#: templates/bills/microspective.html:123
|
||||||
msgstr "Creada"
|
msgid "total"
|
||||||
|
msgstr "total"
|
||||||
|
|
||||||
#: admin.py:236
|
#: admin.py:275
|
||||||
#, fuzzy
|
msgid "This bill has been amended, this value may not be valid."
|
||||||
#| msgid "Close"
|
msgstr ""
|
||||||
msgid "Closed"
|
|
||||||
msgstr "Tanca"
|
|
||||||
|
|
||||||
#: admin.py:237
|
#: admin.py:280
|
||||||
#, fuzzy
|
msgid "Payment"
|
||||||
#| msgid "updated on"
|
msgstr "Pagament"
|
||||||
msgid "Updated"
|
|
||||||
msgstr "actualitzada el"
|
|
||||||
|
|
||||||
#: admin.py:246
|
#: admin.py:304
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "amended line"
|
#| msgid "amended line"
|
||||||
msgid "Amends"
|
msgid "Amends"
|
||||||
msgstr "línia rectificada"
|
msgstr "línia rectificada"
|
||||||
|
|
||||||
#: admin.py:252
|
#: admin.py:330
|
||||||
|
msgid "Dates"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin.py:335
|
||||||
|
msgid "Raw"
|
||||||
|
msgstr "Raw"
|
||||||
|
|
||||||
|
#: admin.py:358 models.py:75
|
||||||
|
msgid "Created"
|
||||||
|
msgstr "Creada"
|
||||||
|
|
||||||
|
#: admin.py:359
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Close"
|
||||||
|
msgid "Closed"
|
||||||
|
msgstr "Tanca"
|
||||||
|
|
||||||
|
#: admin.py:360
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "updated on"
|
||||||
|
msgid "Updated"
|
||||||
|
msgstr "actualitzada el"
|
||||||
|
|
||||||
|
#: admin.py:375
|
||||||
msgid "lines"
|
msgid "lines"
|
||||||
msgstr "línies"
|
msgstr "línies"
|
||||||
|
|
||||||
#: admin.py:257 filters.py:46 templates/bills/microspective.html:118
|
#: admin.py:389 models.py:108 models.py:501
|
||||||
msgid "total"
|
|
||||||
msgstr "total"
|
|
||||||
|
|
||||||
#: admin.py:265 models.py:104 models.py:460
|
|
||||||
msgid "type"
|
msgid "type"
|
||||||
msgstr "tipus"
|
msgstr "tipus"
|
||||||
|
|
||||||
#: admin.py:282
|
|
||||||
msgid "This bill has been amended, this value may not be valid."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: admin.py:287
|
|
||||||
msgid "Payment"
|
|
||||||
msgstr "Pagament"
|
|
||||||
|
|
||||||
#: filters.py:21
|
#: filters.py:21
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr "Tot"
|
msgstr "Tot"
|
||||||
|
|
||||||
#: filters.py:22 models.py:88
|
#: filters.py:22 models.py:91
|
||||||
msgid "Invoice"
|
msgid "Invoice"
|
||||||
msgstr "Factura"
|
msgstr "Factura"
|
||||||
|
|
||||||
#: filters.py:23 models.py:90
|
#: filters.py:23 models.py:93
|
||||||
msgid "Fee"
|
msgid "Fee"
|
||||||
msgstr "Quota de soci"
|
msgstr "Quota de soci"
|
||||||
|
|
||||||
|
@ -231,65 +262,67 @@ msgstr "Pro-forma"
|
||||||
msgid "Amendment fee"
|
msgid "Amendment fee"
|
||||||
msgstr "Rectificació de quota de soci"
|
msgstr "Rectificació de quota de soci"
|
||||||
|
|
||||||
#: filters.py:26 models.py:89
|
#: filters.py:26 models.py:92
|
||||||
msgid "Amendment invoice"
|
msgid "Amendment invoice"
|
||||||
msgstr "Factura rectificativa"
|
msgstr "Factura rectificativa"
|
||||||
|
|
||||||
#: filters.py:68
|
#: filters.py:71
|
||||||
msgid "has bill contact"
|
msgid "has bill contact"
|
||||||
msgstr "té contacte de facturació"
|
msgstr "té contacte de facturació"
|
||||||
|
|
||||||
#: filters.py:73
|
#: filters.py:76
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr "Si"
|
msgstr "Si"
|
||||||
|
|
||||||
#: filters.py:74
|
#: filters.py:77
|
||||||
msgid "No"
|
msgid "No"
|
||||||
msgstr "No"
|
msgstr "No"
|
||||||
|
|
||||||
#: filters.py:85
|
#: filters.py:88
|
||||||
msgid "payment state"
|
msgid "payment state"
|
||||||
msgstr "Pagament"
|
msgstr "Pagament"
|
||||||
|
|
||||||
#: filters.py:90 models.py:72
|
#: filters.py:93 models.py:74
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: filters.py:91 models.py:76
|
#: filters.py:94 models.py:78
|
||||||
msgid "Paid"
|
msgid "Paid"
|
||||||
msgstr "Pagat"
|
msgstr "Pagat"
|
||||||
|
|
||||||
#: filters.py:92
|
#: filters.py:95
|
||||||
msgid "Pending"
|
msgid "Pending"
|
||||||
msgstr "Pendent"
|
msgstr "Pendent"
|
||||||
|
|
||||||
#: filters.py:93 models.py:79
|
#: filters.py:96 models.py:81
|
||||||
msgid "Bad debt"
|
msgid "Bad debt"
|
||||||
msgstr "Incobrable"
|
msgstr "Incobrable"
|
||||||
|
|
||||||
#: filters.py:135
|
#: filters.py:138
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "amended line"
|
#| msgid "amended line"
|
||||||
msgid "amended"
|
msgid "amended"
|
||||||
msgstr "línia rectificada"
|
msgstr "línia rectificada"
|
||||||
|
|
||||||
#: filters.py:140
|
#: filters.py:143
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Due date"
|
#| msgid "Due date"
|
||||||
msgid "Closed amends"
|
msgid "Closed amends"
|
||||||
msgstr "Data de pagament"
|
msgstr "Data de pagament"
|
||||||
|
|
||||||
#: filters.py:141
|
#: filters.py:144
|
||||||
msgid "Open or closed amends"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: filters.py:142
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "closed on"
|
#| msgid "Due date"
|
||||||
msgid "No closed amends"
|
msgid "Open amends"
|
||||||
msgstr "tancat el"
|
msgstr "Data de pagament"
|
||||||
|
|
||||||
#: filters.py:143
|
#: filters.py:145
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "amended line"
|
||||||
|
msgid "Any amends"
|
||||||
|
msgstr "línia rectificada"
|
||||||
|
|
||||||
|
#: filters.py:146
|
||||||
msgid "No amends"
|
msgid "No amends"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -309,7 +342,7 @@ msgstr "Tipus"
|
||||||
msgid "Source"
|
msgid "Source"
|
||||||
msgstr "Font"
|
msgstr "Font"
|
||||||
|
|
||||||
#: helpers.py:10
|
#: helpers.py:14
|
||||||
msgid ""
|
msgid ""
|
||||||
"{relation} account \"{account}\" does not have a declared invoice contact. "
|
"{relation} account \"{account}\" does not have a declared invoice contact. "
|
||||||
"You should <a href=\"{url}#invoicecontact-group\">provide one</a>"
|
"You should <a href=\"{url}#invoicecontact-group\">provide one</a>"
|
||||||
|
@ -317,213 +350,235 @@ msgstr ""
|
||||||
"{relation} compte \"{account}\" no te un contacte de facturació. Hauries de "
|
"{relation} compte \"{account}\" no te un contacte de facturació. Hauries de "
|
||||||
"<a href=\"{url}#invoicecontact-group\">proporcionar un</a>"
|
"<a href=\"{url}#invoicecontact-group\">proporcionar un</a>"
|
||||||
|
|
||||||
#: helpers.py:17
|
#: helpers.py:21
|
||||||
msgid "Related"
|
msgid "Related"
|
||||||
msgstr "Relacionat"
|
msgstr "Relacionat"
|
||||||
|
|
||||||
#: helpers.py:24
|
#: helpers.py:28
|
||||||
msgid "Main"
|
msgid "Main"
|
||||||
msgstr "Principal"
|
msgstr "Principal"
|
||||||
|
|
||||||
#: models.py:24 models.py:100
|
#: models.py:26 models.py:104
|
||||||
msgid "account"
|
msgid "account"
|
||||||
msgstr "compte"
|
msgstr "compte"
|
||||||
|
|
||||||
#: models.py:26
|
#: models.py:28
|
||||||
msgid "name"
|
msgid "name"
|
||||||
msgstr "nom"
|
msgstr "nom"
|
||||||
|
|
||||||
#: models.py:27
|
#: models.py:29
|
||||||
msgid "Account full name will be used when left blank."
|
msgid "Account full name will be used when left blank."
|
||||||
msgstr "S'emprarà el nom complet del compte quan es deixi en blanc."
|
msgstr "S'emprarà el nom complet del compte quan es deixi en blanc."
|
||||||
|
|
||||||
#: models.py:28
|
#: models.py:30
|
||||||
msgid "address"
|
msgid "address"
|
||||||
msgstr "adreça"
|
msgstr "adreça"
|
||||||
|
|
||||||
#: models.py:29
|
#: models.py:31
|
||||||
msgid "city"
|
msgid "city"
|
||||||
msgstr "ciutat"
|
msgstr "ciutat"
|
||||||
|
|
||||||
#: models.py:31
|
#: models.py:33
|
||||||
msgid "zip code"
|
msgid "zip code"
|
||||||
msgstr "codi postal"
|
msgstr "codi postal"
|
||||||
|
|
||||||
#: models.py:32
|
#: models.py:34
|
||||||
msgid "Enter a valid zipcode."
|
msgid "Enter a valid zipcode."
|
||||||
msgstr "Introdueix un codi postal vàlid."
|
msgstr "Introdueix un codi postal vàlid."
|
||||||
|
|
||||||
#: models.py:33
|
#: models.py:35
|
||||||
msgid "country"
|
msgid "country"
|
||||||
msgstr "país"
|
msgstr "país"
|
||||||
|
|
||||||
#: models.py:36 templates/admin/bills/bill/report.html:65
|
#: models.py:38 templates/admin/bills/bill/report.html:65
|
||||||
msgid "VAT number"
|
msgid "VAT number"
|
||||||
msgstr "NIF"
|
msgstr "NIF"
|
||||||
|
|
||||||
#: models.py:74
|
#: models.py:76
|
||||||
msgid "Processed"
|
msgid "Processed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:75
|
#: models.py:77
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "amended line"
|
#| msgid "amended line"
|
||||||
msgid "Amended"
|
msgid "Amended"
|
||||||
msgstr "línia rectificada"
|
msgstr "línia rectificada"
|
||||||
|
|
||||||
#: models.py:77
|
#: models.py:79
|
||||||
msgid "Incomplete"
|
msgid "Incomplete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:78
|
#: models.py:80
|
||||||
msgid "Executed"
|
msgid "Executed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:91
|
#: models.py:94
|
||||||
msgid "Amendment Fee"
|
msgid "Amendment Fee"
|
||||||
msgstr "Rectificació de quota de soci"
|
msgstr "Rectificació de quota de soci"
|
||||||
|
|
||||||
#: models.py:92
|
#: models.py:95
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Invoice"
|
||||||
|
msgid "Abono Invoice"
|
||||||
|
msgstr "Abonament"
|
||||||
|
|
||||||
|
#: models.py:96
|
||||||
msgid "Pro forma"
|
msgid "Pro forma"
|
||||||
msgstr "Pro forma"
|
msgstr "Pro forma"
|
||||||
|
|
||||||
#: models.py:99
|
#: models.py:103
|
||||||
msgid "number"
|
msgid "number"
|
||||||
msgstr "número"
|
msgstr "número"
|
||||||
|
|
||||||
#: models.py:102
|
#: models.py:106
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "amended line"
|
#| msgid "amended line"
|
||||||
msgid "amend of"
|
msgid "amend of"
|
||||||
msgstr "línia rectificada"
|
msgstr "línia rectificada"
|
||||||
|
|
||||||
#: models.py:105
|
#: models.py:109
|
||||||
msgid "created on"
|
msgid "created on"
|
||||||
msgstr "creat el"
|
msgstr "creat el"
|
||||||
|
|
||||||
#: models.py:106
|
#: models.py:110
|
||||||
msgid "closed on"
|
msgid "closed on"
|
||||||
msgstr "tancat el"
|
msgstr "tancat el"
|
||||||
|
|
||||||
#: models.py:107
|
#: models.py:111
|
||||||
msgid "open"
|
msgid "open"
|
||||||
msgstr "obert"
|
msgstr "obert"
|
||||||
|
|
||||||
#: models.py:108
|
#: models.py:112
|
||||||
msgid "sent"
|
msgid "sent"
|
||||||
msgstr "enviat"
|
msgstr "enviat"
|
||||||
|
|
||||||
#: models.py:109
|
#: models.py:113
|
||||||
msgid "due on"
|
msgid "due on"
|
||||||
msgstr "es deu"
|
msgstr "es deu"
|
||||||
|
|
||||||
#: models.py:110
|
#: models.py:114
|
||||||
msgid "updated on"
|
msgid "updated on"
|
||||||
msgstr "actualitzada el"
|
msgstr "actualitzada el"
|
||||||
|
|
||||||
#: models.py:112
|
#: models.py:116
|
||||||
msgid "comments"
|
msgid "comments"
|
||||||
msgstr "comentaris"
|
msgstr "comentaris"
|
||||||
|
|
||||||
#: models.py:113
|
#: models.py:117
|
||||||
msgid "HTML"
|
msgid "HTML"
|
||||||
msgstr "HTML"
|
msgstr "HTML"
|
||||||
|
|
||||||
#: models.py:194
|
#: models.py:200
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Type %s is not an amendment."
|
msgid "Type %s is not an amendment."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:196
|
#: models.py:202
|
||||||
msgid "Amend of related account doesn't match bill account."
|
msgid "Amend of related account doesn't match bill account."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:198
|
#: models.py:204
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Bill not in open state."
|
#| msgid "Bill not in open state."
|
||||||
msgid "Related invoice is in open state."
|
msgid "Related invoice is in open state."
|
||||||
msgstr "La factura no està en estat obert"
|
msgstr "La factura no està en estat obert"
|
||||||
|
|
||||||
#: models.py:200
|
#: models.py:206
|
||||||
msgid "Related invoice is an amendment."
|
msgid "Related invoice is an amendment."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:392
|
#: models.py:419
|
||||||
msgid "bill"
|
msgid "bill"
|
||||||
msgstr "factura"
|
msgstr "factura"
|
||||||
|
|
||||||
#: models.py:393 models.py:458 templates/bills/microspective.html:73
|
#: models.py:420 models.py:499 templates/bills/microspective.html:75
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "descripció"
|
msgstr "descripció"
|
||||||
|
|
||||||
#: models.py:394
|
#: models.py:421
|
||||||
msgid "rate"
|
msgid "rate"
|
||||||
msgstr "tarifa"
|
msgstr "tarifa"
|
||||||
|
|
||||||
#: models.py:395
|
#: models.py:422
|
||||||
msgid "quantity"
|
msgid "quantity"
|
||||||
msgstr "quantitat"
|
msgstr "quantitat"
|
||||||
|
|
||||||
#: models.py:397
|
#: models.py:424
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "quantity"
|
#| msgid "quantity"
|
||||||
msgid "Verbose quantity"
|
msgid "Verbose quantity"
|
||||||
msgstr "quantitat"
|
msgstr "quantitat"
|
||||||
|
|
||||||
#: models.py:398 templates/admin/bills/bill/report.html:47
|
#: models.py:425 templates/admin/bills/bill/report.html:47
|
||||||
#: templates/bills/microspective.html:77
|
#: templates/bills/microspective.html:79
|
||||||
#: templates/bills/microspective.html:111
|
#: templates/bills/microspective.html:116
|
||||||
msgid "subtotal"
|
msgid "subtotal"
|
||||||
msgstr "subtotal"
|
msgstr "subtotal"
|
||||||
|
|
||||||
#: models.py:399
|
#: models.py:426
|
||||||
msgid "tax"
|
msgid "tax"
|
||||||
msgstr "impostos"
|
msgstr "impostos"
|
||||||
|
|
||||||
#: models.py:400
|
#: models.py:427
|
||||||
msgid "start"
|
msgid "start"
|
||||||
msgstr "iniciar"
|
msgstr "iniciar"
|
||||||
|
|
||||||
#: models.py:401
|
#: models.py:428
|
||||||
msgid "end"
|
msgid "end"
|
||||||
msgstr "finalitzar"
|
msgstr "finalitzar"
|
||||||
|
|
||||||
#: models.py:403
|
#: models.py:431
|
||||||
msgid "Informative link back to the order"
|
msgid "Informative link back to the order"
|
||||||
msgstr "Enllaç informatiu de l'ordre"
|
msgstr "Enllaç informatiu de l'ordre"
|
||||||
|
|
||||||
#: models.py:404
|
#: models.py:432
|
||||||
msgid "order billed"
|
msgid "order billed"
|
||||||
msgstr "ordre facturada"
|
msgstr "ordre facturada"
|
||||||
|
|
||||||
#: models.py:405
|
#: models.py:433
|
||||||
msgid "order billed until"
|
msgid "order billed until"
|
||||||
msgstr "ordre facturada fins a"
|
msgstr "ordre facturada fins a"
|
||||||
|
|
||||||
#: models.py:406
|
#: models.py:434
|
||||||
msgid "created"
|
msgid "created"
|
||||||
msgstr "creada"
|
msgstr "creada"
|
||||||
|
|
||||||
#: models.py:408
|
#: models.py:436
|
||||||
msgid "amended line"
|
msgid "amended line"
|
||||||
msgstr "línia rectificada"
|
msgstr "línia rectificada"
|
||||||
|
|
||||||
#: models.py:451
|
#: models.py:492
|
||||||
msgid "Volume"
|
msgid "Volume"
|
||||||
msgstr "Volum"
|
msgstr "Volum"
|
||||||
|
|
||||||
#: models.py:452
|
#: models.py:493
|
||||||
msgid "Compensation"
|
msgid "Compensation"
|
||||||
msgstr "Compensació"
|
msgstr "Compensació"
|
||||||
|
|
||||||
#: models.py:453
|
#: models.py:494
|
||||||
msgid "Other"
|
msgid "Other"
|
||||||
msgstr "Altre"
|
msgstr "Altre"
|
||||||
|
|
||||||
#: models.py:457
|
#: models.py:498
|
||||||
msgid "bill line"
|
msgid "bill line"
|
||||||
msgstr "línia de factura"
|
msgstr "línia de factura"
|
||||||
|
|
||||||
|
#: templates/admin/bills/bill/change_list.html:9
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "lines"
|
||||||
|
msgid "Lines"
|
||||||
|
msgstr "línies"
|
||||||
|
|
||||||
|
#: templates/admin/bills/bill/change_list.html:15
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "bill"
|
||||||
|
msgid "Add bill"
|
||||||
|
msgstr "factura"
|
||||||
|
|
||||||
|
#: templates/admin/bills/bill/close_send_download_bills.html:57
|
||||||
|
msgid "Yes, I'm sure"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/bills/bill/report.html:42
|
#: templates/admin/bills/bill/report.html:42
|
||||||
msgid "Summary"
|
msgid "Summary"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -531,19 +586,19 @@ msgstr ""
|
||||||
#: templates/admin/bills/bill/report.html:47
|
#: templates/admin/bills/bill/report.html:47
|
||||||
#: templates/admin/bills/bill/report.html:51
|
#: templates/admin/bills/bill/report.html:51
|
||||||
#: templates/admin/bills/bill/report.html:69
|
#: templates/admin/bills/bill/report.html:69
|
||||||
#: templates/bills/microspective.html:111
|
#: templates/bills/microspective.html:116
|
||||||
#: templates/bills/microspective.html:114
|
#: templates/bills/microspective.html:119
|
||||||
msgid "VAT"
|
msgid "VAT"
|
||||||
msgstr "IVA"
|
msgstr "IVA"
|
||||||
|
|
||||||
#: templates/admin/bills/bill/report.html:51
|
#: templates/admin/bills/bill/report.html:51
|
||||||
#: templates/bills/microspective.html:114
|
#: templates/bills/microspective.html:119
|
||||||
msgid "taxes"
|
msgid "taxes"
|
||||||
msgstr "impostos"
|
msgstr "impostos"
|
||||||
|
|
||||||
#: templates/admin/bills/bill/report.html:56
|
#: templates/admin/bills/bill/report.html:56
|
||||||
#: templates/admin/bills/billline/report.html:60
|
#: templates/admin/bills/billline/report.html:60
|
||||||
#: templates/bills/microspective.html:53
|
#: templates/bills/microspective.html:54
|
||||||
msgid "TOTAL"
|
msgid "TOTAL"
|
||||||
msgstr "TOTAL"
|
msgstr "TOTAL"
|
||||||
|
|
||||||
|
@ -561,8 +616,20 @@ msgstr "Data de pagament"
|
||||||
msgid "Base"
|
msgid "Base"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/bills/billline/change_list.html:6
|
||||||
|
msgid "Home"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/bills/billline/change_list.html:8
|
||||||
|
msgid "Bills"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/bills/billline/change_list.html:9
|
||||||
|
msgid "Multiple bills"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/bills/billline/report.html:42
|
#: templates/admin/bills/billline/report.html:42
|
||||||
msgid "Services"
|
msgid "Service"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/bills/billline/report.html:43
|
#: templates/admin/bills/billline/report.html:43
|
||||||
|
@ -587,27 +654,21 @@ msgstr "quantitat"
|
||||||
msgid "Profit"
|
msgid "Profit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/bills/change_list.html:9
|
#: templates/bills/microspective-fee.html:115
|
||||||
#, fuzzy
|
|
||||||
#| msgid "bill"
|
|
||||||
msgid "Add bill"
|
|
||||||
msgstr "factura"
|
|
||||||
|
|
||||||
#: templates/bills/microspective-fee.html:107
|
|
||||||
msgid "Due date"
|
msgid "Due date"
|
||||||
msgstr "Data de pagament"
|
msgstr "Data de pagament"
|
||||||
|
|
||||||
#: templates/bills/microspective-fee.html:108
|
#: templates/bills/microspective-fee.html:116
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "On %(bank_account)s"
|
msgid "On %(bank_account)s"
|
||||||
msgstr "Al %(bank_account)s"
|
msgstr "Al %(bank_account)s"
|
||||||
|
|
||||||
#: templates/bills/microspective-fee.html:114
|
#: templates/bills/microspective-fee.html:122
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "From %(ini)s to %(end)s"
|
msgid "From %(ini)s to %(end)s"
|
||||||
msgstr "De %(ini)s a %(end)s"
|
msgstr "De %(ini)s a %(end)s"
|
||||||
|
|
||||||
#: templates/bills/microspective-fee.html:121
|
#: templates/bills/microspective-fee.html:144
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
"<strong>With your membership</strong> you are supporting ...\n"
|
"<strong>With your membership</strong> you are supporting ...\n"
|
||||||
|
@ -615,36 +676,36 @@ msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"<strong>Amb la teva quota de soci</strong> estàs donant suport ...\n"
|
"<strong>Amb la teva quota de soci</strong> estàs donant suport ...\n"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:49
|
#: templates/bills/microspective.html:50
|
||||||
msgid "DUE DATE"
|
msgid "DUE DATE"
|
||||||
msgstr "VENCIMENT"
|
msgstr "VENCIMENT"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:57
|
#: templates/bills/microspective.html:58
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(bill_type)s DATE"
|
msgid "%(bill_type)s DATE"
|
||||||
msgstr "DATA %(bill_type)s"
|
msgstr "DATA %(bill_type)s"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:74
|
#: templates/bills/microspective.html:76
|
||||||
msgid "period"
|
msgid "period"
|
||||||
msgstr "període"
|
msgstr "període"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:75
|
#: templates/bills/microspective.html:77
|
||||||
msgid "hrs/qty"
|
msgid "hrs/qty"
|
||||||
msgstr "hrs/qnt"
|
msgstr "hrs/qnt"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:76
|
#: templates/bills/microspective.html:78
|
||||||
msgid "rate/price"
|
msgid "rate/price"
|
||||||
msgstr "tarifa/preu"
|
msgstr "tarifa/preu"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:131
|
#: templates/bills/microspective.html:137
|
||||||
msgid "COMMENTS"
|
msgid "COMMENTS"
|
||||||
msgstr "COMENTARIS"
|
msgstr "COMENTARIS"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:138
|
#: templates/bills/microspective.html:145
|
||||||
msgid "PAYMENT"
|
msgid "PAYMENT"
|
||||||
msgstr "PAGAMENT"
|
msgstr "PAGAMENT"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:142
|
#: templates/bills/microspective.html:149
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
|
@ -658,11 +719,11 @@ msgstr ""
|
||||||
"Pots pagar aquesta <i>%(type)s</i> per transferència bancaria.<br>Inclou el "
|
"Pots pagar aquesta <i>%(type)s</i> per transferència bancaria.<br>Inclou el "
|
||||||
"teu nom i el número de <i>%(type)s</i>. El nostre compte bancari és"
|
"teu nom i el número de <i>%(type)s</i>. El nostre compte bancari és"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:151
|
#: templates/bills/microspective.html:160
|
||||||
msgid "QUESTIONS"
|
msgid "QUESTIONS"
|
||||||
msgstr "PREGUNTES"
|
msgstr "PREGUNTES"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:152
|
#: templates/bills/microspective.html:161
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
|
@ -679,5 +740,10 @@ msgstr ""
|
||||||
"ràpidament possible.\n"
|
"ràpidament possible.\n"
|
||||||
" "
|
" "
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
#~| msgid "closed on"
|
||||||
|
#~ msgid "No closed amends"
|
||||||
|
#~ msgstr "tancat el"
|
||||||
|
|
||||||
#~ msgid "positive price"
|
#~ msgid "positive price"
|
||||||
#~ msgstr "preu positiu"
|
#~ msgstr "preu positiu"
|
||||||
|
|
Binary file not shown.
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2015-10-29 10:51+0000\n"
|
"POT-Creation-Date: 2019-12-20 11:56+0100\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
@ -18,33 +18,33 @@ msgstr ""
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
#: actions.py:31
|
#: actions.py:33
|
||||||
msgid "View"
|
msgid "View"
|
||||||
msgstr "Vista"
|
msgstr "Vista"
|
||||||
|
|
||||||
#: actions.py:42
|
#: actions.py:45
|
||||||
msgid "Selected bills should be in open state"
|
msgid "Selected bills should be in open state"
|
||||||
msgstr "Las facturas seleccionadas están en estado abierto"
|
msgstr "Las facturas seleccionadas están en estado abierto"
|
||||||
|
|
||||||
#: actions.py:57
|
#: actions.py:60
|
||||||
msgid "Selected bills have been closed"
|
msgid "Selected bills have been closed"
|
||||||
msgstr "Las facturas seleccionadas han sido cerradas"
|
msgstr "Las facturas seleccionadas han sido cerradas"
|
||||||
|
|
||||||
#: actions.py:70
|
#: actions.py:73
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "<a href=\"%(url)s\">One related transaction</a> has been created"
|
msgid "<a href=\"%(url)s\">One related transaction</a> has been created"
|
||||||
msgstr "Se ha creado una <a href=\"%(url)s\">transacción</a>"
|
msgstr "Se ha creado una <a href=\"%(url)s\">transacción</a>"
|
||||||
|
|
||||||
#: actions.py:71
|
#: actions.py:74
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "<a href=\"%(url)s\">%(num)i related transactions</a> have been created"
|
msgid "<a href=\"%(url)s\">%(num)i related transactions</a> have been created"
|
||||||
msgstr "Se han creado <a href=\"%(url)s\">%(num)i transacciones</a>"
|
msgstr "Se han creado <a href=\"%(url)s\">%(num)i transacciones</a>"
|
||||||
|
|
||||||
#: actions.py:77
|
#: actions.py:80
|
||||||
msgid "Are you sure about closing the following bills?"
|
msgid "Are you sure about closing the following bills?"
|
||||||
msgstr "Estás a punto de cerrar las sigüientes facturas. ¿Estás seguro?"
|
msgstr "Estás a punto de cerrar las sigüientes facturas. ¿Estás seguro?"
|
||||||
|
|
||||||
#: actions.py:78
|
#: actions.py:81
|
||||||
msgid ""
|
msgid ""
|
||||||
"Once a bill is closed it can not be further modified.</p><p>Please select a "
|
"Once a bill is closed it can not be further modified.</p><p>Please select a "
|
||||||
"payment source for the selected bills"
|
"payment source for the selected bills"
|
||||||
|
@ -52,174 +52,199 @@ msgstr ""
|
||||||
"Una vez cerrada la factura ya no se podrá modificar.</p><p>Por favor "
|
"Una vez cerrada la factura ya no se podrá modificar.</p><p>Por favor "
|
||||||
"seleciona un metodo de pago para las facturas seleccionadas"
|
"seleciona un metodo de pago para las facturas seleccionadas"
|
||||||
|
|
||||||
#: actions.py:91
|
#: actions.py:97
|
||||||
msgid "Close"
|
msgid "Close"
|
||||||
msgstr "Cerrar"
|
msgstr "Cerrar"
|
||||||
|
|
||||||
#: actions.py:109
|
#: actions.py:115
|
||||||
msgid "One bill has been sent."
|
msgid "One bill has been sent."
|
||||||
msgstr "Se ha enviado una factura"
|
msgstr "Se ha enviado una factura"
|
||||||
|
|
||||||
#: actions.py:110
|
#: actions.py:116
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%i bills have been sent."
|
msgid "%i bills have been sent."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:117
|
#: actions.py:123
|
||||||
msgid "Resend"
|
msgid "Resend"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:137
|
#: actions.py:146
|
||||||
msgid "Download"
|
msgid "Download"
|
||||||
msgstr "Descarga"
|
msgstr "Descarga"
|
||||||
|
|
||||||
#: actions.py:153
|
#: actions.py:162
|
||||||
msgid "C.S.D."
|
msgid "C.S.D."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:155
|
#: actions.py:164
|
||||||
msgid "Close, send and download bills in one shot."
|
msgid "Close, send and download bills in one shot."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:216
|
#: actions.py:225
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(norders)s orders and %(nlines)s lines undoed."
|
msgid "%(norders)s orders and %(nlines)s lines undoed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:235
|
#: actions.py:244
|
||||||
msgid "Lines moved"
|
msgid "Lines moved"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:248
|
#: actions.py:257
|
||||||
msgid "Selected bills should be in closed state"
|
msgid "Selected bills should be in closed state"
|
||||||
msgstr "Las facturas seleccionadas están en estado abierto"
|
msgstr "Las facturas seleccionadas están en estado abierto"
|
||||||
|
|
||||||
#: actions.py:265
|
#: actions.py:259
|
||||||
|
#, python-format
|
||||||
|
msgid "%s can not be amended."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: actions.py:279
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(type)s of %(related_type)s %(number)s and creation date %(date)s"
|
msgid "%(type)s of %(related_type)s %(number)s and creation date %(date)s"
|
||||||
msgstr "%(type)s de %(related_type)s %(number)s con fecha de creación %(date)s"
|
msgstr "%(type)s de %(related_type)s %(number)s con fecha de creación %(date)s"
|
||||||
|
|
||||||
#: actions.py:272
|
#: actions.py:286
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(related_type)s %(number)s subtotal for tax %(tax)s%%"
|
msgid "%(related_type)s %(number)s subtotal for tax %(tax)s%%"
|
||||||
msgstr "%(related_type)s %(number)s subtotal %(tax)s%%"
|
msgstr "%(related_type)s %(number)s subtotal %(tax)s%%"
|
||||||
|
|
||||||
#: actions.py:288
|
#: actions.py:303
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "<a href=\"%(url)s\">One amendment bill</a> have been generated."
|
msgid "<a href=\"%(url)s\">One amendment bill</a> have been generated."
|
||||||
msgstr "Se ha creado una <a href=\"%(url)s\">transacción</a>"
|
msgstr "Se ha creado una <a href=\"%(url)s\">transacción</a>"
|
||||||
|
|
||||||
#: actions.py:289
|
#: actions.py:304
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "<a href=\"%(url)s\">%(num)i amendment bills</a> have been generated."
|
msgid "<a href=\"%(url)s\">%(num)i amendment bills</a> have been generated."
|
||||||
msgstr "Se han creado <a href=\"%(url)s\">%(num)i transacciones</a>"
|
msgstr "Se han creado <a href=\"%(url)s\">%(num)i transacciones</a>"
|
||||||
|
|
||||||
#: actions.py:292
|
#: actions.py:307
|
||||||
msgid "Amend"
|
msgid "Amend"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:58 admin.py:103 admin.py:140 forms.py:11
|
#: admin.py:80 admin.py:126 admin.py:180 forms.py:11
|
||||||
#: templates/admin/bills/bill/report.html:43
|
#: templates/admin/bills/bill/report.html:43
|
||||||
#: templates/admin/bills/bill/report.html:70
|
#: templates/admin/bills/bill/report.html:70
|
||||||
msgid "Total"
|
msgid "Total"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:89
|
#: admin.py:112
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:97
|
#: admin.py:120
|
||||||
msgid "Subtotal"
|
msgid "Subtotal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:130
|
#: admin.py:146
|
||||||
|
msgid "Totals"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin.py:150
|
||||||
|
msgid "Order"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin.py:169
|
||||||
msgid "Is open"
|
msgid "Is open"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:135
|
#: admin.py:175
|
||||||
msgid "Subline"
|
msgid "Sublines"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:167
|
#: admin.py:221
|
||||||
msgid "No bills selected."
|
msgid "No bills selected."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:174
|
#: admin.py:229
|
||||||
#, python-format
|
#, fuzzy, python-format
|
||||||
msgid "Manage %s bill lines."
|
#| msgid "bill line"
|
||||||
msgstr ""
|
msgid "Manage %s bill lines"
|
||||||
|
msgstr "linea de factura"
|
||||||
|
|
||||||
#: admin.py:176
|
#: admin.py:231
|
||||||
msgid "Bill not in open state."
|
msgid "Bill not in open state."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:179
|
#: admin.py:234
|
||||||
msgid "Not all bills are in open state."
|
msgid "Not all bills are in open state."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:180
|
#: admin.py:235
|
||||||
msgid "Manage bill lines of multiple bills."
|
msgid "Manage bill lines of multiple bills"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:204
|
#: admin.py:250
|
||||||
msgid "Dates"
|
#, python-format
|
||||||
|
msgid "Subtotal %s%% VAT %s &%s;"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:209
|
#: admin.py:251
|
||||||
msgid "Raw"
|
#, python-format
|
||||||
|
msgid "Taxes %s%% VAT %s &%s;"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:235 models.py:73
|
#: admin.py:255 admin.py:381 filters.py:46
|
||||||
msgid "Created"
|
#: templates/bills/microspective.html:123
|
||||||
|
msgid "total"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:236
|
#: admin.py:275
|
||||||
#, fuzzy
|
msgid "This bill has been amended, this value may not be valid."
|
||||||
#| msgid "Close"
|
msgstr ""
|
||||||
msgid "Closed"
|
|
||||||
msgstr "Cerrar"
|
|
||||||
|
|
||||||
#: admin.py:237
|
#: admin.py:280
|
||||||
#, fuzzy
|
msgid "Payment"
|
||||||
#| msgid "updated on"
|
msgstr "Pago"
|
||||||
msgid "Updated"
|
|
||||||
msgstr "actualizada en"
|
|
||||||
|
|
||||||
#: admin.py:246
|
#: admin.py:304
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Amended"
|
#| msgid "Amended"
|
||||||
msgid "Amends"
|
msgid "Amends"
|
||||||
msgstr "Quota rectificativa"
|
msgstr "Quota rectificativa"
|
||||||
|
|
||||||
#: admin.py:252
|
#: admin.py:330
|
||||||
|
msgid "Dates"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin.py:335
|
||||||
|
msgid "Raw"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin.py:358 models.py:75
|
||||||
|
msgid "Created"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin.py:359
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Close"
|
||||||
|
msgid "Closed"
|
||||||
|
msgstr "Cerrar"
|
||||||
|
|
||||||
|
#: admin.py:360
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "updated on"
|
||||||
|
msgid "Updated"
|
||||||
|
msgstr "actualizada en"
|
||||||
|
|
||||||
|
#: admin.py:375
|
||||||
msgid "lines"
|
msgid "lines"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:257 filters.py:46 templates/bills/microspective.html:118
|
#: admin.py:389 models.py:108 models.py:501
|
||||||
msgid "total"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: admin.py:265 models.py:104 models.py:460
|
|
||||||
msgid "type"
|
msgid "type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:282
|
|
||||||
msgid "This bill has been amended, this value may not be valid."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: admin.py:287
|
|
||||||
msgid "Payment"
|
|
||||||
msgstr "Pago"
|
|
||||||
|
|
||||||
#: filters.py:21
|
#: filters.py:21
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: filters.py:22 models.py:88
|
#: filters.py:22 models.py:91
|
||||||
msgid "Invoice"
|
msgid "Invoice"
|
||||||
msgstr "Factura"
|
msgstr "Factura"
|
||||||
|
|
||||||
#: filters.py:23 models.py:90
|
#: filters.py:23 models.py:93
|
||||||
msgid "Fee"
|
msgid "Fee"
|
||||||
msgstr "Cuota de socio"
|
msgstr "Cuota de socio"
|
||||||
|
|
||||||
|
@ -231,65 +256,67 @@ msgstr ""
|
||||||
msgid "Amendment fee"
|
msgid "Amendment fee"
|
||||||
msgstr "Cuota rectificativa"
|
msgstr "Cuota rectificativa"
|
||||||
|
|
||||||
#: filters.py:26 models.py:89
|
#: filters.py:26 models.py:92
|
||||||
msgid "Amendment invoice"
|
msgid "Amendment invoice"
|
||||||
msgstr "Factura rectificativa"
|
msgstr "Factura rectificativa"
|
||||||
|
|
||||||
#: filters.py:68
|
#: filters.py:71
|
||||||
msgid "has bill contact"
|
msgid "has bill contact"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: filters.py:73
|
#: filters.py:76
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: filters.py:74
|
#: filters.py:77
|
||||||
msgid "No"
|
msgid "No"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: filters.py:85
|
#: filters.py:88
|
||||||
msgid "payment state"
|
msgid "payment state"
|
||||||
msgstr "Pago"
|
msgstr "Pago"
|
||||||
|
|
||||||
#: filters.py:90 models.py:72
|
#: filters.py:93 models.py:74
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: filters.py:91 models.py:76
|
#: filters.py:94 models.py:78
|
||||||
msgid "Paid"
|
msgid "Paid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: filters.py:92
|
#: filters.py:95
|
||||||
msgid "Pending"
|
msgid "Pending"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: filters.py:93 models.py:79
|
#: filters.py:96 models.py:81
|
||||||
msgid "Bad debt"
|
msgid "Bad debt"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: filters.py:135
|
#: filters.py:138
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Amended"
|
#| msgid "Amended"
|
||||||
msgid "amended"
|
msgid "amended"
|
||||||
msgstr "Quota rectificativa"
|
msgstr "Quota rectificativa"
|
||||||
|
|
||||||
#: filters.py:140
|
#: filters.py:143
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Due date"
|
#| msgid "Due date"
|
||||||
msgid "Closed amends"
|
msgid "Closed amends"
|
||||||
msgstr "Fecha de pago"
|
msgstr "Fecha de pago"
|
||||||
|
|
||||||
#: filters.py:141
|
#: filters.py:144
|
||||||
msgid "Open or closed amends"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: filters.py:142
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "closed on"
|
#| msgid "Due date"
|
||||||
msgid "No closed amends"
|
msgid "Open amends"
|
||||||
msgstr "cerrada en"
|
msgstr "Fecha de pago"
|
||||||
|
|
||||||
#: filters.py:143
|
#: filters.py:145
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Amended"
|
||||||
|
msgid "Any amends"
|
||||||
|
msgstr "Quota rectificativa"
|
||||||
|
|
||||||
|
#: filters.py:146
|
||||||
msgid "No amends"
|
msgid "No amends"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -309,213 +336,233 @@ msgstr ""
|
||||||
msgid "Source"
|
msgid "Source"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: helpers.py:10
|
#: helpers.py:14
|
||||||
msgid ""
|
msgid ""
|
||||||
"{relation} account \"{account}\" does not have a declared invoice contact. "
|
"{relation} account \"{account}\" does not have a declared invoice contact. "
|
||||||
"You should <a href=\"{url}#invoicecontact-group\">provide one</a>"
|
"You should <a href=\"{url}#invoicecontact-group\">provide one</a>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: helpers.py:17
|
#: helpers.py:21
|
||||||
msgid "Related"
|
msgid "Related"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: helpers.py:24
|
#: helpers.py:28
|
||||||
msgid "Main"
|
msgid "Main"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:24 models.py:100
|
#: models.py:26 models.py:104
|
||||||
msgid "account"
|
msgid "account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:26
|
#: models.py:28
|
||||||
msgid "name"
|
msgid "name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:27
|
#: models.py:29
|
||||||
msgid "Account full name will be used when left blank."
|
msgid "Account full name will be used when left blank."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:28
|
#: models.py:30
|
||||||
msgid "address"
|
msgid "address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:29
|
#: models.py:31
|
||||||
msgid "city"
|
msgid "city"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:31
|
#: models.py:33
|
||||||
msgid "zip code"
|
msgid "zip code"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:32
|
#: models.py:34
|
||||||
msgid "Enter a valid zipcode."
|
msgid "Enter a valid zipcode."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:33
|
#: models.py:35
|
||||||
msgid "country"
|
msgid "country"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:36 templates/admin/bills/bill/report.html:65
|
#: models.py:38 templates/admin/bills/bill/report.html:65
|
||||||
msgid "VAT number"
|
msgid "VAT number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:74
|
#: models.py:76
|
||||||
msgid "Processed"
|
msgid "Processed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:75
|
#: models.py:77
|
||||||
msgid "Amended"
|
msgid "Amended"
|
||||||
msgstr "Quota rectificativa"
|
msgstr "Quota rectificativa"
|
||||||
|
|
||||||
#: models.py:77
|
#: models.py:79
|
||||||
msgid "Incomplete"
|
msgid "Incomplete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:78
|
#: models.py:80
|
||||||
msgid "Executed"
|
msgid "Executed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:91
|
#: models.py:94
|
||||||
msgid "Amendment Fee"
|
msgid "Amendment Fee"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:92
|
#: models.py:95
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Invoice"
|
||||||
|
msgid "Abono Invoice"
|
||||||
|
msgstr "Abono"
|
||||||
|
|
||||||
|
#: models.py:96
|
||||||
msgid "Pro forma"
|
msgid "Pro forma"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:99
|
#: models.py:103
|
||||||
msgid "number"
|
msgid "number"
|
||||||
msgstr "número"
|
msgstr "número"
|
||||||
|
|
||||||
#: models.py:102
|
#: models.py:106
|
||||||
msgid "amend of"
|
msgid "amend of"
|
||||||
msgstr "rectificación de"
|
msgstr "rectificación de"
|
||||||
|
|
||||||
#: models.py:105
|
#: models.py:109
|
||||||
msgid "created on"
|
msgid "created on"
|
||||||
msgstr "creado en"
|
msgstr "creado en"
|
||||||
|
|
||||||
#: models.py:106
|
#: models.py:110
|
||||||
msgid "closed on"
|
msgid "closed on"
|
||||||
msgstr "cerrada en"
|
msgstr "cerrada en"
|
||||||
|
|
||||||
#: models.py:107
|
#: models.py:111
|
||||||
msgid "open"
|
msgid "open"
|
||||||
msgstr "abierta"
|
msgstr "abierta"
|
||||||
|
|
||||||
#: models.py:108
|
#: models.py:112
|
||||||
msgid "sent"
|
msgid "sent"
|
||||||
msgstr "enviada"
|
msgstr "enviada"
|
||||||
|
|
||||||
#: models.py:109
|
#: models.py:113
|
||||||
msgid "due on"
|
msgid "due on"
|
||||||
msgstr "vencimiento"
|
msgstr "vencimiento"
|
||||||
|
|
||||||
#: models.py:110
|
#: models.py:114
|
||||||
msgid "updated on"
|
msgid "updated on"
|
||||||
msgstr "actualizada en"
|
msgstr "actualizada en"
|
||||||
|
|
||||||
#: models.py:112
|
#: models.py:116
|
||||||
msgid "comments"
|
msgid "comments"
|
||||||
msgstr "comentarios"
|
msgstr "comentarios"
|
||||||
|
|
||||||
#: models.py:113
|
#: models.py:117
|
||||||
msgid "HTML"
|
msgid "HTML"
|
||||||
msgstr "HTML"
|
msgstr "HTML"
|
||||||
|
|
||||||
#: models.py:194
|
#: models.py:200
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Type %s is not an amendment."
|
msgid "Type %s is not an amendment."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:196
|
#: models.py:202
|
||||||
msgid "Amend of related account doesn't match bill account."
|
msgid "Amend of related account doesn't match bill account."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:198
|
#: models.py:204
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Selected bills should be in open state"
|
#| msgid "Selected bills should be in open state"
|
||||||
msgid "Related invoice is in open state."
|
msgid "Related invoice is in open state."
|
||||||
msgstr "Las facturas seleccionadas están en estado abierto"
|
msgstr "Las facturas seleccionadas están en estado abierto"
|
||||||
|
|
||||||
#: models.py:200
|
#: models.py:206
|
||||||
msgid "Related invoice is an amendment."
|
msgid "Related invoice is an amendment."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:392
|
#: models.py:419
|
||||||
msgid "bill"
|
msgid "bill"
|
||||||
msgstr "factura"
|
msgstr "factura"
|
||||||
|
|
||||||
#: models.py:393 models.py:458 templates/bills/microspective.html:73
|
#: models.py:420 models.py:499 templates/bills/microspective.html:75
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "descripción"
|
msgstr "descripción"
|
||||||
|
|
||||||
#: models.py:394
|
#: models.py:421
|
||||||
msgid "rate"
|
msgid "rate"
|
||||||
msgstr "tarifa"
|
msgstr "tarifa"
|
||||||
|
|
||||||
#: models.py:395
|
#: models.py:422
|
||||||
msgid "quantity"
|
msgid "quantity"
|
||||||
msgstr "cantidad"
|
msgstr "cantidad"
|
||||||
|
|
||||||
#: models.py:397
|
#: models.py:424
|
||||||
msgid "Verbose quantity"
|
msgid "Verbose quantity"
|
||||||
msgstr "Cantidad"
|
msgstr "Cantidad"
|
||||||
|
|
||||||
#: models.py:398 templates/admin/bills/bill/report.html:47
|
#: models.py:425 templates/admin/bills/bill/report.html:47
|
||||||
#: templates/bills/microspective.html:77
|
#: templates/bills/microspective.html:79
|
||||||
#: templates/bills/microspective.html:111
|
#: templates/bills/microspective.html:116
|
||||||
msgid "subtotal"
|
msgid "subtotal"
|
||||||
msgstr "subtotal"
|
msgstr "subtotal"
|
||||||
|
|
||||||
#: models.py:399
|
#: models.py:426
|
||||||
msgid "tax"
|
msgid "tax"
|
||||||
msgstr "impuesto"
|
msgstr "impuesto"
|
||||||
|
|
||||||
#: models.py:400
|
#: models.py:427
|
||||||
msgid "start"
|
msgid "start"
|
||||||
msgstr "inicio"
|
msgstr "inicio"
|
||||||
|
|
||||||
#: models.py:401
|
#: models.py:428
|
||||||
msgid "end"
|
msgid "end"
|
||||||
msgstr "fín"
|
msgstr "fín"
|
||||||
|
|
||||||
#: models.py:403
|
#: models.py:431
|
||||||
msgid "Informative link back to the order"
|
msgid "Informative link back to the order"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:404
|
#: models.py:432
|
||||||
msgid "order billed"
|
msgid "order billed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:405
|
#: models.py:433
|
||||||
msgid "order billed until"
|
msgid "order billed until"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:406
|
#: models.py:434
|
||||||
msgid "created"
|
msgid "created"
|
||||||
msgstr "creado"
|
msgstr "creado"
|
||||||
|
|
||||||
#: models.py:408
|
#: models.py:436
|
||||||
msgid "amended line"
|
msgid "amended line"
|
||||||
msgstr "linea rectificativa"
|
msgstr "linea rectificativa"
|
||||||
|
|
||||||
#: models.py:451
|
#: models.py:492
|
||||||
msgid "Volume"
|
msgid "Volume"
|
||||||
msgstr "Volumen"
|
msgstr "Volumen"
|
||||||
|
|
||||||
#: models.py:452
|
#: models.py:493
|
||||||
msgid "Compensation"
|
msgid "Compensation"
|
||||||
msgstr "Compensación"
|
msgstr "Compensación"
|
||||||
|
|
||||||
#: models.py:453
|
#: models.py:494
|
||||||
msgid "Other"
|
msgid "Other"
|
||||||
msgstr "Otro"
|
msgstr "Otro"
|
||||||
|
|
||||||
#: models.py:457
|
#: models.py:498
|
||||||
msgid "bill line"
|
msgid "bill line"
|
||||||
msgstr "linea de factura"
|
msgstr "linea de factura"
|
||||||
|
|
||||||
|
#: templates/admin/bills/bill/change_list.html:9
|
||||||
|
msgid "Lines"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/bills/bill/change_list.html:15
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "bill"
|
||||||
|
msgid "Add bill"
|
||||||
|
msgstr "factura"
|
||||||
|
|
||||||
|
#: templates/admin/bills/bill/close_send_download_bills.html:57
|
||||||
|
msgid "Yes, I'm sure"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/bills/bill/report.html:42
|
#: templates/admin/bills/bill/report.html:42
|
||||||
msgid "Summary"
|
msgid "Summary"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -523,19 +570,19 @@ msgstr ""
|
||||||
#: templates/admin/bills/bill/report.html:47
|
#: templates/admin/bills/bill/report.html:47
|
||||||
#: templates/admin/bills/bill/report.html:51
|
#: templates/admin/bills/bill/report.html:51
|
||||||
#: templates/admin/bills/bill/report.html:69
|
#: templates/admin/bills/bill/report.html:69
|
||||||
#: templates/bills/microspective.html:111
|
#: templates/bills/microspective.html:116
|
||||||
#: templates/bills/microspective.html:114
|
#: templates/bills/microspective.html:119
|
||||||
msgid "VAT"
|
msgid "VAT"
|
||||||
msgstr "IVA"
|
msgstr "IVA"
|
||||||
|
|
||||||
#: templates/admin/bills/bill/report.html:51
|
#: templates/admin/bills/bill/report.html:51
|
||||||
#: templates/bills/microspective.html:114
|
#: templates/bills/microspective.html:119
|
||||||
msgid "taxes"
|
msgid "taxes"
|
||||||
msgstr "impuestos"
|
msgstr "impuestos"
|
||||||
|
|
||||||
#: templates/admin/bills/bill/report.html:56
|
#: templates/admin/bills/bill/report.html:56
|
||||||
#: templates/admin/bills/billline/report.html:60
|
#: templates/admin/bills/billline/report.html:60
|
||||||
#: templates/bills/microspective.html:53
|
#: templates/bills/microspective.html:54
|
||||||
msgid "TOTAL"
|
msgid "TOTAL"
|
||||||
msgstr "TOTAL"
|
msgstr "TOTAL"
|
||||||
|
|
||||||
|
@ -553,8 +600,20 @@ msgstr "Fecha de pago"
|
||||||
msgid "Base"
|
msgid "Base"
|
||||||
msgstr "Base"
|
msgstr "Base"
|
||||||
|
|
||||||
|
#: templates/admin/bills/billline/change_list.html:6
|
||||||
|
msgid "Home"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/bills/billline/change_list.html:8
|
||||||
|
msgid "Bills"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/bills/billline/change_list.html:9
|
||||||
|
msgid "Multiple bills"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/bills/billline/report.html:42
|
#: templates/admin/bills/billline/report.html:42
|
||||||
msgid "Services"
|
msgid "Service"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/bills/billline/report.html:43
|
#: templates/admin/bills/billline/report.html:43
|
||||||
|
@ -579,62 +638,56 @@ msgstr "cantidad"
|
||||||
msgid "Profit"
|
msgid "Profit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin/bills/change_list.html:9
|
#: templates/bills/microspective-fee.html:115
|
||||||
#, fuzzy
|
|
||||||
#| msgid "bill"
|
|
||||||
msgid "Add bill"
|
|
||||||
msgstr "factura"
|
|
||||||
|
|
||||||
#: templates/bills/microspective-fee.html:107
|
|
||||||
msgid "Due date"
|
msgid "Due date"
|
||||||
msgstr "Fecha de pago"
|
msgstr "Fecha de pago"
|
||||||
|
|
||||||
#: templates/bills/microspective-fee.html:108
|
#: templates/bills/microspective-fee.html:116
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "On %(bank_account)s"
|
msgid "On %(bank_account)s"
|
||||||
msgstr "En %(bank_account)s"
|
msgstr "En %(bank_account)s"
|
||||||
|
|
||||||
#: templates/bills/microspective-fee.html:114
|
#: templates/bills/microspective-fee.html:122
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "From %(ini)s to %(end)s"
|
msgid "From %(ini)s to %(end)s"
|
||||||
msgstr "Desde %(ini)s hasta %(end)s"
|
msgstr "Desde %(ini)s hasta %(end)s"
|
||||||
|
|
||||||
#: templates/bills/microspective-fee.html:121
|
#: templates/bills/microspective-fee.html:144
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
"<strong>With your membership</strong> you are supporting ...\n"
|
"<strong>With your membership</strong> you are supporting ...\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/bills/microspective.html:49
|
#: templates/bills/microspective.html:50
|
||||||
msgid "DUE DATE"
|
msgid "DUE DATE"
|
||||||
msgstr "VENCIMIENTO"
|
msgstr "VENCIMIENTO"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:57
|
#: templates/bills/microspective.html:58
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(bill_type)s DATE"
|
msgid "%(bill_type)s DATE"
|
||||||
msgstr "FECHA %(bill_type)s"
|
msgstr "FECHA %(bill_type)s"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:74
|
#: templates/bills/microspective.html:76
|
||||||
msgid "period"
|
msgid "period"
|
||||||
msgstr "periodo"
|
msgstr "periodo"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:75
|
#: templates/bills/microspective.html:77
|
||||||
msgid "hrs/qty"
|
msgid "hrs/qty"
|
||||||
msgstr "hrs/cant"
|
msgstr "hrs/cant"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:76
|
#: templates/bills/microspective.html:78
|
||||||
msgid "rate/price"
|
msgid "rate/price"
|
||||||
msgstr "tarifa/precio"
|
msgstr "tarifa/precio"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:131
|
#: templates/bills/microspective.html:137
|
||||||
msgid "COMMENTS"
|
msgid "COMMENTS"
|
||||||
msgstr "COMENTARIOS"
|
msgstr "COMENTARIOS"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:138
|
#: templates/bills/microspective.html:145
|
||||||
msgid "PAYMENT"
|
msgid "PAYMENT"
|
||||||
msgstr "PAGO"
|
msgstr "PAGO"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:142
|
#: templates/bills/microspective.html:149
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
|
@ -648,11 +701,11 @@ msgstr ""
|
||||||
"Puedes pagar esta <i>%(type)s</i> por transferencia bancaria.<br>Incluye tu "
|
"Puedes pagar esta <i>%(type)s</i> por transferencia bancaria.<br>Incluye tu "
|
||||||
"nombre y el número de <i>%(type)s</i>. Nuestra cuenta bancaria es"
|
"nombre y el número de <i>%(type)s</i>. Nuestra cuenta bancaria es"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:151
|
#: templates/bills/microspective.html:160
|
||||||
msgid "QUESTIONS"
|
msgid "QUESTIONS"
|
||||||
msgstr "PREGUNTAS"
|
msgstr "PREGUNTAS"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:152
|
#: templates/bills/microspective.html:161
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
|
@ -668,3 +721,8 @@ msgstr ""
|
||||||
" contacta con nosotros en %(email)s. Te responderemos lo más "
|
" contacta con nosotros en %(email)s. Te responderemos lo más "
|
||||||
"rapidamente posible.\n"
|
"rapidamente posible.\n"
|
||||||
" "
|
" "
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
#~| msgid "closed on"
|
||||||
|
#~ msgid "No closed amends"
|
||||||
|
#~ msgstr "cerrada en"
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,7 +15,7 @@ class Migration(migrations.Migration):
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='bill',
|
model_name='bill',
|
||||||
name='amend_of',
|
name='amend_of',
|
||||||
field=models.ForeignKey(to='bills.Bill', blank=True, related_name='amends', verbose_name='amend of', null=True),
|
field=models.ForeignKey(to='bills.Bill', blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='amends', verbose_name='amend of', null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='billcontact',
|
model_name='billcontact',
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,12 +1,12 @@
|
||||||
import datetime
|
import datetime
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.core.validators import ValidationError, RegexValidator
|
from django.core.validators import ValidationError, RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Sum
|
from django.db.models import F, Sum
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.template import loader, Context
|
from django.template import loader
|
||||||
from django.utils import timezone, translation
|
from django.utils import timezone, translation
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
@ -24,7 +24,7 @@ from . import settings
|
||||||
|
|
||||||
class BillContact(models.Model):
|
class BillContact(models.Model):
|
||||||
account = models.OneToOneField('accounts.Account', verbose_name=_("account"),
|
account = models.OneToOneField('accounts.Account', verbose_name=_("account"),
|
||||||
related_name='billcontact')
|
related_name='billcontact', on_delete=models.CASCADE)
|
||||||
name = models.CharField(_("name"), max_length=256, blank=True,
|
name = models.CharField(_("name"), max_length=256, blank=True,
|
||||||
help_text=_("Account full name will be used when left blank."))
|
help_text=_("Account full name will be used when left blank."))
|
||||||
address = models.TextField(_("address"))
|
address = models.TextField(_("address"))
|
||||||
|
@ -86,11 +86,13 @@ class Bill(models.Model):
|
||||||
FEE = 'FEE'
|
FEE = 'FEE'
|
||||||
AMENDMENTFEE = 'AMENDMENTFEE'
|
AMENDMENTFEE = 'AMENDMENTFEE'
|
||||||
PROFORMA = 'PROFORMA'
|
PROFORMA = 'PROFORMA'
|
||||||
|
ABONOINVOICE = 'ABONOINVOICE'
|
||||||
TYPES = (
|
TYPES = (
|
||||||
(INVOICE, _("Invoice")),
|
(INVOICE, _("Invoice")),
|
||||||
(AMENDMENTINVOICE, _("Amendment invoice")),
|
(AMENDMENTINVOICE, _("Amendment invoice")),
|
||||||
(FEE, _("Fee")),
|
(FEE, _("Fee")),
|
||||||
(AMENDMENTFEE, _("Amendment Fee")),
|
(AMENDMENTFEE, _("Amendment Fee")),
|
||||||
|
(ABONOINVOICE, _("Abono Invoice")),
|
||||||
(PROFORMA, _("Pro forma")),
|
(PROFORMA, _("Pro forma")),
|
||||||
)
|
)
|
||||||
AMEND_MAP = {
|
AMEND_MAP = {
|
||||||
|
@ -100,9 +102,9 @@ class Bill(models.Model):
|
||||||
|
|
||||||
number = models.CharField(_("number"), max_length=16, unique=True, blank=True)
|
number = models.CharField(_("number"), max_length=16, unique=True, blank=True)
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
||||||
related_name='%(class)s')
|
related_name='%(class)s', on_delete=models.CASCADE)
|
||||||
amend_of = models.ForeignKey('self', null=True, blank=True, verbose_name=_("amend of"),
|
amend_of = models.ForeignKey('self', null=True, blank=True, verbose_name=_("amend of"),
|
||||||
related_name='amends')
|
related_name='amends', on_delete=models.SET_NULL)
|
||||||
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
||||||
created_on = models.DateField(_("created on"), auto_now_add=True)
|
created_on = models.DateField(_("created on"), auto_now_add=True)
|
||||||
closed_on = models.DateField(_("closed on"), blank=True, null=True, db_index=True)
|
closed_on = models.DateField(_("closed on"), blank=True, null=True, db_index=True)
|
||||||
|
@ -301,7 +303,7 @@ class Bill(models.Model):
|
||||||
with translation.override(language or self.account.language):
|
with translation.override(language or self.account.language):
|
||||||
if payment is False:
|
if payment is False:
|
||||||
payment = self.account.paymentsources.get_default()
|
payment = self.account.paymentsources.get_default()
|
||||||
context = Context({
|
context = {
|
||||||
'bill': self,
|
'bill': self,
|
||||||
'lines': self.lines.all().prefetch_related('sublines'),
|
'lines': self.lines.all().prefetch_related('sublines'),
|
||||||
'seller': self.seller,
|
'seller': self.seller,
|
||||||
|
@ -316,7 +318,7 @@ class Bill(models.Model):
|
||||||
'payment': payment and payment.get_bill_context(),
|
'payment': payment and payment.get_bill_context(),
|
||||||
'default_due_date': self.get_due_date(payment=payment),
|
'default_due_date': self.get_due_date(payment=payment),
|
||||||
'now': timezone.now(),
|
'now': timezone.now(),
|
||||||
})
|
}
|
||||||
template_name = 'BILLS_%s_TEMPLATE' % self.get_type()
|
template_name = 'BILLS_%s_TEMPLATE' % self.get_type()
|
||||||
template = getattr(settings, template_name, settings.BILLS_DEFAULT_TEMPLATE)
|
template = getattr(settings, template_name, settings.BILLS_DEFAULT_TEMPLATE)
|
||||||
bill_template = loader.get_template(template)
|
bill_template = loader.get_template(template)
|
||||||
|
@ -392,6 +394,11 @@ class AmendmentInvoice(Bill):
|
||||||
proxy = True
|
proxy = True
|
||||||
|
|
||||||
|
|
||||||
|
class AbonoInvoice(Bill):
|
||||||
|
class Meta:
|
||||||
|
proxy = True
|
||||||
|
|
||||||
|
|
||||||
class Fee(Bill):
|
class Fee(Bill):
|
||||||
class Meta:
|
class Meta:
|
||||||
proxy = True
|
proxy = True
|
||||||
|
@ -409,7 +416,7 @@ class ProForma(Bill):
|
||||||
|
|
||||||
class BillLine(models.Model):
|
class BillLine(models.Model):
|
||||||
""" Base model for bill item representation """
|
""" Base model for bill item representation """
|
||||||
bill = models.ForeignKey(Bill, verbose_name=_("bill"), related_name='lines')
|
bill = models.ForeignKey(Bill, verbose_name=_("bill"), related_name='lines', on_delete=models.CASCADE)
|
||||||
description = models.CharField(_("description"), max_length=256)
|
description = models.CharField(_("description"), max_length=256)
|
||||||
rate = models.DecimalField(_("rate"), blank=True, null=True, max_digits=12, decimal_places=2)
|
rate = models.DecimalField(_("rate"), blank=True, null=True, max_digits=12, decimal_places=2)
|
||||||
quantity = models.DecimalField(_("quantity"), blank=True, null=True, max_digits=12,
|
quantity = models.DecimalField(_("quantity"), blank=True, null=True, max_digits=12,
|
||||||
|
@ -427,7 +434,7 @@ class BillLine(models.Model):
|
||||||
created_on = models.DateField(_("created"), auto_now_add=True)
|
created_on = models.DateField(_("created"), auto_now_add=True)
|
||||||
# Amendment
|
# Amendment
|
||||||
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
|
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
|
||||||
related_name='amendment_lines', null=True, blank=True)
|
related_name='amendment_lines', null=True, blank=True, on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
get_latest_by = 'id'
|
get_latest_by = 'id'
|
||||||
|
@ -488,7 +495,7 @@ class BillSubline(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: order info for undoing
|
# TODO: order info for undoing
|
||||||
line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines')
|
line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines', on_delete=models.CASCADE)
|
||||||
description = models.CharField(_("description"), max_length=256)
|
description = models.CharField(_("description"), max_length=256)
|
||||||
total = models.DecimalField(max_digits=12, decimal_places=2)
|
total = models.DecimalField(max_digits=12, decimal_places=2)
|
||||||
type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)
|
type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)
|
||||||
|
|
|
@ -18,6 +18,9 @@ BILLS_AMENDMENT_INVOICE_NUMBER_PREFIX = Setting('BILLS_AMENDMENT_INVOICE_NUMBER_
|
||||||
'A'
|
'A'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
BILLS_ABONOINVOICE_NUMBER_PREFIX = Setting('BILLS_ABONOINVOICE_NUMBER_PREFIX',
|
||||||
|
'AB'
|
||||||
|
)
|
||||||
|
|
||||||
BILLS_FEE_NUMBER_PREFIX = Setting('BILLS_FEE_NUMBER_PREFIX',
|
BILLS_FEE_NUMBER_PREFIX = Setting('BILLS_FEE_NUMBER_PREFIX',
|
||||||
'F'
|
'F'
|
||||||
|
|
|
@ -282,3 +282,17 @@ a:hover {
|
||||||
#questions {
|
#questions {
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#watermark {
|
||||||
|
color: #d0d0d0;
|
||||||
|
font-size: 100pt;
|
||||||
|
-webkit-transform: rotate(-45deg);
|
||||||
|
-moz-transform: rotate(-45deg);
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
z-index: -1;
|
||||||
|
max-width: 593px;
|
||||||
|
}
|
|
@ -12,6 +12,12 @@
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
{% if bill.is_open %}
|
||||||
|
<!-- TODO DANIEL: falta arreglar el css d'aquesta cosa -->
|
||||||
|
<div id="watermark">
|
||||||
|
<p>ESBORRANY - DRAFT - BORRADOR</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<div id="logo">
|
<div id="logo">
|
||||||
{% block logo %}
|
{% block logo %}
|
||||||
|
|
|
@ -7,7 +7,7 @@ from orchestra.admin.actions import SendEmail
|
||||||
from orchestra.admin.utils import insertattr, change_url
|
from orchestra.admin.utils import insertattr, change_url
|
||||||
from orchestra.contrib.accounts.actions import list_accounts
|
from orchestra.contrib.accounts.actions import list_accounts
|
||||||
from orchestra.contrib.accounts.admin import AccountAdmin, AccountAdminMixin
|
from orchestra.contrib.accounts.admin import AccountAdmin, AccountAdminMixin
|
||||||
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
|
from orchestra.forms.widgets import PaddingCheckboxSelectMultiple
|
||||||
|
|
||||||
from .filters import EmailUsageListFilter
|
from .filters import EmailUsageListFilter
|
||||||
from .models import Contact
|
from .models import Contact
|
||||||
|
@ -72,7 +72,7 @@ class ContactAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
if db_field.name == 'address':
|
if db_field.name == 'address':
|
||||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||||
if db_field.name == 'email_usage':
|
if db_field.name == 'email_usage':
|
||||||
kwargs['widget'] = paddingCheckboxSelectMultiple(130)
|
kwargs['widget'] = PaddingCheckboxSelectMultiple(130)
|
||||||
return super(ContactAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(ContactAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ class ContactInline(admin.StackedInline):
|
||||||
if db_field.name == 'address':
|
if db_field.name == 'address':
|
||||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||||
if db_field.name == 'email_usage':
|
if db_field.name == 'email_usage':
|
||||||
kwargs['widget'] = paddingCheckboxSelectMultiple(45)
|
kwargs['widget'] = PaddingCheckboxSelectMultiple(45)
|
||||||
return super(ContactInline, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(ContactInline, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -33,7 +33,7 @@ class Contact(models.Model):
|
||||||
objects = ContactQuerySet.as_manager()
|
objects = ContactQuerySet.as_manager()
|
||||||
|
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||||
related_name='contacts', null=True)
|
related_name='contacts', null=True, on_delete=models.SET_NULL)
|
||||||
short_name = models.CharField(_("short name"), max_length=128)
|
short_name = models.CharField(_("short name"), max_length=128)
|
||||||
full_name = models.CharField(_("full name"), max_length=256, blank=True)
|
full_name = models.CharField(_("full name"), max_length=256, blank=True)
|
||||||
email = models.EmailField()
|
email = models.EmailField()
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
||||||
|
@ -12,6 +14,10 @@ from .filters import HasUserListFilter, HasDatabaseListFilter
|
||||||
from .forms import DatabaseCreationForm, DatabaseUserChangeForm, DatabaseUserCreationForm
|
from .forms import DatabaseCreationForm, DatabaseUserChangeForm, DatabaseUserCreationForm
|
||||||
from .models import Database, DatabaseUser
|
from .models import Database, DatabaseUser
|
||||||
|
|
||||||
|
def save_selected(modeladmin, request, queryset):
|
||||||
|
for selected in queryset:
|
||||||
|
selected.save()
|
||||||
|
save_selected.short_description = "Re-save selected objects"
|
||||||
|
|
||||||
class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
list_display = ('name', 'type', 'display_users', 'account_link')
|
list_display = ('name', 'type', 'display_users', 'account_link')
|
||||||
|
@ -22,7 +28,7 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {
|
(None, {
|
||||||
'classes': ('extrapretty',),
|
'classes': ('extrapretty',),
|
||||||
'fields': ('account_link', 'name', 'type', 'users', 'display_users'),
|
'fields': ('account_link', 'name', 'type', 'users', 'display_users', 'comments'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
add_fieldsets = (
|
add_fieldsets = (
|
||||||
|
@ -44,16 +50,16 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
filter_horizontal = ['users']
|
filter_horizontal = ['users']
|
||||||
filter_by_account_fields = ('users',)
|
filter_by_account_fields = ('users',)
|
||||||
list_prefetch_related = ('users',)
|
list_prefetch_related = ('users',)
|
||||||
actions = (list_accounts,)
|
actions = (list_accounts, save_selected)
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_users(self, db):
|
def display_users(self, db):
|
||||||
links = []
|
links = []
|
||||||
for user in db.users.all():
|
for user in db.users.all():
|
||||||
link = '<a href="%s">%s</a>' % (change_url(user), user.username)
|
link = format_html('<a href="{}">{}</a>', change_url(user), user.username)
|
||||||
links.append(link)
|
links.append(link)
|
||||||
return '<br>'.join(links)
|
return '<br>'.join(links)
|
||||||
display_users.short_description = _("Users")
|
display_users.short_description = _("Users")
|
||||||
display_users.allow_tags = True
|
|
||||||
display_users.admin_order_field = 'users__username'
|
display_users.admin_order_field = 'users__username'
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
|
@ -93,16 +99,16 @@ class DatabaseUserAdmin(SelectAccountAdminMixin, ChangePasswordAdminMixin, Exten
|
||||||
readonly_fields = ('account_link', 'display_databases',)
|
readonly_fields = ('account_link', 'display_databases',)
|
||||||
filter_by_account_fields = ('databases',)
|
filter_by_account_fields = ('databases',)
|
||||||
list_prefetch_related = ('databases',)
|
list_prefetch_related = ('databases',)
|
||||||
actions = (list_accounts,)
|
actions = (list_accounts, save_selected)
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_databases(self, user):
|
def display_databases(self, user):
|
||||||
links = []
|
links = []
|
||||||
for db in user.databases.all():
|
for db in user.databases.all():
|
||||||
link = '<a href="%s">%s</a>' % (change_url(db), db.name)
|
link = format_html('<a href="{}">{}</a>', change_url(db), db.name)
|
||||||
links.append(link)
|
links.append(link)
|
||||||
return '<br>'.join(links)
|
return '<br>'.join(links)
|
||||||
display_databases.short_description = _("Databases")
|
display_databases.short_description = _("Databases")
|
||||||
display_databases.allow_tags = True
|
|
||||||
display_databases.admin_order_field = 'databases__name'
|
display_databases.admin_order_field = 'databases__name'
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
|
|
|
@ -91,7 +91,7 @@ class DatabaseCreationForm(DatabaseUserCreationForm):
|
||||||
|
|
||||||
class ReadOnlySQLPasswordHashField(ReadOnlyPasswordHashField):
|
class ReadOnlySQLPasswordHashField(ReadOnlyPasswordHashField):
|
||||||
class ReadOnlyPasswordHashWidget(forms.Widget):
|
class ReadOnlyPasswordHashWidget(forms.Widget):
|
||||||
def render(self, name, value, attrs):
|
def render(self, name, value, attrs, renderer=None):
|
||||||
original = ReadOnlyPasswordHashField.widget().render(name, value, attrs)
|
original = ReadOnlyPasswordHashField.widget().render(name, value, attrs)
|
||||||
if 'Invalid' not in original:
|
if 'Invalid' not in original:
|
||||||
return original
|
return original
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import django.db.models.deletion
|
||||||
import orchestra.core.validators
|
import orchestra.core.validators
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,7 +20,7 @@ class Migration(migrations.Migration):
|
||||||
('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
|
('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
|
||||||
('name', models.CharField(verbose_name='name', max_length=64, validators=[orchestra.core.validators.validate_name])),
|
('name', models.CharField(verbose_name='name', max_length=64, validators=[orchestra.core.validators.validate_name])),
|
||||||
('type', models.CharField(default='mysql', choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], verbose_name='type', max_length=32)),
|
('type', models.CharField(default='mysql', choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], verbose_name='type', max_length=32)),
|
||||||
('account', models.ForeignKey(related_name='databases', verbose_name='Account', to=settings.AUTH_USER_MODEL)),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databases', verbose_name='Account', to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
|
@ -29,7 +30,7 @@ class Migration(migrations.Migration):
|
||||||
('username', models.CharField(verbose_name='username', max_length=16, validators=[orchestra.core.validators.validate_name])),
|
('username', models.CharField(verbose_name='username', max_length=16, validators=[orchestra.core.validators.validate_name])),
|
||||||
('password', models.CharField(verbose_name='password', max_length=256)),
|
('password', models.CharField(verbose_name='password', max_length=256)),
|
||||||
('type', models.CharField(default='mysql', choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], verbose_name='type', max_length=32)),
|
('type', models.CharField(default='mysql', choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], verbose_name='type', max_length=32)),
|
||||||
('account', models.ForeignKey(related_name='databaseusers', verbose_name='Account', to=settings.AUTH_USER_MODEL)),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databaseusers', verbose_name='Account', to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name_plural': 'DB users',
|
'verbose_name_plural': 'DB users',
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:25
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import orchestra.core.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('databases', '0001_initial'), ('databases', '0002_auto_20170528_2005'), ('databases', '0003_database_comments'), ('databases', '0004_auto_20210330_1049')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Database',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=64, validators=[orchestra.core.validators.validate_name], verbose_name='name')),
|
||||||
|
('type', models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type')),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databases', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DatabaseUser',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('username', models.CharField(max_length=16, validators=[orchestra.core.validators.validate_name], verbose_name='username')),
|
||||||
|
('password', models.CharField(max_length=256, verbose_name='password')),
|
||||||
|
('type', models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type')),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databaseusers', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'DB users',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='database',
|
||||||
|
name='users',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='databases', to='databases.DatabaseUser', verbose_name='users'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='databaseuser',
|
||||||
|
unique_together=set([('username', 'type')]),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='database',
|
||||||
|
unique_together=set([('name', 'type')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='database',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='databaseuser',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='database',
|
||||||
|
name='comments',
|
||||||
|
field=models.TextField(blank=True, default=''),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='database',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='databaseuser',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2017-05-28 18:05
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('databases', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='database',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='databaseuser',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2020-02-04 11:21
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('databases', '0002_auto_20170528_2005'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='database',
|
||||||
|
name='comments',
|
||||||
|
field=models.TextField(default=''),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,30 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-03-30 10:49
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('databases', '0003_database_comments'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='database',
|
||||||
|
name='comments',
|
||||||
|
field=models.TextField(blank=True, default=''),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='database',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='databaseuser',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -20,8 +20,9 @@ class Database(models.Model):
|
||||||
type = models.CharField(_("type"), max_length=32,
|
type = models.CharField(_("type"), max_length=32,
|
||||||
choices=settings.DATABASES_TYPE_CHOICES,
|
choices=settings.DATABASES_TYPE_CHOICES,
|
||||||
default=settings.DATABASES_DEFAULT_TYPE)
|
default=settings.DATABASES_DEFAULT_TYPE)
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
|
||||||
related_name='databases')
|
verbose_name=_("Account"), related_name='databases')
|
||||||
|
comments = models.TextField(default="", blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('name', 'type')
|
unique_together = ('name', 'type')
|
||||||
|
@ -59,8 +60,8 @@ class DatabaseUser(models.Model):
|
||||||
type = models.CharField(_("type"), max_length=32,
|
type = models.CharField(_("type"), max_length=32,
|
||||||
choices=settings.DATABASES_TYPE_CHOICES,
|
choices=settings.DATABASES_TYPE_CHOICES,
|
||||||
default=settings.DATABASES_DEFAULT_TYPE)
|
default=settings.DATABASES_DEFAULT_TYPE)
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
|
||||||
related_name='databaseusers')
|
verbose_name=_("Account"), related_name='databaseusers')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = _("DB users")
|
verbose_name_plural = _("DB users")
|
||||||
|
|
|
@ -20,7 +20,7 @@ DATABASES_DEFAULT_TYPE = Setting('DATABASES_DEFAULT_TYPE',
|
||||||
|
|
||||||
DATABASES_DEFAULT_HOST = Setting('DATABASES_DEFAULT_HOST',
|
DATABASES_DEFAULT_HOST = Setting('DATABASES_DEFAULT_HOST',
|
||||||
'localhost',
|
'localhost',
|
||||||
validators=[validate_hostname],
|
# validators=[validate_hostname],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
import MySQLdb
|
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import MySQLdb
|
||||||
from django.conf import settings as djsettings
|
from django.conf import settings as djsettings
|
||||||
from django.core.management.base import CommandError
|
from django.core.management.base import CommandError
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from selenium.webdriver.support.select import Select
|
|
||||||
|
|
||||||
from orchestra.admin.utils import change_url
|
from orchestra.admin.utils import change_url
|
||||||
from orchestra.contrib.orchestration.models import Server, Route
|
from orchestra.contrib.orchestration.models import Route, Server
|
||||||
from orchestra.utils.sys import sshrun
|
from orchestra.utils.sys import sshrun
|
||||||
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii, save_response_on_error,
|
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii,
|
||||||
snapshot_on_error)
|
save_response_on_error, snapshot_on_error)
|
||||||
|
from selenium.webdriver.support.select import Select
|
||||||
|
|
||||||
from ... import backends, settings
|
from ... import backends, settings
|
||||||
from ...models import Database, DatabaseUser
|
from ...models import Database, DatabaseUser
|
||||||
|
|
||||||
|
TEST_REST_API = int(os.getenv('TEST_REST_API', '0'))
|
||||||
|
|
||||||
|
|
||||||
class DatabaseTestMixin(object):
|
class DatabaseTestMixin(object):
|
||||||
MASTER_SERVER = os.environ.get('ORCHESTRA_SECOND_SERVER', 'localhost')
|
MASTER_SERVER = os.environ.get('ORCHESTRA_SECOND_SERVER', 'localhost')
|
||||||
|
@ -181,6 +183,7 @@ class MySQLControllerMixin(object):
|
||||||
"""mysql mysql -e 'SELECT * FROM user WHERE user="%(username)s";'""" % context, display=False).stdout)
|
"""mysql mysql -e 'SELECT * FROM user WHERE user="%(username)s";'""" % context, display=False).stdout)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_REST_API, "REST API tests")
|
||||||
class RESTDatabaseMixin(DatabaseTestMixin):
|
class RESTDatabaseMixin(DatabaseTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(RESTDatabaseMixin, self).setUp()
|
super(RESTDatabaseMixin, self).setUp()
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.functions import Concat, Coalesce
|
from django.db.models.functions import Concat, Coalesce
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
from orchestra.admin import ExtendedModelAdmin
|
||||||
|
@ -55,7 +57,7 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
'structured_name', 'display_is_top', 'display_websites', 'display_addresses', 'account_link'
|
'structured_name', 'display_is_top', 'display_websites', 'display_addresses', 'account_link'
|
||||||
)
|
)
|
||||||
add_fields = ('name', 'account')
|
add_fields = ('name', 'account')
|
||||||
fields = ('name', 'account_link', 'display_websites', 'display_addresses')
|
fields = ('name', 'account_link', 'display_websites', 'display_addresses', 'dns2136_address_match_list')
|
||||||
readonly_fields = (
|
readonly_fields = (
|
||||||
'account_link', 'top_link', 'display_websites', 'display_addresses', 'implicit_records'
|
'account_link', 'top_link', 'display_websites', 'display_addresses', 'implicit_records'
|
||||||
)
|
)
|
||||||
|
@ -72,9 +74,8 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
def structured_name(self, domain):
|
def structured_name(self, domain):
|
||||||
if domain.is_top:
|
if domain.is_top:
|
||||||
return domain.name
|
return domain.name
|
||||||
return ' '*4 + domain.name
|
return mark_safe(' '*4 + domain.name)
|
||||||
structured_name.short_description = _("name")
|
structured_name.short_description = _("name")
|
||||||
structured_name.allow_tags = True
|
|
||||||
structured_name.admin_order_field = 'structured_name'
|
structured_name.admin_order_field = 'structured_name'
|
||||||
|
|
||||||
def display_is_top(self, domain):
|
def display_is_top(self, domain):
|
||||||
|
@ -83,6 +84,7 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
display_is_top.boolean = True
|
display_is_top.boolean = True
|
||||||
display_is_top.admin_order_field = 'top'
|
display_is_top.admin_order_field = 'top'
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_websites(self, domain):
|
def display_websites(self, domain):
|
||||||
if apps.isinstalled('orchestra.contrib.websites'):
|
if apps.isinstalled('orchestra.contrib.websites'):
|
||||||
websites = domain.websites.all()
|
websites = domain.websites.all()
|
||||||
|
@ -92,22 +94,22 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
site_link = get_on_site_link(website.get_absolute_url())
|
site_link = get_on_site_link(website.get_absolute_url())
|
||||||
admin_url = change_url(website)
|
admin_url = change_url(website)
|
||||||
title = _("Edit website")
|
title = _("Edit website")
|
||||||
link = '<a href="%s" title="%s">%s %s</a>' % (
|
link = format_html('<a href="{}" title="{}">{} {}</a>',
|
||||||
admin_url, title, website.name, site_link)
|
admin_url, title, website.name, site_link)
|
||||||
links.append(link)
|
links.append(link)
|
||||||
return '<br>'.join(links)
|
return '<br>'.join(links)
|
||||||
add_url = reverse('admin:websites_website_add')
|
add_url = reverse('admin:websites_website_add')
|
||||||
add_url += '?account=%i&domains=%i' % (domain.account_id, domain.pk)
|
add_url += '?account=%i&domains=%i' % (domain.account_id, domain.pk)
|
||||||
image = '<img src="%s"></img>' % static('orchestra/images/add.png')
|
add_link = format_html(
|
||||||
add_link = '<a href="%s" title="%s">%s</a>' % (
|
'<a href="{}" title="{}"><img src="{}" /></a>', add_url,
|
||||||
add_url, _("Add website"), image
|
_("Add website"), static('orchestra/images/add.png'),
|
||||||
)
|
)
|
||||||
return _("No website %s") % (add_link)
|
return _("No website %s") % (add_link)
|
||||||
return '---'
|
return '---'
|
||||||
display_websites.admin_order_field = 'websites__name'
|
display_websites.admin_order_field = 'websites__name'
|
||||||
display_websites.short_description = _("Websites")
|
display_websites.short_description = _("Websites")
|
||||||
display_websites.allow_tags = True
|
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_addresses(self, domain):
|
def display_addresses(self, domain):
|
||||||
if apps.isinstalled('orchestra.contrib.mailboxes'):
|
if apps.isinstalled('orchestra.contrib.mailboxes'):
|
||||||
add_url = reverse('admin:mailboxes_address_add')
|
add_url = reverse('admin:mailboxes_address_add')
|
||||||
|
@ -126,10 +128,9 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
return '---'
|
return '---'
|
||||||
display_addresses.short_description = _("Addresses")
|
display_addresses.short_description = _("Addresses")
|
||||||
display_addresses.admin_order_field = 'addresses__count'
|
display_addresses.admin_order_field = 'addresses__count'
|
||||||
display_addresses.allow_tags = True
|
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def implicit_records(self, domain):
|
def implicit_records(self, domain):
|
||||||
defaults = []
|
|
||||||
types = set(domain.records.values_list('type', flat=True))
|
types = set(domain.records.values_list('type', flat=True))
|
||||||
ttl = settings.DOMAINS_DEFAULT_TTL
|
ttl = settings.DOMAINS_DEFAULT_TTL
|
||||||
lines = []
|
lines = []
|
||||||
|
@ -141,14 +142,13 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
value=record.value
|
value=record.value
|
||||||
)
|
)
|
||||||
if not domain.record_is_implicit(record, types):
|
if not domain.record_is_implicit(record, types):
|
||||||
line = '<strike>%s</strike>' % line
|
line = format_html('<strike>{}</strike>', line)
|
||||||
if record.type is Record.SOA:
|
if record.type is Record.SOA:
|
||||||
lines.insert(0, line)
|
lines.insert(0, line)
|
||||||
else:
|
else:
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
return '<br>'.join(lines)
|
return '<br>'.join(lines)
|
||||||
implicit_records.short_description = _("Implicit records")
|
implicit_records.short_description = _("Implicit records")
|
||||||
implicit_records.allow_tags = True
|
|
||||||
|
|
||||||
def get_fieldsets(self, request, obj=None):
|
def get_fieldsets(self, request, obj=None):
|
||||||
""" Add SOA fields when domain is top """
|
""" Add SOA fields when domain is top """
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from orchestra.api import router
|
from orchestra.api import router
|
||||||
|
@ -19,7 +19,7 @@ class DomainViewSet(AccountApiMixin, viewsets.ModelViewSet):
|
||||||
qs = super(DomainViewSet, self).get_queryset()
|
qs = super(DomainViewSet, self).get_queryset()
|
||||||
return qs.prefetch_related('records')
|
return qs.prefetch_related('records')
|
||||||
|
|
||||||
@detail_route()
|
@action(detail=True)
|
||||||
def view_zone(self, request, pk=None):
|
def view_zone(self, request, pk=None):
|
||||||
domain = self.get_object()
|
domain = self.get_object()
|
||||||
return Response({
|
return Response({
|
||||||
|
|
|
@ -102,7 +102,7 @@ class Bind9MasterDomainController(ServiceController):
|
||||||
self.append(textwrap.dedent("""
|
self.append(textwrap.dedent("""
|
||||||
# Apply changes
|
# Apply changes
|
||||||
if [[ $UPDATED == 1 ]]; then
|
if [[ $UPDATED == 1 ]]; then
|
||||||
service bind9 reload
|
rm /etc/bind/master/*jnl || true; service bind9 restart
|
||||||
fi""")
|
fi""")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -158,6 +158,7 @@ class Bind9MasterDomainController(ServiceController):
|
||||||
'slaves': '; '.join(slaves) or 'none',
|
'slaves': '; '.join(slaves) or 'none',
|
||||||
'also_notify': '; '.join(slaves) + ';' if slaves else '',
|
'also_notify': '; '.join(slaves) + ';' if slaves else '',
|
||||||
'conf_path': self.CONF_PATH,
|
'conf_path': self.CONF_PATH,
|
||||||
|
'dns2136_address_match_list': domain.dns2136_address_match_list
|
||||||
}
|
}
|
||||||
context['conf'] = textwrap.dedent("""\
|
context['conf'] = textwrap.dedent("""\
|
||||||
zone "%(name)s" {
|
zone "%(name)s" {
|
||||||
|
@ -166,6 +167,7 @@ class Bind9MasterDomainController(ServiceController):
|
||||||
file "%(zone_path)s";
|
file "%(zone_path)s";
|
||||||
allow-transfer { %(slaves)s; };
|
allow-transfer { %(slaves)s; };
|
||||||
also-notify { %(also_notify)s };
|
also-notify { %(also_notify)s };
|
||||||
|
allow-update { %(dns2136_address_match_list)s };
|
||||||
notify yes;
|
notify yes;
|
||||||
};""") % context
|
};""") % context
|
||||||
return context
|
return context
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
import django.db.models.deletion
|
||||||
import orchestra.contrib.domains.utils
|
import orchestra.contrib.domains.utils
|
||||||
import orchestra.contrib.domains.validators
|
import orchestra.contrib.domains.validators
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -20,8 +21,8 @@ class Migration(migrations.Migration):
|
||||||
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')),
|
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')),
|
||||||
('name', models.CharField(unique=True, max_length=256, validators=[orchestra.contrib.domains.validators.validate_domain_name, orchestra.contrib.domains.validators.validate_allowed_domain], verbose_name='name', help_text='Domain or subdomain name.')),
|
('name', models.CharField(unique=True, max_length=256, validators=[orchestra.contrib.domains.validators.validate_domain_name, orchestra.contrib.domains.validators.validate_allowed_domain], verbose_name='name', help_text='Domain or subdomain name.')),
|
||||||
('serial', models.IntegerField(default=orchestra.contrib.domains.utils.generate_zone_serial, verbose_name='serial', help_text='Serial number')),
|
('serial', models.IntegerField(default=orchestra.contrib.domains.utils.generate_zone_serial, verbose_name='serial', help_text='Serial number')),
|
||||||
('account', models.ForeignKey(related_name='domains', help_text='Automatically selected for subdomains.', to=settings.AUTH_USER_MODEL, verbose_name='Account', blank=True)),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='domains', help_text='Automatically selected for subdomains.', to=settings.AUTH_USER_MODEL, verbose_name='Account', blank=True)),
|
||||||
('top', models.ForeignKey(null=True, to='domains.Domain', editable=False, related_name='subdomain_set')),
|
('top', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, null=True, to='domains.Domain', editable=False, related_name='subdomain_set')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
|
@ -31,7 +32,7 @@ class Migration(migrations.Migration):
|
||||||
('ttl', models.CharField(help_text='Record TTL, defaults to 1h', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL', blank=True)),
|
('ttl', models.CharField(help_text='Record TTL, defaults to 1h', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL', blank=True)),
|
||||||
('type', models.CharField(max_length=32, verbose_name='type', choices=[('MX', 'MX'), ('NS', 'NS'), ('CNAME', 'CNAME'), ('A', 'A (IPv4 address)'), ('AAAA', 'AAAA (IPv6 address)'), ('SRV', 'SRV'), ('TXT', 'TXT'), ('SOA', 'SOA')])),
|
('type', models.CharField(max_length=32, verbose_name='type', choices=[('MX', 'MX'), ('NS', 'NS'), ('CNAME', 'CNAME'), ('A', 'A (IPv4 address)'), ('AAAA', 'AAAA (IPv6 address)'), ('SRV', 'SRV'), ('TXT', 'TXT'), ('SOA', 'SOA')])),
|
||||||
('value', models.CharField(max_length=256, verbose_name='value')),
|
('value', models.CharField(max_length=256, verbose_name='value')),
|
||||||
('domain', models.ForeignKey(related_name='records', to='domains.Domain', verbose_name='domain')),
|
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='domains.Domain', verbose_name='domain')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:27
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import orchestra.contrib.domains.utils
|
||||||
|
import orchestra.contrib.domains.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('domains', '0001_initial'), ('domains', '0002_auto_20150715_1017'), ('domains', '0003_auto_20150720_1121'), ('domains', '0004_auto_20150720_1121'), ('domains', '0005_auto_20160219_1034'), ('domains', '0006_auto_20170528_2011'), ('domains', '0007_auto_20190805_1134'), ('domains', '0008_domain_dns2136_address_match_list'), ('domains', '0009_auto_20200204_1217'), ('domains', '0010_auto_20210330_1049')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Domain',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text='Domain or subdomain name.', max_length=256, unique=True, validators=[orchestra.contrib.domains.validators.validate_domain_name, orchestra.contrib.domains.validators.validate_allowed_domain], verbose_name='name')),
|
||||||
|
('serial', models.IntegerField(default=orchestra.contrib.domains.utils.generate_zone_serial, help_text='Serial number', verbose_name='serial')),
|
||||||
|
('account', models.ForeignKey(blank=True, help_text='Automatically selected for subdomains.', on_delete=django.db.models.deletion.CASCADE, related_name='domains', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
|
('top', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subdomain_set', to='domains.Domain')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Record',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('ttl', models.CharField(blank=True, help_text='Record TTL, defaults to 1h', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL')),
|
||||||
|
('type', models.CharField(choices=[('MX', 'MX'), ('NS', 'NS'), ('CNAME', 'CNAME'), ('A', 'A (IPv4 address)'), ('AAAA', 'AAAA (IPv6 address)'), ('SRV', 'SRV'), ('TXT', 'TXT'), ('SPF', 'SPF')], max_length=32, verbose_name='type')),
|
||||||
|
('value', models.CharField(help_text='MX, NS and CNAME records sould end with a dot.', max_length=1024, verbose_name='value')),
|
||||||
|
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='domains.Domain', verbose_name='domain')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='domain',
|
||||||
|
name='serial',
|
||||||
|
field=models.IntegerField(default=orchestra.contrib.domains.utils.generate_zone_serial, editable=False, help_text='A revision number that changes whenever this domain is updated.', verbose_name='serial'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='domain',
|
||||||
|
name='expire',
|
||||||
|
field=models.CharField(blank=True, help_text='The time that a secondary server will keep trying to complete a zone transfer. If this time expires prior to a successful zone transfer, the secondary server will expire its zone file. This means the secondary will stop answering queries. The default value is <tt>4w</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='expire'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='domain',
|
||||||
|
name='min_ttl',
|
||||||
|
field=models.CharField(blank=True, help_text='The minimum time-to-live value applies to all resource records in the zone file. This value is supplied in query responses to inform other servers how long they should keep the data in cache. The default value is <tt>1h</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='min TTL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='domain',
|
||||||
|
name='refresh',
|
||||||
|
field=models.CharField(blank=True, help_text="The time a secondary DNS server waits before querying the primary DNS server's SOA record to check for changes. When the refresh time expires, the secondary DNS server requests a copy of the current SOA record from the primary. The primary DNS server complies with this request. The secondary DNS server compares the serial number of the primary DNS server's current SOA record and the serial number in it's own SOA record. If they are different, the secondary DNS server will request a zone transfer from the primary DNS server. The default value is <tt>1d</tt>.", max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='refresh'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='domain',
|
||||||
|
name='retry',
|
||||||
|
field=models.CharField(blank=True, help_text='The time a secondary server waits before retrying a failed zone transfer. Normally, the retry time is less than the refresh time. The default value is <tt>2h</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='retry'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='domain',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_index=True, help_text='Domain or subdomain name.', max_length=256, unique=True, validators=[orchestra.contrib.domains.validators.validate_domain_name, orchestra.contrib.domains.validators.validate_allowed_domain], verbose_name='name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='domain',
|
||||||
|
name='top',
|
||||||
|
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subdomain_set', to='domains.Domain', verbose_name='top domain'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='domain',
|
||||||
|
name='dns2136_address_match_list',
|
||||||
|
field=models.CharField(blank=True, default='key pangea.key;', help_text="A bind-9 'address_match_list' that will be granted permission to perform dns2136 updates. Chiefly used to enable Let's Encrypt self-service validation.", max_length=80),
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
import orchestra.contrib.domains.validators
|
import orchestra.contrib.domains.validators
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@ class Migration(migrations.Migration):
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='domain',
|
model_name='domain',
|
||||||
name='top',
|
name='top',
|
||||||
field=models.ForeignKey(editable=False, verbose_name='top domain', related_name='subdomain_set', to='domains.Domain', null=True),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, editable=False, verbose_name='top domain', related_name='subdomain_set', to='domains.Domain', null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='record',
|
model_name='record',
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2017-05-28 18:11
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import orchestra.contrib.domains.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('domains', '0005_auto_20160219_1034'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='domain',
|
||||||
|
name='min_ttl',
|
||||||
|
field=models.CharField(blank=True, help_text='The minimum time-to-live value applies to all resource records in the zone file. This value is supplied in query responses to inform other servers how long they should keep the data in cache. The default value is <tt>30m</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='min TTL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='record',
|
||||||
|
name='ttl',
|
||||||
|
field=models.CharField(blank=True, help_text='Record TTL, defaults to 30m', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='record',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('MX', 'MX'), ('NS', 'NS'), ('CNAME', 'CNAME'), ('A', 'A (IPv4 address)'), ('AAAA', 'AAAA (IPv6 address)'), ('SRV', 'SRV'), ('TXT', 'TXT'), ('SPF', 'SPF')], max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2019-08-05 09:34
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('domains', '0006_auto_20170528_2011'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='record',
|
||||||
|
name='value',
|
||||||
|
field=models.CharField(help_text='MX, NS and CNAME records sould end with a dot.', max_length=1024, verbose_name='value'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2019-09-20 07:21
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('domains', '0007_auto_20190805_1134'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='domain',
|
||||||
|
name='dns2136_address_match_list',
|
||||||
|
field=models.CharField(blank=True, default='none;', help_text="A bind-9 'address_match_list' that will be granted permission to perform dns2136 updates. Chiefly used to enable Let's Encrypt self-service validation.", max_length=80),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2020-02-04 11:17
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('domains', '0008_domain_dns2136_address_match_list'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='domain',
|
||||||
|
name='dns2136_address_match_list',
|
||||||
|
field=models.CharField(blank=True, default='key pangea.key;', help_text="A bind-9 'address_match_list' that will be granted permission to perform dns2136 updates. Chiefly used to enable Let's Encrypt self-service validation.", max_length=80),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,26 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-03-30 10:49
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import orchestra.contrib.domains.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('domains', '0009_auto_20200204_1217'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='domain',
|
||||||
|
name='min_ttl',
|
||||||
|
field=models.CharField(blank=True, help_text='The minimum time-to-live value applies to all resource records in the zone file. This value is supplied in query responses to inform other servers how long they should keep the data in cache. The default value is <tt>1h</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='min TTL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='record',
|
||||||
|
name='ttl',
|
||||||
|
field=models.CharField(blank=True, help_text='Record TTL, defaults to 1h', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -31,9 +31,9 @@ class Domain(models.Model):
|
||||||
validators.validate_allowed_domain
|
validators.validate_allowed_domain
|
||||||
])
|
])
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), blank=True,
|
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), blank=True,
|
||||||
related_name='domains', help_text=_("Automatically selected for subdomains."))
|
related_name='domains', on_delete=models.CASCADE, help_text=_("Automatically selected for subdomains."))
|
||||||
top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set',
|
top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set',
|
||||||
editable=False, verbose_name=_("top domain"))
|
editable=False, verbose_name=_("top domain"), on_delete=models.CASCADE)
|
||||||
serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, editable=False,
|
serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, editable=False,
|
||||||
help_text=_("A revision number that changes whenever this domain is updated."))
|
help_text=_("A revision number that changes whenever this domain is updated."))
|
||||||
refresh = models.CharField(_("refresh"), max_length=16, blank=True,
|
refresh = models.CharField(_("refresh"), max_length=16, blank=True,
|
||||||
|
@ -65,6 +65,10 @@ class Domain(models.Model):
|
||||||
"zone file. This value is supplied in query responses to inform other "
|
"zone file. This value is supplied in query responses to inform other "
|
||||||
"servers how long they should keep the data in cache. "
|
"servers how long they should keep the data in cache. "
|
||||||
"The default value is <tt>%s</tt>.") % settings.DOMAINS_DEFAULT_MIN_TTL)
|
"The default value is <tt>%s</tt>.") % settings.DOMAINS_DEFAULT_MIN_TTL)
|
||||||
|
dns2136_address_match_list = models.CharField(max_length=80, default=settings.DOMAINS_DEFAULT_DNS2136,
|
||||||
|
blank=True,
|
||||||
|
help_text="A bind-9 'address_match_list' that will be granted permission to perform "
|
||||||
|
"dns2136 updates. Chiefly used to enable Let's Encrypt self-service validation.")
|
||||||
|
|
||||||
objects = DomainQuerySet.as_manager()
|
objects = DomainQuerySet.as_manager()
|
||||||
|
|
||||||
|
@ -314,12 +318,13 @@ class Record(models.Model):
|
||||||
SOA: (validators.validate_soa_record,),
|
SOA: (validators.validate_soa_record,),
|
||||||
}
|
}
|
||||||
|
|
||||||
domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records')
|
domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records', on_delete=models.CASCADE)
|
||||||
ttl = models.CharField(_("TTL"), max_length=8, blank=True,
|
ttl = models.CharField(_("TTL"), max_length=8, blank=True,
|
||||||
help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
|
help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
|
||||||
validators=[validators.validate_zone_interval])
|
validators=[validators.validate_zone_interval])
|
||||||
type = models.CharField(_("type"), max_length=32, choices=TYPE_CHOICES)
|
type = models.CharField(_("type"), max_length=32, choices=TYPE_CHOICES)
|
||||||
value = models.CharField(_("value"), max_length=256,
|
# max_length bumped from 256 to 1024 (arbitrary) on August 2019.
|
||||||
|
value = models.CharField(_("value"), max_length=1024,
|
||||||
help_text=_("MX, NS and CNAME records sould end with a dot."))
|
help_text=_("MX, NS and CNAME records sould end with a dot."))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
@ -122,3 +122,6 @@ DOMAINS_MASTERS = Setting('DOMAINS_MASTERS',
|
||||||
validators=[lambda masters: list(map(validate_ip_address, masters))],
|
validators=[lambda masters: list(map(validate_ip_address, masters))],
|
||||||
help_text="Additional master server ip addresses other than autodiscovered by router.get_servers()."
|
help_text="Additional master server ip addresses other than autodiscovered by router.get_servers()."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#TODO remove pangea-specific default
|
||||||
|
DOMAINS_DEFAULT_DNS2136 = "key pangea.key;"
|
||||||
|
|
|
@ -4,7 +4,7 @@ import socket
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from django.conf import settings as djsettings
|
from django.conf import settings as djsettings
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from selenium.webdriver.support.select import Select
|
from selenium.webdriver.support.select import Select
|
||||||
|
|
||||||
from orchestra.contrib.orchestration.models import Server, Route
|
from orchestra.contrib.orchestration.models import Server, Route
|
||||||
|
|
|
@ -60,7 +60,7 @@ def validate_zone_label(value):
|
||||||
if not value.endswith('.'):
|
if not value.endswith('.'):
|
||||||
msg = _("Use a fully expanded domain name ending with a dot.")
|
msg = _("Use a fully expanded domain name ending with a dot.")
|
||||||
raise ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
if len(value) > 63:
|
if len(value) > 254:
|
||||||
raise ValidationError(_("Labels must be 63 characters or less."))
|
raise ValidationError(_("Labels must be 63 characters or less."))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
|
||||||
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
|
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.contrib.admin.utils import unquote
|
|
||||||
from django.contrib.admin.templatetags.admin_static import static
|
from django.contrib.admin.templatetags.admin_static import static
|
||||||
|
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
|
||||||
|
from django.contrib.admin.utils import unquote
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.urls import NoReverseMatch, reverse
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin.utils import admin_link, admin_date
|
from orchestra.admin.utils import admin_date, admin_link
|
||||||
|
|
||||||
|
|
||||||
class LogEntryAdmin(admin.ModelAdmin):
|
class LogEntryAdmin(admin.ModelAdmin):
|
||||||
|
@ -34,11 +36,12 @@ class LogEntryAdmin(admin.ModelAdmin):
|
||||||
user_link = admin_link('user')
|
user_link = admin_link('user')
|
||||||
display_action_time = admin_date('action_time', short_description=_("Time"))
|
display_action_time = admin_date('action_time', short_description=_("Time"))
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_message(self, log):
|
def display_message(self, log):
|
||||||
edit = '<a href="%(url)s"><img src="%(img)s"></img></a>' % {
|
edit = format_html('<a href="{url}"><img src="{img}"></img></a>', **{
|
||||||
'url': reverse('admin:admin_logentry_change', args=(log.pk,)),
|
'url': reverse('admin:admin_logentry_change', args=(log.pk,)),
|
||||||
'img': static('admin/img/icon-changelink.svg'),
|
'img': static('admin/img/icon-changelink.svg'),
|
||||||
}
|
})
|
||||||
if log.is_addition():
|
if log.is_addition():
|
||||||
return _('Added "%(link)s". %(edit)s') % {
|
return _('Added "%(link)s". %(edit)s') % {
|
||||||
'link': self.content_object_link(log),
|
'link': self.content_object_link(log),
|
||||||
|
@ -57,7 +60,6 @@ class LogEntryAdmin(admin.ModelAdmin):
|
||||||
}
|
}
|
||||||
display_message.short_description = _("Message")
|
display_message.short_description = _("Message")
|
||||||
display_message.admin_order_field = 'action_flag'
|
display_message.admin_order_field = 'action_flag'
|
||||||
display_message.allow_tags = True
|
|
||||||
|
|
||||||
def display_action(self, log):
|
def display_action(self, log):
|
||||||
if log.is_addition():
|
if log.is_addition():
|
||||||
|
@ -75,10 +77,9 @@ class LogEntryAdmin(admin.ModelAdmin):
|
||||||
url = reverse(view, args=(log.object_id,))
|
url = reverse(view, args=(log.object_id,))
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
return log.object_repr
|
return log.object_repr
|
||||||
return '<a href="%s">%s</a>' % (url, log.object_repr)
|
return format_html('<a href="{}">{}</a>', url, log.object_repr)
|
||||||
content_object_link.short_description = _("Content object")
|
content_object_link.short_description = _("Content object")
|
||||||
content_object_link.admin_order_field = 'object_repr'
|
content_object_link.admin_order_field = 'object_repr'
|
||||||
content_object_link.allow_tags = True
|
|
||||||
|
|
||||||
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
|
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
|
||||||
""" Add rel_opts and object to context """
|
""" Add rel_opts and object to context """
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import format_html, strip_tags
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
|
|
||||||
|
@ -50,6 +51,7 @@ class MessageReadOnlyInline(admin.TabularInline):
|
||||||
'all': ('orchestra/css/hide-inline-id.css',)
|
'all': ('orchestra/css/hide-inline-id.css',)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def content_html(self, msg):
|
def content_html(self, msg):
|
||||||
context = {
|
context = {
|
||||||
'number': msg.number,
|
'number': msg.number,
|
||||||
|
@ -58,12 +60,13 @@ class MessageReadOnlyInline(admin.TabularInline):
|
||||||
}
|
}
|
||||||
summary = _("#%(number)i Updated by %(author)s about %(time)s") % context
|
summary = _("#%(number)i Updated by %(author)s about %(time)s") % context
|
||||||
header = '<strong style="color:#666;">%s</strong><hr />' % summary
|
header = '<strong style="color:#666;">%s</strong><hr />' % summary
|
||||||
|
|
||||||
content = markdown(msg.content)
|
content = markdown(msg.content)
|
||||||
content = content.replace('>\n', '>')
|
content = content.replace('>\n', '>')
|
||||||
content = '<div style="padding-left:20px;">%s</div>' % content
|
content = '<div style="padding-left:20px;">%s</div>' % content
|
||||||
|
|
||||||
return header + content
|
return header + content
|
||||||
content_html.short_description = _("Content")
|
content_html.short_description = _("Content")
|
||||||
content_html.allow_tags = True
|
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
return False
|
return False
|
||||||
|
@ -111,10 +114,10 @@ class TicketInline(admin.TabularInline):
|
||||||
colored_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
colored_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
||||||
colored_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
colored_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def ticket_id(self, instance):
|
def ticket_id(self, instance):
|
||||||
return '<b>%s</b>' % admin_link()(instance)
|
return '<b>%s</b>' % admin_link()(instance)
|
||||||
ticket_id.short_description = '#'
|
ticket_id.short_description = '#'
|
||||||
ticket_id.allow_tags = True
|
|
||||||
|
|
||||||
|
|
||||||
class TicketAdmin(ExtendedModelAdmin):
|
class TicketAdmin(ExtendedModelAdmin):
|
||||||
|
@ -135,7 +138,7 @@ class TicketAdmin(ExtendedModelAdmin):
|
||||||
'owner__username'
|
'owner__username'
|
||||||
)
|
)
|
||||||
actions = (
|
actions = (
|
||||||
mark_as_unread, mark_as_read, 'delete_selected', reject_tickets,
|
mark_as_unread, mark_as_read, reject_tickets,
|
||||||
resolve_tickets, close_tickets, take_tickets
|
resolve_tickets, close_tickets, take_tickets
|
||||||
)
|
)
|
||||||
sudo_actions = ('delete_selected',)
|
sudo_actions = ('delete_selected',)
|
||||||
|
@ -192,6 +195,7 @@ class TicketAdmin(ExtendedModelAdmin):
|
||||||
display_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
display_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
||||||
display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_summary(self, ticket):
|
def display_summary(self, ticket):
|
||||||
context = {
|
context = {
|
||||||
'creator': admin_link('creator')(self, ticket) if ticket.creator else ticket.creator_name,
|
'creator': admin_link('creator')(self, ticket) if ticket.creator else ticket.creator_name,
|
||||||
|
@ -207,14 +211,12 @@ class TicketAdmin(ExtendedModelAdmin):
|
||||||
context['updated'] = '. Updated by %(updater)s about %(updated)s' % context
|
context['updated'] = '. Updated by %(updater)s about %(updated)s' % context
|
||||||
return '<h4>Added by %(creator)s about %(created)s%(updated)s</h4>' % context
|
return '<h4>Added by %(creator)s about %(created)s%(updated)s</h4>' % context
|
||||||
display_summary.short_description = 'Summary'
|
display_summary.short_description = 'Summary'
|
||||||
display_summary.allow_tags = True
|
|
||||||
|
|
||||||
def unbold_id(self, ticket):
|
def unbold_id(self, ticket):
|
||||||
""" Unbold id if ticket is read """
|
""" Unbold id if ticket is read """
|
||||||
if ticket.is_read_by(self.user):
|
if ticket.is_read_by(self.user):
|
||||||
return '<span style="font-weight:normal;font-size:11px;">%s</span>' % ticket.pk
|
return format_html('<span style="font-weight:normal;font-size:11px;">{}</span>', ticket.pk)
|
||||||
return ticket.pk
|
return ticket.pk
|
||||||
unbold_id.allow_tags = True
|
|
||||||
unbold_id.short_description = "#"
|
unbold_id.short_description = "#"
|
||||||
unbold_id.admin_order_field = 'id'
|
unbold_id.admin_order_field = 'id'
|
||||||
|
|
||||||
|
@ -222,8 +224,7 @@ class TicketAdmin(ExtendedModelAdmin):
|
||||||
""" Bold subject when tickets are unread for request.user """
|
""" Bold subject when tickets are unread for request.user """
|
||||||
if ticket.is_read_by(self.user):
|
if ticket.is_read_by(self.user):
|
||||||
return ticket.subject
|
return ticket.subject
|
||||||
return "<strong class='unread'>%s</strong>" % ticket.subject
|
return format_html("<strong class='unread'>{}</strong>", ticket.subject)
|
||||||
bold_subject.allow_tags = True
|
|
||||||
bold_subject.short_description = _("Subject")
|
bold_subject.short_description = _("Subject")
|
||||||
bold_subject.admin_order_field = 'subject'
|
bold_subject.admin_order_field = 'subject'
|
||||||
|
|
||||||
|
@ -297,10 +298,9 @@ class QueueAdmin(admin.ModelAdmin):
|
||||||
num = queue.tickets__count
|
num = queue.tickets__count
|
||||||
url = reverse('admin:issues_ticket_changelist')
|
url = reverse('admin:issues_ticket_changelist')
|
||||||
url += '?queue=%i' % queue.pk
|
url += '?queue=%i' % queue.pk
|
||||||
return '<a href="%s">%d</a>' % (url, num)
|
return format_html('<a href="{}">{}</a>', url, num)
|
||||||
num_tickets.short_description = _("Tickets")
|
num_tickets.short_description = _("Tickets")
|
||||||
num_tickets.admin_order_field = 'tickets__count'
|
num_tickets.admin_order_field = 'tickets__count'
|
||||||
num_tickets.allow_tags = True
|
|
||||||
|
|
||||||
def get_list_display(self, request):
|
def get_list_display(self, request):
|
||||||
""" show notifications """
|
""" show notifications """
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from rest_framework import viewsets, mixins
|
from rest_framework import viewsets, mixins
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from orchestra.api import router, LogApiMixin
|
from orchestra.api import router, LogApiMixin
|
||||||
|
@ -13,13 +13,13 @@ class TicketViewSet(LogApiMixin, viewsets.ModelViewSet):
|
||||||
queryset = Ticket.objects.all()
|
queryset = Ticket.objects.all()
|
||||||
serializer_class = TicketSerializer
|
serializer_class = TicketSerializer
|
||||||
|
|
||||||
@detail_route()
|
@action(detail=True)
|
||||||
def mark_as_read(self, request, pk=None):
|
def mark_as_read(self, request, pk=None):
|
||||||
ticket = self.get_object()
|
ticket = self.get_object()
|
||||||
ticket.mark_as_read_by(request.user)
|
ticket.mark_as_read_by(request.user)
|
||||||
return Response({'status': 'Ticket marked as read'})
|
return Response({'status': 'Ticket marked as read'})
|
||||||
|
|
||||||
@detail_route()
|
@action(detail=True)
|
||||||
def mark_as_unread(self, request, pk=None):
|
def mark_as_unread(self, request, pk=None):
|
||||||
ticket = self.get_object()
|
ticket = self.get_object()
|
||||||
ticket.mark_as_unread_by(request.user)
|
ticket.mark_as_unread_by(request.user)
|
||||||
|
|
|
@ -22,7 +22,7 @@ class MarkDownWidget(forms.Textarea):
|
||||||
)
|
)
|
||||||
markdown_help_text = 'HTML not allowed, you can use %s' % markdown_help_text
|
markdown_help_text = 'HTML not allowed, you can use %s' % markdown_help_text
|
||||||
|
|
||||||
def render(self, name, value, attrs):
|
def render(self, name, value, attrs, renderer=None):
|
||||||
widget_id = attrs['id'] if attrs and 'id' in attrs else 'id_%s' % name
|
widget_id = attrs['id'] if attrs and 'id' in attrs else 'id_%s' % name
|
||||||
textarea = super(MarkDownWidget, self).render(name, value, attrs)
|
textarea = super(MarkDownWidget, self).render(name, value, attrs)
|
||||||
preview = ('<a class="load-preview" href="#" data-field="{0}">preview</a>'\
|
preview = ('<a class="load-preview" href="#" data-field="{0}">preview</a>'\
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import orchestra.models.fields
|
import orchestra.models.fields
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -20,7 +21,7 @@ class Migration(migrations.Migration):
|
||||||
('author_name', models.CharField(blank=True, max_length=256, verbose_name='author name')),
|
('author_name', models.CharField(blank=True, max_length=256, verbose_name='author name')),
|
||||||
('content', models.TextField(verbose_name='content')),
|
('content', models.TextField(verbose_name='content')),
|
||||||
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='created on')),
|
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='created on')),
|
||||||
('author', models.ForeignKey(related_name='ticket_messages', to=settings.AUTH_USER_MODEL, verbose_name='author')),
|
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_messages', to=settings.AUTH_USER_MODEL, verbose_name='author')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'get_latest_by': 'id',
|
'get_latest_by': 'id',
|
||||||
|
@ -48,9 +49,9 @@ class Migration(migrations.Migration):
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
||||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
||||||
('cc', models.TextField(blank=True, help_text='emails to send a carbon copy to', verbose_name='CC')),
|
('cc', models.TextField(blank=True, help_text='emails to send a carbon copy to', verbose_name='CC')),
|
||||||
('creator', models.ForeignKey(related_name='tickets_created', null=True, to=settings.AUTH_USER_MODEL, verbose_name='created by')),
|
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tickets_created', null=True, to=settings.AUTH_USER_MODEL, verbose_name='created by')),
|
||||||
('owner', models.ForeignKey(blank=True, related_name='tickets_owned', null=True, to=settings.AUTH_USER_MODEL, verbose_name='assigned to')),
|
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, blank=True, related_name='tickets_owned', null=True, to=settings.AUTH_USER_MODEL, verbose_name='assigned to')),
|
||||||
('queue', models.ForeignKey(blank=True, related_name='tickets', null=True, to='issues.Queue')),
|
('queue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, blank=True, related_name='tickets', null=True, to='issues.Queue')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'ordering': ['-updated_at'],
|
'ordering': ['-updated_at'],
|
||||||
|
@ -60,14 +61,14 @@ class Migration(migrations.Migration):
|
||||||
name='TicketTracker',
|
name='TicketTracker',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
|
('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
|
||||||
('ticket', models.ForeignKey(related_name='trackers', to='issues.Ticket', verbose_name='ticket')),
|
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trackers', to='issues.Ticket', verbose_name='ticket')),
|
||||||
('user', models.ForeignKey(related_name='ticket_trackers', to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_trackers', to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='message',
|
model_name='message',
|
||||||
name='ticket',
|
name='ticket',
|
||||||
field=models.ForeignKey(related_name='messages', to='issues.Ticket', verbose_name='ticket'),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='issues.Ticket', verbose_name='ticket'),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='tickettracker',
|
name='tickettracker',
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:27
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.utils.timezone import utc
|
||||||
|
import orchestra.models.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('issues', '0001_initial'), ('issues', '0002_auto_20150709_1018'), ('issues', '0003_auto_20160320_1127'), ('issues', '0004_auto_20170528_2011')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Message',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('author_name', models.CharField(blank=True, max_length=256, verbose_name='author name')),
|
||||||
|
('content', models.TextField(verbose_name='content')),
|
||||||
|
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='created on')),
|
||||||
|
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_messages', to=settings.AUTH_USER_MODEL, verbose_name='author')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'get_latest_by': 'id',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Queue',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=128, unique=True, verbose_name='name')),
|
||||||
|
('verbose_name', models.CharField(blank=True, max_length=128, verbose_name='verbose_name')),
|
||||||
|
('default', models.BooleanField(default=False, verbose_name='default')),
|
||||||
|
('notify', orchestra.models.fields.MultiSelectField(blank=True, choices=[('SUPPORT', 'Support tickets'), ('ADMIN', 'Administrative'), ('BILLING', 'Billing'), ('TECH', 'Technical'), ('ADDS', 'Announcements'), ('EMERGENCY', 'Emergency contact')], default=('SUPPORT', 'ADMIN', 'BILLING', 'TECH', 'ADDS', 'EMERGENCY'), help_text='Contacts to notify by email', max_length=256, verbose_name='notify')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Ticket',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('creator_name', models.CharField(blank=True, max_length=256, verbose_name='creator name')),
|
||||||
|
('subject', models.CharField(max_length=256, verbose_name='subject')),
|
||||||
|
('description', models.TextField(verbose_name='description')),
|
||||||
|
('priority', models.CharField(choices=[('HIGH', 'High'), ('MEDIUM', 'Medium'), ('LOW', 'Low')], default='MEDIUM', max_length=32, verbose_name='priority')),
|
||||||
|
('state', models.CharField(choices=[('NEW', 'New'), ('IN_PROGRESS', 'In Progress'), ('RESOLVED', 'Resolved'), ('FEEDBACK', 'Feedback'), ('REJECTED', 'Rejected'), ('CLOSED', 'Closed')], default='NEW', max_length=32, verbose_name='state')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
||||||
|
('cc', models.TextField(blank=True, help_text='emails to send a carbon copy to', verbose_name='CC')),
|
||||||
|
('creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tickets_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')),
|
||||||
|
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tickets_owned', to=settings.AUTH_USER_MODEL, verbose_name='assigned to')),
|
||||||
|
('queue', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to='issues.Queue')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-updated_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='TicketTracker',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trackers', to='issues.Ticket', verbose_name='ticket')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_trackers', to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='message',
|
||||||
|
name='ticket',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='issues.Ticket', verbose_name='ticket'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='tickettracker',
|
||||||
|
unique_together=set([('ticket', 'user')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='created'),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='message',
|
||||||
|
name='created_on',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='message',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2016, 3, 20, 10, 27, 45, 766388, tzinfo=utc), verbose_name='created at'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='creator',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets_created', to=settings.AUTH_USER_MODEL, verbose_name='created by'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='owner',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets_owned', to=settings.AUTH_USER_MODEL, verbose_name='assigned to'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='queue',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets', to='issues.Queue'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,32 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2017-05-28 18:11
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('issues', '0003_auto_20160320_1127'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='creator',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets_created', to=settings.AUTH_USER_MODEL, verbose_name='created by'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='owner',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets_owned', to=settings.AUTH_USER_MODEL, verbose_name='assigned to'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='queue',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets', to='issues.Queue'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -161,10 +161,10 @@ class Ticket(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Message(models.Model):
|
class Message(models.Model):
|
||||||
ticket = models.ForeignKey('issues.Ticket', verbose_name=_("ticket"),
|
ticket = models.ForeignKey('issues.Ticket', on_delete=models.CASCADE,
|
||||||
related_name='messages')
|
verbose_name=_("ticket"), related_name='messages')
|
||||||
author = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("author"),
|
author = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.CASCADE,
|
||||||
related_name='ticket_messages')
|
verbose_name=_("author"), related_name='ticket_messages')
|
||||||
author_name = models.CharField(_("author name"), max_length=256, blank=True)
|
author_name = models.CharField(_("author name"), max_length=256, blank=True)
|
||||||
content = models.TextField(_("content"))
|
content = models.TextField(_("content"))
|
||||||
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
||||||
|
@ -191,9 +191,10 @@ class Message(models.Model):
|
||||||
|
|
||||||
class TicketTracker(models.Model):
|
class TicketTracker(models.Model):
|
||||||
""" Keeps track of user read tickets """
|
""" Keeps track of user read tickets """
|
||||||
ticket = models.ForeignKey(Ticket, verbose_name=_("ticket"), related_name='trackers')
|
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE,
|
||||||
user = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("user"),
|
verbose_name=_("ticket"), related_name='trackers')
|
||||||
related_name='ticket_trackers')
|
user = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.CASCADE,
|
||||||
|
verbose_name=_("user"), related_name='ticket_trackers')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (
|
unique_together = (
|
||||||
|
|
|
@ -10,7 +10,7 @@ from .serializers import ListSerializer
|
||||||
class ListViewSet(LogApiMixin, AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet):
|
class ListViewSet(LogApiMixin, AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet):
|
||||||
queryset = List.objects.all()
|
queryset = List.objects.all()
|
||||||
serializer_class = ListSerializer
|
serializer_class = ListSerializer
|
||||||
filter_fields = ('name',)
|
filter_fields = ('name', 'address_domain')
|
||||||
|
|
||||||
|
|
||||||
router.register(r'lists', ListViewSet)
|
router.register(r'lists', ListViewSet)
|
||||||
|
|
|
@ -48,20 +48,14 @@ class MailmanVirtualDomainController(ServiceController):
|
||||||
|
|
||||||
def save(self, mail_list):
|
def save(self, mail_list):
|
||||||
context = self.get_context(mail_list)
|
context = self.get_context(mail_list)
|
||||||
self.include_virtual_alias_domain(context)
|
#self.include_virtual_alias_domain(context)
|
||||||
|
|
||||||
def delete(self, mail_list):
|
def delete(self, mail_list):
|
||||||
context = self.get_context(mail_list)
|
context = self.get_context(mail_list)
|
||||||
self.exclude_virtual_alias_domain(context)
|
#self.exclude_virtual_alias_domain(context)
|
||||||
|
|
||||||
def commit(self):
|
def commit(self):
|
||||||
context = self.get_context_files()
|
context = self.get_context_files()
|
||||||
self.append(textwrap.dedent("""
|
|
||||||
# Apply changes if needed
|
|
||||||
if [[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]]; then
|
|
||||||
service postfix reload
|
|
||||||
fi""") % context
|
|
||||||
)
|
|
||||||
super(MailmanVirtualDomainController, self).commit()
|
super(MailmanVirtualDomainController, self).commit()
|
||||||
|
|
||||||
def get_context_files(self):
|
def get_context_files(self):
|
||||||
|
@ -107,7 +101,7 @@ class MailmanController(MailmanVirtualDomainController):
|
||||||
for suffix in self.address_suffixes:
|
for suffix in self.address_suffixes:
|
||||||
context['suffix'] = suffix
|
context['suffix'] = suffix
|
||||||
# Because mailman doesn't properly handle lists aliases we need two virtual aliases
|
# Because mailman doesn't properly handle lists aliases we need two virtual aliases
|
||||||
aliases.append("%(address_name)s%(suffix)s@%(domain)s\t%(name)s%(suffix)s" % context)
|
aliases.append("%(address_name)s%(suffix)s@%(domain)s\t%(name)s%(suffix)s@grups.pangea.org" % context)
|
||||||
if context['address_name'] != context['name']:
|
if context['address_name'] != context['name']:
|
||||||
# And another with the original list name; Mailman generates links with it
|
# And another with the original list name; Mailman generates links with it
|
||||||
aliases.append("%(name)s%(suffix)s@%(domain)s\t%(name)s%(suffix)s" % context)
|
aliases.append("%(name)s%(suffix)s@%(domain)s\t%(name)s%(suffix)s" % context)
|
||||||
|
@ -116,84 +110,21 @@ class MailmanController(MailmanVirtualDomainController):
|
||||||
def save(self, mail_list):
|
def save(self, mail_list):
|
||||||
context = self.get_context(mail_list)
|
context = self.get_context(mail_list)
|
||||||
# Create list
|
# Create list
|
||||||
self.append(textwrap.dedent("""
|
cmd = "/opt/mailman/venv/bin/python /usr/local/admin/orchestra_mailman3/save.py %(name)s %(admin)s %(address_name)s@%(domain)s" % context
|
||||||
# Create list %(name)s
|
if not mail_list.active:
|
||||||
[[ ! -e '%(mailman_root)s/lists/%(name)s' ]] && {
|
cmd += ' --inactive'
|
||||||
newlist --quiet --emailhost='%(domain)s' '%(name)s' '%(admin)s' '%(password)s'
|
self.append(cmd)
|
||||||
}""") % context)
|
|
||||||
# Custom domain
|
|
||||||
if mail_list.address:
|
|
||||||
context.update({
|
|
||||||
'aliases': self.get_virtual_aliases(context),
|
|
||||||
'num_entries': 2 if context['address_name'] != context['name'] else 1,
|
|
||||||
})
|
|
||||||
self.append(textwrap.dedent("""\
|
|
||||||
# Create list alias for custom domain
|
|
||||||
aliases='%(aliases)s'
|
|
||||||
if ! grep '\s\s*%(name)s\s*$' %(virtual_alias)s > /dev/null; then
|
|
||||||
echo "${aliases}" >> %(virtual_alias)s
|
|
||||||
UPDATED_VIRTUAL_ALIAS=1
|
|
||||||
else
|
|
||||||
existing=$({ grep -E '^\s*(%(address_name)s|%(name)s)@%(address_domain)s\s\s*%(name)s\s*$' %(virtual_alias)s || test $? -lt 2; }|wc -l)
|
|
||||||
if [[ $existing -ne %(num_entries)s ]]; then
|
|
||||||
sed -i -e '/^.*\s%(name)s\(%(suffixes_regex)s\)\s*$/d' \\
|
|
||||||
-e 'N; /^\s*\\n\s*$/d; P; D' %(virtual_alias)s
|
|
||||||
echo "${aliases}" >> %(virtual_alias)s
|
|
||||||
UPDATED_VIRTUAL_ALIAS=1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
echo "require_explicit_destination = 0" | \\
|
|
||||||
%(mailman_root)s/bin/config_list -i /dev/stdin %(name)s
|
|
||||||
echo "host_name = '%(address_domain)s'" | \\
|
|
||||||
%(mailman_root)s/bin/config_list -i /dev/stdin %(name)s""") % context
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.append(textwrap.dedent("""\
|
|
||||||
# Cleanup possible ex-custom domain
|
|
||||||
if ! grep '\s\s*%(name)s\s*$' %(virtual_alias)s > /dev/null; then
|
|
||||||
sed -i "/^.*\s%(name)s\s*$/d" %(virtual_alias)s
|
|
||||||
fi""") % context
|
|
||||||
)
|
|
||||||
# Update
|
|
||||||
if context['password'] is not None:
|
|
||||||
self.append(textwrap.dedent("""\
|
|
||||||
# Re-set password
|
|
||||||
%(mailman_root)s/bin/change_pw --listname="%(name)s" --password="%(password)s"\
|
|
||||||
""") % context
|
|
||||||
)
|
|
||||||
self.include_virtual_alias_domain(context)
|
|
||||||
if mail_list.active:
|
|
||||||
self.append('chmod 775 %(mailman_root)s/lists/%(name)s' % context)
|
|
||||||
else:
|
|
||||||
self.append('chmod 000 %(mailman_root)s/lists/%(name)s' % context)
|
|
||||||
|
|
||||||
def delete(self, mail_list):
|
def delete(self, mail_list):
|
||||||
context = self.get_context(mail_list)
|
context = self.get_context(mail_list)
|
||||||
self.exclude_virtual_alias_domain(context)
|
# Delete list
|
||||||
self.append(textwrap.dedent("""
|
cmd = "/opt/mailman/venv/bin/python /usr/local/admin/orchestra_mailman3/delete.py %(name)s %(admin)s %(address_name)s@%(domain)s" % context
|
||||||
# Remove list %(name)s
|
if not mail_list.active:
|
||||||
sed -i -e '/^.*\s%(name)s\(%(suffixes_regex)s\)\s*$/d' \\
|
cmd += ' --inactive'
|
||||||
-e 'N; /^\s*\\n\s*$/d; P; D' %(virtual_alias)s
|
self.append(cmd)
|
||||||
# Non-existent list archives produce exit code 1
|
|
||||||
exit_code=0
|
|
||||||
rmlist -a %(name)s || exit_code=$?
|
|
||||||
if [[ $exit_code != 0 && $exit_code != 1 ]]; then
|
|
||||||
exit $exit_code
|
|
||||||
fi""") % context
|
|
||||||
)
|
|
||||||
|
|
||||||
def commit(self):
|
def commit(self):
|
||||||
context = self.get_context_files()
|
pass
|
||||||
self.append(textwrap.dedent("""
|
|
||||||
# Apply changes if needed
|
|
||||||
if [[ $UPDATED_VIRTUAL_ALIAS == 1 ]]; then
|
|
||||||
postmap %(virtual_alias)s
|
|
||||||
fi
|
|
||||||
if [[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]]; then
|
|
||||||
service postfix reload
|
|
||||||
fi
|
|
||||||
exit $exit_code""") % context
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_context_files(self):
|
def get_context_files(self):
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import django.db.models.deletion
|
||||||
import orchestra.core.validators
|
import orchestra.core.validators
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,8 +23,8 @@ class Migration(migrations.Migration):
|
||||||
('address_name', models.CharField(max_length=128, validators=[orchestra.core.validators.validate_name], verbose_name='address name', blank=True)),
|
('address_name', models.CharField(max_length=128, validators=[orchestra.core.validators.validate_name], verbose_name='address name', blank=True)),
|
||||||
('admin_email', models.EmailField(max_length=254, verbose_name='admin email', help_text='Administration email address')),
|
('admin_email', models.EmailField(max_length=254, verbose_name='admin email', help_text='Administration email address')),
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='active', help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.')),
|
('is_active', models.BooleanField(default=True, verbose_name='active', help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.')),
|
||||||
('account', models.ForeignKey(related_name='lists', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lists', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
('address_domain', models.ForeignKey(null=True, blank=True, to='domains.Domain', verbose_name='address domain')),
|
('address_domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, null=True, blank=True, to='domains.Domain', verbose_name='address domain')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue