PaySheetTransaction.py 20.7 KB
Newer Older
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
##############################################################################
#
# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
31
from Products.ERP5.Document.Invoice import Invoice
32
from Products.ERP5Type.Utils import cartesianProduct
33
from zLOG import LOG, DEBUG, INFO
Yoshinori Okuji's avatar
Yoshinori Okuji committed
34

Fabien Morin's avatar
Fabien Morin committed
35 36 37
#XXX TODO: review naming of new methods
#XXX WARNING: current API naming may change although model should be stable.

38
class PaySheetTransaction(Invoice):
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
  """
  A paysheet will store data about the salary of an employee
  """

  meta_type = 'ERP5 Pay Sheet Transaction'
  portal_type = 'Pay Sheet Transaction'
  add_permission = Permissions.AddPortalContent
  isPortalContent = 1
  isRADContent = 1

  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

  # Default Properties
  property_sheets = ( PropertySheet.Base
                    , PropertySheet.SimpleItem
                    , PropertySheet.CategoryCore
                    , PropertySheet.Task
                    , PropertySheet.Arrow
                    , PropertySheet.Delivery
                    , PropertySheet.PaySheet
                    , PropertySheet.Movement
                    , PropertySheet.Amount
                    , PropertySheet.XMLObject
                    , PropertySheet.TradeCondition
                    , PropertySheet.DefaultAnnotationLine
                    )

  # Declarative Interface
  __implements__ = ( )


72 73 74 75 76 77 78
  security.declareProtected(Permissions.AccessContentsInformation,
                          'getRatioQuantityFromReference')
  def getRatioQuantityFromReference(self, ratio_reference=None):
    """
    return the ratio value correponding to the ratio_reference,
    None if ratio_reference not found
    """
79 80
    # get ratio lines
    portal_type_list = ['Pay Sheet Model Ratio Line']
Fabien Morin's avatar
Fabien Morin committed
81 82 83 84
    object_ratio_list = self.contentValues(portal_type=portal_type_list)

    # look for ratio lines on the paysheet
    if object_ratio_list:
85 86 87
      for obj in object_ratio_list:
        if obj.getReference() == ratio_reference:
          return obj.getQuantity()
Fabien Morin's avatar
Fabien Morin committed
88 89

    # if not find in the paysheet, look on dependence tree
90
    sub_object_list = self.getInheritedObjectValueList(portal_type_list)
91
    object_ratio_list = sub_object_list
92 93 94
    for object in object_ratio_list:
      if object.getReference() == ratio_reference:
        return object.getQuantity()
Fabien Morin's avatar
Fabien Morin committed
95

96 97 98 99 100 101 102 103 104 105
    return None 

  security.declareProtected(Permissions.AccessContentsInformation,
                          'getRatioQuantityList')
  def getRatioQuantityList(self, ratio_reference_list):
    """
    Return a list of reference_ratio_list correponding values.
    reference_ratio_list is a list of references to the ratio lines
    we want to get.
    """
106
    if not isinstance(ratio_reference_list, (list, tuple)):
107 108 109 110
      return [self.getRatioQuantityFromReference(ratio_reference_list)]
    return [self.getRatioQuantityFromReference(reference) \
        for reference in ratio_reference_list]

111 112 113
  security.declareProtected(Permissions.AccessContentsInformation,
                          'getRatioQuantityFromReference')
  def getAnnotationLineFromReference(self, reference=None):
114 115
    """Return the annotation line corresponding to the reference.
    Returns None if reference not found
116
    """
Fabien Morin's avatar
Fabien Morin committed
117
    # look for annotation lines on the paysheet
118
    annotation_line_list = self.contentValues(portal_type=['Annotation Line'])
Fabien Morin's avatar
Fabien Morin committed
119 120 121 122 123 124
    if annotation_line_list:
      for annotation_line in annotation_line_list:
        if annotation_line.getReference() == reference:
          return annotation_line

    # if not find in the paysheet, look on dependence tree
125
    for annotation_line in self.getInheritedObjectValueList(['Annotation Line']):
126 127
      if annotation_line.getReference() == reference:
        return annotation_line
Fabien Morin's avatar
Fabien Morin committed
128

129 130 131 132 133
    return None 

  security.declareProtected(Permissions.AccessContentsInformation,
                          'getRatioQuantityList')
  def getAnnotationLineListList(self, reference_list):
134
    """Return a list of annotation lines corresponding to the reference_list
135 136 137
    reference_list is a list of references to the Annotation Line we want 
    to get.
    """
138
    if not isinstance(reference_list, (list, tuple)):
139 140 141
      return [self.getAnnotationLineFromReference(reference_list)]
    return [self.getAnnotationLineFromReference(reference) \
        for reference in reference_list]
142

143
  security.declareProtected(Permissions.AddPortalContent,
144 145 146 147
                            'createPaySheetLine')
  def createPaySheetLine(self, cell_list, title='', resource='',
                         description='', base_amount_list=None, int_index=None,
                         categories=None, **kw):
148 149 150 151
    '''
    This function register all paysheet informations in paysheet lines and 
    cells. Select good cells only
    '''
152 153 154
    if not resource:
      raise ValueError, "Cannot create Pay Sheet Line without resource"

155 156 157 158 159 160 161 162 163 164
    good_cell_list = []
    for cell in cell_list:
      if cell['quantity'] or cell['price']:
        good_cell_list.append(cell)
    if len(good_cell_list) == 0:
      return
    # Get all variation categories used in cell_list
    var_cat_list = []
    for cell in good_cell_list:
      # Don't add a variation category if already in it
165
      for category in cell['category_list']:
166
        if category not in var_cat_list:
167 168
          var_cat_list.append(category)

169
    resource_value = self.getPortalObject().unrestrictedTraverse(resource)
170 171
    # Add a new Pay Sheet Line
    payline = self.newContent(
Jérome Perrin's avatar
Jérome Perrin committed
172 173 174 175 176 177 178 179 180 181 182 183 184
                       portal_type='Pay Sheet Line',
                       title=title,
                       description=description,
                       destination=self.getSourceSection(),
                       source_section=resource_value.getSource(),
                       resource_value=resource_value,
                       destination_section=self.getDestinationSection(),
                       variation_base_category_list=('tax_category',
                                                     'salary_range'),
                       variation_category_list=var_cat_list,
                       base_amount_list=base_amount_list,
                       int_index=int_index,
                       **kw)
185

186 187 188 189 190
    # add cells categories to the Pay Sheet Line
    # it's a sort of inheritance of sub-object data
    if categories is not None:
      categories_list = payline.getCategoryList()
      categories_list.extend(categories)
Jérome Perrin's avatar
Jérome Perrin committed
191
      payline.edit(categories=categories_list)
192

193
    base_id = 'movement'
Jérome Perrin's avatar
Jérome Perrin committed
194
    a = payline.updateCellRange(base_id=base_id)
195 196
    # create cell_list
    for cell in good_cell_list:
Jérome Perrin's avatar
Jérome Perrin committed
197
      paycell = payline.newCell(base_id=base_id, *cell['category_list'])
Fabien Morin's avatar
Fabien Morin committed
198
      # if the price aven't be completed, it should be set to 1 (=100%)
199 200
      if not cell['price']:
        cell['price'] = 1
Jérome Perrin's avatar
Jérome Perrin committed
201 202
      paycell.edit(mapped_value_property_list=('price', 'quantity'),
                    force_update=1,
203
                    **cell)
204 205 206
    return payline


Fabien Morin's avatar
Fabien Morin committed
207 208 209
  security.declareProtected(Permissions.AccessContentsInformation,
                          'getEditableModelLineAsDict')
  def getEditableModelLineAsDict(self, listbox, paysheet):
210
    '''
Fabien Morin's avatar
Fabien Morin committed
211 212 213
      listbox is composed by one line for each slice of editables model_lines
      this script will return editable model lines as a dict with the 
      properties that could/have be modified.
214
    '''
Fabien Morin's avatar
Fabien Morin committed
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
    portal = paysheet.getPortalObject()

    model_line_dict = {}
    for line in listbox:
      model_line_url = line['model_line']
      model_line = portal.restrictedTraverse(model_line_url)

      salary_range_relative_url=line['salary_range_relative_url']
      if salary_range_relative_url == '':
        salary_range_relative_url='no_slice'
      
      # if this is the first slice of the model_line, create the dict
      if not model_line_dict.has_key(model_line_url):
        model_line_dict[model_line_url] = {'int_index' :\
            model_line.getIntIndex()}

      model_line_dict[model_line_url][salary_range_relative_url] = {}
      slice_dict = model_line_dict[model_line_url][salary_range_relative_url]
      for tax_category in model_line.getTaxCategoryList():
        if line.has_key('%s_quantity' % tax_category) and \
            line.has_key('%s_price' % tax_category):
236 237 238
          slice_dict[tax_category] = dict(
                      quantity=line['%s_quantity' % tax_category],
                      price=line['%s_price' % tax_category],)
Fabien Morin's avatar
Fabien Morin committed
239
        else:
240 241
          LOG('ERP5', INFO, 'No attribute %s_quantity or %s_price for model_line %s' %
                   ( tax_category, tax_category, model_line_url ))
Fabien Morin's avatar
Fabien Morin committed
242 243
       
    return model_line_dict
Fabien Morin's avatar
Fabien Morin committed
244

Fabien Morin's avatar
Fabien Morin committed
245 246 247 248 249 250 251 252

  security.declareProtected(Permissions.AccessContentsInformation,
                          'getNotEditableModelLineAsDict')
  def getNotEditableModelLineAsDict(self, paysheet):
    '''
      return the not editable lines as dict
    '''
    model = paysheet.getSpecialiseValue()
253 254 255 256 257 258

    def sortByIntIndex(a, b):
      return cmp(a.getIntIndex(), b.getIntIndex())

    # get model lines
    portal_type_list = ['Pay Sheet Model Line']
259
    sub_object_list = paysheet.getInheritedObjectValueList(portal_type_list)
260 261
    sub_object_list.sort(sortByIntIndex)
    model_line_list = sub_object_list
Fabien Morin's avatar
Fabien Morin committed
262 263 264 265 266 267 268 269 270 271 272 273 274 275

    model_line_dict = {}
    for model_line in model_line_list:
      model_line_url = model_line.getRelativeUrl()
      cell_list = model_line.contentValues(portal_type='Pay Sheet Cell')

      for cell in cell_list:
        salary_range_relative_url = \
            cell.getVariationCategoryList(base_category_list='salary_range')
        tax_category = cell.getTaxCategory()
        if len(salary_range_relative_url):
          salary_range_relative_url = salary_range_relative_url[0]
        else:
          salary_range_relative_url = 'no_slice'
Fabien Morin's avatar
Fabien Morin committed
276
        
Fabien Morin's avatar
Fabien Morin committed
277 278 279 280 281 282 283
        # if this is the first slice of the model_line, create the dict
        if not model_line_dict.has_key(model_line_url):
          model_line_dict[model_line_url] = {'int_index' :\
              model_line.getIntIndex()}

        model_line_dict[model_line_url][salary_range_relative_url] = {}
        slice_dict = model_line_dict[model_line_url][salary_range_relative_url]
284 285 286
        slice_dict[tax_category] = dict(quantity=cell.getQuantity(),
                                        price=cell.getPrice())

Fabien Morin's avatar
Fabien Morin committed
287 288
    return model_line_dict

Yoshinori Okuji's avatar
Yoshinori Okuji committed
289

290
  security.declareProtected(Permissions.ModifyPortalContent,
291
                            'createPaySheetLineList')
Fabien Morin's avatar
Fabien Morin committed
292
  def createPaySheetLineList(self, listbox=None, batch_mode=0, **kw):
293
    '''Create all Pay Sheet Lines (editable or not)
Fabien Morin's avatar
Fabien Morin committed
294 295 296 297 298 299 300 301 302 303

      parameters :

      - batch_mode :if batch_mode is enabled (=1) then there is no preview view,
                    and editable lines are considered as not editable lines.
                    This is usefull to generate all PaySheet of a company.
                    Modification values can be made on each paysheet after, by
                    using the "Calculation of the Pay Sheet Transaction"
                    action button. (concerned model lines must be editable)

304 305
    '''

Fabien Morin's avatar
Fabien Morin committed
306 307 308 309 310 311
    paysheet = self 
    
    if not batch_mode and listbox is not None:
      model_line_dict = paysheet.getEditableModelLineAsDict(listbox=listbox,
          paysheet=paysheet)

312
    # Get Precision
Fabien Morin's avatar
Fabien Morin committed
313
    precision = paysheet.getPriceCurrencyValue().getQuantityPrecision()
314

315 316
    # in this dictionary will be saved the current amount corresponding to 
    # the tuple (tax_category, base_amount) :
317 318
    # current_amount = base_amount_dict[base_amount][share]
    base_amount_dict = {}
319

320 321
    model = paysheet.getSpecialiseValue()

322
    def sortByIntIndex(a, b):
Fabien Morin's avatar
Fabien Morin committed
323
      return cmp(a.getIntIndex(), b.getIntIndex())
324 325


326
    # XXX should this be recursive ? then should membership be strict
Fabien Morin's avatar
Fabien Morin committed
327
    base_amount_list = paysheet.portal_categories['base_amount'].contentValues()
328 329
    base_amount_list.sort(sortByIntIndex)

Fabien Morin's avatar
Fabien Morin committed
330 331

    # get model lines
332
    portal_type_list = ['Pay Sheet Model Line']
333
    sub_object_list = paysheet.getInheritedObjectValueList(portal_type_list)
334 335
    sub_object_list.sort(sortByIntIndex)
    model_line_list = sub_object_list
336 337 338 339

    pay_sheet_line_list = []

    # main loop : find all informations and create cell and PaySheetLines
340
    for model_line in model_line_list:
Jérome Perrin's avatar
Jérome Perrin committed
341
      cell_list = []
342
      # test with predicate if this model line could be applied
Fabien Morin's avatar
Fabien Morin committed
343 344
      if not model_line.test(paysheet,):
        # This model_line should not be applied
345 346 347
        LOG('ERP5', DEBUG, 'createPaySheetLineList: Model Line %s (%s) will'
            ' not be applied, because predicates does not match' %
            ( model_line.getTitle(), model_line.getRelativeUrl() ))
348 349
        continue

Jérome Perrin's avatar
Jérome Perrin committed
350
      service = model_line.getResourceValue()
351 352 353
      if service is None:
        raise ValueError, 'Model Line %s has no resource' % (
                                        model_line.getRelativeUrl())
Jérome Perrin's avatar
Jérome Perrin committed
354 355
      title = model_line.getTitleOrId()
      int_index = model_line.getFloatIndex()
356
      base_amount_list = model_line.getBaseAmountList()
Jérome Perrin's avatar
Jérome Perrin committed
357
      resource = service.getRelativeUrl()
Fabien Morin's avatar
Fabien Morin committed
358

359
      if model_line.getDescription():
360 361 362 363 364
        desc = model_line.getDescription()
        # if the model_line description is empty, the payroll service
        # description is used
      else:
        desc = service.getDescription()
365 366

      base_category_list = model_line.getVariationBaseCategoryList()
367
      category_list_list = []
368
      for base_cat in base_category_list:
369 370 371 372
        category_list = model_line.getVariationCategoryList(
                                        base_category_list=base_cat)
        category_list_list.append(category_list)
      cartesian_product = cartesianProduct(category_list_list)
373

Fabien Morin's avatar
Fabien Morin committed
374 375
      share = None
      slice = 'no_slice'
376
      indice = 0
377
      categories = []
378
      for cell_coordinates in cartesian_product:
379
        indice += 1
380
        cell = model_line.getCell(*cell_coordinates)
381
        if cell is None:
382 383 384
          LOG('ERP5', INFO, "Can't find the cell corresponding to those cells"
              " coordinates : %s" % cell_coordinates)
          # XXX is it enough to log ?
385 386
          continue

Fabien Morin's avatar
Fabien Morin committed
387 388 389 390 391 392 393 394 395
        if len(cell.getVariationCategoryList(\
            base_category_list='tax_category')):
          share = cell.getVariationCategoryList(\
              base_category_list='tax_category')[0]

        if len(cell.getVariationCategoryList(\
            base_category_list='salary_range')):
          slice = cell.getVariationCategoryList(\
              base_category_list='salary_range')[0]
396
    
Fabien Morin's avatar
Fabien Morin committed
397 398 399 400 401 402 403 404 405 406
        # get the edited values if this model_line is editable
        # and replace the original cell values by this ones
        if model_line.isEditable() and not batch_mode:
          tax_category = cell.getTaxCategory()

          # get the dict who contain modified values
          line_dict = model_line_dict[model_line.getRelativeUrl()]

          def getModifiedCell(cell, slice_dict, tax_category):
            '''
407
              return a cell with the modified values (contained in slice_dict)
Fabien Morin's avatar
Fabien Morin committed
408 409 410 411 412 413 414 415 416 417 418 419
            '''
            if slice_dict:
              if slice_dict.has_key(tax_category):
                if slice_dict[tax_category].has_key('quantity'):
                  cell = cell.asContext(\
                      quantity=slice_dict[tax_category]['quantity'])
                if slice_dict[tax_category].has_key('price'):
                  cell = cell.asContext(price=slice_dict[tax_category]['price'])
            return cell

          cell = getModifiedCell(cell, line_dict[slice], tax_category)

420 421 422 423 424 425 426
        # get the slice :
        model_slice = model_line.getParentValue().getCell(slice)
        quantity = 0.0
        price = 0.0
        model_slice_min = 0
        model_slice_max = 0
        if model_slice is None:
Fabien Morin's avatar
Fabien Morin committed
427 428
          pass # that's not a problem :)

429
        else:
Fabien Morin's avatar
Fabien Morin committed
430 431 432
          model_slice_min = model_slice.getQuantityRangeMin()
          model_slice_max = model_slice.getQuantityRangeMax()

433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
        ######################
        # calculation part : #
        ######################

        # get script in this order
        # 1 - model_line script
        # 2 - model script
        # 3 - get the default calculation script

        # get the model line script
        script_name = model_line.getCalculationScriptId()
        if script_name is None:
          # if model line script is None, get the default model script
          script_name = model.getDefaultCalculationScriptId()
        if script_name is None:
          # if no calculation script found, use a default script :
          script_name = 'PaySheetTransaction_defaultCalculationScript'

Fabien Morin's avatar
Fabien Morin committed
451
        if getattr(paysheet, script_name, None) is None:
452 453
          raise ValueError, "Unable to find `%s` calculation script" % \
                                                           script_name
Fabien Morin's avatar
Fabien Morin committed
454
        calculation_script = getattr(paysheet, script_name, None)
455 456
        quantity=0
        price=0
Jérome Perrin's avatar
Jérome Perrin committed
457 458
        cell_dict = calculation_script(base_amount_dict=base_amount_dict,
                                       cell=cell,)
459
        cell_dict.update({'category_list': cell_coordinates})
460

461 462 463 464 465
        if cell_dict.has_key('categories'):
          for cat in cell_dict['categories']:
            if cat not in categories:
              categories.append(cat)

466 467
        quantity = cell_dict['quantity']
        price = cell_dict['price']
468

469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485
        if quantity:
          cell_list.append(cell_dict)

          # update the base_participation
          base_participation_list = service.getBaseAmountList(base=1)
          for base_participation in base_participation_list:
            if quantity:
              if base_amount_dict.has_key(base_participation) and \
                  base_amount_dict[base_participation].has_key(share):
                old_val = base_amount_dict[base_participation][share]
              else:
                old_val = 0
              new_val = old_val + quantity
              if not base_amount_dict.has_key(base_participation):
                base_amount_dict[base_participation]={}

              if price:
Jérome Perrin's avatar
Jérome Perrin committed
486
                new_val = round((old_val + quantity*price), precision)
487
              base_amount_dict[base_participation][share] = new_val
488 489 490

      if cell_list:
        # create the PaySheetLine
Fabien Morin's avatar
Fabien Morin committed
491
        pay_sheet_line = paysheet.createPaySheetLine(
Jérome Perrin's avatar
Jérome Perrin committed
492 493 494 495 496 497 498
                                            title=title,
                                            resource=resource,
                                            int_index=int_index,
                                            desc=desc,
                                            base_amount_list=base_amount_list,
                                            cell_list=cell_list,
                                            categories=categories)
499
        pay_sheet_line_list.append(pay_sheet_line)
500

501 502 503

    # this script is used to add a line that permit to have good accounting 
    # lines
504
    post_calculation_script = paysheet._getTypeBasedMethod('postCalculation')
505 506
    if post_calculation_script:
      post_calculation_script()
507 508

    return pay_sheet_line_list
509

510
  def getInheritedObjectValueList(self, portal_type_list, property_list=()):
511
    '''Return a list of all subobjects of the herited model (incuding the
512 513 514
      dependencies).
      If property_list is provided, only subobjects with at least one of those
      properties is defined will be taken into account
515
    '''
516
    model = self.getSpecialiseValue()
517
    model_reference_dict = model.getInheritanceModelReferenceDict(
518 519
                                   portal_type_list=portal_type_list,
                                   property_list=property_list)
520 521

    sub_object_list = []
522 523 524 525
    traverse = self.getPortalObject().unrestrictedTraverse
    for model_url, id_list in model_reference_dict.items():
      model = traverse(model_url)
      sub_object_list.extend([model._getOb(x) for x in id_list])
526 527 528

    return sub_object_list