def is_multiclient():
    try:
        return current_app.multiclient
    except RuntimeError:
        # This is running outside an application context, meaning it is in a separate worker.
        # This means it is indeed NOT a multiclient application
        return False class BaseResource(Resource):

    routes = []

    def get_json(self):
        if not request.data:
            return None
        return json.loads(request.data.decode(), parse_float=decimal.Decimal)

    def get_arg(self, attr, default=None):
        """Get the attr from querystring, form data or json"""
        # This is not working on all versions.
        if self.get_json():
            return self.get_json().get(attr, None)

        return request.form.get(attr, request.args.get(attr, default))

    def get_current_user(self, store):
        auth = request.headers.get('Authorization', '').split('Bearer ')
        token = AccessToken.get_by_token(store=store, token=auth[1])
        return token and token.user

    def get_current_station(self, store, token=None):
        if not token:
            auth = request.headers.get('Authorization', '').split('Bearer ')
            token = auth[1]
        token = AccessToken.get_by_token(store=store, token=token)
        return token and token.station

    def get_current_branch(self, store):
        station = self.get_current_station(store)
        return station and station.branch

    @classmethod
    def ensure_printer(cls, station, retries=20):
        # In multiclient mode there is no local printer
        if is_multiclient():
            return

        assert printer_lock.locked()

        device = DeviceSettings.get_by_station_and_type(station.store, station,
                                                        DeviceSettings.NON_FISCAL_PRINTER_DEVICE)
        if not device:
            # If we have no printer configured, there's nothing to ensure
            return

        # There is no need to lock the printer here, since it should already be locked by the
        # calling site of this method.
        # Test the printer to see if its working properly.
        printer = None
        try:
            printer = api.device_manager.printer
            return printer.is_drawer_open()
        except (SerialException, InvalidReplyException, PrinterError):
            if printer:
                printer._port.close()
            api.device_manager._printer = None
            for i in range(retries):
                log.info('Printer check failed. Reopening: %s', i)
                try:
                    printer = api.device_manager.printer
                    printer.is_drawer_open()
                    break
                except (SerialException, PrinterError):
                    gevent.sleep(1)
            else:
                # Reopening printer failed. re-raise the original exception
                raise

            # Invalidate the printer in the plugins so that it re-opens it
            manager = get_plugin_manager()

            # nfce does not need to reset the printer since it does not cache it.
            sat = get_plugin(manager, 'sat')
            if sat and sat.ui:
                sat.ui.printer = None

            nonfiscal = get_plugin(manager, 'nonfiscal')
            if nonfiscal and nonfiscal.ui:
                nonfiscal.ui.printer = printer

            return printer.is_drawer_open() class EventStream(BaseResource):
    """A stream of events from this server to the application.

    Callsites can use EventStream.add_event(event) to send a message from the server to the client
    asynchronously.

    Note that there should be only one client connected at a time. If more than one are connected,
    all of them will receive all events
    """

    routes = ['/stream']

    @classmethod
    def add_event(cls, data, station=None):
        """If station specified, put a event only on the client stream.
        Otherwise, put it in all streams
        """
        payload = json.dumps(data, cls=JsonEncoder)
        if station:
            if station.is_api:
                return

            receivers = redis_server.publish(station.id, payload)
            if receivers == 0:
                raise EventStreamUnconnectedStation
            return

        redis_server.publish(STREAM_ALL_BRANCHES, payload)

    @classmethod
    def ask_question(cls, station, question):
        """Sends a question down the stream"""
        log.info('Asking %s question: %s', station.name, question)
        cls.add_event({
            'type': 'TEF_ASK_QUESTION',
            'data': question,
        }, station=station)

        log.info('Waiting tef reply')

        redis_server.hset('waiting', station.id, 1)
        # This call will block until some other request puts a reply on this list
        reply = json.loads(redis_server.blpop('reply-%s' % station.id, timeout=0)[1].decode())
        redis_server.hdel('waiting', station.id)

        log.info('Got tef reply: %s', reply)
        return reply

    @classmethod
    def add_event_reply(cls, station_id, reply):
        """Puts a reply from the frontend"""
        log.info('Got reply from %s: %s', station_id, reply)
        assert redis_server.llen('reply-%s' % station_id) == 0
        assert redis_server.hexists('waiting', station_id)

        redis_server.lpush('reply-%s' % station_id, reply)

    @classmethod
    def _get_event_for_device(cls, device_type: DeviceType, device_status: bool):
        if device_type == DeviceType.DRAWER:
            return {
                'type': DRAWER_STATUS_TO_EVENT_TYPE_MAP[device_status],
            }

        if device_type in [DeviceType.PRINTER, DeviceType.SAT, DeviceType.PINPAD]:
            return {
                'type': 'DEVICE_STATUS_CHANGED',
                'device': device_type.value,
                'status': device_status,
            }

    @classmethod
    def add_event_device_status_changed(cls, station, device_type: DeviceType, device_status: bool):
        """Put a device status changed event in a station stream"""
        event = cls._get_event_for_device(device_type, device_status)
        if not event:
            return

        with suppress(EventStreamUnconnectedStation):
            cls.add_event(event, station=station)

    def _loop(self, stream: redis.client.PubSub, station_id):
        while True:
            data = stream.get_message(timeout=10)
            if data is None:
                yield "data: null\n\n"
                continue

            if data['type'] == 'subscribe':
                continue

            if data['data'] == STREAM_FORCE_CLOSE:
                log.info('Stream for station %s changed. Closing old stream', station_id)
                break

            yield "data: " + data['data'].decode() + "\n\n"
        log.info('Closed event stream for %s', station_id)

    def get(self):
        store = api.new_store()
        station = self.get_current_station(store, token=request.args['token'])
        if not station:
            log.info('Invalid token for event stream: %s', request.args['token'])
            return

        log.info('Estabilished event stream for %s', station.id)

        # Break any connections that are still listening on this station's stream
        redis_server.publish(station.id, STREAM_FORCE_CLOSE)

        stream = redis_server.pubsub()
        stream.subscribe(station.id)
        stream.subscribe(STREAM_ALL_BRANCHES)

        if redis_server.hexists('waiting', station.id):
            # There is a new stream for this station, but we were currently waiting for a reply from
            # the same station in the previous event stream. Put an invalid reply there, and clear
            # the flag so that the station can continue working
            redis_server.lpush('reply-%s' % station.id, STREAM_BROKEN)
            redis_server.hdel('waiting', station.id)

        # If we dont put one event, the event stream does not seem to get stabilished in the browser
        self.add_event({}, station)

        EventStreamEstablishedEvent.send(station)

        # This is the best time to check if there are pending transactions, since the frontend just
        # stabilished a connection with the backend (thats us).
        has_canceled = TefCheckPendingEvent.send()
        if has_canceled and has_canceled[0][1]:
            EventStream.add_event({'type': 'TEF_WARNING_MESSAGE',
                                   'message': ('Última transação TEF não foi efetuada.'
                                               ' Favor reter o Cupom.')},
                                  station=station)
            EventStream.add_event({'type': 'CLEAR_SALE'}, station=station)
        station_id = station.id
        store.close()
        return Response(self._loop(stream, station_id), mimetype="text/event-stream")

    def post(self):
        station_id = request.values.get('station_id')
        data = request.json

        if not station_id:
            EventStream.add_event({
                'type': 'EVENT_RECEIVED',
                'data': data,
            })
            return ('event put in streams from all connected stations', 200)

        with api.new_store() as store:
            try:
                station = store.get(BranchStation, station_id)
            except DataError as err:
                return make_response(str(err), 400)

            if not station:
                return make_response('station not found', 404)

            try:
                EventStream.add_event({'type': 'EVENT_RECEIVED', 'data': data}, station=station)
                return make_response('event put in stream from station %s' % station_id, 200)
            except EventStreamUnconnectedStation as err:
                return make_response(str(err), 400) class EventStreamBrokenException(Exception):
    pass STREAM_BROKEN = b'STREAM_BROKEN' @lock_printer
def check_drawer(store=None):
    from .restful import DrawerResource
    try:
        return DrawerResource.ensure_printer(get_current_station(store), retries=1)
    except (SerialException, InvalidReplyException, PrinterError):
        return None
    except socket.timeout as error:
        logger.warning(error)
        return None @lock_pinpad(block=False)
def check_pinpad():
    responses = CheckPinpadStatusEvent.send()
    if len(responses) > 0:
        return responses[0][1]
    return True @lock_sat(block=False)
def check_sat():
    if len(CheckSatStatusEvent.receivers) == 0:
        # No SAT was found, what means there is no need to warn front-end there is a missing
        # or broken SAT
        return True

    event_reply = CheckSatStatusEvent.send()
    return event_reply and event_reply[0][1] PROVIDER_MAP = {
    "BANESCARD CR": "BANESCARD",
    "BANESCARD DE": "BANESCARD",
    "CABAL CREDIT": "CABAL",
    "CABAL DEBITO": "CABAL",
    "CABAL ALIMENTACA": "CABAL VALE",
    "ELO CREDITO": "ELO",
    "ELO DEBITO": "ELO",
    "ELO VISA CRE": "ELO",
    "ELO VISA DEB": "ELO",
    "JCB NACIONAL": "JCB",
    "MAESTRO": "MASTER",
    "MAESTROCP": "MASTER",
    "Mastercard": "MASTER",
    "MASTERCARD": "MASTER",
    "MASTERCARD D": "MASTER",
    "MASTERCARD CREDITO": "MASTER",
    "MASTERCARD DEBIT": "MASTER",
    "Master Maestro": "MASTER",
    "SICREDI DEBI": "SICREDI",
    "SODEXO REFEI": "SODEXO",
    "SODEXO REFEICAO": "SODEXO",
    "SOROCRED CRE": "SOROCRED",
    "SOROCRED CREDITO": "SOROCRED",
    "TICKET RESTA": "TICKET REFEICAO",
    "TICKET RESTAURAN": "TICKET REFEICAO",
    "UP ALIM REFE": "UP ALIM REFEICAO",
    "UP PLANVALE": "UP ALIM REFEICAO",
    "UP POLICARD": "UP ALIM REFEICAO",
    "Visa Credito": "VISA",
    "VISA CREDITO": "VISA",
    "Visa Debito": "VISA",
    "VISA DEBITO": "VISA",
    "VISA ELECTRO": "VISA",
    "VISA ELECTRON": "VISA",
    "VISA ELECTRON NA": "VISA",
    "VR REFEICAO": "VR",
    "REFEICAO": "ALELO"
} class LockFailedException(Exception):
class base_lock_decorator:
class lock_pinpad(base_lock_decorator):
class lock_sat(base_lock_decorator):
    def __init__(self, block):
    def __call__(self, func):
        def new_func(*args, **kwargs):
def lock_printer(func):
    def new_func(*args, **kwargs): def login_required(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        # token should be sent through a header using the format 'Bearer <token>'
        auth = request.headers.get('Authorization', '').split('Bearer ')
        if len(auth) != 2:
            log.warning('Invalid Authorization header: %s', request.headers.get('Authorization'))
            abort(401)

        with api.new_store() as store:
            access_token = AccessToken.get_by_token(store=store, token=auth[1])
            if not access_token:
                log.warning('Token not found: %s', auth)
                abort(403, "invalid token {}".format(auth[1]))

            if not access_token.is_valid():
                abort(403, "token {}".format(access_token.status))

            # FIXME: the user utility acts as a singleton and since we'd like to have stoqserver API
            # accepting requests from different stations (users), we cannot use this pattern as it
            # can lead to racing problems. For now we are willing to put a lock in every request,
            # but the final solution should be a refactor that makes every endpoint use the user
            # provided in the token payload instead of this 'global' one.
            user = store.get(LoginUser, access_token.payload['user_id'])
            provide_utility(ICurrentUser, user, replace=True)

        return f(*args, **kwargs)
    return wrapper def store_provider(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        with api.new_store() as store:
            try:
                return f(store, *args, **kwargs)
            except Exception as e:
                store.retval = False
                raise e

    return wrapper class B1foodLoginResource(BaseResource):
    method_decorators = [info_logger]
    routes = ['/b1food/oauth/authenticate']

    def get(self):
        data = request.args
        if 'client_id' not in data:
            abort(400, 'Missing client_id')
        client_id = data['client_id']

        config = get_config()
        config_client_id = config.get("B1Food", "client_id") or ""
        access_token = config.get("B1Food", "access_token") or ""
        if client_id != config_client_id and config_client_id != "":
            log.error('Login failed for client_id %s', client_id)
            abort(403, 'Login failed for client_id {}'.format(client_id))

        return make_response(jsonify({
            'token_type': 'Bearer',
            'expires_in': -1,
            'access_token': access_token
        }), 200) class IncomeCenterResource(BaseResource):
    method_decorators = [b1food_login_required, info_logger]
    routes = ['/b1food/terceiros/restful/centrosrenda']

    def get(self):
        return [] class BranchResource(BaseResource):
    method_decorators = [login_required, store_provider]
    routes = ['/branch']

    def get(self, store):
        branches = []
        for branch in list(store.find(Branch)):
            branches.append({
                "id": branch.id,
                "name": branch.name,
                "acronym": branch.acronym,
                "is_active": branch.is_active,
                "crt": branch.crt
            })

        return make_response(jsonify({
            'data': branches
        }), 200) class ClientResource(BaseResource):
    """Client RESTful resource."""
    method_decorators = [login_required, store_provider]
    routes = ['/client']

    @classmethod
    def create_address(cls, person, address):
        if not address.get('city_location'):
            log.error('Missing city location')
            abort(400, "Missing city location")

        city_location = CityLocation.get(
            person.store,
            country=address['city_location']['country'],
            city=address['city_location']['city'],
            state=address['city_location']['state'],
        )
        if not city_location:
            log.error('Invalid city location: %s', address['city_location'])
            abort(400, "Invalid city location")

        Address(street=address['street'],
                streetnumber=address['streetnumber'],
                district=address['district'],
                postal_code=address['postal_code'],
                complement=address.get('complement'),
                is_main_address=address['is_main_address'],
                person=person,
                city_location=city_location,
                store=person.store)

    @classmethod
    def get_client(cls, store, cpf):
        tables = [Client,
                  Join(Person, Client.person_id == Person.id),
                  Join(Individual, Individual.person_id == Person.id)]
        return store.using(*tables).find(Client, Individual.cpf == cpf).one()

    @classmethod
    def create_client(cls, store, name, cpf, address=None):
        # TODO: Add phone number
        person = Person(name=name, store=store)
        Individual(cpf=cpf, person=person, store=store)
        if address:
            cls.create_address(person, address)

        client = Client(person=person, store=store)
        return client

    def _dump_client(self, client):
        person = client.person
        birthdate = person.individual.birth_date if person.individual else None

        last_items = {}
        # Disable this for now, since this query is taking way to long to process and no one really
        # uses this.
        # saleviews = person.client.get_client_sales().order_by(Desc('confirm_date'))
        # for saleview in saleviews:
        #     for item in saleview.sale.get_items():
        #         last_items[item.sellable_id] = item.sellable.description
        #         # Just the last 3 products the client bought
        #         if len(last_items) == 3:
        #             break

        if person.company:
            doc = person.company.cnpj
        else:
            doc = person.individual.cpf

        category_name = client.category.name if client.category else ""

        data = dict(
            id=client.id,
            category=client.category_id,
            doc=doc,
            last_items=last_items,
            name=person.name,
            birthdate=birthdate,
            category_name=category_name,
        )

        return data

    def _get_by_doc(self, store, data, doc):
        # Extra precaution in case we ever send the cpf already formatted
        document = format_cpf(raw_document(doc))

        person = Person.get_by_document(store, document)
        if person and person.client:
            data = self._dump_client(person.client)

        # Plugins that listen to this signal will return extra fields
        # to be added to the response
        responses = signal('CheckRewardsPermissionsEvent').send(doc)
        for response in responses:
            data.update(response[1])

        return data

    def _get_by_category(self, store, category_name):
        # Pre-fetch person and Individual as they will be used down the line by _dump_client
        tables = [Client,
                  Join(Person, Person.id == Client.person_id),
                  LeftJoin(Individual, Person.id == Individual.person_id),
                  LeftJoin(Company, Person.id == Company.person_id),
                  Join(ClientCategory, Client.category_id == ClientCategory.id)]
        clients = store.using(*tables).find((Client, Person, Individual, ClientCategory),
                                            ClientCategory.name == category_name)
        retval = []
        for data in clients:
            retval.append(self._dump_client(data[0]))
        return retval

    def get(self, store):
        doc = request.args.get('doc')
        name = request.args.get('name')
        category_name = request.args.get('category_name')

        if doc:
            return self._get_by_doc(store, {'doc': doc, 'name': name}, doc)
        if category_name:
            return self._get_by_category(store, category_name)

        return {'doc': doc, 'name': name}

    def post(self, store):
        data = self.get_json()

        client_name = data.get('client_name')
        client_document = data.get('client_document')
        address_info = data.get('address')

        log.debug("POST /client station: %s payload: %s",
                  self.get_current_station(store), data)

        # We should change the api callsite so that city_location is inside the address
        if address_info:
            address_info.setdefault('city_location', data.get('city_location'))

        if not client_name:
            log.error('no client_name provided: %s', data)
            return {'message': 'no client_name provided'}, 400

        if not client_document:
            log.error('no client_document provided: %s', data)
            return {'message': 'no client_document provided'}, 400
        if not validate_cpf(client_document):
            log.error('invalid client_document provided: %s', data)
            return {'message': 'invalid client_document provided'}, 400

        if not address_info:
            # Note that city location is validated when creating the address
            log.error('no address provided: %s', data)
            return {'message': 'no address provided'}, 400

        client = self.get_client(store, client_document)
        if client:
            log.error('client with cpf %s already exists', client_document)
            return {
                'message': 'A client with this CPF already exists',
                'data': {
                    'id': client.id,
                }
            }, 200

        client = self.create_client(store, client_name, client_document, address_info)
        return {
            'message': 'Client created',
            'data': {
                'id': client.id,
            }
        }, 201 class InventoryResource(BaseResource):
    method_decorators = [login_required, store_provider]
    routes = ['/inventory', '/inventory/<uuid:inventory_id>']

    def _apply_count_to_inventory(self, store, count, inventory):
        stock_not_managed = []
        unmanaged_barcodes = []

        for sellable, quantity in count.items():
            inventory_item = inventory.get_items().find(product_id=sellable.id).one()

            if inventory_item:
                inventory_item.counted_quantity = quantity
                continue

            stock_not_managed_item = {
                'counted_quantity': str(quantity),
                'product': {
                    'sellable': {
                        'barcode': sellable.barcode,
                        'code': sellable.code,
                        'description': sellable.description
                    }
                }
            }
            stock_not_managed.append(stock_not_managed_item)
            unmanaged_barcodes.append(sellable.barcode or sellable.code)

        if unmanaged_barcodes:
            log.warning('Some sellables have not their stock being managed: %s',
                        unmanaged_barcodes)

        return stock_not_managed

    def _convert_count_keys(self, store, count):
        """Convert the count dict keys from string barcodes or codes to its referring sellables.

        In case of barcodes or codes referring to no existing sellable, return them on a list.

        :param count: a dict with string barcodes or codes as keys and its numeric quantities as
        values"""
        data = {}
        not_found = set()

        for barcode, quantity in count.items():
            if not barcode or not isinstance(barcode, str):
                message = ('Invalid barcode provided: {}. '
                           'It should be a not empty string.').format(barcode)
                log.error(message)
                abort(400, message)

            if not isinstance(quantity, (int, Decimal)) or quantity < 0:
                message = ('Invalid quantity provided: {}. '
                           'It should be a not negative number.').format(quantity)
                log.error(message)
                abort(400, message)

            sellable = store.find(Sellable, barcode=str(barcode)).one() or \
                store.find(Sellable, code=str(barcode)).one()

            if not sellable:
                not_found.add(barcode)
                continue

            data[sellable] = quantity

        if not_found:
            log.warning('Some barcodes were not found: %s', not_found)

        return data, not_found

    def post(self, store):
        branch_id = self.get_arg('branch_id')
        count = self.get_arg('count')

        if not branch_id:
            message = 'No branch_id provided'
            log.error(message)
            abort(400, message)
        if not count:
            message = 'No count provided'
            log.error(message)
            abort(400, message)
        if not isinstance(count, dict):
            message = ('count should be a JSON with barcodes or codes as keys '
                       'and counted quantities as values')
            log.error(message)
            abort(400, message)

        branch = store.get(Branch, branch_id)
        if not branch:
            message = 'Branch {} not found'.format(branch_id)
            log.error(message)
            abort(404, message)

        login_user = self.get_current_user(store)
        station = self.get_current_station(store)

        sellable_count, not_found = self._convert_count_keys(store, count)
        query = Sellable in sellable_count.keys()

        inventory = Inventory.create_inventory(store, branch, station, login_user, query)
        stock_not_managed = self._apply_count_to_inventory(store, sellable_count, inventory)

        items_for_adjustment = inventory.get_items_for_adjustment()

        items_for_adjustment_list = []
        for item_for_adjustment in items_for_adjustment:
            items_for_adjustment_list.append({
                'recorded_quantity': str(item_for_adjustment.recorded_quantity.normalize()),
                'counted_quantity': str(item_for_adjustment.counted_quantity.normalize()),
                'difference': str(item_for_adjustment.difference.normalize()),
                'product': {
                    'sellable': {
                        'barcode': item_for_adjustment.product.sellable.barcode,
                        'code': item_for_adjustment.get_code(),
                        'description': item_for_adjustment.get_description()
                    }
                }
            })

        if not items_for_adjustment_list:
            inventory.cancel()

        return make_response(jsonify({
            'id': inventory.id,
            'identifier': inventory.identifier,
            'status': inventory.status,
            'not_found': list(not_found),
            'stock_not_managed': stock_not_managed,
            'items': items_for_adjustment_list
        }), 201)

    def put(self, store, inventory_id):
        new_status = self.get_arg('status')
        if not new_status:
            message = 'No status provided'
            log.error(message)
            abort(400, message)

        if new_status not in [Inventory.STATUS_CLOSED, Inventory.STATUS_CANCELLED]:
            message = 'Status should be {} or {}'.format(
                Inventory.STATUS_CLOSED, Inventory.STATUS_CANCELLED)
            log.error(message)
            abort(400, message)

        inventory = store.get(Inventory, inventory_id)
        if not inventory:
            message = 'Inventory with ID = {} not found'.format(inventory_id)
            log.error(message)
            abort(404, message)

        if new_status == Inventory.STATUS_CANCELLED:
            try:
                inventory.cancel()
            except AssertionError as err:
                log.error(str(err))
                abort(400, str(err))

        elif new_status == Inventory.STATUS_CLOSED:
            login_user = self.get_current_user(store)

            try:
                # FIXME: Move this message to InventoryItem.adjust() from stoqlib
                assert inventory.is_open(
                ), 'It isn\'t possible to close an inventory which is not opened'

                for item in inventory.get_items_for_adjustment():
                    item.actual_quantity = item.counted_quantity
                    item.reason = 'Automatic adjustment'
                    item.adjust(login_user, invoice_number=None)
                inventory.close()
            except AssertionError as err:
                log.error(str(err))
                abort(400, str(err))

        return make_response(jsonify({
            'id': inventory.id,
            'identifier': inventory.identifier,
            'status': inventory.status,
        }), 200) class NfePurchaseResource(BaseResource):
    routes = ['/api/v1/invoice/import']
    method_decorators = [login_required, store_provider]

    def _get_imported_nfe_data(self, store, imported_nfe_id):
        responses = GetImportedNFeByIdEvent.send(store, id=imported_nfe_id)
        imported_nfe_data = responses[0][1]

        return imported_nfe_data

    def post(self, store):
        data = self.get_json()
        imported_nfe_id = data.get('imported_nfe_id')
        branch_id = data.get('branch_id')

        if not imported_nfe_id:
            message = 'No imported_nfe_id provided'
            log.error(message)
            abort(400, message)

        if not branch_id:
            message = 'No branch_id provided'
            log.error(message)
            abort(400, message)

        branch = store.get(Branch, branch_id)
        if not branch:
            message = 'Branch not found'
            log.error(message)
            abort(400, message)

        imported_nfe = self._get_imported_nfe_data(store, imported_nfe_id)
        if not imported_nfe:
            message = 'ImportedNfe not found'
            log.error(message)
            abort(400, message)

        xml_encoded = etree.tostring(imported_nfe.xml, encoding='unicode')
        nfe = NFe(xml_encoded, store)
        nfe_purchase = nfe.process(branch_id=branch_id)

        return Response(json.dumps({'id': nfe_purchase.id,
                                    'invoice_number': nfe_purchase.invoice_number,
                                    'invoice_series': nfe_purchase.invoice_series,
                                    'process_date': str(nfe_purchase.process_date)}),
                        201,
                        mimetype='application/json') class ImportedNfeResource(BaseResource):
    method_decorators = [login_required, store_provider]
    routes = ['/api/v1/imported_nfe']

    def get(self, store):
        from stoqnfe.domain.distribution import ImportedNfe

        cnpj = self.get_arg('cnpj')
        limit = self.get_arg('limit')
        offset = self.get_arg('offset')

        if not cnpj:
            message = "'cnpj' not provided"
            log.error(message)
            abort(400, message)

        if not validate_cnpj(cnpj):
            message = "Invalid 'cnpj' provided"
            log.error(message)
            abort(400, message)

        if limit is not None:
            try:
                limit = int(limit)
            except (TypeError, ValueError):
                message = "'limit' must be a number"
                log.error(message)
                abort(400, message)

            if limit > MAX_PAGE_SIZE:
                message = "'limit' must be lower than %s" % MAX_PAGE_SIZE
                log.error(message)
                abort(400, message)

        if offset is not None:
            try:
                offset = int(offset)
            except (TypeError, ValueError):
                message = "'offset' must be a number"
                log.error(message)
                abort(400, message)

        cnpj = format_cnpj(raw_document(cnpj))
        limit = limit or 20
        offset = offset or 0

        login_user = self.get_current_user(store)
        tables = [Branch, Join(Person, Branch.person_id == Person.id),
                  Join(Company, Company.person_id == Person.id)]
        query = Eq(Company.cnpj, cnpj)
        branches = store.using(*tables).find(Branch, query)

        # XXX There should exist at least one branch in database with
        # the cnpj from ImportedNfes. Otherwise, there is something wrong that
        # could lead to unwanted access to these ImportedNfes.
        assert branches

        for branch in branches:
            has_access = UserBranchAccess.has_access(store, login_user, branch)
            if has_access:
                continue

            message = 'login_user %s does not have access to branch %s' % \
                (login_user.id, branch.id)
            log.error(message)
            abort(403, message)

        query = And(ImportedNfe.cnpj == cnpj,
                    Cast(ImportedNfe.xml, 'text').startswith(NFEPROC_TYPE))
        result = store.find(ImportedNfe, query).order_by(Desc(ImportedNfe.te_id))
        result_count = result.count()
        imported_nfes = result.config(offset=offset, limit=limit)

        records = []
        for imported_nfe in imported_nfes:
            # FIXME: Change it to a store.find() when NFePurchase.key had been implemented
            query = "SELECT id FROM nfe_purchase WHERE cnpj='{}' AND xml::text ilike '%{}%'"
            nfe_purchase = store.execute(query.format(imported_nfe.cnpj,
                                                      imported_nfe.key)).get_one()

            process_date = imported_nfe.process_date
            record = {
                'id': imported_nfe.id,
                'key': imported_nfe.key,
                # Since process_date is a new column, we can't assure that
                # all entries have it fulfilled
                'process_date': process_date and process_date.isoformat(),
                'purchase_invoice_id': nfe_purchase and nfe_purchase[0]
            }
            records.append(record)

        next_offset = offset + limit
        has_next = result_count > next_offset
        next_ = None
        if has_next:
            next_ = self.routes[0] + '?limit={}&offset={}&cnpj={}'.format(
                limit, offset + limit, cnpj)

        has_previous = offset > 0
        previous = None
        if has_previous:
            previous = self.routes[0] + '?limit={}&offset={}&cnpj={}'.format(
                limit, max(offset - limit, 0), cnpj)

        response = {
            'previous': previous,
            'next': next_,
            'count': len(records),
            'total_records': result_count,
            'records': records
        }
        return make_response(jsonify(response), 200) class SellableResource(BaseResource):
    method_decorators = [login_required, store_provider]
    routes = [
        '/sellable',
        '/sellable/<uuid:sellable_id>',
        '/sellable/<uuid:sellable_id>/override/<uuid:branch_id>'
    ]

    def _price_validation(self, data):
        try:
            base_price = Decimal(data.get('base_price', "0.01") or '0.01')
        except (ValueError, DecimalException):
            message = 'Price with incorrect format'
            log.error(message)
            abort(400, message)

        if base_price and base_price < 0:
            message = 'Price must be greater than 0'
            log.error(message)
            abort(400, message)

        return base_price

    def _create_sellable_dict(self, sellable, image):
        return {
            'id': sellable.id,
            'barcode': sellable.barcode,
            'description': sellable.description,
            'notes': sellable.notes,
            'image_id': image.id if image else None
        }

    def post(self, store):
        data = self.get_json()

        log.debug("POST /sellable station: %s payload: %s",
                  self.get_current_station(store), data)

        if 'product' not in data:
            abort(400, 'There is no product data on payload')

        sellable_id = data.get('sellable_id')
        barcode = data.get('barcode')
        description = data.get('description')
        base_price = self._price_validation(data)
        sellable = store.get(Sellable, sellable_id)
        sellable_created_via_sale = sellable and Sellable.NOTES_CREATED_VIA_SALE in sellable.notes

        if sellable and not sellable_created_via_sale:
            message = 'Product with id {} already exists'.format(sellable_id)
            log.warning(message)
            return make_response(jsonify({
                'message': message,
            }), 200)

        if barcode and store.find(Sellable, barcode=barcode):
            message = 'Product with barcode {} already exists'.format(barcode)
            log.warning(message)
            return make_response(jsonify({
                'message': message,
            }), 200)

        if not sellable:
            sellable = Sellable(store=store)
            if sellable_id:
                sellable.id = sellable_id
        sellable.code = barcode
        sellable.barcode = barcode
        sellable.description = description
        # FIXME The sellable is created with STATUS_CLOSED because we need the taxes info
        # to start selling so this is just a temporary sellable just to save it on the
        # database so the override can be created
        sellable.status = Sellable.STATUS_CLOSED
        sellable.base_price = base_price
        # If the sellable was pre-created on a sale it has a notes informing it and to
        # proceed this note is removed
        sellable.notes = sellable.notes.replace(Sellable.NOTES_CREATED_VIA_SALE, "")

        product = sellable.product if sellable_created_via_sale else (
            Product(store=store, sellable=sellable))

        product_data = data.get('product')
        product.manage_stock = product_data.get('manage_stock', False)

        # For clients that will control their inventory, we have to create a Storable
        if product.manage_stock and not store.get(Storable, product.id):
            storable = Storable(store=store, product=product)
            storable.maximum_quantity = 1000

        return make_response(jsonify({
            'message': 'Product created',
            'data': {
                'id': sellable.id,
                'barcode': sellable.barcode,
                'description': sellable.description,
                'status': sellable.status,
            }
        }), 201)

    def put(self, store, sellable_id, branch_id):
        data = self.get_json()
        status = data.get('status')
        base_price = self._price_validation(data)

        log.debug("PUT /sellable/%s/override/%s station: %s payload: %s",
                  sellable_id, branch_id, self.get_current_station(store), data)

        if status and status not in [Sellable.STATUS_AVAILABLE, Sellable.STATUS_CLOSED]:
            message = 'Status must be: {} or {}'.format(Sellable.STATUS_AVAILABLE,
                                                        Sellable.STATUS_CLOSED)
            log.error(message)
            abort(400, message)

        sellable = store.get(Sellable, sellable_id)
        if not sellable:
            message = 'Sellable with ID = {} not found'.format(sellable_id)
            log.error(message)
            abort(404, message)

        branch = store.get(Branch, branch_id)
        if not branch:
            message = 'Branch with ID = {} not found'.format(branch_id)
            log.error(message)
            abort(404, message)

        sbo = SellableBranchOverride.find_by_sellable(branch=branch, sellable=sellable)
        if not sbo:
            sbo = SellableBranchOverride(store=store,
                                         branch=branch,
                                         sellable=sellable)
        if status:
            sbo.status = status
        sbo.base_price = base_price or sbo.base_price

        return make_response(jsonify({
            'message': 'Product updated',
            'data': {
                'sellable_id': sellable_id,
                'base_price': str(sbo.base_price),
                'status': sbo.status,
                'branch_id': branch_id
            }
        }), 200)

    def get(self, store, sellable_id=None):
        if sellable_id:
            sellable = store.get(Sellable, sellable_id)
            if not sellable:
                message = 'Sellable with ID = {} not found'.format(sellable_id)
                log.error(message)
                abort(404, message)

            image = store.find(Image, sellable_id=sellable.id).any()

            return make_response(jsonify({
                "data": self._create_sellable_dict(sellable, image)
            }), 200)

        sellables = []
        for sellable in store.find(Sellable):
            image = store.find(Image, sellable_id=sellable.id).one()
            sellables.append(self._create_sellable_dict(sellable, image))

        # TODO: Maybe add pagination
        return make_response(jsonify({'data': sellables}), 200) class IntegrationWebhookException(Exception):
class WebhookEventResource(BaseResource):
    def check_token(self, token):
    def post(self):
import base64
import datetime
import decimal
import functools
import io
import logging
import gevent
import requests
from decimal import Decimal
from typing import Dict, Optional
from blinker import signal, ANY as ANY_SENDER
from stoqlib.lib.component import provide_utility
from flask import request, abort, send_file, make_response, jsonify
from stoqlib.api import api
from stoqlib.database.interfaces import ICurrentUser
from stoqlib.domain.events import SaleConfirmedRemoteEvent
from stoqlib.domain.image import Image
from stoqlib.domain.overrides import ProductBranchOverride, SellableBranchOverride
from stoqlib.domain.payment.group import PaymentGroup
from stoqlib.domain.payment.method import PaymentMethod
from stoqlib.domain.payment.card import CreditCardData, CreditProvider, CardPaymentDevice
from stoqlib.domain.payment.payment import Payment
from stoqlib.domain.person import (LoginUser, Person, Client, ClientCategory, Company,
                                   Transporter)
from stoqlib.domain.product import Product, Storable, ProductStockItem
from stoqlib.domain.purchase import PurchaseOrder
from stoqlib.domain.sale import Sale, SaleContext, Context, Delivery
from stoqlib.domain.station import BranchStation
from stoqlib.domain.token import AccessToken
from stoqlib.domain.payment.renegotiation import PaymentRenegotiation
from stoqlib.domain.sellable import (Sellable, SellableCategory,
                                     ClientCategoryPrice)
from stoqlib.domain.till import Till, TillSummary
from stoqlib.exceptions import LoginError, TillError, ExternalOrderError
from stoqlib.lib.configparser import get_config
from stoqlib.lib.dateutils import INTERVALTYPE_MONTH, create_date_interval, localnow
from stoqlib.lib.defaults import quantize
from stoqlib.lib.formatters import raw_document, format_document, format_cpf
from stoqlib.lib.translation import dgettext
from stoqlib.lib.pluginmanager import get_plugin_manager
from storm.expr import LeftJoin, Join, And, Eq, Ne, Coalesce, Sum
from stoqserver.app import is_multiclient
from stoqserver.lib.baseresource import BaseResource
from stoqserver.lib.eventstream import EventStream, EventStreamBrokenException, STREAM_BROKEN
from .checks import check_drawer, check_pinpad, check_sat
from .constants import PROVIDER_MAP
from .lock import lock_pinpad, lock_printer, lock_sat, printer_lock, LockFailedException
from ..api.decorators import login_required, store_provider
from ..signals import (GenerateAdvancePaymentReceiptPictureEvent,
                       GenerateInvoicePictureEvent, GenerateTillClosingReceiptImageEvent,
                       GrantLoyaltyPointsEvent, PrintAdvancePaymentReceiptEvent,
                       PrintKitchenCouponEvent, FinishExternalOrderEvent,
                       SearchForPassbookUsersByDocumentEvent, StartPassbookSaleEvent,
                       TefPrintReceiptsEvent, StartExternalOrderEvent,
                       CancelExternalOrderEvent, GenerateExternalOrderReceiptImageEvent,
                       PrintExternalOrderEvent, ReadyToDeliverExternalOrderEvent,
                       PrintTillEntryCouponEvent)
from stoqserver.api.resources.b1food import (B1foodLoginResource, IncomeCenterResource)
from stoqserver.api.resources.branch import BranchResource
from stoqserver.api.resources.client import ClientResource
from stoqserver.api.resources.inventory import InventoryResource
from stoqserver.api.resources.invoice import NfePurchaseResource
from stoqserver.api.resources.imported_nfe import ImportedNfeResource
from stoqserver.api.resources.sellable import SellableResource
from stoqserver.api.resources.webhook import WebhookEvent
    from stoqnfe.events import NfeProgressEvent, NfeWarning, NfeSuccess
    from stoqnfe.exceptions import PrinterException as NfePrinterException, NfeRejectedException
    from stoqsat.exceptions import PrinterException as SatPrinterException
    from storm.references import Reference
        from stoqpassbook.domain import AdvancePayment
        station = store.find(BranchStation, name=station_name, is_active=True).one()
        global PDV_VERSION
        PDV_VERSION = request.args.get('pdv_version')
        if not station:
            log.info('Access denied: station not found: %s', station_name)
            abort(401)

        try:
            # FIXME: Respect the branch the user is in.
            user = LoginUser.authenticate(store, username, pw_hash, current_branch=None)
            provide_utility(ICurrentUser, user, replace=True)
        except LoginError as e:
            log.error('Login failed for user %s', username)
            abort(403, str(e))

        token = AccessToken.get_or_create(store, user, station).token
        return jsonify({
            "token": "JWT {}".format(token),
            "user": {"id": user.id},
        })


class LogoutResource(BaseResource):

    routes = ['/logout']
    method_decorators = [store_provider]

    def post(self, store):
        token = self.get_arg('token')
        token = token and token.split(' ')
        token = token[1] if len(token) == 2 else None

        if not token:
            abort(401)

        token = AccessToken.get_by_token(store=store, token=token)
        if not token:
            abort(403, "invalid token")
        token.revoke()

        return jsonify({"message": "successfully revoked token"})


class AuthResource(BaseResource):
    """Authenticate a user agasint the database.

    This will not replace the ICurrentUser. It will just validate if a login/password is valid.
    """

    routes = ['/auth']
    method_decorators = [login_required, store_provider]

    def post(self, store):
        username = self.get_arg('user')
        pw_hash = self.get_arg('pw_hash')
        permission = self.get_arg('permission')

        try:
            # FIXME: Respect the branch the user is in.
            user = LoginUser.authenticate(store, username, pw_hash, current_branch=None)
        except LoginError as e:
            return make_response(str(e), 403)

        if user.profile.check_app_permission(permission):
            return True
        return make_response(_('User does not have permission'), 403)


class TefResource(BaseResource):
    routes = ['/tef/<signal_name>']
    method_decorators = [login_required, store_provider]

    @lock_printer
    def _print_callback(self, lib, holder, merchant):
        printer = api.device_manager.printer
        if not printer:
            return

        # TODO: Add paramter to control if this will be printed or not
        if merchant:
            printer.print_line(merchant)
            printer.cut_paper()
        if holder:
            printer.print_line(holder)
            printer.cut_paper()

    def _message_callback(self, lib, message, can_abort=False, qrcode=None):
        with api.new_store() as store:
            station = self.get_current_station(store)
            EventStream.add_event({
                'type': 'TEF_DISPLAY_MESSAGE',
                'message': message,
                'can_abort': can_abort,
                'qrcode': qrcode,
            }, station=station)

        # tef library (ntk/sitef) has some blocking calls (specially pinpad comunication).
        # Before returning, we need to briefly hint gevent to let the EventStream co-rotine run,
        # so that the message above can be sent to the frontend.
        gevent.sleep(0.001)

    def _question_callback(self, lib, question):
        with api.new_store() as store:
            station = self.get_current_station(store)
            reply = EventStream.ask_question(station, question)

        if reply is STREAM_BROKEN:
            raise EventStreamBrokenException()
        return reply

    @lock_pinpad(block=True)
    def post(self, store, signal_name):
        station = self.get_current_station(store)
        if signal_name not in ['StartTefSaleSummaryEvent', 'StartTefAdminEvent']:
            till = Till.get_last(store, station)
            if not till or till.status != Till.STATUS_OPEN:
                raise TillError(_('There is no till open'))

        try:
            # Only lock printer in single client mode

            if not is_multiclient():