models.py :  » ERP » frePPLe » frepple-0.8.0 » contrib » django » freppledb » input » Python Open Source

Home
Python Open Source
1.3.1.2 Python
2.Ajax
3.Aspect Oriented
4.Blog
5.Build
6.Business Application
7.Chart Report
8.Content Management Systems
9.Cryptographic
10.Database
11.Development
12.Editor
13.Email
14.ERP
15.Game 2D 3D
16.GIS
17.GUI
18.IDE
19.Installer
20.IRC
21.Issue Tracker
22.Language Interface
23.Log
24.Math
25.Media Sound Audio
26.Mobile
27.Network
28.Parser
29.PDF
30.Project Management
31.RSS
32.Search
33.Security
34.Template Engines
35.Test
36.UML
37.USB Serial
38.Web Frameworks
39.Web Server
40.Web Services
41.Web Unit
42.Wiki
43.Windows
44.XML
Python Open Source » ERP » frePPLe 
frePPLe » frepple 0.8.0 » contrib » django » freppledb » input » models.py
#
# Copyright (C) 2007 by Johan De Taeye
#
# This library is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
# General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#

# file : $URL: https://frepple.svn.sourceforge.net/svnroot/frepple/tags/0.8.0/contrib/django/freppledb/input/models.py $
# revision : $LastChangedRevision: 1168 $  $LastChangedBy: jdetaeye $
# date : $LastChangedDate: 2010-01-29 18:27:40 +0100 (Fri, 29 Jan 2010) $

from datetime import datetime,date
from decimal import Decimal

from django.db import models
from django.db.models import signals
from django.http import HttpRequest
from django.dispatch import dispatcher
from django.contrib.admin.models import LogEntry,CHANGE
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy
from django.conf import settings

from freppledb.common.fields import DurationField


CALENDARID = None


class AuditModel(models.Model):
  '''
  This is an abstract base model.
  It implements the capability to maintain the date of the last modification of the record.
  '''
  # Database fields
  lastmodified = models.DateTimeField(_('last modified'), editable=False, db_index=True, default=datetime.now())

  def save(self, *args, **kwargs):
    # Update the field with every change
    self.lastmodified = datetime.now()

    # Call the real save() method
    super(AuditModel, self).save(*args, **kwargs)

  class Meta:
    abstract = True


searchmode = (
  ('',_('priority')),
  ('PRIORITY',_('priority')),
  ('MINCOST',_('minimum cost')),
  ('MINPENALTY',_('minimum penalty')),
  ('MINCOSTPENALTY',_('minimum cost plus penalty'))
)


class Plan(AuditModel):
  # Database fields
  name = models.CharField(_('name'), max_length=settings.NAMESIZE, null=True, blank=True)
  description = models.CharField(_('description'), max_length=settings.DESCRIPTIONSIZE, null=True, blank=True)
  currentdate = models.DateTimeField(_('current date'))

  def __unicode__(self): return self.name

  class Meta(AuditModel.Meta):
    db_table = 'plan'
    verbose_name = _('plan')
    verbose_name_plural = _('plan') # There will only be 1 plan...


class Calendar(AuditModel):
  # Types of calendars
  calendartypes = (
    ('',_('double')),
    ('calendar_double',_('double')),
    ('calendar_boolean',_('boolean')),
  )

  # Database fields
  name = models.CharField(_('name'), max_length=settings.NAMESIZE, primary_key=True)
  type = models.CharField(_('type'), _('type'), max_length=20, 
    null=True, blank=True, choices=calendartypes,
    help_text= _('Type of data values stored in the calendar')
    )
  description = models.CharField(_('description'), max_length=settings.DESCRIPTIONSIZE, null=True, 
    blank=True)
  category = models.CharField(_('category'), max_length=settings.CATEGORYSIZE, null=True, 
    blank=True, db_index=True)
  subcategory = models.CharField(_('subcategory'), max_length=settings.CATEGORYSIZE, 
    null=True, blank=True, db_index=True)
  defaultvalue = models.DecimalField(_('default value'), max_digits=settings.MAX_DIGITS, 
    decimal_places=settings.DECIMAL_PLACES, default='0.00',
    help_text= _('Value to be used when no entry is effective')
    )

  def currentvalue(self):
    ''' Returns the value of the calendar on this moment.'''
    return self.getvalue(datetime.now())
  currentvalue.short_description = 'current value'

  def getvalue(self, when):
    '''Return the value of the calendar on a certain day.'''
    curValue = self.defaultvalue
    curPriority = None
    # Loop through the entries to find the effective one
    for b in self.buckets.all():
      if not curPriority or curPriority > b.priority:
        thisValue = b.getvalue(when)
        if thisValue:
          # The entry is valid value on this day, and has
          # a higher priority than other entries.
          curValue = thisValue
          curPriority = b.priority
    return curValue

  def setvalue(self, start, end, value, user=None):
    '''Update calendar buckets such that the calendar value is changed
    in the specified date range.
    The admin log is updated if a user is passed as argument.

    @todo The calendar editing isnt as flexible as the frePPLe core: the
    user interface only support non-overlapping calendar entries to keep SQL
    statements easy.
    '''
    # Create a change log entry, if a user is specified
    if user:
      global CALENDARID
      if not CALENDARID:
        CALENDARID = ContentType.objects.get_for_model(models.get_model('input','calendar')).id
      LogEntry.objects.log_action(
        user.id, CALENDARID, self.name, self.name, CHANGE,
        "Updated value to %s for the daterange %s to %s" % (value, start, end)
        )
    for b in self.buckets.filter(enddate__gt=start,startdate__lt=end).order_by('startdate'):
      if b.enddate <= start:
        # Earlier bucket
        continue
      elif b.startdate >= end:
        # Later bucket
        return
      elif b.startdate == start and b.enddate <= end:
        # Overwrite entire bucket
        b.value = str(value)
        b.save()
      elif b.startdate >= start and b.enddate <= end:
        # Bucket became redundant
        b.delete()
      elif b.startdate < start and b.enddate > end:
        # New value is completely within this bucket
        Bucket(calendar=self, startdate=start, value=str(value)).save()
        Bucket(calendar=self, startdate=end, value=str(b.value)).save()
      elif b.startdate < start:
        # An existing bucket is partially before the new daterange
        b.enddate = start
        b.save()
        Bucket(calendar=self, startdate=start, enddate=end, value=str(value)).save()
      elif b.enddate > end:
        # An existing bucket is partially after the new daterange
        Bucket(calendar=self, startdate=b.startdate, enddate=end, value=str(value)).save()
        b.startdate = end
        b.save()
    if self.buckets.count() == 0:
      # There wasn't any bucket yet...
      Bucket(calendar=self, startdate=start, value=str(value)).save()
      Bucket(calendar=self, startdate=end, value="0").save()

  def __unicode__(self): return self.name

  class Meta(AuditModel.Meta):
    db_table = 'calendar'
    verbose_name = _('calendar')
    verbose_name_plural = _('calendars')
    ordering = ['name']


class Bucket(AuditModel):
  '''
  @todo The calendar editing isnt as flexible as the frePPLe core: the
  user interface only support non-overlapping calendar entries to keep SQL
  statements easy.
  The core engine allows the end date to be edited independently.
  '''
  # Database fields
  calendar = models.ForeignKey(Calendar, verbose_name=_('calendar'), related_name='buckets')
  startdate = models.DateTimeField('start date', null=True, blank=True)
  enddate = models.DateTimeField('end date', editable=False, null=True, blank=True, default=datetime(2030,12,31))
  value = models.DecimalField(_('value'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, default='0.00', blank=True)
  priority = models.IntegerField(_('priority'), default=0, blank=True)
  name = models.CharField(_('name'), max_length=settings.NAMESIZE, null=True, blank=True)

  def getvalue(self, when):
    if (self.startdate and when < self.startdate) or (self.enddate and when >= self.enddate):
      # Outside of validity range
      return None
    return self.value

  def __unicode__(self):
    if self.name: return self.name
    return u"%s - %s" % (self.startdate, self.enddate)

  class Meta(AuditModel.Meta):
    ordering = ['startdate','name']
    db_table = 'bucket'
    verbose_name = _('calendar bucket')
    verbose_name_plural = _('calendar buckets')

  @staticmethod
  def updateEndDate(instance, **kwargs):
    '''
    The user edits the start date of the calendar buckets.
    This method will automatically update the end date of a bucket to be
    equal to the start date of the next bucket.

    @todo The calendar editing isnt as flexible as the frePPLe core: the
    user interface only support non-overlapping calendar entries to keep SQL
    statements easy.
    '''
    # Loop through all buckets
    prev = None
    for i in instance.calendar.buckets.all():
      if prev and i.startdate != prev.enddate:
        # Update the end date of the previous bucket to the start date of this one
        prev.enddate = i.startdate
        if prev.enddate == prev.startdate:
          prev.delete()
        else:
          prev.save()
      prev = i
    if prev and prev.enddate != datetime(2030,12,31):
      # Update the last entry
      prev.enddate = datetime(2030,12,31)
      prev.save()

  @staticmethod
  def insertBucket(instance, **kwargs):
    # If the end date is specified, we take it for granted.
    # Ideally we would check all inserts, but that is very time consuming
    # when creating or restoring big datasets.
    if instance.enddate == datetime(2030,12,31):
      Bucket.updateEndDate(instance)

# This dispatcher function is called after a bucket is saved. There seems no cleaner way to do this, since
# the method calendar.buckets.all() is only up to date after the save...
# The method is not very efficient: called for every single bucket, and recursively triggers
# another save and dispatcher event
signals.post_save.connect(Bucket.insertBucket, sender=Bucket)
signals.post_delete.connect(Bucket.updateEndDate, sender=Bucket)


class Location(AuditModel):
  # Database fields
  name = models.CharField(_('name'), max_length=settings.NAMESIZE, primary_key=True)
  description = models.CharField(_('description'), max_length=settings.DESCRIPTIONSIZE, null=True, blank=True)
  category = models.CharField(_('category'), max_length=settings.CATEGORYSIZE, null=True, blank=True, db_index=True)
  subcategory = models.CharField(_('subcategory'), max_length=settings.CATEGORYSIZE, null=True, blank=True, db_index=True)
  available = models.ForeignKey(Calendar, verbose_name=_('available'),
    null=True, blank=True,
    help_text=_('Calendar defining the working hours and holidays of this location'))
  owner = models.ForeignKey('self', verbose_name=_('owner'), null=True, blank=True, related_name='children',
    help_text=_('Hierarchical parent'))

  def __unicode__(self): return self.name

  class Meta(AuditModel.Meta):
    db_table = 'location'
    verbose_name = _('location')
    verbose_name_plural = _('locations')
    ordering = ['name']


class Customer(AuditModel):
  # Database fields
  name = models.CharField(_('name'), max_length=settings.NAMESIZE, primary_key=True)
  description = models.CharField(_('description'), max_length=settings.DESCRIPTIONSIZE, null=True, blank=True)
  category = models.CharField(_('category'), max_length=settings.CATEGORYSIZE, null=True, blank=True, db_index=True)
  subcategory = models.CharField(_('subcategory'), max_length=settings.CATEGORYSIZE, null=True, blank=True, db_index=True)
  owner = models.ForeignKey('self', verbose_name=_('owner'), null=True, blank=True, related_name='children',
    help_text=_('Hierarchical parent'))

  def __unicode__(self): return self.name

  class Meta(AuditModel.Meta):
    db_table = 'customer'
    verbose_name = _('customer')
    verbose_name_plural = _('customers')
    ordering = ['name']


class Item(AuditModel):
  # Database fields
  name = models.CharField(_('name'), max_length=settings.NAMESIZE, primary_key=True)
  description = models.CharField(_('description'), max_length=settings.DESCRIPTIONSIZE, null=True, blank=True)
  category = models.CharField(_('category'), max_length=settings.CATEGORYSIZE, null=True, blank=True, db_index=True)
  subcategory = models.CharField(_('subcategory'), max_length=settings.CATEGORYSIZE, null=True, blank=True, db_index=True)
  operation = models.ForeignKey('Operation', verbose_name=_('delivery operation'), null=True, blank=True,
    help_text=_("Default operation used to ship a demand for this item"))
  price = models.DecimalField(_('price'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_("Selling price of the item"))
  owner = models.ForeignKey('self', verbose_name=_('owner'), null=True, blank=True, related_name='children',
    help_text=_('Hierarchical parent'))

  def __unicode__(self): return self.name

  class Meta(AuditModel.Meta):
    db_table = 'item'
    verbose_name = _('item')
    verbose_name_plural = _('items')
    ordering = ['name']


class Operation(AuditModel):
  # Types of operations
  operationtypes = (
    ('',_('fixed_time')),
    ('operation_fixed_time',_('fixed_time')),
    ('operation_time_per',_('time_per')),
    ('operation_routing',_('routing')),
    ('operation_alternate',_('alternate')),
  )

  # Database fields
  name = models.CharField(_('name'), max_length=settings.NAMESIZE, primary_key=True)
  type = models.CharField(_('type'), _('type'), max_length=20, null=True, blank=True, choices=operationtypes)
  description = models.CharField(_('description'), max_length=settings.DESCRIPTIONSIZE, null=True, blank=True)
  category = models.CharField(_('category'), max_length=settings.CATEGORYSIZE, null=True, blank=True, db_index=True)
  subcategory = models.CharField(_('subcategory'), max_length=settings.CATEGORYSIZE, null=True, blank=True, db_index=True)
  location = models.ForeignKey(Location, verbose_name=_('location'), null=True,
    blank=True, db_index=True)
  fence = DurationField(_('release fence'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_("Operationplans within this time window from the current day are expected to be released to production ERP"))
  pretime = DurationField(_('pre-op time'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_("A delay time to be respected as a soft constraint before starting the operation"))
  posttime = DurationField(_('post-op time'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_("A delay time to be respected as a soft constraint after ending the operation"))
  sizeminimum = models.DecimalField(_('size minimum'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_("A minimum quantity for operationplans"))
  sizemultiple = models.DecimalField(_('size multiple'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_("A multiple quantity for operationplans"))
  sizemaximum = models.DecimalField(_('size maximum'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_("A maximum quantity for operationplans"))
  cost = models.DecimalField(_('cost'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_("Cost per operationplan unit"))
  duration = DurationField(_('duration'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_("A fixed duration for the operation"))
  duration_per = DurationField(_('duration per unit'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_("A variable duration for the operation"))
  search = models.CharField(_('search mode'), _('search mode'), max_length=20, 
    null=True, blank=True, choices=searchmode,
    help_text=_('Method to select prefered alternate')
    )

  def __unicode__(self): return self.name

  def save(self, *args, **kwargs):
    if self.type is None or self.type == '' or self.type == 'operation_fixed_time':
      self.duration_per = None
      self.search = None
    elif self.type == 'operation_alternate':
      self.duration = None
      self.duration_per = None      
    elif self.type != 'operation_time_per':
      self.duration = None
      self.duration_per = None
      self.search = None
      
    # Call the real save() method
    super(Operation, self).save(*args, **kwargs)

  class Meta(AuditModel.Meta):
    db_table = 'operation'
    verbose_name = _('operation')
    verbose_name_plural = _('operations')
    ordering = ['name']


class SubOperation(AuditModel):
  # Database fields
  operation = models.ForeignKey(Operation, verbose_name=_('operation'),
    related_name='suboperations')
  priority = models.IntegerField(_('priority'), default=1)
  suboperation = models.ForeignKey(Operation, verbose_name=_('suboperation'),
    related_name='superoperations')
  effective_start = models.DateTimeField(_('effective start'), null=True, blank=True)
  effective_end = models.DateTimeField(_('effective end'), null=True, blank=True)

  def __unicode__(self):
    return self.operation.name \
      + "   " + str(self.priority) \
      + "   " + self.suboperation.name

  class Meta(AuditModel.Meta):
    db_table = 'suboperation'
    ordering = ['operation','priority','suboperation']
    verbose_name = _('suboperation')
    verbose_name_plural = _('suboperations')


class Buffer(AuditModel):
  # Types of buffers
  buffertypes = (
    ('',_('Default')),
    ('buffer_infinite',_('Infinite')),
    ('buffer_procure',_('Procure')),
  )

  # Fields common to all buffer types
  name = models.CharField(_('name'), max_length=settings.NAMESIZE, primary_key=True)
  description = models.CharField(_('description'), max_length=settings.DESCRIPTIONSIZE, null=True, blank=True)
  category = models.CharField(_('category'), max_length=settings.CATEGORYSIZE, null=True, blank=True, db_index=True)
  subcategory = models.CharField(_('subcategory'), max_length=settings.CATEGORYSIZE, null=True, blank=True, db_index=True)
  type = models.CharField(_('type'), max_length=20, null=True, blank=True, choices=buffertypes, default='')
  location = models.ForeignKey(Location, verbose_name=_('location'), null=True,
    blank=True, db_index=True)
  item = models.ForeignKey(Item, verbose_name=_('item'), db_index=True, null=True)
  onhand = models.DecimalField(_('onhand'),max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, default="0.00", null=True, blank=True, help_text=_('current inventory'))
  minimum = models.ForeignKey(Calendar, verbose_name=_('minimum'),
    null=True, blank=True,
    help_text=_('Calendar storing the safety stock profile'))
  producing = models.ForeignKey(Operation, verbose_name=_('producing'),
    null=True, blank=True, related_name='used_producing',
    help_text=_('Operation to replenish the buffer'))
  carrying_cost = models.DecimalField(_('carrying cost'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_("Cost of holding inventory in this buffer, expressed as an annual percentage of the item price."))
  # Extra fields for procurement buffers
  leadtime = DurationField(_('leadtime'),max_digits=settings.MAX_DIGITS, decimal_places=0, null=True, blank=True,
    help_text=_('Leadtime for supplier of a procure buffer'))
  fence = DurationField(_('fence'),max_digits=settings.MAX_DIGITS, decimal_places=0, null=True, blank=True,
    help_text=_('Frozen fence for creating new procurements'))
  min_inventory = models.DecimalField(_('min_inventory'),max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_('Inventory level that triggers replenishment of a procure buffer'))
  max_inventory = models.DecimalField(_('max_inventory'),max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_('Inventory level to which a procure buffer is replenished'))
  min_interval = DurationField(_('min_interval'),max_digits=settings.MAX_DIGITS, decimal_places=0, null=True, blank=True,
    help_text=_('Minimum time interval between replenishments of a procure buffer'))
  max_interval = DurationField(_('max_interval'),max_digits=settings.MAX_DIGITS, decimal_places=0, null=True, blank=True,
    help_text=_('Maximum time interval between replenishments of a procure buffer'))
  size_minimum = models.DecimalField(_('size_minimum'),max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_('Minimum size of replenishments of a procure buffer'))
  size_multiple = models.DecimalField(_('size_multiple'),max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_('Replenishments of a procure buffer are a multiple of this quantity'))
  size_maximum =  models.DecimalField(_('size_maximum'),max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_('Maximum size of replenishments of a procure buffer'))

  def __unicode__(self): return self.name

  def save(self, *args, **kwargs):
    if self.type == 'buffer_infinite' or self.type == 'buffer_procure':
      # Handle irrelevant fields for infinite and procure buffers
      self.producing = None
    if self.type != 'buffer_procure':
      # Handle irrelevant fields for non-procure buffers
      self.leadtime = None
      self.fence = None
      self.min_inventory = None
      self.max_inventory = None
      self.min_interval = None
      self.max_interval = None
      self.size_minimum = None
      self.size_multiple = None
      self.size_maximum = None
    super(Buffer, self).save(*args, **kwargs)

  class Meta(AuditModel.Meta):
    db_table = 'buffer'
    verbose_name = _('buffer')
    verbose_name_plural = _('buffers')
    ordering = ['name']


class SetupMatrix(AuditModel):
  # Database fields
  name = models.CharField(_('name'), max_length=settings.NAMESIZE, primary_key=True)

  # Methods
  def __unicode__(self): return self.name

  class Meta(AuditModel.Meta):
    db_table = 'setupmatrix'
    verbose_name = _('setup matrix')
    verbose_name_plural = _('setup matrices')
    ordering = ['name']


class SetupRule(AuditModel):
  '''
  A rule that is part of a setup matrix.
  '''
  # Database fields
  setupmatrix = models.ForeignKey(SetupMatrix, verbose_name=_('setup matrix'), related_name='rules')
  priority = models.IntegerField(_('priority'))
  fromsetup = models.CharField(_('from setup'), max_length=settings.NAMESIZE, blank=True, null=True,
    help_text=_("Name of the old setup (wildcard characters are supported)"))
  tosetup = models.CharField(_('to setup'), max_length=settings.NAMESIZE, blank=True, null=True,
    help_text=_("Name of the new setup (wildcard characters are supported)"))
  duration = DurationField(_('duration'), max_digits=settings.MAX_DIGITS, decimal_places=0, null=True, blank=True,
    help_text=_("Duration of the changeover"))
  cost = models.DecimalField(_('cost'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_("Cost of the conversion"))

  def __unicode__(self):
    return u"%s - %s" % (self.setupmatrix.name, self.priority)

  class Meta(AuditModel.Meta):
    ordering = ['priority',]
    db_table = 'setuprule'
    unique_together = (('setupmatrix', 'priority'),)
    verbose_name = _('setup matrix rule')
    verbose_name_plural = _('setup matrix rules')
        

class Resource(AuditModel):
  # Types of resources
  resourcetypes = (
    ('',_('Default')),
    ('resource_infinite',_('Infinite')),
  )

  # Database fields
  name = models.CharField(_('name'), max_length=settings.NAMESIZE, primary_key=True)
  description = models.CharField(_('description'), max_length=settings.DESCRIPTIONSIZE, null=True, blank=True)
  category = models.CharField(_('category'), max_length=settings.CATEGORYSIZE, null=True, blank=True, db_index=True)
  subcategory = models.CharField(_('subcategory'), max_length=settings.CATEGORYSIZE, null=True, blank=True, db_index=True)
  type = models.CharField(_('type'), max_length=20, null=True, blank=True, choices=resourcetypes, default='')
  maximum = models.ForeignKey(Calendar, verbose_name=_('maximum'), null=True, blank=True,
    help_text=_('Calendar defining the available capacity'))
  location = models.ForeignKey(Location, verbose_name=_('location'),
    null=True, blank=True, db_index=True)
  cost = models.DecimalField(_('cost'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_("Cost for using 1 unit of the resource for 1 hour"))
  maxearly = DurationField(_('max early'),max_digits=settings.MAX_DIGITS, decimal_places=0, null=True, blank=True,
    help_text=_('Time window before the ask date where we look for available capacity'))
  setupmatrix = models.ForeignKey(SetupMatrix, verbose_name=_('setup matrix'),
    null=True, blank=True, db_index=True, 
    help_text=_('Setup matrix defining the conversion time and cost'))
  setup = models.CharField(_('setup'), max_length=settings.NAMESIZE, null=True, blank=True,
    help_text=_('Setup of the resource at the start of the plan'))

  # Methods
  def __unicode__(self): return self.name

  def save(self, *args, **kwargs):
    if self.type == 'resource_infinite':
        # These fields are not relevant for infinite resources
        self.maximum = None
        self.maxearly = None
    # Call the real save() method
    super(Resource, self).save(*args, **kwargs)

  class Meta(AuditModel.Meta):
    db_table = 'resource'
    verbose_name = _('resource')
    verbose_name_plural = _('resources')
    ordering = ['name']


class Flow(AuditModel):
  # Types of flow
  flowtypes = (
    ('',_('Start')),
    ('flow_start',_('Start')),
    ('flow_end',_('End')),
  )

  # Database fields
  id = models.AutoField(_('identifier'), primary_key=True)
  operation = models.ForeignKey(Operation, verbose_name=_('operation'),
    db_index=True, related_name='flows')
  thebuffer = models.ForeignKey(Buffer, verbose_name=_('buffer'),
    db_index=True, related_name='flows')
  type = models.CharField(_('type'), max_length=20, null=True, blank=True,
    choices=flowtypes,
    help_text=_('Consume/produce material at the start or the end of the operationplan'),
    )
  effective_start = models.DateTimeField(_('effective start'), null=True, blank=True,
    help_text=_('Validity start date')
    )
  effective_end = models.DateTimeField(_('effective end'), null=True, blank=True,
    help_text=_('Validity end date')
    )
  quantity = models.DecimalField(_('quantity'),max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, 
    default='1.00',
    help_text=_('Quantity to consume or produce per operationplan unit')
    )
  name = models.CharField(_('name'), max_length=settings.NAMESIZE, null=True, blank=True, 
    help_text=_('Optional name of this flow'))
  alternate = models.CharField(_('alternate'), max_length=settings.NAMESIZE, null=True, blank=True,
    help_text=_('Puts the flow in a group of alternate flows'))
  priority = models.IntegerField(_('priority'), default=1, null=True, blank=True,
    help_text=_('Priority of this flow in a group of alternates'))
  search = models.CharField(_('search mode'), _('search mode'), max_length=20, 
    null=True, blank=True, choices=searchmode,
    help_text=_('Method to select prefered alternate')
    )

  def __unicode__(self):
    return '%s - %s' % (self.operation.name, self.thebuffer.name)

  class Meta(AuditModel.Meta):
    db_table = 'flow'
    unique_together = (('operation','thebuffer'),)
    verbose_name = _('flow')
    verbose_name_plural = _('flows')


class Load(AuditModel):
  # Database fields
  id = models.AutoField(_('identifier'), primary_key=True)
  operation = models.ForeignKey(Operation, verbose_name=_('operation'), db_index=True, related_name='loads')
  resource = models.ForeignKey(Resource, verbose_name=_('resource'), db_index=True, related_name='loads')
  quantity = models.DecimalField(_('quantity'),max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, default='1.00')
  effective_start = models.DateTimeField(_('effective start'), null=True, blank=True,
    help_text=_('Validity start date')
    )
  effective_end = models.DateTimeField(_('effective end'), null=True, blank=True,
    help_text=_('Validity end date')
    )
  name = models.CharField(_('name'), max_length=settings.NAMESIZE, null=True, blank=True,
    help_text=_('Optional name of this load')
    )
  alternate = models.CharField(_('alternate'), max_length=settings.NAMESIZE, null=True, blank=True,
    help_text=_('Puts the load in a group of alternate loads')
    )
  priority = models.IntegerField(_('priority'), default=1, null=True, blank=True,
    help_text=_('Priority of this load in a group of alternates')
    )
  setup = models.CharField(_('setup'), max_length=settings.NAMESIZE, null=True, blank=True,
    help_text=_('Setup required on the resource for this operation')
    )
  search = models.CharField(_('search mode'), _('search mode'), max_length=20, 
    null=True, blank=True, choices=searchmode,
    help_text=_('Method to select prefered alternate')
    )

  def __unicode__(self):
    return '%s - %s' % (self.operation.name, self.resource.name)

  class Meta(AuditModel.Meta):
    db_table = 'resourceload'
    unique_together = (('operation','resource'),)
    verbose_name = _('load')
    verbose_name_plural = _('loads')


class OperationPlan(AuditModel):
  # Database fields
  id = models.IntegerField(_('identifier'),primary_key=True,
    help_text=_('Unique identifier of an operationplan'))
  operation = models.ForeignKey(Operation, verbose_name=_('operation'),
    db_index=True)
  quantity = models.DecimalField(_('quantity'),max_digits=settings.MAX_DIGITS,
    decimal_places=settings.DECIMAL_PLACES, default='1.00')
  startdate = models.DateTimeField(_('start date'),help_text=_('start date'))
  enddate = models.DateTimeField(_('end date'),help_text=_('end date'))
  locked = models.BooleanField(_('locked'),default=True,
    help_text=_('Prevent or allow changes'))

  def __unicode__(self): return str(self.id)

  class Meta(AuditModel.Meta):
    db_table = 'operationplan'
    verbose_name = _('operationplan')
    verbose_name_plural = _('operationplans')
    ordering = ['id']


class Demand(AuditModel):
  # The priorities defined here are for convenience only. FrePPLe accepts any number as priority.
  demandpriorities = (
    (1,_('1 - high')),
    (2,_('2 - normal')),
    (3,_('3 - low'))
  )

  # Database fields
  name = models.CharField(_('name'), max_length=settings.NAMESIZE, primary_key=True)
  description = models.CharField(_('description'), max_length=settings.DESCRIPTIONSIZE, null=True, blank=True)
  category = models.CharField(_('category'), max_length=settings.CATEGORYSIZE, null=True, blank=True, db_index=True)
  subcategory = models.CharField(_('subcategory'), max_length=settings.CATEGORYSIZE, null=True, blank=True, db_index=True)
  customer = models.ForeignKey(Customer, verbose_name=_('customer'), null=True, db_index=True)
  item = models.ForeignKey(Item, verbose_name=_('item'), db_index=True)
  due = models.DateTimeField(_('due'),help_text=_('Due date of the demand'))
  operation = models.ForeignKey(Operation,
    verbose_name=_('delivery operation'), null=True, blank=True,
    related_name='used_demand',
    help_text=_('Operation used to satisfy this demand'))
  quantity = models.DecimalField(_('quantity'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES)
  priority = models.PositiveIntegerField(_('priority'),default=2, choices=demandpriorities)
  minshipment = models.DecimalField(_('minimum shipment'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_('Minimum shipment quantity when planning this demand'))
  maxlateness = models.DecimalField(_('maximum lateness'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_("Maximum lateness allowed when planning this demand"))
  owner = models.ForeignKey('self', verbose_name=_('owner'), null=True, blank=True,
    help_text=_('Hierarchical parent'))

  # Convenience methods
  def __unicode__(self): return self.name

  class Meta(AuditModel.Meta):
    db_table = 'demand'
    verbose_name = _('demand')
    verbose_name_plural = _('demands')
    ordering = ['name']


class Forecast(AuditModel):
  # Database fields
  name = models.CharField(_('name'), max_length=settings.NAMESIZE, primary_key=True)
  description = models.CharField(_('description'), max_length=settings.DESCRIPTIONSIZE, null=True, blank=True)
  category = models.CharField(_('category'), max_length=settings.CATEGORYSIZE, null=True, blank=True, db_index=True)
  subcategory = models.CharField(_('subcategory'), max_length=settings.CATEGORYSIZE, null=True, blank=True, db_index=True)
  customer = models.ForeignKey(Customer, verbose_name=_('customer'), null=True, blank=True, db_index=True)
  item = models.ForeignKey(Item, verbose_name=_('item'), db_index=True)
  calendar = models.ForeignKey(Calendar, verbose_name=_('calendar'), null=False)
  operation = models.ForeignKey(Operation, verbose_name=_('delivery operation'), null=True, blank=True,
    related_name='used_forecast', help_text=_('Operation used to satisfy this demand'))
  priority = models.PositiveIntegerField(_('priority'),default=2, choices=Demand.demandpriorities)
  minshipment = models.DecimalField(_('minimum shipment'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_('Minimum shipment quantity when planning this demand'))
  maxlateness = models.DecimalField(_('maximum lateness'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, null=True, blank=True,
    help_text=_("Maximum lateness allowed when planning this demand"))
  discrete = models.BooleanField(_('discrete'),default=True, help_text=_('Round forecast numbers to integers'))

  # Convenience methods
  def __unicode__(self): return self.name

  def setTotal(self, startdate, enddate, quantity):
    '''
    Update the forecast quantity.
    The logic followed is three-fold:
      - If one or more forecast entries already exist in the daterange, the
        quantities of those entries are proportionally rescaled to fit the
        new quantity.
      - If no forecast entries exist yet, we create a new set of entries
        based on the bucket definition of the forecast calendar. This respects
        the weight ratios as defined in the calendar buckets.
      - In case no calendar or no calendar buckets can be identified, we simply
        create a single forecast entry for the specified daterange.
    '''
    # Assure the end date is later than the start date.
    if startdate > enddate:
      tmp = startdate
      startdate = enddate
      enddate = tmp
    # Assure the type of the quantity
    if not isinstance(quantity,Decimal): quantity = Decimal(str(quantity))
    # Round the quantity, if discrete flag is on
    if self.discrete: quantity = quantity.to_integral()
    # Step 0: Check for forecast entries intersecting with the current daterange
    startdate = startdate.date()
    enddate = enddate.date()
    entries = self.entries.filter(enddate__gt=startdate).filter(startdate__lt=enddate)
    if entries:
      # Case 1: Entries already exist in this daterange, which will be rescaled
      # Case 1, step 1: calculate current quantity and "clip" the existing entries
      # if required.
      current = 0
      for i in entries:
        # Calculate the length of this bucket in seconds
        duration = i.enddate - i.startdate
        duration = duration.days+86400*duration.seconds
        if i.startdate == startdate and i.enddate == enddate:
          # This entry has exactly the same daterange: update the quantity and exit
          i.quantity = str(quantity)
          i.save()
          return
        elif i.startdate < startdate and i.enddate > enddate:
          # This bucket starts before the daterange and also ends later.
          # We need to split the entry in three.
          # Part one: after our daterange, create a new entry
          p = i.enddate - enddate
          q = i.quantity * (p.days+86400*p.seconds) / duration
          if self.discrete: q = round(q)
          self.entries.create( \
             startdate = enddate,
             enddate = i.enddate,
             quantity = str(q),
             ).save()
          # Part two: our date range, create a new entry
          self.entries.create( \
             startdate = startdate,
             enddate = enddate,
             quantity = str(quantity),
             ).save()
          # Part three: before our daterange, update the existing entry
          p = startdate - i.startdate
          i.enddate = startdate
          i.quantity = i.quantity * (p.days+86400*p.seconds) / duration
          if self.discrete: i.quantity = round(i.quantity)
          i.quantity = str(i.quantity)
          i.save()
          # Done with this case...
          return
        elif i.startdate >= startdate and i.enddate <= enddate:
          # Entry falls completely in the range
          # TODO Incomplete???
          current += i.quantity
        elif i.startdate < enddate and i.enddate >= enddate:
          # This entry starts in the range and ends later.
          # Split the entry in two.
          p = i.enddate - enddate
          fraction = Decimal(i.quantity * (p.days+86400*p.seconds) / duration)
          current += i.quantity - fraction
          self.entries.create( \
             startdate = i.startdate,
             enddate = enddate,
             quantity = str(i.quantity - fraction),
             ).save()
          i.startdate = enddate
          if self.discrete: i.quantity = str(round(fraction))
          else: i.quantity = str(fraction)
          i.save()
        elif i.enddate > startdate and i.startdate <= startdate:
          # This entry ends in the range and starts earlier.
          # Split the entry in two.
          p = startdate - i.startdate
          fraction = Decimal(i.quantity * (p.days+86400*p.seconds) / duration)
          current += i.quantity - fraction
          self.entries.create( \
             startdate = startdate,
             enddate = i.enddate,
             quantity = str(i.quantity - fraction),
             ).save()
          i.enddate = startdate
          if self.discrete: i.quantity = str(round(fraction))
          else: i.quantity = str(fraction)
          i.save()
      # Case 1, step 2: Rescale the existing entries
      # Note that we retrieve an updated set of buckets from the database here...
      entries = self.entries.filter(enddate__gt=startdate).filter(startdate__lt=enddate)
      factor = quantity / current
      if factor == 0:
        for i in entries: i.delete()
      elif self.discrete:
        # Only put integers
        remainder = 0
        for i in entries:
          q = Decimal(i.quantity * factor + remainder)
          i.quantity = q.to_integral()
          remainder = q - i.quantity
          i.quantity = str(i.quantity)
          i.save()
      else:
        # No rounding required
        for i in entries:
          i.quantity *= factor
          i.quantity = str(i.quantity)
          i.save()
    else:
      # Case 2: No intersecting forecast entries exist yet. We use the
      # calendar buckets to create a new set of forecast entries, respecting
      # the weight of each bucket.
      # Note: if the calendar values are updated later on, such changes are
      # obviously not reflected any more in the forecast entries.
      cal = self.calendar
      if cal:
        entries = cal.buckets.filter(enddate__gt=startdate).filter(startdate__lte=enddate)
      if entries:
        # Case 2a: We found calendar buckets
        # Case 2a, step 1: compute total sum of weight values
        weights = 0
        for i in entries:
          p = min(i.enddate.date(),enddate) - max(i.startdate.date(),startdate)
          q = i.enddate.date() - i.startdate.date()
          weights +=  i.value * (p.days+86400*p.seconds) / (q.days+86400*q.seconds)
        # Case 2a, step 2: create a forecast entry for each calendar bucket
        remainder = Decimal(0)
        if weights == 0:
          # No non-zero weight buckets found: the update is infeasible
          return
        for i in entries:
          p = min(i.enddate.date(),enddate) - max(i.startdate.date(),startdate)
          q = i.enddate.date() - i.startdate.date()
          q = Decimal(quantity * i.value * (p.days+86400*p.seconds) / (q.days+86400*q.seconds) / weights)
          if self.discrete:
            q += remainder
            k = q.to_integral()
            remainder = q - k
            q = k
          if q > 0:
            self.entries.create( \
              startdate=max(i.startdate.date(),startdate),
              enddate=min(i.enddate.date(),enddate),
              quantity=str(q),
              ).save()
      else:
        # Case 2b: No calendar buckets found at all
        # Create a new entry for the daterange
        self.entries.create(startdate=startdate,enddate=enddate,quantity=str(quantity)).save()

  class Meta(AuditModel.Meta):
    db_table = 'forecast'
    verbose_name = _('forecast')
    verbose_name_plural = _('forecasts')
    ordering = ['name']


class ForecastDemand(AuditModel):
  # Database fields
  forecast = models.ForeignKey(Forecast, verbose_name=_('forecast'), null=False, db_index=True, related_name='entries')
  startdate = models.DateField(_('start date'), null=False)
  enddate = models.DateField(_('end date'), null=False)
  quantity = models.DecimalField(_('quantity'), max_digits=settings.MAX_DIGITS, decimal_places=settings.DECIMAL_PLACES, default=0)

  # Convenience methods
  def __unicode__(self): return self.forecast.name + " " + str(self.startdate) + " - " + str(self.enddate)

  class Meta(AuditModel.Meta):
    db_table = 'forecastdemand'
    verbose_name = _('forecast demand')
    verbose_name_plural = _('forecast demands')
www.java2java.com | Contact Us
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.