Commit 070c7057 authored by Patrick's avatar Patrick

Introduce contracts which are at first like boxes

parent 9ebbb534
......@@ -43,6 +43,9 @@ admin.site.register(Product, ProductAdmin)
from repanier.models.box import Box
from repanier.admin.box import BoxAdmin
admin.site.register(Box, BoxAdmin)
from repanier.models.contract import Contract
from repanier.admin.contract import ContractAdmin
admin.site.register(Contract, ContractAdmin)
from repanier.models.staff import Staff
from repanier.admin.staff import StaffWithUserDataAdmin
admin.site.register(Staff, StaffWithUserDataAdmin)
# -*- coding: utf-8
from __future__ import unicode_literals
from os import sep as os_sep
from django import forms
from django.conf import settings
from django.contrib import admin
from django.contrib import messages
from django.contrib.admin import TabularInline
from django.forms import ModelForm, BaseInlineFormSet
from django.forms.formsets import DELETION_FIELD_NAME
from django.shortcuts import render
from django.utils import translation
from django.utils.translation import ugettext_lazy as _
from easy_select2 import Select2
from parler.admin import TranslatableAdmin
from parler.forms import TranslatableModelForm
from repanier.admin.fkey_choice_cache_mixin import ForeignKeyCacheMixin
from repanier.const import DECIMAL_ZERO, ORDER_GROUP, INVOICE_GROUP, \
COORDINATION_GROUP, PERMANENCE_PLANNED, PERMANENCE_CLOSED
from repanier.models.contract import ContractContent, Contract
from repanier.models.product import Product
from repanier.models.offeritem import OfferItemWoReceiver
from repanier.task import task_contract
from repanier.tools import update_offer_item
try:
from urllib.parse import parse_qsl
except ImportError:
from urlparse import parse_qsl
class ContractContentInlineFormSet(BaseInlineFormSet):
def clean(self):
products = set()
for form in self.forms:
if form.cleaned_data and not form.cleaned_data.get('DELETE'):
# This is not an empty form or a "to be deleted" form
product = form.cleaned_data.get('product', None)
if product is not None:
if product in products:
raise forms.ValidationError(_('Duplicate product are not allowed.'))
else:
products.add(product)
def get_queryset(self):
return self.queryset.filter(
product__translations__language_code=translation.get_language()
).order_by(
"product__producer__short_profile_name",
"product__translations__long_name",
"product__order_average_weight",
)
class ContractContentInlineForm(ModelForm):
is_into_offer = forms.BooleanField(
label=_("is_into_offer"), required=False, initial=True)
stock = forms.DecimalField(
label=_("Current stock"), max_digits=9, decimal_places=3, required=False, initial=DECIMAL_ZERO)
limit_order_quantity_to_stock = forms.BooleanField(
label=_("limit maximum order qty of the group to stock qty"), required=False, initial=True)
previous_product = forms.ModelChoiceField(
Product.objects.none(), required=False)
def __init__(self, *args, **kwargs):
super(ContractContentInlineForm, self).__init__(*args, **kwargs)
self.fields["product"].widget.can_add_related = False
self.fields["product"].widget.can_delete_related = False
if self.instance.id is not None:
self.fields["is_into_offer"].initial = self.instance.product.is_into_offer
self.fields["stock"].initial = self.instance.product.stock
self.fields["limit_order_quantity_to_stock"].initial = self.instance.product.limit_order_quantity_to_stock
self.fields["previous_product"].initial = self.instance.product
self.fields["is_into_offer"].disabled = True
self.fields["stock"].disabled = True
self.fields["limit_order_quantity_to_stock"].disabled = True
class Meta:
widgets = {
'product': Select2(select2attrs={'width': '450px'})
}
class ContractContentInline(ForeignKeyCacheMixin, TabularInline):
form = ContractContentInlineForm
formset = ContractContentInlineFormSet
model = ContractContent
ordering = ("product",)
fields = ['product', 'is_into_offer', 'content_quantity', 'stock', 'limit_order_quantity_to_stock',
'get_calculated_customer_content_price']
extra = 0
fk_name = 'contract'
# The stock and limit_order_quantity_to_stock are read only to have only one place to update it : the product.
readonly_fields = [
'get_calculated_customer_content_price'
]
has_add_or_delete_permission = None
def has_delete_permission(self, request, obj=None):
if self.has_add_or_delete_permission is None:
try:
parent_object = Contract.objects.filter(
id=request.resolver_match.args[0]
).only(
"id").order_by('?').first()
if parent_object is not None and OfferItemWoReceiver.objects.filter(
product=parent_object.id,
permanence__status__gt=PERMANENCE_PLANNED,
permanence__status__lt=PERMANENCE_CLOSED
).order_by('?').exists():
self.has_add_or_delete_permission = False
else:
self.has_add_or_delete_permission = True
except:
self.has_add_or_delete_permission = True
return self.has_add_or_delete_permission
def has_add_permission(self, request):
return self.has_delete_permission(request)
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "product":
kwargs["queryset"] = Product.objects.filter(
is_active=True,
# A contract may not include another contract
is_contract=False,
# We can't make any composition with producer preparing baskets on basis of our order.
producer__invoice_by_basket=False,
translations__language_code=translation.get_language()
).order_by(
"producer__short_profile_name",
"translations__long_name",
"order_average_weight",
)
return super(ContractContentInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
class ContractForm(TranslatableModelForm):
calculated_stock = forms.DecimalField(
label=_("Calculated current stock"), max_digits=9, decimal_places=3, required=False, initial=DECIMAL_ZERO)
calculated_customer_contract_price = forms.DecimalField(
label=_("calculated customer contract price"), max_digits=8, decimal_places=2, required=False, initial=DECIMAL_ZERO)
calculated_contract_deposit = forms.DecimalField(
label=_("calculated contract deposit"), max_digits=8, decimal_places=2, required=False, initial=DECIMAL_ZERO)
def __init__(self, *args, **kwargs):
super(ContractForm, self).__init__(*args, **kwargs)
contract = self.instance
if contract.id is not None:
contract_price, contract_deposit = contract.get_calculated_price()
self.fields["calculated_stock"].initial = contract.get_calculated_stock()
self.fields["calculated_customer_contract_price"].initial = contract_price
self.fields["calculated_contract_deposit"].initial = contract_deposit
self.fields["calculated_customer_contract_price"].disabled = True
self.fields["calculated_stock"].disabled = True
self.fields["calculated_contract_deposit"].disabled = True
class ContractAdmin(TranslatableAdmin):
form = ContractForm
model = Contract
list_display = (
'is_into_offer', 'get_long_name', 'language_column',
)
list_display_links = ('get_long_name',)
list_per_page = 16
list_max_show_all = 16
inlines = (ContractContentInline,)
filter_horizontal = ('production_mode',)
ordering = ('customer_unit_price',
'unit_deposit',
'translations__long_name',)
search_fields = ('translations__long_name',)
list_filter = ('is_active',
'is_into_offer')
actions = [
'flip_flop_select_for_offer_status',
'duplicate_contract'
]
def has_delete_permission(self, request, contract=None):
if request.user.groups.filter(
name__in=[ORDER_GROUP, INVOICE_GROUP, COORDINATION_GROUP]).exists() or request.user.is_superuser:
return True
return False
def has_add_permission(self, request):
return self.has_delete_permission(request)
def has_change_permission(self, request, contract=None):
return self.has_delete_permission(request, contract)
def get_list_display(self, request):
self.list_editable = ('stock',)
if settings.DJANGO_SETTINGS_MULTIPLE_LANGUAGE:
return ('get_is_into_offer', 'get_long_name', 'language_column', 'stock')
else:
return ('get_is_into_offer', 'get_long_name', 'stock')
def flip_flop_select_for_offer_status(self, request, queryset):
task_contract.flip_flop_is_into_offer(queryset)
flip_flop_select_for_offer_status.short_description = _(
'flip_flop_select_for_offer_status for offer')
def duplicate_contract(self, request, queryset):
if 'cancel' in request.POST:
user_message = _("Action canceled by the user.")
user_message_level = messages.INFO
self.message_user(request, user_message, user_message_level)
return
contract = queryset.first()
if contract is None:
user_message = _("Action canceled by the system.")
user_message_level = messages.ERROR
self.message_user(request, user_message, user_message_level)
return
if 'apply' in request.POST:
user_message, user_message_level = task_contract.admin_duplicate(queryset)
self.message_user(request, user_message, user_message_level)
return
return render(
request,
'repanier/confirm_admin_duplicate_contract.html', {
'sub_title' : _("Please, confirm the action : duplicate contract"),
'action_checkcontract_name': admin.ACTION_CHECKBOX_NAME,
'action' : 'duplicate_contract',
'product' : contract,
})
duplicate_contract.short_description = _('duplicate contract')
def get_fieldsets(self, request, contract=None):
fields_basic = [
('long_name', 'picture2'),
('calculated_stock', 'calculated_customer_contract_price', 'calculated_contract_deposit'),
('stock', 'customer_unit_price', 'unit_deposit'),
]
fields_advanced_descriptions = [
'placement',
'offer_description',
'production_mode',
]
fields_advanced_options = [
('reference', 'vat_level'),
('is_into_offer', 'is_active', 'is_updated_on')
]
fieldsets = (
(None, {'fields': fields_basic}),
(_('Advanced descriptions'), {'classes': ('collapse',), 'fields': fields_advanced_descriptions}),
(_('Advanced options'), {'classes': ('collapse',), 'fields': fields_advanced_options})
)
return fieldsets
def get_readonly_fields(self, request, customer=None):
return ['is_updated_on']
def get_form(self, request, contract=None, **kwargs):
form = super(ContractAdmin, self).get_form(request, contract, **kwargs)
picture_field = form.base_fields["picture2"]
if hasattr(picture_field.widget, 'upload_to'):
picture_field.widget.upload_to = "%s%s%s" % ("product", os_sep, "contract")
return form
def get_queryset(self, request):
qs = super(ContractAdmin, self).get_queryset(request)
qs = qs.filter(
is_contract=True,
translations__language_code=translation.get_language()
)
return qs
def save_model(self, request, contract, form, change):
super(ContractAdmin, self).save_model(request, contract, form, change)
update_offer_item(contract)
def save_related(self, request, form, formsets, change):
for formset in formsets:
# option.py -> construct_change_message doesn't test the presence of those array not created at form initialisation...
if not hasattr(formset, 'new_objects'): formset.new_objects = []
if not hasattr(formset, 'changed_objects'): formset.changed_objects = []
if not hasattr(formset, 'deleted_objects'): formset.deleted_objects = []
contract = form.instance
try:
formset = formsets[0]
for contract_content_form in formset:
contract_content = contract_content_form.instance
previous_product = contract_content_form.fields['previous_product'].initial
if previous_product is not None and previous_product != contract_content.product:
# Delete the contract_content because the product has changed
contract_content_form.instance.delete()
if contract_content.product is not None:
if contract_content.id is None:
contract_content.contract_id = contract.id
if contract_content_form.cleaned_data.get(DELETION_FIELD_NAME, False):
contract_content_form.instance.delete()
elif contract_content_form.has_changed():
contract_content_form.instance.save()
except IndexError:
# No formset present in list admin, but well in detail admin
pass
......@@ -322,10 +322,16 @@ class ProducerAdmin(ImportExportMixin, admin.ModelAdmin):
return actions
def get_list_display(self, request):
if repanier.apps.REPANIER_SETTINGS_INVOICE:
return ('__str__', 'get_products', 'get_balance', 'phone1', 'email')
if settings.DJANGO_SETTINGS_IS_MINIMALIST:
if repanier.apps.REPANIER_SETTINGS_INVOICE:
return ('__str__', 'get_products', 'get_balance', 'phone1', 'email')
else:
return ('__str__', 'get_products', 'phone1', 'email')
else:
return ('__str__', 'get_products', 'phone1', 'email')
if repanier.apps.REPANIER_SETTINGS_INVOICE:
return ('__str__', 'get_products', 'get_contracts', 'get_balance', 'phone1', 'email')
else:
return ('__str__', 'get_products', 'get_contracts', 'phone1', 'email')
def get_fieldsets(self, request, producer=None):
fields_basic = [
......
......@@ -288,6 +288,9 @@ BOX_UNICODE = "📦" # http://unicode-table.com/fr/1F6CD/
LOCK_UNICODE = "✓🔐"
VALID_UNICODE = "✓"
BANK_NOTE_UNICODE = "💶"
CONTRACT_VALUE_STR = "-1"
CONTRACT_VALUE_INT = -1
CONTRACT_UNICODE = "🤝"
LUT_CONFIRM = (
(True, LOCK_UNICODE), (False, EMPTY_STRING)
......
......@@ -15,8 +15,10 @@ from .purchase import Purchase
from .staff import Staff
# after Producer and Product
from .box import BoxContent
from .contract import ContractContent
# proxies
from .box import Box
from.contract import Contract
from .invoice import CustomerSend
from .offeritem import OfferItemSend, OfferItemClosed, OfferItemWoReceiver
from .permanence import PermanenceInPreparation, PermanenceDone
......@@ -103,7 +103,6 @@ class BoxContent(models.Model):
verbose_name_plural = _("boxes content")
unique_together = ("box", "product",)
index_together = [
# ["box", "product"],
["product", "box"],
]
......
# -*- coding: utf-8
from __future__ import unicode_literals
from django.core.validators import MinValueValidator
from django.db import models
from django.db.models import Sum
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from repanier.const import *
from repanier.fields.RepanierMoneyField import ModelMoneyField
from repanier.models.producer import Producer
from repanier.models.product import Product, product_pre_save
@python_2_unicode_compatible
class Contract(Product):
def get_calculated_stock(self):
# stock : max_digits=9, decimal_places=3 => 1000000 > max(stock)
stock = DECIMAL_MAX_STOCK
for contract_content in ContractContent.objects.filter(
contract_id=self.id,
product__limit_order_quantity_to_stock=True,
content_quantity__gt=DECIMAL_ZERO,
product__is_contract=False # Disallow recursivity
).prefetch_related(
"product"
).only(
"content_quantity", "product__stock", "product__limit_order_quantity_to_stock"
).order_by('?'):
stock = min(stock, contract_content.product.stock // contract_content.content_quantity)
return stock
def get_calculated_price(self):
result_set = ContractContent.objects.filter(box_id=self.id).aggregate(
Sum('calculated_customer_content_price'),
Sum('calculated_content_deposit')
)
box_price = result_set["calculated_customer_content_price__sum"] \
if result_set["calculated_customer_content_price__sum"] is not None else DECIMAL_ZERO
box_deposit = result_set["calculated_content_deposit__sum"] \
if result_set["calculated_content_deposit__sum"] is not None else DECIMAL_ZERO
return box_price, box_deposit
class Meta:
proxy = True
verbose_name = _("box")
verbose_name_plural = _("boxes")
def __str__(self):
return '%s' % self.long_name
@receiver(pre_save, sender=Contract)
def contract_pre_save(sender, **kwargs):
contract = kwargs["instance"]
contract.is_contract = True
contract.producer_id = Producer.objects.filter(
represent_this_buyinggroup=True
).order_by('?').only('id').first().id
contract.order_unit = PRODUCT_ORDER_UNIT_PC
contract.producer_unit_price = contract.customer_unit_price
contract.producer_vat = contract.customer_vat
contract.limit_order_quantity_to_stock = True
# ! Important to initialise all fields of the box. Remember : a box is a product.
product_pre_save(sender, **kwargs)
@python_2_unicode_compatible
class ContractContent(models.Model):
contract = models.ForeignKey(
'Contract', verbose_name=_("contract"),
null=True, blank=True, db_index=True, on_delete=models.PROTECT)
product = models.ForeignKey(
'Product', verbose_name=_("product"), related_name='contract_content',
null=True, blank=True, db_index=True, on_delete=models.PROTECT)
content_quantity = models.DecimalField(
_("content quantity"),
default=DECIMAL_ZERO, max_digits=6, decimal_places=3,
validators=[MinValueValidator(0)])
calculated_customer_content_price = ModelMoneyField(
_("customer content price"),
default=DECIMAL_ZERO, max_digits=8, decimal_places=2)
calculated_content_deposit = ModelMoneyField(
_("content deposit"),
help_text=_('deposit to add to the original content price'),
default=DECIMAL_ZERO, max_digits=8, decimal_places=2)
def get_calculated_customer_content_price(self):
# workaround for a display problem with Money field in the admin list_display
return self.calculated_customer_content_price + self.calculated_content_deposit
get_calculated_customer_content_price.short_description = (_("customer content price"))
get_calculated_customer_content_price.allow_tags = False
class Meta:
verbose_name = _("contract content")
verbose_name_plural = _("contracts content")
unique_together = ("contract", "product",)
index_together = [
["product", "contract"],
]
def __str__(self):
return EMPTY_STRING
@receiver(pre_save, sender=ContractContent)
def contract_content_pre_save(sender, **kwargs):
contract_content = kwargs["instance"]
product_id = contract_content.product_id
if product_id is not None:
product = Product.objects.filter(id=product_id).order_by('?').only(
'customer_unit_price',
'unit_deposit'
).first()
if product is not None:
contract_content.calculated_customer_content_price.amount = contract_content.content_quantity * product.customer_unit_price.amount
contract_content.calculated_content_deposit.amount = int(contract_content.content_quantity) * product.unit_deposit.amount
......@@ -115,8 +115,10 @@ class Item(TranslatableModel):
default=False
)
is_box = models.BooleanField(_("is_box"), default=False)
is_box_content = models.BooleanField(_("is_box_content"), default=False)
is_box = models.BooleanField(_("is a box"), default=False)
is_box_content = models.BooleanField(_("is a box content"), default=False)
is_contract = models.BooleanField(_("is a contract"), default=False)
is_contract_content = models.BooleanField(_("is a contract content"), default=False)
# is_membership_fee = models.BooleanField(_("is_membership_fee"), default=False)
# may_order = models.BooleanField(_("may_order"), default=True)
is_active = models.BooleanField(_("is_active"), default=True)
......
......@@ -118,6 +118,20 @@ class Producer(models.Model):
get_products.short_description = (_("link to his products"))
get_products.allow_tags = True
def get_contracts(self):
# This producer may have contrat's list
if self.is_active:
changeproductslist_url = urlresolvers.reverse(
'admin:repanier_contract_changelist',
)
link = '<a href="%s?is_active__exact=1&producer=%s" class="btn addlink">&nbsp;%s</a>' \
% (changeproductslist_url, str(self.id), _("his contracts"))
return link
return EMPTY_STRING
get_contracts.short_description = (_("link to his contracts"))
get_contracts.allow_tags = True
def get_admin_date_balance(self):
if self.id is not None:
bank_account = BankAccount.objects.filter(
......
// Readable 3.2.0
// Bootswatch
// -----------------------------------------------------
// @import url("//fonts.googleapis.com/css?family=Raleway:400,700");
// Navbar =====================================================================
.navbar {
font-family: @headings-font-family;
&-nav,
&-form {
margin-left: 0;
margin-right: 0;
}
&-nav > li > a {
padding: @padding-base-vertical @padding-base-horizontal;
margin: 12px 6px;
border: 1px solid transparent;
border-radius: @border-radius-base;
&:hover {
border: 1px solid #ddd;
}
}
&-nav > .active > a,
&-nav > .active > a:hover {
border: 1px solid #ddd;
}
&-default .navbar-nav > .active > a:hover {
color: @navbar-default-link-hover-color;
}
&-inverse .navbar-nav > .active > a:hover {
color: @navbar-inverse-link-hover-color;
}
&-brand {
padding-top: 20px;
}
}
@media (max-width: @grid-float-breakpoint) {
.navbar {
.navbar-nav > li > a {
margin: 0;
}
}
}
// Buttons ====================================================================