From 9fa37a915657d3637d53920a348dbc47779ab0a8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 3 Jan 2021 22:57:39 +1100 Subject: [PATCH 01/40] Improve visualisation of global settings --- InvenTree/common/models.py | 16 +++++++++++++++- .../templates/InvenTree/settings/build.html | 2 +- .../templates/InvenTree/settings/global.html | 8 ++++---- .../templates/InvenTree/settings/header.html | 12 ++++++++++++ InvenTree/templates/InvenTree/settings/part.html | 16 +++++++++------- InvenTree/templates/InvenTree/settings/po.html | 2 +- .../templates/InvenTree/settings/setting.html | 9 ++++++++- InvenTree/templates/InvenTree/settings/so.html | 2 +- .../templates/InvenTree/settings/stock.html | 3 +++ 9 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 InvenTree/templates/InvenTree/settings/header.html diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index fa1d4dc5c9..924c55b54d 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -110,7 +110,21 @@ class InvenTreeSetting(models.Model): 'default': True, 'validator': bool }, - + + 'PART_TEMPLATE': { + 'name': _('Template'), + 'description': _('Parts are templates by default'), + 'default': False, + 'validator': bool, + }, + + 'PART_ASSEMBLY': { + 'name': _('Assembly'), + 'description': _('Parts can be assembled from other components by default'), + 'default': False, + 'validator': bool, + }, + 'PART_COMPONENT': { 'name': _('Component'), 'description': _('Parts can be used as sub-components by default'), diff --git a/InvenTree/templates/InvenTree/settings/build.html b/InvenTree/templates/InvenTree/settings/build.html index 781402795b..7d04a8f8b7 100644 --- a/InvenTree/templates/InvenTree/settings/build.html +++ b/InvenTree/templates/InvenTree/settings/build.html @@ -13,7 +13,7 @@ {% block settings %} - + {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_PREFIX" %} {% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_REGEX" %} diff --git a/InvenTree/templates/InvenTree/settings/global.html b/InvenTree/templates/InvenTree/settings/global.html index 775d30b915..76af68b441 100644 --- a/InvenTree/templates/InvenTree/settings/global.html +++ b/InvenTree/templates/InvenTree/settings/global.html @@ -13,11 +13,11 @@ {% block settings %}
- + {% include "InvenTree/settings/header.html" %} - {% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE" %} - {% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" %} - {% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" %} + {% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE" icon="fa-info-circle" %} + {% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" icon="fa-building" %} + {% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-dollar-sign" %}
diff --git a/InvenTree/templates/InvenTree/settings/header.html b/InvenTree/templates/InvenTree/settings/header.html new file mode 100644 index 0000000000..d60a4dd784 --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/header.html @@ -0,0 +1,12 @@ +{% load i18n %} + + + + + + {% trans "Setting" %} + {% trans "Value" %} + {% trans "Description" %} + + + diff --git a/InvenTree/templates/InvenTree/settings/part.html b/InvenTree/templates/InvenTree/settings/part.html index d1ad6e98e9..415449a559 100644 --- a/InvenTree/templates/InvenTree/settings/part.html +++ b/InvenTree/templates/InvenTree/settings/part.html @@ -14,16 +14,18 @@

{% trans "Part Options" %}

- + {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="PART_IPN_REGEX" %} {% include "InvenTree/settings/setting.html" with key="PART_ALLOW_DUPLICATE_IPN" %} - - {% include "InvenTree/settings/setting.html" with key="PART_COMPONENT" %} - {% include "InvenTree/settings/setting.html" with key="PART_PURCHASEABLE" %} - {% include "InvenTree/settings/setting.html" with key="PART_SALABLE" %} - {% include "InvenTree/settings/setting.html" with key="PART_TRACKABLE" %} - + + {% include "InvenTree/settings/setting.html" with key="PART_TEMPLATE" icon="fa-clone" %} + {% include "InvenTree/settings/setting.html" with key="PART_ASSEMBLY" icon="fa-tools" %} + {% include "InvenTree/settings/setting.html" with key="PART_COMPONENT" icon="fa-th"%} + {% include "InvenTree/settings/setting.html" with key="PART_TRACKABLE" icon="fa-directions" %} + {% include "InvenTree/settings/setting.html" with key="PART_PURCHASEABLE" icon="fa-shopping-cart" %} + {% include "InvenTree/settings/setting.html" with key="PART_SALABLE" icon="fa-dollar-sign" %} + {% include "InvenTree/settings/setting.html" with key="PART_COPY_BOM" %} {% include "InvenTree/settings/setting.html" with key="PART_COPY_PARAMETERS" %} {% include "InvenTree/settings/setting.html" with key="PART_COPY_TESTS" %} diff --git a/InvenTree/templates/InvenTree/settings/po.html b/InvenTree/templates/InvenTree/settings/po.html index a709d40dd3..20e3b0074b 100644 --- a/InvenTree/templates/InvenTree/settings/po.html +++ b/InvenTree/templates/InvenTree/settings/po.html @@ -11,7 +11,7 @@ {% block settings %}
- + {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="PURCHASEORDER_REFERENCE_PREFIX" %} diff --git a/InvenTree/templates/InvenTree/settings/setting.html b/InvenTree/templates/InvenTree/settings/setting.html index ffbb78cbbc..b08f4aeb3a 100644 --- a/InvenTree/templates/InvenTree/settings/setting.html +++ b/InvenTree/templates/InvenTree/settings/setting.html @@ -3,6 +3,11 @@ {% setting_object key as setting %} + + + {{ setting.value }}{{ setting.units }} + {% else %} {% trans "No value set" %} {% endif %} diff --git a/InvenTree/templates/InvenTree/settings/so.html b/InvenTree/templates/InvenTree/settings/so.html index 368374532f..4ef1709068 100644 --- a/InvenTree/templates/InvenTree/settings/so.html +++ b/InvenTree/templates/InvenTree/settings/so.html @@ -12,7 +12,7 @@ {% block settings %}
+ {% if icon %} + + {% endif %} + {{ setting.name }} {% if setting.is_bool %} @@ -11,7 +16,9 @@ {% else %} {% if setting.value %} - {{ setting.value }}{{ setting.units }}
- + {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="SALESORDER_REFERENCE_PREFIX" %} diff --git a/InvenTree/templates/InvenTree/settings/stock.html b/InvenTree/templates/InvenTree/settings/stock.html index c3c40087ff..7bec8b566c 100644 --- a/InvenTree/templates/InvenTree/settings/stock.html +++ b/InvenTree/templates/InvenTree/settings/stock.html @@ -10,4 +10,7 @@ {% endblock %} {% block settings %} +
+ No Stock settings available +
{% endblock %} \ No newline at end of file From 6c7b648133352e5c03856a82d6ab772b17eb66e9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 3 Jan 2021 23:06:51 +1100 Subject: [PATCH 02/40] Implement global settings for assembly and template values of Part model --- InvenTree/part/forms.py | 3 + .../migrations/0061_auto_20210103_2306.py | 74 +++++++++++++++ InvenTree/part/models.py | 90 +++++++++++++------ InvenTree/part/settings.py | 16 ++++ InvenTree/part/test_part.py | 4 + 5 files changed, 160 insertions(+), 27 deletions(-) create mode 100644 InvenTree/part/migrations/0061_auto_20210103_2306.py diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 1cbbec0b42..458667f3fe 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -230,6 +230,9 @@ class EditPartForm(HelperForm): 'default_supplier', 'units', 'minimum_stock', + 'component', + 'assembly', + 'is_template', 'trackable', 'purchaseable', 'salable', diff --git a/InvenTree/part/migrations/0061_auto_20210103_2306.py b/InvenTree/part/migrations/0061_auto_20210103_2306.py new file mode 100644 index 0000000000..fa1f8c04c6 --- /dev/null +++ b/InvenTree/part/migrations/0061_auto_20210103_2306.py @@ -0,0 +1,74 @@ +# Generated by Django 3.0.7 on 2021-01-03 12:06 + +import InvenTree.fields +import InvenTree.validators +from django.db import migrations, models +import django.db.models.deletion +import mptt.fields +import part.settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0055_auto_20201117_1453'), + ('part', '0060_merge_20201112_1722'), + ] + + operations = [ + migrations.AlterField( + model_name='part', + name='IPN', + field=models.CharField(blank=True, help_text='Internal Part Number', max_length=100, null=True, validators=[InvenTree.validators.validate_part_ipn], verbose_name='IPN'), + ), + migrations.AlterField( + model_name='part', + name='assembly', + field=models.BooleanField(default=part.settings.part_assembly_default, help_text='Can this part be built from other parts?', verbose_name='Assembly'), + ), + migrations.AlterField( + model_name='part', + name='category', + field=mptt.fields.TreeForeignKey(blank=True, help_text='Part category', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='parts', to='part.PartCategory', verbose_name='Category'), + ), + migrations.AlterField( + model_name='part', + name='default_location', + field=mptt.fields.TreeForeignKey(blank=True, help_text='Where is this item normally stored?', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_parts', to='stock.StockLocation', verbose_name='Default Location'), + ), + migrations.AlterField( + model_name='part', + name='description', + field=models.CharField(help_text='Part description', max_length=250, verbose_name='Description'), + ), + migrations.AlterField( + model_name='part', + name='is_template', + field=models.BooleanField(default=part.settings.part_template_default, help_text='Is this part a template part?', verbose_name='Is Template'), + ), + migrations.AlterField( + model_name='part', + name='keywords', + field=models.CharField(blank=True, help_text='Part keywords to improve visibility in search results', max_length=250, null=True, verbose_name='Keywords'), + ), + migrations.AlterField( + model_name='part', + name='link', + field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'), + ), + migrations.AlterField( + model_name='part', + name='name', + field=models.CharField(help_text='Part name', max_length=100, validators=[InvenTree.validators.validate_part_name], verbose_name='Name'), + ), + migrations.AlterField( + model_name='part', + name='revision', + field=models.CharField(blank=True, help_text='Part revision or version number', max_length=100, null=True, verbose_name='Revision'), + ), + migrations.AlterField( + model_name='part', + name='variant_of', + field=models.ForeignKey(blank=True, help_text='Is this part a variant of another part?', limit_choices_to={'active': True, 'is_template': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='variants', to='part.Part', verbose_name='Variant Of'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index b9d63979e7..bcb9c99749 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -640,36 +640,69 @@ class Part(MPTTModel): parent_part.clean() parent_part.save() - name = models.CharField(max_length=100, blank=False, - help_text=_('Part name'), - validators=[validators.validate_part_name] - ) + name = models.CharField( + max_length=100, blank=False, + help_text=_('Part name'), + verbose_name=_('Name'), + validators=[validators.validate_part_name] + ) - is_template = models.BooleanField(default=False, help_text=_('Is this part a template part?')) + is_template = models.BooleanField( + default=part_settings.part_template_default, + verbose_name=_('Is Template'), + help_text=_('Is this part a template part?') + ) - variant_of = models.ForeignKey('part.Part', related_name='variants', - null=True, blank=True, - limit_choices_to={ - 'is_template': True, - 'active': True, - }, - on_delete=models.SET_NULL, - help_text=_('Is this part a variant of another part?')) + variant_of = models.ForeignKey( + 'part.Part', related_name='variants', + null=True, blank=True, + limit_choices_to={ + 'is_template': True, + 'active': True, + }, + on_delete=models.SET_NULL, + help_text=_('Is this part a variant of another part?'), + verbose_name=_('Variant Of'), + ) - description = models.CharField(max_length=250, blank=False, help_text=_('Part description')) + description = models.CharField( + max_length=250, blank=False, + verbose_name=_('Description'), + help_text=_('Part description') + ) - keywords = models.CharField(max_length=250, blank=True, null=True, help_text=_('Part keywords to improve visibility in search results')) + keywords = models.CharField( + max_length=250, blank=True, null=True, + verbose_name=_('Keywords'), + help_text=_('Part keywords to improve visibility in search results') + ) - category = TreeForeignKey(PartCategory, related_name='parts', - null=True, blank=True, - on_delete=models.DO_NOTHING, - help_text=_('Part category')) + category = TreeForeignKey( + PartCategory, related_name='parts', + null=True, blank=True, + on_delete=models.DO_NOTHING, + verbose_name=_('Category'), + help_text=_('Part category') + ) - IPN = models.CharField(max_length=100, blank=True, null=True, help_text=_('Internal Part Number'), validators=[validators.validate_part_ipn]) + IPN = models.CharField( + max_length=100, blank=True, null=True, + verbose_name=_('IPN'), + help_text=_('Internal Part Number'), + validators=[validators.validate_part_ipn] + ) - revision = models.CharField(max_length=100, blank=True, null=True, help_text=_('Part revision or version number')) + revision = models.CharField( + max_length=100, blank=True, null=True, + help_text=_('Part revision or version number'), + verbose_name=_('Revision'), + ) - link = InvenTreeURLField(blank=True, null=True, help_text=_('Link to external URL')) + link = InvenTreeURLField( + blank=True, null=True, + verbose_name=_('Link'), + help_text=_('Link to external URL') + ) image = StdImageField( upload_to=rename_part_image, @@ -679,10 +712,13 @@ class Part(MPTTModel): delete_orphans=True, ) - default_location = TreeForeignKey('stock.StockLocation', on_delete=models.SET_NULL, - blank=True, null=True, - help_text=_('Where is this item normally stored?'), - related_name='default_parts') + default_location = TreeForeignKey('stock.StockLocation', + on_delete=models.SET_NULL, + blank=True, null=True, + help_text=_('Where is this item normally stored?'), + related_name='default_parts', + verbose_name=_('Default Location'), + ) def get_default_location(self): """ Get the default location for a Part (may be None). @@ -733,7 +769,7 @@ class Part(MPTTModel): units = models.CharField(max_length=20, default="", blank=True, null=True, help_text=_('Stock keeping units for this part')) assembly = models.BooleanField( - default=False, + default=part_settings.part_assembly_default, verbose_name=_('Assembly'), help_text=_('Can this part be built from other parts?') ) diff --git a/InvenTree/part/settings.py b/InvenTree/part/settings.py index 8d87cdffe3..1705f96a80 100644 --- a/InvenTree/part/settings.py +++ b/InvenTree/part/settings.py @@ -8,6 +8,22 @@ from __future__ import unicode_literals from common.models import InvenTreeSetting +def part_assembly_default(): + """ + Returns the default value for the 'assembly' field of a Part object + """ + + return InvenTreeSetting.get_setting('PART_ASSEMBLY') + + +def part_template_default(): + """ + Returns the default value for the 'is_template' field of a Part object + """ + + return InvenTreeSetting.get_setting('PART_TEMPLATE') + + def part_component_default(): """ Returns the default value for the 'component' field of a Part object diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index c02be211b5..4c08911122 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -235,6 +235,8 @@ class PartSettingsTest(TestCase): InvenTreeSetting.set_setting('PART_PURCHASEABLE', val, self.user) InvenTreeSetting.set_setting('PART_SALABLE', val, self.user) InvenTreeSetting.set_setting('PART_TRACKABLE', val, self.user) + InvenTreeSetting.set_setting('PART_ASSEMBLY', val, self.user) + InvenTreeSetting.set_setting('PART_TEMPLATE', val, self.user) self.assertEqual(val, InvenTreeSetting.get_setting('PART_COMPONENT')) self.assertEqual(val, InvenTreeSetting.get_setting('PART_PURCHASEABLE')) @@ -247,6 +249,8 @@ class PartSettingsTest(TestCase): self.assertEqual(part.purchaseable, val) self.assertEqual(part.salable, val) self.assertEqual(part.trackable, val) + self.assertEqual(part.assembly, val) + self.assertEqual(part.is_template, val) Part.objects.filter(pk=part.pk).delete() From 0aeeba808c84062f5cffcd25524e9b1d36293177 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 3 Jan 2021 23:07:21 +1100 Subject: [PATCH 03/40] PEP fixes --- InvenTree/part/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index bcb9c99749..900e0e5a98 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -712,7 +712,8 @@ class Part(MPTTModel): delete_orphans=True, ) - default_location = TreeForeignKey('stock.StockLocation', + default_location = TreeForeignKey( + 'stock.StockLocation', on_delete=models.SET_NULL, blank=True, null=True, help_text=_('Where is this item normally stored?'), From b05504e1c40683b8e310eaf2887d589ba7039f0d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 3 Jan 2021 23:13:58 +1100 Subject: [PATCH 04/40] Add PART_VIRTUAL setting --- InvenTree/common/models.py | 7 +++++++ InvenTree/part/forms.py | 1 + ..._20210103_2306.py => 0061_auto_20210103_2313.py} | 13 ++++++++++++- InvenTree/part/models.py | 8 ++++++-- InvenTree/part/settings.py | 8 ++++++++ InvenTree/templates/InvenTree/settings/part.html | 1 + 6 files changed, 35 insertions(+), 3 deletions(-) rename InvenTree/part/migrations/{0061_auto_20210103_2306.py => 0061_auto_20210103_2313.py} (85%) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 924c55b54d..b3ac4e0381 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -153,6 +153,13 @@ class InvenTreeSetting(models.Model): 'validator': bool, }, + 'PART_VIRTUAL': { + 'name': _('Virtual'), + 'description': _('Parts are virtual by default'), + 'default': False, + 'validator': bool, + }, + 'BUILDORDER_REFERENCE_PREFIX': { 'name': _('Build Order Reference Prefix'), 'description': _('Prefix value for build order reference'), diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 458667f3fe..072e1da8c3 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -236,6 +236,7 @@ class EditPartForm(HelperForm): 'trackable', 'purchaseable', 'salable', + 'virtual', ] diff --git a/InvenTree/part/migrations/0061_auto_20210103_2306.py b/InvenTree/part/migrations/0061_auto_20210103_2313.py similarity index 85% rename from InvenTree/part/migrations/0061_auto_20210103_2306.py rename to InvenTree/part/migrations/0061_auto_20210103_2313.py index fa1f8c04c6..ca0c2a277f 100644 --- a/InvenTree/part/migrations/0061_auto_20210103_2306.py +++ b/InvenTree/part/migrations/0061_auto_20210103_2313.py @@ -1,9 +1,10 @@ -# Generated by Django 3.0.7 on 2021-01-03 12:06 +# Generated by Django 3.0.7 on 2021-01-03 12:13 import InvenTree.fields import InvenTree.validators from django.db import migrations, models import django.db.models.deletion +import markdownx.models import mptt.fields import part.settings @@ -61,6 +62,11 @@ class Migration(migrations.Migration): name='name', field=models.CharField(help_text='Part name', max_length=100, validators=[InvenTree.validators.validate_part_name], verbose_name='Name'), ), + migrations.AlterField( + model_name='part', + name='notes', + field=markdownx.models.MarkdownxField(blank=True, help_text='Part notes - supports Markdown formatting', null=True, verbose_name='Notes'), + ), migrations.AlterField( model_name='part', name='revision', @@ -71,4 +77,9 @@ class Migration(migrations.Migration): name='variant_of', field=models.ForeignKey(blank=True, help_text='Is this part a variant of another part?', limit_choices_to={'active': True, 'is_template': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='variants', to='part.Part', verbose_name='Variant Of'), ), + migrations.AlterField( + model_name='part', + name='virtual', + field=models.BooleanField(default=part.settings.part_virtual_default, help_text='Is this a virtual part, such as a software product or license?', verbose_name='Virtual'), + ), ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 900e0e5a98..fdd80e71b6 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -802,11 +802,15 @@ class Part(MPTTModel): help_text=_('Is this part active?')) virtual = models.BooleanField( - default=False, + default=part_settings.part_virtual_default, verbose_name=_('Virtual'), help_text=_('Is this a virtual part, such as a software product or license?')) - notes = MarkdownxField(blank=True, null=True, help_text=_('Part notes - supports Markdown formatting')) + notes = MarkdownxField( + blank=True, null=True, + verbose_name=_('Notes'), + help_text=_('Part notes - supports Markdown formatting') + ) bom_checksum = models.CharField(max_length=128, blank=True, help_text=_('Stored BOM checksum')) diff --git a/InvenTree/part/settings.py b/InvenTree/part/settings.py index 1705f96a80..801b4dd2ec 100644 --- a/InvenTree/part/settings.py +++ b/InvenTree/part/settings.py @@ -24,6 +24,14 @@ def part_template_default(): return InvenTreeSetting.get_setting('PART_TEMPLATE') +def part_virtual_default(): + """ + Returns the default value for the 'is_virtual' field of Part object + """ + + return InvenTreeSetting.get_setting('PART_VIRTUAL') + + def part_component_default(): """ Returns the default value for the 'component' field of a Part object diff --git a/InvenTree/templates/InvenTree/settings/part.html b/InvenTree/templates/InvenTree/settings/part.html index 415449a559..fe46911dae 100644 --- a/InvenTree/templates/InvenTree/settings/part.html +++ b/InvenTree/templates/InvenTree/settings/part.html @@ -25,6 +25,7 @@ {% include "InvenTree/settings/setting.html" with key="PART_TRACKABLE" icon="fa-directions" %} {% include "InvenTree/settings/setting.html" with key="PART_PURCHASEABLE" icon="fa-shopping-cart" %} {% include "InvenTree/settings/setting.html" with key="PART_SALABLE" icon="fa-dollar-sign" %} + {% include "InvenTree/settings/setting.html" with key="PART_VIRTUAL" icon="fa-ghost" %} {% include "InvenTree/settings/setting.html" with key="PART_COPY_BOM" %} {% include "InvenTree/settings/setting.html" with key="PART_COPY_PARAMETERS" %} From 0f9c4703cf07828b01513bbf892f86750ddda5e6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 3 Jan 2021 23:33:47 +1100 Subject: [PATCH 05/40] Update unit testing --- InvenTree/common/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index 6d6e517ef5..d8777785f9 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -31,7 +31,7 @@ class SettingsTest(TestCase): # There should be two settings objects in the database settings = InvenTreeSetting.objects.all() - self.assertEqual(settings.count(), 2) + self.assertTrue(settings.count() >= 2) instance_name = InvenTreeSetting.objects.get(pk=1) self.assertEqual(instance_name.key, 'INVENTREE_INSTANCE') From 07cda765f03d3cef81c544ce7c504b0245ab4dfd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 3 Jan 2021 23:56:35 +1100 Subject: [PATCH 06/40] Add "expiry_date" field to StockItem model - Also adds "is_expired" function --- .../migrations/0056_stockitem_expiry_date.py | 18 ++++++ InvenTree/stock/models.py | 64 +++++++++++-------- InvenTree/stock/tests.py | 34 ++++++++++ 3 files changed, 89 insertions(+), 27 deletions(-) create mode 100644 InvenTree/stock/migrations/0056_stockitem_expiry_date.py diff --git a/InvenTree/stock/migrations/0056_stockitem_expiry_date.py b/InvenTree/stock/migrations/0056_stockitem_expiry_date.py new file mode 100644 index 0000000000..f558d615a6 --- /dev/null +++ b/InvenTree/stock/migrations/0056_stockitem_expiry_date.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2021-01-03 12:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0055_auto_20201117_1453'), + ] + + operations = [ + migrations.AddField( + model_name='stockitem', + name='expiry_date', + field=models.DateField(blank=True, help_text='Expiry date for stock item. Stock will be considered expired after this date', null=True, verbose_name='Expiry Date'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index d1e46c53a7..24db434d38 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -125,6 +125,7 @@ class StockItem(MPTTModel): serial: Unique serial number for this StockItem link: Optional URL to link to external resource updated: Date that this stock item was last updated (auto) + expiry_date: Expiry date of the StockItem (optional) stocktake_date: Date of last stocktake for this item stocktake_user: User that performed the most recent stocktake review_needed: Flag if StockItem needs review @@ -428,11 +429,19 @@ class StockItem(MPTTModel): related_name='stock_items', null=True, blank=True) - # last time the stock was checked / counted + expiry_date = models.DateField( + blank=True, null=True, + verbose_name=_('Expiry Date'), + help_text=_('Expiry date for stock item. Stock will be considered expired after this date'), + ) + stocktake_date = models.DateField(blank=True, null=True) - stocktake_user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, - related_name='stocktake_stock') + stocktake_user = models.ForeignKey( + User, on_delete=models.SET_NULL, + blank=True, null=True, + related_name='stocktake_stock' + ) review_needed = models.BooleanField(default=False) @@ -459,6 +468,27 @@ class StockItem(MPTTModel): help_text=_('Single unit purchase price at time of purchase'), ) + def is_expired(self): + """ + Returns true if this StockItem is "expired" + + To be "expired", the following conditions must be met: + + - Expiry date is not None + - Expiry date is "in the past" + - The StockItem is otherwise "in stock" + """ + + if self.expiry_date is None: + return False + + if not self.in_stock: + return False + + today = datetime.now().date() + + return self.expiry_date < today + def clearAllocations(self): """ Clear all order allocations for this StockItem: @@ -721,36 +751,16 @@ class StockItem(MPTTModel): @property def in_stock(self): """ - Returns True if this item is in stock + Returns True if this item is in stock. See also: IN_STOCK_FILTER """ - # Quantity must be above zero (unless infinite) - if self.quantity <= 0 and not self.infinite: - return False + query = StockItem.objects.filter(pk=self.pk) - # Not 'in stock' if it has been installed inside another StockItem - if self.belongs_to is not None: - return False - - # Not 'in stock' if it has been sent to a customer - if self.sales_order is not None: - return False + query = query.filter(StockItem.IN_STOCK_FILTER) - # Not 'in stock' if it has been assigned to a customer - if self.customer is not None: - return False - - # Not 'in stock' if it is building - if self.is_building: - return False - - # Not 'in stock' if the status code makes it unavailable - if self.status in StockStatus.UNAVAILABLE_CODES: - return False - - return True + return query.exists() @property def tracking_info_count(self): diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 3d309c0360..274a1328fe 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -49,6 +49,40 @@ class StockTest(TestCase): Part.objects.rebuild() StockItem.objects.rebuild() + def test_expiry(self): + """ + Test expiry date functionality for StockItem model. + """ + + today = datetime.datetime.now().date() + + item = StockItem.objects.create( + location=self.office, + part=Part.objects.get(pk=1), + quantity=10, + ) + + # Without an expiry_date set, item should not be "expired" + self.assertFalse(item.is_expired()) + + # Set the expiry date to today + item.expiry_date = today + item.save() + + self.assertFalse(item.is_expired()) + + # Set the expiry date in the future + item.expiry_date = today + datetime.timedelta(days=5) + item.save() + + self.assertFalse(item.is_expired()) + + # Set the expiry date in the past + item.expiry_date = today - datetime.timedelta(days=5) + item.save() + + self.assertTrue(item.is_expired()) + def test_is_building(self): """ Test that the is_building flag does not count towards stock. From 6d4c81e68beaab9f94d23ad4cc333fd6f11be0fa Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 4 Jan 2021 00:19:48 +1100 Subject: [PATCH 07/40] Add ability to filter by 'expired' status in API --- InvenTree/stock/api.py | 11 +++++++++++ InvenTree/stock/models.py | 3 +++ 2 files changed, 14 insertions(+) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index b74ac200f7..571ec1dd6f 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -525,6 +525,17 @@ class StockList(generics.ListCreateAPIView): # Exclude items which are instaled in another item queryset = queryset.filter(belongs_to=None) + # Filter by 'expired' status + expired = params.get('expired', None) + + if expired is not None: + expired = str2bool(expired) + + if expired: + queryset = queryset.filter(StockItem.EXPIRED_FILTER) + else: + queryset = queryset.exclude(StockItem.EXPIRED_FILTER) + # Filter by customer customer = params.get('customer', None) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 24db434d38..d2d6ec51ee 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -150,6 +150,9 @@ class StockItem(MPTTModel): status__in=StockStatus.AVAILABLE_CODES ) + # A query filter which can be used to filter StockItem objects which have expired + EXPIRED_FILTER = IN_STOCK_FILTER & ~Q(expiry_date=None) & Q(expiry_date__lt=datetime.now().date()) + def save(self, *args, **kwargs): """ Save this StockItem to the database. Performs a number of checks: From 1a930f7f80d04118ac695e0fbc080355e101e087 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 4 Jan 2021 00:20:02 +1100 Subject: [PATCH 08/40] Add ability to edit expiry_date for StockItem --- InvenTree/stock/forms.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 8ab88155e2..4ed7f73f49 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -16,6 +16,7 @@ from mptt.fields import TreeNodeChoiceField from InvenTree.helpers import GetExportFormats from InvenTree.forms import HelperForm from InvenTree.fields import RoundingDecimalFormField +from InvenTree.fields import DatePickerFormField from report.models import TestReport @@ -392,6 +393,10 @@ class EditStockItemForm(HelperForm): part - Cannot be edited after creation """ + expiry_date = DatePickerFormField( + help_text=('Expiration date for this stock item'), + ) + class Meta: model = StockItem @@ -400,6 +405,7 @@ class EditStockItemForm(HelperForm): 'serial', 'batch', 'status', + 'expiry_date', 'purchase_price', 'link', 'delete_on_deplete', From 39b9dcfec9ca1c8d13049d529d27c66b86ab1965 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 4 Jan 2021 00:20:19 +1100 Subject: [PATCH 09/40] Add 'expired' flag to StockItem serializer --- InvenTree/stock/serializers.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index e8675a8fff..730018d98c 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -11,6 +11,9 @@ from .models import StockItemTestResult from django.db.models.functions import Coalesce +from django.db.models import Case, When, Value +from django.db.models import BooleanField + from sql_util.utils import SubquerySum, SubqueryCount from decimal import Decimal @@ -106,6 +109,16 @@ class StockItemSerializer(InvenTreeModelSerializer): tracking_items=SubqueryCount('tracking_info') ) + # Add flag to indicate if the StockItem has expired + queryset = queryset.annotate( + expired=Case( + When( + StockItem.EXPIRED_FILTER, then=Value(True, output_field=BooleanField()), + ), + default=Value(False, output_field=BooleanField()) + ) + ) + return queryset status_text = serializers.CharField(source='get_status_display', read_only=True) @@ -122,6 +135,8 @@ class StockItemSerializer(InvenTreeModelSerializer): allocated = serializers.FloatField(source='allocation_count', required=False) + expired = serializers.BooleanField() + serial = serializers.CharField(required=False) required_tests = serializers.IntegerField(source='required_test_count', read_only=True, required=False) @@ -155,6 +170,8 @@ class StockItemSerializer(InvenTreeModelSerializer): 'belongs_to', 'build', 'customer', + 'expired', + 'expiry_date', 'in_stock', 'is_building', 'link', From a0c95579b42042ed992a3edb165a4aeb5d352b98 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 4 Jan 2021 00:21:47 +1100 Subject: [PATCH 10/40] Display expiry status on StockItem page - Also adds ability to filter Stock table by expired status --- InvenTree/stock/templates/stock/item_base.html | 15 +++++++++++++++ InvenTree/templates/js/stock.js | 4 ++++ InvenTree/templates/js/table_filters.js | 5 +++++ 3 files changed, 24 insertions(+) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 74e93fecc0..4d773b047d 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -71,6 +71,9 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}

{% trans "Stock Item" %} {% stock_status_label item.status large=True %} + {% if item.is_expired %} + {% trans "Expired" %} + {% endif %}


@@ -293,6 +296,18 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}

{% endif %} + {% if item.expiry_date %} + + + + + + {% endif %} diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index f8a7c38d2c..65df58c4b6 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -532,6 +532,10 @@ function loadStockTable(table, options) { html += makeIconBadge('fa-user', '{% trans "Stock item assigned to customer" %}'); } + if (row.expired) { + html += makeIconBadge('fa-stopwatch icon-red', '{% trans "Stock item has expired" %}'); + } + if (row.allocated) { html += makeIconBadge('fa-bookmark', '{% trans "Stock item has been allocated" %}'); } diff --git a/InvenTree/templates/js/table_filters.js b/InvenTree/templates/js/table_filters.js index d2ea26d8c3..054542a4a3 100644 --- a/InvenTree/templates/js/table_filters.js +++ b/InvenTree/templates/js/table_filters.js @@ -106,6 +106,11 @@ function getAvailableTableFilters(tableKey) { title: '{% trans "Depleted" %}', description: '{% trans "Show stock items which are depleted" %}', }, + expired: { + type: 'bool', + title: '{% trans "Expired" %}', + description: '{% trans "Show stock items which have expired" %}', + }, in_stock: { type: 'bool', title: '{% trans "In Stock" %}', From d1ce0f062ecd231bd83dc0b004a0768d10f11d13 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 4 Jan 2021 01:17:05 +1100 Subject: [PATCH 11/40] Improve unit testing for StockItem API --- InvenTree/build/serializers.py | 2 +- InvenTree/order/serializers.py | 2 +- InvenTree/part/fixtures/part.yaml | 1 + InvenTree/stock/fixtures/stock.yaml | 8 +- InvenTree/stock/serializers.py | 2 +- InvenTree/stock/test_api.py | 180 +++++++++++++++++++++++++++- InvenTree/stock/tests.py | 6 +- 7 files changed, 189 insertions(+), 12 deletions(-) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index b71aaecc61..550a3c3a85 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -28,7 +28,7 @@ class BuildSerializer(InvenTreeModelSerializer): quantity = serializers.FloatField() - overdue = serializers.BooleanField() + overdue = serializers.BooleanField(required=False, read_only=True) @staticmethod def annotate_queryset(queryset): diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index f0505728f6..5463feb26f 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -181,7 +181,7 @@ class SalesOrderSerializer(InvenTreeModelSerializer): status_text = serializers.CharField(source='get_status_display', read_only=True) - overdue = serializers.BooleanField() + overdue = serializers.BooleanField(required=False, read_only=True) class Meta: model = SalesOrder diff --git a/InvenTree/part/fixtures/part.yaml b/InvenTree/part/fixtures/part.yaml index f6d9d246db..7d6c09707b 100644 --- a/InvenTree/part/fixtures/part.yaml +++ b/InvenTree/part/fixtures/part.yaml @@ -134,6 +134,7 @@ fields: name: 'Red chair' variant_of: 10000 + IPN: "R.CH" trackable: true category: 7 tree_id: 1 diff --git a/InvenTree/stock/fixtures/stock.yaml b/InvenTree/stock/fixtures/stock.yaml index 719d8a34ce..45a5f5dd7f 100644 --- a/InvenTree/stock/fixtures/stock.yaml +++ b/InvenTree/stock/fixtures/stock.yaml @@ -69,7 +69,7 @@ part: 25 batch: 'ABCDE' location: 7 - quantity: 3 + quantity: 0 level: 0 tree_id: 0 lft: 0 @@ -220,6 +220,7 @@ tree_id: 0 lft: 0 rght: 0 + expiry_date: "1990-10-10" - model: stock.stockitem pk: 521 @@ -232,6 +233,7 @@ tree_id: 0 lft: 0 rght: 0 + status: 60 - model: stock.stockitem pk: 522 @@ -243,4 +245,6 @@ level: 0 tree_id: 0 lft: 0 - rght: 0 \ No newline at end of file + rght: 0 + expiry_date: "1990-10-10" + status: 70 \ No newline at end of file diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 730018d98c..9db1d95dae 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -135,7 +135,7 @@ class StockItemSerializer(InvenTreeModelSerializer): allocated = serializers.FloatField(source='allocation_count', required=False) - expired = serializers.BooleanField() + expired = serializers.BooleanField(required=False, read_only=True) serial = serializers.CharField(required=False) diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py index a34e895ed8..ecca10ba0a 100644 --- a/InvenTree/stock/test_api.py +++ b/InvenTree/stock/test_api.py @@ -1,11 +1,21 @@ +""" +Unit testing for the Stock API +""" + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from datetime import datetime, timedelta + from rest_framework.test import APITestCase from rest_framework import status from django.urls import reverse from django.contrib.auth import get_user_model from InvenTree.helpers import addUserPermissions +from InvenTree.status_codes import StockStatus -from .models import StockLocation +from .models import StockItem, StockLocation class StockAPITestCase(APITestCase): @@ -76,6 +86,170 @@ class StockLocationTest(StockAPITestCase): self.assertEqual(response.status_code, status.HTTP_201_CREATED) +class StockItemListTest(StockAPITestCase): + """ + Tests for the StockItem API LIST endpoint + """ + + list_url = reverse('api-stock-list') + + def get_stock(self, **kwargs): + """ + Filter stock and return JSON object + """ + + response = self.client.get(self.list_url, format='json', data=kwargs) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Return JSON-ified data + return response.data + + def test_get_stock_list(self): + """ + List *all* StockItem objects. + """ + + response = self.get_stock() + + self.assertEqual(len(response), 19) + + def test_filter_by_part(self): + """ + Filter StockItem by Part reference + """ + + response = self.get_stock(part=25) + + self.assertEqual(len(response), 7) + + response = self.get_stock(part=10004) + + self.assertEqual(len(response), 12) + + def test_filter_by_IPN(self): + """ + Filter StockItem by IPN reference + """ + + response = self.get_stock(IPN="R.CH") + self.assertEqual(len(response), 3) + + def test_filter_by_location(self): + """ + Filter StockItem by StockLocation reference + """ + + response = self.get_stock(location=5) + self.assertEqual(len(response), 1) + + response = self.get_stock(location=1, cascade=0) + self.assertEqual(len(response), 0) + + response = self.get_stock(location=1, cascade=1) + self.assertEqual(len(response), 2) + + response = self.get_stock(location=7) + self.assertEqual(len(response), 16) + + def test_filter_by_depleted(self): + """ + Filter StockItem by depleted status + """ + + response = self.get_stock(depleted=1) + self.assertEqual(len(response), 1) + + response = self.get_stock(depleted=0) + self.assertEqual(len(response), 18) + + def test_filter_by_in_stock(self): + """ + Filter StockItem by 'in stock' status + """ + + response = self.get_stock(in_stock=1) + self.assertEqual(len(response), 16) + + response = self.get_stock(in_stock=0) + self.assertEqual(len(response), 3) + + def test_filter_by_status(self): + """ + Filter StockItem by 'status' field + """ + + codes = { + StockStatus.OK: 17, + StockStatus.DESTROYED: 1, + StockStatus.LOST: 1, + StockStatus.DAMAGED: 0, + StockStatus.REJECTED: 0, + } + + for code in codes.keys(): + num = codes[code] + + response = self.get_stock(status=code) + self.assertEqual(len(response), num) + + def test_filter_by_batch(self): + """ + Filter StockItem by batch code + """ + + response = self.get_stock(batch='B123') + self.assertEqual(len(response), 1) + + def test_filter_by_serialized(self): + """ + Filter StockItem by serialized status + """ + + response = self.get_stock(serialized=1) + self.assertEqual(len(response), 12) + + for item in response: + self.assertIsNotNone(item['serial']) + + response = self.get_stock(serialized=0) + self.assertEqual(len(response), 7) + + for item in response: + self.assertIsNone(item['serial']) + + def test_filter_by_expired(self): + """ + Filter StockItem by expiry status + """ + + response = self.get_stock(expired=1) + self.assertEqual(len(response), 1) + + for item in response: + self.assertTrue(item['expired']) + + response = self.get_stock(expired=0) + self.assertEqual(len(response), 18) + + for item in response: + self.assertFalse(item['expired']) + + # Mark some other stock items as expired + today = datetime.now().date() + + for pk in [510, 511, 512]: + item = StockItem.objects.get(pk=pk) + item.expiry_date = today - timedelta(days=pk) + item.save() + + response = self.get_stock(expired=1) + self.assertEqual(len(response), 4) + + response = self.get_stock(expired=0) + self.assertEqual(len(response), 15) + + class StockItemTest(StockAPITestCase): """ Series of API tests for the StockItem API @@ -94,10 +268,6 @@ class StockItemTest(StockAPITestCase): StockLocation.objects.create(name='B', description='location b', parent=top) StockLocation.objects.create(name='C', description='location c', parent=top) - def test_get_stock_list(self): - response = self.client.get(self.list_url, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - def test_create_default_location(self): """ Test the default location functionality, diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 274a1328fe..b0b05b6326 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -177,8 +177,10 @@ class StockTest(TestCase): # There should be 9000 screws in stock self.assertEqual(part.total_stock, 9000) - # There should be 18 widgets in stock - self.assertEqual(StockItem.objects.filter(part=25).aggregate(Sum('quantity'))['quantity__sum'], 19) + # There should be 16 widgets "in stock" + self.assertEqual( + StockItem.objects.filter(part=25).aggregate(Sum('quantity'))['quantity__sum'], 16 + ) def test_delete_location(self): From 4d7c60a13038136e9d1e036a2252f24c6ec40f86 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 4 Jan 2021 09:46:14 +1100 Subject: [PATCH 12/40] Add "expired stock" table to index page --- .../templates/InvenTree/expired_stock.html | 15 +++++++++++++ InvenTree/templates/InvenTree/index.html | 21 +++++++++++++++++-- InvenTree/templates/collapse_index.html | 2 +- 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 InvenTree/templates/InvenTree/expired_stock.html diff --git a/InvenTree/templates/InvenTree/expired_stock.html b/InvenTree/templates/InvenTree/expired_stock.html new file mode 100644 index 0000000000..7ecf117fa7 --- /dev/null +++ b/InvenTree/templates/InvenTree/expired_stock.html @@ -0,0 +1,15 @@ +{% extends "collapse_index.html" %} + +{% load i18n %} + +{% block collapse_title %} + +{% trans "Expired Stock" %} +{% endblock %} + +{% block collapse_content %} + +
{{ item.supplier_part.SKU }}
{% trans "Expiry Date" %} + {{ item.expiry_date }} + {% if item.is_expired %} + {% trans "Expired" %} + {% endif %} +
{% trans "Last Updated" %}
+
+ +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index f862175920..d5b2c87148 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -8,7 +8,7 @@ InvenTree | {% trans "Index" %}

InvenTree


-
+
{% if roles.part.view %} {% include "InvenTree/latest_parts.html" with collapse_id="latest_parts" %} {% include "InvenTree/bom_invalid.html" with collapse_id="bom_invalid" %} @@ -19,11 +19,14 @@ InvenTree | {% trans "Index" %} {% include "InvenTree/build_overdue.html" with collapse_id="build_overdue" %} {% endif %}
-
+
{% if roles.stock.view %} {% include "InvenTree/low_stock.html" with collapse_id="order" %} + {% include "InvenTree/expired_stock.html" with collapse_id="expired" %} {% include "InvenTree/required_stock_build.html" with collapse_id="stock_to_build" %} {% endif %} +
+
{% if roles.purchase_order.view %} {% include "InvenTree/po_outstanding.html" with collapse_id="po_outstanding" %} {% endif %} @@ -83,6 +86,14 @@ loadBuildTable("#build-overdue-table", { disableFilters: true, }); +loadStockTable($("#expired-stock-table"), { + params: { + expired: true, + location_detail: true, + part_detail: true, + }, +}); + loadSimplePartTable("#low-stock-table", "{% url 'api-part-list' %}", { params: { low_stock: true, @@ -151,6 +162,12 @@ $("#build-overdue-table").on('load-success.bs.table', function() { $("#build-overdue-count").html(count); }); +$("#expired-stock-table").on('load-success.bs.table', function() { + var count = $("#expired-stock-table").bootstrapTable('getData').length; + + $("#expired-stock-count").html(count); +}); + $("#low-stock-table").on('load-success.bs.table', function() { var count = $("#low-stock-table").bootstrapTable('getData').length; diff --git a/InvenTree/templates/collapse_index.html b/InvenTree/templates/collapse_index.html index d87f63b244..6e918d7217 100644 --- a/InvenTree/templates/collapse_index.html +++ b/InvenTree/templates/collapse_index.html @@ -1,6 +1,6 @@ {% block collapse_preamble %} {% endblock %} -
+
From 692cee113c0e2ae9aa3748c7de1836aa104baad7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 4 Jan 2021 23:11:35 +1100 Subject: [PATCH 13/40] Display "expiry date" column in stock table --- InvenTree/templates/js/stock.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index 65df58c4b6..af0c85fc02 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -587,6 +587,11 @@ function loadStockTable(table, options) { return locationDetail(row); } }, + { + field: 'expiry_date', + title: '{% trans "Expiry Date" %}', + sortable: true, + }, { field: 'notes', title: '{% trans "Notes" %}', From 37dcf1c1cf4aa148771b2a18c9f6db01bc692a69 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 4 Jan 2021 23:36:11 +1100 Subject: [PATCH 14/40] Add "default_expiry" field to Part model --- InvenTree/part/forms.py | 2 ++ .../migrations/0061_auto_20210104_2331.py | 36 +++++++++++++++++++ InvenTree/part/models.py | 36 ++++++++++++++----- InvenTree/part/templates/part/detail.html | 7 ++++ 4 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 InvenTree/part/migrations/0061_auto_20210104_2331.py diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 1cbbec0b42..4f55ccb14d 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -181,6 +181,7 @@ class EditPartForm(HelperForm): 'keywords': 'fa-key', 'link': 'fa-link', 'IPN': 'fa-hashtag', + 'default_expiry': 'fa-stopwatch', } bom_copy = forms.BooleanField(required=False, @@ -228,6 +229,7 @@ class EditPartForm(HelperForm): 'link', 'default_location', 'default_supplier', + 'default_expiry', 'units', 'minimum_stock', 'trackable', diff --git a/InvenTree/part/migrations/0061_auto_20210104_2331.py b/InvenTree/part/migrations/0061_auto_20210104_2331.py new file mode 100644 index 0000000000..c40b611b29 --- /dev/null +++ b/InvenTree/part/migrations/0061_auto_20210104_2331.py @@ -0,0 +1,36 @@ +# Generated by Django 3.0.7 on 2021-01-04 12:31 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0031_auto_20210103_2215'), + ('part', '0060_merge_20201112_1722'), + ] + + operations = [ + migrations.AddField( + model_name='part', + name='default_expiry', + field=models.PositiveIntegerField(default=0, help_text='Expiry time (in days) for stock items of this part', validators=[django.core.validators.MinValueValidator(0)], verbose_name='Default Expiry'), + ), + migrations.AlterField( + model_name='part', + name='default_supplier', + field=models.ForeignKey(blank=True, help_text='Default supplier part', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_parts', to='company.SupplierPart', verbose_name='Default Supplier'), + ), + migrations.AlterField( + model_name='part', + name='minimum_stock', + field=models.PositiveIntegerField(default=0, help_text='Minimum allowed stock level', validators=[django.core.validators.MinValueValidator(0)], verbose_name='Minimum Stock'), + ), + migrations.AlterField( + model_name='part', + name='units', + field=models.CharField(blank=True, default='', help_text='Stock keeping units for this part', max_length=20, null=True, verbose_name='Units'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index b9d63979e7..390583bd97 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -291,11 +291,12 @@ class Part(MPTTModel): keywords: Optional keywords for improving part search results IPN: Internal part number (optional) revision: Part revision - is_template: If True, this part is a 'template' part and cannot be instantiated as a StockItem + is_template: If True, this part is a 'template' part link: Link to an external page with more information about this part (e.g. internal Wiki) image: Image of this part default_location: Where the item is normally stored (may be null) default_supplier: The default SupplierPart which should be used to procure and stock this part + default_expiry: The default expiry duration for any StockItem instances of this part minimum_stock: Minimum preferred quantity to keep in stock units: Units of measure for this part (default='pcs') salable: Can this part be sold to customers? @@ -722,15 +723,34 @@ class Part(MPTTModel): # Default to None if there are multiple suppliers to choose from return None - default_supplier = models.ForeignKey(SupplierPart, - on_delete=models.SET_NULL, - blank=True, null=True, - help_text=_('Default supplier part'), - related_name='default_parts') + default_supplier = models.ForeignKey( + SupplierPart, + on_delete=models.SET_NULL, + blank=True, null=True, + verbose_name=_('Default Supplier'), + help_text=_('Default supplier part'), + related_name='default_parts' + ) - minimum_stock = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0)], help_text=_('Minimum allowed stock level')) + default_expiry = models.PositiveIntegerField( + default=0, + validators=[MinValueValidator(0)], + verbose_name=_('Default Expiry'), + help_text=_('Expiry time (in days) for stock items of this part'), + ) - units = models.CharField(max_length=20, default="", blank=True, null=True, help_text=_('Stock keeping units for this part')) + minimum_stock = models.PositiveIntegerField( + default=0, validators=[MinValueValidator(0)], + verbose_name=_('Minimum Stock'), + help_text=_('Minimum allowed stock level') + ) + + units = models.CharField( + max_length=20, default="", + blank=True, null=True, + verbose_name=_('Units'), + help_text=_('Stock keeping units for this part') + ) assembly = models.BooleanField( default=False, diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 9711c9fbc8..f723193abb 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -109,6 +109,13 @@ {{ part.minimum_stock }} {% endif %} + {% if part.default_expiry > 0 %} + + + {% trans "Stock Expiry Time" %} + {{ part.default_expiry }} {% trans "days" %} + + {% endif %} {% trans "Creation Date" %} From 7d7d5d24cc75e5b441ddb803d1d7823dc6976cc4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 4 Jan 2021 23:40:51 +1100 Subject: [PATCH 15/40] Pre-fill stockitem expiry date in CreateStockItem form --- InvenTree/stock/forms.py | 5 +++++ InvenTree/stock/views.py | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 4ed7f73f49..5583411009 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -109,6 +109,10 @@ class ConvertStockItemForm(HelperForm): class CreateStockItemForm(HelperForm): """ Form for creating a new StockItem """ + expiry_date = DatePickerFormField( + help_text=('Expiration date for this stock item'), + ) + serial_numbers = forms.CharField(label=_('Serial numbers'), required=False, help_text=_('Enter unique serial numbers (or leave blank)')) def __init__(self, *args, **kwargs): @@ -130,6 +134,7 @@ class CreateStockItemForm(HelperForm): 'batch', 'serial_numbers', 'purchase_price', + 'expiry_date', 'link', 'delete_on_deplete', 'status', diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 582c1b5d89..963d357e31 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -26,7 +26,7 @@ from InvenTree.helpers import str2bool, DownloadFile, GetExportFormats from InvenTree.helpers import extract_serial_numbers from decimal import Decimal, InvalidOperation -from datetime import datetime +from datetime import datetime, timedelta from company.models import Company, SupplierPart from part.models import Part @@ -1596,6 +1596,11 @@ class StockItemCreate(AjaxCreateView): initials['location'] = part.get_default_location() initials['supplier_part'] = part.default_supplier + # If the part has a defined expiry period, extrapolate! + if part.default_expiry > 0: + expiry_date = datetime.now().date() + timedelta(days=part.default_expiry) + initials['expiry_date'] = expiry_date + currency_code = common.settings.currency_code_default() # SupplierPart field has been specified From da02ab3eac5e932b250b2a26f12af8cb4fe6965a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 5 Jan 2021 00:01:44 +1100 Subject: [PATCH 16/40] Add unit testing for view --- InvenTree/part/fixtures/part.yaml | 1 + InvenTree/stock/test_views.py | 43 +++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/InvenTree/part/fixtures/part.yaml b/InvenTree/part/fixtures/part.yaml index 7d6c09707b..508a1577bb 100644 --- a/InvenTree/part/fixtures/part.yaml +++ b/InvenTree/part/fixtures/part.yaml @@ -74,6 +74,7 @@ level: 0 lft: 0 rght: 0 + default_expiry: 10 - model: part.part pk: 50 diff --git a/InvenTree/stock/test_views.py b/InvenTree/stock/test_views.py index 1f55d74eec..021ca9e138 100644 --- a/InvenTree/stock/test_views.py +++ b/InvenTree/stock/test_views.py @@ -6,6 +6,7 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import Group import json +from datetime import datetime, timedelta class StockViewTestCase(TestCase): @@ -135,21 +136,53 @@ class StockItemTest(StockViewTestCase): self.assertEqual(response.status_code, 200) def test_create_item(self): - # Test creation of StockItem - response = self.client.get(reverse('stock-item-create'), {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + """ + Test creation of StockItem + """ + + url = reverse('stock-item-create') + + response = self.client.get(url, {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) - response = self.client.get(reverse('stock-item-create'), {'part': 999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + response = self.client.get(url, {'part': 999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) # Copy from a valid item, valid location - response = self.client.get(reverse('stock-item-create'), {'location': 1, 'copy': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + response = self.client.get(url, {'location': 1, 'copy': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) # Copy from an invalid item, invalid location - response = self.client.get(reverse('stock-item-create'), {'location': 999, 'copy': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + response = self.client.get(url, {'location': 999, 'copy': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) + def test_create_stock_with_expiry(self): + """ + Test creation of stock item of a part with an expiry date. + The initial value for the "expiry_date" field should be pre-filled, + and should be in the future! + """ + + url = reverse('stock-item-create') + + response = self.client.get(url, {'part': 25}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + + self.assertEqual(response.status_code, 200) + + # We are expecting 10 days in the future + expiry = datetime.now().date() + timedelta(10) + + expected = f'name=\\\\"expiry_date\\\\" value=\\\\"{expiry.isoformat()}\\\\"' + + self.assertIn(expected, str(response.content)) + + # Now check with a part which does *not* have a default expiry period + response = self.client.get(url, {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + + expected = 'name=\\\\"expiry_date\\\\" placeholder=\\\\"\\\\"' + + self.assertIn(expected, str(response.content)) + def test_serialize_item(self): # Test the serialization view From 9dc9c0fcb74891118b078c476a689944863771b9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 5 Jan 2021 00:21:26 +1100 Subject: [PATCH 17/40] Auto-populate expiry date for stockitem when created via the API (Now with unit testing!) --- InvenTree/stock/api.py | 10 ++++ .../stock/templates/stock/item_base.html | 3 +- InvenTree/stock/test_api.py | 50 +++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 571ec1dd6f..97f5b5c42e 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -35,6 +35,8 @@ from InvenTree.api import AttachmentMixin from decimal import Decimal, InvalidOperation +from datetime import datetime, timedelta + from rest_framework.serializers import ValidationError from rest_framework.views import APIView from rest_framework.response import Response @@ -342,10 +344,18 @@ class StockList(generics.ListCreateAPIView): # A location was *not* specified - try to infer it if 'location' not in request.data: location = item.part.get_default_location() + if location is not None: item.location = location item.save() + # An expiry date was *not* specified - try to infer it! + if 'expiry_date' not in request.data: + + if item.part.default_expiry > 0: + item.expiry_date = datetime.now().date() + timedelta(days=item.part.default_expiry) + item.save() + # Return a response headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 4d773b047d..b8c143fb4f 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -70,9 +70,10 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% block page_data %}

{% trans "Stock Item" %} - {% stock_status_label item.status large=True %} {% if item.is_expired %} {% trans "Expired" %} + {% else %} + {% stock_status_label item.status large=True %} {% endif %}


diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py index ecca10ba0a..520110469c 100644 --- a/InvenTree/stock/test_api.py +++ b/InvenTree/stock/test_api.py @@ -368,6 +368,56 @@ class StockItemTest(StockAPITestCase): self.assertEqual(response.status_code, status.HTTP_201_CREATED) + def test_default_expiry(self): + """ + Test that the "default_expiry" functionality works via the API. + + - If an expiry_date is specified, use that + - Otherwise, check if the referenced part has a default_expiry defined + - If so, use that! + - Otherwise, no expiry + + Notes: + - Part <25> has a default_expiry of 10 days + + """ + + # First test - create a new StockItem without an expiry date + data = { + 'part': 4, + 'quantity': 10, + } + + response = self.client.post(self.list_url, data) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + self.assertIsNone(response.data['expiry_date']) + + # Second test - create a new StockItem with an explicit expiry date + data['expiry_date'] = '2022-12-12' + + response = self.client.post(self.list_url, data) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + self.assertIsNotNone(response.data['expiry_date']) + self.assertEqual(response.data['expiry_date'], '2022-12-12') + + # Third test - create a new StockItem for a Part which has a default expiry time + data = { + 'part': 25, + 'quantity': 10 + } + + response = self.client.post(self.list_url, data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # Expected expiry date is 10 days in the future + expiry = datetime.now().date() + timedelta(10) + + self.assertEqual(response.data['expiry_date'], expiry.isoformat()) + class StocktakeTest(StockAPITestCase): """ From 9a30108b75c6e57a2ee8af2b1444b575ed719e22 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 5 Jan 2021 00:37:42 +1100 Subject: [PATCH 18/40] Auto-update the expiry date in the StockItem form when switching Part selection --- .../static/script/inventree/modals.js | 7 ++++++- InvenTree/part/serializers.py | 1 + InvenTree/templates/js/stock.js | 19 +++++++++++++++---- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/modals.js b/InvenTree/InvenTree/static/script/inventree/modals.js index f731a5238b..12a496c481 100644 --- a/InvenTree/InvenTree/static/script/inventree/modals.js +++ b/InvenTree/InvenTree/static/script/inventree/modals.js @@ -151,12 +151,17 @@ function enableField(fieldName, enabled, options={}) { } function clearField(fieldName, options={}) { + + setFieldValue(fieldName, '', options); +} + +function setFieldValue(fieldName, value, options={}) { var modal = options.modal || '#modal-form'; var field = getFieldByName(modal, fieldName); - field.val(""); + field.val(value); } diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 0eebe6617d..05fc3091f7 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -289,6 +289,7 @@ class PartSerializer(InvenTreeModelSerializer): 'component', 'description', 'default_location', + 'default_expiry', 'full_name', 'image', 'in_stock', diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index af0c85fc02..ef3ddef33c 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -618,8 +618,8 @@ function loadStockTable(table, options) { if (action == 'move') { secondary.push({ field: 'destination', - label: 'New Location', - title: 'Create new location', + label: '{% trans "New Location" %}', + title: '{% trans "Create new location" %}', url: "/stock/location/new/", }); } @@ -837,14 +837,25 @@ function createNewStockItem(options) { } ); - // Disable serial number field if the part is not trackable + // Request part information from the server inventreeGet( `/api/part/${value}/`, {}, { success: function(response) { - + + // Disable serial number field if the part is not trackable enableField('serial_numbers', response.trackable); clearField('serial_numbers'); + + // Populate the expiry date + if (response.default_expiry <= 0) { + // No expiry date + clearField('expiry_date'); + } else { + var expiry = moment().add(response.default_expiry, 'days'); + + setFieldValue('expiry_date', expiry.format("YYYY-MM-DD")); + } } } ); From 213d6550d3d81f011e5f16f13da9986ab0774858 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 5 Jan 2021 00:54:05 +1100 Subject: [PATCH 19/40] Add new setock settings --- InvenTree/common/models.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index fa1d4dc5c9..ab1e7cf049 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -139,6 +139,20 @@ class InvenTreeSetting(models.Model): 'validator': bool, }, + 'STOCK_ALLOW_EXPIRED_SALE': { + 'name': _('Sell Expired Stock'), + 'description': _('Allow sale of expired stock'), + 'default': False, + 'validator': bool, + }, + + 'STOCK_ALLOW_EXPIRED_BUILD': { + 'name': _('Build Expired Stock'), + 'description': _('Allow building with expired stock'), + 'default': False, + 'validator': bool, + }, + 'BUILDORDER_REFERENCE_PREFIX': { 'name': _('Build Order Reference Prefix'), 'description': _('Prefix value for build order reference'), @@ -479,6 +493,19 @@ class InvenTreeSetting(models.Model): return InvenTree.helpers.str2bool(self.value) + def is_int(self): + """ + Check if the setting is required to be an integer value: + + - int / 'int' = any integer value + - 'pos' / 'positive' = any positive integer value (including zero) + - 'neg' / 'negative' = any negative integer value (including zero) + """ + + valiator = InvenTreeSetting.get_setting_validator(self.key) + + return validator in [int, 'int', 'pos', 'positive', 'neg', 'negative'] + class PriceBreak(models.Model): """ From 855098e30bcbe1330372cf025663cfd448b1dfa0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 5 Jan 2021 00:58:48 +1100 Subject: [PATCH 20/40] Merge conflicting migration files --- .../part/migrations/0062_merge_20210105_0056.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 InvenTree/part/migrations/0062_merge_20210105_0056.py diff --git a/InvenTree/part/migrations/0062_merge_20210105_0056.py b/InvenTree/part/migrations/0062_merge_20210105_0056.py new file mode 100644 index 0000000000..4a8f4378f4 --- /dev/null +++ b/InvenTree/part/migrations/0062_merge_20210105_0056.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.7 on 2021-01-04 13:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0061_auto_20210104_2331'), + ('part', '0061_auto_20210103_2313'), + ] + + operations = [ + ] From 1335c85de1e31a1e62a866fde1b77723ab574edf Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 5 Jan 2021 00:59:10 +1100 Subject: [PATCH 21/40] Edit new stock settings on settings page --- InvenTree/common/models.py | 4 ++++ InvenTree/templates/InvenTree/settings/stock.html | 12 +++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 6183d6cc78..ac1c0e70b3 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -156,6 +156,10 @@ class InvenTreeSetting(models.Model): 'PART_VIRTUAL': { 'name': _('Virtual'), 'description': _('Parts are virtual by default'), + 'default': False, + 'validator': bool, + }, + 'STOCK_ALLOW_EXPIRED_SALE': { 'name': _('Sell Expired Stock'), 'description': _('Allow sale of expired stock'), diff --git a/InvenTree/templates/InvenTree/settings/stock.html b/InvenTree/templates/InvenTree/settings/stock.html index 7bec8b566c..dda5f2c940 100644 --- a/InvenTree/templates/InvenTree/settings/stock.html +++ b/InvenTree/templates/InvenTree/settings/stock.html @@ -10,7 +10,13 @@ {% endblock %} {% block settings %} -
- No Stock settings available -
+

{% trans "Stock Options" %}

+ + + {% include "InvenTree/settings/header.html" %} + + {% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_SALE" %} + {% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_BUILD" %} + +
{% endblock %} \ No newline at end of file From d0fb69e67d4526d865bb1dbe6384ac26bb001afb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 5 Jan 2021 08:50:07 +1100 Subject: [PATCH 22/40] Add option to enable / disable stock expiry feature - Simply hides fields in form views --- InvenTree/common/models.py | 15 +++++++++++++-- InvenTree/common/settings.py | 8 ++++++++ InvenTree/part/views.py | 10 ++++++++++ InvenTree/stock/views.py | 8 ++++++++ InvenTree/templates/InvenTree/settings/stock.html | 1 + 5 files changed, 40 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index ac1c0e70b3..69c499f3b8 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -159,7 +159,14 @@ class InvenTreeSetting(models.Model): 'default': False, 'validator': bool, }, - + + 'STOCK_ENABLE_EXPIRY': { + 'name': _('Stock Expiry'), + 'description': _('Enable stock expiry functionality'), + 'default': False, + 'validator': bool, + }, + 'STOCK_ALLOW_EXPIRED_SALE': { 'name': _('Sell Expired Stock'), 'description': _('Allow sale of expired stock'), @@ -373,6 +380,10 @@ class InvenTreeSetting(models.Model): if setting.is_bool(): value = InvenTree.helpers.str2bool(value) + if setting.is_int(): + # TODO - Coerce to an integer value + pass + else: value = backup_value @@ -523,7 +534,7 @@ class InvenTreeSetting(models.Model): - 'neg' / 'negative' = any negative integer value (including zero) """ - valiator = InvenTreeSetting.get_setting_validator(self.key) + validator = InvenTreeSetting.get_setting_validator(self.key) return validator in [int, 'int', 'pos', 'positive', 'neg', 'negative'] diff --git a/InvenTree/common/settings.py b/InvenTree/common/settings.py index 832a07f040..134d3f3f7c 100644 --- a/InvenTree/common/settings.py +++ b/InvenTree/common/settings.py @@ -21,3 +21,11 @@ def currency_code_default(): code = 'USD' return code + + +def stock_expiry_enabled(): + """ + Returns True if the stock expiry feature is enabled + """ + + return InvenTreeSetting.get_setting('STOCK_ENABLE_EXPIRY') diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index ac4925685f..1d76860ac1 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -35,6 +35,8 @@ from .models import PartSellPriceBreak from common.models import InvenTreeSetting from company.models import SupplierPart +import common.settings as inventree_settings + from . import forms as part_forms from .bom import MakeBomTemplate, BomUploadManager, ExportBom, IsValidBOMFormat @@ -626,6 +628,10 @@ class PartCreate(AjaxCreateView): """ form = super(AjaxCreateView, self).get_form() + # Hide the "default expiry" field if the feature is not enabled + if not inventree_settings.stock_expiry_enabled(): + form.fields.pop('default_expiry') + # Hide the default_supplier field (there are no matching supplier parts yet!) form.fields['default_supplier'].widget = HiddenInput() @@ -918,6 +924,10 @@ class PartEdit(AjaxUpdateView): form = super(AjaxUpdateView, self).get_form() + # Hide the "default expiry" field if the feature is not enabled + if not inventree_settings.stock_expiry_enabled(): + form.fields.pop('default_expiry') + part = self.get_object() form.fields['default_supplier'].queryset = SupplierPart.objects.filter(part=part) diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 963d357e31..ab6f64fb44 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1302,6 +1302,10 @@ class StockItemEdit(AjaxUpdateView): form = super(AjaxUpdateView, self).get_form() + # Hide the "expiry date" field if the feature is not enabled + if not common.settings.stock_expiry_enabled(): + form.fields.pop('expiry_date') + item = self.get_object() # If the part cannot be purchased, hide the supplier_part field @@ -1513,6 +1517,10 @@ class StockItemCreate(AjaxCreateView): form = super().get_form() + # Hide the "expiry date" field if the feature is not enabled + if not common.settings.stock_expiry_enabled(): + form.fields.pop('expiry_date') + part = self.get_part(form=form) if part is not None: diff --git a/InvenTree/templates/InvenTree/settings/stock.html b/InvenTree/templates/InvenTree/settings/stock.html index dda5f2c940..051a251c24 100644 --- a/InvenTree/templates/InvenTree/settings/stock.html +++ b/InvenTree/templates/InvenTree/settings/stock.html @@ -15,6 +15,7 @@ {% include "InvenTree/settings/header.html" %} + {% include "InvenTree/settings/setting.html" with key="STOCK_ENABLE_EXPIRY" %} {% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_SALE" %} {% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_BUILD" %} From 9b086560cbf2b5033b7e9529e00810a5032185a6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 6 Jan 2021 12:09:48 +1100 Subject: [PATCH 23/40] Hide "expiry_date" column in Stock table if feature not enabled --- InvenTree/templates/js/stock.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index ef3ddef33c..d32f52f92b 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -1,4 +1,5 @@ {% load i18n %} +{% load inventree_extras %} {% load status_codes %} /* Stock API functions @@ -587,11 +588,14 @@ function loadStockTable(table, options) { return locationDetail(row); } }, + {% settings_value "STOCK_ENABLE_EXPIRY" as expiry %} + {% if expiry %} { field: 'expiry_date', title: '{% trans "Expiry Date" %}', sortable: true, }, + {% endif %} { field: 'notes', title: '{% trans "Notes" %}', From 580e7599a0b414d2f6dfee51846381eabfe558ba Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 6 Jan 2021 20:22:56 +1100 Subject: [PATCH 24/40] Prevent expired stock from being added to a sales order --- InvenTree/order/views.py | 9 ++++++++- InvenTree/templates/InvenTree/index.html | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index d5658909bb..bd758e39fb 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -24,6 +24,8 @@ from company.models import Company, SupplierPart from stock.models import StockItem, StockLocation from part.models import Part +from common.models import InvenTreeSetting + from . import forms as order_forms from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView @@ -1359,7 +1361,8 @@ class SalesOrderAllocationCreate(AjaxCreateView): try: line = SalesOrderLineItem.objects.get(pk=line_id) - queryset = form.fields['item'].queryset + # Construct a queryset for allowable stock items + queryset = StockItem.objects.filter(StockItem.IN_STOCK_FILTER) # Ensure the part reference matches queryset = queryset.filter(part=line.part) @@ -1369,6 +1372,10 @@ class SalesOrderAllocationCreate(AjaxCreateView): queryset = queryset.exclude(pk__in=allocated) + # Exclude stock items which have expired + if not InvenTreeSetting.get_setting('STOCK_ALLOW_EXPIRED_SALE'): + queryset = queryset.exclude(StockItem.EXPIRED_FILTER) + form.fields['item'].queryset = queryset # Hide the 'line' field diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index d5b2c87148..9fcb818c7a 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load i18n %} +{% load inventree_extras %} {% block page_title %} InvenTree | {% trans "Index" %} {% endblock %} @@ -22,7 +23,10 @@ InvenTree | {% trans "Index" %}
{% if roles.stock.view %} {% include "InvenTree/low_stock.html" with collapse_id="order" %} + {% settings_value "STOCK_ENABLE_EXPIRY" as expiry %} + {% if expiry %} {% include "InvenTree/expired_stock.html" with collapse_id="expired" %} + {% endif %} {% include "InvenTree/required_stock_build.html" with collapse_id="stock_to_build" %} {% endif %}
From 33d6396a4eeba699c3b81b5b1ad43be847147b91 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 6 Jan 2021 21:00:45 +1100 Subject: [PATCH 25/40] Exclude expired stock from builds --- InvenTree/build/models.py | 6 ++++++ InvenTree/build/views.py | 1 + InvenTree/stock/test_views.py | 8 ++++++++ 3 files changed, 15 insertions(+) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 488a8b79e8..96101adafe 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -27,6 +27,8 @@ from InvenTree.helpers import increment, getSetting, normalize from InvenTree.validators import validate_build_order_reference from InvenTree.models import InvenTreeAttachment +from common.models import InvenTreeSetting + import InvenTree.fields from stock import models as StockModels @@ -819,6 +821,10 @@ class Build(MPTTModel): location__in=[loc for loc in self.take_from.getUniqueChildren()] ) + # Exclude expired stock items + if not InvenTreeSetting.get_setting('STOCK_ALLOW_EXPIRED_BUILD'): + items = items.exclude(StockModels.StockItem.EXPIRED_FILTER) + return items @property diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 69d1dd9415..0887c49397 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -903,6 +903,7 @@ class BuildItemCreate(AjaxCreateView): if self.build and self.part: available_items = self.build.availableStockItems(self.part, self.output) + form.fields['stock_item'].queryset = available_items self.available_stock = form.fields['stock_item'].queryset.all() diff --git a/InvenTree/stock/test_views.py b/InvenTree/stock/test_views.py index 021ca9e138..b842de3836 100644 --- a/InvenTree/stock/test_views.py +++ b/InvenTree/stock/test_views.py @@ -5,6 +5,8 @@ from django.urls import reverse from django.contrib.auth import get_user_model from django.contrib.auth.models import Group +from common.models import InvenTreeSetting + import json from datetime import datetime, timedelta @@ -32,6 +34,9 @@ class StockViewTestCase(TestCase): password='password' ) + self.user.is_staff = True + self.user.save() + # Put the user into a group with the correct permissions group = Group.objects.create(name='mygroup') self.user.groups.add(group) @@ -163,6 +168,9 @@ class StockItemTest(StockViewTestCase): and should be in the future! """ + # First, ensure that the expiry date feature is enabled! + InvenTreeSetting.set_setting('STOCK_ENABLE_EXPIRY', True, self.user) + url = reverse('stock-item-create') response = self.client.get(url, {'part': 25}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') From ba915da22b8013f9bdb548a4d15dfbc8ed5b5def Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 6 Jan 2021 22:20:54 +1100 Subject: [PATCH 26/40] Filter StockItem API by staleness --- InvenTree/build/models.py | 4 +- InvenTree/common/models.py | 68 ++++++++++++++----- InvenTree/stock/api.py | 40 ++++++++--- .../templates/InvenTree/settings/setting.html | 2 +- .../templates/InvenTree/settings/stock.html | 1 + 5 files changed, 86 insertions(+), 29 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 96101adafe..c3b399d820 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -27,7 +27,7 @@ from InvenTree.helpers import increment, getSetting, normalize from InvenTree.validators import validate_build_order_reference from InvenTree.models import InvenTreeAttachment -from common.models import InvenTreeSetting +import common.models import InvenTree.fields @@ -822,7 +822,7 @@ class Build(MPTTModel): ) # Exclude expired stock items - if not InvenTreeSetting.get_setting('STOCK_ALLOW_EXPIRED_BUILD'): + if not common.models.InvenTreeSetting.get_setting('STOCK_ALLOW_EXPIRED_BUILD'): items = items.exclude(StockModels.StockItem.EXPIRED_FILTER) return items diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 69c499f3b8..5651a401de 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -174,6 +174,14 @@ class InvenTreeSetting(models.Model): 'validator': bool, }, + 'STOCK_STALE_DAYS': { + 'name': _('Stock Stale Time'), + 'description': _('Number of days stock items are considered stale before expiring'), + 'default': 0, + 'units': _('days'), + 'validator': [int], + }, + 'STOCK_ALLOW_EXPIRED_BUILD': { 'name': _('Build Expired Stock'), 'description': _('Allow building with expired stock'), @@ -381,8 +389,10 @@ class InvenTreeSetting(models.Model): value = InvenTree.helpers.str2bool(value) if setting.is_int(): - # TODO - Coerce to an integer value - pass + try: + value = int(value) + except (ValueError, TypeError): + value = backup_value else: value = backup_value @@ -472,18 +482,26 @@ class InvenTreeSetting(models.Model): return - # Check if a 'type' has been specified for this value - if type(validator) == type: + # Boolean validator + if validator == bool: + # Value must "look like" a boolean value + if InvenTree.helpers.is_bool(self.value): + # Coerce into either "True" or "False" + self.value = str(InvenTree.helpers.str2bool(self.value)) + else: + raise ValidationError({ + 'value': _('Value must be a boolean value') + }) - if validator == bool: - # Value must "look like" a boolean value - if InvenTree.helpers.is_bool(self.value): - # Coerce into either "True" or "False" - self.value = str(InvenTree.helpers.str2bool(self.value)) - else: - raise ValidationError({ - 'value': _('Value must be a boolean value') - }) + # Integer validator + if validator == int: + try: + # Coerce into an integer value + self.value = str(int(self.value)) + except (ValueError, TypeError): + raise ValidationError({ + 'value': _('Value must be an integer value'), + }) def validate_unique(self, exclude=None): """ Ensure that the key:value pair is unique. @@ -528,15 +546,29 @@ class InvenTreeSetting(models.Model): def is_int(self): """ Check if the setting is required to be an integer value: - - - int / 'int' = any integer value - - 'pos' / 'positive' = any positive integer value (including zero) - - 'neg' / 'negative' = any negative integer value (including zero) """ validator = InvenTreeSetting.get_setting_validator(self.key) - return validator in [int, 'int', 'pos', 'positive', 'neg', 'negative'] + if validator == int: + return True + + if type(validator) in [list, tuple]: + for v in validator: + if v == int: + return True + + def as_int(self): + """ + Return the value of this setting converted to a boolean value. + + If an error occurs, return the default value + """ + + try: + value = int() + except (ValueError, TypeError): + return self.default_value() class PriceBreak(models.Model): diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 97f5b5c42e..9f0a4278f5 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -23,6 +23,9 @@ from part.serializers import PartBriefSerializer from company.models import SupplierPart from company.serializers import SupplierPartSerializer +import common.settings +import common.models + from .serializers import StockItemSerializer from .serializers import LocationSerializer, LocationBriefSerializer from .serializers import StockTrackingSerializer @@ -535,16 +538,37 @@ class StockList(generics.ListCreateAPIView): # Exclude items which are instaled in another item queryset = queryset.filter(belongs_to=None) - # Filter by 'expired' status - expired = params.get('expired', None) + if common.settings.stock_expiry_enabled(): - if expired is not None: - expired = str2bool(expired) + # Filter by 'expired' status + expired = params.get('expired', None) - if expired: - queryset = queryset.filter(StockItem.EXPIRED_FILTER) - else: - queryset = queryset.exclude(StockItem.EXPIRED_FILTER) + if expired is not None: + expired = str2bool(expired) + + if expired: + queryset = queryset.filter(StockItem.EXPIRED_FILTER) + else: + queryset = queryset.exclude(StockItem.EXPIRED_FILTER) + + # Filter by 'stale' status + stale = params.get('stale', None) + + if stale is not None: + stale = str2bool(stale) + + # How many days to account for "staleness"? + stale_days = common.models.InvenTreeSetting.get_setting('STOCK_STALE_DAYS') + + if stale_days > 0: + stale_date = datetime.now().date() + timedelta(days=stale_days) + + stale_filter = StockItem.IN_STOCK_FILTER & ~Q(expiry_date=None) & Q(expiry_date__lt=stale_date) + + if stale: + queryset = queryset.filter(stale_filter) + else: + queryset = queryset.exclude(stale_filter) # Filter by customer customer = params.get('customer', None) diff --git a/InvenTree/templates/InvenTree/settings/setting.html b/InvenTree/templates/InvenTree/settings/setting.html index b08f4aeb3a..b7932fc30a 100644 --- a/InvenTree/templates/InvenTree/settings/setting.html +++ b/InvenTree/templates/InvenTree/settings/setting.html @@ -17,7 +17,7 @@ {% else %} {% if setting.value %} - {{ setting.value }}{{ setting.units }} + {{ setting.value }} {{ setting.units }} {% else %} {% trans "No value set" %} diff --git a/InvenTree/templates/InvenTree/settings/stock.html b/InvenTree/templates/InvenTree/settings/stock.html index 051a251c24..5ad308decc 100644 --- a/InvenTree/templates/InvenTree/settings/stock.html +++ b/InvenTree/templates/InvenTree/settings/stock.html @@ -16,6 +16,7 @@ {% include "InvenTree/settings/header.html" %}
{% include "InvenTree/settings/setting.html" with key="STOCK_ENABLE_EXPIRY" %} + {% include "InvenTree/settings/setting.html" with key="STOCK_STALE_DAYS" %} {% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_SALE" %} {% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_BUILD" %} From e62873a65028c0b051b055f86fc4530254d240dd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 6 Jan 2021 22:21:18 +1100 Subject: [PATCH 27/40] Display "stale" status on StockItem info page --- InvenTree/stock/models.py | 35 +++++++++++++++++-- .../stock/templates/stock/item_base.html | 7 +++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index d2d6ec51ee..533aca7596 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -27,9 +27,11 @@ from mptt.models import MPTTModel, TreeForeignKey from djmoney.models.fields import MoneyField from decimal import Decimal, InvalidOperation -from datetime import datetime +from datetime import datetime, timedelta from InvenTree import helpers +import common.models + from InvenTree.status_codes import StockStatus from InvenTree.models import InvenTreeTree, InvenTreeAttachment from InvenTree.fields import InvenTreeURLField @@ -471,9 +473,38 @@ class StockItem(MPTTModel): help_text=_('Single unit purchase price at time of purchase'), ) + def is_stale(self): + """ + Returns True if this Stock item is "stale". + + To be "stale", the following conditions must be met: + + - Expiry date is not None + - Expiry date will "expire" within the configured stale date + - The StockItem is otherwise "in stock" + """ + + if self.expiry_date is None: + return False + + if not self.in_stock: + return False + + today = datetime.now().date() + + stale_days = common.models.InvenTreeSetting.get_setting('STOCK_STALE_DAYS') + + if stale_days <= 0: + return False + + expiry_date = today + timedelta(days=stale_days) + + return self.expiry_date < expiry_date + + def is_expired(self): """ - Returns true if this StockItem is "expired" + Returns True if this StockItem is "expired". To be "expired", the following conditions must be met: diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index b8c143fb4f..cbb6eeafb2 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -74,6 +74,9 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% trans "Expired" %} {% else %} {% stock_status_label item.status large=True %} + {% if item.is_stale %} + {% trans "Stale" %} + {% endif %} {% endif %}
@@ -304,7 +307,9 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
From a5b18640afc95218afc53802eca2eb726a5ac5fc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 6 Jan 2021 22:30:12 +1100 Subject: [PATCH 28/40] Display stale stock on index page --- .../templates/InvenTree/expired_stock.html | 2 +- InvenTree/templates/InvenTree/index.html | 84 +++++-------------- .../templates/InvenTree/index/on_load.html | 5 ++ .../templates/InvenTree/stale_stock.html | 15 ++++ 4 files changed, 42 insertions(+), 64 deletions(-) create mode 100644 InvenTree/templates/InvenTree/index/on_load.html create mode 100644 InvenTree/templates/InvenTree/stale_stock.html diff --git a/InvenTree/templates/InvenTree/expired_stock.html b/InvenTree/templates/InvenTree/expired_stock.html index 7ecf117fa7..20e2591c16 100644 --- a/InvenTree/templates/InvenTree/expired_stock.html +++ b/InvenTree/templates/InvenTree/expired_stock.html @@ -3,7 +3,7 @@ {% load i18n %} {% block collapse_title %} - + {% trans "Expired Stock" %} {% endblock %} diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index 9fcb818c7a..d1e10cb00e 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -26,6 +26,7 @@ InvenTree | {% trans "Index" %} {% settings_value "STOCK_ENABLE_EXPIRY" as expiry %} {% if expiry %} {% include "InvenTree/expired_stock.html" with collapse_id="expired" %} + {% include "InvenTree/stale_stock.html" with collapse_id="stale" %} {% endif %} {% include "InvenTree/required_stock_build.html" with collapse_id="stock_to_build" %} {% endif %} @@ -98,6 +99,14 @@ loadStockTable($("#expired-stock-table"), { }, }); +loadStockTable($("#stale-stock-table"), { + params: { + stale: true, + location_detail: true, + part_detail: true, + }, +}); + loadSimplePartTable("#low-stock-table", "{% url 'api-part-list' %}", { params: { low_stock: true, @@ -136,70 +145,19 @@ loadSalesOrderTable("#so-overdue-table", { } }); -$("#latest-parts-table").on('load-success.bs.table', function() { - var count = $("#latest-parts-table").bootstrapTable('getData').length; +{% include "InvenTree/index/on_load.html" with label="latest-parts" %} +{% include "InvenTree/index/on_load.html" with label="starred-parts" %} +{% include "InvenTree/index/on_load.html" with label="bom-invalid" %} +{% include "InvenTree/index/on_load.html" with label="build-pending" %} +{% include "InvenTree/index/on_load.html" with label="build-overdue" %} - $("#latest-parts-count").html(count); -}); +{% include "InvenTree/index/on_load.html" with label="expired-stock" %} +{% include "InvenTree/index/on_load.html" with label="stale-stock" %} +{% include "InvenTree/index/on_load.html" with label="low-stock" %} +{% include "InvenTree/index/on_load.html" with label="stock-to-build" %} -$("#starred-parts-table").on('load-success.bs.table', function() { - var count = $("#starred-parts-table").bootstrapTable('getData').length; - - $("#starred-parts-count").html(count); -}); - -$("#bom-invalid-table").on('load-success.bs.table', function() { - var count = $("#bom-invalid-table").bootstrapTable('getData').length; - - $("#bom-invalid-count").html(count); -}); - -$("#build-pending-table").on('load-success.bs.table', function() { - var count = $("#build-pending-table").bootstrapTable('getData').length; - - $("#build-pending-count").html(count); -}); - -$("#build-overdue-table").on('load-success.bs.table', function() { - var count = $("#build-overdue-table").bootstrapTable('getData').length; - - $("#build-overdue-count").html(count); -}); - -$("#expired-stock-table").on('load-success.bs.table', function() { - var count = $("#expired-stock-table").bootstrapTable('getData').length; - - $("#expired-stock-count").html(count); -}); - -$("#low-stock-table").on('load-success.bs.table', function() { - var count = $("#low-stock-table").bootstrapTable('getData').length; - - $("#low-stock-count").html(count); -}); - -$("#stock-to-build-table").on('load-success.bs.table', function() { - var count = $("#stock-to-build-table").bootstrapTable('getData').length; - - $("#stock-to-build-count").html(count); -}); - -$("#po-outstanding-table").on('load-success.bs.table', function() { - var count = $("#po-outstanding-table").bootstrapTable('getData').length; - - $("#po-outstanding-count").html(count); -}); - -$("#so-outstanding-table").on('load-success.bs.table', function() { - var count = $("#so-outstanding-table").bootstrapTable('getData').length; - - $("#so-outstanding-count").html(count); -}); - -$("#so-overdue-table").on('load-success.bs.table', function() { - var count = $("#so-overdue-table").bootstrapTable('getData').length; - - $("#so-overdue-count").html(count); -}); +{% include "InvenTree/index/on_load.html" with label="po-outstanding" %} +{% include "InvenTree/index/on_load.html" with label="so-outstanding" %} +{% include "InvenTree/index/on_load.html" with label="so-overdue" %} {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/index/on_load.html b/InvenTree/templates/InvenTree/index/on_load.html new file mode 100644 index 0000000000..a63479e60d --- /dev/null +++ b/InvenTree/templates/InvenTree/index/on_load.html @@ -0,0 +1,5 @@ +$("#{{ label }}-table").on('load-success.bs.table', function() { + var count = $("#{{ label }}-table").bootstrapTable('getData').length; + + $("#{{ label }}-count").html(count); +}); \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/stale_stock.html b/InvenTree/templates/InvenTree/stale_stock.html new file mode 100644 index 0000000000..3cbb74369c --- /dev/null +++ b/InvenTree/templates/InvenTree/stale_stock.html @@ -0,0 +1,15 @@ +{% extends "collapse_index.html" %} + +{% load i18n %} + +{% block collapse_title %} + +{% trans "Stale Stock" %} +{% endblock %} + +{% block collapse_content %} + +
{{ item.expiry_date }} {% if item.is_expired %} - {% trans "Expired" %} + {% trans "Expired" %} + {% elif item.is_stale %} + {% trans "Stale" %} {% endif %}
+
+ +{% endblock %} \ No newline at end of file From 1d6a049c5a3f0aed0c35cda4661d4a9d98281167 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 6 Jan 2021 23:06:49 +1100 Subject: [PATCH 29/40] Annotate stock queryset with stale status --- InvenTree/stock/serializers.py | 21 +++++++++++++++++++++ InvenTree/templates/InvenTree/index.html | 1 + InvenTree/templates/js/stock.js | 4 +++- InvenTree/templates/js/table_filters.js | 5 +++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 9db1d95dae..70ad1abc18 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -13,11 +13,15 @@ from django.db.models.functions import Coalesce from django.db.models import Case, When, Value from django.db.models import BooleanField +from django.db.models import Q from sql_util.utils import SubquerySum, SubqueryCount from decimal import Decimal +from datetime import datetime, timedelta + +import common.models from company.serializers import SupplierPartSerializer from part.serializers import PartBriefSerializer from InvenTree.serializers import UserSerializerBrief, InvenTreeModelSerializer @@ -119,6 +123,20 @@ class StockItemSerializer(InvenTreeModelSerializer): ) ) + # Add flag to indicate if the StockItem is stale + stale_days = common.models.InvenTreeSetting.get_setting('STOCK_STALE_DAYS') + stale_date = datetime.now().date() + timedelta(days=stale_days) + stale_filter = StockItem.IN_STOCK_FILTER & ~Q(expiry_date=None) & Q(expiry_date__lt=stale_date) + + queryset = queryset.annotate( + stale=Case( + When( + stale_filter, then=Value(True, output_field=BooleanField()), + ), + default=Value(False, output_field=BooleanField()), + ) + ) + return queryset status_text = serializers.CharField(source='get_status_display', read_only=True) @@ -137,6 +155,8 @@ class StockItemSerializer(InvenTreeModelSerializer): expired = serializers.BooleanField(required=False, read_only=True) + stale = serializers.BooleanField(required=False, read_only=True) + serial = serializers.CharField(required=False) required_tests = serializers.IntegerField(source='required_test_count', read_only=True, required=False) @@ -185,6 +205,7 @@ class StockItemSerializer(InvenTreeModelSerializer): 'required_tests', 'sales_order', 'serial', + 'stale', 'status', 'status_text', 'supplier_part', diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index d1e10cb00e..1b9a492a22 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -102,6 +102,7 @@ loadStockTable($("#expired-stock-table"), { loadStockTable($("#stale-stock-table"), { params: { stale: true, + expired: false, location_detail: true, part_detail: true, }, diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index d32f52f92b..e3f124d252 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -534,7 +534,9 @@ function loadStockTable(table, options) { } if (row.expired) { - html += makeIconBadge('fa-stopwatch icon-red', '{% trans "Stock item has expired" %}'); + html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Stock item has expired" %}'); + } else if (row.stale) { + html += makeIconBadge('fa-stopwatch', '{% trans "Stock item will expire soon" %}'); } if (row.allocated) { diff --git a/InvenTree/templates/js/table_filters.js b/InvenTree/templates/js/table_filters.js index 054542a4a3..84aa12c139 100644 --- a/InvenTree/templates/js/table_filters.js +++ b/InvenTree/templates/js/table_filters.js @@ -111,6 +111,11 @@ function getAvailableTableFilters(tableKey) { title: '{% trans "Expired" %}', description: '{% trans "Show stock items which have expired" %}', }, + stale: { + type: 'bool', + title: '{% trans "Stale" %}', + description: '{% trans "Show stock which is close to expiring" %}', + }, in_stock: { type: 'bool', title: '{% trans "In Stock" %}', From e5b346e7fa778294d15a5a30354eb1dbae8fcead Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 6 Jan 2021 23:09:26 +1100 Subject: [PATCH 30/40] PEP fixes --- InvenTree/common/models.py | 6 ++++-- InvenTree/stock/models.py | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 5651a401de..5c04f9a7e9 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -566,10 +566,12 @@ class InvenTreeSetting(models.Model): """ try: - value = int() + value = int(self.value) except (ValueError, TypeError): - return self.default_value() + value = self.default_value() + return value + class PriceBreak(models.Model): """ diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 533aca7596..d399c0daf1 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -501,7 +501,6 @@ class StockItem(MPTTModel): return self.expiry_date < expiry_date - def is_expired(self): """ Returns True if this StockItem is "expired". From a8e3e6c8db7e4b3c783b2a122ac7075b00319dd5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 6 Jan 2021 23:12:52 +1100 Subject: [PATCH 31/40] Update translation --- InvenTree/locale/de/LC_MESSAGES/django.po | 1300 ++++++++++++--------- InvenTree/locale/en/LC_MESSAGES/django.po | 1268 +++++++++++--------- InvenTree/locale/es/LC_MESSAGES/django.po | 1268 +++++++++++--------- 3 files changed, 2138 insertions(+), 1698 deletions(-) diff --git a/InvenTree/locale/de/LC_MESSAGES/django.po b/InvenTree/locale/de/LC_MESSAGES/django.po index 74dfaadcaa..3dd1bc4ac8 100644 --- a/InvenTree/locale/de/LC_MESSAGES/django.po +++ b/InvenTree/locale/de/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-01-03 22:16+1100\n" +"POT-Creation-Date: 2021-01-06 23:11+1100\n" "PO-Revision-Date: 2020-05-03 11:32+0200\n" "Last-Translator: Christian Schlüter \n" "Language-Team: C \n" @@ -62,7 +62,7 @@ msgid "Select Category" msgstr "Teilkategorie auswählen" #: InvenTree/helpers.py:361 order/models.py:178 order/models.py:260 -#: stock/views.py:1647 +#: stock/views.py:1660 msgid "Invalid quantity provided" msgstr "Keine gültige Menge" @@ -105,12 +105,12 @@ msgstr "Datei zum Anhängen auswählen" msgid "File comment" msgstr "Datei-Kommentar" -#: InvenTree/models.py:68 templates/js/stock.js:744 +#: InvenTree/models.py:68 templates/js/stock.js:759 msgid "User" msgstr "Benutzer" -#: InvenTree/models.py:106 part/templates/part/params.html:24 -#: templates/js/part.js:129 +#: InvenTree/models.py:106 part/models.py:647 +#: part/templates/part/params.html:24 templates/js/part.js:129 msgid "Name" msgstr "Name" @@ -331,14 +331,14 @@ msgstr "Bestell-Referenz" msgid "Order target date" msgstr "Kein Ziel gesetzt" -#: build/forms.py:39 build/models.py:173 +#: build/forms.py:39 build/models.py:175 msgid "" "Target date for build completion. Build will be overdue after this date." msgstr "" #: build/forms.py:78 build/templates/build/auto_allocate.html:17 #: build/templates/build/build_base.html:83 -#: build/templates/build/detail.html:29 common/models.py:496 +#: build/templates/build/detail.html:29 common/models.py:589 #: company/forms.py:112 company/templates/company/supplier_part_pricing.html:75 #: order/templates/order/order_wizard/select_parts.html:32 #: order/templates/order/purchase_order_detail.html:179 @@ -346,13 +346,13 @@ msgstr "" #: order/templates/order/sales_order_detail.html:156 #: part/templates/part/allocation.html:16 #: part/templates/part/allocation.html:49 -#: part/templates/part/sale_prices.html:82 stock/forms.py:298 +#: part/templates/part/sale_prices.html:82 stock/forms.py:304 #: stock/templates/stock/item_base.html:40 #: stock/templates/stock/item_base.html:46 -#: stock/templates/stock/item_base.html:197 +#: stock/templates/stock/item_base.html:204 #: stock/templates/stock/stock_adjust.html:18 templates/js/barcode.js:338 -#: templates/js/bom.js:195 templates/js/build.js:420 templates/js/stock.js:735 -#: templates/js/stock.js:963 +#: templates/js/bom.js:195 templates/js/build.js:420 templates/js/stock.js:750 +#: templates/js/stock.js:989 msgid "Quantity" msgstr "Anzahl" @@ -362,7 +362,7 @@ msgstr "Anzahl" msgid "Enter quantity for build output" msgstr "Seriennummer für dieses Teil" -#: build/forms.py:83 stock/forms.py:111 +#: build/forms.py:83 stock/forms.py:116 #, fuzzy #| msgid "Serial Number" msgid "Serial numbers" @@ -428,91 +428,92 @@ msgstr "Bauabbruch bestätigen" msgid "Select quantity of stock to allocate" msgstr "Lagerobjekt für Zuordnung auswählen" -#: build/models.py:59 build/templates/build/build_base.html:8 +#: build/models.py:61 build/templates/build/build_base.html:8 #: build/templates/build/build_base.html:35 #: part/templates/part/allocation.html:20 msgid "Build Order" msgstr "Bauauftrag" -#: build/models.py:60 build/templates/build/index.html:6 +#: build/models.py:62 build/templates/build/index.html:6 #: build/templates/build/index.html:14 order/templates/order/so_builds.html:11 #: order/templates/order/so_tabs.html:9 part/templates/part/tabs.html:31 #: templates/InvenTree/settings/tabs.html:28 users/models.py:30 msgid "Build Orders" msgstr "Bauaufträge" -#: build/models.py:75 +#: build/models.py:77 #, fuzzy #| msgid "Order Reference" msgid "Build Order Reference" msgstr "Bestellreferenz" -#: build/models.py:76 order/templates/order/purchase_order_detail.html:174 +#: build/models.py:78 order/templates/order/purchase_order_detail.html:174 #: templates/js/bom.js:187 templates/js/build.js:509 msgid "Reference" msgstr "Referenz" -#: build/models.py:83 build/templates/build/detail.html:19 +#: build/models.py:85 build/templates/build/detail.html:19 #: company/models.py:359 company/templates/company/detail.html:23 #: company/templates/company/supplier_part_base.html:61 #: company/templates/company/supplier_part_detail.html:27 -#: order/templates/order/purchase_order_detail.html:161 +#: order/templates/order/purchase_order_detail.html:161 part/models.py:671 #: part/templates/part/detail.html:51 part/templates/part/set_category.html:14 -#: templates/InvenTree/search.html:147 templates/js/bom.js:180 +#: templates/InvenTree/search.html:147 +#: templates/InvenTree/settings/header.html:9 templates/js/bom.js:180 #: templates/js/bom.js:517 templates/js/build.js:664 templates/js/company.js:56 #: templates/js/order.js:175 templates/js/order.js:263 templates/js/part.js:188 #: templates/js/part.js:271 templates/js/part.js:391 templates/js/part.js:572 -#: templates/js/stock.js:500 templates/js/stock.js:716 +#: templates/js/stock.js:501 templates/js/stock.js:731 msgid "Description" msgstr "Beschreibung" -#: build/models.py:86 +#: build/models.py:88 msgid "Brief description of the build" msgstr "Kurze Beschreibung des Baus" -#: build/models.py:95 build/templates/build/build_base.html:104 +#: build/models.py:97 build/templates/build/build_base.html:104 #: build/templates/build/detail.html:75 msgid "Parent Build" msgstr "Eltern-Bau" -#: build/models.py:96 +#: build/models.py:98 #, fuzzy #| msgid "SalesOrder to which this build is allocated" msgid "BuildOrder to which this build is allocated" msgstr "Bestellung, die diesem Bau zugwiesen ist" -#: build/models.py:101 build/templates/build/auto_allocate.html:16 +#: build/models.py:103 build/templates/build/auto_allocate.html:16 #: build/templates/build/build_base.html:78 #: build/templates/build/detail.html:24 order/models.py:548 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:148 -#: order/templates/order/receive_parts.html:19 part/models.py:315 +#: order/templates/order/receive_parts.html:19 part/models.py:316 #: part/templates/part/part_app_base.html:7 part/templates/part/related.html:26 #: part/templates/part/set_category.html:13 templates/InvenTree/search.html:133 #: templates/js/barcode.js:336 templates/js/bom.js:153 templates/js/bom.js:502 #: templates/js/build.js:669 templates/js/company.js:138 -#: templates/js/part.js:252 templates/js/part.js:357 templates/js/stock.js:474 -#: templates/js/stock.js:1035 +#: templates/js/part.js:252 templates/js/part.js:357 templates/js/stock.js:475 +#: templates/js/stock.js:1061 msgid "Part" msgstr "Teil" -#: build/models.py:109 +#: build/models.py:111 msgid "Select part to build" msgstr "Teil für den Bau wählen" -#: build/models.py:114 +#: build/models.py:116 msgid "Sales Order Reference" msgstr "Bestellungsreferenz" -#: build/models.py:118 +#: build/models.py:120 msgid "SalesOrder to which this build is allocated" msgstr "Bestellung, die diesem Bau zugwiesen ist" -#: build/models.py:123 +#: build/models.py:125 msgid "Source Location" msgstr "Quell-Standort" -#: build/models.py:127 +#: build/models.py:129 msgid "" "Select location to take stock from for this build (leave blank to take from " "any stock location)" @@ -520,155 +521,155 @@ msgstr "" "Lager-Entnahmestandort für diesen Bau wählen (oder leer lassen für einen " "beliebigen Lager-Standort)" -#: build/models.py:132 +#: build/models.py:134 #, fuzzy #| msgid "Destination stock location" msgid "Destination Location" msgstr "Ziel-Lagerbestand" -#: build/models.py:136 +#: build/models.py:138 msgid "Select location where the completed items will be stored" msgstr "" -#: build/models.py:140 +#: build/models.py:142 msgid "Build Quantity" msgstr "Bau-Anzahl" -#: build/models.py:143 +#: build/models.py:145 #, fuzzy #| msgid "Number of parts to build" msgid "Number of stock items to build" msgstr "Anzahl der zu bauenden Teile" -#: build/models.py:147 +#: build/models.py:149 #, fuzzy #| msgid "Completed" msgid "Completed items" msgstr "Fertig" -#: build/models.py:149 +#: build/models.py:151 #, fuzzy #| msgid "Delete this Stock Item when stock is depleted" msgid "Number of stock items which have been completed" msgstr "Objekt löschen wenn Lagerbestand aufgebraucht" -#: build/models.py:153 part/templates/part/part_base.html:155 +#: build/models.py:155 part/templates/part/part_base.html:155 msgid "Build Status" msgstr "Bau-Status" -#: build/models.py:157 +#: build/models.py:159 msgid "Build status code" msgstr "Bau-Statuscode" -#: build/models.py:161 stock/models.py:390 +#: build/models.py:163 stock/models.py:396 msgid "Batch Code" msgstr "Losnummer" -#: build/models.py:165 +#: build/models.py:167 msgid "Batch code for this build output" msgstr "Chargennummer für diese Bau-Ausgabe" -#: build/models.py:172 order/models.py:329 +#: build/models.py:174 order/models.py:329 msgid "Target completion date" msgstr "" -#: build/models.py:186 build/templates/build/detail.html:89 +#: build/models.py:188 build/templates/build/detail.html:89 #: company/templates/company/supplier_part_base.html:68 #: company/templates/company/supplier_part_detail.html:24 #: part/templates/part/detail.html:80 part/templates/part/part_base.html:102 -#: stock/models.py:384 stock/templates/stock/item_base.html:280 +#: stock/models.py:390 stock/templates/stock/item_base.html:287 msgid "External Link" msgstr "Externer Link" -#: build/models.py:187 part/models.py:672 stock/models.py:386 +#: build/models.py:189 part/models.py:705 stock/models.py:392 msgid "Link to external URL" msgstr "Link zu einer externen URL" -#: build/models.py:191 build/templates/build/tabs.html:23 company/models.py:366 +#: build/models.py:193 build/templates/build/tabs.html:23 company/models.py:366 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:18 #: order/templates/order/purchase_order_detail.html:213 -#: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:73 -#: stock/forms.py:307 stock/forms.py:339 stock/forms.py:367 stock/models.py:448 -#: stock/models.py:1433 stock/templates/stock/tabs.html:26 -#: templates/js/barcode.js:391 templates/js/bom.js:263 -#: templates/js/stock.js:116 templates/js/stock.js:588 +#: order/templates/order/so_tabs.html:23 part/models.py:831 +#: part/templates/part/tabs.html:73 stock/forms.py:313 stock/forms.py:345 +#: stock/forms.py:373 stock/models.py:462 stock/models.py:1476 +#: stock/templates/stock/tabs.html:26 templates/js/barcode.js:391 +#: templates/js/bom.js:263 templates/js/stock.js:117 templates/js/stock.js:603 msgid "Notes" msgstr "Notizen" -#: build/models.py:192 +#: build/models.py:194 msgid "Extra build notes" msgstr "Notizen für den Bau" -#: build/models.py:577 +#: build/models.py:579 #, fuzzy #| msgid "No action specified" msgid "No build output specified" msgstr "Keine Aktion angegeben" -#: build/models.py:580 +#: build/models.py:582 msgid "Build output is already completed" msgstr "" -#: build/models.py:583 +#: build/models.py:585 #, fuzzy #| msgid "Quantity does not match serial numbers" msgid "Build output does not match Build Order" msgstr "Anzahl stimmt nicht mit den Seriennummern überein" -#: build/models.py:658 +#: build/models.py:660 #, fuzzy #| msgid "Complete Build" msgid "Completed build output" msgstr "Bau fertigstellen" -#: build/models.py:896 +#: build/models.py:902 msgid "BuildItem must be unique for build, stock_item and install_into" msgstr "" -#: build/models.py:918 +#: build/models.py:924 #, fuzzy #| msgid "Allocate Stock to Build" msgid "Build item must specify a build output" msgstr "Lagerbestand dem Bau zuweisen" -#: build/models.py:923 +#: build/models.py:929 #, python-brace-format msgid "Selected stock item not found in BOM for part '{p}'" msgstr "Ausgewähltes Lagerobjekt nicht in BOM für Teil '{p}' gefunden" -#: build/models.py:927 +#: build/models.py:933 #, python-brace-format msgid "Allocated quantity ({n}) must not exceed available quantity ({q})" msgstr "" "zugewiesene Anzahl ({n}) darf nicht die verfügbare ({q}) Anzahl überschreiten" -#: build/models.py:934 order/models.py:632 +#: build/models.py:940 order/models.py:632 msgid "StockItem is over-allocated" msgstr "Zu viele Lagerobjekte zugewiesen" -#: build/models.py:938 order/models.py:635 +#: build/models.py:944 order/models.py:635 msgid "Allocation quantity must be greater than zero" msgstr "Anzahl muss größer null sein" -#: build/models.py:942 +#: build/models.py:948 msgid "Quantity must be 1 for serialized stock" msgstr "Anzahl muss 1 für Objekte mit Seriennummer sein" -#: build/models.py:982 +#: build/models.py:988 msgid "Build to allocate parts" msgstr "Bau starten um Teile zuzuweisen" -#: build/models.py:989 +#: build/models.py:995 #, fuzzy #| msgid "Remove stock" msgid "Source stock item" msgstr "Bestand entfernen" -#: build/models.py:1001 +#: build/models.py:1007 msgid "Stock quantity to allocate to build" msgstr "Lagerobjekt-Anzahl dem Bau zuweisen" -#: build/models.py:1009 +#: build/models.py:1015 #, fuzzy #| msgid "Destination stock location" msgid "Destination stock item" @@ -705,7 +706,7 @@ msgid "Order required parts" msgstr "Teil bestellen" #: build/templates/build/allocate.html:30 -#: company/templates/company/detail_part.html:28 order/views.py:803 +#: company/templates/company/detail_part.html:28 order/views.py:805 #: part/templates/part/category.html:125 msgid "Order Parts" msgstr "Teile bestellen" @@ -753,11 +754,11 @@ msgid "" "The following stock items will be allocated to the specified build output" msgstr "Lagerobjekt dem Bau zuweisen" -#: build/templates/build/auto_allocate.html:18 stock/forms.py:337 -#: stock/templates/stock/item_base.html:227 +#: build/templates/build/auto_allocate.html:18 stock/forms.py:343 +#: stock/templates/stock/item_base.html:234 #: stock/templates/stock/stock_adjust.html:17 #: templates/InvenTree/search.html:183 templates/js/barcode.js:337 -#: templates/js/build.js:434 templates/js/stock.js:580 +#: templates/js/build.js:434 templates/js/stock.js:587 msgid "Location" msgstr "Standort" @@ -790,7 +791,7 @@ msgstr "Dieser Bau ist Kind von Bau" #: order/templates/order/order_base.html:26 #: order/templates/order/sales_order_base.html:35 #: part/templates/part/category.html:13 part/templates/part/part_base.html:32 -#: stock/templates/stock/item_base.html:90 +#: stock/templates/stock/item_base.html:97 #: stock/templates/stock/location.html:12 #, fuzzy #| msgid "Admin" @@ -801,7 +802,7 @@ msgstr "Admin" #: build/templates/build/build_base.html:92 #: order/templates/order/sales_order_base.html:41 #: order/templates/order/sales_order_base.html:83 -#: templates/js/table_filters.js:190 templates/js/table_filters.js:222 +#: templates/js/table_filters.js:200 templates/js/table_filters.js:232 msgid "Overdue" msgstr "" @@ -830,10 +831,10 @@ msgstr "Bau-Status" #: build/templates/build/build_base.html:88 #: build/templates/build/detail.html:57 #: order/templates/order/receive_parts.html:24 -#: stock/templates/stock/item_base.html:312 templates/InvenTree/search.html:175 +#: stock/templates/stock/item_base.html:333 templates/InvenTree/search.html:175 #: templates/js/barcode.js:42 templates/js/build.js:697 #: templates/js/order.js:180 templates/js/order.js:268 -#: templates/js/stock.js:567 templates/js/stock.js:971 +#: templates/js/stock.js:574 templates/js/stock.js:997 msgid "Status" msgstr "Status" @@ -853,7 +854,7 @@ msgstr "" #: order/templates/order/sales_order_notes.html:10 #: order/templates/order/sales_order_ship.html:25 #: part/templates/part/allocation.html:27 -#: stock/templates/stock/item_base.html:221 templates/js/order.js:229 +#: stock/templates/stock/item_base.html:228 templates/js/order.js:229 msgid "Sales Order" msgstr "Bestellung" @@ -974,7 +975,7 @@ msgstr "Lagerobjekt" msgid "Stock can be taken from any available location." msgstr "Bestand kann jedem verfügbaren Lagerort entnommen werden." -#: build/templates/build/detail.html:44 stock/forms.py:365 +#: build/templates/build/detail.html:44 stock/forms.py:371 #, fuzzy #| msgid "Description" msgid "Destination" @@ -987,9 +988,9 @@ msgid "Destination location not specified" msgstr "Hat dieses Teil Tracking für einzelne Objekte?" #: build/templates/build/detail.html:68 -#: stock/templates/stock/item_base.html:245 templates/js/stock.js:575 -#: templates/js/stock.js:978 templates/js/table_filters.js:80 -#: templates/js/table_filters.js:151 +#: stock/templates/stock/item_base.html:252 templates/js/stock.js:582 +#: templates/js/stock.js:1004 templates/js/table_filters.js:80 +#: templates/js/table_filters.js:161 msgid "Batch" msgstr "Los" @@ -1100,7 +1101,7 @@ msgstr "Lagerbestand dem Bau zuweisen" msgid "Create Build Output" msgstr "Bau-Ausgabe" -#: build/views.py:207 stock/models.py:828 stock/views.py:1668 +#: build/views.py:207 stock/models.py:871 stock/views.py:1681 #, fuzzy #| msgid "Serial numbers already exist: " msgid "Serial numbers already exist" @@ -1233,38 +1234,38 @@ msgstr "verfügbar" msgid "Stock item must be selected" msgstr "Lagerobjekt wurde zugewiesen" -#: build/views.py:1011 +#: build/views.py:1012 msgid "Edit Stock Allocation" msgstr "Teilzuordnung bearbeiten" -#: build/views.py:1016 +#: build/views.py:1017 msgid "Updated Build Item" msgstr "Bauobjekt aktualisiert" -#: build/views.py:1045 +#: build/views.py:1046 #, fuzzy #| msgid "Add Sales Order Attachment" msgid "Add Build Order Attachment" msgstr "Auftragsanhang hinzufügen" -#: build/views.py:1059 order/views.py:111 order/views.py:164 part/views.py:168 +#: build/views.py:1060 order/views.py:113 order/views.py:166 part/views.py:170 #: stock/views.py:180 msgid "Added attachment" msgstr "Anhang hinzugefügt" -#: build/views.py:1095 order/views.py:191 order/views.py:213 +#: build/views.py:1096 order/views.py:193 order/views.py:215 msgid "Edit Attachment" msgstr "Anhang bearbeiten" -#: build/views.py:1106 order/views.py:196 order/views.py:218 +#: build/views.py:1107 order/views.py:198 order/views.py:220 msgid "Attachment updated" msgstr "Anhang aktualisiert" -#: build/views.py:1116 order/views.py:233 order/views.py:248 +#: build/views.py:1117 order/views.py:235 order/views.py:250 msgid "Delete Attachment" msgstr "Anhang löschen" -#: build/views.py:1122 order/views.py:240 order/views.py:255 stock/views.py:238 +#: build/views.py:1123 order/views.py:242 order/views.py:257 stock/views.py:238 msgid "Deleted attachment" msgstr "Anhang gelöscht" @@ -1360,125 +1361,208 @@ msgstr "Teilparametervorlage bearbeiten" msgid "Copy category parameter templates when creating a part" msgstr "" -#: common/models.py:115 part/models.py:743 part/templates/part/detail.html:168 -#: templates/js/table_filters.js:272 +#: common/models.py:115 part/templates/part/detail.html:155 stock/forms.py:255 +#: templates/js/table_filters.js:23 templates/js/table_filters.js:266 +msgid "Template" +msgstr "Vorlage" + +#: common/models.py:116 +#, fuzzy +#| msgid "Part is not a virtual part" +msgid "Parts are templates by default" +msgstr "Teil ist nicht virtuell" + +#: common/models.py:122 part/models.py:794 part/templates/part/detail.html:165 +#: templates/js/table_filters.js:278 +msgid "Assembly" +msgstr "Baugruppe" + +#: common/models.py:123 +#, fuzzy +#| msgid "Part can be assembled from other parts" +msgid "Parts can be assembled from other components by default" +msgstr "Teil kann aus anderen Teilen angefertigt werden" + +#: common/models.py:129 part/models.py:800 part/templates/part/detail.html:175 +#: templates/js/table_filters.js:282 msgid "Component" msgstr "Komponente" -#: common/models.py:116 +#: common/models.py:130 #, fuzzy #| msgid "Part can be used in assemblies" msgid "Parts can be used as sub-components by default" msgstr "Teil kann in Baugruppen benutzt werden" -#: common/models.py:122 part/models.py:754 part/templates/part/detail.html:188 +#: common/models.py:136 part/models.py:811 part/templates/part/detail.html:195 msgid "Purchaseable" msgstr "Kaufbar" -#: common/models.py:123 +#: common/models.py:137 msgid "Parts are purchaseable by default" msgstr "" -#: common/models.py:129 part/models.py:759 part/templates/part/detail.html:198 -#: templates/js/table_filters.js:280 +#: common/models.py:143 part/models.py:816 part/templates/part/detail.html:205 +#: templates/js/table_filters.js:290 msgid "Salable" msgstr "Verkäuflich" -#: common/models.py:130 +#: common/models.py:144 msgid "Parts are salable by default" msgstr "" -#: common/models.py:136 part/models.py:749 part/templates/part/detail.html:178 -#: templates/js/table_filters.js:31 templates/js/table_filters.js:284 +#: common/models.py:150 part/models.py:806 part/templates/part/detail.html:185 +#: templates/js/table_filters.js:31 templates/js/table_filters.js:294 msgid "Trackable" msgstr "nachverfolgbar" -#: common/models.py:137 +#: common/models.py:151 msgid "Parts are trackable by default" msgstr "" -#: common/models.py:143 +#: common/models.py:157 part/models.py:826 part/templates/part/detail.html:145 +#: templates/js/table_filters.js:27 +msgid "Virtual" +msgstr "Virtuell" + +#: common/models.py:158 +#, fuzzy +#| msgid "Part is not a virtual part" +msgid "Parts are virtual by default" +msgstr "Teil ist nicht virtuell" + +#: common/models.py:164 +#, fuzzy +#| msgid "Stock Export Options" +msgid "Stock Expiry" +msgstr "Lagerbestandsexportoptionen" + +#: common/models.py:165 +msgid "Enable stock expiry functionality" +msgstr "" + +#: common/models.py:171 +#, fuzzy +#| msgid "Serialize Stock" +msgid "Sell Expired Stock" +msgstr "Lagerbestand erfassen" + +#: common/models.py:172 +msgid "Allow sale of expired stock" +msgstr "" + +#: common/models.py:178 +#, fuzzy +#| msgid "Stock Item" +msgid "Stock Stale Time" +msgstr "Lagerobjekt" + +#: common/models.py:179 +msgid "Number of days stock items are considered stale before expiring" +msgstr "" + +#: common/models.py:181 part/templates/part/detail.html:116 +msgid "days" +msgstr "" + +#: common/models.py:186 +#, fuzzy +#| msgid "Builds" +msgid "Build Expired Stock" +msgstr "Baue" + +#: common/models.py:187 +msgid "Allow building with expired stock" +msgstr "" + +#: common/models.py:193 #, fuzzy #| msgid "Order Reference" msgid "Build Order Reference Prefix" msgstr "Bestellreferenz" -#: common/models.py:144 +#: common/models.py:194 #, fuzzy #| msgid "Order reference" msgid "Prefix value for build order reference" msgstr "Bestell-Referenz" -#: common/models.py:149 +#: common/models.py:199 #, fuzzy #| msgid "Order Reference" msgid "Build Order Reference Regex" msgstr "Bestellreferenz" -#: common/models.py:150 +#: common/models.py:200 msgid "Regular expression pattern for matching build order reference" msgstr "" -#: common/models.py:154 +#: common/models.py:204 #, fuzzy #| msgid "Sales Order Reference" msgid "Sales Order Reference Prefix" msgstr "Bestellungsreferenz" -#: common/models.py:155 +#: common/models.py:205 #, fuzzy #| msgid "Order reference" msgid "Prefix value for sales order reference" msgstr "Bestell-Referenz" -#: common/models.py:160 +#: common/models.py:210 #, fuzzy #| msgid "Order reference" msgid "Purchase Order Reference Prefix" msgstr "Bestell-Referenz" -#: common/models.py:161 +#: common/models.py:211 #, fuzzy #| msgid "Order reference" msgid "Prefix value for purchase order reference" msgstr "Bestell-Referenz" -#: common/models.py:378 +#: common/models.py:434 msgid "Settings key (must be unique - case insensitive" msgstr "" "Einstellungs-Schlüssel (muss einzigartig sein, Groß-/ Kleinschreibung wird " "nicht beachtet)" -#: common/models.py:380 +#: common/models.py:436 msgid "Settings value" msgstr "Einstellungs-Wert" -#: common/models.py:439 +#: common/models.py:493 msgid "Value must be a boolean value" msgstr "" -#: common/models.py:453 +#: common/models.py:503 +#, fuzzy +#| msgid "Must enter integer value" +msgid "Value must be an integer value" +msgstr "Nur Ganzzahl eingeben" + +#: common/models.py:517 msgid "Key string must be unique" msgstr "Schlüsseltext muss eindeutig sein" -#: common/models.py:497 company/forms.py:113 +#: common/models.py:590 company/forms.py:113 #, fuzzy #| msgid "Price Breaks" msgid "Price break quantity" msgstr "Preisstaffelung" -#: common/models.py:505 company/templates/company/supplier_part_pricing.html:80 +#: common/models.py:598 company/templates/company/supplier_part_pricing.html:80 #: part/templates/part/sale_prices.html:87 templates/js/bom.js:246 msgid "Price" msgstr "Preis" -#: common/models.py:506 +#: common/models.py:599 #, fuzzy #| msgid "Enter a valid quantity" msgid "Unit price at specified quantity" msgstr "Bitte eine gültige Anzahl eingeben" -#: common/models.py:529 +#: common/models.py:622 #, fuzzy #| msgid "Default Location" msgid "Default" @@ -1599,8 +1683,8 @@ msgstr "Produziert diese Firma Teile?" msgid "Currency" msgstr "Währung bearbeiten" -#: company/models.py:313 stock/models.py:338 -#: stock/templates/stock/item_base.html:177 +#: company/models.py:313 stock/models.py:344 +#: stock/templates/stock/item_base.html:184 msgid "Base Part" msgstr "Basisteil" @@ -1613,7 +1697,7 @@ msgstr "Teil auswählen" #: company/templates/company/supplier_part_detail.html:21 #: order/templates/order/order_base.html:79 #: order/templates/order/order_wizard/select_pos.html:30 part/bom.py:170 -#: stock/templates/stock/item_base.html:287 templates/js/company.js:48 +#: stock/templates/stock/item_base.html:294 templates/js/company.js:48 #: templates/js/company.js:164 templates/js/order.js:162 msgid "Supplier" msgstr "Zulieferer" @@ -1652,7 +1736,7 @@ msgstr "MPN" msgid "Manufacturer part number" msgstr "Hersteller-Teilenummer" -#: company/models.py:353 templates/js/company.js:208 +#: company/models.py:353 part/models.py:704 templates/js/company.js:208 msgid "Link" msgstr "Link" @@ -1717,8 +1801,8 @@ msgid "Uses default currency" msgstr "Währung entfernen" #: company/templates/company/detail.html:62 -#: order/templates/order/sales_order_base.html:89 stock/models.py:373 -#: stock/models.py:374 stock/templates/stock/item_base.html:204 +#: order/templates/order/sales_order_base.html:89 stock/models.py:379 +#: stock/models.py:380 stock/templates/stock/item_base.html:211 #: templates/js/company.js:40 templates/js/order.js:250 msgid "Customer" msgstr "Kunde" @@ -1734,7 +1818,7 @@ msgstr "Neues Zuliefererteil anlegen" #: company/templates/company/detail_part.html:18 #: order/templates/order/purchase_order_detail.html:68 -#: part/templates/part/supplier.html:14 templates/js/stock.js:855 +#: part/templates/part/supplier.html:14 templates/js/stock.js:881 msgid "New Supplier Part" msgstr "Neues Zulieferer-Teil" @@ -1762,7 +1846,7 @@ msgid "Delete Parts" msgstr "Teile löschen" #: company/templates/company/detail_part.html:63 -#: part/templates/part/category.html:116 templates/js/stock.js:849 +#: part/templates/part/category.html:116 templates/js/stock.js:875 msgid "New Part" msgstr "Neues Teil" @@ -1855,8 +1939,8 @@ msgid "New Sales Order" msgstr "Neuer Auftrag" #: company/templates/company/supplier_part_base.html:6 -#: company/templates/company/supplier_part_base.html:19 stock/models.py:347 -#: stock/templates/stock/item_base.html:292 templates/js/company.js:180 +#: company/templates/company/supplier_part_base.html:19 stock/models.py:353 +#: stock/templates/stock/item_base.html:299 templates/js/company.js:180 msgid "Supplier Part" msgstr "Zulieferer-Teil" @@ -1897,7 +1981,7 @@ msgid "Pricing Information" msgstr "Preisinformationen ansehen" #: company/templates/company/supplier_part_pricing.html:17 company/views.py:486 -#: part/templates/part/sale_prices.html:14 part/views.py:2555 +#: part/templates/part/sale_prices.html:14 part/views.py:2565 msgid "Add Price Break" msgstr "Preisstaffel hinzufügen" @@ -1934,7 +2018,7 @@ msgstr "Bepreisung" #: company/templates/company/tabs.html:12 part/templates/part/tabs.html:18 #: stock/templates/stock/location.html:17 templates/InvenTree/search.html:155 #: templates/InvenTree/settings/tabs.html:25 templates/js/part.js:192 -#: templates/js/part.js:418 templates/js/stock.js:508 templates/navbar.html:22 +#: templates/js/part.js:418 templates/js/stock.js:509 templates/navbar.html:22 #: users/models.py:29 msgid "Stock" msgstr "Lagerbestand" @@ -1944,7 +2028,7 @@ msgid "Orders" msgstr "Bestellungen" #: company/templates/company/tabs.html:9 -#: order/templates/order/receive_parts.html:14 part/models.py:316 +#: order/templates/order/receive_parts.html:14 part/models.py:317 #: part/templates/part/cat_link.html:7 part/templates/part/category.html:94 #: part/templates/part/category_tabs.html:6 #: templates/InvenTree/settings/tabs.html:22 templates/navbar.html:19 @@ -2017,7 +2101,7 @@ msgstr "Firma gelöscht" msgid "Edit Supplier Part" msgstr "Zuliefererteil bearbeiten" -#: company/views.py:295 templates/js/stock.js:856 +#: company/views.py:295 templates/js/stock.js:882 msgid "Create new Supplier Part" msgstr "Neues Zuliefererteil anlegen" @@ -2025,17 +2109,17 @@ msgstr "Neues Zuliefererteil anlegen" msgid "Delete Supplier Part" msgstr "Zuliefererteil entfernen" -#: company/views.py:492 part/views.py:2561 +#: company/views.py:492 part/views.py:2571 #, fuzzy #| msgid "Add Price Break" msgid "Added new price break" msgstr "Preisstaffel hinzufügen" -#: company/views.py:548 part/views.py:2605 +#: company/views.py:548 part/views.py:2615 msgid "Edit Price Break" msgstr "Preisstaffel bearbeiten" -#: company/views.py:564 part/views.py:2621 +#: company/views.py:564 part/views.py:2631 msgid "Delete Price Break" msgstr "Preisstaffel löschen" @@ -2145,8 +2229,8 @@ msgstr "" msgid "Date order was completed" msgstr "Bestellung als vollständig markieren" -#: order/models.py:176 order/models.py:258 part/views.py:1494 -#: stock/models.py:244 stock/models.py:812 +#: order/models.py:176 order/models.py:258 part/views.py:1504 +#: stock/models.py:250 stock/models.py:855 msgid "Quantity must be greater than zero" msgstr "Anzahl muss größer Null sein" @@ -2184,7 +2268,7 @@ msgstr "Position - Notizen" #: order/models.py:504 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:24 -#: stock/templates/stock/item_base.html:259 templates/js/order.js:146 +#: stock/templates/stock/item_base.html:266 templates/js/order.js:146 msgid "Purchase Order" msgstr "Kaufvertrag" @@ -2196,8 +2280,8 @@ msgstr "Zulieferer-Teil" msgid "Number of items received" msgstr "Empfangene Objekt-Anzahl" -#: order/models.py:527 stock/models.py:458 -#: stock/templates/stock/item_base.html:266 +#: order/models.py:527 stock/models.py:472 +#: stock/templates/stock/item_base.html:273 #, fuzzy #| msgid "Purchase Order" msgid "Purchase Price" @@ -2360,8 +2444,8 @@ msgid "Line Items" msgstr "Position hinzufügen" #: order/templates/order/purchase_order_detail.html:17 -#: order/templates/order/sales_order_detail.html:19 order/views.py:1117 -#: order/views.py:1201 +#: order/templates/order/sales_order_detail.html:19 order/views.py:1119 +#: order/views.py:1203 msgid "Add Line Item" msgstr "Position hinzufügen" @@ -2372,7 +2456,7 @@ msgstr "Bestellpositionen" #: order/templates/order/purchase_order_detail.html:39 #: order/templates/order/purchase_order_detail.html:119 #: part/templates/part/category.html:173 part/templates/part/category.html:215 -#: templates/js/stock.js:861 +#: templates/js/stock.js:627 templates/js/stock.js:887 msgid "New Location" msgstr "Neuer Standort" @@ -2461,8 +2545,8 @@ msgid "Sales Order Items" msgstr "Auftragspositionen" #: order/templates/order/sales_order_detail.html:72 -#: order/templates/order/sales_order_detail.html:154 stock/models.py:378 -#: stock/templates/stock/item_base.html:191 templates/js/build.js:418 +#: order/templates/order/sales_order_detail.html:154 stock/models.py:384 +#: stock/templates/stock/item_base.html:198 templates/js/build.js:418 msgid "Serial Number" msgstr "Seriennummer" @@ -2544,151 +2628,151 @@ msgstr "Sind Sie sicher, dass Sie diese Position löschen möchten?" msgid "Order Items" msgstr "Bestellungspositionen" -#: order/views.py:99 +#: order/views.py:101 msgid "Add Purchase Order Attachment" msgstr "Bestellanhang hinzufügen" -#: order/views.py:150 +#: order/views.py:152 msgid "Add Sales Order Attachment" msgstr "Auftragsanhang hinzufügen" -#: order/views.py:310 +#: order/views.py:312 msgid "Create Purchase Order" msgstr "Bestellung anlegen" -#: order/views.py:346 +#: order/views.py:348 msgid "Create Sales Order" msgstr "Auftrag anlegen" -#: order/views.py:382 +#: order/views.py:384 msgid "Edit Purchase Order" msgstr "Bestellung bearbeiten" -#: order/views.py:403 +#: order/views.py:405 msgid "Edit Sales Order" msgstr "Auftrag bearbeiten" -#: order/views.py:420 +#: order/views.py:422 msgid "Cancel Order" msgstr "Bestellung stornieren" -#: order/views.py:430 order/views.py:457 +#: order/views.py:432 order/views.py:459 msgid "Confirm order cancellation" msgstr "Bestellstornierung bestätigen" -#: order/views.py:433 +#: order/views.py:435 msgid "Order cannot be cancelled as either pending or placed" msgstr "" -#: order/views.py:447 +#: order/views.py:449 msgid "Cancel sales order" msgstr "Auftrag stornieren" -#: order/views.py:460 +#: order/views.py:462 msgid "Order cannot be cancelled" msgstr "" -#: order/views.py:474 +#: order/views.py:476 msgid "Issue Order" msgstr "Bestellung aufgeben" -#: order/views.py:484 +#: order/views.py:486 msgid "Confirm order placement" msgstr "Bestellungstätigung bestätigen" -#: order/views.py:494 +#: order/views.py:496 #, fuzzy #| msgid "Purchase Order Details" msgid "Purchase order issued" msgstr "Bestelldetails" -#: order/views.py:505 +#: order/views.py:507 msgid "Complete Order" msgstr "Auftrag fertigstellen" -#: order/views.py:522 +#: order/views.py:524 #, fuzzy #| msgid "Confirm build completion" msgid "Confirm order completion" msgstr "Bau-Fertigstellung bestätigen" -#: order/views.py:533 +#: order/views.py:535 #, fuzzy #| msgid "Mark order as complete" msgid "Purchase order completed" msgstr "Bestellung als vollständig markieren" -#: order/views.py:543 +#: order/views.py:545 msgid "Ship Order" msgstr "Versenden" -#: order/views.py:560 +#: order/views.py:562 msgid "Confirm order shipment" msgstr "Versand bestätigen" -#: order/views.py:566 +#: order/views.py:568 msgid "Could not ship order" msgstr "Versand fehlgeschlagen" -#: order/views.py:618 +#: order/views.py:620 msgid "Receive Parts" msgstr "Teile empfangen" -#: order/views.py:686 +#: order/views.py:688 msgid "Items received" msgstr "Anzahl empfangener Positionen" -#: order/views.py:700 +#: order/views.py:702 msgid "No destination set" msgstr "Kein Ziel gesetzt" -#: order/views.py:745 +#: order/views.py:747 msgid "Error converting quantity to number" msgstr "Fehler beim Konvertieren zu Zahl" -#: order/views.py:751 +#: order/views.py:753 msgid "Receive quantity less than zero" msgstr "Anzahl kleiner null empfangen" -#: order/views.py:757 +#: order/views.py:759 msgid "No lines specified" msgstr "Keine Zeilen angegeben" -#: order/views.py:1127 +#: order/views.py:1129 #, fuzzy #| msgid "Supplier part description" msgid "Supplier part must be specified" msgstr "Zuliefererbeschreibung des Teils" -#: order/views.py:1133 +#: order/views.py:1135 msgid "Supplier must match for Part and Order" msgstr "Zulieferer muss zum Teil und zur Bestellung passen" -#: order/views.py:1253 order/views.py:1272 +#: order/views.py:1255 order/views.py:1274 msgid "Edit Line Item" msgstr "Position bearbeiten" -#: order/views.py:1289 order/views.py:1302 +#: order/views.py:1291 order/views.py:1304 msgid "Delete Line Item" msgstr "Position löschen" -#: order/views.py:1295 order/views.py:1308 +#: order/views.py:1297 order/views.py:1310 msgid "Deleted line item" msgstr "Position gelöscht" -#: order/views.py:1317 +#: order/views.py:1319 msgid "Allocate Stock to Order" msgstr "Lagerbestand dem Auftrag zuweisen" -#: order/views.py:1387 +#: order/views.py:1394 msgid "Edit Allocation Quantity" msgstr "Zuordnung bearbeiten" -#: order/views.py:1403 +#: order/views.py:1410 msgid "Remove allocation" msgstr "Zuordnung entfernen" -#: part/bom.py:138 part/templates/part/category.html:61 +#: part/bom.py:138 part/models.py:722 part/templates/part/category.html:61 #: part/templates/part/detail.html:87 msgid "Default Location" msgstr "Standard-Lagerort" @@ -2710,11 +2794,11 @@ msgstr "Fehler beim Lesen der Stückliste (ungültige Daten)" msgid "Error reading BOM file (incorrect row size)" msgstr "Fehler beim Lesen der Stückliste (ungültige Zeilengröße)" -#: part/forms.py:61 stock/forms.py:255 +#: part/forms.py:61 stock/forms.py:261 msgid "File Format" msgstr "Dateiformat" -#: part/forms.py:61 stock/forms.py:255 +#: part/forms.py:61 stock/forms.py:261 msgid "Select output file format" msgstr "Ausgabe-Dateiformat auswählen" @@ -2766,7 +2850,7 @@ msgstr "Neues Zulieferer-Teil" msgid "Include part supplier data in exported BOM" msgstr "" -#: part/forms.py:92 part/models.py:1720 +#: part/forms.py:92 part/models.py:1781 msgid "Parent Part" msgstr "Ausgangsteil" @@ -2806,7 +2890,7 @@ msgstr "Teile löschen" msgid "Select part category" msgstr "Teilekategorie wählen" -#: part/forms.py:188 +#: part/forms.py:189 #, fuzzy #| msgid "Perform 'deep copy' which will duplicate all BOM data for this part" msgid "Duplicate all BOM data for this part" @@ -2814,49 +2898,49 @@ msgstr "" "Tiefe Kopie ausführen. Dies wird alle Daten der Stückliste für dieses Teil " "duplizieren" -#: part/forms.py:189 +#: part/forms.py:190 msgid "Copy BOM" msgstr "" -#: part/forms.py:194 +#: part/forms.py:195 msgid "Duplicate all parameter data for this part" msgstr "" -#: part/forms.py:195 +#: part/forms.py:196 #, fuzzy #| msgid "Parameters" msgid "Copy Parameters" msgstr "Parameter" -#: part/forms.py:200 +#: part/forms.py:201 msgid "Confirm part creation" msgstr "Erstellen des Teils bestätigen" -#: part/forms.py:205 +#: part/forms.py:206 #, fuzzy #| msgid "No part parameter templates found" msgid "Include category parameter templates" msgstr "Keine Teilparametervorlagen gefunden" -#: part/forms.py:210 +#: part/forms.py:211 #, fuzzy #| msgid "No part parameter templates found" msgid "Include parent categories parameter templates" msgstr "Keine Teilparametervorlagen gefunden" -#: part/forms.py:285 +#: part/forms.py:291 #, fuzzy #| msgid "Parameter template name must be unique" msgid "Add parameter template to same level categories" msgstr "Vorlagen-Name des Parameters muss eindeutig sein" -#: part/forms.py:289 +#: part/forms.py:295 #, fuzzy #| msgid "Parameter template name must be unique" msgid "Add parameter template to all categories" msgstr "Vorlagen-Name des Parameters muss eindeutig sein" -#: part/forms.py:333 +#: part/forms.py:339 msgid "Input quantity for price calculation" msgstr "Eintragsmenge zur Preisberechnung" @@ -2868,7 +2952,7 @@ msgstr "Standard-Standort für Teile dieser Kategorie" msgid "Default keywords for parts in this category" msgstr "Standard-Stichworte für Teile dieser Kategorie" -#: part/models.py:77 part/models.py:1765 +#: part/models.py:77 part/models.py:1826 #: part/templates/part/part_app_base.html:9 msgid "Part Category" msgstr "Teilkategorie" @@ -2878,142 +2962,185 @@ msgstr "Teilkategorie" msgid "Part Categories" msgstr "Teile-Kategorien" -#: part/models.py:408 part/models.py:418 +#: part/models.py:409 part/models.py:419 #, python-brace-format msgid "Part '{p1}' is used in BOM for '{p2}' (recursive)" msgstr "Teil '{p1}' wird in Stückliste für Teil '{p2}' benutzt (rekursiv)" -#: part/models.py:515 +#: part/models.py:516 #, fuzzy #| msgid "No serial numbers found" msgid "Next available serial numbers are" msgstr "Keine Seriennummern gefunden" -#: part/models.py:519 +#: part/models.py:520 msgid "Next available serial number is" msgstr "" -#: part/models.py:524 +#: part/models.py:525 #, fuzzy #| msgid "Empty serial number string" msgid "Most recent serial number is" msgstr "Keine Seriennummer angegeben" -#: part/models.py:603 +#: part/models.py:604 msgid "Duplicate IPN not allowed in part settings" msgstr "" -#: part/models.py:614 +#: part/models.py:615 msgid "Part must be unique for name, IPN and revision" msgstr "Namen, Teile- und Revisionsnummern müssen eindeutig sein" -#: part/models.py:644 part/templates/part/detail.html:19 +#: part/models.py:646 part/templates/part/detail.html:19 msgid "Part name" msgstr "Name des Teils" -#: part/models.py:648 +#: part/models.py:653 +#, fuzzy +#| msgid "Template" +msgid "Is Template" +msgstr "Vorlage" + +#: part/models.py:654 msgid "Is this part a template part?" msgstr "Ist dieses Teil eine Vorlage?" -#: part/models.py:657 +#: part/models.py:665 msgid "Is this part a variant of another part?" msgstr "Ist dieses Teil eine Variante eines anderen Teils?" -#: part/models.py:659 +#: part/models.py:666 part/templates/part/detail.html:57 +msgid "Variant Of" +msgstr "Variante von" + +#: part/models.py:672 msgid "Part description" msgstr "Beschreibung des Teils" -#: part/models.py:661 +#: part/models.py:677 part/templates/part/category.html:68 +#: part/templates/part/detail.html:64 +msgid "Keywords" +msgstr "Schlüsselwörter" + +#: part/models.py:678 msgid "Part keywords to improve visibility in search results" msgstr "Schlüsselworte um die Sichtbarkeit in Suchergebnissen zu verbessern" -#: part/models.py:666 +#: part/models.py:685 part/templates/part/detail.html:70 +#: part/templates/part/set_category.html:15 templates/js/part.js:405 +msgid "Category" +msgstr "Kategorie" + +#: part/models.py:686 msgid "Part category" msgstr "Teile-Kategorie" -#: part/models.py:668 +#: part/models.py:691 part/templates/part/detail.html:25 +#: part/templates/part/part_base.html:95 templates/js/part.js:180 +msgid "IPN" +msgstr "IPN (Interne Produktnummer)" + +#: part/models.py:692 msgid "Internal Part Number" msgstr "Interne Teilenummer" -#: part/models.py:670 +#: part/models.py:698 msgid "Part revision or version number" msgstr "Revisions- oder Versionsnummer" -#: part/models.py:684 +#: part/models.py:699 part/templates/part/detail.html:32 +#: templates/js/part.js:184 +msgid "Revision" +msgstr "Revision" + +#: part/models.py:720 msgid "Where is this item normally stored?" msgstr "Wo wird dieses Teil normalerweise gelagert?" -#: part/models.py:728 +#: part/models.py:767 part/templates/part/detail.html:94 +msgid "Default Supplier" +msgstr "Standard-Zulieferer" + +#: part/models.py:768 msgid "Default supplier part" msgstr "Standard-Zulieferer?" -#: part/models.py:731 +#: part/models.py:775 +#, fuzzy +#| msgid "Default Supplier" +msgid "Default Expiry" +msgstr "Standard-Zulieferer" + +#: part/models.py:776 +msgid "Expiry time (in days) for stock items of this part" +msgstr "" + +#: part/models.py:781 part/templates/part/detail.html:108 +msgid "Minimum Stock" +msgstr "Minimaler Lagerbestand" + +#: part/models.py:782 msgid "Minimum allowed stock level" msgstr "Minimal zulässiger Lagerbestand" -#: part/models.py:733 +#: part/models.py:788 part/templates/part/detail.html:102 +#: part/templates/part/params.html:26 +msgid "Units" +msgstr "Einheiten" + +#: part/models.py:789 msgid "Stock keeping units for this part" msgstr "Stock Keeping Units (SKU) für dieses Teil" -#: part/models.py:737 part/templates/part/detail.html:158 -#: templates/js/table_filters.js:268 -msgid "Assembly" -msgstr "Baugruppe" - -#: part/models.py:738 +#: part/models.py:795 msgid "Can this part be built from other parts?" msgstr "Kann dieses Teil aus anderen Teilen angefertigt werden?" -#: part/models.py:744 +#: part/models.py:801 msgid "Can this part be used to build other parts?" msgstr "Kann dieses Teil zum Bau von anderen genutzt werden?" -#: part/models.py:750 +#: part/models.py:807 msgid "Does this part have tracking for unique items?" msgstr "Hat dieses Teil Tracking für einzelne Objekte?" -#: part/models.py:755 +#: part/models.py:812 msgid "Can this part be purchased from external suppliers?" msgstr "Kann dieses Teil von externen Zulieferern gekauft werden?" -#: part/models.py:760 +#: part/models.py:817 msgid "Can this part be sold to customers?" msgstr "Kann dieses Teil an Kunden verkauft werden?" -#: part/models.py:764 part/templates/part/detail.html:215 +#: part/models.py:821 part/templates/part/detail.html:222 #: templates/js/table_filters.js:19 templates/js/table_filters.js:55 -#: templates/js/table_filters.js:186 templates/js/table_filters.js:251 +#: templates/js/table_filters.js:196 templates/js/table_filters.js:261 msgid "Active" msgstr "Aktiv" -#: part/models.py:765 +#: part/models.py:822 msgid "Is this part active?" msgstr "Ist dieses Teil aktiv?" -#: part/models.py:769 part/templates/part/detail.html:138 -#: templates/js/table_filters.js:27 -msgid "Virtual" -msgstr "Virtuell" - -#: part/models.py:770 +#: part/models.py:827 msgid "Is this a virtual part, such as a software product or license?" msgstr "Ist dieses Teil virtuell, wie zum Beispiel eine Software oder Lizenz?" -#: part/models.py:772 +#: part/models.py:832 msgid "Part notes - supports Markdown formatting" msgstr "Bemerkungen - unterstüzt Markdown-Formatierung" -#: part/models.py:774 +#: part/models.py:835 msgid "Stored BOM checksum" msgstr "Prüfsumme der Stückliste gespeichert" -#: part/models.py:1593 +#: part/models.py:1654 #, fuzzy #| msgid "Stock item cannot be created for a template Part" msgid "Test templates can only be created for trackable parts" msgstr "Lagerobjekt kann nicht für Vorlagen-Teile angelegt werden" -#: part/models.py:1610 +#: part/models.py:1671 #, fuzzy #| msgid "" #| "A stock item with this serial number already exists for template part " @@ -3023,146 +3150,146 @@ msgstr "" "Ein Teil mit dieser Seriennummer existiert bereits für die Teilevorlage " "{part}" -#: part/models.py:1629 templates/js/part.js:567 templates/js/stock.js:92 +#: part/models.py:1690 templates/js/part.js:567 templates/js/stock.js:93 #, fuzzy #| msgid "Instance Name" msgid "Test Name" msgstr "Instanzname" -#: part/models.py:1630 +#: part/models.py:1691 #, fuzzy #| msgid "Serial number for this item" msgid "Enter a name for the test" msgstr "Seriennummer für dieses Teil" -#: part/models.py:1635 +#: part/models.py:1696 #, fuzzy #| msgid "Description" msgid "Test Description" msgstr "Beschreibung" -#: part/models.py:1636 +#: part/models.py:1697 #, fuzzy #| msgid "Brief description of the build" msgid "Enter description for this test" msgstr "Kurze Beschreibung des Baus" -#: part/models.py:1641 templates/js/part.js:576 -#: templates/js/table_filters.js:172 +#: part/models.py:1702 templates/js/part.js:576 +#: templates/js/table_filters.js:182 msgid "Required" msgstr "benötigt" -#: part/models.py:1642 +#: part/models.py:1703 msgid "Is this test required to pass?" msgstr "" -#: part/models.py:1647 templates/js/part.js:584 +#: part/models.py:1708 templates/js/part.js:584 #, fuzzy #| msgid "Required Parts" msgid "Requires Value" msgstr "benötigte Teile" -#: part/models.py:1648 +#: part/models.py:1709 msgid "Does this test require a value when adding a test result?" msgstr "" -#: part/models.py:1653 templates/js/part.js:591 +#: part/models.py:1714 templates/js/part.js:591 #, fuzzy #| msgid "Delete Attachment" msgid "Requires Attachment" msgstr "Anhang löschen" -#: part/models.py:1654 +#: part/models.py:1715 msgid "Does this test require a file attachment when adding a test result?" msgstr "" -#: part/models.py:1687 +#: part/models.py:1748 msgid "Parameter template name must be unique" msgstr "Vorlagen-Name des Parameters muss eindeutig sein" -#: part/models.py:1692 +#: part/models.py:1753 msgid "Parameter Name" msgstr "Name des Parameters" -#: part/models.py:1694 +#: part/models.py:1755 msgid "Parameter Units" msgstr "Parameter Einheit" -#: part/models.py:1722 part/models.py:1770 +#: part/models.py:1783 part/models.py:1831 #: templates/InvenTree/settings/category.html:62 msgid "Parameter Template" msgstr "Parameter Vorlage" -#: part/models.py:1724 +#: part/models.py:1785 msgid "Parameter Value" msgstr "Parameter Wert" -#: part/models.py:1774 +#: part/models.py:1835 #, fuzzy #| msgid "Parameter Value" msgid "Default Parameter Value" msgstr "Parameter Wert" -#: part/models.py:1804 +#: part/models.py:1865 msgid "Select parent part" msgstr "Ausgangsteil auswählen" -#: part/models.py:1812 +#: part/models.py:1873 msgid "Select part to be used in BOM" msgstr "Teil für die Nutzung in der Stückliste auswählen" -#: part/models.py:1818 +#: part/models.py:1879 msgid "BOM quantity for this BOM item" msgstr "Stücklisten-Anzahl für dieses Stücklisten-Teil" -#: part/models.py:1820 +#: part/models.py:1881 #, fuzzy #| msgid "Confim BOM item deletion" msgid "This BOM item is optional" msgstr "Löschung von BOM-Position bestätigen" -#: part/models.py:1823 +#: part/models.py:1884 msgid "Estimated build wastage quantity (absolute or percentage)" msgstr "Geschätzter Ausschuss (absolut oder prozentual)" -#: part/models.py:1826 +#: part/models.py:1887 msgid "BOM item reference" msgstr "Referenz des Objekts auf der Stückliste" -#: part/models.py:1829 +#: part/models.py:1890 msgid "BOM item notes" msgstr "Notizen zum Stücklisten-Objekt" -#: part/models.py:1831 +#: part/models.py:1892 msgid "BOM line checksum" msgstr "Prüfsumme der Stückliste" -#: part/models.py:1902 part/views.py:1500 part/views.py:1552 -#: stock/models.py:234 +#: part/models.py:1963 part/views.py:1510 part/views.py:1562 +#: stock/models.py:240 #, fuzzy #| msgid "Overage must be an integer value or a percentage" msgid "Quantity must be integer value for trackable parts" msgstr "Überschuss muss eine Ganzzahl oder ein Prozentwert sein" -#: part/models.py:1911 part/models.py:1913 +#: part/models.py:1972 part/models.py:1974 #, fuzzy #| msgid "Supplier part description" msgid "Sub part must be specified" msgstr "Zuliefererbeschreibung des Teils" -#: part/models.py:1916 +#: part/models.py:1977 #, fuzzy #| msgid "New BOM Item" msgid "BOM Item" msgstr "Neue Stücklistenposition" -#: part/models.py:2031 +#: part/models.py:2092 #, fuzzy #| msgid "Select a part" msgid "Select Related Part" msgstr "Teil auswählen" -#: part/models.py:2063 +#: part/models.py:2124 msgid "" "Error creating relationship: check that the part is not related to itself " "and that the relationship is unique" @@ -3183,9 +3310,9 @@ msgstr "Bestellung" #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 #: stock/templates/stock/item_base.html:72 -#: stock/templates/stock/item_base.html:274 +#: stock/templates/stock/item_base.html:281 #: stock/templates/stock/stock_adjust.html:16 templates/js/build.js:751 -#: templates/js/stock.js:705 templates/js/stock.js:954 +#: templates/js/stock.js:720 templates/js/stock.js:980 msgid "Stock Item" msgstr "Lagerobjekt" @@ -3258,7 +3385,7 @@ msgstr "Stückliste validieren" msgid "Validate" msgstr "BOM validieren" -#: part/templates/part/bom.html:62 part/views.py:1791 +#: part/templates/part/bom.html:62 part/views.py:1801 msgid "Export Bill of Materials" msgstr "Stückliste exportieren" @@ -3386,7 +3513,7 @@ msgstr "Neuen Bau beginnen" msgid "All parts" msgstr "Alle Teile" -#: part/templates/part/category.html:24 part/views.py:2182 +#: part/templates/part/category.html:24 part/views.py:2192 msgid "Create new part category" msgstr "Teilkategorie anlegen" @@ -3414,10 +3541,6 @@ msgstr "Pfad zur Kategorie" msgid "Category Description" msgstr "Kategorie-Beschreibung" -#: part/templates/part/category.html:68 part/templates/part/detail.html:64 -msgid "Keywords" -msgstr "Schlüsselwörter" - #: part/templates/part/category.html:74 msgid "Subcategories" msgstr "Unter-Kategorien" @@ -3452,7 +3575,7 @@ msgstr "Teilkategorie auswählen" msgid "Export Data" msgstr "Exportieren" -#: part/templates/part/category.html:174 +#: part/templates/part/category.html:174 templates/js/stock.js:628 #, fuzzy #| msgid "Create New Location" msgid "Create new location" @@ -3476,7 +3599,7 @@ msgstr "Teilkategorie anlegen" msgid "Create new Part Category" msgstr "Teilkategorie anlegen" -#: part/templates/part/category.html:216 stock/views.py:1359 +#: part/templates/part/category.html:216 stock/views.py:1363 msgid "Create new Stock Location" msgstr "Neuen Lager-Standort erstellen" @@ -3506,15 +3629,6 @@ msgstr "Los" msgid "Part Details" msgstr "Teile-Details" -#: part/templates/part/detail.html:25 part/templates/part/part_base.html:95 -#: templates/js/part.js:180 -msgid "IPN" -msgstr "IPN (Interne Produktnummer)" - -#: part/templates/part/detail.html:32 templates/js/part.js:184 -msgid "Revision" -msgstr "Revision" - #: part/templates/part/detail.html:39 #, fuzzy #| msgid "Serial Number" @@ -3527,107 +3641,87 @@ msgstr "Seriennummer" msgid "No serial numbers recorded" msgstr "Keine Seriennummern gefunden" -#: part/templates/part/detail.html:57 -msgid "Variant Of" -msgstr "Variante von" +#: part/templates/part/detail.html:115 +#, fuzzy +#| msgid "Stock Export Options" +msgid "Stock Expiry Time" +msgstr "Lagerbestandsexportoptionen" -#: part/templates/part/detail.html:70 part/templates/part/set_category.html:15 -#: templates/js/part.js:405 -msgid "Category" -msgstr "Kategorie" - -#: part/templates/part/detail.html:94 -msgid "Default Supplier" -msgstr "Standard-Zulieferer" - -#: part/templates/part/detail.html:102 part/templates/part/params.html:26 -msgid "Units" -msgstr "Einheiten" - -#: part/templates/part/detail.html:108 -msgid "Minimum Stock" -msgstr "Minimaler Lagerbestand" - -#: part/templates/part/detail.html:114 templates/js/order.js:276 +#: part/templates/part/detail.html:121 templates/js/order.js:276 msgid "Creation Date" msgstr "Erstelldatum" -#: part/templates/part/detail.html:120 +#: part/templates/part/detail.html:127 msgid "Created By" msgstr "Erstellt von" -#: part/templates/part/detail.html:127 +#: part/templates/part/detail.html:134 msgid "Responsible User" msgstr "Verantwortlicher Benutzer" -#: part/templates/part/detail.html:141 +#: part/templates/part/detail.html:148 msgid "Part is virtual (not a physical part)" msgstr "Teil ist virtuell (kein physisches Teil)" -#: part/templates/part/detail.html:143 +#: part/templates/part/detail.html:150 msgid "Part is not a virtual part" msgstr "Teil ist nicht virtuell" -#: part/templates/part/detail.html:148 stock/forms.py:249 -#: templates/js/table_filters.js:23 templates/js/table_filters.js:256 -msgid "Template" -msgstr "Vorlage" - -#: part/templates/part/detail.html:151 +#: part/templates/part/detail.html:158 #, fuzzy #| msgid "Part cannot be a template part if it is a variant of another part" msgid "Part is a template part (variants can be made from this part)" msgstr "Teil kann keine Vorlage sein wenn es Variante eines anderen Teils ist" -#: part/templates/part/detail.html:153 +#: part/templates/part/detail.html:160 #, fuzzy #| msgid "Part is not a virtual part" msgid "Part is not a template part" msgstr "Teil ist nicht virtuell" -#: part/templates/part/detail.html:161 +#: part/templates/part/detail.html:168 msgid "Part can be assembled from other parts" msgstr "Teil kann aus anderen Teilen angefertigt werden" -#: part/templates/part/detail.html:163 +#: part/templates/part/detail.html:170 msgid "Part cannot be assembled from other parts" msgstr "Teil kann nicht aus anderen Teilen angefertigt werden" -#: part/templates/part/detail.html:171 +#: part/templates/part/detail.html:178 msgid "Part can be used in assemblies" msgstr "Teil kann in Baugruppen benutzt werden" -#: part/templates/part/detail.html:173 +#: part/templates/part/detail.html:180 msgid "Part cannot be used in assemblies" msgstr "Teil kann nicht in Baugruppen benutzt werden" -#: part/templates/part/detail.html:181 +#: part/templates/part/detail.html:188 msgid "Part stock is tracked by serial number" msgstr "Teilebestand in der Seriennummer hinterlegt" -#: part/templates/part/detail.html:183 +#: part/templates/part/detail.html:190 msgid "Part stock is not tracked by serial number" msgstr "Teilebestand ist nicht in der Seriennummer hinterlegt" -#: part/templates/part/detail.html:191 part/templates/part/detail.html:193 +#: part/templates/part/detail.html:198 part/templates/part/detail.html:200 msgid "Part can be purchased from external suppliers" msgstr "Teil kann von externen Zulieferern gekauft werden" -#: part/templates/part/detail.html:201 +#: part/templates/part/detail.html:208 msgid "Part can be sold to customers" msgstr "Teil kann an Kunden verkauft werden" -#: part/templates/part/detail.html:203 +#: part/templates/part/detail.html:210 msgid "Part cannot be sold to customers" msgstr "Teil kann nicht an Kunden verkauft werden" -#: part/templates/part/detail.html:218 +#: part/templates/part/detail.html:225 #, fuzzy #| msgid "This part is not active" msgid "Part is active" msgstr "Dieses Teil ist nicht aktiv" -#: part/templates/part/detail.html:220 +#: part/templates/part/detail.html:227 #, fuzzy #| msgid "This part is not active" msgid "Part is not active" @@ -3647,12 +3741,12 @@ msgstr "Parameter hinzufügen" #: part/templates/part/params.html:15 #: templates/InvenTree/settings/category.html:29 -#: templates/InvenTree/settings/part.html:38 +#: templates/InvenTree/settings/part.html:41 msgid "New Parameter" msgstr "Neuer Parameter" -#: part/templates/part/params.html:25 stock/models.py:1420 -#: templates/js/stock.js:112 +#: part/templates/part/params.html:25 stock/models.py:1463 +#: templates/InvenTree/settings/header.html:8 templates/js/stock.js:113 msgid "Value" msgstr "Wert" @@ -3689,7 +3783,7 @@ msgid "Star this part" msgstr "Teil favorisieren" #: part/templates/part/part_base.html:49 -#: stock/templates/stock/item_base.html:101 +#: stock/templates/stock/item_base.html:108 #: stock/templates/stock/location.html:29 #, fuzzy #| msgid "Source Location" @@ -3697,7 +3791,7 @@ msgid "Barcode actions" msgstr "Quell-Standort" #: part/templates/part/part_base.html:51 -#: stock/templates/stock/item_base.html:103 +#: stock/templates/stock/item_base.html:110 #: stock/templates/stock/location.html:31 #, fuzzy #| msgid "Part QR Code" @@ -3705,7 +3799,7 @@ msgid "Show QR Code" msgstr "Teil-QR-Code" #: part/templates/part/part_base.html:52 -#: stock/templates/stock/item_base.html:104 +#: stock/templates/stock/item_base.html:111 #: stock/templates/stock/location.html:32 msgid "Print Label" msgstr "" @@ -3744,7 +3838,7 @@ msgstr "Vorlage bearbeiten" msgid "Delete part" msgstr "Teile löschen" -#: part/templates/part/part_base.html:124 templates/js/table_filters.js:111 +#: part/templates/part/part_base.html:124 templates/js/table_filters.js:121 msgid "In Stock" msgstr "Auf Lager" @@ -3871,7 +3965,7 @@ msgstr "Stückliste" msgid "Used In" msgstr "Benutzt in" -#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:318 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:339 msgid "Tests" msgstr "" @@ -3907,248 +4001,248 @@ msgstr "Neues Teil hinzufügen" msgid "New Variant" msgstr "Varianten" -#: part/views.py:84 +#: part/views.py:86 #, fuzzy #| msgid "Allocated Parts" msgid "Add Related Part" msgstr "Zugeordnete Teile" -#: part/views.py:140 +#: part/views.py:142 #, fuzzy #| msgid "Delete Supplier Part" msgid "Delete Related Part" msgstr "Zuliefererteil entfernen" -#: part/views.py:152 +#: part/views.py:154 msgid "Add part attachment" msgstr "Teilanhang hinzufügen" -#: part/views.py:207 templates/attachment_table.html:34 +#: part/views.py:209 templates/attachment_table.html:34 msgid "Edit attachment" msgstr "Anhang bearbeiten" -#: part/views.py:213 +#: part/views.py:215 msgid "Part attachment updated" msgstr "Teilanhang aktualisiert" -#: part/views.py:228 +#: part/views.py:230 msgid "Delete Part Attachment" msgstr "Teilanhang löschen" -#: part/views.py:236 +#: part/views.py:238 msgid "Deleted part attachment" msgstr "Teilanhang gelöscht" -#: part/views.py:245 +#: part/views.py:247 #, fuzzy #| msgid "Create Part Parameter Template" msgid "Create Test Template" msgstr "Teilparametervorlage anlegen" -#: part/views.py:274 +#: part/views.py:276 #, fuzzy #| msgid "Edit Template" msgid "Edit Test Template" msgstr "Vorlage bearbeiten" -#: part/views.py:290 +#: part/views.py:292 #, fuzzy #| msgid "Delete Template" msgid "Delete Test Template" msgstr "Vorlage löschen" -#: part/views.py:299 +#: part/views.py:301 msgid "Set Part Category" msgstr "Teilkategorie auswählen" -#: part/views.py:349 +#: part/views.py:351 #, python-brace-format msgid "Set category for {n} parts" msgstr "Kategorie für {n} Teile setzen" -#: part/views.py:384 +#: part/views.py:386 msgid "Create Variant" msgstr "Variante anlegen" -#: part/views.py:466 +#: part/views.py:468 msgid "Duplicate Part" msgstr "Teil duplizieren" -#: part/views.py:473 +#: part/views.py:475 msgid "Copied part" msgstr "Teil kopiert" -#: part/views.py:527 part/views.py:661 +#: part/views.py:529 part/views.py:667 msgid "Possible matches exist - confirm creation of new part" msgstr "" -#: part/views.py:592 templates/js/stock.js:850 +#: part/views.py:594 templates/js/stock.js:876 msgid "Create New Part" msgstr "Neues Teil anlegen" -#: part/views.py:599 +#: part/views.py:601 msgid "Created new part" msgstr "Neues Teil angelegt" -#: part/views.py:830 +#: part/views.py:836 msgid "Part QR Code" msgstr "Teil-QR-Code" -#: part/views.py:849 +#: part/views.py:855 msgid "Upload Part Image" msgstr "Teilbild hochladen" -#: part/views.py:857 part/views.py:894 +#: part/views.py:863 part/views.py:900 msgid "Updated part image" msgstr "Teilbild aktualisiert" -#: part/views.py:866 +#: part/views.py:872 msgid "Select Part Image" msgstr "Teilbild auswählen" -#: part/views.py:897 +#: part/views.py:903 msgid "Part image not found" msgstr "Teilbild nicht gefunden" -#: part/views.py:908 +#: part/views.py:914 msgid "Edit Part Properties" msgstr "Teileigenschaften bearbeiten" -#: part/views.py:935 +#: part/views.py:945 #, fuzzy #| msgid "Duplicate Part" msgid "Duplicate BOM" msgstr "Teil duplizieren" -#: part/views.py:966 +#: part/views.py:976 #, fuzzy #| msgid "Confirm unallocation of build stock" msgid "Confirm duplication of BOM from parent" msgstr "Zuweisungsaufhebung bestätigen" -#: part/views.py:987 +#: part/views.py:997 msgid "Validate BOM" msgstr "BOM validieren" -#: part/views.py:1010 +#: part/views.py:1020 #, fuzzy #| msgid "Confirm that the BOM is correct" msgid "Confirm that the BOM is valid" msgstr "Bestätigen, dass die Stückliste korrekt ist" -#: part/views.py:1021 +#: part/views.py:1031 #, fuzzy #| msgid "Validate Bill of Materials" msgid "Validated Bill of Materials" msgstr "Stückliste validieren" -#: part/views.py:1155 +#: part/views.py:1165 msgid "No BOM file provided" msgstr "Keine Stückliste angegeben" -#: part/views.py:1503 +#: part/views.py:1513 msgid "Enter a valid quantity" msgstr "Bitte eine gültige Anzahl eingeben" -#: part/views.py:1528 part/views.py:1531 +#: part/views.py:1538 part/views.py:1541 msgid "Select valid part" msgstr "Bitte ein gültiges Teil auswählen" -#: part/views.py:1537 +#: part/views.py:1547 msgid "Duplicate part selected" msgstr "Teil doppelt ausgewählt" -#: part/views.py:1575 +#: part/views.py:1585 msgid "Select a part" msgstr "Teil auswählen" -#: part/views.py:1581 +#: part/views.py:1591 #, fuzzy #| msgid "Select part to be used in BOM" msgid "Selected part creates a circular BOM" msgstr "Teil für die Nutzung in der Stückliste auswählen" -#: part/views.py:1585 +#: part/views.py:1595 msgid "Specify quantity" msgstr "Anzahl angeben" -#: part/views.py:1841 +#: part/views.py:1851 msgid "Confirm Part Deletion" msgstr "Löschen des Teils bestätigen" -#: part/views.py:1850 +#: part/views.py:1860 msgid "Part was deleted" msgstr "Teil wurde gelöscht" -#: part/views.py:1859 +#: part/views.py:1869 msgid "Part Pricing" msgstr "Teilbepreisung" -#: part/views.py:1973 +#: part/views.py:1983 msgid "Create Part Parameter Template" msgstr "Teilparametervorlage anlegen" -#: part/views.py:1983 +#: part/views.py:1993 msgid "Edit Part Parameter Template" msgstr "Teilparametervorlage bearbeiten" -#: part/views.py:1992 +#: part/views.py:2002 msgid "Delete Part Parameter Template" msgstr "Teilparametervorlage löschen" -#: part/views.py:2002 +#: part/views.py:2012 msgid "Create Part Parameter" msgstr "Teilparameter anlegen" -#: part/views.py:2054 +#: part/views.py:2064 msgid "Edit Part Parameter" msgstr "Teilparameter bearbeiten" -#: part/views.py:2070 +#: part/views.py:2080 msgid "Delete Part Parameter" msgstr "Teilparameter löschen" -#: part/views.py:2129 +#: part/views.py:2139 msgid "Edit Part Category" msgstr "Teilkategorie bearbeiten" -#: part/views.py:2166 +#: part/views.py:2176 msgid "Delete Part Category" msgstr "Teilkategorie löschen" -#: part/views.py:2174 +#: part/views.py:2184 msgid "Part category was deleted" msgstr "Teilekategorie wurde gelöscht" -#: part/views.py:2230 +#: part/views.py:2240 #, fuzzy #| msgid "Create Part Parameter Template" msgid "Create Category Parameter Template" msgstr "Teilparametervorlage anlegen" -#: part/views.py:2333 +#: part/views.py:2343 #, fuzzy #| msgid "Edit Part Parameter Template" msgid "Edit Category Parameter Template" msgstr "Teilparametervorlage bearbeiten" -#: part/views.py:2391 +#: part/views.py:2401 #, fuzzy #| msgid "Delete Part Parameter Template" msgid "Delete Category Parameter Template" msgstr "Teilparametervorlage löschen" -#: part/views.py:2416 +#: part/views.py:2426 #, fuzzy #| msgid "Create BOM item" msgid "Create BOM Item" msgstr "BOM-Position anlegen" -#: part/views.py:2488 +#: part/views.py:2498 msgid "Edit BOM item" msgstr "BOM-Position beaarbeiten" -#: part/views.py:2545 +#: part/views.py:2555 msgid "Confim BOM item deletion" msgstr "Löschung von BOM-Position bestätigen" @@ -4188,346 +4282,358 @@ msgstr "" msgid "Asset file description" msgstr "Einstellungs-Beschreibung" -#: stock/forms.py:111 +#: stock/forms.py:116 msgid "Enter unique serial numbers (or leave blank)" msgstr "Eindeutige Seriennummern eingeben (oder leer lassen)" -#: stock/forms.py:192 +#: stock/forms.py:198 msgid "Label" msgstr "" -#: stock/forms.py:193 stock/forms.py:249 +#: stock/forms.py:199 stock/forms.py:255 #, fuzzy #| msgid "Select stock item to allocate" msgid "Select test report template" msgstr "Lagerobjekt für Zuordnung auswählen" -#: stock/forms.py:257 +#: stock/forms.py:263 msgid "Include stock items in sub locations" msgstr "Lagerobjekte in untergeordneten Lagerorten einschließen" -#: stock/forms.py:292 +#: stock/forms.py:298 #, fuzzy #| msgid "No stock items matching query" msgid "Stock item to install" msgstr "Keine zur Anfrage passenden Lagerobjekte" -#: stock/forms.py:299 +#: stock/forms.py:305 #, fuzzy #| msgid "Stock Quantity" msgid "Stock quantity to assign" msgstr "Bestand" -#: stock/forms.py:327 +#: stock/forms.py:333 #, fuzzy #| msgid "Quantity must not exceed available stock quantity ({n})" msgid "Must not exceed available quantity" msgstr "Anzahl darf nicht die verfügbare Anzahl überschreiten ({n})" -#: stock/forms.py:337 +#: stock/forms.py:343 #, fuzzy #| msgid "Does this part have tracking for unique items?" msgid "Destination location for uninstalled items" msgstr "Hat dieses Teil Tracking für einzelne Objekte?" -#: stock/forms.py:339 +#: stock/forms.py:345 #, fuzzy #| msgid "Description of the company" msgid "Add transaction note (optional)" msgstr "Firmenbeschreibung" -#: stock/forms.py:341 +#: stock/forms.py:347 #, fuzzy #| msgid "Confirm stock allocation" msgid "Confirm uninstall" msgstr "Lagerbestandszuordnung bestätigen" -#: stock/forms.py:341 +#: stock/forms.py:347 #, fuzzy #| msgid "Confirm movement of stock items" msgid "Confirm removal of installed stock items" msgstr "Bewegung der Lagerobjekte bestätigen" -#: stock/forms.py:365 +#: stock/forms.py:371 msgid "Destination stock location" msgstr "Ziel-Lagerbestand" -#: stock/forms.py:367 +#: stock/forms.py:373 msgid "Add note (required)" msgstr "" -#: stock/forms.py:371 stock/views.py:935 stock/views.py:1133 +#: stock/forms.py:377 stock/views.py:935 stock/views.py:1133 msgid "Confirm stock adjustment" msgstr "Bestands-Anpassung bestätigen" -#: stock/forms.py:371 +#: stock/forms.py:377 msgid "Confirm movement of stock items" msgstr "Bewegung der Lagerobjekte bestätigen" -#: stock/forms.py:373 +#: stock/forms.py:379 #, fuzzy #| msgid "Default Location" msgid "Set Default Location" msgstr "Standard-Lagerort" -#: stock/forms.py:373 +#: stock/forms.py:379 msgid "Set the destination as the default location for selected parts" msgstr "Setze das Ziel als Standard-Ziel für ausgewählte Teile" -#: stock/models.py:179 +#: stock/models.py:185 #, fuzzy #| msgid "Created new stock item" msgid "Created stock item" msgstr "Neues Lagerobjekt erstellt" -#: stock/models.py:215 +#: stock/models.py:221 #, fuzzy #| msgid "A stock item with this serial number already exists" msgid "StockItem with this serial number already exists" msgstr "Ein Teil mit dieser Seriennummer existiert bereits" -#: stock/models.py:251 +#: stock/models.py:257 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "Teile-Typ ('{pf}') muss {pe} sein" -#: stock/models.py:261 stock/models.py:270 +#: stock/models.py:267 stock/models.py:276 msgid "Quantity must be 1 for item with a serial number" msgstr "Anzahl muss für Objekte mit Seriennummer \"1\" sein" -#: stock/models.py:262 +#: stock/models.py:268 msgid "Serial number cannot be set if quantity greater than 1" msgstr "" "Seriennummer kann nicht gesetzt werden wenn die Anzahl größer als \"1\" ist" -#: stock/models.py:284 +#: stock/models.py:290 msgid "Item cannot belong to itself" msgstr "Teil kann nicht zu sich selbst gehören" -#: stock/models.py:290 +#: stock/models.py:296 msgid "Item must have a build reference if is_building=True" msgstr "" -#: stock/models.py:297 +#: stock/models.py:303 msgid "Build reference does not point to the same part object" msgstr "" -#: stock/models.py:330 +#: stock/models.py:336 msgid "Parent Stock Item" msgstr "Eltern-Lagerobjekt" -#: stock/models.py:339 +#: stock/models.py:345 msgid "Base part" msgstr "Basis-Teil" -#: stock/models.py:348 +#: stock/models.py:354 msgid "Select a matching supplier part for this stock item" msgstr "Passenden Zulieferer für dieses Lagerobjekt auswählen" -#: stock/models.py:353 stock/templates/stock/stock_app_base.html:7 +#: stock/models.py:359 stock/templates/stock/stock_app_base.html:7 msgid "Stock Location" msgstr "Lagerort" -#: stock/models.py:356 +#: stock/models.py:362 msgid "Where is this stock item located?" msgstr "Wo wird dieses Teil normalerweise gelagert?" -#: stock/models.py:361 stock/templates/stock/item_base.html:212 +#: stock/models.py:367 stock/templates/stock/item_base.html:219 msgid "Installed In" msgstr "Installiert in" -#: stock/models.py:364 +#: stock/models.py:370 msgid "Is this item installed in another item?" msgstr "Ist dieses Teil in einem anderen verbaut?" -#: stock/models.py:380 +#: stock/models.py:386 msgid "Serial number for this item" msgstr "Seriennummer für dieses Teil" -#: stock/models.py:392 +#: stock/models.py:398 msgid "Batch code for this stock item" msgstr "Losnummer für dieses Lagerobjekt" -#: stock/models.py:396 +#: stock/models.py:402 msgid "Stock Quantity" msgstr "Bestand" -#: stock/models.py:405 +#: stock/models.py:411 msgid "Source Build" msgstr "Quellbau" -#: stock/models.py:407 +#: stock/models.py:413 msgid "Build for this stock item" msgstr "Bau für dieses Lagerobjekt" -#: stock/models.py:418 +#: stock/models.py:424 msgid "Source Purchase Order" msgstr "Quellbestellung" -#: stock/models.py:421 +#: stock/models.py:427 msgid "Purchase order for this stock item" msgstr "Bestellung für dieses Teil" -#: stock/models.py:427 +#: stock/models.py:433 msgid "Destination Sales Order" msgstr "Zielauftrag" -#: stock/models.py:439 +#: stock/models.py:439 stock/templates/stock/item_base.html:306 +#: templates/js/stock.js:597 +#, fuzzy +#| msgid "Export" +msgid "Expiry Date" +msgstr "Exportieren" + +#: stock/models.py:440 +msgid "" +"Expiry date for stock item. Stock will be considered expired after this date" +msgstr "" + +#: stock/models.py:453 msgid "Delete this Stock Item when stock is depleted" msgstr "Objekt löschen wenn Lagerbestand aufgebraucht" -#: stock/models.py:449 stock/templates/stock/item_notes.html:14 +#: stock/models.py:463 stock/templates/stock/item_notes.html:14 #: stock/templates/stock/item_notes.html:30 msgid "Stock Item Notes" msgstr "Lagerobjekt-Notizen" -#: stock/models.py:459 +#: stock/models.py:473 msgid "Single unit purchase price at time of purchase" msgstr "" -#: stock/models.py:510 +#: stock/models.py:573 #, fuzzy #| msgid "Item assigned to customer?" msgid "Assigned to Customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/models.py:512 +#: stock/models.py:575 #, fuzzy #| msgid "Item assigned to customer?" msgid "Manually assigned to customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/models.py:525 +#: stock/models.py:588 #, fuzzy #| msgid "Item assigned to customer?" msgid "Returned from customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/models.py:527 +#: stock/models.py:590 #, fuzzy #| msgid "Create new stock location" msgid "Returned to location" msgstr "Neuen Lagerort anlegen" -#: stock/models.py:652 +#: stock/models.py:715 #, fuzzy #| msgid "Installed in Stock Item" msgid "Installed into stock item" msgstr "In Lagerobjekt installiert" -#: stock/models.py:660 +#: stock/models.py:723 #, fuzzy #| msgid "Installed in Stock Item" msgid "Installed stock item" msgstr "In Lagerobjekt installiert" -#: stock/models.py:684 +#: stock/models.py:747 #, fuzzy #| msgid "Installed in Stock Item" msgid "Uninstalled stock item" msgstr "In Lagerobjekt installiert" -#: stock/models.py:703 +#: stock/models.py:766 #, fuzzy #| msgid "Include sublocations" msgid "Uninstalled into location" msgstr "Unterlagerorte einschließen" -#: stock/models.py:803 +#: stock/models.py:846 #, fuzzy #| msgid "Part is not a virtual part" msgid "Part is not set as trackable" msgstr "Teil ist nicht virtuell" -#: stock/models.py:809 +#: stock/models.py:852 msgid "Quantity must be integer" msgstr "Anzahl muss eine Ganzzahl sein" -#: stock/models.py:815 +#: stock/models.py:858 #, python-brace-format msgid "Quantity must not exceed available stock quantity ({n})" msgstr "Anzahl darf nicht die verfügbare Anzahl überschreiten ({n})" -#: stock/models.py:818 +#: stock/models.py:861 msgid "Serial numbers must be a list of integers" msgstr "Seriennummern muss eine Liste von Ganzzahlen sein" -#: stock/models.py:821 +#: stock/models.py:864 msgid "Quantity does not match serial numbers" msgstr "Anzahl stimmt nicht mit den Seriennummern überein" -#: stock/models.py:853 +#: stock/models.py:896 msgid "Add serial number" msgstr "Seriennummer hinzufügen" -#: stock/models.py:856 +#: stock/models.py:899 #, python-brace-format msgid "Serialized {n} items" msgstr "{n} Teile serialisiert" -#: stock/models.py:967 +#: stock/models.py:1010 msgid "StockItem cannot be moved as it is not in stock" msgstr "Lagerobjekt kann nicht bewegt werden, da kein Bestand vorhanden ist" -#: stock/models.py:1321 +#: stock/models.py:1364 msgid "Tracking entry title" msgstr "Name des Eintrags-Trackings" -#: stock/models.py:1323 +#: stock/models.py:1366 msgid "Entry notes" msgstr "Eintrags-Notizen" -#: stock/models.py:1325 +#: stock/models.py:1368 msgid "Link to external page for further information" msgstr "Link auf externe Seite für weitere Informationen" -#: stock/models.py:1385 +#: stock/models.py:1428 #, fuzzy #| msgid "Serial number for this item" msgid "Value must be provided for this test" msgstr "Seriennummer für dieses Teil" -#: stock/models.py:1391 +#: stock/models.py:1434 msgid "Attachment must be uploaded for this test" msgstr "" -#: stock/models.py:1408 +#: stock/models.py:1451 msgid "Test" msgstr "" -#: stock/models.py:1409 +#: stock/models.py:1452 #, fuzzy #| msgid "Part name" msgid "Test name" msgstr "Name des Teils" -#: stock/models.py:1414 +#: stock/models.py:1457 #, fuzzy #| msgid "Search Results" msgid "Result" msgstr "Suchergebnisse" -#: stock/models.py:1415 templates/js/table_filters.js:162 +#: stock/models.py:1458 templates/js/table_filters.js:172 msgid "Test result" msgstr "" -#: stock/models.py:1421 +#: stock/models.py:1464 msgid "Test output value" msgstr "" -#: stock/models.py:1427 +#: stock/models.py:1470 #, fuzzy #| msgid "Attachments" msgid "Attachment" msgstr "Anhänge" -#: stock/models.py:1428 +#: stock/models.py:1471 #, fuzzy #| msgid "Delete attachment" msgid "Test result attachment" msgstr "Anhang löschen" -#: stock/models.py:1434 +#: stock/models.py:1477 #, fuzzy #| msgid "Edit notes" msgid "Test notes" @@ -4592,137 +4698,159 @@ msgstr "" "Dieses Lagerobjekt wird automatisch gelöscht wenn der Lagerbestand " "aufgebraucht ist." -#: stock/templates/stock/item_base.html:107 templates/js/barcode.js:283 +#: stock/templates/stock/item_base.html:74 +#: stock/templates/stock/item_base.html:310 templates/js/table_filters.js:111 +msgid "Expired" +msgstr "" + +#: stock/templates/stock/item_base.html:78 +#: stock/templates/stock/item_base.html:312 templates/js/table_filters.js:116 +msgid "Stale" +msgstr "" + +#: stock/templates/stock/item_base.html:114 templates/js/barcode.js:283 #: templates/js/barcode.js:288 msgid "Unlink Barcode" msgstr "" -#: stock/templates/stock/item_base.html:109 +#: stock/templates/stock/item_base.html:116 msgid "Link Barcode" msgstr "" -#: stock/templates/stock/item_base.html:117 +#: stock/templates/stock/item_base.html:124 #, fuzzy #| msgid "Confirm stock adjustment" msgid "Stock adjustment actions" msgstr "Bestands-Anpassung bestätigen" -#: stock/templates/stock/item_base.html:121 +#: stock/templates/stock/item_base.html:128 #: stock/templates/stock/location.html:41 templates/stock_table.html:23 msgid "Count stock" msgstr "Bestand zählen" -#: stock/templates/stock/item_base.html:122 templates/stock_table.html:21 +#: stock/templates/stock/item_base.html:129 templates/stock_table.html:21 msgid "Add stock" msgstr "Bestand hinzufügen" -#: stock/templates/stock/item_base.html:123 templates/stock_table.html:22 +#: stock/templates/stock/item_base.html:130 templates/stock_table.html:22 msgid "Remove stock" msgstr "Bestand entfernen" -#: stock/templates/stock/item_base.html:125 +#: stock/templates/stock/item_base.html:132 #, fuzzy #| msgid "Order stock" msgid "Transfer stock" msgstr "Bestand bestellen" -#: stock/templates/stock/item_base.html:127 +#: stock/templates/stock/item_base.html:134 #, fuzzy #| msgid "Serialize Stock" msgid "Serialize stock" msgstr "Lagerbestand erfassen" -#: stock/templates/stock/item_base.html:131 +#: stock/templates/stock/item_base.html:138 #, fuzzy #| msgid "Item assigned to customer?" msgid "Assign to customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/templates/stock/item_base.html:134 +#: stock/templates/stock/item_base.html:141 #, fuzzy #| msgid "Count stock" msgid "Return to stock" msgstr "Bestand zählen" -#: stock/templates/stock/item_base.html:138 templates/js/stock.js:991 +#: stock/templates/stock/item_base.html:145 templates/js/stock.js:1017 #, fuzzy #| msgid "Installed in Stock Item" msgid "Uninstall stock item" msgstr "In Lagerobjekt installiert" -#: stock/templates/stock/item_base.html:138 +#: stock/templates/stock/item_base.html:145 msgid "Uninstall" msgstr "" -#: stock/templates/stock/item_base.html:147 +#: stock/templates/stock/item_base.html:154 #: stock/templates/stock/location.html:38 #, fuzzy #| msgid "Stock Locations" msgid "Stock actions" msgstr "Lagerobjekt-Standorte" -#: stock/templates/stock/item_base.html:150 +#: stock/templates/stock/item_base.html:157 #, fuzzy #| msgid "Count stock items" msgid "Convert to variant" msgstr "Lagerobjekte zählen" -#: stock/templates/stock/item_base.html:153 +#: stock/templates/stock/item_base.html:160 #, fuzzy #| msgid "Count stock items" msgid "Duplicate stock item" msgstr "Lagerobjekte zählen" -#: stock/templates/stock/item_base.html:155 +#: stock/templates/stock/item_base.html:162 #, fuzzy #| msgid "Edit Stock Item" msgid "Edit stock item" msgstr "Lagerobjekt bearbeiten" -#: stock/templates/stock/item_base.html:158 +#: stock/templates/stock/item_base.html:165 #, fuzzy #| msgid "Delete Stock Item" msgid "Delete stock item" msgstr "Lagerobjekt löschen" -#: stock/templates/stock/item_base.html:164 +#: stock/templates/stock/item_base.html:171 msgid "Generate test report" msgstr "" -#: stock/templates/stock/item_base.html:172 +#: stock/templates/stock/item_base.html:179 msgid "Stock Item Details" msgstr "Lagerbestands-Details" -#: stock/templates/stock/item_base.html:231 templates/js/build.js:442 +#: stock/templates/stock/item_base.html:238 templates/js/build.js:442 #, fuzzy #| msgid "No stock location set" msgid "No location set" msgstr "Kein Lagerort gesetzt" -#: stock/templates/stock/item_base.html:238 +#: stock/templates/stock/item_base.html:245 #, fuzzy #| msgid "Unique Identifier" msgid "Barcode Identifier" msgstr "Eindeutiger Bezeichner" -#: stock/templates/stock/item_base.html:252 templates/js/build.js:642 +#: stock/templates/stock/item_base.html:259 templates/js/build.js:642 #: templates/navbar.html:25 msgid "Build" msgstr "Bau" -#: stock/templates/stock/item_base.html:273 +#: stock/templates/stock/item_base.html:280 msgid "Parent Item" msgstr "Elternposition" -#: stock/templates/stock/item_base.html:298 +#: stock/templates/stock/item_base.html:310 +#, fuzzy +#| msgid "This stock item is allocated to Build" +msgid "This StockItem expired on" +msgstr "Dieses Lagerobjekt ist dem Bau zugewiesen" + +#: stock/templates/stock/item_base.html:312 +#, fuzzy +#| msgid "Child Stock Items" +msgid "This StockItem expires on" +msgstr "Kind-Lagerobjekte" + +#: stock/templates/stock/item_base.html:319 msgid "Last Updated" msgstr "Zuletzt aktualisiert" -#: stock/templates/stock/item_base.html:303 +#: stock/templates/stock/item_base.html:324 msgid "Last Stocktake" msgstr "Letzte Inventur" -#: stock/templates/stock/item_base.html:307 +#: stock/templates/stock/item_base.html:328 msgid "No stocktake performed" msgstr "Keine Inventur ausgeführt" @@ -4866,7 +4994,7 @@ msgstr "Sind Sie sicher, dass Sie diesen Anhang löschen wollen?" msgid "The following stock items will be uninstalled" msgstr "Die folgenden Objekte werden erstellt" -#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1331 +#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1335 #, fuzzy #| msgid "Count Stock Items" msgid "Convert Stock Item" @@ -5104,43 +5232,43 @@ msgstr "{n} Teile im Lager gelöscht" msgid "Edit Stock Item" msgstr "Lagerobjekt bearbeiten" -#: stock/views.py:1381 +#: stock/views.py:1385 msgid "Serialize Stock" msgstr "Lagerbestand erfassen" -#: stock/views.py:1475 templates/js/build.js:210 +#: stock/views.py:1479 templates/js/build.js:210 msgid "Create new Stock Item" msgstr "Neues Lagerobjekt hinzufügen" -#: stock/views.py:1579 +#: stock/views.py:1587 #, fuzzy #| msgid "Count stock items" msgid "Duplicate Stock Item" msgstr "Lagerobjekte zählen" -#: stock/views.py:1651 +#: stock/views.py:1664 #, fuzzy #| msgid "Quantity must be greater than zero" msgid "Quantity cannot be negative" msgstr "Anzahl muss größer Null sein" -#: stock/views.py:1737 +#: stock/views.py:1750 msgid "Delete Stock Location" msgstr "Standort löschen" -#: stock/views.py:1751 +#: stock/views.py:1764 msgid "Delete Stock Item" msgstr "Lagerobjekt löschen" -#: stock/views.py:1763 +#: stock/views.py:1776 msgid "Delete Stock Tracking Entry" msgstr "Lagerbestands-Tracking-Eintrag löschen" -#: stock/views.py:1782 +#: stock/views.py:1795 msgid "Edit Stock Tracking Entry" msgstr "Lagerbestands-Tracking-Eintrag bearbeiten" -#: stock/views.py:1792 +#: stock/views.py:1805 msgid "Add Stock Tracking Entry" msgstr "Lagerbestands-Tracking-Eintrag hinzufügen" @@ -5180,7 +5308,13 @@ msgstr "Eltern-Bau" msgid "Pending Builds" msgstr "Eltern-Bau" -#: templates/InvenTree/index.html:4 +#: templates/InvenTree/expired_stock.html:7 +#, fuzzy +#| msgid "Assigned" +msgid "Expired Stock" +msgstr "Zugewiesen" + +#: templates/InvenTree/index.html:5 msgid "Index" msgstr "" @@ -5218,13 +5352,13 @@ msgstr "Keine Ergebnisse gefunden" msgid "Enter a search query" msgstr "Auftrag stornieren" -#: templates/InvenTree/search.html:191 templates/js/stock.js:289 +#: templates/InvenTree/search.html:191 templates/js/stock.js:290 #, fuzzy #| msgid "Item assigned to customer?" msgid "Shipped to customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: templates/InvenTree/search.html:194 templates/js/stock.js:299 +#: templates/InvenTree/search.html:194 templates/js/stock.js:300 msgid "No stock location set" msgstr "Kein Lagerort gesetzt" @@ -5265,12 +5399,12 @@ msgid "Default Value" msgstr "Standard-Lagerort" #: templates/InvenTree/settings/category.html:70 -#: templates/InvenTree/settings/part.html:75 +#: templates/InvenTree/settings/part.html:78 msgid "Edit Template" msgstr "Vorlage bearbeiten" #: templates/InvenTree/settings/category.html:71 -#: templates/InvenTree/settings/part.html:76 +#: templates/InvenTree/settings/part.html:79 msgid "Delete Template" msgstr "Vorlage löschen" @@ -5280,6 +5414,12 @@ msgstr "Vorlage löschen" msgid "Global InvenTree Settings" msgstr "InvenTree-Version" +#: templates/InvenTree/settings/header.html:7 +#, fuzzy +#| msgid "Settings" +msgid "Setting" +msgstr "Einstellungen" + #: templates/InvenTree/settings/part.html:9 #, fuzzy #| msgid "Settings" @@ -5292,13 +5432,13 @@ msgstr "Einstellungen" msgid "Part Options" msgstr "Quell-Standort" -#: templates/InvenTree/settings/part.html:34 +#: templates/InvenTree/settings/part.html:37 #, fuzzy #| msgid "Edit Part Parameter Template" msgid "Part Parameter Templates" msgstr "Teilparametervorlage bearbeiten" -#: templates/InvenTree/settings/part.html:55 +#: templates/InvenTree/settings/part.html:58 msgid "No part parameter templates found" msgstr "Keine Teilparametervorlagen gefunden" @@ -5308,11 +5448,11 @@ msgstr "Keine Teilparametervorlagen gefunden" msgid "Purchase Order Settings" msgstr "Bestelldetails" -#: templates/InvenTree/settings/setting.html:16 +#: templates/InvenTree/settings/setting.html:23 msgid "No value set" msgstr "" -#: templates/InvenTree/settings/setting.html:24 +#: templates/InvenTree/settings/setting.html:31 #, fuzzy #| msgid "Settings" msgid "Edit setting" @@ -5335,6 +5475,12 @@ msgstr "Auftragsdetails" msgid "Stock Settings" msgstr "Lagerobjekt-Standorte" +#: templates/InvenTree/settings/stock.html:13 +#, fuzzy +#| msgid "Stock Locations" +msgid "Stock Options" +msgstr "Lagerobjekt-Standorte" + #: templates/InvenTree/settings/tabs.html:3 #: templates/InvenTree/settings/user.html:10 #, fuzzy @@ -5434,6 +5580,12 @@ msgstr "Zielauftrag" msgid "Overdue Sales Orders" msgstr "Bestellungen" +#: templates/InvenTree/stale_stock.html:7 +#, fuzzy +#| msgid "Serialize Stock" +msgid "Stale Stock" +msgstr "Lagerbestand erfassen" + #: templates/InvenTree/starred_parts.html:7 msgid "Starred Parts" msgstr "Teilfavoriten" @@ -5747,7 +5899,7 @@ msgstr "Baugruppe" msgid "No purchase orders found" msgstr "Keine Bestellungen gefunden" -#: templates/js/order.js:188 templates/js/stock.js:687 +#: templates/js/order.js:188 templates/js/stock.js:702 msgid "Date" msgstr "Datum" @@ -5795,8 +5947,8 @@ msgstr "Keine Teile gefunden" msgid "No parts found" msgstr "Keine Teile gefunden" -#: templates/js/part.js:343 templates/js/stock.js:462 -#: templates/js/stock.js:1023 +#: templates/js/part.js:343 templates/js/stock.js:463 +#: templates/js/stock.js:1049 msgid "Select" msgstr "Auswählen" @@ -5804,7 +5956,7 @@ msgstr "Auswählen" msgid "No category" msgstr "Keine Kategorie" -#: templates/js/part.js:429 templates/js/table_filters.js:264 +#: templates/js/part.js:429 templates/js/table_filters.js:274 msgid "Low stock" msgstr "Bestand niedrig" @@ -5826,13 +5978,13 @@ msgstr "" msgid "No test templates matching query" msgstr "Keine zur Anfrage passenden Lagerobjekte" -#: templates/js/part.js:604 templates/js/stock.js:63 +#: templates/js/part.js:604 templates/js/stock.js:64 #, fuzzy #| msgid "Edit Sales Order" msgid "Edit test result" msgstr "Auftrag bearbeiten" -#: templates/js/part.js:605 templates/js/stock.js:64 +#: templates/js/part.js:605 templates/js/stock.js:65 #, fuzzy #| msgid "Delete attachment" msgid "Delete test result" @@ -5842,137 +5994,149 @@ msgstr "Anhang löschen" msgid "This test is defined for a parent part" msgstr "" -#: templates/js/stock.js:26 +#: templates/js/stock.js:27 msgid "PASS" msgstr "" -#: templates/js/stock.js:28 +#: templates/js/stock.js:29 msgid "FAIL" msgstr "" -#: templates/js/stock.js:33 +#: templates/js/stock.js:34 msgid "NO RESULT" msgstr "" -#: templates/js/stock.js:59 +#: templates/js/stock.js:60 #, fuzzy #| msgid "Edit Sales Order" msgid "Add test result" msgstr "Auftrag bearbeiten" -#: templates/js/stock.js:78 +#: templates/js/stock.js:79 #, fuzzy #| msgid "No results found" msgid "No test results found" msgstr "Keine Ergebnisse gefunden" -#: templates/js/stock.js:120 +#: templates/js/stock.js:121 #, fuzzy #| msgid "Shipment Date" msgid "Test Date" msgstr "Versanddatum" -#: templates/js/stock.js:281 +#: templates/js/stock.js:282 msgid "In production" msgstr "" -#: templates/js/stock.js:285 +#: templates/js/stock.js:286 #, fuzzy #| msgid "Installed in Stock Item" msgid "Installed in Stock Item" msgstr "In Lagerobjekt installiert" -#: templates/js/stock.js:293 +#: templates/js/stock.js:294 #, fuzzy #| msgid "Item assigned to customer?" msgid "Assigned to Sales Order" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: templates/js/stock.js:313 +#: templates/js/stock.js:314 msgid "No stock items matching query" msgstr "Keine zur Anfrage passenden Lagerobjekte" -#: templates/js/stock.js:430 +#: templates/js/stock.js:431 #, fuzzy #| msgid "Include sublocations" msgid "Undefined location" msgstr "Unterlagerorte einschließen" -#: templates/js/stock.js:524 +#: templates/js/stock.js:525 #, fuzzy #| msgid "StockItem is lost" msgid "Stock item is in production" msgstr "Lagerobjekt verloren" -#: templates/js/stock.js:529 +#: templates/js/stock.js:530 #, fuzzy #| msgid "This stock item is allocated to Sales Order" msgid "Stock item assigned to sales order" msgstr "Dieses Lagerobjekt ist dem Auftrag zugewiesen" -#: templates/js/stock.js:532 +#: templates/js/stock.js:533 #, fuzzy #| msgid "StockItem has been allocated" msgid "Stock item assigned to customer" msgstr "Lagerobjekt wurde zugewiesen" -#: templates/js/stock.js:536 +#: templates/js/stock.js:537 +#, fuzzy +#| msgid "StockItem has been allocated" +msgid "Stock item has expired" +msgstr "Lagerobjekt wurde zugewiesen" + +#: templates/js/stock.js:539 +#, fuzzy +#| msgid "StockItem is lost" +msgid "Stock item will expire soon" +msgstr "Lagerobjekt verloren" + +#: templates/js/stock.js:543 #, fuzzy #| msgid "StockItem has been allocated" msgid "Stock item has been allocated" msgstr "Lagerobjekt wurde zugewiesen" -#: templates/js/stock.js:540 +#: templates/js/stock.js:547 #, fuzzy #| msgid "Is this item installed in another item?" msgid "Stock item has been installed in another item" msgstr "Ist dieses Teil in einem anderen verbaut?" -#: templates/js/stock.js:548 +#: templates/js/stock.js:555 #, fuzzy #| msgid "StockItem has been allocated" msgid "Stock item has been rejected" msgstr "Lagerobjekt wurde zugewiesen" -#: templates/js/stock.js:552 +#: templates/js/stock.js:559 #, fuzzy #| msgid "StockItem is lost" msgid "Stock item is lost" msgstr "Lagerobjekt verloren" -#: templates/js/stock.js:555 +#: templates/js/stock.js:562 #, fuzzy #| msgid "StockItem is lost" msgid "Stock item is destroyed" msgstr "Lagerobjekt verloren" -#: templates/js/stock.js:559 templates/js/table_filters.js:106 +#: templates/js/stock.js:566 templates/js/table_filters.js:106 #, fuzzy #| msgid "Delete" msgid "Depleted" msgstr "Löschen" -#: templates/js/stock.js:753 +#: templates/js/stock.js:768 msgid "No user information" msgstr "Keine Benutzerinformation" -#: templates/js/stock.js:862 +#: templates/js/stock.js:888 msgid "Create New Location" msgstr "Neuen Standort anlegen" -#: templates/js/stock.js:961 +#: templates/js/stock.js:987 #, fuzzy #| msgid "Serial Number" msgid "Serial" msgstr "Seriennummer" -#: templates/js/stock.js:1054 templates/js/table_filters.js:121 +#: templates/js/stock.js:1080 templates/js/table_filters.js:131 #, fuzzy #| msgid "Installed In" msgid "Installed" msgstr "Installiert in" -#: templates/js/stock.js:1079 +#: templates/js/stock.js:1105 #, fuzzy #| msgid "Installed In" msgid "Install item" @@ -5990,50 +6154,50 @@ msgstr "nachverfolgbar" msgid "Validated" msgstr "BOM validieren" -#: templates/js/table_filters.js:65 templates/js/table_filters.js:131 +#: templates/js/table_filters.js:65 templates/js/table_filters.js:141 #, fuzzy #| msgid "Serialize Stock" msgid "Is Serialized" msgstr "Lagerbestand erfassen" -#: templates/js/table_filters.js:68 templates/js/table_filters.js:138 +#: templates/js/table_filters.js:68 templates/js/table_filters.js:148 #, fuzzy #| msgid "Serial Number" msgid "Serial number GTE" msgstr "Seriennummer" -#: templates/js/table_filters.js:69 templates/js/table_filters.js:139 +#: templates/js/table_filters.js:69 templates/js/table_filters.js:149 #, fuzzy #| msgid "Serial number for this item" msgid "Serial number greater than or equal to" msgstr "Seriennummer für dieses Teil" -#: templates/js/table_filters.js:72 templates/js/table_filters.js:142 +#: templates/js/table_filters.js:72 templates/js/table_filters.js:152 #, fuzzy #| msgid "Serial Number" msgid "Serial number LTE" msgstr "Seriennummer" -#: templates/js/table_filters.js:73 templates/js/table_filters.js:143 +#: templates/js/table_filters.js:73 templates/js/table_filters.js:153 #, fuzzy #| msgid "Serial numbers already exist: " msgid "Serial number less than or equal to" msgstr "Seriennummern existieren bereits:" #: templates/js/table_filters.js:76 templates/js/table_filters.js:77 -#: templates/js/table_filters.js:134 templates/js/table_filters.js:135 +#: templates/js/table_filters.js:144 templates/js/table_filters.js:145 #, fuzzy #| msgid "Serial Number" msgid "Serial number" msgstr "Seriennummer" -#: templates/js/table_filters.js:81 templates/js/table_filters.js:152 +#: templates/js/table_filters.js:81 templates/js/table_filters.js:162 #, fuzzy #| msgid "Batch Code" msgid "Batch code" msgstr "Losnummer" -#: templates/js/table_filters.js:91 templates/js/table_filters.js:231 +#: templates/js/table_filters.js:91 templates/js/table_filters.js:241 msgid "Active parts" msgstr "Aktive Teile" @@ -6064,84 +6228,96 @@ msgid "Show stock items which are depleted" msgstr "Objekt löschen wenn Lagerbestand aufgebraucht" #: templates/js/table_filters.js:112 +#, fuzzy +#| msgid "Delete this Stock Item when stock is depleted" +msgid "Show stock items which have expired" +msgstr "Objekt löschen wenn Lagerbestand aufgebraucht" + +#: templates/js/table_filters.js:117 +#, fuzzy +#| msgid "Delete this Stock Item when stock is depleted" +msgid "Show stock which is close to expiring" +msgstr "Objekt löschen wenn Lagerbestand aufgebraucht" + +#: templates/js/table_filters.js:122 msgid "Show items which are in stock" msgstr "" -#: templates/js/table_filters.js:116 +#: templates/js/table_filters.js:126 msgid "In Production" msgstr "" -#: templates/js/table_filters.js:117 +#: templates/js/table_filters.js:127 #, fuzzy #| msgid "Delete this Stock Item when stock is depleted" msgid "Show items which are in production" msgstr "Objekt löschen wenn Lagerbestand aufgebraucht" -#: templates/js/table_filters.js:122 +#: templates/js/table_filters.js:132 #, fuzzy #| msgid "Is this item installed in another item?" msgid "Show stock items which are installed in another item" msgstr "Ist dieses Teil in einem anderen verbaut?" -#: templates/js/table_filters.js:126 +#: templates/js/table_filters.js:136 #, fuzzy #| msgid "Item assigned to customer?" msgid "Sent to customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: templates/js/table_filters.js:127 +#: templates/js/table_filters.js:137 msgid "Show items which have been assigned to a customer" msgstr "" -#: templates/js/table_filters.js:147 templates/js/table_filters.js:148 +#: templates/js/table_filters.js:157 templates/js/table_filters.js:158 msgid "Stock status" msgstr "Bestandsstatus" -#: templates/js/table_filters.js:181 +#: templates/js/table_filters.js:191 msgid "Build status" msgstr "Bau-Status" -#: templates/js/table_filters.js:200 templates/js/table_filters.js:213 +#: templates/js/table_filters.js:210 templates/js/table_filters.js:223 msgid "Order status" msgstr "Bestellstatus" -#: templates/js/table_filters.js:205 templates/js/table_filters.js:218 +#: templates/js/table_filters.js:215 templates/js/table_filters.js:228 #, fuzzy #| msgid "Cascading" msgid "Outstanding" msgstr "Kaskadierend" -#: templates/js/table_filters.js:241 +#: templates/js/table_filters.js:251 msgid "Include subcategories" msgstr "Unterkategorien einschließen" -#: templates/js/table_filters.js:242 +#: templates/js/table_filters.js:252 msgid "Include parts in subcategories" msgstr "Teile in Unterkategorien einschließen" -#: templates/js/table_filters.js:246 +#: templates/js/table_filters.js:256 msgid "Has IPN" msgstr "" -#: templates/js/table_filters.js:247 +#: templates/js/table_filters.js:257 #, fuzzy #| msgid "Internal Part Number" msgid "Part has internal part number" msgstr "Interne Teilenummer" -#: templates/js/table_filters.js:252 +#: templates/js/table_filters.js:262 msgid "Show active parts" msgstr "Aktive Teile anzeigen" -#: templates/js/table_filters.js:260 +#: templates/js/table_filters.js:270 msgid "Stock available" msgstr "Bestand verfügbar" -#: templates/js/table_filters.js:276 +#: templates/js/table_filters.js:286 msgid "Starred" msgstr "Favorit" -#: templates/js/table_filters.js:288 +#: templates/js/table_filters.js:298 msgid "Purchasable" msgstr "Käuflich" diff --git a/InvenTree/locale/en/LC_MESSAGES/django.po b/InvenTree/locale/en/LC_MESSAGES/django.po index a2b2081ed3..395f42c4e6 100644 --- a/InvenTree/locale/en/LC_MESSAGES/django.po +++ b/InvenTree/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-01-03 22:16+1100\n" +"POT-Creation-Date: 2021-01-06 23:11+1100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -55,7 +55,7 @@ msgid "Select Category" msgstr "" #: InvenTree/helpers.py:361 order/models.py:178 order/models.py:260 -#: stock/views.py:1647 +#: stock/views.py:1660 msgid "Invalid quantity provided" msgstr "" @@ -95,12 +95,12 @@ msgstr "" msgid "File comment" msgstr "" -#: InvenTree/models.py:68 templates/js/stock.js:744 +#: InvenTree/models.py:68 templates/js/stock.js:759 msgid "User" msgstr "" -#: InvenTree/models.py:106 part/templates/part/params.html:24 -#: templates/js/part.js:129 +#: InvenTree/models.py:106 part/models.py:647 +#: part/templates/part/params.html:24 templates/js/part.js:129 msgid "Name" msgstr "" @@ -295,14 +295,14 @@ msgstr "" msgid "Order target date" msgstr "" -#: build/forms.py:39 build/models.py:173 +#: build/forms.py:39 build/models.py:175 msgid "" "Target date for build completion. Build will be overdue after this date." msgstr "" #: build/forms.py:78 build/templates/build/auto_allocate.html:17 #: build/templates/build/build_base.html:83 -#: build/templates/build/detail.html:29 common/models.py:496 +#: build/templates/build/detail.html:29 common/models.py:589 #: company/forms.py:112 company/templates/company/supplier_part_pricing.html:75 #: order/templates/order/order_wizard/select_parts.html:32 #: order/templates/order/purchase_order_detail.html:179 @@ -310,13 +310,13 @@ msgstr "" #: order/templates/order/sales_order_detail.html:156 #: part/templates/part/allocation.html:16 #: part/templates/part/allocation.html:49 -#: part/templates/part/sale_prices.html:82 stock/forms.py:298 +#: part/templates/part/sale_prices.html:82 stock/forms.py:304 #: stock/templates/stock/item_base.html:40 #: stock/templates/stock/item_base.html:46 -#: stock/templates/stock/item_base.html:197 +#: stock/templates/stock/item_base.html:204 #: stock/templates/stock/stock_adjust.html:18 templates/js/barcode.js:338 -#: templates/js/bom.js:195 templates/js/build.js:420 templates/js/stock.js:735 -#: templates/js/stock.js:963 +#: templates/js/bom.js:195 templates/js/build.js:420 templates/js/stock.js:750 +#: templates/js/stock.js:989 msgid "Quantity" msgstr "" @@ -324,7 +324,7 @@ msgstr "" msgid "Enter quantity for build output" msgstr "" -#: build/forms.py:83 stock/forms.py:111 +#: build/forms.py:83 stock/forms.py:116 msgid "Serial numbers" msgstr "" @@ -372,222 +372,223 @@ msgstr "" msgid "Select quantity of stock to allocate" msgstr "" -#: build/models.py:59 build/templates/build/build_base.html:8 +#: build/models.py:61 build/templates/build/build_base.html:8 #: build/templates/build/build_base.html:35 #: part/templates/part/allocation.html:20 msgid "Build Order" msgstr "" -#: build/models.py:60 build/templates/build/index.html:6 +#: build/models.py:62 build/templates/build/index.html:6 #: build/templates/build/index.html:14 order/templates/order/so_builds.html:11 #: order/templates/order/so_tabs.html:9 part/templates/part/tabs.html:31 #: templates/InvenTree/settings/tabs.html:28 users/models.py:30 msgid "Build Orders" msgstr "" -#: build/models.py:75 +#: build/models.py:77 msgid "Build Order Reference" msgstr "" -#: build/models.py:76 order/templates/order/purchase_order_detail.html:174 +#: build/models.py:78 order/templates/order/purchase_order_detail.html:174 #: templates/js/bom.js:187 templates/js/build.js:509 msgid "Reference" msgstr "" -#: build/models.py:83 build/templates/build/detail.html:19 +#: build/models.py:85 build/templates/build/detail.html:19 #: company/models.py:359 company/templates/company/detail.html:23 #: company/templates/company/supplier_part_base.html:61 #: company/templates/company/supplier_part_detail.html:27 -#: order/templates/order/purchase_order_detail.html:161 +#: order/templates/order/purchase_order_detail.html:161 part/models.py:671 #: part/templates/part/detail.html:51 part/templates/part/set_category.html:14 -#: templates/InvenTree/search.html:147 templates/js/bom.js:180 +#: templates/InvenTree/search.html:147 +#: templates/InvenTree/settings/header.html:9 templates/js/bom.js:180 #: templates/js/bom.js:517 templates/js/build.js:664 templates/js/company.js:56 #: templates/js/order.js:175 templates/js/order.js:263 templates/js/part.js:188 #: templates/js/part.js:271 templates/js/part.js:391 templates/js/part.js:572 -#: templates/js/stock.js:500 templates/js/stock.js:716 +#: templates/js/stock.js:501 templates/js/stock.js:731 msgid "Description" msgstr "" -#: build/models.py:86 +#: build/models.py:88 msgid "Brief description of the build" msgstr "" -#: build/models.py:95 build/templates/build/build_base.html:104 +#: build/models.py:97 build/templates/build/build_base.html:104 #: build/templates/build/detail.html:75 msgid "Parent Build" msgstr "" -#: build/models.py:96 +#: build/models.py:98 msgid "BuildOrder to which this build is allocated" msgstr "" -#: build/models.py:101 build/templates/build/auto_allocate.html:16 +#: build/models.py:103 build/templates/build/auto_allocate.html:16 #: build/templates/build/build_base.html:78 #: build/templates/build/detail.html:24 order/models.py:548 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:148 -#: order/templates/order/receive_parts.html:19 part/models.py:315 +#: order/templates/order/receive_parts.html:19 part/models.py:316 #: part/templates/part/part_app_base.html:7 part/templates/part/related.html:26 #: part/templates/part/set_category.html:13 templates/InvenTree/search.html:133 #: templates/js/barcode.js:336 templates/js/bom.js:153 templates/js/bom.js:502 #: templates/js/build.js:669 templates/js/company.js:138 -#: templates/js/part.js:252 templates/js/part.js:357 templates/js/stock.js:474 -#: templates/js/stock.js:1035 +#: templates/js/part.js:252 templates/js/part.js:357 templates/js/stock.js:475 +#: templates/js/stock.js:1061 msgid "Part" msgstr "" -#: build/models.py:109 +#: build/models.py:111 msgid "Select part to build" msgstr "" -#: build/models.py:114 +#: build/models.py:116 msgid "Sales Order Reference" msgstr "" -#: build/models.py:118 +#: build/models.py:120 msgid "SalesOrder to which this build is allocated" msgstr "" -#: build/models.py:123 +#: build/models.py:125 msgid "Source Location" msgstr "" -#: build/models.py:127 +#: build/models.py:129 msgid "" "Select location to take stock from for this build (leave blank to take from " "any stock location)" msgstr "" -#: build/models.py:132 +#: build/models.py:134 msgid "Destination Location" msgstr "" -#: build/models.py:136 +#: build/models.py:138 msgid "Select location where the completed items will be stored" msgstr "" -#: build/models.py:140 +#: build/models.py:142 msgid "Build Quantity" msgstr "" -#: build/models.py:143 +#: build/models.py:145 msgid "Number of stock items to build" msgstr "" -#: build/models.py:147 +#: build/models.py:149 msgid "Completed items" msgstr "" -#: build/models.py:149 +#: build/models.py:151 msgid "Number of stock items which have been completed" msgstr "" -#: build/models.py:153 part/templates/part/part_base.html:155 +#: build/models.py:155 part/templates/part/part_base.html:155 msgid "Build Status" msgstr "" -#: build/models.py:157 +#: build/models.py:159 msgid "Build status code" msgstr "" -#: build/models.py:161 stock/models.py:390 +#: build/models.py:163 stock/models.py:396 msgid "Batch Code" msgstr "" -#: build/models.py:165 +#: build/models.py:167 msgid "Batch code for this build output" msgstr "" -#: build/models.py:172 order/models.py:329 +#: build/models.py:174 order/models.py:329 msgid "Target completion date" msgstr "" -#: build/models.py:186 build/templates/build/detail.html:89 +#: build/models.py:188 build/templates/build/detail.html:89 #: company/templates/company/supplier_part_base.html:68 #: company/templates/company/supplier_part_detail.html:24 #: part/templates/part/detail.html:80 part/templates/part/part_base.html:102 -#: stock/models.py:384 stock/templates/stock/item_base.html:280 +#: stock/models.py:390 stock/templates/stock/item_base.html:287 msgid "External Link" msgstr "" -#: build/models.py:187 part/models.py:672 stock/models.py:386 +#: build/models.py:189 part/models.py:705 stock/models.py:392 msgid "Link to external URL" msgstr "" -#: build/models.py:191 build/templates/build/tabs.html:23 company/models.py:366 +#: build/models.py:193 build/templates/build/tabs.html:23 company/models.py:366 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:18 #: order/templates/order/purchase_order_detail.html:213 -#: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:73 -#: stock/forms.py:307 stock/forms.py:339 stock/forms.py:367 stock/models.py:448 -#: stock/models.py:1433 stock/templates/stock/tabs.html:26 -#: templates/js/barcode.js:391 templates/js/bom.js:263 -#: templates/js/stock.js:116 templates/js/stock.js:588 +#: order/templates/order/so_tabs.html:23 part/models.py:831 +#: part/templates/part/tabs.html:73 stock/forms.py:313 stock/forms.py:345 +#: stock/forms.py:373 stock/models.py:462 stock/models.py:1476 +#: stock/templates/stock/tabs.html:26 templates/js/barcode.js:391 +#: templates/js/bom.js:263 templates/js/stock.js:117 templates/js/stock.js:603 msgid "Notes" msgstr "" -#: build/models.py:192 +#: build/models.py:194 msgid "Extra build notes" msgstr "" -#: build/models.py:577 +#: build/models.py:579 msgid "No build output specified" msgstr "" -#: build/models.py:580 +#: build/models.py:582 msgid "Build output is already completed" msgstr "" -#: build/models.py:583 +#: build/models.py:585 msgid "Build output does not match Build Order" msgstr "" -#: build/models.py:658 +#: build/models.py:660 msgid "Completed build output" msgstr "" -#: build/models.py:896 +#: build/models.py:902 msgid "BuildItem must be unique for build, stock_item and install_into" msgstr "" -#: build/models.py:918 +#: build/models.py:924 msgid "Build item must specify a build output" msgstr "" -#: build/models.py:923 +#: build/models.py:929 #, python-brace-format msgid "Selected stock item not found in BOM for part '{p}'" msgstr "" -#: build/models.py:927 +#: build/models.py:933 #, python-brace-format msgid "Allocated quantity ({n}) must not exceed available quantity ({q})" msgstr "" -#: build/models.py:934 order/models.py:632 +#: build/models.py:940 order/models.py:632 msgid "StockItem is over-allocated" msgstr "" -#: build/models.py:938 order/models.py:635 +#: build/models.py:944 order/models.py:635 msgid "Allocation quantity must be greater than zero" msgstr "" -#: build/models.py:942 +#: build/models.py:948 msgid "Quantity must be 1 for serialized stock" msgstr "" -#: build/models.py:982 +#: build/models.py:988 msgid "Build to allocate parts" msgstr "" -#: build/models.py:989 +#: build/models.py:995 msgid "Source stock item" msgstr "" -#: build/models.py:1001 +#: build/models.py:1007 msgid "Stock quantity to allocate to build" msgstr "" -#: build/models.py:1009 +#: build/models.py:1015 msgid "Destination stock item" msgstr "" @@ -612,7 +613,7 @@ msgid "Order required parts" msgstr "" #: build/templates/build/allocate.html:30 -#: company/templates/company/detail_part.html:28 order/views.py:803 +#: company/templates/company/detail_part.html:28 order/views.py:805 #: part/templates/part/category.html:125 msgid "Order Parts" msgstr "" @@ -652,11 +653,11 @@ msgid "" "The following stock items will be allocated to the specified build output" msgstr "" -#: build/templates/build/auto_allocate.html:18 stock/forms.py:337 -#: stock/templates/stock/item_base.html:227 +#: build/templates/build/auto_allocate.html:18 stock/forms.py:343 +#: stock/templates/stock/item_base.html:234 #: stock/templates/stock/stock_adjust.html:17 #: templates/InvenTree/search.html:183 templates/js/barcode.js:337 -#: templates/js/build.js:434 templates/js/stock.js:580 +#: templates/js/build.js:434 templates/js/stock.js:587 msgid "Location" msgstr "" @@ -681,7 +682,7 @@ msgstr "" #: order/templates/order/order_base.html:26 #: order/templates/order/sales_order_base.html:35 #: part/templates/part/category.html:13 part/templates/part/part_base.html:32 -#: stock/templates/stock/item_base.html:90 +#: stock/templates/stock/item_base.html:97 #: stock/templates/stock/location.html:12 msgid "Admin view" msgstr "" @@ -690,7 +691,7 @@ msgstr "" #: build/templates/build/build_base.html:92 #: order/templates/order/sales_order_base.html:41 #: order/templates/order/sales_order_base.html:83 -#: templates/js/table_filters.js:190 templates/js/table_filters.js:222 +#: templates/js/table_filters.js:200 templates/js/table_filters.js:232 msgid "Overdue" msgstr "" @@ -717,10 +718,10 @@ msgstr "" #: build/templates/build/build_base.html:88 #: build/templates/build/detail.html:57 #: order/templates/order/receive_parts.html:24 -#: stock/templates/stock/item_base.html:312 templates/InvenTree/search.html:175 +#: stock/templates/stock/item_base.html:333 templates/InvenTree/search.html:175 #: templates/js/barcode.js:42 templates/js/build.js:697 #: templates/js/order.js:180 templates/js/order.js:268 -#: templates/js/stock.js:567 templates/js/stock.js:971 +#: templates/js/stock.js:574 templates/js/stock.js:997 msgid "Status" msgstr "" @@ -740,7 +741,7 @@ msgstr "" #: order/templates/order/sales_order_notes.html:10 #: order/templates/order/sales_order_ship.html:25 #: part/templates/part/allocation.html:27 -#: stock/templates/stock/item_base.html:221 templates/js/order.js:229 +#: stock/templates/stock/item_base.html:228 templates/js/order.js:229 msgid "Sales Order" msgstr "" @@ -832,7 +833,7 @@ msgstr "" msgid "Stock can be taken from any available location." msgstr "" -#: build/templates/build/detail.html:44 stock/forms.py:365 +#: build/templates/build/detail.html:44 stock/forms.py:371 msgid "Destination" msgstr "" @@ -841,9 +842,9 @@ msgid "Destination location not specified" msgstr "" #: build/templates/build/detail.html:68 -#: stock/templates/stock/item_base.html:245 templates/js/stock.js:575 -#: templates/js/stock.js:978 templates/js/table_filters.js:80 -#: templates/js/table_filters.js:151 +#: stock/templates/stock/item_base.html:252 templates/js/stock.js:582 +#: templates/js/stock.js:1004 templates/js/table_filters.js:80 +#: templates/js/table_filters.js:161 msgid "Batch" msgstr "" @@ -935,7 +936,7 @@ msgstr "" msgid "Create Build Output" msgstr "" -#: build/views.py:207 stock/models.py:828 stock/views.py:1668 +#: build/views.py:207 stock/models.py:871 stock/views.py:1681 msgid "Serial numbers already exist" msgstr "" @@ -1032,36 +1033,36 @@ msgstr "" msgid "Stock item must be selected" msgstr "" -#: build/views.py:1011 +#: build/views.py:1012 msgid "Edit Stock Allocation" msgstr "" -#: build/views.py:1016 +#: build/views.py:1017 msgid "Updated Build Item" msgstr "" -#: build/views.py:1045 +#: build/views.py:1046 msgid "Add Build Order Attachment" msgstr "" -#: build/views.py:1059 order/views.py:111 order/views.py:164 part/views.py:168 +#: build/views.py:1060 order/views.py:113 order/views.py:166 part/views.py:170 #: stock/views.py:180 msgid "Added attachment" msgstr "" -#: build/views.py:1095 order/views.py:191 order/views.py:213 +#: build/views.py:1096 order/views.py:193 order/views.py:215 msgid "Edit Attachment" msgstr "" -#: build/views.py:1106 order/views.py:196 order/views.py:218 +#: build/views.py:1107 order/views.py:198 order/views.py:220 msgid "Attachment updated" msgstr "" -#: build/views.py:1116 order/views.py:233 order/views.py:248 +#: build/views.py:1117 order/views.py:235 order/views.py:250 msgid "Delete Attachment" msgstr "" -#: build/views.py:1122 order/views.py:240 order/views.py:255 stock/views.py:238 +#: build/views.py:1123 order/views.py:242 order/views.py:257 stock/views.py:238 msgid "Deleted attachment" msgstr "" @@ -1137,103 +1138,170 @@ msgstr "" msgid "Copy category parameter templates when creating a part" msgstr "" -#: common/models.py:115 part/models.py:743 part/templates/part/detail.html:168 -#: templates/js/table_filters.js:272 -msgid "Component" +#: common/models.py:115 part/templates/part/detail.html:155 stock/forms.py:255 +#: templates/js/table_filters.js:23 templates/js/table_filters.js:266 +msgid "Template" msgstr "" #: common/models.py:116 -msgid "Parts can be used as sub-components by default" +msgid "Parts are templates by default" msgstr "" -#: common/models.py:122 part/models.py:754 part/templates/part/detail.html:188 -msgid "Purchaseable" +#: common/models.py:122 part/models.py:794 part/templates/part/detail.html:165 +#: templates/js/table_filters.js:278 +msgid "Assembly" msgstr "" #: common/models.py:123 -msgid "Parts are purchaseable by default" +msgid "Parts can be assembled from other components by default" msgstr "" -#: common/models.py:129 part/models.py:759 part/templates/part/detail.html:198 -#: templates/js/table_filters.js:280 -msgid "Salable" +#: common/models.py:129 part/models.py:800 part/templates/part/detail.html:175 +#: templates/js/table_filters.js:282 +msgid "Component" msgstr "" #: common/models.py:130 -msgid "Parts are salable by default" +msgid "Parts can be used as sub-components by default" msgstr "" -#: common/models.py:136 part/models.py:749 part/templates/part/detail.html:178 -#: templates/js/table_filters.js:31 templates/js/table_filters.js:284 -msgid "Trackable" +#: common/models.py:136 part/models.py:811 part/templates/part/detail.html:195 +msgid "Purchaseable" msgstr "" #: common/models.py:137 -msgid "Parts are trackable by default" +msgid "Parts are purchaseable by default" msgstr "" -#: common/models.py:143 -msgid "Build Order Reference Prefix" +#: common/models.py:143 part/models.py:816 part/templates/part/detail.html:205 +#: templates/js/table_filters.js:290 +msgid "Salable" msgstr "" #: common/models.py:144 +msgid "Parts are salable by default" +msgstr "" + +#: common/models.py:150 part/models.py:806 part/templates/part/detail.html:185 +#: templates/js/table_filters.js:31 templates/js/table_filters.js:294 +msgid "Trackable" +msgstr "" + +#: common/models.py:151 +msgid "Parts are trackable by default" +msgstr "" + +#: common/models.py:157 part/models.py:826 part/templates/part/detail.html:145 +#: templates/js/table_filters.js:27 +msgid "Virtual" +msgstr "" + +#: common/models.py:158 +msgid "Parts are virtual by default" +msgstr "" + +#: common/models.py:164 +msgid "Stock Expiry" +msgstr "" + +#: common/models.py:165 +msgid "Enable stock expiry functionality" +msgstr "" + +#: common/models.py:171 +msgid "Sell Expired Stock" +msgstr "" + +#: common/models.py:172 +msgid "Allow sale of expired stock" +msgstr "" + +#: common/models.py:178 +msgid "Stock Stale Time" +msgstr "" + +#: common/models.py:179 +msgid "Number of days stock items are considered stale before expiring" +msgstr "" + +#: common/models.py:181 part/templates/part/detail.html:116 +msgid "days" +msgstr "" + +#: common/models.py:186 +msgid "Build Expired Stock" +msgstr "" + +#: common/models.py:187 +msgid "Allow building with expired stock" +msgstr "" + +#: common/models.py:193 +msgid "Build Order Reference Prefix" +msgstr "" + +#: common/models.py:194 msgid "Prefix value for build order reference" msgstr "" -#: common/models.py:149 +#: common/models.py:199 msgid "Build Order Reference Regex" msgstr "" -#: common/models.py:150 +#: common/models.py:200 msgid "Regular expression pattern for matching build order reference" msgstr "" -#: common/models.py:154 +#: common/models.py:204 msgid "Sales Order Reference Prefix" msgstr "" -#: common/models.py:155 +#: common/models.py:205 msgid "Prefix value for sales order reference" msgstr "" -#: common/models.py:160 +#: common/models.py:210 msgid "Purchase Order Reference Prefix" msgstr "" -#: common/models.py:161 +#: common/models.py:211 msgid "Prefix value for purchase order reference" msgstr "" -#: common/models.py:378 +#: common/models.py:434 msgid "Settings key (must be unique - case insensitive" msgstr "" -#: common/models.py:380 +#: common/models.py:436 msgid "Settings value" msgstr "" -#: common/models.py:439 +#: common/models.py:493 msgid "Value must be a boolean value" msgstr "" -#: common/models.py:453 +#: common/models.py:503 +msgid "Value must be an integer value" +msgstr "" + +#: common/models.py:517 msgid "Key string must be unique" msgstr "" -#: common/models.py:497 company/forms.py:113 +#: common/models.py:590 company/forms.py:113 msgid "Price break quantity" msgstr "" -#: common/models.py:505 company/templates/company/supplier_part_pricing.html:80 +#: common/models.py:598 company/templates/company/supplier_part_pricing.html:80 #: part/templates/part/sale_prices.html:87 templates/js/bom.js:246 msgid "Price" msgstr "" -#: common/models.py:506 +#: common/models.py:599 msgid "Unit price at specified quantity" msgstr "" -#: common/models.py:529 +#: common/models.py:622 msgid "Default" msgstr "" @@ -1334,8 +1402,8 @@ msgstr "" msgid "Currency" msgstr "" -#: company/models.py:313 stock/models.py:338 -#: stock/templates/stock/item_base.html:177 +#: company/models.py:313 stock/models.py:344 +#: stock/templates/stock/item_base.html:184 msgid "Base Part" msgstr "" @@ -1348,7 +1416,7 @@ msgstr "" #: company/templates/company/supplier_part_detail.html:21 #: order/templates/order/order_base.html:79 #: order/templates/order/order_wizard/select_pos.html:30 part/bom.py:170 -#: stock/templates/stock/item_base.html:287 templates/js/company.js:48 +#: stock/templates/stock/item_base.html:294 templates/js/company.js:48 #: templates/js/company.js:164 templates/js/order.js:162 msgid "Supplier" msgstr "" @@ -1387,7 +1455,7 @@ msgstr "" msgid "Manufacturer part number" msgstr "" -#: company/models.py:353 templates/js/company.js:208 +#: company/models.py:353 part/models.py:704 templates/js/company.js:208 msgid "Link" msgstr "" @@ -1444,8 +1512,8 @@ msgid "Uses default currency" msgstr "" #: company/templates/company/detail.html:62 -#: order/templates/order/sales_order_base.html:89 stock/models.py:373 -#: stock/models.py:374 stock/templates/stock/item_base.html:204 +#: order/templates/order/sales_order_base.html:89 stock/models.py:379 +#: stock/models.py:380 stock/templates/stock/item_base.html:211 #: templates/js/company.js:40 templates/js/order.js:250 msgid "Customer" msgstr "" @@ -1461,7 +1529,7 @@ msgstr "" #: company/templates/company/detail_part.html:18 #: order/templates/order/purchase_order_detail.html:68 -#: part/templates/part/supplier.html:14 templates/js/stock.js:855 +#: part/templates/part/supplier.html:14 templates/js/stock.js:881 msgid "New Supplier Part" msgstr "" @@ -1485,7 +1553,7 @@ msgid "Delete Parts" msgstr "" #: company/templates/company/detail_part.html:63 -#: part/templates/part/category.html:116 templates/js/stock.js:849 +#: part/templates/part/category.html:116 templates/js/stock.js:875 msgid "New Part" msgstr "" @@ -1577,8 +1645,8 @@ msgid "New Sales Order" msgstr "" #: company/templates/company/supplier_part_base.html:6 -#: company/templates/company/supplier_part_base.html:19 stock/models.py:347 -#: stock/templates/stock/item_base.html:292 templates/js/company.js:180 +#: company/templates/company/supplier_part_base.html:19 stock/models.py:353 +#: stock/templates/stock/item_base.html:299 templates/js/company.js:180 msgid "Supplier Part" msgstr "" @@ -1619,7 +1687,7 @@ msgid "Pricing Information" msgstr "" #: company/templates/company/supplier_part_pricing.html:17 company/views.py:486 -#: part/templates/part/sale_prices.html:14 part/views.py:2555 +#: part/templates/part/sale_prices.html:14 part/views.py:2565 msgid "Add Price Break" msgstr "" @@ -1650,7 +1718,7 @@ msgstr "" #: company/templates/company/tabs.html:12 part/templates/part/tabs.html:18 #: stock/templates/stock/location.html:17 templates/InvenTree/search.html:155 #: templates/InvenTree/settings/tabs.html:25 templates/js/part.js:192 -#: templates/js/part.js:418 templates/js/stock.js:508 templates/navbar.html:22 +#: templates/js/part.js:418 templates/js/stock.js:509 templates/navbar.html:22 #: users/models.py:29 msgid "Stock" msgstr "" @@ -1660,7 +1728,7 @@ msgid "Orders" msgstr "" #: company/templates/company/tabs.html:9 -#: order/templates/order/receive_parts.html:14 part/models.py:316 +#: order/templates/order/receive_parts.html:14 part/models.py:317 #: part/templates/part/cat_link.html:7 part/templates/part/category.html:94 #: part/templates/part/category_tabs.html:6 #: templates/InvenTree/settings/tabs.html:22 templates/navbar.html:19 @@ -1733,7 +1801,7 @@ msgstr "" msgid "Edit Supplier Part" msgstr "" -#: company/views.py:295 templates/js/stock.js:856 +#: company/views.py:295 templates/js/stock.js:882 msgid "Create new Supplier Part" msgstr "" @@ -1741,15 +1809,15 @@ msgstr "" msgid "Delete Supplier Part" msgstr "" -#: company/views.py:492 part/views.py:2561 +#: company/views.py:492 part/views.py:2571 msgid "Added new price break" msgstr "" -#: company/views.py:548 part/views.py:2605 +#: company/views.py:548 part/views.py:2615 msgid "Edit Price Break" msgstr "" -#: company/views.py:564 part/views.py:2621 +#: company/views.py:564 part/views.py:2631 msgid "Delete Price Break" msgstr "" @@ -1847,8 +1915,8 @@ msgstr "" msgid "Date order was completed" msgstr "" -#: order/models.py:176 order/models.py:258 part/views.py:1494 -#: stock/models.py:244 stock/models.py:812 +#: order/models.py:176 order/models.py:258 part/views.py:1504 +#: stock/models.py:250 stock/models.py:855 msgid "Quantity must be greater than zero" msgstr "" @@ -1886,7 +1954,7 @@ msgstr "" #: order/models.py:504 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:24 -#: stock/templates/stock/item_base.html:259 templates/js/order.js:146 +#: stock/templates/stock/item_base.html:266 templates/js/order.js:146 msgid "Purchase Order" msgstr "" @@ -1898,8 +1966,8 @@ msgstr "" msgid "Number of items received" msgstr "" -#: order/models.py:527 stock/models.py:458 -#: stock/templates/stock/item_base.html:266 +#: order/models.py:527 stock/models.py:472 +#: stock/templates/stock/item_base.html:273 msgid "Purchase Price" msgstr "" @@ -2045,8 +2113,8 @@ msgid "Line Items" msgstr "" #: order/templates/order/purchase_order_detail.html:17 -#: order/templates/order/sales_order_detail.html:19 order/views.py:1117 -#: order/views.py:1201 +#: order/templates/order/sales_order_detail.html:19 order/views.py:1119 +#: order/views.py:1203 msgid "Add Line Item" msgstr "" @@ -2057,7 +2125,7 @@ msgstr "" #: order/templates/order/purchase_order_detail.html:39 #: order/templates/order/purchase_order_detail.html:119 #: part/templates/part/category.html:173 part/templates/part/category.html:215 -#: templates/js/stock.js:861 +#: templates/js/stock.js:627 templates/js/stock.js:887 msgid "New Location" msgstr "" @@ -2142,8 +2210,8 @@ msgid "Sales Order Items" msgstr "" #: order/templates/order/sales_order_detail.html:72 -#: order/templates/order/sales_order_detail.html:154 stock/models.py:378 -#: stock/templates/stock/item_base.html:191 templates/js/build.js:418 +#: order/templates/order/sales_order_detail.html:154 stock/models.py:384 +#: stock/templates/stock/item_base.html:198 templates/js/build.js:418 msgid "Serial Number" msgstr "" @@ -2221,143 +2289,143 @@ msgstr "" msgid "Order Items" msgstr "" -#: order/views.py:99 +#: order/views.py:101 msgid "Add Purchase Order Attachment" msgstr "" -#: order/views.py:150 +#: order/views.py:152 msgid "Add Sales Order Attachment" msgstr "" -#: order/views.py:310 +#: order/views.py:312 msgid "Create Purchase Order" msgstr "" -#: order/views.py:346 +#: order/views.py:348 msgid "Create Sales Order" msgstr "" -#: order/views.py:382 +#: order/views.py:384 msgid "Edit Purchase Order" msgstr "" -#: order/views.py:403 +#: order/views.py:405 msgid "Edit Sales Order" msgstr "" -#: order/views.py:420 +#: order/views.py:422 msgid "Cancel Order" msgstr "" -#: order/views.py:430 order/views.py:457 +#: order/views.py:432 order/views.py:459 msgid "Confirm order cancellation" msgstr "" -#: order/views.py:433 +#: order/views.py:435 msgid "Order cannot be cancelled as either pending or placed" msgstr "" -#: order/views.py:447 +#: order/views.py:449 msgid "Cancel sales order" msgstr "" -#: order/views.py:460 +#: order/views.py:462 msgid "Order cannot be cancelled" msgstr "" -#: order/views.py:474 +#: order/views.py:476 msgid "Issue Order" msgstr "" -#: order/views.py:484 +#: order/views.py:486 msgid "Confirm order placement" msgstr "" -#: order/views.py:494 +#: order/views.py:496 msgid "Purchase order issued" msgstr "" -#: order/views.py:505 +#: order/views.py:507 msgid "Complete Order" msgstr "" -#: order/views.py:522 +#: order/views.py:524 msgid "Confirm order completion" msgstr "" -#: order/views.py:533 +#: order/views.py:535 msgid "Purchase order completed" msgstr "" -#: order/views.py:543 +#: order/views.py:545 msgid "Ship Order" msgstr "" -#: order/views.py:560 +#: order/views.py:562 msgid "Confirm order shipment" msgstr "" -#: order/views.py:566 +#: order/views.py:568 msgid "Could not ship order" msgstr "" -#: order/views.py:618 +#: order/views.py:620 msgid "Receive Parts" msgstr "" -#: order/views.py:686 +#: order/views.py:688 msgid "Items received" msgstr "" -#: order/views.py:700 +#: order/views.py:702 msgid "No destination set" msgstr "" -#: order/views.py:745 +#: order/views.py:747 msgid "Error converting quantity to number" msgstr "" -#: order/views.py:751 +#: order/views.py:753 msgid "Receive quantity less than zero" msgstr "" -#: order/views.py:757 +#: order/views.py:759 msgid "No lines specified" msgstr "" -#: order/views.py:1127 +#: order/views.py:1129 msgid "Supplier part must be specified" msgstr "" -#: order/views.py:1133 +#: order/views.py:1135 msgid "Supplier must match for Part and Order" msgstr "" -#: order/views.py:1253 order/views.py:1272 +#: order/views.py:1255 order/views.py:1274 msgid "Edit Line Item" msgstr "" -#: order/views.py:1289 order/views.py:1302 +#: order/views.py:1291 order/views.py:1304 msgid "Delete Line Item" msgstr "" -#: order/views.py:1295 order/views.py:1308 +#: order/views.py:1297 order/views.py:1310 msgid "Deleted line item" msgstr "" -#: order/views.py:1317 +#: order/views.py:1319 msgid "Allocate Stock to Order" msgstr "" -#: order/views.py:1387 +#: order/views.py:1394 msgid "Edit Allocation Quantity" msgstr "" -#: order/views.py:1403 +#: order/views.py:1410 msgid "Remove allocation" msgstr "" -#: part/bom.py:138 part/templates/part/category.html:61 +#: part/bom.py:138 part/models.py:722 part/templates/part/category.html:61 #: part/templates/part/detail.html:87 msgid "Default Location" msgstr "" @@ -2379,11 +2447,11 @@ msgstr "" msgid "Error reading BOM file (incorrect row size)" msgstr "" -#: part/forms.py:61 stock/forms.py:255 +#: part/forms.py:61 stock/forms.py:261 msgid "File Format" msgstr "" -#: part/forms.py:61 stock/forms.py:255 +#: part/forms.py:61 stock/forms.py:261 msgid "Select output file format" msgstr "" @@ -2427,7 +2495,7 @@ msgstr "" msgid "Include part supplier data in exported BOM" msgstr "" -#: part/forms.py:92 part/models.py:1720 +#: part/forms.py:92 part/models.py:1781 msgid "Parent Part" msgstr "" @@ -2459,43 +2527,43 @@ msgstr "" msgid "Select part category" msgstr "" -#: part/forms.py:188 +#: part/forms.py:189 msgid "Duplicate all BOM data for this part" msgstr "" -#: part/forms.py:189 +#: part/forms.py:190 msgid "Copy BOM" msgstr "" -#: part/forms.py:194 +#: part/forms.py:195 msgid "Duplicate all parameter data for this part" msgstr "" -#: part/forms.py:195 +#: part/forms.py:196 msgid "Copy Parameters" msgstr "" -#: part/forms.py:200 +#: part/forms.py:201 msgid "Confirm part creation" msgstr "" -#: part/forms.py:205 +#: part/forms.py:206 msgid "Include category parameter templates" msgstr "" -#: part/forms.py:210 +#: part/forms.py:211 msgid "Include parent categories parameter templates" msgstr "" -#: part/forms.py:285 +#: part/forms.py:291 msgid "Add parameter template to same level categories" msgstr "" -#: part/forms.py:289 +#: part/forms.py:295 msgid "Add parameter template to all categories" msgstr "" -#: part/forms.py:333 +#: part/forms.py:339 msgid "Input quantity for price calculation" msgstr "" @@ -2507,7 +2575,7 @@ msgstr "" msgid "Default keywords for parts in this category" msgstr "" -#: part/models.py:77 part/models.py:1765 +#: part/models.py:77 part/models.py:1826 #: part/templates/part/part_app_base.html:9 msgid "Part Category" msgstr "" @@ -2517,255 +2585,294 @@ msgstr "" msgid "Part Categories" msgstr "" -#: part/models.py:408 part/models.py:418 +#: part/models.py:409 part/models.py:419 #, python-brace-format msgid "Part '{p1}' is used in BOM for '{p2}' (recursive)" msgstr "" -#: part/models.py:515 +#: part/models.py:516 msgid "Next available serial numbers are" msgstr "" -#: part/models.py:519 +#: part/models.py:520 msgid "Next available serial number is" msgstr "" -#: part/models.py:524 +#: part/models.py:525 msgid "Most recent serial number is" msgstr "" -#: part/models.py:603 +#: part/models.py:604 msgid "Duplicate IPN not allowed in part settings" msgstr "" -#: part/models.py:614 +#: part/models.py:615 msgid "Part must be unique for name, IPN and revision" msgstr "" -#: part/models.py:644 part/templates/part/detail.html:19 +#: part/models.py:646 part/templates/part/detail.html:19 msgid "Part name" msgstr "" -#: part/models.py:648 +#: part/models.py:653 +msgid "Is Template" +msgstr "" + +#: part/models.py:654 msgid "Is this part a template part?" msgstr "" -#: part/models.py:657 +#: part/models.py:665 msgid "Is this part a variant of another part?" msgstr "" -#: part/models.py:659 +#: part/models.py:666 part/templates/part/detail.html:57 +msgid "Variant Of" +msgstr "" + +#: part/models.py:672 msgid "Part description" msgstr "" -#: part/models.py:661 +#: part/models.py:677 part/templates/part/category.html:68 +#: part/templates/part/detail.html:64 +msgid "Keywords" +msgstr "" + +#: part/models.py:678 msgid "Part keywords to improve visibility in search results" msgstr "" -#: part/models.py:666 +#: part/models.py:685 part/templates/part/detail.html:70 +#: part/templates/part/set_category.html:15 templates/js/part.js:405 +msgid "Category" +msgstr "" + +#: part/models.py:686 msgid "Part category" msgstr "" -#: part/models.py:668 +#: part/models.py:691 part/templates/part/detail.html:25 +#: part/templates/part/part_base.html:95 templates/js/part.js:180 +msgid "IPN" +msgstr "" + +#: part/models.py:692 msgid "Internal Part Number" msgstr "" -#: part/models.py:670 +#: part/models.py:698 msgid "Part revision or version number" msgstr "" -#: part/models.py:684 +#: part/models.py:699 part/templates/part/detail.html:32 +#: templates/js/part.js:184 +msgid "Revision" +msgstr "" + +#: part/models.py:720 msgid "Where is this item normally stored?" msgstr "" -#: part/models.py:728 +#: part/models.py:767 part/templates/part/detail.html:94 +msgid "Default Supplier" +msgstr "" + +#: part/models.py:768 msgid "Default supplier part" msgstr "" -#: part/models.py:731 +#: part/models.py:775 +msgid "Default Expiry" +msgstr "" + +#: part/models.py:776 +msgid "Expiry time (in days) for stock items of this part" +msgstr "" + +#: part/models.py:781 part/templates/part/detail.html:108 +msgid "Minimum Stock" +msgstr "" + +#: part/models.py:782 msgid "Minimum allowed stock level" msgstr "" -#: part/models.py:733 +#: part/models.py:788 part/templates/part/detail.html:102 +#: part/templates/part/params.html:26 +msgid "Units" +msgstr "" + +#: part/models.py:789 msgid "Stock keeping units for this part" msgstr "" -#: part/models.py:737 part/templates/part/detail.html:158 -#: templates/js/table_filters.js:268 -msgid "Assembly" -msgstr "" - -#: part/models.py:738 +#: part/models.py:795 msgid "Can this part be built from other parts?" msgstr "" -#: part/models.py:744 +#: part/models.py:801 msgid "Can this part be used to build other parts?" msgstr "" -#: part/models.py:750 +#: part/models.py:807 msgid "Does this part have tracking for unique items?" msgstr "" -#: part/models.py:755 +#: part/models.py:812 msgid "Can this part be purchased from external suppliers?" msgstr "" -#: part/models.py:760 +#: part/models.py:817 msgid "Can this part be sold to customers?" msgstr "" -#: part/models.py:764 part/templates/part/detail.html:215 +#: part/models.py:821 part/templates/part/detail.html:222 #: templates/js/table_filters.js:19 templates/js/table_filters.js:55 -#: templates/js/table_filters.js:186 templates/js/table_filters.js:251 +#: templates/js/table_filters.js:196 templates/js/table_filters.js:261 msgid "Active" msgstr "" -#: part/models.py:765 +#: part/models.py:822 msgid "Is this part active?" msgstr "" -#: part/models.py:769 part/templates/part/detail.html:138 -#: templates/js/table_filters.js:27 -msgid "Virtual" -msgstr "" - -#: part/models.py:770 +#: part/models.py:827 msgid "Is this a virtual part, such as a software product or license?" msgstr "" -#: part/models.py:772 +#: part/models.py:832 msgid "Part notes - supports Markdown formatting" msgstr "" -#: part/models.py:774 +#: part/models.py:835 msgid "Stored BOM checksum" msgstr "" -#: part/models.py:1593 +#: part/models.py:1654 msgid "Test templates can only be created for trackable parts" msgstr "" -#: part/models.py:1610 +#: part/models.py:1671 msgid "Test with this name already exists for this part" msgstr "" -#: part/models.py:1629 templates/js/part.js:567 templates/js/stock.js:92 +#: part/models.py:1690 templates/js/part.js:567 templates/js/stock.js:93 msgid "Test Name" msgstr "" -#: part/models.py:1630 +#: part/models.py:1691 msgid "Enter a name for the test" msgstr "" -#: part/models.py:1635 +#: part/models.py:1696 msgid "Test Description" msgstr "" -#: part/models.py:1636 +#: part/models.py:1697 msgid "Enter description for this test" msgstr "" -#: part/models.py:1641 templates/js/part.js:576 -#: templates/js/table_filters.js:172 +#: part/models.py:1702 templates/js/part.js:576 +#: templates/js/table_filters.js:182 msgid "Required" msgstr "" -#: part/models.py:1642 +#: part/models.py:1703 msgid "Is this test required to pass?" msgstr "" -#: part/models.py:1647 templates/js/part.js:584 +#: part/models.py:1708 templates/js/part.js:584 msgid "Requires Value" msgstr "" -#: part/models.py:1648 +#: part/models.py:1709 msgid "Does this test require a value when adding a test result?" msgstr "" -#: part/models.py:1653 templates/js/part.js:591 +#: part/models.py:1714 templates/js/part.js:591 msgid "Requires Attachment" msgstr "" -#: part/models.py:1654 +#: part/models.py:1715 msgid "Does this test require a file attachment when adding a test result?" msgstr "" -#: part/models.py:1687 +#: part/models.py:1748 msgid "Parameter template name must be unique" msgstr "" -#: part/models.py:1692 +#: part/models.py:1753 msgid "Parameter Name" msgstr "" -#: part/models.py:1694 +#: part/models.py:1755 msgid "Parameter Units" msgstr "" -#: part/models.py:1722 part/models.py:1770 +#: part/models.py:1783 part/models.py:1831 #: templates/InvenTree/settings/category.html:62 msgid "Parameter Template" msgstr "" -#: part/models.py:1724 +#: part/models.py:1785 msgid "Parameter Value" msgstr "" -#: part/models.py:1774 +#: part/models.py:1835 msgid "Default Parameter Value" msgstr "" -#: part/models.py:1804 +#: part/models.py:1865 msgid "Select parent part" msgstr "" -#: part/models.py:1812 +#: part/models.py:1873 msgid "Select part to be used in BOM" msgstr "" -#: part/models.py:1818 +#: part/models.py:1879 msgid "BOM quantity for this BOM item" msgstr "" -#: part/models.py:1820 +#: part/models.py:1881 msgid "This BOM item is optional" msgstr "" -#: part/models.py:1823 +#: part/models.py:1884 msgid "Estimated build wastage quantity (absolute or percentage)" msgstr "" -#: part/models.py:1826 +#: part/models.py:1887 msgid "BOM item reference" msgstr "" -#: part/models.py:1829 +#: part/models.py:1890 msgid "BOM item notes" msgstr "" -#: part/models.py:1831 +#: part/models.py:1892 msgid "BOM line checksum" msgstr "" -#: part/models.py:1902 part/views.py:1500 part/views.py:1552 -#: stock/models.py:234 +#: part/models.py:1963 part/views.py:1510 part/views.py:1562 +#: stock/models.py:240 msgid "Quantity must be integer value for trackable parts" msgstr "" -#: part/models.py:1911 part/models.py:1913 +#: part/models.py:1972 part/models.py:1974 msgid "Sub part must be specified" msgstr "" -#: part/models.py:1916 +#: part/models.py:1977 msgid "BOM Item" msgstr "" -#: part/models.py:2031 +#: part/models.py:2092 msgid "Select Related Part" msgstr "" -#: part/models.py:2063 +#: part/models.py:2124 msgid "" "Error creating relationship: check that the part is not related to itself " "and that the relationship is unique" @@ -2786,9 +2893,9 @@ msgstr "" #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 #: stock/templates/stock/item_base.html:72 -#: stock/templates/stock/item_base.html:274 +#: stock/templates/stock/item_base.html:281 #: stock/templates/stock/stock_adjust.html:16 templates/js/build.js:751 -#: templates/js/stock.js:705 templates/js/stock.js:954 +#: templates/js/stock.js:720 templates/js/stock.js:980 msgid "Stock Item" msgstr "" @@ -2853,7 +2960,7 @@ msgstr "" msgid "Validate" msgstr "" -#: part/templates/part/bom.html:62 part/views.py:1791 +#: part/templates/part/bom.html:62 part/views.py:1801 msgid "Export Bill of Materials" msgstr "" @@ -2953,7 +3060,7 @@ msgstr "" msgid "All parts" msgstr "" -#: part/templates/part/category.html:24 part/views.py:2182 +#: part/templates/part/category.html:24 part/views.py:2192 msgid "Create new part category" msgstr "" @@ -2977,10 +3084,6 @@ msgstr "" msgid "Category Description" msgstr "" -#: part/templates/part/category.html:68 part/templates/part/detail.html:64 -msgid "Keywords" -msgstr "" - #: part/templates/part/category.html:74 msgid "Subcategories" msgstr "" @@ -3009,7 +3112,7 @@ msgstr "" msgid "Export Data" msgstr "" -#: part/templates/part/category.html:174 +#: part/templates/part/category.html:174 templates/js/stock.js:628 msgid "Create new location" msgstr "" @@ -3025,7 +3128,7 @@ msgstr "" msgid "Create new Part Category" msgstr "" -#: part/templates/part/category.html:216 stock/views.py:1359 +#: part/templates/part/category.html:216 stock/views.py:1363 msgid "Create new Stock Location" msgstr "" @@ -3049,15 +3152,6 @@ msgstr "" msgid "Part Details" msgstr "" -#: part/templates/part/detail.html:25 part/templates/part/part_base.html:95 -#: templates/js/part.js:180 -msgid "IPN" -msgstr "" - -#: part/templates/part/detail.html:32 templates/js/part.js:184 -msgid "Revision" -msgstr "" - #: part/templates/part/detail.html:39 msgid "Latest Serial Number" msgstr "" @@ -3066,101 +3160,79 @@ msgstr "" msgid "No serial numbers recorded" msgstr "" -#: part/templates/part/detail.html:57 -msgid "Variant Of" +#: part/templates/part/detail.html:115 +msgid "Stock Expiry Time" msgstr "" -#: part/templates/part/detail.html:70 part/templates/part/set_category.html:15 -#: templates/js/part.js:405 -msgid "Category" -msgstr "" - -#: part/templates/part/detail.html:94 -msgid "Default Supplier" -msgstr "" - -#: part/templates/part/detail.html:102 part/templates/part/params.html:26 -msgid "Units" -msgstr "" - -#: part/templates/part/detail.html:108 -msgid "Minimum Stock" -msgstr "" - -#: part/templates/part/detail.html:114 templates/js/order.js:276 +#: part/templates/part/detail.html:121 templates/js/order.js:276 msgid "Creation Date" msgstr "" -#: part/templates/part/detail.html:120 +#: part/templates/part/detail.html:127 msgid "Created By" msgstr "" -#: part/templates/part/detail.html:127 +#: part/templates/part/detail.html:134 msgid "Responsible User" msgstr "" -#: part/templates/part/detail.html:141 +#: part/templates/part/detail.html:148 msgid "Part is virtual (not a physical part)" msgstr "" -#: part/templates/part/detail.html:143 +#: part/templates/part/detail.html:150 msgid "Part is not a virtual part" msgstr "" -#: part/templates/part/detail.html:148 stock/forms.py:249 -#: templates/js/table_filters.js:23 templates/js/table_filters.js:256 -msgid "Template" -msgstr "" - -#: part/templates/part/detail.html:151 +#: part/templates/part/detail.html:158 msgid "Part is a template part (variants can be made from this part)" msgstr "" -#: part/templates/part/detail.html:153 +#: part/templates/part/detail.html:160 msgid "Part is not a template part" msgstr "" -#: part/templates/part/detail.html:161 +#: part/templates/part/detail.html:168 msgid "Part can be assembled from other parts" msgstr "" -#: part/templates/part/detail.html:163 +#: part/templates/part/detail.html:170 msgid "Part cannot be assembled from other parts" msgstr "" -#: part/templates/part/detail.html:171 +#: part/templates/part/detail.html:178 msgid "Part can be used in assemblies" msgstr "" -#: part/templates/part/detail.html:173 +#: part/templates/part/detail.html:180 msgid "Part cannot be used in assemblies" msgstr "" -#: part/templates/part/detail.html:181 +#: part/templates/part/detail.html:188 msgid "Part stock is tracked by serial number" msgstr "" -#: part/templates/part/detail.html:183 +#: part/templates/part/detail.html:190 msgid "Part stock is not tracked by serial number" msgstr "" -#: part/templates/part/detail.html:191 part/templates/part/detail.html:193 +#: part/templates/part/detail.html:198 part/templates/part/detail.html:200 msgid "Part can be purchased from external suppliers" msgstr "" -#: part/templates/part/detail.html:201 +#: part/templates/part/detail.html:208 msgid "Part can be sold to customers" msgstr "" -#: part/templates/part/detail.html:203 +#: part/templates/part/detail.html:210 msgid "Part cannot be sold to customers" msgstr "" -#: part/templates/part/detail.html:218 +#: part/templates/part/detail.html:225 msgid "Part is active" msgstr "" -#: part/templates/part/detail.html:220 +#: part/templates/part/detail.html:227 msgid "Part is not active" msgstr "" @@ -3178,12 +3250,12 @@ msgstr "" #: part/templates/part/params.html:15 #: templates/InvenTree/settings/category.html:29 -#: templates/InvenTree/settings/part.html:38 +#: templates/InvenTree/settings/part.html:41 msgid "New Parameter" msgstr "" -#: part/templates/part/params.html:25 stock/models.py:1420 -#: templates/js/stock.js:112 +#: part/templates/part/params.html:25 stock/models.py:1463 +#: templates/InvenTree/settings/header.html:8 templates/js/stock.js:113 msgid "Value" msgstr "" @@ -3218,19 +3290,19 @@ msgid "Star this part" msgstr "" #: part/templates/part/part_base.html:49 -#: stock/templates/stock/item_base.html:101 +#: stock/templates/stock/item_base.html:108 #: stock/templates/stock/location.html:29 msgid "Barcode actions" msgstr "" #: part/templates/part/part_base.html:51 -#: stock/templates/stock/item_base.html:103 +#: stock/templates/stock/item_base.html:110 #: stock/templates/stock/location.html:31 msgid "Show QR Code" msgstr "" #: part/templates/part/part_base.html:52 -#: stock/templates/stock/item_base.html:104 +#: stock/templates/stock/item_base.html:111 #: stock/templates/stock/location.html:32 msgid "Print Label" msgstr "" @@ -3259,7 +3331,7 @@ msgstr "" msgid "Delete part" msgstr "" -#: part/templates/part/part_base.html:124 templates/js/table_filters.js:111 +#: part/templates/part/part_base.html:124 templates/js/table_filters.js:121 msgid "In Stock" msgstr "" @@ -3368,7 +3440,7 @@ msgstr "" msgid "Used In" msgstr "" -#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:318 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:339 msgid "Tests" msgstr "" @@ -3396,220 +3468,220 @@ msgstr "" msgid "New Variant" msgstr "" -#: part/views.py:84 +#: part/views.py:86 msgid "Add Related Part" msgstr "" -#: part/views.py:140 +#: part/views.py:142 msgid "Delete Related Part" msgstr "" -#: part/views.py:152 +#: part/views.py:154 msgid "Add part attachment" msgstr "" -#: part/views.py:207 templates/attachment_table.html:34 +#: part/views.py:209 templates/attachment_table.html:34 msgid "Edit attachment" msgstr "" -#: part/views.py:213 +#: part/views.py:215 msgid "Part attachment updated" msgstr "" -#: part/views.py:228 +#: part/views.py:230 msgid "Delete Part Attachment" msgstr "" -#: part/views.py:236 +#: part/views.py:238 msgid "Deleted part attachment" msgstr "" -#: part/views.py:245 +#: part/views.py:247 msgid "Create Test Template" msgstr "" -#: part/views.py:274 +#: part/views.py:276 msgid "Edit Test Template" msgstr "" -#: part/views.py:290 +#: part/views.py:292 msgid "Delete Test Template" msgstr "" -#: part/views.py:299 +#: part/views.py:301 msgid "Set Part Category" msgstr "" -#: part/views.py:349 +#: part/views.py:351 #, python-brace-format msgid "Set category for {n} parts" msgstr "" -#: part/views.py:384 +#: part/views.py:386 msgid "Create Variant" msgstr "" -#: part/views.py:466 +#: part/views.py:468 msgid "Duplicate Part" msgstr "" -#: part/views.py:473 +#: part/views.py:475 msgid "Copied part" msgstr "" -#: part/views.py:527 part/views.py:661 +#: part/views.py:529 part/views.py:667 msgid "Possible matches exist - confirm creation of new part" msgstr "" -#: part/views.py:592 templates/js/stock.js:850 +#: part/views.py:594 templates/js/stock.js:876 msgid "Create New Part" msgstr "" -#: part/views.py:599 +#: part/views.py:601 msgid "Created new part" msgstr "" -#: part/views.py:830 +#: part/views.py:836 msgid "Part QR Code" msgstr "" -#: part/views.py:849 +#: part/views.py:855 msgid "Upload Part Image" msgstr "" -#: part/views.py:857 part/views.py:894 +#: part/views.py:863 part/views.py:900 msgid "Updated part image" msgstr "" -#: part/views.py:866 +#: part/views.py:872 msgid "Select Part Image" msgstr "" -#: part/views.py:897 +#: part/views.py:903 msgid "Part image not found" msgstr "" -#: part/views.py:908 +#: part/views.py:914 msgid "Edit Part Properties" msgstr "" -#: part/views.py:935 +#: part/views.py:945 msgid "Duplicate BOM" msgstr "" -#: part/views.py:966 +#: part/views.py:976 msgid "Confirm duplication of BOM from parent" msgstr "" -#: part/views.py:987 +#: part/views.py:997 msgid "Validate BOM" msgstr "" -#: part/views.py:1010 +#: part/views.py:1020 msgid "Confirm that the BOM is valid" msgstr "" -#: part/views.py:1021 +#: part/views.py:1031 msgid "Validated Bill of Materials" msgstr "" -#: part/views.py:1155 +#: part/views.py:1165 msgid "No BOM file provided" msgstr "" -#: part/views.py:1503 +#: part/views.py:1513 msgid "Enter a valid quantity" msgstr "" -#: part/views.py:1528 part/views.py:1531 +#: part/views.py:1538 part/views.py:1541 msgid "Select valid part" msgstr "" -#: part/views.py:1537 +#: part/views.py:1547 msgid "Duplicate part selected" msgstr "" -#: part/views.py:1575 +#: part/views.py:1585 msgid "Select a part" msgstr "" -#: part/views.py:1581 +#: part/views.py:1591 msgid "Selected part creates a circular BOM" msgstr "" -#: part/views.py:1585 +#: part/views.py:1595 msgid "Specify quantity" msgstr "" -#: part/views.py:1841 +#: part/views.py:1851 msgid "Confirm Part Deletion" msgstr "" -#: part/views.py:1850 +#: part/views.py:1860 msgid "Part was deleted" msgstr "" -#: part/views.py:1859 +#: part/views.py:1869 msgid "Part Pricing" msgstr "" -#: part/views.py:1973 +#: part/views.py:1983 msgid "Create Part Parameter Template" msgstr "" -#: part/views.py:1983 +#: part/views.py:1993 msgid "Edit Part Parameter Template" msgstr "" -#: part/views.py:1992 +#: part/views.py:2002 msgid "Delete Part Parameter Template" msgstr "" -#: part/views.py:2002 +#: part/views.py:2012 msgid "Create Part Parameter" msgstr "" -#: part/views.py:2054 +#: part/views.py:2064 msgid "Edit Part Parameter" msgstr "" -#: part/views.py:2070 +#: part/views.py:2080 msgid "Delete Part Parameter" msgstr "" -#: part/views.py:2129 +#: part/views.py:2139 msgid "Edit Part Category" msgstr "" -#: part/views.py:2166 +#: part/views.py:2176 msgid "Delete Part Category" msgstr "" -#: part/views.py:2174 +#: part/views.py:2184 msgid "Part category was deleted" msgstr "" -#: part/views.py:2230 +#: part/views.py:2240 msgid "Create Category Parameter Template" msgstr "" -#: part/views.py:2333 +#: part/views.py:2343 msgid "Edit Category Parameter Template" msgstr "" -#: part/views.py:2391 +#: part/views.py:2401 msgid "Delete Category Parameter Template" msgstr "" -#: part/views.py:2416 +#: part/views.py:2426 msgid "Create BOM Item" msgstr "" -#: part/views.py:2488 +#: part/views.py:2498 msgid "Edit BOM item" msgstr "" -#: part/views.py:2545 +#: part/views.py:2555 msgid "Confim BOM item deletion" msgstr "" @@ -3641,295 +3713,305 @@ msgstr "" msgid "Asset file description" msgstr "" -#: stock/forms.py:111 +#: stock/forms.py:116 msgid "Enter unique serial numbers (or leave blank)" msgstr "" -#: stock/forms.py:192 +#: stock/forms.py:198 msgid "Label" msgstr "" -#: stock/forms.py:193 stock/forms.py:249 +#: stock/forms.py:199 stock/forms.py:255 msgid "Select test report template" msgstr "" -#: stock/forms.py:257 +#: stock/forms.py:263 msgid "Include stock items in sub locations" msgstr "" -#: stock/forms.py:292 +#: stock/forms.py:298 msgid "Stock item to install" msgstr "" -#: stock/forms.py:299 +#: stock/forms.py:305 msgid "Stock quantity to assign" msgstr "" -#: stock/forms.py:327 +#: stock/forms.py:333 msgid "Must not exceed available quantity" msgstr "" -#: stock/forms.py:337 +#: stock/forms.py:343 msgid "Destination location for uninstalled items" msgstr "" -#: stock/forms.py:339 +#: stock/forms.py:345 msgid "Add transaction note (optional)" msgstr "" -#: stock/forms.py:341 +#: stock/forms.py:347 msgid "Confirm uninstall" msgstr "" -#: stock/forms.py:341 +#: stock/forms.py:347 msgid "Confirm removal of installed stock items" msgstr "" -#: stock/forms.py:365 +#: stock/forms.py:371 msgid "Destination stock location" msgstr "" -#: stock/forms.py:367 +#: stock/forms.py:373 msgid "Add note (required)" msgstr "" -#: stock/forms.py:371 stock/views.py:935 stock/views.py:1133 +#: stock/forms.py:377 stock/views.py:935 stock/views.py:1133 msgid "Confirm stock adjustment" msgstr "" -#: stock/forms.py:371 +#: stock/forms.py:377 msgid "Confirm movement of stock items" msgstr "" -#: stock/forms.py:373 +#: stock/forms.py:379 msgid "Set Default Location" msgstr "" -#: stock/forms.py:373 +#: stock/forms.py:379 msgid "Set the destination as the default location for selected parts" msgstr "" -#: stock/models.py:179 +#: stock/models.py:185 msgid "Created stock item" msgstr "" -#: stock/models.py:215 +#: stock/models.py:221 msgid "StockItem with this serial number already exists" msgstr "" -#: stock/models.py:251 +#: stock/models.py:257 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "" -#: stock/models.py:261 stock/models.py:270 +#: stock/models.py:267 stock/models.py:276 msgid "Quantity must be 1 for item with a serial number" msgstr "" -#: stock/models.py:262 +#: stock/models.py:268 msgid "Serial number cannot be set if quantity greater than 1" msgstr "" -#: stock/models.py:284 +#: stock/models.py:290 msgid "Item cannot belong to itself" msgstr "" -#: stock/models.py:290 +#: stock/models.py:296 msgid "Item must have a build reference if is_building=True" msgstr "" -#: stock/models.py:297 +#: stock/models.py:303 msgid "Build reference does not point to the same part object" msgstr "" -#: stock/models.py:330 +#: stock/models.py:336 msgid "Parent Stock Item" msgstr "" -#: stock/models.py:339 +#: stock/models.py:345 msgid "Base part" msgstr "" -#: stock/models.py:348 +#: stock/models.py:354 msgid "Select a matching supplier part for this stock item" msgstr "" -#: stock/models.py:353 stock/templates/stock/stock_app_base.html:7 +#: stock/models.py:359 stock/templates/stock/stock_app_base.html:7 msgid "Stock Location" msgstr "" -#: stock/models.py:356 +#: stock/models.py:362 msgid "Where is this stock item located?" msgstr "" -#: stock/models.py:361 stock/templates/stock/item_base.html:212 +#: stock/models.py:367 stock/templates/stock/item_base.html:219 msgid "Installed In" msgstr "" -#: stock/models.py:364 +#: stock/models.py:370 msgid "Is this item installed in another item?" msgstr "" -#: stock/models.py:380 +#: stock/models.py:386 msgid "Serial number for this item" msgstr "" -#: stock/models.py:392 +#: stock/models.py:398 msgid "Batch code for this stock item" msgstr "" -#: stock/models.py:396 +#: stock/models.py:402 msgid "Stock Quantity" msgstr "" -#: stock/models.py:405 +#: stock/models.py:411 msgid "Source Build" msgstr "" -#: stock/models.py:407 +#: stock/models.py:413 msgid "Build for this stock item" msgstr "" -#: stock/models.py:418 +#: stock/models.py:424 msgid "Source Purchase Order" msgstr "" -#: stock/models.py:421 +#: stock/models.py:427 msgid "Purchase order for this stock item" msgstr "" -#: stock/models.py:427 +#: stock/models.py:433 msgid "Destination Sales Order" msgstr "" -#: stock/models.py:439 +#: stock/models.py:439 stock/templates/stock/item_base.html:306 +#: templates/js/stock.js:597 +msgid "Expiry Date" +msgstr "" + +#: stock/models.py:440 +msgid "" +"Expiry date for stock item. Stock will be considered expired after this date" +msgstr "" + +#: stock/models.py:453 msgid "Delete this Stock Item when stock is depleted" msgstr "" -#: stock/models.py:449 stock/templates/stock/item_notes.html:14 +#: stock/models.py:463 stock/templates/stock/item_notes.html:14 #: stock/templates/stock/item_notes.html:30 msgid "Stock Item Notes" msgstr "" -#: stock/models.py:459 +#: stock/models.py:473 msgid "Single unit purchase price at time of purchase" msgstr "" -#: stock/models.py:510 +#: stock/models.py:573 msgid "Assigned to Customer" msgstr "" -#: stock/models.py:512 +#: stock/models.py:575 msgid "Manually assigned to customer" msgstr "" -#: stock/models.py:525 +#: stock/models.py:588 msgid "Returned from customer" msgstr "" -#: stock/models.py:527 +#: stock/models.py:590 msgid "Returned to location" msgstr "" -#: stock/models.py:652 +#: stock/models.py:715 msgid "Installed into stock item" msgstr "" -#: stock/models.py:660 +#: stock/models.py:723 msgid "Installed stock item" msgstr "" -#: stock/models.py:684 +#: stock/models.py:747 msgid "Uninstalled stock item" msgstr "" -#: stock/models.py:703 +#: stock/models.py:766 msgid "Uninstalled into location" msgstr "" -#: stock/models.py:803 +#: stock/models.py:846 msgid "Part is not set as trackable" msgstr "" -#: stock/models.py:809 +#: stock/models.py:852 msgid "Quantity must be integer" msgstr "" -#: stock/models.py:815 +#: stock/models.py:858 #, python-brace-format msgid "Quantity must not exceed available stock quantity ({n})" msgstr "" -#: stock/models.py:818 +#: stock/models.py:861 msgid "Serial numbers must be a list of integers" msgstr "" -#: stock/models.py:821 +#: stock/models.py:864 msgid "Quantity does not match serial numbers" msgstr "" -#: stock/models.py:853 +#: stock/models.py:896 msgid "Add serial number" msgstr "" -#: stock/models.py:856 +#: stock/models.py:899 #, python-brace-format msgid "Serialized {n} items" msgstr "" -#: stock/models.py:967 +#: stock/models.py:1010 msgid "StockItem cannot be moved as it is not in stock" msgstr "" -#: stock/models.py:1321 +#: stock/models.py:1364 msgid "Tracking entry title" msgstr "" -#: stock/models.py:1323 +#: stock/models.py:1366 msgid "Entry notes" msgstr "" -#: stock/models.py:1325 +#: stock/models.py:1368 msgid "Link to external page for further information" msgstr "" -#: stock/models.py:1385 +#: stock/models.py:1428 msgid "Value must be provided for this test" msgstr "" -#: stock/models.py:1391 +#: stock/models.py:1434 msgid "Attachment must be uploaded for this test" msgstr "" -#: stock/models.py:1408 +#: stock/models.py:1451 msgid "Test" msgstr "" -#: stock/models.py:1409 +#: stock/models.py:1452 msgid "Test name" msgstr "" -#: stock/models.py:1414 +#: stock/models.py:1457 msgid "Result" msgstr "" -#: stock/models.py:1415 templates/js/table_filters.js:162 +#: stock/models.py:1458 templates/js/table_filters.js:172 msgid "Test result" msgstr "" -#: stock/models.py:1421 +#: stock/models.py:1464 msgid "Test output value" msgstr "" -#: stock/models.py:1427 +#: stock/models.py:1470 msgid "Attachment" msgstr "" -#: stock/models.py:1428 +#: stock/models.py:1471 msgid "Test result attachment" msgstr "" -#: stock/models.py:1434 +#: stock/models.py:1477 msgid "Test notes" msgstr "" @@ -3980,111 +4062,129 @@ msgid "" "This stock item will be automatically deleted when all stock is depleted." msgstr "" -#: stock/templates/stock/item_base.html:107 templates/js/barcode.js:283 +#: stock/templates/stock/item_base.html:74 +#: stock/templates/stock/item_base.html:310 templates/js/table_filters.js:111 +msgid "Expired" +msgstr "" + +#: stock/templates/stock/item_base.html:78 +#: stock/templates/stock/item_base.html:312 templates/js/table_filters.js:116 +msgid "Stale" +msgstr "" + +#: stock/templates/stock/item_base.html:114 templates/js/barcode.js:283 #: templates/js/barcode.js:288 msgid "Unlink Barcode" msgstr "" -#: stock/templates/stock/item_base.html:109 +#: stock/templates/stock/item_base.html:116 msgid "Link Barcode" msgstr "" -#: stock/templates/stock/item_base.html:117 +#: stock/templates/stock/item_base.html:124 msgid "Stock adjustment actions" msgstr "" -#: stock/templates/stock/item_base.html:121 +#: stock/templates/stock/item_base.html:128 #: stock/templates/stock/location.html:41 templates/stock_table.html:23 msgid "Count stock" msgstr "" -#: stock/templates/stock/item_base.html:122 templates/stock_table.html:21 +#: stock/templates/stock/item_base.html:129 templates/stock_table.html:21 msgid "Add stock" msgstr "" -#: stock/templates/stock/item_base.html:123 templates/stock_table.html:22 +#: stock/templates/stock/item_base.html:130 templates/stock_table.html:22 msgid "Remove stock" msgstr "" -#: stock/templates/stock/item_base.html:125 +#: stock/templates/stock/item_base.html:132 msgid "Transfer stock" msgstr "" -#: stock/templates/stock/item_base.html:127 +#: stock/templates/stock/item_base.html:134 msgid "Serialize stock" msgstr "" -#: stock/templates/stock/item_base.html:131 +#: stock/templates/stock/item_base.html:138 msgid "Assign to customer" msgstr "" -#: stock/templates/stock/item_base.html:134 +#: stock/templates/stock/item_base.html:141 msgid "Return to stock" msgstr "" -#: stock/templates/stock/item_base.html:138 templates/js/stock.js:991 +#: stock/templates/stock/item_base.html:145 templates/js/stock.js:1017 msgid "Uninstall stock item" msgstr "" -#: stock/templates/stock/item_base.html:138 +#: stock/templates/stock/item_base.html:145 msgid "Uninstall" msgstr "" -#: stock/templates/stock/item_base.html:147 +#: stock/templates/stock/item_base.html:154 #: stock/templates/stock/location.html:38 msgid "Stock actions" msgstr "" -#: stock/templates/stock/item_base.html:150 +#: stock/templates/stock/item_base.html:157 msgid "Convert to variant" msgstr "" -#: stock/templates/stock/item_base.html:153 +#: stock/templates/stock/item_base.html:160 msgid "Duplicate stock item" msgstr "" -#: stock/templates/stock/item_base.html:155 +#: stock/templates/stock/item_base.html:162 msgid "Edit stock item" msgstr "" -#: stock/templates/stock/item_base.html:158 +#: stock/templates/stock/item_base.html:165 msgid "Delete stock item" msgstr "" -#: stock/templates/stock/item_base.html:164 +#: stock/templates/stock/item_base.html:171 msgid "Generate test report" msgstr "" -#: stock/templates/stock/item_base.html:172 +#: stock/templates/stock/item_base.html:179 msgid "Stock Item Details" msgstr "" -#: stock/templates/stock/item_base.html:231 templates/js/build.js:442 +#: stock/templates/stock/item_base.html:238 templates/js/build.js:442 msgid "No location set" msgstr "" -#: stock/templates/stock/item_base.html:238 +#: stock/templates/stock/item_base.html:245 msgid "Barcode Identifier" msgstr "" -#: stock/templates/stock/item_base.html:252 templates/js/build.js:642 +#: stock/templates/stock/item_base.html:259 templates/js/build.js:642 #: templates/navbar.html:25 msgid "Build" msgstr "" -#: stock/templates/stock/item_base.html:273 +#: stock/templates/stock/item_base.html:280 msgid "Parent Item" msgstr "" -#: stock/templates/stock/item_base.html:298 +#: stock/templates/stock/item_base.html:310 +msgid "This StockItem expired on" +msgstr "" + +#: stock/templates/stock/item_base.html:312 +msgid "This StockItem expires on" +msgstr "" + +#: stock/templates/stock/item_base.html:319 msgid "Last Updated" msgstr "" -#: stock/templates/stock/item_base.html:303 +#: stock/templates/stock/item_base.html:324 msgid "Last Stocktake" msgstr "" -#: stock/templates/stock/item_base.html:307 +#: stock/templates/stock/item_base.html:328 msgid "No stocktake performed" msgstr "" @@ -4204,7 +4304,7 @@ msgstr "" msgid "The following stock items will be uninstalled" msgstr "" -#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1331 +#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1335 msgid "Convert Stock Item" msgstr "" @@ -4402,39 +4502,39 @@ msgstr "" msgid "Edit Stock Item" msgstr "" -#: stock/views.py:1381 +#: stock/views.py:1385 msgid "Serialize Stock" msgstr "" -#: stock/views.py:1475 templates/js/build.js:210 +#: stock/views.py:1479 templates/js/build.js:210 msgid "Create new Stock Item" msgstr "" -#: stock/views.py:1579 +#: stock/views.py:1587 msgid "Duplicate Stock Item" msgstr "" -#: stock/views.py:1651 +#: stock/views.py:1664 msgid "Quantity cannot be negative" msgstr "" -#: stock/views.py:1737 +#: stock/views.py:1750 msgid "Delete Stock Location" msgstr "" -#: stock/views.py:1751 +#: stock/views.py:1764 msgid "Delete Stock Item" msgstr "" -#: stock/views.py:1763 +#: stock/views.py:1776 msgid "Delete Stock Tracking Entry" msgstr "" -#: stock/views.py:1782 +#: stock/views.py:1795 msgid "Edit Stock Tracking Entry" msgstr "" -#: stock/views.py:1792 +#: stock/views.py:1805 msgid "Add Stock Tracking Entry" msgstr "" @@ -4466,7 +4566,11 @@ msgstr "" msgid "Pending Builds" msgstr "" -#: templates/InvenTree/index.html:4 +#: templates/InvenTree/expired_stock.html:7 +msgid "Expired Stock" +msgstr "" + +#: templates/InvenTree/index.html:5 msgid "Index" msgstr "" @@ -4494,11 +4598,11 @@ msgstr "" msgid "Enter a search query" msgstr "" -#: templates/InvenTree/search.html:191 templates/js/stock.js:289 +#: templates/InvenTree/search.html:191 templates/js/stock.js:290 msgid "Shipped to customer" msgstr "" -#: templates/InvenTree/search.html:194 templates/js/stock.js:299 +#: templates/InvenTree/search.html:194 templates/js/stock.js:300 msgid "No stock location set" msgstr "" @@ -4527,12 +4631,12 @@ msgid "Default Value" msgstr "" #: templates/InvenTree/settings/category.html:70 -#: templates/InvenTree/settings/part.html:75 +#: templates/InvenTree/settings/part.html:78 msgid "Edit Template" msgstr "" #: templates/InvenTree/settings/category.html:71 -#: templates/InvenTree/settings/part.html:76 +#: templates/InvenTree/settings/part.html:79 msgid "Delete Template" msgstr "" @@ -4540,6 +4644,10 @@ msgstr "" msgid "Global InvenTree Settings" msgstr "" +#: templates/InvenTree/settings/header.html:7 +msgid "Setting" +msgstr "" + #: templates/InvenTree/settings/part.html:9 msgid "Part Settings" msgstr "" @@ -4548,11 +4656,11 @@ msgstr "" msgid "Part Options" msgstr "" -#: templates/InvenTree/settings/part.html:34 +#: templates/InvenTree/settings/part.html:37 msgid "Part Parameter Templates" msgstr "" -#: templates/InvenTree/settings/part.html:55 +#: templates/InvenTree/settings/part.html:58 msgid "No part parameter templates found" msgstr "" @@ -4560,11 +4668,11 @@ msgstr "" msgid "Purchase Order Settings" msgstr "" -#: templates/InvenTree/settings/setting.html:16 +#: templates/InvenTree/settings/setting.html:23 msgid "No value set" msgstr "" -#: templates/InvenTree/settings/setting.html:24 +#: templates/InvenTree/settings/setting.html:31 msgid "Edit setting" msgstr "" @@ -4581,6 +4689,10 @@ msgstr "" msgid "Stock Settings" msgstr "" +#: templates/InvenTree/settings/stock.html:13 +msgid "Stock Options" +msgstr "" + #: templates/InvenTree/settings/tabs.html:3 #: templates/InvenTree/settings/user.html:10 msgid "User Settings" @@ -4656,6 +4768,10 @@ msgstr "" msgid "Overdue Sales Orders" msgstr "" +#: templates/InvenTree/stale_stock.html:7 +msgid "Stale Stock" +msgstr "" + #: templates/InvenTree/starred_parts.html:7 msgid "Starred Parts" msgstr "" @@ -4917,7 +5033,7 @@ msgstr "" msgid "No purchase orders found" msgstr "" -#: templates/js/order.js:188 templates/js/stock.js:687 +#: templates/js/order.js:188 templates/js/stock.js:702 msgid "Date" msgstr "" @@ -4957,8 +5073,8 @@ msgstr "" msgid "No parts found" msgstr "" -#: templates/js/part.js:343 templates/js/stock.js:462 -#: templates/js/stock.js:1023 +#: templates/js/part.js:343 templates/js/stock.js:463 +#: templates/js/stock.js:1049 msgid "Select" msgstr "" @@ -4966,7 +5082,7 @@ msgstr "" msgid "No category" msgstr "" -#: templates/js/part.js:429 templates/js/table_filters.js:264 +#: templates/js/part.js:429 templates/js/table_filters.js:274 msgid "Low stock" msgstr "" @@ -4986,11 +5102,11 @@ msgstr "" msgid "No test templates matching query" msgstr "" -#: templates/js/part.js:604 templates/js/stock.js:63 +#: templates/js/part.js:604 templates/js/stock.js:64 msgid "Edit test result" msgstr "" -#: templates/js/part.js:605 templates/js/stock.js:64 +#: templates/js/part.js:605 templates/js/stock.js:65 msgid "Delete test result" msgstr "" @@ -4998,103 +5114,111 @@ msgstr "" msgid "This test is defined for a parent part" msgstr "" -#: templates/js/stock.js:26 +#: templates/js/stock.js:27 msgid "PASS" msgstr "" -#: templates/js/stock.js:28 +#: templates/js/stock.js:29 msgid "FAIL" msgstr "" -#: templates/js/stock.js:33 +#: templates/js/stock.js:34 msgid "NO RESULT" msgstr "" -#: templates/js/stock.js:59 +#: templates/js/stock.js:60 msgid "Add test result" msgstr "" -#: templates/js/stock.js:78 +#: templates/js/stock.js:79 msgid "No test results found" msgstr "" -#: templates/js/stock.js:120 +#: templates/js/stock.js:121 msgid "Test Date" msgstr "" -#: templates/js/stock.js:281 +#: templates/js/stock.js:282 msgid "In production" msgstr "" -#: templates/js/stock.js:285 +#: templates/js/stock.js:286 msgid "Installed in Stock Item" msgstr "" -#: templates/js/stock.js:293 +#: templates/js/stock.js:294 msgid "Assigned to Sales Order" msgstr "" -#: templates/js/stock.js:313 +#: templates/js/stock.js:314 msgid "No stock items matching query" msgstr "" -#: templates/js/stock.js:430 +#: templates/js/stock.js:431 msgid "Undefined location" msgstr "" -#: templates/js/stock.js:524 +#: templates/js/stock.js:525 msgid "Stock item is in production" msgstr "" -#: templates/js/stock.js:529 +#: templates/js/stock.js:530 msgid "Stock item assigned to sales order" msgstr "" -#: templates/js/stock.js:532 +#: templates/js/stock.js:533 msgid "Stock item assigned to customer" msgstr "" -#: templates/js/stock.js:536 +#: templates/js/stock.js:537 +msgid "Stock item has expired" +msgstr "" + +#: templates/js/stock.js:539 +msgid "Stock item will expire soon" +msgstr "" + +#: templates/js/stock.js:543 msgid "Stock item has been allocated" msgstr "" -#: templates/js/stock.js:540 +#: templates/js/stock.js:547 msgid "Stock item has been installed in another item" msgstr "" -#: templates/js/stock.js:548 +#: templates/js/stock.js:555 msgid "Stock item has been rejected" msgstr "" -#: templates/js/stock.js:552 +#: templates/js/stock.js:559 msgid "Stock item is lost" msgstr "" -#: templates/js/stock.js:555 +#: templates/js/stock.js:562 msgid "Stock item is destroyed" msgstr "" -#: templates/js/stock.js:559 templates/js/table_filters.js:106 +#: templates/js/stock.js:566 templates/js/table_filters.js:106 msgid "Depleted" msgstr "" -#: templates/js/stock.js:753 +#: templates/js/stock.js:768 msgid "No user information" msgstr "" -#: templates/js/stock.js:862 +#: templates/js/stock.js:888 msgid "Create New Location" msgstr "" -#: templates/js/stock.js:961 +#: templates/js/stock.js:987 msgid "Serial" msgstr "" -#: templates/js/stock.js:1054 templates/js/table_filters.js:121 +#: templates/js/stock.js:1080 templates/js/table_filters.js:131 msgid "Installed" msgstr "" -#: templates/js/stock.js:1079 +#: templates/js/stock.js:1105 msgid "Install item" msgstr "" @@ -5106,36 +5230,36 @@ msgstr "" msgid "Validated" msgstr "" -#: templates/js/table_filters.js:65 templates/js/table_filters.js:131 +#: templates/js/table_filters.js:65 templates/js/table_filters.js:141 msgid "Is Serialized" msgstr "" -#: templates/js/table_filters.js:68 templates/js/table_filters.js:138 +#: templates/js/table_filters.js:68 templates/js/table_filters.js:148 msgid "Serial number GTE" msgstr "" -#: templates/js/table_filters.js:69 templates/js/table_filters.js:139 +#: templates/js/table_filters.js:69 templates/js/table_filters.js:149 msgid "Serial number greater than or equal to" msgstr "" -#: templates/js/table_filters.js:72 templates/js/table_filters.js:142 +#: templates/js/table_filters.js:72 templates/js/table_filters.js:152 msgid "Serial number LTE" msgstr "" -#: templates/js/table_filters.js:73 templates/js/table_filters.js:143 +#: templates/js/table_filters.js:73 templates/js/table_filters.js:153 msgid "Serial number less than or equal to" msgstr "" #: templates/js/table_filters.js:76 templates/js/table_filters.js:77 -#: templates/js/table_filters.js:134 templates/js/table_filters.js:135 +#: templates/js/table_filters.js:144 templates/js/table_filters.js:145 msgid "Serial number" msgstr "" -#: templates/js/table_filters.js:81 templates/js/table_filters.js:152 +#: templates/js/table_filters.js:81 templates/js/table_filters.js:162 msgid "Batch code" msgstr "" -#: templates/js/table_filters.js:91 templates/js/table_filters.js:231 +#: templates/js/table_filters.js:91 templates/js/table_filters.js:241 msgid "Active parts" msgstr "" @@ -5164,74 +5288,82 @@ msgid "Show stock items which are depleted" msgstr "" #: templates/js/table_filters.js:112 -msgid "Show items which are in stock" -msgstr "" - -#: templates/js/table_filters.js:116 -msgid "In Production" +msgid "Show stock items which have expired" msgstr "" #: templates/js/table_filters.js:117 -msgid "Show items which are in production" +msgid "Show stock which is close to expiring" msgstr "" #: templates/js/table_filters.js:122 -msgid "Show stock items which are installed in another item" +msgid "Show items which are in stock" msgstr "" #: templates/js/table_filters.js:126 -msgid "Sent to customer" +msgid "In Production" msgstr "" #: templates/js/table_filters.js:127 +msgid "Show items which are in production" +msgstr "" + +#: templates/js/table_filters.js:132 +msgid "Show stock items which are installed in another item" +msgstr "" + +#: templates/js/table_filters.js:136 +msgid "Sent to customer" +msgstr "" + +#: templates/js/table_filters.js:137 msgid "Show items which have been assigned to a customer" msgstr "" -#: templates/js/table_filters.js:147 templates/js/table_filters.js:148 +#: templates/js/table_filters.js:157 templates/js/table_filters.js:158 msgid "Stock status" msgstr "" -#: templates/js/table_filters.js:181 +#: templates/js/table_filters.js:191 msgid "Build status" msgstr "" -#: templates/js/table_filters.js:200 templates/js/table_filters.js:213 +#: templates/js/table_filters.js:210 templates/js/table_filters.js:223 msgid "Order status" msgstr "" -#: templates/js/table_filters.js:205 templates/js/table_filters.js:218 +#: templates/js/table_filters.js:215 templates/js/table_filters.js:228 msgid "Outstanding" msgstr "" -#: templates/js/table_filters.js:241 +#: templates/js/table_filters.js:251 msgid "Include subcategories" msgstr "" -#: templates/js/table_filters.js:242 +#: templates/js/table_filters.js:252 msgid "Include parts in subcategories" msgstr "" -#: templates/js/table_filters.js:246 +#: templates/js/table_filters.js:256 msgid "Has IPN" msgstr "" -#: templates/js/table_filters.js:247 +#: templates/js/table_filters.js:257 msgid "Part has internal part number" msgstr "" -#: templates/js/table_filters.js:252 +#: templates/js/table_filters.js:262 msgid "Show active parts" msgstr "" -#: templates/js/table_filters.js:260 +#: templates/js/table_filters.js:270 msgid "Stock available" msgstr "" -#: templates/js/table_filters.js:276 +#: templates/js/table_filters.js:286 msgid "Starred" msgstr "" -#: templates/js/table_filters.js:288 +#: templates/js/table_filters.js:298 msgid "Purchasable" msgstr "" diff --git a/InvenTree/locale/es/LC_MESSAGES/django.po b/InvenTree/locale/es/LC_MESSAGES/django.po index a2b2081ed3..395f42c4e6 100644 --- a/InvenTree/locale/es/LC_MESSAGES/django.po +++ b/InvenTree/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-01-03 22:16+1100\n" +"POT-Creation-Date: 2021-01-06 23:11+1100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -55,7 +55,7 @@ msgid "Select Category" msgstr "" #: InvenTree/helpers.py:361 order/models.py:178 order/models.py:260 -#: stock/views.py:1647 +#: stock/views.py:1660 msgid "Invalid quantity provided" msgstr "" @@ -95,12 +95,12 @@ msgstr "" msgid "File comment" msgstr "" -#: InvenTree/models.py:68 templates/js/stock.js:744 +#: InvenTree/models.py:68 templates/js/stock.js:759 msgid "User" msgstr "" -#: InvenTree/models.py:106 part/templates/part/params.html:24 -#: templates/js/part.js:129 +#: InvenTree/models.py:106 part/models.py:647 +#: part/templates/part/params.html:24 templates/js/part.js:129 msgid "Name" msgstr "" @@ -295,14 +295,14 @@ msgstr "" msgid "Order target date" msgstr "" -#: build/forms.py:39 build/models.py:173 +#: build/forms.py:39 build/models.py:175 msgid "" "Target date for build completion. Build will be overdue after this date." msgstr "" #: build/forms.py:78 build/templates/build/auto_allocate.html:17 #: build/templates/build/build_base.html:83 -#: build/templates/build/detail.html:29 common/models.py:496 +#: build/templates/build/detail.html:29 common/models.py:589 #: company/forms.py:112 company/templates/company/supplier_part_pricing.html:75 #: order/templates/order/order_wizard/select_parts.html:32 #: order/templates/order/purchase_order_detail.html:179 @@ -310,13 +310,13 @@ msgstr "" #: order/templates/order/sales_order_detail.html:156 #: part/templates/part/allocation.html:16 #: part/templates/part/allocation.html:49 -#: part/templates/part/sale_prices.html:82 stock/forms.py:298 +#: part/templates/part/sale_prices.html:82 stock/forms.py:304 #: stock/templates/stock/item_base.html:40 #: stock/templates/stock/item_base.html:46 -#: stock/templates/stock/item_base.html:197 +#: stock/templates/stock/item_base.html:204 #: stock/templates/stock/stock_adjust.html:18 templates/js/barcode.js:338 -#: templates/js/bom.js:195 templates/js/build.js:420 templates/js/stock.js:735 -#: templates/js/stock.js:963 +#: templates/js/bom.js:195 templates/js/build.js:420 templates/js/stock.js:750 +#: templates/js/stock.js:989 msgid "Quantity" msgstr "" @@ -324,7 +324,7 @@ msgstr "" msgid "Enter quantity for build output" msgstr "" -#: build/forms.py:83 stock/forms.py:111 +#: build/forms.py:83 stock/forms.py:116 msgid "Serial numbers" msgstr "" @@ -372,222 +372,223 @@ msgstr "" msgid "Select quantity of stock to allocate" msgstr "" -#: build/models.py:59 build/templates/build/build_base.html:8 +#: build/models.py:61 build/templates/build/build_base.html:8 #: build/templates/build/build_base.html:35 #: part/templates/part/allocation.html:20 msgid "Build Order" msgstr "" -#: build/models.py:60 build/templates/build/index.html:6 +#: build/models.py:62 build/templates/build/index.html:6 #: build/templates/build/index.html:14 order/templates/order/so_builds.html:11 #: order/templates/order/so_tabs.html:9 part/templates/part/tabs.html:31 #: templates/InvenTree/settings/tabs.html:28 users/models.py:30 msgid "Build Orders" msgstr "" -#: build/models.py:75 +#: build/models.py:77 msgid "Build Order Reference" msgstr "" -#: build/models.py:76 order/templates/order/purchase_order_detail.html:174 +#: build/models.py:78 order/templates/order/purchase_order_detail.html:174 #: templates/js/bom.js:187 templates/js/build.js:509 msgid "Reference" msgstr "" -#: build/models.py:83 build/templates/build/detail.html:19 +#: build/models.py:85 build/templates/build/detail.html:19 #: company/models.py:359 company/templates/company/detail.html:23 #: company/templates/company/supplier_part_base.html:61 #: company/templates/company/supplier_part_detail.html:27 -#: order/templates/order/purchase_order_detail.html:161 +#: order/templates/order/purchase_order_detail.html:161 part/models.py:671 #: part/templates/part/detail.html:51 part/templates/part/set_category.html:14 -#: templates/InvenTree/search.html:147 templates/js/bom.js:180 +#: templates/InvenTree/search.html:147 +#: templates/InvenTree/settings/header.html:9 templates/js/bom.js:180 #: templates/js/bom.js:517 templates/js/build.js:664 templates/js/company.js:56 #: templates/js/order.js:175 templates/js/order.js:263 templates/js/part.js:188 #: templates/js/part.js:271 templates/js/part.js:391 templates/js/part.js:572 -#: templates/js/stock.js:500 templates/js/stock.js:716 +#: templates/js/stock.js:501 templates/js/stock.js:731 msgid "Description" msgstr "" -#: build/models.py:86 +#: build/models.py:88 msgid "Brief description of the build" msgstr "" -#: build/models.py:95 build/templates/build/build_base.html:104 +#: build/models.py:97 build/templates/build/build_base.html:104 #: build/templates/build/detail.html:75 msgid "Parent Build" msgstr "" -#: build/models.py:96 +#: build/models.py:98 msgid "BuildOrder to which this build is allocated" msgstr "" -#: build/models.py:101 build/templates/build/auto_allocate.html:16 +#: build/models.py:103 build/templates/build/auto_allocate.html:16 #: build/templates/build/build_base.html:78 #: build/templates/build/detail.html:24 order/models.py:548 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:148 -#: order/templates/order/receive_parts.html:19 part/models.py:315 +#: order/templates/order/receive_parts.html:19 part/models.py:316 #: part/templates/part/part_app_base.html:7 part/templates/part/related.html:26 #: part/templates/part/set_category.html:13 templates/InvenTree/search.html:133 #: templates/js/barcode.js:336 templates/js/bom.js:153 templates/js/bom.js:502 #: templates/js/build.js:669 templates/js/company.js:138 -#: templates/js/part.js:252 templates/js/part.js:357 templates/js/stock.js:474 -#: templates/js/stock.js:1035 +#: templates/js/part.js:252 templates/js/part.js:357 templates/js/stock.js:475 +#: templates/js/stock.js:1061 msgid "Part" msgstr "" -#: build/models.py:109 +#: build/models.py:111 msgid "Select part to build" msgstr "" -#: build/models.py:114 +#: build/models.py:116 msgid "Sales Order Reference" msgstr "" -#: build/models.py:118 +#: build/models.py:120 msgid "SalesOrder to which this build is allocated" msgstr "" -#: build/models.py:123 +#: build/models.py:125 msgid "Source Location" msgstr "" -#: build/models.py:127 +#: build/models.py:129 msgid "" "Select location to take stock from for this build (leave blank to take from " "any stock location)" msgstr "" -#: build/models.py:132 +#: build/models.py:134 msgid "Destination Location" msgstr "" -#: build/models.py:136 +#: build/models.py:138 msgid "Select location where the completed items will be stored" msgstr "" -#: build/models.py:140 +#: build/models.py:142 msgid "Build Quantity" msgstr "" -#: build/models.py:143 +#: build/models.py:145 msgid "Number of stock items to build" msgstr "" -#: build/models.py:147 +#: build/models.py:149 msgid "Completed items" msgstr "" -#: build/models.py:149 +#: build/models.py:151 msgid "Number of stock items which have been completed" msgstr "" -#: build/models.py:153 part/templates/part/part_base.html:155 +#: build/models.py:155 part/templates/part/part_base.html:155 msgid "Build Status" msgstr "" -#: build/models.py:157 +#: build/models.py:159 msgid "Build status code" msgstr "" -#: build/models.py:161 stock/models.py:390 +#: build/models.py:163 stock/models.py:396 msgid "Batch Code" msgstr "" -#: build/models.py:165 +#: build/models.py:167 msgid "Batch code for this build output" msgstr "" -#: build/models.py:172 order/models.py:329 +#: build/models.py:174 order/models.py:329 msgid "Target completion date" msgstr "" -#: build/models.py:186 build/templates/build/detail.html:89 +#: build/models.py:188 build/templates/build/detail.html:89 #: company/templates/company/supplier_part_base.html:68 #: company/templates/company/supplier_part_detail.html:24 #: part/templates/part/detail.html:80 part/templates/part/part_base.html:102 -#: stock/models.py:384 stock/templates/stock/item_base.html:280 +#: stock/models.py:390 stock/templates/stock/item_base.html:287 msgid "External Link" msgstr "" -#: build/models.py:187 part/models.py:672 stock/models.py:386 +#: build/models.py:189 part/models.py:705 stock/models.py:392 msgid "Link to external URL" msgstr "" -#: build/models.py:191 build/templates/build/tabs.html:23 company/models.py:366 +#: build/models.py:193 build/templates/build/tabs.html:23 company/models.py:366 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:18 #: order/templates/order/purchase_order_detail.html:213 -#: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:73 -#: stock/forms.py:307 stock/forms.py:339 stock/forms.py:367 stock/models.py:448 -#: stock/models.py:1433 stock/templates/stock/tabs.html:26 -#: templates/js/barcode.js:391 templates/js/bom.js:263 -#: templates/js/stock.js:116 templates/js/stock.js:588 +#: order/templates/order/so_tabs.html:23 part/models.py:831 +#: part/templates/part/tabs.html:73 stock/forms.py:313 stock/forms.py:345 +#: stock/forms.py:373 stock/models.py:462 stock/models.py:1476 +#: stock/templates/stock/tabs.html:26 templates/js/barcode.js:391 +#: templates/js/bom.js:263 templates/js/stock.js:117 templates/js/stock.js:603 msgid "Notes" msgstr "" -#: build/models.py:192 +#: build/models.py:194 msgid "Extra build notes" msgstr "" -#: build/models.py:577 +#: build/models.py:579 msgid "No build output specified" msgstr "" -#: build/models.py:580 +#: build/models.py:582 msgid "Build output is already completed" msgstr "" -#: build/models.py:583 +#: build/models.py:585 msgid "Build output does not match Build Order" msgstr "" -#: build/models.py:658 +#: build/models.py:660 msgid "Completed build output" msgstr "" -#: build/models.py:896 +#: build/models.py:902 msgid "BuildItem must be unique for build, stock_item and install_into" msgstr "" -#: build/models.py:918 +#: build/models.py:924 msgid "Build item must specify a build output" msgstr "" -#: build/models.py:923 +#: build/models.py:929 #, python-brace-format msgid "Selected stock item not found in BOM for part '{p}'" msgstr "" -#: build/models.py:927 +#: build/models.py:933 #, python-brace-format msgid "Allocated quantity ({n}) must not exceed available quantity ({q})" msgstr "" -#: build/models.py:934 order/models.py:632 +#: build/models.py:940 order/models.py:632 msgid "StockItem is over-allocated" msgstr "" -#: build/models.py:938 order/models.py:635 +#: build/models.py:944 order/models.py:635 msgid "Allocation quantity must be greater than zero" msgstr "" -#: build/models.py:942 +#: build/models.py:948 msgid "Quantity must be 1 for serialized stock" msgstr "" -#: build/models.py:982 +#: build/models.py:988 msgid "Build to allocate parts" msgstr "" -#: build/models.py:989 +#: build/models.py:995 msgid "Source stock item" msgstr "" -#: build/models.py:1001 +#: build/models.py:1007 msgid "Stock quantity to allocate to build" msgstr "" -#: build/models.py:1009 +#: build/models.py:1015 msgid "Destination stock item" msgstr "" @@ -612,7 +613,7 @@ msgid "Order required parts" msgstr "" #: build/templates/build/allocate.html:30 -#: company/templates/company/detail_part.html:28 order/views.py:803 +#: company/templates/company/detail_part.html:28 order/views.py:805 #: part/templates/part/category.html:125 msgid "Order Parts" msgstr "" @@ -652,11 +653,11 @@ msgid "" "The following stock items will be allocated to the specified build output" msgstr "" -#: build/templates/build/auto_allocate.html:18 stock/forms.py:337 -#: stock/templates/stock/item_base.html:227 +#: build/templates/build/auto_allocate.html:18 stock/forms.py:343 +#: stock/templates/stock/item_base.html:234 #: stock/templates/stock/stock_adjust.html:17 #: templates/InvenTree/search.html:183 templates/js/barcode.js:337 -#: templates/js/build.js:434 templates/js/stock.js:580 +#: templates/js/build.js:434 templates/js/stock.js:587 msgid "Location" msgstr "" @@ -681,7 +682,7 @@ msgstr "" #: order/templates/order/order_base.html:26 #: order/templates/order/sales_order_base.html:35 #: part/templates/part/category.html:13 part/templates/part/part_base.html:32 -#: stock/templates/stock/item_base.html:90 +#: stock/templates/stock/item_base.html:97 #: stock/templates/stock/location.html:12 msgid "Admin view" msgstr "" @@ -690,7 +691,7 @@ msgstr "" #: build/templates/build/build_base.html:92 #: order/templates/order/sales_order_base.html:41 #: order/templates/order/sales_order_base.html:83 -#: templates/js/table_filters.js:190 templates/js/table_filters.js:222 +#: templates/js/table_filters.js:200 templates/js/table_filters.js:232 msgid "Overdue" msgstr "" @@ -717,10 +718,10 @@ msgstr "" #: build/templates/build/build_base.html:88 #: build/templates/build/detail.html:57 #: order/templates/order/receive_parts.html:24 -#: stock/templates/stock/item_base.html:312 templates/InvenTree/search.html:175 +#: stock/templates/stock/item_base.html:333 templates/InvenTree/search.html:175 #: templates/js/barcode.js:42 templates/js/build.js:697 #: templates/js/order.js:180 templates/js/order.js:268 -#: templates/js/stock.js:567 templates/js/stock.js:971 +#: templates/js/stock.js:574 templates/js/stock.js:997 msgid "Status" msgstr "" @@ -740,7 +741,7 @@ msgstr "" #: order/templates/order/sales_order_notes.html:10 #: order/templates/order/sales_order_ship.html:25 #: part/templates/part/allocation.html:27 -#: stock/templates/stock/item_base.html:221 templates/js/order.js:229 +#: stock/templates/stock/item_base.html:228 templates/js/order.js:229 msgid "Sales Order" msgstr "" @@ -832,7 +833,7 @@ msgstr "" msgid "Stock can be taken from any available location." msgstr "" -#: build/templates/build/detail.html:44 stock/forms.py:365 +#: build/templates/build/detail.html:44 stock/forms.py:371 msgid "Destination" msgstr "" @@ -841,9 +842,9 @@ msgid "Destination location not specified" msgstr "" #: build/templates/build/detail.html:68 -#: stock/templates/stock/item_base.html:245 templates/js/stock.js:575 -#: templates/js/stock.js:978 templates/js/table_filters.js:80 -#: templates/js/table_filters.js:151 +#: stock/templates/stock/item_base.html:252 templates/js/stock.js:582 +#: templates/js/stock.js:1004 templates/js/table_filters.js:80 +#: templates/js/table_filters.js:161 msgid "Batch" msgstr "" @@ -935,7 +936,7 @@ msgstr "" msgid "Create Build Output" msgstr "" -#: build/views.py:207 stock/models.py:828 stock/views.py:1668 +#: build/views.py:207 stock/models.py:871 stock/views.py:1681 msgid "Serial numbers already exist" msgstr "" @@ -1032,36 +1033,36 @@ msgstr "" msgid "Stock item must be selected" msgstr "" -#: build/views.py:1011 +#: build/views.py:1012 msgid "Edit Stock Allocation" msgstr "" -#: build/views.py:1016 +#: build/views.py:1017 msgid "Updated Build Item" msgstr "" -#: build/views.py:1045 +#: build/views.py:1046 msgid "Add Build Order Attachment" msgstr "" -#: build/views.py:1059 order/views.py:111 order/views.py:164 part/views.py:168 +#: build/views.py:1060 order/views.py:113 order/views.py:166 part/views.py:170 #: stock/views.py:180 msgid "Added attachment" msgstr "" -#: build/views.py:1095 order/views.py:191 order/views.py:213 +#: build/views.py:1096 order/views.py:193 order/views.py:215 msgid "Edit Attachment" msgstr "" -#: build/views.py:1106 order/views.py:196 order/views.py:218 +#: build/views.py:1107 order/views.py:198 order/views.py:220 msgid "Attachment updated" msgstr "" -#: build/views.py:1116 order/views.py:233 order/views.py:248 +#: build/views.py:1117 order/views.py:235 order/views.py:250 msgid "Delete Attachment" msgstr "" -#: build/views.py:1122 order/views.py:240 order/views.py:255 stock/views.py:238 +#: build/views.py:1123 order/views.py:242 order/views.py:257 stock/views.py:238 msgid "Deleted attachment" msgstr "" @@ -1137,103 +1138,170 @@ msgstr "" msgid "Copy category parameter templates when creating a part" msgstr "" -#: common/models.py:115 part/models.py:743 part/templates/part/detail.html:168 -#: templates/js/table_filters.js:272 -msgid "Component" +#: common/models.py:115 part/templates/part/detail.html:155 stock/forms.py:255 +#: templates/js/table_filters.js:23 templates/js/table_filters.js:266 +msgid "Template" msgstr "" #: common/models.py:116 -msgid "Parts can be used as sub-components by default" +msgid "Parts are templates by default" msgstr "" -#: common/models.py:122 part/models.py:754 part/templates/part/detail.html:188 -msgid "Purchaseable" +#: common/models.py:122 part/models.py:794 part/templates/part/detail.html:165 +#: templates/js/table_filters.js:278 +msgid "Assembly" msgstr "" #: common/models.py:123 -msgid "Parts are purchaseable by default" +msgid "Parts can be assembled from other components by default" msgstr "" -#: common/models.py:129 part/models.py:759 part/templates/part/detail.html:198 -#: templates/js/table_filters.js:280 -msgid "Salable" +#: common/models.py:129 part/models.py:800 part/templates/part/detail.html:175 +#: templates/js/table_filters.js:282 +msgid "Component" msgstr "" #: common/models.py:130 -msgid "Parts are salable by default" +msgid "Parts can be used as sub-components by default" msgstr "" -#: common/models.py:136 part/models.py:749 part/templates/part/detail.html:178 -#: templates/js/table_filters.js:31 templates/js/table_filters.js:284 -msgid "Trackable" +#: common/models.py:136 part/models.py:811 part/templates/part/detail.html:195 +msgid "Purchaseable" msgstr "" #: common/models.py:137 -msgid "Parts are trackable by default" +msgid "Parts are purchaseable by default" msgstr "" -#: common/models.py:143 -msgid "Build Order Reference Prefix" +#: common/models.py:143 part/models.py:816 part/templates/part/detail.html:205 +#: templates/js/table_filters.js:290 +msgid "Salable" msgstr "" #: common/models.py:144 +msgid "Parts are salable by default" +msgstr "" + +#: common/models.py:150 part/models.py:806 part/templates/part/detail.html:185 +#: templates/js/table_filters.js:31 templates/js/table_filters.js:294 +msgid "Trackable" +msgstr "" + +#: common/models.py:151 +msgid "Parts are trackable by default" +msgstr "" + +#: common/models.py:157 part/models.py:826 part/templates/part/detail.html:145 +#: templates/js/table_filters.js:27 +msgid "Virtual" +msgstr "" + +#: common/models.py:158 +msgid "Parts are virtual by default" +msgstr "" + +#: common/models.py:164 +msgid "Stock Expiry" +msgstr "" + +#: common/models.py:165 +msgid "Enable stock expiry functionality" +msgstr "" + +#: common/models.py:171 +msgid "Sell Expired Stock" +msgstr "" + +#: common/models.py:172 +msgid "Allow sale of expired stock" +msgstr "" + +#: common/models.py:178 +msgid "Stock Stale Time" +msgstr "" + +#: common/models.py:179 +msgid "Number of days stock items are considered stale before expiring" +msgstr "" + +#: common/models.py:181 part/templates/part/detail.html:116 +msgid "days" +msgstr "" + +#: common/models.py:186 +msgid "Build Expired Stock" +msgstr "" + +#: common/models.py:187 +msgid "Allow building with expired stock" +msgstr "" + +#: common/models.py:193 +msgid "Build Order Reference Prefix" +msgstr "" + +#: common/models.py:194 msgid "Prefix value for build order reference" msgstr "" -#: common/models.py:149 +#: common/models.py:199 msgid "Build Order Reference Regex" msgstr "" -#: common/models.py:150 +#: common/models.py:200 msgid "Regular expression pattern for matching build order reference" msgstr "" -#: common/models.py:154 +#: common/models.py:204 msgid "Sales Order Reference Prefix" msgstr "" -#: common/models.py:155 +#: common/models.py:205 msgid "Prefix value for sales order reference" msgstr "" -#: common/models.py:160 +#: common/models.py:210 msgid "Purchase Order Reference Prefix" msgstr "" -#: common/models.py:161 +#: common/models.py:211 msgid "Prefix value for purchase order reference" msgstr "" -#: common/models.py:378 +#: common/models.py:434 msgid "Settings key (must be unique - case insensitive" msgstr "" -#: common/models.py:380 +#: common/models.py:436 msgid "Settings value" msgstr "" -#: common/models.py:439 +#: common/models.py:493 msgid "Value must be a boolean value" msgstr "" -#: common/models.py:453 +#: common/models.py:503 +msgid "Value must be an integer value" +msgstr "" + +#: common/models.py:517 msgid "Key string must be unique" msgstr "" -#: common/models.py:497 company/forms.py:113 +#: common/models.py:590 company/forms.py:113 msgid "Price break quantity" msgstr "" -#: common/models.py:505 company/templates/company/supplier_part_pricing.html:80 +#: common/models.py:598 company/templates/company/supplier_part_pricing.html:80 #: part/templates/part/sale_prices.html:87 templates/js/bom.js:246 msgid "Price" msgstr "" -#: common/models.py:506 +#: common/models.py:599 msgid "Unit price at specified quantity" msgstr "" -#: common/models.py:529 +#: common/models.py:622 msgid "Default" msgstr "" @@ -1334,8 +1402,8 @@ msgstr "" msgid "Currency" msgstr "" -#: company/models.py:313 stock/models.py:338 -#: stock/templates/stock/item_base.html:177 +#: company/models.py:313 stock/models.py:344 +#: stock/templates/stock/item_base.html:184 msgid "Base Part" msgstr "" @@ -1348,7 +1416,7 @@ msgstr "" #: company/templates/company/supplier_part_detail.html:21 #: order/templates/order/order_base.html:79 #: order/templates/order/order_wizard/select_pos.html:30 part/bom.py:170 -#: stock/templates/stock/item_base.html:287 templates/js/company.js:48 +#: stock/templates/stock/item_base.html:294 templates/js/company.js:48 #: templates/js/company.js:164 templates/js/order.js:162 msgid "Supplier" msgstr "" @@ -1387,7 +1455,7 @@ msgstr "" msgid "Manufacturer part number" msgstr "" -#: company/models.py:353 templates/js/company.js:208 +#: company/models.py:353 part/models.py:704 templates/js/company.js:208 msgid "Link" msgstr "" @@ -1444,8 +1512,8 @@ msgid "Uses default currency" msgstr "" #: company/templates/company/detail.html:62 -#: order/templates/order/sales_order_base.html:89 stock/models.py:373 -#: stock/models.py:374 stock/templates/stock/item_base.html:204 +#: order/templates/order/sales_order_base.html:89 stock/models.py:379 +#: stock/models.py:380 stock/templates/stock/item_base.html:211 #: templates/js/company.js:40 templates/js/order.js:250 msgid "Customer" msgstr "" @@ -1461,7 +1529,7 @@ msgstr "" #: company/templates/company/detail_part.html:18 #: order/templates/order/purchase_order_detail.html:68 -#: part/templates/part/supplier.html:14 templates/js/stock.js:855 +#: part/templates/part/supplier.html:14 templates/js/stock.js:881 msgid "New Supplier Part" msgstr "" @@ -1485,7 +1553,7 @@ msgid "Delete Parts" msgstr "" #: company/templates/company/detail_part.html:63 -#: part/templates/part/category.html:116 templates/js/stock.js:849 +#: part/templates/part/category.html:116 templates/js/stock.js:875 msgid "New Part" msgstr "" @@ -1577,8 +1645,8 @@ msgid "New Sales Order" msgstr "" #: company/templates/company/supplier_part_base.html:6 -#: company/templates/company/supplier_part_base.html:19 stock/models.py:347 -#: stock/templates/stock/item_base.html:292 templates/js/company.js:180 +#: company/templates/company/supplier_part_base.html:19 stock/models.py:353 +#: stock/templates/stock/item_base.html:299 templates/js/company.js:180 msgid "Supplier Part" msgstr "" @@ -1619,7 +1687,7 @@ msgid "Pricing Information" msgstr "" #: company/templates/company/supplier_part_pricing.html:17 company/views.py:486 -#: part/templates/part/sale_prices.html:14 part/views.py:2555 +#: part/templates/part/sale_prices.html:14 part/views.py:2565 msgid "Add Price Break" msgstr "" @@ -1650,7 +1718,7 @@ msgstr "" #: company/templates/company/tabs.html:12 part/templates/part/tabs.html:18 #: stock/templates/stock/location.html:17 templates/InvenTree/search.html:155 #: templates/InvenTree/settings/tabs.html:25 templates/js/part.js:192 -#: templates/js/part.js:418 templates/js/stock.js:508 templates/navbar.html:22 +#: templates/js/part.js:418 templates/js/stock.js:509 templates/navbar.html:22 #: users/models.py:29 msgid "Stock" msgstr "" @@ -1660,7 +1728,7 @@ msgid "Orders" msgstr "" #: company/templates/company/tabs.html:9 -#: order/templates/order/receive_parts.html:14 part/models.py:316 +#: order/templates/order/receive_parts.html:14 part/models.py:317 #: part/templates/part/cat_link.html:7 part/templates/part/category.html:94 #: part/templates/part/category_tabs.html:6 #: templates/InvenTree/settings/tabs.html:22 templates/navbar.html:19 @@ -1733,7 +1801,7 @@ msgstr "" msgid "Edit Supplier Part" msgstr "" -#: company/views.py:295 templates/js/stock.js:856 +#: company/views.py:295 templates/js/stock.js:882 msgid "Create new Supplier Part" msgstr "" @@ -1741,15 +1809,15 @@ msgstr "" msgid "Delete Supplier Part" msgstr "" -#: company/views.py:492 part/views.py:2561 +#: company/views.py:492 part/views.py:2571 msgid "Added new price break" msgstr "" -#: company/views.py:548 part/views.py:2605 +#: company/views.py:548 part/views.py:2615 msgid "Edit Price Break" msgstr "" -#: company/views.py:564 part/views.py:2621 +#: company/views.py:564 part/views.py:2631 msgid "Delete Price Break" msgstr "" @@ -1847,8 +1915,8 @@ msgstr "" msgid "Date order was completed" msgstr "" -#: order/models.py:176 order/models.py:258 part/views.py:1494 -#: stock/models.py:244 stock/models.py:812 +#: order/models.py:176 order/models.py:258 part/views.py:1504 +#: stock/models.py:250 stock/models.py:855 msgid "Quantity must be greater than zero" msgstr "" @@ -1886,7 +1954,7 @@ msgstr "" #: order/models.py:504 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:24 -#: stock/templates/stock/item_base.html:259 templates/js/order.js:146 +#: stock/templates/stock/item_base.html:266 templates/js/order.js:146 msgid "Purchase Order" msgstr "" @@ -1898,8 +1966,8 @@ msgstr "" msgid "Number of items received" msgstr "" -#: order/models.py:527 stock/models.py:458 -#: stock/templates/stock/item_base.html:266 +#: order/models.py:527 stock/models.py:472 +#: stock/templates/stock/item_base.html:273 msgid "Purchase Price" msgstr "" @@ -2045,8 +2113,8 @@ msgid "Line Items" msgstr "" #: order/templates/order/purchase_order_detail.html:17 -#: order/templates/order/sales_order_detail.html:19 order/views.py:1117 -#: order/views.py:1201 +#: order/templates/order/sales_order_detail.html:19 order/views.py:1119 +#: order/views.py:1203 msgid "Add Line Item" msgstr "" @@ -2057,7 +2125,7 @@ msgstr "" #: order/templates/order/purchase_order_detail.html:39 #: order/templates/order/purchase_order_detail.html:119 #: part/templates/part/category.html:173 part/templates/part/category.html:215 -#: templates/js/stock.js:861 +#: templates/js/stock.js:627 templates/js/stock.js:887 msgid "New Location" msgstr "" @@ -2142,8 +2210,8 @@ msgid "Sales Order Items" msgstr "" #: order/templates/order/sales_order_detail.html:72 -#: order/templates/order/sales_order_detail.html:154 stock/models.py:378 -#: stock/templates/stock/item_base.html:191 templates/js/build.js:418 +#: order/templates/order/sales_order_detail.html:154 stock/models.py:384 +#: stock/templates/stock/item_base.html:198 templates/js/build.js:418 msgid "Serial Number" msgstr "" @@ -2221,143 +2289,143 @@ msgstr "" msgid "Order Items" msgstr "" -#: order/views.py:99 +#: order/views.py:101 msgid "Add Purchase Order Attachment" msgstr "" -#: order/views.py:150 +#: order/views.py:152 msgid "Add Sales Order Attachment" msgstr "" -#: order/views.py:310 +#: order/views.py:312 msgid "Create Purchase Order" msgstr "" -#: order/views.py:346 +#: order/views.py:348 msgid "Create Sales Order" msgstr "" -#: order/views.py:382 +#: order/views.py:384 msgid "Edit Purchase Order" msgstr "" -#: order/views.py:403 +#: order/views.py:405 msgid "Edit Sales Order" msgstr "" -#: order/views.py:420 +#: order/views.py:422 msgid "Cancel Order" msgstr "" -#: order/views.py:430 order/views.py:457 +#: order/views.py:432 order/views.py:459 msgid "Confirm order cancellation" msgstr "" -#: order/views.py:433 +#: order/views.py:435 msgid "Order cannot be cancelled as either pending or placed" msgstr "" -#: order/views.py:447 +#: order/views.py:449 msgid "Cancel sales order" msgstr "" -#: order/views.py:460 +#: order/views.py:462 msgid "Order cannot be cancelled" msgstr "" -#: order/views.py:474 +#: order/views.py:476 msgid "Issue Order" msgstr "" -#: order/views.py:484 +#: order/views.py:486 msgid "Confirm order placement" msgstr "" -#: order/views.py:494 +#: order/views.py:496 msgid "Purchase order issued" msgstr "" -#: order/views.py:505 +#: order/views.py:507 msgid "Complete Order" msgstr "" -#: order/views.py:522 +#: order/views.py:524 msgid "Confirm order completion" msgstr "" -#: order/views.py:533 +#: order/views.py:535 msgid "Purchase order completed" msgstr "" -#: order/views.py:543 +#: order/views.py:545 msgid "Ship Order" msgstr "" -#: order/views.py:560 +#: order/views.py:562 msgid "Confirm order shipment" msgstr "" -#: order/views.py:566 +#: order/views.py:568 msgid "Could not ship order" msgstr "" -#: order/views.py:618 +#: order/views.py:620 msgid "Receive Parts" msgstr "" -#: order/views.py:686 +#: order/views.py:688 msgid "Items received" msgstr "" -#: order/views.py:700 +#: order/views.py:702 msgid "No destination set" msgstr "" -#: order/views.py:745 +#: order/views.py:747 msgid "Error converting quantity to number" msgstr "" -#: order/views.py:751 +#: order/views.py:753 msgid "Receive quantity less than zero" msgstr "" -#: order/views.py:757 +#: order/views.py:759 msgid "No lines specified" msgstr "" -#: order/views.py:1127 +#: order/views.py:1129 msgid "Supplier part must be specified" msgstr "" -#: order/views.py:1133 +#: order/views.py:1135 msgid "Supplier must match for Part and Order" msgstr "" -#: order/views.py:1253 order/views.py:1272 +#: order/views.py:1255 order/views.py:1274 msgid "Edit Line Item" msgstr "" -#: order/views.py:1289 order/views.py:1302 +#: order/views.py:1291 order/views.py:1304 msgid "Delete Line Item" msgstr "" -#: order/views.py:1295 order/views.py:1308 +#: order/views.py:1297 order/views.py:1310 msgid "Deleted line item" msgstr "" -#: order/views.py:1317 +#: order/views.py:1319 msgid "Allocate Stock to Order" msgstr "" -#: order/views.py:1387 +#: order/views.py:1394 msgid "Edit Allocation Quantity" msgstr "" -#: order/views.py:1403 +#: order/views.py:1410 msgid "Remove allocation" msgstr "" -#: part/bom.py:138 part/templates/part/category.html:61 +#: part/bom.py:138 part/models.py:722 part/templates/part/category.html:61 #: part/templates/part/detail.html:87 msgid "Default Location" msgstr "" @@ -2379,11 +2447,11 @@ msgstr "" msgid "Error reading BOM file (incorrect row size)" msgstr "" -#: part/forms.py:61 stock/forms.py:255 +#: part/forms.py:61 stock/forms.py:261 msgid "File Format" msgstr "" -#: part/forms.py:61 stock/forms.py:255 +#: part/forms.py:61 stock/forms.py:261 msgid "Select output file format" msgstr "" @@ -2427,7 +2495,7 @@ msgstr "" msgid "Include part supplier data in exported BOM" msgstr "" -#: part/forms.py:92 part/models.py:1720 +#: part/forms.py:92 part/models.py:1781 msgid "Parent Part" msgstr "" @@ -2459,43 +2527,43 @@ msgstr "" msgid "Select part category" msgstr "" -#: part/forms.py:188 +#: part/forms.py:189 msgid "Duplicate all BOM data for this part" msgstr "" -#: part/forms.py:189 +#: part/forms.py:190 msgid "Copy BOM" msgstr "" -#: part/forms.py:194 +#: part/forms.py:195 msgid "Duplicate all parameter data for this part" msgstr "" -#: part/forms.py:195 +#: part/forms.py:196 msgid "Copy Parameters" msgstr "" -#: part/forms.py:200 +#: part/forms.py:201 msgid "Confirm part creation" msgstr "" -#: part/forms.py:205 +#: part/forms.py:206 msgid "Include category parameter templates" msgstr "" -#: part/forms.py:210 +#: part/forms.py:211 msgid "Include parent categories parameter templates" msgstr "" -#: part/forms.py:285 +#: part/forms.py:291 msgid "Add parameter template to same level categories" msgstr "" -#: part/forms.py:289 +#: part/forms.py:295 msgid "Add parameter template to all categories" msgstr "" -#: part/forms.py:333 +#: part/forms.py:339 msgid "Input quantity for price calculation" msgstr "" @@ -2507,7 +2575,7 @@ msgstr "" msgid "Default keywords for parts in this category" msgstr "" -#: part/models.py:77 part/models.py:1765 +#: part/models.py:77 part/models.py:1826 #: part/templates/part/part_app_base.html:9 msgid "Part Category" msgstr "" @@ -2517,255 +2585,294 @@ msgstr "" msgid "Part Categories" msgstr "" -#: part/models.py:408 part/models.py:418 +#: part/models.py:409 part/models.py:419 #, python-brace-format msgid "Part '{p1}' is used in BOM for '{p2}' (recursive)" msgstr "" -#: part/models.py:515 +#: part/models.py:516 msgid "Next available serial numbers are" msgstr "" -#: part/models.py:519 +#: part/models.py:520 msgid "Next available serial number is" msgstr "" -#: part/models.py:524 +#: part/models.py:525 msgid "Most recent serial number is" msgstr "" -#: part/models.py:603 +#: part/models.py:604 msgid "Duplicate IPN not allowed in part settings" msgstr "" -#: part/models.py:614 +#: part/models.py:615 msgid "Part must be unique for name, IPN and revision" msgstr "" -#: part/models.py:644 part/templates/part/detail.html:19 +#: part/models.py:646 part/templates/part/detail.html:19 msgid "Part name" msgstr "" -#: part/models.py:648 +#: part/models.py:653 +msgid "Is Template" +msgstr "" + +#: part/models.py:654 msgid "Is this part a template part?" msgstr "" -#: part/models.py:657 +#: part/models.py:665 msgid "Is this part a variant of another part?" msgstr "" -#: part/models.py:659 +#: part/models.py:666 part/templates/part/detail.html:57 +msgid "Variant Of" +msgstr "" + +#: part/models.py:672 msgid "Part description" msgstr "" -#: part/models.py:661 +#: part/models.py:677 part/templates/part/category.html:68 +#: part/templates/part/detail.html:64 +msgid "Keywords" +msgstr "" + +#: part/models.py:678 msgid "Part keywords to improve visibility in search results" msgstr "" -#: part/models.py:666 +#: part/models.py:685 part/templates/part/detail.html:70 +#: part/templates/part/set_category.html:15 templates/js/part.js:405 +msgid "Category" +msgstr "" + +#: part/models.py:686 msgid "Part category" msgstr "" -#: part/models.py:668 +#: part/models.py:691 part/templates/part/detail.html:25 +#: part/templates/part/part_base.html:95 templates/js/part.js:180 +msgid "IPN" +msgstr "" + +#: part/models.py:692 msgid "Internal Part Number" msgstr "" -#: part/models.py:670 +#: part/models.py:698 msgid "Part revision or version number" msgstr "" -#: part/models.py:684 +#: part/models.py:699 part/templates/part/detail.html:32 +#: templates/js/part.js:184 +msgid "Revision" +msgstr "" + +#: part/models.py:720 msgid "Where is this item normally stored?" msgstr "" -#: part/models.py:728 +#: part/models.py:767 part/templates/part/detail.html:94 +msgid "Default Supplier" +msgstr "" + +#: part/models.py:768 msgid "Default supplier part" msgstr "" -#: part/models.py:731 +#: part/models.py:775 +msgid "Default Expiry" +msgstr "" + +#: part/models.py:776 +msgid "Expiry time (in days) for stock items of this part" +msgstr "" + +#: part/models.py:781 part/templates/part/detail.html:108 +msgid "Minimum Stock" +msgstr "" + +#: part/models.py:782 msgid "Minimum allowed stock level" msgstr "" -#: part/models.py:733 +#: part/models.py:788 part/templates/part/detail.html:102 +#: part/templates/part/params.html:26 +msgid "Units" +msgstr "" + +#: part/models.py:789 msgid "Stock keeping units for this part" msgstr "" -#: part/models.py:737 part/templates/part/detail.html:158 -#: templates/js/table_filters.js:268 -msgid "Assembly" -msgstr "" - -#: part/models.py:738 +#: part/models.py:795 msgid "Can this part be built from other parts?" msgstr "" -#: part/models.py:744 +#: part/models.py:801 msgid "Can this part be used to build other parts?" msgstr "" -#: part/models.py:750 +#: part/models.py:807 msgid "Does this part have tracking for unique items?" msgstr "" -#: part/models.py:755 +#: part/models.py:812 msgid "Can this part be purchased from external suppliers?" msgstr "" -#: part/models.py:760 +#: part/models.py:817 msgid "Can this part be sold to customers?" msgstr "" -#: part/models.py:764 part/templates/part/detail.html:215 +#: part/models.py:821 part/templates/part/detail.html:222 #: templates/js/table_filters.js:19 templates/js/table_filters.js:55 -#: templates/js/table_filters.js:186 templates/js/table_filters.js:251 +#: templates/js/table_filters.js:196 templates/js/table_filters.js:261 msgid "Active" msgstr "" -#: part/models.py:765 +#: part/models.py:822 msgid "Is this part active?" msgstr "" -#: part/models.py:769 part/templates/part/detail.html:138 -#: templates/js/table_filters.js:27 -msgid "Virtual" -msgstr "" - -#: part/models.py:770 +#: part/models.py:827 msgid "Is this a virtual part, such as a software product or license?" msgstr "" -#: part/models.py:772 +#: part/models.py:832 msgid "Part notes - supports Markdown formatting" msgstr "" -#: part/models.py:774 +#: part/models.py:835 msgid "Stored BOM checksum" msgstr "" -#: part/models.py:1593 +#: part/models.py:1654 msgid "Test templates can only be created for trackable parts" msgstr "" -#: part/models.py:1610 +#: part/models.py:1671 msgid "Test with this name already exists for this part" msgstr "" -#: part/models.py:1629 templates/js/part.js:567 templates/js/stock.js:92 +#: part/models.py:1690 templates/js/part.js:567 templates/js/stock.js:93 msgid "Test Name" msgstr "" -#: part/models.py:1630 +#: part/models.py:1691 msgid "Enter a name for the test" msgstr "" -#: part/models.py:1635 +#: part/models.py:1696 msgid "Test Description" msgstr "" -#: part/models.py:1636 +#: part/models.py:1697 msgid "Enter description for this test" msgstr "" -#: part/models.py:1641 templates/js/part.js:576 -#: templates/js/table_filters.js:172 +#: part/models.py:1702 templates/js/part.js:576 +#: templates/js/table_filters.js:182 msgid "Required" msgstr "" -#: part/models.py:1642 +#: part/models.py:1703 msgid "Is this test required to pass?" msgstr "" -#: part/models.py:1647 templates/js/part.js:584 +#: part/models.py:1708 templates/js/part.js:584 msgid "Requires Value" msgstr "" -#: part/models.py:1648 +#: part/models.py:1709 msgid "Does this test require a value when adding a test result?" msgstr "" -#: part/models.py:1653 templates/js/part.js:591 +#: part/models.py:1714 templates/js/part.js:591 msgid "Requires Attachment" msgstr "" -#: part/models.py:1654 +#: part/models.py:1715 msgid "Does this test require a file attachment when adding a test result?" msgstr "" -#: part/models.py:1687 +#: part/models.py:1748 msgid "Parameter template name must be unique" msgstr "" -#: part/models.py:1692 +#: part/models.py:1753 msgid "Parameter Name" msgstr "" -#: part/models.py:1694 +#: part/models.py:1755 msgid "Parameter Units" msgstr "" -#: part/models.py:1722 part/models.py:1770 +#: part/models.py:1783 part/models.py:1831 #: templates/InvenTree/settings/category.html:62 msgid "Parameter Template" msgstr "" -#: part/models.py:1724 +#: part/models.py:1785 msgid "Parameter Value" msgstr "" -#: part/models.py:1774 +#: part/models.py:1835 msgid "Default Parameter Value" msgstr "" -#: part/models.py:1804 +#: part/models.py:1865 msgid "Select parent part" msgstr "" -#: part/models.py:1812 +#: part/models.py:1873 msgid "Select part to be used in BOM" msgstr "" -#: part/models.py:1818 +#: part/models.py:1879 msgid "BOM quantity for this BOM item" msgstr "" -#: part/models.py:1820 +#: part/models.py:1881 msgid "This BOM item is optional" msgstr "" -#: part/models.py:1823 +#: part/models.py:1884 msgid "Estimated build wastage quantity (absolute or percentage)" msgstr "" -#: part/models.py:1826 +#: part/models.py:1887 msgid "BOM item reference" msgstr "" -#: part/models.py:1829 +#: part/models.py:1890 msgid "BOM item notes" msgstr "" -#: part/models.py:1831 +#: part/models.py:1892 msgid "BOM line checksum" msgstr "" -#: part/models.py:1902 part/views.py:1500 part/views.py:1552 -#: stock/models.py:234 +#: part/models.py:1963 part/views.py:1510 part/views.py:1562 +#: stock/models.py:240 msgid "Quantity must be integer value for trackable parts" msgstr "" -#: part/models.py:1911 part/models.py:1913 +#: part/models.py:1972 part/models.py:1974 msgid "Sub part must be specified" msgstr "" -#: part/models.py:1916 +#: part/models.py:1977 msgid "BOM Item" msgstr "" -#: part/models.py:2031 +#: part/models.py:2092 msgid "Select Related Part" msgstr "" -#: part/models.py:2063 +#: part/models.py:2124 msgid "" "Error creating relationship: check that the part is not related to itself " "and that the relationship is unique" @@ -2786,9 +2893,9 @@ msgstr "" #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 #: stock/templates/stock/item_base.html:72 -#: stock/templates/stock/item_base.html:274 +#: stock/templates/stock/item_base.html:281 #: stock/templates/stock/stock_adjust.html:16 templates/js/build.js:751 -#: templates/js/stock.js:705 templates/js/stock.js:954 +#: templates/js/stock.js:720 templates/js/stock.js:980 msgid "Stock Item" msgstr "" @@ -2853,7 +2960,7 @@ msgstr "" msgid "Validate" msgstr "" -#: part/templates/part/bom.html:62 part/views.py:1791 +#: part/templates/part/bom.html:62 part/views.py:1801 msgid "Export Bill of Materials" msgstr "" @@ -2953,7 +3060,7 @@ msgstr "" msgid "All parts" msgstr "" -#: part/templates/part/category.html:24 part/views.py:2182 +#: part/templates/part/category.html:24 part/views.py:2192 msgid "Create new part category" msgstr "" @@ -2977,10 +3084,6 @@ msgstr "" msgid "Category Description" msgstr "" -#: part/templates/part/category.html:68 part/templates/part/detail.html:64 -msgid "Keywords" -msgstr "" - #: part/templates/part/category.html:74 msgid "Subcategories" msgstr "" @@ -3009,7 +3112,7 @@ msgstr "" msgid "Export Data" msgstr "" -#: part/templates/part/category.html:174 +#: part/templates/part/category.html:174 templates/js/stock.js:628 msgid "Create new location" msgstr "" @@ -3025,7 +3128,7 @@ msgstr "" msgid "Create new Part Category" msgstr "" -#: part/templates/part/category.html:216 stock/views.py:1359 +#: part/templates/part/category.html:216 stock/views.py:1363 msgid "Create new Stock Location" msgstr "" @@ -3049,15 +3152,6 @@ msgstr "" msgid "Part Details" msgstr "" -#: part/templates/part/detail.html:25 part/templates/part/part_base.html:95 -#: templates/js/part.js:180 -msgid "IPN" -msgstr "" - -#: part/templates/part/detail.html:32 templates/js/part.js:184 -msgid "Revision" -msgstr "" - #: part/templates/part/detail.html:39 msgid "Latest Serial Number" msgstr "" @@ -3066,101 +3160,79 @@ msgstr "" msgid "No serial numbers recorded" msgstr "" -#: part/templates/part/detail.html:57 -msgid "Variant Of" +#: part/templates/part/detail.html:115 +msgid "Stock Expiry Time" msgstr "" -#: part/templates/part/detail.html:70 part/templates/part/set_category.html:15 -#: templates/js/part.js:405 -msgid "Category" -msgstr "" - -#: part/templates/part/detail.html:94 -msgid "Default Supplier" -msgstr "" - -#: part/templates/part/detail.html:102 part/templates/part/params.html:26 -msgid "Units" -msgstr "" - -#: part/templates/part/detail.html:108 -msgid "Minimum Stock" -msgstr "" - -#: part/templates/part/detail.html:114 templates/js/order.js:276 +#: part/templates/part/detail.html:121 templates/js/order.js:276 msgid "Creation Date" msgstr "" -#: part/templates/part/detail.html:120 +#: part/templates/part/detail.html:127 msgid "Created By" msgstr "" -#: part/templates/part/detail.html:127 +#: part/templates/part/detail.html:134 msgid "Responsible User" msgstr "" -#: part/templates/part/detail.html:141 +#: part/templates/part/detail.html:148 msgid "Part is virtual (not a physical part)" msgstr "" -#: part/templates/part/detail.html:143 +#: part/templates/part/detail.html:150 msgid "Part is not a virtual part" msgstr "" -#: part/templates/part/detail.html:148 stock/forms.py:249 -#: templates/js/table_filters.js:23 templates/js/table_filters.js:256 -msgid "Template" -msgstr "" - -#: part/templates/part/detail.html:151 +#: part/templates/part/detail.html:158 msgid "Part is a template part (variants can be made from this part)" msgstr "" -#: part/templates/part/detail.html:153 +#: part/templates/part/detail.html:160 msgid "Part is not a template part" msgstr "" -#: part/templates/part/detail.html:161 +#: part/templates/part/detail.html:168 msgid "Part can be assembled from other parts" msgstr "" -#: part/templates/part/detail.html:163 +#: part/templates/part/detail.html:170 msgid "Part cannot be assembled from other parts" msgstr "" -#: part/templates/part/detail.html:171 +#: part/templates/part/detail.html:178 msgid "Part can be used in assemblies" msgstr "" -#: part/templates/part/detail.html:173 +#: part/templates/part/detail.html:180 msgid "Part cannot be used in assemblies" msgstr "" -#: part/templates/part/detail.html:181 +#: part/templates/part/detail.html:188 msgid "Part stock is tracked by serial number" msgstr "" -#: part/templates/part/detail.html:183 +#: part/templates/part/detail.html:190 msgid "Part stock is not tracked by serial number" msgstr "" -#: part/templates/part/detail.html:191 part/templates/part/detail.html:193 +#: part/templates/part/detail.html:198 part/templates/part/detail.html:200 msgid "Part can be purchased from external suppliers" msgstr "" -#: part/templates/part/detail.html:201 +#: part/templates/part/detail.html:208 msgid "Part can be sold to customers" msgstr "" -#: part/templates/part/detail.html:203 +#: part/templates/part/detail.html:210 msgid "Part cannot be sold to customers" msgstr "" -#: part/templates/part/detail.html:218 +#: part/templates/part/detail.html:225 msgid "Part is active" msgstr "" -#: part/templates/part/detail.html:220 +#: part/templates/part/detail.html:227 msgid "Part is not active" msgstr "" @@ -3178,12 +3250,12 @@ msgstr "" #: part/templates/part/params.html:15 #: templates/InvenTree/settings/category.html:29 -#: templates/InvenTree/settings/part.html:38 +#: templates/InvenTree/settings/part.html:41 msgid "New Parameter" msgstr "" -#: part/templates/part/params.html:25 stock/models.py:1420 -#: templates/js/stock.js:112 +#: part/templates/part/params.html:25 stock/models.py:1463 +#: templates/InvenTree/settings/header.html:8 templates/js/stock.js:113 msgid "Value" msgstr "" @@ -3218,19 +3290,19 @@ msgid "Star this part" msgstr "" #: part/templates/part/part_base.html:49 -#: stock/templates/stock/item_base.html:101 +#: stock/templates/stock/item_base.html:108 #: stock/templates/stock/location.html:29 msgid "Barcode actions" msgstr "" #: part/templates/part/part_base.html:51 -#: stock/templates/stock/item_base.html:103 +#: stock/templates/stock/item_base.html:110 #: stock/templates/stock/location.html:31 msgid "Show QR Code" msgstr "" #: part/templates/part/part_base.html:52 -#: stock/templates/stock/item_base.html:104 +#: stock/templates/stock/item_base.html:111 #: stock/templates/stock/location.html:32 msgid "Print Label" msgstr "" @@ -3259,7 +3331,7 @@ msgstr "" msgid "Delete part" msgstr "" -#: part/templates/part/part_base.html:124 templates/js/table_filters.js:111 +#: part/templates/part/part_base.html:124 templates/js/table_filters.js:121 msgid "In Stock" msgstr "" @@ -3368,7 +3440,7 @@ msgstr "" msgid "Used In" msgstr "" -#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:318 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:339 msgid "Tests" msgstr "" @@ -3396,220 +3468,220 @@ msgstr "" msgid "New Variant" msgstr "" -#: part/views.py:84 +#: part/views.py:86 msgid "Add Related Part" msgstr "" -#: part/views.py:140 +#: part/views.py:142 msgid "Delete Related Part" msgstr "" -#: part/views.py:152 +#: part/views.py:154 msgid "Add part attachment" msgstr "" -#: part/views.py:207 templates/attachment_table.html:34 +#: part/views.py:209 templates/attachment_table.html:34 msgid "Edit attachment" msgstr "" -#: part/views.py:213 +#: part/views.py:215 msgid "Part attachment updated" msgstr "" -#: part/views.py:228 +#: part/views.py:230 msgid "Delete Part Attachment" msgstr "" -#: part/views.py:236 +#: part/views.py:238 msgid "Deleted part attachment" msgstr "" -#: part/views.py:245 +#: part/views.py:247 msgid "Create Test Template" msgstr "" -#: part/views.py:274 +#: part/views.py:276 msgid "Edit Test Template" msgstr "" -#: part/views.py:290 +#: part/views.py:292 msgid "Delete Test Template" msgstr "" -#: part/views.py:299 +#: part/views.py:301 msgid "Set Part Category" msgstr "" -#: part/views.py:349 +#: part/views.py:351 #, python-brace-format msgid "Set category for {n} parts" msgstr "" -#: part/views.py:384 +#: part/views.py:386 msgid "Create Variant" msgstr "" -#: part/views.py:466 +#: part/views.py:468 msgid "Duplicate Part" msgstr "" -#: part/views.py:473 +#: part/views.py:475 msgid "Copied part" msgstr "" -#: part/views.py:527 part/views.py:661 +#: part/views.py:529 part/views.py:667 msgid "Possible matches exist - confirm creation of new part" msgstr "" -#: part/views.py:592 templates/js/stock.js:850 +#: part/views.py:594 templates/js/stock.js:876 msgid "Create New Part" msgstr "" -#: part/views.py:599 +#: part/views.py:601 msgid "Created new part" msgstr "" -#: part/views.py:830 +#: part/views.py:836 msgid "Part QR Code" msgstr "" -#: part/views.py:849 +#: part/views.py:855 msgid "Upload Part Image" msgstr "" -#: part/views.py:857 part/views.py:894 +#: part/views.py:863 part/views.py:900 msgid "Updated part image" msgstr "" -#: part/views.py:866 +#: part/views.py:872 msgid "Select Part Image" msgstr "" -#: part/views.py:897 +#: part/views.py:903 msgid "Part image not found" msgstr "" -#: part/views.py:908 +#: part/views.py:914 msgid "Edit Part Properties" msgstr "" -#: part/views.py:935 +#: part/views.py:945 msgid "Duplicate BOM" msgstr "" -#: part/views.py:966 +#: part/views.py:976 msgid "Confirm duplication of BOM from parent" msgstr "" -#: part/views.py:987 +#: part/views.py:997 msgid "Validate BOM" msgstr "" -#: part/views.py:1010 +#: part/views.py:1020 msgid "Confirm that the BOM is valid" msgstr "" -#: part/views.py:1021 +#: part/views.py:1031 msgid "Validated Bill of Materials" msgstr "" -#: part/views.py:1155 +#: part/views.py:1165 msgid "No BOM file provided" msgstr "" -#: part/views.py:1503 +#: part/views.py:1513 msgid "Enter a valid quantity" msgstr "" -#: part/views.py:1528 part/views.py:1531 +#: part/views.py:1538 part/views.py:1541 msgid "Select valid part" msgstr "" -#: part/views.py:1537 +#: part/views.py:1547 msgid "Duplicate part selected" msgstr "" -#: part/views.py:1575 +#: part/views.py:1585 msgid "Select a part" msgstr "" -#: part/views.py:1581 +#: part/views.py:1591 msgid "Selected part creates a circular BOM" msgstr "" -#: part/views.py:1585 +#: part/views.py:1595 msgid "Specify quantity" msgstr "" -#: part/views.py:1841 +#: part/views.py:1851 msgid "Confirm Part Deletion" msgstr "" -#: part/views.py:1850 +#: part/views.py:1860 msgid "Part was deleted" msgstr "" -#: part/views.py:1859 +#: part/views.py:1869 msgid "Part Pricing" msgstr "" -#: part/views.py:1973 +#: part/views.py:1983 msgid "Create Part Parameter Template" msgstr "" -#: part/views.py:1983 +#: part/views.py:1993 msgid "Edit Part Parameter Template" msgstr "" -#: part/views.py:1992 +#: part/views.py:2002 msgid "Delete Part Parameter Template" msgstr "" -#: part/views.py:2002 +#: part/views.py:2012 msgid "Create Part Parameter" msgstr "" -#: part/views.py:2054 +#: part/views.py:2064 msgid "Edit Part Parameter" msgstr "" -#: part/views.py:2070 +#: part/views.py:2080 msgid "Delete Part Parameter" msgstr "" -#: part/views.py:2129 +#: part/views.py:2139 msgid "Edit Part Category" msgstr "" -#: part/views.py:2166 +#: part/views.py:2176 msgid "Delete Part Category" msgstr "" -#: part/views.py:2174 +#: part/views.py:2184 msgid "Part category was deleted" msgstr "" -#: part/views.py:2230 +#: part/views.py:2240 msgid "Create Category Parameter Template" msgstr "" -#: part/views.py:2333 +#: part/views.py:2343 msgid "Edit Category Parameter Template" msgstr "" -#: part/views.py:2391 +#: part/views.py:2401 msgid "Delete Category Parameter Template" msgstr "" -#: part/views.py:2416 +#: part/views.py:2426 msgid "Create BOM Item" msgstr "" -#: part/views.py:2488 +#: part/views.py:2498 msgid "Edit BOM item" msgstr "" -#: part/views.py:2545 +#: part/views.py:2555 msgid "Confim BOM item deletion" msgstr "" @@ -3641,295 +3713,305 @@ msgstr "" msgid "Asset file description" msgstr "" -#: stock/forms.py:111 +#: stock/forms.py:116 msgid "Enter unique serial numbers (or leave blank)" msgstr "" -#: stock/forms.py:192 +#: stock/forms.py:198 msgid "Label" msgstr "" -#: stock/forms.py:193 stock/forms.py:249 +#: stock/forms.py:199 stock/forms.py:255 msgid "Select test report template" msgstr "" -#: stock/forms.py:257 +#: stock/forms.py:263 msgid "Include stock items in sub locations" msgstr "" -#: stock/forms.py:292 +#: stock/forms.py:298 msgid "Stock item to install" msgstr "" -#: stock/forms.py:299 +#: stock/forms.py:305 msgid "Stock quantity to assign" msgstr "" -#: stock/forms.py:327 +#: stock/forms.py:333 msgid "Must not exceed available quantity" msgstr "" -#: stock/forms.py:337 +#: stock/forms.py:343 msgid "Destination location for uninstalled items" msgstr "" -#: stock/forms.py:339 +#: stock/forms.py:345 msgid "Add transaction note (optional)" msgstr "" -#: stock/forms.py:341 +#: stock/forms.py:347 msgid "Confirm uninstall" msgstr "" -#: stock/forms.py:341 +#: stock/forms.py:347 msgid "Confirm removal of installed stock items" msgstr "" -#: stock/forms.py:365 +#: stock/forms.py:371 msgid "Destination stock location" msgstr "" -#: stock/forms.py:367 +#: stock/forms.py:373 msgid "Add note (required)" msgstr "" -#: stock/forms.py:371 stock/views.py:935 stock/views.py:1133 +#: stock/forms.py:377 stock/views.py:935 stock/views.py:1133 msgid "Confirm stock adjustment" msgstr "" -#: stock/forms.py:371 +#: stock/forms.py:377 msgid "Confirm movement of stock items" msgstr "" -#: stock/forms.py:373 +#: stock/forms.py:379 msgid "Set Default Location" msgstr "" -#: stock/forms.py:373 +#: stock/forms.py:379 msgid "Set the destination as the default location for selected parts" msgstr "" -#: stock/models.py:179 +#: stock/models.py:185 msgid "Created stock item" msgstr "" -#: stock/models.py:215 +#: stock/models.py:221 msgid "StockItem with this serial number already exists" msgstr "" -#: stock/models.py:251 +#: stock/models.py:257 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "" -#: stock/models.py:261 stock/models.py:270 +#: stock/models.py:267 stock/models.py:276 msgid "Quantity must be 1 for item with a serial number" msgstr "" -#: stock/models.py:262 +#: stock/models.py:268 msgid "Serial number cannot be set if quantity greater than 1" msgstr "" -#: stock/models.py:284 +#: stock/models.py:290 msgid "Item cannot belong to itself" msgstr "" -#: stock/models.py:290 +#: stock/models.py:296 msgid "Item must have a build reference if is_building=True" msgstr "" -#: stock/models.py:297 +#: stock/models.py:303 msgid "Build reference does not point to the same part object" msgstr "" -#: stock/models.py:330 +#: stock/models.py:336 msgid "Parent Stock Item" msgstr "" -#: stock/models.py:339 +#: stock/models.py:345 msgid "Base part" msgstr "" -#: stock/models.py:348 +#: stock/models.py:354 msgid "Select a matching supplier part for this stock item" msgstr "" -#: stock/models.py:353 stock/templates/stock/stock_app_base.html:7 +#: stock/models.py:359 stock/templates/stock/stock_app_base.html:7 msgid "Stock Location" msgstr "" -#: stock/models.py:356 +#: stock/models.py:362 msgid "Where is this stock item located?" msgstr "" -#: stock/models.py:361 stock/templates/stock/item_base.html:212 +#: stock/models.py:367 stock/templates/stock/item_base.html:219 msgid "Installed In" msgstr "" -#: stock/models.py:364 +#: stock/models.py:370 msgid "Is this item installed in another item?" msgstr "" -#: stock/models.py:380 +#: stock/models.py:386 msgid "Serial number for this item" msgstr "" -#: stock/models.py:392 +#: stock/models.py:398 msgid "Batch code for this stock item" msgstr "" -#: stock/models.py:396 +#: stock/models.py:402 msgid "Stock Quantity" msgstr "" -#: stock/models.py:405 +#: stock/models.py:411 msgid "Source Build" msgstr "" -#: stock/models.py:407 +#: stock/models.py:413 msgid "Build for this stock item" msgstr "" -#: stock/models.py:418 +#: stock/models.py:424 msgid "Source Purchase Order" msgstr "" -#: stock/models.py:421 +#: stock/models.py:427 msgid "Purchase order for this stock item" msgstr "" -#: stock/models.py:427 +#: stock/models.py:433 msgid "Destination Sales Order" msgstr "" -#: stock/models.py:439 +#: stock/models.py:439 stock/templates/stock/item_base.html:306 +#: templates/js/stock.js:597 +msgid "Expiry Date" +msgstr "" + +#: stock/models.py:440 +msgid "" +"Expiry date for stock item. Stock will be considered expired after this date" +msgstr "" + +#: stock/models.py:453 msgid "Delete this Stock Item when stock is depleted" msgstr "" -#: stock/models.py:449 stock/templates/stock/item_notes.html:14 +#: stock/models.py:463 stock/templates/stock/item_notes.html:14 #: stock/templates/stock/item_notes.html:30 msgid "Stock Item Notes" msgstr "" -#: stock/models.py:459 +#: stock/models.py:473 msgid "Single unit purchase price at time of purchase" msgstr "" -#: stock/models.py:510 +#: stock/models.py:573 msgid "Assigned to Customer" msgstr "" -#: stock/models.py:512 +#: stock/models.py:575 msgid "Manually assigned to customer" msgstr "" -#: stock/models.py:525 +#: stock/models.py:588 msgid "Returned from customer" msgstr "" -#: stock/models.py:527 +#: stock/models.py:590 msgid "Returned to location" msgstr "" -#: stock/models.py:652 +#: stock/models.py:715 msgid "Installed into stock item" msgstr "" -#: stock/models.py:660 +#: stock/models.py:723 msgid "Installed stock item" msgstr "" -#: stock/models.py:684 +#: stock/models.py:747 msgid "Uninstalled stock item" msgstr "" -#: stock/models.py:703 +#: stock/models.py:766 msgid "Uninstalled into location" msgstr "" -#: stock/models.py:803 +#: stock/models.py:846 msgid "Part is not set as trackable" msgstr "" -#: stock/models.py:809 +#: stock/models.py:852 msgid "Quantity must be integer" msgstr "" -#: stock/models.py:815 +#: stock/models.py:858 #, python-brace-format msgid "Quantity must not exceed available stock quantity ({n})" msgstr "" -#: stock/models.py:818 +#: stock/models.py:861 msgid "Serial numbers must be a list of integers" msgstr "" -#: stock/models.py:821 +#: stock/models.py:864 msgid "Quantity does not match serial numbers" msgstr "" -#: stock/models.py:853 +#: stock/models.py:896 msgid "Add serial number" msgstr "" -#: stock/models.py:856 +#: stock/models.py:899 #, python-brace-format msgid "Serialized {n} items" msgstr "" -#: stock/models.py:967 +#: stock/models.py:1010 msgid "StockItem cannot be moved as it is not in stock" msgstr "" -#: stock/models.py:1321 +#: stock/models.py:1364 msgid "Tracking entry title" msgstr "" -#: stock/models.py:1323 +#: stock/models.py:1366 msgid "Entry notes" msgstr "" -#: stock/models.py:1325 +#: stock/models.py:1368 msgid "Link to external page for further information" msgstr "" -#: stock/models.py:1385 +#: stock/models.py:1428 msgid "Value must be provided for this test" msgstr "" -#: stock/models.py:1391 +#: stock/models.py:1434 msgid "Attachment must be uploaded for this test" msgstr "" -#: stock/models.py:1408 +#: stock/models.py:1451 msgid "Test" msgstr "" -#: stock/models.py:1409 +#: stock/models.py:1452 msgid "Test name" msgstr "" -#: stock/models.py:1414 +#: stock/models.py:1457 msgid "Result" msgstr "" -#: stock/models.py:1415 templates/js/table_filters.js:162 +#: stock/models.py:1458 templates/js/table_filters.js:172 msgid "Test result" msgstr "" -#: stock/models.py:1421 +#: stock/models.py:1464 msgid "Test output value" msgstr "" -#: stock/models.py:1427 +#: stock/models.py:1470 msgid "Attachment" msgstr "" -#: stock/models.py:1428 +#: stock/models.py:1471 msgid "Test result attachment" msgstr "" -#: stock/models.py:1434 +#: stock/models.py:1477 msgid "Test notes" msgstr "" @@ -3980,111 +4062,129 @@ msgid "" "This stock item will be automatically deleted when all stock is depleted." msgstr "" -#: stock/templates/stock/item_base.html:107 templates/js/barcode.js:283 +#: stock/templates/stock/item_base.html:74 +#: stock/templates/stock/item_base.html:310 templates/js/table_filters.js:111 +msgid "Expired" +msgstr "" + +#: stock/templates/stock/item_base.html:78 +#: stock/templates/stock/item_base.html:312 templates/js/table_filters.js:116 +msgid "Stale" +msgstr "" + +#: stock/templates/stock/item_base.html:114 templates/js/barcode.js:283 #: templates/js/barcode.js:288 msgid "Unlink Barcode" msgstr "" -#: stock/templates/stock/item_base.html:109 +#: stock/templates/stock/item_base.html:116 msgid "Link Barcode" msgstr "" -#: stock/templates/stock/item_base.html:117 +#: stock/templates/stock/item_base.html:124 msgid "Stock adjustment actions" msgstr "" -#: stock/templates/stock/item_base.html:121 +#: stock/templates/stock/item_base.html:128 #: stock/templates/stock/location.html:41 templates/stock_table.html:23 msgid "Count stock" msgstr "" -#: stock/templates/stock/item_base.html:122 templates/stock_table.html:21 +#: stock/templates/stock/item_base.html:129 templates/stock_table.html:21 msgid "Add stock" msgstr "" -#: stock/templates/stock/item_base.html:123 templates/stock_table.html:22 +#: stock/templates/stock/item_base.html:130 templates/stock_table.html:22 msgid "Remove stock" msgstr "" -#: stock/templates/stock/item_base.html:125 +#: stock/templates/stock/item_base.html:132 msgid "Transfer stock" msgstr "" -#: stock/templates/stock/item_base.html:127 +#: stock/templates/stock/item_base.html:134 msgid "Serialize stock" msgstr "" -#: stock/templates/stock/item_base.html:131 +#: stock/templates/stock/item_base.html:138 msgid "Assign to customer" msgstr "" -#: stock/templates/stock/item_base.html:134 +#: stock/templates/stock/item_base.html:141 msgid "Return to stock" msgstr "" -#: stock/templates/stock/item_base.html:138 templates/js/stock.js:991 +#: stock/templates/stock/item_base.html:145 templates/js/stock.js:1017 msgid "Uninstall stock item" msgstr "" -#: stock/templates/stock/item_base.html:138 +#: stock/templates/stock/item_base.html:145 msgid "Uninstall" msgstr "" -#: stock/templates/stock/item_base.html:147 +#: stock/templates/stock/item_base.html:154 #: stock/templates/stock/location.html:38 msgid "Stock actions" msgstr "" -#: stock/templates/stock/item_base.html:150 +#: stock/templates/stock/item_base.html:157 msgid "Convert to variant" msgstr "" -#: stock/templates/stock/item_base.html:153 +#: stock/templates/stock/item_base.html:160 msgid "Duplicate stock item" msgstr "" -#: stock/templates/stock/item_base.html:155 +#: stock/templates/stock/item_base.html:162 msgid "Edit stock item" msgstr "" -#: stock/templates/stock/item_base.html:158 +#: stock/templates/stock/item_base.html:165 msgid "Delete stock item" msgstr "" -#: stock/templates/stock/item_base.html:164 +#: stock/templates/stock/item_base.html:171 msgid "Generate test report" msgstr "" -#: stock/templates/stock/item_base.html:172 +#: stock/templates/stock/item_base.html:179 msgid "Stock Item Details" msgstr "" -#: stock/templates/stock/item_base.html:231 templates/js/build.js:442 +#: stock/templates/stock/item_base.html:238 templates/js/build.js:442 msgid "No location set" msgstr "" -#: stock/templates/stock/item_base.html:238 +#: stock/templates/stock/item_base.html:245 msgid "Barcode Identifier" msgstr "" -#: stock/templates/stock/item_base.html:252 templates/js/build.js:642 +#: stock/templates/stock/item_base.html:259 templates/js/build.js:642 #: templates/navbar.html:25 msgid "Build" msgstr "" -#: stock/templates/stock/item_base.html:273 +#: stock/templates/stock/item_base.html:280 msgid "Parent Item" msgstr "" -#: stock/templates/stock/item_base.html:298 +#: stock/templates/stock/item_base.html:310 +msgid "This StockItem expired on" +msgstr "" + +#: stock/templates/stock/item_base.html:312 +msgid "This StockItem expires on" +msgstr "" + +#: stock/templates/stock/item_base.html:319 msgid "Last Updated" msgstr "" -#: stock/templates/stock/item_base.html:303 +#: stock/templates/stock/item_base.html:324 msgid "Last Stocktake" msgstr "" -#: stock/templates/stock/item_base.html:307 +#: stock/templates/stock/item_base.html:328 msgid "No stocktake performed" msgstr "" @@ -4204,7 +4304,7 @@ msgstr "" msgid "The following stock items will be uninstalled" msgstr "" -#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1331 +#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1335 msgid "Convert Stock Item" msgstr "" @@ -4402,39 +4502,39 @@ msgstr "" msgid "Edit Stock Item" msgstr "" -#: stock/views.py:1381 +#: stock/views.py:1385 msgid "Serialize Stock" msgstr "" -#: stock/views.py:1475 templates/js/build.js:210 +#: stock/views.py:1479 templates/js/build.js:210 msgid "Create new Stock Item" msgstr "" -#: stock/views.py:1579 +#: stock/views.py:1587 msgid "Duplicate Stock Item" msgstr "" -#: stock/views.py:1651 +#: stock/views.py:1664 msgid "Quantity cannot be negative" msgstr "" -#: stock/views.py:1737 +#: stock/views.py:1750 msgid "Delete Stock Location" msgstr "" -#: stock/views.py:1751 +#: stock/views.py:1764 msgid "Delete Stock Item" msgstr "" -#: stock/views.py:1763 +#: stock/views.py:1776 msgid "Delete Stock Tracking Entry" msgstr "" -#: stock/views.py:1782 +#: stock/views.py:1795 msgid "Edit Stock Tracking Entry" msgstr "" -#: stock/views.py:1792 +#: stock/views.py:1805 msgid "Add Stock Tracking Entry" msgstr "" @@ -4466,7 +4566,11 @@ msgstr "" msgid "Pending Builds" msgstr "" -#: templates/InvenTree/index.html:4 +#: templates/InvenTree/expired_stock.html:7 +msgid "Expired Stock" +msgstr "" + +#: templates/InvenTree/index.html:5 msgid "Index" msgstr "" @@ -4494,11 +4598,11 @@ msgstr "" msgid "Enter a search query" msgstr "" -#: templates/InvenTree/search.html:191 templates/js/stock.js:289 +#: templates/InvenTree/search.html:191 templates/js/stock.js:290 msgid "Shipped to customer" msgstr "" -#: templates/InvenTree/search.html:194 templates/js/stock.js:299 +#: templates/InvenTree/search.html:194 templates/js/stock.js:300 msgid "No stock location set" msgstr "" @@ -4527,12 +4631,12 @@ msgid "Default Value" msgstr "" #: templates/InvenTree/settings/category.html:70 -#: templates/InvenTree/settings/part.html:75 +#: templates/InvenTree/settings/part.html:78 msgid "Edit Template" msgstr "" #: templates/InvenTree/settings/category.html:71 -#: templates/InvenTree/settings/part.html:76 +#: templates/InvenTree/settings/part.html:79 msgid "Delete Template" msgstr "" @@ -4540,6 +4644,10 @@ msgstr "" msgid "Global InvenTree Settings" msgstr "" +#: templates/InvenTree/settings/header.html:7 +msgid "Setting" +msgstr "" + #: templates/InvenTree/settings/part.html:9 msgid "Part Settings" msgstr "" @@ -4548,11 +4656,11 @@ msgstr "" msgid "Part Options" msgstr "" -#: templates/InvenTree/settings/part.html:34 +#: templates/InvenTree/settings/part.html:37 msgid "Part Parameter Templates" msgstr "" -#: templates/InvenTree/settings/part.html:55 +#: templates/InvenTree/settings/part.html:58 msgid "No part parameter templates found" msgstr "" @@ -4560,11 +4668,11 @@ msgstr "" msgid "Purchase Order Settings" msgstr "" -#: templates/InvenTree/settings/setting.html:16 +#: templates/InvenTree/settings/setting.html:23 msgid "No value set" msgstr "" -#: templates/InvenTree/settings/setting.html:24 +#: templates/InvenTree/settings/setting.html:31 msgid "Edit setting" msgstr "" @@ -4581,6 +4689,10 @@ msgstr "" msgid "Stock Settings" msgstr "" +#: templates/InvenTree/settings/stock.html:13 +msgid "Stock Options" +msgstr "" + #: templates/InvenTree/settings/tabs.html:3 #: templates/InvenTree/settings/user.html:10 msgid "User Settings" @@ -4656,6 +4768,10 @@ msgstr "" msgid "Overdue Sales Orders" msgstr "" +#: templates/InvenTree/stale_stock.html:7 +msgid "Stale Stock" +msgstr "" + #: templates/InvenTree/starred_parts.html:7 msgid "Starred Parts" msgstr "" @@ -4917,7 +5033,7 @@ msgstr "" msgid "No purchase orders found" msgstr "" -#: templates/js/order.js:188 templates/js/stock.js:687 +#: templates/js/order.js:188 templates/js/stock.js:702 msgid "Date" msgstr "" @@ -4957,8 +5073,8 @@ msgstr "" msgid "No parts found" msgstr "" -#: templates/js/part.js:343 templates/js/stock.js:462 -#: templates/js/stock.js:1023 +#: templates/js/part.js:343 templates/js/stock.js:463 +#: templates/js/stock.js:1049 msgid "Select" msgstr "" @@ -4966,7 +5082,7 @@ msgstr "" msgid "No category" msgstr "" -#: templates/js/part.js:429 templates/js/table_filters.js:264 +#: templates/js/part.js:429 templates/js/table_filters.js:274 msgid "Low stock" msgstr "" @@ -4986,11 +5102,11 @@ msgstr "" msgid "No test templates matching query" msgstr "" -#: templates/js/part.js:604 templates/js/stock.js:63 +#: templates/js/part.js:604 templates/js/stock.js:64 msgid "Edit test result" msgstr "" -#: templates/js/part.js:605 templates/js/stock.js:64 +#: templates/js/part.js:605 templates/js/stock.js:65 msgid "Delete test result" msgstr "" @@ -4998,103 +5114,111 @@ msgstr "" msgid "This test is defined for a parent part" msgstr "" -#: templates/js/stock.js:26 +#: templates/js/stock.js:27 msgid "PASS" msgstr "" -#: templates/js/stock.js:28 +#: templates/js/stock.js:29 msgid "FAIL" msgstr "" -#: templates/js/stock.js:33 +#: templates/js/stock.js:34 msgid "NO RESULT" msgstr "" -#: templates/js/stock.js:59 +#: templates/js/stock.js:60 msgid "Add test result" msgstr "" -#: templates/js/stock.js:78 +#: templates/js/stock.js:79 msgid "No test results found" msgstr "" -#: templates/js/stock.js:120 +#: templates/js/stock.js:121 msgid "Test Date" msgstr "" -#: templates/js/stock.js:281 +#: templates/js/stock.js:282 msgid "In production" msgstr "" -#: templates/js/stock.js:285 +#: templates/js/stock.js:286 msgid "Installed in Stock Item" msgstr "" -#: templates/js/stock.js:293 +#: templates/js/stock.js:294 msgid "Assigned to Sales Order" msgstr "" -#: templates/js/stock.js:313 +#: templates/js/stock.js:314 msgid "No stock items matching query" msgstr "" -#: templates/js/stock.js:430 +#: templates/js/stock.js:431 msgid "Undefined location" msgstr "" -#: templates/js/stock.js:524 +#: templates/js/stock.js:525 msgid "Stock item is in production" msgstr "" -#: templates/js/stock.js:529 +#: templates/js/stock.js:530 msgid "Stock item assigned to sales order" msgstr "" -#: templates/js/stock.js:532 +#: templates/js/stock.js:533 msgid "Stock item assigned to customer" msgstr "" -#: templates/js/stock.js:536 +#: templates/js/stock.js:537 +msgid "Stock item has expired" +msgstr "" + +#: templates/js/stock.js:539 +msgid "Stock item will expire soon" +msgstr "" + +#: templates/js/stock.js:543 msgid "Stock item has been allocated" msgstr "" -#: templates/js/stock.js:540 +#: templates/js/stock.js:547 msgid "Stock item has been installed in another item" msgstr "" -#: templates/js/stock.js:548 +#: templates/js/stock.js:555 msgid "Stock item has been rejected" msgstr "" -#: templates/js/stock.js:552 +#: templates/js/stock.js:559 msgid "Stock item is lost" msgstr "" -#: templates/js/stock.js:555 +#: templates/js/stock.js:562 msgid "Stock item is destroyed" msgstr "" -#: templates/js/stock.js:559 templates/js/table_filters.js:106 +#: templates/js/stock.js:566 templates/js/table_filters.js:106 msgid "Depleted" msgstr "" -#: templates/js/stock.js:753 +#: templates/js/stock.js:768 msgid "No user information" msgstr "" -#: templates/js/stock.js:862 +#: templates/js/stock.js:888 msgid "Create New Location" msgstr "" -#: templates/js/stock.js:961 +#: templates/js/stock.js:987 msgid "Serial" msgstr "" -#: templates/js/stock.js:1054 templates/js/table_filters.js:121 +#: templates/js/stock.js:1080 templates/js/table_filters.js:131 msgid "Installed" msgstr "" -#: templates/js/stock.js:1079 +#: templates/js/stock.js:1105 msgid "Install item" msgstr "" @@ -5106,36 +5230,36 @@ msgstr "" msgid "Validated" msgstr "" -#: templates/js/table_filters.js:65 templates/js/table_filters.js:131 +#: templates/js/table_filters.js:65 templates/js/table_filters.js:141 msgid "Is Serialized" msgstr "" -#: templates/js/table_filters.js:68 templates/js/table_filters.js:138 +#: templates/js/table_filters.js:68 templates/js/table_filters.js:148 msgid "Serial number GTE" msgstr "" -#: templates/js/table_filters.js:69 templates/js/table_filters.js:139 +#: templates/js/table_filters.js:69 templates/js/table_filters.js:149 msgid "Serial number greater than or equal to" msgstr "" -#: templates/js/table_filters.js:72 templates/js/table_filters.js:142 +#: templates/js/table_filters.js:72 templates/js/table_filters.js:152 msgid "Serial number LTE" msgstr "" -#: templates/js/table_filters.js:73 templates/js/table_filters.js:143 +#: templates/js/table_filters.js:73 templates/js/table_filters.js:153 msgid "Serial number less than or equal to" msgstr "" #: templates/js/table_filters.js:76 templates/js/table_filters.js:77 -#: templates/js/table_filters.js:134 templates/js/table_filters.js:135 +#: templates/js/table_filters.js:144 templates/js/table_filters.js:145 msgid "Serial number" msgstr "" -#: templates/js/table_filters.js:81 templates/js/table_filters.js:152 +#: templates/js/table_filters.js:81 templates/js/table_filters.js:162 msgid "Batch code" msgstr "" -#: templates/js/table_filters.js:91 templates/js/table_filters.js:231 +#: templates/js/table_filters.js:91 templates/js/table_filters.js:241 msgid "Active parts" msgstr "" @@ -5164,74 +5288,82 @@ msgid "Show stock items which are depleted" msgstr "" #: templates/js/table_filters.js:112 -msgid "Show items which are in stock" -msgstr "" - -#: templates/js/table_filters.js:116 -msgid "In Production" +msgid "Show stock items which have expired" msgstr "" #: templates/js/table_filters.js:117 -msgid "Show items which are in production" +msgid "Show stock which is close to expiring" msgstr "" #: templates/js/table_filters.js:122 -msgid "Show stock items which are installed in another item" +msgid "Show items which are in stock" msgstr "" #: templates/js/table_filters.js:126 -msgid "Sent to customer" +msgid "In Production" msgstr "" #: templates/js/table_filters.js:127 +msgid "Show items which are in production" +msgstr "" + +#: templates/js/table_filters.js:132 +msgid "Show stock items which are installed in another item" +msgstr "" + +#: templates/js/table_filters.js:136 +msgid "Sent to customer" +msgstr "" + +#: templates/js/table_filters.js:137 msgid "Show items which have been assigned to a customer" msgstr "" -#: templates/js/table_filters.js:147 templates/js/table_filters.js:148 +#: templates/js/table_filters.js:157 templates/js/table_filters.js:158 msgid "Stock status" msgstr "" -#: templates/js/table_filters.js:181 +#: templates/js/table_filters.js:191 msgid "Build status" msgstr "" -#: templates/js/table_filters.js:200 templates/js/table_filters.js:213 +#: templates/js/table_filters.js:210 templates/js/table_filters.js:223 msgid "Order status" msgstr "" -#: templates/js/table_filters.js:205 templates/js/table_filters.js:218 +#: templates/js/table_filters.js:215 templates/js/table_filters.js:228 msgid "Outstanding" msgstr "" -#: templates/js/table_filters.js:241 +#: templates/js/table_filters.js:251 msgid "Include subcategories" msgstr "" -#: templates/js/table_filters.js:242 +#: templates/js/table_filters.js:252 msgid "Include parts in subcategories" msgstr "" -#: templates/js/table_filters.js:246 +#: templates/js/table_filters.js:256 msgid "Has IPN" msgstr "" -#: templates/js/table_filters.js:247 +#: templates/js/table_filters.js:257 msgid "Part has internal part number" msgstr "" -#: templates/js/table_filters.js:252 +#: templates/js/table_filters.js:262 msgid "Show active parts" msgstr "" -#: templates/js/table_filters.js:260 +#: templates/js/table_filters.js:270 msgid "Stock available" msgstr "" -#: templates/js/table_filters.js:276 +#: templates/js/table_filters.js:286 msgid "Starred" msgstr "" -#: templates/js/table_filters.js:288 +#: templates/js/table_filters.js:298 msgid "Purchasable" msgstr "" From 7ac7e8f9690971f5a8cf9c4404ad4fb25ff770ce Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 6 Jan 2021 23:38:01 +1100 Subject: [PATCH 32/40] Fixed unit test --- InvenTree/stock/test_api.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py index 520110469c..9b3e926456 100644 --- a/InvenTree/stock/test_api.py +++ b/InvenTree/stock/test_api.py @@ -15,6 +15,8 @@ from django.contrib.auth import get_user_model from InvenTree.helpers import addUserPermissions from InvenTree.status_codes import StockStatus +from common.models import InvenTreeSetting + from .models import StockItem, StockLocation @@ -36,6 +38,9 @@ class StockAPITestCase(APITestCase): self.user = user.objects.create_user('testuser', 'test@testing.com', 'password') + self.user.is_staff = True + self.user.save() + # Add the necessary permissions to the user perms = [ 'view_stockitemtestresult', @@ -223,6 +228,13 @@ class StockItemListTest(StockAPITestCase): Filter StockItem by expiry status """ + # First, we can assume that the 'stock expiry' feature is disabled + response = self.get_stock(expired=1) + self.assertEqual(len(response), 19) + + # Now, ensure that the expiry date feature is enabled! + InvenTreeSetting.set_setting('STOCK_ENABLE_EXPIRY', True, self.user) + response = self.get_stock(expired=1) self.assertEqual(len(response), 1) From bb72658e767be44b4db6359ad27151754e2c2632 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 Jan 2021 00:18:18 +1100 Subject: [PATCH 33/40] Rearrange button options for StockItem --- InvenTree/report/models.py | 4 +-- InvenTree/stock/forms.py | 2 +- InvenTree/stock/models.py | 36 +++++++++++++++++++ .../stock/templates/stock/item_base.html | 24 +++++++++---- 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index b3725cadc0..4dd5fbfa5c 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -16,7 +16,7 @@ from django.conf import settings from django.core.validators import FileExtensionValidator from django.core.exceptions import ValidationError -from stock.models import StockItem +import stock.models from InvenTree.helpers import validateFilterString @@ -191,7 +191,7 @@ class TestReport(ReportTemplateBase): filters = validateFilterString(self.filters) - items = StockItem.objects.filter(**filters) + items = stock.models.StockItem.objects.filter(**filters) # Ensure the provided StockItem object matches the filters items = items.filter(pk=item.pk) diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 5583411009..ec7cbf7805 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -247,7 +247,7 @@ class TestReportFormatForm(HelperForm): templates = TestReport.objects.filter(enabled=True) for template in templates: - if template.matches_stock_item(self.stock_item): + if template.enabled and template.matches_stock_item(self.stock_item): choices.append((template.pk, template)) return choices diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index d399c0daf1..807d6644ca 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -31,6 +31,7 @@ from datetime import datetime, timedelta from InvenTree import helpers import common.models +import report.models from InvenTree.status_codes import StockStatus from InvenTree.models import InvenTreeTree, InvenTreeAttachment @@ -1306,6 +1307,41 @@ class StockItem(MPTTModel): return status['passed'] >= status['total'] + def available_test_reports(self): + """ + Return a list of TestReport objects which match this StockItem. + """ + + reports = [] + + item_query = StockItem.objects.filter(pk=self.pk) + + for test_report in report.models.TestReport.objects.filter(enabled=True): + + filters = helpers.validateFilterString(test_report.filters) + + if item_query.filter(**filters).exists(): + reports.append(test_report) + + return reports + + @property + def has_test_reports(self): + """ + Return True if there are test reports available for this stock item + """ + + return len(self.available_test_reports()) > 0 + + @property + def has_labels(self): + """ + Return True if there are any label templates available for this stock item + """ + + # TODO - Implement this + return True + @receiver(pre_delete, sender=StockItem, dispatch_uid='stock_item_pre_delete_log') def before_delete_stock_item(sender, instance, using, **kwargs): diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index cbb6eeafb2..bea7057351 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -108,16 +108,29 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} +
+ + {% if item.has_labels or item.has_test_reports %} +
+ +
+ {% endif %} {% if roles.stock.change and not item.is_building %}
@@ -168,9 +181,6 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% endif %} -
{% endblock %} From b4277e09e860acc32c763f219dc7cb49e9331baa Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 Jan 2021 14:21:32 +1100 Subject: [PATCH 34/40] Add fullcalendar javascript library --- .../static/fullcalendar/locales-all.js | 1515 ++ .../static/fullcalendar/locales/af.js | 27 + .../static/fullcalendar/locales/ar-dz.js | 28 + .../static/fullcalendar/locales/ar-kw.js | 28 + .../static/fullcalendar/locales/ar-ly.js | 28 + .../static/fullcalendar/locales/ar-ma.js | 28 + .../static/fullcalendar/locales/ar-sa.js | 28 + .../static/fullcalendar/locales/ar-tn.js | 28 + .../static/fullcalendar/locales/ar.js | 28 + .../static/fullcalendar/locales/az.js | 29 + .../static/fullcalendar/locales/bg.js | 28 + .../static/fullcalendar/locales/bs.js | 29 + .../static/fullcalendar/locales/ca.js | 27 + .../static/fullcalendar/locales/cs.js | 29 + .../static/fullcalendar/locales/cy.js | 28 + .../static/fullcalendar/locales/da.js | 27 + .../static/fullcalendar/locales/de-at.js | 30 + .../static/fullcalendar/locales/de.js | 30 + .../static/fullcalendar/locales/el.js | 27 + .../static/fullcalendar/locales/en-au.js | 14 + .../static/fullcalendar/locales/en-gb.js | 14 + .../static/fullcalendar/locales/en-nz.js | 14 + .../static/fullcalendar/locales/eo.js | 27 + .../static/fullcalendar/locales/es-us.js | 27 + .../static/fullcalendar/locales/es.js | 27 + .../static/fullcalendar/locales/et.js | 29 + .../static/fullcalendar/locales/eu.js | 27 + .../static/fullcalendar/locales/fa.js | 30 + .../static/fullcalendar/locales/fi.js | 27 + .../static/fullcalendar/locales/fr-ca.js | 24 + .../static/fullcalendar/locales/fr-ch.js | 28 + .../static/fullcalendar/locales/fr.js | 28 + .../static/fullcalendar/locales/gl.js | 27 + .../static/fullcalendar/locales/he.js | 24 + .../static/fullcalendar/locales/hi.js | 29 + .../static/fullcalendar/locales/hr.js | 29 + .../static/fullcalendar/locales/hu.js | 27 + .../static/fullcalendar/locales/hy-am.js | 29 + .../static/fullcalendar/locales/id.js | 27 + .../static/fullcalendar/locales/is.js | 27 + .../static/fullcalendar/locales/it.js | 29 + .../static/fullcalendar/locales/ja.js | 25 + .../static/fullcalendar/locales/ka.js | 29 + .../static/fullcalendar/locales/kk.js | 29 + .../static/fullcalendar/locales/ko.js | 23 + .../static/fullcalendar/locales/lb.js | 27 + .../static/fullcalendar/locales/lt.js | 27 + .../static/fullcalendar/locales/lv.js | 29 + .../static/fullcalendar/locales/mk.js | 25 + .../static/fullcalendar/locales/ms.js | 29 + .../static/fullcalendar/locales/nb.js | 27 + .../static/fullcalendar/locales/ne.js | 27 + .../static/fullcalendar/locales/nl.js | 27 + .../static/fullcalendar/locales/nn.js | 27 + .../static/fullcalendar/locales/pl.js | 27 + .../static/fullcalendar/locales/pt-br.js | 25 + .../static/fullcalendar/locales/pt.js | 27 + .../static/fullcalendar/locales/ro.js | 29 + .../static/fullcalendar/locales/ru.js | 29 + .../static/fullcalendar/locales/sk.js | 29 + .../static/fullcalendar/locales/sl.js | 27 + .../static/fullcalendar/locales/sq.js | 29 + .../static/fullcalendar/locales/sr-cyrl.js | 29 + .../static/fullcalendar/locales/sr.js | 29 + .../static/fullcalendar/locales/sv.js | 27 + .../static/fullcalendar/locales/th.js | 30 + .../static/fullcalendar/locales/tr.js | 27 + .../static/fullcalendar/locales/ug.js | 17 + .../static/fullcalendar/locales/uk.js | 29 + .../static/fullcalendar/locales/uz.js | 21 + .../static/fullcalendar/locales/vi.js | 29 + .../static/fullcalendar/locales/zh-cn.js | 30 + .../static/fullcalendar/locales/zh-tw.js | 23 + .../InvenTree/static/fullcalendar/main.css | 1429 ++ .../InvenTree/static/fullcalendar/main.js | 14322 ++++++++++++++++ InvenTree/templates/base.html | 2 + 76 files changed, 19203 insertions(+) create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales-all.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/af.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/ar-dz.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/ar-kw.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/ar-ly.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/ar-ma.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/ar-sa.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/ar-tn.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/ar.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/az.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/bg.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/bs.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/ca.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/cs.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/cy.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/da.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/de-at.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/de.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/el.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/en-au.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/en-gb.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/en-nz.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/eo.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/es-us.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/es.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/et.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/eu.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/fa.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/fi.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/fr-ca.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/fr-ch.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/fr.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/gl.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/he.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/hi.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/hr.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/hu.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/hy-am.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/id.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/is.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/it.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/ja.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/ka.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/kk.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/ko.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/lb.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/lt.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/lv.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/mk.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/ms.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/nb.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/ne.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/nl.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/nn.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/pl.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/pt-br.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/pt.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/ro.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/ru.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/sk.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/sl.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/sq.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/sr-cyrl.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/sr.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/sv.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/th.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/tr.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/ug.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/uk.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/uz.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/vi.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/zh-cn.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/locales/zh-tw.js create mode 100644 InvenTree/InvenTree/static/fullcalendar/main.css create mode 100644 InvenTree/InvenTree/static/fullcalendar/main.js diff --git a/InvenTree/InvenTree/static/fullcalendar/locales-all.js b/InvenTree/InvenTree/static/fullcalendar/locales-all.js new file mode 100644 index 0000000000..81eaf9e99e --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales-all.js @@ -0,0 +1,1515 @@ +[].push.apply(FullCalendar.globalLocales, function () { + 'use strict'; + + var l0 = { + code: 'af', + week: { + dow: 1, // Maandag is die eerste dag van die week. + doy: 4, // Die week wat die 4de Januarie bevat is die eerste week van die jaar. + }, + buttonText: { + prev: 'Vorige', + next: 'Volgende', + today: 'Vandag', + year: 'Jaar', + month: 'Maand', + week: 'Week', + day: 'Dag', + list: 'Agenda', + }, + allDayText: 'Heeldag', + moreLinkText: 'Addisionele', + noEventsText: 'Daar is geen gebeurtenisse nie', + }; + + var l1 = { + code: 'ar-dz', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 4, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + var l2 = { + code: 'ar-kw', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + var l3 = { + code: 'ar-ly', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + var l4 = { + code: 'ar-ma', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + var l5 = { + code: 'ar-sa', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 6, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + var l6 = { + code: 'ar-tn', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + var l7 = { + code: 'ar', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + var l8 = { + code: 'az', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Əvvəl', + next: 'Sonra', + today: 'Bu Gün', + month: 'Ay', + week: 'Həftə', + day: 'Gün', + list: 'Gündəm', + }, + weekText: 'Həftə', + allDayText: 'Bütün Gün', + moreLinkText: function(n) { + return '+ daha çox ' + n + }, + noEventsText: 'Göstərmək üçün hadisə yoxdur', + }; + + var l9 = { + code: 'bg', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'назад', + next: 'напред', + today: 'днес', + month: 'Месец', + week: 'Седмица', + day: 'Ден', + list: 'График', + }, + allDayText: 'Цял ден', + moreLinkText: function(n) { + return '+още ' + n + }, + noEventsText: 'Няма събития за показване', + }; + + var l10 = { + code: 'bs', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Prošli', + next: 'Sljedeći', + today: 'Danas', + month: 'Mjesec', + week: 'Sedmica', + day: 'Dan', + list: 'Raspored', + }, + weekText: 'Sed', + allDayText: 'Cijeli dan', + moreLinkText: function(n) { + return '+ još ' + n + }, + noEventsText: 'Nema događaja za prikazivanje', + }; + + var l11 = { + code: 'ca', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Anterior', + next: 'Següent', + today: 'Avui', + month: 'Mes', + week: 'Setmana', + day: 'Dia', + list: 'Agenda', + }, + weekText: 'Set', + allDayText: 'Tot el dia', + moreLinkText: 'més', + noEventsText: 'No hi ha esdeveniments per mostrar', + }; + + var l12 = { + code: 'cs', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Dříve', + next: 'Později', + today: 'Nyní', + month: 'Měsíc', + week: 'Týden', + day: 'Den', + list: 'Agenda', + }, + weekText: 'Týd', + allDayText: 'Celý den', + moreLinkText: function(n) { + return '+další: ' + n + }, + noEventsText: 'Žádné akce k zobrazení', + }; + + var l13 = { + code: 'cy', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Blaenorol', + next: 'Nesaf', + today: 'Heddiw', + year: 'Blwyddyn', + month: 'Mis', + week: 'Wythnos', + day: 'Dydd', + list: 'Rhestr', + }, + weekText: 'Wythnos', + allDayText: 'Trwy\'r dydd', + moreLinkText: 'Mwy', + noEventsText: 'Dim digwyddiadau', + }; + + var l14 = { + code: 'da', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Forrige', + next: 'Næste', + today: 'I dag', + month: 'Måned', + week: 'Uge', + day: 'Dag', + list: 'Agenda', + }, + weekText: 'Uge', + allDayText: 'Hele dagen', + moreLinkText: 'flere', + noEventsText: 'Ingen arrangementer at vise', + }; + + var l15 = { + code: 'de-at', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Zurück', + next: 'Vor', + today: 'Heute', + year: 'Jahr', + month: 'Monat', + week: 'Woche', + day: 'Tag', + list: 'Terminübersicht', + }, + weekText: 'KW', + allDayText: 'Ganztägig', + moreLinkText: function(n) { + return '+ weitere ' + n + }, + noEventsText: 'Keine Ereignisse anzuzeigen', + }; + + var l16 = { + code: 'de', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Zurück', + next: 'Vor', + today: 'Heute', + year: 'Jahr', + month: 'Monat', + week: 'Woche', + day: 'Tag', + list: 'Terminübersicht', + }, + weekText: 'KW', + allDayText: 'Ganztägig', + moreLinkText: function(n) { + return '+ weitere ' + n + }, + noEventsText: 'Keine Ereignisse anzuzeigen', + }; + + var l17 = { + code: 'el', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4st is the first week of the year. + }, + buttonText: { + prev: 'Προηγούμενος', + next: 'Επόμενος', + today: 'Σήμερα', + month: 'Μήνας', + week: 'Εβδομάδα', + day: 'Ημέρα', + list: 'Ατζέντα', + }, + weekText: 'Εβδ', + allDayText: 'Ολοήμερο', + moreLinkText: 'περισσότερα', + noEventsText: 'Δεν υπάρχουν γεγονότα προς εμφάνιση', + }; + + var l18 = { + code: 'en-au', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + }; + + var l19 = { + code: 'en-gb', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + }; + + var l20 = { + code: 'en-nz', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + }; + + var l21 = { + code: 'eo', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Antaŭa', + next: 'Sekva', + today: 'Hodiaŭ', + month: 'Monato', + week: 'Semajno', + day: 'Tago', + list: 'Tagordo', + }, + weekText: 'Sm', + allDayText: 'Tuta tago', + moreLinkText: 'pli', + noEventsText: 'Neniuj eventoj por montri', + }; + + var l22 = { + code: 'es', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 6, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Ant', + next: 'Sig', + today: 'Hoy', + month: 'Mes', + week: 'Semana', + day: 'Día', + list: 'Agenda', + }, + weekText: 'Sm', + allDayText: 'Todo el día', + moreLinkText: 'más', + noEventsText: 'No hay eventos para mostrar', + }; + + var l23 = { + code: 'es', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Ant', + next: 'Sig', + today: 'Hoy', + month: 'Mes', + week: 'Semana', + day: 'Día', + list: 'Agenda', + }, + weekText: 'Sm', + allDayText: 'Todo el día', + moreLinkText: 'más', + noEventsText: 'No hay eventos para mostrar', + }; + + var l24 = { + code: 'et', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Eelnev', + next: 'Järgnev', + today: 'Täna', + month: 'Kuu', + week: 'Nädal', + day: 'Päev', + list: 'Päevakord', + }, + weekText: 'näd', + allDayText: 'Kogu päev', + moreLinkText: function(n) { + return '+ veel ' + n + }, + noEventsText: 'Kuvamiseks puuduvad sündmused', + }; + + var l25 = { + code: 'eu', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Aur', + next: 'Hur', + today: 'Gaur', + month: 'Hilabetea', + week: 'Astea', + day: 'Eguna', + list: 'Agenda', + }, + weekText: 'As', + allDayText: 'Egun osoa', + moreLinkText: 'gehiago', + noEventsText: 'Ez dago ekitaldirik erakusteko', + }; + + var l26 = { + code: 'fa', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'قبلی', + next: 'بعدی', + today: 'امروز', + month: 'ماه', + week: 'هفته', + day: 'روز', + list: 'برنامه', + }, + weekText: 'هف', + allDayText: 'تمام روز', + moreLinkText: function(n) { + return 'بیش از ' + n + }, + noEventsText: 'هیچ رویدادی به نمایش', + }; + + var l27 = { + code: 'fi', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Edellinen', + next: 'Seuraava', + today: 'Tänään', + month: 'Kuukausi', + week: 'Viikko', + day: 'Päivä', + list: 'Tapahtumat', + }, + weekText: 'Vk', + allDayText: 'Koko päivä', + moreLinkText: 'lisää', + noEventsText: 'Ei näytettäviä tapahtumia', + }; + + var l28 = { + code: 'fr', + buttonText: { + prev: 'Précédent', + next: 'Suivant', + today: "Aujourd'hui", + year: 'Année', + month: 'Mois', + week: 'Semaine', + day: 'Jour', + list: 'Mon planning', + }, + weekText: 'Sem.', + allDayText: 'Toute la journée', + moreLinkText: 'en plus', + noEventsText: 'Aucun événement à afficher', + }; + + var l29 = { + code: 'fr-ch', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Précédent', + next: 'Suivant', + today: 'Courant', + year: 'Année', + month: 'Mois', + week: 'Semaine', + day: 'Jour', + list: 'Mon planning', + }, + weekText: 'Sm', + allDayText: 'Toute la journée', + moreLinkText: 'en plus', + noEventsText: 'Aucun événement à afficher', + }; + + var l30 = { + code: 'fr', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Précédent', + next: 'Suivant', + today: "Aujourd'hui", + year: 'Année', + month: 'Mois', + week: 'Semaine', + day: 'Jour', + list: 'Planning', + }, + weekText: 'Sem.', + allDayText: 'Toute la journée', + moreLinkText: 'en plus', + noEventsText: 'Aucun événement à afficher', + }; + + var l31 = { + code: 'gl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Ant', + next: 'Seg', + today: 'Hoxe', + month: 'Mes', + week: 'Semana', + day: 'Día', + list: 'Axenda', + }, + weekText: 'Sm', + allDayText: 'Todo o día', + moreLinkText: 'máis', + noEventsText: 'Non hai eventos para amosar', + }; + + var l32 = { + code: 'he', + direction: 'rtl', + buttonText: { + prev: 'הקודם', + next: 'הבא', + today: 'היום', + month: 'חודש', + week: 'שבוע', + day: 'יום', + list: 'סדר יום', + }, + allDayText: 'כל היום', + moreLinkText: 'אחר', + noEventsText: 'אין אירועים להצגה', + weekText: 'שבוע', + }; + + var l33 = { + code: 'hi', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 6, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'पिछला', + next: 'अगला', + today: 'आज', + month: 'महीना', + week: 'सप्ताह', + day: 'दिन', + list: 'कार्यसूची', + }, + weekText: 'हफ्ता', + allDayText: 'सभी दिन', + moreLinkText: function(n) { + return '+अधिक ' + n + }, + noEventsText: 'कोई घटनाओं को प्रदर्शित करने के लिए', + }; + + var l34 = { + code: 'hr', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Prijašnji', + next: 'Sljedeći', + today: 'Danas', + month: 'Mjesec', + week: 'Tjedan', + day: 'Dan', + list: 'Raspored', + }, + weekText: 'Tje', + allDayText: 'Cijeli dan', + moreLinkText: function(n) { + return '+ još ' + n + }, + noEventsText: 'Nema događaja za prikaz', + }; + + var l35 = { + code: 'hu', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'vissza', + next: 'előre', + today: 'ma', + month: 'Hónap', + week: 'Hét', + day: 'Nap', + list: 'Napló', + }, + weekText: 'Hét', + allDayText: 'Egész nap', + moreLinkText: 'további', + noEventsText: 'Nincs megjeleníthető esemény', + }; + + var l36 = { + code: 'hy-am', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Նախորդ', + next: 'Հաջորդ', + today: 'Այսօր', + month: 'Ամիս', + week: 'Շաբաթ', + day: 'Օր', + list: 'Օրվա ցուցակ', + }, + weekText: 'Շաբ', + allDayText: 'Ամբողջ օր', + moreLinkText: function(n) { + return '+ ևս ' + n + }, + noEventsText: 'Բացակայում է իրադարձությունը ցուցադրելու', + }; + + var l37 = { + code: 'id', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'mundur', + next: 'maju', + today: 'hari ini', + month: 'Bulan', + week: 'Minggu', + day: 'Hari', + list: 'Agenda', + }, + weekText: 'Mg', + allDayText: 'Sehari penuh', + moreLinkText: 'lebih', + noEventsText: 'Tidak ada acara untuk ditampilkan', + }; + + var l38 = { + code: 'is', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Fyrri', + next: 'Næsti', + today: 'Í dag', + month: 'Mánuður', + week: 'Vika', + day: 'Dagur', + list: 'Dagskrá', + }, + weekText: 'Vika', + allDayText: 'Allan daginn', + moreLinkText: 'meira', + noEventsText: 'Engir viðburðir til að sýna', + }; + + var l39 = { + code: 'it', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Prec', + next: 'Succ', + today: 'Oggi', + month: 'Mese', + week: 'Settimana', + day: 'Giorno', + list: 'Agenda', + }, + weekText: 'Sm', + allDayText: 'Tutto il giorno', + moreLinkText: function(n) { + return '+altri ' + n + }, + noEventsText: 'Non ci sono eventi da visualizzare', + }; + + var l40 = { + code: 'ja', + buttonText: { + prev: '前', + next: '次', + today: '今日', + month: '月', + week: '週', + day: '日', + list: '予定リスト', + }, + weekText: '週', + allDayText: '終日', + moreLinkText: function(n) { + return '他 ' + n + ' 件' + }, + noEventsText: '表示する予定はありません', + }; + + var l41 = { + code: 'ka', + week: { + dow: 1, + doy: 7, + }, + buttonText: { + prev: 'წინა', + next: 'შემდეგი', + today: 'დღეს', + month: 'თვე', + week: 'კვირა', + day: 'დღე', + list: 'დღის წესრიგი', + }, + weekText: 'კვ', + allDayText: 'მთელი დღე', + moreLinkText: function(n) { + return '+ კიდევ ' + n + }, + noEventsText: 'ღონისძიებები არ არის', + }; + + var l42 = { + code: 'kk', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Алдыңғы', + next: 'Келесі', + today: 'Бүгін', + month: 'Ай', + week: 'Апта', + day: 'Күн', + list: 'Күн тәртібі', + }, + weekText: 'Не', + allDayText: 'Күні бойы', + moreLinkText: function(n) { + return '+ тағы ' + n + }, + noEventsText: 'Көрсету үшін оқиғалар жоқ', + }; + + var l43 = { + code: 'ko', + buttonText: { + prev: '이전달', + next: '다음달', + today: '오늘', + month: '월', + week: '주', + day: '일', + list: '일정목록', + }, + weekText: '주', + allDayText: '종일', + moreLinkText: '개', + noEventsText: '일정이 없습니다', + }; + + var l44 = { + code: 'lb', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Zréck', + next: 'Weider', + today: 'Haut', + month: 'Mount', + week: 'Woch', + day: 'Dag', + list: 'Terminiwwersiicht', + }, + weekText: 'W', + allDayText: 'Ganzen Dag', + moreLinkText: 'méi', + noEventsText: 'Nee Evenementer ze affichéieren', + }; + + var l45 = { + code: 'lt', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Atgal', + next: 'Pirmyn', + today: 'Šiandien', + month: 'Mėnuo', + week: 'Savaitė', + day: 'Diena', + list: 'Darbotvarkė', + }, + weekText: 'SAV', + allDayText: 'Visą dieną', + moreLinkText: 'daugiau', + noEventsText: 'Nėra įvykių rodyti', + }; + + var l46 = { + code: 'lv', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Iepr.', + next: 'Nāk.', + today: 'Šodien', + month: 'Mēnesis', + week: 'Nedēļa', + day: 'Diena', + list: 'Dienas kārtība', + }, + weekText: 'Ned.', + allDayText: 'Visu dienu', + moreLinkText: function(n) { + return '+vēl ' + n + }, + noEventsText: 'Nav notikumu', + }; + + var l47 = { + code: 'mk', + buttonText: { + prev: 'претходно', + next: 'следно', + today: 'Денес', + month: 'Месец', + week: 'Недела', + day: 'Ден', + list: 'График', + }, + weekText: 'Сед', + allDayText: 'Цел ден', + moreLinkText: function(n) { + return '+повеќе ' + n + }, + noEventsText: 'Нема настани за прикажување', + }; + + var l48 = { + code: 'ms', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Sebelum', + next: 'Selepas', + today: 'hari ini', + month: 'Bulan', + week: 'Minggu', + day: 'Hari', + list: 'Agenda', + }, + weekText: 'Mg', + allDayText: 'Sepanjang hari', + moreLinkText: function(n) { + return 'masih ada ' + n + ' acara' + }, + noEventsText: 'Tiada peristiwa untuk dipaparkan', + }; + + var l49 = { + code: 'nb', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Forrige', + next: 'Neste', + today: 'I dag', + month: 'Måned', + week: 'Uke', + day: 'Dag', + list: 'Agenda', + }, + weekText: 'Uke', + allDayText: 'Hele dagen', + moreLinkText: 'til', + noEventsText: 'Ingen hendelser å vise', + }; + + var l50 = { + code: 'ne', // code for nepal + week: { + dow: 7, // Sunday is the first day of the week. + doy: 1, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'अघिल्लो', + next: 'अर्को', + today: 'आज', + month: 'महिना', + week: 'हप्ता', + day: 'दिन', + list: 'सूची', + }, + weekText: 'हप्ता', + allDayText: 'दिनभरि', + moreLinkText: 'थप लिंक', + noEventsText: 'देखाउनको लागि कुनै घटनाहरू छैनन्', + }; + + var l51 = { + code: 'nl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Vorige', + next: 'Volgende', + today: 'Vandaag', + year: 'Jaar', + month: 'Maand', + week: 'Week', + day: 'Dag', + list: 'Agenda', + }, + allDayText: 'Hele dag', + moreLinkText: 'extra', + noEventsText: 'Geen evenementen om te laten zien', + }; + + var l52 = { + code: 'nn', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Førre', + next: 'Neste', + today: 'I dag', + month: 'Månad', + week: 'Veke', + day: 'Dag', + list: 'Agenda', + }, + weekText: 'Veke', + allDayText: 'Heile dagen', + moreLinkText: 'til', + noEventsText: 'Ingen hendelser å vise', + }; + + var l53 = { + code: 'pl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Poprzedni', + next: 'Następny', + today: 'Dziś', + month: 'Miesiąc', + week: 'Tydzień', + day: 'Dzień', + list: 'Plan dnia', + }, + weekText: 'Tydz', + allDayText: 'Cały dzień', + moreLinkText: 'więcej', + noEventsText: 'Brak wydarzeń do wyświetlenia', + }; + + var l54 = { + code: 'pt-br', + buttonText: { + prev: 'Anterior', + next: 'Próximo', + today: 'Hoje', + month: 'Mês', + week: 'Semana', + day: 'Dia', + list: 'Lista', + }, + weekText: 'Sm', + allDayText: 'dia inteiro', + moreLinkText: function(n) { + return 'mais +' + n + }, + noEventsText: 'Não há eventos para mostrar', + }; + + var l55 = { + code: 'pt', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Anterior', + next: 'Seguinte', + today: 'Hoje', + month: 'Mês', + week: 'Semana', + day: 'Dia', + list: 'Agenda', + }, + weekText: 'Sem', + allDayText: 'Todo o dia', + moreLinkText: 'mais', + noEventsText: 'Não há eventos para mostrar', + }; + + var l56 = { + code: 'ro', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'precedentă', + next: 'următoare', + today: 'Azi', + month: 'Lună', + week: 'Săptămână', + day: 'Zi', + list: 'Agendă', + }, + weekText: 'Săpt', + allDayText: 'Toată ziua', + moreLinkText: function(n) { + return '+alte ' + n + }, + noEventsText: 'Nu există evenimente de afișat', + }; + + var l57 = { + code: 'ru', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Пред', + next: 'След', + today: 'Сегодня', + month: 'Месяц', + week: 'Неделя', + day: 'День', + list: 'Повестка дня', + }, + weekText: 'Нед', + allDayText: 'Весь день', + moreLinkText: function(n) { + return '+ ещё ' + n + }, + noEventsText: 'Нет событий для отображения', + }; + + var l58 = { + code: 'sk', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Predchádzajúci', + next: 'Nasledujúci', + today: 'Dnes', + month: 'Mesiac', + week: 'Týždeň', + day: 'Deň', + list: 'Rozvrh', + }, + weekText: 'Ty', + allDayText: 'Celý deň', + moreLinkText: function(n) { + return '+ďalšie: ' + n + }, + noEventsText: 'Žiadne akcie na zobrazenie', + }; + + var l59 = { + code: 'sl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Prejšnji', + next: 'Naslednji', + today: 'Trenutni', + month: 'Mesec', + week: 'Teden', + day: 'Dan', + list: 'Dnevni red', + }, + weekText: 'Teden', + allDayText: 'Ves dan', + moreLinkText: 'več', + noEventsText: 'Ni dogodkov za prikaz', + }; + + var l60 = { + code: 'sq', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'mbrapa', + next: 'Përpara', + today: 'sot', + month: 'Muaj', + week: 'Javë', + day: 'Ditë', + list: 'Listë', + }, + weekText: 'Ja', + allDayText: 'Gjithë ditën', + moreLinkText: function(n) { + return '+më tepër ' + n + }, + noEventsText: 'Nuk ka evente për të shfaqur', + }; + + var l61 = { + code: 'sr-cyrl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Претходна', + next: 'следећи', + today: 'Данас', + month: 'Месец', + week: 'Недеља', + day: 'Дан', + list: 'Планер', + }, + weekText: 'Сед', + allDayText: 'Цео дан', + moreLinkText: function(n) { + return '+ још ' + n + }, + noEventsText: 'Нема догађаја за приказ', + }; + + var l62 = { + code: 'sr', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Prethodna', + next: 'Sledeći', + today: 'Danas', + month: 'Mеsеc', + week: 'Nеdеlja', + day: 'Dan', + list: 'Planеr', + }, + weekText: 'Sed', + allDayText: 'Cеo dan', + moreLinkText: function(n) { + return '+ još ' + n + }, + noEventsText: 'Nеma događaja za prikaz', + }; + + var l63 = { + code: 'sv', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Förra', + next: 'Nästa', + today: 'Idag', + month: 'Månad', + week: 'Vecka', + day: 'Dag', + list: 'Program', + }, + weekText: 'v.', + allDayText: 'Heldag', + moreLinkText: 'till', + noEventsText: 'Inga händelser att visa', + }; + + var l64 = { + code: 'th', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'ก่อนหน้า', + next: 'ถัดไป', + prevYear: 'ปีก่อนหน้า', + nextYear: 'ปีถัดไป', + year: 'ปี', + today: 'วันนี้', + month: 'เดือน', + week: 'สัปดาห์', + day: 'วัน', + list: 'กำหนดการ', + }, + weekText: 'สัปดาห์', + allDayText: 'ตลอดวัน', + moreLinkText: 'เพิ่มเติม', + noEventsText: 'ไม่มีกิจกรรมที่จะแสดง', + }; + + var l65 = { + code: 'tr', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'geri', + next: 'ileri', + today: 'bugün', + month: 'Ay', + week: 'Hafta', + day: 'Gün', + list: 'Ajanda', + }, + weekText: 'Hf', + allDayText: 'Tüm gün', + moreLinkText: 'daha fazla', + noEventsText: 'Gösterilecek etkinlik yok', + }; + + var l66 = { + code: 'ug', + buttonText: { + month: 'ئاي', + week: 'ھەپتە', + day: 'كۈن', + list: 'كۈنتەرتىپ', + }, + allDayText: 'پۈتۈن كۈن', + }; + + var l67 = { + code: 'uk', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Попередній', + next: 'далі', + today: 'Сьогодні', + month: 'Місяць', + week: 'Тиждень', + day: 'День', + list: 'Порядок денний', + }, + weekText: 'Тиж', + allDayText: 'Увесь день', + moreLinkText: function(n) { + return '+ще ' + n + '...' + }, + noEventsText: 'Немає подій для відображення', + }; + + var l68 = { + code: 'uz', + buttonText: { + month: 'Oy', + week: 'Xafta', + day: 'Kun', + list: 'Kun tartibi', + }, + allDayText: "Kun bo'yi", + moreLinkText: function(n) { + return '+ yana ' + n + }, + noEventsText: "Ko'rsatish uchun voqealar yo'q", + }; + + var l69 = { + code: 'vi', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Trước', + next: 'Tiếp', + today: 'Hôm nay', + month: 'Tháng', + week: 'Tuần', + day: 'Ngày', + list: 'Lịch biểu', + }, + weekText: 'Tu', + allDayText: 'Cả ngày', + moreLinkText: function(n) { + return '+ thêm ' + n + }, + noEventsText: 'Không có sự kiện để hiển thị', + }; + + var l70 = { + code: 'zh-cn', + week: { + // GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效 + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: '上月', + next: '下月', + today: '今天', + month: '月', + week: '周', + day: '日', + list: '日程', + }, + weekText: '周', + allDayText: '全天', + moreLinkText: function(n) { + return '另外 ' + n + ' 个' + }, + noEventsText: '没有事件显示', + }; + + var l71 = { + code: 'zh-tw', + buttonText: { + prev: '上月', + next: '下月', + today: '今天', + month: '月', + week: '週', + day: '天', + list: '活動列表', + }, + weekText: '周', + allDayText: '整天', + moreLinkText: '顯示更多', + noEventsText: '没有任何活動', + }; + + /* eslint max-len: off */ + + var localesAll = [ + l0, l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, l16, l17, l18, l19, l20, l21, l22, l23, l24, l25, l26, l27, l28, l29, l30, l31, l32, l33, l34, l35, l36, l37, l38, l39, l40, l41, l42, l43, l44, l45, l46, l47, l48, l49, l50, l51, l52, l53, l54, l55, l56, l57, l58, l59, l60, l61, l62, l63, l64, l65, l66, l67, l68, l69, l70, l71, + ]; + + return localesAll; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/af.js b/InvenTree/InvenTree/static/fullcalendar/locales/af.js new file mode 100644 index 0000000000..9441707651 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/af.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var af = { + code: 'af', + week: { + dow: 1, // Maandag is die eerste dag van die week. + doy: 4, // Die week wat die 4de Januarie bevat is die eerste week van die jaar. + }, + buttonText: { + prev: 'Vorige', + next: 'Volgende', + today: 'Vandag', + year: 'Jaar', + month: 'Maand', + week: 'Week', + day: 'Dag', + list: 'Agenda', + }, + allDayText: 'Heeldag', + moreLinkText: 'Addisionele', + noEventsText: 'Daar is geen gebeurtenisse nie', + }; + + return af; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/ar-dz.js b/InvenTree/InvenTree/static/fullcalendar/locales/ar-dz.js new file mode 100644 index 0000000000..15e8a03a5a --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/ar-dz.js @@ -0,0 +1,28 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var arDz = { + code: 'ar-dz', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 4, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + return arDz; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/ar-kw.js b/InvenTree/InvenTree/static/fullcalendar/locales/ar-kw.js new file mode 100644 index 0000000000..daf9221167 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/ar-kw.js @@ -0,0 +1,28 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var arKw = { + code: 'ar-kw', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + return arKw; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/ar-ly.js b/InvenTree/InvenTree/static/fullcalendar/locales/ar-ly.js new file mode 100644 index 0000000000..2ff0986fb1 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/ar-ly.js @@ -0,0 +1,28 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var arLy = { + code: 'ar-ly', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + return arLy; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/ar-ma.js b/InvenTree/InvenTree/static/fullcalendar/locales/ar-ma.js new file mode 100644 index 0000000000..e19ae1d7a9 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/ar-ma.js @@ -0,0 +1,28 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var arMa = { + code: 'ar-ma', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + return arMa; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/ar-sa.js b/InvenTree/InvenTree/static/fullcalendar/locales/ar-sa.js new file mode 100644 index 0000000000..0e1d4fbca0 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/ar-sa.js @@ -0,0 +1,28 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var arSa = { + code: 'ar-sa', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 6, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + return arSa; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/ar-tn.js b/InvenTree/InvenTree/static/fullcalendar/locales/ar-tn.js new file mode 100644 index 0000000000..ea5953dbde --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/ar-tn.js @@ -0,0 +1,28 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var arTn = { + code: 'ar-tn', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + return arTn; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/ar.js b/InvenTree/InvenTree/static/fullcalendar/locales/ar.js new file mode 100644 index 0000000000..1da9728db0 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/ar.js @@ -0,0 +1,28 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var ar = { + code: 'ar', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + return ar; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/az.js b/InvenTree/InvenTree/static/fullcalendar/locales/az.js new file mode 100644 index 0000000000..13ce6ebd82 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/az.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var az = { + code: 'az', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Əvvəl', + next: 'Sonra', + today: 'Bu Gün', + month: 'Ay', + week: 'Həftə', + day: 'Gün', + list: 'Gündəm', + }, + weekText: 'Həftə', + allDayText: 'Bütün Gün', + moreLinkText: function(n) { + return '+ daha çox ' + n + }, + noEventsText: 'Göstərmək üçün hadisə yoxdur', + }; + + return az; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/bg.js b/InvenTree/InvenTree/static/fullcalendar/locales/bg.js new file mode 100644 index 0000000000..6484e0aa21 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/bg.js @@ -0,0 +1,28 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var bg = { + code: 'bg', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'назад', + next: 'напред', + today: 'днес', + month: 'Месец', + week: 'Седмица', + day: 'Ден', + list: 'График', + }, + allDayText: 'Цял ден', + moreLinkText: function(n) { + return '+още ' + n + }, + noEventsText: 'Няма събития за показване', + }; + + return bg; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/bs.js b/InvenTree/InvenTree/static/fullcalendar/locales/bs.js new file mode 100644 index 0000000000..e4d378a39c --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/bs.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var bs = { + code: 'bs', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Prošli', + next: 'Sljedeći', + today: 'Danas', + month: 'Mjesec', + week: 'Sedmica', + day: 'Dan', + list: 'Raspored', + }, + weekText: 'Sed', + allDayText: 'Cijeli dan', + moreLinkText: function(n) { + return '+ još ' + n + }, + noEventsText: 'Nema događaja za prikazivanje', + }; + + return bs; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/ca.js b/InvenTree/InvenTree/static/fullcalendar/locales/ca.js new file mode 100644 index 0000000000..4a3b872550 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/ca.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var ca = { + code: 'ca', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Anterior', + next: 'Següent', + today: 'Avui', + month: 'Mes', + week: 'Setmana', + day: 'Dia', + list: 'Agenda', + }, + weekText: 'Set', + allDayText: 'Tot el dia', + moreLinkText: 'més', + noEventsText: 'No hi ha esdeveniments per mostrar', + }; + + return ca; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/cs.js b/InvenTree/InvenTree/static/fullcalendar/locales/cs.js new file mode 100644 index 0000000000..2d66fba359 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/cs.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var cs = { + code: 'cs', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Dříve', + next: 'Později', + today: 'Nyní', + month: 'Měsíc', + week: 'Týden', + day: 'Den', + list: 'Agenda', + }, + weekText: 'Týd', + allDayText: 'Celý den', + moreLinkText: function(n) { + return '+další: ' + n + }, + noEventsText: 'Žádné akce k zobrazení', + }; + + return cs; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/cy.js b/InvenTree/InvenTree/static/fullcalendar/locales/cy.js new file mode 100644 index 0000000000..3e5d26aeb5 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/cy.js @@ -0,0 +1,28 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var cy = { + code: 'cy', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Blaenorol', + next: 'Nesaf', + today: 'Heddiw', + year: 'Blwyddyn', + month: 'Mis', + week: 'Wythnos', + day: 'Dydd', + list: 'Rhestr', + }, + weekText: 'Wythnos', + allDayText: 'Trwy\'r dydd', + moreLinkText: 'Mwy', + noEventsText: 'Dim digwyddiadau', + }; + + return cy; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/da.js b/InvenTree/InvenTree/static/fullcalendar/locales/da.js new file mode 100644 index 0000000000..c8856d956c --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/da.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var da = { + code: 'da', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Forrige', + next: 'Næste', + today: 'I dag', + month: 'Måned', + week: 'Uge', + day: 'Dag', + list: 'Agenda', + }, + weekText: 'Uge', + allDayText: 'Hele dagen', + moreLinkText: 'flere', + noEventsText: 'Ingen arrangementer at vise', + }; + + return da; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/de-at.js b/InvenTree/InvenTree/static/fullcalendar/locales/de-at.js new file mode 100644 index 0000000000..d509d1ebb4 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/de-at.js @@ -0,0 +1,30 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var deAt = { + code: 'de-at', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Zurück', + next: 'Vor', + today: 'Heute', + year: 'Jahr', + month: 'Monat', + week: 'Woche', + day: 'Tag', + list: 'Terminübersicht', + }, + weekText: 'KW', + allDayText: 'Ganztägig', + moreLinkText: function(n) { + return '+ weitere ' + n + }, + noEventsText: 'Keine Ereignisse anzuzeigen', + }; + + return deAt; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/de.js b/InvenTree/InvenTree/static/fullcalendar/locales/de.js new file mode 100644 index 0000000000..a03d5451e6 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/de.js @@ -0,0 +1,30 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var de = { + code: 'de', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Zurück', + next: 'Vor', + today: 'Heute', + year: 'Jahr', + month: 'Monat', + week: 'Woche', + day: 'Tag', + list: 'Terminübersicht', + }, + weekText: 'KW', + allDayText: 'Ganztägig', + moreLinkText: function(n) { + return '+ weitere ' + n + }, + noEventsText: 'Keine Ereignisse anzuzeigen', + }; + + return de; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/el.js b/InvenTree/InvenTree/static/fullcalendar/locales/el.js new file mode 100644 index 0000000000..01e51044df --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/el.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var el = { + code: 'el', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4st is the first week of the year. + }, + buttonText: { + prev: 'Προηγούμενος', + next: 'Επόμενος', + today: 'Σήμερα', + month: 'Μήνας', + week: 'Εβδομάδα', + day: 'Ημέρα', + list: 'Ατζέντα', + }, + weekText: 'Εβδ', + allDayText: 'Ολοήμερο', + moreLinkText: 'περισσότερα', + noEventsText: 'Δεν υπάρχουν γεγονότα προς εμφάνιση', + }; + + return el; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/en-au.js b/InvenTree/InvenTree/static/fullcalendar/locales/en-au.js new file mode 100644 index 0000000000..259ce9a2e4 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/en-au.js @@ -0,0 +1,14 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var enAu = { + code: 'en-au', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + }; + + return enAu; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/en-gb.js b/InvenTree/InvenTree/static/fullcalendar/locales/en-gb.js new file mode 100644 index 0000000000..286eb10fa5 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/en-gb.js @@ -0,0 +1,14 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var enGb = { + code: 'en-gb', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + }; + + return enGb; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/en-nz.js b/InvenTree/InvenTree/static/fullcalendar/locales/en-nz.js new file mode 100644 index 0000000000..c1006f8f31 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/en-nz.js @@ -0,0 +1,14 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var enNz = { + code: 'en-nz', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + }; + + return enNz; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/eo.js b/InvenTree/InvenTree/static/fullcalendar/locales/eo.js new file mode 100644 index 0000000000..68f36d13fa --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/eo.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var eo = { + code: 'eo', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Antaŭa', + next: 'Sekva', + today: 'Hodiaŭ', + month: 'Monato', + week: 'Semajno', + day: 'Tago', + list: 'Tagordo', + }, + weekText: 'Sm', + allDayText: 'Tuta tago', + moreLinkText: 'pli', + noEventsText: 'Neniuj eventoj por montri', + }; + + return eo; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/es-us.js b/InvenTree/InvenTree/static/fullcalendar/locales/es-us.js new file mode 100644 index 0000000000..2696726cbe --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/es-us.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var esUs = { + code: 'es', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 6, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Ant', + next: 'Sig', + today: 'Hoy', + month: 'Mes', + week: 'Semana', + day: 'Día', + list: 'Agenda', + }, + weekText: 'Sm', + allDayText: 'Todo el día', + moreLinkText: 'más', + noEventsText: 'No hay eventos para mostrar', + }; + + return esUs; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/es.js b/InvenTree/InvenTree/static/fullcalendar/locales/es.js new file mode 100644 index 0000000000..4de4e7667b --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/es.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var es = { + code: 'es', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Ant', + next: 'Sig', + today: 'Hoy', + month: 'Mes', + week: 'Semana', + day: 'Día', + list: 'Agenda', + }, + weekText: 'Sm', + allDayText: 'Todo el día', + moreLinkText: 'más', + noEventsText: 'No hay eventos para mostrar', + }; + + return es; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/et.js b/InvenTree/InvenTree/static/fullcalendar/locales/et.js new file mode 100644 index 0000000000..cd115804aa --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/et.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var et = { + code: 'et', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Eelnev', + next: 'Järgnev', + today: 'Täna', + month: 'Kuu', + week: 'Nädal', + day: 'Päev', + list: 'Päevakord', + }, + weekText: 'näd', + allDayText: 'Kogu päev', + moreLinkText: function(n) { + return '+ veel ' + n + }, + noEventsText: 'Kuvamiseks puuduvad sündmused', + }; + + return et; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/eu.js b/InvenTree/InvenTree/static/fullcalendar/locales/eu.js new file mode 100644 index 0000000000..8f7d056ccc --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/eu.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var eu = { + code: 'eu', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Aur', + next: 'Hur', + today: 'Gaur', + month: 'Hilabetea', + week: 'Astea', + day: 'Eguna', + list: 'Agenda', + }, + weekText: 'As', + allDayText: 'Egun osoa', + moreLinkText: 'gehiago', + noEventsText: 'Ez dago ekitaldirik erakusteko', + }; + + return eu; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/fa.js b/InvenTree/InvenTree/static/fullcalendar/locales/fa.js new file mode 100644 index 0000000000..52aa1781b2 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/fa.js @@ -0,0 +1,30 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var fa = { + code: 'fa', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'قبلی', + next: 'بعدی', + today: 'امروز', + month: 'ماه', + week: 'هفته', + day: 'روز', + list: 'برنامه', + }, + weekText: 'هف', + allDayText: 'تمام روز', + moreLinkText: function(n) { + return 'بیش از ' + n + }, + noEventsText: 'هیچ رویدادی به نمایش', + }; + + return fa; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/fi.js b/InvenTree/InvenTree/static/fullcalendar/locales/fi.js new file mode 100644 index 0000000000..d1cec613be --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/fi.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var fi = { + code: 'fi', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Edellinen', + next: 'Seuraava', + today: 'Tänään', + month: 'Kuukausi', + week: 'Viikko', + day: 'Päivä', + list: 'Tapahtumat', + }, + weekText: 'Vk', + allDayText: 'Koko päivä', + moreLinkText: 'lisää', + noEventsText: 'Ei näytettäviä tapahtumia', + }; + + return fi; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/fr-ca.js b/InvenTree/InvenTree/static/fullcalendar/locales/fr-ca.js new file mode 100644 index 0000000000..54027881b0 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/fr-ca.js @@ -0,0 +1,24 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var frCa = { + code: 'fr', + buttonText: { + prev: 'Précédent', + next: 'Suivant', + today: "Aujourd'hui", + year: 'Année', + month: 'Mois', + week: 'Semaine', + day: 'Jour', + list: 'Mon planning', + }, + weekText: 'Sem.', + allDayText: 'Toute la journée', + moreLinkText: 'en plus', + noEventsText: 'Aucun événement à afficher', + }; + + return frCa; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/fr-ch.js b/InvenTree/InvenTree/static/fullcalendar/locales/fr-ch.js new file mode 100644 index 0000000000..c22591b5f4 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/fr-ch.js @@ -0,0 +1,28 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var frCh = { + code: 'fr-ch', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Précédent', + next: 'Suivant', + today: 'Courant', + year: 'Année', + month: 'Mois', + week: 'Semaine', + day: 'Jour', + list: 'Mon planning', + }, + weekText: 'Sm', + allDayText: 'Toute la journée', + moreLinkText: 'en plus', + noEventsText: 'Aucun événement à afficher', + }; + + return frCh; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/fr.js b/InvenTree/InvenTree/static/fullcalendar/locales/fr.js new file mode 100644 index 0000000000..5fac2c7a78 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/fr.js @@ -0,0 +1,28 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var fr = { + code: 'fr', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Précédent', + next: 'Suivant', + today: "Aujourd'hui", + year: 'Année', + month: 'Mois', + week: 'Semaine', + day: 'Jour', + list: 'Planning', + }, + weekText: 'Sem.', + allDayText: 'Toute la journée', + moreLinkText: 'en plus', + noEventsText: 'Aucun événement à afficher', + }; + + return fr; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/gl.js b/InvenTree/InvenTree/static/fullcalendar/locales/gl.js new file mode 100644 index 0000000000..4614ae52b9 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/gl.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var gl = { + code: 'gl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Ant', + next: 'Seg', + today: 'Hoxe', + month: 'Mes', + week: 'Semana', + day: 'Día', + list: 'Axenda', + }, + weekText: 'Sm', + allDayText: 'Todo o día', + moreLinkText: 'máis', + noEventsText: 'Non hai eventos para amosar', + }; + + return gl; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/he.js b/InvenTree/InvenTree/static/fullcalendar/locales/he.js new file mode 100644 index 0000000000..b5597e7412 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/he.js @@ -0,0 +1,24 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var he = { + code: 'he', + direction: 'rtl', + buttonText: { + prev: 'הקודם', + next: 'הבא', + today: 'היום', + month: 'חודש', + week: 'שבוע', + day: 'יום', + list: 'סדר יום', + }, + allDayText: 'כל היום', + moreLinkText: 'אחר', + noEventsText: 'אין אירועים להצגה', + weekText: 'שבוע', + }; + + return he; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/hi.js b/InvenTree/InvenTree/static/fullcalendar/locales/hi.js new file mode 100644 index 0000000000..ca0ef21f52 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/hi.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var hi = { + code: 'hi', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 6, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'पिछला', + next: 'अगला', + today: 'आज', + month: 'महीना', + week: 'सप्ताह', + day: 'दिन', + list: 'कार्यसूची', + }, + weekText: 'हफ्ता', + allDayText: 'सभी दिन', + moreLinkText: function(n) { + return '+अधिक ' + n + }, + noEventsText: 'कोई घटनाओं को प्रदर्शित करने के लिए', + }; + + return hi; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/hr.js b/InvenTree/InvenTree/static/fullcalendar/locales/hr.js new file mode 100644 index 0000000000..1f3c7142f6 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/hr.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var hr = { + code: 'hr', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Prijašnji', + next: 'Sljedeći', + today: 'Danas', + month: 'Mjesec', + week: 'Tjedan', + day: 'Dan', + list: 'Raspored', + }, + weekText: 'Tje', + allDayText: 'Cijeli dan', + moreLinkText: function(n) { + return '+ još ' + n + }, + noEventsText: 'Nema događaja za prikaz', + }; + + return hr; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/hu.js b/InvenTree/InvenTree/static/fullcalendar/locales/hu.js new file mode 100644 index 0000000000..8d1c9381ea --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/hu.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var hu = { + code: 'hu', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'vissza', + next: 'előre', + today: 'ma', + month: 'Hónap', + week: 'Hét', + day: 'Nap', + list: 'Napló', + }, + weekText: 'Hét', + allDayText: 'Egész nap', + moreLinkText: 'további', + noEventsText: 'Nincs megjeleníthető esemény', + }; + + return hu; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/hy-am.js b/InvenTree/InvenTree/static/fullcalendar/locales/hy-am.js new file mode 100644 index 0000000000..81c86c1dbd --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/hy-am.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var hyAm = { + code: 'hy-am', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Նախորդ', + next: 'Հաջորդ', + today: 'Այսօր', + month: 'Ամիս', + week: 'Շաբաթ', + day: 'Օր', + list: 'Օրվա ցուցակ', + }, + weekText: 'Շաբ', + allDayText: 'Ամբողջ օր', + moreLinkText: function(n) { + return '+ ևս ' + n + }, + noEventsText: 'Բացակայում է իրադարձությունը ցուցադրելու', + }; + + return hyAm; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/id.js b/InvenTree/InvenTree/static/fullcalendar/locales/id.js new file mode 100644 index 0000000000..7653f93234 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/id.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var id = { + code: 'id', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'mundur', + next: 'maju', + today: 'hari ini', + month: 'Bulan', + week: 'Minggu', + day: 'Hari', + list: 'Agenda', + }, + weekText: 'Mg', + allDayText: 'Sehari penuh', + moreLinkText: 'lebih', + noEventsText: 'Tidak ada acara untuk ditampilkan', + }; + + return id; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/is.js b/InvenTree/InvenTree/static/fullcalendar/locales/is.js new file mode 100644 index 0000000000..f9f6594938 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/is.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var is = { + code: 'is', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Fyrri', + next: 'Næsti', + today: 'Í dag', + month: 'Mánuður', + week: 'Vika', + day: 'Dagur', + list: 'Dagskrá', + }, + weekText: 'Vika', + allDayText: 'Allan daginn', + moreLinkText: 'meira', + noEventsText: 'Engir viðburðir til að sýna', + }; + + return is; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/it.js b/InvenTree/InvenTree/static/fullcalendar/locales/it.js new file mode 100644 index 0000000000..4e08f694a7 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/it.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var it = { + code: 'it', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Prec', + next: 'Succ', + today: 'Oggi', + month: 'Mese', + week: 'Settimana', + day: 'Giorno', + list: 'Agenda', + }, + weekText: 'Sm', + allDayText: 'Tutto il giorno', + moreLinkText: function(n) { + return '+altri ' + n + }, + noEventsText: 'Non ci sono eventi da visualizzare', + }; + + return it; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/ja.js b/InvenTree/InvenTree/static/fullcalendar/locales/ja.js new file mode 100644 index 0000000000..7809cdea16 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/ja.js @@ -0,0 +1,25 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var ja = { + code: 'ja', + buttonText: { + prev: '前', + next: '次', + today: '今日', + month: '月', + week: '週', + day: '日', + list: '予定リスト', + }, + weekText: '週', + allDayText: '終日', + moreLinkText: function(n) { + return '他 ' + n + ' 件' + }, + noEventsText: '表示する予定はありません', + }; + + return ja; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/ka.js b/InvenTree/InvenTree/static/fullcalendar/locales/ka.js new file mode 100644 index 0000000000..030e95f724 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/ka.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var ka = { + code: 'ka', + week: { + dow: 1, + doy: 7, + }, + buttonText: { + prev: 'წინა', + next: 'შემდეგი', + today: 'დღეს', + month: 'თვე', + week: 'კვირა', + day: 'დღე', + list: 'დღის წესრიგი', + }, + weekText: 'კვ', + allDayText: 'მთელი დღე', + moreLinkText: function(n) { + return '+ კიდევ ' + n + }, + noEventsText: 'ღონისძიებები არ არის', + }; + + return ka; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/kk.js b/InvenTree/InvenTree/static/fullcalendar/locales/kk.js new file mode 100644 index 0000000000..77db338a35 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/kk.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var kk = { + code: 'kk', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Алдыңғы', + next: 'Келесі', + today: 'Бүгін', + month: 'Ай', + week: 'Апта', + day: 'Күн', + list: 'Күн тәртібі', + }, + weekText: 'Не', + allDayText: 'Күні бойы', + moreLinkText: function(n) { + return '+ тағы ' + n + }, + noEventsText: 'Көрсету үшін оқиғалар жоқ', + }; + + return kk; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/ko.js b/InvenTree/InvenTree/static/fullcalendar/locales/ko.js new file mode 100644 index 0000000000..120c28597f --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/ko.js @@ -0,0 +1,23 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var ko = { + code: 'ko', + buttonText: { + prev: '이전달', + next: '다음달', + today: '오늘', + month: '월', + week: '주', + day: '일', + list: '일정목록', + }, + weekText: '주', + allDayText: '종일', + moreLinkText: '개', + noEventsText: '일정이 없습니다', + }; + + return ko; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/lb.js b/InvenTree/InvenTree/static/fullcalendar/locales/lb.js new file mode 100644 index 0000000000..c65100e9d6 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/lb.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var lb = { + code: 'lb', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Zréck', + next: 'Weider', + today: 'Haut', + month: 'Mount', + week: 'Woch', + day: 'Dag', + list: 'Terminiwwersiicht', + }, + weekText: 'W', + allDayText: 'Ganzen Dag', + moreLinkText: 'méi', + noEventsText: 'Nee Evenementer ze affichéieren', + }; + + return lb; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/lt.js b/InvenTree/InvenTree/static/fullcalendar/locales/lt.js new file mode 100644 index 0000000000..92f629c5d1 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/lt.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var lt = { + code: 'lt', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Atgal', + next: 'Pirmyn', + today: 'Šiandien', + month: 'Mėnuo', + week: 'Savaitė', + day: 'Diena', + list: 'Darbotvarkė', + }, + weekText: 'SAV', + allDayText: 'Visą dieną', + moreLinkText: 'daugiau', + noEventsText: 'Nėra įvykių rodyti', + }; + + return lt; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/lv.js b/InvenTree/InvenTree/static/fullcalendar/locales/lv.js new file mode 100644 index 0000000000..e9677a1e00 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/lv.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var lv = { + code: 'lv', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Iepr.', + next: 'Nāk.', + today: 'Šodien', + month: 'Mēnesis', + week: 'Nedēļa', + day: 'Diena', + list: 'Dienas kārtība', + }, + weekText: 'Ned.', + allDayText: 'Visu dienu', + moreLinkText: function(n) { + return '+vēl ' + n + }, + noEventsText: 'Nav notikumu', + }; + + return lv; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/mk.js b/InvenTree/InvenTree/static/fullcalendar/locales/mk.js new file mode 100644 index 0000000000..9eb98f4956 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/mk.js @@ -0,0 +1,25 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var mk = { + code: 'mk', + buttonText: { + prev: 'претходно', + next: 'следно', + today: 'Денес', + month: 'Месец', + week: 'Недела', + day: 'Ден', + list: 'График', + }, + weekText: 'Сед', + allDayText: 'Цел ден', + moreLinkText: function(n) { + return '+повеќе ' + n + }, + noEventsText: 'Нема настани за прикажување', + }; + + return mk; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/ms.js b/InvenTree/InvenTree/static/fullcalendar/locales/ms.js new file mode 100644 index 0000000000..2539f931c8 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/ms.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var ms = { + code: 'ms', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Sebelum', + next: 'Selepas', + today: 'hari ini', + month: 'Bulan', + week: 'Minggu', + day: 'Hari', + list: 'Agenda', + }, + weekText: 'Mg', + allDayText: 'Sepanjang hari', + moreLinkText: function(n) { + return 'masih ada ' + n + ' acara' + }, + noEventsText: 'Tiada peristiwa untuk dipaparkan', + }; + + return ms; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/nb.js b/InvenTree/InvenTree/static/fullcalendar/locales/nb.js new file mode 100644 index 0000000000..2dab831bf2 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/nb.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var nb = { + code: 'nb', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Forrige', + next: 'Neste', + today: 'I dag', + month: 'Måned', + week: 'Uke', + day: 'Dag', + list: 'Agenda', + }, + weekText: 'Uke', + allDayText: 'Hele dagen', + moreLinkText: 'til', + noEventsText: 'Ingen hendelser å vise', + }; + + return nb; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/ne.js b/InvenTree/InvenTree/static/fullcalendar/locales/ne.js new file mode 100644 index 0000000000..2215dd4a89 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/ne.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var ne = { + code: 'ne', // code for nepal + week: { + dow: 7, // Sunday is the first day of the week. + doy: 1, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'अघिल्लो', + next: 'अर्को', + today: 'आज', + month: 'महिना', + week: 'हप्ता', + day: 'दिन', + list: 'सूची', + }, + weekText: 'हप्ता', + allDayText: 'दिनभरि', + moreLinkText: 'थप लिंक', + noEventsText: 'देखाउनको लागि कुनै घटनाहरू छैनन्', + }; + + return ne; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/nl.js b/InvenTree/InvenTree/static/fullcalendar/locales/nl.js new file mode 100644 index 0000000000..d13d2d2e77 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/nl.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var nl = { + code: 'nl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Vorige', + next: 'Volgende', + today: 'Vandaag', + year: 'Jaar', + month: 'Maand', + week: 'Week', + day: 'Dag', + list: 'Agenda', + }, + allDayText: 'Hele dag', + moreLinkText: 'extra', + noEventsText: 'Geen evenementen om te laten zien', + }; + + return nl; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/nn.js b/InvenTree/InvenTree/static/fullcalendar/locales/nn.js new file mode 100644 index 0000000000..2f9a5fdeb7 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/nn.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var nn = { + code: 'nn', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Førre', + next: 'Neste', + today: 'I dag', + month: 'Månad', + week: 'Veke', + day: 'Dag', + list: 'Agenda', + }, + weekText: 'Veke', + allDayText: 'Heile dagen', + moreLinkText: 'til', + noEventsText: 'Ingen hendelser å vise', + }; + + return nn; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/pl.js b/InvenTree/InvenTree/static/fullcalendar/locales/pl.js new file mode 100644 index 0000000000..996071df85 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/pl.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var pl = { + code: 'pl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Poprzedni', + next: 'Następny', + today: 'Dziś', + month: 'Miesiąc', + week: 'Tydzień', + day: 'Dzień', + list: 'Plan dnia', + }, + weekText: 'Tydz', + allDayText: 'Cały dzień', + moreLinkText: 'więcej', + noEventsText: 'Brak wydarzeń do wyświetlenia', + }; + + return pl; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/pt-br.js b/InvenTree/InvenTree/static/fullcalendar/locales/pt-br.js new file mode 100644 index 0000000000..e26167a60b --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/pt-br.js @@ -0,0 +1,25 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var ptBr = { + code: 'pt-br', + buttonText: { + prev: 'Anterior', + next: 'Próximo', + today: 'Hoje', + month: 'Mês', + week: 'Semana', + day: 'Dia', + list: 'Lista', + }, + weekText: 'Sm', + allDayText: 'dia inteiro', + moreLinkText: function(n) { + return 'mais +' + n + }, + noEventsText: 'Não há eventos para mostrar', + }; + + return ptBr; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/pt.js b/InvenTree/InvenTree/static/fullcalendar/locales/pt.js new file mode 100644 index 0000000000..34dc37706b --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/pt.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var pt = { + code: 'pt', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Anterior', + next: 'Seguinte', + today: 'Hoje', + month: 'Mês', + week: 'Semana', + day: 'Dia', + list: 'Agenda', + }, + weekText: 'Sem', + allDayText: 'Todo o dia', + moreLinkText: 'mais', + noEventsText: 'Não há eventos para mostrar', + }; + + return pt; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/ro.js b/InvenTree/InvenTree/static/fullcalendar/locales/ro.js new file mode 100644 index 0000000000..d74f23cb5f --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/ro.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var ro = { + code: 'ro', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'precedentă', + next: 'următoare', + today: 'Azi', + month: 'Lună', + week: 'Săptămână', + day: 'Zi', + list: 'Agendă', + }, + weekText: 'Săpt', + allDayText: 'Toată ziua', + moreLinkText: function(n) { + return '+alte ' + n + }, + noEventsText: 'Nu există evenimente de afișat', + }; + + return ro; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/ru.js b/InvenTree/InvenTree/static/fullcalendar/locales/ru.js new file mode 100644 index 0000000000..58fa31152e --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/ru.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var ru = { + code: 'ru', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Пред', + next: 'След', + today: 'Сегодня', + month: 'Месяц', + week: 'Неделя', + day: 'День', + list: 'Повестка дня', + }, + weekText: 'Нед', + allDayText: 'Весь день', + moreLinkText: function(n) { + return '+ ещё ' + n + }, + noEventsText: 'Нет событий для отображения', + }; + + return ru; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/sk.js b/InvenTree/InvenTree/static/fullcalendar/locales/sk.js new file mode 100644 index 0000000000..ffb5e8b9a2 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/sk.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var sk = { + code: 'sk', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Predchádzajúci', + next: 'Nasledujúci', + today: 'Dnes', + month: 'Mesiac', + week: 'Týždeň', + day: 'Deň', + list: 'Rozvrh', + }, + weekText: 'Ty', + allDayText: 'Celý deň', + moreLinkText: function(n) { + return '+ďalšie: ' + n + }, + noEventsText: 'Žiadne akcie na zobrazenie', + }; + + return sk; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/sl.js b/InvenTree/InvenTree/static/fullcalendar/locales/sl.js new file mode 100644 index 0000000000..4c62799418 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/sl.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var sl = { + code: 'sl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Prejšnji', + next: 'Naslednji', + today: 'Trenutni', + month: 'Mesec', + week: 'Teden', + day: 'Dan', + list: 'Dnevni red', + }, + weekText: 'Teden', + allDayText: 'Ves dan', + moreLinkText: 'več', + noEventsText: 'Ni dogodkov za prikaz', + }; + + return sl; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/sq.js b/InvenTree/InvenTree/static/fullcalendar/locales/sq.js new file mode 100644 index 0000000000..a7b630ea79 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/sq.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var sq = { + code: 'sq', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'mbrapa', + next: 'Përpara', + today: 'sot', + month: 'Muaj', + week: 'Javë', + day: 'Ditë', + list: 'Listë', + }, + weekText: 'Ja', + allDayText: 'Gjithë ditën', + moreLinkText: function(n) { + return '+më tepër ' + n + }, + noEventsText: 'Nuk ka evente për të shfaqur', + }; + + return sq; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/sr-cyrl.js b/InvenTree/InvenTree/static/fullcalendar/locales/sr-cyrl.js new file mode 100644 index 0000000000..58fecd15b6 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/sr-cyrl.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var srCyrl = { + code: 'sr-cyrl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Претходна', + next: 'следећи', + today: 'Данас', + month: 'Месец', + week: 'Недеља', + day: 'Дан', + list: 'Планер', + }, + weekText: 'Сед', + allDayText: 'Цео дан', + moreLinkText: function(n) { + return '+ још ' + n + }, + noEventsText: 'Нема догађаја за приказ', + }; + + return srCyrl; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/sr.js b/InvenTree/InvenTree/static/fullcalendar/locales/sr.js new file mode 100644 index 0000000000..a6ecda5f7f --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/sr.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var sr = { + code: 'sr', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Prethodna', + next: 'Sledeći', + today: 'Danas', + month: 'Mеsеc', + week: 'Nеdеlja', + day: 'Dan', + list: 'Planеr', + }, + weekText: 'Sed', + allDayText: 'Cеo dan', + moreLinkText: function(n) { + return '+ još ' + n + }, + noEventsText: 'Nеma događaja za prikaz', + }; + + return sr; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/sv.js b/InvenTree/InvenTree/static/fullcalendar/locales/sv.js new file mode 100644 index 0000000000..c040ecd508 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/sv.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var sv = { + code: 'sv', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Förra', + next: 'Nästa', + today: 'Idag', + month: 'Månad', + week: 'Vecka', + day: 'Dag', + list: 'Program', + }, + weekText: 'v.', + allDayText: 'Heldag', + moreLinkText: 'till', + noEventsText: 'Inga händelser att visa', + }; + + return sv; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/th.js b/InvenTree/InvenTree/static/fullcalendar/locales/th.js new file mode 100644 index 0000000000..6e817430b6 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/th.js @@ -0,0 +1,30 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var th = { + code: 'th', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'ก่อนหน้า', + next: 'ถัดไป', + prevYear: 'ปีก่อนหน้า', + nextYear: 'ปีถัดไป', + year: 'ปี', + today: 'วันนี้', + month: 'เดือน', + week: 'สัปดาห์', + day: 'วัน', + list: 'กำหนดการ', + }, + weekText: 'สัปดาห์', + allDayText: 'ตลอดวัน', + moreLinkText: 'เพิ่มเติม', + noEventsText: 'ไม่มีกิจกรรมที่จะแสดง', + }; + + return th; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/tr.js b/InvenTree/InvenTree/static/fullcalendar/locales/tr.js new file mode 100644 index 0000000000..5575f1c8f0 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/tr.js @@ -0,0 +1,27 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var tr = { + code: 'tr', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'geri', + next: 'ileri', + today: 'bugün', + month: 'Ay', + week: 'Hafta', + day: 'Gün', + list: 'Ajanda', + }, + weekText: 'Hf', + allDayText: 'Tüm gün', + moreLinkText: 'daha fazla', + noEventsText: 'Gösterilecek etkinlik yok', + }; + + return tr; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/ug.js b/InvenTree/InvenTree/static/fullcalendar/locales/ug.js new file mode 100644 index 0000000000..266d436feb --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/ug.js @@ -0,0 +1,17 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var ug = { + code: 'ug', + buttonText: { + month: 'ئاي', + week: 'ھەپتە', + day: 'كۈن', + list: 'كۈنتەرتىپ', + }, + allDayText: 'پۈتۈن كۈن', + }; + + return ug; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/uk.js b/InvenTree/InvenTree/static/fullcalendar/locales/uk.js new file mode 100644 index 0000000000..665c5f4d29 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/uk.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var uk = { + code: 'uk', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Попередній', + next: 'далі', + today: 'Сьогодні', + month: 'Місяць', + week: 'Тиждень', + day: 'День', + list: 'Порядок денний', + }, + weekText: 'Тиж', + allDayText: 'Увесь день', + moreLinkText: function(n) { + return '+ще ' + n + '...' + }, + noEventsText: 'Немає подій для відображення', + }; + + return uk; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/uz.js b/InvenTree/InvenTree/static/fullcalendar/locales/uz.js new file mode 100644 index 0000000000..84bb49923f --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/uz.js @@ -0,0 +1,21 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var uz = { + code: 'uz', + buttonText: { + month: 'Oy', + week: 'Xafta', + day: 'Kun', + list: 'Kun tartibi', + }, + allDayText: "Kun bo'yi", + moreLinkText: function(n) { + return '+ yana ' + n + }, + noEventsText: "Ko'rsatish uchun voqealar yo'q", + }; + + return uz; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/vi.js b/InvenTree/InvenTree/static/fullcalendar/locales/vi.js new file mode 100644 index 0000000000..63d58632b1 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/vi.js @@ -0,0 +1,29 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var vi = { + code: 'vi', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Trước', + next: 'Tiếp', + today: 'Hôm nay', + month: 'Tháng', + week: 'Tuần', + day: 'Ngày', + list: 'Lịch biểu', + }, + weekText: 'Tu', + allDayText: 'Cả ngày', + moreLinkText: function(n) { + return '+ thêm ' + n + }, + noEventsText: 'Không có sự kiện để hiển thị', + }; + + return vi; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/zh-cn.js b/InvenTree/InvenTree/static/fullcalendar/locales/zh-cn.js new file mode 100644 index 0000000000..c0b46e9c9f --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/zh-cn.js @@ -0,0 +1,30 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var zhCn = { + code: 'zh-cn', + week: { + // GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效 + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: '上月', + next: '下月', + today: '今天', + month: '月', + week: '周', + day: '日', + list: '日程', + }, + weekText: '周', + allDayText: '全天', + moreLinkText: function(n) { + return '另外 ' + n + ' 个' + }, + noEventsText: '没有事件显示', + }; + + return zhCn; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/locales/zh-tw.js b/InvenTree/InvenTree/static/fullcalendar/locales/zh-tw.js new file mode 100644 index 0000000000..a71cde9a31 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/locales/zh-tw.js @@ -0,0 +1,23 @@ +FullCalendar.globalLocales.push(function () { + 'use strict'; + + var zhTw = { + code: 'zh-tw', + buttonText: { + prev: '上月', + next: '下月', + today: '今天', + month: '月', + week: '週', + day: '天', + list: '活動列表', + }, + weekText: '周', + allDayText: '整天', + moreLinkText: '顯示更多', + noEventsText: '没有任何活動', + }; + + return zhTw; + +}()); diff --git a/InvenTree/InvenTree/static/fullcalendar/main.css b/InvenTree/InvenTree/static/fullcalendar/main.css new file mode 100644 index 0000000000..2b276f5288 --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/main.css @@ -0,0 +1,1429 @@ + +/* classes attached to */ + +.fc-not-allowed, +.fc-not-allowed .fc-event { /* override events' custom cursors */ + cursor: not-allowed; +} + +.fc-unselectable { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-touch-callout: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +.fc { + /* layout of immediate children */ + display: flex; + flex-direction: column; + + font-size: 1em +} +.fc, + .fc *, + .fc *:before, + .fc *:after { + box-sizing: border-box; + } +.fc table { + border-collapse: collapse; + border-spacing: 0; + font-size: 1em; /* normalize cross-browser */ + } +.fc th { + text-align: center; + } +.fc th, + .fc td { + vertical-align: top; + padding: 0; + } +.fc a[data-navlink] { + cursor: pointer; + } +.fc a[data-navlink]:hover { + text-decoration: underline; + } +.fc-direction-ltr { + direction: ltr; + text-align: left; +} +.fc-direction-rtl { + direction: rtl; + text-align: right; +} +.fc-theme-standard td, + .fc-theme-standard th { + border: 1px solid #ddd; + border: 1px solid var(--fc-border-color, #ddd); + } +/* for FF, which doesn't expand a 100% div within a table cell. use absolute positioning */ +/* inner-wrappers are responsible for being absolute */ +/* TODO: best place for this? */ +.fc-liquid-hack td, + .fc-liquid-hack th { + position: relative; + } + +@font-face { + font-family: 'fcicons'; + src: url("data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBfAAAAC8AAAAYGNtYXAXVtKNAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5ZgYydxIAAAF4AAAFNGhlYWQUJ7cIAAAGrAAAADZoaGVhB20DzAAABuQAAAAkaG10eCIABhQAAAcIAAAALGxvY2ED4AU6AAAHNAAAABhtYXhwAA8AjAAAB0wAAAAgbmFtZXsr690AAAdsAAABhnBvc3QAAwAAAAAI9AAAACAAAwPAAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADpBgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6Qb//f//AAAAAAAg6QD//f//AAH/4xcEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAWIAjQKeAskAEwAAJSc3NjQnJiIHAQYUFwEWMjc2NCcCnuLiDQ0MJAz/AA0NAQAMJAwNDcni4gwjDQwM/wANIwz/AA0NDCMNAAAAAQFiAI0CngLJABMAACUBNjQnASYiBwYUHwEHBhQXFjI3AZ4BAA0N/wAMJAwNDeLiDQ0MJAyNAQAMIw0BAAwMDSMM4uINIwwNDQAAAAIA4gC3Ax4CngATACcAACUnNzY0JyYiDwEGFB8BFjI3NjQnISc3NjQnJiIPAQYUHwEWMjc2NCcB87e3DQ0MIw3VDQ3VDSMMDQ0BK7e3DQ0MJAzVDQ3VDCQMDQ3zuLcMJAwNDdUNIwzWDAwNIwy4twwkDA0N1Q0jDNYMDA0jDAAAAgDiALcDHgKeABMAJwAAJTc2NC8BJiIHBhQfAQcGFBcWMjchNzY0LwEmIgcGFB8BBwYUFxYyNwJJ1Q0N1Q0jDA0Nt7cNDQwjDf7V1Q0N1QwkDA0Nt7cNDQwkDLfWDCMN1Q0NDCQMt7gMIw0MDNYMIw3VDQ0MJAy3uAwjDQwMAAADAFUAAAOrA1UAMwBoAHcAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMhMjY1NCYjISIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAAVYRGRkR/qoRGRkRA1UFBAUOCQkVDAsZDf2rDRkLDBUJCA4FBQUFBQUOCQgVDAsZDQJVDRkLDBUJCQ4FBAVVAgECBQMCBwQECAX9qwQJAwQHAwMFAQICAgIBBQMDBwQDCQQCVQUIBAQHAgMFAgEC/oAZEhEZGRESGQAAAAADAFUAAAOrA1UAMwBoAIkAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMzFRQWMzI2PQEzMjY1NCYrATU0JiMiBh0BIyIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAgBkSEhmAERkZEYAZEhIZgBEZGREDVQUEBQ4JCRUMCxkN/asNGQsMFQkIDgUFBQUFBQ4JCBUMCxkNAlUNGQsMFQkJDgUEBVUCAQIFAwIHBAQIBf2rBAkDBAcDAwUBAgICAgEFAwMHBAMJBAJVBQgEBAcCAwUCAQL+gIASGRkSgBkSERmAEhkZEoAZERIZAAABAOIAjQMeAskAIAAAExcHBhQXFjI/ARcWMjc2NC8BNzY0JyYiDwEnJiIHBhQX4uLiDQ0MJAzi4gwkDA0N4uINDQwkDOLiDCQMDQ0CjeLiDSMMDQ3h4Q0NDCMN4uIMIw0MDOLiDAwNIwwAAAABAAAAAQAAa5n0y18PPPUACwQAAAAAANivOVsAAAAA2K85WwAAAAADqwNVAAAACAACAAAAAAAAAAEAAAPA/8AAAAQAAAAAAAOrAAEAAAAAAAAAAAAAAAAAAAALBAAAAAAAAAAAAAAAAgAAAAQAAWIEAAFiBAAA4gQAAOIEAABVBAAAVQQAAOIAAAAAAAoAFAAeAEQAagCqAOoBngJkApoAAQAAAAsAigADAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABAAcAAAABAAAAAAACAAcAYAABAAAAAAADAAcANgABAAAAAAAEAAcAdQABAAAAAAAFAAsAFQABAAAAAAAGAAcASwABAAAAAAAKABoAigADAAEECQABAA4ABwADAAEECQACAA4AZwADAAEECQADAA4APQADAAEECQAEAA4AfAADAAEECQAFABYAIAADAAEECQAGAA4AUgADAAEECQAKADQApGZjaWNvbnMAZgBjAGkAYwBvAG4Ac1ZlcnNpb24gMS4wAFYAZQByAHMAaQBvAG4AIAAxAC4AMGZjaWNvbnMAZgBjAGkAYwBvAG4Ac2ZjaWNvbnMAZgBjAGkAYwBvAG4Ac1JlZ3VsYXIAUgBlAGcAdQBsAGEAcmZjaWNvbnMAZgBjAGkAYwBvAG4Ac0ZvbnQgZ2VuZXJhdGVkIGJ5IEljb01vb24uAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") format('truetype'); + font-weight: normal; + font-style: normal; +} + +.fc-icon { + /* added for fc */ + display: inline-block; + width: 1em; + height: 1em; + text-align: center; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'fcicons' !important; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.fc-icon-chevron-left:before { + content: "\e900"; +} + +.fc-icon-chevron-right:before { + content: "\e901"; +} + +.fc-icon-chevrons-left:before { + content: "\e902"; +} + +.fc-icon-chevrons-right:before { + content: "\e903"; +} + +.fc-icon-minus-square:before { + content: "\e904"; +} + +.fc-icon-plus-square:before { + content: "\e905"; +} + +.fc-icon-x:before { + content: "\e906"; +} +/* +Lots taken from Flatly (MIT): https://bootswatch.com/4/flatly/bootstrap.css + +These styles only apply when the standard-theme is activated. +When it's NOT activated, the fc-button classes won't even be in the DOM. +*/ +.fc { + + /* reset */ + +} +.fc .fc-button { + border-radius: 0; + overflow: visible; + text-transform: none; + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; + } +.fc .fc-button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; + } +.fc .fc-button { + -webkit-appearance: button; + } +.fc .fc-button:not(:disabled) { + cursor: pointer; + } +.fc .fc-button::-moz-focus-inner { + padding: 0; + border-style: none; + } +.fc { + + /* theme */ + +} +.fc .fc-button { + display: inline-block; + font-weight: 400; + text-align: center; + vertical-align: middle; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: transparent; + border: 1px solid transparent; + padding: 0.4em 0.65em; + font-size: 1em; + line-height: 1.5; + border-radius: 0.25em; + } +.fc .fc-button:hover { + text-decoration: none; + } +.fc .fc-button:focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(44, 62, 80, 0.25); + } +.fc .fc-button:disabled { + opacity: 0.65; + } +.fc { + + /* "primary" coloring */ + +} +.fc .fc-button-primary { + color: #fff; + color: var(--fc-button-text-color, #fff); + background-color: #2C3E50; + background-color: var(--fc-button-bg-color, #2C3E50); + border-color: #2C3E50; + border-color: var(--fc-button-border-color, #2C3E50); + } +.fc .fc-button-primary:hover { + color: #fff; + color: var(--fc-button-text-color, #fff); + background-color: #1e2b37; + background-color: var(--fc-button-hover-bg-color, #1e2b37); + border-color: #1a252f; + border-color: var(--fc-button-hover-border-color, #1a252f); + } +.fc .fc-button-primary:disabled { /* not DRY */ + color: #fff; + color: var(--fc-button-text-color, #fff); + background-color: #2C3E50; + background-color: var(--fc-button-bg-color, #2C3E50); + border-color: #2C3E50; + border-color: var(--fc-button-border-color, #2C3E50); /* overrides :hover */ + } +.fc .fc-button-primary:focus { + box-shadow: 0 0 0 0.2rem rgba(76, 91, 106, 0.5); + } +.fc .fc-button-primary:not(:disabled):active, + .fc .fc-button-primary:not(:disabled).fc-button-active { + color: #fff; + color: var(--fc-button-text-color, #fff); + background-color: #1a252f; + background-color: var(--fc-button-active-bg-color, #1a252f); + border-color: #151e27; + border-color: var(--fc-button-active-border-color, #151e27); + } +.fc .fc-button-primary:not(:disabled):active:focus, + .fc .fc-button-primary:not(:disabled).fc-button-active:focus { + box-shadow: 0 0 0 0.2rem rgba(76, 91, 106, 0.5); + } +.fc { + + /* icons within buttons */ + +} +.fc .fc-button .fc-icon { + vertical-align: middle; + font-size: 1.5em; /* bump up the size (but don't make it bigger than line-height of button, which is 1.5em also) */ + } +.fc .fc-button-group { + position: relative; + display: inline-flex; + vertical-align: middle; + } +.fc .fc-button-group > .fc-button { + position: relative; + flex: 1 1 auto; + } +.fc .fc-button-group > .fc-button:hover { + z-index: 1; + } +.fc .fc-button-group > .fc-button:focus, + .fc .fc-button-group > .fc-button:active, + .fc .fc-button-group > .fc-button.fc-button-active { + z-index: 1; + } +.fc-direction-ltr .fc-button-group > .fc-button:not(:first-child) { + margin-left: -1px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } +.fc-direction-ltr .fc-button-group > .fc-button:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } +.fc-direction-rtl .fc-button-group > .fc-button:not(:first-child) { + margin-right: -1px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } +.fc-direction-rtl .fc-button-group > .fc-button:not(:last-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } +.fc .fc-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + } +.fc .fc-toolbar.fc-header-toolbar { + margin-bottom: 1.5em; + } +.fc .fc-toolbar.fc-footer-toolbar { + margin-top: 1.5em; + } +.fc .fc-toolbar-title { + font-size: 1.75em; + margin: 0; + } +.fc-direction-ltr .fc-toolbar > * > :not(:first-child) { + margin-left: .75em; /* space between */ + } +.fc-direction-rtl .fc-toolbar > * > :not(:first-child) { + margin-right: .75em; /* space between */ + } +.fc-direction-rtl .fc-toolbar-ltr { /* when the toolbar-chunk positioning system is explicitly left-to-right */ + flex-direction: row-reverse; + } +.fc .fc-scroller { + -webkit-overflow-scrolling: touch; + position: relative; /* for abs-positioned elements within */ + } +.fc .fc-scroller-liquid { + height: 100%; + } +.fc .fc-scroller-liquid-absolute { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + } +.fc .fc-scroller-harness { + position: relative; + overflow: hidden; + direction: ltr; + /* hack for chrome computing the scroller's right/left wrong for rtl. undone below... */ + /* TODO: demonstrate in codepen */ + } +.fc .fc-scroller-harness-liquid { + height: 100%; + } +.fc-direction-rtl .fc-scroller-harness > .fc-scroller { /* undo above hack */ + direction: rtl; + } +.fc-theme-standard .fc-scrollgrid { + border: 1px solid #ddd; + border: 1px solid var(--fc-border-color, #ddd); /* bootstrap does this. match */ + } +.fc .fc-scrollgrid, + .fc .fc-scrollgrid table { /* all tables (self included) */ + width: 100%; /* because tables don't normally do this */ + table-layout: fixed; + } +.fc .fc-scrollgrid table { /* inner tables */ + border-top-style: hidden; + border-left-style: hidden; + border-right-style: hidden; + } +.fc .fc-scrollgrid { + + border-collapse: separate; + border-right-width: 0; + border-bottom-width: 0; + + } +.fc .fc-scrollgrid-liquid { + height: 100%; + } +.fc .fc-scrollgrid-section { /* a */ + height: 1px /* better than 0, for firefox */ + + } +.fc .fc-scrollgrid-section > td { + height: 1px; /* needs a height so inner div within grow. better than 0, for firefox */ + } +.fc .fc-scrollgrid-section table { + height: 1px; + /* for most browsers, if a height isn't set on the table, can't do liquid-height within cells */ + /* serves as a min-height. harmless */ + } +.fc .fc-scrollgrid-section-liquid { + height: auto + + } +.fc .fc-scrollgrid-section-liquid > td { + height: 100%; /* better than `auto`, for firefox */ + } +.fc .fc-scrollgrid-section > * { + border-top-width: 0; + border-left-width: 0; + } +.fc .fc-scrollgrid-section-header > *, + .fc .fc-scrollgrid-section-footer > * { + border-bottom-width: 0; + } +.fc .fc-scrollgrid-section-body table, + .fc .fc-scrollgrid-section-footer table { + border-bottom-style: hidden; /* head keeps its bottom border tho */ + } +.fc { + + /* stickiness */ + +} +.fc .fc-scrollgrid-section-sticky > * { + background: #fff; + background: var(--fc-page-bg-color, #fff); + position: -webkit-sticky; + position: sticky; + z-index: 2; /* TODO: var */ + /* TODO: box-shadow when sticking */ + } +.fc .fc-scrollgrid-section-header.fc-scrollgrid-section-sticky > * { + top: 0; /* because border-sharing causes a gap at the top */ + /* TODO: give safari -1. has bug */ + } +.fc .fc-scrollgrid-section-footer.fc-scrollgrid-section-sticky > * { + bottom: 0; /* known bug: bottom-stickiness doesn't work in safari */ + } +.fc .fc-scrollgrid-sticky-shim { /* for horizontal scrollbar */ + height: 1px; /* needs height to create scrollbars */ + margin-bottom: -1px; + } +.fc-sticky { /* no .fc wrap because used as child of body */ + position: -webkit-sticky; + position: sticky; +} +.fc .fc-view-harness { + flex-grow: 1; /* because this harness is WITHIN the .fc's flexbox */ + position: relative; + } +.fc { + + /* when the harness controls the height, make the view liquid */ + +} +.fc .fc-view-harness-active > .fc-view { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } +.fc .fc-col-header-cell-cushion { + display: inline-block; /* x-browser for when sticky (when multi-tier header) */ + padding: 2px 4px; + } +.fc .fc-bg-event, + .fc .fc-non-business, + .fc .fc-highlight { + /* will always have a harness with position:relative/absolute, so absolutely expand */ + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } +.fc .fc-non-business { + background: rgba(215, 215, 215, 0.3); + background: var(--fc-non-business-color, rgba(215, 215, 215, 0.3)); + } +.fc .fc-bg-event { + background: rgb(143, 223, 130); + background: var(--fc-bg-event-color, rgb(143, 223, 130)); + opacity: 0.3; + opacity: var(--fc-bg-event-opacity, 0.3) + } +.fc .fc-bg-event .fc-event-title { + margin: .5em; + font-size: .85em; + font-size: var(--fc-small-font-size, .85em); + font-style: italic; + } +.fc .fc-highlight { + background: rgba(188, 232, 241, 0.3); + background: var(--fc-highlight-color, rgba(188, 232, 241, 0.3)); + } +.fc .fc-cell-shaded, + .fc .fc-day-disabled { + background: rgba(208, 208, 208, 0.3); + background: var(--fc-neutral-bg-color, rgba(208, 208, 208, 0.3)); + } +/* link resets */ +/* ---------------------------------------------------------------------------------------------------- */ +a.fc-event, +a.fc-event:hover { + text-decoration: none; +} +/* cursor */ +.fc-event[href], +.fc-event.fc-event-draggable { + cursor: pointer; +} +/* event text content */ +/* ---------------------------------------------------------------------------------------------------- */ +.fc-event .fc-event-main { + position: relative; + z-index: 2; + } +/* dragging */ +/* ---------------------------------------------------------------------------------------------------- */ +.fc-event-dragging:not(.fc-event-selected) { /* MOUSE */ + opacity: 0.75; + } +.fc-event-dragging.fc-event-selected { /* TOUCH */ + box-shadow: 0 2px 7px rgba(0, 0, 0, 0.3); + } +/* resizing */ +/* ---------------------------------------------------------------------------------------------------- */ +/* (subclasses should hone positioning for touch and non-touch) */ +.fc-event .fc-event-resizer { + display: none; + position: absolute; + z-index: 4; + } +.fc-event:hover, /* MOUSE */ +.fc-event-selected { /* TOUCH */ + +} +.fc-event:hover .fc-event-resizer, .fc-event-selected .fc-event-resizer { + display: block; + } +.fc-event-selected .fc-event-resizer { + border-radius: 4px; + border-radius: calc(var(--fc-event-resizer-dot-total-width, 8px) / 2); + border-width: 1px; + border-width: var(--fc-event-resizer-dot-border-width, 1px); + width: 8px; + width: var(--fc-event-resizer-dot-total-width, 8px); + height: 8px; + height: var(--fc-event-resizer-dot-total-width, 8px); + border-style: solid; + border-color: inherit; + background: #fff; + background: var(--fc-page-bg-color, #fff) + + /* expand hit area */ + + } +.fc-event-selected .fc-event-resizer:before { + content: ''; + position: absolute; + top: -20px; + left: -20px; + right: -20px; + bottom: -20px; + } +/* selecting (always TOUCH) */ +/* ---------------------------------------------------------------------------------------------------- */ +.fc-event-selected { + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) + + /* expand hit area (subclasses should expand) */ + +} +.fc-event-selected:before { + content: ""; + position: absolute; + z-index: 3; + top: 0; + left: 0; + right: 0; + bottom: 0; + } +.fc-event-selected { + + /* dimmer effect */ + +} +.fc-event-selected:after { + content: ""; + background: rgba(0, 0, 0, 0.25); + background: var(--fc-event-selected-overlay-color, rgba(0, 0, 0, 0.25)); + position: absolute; + z-index: 1; + + /* assume there's a border on all sides. overcome it. */ + /* sometimes there's NOT a border, in which case the dimmer will go over */ + /* an adjacent border, which looks fine. */ + top: -1px; + left: -1px; + right: -1px; + bottom: -1px; + } +/* +A HORIZONTAL event +*/ +.fc-h-event { /* allowed to be top-level */ + display: block; + border: 1px solid #3788d8; + border: 1px solid var(--fc-event-border-color, #3788d8); + background-color: #3788d8; + background-color: var(--fc-event-bg-color, #3788d8) + +} +.fc-h-event .fc-event-main { + color: #fff; + color: var(--fc-event-text-color, #fff); + } +.fc-h-event .fc-event-main-frame { + display: flex; /* for make fc-event-title-container expand */ + } +.fc-h-event .fc-event-time { + max-width: 100%; /* clip overflow on this element */ + overflow: hidden; + } +.fc-h-event .fc-event-title-container { /* serves as a container for the sticky cushion */ + flex-grow: 1; + flex-shrink: 1; + min-width: 0; /* important for allowing to shrink all the way */ + } +.fc-h-event .fc-event-title { + display: inline-block; /* need this to be sticky cross-browser */ + vertical-align: top; /* for not messing up line-height */ + left: 0; /* for sticky */ + right: 0; /* for sticky */ + max-width: 100%; /* clip overflow on this element */ + overflow: hidden; + } +.fc-h-event.fc-event-selected:before { + /* expand hit area */ + top: -10px; + bottom: -10px; + } +/* adjust border and border-radius (if there is any) for non-start/end */ +.fc-direction-ltr .fc-daygrid-block-event:not(.fc-event-start), +.fc-direction-rtl .fc-daygrid-block-event:not(.fc-event-end) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-left-width: 0; +} +.fc-direction-ltr .fc-daygrid-block-event:not(.fc-event-end), +.fc-direction-rtl .fc-daygrid-block-event:not(.fc-event-start) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right-width: 0; +} +/* resizers */ +.fc-h-event:not(.fc-event-selected) .fc-event-resizer { + top: 0; + bottom: 0; + width: 8px; + width: var(--fc-event-resizer-thickness, 8px); +} +.fc-direction-ltr .fc-h-event:not(.fc-event-selected) .fc-event-resizer-start, +.fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-end { + cursor: w-resize; + left: -4px; + left: calc(var(--fc-event-resizer-thickness, 8px) / -2); +} +.fc-direction-ltr .fc-h-event:not(.fc-event-selected) .fc-event-resizer-end, +.fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-start { + cursor: e-resize; + right: -4px; + right: calc(var(--fc-event-resizer-thickness, 8px) / -2); +} +/* resizers for TOUCH */ +.fc-h-event.fc-event-selected .fc-event-resizer { + top: 50%; + margin-top: -4px; + margin-top: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2); +} +.fc-direction-ltr .fc-h-event.fc-event-selected .fc-event-resizer-start, +.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-end { + left: -4px; + left: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2); +} +.fc-direction-ltr .fc-h-event.fc-event-selected .fc-event-resizer-end, +.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-start { + right: -4px; + right: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2); +} + + +:root { + --fc-daygrid-event-dot-width: 8px; +} +.fc .fc-popover { + position: fixed; + top: 0; /* for when not positioned yet */ + box-shadow: 0 2px 6px rgba(0,0,0,.15); + } +.fc .fc-popover-header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 3px 4px; + } +.fc .fc-popover-title { + margin: 0 2px; + } +.fc .fc-popover-close { + cursor: pointer; + opacity: 0.65; + font-size: 1.1em; + } +.fc-theme-standard .fc-popover { + border: 1px solid #ddd; + border: 1px solid var(--fc-border-color, #ddd); + background: #fff; + background: var(--fc-page-bg-color, #fff); + } +.fc-theme-standard .fc-popover-header { + background: rgba(208, 208, 208, 0.3); + background: var(--fc-neutral-bg-color, rgba(208, 208, 208, 0.3)); + } +/* help things clear margins of inner content */ +.fc-daygrid-day-frame, +.fc-daygrid-day-events, +.fc-daygrid-event-harness { /* for event top/bottom margins */ +} +.fc-daygrid-day-frame:before, .fc-daygrid-day-events:before, .fc-daygrid-event-harness:before { + content: ""; + clear: both; + display: table; } +.fc-daygrid-day-frame:after, .fc-daygrid-day-events:after, .fc-daygrid-event-harness:after { + content: ""; + clear: both; + display: table; } +.fc .fc-daygrid-body { /* a
that wraps the table */ + position: relative; + z-index: 1; /* container inner z-index's because s can't do it */ + } +.fc .fc-daygrid-day.fc-day-today { + background-color: rgba(255, 220, 40, 0.15); + background-color: var(--fc-today-bg-color, rgba(255, 220, 40, 0.15)); + } +.fc .fc-daygrid-day-frame { + position: relative; + min-height: 100%; /* seems to work better than `height` because sets height after rows/cells naturally do it */ + } +.fc { + + /* cell top */ + +} +.fc .fc-daygrid-day-top { + display: flex; + flex-direction: row-reverse; + } +.fc .fc-day-other .fc-daygrid-day-top { + opacity: 0.3; + } +.fc { + + /* day number (within cell top) */ + +} +.fc .fc-daygrid-day-number { + position: relative; + z-index: 4; + padding: 4px; + } +.fc { + + /* event container */ + +} +.fc .fc-daygrid-day-events { + margin-top: 1px; /* needs to be margin, not padding, so that available cell height can be computed */ + } +.fc { + + /* positioning for balanced vs natural */ + +} +.fc .fc-daygrid-body-balanced .fc-daygrid-day-events { + position: absolute; + left: 0; + right: 0; + } +.fc .fc-daygrid-body-unbalanced .fc-daygrid-day-events { + position: relative; /* for containing abs positioned event harnesses */ + min-height: 2em; /* in addition to being a min-height during natural height, equalizes the heights a little bit */ + } +.fc .fc-daygrid-body-natural { /* can coexist with -unbalanced */ + } +.fc .fc-daygrid-body-natural .fc-daygrid-day-events { + margin-bottom: 1em; + } +.fc { + + /* event harness */ + +} +.fc .fc-daygrid-event-harness { + position: relative; + } +.fc .fc-daygrid-event-harness-abs { + position: absolute; + top: 0; /* fallback coords for when cannot yet be computed */ + left: 0; /* */ + right: 0; /* */ + } +.fc .fc-daygrid-bg-harness { + position: absolute; + top: 0; + bottom: 0; + } +.fc { + + /* bg content */ + +} +.fc .fc-daygrid-day-bg .fc-non-business { z-index: 1 } +.fc .fc-daygrid-day-bg .fc-bg-event { z-index: 2 } +.fc .fc-daygrid-day-bg .fc-highlight { z-index: 3 } +.fc { + + /* events */ + +} +.fc .fc-daygrid-event { + z-index: 6; + margin-top: 1px; + } +.fc .fc-daygrid-event.fc-event-mirror { + z-index: 7; + } +.fc { + + /* cell bottom (within day-events) */ + +} +.fc .fc-daygrid-day-bottom { + font-size: .85em; + margin: 2px 3px 0; + } +.fc .fc-daygrid-more-link { + position: relative; + z-index: 4; + cursor: pointer; + } +.fc { + + /* week number (within frame) */ + +} +.fc .fc-daygrid-week-number { + position: absolute; + z-index: 5; + top: 0; + padding: 2px; + min-width: 1.5em; + text-align: center; + background-color: rgba(208, 208, 208, 0.3); + background-color: var(--fc-neutral-bg-color, rgba(208, 208, 208, 0.3)); + color: #808080; + color: var(--fc-neutral-text-color, #808080); + } +.fc { + + /* popover */ + +} +.fc .fc-more-popover { + z-index: 8; + } +.fc .fc-more-popover .fc-popover-body { + min-width: 220px; + padding: 10px; + } +.fc-direction-ltr .fc-daygrid-event.fc-event-start, +.fc-direction-rtl .fc-daygrid-event.fc-event-end { + margin-left: 2px; +} +.fc-direction-ltr .fc-daygrid-event.fc-event-end, +.fc-direction-rtl .fc-daygrid-event.fc-event-start { + margin-right: 2px; +} +.fc-direction-ltr .fc-daygrid-week-number { + left: 0; + border-radius: 0 0 3px 0; + } +.fc-direction-rtl .fc-daygrid-week-number { + right: 0; + border-radius: 0 0 0 3px; + } +.fc-liquid-hack .fc-daygrid-day-frame { + position: static; /* will cause inner absolute stuff to expand to */ + } +.fc-daygrid-event { /* make root-level, because will be dragged-and-dropped outside of a component root */ + position: relative; /* for z-indexes assigned later */ + white-space: nowrap; + border-radius: 3px; /* dot event needs this to when selected */ + font-size: .85em; + font-size: var(--fc-small-font-size, .85em); +} +/* --- the rectangle ("block") style of event --- */ +.fc-daygrid-block-event .fc-event-time { + font-weight: bold; + } +.fc-daygrid-block-event .fc-event-time, + .fc-daygrid-block-event .fc-event-title { + padding: 1px; + } +/* --- the dot style of event --- */ +.fc-daygrid-dot-event { + display: flex; + align-items: center; + padding: 2px 0 + +} +.fc-daygrid-dot-event .fc-event-title { + flex-grow: 1; + flex-shrink: 1; + min-width: 0; /* important for allowing to shrink all the way */ + overflow: hidden; + font-weight: bold; + } +.fc-daygrid-dot-event:hover, + .fc-daygrid-dot-event.fc-event-mirror { + background: rgba(0, 0, 0, 0.1); + } +.fc-daygrid-dot-event.fc-event-selected:before { + /* expand hit area */ + top: -10px; + bottom: -10px; + } +.fc-daygrid-event-dot { /* the actual dot */ + margin: 0 4px; + box-sizing: content-box; + width: 0; + height: 0; + border: 4px solid #3788d8; + border: calc(var(--fc-daygrid-event-dot-width, 8px) / 2) solid var(--fc-event-border-color, #3788d8); + border-radius: 4px; + border-radius: calc(var(--fc-daygrid-event-dot-width, 8px) / 2); +} +/* --- spacing between time and title --- */ +.fc-direction-ltr .fc-daygrid-event .fc-event-time { + margin-right: 3px; + } +.fc-direction-rtl .fc-daygrid-event .fc-event-time { + margin-left: 3px; + } + + +/* +A VERTICAL event +*/ + +.fc-v-event { /* allowed to be top-level */ + display: block; + border: 1px solid #3788d8; + border: 1px solid var(--fc-event-border-color, #3788d8); + background-color: #3788d8; + background-color: var(--fc-event-bg-color, #3788d8) + +} + +.fc-v-event .fc-event-main { + color: #fff; + color: var(--fc-event-text-color, #fff); + height: 100%; + } + +.fc-v-event .fc-event-main-frame { + height: 100%; + display: flex; + flex-direction: column; + } + +.fc-v-event .fc-event-time { + flex-grow: 0; + flex-shrink: 0; + max-height: 100%; + overflow: hidden; + } + +.fc-v-event .fc-event-title-container { /* a container for the sticky cushion */ + flex-grow: 1; + flex-shrink: 1; + min-height: 0; /* important for allowing to shrink all the way */ + } + +.fc-v-event .fc-event-title { /* will have fc-sticky on it */ + top: 0; + bottom: 0; + max-height: 100%; /* clip overflow */ + overflow: hidden; + } + +.fc-v-event:not(.fc-event-start) { + border-top-width: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + +.fc-v-event:not(.fc-event-end) { + border-bottom-width: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + +.fc-v-event.fc-event-selected:before { + /* expand hit area */ + left: -10px; + right: -10px; + } + +.fc-v-event { + + /* resizer (mouse AND touch) */ + +} + +.fc-v-event .fc-event-resizer-start { + cursor: n-resize; + } + +.fc-v-event .fc-event-resizer-end { + cursor: s-resize; + } + +.fc-v-event { + + /* resizer for MOUSE */ + +} + +.fc-v-event:not(.fc-event-selected) .fc-event-resizer { + height: 8px; + height: var(--fc-event-resizer-thickness, 8px); + left: 0; + right: 0; + } + +.fc-v-event:not(.fc-event-selected) .fc-event-resizer-start { + top: -4px; + top: calc(var(--fc-event-resizer-thickness, 8px) / -2); + } + +.fc-v-event:not(.fc-event-selected) .fc-event-resizer-end { + bottom: -4px; + bottom: calc(var(--fc-event-resizer-thickness, 8px) / -2); + } + +.fc-v-event { + + /* resizer for TOUCH (when event is "selected") */ + +} + +.fc-v-event.fc-event-selected .fc-event-resizer { + left: 50%; + margin-left: -4px; + margin-left: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2); + } + +.fc-v-event.fc-event-selected .fc-event-resizer-start { + top: -4px; + top: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2); + } + +.fc-v-event.fc-event-selected .fc-event-resizer-end { + bottom: -4px; + bottom: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2); + } +.fc .fc-timegrid .fc-daygrid-body { /* the all-day daygrid within the timegrid view */ + z-index: 2; /* put above the timegrid-body so that more-popover is above everything. TODO: better solution */ + } +.fc .fc-timegrid-divider { + padding: 0 0 2px; /* browsers get confused when you set height. use padding instead */ + } +.fc .fc-timegrid-body { + position: relative; + z-index: 1; /* scope the z-indexes of slots and cols */ + min-height: 100%; /* fill height always, even when slat table doesn't grow */ + } +.fc .fc-timegrid-axis-chunk { /* for advanced ScrollGrid */ + position: relative /* offset parent for now-indicator-container */ + + } +.fc .fc-timegrid-axis-chunk > table { + position: relative; + z-index: 1; /* above the now-indicator-container */ + } +.fc .fc-timegrid-slots { + position: relative; + z-index: 1; + } +.fc .fc-timegrid-slot { /* a */ + height: 1.5em; + border-bottom: 0 /* each cell owns its top border */ + } +.fc .fc-timegrid-slot:empty:before { + content: '\00a0'; /* make sure there's at least an empty space to create height for height syncing */ + } +.fc .fc-timegrid-slot-minor { + border-top-style: dotted; + } +.fc .fc-timegrid-slot-label-cushion { + display: inline-block; + white-space: nowrap; + } +.fc .fc-timegrid-slot-label { + vertical-align: middle; /* vertical align the slots */ + } +.fc { + + + /* slots AND axis cells (top-left corner of view including the "all-day" text) */ + +} +.fc .fc-timegrid-axis-cushion, + .fc .fc-timegrid-slot-label-cushion { + padding: 0 4px; + } +.fc { + + + /* axis cells (top-left corner of view including the "all-day" text) */ + /* vertical align is more complicated, uses flexbox */ + +} +.fc .fc-timegrid-axis-frame-liquid { + height: 100%; /* will need liquid-hack in FF */ + } +.fc .fc-timegrid-axis-frame { + overflow: hidden; + display: flex; + align-items: center; /* vertical align */ + justify-content: flex-end; /* horizontal align. matches text-align below */ + } +.fc .fc-timegrid-axis-cushion { + max-width: 60px; /* limits the width of the "all-day" text */ + flex-shrink: 0; /* allows text to expand how it normally would, regardless of constrained width */ + } +.fc-direction-ltr .fc-timegrid-slot-label-frame { + text-align: right; + } +.fc-direction-rtl .fc-timegrid-slot-label-frame { + text-align: left; + } +.fc-liquid-hack .fc-timegrid-axis-frame-liquid { + height: auto; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } +.fc .fc-timegrid-col.fc-day-today { + background-color: rgba(255, 220, 40, 0.15); + background-color: var(--fc-today-bg-color, rgba(255, 220, 40, 0.15)); + } +.fc .fc-timegrid-col-frame { + min-height: 100%; /* liquid-hack is below */ + position: relative; + } +.fc-liquid-hack .fc-timegrid-col-frame { + height: auto; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } +.fc-media-screen .fc-timegrid-cols { + position: absolute; /* no z-index. children will decide and go above slots */ + top: 0; + left: 0; + right: 0; + bottom: 0 + } +.fc-media-screen .fc-timegrid-cols > table { + height: 100%; + } +.fc-media-screen .fc-timegrid-col-bg, + .fc-media-screen .fc-timegrid-col-events, + .fc-media-screen .fc-timegrid-now-indicator-container { + position: absolute; + top: 0; + left: 0; + right: 0; + } +.fc-media-screen .fc-timegrid-event-harness { + position: absolute; /* top/left/right/bottom will all be set by JS */ + } +.fc { + + /* bg */ + +} +.fc .fc-timegrid-col-bg { + z-index: 2; /* TODO: kill */ + } +.fc .fc-timegrid-col-bg .fc-non-business { z-index: 1 } +.fc .fc-timegrid-col-bg .fc-bg-event { z-index: 2 } +.fc .fc-timegrid-col-bg .fc-highlight { z-index: 3 } +.fc .fc-timegrid-bg-harness { + position: absolute; /* top/bottom will be set by JS */ + left: 0; + right: 0; + } +.fc { + + /* fg events */ + /* (the mirror segs are put into a separate container with same classname, */ + /* and they must be after the normal seg container to appear at a higher z-index) */ + +} +.fc .fc-timegrid-col-events { + z-index: 3; + /* child event segs have z-indexes that are scoped within this div */ + } +.fc { + + /* now indicator */ + +} +.fc .fc-timegrid-now-indicator-container { + bottom: 0; + overflow: hidden; /* don't let overflow of lines/arrows cause unnecessary scrolling */ + /* z-index is set on the individual elements */ + } +.fc-direction-ltr .fc-timegrid-col-events { + margin: 0 2.5% 0 2px; + } +.fc-direction-rtl .fc-timegrid-col-events { + margin: 0 2px 0 2.5%; + } +.fc-timegrid-event-harness-inset .fc-timegrid-event, +.fc-timegrid-event.fc-event-mirror { + box-shadow: 0px 0px 0px 1px #fff; + box-shadow: 0px 0px 0px 1px var(--fc-page-bg-color, #fff); +} +.fc-timegrid-event { /* events need to be root */ + + font-size: .85em; + + font-size: var(--fc-small-font-size, .85em); + border-radius: 3px + +} +.fc-timegrid-event .fc-event-main { + padding: 1px 1px 0; + } +.fc-timegrid-event .fc-event-time { + white-space: nowrap; + font-size: .85em; + font-size: var(--fc-small-font-size, .85em); + margin-bottom: 1px; + } +.fc-timegrid-event-condensed .fc-event-main-frame { + flex-direction: row; + overflow: hidden; + } +.fc-timegrid-event-condensed .fc-event-time:after { + content: '\00a0-\00a0'; /* dash surrounded by non-breaking spaces */ + } +.fc-timegrid-event-condensed .fc-event-title { + font-size: .85em; + font-size: var(--fc-small-font-size, .85em) + } +.fc-media-screen .fc-timegrid-event { + position: absolute; /* absolute WITHIN the harness */ + top: 0; + bottom: 1px; /* stay away from bottom slot line */ + left: 0; + right: 0; + } +.fc { + + /* line */ + +} +.fc .fc-timegrid-now-indicator-line { + position: absolute; + z-index: 4; + left: 0; + right: 0; + border-style: solid; + border-color: red; + border-color: var(--fc-now-indicator-color, red); + border-width: 1px 0 0; + } +.fc { + + /* arrow */ + +} +.fc .fc-timegrid-now-indicator-arrow { + position: absolute; + z-index: 4; + margin-top: -5px; /* vertically center on top coordinate */ + border-style: solid; + border-color: red; + border-color: var(--fc-now-indicator-color, red); + } +.fc-direction-ltr .fc-timegrid-now-indicator-arrow { + left: 0; + + /* triangle pointing right. TODO: mixin */ + border-width: 5px 0 5px 6px; + border-top-color: transparent; + border-bottom-color: transparent; + } +.fc-direction-rtl .fc-timegrid-now-indicator-arrow { + right: 0; + + /* triangle pointing left. TODO: mixin */ + border-width: 5px 6px 5px 0; + border-top-color: transparent; + border-bottom-color: transparent; + } + + +:root { + --fc-list-event-dot-width: 10px; + --fc-list-event-hover-bg-color: #f5f5f5; +} +.fc-theme-standard .fc-list { + border: 1px solid #ddd; + border: 1px solid var(--fc-border-color, #ddd); + } +.fc { + + /* message when no events */ + +} +.fc .fc-list-empty { + background-color: rgba(208, 208, 208, 0.3); + background-color: var(--fc-neutral-bg-color, rgba(208, 208, 208, 0.3)); + height: 100%; + display: flex; + justify-content: center; + align-items: center; /* vertically aligns fc-list-empty-inner */ + } +.fc .fc-list-empty-cushion { + margin: 5em 0; + } +.fc { + + /* table within the scroller */ + /* ---------------------------------------------------------------------------------------------------- */ + +} +.fc .fc-list-table { + width: 100%; + border-style: hidden; /* kill outer border on theme */ + } +.fc .fc-list-table tr > * { + border-left: 0; + border-right: 0; + } +.fc .fc-list-sticky .fc-list-day > * { /* the cells */ + position: -webkit-sticky; + position: sticky; + top: 0; + background: #fff; + background: var(--fc-page-bg-color, #fff); /* for when headers are styled to be transparent and sticky */ + } +.fc .fc-list-table th { + padding: 0; /* uses an inner-wrapper instead... */ + } +.fc .fc-list-table td, + .fc .fc-list-day-cushion { + padding: 8px 14px; + } +.fc { + + + /* date heading rows */ + /* ---------------------------------------------------------------------------------------------------- */ + +} +.fc .fc-list-day-cushion:after { + content: ""; + clear: both; + display: table; /* clear floating */ + } +.fc-theme-standard .fc-list-day-cushion { + background-color: rgba(208, 208, 208, 0.3); + background-color: var(--fc-neutral-bg-color, rgba(208, 208, 208, 0.3)); + } +.fc-direction-ltr .fc-list-day-text, +.fc-direction-rtl .fc-list-day-side-text { + float: left; +} +.fc-direction-ltr .fc-list-day-side-text, +.fc-direction-rtl .fc-list-day-text { + float: right; +} +/* make the dot closer to the event title */ +.fc-direction-ltr .fc-list-table .fc-list-event-graphic { padding-right: 0 } +.fc-direction-rtl .fc-list-table .fc-list-event-graphic { padding-left: 0 } +.fc .fc-list-event.fc-event-forced-url { + cursor: pointer; /* whole row will seem clickable */ + } +.fc .fc-list-event:hover td { + background-color: #f5f5f5; + background-color: var(--fc-list-event-hover-bg-color, #f5f5f5); + } +.fc { + + /* shrink certain cols */ + +} +.fc .fc-list-event-graphic, + .fc .fc-list-event-time { + white-space: nowrap; + width: 1px; + } +.fc .fc-list-event-dot { + display: inline-block; + box-sizing: content-box; + width: 0; + height: 0; + border: 5px solid #3788d8; + border: calc(var(--fc-list-event-dot-width, 10px) / 2) solid var(--fc-event-border-color, #3788d8); + border-radius: 5px; + border-radius: calc(var(--fc-list-event-dot-width, 10px) / 2); + } +.fc { + + /* reset styling */ + +} +.fc .fc-list-event-title a { + color: inherit; + text-decoration: none; + } +.fc { + + /* underline link when hovering over any part of row */ + +} +.fc .fc-list-event.fc-event-forced-url:hover a { + text-decoration: underline; + } + + + + .fc-theme-bootstrap a:not([href]) { + color: inherit; /* natural color for navlinks */ + } + diff --git a/InvenTree/InvenTree/static/fullcalendar/main.js b/InvenTree/InvenTree/static/fullcalendar/main.js new file mode 100644 index 0000000000..f173a6e89d --- /dev/null +++ b/InvenTree/InvenTree/static/fullcalendar/main.js @@ -0,0 +1,14322 @@ +/*! +FullCalendar v5.5.0 +Docs & License: https://fullcalendar.io/ +(c) 2020 Adam Shaw +*/ +var FullCalendar = (function (exports) { + 'use strict'; + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + /* global Reflect, Promise */ + + var extendStatics = function(d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + + function __extends(d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + } + + var __assign = function() { + __assign = Object.assign || function __assign(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); + }; + + function __spreadArrays() { + for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; + for (var r = Array(s), k = 0, i = 0; i < il; i++) + for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) + r[k] = a[j]; + return r; + } + + var n,u,i,t,o,r,f={},e=[],c=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function s(n,l){for(var u in l)n[u]=l[u];return n}function a(n){var l=n.parentNode;l&&l.removeChild(n);}function v(n,l,u){var i,t,o,r=arguments,f={};for(o in l)"key"==o?i=l[o]:"ref"==o?t=l[o]:f[o]=l[o];if(arguments.length>3)for(u=[u],o=3;o1&&T(t,l,u),l=x(u,t,t,n.__k,null,t.__e,l),"function"==typeof n.type&&(n.__d=l)));}function $(l,u,i,t,o,r,f,e,c){var a,v,h,y,_,w,k,g,b,x,A,P=u.type;if(void 0!==u.constructor)return null;null!=i.__h&&(c=i.__h,e=u.__e=i.__e,u.__h=null,r=[e]),(a=n.__b)&&a(u);try{n:if("function"==typeof P){if(g=u.props,b=(a=P.contextType)&&t[a.__c],x=a?b?b.props.value:a.__:t,i.__c?k=(v=u.__c=i.__c).__=v.__E:("prototype"in P&&P.prototype.render?u.__c=v=new P(g,x):(u.__c=v=new d(g,x),v.constructor=P,v.render=M),b&&b.sub(v),v.props=g,v.state||(v.state={}),v.context=x,v.__n=t,h=v.__d=!0,v.__h=[]),null==v.__s&&(v.__s=v.state),null!=P.getDerivedStateFromProps&&(v.__s==v.state&&(v.__s=s({},v.__s)),s(v.__s,P.getDerivedStateFromProps(g,v.__s))),y=v.props,_=v.state,h)null==P.getDerivedStateFromProps&&null!=v.componentWillMount&&v.componentWillMount(),null!=v.componentDidMount&&v.__h.push(v.componentDidMount);else {if(null==P.getDerivedStateFromProps&&g!==y&&null!=v.componentWillReceiveProps&&v.componentWillReceiveProps(g,x),!v.__e&&null!=v.shouldComponentUpdate&&!1===v.shouldComponentUpdate(g,v.__s,x)||u.__v===i.__v){v.props=g,v.state=v.__s,u.__v!==i.__v&&(v.__d=!1),v.__v=u,u.__e=i.__e,u.__k=i.__k,v.__h.length&&f.push(v),T(u,e,l);break n}null!=v.componentWillUpdate&&v.componentWillUpdate(g,v.__s,x),null!=v.componentDidUpdate&&v.__h.push(function(){v.componentDidUpdate(y,_,w);});}v.context=x,v.props=g,v.state=v.__s,(a=n.__r)&&a(u),v.__d=!1,v.__v=u,v.__P=l,a=v.render(v.props,v.state,v.context),v.state=v.__s,null!=v.getChildContext&&(t=s(s({},t),v.getChildContext())),h||null==v.getSnapshotBeforeUpdate||(w=v.getSnapshotBeforeUpdate(y,_)),A=null!=a&&a.type==p&&null==a.key?a.props.children:a,m(l,Array.isArray(A)?A:[A],u,i,t,o,r,f,e,c),v.base=u.__e,u.__h=null,v.__h.length&&f.push(v),k&&(v.__E=v.__=null),v.__e=!1;}else null==r&&u.__v===i.__v?(u.__k=i.__k,u.__e=i.__e):u.__e=H(i.__e,u,i,t,o,r,f,c);(a=n.diffed)&&a(u);}catch(l){u.__v=null,(c||null!=r)&&(u.__e=e,u.__h=!!c,r[r.indexOf(e)]=null),n.__e(l,u,i);}return u.__e}function j(l,u){n.__c&&n.__c(u,l),l.some(function(u){try{l=u.__h,u.__h=[],l.some(function(n){n.call(u);});}catch(l){n.__e(l,u.__v);}});}function H(n,l,u,i,t,o,r,c){var s,a,v,h,y,p=u.props,d=l.props;if(t="svg"===l.type||t,null!=o)for(s=0;s= 1) { + return Math.min(w, nextW); + } + return w; + } + function weekOfGivenYear(marker, year, dow, doy) { + var firstWeekStart = arrayToUtcDate([year, 0, 1 + firstWeekOffset(year, dow, doy)]); + var dayStart = startOfDay(marker); + var days = Math.round(diffDays(firstWeekStart, dayStart)); + return Math.floor(days / 7) + 1; // zero-indexed + } + // start-of-first-week - start-of-year + function firstWeekOffset(year, dow, doy) { + // first-week day -- which january is always in the first week (4 for iso, 1 for other) + var fwd = 7 + dow - doy; + // first-week day local weekday -- which local weekday is fwd + var fwdlw = (7 + arrayToUtcDate([year, 0, fwd]).getUTCDay() - dow) % 7; + return -fwdlw + fwd - 1; + } + // Array Conversion + function dateToLocalArray(date) { + return [ + date.getFullYear(), + date.getMonth(), + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds(), + date.getMilliseconds(), + ]; + } + function arrayToLocalDate(a) { + return new Date(a[0], a[1] || 0, a[2] == null ? 1 : a[2], // day of month + a[3] || 0, a[4] || 0, a[5] || 0); + } + function dateToUtcArray(date) { + return [ + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate(), + date.getUTCHours(), + date.getUTCMinutes(), + date.getUTCSeconds(), + date.getUTCMilliseconds(), + ]; + } + function arrayToUtcDate(a) { + // according to web standards (and Safari), a month index is required. + // massage if only given a year. + if (a.length === 1) { + a = a.concat([0]); + } + return new Date(Date.UTC.apply(Date, a)); + } + // Other Utils + function isValidDate(m) { + return !isNaN(m.valueOf()); + } + function timeAsMs(m) { + return m.getUTCHours() * 1000 * 60 * 60 + + m.getUTCMinutes() * 1000 * 60 + + m.getUTCSeconds() * 1000 + + m.getUTCMilliseconds(); + } + + function createEventInstance(defId, range, forcedStartTzo, forcedEndTzo) { + return { + instanceId: guid(), + defId: defId, + range: range, + forcedStartTzo: forcedStartTzo == null ? null : forcedStartTzo, + forcedEndTzo: forcedEndTzo == null ? null : forcedEndTzo, + }; + } + + var hasOwnProperty = Object.prototype.hasOwnProperty; + // Merges an array of objects into a single object. + // The second argument allows for an array of property names who's object values will be merged together. + function mergeProps(propObjs, complexPropsMap) { + var dest = {}; + if (complexPropsMap) { + for (var name_1 in complexPropsMap) { + var complexObjs = []; + // collect the trailing object values, stopping when a non-object is discovered + for (var i = propObjs.length - 1; i >= 0; i -= 1) { + var val = propObjs[i][name_1]; + if (typeof val === 'object' && val) { // non-null object + complexObjs.unshift(val); + } + else if (val !== undefined) { + dest[name_1] = val; // if there were no objects, this value will be used + break; + } + } + // if the trailing values were objects, use the merged value + if (complexObjs.length) { + dest[name_1] = mergeProps(complexObjs); + } + } + } + // copy values into the destination, going from last to first + for (var i = propObjs.length - 1; i >= 0; i -= 1) { + var props = propObjs[i]; + for (var name_2 in props) { + if (!(name_2 in dest)) { // if already assigned by previous props or complex props, don't reassign + dest[name_2] = props[name_2]; + } + } + } + return dest; + } + function filterHash(hash, func) { + var filtered = {}; + for (var key in hash) { + if (func(hash[key], key)) { + filtered[key] = hash[key]; + } + } + return filtered; + } + function mapHash(hash, func) { + var newHash = {}; + for (var key in hash) { + newHash[key] = func(hash[key], key); + } + return newHash; + } + function arrayToHash(a) { + var hash = {}; + for (var _i = 0, a_1 = a; _i < a_1.length; _i++) { + var item = a_1[_i]; + hash[item] = true; + } + return hash; + } + function buildHashFromArray(a, func) { + var hash = {}; + for (var i = 0; i < a.length; i += 1) { + var tuple = func(a[i], i); + hash[tuple[0]] = tuple[1]; + } + return hash; + } + function hashValuesToArray(obj) { + var a = []; + for (var key in obj) { + a.push(obj[key]); + } + return a; + } + function isPropsEqual(obj0, obj1) { + if (obj0 === obj1) { + return true; + } + for (var key in obj0) { + if (hasOwnProperty.call(obj0, key)) { + if (!(key in obj1)) { + return false; + } + } + } + for (var key in obj1) { + if (hasOwnProperty.call(obj1, key)) { + if (obj0[key] !== obj1[key]) { + return false; + } + } + } + return true; + } + function getUnequalProps(obj0, obj1) { + var keys = []; + for (var key in obj0) { + if (hasOwnProperty.call(obj0, key)) { + if (!(key in obj1)) { + keys.push(key); + } + } + } + for (var key in obj1) { + if (hasOwnProperty.call(obj1, key)) { + if (obj0[key] !== obj1[key]) { + keys.push(key); + } + } + } + return keys; + } + function compareObjs(oldProps, newProps, equalityFuncs) { + if (equalityFuncs === void 0) { equalityFuncs = {}; } + if (oldProps === newProps) { + return true; + } + for (var key in newProps) { + if (key in oldProps && isObjValsEqual(oldProps[key], newProps[key], equalityFuncs[key])) ; + else { + return false; + } + } + // check for props that were omitted in the new + for (var key in oldProps) { + if (!(key in newProps)) { + return false; + } + } + return true; + } + /* + assumed "true" equality for handler names like "onReceiveSomething" + */ + function isObjValsEqual(val0, val1, comparator) { + if (val0 === val1 || comparator === true) { + return true; + } + if (comparator) { + return comparator(val0, val1); + } + return false; + } + function collectFromHash(hash, startIndex, endIndex, step) { + if (startIndex === void 0) { startIndex = 0; } + if (step === void 0) { step = 1; } + var res = []; + if (endIndex == null) { + endIndex = Object.keys(hash).length; + } + for (var i = startIndex; i < endIndex; i += step) { + var val = hash[i]; + if (val !== undefined) { // will disregard undefined for sparse arrays + res.push(val); + } + } + return res; + } + + function parseRecurring(refined, defaultAllDay, dateEnv, recurringTypes) { + for (var i = 0; i < recurringTypes.length; i += 1) { + var parsed = recurringTypes[i].parse(refined, dateEnv); + if (parsed) { + var allDay = refined.allDay; + if (allDay == null) { + allDay = defaultAllDay; + if (allDay == null) { + allDay = parsed.allDayGuess; + if (allDay == null) { + allDay = false; + } + } + } + return { + allDay: allDay, + duration: parsed.duration, + typeData: parsed.typeData, + typeId: i, + }; + } + } + return null; + } + function expandRecurring(eventStore, framingRange, context) { + var dateEnv = context.dateEnv, pluginHooks = context.pluginHooks, options = context.options; + var defs = eventStore.defs, instances = eventStore.instances; + // remove existing recurring instances + // TODO: bad. always expand events as a second step + instances = filterHash(instances, function (instance) { return !defs[instance.defId].recurringDef; }); + for (var defId in defs) { + var def = defs[defId]; + if (def.recurringDef) { + var duration = def.recurringDef.duration; + if (!duration) { + duration = def.allDay ? + options.defaultAllDayEventDuration : + options.defaultTimedEventDuration; + } + var starts = expandRecurringRanges(def, duration, framingRange, dateEnv, pluginHooks.recurringTypes); + for (var _i = 0, starts_1 = starts; _i < starts_1.length; _i++) { + var start = starts_1[_i]; + var instance = createEventInstance(defId, { + start: start, + end: dateEnv.add(start, duration), + }); + instances[instance.instanceId] = instance; + } + } + } + return { defs: defs, instances: instances }; + } + /* + Event MUST have a recurringDef + */ + function expandRecurringRanges(eventDef, duration, framingRange, dateEnv, recurringTypes) { + var typeDef = recurringTypes[eventDef.recurringDef.typeId]; + var markers = typeDef.expand(eventDef.recurringDef.typeData, { + start: dateEnv.subtract(framingRange.start, duration), + end: framingRange.end, + }, dateEnv); + // the recurrence plugins don't guarantee that all-day events are start-of-day, so we have to + if (eventDef.allDay) { + markers = markers.map(startOfDay); + } + return markers; + } + + var INTERNAL_UNITS = ['years', 'months', 'days', 'milliseconds']; + var PARSE_RE = /^(-?)(?:(\d+)\.)?(\d+):(\d\d)(?::(\d\d)(?:\.(\d\d\d))?)?/; + // Parsing and Creation + function createDuration(input, unit) { + var _a; + if (typeof input === 'string') { + return parseString(input); + } + if (typeof input === 'object' && input) { // non-null object + return parseObject(input); + } + if (typeof input === 'number') { + return parseObject((_a = {}, _a[unit || 'milliseconds'] = input, _a)); + } + return null; + } + function parseString(s) { + var m = PARSE_RE.exec(s); + if (m) { + var sign = m[1] ? -1 : 1; + return { + years: 0, + months: 0, + days: sign * (m[2] ? parseInt(m[2], 10) : 0), + milliseconds: sign * ((m[3] ? parseInt(m[3], 10) : 0) * 60 * 60 * 1000 + // hours + (m[4] ? parseInt(m[4], 10) : 0) * 60 * 1000 + // minutes + (m[5] ? parseInt(m[5], 10) : 0) * 1000 + // seconds + (m[6] ? parseInt(m[6], 10) : 0) // ms + ), + }; + } + return null; + } + function parseObject(obj) { + var duration = { + years: obj.years || obj.year || 0, + months: obj.months || obj.month || 0, + days: obj.days || obj.day || 0, + milliseconds: (obj.hours || obj.hour || 0) * 60 * 60 * 1000 + // hours + (obj.minutes || obj.minute || 0) * 60 * 1000 + // minutes + (obj.seconds || obj.second || 0) * 1000 + // seconds + (obj.milliseconds || obj.millisecond || obj.ms || 0), + }; + var weeks = obj.weeks || obj.week; + if (weeks) { + duration.days += weeks * 7; + duration.specifiedWeeks = true; + } + return duration; + } + // Equality + function durationsEqual(d0, d1) { + return d0.years === d1.years && + d0.months === d1.months && + d0.days === d1.days && + d0.milliseconds === d1.milliseconds; + } + function asCleanDays(dur) { + if (!dur.years && !dur.months && !dur.milliseconds) { + return dur.days; + } + return 0; + } + // Simple Math + function addDurations(d0, d1) { + return { + years: d0.years + d1.years, + months: d0.months + d1.months, + days: d0.days + d1.days, + milliseconds: d0.milliseconds + d1.milliseconds, + }; + } + function subtractDurations(d1, d0) { + return { + years: d1.years - d0.years, + months: d1.months - d0.months, + days: d1.days - d0.days, + milliseconds: d1.milliseconds - d0.milliseconds, + }; + } + function multiplyDuration(d, n) { + return { + years: d.years * n, + months: d.months * n, + days: d.days * n, + milliseconds: d.milliseconds * n, + }; + } + // Conversions + // "Rough" because they are based on average-case Gregorian months/years + function asRoughYears(dur) { + return asRoughDays(dur) / 365; + } + function asRoughMonths(dur) { + return asRoughDays(dur) / 30; + } + function asRoughDays(dur) { + return asRoughMs(dur) / 864e5; + } + function asRoughMinutes(dur) { + return asRoughMs(dur) / (1000 * 60); + } + function asRoughSeconds(dur) { + return asRoughMs(dur) / 1000; + } + function asRoughMs(dur) { + return dur.years * (365 * 864e5) + + dur.months * (30 * 864e5) + + dur.days * 864e5 + + dur.milliseconds; + } + // Advanced Math + function wholeDivideDurations(numerator, denominator) { + var res = null; + for (var i = 0; i < INTERNAL_UNITS.length; i += 1) { + var unit = INTERNAL_UNITS[i]; + if (denominator[unit]) { + var localRes = numerator[unit] / denominator[unit]; + if (!isInt(localRes) || (res !== null && res !== localRes)) { + return null; + } + res = localRes; + } + else if (numerator[unit]) { + // needs to divide by something but can't! + return null; + } + } + return res; + } + function greatestDurationDenominator(dur) { + var ms = dur.milliseconds; + if (ms) { + if (ms % 1000 !== 0) { + return { unit: 'millisecond', value: ms }; + } + if (ms % (1000 * 60) !== 0) { + return { unit: 'second', value: ms / 1000 }; + } + if (ms % (1000 * 60 * 60) !== 0) { + return { unit: 'minute', value: ms / (1000 * 60) }; + } + if (ms) { + return { unit: 'hour', value: ms / (1000 * 60 * 60) }; + } + } + if (dur.days) { + if (dur.specifiedWeeks && dur.days % 7 === 0) { + return { unit: 'week', value: dur.days / 7 }; + } + return { unit: 'day', value: dur.days }; + } + if (dur.months) { + return { unit: 'month', value: dur.months }; + } + if (dur.years) { + return { unit: 'year', value: dur.years }; + } + return { unit: 'millisecond', value: 0 }; + } + + // timeZoneOffset is in minutes + function buildIsoString(marker, timeZoneOffset, stripZeroTime) { + if (stripZeroTime === void 0) { stripZeroTime = false; } + var s = marker.toISOString(); + s = s.replace('.000', ''); + if (stripZeroTime) { + s = s.replace('T00:00:00Z', ''); + } + if (s.length > 10) { // time part wasn't stripped, can add timezone info + if (timeZoneOffset == null) { + s = s.replace('Z', ''); + } + else if (timeZoneOffset !== 0) { + s = s.replace('Z', formatTimeZoneOffset(timeZoneOffset, true)); + } + // otherwise, its UTC-0 and we want to keep the Z + } + return s; + } + // formats the date, but with no time part + // TODO: somehow merge with buildIsoString and stripZeroTime + // TODO: rename. omit "string" + function formatDayString(marker) { + return marker.toISOString().replace(/T.*$/, ''); + } + // TODO: use Date::toISOString and use everything after the T? + function formatIsoTimeString(marker) { + return padStart(marker.getUTCHours(), 2) + ':' + + padStart(marker.getUTCMinutes(), 2) + ':' + + padStart(marker.getUTCSeconds(), 2); + } + function formatTimeZoneOffset(minutes, doIso) { + if (doIso === void 0) { doIso = false; } + var sign = minutes < 0 ? '-' : '+'; + var abs = Math.abs(minutes); + var hours = Math.floor(abs / 60); + var mins = Math.round(abs % 60); + if (doIso) { + return sign + padStart(hours, 2) + ":" + padStart(mins, 2); + } + return "GMT" + sign + hours + (mins ? ":" + padStart(mins, 2) : ''); + } + + // TODO: new util arrayify? + function removeExact(array, exactVal) { + var removeCnt = 0; + var i = 0; + while (i < array.length) { + if (array[i] === exactVal) { + array.splice(i, 1); + removeCnt += 1; + } + else { + i += 1; + } + } + return removeCnt; + } + function isArraysEqual(a0, a1, equalityFunc) { + if (a0 === a1) { + return true; + } + var len = a0.length; + var i; + if (len !== a1.length) { // not array? or not same length? + return false; + } + for (i = 0; i < len; i += 1) { + if (!(equalityFunc ? equalityFunc(a0[i], a1[i]) : a0[i] === a1[i])) { + return false; + } + } + return true; + } + + function memoize(workerFunc, resEquality, teardownFunc) { + var currentArgs; + var currentRes; + return function () { + var newArgs = []; + for (var _i = 0; _i < arguments.length; _i++) { + newArgs[_i] = arguments[_i]; + } + if (!currentArgs) { + currentRes = workerFunc.apply(this, newArgs); + } + else if (!isArraysEqual(currentArgs, newArgs)) { + if (teardownFunc) { + teardownFunc(currentRes); + } + var res = workerFunc.apply(this, newArgs); + if (!resEquality || !resEquality(res, currentRes)) { + currentRes = res; + } + } + currentArgs = newArgs; + return currentRes; + }; + } + function memoizeObjArg(workerFunc, resEquality, teardownFunc) { + var _this = this; + var currentArg; + var currentRes; + return function (newArg) { + if (!currentArg) { + currentRes = workerFunc.call(_this, newArg); + } + else if (!isPropsEqual(currentArg, newArg)) { + if (teardownFunc) { + teardownFunc(currentRes); + } + var res = workerFunc.call(_this, newArg); + if (!resEquality || !resEquality(res, currentRes)) { + currentRes = res; + } + } + currentArg = newArg; + return currentRes; + }; + } + function memoizeArraylike(// used at all? + workerFunc, resEquality, teardownFunc) { + var _this = this; + var currentArgSets = []; + var currentResults = []; + return function (newArgSets) { + var currentLen = currentArgSets.length; + var newLen = newArgSets.length; + var i = 0; + for (; i < currentLen; i += 1) { + if (!newArgSets[i]) { // one of the old sets no longer exists + if (teardownFunc) { + teardownFunc(currentResults[i]); + } + } + else if (!isArraysEqual(currentArgSets[i], newArgSets[i])) { + if (teardownFunc) { + teardownFunc(currentResults[i]); + } + var res = workerFunc.apply(_this, newArgSets[i]); + if (!resEquality || !resEquality(res, currentResults[i])) { + currentResults[i] = res; + } + } + } + for (; i < newLen; i += 1) { + currentResults[i] = workerFunc.apply(_this, newArgSets[i]); + } + currentArgSets = newArgSets; + currentResults.splice(newLen); // remove excess + return currentResults; + }; + } + function memoizeHashlike(// used? + workerFunc, resEquality, teardownFunc) { + var _this = this; + var currentArgHash = {}; + var currentResHash = {}; + return function (newArgHash) { + var newResHash = {}; + for (var key in newArgHash) { + if (!currentResHash[key]) { + newResHash[key] = workerFunc.apply(_this, newArgHash[key]); + } + else if (!isArraysEqual(currentArgHash[key], newArgHash[key])) { + if (teardownFunc) { + teardownFunc(currentResHash[key]); + } + var res = workerFunc.apply(_this, newArgHash[key]); + newResHash[key] = (resEquality && resEquality(res, currentResHash[key])) + ? currentResHash[key] + : res; + } + else { + newResHash[key] = currentResHash[key]; + } + } + currentArgHash = newArgHash; + currentResHash = newResHash; + return newResHash; + }; + } + + var EXTENDED_SETTINGS_AND_SEVERITIES = { + week: 3, + separator: 0, + omitZeroMinute: 0, + meridiem: 0, + omitCommas: 0, + }; + var STANDARD_DATE_PROP_SEVERITIES = { + timeZoneName: 7, + era: 6, + year: 5, + month: 4, + day: 2, + weekday: 2, + hour: 1, + minute: 1, + second: 1, + }; + var MERIDIEM_RE = /\s*([ap])\.?m\.?/i; // eats up leading spaces too + var COMMA_RE = /,/g; // we need re for globalness + var MULTI_SPACE_RE = /\s+/g; + var LTR_RE = /\u200e/g; // control character + var UTC_RE = /UTC|GMT/; + var NativeFormatter = /** @class */ (function () { + function NativeFormatter(formatSettings) { + var standardDateProps = {}; + var extendedSettings = {}; + var severity = 0; + for (var name_1 in formatSettings) { + if (name_1 in EXTENDED_SETTINGS_AND_SEVERITIES) { + extendedSettings[name_1] = formatSettings[name_1]; + severity = Math.max(EXTENDED_SETTINGS_AND_SEVERITIES[name_1], severity); + } + else { + standardDateProps[name_1] = formatSettings[name_1]; + if (name_1 in STANDARD_DATE_PROP_SEVERITIES) { // TODO: what about hour12? no severity + severity = Math.max(STANDARD_DATE_PROP_SEVERITIES[name_1], severity); + } + } + } + this.standardDateProps = standardDateProps; + this.extendedSettings = extendedSettings; + this.severity = severity; + this.buildFormattingFunc = memoize(buildFormattingFunc); + } + NativeFormatter.prototype.format = function (date, context) { + return this.buildFormattingFunc(this.standardDateProps, this.extendedSettings, context)(date); + }; + NativeFormatter.prototype.formatRange = function (start, end, context, betterDefaultSeparator) { + var _a = this, standardDateProps = _a.standardDateProps, extendedSettings = _a.extendedSettings; + var diffSeverity = computeMarkerDiffSeverity(start.marker, end.marker, context.calendarSystem); + if (!diffSeverity) { + return this.format(start, context); + } + var biggestUnitForPartial = diffSeverity; + if (biggestUnitForPartial > 1 && // the two dates are different in a way that's larger scale than time + (standardDateProps.year === 'numeric' || standardDateProps.year === '2-digit') && + (standardDateProps.month === 'numeric' || standardDateProps.month === '2-digit') && + (standardDateProps.day === 'numeric' || standardDateProps.day === '2-digit')) { + biggestUnitForPartial = 1; // make it look like the dates are only different in terms of time + } + var full0 = this.format(start, context); + var full1 = this.format(end, context); + if (full0 === full1) { + return full0; + } + var partialDateProps = computePartialFormattingOptions(standardDateProps, biggestUnitForPartial); + var partialFormattingFunc = buildFormattingFunc(partialDateProps, extendedSettings, context); + var partial0 = partialFormattingFunc(start); + var partial1 = partialFormattingFunc(end); + var insertion = findCommonInsertion(full0, partial0, full1, partial1); + var separator = extendedSettings.separator || betterDefaultSeparator || context.defaultSeparator || ''; + if (insertion) { + return insertion.before + partial0 + separator + partial1 + insertion.after; + } + return full0 + separator + full1; + }; + NativeFormatter.prototype.getLargestUnit = function () { + switch (this.severity) { + case 7: + case 6: + case 5: + return 'year'; + case 4: + return 'month'; + case 3: + return 'week'; + case 2: + return 'day'; + default: + return 'time'; // really? + } + }; + return NativeFormatter; + }()); + function buildFormattingFunc(standardDateProps, extendedSettings, context) { + var standardDatePropCnt = Object.keys(standardDateProps).length; + if (standardDatePropCnt === 1 && standardDateProps.timeZoneName === 'short') { + return function (date) { return (formatTimeZoneOffset(date.timeZoneOffset)); }; + } + if (standardDatePropCnt === 0 && extendedSettings.week) { + return function (date) { return (formatWeekNumber(context.computeWeekNumber(date.marker), context.weekText, context.locale, extendedSettings.week)); }; + } + return buildNativeFormattingFunc(standardDateProps, extendedSettings, context); + } + function buildNativeFormattingFunc(standardDateProps, extendedSettings, context) { + standardDateProps = __assign({}, standardDateProps); // copy + extendedSettings = __assign({}, extendedSettings); // copy + sanitizeSettings(standardDateProps, extendedSettings); + standardDateProps.timeZone = 'UTC'; // we leverage the only guaranteed timeZone for our UTC markers + var normalFormat = new Intl.DateTimeFormat(context.locale.codes, standardDateProps); + var zeroFormat; // needed? + if (extendedSettings.omitZeroMinute) { + var zeroProps = __assign({}, standardDateProps); + delete zeroProps.minute; // seconds and ms were already considered in sanitizeSettings + zeroFormat = new Intl.DateTimeFormat(context.locale.codes, zeroProps); + } + return function (date) { + var marker = date.marker; + var format; + if (zeroFormat && !marker.getUTCMinutes()) { + format = zeroFormat; + } + else { + format = normalFormat; + } + var s = format.format(marker); + return postProcess(s, date, standardDateProps, extendedSettings, context); + }; + } + function sanitizeSettings(standardDateProps, extendedSettings) { + // deal with a browser inconsistency where formatting the timezone + // requires that the hour/minute be present. + if (standardDateProps.timeZoneName) { + if (!standardDateProps.hour) { + standardDateProps.hour = '2-digit'; + } + if (!standardDateProps.minute) { + standardDateProps.minute = '2-digit'; + } + } + // only support short timezone names + if (standardDateProps.timeZoneName === 'long') { + standardDateProps.timeZoneName = 'short'; + } + // if requesting to display seconds, MUST display minutes + if (extendedSettings.omitZeroMinute && (standardDateProps.second || standardDateProps.millisecond)) { + delete extendedSettings.omitZeroMinute; + } + } + function postProcess(s, date, standardDateProps, extendedSettings, context) { + s = s.replace(LTR_RE, ''); // remove left-to-right control chars. do first. good for other regexes + if (standardDateProps.timeZoneName === 'short') { + s = injectTzoStr(s, (context.timeZone === 'UTC' || date.timeZoneOffset == null) ? + 'UTC' : // important to normalize for IE, which does "GMT" + formatTimeZoneOffset(date.timeZoneOffset)); + } + if (extendedSettings.omitCommas) { + s = s.replace(COMMA_RE, '').trim(); + } + if (extendedSettings.omitZeroMinute) { + s = s.replace(':00', ''); // zeroFormat doesn't always achieve this + } + // ^ do anything that might create adjacent spaces before this point, + // because MERIDIEM_RE likes to eat up loading spaces + if (extendedSettings.meridiem === false) { + s = s.replace(MERIDIEM_RE, '').trim(); + } + else if (extendedSettings.meridiem === 'narrow') { // a/p + s = s.replace(MERIDIEM_RE, function (m0, m1) { return m1.toLocaleLowerCase(); }); + } + else if (extendedSettings.meridiem === 'short') { // am/pm + s = s.replace(MERIDIEM_RE, function (m0, m1) { return m1.toLocaleLowerCase() + "m"; }); + } + else if (extendedSettings.meridiem === 'lowercase') { // other meridiem transformers already converted to lowercase + s = s.replace(MERIDIEM_RE, function (m0) { return m0.toLocaleLowerCase(); }); + } + s = s.replace(MULTI_SPACE_RE, ' '); + s = s.trim(); + return s; + } + function injectTzoStr(s, tzoStr) { + var replaced = false; + s = s.replace(UTC_RE, function () { + replaced = true; + return tzoStr; + }); + // IE11 doesn't include UTC/GMT in the original string, so append to end + if (!replaced) { + s += " " + tzoStr; + } + return s; + } + function formatWeekNumber(num, weekText, locale, display) { + var parts = []; + if (display === 'narrow') { + parts.push(weekText); + } + else if (display === 'short') { + parts.push(weekText, ' '); + } + // otherwise, considered 'numeric' + parts.push(locale.simpleNumberFormat.format(num)); + if (locale.options.direction === 'rtl') { // TODO: use control characters instead? + parts.reverse(); + } + return parts.join(''); + } + // Range Formatting Utils + // 0 = exactly the same + // 1 = different by time + // and bigger + function computeMarkerDiffSeverity(d0, d1, ca) { + if (ca.getMarkerYear(d0) !== ca.getMarkerYear(d1)) { + return 5; + } + if (ca.getMarkerMonth(d0) !== ca.getMarkerMonth(d1)) { + return 4; + } + if (ca.getMarkerDay(d0) !== ca.getMarkerDay(d1)) { + return 2; + } + if (timeAsMs(d0) !== timeAsMs(d1)) { + return 1; + } + return 0; + } + function computePartialFormattingOptions(options, biggestUnit) { + var partialOptions = {}; + for (var name_2 in options) { + if (!(name_2 in STANDARD_DATE_PROP_SEVERITIES) || // not a date part prop (like timeZone) + STANDARD_DATE_PROP_SEVERITIES[name_2] <= biggestUnit) { + partialOptions[name_2] = options[name_2]; + } + } + return partialOptions; + } + function findCommonInsertion(full0, partial0, full1, partial1) { + var i0 = 0; + while (i0 < full0.length) { + var found0 = full0.indexOf(partial0, i0); + if (found0 === -1) { + break; + } + var before0 = full0.substr(0, found0); + i0 = found0 + partial0.length; + var after0 = full0.substr(i0); + var i1 = 0; + while (i1 < full1.length) { + var found1 = full1.indexOf(partial1, i1); + if (found1 === -1) { + break; + } + var before1 = full1.substr(0, found1); + i1 = found1 + partial1.length; + var after1 = full1.substr(i1); + if (before0 === before1 && after0 === after1) { + return { + before: before0, + after: after0, + }; + } + } + } + return null; + } + + function expandZonedMarker(dateInfo, calendarSystem) { + var a = calendarSystem.markerToArray(dateInfo.marker); + return { + marker: dateInfo.marker, + timeZoneOffset: dateInfo.timeZoneOffset, + array: a, + year: a[0], + month: a[1], + day: a[2], + hour: a[3], + minute: a[4], + second: a[5], + millisecond: a[6], + }; + } + + function createVerboseFormattingArg(start, end, context, betterDefaultSeparator) { + var startInfo = expandZonedMarker(start, context.calendarSystem); + var endInfo = end ? expandZonedMarker(end, context.calendarSystem) : null; + return { + date: startInfo, + start: startInfo, + end: endInfo, + timeZone: context.timeZone, + localeCodes: context.locale.codes, + defaultSeparator: betterDefaultSeparator || context.defaultSeparator, + }; + } + + /* + TODO: fix the terminology of "formatter" vs "formatting func" + */ + /* + At the time of instantiation, this object does not know which cmd-formatting system it will use. + It receives this at the time of formatting, as a setting. + */ + var CmdFormatter = /** @class */ (function () { + function CmdFormatter(cmdStr) { + this.cmdStr = cmdStr; + } + CmdFormatter.prototype.format = function (date, context, betterDefaultSeparator) { + return context.cmdFormatter(this.cmdStr, createVerboseFormattingArg(date, null, context, betterDefaultSeparator)); + }; + CmdFormatter.prototype.formatRange = function (start, end, context, betterDefaultSeparator) { + return context.cmdFormatter(this.cmdStr, createVerboseFormattingArg(start, end, context, betterDefaultSeparator)); + }; + return CmdFormatter; + }()); + + var FuncFormatter = /** @class */ (function () { + function FuncFormatter(func) { + this.func = func; + } + FuncFormatter.prototype.format = function (date, context, betterDefaultSeparator) { + return this.func(createVerboseFormattingArg(date, null, context, betterDefaultSeparator)); + }; + FuncFormatter.prototype.formatRange = function (start, end, context, betterDefaultSeparator) { + return this.func(createVerboseFormattingArg(start, end, context, betterDefaultSeparator)); + }; + return FuncFormatter; + }()); + + function createFormatter(input) { + if (typeof input === 'object' && input) { // non-null object + return new NativeFormatter(input); + } + if (typeof input === 'string') { + return new CmdFormatter(input); + } + if (typeof input === 'function') { + return new FuncFormatter(input); + } + return null; + } + + // base options + // ------------ + var BASE_OPTION_REFINERS = { + navLinkDayClick: identity, + navLinkWeekClick: identity, + duration: createDuration, + bootstrapFontAwesome: identity, + buttonIcons: identity, + customButtons: identity, + defaultAllDayEventDuration: createDuration, + defaultTimedEventDuration: createDuration, + nextDayThreshold: createDuration, + scrollTime: createDuration, + slotMinTime: createDuration, + slotMaxTime: createDuration, + dayPopoverFormat: createFormatter, + slotDuration: createDuration, + snapDuration: createDuration, + headerToolbar: identity, + footerToolbar: identity, + defaultRangeSeparator: String, + titleRangeSeparator: String, + forceEventDuration: Boolean, + dayHeaders: Boolean, + dayHeaderFormat: createFormatter, + dayHeaderClassNames: identity, + dayHeaderContent: identity, + dayHeaderDidMount: identity, + dayHeaderWillUnmount: identity, + dayCellClassNames: identity, + dayCellContent: identity, + dayCellDidMount: identity, + dayCellWillUnmount: identity, + initialView: String, + aspectRatio: Number, + weekends: Boolean, + weekNumberCalculation: identity, + weekNumbers: Boolean, + weekNumberClassNames: identity, + weekNumberContent: identity, + weekNumberDidMount: identity, + weekNumberWillUnmount: identity, + editable: Boolean, + viewClassNames: identity, + viewDidMount: identity, + viewWillUnmount: identity, + nowIndicator: Boolean, + nowIndicatorClassNames: identity, + nowIndicatorContent: identity, + nowIndicatorDidMount: identity, + nowIndicatorWillUnmount: identity, + showNonCurrentDates: Boolean, + lazyFetching: Boolean, + startParam: String, + endParam: String, + timeZoneParam: String, + timeZone: String, + locales: identity, + locale: identity, + themeSystem: String, + dragRevertDuration: Number, + dragScroll: Boolean, + allDayMaintainDuration: Boolean, + unselectAuto: Boolean, + dropAccept: identity, + eventOrder: parseFieldSpecs, + handleWindowResize: Boolean, + windowResizeDelay: Number, + longPressDelay: Number, + eventDragMinDistance: Number, + expandRows: Boolean, + height: identity, + contentHeight: identity, + direction: String, + weekNumberFormat: createFormatter, + eventResizableFromStart: Boolean, + displayEventTime: Boolean, + displayEventEnd: Boolean, + weekText: String, + progressiveEventRendering: Boolean, + businessHours: identity, + initialDate: identity, + now: identity, + eventDataTransform: identity, + stickyHeaderDates: identity, + stickyFooterScrollbar: identity, + viewHeight: identity, + defaultAllDay: Boolean, + eventSourceFailure: identity, + eventSourceSuccess: identity, + eventDisplay: String, + eventStartEditable: Boolean, + eventDurationEditable: Boolean, + eventOverlap: identity, + eventConstraint: identity, + eventAllow: identity, + eventBackgroundColor: String, + eventBorderColor: String, + eventTextColor: String, + eventColor: String, + eventClassNames: identity, + eventContent: identity, + eventDidMount: identity, + eventWillUnmount: identity, + selectConstraint: identity, + selectOverlap: identity, + selectAllow: identity, + droppable: Boolean, + unselectCancel: String, + slotLabelFormat: identity, + slotLaneClassNames: identity, + slotLaneContent: identity, + slotLaneDidMount: identity, + slotLaneWillUnmount: identity, + slotLabelClassNames: identity, + slotLabelContent: identity, + slotLabelDidMount: identity, + slotLabelWillUnmount: identity, + dayMaxEvents: identity, + dayMaxEventRows: identity, + dayMinWidth: Number, + slotLabelInterval: createDuration, + allDayText: String, + allDayClassNames: identity, + allDayContent: identity, + allDayDidMount: identity, + allDayWillUnmount: identity, + slotMinWidth: Number, + navLinks: Boolean, + eventTimeFormat: createFormatter, + rerenderDelay: Number, + moreLinkText: identity, + selectMinDistance: Number, + selectable: Boolean, + selectLongPressDelay: Number, + eventLongPressDelay: Number, + selectMirror: Boolean, + eventMinHeight: Number, + slotEventOverlap: Boolean, + plugins: identity, + firstDay: Number, + dayCount: Number, + dateAlignment: String, + dateIncrement: createDuration, + hiddenDays: identity, + monthMode: Boolean, + fixedWeekCount: Boolean, + validRange: identity, + visibleRange: identity, + titleFormat: identity, + // only used by list-view, but languages define the value, so we need it in base options + noEventsText: String, + }; + // do NOT give a type here. need `typeof BASE_OPTION_DEFAULTS` to give real results. + // raw values. + var BASE_OPTION_DEFAULTS = { + eventDisplay: 'auto', + defaultRangeSeparator: ' - ', + titleRangeSeparator: ' \u2013 ', + defaultTimedEventDuration: '01:00:00', + defaultAllDayEventDuration: { day: 1 }, + forceEventDuration: false, + nextDayThreshold: '00:00:00', + dayHeaders: true, + initialView: '', + aspectRatio: 1.35, + headerToolbar: { + start: 'title', + center: '', + end: 'today prev,next', + }, + weekends: true, + weekNumbers: false, + weekNumberCalculation: 'local', + editable: false, + nowIndicator: false, + scrollTime: '06:00:00', + slotMinTime: '00:00:00', + slotMaxTime: '24:00:00', + showNonCurrentDates: true, + lazyFetching: true, + startParam: 'start', + endParam: 'end', + timeZoneParam: 'timeZone', + timeZone: 'local', + locales: [], + locale: '', + themeSystem: 'standard', + dragRevertDuration: 500, + dragScroll: true, + allDayMaintainDuration: false, + unselectAuto: true, + dropAccept: '*', + eventOrder: 'start,-duration,allDay,title', + dayPopoverFormat: { month: 'long', day: 'numeric', year: 'numeric' }, + handleWindowResize: true, + windowResizeDelay: 100, + longPressDelay: 1000, + eventDragMinDistance: 5, + expandRows: false, + navLinks: false, + selectable: false, + }; + // calendar listeners + // ------------------ + var CALENDAR_LISTENER_REFINERS = { + datesSet: identity, + eventsSet: identity, + eventAdd: identity, + eventChange: identity, + eventRemove: identity, + windowResize: identity, + eventClick: identity, + eventMouseEnter: identity, + eventMouseLeave: identity, + select: identity, + unselect: identity, + loading: identity, + // internal + _unmount: identity, + _beforeprint: identity, + _afterprint: identity, + _noEventDrop: identity, + _noEventResize: identity, + _resize: identity, + _scrollRequest: identity, + }; + // calendar-specific options + // ------------------------- + var CALENDAR_OPTION_REFINERS = { + buttonText: identity, + views: identity, + plugins: identity, + initialEvents: identity, + events: identity, + eventSources: identity, + }; + var COMPLEX_OPTION_COMPARATORS = { + headerToolbar: isBoolComplexEqual, + footerToolbar: isBoolComplexEqual, + buttonText: isBoolComplexEqual, + buttonIcons: isBoolComplexEqual, + }; + function isBoolComplexEqual(a, b) { + if (typeof a === 'object' && typeof b === 'object' && a && b) { // both non-null objects + return isPropsEqual(a, b); + } + return a === b; + } + // view-specific options + // --------------------- + var VIEW_OPTION_REFINERS = { + type: String, + component: identity, + buttonText: String, + buttonTextKey: String, + dateProfileGeneratorClass: identity, + usesMinMaxTime: Boolean, + classNames: identity, + content: identity, + didMount: identity, + willUnmount: identity, + }; + // util funcs + // ---------------------------------------------------------------------------------------------------- + function mergeRawOptions(optionSets) { + return mergeProps(optionSets, COMPLEX_OPTION_COMPARATORS); + } + function refineProps(input, refiners) { + var refined = {}; + var extra = {}; + for (var propName in refiners) { + if (propName in input) { + refined[propName] = refiners[propName](input[propName]); + } + } + for (var propName in input) { + if (!(propName in refiners)) { + extra[propName] = input[propName]; + } + } + return { refined: refined, extra: extra }; + } + function identity(raw) { + return raw; + } + + function parseEvents(rawEvents, eventSource, context, allowOpenRange) { + var eventStore = createEmptyEventStore(); + var eventRefiners = buildEventRefiners(context); + for (var _i = 0, rawEvents_1 = rawEvents; _i < rawEvents_1.length; _i++) { + var rawEvent = rawEvents_1[_i]; + var tuple = parseEvent(rawEvent, eventSource, context, allowOpenRange, eventRefiners); + if (tuple) { + eventTupleToStore(tuple, eventStore); + } + } + return eventStore; + } + function eventTupleToStore(tuple, eventStore) { + if (eventStore === void 0) { eventStore = createEmptyEventStore(); } + eventStore.defs[tuple.def.defId] = tuple.def; + if (tuple.instance) { + eventStore.instances[tuple.instance.instanceId] = tuple.instance; + } + return eventStore; + } + // retrieves events that have the same groupId as the instance specified by `instanceId` + // or they are the same as the instance. + // why might instanceId not be in the store? an event from another calendar? + function getRelevantEvents(eventStore, instanceId) { + var instance = eventStore.instances[instanceId]; + if (instance) { + var def_1 = eventStore.defs[instance.defId]; + // get events/instances with same group + var newStore = filterEventStoreDefs(eventStore, function (lookDef) { return isEventDefsGrouped(def_1, lookDef); }); + // add the original + // TODO: wish we could use eventTupleToStore or something like it + newStore.defs[def_1.defId] = def_1; + newStore.instances[instance.instanceId] = instance; + return newStore; + } + return createEmptyEventStore(); + } + function isEventDefsGrouped(def0, def1) { + return Boolean(def0.groupId && def0.groupId === def1.groupId); + } + function createEmptyEventStore() { + return { defs: {}, instances: {} }; + } + function mergeEventStores(store0, store1) { + return { + defs: __assign(__assign({}, store0.defs), store1.defs), + instances: __assign(__assign({}, store0.instances), store1.instances), + }; + } + function filterEventStoreDefs(eventStore, filterFunc) { + var defs = filterHash(eventStore.defs, filterFunc); + var instances = filterHash(eventStore.instances, function (instance) { return (defs[instance.defId] // still exists? + ); }); + return { defs: defs, instances: instances }; + } + function excludeSubEventStore(master, sub) { + var defs = master.defs, instances = master.instances; + var filteredDefs = {}; + var filteredInstances = {}; + for (var defId in defs) { + if (!sub.defs[defId]) { // not explicitly excluded + filteredDefs[defId] = defs[defId]; + } + } + for (var instanceId in instances) { + if (!sub.instances[instanceId] && // not explicitly excluded + filteredDefs[instances[instanceId].defId] // def wasn't filtered away + ) { + filteredInstances[instanceId] = instances[instanceId]; + } + } + return { + defs: filteredDefs, + instances: filteredInstances, + }; + } + + function normalizeConstraint(input, context) { + if (Array.isArray(input)) { + return parseEvents(input, null, context, true); // allowOpenRange=true + } + if (typeof input === 'object' && input) { // non-null object + return parseEvents([input], null, context, true); // allowOpenRange=true + } + if (input != null) { + return String(input); + } + return null; + } + + function parseClassNames(raw) { + if (Array.isArray(raw)) { + return raw; + } + if (typeof raw === 'string') { + return raw.split(/\s+/); + } + return []; + } + + // TODO: better called "EventSettings" or "EventConfig" + // TODO: move this file into structs + // TODO: separate constraint/overlap/allow, because selection uses only that, not other props + var EVENT_UI_REFINERS = { + display: String, + editable: Boolean, + startEditable: Boolean, + durationEditable: Boolean, + constraint: identity, + overlap: identity, + allow: identity, + className: parseClassNames, + classNames: parseClassNames, + color: String, + backgroundColor: String, + borderColor: String, + textColor: String, + }; + var EMPTY_EVENT_UI = { + display: null, + startEditable: null, + durationEditable: null, + constraints: [], + overlap: null, + allows: [], + backgroundColor: '', + borderColor: '', + textColor: '', + classNames: [], + }; + function createEventUi(refined, context) { + var constraint = normalizeConstraint(refined.constraint, context); + return { + display: refined.display || null, + startEditable: refined.startEditable != null ? refined.startEditable : refined.editable, + durationEditable: refined.durationEditable != null ? refined.durationEditable : refined.editable, + constraints: constraint != null ? [constraint] : [], + overlap: refined.overlap != null ? refined.overlap : null, + allows: refined.allow != null ? [refined.allow] : [], + backgroundColor: refined.backgroundColor || refined.color || '', + borderColor: refined.borderColor || refined.color || '', + textColor: refined.textColor || '', + classNames: (refined.className || []).concat(refined.classNames || []), + }; + } + // TODO: prevent against problems with <2 args! + function combineEventUis(uis) { + return uis.reduce(combineTwoEventUis, EMPTY_EVENT_UI); + } + function combineTwoEventUis(item0, item1) { + return { + display: item1.display != null ? item1.display : item0.display, + startEditable: item1.startEditable != null ? item1.startEditable : item0.startEditable, + durationEditable: item1.durationEditable != null ? item1.durationEditable : item0.durationEditable, + constraints: item0.constraints.concat(item1.constraints), + overlap: typeof item1.overlap === 'boolean' ? item1.overlap : item0.overlap, + allows: item0.allows.concat(item1.allows), + backgroundColor: item1.backgroundColor || item0.backgroundColor, + borderColor: item1.borderColor || item0.borderColor, + textColor: item1.textColor || item0.textColor, + classNames: item0.classNames.concat(item1.classNames), + }; + } + + var EVENT_NON_DATE_REFINERS = { + id: String, + groupId: String, + title: String, + url: String, + }; + var EVENT_DATE_REFINERS = { + start: identity, + end: identity, + date: identity, + allDay: Boolean, + }; + var EVENT_REFINERS = __assign(__assign(__assign({}, EVENT_NON_DATE_REFINERS), EVENT_DATE_REFINERS), { extendedProps: identity }); + function parseEvent(raw, eventSource, context, allowOpenRange, refiners) { + if (refiners === void 0) { refiners = buildEventRefiners(context); } + var _a = refineEventDef(raw, context, refiners), refined = _a.refined, extra = _a.extra; + var defaultAllDay = computeIsDefaultAllDay(eventSource, context); + var recurringRes = parseRecurring(refined, defaultAllDay, context.dateEnv, context.pluginHooks.recurringTypes); + if (recurringRes) { + var def = parseEventDef(refined, extra, eventSource ? eventSource.sourceId : '', recurringRes.allDay, Boolean(recurringRes.duration), context); + def.recurringDef = { + typeId: recurringRes.typeId, + typeData: recurringRes.typeData, + duration: recurringRes.duration, + }; + return { def: def, instance: null }; + } + var singleRes = parseSingle(refined, defaultAllDay, context, allowOpenRange); + if (singleRes) { + var def = parseEventDef(refined, extra, eventSource ? eventSource.sourceId : '', singleRes.allDay, singleRes.hasEnd, context); + var instance = createEventInstance(def.defId, singleRes.range, singleRes.forcedStartTzo, singleRes.forcedEndTzo); + return { def: def, instance: instance }; + } + return null; + } + function refineEventDef(raw, context, refiners) { + if (refiners === void 0) { refiners = buildEventRefiners(context); } + return refineProps(raw, refiners); + } + function buildEventRefiners(context) { + return __assign(__assign(__assign({}, EVENT_UI_REFINERS), EVENT_REFINERS), context.pluginHooks.eventRefiners); + } + /* + Will NOT populate extendedProps with the leftover properties. + Will NOT populate date-related props. + */ + function parseEventDef(refined, extra, sourceId, allDay, hasEnd, context) { + var def = { + title: refined.title || '', + groupId: refined.groupId || '', + publicId: refined.id || '', + url: refined.url || '', + recurringDef: null, + defId: guid(), + sourceId: sourceId, + allDay: allDay, + hasEnd: hasEnd, + ui: createEventUi(refined, context), + extendedProps: __assign(__assign({}, (refined.extendedProps || {})), extra), + }; + for (var _i = 0, _a = context.pluginHooks.eventDefMemberAdders; _i < _a.length; _i++) { + var memberAdder = _a[_i]; + __assign(def, memberAdder(refined)); + } + // help out EventApi from having user modify props + Object.freeze(def.ui.classNames); + Object.freeze(def.extendedProps); + return def; + } + function parseSingle(refined, defaultAllDay, context, allowOpenRange) { + var allDay = refined.allDay; + var startMeta; + var startMarker = null; + var hasEnd = false; + var endMeta; + var endMarker = null; + var startInput = refined.start != null ? refined.start : refined.date; + startMeta = context.dateEnv.createMarkerMeta(startInput); + if (startMeta) { + startMarker = startMeta.marker; + } + else if (!allowOpenRange) { + return null; + } + if (refined.end != null) { + endMeta = context.dateEnv.createMarkerMeta(refined.end); + } + if (allDay == null) { + if (defaultAllDay != null) { + allDay = defaultAllDay; + } + else { + // fall back to the date props LAST + allDay = (!startMeta || startMeta.isTimeUnspecified) && + (!endMeta || endMeta.isTimeUnspecified); + } + } + if (allDay && startMarker) { + startMarker = startOfDay(startMarker); + } + if (endMeta) { + endMarker = endMeta.marker; + if (allDay) { + endMarker = startOfDay(endMarker); + } + if (startMarker && endMarker <= startMarker) { + endMarker = null; + } + } + if (endMarker) { + hasEnd = true; + } + else if (!allowOpenRange) { + hasEnd = context.options.forceEventDuration || false; + endMarker = context.dateEnv.add(startMarker, allDay ? + context.options.defaultAllDayEventDuration : + context.options.defaultTimedEventDuration); + } + return { + allDay: allDay, + hasEnd: hasEnd, + range: { start: startMarker, end: endMarker }, + forcedStartTzo: startMeta ? startMeta.forcedTzo : null, + forcedEndTzo: endMeta ? endMeta.forcedTzo : null, + }; + } + function computeIsDefaultAllDay(eventSource, context) { + var res = null; + if (eventSource) { + res = eventSource.defaultAllDay; + } + if (res == null) { + res = context.options.defaultAllDay; + } + return res; + } + + /* Date stuff that doesn't belong in datelib core + ----------------------------------------------------------------------------------------------------------------------*/ + // given a timed range, computes an all-day range that has the same exact duration, + // but whose start time is aligned with the start of the day. + function computeAlignedDayRange(timedRange) { + var dayCnt = Math.floor(diffDays(timedRange.start, timedRange.end)) || 1; + var start = startOfDay(timedRange.start); + var end = addDays(start, dayCnt); + return { start: start, end: end }; + } + // given a timed range, computes an all-day range based on how for the end date bleeds into the next day + // TODO: give nextDayThreshold a default arg + function computeVisibleDayRange(timedRange, nextDayThreshold) { + if (nextDayThreshold === void 0) { nextDayThreshold = createDuration(0); } + var startDay = null; + var endDay = null; + if (timedRange.end) { + endDay = startOfDay(timedRange.end); + var endTimeMS = timedRange.end.valueOf() - endDay.valueOf(); // # of milliseconds into `endDay` + // If the end time is actually inclusively part of the next day and is equal to or + // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`. + // Otherwise, leaving it as inclusive will cause it to exclude `endDay`. + if (endTimeMS && endTimeMS >= asRoughMs(nextDayThreshold)) { + endDay = addDays(endDay, 1); + } + } + if (timedRange.start) { + startDay = startOfDay(timedRange.start); // the beginning of the day the range starts + // If end is within `startDay` but not past nextDayThreshold, assign the default duration of one day. + if (endDay && endDay <= startDay) { + endDay = addDays(startDay, 1); + } + } + return { start: startDay, end: endDay }; + } + // spans from one day into another? + function isMultiDayRange(range) { + var visibleRange = computeVisibleDayRange(range); + return diffDays(visibleRange.start, visibleRange.end) > 1; + } + function diffDates(date0, date1, dateEnv, largeUnit) { + if (largeUnit === 'year') { + return createDuration(dateEnv.diffWholeYears(date0, date1), 'year'); + } + if (largeUnit === 'month') { + return createDuration(dateEnv.diffWholeMonths(date0, date1), 'month'); + } + return diffDayAndTime(date0, date1); // returns a duration + } + + function parseRange(input, dateEnv) { + var start = null; + var end = null; + if (input.start) { + start = dateEnv.createMarker(input.start); + } + if (input.end) { + end = dateEnv.createMarker(input.end); + } + if (!start && !end) { + return null; + } + if (start && end && end < start) { + return null; + } + return { start: start, end: end }; + } + // SIDE-EFFECT: will mutate ranges. + // Will return a new array result. + function invertRanges(ranges, constraintRange) { + var invertedRanges = []; + var start = constraintRange.start; // the end of the previous range. the start of the new range + var i; + var dateRange; + // ranges need to be in order. required for our date-walking algorithm + ranges.sort(compareRanges); + for (i = 0; i < ranges.length; i += 1) { + dateRange = ranges[i]; + // add the span of time before the event (if there is any) + if (dateRange.start > start) { // compare millisecond time (skip any ambig logic) + invertedRanges.push({ start: start, end: dateRange.start }); + } + if (dateRange.end > start) { + start = dateRange.end; + } + } + // add the span of time after the last event (if there is any) + if (start < constraintRange.end) { // compare millisecond time (skip any ambig logic) + invertedRanges.push({ start: start, end: constraintRange.end }); + } + return invertedRanges; + } + function compareRanges(range0, range1) { + return range0.start.valueOf() - range1.start.valueOf(); // earlier ranges go first + } + function intersectRanges(range0, range1) { + var start = range0.start, end = range0.end; + var newRange = null; + if (range1.start !== null) { + if (start === null) { + start = range1.start; + } + else { + start = new Date(Math.max(start.valueOf(), range1.start.valueOf())); + } + } + if (range1.end != null) { + if (end === null) { + end = range1.end; + } + else { + end = new Date(Math.min(end.valueOf(), range1.end.valueOf())); + } + } + if (start === null || end === null || start < end) { + newRange = { start: start, end: end }; + } + return newRange; + } + function rangesEqual(range0, range1) { + return (range0.start === null ? null : range0.start.valueOf()) === (range1.start === null ? null : range1.start.valueOf()) && + (range0.end === null ? null : range0.end.valueOf()) === (range1.end === null ? null : range1.end.valueOf()); + } + function rangesIntersect(range0, range1) { + return (range0.end === null || range1.start === null || range0.end > range1.start) && + (range0.start === null || range1.end === null || range0.start < range1.end); + } + function rangeContainsRange(outerRange, innerRange) { + return (outerRange.start === null || (innerRange.start !== null && innerRange.start >= outerRange.start)) && + (outerRange.end === null || (innerRange.end !== null && innerRange.end <= outerRange.end)); + } + function rangeContainsMarker(range, date) { + return (range.start === null || date >= range.start) && + (range.end === null || date < range.end); + } + // If the given date is not within the given range, move it inside. + // (If it's past the end, make it one millisecond before the end). + function constrainMarkerToRange(date, range) { + if (range.start != null && date < range.start) { + return range.start; + } + if (range.end != null && date >= range.end) { + return new Date(range.end.valueOf() - 1); + } + return date; + } + + /* + Specifying nextDayThreshold signals that all-day ranges should be sliced. + */ + function sliceEventStore(eventStore, eventUiBases, framingRange, nextDayThreshold) { + var inverseBgByGroupId = {}; + var inverseBgByDefId = {}; + var defByGroupId = {}; + var bgRanges = []; + var fgRanges = []; + var eventUis = compileEventUis(eventStore.defs, eventUiBases); + for (var defId in eventStore.defs) { + var def = eventStore.defs[defId]; + var ui = eventUis[def.defId]; + if (ui.display === 'inverse-background') { + if (def.groupId) { + inverseBgByGroupId[def.groupId] = []; + if (!defByGroupId[def.groupId]) { + defByGroupId[def.groupId] = def; + } + } + else { + inverseBgByDefId[defId] = []; + } + } + } + for (var instanceId in eventStore.instances) { + var instance = eventStore.instances[instanceId]; + var def = eventStore.defs[instance.defId]; + var ui = eventUis[def.defId]; + var origRange = instance.range; + var normalRange = (!def.allDay && nextDayThreshold) ? + computeVisibleDayRange(origRange, nextDayThreshold) : + origRange; + var slicedRange = intersectRanges(normalRange, framingRange); + if (slicedRange) { + if (ui.display === 'inverse-background') { + if (def.groupId) { + inverseBgByGroupId[def.groupId].push(slicedRange); + } + else { + inverseBgByDefId[instance.defId].push(slicedRange); + } + } + else if (ui.display !== 'none') { + (ui.display === 'background' ? bgRanges : fgRanges).push({ + def: def, + ui: ui, + instance: instance, + range: slicedRange, + isStart: normalRange.start && normalRange.start.valueOf() === slicedRange.start.valueOf(), + isEnd: normalRange.end && normalRange.end.valueOf() === slicedRange.end.valueOf(), + }); + } + } + } + for (var groupId in inverseBgByGroupId) { // BY GROUP + var ranges = inverseBgByGroupId[groupId]; + var invertedRanges = invertRanges(ranges, framingRange); + for (var _i = 0, invertedRanges_1 = invertedRanges; _i < invertedRanges_1.length; _i++) { + var invertedRange = invertedRanges_1[_i]; + var def = defByGroupId[groupId]; + var ui = eventUis[def.defId]; + bgRanges.push({ + def: def, + ui: ui, + instance: null, + range: invertedRange, + isStart: false, + isEnd: false, + }); + } + } + for (var defId in inverseBgByDefId) { + var ranges = inverseBgByDefId[defId]; + var invertedRanges = invertRanges(ranges, framingRange); + for (var _a = 0, invertedRanges_2 = invertedRanges; _a < invertedRanges_2.length; _a++) { + var invertedRange = invertedRanges_2[_a]; + bgRanges.push({ + def: eventStore.defs[defId], + ui: eventUis[defId], + instance: null, + range: invertedRange, + isStart: false, + isEnd: false, + }); + } + } + return { bg: bgRanges, fg: fgRanges }; + } + function hasBgRendering(def) { + return def.ui.display === 'background' || def.ui.display === 'inverse-background'; + } + function setElSeg(el, seg) { + el.fcSeg = seg; + } + function getElSeg(el) { + return el.fcSeg || + el.parentNode.fcSeg || // for the harness + null; + } + // event ui computation + function compileEventUis(eventDefs, eventUiBases) { + return mapHash(eventDefs, function (eventDef) { return compileEventUi(eventDef, eventUiBases); }); + } + function compileEventUi(eventDef, eventUiBases) { + var uis = []; + if (eventUiBases['']) { + uis.push(eventUiBases['']); + } + if (eventUiBases[eventDef.defId]) { + uis.push(eventUiBases[eventDef.defId]); + } + uis.push(eventDef.ui); + return combineEventUis(uis); + } + function sortEventSegs(segs, eventOrderSpecs) { + var objs = segs.map(buildSegCompareObj); + objs.sort(function (obj0, obj1) { return compareByFieldSpecs(obj0, obj1, eventOrderSpecs); }); + return objs.map(function (c) { return c._seg; }); + } + // returns a object with all primitive props that can be compared + function buildSegCompareObj(seg) { + var eventRange = seg.eventRange; + var eventDef = eventRange.def; + var range = eventRange.instance ? eventRange.instance.range : eventRange.range; + var start = range.start ? range.start.valueOf() : 0; // TODO: better support for open-range events + var end = range.end ? range.end.valueOf() : 0; // " + return __assign(__assign(__assign({}, eventDef.extendedProps), eventDef), { id: eventDef.publicId, start: start, + end: end, duration: end - start, allDay: Number(eventDef.allDay), _seg: seg }); + } + function computeSegDraggable(seg, context) { + var pluginHooks = context.pluginHooks; + var transformers = pluginHooks.isDraggableTransformers; + var _a = seg.eventRange, def = _a.def, ui = _a.ui; + var val = ui.startEditable; + for (var _i = 0, transformers_1 = transformers; _i < transformers_1.length; _i++) { + var transformer = transformers_1[_i]; + val = transformer(val, def, ui, context); + } + return val; + } + function computeSegStartResizable(seg, context) { + return seg.isStart && seg.eventRange.ui.durationEditable && context.options.eventResizableFromStart; + } + function computeSegEndResizable(seg, context) { + return seg.isEnd && seg.eventRange.ui.durationEditable; + } + function buildSegTimeText(seg, timeFormat, context, defaultDisplayEventTime, // defaults to true + defaultDisplayEventEnd, // defaults to true + startOverride, endOverride) { + var dateEnv = context.dateEnv, options = context.options; + var displayEventTime = options.displayEventTime, displayEventEnd = options.displayEventEnd; + var eventDef = seg.eventRange.def; + var eventInstance = seg.eventRange.instance; + if (displayEventTime == null) { + displayEventTime = defaultDisplayEventTime !== false; + } + if (displayEventEnd == null) { + displayEventEnd = defaultDisplayEventEnd !== false; + } + if (displayEventTime && !eventDef.allDay && (seg.isStart || seg.isEnd)) { + var segStart = startOverride || (seg.isStart ? eventInstance.range.start : (seg.start || seg.eventRange.range.start)); + var segEnd = endOverride || (seg.isEnd ? eventInstance.range.end : (seg.end || seg.eventRange.range.end)); + if (displayEventEnd && eventDef.hasEnd) { + return dateEnv.formatRange(segStart, segEnd, timeFormat, { + forcedStartTzo: startOverride ? null : eventInstance.forcedStartTzo, + forcedEndTzo: endOverride ? null : eventInstance.forcedEndTzo, + }); + } + return dateEnv.format(segStart, timeFormat, { + forcedTzo: startOverride ? null : eventInstance.forcedStartTzo, + }); + } + return ''; + } + function getSegMeta(seg, todayRange, nowDate) { + var segRange = seg.eventRange.range; + return { + isPast: segRange.end < (nowDate || todayRange.start), + isFuture: segRange.start >= (nowDate || todayRange.end), + isToday: todayRange && rangeContainsMarker(todayRange, segRange.start), + }; + } + function getEventClassNames(props) { + var classNames = ['fc-event']; + if (props.isMirror) { + classNames.push('fc-event-mirror'); + } + if (props.isDraggable) { + classNames.push('fc-event-draggable'); + } + if (props.isStartResizable || props.isEndResizable) { + classNames.push('fc-event-resizable'); + } + if (props.isDragging) { + classNames.push('fc-event-dragging'); + } + if (props.isResizing) { + classNames.push('fc-event-resizing'); + } + if (props.isSelected) { + classNames.push('fc-event-selected'); + } + if (props.isStart) { + classNames.push('fc-event-start'); + } + if (props.isEnd) { + classNames.push('fc-event-end'); + } + if (props.isPast) { + classNames.push('fc-event-past'); + } + if (props.isToday) { + classNames.push('fc-event-today'); + } + if (props.isFuture) { + classNames.push('fc-event-future'); + } + return classNames; + } + function buildEventRangeKey(eventRange) { + return eventRange.instance + ? eventRange.instance.instanceId + : eventRange.def.defId + ":" + eventRange.range.start.toISOString(); + // inverse-background events don't have specific instances. TODO: better solution + } + + var STANDARD_PROPS = { + start: identity, + end: identity, + allDay: Boolean, + }; + function parseDateSpan(raw, dateEnv, defaultDuration) { + var span = parseOpenDateSpan(raw, dateEnv); + var range = span.range; + if (!range.start) { + return null; + } + if (!range.end) { + if (defaultDuration == null) { + return null; + } + range.end = dateEnv.add(range.start, defaultDuration); + } + return span; + } + /* + TODO: somehow combine with parseRange? + Will return null if the start/end props were present but parsed invalidly. + */ + function parseOpenDateSpan(raw, dateEnv) { + var _a = refineProps(raw, STANDARD_PROPS), standardProps = _a.refined, extra = _a.extra; + var startMeta = standardProps.start ? dateEnv.createMarkerMeta(standardProps.start) : null; + var endMeta = standardProps.end ? dateEnv.createMarkerMeta(standardProps.end) : null; + var allDay = standardProps.allDay; + if (allDay == null) { + allDay = (startMeta && startMeta.isTimeUnspecified) && + (!endMeta || endMeta.isTimeUnspecified); + } + return __assign({ range: { + start: startMeta ? startMeta.marker : null, + end: endMeta ? endMeta.marker : null, + }, allDay: allDay }, extra); + } + function isDateSpansEqual(span0, span1) { + return rangesEqual(span0.range, span1.range) && + span0.allDay === span1.allDay && + isSpanPropsEqual(span0, span1); + } + // the NON-DATE-RELATED props + function isSpanPropsEqual(span0, span1) { + for (var propName in span1) { + if (propName !== 'range' && propName !== 'allDay') { + if (span0[propName] !== span1[propName]) { + return false; + } + } + } + // are there any props that span0 has that span1 DOESN'T have? + // both have range/allDay, so no need to special-case. + for (var propName in span0) { + if (!(propName in span1)) { + return false; + } + } + return true; + } + function buildDateSpanApi(span, dateEnv) { + return __assign(__assign({}, buildRangeApi(span.range, dateEnv, span.allDay)), { allDay: span.allDay }); + } + function buildRangeApiWithTimeZone(range, dateEnv, omitTime) { + return __assign(__assign({}, buildRangeApi(range, dateEnv, omitTime)), { timeZone: dateEnv.timeZone }); + } + function buildRangeApi(range, dateEnv, omitTime) { + return { + start: dateEnv.toDate(range.start), + end: dateEnv.toDate(range.end), + startStr: dateEnv.formatIso(range.start, { omitTime: omitTime }), + endStr: dateEnv.formatIso(range.end, { omitTime: omitTime }), + }; + } + function fabricateEventRange(dateSpan, eventUiBases, context) { + var res = refineEventDef({ editable: false }, context); + var def = parseEventDef(res.refined, res.extra, '', // sourceId + dateSpan.allDay, true, // hasEnd + context); + return { + def: def, + ui: compileEventUi(def, eventUiBases), + instance: createEventInstance(def.defId, dateSpan.range), + range: dateSpan.range, + isStart: true, + isEnd: true, + }; + } + + function triggerDateSelect(selection, pev, context) { + context.emitter.trigger('select', __assign(__assign({}, buildDateSpanApiWithContext(selection, context)), { jsEvent: pev ? pev.origEvent : null, view: context.viewApi || context.calendarApi.view })); + } + function triggerDateUnselect(pev, context) { + context.emitter.trigger('unselect', { + jsEvent: pev ? pev.origEvent : null, + view: context.viewApi || context.calendarApi.view, + }); + } + function buildDateSpanApiWithContext(dateSpan, context) { + var props = {}; + for (var _i = 0, _a = context.pluginHooks.dateSpanTransforms; _i < _a.length; _i++) { + var transform = _a[_i]; + __assign(props, transform(dateSpan, context)); + } + __assign(props, buildDateSpanApi(dateSpan, context.dateEnv)); + return props; + } + // Given an event's allDay status and start date, return what its fallback end date should be. + // TODO: rename to computeDefaultEventEnd + function getDefaultEventEnd(allDay, marker, context) { + var dateEnv = context.dateEnv, options = context.options; + var end = marker; + if (allDay) { + end = startOfDay(end); + end = dateEnv.add(end, options.defaultAllDayEventDuration); + } + else { + end = dateEnv.add(end, options.defaultTimedEventDuration); + } + return end; + } + + // applies the mutation to ALL defs/instances within the event store + function applyMutationToEventStore(eventStore, eventConfigBase, mutation, context) { + var eventConfigs = compileEventUis(eventStore.defs, eventConfigBase); + var dest = createEmptyEventStore(); + for (var defId in eventStore.defs) { + var def = eventStore.defs[defId]; + dest.defs[defId] = applyMutationToEventDef(def, eventConfigs[defId], mutation, context); + } + for (var instanceId in eventStore.instances) { + var instance = eventStore.instances[instanceId]; + var def = dest.defs[instance.defId]; // important to grab the newly modified def + dest.instances[instanceId] = applyMutationToEventInstance(instance, def, eventConfigs[instance.defId], mutation, context); + } + return dest; + } + function applyMutationToEventDef(eventDef, eventConfig, mutation, context) { + var standardProps = mutation.standardProps || {}; + // if hasEnd has not been specified, guess a good value based on deltas. + // if duration will change, there's no way the default duration will persist, + // and thus, we need to mark the event as having a real end + if (standardProps.hasEnd == null && + eventConfig.durationEditable && + (mutation.startDelta || mutation.endDelta)) { + standardProps.hasEnd = true; // TODO: is this mutation okay? + } + var copy = __assign(__assign(__assign({}, eventDef), standardProps), { ui: __assign(__assign({}, eventDef.ui), standardProps.ui) }); + if (mutation.extendedProps) { + copy.extendedProps = __assign(__assign({}, copy.extendedProps), mutation.extendedProps); + } + for (var _i = 0, _a = context.pluginHooks.eventDefMutationAppliers; _i < _a.length; _i++) { + var applier = _a[_i]; + applier(copy, mutation, context); + } + if (!copy.hasEnd && context.options.forceEventDuration) { + copy.hasEnd = true; + } + return copy; + } + function applyMutationToEventInstance(eventInstance, eventDef, // must first be modified by applyMutationToEventDef + eventConfig, mutation, context) { + var dateEnv = context.dateEnv; + var forceAllDay = mutation.standardProps && mutation.standardProps.allDay === true; + var clearEnd = mutation.standardProps && mutation.standardProps.hasEnd === false; + var copy = __assign({}, eventInstance); + if (forceAllDay) { + copy.range = computeAlignedDayRange(copy.range); + } + if (mutation.datesDelta && eventConfig.startEditable) { + copy.range = { + start: dateEnv.add(copy.range.start, mutation.datesDelta), + end: dateEnv.add(copy.range.end, mutation.datesDelta), + }; + } + if (mutation.startDelta && eventConfig.durationEditable) { + copy.range = { + start: dateEnv.add(copy.range.start, mutation.startDelta), + end: copy.range.end, + }; + } + if (mutation.endDelta && eventConfig.durationEditable) { + copy.range = { + start: copy.range.start, + end: dateEnv.add(copy.range.end, mutation.endDelta), + }; + } + if (clearEnd) { + copy.range = { + start: copy.range.start, + end: getDefaultEventEnd(eventDef.allDay, copy.range.start, context), + }; + } + // in case event was all-day but the supplied deltas were not + // better util for this? + if (eventDef.allDay) { + copy.range = { + start: startOfDay(copy.range.start), + end: startOfDay(copy.range.end), + }; + } + // handle invalid durations + if (copy.range.end < copy.range.start) { + copy.range.end = getDefaultEventEnd(eventDef.allDay, copy.range.start, context); + } + return copy; + } + + // no public types yet. when there are, export from: + // import {} from './api-type-deps' + var ViewApi = /** @class */ (function () { + function ViewApi(type, getCurrentData, dateEnv) { + this.type = type; + this.getCurrentData = getCurrentData; + this.dateEnv = dateEnv; + } + Object.defineProperty(ViewApi.prototype, "calendar", { + get: function () { + return this.getCurrentData().calendarApi; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(ViewApi.prototype, "title", { + get: function () { + return this.getCurrentData().viewTitle; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(ViewApi.prototype, "activeStart", { + get: function () { + return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.start); + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(ViewApi.prototype, "activeEnd", { + get: function () { + return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.end); + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(ViewApi.prototype, "currentStart", { + get: function () { + return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.start); + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(ViewApi.prototype, "currentEnd", { + get: function () { + return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.end); + }, + enumerable: false, + configurable: true + }); + ViewApi.prototype.getOption = function (name) { + return this.getCurrentData().options[name]; // are the view-specific options + }; + return ViewApi; + }()); + + var EVENT_SOURCE_REFINERS = { + id: String, + defaultAllDay: Boolean, + url: String, + format: String, + events: identity, + eventDataTransform: identity, + // for any network-related sources + success: identity, + failure: identity, + }; + function parseEventSource(raw, context, refiners) { + if (refiners === void 0) { refiners = buildEventSourceRefiners(context); } + var rawObj; + if (typeof raw === 'string') { + rawObj = { url: raw }; + } + else if (typeof raw === 'function' || Array.isArray(raw)) { + rawObj = { events: raw }; + } + else if (typeof raw === 'object' && raw) { // not null + rawObj = raw; + } + if (rawObj) { + var _a = refineProps(rawObj, refiners), refined = _a.refined, extra = _a.extra; + var metaRes = buildEventSourceMeta(refined, context); + if (metaRes) { + return { + _raw: raw, + isFetching: false, + latestFetchId: '', + fetchRange: null, + defaultAllDay: refined.defaultAllDay, + eventDataTransform: refined.eventDataTransform, + success: refined.success, + failure: refined.failure, + publicId: refined.id || '', + sourceId: guid(), + sourceDefId: metaRes.sourceDefId, + meta: metaRes.meta, + ui: createEventUi(refined, context), + extendedProps: extra, + }; + } + } + return null; + } + function buildEventSourceRefiners(context) { + return __assign(__assign(__assign({}, EVENT_UI_REFINERS), EVENT_SOURCE_REFINERS), context.pluginHooks.eventSourceRefiners); + } + function buildEventSourceMeta(raw, context) { + var defs = context.pluginHooks.eventSourceDefs; + for (var i = defs.length - 1; i >= 0; i -= 1) { // later-added plugins take precedence + var def = defs[i]; + var meta = def.parseMeta(raw); + if (meta) { + return { sourceDefId: i, meta: meta }; + } + } + return null; + } + + function reduceCurrentDate(currentDate, action) { + switch (action.type) { + case 'CHANGE_DATE': + return action.dateMarker; + default: + return currentDate; + } + } + function getInitialDate(options, dateEnv) { + var initialDateInput = options.initialDate; + // compute the initial ambig-timezone date + if (initialDateInput != null) { + return dateEnv.createMarker(initialDateInput); + } + return getNow(options.now, dateEnv); // getNow already returns unzoned + } + function getNow(nowInput, dateEnv) { + if (typeof nowInput === 'function') { + nowInput = nowInput(); + } + if (nowInput == null) { + return dateEnv.createNowMarker(); + } + return dateEnv.createMarker(nowInput); + } + + var CalendarApi = /** @class */ (function () { + function CalendarApi() { + } + CalendarApi.prototype.getCurrentData = function () { + return this.currentDataManager.getCurrentData(); + }; + CalendarApi.prototype.dispatch = function (action) { + return this.currentDataManager.dispatch(action); + }; + Object.defineProperty(CalendarApi.prototype, "view", { + get: function () { return this.getCurrentData().viewApi; } // for public API + , + enumerable: false, + configurable: true + }); + CalendarApi.prototype.batchRendering = function (callback) { + callback(); + }; + CalendarApi.prototype.updateSize = function () { + this.trigger('_resize', true); + }; + // Options + // ----------------------------------------------------------------------------------------------------------------- + CalendarApi.prototype.setOption = function (name, val) { + this.dispatch({ + type: 'SET_OPTION', + optionName: name, + rawOptionValue: val, + }); + }; + CalendarApi.prototype.getOption = function (name) { + return this.currentDataManager.currentCalendarOptionsInput[name]; + }; + CalendarApi.prototype.getAvailableLocaleCodes = function () { + return Object.keys(this.getCurrentData().availableRawLocales); + }; + // Trigger + // ----------------------------------------------------------------------------------------------------------------- + CalendarApi.prototype.on = function (handlerName, handler) { + var currentDataManager = this.currentDataManager; + if (currentDataManager.currentCalendarOptionsRefiners[handlerName]) { + currentDataManager.emitter.on(handlerName, handler); + } + else { + console.warn("Unknown listener name '" + handlerName + "'"); + } + }; + CalendarApi.prototype.off = function (handlerName, handler) { + this.currentDataManager.emitter.off(handlerName, handler); + }; + // not meant for public use + CalendarApi.prototype.trigger = function (handlerName) { + var _a; + var args = []; + for (var _i = 1; _i < arguments.length; _i++) { + args[_i - 1] = arguments[_i]; + } + (_a = this.currentDataManager.emitter).trigger.apply(_a, __spreadArrays([handlerName], args)); + }; + // View + // ----------------------------------------------------------------------------------------------------------------- + CalendarApi.prototype.changeView = function (viewType, dateOrRange) { + var _this = this; + this.batchRendering(function () { + _this.unselect(); + if (dateOrRange) { + if (dateOrRange.start && dateOrRange.end) { // a range + _this.dispatch({ + type: 'CHANGE_VIEW_TYPE', + viewType: viewType, + }); + _this.dispatch({ + type: 'SET_OPTION', + optionName: 'visibleRange', + rawOptionValue: dateOrRange, + }); + } + else { + var dateEnv = _this.getCurrentData().dateEnv; + _this.dispatch({ + type: 'CHANGE_VIEW_TYPE', + viewType: viewType, + dateMarker: dateEnv.createMarker(dateOrRange), + }); + } + } + else { + _this.dispatch({ + type: 'CHANGE_VIEW_TYPE', + viewType: viewType, + }); + } + }); + }; + // Forces navigation to a view for the given date. + // `viewType` can be a specific view name or a generic one like "week" or "day". + // needs to change + CalendarApi.prototype.zoomTo = function (dateMarker, viewType) { + var state = this.getCurrentData(); + var spec; + viewType = viewType || 'day'; // day is default zoom + spec = state.viewSpecs[viewType] || this.getUnitViewSpec(viewType); + this.unselect(); + if (spec) { + this.dispatch({ + type: 'CHANGE_VIEW_TYPE', + viewType: spec.type, + dateMarker: dateMarker, + }); + } + else { + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker: dateMarker, + }); + } + }; + // Given a duration singular unit, like "week" or "day", finds a matching view spec. + // Preference is given to views that have corresponding buttons. + CalendarApi.prototype.getUnitViewSpec = function (unit) { + var _a = this.getCurrentData(), viewSpecs = _a.viewSpecs, toolbarConfig = _a.toolbarConfig; + var viewTypes = [].concat(toolbarConfig.viewsWithButtons); + var i; + var spec; + for (var viewType in viewSpecs) { + viewTypes.push(viewType); + } + for (i = 0; i < viewTypes.length; i += 1) { + spec = viewSpecs[viewTypes[i]]; + if (spec) { + if (spec.singleUnit === unit) { + return spec; + } + } + } + return null; + }; + // Current Date + // ----------------------------------------------------------------------------------------------------------------- + CalendarApi.prototype.prev = function () { + this.unselect(); + this.dispatch({ type: 'PREV' }); + }; + CalendarApi.prototype.next = function () { + this.unselect(); + this.dispatch({ type: 'NEXT' }); + }; + CalendarApi.prototype.prevYear = function () { + var state = this.getCurrentData(); + this.unselect(); + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker: state.dateEnv.addYears(state.currentDate, -1), + }); + }; + CalendarApi.prototype.nextYear = function () { + var state = this.getCurrentData(); + this.unselect(); + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker: state.dateEnv.addYears(state.currentDate, 1), + }); + }; + CalendarApi.prototype.today = function () { + var state = this.getCurrentData(); + this.unselect(); + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker: getNow(state.calendarOptions.now, state.dateEnv), + }); + }; + CalendarApi.prototype.gotoDate = function (zonedDateInput) { + var state = this.getCurrentData(); + this.unselect(); + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker: state.dateEnv.createMarker(zonedDateInput), + }); + }; + CalendarApi.prototype.incrementDate = function (deltaInput) { + var state = this.getCurrentData(); + var delta = createDuration(deltaInput); + if (delta) { // else, warn about invalid input? + this.unselect(); + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker: state.dateEnv.add(state.currentDate, delta), + }); + } + }; + // for external API + CalendarApi.prototype.getDate = function () { + var state = this.getCurrentData(); + return state.dateEnv.toDate(state.currentDate); + }; + // Date Formatting Utils + // ----------------------------------------------------------------------------------------------------------------- + CalendarApi.prototype.formatDate = function (d, formatter) { + var dateEnv = this.getCurrentData().dateEnv; + return dateEnv.format(dateEnv.createMarker(d), createFormatter(formatter)); + }; + // `settings` is for formatter AND isEndExclusive + CalendarApi.prototype.formatRange = function (d0, d1, settings) { + var dateEnv = this.getCurrentData().dateEnv; + return dateEnv.formatRange(dateEnv.createMarker(d0), dateEnv.createMarker(d1), createFormatter(settings), settings); + }; + CalendarApi.prototype.formatIso = function (d, omitTime) { + var dateEnv = this.getCurrentData().dateEnv; + return dateEnv.formatIso(dateEnv.createMarker(d), { omitTime: omitTime }); + }; + // Date Selection / Event Selection / DayClick + // ----------------------------------------------------------------------------------------------------------------- + // this public method receives start/end dates in any format, with any timezone + // NOTE: args were changed from v3 + CalendarApi.prototype.select = function (dateOrObj, endDate) { + var selectionInput; + if (endDate == null) { + if (dateOrObj.start != null) { + selectionInput = dateOrObj; + } + else { + selectionInput = { + start: dateOrObj, + end: null, + }; + } + } + else { + selectionInput = { + start: dateOrObj, + end: endDate, + }; + } + var state = this.getCurrentData(); + var selection = parseDateSpan(selectionInput, state.dateEnv, createDuration({ days: 1 })); + if (selection) { // throw parse error otherwise? + this.dispatch({ type: 'SELECT_DATES', selection: selection }); + triggerDateSelect(selection, null, state); + } + }; + // public method + CalendarApi.prototype.unselect = function (pev) { + var state = this.getCurrentData(); + if (state.dateSelection) { + this.dispatch({ type: 'UNSELECT_DATES' }); + triggerDateUnselect(pev, state); + } + }; + // Public Events API + // ----------------------------------------------------------------------------------------------------------------- + CalendarApi.prototype.addEvent = function (eventInput, sourceInput) { + if (eventInput instanceof EventApi) { + var def = eventInput._def; + var instance = eventInput._instance; + var currentData = this.getCurrentData(); + // not already present? don't want to add an old snapshot + if (!currentData.eventStore.defs[def.defId]) { + this.dispatch({ + type: 'ADD_EVENTS', + eventStore: eventTupleToStore({ def: def, instance: instance }), + }); + this.triggerEventAdd(eventInput); + } + return eventInput; + } + var state = this.getCurrentData(); + var eventSource; + if (sourceInput instanceof EventSourceApi) { + eventSource = sourceInput.internalEventSource; + } + else if (typeof sourceInput === 'boolean') { + if (sourceInput) { // true. part of the first event source + eventSource = hashValuesToArray(state.eventSources)[0]; + } + } + else if (sourceInput != null) { // an ID. accepts a number too + var sourceApi = this.getEventSourceById(sourceInput); // TODO: use an internal function + if (!sourceApi) { + console.warn("Could not find an event source with ID \"" + sourceInput + "\""); // TODO: test + return null; + } + eventSource = sourceApi.internalEventSource; + } + var tuple = parseEvent(eventInput, eventSource, state, false); + if (tuple) { + var newEventApi = new EventApi(state, tuple.def, tuple.def.recurringDef ? null : tuple.instance); + this.dispatch({ + type: 'ADD_EVENTS', + eventStore: eventTupleToStore(tuple), + }); + this.triggerEventAdd(newEventApi); + return newEventApi; + } + return null; + }; + CalendarApi.prototype.triggerEventAdd = function (eventApi) { + var _this = this; + var emitter = this.getCurrentData().emitter; + emitter.trigger('eventAdd', { + event: eventApi, + relatedEvents: [], + revert: function () { + _this.dispatch({ + type: 'REMOVE_EVENTS', + eventStore: eventApiToStore(eventApi), + }); + }, + }); + }; + // TODO: optimize + CalendarApi.prototype.getEventById = function (id) { + var state = this.getCurrentData(); + var _a = state.eventStore, defs = _a.defs, instances = _a.instances; + id = String(id); + for (var defId in defs) { + var def = defs[defId]; + if (def.publicId === id) { + if (def.recurringDef) { + return new EventApi(state, def, null); + } + for (var instanceId in instances) { + var instance = instances[instanceId]; + if (instance.defId === def.defId) { + return new EventApi(state, def, instance); + } + } + } + } + return null; + }; + CalendarApi.prototype.getEvents = function () { + var currentData = this.getCurrentData(); + return buildEventApis(currentData.eventStore, currentData); + }; + CalendarApi.prototype.removeAllEvents = function () { + this.dispatch({ type: 'REMOVE_ALL_EVENTS' }); + }; + // Public Event Sources API + // ----------------------------------------------------------------------------------------------------------------- + CalendarApi.prototype.getEventSources = function () { + var state = this.getCurrentData(); + var sourceHash = state.eventSources; + var sourceApis = []; + for (var internalId in sourceHash) { + sourceApis.push(new EventSourceApi(state, sourceHash[internalId])); + } + return sourceApis; + }; + CalendarApi.prototype.getEventSourceById = function (id) { + var state = this.getCurrentData(); + var sourceHash = state.eventSources; + id = String(id); + for (var sourceId in sourceHash) { + if (sourceHash[sourceId].publicId === id) { + return new EventSourceApi(state, sourceHash[sourceId]); + } + } + return null; + }; + CalendarApi.prototype.addEventSource = function (sourceInput) { + var state = this.getCurrentData(); + if (sourceInput instanceof EventSourceApi) { + // not already present? don't want to add an old snapshot + if (!state.eventSources[sourceInput.internalEventSource.sourceId]) { + this.dispatch({ + type: 'ADD_EVENT_SOURCES', + sources: [sourceInput.internalEventSource], + }); + } + return sourceInput; + } + var eventSource = parseEventSource(sourceInput, state); + if (eventSource) { // TODO: error otherwise? + this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: [eventSource] }); + return new EventSourceApi(state, eventSource); + } + return null; + }; + CalendarApi.prototype.removeAllEventSources = function () { + this.dispatch({ type: 'REMOVE_ALL_EVENT_SOURCES' }); + }; + CalendarApi.prototype.refetchEvents = function () { + this.dispatch({ type: 'FETCH_EVENT_SOURCES' }); + }; + // Scroll + // ----------------------------------------------------------------------------------------------------------------- + CalendarApi.prototype.scrollToTime = function (timeInput) { + var time = createDuration(timeInput); + if (time) { + this.trigger('_scrollRequest', { time: time }); + } + }; + return CalendarApi; + }()); + + var EventApi = /** @class */ (function () { + // instance will be null if expressing a recurring event that has no current instances, + // OR if trying to validate an incoming external event that has no dates assigned + function EventApi(context, def, instance) { + this._context = context; + this._def = def; + this._instance = instance || null; + } + /* + TODO: make event struct more responsible for this + */ + EventApi.prototype.setProp = function (name, val) { + var _a, _b; + if (name in EVENT_DATE_REFINERS) { + console.warn('Could not set date-related prop \'name\'. Use one of the date-related methods instead.'); + } + else if (name in EVENT_NON_DATE_REFINERS) { + val = EVENT_NON_DATE_REFINERS[name](val); + this.mutate({ + standardProps: (_a = {}, _a[name] = val, _a), + }); + } + else if (name in EVENT_UI_REFINERS) { + var ui = EVENT_UI_REFINERS[name](val); + if (name === 'color') { + ui = { backgroundColor: val, borderColor: val }; + } + else if (name === 'editable') { + ui = { startEditable: val, durationEditable: val }; + } + else { + ui = (_b = {}, _b[name] = val, _b); + } + this.mutate({ + standardProps: { ui: ui }, + }); + } + else { + console.warn("Could not set prop '" + name + "'. Use setExtendedProp instead."); + } + }; + EventApi.prototype.setExtendedProp = function (name, val) { + var _a; + this.mutate({ + extendedProps: (_a = {}, _a[name] = val, _a), + }); + }; + EventApi.prototype.setStart = function (startInput, options) { + if (options === void 0) { options = {}; } + var dateEnv = this._context.dateEnv; + var start = dateEnv.createMarker(startInput); + if (start && this._instance) { // TODO: warning if parsed bad + var instanceRange = this._instance.range; + var startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity); // what if parsed bad!? + if (options.maintainDuration) { + this.mutate({ datesDelta: startDelta }); + } + else { + this.mutate({ startDelta: startDelta }); + } + } + }; + EventApi.prototype.setEnd = function (endInput, options) { + if (options === void 0) { options = {}; } + var dateEnv = this._context.dateEnv; + var end; + if (endInput != null) { + end = dateEnv.createMarker(endInput); + if (!end) { + return; // TODO: warning if parsed bad + } + } + if (this._instance) { + if (end) { + var endDelta = diffDates(this._instance.range.end, end, dateEnv, options.granularity); + this.mutate({ endDelta: endDelta }); + } + else { + this.mutate({ standardProps: { hasEnd: false } }); + } + } + }; + EventApi.prototype.setDates = function (startInput, endInput, options) { + if (options === void 0) { options = {}; } + var dateEnv = this._context.dateEnv; + var standardProps = { allDay: options.allDay }; + var start = dateEnv.createMarker(startInput); + var end; + if (!start) { + return; // TODO: warning if parsed bad + } + if (endInput != null) { + end = dateEnv.createMarker(endInput); + if (!end) { // TODO: warning if parsed bad + return; + } + } + if (this._instance) { + var instanceRange = this._instance.range; + // when computing the diff for an event being converted to all-day, + // compute diff off of the all-day values the way event-mutation does. + if (options.allDay === true) { + instanceRange = computeAlignedDayRange(instanceRange); + } + var startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity); + if (end) { + var endDelta = diffDates(instanceRange.end, end, dateEnv, options.granularity); + if (durationsEqual(startDelta, endDelta)) { + this.mutate({ datesDelta: startDelta, standardProps: standardProps }); + } + else { + this.mutate({ startDelta: startDelta, endDelta: endDelta, standardProps: standardProps }); + } + } + else { // means "clear the end" + standardProps.hasEnd = false; + this.mutate({ datesDelta: startDelta, standardProps: standardProps }); + } + } + }; + EventApi.prototype.moveStart = function (deltaInput) { + var delta = createDuration(deltaInput); + if (delta) { // TODO: warning if parsed bad + this.mutate({ startDelta: delta }); + } + }; + EventApi.prototype.moveEnd = function (deltaInput) { + var delta = createDuration(deltaInput); + if (delta) { // TODO: warning if parsed bad + this.mutate({ endDelta: delta }); + } + }; + EventApi.prototype.moveDates = function (deltaInput) { + var delta = createDuration(deltaInput); + if (delta) { // TODO: warning if parsed bad + this.mutate({ datesDelta: delta }); + } + }; + EventApi.prototype.setAllDay = function (allDay, options) { + if (options === void 0) { options = {}; } + var standardProps = { allDay: allDay }; + var maintainDuration = options.maintainDuration; + if (maintainDuration == null) { + maintainDuration = this._context.options.allDayMaintainDuration; + } + if (this._def.allDay !== allDay) { + standardProps.hasEnd = maintainDuration; + } + this.mutate({ standardProps: standardProps }); + }; + EventApi.prototype.formatRange = function (formatInput) { + var dateEnv = this._context.dateEnv; + var instance = this._instance; + var formatter = createFormatter(formatInput); + if (this._def.hasEnd) { + return dateEnv.formatRange(instance.range.start, instance.range.end, formatter, { + forcedStartTzo: instance.forcedStartTzo, + forcedEndTzo: instance.forcedEndTzo, + }); + } + return dateEnv.format(instance.range.start, formatter, { + forcedTzo: instance.forcedStartTzo, + }); + }; + EventApi.prototype.mutate = function (mutation) { + var instance = this._instance; + if (instance) { + var def = this._def; + var context_1 = this._context; + var eventStore_1 = context_1.getCurrentData().eventStore; + var relevantEvents = getRelevantEvents(eventStore_1, instance.instanceId); + var eventConfigBase = { + '': { + display: '', + startEditable: true, + durationEditable: true, + constraints: [], + overlap: null, + allows: [], + backgroundColor: '', + borderColor: '', + textColor: '', + classNames: [], + }, + }; + relevantEvents = applyMutationToEventStore(relevantEvents, eventConfigBase, mutation, context_1); + var oldEvent = new EventApi(context_1, def, instance); // snapshot + this._def = relevantEvents.defs[def.defId]; + this._instance = relevantEvents.instances[instance.instanceId]; + context_1.dispatch({ + type: 'MERGE_EVENTS', + eventStore: relevantEvents, + }); + context_1.emitter.trigger('eventChange', { + oldEvent: oldEvent, + event: this, + relatedEvents: buildEventApis(relevantEvents, context_1, instance), + revert: function () { + context_1.dispatch({ + type: 'RESET_EVENTS', + eventStore: eventStore_1, + }); + }, + }); + } + }; + EventApi.prototype.remove = function () { + var context = this._context; + var asStore = eventApiToStore(this); + context.dispatch({ + type: 'REMOVE_EVENTS', + eventStore: asStore, + }); + context.emitter.trigger('eventRemove', { + event: this, + relatedEvents: [], + revert: function () { + context.dispatch({ + type: 'MERGE_EVENTS', + eventStore: asStore, + }); + }, + }); + }; + Object.defineProperty(EventApi.prototype, "source", { + get: function () { + var sourceId = this._def.sourceId; + if (sourceId) { + return new EventSourceApi(this._context, this._context.getCurrentData().eventSources[sourceId]); + } + return null; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "start", { + get: function () { + return this._instance ? + this._context.dateEnv.toDate(this._instance.range.start) : + null; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "end", { + get: function () { + return (this._instance && this._def.hasEnd) ? + this._context.dateEnv.toDate(this._instance.range.end) : + null; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "startStr", { + get: function () { + var instance = this._instance; + if (instance) { + return this._context.dateEnv.formatIso(instance.range.start, { + omitTime: this._def.allDay, + forcedTzo: instance.forcedStartTzo, + }); + } + return ''; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "endStr", { + get: function () { + var instance = this._instance; + if (instance && this._def.hasEnd) { + return this._context.dateEnv.formatIso(instance.range.end, { + omitTime: this._def.allDay, + forcedTzo: instance.forcedEndTzo, + }); + } + return ''; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "id", { + // computable props that all access the def + // TODO: find a TypeScript-compatible way to do this at scale + get: function () { return this._def.publicId; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "groupId", { + get: function () { return this._def.groupId; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "allDay", { + get: function () { return this._def.allDay; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "title", { + get: function () { return this._def.title; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "url", { + get: function () { return this._def.url; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "display", { + get: function () { return this._def.ui.display || 'auto'; } // bad. just normalize the type earlier + , + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "startEditable", { + get: function () { return this._def.ui.startEditable; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "durationEditable", { + get: function () { return this._def.ui.durationEditable; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "constraint", { + get: function () { return this._def.ui.constraints[0] || null; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "overlap", { + get: function () { return this._def.ui.overlap; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "allow", { + get: function () { return this._def.ui.allows[0] || null; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "backgroundColor", { + get: function () { return this._def.ui.backgroundColor; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "borderColor", { + get: function () { return this._def.ui.borderColor; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "textColor", { + get: function () { return this._def.ui.textColor; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "classNames", { + // NOTE: user can't modify these because Object.freeze was called in event-def parsing + get: function () { return this._def.ui.classNames; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "extendedProps", { + get: function () { return this._def.extendedProps; }, + enumerable: false, + configurable: true + }); + EventApi.prototype.toPlainObject = function (settings) { + if (settings === void 0) { settings = {}; } + var def = this._def; + var ui = def.ui; + var _a = this, startStr = _a.startStr, endStr = _a.endStr; + var res = {}; + if (def.title) { + res.title = def.title; + } + if (startStr) { + res.start = startStr; + } + if (endStr) { + res.end = endStr; + } + if (def.publicId) { + res.id = def.publicId; + } + if (def.groupId) { + res.groupId = def.groupId; + } + if (def.url) { + res.url = def.url; + } + if (ui.display && ui.display !== 'auto') { + res.display = ui.display; + } + // TODO: what about recurring-event properties??? + // TODO: include startEditable/durationEditable/constraint/overlap/allow + if (settings.collapseColor && ui.backgroundColor && ui.backgroundColor === ui.borderColor) { + res.color = ui.backgroundColor; + } + else { + if (ui.backgroundColor) { + res.backgroundColor = ui.backgroundColor; + } + if (ui.borderColor) { + res.borderColor = ui.borderColor; + } + } + if (ui.textColor) { + res.textColor = ui.textColor; + } + if (ui.classNames.length) { + res.classNames = ui.classNames; + } + if (Object.keys(def.extendedProps).length) { + if (settings.collapseExtendedProps) { + __assign(res, def.extendedProps); + } + else { + res.extendedProps = def.extendedProps; + } + } + return res; + }; + EventApi.prototype.toJSON = function () { + return this.toPlainObject(); + }; + return EventApi; + }()); + function eventApiToStore(eventApi) { + var _a, _b; + var def = eventApi._def; + var instance = eventApi._instance; + return { + defs: (_a = {}, _a[def.defId] = def, _a), + instances: instance + ? (_b = {}, _b[instance.instanceId] = instance, _b) : {}, + }; + } + function buildEventApis(eventStore, context, excludeInstance) { + var defs = eventStore.defs, instances = eventStore.instances; + var eventApis = []; + var excludeInstanceId = excludeInstance ? excludeInstance.instanceId : ''; + for (var id in instances) { + var instance = instances[id]; + var def = defs[instance.defId]; + if (instance.instanceId !== excludeInstanceId) { + eventApis.push(new EventApi(context, def, instance)); + } + } + return eventApis; + } + + var calendarSystemClassMap = {}; + function registerCalendarSystem(name, theClass) { + calendarSystemClassMap[name] = theClass; + } + function createCalendarSystem(name) { + return new calendarSystemClassMap[name](); + } + var GregorianCalendarSystem = /** @class */ (function () { + function GregorianCalendarSystem() { + } + GregorianCalendarSystem.prototype.getMarkerYear = function (d) { + return d.getUTCFullYear(); + }; + GregorianCalendarSystem.prototype.getMarkerMonth = function (d) { + return d.getUTCMonth(); + }; + GregorianCalendarSystem.prototype.getMarkerDay = function (d) { + return d.getUTCDate(); + }; + GregorianCalendarSystem.prototype.arrayToMarker = function (arr) { + return arrayToUtcDate(arr); + }; + GregorianCalendarSystem.prototype.markerToArray = function (marker) { + return dateToUtcArray(marker); + }; + return GregorianCalendarSystem; + }()); + registerCalendarSystem('gregory', GregorianCalendarSystem); + + var ISO_RE = /^\s*(\d{4})(-?(\d{2})(-?(\d{2})([T ](\d{2}):?(\d{2})(:?(\d{2})(\.(\d+))?)?(Z|(([-+])(\d{2})(:?(\d{2}))?))?)?)?)?$/; + function parse(str) { + var m = ISO_RE.exec(str); + if (m) { + var marker = new Date(Date.UTC(Number(m[1]), m[3] ? Number(m[3]) - 1 : 0, Number(m[5] || 1), Number(m[7] || 0), Number(m[8] || 0), Number(m[10] || 0), m[12] ? Number("0." + m[12]) * 1000 : 0)); + if (isValidDate(marker)) { + var timeZoneOffset = null; + if (m[13]) { + timeZoneOffset = (m[15] === '-' ? -1 : 1) * (Number(m[16] || 0) * 60 + + Number(m[18] || 0)); + } + return { + marker: marker, + isTimeUnspecified: !m[6], + timeZoneOffset: timeZoneOffset, + }; + } + } + return null; + } + + var DateEnv = /** @class */ (function () { + function DateEnv(settings) { + var timeZone = this.timeZone = settings.timeZone; + var isNamedTimeZone = timeZone !== 'local' && timeZone !== 'UTC'; + if (settings.namedTimeZoneImpl && isNamedTimeZone) { + this.namedTimeZoneImpl = new settings.namedTimeZoneImpl(timeZone); + } + this.canComputeOffset = Boolean(!isNamedTimeZone || this.namedTimeZoneImpl); + this.calendarSystem = createCalendarSystem(settings.calendarSystem); + this.locale = settings.locale; + this.weekDow = settings.locale.week.dow; + this.weekDoy = settings.locale.week.doy; + if (settings.weekNumberCalculation === 'ISO') { + this.weekDow = 1; + this.weekDoy = 4; + } + if (typeof settings.firstDay === 'number') { + this.weekDow = settings.firstDay; + } + if (typeof settings.weekNumberCalculation === 'function') { + this.weekNumberFunc = settings.weekNumberCalculation; + } + this.weekText = settings.weekText != null ? settings.weekText : settings.locale.options.weekText; + this.cmdFormatter = settings.cmdFormatter; + this.defaultSeparator = settings.defaultSeparator; + } + // Creating / Parsing + DateEnv.prototype.createMarker = function (input) { + var meta = this.createMarkerMeta(input); + if (meta === null) { + return null; + } + return meta.marker; + }; + DateEnv.prototype.createNowMarker = function () { + if (this.canComputeOffset) { + return this.timestampToMarker(new Date().valueOf()); + } + // if we can't compute the current date val for a timezone, + // better to give the current local date vals than UTC + return arrayToUtcDate(dateToLocalArray(new Date())); + }; + DateEnv.prototype.createMarkerMeta = function (input) { + if (typeof input === 'string') { + return this.parse(input); + } + var marker = null; + if (typeof input === 'number') { + marker = this.timestampToMarker(input); + } + else if (input instanceof Date) { + input = input.valueOf(); + if (!isNaN(input)) { + marker = this.timestampToMarker(input); + } + } + else if (Array.isArray(input)) { + marker = arrayToUtcDate(input); + } + if (marker === null || !isValidDate(marker)) { + return null; + } + return { marker: marker, isTimeUnspecified: false, forcedTzo: null }; + }; + DateEnv.prototype.parse = function (s) { + var parts = parse(s); + if (parts === null) { + return null; + } + var marker = parts.marker; + var forcedTzo = null; + if (parts.timeZoneOffset !== null) { + if (this.canComputeOffset) { + marker = this.timestampToMarker(marker.valueOf() - parts.timeZoneOffset * 60 * 1000); + } + else { + forcedTzo = parts.timeZoneOffset; + } + } + return { marker: marker, isTimeUnspecified: parts.isTimeUnspecified, forcedTzo: forcedTzo }; + }; + // Accessors + DateEnv.prototype.getYear = function (marker) { + return this.calendarSystem.getMarkerYear(marker); + }; + DateEnv.prototype.getMonth = function (marker) { + return this.calendarSystem.getMarkerMonth(marker); + }; + // Adding / Subtracting + DateEnv.prototype.add = function (marker, dur) { + var a = this.calendarSystem.markerToArray(marker); + a[0] += dur.years; + a[1] += dur.months; + a[2] += dur.days; + a[6] += dur.milliseconds; + return this.calendarSystem.arrayToMarker(a); + }; + DateEnv.prototype.subtract = function (marker, dur) { + var a = this.calendarSystem.markerToArray(marker); + a[0] -= dur.years; + a[1] -= dur.months; + a[2] -= dur.days; + a[6] -= dur.milliseconds; + return this.calendarSystem.arrayToMarker(a); + }; + DateEnv.prototype.addYears = function (marker, n) { + var a = this.calendarSystem.markerToArray(marker); + a[0] += n; + return this.calendarSystem.arrayToMarker(a); + }; + DateEnv.prototype.addMonths = function (marker, n) { + var a = this.calendarSystem.markerToArray(marker); + a[1] += n; + return this.calendarSystem.arrayToMarker(a); + }; + // Diffing Whole Units + DateEnv.prototype.diffWholeYears = function (m0, m1) { + var calendarSystem = this.calendarSystem; + if (timeAsMs(m0) === timeAsMs(m1) && + calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1) && + calendarSystem.getMarkerMonth(m0) === calendarSystem.getMarkerMonth(m1)) { + return calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0); + } + return null; + }; + DateEnv.prototype.diffWholeMonths = function (m0, m1) { + var calendarSystem = this.calendarSystem; + if (timeAsMs(m0) === timeAsMs(m1) && + calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1)) { + return (calendarSystem.getMarkerMonth(m1) - calendarSystem.getMarkerMonth(m0)) + + (calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0)) * 12; + } + return null; + }; + // Range / Duration + DateEnv.prototype.greatestWholeUnit = function (m0, m1) { + var n = this.diffWholeYears(m0, m1); + if (n !== null) { + return { unit: 'year', value: n }; + } + n = this.diffWholeMonths(m0, m1); + if (n !== null) { + return { unit: 'month', value: n }; + } + n = diffWholeWeeks(m0, m1); + if (n !== null) { + return { unit: 'week', value: n }; + } + n = diffWholeDays(m0, m1); + if (n !== null) { + return { unit: 'day', value: n }; + } + n = diffHours(m0, m1); + if (isInt(n)) { + return { unit: 'hour', value: n }; + } + n = diffMinutes(m0, m1); + if (isInt(n)) { + return { unit: 'minute', value: n }; + } + n = diffSeconds(m0, m1); + if (isInt(n)) { + return { unit: 'second', value: n }; + } + return { unit: 'millisecond', value: m1.valueOf() - m0.valueOf() }; + }; + DateEnv.prototype.countDurationsBetween = function (m0, m1, d) { + // TODO: can use greatestWholeUnit + var diff; + if (d.years) { + diff = this.diffWholeYears(m0, m1); + if (diff !== null) { + return diff / asRoughYears(d); + } + } + if (d.months) { + diff = this.diffWholeMonths(m0, m1); + if (diff !== null) { + return diff / asRoughMonths(d); + } + } + if (d.days) { + diff = diffWholeDays(m0, m1); + if (diff !== null) { + return diff / asRoughDays(d); + } + } + return (m1.valueOf() - m0.valueOf()) / asRoughMs(d); + }; + // Start-Of + // these DON'T return zoned-dates. only UTC start-of dates + DateEnv.prototype.startOf = function (m, unit) { + if (unit === 'year') { + return this.startOfYear(m); + } + if (unit === 'month') { + return this.startOfMonth(m); + } + if (unit === 'week') { + return this.startOfWeek(m); + } + if (unit === 'day') { + return startOfDay(m); + } + if (unit === 'hour') { + return startOfHour(m); + } + if (unit === 'minute') { + return startOfMinute(m); + } + if (unit === 'second') { + return startOfSecond(m); + } + return null; + }; + DateEnv.prototype.startOfYear = function (m) { + return this.calendarSystem.arrayToMarker([ + this.calendarSystem.getMarkerYear(m), + ]); + }; + DateEnv.prototype.startOfMonth = function (m) { + return this.calendarSystem.arrayToMarker([ + this.calendarSystem.getMarkerYear(m), + this.calendarSystem.getMarkerMonth(m), + ]); + }; + DateEnv.prototype.startOfWeek = function (m) { + return this.calendarSystem.arrayToMarker([ + this.calendarSystem.getMarkerYear(m), + this.calendarSystem.getMarkerMonth(m), + m.getUTCDate() - ((m.getUTCDay() - this.weekDow + 7) % 7), + ]); + }; + // Week Number + DateEnv.prototype.computeWeekNumber = function (marker) { + if (this.weekNumberFunc) { + return this.weekNumberFunc(this.toDate(marker)); + } + return weekOfYear(marker, this.weekDow, this.weekDoy); + }; + // TODO: choke on timeZoneName: long + DateEnv.prototype.format = function (marker, formatter, dateOptions) { + if (dateOptions === void 0) { dateOptions = {}; } + return formatter.format({ + marker: marker, + timeZoneOffset: dateOptions.forcedTzo != null ? + dateOptions.forcedTzo : + this.offsetForMarker(marker), + }, this); + }; + DateEnv.prototype.formatRange = function (start, end, formatter, dateOptions) { + if (dateOptions === void 0) { dateOptions = {}; } + if (dateOptions.isEndExclusive) { + end = addMs(end, -1); + } + return formatter.formatRange({ + marker: start, + timeZoneOffset: dateOptions.forcedStartTzo != null ? + dateOptions.forcedStartTzo : + this.offsetForMarker(start), + }, { + marker: end, + timeZoneOffset: dateOptions.forcedEndTzo != null ? + dateOptions.forcedEndTzo : + this.offsetForMarker(end), + }, this, dateOptions.defaultSeparator); + }; + /* + DUMB: the omitTime arg is dumb. if we omit the time, we want to omit the timezone offset. and if we do that, + might as well use buildIsoString or some other util directly + */ + DateEnv.prototype.formatIso = function (marker, extraOptions) { + if (extraOptions === void 0) { extraOptions = {}; } + var timeZoneOffset = null; + if (!extraOptions.omitTimeZoneOffset) { + if (extraOptions.forcedTzo != null) { + timeZoneOffset = extraOptions.forcedTzo; + } + else { + timeZoneOffset = this.offsetForMarker(marker); + } + } + return buildIsoString(marker, timeZoneOffset, extraOptions.omitTime); + }; + // TimeZone + DateEnv.prototype.timestampToMarker = function (ms) { + if (this.timeZone === 'local') { + return arrayToUtcDate(dateToLocalArray(new Date(ms))); + } + if (this.timeZone === 'UTC' || !this.namedTimeZoneImpl) { + return new Date(ms); + } + return arrayToUtcDate(this.namedTimeZoneImpl.timestampToArray(ms)); + }; + DateEnv.prototype.offsetForMarker = function (m) { + if (this.timeZone === 'local') { + return -arrayToLocalDate(dateToUtcArray(m)).getTimezoneOffset(); // convert "inverse" offset to "normal" offset + } + if (this.timeZone === 'UTC') { + return 0; + } + if (this.namedTimeZoneImpl) { + return this.namedTimeZoneImpl.offsetForArray(dateToUtcArray(m)); + } + return null; + }; + // Conversion + DateEnv.prototype.toDate = function (m, forcedTzo) { + if (this.timeZone === 'local') { + return arrayToLocalDate(dateToUtcArray(m)); + } + if (this.timeZone === 'UTC') { + return new Date(m.valueOf()); // make sure it's a copy + } + if (!this.namedTimeZoneImpl) { + return new Date(m.valueOf() - (forcedTzo || 0)); + } + return new Date(m.valueOf() - + this.namedTimeZoneImpl.offsetForArray(dateToUtcArray(m)) * 1000 * 60); + }; + return DateEnv; + }()); + + var globalLocales = []; + + var RAW_EN_LOCALE = { + code: 'en', + week: { + dow: 0, + doy: 4, + }, + direction: 'ltr', + buttonText: { + prev: 'prev', + next: 'next', + prevYear: 'prev year', + nextYear: 'next year', + year: 'year', + today: 'today', + month: 'month', + week: 'week', + day: 'day', + list: 'list', + }, + weekText: 'W', + allDayText: 'all-day', + moreLinkText: 'more', + noEventsText: 'No events to display', + }; + function organizeRawLocales(explicitRawLocales) { + var defaultCode = explicitRawLocales.length > 0 ? explicitRawLocales[0].code : 'en'; + var allRawLocales = globalLocales.concat(explicitRawLocales); + var rawLocaleMap = { + en: RAW_EN_LOCALE, + }; + for (var _i = 0, allRawLocales_1 = allRawLocales; _i < allRawLocales_1.length; _i++) { + var rawLocale = allRawLocales_1[_i]; + rawLocaleMap[rawLocale.code] = rawLocale; + } + return { + map: rawLocaleMap, + defaultCode: defaultCode, + }; + } + function buildLocale(inputSingular, available) { + if (typeof inputSingular === 'object' && !Array.isArray(inputSingular)) { + return parseLocale(inputSingular.code, [inputSingular.code], inputSingular); + } + return queryLocale(inputSingular, available); + } + function queryLocale(codeArg, available) { + var codes = [].concat(codeArg || []); // will convert to array + var raw = queryRawLocale(codes, available) || RAW_EN_LOCALE; + return parseLocale(codeArg, codes, raw); + } + function queryRawLocale(codes, available) { + for (var i = 0; i < codes.length; i += 1) { + var parts = codes[i].toLocaleLowerCase().split('-'); + for (var j = parts.length; j > 0; j -= 1) { + var simpleId = parts.slice(0, j).join('-'); + if (available[simpleId]) { + return available[simpleId]; + } + } + } + return null; + } + function parseLocale(codeArg, codes, raw) { + var merged = mergeProps([RAW_EN_LOCALE, raw], ['buttonText']); + delete merged.code; // don't want this part of the options + var week = merged.week; + delete merged.week; + return { + codeArg: codeArg, + codes: codes, + week: week, + simpleNumberFormat: new Intl.NumberFormat(codeArg), + options: merged, + }; + } + + function formatDate(dateInput, options) { + if (options === void 0) { options = {}; } + var dateEnv = buildDateEnv(options); + var formatter = createFormatter(options); + var dateMeta = dateEnv.createMarkerMeta(dateInput); + if (!dateMeta) { // TODO: warning? + return ''; + } + return dateEnv.format(dateMeta.marker, formatter, { + forcedTzo: dateMeta.forcedTzo, + }); + } + function formatRange(startInput, endInput, options) { + var dateEnv = buildDateEnv(typeof options === 'object' && options ? options : {}); // pass in if non-null object + var formatter = createFormatter(options); + var startMeta = dateEnv.createMarkerMeta(startInput); + var endMeta = dateEnv.createMarkerMeta(endInput); + if (!startMeta || !endMeta) { // TODO: warning? + return ''; + } + return dateEnv.formatRange(startMeta.marker, endMeta.marker, formatter, { + forcedStartTzo: startMeta.forcedTzo, + forcedEndTzo: endMeta.forcedTzo, + isEndExclusive: options.isEndExclusive, + defaultSeparator: BASE_OPTION_DEFAULTS.defaultRangeSeparator, + }); + } + // TODO: more DRY and optimized + function buildDateEnv(settings) { + var locale = buildLocale(settings.locale || 'en', organizeRawLocales([]).map); // TODO: don't hardcode 'en' everywhere + return new DateEnv(__assign(__assign({ timeZone: BASE_OPTION_DEFAULTS.timeZone, calendarSystem: 'gregory' }, settings), { locale: locale })); + } + + var DEF_DEFAULTS = { + startTime: '09:00', + endTime: '17:00', + daysOfWeek: [1, 2, 3, 4, 5], + display: 'inverse-background', + classNames: 'fc-non-business', + groupId: '_businessHours', + }; + /* + TODO: pass around as EventDefHash!!! + */ + function parseBusinessHours(input, context) { + return parseEvents(refineInputs(input), null, context); + } + function refineInputs(input) { + var rawDefs; + if (input === true) { + rawDefs = [{}]; // will get DEF_DEFAULTS verbatim + } + else if (Array.isArray(input)) { + // if specifying an array, every sub-definition NEEDS a day-of-week + rawDefs = input.filter(function (rawDef) { return rawDef.daysOfWeek; }); + } + else if (typeof input === 'object' && input) { // non-null object + rawDefs = [input]; + } + else { // is probably false + rawDefs = []; + } + rawDefs = rawDefs.map(function (rawDef) { return (__assign(__assign({}, DEF_DEFAULTS), rawDef)); }); + return rawDefs; + } + + function pointInsideRect(point, rect) { + return point.left >= rect.left && + point.left < rect.right && + point.top >= rect.top && + point.top < rect.bottom; + } + // Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false + function intersectRects(rect1, rect2) { + var res = { + left: Math.max(rect1.left, rect2.left), + right: Math.min(rect1.right, rect2.right), + top: Math.max(rect1.top, rect2.top), + bottom: Math.min(rect1.bottom, rect2.bottom), + }; + if (res.left < res.right && res.top < res.bottom) { + return res; + } + return false; + } + function translateRect(rect, deltaX, deltaY) { + return { + left: rect.left + deltaX, + right: rect.right + deltaX, + top: rect.top + deltaY, + bottom: rect.bottom + deltaY, + }; + } + // Returns a new point that will have been moved to reside within the given rectangle + function constrainPoint(point, rect) { + return { + left: Math.min(Math.max(point.left, rect.left), rect.right), + top: Math.min(Math.max(point.top, rect.top), rect.bottom), + }; + } + // Returns a point that is the center of the given rectangle + function getRectCenter(rect) { + return { + left: (rect.left + rect.right) / 2, + top: (rect.top + rect.bottom) / 2, + }; + } + // Subtracts point2's coordinates from point1's coordinates, returning a delta + function diffPoints(point1, point2) { + return { + left: point1.left - point2.left, + top: point1.top - point2.top, + }; + } + + var canVGrowWithinCell; + function getCanVGrowWithinCell() { + if (canVGrowWithinCell == null) { + canVGrowWithinCell = computeCanVGrowWithinCell(); + } + return canVGrowWithinCell; + } + function computeCanVGrowWithinCell() { + // for SSR, because this function is call immediately at top-level + // TODO: just make this logic execute top-level, immediately, instead of doing lazily + if (typeof document === 'undefined') { + return true; + } + var el = document.createElement('div'); + el.style.position = 'absolute'; + el.style.top = '0px'; + el.style.left = '0px'; + el.innerHTML = '
'; + el.querySelector('table').style.height = '100px'; + el.querySelector('div').style.height = '100%'; + document.body.appendChild(el); + var div = el.querySelector('div'); + var possible = div.offsetHeight > 0; + document.body.removeChild(el); + return possible; + } + + var EMPTY_EVENT_STORE = createEmptyEventStore(); // for purecomponents. TODO: keep elsewhere + var Splitter = /** @class */ (function () { + function Splitter() { + this.getKeysForEventDefs = memoize(this._getKeysForEventDefs); + this.splitDateSelection = memoize(this._splitDateSpan); + this.splitEventStore = memoize(this._splitEventStore); + this.splitIndividualUi = memoize(this._splitIndividualUi); + this.splitEventDrag = memoize(this._splitInteraction); + this.splitEventResize = memoize(this._splitInteraction); + this.eventUiBuilders = {}; // TODO: typescript protection + } + Splitter.prototype.splitProps = function (props) { + var _this = this; + var keyInfos = this.getKeyInfo(props); + var defKeys = this.getKeysForEventDefs(props.eventStore); + var dateSelections = this.splitDateSelection(props.dateSelection); + var individualUi = this.splitIndividualUi(props.eventUiBases, defKeys); // the individual *bases* + var eventStores = this.splitEventStore(props.eventStore, defKeys); + var eventDrags = this.splitEventDrag(props.eventDrag); + var eventResizes = this.splitEventResize(props.eventResize); + var splitProps = {}; + this.eventUiBuilders = mapHash(keyInfos, function (info, key) { return _this.eventUiBuilders[key] || memoize(buildEventUiForKey); }); + for (var key in keyInfos) { + var keyInfo = keyInfos[key]; + var eventStore = eventStores[key] || EMPTY_EVENT_STORE; + var buildEventUi = this.eventUiBuilders[key]; + splitProps[key] = { + businessHours: keyInfo.businessHours || props.businessHours, + dateSelection: dateSelections[key] || null, + eventStore: eventStore, + eventUiBases: buildEventUi(props.eventUiBases[''], keyInfo.ui, individualUi[key]), + eventSelection: eventStore.instances[props.eventSelection] ? props.eventSelection : '', + eventDrag: eventDrags[key] || null, + eventResize: eventResizes[key] || null, + }; + } + return splitProps; + }; + Splitter.prototype._splitDateSpan = function (dateSpan) { + var dateSpans = {}; + if (dateSpan) { + var keys = this.getKeysForDateSpan(dateSpan); + for (var _i = 0, keys_1 = keys; _i < keys_1.length; _i++) { + var key = keys_1[_i]; + dateSpans[key] = dateSpan; + } + } + return dateSpans; + }; + Splitter.prototype._getKeysForEventDefs = function (eventStore) { + var _this = this; + return mapHash(eventStore.defs, function (eventDef) { return _this.getKeysForEventDef(eventDef); }); + }; + Splitter.prototype._splitEventStore = function (eventStore, defKeys) { + var defs = eventStore.defs, instances = eventStore.instances; + var splitStores = {}; + for (var defId in defs) { + for (var _i = 0, _a = defKeys[defId]; _i < _a.length; _i++) { + var key = _a[_i]; + if (!splitStores[key]) { + splitStores[key] = createEmptyEventStore(); + } + splitStores[key].defs[defId] = defs[defId]; + } + } + for (var instanceId in instances) { + var instance = instances[instanceId]; + for (var _b = 0, _c = defKeys[instance.defId]; _b < _c.length; _b++) { + var key = _c[_b]; + if (splitStores[key]) { // must have already been created + splitStores[key].instances[instanceId] = instance; + } + } + } + return splitStores; + }; + Splitter.prototype._splitIndividualUi = function (eventUiBases, defKeys) { + var splitHashes = {}; + for (var defId in eventUiBases) { + if (defId) { // not the '' key + for (var _i = 0, _a = defKeys[defId]; _i < _a.length; _i++) { + var key = _a[_i]; + if (!splitHashes[key]) { + splitHashes[key] = {}; + } + splitHashes[key][defId] = eventUiBases[defId]; + } + } + } + return splitHashes; + }; + Splitter.prototype._splitInteraction = function (interaction) { + var splitStates = {}; + if (interaction) { + var affectedStores_1 = this._splitEventStore(interaction.affectedEvents, this._getKeysForEventDefs(interaction.affectedEvents)); + // can't rely on defKeys because event data is mutated + var mutatedKeysByDefId = this._getKeysForEventDefs(interaction.mutatedEvents); + var mutatedStores_1 = this._splitEventStore(interaction.mutatedEvents, mutatedKeysByDefId); + var populate = function (key) { + if (!splitStates[key]) { + splitStates[key] = { + affectedEvents: affectedStores_1[key] || EMPTY_EVENT_STORE, + mutatedEvents: mutatedStores_1[key] || EMPTY_EVENT_STORE, + isEvent: interaction.isEvent, + }; + } + }; + for (var key in affectedStores_1) { + populate(key); + } + for (var key in mutatedStores_1) { + populate(key); + } + } + return splitStates; + }; + return Splitter; + }()); + function buildEventUiForKey(allUi, eventUiForKey, individualUi) { + var baseParts = []; + if (allUi) { + baseParts.push(allUi); + } + if (eventUiForKey) { + baseParts.push(eventUiForKey); + } + var stuff = { + '': combineEventUis(baseParts), + }; + if (individualUi) { + __assign(stuff, individualUi); + } + return stuff; + } + + function getDateMeta(date, todayRange, nowDate, dateProfile) { + return { + dow: date.getUTCDay(), + isDisabled: Boolean(dateProfile && !rangeContainsMarker(dateProfile.activeRange, date)), + isOther: Boolean(dateProfile && !rangeContainsMarker(dateProfile.currentRange, date)), + isToday: Boolean(todayRange && rangeContainsMarker(todayRange, date)), + isPast: Boolean(nowDate ? (date < nowDate) : todayRange ? (date < todayRange.start) : false), + isFuture: Boolean(nowDate ? (date > nowDate) : todayRange ? (date >= todayRange.end) : false), + }; + } + function getDayClassNames(meta, theme) { + var classNames = [ + 'fc-day', + "fc-day-" + DAY_IDS[meta.dow], + ]; + if (meta.isDisabled) { + classNames.push('fc-day-disabled'); + } + else { + if (meta.isToday) { + classNames.push('fc-day-today'); + classNames.push(theme.getClass('today')); + } + if (meta.isPast) { + classNames.push('fc-day-past'); + } + if (meta.isFuture) { + classNames.push('fc-day-future'); + } + if (meta.isOther) { + classNames.push('fc-day-other'); + } + } + return classNames; + } + function getSlotClassNames(meta, theme) { + var classNames = [ + 'fc-slot', + "fc-slot-" + DAY_IDS[meta.dow], + ]; + if (meta.isDisabled) { + classNames.push('fc-slot-disabled'); + } + else { + if (meta.isToday) { + classNames.push('fc-slot-today'); + classNames.push(theme.getClass('today')); + } + if (meta.isPast) { + classNames.push('fc-slot-past'); + } + if (meta.isFuture) { + classNames.push('fc-slot-future'); + } + } + return classNames; + } + + function buildNavLinkData(date, type) { + if (type === void 0) { type = 'day'; } + return JSON.stringify({ + date: formatDayString(date), + type: type, + }); + } + + var _isRtlScrollbarOnLeft = null; + function getIsRtlScrollbarOnLeft() { + if (_isRtlScrollbarOnLeft === null) { + _isRtlScrollbarOnLeft = computeIsRtlScrollbarOnLeft(); + } + return _isRtlScrollbarOnLeft; + } + function computeIsRtlScrollbarOnLeft() { + var outerEl = document.createElement('div'); + applyStyle(outerEl, { + position: 'absolute', + top: -1000, + left: 0, + border: 0, + padding: 0, + overflow: 'scroll', + direction: 'rtl', + }); + outerEl.innerHTML = '
'; + document.body.appendChild(outerEl); + var innerEl = outerEl.firstChild; + var res = innerEl.getBoundingClientRect().left > outerEl.getBoundingClientRect().left; + removeElement(outerEl); + return res; + } + + var _scrollbarWidths; + function getScrollbarWidths() { + if (!_scrollbarWidths) { + _scrollbarWidths = computeScrollbarWidths(); + } + return _scrollbarWidths; + } + function computeScrollbarWidths() { + var el = document.createElement('div'); + el.style.overflow = 'scroll'; + el.style.position = 'absolute'; + el.style.top = '-9999px'; + el.style.left = '-9999px'; + document.body.appendChild(el); + var res = computeScrollbarWidthsForEl(el); + document.body.removeChild(el); + return res; + } + // WARNING: will include border + function computeScrollbarWidthsForEl(el) { + return { + x: el.offsetHeight - el.clientHeight, + y: el.offsetWidth - el.clientWidth, + }; + } + + function computeEdges(el, getPadding) { + if (getPadding === void 0) { getPadding = false; } + var computedStyle = window.getComputedStyle(el); + var borderLeft = parseInt(computedStyle.borderLeftWidth, 10) || 0; + var borderRight = parseInt(computedStyle.borderRightWidth, 10) || 0; + var borderTop = parseInt(computedStyle.borderTopWidth, 10) || 0; + var borderBottom = parseInt(computedStyle.borderBottomWidth, 10) || 0; + var badScrollbarWidths = computeScrollbarWidthsForEl(el); // includes border! + var scrollbarLeftRight = badScrollbarWidths.y - borderLeft - borderRight; + var scrollbarBottom = badScrollbarWidths.x - borderTop - borderBottom; + var res = { + borderLeft: borderLeft, + borderRight: borderRight, + borderTop: borderTop, + borderBottom: borderBottom, + scrollbarBottom: scrollbarBottom, + scrollbarLeft: 0, + scrollbarRight: 0, + }; + if (getIsRtlScrollbarOnLeft() && computedStyle.direction === 'rtl') { // is the scrollbar on the left side? + res.scrollbarLeft = scrollbarLeftRight; + } + else { + res.scrollbarRight = scrollbarLeftRight; + } + if (getPadding) { + res.paddingLeft = parseInt(computedStyle.paddingLeft, 10) || 0; + res.paddingRight = parseInt(computedStyle.paddingRight, 10) || 0; + res.paddingTop = parseInt(computedStyle.paddingTop, 10) || 0; + res.paddingBottom = parseInt(computedStyle.paddingBottom, 10) || 0; + } + return res; + } + function computeInnerRect(el, goWithinPadding, doFromWindowViewport) { + if (goWithinPadding === void 0) { goWithinPadding = false; } + var outerRect = doFromWindowViewport ? el.getBoundingClientRect() : computeRect(el); + var edges = computeEdges(el, goWithinPadding); + var res = { + left: outerRect.left + edges.borderLeft + edges.scrollbarLeft, + right: outerRect.right - edges.borderRight - edges.scrollbarRight, + top: outerRect.top + edges.borderTop, + bottom: outerRect.bottom - edges.borderBottom - edges.scrollbarBottom, + }; + if (goWithinPadding) { + res.left += edges.paddingLeft; + res.right -= edges.paddingRight; + res.top += edges.paddingTop; + res.bottom -= edges.paddingBottom; + } + return res; + } + function computeRect(el) { + var rect = el.getBoundingClientRect(); + return { + left: rect.left + window.pageXOffset, + top: rect.top + window.pageYOffset, + right: rect.right + window.pageXOffset, + bottom: rect.bottom + window.pageYOffset, + }; + } + function computeHeightAndMargins(el) { + return el.getBoundingClientRect().height + computeVMargins(el); + } + function computeVMargins(el) { + var computed = window.getComputedStyle(el); + return parseInt(computed.marginTop, 10) + + parseInt(computed.marginBottom, 10); + } + // does not return window + function getClippingParents(el) { + var parents = []; + while (el instanceof HTMLElement) { // will stop when gets to document or null + var computedStyle = window.getComputedStyle(el); + if (computedStyle.position === 'fixed') { + break; + } + if ((/(auto|scroll)/).test(computedStyle.overflow + computedStyle.overflowY + computedStyle.overflowX)) { + parents.push(el); + } + el = el.parentNode; + } + return parents; + } + + // given a function that resolves a result asynchronously. + // the function can either call passed-in success and failure callbacks, + // or it can return a promise. + // if you need to pass additional params to func, bind them first. + function unpromisify(func, success, failure) { + // guard against success/failure callbacks being called more than once + // and guard against a promise AND callback being used together. + var isResolved = false; + var wrappedSuccess = function () { + if (!isResolved) { + isResolved = true; + success.apply(this, arguments); // eslint-disable-line prefer-rest-params + } + }; + var wrappedFailure = function () { + if (!isResolved) { + isResolved = true; + if (failure) { + failure.apply(this, arguments); // eslint-disable-line prefer-rest-params + } + } + }; + var res = func(wrappedSuccess, wrappedFailure); + if (res && typeof res.then === 'function') { + res.then(wrappedSuccess, wrappedFailure); + } + } + + var Emitter = /** @class */ (function () { + function Emitter() { + this.handlers = {}; + this.thisContext = null; + } + Emitter.prototype.setThisContext = function (thisContext) { + this.thisContext = thisContext; + }; + Emitter.prototype.setOptions = function (options) { + this.options = options; + }; + Emitter.prototype.on = function (type, handler) { + addToHash(this.handlers, type, handler); + }; + Emitter.prototype.off = function (type, handler) { + removeFromHash(this.handlers, type, handler); + }; + Emitter.prototype.trigger = function (type) { + var args = []; + for (var _i = 1; _i < arguments.length; _i++) { + args[_i - 1] = arguments[_i]; + } + var attachedHandlers = this.handlers[type] || []; + var optionHandler = this.options && this.options[type]; + var handlers = [].concat(optionHandler || [], attachedHandlers); + for (var _a = 0, handlers_1 = handlers; _a < handlers_1.length; _a++) { + var handler = handlers_1[_a]; + handler.apply(this.thisContext, args); + } + }; + Emitter.prototype.hasHandlers = function (type) { + return (this.handlers[type] && this.handlers[type].length) || + (this.options && this.options[type]); + }; + return Emitter; + }()); + function addToHash(hash, type, handler) { + (hash[type] || (hash[type] = [])) + .push(handler); + } + function removeFromHash(hash, type, handler) { + if (handler) { + if (hash[type]) { + hash[type] = hash[type].filter(function (func) { return func !== handler; }); + } + } + else { + delete hash[type]; // remove all handler funcs for this type + } + } + + /* + Records offset information for a set of elements, relative to an origin element. + Can record the left/right OR the top/bottom OR both. + Provides methods for querying the cache by position. + */ + var PositionCache = /** @class */ (function () { + function PositionCache(originEl, els, isHorizontal, isVertical) { + this.els = els; + var originClientRect = this.originClientRect = originEl.getBoundingClientRect(); // relative to viewport top-left + if (isHorizontal) { + this.buildElHorizontals(originClientRect.left); + } + if (isVertical) { + this.buildElVerticals(originClientRect.top); + } + } + // Populates the left/right internal coordinate arrays + PositionCache.prototype.buildElHorizontals = function (originClientLeft) { + var lefts = []; + var rights = []; + for (var _i = 0, _a = this.els; _i < _a.length; _i++) { + var el = _a[_i]; + var rect = el.getBoundingClientRect(); + lefts.push(rect.left - originClientLeft); + rights.push(rect.right - originClientLeft); + } + this.lefts = lefts; + this.rights = rights; + }; + // Populates the top/bottom internal coordinate arrays + PositionCache.prototype.buildElVerticals = function (originClientTop) { + var tops = []; + var bottoms = []; + for (var _i = 0, _a = this.els; _i < _a.length; _i++) { + var el = _a[_i]; + var rect = el.getBoundingClientRect(); + tops.push(rect.top - originClientTop); + bottoms.push(rect.bottom - originClientTop); + } + this.tops = tops; + this.bottoms = bottoms; + }; + // Given a left offset (from document left), returns the index of the el that it horizontally intersects. + // If no intersection is made, returns undefined. + PositionCache.prototype.leftToIndex = function (leftPosition) { + var _a = this, lefts = _a.lefts, rights = _a.rights; + var len = lefts.length; + var i; + for (i = 0; i < len; i += 1) { + if (leftPosition >= lefts[i] && leftPosition < rights[i]) { + return i; + } + } + return undefined; // TODO: better + }; + // Given a top offset (from document top), returns the index of the el that it vertically intersects. + // If no intersection is made, returns undefined. + PositionCache.prototype.topToIndex = function (topPosition) { + var _a = this, tops = _a.tops, bottoms = _a.bottoms; + var len = tops.length; + var i; + for (i = 0; i < len; i += 1) { + if (topPosition >= tops[i] && topPosition < bottoms[i]) { + return i; + } + } + return undefined; // TODO: better + }; + // Gets the width of the element at the given index + PositionCache.prototype.getWidth = function (leftIndex) { + return this.rights[leftIndex] - this.lefts[leftIndex]; + }; + // Gets the height of the element at the given index + PositionCache.prototype.getHeight = function (topIndex) { + return this.bottoms[topIndex] - this.tops[topIndex]; + }; + return PositionCache; + }()); + + /* eslint max-classes-per-file: "off" */ + /* + An object for getting/setting scroll-related information for an element. + Internally, this is done very differently for window versus DOM element, + so this object serves as a common interface. + */ + var ScrollController = /** @class */ (function () { + function ScrollController() { + } + ScrollController.prototype.getMaxScrollTop = function () { + return this.getScrollHeight() - this.getClientHeight(); + }; + ScrollController.prototype.getMaxScrollLeft = function () { + return this.getScrollWidth() - this.getClientWidth(); + }; + ScrollController.prototype.canScrollVertically = function () { + return this.getMaxScrollTop() > 0; + }; + ScrollController.prototype.canScrollHorizontally = function () { + return this.getMaxScrollLeft() > 0; + }; + ScrollController.prototype.canScrollUp = function () { + return this.getScrollTop() > 0; + }; + ScrollController.prototype.canScrollDown = function () { + return this.getScrollTop() < this.getMaxScrollTop(); + }; + ScrollController.prototype.canScrollLeft = function () { + return this.getScrollLeft() > 0; + }; + ScrollController.prototype.canScrollRight = function () { + return this.getScrollLeft() < this.getMaxScrollLeft(); + }; + return ScrollController; + }()); + var ElementScrollController = /** @class */ (function (_super) { + __extends(ElementScrollController, _super); + function ElementScrollController(el) { + var _this = _super.call(this) || this; + _this.el = el; + return _this; + } + ElementScrollController.prototype.getScrollTop = function () { + return this.el.scrollTop; + }; + ElementScrollController.prototype.getScrollLeft = function () { + return this.el.scrollLeft; + }; + ElementScrollController.prototype.setScrollTop = function (top) { + this.el.scrollTop = top; + }; + ElementScrollController.prototype.setScrollLeft = function (left) { + this.el.scrollLeft = left; + }; + ElementScrollController.prototype.getScrollWidth = function () { + return this.el.scrollWidth; + }; + ElementScrollController.prototype.getScrollHeight = function () { + return this.el.scrollHeight; + }; + ElementScrollController.prototype.getClientHeight = function () { + return this.el.clientHeight; + }; + ElementScrollController.prototype.getClientWidth = function () { + return this.el.clientWidth; + }; + return ElementScrollController; + }(ScrollController)); + var WindowScrollController = /** @class */ (function (_super) { + __extends(WindowScrollController, _super); + function WindowScrollController() { + return _super !== null && _super.apply(this, arguments) || this; + } + WindowScrollController.prototype.getScrollTop = function () { + return window.pageYOffset; + }; + WindowScrollController.prototype.getScrollLeft = function () { + return window.pageXOffset; + }; + WindowScrollController.prototype.setScrollTop = function (n) { + window.scroll(window.pageXOffset, n); + }; + WindowScrollController.prototype.setScrollLeft = function (n) { + window.scroll(n, window.pageYOffset); + }; + WindowScrollController.prototype.getScrollWidth = function () { + return document.documentElement.scrollWidth; + }; + WindowScrollController.prototype.getScrollHeight = function () { + return document.documentElement.scrollHeight; + }; + WindowScrollController.prototype.getClientHeight = function () { + return document.documentElement.clientHeight; + }; + WindowScrollController.prototype.getClientWidth = function () { + return document.documentElement.clientWidth; + }; + return WindowScrollController; + }(ScrollController)); + + var Theme = /** @class */ (function () { + function Theme(calendarOptions) { + if (this.iconOverrideOption) { + this.setIconOverride(calendarOptions[this.iconOverrideOption]); + } + } + Theme.prototype.setIconOverride = function (iconOverrideHash) { + var iconClassesCopy; + var buttonName; + if (typeof iconOverrideHash === 'object' && iconOverrideHash) { // non-null object + iconClassesCopy = __assign({}, this.iconClasses); + for (buttonName in iconOverrideHash) { + iconClassesCopy[buttonName] = this.applyIconOverridePrefix(iconOverrideHash[buttonName]); + } + this.iconClasses = iconClassesCopy; + } + else if (iconOverrideHash === false) { + this.iconClasses = {}; + } + }; + Theme.prototype.applyIconOverridePrefix = function (className) { + var prefix = this.iconOverridePrefix; + if (prefix && className.indexOf(prefix) !== 0) { // if not already present + className = prefix + className; + } + return className; + }; + Theme.prototype.getClass = function (key) { + return this.classes[key] || ''; + }; + Theme.prototype.getIconClass = function (buttonName, isRtl) { + var className; + if (isRtl && this.rtlIconClasses) { + className = this.rtlIconClasses[buttonName] || this.iconClasses[buttonName]; + } + else { + className = this.iconClasses[buttonName]; + } + if (className) { + return this.baseIconClass + " " + className; + } + return ''; + }; + Theme.prototype.getCustomButtonIconClass = function (customButtonProps) { + var className; + if (this.iconOverrideCustomButtonOption) { + className = customButtonProps[this.iconOverrideCustomButtonOption]; + if (className) { + return this.baseIconClass + " " + this.applyIconOverridePrefix(className); + } + } + return ''; + }; + return Theme; + }()); + Theme.prototype.classes = {}; + Theme.prototype.iconClasses = {}; + Theme.prototype.baseIconClass = ''; + Theme.prototype.iconOverridePrefix = ''; + + /// + if (typeof FullCalendarVDom === 'undefined') { + throw new Error('Please import the top-level fullcalendar lib before attempting to import a plugin.'); + } + var Component = FullCalendarVDom.Component; + var createElement = FullCalendarVDom.createElement; + var render = FullCalendarVDom.render; + var createRef = FullCalendarVDom.createRef; + var Fragment = FullCalendarVDom.Fragment; + var createContext$1 = FullCalendarVDom.createContext; + var flushToDom$1 = FullCalendarVDom.flushToDom; + var unmountComponentAtNode$1 = FullCalendarVDom.unmountComponentAtNode; + + var ScrollResponder = /** @class */ (function () { + function ScrollResponder(execFunc, emitter, scrollTime) { + var _this = this; + this.execFunc = execFunc; + this.emitter = emitter; + this.scrollTime = scrollTime; + this.handleScrollRequest = function (request) { + _this.queuedRequest = __assign({}, _this.queuedRequest || {}, request); + _this.drain(); + }; + emitter.on('_scrollRequest', this.handleScrollRequest); + this.fireInitialScroll(); + } + ScrollResponder.prototype.detach = function () { + this.emitter.off('_scrollRequest', this.handleScrollRequest); + }; + ScrollResponder.prototype.update = function (isDatesNew) { + if (isDatesNew) { + this.fireInitialScroll(); // will drain + } + else { + this.drain(); + } + }; + ScrollResponder.prototype.fireInitialScroll = function () { + this.handleScrollRequest({ + time: this.scrollTime, + }); + }; + ScrollResponder.prototype.drain = function () { + if (this.queuedRequest && this.execFunc(this.queuedRequest)) { + this.queuedRequest = null; + } + }; + return ScrollResponder; + }()); + + var ViewContextType = createContext$1({}); // for Components + function buildViewContext(viewSpec, viewApi, viewOptions, dateProfileGenerator, dateEnv, theme, pluginHooks, dispatch, getCurrentData, emitter, calendarApi, registerInteractiveComponent, unregisterInteractiveComponent) { + return { + dateEnv: dateEnv, + options: viewOptions, + pluginHooks: pluginHooks, + emitter: emitter, + dispatch: dispatch, + getCurrentData: getCurrentData, + calendarApi: calendarApi, + viewSpec: viewSpec, + viewApi: viewApi, + dateProfileGenerator: dateProfileGenerator, + theme: theme, + isRtl: viewOptions.direction === 'rtl', + addResizeHandler: function (handler) { + emitter.on('_resize', handler); + }, + removeResizeHandler: function (handler) { + emitter.off('_resize', handler); + }, + createScrollResponder: function (execFunc) { + return new ScrollResponder(execFunc, emitter, createDuration(viewOptions.scrollTime)); + }, + registerInteractiveComponent: registerInteractiveComponent, + unregisterInteractiveComponent: unregisterInteractiveComponent, + }; + } + + /* eslint max-classes-per-file: off */ + var PureComponent = /** @class */ (function (_super) { + __extends(PureComponent, _super); + function PureComponent() { + return _super !== null && _super.apply(this, arguments) || this; + } + PureComponent.prototype.shouldComponentUpdate = function (nextProps, nextState) { + if (this.debug) { + // eslint-disable-next-line no-console + console.log(getUnequalProps(nextProps, this.props), getUnequalProps(nextState, this.state)); + } + return !compareObjs(this.props, nextProps, this.propEquality) || + !compareObjs(this.state, nextState, this.stateEquality); + }; + PureComponent.addPropsEquality = addPropsEquality; + PureComponent.addStateEquality = addStateEquality; + PureComponent.contextType = ViewContextType; + return PureComponent; + }(Component)); + PureComponent.prototype.propEquality = {}; + PureComponent.prototype.stateEquality = {}; + var BaseComponent = /** @class */ (function (_super) { + __extends(BaseComponent, _super); + function BaseComponent() { + return _super !== null && _super.apply(this, arguments) || this; + } + BaseComponent.contextType = ViewContextType; + return BaseComponent; + }(PureComponent)); + function addPropsEquality(propEquality) { + var hash = Object.create(this.prototype.propEquality); + __assign(hash, propEquality); + this.prototype.propEquality = hash; + } + function addStateEquality(stateEquality) { + var hash = Object.create(this.prototype.stateEquality); + __assign(hash, stateEquality); + this.prototype.stateEquality = hash; + } + // use other one + function setRef(ref, current) { + if (typeof ref === 'function') { + ref(current); + } + else if (ref) { + // see https://github.com/facebook/react/issues/13029 + ref.current = current; + } + } + + function reduceEventStore(eventStore, action, eventSources, dateProfile, context) { + switch (action.type) { + case 'RECEIVE_EVENTS': // raw + return receiveRawEvents(eventStore, eventSources[action.sourceId], action.fetchId, action.fetchRange, action.rawEvents, context); + case 'ADD_EVENTS': // already parsed, but not expanded + return addEvent(eventStore, action.eventStore, // new ones + dateProfile ? dateProfile.activeRange : null, context); + case 'RESET_EVENTS': + return action.eventStore; + case 'MERGE_EVENTS': // already parsed and expanded + return mergeEventStores(eventStore, action.eventStore); + case 'PREV': // TODO: how do we track all actions that affect dateProfile :( + case 'NEXT': + case 'CHANGE_DATE': + case 'CHANGE_VIEW_TYPE': + if (dateProfile) { + return expandRecurring(eventStore, dateProfile.activeRange, context); + } + return eventStore; + case 'REMOVE_EVENTS': + return excludeSubEventStore(eventStore, action.eventStore); + case 'REMOVE_EVENT_SOURCE': + return excludeEventsBySourceId(eventStore, action.sourceId); + case 'REMOVE_ALL_EVENT_SOURCES': + return filterEventStoreDefs(eventStore, function (eventDef) { return (!eventDef.sourceId // only keep events with no source id + ); }); + case 'REMOVE_ALL_EVENTS': + return createEmptyEventStore(); + default: + return eventStore; + } + } + function receiveRawEvents(eventStore, eventSource, fetchId, fetchRange, rawEvents, context) { + if (eventSource && // not already removed + fetchId === eventSource.latestFetchId // TODO: wish this logic was always in event-sources + ) { + var subset = parseEvents(transformRawEvents(rawEvents, eventSource, context), eventSource, context); + if (fetchRange) { + subset = expandRecurring(subset, fetchRange, context); + } + return mergeEventStores(excludeEventsBySourceId(eventStore, eventSource.sourceId), subset); + } + return eventStore; + } + function transformRawEvents(rawEvents, eventSource, context) { + var calEachTransform = context.options.eventDataTransform; + var sourceEachTransform = eventSource ? eventSource.eventDataTransform : null; + if (sourceEachTransform) { + rawEvents = transformEachRawEvent(rawEvents, sourceEachTransform); + } + if (calEachTransform) { + rawEvents = transformEachRawEvent(rawEvents, calEachTransform); + } + return rawEvents; + } + function transformEachRawEvent(rawEvents, func) { + var refinedEvents; + if (!func) { + refinedEvents = rawEvents; + } + else { + refinedEvents = []; + for (var _i = 0, rawEvents_1 = rawEvents; _i < rawEvents_1.length; _i++) { + var rawEvent = rawEvents_1[_i]; + var refinedEvent = func(rawEvent); + if (refinedEvent) { + refinedEvents.push(refinedEvent); + } + else if (refinedEvent == null) { + refinedEvents.push(rawEvent); + } // if a different falsy value, do nothing + } + } + return refinedEvents; + } + function addEvent(eventStore, subset, expandRange, context) { + if (expandRange) { + subset = expandRecurring(subset, expandRange, context); + } + return mergeEventStores(eventStore, subset); + } + function rezoneEventStoreDates(eventStore, oldDateEnv, newDateEnv) { + var defs = eventStore.defs; + var instances = mapHash(eventStore.instances, function (instance) { + var def = defs[instance.defId]; + if (def.allDay || def.recurringDef) { + return instance; // isn't dependent on timezone + } + return __assign(__assign({}, instance), { range: { + start: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.start, instance.forcedStartTzo)), + end: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.end, instance.forcedEndTzo)), + }, forcedStartTzo: newDateEnv.canComputeOffset ? null : instance.forcedStartTzo, forcedEndTzo: newDateEnv.canComputeOffset ? null : instance.forcedEndTzo }); + }); + return { defs: defs, instances: instances }; + } + function excludeEventsBySourceId(eventStore, sourceId) { + return filterEventStoreDefs(eventStore, function (eventDef) { return eventDef.sourceId !== sourceId; }); + } + // QUESTION: why not just return instances? do a general object-property-exclusion util + function excludeInstances(eventStore, removals) { + return { + defs: eventStore.defs, + instances: filterHash(eventStore.instances, function (instance) { return !removals[instance.instanceId]; }), + }; + } + + // high-level segmenting-aware tester functions + // ------------------------------------------------------------------------------------------------------------------------ + function isInteractionValid(interaction, context) { + return isNewPropsValid({ eventDrag: interaction }, context); // HACK: the eventDrag props is used for ALL interactions + } + function isDateSelectionValid(dateSelection, context) { + return isNewPropsValid({ dateSelection: dateSelection }, context); + } + function isNewPropsValid(newProps, context) { + var calendarState = context.getCurrentData(); + var props = __assign({ businessHours: calendarState.businessHours, dateSelection: '', eventStore: calendarState.eventStore, eventUiBases: calendarState.eventUiBases, eventSelection: '', eventDrag: null, eventResize: null }, newProps); + return (context.pluginHooks.isPropsValid || isPropsValid)(props, context); + } + function isPropsValid(state, context, dateSpanMeta, filterConfig) { + if (dateSpanMeta === void 0) { dateSpanMeta = {}; } + if (state.eventDrag && !isInteractionPropsValid(state, context, dateSpanMeta, filterConfig)) { + return false; + } + if (state.dateSelection && !isDateSelectionPropsValid(state, context, dateSpanMeta, filterConfig)) { + return false; + } + return true; + } + // Moving Event Validation + // ------------------------------------------------------------------------------------------------------------------------ + function isInteractionPropsValid(state, context, dateSpanMeta, filterConfig) { + var currentState = context.getCurrentData(); + var interaction = state.eventDrag; // HACK: the eventDrag props is used for ALL interactions + var subjectEventStore = interaction.mutatedEvents; + var subjectDefs = subjectEventStore.defs; + var subjectInstances = subjectEventStore.instances; + var subjectConfigs = compileEventUis(subjectDefs, interaction.isEvent ? + state.eventUiBases : + { '': currentState.selectionConfig }); + if (filterConfig) { + subjectConfigs = mapHash(subjectConfigs, filterConfig); + } + // exclude the subject events. TODO: exclude defs too? + var otherEventStore = excludeInstances(state.eventStore, interaction.affectedEvents.instances); + var otherDefs = otherEventStore.defs; + var otherInstances = otherEventStore.instances; + var otherConfigs = compileEventUis(otherDefs, state.eventUiBases); + for (var subjectInstanceId in subjectInstances) { + var subjectInstance = subjectInstances[subjectInstanceId]; + var subjectRange = subjectInstance.range; + var subjectConfig = subjectConfigs[subjectInstance.defId]; + var subjectDef = subjectDefs[subjectInstance.defId]; + // constraint + if (!allConstraintsPass(subjectConfig.constraints, subjectRange, otherEventStore, state.businessHours, context)) { + return false; + } + // overlap + var eventOverlap = context.options.eventOverlap; + var eventOverlapFunc = typeof eventOverlap === 'function' ? eventOverlap : null; + for (var otherInstanceId in otherInstances) { + var otherInstance = otherInstances[otherInstanceId]; + // intersect! evaluate + if (rangesIntersect(subjectRange, otherInstance.range)) { + var otherOverlap = otherConfigs[otherInstance.defId].overlap; + // consider the other event's overlap. only do this if the subject event is a "real" event + if (otherOverlap === false && interaction.isEvent) { + return false; + } + if (subjectConfig.overlap === false) { + return false; + } + if (eventOverlapFunc && !eventOverlapFunc(new EventApi(context, otherDefs[otherInstance.defId], otherInstance), // still event + new EventApi(context, subjectDef, subjectInstance))) { + return false; + } + } + } + // allow (a function) + var calendarEventStore = currentState.eventStore; // need global-to-calendar, not local to component (splittable)state + for (var _i = 0, _a = subjectConfig.allows; _i < _a.length; _i++) { + var subjectAllow = _a[_i]; + var subjectDateSpan = __assign(__assign({}, dateSpanMeta), { range: subjectInstance.range, allDay: subjectDef.allDay }); + var origDef = calendarEventStore.defs[subjectDef.defId]; + var origInstance = calendarEventStore.instances[subjectInstanceId]; + var eventApi = void 0; + if (origDef) { // was previously in the calendar + eventApi = new EventApi(context, origDef, origInstance); + } + else { // was an external event + eventApi = new EventApi(context, subjectDef); // no instance, because had no dates + } + if (!subjectAllow(buildDateSpanApiWithContext(subjectDateSpan, context), eventApi)) { + return false; + } + } + } + return true; + } + // Date Selection Validation + // ------------------------------------------------------------------------------------------------------------------------ + function isDateSelectionPropsValid(state, context, dateSpanMeta, filterConfig) { + var relevantEventStore = state.eventStore; + var relevantDefs = relevantEventStore.defs; + var relevantInstances = relevantEventStore.instances; + var selection = state.dateSelection; + var selectionRange = selection.range; + var selectionConfig = context.getCurrentData().selectionConfig; + if (filterConfig) { + selectionConfig = filterConfig(selectionConfig); + } + // constraint + if (!allConstraintsPass(selectionConfig.constraints, selectionRange, relevantEventStore, state.businessHours, context)) { + return false; + } + // overlap + var selectOverlap = context.options.selectOverlap; + var selectOverlapFunc = typeof selectOverlap === 'function' ? selectOverlap : null; + for (var relevantInstanceId in relevantInstances) { + var relevantInstance = relevantInstances[relevantInstanceId]; + // intersect! evaluate + if (rangesIntersect(selectionRange, relevantInstance.range)) { + if (selectionConfig.overlap === false) { + return false; + } + if (selectOverlapFunc && !selectOverlapFunc(new EventApi(context, relevantDefs[relevantInstance.defId], relevantInstance), null)) { + return false; + } + } + } + // allow (a function) + for (var _i = 0, _a = selectionConfig.allows; _i < _a.length; _i++) { + var selectionAllow = _a[_i]; + var fullDateSpan = __assign(__assign({}, dateSpanMeta), selection); + if (!selectionAllow(buildDateSpanApiWithContext(fullDateSpan, context), null)) { + return false; + } + } + return true; + } + // Constraint Utils + // ------------------------------------------------------------------------------------------------------------------------ + function allConstraintsPass(constraints, subjectRange, otherEventStore, businessHoursUnexpanded, context) { + for (var _i = 0, constraints_1 = constraints; _i < constraints_1.length; _i++) { + var constraint = constraints_1[_i]; + if (!anyRangesContainRange(constraintToRanges(constraint, subjectRange, otherEventStore, businessHoursUnexpanded, context), subjectRange)) { + return false; + } + } + return true; + } + function constraintToRanges(constraint, subjectRange, // for expanding a recurring constraint, or expanding business hours + otherEventStore, // for if constraint is an even group ID + businessHoursUnexpanded, // for if constraint is 'businessHours' + context) { + if (constraint === 'businessHours') { + return eventStoreToRanges(expandRecurring(businessHoursUnexpanded, subjectRange, context)); + } + if (typeof constraint === 'string') { // an group ID + return eventStoreToRanges(filterEventStoreDefs(otherEventStore, function (eventDef) { return eventDef.groupId === constraint; })); + } + if (typeof constraint === 'object' && constraint) { // non-null object + return eventStoreToRanges(expandRecurring(constraint, subjectRange, context)); + } + return []; // if it's false + } + // TODO: move to event-store file? + function eventStoreToRanges(eventStore) { + var instances = eventStore.instances; + var ranges = []; + for (var instanceId in instances) { + ranges.push(instances[instanceId].range); + } + return ranges; + } + // TODO: move to geom file? + function anyRangesContainRange(outerRanges, innerRange) { + for (var _i = 0, outerRanges_1 = outerRanges; _i < outerRanges_1.length; _i++) { + var outerRange = outerRanges_1[_i]; + if (rangeContainsRange(outerRange, innerRange)) { + return true; + } + } + return false; + } + + /* + an INTERACTABLE date component + + PURPOSES: + - hook up to fg, fill, and mirror renderers + - interface for dragging and hits + */ + var DateComponent = /** @class */ (function (_super) { + __extends(DateComponent, _super); + function DateComponent() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.uid = guid(); + return _this; + } + // Hit System + // ----------------------------------------------------------------------------------------------------------------- + DateComponent.prototype.prepareHits = function () { + }; + DateComponent.prototype.queryHit = function (positionLeft, positionTop, elWidth, elHeight) { + return null; // this should be abstract + }; + // Validation + // ----------------------------------------------------------------------------------------------------------------- + DateComponent.prototype.isInteractionValid = function (interaction) { + var dateProfile = this.props.dateProfile; // HACK + var instances = interaction.mutatedEvents.instances; + if (dateProfile) { // HACK for MorePopover + for (var instanceId in instances) { + if (!rangeContainsRange(dateProfile.validRange, instances[instanceId].range)) { + return false; + } + } + } + return isInteractionValid(interaction, this.context); + }; + DateComponent.prototype.isDateSelectionValid = function (selection) { + var dateProfile = this.props.dateProfile; // HACK + if (dateProfile && // HACK for MorePopover + !rangeContainsRange(dateProfile.validRange, selection.range)) { + return false; + } + return isDateSelectionValid(selection, this.context); + }; + // Pointer Interaction Utils + // ----------------------------------------------------------------------------------------------------------------- + DateComponent.prototype.isValidSegDownEl = function (el) { + return !this.props.eventDrag && // HACK + !this.props.eventResize && // HACK + !elementClosest(el, '.fc-event-mirror'); + }; + DateComponent.prototype.isValidDateDownEl = function (el) { + return !elementClosest(el, '.fc-event:not(.fc-bg-event)') && + !elementClosest(el, '.fc-daygrid-more-link') && // a "more.." link + !elementClosest(el, 'a[data-navlink]') && // a clickable nav link + !elementClosest(el, '.fc-popover'); // hack + }; + return DateComponent; + }(BaseComponent)); + + // TODO: easier way to add new hooks? need to update a million things + function createPlugin(input) { + return { + id: guid(), + deps: input.deps || [], + reducers: input.reducers || [], + isLoadingFuncs: input.isLoadingFuncs || [], + contextInit: [].concat(input.contextInit || []), + eventRefiners: input.eventRefiners || {}, + eventDefMemberAdders: input.eventDefMemberAdders || [], + eventSourceRefiners: input.eventSourceRefiners || {}, + isDraggableTransformers: input.isDraggableTransformers || [], + eventDragMutationMassagers: input.eventDragMutationMassagers || [], + eventDefMutationAppliers: input.eventDefMutationAppliers || [], + dateSelectionTransformers: input.dateSelectionTransformers || [], + datePointTransforms: input.datePointTransforms || [], + dateSpanTransforms: input.dateSpanTransforms || [], + views: input.views || {}, + viewPropsTransformers: input.viewPropsTransformers || [], + isPropsValid: input.isPropsValid || null, + externalDefTransforms: input.externalDefTransforms || [], + eventResizeJoinTransforms: input.eventResizeJoinTransforms || [], + viewContainerAppends: input.viewContainerAppends || [], + eventDropTransformers: input.eventDropTransformers || [], + componentInteractions: input.componentInteractions || [], + calendarInteractions: input.calendarInteractions || [], + themeClasses: input.themeClasses || {}, + eventSourceDefs: input.eventSourceDefs || [], + cmdFormatter: input.cmdFormatter, + recurringTypes: input.recurringTypes || [], + namedTimeZonedImpl: input.namedTimeZonedImpl, + initialView: input.initialView || '', + elementDraggingImpl: input.elementDraggingImpl, + optionChangeHandlers: input.optionChangeHandlers || {}, + scrollGridImpl: input.scrollGridImpl || null, + contentTypeHandlers: input.contentTypeHandlers || {}, + listenerRefiners: input.listenerRefiners || {}, + optionRefiners: input.optionRefiners || {}, + propSetHandlers: input.propSetHandlers || {}, + }; + } + function buildPluginHooks(pluginDefs, globalDefs) { + var isAdded = {}; + var hooks = { + reducers: [], + isLoadingFuncs: [], + contextInit: [], + eventRefiners: {}, + eventDefMemberAdders: [], + eventSourceRefiners: {}, + isDraggableTransformers: [], + eventDragMutationMassagers: [], + eventDefMutationAppliers: [], + dateSelectionTransformers: [], + datePointTransforms: [], + dateSpanTransforms: [], + views: {}, + viewPropsTransformers: [], + isPropsValid: null, + externalDefTransforms: [], + eventResizeJoinTransforms: [], + viewContainerAppends: [], + eventDropTransformers: [], + componentInteractions: [], + calendarInteractions: [], + themeClasses: {}, + eventSourceDefs: [], + cmdFormatter: null, + recurringTypes: [], + namedTimeZonedImpl: null, + initialView: '', + elementDraggingImpl: null, + optionChangeHandlers: {}, + scrollGridImpl: null, + contentTypeHandlers: {}, + listenerRefiners: {}, + optionRefiners: {}, + propSetHandlers: {}, + }; + function addDefs(defs) { + for (var _i = 0, defs_1 = defs; _i < defs_1.length; _i++) { + var def = defs_1[_i]; + if (!isAdded[def.id]) { + isAdded[def.id] = true; + addDefs(def.deps); + hooks = combineHooks(hooks, def); + } + } + } + if (pluginDefs) { + addDefs(pluginDefs); + } + addDefs(globalDefs); + return hooks; + } + function buildBuildPluginHooks() { + var currentOverrideDefs = []; + var currentGlobalDefs = []; + var currentHooks; + return function (overrideDefs, globalDefs) { + if (!currentHooks || !isArraysEqual(overrideDefs, currentOverrideDefs) || !isArraysEqual(globalDefs, currentGlobalDefs)) { + currentHooks = buildPluginHooks(overrideDefs, globalDefs); + } + currentOverrideDefs = overrideDefs; + currentGlobalDefs = globalDefs; + return currentHooks; + }; + } + function combineHooks(hooks0, hooks1) { + return { + reducers: hooks0.reducers.concat(hooks1.reducers), + isLoadingFuncs: hooks0.isLoadingFuncs.concat(hooks1.isLoadingFuncs), + contextInit: hooks0.contextInit.concat(hooks1.contextInit), + eventRefiners: __assign(__assign({}, hooks0.eventRefiners), hooks1.eventRefiners), + eventDefMemberAdders: hooks0.eventDefMemberAdders.concat(hooks1.eventDefMemberAdders), + eventSourceRefiners: __assign(__assign({}, hooks0.eventSourceRefiners), hooks1.eventSourceRefiners), + isDraggableTransformers: hooks0.isDraggableTransformers.concat(hooks1.isDraggableTransformers), + eventDragMutationMassagers: hooks0.eventDragMutationMassagers.concat(hooks1.eventDragMutationMassagers), + eventDefMutationAppliers: hooks0.eventDefMutationAppliers.concat(hooks1.eventDefMutationAppliers), + dateSelectionTransformers: hooks0.dateSelectionTransformers.concat(hooks1.dateSelectionTransformers), + datePointTransforms: hooks0.datePointTransforms.concat(hooks1.datePointTransforms), + dateSpanTransforms: hooks0.dateSpanTransforms.concat(hooks1.dateSpanTransforms), + views: __assign(__assign({}, hooks0.views), hooks1.views), + viewPropsTransformers: hooks0.viewPropsTransformers.concat(hooks1.viewPropsTransformers), + isPropsValid: hooks1.isPropsValid || hooks0.isPropsValid, + externalDefTransforms: hooks0.externalDefTransforms.concat(hooks1.externalDefTransforms), + eventResizeJoinTransforms: hooks0.eventResizeJoinTransforms.concat(hooks1.eventResizeJoinTransforms), + viewContainerAppends: hooks0.viewContainerAppends.concat(hooks1.viewContainerAppends), + eventDropTransformers: hooks0.eventDropTransformers.concat(hooks1.eventDropTransformers), + calendarInteractions: hooks0.calendarInteractions.concat(hooks1.calendarInteractions), + componentInteractions: hooks0.componentInteractions.concat(hooks1.componentInteractions), + themeClasses: __assign(__assign({}, hooks0.themeClasses), hooks1.themeClasses), + eventSourceDefs: hooks0.eventSourceDefs.concat(hooks1.eventSourceDefs), + cmdFormatter: hooks1.cmdFormatter || hooks0.cmdFormatter, + recurringTypes: hooks0.recurringTypes.concat(hooks1.recurringTypes), + namedTimeZonedImpl: hooks1.namedTimeZonedImpl || hooks0.namedTimeZonedImpl, + initialView: hooks0.initialView || hooks1.initialView, + elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl, + optionChangeHandlers: __assign(__assign({}, hooks0.optionChangeHandlers), hooks1.optionChangeHandlers), + scrollGridImpl: hooks1.scrollGridImpl || hooks0.scrollGridImpl, + contentTypeHandlers: __assign(__assign({}, hooks0.contentTypeHandlers), hooks1.contentTypeHandlers), + listenerRefiners: __assign(__assign({}, hooks0.listenerRefiners), hooks1.listenerRefiners), + optionRefiners: __assign(__assign({}, hooks0.optionRefiners), hooks1.optionRefiners), + propSetHandlers: __assign(__assign({}, hooks0.propSetHandlers), hooks1.propSetHandlers), + }; + } + + var StandardTheme = /** @class */ (function (_super) { + __extends(StandardTheme, _super); + function StandardTheme() { + return _super !== null && _super.apply(this, arguments) || this; + } + return StandardTheme; + }(Theme)); + StandardTheme.prototype.classes = { + root: 'fc-theme-standard', + tableCellShaded: 'fc-cell-shaded', + buttonGroup: 'fc-button-group', + button: 'fc-button fc-button-primary', + buttonActive: 'fc-button-active', + }; + StandardTheme.prototype.baseIconClass = 'fc-icon'; + StandardTheme.prototype.iconClasses = { + close: 'fc-icon-x', + prev: 'fc-icon-chevron-left', + next: 'fc-icon-chevron-right', + prevYear: 'fc-icon-chevrons-left', + nextYear: 'fc-icon-chevrons-right', + }; + StandardTheme.prototype.rtlIconClasses = { + prev: 'fc-icon-chevron-right', + next: 'fc-icon-chevron-left', + prevYear: 'fc-icon-chevrons-right', + nextYear: 'fc-icon-chevrons-left', + }; + StandardTheme.prototype.iconOverrideOption = 'buttonIcons'; // TODO: make TS-friendly + StandardTheme.prototype.iconOverrideCustomButtonOption = 'icon'; + StandardTheme.prototype.iconOverridePrefix = 'fc-icon-'; + + function compileViewDefs(defaultConfigs, overrideConfigs) { + var hash = {}; + var viewType; + for (viewType in defaultConfigs) { + ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs); + } + for (viewType in overrideConfigs) { + ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs); + } + return hash; + } + function ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs) { + if (hash[viewType]) { + return hash[viewType]; + } + var viewDef = buildViewDef(viewType, hash, defaultConfigs, overrideConfigs); + if (viewDef) { + hash[viewType] = viewDef; + } + return viewDef; + } + function buildViewDef(viewType, hash, defaultConfigs, overrideConfigs) { + var defaultConfig = defaultConfigs[viewType]; + var overrideConfig = overrideConfigs[viewType]; + var queryProp = function (name) { return ((defaultConfig && defaultConfig[name] !== null) ? defaultConfig[name] : + ((overrideConfig && overrideConfig[name] !== null) ? overrideConfig[name] : null)); }; + var theComponent = queryProp('component'); + var superType = queryProp('superType'); + var superDef = null; + if (superType) { + if (superType === viewType) { + throw new Error('Can\'t have a custom view type that references itself'); + } + superDef = ensureViewDef(superType, hash, defaultConfigs, overrideConfigs); + } + if (!theComponent && superDef) { + theComponent = superDef.component; + } + if (!theComponent) { + return null; // don't throw a warning, might be settings for a single-unit view + } + return { + type: viewType, + component: theComponent, + defaults: __assign(__assign({}, (superDef ? superDef.defaults : {})), (defaultConfig ? defaultConfig.rawOptions : {})), + overrides: __assign(__assign({}, (superDef ? superDef.overrides : {})), (overrideConfig ? overrideConfig.rawOptions : {})), + }; + } + + /* eslint max-classes-per-file: off */ + // NOTE: in JSX, you should always use this class with arg. otherwise, will default to any??? + var RenderHook = /** @class */ (function (_super) { + __extends(RenderHook, _super); + function RenderHook() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.rootElRef = createRef(); + _this.handleRootEl = function (el) { + setRef(_this.rootElRef, el); + if (_this.props.elRef) { + setRef(_this.props.elRef, el); + } + }; + return _this; + } + RenderHook.prototype.render = function () { + var _this = this; + var props = this.props; + var hookProps = props.hookProps; + return (createElement(MountHook, { hookProps: hookProps, didMount: props.didMount, willUnmount: props.willUnmount, elRef: this.handleRootEl }, function (rootElRef) { return (createElement(ContentHook, { hookProps: hookProps, content: props.content, defaultContent: props.defaultContent, backupElRef: _this.rootElRef }, function (innerElRef, innerContent) { return props.children(rootElRef, normalizeClassNames(props.classNames, hookProps), innerElRef, innerContent); })); })); + }; + return RenderHook; + }(BaseComponent)); + // TODO: rename to be about function, not default. use in above type + // for forcing rerender of components that use the ContentHook + var CustomContentRenderContext = createContext$1(0); + function ContentHook(props) { + return (createElement(CustomContentRenderContext.Consumer, null, function (renderId) { return (createElement(ContentHookInner, __assign({ renderId: renderId }, props))); })); + } + var ContentHookInner = /** @class */ (function (_super) { + __extends(ContentHookInner, _super); + function ContentHookInner() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.innerElRef = createRef(); + return _this; + } + ContentHookInner.prototype.render = function () { + return this.props.children(this.innerElRef, this.renderInnerContent()); + }; + ContentHookInner.prototype.componentDidMount = function () { + this.updateCustomContent(); + }; + ContentHookInner.prototype.componentDidUpdate = function () { + this.updateCustomContent(); + }; + ContentHookInner.prototype.componentWillUnmount = function () { + if (this.customContentInfo && this.customContentInfo.destroy) { + this.customContentInfo.destroy(); + } + }; + ContentHookInner.prototype.renderInnerContent = function () { + var contentTypeHandlers = this.context.pluginHooks.contentTypeHandlers; + var _a = this, props = _a.props, customContentInfo = _a.customContentInfo; + var rawVal = props.content; + var innerContent = normalizeContent(rawVal, props.hookProps); + var innerContentVDom = null; + if (innerContent === undefined) { // use the default + innerContent = normalizeContent(props.defaultContent, props.hookProps); + } + if (innerContent !== undefined) { // we allow custom content handlers to return nothing + if (customContentInfo) { + customContentInfo.contentVal = innerContent[customContentInfo.contentKey]; + } + else if (typeof innerContent === 'object') { + // look for a prop that would indicate a custom content handler is needed + for (var contentKey in contentTypeHandlers) { + if (innerContent[contentKey] !== undefined) { + var stuff = contentTypeHandlers[contentKey](); + customContentInfo = this.customContentInfo = __assign({ contentKey: contentKey, contentVal: innerContent[contentKey] }, stuff); + break; + } + } + } + if (customContentInfo) { + innerContentVDom = []; // signal that something was specified + } + else { + innerContentVDom = innerContent; // assume a [p]react vdom node. use it + } + } + return innerContentVDom; + }; + ContentHookInner.prototype.updateCustomContent = function () { + if (this.customContentInfo) { + this.customContentInfo.render(this.innerElRef.current || this.props.backupElRef.current, // the element to render into + this.customContentInfo.contentVal); + } + }; + return ContentHookInner; + }(BaseComponent)); + var MountHook = /** @class */ (function (_super) { + __extends(MountHook, _super); + function MountHook() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.handleRootEl = function (rootEl) { + _this.rootEl = rootEl; + if (_this.props.elRef) { + setRef(_this.props.elRef, rootEl); + } + }; + return _this; + } + MountHook.prototype.render = function () { + return this.props.children(this.handleRootEl); + }; + MountHook.prototype.componentDidMount = function () { + var callback = this.props.didMount; + if (callback) { + callback(__assign(__assign({}, this.props.hookProps), { el: this.rootEl })); + } + }; + MountHook.prototype.componentWillUnmount = function () { + var callback = this.props.willUnmount; + if (callback) { + callback(__assign(__assign({}, this.props.hookProps), { el: this.rootEl })); + } + }; + return MountHook; + }(BaseComponent)); + function buildClassNameNormalizer() { + var currentGenerator; + var currentHookProps; + var currentClassNames = []; + return function (generator, hookProps) { + if (!currentHookProps || !isPropsEqual(currentHookProps, hookProps) || generator !== currentGenerator) { + currentGenerator = generator; + currentHookProps = hookProps; + currentClassNames = normalizeClassNames(generator, hookProps); + } + return currentClassNames; + }; + } + function normalizeClassNames(classNames, hookProps) { + if (typeof classNames === 'function') { + classNames = classNames(hookProps); + } + return parseClassNames(classNames); + } + function normalizeContent(input, hookProps) { + if (typeof input === 'function') { + return input(hookProps, createElement); // give the function the vdom-creation func + } + return input; + } + + var ViewRoot = /** @class */ (function (_super) { + __extends(ViewRoot, _super); + function ViewRoot() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.normalizeClassNames = buildClassNameNormalizer(); + return _this; + } + ViewRoot.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context; + var options = context.options; + var hookProps = { view: context.viewApi }; + var customClassNames = this.normalizeClassNames(options.viewClassNames, hookProps); + return (createElement(MountHook, { hookProps: hookProps, didMount: options.viewDidMount, willUnmount: options.viewWillUnmount, elRef: props.elRef }, function (rootElRef) { return props.children(rootElRef, ["fc-" + props.viewSpec.type + "-view", 'fc-view'].concat(customClassNames)); })); + }; + return ViewRoot; + }(BaseComponent)); + + function parseViewConfigs(inputs) { + return mapHash(inputs, parseViewConfig); + } + function parseViewConfig(input) { + var rawOptions = typeof input === 'function' ? + { component: input } : + input; + var component = rawOptions.component; + if (rawOptions.content) { + component = createViewHookComponent(rawOptions); + // TODO: remove content/classNames/didMount/etc from options? + } + return { + superType: rawOptions.type, + component: component, + rawOptions: rawOptions, + }; + } + function createViewHookComponent(options) { + return function (viewProps) { return (createElement(ViewContextType.Consumer, null, function (context) { return (createElement(ViewRoot, { viewSpec: context.viewSpec }, function (viewElRef, viewClassNames) { + var hookProps = __assign(__assign({}, viewProps), { nextDayThreshold: context.options.nextDayThreshold }); + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.classNames, content: options.content, didMount: options.didMount, willUnmount: options.willUnmount, elRef: viewElRef }, function (rootElRef, customClassNames, innerElRef, innerContent) { return (createElement("div", { className: viewClassNames.concat(customClassNames).join(' '), ref: rootElRef }, innerContent)); })); + })); })); }; + } + + function buildViewSpecs(defaultInputs, optionOverrides, dynamicOptionOverrides, localeDefaults) { + var defaultConfigs = parseViewConfigs(defaultInputs); + var overrideConfigs = parseViewConfigs(optionOverrides.views); + var viewDefs = compileViewDefs(defaultConfigs, overrideConfigs); + return mapHash(viewDefs, function (viewDef) { return buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides, localeDefaults); }); + } + function buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides, localeDefaults) { + var durationInput = viewDef.overrides.duration || + viewDef.defaults.duration || + dynamicOptionOverrides.duration || + optionOverrides.duration; + var duration = null; + var durationUnit = ''; + var singleUnit = ''; + var singleUnitOverrides = {}; + if (durationInput) { + duration = createDurationCached(durationInput); + if (duration) { // valid? + var denom = greatestDurationDenominator(duration); + durationUnit = denom.unit; + if (denom.value === 1) { + singleUnit = durationUnit; + singleUnitOverrides = overrideConfigs[durationUnit] ? overrideConfigs[durationUnit].rawOptions : {}; + } + } + } + var queryButtonText = function (optionsSubset) { + var buttonTextMap = optionsSubset.buttonText || {}; + var buttonTextKey = viewDef.defaults.buttonTextKey; + if (buttonTextKey != null && buttonTextMap[buttonTextKey] != null) { + return buttonTextMap[buttonTextKey]; + } + if (buttonTextMap[viewDef.type] != null) { + return buttonTextMap[viewDef.type]; + } + if (buttonTextMap[singleUnit] != null) { + return buttonTextMap[singleUnit]; + } + return null; + }; + return { + type: viewDef.type, + component: viewDef.component, + duration: duration, + durationUnit: durationUnit, + singleUnit: singleUnit, + optionDefaults: viewDef.defaults, + optionOverrides: __assign(__assign({}, singleUnitOverrides), viewDef.overrides), + buttonTextOverride: queryButtonText(dynamicOptionOverrides) || + queryButtonText(optionOverrides) || // constructor-specified buttonText lookup hash takes precedence + viewDef.overrides.buttonText, + buttonTextDefault: queryButtonText(localeDefaults) || + viewDef.defaults.buttonText || + queryButtonText(BASE_OPTION_DEFAULTS) || + viewDef.type, + }; + } + // hack to get memoization working + var durationInputMap = {}; + function createDurationCached(durationInput) { + var json = JSON.stringify(durationInput); + var res = durationInputMap[json]; + if (res === undefined) { + res = createDuration(durationInput); + durationInputMap[json] = res; + } + return res; + } + + var DateProfileGenerator = /** @class */ (function () { + function DateProfileGenerator(props) { + this.props = props; + this.nowDate = getNow(props.nowInput, props.dateEnv); + this.initHiddenDays(); + } + /* Date Range Computation + ------------------------------------------------------------------------------------------------------------------*/ + // Builds a structure with info about what the dates/ranges will be for the "prev" view. + DateProfileGenerator.prototype.buildPrev = function (currentDateProfile, currentDate, forceToValid) { + var dateEnv = this.props.dateEnv; + var prevDate = dateEnv.subtract(dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month + currentDateProfile.dateIncrement); + return this.build(prevDate, -1, forceToValid); + }; + // Builds a structure with info about what the dates/ranges will be for the "next" view. + DateProfileGenerator.prototype.buildNext = function (currentDateProfile, currentDate, forceToValid) { + var dateEnv = this.props.dateEnv; + var nextDate = dateEnv.add(dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month + currentDateProfile.dateIncrement); + return this.build(nextDate, 1, forceToValid); + }; + // Builds a structure holding dates/ranges for rendering around the given date. + // Optional direction param indicates whether the date is being incremented/decremented + // from its previous value. decremented = -1, incremented = 1 (default). + DateProfileGenerator.prototype.build = function (currentDate, direction, forceToValid) { + if (forceToValid === void 0) { forceToValid = true; } + var props = this.props; + var validRange; + var currentInfo; + var isRangeAllDay; + var renderRange; + var activeRange; + var isValid; + validRange = this.buildValidRange(); + validRange = this.trimHiddenDays(validRange); + if (forceToValid) { + currentDate = constrainMarkerToRange(currentDate, validRange); + } + currentInfo = this.buildCurrentRangeInfo(currentDate, direction); + isRangeAllDay = /^(year|month|week|day)$/.test(currentInfo.unit); + renderRange = this.buildRenderRange(this.trimHiddenDays(currentInfo.range), currentInfo.unit, isRangeAllDay); + renderRange = this.trimHiddenDays(renderRange); + activeRange = renderRange; + if (!props.showNonCurrentDates) { + activeRange = intersectRanges(activeRange, currentInfo.range); + } + activeRange = this.adjustActiveRange(activeRange); + activeRange = intersectRanges(activeRange, validRange); // might return null + // it's invalid if the originally requested date is not contained, + // or if the range is completely outside of the valid range. + isValid = rangesIntersect(currentInfo.range, validRange); + return { + // constraint for where prev/next operations can go and where events can be dragged/resized to. + // an object with optional start and end properties. + validRange: validRange, + // range the view is formally responsible for. + // for example, a month view might have 1st-31st, excluding padded dates + currentRange: currentInfo.range, + // name of largest unit being displayed, like "month" or "week" + currentRangeUnit: currentInfo.unit, + isRangeAllDay: isRangeAllDay, + // dates that display events and accept drag-n-drop + // will be `null` if no dates accept events + activeRange: activeRange, + // date range with a rendered skeleton + // includes not-active days that need some sort of DOM + renderRange: renderRange, + // Duration object that denotes the first visible time of any given day + slotMinTime: props.slotMinTime, + // Duration object that denotes the exclusive visible end time of any given day + slotMaxTime: props.slotMaxTime, + isValid: isValid, + // how far the current date will move for a prev/next operation + dateIncrement: this.buildDateIncrement(currentInfo.duration), + }; + }; + // Builds an object with optional start/end properties. + // Indicates the minimum/maximum dates to display. + // not responsible for trimming hidden days. + DateProfileGenerator.prototype.buildValidRange = function () { + var input = this.props.validRangeInput; + var simpleInput = typeof input === 'function' + ? input.call(this.props.calendarApi, this.nowDate) + : input; + return this.refineRange(simpleInput) || + { start: null, end: null }; // completely open-ended + }; + // Builds a structure with info about the "current" range, the range that is + // highlighted as being the current month for example. + // See build() for a description of `direction`. + // Guaranteed to have `range` and `unit` properties. `duration` is optional. + DateProfileGenerator.prototype.buildCurrentRangeInfo = function (date, direction) { + var props = this.props; + var duration = null; + var unit = null; + var range = null; + var dayCount; + if (props.duration) { + duration = props.duration; + unit = props.durationUnit; + range = this.buildRangeFromDuration(date, direction, duration, unit); + } + else if ((dayCount = this.props.dayCount)) { + unit = 'day'; + range = this.buildRangeFromDayCount(date, direction, dayCount); + } + else if ((range = this.buildCustomVisibleRange(date))) { + unit = props.dateEnv.greatestWholeUnit(range.start, range.end).unit; + } + else { + duration = this.getFallbackDuration(); + unit = greatestDurationDenominator(duration).unit; + range = this.buildRangeFromDuration(date, direction, duration, unit); + } + return { duration: duration, unit: unit, range: range }; + }; + DateProfileGenerator.prototype.getFallbackDuration = function () { + return createDuration({ day: 1 }); + }; + // Returns a new activeRange to have time values (un-ambiguate) + // slotMinTime or slotMaxTime causes the range to expand. + DateProfileGenerator.prototype.adjustActiveRange = function (range) { + var _a = this.props, dateEnv = _a.dateEnv, usesMinMaxTime = _a.usesMinMaxTime, slotMinTime = _a.slotMinTime, slotMaxTime = _a.slotMaxTime; + var start = range.start, end = range.end; + if (usesMinMaxTime) { + // expand active range if slotMinTime is negative (why not when positive?) + if (asRoughDays(slotMinTime) < 0) { + start = startOfDay(start); // necessary? + start = dateEnv.add(start, slotMinTime); + } + // expand active range if slotMaxTime is beyond one day (why not when negative?) + if (asRoughDays(slotMaxTime) > 1) { + end = startOfDay(end); // necessary? + end = addDays(end, -1); + end = dateEnv.add(end, slotMaxTime); + } + } + return { start: start, end: end }; + }; + // Builds the "current" range when it is specified as an explicit duration. + // `unit` is the already-computed greatestDurationDenominator unit of duration. + DateProfileGenerator.prototype.buildRangeFromDuration = function (date, direction, duration, unit) { + var _a = this.props, dateEnv = _a.dateEnv, dateAlignment = _a.dateAlignment; + var start; + var end; + var res; + // compute what the alignment should be + if (!dateAlignment) { + var dateIncrement = this.props.dateIncrement; + if (dateIncrement) { + // use the smaller of the two units + if (asRoughMs(dateIncrement) < asRoughMs(duration)) { + dateAlignment = greatestDurationDenominator(dateIncrement).unit; + } + else { + dateAlignment = unit; + } + } + else { + dateAlignment = unit; + } + } + // if the view displays a single day or smaller + if (asRoughDays(duration) <= 1) { + if (this.isHiddenDay(start)) { + start = this.skipHiddenDays(start, direction); + start = startOfDay(start); + } + } + function computeRes() { + start = dateEnv.startOf(date, dateAlignment); + end = dateEnv.add(start, duration); + res = { start: start, end: end }; + } + computeRes(); + // if range is completely enveloped by hidden days, go past the hidden days + if (!this.trimHiddenDays(res)) { + date = this.skipHiddenDays(date, direction); + computeRes(); + } + return res; + }; + // Builds the "current" range when a dayCount is specified. + DateProfileGenerator.prototype.buildRangeFromDayCount = function (date, direction, dayCount) { + var _a = this.props, dateEnv = _a.dateEnv, dateAlignment = _a.dateAlignment; + var runningCount = 0; + var start = date; + var end; + if (dateAlignment) { + start = dateEnv.startOf(start, dateAlignment); + } + start = startOfDay(start); + start = this.skipHiddenDays(start, direction); + end = start; + do { + end = addDays(end, 1); + if (!this.isHiddenDay(end)) { + runningCount += 1; + } + } while (runningCount < dayCount); + return { start: start, end: end }; + }; + // Builds a normalized range object for the "visible" range, + // which is a way to define the currentRange and activeRange at the same time. + DateProfileGenerator.prototype.buildCustomVisibleRange = function (date) { + var props = this.props; + var input = props.visibleRangeInput; + var simpleInput = typeof input === 'function' + ? input.call(props.calendarApi, props.dateEnv.toDate(date)) + : input; + var range = this.refineRange(simpleInput); + if (range && (range.start == null || range.end == null)) { + return null; + } + return range; + }; + // Computes the range that will represent the element/cells for *rendering*, + // but which may have voided days/times. + // not responsible for trimming hidden days. + DateProfileGenerator.prototype.buildRenderRange = function (currentRange, currentRangeUnit, isRangeAllDay) { + return currentRange; + }; + // Compute the duration value that should be added/substracted to the current date + // when a prev/next operation happens. + DateProfileGenerator.prototype.buildDateIncrement = function (fallback) { + var dateIncrement = this.props.dateIncrement; + var customAlignment; + if (dateIncrement) { + return dateIncrement; + } + if ((customAlignment = this.props.dateAlignment)) { + return createDuration(1, customAlignment); + } + if (fallback) { + return fallback; + } + return createDuration({ days: 1 }); + }; + DateProfileGenerator.prototype.refineRange = function (rangeInput) { + if (rangeInput) { + var range = parseRange(rangeInput, this.props.dateEnv); + if (range) { + range = computeVisibleDayRange(range); + } + return range; + } + return null; + }; + /* Hidden Days + ------------------------------------------------------------------------------------------------------------------*/ + // Initializes internal variables related to calculating hidden days-of-week + DateProfileGenerator.prototype.initHiddenDays = function () { + var hiddenDays = this.props.hiddenDays || []; // array of day-of-week indices that are hidden + var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool) + var dayCnt = 0; + var i; + if (this.props.weekends === false) { + hiddenDays.push(0, 6); // 0=sunday, 6=saturday + } + for (i = 0; i < 7; i += 1) { + if (!(isHiddenDayHash[i] = hiddenDays.indexOf(i) !== -1)) { + dayCnt += 1; + } + } + if (!dayCnt) { + throw new Error('invalid hiddenDays'); // all days were hidden? bad. + } + this.isHiddenDayHash = isHiddenDayHash; + }; + // Remove days from the beginning and end of the range that are computed as hidden. + // If the whole range is trimmed off, returns null + DateProfileGenerator.prototype.trimHiddenDays = function (range) { + var start = range.start, end = range.end; + if (start) { + start = this.skipHiddenDays(start); + } + if (end) { + end = this.skipHiddenDays(end, -1, true); + } + if (start == null || end == null || start < end) { + return { start: start, end: end }; + } + return null; + }; + // Is the current day hidden? + // `day` is a day-of-week index (0-6), or a Date (used for UTC) + DateProfileGenerator.prototype.isHiddenDay = function (day) { + if (day instanceof Date) { + day = day.getUTCDay(); + } + return this.isHiddenDayHash[day]; + }; + // Incrementing the current day until it is no longer a hidden day, returning a copy. + // DOES NOT CONSIDER validRange! + // If the initial value of `date` is not a hidden day, don't do anything. + // Pass `isExclusive` as `true` if you are dealing with an end date. + // `inc` defaults to `1` (increment one day forward each time) + DateProfileGenerator.prototype.skipHiddenDays = function (date, inc, isExclusive) { + if (inc === void 0) { inc = 1; } + if (isExclusive === void 0) { isExclusive = false; } + while (this.isHiddenDayHash[(date.getUTCDay() + (isExclusive ? inc : 0) + 7) % 7]) { + date = addDays(date, inc); + } + return date; + }; + return DateProfileGenerator; + }()); + + function reduceViewType(viewType, action) { + switch (action.type) { + case 'CHANGE_VIEW_TYPE': + viewType = action.viewType; + } + return viewType; + } + + function reduceDynamicOptionOverrides(dynamicOptionOverrides, action) { + var _a; + switch (action.type) { + case 'SET_OPTION': + return __assign(__assign({}, dynamicOptionOverrides), (_a = {}, _a[action.optionName] = action.rawOptionValue, _a)); + default: + return dynamicOptionOverrides; + } + } + + function reduceDateProfile(currentDateProfile, action, currentDate, dateProfileGenerator) { + var dp; + switch (action.type) { + case 'CHANGE_VIEW_TYPE': + return dateProfileGenerator.build(action.dateMarker || currentDate); + case 'CHANGE_DATE': + if (!currentDateProfile.activeRange || + !rangeContainsMarker(currentDateProfile.currentRange, action.dateMarker) // don't move if date already in view + ) { + return dateProfileGenerator.build(action.dateMarker); + } + break; + case 'PREV': + dp = dateProfileGenerator.buildPrev(currentDateProfile, currentDate); + if (dp.isValid) { + return dp; + } + break; + case 'NEXT': + dp = dateProfileGenerator.buildNext(currentDateProfile, currentDate); + if (dp.isValid) { + return dp; + } + break; + } + return currentDateProfile; + } + + function initEventSources(calendarOptions, dateProfile, context) { + var activeRange = dateProfile ? dateProfile.activeRange : null; + return addSources({}, parseInitialSources(calendarOptions, context), activeRange, context); + } + function reduceEventSources(eventSources, action, dateProfile, context) { + var activeRange = dateProfile ? dateProfile.activeRange : null; // need this check? + switch (action.type) { + case 'ADD_EVENT_SOURCES': // already parsed + return addSources(eventSources, action.sources, activeRange, context); + case 'REMOVE_EVENT_SOURCE': + return removeSource(eventSources, action.sourceId); + case 'PREV': // TODO: how do we track all actions that affect dateProfile :( + case 'NEXT': + case 'CHANGE_DATE': + case 'CHANGE_VIEW_TYPE': + if (dateProfile) { + return fetchDirtySources(eventSources, activeRange, context); + } + return eventSources; + case 'FETCH_EVENT_SOURCES': + return fetchSourcesByIds(eventSources, action.sourceIds ? // why no type? + arrayToHash(action.sourceIds) : + excludeStaticSources(eventSources, context), activeRange, context); + case 'RECEIVE_EVENTS': + case 'RECEIVE_EVENT_ERROR': + return receiveResponse(eventSources, action.sourceId, action.fetchId, action.fetchRange); + case 'REMOVE_ALL_EVENT_SOURCES': + return {}; + default: + return eventSources; + } + } + function reduceEventSourcesNewTimeZone(eventSources, dateProfile, context) { + var activeRange = dateProfile ? dateProfile.activeRange : null; // need this check? + return fetchSourcesByIds(eventSources, excludeStaticSources(eventSources, context), activeRange, context); + } + function computeEventSourcesLoading(eventSources) { + for (var sourceId in eventSources) { + if (eventSources[sourceId].isFetching) { + return true; + } + } + return false; + } + function addSources(eventSourceHash, sources, fetchRange, context) { + var hash = {}; + for (var _i = 0, sources_1 = sources; _i < sources_1.length; _i++) { + var source = sources_1[_i]; + hash[source.sourceId] = source; + } + if (fetchRange) { + hash = fetchDirtySources(hash, fetchRange, context); + } + return __assign(__assign({}, eventSourceHash), hash); + } + function removeSource(eventSourceHash, sourceId) { + return filterHash(eventSourceHash, function (eventSource) { return eventSource.sourceId !== sourceId; }); + } + function fetchDirtySources(sourceHash, fetchRange, context) { + return fetchSourcesByIds(sourceHash, filterHash(sourceHash, function (eventSource) { return isSourceDirty(eventSource, fetchRange, context); }), fetchRange, context); + } + function isSourceDirty(eventSource, fetchRange, context) { + if (!doesSourceNeedRange(eventSource, context)) { + return !eventSource.latestFetchId; + } + return !context.options.lazyFetching || + !eventSource.fetchRange || + eventSource.isFetching || // always cancel outdated in-progress fetches + fetchRange.start < eventSource.fetchRange.start || + fetchRange.end > eventSource.fetchRange.end; + } + function fetchSourcesByIds(prevSources, sourceIdHash, fetchRange, context) { + var nextSources = {}; + for (var sourceId in prevSources) { + var source = prevSources[sourceId]; + if (sourceIdHash[sourceId]) { + nextSources[sourceId] = fetchSource(source, fetchRange, context); + } + else { + nextSources[sourceId] = source; + } + } + return nextSources; + } + function fetchSource(eventSource, fetchRange, context) { + var options = context.options, calendarApi = context.calendarApi; + var sourceDef = context.pluginHooks.eventSourceDefs[eventSource.sourceDefId]; + var fetchId = guid(); + sourceDef.fetch({ + eventSource: eventSource, + range: fetchRange, + context: context, + }, function (res) { + var rawEvents = res.rawEvents; + if (options.eventSourceSuccess) { + rawEvents = options.eventSourceSuccess.call(calendarApi, rawEvents, res.xhr) || rawEvents; + } + if (eventSource.success) { + rawEvents = eventSource.success.call(calendarApi, rawEvents, res.xhr) || rawEvents; + } + context.dispatch({ + type: 'RECEIVE_EVENTS', + sourceId: eventSource.sourceId, + fetchId: fetchId, + fetchRange: fetchRange, + rawEvents: rawEvents, + }); + }, function (error) { + console.warn(error.message, error); + if (options.eventSourceFailure) { + options.eventSourceFailure.call(calendarApi, error); + } + if (eventSource.failure) { + eventSource.failure(error); + } + context.dispatch({ + type: 'RECEIVE_EVENT_ERROR', + sourceId: eventSource.sourceId, + fetchId: fetchId, + fetchRange: fetchRange, + error: error, + }); + }); + return __assign(__assign({}, eventSource), { isFetching: true, latestFetchId: fetchId }); + } + function receiveResponse(sourceHash, sourceId, fetchId, fetchRange) { + var _a; + var eventSource = sourceHash[sourceId]; + if (eventSource && // not already removed + fetchId === eventSource.latestFetchId) { + return __assign(__assign({}, sourceHash), (_a = {}, _a[sourceId] = __assign(__assign({}, eventSource), { isFetching: false, fetchRange: fetchRange }), _a)); + } + return sourceHash; + } + function excludeStaticSources(eventSources, context) { + return filterHash(eventSources, function (eventSource) { return doesSourceNeedRange(eventSource, context); }); + } + function parseInitialSources(rawOptions, context) { + var refiners = buildEventSourceRefiners(context); + var rawSources = [].concat(rawOptions.eventSources || []); + var sources = []; // parsed + if (rawOptions.initialEvents) { + rawSources.unshift(rawOptions.initialEvents); + } + if (rawOptions.events) { + rawSources.unshift(rawOptions.events); + } + for (var _i = 0, rawSources_1 = rawSources; _i < rawSources_1.length; _i++) { + var rawSource = rawSources_1[_i]; + var source = parseEventSource(rawSource, context, refiners); + if (source) { + sources.push(source); + } + } + return sources; + } + function doesSourceNeedRange(eventSource, context) { + var defs = context.pluginHooks.eventSourceDefs; + return !defs[eventSource.sourceDefId].ignoreRange; + } + + function reduceDateSelection(currentSelection, action) { + switch (action.type) { + case 'UNSELECT_DATES': + return null; + case 'SELECT_DATES': + return action.selection; + default: + return currentSelection; + } + } + + function reduceSelectedEvent(currentInstanceId, action) { + switch (action.type) { + case 'UNSELECT_EVENT': + return ''; + case 'SELECT_EVENT': + return action.eventInstanceId; + default: + return currentInstanceId; + } + } + + function reduceEventDrag(currentDrag, action) { + var newDrag; + switch (action.type) { + case 'UNSET_EVENT_DRAG': + return null; + case 'SET_EVENT_DRAG': + newDrag = action.state; + return { + affectedEvents: newDrag.affectedEvents, + mutatedEvents: newDrag.mutatedEvents, + isEvent: newDrag.isEvent, + }; + default: + return currentDrag; + } + } + + function reduceEventResize(currentResize, action) { + var newResize; + switch (action.type) { + case 'UNSET_EVENT_RESIZE': + return null; + case 'SET_EVENT_RESIZE': + newResize = action.state; + return { + affectedEvents: newResize.affectedEvents, + mutatedEvents: newResize.mutatedEvents, + isEvent: newResize.isEvent, + }; + default: + return currentResize; + } + } + + function parseToolbars(calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) { + var viewsWithButtons = []; + var headerToolbar = calendarOptions.headerToolbar ? parseToolbar(calendarOptions.headerToolbar, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi, viewsWithButtons) : null; + var footerToolbar = calendarOptions.footerToolbar ? parseToolbar(calendarOptions.footerToolbar, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi, viewsWithButtons) : null; + return { headerToolbar: headerToolbar, footerToolbar: footerToolbar, viewsWithButtons: viewsWithButtons }; + } + function parseToolbar(sectionStrHash, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi, viewsWithButtons) { + return mapHash(sectionStrHash, function (sectionStr) { return parseSection(sectionStr, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi, viewsWithButtons); }); + } + /* + BAD: querying icons and text here. should be done at render time + */ + function parseSection(sectionStr, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi, viewsWithButtons) { + var isRtl = calendarOptions.direction === 'rtl'; + var calendarCustomButtons = calendarOptions.customButtons || {}; + var calendarButtonTextOverrides = calendarOptionOverrides.buttonText || {}; + var calendarButtonText = calendarOptions.buttonText || {}; + var sectionSubstrs = sectionStr ? sectionStr.split(' ') : []; + return sectionSubstrs.map(function (buttonGroupStr) { return (buttonGroupStr.split(',').map(function (buttonName) { + if (buttonName === 'title') { + return { buttonName: buttonName }; + } + var customButtonProps; + var viewSpec; + var buttonClick; + var buttonIcon; // only one of these will be set + var buttonText; // " + if ((customButtonProps = calendarCustomButtons[buttonName])) { + buttonClick = function (ev) { + if (customButtonProps.click) { + customButtonProps.click.call(ev.target, ev, ev.target); + } + }; + (buttonIcon = theme.getCustomButtonIconClass(customButtonProps)) || + (buttonIcon = theme.getIconClass(buttonName, isRtl)) || + (buttonText = customButtonProps.text); + } + else if ((viewSpec = viewSpecs[buttonName])) { + viewsWithButtons.push(buttonName); + buttonClick = function () { + calendarApi.changeView(buttonName); + }; + (buttonText = viewSpec.buttonTextOverride) || + (buttonIcon = theme.getIconClass(buttonName, isRtl)) || + (buttonText = viewSpec.buttonTextDefault); + } + else if (calendarApi[buttonName]) { // a calendarApi method + buttonClick = function () { + calendarApi[buttonName](); + }; + (buttonText = calendarButtonTextOverrides[buttonName]) || + (buttonIcon = theme.getIconClass(buttonName, isRtl)) || + (buttonText = calendarButtonText[buttonName]); + // ^ everything else is considered default + } + return { buttonName: buttonName, buttonClick: buttonClick, buttonIcon: buttonIcon, buttonText: buttonText }; + })); }); + } + + var eventSourceDef = { + ignoreRange: true, + parseMeta: function (refined) { + if (Array.isArray(refined.events)) { + return refined.events; + } + return null; + }, + fetch: function (arg, success) { + success({ + rawEvents: arg.eventSource.meta, + }); + }, + }; + var arrayEventSourcePlugin = createPlugin({ + eventSourceDefs: [eventSourceDef], + }); + + var eventSourceDef$1 = { + parseMeta: function (refined) { + if (typeof refined.events === 'function') { + return refined.events; + } + return null; + }, + fetch: function (arg, success, failure) { + var dateEnv = arg.context.dateEnv; + var func = arg.eventSource.meta; + unpromisify(func.bind(null, buildRangeApiWithTimeZone(arg.range, dateEnv)), function (rawEvents) { + success({ rawEvents: rawEvents }); // needs an object response + }, failure); + }, + }; + var funcEventSourcePlugin = createPlugin({ + eventSourceDefs: [eventSourceDef$1], + }); + + function requestJson(method, url, params, successCallback, failureCallback) { + method = method.toUpperCase(); + var body = null; + if (method === 'GET') { + url = injectQueryStringParams(url, params); + } + else { + body = encodeParams(params); + } + var xhr = new XMLHttpRequest(); + xhr.open(method, url, true); + if (method !== 'GET') { + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } + xhr.onload = function () { + if (xhr.status >= 200 && xhr.status < 400) { + var parsed = false; + var res = void 0; + try { + res = JSON.parse(xhr.responseText); + parsed = true; + } + catch (err) { + // will handle parsed=false + } + if (parsed) { + successCallback(res, xhr); + } + else { + failureCallback('Failure parsing JSON', xhr); + } + } + else { + failureCallback('Request failed', xhr); + } + }; + xhr.onerror = function () { + failureCallback('Request failed', xhr); + }; + xhr.send(body); + } + function injectQueryStringParams(url, params) { + return url + + (url.indexOf('?') === -1 ? '?' : '&') + + encodeParams(params); + } + function encodeParams(params) { + var parts = []; + for (var key in params) { + parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(params[key])); + } + return parts.join('&'); + } + + var JSON_FEED_EVENT_SOURCE_REFINERS = { + method: String, + extraParams: identity, + startParam: String, + endParam: String, + timeZoneParam: String, + }; + + var eventSourceDef$2 = { + parseMeta: function (refined) { + if (refined.url && (refined.format === 'json' || !refined.format)) { + return { + url: refined.url, + format: 'json', + method: (refined.method || 'GET').toUpperCase(), + extraParams: refined.extraParams, + startParam: refined.startParam, + endParam: refined.endParam, + timeZoneParam: refined.timeZoneParam, + }; + } + return null; + }, + fetch: function (arg, success, failure) { + var meta = arg.eventSource.meta; + var requestParams = buildRequestParams(meta, arg.range, arg.context); + requestJson(meta.method, meta.url, requestParams, function (rawEvents, xhr) { + success({ rawEvents: rawEvents, xhr: xhr }); + }, function (errorMessage, xhr) { + failure({ message: errorMessage, xhr: xhr }); + }); + }, + }; + var jsonFeedEventSourcePlugin = createPlugin({ + eventSourceRefiners: JSON_FEED_EVENT_SOURCE_REFINERS, + eventSourceDefs: [eventSourceDef$2], + }); + function buildRequestParams(meta, range, context) { + var dateEnv = context.dateEnv, options = context.options; + var startParam; + var endParam; + var timeZoneParam; + var customRequestParams; + var params = {}; + startParam = meta.startParam; + if (startParam == null) { + startParam = options.startParam; + } + endParam = meta.endParam; + if (endParam == null) { + endParam = options.endParam; + } + timeZoneParam = meta.timeZoneParam; + if (timeZoneParam == null) { + timeZoneParam = options.timeZoneParam; + } + // retrieve any outbound GET/POST data from the options + if (typeof meta.extraParams === 'function') { + // supplied as a function that returns a key/value object + customRequestParams = meta.extraParams(); + } + else { + // probably supplied as a straight key/value object + customRequestParams = meta.extraParams || {}; + } + __assign(params, customRequestParams); + params[startParam] = dateEnv.formatIso(range.start); + params[endParam] = dateEnv.formatIso(range.end); + if (dateEnv.timeZone !== 'local') { + params[timeZoneParam] = dateEnv.timeZone; + } + return params; + } + + var SIMPLE_RECURRING_REFINERS = { + daysOfWeek: identity, + startTime: createDuration, + endTime: createDuration, + duration: createDuration, + startRecur: identity, + endRecur: identity, + }; + + var recurring = { + parse: function (refined, dateEnv) { + if (refined.daysOfWeek || refined.startTime || refined.endTime || refined.startRecur || refined.endRecur) { + var recurringData = { + daysOfWeek: refined.daysOfWeek || null, + startTime: refined.startTime || null, + endTime: refined.endTime || null, + startRecur: refined.startRecur ? dateEnv.createMarker(refined.startRecur) : null, + endRecur: refined.endRecur ? dateEnv.createMarker(refined.endRecur) : null, + }; + var duration = void 0; + if (refined.duration) { + duration = refined.duration; + } + if (!duration && refined.startTime && refined.endTime) { + duration = subtractDurations(refined.endTime, refined.startTime); + } + return { + allDayGuess: Boolean(!refined.startTime && !refined.endTime), + duration: duration, + typeData: recurringData, + }; + } + return null; + }, + expand: function (typeData, framingRange, dateEnv) { + var clippedFramingRange = intersectRanges(framingRange, { start: typeData.startRecur, end: typeData.endRecur }); + if (clippedFramingRange) { + return expandRanges(typeData.daysOfWeek, typeData.startTime, clippedFramingRange, dateEnv); + } + return []; + }, + }; + var simpleRecurringEventsPlugin = createPlugin({ + recurringTypes: [recurring], + eventRefiners: SIMPLE_RECURRING_REFINERS, + }); + function expandRanges(daysOfWeek, startTime, framingRange, dateEnv) { + var dowHash = daysOfWeek ? arrayToHash(daysOfWeek) : null; + var dayMarker = startOfDay(framingRange.start); + var endMarker = framingRange.end; + var instanceStarts = []; + while (dayMarker < endMarker) { + var instanceStart + // if everyday, or this particular day-of-week + = void 0; + // if everyday, or this particular day-of-week + if (!dowHash || dowHash[dayMarker.getUTCDay()]) { + if (startTime) { + instanceStart = dateEnv.add(dayMarker, startTime); + } + else { + instanceStart = dayMarker; + } + instanceStarts.push(instanceStart); + } + dayMarker = addDays(dayMarker, 1); + } + return instanceStarts; + } + + var changeHandlerPlugin = createPlugin({ + optionChangeHandlers: { + events: function (events, context) { + handleEventSources([events], context); + }, + eventSources: handleEventSources, + }, + }); + /* + BUG: if `event` was supplied, all previously-given `eventSources` will be wiped out + */ + function handleEventSources(inputs, context) { + var unfoundSources = hashValuesToArray(context.getCurrentData().eventSources); + var newInputs = []; + for (var _i = 0, inputs_1 = inputs; _i < inputs_1.length; _i++) { + var input = inputs_1[_i]; + var inputFound = false; + for (var i = 0; i < unfoundSources.length; i += 1) { + if (unfoundSources[i]._raw === input) { + unfoundSources.splice(i, 1); // delete + inputFound = true; + break; + } + } + if (!inputFound) { + newInputs.push(input); + } + } + for (var _a = 0, unfoundSources_1 = unfoundSources; _a < unfoundSources_1.length; _a++) { + var unfoundSource = unfoundSources_1[_a]; + context.dispatch({ + type: 'REMOVE_EVENT_SOURCE', + sourceId: unfoundSource.sourceId, + }); + } + for (var _b = 0, newInputs_1 = newInputs; _b < newInputs_1.length; _b++) { + var newInput = newInputs_1[_b]; + context.calendarApi.addEventSource(newInput); + } + } + + function handleDateProfile(dateProfile, context) { + context.emitter.trigger('datesSet', __assign(__assign({}, buildRangeApiWithTimeZone(dateProfile.activeRange, context.dateEnv)), { view: context.viewApi })); + } + + function handleEventStore(eventStore, context) { + var emitter = context.emitter; + if (emitter.hasHandlers('eventsSet')) { + emitter.trigger('eventsSet', buildEventApis(eventStore, context)); + } + } + + /* + this array is exposed on the root namespace so that UMD plugins can add to it. + see the rollup-bundles script. + */ + var globalPlugins = [ + arrayEventSourcePlugin, + funcEventSourcePlugin, + jsonFeedEventSourcePlugin, + simpleRecurringEventsPlugin, + changeHandlerPlugin, + createPlugin({ + isLoadingFuncs: [ + function (state) { return computeEventSourcesLoading(state.eventSources); }, + ], + contentTypeHandlers: { + html: function () { return ({ render: injectHtml }); }, + domNodes: function () { return ({ render: injectDomNodes }); }, + }, + propSetHandlers: { + dateProfile: handleDateProfile, + eventStore: handleEventStore, + }, + }), + ]; + function injectHtml(el, html) { + el.innerHTML = html; + } + function injectDomNodes(el, domNodes) { + var oldNodes = Array.prototype.slice.call(el.childNodes); // TODO: use array util + var newNodes = Array.prototype.slice.call(domNodes); // TODO: use array util + if (!isArraysEqual(oldNodes, newNodes)) { + for (var _i = 0, newNodes_1 = newNodes; _i < newNodes_1.length; _i++) { + var newNode = newNodes_1[_i]; + el.appendChild(newNode); + } + oldNodes.forEach(removeElement); + } + } + + var DelayedRunner = /** @class */ (function () { + function DelayedRunner(drainedOption) { + this.drainedOption = drainedOption; + this.isRunning = false; + this.isDirty = false; + this.pauseDepths = {}; + this.timeoutId = 0; + } + DelayedRunner.prototype.request = function (delay) { + this.isDirty = true; + if (!this.isPaused()) { + this.clearTimeout(); + if (delay == null) { + this.tryDrain(); + } + else { + this.timeoutId = setTimeout(// NOT OPTIMAL! TODO: look at debounce + this.tryDrain.bind(this), delay); + } + } + }; + DelayedRunner.prototype.pause = function (scope) { + if (scope === void 0) { scope = ''; } + var pauseDepths = this.pauseDepths; + pauseDepths[scope] = (pauseDepths[scope] || 0) + 1; + this.clearTimeout(); + }; + DelayedRunner.prototype.resume = function (scope, force) { + if (scope === void 0) { scope = ''; } + var pauseDepths = this.pauseDepths; + if (scope in pauseDepths) { + if (force) { + delete pauseDepths[scope]; + } + else { + pauseDepths[scope] -= 1; + var depth = pauseDepths[scope]; + if (depth <= 0) { + delete pauseDepths[scope]; + } + } + this.tryDrain(); + } + }; + DelayedRunner.prototype.isPaused = function () { + return Object.keys(this.pauseDepths).length; + }; + DelayedRunner.prototype.tryDrain = function () { + if (!this.isRunning && !this.isPaused()) { + this.isRunning = true; + while (this.isDirty) { + this.isDirty = false; + this.drained(); // might set isDirty to true again + } + this.isRunning = false; + } + }; + DelayedRunner.prototype.clear = function () { + this.clearTimeout(); + this.isDirty = false; + this.pauseDepths = {}; + }; + DelayedRunner.prototype.clearTimeout = function () { + if (this.timeoutId) { + clearTimeout(this.timeoutId); + this.timeoutId = 0; + } + }; + DelayedRunner.prototype.drained = function () { + if (this.drainedOption) { + this.drainedOption(); + } + }; + return DelayedRunner; + }()); + + var TaskRunner = /** @class */ (function () { + function TaskRunner(runTaskOption, drainedOption) { + this.runTaskOption = runTaskOption; + this.drainedOption = drainedOption; + this.queue = []; + this.delayedRunner = new DelayedRunner(this.drain.bind(this)); + } + TaskRunner.prototype.request = function (task, delay) { + this.queue.push(task); + this.delayedRunner.request(delay); + }; + TaskRunner.prototype.pause = function (scope) { + this.delayedRunner.pause(scope); + }; + TaskRunner.prototype.resume = function (scope, force) { + this.delayedRunner.resume(scope, force); + }; + TaskRunner.prototype.drain = function () { + var queue = this.queue; + while (queue.length) { + var completedTasks = []; + var task = void 0; + while ((task = queue.shift())) { + this.runTask(task); + completedTasks.push(task); + } + this.drained(completedTasks); + } // keep going, in case new tasks were added in the drained handler + }; + TaskRunner.prototype.runTask = function (task) { + if (this.runTaskOption) { + this.runTaskOption(task); + } + }; + TaskRunner.prototype.drained = function (completedTasks) { + if (this.drainedOption) { + this.drainedOption(completedTasks); + } + }; + return TaskRunner; + }()); + + // Computes what the title at the top of the calendarApi should be for this view + function buildTitle(dateProfile, viewOptions, dateEnv) { + var range; + // for views that span a large unit of time, show the proper interval, ignoring stray days before and after + if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) { + range = dateProfile.currentRange; + } + else { // for day units or smaller, use the actual day range + range = dateProfile.activeRange; + } + return dateEnv.formatRange(range.start, range.end, createFormatter(viewOptions.titleFormat || buildTitleFormat(dateProfile)), { + isEndExclusive: dateProfile.isRangeAllDay, + defaultSeparator: viewOptions.titleRangeSeparator, + }); + } + // Generates the format string that should be used to generate the title for the current date range. + // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`. + function buildTitleFormat(dateProfile) { + var currentRangeUnit = dateProfile.currentRangeUnit; + if (currentRangeUnit === 'year') { + return { year: 'numeric' }; + } + if (currentRangeUnit === 'month') { + return { year: 'numeric', month: 'long' }; // like "September 2014" + } + var days = diffWholeDays(dateProfile.currentRange.start, dateProfile.currentRange.end); + if (days !== null && days > 1) { + // multi-day range. shorter, like "Sep 9 - 10 2014" + return { year: 'numeric', month: 'short', day: 'numeric' }; + } + // one day. longer, like "September 9 2014" + return { year: 'numeric', month: 'long', day: 'numeric' }; + } + + // in future refactor, do the redux-style function(state=initial) for initial-state + // also, whatever is happening in constructor, have it happen in action queue too + var CalendarDataManager = /** @class */ (function () { + function CalendarDataManager(props) { + var _this = this; + this.computeOptionsData = memoize(this._computeOptionsData); + this.computeCurrentViewData = memoize(this._computeCurrentViewData); + this.organizeRawLocales = memoize(organizeRawLocales); + this.buildLocale = memoize(buildLocale); + this.buildPluginHooks = buildBuildPluginHooks(); + this.buildDateEnv = memoize(buildDateEnv$1); + this.buildTheme = memoize(buildTheme); + this.parseToolbars = memoize(parseToolbars); + this.buildViewSpecs = memoize(buildViewSpecs); + this.buildDateProfileGenerator = memoizeObjArg(buildDateProfileGenerator); + this.buildViewApi = memoize(buildViewApi); + this.buildViewUiProps = memoizeObjArg(buildViewUiProps); + this.buildEventUiBySource = memoize(buildEventUiBySource, isPropsEqual); + this.buildEventUiBases = memoize(buildEventUiBases); + this.parseContextBusinessHours = memoizeObjArg(parseContextBusinessHours); + this.buildTitle = memoize(buildTitle); + this.emitter = new Emitter(); + this.actionRunner = new TaskRunner(this._handleAction.bind(this), this.updateData.bind(this)); + this.currentCalendarOptionsInput = {}; + this.currentCalendarOptionsRefined = {}; + this.currentViewOptionsInput = {}; + this.currentViewOptionsRefined = {}; + this.currentCalendarOptionsRefiners = {}; + this.getCurrentData = function () { return _this.data; }; + this.dispatch = function (action) { + _this.actionRunner.request(action); // protects against recursive calls to _handleAction + }; + this.props = props; + this.actionRunner.pause(); + var dynamicOptionOverrides = {}; + var optionsData = this.computeOptionsData(props.optionOverrides, dynamicOptionOverrides, props.calendarApi); + var currentViewType = optionsData.calendarOptions.initialView || optionsData.pluginHooks.initialView; + var currentViewData = this.computeCurrentViewData(currentViewType, optionsData, props.optionOverrides, dynamicOptionOverrides); + // wire things up + // TODO: not DRY + props.calendarApi.currentDataManager = this; + this.emitter.setThisContext(props.calendarApi); + this.emitter.setOptions(currentViewData.options); + var currentDate = getInitialDate(optionsData.calendarOptions, optionsData.dateEnv); + var dateProfile = currentViewData.dateProfileGenerator.build(currentDate); + if (!rangeContainsMarker(dateProfile.activeRange, currentDate)) { + currentDate = dateProfile.currentRange.start; + } + var calendarContext = { + dateEnv: optionsData.dateEnv, + options: optionsData.calendarOptions, + pluginHooks: optionsData.pluginHooks, + calendarApi: props.calendarApi, + dispatch: this.dispatch, + emitter: this.emitter, + getCurrentData: this.getCurrentData, + }; + // needs to be after setThisContext + for (var _i = 0, _a = optionsData.pluginHooks.contextInit; _i < _a.length; _i++) { + var callback = _a[_i]; + callback(calendarContext); + } + // NOT DRY + var eventSources = initEventSources(optionsData.calendarOptions, dateProfile, calendarContext); + var initialState = { + dynamicOptionOverrides: dynamicOptionOverrides, + currentViewType: currentViewType, + currentDate: currentDate, + dateProfile: dateProfile, + businessHours: this.parseContextBusinessHours(calendarContext), + eventSources: eventSources, + eventUiBases: {}, + eventStore: createEmptyEventStore(), + renderableEventStore: createEmptyEventStore(), + dateSelection: null, + eventSelection: '', + eventDrag: null, + eventResize: null, + selectionConfig: this.buildViewUiProps(calendarContext).selectionConfig, + }; + var contextAndState = __assign(__assign({}, calendarContext), initialState); + for (var _b = 0, _c = optionsData.pluginHooks.reducers; _b < _c.length; _b++) { + var reducer = _c[_b]; + __assign(initialState, reducer(null, null, contextAndState)); + } + if (computeIsLoading(initialState, calendarContext)) { + this.emitter.trigger('loading', true); // NOT DRY + } + this.state = initialState; + this.updateData(); + this.actionRunner.resume(); + } + CalendarDataManager.prototype.resetOptions = function (optionOverrides, append) { + var props = this.props; + props.optionOverrides = append + ? __assign(__assign({}, props.optionOverrides), optionOverrides) : optionOverrides; + this.actionRunner.request({ + type: 'NOTHING', + }); + }; + CalendarDataManager.prototype._handleAction = function (action) { + var _a = this, props = _a.props, state = _a.state, emitter = _a.emitter; + var dynamicOptionOverrides = reduceDynamicOptionOverrides(state.dynamicOptionOverrides, action); + var optionsData = this.computeOptionsData(props.optionOverrides, dynamicOptionOverrides, props.calendarApi); + var currentViewType = reduceViewType(state.currentViewType, action); + var currentViewData = this.computeCurrentViewData(currentViewType, optionsData, props.optionOverrides, dynamicOptionOverrides); + // wire things up + // TODO: not DRY + props.calendarApi.currentDataManager = this; + emitter.setThisContext(props.calendarApi); + emitter.setOptions(currentViewData.options); + var calendarContext = { + dateEnv: optionsData.dateEnv, + options: optionsData.calendarOptions, + pluginHooks: optionsData.pluginHooks, + calendarApi: props.calendarApi, + dispatch: this.dispatch, + emitter: emitter, + getCurrentData: this.getCurrentData, + }; + var currentDate = state.currentDate, dateProfile = state.dateProfile; + if (this.data && this.data.dateProfileGenerator !== currentViewData.dateProfileGenerator) { // hack + dateProfile = currentViewData.dateProfileGenerator.build(currentDate); + } + currentDate = reduceCurrentDate(currentDate, action); + dateProfile = reduceDateProfile(dateProfile, action, currentDate, currentViewData.dateProfileGenerator); + if (!rangeContainsMarker(dateProfile.currentRange, currentDate)) { + currentDate = dateProfile.currentRange.start; + } + var eventSources = reduceEventSources(state.eventSources, action, dateProfile, calendarContext); + var eventStore = reduceEventStore(state.eventStore, action, eventSources, dateProfile, calendarContext); + var isEventsLoading = computeEventSourcesLoading(eventSources); // BAD. also called in this func in computeIsLoading + var renderableEventStore = (isEventsLoading && !currentViewData.options.progressiveEventRendering) ? + (state.renderableEventStore || eventStore) : // try from previous state + eventStore; + var _b = this.buildViewUiProps(calendarContext), eventUiSingleBase = _b.eventUiSingleBase, selectionConfig = _b.selectionConfig; // will memoize obj + var eventUiBySource = this.buildEventUiBySource(eventSources); + var eventUiBases = this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource); + var newState = { + dynamicOptionOverrides: dynamicOptionOverrides, + currentViewType: currentViewType, + currentDate: currentDate, + dateProfile: dateProfile, + eventSources: eventSources, + eventStore: eventStore, + renderableEventStore: renderableEventStore, + selectionConfig: selectionConfig, + eventUiBases: eventUiBases, + businessHours: this.parseContextBusinessHours(calendarContext), + dateSelection: reduceDateSelection(state.dateSelection, action), + eventSelection: reduceSelectedEvent(state.eventSelection, action), + eventDrag: reduceEventDrag(state.eventDrag, action), + eventResize: reduceEventResize(state.eventResize, action), + }; + var contextAndState = __assign(__assign({}, calendarContext), newState); + for (var _i = 0, _c = optionsData.pluginHooks.reducers; _i < _c.length; _i++) { + var reducer = _c[_i]; + __assign(newState, reducer(state, action, contextAndState)); // give the OLD state, for old value + } + var wasLoading = computeIsLoading(state, calendarContext); + var isLoading = computeIsLoading(newState, calendarContext); + // TODO: use propSetHandlers in plugin system + if (!wasLoading && isLoading) { + emitter.trigger('loading', true); + } + else if (wasLoading && !isLoading) { + emitter.trigger('loading', false); + } + this.state = newState; + if (props.onAction) { + props.onAction(action); + } + }; + CalendarDataManager.prototype.updateData = function () { + var _a = this, props = _a.props, state = _a.state; + var oldData = this.data; + var optionsData = this.computeOptionsData(props.optionOverrides, state.dynamicOptionOverrides, props.calendarApi); + var currentViewData = this.computeCurrentViewData(state.currentViewType, optionsData, props.optionOverrides, state.dynamicOptionOverrides); + var data = this.data = __assign(__assign(__assign({ viewTitle: this.buildTitle(state.dateProfile, currentViewData.options, optionsData.dateEnv), calendarApi: props.calendarApi, dispatch: this.dispatch, emitter: this.emitter, getCurrentData: this.getCurrentData }, optionsData), currentViewData), state); + var changeHandlers = optionsData.pluginHooks.optionChangeHandlers; + var oldCalendarOptions = oldData && oldData.calendarOptions; + var newCalendarOptions = optionsData.calendarOptions; + if (oldCalendarOptions && oldCalendarOptions !== newCalendarOptions) { + if (oldCalendarOptions.timeZone !== newCalendarOptions.timeZone) { + // hack + state.eventSources = data.eventSources = reduceEventSourcesNewTimeZone(data.eventSources, state.dateProfile, data); + state.eventStore = data.eventStore = rezoneEventStoreDates(data.eventStore, oldData.dateEnv, data.dateEnv); + } + for (var optionName in changeHandlers) { + if (oldCalendarOptions[optionName] !== newCalendarOptions[optionName]) { + changeHandlers[optionName](newCalendarOptions[optionName], data); + } + } + } + if (props.onData) { + props.onData(data); + } + }; + CalendarDataManager.prototype._computeOptionsData = function (optionOverrides, dynamicOptionOverrides, calendarApi) { + // TODO: blacklist options that are handled by optionChangeHandlers + var _a = this.processRawCalendarOptions(optionOverrides, dynamicOptionOverrides), refinedOptions = _a.refinedOptions, pluginHooks = _a.pluginHooks, localeDefaults = _a.localeDefaults, availableLocaleData = _a.availableLocaleData, extra = _a.extra; + warnUnknownOptions(extra); + var dateEnv = this.buildDateEnv(refinedOptions.timeZone, refinedOptions.locale, refinedOptions.weekNumberCalculation, refinedOptions.firstDay, refinedOptions.weekText, pluginHooks, availableLocaleData, refinedOptions.defaultRangeSeparator); + var viewSpecs = this.buildViewSpecs(pluginHooks.views, optionOverrides, dynamicOptionOverrides, localeDefaults); + var theme = this.buildTheme(refinedOptions, pluginHooks); + var toolbarConfig = this.parseToolbars(refinedOptions, optionOverrides, theme, viewSpecs, calendarApi); + return { + calendarOptions: refinedOptions, + pluginHooks: pluginHooks, + dateEnv: dateEnv, + viewSpecs: viewSpecs, + theme: theme, + toolbarConfig: toolbarConfig, + localeDefaults: localeDefaults, + availableRawLocales: availableLocaleData.map, + }; + }; + // always called from behind a memoizer + CalendarDataManager.prototype.processRawCalendarOptions = function (optionOverrides, dynamicOptionOverrides) { + var _a = mergeRawOptions([ + BASE_OPTION_DEFAULTS, + optionOverrides, + dynamicOptionOverrides, + ]), locales = _a.locales, locale = _a.locale; + var availableLocaleData = this.organizeRawLocales(locales); + var availableRawLocales = availableLocaleData.map; + var localeDefaults = this.buildLocale(locale || availableLocaleData.defaultCode, availableRawLocales).options; + var pluginHooks = this.buildPluginHooks(optionOverrides.plugins || [], globalPlugins); + var refiners = this.currentCalendarOptionsRefiners = __assign(__assign(__assign(__assign(__assign({}, BASE_OPTION_REFINERS), CALENDAR_LISTENER_REFINERS), CALENDAR_OPTION_REFINERS), pluginHooks.listenerRefiners), pluginHooks.optionRefiners); + var extra = {}; + var raw = mergeRawOptions([ + BASE_OPTION_DEFAULTS, + localeDefaults, + optionOverrides, + dynamicOptionOverrides, + ]); + var refined = {}; + var currentRaw = this.currentCalendarOptionsInput; + var currentRefined = this.currentCalendarOptionsRefined; + var anyChanges = false; + for (var optionName in raw) { + if (optionName !== 'plugins') { // because plugins is special-cased + if (raw[optionName] === currentRaw[optionName] || + (COMPLEX_OPTION_COMPARATORS[optionName] && + (optionName in currentRaw) && + COMPLEX_OPTION_COMPARATORS[optionName](currentRaw[optionName], raw[optionName]))) { + refined[optionName] = currentRefined[optionName]; + } + else if (refiners[optionName]) { + refined[optionName] = refiners[optionName](raw[optionName]); + anyChanges = true; + } + else { + extra[optionName] = currentRaw[optionName]; + } + } + } + if (anyChanges) { + this.currentCalendarOptionsInput = raw; + this.currentCalendarOptionsRefined = refined; + } + return { + rawOptions: this.currentCalendarOptionsInput, + refinedOptions: this.currentCalendarOptionsRefined, + pluginHooks: pluginHooks, + availableLocaleData: availableLocaleData, + localeDefaults: localeDefaults, + extra: extra, + }; + }; + CalendarDataManager.prototype._computeCurrentViewData = function (viewType, optionsData, optionOverrides, dynamicOptionOverrides) { + var viewSpec = optionsData.viewSpecs[viewType]; + if (!viewSpec) { + throw new Error("viewType \"" + viewType + "\" is not available. Please make sure you've loaded all neccessary plugins"); + } + var _a = this.processRawViewOptions(viewSpec, optionsData.pluginHooks, optionsData.localeDefaults, optionOverrides, dynamicOptionOverrides), refinedOptions = _a.refinedOptions, extra = _a.extra; + warnUnknownOptions(extra); + var dateProfileGenerator = this.buildDateProfileGenerator({ + dateProfileGeneratorClass: viewSpec.optionDefaults.dateProfileGeneratorClass, + duration: viewSpec.duration, + durationUnit: viewSpec.durationUnit, + usesMinMaxTime: viewSpec.optionDefaults.usesMinMaxTime, + dateEnv: optionsData.dateEnv, + calendarApi: this.props.calendarApi, + slotMinTime: refinedOptions.slotMinTime, + slotMaxTime: refinedOptions.slotMaxTime, + showNonCurrentDates: refinedOptions.showNonCurrentDates, + dayCount: refinedOptions.dayCount, + dateAlignment: refinedOptions.dateAlignment, + dateIncrement: refinedOptions.dateIncrement, + hiddenDays: refinedOptions.hiddenDays, + weekends: refinedOptions.weekends, + nowInput: refinedOptions.now, + validRangeInput: refinedOptions.validRange, + visibleRangeInput: refinedOptions.visibleRange, + monthMode: refinedOptions.monthMode, + fixedWeekCount: refinedOptions.fixedWeekCount, + }); + var viewApi = this.buildViewApi(viewType, this.getCurrentData, optionsData.dateEnv); + return { viewSpec: viewSpec, options: refinedOptions, dateProfileGenerator: dateProfileGenerator, viewApi: viewApi }; + }; + CalendarDataManager.prototype.processRawViewOptions = function (viewSpec, pluginHooks, localeDefaults, optionOverrides, dynamicOptionOverrides) { + var raw = mergeRawOptions([ + BASE_OPTION_DEFAULTS, + viewSpec.optionDefaults, + localeDefaults, + optionOverrides, + viewSpec.optionOverrides, + dynamicOptionOverrides, + ]); + var refiners = __assign(__assign(__assign(__assign(__assign(__assign({}, BASE_OPTION_REFINERS), CALENDAR_LISTENER_REFINERS), CALENDAR_OPTION_REFINERS), VIEW_OPTION_REFINERS), pluginHooks.listenerRefiners), pluginHooks.optionRefiners); + var refined = {}; + var currentRaw = this.currentViewOptionsInput; + var currentRefined = this.currentViewOptionsRefined; + var anyChanges = false; + var extra = {}; + for (var optionName in raw) { + if (raw[optionName] === currentRaw[optionName]) { + refined[optionName] = currentRefined[optionName]; + } + else { + if (raw[optionName] === this.currentCalendarOptionsInput[optionName]) { + if (optionName in this.currentCalendarOptionsRefined) { // might be an "extra" prop + refined[optionName] = this.currentCalendarOptionsRefined[optionName]; + } + } + else if (refiners[optionName]) { + refined[optionName] = refiners[optionName](raw[optionName]); + } + else { + extra[optionName] = raw[optionName]; + } + anyChanges = true; + } + } + if (anyChanges) { + this.currentViewOptionsInput = raw; + this.currentViewOptionsRefined = refined; + } + return { + rawOptions: this.currentViewOptionsInput, + refinedOptions: this.currentViewOptionsRefined, + extra: extra, + }; + }; + return CalendarDataManager; + }()); + function buildDateEnv$1(timeZone, explicitLocale, weekNumberCalculation, firstDay, weekText, pluginHooks, availableLocaleData, defaultSeparator) { + var locale = buildLocale(explicitLocale || availableLocaleData.defaultCode, availableLocaleData.map); + return new DateEnv({ + calendarSystem: 'gregory', + timeZone: timeZone, + namedTimeZoneImpl: pluginHooks.namedTimeZonedImpl, + locale: locale, + weekNumberCalculation: weekNumberCalculation, + firstDay: firstDay, + weekText: weekText, + cmdFormatter: pluginHooks.cmdFormatter, + defaultSeparator: defaultSeparator, + }); + } + function buildTheme(options, pluginHooks) { + var ThemeClass = pluginHooks.themeClasses[options.themeSystem] || StandardTheme; + return new ThemeClass(options); + } + function buildDateProfileGenerator(props) { + var DateProfileGeneratorClass = props.dateProfileGeneratorClass || DateProfileGenerator; + return new DateProfileGeneratorClass(props); + } + function buildViewApi(type, getCurrentData, dateEnv) { + return new ViewApi(type, getCurrentData, dateEnv); + } + function buildEventUiBySource(eventSources) { + return mapHash(eventSources, function (eventSource) { return eventSource.ui; }); + } + function buildEventUiBases(eventDefs, eventUiSingleBase, eventUiBySource) { + var eventUiBases = { '': eventUiSingleBase }; + for (var defId in eventDefs) { + var def = eventDefs[defId]; + if (def.sourceId && eventUiBySource[def.sourceId]) { + eventUiBases[defId] = eventUiBySource[def.sourceId]; + } + } + return eventUiBases; + } + function buildViewUiProps(calendarContext) { + var options = calendarContext.options; + return { + eventUiSingleBase: createEventUi({ + display: options.eventDisplay, + editable: options.editable, + startEditable: options.eventStartEditable, + durationEditable: options.eventDurationEditable, + constraint: options.eventConstraint, + overlap: typeof options.eventOverlap === 'boolean' ? options.eventOverlap : undefined, + allow: options.eventAllow, + backgroundColor: options.eventBackgroundColor, + borderColor: options.eventBorderColor, + textColor: options.eventTextColor, + color: options.eventColor, + }, calendarContext), + selectionConfig: createEventUi({ + constraint: options.selectConstraint, + overlap: typeof options.selectOverlap === 'boolean' ? options.selectOverlap : undefined, + allow: options.selectAllow, + }, calendarContext), + }; + } + function computeIsLoading(state, context) { + for (var _i = 0, _a = context.pluginHooks.isLoadingFuncs; _i < _a.length; _i++) { + var isLoadingFunc = _a[_i]; + if (isLoadingFunc(state)) { + return true; + } + } + return false; + } + function parseContextBusinessHours(calendarContext) { + return parseBusinessHours(calendarContext.options.businessHours, calendarContext); + } + function warnUnknownOptions(options, viewName) { + for (var optionName in options) { + console.warn("Unknown option '" + optionName + "'" + + (viewName ? " for view '" + viewName + "'" : '')); + } + } + + // TODO: move this to react plugin? + var CalendarDataProvider = /** @class */ (function (_super) { + __extends(CalendarDataProvider, _super); + function CalendarDataProvider(props) { + var _this = _super.call(this, props) || this; + _this.handleData = function (data) { + if (!_this.dataManager) { // still within initial run, before assignment in constructor + // eslint-disable-next-line react/no-direct-mutation-state + _this.state = data; // can't use setState yet + } + else { + _this.setState(data); + } + }; + _this.dataManager = new CalendarDataManager({ + optionOverrides: props.optionOverrides, + calendarApi: props.calendarApi, + onData: _this.handleData, + }); + return _this; + } + CalendarDataProvider.prototype.render = function () { + return this.props.children(this.state); + }; + CalendarDataProvider.prototype.componentDidUpdate = function (prevProps) { + var newOptionOverrides = this.props.optionOverrides; + if (newOptionOverrides !== prevProps.optionOverrides) { // prevent recursive handleData + this.dataManager.resetOptions(newOptionOverrides); + } + }; + return CalendarDataProvider; + }(Component)); + + // HELPERS + /* + if nextDayThreshold is specified, slicing is done in an all-day fashion. + you can get nextDayThreshold from context.nextDayThreshold + */ + function sliceEvents(props, allDay) { + return sliceEventStore(props.eventStore, props.eventUiBases, props.dateProfile.activeRange, allDay ? props.nextDayThreshold : null).fg; + } + + var NamedTimeZoneImpl = /** @class */ (function () { + function NamedTimeZoneImpl(timeZoneName) { + this.timeZoneName = timeZoneName; + } + return NamedTimeZoneImpl; + }()); + + var Interaction = /** @class */ (function () { + function Interaction(settings) { + this.component = settings.component; + } + Interaction.prototype.destroy = function () { + }; + return Interaction; + }()); + function parseInteractionSettings(component, input) { + return { + component: component, + el: input.el, + useEventCenter: input.useEventCenter != null ? input.useEventCenter : true, + }; + } + function interactionSettingsToStore(settings) { + var _a; + return _a = {}, + _a[settings.component.uid] = settings, + _a; + } + // global state + var interactionSettingsStore = {}; + + /* + An abstraction for a dragging interaction originating on an event. + Does higher-level things than PointerDragger, such as possibly: + - a "mirror" that moves with the pointer + - a minimum number of pixels or other criteria for a true drag to begin + + subclasses must emit: + - pointerdown + - dragstart + - dragmove + - pointerup + - dragend + */ + var ElementDragging = /** @class */ (function () { + function ElementDragging(el, selector) { + this.emitter = new Emitter(); + } + ElementDragging.prototype.destroy = function () { + }; + ElementDragging.prototype.setMirrorIsVisible = function (bool) { + // optional if subclass doesn't want to support a mirror + }; + ElementDragging.prototype.setMirrorNeedsRevert = function (bool) { + // optional if subclass doesn't want to support a mirror + }; + ElementDragging.prototype.setAutoScrollEnabled = function (bool) { + // optional + }; + return ElementDragging; + }()); + + // TODO: get rid of this in favor of options system, + // tho it's really easy to access this globally rather than pass thru options. + var config = {}; + + /* + Information about what will happen when an external element is dragged-and-dropped + onto a calendar. Contains information for creating an event. + */ + var DRAG_META_REFINERS = { + startTime: createDuration, + duration: createDuration, + create: Boolean, + sourceId: String, + }; + function parseDragMeta(raw) { + var _a = refineProps(raw, DRAG_META_REFINERS), refined = _a.refined, extra = _a.extra; + return { + startTime: refined.startTime || null, + duration: refined.duration || null, + create: refined.create != null ? refined.create : true, + sourceId: refined.sourceId, + leftoverProps: extra, + }; + } + + var ToolbarSection = /** @class */ (function (_super) { + __extends(ToolbarSection, _super); + function ToolbarSection() { + return _super !== null && _super.apply(this, arguments) || this; + } + ToolbarSection.prototype.render = function () { + var _this = this; + var children = this.props.widgetGroups.map(function (widgetGroup) { return _this.renderWidgetGroup(widgetGroup); }); + return createElement.apply(void 0, __spreadArrays(['div', { className: 'fc-toolbar-chunk' }], children)); + }; + ToolbarSection.prototype.renderWidgetGroup = function (widgetGroup) { + var props = this.props; + var theme = this.context.theme; + var children = []; + var isOnlyButtons = true; + for (var _i = 0, widgetGroup_1 = widgetGroup; _i < widgetGroup_1.length; _i++) { + var widget = widgetGroup_1[_i]; + var buttonName = widget.buttonName, buttonClick = widget.buttonClick, buttonText = widget.buttonText, buttonIcon = widget.buttonIcon; + if (buttonName === 'title') { + isOnlyButtons = false; + children.push(createElement("h2", { className: "fc-toolbar-title" }, props.title)); + } + else { + var ariaAttrs = buttonIcon ? { 'aria-label': buttonName } : {}; + var buttonClasses = ["fc-" + buttonName + "-button", theme.getClass('button')]; + if (buttonName === props.activeButton) { + buttonClasses.push(theme.getClass('buttonActive')); + } + var isDisabled = (!props.isTodayEnabled && buttonName === 'today') || + (!props.isPrevEnabled && buttonName === 'prev') || + (!props.isNextEnabled && buttonName === 'next'); + children.push(createElement("button", __assign({ disabled: isDisabled, className: buttonClasses.join(' '), onClick: buttonClick, type: "button" }, ariaAttrs), buttonText || (buttonIcon ? createElement("span", { className: buttonIcon }) : ''))); + } + } + if (children.length > 1) { + var groupClassName = (isOnlyButtons && theme.getClass('buttonGroup')) || ''; + return createElement.apply(void 0, __spreadArrays(['div', { className: groupClassName }], children)); + } + return children[0]; + }; + return ToolbarSection; + }(BaseComponent)); + + var Toolbar = /** @class */ (function (_super) { + __extends(Toolbar, _super); + function Toolbar() { + return _super !== null && _super.apply(this, arguments) || this; + } + Toolbar.prototype.render = function () { + var _a = this.props, model = _a.model, extraClassName = _a.extraClassName; + var forceLtr = false; + var startContent; + var endContent; + var centerContent = model.center; + if (model.left) { + forceLtr = true; + startContent = model.left; + } + else { + startContent = model.start; + } + if (model.right) { + forceLtr = true; + endContent = model.right; + } + else { + endContent = model.end; + } + var classNames = [ + extraClassName || '', + 'fc-toolbar', + forceLtr ? 'fc-toolbar-ltr' : '', + ]; + return (createElement("div", { className: classNames.join(' ') }, + this.renderSection('start', startContent || []), + this.renderSection('center', centerContent || []), + this.renderSection('end', endContent || []))); + }; + Toolbar.prototype.renderSection = function (key, widgetGroups) { + var props = this.props; + return (createElement(ToolbarSection, { key: key, widgetGroups: widgetGroups, title: props.title, activeButton: props.activeButton, isTodayEnabled: props.isTodayEnabled, isPrevEnabled: props.isPrevEnabled, isNextEnabled: props.isNextEnabled })); + }; + return Toolbar; + }(BaseComponent)); + + // TODO: do function component? + var ViewContainer = /** @class */ (function (_super) { + __extends(ViewContainer, _super); + function ViewContainer() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.state = { + availableWidth: null, + }; + _this.handleEl = function (el) { + _this.el = el; + setRef(_this.props.elRef, el); + _this.updateAvailableWidth(); + }; + _this.handleResize = function () { + _this.updateAvailableWidth(); + }; + return _this; + } + ViewContainer.prototype.render = function () { + var _a = this, props = _a.props, state = _a.state; + var aspectRatio = props.aspectRatio; + var classNames = [ + 'fc-view-harness', + (aspectRatio || props.liquid || props.height) + ? 'fc-view-harness-active' // harness controls the height + : 'fc-view-harness-passive', + ]; + var height = ''; + var paddingBottom = ''; + if (aspectRatio) { + if (state.availableWidth !== null) { + height = state.availableWidth / aspectRatio; + } + else { + // while waiting to know availableWidth, we can't set height to *zero* + // because will cause lots of unnecessary scrollbars within scrollgrid. + // BETTER: don't start rendering ANYTHING yet until we know container width + // NOTE: why not always use paddingBottom? Causes height oscillation (issue 5606) + paddingBottom = (1 / aspectRatio) * 100 + "%"; + } + } + else { + height = props.height || ''; + } + return (createElement("div", { ref: this.handleEl, onClick: props.onClick, className: classNames.join(' '), style: { height: height, paddingBottom: paddingBottom } }, props.children)); + }; + ViewContainer.prototype.componentDidMount = function () { + this.context.addResizeHandler(this.handleResize); + }; + ViewContainer.prototype.componentWillUnmount = function () { + this.context.removeResizeHandler(this.handleResize); + }; + ViewContainer.prototype.updateAvailableWidth = function () { + if (this.el && // needed. but why? + this.props.aspectRatio // aspectRatio is the only height setting that needs availableWidth + ) { + this.setState({ availableWidth: this.el.offsetWidth }); + } + }; + return ViewContainer; + }(BaseComponent)); + + /* + Detects when the user clicks on an event within a DateComponent + */ + var EventClicking = /** @class */ (function (_super) { + __extends(EventClicking, _super); + function EventClicking(settings) { + var _this = _super.call(this, settings) || this; + _this.handleSegClick = function (ev, segEl) { + var component = _this.component; + var context = component.context; + var seg = getElSeg(segEl); + if (seg && // might be the
surrounding the more link + component.isValidSegDownEl(ev.target)) { + // our way to simulate a link click for elements that can't be tags + // grab before trigger fired in case trigger trashes DOM thru rerendering + var hasUrlContainer = elementClosest(ev.target, '.fc-event-forced-url'); + var url = hasUrlContainer ? hasUrlContainer.querySelector('a[href]').href : ''; + context.emitter.trigger('eventClick', { + el: segEl, + event: new EventApi(component.context, seg.eventRange.def, seg.eventRange.instance), + jsEvent: ev, + view: context.viewApi, + }); + if (url && !ev.defaultPrevented) { + window.location.href = url; + } + } + }; + _this.destroy = listenBySelector(settings.el, 'click', '.fc-event', // on both fg and bg events + _this.handleSegClick); + return _this; + } + return EventClicking; + }(Interaction)); + + /* + Triggers events and adds/removes core classNames when the user's pointer + enters/leaves event-elements of a component. + */ + var EventHovering = /** @class */ (function (_super) { + __extends(EventHovering, _super); + function EventHovering(settings) { + var _this = _super.call(this, settings) || this; + // for simulating an eventMouseLeave when the event el is destroyed while mouse is over it + _this.handleEventElRemove = function (el) { + if (el === _this.currentSegEl) { + _this.handleSegLeave(null, _this.currentSegEl); + } + }; + _this.handleSegEnter = function (ev, segEl) { + if (getElSeg(segEl)) { // TODO: better way to make sure not hovering over more+ link or its wrapper + _this.currentSegEl = segEl; + _this.triggerEvent('eventMouseEnter', ev, segEl); + } + }; + _this.handleSegLeave = function (ev, segEl) { + if (_this.currentSegEl) { + _this.currentSegEl = null; + _this.triggerEvent('eventMouseLeave', ev, segEl); + } + }; + _this.removeHoverListeners = listenToHoverBySelector(settings.el, '.fc-event', // on both fg and bg events + _this.handleSegEnter, _this.handleSegLeave); + return _this; + } + EventHovering.prototype.destroy = function () { + this.removeHoverListeners(); + }; + EventHovering.prototype.triggerEvent = function (publicEvName, ev, segEl) { + var component = this.component; + var context = component.context; + var seg = getElSeg(segEl); + if (!ev || component.isValidSegDownEl(ev.target)) { + context.emitter.trigger(publicEvName, { + el: segEl, + event: new EventApi(context, seg.eventRange.def, seg.eventRange.instance), + jsEvent: ev, + view: context.viewApi, + }); + } + }; + return EventHovering; + }(Interaction)); + + var CalendarContent = /** @class */ (function (_super) { + __extends(CalendarContent, _super); + function CalendarContent() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.buildViewContext = memoize(buildViewContext); + _this.buildViewPropTransformers = memoize(buildViewPropTransformers); + _this.buildToolbarProps = memoize(buildToolbarProps); + _this.handleNavLinkClick = buildDelegationHandler('a[data-navlink]', _this._handleNavLinkClick.bind(_this)); + _this.headerRef = createRef(); + _this.footerRef = createRef(); + _this.interactionsStore = {}; + // Component Registration + // ----------------------------------------------------------------------------------------------------------------- + _this.registerInteractiveComponent = function (component, settingsInput) { + var settings = parseInteractionSettings(component, settingsInput); + var DEFAULT_INTERACTIONS = [ + EventClicking, + EventHovering, + ]; + var interactionClasses = DEFAULT_INTERACTIONS.concat(_this.props.pluginHooks.componentInteractions); + var interactions = interactionClasses.map(function (TheInteractionClass) { return new TheInteractionClass(settings); }); + _this.interactionsStore[component.uid] = interactions; + interactionSettingsStore[component.uid] = settings; + }; + _this.unregisterInteractiveComponent = function (component) { + for (var _i = 0, _a = _this.interactionsStore[component.uid]; _i < _a.length; _i++) { + var listener = _a[_i]; + listener.destroy(); + } + delete _this.interactionsStore[component.uid]; + delete interactionSettingsStore[component.uid]; + }; + // Resizing + // ----------------------------------------------------------------------------------------------------------------- + _this.resizeRunner = new DelayedRunner(function () { + _this.props.emitter.trigger('_resize', true); // should window resizes be considered "forced" ? + _this.props.emitter.trigger('windowResize', { view: _this.props.viewApi }); + }); + _this.handleWindowResize = function (ev) { + var options = _this.props.options; + if (options.handleWindowResize && + ev.target === window // avoid jqui events + ) { + _this.resizeRunner.request(options.windowResizeDelay); + } + }; + return _this; + } + /* + renders INSIDE of an outer div + */ + CalendarContent.prototype.render = function () { + var props = this.props; + var toolbarConfig = props.toolbarConfig, options = props.options; + var toolbarProps = this.buildToolbarProps(props.viewSpec, props.dateProfile, props.dateProfileGenerator, props.currentDate, getNow(props.options.now, props.dateEnv), // TODO: use NowTimer???? + props.viewTitle); + var viewVGrow = false; + var viewHeight = ''; + var viewAspectRatio; + if (props.isHeightAuto || props.forPrint) { + viewHeight = ''; + } + else if (options.height != null) { + viewVGrow = true; + } + else if (options.contentHeight != null) { + viewHeight = options.contentHeight; + } + else { + viewAspectRatio = Math.max(options.aspectRatio, 0.5); // prevent from getting too tall + } + var viewContext = this.buildViewContext(props.viewSpec, props.viewApi, props.options, props.dateProfileGenerator, props.dateEnv, props.theme, props.pluginHooks, props.dispatch, props.getCurrentData, props.emitter, props.calendarApi, this.registerInteractiveComponent, this.unregisterInteractiveComponent); + return (createElement(ViewContextType.Provider, { value: viewContext }, + toolbarConfig.headerToolbar && (createElement(Toolbar, __assign({ ref: this.headerRef, extraClassName: "fc-header-toolbar", model: toolbarConfig.headerToolbar }, toolbarProps))), + createElement(ViewContainer, { liquid: viewVGrow, height: viewHeight, aspectRatio: viewAspectRatio, onClick: this.handleNavLinkClick }, + this.renderView(props), + this.buildAppendContent()), + toolbarConfig.footerToolbar && (createElement(Toolbar, __assign({ ref: this.footerRef, extraClassName: "fc-footer-toolbar", model: toolbarConfig.footerToolbar }, toolbarProps))))); + }; + CalendarContent.prototype.componentDidMount = function () { + var props = this.props; + this.calendarInteractions = props.pluginHooks.calendarInteractions + .map(function (CalendarInteractionClass) { return new CalendarInteractionClass(props); }); + window.addEventListener('resize', this.handleWindowResize); + var propSetHandlers = props.pluginHooks.propSetHandlers; + for (var propName in propSetHandlers) { + propSetHandlers[propName](props[propName], props); + } + }; + CalendarContent.prototype.componentDidUpdate = function (prevProps) { + var props = this.props; + var propSetHandlers = props.pluginHooks.propSetHandlers; + for (var propName in propSetHandlers) { + if (props[propName] !== prevProps[propName]) { + propSetHandlers[propName](props[propName], props); + } + } + }; + CalendarContent.prototype.componentWillUnmount = function () { + window.removeEventListener('resize', this.handleWindowResize); + this.resizeRunner.clear(); + for (var _i = 0, _a = this.calendarInteractions; _i < _a.length; _i++) { + var interaction = _a[_i]; + interaction.destroy(); + } + this.props.emitter.trigger('_unmount'); + }; + CalendarContent.prototype._handleNavLinkClick = function (ev, anchorEl) { + var _a = this.props, dateEnv = _a.dateEnv, options = _a.options, calendarApi = _a.calendarApi; + var navLinkOptions = anchorEl.getAttribute('data-navlink'); + navLinkOptions = navLinkOptions ? JSON.parse(navLinkOptions) : {}; + var dateMarker = dateEnv.createMarker(navLinkOptions.date); + var viewType = navLinkOptions.type; + var customAction = viewType === 'day' ? options.navLinkDayClick : + viewType === 'week' ? options.navLinkWeekClick : null; + if (typeof customAction === 'function') { + customAction.call(calendarApi, dateEnv.toDate(dateMarker), ev); + } + else { + if (typeof customAction === 'string') { + viewType = customAction; + } + calendarApi.zoomTo(dateMarker, viewType); + } + }; + CalendarContent.prototype.buildAppendContent = function () { + var props = this.props; + var children = props.pluginHooks.viewContainerAppends.map(function (buildAppendContent) { return buildAppendContent(props); }); + return createElement.apply(void 0, __spreadArrays([Fragment, {}], children)); + }; + CalendarContent.prototype.renderView = function (props) { + var pluginHooks = props.pluginHooks; + var viewSpec = props.viewSpec; + var viewProps = { + dateProfile: props.dateProfile, + businessHours: props.businessHours, + eventStore: props.renderableEventStore, + eventUiBases: props.eventUiBases, + dateSelection: props.dateSelection, + eventSelection: props.eventSelection, + eventDrag: props.eventDrag, + eventResize: props.eventResize, + isHeightAuto: props.isHeightAuto, + forPrint: props.forPrint, + }; + var transformers = this.buildViewPropTransformers(pluginHooks.viewPropsTransformers); + for (var _i = 0, transformers_1 = transformers; _i < transformers_1.length; _i++) { + var transformer = transformers_1[_i]; + __assign(viewProps, transformer.transform(viewProps, props)); + } + var ViewComponent = viewSpec.component; + return (createElement(ViewComponent, __assign({}, viewProps))); + }; + return CalendarContent; + }(PureComponent)); + function buildToolbarProps(viewSpec, dateProfile, dateProfileGenerator, currentDate, now, title) { + // don't force any date-profiles to valid date profiles (the `false`) so that we can tell if it's invalid + var todayInfo = dateProfileGenerator.build(now, undefined, false); // TODO: need `undefined` or else INFINITE LOOP for some reason + var prevInfo = dateProfileGenerator.buildPrev(dateProfile, currentDate, false); + var nextInfo = dateProfileGenerator.buildNext(dateProfile, currentDate, false); + return { + title: title, + activeButton: viewSpec.type, + isTodayEnabled: todayInfo.isValid && !rangeContainsMarker(dateProfile.currentRange, now), + isPrevEnabled: prevInfo.isValid, + isNextEnabled: nextInfo.isValid, + }; + } + // Plugin + // ----------------------------------------------------------------------------------------------------------------- + function buildViewPropTransformers(theClasses) { + return theClasses.map(function (TheClass) { return new TheClass(); }); + } + + var CalendarRoot = /** @class */ (function (_super) { + __extends(CalendarRoot, _super); + function CalendarRoot() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.state = { + forPrint: false, + }; + _this.handleBeforePrint = function () { + _this.setState({ forPrint: true }); + }; + _this.handleAfterPrint = function () { + _this.setState({ forPrint: false }); + }; + return _this; + } + CalendarRoot.prototype.render = function () { + var props = this.props; + var options = props.options; + var forPrint = this.state.forPrint; + var isHeightAuto = forPrint || options.height === 'auto' || options.contentHeight === 'auto'; + var height = (!isHeightAuto && options.height != null) ? options.height : ''; + var classNames = [ + 'fc', + forPrint ? 'fc-media-print' : 'fc-media-screen', + "fc-direction-" + options.direction, + props.theme.getClass('root'), + ]; + if (!getCanVGrowWithinCell()) { + classNames.push('fc-liquid-hack'); + } + return props.children(classNames, height, isHeightAuto, forPrint); + }; + CalendarRoot.prototype.componentDidMount = function () { + var emitter = this.props.emitter; + emitter.on('_beforeprint', this.handleBeforePrint); + emitter.on('_afterprint', this.handleAfterPrint); + }; + CalendarRoot.prototype.componentWillUnmount = function () { + var emitter = this.props.emitter; + emitter.off('_beforeprint', this.handleBeforePrint); + emitter.off('_afterprint', this.handleAfterPrint); + }; + return CalendarRoot; + }(BaseComponent)); + + // Computes a default column header formatting string if `colFormat` is not explicitly defined + function computeFallbackHeaderFormat(datesRepDistinctDays, dayCnt) { + // if more than one week row, or if there are a lot of columns with not much space, + // put just the day numbers will be in each cell + if (!datesRepDistinctDays || dayCnt > 10) { + return createFormatter({ weekday: 'short' }); // "Sat" + } + if (dayCnt > 1) { + return createFormatter({ weekday: 'short', month: 'numeric', day: 'numeric', omitCommas: true }); // "Sat 11/12" + } + return createFormatter({ weekday: 'long' }); // "Saturday" + } + + var CLASS_NAME = 'fc-col-header-cell'; // do the cushion too? no + function renderInner(hookProps) { + return hookProps.text; + } + + var TableDateCell = /** @class */ (function (_super) { + __extends(TableDateCell, _super); + function TableDateCell() { + return _super !== null && _super.apply(this, arguments) || this; + } + TableDateCell.prototype.render = function () { + var _a = this.context, dateEnv = _a.dateEnv, options = _a.options, theme = _a.theme, viewApi = _a.viewApi; + var props = this.props; + var date = props.date, dateProfile = props.dateProfile; + var dayMeta = getDateMeta(date, props.todayRange, null, dateProfile); + var classNames = [CLASS_NAME].concat(getDayClassNames(dayMeta, theme)); + var text = dateEnv.format(date, props.dayHeaderFormat); + // if colCnt is 1, we are already in a day-view and don't need a navlink + var navLinkAttrs = (options.navLinks && !dayMeta.isDisabled && props.colCnt > 1) + ? { 'data-navlink': buildNavLinkData(date), tabIndex: 0 } + : {}; + var hookProps = __assign(__assign(__assign({ date: dateEnv.toDate(date), view: viewApi }, props.extraHookProps), { text: text }), dayMeta); + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.dayHeaderClassNames, content: options.dayHeaderContent, defaultContent: renderInner, didMount: options.dayHeaderDidMount, willUnmount: options.dayHeaderWillUnmount }, function (rootElRef, customClassNames, innerElRef, innerContent) { return (createElement("th", __assign({ ref: rootElRef, className: classNames.concat(customClassNames).join(' '), "data-date": !dayMeta.isDisabled ? formatDayString(date) : undefined, colSpan: props.colSpan }, props.extraDataAttrs), + createElement("div", { className: "fc-scrollgrid-sync-inner" }, !dayMeta.isDisabled && (createElement("a", __assign({ ref: innerElRef, className: [ + 'fc-col-header-cell-cushion', + props.isSticky ? 'fc-sticky' : '', + ].join(' ') }, navLinkAttrs), innerContent))))); })); + }; + return TableDateCell; + }(BaseComponent)); + + var TableDowCell = /** @class */ (function (_super) { + __extends(TableDowCell, _super); + function TableDowCell() { + return _super !== null && _super.apply(this, arguments) || this; + } + TableDowCell.prototype.render = function () { + var props = this.props; + var _a = this.context, dateEnv = _a.dateEnv, theme = _a.theme, viewApi = _a.viewApi, options = _a.options; + var date = addDays(new Date(259200000), props.dow); // start with Sun, 04 Jan 1970 00:00:00 GMT + var dateMeta = { + dow: props.dow, + isDisabled: false, + isFuture: false, + isPast: false, + isToday: false, + isOther: false, + }; + var classNames = [CLASS_NAME].concat(getDayClassNames(dateMeta, theme), props.extraClassNames || []); + var text = dateEnv.format(date, props.dayHeaderFormat); + var hookProps = __assign(__assign(__assign(__assign({ // TODO: make this public? + date: date }, dateMeta), { view: viewApi }), props.extraHookProps), { text: text }); + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.dayHeaderClassNames, content: options.dayHeaderContent, defaultContent: renderInner, didMount: options.dayHeaderDidMount, willUnmount: options.dayHeaderWillUnmount }, function (rootElRef, customClassNames, innerElRef, innerContent) { return (createElement("th", __assign({ ref: rootElRef, className: classNames.concat(customClassNames).join(' '), colSpan: props.colSpan }, props.extraDataAttrs), + createElement("div", { className: "fc-scrollgrid-sync-inner" }, + createElement("a", { className: [ + 'fc-col-header-cell-cushion', + props.isSticky ? 'fc-sticky' : '', + ].join(' '), ref: innerElRef }, innerContent)))); })); + }; + return TableDowCell; + }(BaseComponent)); + + var NowTimer = /** @class */ (function (_super) { + __extends(NowTimer, _super); + function NowTimer(props, context) { + var _this = _super.call(this, props, context) || this; + _this.initialNowDate = getNow(context.options.now, context.dateEnv); + _this.initialNowQueriedMs = new Date().valueOf(); + _this.state = _this.computeTiming().currentState; + return _this; + } + NowTimer.prototype.render = function () { + var _a = this, props = _a.props, state = _a.state; + return props.children(state.nowDate, state.todayRange); + }; + NowTimer.prototype.componentDidMount = function () { + this.setTimeout(); + }; + NowTimer.prototype.componentDidUpdate = function (prevProps) { + if (prevProps.unit !== this.props.unit) { + this.clearTimeout(); + this.setTimeout(); + } + }; + NowTimer.prototype.componentWillUnmount = function () { + this.clearTimeout(); + }; + NowTimer.prototype.computeTiming = function () { + var _a = this, props = _a.props, context = _a.context; + var unroundedNow = addMs(this.initialNowDate, new Date().valueOf() - this.initialNowQueriedMs); + var currentUnitStart = context.dateEnv.startOf(unroundedNow, props.unit); + var nextUnitStart = context.dateEnv.add(currentUnitStart, createDuration(1, props.unit)); + var waitMs = nextUnitStart.valueOf() - unroundedNow.valueOf(); + // there is a max setTimeout ms value (https://stackoverflow.com/a/3468650/96342) + // ensure no longer than a day + waitMs = Math.min(1000 * 60 * 60 * 24, waitMs); + return { + currentState: { nowDate: currentUnitStart, todayRange: buildDayRange(currentUnitStart) }, + nextState: { nowDate: nextUnitStart, todayRange: buildDayRange(nextUnitStart) }, + waitMs: waitMs, + }; + }; + NowTimer.prototype.setTimeout = function () { + var _this = this; + var _a = this.computeTiming(), nextState = _a.nextState, waitMs = _a.waitMs; + this.timeoutId = setTimeout(function () { + _this.setState(nextState, function () { + _this.setTimeout(); + }); + }, waitMs); + }; + NowTimer.prototype.clearTimeout = function () { + if (this.timeoutId) { + clearTimeout(this.timeoutId); + } + }; + NowTimer.contextType = ViewContextType; + return NowTimer; + }(Component)); + function buildDayRange(date) { + var start = startOfDay(date); + var end = addDays(start, 1); + return { start: start, end: end }; + } + + var DayHeader = /** @class */ (function (_super) { + __extends(DayHeader, _super); + function DayHeader() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.createDayHeaderFormatter = memoize(createDayHeaderFormatter); + return _this; + } + DayHeader.prototype.render = function () { + var context = this.context; + var _a = this.props, dates = _a.dates, dateProfile = _a.dateProfile, datesRepDistinctDays = _a.datesRepDistinctDays, renderIntro = _a.renderIntro; + var dayHeaderFormat = this.createDayHeaderFormatter(context.options.dayHeaderFormat, datesRepDistinctDays, dates.length); + return (createElement(NowTimer, { unit: "day" }, function (nowDate, todayRange) { return (createElement("tr", null, + renderIntro && renderIntro('day'), + dates.map(function (date) { return (datesRepDistinctDays ? (createElement(TableDateCell, { key: date.toISOString(), date: date, dateProfile: dateProfile, todayRange: todayRange, colCnt: dates.length, dayHeaderFormat: dayHeaderFormat })) : (createElement(TableDowCell, { key: date.getUTCDay(), dow: date.getUTCDay(), dayHeaderFormat: dayHeaderFormat }))); }))); })); + }; + return DayHeader; + }(BaseComponent)); + function createDayHeaderFormatter(explicitFormat, datesRepDistinctDays, dateCnt) { + return explicitFormat || computeFallbackHeaderFormat(datesRepDistinctDays, dateCnt); + } + + var DaySeriesModel = /** @class */ (function () { + function DaySeriesModel(range, dateProfileGenerator) { + var date = range.start; + var end = range.end; + var indices = []; + var dates = []; + var dayIndex = -1; + while (date < end) { // loop each day from start to end + if (dateProfileGenerator.isHiddenDay(date)) { + indices.push(dayIndex + 0.5); // mark that it's between indices + } + else { + dayIndex += 1; + indices.push(dayIndex); + dates.push(date); + } + date = addDays(date, 1); + } + this.dates = dates; + this.indices = indices; + this.cnt = dates.length; + } + DaySeriesModel.prototype.sliceRange = function (range) { + var firstIndex = this.getDateDayIndex(range.start); // inclusive first index + var lastIndex = this.getDateDayIndex(addDays(range.end, -1)); // inclusive last index + var clippedFirstIndex = Math.max(0, firstIndex); + var clippedLastIndex = Math.min(this.cnt - 1, lastIndex); + // deal with in-between indices + clippedFirstIndex = Math.ceil(clippedFirstIndex); // in-between starts round to next cell + clippedLastIndex = Math.floor(clippedLastIndex); // in-between ends round to prev cell + if (clippedFirstIndex <= clippedLastIndex) { + return { + firstIndex: clippedFirstIndex, + lastIndex: clippedLastIndex, + isStart: firstIndex === clippedFirstIndex, + isEnd: lastIndex === clippedLastIndex, + }; + } + return null; + }; + // Given a date, returns its chronolocial cell-index from the first cell of the grid. + // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets. + // If before the first offset, returns a negative number. + // If after the last offset, returns an offset past the last cell offset. + // Only works for *start* dates of cells. Will not work for exclusive end dates for cells. + DaySeriesModel.prototype.getDateDayIndex = function (date) { + var indices = this.indices; + var dayOffset = Math.floor(diffDays(this.dates[0], date)); + if (dayOffset < 0) { + return indices[0] - 1; + } + if (dayOffset >= indices.length) { + return indices[indices.length - 1] + 1; + } + return indices[dayOffset]; + }; + return DaySeriesModel; + }()); + + var DayTableModel = /** @class */ (function () { + function DayTableModel(daySeries, breakOnWeeks) { + var dates = daySeries.dates; + var daysPerRow; + var firstDay; + var rowCnt; + if (breakOnWeeks) { + // count columns until the day-of-week repeats + firstDay = dates[0].getUTCDay(); + for (daysPerRow = 1; daysPerRow < dates.length; daysPerRow += 1) { + if (dates[daysPerRow].getUTCDay() === firstDay) { + break; + } + } + rowCnt = Math.ceil(dates.length / daysPerRow); + } + else { + rowCnt = 1; + daysPerRow = dates.length; + } + this.rowCnt = rowCnt; + this.colCnt = daysPerRow; + this.daySeries = daySeries; + this.cells = this.buildCells(); + this.headerDates = this.buildHeaderDates(); + } + DayTableModel.prototype.buildCells = function () { + var rows = []; + for (var row = 0; row < this.rowCnt; row += 1) { + var cells = []; + for (var col = 0; col < this.colCnt; col += 1) { + cells.push(this.buildCell(row, col)); + } + rows.push(cells); + } + return rows; + }; + DayTableModel.prototype.buildCell = function (row, col) { + var date = this.daySeries.dates[row * this.colCnt + col]; + return { + key: date.toISOString(), + date: date, + }; + }; + DayTableModel.prototype.buildHeaderDates = function () { + var dates = []; + for (var col = 0; col < this.colCnt; col += 1) { + dates.push(this.cells[0][col].date); + } + return dates; + }; + DayTableModel.prototype.sliceRange = function (range) { + var colCnt = this.colCnt; + var seriesSeg = this.daySeries.sliceRange(range); + var segs = []; + if (seriesSeg) { + var firstIndex = seriesSeg.firstIndex, lastIndex = seriesSeg.lastIndex; + var index = firstIndex; + while (index <= lastIndex) { + var row = Math.floor(index / colCnt); + var nextIndex = Math.min((row + 1) * colCnt, lastIndex + 1); + segs.push({ + row: row, + firstCol: index % colCnt, + lastCol: (nextIndex - 1) % colCnt, + isStart: seriesSeg.isStart && index === firstIndex, + isEnd: seriesSeg.isEnd && (nextIndex - 1) === lastIndex, + }); + index = nextIndex; + } + } + return segs; + }; + return DayTableModel; + }()); + + var Slicer = /** @class */ (function () { + function Slicer() { + this.sliceBusinessHours = memoize(this._sliceBusinessHours); + this.sliceDateSelection = memoize(this._sliceDateSpan); + this.sliceEventStore = memoize(this._sliceEventStore); + this.sliceEventDrag = memoize(this._sliceInteraction); + this.sliceEventResize = memoize(this._sliceInteraction); + this.forceDayIfListItem = false; // hack + } + Slicer.prototype.sliceProps = function (props, dateProfile, nextDayThreshold, context) { + var extraArgs = []; + for (var _i = 4; _i < arguments.length; _i++) { + extraArgs[_i - 4] = arguments[_i]; + } + var eventUiBases = props.eventUiBases; + var eventSegs = this.sliceEventStore.apply(this, __spreadArrays([props.eventStore, eventUiBases, dateProfile, nextDayThreshold], extraArgs)); + return { + dateSelectionSegs: this.sliceDateSelection.apply(this, __spreadArrays([props.dateSelection, eventUiBases, context], extraArgs)), + businessHourSegs: this.sliceBusinessHours.apply(this, __spreadArrays([props.businessHours, dateProfile, nextDayThreshold, context], extraArgs)), + fgEventSegs: eventSegs.fg, + bgEventSegs: eventSegs.bg, + eventDrag: this.sliceEventDrag.apply(this, __spreadArrays([props.eventDrag, eventUiBases, dateProfile, nextDayThreshold], extraArgs)), + eventResize: this.sliceEventResize.apply(this, __spreadArrays([props.eventResize, eventUiBases, dateProfile, nextDayThreshold], extraArgs)), + eventSelection: props.eventSelection, + }; // TODO: give interactionSegs? + }; + Slicer.prototype.sliceNowDate = function (// does not memoize + date, context) { + var extraArgs = []; + for (var _i = 2; _i < arguments.length; _i++) { + extraArgs[_i - 2] = arguments[_i]; + } + return this._sliceDateSpan.apply(this, __spreadArrays([{ range: { start: date, end: addMs(date, 1) }, allDay: false }, + {}, + context], extraArgs)); + }; + Slicer.prototype._sliceBusinessHours = function (businessHours, dateProfile, nextDayThreshold, context) { + var extraArgs = []; + for (var _i = 4; _i < arguments.length; _i++) { + extraArgs[_i - 4] = arguments[_i]; + } + if (!businessHours) { + return []; + } + return this._sliceEventStore.apply(this, __spreadArrays([expandRecurring(businessHours, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), context), + {}, + dateProfile, + nextDayThreshold], extraArgs)).bg; + }; + Slicer.prototype._sliceEventStore = function (eventStore, eventUiBases, dateProfile, nextDayThreshold) { + var extraArgs = []; + for (var _i = 4; _i < arguments.length; _i++) { + extraArgs[_i - 4] = arguments[_i]; + } + if (eventStore) { + var rangeRes = sliceEventStore(eventStore, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold); + return { + bg: this.sliceEventRanges(rangeRes.bg, extraArgs), + fg: this.sliceEventRanges(rangeRes.fg, extraArgs), + }; + } + return { bg: [], fg: [] }; + }; + Slicer.prototype._sliceInteraction = function (interaction, eventUiBases, dateProfile, nextDayThreshold) { + var extraArgs = []; + for (var _i = 4; _i < arguments.length; _i++) { + extraArgs[_i - 4] = arguments[_i]; + } + if (!interaction) { + return null; + } + var rangeRes = sliceEventStore(interaction.mutatedEvents, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold); + return { + segs: this.sliceEventRanges(rangeRes.fg, extraArgs), + affectedInstances: interaction.affectedEvents.instances, + isEvent: interaction.isEvent, + }; + }; + Slicer.prototype._sliceDateSpan = function (dateSpan, eventUiBases, context) { + var extraArgs = []; + for (var _i = 3; _i < arguments.length; _i++) { + extraArgs[_i - 3] = arguments[_i]; + } + if (!dateSpan) { + return []; + } + var eventRange = fabricateEventRange(dateSpan, eventUiBases, context); + var segs = this.sliceRange.apply(this, __spreadArrays([dateSpan.range], extraArgs)); + for (var _a = 0, segs_1 = segs; _a < segs_1.length; _a++) { + var seg = segs_1[_a]; + seg.eventRange = eventRange; + } + return segs; + }; + /* + "complete" seg means it has component and eventRange + */ + Slicer.prototype.sliceEventRanges = function (eventRanges, extraArgs) { + var segs = []; + for (var _i = 0, eventRanges_1 = eventRanges; _i < eventRanges_1.length; _i++) { + var eventRange = eventRanges_1[_i]; + segs.push.apply(segs, this.sliceEventRange(eventRange, extraArgs)); + } + return segs; + }; + /* + "complete" seg means it has component and eventRange + */ + Slicer.prototype.sliceEventRange = function (eventRange, extraArgs) { + var dateRange = eventRange.range; + // hack to make multi-day events that are being force-displayed as list-items to take up only one day + if (this.forceDayIfListItem && eventRange.ui.display === 'list-item') { + dateRange = { + start: dateRange.start, + end: addDays(dateRange.start, 1), + }; + } + var segs = this.sliceRange.apply(this, __spreadArrays([dateRange], extraArgs)); + for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) { + var seg = segs_2[_i]; + seg.eventRange = eventRange; + seg.isStart = eventRange.isStart && seg.isStart; + seg.isEnd = eventRange.isEnd && seg.isEnd; + } + return segs; + }; + return Slicer; + }()); + /* + for incorporating slotMinTime/slotMaxTime if appropriate + TODO: should be part of DateProfile! + TimelineDateProfile already does this btw + */ + function computeActiveRange(dateProfile, isComponentAllDay) { + var range = dateProfile.activeRange; + if (isComponentAllDay) { + return range; + } + return { + start: addMs(range.start, dateProfile.slotMinTime.milliseconds), + end: addMs(range.end, dateProfile.slotMaxTime.milliseconds - 864e5), + }; + } + + var VISIBLE_HIDDEN_RE = /^(visible|hidden)$/; + var Scroller = /** @class */ (function (_super) { + __extends(Scroller, _super); + function Scroller() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.handleEl = function (el) { + _this.el = el; + setRef(_this.props.elRef, el); + }; + return _this; + } + Scroller.prototype.render = function () { + var props = this.props; + var liquid = props.liquid, liquidIsAbsolute = props.liquidIsAbsolute; + var isAbsolute = liquid && liquidIsAbsolute; + var className = ['fc-scroller']; + if (liquid) { + if (liquidIsAbsolute) { + className.push('fc-scroller-liquid-absolute'); + } + else { + className.push('fc-scroller-liquid'); + } + } + return (createElement("div", { ref: this.handleEl, className: className.join(' '), style: { + overflowX: props.overflowX, + overflowY: props.overflowY, + left: (isAbsolute && -(props.overcomeLeft || 0)) || '', + right: (isAbsolute && -(props.overcomeRight || 0)) || '', + bottom: (isAbsolute && -(props.overcomeBottom || 0)) || '', + marginLeft: (!isAbsolute && -(props.overcomeLeft || 0)) || '', + marginRight: (!isAbsolute && -(props.overcomeRight || 0)) || '', + marginBottom: (!isAbsolute && -(props.overcomeBottom || 0)) || '', + maxHeight: props.maxHeight || '', + } }, props.children)); + }; + Scroller.prototype.needsXScrolling = function () { + if (VISIBLE_HIDDEN_RE.test(this.props.overflowX)) { + return false; + } + // testing scrollWidth>clientWidth is unreliable cross-browser when pixel heights aren't integers. + // much more reliable to see if children are taller than the scroller, even tho doesn't account for + // inner-child margins and absolute positioning + var el = this.el; + var realClientWidth = this.el.getBoundingClientRect().width - this.getYScrollbarWidth(); + var children = el.children; + for (var i = 0; i < children.length; i += 1) { + var childEl = children[i]; + if (childEl.getBoundingClientRect().width > realClientWidth) { + return true; + } + } + return false; + }; + Scroller.prototype.needsYScrolling = function () { + if (VISIBLE_HIDDEN_RE.test(this.props.overflowY)) { + return false; + } + // testing scrollHeight>clientHeight is unreliable cross-browser when pixel heights aren't integers. + // much more reliable to see if children are taller than the scroller, even tho doesn't account for + // inner-child margins and absolute positioning + var el = this.el; + var realClientHeight = this.el.getBoundingClientRect().height - this.getXScrollbarWidth(); + var children = el.children; + for (var i = 0; i < children.length; i += 1) { + var childEl = children[i]; + if (childEl.getBoundingClientRect().height > realClientHeight) { + return true; + } + } + return false; + }; + Scroller.prototype.getXScrollbarWidth = function () { + if (VISIBLE_HIDDEN_RE.test(this.props.overflowX)) { + return 0; + } + return this.el.offsetHeight - this.el.clientHeight; // only works because we guarantee no borders. TODO: add to CSS with important? + }; + Scroller.prototype.getYScrollbarWidth = function () { + if (VISIBLE_HIDDEN_RE.test(this.props.overflowY)) { + return 0; + } + return this.el.offsetWidth - this.el.clientWidth; // only works because we guarantee no borders. TODO: add to CSS with important? + }; + return Scroller; + }(BaseComponent)); + + /* + TODO: somehow infer OtherArgs from masterCallback? + TODO: infer RefType from masterCallback if provided + */ + var RefMap = /** @class */ (function () { + function RefMap(masterCallback) { + var _this = this; + this.masterCallback = masterCallback; + this.currentMap = {}; + this.depths = {}; + this.callbackMap = {}; + this.handleValue = function (val, key) { + var _a = _this, depths = _a.depths, currentMap = _a.currentMap; + var removed = false; + var added = false; + if (val !== null) { + // for bug... ACTUALLY: can probably do away with this now that callers don't share numeric indices anymore + removed = (key in currentMap); + currentMap[key] = val; + depths[key] = (depths[key] || 0) + 1; + added = true; + } + else { + depths[key] -= 1; + if (!depths[key]) { + delete currentMap[key]; + delete _this.callbackMap[key]; + removed = true; + } + } + if (_this.masterCallback) { + if (removed) { + _this.masterCallback(null, String(key)); + } + if (added) { + _this.masterCallback(val, String(key)); + } + } + }; + } + RefMap.prototype.createRef = function (key) { + var _this = this; + var refCallback = this.callbackMap[key]; + if (!refCallback) { + refCallback = this.callbackMap[key] = function (val) { + _this.handleValue(val, String(key)); + }; + } + return refCallback; + }; + // TODO: check callers that don't care about order. should use getAll instead + // NOTE: this method has become less valuable now that we are encouraged to map order by some other index + // TODO: provide ONE array-export function, buildArray, which fails on non-numeric indexes. caller can manipulate and "collect" + RefMap.prototype.collect = function (startIndex, endIndex, step) { + return collectFromHash(this.currentMap, startIndex, endIndex, step); + }; + RefMap.prototype.getAll = function () { + return hashValuesToArray(this.currentMap); + }; + return RefMap; + }()); + + function computeShrinkWidth(chunkEls) { + var shrinkCells = findElements(chunkEls, '.fc-scrollgrid-shrink'); + var largestWidth = 0; + for (var _i = 0, shrinkCells_1 = shrinkCells; _i < shrinkCells_1.length; _i++) { + var shrinkCell = shrinkCells_1[_i]; + largestWidth = Math.max(largestWidth, computeSmallestCellWidth(shrinkCell)); + } + return Math.ceil(largestWidth); // elements work best with integers. round up to ensure contents fits + } + function getSectionHasLiquidHeight(props, sectionConfig) { + return props.liquid && sectionConfig.liquid; // does the section do liquid-height? (need to have whole scrollgrid liquid-height as well) + } + function getAllowYScrolling(props, sectionConfig) { + return sectionConfig.maxHeight != null || // if its possible for the height to max out, we might need scrollbars + getSectionHasLiquidHeight(props, sectionConfig); // if the section is liquid height, it might condense enough to require scrollbars + } + // TODO: ONLY use `arg`. force out internal function to use same API + function renderChunkContent(sectionConfig, chunkConfig, arg) { + var expandRows = arg.expandRows; + var content = typeof chunkConfig.content === 'function' ? + chunkConfig.content(arg) : + createElement('table', { + className: [ + chunkConfig.tableClassName, + sectionConfig.syncRowHeights ? 'fc-scrollgrid-sync-table' : '', + ].join(' '), + style: { + minWidth: arg.tableMinWidth, + width: arg.clientWidth, + height: expandRows ? arg.clientHeight : '', + }, + }, arg.tableColGroupNode, createElement('tbody', {}, typeof chunkConfig.rowContent === 'function' ? chunkConfig.rowContent(arg) : chunkConfig.rowContent)); + return content; + } + function isColPropsEqual(cols0, cols1) { + return isArraysEqual(cols0, cols1, isPropsEqual); + } + function renderMicroColGroup(cols, shrinkWidth) { + var colNodes = []; + /* + for ColProps with spans, it would have been great to make a single + HOWEVER, Chrome was getting messing up distributing the width to elements makes Chrome behave. + */ + for (var _i = 0, cols_1 = cols; _i < cols_1.length; _i++) { + var colProps = cols_1[_i]; + var span = colProps.span || 1; + for (var i = 0; i < span; i += 1) { + colNodes.push(createElement("col", { style: { + width: colProps.width === 'shrink' ? sanitizeShrinkWidth(shrinkWidth) : (colProps.width || ''), + minWidth: colProps.minWidth || '', + } })); + } + } + return createElement.apply(void 0, __spreadArrays(['colgroup', {}], colNodes)); + } + function sanitizeShrinkWidth(shrinkWidth) { + /* why 4? if we do 0, it will kill any border, which are needed for computeSmallestCellWidth + 4 accounts for 2 2-pixel borders. TODO: better solution? */ + return shrinkWidth == null ? 4 : shrinkWidth; + } + function hasShrinkWidth(cols) { + for (var _i = 0, cols_2 = cols; _i < cols_2.length; _i++) { + var col = cols_2[_i]; + if (col.width === 'shrink') { + return true; + } + } + return false; + } + function getScrollGridClassNames(liquid, context) { + var classNames = [ + 'fc-scrollgrid', + context.theme.getClass('table'), + ]; + if (liquid) { + classNames.push('fc-scrollgrid-liquid'); + } + return classNames; + } + function getSectionClassNames(sectionConfig, wholeTableVGrow) { + var classNames = [ + 'fc-scrollgrid-section', + "fc-scrollgrid-section-" + sectionConfig.type, + sectionConfig.className, + ]; + if (wholeTableVGrow && sectionConfig.liquid && sectionConfig.maxHeight == null) { + classNames.push('fc-scrollgrid-section-liquid'); + } + if (sectionConfig.isSticky) { + classNames.push('fc-scrollgrid-section-sticky'); + } + return classNames; + } + function renderScrollShim(arg) { + return (createElement("div", { className: "fc-scrollgrid-sticky-shim", style: { + width: arg.clientWidth, + minWidth: arg.tableMinWidth, + } })); + } + function getStickyHeaderDates(options) { + var stickyHeaderDates = options.stickyHeaderDates; + if (stickyHeaderDates == null || stickyHeaderDates === 'auto') { + stickyHeaderDates = options.height === 'auto' || options.viewHeight === 'auto'; + } + return stickyHeaderDates; + } + function getStickyFooterScrollbar(options) { + var stickyFooterScrollbar = options.stickyFooterScrollbar; + if (stickyFooterScrollbar == null || stickyFooterScrollbar === 'auto') { + stickyFooterScrollbar = options.height === 'auto' || options.viewHeight === 'auto'; + } + return stickyFooterScrollbar; + } + + var SimpleScrollGrid = /** @class */ (function (_super) { + __extends(SimpleScrollGrid, _super); + function SimpleScrollGrid() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.processCols = memoize(function (a) { return a; }, isColPropsEqual); // so we get same `cols` props every time + // yucky to memoize VNodes, but much more efficient for consumers + _this.renderMicroColGroup = memoize(renderMicroColGroup); + _this.scrollerRefs = new RefMap(); + _this.scrollerElRefs = new RefMap(_this._handleScrollerEl.bind(_this)); + _this.state = { + shrinkWidth: null, + forceYScrollbars: false, + scrollerClientWidths: {}, + scrollerClientHeights: {}, + }; + // TODO: can do a really simple print-view. dont need to join rows + _this.handleSizing = function () { + _this.setState(__assign({ shrinkWidth: _this.computeShrinkWidth() }, _this.computeScrollerDims())); + }; + return _this; + } + SimpleScrollGrid.prototype.render = function () { + var _a = this, props = _a.props, state = _a.state, context = _a.context; + var sectionConfigs = props.sections || []; + var cols = this.processCols(props.cols); + var microColGroupNode = this.renderMicroColGroup(cols, state.shrinkWidth); + var classNames = getScrollGridClassNames(props.liquid, context); + // TODO: make DRY + var configCnt = sectionConfigs.length; + var configI = 0; + var currentConfig; + var headSectionNodes = []; + var bodySectionNodes = []; + var footSectionNodes = []; + while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'header') { + headSectionNodes.push(this.renderSection(currentConfig, configI, microColGroupNode)); + configI += 1; + } + while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'body') { + bodySectionNodes.push(this.renderSection(currentConfig, configI, microColGroupNode)); + configI += 1; + } + while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'footer') { + footSectionNodes.push(this.renderSection(currentConfig, configI, microColGroupNode)); + configI += 1; + } + // firefox bug: when setting height on table and there is a thead or tfoot, + // the necessary height:100% on the liquid-height body section forces the *whole* table to be taller. (bug #5524) + // use getCanVGrowWithinCell as a way to detect table-stupid firefox. + // if so, use a simpler dom structure, jam everything into a lone tbody. + var isBuggy = !getCanVGrowWithinCell(); + return createElement('table', { + className: classNames.join(' '), + style: { height: props.height }, + }, Boolean(!isBuggy && headSectionNodes.length) && createElement.apply(void 0, __spreadArrays(['thead', {}], headSectionNodes)), Boolean(!isBuggy && bodySectionNodes.length) && createElement.apply(void 0, __spreadArrays(['tbody', {}], bodySectionNodes)), Boolean(!isBuggy && footSectionNodes.length) && createElement.apply(void 0, __spreadArrays(['tfoot', {}], footSectionNodes)), isBuggy && createElement.apply(void 0, __spreadArrays(['tbody', {}], headSectionNodes, bodySectionNodes, footSectionNodes))); + }; + SimpleScrollGrid.prototype.renderSection = function (sectionConfig, sectionI, microColGroupNode) { + if ('outerContent' in sectionConfig) { + return (createElement(Fragment, { key: sectionConfig.key }, sectionConfig.outerContent)); + } + return (createElement("tr", { key: sectionConfig.key, className: getSectionClassNames(sectionConfig, this.props.liquid).join(' ') }, this.renderChunkTd(sectionConfig, sectionI, microColGroupNode, sectionConfig.chunk))); + }; + SimpleScrollGrid.prototype.renderChunkTd = function (sectionConfig, sectionI, microColGroupNode, chunkConfig) { + if ('outerContent' in chunkConfig) { + return chunkConfig.outerContent; + } + var props = this.props; + var _a = this.state, forceYScrollbars = _a.forceYScrollbars, scrollerClientWidths = _a.scrollerClientWidths, scrollerClientHeights = _a.scrollerClientHeights; + var needsYScrolling = getAllowYScrolling(props, sectionConfig); // TODO: do lazily. do in section config? + var isLiquid = getSectionHasLiquidHeight(props, sectionConfig); + // for `!props.liquid` - is WHOLE scrollgrid natural height? + // TODO: do same thing in advanced scrollgrid? prolly not b/c always has horizontal scrollbars + var overflowY = !props.liquid ? 'visible' : + forceYScrollbars ? 'scroll' : + !needsYScrolling ? 'hidden' : + 'auto'; + var content = renderChunkContent(sectionConfig, chunkConfig, { + tableColGroupNode: microColGroupNode, + tableMinWidth: '', + clientWidth: scrollerClientWidths[sectionI] !== undefined ? scrollerClientWidths[sectionI] : null, + clientHeight: scrollerClientHeights[sectionI] !== undefined ? scrollerClientHeights[sectionI] : null, + expandRows: sectionConfig.expandRows, + syncRowHeights: false, + rowSyncHeights: [], + reportRowHeightChange: function () { }, + }); + return (createElement("td", { ref: chunkConfig.elRef }, + createElement("div", { className: "fc-scroller-harness" + (isLiquid ? ' fc-scroller-harness-liquid' : '') }, + createElement(Scroller, { ref: this.scrollerRefs.createRef(sectionI), elRef: this.scrollerElRefs.createRef(sectionI), overflowY: overflowY, overflowX: !props.liquid ? 'visible' : 'hidden' /* natural height? */, maxHeight: sectionConfig.maxHeight, liquid: isLiquid, liquidIsAbsolute // because its within a harness + : true }, content)))); + }; + SimpleScrollGrid.prototype._handleScrollerEl = function (scrollerEl, key) { + var sectionI = parseInt(key, 10); + var chunkConfig = this.props.sections[sectionI].chunk; + setRef(chunkConfig.scrollerElRef, scrollerEl); + }; + SimpleScrollGrid.prototype.componentDidMount = function () { + this.handleSizing(); + this.context.addResizeHandler(this.handleSizing); + }; + SimpleScrollGrid.prototype.componentDidUpdate = function () { + // TODO: need better solution when state contains non-sizing things + this.handleSizing(); + }; + SimpleScrollGrid.prototype.componentWillUnmount = function () { + this.context.removeResizeHandler(this.handleSizing); + }; + SimpleScrollGrid.prototype.computeShrinkWidth = function () { + return hasShrinkWidth(this.props.cols) + ? computeShrinkWidth(this.scrollerElRefs.getAll()) + : 0; + }; + SimpleScrollGrid.prototype.computeScrollerDims = function () { + var scrollbarWidth = getScrollbarWidths(); + var sectionCnt = this.props.sections.length; + var _a = this, scrollerRefs = _a.scrollerRefs, scrollerElRefs = _a.scrollerElRefs; + var forceYScrollbars = false; + var scrollerClientWidths = {}; + var scrollerClientHeights = {}; + for (var sectionI = 0; sectionI < sectionCnt; sectionI += 1) { // along edge + var scroller = scrollerRefs.currentMap[sectionI]; + if (scroller && scroller.needsYScrolling()) { + forceYScrollbars = true; + break; + } + } + for (var sectionI = 0; sectionI < sectionCnt; sectionI += 1) { // along edge + var scrollerEl = scrollerElRefs.currentMap[sectionI]; + if (scrollerEl) { + var harnessEl = scrollerEl.parentNode; // TODO: weird way to get this. need harness b/c doesn't include table borders + scrollerClientWidths[sectionI] = Math.floor(harnessEl.getBoundingClientRect().width - (forceYScrollbars + ? scrollbarWidth.y // use global because scroller might not have scrollbars yet but will need them in future + : 0)); + scrollerClientHeights[sectionI] = Math.floor(harnessEl.getBoundingClientRect().height); + } + } + return { forceYScrollbars: forceYScrollbars, scrollerClientWidths: scrollerClientWidths, scrollerClientHeights: scrollerClientHeights }; + }; + return SimpleScrollGrid; + }(BaseComponent)); + SimpleScrollGrid.addStateEquality({ + scrollerClientWidths: isPropsEqual, + scrollerClientHeights: isPropsEqual, + }); + + var EventRoot = /** @class */ (function (_super) { + __extends(EventRoot, _super); + function EventRoot() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.elRef = createRef(); + return _this; + } + EventRoot.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context; + var options = context.options; + var seg = props.seg; + var eventRange = seg.eventRange; + var ui = eventRange.ui; + var hookProps = { + event: new EventApi(context, eventRange.def, eventRange.instance), + view: context.viewApi, + timeText: props.timeText, + textColor: ui.textColor, + backgroundColor: ui.backgroundColor, + borderColor: ui.borderColor, + isDraggable: !props.disableDragging && computeSegDraggable(seg, context), + isStartResizable: !props.disableResizing && computeSegStartResizable(seg, context), + isEndResizable: !props.disableResizing && computeSegEndResizable(seg), + isMirror: Boolean(props.isDragging || props.isResizing || props.isDateSelecting), + isStart: Boolean(seg.isStart), + isEnd: Boolean(seg.isEnd), + isPast: Boolean(props.isPast), + isFuture: Boolean(props.isFuture), + isToday: Boolean(props.isToday), + isSelected: Boolean(props.isSelected), + isDragging: Boolean(props.isDragging), + isResizing: Boolean(props.isResizing), + }; + var standardClassNames = getEventClassNames(hookProps).concat(ui.classNames); + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.eventClassNames, content: options.eventContent, defaultContent: props.defaultContent, didMount: options.eventDidMount, willUnmount: options.eventWillUnmount, elRef: this.elRef }, function (rootElRef, customClassNames, innerElRef, innerContent) { return props.children(rootElRef, standardClassNames.concat(customClassNames), innerElRef, innerContent, hookProps); })); + }; + EventRoot.prototype.componentDidMount = function () { + setElSeg(this.elRef.current, this.props.seg); + }; + /* + need to re-assign seg to the element if seg changes, even if the element is the same + */ + EventRoot.prototype.componentDidUpdate = function (prevProps) { + var seg = this.props.seg; + if (seg !== prevProps.seg) { + setElSeg(this.elRef.current, seg); + } + }; + return EventRoot; + }(BaseComponent)); + + // should not be a purecomponent + var StandardEvent = /** @class */ (function (_super) { + __extends(StandardEvent, _super); + function StandardEvent() { + return _super !== null && _super.apply(this, arguments) || this; + } + StandardEvent.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context; + var seg = props.seg; + var timeFormat = context.options.eventTimeFormat || props.defaultTimeFormat; + var timeText = buildSegTimeText(seg, timeFormat, context, props.defaultDisplayEventTime, props.defaultDisplayEventEnd); + return (createElement(EventRoot, { seg: seg, timeText: timeText, disableDragging: props.disableDragging, disableResizing: props.disableResizing, defaultContent: props.defaultContent || renderInnerContent, isDragging: props.isDragging, isResizing: props.isResizing, isDateSelecting: props.isDateSelecting, isSelected: props.isSelected, isPast: props.isPast, isFuture: props.isFuture, isToday: props.isToday }, function (rootElRef, classNames, innerElRef, innerContent, hookProps) { return (createElement("a", __assign({ className: props.extraClassNames.concat(classNames).join(' '), style: { + borderColor: hookProps.borderColor, + backgroundColor: hookProps.backgroundColor, + }, ref: rootElRef }, getSegAnchorAttrs(seg)), + createElement("div", { className: "fc-event-main", ref: innerElRef, style: { color: hookProps.textColor } }, innerContent), + hookProps.isStartResizable && + createElement("div", { className: "fc-event-resizer fc-event-resizer-start" }), + hookProps.isEndResizable && + createElement("div", { className: "fc-event-resizer fc-event-resizer-end" }))); })); + }; + return StandardEvent; + }(BaseComponent)); + function renderInnerContent(innerProps) { + return (createElement("div", { className: "fc-event-main-frame" }, + innerProps.timeText && (createElement("div", { className: "fc-event-time" }, innerProps.timeText)), + createElement("div", { className: "fc-event-title-container" }, + createElement("div", { className: "fc-event-title fc-sticky" }, innerProps.event.title || createElement(Fragment, null, "\u00A0"))))); + } + function getSegAnchorAttrs(seg) { + var url = seg.eventRange.def.url; + return url ? { href: url } : {}; + } + + var NowIndicatorRoot = function (props) { return (createElement(ViewContextType.Consumer, null, function (context) { + var options = context.options; + var hookProps = { + isAxis: props.isAxis, + date: context.dateEnv.toDate(props.date), + view: context.viewApi, + }; + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.nowIndicatorClassNames, content: options.nowIndicatorContent, didMount: options.nowIndicatorDidMount, willUnmount: options.nowIndicatorWillUnmount }, props.children)); + })); }; + + var DAY_NUM_FORMAT = createFormatter({ day: 'numeric' }); + var DayCellContent = /** @class */ (function (_super) { + __extends(DayCellContent, _super); + function DayCellContent() { + return _super !== null && _super.apply(this, arguments) || this; + } + DayCellContent.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context; + var options = context.options; + var hookProps = refineDayCellHookProps({ + date: props.date, + dateProfile: props.dateProfile, + todayRange: props.todayRange, + showDayNumber: props.showDayNumber, + extraProps: props.extraHookProps, + viewApi: context.viewApi, + dateEnv: context.dateEnv, + }); + return (createElement(ContentHook, { hookProps: hookProps, content: options.dayCellContent, defaultContent: props.defaultContent }, props.children)); + }; + return DayCellContent; + }(BaseComponent)); + function refineDayCellHookProps(raw) { + var date = raw.date, dateEnv = raw.dateEnv; + var dayMeta = getDateMeta(date, raw.todayRange, null, raw.dateProfile); + return __assign(__assign(__assign({ date: dateEnv.toDate(date), view: raw.viewApi }, dayMeta), { dayNumberText: raw.showDayNumber ? dateEnv.format(date, DAY_NUM_FORMAT) : '' }), raw.extraProps); + } + + var DayCellRoot = /** @class */ (function (_super) { + __extends(DayCellRoot, _super); + function DayCellRoot() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.refineHookProps = memoizeObjArg(refineDayCellHookProps); + _this.normalizeClassNames = buildClassNameNormalizer(); + return _this; + } + DayCellRoot.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context; + var options = context.options; + var hookProps = this.refineHookProps({ + date: props.date, + dateProfile: props.dateProfile, + todayRange: props.todayRange, + showDayNumber: props.showDayNumber, + extraProps: props.extraHookProps, + viewApi: context.viewApi, + dateEnv: context.dateEnv, + }); + var classNames = getDayClassNames(hookProps, context.theme).concat(hookProps.isDisabled + ? [] // don't use custom classNames if disabled + : this.normalizeClassNames(options.dayCellClassNames, hookProps)); + var dataAttrs = hookProps.isDisabled ? {} : { + 'data-date': formatDayString(props.date), + }; + return (createElement(MountHook, { hookProps: hookProps, didMount: options.dayCellDidMount, willUnmount: options.dayCellWillUnmount, elRef: props.elRef }, function (rootElRef) { return props.children(rootElRef, classNames, dataAttrs, hookProps.isDisabled); })); + }; + return DayCellRoot; + }(BaseComponent)); + + function renderFill(fillType) { + return (createElement("div", { className: "fc-" + fillType })); + } + var BgEvent = function (props) { return (createElement(EventRoot, { defaultContent: renderInnerContent$1, seg: props.seg /* uselesss i think */, timeText: "", disableDragging: true, disableResizing: true, isDragging: false, isResizing: false, isDateSelecting: false, isSelected: false, isPast: props.isPast, isFuture: props.isFuture, isToday: props.isToday }, function (rootElRef, classNames, innerElRef, innerContent, hookProps) { return (createElement("div", { ref: rootElRef, className: ['fc-bg-event'].concat(classNames).join(' '), style: { + backgroundColor: hookProps.backgroundColor, + } }, innerContent)); })); }; + function renderInnerContent$1(props) { + var title = props.event.title; + return title && (createElement("div", { className: "fc-event-title" }, props.event.title)); + } + + var WeekNumberRoot = function (props) { return (createElement(ViewContextType.Consumer, null, function (context) { + var dateEnv = context.dateEnv, options = context.options; + var date = props.date; + var format = options.weekNumberFormat || props.defaultFormat; + var num = dateEnv.computeWeekNumber(date); // TODO: somehow use for formatting as well? + var text = dateEnv.format(date, format); + var hookProps = { num: num, text: text, date: date }; + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.weekNumberClassNames, content: options.weekNumberContent, defaultContent: renderInner$1, didMount: options.weekNumberDidMount, willUnmount: options.weekNumberWillUnmount }, props.children)); + })); }; + function renderInner$1(innerProps) { + return innerProps.text; + } + + // exports + // -------------------------------------------------------------------------------------------------- + var version = '5.5.0'; // important to type it, so .d.ts has generic string + + var Calendar = /** @class */ (function (_super) { + __extends(Calendar, _super); + function Calendar(el, optionOverrides) { + if (optionOverrides === void 0) { optionOverrides = {}; } + var _this = _super.call(this) || this; + _this.isRendering = false; + _this.isRendered = false; + _this.currentClassNames = []; + _this.customContentRenderId = 0; // will affect custom generated classNames? + _this.handleAction = function (action) { + // actions we know we want to render immediately + switch (action.type) { + case 'SET_EVENT_DRAG': + case 'SET_EVENT_RESIZE': + _this.renderRunner.tryDrain(); + } + }; + _this.handleData = function (data) { + _this.currentData = data; + _this.renderRunner.request(data.calendarOptions.rerenderDelay); + }; + _this.handleRenderRequest = function () { + if (_this.isRendering) { + _this.isRendered = true; + var currentData_1 = _this.currentData; + render(createElement(CalendarRoot, { options: currentData_1.calendarOptions, theme: currentData_1.theme, emitter: currentData_1.emitter }, function (classNames, height, isHeightAuto, forPrint) { + _this.setClassNames(classNames); + _this.setHeight(height); + return (createElement(CustomContentRenderContext.Provider, { value: _this.customContentRenderId }, + createElement(CalendarContent, __assign({ isHeightAuto: isHeightAuto, forPrint: forPrint }, currentData_1)))); + }), _this.el); + } + else if (_this.isRendered) { + _this.isRendered = false; + unmountComponentAtNode$1(_this.el); + _this.setClassNames([]); + _this.setHeight(''); + } + flushToDom$1(); + }; + _this.el = el; + _this.renderRunner = new DelayedRunner(_this.handleRenderRequest); + new CalendarDataManager({ + optionOverrides: optionOverrides, + calendarApi: _this, + onAction: _this.handleAction, + onData: _this.handleData, + }); + return _this; + } + Object.defineProperty(Calendar.prototype, "view", { + get: function () { return this.currentData.viewApi; } // for public API + , + enumerable: false, + configurable: true + }); + Calendar.prototype.render = function () { + var wasRendering = this.isRendering; + if (!wasRendering) { + this.isRendering = true; + } + else { + this.customContentRenderId += 1; + } + this.renderRunner.request(); + if (wasRendering) { + this.updateSize(); + } + }; + Calendar.prototype.destroy = function () { + if (this.isRendering) { + this.isRendering = false; + this.renderRunner.request(); + } + }; + Calendar.prototype.updateSize = function () { + _super.prototype.updateSize.call(this); + flushToDom$1(); + }; + Calendar.prototype.batchRendering = function (func) { + this.renderRunner.pause('batchRendering'); + func(); + this.renderRunner.resume('batchRendering'); + }; + Calendar.prototype.pauseRendering = function () { + this.renderRunner.pause('pauseRendering'); + }; + Calendar.prototype.resumeRendering = function () { + this.renderRunner.resume('pauseRendering', true); + }; + Calendar.prototype.resetOptions = function (optionOverrides, append) { + this.currentDataManager.resetOptions(optionOverrides, append); + }; + Calendar.prototype.setClassNames = function (classNames) { + if (!isArraysEqual(classNames, this.currentClassNames)) { + var classList = this.el.classList; + for (var _i = 0, _a = this.currentClassNames; _i < _a.length; _i++) { + var className = _a[_i]; + classList.remove(className); + } + for (var _b = 0, classNames_1 = classNames; _b < classNames_1.length; _b++) { + var className = classNames_1[_b]; + classList.add(className); + } + this.currentClassNames = classNames; + } + }; + Calendar.prototype.setHeight = function (height) { + applyStyleProp(this.el, 'height', height); + }; + return Calendar; + }(CalendarApi)); + + config.touchMouseIgnoreWait = 500; + var ignoreMouseDepth = 0; + var listenerCnt = 0; + var isWindowTouchMoveCancelled = false; + /* + Uses a "pointer" abstraction, which monitors UI events for both mouse and touch. + Tracks when the pointer "drags" on a certain element, meaning down+move+up. + + Also, tracks if there was touch-scrolling. + Also, can prevent touch-scrolling from happening. + Also, can fire pointermove events when scrolling happens underneath, even when no real pointer movement. + + emits: + - pointerdown + - pointermove + - pointerup + */ + var PointerDragging = /** @class */ (function () { + function PointerDragging(containerEl) { + var _this = this; + this.subjectEl = null; + // options that can be directly assigned by caller + this.selector = ''; // will cause subjectEl in all emitted events to be this element + this.handleSelector = ''; + this.shouldIgnoreMove = false; + this.shouldWatchScroll = true; // for simulating pointermove on scroll + // internal states + this.isDragging = false; + this.isTouchDragging = false; + this.wasTouchScroll = false; + // Mouse + // ---------------------------------------------------------------------------------------------------- + this.handleMouseDown = function (ev) { + if (!_this.shouldIgnoreMouse() && + isPrimaryMouseButton(ev) && + _this.tryStart(ev)) { + var pev = _this.createEventFromMouse(ev, true); + _this.emitter.trigger('pointerdown', pev); + _this.initScrollWatch(pev); + if (!_this.shouldIgnoreMove) { + document.addEventListener('mousemove', _this.handleMouseMove); + } + document.addEventListener('mouseup', _this.handleMouseUp); + } + }; + this.handleMouseMove = function (ev) { + var pev = _this.createEventFromMouse(ev); + _this.recordCoords(pev); + _this.emitter.trigger('pointermove', pev); + }; + this.handleMouseUp = function (ev) { + document.removeEventListener('mousemove', _this.handleMouseMove); + document.removeEventListener('mouseup', _this.handleMouseUp); + _this.emitter.trigger('pointerup', _this.createEventFromMouse(ev)); + _this.cleanup(); // call last so that pointerup has access to props + }; + // Touch + // ---------------------------------------------------------------------------------------------------- + this.handleTouchStart = function (ev) { + if (_this.tryStart(ev)) { + _this.isTouchDragging = true; + var pev = _this.createEventFromTouch(ev, true); + _this.emitter.trigger('pointerdown', pev); + _this.initScrollWatch(pev); + // unlike mouse, need to attach to target, not document + // https://stackoverflow.com/a/45760014 + var targetEl = ev.target; + if (!_this.shouldIgnoreMove) { + targetEl.addEventListener('touchmove', _this.handleTouchMove); + } + targetEl.addEventListener('touchend', _this.handleTouchEnd); + targetEl.addEventListener('touchcancel', _this.handleTouchEnd); // treat it as a touch end + // attach a handler to get called when ANY scroll action happens on the page. + // this was impossible to do with normal on/off because 'scroll' doesn't bubble. + // http://stackoverflow.com/a/32954565/96342 + window.addEventListener('scroll', _this.handleTouchScroll, true); + } + }; + this.handleTouchMove = function (ev) { + var pev = _this.createEventFromTouch(ev); + _this.recordCoords(pev); + _this.emitter.trigger('pointermove', pev); + }; + this.handleTouchEnd = function (ev) { + if (_this.isDragging) { // done to guard against touchend followed by touchcancel + var targetEl = ev.target; + targetEl.removeEventListener('touchmove', _this.handleTouchMove); + targetEl.removeEventListener('touchend', _this.handleTouchEnd); + targetEl.removeEventListener('touchcancel', _this.handleTouchEnd); + window.removeEventListener('scroll', _this.handleTouchScroll, true); // useCaptured=true + _this.emitter.trigger('pointerup', _this.createEventFromTouch(ev)); + _this.cleanup(); // call last so that pointerup has access to props + _this.isTouchDragging = false; + startIgnoringMouse(); + } + }; + this.handleTouchScroll = function () { + _this.wasTouchScroll = true; + }; + this.handleScroll = function (ev) { + if (!_this.shouldIgnoreMove) { + var pageX = (window.pageXOffset - _this.prevScrollX) + _this.prevPageX; + var pageY = (window.pageYOffset - _this.prevScrollY) + _this.prevPageY; + _this.emitter.trigger('pointermove', { + origEvent: ev, + isTouch: _this.isTouchDragging, + subjectEl: _this.subjectEl, + pageX: pageX, + pageY: pageY, + deltaX: pageX - _this.origPageX, + deltaY: pageY - _this.origPageY, + }); + } + }; + this.containerEl = containerEl; + this.emitter = new Emitter(); + containerEl.addEventListener('mousedown', this.handleMouseDown); + containerEl.addEventListener('touchstart', this.handleTouchStart, { passive: true }); + listenerCreated(); + } + PointerDragging.prototype.destroy = function () { + this.containerEl.removeEventListener('mousedown', this.handleMouseDown); + this.containerEl.removeEventListener('touchstart', this.handleTouchStart, { passive: true }); + listenerDestroyed(); + }; + PointerDragging.prototype.tryStart = function (ev) { + var subjectEl = this.querySubjectEl(ev); + var downEl = ev.target; + if (subjectEl && + (!this.handleSelector || elementClosest(downEl, this.handleSelector))) { + this.subjectEl = subjectEl; + this.isDragging = true; // do this first so cancelTouchScroll will work + this.wasTouchScroll = false; + return true; + } + return false; + }; + PointerDragging.prototype.cleanup = function () { + isWindowTouchMoveCancelled = false; + this.isDragging = false; + this.subjectEl = null; + // keep wasTouchScroll around for later access + this.destroyScrollWatch(); + }; + PointerDragging.prototype.querySubjectEl = function (ev) { + if (this.selector) { + return elementClosest(ev.target, this.selector); + } + return this.containerEl; + }; + PointerDragging.prototype.shouldIgnoreMouse = function () { + return ignoreMouseDepth || this.isTouchDragging; + }; + // can be called by user of this class, to cancel touch-based scrolling for the current drag + PointerDragging.prototype.cancelTouchScroll = function () { + if (this.isDragging) { + isWindowTouchMoveCancelled = true; + } + }; + // Scrolling that simulates pointermoves + // ---------------------------------------------------------------------------------------------------- + PointerDragging.prototype.initScrollWatch = function (ev) { + if (this.shouldWatchScroll) { + this.recordCoords(ev); + window.addEventListener('scroll', this.handleScroll, true); // useCapture=true + } + }; + PointerDragging.prototype.recordCoords = function (ev) { + if (this.shouldWatchScroll) { + this.prevPageX = ev.pageX; + this.prevPageY = ev.pageY; + this.prevScrollX = window.pageXOffset; + this.prevScrollY = window.pageYOffset; + } + }; + PointerDragging.prototype.destroyScrollWatch = function () { + if (this.shouldWatchScroll) { + window.removeEventListener('scroll', this.handleScroll, true); // useCaptured=true + } + }; + // Event Normalization + // ---------------------------------------------------------------------------------------------------- + PointerDragging.prototype.createEventFromMouse = function (ev, isFirst) { + var deltaX = 0; + var deltaY = 0; + // TODO: repeat code + if (isFirst) { + this.origPageX = ev.pageX; + this.origPageY = ev.pageY; + } + else { + deltaX = ev.pageX - this.origPageX; + deltaY = ev.pageY - this.origPageY; + } + return { + origEvent: ev, + isTouch: false, + subjectEl: this.subjectEl, + pageX: ev.pageX, + pageY: ev.pageY, + deltaX: deltaX, + deltaY: deltaY, + }; + }; + PointerDragging.prototype.createEventFromTouch = function (ev, isFirst) { + var touches = ev.touches; + var pageX; + var pageY; + var deltaX = 0; + var deltaY = 0; + // if touch coords available, prefer, + // because FF would give bad ev.pageX ev.pageY + if (touches && touches.length) { + pageX = touches[0].pageX; + pageY = touches[0].pageY; + } + else { + pageX = ev.pageX; + pageY = ev.pageY; + } + // TODO: repeat code + if (isFirst) { + this.origPageX = pageX; + this.origPageY = pageY; + } + else { + deltaX = pageX - this.origPageX; + deltaY = pageY - this.origPageY; + } + return { + origEvent: ev, + isTouch: true, + subjectEl: this.subjectEl, + pageX: pageX, + pageY: pageY, + deltaX: deltaX, + deltaY: deltaY, + }; + }; + return PointerDragging; + }()); + // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) + function isPrimaryMouseButton(ev) { + return ev.button === 0 && !ev.ctrlKey; + } + // Ignoring fake mouse events generated by touch + // ---------------------------------------------------------------------------------------------------- + function startIgnoringMouse() { + ignoreMouseDepth += 1; + setTimeout(function () { + ignoreMouseDepth -= 1; + }, config.touchMouseIgnoreWait); + } + // We want to attach touchmove as early as possible for Safari + // ---------------------------------------------------------------------------------------------------- + function listenerCreated() { + listenerCnt += 1; + if (listenerCnt === 1) { + window.addEventListener('touchmove', onWindowTouchMove, { passive: false }); + } + } + function listenerDestroyed() { + listenerCnt -= 1; + if (!listenerCnt) { + window.removeEventListener('touchmove', onWindowTouchMove, { passive: false }); + } + } + function onWindowTouchMove(ev) { + if (isWindowTouchMoveCancelled) { + ev.preventDefault(); + } + } + + /* + An effect in which an element follows the movement of a pointer across the screen. + The moving element is a clone of some other element. + Must call start + handleMove + stop. + */ + var ElementMirror = /** @class */ (function () { + function ElementMirror() { + this.isVisible = false; // must be explicitly enabled + this.sourceEl = null; + this.mirrorEl = null; + this.sourceElRect = null; // screen coords relative to viewport + // options that can be set directly by caller + this.parentNode = document.body; + this.zIndex = 9999; + this.revertDuration = 0; + } + ElementMirror.prototype.start = function (sourceEl, pageX, pageY) { + this.sourceEl = sourceEl; + this.sourceElRect = this.sourceEl.getBoundingClientRect(); + this.origScreenX = pageX - window.pageXOffset; + this.origScreenY = pageY - window.pageYOffset; + this.deltaX = 0; + this.deltaY = 0; + this.updateElPosition(); + }; + ElementMirror.prototype.handleMove = function (pageX, pageY) { + this.deltaX = (pageX - window.pageXOffset) - this.origScreenX; + this.deltaY = (pageY - window.pageYOffset) - this.origScreenY; + this.updateElPosition(); + }; + // can be called before start + ElementMirror.prototype.setIsVisible = function (bool) { + if (bool) { + if (!this.isVisible) { + if (this.mirrorEl) { + this.mirrorEl.style.display = ''; + } + this.isVisible = bool; // needs to happen before updateElPosition + this.updateElPosition(); // because was not updating the position while invisible + } + } + else if (this.isVisible) { + if (this.mirrorEl) { + this.mirrorEl.style.display = 'none'; + } + this.isVisible = bool; + } + }; + // always async + ElementMirror.prototype.stop = function (needsRevertAnimation, callback) { + var _this = this; + var done = function () { + _this.cleanup(); + callback(); + }; + if (needsRevertAnimation && + this.mirrorEl && + this.isVisible && + this.revertDuration && // if 0, transition won't work + (this.deltaX || this.deltaY) // if same coords, transition won't work + ) { + this.doRevertAnimation(done, this.revertDuration); + } + else { + setTimeout(done, 0); + } + }; + ElementMirror.prototype.doRevertAnimation = function (callback, revertDuration) { + var mirrorEl = this.mirrorEl; + var finalSourceElRect = this.sourceEl.getBoundingClientRect(); // because autoscrolling might have happened + mirrorEl.style.transition = + 'top ' + revertDuration + 'ms,' + + 'left ' + revertDuration + 'ms'; + applyStyle(mirrorEl, { + left: finalSourceElRect.left, + top: finalSourceElRect.top, + }); + whenTransitionDone(mirrorEl, function () { + mirrorEl.style.transition = ''; + callback(); + }); + }; + ElementMirror.prototype.cleanup = function () { + if (this.mirrorEl) { + removeElement(this.mirrorEl); + this.mirrorEl = null; + } + this.sourceEl = null; + }; + ElementMirror.prototype.updateElPosition = function () { + if (this.sourceEl && this.isVisible) { + applyStyle(this.getMirrorEl(), { + left: this.sourceElRect.left + this.deltaX, + top: this.sourceElRect.top + this.deltaY, + }); + } + }; + ElementMirror.prototype.getMirrorEl = function () { + var sourceElRect = this.sourceElRect; + var mirrorEl = this.mirrorEl; + if (!mirrorEl) { + mirrorEl = this.mirrorEl = this.sourceEl.cloneNode(true); // cloneChildren=true + // we don't want long taps or any mouse interaction causing selection/menus. + // would use preventSelection(), but that prevents selectstart, causing problems. + mirrorEl.classList.add('fc-unselectable'); + mirrorEl.classList.add('fc-event-dragging'); + applyStyle(mirrorEl, { + position: 'fixed', + zIndex: this.zIndex, + visibility: '', + boxSizing: 'border-box', + width: sourceElRect.right - sourceElRect.left, + height: sourceElRect.bottom - sourceElRect.top, + right: 'auto', + bottom: 'auto', + margin: 0, + }); + this.parentNode.appendChild(mirrorEl); + } + return mirrorEl; + }; + return ElementMirror; + }()); + + /* + Is a cache for a given element's scroll information (all the info that ScrollController stores) + in addition the "client rectangle" of the element.. the area within the scrollbars. + + The cache can be in one of two modes: + - doesListening:false - ignores when the container is scrolled by someone else + - doesListening:true - watch for scrolling and update the cache + */ + var ScrollGeomCache = /** @class */ (function (_super) { + __extends(ScrollGeomCache, _super); + function ScrollGeomCache(scrollController, doesListening) { + var _this = _super.call(this) || this; + _this.handleScroll = function () { + _this.scrollTop = _this.scrollController.getScrollTop(); + _this.scrollLeft = _this.scrollController.getScrollLeft(); + _this.handleScrollChange(); + }; + _this.scrollController = scrollController; + _this.doesListening = doesListening; + _this.scrollTop = _this.origScrollTop = scrollController.getScrollTop(); + _this.scrollLeft = _this.origScrollLeft = scrollController.getScrollLeft(); + _this.scrollWidth = scrollController.getScrollWidth(); + _this.scrollHeight = scrollController.getScrollHeight(); + _this.clientWidth = scrollController.getClientWidth(); + _this.clientHeight = scrollController.getClientHeight(); + _this.clientRect = _this.computeClientRect(); // do last in case it needs cached values + if (_this.doesListening) { + _this.getEventTarget().addEventListener('scroll', _this.handleScroll); + } + return _this; + } + ScrollGeomCache.prototype.destroy = function () { + if (this.doesListening) { + this.getEventTarget().removeEventListener('scroll', this.handleScroll); + } + }; + ScrollGeomCache.prototype.getScrollTop = function () { + return this.scrollTop; + }; + ScrollGeomCache.prototype.getScrollLeft = function () { + return this.scrollLeft; + }; + ScrollGeomCache.prototype.setScrollTop = function (top) { + this.scrollController.setScrollTop(top); + if (!this.doesListening) { + // we are not relying on the element to normalize out-of-bounds scroll values + // so we need to sanitize ourselves + this.scrollTop = Math.max(Math.min(top, this.getMaxScrollTop()), 0); + this.handleScrollChange(); + } + }; + ScrollGeomCache.prototype.setScrollLeft = function (top) { + this.scrollController.setScrollLeft(top); + if (!this.doesListening) { + // we are not relying on the element to normalize out-of-bounds scroll values + // so we need to sanitize ourselves + this.scrollLeft = Math.max(Math.min(top, this.getMaxScrollLeft()), 0); + this.handleScrollChange(); + } + }; + ScrollGeomCache.prototype.getClientWidth = function () { + return this.clientWidth; + }; + ScrollGeomCache.prototype.getClientHeight = function () { + return this.clientHeight; + }; + ScrollGeomCache.prototype.getScrollWidth = function () { + return this.scrollWidth; + }; + ScrollGeomCache.prototype.getScrollHeight = function () { + return this.scrollHeight; + }; + ScrollGeomCache.prototype.handleScrollChange = function () { + }; + return ScrollGeomCache; + }(ScrollController)); + + var ElementScrollGeomCache = /** @class */ (function (_super) { + __extends(ElementScrollGeomCache, _super); + function ElementScrollGeomCache(el, doesListening) { + return _super.call(this, new ElementScrollController(el), doesListening) || this; + } + ElementScrollGeomCache.prototype.getEventTarget = function () { + return this.scrollController.el; + }; + ElementScrollGeomCache.prototype.computeClientRect = function () { + return computeInnerRect(this.scrollController.el); + }; + return ElementScrollGeomCache; + }(ScrollGeomCache)); + + var WindowScrollGeomCache = /** @class */ (function (_super) { + __extends(WindowScrollGeomCache, _super); + function WindowScrollGeomCache(doesListening) { + return _super.call(this, new WindowScrollController(), doesListening) || this; + } + WindowScrollGeomCache.prototype.getEventTarget = function () { + return window; + }; + WindowScrollGeomCache.prototype.computeClientRect = function () { + return { + left: this.scrollLeft, + right: this.scrollLeft + this.clientWidth, + top: this.scrollTop, + bottom: this.scrollTop + this.clientHeight, + }; + }; + // the window is the only scroll object that changes it's rectangle relative + // to the document's topleft as it scrolls + WindowScrollGeomCache.prototype.handleScrollChange = function () { + this.clientRect = this.computeClientRect(); + }; + return WindowScrollGeomCache; + }(ScrollGeomCache)); + + // If available we are using native "performance" API instead of "Date" + // Read more about it on MDN: + // https://developer.mozilla.org/en-US/docs/Web/API/Performance + var getTime = typeof performance === 'function' ? performance.now : Date.now; + /* + For a pointer interaction, automatically scrolls certain scroll containers when the pointer + approaches the edge. + + The caller must call start + handleMove + stop. + */ + var AutoScroller = /** @class */ (function () { + function AutoScroller() { + var _this = this; + // options that can be set by caller + this.isEnabled = true; + this.scrollQuery = [window, '.fc-scroller']; + this.edgeThreshold = 50; // pixels + this.maxVelocity = 300; // pixels per second + // internal state + this.pointerScreenX = null; + this.pointerScreenY = null; + this.isAnimating = false; + this.scrollCaches = null; + // protect against the initial pointerdown being too close to an edge and starting the scroll + this.everMovedUp = false; + this.everMovedDown = false; + this.everMovedLeft = false; + this.everMovedRight = false; + this.animate = function () { + if (_this.isAnimating) { // wasn't cancelled between animation calls + var edge = _this.computeBestEdge(_this.pointerScreenX + window.pageXOffset, _this.pointerScreenY + window.pageYOffset); + if (edge) { + var now = getTime(); + _this.handleSide(edge, (now - _this.msSinceRequest) / 1000); + _this.requestAnimation(now); + } + else { + _this.isAnimating = false; // will stop animation + } + } + }; + } + AutoScroller.prototype.start = function (pageX, pageY) { + if (this.isEnabled) { + this.scrollCaches = this.buildCaches(); + this.pointerScreenX = null; + this.pointerScreenY = null; + this.everMovedUp = false; + this.everMovedDown = false; + this.everMovedLeft = false; + this.everMovedRight = false; + this.handleMove(pageX, pageY); + } + }; + AutoScroller.prototype.handleMove = function (pageX, pageY) { + if (this.isEnabled) { + var pointerScreenX = pageX - window.pageXOffset; + var pointerScreenY = pageY - window.pageYOffset; + var yDelta = this.pointerScreenY === null ? 0 : pointerScreenY - this.pointerScreenY; + var xDelta = this.pointerScreenX === null ? 0 : pointerScreenX - this.pointerScreenX; + if (yDelta < 0) { + this.everMovedUp = true; + } + else if (yDelta > 0) { + this.everMovedDown = true; + } + if (xDelta < 0) { + this.everMovedLeft = true; + } + else if (xDelta > 0) { + this.everMovedRight = true; + } + this.pointerScreenX = pointerScreenX; + this.pointerScreenY = pointerScreenY; + if (!this.isAnimating) { + this.isAnimating = true; + this.requestAnimation(getTime()); + } + } + }; + AutoScroller.prototype.stop = function () { + if (this.isEnabled) { + this.isAnimating = false; // will stop animation + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + scrollCache.destroy(); + } + this.scrollCaches = null; + } + }; + AutoScroller.prototype.requestAnimation = function (now) { + this.msSinceRequest = now; + requestAnimationFrame(this.animate); + }; + AutoScroller.prototype.handleSide = function (edge, seconds) { + var scrollCache = edge.scrollCache; + var edgeThreshold = this.edgeThreshold; + var invDistance = edgeThreshold - edge.distance; + var velocity = // the closer to the edge, the faster we scroll + ((invDistance * invDistance) / (edgeThreshold * edgeThreshold)) * // quadratic + this.maxVelocity * seconds; + var sign = 1; + switch (edge.name) { + case 'left': + sign = -1; + // falls through + case 'right': + scrollCache.setScrollLeft(scrollCache.getScrollLeft() + velocity * sign); + break; + case 'top': + sign = -1; + // falls through + case 'bottom': + scrollCache.setScrollTop(scrollCache.getScrollTop() + velocity * sign); + break; + } + }; + // left/top are relative to document topleft + AutoScroller.prototype.computeBestEdge = function (left, top) { + var edgeThreshold = this.edgeThreshold; + var bestSide = null; + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + var rect = scrollCache.clientRect; + var leftDist = left - rect.left; + var rightDist = rect.right - left; + var topDist = top - rect.top; + var bottomDist = rect.bottom - top; + // completely within the rect? + if (leftDist >= 0 && rightDist >= 0 && topDist >= 0 && bottomDist >= 0) { + if (topDist <= edgeThreshold && this.everMovedUp && scrollCache.canScrollUp() && + (!bestSide || bestSide.distance > topDist)) { + bestSide = { scrollCache: scrollCache, name: 'top', distance: topDist }; + } + if (bottomDist <= edgeThreshold && this.everMovedDown && scrollCache.canScrollDown() && + (!bestSide || bestSide.distance > bottomDist)) { + bestSide = { scrollCache: scrollCache, name: 'bottom', distance: bottomDist }; + } + if (leftDist <= edgeThreshold && this.everMovedLeft && scrollCache.canScrollLeft() && + (!bestSide || bestSide.distance > leftDist)) { + bestSide = { scrollCache: scrollCache, name: 'left', distance: leftDist }; + } + if (rightDist <= edgeThreshold && this.everMovedRight && scrollCache.canScrollRight() && + (!bestSide || bestSide.distance > rightDist)) { + bestSide = { scrollCache: scrollCache, name: 'right', distance: rightDist }; + } + } + } + return bestSide; + }; + AutoScroller.prototype.buildCaches = function () { + return this.queryScrollEls().map(function (el) { + if (el === window) { + return new WindowScrollGeomCache(false); // false = don't listen to user-generated scrolls + } + return new ElementScrollGeomCache(el, false); // false = don't listen to user-generated scrolls + }); + }; + AutoScroller.prototype.queryScrollEls = function () { + var els = []; + for (var _i = 0, _a = this.scrollQuery; _i < _a.length; _i++) { + var query = _a[_i]; + if (typeof query === 'object') { + els.push(query); + } + else { + els.push.apply(els, Array.prototype.slice.call(document.querySelectorAll(query))); + } + } + return els; + }; + return AutoScroller; + }()); + + /* + Monitors dragging on an element. Has a number of high-level features: + - minimum distance required before dragging + - minimum wait time ("delay") before dragging + - a mirror element that follows the pointer + */ + var FeaturefulElementDragging = /** @class */ (function (_super) { + __extends(FeaturefulElementDragging, _super); + function FeaturefulElementDragging(containerEl, selector) { + var _this = _super.call(this, containerEl) || this; + // options that can be directly set by caller + // the caller can also set the PointerDragging's options as well + _this.delay = null; + _this.minDistance = 0; + _this.touchScrollAllowed = true; // prevents drag from starting and blocks scrolling during drag + _this.mirrorNeedsRevert = false; + _this.isInteracting = false; // is the user validly moving the pointer? lasts until pointerup + _this.isDragging = false; // is it INTENTFULLY dragging? lasts until after revert animation + _this.isDelayEnded = false; + _this.isDistanceSurpassed = false; + _this.delayTimeoutId = null; + _this.onPointerDown = function (ev) { + if (!_this.isDragging) { // so new drag doesn't happen while revert animation is going + _this.isInteracting = true; + _this.isDelayEnded = false; + _this.isDistanceSurpassed = false; + preventSelection(document.body); + preventContextMenu(document.body); + // prevent links from being visited if there's an eventual drag. + // also prevents selection in older browsers (maybe?). + // not necessary for touch, besides, browser would complain about passiveness. + if (!ev.isTouch) { + ev.origEvent.preventDefault(); + } + _this.emitter.trigger('pointerdown', ev); + if (_this.isInteracting && // not destroyed via pointerdown handler + !_this.pointer.shouldIgnoreMove) { + // actions related to initiating dragstart+dragmove+dragend... + _this.mirror.setIsVisible(false); // reset. caller must set-visible + _this.mirror.start(ev.subjectEl, ev.pageX, ev.pageY); // must happen on first pointer down + _this.startDelay(ev); + if (!_this.minDistance) { + _this.handleDistanceSurpassed(ev); + } + } + } + }; + _this.onPointerMove = function (ev) { + if (_this.isInteracting) { + _this.emitter.trigger('pointermove', ev); + if (!_this.isDistanceSurpassed) { + var minDistance = _this.minDistance; + var distanceSq = void 0; // current distance from the origin, squared + var deltaX = ev.deltaX, deltaY = ev.deltaY; + distanceSq = deltaX * deltaX + deltaY * deltaY; + if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem + _this.handleDistanceSurpassed(ev); + } + } + if (_this.isDragging) { + // a real pointer move? (not one simulated by scrolling) + if (ev.origEvent.type !== 'scroll') { + _this.mirror.handleMove(ev.pageX, ev.pageY); + _this.autoScroller.handleMove(ev.pageX, ev.pageY); + } + _this.emitter.trigger('dragmove', ev); + } + } + }; + _this.onPointerUp = function (ev) { + if (_this.isInteracting) { + _this.isInteracting = false; + allowSelection(document.body); + allowContextMenu(document.body); + _this.emitter.trigger('pointerup', ev); // can potentially set mirrorNeedsRevert + if (_this.isDragging) { + _this.autoScroller.stop(); + _this.tryStopDrag(ev); // which will stop the mirror + } + if (_this.delayTimeoutId) { + clearTimeout(_this.delayTimeoutId); + _this.delayTimeoutId = null; + } + } + }; + var pointer = _this.pointer = new PointerDragging(containerEl); + pointer.emitter.on('pointerdown', _this.onPointerDown); + pointer.emitter.on('pointermove', _this.onPointerMove); + pointer.emitter.on('pointerup', _this.onPointerUp); + if (selector) { + pointer.selector = selector; + } + _this.mirror = new ElementMirror(); + _this.autoScroller = new AutoScroller(); + return _this; + } + FeaturefulElementDragging.prototype.destroy = function () { + this.pointer.destroy(); + // HACK: simulate a pointer-up to end the current drag + // TODO: fire 'dragend' directly and stop interaction. discourage use of pointerup event (b/c might not fire) + this.onPointerUp({}); + }; + FeaturefulElementDragging.prototype.startDelay = function (ev) { + var _this = this; + if (typeof this.delay === 'number') { + this.delayTimeoutId = setTimeout(function () { + _this.delayTimeoutId = null; + _this.handleDelayEnd(ev); + }, this.delay); // not assignable to number! + } + else { + this.handleDelayEnd(ev); + } + }; + FeaturefulElementDragging.prototype.handleDelayEnd = function (ev) { + this.isDelayEnded = true; + this.tryStartDrag(ev); + }; + FeaturefulElementDragging.prototype.handleDistanceSurpassed = function (ev) { + this.isDistanceSurpassed = true; + this.tryStartDrag(ev); + }; + FeaturefulElementDragging.prototype.tryStartDrag = function (ev) { + if (this.isDelayEnded && this.isDistanceSurpassed) { + if (!this.pointer.wasTouchScroll || this.touchScrollAllowed) { + this.isDragging = true; + this.mirrorNeedsRevert = false; + this.autoScroller.start(ev.pageX, ev.pageY); + this.emitter.trigger('dragstart', ev); + if (this.touchScrollAllowed === false) { + this.pointer.cancelTouchScroll(); + } + } + } + }; + FeaturefulElementDragging.prototype.tryStopDrag = function (ev) { + // .stop() is ALWAYS asynchronous, which we NEED because we want all pointerup events + // that come from the document to fire beforehand. much more convenient this way. + this.mirror.stop(this.mirrorNeedsRevert, this.stopDrag.bind(this, ev)); + }; + FeaturefulElementDragging.prototype.stopDrag = function (ev) { + this.isDragging = false; + this.emitter.trigger('dragend', ev); + }; + // fill in the implementations... + FeaturefulElementDragging.prototype.setIgnoreMove = function (bool) { + this.pointer.shouldIgnoreMove = bool; + }; + FeaturefulElementDragging.prototype.setMirrorIsVisible = function (bool) { + this.mirror.setIsVisible(bool); + }; + FeaturefulElementDragging.prototype.setMirrorNeedsRevert = function (bool) { + this.mirrorNeedsRevert = bool; + }; + FeaturefulElementDragging.prototype.setAutoScrollEnabled = function (bool) { + this.autoScroller.isEnabled = bool; + }; + return FeaturefulElementDragging; + }(ElementDragging)); + + /* + When this class is instantiated, it records the offset of an element (relative to the document topleft), + and continues to monitor scrolling, updating the cached coordinates if it needs to. + Does not access the DOM after instantiation, so highly performant. + + Also keeps track of all scrolling/overflow:hidden containers that are parents of the given element + and an determine if a given point is inside the combined clipping rectangle. + */ + var OffsetTracker = /** @class */ (function () { + function OffsetTracker(el) { + this.origRect = computeRect(el); + // will work fine for divs that have overflow:hidden + this.scrollCaches = getClippingParents(el).map(function (scrollEl) { return new ElementScrollGeomCache(scrollEl, true); }); + } + OffsetTracker.prototype.destroy = function () { + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + scrollCache.destroy(); + } + }; + OffsetTracker.prototype.computeLeft = function () { + var left = this.origRect.left; + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + left += scrollCache.origScrollLeft - scrollCache.getScrollLeft(); + } + return left; + }; + OffsetTracker.prototype.computeTop = function () { + var top = this.origRect.top; + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + top += scrollCache.origScrollTop - scrollCache.getScrollTop(); + } + return top; + }; + OffsetTracker.prototype.isWithinClipping = function (pageX, pageY) { + var point = { left: pageX, top: pageY }; + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + if (!isIgnoredClipping(scrollCache.getEventTarget()) && + !pointInsideRect(point, scrollCache.clientRect)) { + return false; + } + } + return true; + }; + return OffsetTracker; + }()); + // certain clipping containers should never constrain interactions, like and + // https://github.com/fullcalendar/fullcalendar/issues/3615 + function isIgnoredClipping(node) { + var tagName = node.tagName; + return tagName === 'HTML' || tagName === 'BODY'; + } + + /* + Tracks movement over multiple droppable areas (aka "hits") + that exist in one or more DateComponents. + Relies on an existing draggable. + + emits: + - pointerdown + - dragstart + - hitchange - fires initially, even if not over a hit + - pointerup + - (hitchange - again, to null, if ended over a hit) + - dragend + */ + var HitDragging = /** @class */ (function () { + function HitDragging(dragging, droppableStore) { + var _this = this; + // options that can be set by caller + this.useSubjectCenter = false; + this.requireInitial = true; // if doesn't start out on a hit, won't emit any events + this.initialHit = null; + this.movingHit = null; + this.finalHit = null; // won't ever be populated if shouldIgnoreMove + this.handlePointerDown = function (ev) { + var dragging = _this.dragging; + _this.initialHit = null; + _this.movingHit = null; + _this.finalHit = null; + _this.prepareHits(); + _this.processFirstCoord(ev); + if (_this.initialHit || !_this.requireInitial) { + dragging.setIgnoreMove(false); + // TODO: fire this before computing processFirstCoord, so listeners can cancel. this gets fired by almost every handler :( + _this.emitter.trigger('pointerdown', ev); + } + else { + dragging.setIgnoreMove(true); + } + }; + this.handleDragStart = function (ev) { + _this.emitter.trigger('dragstart', ev); + _this.handleMove(ev, true); // force = fire even if initially null + }; + this.handleDragMove = function (ev) { + _this.emitter.trigger('dragmove', ev); + _this.handleMove(ev); + }; + this.handlePointerUp = function (ev) { + _this.releaseHits(); + _this.emitter.trigger('pointerup', ev); + }; + this.handleDragEnd = function (ev) { + if (_this.movingHit) { + _this.emitter.trigger('hitupdate', null, true, ev); + } + _this.finalHit = _this.movingHit; + _this.movingHit = null; + _this.emitter.trigger('dragend', ev); + }; + this.droppableStore = droppableStore; + dragging.emitter.on('pointerdown', this.handlePointerDown); + dragging.emitter.on('dragstart', this.handleDragStart); + dragging.emitter.on('dragmove', this.handleDragMove); + dragging.emitter.on('pointerup', this.handlePointerUp); + dragging.emitter.on('dragend', this.handleDragEnd); + this.dragging = dragging; + this.emitter = new Emitter(); + } + // sets initialHit + // sets coordAdjust + HitDragging.prototype.processFirstCoord = function (ev) { + var origPoint = { left: ev.pageX, top: ev.pageY }; + var adjustedPoint = origPoint; + var subjectEl = ev.subjectEl; + var subjectRect; + if (subjectEl !== document) { + subjectRect = computeRect(subjectEl); + adjustedPoint = constrainPoint(adjustedPoint, subjectRect); + } + var initialHit = this.initialHit = this.queryHitForOffset(adjustedPoint.left, adjustedPoint.top); + if (initialHit) { + if (this.useSubjectCenter && subjectRect) { + var slicedSubjectRect = intersectRects(subjectRect, initialHit.rect); + if (slicedSubjectRect) { + adjustedPoint = getRectCenter(slicedSubjectRect); + } + } + this.coordAdjust = diffPoints(adjustedPoint, origPoint); + } + else { + this.coordAdjust = { left: 0, top: 0 }; + } + }; + HitDragging.prototype.handleMove = function (ev, forceHandle) { + var hit = this.queryHitForOffset(ev.pageX + this.coordAdjust.left, ev.pageY + this.coordAdjust.top); + if (forceHandle || !isHitsEqual(this.movingHit, hit)) { + this.movingHit = hit; + this.emitter.trigger('hitupdate', hit, false, ev); + } + }; + HitDragging.prototype.prepareHits = function () { + this.offsetTrackers = mapHash(this.droppableStore, function (interactionSettings) { + interactionSettings.component.prepareHits(); + return new OffsetTracker(interactionSettings.el); + }); + }; + HitDragging.prototype.releaseHits = function () { + var offsetTrackers = this.offsetTrackers; + for (var id in offsetTrackers) { + offsetTrackers[id].destroy(); + } + this.offsetTrackers = {}; + }; + HitDragging.prototype.queryHitForOffset = function (offsetLeft, offsetTop) { + var _a = this, droppableStore = _a.droppableStore, offsetTrackers = _a.offsetTrackers; + var bestHit = null; + for (var id in droppableStore) { + var component = droppableStore[id].component; + var offsetTracker = offsetTrackers[id]; + if (offsetTracker && // wasn't destroyed mid-drag + offsetTracker.isWithinClipping(offsetLeft, offsetTop)) { + var originLeft = offsetTracker.computeLeft(); + var originTop = offsetTracker.computeTop(); + var positionLeft = offsetLeft - originLeft; + var positionTop = offsetTop - originTop; + var origRect = offsetTracker.origRect; + var width = origRect.right - origRect.left; + var height = origRect.bottom - origRect.top; + if ( + // must be within the element's bounds + positionLeft >= 0 && positionLeft < width && + positionTop >= 0 && positionTop < height) { + var hit = component.queryHit(positionLeft, positionTop, width, height); + var dateProfile = component.context.getCurrentData().dateProfile; + if (hit && + ( + // make sure the hit is within activeRange, meaning it's not a deal cell + rangeContainsRange(dateProfile.activeRange, hit.dateSpan.range)) && + (!bestHit || hit.layer > bestHit.layer)) { + // TODO: better way to re-orient rectangle + hit.rect.left += originLeft; + hit.rect.right += originLeft; + hit.rect.top += originTop; + hit.rect.bottom += originTop; + bestHit = hit; + } + } + } + } + return bestHit; + }; + return HitDragging; + }()); + function isHitsEqual(hit0, hit1) { + if (!hit0 && !hit1) { + return true; + } + if (Boolean(hit0) !== Boolean(hit1)) { + return false; + } + return isDateSpansEqual(hit0.dateSpan, hit1.dateSpan); + } + + function buildDatePointApiWithContext(dateSpan, context) { + var props = {}; + for (var _i = 0, _a = context.pluginHooks.datePointTransforms; _i < _a.length; _i++) { + var transform = _a[_i]; + __assign(props, transform(dateSpan, context)); + } + __assign(props, buildDatePointApi(dateSpan, context.dateEnv)); + return props; + } + function buildDatePointApi(span, dateEnv) { + return { + date: dateEnv.toDate(span.range.start), + dateStr: dateEnv.formatIso(span.range.start, { omitTime: span.allDay }), + allDay: span.allDay, + }; + } + + /* + Monitors when the user clicks on a specific date/time of a component. + A pointerdown+pointerup on the same "hit" constitutes a click. + */ + var DateClicking = /** @class */ (function (_super) { + __extends(DateClicking, _super); + function DateClicking(settings) { + var _this = _super.call(this, settings) || this; + _this.handlePointerDown = function (pev) { + var dragging = _this.dragging; + var downEl = pev.origEvent.target; + // do this in pointerdown (not dragend) because DOM might be mutated by the time dragend is fired + dragging.setIgnoreMove(!_this.component.isValidDateDownEl(downEl)); + }; + // won't even fire if moving was ignored + _this.handleDragEnd = function (ev) { + var component = _this.component; + var pointer = _this.dragging.pointer; + if (!pointer.wasTouchScroll) { + var _a = _this.hitDragging, initialHit = _a.initialHit, finalHit = _a.finalHit; + if (initialHit && finalHit && isHitsEqual(initialHit, finalHit)) { + var context = component.context; + var arg = __assign(__assign({}, buildDatePointApiWithContext(initialHit.dateSpan, context)), { dayEl: initialHit.dayEl, jsEvent: ev.origEvent, view: context.viewApi || context.calendarApi.view }); + context.emitter.trigger('dateClick', arg); + } + } + }; + // we DO want to watch pointer moves because otherwise finalHit won't get populated + _this.dragging = new FeaturefulElementDragging(settings.el); + _this.dragging.autoScroller.isEnabled = false; + var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, interactionSettingsToStore(settings)); + hitDragging.emitter.on('pointerdown', _this.handlePointerDown); + hitDragging.emitter.on('dragend', _this.handleDragEnd); + return _this; + } + DateClicking.prototype.destroy = function () { + this.dragging.destroy(); + }; + return DateClicking; + }(Interaction)); + + /* + Tracks when the user selects a portion of time of a component, + constituted by a drag over date cells, with a possible delay at the beginning of the drag. + */ + var DateSelecting = /** @class */ (function (_super) { + __extends(DateSelecting, _super); + function DateSelecting(settings) { + var _this = _super.call(this, settings) || this; + _this.dragSelection = null; + _this.handlePointerDown = function (ev) { + var _a = _this, component = _a.component, dragging = _a.dragging; + var options = component.context.options; + var canSelect = options.selectable && + component.isValidDateDownEl(ev.origEvent.target); + // don't bother to watch expensive moves if component won't do selection + dragging.setIgnoreMove(!canSelect); + // if touch, require user to hold down + dragging.delay = ev.isTouch ? getComponentTouchDelay(component) : null; + }; + _this.handleDragStart = function (ev) { + _this.component.context.calendarApi.unselect(ev); // unselect previous selections + }; + _this.handleHitUpdate = function (hit, isFinal) { + var context = _this.component.context; + var dragSelection = null; + var isInvalid = false; + if (hit) { + dragSelection = joinHitsIntoSelection(_this.hitDragging.initialHit, hit, context.pluginHooks.dateSelectionTransformers); + if (!dragSelection || !_this.component.isDateSelectionValid(dragSelection)) { + isInvalid = true; + dragSelection = null; + } + } + if (dragSelection) { + context.dispatch({ type: 'SELECT_DATES', selection: dragSelection }); + } + else if (!isFinal) { // only unselect if moved away while dragging + context.dispatch({ type: 'UNSELECT_DATES' }); + } + if (!isInvalid) { + enableCursor(); + } + else { + disableCursor(); + } + if (!isFinal) { + _this.dragSelection = dragSelection; // only clear if moved away from all hits while dragging + } + }; + _this.handlePointerUp = function (pev) { + if (_this.dragSelection) { + // selection is already rendered, so just need to report selection + triggerDateSelect(_this.dragSelection, pev, _this.component.context); + _this.dragSelection = null; + } + }; + var component = settings.component; + var options = component.context.options; + var dragging = _this.dragging = new FeaturefulElementDragging(settings.el); + dragging.touchScrollAllowed = false; + dragging.minDistance = options.selectMinDistance || 0; + dragging.autoScroller.isEnabled = options.dragScroll; + var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, interactionSettingsToStore(settings)); + hitDragging.emitter.on('pointerdown', _this.handlePointerDown); + hitDragging.emitter.on('dragstart', _this.handleDragStart); + hitDragging.emitter.on('hitupdate', _this.handleHitUpdate); + hitDragging.emitter.on('pointerup', _this.handlePointerUp); + return _this; + } + DateSelecting.prototype.destroy = function () { + this.dragging.destroy(); + }; + return DateSelecting; + }(Interaction)); + function getComponentTouchDelay(component) { + var options = component.context.options; + var delay = options.selectLongPressDelay; + if (delay == null) { + delay = options.longPressDelay; + } + return delay; + } + function joinHitsIntoSelection(hit0, hit1, dateSelectionTransformers) { + var dateSpan0 = hit0.dateSpan; + var dateSpan1 = hit1.dateSpan; + var ms = [ + dateSpan0.range.start, + dateSpan0.range.end, + dateSpan1.range.start, + dateSpan1.range.end, + ]; + ms.sort(compareNumbers); + var props = {}; + for (var _i = 0, dateSelectionTransformers_1 = dateSelectionTransformers; _i < dateSelectionTransformers_1.length; _i++) { + var transformer = dateSelectionTransformers_1[_i]; + var res = transformer(hit0, hit1); + if (res === false) { + return null; + } + if (res) { + __assign(props, res); + } + } + props.range = { start: ms[0], end: ms[3] }; + props.allDay = dateSpan0.allDay; + return props; + } + + var EventDragging = /** @class */ (function (_super) { + __extends(EventDragging, _super); + function EventDragging(settings) { + var _this = _super.call(this, settings) || this; + // internal state + _this.subjectEl = null; + _this.subjectSeg = null; // the seg being selected/dragged + _this.isDragging = false; + _this.eventRange = null; + _this.relevantEvents = null; // the events being dragged + _this.receivingContext = null; + _this.validMutation = null; + _this.mutatedRelevantEvents = null; + _this.handlePointerDown = function (ev) { + var origTarget = ev.origEvent.target; + var _a = _this, component = _a.component, dragging = _a.dragging; + var mirror = dragging.mirror; + var options = component.context.options; + var initialContext = component.context; + _this.subjectEl = ev.subjectEl; + var subjectSeg = _this.subjectSeg = getElSeg(ev.subjectEl); + var eventRange = _this.eventRange = subjectSeg.eventRange; + var eventInstanceId = eventRange.instance.instanceId; + _this.relevantEvents = getRelevantEvents(initialContext.getCurrentData().eventStore, eventInstanceId); + dragging.minDistance = ev.isTouch ? 0 : options.eventDragMinDistance; + dragging.delay = + // only do a touch delay if touch and this event hasn't been selected yet + (ev.isTouch && eventInstanceId !== component.props.eventSelection) ? + getComponentTouchDelay$1(component) : + null; + if (options.fixedMirrorParent) { + mirror.parentNode = options.fixedMirrorParent; + } + else { + mirror.parentNode = elementClosest(origTarget, '.fc'); + } + mirror.revertDuration = options.dragRevertDuration; + var isValid = component.isValidSegDownEl(origTarget) && + !elementClosest(origTarget, '.fc-event-resizer'); // NOT on a resizer + dragging.setIgnoreMove(!isValid); + // disable dragging for elements that are resizable (ie, selectable) + // but are not draggable + _this.isDragging = isValid && + ev.subjectEl.classList.contains('fc-event-draggable'); + }; + _this.handleDragStart = function (ev) { + var initialContext = _this.component.context; + var eventRange = _this.eventRange; + var eventInstanceId = eventRange.instance.instanceId; + if (ev.isTouch) { + // need to select a different event? + if (eventInstanceId !== _this.component.props.eventSelection) { + initialContext.dispatch({ type: 'SELECT_EVENT', eventInstanceId: eventInstanceId }); + } + } + else { + // if now using mouse, but was previous touch interaction, clear selected event + initialContext.dispatch({ type: 'UNSELECT_EVENT' }); + } + if (_this.isDragging) { + initialContext.calendarApi.unselect(ev); // unselect *date* selection + initialContext.emitter.trigger('eventDragStart', { + el: _this.subjectEl, + event: new EventApi(initialContext, eventRange.def, eventRange.instance), + jsEvent: ev.origEvent, + view: initialContext.viewApi, + }); + } + }; + _this.handleHitUpdate = function (hit, isFinal) { + if (!_this.isDragging) { + return; + } + var relevantEvents = _this.relevantEvents; + var initialHit = _this.hitDragging.initialHit; + var initialContext = _this.component.context; + // states based on new hit + var receivingContext = null; + var mutation = null; + var mutatedRelevantEvents = null; + var isInvalid = false; + var interaction = { + affectedEvents: relevantEvents, + mutatedEvents: createEmptyEventStore(), + isEvent: true, + }; + if (hit) { + var receivingComponent = hit.component; + receivingContext = receivingComponent.context; + var receivingOptions = receivingContext.options; + if (initialContext === receivingContext || + (receivingOptions.editable && receivingOptions.droppable)) { + mutation = computeEventMutation(initialHit, hit, receivingContext.getCurrentData().pluginHooks.eventDragMutationMassagers); + if (mutation) { + mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, receivingContext.getCurrentData().eventUiBases, mutation, receivingContext); + interaction.mutatedEvents = mutatedRelevantEvents; + if (!receivingComponent.isInteractionValid(interaction)) { + isInvalid = true; + mutation = null; + mutatedRelevantEvents = null; + interaction.mutatedEvents = createEmptyEventStore(); + } + } + } + else { + receivingContext = null; + } + } + _this.displayDrag(receivingContext, interaction); + if (!isInvalid) { + enableCursor(); + } + else { + disableCursor(); + } + if (!isFinal) { + if (initialContext === receivingContext && // TODO: write test for this + isHitsEqual(initialHit, hit)) { + mutation = null; + } + _this.dragging.setMirrorNeedsRevert(!mutation); + // render the mirror if no already-rendered mirror + // TODO: wish we could somehow wait for dispatch to guarantee render + _this.dragging.setMirrorIsVisible(!hit || !document.querySelector('.fc-event-mirror')); + // assign states based on new hit + _this.receivingContext = receivingContext; + _this.validMutation = mutation; + _this.mutatedRelevantEvents = mutatedRelevantEvents; + } + }; + _this.handlePointerUp = function () { + if (!_this.isDragging) { + _this.cleanup(); // because handleDragEnd won't fire + } + }; + _this.handleDragEnd = function (ev) { + if (_this.isDragging) { + var initialContext_1 = _this.component.context; + var initialView = initialContext_1.viewApi; + var _a = _this, receivingContext_1 = _a.receivingContext, validMutation = _a.validMutation; + var eventDef = _this.eventRange.def; + var eventInstance = _this.eventRange.instance; + var eventApi = new EventApi(initialContext_1, eventDef, eventInstance); + var relevantEvents_1 = _this.relevantEvents; + var mutatedRelevantEvents_1 = _this.mutatedRelevantEvents; + var finalHit = _this.hitDragging.finalHit; + _this.clearDrag(); // must happen after revert animation + initialContext_1.emitter.trigger('eventDragStop', { + el: _this.subjectEl, + event: eventApi, + jsEvent: ev.origEvent, + view: initialView, + }); + if (validMutation) { + // dropped within same calendar + if (receivingContext_1 === initialContext_1) { + var updatedEventApi = new EventApi(initialContext_1, mutatedRelevantEvents_1.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents_1.instances[eventInstance.instanceId] : null); + initialContext_1.dispatch({ + type: 'MERGE_EVENTS', + eventStore: mutatedRelevantEvents_1, + }); + var eventChangeArg = { + oldEvent: eventApi, + event: updatedEventApi, + relatedEvents: buildEventApis(mutatedRelevantEvents_1, initialContext_1, eventInstance), + revert: function () { + initialContext_1.dispatch({ + type: 'MERGE_EVENTS', + eventStore: relevantEvents_1, + }); + }, + }; + var transformed = {}; + for (var _i = 0, _b = initialContext_1.getCurrentData().pluginHooks.eventDropTransformers; _i < _b.length; _i++) { + var transformer = _b[_i]; + __assign(transformed, transformer(validMutation, initialContext_1)); + } + initialContext_1.emitter.trigger('eventDrop', __assign(__assign(__assign({}, eventChangeArg), transformed), { el: ev.subjectEl, delta: validMutation.datesDelta, jsEvent: ev.origEvent, view: initialView })); + initialContext_1.emitter.trigger('eventChange', eventChangeArg); + // dropped in different calendar + } + else if (receivingContext_1) { + var eventRemoveArg = { + event: eventApi, + relatedEvents: buildEventApis(relevantEvents_1, initialContext_1, eventInstance), + revert: function () { + initialContext_1.dispatch({ + type: 'MERGE_EVENTS', + eventStore: relevantEvents_1, + }); + }, + }; + initialContext_1.emitter.trigger('eventLeave', __assign(__assign({}, eventRemoveArg), { draggedEl: ev.subjectEl, view: initialView })); + initialContext_1.dispatch({ + type: 'REMOVE_EVENTS', + eventStore: relevantEvents_1, + }); + initialContext_1.emitter.trigger('eventRemove', eventRemoveArg); + var addedEventDef = mutatedRelevantEvents_1.defs[eventDef.defId]; + var addedEventInstance = mutatedRelevantEvents_1.instances[eventInstance.instanceId]; + var addedEventApi = new EventApi(receivingContext_1, addedEventDef, addedEventInstance); + receivingContext_1.dispatch({ + type: 'MERGE_EVENTS', + eventStore: mutatedRelevantEvents_1, + }); + var eventAddArg = { + event: addedEventApi, + relatedEvents: buildEventApis(mutatedRelevantEvents_1, receivingContext_1, addedEventInstance), + revert: function () { + receivingContext_1.dispatch({ + type: 'REMOVE_EVENTS', + eventStore: mutatedRelevantEvents_1, + }); + }, + }; + receivingContext_1.emitter.trigger('eventAdd', eventAddArg); + if (ev.isTouch) { + receivingContext_1.dispatch({ + type: 'SELECT_EVENT', + eventInstanceId: eventInstance.instanceId, + }); + } + receivingContext_1.emitter.trigger('drop', __assign(__assign({}, buildDatePointApiWithContext(finalHit.dateSpan, receivingContext_1)), { draggedEl: ev.subjectEl, jsEvent: ev.origEvent, view: finalHit.component.context.viewApi })); + receivingContext_1.emitter.trigger('eventReceive', __assign(__assign({}, eventAddArg), { draggedEl: ev.subjectEl, view: finalHit.component.context.viewApi })); + } + } + else { + initialContext_1.emitter.trigger('_noEventDrop'); + } + } + _this.cleanup(); + }; + var component = _this.component; + var options = component.context.options; + var dragging = _this.dragging = new FeaturefulElementDragging(settings.el); + dragging.pointer.selector = EventDragging.SELECTOR; + dragging.touchScrollAllowed = false; + dragging.autoScroller.isEnabled = options.dragScroll; + var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, interactionSettingsStore); + hitDragging.useSubjectCenter = settings.useEventCenter; + hitDragging.emitter.on('pointerdown', _this.handlePointerDown); + hitDragging.emitter.on('dragstart', _this.handleDragStart); + hitDragging.emitter.on('hitupdate', _this.handleHitUpdate); + hitDragging.emitter.on('pointerup', _this.handlePointerUp); + hitDragging.emitter.on('dragend', _this.handleDragEnd); + return _this; + } + EventDragging.prototype.destroy = function () { + this.dragging.destroy(); + }; + // render a drag state on the next receivingCalendar + EventDragging.prototype.displayDrag = function (nextContext, state) { + var initialContext = this.component.context; + var prevContext = this.receivingContext; + // does the previous calendar need to be cleared? + if (prevContext && prevContext !== nextContext) { + // does the initial calendar need to be cleared? + // if so, don't clear all the way. we still need to to hide the affectedEvents + if (prevContext === initialContext) { + prevContext.dispatch({ + type: 'SET_EVENT_DRAG', + state: { + affectedEvents: state.affectedEvents, + mutatedEvents: createEmptyEventStore(), + isEvent: true, + }, + }); + // completely clear the old calendar if it wasn't the initial + } + else { + prevContext.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + } + if (nextContext) { + nextContext.dispatch({ type: 'SET_EVENT_DRAG', state: state }); + } + }; + EventDragging.prototype.clearDrag = function () { + var initialCalendar = this.component.context; + var receivingContext = this.receivingContext; + if (receivingContext) { + receivingContext.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + // the initial calendar might have an dummy drag state from displayDrag + if (initialCalendar !== receivingContext) { + initialCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + }; + EventDragging.prototype.cleanup = function () { + this.subjectSeg = null; + this.isDragging = false; + this.eventRange = null; + this.relevantEvents = null; + this.receivingContext = null; + this.validMutation = null; + this.mutatedRelevantEvents = null; + }; + // TODO: test this in IE11 + // QUESTION: why do we need it on the resizable??? + EventDragging.SELECTOR = '.fc-event-draggable, .fc-event-resizable'; + return EventDragging; + }(Interaction)); + function computeEventMutation(hit0, hit1, massagers) { + var dateSpan0 = hit0.dateSpan; + var dateSpan1 = hit1.dateSpan; + var date0 = dateSpan0.range.start; + var date1 = dateSpan1.range.start; + var standardProps = {}; + if (dateSpan0.allDay !== dateSpan1.allDay) { + standardProps.allDay = dateSpan1.allDay; + standardProps.hasEnd = hit1.component.context.options.allDayMaintainDuration; + if (dateSpan1.allDay) { + // means date1 is already start-of-day, + // but date0 needs to be converted + date0 = startOfDay(date0); + } + } + var delta = diffDates(date0, date1, hit0.component.context.dateEnv, hit0.component === hit1.component ? + hit0.component.largeUnit : + null); + if (delta.milliseconds) { // has hours/minutes/seconds + standardProps.allDay = false; + } + var mutation = { + datesDelta: delta, + standardProps: standardProps, + }; + for (var _i = 0, massagers_1 = massagers; _i < massagers_1.length; _i++) { + var massager = massagers_1[_i]; + massager(mutation, hit0, hit1); + } + return mutation; + } + function getComponentTouchDelay$1(component) { + var options = component.context.options; + var delay = options.eventLongPressDelay; + if (delay == null) { + delay = options.longPressDelay; + } + return delay; + } + + var EventResizing = /** @class */ (function (_super) { + __extends(EventResizing, _super); + function EventResizing(settings) { + var _this = _super.call(this, settings) || this; + // internal state + _this.draggingSegEl = null; + _this.draggingSeg = null; // TODO: rename to resizingSeg? subjectSeg? + _this.eventRange = null; + _this.relevantEvents = null; + _this.validMutation = null; + _this.mutatedRelevantEvents = null; + _this.handlePointerDown = function (ev) { + var component = _this.component; + var segEl = _this.querySegEl(ev); + var seg = getElSeg(segEl); + var eventRange = _this.eventRange = seg.eventRange; + _this.dragging.minDistance = component.context.options.eventDragMinDistance; + // if touch, need to be working with a selected event + _this.dragging.setIgnoreMove(!_this.component.isValidSegDownEl(ev.origEvent.target) || + (ev.isTouch && _this.component.props.eventSelection !== eventRange.instance.instanceId)); + }; + _this.handleDragStart = function (ev) { + var context = _this.component.context; + var eventRange = _this.eventRange; + _this.relevantEvents = getRelevantEvents(context.getCurrentData().eventStore, _this.eventRange.instance.instanceId); + var segEl = _this.querySegEl(ev); + _this.draggingSegEl = segEl; + _this.draggingSeg = getElSeg(segEl); + context.calendarApi.unselect(); + context.emitter.trigger('eventResizeStart', { + el: segEl, + event: new EventApi(context, eventRange.def, eventRange.instance), + jsEvent: ev.origEvent, + view: context.viewApi, + }); + }; + _this.handleHitUpdate = function (hit, isFinal, ev) { + var context = _this.component.context; + var relevantEvents = _this.relevantEvents; + var initialHit = _this.hitDragging.initialHit; + var eventInstance = _this.eventRange.instance; + var mutation = null; + var mutatedRelevantEvents = null; + var isInvalid = false; + var interaction = { + affectedEvents: relevantEvents, + mutatedEvents: createEmptyEventStore(), + isEvent: true, + }; + if (hit) { + mutation = computeMutation(initialHit, hit, ev.subjectEl.classList.contains('fc-event-resizer-start'), eventInstance.range, context.pluginHooks.eventResizeJoinTransforms); + } + if (mutation) { + mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, context.getCurrentData().eventUiBases, mutation, context); + interaction.mutatedEvents = mutatedRelevantEvents; + if (!_this.component.isInteractionValid(interaction)) { + isInvalid = true; + mutation = null; + mutatedRelevantEvents = null; + interaction.mutatedEvents = null; + } + } + if (mutatedRelevantEvents) { + context.dispatch({ + type: 'SET_EVENT_RESIZE', + state: interaction, + }); + } + else { + context.dispatch({ type: 'UNSET_EVENT_RESIZE' }); + } + if (!isInvalid) { + enableCursor(); + } + else { + disableCursor(); + } + if (!isFinal) { + if (mutation && isHitsEqual(initialHit, hit)) { + mutation = null; + } + _this.validMutation = mutation; + _this.mutatedRelevantEvents = mutatedRelevantEvents; + } + }; + _this.handleDragEnd = function (ev) { + var context = _this.component.context; + var eventDef = _this.eventRange.def; + var eventInstance = _this.eventRange.instance; + var eventApi = new EventApi(context, eventDef, eventInstance); + var relevantEvents = _this.relevantEvents; + var mutatedRelevantEvents = _this.mutatedRelevantEvents; + context.emitter.trigger('eventResizeStop', { + el: _this.draggingSegEl, + event: eventApi, + jsEvent: ev.origEvent, + view: context.viewApi, + }); + if (_this.validMutation) { + var updatedEventApi = new EventApi(context, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null); + context.dispatch({ + type: 'MERGE_EVENTS', + eventStore: mutatedRelevantEvents, + }); + var eventChangeArg = { + oldEvent: eventApi, + event: updatedEventApi, + relatedEvents: buildEventApis(mutatedRelevantEvents, context, eventInstance), + revert: function () { + context.dispatch({ + type: 'MERGE_EVENTS', + eventStore: relevantEvents, + }); + }, + }; + context.emitter.trigger('eventResize', __assign(__assign({}, eventChangeArg), { el: _this.draggingSegEl, startDelta: _this.validMutation.startDelta || createDuration(0), endDelta: _this.validMutation.endDelta || createDuration(0), jsEvent: ev.origEvent, view: context.viewApi })); + context.emitter.trigger('eventChange', eventChangeArg); + } + else { + context.emitter.trigger('_noEventResize'); + } + // reset all internal state + _this.draggingSeg = null; + _this.relevantEvents = null; + _this.validMutation = null; + // okay to keep eventInstance around. useful to set it in handlePointerDown + }; + var component = settings.component; + var dragging = _this.dragging = new FeaturefulElementDragging(settings.el); + dragging.pointer.selector = '.fc-event-resizer'; + dragging.touchScrollAllowed = false; + dragging.autoScroller.isEnabled = component.context.options.dragScroll; + var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, interactionSettingsToStore(settings)); + hitDragging.emitter.on('pointerdown', _this.handlePointerDown); + hitDragging.emitter.on('dragstart', _this.handleDragStart); + hitDragging.emitter.on('hitupdate', _this.handleHitUpdate); + hitDragging.emitter.on('dragend', _this.handleDragEnd); + return _this; + } + EventResizing.prototype.destroy = function () { + this.dragging.destroy(); + }; + EventResizing.prototype.querySegEl = function (ev) { + return elementClosest(ev.subjectEl, '.fc-event'); + }; + return EventResizing; + }(Interaction)); + function computeMutation(hit0, hit1, isFromStart, instanceRange, transforms) { + var dateEnv = hit0.component.context.dateEnv; + var date0 = hit0.dateSpan.range.start; + var date1 = hit1.dateSpan.range.start; + var delta = diffDates(date0, date1, dateEnv, hit0.component.largeUnit); + var props = {}; + for (var _i = 0, transforms_1 = transforms; _i < transforms_1.length; _i++) { + var transform = transforms_1[_i]; + var res = transform(hit0, hit1); + if (res === false) { + return null; + } + if (res) { + __assign(props, res); + } + } + if (isFromStart) { + if (dateEnv.add(instanceRange.start, delta) < instanceRange.end) { + props.startDelta = delta; + return props; + } + } + else if (dateEnv.add(instanceRange.end, delta) > instanceRange.start) { + props.endDelta = delta; + return props; + } + return null; + } + + var UnselectAuto = /** @class */ (function () { + function UnselectAuto(context) { + var _this = this; + this.context = context; + this.isRecentPointerDateSelect = false; // wish we could use a selector to detect date selection, but uses hit system + this.matchesCancel = false; + this.matchesEvent = false; + this.onSelect = function (selectInfo) { + if (selectInfo.jsEvent) { + _this.isRecentPointerDateSelect = true; + } + }; + this.onDocumentPointerDown = function (pev) { + var unselectCancel = _this.context.options.unselectCancel; + var downEl = pev.origEvent.target; + _this.matchesCancel = !!elementClosest(downEl, unselectCancel); + _this.matchesEvent = !!elementClosest(downEl, EventDragging.SELECTOR); // interaction started on an event? + }; + this.onDocumentPointerUp = function (pev) { + var context = _this.context; + var documentPointer = _this.documentPointer; + var calendarState = context.getCurrentData(); + // touch-scrolling should never unfocus any type of selection + if (!documentPointer.wasTouchScroll) { + if (calendarState.dateSelection && // an existing date selection? + !_this.isRecentPointerDateSelect // a new pointer-initiated date selection since last onDocumentPointerUp? + ) { + var unselectAuto = context.options.unselectAuto; + if (unselectAuto && (!unselectAuto || !_this.matchesCancel)) { + context.calendarApi.unselect(pev); + } + } + if (calendarState.eventSelection && // an existing event selected? + !_this.matchesEvent // interaction DIDN'T start on an event + ) { + context.dispatch({ type: 'UNSELECT_EVENT' }); + } + } + _this.isRecentPointerDateSelect = false; + }; + var documentPointer = this.documentPointer = new PointerDragging(document); + documentPointer.shouldIgnoreMove = true; + documentPointer.shouldWatchScroll = false; + documentPointer.emitter.on('pointerdown', this.onDocumentPointerDown); + documentPointer.emitter.on('pointerup', this.onDocumentPointerUp); + /* + TODO: better way to know about whether there was a selection with the pointer + */ + context.emitter.on('select', this.onSelect); + } + UnselectAuto.prototype.destroy = function () { + this.context.emitter.off('select', this.onSelect); + this.documentPointer.destroy(); + }; + return UnselectAuto; + }()); + + var OPTION_REFINERS = { + fixedMirrorParent: identity, + }; + var LISTENER_REFINERS = { + dateClick: identity, + eventDragStart: identity, + eventDragStop: identity, + eventDrop: identity, + eventResizeStart: identity, + eventResizeStop: identity, + eventResize: identity, + drop: identity, + eventReceive: identity, + eventLeave: identity, + }; + + /* + Given an already instantiated draggable object for one-or-more elements, + Interprets any dragging as an attempt to drag an events that lives outside + of a calendar onto a calendar. + */ + var ExternalElementDragging = /** @class */ (function () { + function ExternalElementDragging(dragging, suppliedDragMeta) { + var _this = this; + this.receivingContext = null; + this.droppableEvent = null; // will exist for all drags, even if create:false + this.suppliedDragMeta = null; + this.dragMeta = null; + this.handleDragStart = function (ev) { + _this.dragMeta = _this.buildDragMeta(ev.subjectEl); + }; + this.handleHitUpdate = function (hit, isFinal, ev) { + var dragging = _this.hitDragging.dragging; + var receivingContext = null; + var droppableEvent = null; + var isInvalid = false; + var interaction = { + affectedEvents: createEmptyEventStore(), + mutatedEvents: createEmptyEventStore(), + isEvent: _this.dragMeta.create, + }; + if (hit) { + receivingContext = hit.component.context; + if (_this.canDropElOnCalendar(ev.subjectEl, receivingContext)) { + droppableEvent = computeEventForDateSpan(hit.dateSpan, _this.dragMeta, receivingContext); + interaction.mutatedEvents = eventTupleToStore(droppableEvent); + isInvalid = !isInteractionValid(interaction, receivingContext); + if (isInvalid) { + interaction.mutatedEvents = createEmptyEventStore(); + droppableEvent = null; + } + } + } + _this.displayDrag(receivingContext, interaction); + // show mirror if no already-rendered mirror element OR if we are shutting down the mirror (?) + // TODO: wish we could somehow wait for dispatch to guarantee render + dragging.setMirrorIsVisible(isFinal || !droppableEvent || !document.querySelector('.fc-event-mirror')); + if (!isInvalid) { + enableCursor(); + } + else { + disableCursor(); + } + if (!isFinal) { + dragging.setMirrorNeedsRevert(!droppableEvent); + _this.receivingContext = receivingContext; + _this.droppableEvent = droppableEvent; + } + }; + this.handleDragEnd = function (pev) { + var _a = _this, receivingContext = _a.receivingContext, droppableEvent = _a.droppableEvent; + _this.clearDrag(); + if (receivingContext && droppableEvent) { + var finalHit = _this.hitDragging.finalHit; + var finalView = finalHit.component.context.viewApi; + var dragMeta = _this.dragMeta; + receivingContext.emitter.trigger('drop', __assign(__assign({}, buildDatePointApiWithContext(finalHit.dateSpan, receivingContext)), { draggedEl: pev.subjectEl, jsEvent: pev.origEvent, view: finalView })); + if (dragMeta.create) { + var addingEvents_1 = eventTupleToStore(droppableEvent); + receivingContext.dispatch({ + type: 'MERGE_EVENTS', + eventStore: addingEvents_1, + }); + if (pev.isTouch) { + receivingContext.dispatch({ + type: 'SELECT_EVENT', + eventInstanceId: droppableEvent.instance.instanceId, + }); + } + // signal that an external event landed + receivingContext.emitter.trigger('eventReceive', { + event: new EventApi(receivingContext, droppableEvent.def, droppableEvent.instance), + relatedEvents: [], + revert: function () { + receivingContext.dispatch({ + type: 'REMOVE_EVENTS', + eventStore: addingEvents_1, + }); + }, + draggedEl: pev.subjectEl, + view: finalView, + }); + } + } + _this.receivingContext = null; + _this.droppableEvent = null; + }; + var hitDragging = this.hitDragging = new HitDragging(dragging, interactionSettingsStore); + hitDragging.requireInitial = false; // will start outside of a component + hitDragging.emitter.on('dragstart', this.handleDragStart); + hitDragging.emitter.on('hitupdate', this.handleHitUpdate); + hitDragging.emitter.on('dragend', this.handleDragEnd); + this.suppliedDragMeta = suppliedDragMeta; + } + ExternalElementDragging.prototype.buildDragMeta = function (subjectEl) { + if (typeof this.suppliedDragMeta === 'object') { + return parseDragMeta(this.suppliedDragMeta); + } + if (typeof this.suppliedDragMeta === 'function') { + return parseDragMeta(this.suppliedDragMeta(subjectEl)); + } + return getDragMetaFromEl(subjectEl); + }; + ExternalElementDragging.prototype.displayDrag = function (nextContext, state) { + var prevContext = this.receivingContext; + if (prevContext && prevContext !== nextContext) { + prevContext.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + if (nextContext) { + nextContext.dispatch({ type: 'SET_EVENT_DRAG', state: state }); + } + }; + ExternalElementDragging.prototype.clearDrag = function () { + if (this.receivingContext) { + this.receivingContext.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + }; + ExternalElementDragging.prototype.canDropElOnCalendar = function (el, receivingContext) { + var dropAccept = receivingContext.options.dropAccept; + if (typeof dropAccept === 'function') { + return dropAccept.call(receivingContext.calendarApi, el); + } + if (typeof dropAccept === 'string' && dropAccept) { + return Boolean(elementMatches(el, dropAccept)); + } + return true; + }; + return ExternalElementDragging; + }()); + // Utils for computing event store from the DragMeta + // ---------------------------------------------------------------------------------------------------- + function computeEventForDateSpan(dateSpan, dragMeta, context) { + var defProps = __assign({}, dragMeta.leftoverProps); + for (var _i = 0, _a = context.pluginHooks.externalDefTransforms; _i < _a.length; _i++) { + var transform = _a[_i]; + __assign(defProps, transform(dateSpan, dragMeta)); + } + var _b = refineEventDef(defProps, context), refined = _b.refined, extra = _b.extra; + var def = parseEventDef(refined, extra, dragMeta.sourceId, dateSpan.allDay, context.options.forceEventDuration || Boolean(dragMeta.duration), // hasEnd + context); + var start = dateSpan.range.start; + // only rely on time info if drop zone is all-day, + // otherwise, we already know the time + if (dateSpan.allDay && dragMeta.startTime) { + start = context.dateEnv.add(start, dragMeta.startTime); + } + var end = dragMeta.duration ? + context.dateEnv.add(start, dragMeta.duration) : + getDefaultEventEnd(dateSpan.allDay, start, context); + var instance = createEventInstance(def.defId, { start: start, end: end }); + return { def: def, instance: instance }; + } + // Utils for extracting data from element + // ---------------------------------------------------------------------------------------------------- + function getDragMetaFromEl(el) { + var str = getEmbeddedElData(el, 'event'); + var obj = str ? + JSON.parse(str) : + { create: false }; // if no embedded data, assume no event creation + return parseDragMeta(obj); + } + config.dataAttrPrefix = ''; + function getEmbeddedElData(el, name) { + var prefix = config.dataAttrPrefix; + var prefixedName = (prefix ? prefix + '-' : '') + name; + return el.getAttribute('data-' + prefixedName) || ''; + } + + /* + Makes an element (that is *external* to any calendar) draggable. + Can pass in data that determines how an event will be created when dropped onto a calendar. + Leverages FullCalendar's internal drag-n-drop functionality WITHOUT a third-party drag system. + */ + var ExternalDraggable = /** @class */ (function () { + function ExternalDraggable(el, settings) { + var _this = this; + if (settings === void 0) { settings = {}; } + this.handlePointerDown = function (ev) { + var dragging = _this.dragging; + var _a = _this.settings, minDistance = _a.minDistance, longPressDelay = _a.longPressDelay; + dragging.minDistance = + minDistance != null ? + minDistance : + (ev.isTouch ? 0 : BASE_OPTION_DEFAULTS.eventDragMinDistance); + dragging.delay = + ev.isTouch ? // TODO: eventually read eventLongPressDelay instead vvv + (longPressDelay != null ? longPressDelay : BASE_OPTION_DEFAULTS.longPressDelay) : + 0; + }; + this.handleDragStart = function (ev) { + if (ev.isTouch && + _this.dragging.delay && + ev.subjectEl.classList.contains('fc-event')) { + _this.dragging.mirror.getMirrorEl().classList.add('fc-event-selected'); + } + }; + this.settings = settings; + var dragging = this.dragging = new FeaturefulElementDragging(el); + dragging.touchScrollAllowed = false; + if (settings.itemSelector != null) { + dragging.pointer.selector = settings.itemSelector; + } + if (settings.appendTo != null) { + dragging.mirror.parentNode = settings.appendTo; // TODO: write tests + } + dragging.emitter.on('pointerdown', this.handlePointerDown); + dragging.emitter.on('dragstart', this.handleDragStart); + new ExternalElementDragging(dragging, settings.eventData); // eslint-disable-line no-new + } + ExternalDraggable.prototype.destroy = function () { + this.dragging.destroy(); + }; + return ExternalDraggable; + }()); + + /* + Detects when a *THIRD-PARTY* drag-n-drop system interacts with elements. + The third-party system is responsible for drawing the visuals effects of the drag. + This class simply monitors for pointer movements and fires events. + It also has the ability to hide the moving element (the "mirror") during the drag. + */ + var InferredElementDragging = /** @class */ (function (_super) { + __extends(InferredElementDragging, _super); + function InferredElementDragging(containerEl) { + var _this = _super.call(this, containerEl) || this; + _this.shouldIgnoreMove = false; + _this.mirrorSelector = ''; + _this.currentMirrorEl = null; + _this.handlePointerDown = function (ev) { + _this.emitter.trigger('pointerdown', ev); + if (!_this.shouldIgnoreMove) { + // fire dragstart right away. does not support delay or min-distance + _this.emitter.trigger('dragstart', ev); + } + }; + _this.handlePointerMove = function (ev) { + if (!_this.shouldIgnoreMove) { + _this.emitter.trigger('dragmove', ev); + } + }; + _this.handlePointerUp = function (ev) { + _this.emitter.trigger('pointerup', ev); + if (!_this.shouldIgnoreMove) { + // fire dragend right away. does not support a revert animation + _this.emitter.trigger('dragend', ev); + } + }; + var pointer = _this.pointer = new PointerDragging(containerEl); + pointer.emitter.on('pointerdown', _this.handlePointerDown); + pointer.emitter.on('pointermove', _this.handlePointerMove); + pointer.emitter.on('pointerup', _this.handlePointerUp); + return _this; + } + InferredElementDragging.prototype.destroy = function () { + this.pointer.destroy(); + }; + InferredElementDragging.prototype.setIgnoreMove = function (bool) { + this.shouldIgnoreMove = bool; + }; + InferredElementDragging.prototype.setMirrorIsVisible = function (bool) { + if (bool) { + // restore a previously hidden element. + // use the reference in case the selector class has already been removed. + if (this.currentMirrorEl) { + this.currentMirrorEl.style.visibility = ''; + this.currentMirrorEl = null; + } + } + else { + var mirrorEl = this.mirrorSelector ? + document.querySelector(this.mirrorSelector) : + null; + if (mirrorEl) { + this.currentMirrorEl = mirrorEl; + mirrorEl.style.visibility = 'hidden'; + } + } + }; + return InferredElementDragging; + }(ElementDragging)); + + /* + Bridges third-party drag-n-drop systems with FullCalendar. + Must be instantiated and destroyed by caller. + */ + var ThirdPartyDraggable = /** @class */ (function () { + function ThirdPartyDraggable(containerOrSettings, settings) { + var containerEl = document; + if ( + // wish we could just test instanceof EventTarget, but doesn't work in IE11 + containerOrSettings === document || + containerOrSettings instanceof Element) { + containerEl = containerOrSettings; + settings = settings || {}; + } + else { + settings = (containerOrSettings || {}); + } + var dragging = this.dragging = new InferredElementDragging(containerEl); + if (typeof settings.itemSelector === 'string') { + dragging.pointer.selector = settings.itemSelector; + } + else if (containerEl === document) { + dragging.pointer.selector = '[data-event]'; + } + if (typeof settings.mirrorSelector === 'string') { + dragging.mirrorSelector = settings.mirrorSelector; + } + new ExternalElementDragging(dragging, settings.eventData); // eslint-disable-line no-new + } + ThirdPartyDraggable.prototype.destroy = function () { + this.dragging.destroy(); + }; + return ThirdPartyDraggable; + }()); + + var interactionPlugin = createPlugin({ + componentInteractions: [DateClicking, DateSelecting, EventDragging, EventResizing], + calendarInteractions: [UnselectAuto], + elementDraggingImpl: FeaturefulElementDragging, + optionRefiners: OPTION_REFINERS, + listenerRefiners: LISTENER_REFINERS, + }); + + /* An abstract class for the daygrid views, as well as month view. Renders one or more rows of day cells. + ----------------------------------------------------------------------------------------------------------------------*/ + // It is a manager for a Table subcomponent, which does most of the heavy lifting. + // It is responsible for managing width/height. + var TableView = /** @class */ (function (_super) { + __extends(TableView, _super); + function TableView() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.headerElRef = createRef(); + return _this; + } + TableView.prototype.renderSimpleLayout = function (headerRowContent, bodyContent) { + var _a = this, props = _a.props, context = _a.context; + var sections = []; + var stickyHeaderDates = getStickyHeaderDates(context.options); + if (headerRowContent) { + sections.push({ + type: 'header', + key: 'header', + isSticky: stickyHeaderDates, + chunk: { + elRef: this.headerElRef, + tableClassName: 'fc-col-header', + rowContent: headerRowContent, + }, + }); + } + sections.push({ + type: 'body', + key: 'body', + liquid: true, + chunk: { content: bodyContent }, + }); + return (createElement(ViewRoot, { viewSpec: context.viewSpec }, function (rootElRef, classNames) { return (createElement("div", { ref: rootElRef, className: ['fc-daygrid'].concat(classNames).join(' ') }, + createElement(SimpleScrollGrid, { liquid: !props.isHeightAuto && !props.forPrint, cols: [] /* TODO: make optional? */, sections: sections }))); })); + }; + TableView.prototype.renderHScrollLayout = function (headerRowContent, bodyContent, colCnt, dayMinWidth) { + var ScrollGrid = this.context.pluginHooks.scrollGridImpl; + if (!ScrollGrid) { + throw new Error('No ScrollGrid implementation'); + } + var _a = this, props = _a.props, context = _a.context; + var stickyHeaderDates = !props.forPrint && getStickyHeaderDates(context.options); + var stickyFooterScrollbar = !props.forPrint && getStickyFooterScrollbar(context.options); + var sections = []; + if (headerRowContent) { + sections.push({ + type: 'header', + key: 'header', + isSticky: stickyHeaderDates, + chunks: [{ + key: 'main', + elRef: this.headerElRef, + tableClassName: 'fc-col-header', + rowContent: headerRowContent, + }], + }); + } + sections.push({ + type: 'body', + key: 'body', + liquid: true, + chunks: [{ + key: 'main', + content: bodyContent, + }], + }); + if (stickyFooterScrollbar) { + sections.push({ + type: 'footer', + key: 'footer', + isSticky: true, + chunks: [{ + key: 'main', + content: renderScrollShim, + }], + }); + } + return (createElement(ViewRoot, { viewSpec: context.viewSpec }, function (rootElRef, classNames) { return (createElement("div", { ref: rootElRef, className: ['fc-daygrid'].concat(classNames).join(' ') }, + createElement(ScrollGrid, { liquid: !props.isHeightAuto && !props.forPrint, colGroups: [{ cols: [{ span: colCnt, minWidth: dayMinWidth }] }], sections: sections }))); })); + }; + return TableView; + }(DateComponent)); + + function splitSegsByRow(segs, rowCnt) { + var byRow = []; + for (var i = 0; i < rowCnt; i += 1) { + byRow[i] = []; + } + for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) { + var seg = segs_1[_i]; + byRow[seg.row].push(seg); + } + return byRow; + } + function splitSegsByFirstCol(segs, colCnt) { + var byCol = []; + for (var i = 0; i < colCnt; i += 1) { + byCol[i] = []; + } + for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) { + var seg = segs_2[_i]; + byCol[seg.firstCol].push(seg); + } + return byCol; + } + function splitInteractionByRow(ui, rowCnt) { + var byRow = []; + if (!ui) { + for (var i = 0; i < rowCnt; i += 1) { + byRow[i] = null; + } + } + else { + for (var i = 0; i < rowCnt; i += 1) { + byRow[i] = { + affectedInstances: ui.affectedInstances, + isEvent: ui.isEvent, + segs: [], + }; + } + for (var _i = 0, _a = ui.segs; _i < _a.length; _i++) { + var seg = _a[_i]; + byRow[seg.row].segs.push(seg); + } + } + return byRow; + } + + var TableCellTop = /** @class */ (function (_super) { + __extends(TableCellTop, _super); + function TableCellTop() { + return _super !== null && _super.apply(this, arguments) || this; + } + TableCellTop.prototype.render = function () { + var props = this.props; + var navLinkAttrs = this.context.options.navLinks + ? { 'data-navlink': buildNavLinkData(props.date), tabIndex: 0 } + : {}; + return (createElement(DayCellContent, { date: props.date, dateProfile: props.dateProfile, todayRange: props.todayRange, showDayNumber: props.showDayNumber, extraHookProps: props.extraHookProps, defaultContent: renderTopInner }, function (innerElRef, innerContent) { return ((innerContent || props.forceDayTop) && (createElement("div", { className: "fc-daygrid-day-top", ref: innerElRef }, + createElement("a", __assign({ className: "fc-daygrid-day-number" }, navLinkAttrs), innerContent || createElement(Fragment, null, "\u00A0"))))); })); + }; + return TableCellTop; + }(BaseComponent)); + function renderTopInner(props) { + return props.dayNumberText; + } + + var DEFAULT_WEEK_NUM_FORMAT = createFormatter({ week: 'narrow' }); + var TableCell = /** @class */ (function (_super) { + __extends(TableCell, _super); + function TableCell() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.handleRootEl = function (el) { + _this.rootEl = el; + setRef(_this.props.elRef, el); + }; + _this.handleMoreLinkClick = function (ev) { + var props = _this.props; + if (props.onMoreClick) { + var allSegs = props.segsByEachCol; + var hiddenSegs = allSegs.filter(function (seg) { return props.segIsHidden[seg.eventRange.instance.instanceId]; }); + props.onMoreClick({ + date: props.date, + allSegs: allSegs, + hiddenSegs: hiddenSegs, + moreCnt: props.moreCnt, + dayEl: _this.rootEl, + ev: ev, + }); + } + }; + return _this; + } + TableCell.prototype.render = function () { + var _this = this; + var _a = this.context, options = _a.options, viewApi = _a.viewApi; + var props = this.props; + var date = props.date, dateProfile = props.dateProfile; + var hookProps = { + num: props.moreCnt, + text: props.buildMoreLinkText(props.moreCnt), + view: viewApi, + }; + var navLinkAttrs = options.navLinks + ? { 'data-navlink': buildNavLinkData(date, 'week'), tabIndex: 0 } + : {}; + return (createElement(DayCellRoot, { date: date, dateProfile: dateProfile, todayRange: props.todayRange, showDayNumber: props.showDayNumber, extraHookProps: props.extraHookProps, elRef: this.handleRootEl }, function (dayElRef, dayClassNames, rootDataAttrs, isDisabled) { return (createElement("td", __assign({ ref: dayElRef, className: ['fc-daygrid-day'].concat(dayClassNames, props.extraClassNames || []).join(' ') }, rootDataAttrs, props.extraDataAttrs), + createElement("div", { className: "fc-daygrid-day-frame fc-scrollgrid-sync-inner", ref: props.innerElRef /* different from hook system! RENAME */ }, + props.showWeekNumber && (createElement(WeekNumberRoot, { date: date, defaultFormat: DEFAULT_WEEK_NUM_FORMAT }, function (weekElRef, weekClassNames, innerElRef, innerContent) { return (createElement("a", __assign({ ref: weekElRef, className: ['fc-daygrid-week-number'].concat(weekClassNames).join(' ') }, navLinkAttrs), innerContent)); })), + !isDisabled && (createElement(TableCellTop, { date: date, dateProfile: dateProfile, showDayNumber: props.showDayNumber, forceDayTop: props.forceDayTop, todayRange: props.todayRange, extraHookProps: props.extraHookProps })), + createElement("div", { className: "fc-daygrid-day-events", ref: props.fgContentElRef, style: { paddingBottom: props.fgPaddingBottom } }, + props.fgContent, + Boolean(props.moreCnt) && (createElement("div", { className: "fc-daygrid-day-bottom", style: { marginTop: props.moreMarginTop } }, + createElement(RenderHook, { hookProps: hookProps, classNames: options.moreLinkClassNames, content: options.moreLinkContent, defaultContent: renderMoreLinkInner, didMount: options.moreLinkDidMount, willUnmount: options.moreLinkWillUnmount }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("a", { ref: rootElRef, className: ['fc-daygrid-more-link'].concat(classNames).join(' '), onClick: _this.handleMoreLinkClick }, innerContent)); })))), + createElement("div", { className: "fc-daygrid-day-bg" }, props.bgContent)))); })); + }; + return TableCell; + }(DateComponent)); + TableCell.addPropsEquality({ + onMoreClick: true, + }); + function renderMoreLinkInner(props) { + return props.text; + } + + var DEFAULT_TABLE_EVENT_TIME_FORMAT = createFormatter({ + hour: 'numeric', + minute: '2-digit', + omitZeroMinute: true, + meridiem: 'narrow', + }); + function hasListItemDisplay(seg) { + var display = seg.eventRange.ui.display; + return display === 'list-item' || (display === 'auto' && + !seg.eventRange.def.allDay && + seg.firstCol === seg.lastCol && // can't be multi-day + seg.isStart && // " + seg.isEnd // " + ); + } + + var TableListItemEvent = /** @class */ (function (_super) { + __extends(TableListItemEvent, _super); + function TableListItemEvent() { + return _super !== null && _super.apply(this, arguments) || this; + } + TableListItemEvent.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context; + var timeFormat = context.options.eventTimeFormat || DEFAULT_TABLE_EVENT_TIME_FORMAT; + var timeText = buildSegTimeText(props.seg, timeFormat, context, true, props.defaultDisplayEventEnd); + return (createElement(EventRoot, { seg: props.seg, timeText: timeText, defaultContent: renderInnerContent$2, isDragging: props.isDragging, isResizing: false, isDateSelecting: false, isSelected: props.isSelected, isPast: props.isPast, isFuture: props.isFuture, isToday: props.isToday }, function (rootElRef, classNames, innerElRef, innerContent) { return ( // we don't use styles! + createElement("a", __assign({ className: ['fc-daygrid-event', 'fc-daygrid-dot-event'].concat(classNames).join(' '), ref: rootElRef }, getSegAnchorAttrs$1(props.seg)), innerContent)); })); + }; + return TableListItemEvent; + }(BaseComponent)); + function renderInnerContent$2(innerProps) { + return (createElement(Fragment, null, + createElement("div", { className: "fc-daygrid-event-dot", style: { borderColor: innerProps.borderColor || innerProps.backgroundColor } }), + innerProps.timeText && (createElement("div", { className: "fc-event-time" }, innerProps.timeText)), + createElement("div", { className: "fc-event-title" }, innerProps.event.title || createElement(Fragment, null, "\u00A0")))); + } + function getSegAnchorAttrs$1(seg) { + var url = seg.eventRange.def.url; + return url ? { href: url } : {}; + } + + var TableBlockEvent = /** @class */ (function (_super) { + __extends(TableBlockEvent, _super); + function TableBlockEvent() { + return _super !== null && _super.apply(this, arguments) || this; + } + TableBlockEvent.prototype.render = function () { + var props = this.props; + return (createElement(StandardEvent, __assign({}, props, { extraClassNames: ['fc-daygrid-event', 'fc-daygrid-block-event', 'fc-h-event'], defaultTimeFormat: DEFAULT_TABLE_EVENT_TIME_FORMAT, defaultDisplayEventEnd: props.defaultDisplayEventEnd, disableResizing: !props.seg.eventRange.def.allDay }))); + }; + return TableBlockEvent; + }(BaseComponent)); + + function computeFgSegPlacement(// for one row. TODO: print mode? + cellModels, segs, dayMaxEvents, dayMaxEventRows, eventHeights, maxContentHeight, colCnt, eventOrderSpecs) { + var colPlacements = []; // if event spans multiple cols, its present in each col + var moreCnts = []; // by-col + var segIsHidden = {}; + var segTops = {}; // always populated for each seg + var segMarginTops = {}; // simetimes populated for each seg + var moreTops = {}; + var paddingBottoms = {}; // for each cell's inner-wrapper div + for (var i = 0; i < colCnt; i += 1) { + colPlacements.push([]); + moreCnts.push(0); + } + segs = sortEventSegs(segs, eventOrderSpecs); + for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) { + var seg = segs_1[_i]; + var instanceId = seg.eventRange.instance.instanceId; + var eventHeight = eventHeights[instanceId + ':' + seg.firstCol]; + placeSeg(seg, eventHeight || 0); // will keep colPlacements sorted by top + } + if (dayMaxEvents === true || dayMaxEventRows === true) { + limitByMaxHeight(moreCnts, segIsHidden, colPlacements, maxContentHeight); // populates moreCnts/segIsHidden + } + else if (typeof dayMaxEvents === 'number') { + limitByMaxEvents(moreCnts, segIsHidden, colPlacements, dayMaxEvents); // populates moreCnts/segIsHidden + } + else if (typeof dayMaxEventRows === 'number') { + limitByMaxRows(moreCnts, segIsHidden, colPlacements, dayMaxEventRows); // populates moreCnts/segIsHidden + } + // computes segTops/segMarginTops/moreTops/paddingBottoms + for (var col = 0; col < colCnt; col += 1) { + var placements = colPlacements[col]; + var currentNonAbsBottom = 0; + var currentAbsHeight = 0; + for (var _a = 0, placements_1 = placements; _a < placements_1.length; _a++) { + var placement = placements_1[_a]; + var seg = placement.seg; + if (!segIsHidden[seg.eventRange.instance.instanceId]) { + segTops[seg.eventRange.instance.instanceId] = placement.top; // from top of container + if (seg.firstCol === seg.lastCol && seg.isStart && seg.isEnd) { // TODO: simpler way? NOT DRY + segMarginTops[seg.eventRange.instance.instanceId] = + placement.top - currentNonAbsBottom; // from previous seg bottom + currentAbsHeight = 0; + currentNonAbsBottom = placement.bottom; + } + else { // multi-col event, abs positioned + currentAbsHeight = placement.bottom - currentNonAbsBottom; + } + } + } + if (currentAbsHeight) { + if (moreCnts[col]) { + moreTops[col] = currentAbsHeight; + } + else { + paddingBottoms[col] = currentAbsHeight; + } + } + } + function placeSeg(seg, segHeight) { + if (!tryPlaceSegAt(seg, segHeight, 0)) { + for (var col = seg.firstCol; col <= seg.lastCol; col += 1) { + for (var _i = 0, _a = colPlacements[col]; _i < _a.length; _i++) { // will repeat multi-day segs!!!!!!! bad!!!!!! + var placement = _a[_i]; + if (tryPlaceSegAt(seg, segHeight, placement.bottom)) { + return; + } + } + } + } + } + function tryPlaceSegAt(seg, segHeight, top) { + if (canPlaceSegAt(seg, segHeight, top)) { + for (var col = seg.firstCol; col <= seg.lastCol; col += 1) { + var placements = colPlacements[col]; + var insertionIndex = 0; + while (insertionIndex < placements.length && + top >= placements[insertionIndex].top) { + insertionIndex += 1; + } + placements.splice(insertionIndex, 0, { + seg: seg, + top: top, + bottom: top + segHeight, + }); + } + return true; + } + return false; + } + function canPlaceSegAt(seg, segHeight, top) { + for (var col = seg.firstCol; col <= seg.lastCol; col += 1) { + for (var _i = 0, _a = colPlacements[col]; _i < _a.length; _i++) { + var placement = _a[_i]; + if (top < placement.bottom && top + segHeight > placement.top) { // collide? + return false; + } + } + } + return true; + } + // what does this do!? + for (var instanceIdAndFirstCol in eventHeights) { + if (!eventHeights[instanceIdAndFirstCol]) { + segIsHidden[instanceIdAndFirstCol.split(':')[0]] = true; + } + } + var segsByFirstCol = colPlacements.map(extractFirstColSegs); // operates on the sorted cols + var segsByEachCol = colPlacements.map(function (placements, col) { + var segsForCols = extractAllColSegs(placements); + segsForCols = resliceDaySegs(segsForCols, cellModels[col].date, col); + return segsForCols; + }); + return { + segsByFirstCol: segsByFirstCol, + segsByEachCol: segsByEachCol, + segIsHidden: segIsHidden, + segTops: segTops, + segMarginTops: segMarginTops, + moreCnts: moreCnts, + moreTops: moreTops, + paddingBottoms: paddingBottoms, + }; + } + function extractFirstColSegs(oneColPlacements, col) { + var segs = []; + for (var _i = 0, oneColPlacements_1 = oneColPlacements; _i < oneColPlacements_1.length; _i++) { + var placement = oneColPlacements_1[_i]; + if (placement.seg.firstCol === col) { + segs.push(placement.seg); + } + } + return segs; + } + function extractAllColSegs(oneColPlacements) { + var segs = []; + for (var _i = 0, oneColPlacements_2 = oneColPlacements; _i < oneColPlacements_2.length; _i++) { + var placement = oneColPlacements_2[_i]; + segs.push(placement.seg); + } + return segs; + } + function limitByMaxHeight(hiddenCnts, segIsHidden, colPlacements, maxContentHeight) { + limitEvents(hiddenCnts, segIsHidden, colPlacements, true, function (placement) { return placement.bottom <= maxContentHeight; }); + } + function limitByMaxEvents(hiddenCnts, segIsHidden, colPlacements, dayMaxEvents) { + limitEvents(hiddenCnts, segIsHidden, colPlacements, false, function (placement, levelIndex) { return levelIndex < dayMaxEvents; }); + } + function limitByMaxRows(hiddenCnts, segIsHidden, colPlacements, dayMaxEventRows) { + limitEvents(hiddenCnts, segIsHidden, colPlacements, true, function (placement, levelIndex) { return levelIndex < dayMaxEventRows; }); + } + /* + populates the given hiddenCnts/segIsHidden, which are supplied empty. + TODO: return them instead + */ + function limitEvents(hiddenCnts, segIsHidden, colPlacements, _moreLinkConsumesLevel, isPlacementInBounds) { + var colCnt = hiddenCnts.length; + var segIsVisible = {}; // TODO: instead, use segIsHidden with true/false? + var visibleColPlacements = []; // will mirror colPlacements + for (var col = 0; col < colCnt; col += 1) { + visibleColPlacements.push([]); + } + for (var col = 0; col < colCnt; col += 1) { + var placements = colPlacements[col]; + var level = 0; + for (var _i = 0, placements_2 = placements; _i < placements_2.length; _i++) { + var placement = placements_2[_i]; + if (isPlacementInBounds(placement, level)) { + recordVisible(placement); + } + else { + recordHidden(placement, level, _moreLinkConsumesLevel); + } + // only considered a level if the seg had height + if (placement.top !== placement.bottom) { + level += 1; + } + } + } + function recordVisible(placement) { + var seg = placement.seg; + var instanceId = seg.eventRange.instance.instanceId; + if (!segIsVisible[instanceId]) { + segIsVisible[instanceId] = true; + for (var col = seg.firstCol; col <= seg.lastCol; col += 1) { + var destPlacements = visibleColPlacements[col]; + var newPosition = 0; + // insert while keeping top sorted in each column + while (newPosition < destPlacements.length && + placement.top >= destPlacements[newPosition].top) { + newPosition += 1; + } + destPlacements.splice(newPosition, 0, placement); + } + } + } + function recordHidden(placement, currentLevel, moreLinkConsumesLevel) { + var seg = placement.seg; + var instanceId = seg.eventRange.instance.instanceId; + if (!segIsHidden[instanceId]) { + segIsHidden[instanceId] = true; + for (var col = seg.firstCol; col <= seg.lastCol; col += 1) { + hiddenCnts[col] += 1; + var hiddenCnt = hiddenCnts[col]; + if (moreLinkConsumesLevel && hiddenCnt === 1 && currentLevel > 0) { + var doomedLevel = currentLevel - 1; + while (visibleColPlacements[col].length > doomedLevel) { + recordHidden(visibleColPlacements[col].pop(), // removes + visibleColPlacements[col].length, // will execute after the pop. will be the index of the removed placement + false); + } + } + } + } + } + } + // Given the events within an array of segment objects, reslice them to be in a single day + function resliceDaySegs(segs, dayDate, colIndex) { + var dayStart = dayDate; + var dayEnd = addDays(dayStart, 1); + var dayRange = { start: dayStart, end: dayEnd }; + var newSegs = []; + for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) { + var seg = segs_2[_i]; + var eventRange = seg.eventRange; + var origRange = eventRange.range; + var slicedRange = intersectRanges(origRange, dayRange); + if (slicedRange) { + newSegs.push(__assign(__assign({}, seg), { firstCol: colIndex, lastCol: colIndex, eventRange: { + def: eventRange.def, + ui: __assign(__assign({}, eventRange.ui), { durationEditable: false }), + instance: eventRange.instance, + range: slicedRange, + }, isStart: seg.isStart && slicedRange.start.valueOf() === origRange.start.valueOf(), isEnd: seg.isEnd && slicedRange.end.valueOf() === origRange.end.valueOf() })); + } + } + return newSegs; + } + + var TableRow = /** @class */ (function (_super) { + __extends(TableRow, _super); + function TableRow() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.cellElRefs = new RefMap(); // the ? + createElement("tr", { className: "fc-scrollgrid-section" }, + createElement("td", { className: 'fc-timegrid-divider ' + context.theme.getClass('tableCellShaded') }))), + }); + } + sections.push({ + type: 'body', + key: 'body', + liquid: true, + expandRows: Boolean(context.options.expandRows), + chunk: { + scrollerElRef: this.scrollerElRef, + content: timeContent, + }, + }); + return (createElement(ViewRoot, { viewSpec: context.viewSpec, elRef: this.rootElRef }, function (rootElRef, classNames) { return (createElement("div", { className: ['fc-timegrid'].concat(classNames).join(' '), ref: rootElRef }, + createElement(SimpleScrollGrid, { liquid: !props.isHeightAuto && !props.forPrint, cols: [{ width: 'shrink' }], sections: sections }))); })); + }; + TimeColsView.prototype.renderHScrollLayout = function (headerRowContent, allDayContent, timeContent, colCnt, dayMinWidth, slatMetas, slatCoords) { + var _this = this; + var ScrollGrid = this.context.pluginHooks.scrollGridImpl; + if (!ScrollGrid) { + throw new Error('No ScrollGrid implementation'); + } + var _a = this, context = _a.context, props = _a.props; + var stickyHeaderDates = !props.forPrint && getStickyHeaderDates(context.options); + var stickyFooterScrollbar = !props.forPrint && getStickyFooterScrollbar(context.options); + var sections = []; + if (headerRowContent) { + sections.push({ + type: 'header', + key: 'header', + isSticky: stickyHeaderDates, + syncRowHeights: true, + chunks: [ + { + key: 'axis', + rowContent: function (arg) { return (createElement("tr", null, _this.renderHeadAxis('day', arg.rowSyncHeights[0]))); }, + }, + { + key: 'cols', + elRef: this.headerElRef, + tableClassName: 'fc-col-header', + rowContent: headerRowContent, + }, + ], + }); + } + if (allDayContent) { + sections.push({ + type: 'body', + key: 'all-day', + syncRowHeights: true, + chunks: [ + { + key: 'axis', + rowContent: function (contentArg) { return (createElement("tr", null, _this.renderTableRowAxis(contentArg.rowSyncHeights[0]))); }, + }, + { + key: 'cols', + content: allDayContent, + }, + ], + }); + sections.push({ + key: 'all-day-divider', + type: 'body', + outerContent: ( // TODO: rename to cellContent so don't need to define ? + createElement("tr", { className: "fc-scrollgrid-section" }, + createElement("td", { colSpan: 2, className: 'fc-timegrid-divider ' + context.theme.getClass('tableCellShaded') }))), + }); + } + var isNowIndicator = context.options.nowIndicator; + sections.push({ + type: 'body', + key: 'body', + liquid: true, + expandRows: Boolean(context.options.expandRows), + chunks: [ + { + key: 'axis', + content: function (arg) { return ( + // TODO: make this now-indicator arrow more DRY with TimeColsContent + createElement("div", { className: "fc-timegrid-axis-chunk" }, + createElement("table", { style: { height: arg.expandRows ? arg.clientHeight : '' } }, + arg.tableColGroupNode, + createElement("tbody", null, + createElement(TimeBodyAxis, { slatMetas: slatMetas }))), + createElement("div", { className: "fc-timegrid-now-indicator-container" }, + createElement(NowTimer, { unit: isNowIndicator ? 'minute' : 'day' /* hacky */ }, function (nowDate) { + var nowIndicatorTop = isNowIndicator && + slatCoords && + slatCoords.safeComputeTop(nowDate); // might return void + if (typeof nowIndicatorTop === 'number') { + return (createElement(NowIndicatorRoot, { isAxis: true, date: nowDate }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("div", { ref: rootElRef, className: ['fc-timegrid-now-indicator-arrow'].concat(classNames).join(' '), style: { top: nowIndicatorTop } }, innerContent)); })); + } + return null; + })))); }, + }, + { + key: 'cols', + scrollerElRef: this.scrollerElRef, + content: timeContent, + }, + ], + }); + if (stickyFooterScrollbar) { + sections.push({ + key: 'footer', + type: 'footer', + isSticky: true, + chunks: [ + { + key: 'axis', + content: renderScrollShim, + }, + { + key: 'cols', + content: renderScrollShim, + }, + ], + }); + } + return (createElement(ViewRoot, { viewSpec: context.viewSpec, elRef: this.rootElRef }, function (rootElRef, classNames) { return (createElement("div", { className: ['fc-timegrid'].concat(classNames).join(' '), ref: rootElRef }, + createElement(ScrollGrid, { liquid: !props.isHeightAuto && !props.forPrint, colGroups: [ + { width: 'shrink', cols: [{ width: 'shrink' }] }, + { cols: [{ span: colCnt, minWidth: dayMinWidth }] }, + ], sections: sections }))); })); + }; + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + TimeColsView.prototype.getAllDayMaxEventProps = function () { + var _a = this.context.options, dayMaxEvents = _a.dayMaxEvents, dayMaxEventRows = _a.dayMaxEventRows; + if (dayMaxEvents === true || dayMaxEventRows === true) { // is auto? + dayMaxEvents = undefined; + dayMaxEventRows = AUTO_ALL_DAY_MAX_EVENT_ROWS; // make sure "auto" goes to a real number + } + return { dayMaxEvents: dayMaxEvents, dayMaxEventRows: dayMaxEventRows }; + }; + return TimeColsView; + }(DateComponent)); + function renderAllDayInner(hookProps) { + return hookProps.text; + } + + var TimeColsSlatsCoords = /** @class */ (function () { + function TimeColsSlatsCoords(positions, dateProfile, slatMetas) { + this.positions = positions; + this.dateProfile = dateProfile; + this.slatMetas = slatMetas; + } + TimeColsSlatsCoords.prototype.safeComputeTop = function (date) { + var dateProfile = this.dateProfile; + if (rangeContainsMarker(dateProfile.currentRange, date)) { + var startOfDayDate = startOfDay(date); + var timeMs = date.valueOf() - startOfDayDate.valueOf(); + if (timeMs >= asRoughMs(dateProfile.slotMinTime) && + timeMs < asRoughMs(dateProfile.slotMaxTime)) { + return this.computeTimeTop(createDuration(timeMs)); + } + } + return null; + }; + // Computes the top coordinate, relative to the bounds of the grid, of the given date. + // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight. + TimeColsSlatsCoords.prototype.computeDateTop = function (when, startOfDayDate) { + if (!startOfDayDate) { + startOfDayDate = startOfDay(when); + } + return this.computeTimeTop(createDuration(when.valueOf() - startOfDayDate.valueOf())); + }; + // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). + // This is a makeshify way to compute the time-top. Assumes all slatMetas dates are uniform. + // Eventually allow computation with arbirary slat dates. + TimeColsSlatsCoords.prototype.computeTimeTop = function (duration) { + var _a = this, positions = _a.positions, dateProfile = _a.dateProfile, slatMetas = _a.slatMetas; + var len = positions.els.length; + // we assume dates are uniform + var slotDurationMs = slatMetas[1].date.valueOf() - slatMetas[0].date.valueOf(); + // floating-point value of # of slots covered + var slatCoverage = (duration.milliseconds - asRoughMs(dateProfile.slotMinTime)) / slotDurationMs; + var slatIndex; + var slatRemainder; + // compute a floating-point number for how many slats should be progressed through. + // from 0 to number of slats (inclusive) + // constrained because slotMinTime/slotMaxTime might be customized. + slatCoverage = Math.max(0, slatCoverage); + slatCoverage = Math.min(len, slatCoverage); + // an integer index of the furthest whole slat + // from 0 to number slats (*exclusive*, so len-1) + slatIndex = Math.floor(slatCoverage); + slatIndex = Math.min(slatIndex, len - 1); + // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition. + // could be 1.0 if slatCoverage is covering *all* the slots + slatRemainder = slatCoverage - slatIndex; + return positions.tops[slatIndex] + + positions.getHeight(slatIndex) * slatRemainder; + }; + return TimeColsSlatsCoords; + }()); + + var TimeColsSlatsBody = /** @class */ (function (_super) { + __extends(TimeColsSlatsBody, _super); + function TimeColsSlatsBody() { + return _super !== null && _super.apply(this, arguments) || this; + } + TimeColsSlatsBody.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context; + var options = context.options; + var slatElRefs = props.slatElRefs; + return (createElement("tbody", null, props.slatMetas.map(function (slatMeta, i) { + var hookProps = { + time: slatMeta.time, + date: context.dateEnv.toDate(slatMeta.date), + view: context.viewApi, + }; + var classNames = [ + 'fc-timegrid-slot', + 'fc-timegrid-slot-lane', + slatMeta.isLabeled ? '' : 'fc-timegrid-slot-minor', + ]; + return (createElement("tr", { key: slatMeta.key, ref: slatElRefs.createRef(slatMeta.key) }, + props.axis && (createElement(TimeColsAxisCell, __assign({}, slatMeta))), + createElement(RenderHook, { hookProps: hookProps, classNames: options.slotLaneClassNames, content: options.slotLaneContent, didMount: options.slotLaneDidMount, willUnmount: options.slotLaneWillUnmount }, function (rootElRef, customClassNames, innerElRef, innerContent) { return (createElement("td", { ref: rootElRef, className: classNames.concat(customClassNames).join(' '), "data-time": slatMeta.isoTimeStr }, innerContent)); }))); + }))); + }; + return TimeColsSlatsBody; + }(BaseComponent)); + + /* + for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. + */ + var TimeColsSlats = /** @class */ (function (_super) { + __extends(TimeColsSlats, _super); + function TimeColsSlats() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.rootElRef = createRef(); + _this.slatElRefs = new RefMap(); + return _this; + } + TimeColsSlats.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context; + return (createElement("div", { className: "fc-timegrid-slots", ref: this.rootElRef }, + createElement("table", { className: context.theme.getClass('table'), style: { + minWidth: props.tableMinWidth, + width: props.clientWidth, + height: props.minHeight, + } }, + props.tableColGroupNode /* relies on there only being a single for the axis */, + createElement(TimeColsSlatsBody, { slatElRefs: this.slatElRefs, axis: props.axis, slatMetas: props.slatMetas })))); + }; + TimeColsSlats.prototype.componentDidMount = function () { + this.updateSizing(); + }; + TimeColsSlats.prototype.componentDidUpdate = function () { + this.updateSizing(); + }; + TimeColsSlats.prototype.componentWillUnmount = function () { + if (this.props.onCoords) { + this.props.onCoords(null); + } + }; + TimeColsSlats.prototype.updateSizing = function () { + var props = this.props; + if (props.onCoords && + props.clientWidth !== null // means sizing has stabilized + ) { + var rootEl = this.rootElRef.current; + if (rootEl.offsetHeight) { // not hidden by css + props.onCoords(new TimeColsSlatsCoords(new PositionCache(this.rootElRef.current, collectSlatEls(this.slatElRefs.currentMap, props.slatMetas), false, true), this.props.dateProfile, props.slatMetas)); + } + } + }; + return TimeColsSlats; + }(BaseComponent)); + function collectSlatEls(elMap, slatMetas) { + return slatMetas.map(function (slatMeta) { return elMap[slatMeta.key]; }); + } + + function splitSegsByCol(segs, colCnt) { + var segsByCol = []; + var i; + for (i = 0; i < colCnt; i += 1) { + segsByCol.push([]); + } + if (segs) { + for (i = 0; i < segs.length; i += 1) { + segsByCol[segs[i].col].push(segs[i]); + } + } + return segsByCol; + } + function splitInteractionByCol(ui, colCnt) { + var byRow = []; + if (!ui) { + for (var i = 0; i < colCnt; i += 1) { + byRow[i] = null; + } + } + else { + for (var i = 0; i < colCnt; i += 1) { + byRow[i] = { + affectedInstances: ui.affectedInstances, + isEvent: ui.isEvent, + segs: [], + }; + } + for (var _i = 0, _a = ui.segs; _i < _a.length; _i++) { + var seg = _a[_i]; + byRow[seg.col].segs.push(seg); + } + } + return byRow; + } + + // UNFORTUNATELY, assigns results to the top/bottom/level/forwardCoord/backwardCoord props of the actual segs. + // TODO: return hash (by instanceId) of results + function computeSegCoords(segs, dayDate, slatCoords, eventMinHeight, eventOrderSpecs) { + computeSegVerticals(segs, dayDate, slatCoords, eventMinHeight); + return computeSegHorizontals(segs, eventOrderSpecs); // requires top/bottom from computeSegVerticals + } + // For each segment in an array, computes and assigns its top and bottom properties + function computeSegVerticals(segs, dayDate, slatCoords, eventMinHeight) { + for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) { + var seg = segs_1[_i]; + seg.top = slatCoords.computeDateTop(seg.start, dayDate); + seg.bottom = Math.max(seg.top + (eventMinHeight || 0), // yuck + slatCoords.computeDateTop(seg.end, dayDate)); + } + } + // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. + // Assumed the segs are already ordered. + // NOTE: Also reorders the given array by date! + function computeSegHorizontals(segs, eventOrderSpecs) { + // IMPORTANT TO CLEAR OLD RESULTS :( + for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) { + var seg = segs_2[_i]; + seg.level = null; + seg.forwardCoord = null; + seg.backwardCoord = null; + seg.forwardPressure = null; + } + segs = sortEventSegs(segs, eventOrderSpecs); + var level0; + var levels = buildSlotSegLevels(segs); + computeForwardSlotSegs(levels); + if ((level0 = levels[0])) { + for (var _a = 0, level0_1 = level0; _a < level0_1.length; _a++) { + var seg = level0_1[_a]; + computeSlotSegPressures(seg); + } + for (var _b = 0, level0_2 = level0; _b < level0_2.length; _b++) { + var seg = level0_2[_b]; + computeSegForwardBack(seg, 0, 0, eventOrderSpecs); + } + } + return segs; + } + // Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is + // left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date. + function buildSlotSegLevels(segs) { + var levels = []; + var i; + var seg; + var j; + for (i = 0; i < segs.length; i += 1) { + seg = segs[i]; + // go through all the levels and stop on the first level where there are no collisions + for (j = 0; j < levels.length; j += 1) { + if (!computeSlotSegCollisions(seg, levels[j]).length) { + break; + } + } + seg.level = j; + (levels[j] || (levels[j] = [])).push(seg); + } + return levels; + } + // Find all the segments in `otherSegs` that vertically collide with `seg`. + // Append into an optionally-supplied `results` array and return. + function computeSlotSegCollisions(seg, otherSegs, results) { + if (results === void 0) { results = []; } + for (var i = 0; i < otherSegs.length; i += 1) { + if (isSlotSegCollision(seg, otherSegs[i])) { + results.push(otherSegs[i]); + } + } + return results; + } + // Do these segments occupy the same vertical space? + function isSlotSegCollision(seg1, seg2) { + return seg1.bottom > seg2.top && seg1.top < seg2.bottom; + } + // For every segment, figure out the other segments that are in subsequent + // levels that also occupy the same vertical space. Accumulate in seg.forwardSegs + function computeForwardSlotSegs(levels) { + var i; + var level; + var j; + var seg; + var k; + for (i = 0; i < levels.length; i += 1) { + level = levels[i]; + for (j = 0; j < level.length; j += 1) { + seg = level[j]; + seg.forwardSegs = []; + for (k = i + 1; k < levels.length; k += 1) { + computeSlotSegCollisions(seg, levels[k], seg.forwardSegs); + } + } + } + } + // Figure out which path forward (via seg.forwardSegs) results in the longest path until + // the furthest edge is reached. The number of segments in this path will be seg.forwardPressure + function computeSlotSegPressures(seg) { + var forwardSegs = seg.forwardSegs; + var forwardPressure = 0; + var i; + var forwardSeg; + if (seg.forwardPressure == null) { // not already computed + for (i = 0; i < forwardSegs.length; i += 1) { + forwardSeg = forwardSegs[i]; + // figure out the child's maximum forward path + computeSlotSegPressures(forwardSeg); + // either use the existing maximum, or use the child's forward pressure + // plus one (for the forwardSeg itself) + forwardPressure = Math.max(forwardPressure, 1 + forwardSeg.forwardPressure); + } + seg.forwardPressure = forwardPressure; + } + } + // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range + // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and + // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left. + // + // The segment might be part of a "series", which means consecutive segments with the same pressure + // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of + // segments behind this one in the current series, and `seriesBackwardCoord` is the starting + // coordinate of the first segment in the series. + function computeSegForwardBack(seg, seriesBackwardPressure, seriesBackwardCoord, eventOrderSpecs) { + var forwardSegs = seg.forwardSegs; + var i; + if (seg.forwardCoord == null) { // not already computed + if (!forwardSegs.length) { + // if there are no forward segments, this segment should butt up against the edge + seg.forwardCoord = 1; + } + else { + // sort highest pressure first + sortForwardSegs(forwardSegs, eventOrderSpecs); + // this segment's forwardCoord will be calculated from the backwardCoord of the + // highest-pressure forward segment. + computeSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord, eventOrderSpecs); + seg.forwardCoord = forwardSegs[0].backwardCoord; + } + // calculate the backwardCoord from the forwardCoord. consider the series + seg.backwardCoord = seg.forwardCoord - + (seg.forwardCoord - seriesBackwardCoord) / // available width for series + (seriesBackwardPressure + 1); // # of segments in the series + // use this segment's coordinates to computed the coordinates of the less-pressurized + // forward segments + for (i = 0; i < forwardSegs.length; i += 1) { + computeSegForwardBack(forwardSegs[i], 0, seg.forwardCoord, eventOrderSpecs); + } + } + } + function sortForwardSegs(forwardSegs, eventOrderSpecs) { + var objs = forwardSegs.map(buildTimeGridSegCompareObj); + var specs = [ + // put higher-pressure first + { field: 'forwardPressure', order: -1 }, + // put segments that are closer to initial edge first (and favor ones with no coords yet) + { field: 'backwardCoord', order: 1 }, + ].concat(eventOrderSpecs); + objs.sort(function (obj0, obj1) { return compareByFieldSpecs(obj0, obj1, specs); }); + return objs.map(function (c) { return c._seg; }); + } + function buildTimeGridSegCompareObj(seg) { + var obj = buildSegCompareObj(seg); + obj.forwardPressure = seg.forwardPressure; + obj.backwardCoord = seg.backwardCoord; + return obj; + } + + var DEFAULT_TIME_FORMAT = createFormatter({ + hour: 'numeric', + minute: '2-digit', + meridiem: false, + }); + var TimeColEvent = /** @class */ (function (_super) { + __extends(TimeColEvent, _super); + function TimeColEvent() { + return _super !== null && _super.apply(this, arguments) || this; + } + TimeColEvent.prototype.render = function () { + var classNames = [ + 'fc-timegrid-event', + 'fc-v-event', + ]; + if (this.props.isCondensed) { + classNames.push('fc-timegrid-event-condensed'); + } + return (createElement(StandardEvent, __assign({}, this.props, { defaultTimeFormat: DEFAULT_TIME_FORMAT, extraClassNames: classNames }))); + }; + return TimeColEvent; + }(BaseComponent)); + + var TimeColMisc = /** @class */ (function (_super) { + __extends(TimeColMisc, _super); + function TimeColMisc() { + return _super !== null && _super.apply(this, arguments) || this; + } + TimeColMisc.prototype.render = function () { + var props = this.props; + return (createElement(DayCellContent, { date: props.date, dateProfile: props.dateProfile, todayRange: props.todayRange, extraHookProps: props.extraHookProps }, function (innerElRef, innerContent) { return (innerContent && + createElement("div", { className: "fc-timegrid-col-misc", ref: innerElRef }, innerContent)); })); + }; + return TimeColMisc; + }(BaseComponent)); + + config.timeGridEventCondensedHeight = 30; + var TimeCol = /** @class */ (function (_super) { + __extends(TimeCol, _super); + function TimeCol() { + return _super !== null && _super.apply(this, arguments) || this; + } + TimeCol.prototype.render = function () { + var _this = this; + var _a = this, props = _a.props, context = _a.context; + var isSelectMirror = context.options.selectMirror; + var mirrorSegs = (props.eventDrag && props.eventDrag.segs) || + (props.eventResize && props.eventResize.segs) || + (isSelectMirror && props.dateSelectionSegs) || + []; + var interactionAffectedInstances = // TODO: messy way to compute this + (props.eventDrag && props.eventDrag.affectedInstances) || + (props.eventResize && props.eventResize.affectedInstances) || + {}; + return (createElement(DayCellRoot, { elRef: props.elRef, date: props.date, dateProfile: props.dateProfile, todayRange: props.todayRange, extraHookProps: props.extraHookProps }, function (rootElRef, classNames, dataAttrs) { return (createElement("td", __assign({ ref: rootElRef, className: ['fc-timegrid-col'].concat(classNames, props.extraClassNames || []).join(' ') }, dataAttrs, props.extraDataAttrs), + createElement("div", { className: "fc-timegrid-col-frame" }, + createElement("div", { className: "fc-timegrid-col-bg" }, + _this.renderFillSegs(props.businessHourSegs, 'non-business'), + _this.renderFillSegs(props.bgEventSegs, 'bg-event'), + _this.renderFillSegs(props.dateSelectionSegs, 'highlight')), + createElement("div", { className: "fc-timegrid-col-events" }, _this.renderFgSegs(props.fgEventSegs, interactionAffectedInstances)), + createElement("div", { className: "fc-timegrid-col-events" }, _this.renderFgSegs(mirrorSegs, {}, Boolean(props.eventDrag), Boolean(props.eventResize), Boolean(isSelectMirror))), + createElement("div", { className: "fc-timegrid-now-indicator-container" }, _this.renderNowIndicator(props.nowIndicatorSegs)), + createElement(TimeColMisc, { date: props.date, dateProfile: props.dateProfile, todayRange: props.todayRange, extraHookProps: props.extraHookProps })))); })); + }; + TimeCol.prototype.renderFgSegs = function (segs, segIsInvisible, isDragging, isResizing, isDateSelecting) { + var props = this.props; + if (props.forPrint) { + return this.renderPrintFgSegs(segs); + } + if (props.slatCoords) { + return this.renderPositionedFgSegs(segs, segIsInvisible, isDragging, isResizing, isDateSelecting); + } + return null; + }; + TimeCol.prototype.renderPrintFgSegs = function (segs) { + var _a = this, props = _a.props, context = _a.context; + // not DRY + segs = sortEventSegs(segs, context.options.eventOrder); + return segs.map(function (seg) { return (createElement("div", { className: "fc-timegrid-event-harness", key: seg.eventRange.instance.instanceId }, + createElement(TimeColEvent, __assign({ seg: seg, isDragging: false, isResizing: false, isDateSelecting: false, isSelected: false, isCondensed: false }, getSegMeta(seg, props.todayRange, props.nowDate))))); }); + }; + TimeCol.prototype.renderPositionedFgSegs = function (segs, segIsInvisible, isDragging, isResizing, isDateSelecting) { + var _this = this; + var _a = this, context = _a.context, props = _a.props; + // assigns TO THE SEGS THEMSELVES + // also, receives resorted array + segs = computeSegCoords(segs, props.date, props.slatCoords, context.options.eventMinHeight, context.options.eventOrder); + return segs.map(function (seg) { + var instanceId = seg.eventRange.instance.instanceId; + var isMirror = isDragging || isResizing || isDateSelecting; + var positionCss = isMirror + // will span entire column width + // also, won't assign z-index, which is good, fc-event-mirror will overpower other harnesses + ? __assign({ left: 0, right: 0 }, _this.computeSegTopBottomCss(seg)) : _this.computeFgSegPositionCss(seg); + return (createElement("div", { className: 'fc-timegrid-event-harness' + (seg.level > 0 ? ' fc-timegrid-event-harness-inset' : ''), key: instanceId, style: __assign({ visibility: segIsInvisible[instanceId] ? 'hidden' : '' }, positionCss) }, + createElement(TimeColEvent, __assign({ seg: seg, isDragging: isDragging, isResizing: isResizing, isDateSelecting: isDateSelecting, isSelected: instanceId === props.eventSelection, isCondensed: (seg.bottom - seg.top) < config.timeGridEventCondensedHeight }, getSegMeta(seg, props.todayRange, props.nowDate))))); + }); + }; + TimeCol.prototype.renderFillSegs = function (segs, fillType) { + var _this = this; + var _a = this, context = _a.context, props = _a.props; + if (!props.slatCoords) { + return null; + } + // BAD: assigns TO THE SEGS THEMSELVES + computeSegVerticals(segs, props.date, props.slatCoords, context.options.eventMinHeight); + var children = segs.map(function (seg) { return (createElement("div", { key: buildEventRangeKey(seg.eventRange), className: "fc-timegrid-bg-harness", style: _this.computeSegTopBottomCss(seg) }, fillType === 'bg-event' ? + createElement(BgEvent, __assign({ seg: seg }, getSegMeta(seg, props.todayRange, props.nowDate))) : + renderFill(fillType))); }); + return createElement(Fragment, null, children); + }; + TimeCol.prototype.renderNowIndicator = function (segs) { + var _a = this.props, slatCoords = _a.slatCoords, date = _a.date; + if (!slatCoords) { + return null; + } + return segs.map(function (seg, i) { return (createElement(NowIndicatorRoot, { isAxis: false, date: date, + // key doesn't matter. will only ever be one + key: i }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("div", { ref: rootElRef, className: ['fc-timegrid-now-indicator-line'].concat(classNames).join(' '), style: { top: slatCoords.computeDateTop(seg.start, date) } }, innerContent)); })); }); + }; + TimeCol.prototype.computeFgSegPositionCss = function (seg) { + var _a = this.context, isRtl = _a.isRtl, options = _a.options; + var shouldOverlap = options.slotEventOverlap; + var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point + var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point + var left; // amount of space from left edge, a fraction of the total width + var right; // amount of space from right edge, a fraction of the total width + if (shouldOverlap) { + // double the width, but don't go beyond the maximum forward coordinate (1.0) + forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2); + } + if (isRtl) { + left = 1 - forwardCoord; + right = backwardCoord; + } + else { + left = backwardCoord; + right = 1 - forwardCoord; + } + var props = { + zIndex: seg.level + 1, + left: left * 100 + '%', + right: right * 100 + '%', + }; + if (shouldOverlap && seg.forwardPressure) { + // add padding to the edge so that forward stacked events don't cover the resizer's icon + props[isRtl ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width + } + return __assign(__assign({}, props), this.computeSegTopBottomCss(seg)); + }; + TimeCol.prototype.computeSegTopBottomCss = function (seg) { + return { + top: seg.top, + bottom: -seg.bottom, + }; + }; + return TimeCol; + }(BaseComponent)); + + var TimeColsContent = /** @class */ (function (_super) { + __extends(TimeColsContent, _super); + function TimeColsContent() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.splitFgEventSegs = memoize(splitSegsByCol); + _this.splitBgEventSegs = memoize(splitSegsByCol); + _this.splitBusinessHourSegs = memoize(splitSegsByCol); + _this.splitNowIndicatorSegs = memoize(splitSegsByCol); + _this.splitDateSelectionSegs = memoize(splitSegsByCol); + _this.splitEventDrag = memoize(splitInteractionByCol); + _this.splitEventResize = memoize(splitInteractionByCol); + _this.rootElRef = createRef(); + _this.cellElRefs = new RefMap(); + return _this; + } + TimeColsContent.prototype.render = function () { + var _this = this; + var _a = this, props = _a.props, context = _a.context; + var nowIndicatorTop = context.options.nowIndicator && + props.slatCoords && + props.slatCoords.safeComputeTop(props.nowDate); // might return void + var colCnt = props.cells.length; + var fgEventSegsByRow = this.splitFgEventSegs(props.fgEventSegs, colCnt); + var bgEventSegsByRow = this.splitBgEventSegs(props.bgEventSegs, colCnt); + var businessHourSegsByRow = this.splitBusinessHourSegs(props.businessHourSegs, colCnt); + var nowIndicatorSegsByRow = this.splitNowIndicatorSegs(props.nowIndicatorSegs, colCnt); + var dateSelectionSegsByRow = this.splitDateSelectionSegs(props.dateSelectionSegs, colCnt); + var eventDragByRow = this.splitEventDrag(props.eventDrag, colCnt); + var eventResizeByRow = this.splitEventResize(props.eventResize, colCnt); + return (createElement("div", { className: "fc-timegrid-cols", ref: this.rootElRef }, + createElement("table", { style: { + minWidth: props.tableMinWidth, + width: props.clientWidth, + } }, + props.tableColGroupNode, + createElement("tbody", null, + createElement("tr", null, + props.axis && (createElement("td", { className: "fc-timegrid-col fc-timegrid-axis" }, + createElement("div", { className: "fc-timegrid-col-frame" }, + createElement("div", { className: "fc-timegrid-now-indicator-container" }, typeof nowIndicatorTop === 'number' && (createElement(NowIndicatorRoot, { isAxis: true, date: props.nowDate }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("div", { ref: rootElRef, className: ['fc-timegrid-now-indicator-arrow'].concat(classNames).join(' '), style: { top: nowIndicatorTop } }, innerContent)); })))))), + props.cells.map(function (cell, i) { return (createElement(TimeCol, { key: cell.key, elRef: _this.cellElRefs.createRef(cell.key), dateProfile: props.dateProfile, date: cell.date, nowDate: props.nowDate, todayRange: props.todayRange, extraHookProps: cell.extraHookProps, extraDataAttrs: cell.extraDataAttrs, extraClassNames: cell.extraClassNames, fgEventSegs: fgEventSegsByRow[i], bgEventSegs: bgEventSegsByRow[i], businessHourSegs: businessHourSegsByRow[i], nowIndicatorSegs: nowIndicatorSegsByRow[i], dateSelectionSegs: dateSelectionSegsByRow[i], eventDrag: eventDragByRow[i], eventResize: eventResizeByRow[i], slatCoords: props.slatCoords, eventSelection: props.eventSelection, forPrint: props.forPrint })); })))))); + }; + TimeColsContent.prototype.componentDidMount = function () { + this.updateCoords(); + }; + TimeColsContent.prototype.componentDidUpdate = function () { + this.updateCoords(); + }; + TimeColsContent.prototype.updateCoords = function () { + var props = this.props; + if (props.onColCoords && + props.clientWidth !== null // means sizing has stabilized + ) { + props.onColCoords(new PositionCache(this.rootElRef.current, collectCellEls(this.cellElRefs.currentMap, props.cells), true, // horizontal + false)); + } + }; + return TimeColsContent; + }(BaseComponent)); + function collectCellEls(elMap, cells) { + return cells.map(function (cell) { return elMap[cell.key]; }); + } + + /* A component that renders one or more columns of vertical time slots + ----------------------------------------------------------------------------------------------------------------------*/ + var TimeCols = /** @class */ (function (_super) { + __extends(TimeCols, _super); + function TimeCols() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.processSlotOptions = memoize(processSlotOptions); + _this.state = { + slatCoords: null, + }; + _this.handleScrollRequest = function (request) { + var onScrollTopRequest = _this.props.onScrollTopRequest; + var slatCoords = _this.state.slatCoords; + if (onScrollTopRequest && slatCoords) { + if (request.time) { + var top_1 = slatCoords.computeTimeTop(request.time); + top_1 = Math.ceil(top_1); // zoom can give weird floating-point values. rather scroll a little bit further + if (top_1) { + top_1 += 1; // to overcome top border that slots beyond the first have. looks better + } + onScrollTopRequest(top_1); + } + return true; + } + return false; + }; + _this.handleColCoords = function (colCoords) { + _this.colCoords = colCoords; + }; + _this.handleSlatCoords = function (slatCoords) { + _this.setState({ slatCoords: slatCoords }); + if (_this.props.onSlatCoords) { + _this.props.onSlatCoords(slatCoords); + } + }; + return _this; + } + TimeCols.prototype.render = function () { + var _a = this, props = _a.props, state = _a.state; + return (createElement("div", { className: "fc-timegrid-body", ref: props.rootElRef, style: { + // these props are important to give this wrapper correct dimensions for interactions + // TODO: if we set it here, can we avoid giving to inner tables? + width: props.clientWidth, + minWidth: props.tableMinWidth, + } }, + createElement(TimeColsSlats, { axis: props.axis, dateProfile: props.dateProfile, slatMetas: props.slatMetas, clientWidth: props.clientWidth, minHeight: props.expandRows ? props.clientHeight : '', tableMinWidth: props.tableMinWidth, tableColGroupNode: props.axis ? props.tableColGroupNode : null /* axis depends on the colgroup's shrinking */, onCoords: this.handleSlatCoords }), + createElement(TimeColsContent, { cells: props.cells, axis: props.axis, dateProfile: props.dateProfile, businessHourSegs: props.businessHourSegs, bgEventSegs: props.bgEventSegs, fgEventSegs: props.fgEventSegs, dateSelectionSegs: props.dateSelectionSegs, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, todayRange: props.todayRange, nowDate: props.nowDate, nowIndicatorSegs: props.nowIndicatorSegs, clientWidth: props.clientWidth, tableMinWidth: props.tableMinWidth, tableColGroupNode: props.tableColGroupNode, slatCoords: state.slatCoords, onColCoords: this.handleColCoords, forPrint: props.forPrint }))); + }; + TimeCols.prototype.componentDidMount = function () { + this.scrollResponder = this.context.createScrollResponder(this.handleScrollRequest); + }; + TimeCols.prototype.componentDidUpdate = function (prevProps) { + this.scrollResponder.update(prevProps.dateProfile !== this.props.dateProfile); + }; + TimeCols.prototype.componentWillUnmount = function () { + this.scrollResponder.detach(); + }; + TimeCols.prototype.positionToHit = function (positionLeft, positionTop) { + var _a = this.context, dateEnv = _a.dateEnv, options = _a.options; + var colCoords = this.colCoords; + var dateProfile = this.props.dateProfile; + var slatCoords = this.state.slatCoords; + var _b = this.processSlotOptions(this.props.slotDuration, options.snapDuration), snapDuration = _b.snapDuration, snapsPerSlot = _b.snapsPerSlot; + var colIndex = colCoords.leftToIndex(positionLeft); + var slatIndex = slatCoords.positions.topToIndex(positionTop); + if (colIndex != null && slatIndex != null) { + var slatTop = slatCoords.positions.tops[slatIndex]; + var slatHeight = slatCoords.positions.getHeight(slatIndex); + var partial = (positionTop - slatTop) / slatHeight; // floating point number between 0 and 1 + var localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat + var snapIndex = slatIndex * snapsPerSlot + localSnapIndex; + var dayDate = this.props.cells[colIndex].date; + var time = addDurations(dateProfile.slotMinTime, multiplyDuration(snapDuration, snapIndex)); + var start = dateEnv.add(dayDate, time); + var end = dateEnv.add(start, snapDuration); + return { + col: colIndex, + dateSpan: { + range: { start: start, end: end }, + allDay: false, + }, + dayEl: colCoords.els[colIndex], + relativeRect: { + left: colCoords.lefts[colIndex], + right: colCoords.rights[colIndex], + top: slatTop, + bottom: slatTop + slatHeight, + }, + }; + } + return null; + }; + return TimeCols; + }(BaseComponent)); + function processSlotOptions(slotDuration, snapDurationOverride) { + var snapDuration = snapDurationOverride || slotDuration; + var snapsPerSlot = wholeDivideDurations(slotDuration, snapDuration); + if (snapsPerSlot === null) { + snapDuration = slotDuration; + snapsPerSlot = 1; + // TODO: say warning? + } + return { snapDuration: snapDuration, snapsPerSlot: snapsPerSlot }; + } + + var DayTimeColsSlicer = /** @class */ (function (_super) { + __extends(DayTimeColsSlicer, _super); + function DayTimeColsSlicer() { + return _super !== null && _super.apply(this, arguments) || this; + } + DayTimeColsSlicer.prototype.sliceRange = function (range, dayRanges) { + var segs = []; + for (var col = 0; col < dayRanges.length; col += 1) { + var segRange = intersectRanges(range, dayRanges[col]); + if (segRange) { + segs.push({ + start: segRange.start, + end: segRange.end, + isStart: segRange.start.valueOf() === range.start.valueOf(), + isEnd: segRange.end.valueOf() === range.end.valueOf(), + col: col, + }); + } + } + return segs; + }; + return DayTimeColsSlicer; + }(Slicer)); + + var DayTimeCols = /** @class */ (function (_super) { + __extends(DayTimeCols, _super); + function DayTimeCols() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.buildDayRanges = memoize(buildDayRanges); + _this.slicer = new DayTimeColsSlicer(); + _this.timeColsRef = createRef(); + _this.handleRootEl = function (rootEl) { + if (rootEl) { + _this.context.registerInteractiveComponent(_this, { el: rootEl }); + } + else { + _this.context.unregisterInteractiveComponent(_this); + } + }; + return _this; + } + DayTimeCols.prototype.render = function () { + var _this = this; + var _a = this, props = _a.props, context = _a.context; + var dateProfile = props.dateProfile, dayTableModel = props.dayTableModel; + var isNowIndicator = context.options.nowIndicator; + var dayRanges = this.buildDayRanges(dayTableModel, dateProfile, context.dateEnv); + // give it the first row of cells + // TODO: would move this further down hierarchy, but sliceNowDate needs it + return (createElement(NowTimer, { unit: isNowIndicator ? 'minute' : 'day' }, function (nowDate, todayRange) { return (createElement(TimeCols, __assign({ ref: _this.timeColsRef, rootElRef: _this.handleRootEl }, _this.slicer.sliceProps(props, dateProfile, null, context, dayRanges), { forPrint: props.forPrint, axis: props.axis, dateProfile: dateProfile, slatMetas: props.slatMetas, slotDuration: props.slotDuration, cells: dayTableModel.cells[0], tableColGroupNode: props.tableColGroupNode, tableMinWidth: props.tableMinWidth, clientWidth: props.clientWidth, clientHeight: props.clientHeight, expandRows: props.expandRows, nowDate: nowDate, nowIndicatorSegs: isNowIndicator && _this.slicer.sliceNowDate(nowDate, context, dayRanges), todayRange: todayRange, onScrollTopRequest: props.onScrollTopRequest, onSlatCoords: props.onSlatCoords }))); })); + }; + DayTimeCols.prototype.queryHit = function (positionLeft, positionTop) { + var rawHit = this.timeColsRef.current.positionToHit(positionLeft, positionTop); + if (rawHit) { + return { + component: this, + dateSpan: rawHit.dateSpan, + dayEl: rawHit.dayEl, + rect: { + left: rawHit.relativeRect.left, + right: rawHit.relativeRect.right, + top: rawHit.relativeRect.top, + bottom: rawHit.relativeRect.bottom, + }, + layer: 0, + }; + } + return null; + }; + return DayTimeCols; + }(DateComponent)); + function buildDayRanges(dayTableModel, dateProfile, dateEnv) { + var ranges = []; + for (var _i = 0, _a = dayTableModel.headerDates; _i < _a.length; _i++) { + var date = _a[_i]; + ranges.push({ + start: dateEnv.add(date, dateProfile.slotMinTime), + end: dateEnv.add(date, dateProfile.slotMaxTime), + }); + } + return ranges; + } + + // potential nice values for the slot-duration and interval-duration + // from largest to smallest + var STOCK_SUB_DURATIONS = [ + { hours: 1 }, + { minutes: 30 }, + { minutes: 15 }, + { seconds: 30 }, + { seconds: 15 }, + ]; + function buildSlatMetas(slotMinTime, slotMaxTime, explicitLabelInterval, slotDuration, dateEnv) { + var dayStart = new Date(0); + var slatTime = slotMinTime; + var slatIterator = createDuration(0); + var labelInterval = explicitLabelInterval || computeLabelInterval(slotDuration); + var metas = []; + while (asRoughMs(slatTime) < asRoughMs(slotMaxTime)) { + var date = dateEnv.add(dayStart, slatTime); + var isLabeled = wholeDivideDurations(slatIterator, labelInterval) !== null; + metas.push({ + date: date, + time: slatTime, + key: date.toISOString(), + isoTimeStr: formatIsoTimeString(date), + isLabeled: isLabeled, + }); + slatTime = addDurations(slatTime, slotDuration); + slatIterator = addDurations(slatIterator, slotDuration); + } + return metas; + } + // Computes an automatic value for slotLabelInterval + function computeLabelInterval(slotDuration) { + var i; + var labelInterval; + var slotsPerLabel; + // find the smallest stock label interval that results in more than one slots-per-label + for (i = STOCK_SUB_DURATIONS.length - 1; i >= 0; i -= 1) { + labelInterval = createDuration(STOCK_SUB_DURATIONS[i]); + slotsPerLabel = wholeDivideDurations(labelInterval, slotDuration); + if (slotsPerLabel !== null && slotsPerLabel > 1) { + return labelInterval; + } + } + return slotDuration; // fall back + } + + var DayTimeColsView = /** @class */ (function (_super) { + __extends(DayTimeColsView, _super); + function DayTimeColsView() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.buildTimeColsModel = memoize(buildTimeColsModel); + _this.buildSlatMetas = memoize(buildSlatMetas); + return _this; + } + DayTimeColsView.prototype.render = function () { + var _this = this; + var _a = this.context, options = _a.options, dateEnv = _a.dateEnv, dateProfileGenerator = _a.dateProfileGenerator; + var props = this.props; + var dateProfile = props.dateProfile; + var dayTableModel = this.buildTimeColsModel(dateProfile, dateProfileGenerator); + var splitProps = this.allDaySplitter.splitProps(props); + var slatMetas = this.buildSlatMetas(dateProfile.slotMinTime, dateProfile.slotMaxTime, options.slotLabelInterval, options.slotDuration, dateEnv); + var dayMinWidth = options.dayMinWidth; + var hasAttachedAxis = !dayMinWidth; + var hasDetachedAxis = dayMinWidth; + var headerContent = options.dayHeaders && (createElement(DayHeader, { dates: dayTableModel.headerDates, dateProfile: dateProfile, datesRepDistinctDays: true, renderIntro: hasAttachedAxis ? this.renderHeadAxis : null })); + var allDayContent = (options.allDaySlot !== false) && (function (contentArg) { return (createElement(DayTable, __assign({}, splitProps.allDay, { dateProfile: dateProfile, dayTableModel: dayTableModel, nextDayThreshold: options.nextDayThreshold, tableMinWidth: contentArg.tableMinWidth, colGroupNode: contentArg.tableColGroupNode, renderRowIntro: hasAttachedAxis ? _this.renderTableRowAxis : null, showWeekNumbers: false, expandRows: false, headerAlignElRef: _this.headerElRef, clientWidth: contentArg.clientWidth, clientHeight: contentArg.clientHeight, forPrint: props.forPrint }, _this.getAllDayMaxEventProps()))); }); + var timeGridContent = function (contentArg) { return (createElement(DayTimeCols, __assign({}, splitProps.timed, { dayTableModel: dayTableModel, dateProfile: dateProfile, axis: hasAttachedAxis, slotDuration: options.slotDuration, slatMetas: slatMetas, forPrint: props.forPrint, tableColGroupNode: contentArg.tableColGroupNode, tableMinWidth: contentArg.tableMinWidth, clientWidth: contentArg.clientWidth, clientHeight: contentArg.clientHeight, onSlatCoords: _this.handleSlatCoords, expandRows: contentArg.expandRows, onScrollTopRequest: _this.handleScrollTopRequest }))); }; + return hasDetachedAxis + ? this.renderHScrollLayout(headerContent, allDayContent, timeGridContent, dayTableModel.colCnt, dayMinWidth, slatMetas, this.state.slatCoords) + : this.renderSimpleLayout(headerContent, allDayContent, timeGridContent); + }; + return DayTimeColsView; + }(TimeColsView)); + function buildTimeColsModel(dateProfile, dateProfileGenerator) { + var daySeries = new DaySeriesModel(dateProfile.renderRange, dateProfileGenerator); + return new DayTableModel(daySeries, false); + } + + var OPTION_REFINERS$2 = { + allDaySlot: Boolean, + }; + + var timeGridPlugin = createPlugin({ + initialView: 'timeGridWeek', + optionRefiners: OPTION_REFINERS$2, + views: { + timeGrid: { + component: DayTimeColsView, + usesMinMaxTime: true, + allDaySlot: true, + slotDuration: '00:30:00', + slotEventOverlap: true, + }, + timeGridDay: { + type: 'timeGrid', + duration: { days: 1 }, + }, + timeGridWeek: { + type: 'timeGrid', + duration: { weeks: 1 }, + }, + }, + }); + + var ListViewHeaderRow = /** @class */ (function (_super) { + __extends(ListViewHeaderRow, _super); + function ListViewHeaderRow() { + return _super !== null && _super.apply(this, arguments) || this; + } + ListViewHeaderRow.prototype.render = function () { + var _a = this.props, dayDate = _a.dayDate, todayRange = _a.todayRange; + var _b = this.context, theme = _b.theme, dateEnv = _b.dateEnv, options = _b.options, viewApi = _b.viewApi; + var dayMeta = getDateMeta(dayDate, todayRange); + // will ever be falsy? + var text = options.listDayFormat ? dateEnv.format(dayDate, options.listDayFormat) : ''; + // will ever be falsy? also, BAD NAME "alt" + var sideText = options.listDaySideFormat ? dateEnv.format(dayDate, options.listDaySideFormat) : ''; + var navLinkData = options.navLinks + ? buildNavLinkData(dayDate) + : null; + var hookProps = __assign({ date: dateEnv.toDate(dayDate), view: viewApi, text: text, + sideText: sideText, + navLinkData: navLinkData }, dayMeta); + var classNames = ['fc-list-day'].concat(getDayClassNames(dayMeta, theme)); + // TODO: make a reusable HOC for dayHeader (used in daygrid/timegrid too) + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.dayHeaderClassNames, content: options.dayHeaderContent, defaultContent: renderInnerContent$4, didMount: options.dayHeaderDidMount, willUnmount: options.dayHeaderWillUnmount }, function (rootElRef, customClassNames, innerElRef, innerContent) { return (createElement("tr", { ref: rootElRef, className: classNames.concat(customClassNames).join(' '), "data-date": formatDayString(dayDate) }, + createElement("th", { colSpan: 3 }, + createElement("div", { className: 'fc-list-day-cushion ' + theme.getClass('tableCellShaded'), ref: innerElRef }, innerContent)))); })); + }; + return ListViewHeaderRow; + }(BaseComponent)); + function renderInnerContent$4(props) { + var navLinkAttrs = props.navLinkData // is there a type for this? + ? { 'data-navlink': props.navLinkData, tabIndex: 0 } + : {}; + return (createElement(Fragment, null, + props.text && (createElement("a", __assign({ className: "fc-list-day-text" }, navLinkAttrs), props.text)), + props.sideText && (createElement("a", __assign({ className: "fc-list-day-side-text" }, navLinkAttrs), props.sideText)))); + } + + var DEFAULT_TIME_FORMAT$1 = createFormatter({ + hour: 'numeric', + minute: '2-digit', + meridiem: 'short', + }); + var ListViewEventRow = /** @class */ (function (_super) { + __extends(ListViewEventRow, _super); + function ListViewEventRow() { + return _super !== null && _super.apply(this, arguments) || this; + } + ListViewEventRow.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context; + var seg = props.seg; + var timeFormat = context.options.eventTimeFormat || DEFAULT_TIME_FORMAT$1; + return (createElement(EventRoot, { seg: seg, timeText: "" // BAD. because of all-day content + , disableDragging: true, disableResizing: true, defaultContent: renderEventInnerContent, isPast: props.isPast, isFuture: props.isFuture, isToday: props.isToday, isSelected: props.isSelected, isDragging: props.isDragging, isResizing: props.isResizing, isDateSelecting: props.isDateSelecting }, function (rootElRef, classNames, innerElRef, innerContent, hookProps) { return (createElement("tr", { className: ['fc-list-event', hookProps.event.url ? 'fc-event-forced-url' : ''].concat(classNames).join(' '), ref: rootElRef }, + buildTimeContent(seg, timeFormat, context), + createElement("td", { className: "fc-list-event-graphic" }, + createElement("span", { className: "fc-list-event-dot", style: { borderColor: hookProps.borderColor || hookProps.backgroundColor } })), + createElement("td", { className: "fc-list-event-title", ref: innerElRef }, innerContent))); })); + }; + return ListViewEventRow; + }(BaseComponent)); + function renderEventInnerContent(props) { + var event = props.event; + var url = event.url; + var anchorAttrs = url ? { href: url } : {}; + return (createElement("a", __assign({}, anchorAttrs), event.title)); + } + function buildTimeContent(seg, timeFormat, context) { + var options = context.options; + if (options.displayEventTime !== false) { + var eventDef = seg.eventRange.def; + var eventInstance = seg.eventRange.instance; + var doAllDay = false; + var timeText = void 0; + if (eventDef.allDay) { + doAllDay = true; + } + else if (isMultiDayRange(seg.eventRange.range)) { // TODO: use (!isStart || !isEnd) instead? + if (seg.isStart) { + timeText = buildSegTimeText(seg, timeFormat, context, null, null, eventInstance.range.start, seg.end); + } + else if (seg.isEnd) { + timeText = buildSegTimeText(seg, timeFormat, context, null, null, seg.start, eventInstance.range.end); + } + else { + doAllDay = true; + } + } + else { + timeText = buildSegTimeText(seg, timeFormat, context); + } + if (doAllDay) { + var hookProps = { + text: context.options.allDayText, + view: context.viewApi, + }; + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.allDayClassNames, content: options.allDayContent, defaultContent: renderAllDayInner$1, didMount: options.allDayDidMount, willUnmount: options.allDayWillUnmount }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("td", { className: ['fc-list-event-time'].concat(classNames).join(' '), ref: rootElRef }, innerContent)); })); + } + return (createElement("td", { className: "fc-list-event-time" }, timeText)); + } + return null; + } + function renderAllDayInner$1(hookProps) { + return hookProps.text; + } + + /* + Responsible for the scroller, and forwarding event-related actions into the "grid". + */ + var ListView = /** @class */ (function (_super) { + __extends(ListView, _super); + function ListView() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.computeDateVars = memoize(computeDateVars); + _this.eventStoreToSegs = memoize(_this._eventStoreToSegs); + _this.setRootEl = function (rootEl) { + if (rootEl) { + _this.context.registerInteractiveComponent(_this, { + el: rootEl, + }); + } + else { + _this.context.unregisterInteractiveComponent(_this); + } + }; + return _this; + } + ListView.prototype.render = function () { + var _this = this; + var _a = this, props = _a.props, context = _a.context; + var extraClassNames = [ + 'fc-list', + context.theme.getClass('table'), + context.options.stickyHeaderDates !== false ? 'fc-list-sticky' : '', + ]; + var _b = this.computeDateVars(props.dateProfile), dayDates = _b.dayDates, dayRanges = _b.dayRanges; + var eventSegs = this.eventStoreToSegs(props.eventStore, props.eventUiBases, dayRanges); + return (createElement(ViewRoot, { viewSpec: context.viewSpec, elRef: this.setRootEl }, function (rootElRef, classNames) { return (createElement("div", { ref: rootElRef, className: extraClassNames.concat(classNames).join(' ') }, + createElement(Scroller, { liquid: !props.isHeightAuto, overflowX: props.isHeightAuto ? 'visible' : 'hidden', overflowY: props.isHeightAuto ? 'visible' : 'auto' }, eventSegs.length > 0 ? + _this.renderSegList(eventSegs, dayDates) : + _this.renderEmptyMessage()))); })); + }; + ListView.prototype.renderEmptyMessage = function () { + var _a = this.context, options = _a.options, viewApi = _a.viewApi; + var hookProps = { + text: options.noEventsText, + view: viewApi, + }; + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.noEventsClassNames, content: options.noEventsContent, defaultContent: renderNoEventsInner, didMount: options.noEventsDidMount, willUnmount: options.noEventsWillUnmount }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("div", { className: ['fc-list-empty'].concat(classNames).join(' '), ref: rootElRef }, + createElement("div", { className: "fc-list-empty-cushion", ref: innerElRef }, innerContent))); })); + }; + ListView.prototype.renderSegList = function (allSegs, dayDates) { + var _a = this.context, theme = _a.theme, options = _a.options; + var segsByDay = groupSegsByDay(allSegs); // sparse array + return (createElement(NowTimer, { unit: "day" }, function (nowDate, todayRange) { + var innerNodes = []; + for (var dayIndex = 0; dayIndex < segsByDay.length; dayIndex += 1) { + var daySegs = segsByDay[dayIndex]; + if (daySegs) { // sparse array, so might be undefined + var dayStr = dayDates[dayIndex].toISOString(); + // append a day header + innerNodes.push(createElement(ListViewHeaderRow, { key: dayStr, dayDate: dayDates[dayIndex], todayRange: todayRange })); + daySegs = sortEventSegs(daySegs, options.eventOrder); + for (var _i = 0, daySegs_1 = daySegs; _i < daySegs_1.length; _i++) { + var seg = daySegs_1[_i]; + innerNodes.push(createElement(ListViewEventRow, __assign({ key: dayStr + ':' + seg.eventRange.instance.instanceId /* are multiple segs for an instanceId */, seg: seg, isDragging: false, isResizing: false, isDateSelecting: false, isSelected: false }, getSegMeta(seg, todayRange, nowDate)))); + } + } + } + return (createElement("table", { className: 'fc-list-table ' + theme.getClass('table') }, + createElement("tbody", null, innerNodes))); + })); + }; + ListView.prototype._eventStoreToSegs = function (eventStore, eventUiBases, dayRanges) { + return this.eventRangesToSegs(sliceEventStore(eventStore, eventUiBases, this.props.dateProfile.activeRange, this.context.options.nextDayThreshold).fg, dayRanges); + }; + ListView.prototype.eventRangesToSegs = function (eventRanges, dayRanges) { + var segs = []; + for (var _i = 0, eventRanges_1 = eventRanges; _i < eventRanges_1.length; _i++) { + var eventRange = eventRanges_1[_i]; + segs.push.apply(segs, this.eventRangeToSegs(eventRange, dayRanges)); + } + return segs; + }; + ListView.prototype.eventRangeToSegs = function (eventRange, dayRanges) { + var dateEnv = this.context.dateEnv; + var nextDayThreshold = this.context.options.nextDayThreshold; + var range = eventRange.range; + var allDay = eventRange.def.allDay; + var dayIndex; + var segRange; + var seg; + var segs = []; + for (dayIndex = 0; dayIndex < dayRanges.length; dayIndex += 1) { + segRange = intersectRanges(range, dayRanges[dayIndex]); + if (segRange) { + seg = { + component: this, + eventRange: eventRange, + start: segRange.start, + end: segRange.end, + isStart: eventRange.isStart && segRange.start.valueOf() === range.start.valueOf(), + isEnd: eventRange.isEnd && segRange.end.valueOf() === range.end.valueOf(), + dayIndex: dayIndex, + }; + segs.push(seg); + // detect when range won't go fully into the next day, + // and mutate the latest seg to the be the end. + if (!seg.isEnd && !allDay && + dayIndex + 1 < dayRanges.length && + range.end < + dateEnv.add(dayRanges[dayIndex + 1].start, nextDayThreshold)) { + seg.end = range.end; + seg.isEnd = true; + break; + } + } + } + return segs; + }; + return ListView; + }(DateComponent)); + function renderNoEventsInner(hookProps) { + return hookProps.text; + } + function computeDateVars(dateProfile) { + var dayStart = startOfDay(dateProfile.renderRange.start); + var viewEnd = dateProfile.renderRange.end; + var dayDates = []; + var dayRanges = []; + while (dayStart < viewEnd) { + dayDates.push(dayStart); + dayRanges.push({ + start: dayStart, + end: addDays(dayStart, 1), + }); + dayStart = addDays(dayStart, 1); + } + return { dayDates: dayDates, dayRanges: dayRanges }; + } + // Returns a sparse array of arrays, segs grouped by their dayIndex + function groupSegsByDay(segs) { + var segsByDay = []; // sparse array + var i; + var seg; + for (i = 0; i < segs.length; i += 1) { + seg = segs[i]; + (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = [])) + .push(seg); + } + return segsByDay; + } + + var OPTION_REFINERS$3 = { + listDayFormat: createFalsableFormatter, + listDaySideFormat: createFalsableFormatter, + noEventsClassNames: identity, + noEventsContent: identity, + noEventsDidMount: identity, + noEventsWillUnmount: identity, + }; + function createFalsableFormatter(input) { + return input === false ? null : createFormatter(input); + } + + var listPlugin = createPlugin({ + optionRefiners: OPTION_REFINERS$3, + views: { + list: { + component: ListView, + buttonTextKey: 'list', + listDayFormat: { month: 'long', day: 'numeric', year: 'numeric' }, + }, + listDay: { + type: 'list', + duration: { days: 1 }, + listDayFormat: { weekday: 'long' }, + }, + listWeek: { + type: 'list', + duration: { weeks: 1 }, + listDayFormat: { weekday: 'long' }, + listDaySideFormat: { month: 'long', day: 'numeric', year: 'numeric' }, + }, + listMonth: { + type: 'list', + duration: { month: 1 }, + listDaySideFormat: { weekday: 'long' }, + }, + listYear: { + type: 'list', + duration: { year: 1 }, + listDaySideFormat: { weekday: 'long' }, + }, + }, + }); + + var BootstrapTheme = /** @class */ (function (_super) { + __extends(BootstrapTheme, _super); + function BootstrapTheme() { + return _super !== null && _super.apply(this, arguments) || this; + } + return BootstrapTheme; + }(Theme)); + BootstrapTheme.prototype.classes = { + root: 'fc-theme-bootstrap', + table: 'table-bordered', + tableCellShaded: 'table-active', + buttonGroup: 'btn-group', + button: 'btn btn-primary', + buttonActive: 'active', + popover: 'popover', + popoverHeader: 'popover-header', + popoverContent: 'popover-body', + }; + BootstrapTheme.prototype.baseIconClass = 'fa'; + BootstrapTheme.prototype.iconClasses = { + close: 'fa-times', + prev: 'fa-chevron-left', + next: 'fa-chevron-right', + prevYear: 'fa-angle-double-left', + nextYear: 'fa-angle-double-right', + }; + BootstrapTheme.prototype.rtlIconClasses = { + prev: 'fa-chevron-right', + next: 'fa-chevron-left', + prevYear: 'fa-angle-double-right', + nextYear: 'fa-angle-double-left', + }; + BootstrapTheme.prototype.iconOverrideOption = 'bootstrapFontAwesome'; // TODO: make TS-friendly. move the option-processing into this plugin + BootstrapTheme.prototype.iconOverrideCustomButtonOption = 'bootstrapFontAwesome'; + BootstrapTheme.prototype.iconOverridePrefix = 'fa-'; + var plugin = createPlugin({ + themeClasses: { + bootstrap: BootstrapTheme, + }, + }); + + // rename this file to options.ts like other packages? + var OPTION_REFINERS$4 = { + googleCalendarApiKey: String, + }; + + var EVENT_SOURCE_REFINERS$1 = { + googleCalendarApiKey: String, + googleCalendarId: String, + googleCalendarApiBase: String, + extraParams: identity, + }; + + // TODO: expose somehow + var API_BASE = 'https://www.googleapis.com/calendar/v3/calendars'; + var eventSourceDef$3 = { + parseMeta: function (refined) { + var googleCalendarId = refined.googleCalendarId; + if (!googleCalendarId && refined.url) { + googleCalendarId = parseGoogleCalendarId(refined.url); + } + if (googleCalendarId) { + return { + googleCalendarId: googleCalendarId, + googleCalendarApiKey: refined.googleCalendarApiKey, + googleCalendarApiBase: refined.googleCalendarApiBase, + extraParams: refined.extraParams, + }; + } + return null; + }, + fetch: function (arg, onSuccess, onFailure) { + var _a = arg.context, dateEnv = _a.dateEnv, options = _a.options; + var meta = arg.eventSource.meta; + var apiKey = meta.googleCalendarApiKey || options.googleCalendarApiKey; + if (!apiKey) { + onFailure({ + message: 'Specify a googleCalendarApiKey. See http://fullcalendar.io/docs/google_calendar/', + }); + } + else { + var url = buildUrl(meta); + // TODO: make DRY with json-feed-event-source + var extraParams = meta.extraParams; + var extraParamsObj = typeof extraParams === 'function' ? extraParams() : extraParams; + var requestParams_1 = buildRequestParams$1(arg.range, apiKey, extraParamsObj, dateEnv); + requestJson('GET', url, requestParams_1, function (body, xhr) { + if (body.error) { + onFailure({ + message: 'Google Calendar API: ' + body.error.message, + errors: body.error.errors, + xhr: xhr, + }); + } + else { + onSuccess({ + rawEvents: gcalItemsToRawEventDefs(body.items, requestParams_1.timeZone), + xhr: xhr, + }); + } + }, function (message, xhr) { + onFailure({ message: message, xhr: xhr }); + }); + } + }, + }; + function parseGoogleCalendarId(url) { + var match; + // detect if the ID was specified as a single string. + // will match calendars like "asdf1234@calendar.google.com" in addition to person email calendars. + if (/^[^/]+@([^/.]+\.)*(google|googlemail|gmail)\.com$/.test(url)) { + return url; + } + if ((match = /^https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/([^/]*)/.exec(url)) || + (match = /^https?:\/\/www.google.com\/calendar\/feeds\/([^/]*)/.exec(url))) { + return decodeURIComponent(match[1]); + } + return null; + } + function buildUrl(meta) { + var apiBase = meta.googleCalendarApiBase; + if (!apiBase) { + apiBase = API_BASE; + } + return apiBase + '/' + encodeURIComponent(meta.googleCalendarId) + '/events'; + } + function buildRequestParams$1(range, apiKey, extraParams, dateEnv) { + var params; + var startStr; + var endStr; + if (dateEnv.canComputeOffset) { + // strings will naturally have offsets, which GCal needs + startStr = dateEnv.formatIso(range.start); + endStr = dateEnv.formatIso(range.end); + } + else { + // when timezone isn't known, we don't know what the UTC offset should be, so ask for +/- 1 day + // from the UTC day-start to guarantee we're getting all the events + // (start/end will be UTC-coerced dates, so toISOString is okay) + startStr = addDays(range.start, -1).toISOString(); + endStr = addDays(range.end, 1).toISOString(); + } + params = __assign(__assign({}, (extraParams || {})), { key: apiKey, timeMin: startStr, timeMax: endStr, singleEvents: true, maxResults: 9999 }); + if (dateEnv.timeZone !== 'local') { + params.timeZone = dateEnv.timeZone; + } + return params; + } + function gcalItemsToRawEventDefs(items, gcalTimezone) { + return items.map(function (item) { return gcalItemToRawEventDef(item, gcalTimezone); }); + } + function gcalItemToRawEventDef(item, gcalTimezone) { + var url = item.htmlLink || null; + // make the URLs for each event show times in the correct timezone + if (url && gcalTimezone) { + url = injectQsComponent(url, 'ctz=' + gcalTimezone); + } + return { + id: item.id, + title: item.summary, + start: item.start.dateTime || item.start.date, + end: item.end.dateTime || item.end.date, + url: url, + location: item.location, + description: item.description, + }; + } + // Injects a string like "arg=value" into the querystring of a URL + // TODO: move to a general util file? + function injectQsComponent(url, component) { + // inject it after the querystring but before the fragment + return url.replace(/(\?.*?)?(#|$)/, function (whole, qs, hash) { return (qs ? qs + '&' : '?') + component + hash; }); + } + var googleCalendarPlugin = createPlugin({ + eventSourceDefs: [eventSourceDef$3], + optionRefiners: OPTION_REFINERS$4, + eventSourceRefiners: EVENT_SOURCE_REFINERS$1, + }); + + globalPlugins.push(interactionPlugin, dayGridPlugin, timeGridPlugin, listPlugin, plugin, googleCalendarPlugin); + + exports.BASE_OPTION_DEFAULTS = BASE_OPTION_DEFAULTS; + exports.BASE_OPTION_REFINERS = BASE_OPTION_REFINERS; + exports.BaseComponent = BaseComponent; + exports.BgEvent = BgEvent; + exports.BootstrapTheme = BootstrapTheme; + exports.Calendar = Calendar; + exports.CalendarApi = CalendarApi; + exports.CalendarContent = CalendarContent; + exports.CalendarDataManager = CalendarDataManager; + exports.CalendarDataProvider = CalendarDataProvider; + exports.CalendarRoot = CalendarRoot; + exports.Component = Component; + exports.ContentHook = ContentHook; + exports.CustomContentRenderContext = CustomContentRenderContext; + exports.DateComponent = DateComponent; + exports.DateEnv = DateEnv; + exports.DateProfileGenerator = DateProfileGenerator; + exports.DayCellContent = DayCellContent; + exports.DayCellRoot = DayCellRoot; + exports.DayGridView = DayTableView; + exports.DayHeader = DayHeader; + exports.DaySeriesModel = DaySeriesModel; + exports.DayTable = DayTable; + exports.DayTableModel = DayTableModel; + exports.DayTableSlicer = DayTableSlicer; + exports.DayTimeCols = DayTimeCols; + exports.DayTimeColsSlicer = DayTimeColsSlicer; + exports.DayTimeColsView = DayTimeColsView; + exports.DelayedRunner = DelayedRunner; + exports.Draggable = ExternalDraggable; + exports.ElementDragging = ElementDragging; + exports.ElementScrollController = ElementScrollController; + exports.Emitter = Emitter; + exports.EventApi = EventApi; + exports.EventRoot = EventRoot; + exports.EventSourceApi = EventSourceApi; + exports.FeaturefulElementDragging = FeaturefulElementDragging; + exports.Fragment = Fragment; + exports.Interaction = Interaction; + exports.ListView = ListView; + exports.MountHook = MountHook; + exports.NamedTimeZoneImpl = NamedTimeZoneImpl; + exports.NowIndicatorRoot = NowIndicatorRoot; + exports.NowTimer = NowTimer; + exports.PointerDragging = PointerDragging; + exports.PositionCache = PositionCache; + exports.RefMap = RefMap; + exports.RenderHook = RenderHook; + exports.ScrollController = ScrollController; + exports.ScrollResponder = ScrollResponder; + exports.Scroller = Scroller; + exports.SimpleScrollGrid = SimpleScrollGrid; + exports.Slicer = Slicer; + exports.Splitter = Splitter; + exports.StandardEvent = StandardEvent; + exports.Table = Table; + exports.TableDateCell = TableDateCell; + exports.TableDowCell = TableDowCell; + exports.TableView = TableView; + exports.Theme = Theme; + exports.ThirdPartyDraggable = ThirdPartyDraggable; + exports.TimeCols = TimeCols; + exports.TimeColsSlatsCoords = TimeColsSlatsCoords; + exports.TimeColsView = TimeColsView; + exports.ViewApi = ViewApi; + exports.ViewContextType = ViewContextType; + exports.ViewRoot = ViewRoot; + exports.WeekNumberRoot = WeekNumberRoot; + exports.WindowScrollController = WindowScrollController; + exports.addDays = addDays; + exports.addDurations = addDurations; + exports.addMs = addMs; + exports.addWeeks = addWeeks; + exports.allowContextMenu = allowContextMenu; + exports.allowSelection = allowSelection; + exports.applyMutationToEventStore = applyMutationToEventStore; + exports.applyStyle = applyStyle; + exports.applyStyleProp = applyStyleProp; + exports.asCleanDays = asCleanDays; + exports.asRoughMinutes = asRoughMinutes; + exports.asRoughMs = asRoughMs; + exports.asRoughSeconds = asRoughSeconds; + exports.buildClassNameNormalizer = buildClassNameNormalizer; + exports.buildDayRanges = buildDayRanges; + exports.buildDayTableModel = buildDayTableModel; + exports.buildEventApis = buildEventApis; + exports.buildEventRangeKey = buildEventRangeKey; + exports.buildHashFromArray = buildHashFromArray; + exports.buildNavLinkData = buildNavLinkData; + exports.buildSegCompareObj = buildSegCompareObj; + exports.buildSegTimeText = buildSegTimeText; + exports.buildSlatMetas = buildSlatMetas; + exports.buildTimeColsModel = buildTimeColsModel; + exports.collectFromHash = collectFromHash; + exports.combineEventUis = combineEventUis; + exports.compareByFieldSpec = compareByFieldSpec; + exports.compareByFieldSpecs = compareByFieldSpecs; + exports.compareNumbers = compareNumbers; + exports.compareObjs = compareObjs; + exports.computeEdges = computeEdges; + exports.computeFallbackHeaderFormat = computeFallbackHeaderFormat; + exports.computeHeightAndMargins = computeHeightAndMargins; + exports.computeInnerRect = computeInnerRect; + exports.computeRect = computeRect; + exports.computeSegDraggable = computeSegDraggable; + exports.computeSegEndResizable = computeSegEndResizable; + exports.computeSegStartResizable = computeSegStartResizable; + exports.computeShrinkWidth = computeShrinkWidth; + exports.computeSmallestCellWidth = computeSmallestCellWidth; + exports.computeVisibleDayRange = computeVisibleDayRange; + exports.config = config; + exports.constrainPoint = constrainPoint; + exports.createContext = createContext$1; + exports.createDuration = createDuration; + exports.createElement = createElement; + exports.createEmptyEventStore = createEmptyEventStore; + exports.createEventInstance = createEventInstance; + exports.createEventUi = createEventUi; + exports.createFormatter = createFormatter; + exports.createPlugin = createPlugin; + exports.createRef = createRef; + exports.diffDates = diffDates; + exports.diffDayAndTime = diffDayAndTime; + exports.diffDays = diffDays; + exports.diffPoints = diffPoints; + exports.diffWeeks = diffWeeks; + exports.diffWholeDays = diffWholeDays; + exports.diffWholeWeeks = diffWholeWeeks; + exports.disableCursor = disableCursor; + exports.elementClosest = elementClosest; + exports.elementMatches = elementMatches; + exports.enableCursor = enableCursor; + exports.eventTupleToStore = eventTupleToStore; + exports.filterEventStoreDefs = filterEventStoreDefs; + exports.filterHash = filterHash; + exports.findDirectChildren = findDirectChildren; + exports.findElements = findElements; + exports.flexibleCompare = flexibleCompare; + exports.flushToDom = flushToDom$1; + exports.formatDate = formatDate; + exports.formatDayString = formatDayString; + exports.formatIsoTimeString = formatIsoTimeString; + exports.formatRange = formatRange; + exports.getAllowYScrolling = getAllowYScrolling; + exports.getCanVGrowWithinCell = getCanVGrowWithinCell; + exports.getClippingParents = getClippingParents; + exports.getDateMeta = getDateMeta; + exports.getDayClassNames = getDayClassNames; + exports.getDefaultEventEnd = getDefaultEventEnd; + exports.getElSeg = getElSeg; + exports.getEventClassNames = getEventClassNames; + exports.getIsRtlScrollbarOnLeft = getIsRtlScrollbarOnLeft; + exports.getRectCenter = getRectCenter; + exports.getRelevantEvents = getRelevantEvents; + exports.getScrollGridClassNames = getScrollGridClassNames; + exports.getScrollbarWidths = getScrollbarWidths; + exports.getSectionClassNames = getSectionClassNames; + exports.getSectionHasLiquidHeight = getSectionHasLiquidHeight; + exports.getSegMeta = getSegMeta; + exports.getSlotClassNames = getSlotClassNames; + exports.getStickyFooterScrollbar = getStickyFooterScrollbar; + exports.getStickyHeaderDates = getStickyHeaderDates; + exports.getUnequalProps = getUnequalProps; + exports.globalLocales = globalLocales; + exports.globalPlugins = globalPlugins; + exports.greatestDurationDenominator = greatestDurationDenominator; + exports.guid = guid; + exports.hasBgRendering = hasBgRendering; + exports.hasShrinkWidth = hasShrinkWidth; + exports.identity = identity; + exports.interactionSettingsStore = interactionSettingsStore; + exports.interactionSettingsToStore = interactionSettingsToStore; + exports.intersectRanges = intersectRanges; + exports.intersectRects = intersectRects; + exports.isArraysEqual = isArraysEqual; + exports.isColPropsEqual = isColPropsEqual; + exports.isDateSpansEqual = isDateSpansEqual; + exports.isInt = isInt; + exports.isInteractionValid = isInteractionValid; + exports.isMultiDayRange = isMultiDayRange; + exports.isPropsEqual = isPropsEqual; + exports.isPropsValid = isPropsValid; + exports.isValidDate = isValidDate; + exports.listenBySelector = listenBySelector; + exports.mapHash = mapHash; + exports.memoize = memoize; + exports.memoizeArraylike = memoizeArraylike; + exports.memoizeHashlike = memoizeHashlike; + exports.memoizeObjArg = memoizeObjArg; + exports.mergeEventStores = mergeEventStores; + exports.multiplyDuration = multiplyDuration; + exports.padStart = padStart; + exports.parseBusinessHours = parseBusinessHours; + exports.parseClassNames = parseClassNames; + exports.parseDragMeta = parseDragMeta; + exports.parseEventDef = parseEventDef; + exports.parseFieldSpecs = parseFieldSpecs; + exports.parseMarker = parse; + exports.pointInsideRect = pointInsideRect; + exports.preventContextMenu = preventContextMenu; + exports.preventDefault = preventDefault; + exports.preventSelection = preventSelection; + exports.rangeContainsMarker = rangeContainsMarker; + exports.rangeContainsRange = rangeContainsRange; + exports.rangesEqual = rangesEqual; + exports.rangesIntersect = rangesIntersect; + exports.refineEventDef = refineEventDef; + exports.refineProps = refineProps; + exports.removeElement = removeElement; + exports.removeExact = removeExact; + exports.render = render; + exports.renderChunkContent = renderChunkContent; + exports.renderFill = renderFill; + exports.renderMicroColGroup = renderMicroColGroup; + exports.renderScrollShim = renderScrollShim; + exports.requestJson = requestJson; + exports.sanitizeShrinkWidth = sanitizeShrinkWidth; + exports.setElSeg = setElSeg; + exports.setRef = setRef; + exports.sliceEventStore = sliceEventStore; + exports.sliceEvents = sliceEvents; + exports.sortEventSegs = sortEventSegs; + exports.startOfDay = startOfDay; + exports.translateRect = translateRect; + exports.triggerDateSelect = triggerDateSelect; + exports.unmountComponentAtNode = unmountComponentAtNode$1; + exports.unpromisify = unpromisify; + exports.version = version; + exports.whenTransitionDone = whenTransitionDone; + exports.wholeDivideDurations = wholeDivideDurations; + + Object.defineProperty(exports, '__esModule', { value: true }); + + return exports; + +}({})); diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index f9a52810a1..2b56df1716 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -40,6 +40,7 @@ + @@ -103,6 +104,7 @@ InvenTree + From 5f6442ba6b796b565cb3b6296d579fac47109a8a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 Jan 2021 18:47:29 +1100 Subject: [PATCH 35/40] Render sales orders to a calendar view --- InvenTree/InvenTree/status_codes.py | 5 + InvenTree/order/api.py | 9 ++ InvenTree/order/models.py | 38 +++++++ .../order/templates/order/sales_orders.html | 102 ++++++++++++++++++ 4 files changed, 154 insertions(+) diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index 1d5335ad2f..916b3341cb 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -152,6 +152,11 @@ class SalesOrderStatus(StatusCode): PENDING, ] + # Completed orders + COMPLETE = [ + SHIPPED, + ] + class StockStatus(StatusCode): diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 6b8a3c81e0..12f0854c19 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -9,6 +9,8 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics from rest_framework import filters +from django.db.models import Q + from django.conf.urls import url, include from InvenTree.helpers import str2bool @@ -293,6 +295,13 @@ class SOList(generics.ListCreateAPIView): except (Part.DoesNotExist, ValueError): pass + # Filter by 'date range' + min_date = params.get('min_date', None) + max_date = params.get('max_date', None) + + if min_date is not None and max_date is not None: + queryset = SalesOrder.filter_interesting_orders(queryset, min_date, max_date) + return queryset filter_backends = [ diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 9045ab86d7..d0373fa324 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -301,6 +301,44 @@ class SalesOrder(Order): OVERDUE_FILTER = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date()) + @staticmethod + def filter_interesting_orders(queryset, min_date, max_date): + """ + Filter by "minimum and maximum date range" + + - Specified as min_date, max_date + - Both must be specified for filter to be applied + - Determine which "interesting" orders exist between these dates + + To be "interesting": + - A "completed" order where the completion date lies within the date range + - A "pending" order where the target date lies within the date range + - TODO: An "overdue" order where the target date is in the past + """ + + DATE_FMT = '%Y-%m-%d' # ISO format date string + + # Ensure that both dates are valid + try: + min_date = datetime.strptime(str(min_date), DATE_FMT).date() + max_date = datetime.strptime(str(max_date), DATE_FMT).date() + except (ValueError, TypeError): + # Date processing error, return queryset unchanged + return queryset + + # Construct a queryset for "completed" orders within the range + COMPLETED = Q(status__in=SalesOrderStatus.COMPLETE) & Q(shipment_date__gte=min_date) & Q(shipment_date__lte=max_date) + + # Construct a queryset for "pending" orders within the range + PENDING = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date) + + # Construct a queryset for "overdue" orders within the range + FILTER = COMPLETED | PENDING + + queryset = queryset.filter(FILTER) + + return queryset + def __str__(self): prefix = getSetting('SALESORDER_REFERENCE_PREFIX') diff --git a/InvenTree/order/templates/order/sales_orders.html b/InvenTree/order/templates/order/sales_orders.html index acfd875078..f9031912bf 100644 --- a/InvenTree/order/templates/order/sales_orders.html +++ b/InvenTree/order/templates/order/sales_orders.html @@ -1,5 +1,6 @@ {% extends "base.html" %} +{% load inventree_extras %} {% load static %} {% load i18n %} @@ -12,6 +13,10 @@ InvenTree | {% trans "Sales Orders" %}

{% trans "Sales Orders" %}


+
+
+
+
{% if roles.sales_order.add %} @@ -29,9 +34,106 @@ InvenTree | {% trans "Sales Orders" %} {% endblock %} +{% block js_load %} +{{ block.super }} + + +{% endblock %} + {% block js_ready %} {{ block.super }} + loadSalesOrderTable("#sales-order-table", { url: "{% url 'api-so-list' %}", }); From b7203f0ebb12faeacf629c93cfbc4661597aba2e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 Jan 2021 22:32:17 +1100 Subject: [PATCH 36/40] Switch between calendar and list view for sales orders --- .../order/templates/order/sales_orders.html | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/InvenTree/order/templates/order/sales_orders.html b/InvenTree/order/templates/order/sales_orders.html index f9031912bf..f75f9fa36b 100644 --- a/InvenTree/order/templates/order/sales_orders.html +++ b/InvenTree/order/templates/order/sales_orders.html @@ -12,10 +12,7 @@ InvenTree | {% trans "Sales Orders" %}

{% trans "Sales Orders" %}


- -
-
-
+
@@ -23,6 +20,12 @@ InvenTree | {% trans "Sales Orders" %} {% endif %} + +
@@ -31,6 +34,9 @@ InvenTree | {% trans "Sales Orders" %}
/ elements with colspans. + SOLUTION: making individual
+ _this.frameElRefs = new RefMap(); // the fc-daygrid-day-frame + _this.fgElRefs = new RefMap(); // the fc-daygrid-day-events + _this.segHarnessRefs = new RefMap(); // indexed by "instanceId:firstCol" + _this.rootElRef = createRef(); + _this.state = { + framePositions: null, + maxContentHeight: null, + segHeights: {}, + }; + return _this; + } + TableRow.prototype.render = function () { + var _this = this; + var _a = this, props = _a.props, state = _a.state, context = _a.context; + var colCnt = props.cells.length; + var businessHoursByCol = splitSegsByFirstCol(props.businessHourSegs, colCnt); + var bgEventSegsByCol = splitSegsByFirstCol(props.bgEventSegs, colCnt); + var highlightSegsByCol = splitSegsByFirstCol(this.getHighlightSegs(), colCnt); + var mirrorSegsByCol = splitSegsByFirstCol(this.getMirrorSegs(), colCnt); + var _b = computeFgSegPlacement(props.cells, props.fgEventSegs, props.dayMaxEvents, props.dayMaxEventRows, state.segHeights, state.maxContentHeight, colCnt, context.options.eventOrder), paddingBottoms = _b.paddingBottoms, segsByFirstCol = _b.segsByFirstCol, segsByEachCol = _b.segsByEachCol, segIsHidden = _b.segIsHidden, segTops = _b.segTops, segMarginTops = _b.segMarginTops, moreCnts = _b.moreCnts, moreTops = _b.moreTops; + var selectedInstanceHash = // TODO: messy way to compute this + (props.eventDrag && props.eventDrag.affectedInstances) || + (props.eventResize && props.eventResize.affectedInstances) || + {}; + return (createElement("tr", { ref: this.rootElRef }, + props.renderIntro && props.renderIntro(), + props.cells.map(function (cell, col) { + var normalFgNodes = _this.renderFgSegs(segsByFirstCol[col], segIsHidden, segTops, segMarginTops, selectedInstanceHash, props.todayRange); + var mirrorFgNodes = _this.renderFgSegs(mirrorSegsByCol[col], {}, segTops, // use same tops as real rendering + {}, {}, props.todayRange, Boolean(props.eventDrag), Boolean(props.eventResize), false); + return (createElement(TableCell, { key: cell.key, elRef: _this.cellElRefs.createRef(cell.key), innerElRef: _this.frameElRefs.createRef(cell.key) /* FF problem, but okay to use for left/right. TODO: rename prop */, dateProfile: props.dateProfile, date: cell.date, showDayNumber: props.showDayNumbers, showWeekNumber: props.showWeekNumbers && col === 0, forceDayTop: props.showWeekNumbers /* even displaying weeknum for row, not necessarily day */, todayRange: props.todayRange, extraHookProps: cell.extraHookProps, extraDataAttrs: cell.extraDataAttrs, extraClassNames: cell.extraClassNames, moreCnt: moreCnts[col], buildMoreLinkText: props.buildMoreLinkText, onMoreClick: function (arg) { + props.onMoreClick(__assign(__assign({}, arg), { fromCol: col })); + }, segIsHidden: segIsHidden, moreMarginTop: moreTops[col] /* rename */, segsByEachCol: segsByEachCol[col], fgPaddingBottom: paddingBottoms[col], fgContentElRef: _this.fgElRefs.createRef(cell.key), fgContent: ( // Fragment scopes the keys + createElement(Fragment, null, + createElement(Fragment, null, normalFgNodes), + createElement(Fragment, null, mirrorFgNodes))), bgContent: ( // Fragment scopes the keys + createElement(Fragment, null, + _this.renderFillSegs(highlightSegsByCol[col], 'highlight'), + _this.renderFillSegs(businessHoursByCol[col], 'non-business'), + _this.renderFillSegs(bgEventSegsByCol[col], 'bg-event'))) })); + }))); + }; + TableRow.prototype.componentDidMount = function () { + this.updateSizing(true); + }; + TableRow.prototype.componentDidUpdate = function (prevProps, prevState) { + var currentProps = this.props; + this.updateSizing(!isPropsEqual(prevProps, currentProps)); + }; + TableRow.prototype.getHighlightSegs = function () { + var props = this.props; + if (props.eventDrag && props.eventDrag.segs.length) { // messy check + return props.eventDrag.segs; + } + if (props.eventResize && props.eventResize.segs.length) { // messy check + return props.eventResize.segs; + } + return props.dateSelectionSegs; + }; + TableRow.prototype.getMirrorSegs = function () { + var props = this.props; + if (props.eventResize && props.eventResize.segs.length) { // messy check + return props.eventResize.segs; + } + return []; + }; + TableRow.prototype.renderFgSegs = function (segs, segIsHidden, // does NOT mean display:hidden + segTops, segMarginTops, selectedInstanceHash, todayRange, isDragging, isResizing, isDateSelecting) { + var context = this.context; + var eventSelection = this.props.eventSelection; + var framePositions = this.state.framePositions; + var defaultDisplayEventEnd = this.props.cells.length === 1; // colCnt === 1 + var nodes = []; + if (framePositions) { + for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) { + var seg = segs_1[_i]; + var instanceId = seg.eventRange.instance.instanceId; + var isMirror = isDragging || isResizing || isDateSelecting; + var isSelected = selectedInstanceHash[instanceId]; + var isInvisible = segIsHidden[instanceId] || isSelected; + // TODO: simpler way? NOT DRY + var isAbsolute = segIsHidden[instanceId] || isMirror || seg.firstCol !== seg.lastCol || !seg.isStart || !seg.isEnd; + var marginTop = void 0; + var top_1 = void 0; + var left = void 0; + var right = void 0; + if (isAbsolute) { + top_1 = segTops[instanceId]; + if (context.isRtl) { + right = 0; + left = framePositions.lefts[seg.lastCol] - framePositions.lefts[seg.firstCol]; + } + else { + left = 0; + right = framePositions.rights[seg.firstCol] - framePositions.rights[seg.lastCol]; + } + } + else { + marginTop = segMarginTops[instanceId]; + } + /* + known bug: events that are force to be list-item but span multiple days still take up space in later columns + */ + nodes.push(createElement("div", { className: 'fc-daygrid-event-harness' + (isAbsolute ? ' fc-daygrid-event-harness-abs' : ''), key: instanceId, + // in print mode when in mult cols, could collide + ref: isMirror ? null : this.segHarnessRefs.createRef(instanceId + ':' + seg.firstCol), style: { + visibility: isInvisible ? 'hidden' : '', + marginTop: marginTop || '', + top: top_1 || '', + left: left || '', + right: right || '', + } }, hasListItemDisplay(seg) ? (createElement(TableListItemEvent, __assign({ seg: seg, isDragging: isDragging, isSelected: instanceId === eventSelection, defaultDisplayEventEnd: defaultDisplayEventEnd }, getSegMeta(seg, todayRange)))) : (createElement(TableBlockEvent, __assign({ seg: seg, isDragging: isDragging, isResizing: isResizing, isDateSelecting: isDateSelecting, isSelected: instanceId === eventSelection, defaultDisplayEventEnd: defaultDisplayEventEnd }, getSegMeta(seg, todayRange)))))); + } + } + return nodes; + }; + TableRow.prototype.renderFillSegs = function (segs, fillType) { + var isRtl = this.context.isRtl; + var todayRange = this.props.todayRange; + var framePositions = this.state.framePositions; + var nodes = []; + if (framePositions) { + for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) { + var seg = segs_2[_i]; + var leftRightCss = isRtl ? { + right: 0, + left: framePositions.lefts[seg.lastCol] - framePositions.lefts[seg.firstCol], + } : { + left: 0, + right: framePositions.rights[seg.firstCol] - framePositions.rights[seg.lastCol], + }; + nodes.push(createElement("div", { key: buildEventRangeKey(seg.eventRange), className: "fc-daygrid-bg-harness", style: leftRightCss }, fillType === 'bg-event' ? + createElement(BgEvent, __assign({ seg: seg }, getSegMeta(seg, todayRange))) : + renderFill(fillType))); + } + } + return createElement.apply(void 0, __spreadArrays([Fragment, {}], nodes)); + }; + TableRow.prototype.updateSizing = function (isExternalSizingChange) { + var _a = this, props = _a.props, frameElRefs = _a.frameElRefs; + if (props.clientWidth !== null) { // positioning ready? + if (isExternalSizingChange) { + var frameEls = props.cells.map(function (cell) { return frameElRefs.currentMap[cell.key]; }); + if (frameEls.length) { + var originEl = this.rootElRef.current; + this.setState({ + framePositions: new PositionCache(originEl, frameEls, true, // isHorizontal + false), + }); + } + } + var limitByContentHeight = props.dayMaxEvents === true || props.dayMaxEventRows === true; + this.setState({ + segHeights: this.computeSegHeights(), + maxContentHeight: limitByContentHeight ? this.computeMaxContentHeight() : null, + }); + } + }; + TableRow.prototype.computeSegHeights = function () { + return mapHash(this.segHarnessRefs.currentMap, function (eventHarnessEl) { return (eventHarnessEl.getBoundingClientRect().height); }); + }; + TableRow.prototype.computeMaxContentHeight = function () { + var firstKey = this.props.cells[0].key; + var cellEl = this.cellElRefs.currentMap[firstKey]; + var fcContainerEl = this.fgElRefs.currentMap[firstKey]; + return cellEl.getBoundingClientRect().bottom - fcContainerEl.getBoundingClientRect().top; + }; + TableRow.prototype.getCellEls = function () { + var elMap = this.cellElRefs.currentMap; + return this.props.cells.map(function (cell) { return elMap[cell.key]; }); + }; + return TableRow; + }(DateComponent)); + TableRow.addPropsEquality({ + onMoreClick: true, + }); + TableRow.addStateEquality({ + segHeights: isPropsEqual, + }); + + var PADDING_FROM_VIEWPORT = 10; + var SCROLL_DEBOUNCE = 10; + var Popover = /** @class */ (function (_super) { + __extends(Popover, _super); + function Popover() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.repositioner = new DelayedRunner(_this.updateSize.bind(_this)); + _this.handleRootEl = function (el) { + _this.rootEl = el; + if (_this.props.elRef) { + setRef(_this.props.elRef, el); + } + }; + // Triggered when the user clicks *anywhere* in the document, for the autoHide feature + _this.handleDocumentMousedown = function (ev) { + var onClose = _this.props.onClose; + // only hide the popover if the click happened outside the popover + if (onClose && !_this.rootEl.contains(ev.target)) { + onClose(); + } + }; + _this.handleDocumentScroll = function () { + _this.repositioner.request(SCROLL_DEBOUNCE); + }; + _this.handleCloseClick = function () { + var onClose = _this.props.onClose; + if (onClose) { + onClose(); + } + }; + return _this; + } + Popover.prototype.render = function () { + var theme = this.context.theme; + var props = this.props; + var classNames = [ + 'fc-popover', + theme.getClass('popover'), + ].concat(props.extraClassNames || []); + return (createElement("div", __assign({ className: classNames.join(' ') }, props.extraAttrs, { ref: this.handleRootEl }), + createElement("div", { className: 'fc-popover-header ' + theme.getClass('popoverHeader') }, + createElement("span", { className: "fc-popover-title" }, props.title), + createElement("span", { className: 'fc-popover-close ' + theme.getIconClass('close'), onClick: this.handleCloseClick })), + createElement("div", { className: 'fc-popover-body ' + theme.getClass('popoverContent') }, props.children))); + }; + Popover.prototype.componentDidMount = function () { + document.addEventListener('mousedown', this.handleDocumentMousedown); + document.addEventListener('scroll', this.handleDocumentScroll); + this.updateSize(); + }; + Popover.prototype.componentWillUnmount = function () { + document.removeEventListener('mousedown', this.handleDocumentMousedown); + document.removeEventListener('scroll', this.handleDocumentScroll); + }; + // TODO: adjust on window resize + /* + NOTE: the popover is position:fixed, so coordinates are relative to the viewport + NOTE: the PARENT calls this as well, on window resize. we would have wanted to use the repositioner, + but need to ensure that all other components have updated size first (for alignmentEl) + */ + Popover.prototype.updateSize = function () { + var _a = this.props, alignmentEl = _a.alignmentEl, topAlignmentEl = _a.topAlignmentEl; + var rootEl = this.rootEl; + if (!rootEl) { + return; // not sure why this was null, but we shouldn't let external components call updateSize() anyway + } + var dims = rootEl.getBoundingClientRect(); // only used for width,height + var alignment = alignmentEl.getBoundingClientRect(); + var top = topAlignmentEl ? topAlignmentEl.getBoundingClientRect().top : alignment.top; + top = Math.min(top, window.innerHeight - dims.height - PADDING_FROM_VIEWPORT); + top = Math.max(top, PADDING_FROM_VIEWPORT); + var left; + if (this.context.isRtl) { + left = alignment.right - dims.width; + } + else { + left = alignment.left; + } + left = Math.min(left, window.innerWidth - dims.width - PADDING_FROM_VIEWPORT); + left = Math.max(left, PADDING_FROM_VIEWPORT); + applyStyle(rootEl, { top: top, left: left }); + }; + return Popover; + }(BaseComponent)); + + var MorePopover = /** @class */ (function (_super) { + __extends(MorePopover, _super); + function MorePopover() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.rootElRef = createRef(); + return _this; + } + MorePopover.prototype.render = function () { + var _a = this.context, options = _a.options, dateEnv = _a.dateEnv; + var props = this.props; + var date = props.date, hiddenInstances = props.hiddenInstances, todayRange = props.todayRange, dateProfile = props.dateProfile, selectedInstanceId = props.selectedInstanceId; + var title = dateEnv.format(date, options.dayPopoverFormat); + return (createElement(DayCellRoot, { date: date, dateProfile: dateProfile, todayRange: todayRange, elRef: this.rootElRef }, function (rootElRef, dayClassNames, dataAttrs) { return (createElement(Popover, { elRef: rootElRef, title: title, extraClassNames: ['fc-more-popover'].concat(dayClassNames), extraAttrs: dataAttrs, onClose: props.onCloseClick, alignmentEl: props.alignmentEl, topAlignmentEl: props.topAlignmentEl }, + createElement(DayCellContent, { date: date, dateProfile: dateProfile, todayRange: todayRange }, function (innerElRef, innerContent) { return (innerContent && + createElement("div", { className: "fc-more-popover-misc", ref: innerElRef }, innerContent)); }), + props.segs.map(function (seg) { + var instanceId = seg.eventRange.instance.instanceId; + return (createElement("div", { className: "fc-daygrid-event-harness", key: instanceId, style: { + visibility: hiddenInstances[instanceId] ? 'hidden' : '', + } }, hasListItemDisplay(seg) ? (createElement(TableListItemEvent, __assign({ seg: seg, isDragging: false, isSelected: instanceId === selectedInstanceId, defaultDisplayEventEnd: false }, getSegMeta(seg, todayRange)))) : (createElement(TableBlockEvent, __assign({ seg: seg, isDragging: false, isResizing: false, isDateSelecting: false, isSelected: instanceId === selectedInstanceId, defaultDisplayEventEnd: false }, getSegMeta(seg, todayRange)))))); + }))); })); + }; + MorePopover.prototype.positionToHit = function (positionLeft, positionTop, originEl) { + var rootEl = this.rootElRef.current; + if (!originEl || !rootEl) { // why? + return null; + } + var originRect = originEl.getBoundingClientRect(); + var elRect = rootEl.getBoundingClientRect(); + var newOriginLeft = elRect.left - originRect.left; + var newOriginTop = elRect.top - originRect.top; + var localLeft = positionLeft - newOriginLeft; + var localTop = positionTop - newOriginTop; + var date = this.props.date; + if ( // ugly way to detect intersection + localLeft >= 0 && localLeft < elRect.width && + localTop >= 0 && localTop < elRect.height) { + return { + dateSpan: { + allDay: true, + range: { start: date, end: addDays(date, 1) }, + }, + dayEl: rootEl, + relativeRect: { + left: newOriginLeft, + top: newOriginTop, + right: elRect.width, + bottom: elRect.height, + }, + layer: 1, + }; + } + return null; + }; + return MorePopover; + }(DateComponent)); + + var Table = /** @class */ (function (_super) { + __extends(Table, _super); + function Table() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.splitBusinessHourSegs = memoize(splitSegsByRow); + _this.splitBgEventSegs = memoize(splitSegsByRow); + _this.splitFgEventSegs = memoize(splitSegsByRow); + _this.splitDateSelectionSegs = memoize(splitSegsByRow); + _this.splitEventDrag = memoize(splitInteractionByRow); + _this.splitEventResize = memoize(splitInteractionByRow); + _this.buildBuildMoreLinkText = memoize(buildBuildMoreLinkText); + _this.morePopoverRef = createRef(); + _this.rowRefs = new RefMap(); + _this.state = { + morePopoverState: null, + }; + _this.handleRootEl = function (rootEl) { + _this.rootEl = rootEl; + setRef(_this.props.elRef, rootEl); + }; + // TODO: bad names "more link click" versus "more click" + _this.handleMoreLinkClick = function (arg) { + var context = _this.context; + var dateEnv = context.dateEnv; + var clickOption = context.options.moreLinkClick; + function segForPublic(seg) { + var _a = seg.eventRange, def = _a.def, instance = _a.instance, range = _a.range; + return { + event: new EventApi(context, def, instance), + start: dateEnv.toDate(range.start), + end: dateEnv.toDate(range.end), + isStart: seg.isStart, + isEnd: seg.isEnd, + }; + } + if (typeof clickOption === 'function') { + clickOption = clickOption({ + date: dateEnv.toDate(arg.date), + allDay: true, + allSegs: arg.allSegs.map(segForPublic), + hiddenSegs: arg.hiddenSegs.map(segForPublic), + jsEvent: arg.ev, + view: context.viewApi, + }); // hack to handle void + } + if (!clickOption || clickOption === 'popover') { + _this.setState({ + morePopoverState: __assign(__assign({}, arg), { currentFgEventSegs: _this.props.fgEventSegs, fromRow: arg.fromRow, fromCol: arg.fromCol }), + }); + } + else if (typeof clickOption === 'string') { // a view name + context.calendarApi.zoomTo(arg.date, clickOption); + } + }; + _this.handleMorePopoverClose = function () { + _this.setState({ + morePopoverState: null, + }); + }; + return _this; + } + Table.prototype.render = function () { + var _this = this; + var props = this.props; + var dateProfile = props.dateProfile, dayMaxEventRows = props.dayMaxEventRows, dayMaxEvents = props.dayMaxEvents, expandRows = props.expandRows; + var morePopoverState = this.state.morePopoverState; + var rowCnt = props.cells.length; + var businessHourSegsByRow = this.splitBusinessHourSegs(props.businessHourSegs, rowCnt); + var bgEventSegsByRow = this.splitBgEventSegs(props.bgEventSegs, rowCnt); + var fgEventSegsByRow = this.splitFgEventSegs(props.fgEventSegs, rowCnt); + var dateSelectionSegsByRow = this.splitDateSelectionSegs(props.dateSelectionSegs, rowCnt); + var eventDragByRow = this.splitEventDrag(props.eventDrag, rowCnt); + var eventResizeByRow = this.splitEventResize(props.eventResize, rowCnt); + var buildMoreLinkText = this.buildBuildMoreLinkText(this.context.options.moreLinkText); + var limitViaBalanced = dayMaxEvents === true || dayMaxEventRows === true; + // if rows can't expand to fill fixed height, can't do balanced-height event limit + // TODO: best place to normalize these options? + if (limitViaBalanced && !expandRows) { + limitViaBalanced = false; + dayMaxEventRows = null; + dayMaxEvents = null; + } + var classNames = [ + 'fc-daygrid-body', + limitViaBalanced ? 'fc-daygrid-body-balanced' : 'fc-daygrid-body-unbalanced', + expandRows ? '' : 'fc-daygrid-body-natural', + ]; + return (createElement("div", { className: classNames.join(' '), ref: this.handleRootEl, style: { + // these props are important to give this wrapper correct dimensions for interactions + // TODO: if we set it here, can we avoid giving to inner tables? + width: props.clientWidth, + minWidth: props.tableMinWidth, + } }, + createElement(NowTimer, { unit: "day" }, function (nowDate, todayRange) { return (createElement(Fragment, null, + createElement("table", { className: "fc-scrollgrid-sync-table", style: { + width: props.clientWidth, + minWidth: props.tableMinWidth, + height: expandRows ? props.clientHeight : '', + } }, + props.colGroupNode, + createElement("tbody", null, props.cells.map(function (cells, row) { return (createElement(TableRow, { ref: _this.rowRefs.createRef(row), key: cells.length + ? cells[0].date.toISOString() /* best? or put key on cell? or use diff formatter? */ + : row // in case there are no cells (like when resource view is loading) + , showDayNumbers: rowCnt > 1, showWeekNumbers: props.showWeekNumbers, todayRange: todayRange, dateProfile: dateProfile, cells: cells, renderIntro: props.renderRowIntro, businessHourSegs: businessHourSegsByRow[row], eventSelection: props.eventSelection, bgEventSegs: bgEventSegsByRow[row].filter(isSegAllDay) /* hack */, fgEventSegs: fgEventSegsByRow[row], dateSelectionSegs: dateSelectionSegsByRow[row], eventDrag: eventDragByRow[row], eventResize: eventResizeByRow[row], dayMaxEvents: dayMaxEvents, dayMaxEventRows: dayMaxEventRows, clientWidth: props.clientWidth, clientHeight: props.clientHeight, buildMoreLinkText: buildMoreLinkText, onMoreClick: function (arg) { + _this.handleMoreLinkClick(__assign(__assign({}, arg), { fromRow: row })); + } })); }))), + (!props.forPrint && morePopoverState && morePopoverState.currentFgEventSegs === props.fgEventSegs) && (createElement(MorePopover, { ref: _this.morePopoverRef, date: morePopoverState.date, dateProfile: dateProfile, segs: morePopoverState.allSegs, alignmentEl: morePopoverState.dayEl, topAlignmentEl: rowCnt === 1 ? props.headerAlignElRef.current : null, onCloseClick: _this.handleMorePopoverClose, selectedInstanceId: props.eventSelection, hiddenInstances: // yuck + (props.eventDrag ? props.eventDrag.affectedInstances : null) || + (props.eventResize ? props.eventResize.affectedInstances : null) || + {}, todayRange: todayRange })))); }))); + }; + // Hit System + // ---------------------------------------------------------------------------------------------------- + Table.prototype.prepareHits = function () { + this.rowPositions = new PositionCache(this.rootEl, this.rowRefs.collect().map(function (rowObj) { return rowObj.getCellEls()[0]; }), // first cell el in each row. TODO: not optimal + false, true); + this.colPositions = new PositionCache(this.rootEl, this.rowRefs.currentMap[0].getCellEls(), // cell els in first row + true, // horizontal + false); + }; + Table.prototype.positionToHit = function (leftPosition, topPosition) { + var morePopover = this.morePopoverRef.current; + var morePopoverHit = morePopover ? morePopover.positionToHit(leftPosition, topPosition, this.rootEl) : null; + var morePopoverState = this.state.morePopoverState; + if (morePopoverHit) { + return __assign({ row: morePopoverState.fromRow, col: morePopoverState.fromCol }, morePopoverHit); + } + var _a = this, colPositions = _a.colPositions, rowPositions = _a.rowPositions; + var col = colPositions.leftToIndex(leftPosition); + var row = rowPositions.topToIndex(topPosition); + if (row != null && col != null) { + return { + row: row, + col: col, + dateSpan: { + range: this.getCellRange(row, col), + allDay: true, + }, + dayEl: this.getCellEl(row, col), + relativeRect: { + left: colPositions.lefts[col], + right: colPositions.rights[col], + top: rowPositions.tops[row], + bottom: rowPositions.bottoms[row], + }, + }; + } + return null; + }; + Table.prototype.getCellEl = function (row, col) { + return this.rowRefs.currentMap[row].getCellEls()[col]; // TODO: not optimal + }; + Table.prototype.getCellRange = function (row, col) { + var start = this.props.cells[row][col].date; + var end = addDays(start, 1); + return { start: start, end: end }; + }; + return Table; + }(DateComponent)); + function buildBuildMoreLinkText(moreLinkTextInput) { + if (typeof moreLinkTextInput === 'function') { + return moreLinkTextInput; + } + return function (num) { return "+" + num + " " + moreLinkTextInput; }; + } + function isSegAllDay(seg) { + return seg.eventRange.def.allDay; + } + + var DayTableSlicer = /** @class */ (function (_super) { + __extends(DayTableSlicer, _super); + function DayTableSlicer() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.forceDayIfListItem = true; + return _this; + } + DayTableSlicer.prototype.sliceRange = function (dateRange, dayTableModel) { + return dayTableModel.sliceRange(dateRange); + }; + return DayTableSlicer; + }(Slicer)); + + var DayTable = /** @class */ (function (_super) { + __extends(DayTable, _super); + function DayTable() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.slicer = new DayTableSlicer(); + _this.tableRef = createRef(); + _this.handleRootEl = function (rootEl) { + if (rootEl) { + _this.context.registerInteractiveComponent(_this, { el: rootEl }); + } + else { + _this.context.unregisterInteractiveComponent(_this); + } + }; + return _this; + } + DayTable.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context; + return (createElement(Table, __assign({ ref: this.tableRef, elRef: this.handleRootEl }, this.slicer.sliceProps(props, props.dateProfile, props.nextDayThreshold, context, props.dayTableModel), { dateProfile: props.dateProfile, cells: props.dayTableModel.cells, colGroupNode: props.colGroupNode, tableMinWidth: props.tableMinWidth, renderRowIntro: props.renderRowIntro, dayMaxEvents: props.dayMaxEvents, dayMaxEventRows: props.dayMaxEventRows, showWeekNumbers: props.showWeekNumbers, expandRows: props.expandRows, headerAlignElRef: props.headerAlignElRef, clientWidth: props.clientWidth, clientHeight: props.clientHeight, forPrint: props.forPrint }))); + }; + DayTable.prototype.prepareHits = function () { + this.tableRef.current.prepareHits(); + }; + DayTable.prototype.queryHit = function (positionLeft, positionTop) { + var rawHit = this.tableRef.current.positionToHit(positionLeft, positionTop); + if (rawHit) { + return { + component: this, + dateSpan: rawHit.dateSpan, + dayEl: rawHit.dayEl, + rect: { + left: rawHit.relativeRect.left, + right: rawHit.relativeRect.right, + top: rawHit.relativeRect.top, + bottom: rawHit.relativeRect.bottom, + }, + layer: 0, + }; + } + return null; + }; + return DayTable; + }(DateComponent)); + + var DayTableView = /** @class */ (function (_super) { + __extends(DayTableView, _super); + function DayTableView() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.buildDayTableModel = memoize(buildDayTableModel); + _this.headerRef = createRef(); + _this.tableRef = createRef(); + return _this; + } + DayTableView.prototype.render = function () { + var _this = this; + var _a = this.context, options = _a.options, dateProfileGenerator = _a.dateProfileGenerator; + var props = this.props; + var dayTableModel = this.buildDayTableModel(props.dateProfile, dateProfileGenerator); + var headerContent = options.dayHeaders && (createElement(DayHeader, { ref: this.headerRef, dateProfile: props.dateProfile, dates: dayTableModel.headerDates, datesRepDistinctDays: dayTableModel.rowCnt === 1 })); + var bodyContent = function (contentArg) { return (createElement(DayTable, { ref: _this.tableRef, dateProfile: props.dateProfile, dayTableModel: dayTableModel, businessHours: props.businessHours, dateSelection: props.dateSelection, eventStore: props.eventStore, eventUiBases: props.eventUiBases, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, nextDayThreshold: options.nextDayThreshold, colGroupNode: contentArg.tableColGroupNode, tableMinWidth: contentArg.tableMinWidth, dayMaxEvents: options.dayMaxEvents, dayMaxEventRows: options.dayMaxEventRows, showWeekNumbers: options.weekNumbers, expandRows: !props.isHeightAuto, headerAlignElRef: _this.headerElRef, clientWidth: contentArg.clientWidth, clientHeight: contentArg.clientHeight, forPrint: props.forPrint })); }; + return options.dayMinWidth + ? this.renderHScrollLayout(headerContent, bodyContent, dayTableModel.colCnt, options.dayMinWidth) + : this.renderSimpleLayout(headerContent, bodyContent); + }; + return DayTableView; + }(TableView)); + function buildDayTableModel(dateProfile, dateProfileGenerator) { + var daySeries = new DaySeriesModel(dateProfile.renderRange, dateProfileGenerator); + return new DayTableModel(daySeries, /year|month|week/.test(dateProfile.currentRangeUnit)); + } + + var TableDateProfileGenerator = /** @class */ (function (_super) { + __extends(TableDateProfileGenerator, _super); + function TableDateProfileGenerator() { + return _super !== null && _super.apply(this, arguments) || this; + } + // Computes the date range that will be rendered. + TableDateProfileGenerator.prototype.buildRenderRange = function (currentRange, currentRangeUnit, isRangeAllDay) { + var dateEnv = this.props.dateEnv; + var renderRange = _super.prototype.buildRenderRange.call(this, currentRange, currentRangeUnit, isRangeAllDay); + var start = renderRange.start; + var end = renderRange.end; + var endOfWeek; + // year and month views should be aligned with weeks. this is already done for week + if (/^(year|month)$/.test(currentRangeUnit)) { + start = dateEnv.startOfWeek(start); + // make end-of-week if not already + endOfWeek = dateEnv.startOfWeek(end); + if (endOfWeek.valueOf() !== end.valueOf()) { + end = addWeeks(endOfWeek, 1); + } + } + // ensure 6 weeks + if (this.props.monthMode && + this.props.fixedWeekCount) { + var rowCnt = Math.ceil(// could be partial weeks due to hiddenDays + diffWeeks(start, end)); + end = addWeeks(end, 6 - rowCnt); + } + return { start: start, end: end }; + }; + return TableDateProfileGenerator; + }(DateProfileGenerator)); + + var OPTION_REFINERS$1 = { + moreLinkClick: identity, + moreLinkClassNames: identity, + moreLinkContent: identity, + moreLinkDidMount: identity, + moreLinkWillUnmount: identity, + }; + + var dayGridPlugin = createPlugin({ + initialView: 'dayGridMonth', + optionRefiners: OPTION_REFINERS$1, + views: { + dayGrid: { + component: DayTableView, + dateProfileGeneratorClass: TableDateProfileGenerator, + }, + dayGridDay: { + type: 'dayGrid', + duration: { days: 1 }, + }, + dayGridWeek: { + type: 'dayGrid', + duration: { weeks: 1 }, + }, + dayGridMonth: { + type: 'dayGrid', + duration: { months: 1 }, + monthMode: true, + fixedWeekCount: true, + }, + }, + }); + + var AllDaySplitter = /** @class */ (function (_super) { + __extends(AllDaySplitter, _super); + function AllDaySplitter() { + return _super !== null && _super.apply(this, arguments) || this; + } + AllDaySplitter.prototype.getKeyInfo = function () { + return { + allDay: {}, + timed: {}, + }; + }; + AllDaySplitter.prototype.getKeysForDateSpan = function (dateSpan) { + if (dateSpan.allDay) { + return ['allDay']; + } + return ['timed']; + }; + AllDaySplitter.prototype.getKeysForEventDef = function (eventDef) { + if (!eventDef.allDay) { + return ['timed']; + } + if (hasBgRendering(eventDef)) { + return ['timed', 'allDay']; + } + return ['allDay']; + }; + return AllDaySplitter; + }(Splitter)); + + var DEFAULT_SLAT_LABEL_FORMAT = createFormatter({ + hour: 'numeric', + minute: '2-digit', + omitZeroMinute: true, + meridiem: 'short', + }); + function TimeColsAxisCell(props) { + var classNames = [ + 'fc-timegrid-slot', + 'fc-timegrid-slot-label', + props.isLabeled ? 'fc-scrollgrid-shrink' : 'fc-timegrid-slot-minor', + ]; + return (createElement(ViewContextType.Consumer, null, function (context) { + if (!props.isLabeled) { + return (createElement("td", { className: classNames.join(' '), "data-time": props.isoTimeStr })); + } + var dateEnv = context.dateEnv, options = context.options, viewApi = context.viewApi; + var labelFormat = // TODO: fully pre-parse + options.slotLabelFormat == null ? DEFAULT_SLAT_LABEL_FORMAT : + Array.isArray(options.slotLabelFormat) ? createFormatter(options.slotLabelFormat[0]) : + createFormatter(options.slotLabelFormat); + var hookProps = { + level: 0, + time: props.time, + date: dateEnv.toDate(props.date), + view: viewApi, + text: dateEnv.format(props.date, labelFormat), + }; + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.slotLabelClassNames, content: options.slotLabelContent, defaultContent: renderInnerContent$3, didMount: options.slotLabelDidMount, willUnmount: options.slotLabelWillUnmount }, function (rootElRef, customClassNames, innerElRef, innerContent) { return (createElement("td", { ref: rootElRef, className: classNames.concat(customClassNames).join(' '), "data-time": props.isoTimeStr }, + createElement("div", { className: "fc-timegrid-slot-label-frame fc-scrollgrid-shrink-frame" }, + createElement("div", { className: "fc-timegrid-slot-label-cushion fc-scrollgrid-shrink-cushion", ref: innerElRef }, innerContent)))); })); + })); + } + function renderInnerContent$3(props) { + return props.text; + } + + var TimeBodyAxis = /** @class */ (function (_super) { + __extends(TimeBodyAxis, _super); + function TimeBodyAxis() { + return _super !== null && _super.apply(this, arguments) || this; + } + TimeBodyAxis.prototype.render = function () { + return this.props.slatMetas.map(function (slatMeta) { return (createElement("tr", { key: slatMeta.key }, + createElement(TimeColsAxisCell, __assign({}, slatMeta)))); }); + }; + return TimeBodyAxis; + }(BaseComponent)); + + var DEFAULT_WEEK_NUM_FORMAT$1 = createFormatter({ week: 'short' }); + var AUTO_ALL_DAY_MAX_EVENT_ROWS = 5; + var TimeColsView = /** @class */ (function (_super) { + __extends(TimeColsView, _super); + function TimeColsView() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.allDaySplitter = new AllDaySplitter(); // for use by subclasses + _this.headerElRef = createRef(); + _this.rootElRef = createRef(); + _this.scrollerElRef = createRef(); + _this.state = { + slatCoords: null, + }; + _this.handleScrollTopRequest = function (scrollTop) { + var scrollerEl = _this.scrollerElRef.current; + if (scrollerEl) { // TODO: not sure how this could ever be null. weirdness with the reducer + scrollerEl.scrollTop = scrollTop; + } + }; + /* Header Render Methods + ------------------------------------------------------------------------------------------------------------------*/ + _this.renderHeadAxis = function (rowKey, frameHeight) { + if (frameHeight === void 0) { frameHeight = ''; } + var options = _this.context.options; + var dateProfile = _this.props.dateProfile; + var range = dateProfile.renderRange; + var dayCnt = diffDays(range.start, range.end); + var navLinkAttrs = (options.navLinks && dayCnt === 1) // only do in day views (to avoid doing in week views that dont need it) + ? { 'data-navlink': buildNavLinkData(range.start, 'week'), tabIndex: 0 } + : {}; + if (options.weekNumbers && rowKey === 'day') { + return (createElement(WeekNumberRoot, { date: range.start, defaultFormat: DEFAULT_WEEK_NUM_FORMAT$1 }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("th", { ref: rootElRef, className: [ + 'fc-timegrid-axis', + 'fc-scrollgrid-shrink', + ].concat(classNames).join(' ') }, + createElement("div", { className: "fc-timegrid-axis-frame fc-scrollgrid-shrink-frame fc-timegrid-axis-frame-liquid", style: { height: frameHeight } }, + createElement("a", __assign({ ref: innerElRef, className: "fc-timegrid-axis-cushion fc-scrollgrid-shrink-cushion fc-scrollgrid-sync-inner" }, navLinkAttrs), innerContent)))); })); + } + return (createElement("th", { className: "fc-timegrid-axis" }, + createElement("div", { className: "fc-timegrid-axis-frame", style: { height: frameHeight } }))); + }; + /* Table Component Render Methods + ------------------------------------------------------------------------------------------------------------------*/ + // only a one-way height sync. we don't send the axis inner-content height to the DayGrid, + // but DayGrid still needs to have classNames on inner elements in order to measure. + _this.renderTableRowAxis = function (rowHeight) { + var _a = _this.context, options = _a.options, viewApi = _a.viewApi; + var hookProps = { + text: options.allDayText, + view: viewApi, + }; + return ( + // TODO: make reusable hook. used in list view too + createElement(RenderHook, { hookProps: hookProps, classNames: options.allDayClassNames, content: options.allDayContent, defaultContent: renderAllDayInner, didMount: options.allDayDidMount, willUnmount: options.allDayWillUnmount }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("td", { ref: rootElRef, className: [ + 'fc-timegrid-axis', + 'fc-scrollgrid-shrink', + ].concat(classNames).join(' ') }, + createElement("div", { className: 'fc-timegrid-axis-frame fc-scrollgrid-shrink-frame' + (rowHeight == null ? ' fc-timegrid-axis-frame-liquid' : ''), style: { height: rowHeight } }, + createElement("span", { className: "fc-timegrid-axis-cushion fc-scrollgrid-shrink-cushion fc-scrollgrid-sync-inner", ref: innerElRef }, innerContent)))); })); + }; + _this.handleSlatCoords = function (slatCoords) { + _this.setState({ slatCoords: slatCoords }); + }; + return _this; + } + // rendering + // ---------------------------------------------------------------------------------------------------- + TimeColsView.prototype.renderSimpleLayout = function (headerRowContent, allDayContent, timeContent) { + var _a = this, context = _a.context, props = _a.props; + var sections = []; + var stickyHeaderDates = getStickyHeaderDates(context.options); + if (headerRowContent) { + sections.push({ + type: 'header', + key: 'header', + isSticky: stickyHeaderDates, + chunk: { + elRef: this.headerElRef, + tableClassName: 'fc-col-header', + rowContent: headerRowContent, + }, + }); + } + if (allDayContent) { + sections.push({ + type: 'body', + key: 'all-day', + chunk: { content: allDayContent }, + }); + sections.push({ + type: 'body', + key: 'all-day-divider', + outerContent: ( // TODO: rename to cellContent so don't need to define
+
+ + {% endblock %} @@ -113,11 +119,11 @@ InvenTree | {% trans "Sales Orders" %} var calendar = null; document.addEventListener('DOMContentLoaded', function() { - var calendarEl = document.getElementById('calendar'); + var calendarEl = document.getElementById('sales-order-calendar'); calendar = new FullCalendar.Calendar(calendarEl, { initialView: 'dayGridMonth', nowIndicator: true, - aspectRatio: 2, + aspectRatio: 2.5, width: '80%', datesSet: function() { loadOrderEvents(calendar); @@ -133,6 +139,32 @@ InvenTree | {% trans "Sales Orders" %} {% block js_ready %} {{ block.super }} +$("#sales-order-calendar").hide(); +$("#view-list").hide(); + +$('#view-calendar').click(function() { + // Hide the list view, show the calendar view + $("#sales-order-table").hide(); + $("#view-calendar").hide(); + $(".fixed-table-pagination").hide(); + $(".columns-right").hide(); + $(".search").hide(); + + $("#sales-order-calendar").show(); + $("#view-list").show(); +}); + +$("#view-list").click(function() { + // Hide the calendar view, show the list view + $("#sales-order-calendar").hide(); + $("#view-list").hide(); + + $(".fixed-table-pagination").show(); + $(".columns-right").show(); + $(".search").show(); + $("#sales-order-table").show(); + $("#view-calendar").show(); +}); loadSalesOrderTable("#sales-order-table", { url: "{% url 'api-so-list' %}", From 38b6367453d5d804631e0ccfe1033d80361327cf Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 Jan 2021 22:34:17 +1100 Subject: [PATCH 37/40] PEP fixes --- InvenTree/order/api.py | 2 -- InvenTree/order/models.py | 14 +++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 12f0854c19..e240c69861 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -9,8 +9,6 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics from rest_framework import filters -from django.db.models import Q - from django.conf.urls import url, include from InvenTree.helpers import str2bool diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index d0373fa324..27bcd4acca 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -316,26 +316,26 @@ class SalesOrder(Order): - TODO: An "overdue" order where the target date is in the past """ - DATE_FMT = '%Y-%m-%d' # ISO format date string + date_fmt = '%Y-%m-%d' # ISO format date string # Ensure that both dates are valid try: - min_date = datetime.strptime(str(min_date), DATE_FMT).date() - max_date = datetime.strptime(str(max_date), DATE_FMT).date() + min_date = datetime.strptime(str(min_date), date_fmt).date() + max_date = datetime.strptime(str(max_date), date_fmt).date() except (ValueError, TypeError): # Date processing error, return queryset unchanged return queryset # Construct a queryset for "completed" orders within the range - COMPLETED = Q(status__in=SalesOrderStatus.COMPLETE) & Q(shipment_date__gte=min_date) & Q(shipment_date__lte=max_date) + completed = Q(status__in=SalesOrderStatus.COMPLETE) & Q(shipment_date__gte=min_date) & Q(shipment_date__lte=max_date) # Construct a queryset for "pending" orders within the range - PENDING = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date) + pending = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date) # Construct a queryset for "overdue" orders within the range - FILTER = COMPLETED | PENDING + flt = completed | pending - queryset = queryset.filter(FILTER) + queryset = queryset.filter(flt) return queryset From 76c86e7b2f54ed9ba2ee3ff88d7ceb35f3765f43 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 Jan 2021 23:04:00 +1100 Subject: [PATCH 38/40] Calendar view for purchase orders --- InvenTree/InvenTree/urls.py | 1 + InvenTree/order/api.py | 9 +- InvenTree/order/models.py | 43 ++++++- .../templates/order/purchase_orders.html | 112 ++++++++++++++++++ .../order/templates/order/sales_orders.html | 24 +--- InvenTree/templates/base.html | 1 + InvenTree/templates/js/calendar.js | 25 ++++ 7 files changed, 193 insertions(+), 22 deletions(-) create mode 100644 InvenTree/templates/js/calendar.js diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 944c6432b5..79c662182e 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -93,6 +93,7 @@ dynamic_javascript_urls = [ url(r'^barcode.js', DynamicJsView.as_view(template_name='js/barcode.js'), name='barcode.js'), url(r'^bom.js', DynamicJsView.as_view(template_name='js/bom.js'), name='bom.js'), url(r'^build.js', DynamicJsView.as_view(template_name='js/build.js'), name='build.js'), + url(r'^calendar.js', DynamicJsView.as_view(template_name='js/calendar.js'), name='calendar.js'), url(r'^company.js', DynamicJsView.as_view(template_name='js/company.js'), name='company.js'), url(r'^order.js', DynamicJsView.as_view(template_name='js/order.js'), name='order.js'), url(r'^part.js', DynamicJsView.as_view(template_name='js/part.js'), name='part.js'), diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index e240c69861..972ff16f9c 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -107,6 +107,13 @@ class POList(generics.ListCreateAPIView): except (ValueError, SupplierPart.DoesNotExist): pass + # Filter by 'date range' + min_date = params.get('min_date', None) + max_date = params.get('max_date', None) + + if min_date is not None and max_date is not None: + queryset = PurchaseOrder.filterByDate(queryset, min_date, max_date) + return queryset filter_backends = [ @@ -298,7 +305,7 @@ class SOList(generics.ListCreateAPIView): max_date = params.get('max_date', None) if min_date is not None and max_date is not None: - queryset = SalesOrder.filter_interesting_orders(queryset, min_date, max_date) + queryset = SalesOrder.filterByDate(queryset, min_date, max_date) return queryset diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 27bcd4acca..08b88264ae 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -121,6 +121,44 @@ class PurchaseOrder(Order): received_by: User that received the goods """ + @staticmethod + def filterByDate(queryset, min_date, max_date): + """ + Filter by 'minimum and maximum date range' + + - Specified as min_date, max_date + - Both must be specified for filter to be applied + - Determine which "interesting" orders exist bewteen these dates + + To be "interesting": + - A "received" order where the received date lies within the date range + - TODO: A "pending" order where the target date lies within the date range + - TODO: An "overdue" order where the target date is in the past + """ + + date_fmt = '%Y-%m-%d' # ISO format date string + + # Ensure that both dates are valid + try: + min_date = datetime.strptime(str(min_date), date_fmt).date() + max_date = datetime.strptime(str(max_date), date_fmt).date() + except (ValueError, TypeError): + # Date processing error, return queryset unchanged + return queryset + + # Construct a queryset for "received" orders within the range + received = Q(status=PurchaseOrderStatus.COMPLETE) & Q(complete_date__gte=min_date) & Q(complete_date__lte=max_date) + + # TODO - Construct a queryset for "pending" orders within the range + + # TODO - Construct a queryset for "overdue" orders within the range + + flt = received + + queryset = queryset.filter(flt) + + return queryset + def __str__(self): prefix = getSetting('PURCHASEORDER_REFERENCE_PREFIX') @@ -302,7 +340,7 @@ class SalesOrder(Order): OVERDUE_FILTER = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date()) @staticmethod - def filter_interesting_orders(queryset, min_date, max_date): + def filterByDate(queryset, min_date, max_date): """ Filter by "minimum and maximum date range" @@ -332,7 +370,8 @@ class SalesOrder(Order): # Construct a queryset for "pending" orders within the range pending = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date) - # Construct a queryset for "overdue" orders within the range + # TODO: Construct a queryset for "overdue" orders within the range + flt = completed | pending queryset = queryset.filter(flt) diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html index 347033a888..53502376cb 100644 --- a/InvenTree/order/templates/order/purchase_orders.html +++ b/InvenTree/order/templates/order/purchase_orders.html @@ -1,5 +1,6 @@ {% extends "base.html" %} +{% load inventree_extras %} {% load static %} {% load i18n %} @@ -18,6 +19,12 @@ InvenTree | {% trans "Purchase Orders" %} {% endif %} + +
@@ -27,11 +34,116 @@ InvenTree | {% trans "Purchase Orders" %}
+
+ +{% endblock %} + +{% block js_load %} +{{ block.super }} + + + {% endblock %} {% block js_ready %} {{ block.super }} +$('#purchase-order-calendar').hide(); +$('#view-list').hide(); + +$('#view-calendar').click(function() { + // Hide the list view, show the calendar view + $("#purchase-order-table").hide(); + $("#view-calendar").hide(); + $(".fixed-table-pagination").hide(); + $(".columns-right").hide(); + $(".search").hide(); + $('#filter-list-salesorder').hide(); + + $("#purchase-order-calendar").show(); + $("#view-list").show(); +}); + +$("#view-list").click(function() { + // Hide the calendar view, show the list view + $("#purchase-order-calendar").hide(); + $("#view-list").hide(); + + $(".fixed-table-pagination").show(); + $(".columns-right").show(); + $(".search").show(); + $("#purchase-order-table").show(); + $('#filter-list-salesorder').show(); + $("#view-calendar").show(); +}); + $("#po-create").click(function() { launchModalForm("{% url 'po-create' %}", { diff --git a/InvenTree/order/templates/order/sales_orders.html b/InvenTree/order/templates/order/sales_orders.html index f75f9fa36b..257ee13887 100644 --- a/InvenTree/order/templates/order/sales_orders.html +++ b/InvenTree/order/templates/order/sales_orders.html @@ -12,7 +12,6 @@ InvenTree | {% trans "Sales Orders" %}

{% trans "Sales Orders" %}


-
@@ -34,10 +33,9 @@ InvenTree | {% trans "Sales Orders" %}
+
- - {% endblock %} {% block js_load %} @@ -45,25 +43,12 @@ InvenTree | {% trans "Sales Orders" %} + diff --git a/InvenTree/templates/js/calendar.js b/InvenTree/templates/js/calendar.js new file mode 100644 index 0000000000..861bbe1727 --- /dev/null +++ b/InvenTree/templates/js/calendar.js @@ -0,0 +1,25 @@ +{% load i18n %} + +/** + * Helper functions for calendar display + */ + +function startDate(calendar) { + // Extract the first displayed date on the calendar + return calendar.currentData.dateProfile.activeRange.start.toISOString().split("T")[0]; +} + +function endDate(calendar) { + // Extract the last display date on the calendar + return calendar.currentData.dateProfile.activeRange.end.toISOString().split("T")[0]; +} + +function clearEvents(calendar) { + // Remove all events from the calendar + + var events = calendar.getEvents(); + + events.forEach(function(event) { + event.remove(); + }) +} \ No newline at end of file From 47b0f40e97b7470499f660915d2faf4544874cdb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 Jan 2021 23:41:54 +1100 Subject: [PATCH 39/40] Calendar view for build orders --- InvenTree/build/api.py | 7 + InvenTree/build/models.py | 31 +++++ .../build/templates/build/build_base.html | 9 ++ InvenTree/build/templates/build/index.html | 123 +++++++++++++++++- InvenTree/order/models.py | 4 +- .../templates/order/purchase_orders.html | 12 +- 6 files changed, 179 insertions(+), 7 deletions(-) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index ffc8d97a92..d76c0a4b51 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -90,6 +90,13 @@ class BuildList(generics.ListCreateAPIView): if part is not None: queryset = queryset.filter(part=part) + # Filter by 'date range' + min_date = params.get('min_date', None) + max_date = params.get('max_date', None) + + if min_date is not None and max_date is not None: + queryset = Build.filterByDate(queryset, min_date, max_date) + return queryset def get_serializer(self, *args, **kwargs): diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index c3b399d820..10b3b00259 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -61,6 +61,37 @@ class Build(MPTTModel): verbose_name = _("Build Order") verbose_name_plural = _("Build Orders") + @staticmethod + def filterByDate(queryset, min_date, max_date): + """ + Filter by 'minimum and maximum date range' + + - Specified as min_date, max_date + - Both must be specified for filter to be applied + """ + + date_fmt = '%Y-%m-%d' # ISO format date string + + # Ensure that both dates are valid + try: + min_date = datetime.strptime(str(min_date), date_fmt).date() + max_date = datetime.strptime(str(max_date), date_fmt).date() + except (ValueError, TypeError): + # Date processing error, return queryset unchanged + return queryset + + # Order was completed within the specified range + completed = Q(status=BuildStatus.COMPLETE) & Q(completion_date__gte=min_date) & Q(completion_date__lte=max_date) + + # Order target date falls witin specified range + pending = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date) + + # TODO - Construct a queryset for "overdue" orders + + queryset = queryset.filter(completed | pending) + + return queryset + def __str__(self): prefix = getSetting("BUILDORDER_REFERENCE_PREFIX") diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index 1124dd16c0..9ca3fff818 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -88,11 +88,20 @@ src="{% static 'img/blank_image.png' %}" {% trans "Status" %} {% build_status_label build.status %} + + + {% if build.target_date %} + + + {% trans "Target Date" %} + + {{ build.target_date }} {% if build.is_overdue %} {% trans "Overdue" %} {% endif %} + {% endif %} {% trans "Progress" %} diff --git a/InvenTree/build/templates/build/index.html b/InvenTree/build/templates/build/index.html index 562dc746fb..37993107a7 100644 --- a/InvenTree/build/templates/build/index.html +++ b/InvenTree/build/templates/build/index.html @@ -1,4 +1,6 @@ {% extends "base.html" %} + +{% load inventree_extras %} {% load static %} {% load i18n %} @@ -8,7 +10,6 @@ InvenTree | {% trans "Build Orders" %} {% block content %} -

{% trans "Build Orders" %}

@@ -21,8 +22,17 @@ InvenTree | {% trans "Build Orders" %}
+ {% if roles.build.add %} + {% trans "New Build Order" %} + + {% endif %} + +
@@ -33,11 +43,120 @@ InvenTree | {% trans "Build Orders" %}
+
+ +{% endblock %} + +{% block js_load %} +{{ block.super }} + + {% endblock %} {% block js_ready %} {{ block.super }} +$('#build-order-calendar').hide(); +$('#view-list').hide(); + +$('#view-calendar').click(function() { + // Hide the list view, show the calendar view + $("#build-table").hide(); + $("#view-calendar").hide(); + $(".fixed-table-pagination").hide(); + $(".columns-right").hide(); + $(".search").hide(); + + $("#build-order-calendar").show(); + $("#view-list").show(); +}); + +$("#view-list").click(function() { + // Hide the calendar view, show the list view + $("#build-order-calendar").hide(); + $("#view-list").hide(); + + $(".fixed-table-pagination").show(); + $(".columns-right").show(); + $(".search").show(); + $("#build-table").show(); + $("#view-calendar").show(); +}); + $("#collapse-item-active").collapse().show(); $("#new-build").click(function() { diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 08b88264ae..184452f24e 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -372,9 +372,7 @@ class SalesOrder(Order): # TODO: Construct a queryset for "overdue" orders within the range - flt = completed | pending - - queryset = queryset.filter(flt) + queryset = queryset.filter(completed | pending) return queryset diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html index 53502376cb..d05e8fbf86 100644 --- a/InvenTree/order/templates/order/purchase_orders.html +++ b/InvenTree/order/templates/order/purchase_orders.html @@ -74,7 +74,15 @@ InvenTree | {% trans "Purchase Orders" %} var title = `${prefix}${order.reference} - ${order.supplier_detail.name}`; - var color = '#25c235'; + var color = '#4c68f5'; + + if (order.complete_date) { + color = '#25c235'; + } else if (order.overdue) { + color = '#c22525'; + } else { + color = '#4c68f5'; + } var event = { title: title, @@ -106,7 +114,7 @@ InvenTree | {% trans "Purchase Orders" %} }); calendar.render(); - }) + }); From 75f31ecc6331edf5f04509028c4b9a637433026e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 Jan 2021 23:50:34 +1100 Subject: [PATCH 40/40] Update translations --- InvenTree/locale/de/LC_MESSAGES/django.po | 468 +++++++++++----------- InvenTree/locale/en/LC_MESSAGES/django.po | 462 ++++++++++----------- InvenTree/locale/es/LC_MESSAGES/django.po | 462 ++++++++++----------- 3 files changed, 718 insertions(+), 674 deletions(-) diff --git a/InvenTree/locale/de/LC_MESSAGES/django.po b/InvenTree/locale/de/LC_MESSAGES/django.po index 3dd1bc4ac8..31e3f80413 100644 --- a/InvenTree/locale/de/LC_MESSAGES/django.po +++ b/InvenTree/locale/de/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-01-06 23:11+1100\n" +"POT-Creation-Date: 2021-01-07 23:48+1100\n" "PO-Revision-Date: 2020-05-03 11:32+0200\n" "Last-Translator: Christian Schlüter \n" "Language-Team: C \n" @@ -61,7 +61,7 @@ msgstr "" msgid "Select Category" msgstr "Teilkategorie auswählen" -#: InvenTree/helpers.py:361 order/models.py:178 order/models.py:260 +#: InvenTree/helpers.py:361 order/models.py:216 order/models.py:298 #: stock/views.py:1660 msgid "Invalid quantity provided" msgstr "Keine gültige Menge" @@ -147,7 +147,7 @@ msgid "InvenTree system health checks failed" msgstr "Instanzname" #: InvenTree/status_codes.py:94 InvenTree/status_codes.py:135 -#: InvenTree/status_codes.py:223 +#: InvenTree/status_codes.py:228 msgid "Pending" msgstr "Ausstehend" @@ -155,22 +155,22 @@ msgstr "Ausstehend" msgid "Placed" msgstr "Platziert" -#: InvenTree/status_codes.py:96 InvenTree/status_codes.py:226 +#: InvenTree/status_codes.py:96 InvenTree/status_codes.py:231 msgid "Complete" msgstr "Fertig" #: InvenTree/status_codes.py:97 InvenTree/status_codes.py:137 -#: InvenTree/status_codes.py:225 +#: InvenTree/status_codes.py:230 msgid "Cancelled" msgstr "Storniert" #: InvenTree/status_codes.py:98 InvenTree/status_codes.py:138 -#: InvenTree/status_codes.py:175 +#: InvenTree/status_codes.py:180 msgid "Lost" msgstr "Verloren" #: InvenTree/status_codes.py:99 InvenTree/status_codes.py:139 -#: InvenTree/status_codes.py:177 +#: InvenTree/status_codes.py:182 msgid "Returned" msgstr "Zurückgegeben" @@ -179,27 +179,27 @@ msgstr "Zurückgegeben" msgid "Shipped" msgstr "Versendet" -#: InvenTree/status_codes.py:171 +#: InvenTree/status_codes.py:176 msgid "OK" msgstr "OK" -#: InvenTree/status_codes.py:172 +#: InvenTree/status_codes.py:177 msgid "Attention needed" msgstr "erfordert Eingriff" -#: InvenTree/status_codes.py:173 +#: InvenTree/status_codes.py:178 msgid "Damaged" msgstr "Beschädigt" -#: InvenTree/status_codes.py:174 +#: InvenTree/status_codes.py:179 msgid "Destroyed" msgstr "Zerstört" -#: InvenTree/status_codes.py:176 +#: InvenTree/status_codes.py:181 msgid "Rejected" msgstr "" -#: InvenTree/status_codes.py:224 +#: InvenTree/status_codes.py:229 #, fuzzy #| msgid "Location" msgid "Production" @@ -331,7 +331,7 @@ msgstr "Bestell-Referenz" msgid "Order target date" msgstr "Kein Ziel gesetzt" -#: build/forms.py:39 build/models.py:175 +#: build/forms.py:39 build/models.py:206 msgid "" "Target date for build completion. Build will be overdue after this date." msgstr "" @@ -349,7 +349,7 @@ msgstr "" #: part/templates/part/sale_prices.html:82 stock/forms.py:304 #: stock/templates/stock/item_base.html:40 #: stock/templates/stock/item_base.html:46 -#: stock/templates/stock/item_base.html:204 +#: stock/templates/stock/item_base.html:214 #: stock/templates/stock/stock_adjust.html:18 templates/js/barcode.js:338 #: templates/js/bom.js:195 templates/js/build.js:420 templates/js/stock.js:750 #: templates/js/stock.js:989 @@ -434,25 +434,25 @@ msgstr "Lagerobjekt für Zuordnung auswählen" msgid "Build Order" msgstr "Bauauftrag" -#: build/models.py:62 build/templates/build/index.html:6 -#: build/templates/build/index.html:14 order/templates/order/so_builds.html:11 +#: build/models.py:62 build/templates/build/index.html:8 +#: build/templates/build/index.html:15 order/templates/order/so_builds.html:11 #: order/templates/order/so_tabs.html:9 part/templates/part/tabs.html:31 #: templates/InvenTree/settings/tabs.html:28 users/models.py:30 msgid "Build Orders" msgstr "Bauaufträge" -#: build/models.py:77 +#: build/models.py:108 #, fuzzy #| msgid "Order Reference" msgid "Build Order Reference" msgstr "Bestellreferenz" -#: build/models.py:78 order/templates/order/purchase_order_detail.html:174 +#: build/models.py:109 order/templates/order/purchase_order_detail.html:174 #: templates/js/bom.js:187 templates/js/build.js:509 msgid "Reference" msgstr "Referenz" -#: build/models.py:85 build/templates/build/detail.html:19 +#: build/models.py:116 build/templates/build/detail.html:19 #: company/models.py:359 company/templates/company/detail.html:23 #: company/templates/company/supplier_part_base.html:61 #: company/templates/company/supplier_part_detail.html:27 @@ -467,24 +467,24 @@ msgstr "Referenz" msgid "Description" msgstr "Beschreibung" -#: build/models.py:88 +#: build/models.py:119 msgid "Brief description of the build" msgstr "Kurze Beschreibung des Baus" -#: build/models.py:97 build/templates/build/build_base.html:104 +#: build/models.py:128 build/templates/build/build_base.html:113 #: build/templates/build/detail.html:75 msgid "Parent Build" msgstr "Eltern-Bau" -#: build/models.py:98 +#: build/models.py:129 #, fuzzy #| msgid "SalesOrder to which this build is allocated" msgid "BuildOrder to which this build is allocated" msgstr "Bestellung, die diesem Bau zugwiesen ist" -#: build/models.py:103 build/templates/build/auto_allocate.html:16 +#: build/models.py:134 build/templates/build/auto_allocate.html:16 #: build/templates/build/build_base.html:78 -#: build/templates/build/detail.html:24 order/models.py:548 +#: build/templates/build/detail.html:24 order/models.py:623 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:148 #: order/templates/order/receive_parts.html:19 part/models.py:316 @@ -497,23 +497,23 @@ msgstr "Bestellung, die diesem Bau zugwiesen ist" msgid "Part" msgstr "Teil" -#: build/models.py:111 +#: build/models.py:142 msgid "Select part to build" msgstr "Teil für den Bau wählen" -#: build/models.py:116 +#: build/models.py:147 msgid "Sales Order Reference" msgstr "Bestellungsreferenz" -#: build/models.py:120 +#: build/models.py:151 msgid "SalesOrder to which this build is allocated" msgstr "Bestellung, die diesem Bau zugwiesen ist" -#: build/models.py:125 +#: build/models.py:156 msgid "Source Location" msgstr "Quell-Standort" -#: build/models.py:129 +#: build/models.py:160 msgid "" "Select location to take stock from for this build (leave blank to take from " "any stock location)" @@ -521,155 +521,155 @@ msgstr "" "Lager-Entnahmestandort für diesen Bau wählen (oder leer lassen für einen " "beliebigen Lager-Standort)" -#: build/models.py:134 +#: build/models.py:165 #, fuzzy #| msgid "Destination stock location" msgid "Destination Location" msgstr "Ziel-Lagerbestand" -#: build/models.py:138 +#: build/models.py:169 msgid "Select location where the completed items will be stored" msgstr "" -#: build/models.py:142 +#: build/models.py:173 msgid "Build Quantity" msgstr "Bau-Anzahl" -#: build/models.py:145 +#: build/models.py:176 #, fuzzy #| msgid "Number of parts to build" msgid "Number of stock items to build" msgstr "Anzahl der zu bauenden Teile" -#: build/models.py:149 +#: build/models.py:180 #, fuzzy #| msgid "Completed" msgid "Completed items" msgstr "Fertig" -#: build/models.py:151 +#: build/models.py:182 #, fuzzy #| msgid "Delete this Stock Item when stock is depleted" msgid "Number of stock items which have been completed" msgstr "Objekt löschen wenn Lagerbestand aufgebraucht" -#: build/models.py:155 part/templates/part/part_base.html:155 +#: build/models.py:186 part/templates/part/part_base.html:155 msgid "Build Status" msgstr "Bau-Status" -#: build/models.py:159 +#: build/models.py:190 msgid "Build status code" msgstr "Bau-Statuscode" -#: build/models.py:163 stock/models.py:396 +#: build/models.py:194 stock/models.py:397 msgid "Batch Code" msgstr "Losnummer" -#: build/models.py:167 +#: build/models.py:198 msgid "Batch code for this build output" msgstr "Chargennummer für diese Bau-Ausgabe" -#: build/models.py:174 order/models.py:329 +#: build/models.py:205 order/models.py:404 msgid "Target completion date" msgstr "" -#: build/models.py:188 build/templates/build/detail.html:89 +#: build/models.py:219 build/templates/build/detail.html:89 #: company/templates/company/supplier_part_base.html:68 #: company/templates/company/supplier_part_detail.html:24 #: part/templates/part/detail.html:80 part/templates/part/part_base.html:102 -#: stock/models.py:390 stock/templates/stock/item_base.html:287 +#: stock/models.py:391 stock/templates/stock/item_base.html:297 msgid "External Link" msgstr "Externer Link" -#: build/models.py:189 part/models.py:705 stock/models.py:392 +#: build/models.py:220 part/models.py:705 stock/models.py:393 msgid "Link to external URL" msgstr "Link zu einer externen URL" -#: build/models.py:193 build/templates/build/tabs.html:23 company/models.py:366 +#: build/models.py:224 build/templates/build/tabs.html:23 company/models.py:366 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:18 #: order/templates/order/purchase_order_detail.html:213 #: order/templates/order/so_tabs.html:23 part/models.py:831 #: part/templates/part/tabs.html:73 stock/forms.py:313 stock/forms.py:345 -#: stock/forms.py:373 stock/models.py:462 stock/models.py:1476 +#: stock/forms.py:373 stock/models.py:463 stock/models.py:1512 #: stock/templates/stock/tabs.html:26 templates/js/barcode.js:391 #: templates/js/bom.js:263 templates/js/stock.js:117 templates/js/stock.js:603 msgid "Notes" msgstr "Notizen" -#: build/models.py:194 +#: build/models.py:225 msgid "Extra build notes" msgstr "Notizen für den Bau" -#: build/models.py:579 +#: build/models.py:610 #, fuzzy #| msgid "No action specified" msgid "No build output specified" msgstr "Keine Aktion angegeben" -#: build/models.py:582 +#: build/models.py:613 msgid "Build output is already completed" msgstr "" -#: build/models.py:585 +#: build/models.py:616 #, fuzzy #| msgid "Quantity does not match serial numbers" msgid "Build output does not match Build Order" msgstr "Anzahl stimmt nicht mit den Seriennummern überein" -#: build/models.py:660 +#: build/models.py:691 #, fuzzy #| msgid "Complete Build" msgid "Completed build output" msgstr "Bau fertigstellen" -#: build/models.py:902 +#: build/models.py:933 msgid "BuildItem must be unique for build, stock_item and install_into" msgstr "" -#: build/models.py:924 +#: build/models.py:955 #, fuzzy #| msgid "Allocate Stock to Build" msgid "Build item must specify a build output" msgstr "Lagerbestand dem Bau zuweisen" -#: build/models.py:929 +#: build/models.py:960 #, python-brace-format msgid "Selected stock item not found in BOM for part '{p}'" msgstr "Ausgewähltes Lagerobjekt nicht in BOM für Teil '{p}' gefunden" -#: build/models.py:933 +#: build/models.py:964 #, python-brace-format msgid "Allocated quantity ({n}) must not exceed available quantity ({q})" msgstr "" "zugewiesene Anzahl ({n}) darf nicht die verfügbare ({q}) Anzahl überschreiten" -#: build/models.py:940 order/models.py:632 +#: build/models.py:971 order/models.py:707 msgid "StockItem is over-allocated" msgstr "Zu viele Lagerobjekte zugewiesen" -#: build/models.py:944 order/models.py:635 +#: build/models.py:975 order/models.py:710 msgid "Allocation quantity must be greater than zero" msgstr "Anzahl muss größer null sein" -#: build/models.py:948 +#: build/models.py:979 msgid "Quantity must be 1 for serialized stock" msgstr "Anzahl muss 1 für Objekte mit Seriennummer sein" -#: build/models.py:988 +#: build/models.py:1019 msgid "Build to allocate parts" msgstr "Bau starten um Teile zuzuweisen" -#: build/models.py:995 +#: build/models.py:1026 #, fuzzy #| msgid "Remove stock" msgid "Source stock item" msgstr "Bestand entfernen" -#: build/models.py:1007 +#: build/models.py:1038 msgid "Stock quantity to allocate to build" msgstr "Lagerobjekt-Anzahl dem Bau zuweisen" -#: build/models.py:1015 +#: build/models.py:1046 #, fuzzy #| msgid "Destination stock location" msgid "Destination stock item" @@ -755,7 +755,7 @@ msgid "" msgstr "Lagerobjekt dem Bau zuweisen" #: build/templates/build/auto_allocate.html:18 stock/forms.py:343 -#: stock/templates/stock/item_base.html:234 +#: stock/templates/stock/item_base.html:244 #: stock/templates/stock/stock_adjust.html:17 #: templates/InvenTree/search.html:183 templates/js/barcode.js:337 #: templates/js/build.js:434 templates/js/stock.js:587 @@ -799,7 +799,7 @@ msgid "Admin view" msgstr "Admin" #: build/templates/build/build_base.html:43 -#: build/templates/build/build_base.html:92 +#: build/templates/build/build_base.html:100 #: order/templates/order/sales_order_base.html:41 #: order/templates/order/sales_order_base.html:83 #: templates/js/table_filters.js:200 templates/js/table_filters.js:232 @@ -831,30 +831,39 @@ msgstr "Bau-Status" #: build/templates/build/build_base.html:88 #: build/templates/build/detail.html:57 #: order/templates/order/receive_parts.html:24 -#: stock/templates/stock/item_base.html:333 templates/InvenTree/search.html:175 +#: stock/templates/stock/item_base.html:343 templates/InvenTree/search.html:175 #: templates/js/barcode.js:42 templates/js/build.js:697 #: templates/js/order.js:180 templates/js/order.js:268 #: templates/js/stock.js:574 templates/js/stock.js:997 msgid "Status" msgstr "Status" -#: build/templates/build/build_base.html:92 +#: build/templates/build/build_base.html:96 +#: build/templates/build/detail.html:100 +#: order/templates/order/sales_order_base.html:114 templates/js/build.js:710 +#: templates/js/order.js:281 +#, fuzzy +#| msgid "Shipment Date" +msgid "Target Date" +msgstr "Versanddatum" + +#: build/templates/build/build_base.html:100 msgid "This build was due on" msgstr "" -#: build/templates/build/build_base.html:98 +#: build/templates/build/build_base.html:107 #: build/templates/build/detail.html:62 msgid "Progress" msgstr "" -#: build/templates/build/build_base.html:111 -#: build/templates/build/detail.html:82 order/models.py:546 +#: build/templates/build/build_base.html:120 +#: build/templates/build/detail.html:82 order/models.py:621 #: order/templates/order/sales_order_base.html:9 #: order/templates/order/sales_order_base.html:33 #: order/templates/order/sales_order_notes.html:10 #: order/templates/order/sales_order_ship.html:25 #: part/templates/part/allocation.html:27 -#: stock/templates/stock/item_base.html:228 templates/js/order.js:229 +#: stock/templates/stock/item_base.html:238 templates/js/order.js:229 msgid "Sales Order" msgstr "Bestellung" @@ -988,7 +997,7 @@ msgid "Destination location not specified" msgstr "Hat dieses Teil Tracking für einzelne Objekte?" #: build/templates/build/detail.html:68 -#: stock/templates/stock/item_base.html:252 templates/js/stock.js:582 +#: stock/templates/stock/item_base.html:262 templates/js/stock.js:582 #: templates/js/stock.js:1004 templates/js/table_filters.js:80 #: templates/js/table_filters.js:161 msgid "Batch" @@ -1000,14 +1009,6 @@ msgstr "Los" msgid "Created" msgstr "Erstellt" -#: build/templates/build/detail.html:100 -#: order/templates/order/sales_order_base.html:114 templates/js/build.js:710 -#: templates/js/order.js:281 -#, fuzzy -#| msgid "Shipment Date" -msgid "Target Date" -msgstr "Versanddatum" - #: build/templates/build/detail.html:106 #, fuzzy #| msgid "No destination set" @@ -1031,10 +1032,22 @@ msgstr "Bau-Zuweisung ist vollständig" msgid "Alter the quantity of stock allocated to the build output" msgstr "Lagerobjekt-Anzahl dem Bau zuweisen" -#: build/templates/build/index.html:25 build/views.py:658 +#: build/templates/build/index.html:27 build/views.py:658 msgid "New Build Order" msgstr "Neuer Bauauftrag" +#: build/templates/build/index.html:30 +#: order/templates/order/purchase_orders.html:22 +#: order/templates/order/sales_orders.html:22 +msgid "Display calendar view" +msgstr "" + +#: build/templates/build/index.html:33 +#: order/templates/order/purchase_orders.html:25 +#: order/templates/order/sales_orders.html:25 +msgid "Display list view" +msgstr "" + #: build/templates/build/notes.html:13 build/templates/build/notes.html:30 msgid "Build Notes" msgstr "Bau-Bemerkungen" @@ -1101,7 +1114,7 @@ msgstr "Lagerbestand dem Bau zuweisen" msgid "Create Build Output" msgstr "Bau-Ausgabe" -#: build/views.py:207 stock/models.py:871 stock/views.py:1681 +#: build/views.py:207 stock/models.py:872 stock/views.py:1681 #, fuzzy #| msgid "Serial numbers already exist: " msgid "Serial numbers already exist" @@ -1683,8 +1696,8 @@ msgstr "Produziert diese Firma Teile?" msgid "Currency" msgstr "Währung bearbeiten" -#: company/models.py:313 stock/models.py:344 -#: stock/templates/stock/item_base.html:184 +#: company/models.py:313 stock/models.py:345 +#: stock/templates/stock/item_base.html:194 msgid "Base Part" msgstr "Basisteil" @@ -1697,7 +1710,7 @@ msgstr "Teil auswählen" #: company/templates/company/supplier_part_detail.html:21 #: order/templates/order/order_base.html:79 #: order/templates/order/order_wizard/select_pos.html:30 part/bom.py:170 -#: stock/templates/stock/item_base.html:294 templates/js/company.js:48 +#: stock/templates/stock/item_base.html:304 templates/js/company.js:48 #: templates/js/company.js:164 templates/js/order.js:162 msgid "Supplier" msgstr "Zulieferer" @@ -1801,8 +1814,8 @@ msgid "Uses default currency" msgstr "Währung entfernen" #: company/templates/company/detail.html:62 -#: order/templates/order/sales_order_base.html:89 stock/models.py:379 -#: stock/models.py:380 stock/templates/stock/item_base.html:211 +#: order/templates/order/sales_order_base.html:89 stock/models.py:380 +#: stock/models.py:381 stock/templates/stock/item_base.html:221 #: templates/js/company.js:40 templates/js/order.js:250 msgid "Customer" msgstr "Kunde" @@ -1900,8 +1913,8 @@ msgstr "" #: company/templates/company/purchase_orders.html:9 #: company/templates/company/tabs.html:17 -#: order/templates/order/purchase_orders.html:7 -#: order/templates/order/purchase_orders.html:12 +#: order/templates/order/purchase_orders.html:8 +#: order/templates/order/purchase_orders.html:13 #: part/templates/part/orders.html:9 part/templates/part/tabs.html:48 #: templates/InvenTree/settings/tabs.html:31 templates/navbar.html:33 #: users/models.py:31 @@ -1909,19 +1922,19 @@ msgid "Purchase Orders" msgstr "Bestellungen" #: company/templates/company/purchase_orders.html:15 -#: order/templates/order/purchase_orders.html:18 +#: order/templates/order/purchase_orders.html:19 msgid "Create new purchase order" msgstr "Neue Bestellung anlegen" #: company/templates/company/purchase_orders.html:16 -#: order/templates/order/purchase_orders.html:19 +#: order/templates/order/purchase_orders.html:20 msgid "New Purchase Order" msgstr "Neue Bestellung" #: company/templates/company/sales_orders.html:9 #: company/templates/company/tabs.html:22 -#: order/templates/order/sales_orders.html:7 -#: order/templates/order/sales_orders.html:12 +#: order/templates/order/sales_orders.html:8 +#: order/templates/order/sales_orders.html:13 #: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:56 #: templates/InvenTree/settings/tabs.html:34 templates/navbar.html:42 #: users/models.py:32 @@ -1929,18 +1942,18 @@ msgid "Sales Orders" msgstr "Bestellungen" #: company/templates/company/sales_orders.html:15 -#: order/templates/order/sales_orders.html:18 +#: order/templates/order/sales_orders.html:19 msgid "Create new sales order" msgstr "Neuen Auftrag anlegen" #: company/templates/company/sales_orders.html:16 -#: order/templates/order/sales_orders.html:19 +#: order/templates/order/sales_orders.html:20 msgid "New Sales Order" msgstr "Neuer Auftrag" #: company/templates/company/supplier_part_base.html:6 -#: company/templates/company/supplier_part_base.html:19 stock/models.py:353 -#: stock/templates/stock/item_base.html:299 templates/js/company.js:180 +#: company/templates/company/supplier_part_base.html:19 stock/models.py:354 +#: stock/templates/stock/item_base.html:309 templates/js/company.js:180 msgid "Supplier Part" msgstr "Zulieferer-Teil" @@ -2184,7 +2197,7 @@ msgstr "Bestell-Referenz" msgid "Enter sales order number" msgstr "Auftrag stornieren" -#: order/forms.py:134 order/models.py:330 +#: order/forms.py:134 order/models.py:405 msgid "" "Target date for order completion. Order will be overdue after this date." msgstr "" @@ -2205,115 +2218,115 @@ msgstr "Link auf externe Seite" msgid "Order notes" msgstr "Bestell-Notizen" -#: order/models.py:131 order/models.py:323 +#: order/models.py:169 order/models.py:398 #, fuzzy #| msgid "Purchase Order Details" msgid "Purchase order status" msgstr "Bestelldetails" -#: order/models.py:139 +#: order/models.py:177 msgid "Company from which the items are being ordered" msgstr "" -#: order/models.py:142 +#: order/models.py:180 msgid "Supplier order reference code" msgstr "Bestellreferenz" -#: order/models.py:151 +#: order/models.py:189 msgid "Date order was issued" msgstr "" -#: order/models.py:153 +#: order/models.py:191 #, fuzzy #| msgid "Mark order as complete" msgid "Date order was completed" msgstr "Bestellung als vollständig markieren" -#: order/models.py:176 order/models.py:258 part/views.py:1504 -#: stock/models.py:250 stock/models.py:855 +#: order/models.py:214 order/models.py:296 part/views.py:1504 +#: stock/models.py:251 stock/models.py:856 msgid "Quantity must be greater than zero" msgstr "Anzahl muss größer Null sein" -#: order/models.py:181 +#: order/models.py:219 msgid "Part supplier must match PO supplier" msgstr "Teile-Zulieferer muss dem Zulieferer des Kaufvertrags entsprechen" -#: order/models.py:253 +#: order/models.py:291 msgid "Lines can only be received against an order marked as 'Placed'" msgstr "Nur Teile aufgegebener Bestllungen können empfangen werden" -#: order/models.py:319 +#: order/models.py:394 msgid "Company to which the items are being sold" msgstr "" -#: order/models.py:325 +#: order/models.py:400 msgid "Customer order reference code" msgstr "Bestellreferenz" -#: order/models.py:387 +#: order/models.py:462 msgid "SalesOrder cannot be shipped as it is not currently pending" msgstr "Bestellung kann nicht versendet werden weil sie nicht anhängig ist" -#: order/models.py:474 +#: order/models.py:549 msgid "Item quantity" msgstr "Anzahl" -#: order/models.py:476 +#: order/models.py:551 msgid "Line item reference" msgstr "Position - Referenz" -#: order/models.py:478 +#: order/models.py:553 msgid "Line item notes" msgstr "Position - Notizen" -#: order/models.py:504 order/templates/order/order_base.html:9 +#: order/models.py:579 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:24 -#: stock/templates/stock/item_base.html:266 templates/js/order.js:146 +#: stock/templates/stock/item_base.html:276 templates/js/order.js:146 msgid "Purchase Order" msgstr "Kaufvertrag" -#: order/models.py:517 +#: order/models.py:592 msgid "Supplier part" msgstr "Zulieferer-Teil" -#: order/models.py:520 +#: order/models.py:595 msgid "Number of items received" msgstr "Empfangene Objekt-Anzahl" -#: order/models.py:527 stock/models.py:472 -#: stock/templates/stock/item_base.html:273 +#: order/models.py:602 stock/models.py:473 +#: stock/templates/stock/item_base.html:283 #, fuzzy #| msgid "Purchase Order" msgid "Purchase Price" msgstr "Kaufvertrag" -#: order/models.py:528 +#: order/models.py:603 #, fuzzy #| msgid "Purchase Order" msgid "Unit purchase price" msgstr "Kaufvertrag" -#: order/models.py:623 +#: order/models.py:698 msgid "Cannot allocate stock item to a line with a different part" msgstr "Kann Lagerobjekt keiner Zeile mit einem anderen Teil hinzufügen" -#: order/models.py:625 +#: order/models.py:700 msgid "Cannot allocate stock to a line without a part" msgstr "Kann Lagerobjekt keiner Zeile ohne Teil hinzufügen" -#: order/models.py:628 +#: order/models.py:703 msgid "Allocation quantity cannot exceed stock quantity" msgstr "zugewiesene Anzahl darf nicht die verfügbare Anzahl überschreiten" -#: order/models.py:638 +#: order/models.py:713 msgid "Quantity must be 1 for serialized stock item" msgstr "Anzahl muss 1 für Objekte mit Seriennummer sein" -#: order/models.py:654 +#: order/models.py:729 msgid "Select stock item to allocate" msgstr "Lagerobjekt für Zuordnung auswählen" -#: order/models.py:657 +#: order/models.py:732 msgid "Enter stock allocation quantity" msgstr "Zuordnungsanzahl eingeben" @@ -2545,8 +2558,8 @@ msgid "Sales Order Items" msgstr "Auftragspositionen" #: order/templates/order/sales_order_detail.html:72 -#: order/templates/order/sales_order_detail.html:154 stock/models.py:384 -#: stock/templates/stock/item_base.html:198 templates/js/build.js:418 +#: order/templates/order/sales_order_detail.html:154 stock/models.py:385 +#: stock/templates/stock/item_base.html:208 templates/js/build.js:418 msgid "Serial Number" msgstr "Seriennummer" @@ -3265,7 +3278,7 @@ msgid "BOM line checksum" msgstr "Prüfsumme der Stückliste" #: part/models.py:1963 part/views.py:1510 part/views.py:1562 -#: stock/models.py:240 +#: stock/models.py:241 #, fuzzy #| msgid "Overage must be an integer value or a percentage" msgid "Quantity must be integer value for trackable parts" @@ -3310,7 +3323,7 @@ msgstr "Bestellung" #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 #: stock/templates/stock/item_base.html:72 -#: stock/templates/stock/item_base.html:281 +#: stock/templates/stock/item_base.html:291 #: stock/templates/stock/stock_adjust.html:16 templates/js/build.js:751 #: templates/js/stock.js:720 templates/js/stock.js:980 msgid "Stock Item" @@ -3745,7 +3758,7 @@ msgstr "Parameter hinzufügen" msgid "New Parameter" msgstr "Neuer Parameter" -#: part/templates/part/params.html:25 stock/models.py:1463 +#: part/templates/part/params.html:25 stock/models.py:1499 #: templates/InvenTree/settings/header.html:8 templates/js/stock.js:113 msgid "Value" msgstr "Wert" @@ -3799,7 +3812,7 @@ msgid "Show QR Code" msgstr "Teil-QR-Code" #: part/templates/part/part_base.html:52 -#: stock/templates/stock/item_base.html:111 +#: stock/templates/stock/item_base.html:126 #: stock/templates/stock/location.html:32 msgid "Print Label" msgstr "" @@ -3965,7 +3978,7 @@ msgstr "Stückliste" msgid "Used In" msgstr "Benutzt in" -#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:339 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:349 msgid "Tests" msgstr "" @@ -4368,272 +4381,272 @@ msgstr "Standard-Lagerort" msgid "Set the destination as the default location for selected parts" msgstr "Setze das Ziel als Standard-Ziel für ausgewählte Teile" -#: stock/models.py:185 +#: stock/models.py:186 #, fuzzy #| msgid "Created new stock item" msgid "Created stock item" msgstr "Neues Lagerobjekt erstellt" -#: stock/models.py:221 +#: stock/models.py:222 #, fuzzy #| msgid "A stock item with this serial number already exists" msgid "StockItem with this serial number already exists" msgstr "Ein Teil mit dieser Seriennummer existiert bereits" -#: stock/models.py:257 +#: stock/models.py:258 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "Teile-Typ ('{pf}') muss {pe} sein" -#: stock/models.py:267 stock/models.py:276 +#: stock/models.py:268 stock/models.py:277 msgid "Quantity must be 1 for item with a serial number" msgstr "Anzahl muss für Objekte mit Seriennummer \"1\" sein" -#: stock/models.py:268 +#: stock/models.py:269 msgid "Serial number cannot be set if quantity greater than 1" msgstr "" "Seriennummer kann nicht gesetzt werden wenn die Anzahl größer als \"1\" ist" -#: stock/models.py:290 +#: stock/models.py:291 msgid "Item cannot belong to itself" msgstr "Teil kann nicht zu sich selbst gehören" -#: stock/models.py:296 +#: stock/models.py:297 msgid "Item must have a build reference if is_building=True" msgstr "" -#: stock/models.py:303 +#: stock/models.py:304 msgid "Build reference does not point to the same part object" msgstr "" -#: stock/models.py:336 +#: stock/models.py:337 msgid "Parent Stock Item" msgstr "Eltern-Lagerobjekt" -#: stock/models.py:345 +#: stock/models.py:346 msgid "Base part" msgstr "Basis-Teil" -#: stock/models.py:354 +#: stock/models.py:355 msgid "Select a matching supplier part for this stock item" msgstr "Passenden Zulieferer für dieses Lagerobjekt auswählen" -#: stock/models.py:359 stock/templates/stock/stock_app_base.html:7 +#: stock/models.py:360 stock/templates/stock/stock_app_base.html:7 msgid "Stock Location" msgstr "Lagerort" -#: stock/models.py:362 +#: stock/models.py:363 msgid "Where is this stock item located?" msgstr "Wo wird dieses Teil normalerweise gelagert?" -#: stock/models.py:367 stock/templates/stock/item_base.html:219 +#: stock/models.py:368 stock/templates/stock/item_base.html:229 msgid "Installed In" msgstr "Installiert in" -#: stock/models.py:370 +#: stock/models.py:371 msgid "Is this item installed in another item?" msgstr "Ist dieses Teil in einem anderen verbaut?" -#: stock/models.py:386 +#: stock/models.py:387 msgid "Serial number for this item" msgstr "Seriennummer für dieses Teil" -#: stock/models.py:398 +#: stock/models.py:399 msgid "Batch code for this stock item" msgstr "Losnummer für dieses Lagerobjekt" -#: stock/models.py:402 +#: stock/models.py:403 msgid "Stock Quantity" msgstr "Bestand" -#: stock/models.py:411 +#: stock/models.py:412 msgid "Source Build" msgstr "Quellbau" -#: stock/models.py:413 +#: stock/models.py:414 msgid "Build for this stock item" msgstr "Bau für dieses Lagerobjekt" -#: stock/models.py:424 +#: stock/models.py:425 msgid "Source Purchase Order" msgstr "Quellbestellung" -#: stock/models.py:427 +#: stock/models.py:428 msgid "Purchase order for this stock item" msgstr "Bestellung für dieses Teil" -#: stock/models.py:433 +#: stock/models.py:434 msgid "Destination Sales Order" msgstr "Zielauftrag" -#: stock/models.py:439 stock/templates/stock/item_base.html:306 +#: stock/models.py:440 stock/templates/stock/item_base.html:316 #: templates/js/stock.js:597 #, fuzzy #| msgid "Export" msgid "Expiry Date" msgstr "Exportieren" -#: stock/models.py:440 +#: stock/models.py:441 msgid "" "Expiry date for stock item. Stock will be considered expired after this date" msgstr "" -#: stock/models.py:453 +#: stock/models.py:454 msgid "Delete this Stock Item when stock is depleted" msgstr "Objekt löschen wenn Lagerbestand aufgebraucht" -#: stock/models.py:463 stock/templates/stock/item_notes.html:14 +#: stock/models.py:464 stock/templates/stock/item_notes.html:14 #: stock/templates/stock/item_notes.html:30 msgid "Stock Item Notes" msgstr "Lagerobjekt-Notizen" -#: stock/models.py:473 +#: stock/models.py:474 msgid "Single unit purchase price at time of purchase" msgstr "" -#: stock/models.py:573 +#: stock/models.py:574 #, fuzzy #| msgid "Item assigned to customer?" msgid "Assigned to Customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/models.py:575 +#: stock/models.py:576 #, fuzzy #| msgid "Item assigned to customer?" msgid "Manually assigned to customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/models.py:588 +#: stock/models.py:589 #, fuzzy #| msgid "Item assigned to customer?" msgid "Returned from customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/models.py:590 +#: stock/models.py:591 #, fuzzy #| msgid "Create new stock location" msgid "Returned to location" msgstr "Neuen Lagerort anlegen" -#: stock/models.py:715 +#: stock/models.py:716 #, fuzzy #| msgid "Installed in Stock Item" msgid "Installed into stock item" msgstr "In Lagerobjekt installiert" -#: stock/models.py:723 +#: stock/models.py:724 #, fuzzy #| msgid "Installed in Stock Item" msgid "Installed stock item" msgstr "In Lagerobjekt installiert" -#: stock/models.py:747 +#: stock/models.py:748 #, fuzzy #| msgid "Installed in Stock Item" msgid "Uninstalled stock item" msgstr "In Lagerobjekt installiert" -#: stock/models.py:766 +#: stock/models.py:767 #, fuzzy #| msgid "Include sublocations" msgid "Uninstalled into location" msgstr "Unterlagerorte einschließen" -#: stock/models.py:846 +#: stock/models.py:847 #, fuzzy #| msgid "Part is not a virtual part" msgid "Part is not set as trackable" msgstr "Teil ist nicht virtuell" -#: stock/models.py:852 +#: stock/models.py:853 msgid "Quantity must be integer" msgstr "Anzahl muss eine Ganzzahl sein" -#: stock/models.py:858 +#: stock/models.py:859 #, python-brace-format msgid "Quantity must not exceed available stock quantity ({n})" msgstr "Anzahl darf nicht die verfügbare Anzahl überschreiten ({n})" -#: stock/models.py:861 +#: stock/models.py:862 msgid "Serial numbers must be a list of integers" msgstr "Seriennummern muss eine Liste von Ganzzahlen sein" -#: stock/models.py:864 +#: stock/models.py:865 msgid "Quantity does not match serial numbers" msgstr "Anzahl stimmt nicht mit den Seriennummern überein" -#: stock/models.py:896 +#: stock/models.py:897 msgid "Add serial number" msgstr "Seriennummer hinzufügen" -#: stock/models.py:899 +#: stock/models.py:900 #, python-brace-format msgid "Serialized {n} items" msgstr "{n} Teile serialisiert" -#: stock/models.py:1010 +#: stock/models.py:1011 msgid "StockItem cannot be moved as it is not in stock" msgstr "Lagerobjekt kann nicht bewegt werden, da kein Bestand vorhanden ist" -#: stock/models.py:1364 +#: stock/models.py:1400 msgid "Tracking entry title" msgstr "Name des Eintrags-Trackings" -#: stock/models.py:1366 +#: stock/models.py:1402 msgid "Entry notes" msgstr "Eintrags-Notizen" -#: stock/models.py:1368 +#: stock/models.py:1404 msgid "Link to external page for further information" msgstr "Link auf externe Seite für weitere Informationen" -#: stock/models.py:1428 +#: stock/models.py:1464 #, fuzzy #| msgid "Serial number for this item" msgid "Value must be provided for this test" msgstr "Seriennummer für dieses Teil" -#: stock/models.py:1434 +#: stock/models.py:1470 msgid "Attachment must be uploaded for this test" msgstr "" -#: stock/models.py:1451 +#: stock/models.py:1487 msgid "Test" msgstr "" -#: stock/models.py:1452 +#: stock/models.py:1488 #, fuzzy #| msgid "Part name" msgid "Test name" msgstr "Name des Teils" -#: stock/models.py:1457 +#: stock/models.py:1493 #, fuzzy #| msgid "Search Results" msgid "Result" msgstr "Suchergebnisse" -#: stock/models.py:1458 templates/js/table_filters.js:172 +#: stock/models.py:1494 templates/js/table_filters.js:172 msgid "Test result" msgstr "" -#: stock/models.py:1464 +#: stock/models.py:1500 msgid "Test output value" msgstr "" -#: stock/models.py:1470 +#: stock/models.py:1506 #, fuzzy #| msgid "Attachments" msgid "Attachment" msgstr "Anhänge" -#: stock/models.py:1471 +#: stock/models.py:1507 #, fuzzy #| msgid "Delete attachment" msgid "Test result attachment" msgstr "Anhang löschen" -#: stock/models.py:1477 +#: stock/models.py:1513 #, fuzzy #| msgid "Edit notes" msgid "Test notes" @@ -4699,158 +4712,165 @@ msgstr "" "aufgebraucht ist." #: stock/templates/stock/item_base.html:74 -#: stock/templates/stock/item_base.html:310 templates/js/table_filters.js:111 +#: stock/templates/stock/item_base.html:320 templates/js/table_filters.js:111 msgid "Expired" msgstr "" #: stock/templates/stock/item_base.html:78 -#: stock/templates/stock/item_base.html:312 templates/js/table_filters.js:116 +#: stock/templates/stock/item_base.html:322 templates/js/table_filters.js:116 msgid "Stale" msgstr "" -#: stock/templates/stock/item_base.html:114 templates/js/barcode.js:283 +#: stock/templates/stock/item_base.html:113 templates/js/barcode.js:283 #: templates/js/barcode.js:288 msgid "Unlink Barcode" msgstr "" -#: stock/templates/stock/item_base.html:116 +#: stock/templates/stock/item_base.html:115 msgid "Link Barcode" msgstr "" -#: stock/templates/stock/item_base.html:124 +#: stock/templates/stock/item_base.html:123 +#, fuzzy +#| msgid "Confirm stock adjustment" +msgid "Document actions" +msgstr "Bestands-Anpassung bestätigen" + +#: stock/templates/stock/item_base.html:129 +#: stock/templates/stock/item_tests.html:25 +msgid "Test Report" +msgstr "" + +#: stock/templates/stock/item_base.html:137 #, fuzzy #| msgid "Confirm stock adjustment" msgid "Stock adjustment actions" msgstr "Bestands-Anpassung bestätigen" -#: stock/templates/stock/item_base.html:128 +#: stock/templates/stock/item_base.html:141 #: stock/templates/stock/location.html:41 templates/stock_table.html:23 msgid "Count stock" msgstr "Bestand zählen" -#: stock/templates/stock/item_base.html:129 templates/stock_table.html:21 +#: stock/templates/stock/item_base.html:142 templates/stock_table.html:21 msgid "Add stock" msgstr "Bestand hinzufügen" -#: stock/templates/stock/item_base.html:130 templates/stock_table.html:22 +#: stock/templates/stock/item_base.html:143 templates/stock_table.html:22 msgid "Remove stock" msgstr "Bestand entfernen" -#: stock/templates/stock/item_base.html:132 +#: stock/templates/stock/item_base.html:145 #, fuzzy #| msgid "Order stock" msgid "Transfer stock" msgstr "Bestand bestellen" -#: stock/templates/stock/item_base.html:134 +#: stock/templates/stock/item_base.html:147 #, fuzzy #| msgid "Serialize Stock" msgid "Serialize stock" msgstr "Lagerbestand erfassen" -#: stock/templates/stock/item_base.html:138 +#: stock/templates/stock/item_base.html:151 #, fuzzy #| msgid "Item assigned to customer?" msgid "Assign to customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/templates/stock/item_base.html:141 +#: stock/templates/stock/item_base.html:154 #, fuzzy #| msgid "Count stock" msgid "Return to stock" msgstr "Bestand zählen" -#: stock/templates/stock/item_base.html:145 templates/js/stock.js:1017 +#: stock/templates/stock/item_base.html:158 templates/js/stock.js:1017 #, fuzzy #| msgid "Installed in Stock Item" msgid "Uninstall stock item" msgstr "In Lagerobjekt installiert" -#: stock/templates/stock/item_base.html:145 +#: stock/templates/stock/item_base.html:158 msgid "Uninstall" msgstr "" -#: stock/templates/stock/item_base.html:154 +#: stock/templates/stock/item_base.html:167 #: stock/templates/stock/location.html:38 #, fuzzy #| msgid "Stock Locations" msgid "Stock actions" msgstr "Lagerobjekt-Standorte" -#: stock/templates/stock/item_base.html:157 +#: stock/templates/stock/item_base.html:170 #, fuzzy #| msgid "Count stock items" msgid "Convert to variant" msgstr "Lagerobjekte zählen" -#: stock/templates/stock/item_base.html:160 +#: stock/templates/stock/item_base.html:173 #, fuzzy #| msgid "Count stock items" msgid "Duplicate stock item" msgstr "Lagerobjekte zählen" -#: stock/templates/stock/item_base.html:162 +#: stock/templates/stock/item_base.html:175 #, fuzzy #| msgid "Edit Stock Item" msgid "Edit stock item" msgstr "Lagerobjekt bearbeiten" -#: stock/templates/stock/item_base.html:165 +#: stock/templates/stock/item_base.html:178 #, fuzzy #| msgid "Delete Stock Item" msgid "Delete stock item" msgstr "Lagerobjekt löschen" -#: stock/templates/stock/item_base.html:171 -msgid "Generate test report" -msgstr "" - -#: stock/templates/stock/item_base.html:179 +#: stock/templates/stock/item_base.html:189 msgid "Stock Item Details" msgstr "Lagerbestands-Details" -#: stock/templates/stock/item_base.html:238 templates/js/build.js:442 +#: stock/templates/stock/item_base.html:248 templates/js/build.js:442 #, fuzzy #| msgid "No stock location set" msgid "No location set" msgstr "Kein Lagerort gesetzt" -#: stock/templates/stock/item_base.html:245 +#: stock/templates/stock/item_base.html:255 #, fuzzy #| msgid "Unique Identifier" msgid "Barcode Identifier" msgstr "Eindeutiger Bezeichner" -#: stock/templates/stock/item_base.html:259 templates/js/build.js:642 +#: stock/templates/stock/item_base.html:269 templates/js/build.js:642 #: templates/navbar.html:25 msgid "Build" msgstr "Bau" -#: stock/templates/stock/item_base.html:280 +#: stock/templates/stock/item_base.html:290 msgid "Parent Item" msgstr "Elternposition" -#: stock/templates/stock/item_base.html:310 +#: stock/templates/stock/item_base.html:320 #, fuzzy #| msgid "This stock item is allocated to Build" msgid "This StockItem expired on" msgstr "Dieses Lagerobjekt ist dem Bau zugewiesen" -#: stock/templates/stock/item_base.html:312 +#: stock/templates/stock/item_base.html:322 #, fuzzy #| msgid "Child Stock Items" msgid "This StockItem expires on" msgstr "Kind-Lagerobjekte" -#: stock/templates/stock/item_base.html:319 +#: stock/templates/stock/item_base.html:329 msgid "Last Updated" msgstr "Zuletzt aktualisiert" -#: stock/templates/stock/item_base.html:324 +#: stock/templates/stock/item_base.html:334 msgid "Last Stocktake" msgstr "Letzte Inventur" -#: stock/templates/stock/item_base.html:328 +#: stock/templates/stock/item_base.html:338 msgid "No stocktake performed" msgstr "Keine Inventur ausgeführt" @@ -4918,10 +4938,6 @@ msgstr "Vorlage löschen" msgid "Add Test Data" msgstr "" -#: stock/templates/stock/item_tests.html:25 -msgid "Test Report" -msgstr "" - #: stock/templates/stock/location.html:18 msgid "All stock items" msgstr "Alle Lagerobjekte" diff --git a/InvenTree/locale/en/LC_MESSAGES/django.po b/InvenTree/locale/en/LC_MESSAGES/django.po index 395f42c4e6..18f86b6972 100644 --- a/InvenTree/locale/en/LC_MESSAGES/django.po +++ b/InvenTree/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-01-06 23:11+1100\n" +"POT-Creation-Date: 2021-01-07 23:48+1100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -54,7 +54,7 @@ msgstr "" msgid "Select Category" msgstr "" -#: InvenTree/helpers.py:361 order/models.py:178 order/models.py:260 +#: InvenTree/helpers.py:361 order/models.py:216 order/models.py:298 #: stock/views.py:1660 msgid "Invalid quantity provided" msgstr "" @@ -133,7 +133,7 @@ msgid "InvenTree system health checks failed" msgstr "" #: InvenTree/status_codes.py:94 InvenTree/status_codes.py:135 -#: InvenTree/status_codes.py:223 +#: InvenTree/status_codes.py:228 msgid "Pending" msgstr "" @@ -141,22 +141,22 @@ msgstr "" msgid "Placed" msgstr "" -#: InvenTree/status_codes.py:96 InvenTree/status_codes.py:226 +#: InvenTree/status_codes.py:96 InvenTree/status_codes.py:231 msgid "Complete" msgstr "" #: InvenTree/status_codes.py:97 InvenTree/status_codes.py:137 -#: InvenTree/status_codes.py:225 +#: InvenTree/status_codes.py:230 msgid "Cancelled" msgstr "" #: InvenTree/status_codes.py:98 InvenTree/status_codes.py:138 -#: InvenTree/status_codes.py:175 +#: InvenTree/status_codes.py:180 msgid "Lost" msgstr "" #: InvenTree/status_codes.py:99 InvenTree/status_codes.py:139 -#: InvenTree/status_codes.py:177 +#: InvenTree/status_codes.py:182 msgid "Returned" msgstr "" @@ -165,27 +165,27 @@ msgstr "" msgid "Shipped" msgstr "" -#: InvenTree/status_codes.py:171 +#: InvenTree/status_codes.py:176 msgid "OK" msgstr "" -#: InvenTree/status_codes.py:172 +#: InvenTree/status_codes.py:177 msgid "Attention needed" msgstr "" -#: InvenTree/status_codes.py:173 +#: InvenTree/status_codes.py:178 msgid "Damaged" msgstr "" -#: InvenTree/status_codes.py:174 +#: InvenTree/status_codes.py:179 msgid "Destroyed" msgstr "" -#: InvenTree/status_codes.py:176 +#: InvenTree/status_codes.py:181 msgid "Rejected" msgstr "" -#: InvenTree/status_codes.py:224 +#: InvenTree/status_codes.py:229 msgid "Production" msgstr "" @@ -295,7 +295,7 @@ msgstr "" msgid "Order target date" msgstr "" -#: build/forms.py:39 build/models.py:175 +#: build/forms.py:39 build/models.py:206 msgid "" "Target date for build completion. Build will be overdue after this date." msgstr "" @@ -313,7 +313,7 @@ msgstr "" #: part/templates/part/sale_prices.html:82 stock/forms.py:304 #: stock/templates/stock/item_base.html:40 #: stock/templates/stock/item_base.html:46 -#: stock/templates/stock/item_base.html:204 +#: stock/templates/stock/item_base.html:214 #: stock/templates/stock/stock_adjust.html:18 templates/js/barcode.js:338 #: templates/js/bom.js:195 templates/js/build.js:420 templates/js/stock.js:750 #: templates/js/stock.js:989 @@ -378,23 +378,23 @@ msgstr "" msgid "Build Order" msgstr "" -#: build/models.py:62 build/templates/build/index.html:6 -#: build/templates/build/index.html:14 order/templates/order/so_builds.html:11 +#: build/models.py:62 build/templates/build/index.html:8 +#: build/templates/build/index.html:15 order/templates/order/so_builds.html:11 #: order/templates/order/so_tabs.html:9 part/templates/part/tabs.html:31 #: templates/InvenTree/settings/tabs.html:28 users/models.py:30 msgid "Build Orders" msgstr "" -#: build/models.py:77 +#: build/models.py:108 msgid "Build Order Reference" msgstr "" -#: build/models.py:78 order/templates/order/purchase_order_detail.html:174 +#: build/models.py:109 order/templates/order/purchase_order_detail.html:174 #: templates/js/bom.js:187 templates/js/build.js:509 msgid "Reference" msgstr "" -#: build/models.py:85 build/templates/build/detail.html:19 +#: build/models.py:116 build/templates/build/detail.html:19 #: company/models.py:359 company/templates/company/detail.html:23 #: company/templates/company/supplier_part_base.html:61 #: company/templates/company/supplier_part_detail.html:27 @@ -409,22 +409,22 @@ msgstr "" msgid "Description" msgstr "" -#: build/models.py:88 +#: build/models.py:119 msgid "Brief description of the build" msgstr "" -#: build/models.py:97 build/templates/build/build_base.html:104 +#: build/models.py:128 build/templates/build/build_base.html:113 #: build/templates/build/detail.html:75 msgid "Parent Build" msgstr "" -#: build/models.py:98 +#: build/models.py:129 msgid "BuildOrder to which this build is allocated" msgstr "" -#: build/models.py:103 build/templates/build/auto_allocate.html:16 +#: build/models.py:134 build/templates/build/auto_allocate.html:16 #: build/templates/build/build_base.html:78 -#: build/templates/build/detail.html:24 order/models.py:548 +#: build/templates/build/detail.html:24 order/models.py:623 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:148 #: order/templates/order/receive_parts.html:19 part/models.py:316 @@ -437,158 +437,158 @@ msgstr "" msgid "Part" msgstr "" -#: build/models.py:111 +#: build/models.py:142 msgid "Select part to build" msgstr "" -#: build/models.py:116 +#: build/models.py:147 msgid "Sales Order Reference" msgstr "" -#: build/models.py:120 +#: build/models.py:151 msgid "SalesOrder to which this build is allocated" msgstr "" -#: build/models.py:125 +#: build/models.py:156 msgid "Source Location" msgstr "" -#: build/models.py:129 +#: build/models.py:160 msgid "" "Select location to take stock from for this build (leave blank to take from " "any stock location)" msgstr "" -#: build/models.py:134 +#: build/models.py:165 msgid "Destination Location" msgstr "" -#: build/models.py:138 +#: build/models.py:169 msgid "Select location where the completed items will be stored" msgstr "" -#: build/models.py:142 +#: build/models.py:173 msgid "Build Quantity" msgstr "" -#: build/models.py:145 +#: build/models.py:176 msgid "Number of stock items to build" msgstr "" -#: build/models.py:149 +#: build/models.py:180 msgid "Completed items" msgstr "" -#: build/models.py:151 +#: build/models.py:182 msgid "Number of stock items which have been completed" msgstr "" -#: build/models.py:155 part/templates/part/part_base.html:155 +#: build/models.py:186 part/templates/part/part_base.html:155 msgid "Build Status" msgstr "" -#: build/models.py:159 +#: build/models.py:190 msgid "Build status code" msgstr "" -#: build/models.py:163 stock/models.py:396 +#: build/models.py:194 stock/models.py:397 msgid "Batch Code" msgstr "" -#: build/models.py:167 +#: build/models.py:198 msgid "Batch code for this build output" msgstr "" -#: build/models.py:174 order/models.py:329 +#: build/models.py:205 order/models.py:404 msgid "Target completion date" msgstr "" -#: build/models.py:188 build/templates/build/detail.html:89 +#: build/models.py:219 build/templates/build/detail.html:89 #: company/templates/company/supplier_part_base.html:68 #: company/templates/company/supplier_part_detail.html:24 #: part/templates/part/detail.html:80 part/templates/part/part_base.html:102 -#: stock/models.py:390 stock/templates/stock/item_base.html:287 +#: stock/models.py:391 stock/templates/stock/item_base.html:297 msgid "External Link" msgstr "" -#: build/models.py:189 part/models.py:705 stock/models.py:392 +#: build/models.py:220 part/models.py:705 stock/models.py:393 msgid "Link to external URL" msgstr "" -#: build/models.py:193 build/templates/build/tabs.html:23 company/models.py:366 +#: build/models.py:224 build/templates/build/tabs.html:23 company/models.py:366 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:18 #: order/templates/order/purchase_order_detail.html:213 #: order/templates/order/so_tabs.html:23 part/models.py:831 #: part/templates/part/tabs.html:73 stock/forms.py:313 stock/forms.py:345 -#: stock/forms.py:373 stock/models.py:462 stock/models.py:1476 +#: stock/forms.py:373 stock/models.py:463 stock/models.py:1512 #: stock/templates/stock/tabs.html:26 templates/js/barcode.js:391 #: templates/js/bom.js:263 templates/js/stock.js:117 templates/js/stock.js:603 msgid "Notes" msgstr "" -#: build/models.py:194 +#: build/models.py:225 msgid "Extra build notes" msgstr "" -#: build/models.py:579 +#: build/models.py:610 msgid "No build output specified" msgstr "" -#: build/models.py:582 +#: build/models.py:613 msgid "Build output is already completed" msgstr "" -#: build/models.py:585 +#: build/models.py:616 msgid "Build output does not match Build Order" msgstr "" -#: build/models.py:660 +#: build/models.py:691 msgid "Completed build output" msgstr "" -#: build/models.py:902 +#: build/models.py:933 msgid "BuildItem must be unique for build, stock_item and install_into" msgstr "" -#: build/models.py:924 +#: build/models.py:955 msgid "Build item must specify a build output" msgstr "" -#: build/models.py:929 +#: build/models.py:960 #, python-brace-format msgid "Selected stock item not found in BOM for part '{p}'" msgstr "" -#: build/models.py:933 +#: build/models.py:964 #, python-brace-format msgid "Allocated quantity ({n}) must not exceed available quantity ({q})" msgstr "" -#: build/models.py:940 order/models.py:632 +#: build/models.py:971 order/models.py:707 msgid "StockItem is over-allocated" msgstr "" -#: build/models.py:944 order/models.py:635 +#: build/models.py:975 order/models.py:710 msgid "Allocation quantity must be greater than zero" msgstr "" -#: build/models.py:948 +#: build/models.py:979 msgid "Quantity must be 1 for serialized stock" msgstr "" -#: build/models.py:988 +#: build/models.py:1019 msgid "Build to allocate parts" msgstr "" -#: build/models.py:995 +#: build/models.py:1026 msgid "Source stock item" msgstr "" -#: build/models.py:1007 +#: build/models.py:1038 msgid "Stock quantity to allocate to build" msgstr "" -#: build/models.py:1015 +#: build/models.py:1046 msgid "Destination stock item" msgstr "" @@ -654,7 +654,7 @@ msgid "" msgstr "" #: build/templates/build/auto_allocate.html:18 stock/forms.py:343 -#: stock/templates/stock/item_base.html:234 +#: stock/templates/stock/item_base.html:244 #: stock/templates/stock/stock_adjust.html:17 #: templates/InvenTree/search.html:183 templates/js/barcode.js:337 #: templates/js/build.js:434 templates/js/stock.js:587 @@ -688,7 +688,7 @@ msgid "Admin view" msgstr "" #: build/templates/build/build_base.html:43 -#: build/templates/build/build_base.html:92 +#: build/templates/build/build_base.html:100 #: order/templates/order/sales_order_base.html:41 #: order/templates/order/sales_order_base.html:83 #: templates/js/table_filters.js:200 templates/js/table_filters.js:232 @@ -718,30 +718,37 @@ msgstr "" #: build/templates/build/build_base.html:88 #: build/templates/build/detail.html:57 #: order/templates/order/receive_parts.html:24 -#: stock/templates/stock/item_base.html:333 templates/InvenTree/search.html:175 +#: stock/templates/stock/item_base.html:343 templates/InvenTree/search.html:175 #: templates/js/barcode.js:42 templates/js/build.js:697 #: templates/js/order.js:180 templates/js/order.js:268 #: templates/js/stock.js:574 templates/js/stock.js:997 msgid "Status" msgstr "" -#: build/templates/build/build_base.html:92 +#: build/templates/build/build_base.html:96 +#: build/templates/build/detail.html:100 +#: order/templates/order/sales_order_base.html:114 templates/js/build.js:710 +#: templates/js/order.js:281 +msgid "Target Date" +msgstr "" + +#: build/templates/build/build_base.html:100 msgid "This build was due on" msgstr "" -#: build/templates/build/build_base.html:98 +#: build/templates/build/build_base.html:107 #: build/templates/build/detail.html:62 msgid "Progress" msgstr "" -#: build/templates/build/build_base.html:111 -#: build/templates/build/detail.html:82 order/models.py:546 +#: build/templates/build/build_base.html:120 +#: build/templates/build/detail.html:82 order/models.py:621 #: order/templates/order/sales_order_base.html:9 #: order/templates/order/sales_order_base.html:33 #: order/templates/order/sales_order_notes.html:10 #: order/templates/order/sales_order_ship.html:25 #: part/templates/part/allocation.html:27 -#: stock/templates/stock/item_base.html:228 templates/js/order.js:229 +#: stock/templates/stock/item_base.html:238 templates/js/order.js:229 msgid "Sales Order" msgstr "" @@ -842,7 +849,7 @@ msgid "Destination location not specified" msgstr "" #: build/templates/build/detail.html:68 -#: stock/templates/stock/item_base.html:252 templates/js/stock.js:582 +#: stock/templates/stock/item_base.html:262 templates/js/stock.js:582 #: templates/js/stock.js:1004 templates/js/table_filters.js:80 #: templates/js/table_filters.js:161 msgid "Batch" @@ -854,12 +861,6 @@ msgstr "" msgid "Created" msgstr "" -#: build/templates/build/detail.html:100 -#: order/templates/order/sales_order_base.html:114 templates/js/build.js:710 -#: templates/js/order.js:281 -msgid "Target Date" -msgstr "" - #: build/templates/build/detail.html:106 msgid "No target date set" msgstr "" @@ -877,10 +878,22 @@ msgstr "" msgid "Alter the quantity of stock allocated to the build output" msgstr "" -#: build/templates/build/index.html:25 build/views.py:658 +#: build/templates/build/index.html:27 build/views.py:658 msgid "New Build Order" msgstr "" +#: build/templates/build/index.html:30 +#: order/templates/order/purchase_orders.html:22 +#: order/templates/order/sales_orders.html:22 +msgid "Display calendar view" +msgstr "" + +#: build/templates/build/index.html:33 +#: order/templates/order/purchase_orders.html:25 +#: order/templates/order/sales_orders.html:25 +msgid "Display list view" +msgstr "" + #: build/templates/build/notes.html:13 build/templates/build/notes.html:30 msgid "Build Notes" msgstr "" @@ -936,7 +949,7 @@ msgstr "" msgid "Create Build Output" msgstr "" -#: build/views.py:207 stock/models.py:871 stock/views.py:1681 +#: build/views.py:207 stock/models.py:872 stock/views.py:1681 msgid "Serial numbers already exist" msgstr "" @@ -1402,8 +1415,8 @@ msgstr "" msgid "Currency" msgstr "" -#: company/models.py:313 stock/models.py:344 -#: stock/templates/stock/item_base.html:184 +#: company/models.py:313 stock/models.py:345 +#: stock/templates/stock/item_base.html:194 msgid "Base Part" msgstr "" @@ -1416,7 +1429,7 @@ msgstr "" #: company/templates/company/supplier_part_detail.html:21 #: order/templates/order/order_base.html:79 #: order/templates/order/order_wizard/select_pos.html:30 part/bom.py:170 -#: stock/templates/stock/item_base.html:294 templates/js/company.js:48 +#: stock/templates/stock/item_base.html:304 templates/js/company.js:48 #: templates/js/company.js:164 templates/js/order.js:162 msgid "Supplier" msgstr "" @@ -1512,8 +1525,8 @@ msgid "Uses default currency" msgstr "" #: company/templates/company/detail.html:62 -#: order/templates/order/sales_order_base.html:89 stock/models.py:379 -#: stock/models.py:380 stock/templates/stock/item_base.html:211 +#: order/templates/order/sales_order_base.html:89 stock/models.py:380 +#: stock/models.py:381 stock/templates/stock/item_base.html:221 #: templates/js/company.js:40 templates/js/order.js:250 msgid "Customer" msgstr "" @@ -1606,8 +1619,8 @@ msgstr "" #: company/templates/company/purchase_orders.html:9 #: company/templates/company/tabs.html:17 -#: order/templates/order/purchase_orders.html:7 -#: order/templates/order/purchase_orders.html:12 +#: order/templates/order/purchase_orders.html:8 +#: order/templates/order/purchase_orders.html:13 #: part/templates/part/orders.html:9 part/templates/part/tabs.html:48 #: templates/InvenTree/settings/tabs.html:31 templates/navbar.html:33 #: users/models.py:31 @@ -1615,19 +1628,19 @@ msgid "Purchase Orders" msgstr "" #: company/templates/company/purchase_orders.html:15 -#: order/templates/order/purchase_orders.html:18 +#: order/templates/order/purchase_orders.html:19 msgid "Create new purchase order" msgstr "" #: company/templates/company/purchase_orders.html:16 -#: order/templates/order/purchase_orders.html:19 +#: order/templates/order/purchase_orders.html:20 msgid "New Purchase Order" msgstr "" #: company/templates/company/sales_orders.html:9 #: company/templates/company/tabs.html:22 -#: order/templates/order/sales_orders.html:7 -#: order/templates/order/sales_orders.html:12 +#: order/templates/order/sales_orders.html:8 +#: order/templates/order/sales_orders.html:13 #: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:56 #: templates/InvenTree/settings/tabs.html:34 templates/navbar.html:42 #: users/models.py:32 @@ -1635,18 +1648,18 @@ msgid "Sales Orders" msgstr "" #: company/templates/company/sales_orders.html:15 -#: order/templates/order/sales_orders.html:18 +#: order/templates/order/sales_orders.html:19 msgid "Create new sales order" msgstr "" #: company/templates/company/sales_orders.html:16 -#: order/templates/order/sales_orders.html:19 +#: order/templates/order/sales_orders.html:20 msgid "New Sales Order" msgstr "" #: company/templates/company/supplier_part_base.html:6 -#: company/templates/company/supplier_part_base.html:19 stock/models.py:353 -#: stock/templates/stock/item_base.html:299 templates/js/company.js:180 +#: company/templates/company/supplier_part_base.html:19 stock/models.py:354 +#: stock/templates/stock/item_base.html:309 templates/js/company.js:180 msgid "Supplier Part" msgstr "" @@ -1874,7 +1887,7 @@ msgstr "" msgid "Enter sales order number" msgstr "" -#: order/forms.py:134 order/models.py:330 +#: order/forms.py:134 order/models.py:405 msgid "" "Target date for order completion. Order will be overdue after this date." msgstr "" @@ -1895,107 +1908,107 @@ msgstr "" msgid "Order notes" msgstr "" -#: order/models.py:131 order/models.py:323 +#: order/models.py:169 order/models.py:398 msgid "Purchase order status" msgstr "" -#: order/models.py:139 +#: order/models.py:177 msgid "Company from which the items are being ordered" msgstr "" -#: order/models.py:142 +#: order/models.py:180 msgid "Supplier order reference code" msgstr "" -#: order/models.py:151 +#: order/models.py:189 msgid "Date order was issued" msgstr "" -#: order/models.py:153 +#: order/models.py:191 msgid "Date order was completed" msgstr "" -#: order/models.py:176 order/models.py:258 part/views.py:1504 -#: stock/models.py:250 stock/models.py:855 +#: order/models.py:214 order/models.py:296 part/views.py:1504 +#: stock/models.py:251 stock/models.py:856 msgid "Quantity must be greater than zero" msgstr "" -#: order/models.py:181 +#: order/models.py:219 msgid "Part supplier must match PO supplier" msgstr "" -#: order/models.py:253 +#: order/models.py:291 msgid "Lines can only be received against an order marked as 'Placed'" msgstr "" -#: order/models.py:319 +#: order/models.py:394 msgid "Company to which the items are being sold" msgstr "" -#: order/models.py:325 +#: order/models.py:400 msgid "Customer order reference code" msgstr "" -#: order/models.py:387 +#: order/models.py:462 msgid "SalesOrder cannot be shipped as it is not currently pending" msgstr "" -#: order/models.py:474 +#: order/models.py:549 msgid "Item quantity" msgstr "" -#: order/models.py:476 +#: order/models.py:551 msgid "Line item reference" msgstr "" -#: order/models.py:478 +#: order/models.py:553 msgid "Line item notes" msgstr "" -#: order/models.py:504 order/templates/order/order_base.html:9 +#: order/models.py:579 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:24 -#: stock/templates/stock/item_base.html:266 templates/js/order.js:146 +#: stock/templates/stock/item_base.html:276 templates/js/order.js:146 msgid "Purchase Order" msgstr "" -#: order/models.py:517 +#: order/models.py:592 msgid "Supplier part" msgstr "" -#: order/models.py:520 +#: order/models.py:595 msgid "Number of items received" msgstr "" -#: order/models.py:527 stock/models.py:472 -#: stock/templates/stock/item_base.html:273 +#: order/models.py:602 stock/models.py:473 +#: stock/templates/stock/item_base.html:283 msgid "Purchase Price" msgstr "" -#: order/models.py:528 +#: order/models.py:603 msgid "Unit purchase price" msgstr "" -#: order/models.py:623 +#: order/models.py:698 msgid "Cannot allocate stock item to a line with a different part" msgstr "" -#: order/models.py:625 +#: order/models.py:700 msgid "Cannot allocate stock to a line without a part" msgstr "" -#: order/models.py:628 +#: order/models.py:703 msgid "Allocation quantity cannot exceed stock quantity" msgstr "" -#: order/models.py:638 +#: order/models.py:713 msgid "Quantity must be 1 for serialized stock item" msgstr "" -#: order/models.py:654 +#: order/models.py:729 msgid "Select stock item to allocate" msgstr "" -#: order/models.py:657 +#: order/models.py:732 msgid "Enter stock allocation quantity" msgstr "" @@ -2210,8 +2223,8 @@ msgid "Sales Order Items" msgstr "" #: order/templates/order/sales_order_detail.html:72 -#: order/templates/order/sales_order_detail.html:154 stock/models.py:384 -#: stock/templates/stock/item_base.html:198 templates/js/build.js:418 +#: order/templates/order/sales_order_detail.html:154 stock/models.py:385 +#: stock/templates/stock/item_base.html:208 templates/js/build.js:418 msgid "Serial Number" msgstr "" @@ -2856,7 +2869,7 @@ msgid "BOM line checksum" msgstr "" #: part/models.py:1963 part/views.py:1510 part/views.py:1562 -#: stock/models.py:240 +#: stock/models.py:241 msgid "Quantity must be integer value for trackable parts" msgstr "" @@ -2893,7 +2906,7 @@ msgstr "" #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 #: stock/templates/stock/item_base.html:72 -#: stock/templates/stock/item_base.html:281 +#: stock/templates/stock/item_base.html:291 #: stock/templates/stock/stock_adjust.html:16 templates/js/build.js:751 #: templates/js/stock.js:720 templates/js/stock.js:980 msgid "Stock Item" @@ -3254,7 +3267,7 @@ msgstr "" msgid "New Parameter" msgstr "" -#: part/templates/part/params.html:25 stock/models.py:1463 +#: part/templates/part/params.html:25 stock/models.py:1499 #: templates/InvenTree/settings/header.html:8 templates/js/stock.js:113 msgid "Value" msgstr "" @@ -3302,7 +3315,7 @@ msgid "Show QR Code" msgstr "" #: part/templates/part/part_base.html:52 -#: stock/templates/stock/item_base.html:111 +#: stock/templates/stock/item_base.html:126 #: stock/templates/stock/location.html:32 msgid "Print Label" msgstr "" @@ -3440,7 +3453,7 @@ msgstr "" msgid "Used In" msgstr "" -#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:339 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:349 msgid "Tests" msgstr "" @@ -3781,237 +3794,237 @@ msgstr "" msgid "Set the destination as the default location for selected parts" msgstr "" -#: stock/models.py:185 +#: stock/models.py:186 msgid "Created stock item" msgstr "" -#: stock/models.py:221 +#: stock/models.py:222 msgid "StockItem with this serial number already exists" msgstr "" -#: stock/models.py:257 +#: stock/models.py:258 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "" -#: stock/models.py:267 stock/models.py:276 +#: stock/models.py:268 stock/models.py:277 msgid "Quantity must be 1 for item with a serial number" msgstr "" -#: stock/models.py:268 +#: stock/models.py:269 msgid "Serial number cannot be set if quantity greater than 1" msgstr "" -#: stock/models.py:290 +#: stock/models.py:291 msgid "Item cannot belong to itself" msgstr "" -#: stock/models.py:296 +#: stock/models.py:297 msgid "Item must have a build reference if is_building=True" msgstr "" -#: stock/models.py:303 +#: stock/models.py:304 msgid "Build reference does not point to the same part object" msgstr "" -#: stock/models.py:336 +#: stock/models.py:337 msgid "Parent Stock Item" msgstr "" -#: stock/models.py:345 +#: stock/models.py:346 msgid "Base part" msgstr "" -#: stock/models.py:354 +#: stock/models.py:355 msgid "Select a matching supplier part for this stock item" msgstr "" -#: stock/models.py:359 stock/templates/stock/stock_app_base.html:7 +#: stock/models.py:360 stock/templates/stock/stock_app_base.html:7 msgid "Stock Location" msgstr "" -#: stock/models.py:362 +#: stock/models.py:363 msgid "Where is this stock item located?" msgstr "" -#: stock/models.py:367 stock/templates/stock/item_base.html:219 +#: stock/models.py:368 stock/templates/stock/item_base.html:229 msgid "Installed In" msgstr "" -#: stock/models.py:370 +#: stock/models.py:371 msgid "Is this item installed in another item?" msgstr "" -#: stock/models.py:386 +#: stock/models.py:387 msgid "Serial number for this item" msgstr "" -#: stock/models.py:398 +#: stock/models.py:399 msgid "Batch code for this stock item" msgstr "" -#: stock/models.py:402 +#: stock/models.py:403 msgid "Stock Quantity" msgstr "" -#: stock/models.py:411 +#: stock/models.py:412 msgid "Source Build" msgstr "" -#: stock/models.py:413 +#: stock/models.py:414 msgid "Build for this stock item" msgstr "" -#: stock/models.py:424 +#: stock/models.py:425 msgid "Source Purchase Order" msgstr "" -#: stock/models.py:427 +#: stock/models.py:428 msgid "Purchase order for this stock item" msgstr "" -#: stock/models.py:433 +#: stock/models.py:434 msgid "Destination Sales Order" msgstr "" -#: stock/models.py:439 stock/templates/stock/item_base.html:306 +#: stock/models.py:440 stock/templates/stock/item_base.html:316 #: templates/js/stock.js:597 msgid "Expiry Date" msgstr "" -#: stock/models.py:440 +#: stock/models.py:441 msgid "" "Expiry date for stock item. Stock will be considered expired after this date" msgstr "" -#: stock/models.py:453 +#: stock/models.py:454 msgid "Delete this Stock Item when stock is depleted" msgstr "" -#: stock/models.py:463 stock/templates/stock/item_notes.html:14 +#: stock/models.py:464 stock/templates/stock/item_notes.html:14 #: stock/templates/stock/item_notes.html:30 msgid "Stock Item Notes" msgstr "" -#: stock/models.py:473 +#: stock/models.py:474 msgid "Single unit purchase price at time of purchase" msgstr "" -#: stock/models.py:573 +#: stock/models.py:574 msgid "Assigned to Customer" msgstr "" -#: stock/models.py:575 +#: stock/models.py:576 msgid "Manually assigned to customer" msgstr "" -#: stock/models.py:588 +#: stock/models.py:589 msgid "Returned from customer" msgstr "" -#: stock/models.py:590 +#: stock/models.py:591 msgid "Returned to location" msgstr "" -#: stock/models.py:715 +#: stock/models.py:716 msgid "Installed into stock item" msgstr "" -#: stock/models.py:723 +#: stock/models.py:724 msgid "Installed stock item" msgstr "" -#: stock/models.py:747 +#: stock/models.py:748 msgid "Uninstalled stock item" msgstr "" -#: stock/models.py:766 +#: stock/models.py:767 msgid "Uninstalled into location" msgstr "" -#: stock/models.py:846 +#: stock/models.py:847 msgid "Part is not set as trackable" msgstr "" -#: stock/models.py:852 +#: stock/models.py:853 msgid "Quantity must be integer" msgstr "" -#: stock/models.py:858 +#: stock/models.py:859 #, python-brace-format msgid "Quantity must not exceed available stock quantity ({n})" msgstr "" -#: stock/models.py:861 +#: stock/models.py:862 msgid "Serial numbers must be a list of integers" msgstr "" -#: stock/models.py:864 +#: stock/models.py:865 msgid "Quantity does not match serial numbers" msgstr "" -#: stock/models.py:896 +#: stock/models.py:897 msgid "Add serial number" msgstr "" -#: stock/models.py:899 +#: stock/models.py:900 #, python-brace-format msgid "Serialized {n} items" msgstr "" -#: stock/models.py:1010 +#: stock/models.py:1011 msgid "StockItem cannot be moved as it is not in stock" msgstr "" -#: stock/models.py:1364 +#: stock/models.py:1400 msgid "Tracking entry title" msgstr "" -#: stock/models.py:1366 +#: stock/models.py:1402 msgid "Entry notes" msgstr "" -#: stock/models.py:1368 +#: stock/models.py:1404 msgid "Link to external page for further information" msgstr "" -#: stock/models.py:1428 +#: stock/models.py:1464 msgid "Value must be provided for this test" msgstr "" -#: stock/models.py:1434 +#: stock/models.py:1470 msgid "Attachment must be uploaded for this test" msgstr "" -#: stock/models.py:1451 +#: stock/models.py:1487 msgid "Test" msgstr "" -#: stock/models.py:1452 +#: stock/models.py:1488 msgid "Test name" msgstr "" -#: stock/models.py:1457 +#: stock/models.py:1493 msgid "Result" msgstr "" -#: stock/models.py:1458 templates/js/table_filters.js:172 +#: stock/models.py:1494 templates/js/table_filters.js:172 msgid "Test result" msgstr "" -#: stock/models.py:1464 +#: stock/models.py:1500 msgid "Test output value" msgstr "" -#: stock/models.py:1470 +#: stock/models.py:1506 msgid "Attachment" msgstr "" -#: stock/models.py:1471 +#: stock/models.py:1507 msgid "Test result attachment" msgstr "" -#: stock/models.py:1477 +#: stock/models.py:1513 msgid "Test notes" msgstr "" @@ -4063,128 +4076,133 @@ msgid "" msgstr "" #: stock/templates/stock/item_base.html:74 -#: stock/templates/stock/item_base.html:310 templates/js/table_filters.js:111 +#: stock/templates/stock/item_base.html:320 templates/js/table_filters.js:111 msgid "Expired" msgstr "" #: stock/templates/stock/item_base.html:78 -#: stock/templates/stock/item_base.html:312 templates/js/table_filters.js:116 +#: stock/templates/stock/item_base.html:322 templates/js/table_filters.js:116 msgid "Stale" msgstr "" -#: stock/templates/stock/item_base.html:114 templates/js/barcode.js:283 +#: stock/templates/stock/item_base.html:113 templates/js/barcode.js:283 #: templates/js/barcode.js:288 msgid "Unlink Barcode" msgstr "" -#: stock/templates/stock/item_base.html:116 +#: stock/templates/stock/item_base.html:115 msgid "Link Barcode" msgstr "" -#: stock/templates/stock/item_base.html:124 +#: stock/templates/stock/item_base.html:123 +msgid "Document actions" +msgstr "" + +#: stock/templates/stock/item_base.html:129 +#: stock/templates/stock/item_tests.html:25 +msgid "Test Report" +msgstr "" + +#: stock/templates/stock/item_base.html:137 msgid "Stock adjustment actions" msgstr "" -#: stock/templates/stock/item_base.html:128 +#: stock/templates/stock/item_base.html:141 #: stock/templates/stock/location.html:41 templates/stock_table.html:23 msgid "Count stock" msgstr "" -#: stock/templates/stock/item_base.html:129 templates/stock_table.html:21 +#: stock/templates/stock/item_base.html:142 templates/stock_table.html:21 msgid "Add stock" msgstr "" -#: stock/templates/stock/item_base.html:130 templates/stock_table.html:22 +#: stock/templates/stock/item_base.html:143 templates/stock_table.html:22 msgid "Remove stock" msgstr "" -#: stock/templates/stock/item_base.html:132 +#: stock/templates/stock/item_base.html:145 msgid "Transfer stock" msgstr "" -#: stock/templates/stock/item_base.html:134 +#: stock/templates/stock/item_base.html:147 msgid "Serialize stock" msgstr "" -#: stock/templates/stock/item_base.html:138 +#: stock/templates/stock/item_base.html:151 msgid "Assign to customer" msgstr "" -#: stock/templates/stock/item_base.html:141 +#: stock/templates/stock/item_base.html:154 msgid "Return to stock" msgstr "" -#: stock/templates/stock/item_base.html:145 templates/js/stock.js:1017 +#: stock/templates/stock/item_base.html:158 templates/js/stock.js:1017 msgid "Uninstall stock item" msgstr "" -#: stock/templates/stock/item_base.html:145 +#: stock/templates/stock/item_base.html:158 msgid "Uninstall" msgstr "" -#: stock/templates/stock/item_base.html:154 +#: stock/templates/stock/item_base.html:167 #: stock/templates/stock/location.html:38 msgid "Stock actions" msgstr "" -#: stock/templates/stock/item_base.html:157 +#: stock/templates/stock/item_base.html:170 msgid "Convert to variant" msgstr "" -#: stock/templates/stock/item_base.html:160 +#: stock/templates/stock/item_base.html:173 msgid "Duplicate stock item" msgstr "" -#: stock/templates/stock/item_base.html:162 +#: stock/templates/stock/item_base.html:175 msgid "Edit stock item" msgstr "" -#: stock/templates/stock/item_base.html:165 +#: stock/templates/stock/item_base.html:178 msgid "Delete stock item" msgstr "" -#: stock/templates/stock/item_base.html:171 -msgid "Generate test report" -msgstr "" - -#: stock/templates/stock/item_base.html:179 +#: stock/templates/stock/item_base.html:189 msgid "Stock Item Details" msgstr "" -#: stock/templates/stock/item_base.html:238 templates/js/build.js:442 +#: stock/templates/stock/item_base.html:248 templates/js/build.js:442 msgid "No location set" msgstr "" -#: stock/templates/stock/item_base.html:245 +#: stock/templates/stock/item_base.html:255 msgid "Barcode Identifier" msgstr "" -#: stock/templates/stock/item_base.html:259 templates/js/build.js:642 +#: stock/templates/stock/item_base.html:269 templates/js/build.js:642 #: templates/navbar.html:25 msgid "Build" msgstr "" -#: stock/templates/stock/item_base.html:280 +#: stock/templates/stock/item_base.html:290 msgid "Parent Item" msgstr "" -#: stock/templates/stock/item_base.html:310 +#: stock/templates/stock/item_base.html:320 msgid "This StockItem expired on" msgstr "" -#: stock/templates/stock/item_base.html:312 +#: stock/templates/stock/item_base.html:322 msgid "This StockItem expires on" msgstr "" -#: stock/templates/stock/item_base.html:319 +#: stock/templates/stock/item_base.html:329 msgid "Last Updated" msgstr "" -#: stock/templates/stock/item_base.html:324 +#: stock/templates/stock/item_base.html:334 msgid "Last Stocktake" msgstr "" -#: stock/templates/stock/item_base.html:328 +#: stock/templates/stock/item_base.html:338 msgid "No stocktake performed" msgstr "" @@ -4240,10 +4258,6 @@ msgstr "" msgid "Add Test Data" msgstr "" -#: stock/templates/stock/item_tests.html:25 -msgid "Test Report" -msgstr "" - #: stock/templates/stock/location.html:18 msgid "All stock items" msgstr "" diff --git a/InvenTree/locale/es/LC_MESSAGES/django.po b/InvenTree/locale/es/LC_MESSAGES/django.po index 395f42c4e6..18f86b6972 100644 --- a/InvenTree/locale/es/LC_MESSAGES/django.po +++ b/InvenTree/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-01-06 23:11+1100\n" +"POT-Creation-Date: 2021-01-07 23:48+1100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -54,7 +54,7 @@ msgstr "" msgid "Select Category" msgstr "" -#: InvenTree/helpers.py:361 order/models.py:178 order/models.py:260 +#: InvenTree/helpers.py:361 order/models.py:216 order/models.py:298 #: stock/views.py:1660 msgid "Invalid quantity provided" msgstr "" @@ -133,7 +133,7 @@ msgid "InvenTree system health checks failed" msgstr "" #: InvenTree/status_codes.py:94 InvenTree/status_codes.py:135 -#: InvenTree/status_codes.py:223 +#: InvenTree/status_codes.py:228 msgid "Pending" msgstr "" @@ -141,22 +141,22 @@ msgstr "" msgid "Placed" msgstr "" -#: InvenTree/status_codes.py:96 InvenTree/status_codes.py:226 +#: InvenTree/status_codes.py:96 InvenTree/status_codes.py:231 msgid "Complete" msgstr "" #: InvenTree/status_codes.py:97 InvenTree/status_codes.py:137 -#: InvenTree/status_codes.py:225 +#: InvenTree/status_codes.py:230 msgid "Cancelled" msgstr "" #: InvenTree/status_codes.py:98 InvenTree/status_codes.py:138 -#: InvenTree/status_codes.py:175 +#: InvenTree/status_codes.py:180 msgid "Lost" msgstr "" #: InvenTree/status_codes.py:99 InvenTree/status_codes.py:139 -#: InvenTree/status_codes.py:177 +#: InvenTree/status_codes.py:182 msgid "Returned" msgstr "" @@ -165,27 +165,27 @@ msgstr "" msgid "Shipped" msgstr "" -#: InvenTree/status_codes.py:171 +#: InvenTree/status_codes.py:176 msgid "OK" msgstr "" -#: InvenTree/status_codes.py:172 +#: InvenTree/status_codes.py:177 msgid "Attention needed" msgstr "" -#: InvenTree/status_codes.py:173 +#: InvenTree/status_codes.py:178 msgid "Damaged" msgstr "" -#: InvenTree/status_codes.py:174 +#: InvenTree/status_codes.py:179 msgid "Destroyed" msgstr "" -#: InvenTree/status_codes.py:176 +#: InvenTree/status_codes.py:181 msgid "Rejected" msgstr "" -#: InvenTree/status_codes.py:224 +#: InvenTree/status_codes.py:229 msgid "Production" msgstr "" @@ -295,7 +295,7 @@ msgstr "" msgid "Order target date" msgstr "" -#: build/forms.py:39 build/models.py:175 +#: build/forms.py:39 build/models.py:206 msgid "" "Target date for build completion. Build will be overdue after this date." msgstr "" @@ -313,7 +313,7 @@ msgstr "" #: part/templates/part/sale_prices.html:82 stock/forms.py:304 #: stock/templates/stock/item_base.html:40 #: stock/templates/stock/item_base.html:46 -#: stock/templates/stock/item_base.html:204 +#: stock/templates/stock/item_base.html:214 #: stock/templates/stock/stock_adjust.html:18 templates/js/barcode.js:338 #: templates/js/bom.js:195 templates/js/build.js:420 templates/js/stock.js:750 #: templates/js/stock.js:989 @@ -378,23 +378,23 @@ msgstr "" msgid "Build Order" msgstr "" -#: build/models.py:62 build/templates/build/index.html:6 -#: build/templates/build/index.html:14 order/templates/order/so_builds.html:11 +#: build/models.py:62 build/templates/build/index.html:8 +#: build/templates/build/index.html:15 order/templates/order/so_builds.html:11 #: order/templates/order/so_tabs.html:9 part/templates/part/tabs.html:31 #: templates/InvenTree/settings/tabs.html:28 users/models.py:30 msgid "Build Orders" msgstr "" -#: build/models.py:77 +#: build/models.py:108 msgid "Build Order Reference" msgstr "" -#: build/models.py:78 order/templates/order/purchase_order_detail.html:174 +#: build/models.py:109 order/templates/order/purchase_order_detail.html:174 #: templates/js/bom.js:187 templates/js/build.js:509 msgid "Reference" msgstr "" -#: build/models.py:85 build/templates/build/detail.html:19 +#: build/models.py:116 build/templates/build/detail.html:19 #: company/models.py:359 company/templates/company/detail.html:23 #: company/templates/company/supplier_part_base.html:61 #: company/templates/company/supplier_part_detail.html:27 @@ -409,22 +409,22 @@ msgstr "" msgid "Description" msgstr "" -#: build/models.py:88 +#: build/models.py:119 msgid "Brief description of the build" msgstr "" -#: build/models.py:97 build/templates/build/build_base.html:104 +#: build/models.py:128 build/templates/build/build_base.html:113 #: build/templates/build/detail.html:75 msgid "Parent Build" msgstr "" -#: build/models.py:98 +#: build/models.py:129 msgid "BuildOrder to which this build is allocated" msgstr "" -#: build/models.py:103 build/templates/build/auto_allocate.html:16 +#: build/models.py:134 build/templates/build/auto_allocate.html:16 #: build/templates/build/build_base.html:78 -#: build/templates/build/detail.html:24 order/models.py:548 +#: build/templates/build/detail.html:24 order/models.py:623 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:148 #: order/templates/order/receive_parts.html:19 part/models.py:316 @@ -437,158 +437,158 @@ msgstr "" msgid "Part" msgstr "" -#: build/models.py:111 +#: build/models.py:142 msgid "Select part to build" msgstr "" -#: build/models.py:116 +#: build/models.py:147 msgid "Sales Order Reference" msgstr "" -#: build/models.py:120 +#: build/models.py:151 msgid "SalesOrder to which this build is allocated" msgstr "" -#: build/models.py:125 +#: build/models.py:156 msgid "Source Location" msgstr "" -#: build/models.py:129 +#: build/models.py:160 msgid "" "Select location to take stock from for this build (leave blank to take from " "any stock location)" msgstr "" -#: build/models.py:134 +#: build/models.py:165 msgid "Destination Location" msgstr "" -#: build/models.py:138 +#: build/models.py:169 msgid "Select location where the completed items will be stored" msgstr "" -#: build/models.py:142 +#: build/models.py:173 msgid "Build Quantity" msgstr "" -#: build/models.py:145 +#: build/models.py:176 msgid "Number of stock items to build" msgstr "" -#: build/models.py:149 +#: build/models.py:180 msgid "Completed items" msgstr "" -#: build/models.py:151 +#: build/models.py:182 msgid "Number of stock items which have been completed" msgstr "" -#: build/models.py:155 part/templates/part/part_base.html:155 +#: build/models.py:186 part/templates/part/part_base.html:155 msgid "Build Status" msgstr "" -#: build/models.py:159 +#: build/models.py:190 msgid "Build status code" msgstr "" -#: build/models.py:163 stock/models.py:396 +#: build/models.py:194 stock/models.py:397 msgid "Batch Code" msgstr "" -#: build/models.py:167 +#: build/models.py:198 msgid "Batch code for this build output" msgstr "" -#: build/models.py:174 order/models.py:329 +#: build/models.py:205 order/models.py:404 msgid "Target completion date" msgstr "" -#: build/models.py:188 build/templates/build/detail.html:89 +#: build/models.py:219 build/templates/build/detail.html:89 #: company/templates/company/supplier_part_base.html:68 #: company/templates/company/supplier_part_detail.html:24 #: part/templates/part/detail.html:80 part/templates/part/part_base.html:102 -#: stock/models.py:390 stock/templates/stock/item_base.html:287 +#: stock/models.py:391 stock/templates/stock/item_base.html:297 msgid "External Link" msgstr "" -#: build/models.py:189 part/models.py:705 stock/models.py:392 +#: build/models.py:220 part/models.py:705 stock/models.py:393 msgid "Link to external URL" msgstr "" -#: build/models.py:193 build/templates/build/tabs.html:23 company/models.py:366 +#: build/models.py:224 build/templates/build/tabs.html:23 company/models.py:366 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:18 #: order/templates/order/purchase_order_detail.html:213 #: order/templates/order/so_tabs.html:23 part/models.py:831 #: part/templates/part/tabs.html:73 stock/forms.py:313 stock/forms.py:345 -#: stock/forms.py:373 stock/models.py:462 stock/models.py:1476 +#: stock/forms.py:373 stock/models.py:463 stock/models.py:1512 #: stock/templates/stock/tabs.html:26 templates/js/barcode.js:391 #: templates/js/bom.js:263 templates/js/stock.js:117 templates/js/stock.js:603 msgid "Notes" msgstr "" -#: build/models.py:194 +#: build/models.py:225 msgid "Extra build notes" msgstr "" -#: build/models.py:579 +#: build/models.py:610 msgid "No build output specified" msgstr "" -#: build/models.py:582 +#: build/models.py:613 msgid "Build output is already completed" msgstr "" -#: build/models.py:585 +#: build/models.py:616 msgid "Build output does not match Build Order" msgstr "" -#: build/models.py:660 +#: build/models.py:691 msgid "Completed build output" msgstr "" -#: build/models.py:902 +#: build/models.py:933 msgid "BuildItem must be unique for build, stock_item and install_into" msgstr "" -#: build/models.py:924 +#: build/models.py:955 msgid "Build item must specify a build output" msgstr "" -#: build/models.py:929 +#: build/models.py:960 #, python-brace-format msgid "Selected stock item not found in BOM for part '{p}'" msgstr "" -#: build/models.py:933 +#: build/models.py:964 #, python-brace-format msgid "Allocated quantity ({n}) must not exceed available quantity ({q})" msgstr "" -#: build/models.py:940 order/models.py:632 +#: build/models.py:971 order/models.py:707 msgid "StockItem is over-allocated" msgstr "" -#: build/models.py:944 order/models.py:635 +#: build/models.py:975 order/models.py:710 msgid "Allocation quantity must be greater than zero" msgstr "" -#: build/models.py:948 +#: build/models.py:979 msgid "Quantity must be 1 for serialized stock" msgstr "" -#: build/models.py:988 +#: build/models.py:1019 msgid "Build to allocate parts" msgstr "" -#: build/models.py:995 +#: build/models.py:1026 msgid "Source stock item" msgstr "" -#: build/models.py:1007 +#: build/models.py:1038 msgid "Stock quantity to allocate to build" msgstr "" -#: build/models.py:1015 +#: build/models.py:1046 msgid "Destination stock item" msgstr "" @@ -654,7 +654,7 @@ msgid "" msgstr "" #: build/templates/build/auto_allocate.html:18 stock/forms.py:343 -#: stock/templates/stock/item_base.html:234 +#: stock/templates/stock/item_base.html:244 #: stock/templates/stock/stock_adjust.html:17 #: templates/InvenTree/search.html:183 templates/js/barcode.js:337 #: templates/js/build.js:434 templates/js/stock.js:587 @@ -688,7 +688,7 @@ msgid "Admin view" msgstr "" #: build/templates/build/build_base.html:43 -#: build/templates/build/build_base.html:92 +#: build/templates/build/build_base.html:100 #: order/templates/order/sales_order_base.html:41 #: order/templates/order/sales_order_base.html:83 #: templates/js/table_filters.js:200 templates/js/table_filters.js:232 @@ -718,30 +718,37 @@ msgstr "" #: build/templates/build/build_base.html:88 #: build/templates/build/detail.html:57 #: order/templates/order/receive_parts.html:24 -#: stock/templates/stock/item_base.html:333 templates/InvenTree/search.html:175 +#: stock/templates/stock/item_base.html:343 templates/InvenTree/search.html:175 #: templates/js/barcode.js:42 templates/js/build.js:697 #: templates/js/order.js:180 templates/js/order.js:268 #: templates/js/stock.js:574 templates/js/stock.js:997 msgid "Status" msgstr "" -#: build/templates/build/build_base.html:92 +#: build/templates/build/build_base.html:96 +#: build/templates/build/detail.html:100 +#: order/templates/order/sales_order_base.html:114 templates/js/build.js:710 +#: templates/js/order.js:281 +msgid "Target Date" +msgstr "" + +#: build/templates/build/build_base.html:100 msgid "This build was due on" msgstr "" -#: build/templates/build/build_base.html:98 +#: build/templates/build/build_base.html:107 #: build/templates/build/detail.html:62 msgid "Progress" msgstr "" -#: build/templates/build/build_base.html:111 -#: build/templates/build/detail.html:82 order/models.py:546 +#: build/templates/build/build_base.html:120 +#: build/templates/build/detail.html:82 order/models.py:621 #: order/templates/order/sales_order_base.html:9 #: order/templates/order/sales_order_base.html:33 #: order/templates/order/sales_order_notes.html:10 #: order/templates/order/sales_order_ship.html:25 #: part/templates/part/allocation.html:27 -#: stock/templates/stock/item_base.html:228 templates/js/order.js:229 +#: stock/templates/stock/item_base.html:238 templates/js/order.js:229 msgid "Sales Order" msgstr "" @@ -842,7 +849,7 @@ msgid "Destination location not specified" msgstr "" #: build/templates/build/detail.html:68 -#: stock/templates/stock/item_base.html:252 templates/js/stock.js:582 +#: stock/templates/stock/item_base.html:262 templates/js/stock.js:582 #: templates/js/stock.js:1004 templates/js/table_filters.js:80 #: templates/js/table_filters.js:161 msgid "Batch" @@ -854,12 +861,6 @@ msgstr "" msgid "Created" msgstr "" -#: build/templates/build/detail.html:100 -#: order/templates/order/sales_order_base.html:114 templates/js/build.js:710 -#: templates/js/order.js:281 -msgid "Target Date" -msgstr "" - #: build/templates/build/detail.html:106 msgid "No target date set" msgstr "" @@ -877,10 +878,22 @@ msgstr "" msgid "Alter the quantity of stock allocated to the build output" msgstr "" -#: build/templates/build/index.html:25 build/views.py:658 +#: build/templates/build/index.html:27 build/views.py:658 msgid "New Build Order" msgstr "" +#: build/templates/build/index.html:30 +#: order/templates/order/purchase_orders.html:22 +#: order/templates/order/sales_orders.html:22 +msgid "Display calendar view" +msgstr "" + +#: build/templates/build/index.html:33 +#: order/templates/order/purchase_orders.html:25 +#: order/templates/order/sales_orders.html:25 +msgid "Display list view" +msgstr "" + #: build/templates/build/notes.html:13 build/templates/build/notes.html:30 msgid "Build Notes" msgstr "" @@ -936,7 +949,7 @@ msgstr "" msgid "Create Build Output" msgstr "" -#: build/views.py:207 stock/models.py:871 stock/views.py:1681 +#: build/views.py:207 stock/models.py:872 stock/views.py:1681 msgid "Serial numbers already exist" msgstr "" @@ -1402,8 +1415,8 @@ msgstr "" msgid "Currency" msgstr "" -#: company/models.py:313 stock/models.py:344 -#: stock/templates/stock/item_base.html:184 +#: company/models.py:313 stock/models.py:345 +#: stock/templates/stock/item_base.html:194 msgid "Base Part" msgstr "" @@ -1416,7 +1429,7 @@ msgstr "" #: company/templates/company/supplier_part_detail.html:21 #: order/templates/order/order_base.html:79 #: order/templates/order/order_wizard/select_pos.html:30 part/bom.py:170 -#: stock/templates/stock/item_base.html:294 templates/js/company.js:48 +#: stock/templates/stock/item_base.html:304 templates/js/company.js:48 #: templates/js/company.js:164 templates/js/order.js:162 msgid "Supplier" msgstr "" @@ -1512,8 +1525,8 @@ msgid "Uses default currency" msgstr "" #: company/templates/company/detail.html:62 -#: order/templates/order/sales_order_base.html:89 stock/models.py:379 -#: stock/models.py:380 stock/templates/stock/item_base.html:211 +#: order/templates/order/sales_order_base.html:89 stock/models.py:380 +#: stock/models.py:381 stock/templates/stock/item_base.html:221 #: templates/js/company.js:40 templates/js/order.js:250 msgid "Customer" msgstr "" @@ -1606,8 +1619,8 @@ msgstr "" #: company/templates/company/purchase_orders.html:9 #: company/templates/company/tabs.html:17 -#: order/templates/order/purchase_orders.html:7 -#: order/templates/order/purchase_orders.html:12 +#: order/templates/order/purchase_orders.html:8 +#: order/templates/order/purchase_orders.html:13 #: part/templates/part/orders.html:9 part/templates/part/tabs.html:48 #: templates/InvenTree/settings/tabs.html:31 templates/navbar.html:33 #: users/models.py:31 @@ -1615,19 +1628,19 @@ msgid "Purchase Orders" msgstr "" #: company/templates/company/purchase_orders.html:15 -#: order/templates/order/purchase_orders.html:18 +#: order/templates/order/purchase_orders.html:19 msgid "Create new purchase order" msgstr "" #: company/templates/company/purchase_orders.html:16 -#: order/templates/order/purchase_orders.html:19 +#: order/templates/order/purchase_orders.html:20 msgid "New Purchase Order" msgstr "" #: company/templates/company/sales_orders.html:9 #: company/templates/company/tabs.html:22 -#: order/templates/order/sales_orders.html:7 -#: order/templates/order/sales_orders.html:12 +#: order/templates/order/sales_orders.html:8 +#: order/templates/order/sales_orders.html:13 #: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:56 #: templates/InvenTree/settings/tabs.html:34 templates/navbar.html:42 #: users/models.py:32 @@ -1635,18 +1648,18 @@ msgid "Sales Orders" msgstr "" #: company/templates/company/sales_orders.html:15 -#: order/templates/order/sales_orders.html:18 +#: order/templates/order/sales_orders.html:19 msgid "Create new sales order" msgstr "" #: company/templates/company/sales_orders.html:16 -#: order/templates/order/sales_orders.html:19 +#: order/templates/order/sales_orders.html:20 msgid "New Sales Order" msgstr "" #: company/templates/company/supplier_part_base.html:6 -#: company/templates/company/supplier_part_base.html:19 stock/models.py:353 -#: stock/templates/stock/item_base.html:299 templates/js/company.js:180 +#: company/templates/company/supplier_part_base.html:19 stock/models.py:354 +#: stock/templates/stock/item_base.html:309 templates/js/company.js:180 msgid "Supplier Part" msgstr "" @@ -1874,7 +1887,7 @@ msgstr "" msgid "Enter sales order number" msgstr "" -#: order/forms.py:134 order/models.py:330 +#: order/forms.py:134 order/models.py:405 msgid "" "Target date for order completion. Order will be overdue after this date." msgstr "" @@ -1895,107 +1908,107 @@ msgstr "" msgid "Order notes" msgstr "" -#: order/models.py:131 order/models.py:323 +#: order/models.py:169 order/models.py:398 msgid "Purchase order status" msgstr "" -#: order/models.py:139 +#: order/models.py:177 msgid "Company from which the items are being ordered" msgstr "" -#: order/models.py:142 +#: order/models.py:180 msgid "Supplier order reference code" msgstr "" -#: order/models.py:151 +#: order/models.py:189 msgid "Date order was issued" msgstr "" -#: order/models.py:153 +#: order/models.py:191 msgid "Date order was completed" msgstr "" -#: order/models.py:176 order/models.py:258 part/views.py:1504 -#: stock/models.py:250 stock/models.py:855 +#: order/models.py:214 order/models.py:296 part/views.py:1504 +#: stock/models.py:251 stock/models.py:856 msgid "Quantity must be greater than zero" msgstr "" -#: order/models.py:181 +#: order/models.py:219 msgid "Part supplier must match PO supplier" msgstr "" -#: order/models.py:253 +#: order/models.py:291 msgid "Lines can only be received against an order marked as 'Placed'" msgstr "" -#: order/models.py:319 +#: order/models.py:394 msgid "Company to which the items are being sold" msgstr "" -#: order/models.py:325 +#: order/models.py:400 msgid "Customer order reference code" msgstr "" -#: order/models.py:387 +#: order/models.py:462 msgid "SalesOrder cannot be shipped as it is not currently pending" msgstr "" -#: order/models.py:474 +#: order/models.py:549 msgid "Item quantity" msgstr "" -#: order/models.py:476 +#: order/models.py:551 msgid "Line item reference" msgstr "" -#: order/models.py:478 +#: order/models.py:553 msgid "Line item notes" msgstr "" -#: order/models.py:504 order/templates/order/order_base.html:9 +#: order/models.py:579 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:24 -#: stock/templates/stock/item_base.html:266 templates/js/order.js:146 +#: stock/templates/stock/item_base.html:276 templates/js/order.js:146 msgid "Purchase Order" msgstr "" -#: order/models.py:517 +#: order/models.py:592 msgid "Supplier part" msgstr "" -#: order/models.py:520 +#: order/models.py:595 msgid "Number of items received" msgstr "" -#: order/models.py:527 stock/models.py:472 -#: stock/templates/stock/item_base.html:273 +#: order/models.py:602 stock/models.py:473 +#: stock/templates/stock/item_base.html:283 msgid "Purchase Price" msgstr "" -#: order/models.py:528 +#: order/models.py:603 msgid "Unit purchase price" msgstr "" -#: order/models.py:623 +#: order/models.py:698 msgid "Cannot allocate stock item to a line with a different part" msgstr "" -#: order/models.py:625 +#: order/models.py:700 msgid "Cannot allocate stock to a line without a part" msgstr "" -#: order/models.py:628 +#: order/models.py:703 msgid "Allocation quantity cannot exceed stock quantity" msgstr "" -#: order/models.py:638 +#: order/models.py:713 msgid "Quantity must be 1 for serialized stock item" msgstr "" -#: order/models.py:654 +#: order/models.py:729 msgid "Select stock item to allocate" msgstr "" -#: order/models.py:657 +#: order/models.py:732 msgid "Enter stock allocation quantity" msgstr "" @@ -2210,8 +2223,8 @@ msgid "Sales Order Items" msgstr "" #: order/templates/order/sales_order_detail.html:72 -#: order/templates/order/sales_order_detail.html:154 stock/models.py:384 -#: stock/templates/stock/item_base.html:198 templates/js/build.js:418 +#: order/templates/order/sales_order_detail.html:154 stock/models.py:385 +#: stock/templates/stock/item_base.html:208 templates/js/build.js:418 msgid "Serial Number" msgstr "" @@ -2856,7 +2869,7 @@ msgid "BOM line checksum" msgstr "" #: part/models.py:1963 part/views.py:1510 part/views.py:1562 -#: stock/models.py:240 +#: stock/models.py:241 msgid "Quantity must be integer value for trackable parts" msgstr "" @@ -2893,7 +2906,7 @@ msgstr "" #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 #: stock/templates/stock/item_base.html:72 -#: stock/templates/stock/item_base.html:281 +#: stock/templates/stock/item_base.html:291 #: stock/templates/stock/stock_adjust.html:16 templates/js/build.js:751 #: templates/js/stock.js:720 templates/js/stock.js:980 msgid "Stock Item" @@ -3254,7 +3267,7 @@ msgstr "" msgid "New Parameter" msgstr "" -#: part/templates/part/params.html:25 stock/models.py:1463 +#: part/templates/part/params.html:25 stock/models.py:1499 #: templates/InvenTree/settings/header.html:8 templates/js/stock.js:113 msgid "Value" msgstr "" @@ -3302,7 +3315,7 @@ msgid "Show QR Code" msgstr "" #: part/templates/part/part_base.html:52 -#: stock/templates/stock/item_base.html:111 +#: stock/templates/stock/item_base.html:126 #: stock/templates/stock/location.html:32 msgid "Print Label" msgstr "" @@ -3440,7 +3453,7 @@ msgstr "" msgid "Used In" msgstr "" -#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:339 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:349 msgid "Tests" msgstr "" @@ -3781,237 +3794,237 @@ msgstr "" msgid "Set the destination as the default location for selected parts" msgstr "" -#: stock/models.py:185 +#: stock/models.py:186 msgid "Created stock item" msgstr "" -#: stock/models.py:221 +#: stock/models.py:222 msgid "StockItem with this serial number already exists" msgstr "" -#: stock/models.py:257 +#: stock/models.py:258 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "" -#: stock/models.py:267 stock/models.py:276 +#: stock/models.py:268 stock/models.py:277 msgid "Quantity must be 1 for item with a serial number" msgstr "" -#: stock/models.py:268 +#: stock/models.py:269 msgid "Serial number cannot be set if quantity greater than 1" msgstr "" -#: stock/models.py:290 +#: stock/models.py:291 msgid "Item cannot belong to itself" msgstr "" -#: stock/models.py:296 +#: stock/models.py:297 msgid "Item must have a build reference if is_building=True" msgstr "" -#: stock/models.py:303 +#: stock/models.py:304 msgid "Build reference does not point to the same part object" msgstr "" -#: stock/models.py:336 +#: stock/models.py:337 msgid "Parent Stock Item" msgstr "" -#: stock/models.py:345 +#: stock/models.py:346 msgid "Base part" msgstr "" -#: stock/models.py:354 +#: stock/models.py:355 msgid "Select a matching supplier part for this stock item" msgstr "" -#: stock/models.py:359 stock/templates/stock/stock_app_base.html:7 +#: stock/models.py:360 stock/templates/stock/stock_app_base.html:7 msgid "Stock Location" msgstr "" -#: stock/models.py:362 +#: stock/models.py:363 msgid "Where is this stock item located?" msgstr "" -#: stock/models.py:367 stock/templates/stock/item_base.html:219 +#: stock/models.py:368 stock/templates/stock/item_base.html:229 msgid "Installed In" msgstr "" -#: stock/models.py:370 +#: stock/models.py:371 msgid "Is this item installed in another item?" msgstr "" -#: stock/models.py:386 +#: stock/models.py:387 msgid "Serial number for this item" msgstr "" -#: stock/models.py:398 +#: stock/models.py:399 msgid "Batch code for this stock item" msgstr "" -#: stock/models.py:402 +#: stock/models.py:403 msgid "Stock Quantity" msgstr "" -#: stock/models.py:411 +#: stock/models.py:412 msgid "Source Build" msgstr "" -#: stock/models.py:413 +#: stock/models.py:414 msgid "Build for this stock item" msgstr "" -#: stock/models.py:424 +#: stock/models.py:425 msgid "Source Purchase Order" msgstr "" -#: stock/models.py:427 +#: stock/models.py:428 msgid "Purchase order for this stock item" msgstr "" -#: stock/models.py:433 +#: stock/models.py:434 msgid "Destination Sales Order" msgstr "" -#: stock/models.py:439 stock/templates/stock/item_base.html:306 +#: stock/models.py:440 stock/templates/stock/item_base.html:316 #: templates/js/stock.js:597 msgid "Expiry Date" msgstr "" -#: stock/models.py:440 +#: stock/models.py:441 msgid "" "Expiry date for stock item. Stock will be considered expired after this date" msgstr "" -#: stock/models.py:453 +#: stock/models.py:454 msgid "Delete this Stock Item when stock is depleted" msgstr "" -#: stock/models.py:463 stock/templates/stock/item_notes.html:14 +#: stock/models.py:464 stock/templates/stock/item_notes.html:14 #: stock/templates/stock/item_notes.html:30 msgid "Stock Item Notes" msgstr "" -#: stock/models.py:473 +#: stock/models.py:474 msgid "Single unit purchase price at time of purchase" msgstr "" -#: stock/models.py:573 +#: stock/models.py:574 msgid "Assigned to Customer" msgstr "" -#: stock/models.py:575 +#: stock/models.py:576 msgid "Manually assigned to customer" msgstr "" -#: stock/models.py:588 +#: stock/models.py:589 msgid "Returned from customer" msgstr "" -#: stock/models.py:590 +#: stock/models.py:591 msgid "Returned to location" msgstr "" -#: stock/models.py:715 +#: stock/models.py:716 msgid "Installed into stock item" msgstr "" -#: stock/models.py:723 +#: stock/models.py:724 msgid "Installed stock item" msgstr "" -#: stock/models.py:747 +#: stock/models.py:748 msgid "Uninstalled stock item" msgstr "" -#: stock/models.py:766 +#: stock/models.py:767 msgid "Uninstalled into location" msgstr "" -#: stock/models.py:846 +#: stock/models.py:847 msgid "Part is not set as trackable" msgstr "" -#: stock/models.py:852 +#: stock/models.py:853 msgid "Quantity must be integer" msgstr "" -#: stock/models.py:858 +#: stock/models.py:859 #, python-brace-format msgid "Quantity must not exceed available stock quantity ({n})" msgstr "" -#: stock/models.py:861 +#: stock/models.py:862 msgid "Serial numbers must be a list of integers" msgstr "" -#: stock/models.py:864 +#: stock/models.py:865 msgid "Quantity does not match serial numbers" msgstr "" -#: stock/models.py:896 +#: stock/models.py:897 msgid "Add serial number" msgstr "" -#: stock/models.py:899 +#: stock/models.py:900 #, python-brace-format msgid "Serialized {n} items" msgstr "" -#: stock/models.py:1010 +#: stock/models.py:1011 msgid "StockItem cannot be moved as it is not in stock" msgstr "" -#: stock/models.py:1364 +#: stock/models.py:1400 msgid "Tracking entry title" msgstr "" -#: stock/models.py:1366 +#: stock/models.py:1402 msgid "Entry notes" msgstr "" -#: stock/models.py:1368 +#: stock/models.py:1404 msgid "Link to external page for further information" msgstr "" -#: stock/models.py:1428 +#: stock/models.py:1464 msgid "Value must be provided for this test" msgstr "" -#: stock/models.py:1434 +#: stock/models.py:1470 msgid "Attachment must be uploaded for this test" msgstr "" -#: stock/models.py:1451 +#: stock/models.py:1487 msgid "Test" msgstr "" -#: stock/models.py:1452 +#: stock/models.py:1488 msgid "Test name" msgstr "" -#: stock/models.py:1457 +#: stock/models.py:1493 msgid "Result" msgstr "" -#: stock/models.py:1458 templates/js/table_filters.js:172 +#: stock/models.py:1494 templates/js/table_filters.js:172 msgid "Test result" msgstr "" -#: stock/models.py:1464 +#: stock/models.py:1500 msgid "Test output value" msgstr "" -#: stock/models.py:1470 +#: stock/models.py:1506 msgid "Attachment" msgstr "" -#: stock/models.py:1471 +#: stock/models.py:1507 msgid "Test result attachment" msgstr "" -#: stock/models.py:1477 +#: stock/models.py:1513 msgid "Test notes" msgstr "" @@ -4063,128 +4076,133 @@ msgid "" msgstr "" #: stock/templates/stock/item_base.html:74 -#: stock/templates/stock/item_base.html:310 templates/js/table_filters.js:111 +#: stock/templates/stock/item_base.html:320 templates/js/table_filters.js:111 msgid "Expired" msgstr "" #: stock/templates/stock/item_base.html:78 -#: stock/templates/stock/item_base.html:312 templates/js/table_filters.js:116 +#: stock/templates/stock/item_base.html:322 templates/js/table_filters.js:116 msgid "Stale" msgstr "" -#: stock/templates/stock/item_base.html:114 templates/js/barcode.js:283 +#: stock/templates/stock/item_base.html:113 templates/js/barcode.js:283 #: templates/js/barcode.js:288 msgid "Unlink Barcode" msgstr "" -#: stock/templates/stock/item_base.html:116 +#: stock/templates/stock/item_base.html:115 msgid "Link Barcode" msgstr "" -#: stock/templates/stock/item_base.html:124 +#: stock/templates/stock/item_base.html:123 +msgid "Document actions" +msgstr "" + +#: stock/templates/stock/item_base.html:129 +#: stock/templates/stock/item_tests.html:25 +msgid "Test Report" +msgstr "" + +#: stock/templates/stock/item_base.html:137 msgid "Stock adjustment actions" msgstr "" -#: stock/templates/stock/item_base.html:128 +#: stock/templates/stock/item_base.html:141 #: stock/templates/stock/location.html:41 templates/stock_table.html:23 msgid "Count stock" msgstr "" -#: stock/templates/stock/item_base.html:129 templates/stock_table.html:21 +#: stock/templates/stock/item_base.html:142 templates/stock_table.html:21 msgid "Add stock" msgstr "" -#: stock/templates/stock/item_base.html:130 templates/stock_table.html:22 +#: stock/templates/stock/item_base.html:143 templates/stock_table.html:22 msgid "Remove stock" msgstr "" -#: stock/templates/stock/item_base.html:132 +#: stock/templates/stock/item_base.html:145 msgid "Transfer stock" msgstr "" -#: stock/templates/stock/item_base.html:134 +#: stock/templates/stock/item_base.html:147 msgid "Serialize stock" msgstr "" -#: stock/templates/stock/item_base.html:138 +#: stock/templates/stock/item_base.html:151 msgid "Assign to customer" msgstr "" -#: stock/templates/stock/item_base.html:141 +#: stock/templates/stock/item_base.html:154 msgid "Return to stock" msgstr "" -#: stock/templates/stock/item_base.html:145 templates/js/stock.js:1017 +#: stock/templates/stock/item_base.html:158 templates/js/stock.js:1017 msgid "Uninstall stock item" msgstr "" -#: stock/templates/stock/item_base.html:145 +#: stock/templates/stock/item_base.html:158 msgid "Uninstall" msgstr "" -#: stock/templates/stock/item_base.html:154 +#: stock/templates/stock/item_base.html:167 #: stock/templates/stock/location.html:38 msgid "Stock actions" msgstr "" -#: stock/templates/stock/item_base.html:157 +#: stock/templates/stock/item_base.html:170 msgid "Convert to variant" msgstr "" -#: stock/templates/stock/item_base.html:160 +#: stock/templates/stock/item_base.html:173 msgid "Duplicate stock item" msgstr "" -#: stock/templates/stock/item_base.html:162 +#: stock/templates/stock/item_base.html:175 msgid "Edit stock item" msgstr "" -#: stock/templates/stock/item_base.html:165 +#: stock/templates/stock/item_base.html:178 msgid "Delete stock item" msgstr "" -#: stock/templates/stock/item_base.html:171 -msgid "Generate test report" -msgstr "" - -#: stock/templates/stock/item_base.html:179 +#: stock/templates/stock/item_base.html:189 msgid "Stock Item Details" msgstr "" -#: stock/templates/stock/item_base.html:238 templates/js/build.js:442 +#: stock/templates/stock/item_base.html:248 templates/js/build.js:442 msgid "No location set" msgstr "" -#: stock/templates/stock/item_base.html:245 +#: stock/templates/stock/item_base.html:255 msgid "Barcode Identifier" msgstr "" -#: stock/templates/stock/item_base.html:259 templates/js/build.js:642 +#: stock/templates/stock/item_base.html:269 templates/js/build.js:642 #: templates/navbar.html:25 msgid "Build" msgstr "" -#: stock/templates/stock/item_base.html:280 +#: stock/templates/stock/item_base.html:290 msgid "Parent Item" msgstr "" -#: stock/templates/stock/item_base.html:310 +#: stock/templates/stock/item_base.html:320 msgid "This StockItem expired on" msgstr "" -#: stock/templates/stock/item_base.html:312 +#: stock/templates/stock/item_base.html:322 msgid "This StockItem expires on" msgstr "" -#: stock/templates/stock/item_base.html:319 +#: stock/templates/stock/item_base.html:329 msgid "Last Updated" msgstr "" -#: stock/templates/stock/item_base.html:324 +#: stock/templates/stock/item_base.html:334 msgid "Last Stocktake" msgstr "" -#: stock/templates/stock/item_base.html:328 +#: stock/templates/stock/item_base.html:338 msgid "No stocktake performed" msgstr "" @@ -4240,10 +4258,6 @@ msgstr "" msgid "Add Test Data" msgstr "" -#: stock/templates/stock/item_tests.html:25 -msgid "Test Report" -msgstr "" - #: stock/templates/stock/location.html:18 msgid "All stock items" msgstr ""