""".format(row.id, row.vat)
+ return self.vat_template.render(context={"expense": row, "can_edit_vat": self.can_edit_vat}, request=self.request)
else:
return super(ExpenseTableDT, self).render_column(row, column)
@@ -125,11 +126,16 @@ class ExpenseTable(tables.Table):
state = tables.TemplateColumn(template_name="expense/_expense_state_column.html", orderable=False)
expense_date = tables.TemplateColumn("""{{ record.expense_date }}""") # Title attr is just used to have an easy to parse hidden value for sorting
update_date = tables.TemplateColumn("""{{ record.update_date }}""", attrs=TABLES2_HIDE_COL_MD) # Title attr is just used to have an easy to parse hidden value for sorting
- vat = tables.TemplateColumn("""{% load l10n %}
{{record.vat}}
""")
+ transitions_template = get_template("expense/_expense_transitions_column.html")
+ vat_template = get_template("expense/_expense_vat_column.html")
+
def render_user(self, value):
return link_to_consultant(value)
+ def render_vat(self, record):
+ return self.vat_template.render(context={"expense": record, "can_edit_vat": True})
+
class Meta:
model = Expense
sequence = ("id", "user", "description", "lead", "amount", "vat", "chargeable", "corporate_card", "receipt", "state", "expense_date", "update_date", "comment")
@@ -142,35 +148,18 @@ class Meta:
class ExpenseWorkflowTable(ExpenseTable):
transitions = tables.Column(accessor="pk")
- def render_transitions(self, record):
- result = []
- for transition in self.transitionsData[record.id]:
- result.append("""%s"""
- % (expense_transition_to_state_display(transition), reverse("expense:update_expense_state", args=[record.id, transition]), expense_transition_to_state_display(transition)[0:2]))
- if self.expenseEditPerm[record.id]:
- result.append("%s"
- % (smart_str(_("Edit")),
- reverse("expense:expenses", kwargs={"expense_id": record.id}),
- # Translators: Ed is the short term for Edit
- smart_str(_("Ed"))))
- result.append("%s" %
- (smart_str(_("Delete")),
- reverse("expense:expense_delete", kwargs={"expense_id": record.id}),
- # Translators: De is the short term for Delete
- smart_str(_("De"))))
- result.append("%s" %
- (smart_str(_("Clone")),
- reverse("expense:clone_expense", kwargs={"clone_from": record.id}),
- # Translators: Cl is the short term for Clone
- smart_str(_("Cl"))))
- return mark_safe(" ".join(result))
-
class Meta:
sequence = ("id", "user", "description", "lead", "amount", "chargeable", "corporate_card", "receipt", "state", "transitions", "expense_date", "update_date", "comment")
fields = sequence
class UserExpenseWorkflowTable(ExpenseWorkflowTable):
+ def render_transitions(self, record):
+ return self.transitions_template.render(context={"record": record,
+ "transitions": [],
+ "expense_edit_perm": can_edit_expense(record, self.request.user)},
+ request=self.request)
+
class Meta:
attrs = {"class": "pydici-tables2 table table-hover table-striped table-sm", "id": "user_expense_workflow_table"}
prefix = "user_expense_workflow_table"
@@ -180,6 +169,14 @@ class Meta:
class ManagedExpenseWorkflowTable(ExpenseWorkflowTable):
description = tables.TemplateColumn("""{% load l10n %} {{ record.description }}""",
attrs={"td": {"class": "description"}})
+
+ def render_transitions(self, record):
+ transitions = [(t, expense_transition_to_state_display(t), expense_transition_to_state_display(t)[0:2]) for t in expense_next_states(record, self.request.user)]
+ return self.transitions_template.render(context={"record": record,
+ "transitions": transitions,
+ "expense_edit_perm": can_edit_expense(record, self.request.user)},
+ request=self.request)
+
class Meta:
attrs = {"class": "pydici-tables2 table table-hover table-striped table-sm", "id": "managed_expense_workflow_table"}
prefix = "managed_expense_workflow_table"
diff --git a/expense/urls.py b/expense/urls.py
index aaeced5d..fce650b0 100644
--- a/expense/urls.py
+++ b/expense/urls.py
@@ -14,8 +14,8 @@
re_path(r'^(?P\d+)/receipt$', v.expense_receipt, name="expense_receipt"),
re_path(r'^(?P\d+)/delete$', v.expense_delete, name="expense_delete"),
re_path(r'^(?P\d+)/change$', v.expenses, name="expenses"),
+ re_path(r'^(?P\d+)/expense_vat$', v.update_expense_vat, name="update_expense_vat"),
re_path(r'^(?P\d+)/(?P\w+)$', v.update_expense_state, name="update_expense_state"),
- re_path(r'^expense_vat$', v.update_expense_vat, name="update_expense_vat"),
re_path(r'^clone/(?P\d+)$', v.expenses, name="clone_expense"),
re_path(r'^mission/(?P\d+)$', v.lead_expenses, name="lead_expenses"),
re_path(r'^history/?$', v.expenses_history, name="expenses_history"),
diff --git a/expense/views.py b/expense/views.py
index 4499d5e2..708ffefd 100644
--- a/expense/views.py
+++ b/expense/views.py
@@ -6,7 +6,6 @@
"""
from datetime import date
-import json
from io import BytesIO
import decimal
@@ -19,13 +18,13 @@
from django.shortcuts import render, redirect
from django.contrib import messages
-from expense.forms import ExpenseForm, ExpensePaymentForm
+from expense.forms import ExpenseForm, ExpensePaymentForm, ExpenseVATForm
from expense.models import Expense, ExpensePayment
from expense.tables import ExpenseTable, UserExpenseWorkflowTable, ManagedExpenseWorkflowTable
from leads.models import Lead
from people.models import Consultant
from core.decorator import pydici_non_public, pydici_feature, pydici_subcontractor
-from expense.utils import expense_next_states, can_edit_expense, in_terminal_state, user_expense_perm
+from expense.utils import expense_next_states, can_edit_expense, in_terminal_state, user_expense_perm, expense_transition_to_state_display
from people.utils import users_are_in_same_company
from crm.utils import get_subsidiary_from_session
@@ -53,10 +52,14 @@ def expense(request, expense_id):
(expense_subsidiary_manager and users_are_in_same_company(expense.user, request.user))):
return HttpResponseRedirect(reverse("core:forbidden"))
+ transitions = [(t, expense_transition_to_state_display(t), expense_transition_to_state_display(t)[0:2]) for t in
+ expense_next_states(expense, request.user)]
+
return render(request, "expense/expense.html",
{"expense": expense,
"can_edit": can_edit_expense(expense, request.user),
"can_edit_vat": expense_administrator or expense_paymaster,
+ "transitions": transitions,
"user": request.user})
@@ -137,13 +140,9 @@ def expenses(request, expense_id=None, clone_from=None):
managed_expenses = team_expenses
userExpenseTable = UserExpenseWorkflowTable(user_expenses)
- userExpenseTable.transitionsData = dict([(e.id, []) for e in user_expenses]) # Inject expense allowed transitions. Always empty for own expense
- userExpenseTable.expenseEditPerm = dict([(e.id, can_edit_expense(e, request.user)) for e in user_expenses]) # Inject expense edit permissions
RequestConfig(request, paginate={"per_page": 50}).configure(userExpenseTable)
managedExpenseTable = ManagedExpenseWorkflowTable(managed_expenses)
- managedExpenseTable.transitionsData = dict([(e.id, expense_next_states(e, request.user)) for e in managed_expenses]) # Inject expense allowed transitions
- managedExpenseTable.expenseEditPerm = dict([(e.id, can_edit_expense(e, request.user)) for e in managed_expenses]) # Inject expense edit permissions
RequestConfig(request, paginate={"per_page": 100}).configure(managedExpenseTable)
return render(request, "expense/expenses.html",
{"user_expense_table": userExpenseTable,
@@ -213,7 +212,7 @@ def expenses_history(request):
{ className: "hidden-xs hidden-sm hidden-md", "targets": [2, 10, 12, 13]},
{ className: "description", "targets": [3]},
{ className: "amount", "targets": [5]}],
- "fnDrawCallback": function( oSettings ) {make_vat_editable(); }''',
+ "drawCallback": function( oSettings ) {htmx.process(document.body); }''',
"can_edit_vat": expense_administrator or expense_paymaster,
"user": request.user})
@@ -253,57 +252,60 @@ def chargeable_expenses(request):
@pydici_feature("reports")
def update_expense_state(request, expense_id, target_state):
"""Do workflow transition for that expense."""
- error = False
message = ""
try:
expense = Expense.objects.get(id=expense_id)
except Expense.DoesNotExist:
- message = _("Expense %s does not exist" % expense_id)
- error = True
-
- if not error:
- next_states = expense_next_states(expense, request.user)
- if target_state in next_states:
- expense.state = target_state
- if in_terminal_state(expense):
- expense.workflow_in_progress = False
- expense.save()
- message = _("Successfully update expense")
- else:
- message = ("Transition %s is not allowed" % target_state)
- error = True
+ return HttpResponse(_("Expense %s does not exist" % expense_id), status=404)
- response = {"message": message,
- "expense_id": expense_id,
- "error": error}
-
- return HttpResponse(json.dumps(response), content_type="application/json")
+ next_states = expense_next_states(expense, request.user)
+ if target_state in next_states:
+ expense.state = target_state
+ if in_terminal_state(expense):
+ expense.workflow_in_progress = False
+ expense.save()
+ transitions = [(t, expense_transition_to_state_display(t), expense_transition_to_state_display(t)[0:2]) for t in
+ expense_next_states(expense, request.user)]
+ return render(request, "expense/_expense_transitions_column.html",
+ {"record": expense,
+ "transitions": transitions,
+ "expense_edit_perm": can_edit_expense(expense, request.user)})
+ else:
+ message = ("Transition %s is not allowed" % target_state)
+ return HttpResponse(message)
@pydici_non_public
@pydici_feature("management")
-def update_expense_vat(request):
+def update_expense_vat(request, expense_id):
"""Update expense VAT."""
-
- expense_administrator, expense_subsidiary_manager, expense_manager, expense_paymaster, expense_requester = user_expense_perm(request.user)
+ expense_administrator, expense_subsidiary_manager, expense_manager, expense_paymaster, expense_requester = user_expense_perm(
+ request.user)
if not (expense_administrator or expense_paymaster):
return HttpResponseForbidden()
try:
- expense_id = request.POST["id"]
- value = request.POST["value"].replace(",", ".")
expense = Expense.objects.get(id=expense_id)
- expense.vat = decimal.Decimal(value)
- expense.save()
- message = value
except Expense.DoesNotExist:
- message = _("Expense %s does not exist" % expense_id)
- except (ValueError, decimal.InvalidOperation):
- message = _("Incorrect value")
+ return HttpResponse(_("Expense does not exist"), status=404)
- return HttpResponse(message)
+ if request.method == "GET":
+ form = ExpenseVATForm(instance=expense)
+ return HttpResponse("" % (
+ reverse("expense:update_expense_vat", args=[expense_id]), form))
+
+ else:
+ form = ExpenseVATForm(request.POST, instance=expense)
+ if form.is_valid():
+ form.save()
+ form = ExpenseVATForm(instance=expense)
+ return render(request, "expense/_expense_vat_column.html",
+ {"expense": expense,
+ "can_edit_vat": expense_administrator or expense_paymaster,
+ "form": form,
+ "user": request.user})
@pydici_non_public
diff --git a/leads/forms.py b/leads/forms.py
index 0577aaa0..52c78f52 100644
--- a/leads/forms.py
+++ b/leads/forms.py
@@ -12,11 +12,11 @@
from django.utils.encoding import smart_str
from django import forms
-from crispy_forms.layout import Layout, Div, Column, Fieldset, Field, HTML, Row
+from crispy_forms.layout import Layout, Column, Fieldset, Field, HTML, Row
from crispy_forms.bootstrap import AppendedText, TabHolder, Tab, FieldWithButtons
-from django_select2.forms import ModelSelect2Widget
+from django_select2.forms import ModelSelect2Widget, ModelSelect2TagWidget
from taggit.forms import TagField
-
+from taggit.models import Tag
from leads.models import Lead
from people.models import Consultant, SalesMan
@@ -24,6 +24,7 @@
from people.forms import ConsultantChoices, ConsultantMChoices, SalesManChoices
from crm.forms import ClientChoices, BusinessBrokerChoices
from core.forms import PydiciCrispyModelForm, PydiciSelect2WidgetMixin
+from core.utils import capitalize
class LeadChoices(PydiciSelect2WidgetMixin, ModelSelect2Widget):
@@ -66,6 +67,45 @@ def get_queryset(self):
return qs.distinct()
+class LeadTagChoices(PydiciSelect2WidgetMixin, ModelSelect2TagWidget):
+ model = Tag
+ search_fields = ["name__icontains"]
+ queryset = Tag.objects.all()
+
+ def __init__(self, *args, **kwargs):
+ self.lead = kwargs.pop("lead", None)
+ super(LeadTagChoices, self).__init__(*args, **kwargs)
+
+ def get_queryset(self):
+ qs = super(LeadTagChoices, self).get_queryset()
+ if self.lead:
+ qs = qs.exclude(lead__id=self.lead.id) # Exclude existing tags
+ return qs
+
+ def value_from_datadict(self, data, files, name):
+ """Create objects for given non-pimary-key values. Return list of all primary keys."""
+ cleaned_values = []
+ values = set(super().value_from_datadict(data, files, name))
+ for value in values:
+ try: # Tag exists
+ cleaned_values.append(self.queryset.get(pk=int(value)).pk)
+ except (ValueError, TypeError) or self.model.DoesNotExist:
+ # We need to create it needed
+ tag, created = self.queryset.get_or_create(name=capitalize(value))
+ cleaned_values.append(tag.pk)
+
+ return cleaned_values
+
+
+class LeadTagForm(forms.Form):
+
+ def __init__(self, *args, **kwargs):
+ lead = kwargs.pop("lead", None)
+ super(LeadTagForm, self).__init__(*args, **kwargs)
+ self.fields["tag"] = forms.ModelMultipleChoiceField(widget=LeadTagChoices(lead=lead, attrs={"data-placeholder": _("New tags"), "style": "min-width: 200px;"}),
+ queryset=Tag.objects, label=False)
+
+
class LeadForm(PydiciCrispyModelForm):
class Meta:
model = Lead
@@ -120,7 +160,6 @@ def clean_sales(self):
# We can't tolerate that sale amount is not known at this step of the process
raise ValidationError(_("Sales amount must be defined at this step of the commercial process"))
-
def clean_start_date(self):
"""Ensure start_date amount is defined at lead when commercial proposition has been sent"""
if self.cleaned_data["start_date"] or self.data["state"] in ('QUALIF', 'WRITE_OFFER', 'SLEEPING', 'LOST', 'FORGIVEN'):
diff --git a/leads/models.py b/leads/models.py
index 3bfb5b8d..786fd536 100644
--- a/leads/models.py
+++ b/leads/models.py
@@ -286,6 +286,17 @@ def getDocURL(self):
else:
return ""
+ def suggested_tags(self):
+ """Find suggested tags for this lead except if it has already at least two tags"""
+ from leads.learn import predict_tags # Late import to avoid circular dependency
+ tags = self.tags.all()
+ if tags.count() < 3:
+ suggestedTags = set(predict_tags(self))
+ suggestedTags -= set(tags)
+ else:
+ suggestedTags = []
+ return suggestedTags
+
def get_absolute_url(self):
return reverse('leads:detail', args=[str(self.id)])
diff --git a/leads/tables.py b/leads/tables.py
index 50e8f1ba..50f14f14 100644
--- a/leads/tables.py
+++ b/leads/tables.py
@@ -166,3 +166,18 @@ def render_column(self, row, column):
else:
return super(LeadToBill, self).render_column(row, column)
+
+class SupplierLeadTableDT(LeadTableDT):
+ def get_initial_queryset(self):
+ qs = Lead.objects.filter(mission__timesheet__consultant__subcontractor_company__company_id=self.kwargs["supplier_id"]).distinct()
+ return qs.select_related("client__contact", "client__organisation__company", "responsible", "subsidiary")
+
+
+class BusinessBrokerLeadTableDT(LeadTableDT):
+ """Business broker or paying authority leads"""
+ def get_initial_queryset(self):
+ qs = Lead.objects.filter(Q(business_broker__company_id=self.kwargs["businessbroker_id"]) | Q(paying_authority__company_id=self.kwargs["businessbroker_id"]))
+ qs = qs.distinct()
+ return qs.select_related("client__contact", "client__organisation__company", "responsible", "subsidiary")
+
+
diff --git a/leads/urls.py b/leads/urls.py
index 9dbab8a3..76bda06b 100644
--- a/leads/urls.py
+++ b/leads/urls.py
@@ -5,7 +5,7 @@
"""
from django.urls import re_path
-from leads.tables import LeadTableDT, ActiveLeadTableDT, RecentArchivedLeadTableDT, ClientCompanyLeadTableDT, LeadToBill, TagTableDT
+import leads.tables as t
import leads.views as v
@@ -14,7 +14,8 @@
re_path(r'^tag/(?P\d+)/$', v.tag, name="tag"),
re_path(r'^tags/(?P\d+)$', v.tags, name="tags"),
re_path(r'^tag/add$', v.add_tag, name="add_tag"),
- re_path(r'^tag/remove/(?P\d+)/(?P\d+)$', v.remove_tag, name="remove_tag"),
+ re_path(r'^tag/add/(?P\d+)/(?P\d+)?$', v.add_tag, name="add_tag"),
+ re_path(r'^tag/remove/(?P\d+)/(?P\d+)$', v.remove_tag, name="remove_tag"),
re_path(r'^tag/manage$', v.manage_tags, name="manage_tags"),
re_path(r'^(?P\d+)/$', v.detail, name="detail"),
re_path(r'^leads$', v.leads, name="leads"),
@@ -32,10 +33,12 @@
re_path(r'^pivotable/(?P\d+)/$', v.leads_pivotable, name="leads-pivotable-year"),
re_path(r'^pivotable/all$', v.leads_pivotable, {"year": "all"}, name="leads-pivotable-all"),
re_path(r'^pivotable/lead/(?P\d+)$', v.lead_pivotable, name="lead_pivotable"),
- re_path(r'^datatable/all-lead/data/$', LeadTableDT.as_view(), name='lead_table_DT'),
- re_path(r'^datatable/active-lead/data/$', ActiveLeadTableDT.as_view(), name='active_lead_table_DT'),
- re_path(r'^datatable/recent-archived-lead/data/$', RecentArchivedLeadTableDT.as_view(), name='recent_archived_lead_table_DT'),
- re_path(r'^datatable/clientcompany-lead/(?P\d+)/data/$', ClientCompanyLeadTableDT.as_view(), name='client_company_lead_table_DT'),
- re_path(r'^datatable/leads-to-bill/data/$', LeadToBill.as_view(), name='leads_to_bill_table_DT'),
- re_path(r'^datatable/all-tags/data/$', TagTableDT.as_view(), name='tag_table_DT'),
+ re_path(r'^datatable/all-lead/data/$', t.LeadTableDT.as_view(), name='lead_table_DT'),
+ re_path(r'^datatable/active-lead/data/$', t.ActiveLeadTableDT.as_view(), name='active_lead_table_DT'),
+ re_path(r'^datatable/recent-archived-lead/data/$', t.RecentArchivedLeadTableDT.as_view(), name='recent_archived_lead_table_DT'),
+ re_path(r'^datatable/clientcompany-lead/(?P\d+)/data/$', t.ClientCompanyLeadTableDT.as_view(), name='client_company_lead_table_DT'),
+ re_path(r'^datatable/suppliercompany-lead/(?P\d+)/data/$', t.SupplierLeadTableDT.as_view(), name='supplier_company_lead_table_DT'),
+ re_path(r'^datatable/businessbroker-lead/(?P\d+)/data/$', t.BusinessBrokerLeadTableDT.as_view(), name='businessbroker_lead_table_DT'),
+ re_path(r'^datatable/leads-to-bill/data/$', t.LeadToBill.as_view(), name='leads_to_bill_table_DT'),
+ re_path(r'^datatable/all-tags/data/$', t.TagTableDT.as_view(), name='tag_table_DT'),
]
diff --git a/leads/views.py b/leads/views.py
index b1421c15..4f168081 100644
--- a/leads/views.py
+++ b/leads/views.py
@@ -29,11 +29,11 @@
from core.utils import sortedValues, COLORS, get_parameter, moving_average, nextMonth
from crm.utils import get_subsidiary_from_session
from leads.models import Lead
-from leads.forms import LeadForm
+from leads.forms import LeadForm, LeadTagForm
from leads.utils import post_save_lead, leads_state_stat
from leads.learn import compute_leads_state, compute_lead_similarity
-from leads.learn import predict_tags, predict_similar
-from core.utils import capitalize, getLeadDirs, createProjectTree, get_fiscal_years_from_qs, to_int_or_round
+from leads.learn import predict_similar
+from core.utils import getLeadDirs, createProjectTree, get_fiscal_years_from_qs, to_int_or_round
from core.decorator import pydici_non_public, pydici_feature
from people.models import Consultant
from people.tasks import compute_consultant_tasks
@@ -88,15 +88,6 @@ def detail(request, lead_id):
previous_lead = None
active_count = None
- # Find suggested tags for this lead except if it has already at least two tags
- tags = lead.tags.all()
- if tags.count() < 3:
- suggestedTags = set(predict_tags(lead))
- suggestedTags -= set(tags)
- else:
- suggestedTags = []
-
-
return render(request, "leads/lead_detail.html",
{"lead": lead,
"active_count": active_count,
@@ -105,9 +96,9 @@ def detail(request, lead_id):
"previous_lead": previous_lead,
"link_root": reverse("core:index"),
"completion_url": reverse("leads:tags", args=[lead.id, ]),
- "suggested_tags": suggestedTags,
"similar_leads": predict_similar(lead),
"enable_doc_tab": bool(settings.DOCUMENT_PROJECT_PATH),
+ "lead_tag_form": LeadTagForm(lead=lead),
"user": request.user})
@pydici_non_public
@@ -260,33 +251,40 @@ def tag(request, tag_id):
@pydici_non_public
@pydici_feature("leads")
@permission_required("leads.change_lead")
-def add_tag(request):
- """Add a tag to a lead. Create the tag if needed"""
- answer = {"tag_created": True, "tag_url": "", "tag_name": ""}
- if request.POST["tag"]:
- tagName = capitalize(request.POST["tag"])
- lead = Lead.objects.get(id=int(request.POST["lead_id"]))
- if tagName in lead.tags.all().values_list("name", flat=True):
- answer["tag_created"] = False
- lead.tags.add(tagName)
- if lead.state not in ("WON", "LOST", "FORGIVEN"):
- compute_leads_state.delay(relearn=False, leads_id=[lead.id,]) # Update (in background) lead proba state as tag are used in computation
- compute_lead_similarity.delay() # update lead similarity model in background
- compute_consultant_tasks.delay(lead.responsible.id) # update consultants tasks in background
- tag = Tag.objects.filter(name=tagName)[0] # We should have only one, but in case of bad data, just take the first one
- answer["tag_url"] = reverse("leads:tag", args=[tag.id, ])
- answer["tag_remove_url"] = reverse("leads:remove_tag", args=[tag.id, lead.id])
- answer["tag_name"] = tag.name
- answer["id"] = tag.id
- return HttpResponse(json.dumps(answer), content_type="application/json")
+def add_tag(request, lead_id, tag_id=None):
+ """Add a tag by id (PUT) to a lead or create (through POST) a new one and attach it and return tag banner."""
+ try:
+ lead = Lead.objects.get(id=lead_id)
+ except Lead.DoesNotExist:
+ return Http404()
+ if request.method == "POST":
+ form = LeadTagForm(request.POST)
+ if form.is_valid():
+ tags = form.cleaned_data["tag"]
+ lead.tags.add(*tags)
+ else: # Returns forms with errors
+ return render(request, "leads/_tags_banner.html", {"lead": lead, "lead_tag_form": form})
+ elif tag_id: # PUT, we add an existing tag
+ try:
+ tag = Tag.objects.get(id=tag_id)
+ lead.tags.add(tag)
+ except Tag.DoesNotExist:
+ return HttpResponse(_("Invalid tag"), status=400)
+ else:
+ return HttpResponse(_("No tag provided"), status=400)
+
+ if lead.state not in ("WON", "LOST", "FORGIVEN"):
+ compute_leads_state.delay(relearn=False, leads_id=[lead.id,]) # Update (in background) lead proba state as tag are used in computation
+ compute_lead_similarity.delay() # update lead similarity model in background
+ compute_consultant_tasks.delay(lead.responsible.id) # update consultants tasks in background
+ return render(request, "leads/_tags_banner.html", {"lead": lead, "lead_tag_form": LeadTagForm(lead=lead) })
@pydici_non_public
@pydici_feature("leads")
@permission_required("leads.change_lead")
-def remove_tag(request, tag_id, lead_id):
- """Remove a tag to a lead"""
- answer = {"error": False, "id": tag_id}
+def remove_tag(request, lead_id, tag_id):
+ """Remove a tag to a lead and return tag banner"""
try:
tag = Tag.objects.get(id=tag_id)
lead = Lead.objects.get(id=lead_id)
@@ -295,8 +293,8 @@ def remove_tag(request, tag_id, lead_id):
compute_leads_state.delay(relearn=False, leads_id=[lead.id, ]) # Update (in background) lead proba state as tag are used in computation
compute_lead_similarity.delay() # update lead similarity model in background
except (Tag.DoesNotExist, Lead.DoesNotExist):
- answer["error"] = True
- return HttpResponse(json.dumps(answer), content_type="application/json")
+ return Http404()
+ return render(request, "leads/_tags_banner.html", {"lead": lead, "lead_tag_form": LeadTagForm(lead=lead)})
@pydici_non_public
@@ -329,6 +327,7 @@ def manage_tags(request):
@pydici_feature("leads")
def tags(request, lead_id):
"""@return: all tags that contains q parameter and are not already associated to this lead as a simple text list"""
+ #TODO: remove this function
tags = Tag.objects.all().exclude(lead__id=lead_id) # Exclude existing tags
tags = tags.filter(name__icontains=request.GET["term"])
tags = tags.values_list("name", flat=True)
diff --git a/locale/fr/LC_MESSAGES/django.mo b/locale/fr/LC_MESSAGES/django.mo
index 618f0843..2ade2a90 100644
Binary files a/locale/fr/LC_MESSAGES/django.mo and b/locale/fr/LC_MESSAGES/django.mo differ
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index c77e227d..ff0315d8 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-06 16:40+0200\n"
-"PO-Revision-Date: 2024-10-06 16:39+0200\n"
+"POT-Creation-Date: 2024-10-21 00:02+0200\n"
+"PO-Revision-Date: 2024-10-21 00:02+0200\n"
"Last-Translator: Sébastien Renard \n"
"Language-Team: French \n"
"Language: fr\n"
@@ -20,11 +20,11 @@ msgstr ""
#: billing/admin.py:27 billing/admin.py:40 billing/forms.py:71
#: billing/models.py:355 core/models.py:82 expense/models.py:102
-#: leads/models.py:140 leads/views.py:199 staffing/models.py:28
+#: leads/models.py:140 leads/views.py:190 staffing/models.py:28
#: staffing/models.py:39 staffing/models.py:74 staffing/models.py:476
-#: templates/billing/bill.html:82 templates/expense/expense.html:27
-#: templates/expense/expense_archive.html:24
-#: templates/expense/expense_list.html:15 templates/leads/lead_detail.html:51
+#: templates/billing/bill.html:82 templates/expense/expense.html:22
+#: templates/expense/expense_archive.html:19
+#: templates/expense/expense_list.html:15 templates/leads/lead_detail.html:52
#: templates/staffing/mission.html:34
msgid "Description"
msgstr "Description"
@@ -38,14 +38,14 @@ msgid "Dates"
msgstr "Dates"
#: billing/admin.py:30 billing/admin.py:43 billing/models.py:172
-#: billing/models.py:265 leads/models.py:78 leads/models.py:300
-#: leads/views.py:200 templates/billing/_lead_billing.html:38
+#: billing/models.py:265 leads/models.py:78 leads/models.py:311
+#: leads/views.py:191 templates/billing/_lead_billing.html:38
#: templates/billing/_lead_billing.html:95
#: templates/billing/client_bill_detail.html:49
#: templates/billing/client_bills_archive.html:18
#: templates/billing/client_bills_in_creation.html:17
#: templates/billing/supplier_bills_archive.html:18
-#: templates/expense/expense.html:76 templates/expense/expense_archive.html:31
+#: templates/expense/expense.html:71 templates/expense/expense_archive.html:26
#: templates/expense/expense_list.html:21
msgid "State"
msgstr "État"
@@ -99,13 +99,13 @@ msgid "Consultant must be defined for time spent mission"
msgstr "Il faut définir le consultant pour les missions au temps passé"
#: billing/forms.py:171 billing/models.py:351 billing/utils.py:175
-#: expense/models.py:159 templates/billing/_lead_billing.html:23
-#: templates/expense/expense.html:13 templates/expense/expenses.html:8
+#: expense/models.py:160 templates/billing/_lead_billing.html:23
+#: templates/expense/expense.html:9 templates/expense/expenses.html:8
msgid "Expense"
msgstr "Note de frais"
#: billing/forms.py:172 billing/models.py:352 expense/models.py:107
-#: templates/expense/expense.html:51 templates/expense/expense_archive.html:33
+#: templates/expense/expense.html:46 templates/expense/expense_archive.html:28
#: templates/expense/expense_list.html:22
msgid "Expense date"
msgstr "Date de dépense"
@@ -121,9 +121,9 @@ msgstr ""
"Si aucune note de frais n'est sélectionnée, indiquez le montant ou le "
"montant TTC"
-#: billing/models.py:74 billing/views.py:152 expense/forms.py:71
-#: expense/forms.py:76 expense/models.py:104 leads/models.py:294
-#: staffing/models.py:72 staffing/views.py:1428
+#: billing/models.py:74 billing/views.py:152 expense/forms.py:81
+#: expense/forms.py:86 expense/models.py:104 leads/models.py:305
+#: staffing/models.py:72 staffing/views.py:1411
#: templates/billing/bill_review.html:207
#: templates/billing/client_bill_detail.html:25
#: templates/billing/client_bills_archive.html:15
@@ -134,7 +134,7 @@ msgstr ""
#: templates/billing/supplier_bills_validation.html:76
#: templates/core/index.html:13 templates/core/index.html:38
#: templates/core/index.html:62 templates/core/index.html:86
-#: templates/expense/expense.html:72 templates/expense/expense_archive.html:25
+#: templates/expense/expense.html:67 templates/expense/expense_archive.html:20
#: templates/expense/expense_list.html:17
msgid "Lead"
msgstr "Affaire"
@@ -162,14 +162,14 @@ msgstr "N° facture"
#: templates/billing/supplier_bills_archive.html:16
#: templates/billing/supplier_bills_validation.html:25
#: templates/billing/supplier_bills_validation.html:80
-#: templates/expense/expense.html:47 templates/expense/expense_archive.html:32
+#: templates/expense/expense.html:42 templates/expense/expense_archive.html:27
#: templates/leads/review.html:53 templates/leads/review.html:86
msgid "Creation date"
msgstr "Date de création"
-#: billing/models.py:77 leads/views.py:200
+#: billing/models.py:77 leads/views.py:191
#: templates/billing/_lead_billing.html:41
-#: templates/billing/_lead_billing.html:98 templates/leads/lead_detail.html:168
+#: templates/billing/_lead_billing.html:98 templates/leads/lead_detail.html:141
#: templates/leads/review.html:54
msgid "Due date"
msgstr "Date d'échéance"
@@ -179,7 +179,7 @@ msgstr "Date d'échéance"
#: templates/billing/_lead_billing.html:99
#: templates/billing/client_bills_archive.html:17
#: templates/billing/supplier_bills_archive.html:17
-#: templates/expense/expense_payments.html:50
+#: templates/expense/expense_payments.html:46
msgid "Payment date"
msgstr "Date de paiement"
@@ -240,7 +240,7 @@ msgid "Sent"
msgstr "Envoyée"
#: billing/models.py:168 billing/models.py:262 expense/models.py:35
-#: templates/expense/_expense_state_column.html:5
+#: templates/expense/_expense_state_column.html:6
msgid "Paid"
msgstr "Payée"
@@ -274,7 +274,7 @@ msgstr "Commentaires client"
#: billing/models.py:179 leads/models.py:83 staffing/models.py:92
#: templates/billing/client_bill_detail.html:33
-#: templates/leads/lead_detail.html:143
+#: templates/leads/lead_detail.html:116
msgid "Client deal id"
msgstr "Référence affaire client"
@@ -334,8 +334,8 @@ msgstr "Libellé"
#: billing/tests.py:268 billing/tests.py:277 billing/tests.py:283
#: billing/tests.py:289 billing/utils.py:164 billing/utils.py:182
#: billing/utils.py:198 billing/utils.py:218 billing/utils.py:225
-#: core/views.py:376 core/views.py:397 crm/views.py:485 staffing/views.py:1633
-#: staffing/views.py:1672 staffing/views.py:1687
+#: core/views.py:376 core/views.py:397 crm/views.py:521 staffing/views.py:1615
+#: staffing/views.py:1654 staffing/views.py:1669
#: templates/billing/_lead_billing.html:24
#: templates/billing/client_billing_control_pivotable.html:56
#: templates/billing/client_billing_control_pivotable.html:62
@@ -353,22 +353,24 @@ msgstr "Libellé"
msgid "amount"
msgstr "montant"
-#: billing/utils.py:152 leads/views.py:621 staffing/views.py:2051
+#: billing/utils.py:152 leads/views.py:620 staffing/views.py:2043
#: templates/billing/client_billing_control_pivotable.html:52
#: templates/crm/clientcompany_detail.html:297
+#: templates/crm/clientcompany_detail.html:321
+#: templates/crm/clientcompany_detail.html:345
#: templates/expense/expense_list.html:16 templates/leads/leads.html:17
#: templates/leads/leads_pivotable.html:80
#: templates/leads/leads_to_bill.html:16
msgid "deal id"
msgstr "N° affaire"
-#: billing/utils.py:153 leads/views.py:623 staffing/views.py:2053
+#: billing/utils.py:153 leads/views.py:622 staffing/views.py:2045
msgid "client organisation"
msgstr "organisation client"
-#: billing/utils.py:154 billing/views.py:155 leads/views.py:624
-#: staffing/views.py:2054 staffing/views.py:2087 staffing/views.py:2103
-#: staffing/views.py:2104
+#: billing/utils.py:154 billing/views.py:155 leads/views.py:623
+#: staffing/views.py:2046 staffing/views.py:2079 staffing/views.py:2095
+#: staffing/views.py:2096
#: templates/billing/client_billing_control_pivotable.html:52
#: templates/billing/client_billing_control_pivotable.html:67
#: templates/billing/payment_delay.html:50
@@ -381,19 +383,19 @@ msgstr "organisation client"
msgid "client company"
msgstr "entreprise"
-#: billing/utils.py:155 leads/views.py:628 staffing/views.py:2057
+#: billing/utils.py:155 leads/views.py:627 staffing/views.py:2049
#: templates/staffing/turnover_pivotable.html:71
msgid "broker"
msgstr "apporteur"
-#: billing/utils.py:155 staffing/views.py:2057
+#: billing/utils.py:155 staffing/views.py:2049
msgid "Direct"
msgstr "Direct"
-#: billing/utils.py:156 core/views.py:373 core/views.py:394 crm/views.py:482
-#: leads/views.py:632 staffing/models.py:456 staffing/views.py:1630
-#: staffing/views.py:1669 staffing/views.py:1684 staffing/views.py:1732
-#: staffing/views.py:2058 staffing/views.py:2091 staffing/views.py:2144
+#: billing/utils.py:156 core/views.py:373 core/views.py:394 crm/views.py:518
+#: leads/views.py:631 staffing/models.py:456 staffing/views.py:1612
+#: staffing/views.py:1651 staffing/views.py:1666 staffing/views.py:1714
+#: staffing/views.py:2050 staffing/views.py:2083 staffing/views.py:2136
#: templates/billing/bill_review.html:34 templates/billing/bill_review.html:82
#: templates/billing/bill_review.html:127
#: templates/billing/bill_review.html:173
@@ -409,6 +411,8 @@ msgstr "Direct"
#: templates/crm/_clientcompany_rates_margin.html:102
#: templates/crm/clientcompany_detail.html:267
#: templates/crm/clientcompany_detail.html:298
+#: templates/crm/clientcompany_detail.html:322
+#: templates/crm/clientcompany_detail.html:346
#: templates/leads/lead_pivotable.html:38 templates/leads/leads.html:18
#: templates/leads/leads_pivotable.html:58
#: templates/leads/leads_pivotable.html:65
@@ -434,9 +438,11 @@ msgstr "Direct"
msgid "subsidiary"
msgstr "filiale"
-#: billing/utils.py:157 billing/utils.py:190 leads/views.py:627
-#: staffing/views.py:2055 templates/crm/clientcompany_detail.html:268
-#: templates/crm/clientcompany_detail.html:299 templates/leads/leads.html:19
+#: billing/utils.py:157 billing/utils.py:190 leads/views.py:626
+#: staffing/views.py:2047 templates/crm/clientcompany_detail.html:268
+#: templates/crm/clientcompany_detail.html:299
+#: templates/crm/clientcompany_detail.html:323
+#: templates/crm/clientcompany_detail.html:347 templates/leads/leads.html:19
#: templates/leads/leads_pivotable.html:58
#: templates/leads/leads_pivotable.html:65
#: templates/leads/leads_to_bill.html:18
@@ -449,9 +455,9 @@ msgid "manager"
msgstr "manager"
#: billing/utils.py:159 billing/utils.py:216 billing/utils.py:227
-#: crm/views.py:480 staffing/models.py:455 staffing/views.py:1629
-#: staffing/views.py:1667 staffing/views.py:1682 staffing/views.py:1731
-#: staffing/views.py:2143 templates/billing/_lead_billing.html:20
+#: crm/views.py:516 staffing/models.py:455 staffing/views.py:1611
+#: staffing/views.py:1649 staffing/views.py:1664 staffing/views.py:1713
+#: staffing/views.py:2135 templates/billing/_lead_billing.html:20
#: templates/billing/pre_billing.html:92 templates/billing/pre_billing.html:159
#: templates/crm/_clientcompany_rates_margin.html:102
#: templates/crm/_clientcompany_rates_margin.html:108
@@ -468,8 +474,8 @@ msgid "consultant"
msgstr "consultant"
#: billing/utils.py:165 billing/utils.py:179 billing/utils.py:196
-#: billing/utils.py:217 billing/utils.py:224 staffing/views.py:1729
-#: staffing/views.py:2084 staffing/views.py:2092 staffing/views.py:2142
+#: billing/utils.py:217 billing/utils.py:224 staffing/views.py:1711
+#: staffing/views.py:2076 staffing/views.py:2084 staffing/views.py:2134
#: templates/billing/_lead_billing.html:21
#: templates/billing/client_billing_control_pivotable.html:52
#: templates/billing/client_billing_control_pivotable.html:55
@@ -491,8 +497,8 @@ msgstr "mois"
#: billing/utils.py:166 billing/utils.py:180 billing/utils.py:197
#: billing/utils.py:219 billing/utils.py:226 core/views.py:372
-#: core/views.py:393 staffing/views.py:1534 staffing/views.py:1631
-#: staffing/views.py:1670 staffing/views.py:1685 staffing/views.py:1730
+#: core/views.py:393 staffing/views.py:1517 staffing/views.py:1613
+#: staffing/views.py:1652 staffing/views.py:1667 staffing/views.py:1712
#: templates/billing/_lead_billing.html:20
#: templates/billing/client_billing_control_pivotable.html:53
#: templates/core/risks.html:41 templates/core/risks.html:48
@@ -519,7 +525,7 @@ msgid "mission"
msgstr "mission"
#: billing/utils.py:170 billing/utils.py:181 billing/utils.py:191
-#: staffing/models.py:457 staffing/views.py:2056
+#: staffing/models.py:457 staffing/views.py:2048
#: templates/billing/_lead_billing.html:20
#: templates/billing/client_billing_control_pivotable.html:52
#: templates/billing/client_billing_control_pivotable.html:74
@@ -541,7 +547,7 @@ msgstr "Frais refacturables"
msgid "Done work"
msgstr "Montant réalisé"
-#: billing/views.py:153 leads/forms.py:74 leads/models.py:73
+#: billing/views.py:153 leads/forms.py:114 leads/models.py:73
#: staffing/models.py:86 templates/billing/bill_review.html:36
#: templates/billing/bill_review.html:208
#: templates/billing/payment_delay.html:89
@@ -549,9 +555,9 @@ msgstr "Montant réalisé"
#: templates/billing/supplier_bills_validation.html:77
#: templates/core/index.html:14 templates/core/index.html:39
#: templates/core/index.html:63 templates/core/index.html:87
-#: templates/leads/lead_detail.html:150 templates/leads/review.html:48
+#: templates/leads/lead_detail.html:123 templates/leads/review.html:48
#: templates/leads/review.html:81 templates/staffing/fixed_price_report.html:18
-#: templates/staffing/mission.html:97
+#: templates/staffing/mission.html:98
msgid "Responsible"
msgstr "Responsable"
@@ -566,25 +572,25 @@ msgstr "Responsable"
#: templates/billing/payment_delay.html:82
#: templates/billing/payment_delay.html:97
#: templates/billing/supplier_bills_archive.html:14
-#: templates/leads/lead_detail.html:147 templates/leads/review.html:47
+#: templates/leads/lead_detail.html:120 templates/leads/review.html:47
#: templates/leads/review.html:80 templates/staffing/fixed_price_report.html:17
-#: templates/staffing/mission.html:94
+#: templates/staffing/mission.html:95
msgid "Subsidiary"
msgstr "Filiale"
-#: billing/views.py:156 leads/forms.py:77 leads/models.py:75
+#: billing/views.py:156 leads/forms.py:117 leads/models.py:75
#: templates/billing/payment_delay.html:66
-#: templates/billing/payment_delay.html:74 templates/leads/lead_detail.html:164
+#: templates/billing/payment_delay.html:74 templates/leads/lead_detail.html:137
msgid "Paying authority"
msgstr "Organisme payeur"
-#: billing/views.py:157 staffing/models.py:76 staffing/views.py:1428
+#: billing/views.py:157 staffing/models.py:76 staffing/views.py:1411
#: templates/billing/payment_delay.html:50
#: templates/billing/payment_delay.html:58
#: templates/billing/payment_delay.html:66
#: templates/billing/payment_delay.html:74
-#: templates/billing/payment_delay.html:89 templates/leads/lead_detail.html:232
-#: templates/staffing/mission.html:124 templates/staffing/mission.html:168
+#: templates/billing/payment_delay.html:89 templates/leads/lead_detail.html:205
+#: templates/staffing/mission.html:125 templates/staffing/mission.html:169
msgid "Billing mode"
msgstr "Mode de facturation"
@@ -606,7 +612,9 @@ msgstr "retard de paiement"
#: billing/views.py:161 templates/billing/payment_delay.html:83
#: templates/billing/payment_delay.html:98
-#: templates/crm/clientcompany_detail.html:302 templates/leads/leads.html:22
+#: templates/crm/clientcompany_detail.html:302
+#: templates/crm/clientcompany_detail.html:326
+#: templates/crm/clientcompany_detail.html:350 templates/leads/leads.html:22
#: templates/leads/leads_to_bill.html:19
msgid "creation date"
msgstr "date de création"
@@ -654,14 +662,14 @@ msgstr ""
"Impossible de supprimer une facture dans l'état %s. Vous pouvez par contre "
"l'annuler."
-#: billing/views.py:606 people/utils.py:32 staffing/views.py:607
-#: staffing/views.py:825 staffing/views.py:1562
+#: billing/views.py:606 people/utils.py:32 staffing/views.py:592
+#: staffing/views.py:810 staffing/views.py:1544
#, python-format
msgid "team %(manager_name)s"
msgstr "équipe %(manager_name)s"
-#: billing/views.py:618 people/utils.py:22 staffing/views.py:626
-#: staffing/views.py:837 staffing/views.py:1607
+#: billing/views.py:618 people/utils.py:22 staffing/views.py:611
+#: staffing/views.py:822 staffing/views.py:1589
msgid "Everybody"
msgstr "Tout le monde"
@@ -800,13 +808,14 @@ msgstr "On s'est déjà vu ?"
msgid "sorry, I don't know you"
msgstr "Désolé, on ne se connaît pas"
-#: core/forms.py:53 expense/forms.py:68 staffing/forms.py:217
+#: core/forms.py:53 expense/forms.py:78 staffing/forms.py:217
#: templates/billing/client_bill_form.html:29
#: templates/billing/supplier_bill_form.html:20
+#: templates/crm/_mission_contact_form.html:12
#: templates/crm/client-popup.html:11
#: templates/staffing/consultant_staffing.html:63
#: templates/staffing/consultant_timesheet.html:153
-#: templates/staffing/mission_contacts.html:39
+#: templates/staffing/mission_contacts.html:43
#: templates/staffing/mission_staffing.html:77
msgid "Save"
msgstr "Enregistrer"
@@ -959,7 +968,7 @@ msgstr "Clé"
msgid "Value"
msgstr "Valeur"
-#: core/models.py:81 staffing/models.py:75 templates/staffing/mission.html:91
+#: core/models.py:81 staffing/models.py:75 templates/staffing/mission.html:92
msgid "Type"
msgstr "Type"
@@ -991,17 +1000,19 @@ msgstr "n° d'affaire"
msgid "deal"
msgstr "affaire"
-#: core/views.py:377 core/views.py:398 crm/models.py:482 crm/models.py:511
-#: staffing/views.py:1534 templates/core/risks.html:42
+#: core/views.py:377 core/views.py:398 crm/models.py:482 crm/models.py:510
+#: staffing/views.py:1517 templates/core/risks.html:42
#: templates/crm/_contact_list.html:10
#: templates/crm/businessbroker_list.html:13 templates/crm/contact_list.html:13
#: templates/crm/supplier_list.html:13
msgid "company"
msgstr "société"
-#: core/views.py:378 core/views.py:399 crm/views.py:483 crm/views.py:543
+#: core/views.py:378 core/views.py:399 crm/views.py:519 crm/views.py:579
#: templates/crm/_clientcompany_rates_margin.html:114
#: templates/crm/clientcompany_detail.html:295
+#: templates/crm/clientcompany_detail.html:319
+#: templates/crm/clientcompany_detail.html:343
#: templates/crm/company_pivotable.html:49 templates/leads/leads.html:15
#: templates/leads/leads_to_bill.html:14
msgid "client"
@@ -1016,7 +1027,7 @@ msgid "Yes"
msgstr "Oui"
#: core/views.py:424 templates/core/delete.html:17
-#: templates/expense/expense.html:88
+#: templates/expense/expense.html:83
msgid "No"
msgstr "Non"
@@ -1065,16 +1076,16 @@ msgid "Leave blank to use standard name"
msgstr "Laissez vide pour utiliser le nom standard"
#: crm/forms.py:208 crm/models.py:142
-#: templates/crm/clientcompany_detail.html:363
-#: templates/staffing/mission_contacts.html:13
+#: templates/crm/clientcompany_detail.html:410
+#: templates/staffing/mission_contacts.html:17
msgid "Company"
msgstr "Société"
#: crm/forms.py:228 crm/models.py:301 crm/models.py:328 crm/models.py:359
-#: crm/models.py:483 crm/models.py:516
+#: crm/models.py:483 crm/models.py:515
#: templates/crm/_clientcompany_rates_margin.html:15
#: templates/crm/_clientcompany_rates_margin.html:48
-#: templates/leads/lead_detail.html:135
+#: templates/leads/lead_detail.html:108
#: templates/people/subcontractor_detail.html:8
msgid "Contact"
msgstr "Contact"
@@ -1103,13 +1114,13 @@ msgstr "n° d'immatriculation"
msgid "VAT id"
msgstr "n° TVA"
-#: crm/models.py:60 crm/models.py:82 crm/models.py:184 crm/models.py:500
-#: expense/models.py:67 leads/models.py:65 leads/views.py:199
+#: crm/models.py:60 crm/models.py:82 crm/models.py:184 crm/models.py:499
+#: expense/models.py:67 leads/models.py:65 leads/views.py:190
#: people/models.py:31 people/models.py:327
-#: templates/crm/clientcompany_detail.html:362
+#: templates/crm/clientcompany_detail.html:409
#: templates/leads/manage_tags.html:22 templates/leads/review.html:45
#: templates/leads/review.html:78 templates/staffing/fixed_price_report.html:15
-#: templates/staffing/mission_contacts.html:12
+#: templates/staffing/mission_contacts.html:16
msgid "Name"
msgstr "Nom"
@@ -1117,7 +1128,7 @@ msgstr "Nom"
msgid "Code"
msgstr "Code"
-#: crm/models.py:88 crm/models.py:106 crm/models.py:151 staffing/views.py:2060
+#: crm/models.py:88 crm/models.py:106 crm/models.py:151 staffing/views.py:2052
#: templates/crm/clientcompany_ranking.html:14
#: templates/staffing/turnover_pivotable.html:109
#: templates/staffing/turnover_pivotable.html:117
@@ -1170,23 +1181,23 @@ msgid "Billing language"
msgstr "Langue des factures"
#: crm/models.py:186 people/models.py:332
-#: templates/staffing/mission_contacts.html:16
+#: templates/staffing/mission_contacts.html:20
msgid "Phone"
msgstr "Téléphone"
-#: crm/models.py:187 templates/crm/clientcompany_detail.html:367
-#: templates/staffing/mission_contacts.html:17
+#: crm/models.py:187 templates/crm/clientcompany_detail.html:414
+#: templates/staffing/mission_contacts.html:21
msgid "Mobile phone"
msgstr "Téléphone mobile"
-#: crm/models.py:188 templates/crm/clientcompany_detail.html:368
-#: templates/staffing/mission_contacts.html:18
+#: crm/models.py:188 templates/crm/clientcompany_detail.html:415
+#: templates/staffing/mission_contacts.html:22
msgid "Fax"
msgstr "Fax"
-#: crm/models.py:189 crm/models.py:512
-#: templates/crm/clientcompany_detail.html:364
-#: templates/staffing/mission_contacts.html:14
+#: crm/models.py:189 crm/models.py:511
+#: templates/crm/clientcompany_detail.html:411
+#: templates/staffing/mission_contacts.html:18
msgid "Function"
msgstr "Fonction"
@@ -1207,7 +1218,7 @@ msgstr "%s affaires"
msgid "Broker company"
msgstr "Société de l'apporteur"
-#: crm/models.py:321 leads/forms.py:76 leads/models.py:74
+#: crm/models.py:321 leads/forms.py:116 leads/models.py:74
msgid "Business broker"
msgstr "Apporteur d'affaires"
@@ -1269,7 +1280,7 @@ msgstr "Alignement stratégique"
msgid "Active"
msgstr "Active"
-#: crm/models.py:476 leads/models.py:80 leads/views.py:199
+#: crm/models.py:476 leads/models.py:80 leads/views.py:190
#: templates/billing/bill_review.html:35 templates/billing/bill_review.html:83
#: templates/billing/bill_review.html:128
#: templates/billing/bill_review.html:174
@@ -1283,23 +1294,23 @@ msgstr "Active"
msgid "Client"
msgstr "Client"
-#: crm/models.py:494
+#: crm/models.py:493
msgid "Mission contact"
msgstr "Contact mission"
-#: crm/models.py:513 templates/crm/clientcompany_detail.html:366
+#: crm/models.py:512 templates/crm/clientcompany_detail.html:413
msgid "Phone Switchboard"
msgstr "Standard téléphonique"
-#: crm/models.py:514
+#: crm/models.py:513
msgid "Generic email"
msgstr "Courriel générique"
-#: crm/models.py:515
+#: crm/models.py:514
msgid "Generic fax"
msgstr "Fax générique"
-#: crm/models.py:554
+#: crm/models.py:553
msgid "Administrative contact"
msgstr "Contact administratif"
@@ -1307,23 +1318,23 @@ msgstr "Contact administratif"
msgid "Contact removed successfully"
msgstr "Contact supprimé avec succès "
-#: crm/views.py:400
+#: crm/views.py:434
msgid "overdue"
msgstr "en retard"
-#: crm/views.py:401
+#: crm/views.py:435
msgid "soon due"
msgstr "bientôt en retard"
-#: crm/views.py:402
+#: crm/views.py:436
msgid "last 12 months"
msgstr "12 derniers mois"
-#: crm/views.py:481 staffing/views.py:1668 staffing/views.py:1683
+#: crm/views.py:517 staffing/views.py:1650 staffing/views.py:1665
msgid "profile"
msgstr "profile"
-#: crm/views.py:484 staffing/views.py:1671 staffing/views.py:1686
+#: crm/views.py:520 staffing/views.py:1653 staffing/views.py:1668
#: templates/crm/_clientcompany_rates_margin.html:101
#: templates/crm/_clientcompany_rates_margin.html:107
#: templates/crm/_clientcompany_rates_margin.html:113
@@ -1333,38 +1344,38 @@ msgstr "profile"
msgid "period"
msgstr "période"
-#: crm/views.py:614
+#: crm/views.py:650
msgid "Others"
msgstr "Autres"
-#: expense/forms.py:88
+#: expense/forms.py:98
msgid "Attached file must be less than 10 Mb"
msgstr "Le fichier joint doit faire moins de 10 Mo"
-#: expense/forms.py:92
+#: expense/forms.py:102
#, python-format
msgid "Use a valid extension (%s)"
msgstr "Utilisez une extension valide (%s)"
-#: expense/forms.py:98
+#: expense/forms.py:108
msgid "You must define a lead if expense is chargeable"
msgstr "Vous devez définir une affaire si les frais sont refacturables"
-#: expense/forms.py:104 expense/models.py:160
+#: expense/forms.py:114 expense/models.py:161
#: templates/core/_pydici_menu.html:143 templates/core/_pydici_menu.html:146
#: templates/core/_pydici_menu.html:238 templates/staffing/mission.html:43
msgid "Expenses"
msgstr "Notes de frais"
-#: expense/forms.py:104
+#: expense/forms.py:114
msgid "Select expenses to pay..."
msgstr "Choisissez les notes de frais à rembourser..."
-#: expense/forms.py:105
+#: expense/forms.py:115
msgid "payment date"
msgstr "date de paiement"
-#: expense/forms.py:124
+#: expense/forms.py:134
msgid "All expenses of a payment must belongs to same user"
msgstr ""
"Toutes les notes de frais d'un remboursement doivent appartenir au même "
@@ -1414,13 +1425,13 @@ msgstr "Catégories de dépenses"
msgid "Expenses payment"
msgstr "Remboursement de frais"
-#: expense/models.py:97 templates/expense/expense_payments.html:12
-#: templates/expense/expense_payments.html:43
+#: expense/models.py:97 templates/expense/expense_payments.html:8
+#: templates/expense/expense_payments.html:39
msgid "Expenses payments"
msgstr "Remboursements de frais"
-#: expense/models.py:105 templates/expense/expense.html:68
-#: templates/expense/expense_archive.html:29
+#: expense/models.py:105 templates/expense/expense.html:63
+#: templates/expense/expense_archive.html:24
#: templates/expense/expense_list.html:19
msgid "Chargeable"
msgstr "Refacturable"
@@ -1432,31 +1443,31 @@ msgid "Date"
msgstr "Date"
#: expense/models.py:108 leads/models.py:85 leads/models.py:132
-#: leads/views.py:201 staffing/models.py:82 templates/core/index.html:18
+#: leads/views.py:192 staffing/models.py:82 templates/core/index.html:18
#: templates/core/index.html:43 templates/core/index.html:67
#: templates/core/index.html:91
msgid "Updated"
msgstr "Mise à jour"
-#: expense/models.py:109 expense/tables.py:191 templates/billing/bill.html:166
+#: expense/models.py:109 expense/tables.py:188 templates/billing/bill.html:166
#: templates/billing/client_bill_detail.html:37
-#: templates/expense/expense.html:39 templates/expense/expense_archive.html:26
+#: templates/expense/expense.html:34 templates/expense/expense_archive.html:21
#: templates/expense/expense_list.html:18
-#: templates/expense/expense_payments.html:49
+#: templates/expense/expense_payments.html:45
msgid "Amount"
msgstr "Montant"
-#: expense/models.py:110 templates/expense/expense.html:43
-#: templates/expense/expense_archive.html:27
+#: expense/models.py:110 templates/expense/expense.html:38
+#: templates/expense/expense_archive.html:22
msgid "VAT (€)"
msgstr "TVA (€)"
-#: expense/models.py:111 templates/expense/expense.html:35
-#: templates/expense/expense_archive.html:23
+#: expense/models.py:111 templates/expense/expense.html:30
+#: templates/expense/expense_archive.html:18
msgid "Category"
msgstr "Catégorie"
-#: expense/models.py:112 templates/expense/expense_archive.html:28
+#: expense/models.py:112 templates/expense/expense_archive.html:23
#: templates/expense/expense_list.html:20
msgid "Receipt"
msgstr "Justificatif"
@@ -1465,7 +1476,7 @@ msgstr "Justificatif"
msgid "Paid with corporate card"
msgstr "Payé avec la carte société"
-#: expense/models.py:117 leads/views.py:629
+#: expense/models.py:117 leads/views.py:628
#: templates/leads/leads_pivotable.html:52
#: templates/leads/leads_pivotable.html:67
#: templates/leads/leads_pivotable.html:75
@@ -1473,157 +1484,129 @@ msgstr "Payé avec la carte société"
msgid "state"
msgstr "état"
-#: expense/tables.py:122 expense/tables.py:190 people/models.py:273
-#: staffing/views.py:1429 templates/billing/bill.html:164
-#: templates/expense/expense.html:31 templates/expense/expense_archive.html:22
-#: templates/expense/expense_payments.html:48
-#: templates/staffing/mission_consultants.html:9
+#: expense/tables.py:123 expense/tables.py:187 people/models.py:273
+#: staffing/views.py:1412 templates/billing/bill.html:164
+#: templates/expense/expense.html:26 templates/expense/expense_archive.html:17
+#: templates/expense/expense_payments.html:44
+#: templates/staffing/mission_consultants.html:10
#: templates/staffing/mission_staffing.html:22
#: templates/staffing/optimise_pdc.html:85
msgid "Consultant"
msgstr "Consultant"
-#: expense/tables.py:152
-msgid "Edit"
-msgstr "Modifier"
-
-#. Translators: Ed is the short term for Edit
-#: expense/tables.py:155
-msgid "Ed"
-msgstr "Mo"
-
-#: expense/tables.py:157
-msgid "Delete"
-msgstr "Supprimer"
-
-#. Translators: De is the short term for Delete
-#: expense/tables.py:160
-msgid "De"
-msgstr "Su"
-
-#: expense/tables.py:162
-msgid "Clone"
-msgstr "Cloner"
-
-#. Translators: Cl is the short term for Clone
-#: expense/tables.py:165
-msgid "Cl"
-msgstr "Cl"
-
-#: expense/tables.py:193
+#: expense/tables.py:190
msgid "detail"
msgstr "détail"
-#: expense/tables.py:194
+#: expense/tables.py:191
msgid "change"
msgstr "modifier"
-#: expense/views.py:87 expense/views.py:186
+#: expense/views.py:90 expense/views.py:185
msgid "You are not allowed to edit that expense"
msgstr "Vous n'êtes pas autorisé à modifier cette note de frais"
-#: expense/views.py:91 expense/views.py:190 expense/views.py:262
-#: expense/views.py:302
+#: expense/views.py:94 expense/views.py:189 expense/views.py:260
#, python-format
msgid "Expense %s does not exist"
msgstr "La note de frais %s n'existe pas"
-#: expense/views.py:195
+#: expense/views.py:194
#, python-format
msgid "Expense %s has been deleted"
msgstr "La note de frais %s a été supprimée"
-#: expense/views.py:272
-msgid "Successfully update expense"
-msgstr "Note de frais mise à jour avec succès"
-
-#: expense/views.py:304 staffing/views.py:1811
-msgid "Incorrect value"
-msgstr "Valeur incorrecte"
+#: expense/views.py:292
+msgid "Expense does not exist"
+msgstr "La note de frais n'existe pas"
-#: expense/views.py:384
+#: expense/views.py:386
#, python-format
msgid "Expense payment %s does not exist"
msgstr "Le remboursement de frais %s n'existe pas"
-#: leads/admin.py:21 leads/forms.py:94
+#: leads/admin.py:21 leads/forms.py:134
msgid "State and tracking"
msgstr "État et suivi"
-#: leads/admin.py:22 leads/forms.py:101
+#: leads/admin.py:22 leads/forms.py:141
msgid "Commercial"
msgstr "Commercial"
-#: leads/admin.py:23 leads/forms.py:107 leads/views.py:200
+#: leads/admin.py:23 leads/forms.py:147 leads/views.py:191
#: staffing/models.py:507 templates/billing/bill_review.html:209
#: templates/leads/review.html:49 templates/leads/review.html:82
msgid "Staffing"
msgstr "Staffing"
-#: leads/forms.py:75 leads/models.py:70 leads/views.py:199 people/models.py:344
-#: templates/core/index.html:15 templates/core/index.html:40
-#: templates/core/index.html:64 templates/core/index.html:88
-#: templates/leads/lead_detail.html:154
+#: leads/forms.py:105
+msgid "New tags"
+msgstr "Nouvelles étiquettes"
+
+#: leads/forms.py:115 leads/models.py:70 leads/views.py:190
+#: people/models.py:344 templates/core/index.html:15
+#: templates/core/index.html:40 templates/core/index.html:64
+#: templates/core/index.html:88 templates/leads/lead_detail.html:127
msgid "Salesman"
msgstr "Commercial"
-#: leads/forms.py:76
+#: leads/forms.py:116
msgid "If the leads was brought by a third party"
msgstr "Si l'affaire a été apportée par un tiers"
-#: leads/forms.py:77
+#: leads/forms.py:117
msgid "If payment is done by a third party"
msgstr "Si la facturation est faite via un tiers"
-#: leads/forms.py:85 staffing/forms.py:351
+#: leads/forms.py:125 staffing/forms.py:351
msgid "Identification"
msgstr "Identification"
-#: leads/forms.py:86
+#: leads/forms.py:126
msgid "Name of the lead. don't include client name"
msgstr "Nom de l'affaire. Ne pas inclure le nom du client"
-#: leads/forms.py:93
+#: leads/forms.py:133
msgid "Next commercial action to be done"
msgstr "Prochaine action commercial à prendre"
-#: leads/forms.py:96 staffing/forms.py:369
+#: leads/forms.py:136 staffing/forms.py:369
msgid "Leave blank to auto generate"
msgstr "Laissez vide pour auto générer"
-#: leads/forms.py:97
+#: leads/forms.py:137
msgid "Due date for next step"
msgstr "Échéance de la prochaine étape"
-#: leads/forms.py:98
+#: leads/forms.py:138
msgid "Internal client reference"
msgstr "Référence interne du client"
-#: leads/forms.py:99
+#: leads/forms.py:139
msgid "Date of the operational start"
msgstr "Date du démarrage effectif"
-#: leads/forms.py:107
+#: leads/forms.py:147
msgid "People that could contribute..."
msgstr "Personnes qui pourraient contribuer..."
-#: leads/forms.py:108
+#: leads/forms.py:148
msgid "People outside company that could contribute..."
msgstr "Personnes externes à la société qui pourraient contribuer..."
-#: leads/forms.py:121
+#: leads/forms.py:161
msgid "Sales amount must be defined at this step of the commercial process"
msgstr ""
"Le montant de l'affaire doit être connu à cette étape du processus d'avant-"
"vente"
-#: leads/forms.py:131
+#: leads/forms.py:170
msgid "Start date must be defined at this step of the commercial process"
msgstr ""
"La date de démarrage de l'affaire doit être connue à cette étape du "
"processus d'avant-vente"
-#: leads/forms.py:142
+#: leads/forms.py:181
msgid ""
"Deal id must be unique. Use another value or let the field blank for "
"automatic computation"
@@ -1665,12 +1648,12 @@ msgstr "Abandonnée"
msgid "Sleeping"
msgstr "En sommeil"
-#: leads/models.py:67 templates/leads/lead_detail.html:213
-#: templates/staffing/mission.html:144
+#: leads/models.py:67 templates/leads/lead_detail.html:186
+#: templates/staffing/mission.html:145
msgid "Administrative notes"
msgstr "Commentaires administratifs"
-#: leads/models.py:68 templates/leads/lead_detail.html:186
+#: leads/models.py:68 templates/leads/lead_detail.html:159
msgid "Action"
msgstr "Action"
@@ -1682,7 +1665,7 @@ msgstr "Montant (k€)"
msgid "External staffing"
msgstr "Staffing externe"
-#: leads/models.py:76 leads/views.py:199 people/models.py:320
+#: leads/models.py:76 leads/views.py:190 people/models.py:320
msgid "Starting"
msgstr "Démarrage"
@@ -1690,13 +1673,13 @@ msgstr "Démarrage"
msgid "Due"
msgstr "Échéance"
-#: leads/models.py:81 leads/views.py:200
+#: leads/models.py:81 leads/views.py:191
msgid "Creation"
msgstr "Création"
-#: leads/models.py:82 staffing/views.py:1054 staffing/views.py:1428
+#: leads/models.py:82 staffing/views.py:1037 staffing/views.py:1411
#: templates/billing/client_bills_archive.html:14
-#: templates/leads/lead_detail.html:139 templates/leads/review.html:46
+#: templates/leads/lead_detail.html:112 templates/leads/review.html:46
#: templates/leads/review.html:79
msgid "Deal id"
msgstr "N° affaire"
@@ -1705,7 +1688,7 @@ msgstr "N° affaire"
msgid "Send lead by email"
msgstr "Envoyer par courriel"
-#: leads/models.py:301
+#: leads/models.py:312
msgid "Score"
msgstr "Score"
@@ -1742,61 +1725,71 @@ msgstr "La probabilité de la mission a été mise à 100 %"
msgid "According mission has been archived"
msgstr "La mission correspondante a été archivée"
-#: leads/views.py:195
+#: leads/views.py:186
msgid "leads.csv"
msgstr "affaires.csv"
-#: leads/views.py:199
+#: leads/views.py:190
msgid "Managed by"
msgstr "Géré par"
-#: leads/views.py:200 templates/billing/bill_review.html:210
+#: leads/views.py:191 templates/billing/bill_review.html:210
#: templates/core/index.html:16 templates/core/index.html:41
#: templates/core/index.html:65 templates/core/index.html:89
#: templates/crm/clientcompany_ranking.html:19
-#: templates/leads/lead_detail.html:177
+#: templates/leads/lead_detail.html:150
msgid "Sales (k€)"
msgstr "C.A. (k€)"
-#: leads/views.py:363 templates/core/_pydici_menu.html:39
+#: leads/views.py:272
+msgid "Invalid tag"
+msgstr "Étiquette invalide"
+
+#: leads/views.py:274
+msgid "No tag provided"
+msgstr "Aucune étiquette fournie"
+
+#: leads/views.py:362 templates/core/_pydici_menu.html:39
#: templates/leads/leads.html:11
msgid "All leads"
msgstr "Toutes les affaires"
-#: leads/views.py:363 templates/crm/graph_company_business_activity.html:30
+#: leads/views.py:362 templates/crm/graph_company_business_activity.html:30
msgid "Won leads"
msgstr "Affaires gagnées"
-#: leads/views.py:525
+#: leads/views.py:524
#, python-format
msgid "Last %s days"
msgstr "%s derniers jours"
-#: leads/views.py:540 templates/leads/graph_leads_activity.html:49
+#: leads/views.py:539 templates/leads/graph_leads_activity.html:49
msgid "duration"
msgstr "durée"
-#: leads/views.py:541 templates/leads/graph_leads_activity.html:50
+#: leads/views.py:540 templates/leads/graph_leads_activity.html:50
msgid "average duration 6 months"
msgstr "durée moyenne sur 6 mois"
-#: leads/views.py:550
+#: leads/views.py:549
#, python-format
msgid "%s leads in progress"
msgstr "%s affaires en cours"
-#: leads/views.py:602 templates/leads/leads_pivotable.html:59
+#: leads/views.py:601 templates/leads/leads_pivotable.html:59
msgid "sales (interval)"
msgstr "prix (interval)"
-#: leads/views.py:602 leads/views.py:625
+#: leads/views.py:601 leads/views.py:624
msgid "sales (k€)"
msgstr "C.A. (k€)"
-#: leads/views.py:622 staffing/views.py:1534 staffing/views.py:2052
+#: leads/views.py:621 staffing/views.py:1517 staffing/views.py:2044
#: templates/crm/_contact_list.html:9
#: templates/crm/clientcompany_detail.html:266
#: templates/crm/clientcompany_detail.html:296
+#: templates/crm/clientcompany_detail.html:320
+#: templates/crm/clientcompany_detail.html:344
#: templates/crm/contact_detail.html:20 templates/crm/contact_list.html:12
#: templates/leads/leads.html:16 templates/leads/leads_to_bill.html:15
#: templates/people/consultants_tasks.html:17
@@ -1804,7 +1797,7 @@ msgstr "C.A. (k€)"
msgid "name"
msgstr "nom"
-#: leads/views.py:626 staffing/models.py:458
+#: leads/views.py:625 staffing/models.py:458
#: templates/crm/company_pivotable.html:50
#: templates/crm/company_pivotable.html:56
#: templates/crm/company_pivotable.html:62
@@ -1813,16 +1806,16 @@ msgstr "nom"
msgid "date"
msgstr "date"
-#: leads/views.py:630 templates/leads/leads_pivotable.html:68
+#: leads/views.py:629 templates/leads/leads_pivotable.html:68
msgid "billed (€)"
msgstr "facturé (€)"
-#: leads/views.py:631 templates/leads/leads_pivotable.html:76
+#: leads/views.py:630 templates/leads/leads_pivotable.html:76
#: templates/leads/leads_pivotable.html:84
msgid "Over budget margin (€)"
msgstr "Marge au delà du budget (€)"
-#: leads/views.py:633
+#: leads/views.py:632
msgid "active"
msgstr "active"
@@ -1855,7 +1848,7 @@ msgstr "Profil"
msgid "Subcontractor"
msgstr "Sous-traitant"
-#: people/models.py:317 staffing/views.py:1685
+#: people/models.py:317 staffing/views.py:1667
#: templates/people/consultant_detail.html:237
#: templates/people/consultant_detail.html:243
#: templates/staffing/_consultant_prod_tooltip.html:2
@@ -1863,7 +1856,7 @@ msgstr "Sous-traitant"
msgid "daily rate"
msgstr "taux journalier"
-#: people/models.py:318 staffing/views.py:1670 staffing/views.py:2215
+#: people/models.py:318 staffing/views.py:1652 staffing/views.py:2207
#: templates/staffing/_consultant_prod_tooltip.html:1
#: templates/staffing/graph_timesheet_rates_bar.html:15
#: templates/staffing/graph_timesheet_rates_bar.html:25
@@ -2154,7 +2147,7 @@ msgstr "Charge"
#: staffing/forms.py:212 templates/billing/_lead_billing.html:43
#: templates/billing/_lead_billing.html:100
#: templates/billing/client_bill_detail.html:53
-#: templates/expense/expense.html:80 templates/expense/expense_archive.html:35
+#: templates/expense/expense.html:75 templates/expense/expense_archive.html:30
msgid "Comment"
msgstr "Remarques"
@@ -2171,7 +2164,7 @@ msgstr "Dates de staffing"
msgid "mission id: %s"
msgstr "n° de mission : %s"
-#: staffing/forms.py:324 staffing/views.py:1367
+#: staffing/forms.py:324 staffing/views.py:1350
msgid "Days without lunch ticket"
msgstr "Jours sans ticket restaurant"
@@ -2385,15 +2378,15 @@ msgctxt "masculine"
msgid "None"
msgstr "Aucun"
-#: staffing/models.py:73 staffing/views.py:1428
+#: staffing/models.py:73 staffing/views.py:1411
#: templates/crm/clientcompany_detail.html:270
#: templates/staffing/_mission_table.html:20
#: templates/staffing/fixed_price_report.html:16
-#: templates/staffing/mission.html:100
+#: templates/staffing/mission.html:101
msgid "Mission id"
msgstr "N° de mission"
-#: staffing/models.py:77 templates/staffing/mission.html:127
+#: staffing/models.py:77 templates/staffing/mission.html:128
msgid "Management mode"
msgstr "Mode de gestion"
@@ -2420,7 +2413,7 @@ msgstr "code analytique"
msgid "marketing product"
msgstr "produit marketing"
-#: staffing/models.py:89 templates/leads/lead_detail.html:171
+#: staffing/models.py:89 templates/leads/lead_detail.html:144
#: templates/leads/review.html:55 templates/leads/review.html:87
msgid "Start date"
msgstr "Date de début"
@@ -2434,7 +2427,7 @@ msgid "Min charge multiple per day"
msgstr "Multiple mini de charge par jour"
#: staffing/models.py:432 templates/crm/clientcompany_detail.html:253
-#: templates/staffing/mission.html:104
+#: templates/staffing/mission.html:105
msgid "undefined"
msgstr "À définir"
@@ -2467,10 +2460,10 @@ msgstr "prévu (jours)"
msgid "forecast (€)"
msgstr "prévu (€)"
-#: staffing/models.py:470 staffing/views.py:1054 staffing/views.py:1329
-#: staffing/views.py:1428 templates/leads/lead_detail.html:227
+#: staffing/models.py:470 staffing/views.py:1037 staffing/views.py:1312
+#: staffing/views.py:1411 templates/leads/lead_detail.html:200
#: templates/staffing/consultant_staffing.html:11
-#: templates/staffing/mission.html:163 templates/staffing/optimise_pdc.html:84
+#: templates/staffing/mission.html:164 templates/staffing/optimise_pdc.html:84
#: templates/staffing/optimise_pdc.html:109
#: templates/staffing/pdc_detail.html:8
msgid "Mission"
@@ -2499,14 +2492,14 @@ msgstr "Pas de ticket restaurant"
msgid "Lunch ticket"
msgstr "Ticket restaurant"
-#: staffing/models.py:543 staffing/views.py:1429
+#: staffing/models.py:543 staffing/views.py:1412
#: templates/crm/_clientcompany_rates_margin.html:17
#: templates/crm/clientcompany_ranking.html:18
#: templates/staffing/rates_report.html:23
msgid "Daily rate"
msgstr "Taux journalier"
-#: staffing/models.py:544 staffing/views.py:1429
+#: staffing/models.py:544 staffing/views.py:1412
msgid "Bought daily rate"
msgstr "Taux journalier d'achat"
@@ -2551,14 +2544,14 @@ msgstr "Toutes les missions"
#: staffing/tables.py:93 templates/billing/bill_review.html:220
#: templates/core/index.html:25 templates/core/index.html:50
#: templates/core/index.html:74 templates/core/index.html:98
-#: templates/crm/clientcompany_detail.html:322
-#: templates/leads/lead_detail.html:150 templates/leads/lead_detail.html:196
-#: templates/leads/lead_detail.html:241 templates/leads/lead_detail.html:243
+#: templates/crm/clientcompany_detail.html:369
+#: templates/leads/lead_detail.html:123 templates/leads/lead_detail.html:169
+#: templates/leads/lead_detail.html:214 templates/leads/lead_detail.html:216
#: templates/leads/lead_mail.html:8 templates/leads/lead_mail.html:12
#: templates/leads/lead_mail.txt:9 templates/leads/lead_mail.txt:13
#: templates/leads/mail.html:8 templates/leads/mail.txt:7
-#: templates/leads/mail.txt:8 templates/staffing/mission.html:124
-#: templates/staffing/mission.html:177 templates/staffing/mission.html:179
+#: templates/leads/mail.txt:8 templates/staffing/mission.html:125
+#: templates/staffing/mission.html:178 templates/staffing/mission.html:180
#: templates/staffing/mission_timesheet.html:90
msgid "To be defined"
msgstr "À définir"
@@ -2586,7 +2579,7 @@ msgstr ""
msgid "Consultants"
msgstr "Consultants"
-#: staffing/utils.py:285 staffing/views.py:1055 staffing/views.py:1534
+#: staffing/utils.py:285 staffing/views.py:1038 staffing/views.py:1517
#: templates/billing/pre_billing.html:92 templates/billing/pre_billing.html:100
#: templates/billing/pre_billing.html:159
#: templates/billing/pre_billing.html:167 templates/staffing/pdc_detail.html:19
@@ -2610,11 +2603,11 @@ msgstr "La charge doit être un multiple (%s)"
msgid "Charge cannot exceed forecast (%s)"
msgstr "La charge ne peut pas excéder la prévision (%s)"
-#: staffing/views.py:212 staffing/views.py:256
+#: staffing/views.py:196 staffing/views.py:240
msgid "Duplicate data error"
msgstr "Erreur de donn es en doublon"
-#: staffing/views.py:318
+#: staffing/views.py:303
#, python-format
msgid ""
"Staffing has been ignored for mission %(mission_name)s because "
@@ -2624,7 +2617,7 @@ msgstr ""
"date prévue %(staffing_date)s est antérieure à la fin de la mission "
"(%(start_date)s)"
-#: staffing/views.py:325
+#: staffing/views.py:310
#, python-format
msgid ""
"Staffing has been ignored for mission %(mission_name)s because "
@@ -2634,29 +2627,29 @@ msgstr ""
"date prévue %(staffing_date)s est postérieure à la fin de la mission "
"(%(end_date)s)"
-#: staffing/views.py:343
+#: staffing/views.py:328
msgid "Staffing has been updated"
msgstr "Les prévisions de charge ont été mises à jour"
-#: staffing/views.py:381
+#: staffing/views.py:366
#, python-format
msgid "Staffing has been shifted by %s month"
msgstr "Les prévisions de charge ont été décalées de %s mois"
-#: staffing/views.py:405
+#: staffing/views.py:390
msgid "Only won leads"
msgstr "Uniquement les affaires gagnées"
-#: staffing/views.py:405
+#: staffing/views.py:390
msgid "Only consider won leads for staffing forecasting"
msgstr ""
"Ne prend en compte que les missions gagnées pour les prévisions de charge"
-#: staffing/views.py:406
+#: staffing/views.py:391
msgid "Balanced staffing projection"
msgstr "Projections de staffing pondérées"
-#: staffing/views.py:406
+#: staffing/views.py:391
msgid ""
"Add missions forcecast staffing even if still not won with a ponderation "
"based on the mission won probability"
@@ -2664,11 +2657,11 @@ msgstr ""
"Ajoute les prévisions de charge des missions même si elles ne sont pas "
"gagnées en pondérant avec la probabilité de gagner."
-#: staffing/views.py:407
+#: staffing/views.py:392
msgid "Full staffing projection"
msgstr "Projections de staffing complètes"
-#: staffing/views.py:407
+#: staffing/views.py:392
msgid ""
"Add missions forcecast staffing even if still not won without any "
"ponderation. All forecast is considered."
@@ -2676,93 +2669,101 @@ msgstr ""
"Ajoute les prévisions de charge des missions même si elles ne sont pas "
"gagnées sans aucun pondération. Toutes les prévisions sont prises en compte."
-#: staffing/views.py:410
+#: staffing/views.py:395
msgid "Group by Manager"
msgstr "Groupement par manager"
-#: staffing/views.py:411
+#: staffing/views.py:396
msgid "Group by Level"
msgstr "Groupement par position"
-#: staffing/views.py:794 templates/staffing/prod_report.html:119
+#: staffing/views.py:779 templates/staffing/prod_report.html:119
msgid "Prod rate delta"
msgstr "Écart de taux de prod."
-#: staffing/views.py:795 templates/staffing/prod_report.html:120
+#: staffing/views.py:780 templates/staffing/prod_report.html:120
msgid "Daily rate delta"
msgstr "Écart de taux journalier"
-#: staffing/views.py:813
+#: staffing/views.py:798
msgid "Missing timesheet"
msgstr "Pointages manquants"
-#: staffing/views.py:1044 staffing/views.py:1392 staffing/views.py:1415
+#: staffing/views.py:859
+msgid "mission archived"
+msgstr "mission archivée"
+
+#: staffing/views.py:861
+msgid "mission not found"
+msgstr "mission inexistante"
+
+#: staffing/views.py:1027 staffing/views.py:1375 staffing/views.py:1398
msgid "timesheet.csv"
msgstr "feuille-de-temps.csv"
-#: staffing/views.py:1354 templates/billing/_lead_billing.html:66
-#: templates/billing/bill.html:179 templates/leads/lead_detail.html:256
+#: staffing/views.py:1337 templates/billing/_lead_billing.html:66
+#: templates/billing/bill.html:179 templates/leads/lead_detail.html:229
#: templates/staffing/mission_timesheet.html:38
#: templates/staffing/mission_timesheet.html:117
#: templates/staffing/prod_report.html:78
msgid "Total"
msgstr "Total"
-#: staffing/views.py:1428
+#: staffing/views.py:1411
msgid "Lead Price (k€)"
msgstr "Montant affaire (k€)"
-#: staffing/views.py:1428
+#: staffing/views.py:1411
msgid "Mission Price (k€)"
msgstr "Montant mission (k€)"
-#: staffing/views.py:1429
+#: staffing/views.py:1412
msgid "Past done days"
msgstr "Jours réalisés dans le passé"
-#: staffing/views.py:1429 templates/billing/bill_review.html:211
-#: templates/leads/lead_detail.html:229 templates/staffing/mission.html:165
+#: staffing/views.py:1412 templates/billing/bill_review.html:211
+#: templates/leads/lead_detail.html:202 templates/staffing/mission.html:166
#: templates/staffing/mission_timesheet.html:12
msgid "Done days"
msgstr "Jours réalisés"
-#: staffing/views.py:1429 templates/staffing/mission_timesheet.html:14
+#: staffing/views.py:1412 templates/staffing/mission_timesheet.html:14
msgid "Days to be done"
msgstr "Jours à faire"
-#: staffing/views.py:1480
+#: staffing/views.py:1463
msgid "holidays_timesheet.csv"
msgstr "feuille-de-temps-congés.csv"
-#: staffing/views.py:1534
+#: staffing/views.py:1517
msgid "trigramme"
msgstr "trigramme"
-#: staffing/views.py:1534 staffing/views.py:1733
+#: staffing/views.py:1517 staffing/views.py:1715
msgid "profil"
msgstr "profil"
-#: staffing/views.py:1534
+#: staffing/views.py:1517
msgid "start"
msgstr "début"
-#: staffing/views.py:1534
+#: staffing/views.py:1517
msgid "end"
msgstr "fin"
-#: staffing/views.py:1625
+#: staffing/views.py:1607
msgid "current"
msgstr "actuel"
-#: staffing/views.py:1625
+#: staffing/views.py:1607
msgid "next"
msgstr "prochain"
-#: staffing/views.py:1632 templates/staffing/rate_objective_report.html:41
+#: staffing/views.py:1614 templates/staffing/rate_objective_report.html:41
msgid "horizon"
msgstr "horizon"
-#: staffing/views.py:1734 staffing/views.py:2077
+#: staffing/views.py:1716 staffing/views.py:2069
#: templates/billing/pre_billing.html:92 templates/billing/pre_billing.html:159
#: templates/staffing/missions_report.html:67
#: templates/staffing/missions_report.html:73
@@ -2770,37 +2771,33 @@ msgstr "horizon"
msgid "days"
msgstr "jours"
-#: staffing/views.py:1761
+#: staffing/views.py:1742
msgid "This lead has no mission defined"
msgstr "Cette affaire n'a aucune mission définie"
-#: staffing/views.py:1788
-msgid "You are not allowed to do that"
-msgstr "Vous n'êtes pas autorisé à faire cela"
+#: staffing/views.py:1776
+msgid "Mission or consultant does not exist"
+msgstr "La mission ou le consultant n'existe pas"
-#: staffing/views.py:1797
+#: staffing/views.py:1785
#, python-brace-format
msgid "daily rate for {consultant}"
msgstr "Taux journalier pour {consultant}"
-#: staffing/views.py:1800
+#: staffing/views.py:1790
#, python-brace-format
msgid "bought daily rate for {consultant}"
msgstr "Taux journalier d'achat pour {consultant}"
-#: staffing/views.py:1809
-msgid "Mission or consultant does not exist"
-msgstr "La mission ou le consultant n'existe pas"
-
-#: staffing/views.py:1943
+#: staffing/views.py:1935
msgid "Predefined assignment must be in consultant list"
msgstr "Les affectations prédéfinis doivent être dans la liste des consultants"
-#: staffing/views.py:1947
+#: staffing/views.py:1939
msgid "Excluded consultant must be in consultant list"
msgstr "Les affectations exclus être dans la liste des consultants"
-#: staffing/views.py:1968
+#: staffing/views.py:1960
msgid ""
"There's no solution. Add consultants, remove mission, exclusions or relax "
"experience ratio constraint"
@@ -2808,18 +2805,18 @@ msgstr ""
"Aucune solution. Ajoutez des consultants, retirez des missions, des "
"exclusions ou relâchez les contraintes de ratio d'expérience."
-#: staffing/views.py:2059 templates/staffing/mission.html:104
+#: staffing/views.py:2051 templates/staffing/mission.html:105
#: templates/staffing/turnover_pivotable.html:101
#: templates/staffing/turnover_pivotable.html:118
#: templates/staffing/turnover_pivotable.html:125
msgid "Marketing product"
msgstr "Produit marketing"
-#: staffing/views.py:2059 staffing/views.py:2060
+#: staffing/views.py:2051 staffing/views.py:2052
msgid "Undefined"
msgstr "À définir"
-#: staffing/views.py:2076 staffing/views.py:2087
+#: staffing/views.py:2068 staffing/views.py:2079
#: templates/billing/graph_yearly_billing.html:37
#: templates/staffing/turnover_pivotable.html:91
#: templates/staffing/turnover_pivotable.html:97
@@ -2830,23 +2827,23 @@ msgstr "À définir"
msgid "turnover (€)"
msgstr "Chiffre d'affaire (€)"
-#: staffing/views.py:2078
+#: staffing/views.py:2070
msgid "external subcontractor turnover (€)"
msgstr "CA sous-traitants externes (€)"
-#: staffing/views.py:2079
+#: staffing/views.py:2071
msgid "external subcontractor days"
msgstr "jours sous-traitants externes"
-#: staffing/views.py:2080
+#: staffing/views.py:2072
msgid "internal subcontractor turnover (€)"
msgstr "CA sous-traitants internes (€)"
-#: staffing/views.py:2081
+#: staffing/views.py:2073
msgid "internal subcontractor days"
msgstr "jours sous-traitants internes"
-#: staffing/views.py:2082 staffing/views.py:2097
+#: staffing/views.py:2074 staffing/views.py:2089
#: templates/staffing/turnover_pivotable.html:61
#: templates/staffing/turnover_pivotable.html:67
#: templates/staffing/turnover_pivotable.html:75
@@ -2855,39 +2852,39 @@ msgstr "jours sous-traitants internes"
msgid "own turnover (€)"
msgstr "Chiffre d'affaire propre (€)"
-#: staffing/views.py:2083
+#: staffing/views.py:2075
msgid "own days"
msgstr "Jours effectif propre"
-#: staffing/views.py:2085 staffing/views.py:2093
+#: staffing/views.py:2077 staffing/views.py:2085
msgid "fiscal year"
msgstr "Année fiscale"
-#: staffing/views.py:2104 staffing/views.py:2106
+#: staffing/views.py:2096 staffing/views.py:2098
#: templates/staffing/turnover_pivotable.html:87
msgid "top client company"
msgstr "top entreprise"
-#: staffing/views.py:2106
+#: staffing/views.py:2098
msgid "others"
msgstr "autres"
-#: staffing/views.py:2145 staffing/views.py:2147
+#: staffing/views.py:2137 staffing/views.py:2139
#: templates/staffing/lunch_tickets_pivotable.html:59
msgid "days off previous month"
msgstr "jours de congés du mois précédent"
-#: staffing/views.py:2146 staffing/views.py:2147
+#: staffing/views.py:2138 staffing/views.py:2139
#: templates/staffing/lunch_tickets_pivotable.html:65
msgid "days without tickets previous month"
msgstr "jours sans ticket restaurant le mois précédent"
-#: staffing/views.py:2147 templates/staffing/lunch_tickets_pivotable.html:47
+#: staffing/views.py:2139 templates/staffing/lunch_tickets_pivotable.html:47
#: templates/staffing/lunch_tickets_pivotable.html:53
msgid "deserved tickets"
msgstr "tickets mérités"
-#: staffing/views.py:2301
+#: staffing/views.py:2293
msgid "Global"
msgstr "Global"
@@ -3519,35 +3516,35 @@ msgstr ""
"un mail à un administrateur pour mettre à jour vos droits :
\n"
#: templates/core/_datatables-dj-tables.html:26
-#: templates/core/_datatables.html:22
+#: templates/core/_datatables.html:23
msgid "Nothing found"
msgstr "Aucun résultat"
#: templates/core/_datatables-dj-tables.html:27
-#: templates/core/_datatables.html:23
+#: templates/core/_datatables.html:24
msgid "Showing _START_ to _END_ of _TOTAL_ records"
msgstr "Affichage de _START_ à _END_ sur _TOTAL_ enregistrements"
#: templates/core/_datatables-dj-tables.html:28
-#: templates/core/_datatables.html:24
+#: templates/core/_datatables.html:25
msgid "Showing 0 to 0 of 0 records"
msgstr "Affichage de 0 à 0 sur 0 enregistrement"
#: templates/core/_datatables-dj-tables.html:29
-#: templates/core/_datatables.html:25
+#: templates/core/_datatables.html:26
msgid "(filtered from _MAX_ total records)"
msgstr "(filtrés parmi _MAX_ enregistrements au total)"
#: templates/core/_datatables-dj-tables.html:30
-#: templates/core/_datatables.html:26
+#: templates/core/_datatables.html:27
msgid "Search:"
msgstr "Rechercher :"
-#: templates/core/_datatables.html:21
+#: templates/core/_datatables.html:22
msgid "Processing..."
msgstr "En cours de traitement..."
-#: templates/core/_datatables.html:27
+#: templates/core/_datatables.html:28
msgid "Show _MENU_ entries"
msgstr "Afficher _MENU_ éléments"
@@ -3692,7 +3689,7 @@ msgstr "Mon plan de charge"
#: templates/core/_pydici_menu.html:80 templates/core/_pydici_menu.html:149
#: templates/core/_pydici_menu.html:179
#: templates/crm/clientcompany_detail.html:38
-#: templates/leads/lead_detail.html:55 templates/staffing/mission.html:45
+#: templates/leads/lead_detail.html:56 templates/staffing/mission.html:45
msgid "Reporting"
msgstr "Rapports"
@@ -3782,7 +3779,7 @@ msgid "New client"
msgstr "Nouveau client"
#: templates/core/_pydici_menu.html:123
-#: templates/staffing/mission_contacts.html:42
+#: templates/staffing/mission_contacts.html:49
msgid "New mission contact"
msgstr "Nouveau contact mission"
@@ -3791,7 +3788,7 @@ msgid "New business broker"
msgstr "Nouvel apporteur d'affaires"
#: templates/core/_pydici_menu.html:125
-#: templates/crm/clientcompany_detail.html:399
+#: templates/crm/clientcompany_detail.html:446
msgid "New administrative contact"
msgstr "Nouveau contact administratif"
@@ -3826,14 +3823,14 @@ msgstr "Revue des notes de frais refacturables"
#: templates/core/_pydici_menu.html:151
#: templates/expense/expense_archive.html:6
-#: templates/expense/expense_payments.html:68
+#: templates/expense/expense_payments.html:64
#: templates/expense/expenses.html:52
msgid "Expenses history"
msgstr "Historique des notes de frais"
#: templates/core/_pydici_menu.html:159
#: templates/crm/clientcompany_detail.html:37
-#: templates/leads/lead_detail.html:53 templates/staffing/mission.html:47
+#: templates/leads/lead_detail.html:54 templates/staffing/mission.html:47
msgid "Billing"
msgstr "Facturation"
@@ -4140,10 +4137,8 @@ msgstr "Ajouter ou modifier un client"
#: templates/crm/clientcompany_detail.html:16
#: templates/crm/clientcompany_detail.html:251
#: templates/crm/clientcompany_detail.html:253
-#: templates/expense/_make_vat_editable.html:8
-#: templates/leads/lead_detail.html:44 templates/leads/lead_detail.html:46
-#: templates/staffing/mission.html:217
-#: templates/staffing/mission_consultants.html:37
+#: templates/leads/lead_detail.html:45 templates/leads/lead_detail.html:47
+#: templates/staffing/mission.html:218
msgid "click to edit..."
msgstr "Cliquez pour modifier..."
@@ -4247,7 +4242,9 @@ msgid "nature"
msgstr "nature"
#: templates/crm/clientcompany_detail.html:271
-#: templates/crm/clientcompany_detail.html:300 templates/leads/leads.html:20
+#: templates/crm/clientcompany_detail.html:300
+#: templates/crm/clientcompany_detail.html:324
+#: templates/crm/clientcompany_detail.html:348 templates/leads/leads.html:20
#: templates/leads/leads_to_bill.html:20
#: templates/staffing/_mission_table.html:21
msgid "amount (k€)"
@@ -4268,37 +4265,47 @@ msgstr "Prévisions mise à jour récemment"
msgid "Archiving"
msgstr "Archivage"
-#: templates/crm/clientcompany_detail.html:301 templates/leads/leads.html:21
+#: templates/crm/clientcompany_detail.html:301
+#: templates/crm/clientcompany_detail.html:325
+#: templates/crm/clientcompany_detail.html:349 templates/leads/leads.html:21
msgid "status"
msgstr "état"
-#: templates/crm/clientcompany_detail.html:319
+#: templates/crm/clientcompany_detail.html:315
+msgid "Supplier Leads"
+msgstr "Affaire en tant que fournisseur"
+
+#: templates/crm/clientcompany_detail.html:339
+msgid "Business broker Leads"
+msgstr "Affaires apportées ou sous-traitées"
+
+#: templates/crm/clientcompany_detail.html:366
msgid "Business owner for this company:"
msgstr "Responsable business pour cette société : "
-#: templates/crm/clientcompany_detail.html:325
+#: templates/crm/clientcompany_detail.html:372
msgid "Consultants that work for this company:"
msgstr "Les consultants qui ont travaillé pour cette société :"
-#: templates/crm/clientcompany_detail.html:332
+#: templates/crm/clientcompany_detail.html:379
#: templates/people/consultant.html:19
msgid "subcontractor"
msgstr "Sous-traitant"
-#: templates/crm/clientcompany_detail.html:341
+#: templates/crm/clientcompany_detail.html:388
msgid "Business contacts"
msgstr "Contacts business"
-#: templates/crm/clientcompany_detail.html:349
+#: templates/crm/clientcompany_detail.html:396
msgid "Mission contacts"
msgstr "Contacts mission"
-#: templates/crm/clientcompany_detail.html:357
+#: templates/crm/clientcompany_detail.html:404
msgid "Administrative contacts"
msgstr "Contacts administratifs"
-#: templates/crm/clientcompany_detail.html:365
-#: templates/staffing/mission_contacts.html:15
+#: templates/crm/clientcompany_detail.html:412
+#: templates/staffing/mission_contacts.html:19
msgid "Email"
msgstr "Courriel"
@@ -4390,35 +4397,63 @@ msgstr "Fournisseurs"
msgid "Yes, but not charged yet"
msgstr "Oui, mais non refacturé"
+#: templates/expense/_expense_transitions_column.html:19
+msgid "Edit"
+msgstr "Modifier"
+
+#: templates/expense/_expense_transitions_column.html:20
+msgid "Ed"
+msgstr "Mo"
+
+#: templates/expense/_expense_transitions_column.html:22
+msgid "Delete"
+msgstr "Supprimer"
+
+#: templates/expense/_expense_transitions_column.html:23
+msgid "De"
+msgstr "Su"
+
+#: templates/expense/_expense_transitions_column.html:27
+msgid "Clone"
+msgstr "Cloner"
+
+#: templates/expense/_expense_transitions_column.html:28
+msgid "Cl"
+msgstr "Cl"
+
#: templates/expense/chargeable_expenses.html:9
msgid "Non billed chargeable expenses"
msgstr "Notes de frais refactuables non refacturées"
-#: templates/expense/expense.html:19
+#: templates/expense/expense.html:14
#, python-format
msgid "Expense n° %(id)s"
msgstr "Frais n° %(id)s"
-#: templates/expense/expense.html:55 templates/expense/expense_archive.html:34
-#: templates/expense/expense_list.html:23 templates/leads/lead_detail.html:174
+#: templates/expense/expense.html:50 templates/expense/expense_archive.html:29
+#: templates/expense/expense_list.html:23 templates/leads/lead_detail.html:147
#: templates/leads/review.html:56 templates/leads/review.html:88
msgid "Update date"
msgstr "Date de mise à jour"
-#: templates/expense/expense.html:59 templates/expense/expense_archive.html:30
+#: templates/expense/expense.html:54 templates/expense/expense_archive.html:25
msgid "Corporate card"
msgstr "Carte société"
-#: templates/expense/expense.html:84
+#: templates/expense/expense.html:79
msgid "Payment"
msgstr "Paiement"
-#: templates/expense/expense_archive.html:17
+#: templates/expense/expense.html:87
+msgid "Transitions"
+msgstr "Transitions"
+
+#: templates/expense/expense_archive.html:12
msgid "Expenses archive"
msgstr "Historique des notes de frais"
-#: templates/expense/expense_archive.html:21
-#: templates/expense/expense_payments.html:47
+#: templates/expense/expense_archive.html:16
+#: templates/expense/expense_payments.html:43
msgid "#"
msgstr "#"
@@ -4430,35 +4465,35 @@ msgstr "Personne"
msgid "No expense"
msgstr "Aucune note de frais"
-#: templates/expense/expense_payment_detail.html:16
+#: templates/expense/expense_payment_detail.html:12
msgid "Expense payment detail"
msgstr "Détail du remboursement de frais"
-#: templates/expense/expense_payment_detail.html:21
+#: templates/expense/expense_payment_detail.html:17
msgid "Expenses of payment n°"
msgstr "Notes de frais du remboursement n°"
-#: templates/expense/expense_payment_detail.html:26
+#: templates/expense/expense_payment_detail.html:22
msgid "Total: "
msgstr "Total : "
-#: templates/expense/expense_payments.html:19
+#: templates/expense/expense_payments.html:15
msgid "Expenses to pay"
msgstr "Notes de frais à rembourser"
-#: templates/expense/expense_payments.html:24
+#: templates/expense/expense_payments.html:20
msgid "No expense to pay"
msgstr "Aucune note de frais à rembourser"
-#: templates/expense/expense_payments.html:30
+#: templates/expense/expense_payments.html:26
msgid "Modify the expense payment"
msgstr "Modifier le remboursement de frais"
-#: templates/expense/expense_payments.html:32
+#: templates/expense/expense_payments.html:28
msgid "Add an expense payment"
msgstr "Ajouter un remboursement de frais"
-#: templates/expense/expense_payments.html:51
+#: templates/expense/expense_payments.html:47
msgid "Modification"
msgstr "Modification"
@@ -4490,6 +4525,14 @@ msgstr "Les livrables sont manquants. Merci de les ranger ici : "
msgid "All lead tags"
msgstr "Toutes les étiquettes des affaires"
+#: templates/leads/_tags_banner.html:25
+msgid "Add"
+msgstr "Ajouter"
+
+#: templates/leads/_tags_banner.html:33
+msgid "Suggested tags: "
+msgstr "Étiquette(s) suggérée(s) : "
+
#: templates/leads/graph_leads_activity.html:13
msgid "New leads per week"
msgstr "Nouvelles affaires par semaine"
@@ -4560,139 +4603,131 @@ msgstr "taux de transformation (%%)"
msgid "Add or modify a Lead"
msgstr "Ajouter ou modifier une affaire"
-#: templates/leads/lead_detail.html:23
+#: templates/leads/lead_detail.html:24
msgid "Lead detail"
msgstr "Détail de l'affaire"
-#: templates/leads/lead_detail.html:52 templates/people/consultant.html:41
+#: templates/leads/lead_detail.html:53 templates/people/consultant.html:41
#: templates/staffing/missions.html:6
msgid "Missions"
msgstr "Missions"
-#: templates/leads/lead_detail.html:54 templates/staffing/mission.html:44
+#: templates/leads/lead_detail.html:55 templates/staffing/mission.html:44
msgid "Documents"
msgstr "Documents"
-#: templates/leads/lead_detail.html:56 templates/staffing/mission.html:48
+#: templates/leads/lead_detail.html:57 templates/staffing/mission.html:48
msgid "History"
msgstr "Historique"
-#: templates/leads/lead_detail.html:84
-msgid "Suggested tags: "
-msgstr "Étiquette(s) suggérée(s) : "
-
-#: templates/leads/lead_detail.html:118
+#: templates/leads/lead_detail.html:91
msgctxt "short"
msgid "Estimation"
msgstr "Estimation"
-#: templates/leads/lead_detail.html:132
+#: templates/leads/lead_detail.html:105
msgid "Informations"
msgstr "Informations"
-#: templates/leads/lead_detail.html:136
+#: templates/leads/lead_detail.html:109
msgctxt "client"
msgid "Unknown"
msgstr "Inconnu"
-#: templates/leads/lead_detail.html:159
+#: templates/leads/lead_detail.html:132
msgid "Broker"
msgstr "Apporteur"
-#: templates/leads/lead_detail.html:168 templates/leads/lead_detail.html:171
+#: templates/leads/lead_detail.html:141 templates/leads/lead_detail.html:144
msgid "Unknown date"
msgstr "Date inconnue"
-#: templates/leads/lead_detail.html:177
+#: templates/leads/lead_detail.html:150
msgid "Unknown amount"
msgstr "Montant inconnu"
-#: templates/leads/lead_detail.html:180
+#: templates/leads/lead_detail.html:153
msgid "Estimation"
msgstr "Estimation"
-#: templates/leads/lead_detail.html:183
+#: templates/leads/lead_detail.html:156
msgid "Potential resource(s)"
msgstr "Ressources potentielles"
-#: templates/leads/lead_detail.html:186 templates/leads/lead_mail.html:18
+#: templates/leads/lead_detail.html:159 templates/leads/lead_mail.html:18
#: templates/leads/mail.txt:10
msgid "Nothing"
msgstr "Rien"
-#: templates/leads/lead_detail.html:192
+#: templates/leads/lead_detail.html:165
#: templates/staffing/mission_timesheet.html:86
msgid "Profitability"
msgstr "Rentabilité"
-#: templates/leads/lead_detail.html:194 templates/staffing/mission.html:121
+#: templates/leads/lead_detail.html:167 templates/staffing/mission.html:122
#: templates/staffing/mission_timesheet.html:88
msgid "Sold"
msgstr "Vendu"
-#: templates/leads/lead_detail.html:198 templates/leads/lead_detail.html:248
+#: templates/leads/lead_detail.html:171 templates/leads/lead_detail.html:221
msgid "Unattributed"
msgstr "Non attribué"
-#: templates/leads/lead_detail.html:198
+#: templates/leads/lead_detail.html:171
msgid "Non attributed amount to missions"
msgstr "Montant non attribué à des missions"
-#: templates/leads/lead_detail.html:199
+#: templates/leads/lead_detail.html:172
#: templates/staffing/mission_timesheet.html:108
msgid "Margin over rate objective"
msgstr "Marge sur objectifs de TJM"
-#: templates/leads/lead_detail.html:200
+#: templates/leads/lead_detail.html:173
msgid "Full margin"
msgstr "Marge total"
-#: templates/leads/lead_detail.html:200
+#: templates/leads/lead_detail.html:173
msgid "Sum of objective margin and unused days for fixed price missions"
msgstr ""
"Somme des marges sur objectifs et des montants non utilisés pour les forfaits"
-#: templates/leads/lead_detail.html:204
+#: templates/leads/lead_detail.html:177
msgid "Similar leads"
msgstr "Affaires similaires"
-#: templates/leads/lead_detail.html:222
+#: templates/leads/lead_detail.html:195
msgid "Missions of this lead:"
msgstr "Missions de cette affaire : "
-#: templates/leads/lead_detail.html:228 templates/staffing/mission.html:164
+#: templates/leads/lead_detail.html:201 templates/staffing/mission.html:165
msgid "id"
msgstr "n°"
-#: templates/leads/lead_detail.html:230 templates/staffing/mission.html:166
+#: templates/leads/lead_detail.html:203 templates/staffing/mission.html:167
msgid "Done work (k€)"
msgstr "Montant réalisé (k€)"
-#: templates/leads/lead_detail.html:231
+#: templates/leads/lead_detail.html:204
#: templates/staffing/fixed_price_report.html:19
-#: templates/staffing/mission.html:167
+#: templates/staffing/mission.html:168
msgid "Sold (k€)"
msgstr "Vendu (k€)"
-#: templates/leads/lead_detail.html:233
+#: templates/leads/lead_detail.html:206
#: templates/staffing/_mission_table.html:23
-#: templates/staffing/mission.html:169
+#: templates/staffing/mission.html:170
msgid "product"
msgstr "produit"
-#: templates/leads/lead_detail.html:268
+#: templates/leads/lead_detail.html:241
msgid "Create a new mission for this lead"
msgstr "Créer une nouvelle mission pour cette affaire"
-#: templates/leads/lead_detail.html:272
+#: templates/leads/lead_detail.html:245
msgid "A mission is created once a commercial proposal has been sent."
msgstr ""
"Une mission est créée une fois que la proposition commerciale est envoyée."
-#: templates/leads/lead_detail.html:314
-msgid "Tag already exists"
-msgstr "Étiquette existante"
-
#: templates/leads/lead_mail.html:4 templates/leads/lead_mail.txt:5
msgid "Contact: "
msgstr "Contact : "
@@ -5049,11 +5084,6 @@ msgstr "Afficher uniquement les missions actives"
msgid "Display all missions"
msgstr "Afficher toutes les missions"
-#: templates/staffing/_mission_table.html:43
-#: templates/staffing/mission.html:232
-msgid "Archiving failed"
-msgstr "Échec de l'archivage"
-
#: templates/staffing/_mission_table_archive_column.html:11
msgid "Archive"
msgstr "Archiver"
@@ -5301,43 +5331,43 @@ msgstr "Cette mission n'a plus de staffing défini dans le futur"
msgid "Still to be billed"
msgstr "Reste à facturer"
-#: templates/staffing/mission.html:63
+#: templates/staffing/mission.html:64
msgid "Archive this mission"
msgstr "Archiver cette mission"
-#: templates/staffing/mission.html:70
+#: templates/staffing/mission.html:71
msgid "Staffing has not been updated recently"
msgstr "Les prévisions n'ont pas été mises à jour récemment"
-#: templates/staffing/mission.html:77
+#: templates/staffing/mission.html:78
msgid "Marketing product is not yet defined"
msgstr "Le produit marketing de cette mission n'est pas défini"
-#: templates/staffing/mission.html:81
+#: templates/staffing/mission.html:82
msgid "This mission is archived"
msgstr "Cette mission est archivée"
-#: templates/staffing/mission.html:108
+#: templates/staffing/mission.html:109
msgid "Analytic code"
msgstr "Code analytique"
-#: templates/staffing/mission.html:113 templates/staffing/mission.html:117
+#: templates/staffing/mission.html:114 templates/staffing/mission.html:118
msgid "Client lead id"
msgstr "Référence affaire client"
-#: templates/staffing/mission.html:130
+#: templates/staffing/mission.html:131
msgid "Probability"
msgstr "Probabilité"
-#: templates/staffing/mission.html:133
+#: templates/staffing/mission.html:134
msgid "Lead of this mission"
msgstr "Affaire de cette mission"
-#: templates/staffing/mission.html:140
+#: templates/staffing/mission.html:141
msgid "Lead's description"
msgstr "Description de l'affaire"
-#: templates/staffing/mission.html:159
+#: templates/staffing/mission.html:160
msgid "Other missions linked to this lead"
msgstr "Autres missions liées à cette affaire"
@@ -5345,24 +5375,24 @@ msgstr "Autres missions liées à cette affaire"
msgid "Consultants currently implicated in this mission"
msgstr "Consultants impliqués actuellement dans cette mission"
-#: templates/staffing/mission_consultants.html:10
+#: templates/staffing/mission_consultants.html:11
msgid "Daily rate (€)"
msgstr "Taux journalier (€)"
-#: templates/staffing/mission_consultants.html:11
+#: templates/staffing/mission_consultants.html:12
msgid "Buy rate (€)"
msgstr "Taux d'achat (€)"
-#: templates/staffing/mission_consultants.html:13
+#: templates/staffing/mission_consultants.html:14
#, python-format
msgid "Objective %(d)s (€)"
msgstr "Taux budget %(d)s (€)"
-#: templates/staffing/mission_contacts.html:32
+#: templates/staffing/mission_contacts.html:36
msgid "Add or edit"
msgstr "Ajouter ou modifier"
-#: templates/staffing/mission_contacts.html:45
+#: templates/staffing/mission_contacts.html:52
msgid "Close"
msgstr "Fermer"
@@ -5711,6 +5741,21 @@ msgstr "Proportion de sous traitance par produit"
msgid "Sum ratio"
msgstr "Ratio de sommes"
+#~ msgid "Successfully update expense"
+#~ msgstr "Note de frais mise à jour avec succès"
+
+#~ msgid "Incorrect value"
+#~ msgstr "Valeur incorrecte"
+
+#~ msgid "You are not allowed to do that"
+#~ msgstr "Vous n'êtes pas autorisé à faire cela"
+
+#~ msgid "Tag already exists"
+#~ msgstr "Étiquette existante"
+
+#~ msgid "Archiving failed"
+#~ msgstr "Échec de l'archivage"
+
#~ msgid "expert"
#~ msgstr "expert"
@@ -5751,9 +5796,6 @@ msgstr "Ratio de sommes"
#~ msgid "Last modified or created leads"
#~ msgstr "Dernières affaires modifiées ou créées"
-#~ msgid "New leads"
-#~ msgstr "Nouvelles affaires"
-
#~ msgid "Last new lead created"
#~ msgstr "Dernières affaires créées"
@@ -6579,9 +6621,6 @@ msgstr "Ratio de sommes"
#~ msgid "Go"
#~ msgstr "Envoyer"
-#~ msgid "Add..."
-#~ msgstr "Ajouter..."
-
#~ msgid "My last actions"
#~ msgstr "Mes dernières actions"
diff --git a/media/js/htmx-2.0.3.min.js b/media/js/htmx-2.0.3.min.js
new file mode 100644
index 00000000..423cf011
--- /dev/null
+++ b/media/js/htmx-2.0.3.min.js
@@ -0,0 +1 @@
+var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=cn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true},parseInterval:null,_:null,version:"2.0.3"};Q.onLoad=j;Q.process=kt;Q.on=ye;Q.off=be;Q.trigger=de;Q.ajax=Rn;Q.find=r;Q.findAll=x;Q.closest=g;Q.remove=z;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=$e;Q.defineExtension=Fn;Q.removeExtension=Bn;Q.logAll=V;Q.logNone=_;Q.parseInterval=h;Q._=e;const n={addTriggerHandler:St,bodyContains:le,canAccessLocalStorage:B,findThisElement:Se,filterValues:dn,swap:$e,hasAttribute:s,getAttributeValue:te,getClosestAttributeValue:re,getClosestMatch:i,getExpressionVars:En,getHeaders:fn,getInputValues:cn,getInternalData:ie,getSwapSpecification:gn,getTriggerSpecs:st,getTarget:Ee,makeFragment:P,mergeObjects:ce,makeSettleInfo:xn,oobSwap:He,querySelectorExt:ae,settleImmediately:Kt,shouldCancel:dt,triggerEvent:de,triggerErrorEvent:fe,withExtensions:Ft};const o=["get","post","put","delete","patch"];const R=o.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function h(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function c(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function ne(){return document}function m(e,t){return e.getRootNode?e.getRootNode({composed:t}):ne()}function i(e,t){while(e&&!t(e)){e=c(e)}return e||null}function H(e,t,n){const r=te(t,n);const o=te(t,"hx-disinherit");var i=te(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function re(t,n){let r=null;i(t,function(e){return!!(r=H(t,ue(e),n))});if(r!=="unset"){return r}}function d(e,t){const n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return!!n&&n.call(e,t)}function T(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function q(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function L(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function N(e){const t=ne().createElement("script");se(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function A(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(A(e)){const t=N(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){C(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/]*)?>[\s\S]*?<\/head>/i,"");const n=T(t);let r;if(n==="html"){r=new DocumentFragment;const i=q(e);L(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=q(t);L(r,i.body);r.title=i.title}else{const i=q(''+t+"");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function oe(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return typeof e==="function"}function D(e){return t(e,"Object")}function ie(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function M(t){const n=[];if(t){for(let e=0;e=0}function le(e){const t=e.getRootNode&&e.getRootNode();if(t&&t instanceof window.ShadowRoot){return ne().body.contains(t.host)}else{return ne().body.contains(e)}}function F(e){return e.trim().split(/\s+/)}function ce(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function S(e){try{return JSON.parse(e)}catch(e){C(e);return null}}function B(){const e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function U(t){try{const e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return vn(ne().body,function(){return eval(e)})}function j(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function _(){Q.logger=null}function r(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return r(ne(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(ne(),e)}}function E(){return window}function z(e,t){e=y(e);if(t){E().setTimeout(function(){z(e);e=null},t)}else{c(e).removeChild(e)}}function ue(e){return e instanceof Element?e:null}function $(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function f(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ue(y(e));if(!e){return}if(n){E().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ue(y(e));if(!r){return}if(n){E().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=y(e);e.classList.toggle(t)}function Z(e,t){e=y(e);se(e.parentElement.children,function(e){G(e,t)});K(ue(e),t)}function g(e,t){e=ue(y(e));if(e&&e.closest){return e.closest(t)}else{do{if(e==null||d(e,t)){return e}}while(e=e&&ue(c(e)));return null}}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function ge(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function p(e,t,n){e=y(e);if(t.indexOf("closest ")===0){return[g(ue(e),ge(t.substr(8)))]}else if(t.indexOf("find ")===0){return[r(f(e),ge(t.substr(5)))]}else if(t==="next"){return[ue(e).nextElementSibling]}else if(t.indexOf("next ")===0){return[pe(e,ge(t.substr(5)),!!n)]}else if(t==="previous"){return[ue(e).previousElementSibling]}else if(t.indexOf("previous ")===0){return[me(e,ge(t.substr(9)),!!n)]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else if(t==="root"){return[m(e,!!n)]}else if(t==="host"){return[e.getRootNode().host]}else if(t.indexOf("global ")===0){return p(e,t.slice(7),true)}else{return M(f(m(e,!!n)).querySelectorAll(ge(t)))}}var pe=function(t,e,n){const r=f(m(t,n)).querySelectorAll(e);for(let e=0;e=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ae(e,t){if(typeof e!=="string"){return p(e,t)[0]}else{return p(ne().body,e)[0]}}function y(e,t){if(typeof e==="string"){return r(f(t)||document,e)}else{return e}}function xe(e,t,n,r){if(k(t)){return{target:ne().body,event:J(e),listener:t,options:n}}else{return{target:y(e),event:J(t),listener:n,options:r}}}function ye(t,n,r,o){Vn(function(){const e=xe(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=k(n);return e?n:r}function be(t,n,r){Vn(function(){const e=xe(t,n,r);e.target.removeEventListener(e.event,e.listener)});return k(n)?n:r}const ve=ne().createElement("output");function we(e,t){const n=re(e,t);if(n){if(n==="this"){return[Se(e,t)]}else{const r=p(e,n);if(r.length===0){C('The selector "'+n+'" on '+t+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ue(i(e,function(e){return te(ue(e),t)!=null}))}function Ee(e){const t=re(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ae(e,t)}}else{const n=ie(e);if(n.boosted){return ne().body}else{return e}}}function Ce(t){const n=Q.config.attributesToSettle;for(let e=0;e0){s=e.substr(0,e.indexOf(":"));n=e.substr(e.indexOf(":")+1,e.length)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=p(t,n,false);if(r){se(r,function(e){let t;const n=o.cloneNode(true);t=ne().createDocumentFragment();t.appendChild(n);if(!Re(s,e)){t=f(n)}const r={shouldSwap:true,target:e,fragment:t};if(!de(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);_e(s,e,e,t,i);Te()}se(i.elts,function(e){de(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(ne().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Te(){const e=r("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=r("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){se(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=te(e,"id");const n=ne().getElementById(t);if(n!=null){if(e.moveBefore){let e=r("#--htmx-preserve-pantry--");if(e==null){ne().body.insertAdjacentHTML("afterend","");e=r("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Le(l,e,c){se(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=f(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Ne(e){return function(){G(e,Q.config.addedClass);kt(ue(e));Ae(f(e));de(e,"htmx:load")}}function Ae(e){const t="[autofocus]";const n=$(d(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function u(e,t,n,r){Le(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ue(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Ne(o))}}}function Ie(e,t){let n=0;while(n0}function $e(e,t,r,o){if(!o){o={}}e=y(e);const i=o.contextElement?m(o.contextElement,false):ne();const n=document.activeElement;let s={};try{s={elt:n,start:n?n.selectionStart:null,end:n?n.selectionEnd:null}}catch(e){}const l=xn(e);if(r.swapStyle==="textContent"){e.textContent=t}else{let n=P(t);l.title=n.title;if(o.selectOOB){const u=o.selectOOB.split(",");for(let t=0;t0){E().setTimeout(c,r.settleDelay)}else{c()}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=S(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(D(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}de(n,i,e)}}}else{const s=r.split(",");for(let e=0;e0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=vn(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(ne().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function w(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=w(e,Qe).trim();e.shift()}else{t=w(e,b)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{w(o,v);const l=o.length;const c=w(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};w(o,v);u.pollInterval=h(w(o,/[,\[\s]/));w(o,v);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const a={trigger:c};var i=nt(e,o,"event");if(i){a.eventFilter=i}w(o,v);while(o.length>0&&o[0]!==","){const f=o.shift();if(f==="changed"){a.changed=true}else if(f==="once"){a.once=true}else if(f==="consume"){a.consume=true}else if(f==="delay"&&o[0]===":"){o.shift();a.delay=h(w(o,b))}else if(f==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=w(o,b);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const d=rt(o);if(d.length>0){s+=" "+d}}}a.from=s}else if(f==="target"&&o[0]===":"){o.shift();a.target=rt(o)}else if(f==="throttle"&&o[0]===":"){o.shift();a.throttle=h(w(o,b))}else if(f==="queue"&&o[0]===":"){o.shift();a.queue=w(o,b)}else if(f==="root"&&o[0]===":"){o.shift();a[f]=rt(o)}else if(f==="threshold"&&o[0]===":"){o.shift();a[f]=w(o,b)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}w(o,v)}r.push(a)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}w(o,v)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=te(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(d(e,"form")){return[{trigger:"submit"}]}else if(d(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(d(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){ie(e).cancelled=true}function ct(e,t,n){const r=ie(e);r.timeout=E().setTimeout(function(){if(le(e)&&r.cancelled!==true){if(!gt(n,e,Mt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function at(e){return g(e,Q.config.disableSelector)}function ft(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){pt(t,function(e,t){const n=ue(e);if(at(n)){a(n);return}he(r,o,n,t)},n,e,true)})}}function dt(e,t){const n=ue(t);if(!n){return false}if(e.type==="submit"||e.type==="click"){if(n.tagName==="FORM"){return true}if(d(n,'input[type="submit"], button')&&g(n,"form")!==null){return true}if(n instanceof HTMLAnchorElement&&n.href&&(n.getAttribute("href")==="#"||n.getAttribute("href").indexOf("#")!==0)){return true}}return false}function ht(e,t){return ie(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function gt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(ne().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function pt(l,c,e,u,a){const f=ie(l);let t;if(u.from){t=p(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in f)){f.lastValue=new WeakMap}t.forEach(function(e){if(!f.lastValue.has(u)){f.lastValue.set(u,new WeakMap)}f.lastValue.get(u).set(e,e.value)})}se(t,function(i){const s=function(e){if(!le(l)){i.removeEventListener(u.trigger,s);return}if(ht(l,e)){return}if(a||dt(e,l)){e.preventDefault()}if(gt(u,l,e)){return}const t=ie(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!d(ue(e.target),u.target)){return}}if(u.once){if(f.triggeredOnce){return}else{f.triggeredOnce=true}}if(u.changed){const n=event.target;const r=n.value;const o=f.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(f.delayed){clearTimeout(f.delayed)}if(f.throttle){return}if(u.throttle>0){if(!f.throttle){de(l,"htmx:trigger");c(l,e);f.throttle=E().setTimeout(function(){f.throttle=null},u.throttle)}}else if(u.delay>0){f.delayed=E().setTimeout(function(){de(l,"htmx:trigger");c(l,e)},u.delay)}else{de(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let xt=null;function yt(){if(!xt){xt=function(){mt=true};window.addEventListener("scroll",xt);window.addEventListener("resize",xt);setInterval(function(){if(mt){mt=false;se(ne().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&X(e)){e.setAttribute("data-hx-revealed","true");const t=ie(e);if(t.initHash){de(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){de(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;t(e)}};if(r>0){E().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;se(o,function(r){if(s(t,"hx-"+r)){const o=te(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ue(e);if(g(n,Q.config.disableSelector)){a(n);return}he(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){yt();pt(r,n,t,e);bt(ue(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ae(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e0){t.polling=true;ct(ue(r),n,e)}else{pt(r,n,t,e)}}function Et(e){const t=ue(e);if(!t){return false}const n=t.attributes;for(let e=0;e", "+e).join(""));return o}else{return[]}}function Tt(e){const t=g(ue(e.target),"button, input[type='submit']");const n=Lt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Lt(e);if(t){t.lastButtonClicked=null}}function Lt(e){const t=g(ue(e.target),"button, input[type='submit']");if(!t){return}const n=y("#"+ee(t,"form"),t.getRootNode())||g(t,"form");if(!n){return}return ie(n)}function Nt(e){e.addEventListener("click",Tt);e.addEventListener("focusin",Tt);e.addEventListener("focusout",qt)}function At(t,e,n){const r=ie(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){vn(t,function(){if(at(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function It(t){ke(t);for(let e=0;eQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(ne().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Vt(t){if(!B()){return null}t=U(t);const n=S(localStorage.getItem("htmx-history-cache"))||[];for(let e=0;e=200&&this.status<400){de(ne().body,"htmx:historyCacheMissLoad",i);const e=P(this.response);const t=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;const n=Ut();const r=xn(n);kn(e.title);qe(e);Ve(n,t,r);Te();Kt(r.tasks);Bt=o;de(ne().body,"htmx:historyRestore",{path:o,cacheMiss:true,serverResponse:this.response})}else{fe(ne().body,"htmx:historyCacheMissLoadError",i)}};e.send()}function Wt(e){zt();e=e||location.pathname+location.search;const t=Vt(e);if(t){const n=P(t.content);const r=Ut();const o=xn(r);kn(t.title);qe(n);Ve(r,n,o);Te();Kt(o.tasks);E().setTimeout(function(){window.scrollTo(0,t.scroll)},0);Bt=e;de(ne().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{Gt(e)}}}function Zt(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function Yt(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function Qt(e,t){se(e.concat(t),function(e){const t=ie(e);t.requestCount=(t.requestCount||1)-1});se(e,function(e){const t=ie(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});se(t,function(e){const t=ie(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function en(t,n){for(let e=0;en.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);se(e,e=>r.append(t,e))}}function on(t,n,r,o,i){if(o==null||en(t,o)){return}else{t.push(o)}if(tn(o)){const s=ee(o,"name");let e=o.value;if(o instanceof HTMLSelectElement&&o.multiple){e=M(o.querySelectorAll("option:checked")).map(function(e){return e.value})}if(o instanceof HTMLInputElement&&o.files){e=M(o.files)}nn(s,e,n);if(i){sn(o,r)}}if(o instanceof HTMLFormElement){se(o.elements,function(e){if(t.indexOf(e)>=0){rn(e.name,e.value,n)}else{t.push(e)}if(i){sn(e,r)}});new FormData(o).forEach(function(e,t){if(e instanceof File&&e.name===""){return}nn(t,e,n)})}}function sn(e,t){const n=e;if(n.willValidate){de(n,"htmx:validation:validate");if(!n.checkValidity()){t.push({elt:n,message:n.validationMessage,validity:n.validity});de(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})}}}function ln(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function cn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=ie(e);if(s.lastButtonClicked&&!le(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||te(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){on(n,o,i,g(e,"form"),l)}on(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const a=ee(u,"name");nn(a,u.value,o)}const c=we(e,"hx-include");se(c,function(e){on(n,r,i,ue(e),l);if(!d(e,"form")){se(f(e).querySelectorAll(ot),function(e){on(n,r,i,e,l)})}});ln(r,o);return{errors:i,formData:r,values:Nn(r)}}function un(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function an(e){e=qn(e);let n="";e.forEach(function(e,t){n=un(n,t,e)});return n}function fn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":te(t,"id"),"HX-Current-URL":ne().location.href};bn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(ie(e).boosted){r["HX-Boosted"]="true"}return r}function dn(n,e){const t=re(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){se(t.substr(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;se(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function hn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function gn(e,t){const n=t||re(e,"hx-swap");const r={swapStyle:ie(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ie(e).boosted&&!hn(e)){r.show="top"}if(n){const s=F(n);if(s.length>0){for(let e=0;e0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const a=l.substr(5);var o=a.split(":");const f=o.pop();var i=o.length>0?o.join(":"):null;r.show=f;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const d=l.substr("focus-scroll:".length);r.focusScroll=d=="true"}else if(e==0){r.swapStyle=l}else{C("Unknown modifier in hx-swap: "+l)}}}}return r}function pn(e){return re(e,"hx-encoding")==="multipart/form-data"||d(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function mn(t,n,r){let o=null;Ft(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(pn(n)){return ln(new FormData,qn(r))}else{return an(r)}}}function xn(e){return{tasks:[],elts:[e]}}function yn(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ue(ae(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ue(ae(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function bn(r,e,o,i){if(i==null){i={}}if(r==null){return i}const s=te(r,e);if(s){let e=s.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.substr(11);t=true}else if(e.indexOf("js:")===0){e=e.substr(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=vn(r,function(){return Function("return ("+e+")")()},{})}else{n=S(e)}for(const l in n){if(n.hasOwnProperty(l)){if(i[l]==null){i[l]=n[l]}}}}return bn(ue(c(r)),e,o,i)}function vn(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function wn(e,t){return bn(e,"hx-vars",true,t)}function Sn(e,t){return bn(e,"hx-vals",false,t)}function En(e){return ce(wn(e),Sn(e))}function Cn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function On(t){if(t.responseURL&&typeof URL!=="undefined"){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(ne().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function O(e,t){return t.test(e.getAllResponseHeaders())}function Rn(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return he(t,n,null,null,{targetOverride:y(r)||ve,returnPromise:true})}else{let e=y(r.target);if(r.target&&!e||!e&&!y(r.source)){e=ve}return he(t,n,y(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(t,n,null,null,{returnPromise:true})}}function Hn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function Tn(e,t,n){let r;let o;if(typeof URL==="function"){o=new URL(t,document.location.href);const i=document.location.origin;r=i===o.origin}else{o=t;r=l(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!r){return false}}return de(e,"htmx:validateUrl",ce({url:o,sameHost:r},n))}function qn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Ln(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function Nn(r){return new Proxy(r,{get:function(e,t){if(typeof t==="symbol"){return Reflect.get(e,t)}if(t==="toJSON"){return()=>Object.fromEntries(r)}if(t in e){if(typeof e[t]==="function"){return function(){return r[t].apply(r,arguments)}}else{return e[t]}}const n=r.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Ln(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function he(t,n,r,o,i,D){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=ne().body}const M=i.handler||Dn;const X=i.select||null;if(!le(r)){oe(s);return e}const c=i.targetOverride||ue(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:te(r,"hx-target")});oe(l);return e}let u=ie(r);const a=u.lastButtonClicked;if(a){const L=ee(a,"formaction");if(L!=null){n=L}const N=ee(a,"formmethod");if(N!=null){if(N.toLowerCase()!=="dialog"){t=N}}}const f=re(r,"hx-confirm");if(D===undefined){const K=function(e){return he(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:f};if(de(r,"htmx:confirm",G)===false){oe(s);return e}}let d=r;let h=re(r,"hx-sync");let g=null;let F=false;if(h){const A=h.split(":");const I=A[0].trim();if(I==="this"){d=Se(r,"hx-sync")}else{d=ue(ae(r,I))}h=(A[1]||"drop").trim();u=ie(d);if(h==="drop"&&u.xhr&&u.abortable!==true){oe(s);return e}else if(h==="abort"){if(u.xhr){oe(s);return e}else{F=true}}else if(h==="replace"){de(d,"htmx:abort")}else if(h.indexOf("queue")===0){const W=h.split(" ");g=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){de(d,"htmx:abort")}else{if(g==null){if(o){const P=ie(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){g=P.triggerSpec.queue}}if(g==null){g="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(g==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(g==="all"){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(g==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){he(t,n,r,o,i)})}oe(s);return e}}const p=new XMLHttpRequest;u.xhr=p;u.abortable=F;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const B=re(r,"hx-prompt");if(B){var x=prompt(B);if(x===null||!de(r,"htmx:prompt",{prompt:x,target:c})){oe(s);m();return e}}if(f&&!D){if(!confirm(f)){oe(s);m();return e}}let y=fn(r,c,x);if(t!=="get"&&!pn(r)){y["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){y=ce(y,i.headers)}const U=cn(r,t);let b=U.errors;const j=U.formData;if(i.values){ln(j,qn(i.values))}const V=qn(En(r));const v=ln(j,V);let w=dn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=ne().location.href}const S=bn(r,"hx-request");const _=ie(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:_,useUrlParams:E,formData:w,parameters:Nn(w),unfilteredFormData:v,unfilteredParameters:Nn(v),headers:y,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!de(r,"htmx:configRequest",C)){oe(s);m();return e}n=C.path;t=C.verb;y=C.headers;w=qn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){de(r,"htmx:validation:halted",C);oe(s);m();return e}const z=n.split("#");const $=z[0];const O=z[1];let R=n;if(E){R=$;const Z=!w.keys().next().done;if(Z){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=an(w);if(O){R+="#"+O}}}if(!Tn(r,R,C)){fe(r,"htmx:invalidPath",C);oe(l);return e}p.open(t.toUpperCase(),R,true);p.overrideMimeType("text/html");p.withCredentials=C.withCredentials;p.timeout=C.timeout;if(S.noHeaders){}else{for(const k in y){if(y.hasOwnProperty(k)){const Y=y[k];Cn(p,k,Y)}}}const H={xhr:p,target:c,requestConfig:C,etc:i,boosted:_,select:X,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};p.onload=function(){try{const t=Hn(r);H.pathInfo.responsePath=On(p);M(r,H);if(H.keepIndicators!==true){Qt(T,q)}de(r,"htmx:afterRequest",H);de(r,"htmx:afterOnLoad",H);if(!le(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(le(n)){e=n}}if(e){de(e,"htmx:afterRequest",H);de(e,"htmx:afterOnLoad",H)}}oe(s);m()}catch(e){fe(r,"htmx:onLoadError",ce({error:e},H));throw e}};p.onerror=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);oe(l);m()};p.onabort=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);oe(l);m()};p.ontimeout=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);oe(l);m()};if(!de(r,"htmx:beforeRequest",H)){oe(s);m();return e}var T=Zt(r);var q=Yt(r);se(["loadstart","loadend","progress","abort"],function(t){se([p,p.upload],function(e){e.addEventListener(t,function(e){de(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});de(r,"htmx:beforeSend",H);const J=E?null:mn(p,r,w);p.send(J);return e}function An(e,t){const n=t.xhr;let r=null;let o=null;if(O(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(O(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(O(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=re(e,"hx-push-url");const c=re(e,"hx-replace-url");const u=ie(e).boosted;let a=null;let f=null;if(l){a="push";f=l}else if(c){a="replace";f=c}else if(u){a="push";f=s||i}if(f){if(f==="false"){return{}}if(f==="true"){f=s||i}if(t.pathInfo.anchor&&f.indexOf("#")===-1){f=f+"#"+t.pathInfo.anchor}return{type:a,path:f}}else{return{}}}function In(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Pn(e){for(var t=0;t0){E().setTimeout(e,x.swapDelay)}else{e()}}if(f){fe(o,"htmx:responseError",ce({error:"Response Status Error Code "+s.status+" from "+i.pathInfo.requestPath},i))}}const Mn={};function Xn(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function Fn(e,t){if(t.init){t.init(n)}Mn[e]=ce(Xn(),t)}function Bn(e){delete Mn[e]}function Un(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=te(e,"hx-ext");if(t){se(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=Mn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return Un(ue(c(e)),n,r)}var jn=false;ne().addEventListener("DOMContentLoaded",function(){jn=true});function Vn(e){if(jn||ne().readyState==="complete"){e()}else{ne().addEventListener("DOMContentLoaded",e)}}function _n(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";ne().head.insertAdjacentHTML("beforeend","")}}function zn(){const e=ne().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function $n(){const e=zn();if(e){Q.config=ce(Q.config,e)}}Vn(function(){$n();_n();let e=ne().body;kt(e);const t=ne().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=ie(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){Wt();se(t,function(e){de(e,"htmx:restored",{document:ne(),triggerEvent:de})})}else{if(n){n(e)}}};E().setTimeout(function(){de(e,"htmx:load",{});e=null},0)});return Q}();
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 3ffc9a13..cb0673ef 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,7 +5,7 @@ django-taggit==4.0
django-taggit-templatetags2==1.6.1
django-crispy-forms==2.0
crispy-bootstrap5==0.7
-Django-Select2==8.1.2
+Django-Select2==8.2.1
pymemcache==4.0
django-datatables-view==1.20.0
WeasyPrint==62.3
diff --git a/staffing/urls.py b/staffing/urls.py
index b214acd5..9ec52f2b 100644
--- a/staffing/urls.py
+++ b/staffing/urls.py
@@ -48,7 +48,7 @@
re_path(r'^non-prod_report/?$', v.missions_report, {"nature": "NONPROD"}, name="nonprod-pivotable"),
re_path(r'^non-prod_report/all$', v.missions_report, {"nature": "NONPROD", "year": "all"}, name="nonprod-pivotable-all"),
re_path(r'^contacts/mission/(?P\d+)/$', v.mission_contacts, name="mission_contacts"),
- re_path(r'^rate/?$', v.mission_consultant_rate, name="mission_consultant_rate"),
+ re_path(r'^rate/mission/(?P\d+)/consultant/(?P\d+)/$', v.mission_consultant_rate, name="mission_consultant_rate"),
re_path(r'^pdc-detail/(?P\d+)/(?P\d+)/?$', v.pdc_detail, name="pdc_detail"),
re_path(r'^datatable/all-missions/data/$', t.MissionsTableDT.as_view(), name='all_mission_table_DT'),
re_path(r'^datatable/consultant-all-missions/(?P\d+)/data/$', t.MissionsTableDT.as_view(), name='consultant_all_mission_table_DT'),
diff --git a/staffing/utils.py b/staffing/utils.py
index 26bc6914..b894f417 100644
--- a/staffing/utils.py
+++ b/staffing/utils.py
@@ -397,3 +397,17 @@ def check_timesheet_validity(missions, consultant, month):
if limited_individual_mode_offending_missions:
return _("Charge cannot exceed forecast (%s)") %\
", ".join([str(m) for m in limited_individual_mode_offending_missions])
+
+
+def compute_mission_consultant_rates(mission):
+ """helper function to compute mission rates for each consultant for tab display and htmx edit form"""
+ rates = {}
+ objective_rates = mission.consultant_objective_rates()
+ for consultant, rate in mission.consultant_rates().items():
+ rates[consultant] = (rate, objective_rates.get(consultant))
+ try:
+ objective_dates = [i[0] for i in list(objective_rates.values())[0]]
+ except IndexError:
+ # No consultant or no objective on mission timeframe
+ objective_dates = []
+ return objective_dates, rates
diff --git a/staffing/views.py b/staffing/views.py
index 5f8f69e0..fa5e2a24 100644
--- a/staffing/views.py
+++ b/staffing/views.py
@@ -15,7 +15,7 @@
from django.core.cache import cache
from django.shortcuts import render, redirect
-from django.http import HttpResponseRedirect, HttpResponse, Http404
+from django.http import HttpResponseRedirect, HttpResponse, Http404, HttpResponseForbidden
from django.contrib.auth.decorators import permission_required
from django.forms.models import inlineformset_factory
from django.forms import formset_factory
@@ -46,10 +46,10 @@
from core.utils import working_days, nextMonth, previousMonth, daysOfMonth, previousWeek, nextWeek, monthWeekNumber, \
to_int_or_round, COLORS, cumulateList, user_has_feature, get_parameter, \
get_fiscal_years_from_qs, get_fiscal_year
-from core.decorator import pydici_non_public, pydici_feature, PydiciNonPublicdMixin, pydici_subcontractor
+from core.decorator import pydici_non_public, pydici_feature, PydiciNonPublicdMixin
from staffing.utils import gatherTimesheetData, saveTimesheetData, saveFormsetAndLog, \
sortMissions, holidayDays, staffingDates, time_string_for_day_percent, \
- timesheet_report_data, timesheet_report_data_grouped, check_timesheet_validity
+ timesheet_report_data, timesheet_report_data_grouped, check_timesheet_validity, compute_mission_consultant_rates
from staffing.forms import MissionForm, OptimiserForm, MissionOptimiserForm, MissionOptimiserFormsetHelper
from staffing.optim import solve_pdc, solver_solution_format, compute_consultant_freetime, compute_consultant_rates, solver_apply_forecast
from staffing.optim import OPTIM_NEWBIE_SENIOR_LIMIT, OPTIM_SENIOR_DIRECTOR_LIMIT
@@ -123,27 +123,19 @@ def check_user_timesheet_access(user, consultant, timesheet_month):
@pydici_non_public
-def missions(request, only_active=True, consultant_id=None):
+def missions(request, only_active=True):
"""List of missions"""
- if consultant_id:
- consultant = Consultant.objects.get(id=consultant_id)
- if only_active:
- data_url = reverse('staffing:consultant_active_mission_table_DT', args=(consultant_id,))
- else:
- data_url = reverse('staffing:consultant_all_mission_table_DT', args=(consultant_id,))
+ if only_active:
+ data_url = reverse('staffing:active_mission_table_DT')
else:
- consultant = None
- if only_active:
- data_url = reverse('staffing:active_mission_table_DT')
- else:
- data_url = reverse('staffing:all_mission_table_DT')
+ data_url = reverse('staffing:all_mission_table_DT')
return render(request, "staffing/missions.html",
{"all": not only_active,
- "consultant": consultant,
"data_url": data_url,
"datatable_options": ''' "columnDefs": [{ "orderable": false, "targets": [4, 8, 9] },
{ className: "hidden-xs hidden-sm hidden-md", "targets": [6,7,8,9]}],
- "order": [[0, "asc"]] ''',
+ "order": [[0, "asc"]],
+ "drawCallback": function( oSettings ) {htmx.process(document.body); }''',
"user": request.user})
@@ -163,15 +155,7 @@ def mission_home(request, mission_id):
@pydici_non_public
def mission_consultants(request, mission_id):
mission = Mission.objects.get(id=mission_id)
- rates = {}
- objective_rates = mission.consultant_objective_rates()
- for consultant, rate in mission.consultant_rates().items():
- rates[consultant] = (rate, objective_rates.get(consultant))
- try:
- objective_dates = [i[0] for i in list(objective_rates.values())[0]]
- except IndexError:
- # No consultant or no objective on mission timeframe
- objective_dates = []
+ objective_dates, rates = compute_mission_consultant_rates(mission)
return render(request, "staffing/mission_consultants.html",
{"mission": mission,
"objective_dates": objective_dates,
@@ -284,7 +268,8 @@ def consultant_missions(request, only_active=True, consultant_id=None):
"data_url": data_url,
"datatable_options": ''' "columnDefs": [{ "orderable": false, "targets": [4, 8, 9] },
{ className: "hidden-xs hidden-sm hidden-md", "targets": [6,7,8,9]}],
- "order": [[3, "asc"]]
+ "order": [[3, "asc"]],
+ "drawCallback": function( oSettings ) {htmx.process(document.body); }
''',
"user": request.user})
@@ -866,16 +851,14 @@ def fixed_price_missions_report(request):
@pydici_non_public
def deactivate_mission(request, mission_id):
- """Deactivate the given mission"""
+ """Deactivate the given mission. Fragment for htmx call"""
try:
- error = False
mission = Mission.objects.get(id=mission_id)
mission.active = False
mission.save()
+ return HttpResponse(_("mission archived"))
except Mission.DoesNotExist:
- error = True
- return HttpResponse(json.dumps({"error": error, "id": mission_id}),
- content_type="application/json")
+ return HttpResponse(_("mission not found"))
@cache_control(no_store=True)
@@ -1546,7 +1529,6 @@ def holiday_csv_timesheet(request, year=None, month=None):
return response
-
@pydici_non_public
@pydici_feature("management")
def holidays_planning(request, year=None, month=None):
@@ -1741,7 +1723,6 @@ def missions_report(request, year=None, nature="HOLIDAYS"):
"derivedAttributes": [],})
-
@pydici_non_public
@pydici_feature("leads")
@permission_required("staffing.add_mission")
@@ -1780,35 +1761,48 @@ def create_new_mission_from_lead(request, lead_id):
@pydici_non_public
-def mission_consultant_rate(request):
- """Select or create financial condition for this consultant/mission tuple and update it
- This is intended to be used through a jquery jeditable call"""
+def mission_consultant_rate(request, mission_id, consultant_id):
+ """Select or create financial condition for this consultant/mission tuple and update it with htmx"""
if not (request.user.has_perm("staffing.add_financialcondition") and
- request.user.has_perm("staffing.change_financialcondition")):
- return HttpResponse(_("You are not allowed to do that"))
+ request.user.has_perm("staffing.change_financialcondition")):
+ return HttpResponseForbidden()
+
try:
- sold, mission_id, consultant_id = request.POST["id"].split("-")
mission = Mission.objects.get(id=mission_id)
consultant = Consultant.objects.get(id=consultant_id)
condition, created = FinancialCondition.objects.get_or_create(mission=mission, consultant=consultant,
defaults={"daily_rate": 0})
- value = escape(request.POST["value"].replace(" ", ""))
- if sold == "sold":
+ except (Mission.DoesNotExist, Consultant.DoesNotExist):
+ return HttpResponse(_("Mission or consultant does not exist"), status=404)
+
+ if request.method == "GET":
+ edit = True
+ else:
+ edit = False
+ change = None
+ if request.POST.get("sold"):
+ value = request.POST["sold"]
change = {_(f"daily rate for {consultant}"): [condition.daily_rate, value]}
condition.daily_rate = value
- else:
+
+ if request.POST.get("bought"):
+ value = request.POST["bought"]
change = {_(f"bought daily rate for {consultant}"): [condition.daily_rate, value]}
condition.bought_daily_rate = value
- condition.save()
- if mission.responsible:
- compute_consultant_tasks.delay(mission.responsible.id)
- LogEntry.objects.log_create(instance=mission, actor=request.user, action=LogEntry.Action.UPDATE, changes=json.dumps(change))
- return HttpResponse(value)
- except (Mission.DoesNotExist, Consultant.DoesNotExist):
- return HttpResponse(_("Mission or consultant does not exist"))
- except ValueError:
- return HttpResponse(_("Incorrect value"))
+ if change:
+ try:
+ condition.save()
+ cache.delete("Mission.consultant_rates%s" % mission.id) # flush rate cache
+ except ValueError:
+ return HttpResponse(status=400)
+ if mission.responsible:
+ compute_consultant_tasks.delay(mission.responsible.id)
+ LogEntry.objects.log_create(instance=mission, actor=request.user, action=LogEntry.Action.UPDATE, changes=json.dumps(change))
+
+ objective_dates, rates = compute_mission_consultant_rates(mission)
+ return render(request, "staffing/_mission_consultants_rate.html", {"mission": mission, "consultant": consultant,
+ "rate": rates[consultant], "edit": edit})
@pydici_non_public
@@ -1873,11 +1867,9 @@ def mission_contacts(request, mission_id):
form = MissionContactsForm(request.POST, instance=mission)
if form.is_valid():
form.save()
- return HttpResponseRedirect(reverse("staffing:mission_home", args=[mission.id, ]))
# Unbound form
form = MissionContactsForm(instance=mission)
- # TODO: add link to add mission contact
missionContacts = mission.contacts.select_related().order_by("company")
return render(request, "staffing/mission_contacts.html",
{"mission": mission,
diff --git a/templates/core/_datatables.html b/templates/core/_datatables.html
index f957f918..537da518 100644
--- a/templates/core/_datatables.html
+++ b/templates/core/_datatables.html
@@ -12,8 +12,9 @@
"responsive": true,
"fixedHeader": true,
"ajax": "{{ data_url }}",
- buttons: ['excel', 'csv'],
- dom: "<'row'<'col-md-12'f>><'row'<'col-md-12'tr>><'row'<'col-md-3'l><'col-md-3'B><'col-md-6'p>>",
+ layout: {
+ topStart: ['pageLength', { buttons: [ 'excel', 'csv' ]}],
+ },
{% if datatable_options %}{{ datatable_options|safe }}, {% endif %}
"language": {
"decimal": ",",
@@ -25,12 +26,7 @@
"infoFiltered": "{% trans '(filtered from _MAX_ total records)' %}",
"search": "{% trans 'Search:' %}",
"lengthMenu": "{% trans 'Show _MENU_ entries' %}",
- paginate:{
- first:'<<',
- last: '>>',
- next: '>',
- previous: '<'
- },
+
},
});
});
diff --git a/templates/core/_form.html b/templates/core/_form.html
new file mode 100644
index 00000000..a7f0a246
--- /dev/null
+++ b/templates/core/_form.html
@@ -0,0 +1,5 @@
+{# Standard pydici single form fragment for generic views #}
+{% load crispy_forms_tags %}
+
+{% crispy form form.helper %}
+{% include "core/_datepicker.html" %}
\ No newline at end of file
diff --git a/templates/core/pydici.html b/templates/core/pydici.html
index 5b3e3bb2..2ba7d375 100644
--- a/templates/core/pydici.html
+++ b/templates/core/pydici.html
@@ -23,7 +23,8 @@
-
+
+
{% block extracss %}{% endblock %}
@@ -40,6 +41,8 @@
+
+
@@ -47,7 +50,7 @@
{% endif %}
-
+
{% block extrajs %}{% endblock %}
diff --git a/templates/crm/_mission_contact_form.html b/templates/crm/_mission_contact_form.html
new file mode 100644
index 00000000..6e40636b
--- /dev/null
+++ b/templates/crm/_mission_contact_form.html
@@ -0,0 +1,13 @@
+{# form to create a new contact linked to a mission #}
+{# context: missionContactForm and contactForm #}
+{% load crispy_forms_tags %}
+{% load i18n %}
+
+
\ No newline at end of file
diff --git a/templates/crm/clientcompany_detail.html b/templates/crm/clientcompany_detail.html
index f30bcc42..787d2c98 100644
--- a/templates/crm/clientcompany_detail.html
+++ b/templates/crm/clientcompany_detail.html
@@ -312,6 +312,53 @@
{% trans "Leads" %}
{% endwith %}
{% endwith %}
+
{% trans "Supplier Leads" %}
+
+
+
+
{% trans "client" %}
+
{% trans "name" %}
+
{% trans "deal id" %}
+
{% trans "subsidiary" %}
+
{% trans "responsible" %}
+
{% trans "amount (k€)" %}
+
{% trans "status" %}
+
{% trans "creation date" %}
+
+
+
+
+
+
+ {% with "supplier_lead_table" as table_id %}
+ {% with supplier_lead_data_url as data_url %}
+ {% include "core/_datatables.html" %}
+ {% endwith %}
+ {% endwith %}
+
+
{% trans "Business broker Leads" %}
+
+
+
+
{% trans "client" %}
+
{% trans "name" %}
+
{% trans "deal id" %}
+
{% trans "subsidiary" %}
+
{% trans "responsible" %}
+
{% trans "amount (k€)" %}
+
{% trans "status" %}
+
{% trans "creation date" %}
+
+
+
+
+
+
+ {% with "businessbroker_lead_table" as table_id %}
+ {% with businessbroker_lead_data_url as data_url %}
+ {% include "core/_datatables.html" %}
+ {% endwith %}
+ {% endwith %}
{% if record.state == "PAID" %}
{% trans "Paid" %}
{% else %}
{{ record.get_state_display }}
-{% endif %}
\ No newline at end of file
+{% endif %}
+
\ No newline at end of file
diff --git a/templates/expense/_expense_transitions_column.html b/templates/expense/_expense_transitions_column.html
new file mode 100644
index 00000000..7e393aef
--- /dev/null
+++ b/templates/expense/_expense_transitions_column.html
@@ -0,0 +1,30 @@
+{% load i18n %}
+{% load l10n %}
+{# context : record (expense object) #}
+
+{# out of band swap for expense state #}
+