Compare commits
128 Commits
dependabot
...
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
|
||||
```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 create --name orchestra -i -t -u orchestra -w /home/orchestra orchestra bash
|
||||
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
|
||||
```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:
|
||||
```bash
|
||||
cd panel
|
||||
python3 manage.py migrate
|
||||
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
|
||||
```bash
|
||||
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 django.contrib import admin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.shortcuts import render, redirect
|
||||
from django.utils.safestring import mark_safe
|
||||
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 fluent_dashboard import dashboard, appsettings
|
||||
from fluent_dashboard.modules import CmsAppIconList
|
||||
|
|
|
@ -5,7 +5,7 @@ from django import forms
|
|||
from django.contrib.admin import helpers
|
||||
from django.core import validators
|
||||
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 orchestra.forms.widgets import SpanWidget
|
||||
|
@ -28,9 +28,9 @@ class AdminFormMixin(object):
|
|||
' {% include "admin/includes/fieldset.html" %}'
|
||||
'{% endfor %}'
|
||||
)
|
||||
context = Context({
|
||||
context = {
|
||||
'adminform': adminform
|
||||
})
|
||||
}
|
||||
return template.render(context)
|
||||
|
||||
|
||||
|
@ -71,9 +71,9 @@ class AdminFormSet(BaseModelFormSet):
|
|||
</div>
|
||||
</div>""")
|
||||
)
|
||||
context = Context({
|
||||
context = {
|
||||
'formset': self
|
||||
})
|
||||
}
|
||||
return template.render(context)
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from copy import deepcopy
|
||||
|
||||
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.translation import ugettext_lazy as _
|
||||
|
||||
|
|
|
@ -6,11 +6,11 @@ from functools import wraps
|
|||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
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.shortcuts import redirect
|
||||
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 orchestra.models.utils import get_field_value
|
||||
|
@ -113,21 +113,21 @@ def admin_link(*args, **kwargs):
|
|||
return '---'
|
||||
if not getattr(obj, 'pk', None):
|
||||
return '---'
|
||||
display = kwargs.get('display')
|
||||
if display:
|
||||
display = getattr(obj, display, display)
|
||||
display_ = kwargs.get('display')
|
||||
if display_:
|
||||
display_ = getattr(obj, display_, display_)
|
||||
else:
|
||||
display = obj
|
||||
display_ = obj
|
||||
try:
|
||||
url = change_url(obj)
|
||||
except NoReverseMatch:
|
||||
# Does not has admin
|
||||
return str(display)
|
||||
return str(display_)
|
||||
extra = ''
|
||||
if kwargs['popup']:
|
||||
extra = 'onclick="return showAddAnotherPopup(this);"'
|
||||
extra = mark_safe('onclick="return showAddAnotherPopup(this);"')
|
||||
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
|
||||
|
@ -158,7 +158,7 @@ def admin_date(*args, **kwargs):
|
|||
date = date.strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||
else:
|
||||
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):
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
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 .serializers import SetPasswordSerializer
|
||||
|
||||
|
||||
class SetPasswordApiMixin(object):
|
||||
@detail_route(methods=['post'], serializer_class=SetPasswordSerializer)
|
||||
@action(detail=True, methods=['post'], serializer_class=SetPasswordSerializer)
|
||||
def set_password(self, request, pk):
|
||||
obj = self.get_object()
|
||||
data = request.DATA
|
||||
data = request.data
|
||||
if isinstance(data, str):
|
||||
data = {
|
||||
'password': data
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from django.core.urlresolvers import NoReverseMatch
|
||||
from django.urls import NoReverseMatch
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
|
||||
|
@ -23,16 +23,16 @@ def link_wrap(view, view_names):
|
|||
return wrapper
|
||||
|
||||
|
||||
def insert_links(viewset, base_name):
|
||||
collection_links = ['api-root', '%s-list' % base_name]
|
||||
object_links = ['api-root', '%s-list' % base_name, '%s-detail' % base_name]
|
||||
def insert_links(viewset, basename):
|
||||
collection_links = ['api-root', '%s-list' % basename]
|
||||
object_links = ['api-root', '%s-list' % basename, '%s-detail' % basename]
|
||||
exception_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
|
||||
for methodname in dir(viewset):
|
||||
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'):
|
||||
list_links.append(view_name)
|
||||
retrieve_links.append(view_name)
|
||||
|
|
|
@ -65,12 +65,12 @@ class LinkHeaderRouter(DefaultRouter):
|
|||
APIRoot.router = self
|
||||
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 """
|
||||
if base_name is None:
|
||||
base_name = self.get_default_base_name(viewset)
|
||||
insert_links(viewset, base_name)
|
||||
self.registry.append((prefix, viewset, base_name))
|
||||
if basename is None:
|
||||
basename = self.get_default_basename(viewset)
|
||||
insert_links(viewset, basename)
|
||||
self.registry.append((prefix, viewset, basename))
|
||||
|
||||
def get_viewset(self, prefix_or_model):
|
||||
for _prefix, viewset, __ in self.registry:
|
||||
|
|
|
@ -23,7 +23,7 @@ class APIRoot(views.APIView):
|
|||
'accountancy': {},
|
||||
'services': {},
|
||||
}
|
||||
if not request.user.is_anonymous():
|
||||
if not request.user.is_anonymous:
|
||||
list_name = '{basename}-list'
|
||||
detail_name = '{basename}-detail'
|
||||
for prefix, viewset, basename in self.router.registry:
|
||||
|
|
|
@ -64,7 +64,10 @@ class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
|
|||
class RelatedHyperlinkedModelSerializer(HyperlinkedModelSerializer):
|
||||
""" returns object on to_internal_value based on URL """
|
||||
def to_internal_value(self, data):
|
||||
url = data.get('url')
|
||||
try:
|
||||
url = data.get('url')
|
||||
except AttributeError:
|
||||
url = None
|
||||
if not url:
|
||||
raise ValidationError({
|
||||
'url': "URL is required."
|
||||
|
@ -81,14 +84,14 @@ class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer):
|
|||
validators=[validate_password], write_only=True, required=False,
|
||||
style={'widget': widgets.PasswordInput})
|
||||
|
||||
def validate_password(self, attrs, source):
|
||||
def validate_password(self, value):
|
||||
""" POST only password """
|
||||
if self.instance:
|
||||
if 'password' in attrs:
|
||||
if value:
|
||||
raise serializers.ValidationError(_("Can not set password"))
|
||||
elif 'password' not in attrs:
|
||||
elif not value:
|
||||
raise serializers.ValidationError(_("Password required"))
|
||||
return attrs
|
||||
return value
|
||||
|
||||
def validate(self, attrs):
|
||||
""" remove password in case is not a real model field """
|
||||
|
@ -98,7 +101,7 @@ class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer):
|
|||
pass
|
||||
else:
|
||||
password = attrs.pop('password', None)
|
||||
attrs = super(SetPasswordSerializer, self).validate()
|
||||
attrs = super().validate(attrs)
|
||||
if password is not None:
|
||||
attrs['password'] = password
|
||||
return attrs
|
||||
|
|
|
@ -157,7 +157,7 @@ function install_requirements () {
|
|||
PIP="${PIP} \
|
||||
selenium \
|
||||
xvfbwrapper \
|
||||
freezegun \
|
||||
freezegun==0.3.14 \
|
||||
coverage \
|
||||
flake8 \
|
||||
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)
|
||||
if [[ ! $wkhtmltox_version ]] || [[ $wkhtmltox_version != 0.12.2.1 && $minor == ${wkhtmltox_version} ]]; then
|
||||
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; }
|
||||
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):
|
||||
command = 'python3 -W ignore::DeprecationWarning {manage} runtask {task_id}'.format(
|
||||
manage=manage, task_id=task_id)
|
||||
proc = run(command, async=True)
|
||||
proc = run(command, run_async=True)
|
||||
yield proc
|
||||
|
||||
|
||||
|
@ -201,7 +201,7 @@ def fire_pending_messages(settings, db):
|
|||
|
||||
if has_pending_messages(settings, db):
|
||||
command = 'python3 -W ignore::DeprecationWarning {manage} sendpendingmessages'.format(manage=manage)
|
||||
proc = run(command, async=True)
|
||||
proc = run(command, run_async=True)
|
||||
yield proc
|
||||
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ SECRET_KEY = '{{ secret_key }}'
|
|||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
# Application definition
|
||||
|
||||
|
@ -65,6 +66,7 @@ INSTALLED_APPS = [
|
|||
'admin_tools.dashboard',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'django_filters',
|
||||
'passlib.ext.django',
|
||||
'django_countries',
|
||||
# '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'
|
||||
|
||||
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
|
||||
# https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
|
||||
|
||||
|
@ -168,22 +203,6 @@ LOCALE_PATHS = (
|
|||
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'
|
||||
|
||||
|
||||
|
@ -228,7 +247,7 @@ REST_FRAMEWORK = {
|
|||
'rest_framework.authentication.TokenAuthentication',
|
||||
),
|
||||
'DEFAULT_FILTER_BACKENDS': (
|
||||
('rest_framework.filters.DjangoFilterBackend',)
|
||||
('django_filters.rest_framework.DjangoFilterBackend',)
|
||||
),
|
||||
}
|
||||
|
||||
|
@ -242,7 +261,6 @@ PASSLIB_CONFIG = (
|
|||
"default = sha512_crypt\n"
|
||||
"deprecated = django_pbkdf2_sha1, django_salted_sha1, django_salted_md5, "
|
||||
" django_des_crypt, des_crypt, hex_md5\n"
|
||||
"all__vary_rounds = 0.05\n"
|
||||
"django_pbkdf2_sha256__min_rounds = 10000\n"
|
||||
"sha512_crypt__min_rounds = 80000\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.utils import NestedObjects, quote
|
||||
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.shortcuts import redirect, render
|
||||
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():
|
||||
count = 0
|
||||
# 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
|
||||
# Discount account
|
||||
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.admin.utils import unquote
|
||||
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.templatetags.static import static
|
||||
from django.utils.safestring import mark_safe
|
||||
|
@ -158,6 +158,7 @@ class AccountListAdmin(AccountAdmin):
|
|||
actions = None
|
||||
change_list_template = 'admin/accounts/account/select_account_list.html'
|
||||
|
||||
@mark_safe
|
||||
def select_account(self, instance):
|
||||
# TODO get query string from request.META['QUERY_STRING'] to preserve filters
|
||||
context = {
|
||||
|
@ -167,7 +168,6 @@ class AccountListAdmin(AccountAdmin):
|
|||
}
|
||||
return _('<a href="%(url)s">%(plus)s Add to %(name)s</a>') % context
|
||||
select_account.short_description = _("account")
|
||||
select_account.allow_tags = True
|
||||
select_account.admin_order_field = 'username'
|
||||
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
|
@ -207,6 +207,7 @@ class AccountAdminMixin(object):
|
|||
account = None
|
||||
list_select_related = ('account',)
|
||||
|
||||
@mark_safe
|
||||
def display_active(self, instance):
|
||||
if not instance.is_active:
|
||||
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 src="%s" alt="False">' % static('admin/img/icon-yes.svg')
|
||||
display_active.short_description = _("active")
|
||||
display_active.allow_tags = True
|
||||
display_active.admin_order_field = 'is_active'
|
||||
|
||||
def account_link(self, instance):
|
||||
account = instance.account if instance.pk else self.account
|
||||
return admin_link()(account)
|
||||
account_link.short_description = _("account")
|
||||
account_link.allow_tags = True
|
||||
account_link.admin_order_field = 'account__username'
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
|
|
|
@ -47,7 +47,7 @@ def create_account_creation_form():
|
|||
# Previous validation error
|
||||
return
|
||||
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():
|
||||
errors['username'] = _("A system user with this name already exists.")
|
||||
for model, key, related_kwargs, __ in create_related:
|
||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||
|
||||
from django.db import models, migrations
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
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_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)),
|
||||
('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={
|
||||
'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 import Operation
|
||||
from orchestra.core import services
|
||||
from orchestra import core
|
||||
from orchestra.models.utils import has_db_field
|
||||
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')
|
||||
])
|
||||
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)
|
||||
full_name = models.CharField(_("full name"), max_length=256)
|
||||
email = models.EmailField(_('email address'), help_text=_("Used for password recovery"))
|
||||
|
@ -52,6 +52,11 @@ class Account(auth.AbstractBaseUser):
|
|||
USERNAME_FIELD = 'username'
|
||||
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):
|
||||
return self.name
|
||||
|
||||
|
@ -98,7 +103,7 @@ class Account(auth.AbstractBaseUser):
|
|||
]
|
||||
for rel in related_fields:
|
||||
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():
|
||||
yield obj
|
||||
|
||||
|
@ -141,12 +146,25 @@ class Account(auth.AbstractBaseUser):
|
|||
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
|
||||
provided, permissions for this specific object are checked.
|
||||
applabel.action_modelname
|
||||
"""
|
||||
if not self.is_active:
|
||||
return False
|
||||
# Active superusers have all permissions.
|
||||
if self.is_active and self.is_superuser:
|
||||
if self.is_superuser:
|
||||
return True
|
||||
# Otherwise we need to check the backends.
|
||||
return auth._user_has_perm(self, perm, obj)
|
||||
app, action_model = perm.split('.')
|
||||
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):
|
||||
"""
|
||||
|
@ -167,7 +185,6 @@ class Account(auth.AbstractBaseUser):
|
|||
# Active superusers have all permissions.
|
||||
if self.is_active and self.is_superuser:
|
||||
return True
|
||||
return auth._user_has_module_perms(self, app_label)
|
||||
|
||||
def get_related_passwords(self, db_field=False):
|
||||
related = [
|
||||
|
|
|
@ -7,7 +7,7 @@ class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
|||
class Meta:
|
||||
model = Account
|
||||
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'
|
||||
)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ from datetime import date
|
|||
from django.contrib import messages
|
||||
from django.contrib.admin import helpers
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.db import transaction
|
||||
from django.forms.models import modelformset_factory
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
|
|
|
@ -2,11 +2,12 @@ from django import forms
|
|||
from django.conf.urls import url
|
||||
from django.contrib import admin, messages
|
||||
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.models import F, Sum, Prefetch
|
||||
from django.db.models.functions import Coalesce
|
||||
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_lazy as _
|
||||
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.contrib.accounts.actions import list_accounts
|
||||
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 .filters import (BillTypeListFilter, HasBillContactListFilter, TotalListFilter,
|
||||
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)
|
||||
|
||||
|
||||
|
@ -67,6 +68,7 @@ class BillLineInline(admin.TabularInline):
|
|||
|
||||
order_link = admin_link('order', display='pk')
|
||||
|
||||
@mark_safe
|
||||
def display_total(self, line):
|
||||
if line.pk:
|
||||
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">%s</a>' % (url, total)
|
||||
display_total.short_description = _("Total")
|
||||
display_total.allow_tags = True
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
""" Make value input widget bigger """
|
||||
|
@ -104,27 +105,26 @@ class ClosedBillLineInline(BillLineInline):
|
|||
readonly_fields = fields
|
||||
can_delete = False
|
||||
|
||||
@mark_safe
|
||||
def display_description(self, line):
|
||||
descriptions = [line.description]
|
||||
for subline in line.sublines.all():
|
||||
descriptions.append(' '*4+subline.description)
|
||||
descriptions.append(' ' * 4 + subline.description)
|
||||
return '<br>'.join(descriptions)
|
||||
display_description.short_description = _("Description")
|
||||
display_description.allow_tags = True
|
||||
|
||||
@mark_safe
|
||||
def display_subtotal(self, line):
|
||||
subtotals = [' ' + str(line.subtotal)]
|
||||
for subline in line.sublines.all():
|
||||
subtotals.append(str(subline.total))
|
||||
return '<br>'.join(subtotals)
|
||||
display_subtotal.short_description = _("Subtotal")
|
||||
display_subtotal.allow_tags = True
|
||||
|
||||
def display_total(self, line):
|
||||
if line.pk:
|
||||
return line.compute_total()
|
||||
display_total.short_description = _("Total")
|
||||
display_total.allow_tags = True
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
@ -242,6 +242,7 @@ class BillLineManagerAdmin(BillLineAdmin):
|
|||
|
||||
|
||||
class BillAdminMixin(AccountAdminMixin):
|
||||
@mark_safe
|
||||
def display_total_with_subtotals(self, bill):
|
||||
if bill.pk:
|
||||
currency = settings.BILLS_CURRENCY.lower()
|
||||
|
@ -251,10 +252,10 @@ class BillAdminMixin(AccountAdminMixin):
|
|||
subtotals.append(_("Taxes %s%% VAT %s &%s;") % (tax, subtotal[1], currency))
|
||||
subtotals = '\n'.join(subtotals)
|
||||
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.admin_order_field = 'approx_total'
|
||||
|
||||
@mark_safe
|
||||
def display_payment_state(self, bill):
|
||||
if bill.pk:
|
||||
t_opts = bill.transactions.model._meta
|
||||
|
@ -276,7 +277,6 @@ class BillAdminMixin(AccountAdminMixin):
|
|||
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
|
||||
return '<a href="{url}" style="color:{color}" title="{title}">{name}</a>'.format(
|
||||
url=url, color=color, name=state, title=title)
|
||||
display_payment_state.allow_tags = True
|
||||
display_payment_state.short_description = _("Payment")
|
||||
|
||||
def get_queryset(self, request):
|
||||
|
@ -376,16 +376,14 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin):
|
|||
|
||||
def display_total(self, bill):
|
||||
currency = settings.BILLS_CURRENCY.lower()
|
||||
return '%s &%s;' % (bill.compute_total(), currency)
|
||||
display_total.allow_tags = True
|
||||
return format_html('{} &{};', bill.compute_total(), currency)
|
||||
display_total.short_description = _("total")
|
||||
display_total.admin_order_field = 'approx_total'
|
||||
|
||||
def type_link(self, bill):
|
||||
bill_type = bill.type.lower()
|
||||
url = reverse('admin:bills_%s_changelist' % bill_type)
|
||||
return '<a href="%s">%s</a>' % (url, bill.get_type_display())
|
||||
type_link.allow_tags = True
|
||||
return format_html('<a href="{}">{}</a>', url, bill.get_type_display())
|
||||
type_link.short_description = _("type")
|
||||
type_link.admin_order_field = 'type'
|
||||
|
||||
|
@ -461,6 +459,7 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin):
|
|||
admin.site.register(Bill, BillAdmin)
|
||||
admin.site.register(Invoice, BillAdmin)
|
||||
admin.site.register(AmendmentInvoice, BillAdmin)
|
||||
admin.site.register(AbonoInvoice, BillAdmin)
|
||||
admin.site.register(Fee, BillAdmin)
|
||||
admin.site.register(AmendmentFee, BillAdmin)
|
||||
admin.site.register(ProForma, BillAdmin)
|
||||
|
@ -478,7 +477,7 @@ class BillContactInline(admin.StackedInline):
|
|||
if db_field.name == 'address':
|
||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||
if db_field.name == 'email_usage':
|
||||
kwargs['widget'] = paddingCheckboxSelectMultiple(45)
|
||||
kwargs['widget'] = PaddingCheckboxSelectMultiple(45)
|
||||
return super().formfield_for_dbfield(db_field, **kwargs)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.http import HttpResponse
|
||||
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.contrib.accounts.api import AccountApiMixin
|
||||
|
@ -15,7 +15,7 @@ class BillViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
|||
queryset = Bill.objects.all()
|
||||
serializer_class = BillSerializer
|
||||
|
||||
@detail_route(methods=['get'])
|
||||
@action(detail=True, methods=['get'])
|
||||
def document(self, request, pk):
|
||||
bill = self.get_object()
|
||||
content_type = request.META.get('HTTP_ACCEPT')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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.html import format_html
|
||||
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)
|
||||
send(request, mark_safe(message))
|
||||
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'):
|
||||
account = force_text(main)
|
||||
url = reverse('admin:accounts_account_change', args=(main.id,))
|
||||
|
|
Binary file not shown.
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\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"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -18,33 +18,33 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: actions.py:31
|
||||
#: actions.py:33
|
||||
msgid "View"
|
||||
msgstr "Vista"
|
||||
|
||||
#: actions.py:42
|
||||
#: actions.py:45
|
||||
msgid "Selected bills should be in open state"
|
||||
msgstr "Les factures seleccionades han d'estar en estat obert"
|
||||
|
||||
#: actions.py:57
|
||||
#: actions.py:60
|
||||
msgid "Selected bills have been closed"
|
||||
msgstr "Les factures seleccionades han estat tancades"
|
||||
|
||||
#: actions.py:70
|
||||
#: actions.py:73
|
||||
#, python-format
|
||||
msgid "<a href=\"%(url)s\">One related transaction</a> has been created"
|
||||
msgstr "S'ha creat una <a href=\"%(url)s\">transacció</a>"
|
||||
|
||||
#: actions.py:71
|
||||
#: actions.py:74
|
||||
#, python-format
|
||||
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>"
|
||||
|
||||
#: actions.py:77
|
||||
#: actions.py:80
|
||||
msgid "Are you sure about closing the following bills?"
|
||||
msgstr "Estàs a punt de tancar les següents factures, estàs segur?"
|
||||
|
||||
#: actions.py:78
|
||||
#: actions.py:81
|
||||
msgid ""
|
||||
"Once a bill is closed it can not be further modified.</p><p>Please select a "
|
||||
"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 "
|
||||
"plau selecciona un mètode de pagament per les factures seleccionades"
|
||||
|
||||
#: actions.py:91
|
||||
#: actions.py:97
|
||||
msgid "Close"
|
||||
msgstr "Tanca"
|
||||
|
||||
#: actions.py:109
|
||||
#: actions.py:115
|
||||
msgid "One bill has been sent."
|
||||
msgstr "S'ha creat una factura"
|
||||
|
||||
#: actions.py:110
|
||||
#: actions.py:116
|
||||
#, python-format
|
||||
msgid "%i bills have been sent."
|
||||
msgstr "S'han enviat %i factures."
|
||||
|
||||
#: actions.py:117
|
||||
#: actions.py:123
|
||||
msgid "Resend"
|
||||
msgstr "Reenviat"
|
||||
|
||||
#: actions.py:137
|
||||
#: actions.py:146
|
||||
msgid "Download"
|
||||
msgstr "Descarrega"
|
||||
|
||||
#: actions.py:153
|
||||
#: actions.py:162
|
||||
msgid "C.S.D."
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:155
|
||||
#: actions.py:164
|
||||
msgid "Close, send and download bills in one shot."
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:216
|
||||
#: actions.py:225
|
||||
#, python-format
|
||||
msgid "%(norders)s orders and %(nlines)s lines undoed."
|
||||
msgstr "%(norders)s ordres i %(nlines)s línies desfetes."
|
||||
|
||||
#: actions.py:235
|
||||
#: actions.py:244
|
||||
msgid "Lines moved"
|
||||
msgstr "Línies mogudes"
|
||||
|
||||
#: actions.py:248
|
||||
#: actions.py:257
|
||||
msgid "Selected bills should be in closed state"
|
||||
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
|
||||
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"
|
||||
|
||||
#: actions.py:272
|
||||
#: actions.py:286
|
||||
#, python-format
|
||||
msgid "%(related_type)s %(number)s subtotal for tax %(tax)s%%"
|
||||
msgstr "%(related_type)s %(number)s subtotal %(tax)s%%"
|
||||
|
||||
#: actions.py:288
|
||||
#: actions.py:303
|
||||
#, python-format
|
||||
msgid "<a href=\"%(url)s\">One amendment bill</a> have been generated."
|
||||
msgstr "S'ha creat una <a href=\"%(url)s\">transacció</a>"
|
||||
|
||||
#: actions.py:289
|
||||
#: actions.py:304
|
||||
#, python-format
|
||||
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>"
|
||||
|
||||
#: actions.py:292
|
||||
#: actions.py:307
|
||||
msgid "Amend"
|
||||
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:70
|
||||
msgid "Total"
|
||||
msgstr "Total"
|
||||
|
||||
#: admin.py:89
|
||||
#: admin.py:112
|
||||
msgid "Description"
|
||||
msgstr "Descripció"
|
||||
|
||||
#: admin.py:97
|
||||
#: admin.py:120
|
||||
msgid "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"
|
||||
msgstr "És oberta"
|
||||
|
||||
#: admin.py:135
|
||||
msgid "Subline"
|
||||
#: admin.py:175
|
||||
#, fuzzy
|
||||
#| msgid "Subline"
|
||||
msgid "Sublines"
|
||||
msgstr "Sublínia"
|
||||
|
||||
#: admin.py:167
|
||||
#: admin.py:221
|
||||
msgid "No bills selected."
|
||||
msgstr "No hi ha factures seleccionades"
|
||||
|
||||
#: admin.py:174
|
||||
#, python-format
|
||||
msgid "Manage %s bill lines."
|
||||
#: admin.py:229
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Manage %s bill lines."
|
||||
msgid "Manage %s bill lines"
|
||||
msgstr "Gestiona %s línies de factura."
|
||||
|
||||
#: admin.py:176
|
||||
#: admin.py:231
|
||||
msgid "Bill not in open state."
|
||||
msgstr "La factura no està en estat obert"
|
||||
|
||||
#: admin.py:179
|
||||
#: admin.py:234
|
||||
msgid "Not all bills are in open state."
|
||||
msgstr "No totes les factures estan en estat obert"
|
||||
|
||||
#: admin.py:180
|
||||
msgid "Manage bill lines of multiple bills."
|
||||
#: admin.py:235
|
||||
#, fuzzy
|
||||
#| msgid "Manage bill lines of multiple bills."
|
||||
msgid "Manage bill lines of multiple bills"
|
||||
msgstr "Gestiona línies de factura de multiples factures."
|
||||
|
||||
#: admin.py:204
|
||||
msgid "Dates"
|
||||
#: admin.py:250
|
||||
#, python-format
|
||||
msgid "Subtotal %s%% VAT %s &%s;"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:209
|
||||
msgid "Raw"
|
||||
msgstr "Raw"
|
||||
#: admin.py:251
|
||||
#, python-format
|
||||
msgid "Taxes %s%% VAT %s &%s;"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:235 models.py:73
|
||||
msgid "Created"
|
||||
msgstr "Creada"
|
||||
#: admin.py:255 admin.py:381 filters.py:46
|
||||
#: templates/bills/microspective.html:123
|
||||
msgid "total"
|
||||
msgstr "total"
|
||||
|
||||
#: admin.py:236
|
||||
#, fuzzy
|
||||
#| msgid "Close"
|
||||
msgid "Closed"
|
||||
msgstr "Tanca"
|
||||
#: admin.py:275
|
||||
msgid "This bill has been amended, this value may not be valid."
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:237
|
||||
#, fuzzy
|
||||
#| msgid "updated on"
|
||||
msgid "Updated"
|
||||
msgstr "actualitzada el"
|
||||
#: admin.py:280
|
||||
msgid "Payment"
|
||||
msgstr "Pagament"
|
||||
|
||||
#: admin.py:246
|
||||
#: admin.py:304
|
||||
#, fuzzy
|
||||
#| msgid "amended line"
|
||||
msgid "Amends"
|
||||
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"
|
||||
msgstr "línies"
|
||||
|
||||
#: admin.py:257 filters.py:46 templates/bills/microspective.html:118
|
||||
msgid "total"
|
||||
msgstr "total"
|
||||
|
||||
#: admin.py:265 models.py:104 models.py:460
|
||||
#: admin.py:389 models.py:108 models.py:501
|
||||
msgid "type"
|
||||
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
|
||||
msgid "All"
|
||||
msgstr "Tot"
|
||||
|
||||
#: filters.py:22 models.py:88
|
||||
#: filters.py:22 models.py:91
|
||||
msgid "Invoice"
|
||||
msgstr "Factura"
|
||||
|
||||
#: filters.py:23 models.py:90
|
||||
#: filters.py:23 models.py:93
|
||||
msgid "Fee"
|
||||
msgstr "Quota de soci"
|
||||
|
||||
|
@ -231,65 +262,67 @@ msgstr "Pro-forma"
|
|||
msgid "Amendment fee"
|
||||
msgstr "Rectificació de quota de soci"
|
||||
|
||||
#: filters.py:26 models.py:89
|
||||
#: filters.py:26 models.py:92
|
||||
msgid "Amendment invoice"
|
||||
msgstr "Factura rectificativa"
|
||||
|
||||
#: filters.py:68
|
||||
#: filters.py:71
|
||||
msgid "has bill contact"
|
||||
msgstr "té contacte de facturació"
|
||||
|
||||
#: filters.py:73
|
||||
#: filters.py:76
|
||||
msgid "Yes"
|
||||
msgstr "Si"
|
||||
|
||||
#: filters.py:74
|
||||
#: filters.py:77
|
||||
msgid "No"
|
||||
msgstr "No"
|
||||
|
||||
#: filters.py:85
|
||||
#: filters.py:88
|
||||
msgid "payment state"
|
||||
msgstr "Pagament"
|
||||
|
||||
#: filters.py:90 models.py:72
|
||||
#: filters.py:93 models.py:74
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:91 models.py:76
|
||||
#: filters.py:94 models.py:78
|
||||
msgid "Paid"
|
||||
msgstr "Pagat"
|
||||
|
||||
#: filters.py:92
|
||||
#: filters.py:95
|
||||
msgid "Pending"
|
||||
msgstr "Pendent"
|
||||
|
||||
#: filters.py:93 models.py:79
|
||||
#: filters.py:96 models.py:81
|
||||
msgid "Bad debt"
|
||||
msgstr "Incobrable"
|
||||
|
||||
#: filters.py:135
|
||||
#: filters.py:138
|
||||
#, fuzzy
|
||||
#| msgid "amended line"
|
||||
msgid "amended"
|
||||
msgstr "línia rectificada"
|
||||
|
||||
#: filters.py:140
|
||||
#: filters.py:143
|
||||
#, fuzzy
|
||||
#| msgid "Due date"
|
||||
msgid "Closed amends"
|
||||
msgstr "Data de pagament"
|
||||
|
||||
#: filters.py:141
|
||||
msgid "Open or closed amends"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:142
|
||||
#: filters.py:144
|
||||
#, fuzzy
|
||||
#| msgid "closed on"
|
||||
msgid "No closed amends"
|
||||
msgstr "tancat el"
|
||||
#| msgid "Due date"
|
||||
msgid "Open amends"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
|
@ -309,7 +342,7 @@ msgstr "Tipus"
|
|||
msgid "Source"
|
||||
msgstr "Font"
|
||||
|
||||
#: helpers.py:10
|
||||
#: helpers.py:14
|
||||
msgid ""
|
||||
"{relation} account \"{account}\" does not have a declared invoice contact. "
|
||||
"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 "
|
||||
"<a href=\"{url}#invoicecontact-group\">proporcionar un</a>"
|
||||
|
||||
#: helpers.py:17
|
||||
#: helpers.py:21
|
||||
msgid "Related"
|
||||
msgstr "Relacionat"
|
||||
|
||||
#: helpers.py:24
|
||||
#: helpers.py:28
|
||||
msgid "Main"
|
||||
msgstr "Principal"
|
||||
|
||||
#: models.py:24 models.py:100
|
||||
#: models.py:26 models.py:104
|
||||
msgid "account"
|
||||
msgstr "compte"
|
||||
|
||||
#: models.py:26
|
||||
#: models.py:28
|
||||
msgid "name"
|
||||
msgstr "nom"
|
||||
|
||||
#: models.py:27
|
||||
#: models.py:29
|
||||
msgid "Account full name will be used when left blank."
|
||||
msgstr "S'emprarà el nom complet del compte quan es deixi en blanc."
|
||||
|
||||
#: models.py:28
|
||||
#: models.py:30
|
||||
msgid "address"
|
||||
msgstr "adreça"
|
||||
|
||||
#: models.py:29
|
||||
#: models.py:31
|
||||
msgid "city"
|
||||
msgstr "ciutat"
|
||||
|
||||
#: models.py:31
|
||||
#: models.py:33
|
||||
msgid "zip code"
|
||||
msgstr "codi postal"
|
||||
|
||||
#: models.py:32
|
||||
#: models.py:34
|
||||
msgid "Enter a valid zipcode."
|
||||
msgstr "Introdueix un codi postal vàlid."
|
||||
|
||||
#: models.py:33
|
||||
#: models.py:35
|
||||
msgid "country"
|
||||
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"
|
||||
msgstr "NIF"
|
||||
|
||||
#: models.py:74
|
||||
#: models.py:76
|
||||
msgid "Processed"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:75
|
||||
#: models.py:77
|
||||
#, fuzzy
|
||||
#| msgid "amended line"
|
||||
msgid "Amended"
|
||||
msgstr "línia rectificada"
|
||||
|
||||
#: models.py:77
|
||||
#: models.py:79
|
||||
msgid "Incomplete"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:78
|
||||
#: models.py:80
|
||||
msgid "Executed"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:91
|
||||
#: models.py:94
|
||||
msgid "Amendment Fee"
|
||||
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"
|
||||
msgstr "Pro forma"
|
||||
|
||||
#: models.py:99
|
||||
#: models.py:103
|
||||
msgid "number"
|
||||
msgstr "número"
|
||||
|
||||
#: models.py:102
|
||||
#: models.py:106
|
||||
#, fuzzy
|
||||
#| msgid "amended line"
|
||||
msgid "amend of"
|
||||
msgstr "línia rectificada"
|
||||
|
||||
#: models.py:105
|
||||
#: models.py:109
|
||||
msgid "created on"
|
||||
msgstr "creat el"
|
||||
|
||||
#: models.py:106
|
||||
#: models.py:110
|
||||
msgid "closed on"
|
||||
msgstr "tancat el"
|
||||
|
||||
#: models.py:107
|
||||
#: models.py:111
|
||||
msgid "open"
|
||||
msgstr "obert"
|
||||
|
||||
#: models.py:108
|
||||
#: models.py:112
|
||||
msgid "sent"
|
||||
msgstr "enviat"
|
||||
|
||||
#: models.py:109
|
||||
#: models.py:113
|
||||
msgid "due on"
|
||||
msgstr "es deu"
|
||||
|
||||
#: models.py:110
|
||||
#: models.py:114
|
||||
msgid "updated on"
|
||||
msgstr "actualitzada el"
|
||||
|
||||
#: models.py:112
|
||||
#: models.py:116
|
||||
msgid "comments"
|
||||
msgstr "comentaris"
|
||||
|
||||
#: models.py:113
|
||||
#: models.py:117
|
||||
msgid "HTML"
|
||||
msgstr "HTML"
|
||||
|
||||
#: models.py:194
|
||||
#: models.py:200
|
||||
#, python-format
|
||||
msgid "Type %s is not an amendment."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:196
|
||||
#: models.py:202
|
||||
msgid "Amend of related account doesn't match bill account."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:198
|
||||
#: models.py:204
|
||||
#, fuzzy
|
||||
#| msgid "Bill not in open state."
|
||||
msgid "Related invoice is in open state."
|
||||
msgstr "La factura no està en estat obert"
|
||||
|
||||
#: models.py:200
|
||||
#: models.py:206
|
||||
msgid "Related invoice is an amendment."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:392
|
||||
#: models.py:419
|
||||
msgid "bill"
|
||||
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"
|
||||
msgstr "descripció"
|
||||
|
||||
#: models.py:394
|
||||
#: models.py:421
|
||||
msgid "rate"
|
||||
msgstr "tarifa"
|
||||
|
||||
#: models.py:395
|
||||
#: models.py:422
|
||||
msgid "quantity"
|
||||
msgstr "quantitat"
|
||||
|
||||
#: models.py:397
|
||||
#: models.py:424
|
||||
#, fuzzy
|
||||
#| msgid "quantity"
|
||||
msgid "Verbose quantity"
|
||||
msgstr "quantitat"
|
||||
|
||||
#: models.py:398 templates/admin/bills/bill/report.html:47
|
||||
#: templates/bills/microspective.html:77
|
||||
#: templates/bills/microspective.html:111
|
||||
#: models.py:425 templates/admin/bills/bill/report.html:47
|
||||
#: templates/bills/microspective.html:79
|
||||
#: templates/bills/microspective.html:116
|
||||
msgid "subtotal"
|
||||
msgstr "subtotal"
|
||||
|
||||
#: models.py:399
|
||||
#: models.py:426
|
||||
msgid "tax"
|
||||
msgstr "impostos"
|
||||
|
||||
#: models.py:400
|
||||
#: models.py:427
|
||||
msgid "start"
|
||||
msgstr "iniciar"
|
||||
|
||||
#: models.py:401
|
||||
#: models.py:428
|
||||
msgid "end"
|
||||
msgstr "finalitzar"
|
||||
|
||||
#: models.py:403
|
||||
#: models.py:431
|
||||
msgid "Informative link back to the order"
|
||||
msgstr "Enllaç informatiu de l'ordre"
|
||||
|
||||
#: models.py:404
|
||||
#: models.py:432
|
||||
msgid "order billed"
|
||||
msgstr "ordre facturada"
|
||||
|
||||
#: models.py:405
|
||||
#: models.py:433
|
||||
msgid "order billed until"
|
||||
msgstr "ordre facturada fins a"
|
||||
|
||||
#: models.py:406
|
||||
#: models.py:434
|
||||
msgid "created"
|
||||
msgstr "creada"
|
||||
|
||||
#: models.py:408
|
||||
#: models.py:436
|
||||
msgid "amended line"
|
||||
msgstr "línia rectificada"
|
||||
|
||||
#: models.py:451
|
||||
#: models.py:492
|
||||
msgid "Volume"
|
||||
msgstr "Volum"
|
||||
|
||||
#: models.py:452
|
||||
#: models.py:493
|
||||
msgid "Compensation"
|
||||
msgstr "Compensació"
|
||||
|
||||
#: models.py:453
|
||||
#: models.py:494
|
||||
msgid "Other"
|
||||
msgstr "Altre"
|
||||
|
||||
#: models.py:457
|
||||
#: models.py:498
|
||||
msgid "bill line"
|
||||
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
|
||||
msgid "Summary"
|
||||
msgstr ""
|
||||
|
@ -531,19 +586,19 @@ msgstr ""
|
|||
#: templates/admin/bills/bill/report.html:47
|
||||
#: templates/admin/bills/bill/report.html:51
|
||||
#: templates/admin/bills/bill/report.html:69
|
||||
#: templates/bills/microspective.html:111
|
||||
#: templates/bills/microspective.html:114
|
||||
#: templates/bills/microspective.html:116
|
||||
#: templates/bills/microspective.html:119
|
||||
msgid "VAT"
|
||||
msgstr "IVA"
|
||||
|
||||
#: templates/admin/bills/bill/report.html:51
|
||||
#: templates/bills/microspective.html:114
|
||||
#: templates/bills/microspective.html:119
|
||||
msgid "taxes"
|
||||
msgstr "impostos"
|
||||
|
||||
#: templates/admin/bills/bill/report.html:56
|
||||
#: templates/admin/bills/billline/report.html:60
|
||||
#: templates/bills/microspective.html:53
|
||||
#: templates/bills/microspective.html:54
|
||||
msgid "TOTAL"
|
||||
msgstr "TOTAL"
|
||||
|
||||
|
@ -561,8 +616,20 @@ msgstr "Data de pagament"
|
|||
msgid "Base"
|
||||
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
|
||||
msgid "Services"
|
||||
msgid "Service"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin/bills/billline/report.html:43
|
||||
|
@ -587,27 +654,21 @@ msgstr "quantitat"
|
|||
msgid "Profit"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin/bills/change_list.html:9
|
||||
#, fuzzy
|
||||
#| msgid "bill"
|
||||
msgid "Add bill"
|
||||
msgstr "factura"
|
||||
|
||||
#: templates/bills/microspective-fee.html:107
|
||||
#: templates/bills/microspective-fee.html:115
|
||||
msgid "Due date"
|
||||
msgstr "Data de pagament"
|
||||
|
||||
#: templates/bills/microspective-fee.html:108
|
||||
#: templates/bills/microspective-fee.html:116
|
||||
#, python-format
|
||||
msgid "On %(bank_account)s"
|
||||
msgstr "Al %(bank_account)s"
|
||||
|
||||
#: templates/bills/microspective-fee.html:114
|
||||
#: templates/bills/microspective-fee.html:122
|
||||
#, python-format
|
||||
msgid "From %(ini)s to %(end)s"
|
||||
msgstr "De %(ini)s a %(end)s"
|
||||
|
||||
#: templates/bills/microspective-fee.html:121
|
||||
#: templates/bills/microspective-fee.html:144
|
||||
msgid ""
|
||||
"\n"
|
||||
"<strong>With your membership</strong> you are supporting ...\n"
|
||||
|
@ -615,36 +676,36 @@ msgstr ""
|
|||
"\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"
|
||||
msgstr "VENCIMENT"
|
||||
|
||||
#: templates/bills/microspective.html:57
|
||||
#: templates/bills/microspective.html:58
|
||||
#, python-format
|
||||
msgid "%(bill_type)s DATE"
|
||||
msgstr "DATA %(bill_type)s"
|
||||
|
||||
#: templates/bills/microspective.html:74
|
||||
#: templates/bills/microspective.html:76
|
||||
msgid "period"
|
||||
msgstr "període"
|
||||
|
||||
#: templates/bills/microspective.html:75
|
||||
#: templates/bills/microspective.html:77
|
||||
msgid "hrs/qty"
|
||||
msgstr "hrs/qnt"
|
||||
|
||||
#: templates/bills/microspective.html:76
|
||||
#: templates/bills/microspective.html:78
|
||||
msgid "rate/price"
|
||||
msgstr "tarifa/preu"
|
||||
|
||||
#: templates/bills/microspective.html:131
|
||||
#: templates/bills/microspective.html:137
|
||||
msgid "COMMENTS"
|
||||
msgstr "COMENTARIS"
|
||||
|
||||
#: templates/bills/microspective.html:138
|
||||
#: templates/bills/microspective.html:145
|
||||
msgid "PAYMENT"
|
||||
msgstr "PAGAMENT"
|
||||
|
||||
#: templates/bills/microspective.html:142
|
||||
#: templates/bills/microspective.html:149
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
|
@ -658,11 +719,11 @@ msgstr ""
|
|||
"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"
|
||||
|
||||
#: templates/bills/microspective.html:151
|
||||
#: templates/bills/microspective.html:160
|
||||
msgid "QUESTIONS"
|
||||
msgstr "PREGUNTES"
|
||||
|
||||
#: templates/bills/microspective.html:152
|
||||
#: templates/bills/microspective.html:161
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
|
@ -679,5 +740,10 @@ msgstr ""
|
|||
"ràpidament possible.\n"
|
||||
" "
|
||||
|
||||
#, fuzzy
|
||||
#~| msgid "closed on"
|
||||
#~ msgid "No closed amends"
|
||||
#~ msgstr "tancat el"
|
||||
|
||||
#~ msgid "positive price"
|
||||
#~ msgstr "preu positiu"
|
||||
|
|
Binary file not shown.
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\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"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -18,33 +18,33 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: actions.py:31
|
||||
#: actions.py:33
|
||||
msgid "View"
|
||||
msgstr "Vista"
|
||||
|
||||
#: actions.py:42
|
||||
#: actions.py:45
|
||||
msgid "Selected bills should be in open state"
|
||||
msgstr "Las facturas seleccionadas están en estado abierto"
|
||||
|
||||
#: actions.py:57
|
||||
#: actions.py:60
|
||||
msgid "Selected bills have been closed"
|
||||
msgstr "Las facturas seleccionadas han sido cerradas"
|
||||
|
||||
#: actions.py:70
|
||||
#: actions.py:73
|
||||
#, python-format
|
||||
msgid "<a href=\"%(url)s\">One related transaction</a> has been created"
|
||||
msgstr "Se ha creado una <a href=\"%(url)s\">transacción</a>"
|
||||
|
||||
#: actions.py:71
|
||||
#: actions.py:74
|
||||
#, python-format
|
||||
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>"
|
||||
|
||||
#: actions.py:77
|
||||
#: actions.py:80
|
||||
msgid "Are you sure about closing the following bills?"
|
||||
msgstr "Estás a punto de cerrar las sigüientes facturas. ¿Estás seguro?"
|
||||
|
||||
#: actions.py:78
|
||||
#: actions.py:81
|
||||
msgid ""
|
||||
"Once a bill is closed it can not be further modified.</p><p>Please select a "
|
||||
"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 "
|
||||
"seleciona un metodo de pago para las facturas seleccionadas"
|
||||
|
||||
#: actions.py:91
|
||||
#: actions.py:97
|
||||
msgid "Close"
|
||||
msgstr "Cerrar"
|
||||
|
||||
#: actions.py:109
|
||||
#: actions.py:115
|
||||
msgid "One bill has been sent."
|
||||
msgstr "Se ha enviado una factura"
|
||||
|
||||
#: actions.py:110
|
||||
#: actions.py:116
|
||||
#, python-format
|
||||
msgid "%i bills have been sent."
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:117
|
||||
#: actions.py:123
|
||||
msgid "Resend"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:137
|
||||
#: actions.py:146
|
||||
msgid "Download"
|
||||
msgstr "Descarga"
|
||||
|
||||
#: actions.py:153
|
||||
#: actions.py:162
|
||||
msgid "C.S.D."
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:155
|
||||
#: actions.py:164
|
||||
msgid "Close, send and download bills in one shot."
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:216
|
||||
#: actions.py:225
|
||||
#, python-format
|
||||
msgid "%(norders)s orders and %(nlines)s lines undoed."
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:235
|
||||
#: actions.py:244
|
||||
msgid "Lines moved"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:248
|
||||
#: actions.py:257
|
||||
msgid "Selected bills should be in closed state"
|
||||
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
|
||||
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"
|
||||
|
||||
#: actions.py:272
|
||||
#: actions.py:286
|
||||
#, python-format
|
||||
msgid "%(related_type)s %(number)s subtotal for tax %(tax)s%%"
|
||||
msgstr "%(related_type)s %(number)s subtotal %(tax)s%%"
|
||||
|
||||
#: actions.py:288
|
||||
#: actions.py:303
|
||||
#, python-format
|
||||
msgid "<a href=\"%(url)s\">One amendment bill</a> have been generated."
|
||||
msgstr "Se ha creado una <a href=\"%(url)s\">transacción</a>"
|
||||
|
||||
#: actions.py:289
|
||||
#: actions.py:304
|
||||
#, python-format
|
||||
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>"
|
||||
|
||||
#: actions.py:292
|
||||
#: actions.py:307
|
||||
msgid "Amend"
|
||||
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:70
|
||||
msgid "Total"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:89
|
||||
#: admin.py:112
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:97
|
||||
#: admin.py:120
|
||||
msgid "Subtotal"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:130
|
||||
#: admin.py:146
|
||||
msgid "Totals"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:150
|
||||
msgid "Order"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:169
|
||||
msgid "Is open"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:135
|
||||
msgid "Subline"
|
||||
#: admin.py:175
|
||||
msgid "Sublines"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:167
|
||||
#: admin.py:221
|
||||
msgid "No bills selected."
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:174
|
||||
#, python-format
|
||||
msgid "Manage %s bill lines."
|
||||
msgstr ""
|
||||
#: admin.py:229
|
||||
#, fuzzy, python-format
|
||||
#| msgid "bill line"
|
||||
msgid "Manage %s bill lines"
|
||||
msgstr "linea de factura"
|
||||
|
||||
#: admin.py:176
|
||||
#: admin.py:231
|
||||
msgid "Bill not in open state."
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:179
|
||||
#: admin.py:234
|
||||
msgid "Not all bills are in open state."
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:180
|
||||
msgid "Manage bill lines of multiple bills."
|
||||
#: admin.py:235
|
||||
msgid "Manage bill lines of multiple bills"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:204
|
||||
msgid "Dates"
|
||||
#: admin.py:250
|
||||
#, python-format
|
||||
msgid "Subtotal %s%% VAT %s &%s;"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:209
|
||||
msgid "Raw"
|
||||
#: admin.py:251
|
||||
#, python-format
|
||||
msgid "Taxes %s%% VAT %s &%s;"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:235 models.py:73
|
||||
msgid "Created"
|
||||
#: admin.py:255 admin.py:381 filters.py:46
|
||||
#: templates/bills/microspective.html:123
|
||||
msgid "total"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:236
|
||||
#, fuzzy
|
||||
#| msgid "Close"
|
||||
msgid "Closed"
|
||||
msgstr "Cerrar"
|
||||
#: admin.py:275
|
||||
msgid "This bill has been amended, this value may not be valid."
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:237
|
||||
#, fuzzy
|
||||
#| msgid "updated on"
|
||||
msgid "Updated"
|
||||
msgstr "actualizada en"
|
||||
#: admin.py:280
|
||||
msgid "Payment"
|
||||
msgstr "Pago"
|
||||
|
||||
#: admin.py:246
|
||||
#: admin.py:304
|
||||
#, fuzzy
|
||||
#| msgid "Amended"
|
||||
msgid "Amends"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:257 filters.py:46 templates/bills/microspective.html:118
|
||||
msgid "total"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:265 models.py:104 models.py:460
|
||||
#: admin.py:389 models.py:108 models.py:501
|
||||
msgid "type"
|
||||
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
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:22 models.py:88
|
||||
#: filters.py:22 models.py:91
|
||||
msgid "Invoice"
|
||||
msgstr "Factura"
|
||||
|
||||
#: filters.py:23 models.py:90
|
||||
#: filters.py:23 models.py:93
|
||||
msgid "Fee"
|
||||
msgstr "Cuota de socio"
|
||||
|
||||
|
@ -231,65 +256,67 @@ msgstr ""
|
|||
msgid "Amendment fee"
|
||||
msgstr "Cuota rectificativa"
|
||||
|
||||
#: filters.py:26 models.py:89
|
||||
#: filters.py:26 models.py:92
|
||||
msgid "Amendment invoice"
|
||||
msgstr "Factura rectificativa"
|
||||
|
||||
#: filters.py:68
|
||||
#: filters.py:71
|
||||
msgid "has bill contact"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:73
|
||||
#: filters.py:76
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:74
|
||||
#: filters.py:77
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:85
|
||||
#: filters.py:88
|
||||
msgid "payment state"
|
||||
msgstr "Pago"
|
||||
|
||||
#: filters.py:90 models.py:72
|
||||
#: filters.py:93 models.py:74
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:91 models.py:76
|
||||
#: filters.py:94 models.py:78
|
||||
msgid "Paid"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:92
|
||||
#: filters.py:95
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:93 models.py:79
|
||||
#: filters.py:96 models.py:81
|
||||
msgid "Bad debt"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:135
|
||||
#: filters.py:138
|
||||
#, fuzzy
|
||||
#| msgid "Amended"
|
||||
msgid "amended"
|
||||
msgstr "Quota rectificativa"
|
||||
|
||||
#: filters.py:140
|
||||
#: filters.py:143
|
||||
#, fuzzy
|
||||
#| msgid "Due date"
|
||||
msgid "Closed amends"
|
||||
msgstr "Fecha de pago"
|
||||
|
||||
#: filters.py:141
|
||||
msgid "Open or closed amends"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:142
|
||||
#: filters.py:144
|
||||
#, fuzzy
|
||||
#| msgid "closed on"
|
||||
msgid "No closed amends"
|
||||
msgstr "cerrada en"
|
||||
#| msgid "Due date"
|
||||
msgid "Open amends"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
|
@ -309,213 +336,233 @@ msgstr ""
|
|||
msgid "Source"
|
||||
msgstr ""
|
||||
|
||||
#: helpers.py:10
|
||||
#: helpers.py:14
|
||||
msgid ""
|
||||
"{relation} account \"{account}\" does not have a declared invoice contact. "
|
||||
"You should <a href=\"{url}#invoicecontact-group\">provide one</a>"
|
||||
msgstr ""
|
||||
|
||||
#: helpers.py:17
|
||||
#: helpers.py:21
|
||||
msgid "Related"
|
||||
msgstr ""
|
||||
|
||||
#: helpers.py:24
|
||||
#: helpers.py:28
|
||||
msgid "Main"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:24 models.py:100
|
||||
#: models.py:26 models.py:104
|
||||
msgid "account"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:26
|
||||
#: models.py:28
|
||||
msgid "name"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:27
|
||||
#: models.py:29
|
||||
msgid "Account full name will be used when left blank."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:28
|
||||
#: models.py:30
|
||||
msgid "address"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:29
|
||||
#: models.py:31
|
||||
msgid "city"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:31
|
||||
#: models.py:33
|
||||
msgid "zip code"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:32
|
||||
#: models.py:34
|
||||
msgid "Enter a valid zipcode."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:33
|
||||
#: models.py:35
|
||||
msgid "country"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:36 templates/admin/bills/bill/report.html:65
|
||||
#: models.py:38 templates/admin/bills/bill/report.html:65
|
||||
msgid "VAT number"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:74
|
||||
#: models.py:76
|
||||
msgid "Processed"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:75
|
||||
#: models.py:77
|
||||
msgid "Amended"
|
||||
msgstr "Quota rectificativa"
|
||||
|
||||
#: models.py:77
|
||||
#: models.py:79
|
||||
msgid "Incomplete"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:78
|
||||
#: models.py:80
|
||||
msgid "Executed"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:91
|
||||
#: models.py:94
|
||||
msgid "Amendment Fee"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:92
|
||||
#: models.py:95
|
||||
#, fuzzy
|
||||
#| msgid "Invoice"
|
||||
msgid "Abono Invoice"
|
||||
msgstr "Abono"
|
||||
|
||||
#: models.py:96
|
||||
msgid "Pro forma"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:99
|
||||
#: models.py:103
|
||||
msgid "number"
|
||||
msgstr "número"
|
||||
|
||||
#: models.py:102
|
||||
#: models.py:106
|
||||
msgid "amend of"
|
||||
msgstr "rectificación de"
|
||||
|
||||
#: models.py:105
|
||||
#: models.py:109
|
||||
msgid "created on"
|
||||
msgstr "creado en"
|
||||
|
||||
#: models.py:106
|
||||
#: models.py:110
|
||||
msgid "closed on"
|
||||
msgstr "cerrada en"
|
||||
|
||||
#: models.py:107
|
||||
#: models.py:111
|
||||
msgid "open"
|
||||
msgstr "abierta"
|
||||
|
||||
#: models.py:108
|
||||
#: models.py:112
|
||||
msgid "sent"
|
||||
msgstr "enviada"
|
||||
|
||||
#: models.py:109
|
||||
#: models.py:113
|
||||
msgid "due on"
|
||||
msgstr "vencimiento"
|
||||
|
||||
#: models.py:110
|
||||
#: models.py:114
|
||||
msgid "updated on"
|
||||
msgstr "actualizada en"
|
||||
|
||||
#: models.py:112
|
||||
#: models.py:116
|
||||
msgid "comments"
|
||||
msgstr "comentarios"
|
||||
|
||||
#: models.py:113
|
||||
#: models.py:117
|
||||
msgid "HTML"
|
||||
msgstr "HTML"
|
||||
|
||||
#: models.py:194
|
||||
#: models.py:200
|
||||
#, python-format
|
||||
msgid "Type %s is not an amendment."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:196
|
||||
#: models.py:202
|
||||
msgid "Amend of related account doesn't match bill account."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:198
|
||||
#: models.py:204
|
||||
#, fuzzy
|
||||
#| msgid "Selected bills should be in open state"
|
||||
msgid "Related invoice is in open state."
|
||||
msgstr "Las facturas seleccionadas están en estado abierto"
|
||||
|
||||
#: models.py:200
|
||||
#: models.py:206
|
||||
msgid "Related invoice is an amendment."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:392
|
||||
#: models.py:419
|
||||
msgid "bill"
|
||||
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"
|
||||
msgstr "descripción"
|
||||
|
||||
#: models.py:394
|
||||
#: models.py:421
|
||||
msgid "rate"
|
||||
msgstr "tarifa"
|
||||
|
||||
#: models.py:395
|
||||
#: models.py:422
|
||||
msgid "quantity"
|
||||
msgstr "cantidad"
|
||||
|
||||
#: models.py:397
|
||||
#: models.py:424
|
||||
msgid "Verbose quantity"
|
||||
msgstr "Cantidad"
|
||||
|
||||
#: models.py:398 templates/admin/bills/bill/report.html:47
|
||||
#: templates/bills/microspective.html:77
|
||||
#: templates/bills/microspective.html:111
|
||||
#: models.py:425 templates/admin/bills/bill/report.html:47
|
||||
#: templates/bills/microspective.html:79
|
||||
#: templates/bills/microspective.html:116
|
||||
msgid "subtotal"
|
||||
msgstr "subtotal"
|
||||
|
||||
#: models.py:399
|
||||
#: models.py:426
|
||||
msgid "tax"
|
||||
msgstr "impuesto"
|
||||
|
||||
#: models.py:400
|
||||
#: models.py:427
|
||||
msgid "start"
|
||||
msgstr "inicio"
|
||||
|
||||
#: models.py:401
|
||||
#: models.py:428
|
||||
msgid "end"
|
||||
msgstr "fín"
|
||||
|
||||
#: models.py:403
|
||||
#: models.py:431
|
||||
msgid "Informative link back to the order"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:404
|
||||
#: models.py:432
|
||||
msgid "order billed"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:405
|
||||
#: models.py:433
|
||||
msgid "order billed until"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:406
|
||||
#: models.py:434
|
||||
msgid "created"
|
||||
msgstr "creado"
|
||||
|
||||
#: models.py:408
|
||||
#: models.py:436
|
||||
msgid "amended line"
|
||||
msgstr "linea rectificativa"
|
||||
|
||||
#: models.py:451
|
||||
#: models.py:492
|
||||
msgid "Volume"
|
||||
msgstr "Volumen"
|
||||
|
||||
#: models.py:452
|
||||
#: models.py:493
|
||||
msgid "Compensation"
|
||||
msgstr "Compensación"
|
||||
|
||||
#: models.py:453
|
||||
#: models.py:494
|
||||
msgid "Other"
|
||||
msgstr "Otro"
|
||||
|
||||
#: models.py:457
|
||||
#: models.py:498
|
||||
msgid "bill line"
|
||||
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
|
||||
msgid "Summary"
|
||||
msgstr ""
|
||||
|
@ -523,19 +570,19 @@ msgstr ""
|
|||
#: templates/admin/bills/bill/report.html:47
|
||||
#: templates/admin/bills/bill/report.html:51
|
||||
#: templates/admin/bills/bill/report.html:69
|
||||
#: templates/bills/microspective.html:111
|
||||
#: templates/bills/microspective.html:114
|
||||
#: templates/bills/microspective.html:116
|
||||
#: templates/bills/microspective.html:119
|
||||
msgid "VAT"
|
||||
msgstr "IVA"
|
||||
|
||||
#: templates/admin/bills/bill/report.html:51
|
||||
#: templates/bills/microspective.html:114
|
||||
#: templates/bills/microspective.html:119
|
||||
msgid "taxes"
|
||||
msgstr "impuestos"
|
||||
|
||||
#: templates/admin/bills/bill/report.html:56
|
||||
#: templates/admin/bills/billline/report.html:60
|
||||
#: templates/bills/microspective.html:53
|
||||
#: templates/bills/microspective.html:54
|
||||
msgid "TOTAL"
|
||||
msgstr "TOTAL"
|
||||
|
||||
|
@ -553,8 +600,20 @@ msgstr "Fecha de pago"
|
|||
msgid "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
|
||||
msgid "Services"
|
||||
msgid "Service"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin/bills/billline/report.html:43
|
||||
|
@ -579,62 +638,56 @@ msgstr "cantidad"
|
|||
msgid "Profit"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin/bills/change_list.html:9
|
||||
#, fuzzy
|
||||
#| msgid "bill"
|
||||
msgid "Add bill"
|
||||
msgstr "factura"
|
||||
|
||||
#: templates/bills/microspective-fee.html:107
|
||||
#: templates/bills/microspective-fee.html:115
|
||||
msgid "Due date"
|
||||
msgstr "Fecha de pago"
|
||||
|
||||
#: templates/bills/microspective-fee.html:108
|
||||
#: templates/bills/microspective-fee.html:116
|
||||
#, python-format
|
||||
msgid "On %(bank_account)s"
|
||||
msgstr "En %(bank_account)s"
|
||||
|
||||
#: templates/bills/microspective-fee.html:114
|
||||
#: templates/bills/microspective-fee.html:122
|
||||
#, python-format
|
||||
msgid "From %(ini)s to %(end)s"
|
||||
msgstr "Desde %(ini)s hasta %(end)s"
|
||||
|
||||
#: templates/bills/microspective-fee.html:121
|
||||
#: templates/bills/microspective-fee.html:144
|
||||
msgid ""
|
||||
"\n"
|
||||
"<strong>With your membership</strong> you are supporting ...\n"
|
||||
msgstr ""
|
||||
|
||||
#: templates/bills/microspective.html:49
|
||||
#: templates/bills/microspective.html:50
|
||||
msgid "DUE DATE"
|
||||
msgstr "VENCIMIENTO"
|
||||
|
||||
#: templates/bills/microspective.html:57
|
||||
#: templates/bills/microspective.html:58
|
||||
#, python-format
|
||||
msgid "%(bill_type)s DATE"
|
||||
msgstr "FECHA %(bill_type)s"
|
||||
|
||||
#: templates/bills/microspective.html:74
|
||||
#: templates/bills/microspective.html:76
|
||||
msgid "period"
|
||||
msgstr "periodo"
|
||||
|
||||
#: templates/bills/microspective.html:75
|
||||
#: templates/bills/microspective.html:77
|
||||
msgid "hrs/qty"
|
||||
msgstr "hrs/cant"
|
||||
|
||||
#: templates/bills/microspective.html:76
|
||||
#: templates/bills/microspective.html:78
|
||||
msgid "rate/price"
|
||||
msgstr "tarifa/precio"
|
||||
|
||||
#: templates/bills/microspective.html:131
|
||||
#: templates/bills/microspective.html:137
|
||||
msgid "COMMENTS"
|
||||
msgstr "COMENTARIOS"
|
||||
|
||||
#: templates/bills/microspective.html:138
|
||||
#: templates/bills/microspective.html:145
|
||||
msgid "PAYMENT"
|
||||
msgstr "PAGO"
|
||||
|
||||
#: templates/bills/microspective.html:142
|
||||
#: templates/bills/microspective.html:149
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
|
@ -648,11 +701,11 @@ msgstr ""
|
|||
"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"
|
||||
|
||||
#: templates/bills/microspective.html:151
|
||||
#: templates/bills/microspective.html:160
|
||||
msgid "QUESTIONS"
|
||||
msgstr "PREGUNTAS"
|
||||
|
||||
#: templates/bills/microspective.html:152
|
||||
#: templates/bills/microspective.html:161
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
|
@ -668,3 +721,8 @@ msgstr ""
|
|||
" contacta con nosotros en %(email)s. Te responderemos lo más "
|
||||
"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 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
|
@ -14,7 +15,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='bill',
|
||||
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(
|
||||
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
|
||||
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.db import models
|
||||
from django.db.models import F, Sum
|
||||
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.encoding import force_text
|
||||
from django.utils.functional import cached_property
|
||||
|
@ -24,7 +24,7 @@ from . import settings
|
|||
|
||||
class BillContact(models.Model):
|
||||
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,
|
||||
help_text=_("Account full name will be used when left blank."))
|
||||
address = models.TextField(_("address"))
|
||||
|
@ -86,11 +86,13 @@ class Bill(models.Model):
|
|||
FEE = 'FEE'
|
||||
AMENDMENTFEE = 'AMENDMENTFEE'
|
||||
PROFORMA = 'PROFORMA'
|
||||
ABONOINVOICE = 'ABONOINVOICE'
|
||||
TYPES = (
|
||||
(INVOICE, _("Invoice")),
|
||||
(AMENDMENTINVOICE, _("Amendment invoice")),
|
||||
(FEE, _("Fee")),
|
||||
(AMENDMENTFEE, _("Amendment Fee")),
|
||||
(ABONOINVOICE, _("Abono Invoice")),
|
||||
(PROFORMA, _("Pro forma")),
|
||||
)
|
||||
AMEND_MAP = {
|
||||
|
@ -100,9 +102,9 @@ class Bill(models.Model):
|
|||
|
||||
number = models.CharField(_("number"), max_length=16, unique=True, blank=True)
|
||||
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"),
|
||||
related_name='amends')
|
||||
related_name='amends', on_delete=models.SET_NULL)
|
||||
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
||||
created_on = models.DateField(_("created on"), auto_now_add=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):
|
||||
if payment is False:
|
||||
payment = self.account.paymentsources.get_default()
|
||||
context = Context({
|
||||
context = {
|
||||
'bill': self,
|
||||
'lines': self.lines.all().prefetch_related('sublines'),
|
||||
'seller': self.seller,
|
||||
|
@ -316,7 +318,7 @@ class Bill(models.Model):
|
|||
'payment': payment and payment.get_bill_context(),
|
||||
'default_due_date': self.get_due_date(payment=payment),
|
||||
'now': timezone.now(),
|
||||
})
|
||||
}
|
||||
template_name = 'BILLS_%s_TEMPLATE' % self.get_type()
|
||||
template = getattr(settings, template_name, settings.BILLS_DEFAULT_TEMPLATE)
|
||||
bill_template = loader.get_template(template)
|
||||
|
@ -392,6 +394,11 @@ class AmendmentInvoice(Bill):
|
|||
proxy = True
|
||||
|
||||
|
||||
class AbonoInvoice(Bill):
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
|
||||
class Fee(Bill):
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
@ -409,7 +416,7 @@ class ProForma(Bill):
|
|||
|
||||
class BillLine(models.Model):
|
||||
""" 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)
|
||||
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,
|
||||
|
@ -427,7 +434,7 @@ class BillLine(models.Model):
|
|||
created_on = models.DateField(_("created"), auto_now_add=True)
|
||||
# Amendment
|
||||
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:
|
||||
get_latest_by = 'id'
|
||||
|
@ -488,7 +495,7 @@ class BillSubline(models.Model):
|
|||
)
|
||||
|
||||
# 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)
|
||||
total = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
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'
|
||||
)
|
||||
|
||||
BILLS_ABONOINVOICE_NUMBER_PREFIX = Setting('BILLS_ABONOINVOICE_NUMBER_PREFIX',
|
||||
'AB'
|
||||
)
|
||||
|
||||
BILLS_FEE_NUMBER_PREFIX = Setting('BILLS_FEE_NUMBER_PREFIX',
|
||||
'F'
|
||||
|
|
|
@ -282,3 +282,17 @@ a:hover {
|
|||
#questions {
|
||||
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 %}
|
||||
<div class="wrapper">
|
||||
<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 %}
|
||||
<div id="logo">
|
||||
{% block logo %}
|
||||
|
|
|
@ -7,7 +7,7 @@ from orchestra.admin.actions import SendEmail
|
|||
from orchestra.admin.utils import insertattr, change_url
|
||||
from orchestra.contrib.accounts.actions import list_accounts
|
||||
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 .models import Contact
|
||||
|
@ -72,7 +72,7 @@ class ContactAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
if db_field.name == 'address':
|
||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||
if db_field.name == 'email_usage':
|
||||
kwargs['widget'] = paddingCheckboxSelectMultiple(130)
|
||||
kwargs['widget'] = PaddingCheckboxSelectMultiple(130)
|
||||
return super(ContactAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||
|
||||
|
||||
|
@ -101,7 +101,7 @@ class ContactInline(admin.StackedInline):
|
|||
if db_field.name == 'address':
|
||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||
if db_field.name == 'email_usage':
|
||||
kwargs['widget'] = paddingCheckboxSelectMultiple(45)
|
||||
kwargs['widget'] = PaddingCheckboxSelectMultiple(45)
|
||||
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()
|
||||
|
||||
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)
|
||||
full_name = models.CharField(_("full name"), max_length=256, blank=True)
|
||||
email = models.EmailField()
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from django.conf.urls import url
|
||||
from django.contrib import admin
|
||||
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 orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
||||
|
@ -12,6 +14,10 @@ from .filters import HasUserListFilter, HasDatabaseListFilter
|
|||
from .forms import DatabaseCreationForm, DatabaseUserChangeForm, DatabaseUserCreationForm
|
||||
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):
|
||||
list_display = ('name', 'type', 'display_users', 'account_link')
|
||||
|
@ -22,7 +28,7 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
|||
fieldsets = (
|
||||
(None, {
|
||||
'classes': ('extrapretty',),
|
||||
'fields': ('account_link', 'name', 'type', 'users', 'display_users'),
|
||||
'fields': ('account_link', 'name', 'type', 'users', 'display_users', 'comments'),
|
||||
}),
|
||||
)
|
||||
add_fieldsets = (
|
||||
|
@ -44,16 +50,16 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
|||
filter_horizontal = ['users']
|
||||
filter_by_account_fields = ('users',)
|
||||
list_prefetch_related = ('users',)
|
||||
actions = (list_accounts,)
|
||||
actions = (list_accounts, save_selected)
|
||||
|
||||
@mark_safe
|
||||
def display_users(self, db):
|
||||
links = []
|
||||
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)
|
||||
return '<br>'.join(links)
|
||||
display_users.short_description = _("Users")
|
||||
display_users.allow_tags = True
|
||||
display_users.admin_order_field = 'users__username'
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
|
@ -93,16 +99,16 @@ class DatabaseUserAdmin(SelectAccountAdminMixin, ChangePasswordAdminMixin, Exten
|
|||
readonly_fields = ('account_link', 'display_databases',)
|
||||
filter_by_account_fields = ('databases',)
|
||||
list_prefetch_related = ('databases',)
|
||||
actions = (list_accounts,)
|
||||
actions = (list_accounts, save_selected)
|
||||
|
||||
@mark_safe
|
||||
def display_databases(self, user):
|
||||
links = []
|
||||
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)
|
||||
return '<br>'.join(links)
|
||||
display_databases.short_description = _("Databases")
|
||||
display_databases.allow_tags = True
|
||||
display_databases.admin_order_field = 'databases__name'
|
||||
|
||||
def get_urls(self):
|
||||
|
|
|
@ -91,7 +91,7 @@ class DatabaseCreationForm(DatabaseUserCreationForm):
|
|||
|
||||
class ReadOnlySQLPasswordHashField(ReadOnlyPasswordHashField):
|
||||
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)
|
||||
if 'Invalid' not in original:
|
||||
return original
|
||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
import django.db.models.deletion
|
||||
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)),
|
||||
('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)),
|
||||
('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(
|
||||
|
@ -29,7 +30,7 @@ class Migration(migrations.Migration):
|
|||
('username', models.CharField(verbose_name='username', max_length=16, validators=[orchestra.core.validators.validate_name])),
|
||||
('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)),
|
||||
('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={
|
||||
'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,
|
||||
choices=settings.DATABASES_TYPE_CHOICES,
|
||||
default=settings.DATABASES_DEFAULT_TYPE)
|
||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||
related_name='databases')
|
||||
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
|
||||
verbose_name=_("Account"), related_name='databases')
|
||||
comments = models.TextField(default="", blank=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('name', 'type')
|
||||
|
@ -59,8 +60,8 @@ class DatabaseUser(models.Model):
|
|||
type = models.CharField(_("type"), max_length=32,
|
||||
choices=settings.DATABASES_TYPE_CHOICES,
|
||||
default=settings.DATABASES_DEFAULT_TYPE)
|
||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||
related_name='databaseusers')
|
||||
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
|
||||
verbose_name=_("Account"), related_name='databaseusers')
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = _("DB users")
|
||||
|
|
|
@ -20,7 +20,7 @@ DATABASES_DEFAULT_TYPE = Setting('DATABASES_DEFAULT_TYPE',
|
|||
|
||||
DATABASES_DEFAULT_HOST = Setting('DATABASES_DEFAULT_HOST',
|
||||
'localhost',
|
||||
validators=[validate_hostname],
|
||||
# validators=[validate_hostname],
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
import MySQLdb
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import MySQLdb
|
||||
from django.conf import settings as djsettings
|
||||
from django.core.management.base import CommandError
|
||||
from django.core.urlresolvers import reverse
|
||||
from selenium.webdriver.support.select import Select
|
||||
|
||||
from django.urls import reverse
|
||||
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.tests import (BaseLiveServerTestCase, random_ascii, save_response_on_error,
|
||||
snapshot_on_error)
|
||||
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii,
|
||||
save_response_on_error, snapshot_on_error)
|
||||
from selenium.webdriver.support.select import Select
|
||||
|
||||
from ... import backends, settings
|
||||
from ...models import Database, DatabaseUser
|
||||
|
||||
TEST_REST_API = int(os.getenv('TEST_REST_API', '0'))
|
||||
|
||||
|
||||
class DatabaseTestMixin(object):
|
||||
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)
|
||||
|
||||
|
||||
@unittest.skipUnless(TEST_REST_API, "REST API tests")
|
||||
class RESTDatabaseMixin(DatabaseTestMixin):
|
||||
def setUp(self):
|
||||
super(RESTDatabaseMixin, self).setUp()
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
from django.contrib import admin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.db import models
|
||||
from django.db.models.functions import Concat, Coalesce
|
||||
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 orchestra.admin import ExtendedModelAdmin
|
||||
|
@ -55,7 +57,7 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
'structured_name', 'display_is_top', 'display_websites', 'display_addresses', 'account_link'
|
||||
)
|
||||
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 = (
|
||||
'account_link', 'top_link', 'display_websites', 'display_addresses', 'implicit_records'
|
||||
)
|
||||
|
@ -72,9 +74,8 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
def structured_name(self, domain):
|
||||
if domain.is_top:
|
||||
return domain.name
|
||||
return ' '*4 + domain.name
|
||||
return mark_safe(' '*4 + domain.name)
|
||||
structured_name.short_description = _("name")
|
||||
structured_name.allow_tags = True
|
||||
structured_name.admin_order_field = 'structured_name'
|
||||
|
||||
def display_is_top(self, domain):
|
||||
|
@ -83,6 +84,7 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
display_is_top.boolean = True
|
||||
display_is_top.admin_order_field = 'top'
|
||||
|
||||
@mark_safe
|
||||
def display_websites(self, domain):
|
||||
if apps.isinstalled('orchestra.contrib.websites'):
|
||||
websites = domain.websites.all()
|
||||
|
@ -92,22 +94,22 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
site_link = get_on_site_link(website.get_absolute_url())
|
||||
admin_url = change_url(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)
|
||||
links.append(link)
|
||||
return '<br>'.join(links)
|
||||
add_url = reverse('admin:websites_website_add')
|
||||
add_url += '?account=%i&domains=%i' % (domain.account_id, domain.pk)
|
||||
image = '<img src="%s"></img>' % static('orchestra/images/add.png')
|
||||
add_link = '<a href="%s" title="%s">%s</a>' % (
|
||||
add_url, _("Add website"), image
|
||||
add_link = format_html(
|
||||
'<a href="{}" title="{}"><img src="{}" /></a>', add_url,
|
||||
_("Add website"), static('orchestra/images/add.png'),
|
||||
)
|
||||
return _("No website %s") % (add_link)
|
||||
return '---'
|
||||
display_websites.admin_order_field = 'websites__name'
|
||||
display_websites.short_description = _("Websites")
|
||||
display_websites.allow_tags = True
|
||||
|
||||
@mark_safe
|
||||
def display_addresses(self, domain):
|
||||
if apps.isinstalled('orchestra.contrib.mailboxes'):
|
||||
add_url = reverse('admin:mailboxes_address_add')
|
||||
|
@ -126,10 +128,9 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
return '---'
|
||||
display_addresses.short_description = _("Addresses")
|
||||
display_addresses.admin_order_field = 'addresses__count'
|
||||
display_addresses.allow_tags = True
|
||||
|
||||
@mark_safe
|
||||
def implicit_records(self, domain):
|
||||
defaults = []
|
||||
types = set(domain.records.values_list('type', flat=True))
|
||||
ttl = settings.DOMAINS_DEFAULT_TTL
|
||||
lines = []
|
||||
|
@ -141,14 +142,13 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
value=record.value
|
||||
)
|
||||
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:
|
||||
lines.insert(0, line)
|
||||
else:
|
||||
lines.append(line)
|
||||
return '<br>'.join(lines)
|
||||
implicit_records.short_description = _("Implicit records")
|
||||
implicit_records.allow_tags = True
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
""" Add SOA fields when domain is top """
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 orchestra.api import router
|
||||
|
@ -19,7 +19,7 @@ class DomainViewSet(AccountApiMixin, viewsets.ModelViewSet):
|
|||
qs = super(DomainViewSet, self).get_queryset()
|
||||
return qs.prefetch_related('records')
|
||||
|
||||
@detail_route()
|
||||
@action(detail=True)
|
||||
def view_zone(self, request, pk=None):
|
||||
domain = self.get_object()
|
||||
return Response({
|
||||
|
|
|
@ -102,7 +102,7 @@ class Bind9MasterDomainController(ServiceController):
|
|||
self.append(textwrap.dedent("""
|
||||
# Apply changes
|
||||
if [[ $UPDATED == 1 ]]; then
|
||||
service bind9 reload
|
||||
rm /etc/bind/master/*jnl || true; service bind9 restart
|
||||
fi""")
|
||||
)
|
||||
|
||||
|
@ -158,6 +158,7 @@ class Bind9MasterDomainController(ServiceController):
|
|||
'slaves': '; '.join(slaves) or 'none',
|
||||
'also_notify': '; '.join(slaves) + ';' if slaves else '',
|
||||
'conf_path': self.CONF_PATH,
|
||||
'dns2136_address_match_list': domain.dns2136_address_match_list
|
||||
}
|
||||
context['conf'] = textwrap.dedent("""\
|
||||
zone "%(name)s" {
|
||||
|
@ -166,6 +167,7 @@ class Bind9MasterDomainController(ServiceController):
|
|||
file "%(zone_path)s";
|
||||
allow-transfer { %(slaves)s; };
|
||||
also-notify { %(also_notify)s };
|
||||
allow-update { %(dns2136_address_match_list)s };
|
||||
notify yes;
|
||||
};""") % context
|
||||
return context
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.db.models.deletion
|
||||
import orchestra.contrib.domains.utils
|
||||
import orchestra.contrib.domains.validators
|
||||
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')),
|
||||
('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')),
|
||||
('account', models.ForeignKey(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')),
|
||||
('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(on_delete=django.db.models.deletion.CASCADE, null=True, to='domains.Domain', editable=False, related_name='subdomain_set')),
|
||||
],
|
||||
),
|
||||
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)),
|
||||
('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')),
|
||||
('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 django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import orchestra.contrib.domains.validators
|
||||
|
||||
|
||||
|
@ -20,7 +21,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='domain',
|
||||
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(
|
||||
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
|
||||
])
|
||||
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',
|
||||
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,
|
||||
help_text=_("A revision number that changes whenever this domain is updated."))
|
||||
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 "
|
||||
"servers how long they should keep the data in cache. "
|
||||
"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()
|
||||
|
||||
|
@ -314,12 +318,13 @@ class Record(models.Model):
|
|||
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,
|
||||
help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
|
||||
validators=[validators.validate_zone_interval])
|
||||
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."))
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
@ -122,3 +122,6 @@ DOMAINS_MASTERS = Setting('DOMAINS_MASTERS',
|
|||
validators=[lambda masters: list(map(validate_ip_address, masters))],
|
||||
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 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 orchestra.contrib.orchestration.models import Server, Route
|
||||
|
|
|
@ -60,7 +60,7 @@ def validate_zone_label(value):
|
|||
if not value.endswith('.'):
|
||||
msg = _("Use a fully expanded domain name ending with a dot.")
|
||||
raise ValidationError(msg)
|
||||
if len(value) > 63:
|
||||
if len(value) > 254:
|
||||
raise ValidationError(_("Labels must be 63 characters or less."))
|
||||
|
||||
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
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_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):
|
||||
|
@ -34,11 +36,12 @@ class LogEntryAdmin(admin.ModelAdmin):
|
|||
user_link = admin_link('user')
|
||||
display_action_time = admin_date('action_time', short_description=_("Time"))
|
||||
|
||||
@mark_safe
|
||||
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,)),
|
||||
'img': static('admin/img/icon-changelink.svg'),
|
||||
}
|
||||
})
|
||||
if log.is_addition():
|
||||
return _('Added "%(link)s". %(edit)s') % {
|
||||
'link': self.content_object_link(log),
|
||||
|
@ -57,7 +60,6 @@ class LogEntryAdmin(admin.ModelAdmin):
|
|||
}
|
||||
display_message.short_description = _("Message")
|
||||
display_message.admin_order_field = 'action_flag'
|
||||
display_message.allow_tags = True
|
||||
|
||||
def display_action(self, log):
|
||||
if log.is_addition():
|
||||
|
@ -75,10 +77,9 @@ class LogEntryAdmin(admin.ModelAdmin):
|
|||
url = reverse(view, args=(log.object_id,))
|
||||
except NoReverseMatch:
|
||||
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.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):
|
||||
""" Add rel_opts and object to context """
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from django import forms
|
||||
from django.conf.urls import url
|
||||
from django.contrib import admin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.db import models
|
||||
from django.http import HttpResponse
|
||||
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 markdown import markdown
|
||||
|
||||
|
@ -50,6 +51,7 @@ class MessageReadOnlyInline(admin.TabularInline):
|
|||
'all': ('orchestra/css/hide-inline-id.css',)
|
||||
}
|
||||
|
||||
@mark_safe
|
||||
def content_html(self, msg):
|
||||
context = {
|
||||
'number': msg.number,
|
||||
|
@ -58,12 +60,13 @@ class MessageReadOnlyInline(admin.TabularInline):
|
|||
}
|
||||
summary = _("#%(number)i Updated by %(author)s about %(time)s") % context
|
||||
header = '<strong style="color:#666;">%s</strong><hr />' % summary
|
||||
|
||||
content = markdown(msg.content)
|
||||
content = content.replace('>\n', '>')
|
||||
content = '<div style="padding-left:20px;">%s</div>' % content
|
||||
|
||||
return header + content
|
||||
content_html.short_description = _("Content")
|
||||
content_html.allow_tags = True
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
@ -111,10 +114,10 @@ class TicketInline(admin.TabularInline):
|
|||
colored_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
||||
colored_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
||||
|
||||
@mark_safe
|
||||
def ticket_id(self, instance):
|
||||
return '<b>%s</b>' % admin_link()(instance)
|
||||
ticket_id.short_description = '#'
|
||||
ticket_id.allow_tags = True
|
||||
|
||||
|
||||
class TicketAdmin(ExtendedModelAdmin):
|
||||
|
@ -135,7 +138,7 @@ class TicketAdmin(ExtendedModelAdmin):
|
|||
'owner__username'
|
||||
)
|
||||
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
|
||||
)
|
||||
sudo_actions = ('delete_selected',)
|
||||
|
@ -192,6 +195,7 @@ class TicketAdmin(ExtendedModelAdmin):
|
|||
display_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
||||
display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
||||
|
||||
@mark_safe
|
||||
def display_summary(self, ticket):
|
||||
context = {
|
||||
'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
|
||||
return '<h4>Added by %(creator)s about %(created)s%(updated)s</h4>' % context
|
||||
display_summary.short_description = 'Summary'
|
||||
display_summary.allow_tags = True
|
||||
|
||||
def unbold_id(self, ticket):
|
||||
""" Unbold id if ticket is read """
|
||||
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
|
||||
unbold_id.allow_tags = True
|
||||
unbold_id.short_description = "#"
|
||||
unbold_id.admin_order_field = 'id'
|
||||
|
||||
|
@ -222,8 +224,7 @@ class TicketAdmin(ExtendedModelAdmin):
|
|||
""" Bold subject when tickets are unread for request.user """
|
||||
if ticket.is_read_by(self.user):
|
||||
return ticket.subject
|
||||
return "<strong class='unread'>%s</strong>" % ticket.subject
|
||||
bold_subject.allow_tags = True
|
||||
return format_html("<strong class='unread'>{}</strong>", ticket.subject)
|
||||
bold_subject.short_description = _("Subject")
|
||||
bold_subject.admin_order_field = 'subject'
|
||||
|
||||
|
@ -297,10 +298,9 @@ class QueueAdmin(admin.ModelAdmin):
|
|||
num = queue.tickets__count
|
||||
url = reverse('admin:issues_ticket_changelist')
|
||||
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.admin_order_field = 'tickets__count'
|
||||
num_tickets.allow_tags = True
|
||||
|
||||
def get_list_display(self, request):
|
||||
""" show notifications """
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 orchestra.api import router, LogApiMixin
|
||||
|
@ -13,13 +13,13 @@ class TicketViewSet(LogApiMixin, viewsets.ModelViewSet):
|
|||
queryset = Ticket.objects.all()
|
||||
serializer_class = TicketSerializer
|
||||
|
||||
@detail_route()
|
||||
@action(detail=True)
|
||||
def mark_as_read(self, request, pk=None):
|
||||
ticket = self.get_object()
|
||||
ticket.mark_as_read_by(request.user)
|
||||
return Response({'status': 'Ticket marked as read'})
|
||||
|
||||
@detail_route()
|
||||
@action(detail=True)
|
||||
def mark_as_unread(self, request, pk=None):
|
||||
ticket = self.get_object()
|
||||
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
|
||||
|
||||
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
|
||||
textarea = super(MarkDownWidget, self).render(name, value, attrs)
|
||||
preview = ('<a class="load-preview" href="#" data-field="{0}">preview</a>'\
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import models, migrations
|
||||
import orchestra.models.fields
|
||||
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')),
|
||||
('content', models.TextField(verbose_name='content')),
|
||||
('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={
|
||||
'get_latest_by': 'id',
|
||||
|
@ -48,9 +49,9 @@ class Migration(migrations.Migration):
|
|||
('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(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')),
|
||||
('queue', models.ForeignKey(blank=True, related_name='tickets', null=True, to='issues.Queue')),
|
||||
('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(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(on_delete=django.db.models.deletion.CASCADE, blank=True, related_name='tickets', null=True, to='issues.Queue')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-updated_at'],
|
||||
|
@ -60,14 +61,14 @@ class Migration(migrations.Migration):
|
|||
name='TicketTracker',
|
||||
fields=[
|
||||
('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')),
|
||||
('user', models.ForeignKey(related_name='ticket_trackers', to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||
('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(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(
|
||||
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):
|
||||
ticket = models.ForeignKey('issues.Ticket', verbose_name=_("ticket"),
|
||||
related_name='messages')
|
||||
author = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("author"),
|
||||
related_name='ticket_messages')
|
||||
ticket = models.ForeignKey('issues.Ticket', on_delete=models.CASCADE,
|
||||
verbose_name=_("ticket"), related_name='messages')
|
||||
author = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.CASCADE,
|
||||
verbose_name=_("author"), related_name='ticket_messages')
|
||||
author_name = models.CharField(_("author name"), max_length=256, blank=True)
|
||||
content = models.TextField(_("content"))
|
||||
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
||||
|
@ -191,9 +191,10 @@ class Message(models.Model):
|
|||
|
||||
class TicketTracker(models.Model):
|
||||
""" Keeps track of user read tickets """
|
||||
ticket = models.ForeignKey(Ticket, verbose_name=_("ticket"), related_name='trackers')
|
||||
user = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("user"),
|
||||
related_name='ticket_trackers')
|
||||
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE,
|
||||
verbose_name=_("ticket"), related_name='trackers')
|
||||
user = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.CASCADE,
|
||||
verbose_name=_("user"), related_name='ticket_trackers')
|
||||
|
||||
class Meta:
|
||||
unique_together = (
|
||||
|
|
|
@ -10,7 +10,7 @@ from .serializers import ListSerializer
|
|||
class ListViewSet(LogApiMixin, AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet):
|
||||
queryset = List.objects.all()
|
||||
serializer_class = ListSerializer
|
||||
filter_fields = ('name',)
|
||||
filter_fields = ('name', 'address_domain')
|
||||
|
||||
|
||||
router.register(r'lists', ListViewSet)
|
||||
|
|
|
@ -48,20 +48,14 @@ class MailmanVirtualDomainController(ServiceController):
|
|||
|
||||
def save(self, 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):
|
||||
context = self.get_context(mail_list)
|
||||
self.exclude_virtual_alias_domain(context)
|
||||
#self.exclude_virtual_alias_domain(context)
|
||||
|
||||
def commit(self):
|
||||
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()
|
||||
|
||||
def get_context_files(self):
|
||||
|
@ -107,7 +101,7 @@ class MailmanController(MailmanVirtualDomainController):
|
|||
for suffix in self.address_suffixes:
|
||||
context['suffix'] = suffix
|
||||
# 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']:
|
||||
# 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)
|
||||
|
@ -116,84 +110,21 @@ class MailmanController(MailmanVirtualDomainController):
|
|||
def save(self, mail_list):
|
||||
context = self.get_context(mail_list)
|
||||
# Create list
|
||||
self.append(textwrap.dedent("""
|
||||
# Create list %(name)s
|
||||
[[ ! -e '%(mailman_root)s/lists/%(name)s' ]] && {
|
||||
newlist --quiet --emailhost='%(domain)s' '%(name)s' '%(admin)s' '%(password)s'
|
||||
}""") % 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)
|
||||
cmd = "/opt/mailman/venv/bin/python /usr/local/admin/orchestra_mailman3/save.py %(name)s %(admin)s %(address_name)s@%(domain)s" % context
|
||||
if not mail_list.active:
|
||||
cmd += ' --inactive'
|
||||
self.append(cmd)
|
||||
|
||||
def delete(self, mail_list):
|
||||
context = self.get_context(mail_list)
|
||||
self.exclude_virtual_alias_domain(context)
|
||||
self.append(textwrap.dedent("""
|
||||
# Remove list %(name)s
|
||||
sed -i -e '/^.*\s%(name)s\(%(suffixes_regex)s\)\s*$/d' \\
|
||||
-e 'N; /^\s*\\n\s*$/d; P; D' %(virtual_alias)s
|
||||
# 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
|
||||
)
|
||||
# Delete list
|
||||
cmd = "/opt/mailman/venv/bin/python /usr/local/admin/orchestra_mailman3/delete.py %(name)s %(admin)s %(address_name)s@%(domain)s" % context
|
||||
if not mail_list.active:
|
||||
cmd += ' --inactive'
|
||||
self.append(cmd)
|
||||
|
||||
def commit(self):
|
||||
context = self.get_context_files()
|
||||
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
|
||||
)
|
||||
pass
|
||||
|
||||
def get_context_files(self):
|
||||
return {
|
||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
import django.db.models.deletion
|
||||
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)),
|
||||
('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.')),
|
||||
('account', models.ForeignKey(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')),
|
||||
('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(on_delete=django.db.models.deletion.CASCADE, null=True, blank=True, to='domains.Domain', verbose_name='address domain')),
|
||||
],
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue