PaySheetTransaction.py 21 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 172 173 174
    # Add a new Pay Sheet Line
    payline = self.newContent(
             portal_type                  = 'Pay Sheet Line',
             title                        = title,
             description                  = description,
175
             destination                  = self.getSourceSection(),
176 177
             source_section               = resource_value.getSource(),
             resource_value               = resource_value,
178 179 180
             destination_section          = self.getDestinationSection(),
             variation_base_category_list = ('tax_category', 'salary_range'),
             variation_category_list      = var_cat_list,
181
             base_amount_list             = base_amount_list,
182
             int_index                    = int_index,
183 184
             **kw)

185 186 187 188 189 190 191
    # 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)
      payline.edit(categories = categories_list)

192
    base_id = 'movement'
Fabien Morin's avatar
Fabien Morin committed
193
    a = payline.updateCellRange(base_id = base_id)
194 195
    # create cell_list
    for cell in good_cell_list:
196
      paycell = payline.newCell(base_id = base_id, *cell['category_list'])
Fabien Morin's avatar
Fabien Morin committed
197
      # if the price aven't be completed, it should be set to 1 (=100%)
198 199
      if not cell['price']:
        cell['price'] = 1
Fabien Morin's avatar
Fabien Morin committed
200 201
      paycell.edit( mapped_value_property_list = ('price', 'quantity'),
                    force_update               = 1,
202
                    **cell)
203 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 317

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

321 322
    model = paysheet.getSpecialiseValue()

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


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 341 342
    for model_line in model_line_list:
      cell_list       = []
      # 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

Fabien Morin's avatar
Fabien Morin committed
350 351 352
      service          = model_line.getResourceValue()
      title            = model_line.getTitleOrId()
      int_index        = model_line.getFloatIndex()
353
      base_amount_list = model_line.getBaseAmountList()
354
      resource         = service.getRelativeUrl()
Fabien Morin's avatar
Fabien Morin committed
355

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

      base_category_list = model_line.getVariationBaseCategoryList()
364
      category_list_list = []
365
      for base_cat in base_category_list:
366 367 368 369
        category_list = model_line.getVariationCategoryList(
                                        base_category_list=base_cat)
        category_list_list.append(category_list)
      cartesian_product = cartesianProduct(category_list_list)
370

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

Fabien Morin's avatar
Fabien Morin committed
384 385 386 387 388 389 390 391 392
        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]
393
    
Fabien Morin's avatar
Fabien Morin committed
394 395 396 397 398 399 400 401 402 403
        # 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):
            '''
404
              return a cell with the modified values (contained in slice_dict)
Fabien Morin's avatar
Fabien Morin committed
405 406 407 408 409 410 411 412 413 414 415 416
            '''
            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)

417 418 419 420 421 422 423
        # 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
424 425
          pass # that's not a problem :)

426
        else:
Fabien Morin's avatar
Fabien Morin committed
427 428 429
          model_slice_min = model_slice.getQuantityRangeMin()
          model_slice_max = model_slice.getQuantityRangeMax()

430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447
        ######################
        # 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
448
        if getattr(paysheet, script_name, None) is None:
449 450
          raise ValueError, "Unable to find `%s` calculation script" % \
                                                           script_name
Fabien Morin's avatar
Fabien Morin committed
451
        calculation_script = getattr(paysheet, script_name, None)
452 453
        quantity=0
        price=0
454 455
        cell_dict = calculation_script(base_amount_dict=base_amount_dict, 
                                        cell=cell,)
456
        cell_dict.update({'category_list': cell_coordinates})
457

458 459 460 461 462
        if cell_dict.has_key('categories'):
          for cat in cell_dict['categories']:
            if cat not in categories:
              categories.append(cat)

463 464
        quantity = cell_dict['quantity']
        price = cell_dict['price']
465

466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484
        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:
                new_val = round((old_val + quantity*price), precision) 
              base_amount_dict[base_participation][share] = new_val
485 486 487

      if cell_list:
        # create the PaySheetLine
Fabien Morin's avatar
Fabien Morin committed
488
        pay_sheet_line = paysheet.createPaySheetLine(
Fabien Morin's avatar
typo  
Fabien Morin committed
489
                                            title     = title,
490
                                            resource  = resource,
Fabien Morin's avatar
typo  
Fabien Morin committed
491 492 493 494 495
                                            int_index = int_index,
                                            desc      = desc,
                                            base_amount_list = base_amount_list,
                                            cell_list = cell_list,
                                            categories= categories)
496
        pay_sheet_line_list.append(pay_sheet_line)
497

498 499 500

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

    return pay_sheet_line_list
506

507
  def getInheritedObjectValueList(self, portal_type_list):
508
    '''Return a list of all subobjects of the herited model (incuding the
509
      dependencies)
510
    '''
511 512
    model = self.getSpecialiseValue()

513 514
    model_reference_dict = model.getInheritanceModelReferenceDict(
                                   portal_type_list=portal_type_list)
515 516 517 518 519 520

    # add line of base model without reference
    model_dict = model.getReferenceDict(\
        portal_type_list=portal_type_list,
        get_none_reference=1)
    id_list = model_dict.values()
521 522 523 524
    if model_reference_dict.has_key(model.getRelativeUrl()):
      model_reference_dict[model.getRelativeUrl()].extend(id_list)
    else:
      model_reference_dict[model.getRelativeUrl()]=id_list
525 526

    # get sub objects
527 528
    key_list = model_reference_dict.keys()

529 530
    sub_object_list = []

531 532
    for key in key_list:
      id_list = model_reference_dict[key]
533
      model = self.getPortalObject().unrestrictedTraverse(key)
534
      if model is None:
535
        # XXX is it supposed to happen ?
536
        LOG("getInheritedObjectValueList :", 0, "can't find model %s" % key)
537 538 539 540 541 542 543

      for id in id_list:
        object = model._getOb(id)
        sub_object_list.append(object)

    return sub_object_list