# # Copyright (C) 2002-2013 Corporation of Balclutha. All rights Reserved. # # 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. # # 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 # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT # OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # import stuff import AccessControl, types, string, os, operator, logging from AccessControl.Permissions import view_management_screens, manage_properties, \ access_contents_information from DateTime import DateTime from Acquisition import aq_base from OFS.userfolder import manage_addUserFolder from AccessControl.User import BasicUser from Products.PageTemplates.PageTemplateFile import PageTemplateFile from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate from Products.AdvancedQuery import Between, In, Eq from Products.BastionBanking.ZCurrency import ZCurrency from Products.CMFCore.permissions import View from utils import _mime_str, assert_currency, floor_date from BLGlobals import EPOCH from BLBase import * from BLReport import BLReport from BLSubsidiaryLedger import BLSubsidiaryLedger from BLSubsidiaryAccount import BLSubsidiaryAccount from BLEntry import BLEntry from BLSubsidiaryEntry import BLSubsidiaryEntry from BLEntryTemplate import BLEntryTemplate from BLTransaction import BLTransaction from BLProcess import manage_addBLProcess from Permissions import ManageBastionLedgers, OperateBastionLedgers, setDefaultRoles from Products.CMFCore import permissions from BLAccount import BLAccounts, addBLAccount from Exceptions import UnexpectedTransaction from zope.interface import implements from interfaces.payroll import IPaySlip, ITimesheetSlot LOG = logging.getLogger('BLPayroll') # this is as per DateTime ... _daymap ={'sunday': 1, 'sun': 1, 'monday': 2, 'mon': 2, 'tuesday': 3, 'tues': 3, 'tue': 3, 'wednesday': 4, 'wed': 4, 'thursday': 5, 'thurs': 5, 'thur': 5, 'thu': 5, 'friday': 6, 'fri': 6, 'saturday': 7, 'sat': 7} def select_day_of_week(): return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] manage_addBLPayrollForm = PageTemplateFile('zpt/add_payroll', globals()) def manage_addBLPayroll(self, id, controlAccount, timesheet_day='Friday', title='', REQUEST=None): """ adds a user container """ try: # do some checking ... if type(controlAccount) == types.StringType: control = self.Ledger._getOb(controlAccount) else: control = controlAccount assert control.meta_type =='BLAccount', "Incorrect Control Account Type - Must be strictly GL" #assert not getattr(aq_base(control), id, None),"A Subsidiary Ledger Already Exists for this account." self._setObject(id, BLPayroll(id, title, control, timesheet_day, control.base_currency)) except: # TODO: a messagedialogue .. raise if REQUEST is not None: return self.manage_main(self, REQUEST) return id def addBLPayroll(self, id, title='', REQUEST=None): """ Plone Add """ control = addBLAccount(self.Ledger, id, title) self.manage_addBLPayroll(id, control, title=title) return id class BLPayroll(BLSubsidiaryLedger, CalendarSupport): """ A simple payroll ledger incorporating automated scheduling of execution """ meta_type = portal_type = 'BLPayroll' implements(ITimesheetSlot) __ac_permissions__ = BLSubsidiaryLedger.__ac_permissions__ + ( (access_contents_information, ('selectTimesheetDay', 'payrollDay', 'payrollDayNumber', 'getDay', 'isTimesheetsRequired', 'get_size', 'superannuationEmployees', 'superannuationSum', 'timesheetDayNumber', 'getTimesheet', 'getTimesheetDay', 'getTimesheets', )), (view, ('getPayrollDay', 'selectDayOfWeek', 'getPayroll', 'getTimesheetSlots')), (OperateBastionLedgers, ('manage_payrolls', 'manage_taxCertificates', 'isRun', 'manage_config', 'manage_accounts', 'manage_payEmployees', 'manage_timesheet', ) ), (ManageBastionLedgers, ('manage_edit',)), ) #dontAllowCopyAndPaste = 1 account_types = ('BLEmployee',) manage_options = ( {'label': 'Payroll', 'action': 'manage_payrolls', 'help':('BastionLedger', 'payroll.stx') }, {'label': 'View', 'action':'',}, {'label': 'Config', 'action': 'manage_config' }, #{'label': 'Extensions', 'action': 'manage_common_sheet' }, {'label': 'Employees', 'action': 'manage_accounts' }, ) + BLSubsidiaryLedger.manage_options[3:] # we've got a required macro here manage_ledgerPropsForm = BLSubsidiaryLedger.manage_propertiesForm manage_payrolls = manage_main = PageTemplateFile('zpt/view_payrolls', globals()) manage_propertiesForm = PageTemplateFile('zpt/payroll_props', globals()) try: manage_payrolls._setName('manage_payrolls') except: pass manage_config = PortalFolder.manage_main try: manage_config._setName('manage_config') except: pass #manage_main._setName('manage_main') def __init__(self, id, title, control, timesheet_day, currencies, account_prefix='E', txn_prefix='P'): BLSubsidiaryLedger.__init__(self, id, title, (control,), currencies, 1000000, account_prefix, 1, txn_prefix ) self.timesheet_day = timesheet_day self.timesheets_required = 0 def all_meta_types(self): """ """ #return [ ProductsDictionary('BLEmployee'), # ProductsDictionary('BLTransactionTemplate'), # ProductsDictionary('BLTimesheetSlot'), # ProductsDictionary('Page Template') ] return [ ProductsDictionary('BLEmployee'), ProductsDictionary('BLSubsidiaryTransaction'), ProductsDictionary('BLTimesheetSlot'), ProductsDictionary('Script (Python)'), ProductsDictionary('Page Template') ] def selectTimesheetDay(self): """ return the selection day - TODO? WFT .. """ return select_timesheet_day() def payrollDay(self): """ the week day which the payroll will be run """ return self.timesheet_day def payrollDayNumber(self): """ maps timesheet day to Plone Calendar day """ number = _daymap[self.timesheet_day.lower()] - 2 if number < 0: return number + 7 return number def getDay(self, day): if day.Day() == self.timesheet_day: return '<a href="manage_main?date:date=%s"><strong>%s</strong></a>' % \ (day.strftime('%Y/%m/%d'), day.day()) return day.day() def getPayroll(self, day): """ returns a hash keyed on employee id of payroll transactions for the closest payroll date to day """ day = self.getPayrollDay(day) entries = {} for txn in map(lambda x: x.getObject(), self.bastionLedger().evalAdvancedQuery(Eq('ledgerId', self.getId(), filter=True) & \ In('meta_type', self.transaction_types) & \ Between('effective', day-1, day+1) & \ In('tags', ('payroll',)))): # there should only be one subsidiary entry ... sub_entries = txn.objectValues('BLSubsidiaryEntry') if sub_entries: employee_id = sub_entries[0].blAccount().getId() entries[employee_id] = txn return entries def getPayrollDay(self, date): # normalise and snap to grid ... day = DateTime(date.year(), date.month(), date.day()) timesheet_day = self.timesheet_day while day.Day() != timesheet_day: day = day - 1 return day def selectDayOfWeek(self): return select_day_of_week() def isRun(self, date=None, REQUEST=None): """ returns whether or not the payroll has been run for this (weekly) period """ date = date or DateTime() return len(self.bastionLedger().evalAdvancedQuery(Eq('ledgerId', self.getId(), filter=True) & \ In('meta_type', self.transaction_types) & \ Between('effective', date-6, date+1) & \ In('tags', ('payroll',)))) > 0 def manage_payEmployees(self, ids=[], effective=None, force=False, REQUEST=None): """ generate and post the payroll txn by running the 'Payable' template """ effective = effective or DateTime() LOG.debug( "BLPayroll::manage_payEmployees(%s)" % effective) if self.isRun(effective) and not force: if REQUEST is not None: REQUEST.set('manage_tabs_message', 'Payroll Already Run!') return self.manage_main(self, REQUEST) else: return employees = filter(lambda x,ids=ids: x.getId() in ids, self.accountValues()) # filter non-timesheet approved employees ... if self.timesheets_required: employees = filter(lambda e,date=effective: e.getTimesheetStatus(date) == 'approved', employees) # # call our payroll transaction template (this also automatically posts)... # note that payslip processing requires a separate transaction per employee ... # payslip_id = effective.strftime('%Y-%m-%d') zero = self.zeroAmount() for employee in employees: txn = self.blp_employee_payable.generate(accounts=[employee], title='Auto - Payroll', effective=effective, tags=['payroll']) employee._setObject(payslip_id, BLPaySlip(payslip_id, payslip_id, txn.getId(), zero, zero, zero)) if REQUEST is not None: REQUEST.set('manage_tabs_message', 'Payroll Processed!') return self.manage_main(self, REQUEST) def isTimesheetsRequired(self): if getattr(aq_base(self), 'timesheets_required', None): return self.timesheets_required else: self.timesheets_required = False return False def get_size(self): return 0 def manage_editTimesheetProps(self, timesheet_day, timesheets_required=False): """ edit extended ledger props """ self.timesheet_day = timesheet_day self.timesheets_required = timesheets_required def superannuationEmployees(self, start_date, end_date): """ return lists of employee accounts with same super fund active within the specified date range """ results = [] employees = list(self.accountValues()) employees.sort(lambda x,y: x.superFund() <= y.superFund()) if employees: active_list = [] fund = '' for employee in employees: employee_fund = employee.superFund() if not employee_fund: continue if active_list and employee_fund != fund: results.append(active_list) active_list = [] fund = employee_fund active_list.append(employee) # append the last one ... if active_list: results.append(active_list) return results def superannuationSum(self, tag, employees, start_date, end_date): """ helper to sum a bunch of employee's super contribution by tag (ie wages_super = company contribution) """ return reduce(operator.add, map(lambda x,tag=tag,start=start_date,end=end_date: x.sum(tag,[start,end]), employees)) def manage_taxCertificates(self, start, end, send=True, REQUEST=None): """ prepare and mailout annual tax certificates to employees who've worked for you this year """ mailhost = None if send: try: mailhost = self.superValues(['Mail Host', 'Secure Mail Host'])[0] except: # TODO - exception handling ... if REQUEST: REQUEST.set('manage_tabs_message', 'No Mail Host Found') return self.manage_main(self, REQUEST) raise if not getattr(self, 'blemployee_tax_certificate', None) or \ not getattr(self, 'blpayroll_payg_summary', None): LOG.info('No Tax certificate processing, returning') return counter = 0 reports = self.Reports id = end.strftime('%Y-%m-%d') title_date = end.strftime('%d %b %Y') if getattr(reports, 'payg-payment-summary-%s' % id, None): reports._delObject('payg-payment-summary-%s' % id) reports._setObject('payg-payment-summary-%s' % id, BLReport('payg-payment-summary-%s' % id, 'PAYG payment summary %s' % title_date, end, self.blpayroll_payg_summary(startdate=start, enddate=end).encode('ascii', 'ignore'), tags=('EOP',))) subject = '%s - Tax Certificate' % self.aq_parent.title payer_options = {'purpose':'Payer Tax Return Copy', 'startdate':start, 'enddate': end, 'message':"""Must be sent to the Taxation Office by the Payer along with the PAYG payment summary statement."""} personal_options = {'purpose':'Payee Personal Record Copy', 'startdate':start, 'enddate': end, 'message':"""This copy is to be kept by the Payee for the Payee's records only. <b>Do not send this copy to the Taxation Office</b>."""} tax_office_options = {'purpose':'Payee Tax Return Copy', 'startdate':start, 'enddate': end, 'message':"""Must be sent to the Taxation Office by the Payee in accordance with the notes supplied with this certificate."""} # TODO - figure out which employees ... for employee in self.accountValues(status='open'): counter += 1 if getattr(reports, 'tax_certificate-%s-%s' % (employee.getId(), id), None): reports._delObject('tax-certificate-%s-%s' % (employee.getId(), id)) reports._setObject('tax-certificate-%s-%s' % (employee.getId(), id), BLReport('tax-certificate-%s-%s' % (employee.getId(), id), 'Tax Cert - %s %s' % (employee.title, title_date), end, employee.blemployee_tax_certificate(**payer_options).encode('ascii', 'ignore'), tags=('EOP',))) if getattr(aq_base(employee), 'tax-certificate-%s' % id, None): employee._delObject('tax-certificate-%s' % id) tax_office_text = employee.blemployee_tax_certificate(**tax_office_options).encode('ascii', 'ignore') employee._setObject('tax-certificate-%s' % id, BLReport('tax-certificate-%s' % id, 'Tax Certificate %s' % title_date, end, tax_office_text, tags=('EOP',))) if getattr(aq_base(employee), 'tax-certificate-copy-%s' % id, None): employee._delObject('tax-certificate-copy-%s' % id) personal_text = employee.blemployee_tax_certificate(**personal_options).encode('ascii', 'ignore') employee._setObject('tax-certificate-copy-%s' % id, BLReport('tax-certificate-copy-%s' % id, 'Tax Certificate - COPY %s' % title_date, end, personal_text, tags=('EOP',))) if send: mailhost.send(_mime_str({'Subject':subject, 'From':self.email, 'To':employee.email,}, '', [('personal.html', 'text/html', personal_text), ('tax_office.html', 'text/html', tax_office_text)]), [employee.email], self.email, subject) if REQUEST: REQUEST.set('manage_tabs_message', 'Processed %i tax certificates' % counter) return self.manage_main(self, REQUEST) def timesheetDayNumber(self): return _daymap[self.timesheet_day.lower()] def getTimesheet(self, day): day = self.getTimesheetDay(day) try: return self.getEventsForDay(day)[0] except IndexError: # bugger - no record, best create one ... LOG.debug( "BLEmployeeTimesheetFolder::getTimesheet doing generateEventId ...") timesheet = BLTimesheet(_generateEventId(self), '', day, [], self.getTimesheetSlots()) self._setObject(timesheet.getId(), timesheet) return timesheet except: raise raise KeyError, "not found for day: %s" % day def manage_timesheet(self, date, entries, REQUEST=None): """ add/update timesheet records ... """ timesheet = self.getTimesheet(date) timesheet.update(entries) if REQUEST is not None: REQUEST.RESPONSE.redirect(REQUEST['URL1']) def getTimesheetDay(self, date=DateTime()): """ return the week day of the nearest previous timesheet posting day """ # normalise and snap to grid ... day = DateTime(date.year(), date.month(), date.day()) while day.Day() != self.timesheet_day: day = day - 1 return day def getTimesheetSlots(self): """ retrieve from Payroll parameters ... """ slots = [] if getattr(aq_base(self), 'Config', None): slots.extend(map(lambda x: x.__of__(self), self.Config.objectValues('FSTimesheetSlot'))) slots.extend(self.objectValues('BLTimesheetSlot')) return slots def getTimesheets(self, datemin, datemax): # query catalog ... # # we need to figure out the correct catalog query ... # return map(lambda x: x.getObject(), self.searchResults(meta_type='BLTimeSheet', effective={'query':[datemin, datemax], 'range':'minmax'})) def _repair(self): BLSubsidiaryLedger._repair(self) for txn in self.transactionValues(): if txn.title == 'Auto - Payable': txn._updateProperty('tags', ('payroll',)) # TODO - main ledger _repair isn't doing this ... #self.manage_reindexIndex(['tags']) AccessControl.class_init.InitializeClass(BLPayroll) manage_addBLEmployeeForm = PageTemplateFile('zpt/add_employee', globals()) def manage_addBLEmployee(self, title, currency, start_day, description='',uid='', pwd='', id='', type='', REQUEST=None): """ an employee """ self = self.this() if not id: id = self.nextAccountId() self._setObject(id, BLEmployee(id, title, description, type or self.accountType(), currency, start_day)) # make this user the owner ... employee = getattr(self, id) if uid: if not isinstance(uid, BasicUser): assert pwd, 'user password required' manage_addUserFolder(employee) acl_users = employee.acl_users acl_users.userFolderAddUser(uid, pwd, [], []) uid = employee.acl_users.getUser(uid).__of__(acl_users) employee.changeOwnership(uid) employee.manage_setLocalRoles(id, ['Owner']) bt = getToolByName(self, 'portal_bastionledger') if bt.hasTaxTable('personal_tax'): employee.manage_addTaxGroup('personal_tax') if REQUEST is not None: REQUEST.RESPONSE.redirect("%s/%s/manage_main" % (REQUEST['URL3'], id)) else: return employee def addBLEmployee(self, id, title=''): """ Plone Constructor """ employee = manage_addBLEmployee(self, id=id, title=title, currency=self.currencies[0], start_day=DateTime()) id = employee.getId() return id class BLPaySlip(PortalContent): """ encapsulate a payslip - which in turn saves a copy of a payroll transaction so it can be live isolated from any transaction purging regime """ meta_type = portal_type = 'BLPaySlip' implements(IPaySlip) default_view = 'blemployee_payslip' __ac_permissions__ = ( (view_management_screens, ('manage_main',)), (OperateBastionLedgers, ('validTxnId',)), (ManageBastionLedgers, ('calculateRunningTotals',)), (access_contents_information, ('gross', 'net', 'tax', 'effective', 'blTransaction')), ) + PortalContent.__ac_permissions__ _properties = PortalContent._properties + ( {'id':'txn_id', 'type':'string', 'mode':'w'}, {'id':'gross_to_date', 'type':'currency', 'mode':'r'}, {'id':'tax_to_date', 'type':'currency', 'mode':'r'}, {'id':'net_to_date', 'type':'currency', 'mode':'r'}, ) manage_options = ( {'label':'Payslip', 'action':'manage_main' }, {'label':'Properties', 'action':'manage_propertiesForm',}, {'label':'View', 'action':'' }, ) + PortalContent.manage_options manage_main = PageTemplateFile('zpt/view_payslip', globals()) def __init__(self, id, title, txn_id, gross_to_date, tax_to_date, net_to_date): PortalContent.__init__(self, id, title) self.txn_id = txn_id self.gross_to_date = gross_to_date self.tax_to_date = tax_to_date self.net_to_date = net_to_date def _updateProperty(self, name, value): PortalContent._updateProperty(self, name, value) if name == 'txn_id' and not self.validTxnId(): raise UnexpectedTransaction, value def calculateRunningTotals(self, starting_date=None): """ go do our summated totals """ txn = self.blTransaction() if txn: employee = self.aq_parent if not starting_date: starting_date = employee.openingDate() dt_range = (starting_date, txn.effective()) self.gross_to_date = employee.sum(self.Ledger.accountIds(tags='wages_exp')[0], dt_range) # net is a bit naf - we're really just trying to exclude any payments self.net_to_date = employee.sum(employee.getId(), dt_range, debits=False, credits=True) self.tax_to_date = employee.sum(self.Ledger.accountIds(tags='wages_tax')[0], dt_range) else: self.gross_to_date = self.net_to_date = self.tax_to_date = self.zeroAmount() def effective(self): """ """ txn = self.blTransaction() if txn: return txn.effective() return DateTime(self.created()) def gross(self): """ """ txn = self.blTransaction() if txn: gross_account = self.Ledger.accountValues(tags='wages_exp')[0] try: return txn.blEntry(gross_account.getId()).absAmount() except: raise UnexpectedTransaction, '%s - %s' % (self.getId(), txn) return self.zeroAmount() def net(self): """ """ txn = self.blTransaction() if txn: try: return txn.blEntry(self.aq_parent.getId()).absAmount() except: raise UnexpectedTransaction, '%s - %s' % (self.getId(), txn) return self.zeroAmount() def tax(self): """ get the tax entry (which won't be present if tax isn't payable) """ txn = self.blTransaction() if txn: tax_accounts = self.Ledger.accountValues(tags='wages_tax') if tax_accounts: e = txn.blEntry(tax_accounts[0].getId()) if e: return e.amount return self.zeroAmount() def validTxnId(self): """ hmmm - some of these are screwed .... but we're just checking that it looks like a payroll payment """ txn = self.blTransaction() if not txn: return False # tax isn't compulsory ... #for tag in ('wages_tax', 'wages_exp'): for tag in ('wages_exp',): accounts = self.Ledger.accountValues(tags=tag) if accounts: try: entry = txn.blEntry(accounts[0].getId()) except: return False try: entry = txn.blEntry(self.aq_parent.getId()) return True except: pass return False def __cmp__(self, other): """ allow participation in objectItemsByDate ... """ if isinstance(other, BLPaySlip): x = self.effective() y = other.effective() if x > y: return -1 elif x < y: return 1 return 0 return 1 def blTransaction(self): """ return underlying transaction supporting this payslip, or None if not found - ie an old period txn etc etc """ return self.aq_parent.aq_parent._getOb(self.txn_id, None) def accountId(self): return self.aq_parent.getId() def transactionId(self): return self.txn_id def _repair(self): self.calculateRunningTotals() AccessControl.class_init.InitializeClass(BLPaySlip) class BLEmployee(BLSubsidiaryAccount): meta_type = portal_type = 'BLEmployee' Basis = [ 'Hour', 'Day', 'Month', 'Year' ] __ac_permissions__ = BLSubsidiaryAccount.__ac_permissions__ + ( (access_contents_information, ('blTransaction', 'entryTemplateValues',)), (view, ('superFund', 'getTimesheetStatus', 'grossBreakDown', 'gross', 'manage_editPublic')), (permissions.ModifyPortalContent, ('manage_deletePayslips',)), (view_management_screens, ('manage_details',)), (OperateBastionLedgers, ('manage_editPrivate', 'setTimesheetsProcessed', 'manage_recalculatePayslips')), ) def manage_options(self): options = [ {'label': 'Payslips', 'action': 'manage_main', 'help':('BastionLedger', 'employee.stx') }, {'label': 'Deductions', 'action': 'manage_entries' } ] options.extend(BLSubsidiaryAccount.manage_options(self)) return options manage_details = PageTemplateFile('zpt/edit_employee', globals()) manage_main = PageTemplateFile('zpt/view_payslips', globals()) def __init__(self, id, title, description, type, currency, start_day=DateTime(), department='', email='', contact='', address='', phone='', salary=0, rate=0, basis='Hour', tax_num='', tax_code='', accrued_leave=0.0, bank_acc='', dob=EPOCH, sick_days=0.0, super_number='', super_details=''): BLSubsidiaryAccount.__init__(self, id, title, description, type, 'Payroll',currency, id) BLEmployee.manage_editPublic(self, title, description, email, address, phone, tax_num, bank_acc, dob, super_number, super_details) BLEmployee.manage_editPrivate(self, start_day, department, salary, rate, basis, tax_code, accrued_leave, sick_days) def all_meta_types(self): # deductions ... return [ ProductsDictionary('BLEntryTemplate') ] def manage_recalculatePayslips(self, start=None, REQUEST=None): """ regenenerate payslip running balances from the date (or opening date) """ count = 0 for payslip in map(lambda x: x.getObject(), self.bastionLedger().searchResults(meta_type='BLPaySlip', accountId=self.getId(), effective={'query': start or self.openingDate(), 'range': 'min'})): payslip.calculateRunningTotals() count += 1 if REQUEST: REQUEST.set('manage_tabs_message', 'Recalculated %i payslips' % count) return self.manage_main(self, REQUEST) def manage_editPublic(self, title='', description='', email='', address='', phone='', tax_number='', bank_account='', date_of_birth=EPOCH, super_number='', super_details='', REQUEST=None): """ edit the publicly associated attributes """ self.title = title self.description = description self.email = email self.address = address self.phone = phone self.tax_number = tax_number self.bank_account = bank_account if not isinstance(date_of_birth, DateTime): date_of_birth = DateTime(date_of_birth) self.date_of_birth = date_of_birth self.super_number = super_number self.super_details = super_details if REQUEST is not None: REQUEST.set('management_view', 'Details') return self.manage_details(self, REQUEST) return 0 def manage_editPrivate(self, start_day=DateTime(), department='', salary=0, rate=0, basis='Hour', tax_code='', accrued_leave=0.0, sick_days=0.0, REQUEST=None): """ edit details that affect payroll calculations """ if not isinstance(start_day, DateTime): start_day = DateTime(start_day) self.start_day = start_day self.department = department try: assert_currency(salary) except: salary = ZCurrency(salary, self.base_currency) self.salary = salary try: assert_currency(rate) except: rate = ZCurrency(rate, self.base_currency) self.hourly_rate = rate self.basis = basis self.tax_code = tax_code self.accrued_leave = accrued_leave self.sick_days = sick_days if REQUEST is not None: REQUEST.set('management_view', 'Details') return self.manage_details(self, REQUEST) return 0 def entryTemplateValues(self): """ """ return self.objectValues('BLEntryTemplate') def superFund(self): """ return the super fund name (ie first line of super_details) """ if self.super_details: return self.super_details.split('\n')[0] def ___repr__(self): return str(self.__dict__) def blTransaction(self, entry_id): return self._getOb(entry_id) def getTimesheetStatus(self, date): if self.aq_parent.isTimesheetsRequired(): return self.Timesheets.getTimesheet(date).status() # # otherwise return OK Timesheet status ... # return 'approved' def grossBreakDown(self, datemin, datemax): slots = self.aq_parent.objectValues('BLTimesheetSlot') results = {} for slot in slots: results[slot.getId()] = ZCurrency(self.base_currency, 0) for timesheet in self.Timesheets.getTimesheets(datemin, datemax): for slot in slots: # # There's something f**ked with the ZCurrency class mathematics ... # results[slot.getId()] += ZCurrency(self.base_currency, self.hourly_rate().amount() * slot.ratio() * timesheet.sum(slot.getId())) return results def gross(self, datemin, datemax): """ the total employee income for specified date range """ total = ZCurrency(self.base_currency, 0) for amt in self.grossBreakDown(datemin, datemax).values(): LOG.debug( "BLEmployee::gross() adding %s" % amt) total += amt LOG.debug( "BLEmployee::gross() - %s" % total) #return reduce( operator.add, self.grossBreakDown(datemin, datemax).values() ) return total def setTimesheetsProcessed(self, datemin, datemax): for timesheet in self.Timesheets.getTimesheets(datemin, datemax): timesheet._status('processed') def manage_deletePayslips(self, ids=[], REQUEST=None): """ """ for id in ids: try: ob = self._getOb(id) if ob.meta_type == 'BLPaySlip': self._delObject(id) except (KeyError, AttributeError): pass if REQUEST: return self.manage_main(self, REQUEST) def _repair(self): BLSubsidiaryAccount._repair(self) if not getattr(aq_base(self), 'basis', None): self.basis = 'Hour' for payslip in self.objectValues('BLPaySlip'): payslip._repair() AccessControl.class_init.InitializeClass(BLEmployee) manage_addBLEmployeeEntryTemplateForm = PageTemplateFile('zpt/add_emplentrytmpl', globals()) def manage_addBLEmployeeEntryTemplate(self, id, currency, account, REQUEST=None): """ Add an entry to a transaction ... """ ac = self.Ledger._getOb(account) assert 'empl_dedtn' in ac.tags, 'Invalid Account: %s' % account self._setObject(id, BLEmployeeEntryTemplate(id, ac.title, currency, account)) if REQUEST is not None: REQUEST.RESPONSE.redirect("%s/%s/manage_workspace" % (REQUEST['URL3'], id)) return id class BLEmployeeEntryTemplate (BLEntryTemplate): """ A restricted employee deduction template """ meta_type = portal_type = 'BLEmployeeEntryTemplate' __setstate__ = BLEntryTemplate.__setstate__ _params='employee' def __init__(self, id, title, currency, account): BLEntryTemplate.__init__(self, id, title, currency, account) self.write("""# # return an amount (possibly calculated ...) # from Products.BastionBanking.ZCurrency import ZCurrency gross = context.Gross.amount tax = context.Tax.amount return ZCurrency(script.currency, 0.00) """) def __call__(self, employee): # # we expect to receive a currency from which to build up a 'safe' entry - ie one # which we can be reasonably confident that our employee isn't going to stiff us # with something inappropriate ... # # we are return both the debit and credit side - the credit side will automatically # aggregate with the employee's net # currency = BLEntryTemplate.__call__(self, employee) assert_currency(currency) ac = self.Ledger._getOb(self.account) currency = abs(currency) if ac.type in ['Liability', 'Proprietorship', 'Income']: currency = -currency return [ BLEntry(self.account, self.title, 'Ledger/%s' % self.account, currency, self.aq_parent.aq_parent.getId()) ] AccessControl.class_init.InitializeClass(BLEmployeeEntryTemplate) manage_addBLTimesheetSlotForm = PageTemplateFile('zpt/add_timesheetslot', globals()) def manage_addBLTimesheetSlot(self, id, ratio=1.0, max_hrs=8, min_hrs=0, defaults=[0,0,0,0,0,0,0], title='', REQUEST=None): """ define and verify timesheet slots, their acceptable values, and actual payment amounts """ try: self._setObject(id, BLTimesheetSlot(id, title, ratio, max_hrs, min_hrs, defaults)) except: # TODO: a messagedialogue .. raise if REQUEST is not None: #REQUEST.RESPONSE.redirect("%s/%s/manage_workspace" % (REQUEST['URL3'], id)) return self.manage_main(self, REQUEST) return id class BLTimesheetSlot( PortalContent ): """ This class provides a mechanism to support user-defined semantics between accounts - principally by maintaining a list for this 'id' tag. It is reasonably context-independent, but it does expect to find a GL with the list of available accounts using it's accountContainer method """ meta_type = portal_type = 'BLTimesheetSlot' __ac_permissions__ = PortalContent.__ac_permissions__ + ( (ManageBastionLedgers, ('manage_edit',)), ) _properties = ( {'id':'ratio', 'type':'float', 'mode':'w'}, {'id':'min_hrs', 'type':'float', 'mode':'w'}, {'id':'max_hrs', 'type':'float', 'mode':'w'}, ) manage_options = ( {'label': 'Details', 'action': 'manage_main', }, ) + PortalContent.manage_options manage_main = PageTemplateFile('zpt/edit_timesheetslot', globals()) def __init__(self, id, title, ratio, max_hrs, min_hrs, defaults): self.id = id self.manage_edit(title, ratio, max_hrs, min_hrs, defaults) def manage_edit(self, title, ratio, max_hrs, min_hrs, defaults, REQUEST=None): """ """ self.title = title self.ratio = ratio self.min_hrs = min_hrs self.max_hrs = max_hrs self.defaults = defaults if REQUEST is not None: return self.manage_main(self, REQUEST) AccessControl.class_init.InitializeClass(BLTimesheetSlot) manage_addBLTimesheetForm = PageTemplateFile('zpt/add_emptimesheet', globals()) def manage_addBLTimesheet(self, date, entries, id=None, REQUEST=None): """ Add a timesheet """ self = self.this() if not id: id = date.strftime('timesheet-%Y-%m-%d') # check unique date ... ts = getattr(aq_base(self),id,None) if ts: if REQUEST is not None: REQUEST.RESPONSE.redirect('%s/manage_main?manage_tabs_message=Already+Submitted (%s)!!' % ( REQUEST['URL3'], id)) return else: raise KeyError, "already submitted (%s)!!" % id self._setObject(id, BLTimesheet(id, date.strftime('%Y-%m-%d'), date, entries)) if REQUEST: REQUEST.RESPONSE.redirect('%s/manage_main' % REQUEST['URL3']) return id class BLTimesheet (PortalContent): """ the record attribute is a list (corresponding to slots) of hashes containing keys of day and slot-id the slot-id's value is a list of hours corresponding to each week day """ meta_type = portal_type = 'BLTimesheet' __ac_permissions__ = ( (access_contents_information, ('sum', 'normalisedTime', 'startDay', 'endDay', 'slots', 'getTimesheetSlots')), (OperateBastionLedgers, ('manage_edit', 'manage_approve', 'manage_reject')), # this is wrong ... ) + PortalContent.__ac_permissions__ _properties = PortalContent._properties + ( {'id':'day', 'type':'date', 'mode':'w'}, ) manage_options = ( {'label':'Timesheet', 'action':'manage_main'}, {'label':'Properties', 'action':'manage_propertiesForm'}, ) + PortalContent.manage_options manage_main = PageTemplateFile('zpt/view_emptimesheet', globals()) def __init__(self, id, title, date, entries=[], slots=None): self.id = id self.title = title self.day = date self.records = entries if self.records == []: assert slots != None, "Slots must be applied if no entries!" for i in range(0, 7): rec = {'day':self.day + (i - 6) } for slot in slots: rec[slot.getId()] = slot.defaults[ rec['day'].dow() ] self.records.append( rec ) def manage_edit(self, entries, REQUEST=None): """ """ # TODO: need some validation code here ... self.records = entries self._status('pending') if REQUEST: REQUEST.set('manage_tabs_message', 'Updated') return self.manage_main(self, REQUEST) def manage_approve(self, REQUEST=None): """ """ assert self.status() == 'approval', "Wrong state: %s " % self.status() self.setStatus('approved') if REQUEST: return self.manage_main(self, REQUEST) def manage_reject(self, REQUEST=None): """ """ assert self.status() == 'Approval', "Wrong state: %s " % self.status() self.setStatus('rejected') if REQUEST: return self.manage_main(self, REQUEST) def setStatus(self, status): # maybe we need to do some auto state changing later ... self._status(status) def sum(self, slot_id): return reduce( operator.add, map( lambda x, y=slot_id: x[y], self.records ) ) def normalisedTime(self): hours = 0.0 for slot in self.getTimesheetSlots(): hours += self.sum(slot.getId()) * slot.ratio return hours def startDay(self): return self.day - 6 def endDay(self): return self.day def slots(self): """ return the slot's that this timesheet has snapshotted for """ return filter(lambda x: x != 'day', self.records[0].keys()) def getTimesheetSlots(self): """ get slots associated with timesheet """ return self.aq_parent.getTimesheetSlots() def __str__(self): """ lazy textual display ... """ str = '' for day in self.records: str += day['day'].strftime('%Y/%m/%d') for slot in filter( lambda x: x != 'day', day.keys() ): str += ' %s (%.1d)' % (slot, day[slot]) str += '\n' return str AccessControl.class_init.InitializeClass(BLTimesheet) def addTimesheetSlots(ob, event): """ add default timesheet slots """ factory = ob.manage_addProduct['BastionLedger'].manage_addBLTimesheetSlot factory('normal', title='Normal', defaults=[ 0, 8.0, 8.0, 8.0, 8.0, 8.0, 0]) factory('leave', title='Leave', defaults=[ 0, 8.0, 8.0, 8.0, 8.0, 8.0, 0]) factory('sick', title='Sickness', defaults=[ 0, 8.0, 8.0, 8.0, 8.0, 8.0, 0]) factory('overtime', title='Overtime', defaults=[8.0, 4.0, 4.0, 4.0, 4.0, 4.0, 8.0]) def addPaySlip(ob, event): """ go do our summated totals """ # OLD-CATALOG try: if not ob.validTxnId(): raise UnexpectedTransaction, (ob.txn_id, str(ob.blTransaction())) except AttributeError: pass try: ob.calculateRunningTotals() except: # TODO - screwed up ZCatalogs .. pass # # these are to be removed once conversions completed ... # class BLEmployeeEntriesFolder(PortalFolder): pass class BLEmployeeTimesheetFolder(PortalFolder): pass