Asosiy kontentga o‘tish

Maqolalar

Darslik 19 daqiqa boshlang'ich 8 ko‘rish 0 yoqdi

Do'konlarda beriladigan cheklarni Python yordamida yasashni o'rganamiz

#python #database

Mundarija

Cho'ntagingizdagi do'kon chekini bir ko'rib chiqing. Unda mahsulot nomi, nechta olganingiz, bir donasining narxi, QQS foizi, jami summa, do'konning STIR raqami, sana, kassir ismi va pastda QR kod bor.

Bu ma'lumotlar chekda shunchaki turmaydi. Aslida chek — bazadagi ma'lumotlarning qog'ozga chiqarilgan ko'rinishi. Agar bazadan kerakli qatorlarni o'qiy olsangiz, xuddi shunday chekni o'zingiz ham yasay olasiz.

Keling, aynan shuni qilib ko'ramiz. Oxirida qo'lingizda shunday ko'rinishdagi tayyor PDF fayl bo'ladi:

Python bilan yasalgan chek: logo, STIR, mahsulotlar, QQS va QR kod

Bu rasm emas, PDF fayl. Uni chop etsangiz, qo'lingizda haqiqiy chek bo'ladi.

Nimalar kerak bo'ladi#

Bilim tomondan Python asoslarini — funksiya, sikl, f-string — va SQL'dan SELECT so'rovini bilish yetadi. PDF va QR kod bilan ishlashni esa shu yerning o'zida o'rganamiz.

Kutubxonalardan ikkitasini ishlatamiz:

  • reportlab — PDF yaratish uchun. Uni bo'm-bo'sh varaq deb tasavvur qiling: siz unga qaysi joyga nima yozish yoki chizishni aytasiz.

  • segno — QR kod yaratish uchun. Ishlatilishi sodda: ma'lumot berasiz, u sizga QR kod tayyorlab beradi.

Loyihani yasashni boshlaymiz#

Terminal
mkdir chek && cd chek
mkdir db cheklar qr logo
python3 -m venv .venv
source .venv/bin/activate
pip install reportlab segno Pillow

Papkalarni oldindan yaratib olganimiz bejiz emas. Chunki reportlab mavjud bo‘lmagan papkaga fayl yozmoqchi bo‘lsa, xatolik yuz beradi va dastur to‘xtaydi. Bu holatni ayniqsa dasturni birinchi marta ishga tushirganda ko‘p uchratish mumkin.

O'rnatilganini tekshiramiz:

Terminal
python -c "import reportlab, segno; print(reportlab.Version, segno.__version__)"
Natija
5.0.1 1.6.6

Sizda raqamlar boshqacharoq bo'lishi mumkin, muhimi — xatolik chiqmasin.

Papkalar shunday ko'rinishda tuziladi:

Fayl tuzilishi
chek/
├── .venv/
├── db/          ← baza shu papkada yaratiladi
├── cheklar/     ← tayyor PDF cheklar
├── qr/          ← QR rasmlari
└── logo/        ← do'kon logotipi

1-qadam: ma'lumot#

Chek yaratishni bazadan boshlaymiz. Buning uchun bizga bitta jadvalning o‘zi yetadi. Unda xaridor nima olgani, nechta olgani, narxi va QQS foizi saqlanadi.

seed.py
"""Mashq uchun ma'lumot: bitta savdo cheki uchun bir nechta mahsulot."""

import sqlite3

MAHSULOTLAR = [
    (1, "Non - bug'doy", 2, 4500.00, 12.0),
    (1, "Sut 1L - Nestle", 1, 14900.00, 12.0),
    (1, "Tuxum 10 dona", 1, 22000.00, 0.0),
    (1, "Qahva - Jacobs 190g", 1, 89000.00, 12.0),
    (1, "Shokolad - Milka", 3, 18500.00, 12.0),
]

con = sqlite3.connect("db/savdo.db")
cur = con.cursor()
cur.execute("DROP TABLE IF EXISTS Purchase")
cur.execute("""
    CREATE TABLE Purchase (
        id      INTEGER NOT NULL,   -- chek raqami
        product TEXT    NOT NULL,
        count   INTEGER NOT NULL,
        price   REAL    NOT NULL,   -- bir dona narxi
        qqs     REAL    NOT NULL    -- foizda
    )
""")
cur.executemany("INSERT INTO Purchase VALUES (?, ?, ?, ?, ?)", MAHSULOTLAR)
con.commit()
print(f"{cur.execute('SELECT COUNT(*) FROM Purchase').fetchone()[0]} ta qator yozildi")

E'tibor bering: birinchi ustun id — bu mahsulotning raqami emas, chek raqami. Bitta chekdagi hamma mahsulotda u bir xil bo'ladi. Ya'ni "1-chekda beshta mahsulot bor" degani.

Tuxumning QQS'i 0.0 — bu ataylab qilingan. Haqiqiy hayotda ayrim mahsulotlarga QQS qo‘llanmaydi. Shuning uchun kodimiz bunday holatlarni ham hisobga olishi kerak.

Terminal
python seed.py
Natija
5 ta qator yozildi

2-qadam: bazadan o'qish#

PDF yaratishga o‘tishdan oldin, ma’lumotlar to‘g‘ri ekanini tekshirib olaylik. Chekning har bir qatori uchun bizga beshta ma’lumot kerak bo‘ladi: mahsulot nomi, dona soni, narxi, jami summa (dona × narx) va QQS.

Yaxshi tomoni shundaki, jami summani Python’da alohida hisoblashimiz shart emas. dona × narx hisobini SQL’ning o‘zi bajarib beradi:

oqish.py
"""Endi bazadan bitta chek ichidagi barcha mahsulotlarni olib chiqamiz."""

import sqlite3

CHEK_RAQAMI = 1

con = sqlite3.connect("db/savdo.db")
cur = con.cursor()

qatorlar = cur.execute(
    """
    SELECT product, count, price, count * price, qqs
    FROM Purchase
    WHERE id = ?
    """,
    (CHEK_RAQAMI,),
).fetchall()

for nom, dona, narx, jami, qqs in qatorlar:
    print(f"{nom:<24} {dona} x {narx:>10,.2f} = {jami:>10,.2f}  (QQS {qqs:g}%)")
Natija
Non - bug'doy            2 x   4,500.00 =   9,000.00  (QQS 12%)
Sut 1L - Nestle          1 x  14,900.00 =  14,900.00  (QQS 12%)
Tuxum 10 dona            1 x  22,000.00 =  22,000.00  (QQS 0%)
Qahva - Jacobs 190g      1 x  89,000.00 =  89,000.00  (QQS 12%)
Shokolad - Milka         3 x  18,500.00 =  55,500.00  (QQS 12%)

So'rovdagi ? ga alohida to'xtalay. Chek raqamini so'rov matniga qo'shib yozish ham mumkin edi:

Python
# BUNDAY YOZMANG
cur.execute(f"SELECT ... WHERE id = {CHEK_RAQAMI}")

Bu usul ishlaydi. Lekin qiymat foydalanuvchidan keladigan bo‘lsa, uni so‘rov ichiga to‘g‘ridan-to‘g‘ri yozish xavfli. Bu SQL injection muammosiga olib kelishi mumkin. Bu mavzu haqida alohida maqola bor. Eng yaxshisi, boshidan to‘g‘ri usulga o‘rganamiz: qiymatni ? orqali alohida uzatamiz.

3-qadam: birinchi PDF va kutilmagan narsa#

Endi reportlab bilan tanishamiz. Avval bo‘sh varaq yaratib, unga ikkita joyga matn yozib ko‘ramiz:

sinov.py
"""Birinchi PDF orqali koordinatalar qayerdan boshlanishini ko‘rib olamiz."""

from reportlab.pdfgen import canvas

ENI, BOYI = 300, 200

can = canvas.Canvas("cheklar/sinov.pdf", pagesize=(ENI, BOYI))
can.setFont("Helvetica", 11)

can.drawString(20, 20, "men pastdaman (y=20)")
can.drawString(20, BOYI - 20, "men yuqoridaman (y=180)")

can.showPage()
can.save()
print("cheklar/sinov.pdf yozildi")

O'lchamlari punktda beriladi: 1 punkt ≈ 0.35 mm, Masalan, 300 punkt ≈ 10.6 sm. Do‘kon chekining eni odatda shunga yaqin bo‘ladi.

Terminal
python sinov.py
Natija
cheklar/sinov.pdf yozildi

cheklar/sinov.pdf ni ochib ko'ring:

PDF koordinatalari pastki chap burchakdan boshlanadi

Mana shu joyda ko‘pchilik adashadi. Ekranda, HTML'da yoki rasm tahrirlashda koordinata yuqori chap burchakdan boshlanadi va y pastga qarab o‘sadi, PDF'da esa buning aksi: nol nuqta pastki chap burchakda joylashadi va y yuqoriga qarab o‘sadi.

Shuning uchun y=20 pastroqda, y=180 esa yuqoriroqda chiqadi. Shu tamoyilni bir marta tushunib olsangiz, keyingi qismi ancha oson bo‘ladi.

4-qadam: bitta mahsulot qatorini chiqaramiz#

Chekda har bir mahsulot ikki ustunda yoziladi: chap tomonda nomi, o‘ng tomonda esa summasi. Summalar o‘ng chetiga tekislanadi, shunda barcha raqamlar bir chiziqda turadi va ularni ko‘z bilan solishtirish osonroq bo‘ladi.

reportlab'da buning uchun ikkita qulay funksiya bor:

qator.py
y = BOYI - 30
can.drawString(CHET, y, "Qahva - Jacobs 190g")  # chapdan boshlab yozadi
can.drawRightString(ENI - CHET, y, "89,000.00")  # o'ngdan tugatadi

can.drawString(CHET, y - 15, "1 x")
can.drawRightString(ENI - CHET, y - 15, "89,000.00")

can.drawString(CHET, y - 30, "QQS 12%")
can.drawRightString(ENI - CHET, y - 30, "10,680.00")

drawString berilgan nuqtadan o'ngga qarab yozadi, drawRightString esa matnni berilgan nuqtada tugatadi. Agar ikkinchisi bo‘lmaganida, har bir raqamning kengligini alohida hisoblab, chekinishni o‘zingiz belgilashingizga to‘g‘ri kelardi.

Har bir qatordan keyin y qiymatini 15 punktga kamaytiryapmiz, chunki chek bo‘ylab pastga qarab tushyapmiz.

5-qadam: ajratuvchi chiziq — taxmin qilmaymiz, o‘lchaymiz#

Chekdagi mahsulotlarni ----- ko‘rinishidagi chiziq bilan ajratamiz. Savol: nechta - yozish kerak?

Eng oson yo‘l — "60 ta bo‘lsa yetadi" deb yozib qo‘yish. Lekin shrift o‘lchami yoki chek eni o‘zgarsa, chiziq yo kalta qoladi, yo chetdan chiqib ketadi.

To‘g‘ri yo‘l — o‘lchash. reportlab matnning kengligini hisoblab bera oladi:

chiziq.py
def chiziq(can, y, belgi="-"):
    """Chiziq chek eniga mos ravishda chetdan chetgacha chiziladi."""
    kerak = ENI - CHET * 2
    soni = 1
    while can.stringWidth(belgi * soni) < kerak:
        soni += 1
    matn = belgi * (soni - 1)  # Ba’zan oxirgi belgisi chetdan chiqib ketishi mumkin.
    x = (ENI - can.stringWidth(matn)) / 2
    can.drawString(x, y, matn)
    return soni - 1

Sikl belgilarni bittadan qo‘shib boradi va kenglik belgilangan chegaradan oshishi bilan to‘xtaydi. Oxirgi qo‘shilgan belgi chetdan chiqib ketgani uchun, bir qadam orqaga qaytamiz.

Endi buni ikki xil belgi bilan sinab ko‘ramiz:

Natija
bir qatorga 70 ta '-' sig'di
bir qatorga 40 ta '=' sig'di

Farqni ko‘rdingizmi? = belgisi - belgisidan ancha keng. Agar belgilar sonini qo‘lda belgilaganimizda, = bilan chizilgan chiziq chek chegarasidan chiqib ketishi mumkin edi.

6-qadam: QR kod#

Endi chekning zamonaviy qismiga o‘tamiz. QR kod ichiga chek ma’lumotlarini joylaymiz — uni telefon bilan skanerlaganda, xaridor nima sotib olganini ko‘rishi mumkin.

segno bilan buni atigi uch qatorda qilish mumkin:

qr.py
def qr_yasash(chek_raqami, jami, qqs, vaqt):
    """QR ni faylga saqlab, yo'lini qaytaradi."""
    matn = (
        f"Chek: #{chek_raqami}\n"
        f"STIR: {STIR}\n"
        f"Sana: {vaqt:%d-%m-%Y %H:%M}\n"
        f"Kassir: {KASSIR}\n"
        f"Jami: {jami:,.2f} so'm\n"
        f"QQS: {qqs:,.2f} so'm"
    )
    yol = f"qr/chek{chek_raqami}.png"
    segno.make_qr(matn).save(yol, scale=3)
    return yol

scale=3 — QR nuqtalarining kattaligi. Qiymatni juda kichik qilsangiz, QR ixchamroq bo‘ladi, lekin telefon uni o‘qishda qiynalishi mumkin. 3 — chek uchun qulay va ishonchli o‘lcham.

Funksiya QR rasmini saqlab, uning yo‘lini qaytarayotganiga e’tibor bering. Bu bizga keyingi qismda kerak bo‘ladi: segno QR kodni boshqa formatlarda ham bera oladi, lekin reportlab bilan ishlashda rasm faylining yo‘lini uzatish eng sodda usul.

7-qadam: logo va rasmni markazga joylaymiz#

Har bir do‘kon logotipi turli o‘lchamda bo‘lishi mumkin. Shuning uchun kod logoni chek eniga moslashtirishi kerak, lekin uni cho‘zib yubormasdan, nisbatini saqlagan holda:

rasm.py
def rasm(can, yol, y, kenglik):
    """Rasmni markazga qo'yadi, kengligini berilgan o'lchamga keltiradi."""
    w, h = ImageReader(yol).getSize()
    balandlik = h * (kenglik / w)
    can.drawImage(yol, (ENI - kenglik) / 2, y, kenglik, balandlik, mask="auto")
    return balandlik

U yerda uchta ish bajariladi: rasmning asl o‘lchami olinadi, nisbat saqlangan holda yangi balandlik hisoblanadi va x koordinatasi rasm chek markaziga tushadigan qilib topiladi.

mask="auto" esa rasmning shaffof joylari oq kvadrat bo‘lib chiqib qolmasligi uchun kerak. Funksiya rasmning balandligini qaytaradi, chunki keyingi elementni qayerdan boshlashni shu orqali bilamiz.

Hammasini birlashtiramiz#

Endi barcha bo‘laklarni bitta faylga yig‘amiz. Tartib quyidagicha: logo, do‘kon ma’lumotlari, mahsulotlar, jami summa va QR kod.

Bu yerda bitta muhim qaror bor — sahifa balandligini oldindan hisoblash.

Mahsulotlar soni oldindan ma’lum, har biri qancha joy egallashi ham aniq. Demak, chek balandligi qancha bo‘lishini uni chizishdan oldin hisoblab olishimiz mumkin.

Python
boyi = CHET + qr_h + 60 + len(qatorlar) * QATOR + logo_h + CHET * 2
chek.py
"""Do'kon cheki: bazadan o'qib, PDF qilib chiqaramiz."""

import sqlite3
from datetime import datetime

import segno
from reportlab.lib.utils import ImageReader
from reportlab.pdfgen import canvas

DOKON = "BOZOR market"
STIR = 307575237
KASSIR = "Asadbek Solijonov"

ENI = 300  # chek eni, punktda (~10.6 sm)
CHET = 20  # yon chetlar
QATOR = 70  # bitta mahsulotga ajratilgan balandlik
SHRIFT = ("Helvetica", 11)


def mahsulotlar(chek_raqami):
    """Chekdagi qatorlar: (nomi, dona, narx, jami, qqs%, qqs summasi)."""
    con = sqlite3.connect("db/savdo.db")
    return con.execute(
        """
        SELECT product, count, price,
               count * price               AS jami,
               qqs,
               count * price * qqs / 100   AS qqs_summa
        FROM Purchase
        WHERE id = ?
        """,
        (chek_raqami,),
    ).fetchall()


def chek_yasash(chek_raqami):
    qatorlar = mahsulotlar(chek_raqami)
    if not qatorlar:
        raise ValueError(f"#{chek_raqami} chekda mahsulot yo'q")

    vaqt = datetime.now()
    jami = sum(q[3] for q in qatorlar)
    qqs = sum(q[5] for q in qatorlar)
    qr_yoli = qr_yasash(chek_raqami, jami, qqs, vaqt)

    logo_h = 200 * (120 / 600)  # logo 120pt kenglikda
    qr_h = 120  # QR ham 120pt
    boyi = CHET + qr_h + 60 + len(qatorlar) * QATOR + logo_h + CHET * 2

    yol = f"cheklar/chek{chek_raqami}.pdf"
    can = canvas.Canvas(yol, pagesize=(ENI, boyi))
    can.setFont(*SHRIFT)

    y = boyi - CHET
    y -= rasm(can, "logo/logo.png", y - logo_h, 120) + 10

    can.drawCentredString(ENI / 2, y - 12, f"STIR: {STIR}")
    can.drawCentredString(ENI / 2, y - 26, f"{vaqt:%d-%m-%Y  %H:%M}")
    y -= 40

    for nom, dona, narx, qator_jami, foiz, qator_qqs in qatorlar:
        chiziq(can, y)
        y -= 16
        can.drawString(CHET, y, nom[:28])
        can.drawRightString(ENI - CHET, y, f"{narx:,.2f}")
        can.drawString(CHET, y - 15, f"{dona} x")
        can.drawRightString(ENI - CHET, y - 15, f"{qator_jami:,.2f}")
        can.drawString(CHET, y - 30, f"QQS {foiz:g}%")
        can.drawRightString(ENI - CHET, y - 30, f"{qator_qqs:,.2f}")
        y -= QATOR - 16

    chiziq(can, y, "=")
    y -= 18
    can.drawString(CHET, y, "JAMI")
    can.drawRightString(ENI - CHET, y, f"{jami:,.2f}")
    can.drawString(CHET, y - 15, "shundan QQS")
    can.drawRightString(ENI - CHET, y - 15, f"{qqs:,.2f}")
    y -= 40

    rasm(can, qr_yoli, y - qr_h, qr_h)

    can.showPage()
    can.save()
    return yol


if __name__ == "__main__":
    print("Chek yasalmoqda...")
    print("Tayyor:", chek_yasash(1))

Yuqorida yozgan qr_yasash, chiziq va rasm funksiyalarini ham shu faylga qo‘shamiz. Ular 5-, 6- va 7-qadamlarda yozilgan edi.

Endi dasturni ishga tushiramiz:

Terminal
python chek.py
Natija
Chek yasalmoqda...
Tayyor: cheklar/chek1.pdf

cheklar/chek1.pdf faylini ochib ko‘ring. Maqola boshida ko‘rgan chekimiz aynan shu fayl.

Hisobni ham tekshirib olaylik: 9,000 + 14,900 + 22,000 + 89,000 + 55,500 = 190,400. Demak, chekdagi JAMI ham 190,400 bo‘lishi kerak.

Endi buzib ko'ring#

Kodni tushunganingizni tekshirishning eng yaxshi usullaridan biri — uni ataylab buzib ko'rish.

Har bir o'zgarishdan keyin python chek.py ni qayta ishga tushiring:

  1. ENI ni 300 dan 200 ga tushiring. Chiziq qisqaradimi? Mahsulot nomlari bir-biriga yaqinlashib yoki ustma-ust chiqib qoladimi?

  2. QATOR ni 70 dan 40 ga tushiring. Qatorlar bir-biriga kirishib ketadi. Demak, bu qiymat bejiz tanlanmagan.

  3. chiziq() ichidagi belgi ni "=" ga o'zgartiring. Chiziq baribir chek eniga mos tushadimi?

  4. qr/ papkasini o'chirib, dasturni qayta ishga tushiring. Qanday xatolik chiqadi? Endi papkalarni nega oldindan yaratganimiz aniqroq bo'ladi.

  5. Bazaga oltinchi mahsulot qo'shing (id = 1 bilan). Chek balandligi avtomatik uzayadimi?

Oxirgi sinov eng muhimi: agar yangi mahsulot qo'shilganda chek o'zi uzaysa, demak sahifa balandligini hisoblashni to'g'ri yozganmiz.

Shu qismning asosiy narsalari#

Nima Qayerda
canvas.Canvas(fayl, pagesize=(eni, boyi)) bo'sh PDF yaratish
drawString(x, y, matn) matnni chapdan yozish
drawRightString(x, y, matn) matnni o'ngga tekislab yozish
drawCentredString(x, y, matn) matnni markazga yozish
stringWidth(matn) matn kengligini o'lchash
drawImage(yol, x, y, w, h, mask="auto") rasm joylash
segno.make_qr(matn).save(yol, scale=3) QR kod yaratish
showPage() + save() sahifani yakunlash va PDF'ni saqlash

Va ikkita muhim qoida: PDF'da nol nuqta pastki chap burchakda, matn kengligi esa taxmin qilinmaydi, o'lchanadi.

Keyingi qismda#

Chek ishlayapti. Lekin kodning ishlashi bilan uning sifatli yozilgani — ikki xil narsa.

Keyingi qismda shu kodga tanqidiy ko'z bilan yana bir bor qaraymiz: do'kon nomi nega kod ichida yozilgan, chek raqamini kim beradi, bir xil QR nega ikki marta yaratiladi va requirements.txt faylida versiyalarni qulflamaslik vaqt o'tishi bilan qanday muammolarga olib kelishi mumkin.

Bular faqat nazariy muammolar emas — real loyihalarda ham tez-tez uchrab turadi.

8 ko‘rish
Ulashish