تقارير نهاية الوردية ونهاية اليوم هي أكثر ميزات نظام نقاط البيع أهمية تشغيلياً. إذا اختلفت الأرقام عن طباعة طرف الدفع، تتوقف العمليات. الخطأ في هذه التقارير يدمر الثقة أسرع من أي فشل آخر في نظام نقاط البيع.
تقارير نهاية الوردية ونهاية اليوم هي الميزة التشغيلية الأهم في أي نظام نقاط بيع. مدراء المطاعم يُغلقون الورديات مرتين يومياً. أمناء الصندوق يتسلمون بين الورديات. المحاسبون يوفقون الإجماليات اليومية مع مطبوعات طرفيات الدفع. إذا اختلف تقرير الوردية عن مطبوعة طرفية الدفع، تتوقف العمليات وينفق شخص ما ساعة في التحقيق.
ما الذي يجب أن يحتويه تقرير الوردية؟
مدراء المطاعم والمتاجر اللبنانية لديهم تصور واضح لما يجب أن يُظهره ملخص الوردية. ليس مجرد قائمة معاملات، بل أداة توفيق. التقرير يجب أن يُجيب على:
- كم يجب أن يكون في الدرج من نقود؟
- كم أُخذ بالبطاقة، وهل يتطابق مع إجمالي الطرفية؟
- كم عملية إلغاء جرت، ومن أذن بها؟
- ما الخصومات التي طُبقت ولمن؟
تقرير مفيد يشمل: المبيعات الإجمالية حسب الفئة، الخصومات وأنواعها، الإلغاءات والمردودات مع تحديد المشغل، صافي المبيعات بعد الخصومات والإلغاءات، المبيعات حسب طريقة الدفع، النقد المتوقع في الدرج، وعدد المعاملات ومتوسط قيمة المعاملة.
تصميم نموذج بيانات الوردية
الوردية هي نافذة زمنية محددة يملكها أمين صندوق أو مدير، مرتبطة بطرفية ومكان محدد:
CREATE TABLE shifts (
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
location_id bigint NOT NULL,
terminal_id bigint NOT NULL,
cashier_id bigint NOT NULL,
opened_by bigint NOT NULL,
closed_by bigint,
opened_at timestamptz NOT NULL DEFAULT now(),
closed_at timestamptz,
opening_float decimal(12, 2) NOT NULL DEFAULT 0,
status text NOT NULL DEFAULT 'open'
);
CREATE TABLE shift_summaries (
id bigserial PRIMARY KEY,
shift_id bigint NOT NULL REFERENCES shifts(id),
gross_sales decimal(12, 2) NOT NULL,
total_discounts decimal(12, 2) NOT NULL,
total_voids decimal(12, 2) NOT NULL,
total_refunds decimal(12, 2) NOT NULL,
net_sales decimal(12, 2) NOT NULL,
cash_sales decimal(12, 2) NOT NULL,
card_sales decimal(12, 2) NOT NULL,
transaction_count int NOT NULL,
void_count int NOT NULL,
expected_cash_in_drawer decimal(12, 2) NOT NULL,
computed_at timestamptz NOT NULL DEFAULT now()
);
جدول shift_summaries يُخزن القيم المحسوبة مسبقاً عند الإغلاق. لا تُعيد حساب إجماليات الوردية من المعاملات الخام بعد الإغلاق أبداً. الملخص هو السجل الموثوق لما حدث خلال تلك الوردية.
حساب إجماليات الوردية بشكل صحيح
الخطأ الأكثر شيوعاً في حساب تقرير الوردية هو التعامل مع الإلغاءات بشكل خاطئ. الإلغاء يُلغي معاملة بالكامل. المردود هو معاملة سالبة منفصلة. الرياضيات مختلفة:
for rows.Next() {
var order Order
rows.Scan(&order)
if order.Status == "voided" {
// إلغاء: تتبعه منفصلاً دون احتسابه ضمن المبيعات الإجمالية
totals.TotalVoids = totals.TotalVoids.Add(order.Total)
totals.VoidCount++
continue
}
if order.Status == "refunded" {
totals.TotalRefunds = totals.TotalRefunds.Add(order.Total)
totals.RefundCount++
}
totals.GrossSales = totals.GrossSales.Add(order.Subtotal)
totals.TotalDiscounts = totals.TotalDiscounts.Add(order.DiscountAmount)
totals.TransactionCount++
switch order.PaymentMethod {
case "cash":
totals.CashSales = totals.CashSales.Add(order.Total)
case "card":
totals.CardSales = totals.CardSales.Add(order.Total)
}
}
totals.NetSales = totals.GrossSales.
Sub(totals.TotalDiscounts).
Sub(totals.TotalVoids).
Sub(totals.TotalRefunds)
إغلاق الوردية بشكل ذري
إغلاق الوردية عملية من خطوتين: حساب الإجماليات وتسجيل حالة الإغلاق. يجب أن يحدثا في معاملة واحدة:
func (s *ShiftService) CloseShift(ctx context.Context, shiftID int64, closedByID int64) (*ShiftSummary, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
var shift Shift
err = tx.QueryRowContext(ctx, `
SELECT id, status, opening_float FROM shifts
WHERE id = $1 AND tenant_id = $2 FOR UPDATE
`, shiftID, s.tenantID).Scan(&shift.ID, &shift.Status, &shift.OpeningFloat)
if err != nil {
return nil, err
}
if shift.Status == "closed" {
return nil, ErrShiftAlreadyClosed
}
totals, err := s.computeTotalsInTx(ctx, tx, shiftID)
if err != nil {
return nil, err
}
expectedCash := shift.OpeningFloat.Add(totals.CashSales)
// إدراج الملخص
_, err = tx.ExecContext(ctx, `
INSERT INTO shift_summaries (...) VALUES (...)
`, ...)
// تحديث حالة الوردية
_, err = tx.ExecContext(ctx, `
UPDATE shifts SET status = 'closed', closed_at = now(), closed_by = $1 WHERE id = $2
`, closedByID, shiftID)
return &ShiftSummary{Totals: totals, ExpectedCash: expectedCash}, tx.Commit()
}
التعامل مع طرق الدفع المتعددة
في المطاعم اللبنانية، مشهد الدفع متشعب. وردية واحدة قد تشمل: نقد بالدولار، نقد بالليرة اللبنانية، بطاقة ائتمان عبر شركة محلية، ودفع رقمي عبر تطبيق محفظة. لكل طريقة دفع آثار تشغيلية مختلفة للتوفيق.
CREATE TABLE order_tenders (
id bigserial PRIMARY KEY,
order_id bigint NOT NULL,
shift_id bigint NOT NULL,
method text NOT NULL, -- 'cash_usd', 'cash_lbp', 'card_visa', 'wallet_app'
amount decimal(12, 2) NOT NULL,
currency char(3) NOT NULL DEFAULT 'USD',
exchange_rate decimal(10, 6),
reference text
);
دروس تشغيلية من عملاء المطاعم اللبنانية
ثلاثة أنماط تظهر باستمرار عبر عمليات نشر نظام نقاط البيع في المطاعم اللبنانية:
المدراء يُغلقون الورديات في أوقات غير منتظمة. وردية قد تستمر 6 ساعات أو 14 ساعة حسب ازدحام المطعم. نموذج بيانات الوردية يجب أن يتعامل مع الورديات المفتوحة عبر منتصف الليل دون إفساد الإجماليات اليومية.
تفويض الإلغاء مصدر دائم للاحتكاك التشغيلي. الإلغاءات يجب أن تتطلب رقم سري للمدير أو تجاوز المشرف. كل إلغاء يجب أن يُسجل من أذن به.
انقطاع الاتصال أثناء إغلاق الوردية. الشبكات اللبنانية غير مستقرة. عملية إغلاق الوردية يجب أن تكون قابلة للتكرار الآمن: إذا فقدت طرفية نقاط البيع الاتصال في منتصف الطلب وضغط المدير إغلاقاً مرة أخرى، فيجب أن تُكشف المحاولة الثانية على الحالة المغلقة بالفعل وتُعيد الملخص الموجود.
الخلاصة
احسب وخزّن ملخصات الوردية عند الإغلاق. لا تُعد الحساب من المعاملات الخام. تعامل مع الإلغاءات والمردودات كأنواع سجل مستقلة. استخدم SELECT FOR UPDATE لمنع محاولات الإغلاق المتزامنة. ادعم تتبع العطاءات متعددة العملات من اليوم الأول.



