#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# © Hub People 2024› https://www.hubpeople.be · https://www.geezteem.com/455
# AGPL-3.0-or-later - https://www.gnu.org/licenses/agpl-3.0.txt

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
from appy.all import *
from appy.model.tool import Tool as BaseTool

from . import Vars
from .Facet import Facet
from .UserData import UserData
from .journeyman.Tag import Tag
from .group.HpGroup import HpGroup
from .billing.Invoice import Invoice
from .timetracker.syncer import Syncer
from .journeyman.Message import Message
from .billing.PriceScheme import PriceScheme
from .billing.InvoiceData import InvoiceData
from .journeyman.Beneficiary import Beneficiary
from .billing.Publisher import InvoicePublisher

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
HOURSPAN_KO = 'Group "%s" - Hourspan\'s short name "%s" is invalid.'
UPD_MSR     = "Updating Message's master/slave relationships..."
UPD_MSR_OK  = 'Done.'
NIGHT_START = '*** HubPeople nightlife ***'
NIGHT_STS   = 'Status of %s task(s) and/or meet(s) recomputed.'
NIGHT_END   = '*** Done ***'
REFRESH_A   = 'Refreshing all message statuses...'
REFRESH_G   = 'Refreshing message statuses for group "%s" (%s)...'
FACETED     = 'Group %d (%s) contains %d faceted user(s).'
FACETED_NO  = 'There is no faceted user on this site.'

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ttSubject = "Plat-com v2.1.1 · L'export des mensuels en feuille de calcul a " \
            "évolué"

ttBody = '''Bonjour,

Vous recevez ce mail car vous êtes défini·e comme gestionnaire RH du
groupe "%s" sur le workspace "%s" de Plat-com.

La nouvelle version de Plat-com, qui vient d'être déployée sur votre workspace,
apporte des changements concernant l'export des mensuels sous la forme de
feuilles de calcul, telles que transmises à votre secrétariat social.

Deux colonnes supplémentaires y ont été ajoutées.
1. En début de fichier, la première colonne inclut désormais le numéro du
   contrat.
2. L'avant-dernière colonne précise de type de subside.

Ces informations étaient déjà présentes, mais peu lisibles, au sein de la
colonne précédemment nommée « Régime de travail ». Elles ont été enlevées de
cette dernière pour, maintenant, faire l'objet de ces 2 colonnes additionnelles.

Par ailleurs, vous pouvez maintenant exporter ces feuilles de calcul en
modifiant l'ordre de tri de ses entrées. Le tri par numéro de contrat pourra
probablement faciliter la vie de votre secrétariat social.

Tout est expliqué ici:

https://www.geezteem.com/2438

Bon travail et bon amusement sur Plat-com !
L'équipe Plat-com
'''

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class Tool:
    '''Configuration panel for HubPeople'''

    isTool = True
    Message = Message
    traverse = BaseTool.traverse.copy()

    def toAdminView(self):
        '''Show some elements to the "admin" account only'''
        if self.user.login == 'admin': return 'view'

    def toManagerView(self):
        '''Show some elements to Managers only'''
        if self.user.hasRole('Manager'): return 'view'

    def toManager(self):
        '''Show some elements to Managers only'''
        return self.user.hasRole('Manager')

    @staticmethod
    def update(class_):
        '''Hide the title'''
        fields = class_.fields
        fields['title'].show = False
        fields['translations'].page.show = False
        fields['groups'].page.show = class_.python.toAdminView

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    #                          Global parameters
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    pp = {'page': Page('parameters', show=toManager),
          'group': Group('main', style='grid', hasLabel=False)}

    # Acronym for the workspace (site)
    acronym = String(multiplicity=(1,1), layouts=Layouts.gd, width=5, **pp)

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    #                               Billing
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    # Price schemes

    priceSchemes = Ref(PriceScheme, add=True, link=False, multiplicity=(0,None),
      back=Ref(attribute='tool1', show=False), actionsDisplay='inline',
      showHeaders=True, shownInfo=PriceScheme.listColumns, layouts=Layouts.dv,
      page=Page('priceSchemes', show=toManagerView, phase='invoices'))

    pi = {'page': Page('invoiceParams', show=toManager, phase='invoices'),
          'group': Group('main', ['17%','83%'], style='grid', hasLabel=False)}

    # Price scheme per module
    def listModules(self):
        '''Lists the HubPeople modules'''
        return [(mod, self.translate(f'module_{mod}')) for mod in Vars.modules]

    sub = (
      ('scheme', Ref(PriceScheme, add=False, link=True, render='links')),
    )

    modulePrices = Dict(listModules, sub, layouts=List.Layouts.gd, **pi)

    # The day and month at which, each year, invoices must be produced

    def validDay(self, value):
        '''Ensures p_value is a valid day number'''
        if 1 <= value <= 31: return True
        return self.translate('day_ko')

    invoiceDay = Integer(default=5, layouts=Layouts.gd, width=2,
                         validator=validDay, **pi)

    def validMonth(self, value):
        '''Ensures p_value is a valid day number'''
        if 1 <= value <= 12: return True
        return self.translate('month_ko')

    invoiceMonth = Integer(default=1, layouts=Layouts.gd, width=2,
                           validator=validMonth, **pi)

    # The day at which, every month, users are counted
    countDay = Integer(default=1, layouts=Layouts.gd, width=2,
                       validator=validDay, **pi)

    # VAT (value-added tax) rate, as float value between 0.0 and 1.0, to be
    # applied to any invoice.

    def validVat(self, value):
        '''Ensure the VAT is between 0 and 1'''
        if 0.0 < value < 1.0:
            return True
        return self.translate('vat_ko')

    vat = Float(default=0.21, multiplicity=(1,1), layouts=Layouts.gd,
                validator=validVat, **pi)

    # If the following field is True, any invoice entry whose total is 0 will be
    # removed from the result.
    noZeroEntry = Boolean(layouts=Boolean.Layouts.gdl, **pi)

    # The logo to inject in every invoice
    invoicerLogo = File(isImage=True, layouts=Layouts.gd, **pi)
    invoicerName = String(**pi)
    invoicerNiceName = String(layouts=Layouts.gd, **pi)
    invoicerVat = String(**pi)
    invoicerAddress = Text(layouts=Layouts.b, **pi)
    invoicerIban = String(validator=String.IBAN, **pi)

    def getDefaultBody(self):
        '''Returns the default text for invoice mails'''
        return self.translate('invoice_mail_body', asText=True)

    invoiceMailBody = Text(default=getDefaultBody, width='29em', height=10,
                           **pi)

    # When an invoice is sent by email to a group, it can be bcc'ed to the
    # following email addresss, if specified. This address could represent the
    # entry point for an accounting system.
    invoicerBcc = String(validator=String.EMAIL, layouts=Layouts.gd, **pi)

    # Invoice numbers: prefix and number of the last generated invoice
    invoicePrefix = String(default='HP', width=3, layouts=Layouts.gd, **pi)
    invoiceLast = Integer(default=0, layouts=Layouts.gd, **pi)

    # Action for publishing all created invoices at once
    publishInvoices = Action(action=lambda o, options: options.run(o),
                             options=InvoicePublisher, page=pi['page'],
                             icon='send.svg', show='buttons')

    # Annual invoice-data (user counts)
    pageIDT = Page('invoiceData', show=toManagerView, phase='invoices')

    invoiceData = Ref(InvoiceData, add=False, link=False, multiplicity=(0,None),
      insert='start', back=Ref(attribute='toTool', show=False), page=pageIDT,
      layouts=Layouts.dv, actionsDisplay='inline')

    addInvoiceData = Action(action=InvoiceData.add, page=pageIDT, confirm=True,
                            show='buttons')

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    #                             Main methods
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    def validate(self, new, errors):
        '''Validate invoicing-related fields'''
        page = self.req.page or 'main'
        if page == 'invoiceParams':
            if new.invoiceMonth == 12:
                # The count day must come before the invoice day, because, in
                # that case, we will generate invoices for the current uyear,
                # thus including the invoice month.
                if new.countDay >= new.invoiceDay:
                    text = self.translate('invoice_days_ko')
                    errors.countDay = errors.invoiceDay = text

    allSyncModes = 'no', 'add', 'sync'

    def listSyncModes(self):
        '''Lists the various sync modes being available for timetracker
           objects.'''
        r = []
        for mode in self.allSyncModes:
            r.append((mode, self.translate(f'sync_{mode}')))
        return r

    def getHomePage(self):
        '''Returns the list of groups'''
        user = self.user
        # Return the home page (the default) for anonymous users
        if user.isAnon(): return
        # a Manager gets the config
        if user.hasRole('Manager'):
            r = f'{self.url}/view'
        else:
            # Block the user if he is not among any active group
            groups = user.getHpGroups()
            if not groups:
                self.raiseMessage(self.translate('no_active_group'))
            # Each user may choose its default search
            name = user.defaultSearch
            r = f'{self.url}/Search/results?className=Message&search={name}'
        return r

    def showPortletAt(self, url):
        '''Don't show the portlet for anonymous users'''
        return self.user.login != 'anon'

    def notInPopup(self):
        '''Determines the visibility of fields that must be shown everywhere,
           excepted when the activity is shown in the popup.'''
        return self.req.popup != 'True'

    def getViewableElements(self, elements, activeOnly=True):
        '''Among p_elements, get those being viewable by the current user, and
           those being active only if p_activeOnly is True.'''
        r = []
        for elem in elements:
            # Facet-only users, ie, may not be allowed to view some object
            if not elem.allows('read'): continue
            # Keep the element depending on p_activeOnly
            if not activeOnly or elem.state == 'active':
                r.append(elem)
        return r

    def getElementsForGroup(self, group, type, o=None):
        '''Gets, for some p_group, its active sub-elements of some p_type'''
        # p_type can be: 'tags', 'beneficiaries', 'contacts', 'categories',
        # 'classifiers' or 'relationTypes'.
        if not group: return ()
        # If we are called from a search form, we must get all sub-objects, even
        # inactive ones (the "search" key is present in the request if we are
        # called from a filter).
        fromSearch = o is None and not self.req.search
        r = self.getViewableElements(getattr(group, type),
                                     activeOnly=not fromSearch)
        # Get elements from the parent group when relevant
        if type in group.inheritedElements:
            name = f'inherited{type[0].upper()}{type[1:]}'
            if not group.isEmpty(name):
                inherited = self.getViewableElements(getattr(group, name),
                                                     activeOnly=not fromSearch)
                if inherited:
                    r = inherited + r
        # If we have an p_o(bject), get the already stored elements on it
        if o:
            tied = o.getObjectsUsedBy(type)
            if tied:
                for e in tied:
                    if e not in r:
                        r.insert(0, e)
        return r

    def hasAuthGroup(self):
        '''Is some authentication context (HP group) activated ?'''
        return self.guard.authContext

    def getAuthGroup(self, id=False):
        '''Gets the HP group defined as authentication context'''
        guard = self.guard
        groupId = guard.authContext
        if not groupId: return
        # There is an auth group: return its ID or the object when relevant
        return groupId if id else guard.authObject

    def setMessageSlaveFields(self):
        '''Set master/slave fields within the Message class: it depends on every
           HP group configuration.'''
        self.log(UPD_MSR)
        # HP groups for which file upload must be enabled
        attachmentMasters = []
        # HP groups for which external links must be enabled
        linkMasters = []
        # Browse HP groups
        for group in self.search('HpGroup'):
            if group.messageAttachmentEnabled:
                attachmentMasters.append(group.id)
            if group.messageLinkEnabled:
                linkMasters.append(group.id)
            # Log any hourspan whose short name would not conform to the new
            # regular expression.
            if not group.isEmpty('hourspans'):
                for hourspan in group.hourspans:
                    if not hourspan.rexShort.match(hourspan.shortName):
                        self.log(HOURSPAN_KO % (group.title,hourspan.shortName))
        # The master field is Message.group
        master = Message.group
        master.setSlave(Message.attachment, attachmentMasters)
        master.setSlave(Message.attachment2, attachmentMasters)
        master.setSlave(Message.attachment3, attachmentMasters)
        master.setSlave(Message.externalLink, linkMasters)
        self.log(UPD_MSR_OK)

    def staticVersions(self):
        '''Versions for CSS and JS files, used to force reload by the browser'''
        versions = self.config.server.static.versions
        versions['hubpeople.css'] = 9
        versions['hubpeople.js'] = 4

    def setSubGroups(self):
        '''Ensure every HP group sub-group exists'''
        for group in self.search('HpGroup'):
            group.createSubGroups()

    def warnTimetrackers(self):
        '''Sens a mail to timetrackers of every unique or main group'''
        for group in self.search('HpGroup'):
            if group.showTimetrackerParentOrSingle(show=True):
                body = ttBody % (group.title, self.acronym)
                group.sendMailTo('timetrackers', ttSubject, body)

    def createWeekTypes(self):
        '''Create the default wedek types in every timetracked unique or main
           group, if not already done.'''
        from .timetracker.WeekType import WeekType
        for group in self.search('HpGroup'):
            if group.showTimetrackerParentOrSingle(show=True) and \
               group.isEmpty('weekTypes'):
                # Inject default week types into this group
                for short, title, descr in WeekType.default:
                    group.create('weekTypes', title=title, description=descr,
                                 shortName=short)
                self.log(f'{group.strinG()} :: Default week types created.')

    def onInstall(self):
        '''Installation method'''
        self.setMessageSlaveFields()
        self.staticVersions()
        PriceScheme.setDefault(self)
        # Ensure invoice data exists for the current year
        InvoiceData.createIf(self)
        # Ensure every HP group sub-group exists
        self.setSubGroups()

    def refreshMessageStatuses(self, hpGroup=None):
        '''Refreshes the status of all meets and tasks or of those from
           p_hpGroup if given.'''
        if hpGroup:
            message = REFRESH_G % (hpGroup.identifier, hpGroup.title)
        else:
            message = REFRESH_A
        self.log(message)
        expr = 'o.computePresence(); o.reindex(fields=ctx.fields); ctx.count+=1'
        params = {'context': O(count=0, fields=('status', 'presence')),
                  'expression': expr, 'messageType': or_('task', 'meet')}
        if hpGroup: params['group'] = hpGroup.id
        return self.compute('Message', **params)

    traverse['onRefreshStatuses'] = 'Authenticated'
    def onRefreshStatuses(self):
        '''A group manager asks to refresh status of his group's meets and tasks
           from the UI.'''
        # Finer-grained security check
        authGroup = self.getAuthGroup()
        if not authGroup: raise self.raiseUnauthorized()
        if not self.user.isManagerOn(authGroup): self.raiseUnauthorized()
        r = self.refreshMessageStatuses(authGroup)
        msg = self.translate('refreshed_statuses', mapping={'nb': r.count})
        self.H().commit = True
        self.log(msg)
        self.goto(message=msg)

    def nightBillingActions(self):
        '''Perform, when appropriate, billing-related actions'''
        # Compute invoice data if relevant
        InvoiceData.addIf(self)
        # Generate invoices if relevant
        Invoice.generateYearly(self)

    def syncAll(self):
        '''Synchronizes objects between parent and child groups'''
        Syncer.syncAll(self)

    def nightlife(self):
        '''Refresh the status of tasks and meets'''
        self.log(NIGHT_START)
        r = self.refreshMessageStatuses()
        self.log(NIGHT_STS % r.count)
        # Update facets
        Facet.updateMessages(self)
        # Perform billing-related actions
        self.nightBillingActions()
        # Sync objects between parent and children groups
        self.syncAll()
        self.log(NIGHT_END)

    def logFaceted(self):
        '''Logs the number of faceted users, per group'''
        found = False
        for group in self.search('HpGroup'):
            count = 0
            for user in group.users:
                if user.isFaceted():
                    count += 1
                    found = True
            if count:
                self.log(FACETED % (group.iid, group.getShownValue(), count))
        if not found:
            self.log(FACETED_NO)
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
