Connecting a POS Printer to Windows OS Using Django: A Comprehensive Guide
Tags: webdev, django, python, pos
Published: 2024-05-20 16:38:22

In this post, we’ll walk through the process of connecting a thermal printer or any external devices like a barcode reader, passport scanner to a Windows OS using Django. I am using the Oscar POS purchased from Amazon. We’ll cover setting up a virtual environment, creating a Django project, and building an interface for dynamic printing using `win32print`.

The project code can be found here: [GitHub Repository](https://github.com/tarek421995/POS-Printer)

Before starting, you should install the drivers that come with this printer. Other models do not require any driver to build a program to use them by using the `pyusb` library. More expensive devices have `product_id` and `vendor_id`.

At the end, I will provide a brief detail about using the `pyusb` library and connecting devices to it.

## Prerequisites
Before we begin, ensure you have the following:
- Python installed on your system
- Django installed in your Python environment
- A POS thermal printer for Invoices and Queue Tickets with the necessary drivers installed on your Windows OS

## Step 1: Setting Up the Virtual Environment
First, we’ll create a virtual environment for our project. Open your command prompt and run the following commands:
    
    # Install virtualenv if you haven't already
    pip install virtualenv

    # Create a virtual environment named 'printer_env'
    virtualenv printer_env

    # Activate the virtual environment
    # On Windows
    printer_env\Scripts\activate

    # On macOS/Linux
    source printer_env/bin/activate

## Step 2: Installing Required Packages
Within the virtual environment, install Django and other required packages:

    pip install django pywin32

## Step 3: Setting Up the Django Project
Create a new Django project and app:

    # Create a Django project named 'printer_project'
    django-admin startproject printer_project

    # Navigate to the project directory
    cd printer_project

    # Create a Django app named 'pos'
    python manage.py startapp pos

Add the new app to your `INSTALLED_APPS` in `printer_project/settings.py`:

```python
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'pos',
]

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / "templates"],  # Add this line
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
```

## Step 4: Setting Up Models
Define the models for saving sequence numbers and printer preferences. In `pos/models.py`, add:

```python
from django.db import models

class Sequence(models.Model):
    number = models.IntegerField(default=0)

class PrinterPreference(models.Model):
    device_name = models.CharField(max_length=255)
    device_id = models.CharField(max_length=255, unique=True)
    is_default = models.BooleanField(default=False)
    
    def __str__(self):
        return f"{self.device_name} - Default: {self.is_default}"
```

Run the migrations to create these models in the database:

    python manage.py makemigrations pos
    python manage.py migrate

## Step 5: Setting Up the Admin Interface
Register your models in `pos/admin.py`:

```python
from django.contrib import admin
from .models import PrinterPreference, Sequence

admin.site.register(PrinterPreference)
admin.site.register(Sequence)
```

## Step 6: Creating Views and Templates
Create views for handling sequence numbers and listing USB devices in `pos/views.py`:

```python
from django.http import JsonResponse, HttpResponse
from django.shortcuts import redirect, render
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt
from .models import Sequence, PrinterPreference
import win32print
import win32com.client
import pythoncom
import logging

def home(request):
    return render(request, "sequence.html")

@csrf_exempt  # For demonstration purposes only, consider CSRF in production
@require_http_methods(["POST"])
def sequence_view(request):
    number_to_print = request.POST.get('number_to_print')
    if 'reset' in request.POST:
        Sequence.objects.update(number=0)
        return JsonResponse({'number': 0, 'status': 'Sequence reset'})
    
    sequence = Sequence.objects.get(id=1)
    if number_to_print:
        try:
            number_to_print = int(number_to_print)
            sequence.number = number_to_print
            sequence.save()
            response = print_sequence(sequence.number)
            return JsonResponse({'number': number_to_print, 'status': 'Sequence Setting Success', **response})
        except ValueError:
            return JsonResponse({'status': 'Invalid number'}, status=400)
    else:
        sequence.number += 1
        sequence.save()
        response = print_sequence(sequence.number)
    return JsonResponse({'number': sequence.number, **response})

def print_sequence(number):
    data = b'\x1b\x40'
    data += b'\x1b\x37\x06\x15\x10'
    data += b'\x1b\x21\x30'
    data += 'Your Company Name\n'.encode('utf-8')
    data += b'\x1b\x21\x00'
    data += 'Registration\n'.encode('utf-8')
    data += b'\x1b\x21\x10'
    data += 'INFO : \n'.encode('utf-8')
    data += b'--------------------------------\n'
    data += b'\x1b\x21\x30'
    data += 'TOKEN NO:{}\n'.format(number).encode('utf-8')
    data += b'\x1d\x56\x42\x00'
    data += b'\x1B\x42\x00\x00'
    try:
        default_printer = PrinterPreference.objects.filter(is_default=True).first()
        if not default_printer:
            return {"status": "error", "message": "No default printer selected."}
        
        printer_name = default_printer.device_name
        printer_handle = win32print.OpenPrinter(printer_name)
        try:
            job_info = ("Token Print", None, "RAW")
            job_id = win32print.StartDocPrinter(printer_handle, 1, job_info)
            win32print.StartPagePrinter(printer_handle)
            win32print.WritePrinter(printer_handle, data)
            win32print.EndPagePrinter(printer_handle)
            win32print.EndDocPrinter(printer_handle)
        finally:
            win32print.ClosePrinter(printer_handle)
        return {"status": "success", "message": f"Printed sequence number {number}"}
    except Exception as e:
        return {"status": "error", "message": f"An error occurred: {e}"}

logging.basicConfig(level=logging.DEBUG)

def list_devices():
    try:
        pythoncom.CoInitialize()
        wmi = win32com.client.Dispatch("WbemScripting.SWbemLocator")
        wmi_service = wmi.ConnectServer(".", "root\cimv2")
        devices = wmi_service.ExecQuery("SELECT * FROM Win32_PnPEntity")
        device_list = [{'device_name': device.Name} for device in devices]
        return device_list
    except pythoncom.com_error as e:
        return [{'error': 'Failed to list devices due to a WMI error.'}]
    finally:
        pythoncom.CoUninitialize()

@csrf_exempt  # For demonstration purposes only, consider CSRF in production
@require_http_methods(["GET", "POST"])
def list_usb_devices(request):
    if request.method == "POST":
        default_printer_name = request.POST.get('default_printer')
        if default_printer_name:
            PrinterPreference.objects.all().update(is_default=False)
            our_printer, created = PrinterPreference.objects.get_or_create(device_name=default_printer_name)
            if our_printer:
                our_printer.is_default = True
                our_printer.save()
            return redirect('pos:home')
    device_list = list_devices()
    if not device_list:
        device_list = [{'error': "No USB devices found."}]
    return render(request, 'list_usb_devices.html', {'devices': device_list})
```

Create the templates `sequence.html` and `list_usb_devices.html` in `pos/templates/`:

`sequence.html`:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Sequence Number</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.bundle.min.js"></script>
    <style>
        .btn-pos { background-color: orange; color: white; padding: 20px 40px; font-size: 1.5rem; margin: 10px 0; }
        .btn-pos:hover { background-color: darkorange; }
        .input-pos { font-size: 1.5rem; padding: 10px

; }
        .logo-corner { position: absolute; top: 10px; left: 10px; }
        .container { max-width: 800px; }
    </style>
    <script>
        $(document).ready(function() {
            $('form').on('submit', function(e) {
                e.preventDefault();
                var numberToPrint = $('#number_to_print').val();
                var reset = $('button[name="reset"]').is(':focus');
                var postData = {'csrfmiddlewaretoken': '{{ csrf_token }}'};
                if (reset) {
                    postData['reset'] = true;
                } else {
                    postData['number_to_print'] = numberToPrint;
                }
                $.ajax({
                    type: 'POST',
                    url: "{% url 'pos:sequence_view' %}",
                    data: postData,
                    success: function(response) {
                        if (response.status === "success" || response.status === "Sequence reset") {
                            $('#number').text(response.number);
                            if (reset) {
                                $('form')[0].reset();
                            }
                        } else if (response.status === "error") {
                            alert("Error: " + response.message);
                        }
                    },
                    error: function(error) {
                        console.log(error);
                        alert('An error occurred.');
                    }
                });
            });
        });
    </script>
</head>
<body>
    <div class="logo-corner">
        <img src="https://eu-central-1.linodeobjects.com/roadslink/images/file.png" alt="Logo" height="60">
    </div>
    <div class="container mt-5">
        <h1 class="mb-3">Sequence Number: <span id="number">{{ number }}</span></h1>
        <form method="post" class="text-center">
            {% csrf_token %}
            <div class="form-group">
                <input type="text" class="form-control input-pos" id="number_to_print" name="number_to_print" placeholder="Enter/Set a number to print">
            </div>
            <button type="submit" name="print_next" class="btn btn-pos btn-block">Print Next Number</button>
            <button type="submit" name="reset" class="btn btn-pos btn-block">Reset</button>
        </form>
        <a href="{% url 'pos:list_usb_devices' %}">Select your device if not running</a>
    </div>
</body>
</html>
```

`list_usb_devices.html`:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>List USB Devices</title>
</head>
<body>
    <h1>Select Default Printer</h1>
    {% if error %}
        <p style="color: red;">Error: {{ error }}</p>
    {% endif %}
    <form method="post">
        {% csrf_token %}
        <ul>
            {% for device in devices %}
                <li>
                    <label>
                        <input type="radio" name="default_printer" value="{{ device.device_name }}">
                        {{ device.device_name }}
                    </label>
                </li>
            {% endfor %}
        </ul>
        <button type="submit">Set as Default Printer</button>
    </form>
</body>
</html>
```

## Step 7: Setting Up URL Routes
Define URL routes for your views in `pos/urls.py`:

```python
from django.urls import path
from .views import home, list_usb_devices, sequence_view

app_name = 'pos'
urlpatterns = [
    path('', home, name='home'),
    path('sequence_view', sequence_view, name='sequence_view'),
    path('list_usb_devices/', list_usb_devices, name='list_usb_devices'),
]
```

Include the `pos` app URLs in the main project URL configuration in `printer_project/urls.py`:

```python
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('pos.urls')),
]
```

## Step 8: Running the Project
Start the Django development server:

    python manage.py runserver

Navigate to `http://127.0.0.1:8000/` in your web browser. You should see the sequence number interface where you can print the next number or reset the sequence. Use the link `http://127.0.0.1:8000/list_usb_devices` to select and set your default printer.

## Testing with pyusb
Using this method is more product support but requires verified product factory who register his devices in all operating systems like Windows or Linux.

    pip install pyusb
    import usb.core
    import usb.util

    def list_usb_devices():
        # Find all connected USB devices
        devices = usb.core.find(find_all=True)

        # Iterate over each device and print information
        for device in devices:
            print(f"Device: {device}")
            print(f"  - idVendor: {hex(device.idVendor)}")
            print(f"  - idProduct: {hex(device.idProduct)}")
            print(f"  - Manufacturer: {usb.util.get_string(device, device.iManufacturer)}")
            print(f"  - Product: {usb.util.get_string(device, device.iProduct)}")
            print(f"  - Serial Number: {usb.util.get_string(device, device.iSerialNumber)}")
            print()

    if __name__ == "__main__":
        list_usb_devices()

**Result:**

    Device: DEVICE ID 1d6b:0003 on Bus 004 Address 001
     =================
     bLength                :   0x12 (18 bytes)
     bDescriptorType        :    0x1 Device
     bcdUSB                 :  0x310 USB 3.1
     bDeviceClass           :    0x9 Hub
     bDeviceSubClass        :    0x0
     bDeviceProtocol        :    0x3
     bMaxPacketSize0        :    0x9 (9 bytes)
     idVendor               : 0x1d6b
     idProduct              : 0x0003
     bcdDevice              :  0x602 Device 6.02
     iManufacturer          :    0x3 Error Accessing String
     iProduct               :    0x2 Error Accessing String
     iSerialNumber          :    0x1 Error Accessing String
     bNumConfigurations     :    0x1
      CONFIGURATION 1: 0 mA
      ====================================
       bLength              :    0x9 (9 bytes)
       bDescriptorType      :    0x2 Configuration
       wTotalLength         :   0x1f (31 bytes)
       bNumInterfaces       :    0x1
       bConfigurationValue  :    0x1
       iConfiguration       :    0x0 
       bmAttributes         :   0xe0 Self Powered, Remote Wakeup
       bMaxPower            :    0x0 (0 mA)
        INTERFACE 0: Hub
        =======================================
         bLength            :    0x9 (9 bytes)
         bDescriptorType    :    0x4 Interface
         bInterfaceNumber   :    0x0
         bAlternateSetting  :    0x0
         bNumEndpoints      :    0x1
         bInterfaceClass    :    0x9 Hub
         bInterfaceSubClass :    0x0
         bInterfaceProtocol :    0x0
         iInterface         :    0x0 
          ENDPOINT 0x81: Interrupt IN
          ==========================
           bLength          :    0x7 (7 bytes)
           bDescriptorType  :    0x5 Endpoint
           bEndpointAddress :   0x81 IN
           bmAttributes     :    0x3 Interrupt
           wMaxPacketSize   :    0x4 (4 bytes)
           bInterval        :    0xc

    - idVendor: 0x1d6b
    - idProduct: 0x3

Part 2: I will make another article about `pyusb` with a passport scanner.

## Bonus Section
I have created Docker composing and a Dockerfile for building an image that will work in any Operating Systems.

### Docker Installation
To run this project in a Docker container, use the following Dockerfile and `docker-compose.yml`.

**Dockerfile**

```dockerfile
FROM python:3.11

# Install dependencies
RUN apt-get update && apt-get install -y \
    usbutils \
    libusb-1.0-0-dev \
    && rm -rf /var/lib/apt/lists/*

# Set work directory
WORKDIR /app

# Install Python dependencies
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt

# Copy project files
COPY . .

# Set the environment variable for Django settings
ENV DJANGO_SETTINGS_MODULE=printer.settings

# Expose port 8000
EXPOSE 8000

# Run the Django server
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
```

**docker-compose.yml**

```yaml
version: '3.10'

services:
  web:
    build: .
    command: sh -c "python manage.py runserver 0.0.0.0:8000"
    volumes:
      - .:/app
    ports:
      - "8000:8000"
    environment:
     

 - DJANGO_SETTINGS_MODULE=printer.settings
    devices:
      - "/dev/bus/usb:/dev/bus/usb"
    privileged: true
```

## Conclusion
In this post, we covered setting up a virtual environment, creating a Django project, and building an interface for dynamic printing using `win32print`. By following these steps, you can connect and use a thermal printer with a Django web application on Windows OS. This setup allows for flexible and dynamic printing solutions tailored to your specific needs.

Feel free to customize the code and templates to suit your project requirements. If you have any questions or run into issues, leave a comment below, and I’ll be happy to help.

Happy printing and device integration!!