mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 03:26:45 +00:00
* Squashed commit of the following: commit 52d7ff0f650bbcfa2d93ac96562b44269d3812a7 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 23:03:20 2024 +0100 fixed lookup commit 0d076eaea89dce24f08af247479b3b4dff1b4df3 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 23:03:08 2024 +0100 switched to pathlib for lookup commit 473e75eda205793769946e923748356ffd7e5b4b Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:52:30 2024 +0100 fix wrong url response commit fd74f8d703399c19cb3616ea3b2656a50cd7a6e5 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 21:14:38 2024 +0100 switched to ruff for import sorting commit f83fedbbb8de261ff8c706e179519e58e7a91064 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 21:03:14 2024 +0100 switched to single quotes everywhere commit a92442e60e23be0ff5dcf42d222b0d95823ecb9b Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:58:23 2024 +0100 added autofixes commit cc66c93136fcae8a701810a4f4f38ef3b570be61 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:56:47 2024 +0100 enable autoformat commit 1f343606ec1f2a99acf8a37b9900d78a8fb37282 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:42:14 2024 +0100 Squashed commit of the following: commit f5cf7b2e7872fc19633321713965763d1890b495 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:36:57 2024 +0100 fixed reqs commit 9d845bee98befa4e53c2ac3c783bd704369e3ad2 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:32:35 2024 +0100 disable autofix/format commit aff5f271484c3500df7ddde043767c008ce4af21 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:50 2024 +0100 adjust checks commit 47271cf1efa848ec8374a0d83b5646d06fffa6e7 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:22 2024 +0100 reorder order of operations commit e1bf178b40b3f0d2d59ba92209156c43095959d2 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:01:09 2024 +0100 adapted ruff settings to better fit code base commit ad7d88a6f4f15c9552522131c4e207256fc2bbf6 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:59:45 2024 +0100 auto fixed docstring commit a2e54a760e17932dbbc2de0dec23906107f2cda9 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:46:35 2024 +0100 fix getattr useage commit cb80c73bc6c0be7f5d2ed3cc9b2ac03fdefd5c41 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:25:09 2024 +0100 fix requirements file commit b7780bbd21a32007f3b0ce495b519bf59bb19bf5 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:42:28 2024 +0100 fix removed sections commit 71f1681f55c15f62c16c1d7f30a745adc496db97 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:41:21 2024 +0100 fix djlint syntax commit a0bcf1bccef8a8ffd482f38e2063bc9066e1d759 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:35:28 2024 +0100 remove flake8 from code base commit 22475b31cc06919785be046e007915e43f356793 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:34:56 2024 +0100 remove flake8 from code base commit 0413350f14773ac6161473e0cfb069713c13c691 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:39 2024 +0100 moved ruff section commit d90c48a0bf98befdfacbbb093ee56cdb28afb40d Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:24 2024 +0100 move djlint config to pyproject commit c5ce55d5119bf2e35e429986f62f875c86178ae1 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:20:39 2024 +0100 added isort again commit 42a41d23afc280d4ee6f0e640148abc6f460f05a Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:19:02 2024 +0100 move config section commit 85692331816348cb1145570340d1f6488a8265cc Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:17:52 2024 +0100 fix codespell error commit 2897c6704d1311a800ce5aa47878d96d6980b377 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 17:29:21 2024 +0100 replaced flake8 with ruff mostly for speed improvements * enable docstring checks * fix docstrings * fixed D417 Missing argument description * Squashed commit of the following: commit d3b795824b5d6d1c0eda67150b45b5cd672b3f6b Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:56:17 2024 +0100 fixed source path commit 0bac0c19b88897a19d5c995e4ff50427718b827e Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:47:53 2024 +0100 fixed req commit 9f61f01d9cc01f1fb7123102f3658c890469b8ce Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:45:18 2024 +0100 added missing toml req commit 91b71ed24a6761b629768d0ad8829fec2819a966 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:49:50 2024 +0100 moved isort config commit 12460b04196b12d0272d40552402476d5492fea5 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:43:22 2024 +0100 remove flake8 section from setup.cfg commit f5cf7b2e7872fc19633321713965763d1890b495 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:36:57 2024 +0100 fixed reqs commit 9d845bee98befa4e53c2ac3c783bd704369e3ad2 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:32:35 2024 +0100 disable autofix/format commit aff5f271484c3500df7ddde043767c008ce4af21 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:50 2024 +0100 adjust checks commit 47271cf1efa848ec8374a0d83b5646d06fffa6e7 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:22 2024 +0100 reorder order of operations commit e1bf178b40b3f0d2d59ba92209156c43095959d2 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:01:09 2024 +0100 adapted ruff settings to better fit code base commit ad7d88a6f4f15c9552522131c4e207256fc2bbf6 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:59:45 2024 +0100 auto fixed docstring commit a2e54a760e17932dbbc2de0dec23906107f2cda9 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:46:35 2024 +0100 fix getattr useage commit cb80c73bc6c0be7f5d2ed3cc9b2ac03fdefd5c41 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:25:09 2024 +0100 fix requirements file commit b7780bbd21a32007f3b0ce495b519bf59bb19bf5 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:42:28 2024 +0100 fix removed sections commit 71f1681f55c15f62c16c1d7f30a745adc496db97 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:41:21 2024 +0100 fix djlint syntax commit a0bcf1bccef8a8ffd482f38e2063bc9066e1d759 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:35:28 2024 +0100 remove flake8 from code base commit 22475b31cc06919785be046e007915e43f356793 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:34:56 2024 +0100 remove flake8 from code base commit 0413350f14773ac6161473e0cfb069713c13c691 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:39 2024 +0100 moved ruff section commit d90c48a0bf98befdfacbbb093ee56cdb28afb40d Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:24 2024 +0100 move djlint config to pyproject commit c5ce55d5119bf2e35e429986f62f875c86178ae1 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:20:39 2024 +0100 added isort again commit 42a41d23afc280d4ee6f0e640148abc6f460f05a Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:19:02 2024 +0100 move config section commit 85692331816348cb1145570340d1f6488a8265cc Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:17:52 2024 +0100 fix codespell error commit 2897c6704d1311a800ce5aa47878d96d6980b377 Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 17:29:21 2024 +0100 replaced flake8 with ruff mostly for speed improvements * fix pyproject * make docstrings more uniform * auto-format * fix order * revert url change
377 lines
13 KiB
Python
377 lines
13 KiB
Python
"""Unit tests for the SalesOrder models."""
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.auth.models import Group
|
|
from django.core.exceptions import ValidationError
|
|
from django.test import TestCase
|
|
|
|
import order.tasks
|
|
from common.models import InvenTreeSetting, NotificationMessage
|
|
from company.models import Company
|
|
from InvenTree import status_codes as status
|
|
from order.models import (
|
|
SalesOrder,
|
|
SalesOrderAllocation,
|
|
SalesOrderExtraLine,
|
|
SalesOrderLineItem,
|
|
SalesOrderShipment,
|
|
)
|
|
from part.models import Part
|
|
from stock.models import StockItem
|
|
from users.models import Owner
|
|
|
|
|
|
class SalesOrderTest(TestCase):
|
|
"""Run tests to ensure that the SalesOrder model is working correctly."""
|
|
|
|
fixtures = ['users']
|
|
|
|
@classmethod
|
|
def setUpTestData(cls):
|
|
"""Initial setup for this set of unit tests."""
|
|
# Create a Company to ship the goods to
|
|
cls.customer = Company.objects.create(
|
|
name='ABC Co', description='My customer', is_customer=True
|
|
)
|
|
|
|
# Create a Part to ship
|
|
cls.part = Part.objects.create(
|
|
name='Spanner',
|
|
salable=True,
|
|
description='A spanner that I sell',
|
|
is_template=True,
|
|
)
|
|
cls.variant = Part.objects.create(
|
|
name='Blue Spanner',
|
|
salable=True,
|
|
description='A blue spanner that I sell',
|
|
variant_of=cls.part,
|
|
)
|
|
|
|
# Create some stock!
|
|
cls.Sa = StockItem.objects.create(part=cls.part, quantity=100)
|
|
cls.Sb = StockItem.objects.create(part=cls.part, quantity=200)
|
|
cls.Sc = StockItem.objects.create(part=cls.variant, quantity=100)
|
|
|
|
# Create a SalesOrder to ship against
|
|
cls.order = SalesOrder.objects.create(
|
|
customer=cls.customer, reference='SO-1234', customer_reference='ABC 55555'
|
|
)
|
|
|
|
# Create a Shipment against this SalesOrder
|
|
cls.shipment = SalesOrderShipment.objects.create(
|
|
order=cls.order, reference='SO-001'
|
|
)
|
|
|
|
# Create a line item
|
|
cls.line = SalesOrderLineItem.objects.create(
|
|
quantity=50, order=cls.order, part=cls.part
|
|
)
|
|
|
|
# Create an extra line
|
|
cls.extraline = SalesOrderExtraLine.objects.create(
|
|
quantity=1, order=cls.order, reference='Extra line'
|
|
)
|
|
|
|
def test_so_reference(self):
|
|
"""Unit tests for sales order generation."""
|
|
# Test that a good reference is created when we have no existing orders
|
|
SalesOrder.objects.all().delete()
|
|
|
|
self.assertEqual(SalesOrder.generate_reference(), 'SO-0001')
|
|
|
|
def test_rebuild_reference(self):
|
|
"""Test that the 'reference_int' field gets rebuilt when the model is saved."""
|
|
self.assertEqual(self.order.reference_int, 1234)
|
|
|
|
self.order.reference = '999'
|
|
self.order.save()
|
|
self.assertEqual(self.order.reference_int, 999)
|
|
|
|
self.order.reference = '1000K'
|
|
self.order.save()
|
|
self.assertEqual(self.order.reference_int, 1000)
|
|
|
|
def test_overdue(self):
|
|
"""Tests for overdue functionality."""
|
|
today = datetime.now().date()
|
|
|
|
# By default, order is *not* overdue as the target date is not set
|
|
self.assertFalse(self.order.is_overdue)
|
|
|
|
# Set target date in the past
|
|
self.order.target_date = today - timedelta(days=5)
|
|
self.order.save()
|
|
self.assertTrue(self.order.is_overdue)
|
|
|
|
# Set target date in the future
|
|
self.order.target_date = today + timedelta(days=5)
|
|
self.order.save()
|
|
self.assertFalse(self.order.is_overdue)
|
|
|
|
def test_empty_order(self):
|
|
"""Test for an empty order."""
|
|
self.assertEqual(self.line.quantity, 50)
|
|
self.assertEqual(self.line.allocated_quantity(), 0)
|
|
self.assertEqual(self.line.fulfilled_quantity(), 0)
|
|
self.assertFalse(self.line.is_fully_allocated())
|
|
self.assertFalse(self.line.is_overallocated())
|
|
|
|
self.assertTrue(self.order.is_pending)
|
|
self.assertFalse(self.order.is_fully_allocated())
|
|
|
|
def test_add_duplicate_line_item(self):
|
|
"""Adding a duplicate line item to a SalesOrder is accepted."""
|
|
for ii in range(1, 5):
|
|
SalesOrderLineItem.objects.create(
|
|
order=self.order, part=self.part, quantity=ii
|
|
)
|
|
|
|
def allocate_stock(self, full=True):
|
|
"""Allocate stock to the order."""
|
|
SalesOrderAllocation.objects.create(
|
|
line=self.line,
|
|
shipment=self.shipment,
|
|
item=StockItem.objects.get(pk=self.Sa.pk),
|
|
quantity=25,
|
|
)
|
|
|
|
SalesOrderAllocation.objects.create(
|
|
line=self.line,
|
|
shipment=self.shipment,
|
|
item=StockItem.objects.get(pk=self.Sb.pk),
|
|
quantity=25 if full else 20,
|
|
)
|
|
|
|
def test_over_allocate(self):
|
|
"""Test that over allocation logic works."""
|
|
SA = StockItem.objects.create(part=self.part, quantity=9)
|
|
|
|
# First three allocations should succeed
|
|
for _i in range(3):
|
|
allocation = SalesOrderAllocation.objects.create(
|
|
line=self.line, item=SA, quantity=3, shipment=self.shipment
|
|
)
|
|
|
|
# Editing an existing allocation with a larger quantity should fail
|
|
with self.assertRaises(ValidationError):
|
|
allocation.quantity = 4
|
|
allocation.save()
|
|
allocation.clean()
|
|
|
|
# Next allocation should fail
|
|
with self.assertRaises(ValidationError):
|
|
allocation = SalesOrderAllocation.objects.create(
|
|
line=self.line, item=SA, quantity=3, shipment=self.shipment
|
|
)
|
|
|
|
allocation.clean()
|
|
|
|
def test_allocate_partial(self):
|
|
"""Partially allocate stock."""
|
|
self.allocate_stock(False)
|
|
|
|
self.assertFalse(self.order.is_fully_allocated())
|
|
self.assertFalse(self.line.is_fully_allocated())
|
|
self.assertEqual(self.line.allocated_quantity(), 45)
|
|
self.assertEqual(self.line.fulfilled_quantity(), 0)
|
|
|
|
def test_allocate_full(self):
|
|
"""Fully allocate stock."""
|
|
self.allocate_stock(True)
|
|
|
|
self.assertTrue(self.order.is_fully_allocated())
|
|
self.assertTrue(self.line.is_fully_allocated())
|
|
self.assertEqual(self.line.allocated_quantity(), 50)
|
|
|
|
def test_allocate_variant(self):
|
|
"""Allocate a variant of the designated item."""
|
|
SalesOrderAllocation.objects.create(
|
|
line=self.line,
|
|
shipment=self.shipment,
|
|
item=StockItem.objects.get(pk=self.Sc.pk),
|
|
quantity=50,
|
|
)
|
|
self.assertEqual(self.line.allocated_quantity(), 50)
|
|
|
|
def test_order_cancel(self):
|
|
"""Allocate line items then cancel the order."""
|
|
self.allocate_stock(True)
|
|
|
|
self.assertEqual(SalesOrderAllocation.objects.count(), 2)
|
|
self.assertEqual(self.order.status, status.SalesOrderStatus.PENDING)
|
|
|
|
self.order.cancel_order()
|
|
self.assertEqual(SalesOrderAllocation.objects.count(), 0)
|
|
self.assertEqual(self.order.status, status.SalesOrderStatus.CANCELLED)
|
|
|
|
with self.assertRaises(ValidationError):
|
|
self.order.can_complete(raise_error=True)
|
|
|
|
# Now try to ship it - should fail
|
|
result = self.order.complete_order(None)
|
|
self.assertFalse(result)
|
|
|
|
def test_complete_order(self):
|
|
"""Allocate line items, then ship the order."""
|
|
# Assert some stuff before we run the test
|
|
# Initially there are three stock items
|
|
self.assertEqual(StockItem.objects.count(), 3)
|
|
|
|
# Take 25 units from each StockItem
|
|
self.allocate_stock(True)
|
|
|
|
self.assertEqual(SalesOrderAllocation.objects.count(), 2)
|
|
|
|
# Attempt to complete the order (but shipments are not completed!)
|
|
result = self.order.complete_order(None)
|
|
|
|
self.assertFalse(result)
|
|
|
|
self.assertIsNone(self.shipment.shipment_date)
|
|
self.assertFalse(self.shipment.is_complete())
|
|
|
|
# Mark the shipments as complete
|
|
self.shipment.complete_shipment(None)
|
|
self.assertTrue(self.shipment.is_complete())
|
|
|
|
# Now, should be OK to ship
|
|
result = self.order.complete_order(None)
|
|
|
|
self.assertTrue(result)
|
|
|
|
self.assertEqual(self.order.status, status.SalesOrderStatus.SHIPPED)
|
|
self.assertIsNotNone(self.order.shipment_date)
|
|
|
|
# There should now be 5 stock items
|
|
self.assertEqual(StockItem.objects.count(), 5)
|
|
|
|
sa = StockItem.objects.get(pk=self.Sa.pk)
|
|
sb = StockItem.objects.get(pk=self.Sb.pk)
|
|
sc = StockItem.objects.get(pk=self.Sc.pk)
|
|
|
|
# 25 units subtracted from each of the original non-variant items
|
|
self.assertEqual(sa.quantity, 75)
|
|
self.assertEqual(sb.quantity, 175)
|
|
self.assertEqual(sc.quantity, 100)
|
|
|
|
# And 2 items created which are associated with the order
|
|
outputs = StockItem.objects.filter(sales_order=self.order)
|
|
self.assertEqual(outputs.count(), 2)
|
|
|
|
for item in outputs.all():
|
|
self.assertEqual(item.quantity, 25)
|
|
|
|
self.assertEqual(sa.sales_order, None)
|
|
self.assertEqual(sb.sales_order, None)
|
|
|
|
# And the allocations still exist
|
|
self.assertEqual(SalesOrderAllocation.objects.count(), 2)
|
|
|
|
self.assertEqual(self.order.status, status.SalesOrderStatus.SHIPPED)
|
|
|
|
self.assertTrue(self.order.is_fully_allocated())
|
|
self.assertTrue(self.line.is_fully_allocated())
|
|
self.assertEqual(self.line.fulfilled_quantity(), 50)
|
|
self.assertEqual(self.line.allocated_quantity(), 50)
|
|
|
|
def test_default_shipment(self):
|
|
"""Test sales order default shipment creation."""
|
|
# Default setting value should be False
|
|
self.assertEqual(
|
|
False, InvenTreeSetting.get_setting('SALESORDER_DEFAULT_SHIPMENT')
|
|
)
|
|
|
|
# Create an order
|
|
order_1 = SalesOrder.objects.create(
|
|
customer=self.customer, reference='1235', customer_reference='ABC 55556'
|
|
)
|
|
|
|
# Order should have no shipments when setting is False
|
|
self.assertEqual(0, order_1.shipment_count)
|
|
|
|
# Update setting to True
|
|
InvenTreeSetting.set_setting('SALESORDER_DEFAULT_SHIPMENT', True, None)
|
|
self.assertEqual(
|
|
True, InvenTreeSetting.get_setting('SALESORDER_DEFAULT_SHIPMENT')
|
|
)
|
|
|
|
# Create a second order
|
|
order_2 = SalesOrder.objects.create(
|
|
customer=self.customer, reference='1236', customer_reference='ABC 55557'
|
|
)
|
|
|
|
# Order should have one shipment
|
|
self.assertEqual(1, order_2.shipment_count)
|
|
self.assertEqual(1, order_2.pending_shipments().count())
|
|
|
|
# Shipment should have default reference of '1'
|
|
self.assertEqual('1', order_2.pending_shipments()[0].reference)
|
|
|
|
def test_shipment_delivery(self):
|
|
"""Test the shipment delivery settings."""
|
|
# Shipment delivery date should be empty before setting date
|
|
self.assertIsNone(self.shipment.delivery_date)
|
|
self.assertFalse(self.shipment.is_delivered())
|
|
|
|
def test_overdue_notification(self):
|
|
"""Test overdue sales order notification."""
|
|
self.order.created_by = get_user_model().objects.get(pk=3)
|
|
self.order.responsible = Owner.create(obj=Group.objects.get(pk=2))
|
|
self.order.target_date = datetime.now().date() - timedelta(days=1)
|
|
self.order.save()
|
|
|
|
# Check for overdue sales orders
|
|
order.tasks.check_overdue_sales_orders()
|
|
|
|
messages = NotificationMessage.objects.filter(
|
|
category='order.overdue_sales_order'
|
|
)
|
|
|
|
self.assertEqual(len(messages), 1)
|
|
|
|
def test_new_so_notification(self):
|
|
"""Test that a notification is sent when a new SalesOrder is created.
|
|
|
|
- The responsible user should receive a notification
|
|
- The creating user should *not* receive a notification
|
|
"""
|
|
SalesOrder.objects.create(
|
|
customer=self.customer,
|
|
reference='1234567',
|
|
created_by=get_user_model().objects.get(pk=3),
|
|
responsible=Owner.create(obj=Group.objects.get(pk=3)),
|
|
)
|
|
|
|
messages = NotificationMessage.objects.filter(category='order.new_salesorder')
|
|
|
|
# A notification should have been generated for user 4 (who is a member of group 3)
|
|
self.assertTrue(messages.filter(user__pk=4).exists())
|
|
|
|
# However *no* notification should have been generated for the creating user
|
|
self.assertFalse(messages.filter(user__pk=3).exists())
|
|
|
|
def test_metadata(self):
|
|
"""Unit tests for the metadata field."""
|
|
for model in [
|
|
SalesOrder,
|
|
SalesOrderLineItem,
|
|
SalesOrderExtraLine,
|
|
SalesOrderShipment,
|
|
]:
|
|
p = model.objects.first()
|
|
|
|
self.assertIsNone(p.get_metadata('test'))
|
|
self.assertEqual(p.get_metadata('test', backup_value=123), 123)
|
|
|
|
# Test update via the set_metadata() method
|
|
p.set_metadata('test', 3)
|
|
self.assertEqual(p.get_metadata('test'), 3)
|
|
|
|
for k in ['apple', 'banana', 'carrot', 'carrot', 'banana']:
|
|
p.set_metadata(k, k)
|
|
|
|
self.assertEqual(len(p.metadata.keys()), 4)
|