InventoryBrain.py 13 KB
Newer Older
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14
##############################################################################
#
# Copyright (c) 2002 Nexedi SARL. All Rights Reserved.
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
from Products.ZSQLCatalog.zsqlbrain import ZSQLBrain
15
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
Jean-Paul Smets's avatar
Jean-Paul Smets committed
16
from ZTUtils import make_query
Sebastien Robin's avatar
Sebastien Robin committed
17
from Products.CMFCore.utils import getToolByName
18
from zLOG import LOG, PROBLEM
19
from Products.ERP5Type.Message import translateString
20
from ComputedAttribute import ComputedAttribute
21

22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
class ComputedAttributeGetItemCompatibleMixin(ZSQLBrain):
  """A brain that supports accessing computed attributes using __getitem__
  protocol.
  """
  def __init__(self):
    # __getitem__ returns the computed attribute directly, but if we access
    # brain['node_title'] we expect to have the attribute after computation,
    # not the ComputedAttribute attribue instance. Defining a __getitem__
    # method on that class is not enough, because the brain class is not
    # directly this class but a class created on the fly that also inherits
    # from Record which already defines a __getitem__ method.
    # We cannot patch the instance, because Record does not allow this kind of
    # mutation, but as the class is created on the fly, for each query, it's
    # safe to patch the class. See Shared/DC/ZRDB/Results.py for more detail.
    # A Records holds a list of r instances, only the first __init__ needs to
    # do this patching.
    if not hasattr(self.__class__, '__super__getitem__'):
      self.__class__.__super__getitem__ = self.__class__.__getitem__
      self.__class__.__getitem__ =\
        ComputedAttributeGetItemCompatibleMixin.__getitem__

  # ComputedAttribute compatibility for __getitem__
  def __getitem__(self, name):
    item = self.__super__getitem__(name)
    if isinstance(item, ComputedAttribute):
      return item.__of__(self)
    return item

class InventoryListBrain(ComputedAttributeGetItemCompatibleMixin):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
51 52 53 54
  """
    Lists each variation
  """
  # Stock management
Sebastien Robin's avatar
Sebastien Robin committed
55
  def getInventory(self, **kw):
56
    simulation_tool = getToolByName(self, 'portal_simulation')
Romain Courteaud's avatar
Romain Courteaud committed
57
    return simulation_tool.getInventory(
58
                   node_uid=self.node_uid,
Romain Courteaud's avatar
Romain Courteaud committed
59
                   variation_text=self.variation_text,
60
                   resource_uid=self.resource_uid, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
61

Sebastien Robin's avatar
Sebastien Robin committed
62
  def getCurrentInventory(self,**kw):
63
    simulation_tool = getToolByName(self, 'portal_simulation')
Romain Courteaud's avatar
Romain Courteaud committed
64
    return simulation_tool.getCurrentInventory(
65
                             node_uid=self.node_uid,
Romain Courteaud's avatar
Romain Courteaud committed
66
                             variation_text=self.variation_text,
67
                             resource_uid=self.resource_uid, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
68

Sebastien Robin's avatar
Sebastien Robin committed
69 70
  def getFutureInventory(self,**kw):
    simulation_tool = getToolByName(self,'portal_simulation')
Romain Courteaud's avatar
Romain Courteaud committed
71
    return simulation_tool.getFutureInventory(
72
                              node_uid=self.node_uid,
Romain Courteaud's avatar
Romain Courteaud committed
73
                              variation_text=self.variation_text,
74
                              resource_uid=self.resource_uid, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
75

Sebastien Robin's avatar
Sebastien Robin committed
76 77
  def getAvailableInventory(self,**kw):
    simulation_tool = getToolByName(self,'portal_simulation')
78
    return simulation_tool.getAvailableInventory(
79
                             node_uid=self.node_uid,
80
                             variation_text=self.variation_text,
81
                             resource_uid=self.resource_uid, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
82 83

  def getQuantityUnit(self, **kw):
84
    resource = self.getResourceValue()
85
    if resource is not None:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
86 87
      return resource.getQuantityUnit()

88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
  def _getObjectByUid(self, uid):
    uid_cache = getTransactionalVariable().setdefault(
                    'InventoryBrain.uid_cache', {None: None})
    try:
      return uid_cache[uid]
    except KeyError:
      result_list = self.portal_catalog(uid=uid, limit=1,
        select_dict=dict(title=None, relative_url=None))
      result = None
      if result_list:
        result = result_list[0]
      uid_cache[uid] = result
      return result

  def getSectionValue(self):
    return self._getObjectByUid(self.section_uid)

  def getSectionTitle(self):
    section = self.getSectionValue()
    if section is not None:
      return section.title
  section_title = ComputedAttribute(getSectionTitle, 1)

  def getSectionRelativeUrl(self):
    section = self.getSectionValue()
    if section is not None:
      return section.relative_url
  section_relative_url = ComputedAttribute(getSectionRelativeUrl, 1)

  def getNodeValue(self):
    return self._getObjectByUid(self.node_uid)

  def getNodeTitle(self):
    node = self.getNodeValue()
    if node is not None:
      return node.title
  node_title = ComputedAttribute(getNodeTitle, 1)

  def getNodeRelativeUrl(self):
    node = self.getNodeValue()
    if node is not None:
      return node.relative_url
  node_relative_url = ComputedAttribute(getNodeRelativeUrl, 1)

  def getResourceValue(self):
    return self._getObjectByUid(self.resource_uid)

  def getResourceTitle(self):
    resource = self.getResourceValue()
    if resource is not None:
      return resource.title
  resource_title = ComputedAttribute(getResourceTitle, 1)

  def getResourceRelativeUrl(self):
    resource = self.getResourceValue()
    if resource is not None:
      return resource.relative_url
  resource_relative_url = ComputedAttribute(getResourceRelativeUrl, 1)

147 148 149 150 151 152
  def getResourceReference(self):
    resource = self.getResourceValue()
    if resource is not None:
      return resource.getReference()
  resource_reference = ComputedAttribute(getResourceReference, 1)

Jean-Paul Smets's avatar
Jean-Paul Smets committed
153
  def getListItemUrl(self, cname_id, selection_index, selection_name):
154 155
    """Returns the URL for column `cname_id`. Used by ListBox
    """
156
    resource = self.getResourceValue()
157 158 159 160 161
    if cname_id in ('getExplanationText', 'getExplanation', ):
      o = self.getObject()
      if o is not None:
        if not getattr(o, 'isDelivery', 0):
          explanation = o.getExplanationValue()
162
        else:
163 164 165 166
          # Additional inventory movements are catalogged in stock table
          # with the inventory's uid. Then they are their own explanation.
          explanation = o
        if explanation is not None:
Jérome Perrin's avatar
Jérome Perrin committed
167 168
          return explanation.absolute_url()
      return ''
169
    elif resource is not None:
170
      # A resource is defined, so try to display the movement list
Jérome Perrin's avatar
Jérome Perrin committed
171
      form_id = 'Resource_viewMovementHistory'
172 173 174 175 176 177 178 179
      query_kw = {
        'variation_text': self.variation_text,
        'selection_name': selection_name,
        'selection_index': selection_index,
        'domain_name': selection_name,
      }
      # Add parameters to query_kw
      query_kw_update = {}
180

181
      if cname_id in ('transformed_resource_title', ):
182
        return resource.absolute_url()
183
      elif cname_id in ('getCurrentInventory', ):
184
        query_kw_update = {
185 186 187 188 189 190
          'simulation_state': 
            list(self.getPortalCurrentInventoryStateList() + \
            self.getPortalTransitInventoryStateList()),
          'omit_transit': 1,
          'transit_simulation_state': list(
                 self.getPortalTransitInventoryStateList())
191
        }
192

193 194
      elif cname_id in ('getAvailableInventory', ):
        query_kw_update = {
195 196 197 198 199 200 201 202 203
          'simulation_state': list(self.getPortalCurrentInventoryStateList() + \
                            self.getPortalTransitInventoryStateList()),
          'omit_transit': 1,
          'transit_simulation_state': list(self.getPortalTransitInventoryStateList()),
          'reserved_kw': {
            'simulation_state': list(self.getPortalReservedInventoryStateList()),
            'transit_simulation_state': list(self.getPortalTransitInventoryStateList()),
            'omit_input': 1
          }
204 205 206 207 208
        }
      elif cname_id in ('getFutureInventory', 'inventory', ):
        query_kw_update = {
          'simulation_state': \
            list(self.getPortalFutureInventoryStateList()) + \
209
            list(self.getPortalTransitInventoryStateList()) + \
210 211 212 213 214 215 216 217 218 219 220 221
            list(self.getPortalReservedInventoryStateList()) + \
            list(self.getPortalCurrentInventoryStateList())
        }
      elif cname_id in ('getInventoryAtDate', ):
        query_kw_update = {
          'to_date': self.at_date,
          'simulation_state': \
            list(self.getPortalFutureInventoryStateList()) + \
            list(self.getPortalReservedInventoryStateList())
        }
      query_kw.update(query_kw_update)
      return '%s/%s?%s&reset=1' % ( resource.absolute_url(),
Jérome Perrin's avatar
Jérome Perrin committed
222
                                    form_id,
223 224 225 226
                                    make_query(**query_kw) )

    # default case, if it's a movement, return link to the explanation of this
    # movement.
227 228 229
    document = self.getObject()
    if document.isMovement():
      explanation = document.getExplanationValue()
230
      if explanation is not None:
Jérome Perrin's avatar
Jérome Perrin committed
231
        return explanation.absolute_url()
232
    return ''
Jean-Paul Smets's avatar
Jean-Paul Smets committed
233

234 235 236 237
  def getExplanationText(self):
    # Returns an explanation of the movement
    o = self.getObject()
    if o is not None:
238
      # Get the delivery/order
239 240 241 242 243 244
      if not getattr(o, 'isDelivery', 0):
        delivery = o.getExplanationValue()
      else:
        # Additional inventory movements are catalogged in stock table
        # with the inventory's uid. Then they are their own explanation.
        delivery = o
245
      if delivery is not None:
246 247 248 249
        mapping = {
          'delivery_portal_type' : delivery.getTranslatedPortalType(),
          'delivery_title' : delivery.getTitleOrId()
        }
250 251
        causality = delivery.getCausalityValue()
        if causality is not None:
252
          mapping['causality_portal_type'] = causality.getTranslatedPortalType()
253
          mapping['causality_title'] = causality.getTitleOrId()
254 255 256 257
          return translateString(
            "${delivery_portal_type} ${delivery_title} "
            "(${causality_portal_type} ${causality_title})",
            mapping=mapping)
258
        else :
259 260 261
          return translateString("${delivery_portal_type} ${delivery_title}",
                                 mapping=mapping)
    return translateString('Unknown')
262

263 264 265 266
class TrackingListBrain(InventoryListBrain):
  """
  List of aggregated movements
  """
267
  def getDate(self):
268 269 270 271 272 273 274
    if not self.date:
      return
    # convert the date in the movement's original timezone.
    # This is a somehow heavy operation, but fortunatly it's only called when
    # the brain is accessed from the Shared.DC.ZRDB.Results.Results instance
    obj = self.getObject()
    if obj is not None:
275 276
      portal = obj.getPortalObject()
      movement = portal.portal_catalog.getObject(self.delivery_uid)
277 278 279
      date = movement.getStartDate() or movement.getStopDate()
      if date is not None:
        timezone = date.timezone()
280 281
        return self.date.toZone(timezone)
    return self.date
282

283 284 285 286 287

class MovementHistoryListBrain(InventoryListBrain):
  """Brain for getMovementHistoryList
  """
  def __init__(self):
288
    InventoryListBrain.__init__(self)
289 290 291 292 293 294 295
    if not self.date:
      return
    # convert the date in the movement's original timezone.
    # This is a somehow heavy operation, but fortunatly it's only called when
    # the brain is accessed from the Shared.DC.ZRDB.Results.Results instance
    obj = self.getObject()
    if obj is not None:
296
      timezone = None
297
      if self.node_uid == obj.getSourceUid():
298 299 300
        start_date = obj.getStartDate()
        if start_date is not None:
          timezone = start_date.timezone()
301
      else:
302 303 304 305 306
        stop_date = obj.getStopDate()
        if stop_date is not None:
          timezone = stop_date.timezone()
      if timezone is not None:
        self.date = self.date.toZone(timezone)
307

308 309 310 311 312 313 314 315 316 317 318 319
  def getListItemUrl(self, cname_id, selection_index, selection_name):
    """Returns the URL for column `cname_id`. Used by ListBox
    Here we just want a link to the explanation of movement.
    """
    document = self.getObject()
    if document.isMovement():
      explanation = document.getExplanationValue()
      if explanation is not None:
        return explanation.absolute_url()
    return ''


320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343
  def _debit(self):
    if self.getObject().isCancellationAmount():
      return min(self.total_quantity, 0)
    return max(self.total_quantity, 0)
  debit = ComputedAttribute(_debit, 1)

  def _credit(self):
    if self.getObject().isCancellationAmount():
      return min(-(self.total_quantity or 0), 0)
    return max(-(self.total_quantity or 0), 0)
  credit = ComputedAttribute(_credit, 1)

  def _debit_price(self):
    if self.getObject().isCancellationAmount():
      return min(self.total_price, 0)
    return max(self.total_price, 0)
  debit_price = ComputedAttribute(_debit_price, 1)

  def _credit_price(self):
    if self.getObject().isCancellationAmount():
      return min(-(self.total_price or 0), 0)
    return max(-(self.total_price or 0), 0)
  credit_price = ComputedAttribute(_credit_price, 1)