<commit_before>##############################################################################
# For copyright and license notices, see __manifest__.py file in module root
# directory
##############################################################################
from odoo import models, fields, api, _
from odoo.exceptions import UserError

class AccountMove(models.Model):
    _inherit = 'account.move'

    move_currency_id = fields.Many2one(
        'res.currency',
        'Secondary Currency',
        help='If you set a currency here, then this invoice values will be '
        'also stored in the related Account Move Secondary Currency',
        readonly=True,
        states={'draft': [('readonly', False)]},
    )

    move_inverse_currency_rate = fields.Float(
        digits=(16, 4),
        string='Account Move Secondary Currency Rate',
        readonly=True,
        states={'draft': [('readonly', False)]},
    )

    @api.depends('move_currency_id')
    def _compute_amount(self):
        """
        Arreglamos que odoo nos convierte la deuda del asiento, expresada
        en otra moneda, a pesos que es lo que tenemos en la factura.
        Modificamos para que no lo convierta y muestre deuda en pesos si hay
        secondary currency
        TODO ver si podemos eliminar esto, es horrible
        """
        res = super()._compute_amount()
        for move in self.filtered('move_currency_id'):
            # si tenemos secondary currency no lo convertimos, mostramos
            # la deuda en moneda de cia
            if move.move_type == 'entry' or move.is_outbound():
                sign = 1
            else:
                sign = -1
            move.amount_untaxed = sign * -move.amount_untaxed_signed
            move.amount_tax = sign * -move.amount_tax_signed
            move.amount_total = sign * -move.amount_total_signed
            move.amount_residual = -sign * move.amount_residual_signed
        return res

    @api.onchange('move_currency_id')
    def change_move_currency(self):
        if not self.move_currency_id:
            self.move_inverse_currency_rate = False
        else:
            self.move_inverse_currency_rate = self.move_currency_id._convert(
                1.0, self.company_id.currency_id, self.company_id,
                self.invoice_date or fields.Date.context_today(self))

    @api.constrains('move_currency_id', 'currency_id')
    def check_move_currency(self):
        for rec in self.filtered('move_currency_id'):
            if rec.move_currency_id == rec.currency_id:
                raise UserError(_(
                    'Secondary currency can not be the same as Invoice '
                    'Currency'))
            if rec.currency_id != rec.company_id.currency_id:
                raise UserError(_(
                    'Can not use Secondary currency if invoice is in a '
                    'Currency different from Company Currency'))

    def _post(self, soft=True):
        """ para no tener que mandar el move currency en el context o hacer tmb onchanges nos la simpificamos y solo
        lo computamos en el post. Algo similar pasa tambien en odoo nativo, si bien a priroi se setea la currency_id
        (cuando la factura es en otra moneda) se puede forzar que no pase y luego se setea en el post"""
        res = super()._post(soft=soft)
        for rec in self.filtered('move_currency_id'):
            if not rec.move_inverse_currency_rate:
                raise UserError(_('If Secondary currency select you must set rate. Check invoice id: %s') % rec.id)
            # only debt lines because if not invoice lines prices are shown on move currency
            for line in rec.line_ids.filtered(
                    lambda line: line.account_id.user_type_id.type in ('receivable', 'payable')):
                amount = line.debit if line.debit else line.credit
                sign = 1.0 if line.debit else -1.0
                line.currency_id = self.move_currency_id.id
                line.amount_currency = sign * self.move_currency_id.round(amount / self.move_inverse_currency_rate)
        return res
<commit_msg>[FIX] account_invoice_move_currency; journal entries to draft<commit_after>##############################################################################
# For copyright and license notices, see __manifest__.py file in module root
# directory
##############################################################################
from odoo import models, fields, api, _
from odoo.exceptions import UserError

class AccountMove(models.Model):
    _inherit = 'account.move'

    move_currency_id = fields.Many2one(
        'res.currency',
        'Secondary Currency',
        help='If you set a currency here, then this invoice values will be '
        'also stored in the related Account Move Secondary Currency',
        readonly=True,
        states={'draft': [('readonly', False)]},
    )

    move_inverse_currency_rate = fields.Float(
        digits=(16, 4),
        string='Account Move Secondary Currency Rate',
        readonly=True,
        states={'draft': [('readonly', False)]},
    )

    @api.depends('move_currency_id')
    def _compute_amount(self):
        """
        Arreglamos que odoo nos convierte la deuda del asiento, expresada
        en otra moneda, a pesos que es lo que tenemos en la factura.
        Modificamos para que no lo convierta y muestre deuda en pesos si hay
        secondary currency
        TODO ver si podemos eliminar esto, es horrible
        """
        res = super()._compute_amount()
        for move in self.filtered('move_currency_id'):
            # si tenemos secondary currency no lo convertimos, mostramos
            # la deuda en moneda de cia
            if move.move_type == 'entry' or move.is_outbound():
                sign = 1
            else:
                sign = -1
            move.amount_untaxed = sign * -move.amount_untaxed_signed
            move.amount_tax = sign * -move.amount_tax_signed
            move.amount_total = sign * -move.amount_total_signed
            move.amount_residual = -sign * move.amount_residual_signed
        return res

    @api.onchange('move_currency_id')
    def change_move_currency(self):
        if not self.move_currency_id:
            self.move_inverse_currency_rate = False
        else:
            self.move_inverse_currency_rate = self.move_currency_id._convert(
                1.0, self.company_id.currency_id, self.company_id,
                self.invoice_date or fields.Date.context_today(self))

    @api.constrains('move_currency_id', 'currency_id')
    def check_move_currency(self):
        for rec in self.filtered('move_currency_id'):
            if rec.move_currency_id == rec.currency_id:
                raise UserError(_(
                    'Secondary currency can not be the same as Invoice '
                    'Currency'))
            if rec.currency_id != rec.company_id.currency_id:
                raise UserError(_(
                    'Can not use Secondary currency if invoice is in a '
                    'Currency different from Company Currency'))

    def _post(self, soft=True):
        """ para no tener que mandar el move currency en el context o hacer tmb onchanges nos la simpificamos y solo
        lo computamos en el post. Algo similar pasa tambien en odoo nativo, si bien a priroi se setea la currency_id
        (cuando la factura es en otra moneda) se puede forzar que no pase y luego se setea en el post"""
        res = super()._post(soft=soft)
        for rec in self.filtered('move_currency_id'):
            if not rec.move_inverse_currency_rate:
                raise UserError(_('If Secondary currency select you must set rate. Check invoice id: %s') % rec.id)
            # only debt lines because if not invoice lines prices are shown on move currency
            for line in rec.line_ids.filtered(
                    lambda line: line.account_id.user_type_id.type in ('receivable', 'payable')):
                amount = line.debit if line.debit else line.credit
                sign = 1.0 if line.debit else -1.0
                line.currency_id = self.move_currency_id.id
                line.amount_currency = sign * self.move_currency_id.round(amount / self.move_inverse_currency_rate)
        return res

    def button_draft(self):
        """ In order to be able to return a secondary currency invoice to draft and avoid Odoo to recompute the move
        totals amounts using the wrong currency (the secondary currency, we use the next two fields only as a hack so
        we need to clean them to avoid Odoo compute the invoice amounts wrongly) """
        if self.is_invoice():
            self.line_ids.write({
                'currency_id': False,
                'amount_currency': 0.0,
            })
        return super().button_draft()
<|endoftext|>


<commit_before>def export_lab_orders(sda_container, patient):
    pass
<commit_msg>Export results<commit_after>from sqlalchemy import null

from radar.models.results import Result, Observation

def export_lab_orders(sda_container, patient):
    q = Result.query
    q = q.filter(Result.patient == patient)
    q = q.filter(Result.source_type == 'RADAR')
    q = q.join(Result.observation)
    q = q.filter(Observation.pv_code != null())
    results = q.all()

    if not results:
        return

    sda_lab_orders = sda_container.setdefault('lab_orders', list())

    for result in results:
        sda_lab_order = {
            'external_id': str(result.id),
            'order_item': {
                'sda_coding_standard': 'PV',
                'code': result.observation.pv_code,
                'description': result.observation.pv_code
            },
            'result': {
                'result_items': [
                    {
                        'observation_time': result.date,
                        'test_item_code': {
                            'sda_coding_standard': 'PV',
                            'code': result.observation.pv_code,
                            'description': result.observation.pv_code
                        },
                        'result_value': str(result.value)
                    }
                ]
            },
            'entering_organization': {
                'code': result.source_group.code,
                'description': result.source_group.description
            }
        }

        sda_lab_orders.appned(sda_lab_order)
<|endoftext|>


<commit_before>#!/usr/bin/python

import subprocess
import os
import json

class VolumeManager:
    def __init__(self, base_cmdline, mount_root):
        self.base_cmdline = base_cmdline
        self.mount_root = mount_root

    def start_server(self, pidfile, cmdline):
        start_cmdline = ["start-stop-daemon", "-S", "-b", "-m", "-p", pidfile,
                        "--exec"] + self.base_cmdline + ["--"] + cmdline
        subprocess.check_call(start_cmdline)

    def stop_server(self, pidfile):
        stop_cmdline = ["start-stop-daemon", "-K", "-p", pidfile, "-x"] + self.base_cmdline
        return subprocess.call(stop_cmdline)

    def check_server(self, pidfile):
        check_cmdline = ["start-stop-daemon", "-T", "-p", pidfile]
        return subprocess.call(check_cmdline)

    def start_server_container(self, name, cfg_root, file_root, container, cmdline):
        start_cmdline = ["docker", "run", "--privileged",
                        "--name", name, "-d",
                        "-v", "/etc/ssl/certs:/etc/ssl/certs",
                        "-v", os.path.expanduser("~") + "/.aws:/root/.aws",
                        "-v", "/dev:/host/dev",
                        "-v", "/proc:/host/proc",
                        "-v", cfg_root + ":" + cfg_root,
                        "-v", file_root + ":" + file_root,
                        container,
                        ] + cmdline
        return subprocess.check_call(start_cmdline)

    def stop_server_container(self, name):
        stop_cmdline = ["docker", "rm", "-fv", name]
        return subprocess.check_call(stop_cmdline)

    def check_server_container(self, name):
        check_cmdline = ["docker", "inspect", "-f", "{{.State.Running}}", name]
        output = subprocess.check_output(check_cmdline)
        return output.startswith("true")

    def server_info(self):
        return subprocess.check_output(self.base_cmdline + ["info"])

    def create_volume(self, size = "", name = "", backup = "", driver = "",
                    volume_id = "", volume_type = "", iops = "", forvm = False):
        cmd = ["create"]
        if name != "":
            cmd = cmd + [name]
        if size != "":
            cmd = cmd + ["--size", size]
        if backup != "":
            cmd = cmd + ["--backup", backup]
        if driver != "":
            cmd = cmd + ["--driver", driver]
        if volume_id != "":
            cmd = cmd + ["--id", volume_id]
        if volume_type != "":
            cmd = cmd + ["--type", volume_type]
        if iops != "":
            cmd = cmd + ["--iops", iops]
        if forvm:
            cmd = cmd + ["--vm"]
        data = subprocess.check_output(self.base_cmdline + cmd)
        return data.strip()

    def delete_volume(self, volume, ref_only = False):
        cmdline = self.base_cmdline + ["delete", volume]
        if ref_only:
            cmdline += ["--reference"]
        subprocess.check_call(cmdline)

    def mount_volume_with_path(self, volume):
        volume_mount_dir = os.path.join(self.mount_root, volume)
        if not os.path.exists(volume_mount_dir):
            os.makedirs(volume_mount_dir)
        assert os.path.exists(volume_mount_dir)
        cmdline = self.base_cmdline + ["mount", volume,
                    "--mountpoint", volume_mount_dir]
        subprocess.check_call(cmdline)
        return volume_mount_dir

    def mount_volume(self, volume):
        cmdline = self.base_cmdline + ["mount", volume]

        data = subprocess.check_output(cmdline)
        return data.strip()

    def umount_volume(self, volume):
        subprocess.check_call(self.base_cmdline + ["umount", volume])

    def list_volumes(self):
        data = subprocess.check_output(self.base_cmdline + ["list"])
        volumes = json.loads(data)
        return volumes

    def inspect_volume(self, volume):
        cmd = ["inspect", volume]
        data = subprocess.check_output(self.base_cmdline + cmd)

        return json.loads(data)

    def create_snapshot(self, volume, name = ""):
        cmd = ["snapshot", "create", volume]
        if name != "":
                cmd += ["--name", name]
        data = subprocess.check_output(self.base_cmdline + cmd)
        return data.strip()

    def delete_snapshot(self, snapshot):
        subprocess.check_call(self.base_cmdline + ["snapshot", "delete",
                snapshot])

    def inspect_snapshot(self, snapshot):
        output = subprocess.check_output(self.base_cmdline + ["snapshot", "inspect",
                snapshot])
        snapshot = json.loads(output)
        return snapshot

    def create_backup(self, snapshot, dest = ""):
        cmdline = self.base_cmdline + ["backup", "create", snapshot]
        if dest != "":
            cmdline += ["--dest", dest]
        data = subprocess.check_output(cmdline)
        return data.strip()

    def delete_backup(self, backup):
        subprocess.check_call(self.base_cmdline + ["backup", "delete", backup])

    def list_backup(self, dest, volume_name = ""):
        cmd = ["backup", "list", dest]
        if volume_name != "":
            cmd += ["--volume-name", volume_name]
        data = subprocess.check_output(self.base_cmdline + cmd)
        backups = json.loads(data)
        return backups

    def inspect_backup(self, backup):
        data = subprocess.check_output(self.base_cmdline + ["backup",
            "inspect", backup])
        backups = json.loads(data)
        return backups
<commit_msg>Use human-readable flag names for start-stop-daemon<commit_after>#!/usr/bin/python

import subprocess
import os
import json

class VolumeManager:
    def __init__(self, base_cmdline, mount_root):
        self.base_cmdline = base_cmdline
        self.mount_root = mount_root

    def start_server(self, pidfile, cmdline):
        start_cmdline = ["start-stop-daemon", "--start", "--background",
                        "--make-pidfile", "--pidfile", pidfile,
                        "--exec"] + self.base_cmdline + ["--"] + cmdline
        subprocess.check_call(start_cmdline)

    def stop_server(self, pidfile):
        stop_cmdline = ["start-stop-daemon", "--stop", "--pidfile", pidfile, "--exec"] + self.base_cmdline
        return subprocess.call(stop_cmdline)

    def check_server(self, pidfile):
        check_cmdline = ["start-stop-daemon", "--status", "--pidfile", pidfile]
        return subprocess.call(check_cmdline)

    def start_server_container(self, name, cfg_root, file_root, container, cmdline):
        start_cmdline = ["docker", "run", "--privileged",
                        "--name", name, "-d",
                        "-v", "/etc/ssl/certs:/etc/ssl/certs",
                        "-v", os.path.expanduser("~") + "/.aws:/root/.aws",
                        "-v", "/dev:/host/dev",
                        "-v", "/proc:/host/proc",
                        "-v", cfg_root + ":" + cfg_root,
                        "-v", file_root + ":" + file_root,
                        container,
                        ] + cmdline
        return subprocess.check_call(start_cmdline)

    def stop_server_container(self, name):
        stop_cmdline = ["docker", "rm", "-fv", name]
        return subprocess.check_call(stop_cmdline)

    def check_server_container(self, name):
        check_cmdline = ["docker", "inspect", "-f", "{{.State.Running}}", name]
        output = subprocess.check_output(check_cmdline)
        return output.startswith("true")

    def server_info(self):
        return subprocess.check_output(self.base_cmdline + ["info"])

    def create_volume(self, size = "", name = "", backup = "", driver = "",
                    volume_id = "", volume_type = "", iops = "", forvm = False):
        cmd = ["create"]
        if name != "":
            cmd = cmd + [name]
        if size != "":
            cmd = cmd + ["--size", size]
        if backup != "":
            cmd = cmd + ["--backup", backup]
        if driver != "":
            cmd = cmd + ["--driver", driver]
        if volume_id != "":
            cmd = cmd + ["--id", volume_id]
        if volume_type != "":
            cmd = cmd + ["--type", volume_type]
        if iops != "":
            cmd = cmd + ["--iops", iops]
        if forvm:
            cmd = cmd + ["--vm"]
        data = subprocess.check_output(self.base_cmdline + cmd)
        return data.strip()

    def delete_volume(self, volume, ref_only = False):
        cmdline = self.base_cmdline + ["delete", volume]
        if ref_only:
            cmdline += ["--reference"]
        subprocess.check_call(cmdline)

    def mount_volume_with_path(self, volume):
        volume_mount_dir = os.path.join(self.mount_root, volume)
        if not os.path.exists(volume_mount_dir):
            os.makedirs(volume_mount_dir)
        assert os.path.exists(volume_mount_dir)
        cmdline = self.base_cmdline + ["mount", volume,
                    "--mountpoint", volume_mount_dir]
        subprocess.check_call(cmdline)
        return volume_mount_dir

    def mount_volume(self, volume):
        cmdline = self.base_cmdline + ["mount", volume]

        data = subprocess.check_output(cmdline)
        return data.strip()

    def umount_volume(self, volume):
        subprocess.check_call(self.base_cmdline + ["umount", volume])

    def list_volumes(self):
        data = subprocess.check_output(self.base_cmdline + ["list"])
        volumes = json.loads(data)
        return volumes

    def inspect_volume(self, volume):
        cmd = ["inspect", volume]
        data = subprocess.check_output(self.base_cmdline + cmd)

        return json.loads(data)

    def create_snapshot(self, volume, name = ""):
        cmd = ["snapshot", "create", volume]
        if name != "":
                cmd += ["--name", name]
        data = subprocess.check_output(self.base_cmdline + cmd)
        return data.strip()

    def delete_snapshot(self, snapshot):
        subprocess.check_call(self.base_cmdline + ["snapshot", "delete",
                snapshot])

    def inspect_snapshot(self, snapshot):
        output = subprocess.check_output(self.base_cmdline + ["snapshot", "inspect",
                snapshot])
        snapshot = json.loads(output)
        return snapshot

    def create_backup(self, snapshot, dest = ""):
        cmdline = self.base_cmdline + ["backup", "create", snapshot]
        if dest != "":
            cmdline += ["--dest", dest]
        data = subprocess.check_output(cmdline)
        return data.strip()

    def delete_backup(self, backup):
        subprocess.check_call(self.base_cmdline + ["backup", "delete", backup])

    def list_backup(self, dest, volume_name = ""):
        cmd = ["backup", "list", dest]
        if volume_name != "":
            cmd += ["--volume-name", volume_name]
        data = subprocess.check_output(self.base_cmdline + cmd)
        backups = json.loads(data)
        return backups

    def inspect_backup(self, backup):
        data = subprocess.check_output(self.base_cmdline + ["backup",
            "inspect", backup])
        backups = json.loads(data)
        return backups
<|endoftext|>


<commit_before>#-*- coding: utf-8 -*-
# version string following pep-0396 and pep-0386
__version__ = '0.9pbs.103'  # pragma: nocover

default_app_config = 'filer.apps.FilerConfig'
<commit_msg>Bump version<commit_after>#-*- coding: utf-8 -*-
# version string following pep-0396 and pep-0386
__version__ = '0.9pbs.104'  # pragma: nocover

default_app_config = 'filer.apps.FilerConfig'
<|endoftext|>


<commit_before>from flask import json, jsonify, request, abort, url_for, redirect
from flask.views import MethodView
import random

from backend import app, rc, db
from backend.models import Nicety, SiteConfiguration
from backend.auth import current_user, needs_authorization
from datetime import datetime, timedelta
from sqlalchemy import func

from urllib.request import Request, urlopen
import backend.cache as cache
import backend.config as config
import backend.util as util

@app.route('/api/v1/niceties-to-print')
@needs_authorization
def niceties_to_print():
    ret = {}    # Mapping from target_id to a list of niceties for that person
    last_target = None
    # Loop through all the Niceties that exist for the open batch
    # The batch end date might be 6 weeks or more away, if you're doing a six week stint.
    # So that means it won't be picked up by .. which means it won't appear in the list to print!

    # this might need to be improve, possibly
    # 1) filter the niceties by the latest end_date
    # 2) ..
    is_faculty = True #json.loads(person(current_user().id).data)['is_faculty']
    two_weeks_from_now = datetime.now() - timedelta(days=14)
    if is_faculty == True:
        valid_niceties = (Nicety.query
                          #.filter(Nicety.end_date < two_weeks_from_now)
                          .order_by(Nicety.target_id)
                          .all())
        for n in valid_niceties:
            if n.target_id != last_target:
                # ... set up the test for the next one
                last_target = n.target_id
                ret[n.target_id] = []  # initialize the dictionary
            if n.anonymous == False:
                ret[n.target_id].append({
                    'author_id': n.author_id,
                    'name': json.loads(person(n.author_id).data)['name'],
                    'text': n.text,
                })
            else:
                ret[n.target_id].append({
                    'text': n.text,
                })
        return jsonify([
            {
                'to': json.loads(person(k).data)['name'],
                'niceties': v
            }
            for k, v in ret.items()
        ])
    else:
        return jsonify({'authorized': "false"})

@app.route('/api/v1/show-niceties')
@needs_authorization
def get_niceties_for_current_user():
    """Returns JSON list like:
    [
        {
            author_id: 0,   // target_id
            end_date: "",
            anonymous: true/false,
            text: "",
        },
        {
            author_id: int,
            end_date: "",
            anonymous: true/false,
            text: "",
        },
        ...
    ]
    """
    ret = []
    whoami = current_user().id
    two_weeks_from_now = datetime.now() - timedelta(days=14)
    valid_niceties = (Nicety.query
                      .filter(Nicety.end_date > datetime.now())
                      .filter(Nicety.target_id == whoami)
                      .all())
    for n in valid_niceties:
        if n.anonymous == True:
            store = {
                'end_date': n.end_date,
                'anonymous': n.anonymous,
                'text': n.text
            }
        else:
            store = {
                'avatar_url': json.loads(person(n.author_id).data)['avatar_url'],
                'author_id': n.author_id,
                'end_date': n.end_date,
                'anonymous': n.anonymous,
                'text': n.text
            }
        ret.append(store)
    return jsonify(ret)
    pass

@app.route('/api/v1/batches/<int:batch_id>/people')
@needs_authorization
def batch_people(batch_id):
    try:
        cache_key = 'batches_people_list:{}'.format(batch_id)
        people = cache.get(cache_key)
    except cache.NotInCache:
        people = []
        for p in rc.get('batches/{}/people'.format(batch_id)).data:
            people.append({
                'id': p['id'],
                'name': util.name_from_rc_person(p),
                'avatar_url': p['image'],
                'stints': p['stints'],
                'bio': p['bio'],
                'interests': p['interests'],
                'before_rc': p['before_rc'],
                'during_rc': p['during_rc'],
                'job': p['job'],
                'twitter': p['twitter'],
                'github': p['github'],
            })
        cache.set(cache_key, people)
    random.seed(current_user().random_seed)
    random.shuffle(people)  # This order will be random but consistent for the user
    return jsonify(people)

@app.route('/api/v1/faculty')
@needs_authorization
def get_faculty():
    ''' faculty will always appear in
    the most recent batch!
    '''
    cache_key = 'faculty_list'
    try:
        faculty = cache.get(cache_key)
    except cache.NotInCache:
        faculty = []
        for open_batch in get_open_batches():
            for p in rc.get('batches/{}/people'.format(open_batch['id'])).data:
                if p['is_faculty']:
                    faculty.append({
                        'id': p['id'],
                        'name': util.name_from_rc_person(p),
                        'avatar_url': p['image'],
                        'stints': p['stints'],
                        'bio': p['bio'],
                        'interests': p['interests'],
                        'before_rc': p['before_rc'],
                        'during_rc': p['during_rc'],
                        'job': p['job'],
                        'twitter': p['twitter'],
                        'github': p['github'],
                    })
        cache.set(cache_key, faculty)
    return jsonify(faculty)

def get_users(batches):
    batches = rc.get('batches').data
    return [person for batch in batches for person in rc.get('batches/{}/people'.format(batch['id'])).data]

# private function
def get_current_batches():
    batches = rc.get('batches').data
    return [batch for batch in batches if util.end_date_within_range(batch['end_date'])]

def get_current_users():
    return get_users(get_current_batches())

# return tuple (X,Y), where X is people about to leave, Y is all else
def partition_current_users(users):
    ret = {}
    earliest_date = datetime.now()
    for u in users:
        print(u)
        for stint in u['stints']:
            user_date = datetime.strptime(stint['end_date'], '%Y-%m-%d')
            if user_date < earliest_date:
                earliest_date = user_date
        if user_date in ret:
            ret[user_date].append(u)
        elif user_date != None:
            ret[user_date] = [u]
    ret.pop(earliest_date)
    return ret

@app.route('/api/v1/test')
def test():
    return jsonify(partition_current_users(get_current_users()))

# return tuple (X,Y), where X is people about to leave, Y is current people not about to leave
def partition_users():
    users = get_users()
    pass

def get_current_batches():
    try:
        cache_key = 'open_batches_list'
        return cache.get(cache_key)
    except cache.NotInCache:
        batches = rc.get('batches').data
        print(batches)
        ret = []
        for batch in batches:
            if util.end_date_within_range(batch['end_date']):
                print(batch)
                batch['closing_time'] = util.batch_closing_time(batch['end_date']).isoformat()
                batch['warning_time'] = util.batch_closing_warning_time(batch['end_date']).isoformat()
                ret.append(batch)
        cache.set(cache_key, ret)
    return ret

@app.route('/api/v1/people')
@needs_authorization
def display_people():
    cache_key = 'people_list'
    try:
        people = cache.get(cache_key)
    except cache.NotInCache:
        people = []
        for open_batch in get_open_batches():
            for p in rc.get('batches/{}/people'.format(open_batch['id'])).data:
                latest_end_date = None
                for stint in p['stints']:
                    e = datetime.strptime(stint['end_date'], '%Y-%m-%d')
                    if latest_end_date is None or e > latest_end_date:
                        latest_end_date = e
                if (latest_end_date is not None and
                    util.end_date_within_range(latest_end_date) and
                    (   # Batchlings have   is_hacker_schooler = True,      is_faculty = False
                        # Faculty have      is_hacker_schooler = ?,         is_faculty = True
                        # Residents have    is_hacker_schooler = False,     is_faculty = False
                        (p['is_hacker_schooler'] and not p['is_faculty']) or
                        (not p['is_faculty'] and not p['is_hacker_schooler'] and config.get(config.INCLUDE_RESIDENTS, False)) or
                        (p['is_faculty'] and config.get(config.INCLUDE_FACULTY, False)))):
                    people.append({
                        'id': p['id'],
                        'name': util.name_from_rc_person(p),
                        'avatar_url': p['image'],
                        'end_date': '{:%Y-%m-%d}'.format(latest_end_date),
                        'job': p['job'],
                        'twitter': p['twitter'],
                        'github': p['github'],
                        'bio': p['bio'],
                        'interests': p['interests'],
                        'before_rc': p['before_rc'],
                        'during_rc': p['during_rc'],
                    })
        cache.set(cache_key, people)
    whoami = current_user().id
    #am_i_leaving =
    all_but_me = list(person for person in people if person['id'] != whoami)
    random.seed(current_user().random_seed)
    random.shuffle(all_but_me)  # This order will be random but consistent for the user
    for p in all_but_me:
        p['placeholder'] = urlopen("https://api.github.com/users/{}/repos".format(p['github'])).read()
        print("hiiiiii")
    return jsonify(all_but_me)

@app.route('/api/v1/people/<int:person_id>')
@needs_authorization
def person(person_id):
    cache_key = 'person:{}'.format(person_id)
    try:
        return cache.get(cache_key)
    except cache.NotInCache:
        p = rc.get('people/{}'.format(person_id)).data
        person = {
            'id': p['id'],
            'name': util.name_from_rc_person(p),
            'avatar_url': p['image'],
            'is_faculty': p['is_faculty'],
            'bio': p['bio'],
            'interests': p['interests'],
            'before_rc': p['before_rc'],
            'during_rc': p['during_rc'],
            'job': p['job'],
            'twitter': p['twitter'],
            'github': p['github'],
        }
        person_json = jsonify(person)
        cache.set(cache_key, person_json)
        return person_json

class NicetyFromMeAPI(MethodView):
    def get(end_date, person_id):
        if current_user() is None:
            redirect(url_for('authorized'))
        try:
            nicety = (
                Nicety
                .query
                .filter_by(
                    end_date=end_date,
                    target_id=person_id,
                    author_id=current_user().id)
                .one())
        except db.exc.NoResultFound:
            nicety = Nicety(
                end_date=end_date,
                target_id=person_id,
                author_id=current_user().id,
                anonymous=current_user().anonymous_by_default)
            db.session.add(nicety)
            db.session.commit()
        return jsonify(nicety.__dict__)

    def post(end_date, person_id):
        if current_user() is None:
            redirect(url_for('authorized'))
        nicety = (
            Nicety
            .query
            .filter_by(
                end_date=end_date,
                target_id=person_id,
                author_id=current_user().id)
            .one())
        nicety.anonymous = request.form.get("anonymous", current_user().anonymous_by_default)
        text = request.form.get("text").strip()
        if '' == text:
            text = None
        nicety.text = text
        nicety.faculty_reviewed = False
        db.session.commit()
        return jsonify({'status': 'OK'})

app.add_url_rule(
    '/api/v1/niceties/<int:end_date>/<int:person_id>',
    view_func=NicetyFromMeAPI.as_view('nicety_from_me'))

@app.route('/api/v1/post-niceties', methods=['POST'])
@needs_authorization
def save_niceties():
    """Expects JSON list like:
    [
        {
            target_id: 0,   // target_id
            end_date: "",
            anonymous: true/false,
            text: "",
        },
        {
            target_id: int,
            end_date: "",
            anonymous: true/false,
            text: "",
        },
        ...
    ]
    """
    niceties_to_save = json.loads(request.form.get("niceties", "[]"))
    for n in niceties_to_save:
        nicety = (
            Nicety
            .query      # Query is always about getting Nicety objects from the database
            .filter_by(
                end_date=datetime.strptime(n.get("end_date"), "%Y-%m-%d").date(),
                target_id=n.get("target_id"),
                author_id=current_user().id)
            .one_or_none())
        if nicety is None:
            nicety = Nicety(
                end_date=datetime.strptime(n.get("end_date"), "%Y-%m-%d").date(),
                target_id=n.get("target_id"),
                author_id=current_user().id)
            db.session.add(nicety)
            # We need to add for the new one, but we don't need to add where we have used .query
            # Now any change to the nicety object (variable) is tracked by the object
            # so it knows what it will have to update.
            # And then when we call db.session.commit() that knows about the object
            # (beacuse Nicety.query... uses the db.session behind the scenes).
            # So then db.session.commit() sends the update (or insert) for every object
            # in the session. This includes every object created by a [model].query and
            # everything added to the session with db.session.add().
        nicety.anonymous = n.get("anonymous", current_user().anonymous_by_default)
        text = n.get("text").strip()
        if '' == text:
            text = None
        nicety.text = text
        nicety.faculty_reviewed = False
    db.session.commit()
    return jsonify({'status': 'OK'})

class SiteSettingsAPI(MethodView):
    def get(self):
        user = current_user()
        if user is None:
            return redirect(url_for('authorized'))
        if not user.faculty:
            return abort(403)
        return jsonify({c.key: config.to_frontend_value(c) for c in SiteConfiguration.query.all()})

    def post(self):
        if current_user() is None:
            redirect(url_for('authorized'))
            user = current_user()
        if not user.faculty:
            return abort(403)
        key = request.form.get('key', None)
        value = request.form.get('value', None)
        try:
            try:
                config.set(key, config.from_frontend_value(key, json.loads(value)))
                return jsonify({'status': 'OK'})
            except ValueError:
                return abort(404)
        except:
            return abort(400)

app.add_url_rule(
    '/api/v1/site_settings',
    view_func=SiteSettingsAPI.as_view('site_settings'))
<commit_msg>stuff<commit_after>from flask import json, jsonify, request, abort, url_for, redirect
from flask.views import MethodView
import random

from backend import app, rc, db
from backend.models import Nicety, SiteConfiguration
from backend.auth import current_user, needs_authorization
from datetime import datetime, timedelta
from sqlalchemy import func

from urllib.request import Request, urlopen
import backend.cache as cache
import backend.config as config
import backend.util as util

@app.route('/api/v1/niceties-to-print')
@needs_authorization
def niceties_to_print():
    ret = {}    # Mapping from target_id to a list of niceties for that person
    last_target = None
    # Loop through all the Niceties that exist for the open batch
    # The batch end date might be 6 weeks or more away, if you're doing a six week stint.
    # So that means it won't be picked up by .. which means it won't appear in the list to print!

    # this might need to be improve, possibly
    # 1) filter the niceties by the latest end_date
    # 2) ..
    is_faculty = True #json.loads(person(current_user().id).data)['is_faculty']
    two_weeks_from_now = datetime.now() - timedelta(days=14)
    if is_faculty == True:
        valid_niceties = (Nicety.query
                          #.filter(Nicety.end_date < two_weeks_from_now)
                          .order_by(Nicety.target_id)
                          .all())
        for n in valid_niceties:
            if n.target_id != last_target:
                # ... set up the test for the next one
                last_target = n.target_id
                ret[n.target_id] = []  # initialize the dictionary
            if n.anonymous == False:
                ret[n.target_id].append({
                    'author_id': n.author_id,
                    'name': json.loads(person(n.author_id).data)['name'],
                    'text': n.text,
                })
            else:
                ret[n.target_id].append({
                    'text': n.text,
                })
        return jsonify([
            {
                'to': json.loads(person(k).data)['name'],
                'niceties': v
            }
            for k, v in ret.items()
        ])
    else:
        return jsonify({'authorized': "false"})

@app.route('/api/v1/show-niceties')
@needs_authorization
def get_niceties_for_current_user():
    """Returns JSON list like:
    [
        {
            author_id: 0,   // target_id
            end_date: "",
            anonymous: true/false,
            text: "",
        },
        {
            author_id: int,
            end_date: "",
            anonymous: true/false,
            text: "",
        },
        ...
    ]
    """
    ret = []
    whoami = current_user().id
    two_weeks_from_now = datetime.now() - timedelta(days=14)
    valid_niceties = (Nicety.query
                      .filter(Nicety.end_date > datetime.now())
                      .filter(Nicety.target_id == whoami)
                      .all())
    for n in valid_niceties:
        if n.anonymous == True:
            store = {
                'end_date': n.end_date,
                'anonymous': n.anonymous,
                'text': n.text
            }
        else:
            store = {
                'avatar_url': json.loads(person(n.author_id).data)['avatar_url'],
                'author_id': n.author_id,
                'end_date': n.end_date,
                'anonymous': n.anonymous,
                'text': n.text
            }
        ret.append(store)
    return jsonify(ret)
    pass

@app.route('/api/v1/batches/<int:batch_id>/people')
@needs_authorization
def batch_people(batch_id):
    try:
        cache_key = 'batches_people_list:{}'.format(batch_id)
        people = cache.get(cache_key)
    except cache.NotInCache:
        people = []
        for p in rc.get('batches/{}/people'.format(batch_id)).data:
            people.append({
                'id': p['id'],
                'name': util.name_from_rc_person(p),
                'avatar_url': p['image'],
                'stints': p['stints'],
                'bio': p['bio'],
                'interests': p['interests'],
                'before_rc': p['before_rc'],
                'during_rc': p['during_rc'],
                'job': p['job'],
                'twitter': p['twitter'],
                'github': p['github'],
            })
        cache.set(cache_key, people)
    random.seed(current_user().random_seed)
    random.shuffle(people)  # This order will be random but consistent for the user
    return jsonify(people)

@app.route('/api/v1/faculty')
@needs_authorization
def get_faculty():
    ''' faculty will always appear in
    the most recent batch!
    '''
    cache_key = 'faculty_list'
    try:
        faculty = cache.get(cache_key)
    except cache.NotInCache:
        faculty = []
        for open_batch in get_open_batches():
            for p in rc.get('batches/{}/people'.format(open_batch['id'])).data:
                if p['is_faculty']:
                    faculty.append({
                        'id': p['id'],
                        'name': util.name_from_rc_person(p),
                        'avatar_url': p['image'],
                        'stints': p['stints'],
                        'bio': p['bio'],
                        'interests': p['interests'],
                        'before_rc': p['before_rc'],
                        'during_rc': p['during_rc'],
                        'job': p['job'],
                        'twitter': p['twitter'],
                        'github': p['github'],
                    })
        cache.set(cache_key, faculty)
    return jsonify(faculty)

def get_users(batches):
    batches = rc.get('batches').data
    return [person for batch in batches for person in rc.get('batches/{}/people'.format(batch['id'])).data]

# private function
def get_current_batches():
    batches = rc.get('batches').data
    return [batch for batch in batches if util.end_date_within_range(batch['end_date'])]

def get_current_users():
    return get_users(get_current_batches())

# return tuple (X,Y), where X is people about to leave, Y is all else
def partition_current_users(users):
    ret = {}
    earliest_date = datetime.now()
    for u in users:
        print(u)
        for stint in u['stints']:
            user_date = datetime.strptime(stint['end_date'], '%Y-%m-%d')
            if user_date < earliest_date:
                earliest_date = user_date
        if user_date in ret:
            ret[user_date].append(u)
        elif user_date != None:
            ret[user_date] = [u]
    ret.pop(earliest_date)
    return ret

@app.route('/api/v1/test')
def test():
    return jsonify(partition_current_users(get_current_users()))

# return tuple (X,Y), where X is people about to leave, Y is current people not about to leave
def partition_users():
    users = get_users()
    pass

def get_current_batches():
    try:
        cache_key = 'open_batches_list'
        return cache.get(cache_key)
    except cache.NotInCache:
        batches = rc.get('batches').data
        print(batches)
        ret = []
        for batch in batches:
            if util.end_date_within_range(batch['end_date']):
                print(batch)
                batch['closing_time'] = util.batch_closing_time(batch['end_date']).isoformat()
                batch['warning_time'] = util.batch_closing_warning_time(batch['end_date']).isoformat()
                ret.append(batch)
        cache.set(cache_key, ret)
    return ret

@app.route('/api/v1/people')
@needs_authorization
def display_people():
    cache_key = 'people_list'
    try:
        people = cache.get(cache_key)
    except cache.NotInCache:
        people = []
        for open_batch in get_open_batches():
            for p in rc.get('batches/{}/people'.format(open_batch['id'])).data:
                latest_end_date = None
                for stint in p['stints']:
                    e = datetime.strptime(stint['end_date'], '%Y-%m-%d')
                    if latest_end_date is None or e > latest_end_date:
                        latest_end_date = e
                if (latest_end_date is not None and
                    util.end_date_within_range(latest_end_date) and
                    (   # Batchlings have   is_hacker_schooler = True,      is_faculty = False
                        # Faculty have      is_hacker_schooler = ?,         is_faculty = True
                        # Residents have    is_hacker_schooler = False,     is_faculty = False
                        (p['is_hacker_schooler'] and not p['is_faculty']) or
                        (not p['is_faculty'] and not p['is_hacker_schooler'] and config.get(config.INCLUDE_RESIDENTS, False)) or
                        (p['is_faculty'] and config.get(config.INCLUDE_FACULTY, False)))):
                    people.append({
                        'id': p['id'],
                        'name': util.name_from_rc_person(p),
                        'avatar_url': p['image'],
                        'end_date': '{:%Y-%m-%d}'.format(latest_end_date),
                        'job': p['job'],
                        'twitter': p['twitter'],
                        'github': p['github'],
                        'bio': p['bio'],
                        'interests': p['interests'],
                        'before_rc': p['before_rc'],
                        'during_rc': p['during_rc'],
                    })
        cache.set(cache_key, people)
    whoami = current_user().id
    #am_i_leaving =
    all_but_me = list(person for person in people if person['id'] != whoami)
    random.seed(current_user().random_seed)
    random.shuffle(all_but_me)  # This order will be random but consistent for the user
    for p in all_but_me:
        print(p['github'])
        if p['github'] is not None:
            p['placeholder'] = urlopen("https://api.github.com/users/{}/repos".format(p['github'])).read()
        print("hiiiiii")
    return jsonify(all_but_me)

@app.route('/api/v1/people/<int:person_id>')
@needs_authorization
def person(person_id):
    cache_key = 'person:{}'.format(person_id)
    try:
        return cache.get(cache_key)
    except cache.NotInCache:
        p = rc.get('people/{}'.format(person_id)).data
        person = {
            'id': p['id'],
            'name': util.name_from_rc_person(p),
            'avatar_url': p['image'],
            'is_faculty': p['is_faculty'],
            'bio': p['bio'],
            'interests': p['interests'],
            'before_rc': p['before_rc'],
            'during_rc': p['during_rc'],
            'job': p['job'],
            'twitter': p['twitter'],
            'github': p['github'],
        }
        person_json = jsonify(person)
        cache.set(cache_key, person_json)
        return person_json

class NicetyFromMeAPI(MethodView):
    def get(end_date, person_id):
        if current_user() is None:
            redirect(url_for('authorized'))
        try:
            nicety = (
                Nicety
                .query
                .filter_by(
                    end_date=end_date,
                    target_id=person_id,
                    author_id=current_user().id)
                .one())
        except db.exc.NoResultFound:
            nicety = Nicety(
                end_date=end_date,
                target_id=person_id,
                author_id=current_user().id,
                anonymous=current_user().anonymous_by_default)
            db.session.add(nicety)
            db.session.commit()
        return jsonify(nicety.__dict__)

    def post(end_date, person_id):
        if current_user() is None:
            redirect(url_for('authorized'))
        nicety = (
            Nicety
            .query
            .filter_by(
                end_date=end_date,
                target_id=person_id,
                author_id=current_user().id)
            .one())
        nicety.anonymous = request.form.get("anonymous", current_user().anonymous_by_default)
        text = request.form.get("text").strip()
        if '' == text:
            text = None
        nicety.text = text
        nicety.faculty_reviewed = False
        db.session.commit()
        return jsonify({'status': 'OK'})

app.add_url_rule(
    '/api/v1/niceties/<int:end_date>/<int:person_id>',
    view_func=NicetyFromMeAPI.as_view('nicety_from_me'))

@app.route('/api/v1/post-niceties', methods=['POST'])
@needs_authorization
def save_niceties():
    """Expects JSON list like:
    [
        {
            target_id: 0,   // target_id
            end_date: "",
            anonymous: true/false,
            text: "",
        },
        {
            target_id: int,
            end_date: "",
            anonymous: true/false,
            text: "",
        },
        ...
    ]
    """
    niceties_to_save = json.loads(request.form.get("niceties", "[]"))
    for n in niceties_to_save:
        nicety = (
            Nicety
            .query      # Query is always about getting Nicety objects from the database
            .filter_by(
                end_date=datetime.strptime(n.get("end_date"), "%Y-%m-%d").date(),
                target_id=n.get("target_id"),
                author_id=current_user().id)
            .one_or_none())
        if nicety is None:
            nicety = Nicety(
                end_date=datetime.strptime(n.get("end_date"), "%Y-%m-%d").date(),
                target_id=n.get("target_id"),
                author_id=current_user().id)
            db.session.add(nicety)
            # We need to add for the new one, but we don't need to add where we have used .query
            # Now any change to the nicety object (variable) is tracked by the object
            # so it knows what it will have to update.
            # And then when we call db.session.commit() that knows about the object
            # (beacuse Nicety.query... uses the db.session behind the scenes).
            # So then db.session.commit() sends the update (or insert) for every object
            # in the session. This includes every object created by a [model].query and
            # everything added to the session with db.session.add().
        nicety.anonymous = n.get("anonymous", current_user().anonymous_by_default)
        text = n.get("text").strip()
        if '' == text:
            text = None
        nicety.text = text
        nicety.faculty_reviewed = False
    db.session.commit()
    return jsonify({'status': 'OK'})

class SiteSettingsAPI(MethodView):
    def get(self):
        user = current_user()
        if user is None:
            return redirect(url_for('authorized'))
        if not user.faculty:
            return abort(403)
        return jsonify({c.key: config.to_frontend_value(c) for c in SiteConfiguration.query.all()})

    def post(self):
        if current_user() is None:
            redirect(url_for('authorized'))
            user = current_user()
        if not user.faculty:
            return abort(403)
        key = request.form.get('key', None)
        value = request.form.get('value', None)
        try:
            try:
                config.set(key, config.from_frontend_value(key, json.loads(value)))
                return jsonify({'status': 'OK'})
            except ValueError:
                return abort(404)
        except:
            return abort(400)

app.add_url_rule(
    '/api/v1/site_settings',
    view_func=SiteSettingsAPI.as_view('site_settings'))
<|endoftext|>


<commit_before>"""Support for (EMEA/EU-based) Honeywell TCC climate systems.

Such systems include evohome (multi-zone), and Round Thermostat (single zone).
"""
import asyncio
from datetime import datetime, timedelta
import logging
from typing import Any, Dict, Optional, Tuple

import requests.exceptions
import voluptuous as vol
import evohomeclient2

from homeassistant.const import (
    CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME,
    HTTP_SERVICE_UNAVAILABLE, HTTP_TOO_MANY_REQUESTS, TEMP_CELSIUS)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.dispatcher import (
    async_dispatcher_connect, async_dispatcher_send)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import (
    async_track_point_in_utc_time, track_time_interval)
from homeassistant.util.dt import parse_datetime, utcnow

from .const import DOMAIN, STORAGE_VERSION, STORAGE_KEY, GWS, TCS

_LOGGER = logging.getLogger(__name__)

CONF_ACCESS_TOKEN_EXPIRES = 'access_token_expires'
CONF_REFRESH_TOKEN = 'refresh_token'

CONF_LOCATION_IDX = 'location_idx'
SCAN_INTERVAL_DEFAULT = timedelta(seconds=300)
SCAN_INTERVAL_MINIMUM = timedelta(seconds=60)

CONFIG_SCHEMA = vol.Schema({
    DOMAIN: vol.Schema({
        vol.Required(CONF_USERNAME): cv.string,
        vol.Required(CONF_PASSWORD): cv.string,
        vol.Optional(CONF_LOCATION_IDX, default=0): cv.positive_int,
        vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL_DEFAULT):
            vol.All(cv.time_period, vol.Range(min=SCAN_INTERVAL_MINIMUM)),
    }),
}, extra=vol.ALLOW_EXTRA)

def _local_dt_to_utc(dt_naive: datetime) -> datetime:
    dt_aware = utcnow() + (dt_naive - datetime.now())
    if dt_aware.microsecond >= 500000:
        dt_aware += timedelta(seconds=1)
    return dt_aware.replace(microsecond=0)

def _utc_to_local_dt(dt_aware: datetime) -> datetime:
    dt_naive = datetime.now() + (dt_aware - utcnow())
    if dt_naive.microsecond >= 500000:
        dt_naive += timedelta(seconds=1)
    return dt_naive.replace(microsecond=0)

def _handle_exception(err) -> bool:
    try:
        raise err

    except evohomeclient2.AuthenticationError:
        _LOGGER.error(
            "Failed to (re)authenticate with the vendor's server. "
            "Check that your username and password are correct. "
            "Message is: %s",
            err
        )
        return False

    except requests.exceptions.ConnectionError:
        # this appears to be common with Honeywell's servers
        _LOGGER.warning(
            "Unable to connect with the vendor's server. "
            "Check your network and the vendor's status page."
            "Message is: %s",
            err
        )
        return False

    except requests.exceptions.HTTPError:
        if err.response.status_code == HTTP_SERVICE_UNAVAILABLE:
            _LOGGER.warning(
                "Vendor says their server is currently unavailable. "
                "Check the vendor's status page."
            )
            return False

        if err.response.status_code == HTTP_TOO_MANY_REQUESTS:
            _LOGGER.warning(
                "The vendor's API rate limit has been exceeded. "
                "Consider increasing the %s.", CONF_SCAN_INTERVAL
            )
            return False

        raise  # we don't expect/handle any other HTTPErrors

def setup(hass, hass_config) -> bool:
    """Create a (EMEA/EU-based) Honeywell evohome system."""
    broker = EvoBroker(hass, hass_config[DOMAIN])
    if not broker.init_client():
        return False

    load_platform(hass, 'climate', DOMAIN, {}, hass_config)
    if broker.tcs.hotwater:
        load_platform(hass, 'water_heater', DOMAIN, {}, hass_config)

    track_time_interval(
        hass, broker.update, hass_config[DOMAIN][CONF_SCAN_INTERVAL]
    )

    return True

class EvoBroker:
    """Container for evohome client and data."""

    def __init__(self, hass, params) -> None:
        """Initialize the evohome client and data structure."""
        self.hass = hass
        self.params = params

        self.config = self.status = self.timers = {}

        self.client = self.tcs = None
        self._app_storage = None

        hass.data[DOMAIN] = {}
        hass.data[DOMAIN]['broker'] = self

    def init_client(self) -> bool:
        """Initialse the evohome data broker.

        Return True if this is successful, otherwise return False.
        """
        refresh_token, access_token, access_token_expires = \
            asyncio.run_coroutine_threadsafe(
                self._load_auth_tokens(), self.hass.loop).result()

        # evohomeclient2 uses local datetimes
        if access_token_expires is not None:
            access_token_expires = _utc_to_local_dt(access_token_expires)

        try:
            client = self.client = evohomeclient2.EvohomeClient(
                self.params[CONF_USERNAME],
                self.params[CONF_PASSWORD],
                refresh_token=refresh_token,
                access_token=access_token,
                access_token_expires=access_token_expires
            )

        except (requests.exceptions.RequestException,
                evohomeclient2.AuthenticationError) as err:
            if not _handle_exception(err):
                return False

        finally:
            self.params[CONF_PASSWORD] = 'REDACTED'

        self.hass.add_job(self._save_auth_tokens())

        loc_idx = self.params[CONF_LOCATION_IDX]
        try:
            self.config = client.installation_info[loc_idx][GWS][0][TCS][0]

        except IndexError:
            _LOGGER.error(
                "Config error: '%s' = %s, but its valid range is 0-%s. "
                "Unable to continue. "
                "Fix any configuration errors and restart HA.",
                CONF_LOCATION_IDX, loc_idx, len(client.installation_info) - 1
            )
            return False

        self.tcs = client.locations[loc_idx]._gateways[0]._control_systems[0]  # noqa: E501; pylint: disable=protected-access

        _LOGGER.debug("Config = %s", self.config)
        if _LOGGER.isEnabledFor(logging.DEBUG):
            # don't do an I/O unless required
            _LOGGER.debug(
                "Status = %s",
                client.locations[loc_idx].status()[GWS][0][TCS][0])

        return True

    async def _load_auth_tokens(self) -> Tuple[
            Optional[str], Optional[str], Optional[datetime]]:
        store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
        app_storage = self._app_storage = await store.async_load()

        if app_storage.get(CONF_USERNAME) == self.params[CONF_USERNAME]:
            refresh_token = app_storage.get(CONF_REFRESH_TOKEN)
            access_token = app_storage.get(CONF_ACCESS_TOKEN)
            at_expires_str = app_storage.get(CONF_ACCESS_TOKEN_EXPIRES)
            if at_expires_str:
                at_expires_dt = parse_datetime(at_expires_str)
            else:
                at_expires_dt = None

            return (refresh_token, access_token, at_expires_dt)

        return (None, None, None)  # account switched: so tokens wont be valid

    async def _save_auth_tokens(self, *args) -> None:
        # evohomeclient2 uses local datetimes
        access_token_expires = _local_dt_to_utc(
            self.client.access_token_expires)

        self._app_storage[CONF_USERNAME] = self.params[CONF_USERNAME]
        self._app_storage[CONF_REFRESH_TOKEN] = self.client.refresh_token
        self._app_storage[CONF_ACCESS_TOKEN] = self.client.access_token
        self._app_storage[CONF_ACCESS_TOKEN_EXPIRES] = \
            access_token_expires.isoformat()

        store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
        await store.async_save(self._app_storage)

        async_track_point_in_utc_time(
            self.hass,
            self._save_auth_tokens,
            access_token_expires + self.params[CONF_SCAN_INTERVAL]
        )

    def update(self, *args, **kwargs) -> None:
        """Get the latest state data of the entire evohome Location.

        This includes state data for the Controller and all its child devices,
        such as the operating mode of the Controller and the current temp of
        its children (e.g. Zones, DHW controller).
        """
        loc_idx = self.params[CONF_LOCATION_IDX]

        try:
            status = self.client.locations[loc_idx].status()[GWS][0][TCS][0]
        except (requests.exceptions.RequestException,
                evohomeclient2.AuthenticationError) as err:
            _handle_exception(err)
        else:
            self.timers['statusUpdated'] = utcnow()

        _LOGGER.debug("Status = %s", status)

        # inform the evohome devices that state data has been updated
        async_dispatcher_send(self.hass, DOMAIN, {'signal': 'refresh'})

class EvoDevice(Entity):
    """Base for any evohome device.

    This includes the Controller, (up to 12) Heating Zones and
    (optionally) a DHW controller.
    """

    def __init__(self, evo_broker, evo_device) -> None:
        """Initialize the evohome entity."""
        self._evo_device = evo_device
        self._evo_tcs = evo_broker.tcs

        self._name = self._icon = self._precision = None
        self._state_attributes = []

        self._supported_features = None
        self._setpoints = None

    @callback
    def _refresh(self, packet):
        if packet['signal'] == 'refresh':
            self.async_schedule_update_ha_state(force_refresh=True)

    def get_setpoints(self) -> Optional[Dict[str, Any]]:
        """Return the current/next scheduled switchpoints.

        Only Zones & DHW controllers (but not the TCS) have schedules.
        """
        switchpoints = {}
        schedule = self._evo_device.schedule()

        if not schedule['DailySchedules']:
            return None

        day_time = datetime.now()
        day_of_week = int(day_time.strftime('%w'))  # 0 is Sunday

        # Iterate today's switchpoints until past the current time of day...
        day = schedule['DailySchedules'][day_of_week]
        sp_idx = -1  # last switchpoint of the day before
        for i, tmp in enumerate(day['Switchpoints']):
            if day_time.strftime('%H:%M:%S') > tmp['TimeOfDay']:
                sp_idx = i  # current setpoint
            else:
                break

        # Did the current SP start yesterday? Does the next start SP tomorrow?
        current_sp_day = -1 if sp_idx == -1 else 0
        next_sp_day = 1 if sp_idx + 1 == len(day['Switchpoints']) else 0

        for key, offset, idx in [
                ('current', current_sp_day, sp_idx),
                ('next', next_sp_day, (sp_idx + 1) * (1 - next_sp_day))]:

            spt = switchpoints[key] = {}

            sp_date = (day_time + timedelta(days=offset)).strftime('%Y-%m-%d')
            day = schedule['DailySchedules'][(day_of_week + offset) % 7]
            switchpoint = day['Switchpoints'][idx]

            dt_naive = datetime.strptime(
                '{}T{}'.format(sp_date, switchpoint['TimeOfDay']),
                '%Y-%m-%dT%H:%M:%S')

            spt['from'] = _local_dt_to_utc(dt_naive).isoformat()
            try:
                spt['temperature'] = switchpoint['heatSetpoint']
            except KeyError:
                spt['state'] = switchpoint['DhwState']

        return switchpoints

    @property
    def should_poll(self) -> bool:
        """Evohome entities should not be polled."""
        return False

    @property
    def name(self) -> str:
        """Return the name of the Evohome entity."""
        return self._name

    @property
    def device_state_attributes(self) -> Dict[str, Any]:
        """Return the Evohome-specific state attributes."""
        status = {}
        for attr in self._state_attributes:
            if attr != 'setpoints':
                status[attr] = getattr(self._evo_device, attr)

        if 'setpoints' in self._state_attributes:
            status['setpoints'] = self._setpoints

        return {'status': status}

    @property
    def icon(self) -> str:
        """Return the icon to use in the frontend UI."""
        return self._icon

    @property
    def supported_features(self) -> int:
        """Get the flag of supported features of the device."""
        return self._supported_features

    async def async_added_to_hass(self) -> None:
        """Run when entity about to be added to hass."""
        async_dispatcher_connect(self.hass, DOMAIN, self._refresh)

    @property
    def precision(self) -> float:
        """Return the temperature precision to use in the frontend UI."""
        return self._precision

    @property
    def temperature_unit(self) -> str:
        """Return the temperature unit to use in the frontend UI."""
        return TEMP_CELSIUS

    def update(self) -> None:
        """Get the latest state data."""
        self._setpoints = self.get_setpoints()
<commit_msg>[climate-1.0] Bugfix evohome showstopper (#25139)<commit_after>"""Support for (EMEA/EU-based) Honeywell TCC climate systems.

Such systems include evohome (multi-zone), and Round Thermostat (single zone).
"""
import asyncio
from datetime import datetime, timedelta
import logging
from typing import Any, Dict, Optional, Tuple

import requests.exceptions
import voluptuous as vol
import evohomeclient2

from homeassistant.const import (
    CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME,
    HTTP_SERVICE_UNAVAILABLE, HTTP_TOO_MANY_REQUESTS, TEMP_CELSIUS)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.dispatcher import (
    async_dispatcher_connect, async_dispatcher_send)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import (
    async_track_point_in_utc_time, track_time_interval)
from homeassistant.util.dt import parse_datetime, utcnow

from .const import DOMAIN, STORAGE_VERSION, STORAGE_KEY, GWS, TCS

_LOGGER = logging.getLogger(__name__)

CONF_ACCESS_TOKEN_EXPIRES = 'access_token_expires'
CONF_REFRESH_TOKEN = 'refresh_token'

CONF_LOCATION_IDX = 'location_idx'
SCAN_INTERVAL_DEFAULT = timedelta(seconds=300)
SCAN_INTERVAL_MINIMUM = timedelta(seconds=60)

CONFIG_SCHEMA = vol.Schema({
    DOMAIN: vol.Schema({
        vol.Required(CONF_USERNAME): cv.string,
        vol.Required(CONF_PASSWORD): cv.string,
        vol.Optional(CONF_LOCATION_IDX, default=0): cv.positive_int,
        vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL_DEFAULT):
            vol.All(cv.time_period, vol.Range(min=SCAN_INTERVAL_MINIMUM)),
    }),
}, extra=vol.ALLOW_EXTRA)

def _local_dt_to_utc(dt_naive: datetime) -> datetime:
    dt_aware = utcnow() + (dt_naive - datetime.now())
    if dt_aware.microsecond >= 500000:
        dt_aware += timedelta(seconds=1)
    return dt_aware.replace(microsecond=0)

def _utc_to_local_dt(dt_aware: datetime) -> datetime:
    dt_naive = datetime.now() + (dt_aware - utcnow())
    if dt_naive.microsecond >= 500000:
        dt_naive += timedelta(seconds=1)
    return dt_naive.replace(microsecond=0)

def _handle_exception(err) -> bool:
    try:
        raise err

    except evohomeclient2.AuthenticationError:
        _LOGGER.error(
            "Failed to (re)authenticate with the vendor's server. "
            "Check that your username and password are correct. "
            "Message is: %s",
            err
        )
        return False

    except requests.exceptions.ConnectionError:
        # this appears to be common with Honeywell's servers
        _LOGGER.warning(
            "Unable to connect with the vendor's server. "
            "Check your network and the vendor's status page."
            "Message is: %s",
            err
        )
        return False

    except requests.exceptions.HTTPError:
        if err.response.status_code == HTTP_SERVICE_UNAVAILABLE:
            _LOGGER.warning(
                "Vendor says their server is currently unavailable. "
                "Check the vendor's status page."
            )
            return False

        if err.response.status_code == HTTP_TOO_MANY_REQUESTS:
            _LOGGER.warning(
                "The vendor's API rate limit has been exceeded. "
                "Consider increasing the %s.", CONF_SCAN_INTERVAL
            )
            return False

        raise  # we don't expect/handle any other HTTPErrors

def setup(hass, hass_config) -> bool:
    """Create a (EMEA/EU-based) Honeywell evohome system."""
    broker = EvoBroker(hass, hass_config[DOMAIN])
    if not broker.init_client():
        return False

    load_platform(hass, 'climate', DOMAIN, {}, hass_config)
    if broker.tcs.hotwater:
        load_platform(hass, 'water_heater', DOMAIN, {}, hass_config)

    track_time_interval(
        hass, broker.update, hass_config[DOMAIN][CONF_SCAN_INTERVAL]
    )

    return True

class EvoBroker:
    """Container for evohome client and data."""

    def __init__(self, hass, params) -> None:
        """Initialize the evohome client and data structure."""
        self.hass = hass
        self.params = params

        self.config = self.status = self.timers = {}

        self.client = self.tcs = None
        self._app_storage = {}

        hass.data[DOMAIN] = {}
        hass.data[DOMAIN]['broker'] = self

    def init_client(self) -> bool:
        """Initialse the evohome data broker.

        Return True if this is successful, otherwise return False.
        """
        refresh_token, access_token, access_token_expires = \
            asyncio.run_coroutine_threadsafe(
                self._load_auth_tokens(), self.hass.loop).result()

        # evohomeclient2 uses local datetimes
        if access_token_expires is not None:
            access_token_expires = _utc_to_local_dt(access_token_expires)

        try:
            client = self.client = evohomeclient2.EvohomeClient(
                self.params[CONF_USERNAME],
                self.params[CONF_PASSWORD],
                refresh_token=refresh_token,
                access_token=access_token,
                access_token_expires=access_token_expires
            )

        except (requests.exceptions.RequestException,
                evohomeclient2.AuthenticationError) as err:
            if not _handle_exception(err):
                return False

        finally:
            self.params[CONF_PASSWORD] = 'REDACTED'

        self.hass.add_job(self._save_auth_tokens())

        loc_idx = self.params[CONF_LOCATION_IDX]
        try:
            self.config = client.installation_info[loc_idx][GWS][0][TCS][0]

        except IndexError:
            _LOGGER.error(
                "Config error: '%s' = %s, but its valid range is 0-%s. "
                "Unable to continue. "
                "Fix any configuration errors and restart HA.",
                CONF_LOCATION_IDX, loc_idx, len(client.installation_info) - 1
            )
            return False

        self.tcs = client.locations[loc_idx]._gateways[0]._control_systems[0]  # noqa: E501; pylint: disable=protected-access

        _LOGGER.debug("Config = %s", self.config)
        if _LOGGER.isEnabledFor(logging.DEBUG):
            # don't do an I/O unless required
            _LOGGER.debug(
                "Status = %s",
                client.locations[loc_idx].status()[GWS][0][TCS][0])

        return True

    async def _load_auth_tokens(self) -> Tuple[
            Optional[str], Optional[str], Optional[datetime]]:
        store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
        app_storage = self._app_storage = await store.async_load()

        if app_storage is None:
            app_storage = self._app_storage = {}

        if app_storage.get(CONF_USERNAME) == self.params[CONF_USERNAME]:
            refresh_token = app_storage.get(CONF_REFRESH_TOKEN)
            access_token = app_storage.get(CONF_ACCESS_TOKEN)
            at_expires_str = app_storage.get(CONF_ACCESS_TOKEN_EXPIRES)
            if at_expires_str:
                at_expires_dt = parse_datetime(at_expires_str)
            else:
                at_expires_dt = None

            return (refresh_token, access_token, at_expires_dt)

        return (None, None, None)  # account switched: so tokens wont be valid

    async def _save_auth_tokens(self, *args) -> None:
        # evohomeclient2 uses local datetimes
        access_token_expires = _local_dt_to_utc(
            self.client.access_token_expires)

        self._app_storage[CONF_USERNAME] = self.params[CONF_USERNAME]
        self._app_storage[CONF_REFRESH_TOKEN] = self.client.refresh_token
        self._app_storage[CONF_ACCESS_TOKEN] = self.client.access_token
        self._app_storage[CONF_ACCESS_TOKEN_EXPIRES] = \
            access_token_expires.isoformat()

        store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
        await store.async_save(self._app_storage)

        async_track_point_in_utc_time(
            self.hass,
            self._save_auth_tokens,
            access_token_expires + self.params[CONF_SCAN_INTERVAL]
        )

    def update(self, *args, **kwargs) -> None:
        """Get the latest state data of the entire evohome Location.

        This includes state data for the Controller and all its child devices,
        such as the operating mode of the Controller and the current temp of
        its children (e.g. Zones, DHW controller).
        """
        loc_idx = self.params[CONF_LOCATION_IDX]

        try:
            status = self.client.locations[loc_idx].status()[GWS][0][TCS][0]
        except (requests.exceptions.RequestException,
                evohomeclient2.AuthenticationError) as err:
            _handle_exception(err)
        else:
            self.timers['statusUpdated'] = utcnow()

        _LOGGER.debug("Status = %s", status)

        # inform the evohome devices that state data has been updated
        async_dispatcher_send(self.hass, DOMAIN, {'signal': 'refresh'})

class EvoDevice(Entity):
    """Base for any evohome device.

    This includes the Controller, (up to 12) Heating Zones and
    (optionally) a DHW controller.
    """

    def __init__(self, evo_broker, evo_device) -> None:
        """Initialize the evohome entity."""
        self._evo_device = evo_device
        self._evo_tcs = evo_broker.tcs

        self._name = self._icon = self._precision = None
        self._state_attributes = []

        self._supported_features = None
        self._setpoints = None

    @callback
    def _refresh(self, packet):
        if packet['signal'] == 'refresh':
            self.async_schedule_update_ha_state(force_refresh=True)

    def get_setpoints(self) -> Optional[Dict[str, Any]]:
        """Return the current/next scheduled switchpoints.

        Only Zones & DHW controllers (but not the TCS) have schedules.
        """
        switchpoints = {}
        schedule = self._evo_device.schedule()

        if not schedule['DailySchedules']:
            return None

        day_time = datetime.now()
        day_of_week = int(day_time.strftime('%w'))  # 0 is Sunday

        # Iterate today's switchpoints until past the current time of day...
        day = schedule['DailySchedules'][day_of_week]
        sp_idx = -1  # last switchpoint of the day before
        for i, tmp in enumerate(day['Switchpoints']):
            if day_time.strftime('%H:%M:%S') > tmp['TimeOfDay']:
                sp_idx = i  # current setpoint
            else:
                break

        # Did the current SP start yesterday? Does the next start SP tomorrow?
        current_sp_day = -1 if sp_idx == -1 else 0
        next_sp_day = 1 if sp_idx + 1 == len(day['Switchpoints']) else 0

        for key, offset, idx in [
                ('current', current_sp_day, sp_idx),
                ('next', next_sp_day, (sp_idx + 1) * (1 - next_sp_day))]:

            spt = switchpoints[key] = {}

            sp_date = (day_time + timedelta(days=offset)).strftime('%Y-%m-%d')
            day = schedule['DailySchedules'][(day_of_week + offset) % 7]
            switchpoint = day['Switchpoints'][idx]

            dt_naive = datetime.strptime(
                '{}T{}'.format(sp_date, switchpoint['TimeOfDay']),
                '%Y-%m-%dT%H:%M:%S')

            spt['from'] = _local_dt_to_utc(dt_naive).isoformat()
            try:
                spt['temperature'] = switchpoint['heatSetpoint']
            except KeyError:
                spt['state'] = switchpoint['DhwState']

        return switchpoints

    @property
    def should_poll(self) -> bool:
        """Evohome entities should not be polled."""
        return False

    @property
    def name(self) -> str:
        """Return the name of the Evohome entity."""
        return self._name

    @property
    def device_state_attributes(self) -> Dict[str, Any]:
        """Return the Evohome-specific state attributes."""
        status = {}
        for attr in self._state_attributes:
            if attr != 'setpoints':
                status[attr] = getattr(self._evo_device, attr)

        if 'setpoints' in self._state_attributes:
            status['setpoints'] = self._setpoints

        return {'status': status}

    @property
    def icon(self) -> str:
        """Return the icon to use in the frontend UI."""
        return self._icon

    @property
    def supported_features(self) -> int:
        """Get the flag of supported features of the device."""
        return self._supported_features

    async def async_added_to_hass(self) -> None:
        """Run when entity about to be added to hass."""
        async_dispatcher_connect(self.hass, DOMAIN, self._refresh)

    @property
    def precision(self) -> float:
        """Return the temperature precision to use in the frontend UI."""
        return self._precision

    @property
    def temperature_unit(self) -> str:
        """Return the temperature unit to use in the frontend UI."""
        return TEMP_CELSIUS

    def update(self) -> None:
        """Get the latest state data."""
        self._setpoints = self.get_setpoints()
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
#
#    Account Invoice - Actual Costs and Margins
#    © 1200 WebDevelopment <http://1200wd.com/>
#    2016 November
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as
#    published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

from openerp import models, fields, api, exceptions, _
import openerp.addons.decimal_precision as dp
import logging

_logger = logging.getLogger(__name__)

class AccountInvoiceLine(models.Model):
    _inherit = "account.invoice.line"

    @api.one
    # @api.depends('product_id', 'quantity', 'price_subtotal')
    # TODO: Recalculate margins and actual costs when writing invoice rule or invoice
    # FIXME: Last inserted invoice line not calculated
    # FIXME: Remove @api.one
    def get_actual_costs(self):
        self.actual_cost = 0
        if self.invoice_id.type in ['out_invoice', 'out_refund']:
            if self.product_id.product_tmpl_id.actual_cost:
                self.actual_cost = self.product_id.product_tmpl_id.actual_cost * self.quantity
            if self.price_subtotal and self.actual_cost:
                self.margin_perc = ( 1 - (self.actual_cost / self.price_subtotal) ) * 100
            _logger.debug("1200wd - Invoice line {}: Update Actual Cost to {} and margin to {}%".
                          format(self.id, self.actual_cost, self.margin_perc))

    actual_cost = fields.Float(string="Actual Cost Price", readonly=True,
                               compute="get_actual_costs", store=True,
                               digits_compute=dp.get_precision('Product Price'),
                               help="Actual Purchase Price for the product of this invoice line")
    discount = fields.Float(group_operator="avg")
    margin_perc = fields.Float(string="Margin %", readonly=True,
                               digits=(16, 1), group_operator="avg",
                               compute="get_actual_costs", store=True,
                               help="Profit margin of this invoice line")

class AccountInvoice(models.Model):
    _inherit = "account.invoice"

    @api.one
    # @api.depends('invoice_line.product_id', 'invoice_line.quantity', 'invoice_line.price_subtotal')
    def calculate_total_actual_costs(self):
        self.actual_cost_total = 0
        self.margin_perc = 0
        if self.type in ['out_invoice', 'out_refund']:
            for line in self.invoice_line:
                line.get_actual_costs()
                self.actual_cost_total += line.actual_cost or 0.0
            if self.amount_untaxed and self.actual_cost_total:
                self.margin_perc = ( 1 - ( self.actual_cost_total / self.amount_untaxed) ) * 100
            _logger.debug("1200wd - Update invoice total actual costs {} and margin to {}%".
                          format(self.actual_cost_total, self.margin_perc))

    actual_cost_total = fields.Float(string="Total Actual Cost", readonly=True,
                                     digits_compute=dp.get_precision('Product Price'),
                                     compute="calculate_total_actual_costs", store=True,
                                     help="Total Actual costs of this invoice, excluding tax")
    margin_perc = fields.Float(string="Margin %", readonly=True,
                               digits=(16, 1), group_operator="avg",
                               compute="calculate_total_actual_costs", store=True,
                               help="Profit margin of this invoice")

    @api.multi
    def write(self, vals):
        if 'invoice_line' in vals:
            for inv in self:
                inv.calculate_total_actual_costs()
        super(AccountInvoice, self).write(vals)
        return True
<commit_msg>[FIX] Last inserted invoice line not calculated<commit_after># -*- coding: utf-8 -*-
#
#    Account Invoice - Actual Costs and Margins
#    © 1200 WebDevelopment <http://1200wd.com/>
#    2016 November
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as
#    published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

from openerp import models, fields, api, exceptions, _
import openerp.addons.decimal_precision as dp
import logging

_logger = logging.getLogger(__name__)

class AccountInvoiceLine(models.Model):
    _inherit = "account.invoice.line"

    @api.one
    # @api.depends('product_id', 'quantity', 'price_subtotal')
    # TODO: Recalculate margins and actual costs when writing invoice rule or invoice
    # FIXME: Remove @api.one
    def get_actual_costs(self):
        self.actual_cost = 0
        if self.invoice_id.type in ['out_invoice', 'out_refund']:
            if self.product_id.product_tmpl_id.actual_cost:
                self.actual_cost = self.product_id.product_tmpl_id.actual_cost * self.quantity
            if self.price_subtotal and self.actual_cost:
                self.margin_perc = ( 1 - (self.actual_cost / self.price_subtotal) ) * 100
            _logger.debug("1200wd - Invoice line {}: Update Actual Cost to {} and margin to {}%".
                          format(self.id, self.actual_cost, self.margin_perc))

    actual_cost = fields.Float(string="Actual Cost Price", readonly=True,
                               compute="get_actual_costs", store=True,
                               digits_compute=dp.get_precision('Product Price'),
                               help="Actual Purchase Price for the product of this invoice line")
    discount = fields.Float(group_operator="avg")
    margin_perc = fields.Float(string="Margin %", readonly=True,
                               digits=(16, 1), group_operator="avg",
                               compute="get_actual_costs", store=True,
                               help="Profit margin of this invoice line")

class AccountInvoice(models.Model):
    _inherit = "account.invoice"

    @api.one
    # @api.depends('invoice_line.product_id', 'invoice_line.quantity', 'invoice_line.price_subtotal')
    def calculate_total_actual_costs(self):
        self.actual_cost_total = 0
        self.margin_perc = 0
        if self.type in ['out_invoice', 'out_refund']:
            for line in self.invoice_line:
                line.get_actual_costs()
                self.actual_cost_total += line.actual_cost or 0.0
            if self.amount_untaxed and self.actual_cost_total:
                self.margin_perc = ( 1 - ( self.actual_cost_total / self.amount_untaxed) ) * 100
            _logger.debug("1200wd - Update invoice total actual costs {} and margin to {}%".
                          format(self.actual_cost_total, self.margin_perc))

    actual_cost_total = fields.Float(string="Total Actual Cost", readonly=True,
                                     digits_compute=dp.get_precision('Product Price'),
                                     compute="calculate_total_actual_costs", store=True,
                                     help="Total Actual costs of this invoice, excluding tax")
    margin_perc = fields.Float(string="Margin %", readonly=True,
                               digits=(16, 1), group_operator="avg",
                               compute="calculate_total_actual_costs", store=True,
                               help="Profit margin of this invoice")

    @api.multi
    def write(self, vals):
        super(AccountInvoice, self).write(vals)
        if 'invoice_line' in vals:
            for inv in self:
                inv.calculate_total_actual_costs()
        return True
<|endoftext|>


<commit_before>from allauth.socialaccount import app_settings
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider

class SpotifyAccount(ProviderAccount):
    pass

class SpotifyOAuth2Provider(OAuth2Provider):
    id = 'spotify'
    name = 'Spotify'
    account_class = SpotifyAccount

    def extract_uid(self, data):
        return data['id']

    def extract_common_fields(self, data):
        return dict(name=data.get('display_name'),
                    email=data.get('email'))

    def get_default_scope(self):
        scope = []
        if app_settings.QUERY_EMAIL:
            scope.append('user-read-email')
        return scope

provider_classes = [SpotifyOAuth2Provider]
<commit_msg>feat(spotify): Provide profile URL, avatar URL, display name<commit_after>from allauth.socialaccount import app_settings
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider

class SpotifyAccount(ProviderAccount):
    def get_profile_url(self):
        return self.account.extra_data.get('external_urls').get('spotify')

    def get_avatar_url(self):
        try:
            return self.account.extra_data.get('images')[0].get('url')
        except IndexError:
            return None

    def to_str(self):
        dflt = super(SpotifyAccount, self).to_str()
        return self.account.extra_data.get('display_name', dflt)

class SpotifyOAuth2Provider(OAuth2Provider):
    id = 'spotify'
    name = 'Spotify'
    account_class = SpotifyAccount

    def extract_uid(self, data):
        return data['id']

    def extract_common_fields(self, data):
        return dict(name=data.get('display_name'),
                    email=data.get('email'))

    def get_default_scope(self):
        scope = []
        if app_settings.QUERY_EMAIL:
            scope.append('user-read-email')
        return scope

provider_classes = [SpotifyOAuth2Provider]
<|endoftext|>


<commit_before>#-*- coding: utf-8 -*-
# version string following pep-0396 and pep-0386
__version__ = '0.9pbs.58'  # pragma: nocover
<commit_msg>Bump filer version to 0.9pbs.59<commit_after>#-*- coding: utf-8 -*-
# version string following pep-0396 and pep-0386
__version__ = '0.9pbs.59'  # pragma: nocover
<|endoftext|>