PaySheetTransaction.py 21.3 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
                            'createPaySheetLine')
  def createPaySheetLine(self, cell_list, title='', resource='',
146
                         description='', base_contribution_list=None, int_index=None,
147
                         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
                       portal_type='Pay Sheet Line',
                       title=title,
                       description=description,
                       destination=self.getSourceSection(),
                       resource_value=resource_value,
                       destination_section=self.getDestinationSection(),
                       variation_base_category_list=('tax_category',
                                                     'salary_range'),
                       variation_category_list=var_cat_list,
181
                       base_contribution_list=base_contribution_list,
Jérome Perrin's avatar
Jérome Perrin committed
182 183
                       int_index=int_index,
                       **kw)
184

185 186
    # add cells categories to the Pay Sheet Line
    # it's a sort of inheritance of sub-object data
187
    if categories:
188 189
      categories_list = payline.getCategoryList()
      categories_list.extend(categories)
190
      # XXX editing categories directly is wrong !
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 198
      paycell = payline.newCell(base_id=base_id, *cell['category_list'])
      paycell.edit(mapped_value_property_list=('price', 'quantity'),
199 200
                   force_update=1,
                   **cell)
201 202 203
    return payline


Fabien Morin's avatar
Fabien Morin committed
204 205 206
  security.declareProtected(Permissions.AccessContentsInformation,
                          'getEditableModelLineAsDict')
  def getEditableModelLineAsDict(self, listbox, paysheet):
207
    '''
Fabien Morin's avatar
Fabien Morin committed
208 209 210
      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.
211
    '''
Fabien Morin's avatar
Fabien Morin committed
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
    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):
233 234 235
          slice_dict[tax_category] = dict(
                      quantity=line['%s_quantity' % tax_category],
                      price=line['%s_price' % tax_category],)
Fabien Morin's avatar
Fabien Morin committed
236
        else:
237 238
          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
239 240
       
    return model_line_dict
Fabien Morin's avatar
Fabien Morin committed
241

Fabien Morin's avatar
Fabien Morin committed
242 243 244 245 246 247 248 249

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

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

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

    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
273
        
Fabien Morin's avatar
Fabien Morin committed
274 275 276 277 278 279 280
        # 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]
281 282 283
        slice_dict[tax_category] = dict(quantity=cell.getQuantity(),
                                        price=cell.getPrice())

Fabien Morin's avatar
Fabien Morin committed
284 285
    return model_line_dict

Yoshinori Okuji's avatar
Yoshinori Okuji committed
286

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

      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)

301 302
    '''

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

309
    # Get Precision
Fabien Morin's avatar
Fabien Morin committed
310
    precision = paysheet.getPriceCurrencyValue().getQuantityPrecision()
311

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

317 318
    model = paysheet.getSpecialiseValue()

319
    def sortByIntIndex(a, b):
Fabien Morin's avatar
Fabien Morin committed
320
      return cmp(a.getIntIndex(), b.getIntIndex())
321

Fabien Morin's avatar
Fabien Morin committed
322
    # get model lines
323
    portal_type_list = ['Pay Sheet Model Line']
324
    sub_object_list = paysheet.getInheritedObjectValueList(portal_type_list)
325 326
    sub_object_list.sort(sortByIntIndex)
    model_line_list = sub_object_list
327 328 329 330

    pay_sheet_line_list = []

    # main loop : find all informations and create cell and PaySheetLines
331
    for model_line in model_line_list:
Jérome Perrin's avatar
Jérome Perrin committed
332
      cell_list = []
333
      # test with predicate if this model line could be applied
Fabien Morin's avatar
Fabien Morin committed
334 335
      if not model_line.test(paysheet,):
        # This model_line should not be applied
336 337 338
        LOG('ERP5', DEBUG, 'createPaySheetLineList: Model Line %s (%s) will'
            ' not be applied, because predicates does not match' %
            ( model_line.getTitle(), model_line.getRelativeUrl() ))
339 340
        continue

Jérome Perrin's avatar
Jérome Perrin committed
341
      service = model_line.getResourceValue()
342 343 344
      if service is None:
        raise ValueError, 'Model Line %s has no resource' % (
                                        model_line.getRelativeUrl())
Jérome Perrin's avatar
Jérome Perrin committed
345 346 347
      title = model_line.getTitleOrId()
      int_index = model_line.getFloatIndex()
      resource = service.getRelativeUrl()
348
      base_contribution_list = model_line.getBaseContributionList()
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
      
      # get the service provider, either on the model line, or using the
      # annotation line reference.
      source_section = None
      source_annotation_line_reference = \
                    model_line.getSourceAnnotationLineReference()
      if model_line.getSource():
        source_section = model_line.getSource()
      elif source_annotation_line_reference:
        for annotation_line in paysheet.contentValues(
                                    portal_type='Annotation Line'):
          annotation_line_reference = annotation_line.getReference() \
                                           or annotation_line.getId()
          if annotation_line_reference == source_annotation_line_reference \
              and annotation_line.getSource():
            source_section = annotation_line.getSource()
            break
Fabien Morin's avatar
Fabien Morin committed
366

367
      if model_line.getDescription():
368 369 370 371 372
        desc = model_line.getDescription()
        # if the model_line description is empty, the payroll service
        # description is used
      else:
        desc = service.getDescription()
373 374

      base_category_list = model_line.getVariationBaseCategoryList()
375
      category_list_list = []
376
      for base_cat in base_category_list:
377 378 379 380
        category_list = model_line.getVariationCategoryList(
                                        base_category_list=base_cat)
        category_list_list.append(category_list)
      cartesian_product = cartesianProduct(category_list_list)
381

Fabien Morin's avatar
Fabien Morin committed
382 383
      share = None
      slice = 'no_slice'
384
      indice = 0
385
      categories = []
386
      for cell_coordinates in cartesian_product:
387
        indice += 1
388
        cell = model_line.getCell(*cell_coordinates)
389
        if cell is None:
390 391 392
          LOG('ERP5', INFO, "Can't find the cell corresponding to those cells"
              " coordinates : %s" % cell_coordinates)
          # XXX is it enough to log ?
393 394
          continue

Fabien Morin's avatar
Fabien Morin committed
395 396 397 398 399 400 401 402 403
        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]
404
    
Fabien Morin's avatar
Fabien Morin committed
405 406 407 408 409 410 411 412 413 414
        # 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):
            '''
415
              return a cell with the modified values (contained in slice_dict)
Fabien Morin's avatar
Fabien Morin committed
416 417 418 419 420 421 422 423 424 425 426 427
            '''
            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)

428 429 430 431 432 433 434
        # 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
435 436
          pass # that's not a problem :)

437
        else:
Fabien Morin's avatar
Fabien Morin committed
438 439 440
          model_slice_min = model_slice.getQuantityRangeMin()
          model_slice_max = model_slice.getQuantityRangeMax()

441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
        ######################
        # 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
459
        if getattr(paysheet, script_name, None) is None:
460 461
          raise ValueError, "Unable to find `%s` calculation script" % \
                                                           script_name
Fabien Morin's avatar
Fabien Morin committed
462
        calculation_script = getattr(paysheet, script_name, None)
463 464
        quantity=0
        price=0
Jérome Perrin's avatar
Jérome Perrin committed
465 466
        cell_dict = calculation_script(base_amount_dict=base_amount_dict,
                                       cell=cell,)
467
        cell_dict.update({'category_list': cell_coordinates})
468

469 470 471 472 473
        if cell_dict.has_key('categories'):
          for cat in cell_dict['categories']:
            if cat not in categories:
              categories.append(cat)

474 475
        quantity = cell_dict['quantity']
        price = cell_dict['price']
476

477
        if quantity and price:
478 479
          cell_list.append(cell_dict)

480 481
          # update the base_contribution
          for base_contribution in base_contribution_list:
482
            if quantity:
483 484 485
              if base_amount_dict.has_key(base_contribution) and \
                  base_amount_dict[base_contribution].has_key(share):
                old_val = base_amount_dict[base_contribution][share]
486 487 488
              else:
                old_val = 0
              new_val = old_val + quantity
489 490
              if not base_amount_dict.has_key(base_contribution):
                base_amount_dict[base_contribution]={}
491 492

              if price:
Jérome Perrin's avatar
Jérome Perrin committed
493
                new_val = round((old_val + quantity*price), precision)
494
              base_amount_dict[base_contribution][share] = new_val
495 496 497

      if cell_list:
        # create the PaySheetLine
Fabien Morin's avatar
Fabien Morin committed
498
        pay_sheet_line = paysheet.createPaySheetLine(
Jérome Perrin's avatar
Jérome Perrin committed
499 500
                                            title=title,
                                            resource=resource,
501
                                            source_section=source_section,
Jérome Perrin's avatar
Jérome Perrin committed
502 503
                                            int_index=int_index,
                                            desc=desc,
504
                                            base_contribution_list=base_contribution_list,
Jérome Perrin's avatar
Jérome Perrin committed
505 506
                                            cell_list=cell_list,
                                            categories=categories)
507
        pay_sheet_line_list.append(pay_sheet_line)
508

509 510 511

    # this script is used to add a line that permit to have good accounting 
    # lines
512
    post_calculation_script = paysheet._getTypeBasedMethod('postCalculation')
513 514
    if post_calculation_script:
      post_calculation_script()
515 516

    return pay_sheet_line_list
517

518
  def getInheritedObjectValueList(self, portal_type_list, property_list=()):
519
    '''Return a list of all subobjects of the herited model (incuding the
520 521 522
      dependencies).
      If property_list is provided, only subobjects with at least one of those
      properties is defined will be taken into account
523
    '''
524
    model = self.getSpecialiseValue()
525
    model_reference_dict = model.getInheritanceModelReferenceDict(
526 527
                                   portal_type_list=portal_type_list,
                                   property_list=property_list)
528 529

    sub_object_list = []
530 531 532 533
    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])
534 535 536

    return sub_object_list