Merge branch 'Aufinal/can-delete-stuff' into 'master'

Délétions d'objets K-Fêt

See merge request klub-dev-ens/gestioCOF!359
This commit is contained in:
Martin Pepin 2019-06-03 23:06:06 +02:00
commit 7f1adf7c4e
14 changed files with 590 additions and 114 deletions

View file

@ -1,3 +1,4 @@
- On peut supprimer des comptes et des articles K-Fêt
- Passage à Django2 - Passage à Django2
- Dev : on peut désactiver la barre de debug avec une variable shell - Dev : on peut désactiver la barre de debug avec une variable shell
- Remplace les CSS de Google par des polices de proximité - Remplace les CSS de Google par des polices de proximité

View file

@ -1 +1,3 @@
default_app_config = "kfet.apps.KFetConfig" default_app_config = "kfet.apps.KFetConfig"
KFET_DELETED_TRIGRAMME = "☠☠☠"
KFET_DELETED_USERNAME = "kfet_deleted_user"

View file

@ -0,0 +1,97 @@
# Generated by Django 2.2 on 2019-05-23 13:20
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("kfet", "0065_choices_promo")]
operations = [
migrations.RemoveField(model_name="checkouttransfer", name="from_checkout"),
migrations.RemoveField(model_name="checkouttransfer", name="to_checkout"),
migrations.AlterField(
model_name="accountnegative",
name="account",
field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="negative",
to="kfet.Account",
),
),
migrations.AlterField(
model_name="checkoutstatement",
name="checkout",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="statements",
to="kfet.Checkout",
),
),
migrations.AlterField(
model_name="inventoryarticle",
name="article",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="kfet.Article"
),
),
migrations.AlterField(
model_name="inventoryarticle",
name="inventory",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="kfet.Inventory"
),
),
migrations.AlterField(
model_name="operation",
name="article",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="operations",
to="kfet.Article",
),
),
migrations.AlterField(
model_name="order",
name="supplier",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="orders",
to="kfet.Supplier",
),
),
migrations.AlterField(
model_name="orderarticle",
name="article",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="kfet.Article"
),
),
migrations.AlterField(
model_name="orderarticle",
name="order",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="kfet.Order"
),
),
migrations.AlterField(
model_name="supplierarticle",
name="article",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="kfet.Article"
),
),
migrations.AlterField(
model_name="supplierarticle",
name="supplier",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="kfet.Supplier"
),
),
migrations.DeleteModel(name="ArticleRule"),
migrations.DeleteModel(name="CheckoutTransfer"),
]

View file

@ -0,0 +1,32 @@
# Generated by Django 2.2 on 2019-05-23 13:54
from django.db import migrations, models
from kfet import KFET_DELETED_TRIGRAMME, KFET_DELETED_USERNAME
def setup_kfet_deleted_user(apps, schema_editor):
"""
Setup models instances for the kfet deleted account.
Username and trigramme are retrieved from kfet.__init__ module.
Other data are registered here.
"""
User = apps.get_model("auth", "User")
CofProfile = apps.get_model("gestioncof", "CofProfile")
Account = apps.get_model("kfet", "Account")
user, _ = User.objects.update_or_create(
username=KFET_DELETED_USERNAME, defaults={"first_name": "Compte K-Fêt supprimé"}
)
profile, _ = CofProfile.objects.update_or_create(user=user)
account, _ = Account.objects.update_or_create(
cofprofile=profile, defaults={"trigramme": KFET_DELETED_TRIGRAMME}
)
class Migration(migrations.Migration):
dependencies = [("kfet", "0066_on_delete_actions")]
operations = [migrations.RunPython(setup_kfet_deleted_user)]

View file

@ -0,0 +1,127 @@
# Generated by Django 2.2 on 2019-05-23 16:17
from django.db import migrations, models
import kfet.models
class Migration(migrations.Migration):
dependencies = [("kfet", "0067_deleted_account")]
operations = [
migrations.AlterField(
model_name="checkout",
name="created_by",
field=models.ForeignKey(
on_delete=models.SET(kfet.models.get_deleted_account),
related_name="+",
to="kfet.Account",
),
),
migrations.AlterField(
model_name="checkoutstatement",
name="by",
field=models.ForeignKey(
on_delete=models.SET(kfet.models.get_deleted_account),
related_name="+",
to="kfet.Account",
),
),
migrations.AlterField(
model_name="inventory",
name="by",
field=models.ForeignKey(
on_delete=models.SET(kfet.models.get_deleted_account),
related_name="+",
to="kfet.Account",
),
),
migrations.AlterField(
model_name="operation",
name="addcost_for",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=models.SET(kfet.models.get_deleted_account),
related_name="addcosts",
to="kfet.Account",
),
),
migrations.AlterField(
model_name="operation",
name="canceled_by",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=models.SET(kfet.models.get_deleted_account),
related_name="+",
to="kfet.Account",
),
),
migrations.AlterField(
model_name="operationgroup",
name="on_acc",
field=models.ForeignKey(
on_delete=models.SET(kfet.models.get_deleted_account),
related_name="opesgroup",
to="kfet.Account",
),
),
migrations.AlterField(
model_name="operationgroup",
name="valid_by",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=models.SET(kfet.models.get_deleted_account),
related_name="+",
to="kfet.Account",
),
),
migrations.AlterField(
model_name="transfer",
name="canceled_by",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=models.SET(kfet.models.get_deleted_account),
related_name="+",
to="kfet.Account",
),
),
migrations.AlterField(
model_name="transfer",
name="from_acc",
field=models.ForeignKey(
on_delete=models.SET(kfet.models.get_deleted_account),
related_name="transfers_from",
to="kfet.Account",
),
),
migrations.AlterField(
model_name="transfer",
name="to_acc",
field=models.ForeignKey(
on_delete=models.SET(kfet.models.get_deleted_account),
related_name="transfers_to",
to="kfet.Account",
),
),
migrations.AlterField(
model_name="transfergroup",
name="valid_by",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=models.SET(kfet.models.get_deleted_account),
related_name="+",
to="kfet.Account",
),
),
]

View file

@ -12,6 +12,7 @@ from django.utils.translation import ugettext_lazy as _
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
from . import KFET_DELETED_TRIGRAMME
from .auth import KFET_GENERIC_TRIGRAMME from .auth import KFET_GENERIC_TRIGRAMME
from .auth.models import GenericTeamToken # noqa from .auth.models import GenericTeamToken # noqa
from .config import kfet_config from .config import kfet_config
@ -151,7 +152,7 @@ class Account(models.Model):
@property @property
def readable(self): def readable(self):
return self.trigramme != "GNR" return self.trigramme not in [KFET_DELETED_TRIGRAMME, KFET_GENERIC_TRIGRAMME]
@property @property
def is_team(self): def is_team(self):
@ -267,12 +268,6 @@ class Account(models.Model):
self.password = hash_password(clear_password) self.password = hash_password(clear_password)
# Surcharge de delete
# Pas de suppression possible
# Cas à régler plus tard
def delete(self, *args, **kwargs):
pass
def update_negative(self): def update_negative(self):
if self.real_balance < 0: if self.real_balance < 0:
if hasattr(self, "negative") and not self.negative.start: if hasattr(self, "negative") and not self.negative.start:
@ -299,6 +294,10 @@ class Account(models.Model):
self.trigramme = trigramme self.trigramme = trigramme
def get_deleted_account():
return Account.objects.get(trigramme=KFET_DELETED_TRIGRAMME)
class AccountNegativeManager(models.Manager): class AccountNegativeManager(models.Manager):
"""Manager for AccountNegative model.""" """Manager for AccountNegative model."""
@ -310,7 +309,7 @@ class AccountNegative(models.Model):
objects = AccountNegativeManager() objects = AccountNegativeManager()
account = models.OneToOneField( account = models.OneToOneField(
Account, on_delete=models.PROTECT, related_name="negative" Account, on_delete=models.CASCADE, related_name="negative"
) )
start = models.DateTimeField(blank=True, null=True, default=None) start = models.DateTimeField(blank=True, null=True, default=None)
balance_offset = models.DecimalField( balance_offset = models.DecimalField(
@ -350,7 +349,9 @@ class CheckoutQuerySet(models.QuerySet):
class Checkout(models.Model): class Checkout(models.Model):
created_by = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="+") created_by = models.ForeignKey(
Account, on_delete=models.SET(get_deleted_account), related_name="+"
)
name = models.CharField(max_length=45) name = models.CharField(max_length=45)
valid_from = models.DateTimeField() valid_from = models.DateTimeField()
valid_to = models.DateTimeField() valid_to = models.DateTimeField()
@ -384,20 +385,12 @@ class Checkout(models.Model):
return ret return ret
class CheckoutTransfer(models.Model):
from_checkout = models.ForeignKey(
Checkout, on_delete=models.PROTECT, related_name="transfers_from"
)
to_checkout = models.ForeignKey(
Checkout, on_delete=models.PROTECT, related_name="transfers_to"
)
amount = models.DecimalField(max_digits=6, decimal_places=2)
class CheckoutStatement(models.Model): class CheckoutStatement(models.Model):
by = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="+") by = models.ForeignKey(
Account, on_delete=models.SET(get_deleted_account), related_name="+"
)
checkout = models.ForeignKey( checkout = models.ForeignKey(
Checkout, on_delete=models.PROTECT, related_name="statements" Checkout, on_delete=models.CASCADE, related_name="statements"
) )
balance_old = models.DecimalField( balance_old = models.DecimalField(
"ancienne balance", max_digits=6, decimal_places=2 "ancienne balance", max_digits=6, decimal_places=2
@ -526,21 +519,13 @@ class Article(models.Model):
return to_ukf(self.price) return to_ukf(self.price)
class ArticleRule(models.Model):
article_on = models.OneToOneField(
Article, on_delete=models.PROTECT, related_name="rule_on"
)
article_to = models.OneToOneField(
Article, on_delete=models.PROTECT, related_name="rule_to"
)
ratio = models.PositiveSmallIntegerField()
class Inventory(models.Model): class Inventory(models.Model):
articles = models.ManyToManyField( articles = models.ManyToManyField(
Article, through="InventoryArticle", related_name="inventories" Article, through="InventoryArticle", related_name="inventories"
) )
by = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="+") by = models.ForeignKey(
Account, on_delete=models.SET(get_deleted_account), related_name="+"
)
at = models.DateTimeField(auto_now_add=True) at = models.DateTimeField(auto_now_add=True)
# Optional # Optional
order = models.OneToOneField( order = models.OneToOneField(
@ -560,8 +545,8 @@ class Inventory(models.Model):
class InventoryArticle(models.Model): class InventoryArticle(models.Model):
inventory = models.ForeignKey(Inventory, on_delete=models.PROTECT) inventory = models.ForeignKey(Inventory, on_delete=models.CASCADE)
article = models.ForeignKey(Article, on_delete=models.PROTECT) article = models.ForeignKey(Article, on_delete=models.CASCADE)
stock_old = models.IntegerField() stock_old = models.IntegerField()
stock_new = models.IntegerField() stock_new = models.IntegerField()
stock_error = models.IntegerField(default=0) stock_error = models.IntegerField(default=0)
@ -592,8 +577,8 @@ class Supplier(models.Model):
class SupplierArticle(models.Model): class SupplierArticle(models.Model):
supplier = models.ForeignKey(Supplier, on_delete=models.PROTECT) supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE)
article = models.ForeignKey(Article, on_delete=models.PROTECT) article = models.ForeignKey(Article, on_delete=models.CASCADE)
at = models.DateTimeField(auto_now_add=True) at = models.DateTimeField(auto_now_add=True)
price_HT = models.DecimalField( price_HT = models.DecimalField(
max_digits=7, decimal_places=4, blank=True, null=True, default=None max_digits=7, decimal_places=4, blank=True, null=True, default=None
@ -608,7 +593,7 @@ class SupplierArticle(models.Model):
class Order(models.Model): class Order(models.Model):
supplier = models.ForeignKey( supplier = models.ForeignKey(
Supplier, on_delete=models.PROTECT, related_name="orders" Supplier, on_delete=models.CASCADE, related_name="orders"
) )
articles = models.ManyToManyField( articles = models.ManyToManyField(
Article, through="OrderArticle", related_name="orders" Article, through="OrderArticle", related_name="orders"
@ -621,8 +606,8 @@ class Order(models.Model):
class OrderArticle(models.Model): class OrderArticle(models.Model):
order = models.ForeignKey(Order, on_delete=models.PROTECT) order = models.ForeignKey(Order, on_delete=models.CASCADE)
article = models.ForeignKey(Article, on_delete=models.PROTECT) article = models.ForeignKey(Article, on_delete=models.CASCADE)
quantity_ordered = models.IntegerField() quantity_ordered = models.IntegerField()
quantity_received = models.IntegerField(default=0) quantity_received = models.IntegerField(default=0)
@ -633,7 +618,7 @@ class TransferGroup(models.Model):
comment = models.CharField(max_length=255, blank=True, default="") comment = models.CharField(max_length=255, blank=True, default="")
valid_by = models.ForeignKey( valid_by = models.ForeignKey(
Account, Account,
on_delete=models.PROTECT, on_delete=models.SET(get_deleted_account),
related_name="+", related_name="+",
blank=True, blank=True,
null=True, null=True,
@ -646,16 +631,18 @@ class Transfer(models.Model):
TransferGroup, on_delete=models.PROTECT, related_name="transfers" TransferGroup, on_delete=models.PROTECT, related_name="transfers"
) )
from_acc = models.ForeignKey( from_acc = models.ForeignKey(
Account, on_delete=models.PROTECT, related_name="transfers_from" Account,
on_delete=models.SET(get_deleted_account),
related_name="transfers_from",
) )
to_acc = models.ForeignKey( to_acc = models.ForeignKey(
Account, on_delete=models.PROTECT, related_name="transfers_to" Account, on_delete=models.SET(get_deleted_account), related_name="transfers_to"
) )
amount = models.DecimalField(max_digits=6, decimal_places=2) amount = models.DecimalField(max_digits=6, decimal_places=2)
# Optional # Optional
canceled_by = models.ForeignKey( canceled_by = models.ForeignKey(
Account, Account,
on_delete=models.PROTECT, on_delete=models.SET(get_deleted_account),
null=True, null=True,
blank=True, blank=True,
default=None, default=None,
@ -669,7 +656,7 @@ class Transfer(models.Model):
class OperationGroup(models.Model): class OperationGroup(models.Model):
on_acc = models.ForeignKey( on_acc = models.ForeignKey(
Account, on_delete=models.PROTECT, related_name="opesgroup" Account, on_delete=models.SET(get_deleted_account), related_name="opesgroup"
) )
checkout = models.ForeignKey( checkout = models.ForeignKey(
Checkout, on_delete=models.PROTECT, related_name="opesgroup" Checkout, on_delete=models.PROTECT, related_name="opesgroup"
@ -681,7 +668,7 @@ class OperationGroup(models.Model):
comment = models.CharField(max_length=255, blank=True, default="") comment = models.CharField(max_length=255, blank=True, default="")
valid_by = models.ForeignKey( valid_by = models.ForeignKey(
Account, Account,
on_delete=models.PROTECT, on_delete=models.SET(get_deleted_account),
related_name="+", related_name="+",
blank=True, blank=True,
null=True, null=True,
@ -717,7 +704,7 @@ class Operation(models.Model):
# Optional # Optional
article = models.ForeignKey( article = models.ForeignKey(
Article, Article,
on_delete=models.PROTECT, on_delete=models.SET_NULL,
related_name="operations", related_name="operations",
blank=True, blank=True,
null=True, null=True,
@ -726,7 +713,7 @@ class Operation(models.Model):
article_nb = models.PositiveSmallIntegerField(blank=True, null=True, default=None) article_nb = models.PositiveSmallIntegerField(blank=True, null=True, default=None)
canceled_by = models.ForeignKey( canceled_by = models.ForeignKey(
Account, Account,
on_delete=models.PROTECT, on_delete=models.SET(get_deleted_account),
related_name="+", related_name="+",
blank=True, blank=True,
null=True, null=True,
@ -735,7 +722,7 @@ class Operation(models.Model):
canceled_at = models.DateTimeField(blank=True, null=True, default=None) canceled_at = models.DateTimeField(blank=True, null=True, default=None)
addcost_for = models.ForeignKey( addcost_for = models.ForeignKey(
Account, Account,
on_delete=models.PROTECT, on_delete=models.SET(get_deleted_account),
related_name="addcosts", related_name="addcosts",
blank=True, blank=True,
null=True, null=True,

View file

@ -57,12 +57,20 @@ aside .heading .sub {
aside .buttons { aside .buttons {
margin-left: -15px; margin-left: -15px;
margin-right: -15px; margin-right: -15px;
flex-wrap: wrap;
} }
aside .buttons > * { aside .buttons > * {
flex: 0 1 auto !important; flex: 0 1 auto !important;
} }
aside .buttons > hr {
flex-basis: 100%;
height: 0;
margin: 0;
border: 0;
}
/* Aside - Text */ /* Aside - Text */

View file

@ -23,11 +23,19 @@
} }
.jconfirm .jconfirm-box .content-pane { .jconfirm .jconfirm-box .content-pane {
margin:0 !important; border-bottom:1px solid #ddd;
margin: 0px !important;
} }
.jconfirm .jconfirm-box .content { .jconfirm .jconfirm-box .content {
border-bottom:1px solid #ddd; padding: 5px;
}
.jconfirm .jconfirm-box .content div.warning {
font-size: 16px;
font-weight: bold;
text-align: center;
margin: 5px 0px;
} }
.jconfirm .jconfirm-box input { .jconfirm .jconfirm-box input {
@ -43,7 +51,7 @@
} }
.jconfirm .jconfirm-box .buttons { .jconfirm .jconfirm-box .buttons {
margin-top:-5px; /* j'arrive pas à voir pk y'a un espace au dessus sinon... */ margin-top:-6px; /* j'arrive pas à voir pk y'a un espace au dessus sinon... */
padding:0; padding:0;
height:40px; height:40px;
} }

View file

@ -1,28 +1,28 @@
function KHistory(options={}) { function KHistory(options = {}) {
$.extend(this, KHistory.default_options, options); $.extend(this, KHistory.default_options, options);
this.$container = $(this.container); this.$container = $(this.container);
this.reset = function() { this.reset = function () {
this.$container.html(''); this.$container.html('');
}; };
this.addOpeGroup = function(opegroup) { this.addOpeGroup = function (opegroup) {
var $day = this._getOrCreateDay(opegroup['at']); var $day = this._getOrCreateDay(opegroup['at']);
var $opegroup = this._opeGroupHtml(opegroup); var $opegroup = this._opeGroupHtml(opegroup);
$day.after($opegroup); $day.after($opegroup);
var trigramme = opegroup['on_acc_trigramme']; var trigramme = opegroup['on_acc_trigramme'];
var is_cof = opegroup['is_cof']; var is_cof = opegroup['is_cof'];
for (var i=0; i<opegroup['opes'].length; i++) { for (var i = 0; i < opegroup['opes'].length; i++) {
var $ope = this._opeHtml(opegroup['opes'][i], is_cof, trigramme); var $ope = this._opeHtml(opegroup['opes'][i], is_cof, trigramme);
$ope.data('opegroup', opegroup['id']); $ope.data('opegroup', opegroup['id']);
$opegroup.after($ope); $opegroup.after($ope);
} }
} }
this._opeHtml = function(ope, is_cof, trigramme) { this._opeHtml = function (ope, is_cof, trigramme) {
var $ope_html = $(this.template_ope); var $ope_html = $(this.template_ope);
var parsed_amount = parseFloat(ope['amount']); var parsed_amount = parseFloat(ope['amount']);
var amount = amountDisplay(parsed_amount, is_cof, trigramme); var amount = amountDisplay(parsed_amount, is_cof, trigramme);
@ -30,9 +30,9 @@ function KHistory(options={}) {
if (ope['type'] == 'purchase') { if (ope['type'] == 'purchase') {
infos1 = ope['article_nb']; infos1 = ope['article_nb'];
infos2 = ope['article__name']; infos2 = ope['article__name'] ? ope['article__name'] : 'Article supprimé';
} else { } else {
infos1 = parsed_amount.toFixed(2)+'€'; infos1 = parsed_amount.toFixed(2) + '€';
switch (ope['type']) { switch (ope['type']) {
case 'initial': case 'initial':
infos2 = 'Initial'; infos2 = 'Initial';
@ -58,7 +58,7 @@ function KHistory(options={}) {
var addcost_for = ope['addcost_for__trigramme']; var addcost_for = ope['addcost_for__trigramme'];
if (addcost_for) { if (addcost_for) {
var addcost_amount = parseFloat(ope['addcost_amount']); var addcost_amount = parseFloat(ope['addcost_amount']);
$ope_html.find('.addcost').text('('+amountDisplay(addcost_amount, is_cof)+'UKF pour '+addcost_for+')'); $ope_html.find('.addcost').text('(' + amountDisplay(addcost_amount, is_cof) + 'UKF pour ' + addcost_for + ')');
} }
if (ope['canceled_at']) if (ope['canceled_at'])
@ -67,26 +67,26 @@ function KHistory(options={}) {
return $ope_html; return $ope_html;
} }
this.cancelOpe = function(ope, $ope = null) { this.cancelOpe = function (ope, $ope = null) {
if (!$ope) if (!$ope)
$ope = this.findOpe(ope['id']); $ope = this.findOpe(ope['id']);
var cancel = 'Annulé'; var cancel = 'Annulé';
var canceled_at = dateUTCToParis(ope['canceled_at']); var canceled_at = dateUTCToParis(ope['canceled_at']);
if (ope['canceled_by__trigramme']) if (ope['canceled_by__trigramme'])
cancel += ' par '+ope['canceled_by__trigramme']; cancel += ' par ' + ope['canceled_by__trigramme'];
cancel += ' le '+canceled_at.format('DD/MM/YY à HH:mm:ss'); cancel += ' le ' + canceled_at.format('DD/MM/YY à HH:mm:ss');
$ope.addClass('canceled').find('.canceled').text(cancel); $ope.addClass('canceled').find('.canceled').text(cancel);
} }
this._opeGroupHtml = function(opegroup) { this._opeGroupHtml = function (opegroup) {
var $opegroup_html = $(this.template_opegroup); var $opegroup_html = $(this.template_opegroup);
var at = dateUTCToParis(opegroup['at']).format('HH:mm:ss'); var at = dateUTCToParis(opegroup['at']).format('HH:mm:ss');
var trigramme = opegroup['on_acc__trigramme']; var trigramme = opegroup['on_acc__trigramme'];
var amount = amountDisplay( var amount = amountDisplay(
parseFloat(opegroup['amount']), opegroup['is_cof'], trigramme); parseFloat(opegroup['amount']), opegroup['is_cof'], trigramme);
var comment = opegroup['comment'] || ''; var comment = opegroup['comment'] || '';
$opegroup_html $opegroup_html
@ -100,15 +100,15 @@ function KHistory(options={}) {
$opegroup_html.find('.trigramme').remove(); $opegroup_html.find('.trigramme').remove();
if (opegroup['valid_by__trigramme']) if (opegroup['valid_by__trigramme'])
$opegroup_html.find('.valid_by').text('Par '+opegroup['valid_by__trigramme']); $opegroup_html.find('.valid_by').text('Par ' + opegroup['valid_by__trigramme']);
return $opegroup_html; return $opegroup_html;
} }
this._getOrCreateDay = function(date) { this._getOrCreateDay = function (date) {
var at = dateUTCToParis(date); var at = dateUTCToParis(date);
var at_ser = at.format('YYYY-MM-DD'); var at_ser = at.format('YYYY-MM-DD');
var $day = this.$container.find('.day').filter(function() { var $day = this.$container.find('.day').filter(function () {
return $(this).data('date') == at_ser return $(this).data('date') == at_ser
}); });
if ($day.length == 1) if ($day.length == 1)
@ -117,23 +117,23 @@ function KHistory(options={}) {
return $day.data('date', at_ser).text(at.format('D MMMM')); return $day.data('date', at_ser).text(at.format('D MMMM'));
} }
this.findOpeGroup = function(id) { this.findOpeGroup = function (id) {
return this.$container.find('.opegroup').filter(function() { return this.$container.find('.opegroup').filter(function () {
return $(this).data('opegroup') == id return $(this).data('opegroup') == id
}); });
} }
this.findOpe = function(id) { this.findOpe = function (id) {
return this.$container.find('.ope').filter(function() { return this.$container.find('.ope').filter(function () {
return $(this).data('ope') == id return $(this).data('ope') == id
}); });
} }
this.cancelOpeGroup = function(opegroup) { this.cancelOpeGroup = function (opegroup) {
var $opegroup = this.findOpeGroup(opegroup['id']); var $opegroup = this.findOpeGroup(opegroup['id']);
var trigramme = $opegroup.find('.trigramme').text(); var trigramme = $opegroup.find('.trigramme').text();
var amount = amountDisplay( var amount = amountDisplay(
parseFloat(opegroup['amount'], opegroup['is_cof'], trigramme)); parseFloat(opegroup['amount'], opegroup['is_cof'], trigramme));
$opegroup.find('.amount').text(amount); $opegroup.find('.amount').text(amount);
} }

View file

@ -20,12 +20,21 @@
<a class="btn btn-default" href="{% url 'kfet.article.update' article.pk %}"> <a class="btn btn-default" href="{% url 'kfet.article.update' article.pk %}">
<span class="glyphicon glyphicon-cog"></span><span>Éditer</span> <span class="glyphicon glyphicon-cog"></span><span>Éditer</span>
</a> </a>
{% if perms.kfet.delete_account %}
<button class="btn btn-default" id="button-delete">
<span class="glyphicon glyphicon-remove"></span><span>Supprimer</span>
</button>
<form method="post" action="{% url 'kfet.article.delete' article.pk %}" id="article-delete-form">
{% csrf_token %}
</form>
{% endif %}
</div> </div>
<div class="text"> <div class="text">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li> <li>
<b>Prix:</b> <span>{{ article.price }}€</span> <b>Prix:</b> <span>{{ article.price }}€</span>
<span data-toggle="tooltip" data-placement="right" class="glyphicon glyphicon-question-sign" title="Hors réduction COF"></span> <span data-toggle="tooltip" data-placement="right" class="glyphicon glyphicon-question-sign"
title="Hors réduction COF"></span>
</li> </li>
<li><b>Stock:</b> {{ article.stock }}</li> <li><b>Stock:</b> {{ article.stock }}</li>
<li><b>En vente:</b> {{ article.is_sold|yesno|title }}</li> <li><b>En vente:</b> {{ article.is_sold|yesno|title }}</li>
@ -63,35 +72,35 @@
<div id="tab_summary" class="tab-pane fade in active"> <div id="tab_summary" class="tab-pane fade in active">
<section> <section>
<div> <div>
<div class="row"> <div class="row">
<div class="col-lg-6"> <div class="col-lg-6">
<h3>Inventaires récents</h3> <h3>Inventaires récents</h3>
<div class="table-responsive"> <div class="table-responsive">
{% include "kfet/article_inventories_snippet.html" with inventoryarts=inventoryarts|slice:5 %} {% include "kfet/article_inventories_snippet.html" with inventoryarts=inventoryarts|slice:5 %}
</div> </div>
</div><!-- col --> </div><!-- col -->
<div class="col-lg-6"> <div class="col-lg-6">
<h3>Derniers prix fournisseurs</h3> <h3>Derniers prix fournisseurs</h3>
<div class="table-responsive"> <div class="table-responsive">
{% include "kfet/article_suppliers_snippet.html" with supplierarts=supplierarts|slice:5 %} {% include "kfet/article_suppliers_snippet.html" with supplierarts=supplierarts|slice:5 %}
</div> </div>
</div><!-- col --> </div><!-- col -->
</div><!-- row --> </div><!-- row -->
</div> </div>
</section> </section>
<section> <section>
<div> <div>
<h3>Ventes</h3> <h3>Ventes</h3>
<div id="stat_last"></div> <div id="stat_last"></div>
</div> </div>
</section> </section>
</div><!-- summary tab --> </div><!-- summary tab -->
@ -110,26 +119,45 @@
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function () {
var stat_last = new StatsGroup( var stat_last = new StatsGroup(
"{% url 'kfet.article.stat.sales.list' article.id %}", "{% url 'kfet.article.stat.sales.list' article.id %}",
$("#stat_last") $("#stat_last")
); );
$('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="tooltip"]').tooltip();
$("table .more").click( function() { $("table .more").click(function () {
$(this).hide(); $(this).hide();
$(this).siblings(".hidden").removeClass("hidden"); $(this).siblings(".hidden").removeClass("hidden");
}); });
let tabs_buttons = $('.tabs-buttons a'); let tabs_buttons = $('.tabs-buttons a');
tabs_buttons.click( function() { tabs_buttons.click(function () {
tabs_buttons.removeClass('focus'); tabs_buttons.removeClass('focus');
$(this).addClass('focus'); $(this).addClass('focus');
}); });
});
// Delete button
$('#button-delete').click(function () {
$.confirm({
title: 'Confirmer la suppression',
content: `
<div class="warning">
<span class='glyphicon glyphicon-warning-sign'></span><span>Cette opération est irréversible !</span>
</div>
`,
backgroundDismiss: true,
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
confirm: function () {
$('#article-delete-form').submit();
}
})
})
});
</script> </script>
{% endblock %} {% endblock %}

View file

@ -18,6 +18,15 @@
<a class="btn btn-default" disabled> <a class="btn btn-default" disabled>
<span class="glyphicon glyphicon-credit-card"></span><span>Créditer</span> <span class="glyphicon glyphicon-credit-card"></span><span>Créditer</span>
</a> </a>
{% if perms.kfet.delete_account %}
<hr>
<button class="btn btn-default" id="button-delete">
<span class="glyphicon glyphicon-remove"></span><span>Supprimer</span>
</button>
<form method="post" action="{% url 'kfet.account.delete' account.trigramme %}" id="account-delete-form">
{% csrf_token %}
</form>
{% endif %}
</div> </div>
<div class="text"> <div class="text">
@ -92,5 +101,24 @@ $( function() {
$(this).addClass('focus'); $(this).addClass('focus');
}); });
// Delete button
$('#button-delete').click(function() {
$.confirm({
title: 'Confirmer la suppression',
content: `
<div class="warning">
<span class='glyphicon glyphicon-warning-sign'></span><span>Cette opération est irréversible !</span>
</div>
<span>Toutes les données associées à ce compte seront anonymisées.</span>
`,
backgroundDismiss: true,
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
confirm: function() {
$('#account-delete-form').submit();
}
})
})
}); });
</script> </script>

View file

@ -8,6 +8,8 @@ from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from .. import KFET_DELETED_TRIGRAMME
from ..auth import KFET_GENERIC_TRIGRAMME
from ..config import kfet_config from ..config import kfet_config
from ..models import ( from ..models import (
Account, Account,
@ -340,6 +342,61 @@ class AccountUpdateViewTests(ViewTestCaseMixin, TestCase):
self.assertForbiddenKfet(r) self.assertForbiddenKfet(r)
class AccountDeleteViewTests(ViewTestCaseMixin, TestCase):
url_name = "kfet.account.delete"
url_kwargs = {"trigramme": "001"}
url_expected = "/k-fet/accounts/001/delete"
auth_user = "team1"
auth_forbidden = [None, "user", "team"]
http_methods = ["GET", "POST"]
with_liq = True
def get_users_extra(self):
return {
"user1": create_user("user1", "001"),
"team1": create_team("team1", "101", perms=["kfet.delete_account"]),
"trez": create_user("trez", "#13"),
}
def test_get_405(self):
r = self.client.get(self.url)
self.assertEqual(r.status_code, 405)
def test_post_ok(self):
r = self.client.post(self.url, {})
self.assertRedirects(r, reverse("kfet.account"))
with self.assertRaises(Account.DoesNotExist):
self.accounts["user1"].refresh_from_db()
def test_protected_accounts(self):
for trigramme in ["LIQ", "#13", KFET_GENERIC_TRIGRAMME, KFET_DELETED_TRIGRAMME]:
if Account.objects.get(trigramme=trigramme).readable:
expected_code = 200
else:
expected_code = 403
r = self.client.post(
reverse(self.url_name, kwargs={"trigramme": trigramme}), {}
)
self.assertRedirects(
r,
reverse("kfet.account.read", kwargs={"trigramme": trigramme}),
target_status_code=expected_code,
)
# Devrait être redondant avec le précédent, mais on sait jamais
self.assertTrue(Account.objects.filter(trigramme=trigramme).exists())
def test_nonempty_accounts(self):
self.accounts["user1"].balance = 1
self.accounts["user1"].save()
r = self.client.post(self.url, {})
self.assertRedirects(r, reverse("kfet.account.read", kwargs=self.url_kwargs))
# Shouldn't throw an error
self.accounts["user1"].refresh_from_db()
class AccountGroupListViewTests(ViewTestCaseMixin, TestCase): class AccountGroupListViewTests(ViewTestCaseMixin, TestCase):
url_name = "kfet.account.group" url_name = "kfet.account.group"
url_expected = "/k-fet/accounts/groups" url_expected = "/k-fet/accounts/groups"
@ -1288,6 +1345,42 @@ class ArticleUpdateViewTests(ViewTestCaseMixin, TestCase):
self.assertForbiddenKfet(r) self.assertForbiddenKfet(r)
class ArticleDeleteViewTests(ViewTestCaseMixin, TestCase):
url_name = "kfet.article.delete"
auth_user = "team1"
auth_forbidden = [None, "user", "team"]
@property
def url_kwargs(self):
return {"pk": self.article.pk}
@property
def url_expected(self):
return "/k-fet/articles/{}/delete".format(self.article.pk)
def get_users_extra(self):
return {"team1": create_team("team1", "101", perms=["kfet.delete_article"])}
def setUp(self):
super().setUp()
self.category = ArticleCategory.objects.create(name="Category")
self.article = Article.objects.create(
name="Article", category=self.category, stock=5, price=Decimal("2.5")
)
def test_get_redirects(self):
r = self.client.get(self.url)
self.assertRedirects(r, reverse("kfet.article.read", kwargs=self.url_kwargs))
def test_post_ok(self):
r = self.client.post(self.url, {})
self.assertRedirects(r, reverse("kfet.article"))
with self.assertRaises(Article.DoesNotExist):
self.article.refresh_from_db()
class ArticleStatSalesListViewTests(ViewTestCaseMixin, TestCase): class ArticleStatSalesListViewTests(ViewTestCaseMixin, TestCase):
url_name = "kfet.article.stat.sales.list" url_name = "kfet.article.stat.sales.list"

View file

@ -57,6 +57,12 @@ urlpatterns = [
views.account_update, views.account_update,
name="kfet.account.update", name="kfet.account.update",
), ),
# Account - Delete
path(
"accounts/<trigramme:trigramme>/delete",
views.AccountDelete.as_view(),
name="kfet.account.delete",
),
# Account - Groups # Account - Groups
path("accounts/groups", views.account_group, name="kfet.account.group"), path("accounts/groups", views.account_group, name="kfet.account.group"),
path( path(
@ -180,6 +186,12 @@ urlpatterns = [
teamkfet_required(views.ArticleUpdate.as_view()), teamkfet_required(views.ArticleUpdate.as_view()),
name="kfet.article.update", name="kfet.article.update",
), ),
# Article - Delete
path(
"articles/<int:pk>/delete",
views.ArticleDelete.as_view(),
name="kfet.article.delete",
),
# Article - Statistics # Article - Statistics
path( path(
"articles/<int:pk>/stat/sales/list", "articles/<int:pk>/stat/sales/list",

View file

@ -7,6 +7,7 @@ from urllib.parse import urlencode
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.models import Permission, User from django.contrib.auth.models import Permission, User
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
@ -20,10 +21,10 @@ from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.generic import DetailView, FormView, ListView, TemplateView from django.views.generic import DetailView, FormView, ListView, TemplateView
from django.views.generic.detail import BaseDetailView from django.views.generic.detail import BaseDetailView
from django.views.generic.edit import CreateView, UpdateView from django.views.generic.edit import CreateView, DeleteView, UpdateView
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
from kfet import consumers from kfet import KFET_DELETED_TRIGRAMME, consumers
from kfet.config import kfet_config from kfet.config import kfet_config
from kfet.decorators import teamkfet_required from kfet.decorators import teamkfet_required
from kfet.forms import ( from kfet.forms import (
@ -78,6 +79,7 @@ from kfet.models import (
) )
from kfet.statistic import ScaleMixin, WeekScale, last_stats_manifest from kfet.statistic import ScaleMixin, WeekScale, last_stats_manifest
from .auth import KFET_GENERIC_TRIGRAMME
from .auth.views import ( # noqa from .auth.views import ( # noqa
AccountGroupCreate, AccountGroupCreate,
AccountGroupUpdate, AccountGroupUpdate,
@ -467,6 +469,43 @@ def account_update(request, trigramme):
}, },
) )
# Account - Delete
class AccountDelete(PermissionRequiredMixin, DeleteView):
model = Account
slug_field = "trigramme"
slug_url_kwarg = "trigramme"
success_url = reverse_lazy("kfet.account")
success_message = "Compte supprimé avec succès !"
permission_required = "kfet.delete_account"
http_method_names = ["post"]
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.balance >= 0.01:
messages.error(
request,
"Impossible de supprimer un compte "
"avec une balance strictement positive !",
)
return redirect("kfet.account.read", self.object.trigramme)
if self.object.trigramme in [
"LIQ",
KFET_GENERIC_TRIGRAMME,
KFET_DELETED_TRIGRAMME,
"#13",
]:
messages.error(request, "Impossible de supprimer un trigramme protégé !")
return redirect("kfet.account.read", self.object.trigramme)
# SuccessMessageMixin does not work with DeleteView, see :
# https://code.djangoproject.com/ticket/21926
messages.success(request, self.success_message)
return super().delete(request, *args, **kwargs)
class AccountNegativeList(ListView): class AccountNegativeList(ListView):
queryset = AccountNegative.objects.select_related( queryset = AccountNegative.objects.select_related(
@ -837,6 +876,20 @@ class ArticleUpdate(SuccessMessageMixin, UpdateView):
return super().form_valid(form) return super().form_valid(form)
class ArticleDelete(PermissionRequiredMixin, DeleteView):
model = Article
success_url = reverse_lazy("kfet.article")
success_message = "Article supprimé avec succès !"
permission_required = "kfet.delete_article"
def get(self, request, *args, **kwargs):
return redirect("kfet.article.read", self.kwargs.get(self.pk_url_kwarg))
def delete(self, request, *args, **kwargs):
messages.success(request, self.success_message)
return super().delete(request, *args, **kwargs)
# ----- # -----
# K-Psul # K-Psul
# ----- # -----