Merge branch 'stock' into 'master'

Stock

See merge request !65
This commit is contained in:
Skia 2017-04-25 16:17:24 +02:00
commit c0531feb27
22 changed files with 1811 additions and 549 deletions

View File

@ -34,9 +34,18 @@
{% endif %}
{% for b in settings.SITH_COUNTER_BARS %}
{% if user.is_in_group(b[1]+" admin") %}
<li><a href="{{ url('counter:details', counter_id=b[0]) }}">{{ b[1] }}</a> -
<li>
<a href="{{ url('counter:details', counter_id=b[0]) }}">{{ b[1] }}</a> -
<a href="{{ url('counter:admin', counter_id=b[0]) }}">{% trans %}Edit{% endtrans %}</a> -
<a href="{{ url('counter:stats', counter_id=b[0]) }}">{% trans %}Stats{% endtrans %}</a></li>
<a href="{{ url('counter:stats', counter_id=b[0]) }}">{% trans %}Stats{% endtrans %}</a> -
{% set c = Counter.objects.filter(id=b[0]).first() %}
{% if c.stock %}
<a href="{{ url('stock:items_list', stock_id=c.stock.id)}}">Stock</a> -
<a href="{{ url('stock:shoppinglist_list', stock_id=c.stock.id)}}">{% trans %}Shopping lists{% endtrans %}</a>
{% else %}
<a href="{{url('stock:new', counter_id=c.id)}}">{% trans %}Create new stock{% endtrans%}</a>
{% endif %}
</li>
{% endif %}
{% endfor %}
</ul>

View File

@ -32,6 +32,12 @@
<a href="{{ url('counter:admin', counter_id=c.id) }}">{% trans %}Edit{% endtrans %}</a> -
<a href="{{ url('counter:stats', counter_id=c.id) }}">{% trans %}Stats{% endtrans %}</a> -
{% endif %}
{% if c.stock %}
<a href="{{ url('stock:items_list', stock_id=c.stock.id)}}">Stock</a> -
<a href="{{ url('stock:shoppinglist_list', stock_id=c.stock.id)}}">{% trans %}Shopping lists{% endtrans %}</a> -
{% else %}
<a href="{{url('stock:new', counter_id=c.id)}}">{% trans %}Create new stock{% endtrans%}</a> -
{% endif %}
{% if user.is_owner(c) %}
<a href="{{ url('counter:prop_admin', counter_id=c.id) }}">{% trans %}Props{% endtrans %}</a>
{% endif %}

View File

@ -118,25 +118,37 @@ class RefillForm(forms.ModelForm):
class CounterTabsMixin(TabedViewMixin):
def get_tabs_title(self):
return self.object
if hasattr(self.object, 'stock_owner') :
return self.object.stock_owner.counter
else:
return self.object
def get_list_of_tabs(self):
tab_list = []
tab_list.append({
'url': reverse_lazy('counter:details', kwargs={'counter_id': self.object.id}),
'url': reverse_lazy('counter:details',
kwargs={'counter_id': self.object.stock_owner.counter.id if hasattr(self.object, 'stock_owner') else self.object.id }),
'slug': 'counter',
'name': _("Counter"),
})
if self.object.type == "BAR":
if self.object.stock_owner.counter.type if hasattr(self.object, 'stock_owner') else self.object.type == "BAR":
tab_list.append({
'url': reverse_lazy('counter:cash_summary', kwargs={'counter_id': self.object.id}),
'url': reverse_lazy('counter:cash_summary',
kwargs={'counter_id': self.object.stock_owner.counter.id if hasattr(self.object, 'stock_owner') else self.object.id}),
'slug': 'cash_summary',
'name': _("Cash summary"),
})
tab_list.append({
'url': reverse_lazy('counter:last_ops', kwargs={'counter_id': self.object.id}),
'url': reverse_lazy('counter:last_ops',
kwargs={'counter_id': self.object.stock_owner.counter.id if hasattr(self.object, 'stock_owner') else self.object.id}),
'slug': 'last_ops',
'name': _("Last operations"),
})
tab_list.append({
'url': reverse_lazy('stock:take_items',
kwargs={'stock_id': self.object.stock.id if hasattr(self.object, 'stock') else self.object.stock_owner.id}),
'slug': 'take_items_from_stock',
'name': _("Take items from stock"),
})
return tab_list
class CounterMain(CounterTabsMixin, CanViewMixin, DetailView, ProcessFormView, FormMixin):
@ -507,6 +519,11 @@ class CounterLogout(RedirectView):
class CounterAdminTabsMixin(TabedViewMixin):
tabs_title = _("Counter administration")
list_of_tabs = [
{
'url': reverse_lazy('stock:list'),
'slug': 'stocks',
'name': _("Stocks"),
},
{
'url': reverse_lazy('counter:admin_list'),
'slug': 'counters',

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -85,6 +85,7 @@ INSTALLED_APPS = (
'com',
'election',
'forum',
'stock',
)
MIDDLEWARE_CLASSES = (

View File

@ -58,6 +58,7 @@ urlpatterns = [
url(r'^com/', include('com.urls', namespace="com", app_name="com")),
url(r'^club/', include('club.urls', namespace="club", app_name="club")),
url(r'^counter/', include('counter.urls', namespace="counter", app_name="counter")),
url(r'^stock/', include('stock.urls', namespace="stock", app_name="stock")),
url(r'^accounting/', include('accounting.urls', namespace="accounting", app_name="accounting")),
url(r'^eboutic/', include('eboutic.urls', namespace="eboutic", app_name="eboutic")),
url(r'^launderette/', include('launderette.urls', namespace="launderette", app_name="launderette")),

25
stock/__init__.py Normal file
View File

@ -0,0 +1,25 @@
# -*- coding:utf-8 -*
#
# Copyright 2016,2017
# - Guillaume "Lo-J" Renaud <renaudg779@gmail.com>
# - Skia <skia@libskia.so>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#

34
stock/admin.py Normal file
View File

@ -0,0 +1,34 @@
# -*- coding:utf-8 -*
#
# Copyright 2016,2017
# - Guillaume "Lo-J" Renaud <renaudg779@gmail.com>
# - Skia <skia@libskia.so>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from django.contrib import admin
from stock.models import Stock, StockItem, ShoppingList, ShoppingListItem
# Register your models here.
admin.site.register(Stock)
admin.site.register(StockItem)
admin.site.register(ShoppingList)
admin.site.register(ShoppingListItem)

View File

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('counter', '0011_auto_20161004_2039'),
]
operations = [
migrations.CreateModel(
name='ShoppingList',
fields=[
('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
('date', models.DateTimeField(verbose_name='date')),
('name', models.CharField(max_length=64, verbose_name='name')),
('todo', models.BooleanField(verbose_name='todo')),
('comment', models.TextField(verbose_name='comment', blank=True, null=True)),
],
),
migrations.CreateModel(
name='ShoppingListItem',
fields=[
('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
('name', models.CharField(max_length=64, verbose_name='name')),
('tobuy_quantity', models.IntegerField(verbose_name='quantity to buy', help_text='quantity to buy during the next shopping session', default=6)),
('bought_quantity', models.IntegerField(verbose_name='quantity bought', help_text='quantity bought during the last shopping session', default=0)),
('shopping_lists', models.ManyToManyField(verbose_name='shopping lists', related_name='shopping_items_to_buy', to='stock.ShoppingList')),
],
),
migrations.CreateModel(
name='Stock',
fields=[
('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
('name', models.CharField(max_length=64, verbose_name='name')),
('counter', models.OneToOneField(verbose_name='counter', related_name='stock', to='counter.Counter')),
],
),
migrations.CreateModel(
name='StockItem',
fields=[
('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
('name', models.CharField(max_length=64, verbose_name='name')),
('unit_quantity', models.IntegerField(verbose_name='unit quantity', help_text='number of element in one box', default=0)),
('effective_quantity', models.IntegerField(verbose_name='effective quantity', help_text='number of box', default=0)),
('minimal_quantity', models.IntegerField(verbose_name='minimal quantity', help_text='if the effective quantity is less than the minimal, item is added to the shopping list', default=1)),
('stock_owner', models.ForeignKey(related_name='items', to='stock.Stock')),
('type', models.ForeignKey(blank=True, null=True, verbose_name='type', related_name='stock_items', on_delete=django.db.models.deletion.SET_NULL, to='counter.ProductType')),
],
),
migrations.AddField(
model_name='shoppinglistitem',
name='stockitem_owner',
field=models.ForeignKey(null=True, related_name='shopping_item', to='stock.StockItem'),
),
migrations.AddField(
model_name='shoppinglistitem',
name='type',
field=models.ForeignKey(blank=True, null=True, verbose_name='type', related_name='shoppinglist_items', on_delete=django.db.models.deletion.SET_NULL, to='counter.ProductType'),
),
migrations.AddField(
model_name='shoppinglist',
name='stock_owner',
field=models.ForeignKey(null=True, related_name='shopping_lists', to='stock.Stock'),
),
]

View File

111
stock/models.py Normal file
View File

@ -0,0 +1,111 @@
# -*- coding:utf-8 -*
#
# Copyright 2016,2017
# - Guillaume "Lo-J" Renaud <renaudg779@gmail.com>
# - Skia <skia@libskia.so>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse
from django.conf import settings
from counter.models import Counter, ProductType
class Stock(models.Model):
"""
The Stock class, this one is used to know how many products are left for a specific counter
"""
name = models.CharField(_('name'), max_length=64)
counter = models.OneToOneField(Counter, verbose_name=_('counter'), related_name='stock')
def __str__(self):
return "%s (%s)" % (self.name, self.counter)
def get_absolute_url(self):
return reverse('stock:list')
def can_be_viewed_by(self, user):
return user.is_in_group(settings.SITH_GROUP_COUNTER_ADMIN_ID)
class StockItem(models.Model):
"""
The StockItem class, element of the stock
"""
name = models.CharField(_('name'), max_length=64)
unit_quantity = models.IntegerField(_('unit quantity'), default=0, help_text=_('number of element in one box'))
effective_quantity = models.IntegerField(_('effective quantity'), default=0, help_text=_('number of box'))
minimal_quantity = models.IntegerField(_('minimal quantity'), default=1,
help_text=_('if the effective quantity is less than the minimal, item is added to the shopping list'))
type = models.ForeignKey(ProductType, related_name="stock_items", verbose_name=_("type"), null=True, blank=True,
on_delete=models.SET_NULL)
stock_owner = models.ForeignKey(Stock, related_name="items")
def __str__(self):
return "%s" % (self.name)
def get_absolute_url(self):
return reverse('stock:items_list', kwargs={'stock_id':self.stock_owner.id})
def can_be_viewed_by(self, user):
return user.is_in_group(settings.SITH_GROUP_COUNTER_ADMIN_ID)
class ShoppingList(models.Model):
"""
The ShoppingList class, used to make an history of the shopping lists
"""
date = models.DateTimeField(_('date'))
name = models.CharField(_('name'), max_length=64)
todo = models.BooleanField(_('todo'))
comment = models.TextField(_('comment'), null=True, blank=True)
stock_owner = models.ForeignKey(Stock, null=True, related_name="shopping_lists")
def __str__(self):
return "%s (%s)" % (self.name, self.date)
def get_absolute_url(self):
return reverse('stock:shoppinglist_list')
def can_be_viewed_by(self, user):
return user.is_in_group(settings.SITH_GROUP_COUNTER_ADMIN_ID)
class ShoppingListItem(models.Model):
"""
"""
shopping_lists = models.ManyToManyField(ShoppingList, verbose_name=_("shopping lists"), related_name="shopping_items_to_buy")
stockitem_owner = models.ForeignKey(StockItem, related_name="shopping_item", null=True)
name = models.CharField(_('name'), max_length=64)
type = models.ForeignKey(ProductType, related_name="shoppinglist_items", verbose_name=_("type"), null=True, blank=True,
on_delete=models.SET_NULL)
tobuy_quantity = models.IntegerField(_('quantity to buy'), default=6, help_text=_("quantity to buy during the next shopping session"))
bought_quantity = models.IntegerField(_('quantity bought'), default=0, help_text=_("quantity bought during the last shopping session"))
def __str__(self):
return "%s - %s" % (self.name, self.shopping_lists.first())
def can_be_viewed_by(self, user):
return user.is_in_group(settings.SITH_GROUP_COUNTER_ADMIN_ID)
def get_absolute_url(self):
return reverse('stock:shoppinglist_list')

View File

@ -0,0 +1,51 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}{{ shoppinglist }}'s items{% endtrans %}
{% endblock %}
{% block content %}
{% if current_tab == "stocks" %}
<a href="{{ url('stock:shoppinglist_list', stock_id=shoppinglist.stock_owner.id)}}">{% trans %}Back{% endtrans %}</a>
{% endif %}
<h3>{{ shoppinglist.name }}</h3>
{% for t in ProductType.objects.order_by('name').all() %}
{% if shoppinglist.shopping_items_to_buy.filter(type=t) %}
<h4>{{ t }}</h4>
<br>
<table>
<thead>
<tr>
<td>{% trans %}Name{% endtrans %}</td>
<td>{% trans %}Quantity asked{% endtrans %}</td>
<td>{% trans %}Quantity bought{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for i in shoppinglist.shopping_items_to_buy.filter(type=t).order_by('name').all() %}
<tr>
<td>{{ i.name }}</td>
<td>{{ i.tobuy_quantity }}</td>
<td>{{ i.bought_quantity }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endfor %}
<h4>{% trans %}Other{% endtrans %}</h4>
<br>
<table>
<thead>
<tr>
<td>{% trans %}Comments{% endtrans %}</td>
</tr>
</thead>
<tbody>
<tr>
<td>{{ shoppinglist.comment }}</td>
</tr>
</tbody>
</table>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans s = stock %}{{ s }}'s quantity to buy{% endtrans %}
{% endblock %}
{% block content %}
<h3>{% trans s = stock %}{{ s }}'s quantity to buy{% endtrans %}</h3>
<div>
<form method="post" action="" class="inline" style="display:inline">
{% csrf_token %}
{{ form.as_p() }}
<p><input type="submit" value="{% trans %}Create shopping list{% endtrans %}" /></p>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,32 @@
{% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link %}
{% block title %}
{{ stock }}
{% endblock %}
{% block content %}
{% if current_tab == "stocks" %}
<p><a href="{{ url('stock:new_item', stock_id=stock.id)}}">{% trans %}New item{% endtrans %}</a></p>
<h5><a href="{{ url('stock:shoppinglist_list', stock_id=stock.id)}}">{% trans %}Shopping lists{% endtrans %}</a></h5>
{% endif %}
{% if stock %}
<h3>{{ stock }}</h3>
{% for t in ProductType.objects.order_by('name') %}
<h4>{{ t }}</h4>
<ul>
{% for i in stock.items.filter(type=t).order_by('name') %}
<li><a href="{{ url('stock:edit_item', item_id=i.id)}}">{{ i }} ({{ i.effective_quantity }} {% trans %}left{% endtrans %})</a></li>
{% endfor %}
</ul>
{% endfor %}
<h4>{% trans %}Others{% endtrans %}</h4>
<ul>
{% for i in stock.items.filter(type=None).order_by('name') %}
<li><a href="{{ url('stock:edit_item', item_id=i.id)}}">{{ i }} ({{ i.effective_quantity }} {% trans %}left{% endtrans %})</a></li>
{% endfor %}
</ul>
{% else %}
{% trans %}There is no items in this stock.{% endtrans %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,24 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Stock list{% endtrans %}
{% endblock %}
{% block content %}
{% if stock_list %}
<h3>{% trans %}Stock list{% endtrans %}</h3>
<ul>
{% for s in stock_list.order_by('name') %}
<li>
{% if user.can_edit(s) %}
<a href="{{ url('stock:items_list', stock_id=s.id) }}">{{ s }}</a>
- <a href="{{ url('stock:edit', stock_id=s.id) }}">Edit</a>
- <a href="{{ url('stock:shoppinglist_list', stock_id=s.id)}}">{% trans %}Shopping lists{% endtrans %}</a>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
{% trans %}There is no stocks in this website.{% endtrans %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,75 @@
{% extends "core/base.jinja" %}
{% block title %}
Shopping list for {{ stock }}
{% endblock %}
{% block content %}
{% if current_tab == "stocks" %}
<a href="{{ url('stock:shoppinglist_create', stock_id=stock.id)}}">{% trans %}Create shopping list{% endtrans %}</a>
{% endif %}
<h3>{% trans s=stock %}Shopping lists history for {{ s }}{% endtrans %}</h3>
<p>
{% trans %}Information :{% endtrans %}
<br>
{% trans %}Use the "update stock" action when you get back from shopping to add the effective quantity bought for each shopping list item.{% endtrans %}
<br>
{% trans %}For example, 3 Cheeseburger (boxes) are aksing in the list, but there were only 2 so, 2 have to be added in the stock quantity.{% endtrans %}
</p>
<h4>{% trans %}To do{% endtrans %}</h4>
<table>
<thead>
<tr>
<td>{% trans %}Date{% endtrans %}</td>
<td>{% trans %}Name{% endtrans %}</td>
<td>{% trans %}Number of items{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for s in stock.shopping_lists.filter(todo=True).filter(stock_owner=stock).order_by('-date').all() %}
<tr>
<td>{{ s.date|localtime|date("Y-m-d H:i") }}</td>
<td><a href="{{ url('stock:shoppinglist_items', stock_id=stock.id, shoppinglist_id=s.id)}}">{{ s.name }}</a></td>
<td>{{ s.shopping_items_to_buy.count() }}</td>
<td>
<a href="{{ url('stock:update_after_shopping', stock_id=stock.id, shoppinglist_id=s.id)}}">{% trans %}Update stock{% endtrans %}</a>
</td>
<td>
<a href="{{ url('stock:shoppinglist_set_done', stock_id=stock.id, shoppinglist_id=s.id)}}">{% trans %}Mark as done{% endtrans %}</a>
</td>
<td>
<a href="{{ url('stock:shoppinglist_delete', stock_id=stock.id, shoppinglist_id=s.id)}}">{% trans %}Delete{% endtrans %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<h4>{% trans %}Done{% endtrans %}</h4>
<table>
<thead>
<tr>
<td>{% trans %}Date{% endtrans %}</td>
<td>{% trans %}Name{% endtrans %}</td>
<td>{% trans %}Number of items{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for s in stock.shopping_lists.filter(todo=False).filter(stock_owner=stock).order_by('-date').all() %}
<tr>
<td>{{ s.date|localtime|date("Y-m-d H:i") }}</td>
<td><a href="{{ url('stock:shoppinglist_items', stock_id=stock.id, shoppinglist_id=s.id)}}">{{ s.name }}</a></td>
<td>{{ s.shopping_items_to_buy.count() }}</td>
<td>
<a href="{{ url('stock:shoppinglist_set_todo', stock_id=stock.id, shoppinglist_id=s.id)}}">{% trans %}Mark as to do{% endtrans %}</a>
</td>
<td>
<a href="{{ url('stock:shoppinglist_delete', stock_id=stock.id, shoppinglist_id=s.id)}}">{% trans %}Delete{% endtrans %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link %}
{% block title %}
{% trans s = stock %}Take items from {{ s }}{% endtrans %}
{% endblock %}
{% block content %}
<h3>{% trans s = stock %}Take items from {{ s }}{% endtrans %}</h3>
<div>
<form method="post" action="" class="inline" style="display:inline">
{% csrf_token %}
{{ form.as_p() }}
<p><input type="submit" value="{% trans %}Take items{% endtrans %}" /></p>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans s = shoppinglist %}Update {{ s }}'s quantity after shopping{% endtrans %}
{% endblock %}
{% block content %}
<h3>{% trans s = shoppinglist %}Update {{ s }}'s quantity after shopping{% endtrans %}</h3>
<div>
<form method="post" action="" class="inline" style="display:inline">
{% csrf_token %}
{{ form.as_p() }}
<p><input type="submit" value="{% trans %}Update stock quantities{% endtrans %}" /></p>
</form>
</div>
{% endblock %}

28
stock/tests.py Normal file
View File

@ -0,0 +1,28 @@
# -*- coding:utf-8 -*
#
# Copyright 2016,2017
# - Guillaume "Lo-J" Renaud <renaudg779@gmail.com>
# - Skia <skia@libskia.so>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from django.test import TestCase
# Create your tests here.

55
stock/urls.py Normal file
View File

@ -0,0 +1,55 @@
# -*- coding:utf-8 -*
#
# Copyright 2016,2017
# - Guillaume "Lo-J" Renaud <renaudg779@gmail.com>
# - Skia <skia@libskia.so>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from django.conf.urls import include, url
from stock.views import *
urlpatterns = [
#Stock urls
url(r'^new/counter/(?P<counter_id>[0-9]+)$', StockCreateView.as_view(), name='new'),
url(r'^edit/(?P<stock_id>[0-9]+)$', StockEditView.as_view(), name='edit'),
url(r'^list$', StockListView.as_view(), name='list'),
# StockItem urls
url(r'^(?P<stock_id>[0-9]+)$', StockItemList.as_view(), name='items_list'),
url(r'^(?P<stock_id>[0-9]+)/stock_item/new_item$', StockItemCreateView.as_view(), name='new_item'),
url(r'^stock_item/(?P<item_id>[0-9]+)/edit$', StockItemEditView.as_view(), name='edit_item'),
url(r'^(?P<stock_id>[0-9]+)/stock_item/take_items$', StockTakeItemsBaseFormView.as_view(), name='take_items'),
# ShoppingList urls
url(r'^(?P<stock_id>[0-9]+)/shopping_list/list$', StockShoppingListView.as_view(), name='shoppinglist_list'),
url(r'^(?P<stock_id>[0-9]+)/shopping_list/create$', StockItemQuantityBaseFormView.as_view(), name='shoppinglist_create'),
url(r'^(?P<stock_id>[0-9]+)/shopping_list/(?P<shoppinglist_id>[0-9]+)/items$', StockShoppingListItemListView.as_view(),
name='shoppinglist_items'),
url(r'^(?P<stock_id>[0-9]+)/shopping_list/(?P<shoppinglist_id>[0-9]+)/delete$', StockShoppingListDeleteView.as_view(),
name='shoppinglist_delete'),
url(r'^(?P<stock_id>[0-9]+)/shopping_list/(?P<shoppinglist_id>[0-9]+)/set_done$', StockShopppingListSetDone.as_view(),
name='shoppinglist_set_done'),
url(r'^(?P<stock_id>[0-9]+)/shopping_list/(?P<shoppinglist_id>[0-9]+)/set_todo$', StockShopppingListSetTodo.as_view(),
name='shoppinglist_set_todo'),
url(r'^(?P<stock_id>[0-9]+)/shopping_list/(?P<shoppinglist_id>[0-9]+)/update_stock$', StockUpdateAfterShopppingBaseFormView.as_view(),
name='update_after_shopping'),
]

442
stock/views.py Normal file
View File

@ -0,0 +1,442 @@
# -*- coding:utf-8 -*
#
# Copyright 2016,2017
# - Guillaume "Lo-J" Renaud <renaudg779@gmail.com>
# - Skia <skia@libskia.so>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from collections import OrderedDict
from datetime import datetime, timedelta
from django.utils import timezone
from django.shortcuts import render, get_object_or_404
from django.views.generic import ListView, DetailView, RedirectView, TemplateView
from django.views.generic.edit import UpdateView, CreateView, DeleteView, ProcessFormView, FormMixin, BaseFormView
from django.utils.translation import ugettext_lazy as _
from django import forms
from django.http import HttpResponseRedirect, HttpResponse
from django.forms.models import modelform_factory
from django.core.urlresolvers import reverse_lazy, reverse
from django.db import transaction, DataError
from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMixin, TabedViewMixin
from counter.views import CounterAdminTabsMixin, CounterTabsMixin
from counter.models import Counter, ProductType
from stock.models import Stock, StockItem, ShoppingList, ShoppingListItem
class StockItemList(CounterAdminTabsMixin, CanCreateMixin, ListView):
"""
The stockitems list view for the counter owner
"""
model = Stock
template_name = 'stock/stock_item_list.jinja'
pk_url_kwarg = "stock_id"
current_tab = "stocks"
def get_context_data(self):
ret = super(StockItemList, self).get_context_data()
if 'stock_id' in self.kwargs.keys():
ret['stock'] = Stock.objects.filter(id=self.kwargs['stock_id']).first();
return ret
class StockListView(CounterAdminTabsMixin, CanViewMixin, ListView):
"""
A list view for the admins
"""
model = Stock
template_name = 'stock/stock_list.jinja'
current_tab = "stocks"
class StockEditForm(forms.ModelForm):
"""
A form to change stock's characteristics
"""
class Meta:
model = Stock
fields = ['name', 'counter']
def __init__(self, *args, **kwargs):
super(StockEditForm, self).__init__(*args, **kwargs)
def save(self, *args, **kwargs):
return super(StockEditForm, self).save(*args, **kwargs)
class StockEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView):
"""
An edit view for the stock
"""
model = Stock
form_class = modelform_factory(Stock, fields=['name', 'counter'])
pk_url_kwarg = "stock_id"
template_name = 'core/edit.jinja'
current_tab = "stocks"
class StockItemEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView):
"""
An edit view for a stock item
"""
model = StockItem
form_class = modelform_factory(StockItem, fields=['name', 'unit_quantity', 'effective_quantity', 'minimal_quantity', 'type', 'stock_owner'])
pk_url_kwarg = "item_id"
template_name = 'core/edit.jinja'
current_tab = "stocks"
class StockCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView):
"""
A create view for a new Stock
"""
model = Stock
form_class = modelform_factory(Stock, fields=['name', 'counter'])
template_name = 'core/create.jinja'
pk_url_kwarg = "counter_id"
current_tab = "stocks"
success_url = reverse_lazy('stock:list')
def get_initial(self):
ret = super(StockCreateView, self).get_initial()
if 'counter_id' in self.kwargs.keys():
ret['counter'] = self.kwargs['counter_id']
return ret
class StockItemCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView):
"""
A create view for a new StockItem
"""
model = StockItem
form_class = modelform_factory(StockItem, fields=['name', 'unit_quantity', 'effective_quantity', 'minimal_quantity', 'type', 'stock_owner'])
template_name = 'core/create.jinja'
pk_url_kwarg = "stock_id"
current_tab = "stocks"
def get_initial(self):
ret = super(StockItemCreateView, self).get_initial()
if 'stock_id' in self.kwargs.keys():
ret['stock_owner'] = self.kwargs['stock_id']
return ret
def get_success_url(self):
return reverse_lazy('stock:items_list', kwargs={'stock_id':self.object.stock_owner.id})
class StockShoppingListView(CounterAdminTabsMixin, CanViewMixin, ListView):
"""
A list view for the people to know the item to buy
"""
model = Stock
template_name = "stock/stock_shopping_list.jinja"
pk_url_kwarg = "stock_id"
current_tab = "stocks"
def get_context_data(self):
ret = super(StockShoppingListView, self).get_context_data()
if 'stock_id' in self.kwargs.keys():
ret['stock'] = Stock.objects.filter(id=self.kwargs['stock_id']).first();
return ret
class StockItemQuantityForm(forms.BaseForm):
def clean(self):
with transaction.atomic():
self.stock = Stock.objects.filter(id=self.stock_id).first()
shopping_list = ShoppingList(name="Courses "+self.stock.counter.name, date=timezone.now(), todo=True)
shopping_list.save()
shopping_list.stock_owner = self.stock
shopping_list.save()
for k,t in self.cleaned_data.items():
if k == 'name':
shopping_list.name = t
shopping_list.save()
elif k == "comment":
shopping_list.comment = t
shopping_list.save()
else:
if t > 0 :
item_id = int(k[5:])
item = StockItem.objects.filter(id=item_id).first()
shoppinglist_item = ShoppingListItem(stockitem_owner=item, name=item.name, type=item.type, tobuy_quantity=t)
shoppinglist_item.save()
shoppinglist_item.shopping_lists.add(shopping_list)
shoppinglist_item.save()
return self.cleaned_data
class StockItemQuantityBaseFormView(CounterAdminTabsMixin, CanEditMixin, DetailView, BaseFormView):
"""
docstring for StockItemOutList
"""
model = StockItem
template_name = "stock/shopping_list_quantity.jinja"
pk_url_kwarg = "stock_id"
current_tab = "stocks"
def get_form_class(self):
fields = OrderedDict()
kwargs = {}
fields['name'] = forms.CharField(max_length=30, required=True, label=_('Shopping list name'))
for t in ProductType.objects.order_by('name').all():
for i in self.stock.items.filter(type=t).order_by('name').all():
if i.effective_quantity <= i.minimal_quantity:
field_name = "item-%s" % (str(i.id))
fields[field_name] = forms.IntegerField(required=True, label=str(i), initial=0,
help_text=_(str(i.effective_quantity)+" left"))
fields['comment'] = forms.CharField(widget=forms.Textarea(attrs={"placeholder":_("Add here, items to buy that are not reference as a stock item (example : sponge, knife, mugs ...)")}),
required=False, label=_("Comments"))
kwargs['stock_id'] = self.stock.id
kwargs['base_fields'] = fields
return type('StockItemQuantityForm', (StockItemQuantityForm,), kwargs)
def get(self, request, *args, **kwargs):
"""
Simple get view
"""
self.stock = Stock.objects.filter(id=self.kwargs['stock_id']).first()
return super(StockItemQuantityBaseFormView, self).get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
"""
Handle the many possibilities of the post request
"""
self.object = self.get_object()
self.stock = Stock.objects.filter(id=self.kwargs['stock_id']).first()
return super(StockItemQuantityBaseFormView, self).post(request, *args, **kwargs)
def form_valid(self, form):
return super(StockItemQuantityBaseFormView, self).form_valid(form)
def get_context_data(self, **kwargs):
kwargs = super(StockItemQuantityBaseFormView, self).get_context_data(**kwargs)
if 'form' not in kwargs.keys():
kwargs['form'] = self.get_form()
kwargs['stock'] = self.stock
return kwargs
def get_success_url(self):
return reverse_lazy('stock:shoppinglist_list', args=self.args, kwargs=self.kwargs)
class StockShoppingListItemListView(CounterAdminTabsMixin, CanViewMixin, ListView):
"""docstring for StockShoppingListItemListView"""
model = ShoppingList
template_name = "stock/shopping_list_items.jinja"
pk_url_kwarg = "shoppinglist_id"
current_tab = "stocks"
def get_context_data(self):
ret = super(StockShoppingListItemListView, self).get_context_data()
if 'shoppinglist_id' in self.kwargs.keys():
ret['shoppinglist'] = ShoppingList.objects.filter(id=self.kwargs['shoppinglist_id']).first();
return ret
class StockShoppingListDeleteView(CounterAdminTabsMixin, CanEditMixin, DeleteView):
"""
Delete a ShoppingList (for the resonsible account)
"""
model = ShoppingList
pk_url_kwarg = "shoppinglist_id"
template_name = 'core/delete_confirm.jinja'
current_tab = "stocks"
def get_success_url(self):
return reverse_lazy('stock:shoppinglist_list', kwargs={'stock_id':self.object.stock_owner.id})
class StockShopppingListSetDone(CanEditMixin, DetailView):
"""
Set a ShoppingList as done
"""
model = ShoppingList
pk_url_kwarg = "shoppinglist_id"
def get(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.todo = False
self.object.save()
return HttpResponseRedirect(reverse('stock:shoppinglist_list', args=self.args, kwargs={'stock_id':self.object.stock_owner.id}))
def post(self, request, *args, **kwargs):
self.object = self.get_object()
return HttpResponseRedirect(reverse('stock:shoppinglist_list', args=self.args, kwargs={'stock_id':self.object.stock_owner.id}))
class StockShopppingListSetTodo(CanEditMixin, DetailView):
"""
Set a ShoppingList as done
"""
model = ShoppingList
pk_url_kwarg = "shoppinglist_id"
def get(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.todo = True
self.object.save()
return HttpResponseRedirect(reverse('stock:shoppinglist_list', args=self.args, kwargs={'stock_id':self.object.stock_owner.id}))
def post(self, request, *args, **kwargs):
self.object = self.get_object()
return HttpResponseRedirect(reverse('stock:shoppinglist_list', args=self.args, kwargs={'stock_id':self.object.stock_owner.id}))
class StockUpdateAfterShopppingForm(forms.BaseForm):
def clean(self):
with transaction.atomic():
self.shoppinglist = ShoppingList.objects.filter(id=self.shoppinglist_id).first()
for k,t in self.cleaned_data.items():
shoppinglist_item_id = int(k[5:])
if int(t) > 0 :
shoppinglist_item = ShoppingListItem.objects.filter(id=shoppinglist_item_id).first()
shoppinglist_item.bought_quantity = int(t)
shoppinglist_item.save()
shoppinglist_item.stockitem_owner.effective_quantity += int(t)
shoppinglist_item.stockitem_owner.save()
self.shoppinglist.todo = False
self.shoppinglist.save()
return self.cleaned_data
class StockUpdateAfterShopppingBaseFormView(CounterAdminTabsMixin, CanEditMixin, DetailView, BaseFormView):
"""
docstring for StockUpdateAfterShopppingBaseFormView
"""
model = ShoppingList
template_name = "stock/update_after_shopping.jinja"
pk_url_kwarg = "shoppinglist_id"
current_tab = "stocks"
def get_form_class(self):
fields = OrderedDict()
kwargs = {}
for t in ProductType.objects.order_by('name').all():
for i in self.shoppinglist.shopping_items_to_buy.filter(type=t).order_by('name').all():
field_name = "item-%s" % (str(i.id))
fields[field_name] = forms.CharField(max_length=30, required=True, label=str(i),
help_text=_(str(i.tobuy_quantity) + " asked"))
kwargs['shoppinglist_id'] = self.shoppinglist.id
kwargs['base_fields'] = fields
return type('StockUpdateAfterShopppingForm', (StockUpdateAfterShopppingForm,), kwargs)
def get(self, request, *args, **kwargs):
self.shoppinglist = ShoppingList.objects.filter(id=self.kwargs['shoppinglist_id']).first()
return super(StockUpdateAfterShopppingBaseFormView, self).get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
"""
Handle the many possibilities of the post request
"""
self.object = self.get_object()
self.shoppinglist = ShoppingList.objects.filter(id=self.kwargs['shoppinglist_id']).first()
return super(StockUpdateAfterShopppingBaseFormView, self).post(request, *args, **kwargs)
def form_valid(self, form):
"""
We handle here the redirection
"""
return super(StockUpdateAfterShopppingBaseFormView, self).form_valid(form)
def get_context_data(self, **kwargs):
kwargs = super(StockUpdateAfterShopppingBaseFormView, self).get_context_data(**kwargs)
if 'form' not in kwargs.keys():
kwargs['form'] = self.get_form()
kwargs['shoppinglist'] = self.shoppinglist
kwargs['stock'] = self.shoppinglist.stock_owner
return kwargs
def get_success_url(self):
self.kwargs.pop('shoppinglist_id', None)
return reverse_lazy('stock:shoppinglist_list', args=self.args, kwargs=self.kwargs)
class StockTakeItemsForm(forms.BaseForm):
"""
docstring for StockTakeItemsFormView
"""
def clean(self):
with transaction.atomic():
for k,t in self.cleaned_data.items():
item_id = int(k[5:])
if t > 0 :
item = StockItem.objects.filter(id=item_id).first()
item.effective_quantity -= t
item.save()
return self.cleaned_data
class StockTakeItemsBaseFormView(CounterTabsMixin, CanEditMixin, DetailView, BaseFormView):
"""
docstring for StockTakeItemsBaseFormView
"""
model = StockItem
template_name = "stock/stock_take_items.jinja"
pk_url_kwarg = "stock_id"
current_tab = "take_items_from_stock"
def get_form_class(self):
fields = OrderedDict()
kwargs = {}
for t in ProductType.objects.order_by('name').all():
for i in self.stock.items.filter(type=t).order_by('name').all():
field_name = "item-%s" % (str(i.id))
fields[field_name] = forms.IntegerField(required=False, label=str(i), initial=0, min_value=0, max_value=i.effective_quantity,
help_text=_("%(effective_quantity)s left" % {"effective_quantity": str(i.effective_quantity)}))
kwargs[field_name] = i.effective_quantity
kwargs['stock_id'] = self.stock.id
kwargs['counter_id'] = self.stock.counter.id
kwargs['base_fields'] = fields
return type('StockTakeItemsForm', (StockTakeItemsForm,), kwargs)
def get(self, request, *args, **kwargs):
"""
Simple get view
"""
self.stock = Stock.objects.filter(id=self.kwargs['stock_id']).first()
return super(StockTakeItemsBaseFormView, self).get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
"""
Handle the many possibilities of the post request
"""
self.object = self.get_object()
self.stock = Stock.objects.filter(id=self.kwargs['stock_id']).first()
if self.stock.counter.type == "BAR" and not ('counter_token' in self.request.session.keys() and
self.request.session['counter_token'] == self.stock.counter.token): # Also check the token to avoid the bar to be stolen
return HttpResponseRedirect(reverse_lazy('counter:details', args=self.args,
kwargs={'counter_id': self.stock.counter.id})+'?bad_location')
return super(StockTakeItemsBaseFormView, self).post(request, *args, **kwargs)
def form_valid(self, form):
return super(StockTakeItemsBaseFormView, self).form_valid(form)
def get_context_data(self, **kwargs):
kwargs = super(StockTakeItemsBaseFormView, self).get_context_data(**kwargs)
if 'form' not in kwargs.keys():
kwargs['form'] = self.get_form()
kwargs['stock'] = self.stock
kwargs['counter'] = self.stock.counter
return kwargs
def get_success_url(self):
stock = Stock.objects.filter(id=self.kwargs['stock_id']).first()
self.kwargs['counter_id'] = stock.counter.id
self.kwargs.pop('stock_id', None)
return reverse_lazy('counter:details', args=self.args, kwargs=self.kwargs)