andrey-ushakov / esc_pos_bluetooth

ESC/POS (thermal, receipt) printing for Flutter & Dart (Android/iOS)
BSD 3-Clause "New" or "Revised" License
246 stars 326 forks source link

How to print more than one ticket based on user input? #87

Open frehiwott opened 3 years ago

frehiwott commented 3 years ago

I was trying to print more than one ticket per user input. But it is not working. I tried different options, but no result.

aviavinas commented 1 year ago

Here is the code i have implemented a printing queue.

import 'dart:async';
import 'dart:collection';

import 'package:bluetooth_enable_fork/bluetooth_enable_fork.dart';
import 'package:collection/collection.dart';
import 'package:esc_pos_bluetooth/esc_pos_bluetooth.dart';
import 'package:esc_pos_utils/esc_pos_utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:location/location.dart' as loc;
import 'package:permission_handler/permission_handler.dart';
import 'package:shopto/models/MOrder.dart';
import 'package:shopto/models/Usr.dart';
import 'package:shopto/utils/reusable.dart';
import 'package:shopto/utils/theme.dart';

class BTPrinter {
  static final PrinterBluetoothManager _printerManager =
      PrinterBluetoothManager();
  static List<PrinterBluetooth> _devices = [];
  static Duration duration = const Duration(seconds: 10);
  static bool _isScanning = false;
  static const PaperSize paper = PaperSize.mm58;
  static final Queue<List<int>> _printingQueue = Queue<List<int>>();

  static stopScanDevices() {
    _printerManager.stopScan();
    _isScanning = false;
  }

  static Future<bool> initPrinter() async {
    _devices = [];

    List<Permission> permissions = [
      Permission.bluetooth,
      Permission.bluetoothScan,
      Permission.bluetoothConnect,
      Permission.location,
    ];

    if (await Usr.runOnce("pos_printer_permission")) {
      // Show a consent dialogue to allow background location for printing
      await showConsent();
    }

    await Future.delayed(const Duration(milliseconds: 500));

    Map<Permission, PermissionStatus> statuses = await permissions.request();

    if (statuses.values.any((p) => p != PermissionStatus.granted)) {
      showToast(
          'Permission denied ! Bluetooth and Location permission required to print');
      return false;
    }

    await enableBluetooth();
    await enableLocation();

    Completer<bool> completer = Completer();

    if (!_isScanning && _devices.isEmpty) {
      try {
        _printerManager.scanResults.listen((devices) async {
          pp('UI: Devices found ${devices.length}');
          _devices = devices;
          if (_devices.isNotEmpty) {
            stopScanDevices();
            if (!completer.isCompleted) completer.complete(true);
          }
        });
        _printerManager.startScan(duration);
        _isScanning = true;

        await Future.delayed(duration);
        _isScanning = false;

        if (_devices.isEmpty) {
          showToast(
              "No printer founds ! Make sure printer is on and try unpairing the printer.");
          if (!completer.isCompleted) completer.complete(false);
        }
      } on Exception catch (_) {
        if (!completer.isCompleted) completer.complete(false);
      }
    } else if (!completer.isCompleted) {
      completer.complete(true);
    }
    return completer.future;
  }

  static showConsent() async {
    await Future.delayed(const Duration(milliseconds: 320));
    await showModalBottomSheet(
      context: navigator.context,
      builder: (context) => Container(
        height: 300,
        padding: const EdgeInsets.all(20),
        child: Column(
          children: [
            Text('Allow background location', style: Txt.h2.bold()),
            const SizedBox(height: 20),
            Text(
              'To print receipts, we need to access your location in the background.',
              textAlign: TextAlign.center,
              style: Txt.h6,
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                goBack();
              },
              child: const Center(child: Text('Allow')),
            ),
          ],
        ),
      ),
    );
  }

  static _print(List<int> ticketIns) async {
    // Add the ticket to the printing queue
    _printingQueue.add(ticketIns);

    // If the queue has more than one item, wait for the previous item to finish printing
    if (_printingQueue.length > 1) {
      return;
    }

    // Process the printing queue
    while (_printingQueue.isNotEmpty) {
      final currentTicket = _printingQueue.first;

      if (_devices.isEmpty) {
        bool initialized = await initPrinter();
        if (!initialized) {
          _printingQueue.removeFirst(); // Remove the failed ticket
          continue;
        }
      }

      if (_devices.isEmpty) {
        showToast(
            "No printer founds ! Make sure printer is on and try unpairing the printer.");
        _printingQueue.removeFirst(); // Remove the failed ticket
        continue;
      }

      _printerManager.selectPrinter(_devices.first);

      final PosPrintResult res =
          await _printerManager.printTicket((currentTicket));
      showToast(res.msg);

      await Future.delayed(const Duration(seconds: 5));

      _printingQueue.removeFirst(); // Remove the printed ticket
    }
  }

  static List<int> _getHeader(Generator ticket, String orderId) {
    List<int> bytes = [];

    // if (Usr.seller.avatarBytes != null) {
    //   final ByteData data = Usr.seller.avatarBytes;
    //   final Uint8List imageBytes = data.buffer.asUint8List();
    //   final Image image = decodeImage(imageBytes);
    //   bytes += ticket.image(image);
    // }

    bytes += ticket.text(Usr.seller.businessName,
        styles: const PosStyles(
            align: PosAlign.center,
            height: PosTextSize.size2,
            width: PosTextSize.size2),
        linesAfter: 1);

    bytes += ticket.text('Ph: ${Usr.seller.phone}',
        styles: const PosStyles(align: PosAlign.center));
    bytes += ticket.text('Web: ${Usr.seller.storeLink}',
        styles: const PosStyles(align: PosAlign.center));
    if (Usr.seller.gstEnabled) {
      bytes += ticket.text('GST: ${Usr.seller.gstIN}',
          styles: const PosStyles(align: PosAlign.center));
    }
    bytes += ticket.text('Order No: #$orderId',
        styles: const PosStyles(align: PosAlign.center), linesAfter: 1);

    return bytes;
  }

  static List<int> _getFooter(Generator ticket, MOrder order) {
    List<int> bytes = [];

    bytes += ticket.hr();
    bytes += ticket.feed(1);

    if (order.tableId?.isEmpty ?? false) {
      bytes += ticket.text('Table No. : ${order.tableId}',
          styles: const PosStyles(align: PosAlign.center, underline: true),
          linesAfter: 1);
    }

    bytes += ticket.text('Thank you!',
        styles: const PosStyles(align: PosAlign.center, bold: true));

    final now = DateTime.now();
    final formatter = DateFormat('MM/dd/yyyy H:m');
    final String timestamp = formatter.format(now);
    bytes += ticket.text(timestamp,
        styles: const PosStyles(align: PosAlign.center), linesAfter: 1);

    bytes += ticket.cut();

    return bytes;
  }

  static printKoT(List<MOrder> orders) async {
    DateTime now = DateTime.now();
    pp("Printing ....");
    List<int> bytes = await getKoTData(orders);
    pp("Get KOT data: ${DateTime.now().difference(now).inMilliseconds}");
    await _print(bytes);
    pp("Print KOT: ${DateTime.now().difference(now).inMilliseconds}");
  }

  static printBill(List<MOrder> orders) async {
    List<int> bytes = await getBillData(orders);
    await _print(bytes);
  }

  static printKoTAndBill(List<MOrder> orders) async {
    List<int> bytes = await getKoTData(orders);
    bytes += await getBillData(orders);
    await _print(bytes);
  }

  static Future<List<int>> getKoTData(List<MOrder> orders) async {
    List<List<MOrder>> groupedOrders =
        groupBy(orders, (o) => o.pid).values.toList();

    final profile = await CapabilityProfile.load();
    final Generator ticket = Generator(paper, profile);
    List<int> bytes = [];

    bytes += _getHeader(ticket, orders.first.transId);

    bytes += ticket.text("KITCHEN ORDER TICKET",
        styles: const PosStyles(align: PosAlign.center, bold: true));
    bytes += ticket.hr();

    bytes += ticket.row([
      PosColumn(text: 'Qty', width: 1),
      PosColumn(text: 'Item', width: 9),
      PosColumn(
          text: 'Price',
          width: 2,
          styles: const PosStyles(align: PosAlign.right)),
    ]);
    bytes += ticket.hr();

    groupedOrders.forEach((og) {
      bytes += ticket.row([
        PosColumn(text: '${og.length}', width: 1),
        PosColumn(text: og.first.title, width: 9),
        PosColumn(
            text: og.first.price.toStringAsFixed(2),
            width: 2,
            styles: const PosStyles(align: PosAlign.right)),
      ]);
    });

    bytes += _getFooter(ticket, orders.first);
    return bytes;
  }

  static Future<List<int>> getBillData(List<MOrder> orders) async {
    List<List<MOrder>> groupedOrders =
        groupBy(orders, (o) => o.pid).values.toList();

    final profile = await CapabilityProfile.load();
    final Generator ticket = Generator(paper, profile);
    List<int> bytes = [];

    bytes += _getHeader(ticket, orders.first.transId);

    bytes += ticket.row([
      PosColumn(text: 'Qty', width: 1),
      PosColumn(text: 'Item', width: 7),
      PosColumn(
          text: 'Price',
          width: 2,
          styles: const PosStyles(align: PosAlign.right)),
      PosColumn(
          text: 'Total',
          width: 2,
          styles: const PosStyles(align: PosAlign.right)),
    ]);
    bytes += ticket.hr();

    groupedOrders.forEach((og) {
      bytes += ticket.row([
        PosColumn(text: '${og.length}', width: 1),
        PosColumn(text: og.first.title, width: 7),
        PosColumn(
            text: og.first.price.toStringAsFixed(2),
            width: 2,
            styles: const PosStyles(align: PosAlign.right)),
        PosColumn(
            text: (og.first.price * og.length).toStringAsFixed(2),
            width: 2,
            styles: const PosStyles(align: PosAlign.right)),
      ]);
    });

    double subTotal = 0;
    orders.forEach((o) => subTotal += o.price);

    double taxRate = orders.first.taxRate;
    double taxable = subTotal / (1 + (taxRate / 100));
    double tax = subTotal - taxable;

    if (Usr.seller.gstEnabled) {
      bytes += ticket.hr();
      bytes += ticket.row([
        PosColumn(
            text: 'Sub Total :',
            width: 7,
            styles: const PosStyles(align: PosAlign.right)),
        PosColumn(
            text: subTotal.toStringAsFixed(2),
            width: 5,
            styles: const PosStyles(align: PosAlign.right)),
      ]);
      bytes += ticket.row([
        PosColumn(
            text: 'Taxable :',
            width: 7,
            styles: const PosStyles(align: PosAlign.right)),
        PosColumn(
            text: taxable.toStringAsFixed(2),
            width: 5,
            styles: const PosStyles(align: PosAlign.right)),
      ]);
      bytes += ticket.row([
        PosColumn(
            text: 'S.G.S.T @${(taxRate / 2).toStringAsFixed(1)}% :',
            width: 7,
            styles: const PosStyles(align: PosAlign.right)),
        PosColumn(
            text: (tax / 2).toStringAsFixed(2),
            width: 5,
            styles: const PosStyles(align: PosAlign.right)),
      ]);
      bytes += ticket.row([
        PosColumn(
            text: 'C.G.S.T @${(taxRate / 2).toStringAsFixed(1)}% :',
            width: 7,
            styles: const PosStyles(align: PosAlign.right)),
        PosColumn(
            text: (tax / 2).toStringAsFixed(2),
            width: 5,
            styles: const PosStyles(align: PosAlign.right)),
      ]);
    }

    bytes += ticket.hr(ch: '=');
    bytes += ticket.row([
      PosColumn(
          text: 'TOTAL',
          width: 6,
          styles: const PosStyles(
            height: PosTextSize.size2,
            width: PosTextSize.size2,
          )),
      PosColumn(
          text: subTotal.toStringAsFixed(2),
          width: 6,
          styles: const PosStyles(
            align: PosAlign.right,
            height: PosTextSize.size2,
            width: PosTextSize.size2,
          )),
    ]);

    bytes += _getFooter(ticket, orders.first);
    return bytes;
  }

  static enableLocation() async {
    loc.Location location = loc.Location();
    bool serviceEnabled = await location.serviceEnabled();
    if (!serviceEnabled) {
      serviceEnabled = await location.requestService();
      if (!serviceEnabled) {
        showToast("Location is required to use printer");
        return;
      }
    }
  }

  static enableBluetooth() async {
    BluetoothEnable.enableBluetooth.then((result) {
      if (result == "true") {
        // Bluetooth has been enabled
      } else if (result == "false") {
        showToast("Bluetooth is required to use printer");
      }
    });
  }
}