mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05:42 +00:00 
			
		
		
		
	Adds ReturnOrder and ReturnOrderAttachment models
This commit is contained in:
		| @@ -320,3 +320,23 @@ class BuildStatus(StatusCode): | |||||||
|         PENDING, |         PENDING, | ||||||
|         PRODUCTION, |         PRODUCTION, | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ReturnOrderStatus(StatusCode): | ||||||
|  |     """Defines a set of status codes for a ReturnOrder""" | ||||||
|  |  | ||||||
|  |     PENDING = 10 | ||||||
|  |     COMPLETE = 30 | ||||||
|  |     CANCELLED = 40 | ||||||
|  |  | ||||||
|  |     options = { | ||||||
|  |         PENDING: _("Pending"), | ||||||
|  |         COMPLETE: _("Complete"), | ||||||
|  |         CANCELLED: _("Cancelled"), | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     colors = { | ||||||
|  |         PENDING: 'secondary', | ||||||
|  |         COMPLETE: 'success', | ||||||
|  |         CANCELLED: 'danger', | ||||||
|  |     } | ||||||
|   | |||||||
| @@ -1423,6 +1423,13 @@ class InvenTreeSetting(BaseInvenTreeSetting): | |||||||
|             'validator': build.validators.validate_build_order_reference_pattern, |             'validator': build.validators.validate_build_order_reference_pattern, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|  |         'RETURNORDER_REFERENCE_PATTERN': { | ||||||
|  |             'name': _('Return Order Reference Pattern'), | ||||||
|  |             'description': _('Required pattern for generating Return Order reference field'), | ||||||
|  |             'default': 'RMA-{ref:04d}', | ||||||
|  |             'validator': order.validators.validate_return_order_reference_pattern, | ||||||
|  |         }, | ||||||
|  |  | ||||||
|         'SALESORDER_REFERENCE_PATTERN': { |         'SALESORDER_REFERENCE_PATTERN': { | ||||||
|             'name': _('Sales Order Reference Pattern'), |             'name': _('Sales Order Reference Pattern'), | ||||||
|             'description': _('Required pattern for generating Sales Order reference field'), |             'description': _('Required pattern for generating Sales Order reference field'), | ||||||
|   | |||||||
| @@ -6,13 +6,9 @@ import import_export.widgets as widgets | |||||||
| from import_export.admin import ImportExportModelAdmin | from import_export.admin import ImportExportModelAdmin | ||||||
| from import_export.fields import Field | from import_export.fields import Field | ||||||
|  |  | ||||||
|  | import order.models as models | ||||||
| from InvenTree.admin import InvenTreeResource | from InvenTree.admin import InvenTreeResource | ||||||
|  |  | ||||||
| from .models import (PurchaseOrder, PurchaseOrderExtraLine, |  | ||||||
|                      PurchaseOrderLineItem, SalesOrder, SalesOrderAllocation, |  | ||||||
|                      SalesOrderExtraLine, SalesOrderLineItem, |  | ||||||
|                      SalesOrderShipment) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # region general classes | # region general classes | ||||||
| class GeneralExtraLineAdmin: | class GeneralExtraLineAdmin: | ||||||
| @@ -42,7 +38,7 @@ class GeneralExtraLineMeta: | |||||||
|  |  | ||||||
| class PurchaseOrderLineItemInlineAdmin(admin.StackedInline): | class PurchaseOrderLineItemInlineAdmin(admin.StackedInline): | ||||||
|     """Inline admin class for the PurchaseOrderLineItem model""" |     """Inline admin class for the PurchaseOrderLineItem model""" | ||||||
|     model = PurchaseOrderLineItem |     model = models.PurchaseOrderLineItem | ||||||
|     extra = 0 |     extra = 0 | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -103,7 +99,7 @@ class PurchaseOrderResource(InvenTreeResource): | |||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         """Metaclass""" |         """Metaclass""" | ||||||
|         model = PurchaseOrder |         model = models.PurchaseOrder | ||||||
|         skip_unchanged = True |         skip_unchanged = True | ||||||
|         clean_model_instances = True |         clean_model_instances = True | ||||||
|         exclude = [ |         exclude = [ | ||||||
| @@ -122,7 +118,7 @@ class PurchaseOrderLineItemResource(InvenTreeResource): | |||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         """Metaclass""" |         """Metaclass""" | ||||||
|         model = PurchaseOrderLineItem |         model = models.PurchaseOrderLineItem | ||||||
|         skip_unchanged = True |         skip_unchanged = True | ||||||
|         report_skipped = False |         report_skipped = False | ||||||
|         clean_model_instances = True |         clean_model_instances = True | ||||||
| @@ -142,7 +138,7 @@ class PurchaseOrderExtraLineResource(InvenTreeResource): | |||||||
|     class Meta(GeneralExtraLineMeta): |     class Meta(GeneralExtraLineMeta): | ||||||
|         """Metaclass options.""" |         """Metaclass options.""" | ||||||
|  |  | ||||||
|         model = PurchaseOrderExtraLine |         model = models.PurchaseOrderExtraLine | ||||||
|  |  | ||||||
|  |  | ||||||
| class SalesOrderResource(InvenTreeResource): | class SalesOrderResource(InvenTreeResource): | ||||||
| @@ -150,7 +146,7 @@ class SalesOrderResource(InvenTreeResource): | |||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         """Metaclass options""" |         """Metaclass options""" | ||||||
|         model = SalesOrder |         model = models.SalesOrder | ||||||
|         skip_unchanged = True |         skip_unchanged = True | ||||||
|         clean_model_instances = True |         clean_model_instances = True | ||||||
|         exclude = [ |         exclude = [ | ||||||
| @@ -169,7 +165,7 @@ class SalesOrderLineItemResource(InvenTreeResource): | |||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         """Metaclass options""" |         """Metaclass options""" | ||||||
|         model = SalesOrderLineItem |         model = models.SalesOrderLineItem | ||||||
|         skip_unchanged = True |         skip_unchanged = True | ||||||
|         report_skipped = False |         report_skipped = False | ||||||
|         clean_model_instances = True |         clean_model_instances = True | ||||||
| @@ -199,7 +195,7 @@ class SalesOrderExtraLineResource(InvenTreeResource): | |||||||
|     class Meta(GeneralExtraLineMeta): |     class Meta(GeneralExtraLineMeta): | ||||||
|         """Metaclass options.""" |         """Metaclass options.""" | ||||||
|  |  | ||||||
|         model = SalesOrderExtraLine |         model = models.SalesOrderExtraLine | ||||||
|  |  | ||||||
|  |  | ||||||
| class PurchaseOrderLineItemAdmin(ImportExportModelAdmin): | class PurchaseOrderLineItemAdmin(ImportExportModelAdmin): | ||||||
| @@ -281,13 +277,41 @@ class SalesOrderAllocationAdmin(ImportExportModelAdmin): | |||||||
|     autocomplete_fields = ('line', 'shipment', 'item',) |     autocomplete_fields = ('line', 'shipment', 'item',) | ||||||
|  |  | ||||||
|  |  | ||||||
| admin.site.register(PurchaseOrder, PurchaseOrderAdmin) | class ReturnOrderAdmin(ImportExportModelAdmin): | ||||||
| admin.site.register(PurchaseOrderLineItem, PurchaseOrderLineItemAdmin) |     """Admin class for the ReturnOrder model""" | ||||||
| admin.site.register(PurchaseOrderExtraLine, PurchaseOrderExtraLineAdmin) |  | ||||||
|  |  | ||||||
| admin.site.register(SalesOrder, SalesOrderAdmin) |     exclude = [ | ||||||
| admin.site.register(SalesOrderLineItem, SalesOrderLineItemAdmin) |         'reference_int', | ||||||
| admin.site.register(SalesOrderExtraLine, SalesOrderExtraLineAdmin) |     ] | ||||||
|  |  | ||||||
| admin.site.register(SalesOrderShipment, SalesOrderShipmentAdmin) |     list_display = [ | ||||||
| admin.site.register(SalesOrderAllocation, SalesOrderAllocationAdmin) |         'reference', | ||||||
|  |         'customer', | ||||||
|  |         'status', | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     search_fields = [ | ||||||
|  |         'reference', | ||||||
|  |         'customer__name', | ||||||
|  |         'description', | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     autocomplete_fields = [ | ||||||
|  |         'customer', | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Purchase Order models | ||||||
|  | admin.site.register(models.PurchaseOrder, PurchaseOrderAdmin) | ||||||
|  | admin.site.register(models.PurchaseOrderLineItem, PurchaseOrderLineItemAdmin) | ||||||
|  | admin.site.register(models.PurchaseOrderExtraLine, PurchaseOrderExtraLineAdmin) | ||||||
|  |  | ||||||
|  | # Sales Order models | ||||||
|  | admin.site.register(models.SalesOrder, SalesOrderAdmin) | ||||||
|  | admin.site.register(models.SalesOrderLineItem, SalesOrderLineItemAdmin) | ||||||
|  | admin.site.register(models.SalesOrderExtraLine, SalesOrderExtraLineAdmin) | ||||||
|  | admin.site.register(models.SalesOrderShipment, SalesOrderShipmentAdmin) | ||||||
|  | admin.site.register(models.SalesOrderAllocation, SalesOrderAllocationAdmin) | ||||||
|  |  | ||||||
|  | # Return Order models | ||||||
|  | admin.site.register(models.ReturnOrder, ReturnOrderAdmin) | ||||||
|   | |||||||
| @@ -34,8 +34,9 @@ from InvenTree.fields import (InvenTreeModelMoneyField, InvenTreeNotesField, | |||||||
|                               InvenTreeURLField, RoundingDecimalField) |                               InvenTreeURLField, RoundingDecimalField) | ||||||
| from InvenTree.helpers import decimal2string, getSetting, notify_responsible | from InvenTree.helpers import decimal2string, getSetting, notify_responsible | ||||||
| from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin | from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin | ||||||
| from InvenTree.status_codes import (PurchaseOrderStatus, SalesOrderStatus, | from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderStatus, | ||||||
|                                     StockHistoryCode, StockStatus) |                                     SalesOrderStatus, StockHistoryCode, | ||||||
|  |                                     StockStatus) | ||||||
| from part import models as PartModels | from part import models as PartModels | ||||||
| from plugin.events import trigger_event | from plugin.events import trigger_event | ||||||
| from plugin.models import MetadataMixin | from plugin.models import MetadataMixin | ||||||
| @@ -199,7 +200,7 @@ class PurchaseOrder(Order): | |||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def api_defaults(cls, request): |     def api_defaults(cls, request): | ||||||
|         """Return default values for thsi model when issuing an API OPTIONS request""" |         """Return default values for this model when issuing an API OPTIONS request""" | ||||||
|  |  | ||||||
|         defaults = { |         defaults = { | ||||||
|             'reference': order.validators.generate_next_purchase_order_reference(), |             'reference': order.validators.generate_next_purchase_order_reference(), | ||||||
| @@ -684,13 +685,16 @@ class SalesOrder(Order): | |||||||
|         on_delete=models.SET_NULL, |         on_delete=models.SET_NULL, | ||||||
|         null=True, |         null=True, | ||||||
|         limit_choices_to={'is_customer': True}, |         limit_choices_to={'is_customer': True}, | ||||||
|         related_name='sales_orders', |         related_name='return_orders', | ||||||
|         verbose_name=_('Customer'), |         verbose_name=_('Customer'), | ||||||
|         help_text=_("Company to which the items are being sold"), |         help_text=_("Company to which the items are being sold"), | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     status = models.PositiveIntegerField(default=SalesOrderStatus.PENDING, choices=SalesOrderStatus.items(), |     status = models.PositiveIntegerField( | ||||||
|                                          verbose_name=_('Status'), help_text=_('Purchase order status')) |         default=SalesOrderStatus.PENDING, | ||||||
|  |         choices=SalesOrderStatus.items(), | ||||||
|  |         verbose_name=_('Status'), help_text=_('Purchase order status') | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def status_text(self): |     def status_text(self): | ||||||
| @@ -1547,3 +1551,100 @@ class SalesOrderAllocation(models.Model): | |||||||
|         # (It may have changed if the stock was split) |         # (It may have changed if the stock was split) | ||||||
|         self.item = item |         self.item = item | ||||||
|         self.save() |         self.save() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ReturnOrder(Order): | ||||||
|  |     """A ReturnOrder represents goods returned from a customer, e.g. an RMA or warranty | ||||||
|  |  | ||||||
|  |     Attributes: | ||||||
|  |         customer: Reference to the customer | ||||||
|  |         sales_order: Reference to an existing SalesOrder (optional) | ||||||
|  |         status: The status of the order (refer to status_codes.ReturnOrderStatus) | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_api_url(): | ||||||
|  |         """Return the API URL associated with the ReturnOrder model""" | ||||||
|  |         return reverse('api-return-list') | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def api_defaults(cls, request): | ||||||
|  |         """Return default values for this model when issuing an API OPTIONS request""" | ||||||
|  |         defaults = { | ||||||
|  |             'reference': order.validators.generate_next_return_order_reference(), | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return defaults | ||||||
|  |  | ||||||
|  |     REFERENCE_PATTERN_SETTING = 'RETURNORDER_REFERENCE_PATTERN' | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         """Render a string representation of this ReturnOrder""" | ||||||
|  |  | ||||||
|  |         return f"{self.reference} - {self.customer.name if self.customer else _('no customer')}" | ||||||
|  |  | ||||||
|  |     reference = models.CharField( | ||||||
|  |         unique=True, | ||||||
|  |         max_length=64, | ||||||
|  |         blank=False, | ||||||
|  |         verbose_name=_('Reference'), | ||||||
|  |         help_text=_('Return Order reference'), | ||||||
|  |         default=order.validators.generate_next_return_order_reference, | ||||||
|  |         validators=[ | ||||||
|  |             order.validators.validate_return_order_reference, | ||||||
|  |         ] | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     customer = models.ForeignKey( | ||||||
|  |         Company, | ||||||
|  |         on_delete=models.SET_NULL, | ||||||
|  |         null=True, | ||||||
|  |         limit_choices_to={'is_customer': True}, | ||||||
|  |         related_name='sales_orders', | ||||||
|  |         verbose_name=_('Customer'), | ||||||
|  |         help_text=_("Company from which items are being returned"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     status = models.PositiveIntegerField( | ||||||
|  |         default=ReturnOrderStatus.PENDING, | ||||||
|  |         choices=ReturnOrderStatus.items(), | ||||||
|  |         verbose_name=_('Status'), help_text=_('Return order status') | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     customer_reference = models.CharField( | ||||||
|  |         max_length=64, blank=True, | ||||||
|  |         verbose_name=_('Customer Reference '), | ||||||
|  |         help_text=_("Customer order reference code") | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     issue_date = models.DateField( | ||||||
|  |         blank=True, null=True, | ||||||
|  |         verbose_name=_('Issue Date'), | ||||||
|  |         help_text=_('Date order was issued') | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     complete_date = models.DateField( | ||||||
|  |         blank=True, null=True, | ||||||
|  |         verbose_name=_('Completion Date'), | ||||||
|  |         help_text=_('Date order was completed') | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ReturnOrderAttachment(InvenTreeAttachment): | ||||||
|  |     """Model for storing file attachments against a ReturnOrder object""" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_api_url(): | ||||||
|  |         """Return the API URL associated with the ReturnOrderAttachment class""" | ||||||
|  |  | ||||||
|  |         return reverse('api-return-attachment-list') | ||||||
|  |  | ||||||
|  |     def getSubdir(self): | ||||||
|  |         """Return the directory path where ReturnOrderAttachment files are located""" | ||||||
|  |         return os.path.join('return_files', str(self.order.id)) | ||||||
|  |  | ||||||
|  |     order = models.ForeignKey( | ||||||
|  |         ReturnOrder, | ||||||
|  |         on_delete=models.CASCADE, | ||||||
|  |         related_name='attachments', | ||||||
|  |     ) | ||||||
|   | |||||||
| @@ -17,6 +17,14 @@ def generate_next_purchase_order_reference(): | |||||||
|     return PurchaseOrder.generate_reference() |     return PurchaseOrder.generate_reference() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def generate_next_return_order_reference(): | ||||||
|  |     """Generate the next available ReturnOrder reference""" | ||||||
|  |  | ||||||
|  |     from order.models import ReturnOrder | ||||||
|  |  | ||||||
|  |     return ReturnOrder.generate_reference() | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_sales_order_reference_pattern(pattern): | def validate_sales_order_reference_pattern(pattern): | ||||||
|     """Validate the SalesOrder reference 'pattern' setting""" |     """Validate the SalesOrder reference 'pattern' setting""" | ||||||
|  |  | ||||||
| @@ -33,6 +41,14 @@ def validate_purchase_order_reference_pattern(pattern): | |||||||
|     PurchaseOrder.validate_reference_pattern(pattern) |     PurchaseOrder.validate_reference_pattern(pattern) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_return_order_reference_pattern(pattern): | ||||||
|  |     """Validate the ReturnOrder reference 'pattern' setting""" | ||||||
|  |  | ||||||
|  |     from order.models import ReturnOrder | ||||||
|  |  | ||||||
|  |     ReturnOrder.validate_reference_pattern(pattern) | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_sales_order_reference(value): | def validate_sales_order_reference(value): | ||||||
|     """Validate that the SalesOrder reference field matches the required pattern""" |     """Validate that the SalesOrder reference field matches the required pattern""" | ||||||
|  |  | ||||||
| @@ -47,3 +63,11 @@ def validate_purchase_order_reference(value): | |||||||
|     from order.models import PurchaseOrder |     from order.models import PurchaseOrder | ||||||
|  |  | ||||||
|     PurchaseOrder.validate_reference_field(value) |     PurchaseOrder.validate_reference_field(value) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_return_order_reference(value): | ||||||
|  |     """Validate that the ReturnOrder reference field matches the required pattern""" | ||||||
|  |  | ||||||
|  |     from order.models import ReturnOrder | ||||||
|  |  | ||||||
|  |     ReturnOrder.validate_reference_field(value) | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								InvenTree/templates/InvenTree/settings/returns.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								InvenTree/templates/InvenTree/settings/returns.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | {% extends "panel.html" %} | ||||||
|  | {% load i18n %} | ||||||
|  |  | ||||||
|  | {% block label %}return-order{% endblock %} | ||||||
|  |  | ||||||
|  | {% block heading %} | ||||||
|  | {% trans "Return Order Settings" %} | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  |  | ||||||
|  | <table class='table table-striped table-condensed'> | ||||||
|  |     <tbody> | ||||||
|  |         {% include "InvenTree/settings/setting.html" with key="RETURNORDER_REFERENCE_PATTERN" %} | ||||||
|  |     </tbody> | ||||||
|  | </table> | ||||||
|  |  | ||||||
|  | {% endblock %} | ||||||
| @@ -42,6 +42,7 @@ | |||||||
| {% include "InvenTree/settings/build.html" %} | {% include "InvenTree/settings/build.html" %} | ||||||
| {% include "InvenTree/settings/po.html" %} | {% include "InvenTree/settings/po.html" %} | ||||||
| {% include "InvenTree/settings/so.html" %} | {% include "InvenTree/settings/so.html" %} | ||||||
|  | {% include "InvenTree/settings/returns.html" %} | ||||||
|  |  | ||||||
| {% include "InvenTree/settings/plugin.html" %} | {% include "InvenTree/settings/plugin.html" %} | ||||||
| {% plugin_list as pl_list %} | {% plugin_list as pl_list %} | ||||||
|   | |||||||
| @@ -52,6 +52,8 @@ | |||||||
| {% include "sidebar_item.html" with label='purchase-order' text=text icon="fa-shopping-cart" %} | {% include "sidebar_item.html" with label='purchase-order' text=text icon="fa-shopping-cart" %} | ||||||
| {% trans "Sales Orders" as text %} | {% trans "Sales Orders" as text %} | ||||||
| {% include "sidebar_item.html" with label='sales-order' text=text icon="fa-truck" %} | {% include "sidebar_item.html" with label='sales-order' text=text icon="fa-truck" %} | ||||||
|  | {% trans "Return Orders" as text %} | ||||||
|  | {% include "sidebar_item.html" with label='return-order' text=text icon="fa-undo" %} | ||||||
|  |  | ||||||
| {% trans "Plugin Settings" as text %} | {% trans "Plugin Settings" as text %} | ||||||
| {% include "sidebar_header.html" with text=text %} | {% include "sidebar_header.html" with text=text %} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user