diff --git a/addons/laundry_management/__init__.py b/addons/laundry_management/__init__.py deleted file mode 100644 index 9b42961..0000000 --- a/addons/laundry_management/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import models -from . import wizard diff --git a/addons/laundry_management/__manifest__.py b/addons/laundry_management/__manifest__.py deleted file mode 100644 index c2049f2..0000000 --- a/addons/laundry_management/__manifest__.py +++ /dev/null @@ -1,151 +0,0 @@ -{ - 'name': 'Laundry Management', - 'version': '19.0.19.0.4', - 'summary': 'Laundry Management', - 'description': 'Laundry Management', - 'author': 'Laundry Management', - 'category': 'Services', - 'license': 'LGPL-3', - 'depends': [ - 'base', - 'mail', - 'sale', # sale.order (legacy refs in commission/dashboard) - 'account', # account.move, account.payment (legacy refs) - 'product', # product.template, product.product - 'base_setup', # res.config.settings integration - 'sales_team', # group_sale_salesman, group_sale_manager - 'point_of_sale', # pos.order ??? source of truth - ], - 'data': [ - # 1. Security ??? groups must load before ACLs and rules - 'security/res_groups.xml', - 'security/ir_rule.xml', - 'security/ir.model.access.csv', - # 2. Sequences - 'data/sequence.xml', - # 3. Master data - 'data/laundry_data.xml', - 'data/service_catalog_data.xml', - # 4. Reports ??? must load before views that reference report actions - 'report/laundry_order_report.xml', - 'report/laundry_thermal_report.xml', - 'report/laundry_work_order_report.xml', - # 5. Action-defining views ??? must load before menus + reporting, - # because both reference these actions by xml id. - 'views/product_template_views.xml', - 'views/laundry_order_type_views.xml', - 'views/laundry_order_attribute_views.xml', - 'views/laundry_order_views.xml', - 'views/pos_order_views.xml', - 'views/laundry_commission_views.xml', - # 6. Configuration views - 'views/laundry_payment_method_views.xml', - 'views/laundry_settings_views.xml', - 'views/pos_config_views.xml', - # 7. Wizards (their actions are referenced from menus / forms) - 'views/laundry_print_wizard_views.xml', - 'wizard/laundry_order_unlock_wizard_views.xml', - # 8. MENUS ??? must load BEFORE any file that adds a child menu - # under menu_laundry_root (e.g. laundry_reporting_views.xml). - 'views/laundry_menus.xml', - # 9. Reporting ??? adds child menus under menu_laundry_root - # (defined in the file above) and under core - # sale.menu_sale_report / account.menu_finance_reports. - 'views/laundry_reporting_views.xml', - ], - 'assets': { - # ???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????? - # POS asset bundle ??? EXPLICIT FILE LIST (no broad globs). - # ???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????? - # Lists only the production-required POS workflow files. - # Suspect / experimental files are commented OUT individually - # rather than disabling the whole bundle. Toggle a single line - # to add or remove a feature. - # - # The defensive guard for the `doHaveConflictWith` crash lives - # in pos_store_patch.js ??? that crash is fixed without removing - # any feature. - # ???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????? - 'point_of_sale._assets_pos': [ - # ?????? Styling (shared with backend kanban) ?????????????????????????????????????????????????????? - 'laundry_management/static/src/scss/laundry_pos.scss', - - # ?????? Reactive store (must load first; other patches use it) - 'laundry_management/static/src/js/laundry_context_store.js', - - # ?????? Model + screen patches (workflow core) ???????????????????????????????????????????????? - 'laundry_management/static/src/js/pos_order_patch.js', - 'laundry_management/static/src/js/pos_store_patch.js', - 'laundry_management/static/src/js/payment_screen_patch.js', - 'laundry_management/static/src/js/order_payment_validation.js', - - # ?????? Settle-Due (production-required) ?????????????????????????????????????????????????????????????????? - 'laundry_management/static/src/js/settle_dues.js', - 'laundry_management/static/src/js/settlement_receipt.js', - 'laundry_management/static/src/js/laundry_settle_banner.js', - 'laundry_management/static/src/js/closing_popup_patch.js', - - # ?????? Customer Laundry Orders popup (production-required) ?????? - 'laundry_management/static/src/js/view_laundry_orders.js', - 'laundry_management/static/src/js/quick_create_partner.js', - - # ?????? Legacy order-type / attribute / delivery flow ??????????????????????????? - 'laundry_management/static/src/js/laundry_order_context_panel.js', - 'laundry_management/static/src/js/popups/laundry_delivery_details_popup.js', - 'laundry_management/static/src/js/popups/laundry_order_attribute_popup.js', - 'laundry_management/static/src/js/popups/laundry_order_type_popup.js', - - # ?????? Cashier UI helpers ???????????????????????????????????????????????????????????????????????????????????????????????????????????? - 'laundry_management/static/src/js/control_buttons_patch.js', - 'laundry_management/static/src/js/navbar_patch.js', - 'laundry_management/static/src/js/order_summary_patch.js', - 'laundry_management/static/src/js/order_tabs_patch.js', - 'laundry_management/static/src/js/ticket_screen_patch.js', - 'laundry_management/static/src/js/order_receipt_patch.js', - 'laundry_management/static/src/js/laundry_receipt_details.js', - 'laundry_management/static/src/js/laundry_pricing_hook.js', - - # ?????? XML templates for the JS files above ?????????????????????????????????????????????????????? - 'laundry_management/static/src/xml/closing_popup_ext.xml', - 'laundry_management/static/src/xml/control_buttons.xml', - 'laundry_management/static/src/xml/laundry_order_context_panel.xml', - 'laundry_management/static/src/xml/laundry_settle_banner.xml', - 'laundry_management/static/src/xml/order_summary_patch.xml', - 'laundry_management/static/src/xml/partner_line.xml', - 'laundry_management/static/src/xml/popups/laundry_delivery_details_popup.xml', - 'laundry_management/static/src/xml/popups/laundry_order_attribute_popup.xml', - 'laundry_management/static/src/xml/popups/laundry_order_type_popup.xml', - 'laundry_management/static/src/xml/quick_create_partner.xml', - 'laundry_management/static/src/xml/receipt.xml', - 'laundry_management/static/src/xml/settle_dues.xml', - 'laundry_management/static/src/xml/settlement_receipt.xml', - 'laundry_management/static/src/xml/view_laundry_orders.xml', - - # ?????? Improved laundry configurator UX (XML-only) ????????????????????????????????? - # Pure XML inheritance on Odoo's ProductConfiguratorPopup - # that adds two CSS classes to the Dialog's contentClass so - # the existing SCSS in laundry_pos.scss enhances the popup - # for laundry-flagged products only. NO JS override, NO - # logic change. The defensive doHaveConflictWith guard in - # pos_store_patch.js handles the data-shape edge case - # independently ??? re-enabling this is safe. - 'laundry_management/static/src/xml/product_configurator_popup.xml', - - # ?????? STILL EXCLUDED ??? recent / experimental ???????????????????????????????????????????????? - # Thermal-receipt component is kept off until an explicit - # printer-side validation. PDF fallback remains the path. - # 'laundry_management/static/src/js/laundry_thermal_receipt.js', - # 'laundry_management/static/src/xml/laundry_thermal_receipt.xml', - ], - 'web.assets_backend': [ - # SCSS shared with backend kanban / dashboard styling. - 'laundry_management/static/src/scss/laundry_pos.scss', - ], - }, - 'demo': [ - 'demo/demo.xml', - ], - 'installable': True, - 'application': True, - 'auto_install': False, -} diff --git a/addons/laundry_management/data/laundry_data.xml b/addons/laundry_management/data/laundry_data.xml deleted file mode 100644 index 6ce2903..0000000 --- a/addons/laundry_management/data/laundry_data.xml +++ /dev/null @@ -1,349 +0,0 @@ - - - - - - Laundry Settlement - service - 0.00 - True - False - True - True - False - - Settlement of outstanding laundry dues. - - - - - Laundry Services - - - - Washing - - - - - Dry Cleaning - - - - - Ironing & Pressing - - - - - Special Care - - - - - Express Service - - - - - - Shirt — Wash & Iron - service - 5.00 - - True - True - False - Standard wash and press for dress shirts and casual shirts. - - - - Trousers — Wash & Iron - service - 6.00 - - True - True - False - - - - T-Shirt / Polo — Wash - service - 4.00 - - True - True - False - - - - Jeans — Wash - service - 7.00 - - True - True - False - - - - Abaya — Wash & Press - service - 10.00 - - True - True - False - - - - Thobe / Dishdasha — Wash & Iron - service - 9.00 - - True - True - False - - - - Blanket / Duvet — Wash - service - 18.00 - - True - True - False - - - - - Suit (2-Piece) — Dry Clean - service - 25.00 - - True - True - False - Full dry cleaning for 2-piece suits. - - - - Jacket / Blazer — Dry Clean - service - 15.00 - - True - True - False - - - - Dress / Gown — Dry Clean - service - 20.00 - - True - True - False - - - - Abaya — Dry Clean & Press - service - 18.00 - - True - True - False - - - - Thobe / Dishdasha — Dry Clean - service - 16.00 - - True - True - False - - - - Wedding Dress — Dry Clean & Preserve - service - 60.00 - - True - True - False - Premium dry cleaning and preservation for wedding dresses. - - - - - Shirt — Iron & Press - service - 3.00 - - True - True - False - - - - Trousers — Iron & Press - service - 4.00 - - True - True - False - - - - Thobe / Dishdasha — Iron & Press - service - 5.00 - - True - True - False - - - - Suit — Iron & Press - service - 8.00 - - True - True - False - - - - Dress / Gown — Iron & Press - service - 7.00 - - True - True - False - - - - - Carpet / Rug — Deep Clean - service - 35.00 - - True - True - False - Deep steam cleaning for carpets and rugs. - - - - Curtain — Wash & Press - service - 25.00 - - True - True - False - - - - Leather Jacket — Clean & Condition - service - 40.00 - - True - True - False - - - - Stain Treatment (per item) - service - 12.00 - - True - True - False - Targeted stain removal treatment applied before washing. - - - - - Express Turnaround (4-Hour) - service - 10.00 - - True - True - False - Priority processing — ready within 4 hours. - - - - Same-Day Delivery Surcharge - service - 8.00 - - True - True - False - Add-on fee for same-day home delivery. - - - - - Service Speed - radio - always - - - - Normal - - 1 - 0.00 - - - - Express - - 2 - 3.00 - - - - - diff --git a/addons/laundry_management/data/sequence.xml b/addons/laundry_management/data/sequence.xml deleted file mode 100644 index 986eee0..0000000 --- a/addons/laundry_management/data/sequence.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - Laundry Order - laundry.order - LND/%(year)s/%(month)s/ - 4 - 1 - 1 - - - - - - - Laundry Session - laundry.session - LS/%(year)s/%(month)s/ - 4 - 1 - 1 - - - - - - - Laundry Item Tracking Code - laundry.order.line.tracking - LI- - 6 - 1 - 1 - - - - - diff --git a/addons/laundry_management/data/service_catalog_data.xml b/addons/laundry_management/data/service_catalog_data.xml deleted file mode 100644 index 8d511a1..0000000 --- a/addons/laundry_management/data/service_catalog_data.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/addons/laundry_management/demo/demo.xml b/addons/laundry_management/demo/demo.xml deleted file mode 100644 index f3626ad..0000000 --- a/addons/laundry_management/demo/demo.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - Service Speed - radio - always - 10 - - - - Normal - - 10 - - - - Express - - 3.0 - 20 - - - - - T-Shirt / Polo Wash - service - True - False - True - True - 8.0 - - Wash + iron for tops. Express adds 3 SAR. - - - - - - - - - diff --git a/addons/laundry_management/doc/RELEASE_NOTES.md b/addons/laundry_management/doc/RELEASE_NOTES.md deleted file mode 100644 index 44aa542..0000000 --- a/addons/laundry_management/doc/RELEASE_NOTES.md +++ /dev/null @@ -1,7 +0,0 @@ -## Module - -#### 26.01.2026 -#### Version 19.0.1.0.0 -#### ADD - -- Initial commit for Laundry Management diff --git a/addons/laundry_management/i18n/ar.po b/addons/laundry_management/i18n/ar.po deleted file mode 100644 index 57d8416..0000000 --- a/addons/laundry_management/i18n/ar.po +++ /dev/null @@ -1,413 +0,0 @@ -# Arabic translation for laundry_management -# Copyright (C) 2026 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 19.0\n" -"Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2026-04-01 00:00+0000\n" -"Language: ar\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" - -#. module: laundry_management -msgid "Laundry" -msgstr "المغسلة" - -#. module: laundry_management -msgid "Laundry Management" -msgstr "إدارة المغسلة" - -#. module: laundry_management -msgid "Laundry Order" -msgstr "طلب مغسلة" - -#. module: laundry_management -msgid "Laundry Orders" -msgstr "طلبات المغسلة" - -#. module: laundry_management -msgid "Orders" -msgstr "الطلبات" - -#. module: laundry_management -msgid "All Orders" -msgstr "جميع الطلبات" - -#. module: laundry_management -msgid "Operations Pipeline" -msgstr "خط سير العمليات" - -#. module: laundry_management -msgid "Ready for Pickup" -msgstr "جاهز للاستلام" - -#. module: laundry_management -msgid "Configuration" -msgstr "الإعدادات" - -#. module: laundry_management -msgid "Service Categories" -msgstr "تصنيفات الخدمات" - -#. module: laundry_management -msgid "Item Types" -msgstr "أنواع القطع" - -#. module: laundry_management -msgid "Order No." -msgstr "رقم الطلب" - -#. module: laundry_management -msgid "Customer" -msgstr "العميل" - -#. module: laundry_management -msgid "Mobile" -msgstr "الجوال" - -#. module: laundry_management -msgid "Order Date" -msgstr "تاريخ الطلب" - -#. module: laundry_management -msgid "Expected Delivery" -msgstr "موعد التسليم المتوقع" - -#. module: laundry_management -msgid "Order Type" -msgstr "نوع الطلب" - -#. module: laundry_management -msgid "Walk-In / Drop-Off" -msgstr "مراجعة مباشرة" - -#. module: laundry_management -msgid "We Pick Up" -msgstr "نستلم من عندك" - -#. module: laundry_management -msgid "Home Delivery" -msgstr "توصيل للمنزل" - -#. module: laundry_management -msgid "Priority" -msgstr "الأولوية" - -#. module: laundry_management -msgid "Normal" -msgstr "عادي" - -#. module: laundry_management -msgid "Urgent" -msgstr "عاجل" - -#. module: laundry_management -msgid "Express" -msgstr "سريع جداً" - -#. module: laundry_management -msgid "Status" -msgstr "الحالة" - -#. module: laundry_management -msgid "Draft" -msgstr "مسودة" - -#. module: laundry_management -msgid "Received" -msgstr "مستلم" - -#. module: laundry_management -msgid "In Process" -msgstr "تحت المعالجة" - -#. module: laundry_management -msgid "Ready" -msgstr "جاهز" - -#. module: laundry_management -msgid "Handed Over" -msgstr "تم التسليم" - -#. module: laundry_management -msgid "Cancelled" -msgstr "ملغي" - -#. module: laundry_management -msgid "Confirm Receipt" -msgstr "تأكيد الاستلام" - -#. module: laundry_management -msgid "Start Processing" -msgstr "بدء المعالجة" - -#. module: laundry_management -msgid "Ready for Pickup" -msgstr "جاهز للاستلام" - -#. module: laundry_management -msgid "Hand Over to Customer" -msgstr "تسليم للعميل" - -#. module: laundry_management -msgid "Cancel Order" -msgstr "إلغاء الطلب" - -#. module: laundry_management -msgid "Reopen" -msgstr "إعادة فتح" - -#. module: laundry_management -msgid "Items & Services" -msgstr "القطع والخدمات" - -#. module: laundry_management -msgid "Item" -msgstr "القطعة" - -#. module: laundry_management -msgid "Item Type" -msgstr "نوع القطعة" - -#. module: laundry_management -msgid "Service" -msgstr "الخدمة" - -#. module: laundry_management -msgid "Category" -msgstr "التصنيف" - -#. module: laundry_management -msgid "Color / Description" -msgstr "اللون / الوصف" - -#. module: laundry_management -msgid "Stain / Damage" -msgstr "بقعة / ضرر" - -#. module: laundry_management -msgid "Special Instructions" -msgstr "تعليمات خاصة" - -#. module: laundry_management -msgid "Qty" -msgstr "الكمية" - -#. module: laundry_management -msgid "Unit Price" -msgstr "سعر الوحدة" - -#. module: laundry_management -msgid "Subtotal" -msgstr "المجموع الجزئي" - -#. module: laundry_management -msgid "Total" -msgstr "الإجمالي" - -#. module: laundry_management -msgid "Total Amount" -msgstr "المبلغ الإجمالي" - -#. module: laundry_management -msgid "Items" -msgstr "القطع" - -#. module: laundry_management -msgid "Payment" -msgstr "الدفع" - -#. module: laundry_management -msgid "Unpaid" -msgstr "غير مدفوع" - -#. module: laundry_management -msgid "Partial" -msgstr "مدفوع جزئياً" - -#. module: laundry_management -msgid "Paid" -msgstr "مدفوع" - -#. module: laundry_management -msgid "Invoice" -msgstr "الفاتورة" - -#. module: laundry_management -msgid "Received By" -msgstr "استُلم بواسطة" - -#. module: laundry_management -msgid "Handed Over By" -msgstr "سُلّم بواسطة" - -#. module: laundry_management -msgid "Customer Notes" -msgstr "ملاحظات العميل" - -#. module: laundry_management -msgid "Internal Notes" -msgstr "ملاحظات داخلية" - -#. module: laundry_management -msgid "Processing Info" -msgstr "معلومات المعالجة" - -#. module: laundry_management -msgid "Notes" -msgstr "الملاحظات" - -#. module: laundry_management -msgid "Washing" -msgstr "غسيل" - -#. module: laundry_management -msgid "Dry Cleaning" -msgstr "تنظيف جاف" - -#. module: laundry_management -msgid "Ironing & Pressing" -msgstr "كوي وضغط" - -#. module: laundry_management -msgid "Special Care" -msgstr "عناية خاصة" - -#. module: laundry_management -msgid "Express Service" -msgstr "خدمة سريعة" - -#. module: laundry_management -msgid "Shirt" -msgstr "قميص" - -#. module: laundry_management -msgid "Trousers / Pants" -msgstr "بنطلون" - -#. module: laundry_management -msgid "Suit (2-Piece)" -msgstr "بدلة (قطعتان)" - -#. module: laundry_management -msgid "Abaya" -msgstr "عباية" - -#. module: laundry_management -msgid "Thobe / Dishdasha" -msgstr "ثوب / دشداشة" - -#. module: laundry_management -msgid "Dress / Gown" -msgstr "فستان / ثوب سهرة" - -#. module: laundry_management -msgid "Jacket / Blazer" -msgstr "جاكيت / بليزر" - -#. module: laundry_management -msgid "T-Shirt / Polo" -msgstr "تيشيرت / بولو" - -#. module: laundry_management -msgid "Jeans" -msgstr "جينز" - -#. module: laundry_management -msgid "Blanket / Duvet" -msgstr "بطانية / لحاف" - -#. module: laundry_management -msgid "Carpet / Rug" -msgstr "سجادة / موكيت" - -#. module: laundry_management -msgid "Curtain" -msgstr "ستارة" - -#. module: laundry_management -msgid "Uniform / Workwear" -msgstr "يونيفورم / ملابس عمل" - -#. module: laundry_management -msgid "Wedding Dress" -msgstr "فستان زفاف" - -#. module: laundry_management -msgid "Other / Custom Item" -msgstr "أخرى / قطعة مخصصة" - -#. module: laundry_management -msgid "Laundry Order Receipt" -msgstr "إيصال طلب المغسلة" - -#. module: laundry_management -msgid "Search Orders" -msgstr "البحث في الطلبات" - -#. module: laundry_management -msgid "Today" -msgstr "اليوم" - -#. module: laundry_management -msgid "Pending" -msgstr "قيد التنفيذ" - -#. module: laundry_management -msgid "Delivered" -msgstr "مسلّم" - -#. module: laundry_management -msgid "Laundry Service Category" -msgstr "تصنيف خدمة المغسلة" - -#. module: laundry_management -msgid "Laundry Item Type" -msgstr "نوع قطعة المغسلة" - -#. module: laundry_management -msgid "Laundry Order Line" -msgstr "سطر طلب المغسلة" - -#. module: laundry_management -msgid "Default Service" -msgstr "الخدمة الافتراضية" - -#. module: laundry_management -msgid "Default Unit Price" -msgstr "السعر الافتراضي للوحدة" - -#. module: laundry_management -msgid "Active" -msgstr "نشط" - -# ── Reporting menus integrated into Sales/Accounting dashboards ────── -#. module: laundry_management -msgid "Laundry Orders Analysis" -msgstr "تحليل طلبات المغسلة" - -#. module: laundry_management -msgid "Laundry Operations (Live)" -msgstr "عمليات المغسلة (مباشر)" - -#. module: laundry_management -msgid "Laundry Invoices" -msgstr "فواتير المغسلة" - -#. module: laundry_management -msgid "Reports" -msgstr "التقارير" - -#. module: laundry_management -msgid "Orders Analysis" -msgstr "تحليل الطلبات" - -#. module: laundry_management -msgid "Operations (Live)" -msgstr "العمليات (مباشر)" - -#. module: laundry_management -msgid "Invoices Analysis" -msgstr "تحليل الفواتير" diff --git a/addons/laundry_management/migrations/19.0.11.0.0/__init__.py b/addons/laundry_management/migrations/19.0.11.0.0/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/addons/laundry_management/migrations/19.0.11.0.0/pre_migrate.py b/addons/laundry_management/migrations/19.0.11.0.0/pre_migrate.py deleted file mode 100644 index ff0e8a5..0000000 --- a/addons/laundry_management/migrations/19.0.11.0.0/pre_migrate.py +++ /dev/null @@ -1,228 +0,0 @@ -""" -Pre-migration script for laundry_management 19.0.11.0.0 - -Architecture change: standalone laundry.order / laundry.order.line / laundry.payment -models are replaced by _inherit = 'sale.order' / 'sale.order.line'. - -This script runs BEFORE Odoo's model sync so that: -1. FK constraints from old wizard tables referencing laundry_order are dropped -2. Old wizard transient records are purged (they reference non-existent rows) -3. Stale ir.model.fields records pointing to old models are removed -4. Old ir.model entries are deleted (unblocking the ORM delete check) -5. Old physical tables are dropped - -Without this, the ORM raises: - "Another model is using the record you are trying to delete. - Blocking model: Laundry Print Wizard (laundry.print.wizard), - Blocking field: order_id" -""" - -import logging - -_logger = logging.getLogger(__name__) - - -# Old standalone models being removed in this version -_OLD_MODELS = [ - 'laundry.order', - 'laundry.order.line', - 'laundry.payment', - 'laundry.order.line.addon', - 'laundry.register.payment.wizard', - 'laundry.product.wizard', - 'laundry.product.wizard.line', - 'laundry.category', - 'laundry.item.type', -] - -# Physical tables that correspond to the old models -_OLD_TABLES = [ - 'laundry_order', - 'laundry_order_line', - 'laundry_payment', - 'laundry_order_line_addon', - 'laundry_register_payment_wizard', - 'laundry_product_wizard', - 'laundry_product_wizard_line', - 'laundry_category', - 'laundry_item_type', -] - -# Transient/wizard tables that may hold rows with FK refs to laundry_order -_WIZARD_TABLES = [ - 'laundry_print_wizard', - 'laundry_session_wizard', - 'laundry_whatsapp_wizard', - 'laundry_register_payment_wizard', - 'laundry_product_wizard', - 'laundry_product_wizard_line', -] - - -def _table_exists(cr, table): - cr.execute( - "SELECT 1 FROM information_schema.tables " - "WHERE table_schema = 'public' AND table_name = %s", - (table,), - ) - return bool(cr.fetchone()) - - -def _column_exists(cr, table, column): - cr.execute( - "SELECT 1 FROM information_schema.columns " - "WHERE table_schema = 'public' AND table_name = %s AND column_name = %s", - (table, column), - ) - return bool(cr.fetchone()) - - -def _drop_fk_constraints_referencing(cr, referenced_table): - """Drop all FK constraints in the DB that point at referenced_table.""" - cr.execute( - """ - SELECT tc.table_name, tc.constraint_name - FROM information_schema.table_constraints tc - JOIN information_schema.referential_constraints rc - ON tc.constraint_name = rc.constraint_name - AND tc.table_schema = rc.constraint_schema - JOIN information_schema.table_constraints tc2 - ON rc.unique_constraint_name = tc2.constraint_name - AND rc.unique_constraint_schema = tc2.table_schema - WHERE tc.constraint_type = 'FOREIGN KEY' - AND tc2.table_name = %s - AND tc.table_schema = 'public' - """, - (referenced_table,), - ) - rows = cr.fetchall() - for src_table, constraint_name in rows: - _logger.info( - 'pre_migrate: dropping FK constraint %s on %s (referenced %s)', - constraint_name, src_table, referenced_table, - ) - cr.execute( - 'ALTER TABLE "%s" DROP CONSTRAINT IF EXISTS "%s"' % - (src_table, constraint_name) - ) - - -def migrate(cr, version): - if not version: - # Fresh install — nothing to clean up - return - - _logger.info('pre_migrate laundry_management %s → 19.0.11.0.0 : start', version) - - # ── Step 1: Purge all transient wizard records ──────────────────────────── - # TransientModel rows expire naturally but DB rows persist during upgrade; - # they hold FK refs that block constraint drops and table drops. - for tbl in _WIZARD_TABLES: - if _table_exists(cr, tbl): - _logger.info('pre_migrate: truncating wizard table %s', tbl) - cr.execute('TRUNCATE TABLE "%s" CASCADE' % tbl) - - # ── Step 2: Drop FK constraints pointing at old tables ──────────────────── - for tbl in _OLD_TABLES: - if _table_exists(cr, tbl): - _drop_fk_constraints_referencing(cr, tbl) - - # Also drop FKs on wizard tables that point at laundry_order (the primary blocker) - # Example: laundry_print_wizard.order_id -> laundry_order.id - for wizard_tbl in _WIZARD_TABLES: - if not _table_exists(cr, wizard_tbl): - continue - # Find and drop any FK on this wizard table that references an old table - cr.execute( - """ - SELECT kcu.column_name, ccu.table_name AS foreign_table_name, tc.constraint_name - FROM information_schema.table_constraints AS tc - JOIN information_schema.key_column_usage AS kcu - ON tc.constraint_name = kcu.constraint_name - AND tc.table_schema = kcu.table_schema - JOIN information_schema.constraint_column_usage AS ccu - ON ccu.constraint_name = tc.constraint_name - AND ccu.table_schema = tc.table_schema - WHERE tc.constraint_type = 'FOREIGN KEY' - AND tc.table_schema = 'public' - AND tc.table_name = %s - AND ccu.table_name = ANY(%s) - """, - (wizard_tbl, _OLD_TABLES), - ) - for col, ref_tbl, constraint_name in cr.fetchall(): - _logger.info( - 'pre_migrate: dropping FK %s on %s.%s -> %s', - constraint_name, wizard_tbl, col, ref_tbl, - ) - cr.execute( - 'ALTER TABLE "%s" DROP CONSTRAINT IF EXISTS "%s"' % - (wizard_tbl, constraint_name) - ) - - # ── Step 3: Remove stale ir.model.fields that ref old models ───────────── - # These are the records that cause "Blocking model: laundry.print.wizard, - # Blocking field: order_id" — the field record itself still has - # relation = 'laundry.order', which makes the ORM think laundry.print.wizard - # still depends on laundry.order. - cr.execute( - """ - DELETE FROM ir_model_fields - WHERE relation = ANY(%s) - """, - (_OLD_MODELS,), - ) - deleted = cr.rowcount - _logger.info('pre_migrate: deleted %d stale ir.model.fields rows', deleted) - - # Also remove fields whose model itself is one of the old models - cr.execute( - """ - DELETE FROM ir_model_fields - WHERE model IN %s - """, - (tuple(_OLD_MODELS),), - ) - _logger.info('pre_migrate: deleted %d ir.model.fields for old models', cr.rowcount) - - # ── Step 4: Remove stale ir.model.fields_by_name cache ─────────────────── - # In some Odoo versions there is a separate constraint/index table. - # Safe to attempt; ignore if table doesn't exist. - if _table_exists(cr, 'ir_model_constraint'): - cr.execute( - """ - DELETE FROM ir_model_constraint imc - USING ir_model im - WHERE imc.model = im.id - AND im.model = ANY(%s) - """, - (_OLD_MODELS,), - ) - _logger.info('pre_migrate: removed %d ir.model.constraint rows', cr.rowcount) - - if _table_exists(cr, 'ir_model_relation'): - cr.execute( - """ - DELETE FROM ir_model_relation imr - USING ir_model im - WHERE imr.model = im.id - AND im.model = ANY(%s) - """, - (_OLD_MODELS,), - ) - _logger.info('pre_migrate: removed %d ir.model.relation rows', cr.rowcount) - - # ── Step 5: Remove ir.model entries for the old models ─────────────────── - cr.execute( - "DELETE FROM ir_model WHERE model = ANY(%s)", - (_OLD_MODELS,), - ) - _logger.info('pre_migrate: deleted %d ir.model rows', cr.rowcount) - - # ── Step 6: Drop old physical tables ───────────────────────────────────── - for tbl in reversed(_OLD_TABLES): # reverse to respect FK order - if _table_exists(cr, tbl): - _logger.info('pre_migrate: dropping table %s', tbl) - cr.execute('DROP TABLE IF EXISTS "%s" CASCADE' % tbl) - - _logger.info('pre_migrate laundry_management: complete') diff --git a/addons/laundry_management/migrations/19.0.12.0.0/__init__.py b/addons/laundry_management/migrations/19.0.12.0.0/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/addons/laundry_management/migrations/19.0.12.0.0/pre_migrate.py b/addons/laundry_management/migrations/19.0.12.0.0/pre_migrate.py deleted file mode 100644 index 7619251..0000000 --- a/addons/laundry_management/migrations/19.0.12.0.0/pre_migrate.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -Pre-migration for laundry_management 19.0.12.0.0 - -Changes handled: -1. Commission states: 'paid' only → now 'pending', 'confirmed', 'paid' - Existing 'paid' rows remain 'paid'. Existing 'pending' rows remain 'pending'. - 'confirmed' is a new state — no existing rows use it, so no data migration needed. - -2. New model: laundry.payment.wizard — just a new table, nothing to clean. - -3. New groups: group_laundry_operator, group_laundry_cashier added to hierarchy. - Existing users with group_laundry_user keep all their permissions (implied). - -4. Access control: new CSV entries for payment wizard and new groups. - Handled automatically by Odoo on upgrade. - -No destructive operations needed in this migration. -The pre_migrate script from 19.0.11.0.0 already cleaned the legacy tables. -""" -import logging - -_logger = logging.getLogger(__name__) - - -def migrate(cr, version): - if not version: - return # Fresh install - - _logger.info('pre_migrate laundry_management 19.0.12.0.0: checking commission states') - - # Ensure commission state column allows new 'confirmed' value. - # In Odoo, Selection fields are stored as VARCHAR — no schema change needed. - # Just verify the column exists and log the existing state distribution. - cr.execute(""" - SELECT state, COUNT(*) FROM laundry_commission - GROUP BY state - ORDER BY state - """) - rows = cr.fetchall() - if rows: - _logger.info( - 'pre_migrate: commission state distribution: %s', - {state: count for state, count in rows} - ) - else: - _logger.info('pre_migrate: laundry_commission table is empty — fresh data') - - _logger.info('pre_migrate laundry_management 19.0.12.0.0: complete') diff --git a/addons/laundry_management/migrations/19.0.13.0.0/__init__.py b/addons/laundry_management/migrations/19.0.13.0.0/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/addons/laundry_management/migrations/19.0.13.0.0/pre_migrate.py b/addons/laundry_management/migrations/19.0.13.0.0/pre_migrate.py deleted file mode 100644 index 4d34bf7..0000000 --- a/addons/laundry_management/migrations/19.0.13.0.0/pre_migrate.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Pre-migration: 19.0.12.0.0 → 19.0.13.0.0 - -Removes three models that are being retired in this version: - - laundry.product.wizard (replaced by native sale.order.line product selection) - - laundry.product.wizard.line (child of above) - - laundry.whatsapp.wizard (replaced by one-click wa.me URL action) - -If these ir.model records are left in the DB while the Python classes no longer -exist, Odoo will log warnings or fail on field-level checks during upgrade. -We clean them here, before the ORM loads. -""" -import logging - -_logger = logging.getLogger(__name__) - -_REMOVED_MODELS = [ - 'laundry.product.wizard', - 'laundry.product.wizard.line', - 'laundry.whatsapp.wizard', -] - -_REMOVED_TABLES = [ - 'laundry_product_wizard', - 'laundry_product_wizard_line', - 'laundry_whatsapp_wizard', -] - - -def migrate(cr, version): - if not version: - return - - _logger.info('pre_migrate 19.0.13.0.0: cleaning retired wizard models %s', _REMOVED_MODELS) - - # 1. Drop physical tables (TransientModels do have real tables in Odoo) - for tbl in _REMOVED_TABLES: - cr.execute(f'DROP TABLE IF EXISTS "{tbl}" CASCADE') - _logger.info(' dropped table: %s', tbl) - - # 2. Remove ir.model.access entries for these models - cr.execute(""" - DELETE FROM ir_model_access - WHERE model_id IN ( - SELECT id FROM ir_model WHERE model = ANY(%s) - ) - """, (_REMOVED_MODELS,)) - _logger.info(' deleted %d ir.model.access rows', cr.rowcount) - - # 3. Remove ir.rule entries - cr.execute(""" - DELETE FROM ir_rule - WHERE model_id IN ( - SELECT id FROM ir_model WHERE model = ANY(%s) - ) - """, (_REMOVED_MODELS,)) - - # 4. Remove ir.model.fields for these models - cr.execute(""" - DELETE FROM ir_model_fields - WHERE model_id IN ( - SELECT id FROM ir_model WHERE model = ANY(%s) - ) - """, (_REMOVED_MODELS,)) - _logger.info(' deleted ir.model.fields rows') - - # 5. Remove ir.model.constraint - cr.execute(""" - DELETE FROM ir_model_constraint - WHERE model_id IN ( - SELECT id FROM ir_model WHERE model = ANY(%s) - ) - """, (_REMOVED_MODELS,)) - - # 6. Remove ir.model.relation - cr.execute(""" - DELETE FROM ir_model_relation - WHERE model_id IN ( - SELECT id FROM ir_model WHERE model = ANY(%s) - ) - """, (_REMOVED_MODELS,)) - - # 7. Remove ir.model.data (XML IDs) for these models - cr.execute(""" - DELETE FROM ir_model_data - WHERE model = 'ir.model' AND name IN ( - SELECT REPLACE(model, '.', '_') || '_' || id::text - FROM ir_model WHERE model = ANY(%s) - ) - """, (_REMOVED_MODELS,)) - # Also delete by res_id - cr.execute(""" - DELETE FROM ir_model_data - WHERE model = 'ir.model' - AND res_id IN (SELECT id FROM ir_model WHERE model = ANY(%s)) - """, (_REMOVED_MODELS,)) - - # 8. Finally remove ir.model entries themselves - cr.execute(""" - DELETE FROM ir_model WHERE model = ANY(%s) - """, (_REMOVED_MODELS,)) - _logger.info(' deleted ir.model entries for retired wizards') - - _logger.info('pre_migrate 19.0.13.0.0: complete') diff --git a/addons/laundry_management/migrations/19.0.19.0.2/post-migrate.py b/addons/laundry_management/migrations/19.0.19.0.2/post-migrate.py deleted file mode 100644 index 1d056d6..0000000 --- a/addons/laundry_management/migrations/19.0.19.0.2/post-migrate.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Phase 1 financial model migration. - -Before: laundry_order.amount_paid was blindly copied from pos_order.amount_paid, -which includes Customer Account / pay-later payments. Every deferred sale -appeared fully paid → amount_due was always 0 → Settle Dues was non-functional. - -After: amount_paid_cash + amount_deferred are computed from pos_payment rows, -classified by pos_payment_method.split_transactions. amount_due is recomputed -as amount_deferred - amount_settled. - -This script rebuilds the split for every existing laundry.order by replaying -its linked pos_order's payment history. -""" -import logging - -_logger = logging.getLogger(__name__) - - -def migrate(cr, version): - if not version: - return - - _logger.info('laundry_management: rebuilding financial split for existing orders') - - # Ensure the new columns exist (ORM will have added them, but be defensive) - cr.execute(""" - ALTER TABLE laundry_order - ADD COLUMN IF NOT EXISTS amount_paid_cash numeric DEFAULT 0, - ADD COLUMN IF NOT EXISTS amount_deferred numeric DEFAULT 0; - """) - - # Rebuild amount_paid_cash / amount_deferred per laundry order - # from pos_payment rows classified by pos_payment_method.split_transactions. - cr.execute(""" - WITH classified AS ( - SELECT - lo.id AS lo_id, - COALESCE(SUM(CASE WHEN pm.split_transactions = FALSE THEN pp.amount ELSE 0 END), 0) AS cash, - COALESCE(SUM(CASE WHEN pm.split_transactions = TRUE THEN pp.amount ELSE 0 END), 0) AS deferred - FROM laundry_order lo - LEFT JOIN pos_payment pp ON pp.pos_order_id = lo.pos_order_id - LEFT JOIN pos_payment_method pm ON pm.id = pp.payment_method_id - GROUP BY lo.id - ) - UPDATE laundry_order lo - SET amount_paid_cash = c.cash, - amount_deferred = c.deferred, - amount_settled = COALESCE(lo.amount_settled, 0) - FROM classified c - WHERE c.lo_id = lo.id; - """) - - # amount_due is a stored compute (amount_deferred - amount_settled). - # Populate it directly here so the values are correct before the ORM - # recomputes (ORM recompute on install will overwrite with same result). - cr.execute(""" - UPDATE laundry_order - SET amount_due = GREATEST( - COALESCE(amount_deferred, 0) - COALESCE(amount_settled, 0), - 0 - ); - """) - - cr.execute(""" - SELECT COUNT(*), - SUM(amount_paid_cash), - SUM(amount_deferred), - SUM(amount_due) - FROM laundry_order - """) - row = cr.fetchone() - _logger.info( - 'laundry_management migration: %s orders — cash=%.2f, deferred=%.2f, due=%.2f', - row[0] or 0, row[1] or 0.0, row[2] or 0.0, row[3] or 0.0, - ) diff --git a/addons/laundry_management/migrations/__init__.py b/addons/laundry_management/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/addons/laundry_management/models/__init__.py b/addons/laundry_management/models/__init__.py deleted file mode 100644 index befd311..0000000 --- a/addons/laundry_management/models/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from . import product_template_ext # adds is_laundry_service to product.template -from . import laundry_order_type # standalone: laundry.order.type -from . import laundry_order_attribute # standalone: laundry.order.attribute -from . import laundry_order # standalone: laundry.order -from . import laundry_order_line # standalone: laundry.order.line -from . import pos_order # extends pos.order with sync_from_ui hook -from . import pos_config_ext # extends pos.config with laundry-pos settings -from . import res_partner # extends res.partner with laundry unpaid count -from . import laundry_order_line_addon # add-on services per order line -from . import laundry_commission # standalone: commission tracking -# NOTE: laundry_dashboard removed — depends on laundry.session (POS-owned) -from . import laundry_payment_method # standalone: configurable payment methods -from . import laundry_settings # extends res.config.settings -from . import account_payment_ext # stamps pos_session_id on settlement payments -from . import pos_session_ext # ships new POS models to the client -# NOTE: laundry_session and account_move removed — session/accounting is POS-owned diff --git a/addons/laundry_management/models/account_move.py b/addons/laundry_management/models/account_move.py deleted file mode 100644 index 694e2b0..0000000 --- a/addons/laundry_management/models/account_move.py +++ /dev/null @@ -1,21 +0,0 @@ -from odoo import models, fields, api - - -class AccountMoveLaundryExt(models.Model): - """Flag invoices originating from laundry orders.""" - _inherit = 'account.move' - - is_laundry_invoice = fields.Boolean( - string='Laundry Invoice', - compute='_compute_is_laundry_invoice', - store=True, - ) - - @api.depends('invoice_line_ids.sale_line_ids.order_id.is_laundry_order') - def _compute_is_laundry_invoice(self): - for move in self: - move.is_laundry_invoice = any( - sol.order_id.is_laundry_order - for line in move.invoice_line_ids - for sol in line.sale_line_ids - ) diff --git a/addons/laundry_management/models/account_payment_ext.py b/addons/laundry_management/models/account_payment_ext.py deleted file mode 100644 index 25ade3d..0000000 --- a/addons/laundry_management/models/account_payment_ext.py +++ /dev/null @@ -1,22 +0,0 @@ -from odoo import models, fields - - -class AccountPaymentLaundryExt(models.Model): - """Informational stamp — links settlement payments to the POS session - that was open when the cashier collected the money. - - This field is purely for visibility (closing-screen summary). It does - NOT inject settlement totals into POS cash-control math. - """ - _inherit = 'account.payment' - - pos_session_id = fields.Many2one( - 'pos.session', string='POS Session', - readonly=True, copy=False, index=True, - help='POS session that was open when this settlement was created.', - ) - settlement_pos_pm_id = fields.Many2one( - 'pos.payment.method', string='Settlement Payment Method', - readonly=True, copy=False, index=True, - help='Original POS payment method chosen by the cashier during settlement.', - ) diff --git a/addons/laundry_management/models/laundry_commission.py b/addons/laundry_management/models/laundry_commission.py deleted file mode 100644 index 73a9fe1..0000000 --- a/addons/laundry_management/models/laundry_commission.py +++ /dev/null @@ -1,125 +0,0 @@ -from odoo import models, fields, api -from odoo.exceptions import UserError - - -class LaundryCommission(models.Model): - """Staff commission tracking (PART 3). - - States: - pending — auto-created when order progresses; awaiting manager review - confirmed — manager has verified and approved the commission - paid — commission has been settled/paid to the staff member - - The commission_account_id (from settings) is informational for now. - Managers can bulk-confirm and bulk-mark-paid from the list view. - """ - _name = 'laundry.commission' - _description = 'Laundry Staff Commission' - _inherit = ['mail.thread'] - _order = 'date desc, id desc' - - name = fields.Char( - string='Reference', - compute='_compute_name', store=True, readonly=True, - ) - order_id = fields.Many2one( - 'sale.order', string='Order', - required=True, ondelete='cascade', index=True, - ) - company_id = fields.Many2one( - 'res.company', - related='order_id.company_id', store=True, index=True, - ) - employee_id = fields.Many2one( - 'res.users', string='Staff Member', - required=True, domain=[('share', '=', False)], - tracking=True, - ) - role = fields.Selection([ - ('reception', 'Reception / Intake'), - ('processing', 'Processing / Cleaning'), - ('delivery', 'Delivery / Handover'), - ], string='Role', required=True, tracking=True) - - commission_type = fields.Selection([ - ('percentage', 'Percentage (%)'), - ('fixed', 'Fixed Amount'), - ], string='Type', required=True, default='percentage') - - rate = fields.Float(string='Rate / Amount', digits=(10, 2)) - base_amount = fields.Float(string='Order Total', digits=(10, 2)) - commission_amount = fields.Float( - string='Commission', - compute='_compute_commission_amount', store=True, digits=(10, 2), - ) - - date = fields.Date(string='Date', required=True, default=fields.Date.today) - - state = fields.Selection([ - ('pending', 'Pending'), - ('confirmed', 'Confirmed'), - ('paid', 'Paid'), - ], default='pending', required=True, tracking=True, copy=False, index=True) - - notes = fields.Text(string='Notes') - - _ROLE_LABELS = { - 'reception': 'Reception', - 'processing': 'Processing', - 'delivery': 'Delivery', - } - - @api.depends('order_id', 'order_id.name', 'role') - def _compute_name(self): - for rec in self: - order = rec.order_id.name or 'NEW' - role = self._ROLE_LABELS.get(rec.role, rec.role or '') - rec.name = f'COM/{order}/{role}' - - @api.depends('commission_type', 'rate', 'base_amount') - def _compute_commission_amount(self): - for rec in self: - if rec.commission_type == 'percentage': - rec.commission_amount = rec.base_amount * rec.rate / 100.0 - else: - rec.commission_amount = rec.rate - - # ── State transitions ───────────────────────────────────────────── - def action_confirm(self): - """Manager confirms commission is valid and approved.""" - for rec in self: - if rec.state != 'pending': - raise UserError( - f'"{rec.name}" cannot be confirmed — current state: {rec.state}.' - ) - rec.write({'state': 'confirmed'}) - rec.message_post( - body=f'Commission confirmed by {self.env.user.name}. ' - f'Amount: {rec.commission_amount:.2f}' - ) - - def action_mark_paid(self): - """Mark commission as settled/paid to staff.""" - for rec in self: - if rec.state == 'paid': - raise UserError(f'"{rec.name}" is already paid.') - if rec.state == 'pending': - # Allow paying directly from pending (manager shortcut) - rec.write({'state': 'paid'}) - else: - rec.write({'state': 'paid'}) - rec.message_post( - body=f'Marked as paid by {self.env.user.name}.' - ) - - def action_reset_pending(self): - """Reset commission back to pending (manager only).""" - for rec in self: - if rec.state == 'paid': - raise UserError( - f'Cannot reset "{rec.name}" — it has already been paid.' - ) - rec.write({'state': 'pending'}) - rec.message_post( - body=f'Reset to pending by {self.env.user.name}.' - ) diff --git a/addons/laundry_management/models/laundry_dashboard.py b/addons/laundry_management/models/laundry_dashboard.py deleted file mode 100644 index 1e6be43..0000000 --- a/addons/laundry_management/models/laundry_dashboard.py +++ /dev/null @@ -1,180 +0,0 @@ -from odoo import models, fields, api - - -class LaundryDashboard(models.TransientModel): - """Live KPI dashboard — queries sale.order with is_laundry_order = True.""" - _name = 'laundry.dashboard' - _description = 'Laundry Dashboard' - - today_orders = fields.Integer(string="Today's Orders") - today_revenue = fields.Monetary(string="Today's Revenue", currency_field='currency_id') - today_collected = fields.Monetary(string='Collected Today', currency_field='currency_id') - today_outstanding = fields.Monetary(string='Outstanding Today', currency_field='currency_id') - - pending_count = fields.Integer(string='Pending Orders') - ready_count = fields.Integer(string='Ready for Pickup') - in_progress_count = fields.Integer(string='In Processing') - draft_count = fields.Integer(string='Quotes / Draft') - - session_is_open = fields.Boolean(string='Session Open') - session_name = fields.Char(string='Session') - session_opening_cash = fields.Monetary(string='Opening Float', currency_field='currency_id') - session_sales = fields.Monetary(string='Session Sales', currency_field='currency_id') - session_cash = fields.Monetary(string='Session Cash', currency_field='currency_id') - session_bank = fields.Monetary(string='Session Bank', currency_field='currency_id') - session_id = fields.Many2one('laundry.session', string='Session Link') - - month_orders = fields.Integer(string='Orders This Month') - month_revenue = fields.Monetary(string='Revenue This Month', currency_field='currency_id') - month_paid = fields.Monetary(string='Collected This Month', currency_field='currency_id') - - currency_id = fields.Many2one( - 'res.currency', - default=lambda self: self.env.company.currency_id, - ) - - @api.model - def _build(self): - today = fields.Date.today() - month_start = today.replace(day=1) - company = self.env.company - Order = self.env['sale.order'] - Payment = self.env['account.payment'] - - _base_domain = [ - ('is_laundry_order', '=', True), - ('company_id', '=', company.id), - ] - - # ── Today ────────────────────────────────────────────────────── - today_orders = Order.search(_base_domain + [ - ('date_order', '>=', fields.Datetime.to_datetime(today)), - ('state', 'not in', ['cancel', 'draft']), - ]) - today_invoices = today_orders.mapped('invoice_ids').filtered( - lambda i: i.state == 'posted' and i.move_type == 'out_invoice' - ) - today_revenue = sum(today_orders.mapped('amount_total')) - today_outstanding = sum( - max(i.amount_residual, 0.0) for i in today_invoices - ) - today_collected = today_revenue - today_outstanding - - # ── Pipeline (all active laundry orders) ────────────────────── - pipeline = Order.search(_base_domain + [ - ('state', '=', 'sale'), - ]) - pending_count = len(pipeline) - ready_count = len(pipeline.filtered(lambda o: o.laundry_state == 'ready')) - in_progress_count = len(pipeline.filtered(lambda o: o.laundry_state == 'processing')) - draft_count = len(Order.search(_base_domain + [('state', '=', 'draft')])) - - # ── Session ──────────────────────────────────────────────────── - session = self.env['laundry.session'].search([ - ('state', '=', 'opened'), - ('company_id', '=', company.id), - ], limit=1) - - # ── Month ────────────────────────────────────────────────────── - month_orders = Order.search(_base_domain + [ - ('date_order', '>=', fields.Datetime.to_datetime(month_start)), - ('state', 'not in', ['cancel', 'draft']), - ]) - month_invoices = month_orders.mapped('invoice_ids').filtered( - lambda i: i.state == 'posted' and i.move_type == 'out_invoice' - ) - month_revenue = sum(month_orders.mapped('amount_total')) - month_outstanding = sum(max(i.amount_residual, 0.0) for i in month_invoices) - month_paid = month_revenue - month_outstanding - - return self.create({ - 'today_orders' : len(today_orders), - 'today_revenue' : today_revenue, - 'today_collected' : max(today_collected, 0.0), - 'today_outstanding' : today_outstanding, - 'pending_count' : pending_count, - 'ready_count' : ready_count, - 'in_progress_count' : in_progress_count, - 'draft_count' : draft_count, - 'session_is_open' : bool(session), - 'session_name' : session.name if session else '', - 'session_opening_cash' : session.opening_cash if session else 0.0, - 'session_sales' : session.total_sales if session else 0.0, - 'session_cash' : session.total_cash if session else 0.0, - 'session_bank' : session.total_bank if session else 0.0, - 'session_id' : session.id if session else False, - 'month_orders' : len(month_orders), - 'month_revenue' : month_revenue, - 'month_paid' : max(month_paid, 0.0), - }) - - @api.model - def action_open_dashboard(self): - rec = self._build() - return { - 'type': 'ir.actions.act_window', - 'name': 'Dashboard', - 'res_model': 'laundry.dashboard', - 'res_id': rec.id, - 'view_mode': 'form', - 'target': 'main', - 'flags': {'mode': 'readonly'}, - } - - def action_refresh(self): - return self.action_open_dashboard() - - def action_new_order(self): - return { - 'type': 'ir.actions.act_window', - 'name': 'New Laundry Order', - 'res_model': 'sale.order', - 'view_mode': 'form', - 'target': 'current', - 'context': { - 'default_is_laundry_order': True, - }, - } - - def action_open_session(self): - if self.session_id: - return { - 'type': 'ir.actions.act_window', - 'name': 'Session', - 'res_model': 'laundry.session', - 'res_id': self.session_id.id, - 'view_mode': 'form', - } - return { - 'type': 'ir.actions.act_window', - 'name': 'Sessions', - 'res_model': 'laundry.session', - 'view_mode': 'list,form', - } - - def action_new_session(self): - return { - 'type': 'ir.actions.act_window', - 'name': 'New Session', - 'res_model': 'laundry.session', - 'view_mode': 'form', - } - - def action_view_ready_orders(self): - return { - 'type': 'ir.actions.act_window', - 'name': 'Ready for Pickup', - 'res_model': 'sale.order', - 'view_mode': 'list,form', - 'domain': [('is_laundry_order', '=', True), ('laundry_state', '=', 'ready')], - } - - def action_view_pending_orders(self): - return { - 'type': 'ir.actions.act_window', - 'name': 'Pending Orders', - 'res_model': 'sale.order', - 'view_mode': 'list,form', - 'domain': [('is_laundry_order', '=', True), ('state', '=', 'sale'), - ('laundry_state', 'not in', ['delivered'])], - } diff --git a/addons/laundry_management/models/laundry_order.py b/addons/laundry_management/models/laundry_order.py deleted file mode 100644 index 1687f69..0000000 --- a/addons/laundry_management/models/laundry_order.py +++ /dev/null @@ -1,765 +0,0 @@ -import logging - -from odoo import models, fields, api, _ -from odoo.exceptions import UserError - -_logger = logging.getLogger(__name__) - -STATES = [ - ('intake', 'Intake'), - ('processing', 'Processing'), - ('ready', 'Ready for Pickup'), - ('delivered', 'Delivered'), - ('cancelled', 'Cancelled'), -] -STATE_KEYS = [s[0] for s in STATES] -FINAL_STATES = {'delivered', 'cancelled'} - -SOURCE_TYPES = [ - ('pos', 'Point of Sale'), - ('manual', 'Manual / Backoffice'), -] - -# Header fields the lock protects. NOT included on purpose: -# - state (workflow advance is always allowed via dedicated actions) -# - amount_settled (settlement engine writes after lock) -# - notes (managerial commentary always allowed) -# - manager_unlocked_* (the unlock wizard writes these) -# - tracking_enabled (Phase-4 prep, manager configuration) -LOCKED_HEADER_FIELDS = frozenset({ - 'partner_id', - 'amount_total', 'amount_paid_cash', 'amount_deferred', - 'order_type_id', 'attribute_ids', - 'is_delivery', 'delivery_address', 'delivery_scheduled_at', - 'priority_level', - 'pos_order_id', 'pos_reference', - 'source_type', 'name', 'company_id', -}) - -# Sentinel context flag set by the POS sync hook (and any other automated -# server-side path) to allow the create + write that bring a fresh -# laundry.order to life. Without this flag, locked orders refuse mutations. -POS_SYNC_CTX = 'laundry_pos_sync' - - -class LaundryOrder(models.Model): - """Standalone laundry order — created from POS. - - POS owns payments/sessions/accounting. This model handles operational - workflow only: intake -> processing -> ready -> delivered. - - Financial model (Phase 1 fix): - amount_total = mirror of pos.order.amount_total - amount_paid_cash = real money collected at origin (cash/card) - amount_deferred = Customer Account / pay-later amount at origin - amount_settled = money collected later via settlement engine - amount_due = amount_deferred - amount_settled - - `amount_due > 0` means the customer still owes real money. - """ - _name = 'laundry.order' - _description = 'Laundry Order' - _inherit = ['mail.thread', 'mail.activity.mixin'] - _order = 'create_date desc, id desc' - - name = fields.Char( - string='Order No.', - required=True, copy=False, readonly=True, - default='New', tracking=True, - ) - - # -- POS link -- - # Optional: manual/backoffice orders have no POS origin. The uniqueness - # constraint below still enforces "one laundry order per pos.order" for - # POS-sourced rows. - pos_order_id = fields.Many2one( - 'pos.order', string='POS Order', - index=True, readonly=True, copy=False, - ondelete='restrict', - ) - pos_reference = fields.Char( - string='POS Reference', - readonly=True, copy=False, index=True, - ) - - # -- Customer -- - partner_id = fields.Many2one( - 'res.partner', string='Customer', - required=True, index=True, tracking=True, - ) - partner_phone = fields.Char( - related='partner_id.phone', string='Phone', readonly=True, - ) - - # -- Company / Currency -- - company_id = fields.Many2one( - 'res.company', string='Company', - required=True, default=lambda self: self.env.company, - index=True, - ) - currency_id = fields.Many2one( - related='company_id.currency_id', store=True, - ) - - # -- Workflow -- - state = fields.Selection( - STATES, - string='Status', - default='intake', - required=True, - tracking=True, - copy=False, - index=True, - ) - - # -- Lines -- - line_ids = fields.One2many( - 'laundry.order.line', 'order_id', string='Order Lines', - ) - - # -- Financial snapshot -- - amount_total = fields.Monetary( - string='Total', - currency_field='currency_id', - readonly=True, copy=False, store=True, - ) - - amount_paid_cash = fields.Monetary( - string='Paid (Cash/Card)', - currency_field='currency_id', - readonly=True, copy=False, store=True, - default=0.0, - help='Amount collected at origin via non-deferred payment methods ' - '(cash, card — any pos.payment.method with split_transactions=False).', - ) - - amount_deferred = fields.Monetary( - string='Deferred', - currency_field='currency_id', - readonly=True, copy=False, store=True, - default=0.0, - help='Amount deferred at origin via Customer Account / pay-later ' - '(any pos.payment.method with split_transactions=True).', - ) - - amount_settled = fields.Monetary( - string='Settled', - currency_field='currency_id', - readonly=True, copy=False, store=True, - default=0.0, - help='Amount collected later via the settlement engine ' - '(account.payment + reconciliation).', - ) - - amount_due = fields.Monetary( - string='Due', - currency_field='currency_id', - compute='_compute_amount_due', - store=True, - help='Remaining balance the customer owes: amount_deferred - amount_settled.', - ) - - # Back-compat alias — views/reports still reference `amount_paid`. - # Computed, non-stored; reflects real cash collected at origin. - amount_paid = fields.Monetary( - string='Paid', - currency_field='currency_id', - compute='_compute_amount_paid_alias', - ) - - # -- Computed -- - item_count = fields.Integer( - string='Items', - compute='_compute_item_count', - store=True, - ) - - # -- Order type / attributes / delivery -- - order_type_id = fields.Many2one( - 'laundry.order.type', string='Order Type', - index=True, tracking=True, - ) - attribute_ids = fields.Many2many( - 'laundry.order.attribute', - 'laundry_order_attribute_rel', - 'order_id', 'attribute_id', - string='Attributes', - ) - is_delivery = fields.Boolean(string='Delivery', tracking=True, index=True) - delivery_address = fields.Text(string='Delivery Address') - delivery_scheduled_at = fields.Datetime(string='Scheduled At') - priority_level = fields.Selection([ - ('normal', 'Normal'), - ('urgent', 'Urgent'), - ], string='Priority', default='normal', tracking=True, index=True) - - # -- Notes -- - notes = fields.Text(string='Notes') - - # ── Source / locking (Phase 3) ──────────────────────────────────── - # source_type is the truth-bearing identity. is_from_pos is a stored - # mirror used in domains, list filters, and rule conditions where a - # selection field would be awkward. - source_type = fields.Selection( - SOURCE_TYPES, - string='Source', - required=True, - default='manual', - readonly=True, - copy=False, - index=True, - tracking=True, - help='POS-sourced orders are hard-locked: lines, prices and the ' - 'customer cannot be edited unless a manager grants a ' - 'temporary unlock window. Manual orders are editable until ' - 'they reach a final state (delivered / cancelled).', - ) - - is_from_pos = fields.Boolean( - string='From POS', - compute='_compute_is_from_pos', - store=True, index=True, - ) - - # Phase-4 prep — flag only, no logic wired yet. - tracking_enabled = fields.Boolean( - string='Per-Item Tracking', - default=False, - copy=False, - help='When enabled, each laundry.order.line will be advanced ' - 'through its own state machine. Phase 4 wires the ' - 'synchronization between order state and item state.', - ) - - # Computed: True when the order refuses mutation of LOCKED_HEADER_FIELDS - # and any line write/create/unlink. Not stored — cheap to recompute and - # depends on a Datetime that ages out without a write. - locked = fields.Boolean( - string='Locked', - compute='_compute_locked', - help='Order is read-only when True. POS-sourced orders are ' - 'always locked. Final-state orders (delivered, cancelled) ' - 'are always locked. Managers can grant a temporary unlock ' - 'window via the "Unlock for Editing" action.', - ) - - manager_unlocked_until = fields.Datetime( - string='Unlock Window Expires', - copy=False, readonly=True, - help='When set in the future, the lock guard is suspended. ' - 'Auto-expires; no manual re-lock required.', - ) - - manager_unlocked_by = fields.Many2one( - 'res.users', string='Last Unlocked By', - copy=False, readonly=True, - ) - - manager_unlock_reason = fields.Char( - string='Last Unlock Reason', - copy=False, readonly=True, - ) - - # Stamped when the order moves to delivered. Powers the avg-processing - # and on-time KPIs on the Operations Dashboard. Outside - # LOCKED_HEADER_FIELDS so action_deliver can write it on POS-locked - # orders without needing the bypass context. - delivered_at = fields.Datetime( - string='Delivered At', - readonly=True, copy=False, index=True, - help='Timestamp set automatically when the order moves to ' - 'Delivered. Used by the analytics dashboard to compute ' - 'processing time and on-time delivery percentage.', - ) - - # -- Constraints -- - _pos_order_uniq = models.Constraint( - 'UNIQUE(pos_order_id)', - 'A laundry order already exists for this POS order.', - ) - - @api.constrains('source_type', 'pos_order_id') - def _check_source_type_consistency(self): - for order in self: - if order.source_type == 'pos' and not order.pos_order_id: - raise UserError(_( - 'Order "%s" is marked as POS-sourced but has no ' - 'linked POS order.', order.name or order.id, - )) - if order.source_type == 'manual' and order.pos_order_id: - raise UserError(_( - 'Order "%s" is marked as manual but has a linked ' - 'POS order. Set source_type="pos" to keep them ' - 'consistent.', order.name or order.id, - )) - - # -- Computed -- - @api.depends('amount_deferred', 'amount_settled') - def _compute_amount_due(self): - for order in self: - order.amount_due = max( - (order.amount_deferred or 0.0) - (order.amount_settled or 0.0), - 0.0, - ) - - @api.depends('amount_paid_cash') - def _compute_amount_paid_alias(self): - for order in self: - order.amount_paid = order.amount_paid_cash or 0.0 - - @api.depends('source_type') - def _compute_is_from_pos(self): - for order in self: - order.is_from_pos = order.source_type == 'pos' - - @api.depends('source_type', 'state', 'manager_unlocked_until') - def _compute_locked(self): - now = fields.Datetime.now() - for order in self: - unlock_active = bool( - order.manager_unlocked_until - and order.manager_unlocked_until > now - ) - base_locked = ( - order.source_type == 'pos' - or order.state in FINAL_STATES - ) - order.locked = base_locked and not unlock_active - - # ── Lock enforcement helpers ────────────────────────────────────── - def _is_pos_sync(self): - """True when the call originates from the POS sync hook (or any - explicit server path that opts in via the context flag). - - Both create() and write() honour this so the bridge from - pos.order can build / refresh the laundry.order without fighting - its own lock guard. This is checked BEFORE anything else in - write() so indirect POS writes (stored-compute flushes, cascades) - can never raise from the guard. - """ - return bool(self.env.context.get(POS_SYNC_CTX)) - - def _check_lock_for_write(self, vals): - """Raise UserError when `vals` would mutate a protected header - field on a currently-locked order. Workflow advances (state, - amount_settled, notes, manager_unlocked_*) are excluded by - whitelist (LOCKED_HEADER_FIELDS). - - Note: the POS-sync bypass is already applied at the top of - `write()` — this helper is only invoked for non-bypassed paths. - """ - protected = LOCKED_HEADER_FIELDS.intersection(vals.keys()) - if not protected: - return - for order in self: - if order.locked: - raise UserError(_( - 'Order "%(name)s" is locked. Editable fields: state ' - 'transitions, internal notes, settlement amount.\n' - 'To edit %(fields)s, ask a manager to use ' - '"Unlock for Editing" first.', - name=order.name, - fields=', '.join(sorted(protected)), - )) - - def _check_lock_for_unlink(self): - """POS-sourced and final-state orders cannot be unlinked. The - manager unlock wizard is intentionally NOT honored here — deletion - requires a stronger affordance (cancellation + audit trail), not - a temporary edit window. - """ - for order in self: - if order.source_type == 'pos': - raise UserError(_( - 'Order "%(name)s" was created from POS and cannot ' - 'be deleted. Cancel the underlying POS order instead.', - name=order.name, - )) - if order.state in FINAL_STATES: - raise UserError(_( - 'Order "%(name)s" is in a final state (%(state)s) ' - 'and cannot be deleted.', - name=order.name, - state=dict(STATES).get(order.state, order.state), - )) - - @api.depends('line_ids.qty') - def _compute_item_count(self): - for order in self: - order.item_count = int(sum(order.line_ids.mapped('qty'))) - - # -- ORM -- - @api.model_create_multi - def create(self, vals_list): - for vals in vals_list: - if vals.get('name', 'New') == 'New': - vals['name'] = ( - self.env['ir.sequence'].next_by_code('laundry.order') - or 'New' - ) - self._apply_type_attribute_inference(vals) - return super().create(vals_list) - - def write(self, vals): - # STEP 1 — POS sync bypass. - # Must be the very first thing we do. Any code path that opts - # into the context flag (POS create/sync, settlement engine, - # stored-compute flushes triggered from inside a bypassed write) - # MUST sail through unconditionally. No locking check, no - # side-effects, no iteration over self — just delegate to super. - # This guarantee is what makes POS payment/settlement flows - # immune to this model's lock guard. - if self._is_pos_sync(): - if _logger.isEnabledFor(logging.DEBUG): - _logger.debug( - 'laundry.order.write BYPASS ids=%s keys=%s', - self.ids, list(vals.keys()), - ) - return super().write(vals) - - # STEP 2 — Forensic trace (DEBUG-level; off by default in prod, - # enable with `--log-level=debug` or `--log-handler=odoo.addons - # .laundry_management.models.laundry_order:DEBUG`). - if _logger.isEnabledFor(logging.DEBUG): - keys = list(vals.keys()) - for order in self: - _logger.debug( - 'laundry.order.write id=%s source=%s state=%s ' - 'locked=%s ctx_keys=%s vals_keys=%s', - order.id, order.source_type, order.state, order.locked, - sorted(self.env.context.keys()), keys, - ) - - # STEP 3 — Lock guard for non-POS callers. - self._check_lock_for_write(vals) - return super().write(vals) - - def unlink(self): - # Explicit: even with POS-sync context, refuse to delete a locked - # order. Deletion is not a mutation the sync path ever issues. - self._check_lock_for_unlink() - return super().unlink() - - @api.model - def _apply_type_attribute_inference(self, vals): - """Fill in priority_level / is_delivery from the selected order - type and attributes when the caller did not explicitly set them. - - Rules: - - type.priority='urgent' OR any attribute with - is_priority_related=True → priority_level='urgent' - - type.is_delivery=True OR any attribute with - is_delivery_related=True → is_delivery=True - - Do NOT overwrite explicit incoming delivery_address / - delivery_scheduled_at with blank values. - """ - type_id = vals.get('order_type_id') - order_type = ( - self.env['laundry.order.type'].browse(type_id) if type_id else None - ) - - attribute_ids = [] - raw_attrs = vals.get('attribute_ids') or [] - for cmd in raw_attrs: - if isinstance(cmd, (list, tuple)) and len(cmd) >= 3: - # Odoo x2m commands: (6,0,[ids]), (4,id), etc. - if cmd[0] == 6 and isinstance(cmd[2], (list, tuple)): - attribute_ids.extend(cmd[2]) - elif cmd[0] == 4 and cmd[1]: - attribute_ids.append(cmd[1]) - elif isinstance(cmd, int): - attribute_ids.append(cmd) - attributes = ( - self.env['laundry.order.attribute'].browse(attribute_ids) - if attribute_ids else self.env['laundry.order.attribute'] - ) - - # Priority - if 'priority_level' not in vals: - urgent = ( - (order_type and order_type.priority == 'urgent') - or any(a.is_priority_related for a in attributes) - ) - vals['priority_level'] = 'urgent' if urgent else 'normal' - - # Delivery - if 'is_delivery' not in vals: - delivery = ( - (order_type and order_type.is_delivery) - or any(a.is_delivery_related for a in attributes) - ) - vals['is_delivery'] = bool(delivery) - - # -- Workflow actions -- - def action_process(self): - for order in self: - if order.state != 'intake': - raise UserError(_( - 'Order "%(name)s" is not in Intake state.', - name=order.name, - )) - order.state = 'processing' - - def action_ready(self): - for order in self: - if order.state != 'processing': - raise UserError(_( - 'Order "%(name)s" is not in Processing state.', - name=order.name, - )) - order.state = 'ready' - - def action_deliver(self): - """Guards: must be Ready + fully paid (amount_due == 0). - - Also stamps `delivered_at` so the dashboard KPIs (avg processing - time, on-time delivery %) can be computed from real data instead - of the heuristic on `write_date`. - """ - for order in self: - if order.state != 'ready': - raise UserError(_( - 'Order "%(name)s" must be Ready before delivery.', - name=order.name, - )) - if order.amount_due > 0: - raise UserError(_( - 'Order "%(name)s" has %(due).2f outstanding. ' - 'Collect payment in POS before delivery.', - name=order.name, - due=order.amount_due, - )) - order.write({ - 'state': 'delivered', - 'delivered_at': fields.Datetime.now(), - }) - - def action_cancel(self): - """Cancel an order. Allowed for manual orders only — POS-sourced - orders must be voided through the POS workflow to keep the sale - and the operational record in sync. - """ - for order in self: - if order.source_type == 'pos': - raise UserError(_( - 'Order "%(name)s" was created from POS and cannot ' - 'be cancelled here. Cancel the underlying POS order ' - 'instead.', - name=order.name, - )) - if order.state in FINAL_STATES: - raise UserError(_( - 'Order "%(name)s" is already %(state)s.', - name=order.name, - state=dict(STATES).get(order.state, order.state), - )) - order.state = 'cancelled' - - # -- Smart button -- - def action_open_pos_order(self): - self.ensure_one() - return { - 'type': 'ir.actions.act_window', - 'res_model': 'pos.order', - 'res_id': self.pos_order_id.id, - 'view_mode': 'form', - 'target': 'current', - } - - # ═════════════════════════════════════════════════════════════════ - # POS "Laundry Orders" popup — server-side RPCs - # --------------------------------------------------------------- - # These methods are the ONLY way the POS popup interacts with the - # model. Each action delegates to the corresponding workflow method - # (`action_process` / `action_ready` / `action_deliver`) which already - # enforces state + amount_due guards server-side. Direct writes to - # LOCKED_HEADER_FIELDS are NOT exposed here — the Phase 3 lock - # remains the sole authority for business-edit protection. - # ═════════════════════════════════════════════════════════════════ - - def _pos_allowed_actions(self): - """Return the list of action keys the popup may render for this - order. Pure function of state + amount_due. Final states only - allow printing. - """ - self.ensure_one() - actions = ['print_work_order'] - if self.state in FINAL_STATES: - return actions - if self.state == 'intake': - actions.append('start_processing') - elif self.state == 'processing': - actions.append('mark_ready') - elif self.state == 'ready': - if self.amount_due <= 0: - actions.append('deliver') - else: - actions.append('collect_payment') - return actions - - def _pos_payment_state(self): - self.ensure_one() - if self.amount_due > 0: - return 'due' - if self.amount_deferred > 0 and self.amount_settled >= self.amount_deferred: - return 'settled' - if self.amount_deferred > 0: - return 'deferred' - return 'paid' - - def _pos_payload(self): - """Compact, UI-ready dict. Single source of truth for the popup - shape — every RPC returns exactly this structure.""" - self.ensure_one() - names = self.line_ids.mapped('product_id.name') - if not names: - names = self.line_ids.mapped('description') - state_selection = dict(self._fields['state'].selection) - return { - 'id': self.id, - 'name': self.name, - 'state': self.state, - 'state_label': state_selection.get(self.state) or self.state, - 'pos_reference': self.pos_reference or '', - 'is_from_pos': self.is_from_pos, - 'create_date': fields.Datetime.to_string(self.create_date) if self.create_date else False, - 'item_count': int(self.item_count or 0), - 'service_summary': ', '.join(dict.fromkeys(names))[:80], - 'amount_total': self.amount_total, - 'amount_paid': self.amount_paid_cash, - 'amount_deferred': self.amount_deferred, - 'amount_settled': self.amount_settled, - 'amount_due': self.amount_due, - 'payment_state': self._pos_payment_state(), - 'is_delivery': self.is_delivery, - 'allowed_actions': self._pos_allowed_actions(), - } - - @api.model - def pos_search_customer_orders(self, partner_id, search_query=False, limit=20): - """Partner-scoped search for the POS popup. - - • Always filters by partner_id (no global search in this phase). - • Optional search_query ilike-matches on name / pos_reference / - partner_phone. - • Hard-capped at 50 rows regardless of the caller's limit. - """ - if not partner_id: - return [] - limit = max(1, min(int(limit or 20), 50)) - q = (search_query or '').strip() - domain = [('partner_id', '=', partner_id)] - if q: - domain = [ - '&', - ('partner_id', '=', partner_id), - '|', '|', - ('name', 'ilike', q), - ('pos_reference', 'ilike', q), - ('partner_phone', 'ilike', q), - ] - orders = self.search( - domain, order='create_date desc, id desc', limit=limit, - ) - return [o._pos_payload() for o in orders] - - def pos_action_start_processing(self): - """POS popup: advance intake → processing. Returns refreshed payload.""" - self.ensure_one() - self.action_process() - return self._pos_payload() - - def pos_action_mark_ready(self): - """POS popup: advance processing → ready. Returns refreshed payload.""" - self.ensure_one() - self.action_ready() - return self._pos_payload() - - def pos_action_deliver(self): - """POS popup: advance ready → delivered. Returns refreshed payload. - - `action_deliver` raises UserError when amount_due > 0 — the popup - surfaces that error; no client-side duplication of the rule. - """ - self.ensure_one() - self.action_deliver() - return self._pos_payload() - - @api.model - def pos_get_thermal_data(self, order_id): - """Build a self-contained payload for the thermal Work-Order - receipt rendered by `laundry_management.LaundryWorkOrderThermal`. - - Independent from `_pos_payload` — that one is for the popup list - (compact); this one carries every line + delivery meta the - cashier needs on the printed slip. - """ - order = self.browse(int(order_id)) - if not order.exists(): - return False - state_label = dict(order._fields['state'].selection).get( - order.state, order.state, - ) - return { - 'id': order.id, - 'name': order.name, - 'state': order.state, - 'state_label': state_label, - 'payment_state': order._pos_payment_state(), - 'pos_reference': order.pos_reference or '', - 'partner_name': order.partner_id.name or '', - # `mobile` is provided by the optional `phone` add-on; fall - # back gracefully when it isn't present in the install. - 'partner_phone': ( - order.partner_id.phone - or getattr(order.partner_id, 'mobile', '') - or '' - ), - 'company_name': order.company_id.name or '', - 'create_date': fields.Datetime.to_string(order.create_date), - 'lines': [{ - 'qty': line.qty, - 'description': ( - line.description - or (line.product_id.name if line.product_id else '') - ), - 'price_unit': line.price_unit, - 'subtotal': line.subtotal, - 'tracking_code': line.tracking_code or '', - } for line in order.line_ids], - 'item_count': int(order.item_count or 0), - 'amount_total': order.amount_total, - 'amount_paid': order.amount_paid_cash, - 'amount_deferred': order.amount_deferred, - 'amount_settled': order.amount_settled, - 'amount_due': order.amount_due, - 'is_delivery': order.is_delivery, - 'delivery_address': order.delivery_address or '', - 'delivery_scheduled_at': ( - fields.Datetime.to_string(order.delivery_scheduled_at) - if order.delivery_scheduled_at else '' - ), - 'currency_symbol': order.currency_id.symbol or '', - 'currency_position': order.currency_id.position or 'after', - } - - def action_open_unlock_wizard(self): - """Open the manager unlock wizard pre-filled with this order. - Access is enforced inside the wizard's action method (group - check + reason required), but we also short-circuit here so - the button itself is silent for non-managers. - """ - self.ensure_one() - if not self.locked: - raise UserError(_( - 'Order "%s" is already editable.', self.name, - )) - return { - 'type': 'ir.actions.act_window', - 'res_model': 'laundry.order.unlock.wizard', - 'view_mode': 'form', - 'target': 'new', - 'context': { - 'default_order_id': self.id, - }, - } diff --git a/addons/laundry_management/models/laundry_order_attribute.py b/addons/laundry_management/models/laundry_order_attribute.py deleted file mode 100644 index 7a53c60..0000000 --- a/addons/laundry_management/models/laundry_order_attribute.py +++ /dev/null @@ -1,62 +0,0 @@ -from odoo import models, fields, api - - -class LaundryOrderAttribute(models.Model): - """Optional per-order badge (Urgent / Hanger / Fold / Delicate / ...). - - Multi-selectable on a laundry order. Semantic flags drive behavior - without name matching, so admins can rename freely. - """ - _name = 'laundry.order.attribute' - _inherit = ['pos.load.mixin'] - _description = 'Laundry Order Attribute' - _order = 'sequence, id' - - name = fields.Char(string='Name', required=True, translate=True) - code = fields.Char(string='Code') - sequence = fields.Integer(default=10) - active = fields.Boolean(default=True) - - color = fields.Char(string='Color') - icon_image = fields.Binary(string='Icon') - description = fields.Text(string='Description', translate=True) - - extra_price = fields.Float( - string='Extra Price', - help='Reserved for future pricing rules. Not applied automatically.', - ) - - pos_available = fields.Boolean(string='Available in POS', default=True) - company_id = fields.Many2one( - 'res.company', string='Company', - default=lambda self: self.env.company, index=True, - ) - - is_delivery_related = fields.Boolean( - string='Delivery Related', - help='Selecting this attribute marks the order as delivery and ' - 'triggers the delivery-details prompt.', - ) - is_priority_related = fields.Boolean( - string='Priority Related', - help='Selecting this attribute promotes the order to urgent priority.', - ) - - @api.model - def _load_pos_data_domain(self, data, config): - return [ - ('pos_available', '=', True), - ('active', '=', True), - '|', - ('company_id', '=', False), - ('company_id', 'in', config.company_id.ids), - ] - - @api.model - def _load_pos_data_fields(self, config): - return [ - 'id', 'name', 'code', 'sequence', - 'color', 'description', - 'extra_price', - 'is_delivery_related', 'is_priority_related', - ] diff --git a/addons/laundry_management/models/laundry_order_line.py b/addons/laundry_management/models/laundry_order_line.py deleted file mode 100644 index 5495911..0000000 --- a/addons/laundry_management/models/laundry_order_line.py +++ /dev/null @@ -1,288 +0,0 @@ -import logging - -from odoo import models, fields, api, _ -from odoo.exceptions import UserError - -from .laundry_order import POS_SYNC_CTX - -_logger = logging.getLogger(__name__) - - -LINE_STATES = [ - ('received', 'Received'), - ('processing', 'Processing'), - ('ready', 'Ready'), - ('delivered', 'Delivered'), -] - -# Line fields the lock protects. NOT included on purpose: -# - state (per-item workflow advance is always allowed) -# - customer_note (operator commentary) -# - tracking_code (auto-assigned; cannot change after create either way) -LOCKED_LINE_FIELDS = frozenset({ - 'product_id', 'description', 'qty', 'price_unit', -}) - - -class LaundryOrderLine(models.Model): - """Line item on a laundry order — maps from pos.order.line. - - Each line carries a unique scannable tracking_code (barcode) and its - own per-item workflow state. The order-level state on laundry.order - remains the source of truth for financial gates; the per-line state - is an operational overlay that supports items moving through the - workflow at different speeds. - """ - _name = 'laundry.order.line' - _description = 'Laundry Order Line' - _order = 'order_id, id' - - order_id = fields.Many2one( - 'laundry.order', string='Order', - required=True, ondelete='cascade', index=True, - ) - # Mirror order-level partner/state so list + kanban views can filter/group - # without costly cross-model joins. - order_partner_id = fields.Many2one( - related='order_id.partner_id', store=True, index=True, readonly=True, - ) - order_state = fields.Selection( - related='order_id.state', store=True, index=True, readonly=True, - ) - product_id = fields.Many2one( - 'product.product', string='Product', - ) - description = fields.Char( - string='Description', - ) - qty = fields.Float( - string='Quantity', - default=1.0, digits=(10, 2), - ) - price_unit = fields.Float( - string='Unit Price', - digits=(10, 2), readonly=True, - ) - customer_note = fields.Char( - string='Customer Note', - ) - subtotal = fields.Float( - string='Subtotal', - compute='_compute_subtotal', - store=True, digits=(10, 2), - ) - - # -- Per-item tracking -- - tracking_code = fields.Char( - string='Tracking Code', - copy=False, readonly=True, index=True, - help='Unique scannable barcode for this item.', - ) - state = fields.Selection( - LINE_STATES, - string='Item Status', - default='received', - required=True, copy=False, index=True, - ) - - _tracking_code_uniq = models.Constraint( - 'UNIQUE(tracking_code)', - 'Tracking code must be unique across all laundry items.', - ) - - @api.depends('qty', 'price_unit') - def _compute_subtotal(self): - for line in self: - line.subtotal = line.qty * line.price_unit - - # Sequence code for the auto-generated tracking_code (barcode). - _TRACKING_SEQ_CODE = 'laundry.order.line.tracking' - - @api.model - def _next_tracking_code(self): - """Allocate a tracking_code that is GUARANTEED unique across the - existing laundry_order_line table. - - Why this exists - ─────────────── - Postgres sequences are NON-transactional: a `nextval()` advances - the sequence even when the surrounding ORM transaction rolls - back. Repeated POS validates that fail (lock, missing partner, - anything) eat sequence values without consuming them in real - rows. Conversely, a partial reseed / data import that inserts - rows with manual tracking_codes leaves the sequence BEHIND the - table's MAX. Either way, `next_by_code()` can return a code - that already exists → UniqueViolation → POS sale silently - misses its laundry-order link (the savepoint in pos_order.py - catches the SQL error to protect the POS commit). - - How this fixes it - ───────────────── - On collision, repair the sequence to (max_tracking_num + 1) - using a direct SQL nextval-skip, then ask the sequence again. - Capped at a few attempts so a real bug (e.g. malformed schema) - still surfaces instead of looping forever. - """ - seq = self.env['ir.sequence'] - for attempt in range(5): - code = seq.next_by_code(self._TRACKING_SEQ_CODE) or False - if not code: - return False - # Cheap collision check; the underlying UNIQUE constraint is - # the real safety net — this just avoids paying the round-trip. - existing = self.sudo().search_count([('tracking_code', '=', code)]) - if not existing: - return code - # Collision — repair sequence past the current MAX, then retry. - self._repair_tracking_sequence() - _logger.warning( - "laundry.order.line tracking sequence collided on %s " - "(attempt %d); repaired and retrying.", code, attempt + 1, - ) - # If we still can't get a unique code after 5 tries, surface the - # problem instead of writing an empty code. - raise UserError(_( - 'Could not allocate a unique tracking code after 5 attempts. ' - 'Check the laundry_management sequence configuration.' - )) - - @api.model - def _repair_tracking_sequence(self): - """Advance the tracking-code sequence past the actual MAX in the - table. Idempotent — safe to call repeatedly. SQL-level so it - works even when the ORM env context is unusual (sudo, sync hook). - """ - self.env.cr.execute(""" - SELECT COALESCE(MAX( - CAST(NULLIF(REGEXP_REPLACE(tracking_code, '[^0-9]', '', 'g'), '') - AS INTEGER) - ), 0) - FROM laundry_order_line - WHERE tracking_code IS NOT NULL; - """) - max_existing = self.env.cr.fetchone()[0] or 0 - # `ir.sequence` writes update number_next; we use the API for - # safety (handles ranges, prefixes, padding). - seq = self.env['ir.sequence'].sudo().search( - [('code', '=', self._TRACKING_SEQ_CODE)], limit=1, - ) - if seq: - seq.write({'number_next': max_existing + 1}) - - @api.model_create_multi - def create(self, vals_list): - # POS-sync bypass FIRST — POS creates the order and its lines in a - # single create_vals payload; both must sail through the guard. - pos_sync = bool(self.env.context.get(POS_SYNC_CTX)) - if not pos_sync: - order_ids = {v.get('order_id') for v in vals_list if v.get('order_id')} - if order_ids: - orders = self.env['laundry.order'].browse(list(order_ids)) - for order in orders: - if order.locked: - raise UserError(_( - 'Cannot add a line to locked order "%s".', - order.name, - )) - for vals in vals_list: - if not vals.get('tracking_code'): - vals['tracking_code'] = self._next_tracking_code() - if _logger.isEnabledFor(logging.DEBUG): - _logger.debug( - 'laundry.order.line.create pos_sync=%s count=%d', - pos_sync, len(vals_list), - ) - return super().create(vals_list) - - def write(self, vals): - if self.env.context.get(POS_SYNC_CTX): - if _logger.isEnabledFor(logging.DEBUG): - _logger.debug( - 'laundry.order.line.write BYPASS ids=%s keys=%s', - self.ids, list(vals.keys()), - ) - return super().write(vals) - protected = LOCKED_LINE_FIELDS.intersection(vals.keys()) - if protected: - for line in self: - if line.order_id.locked: - raise UserError(_( - 'Line on locked order "%(order)s" cannot edit ' - '%(fields)s. Ask a manager to use "Unlock for ' - 'Editing" first.', - order=line.order_id.name, - fields=', '.join(sorted(protected)), - )) - return super().write(vals) - - def unlink(self): - # No bypass: deletion is never issued by the POS sync path. - for line in self: - if line.order_id.locked: - raise UserError(_( - 'Cannot delete a line from locked order "%s".', - line.order_id.name, - )) - return super().unlink() - - # -- Per-line workflow actions (1-click) -- - def action_line_process(self): - for line in self: - if line.state != 'received': - raise UserError(_( - 'Item %(code)s is not in Received state.', - code=line.tracking_code or line.id, - )) - line.state = 'processing' - - def action_line_ready(self): - for line in self: - if line.state != 'processing': - raise UserError(_( - 'Item %(code)s is not in Processing state.', - code=line.tracking_code or line.id, - )) - line.state = 'ready' - - def action_line_deliver(self): - for line in self: - if line.state != 'ready': - raise UserError(_( - 'Item %(code)s must be Ready before delivery.', - code=line.tracking_code or line.id, - )) - line.state = 'delivered' - - @api.model - def action_scan_advance(self, tracking_code): - """Advance an item one stage by its scanned tracking code. - - Intended for barcode scanner workflow: scanner types the code, - this method finds the line and bumps its state to the next stage. - Returns the new state or raises UserError if terminal / unknown. - """ - if not tracking_code: - raise UserError(_('Scan a tracking code.')) - line = self.search([('tracking_code', '=', tracking_code.strip())], limit=1) - if not line: - raise UserError(_('No item with tracking code %s.', tracking_code)) - transitions = { - 'received': line.action_line_process, - 'processing': line.action_line_ready, - 'ready': line.action_line_deliver, - } - action = transitions.get(line.state) - if not action: - raise UserError(_( - 'Item %(code)s is already %(state)s.', - code=line.tracking_code, state=line.state, - )) - action() - return { - 'id': line.id, - 'tracking_code': line.tracking_code, - 'state': line.state, - 'order_name': line.order_id.name, - 'partner_name': line.order_partner_id.name, - 'product_name': line.product_id.display_name, - } diff --git a/addons/laundry_management/models/laundry_order_line_addon.py b/addons/laundry_management/models/laundry_order_line_addon.py deleted file mode 100644 index d8ee954..0000000 --- a/addons/laundry_management/models/laundry_order_line_addon.py +++ /dev/null @@ -1,53 +0,0 @@ -from odoo import models, fields, api - - -class LaundryOrderLineAddon(models.Model): - """Optional add-on service attached to one sale.order.line. - - Examples per item: - - Express handling (+10 SAR) - - Starch / تقطير (+3 SAR) - - Packaging (+2 SAR) - - Perfume scent (+5 SAR) - - The parent line's subtotal does NOT automatically include add-on prices - (sale.order.line controls its own subtotal). Add-ons are tracked here for - printing and reporting purposes; staff can create a separate order line for - the add-on product if billing is required. - """ - _name = 'laundry.order.line.addon' - _description = 'Order Line Add-on' - _order = 'line_id, id' - - line_id = fields.Many2one( - 'sale.order.line', string='Order Line', - required=True, ondelete='cascade', index=True, - ) - # Denormal for easy reporting — derived from sale.order.line.order_id - order_id = fields.Many2one( - 'sale.order', - related='line_id.order_id', store=True, index=True, - ) - - name = fields.Char( - string='Add-on / الإضافة', - required=True, - help='Name of the additional service (e.g. Express, Starch, Packaging).', - ) - price = fields.Float( - string='Price / السعر', - required=True, digits=(10, 2), default=0.0, - ) - quantity = fields.Float( - string='Qty', - default=1.0, digits=(10, 2), - ) - subtotal = fields.Float( - string='Subtotal', - compute='_compute_subtotal', store=True, digits=(10, 2), - ) - - @api.depends('price', 'quantity') - def _compute_subtotal(self): - for addon in self: - addon.subtotal = addon.price * addon.quantity diff --git a/addons/laundry_management/models/laundry_order_type.py b/addons/laundry_management/models/laundry_order_type.py deleted file mode 100644 index 0828909..0000000 --- a/addons/laundry_management/models/laundry_order_type.py +++ /dev/null @@ -1,75 +0,0 @@ -from odoo import models, fields, api - - -class LaundryOrderType(models.Model): - """Main intake type for a laundry order (Standard / Express / Delivery / VIP ...). - - Drives workflow hints (priority, is_delivery) and suggests default - attributes. Admin-configurable from the backend; exposed to POS via - _load_pos_data_* so cashiers see live options in a popup. - """ - _name = 'laundry.order.type' - _inherit = ['pos.load.mixin'] - _description = 'Laundry Order Type' - _order = 'sequence, id' - - name = fields.Char(string='Name', required=True, translate=True) - code = fields.Char(string='Code') - sequence = fields.Integer(default=10) - active = fields.Boolean(default=True) - - priority = fields.Selection([ - ('normal', 'Normal'), - ('urgent', 'Urgent'), - ], string='Priority', default='normal', required=True) - - is_delivery = fields.Boolean(string='Is Delivery') - requires_address = fields.Boolean(string='Requires Address') - requires_scheduled_time = fields.Boolean(string='Requires Scheduled Time') - - color = fields.Char(string='Color', help='Hex color, e.g. #FF8800') - icon_image = fields.Binary(string='Icon') - description = fields.Text(string='Description', translate=True) - - extra_price = fields.Float( - string='Extra Price', - help='Reserved for future pricing rules. Not applied automatically.', - ) - - pos_available = fields.Boolean( - string='Available in POS', default=True, - help='Uncheck to hide this type from the POS popup without archiving.', - ) - company_id = fields.Many2one( - 'res.company', string='Company', - default=lambda self: self.env.company, index=True, - ) - - attribute_ids = fields.Many2many( - 'laundry.order.attribute', - 'laundry_order_type_attribute_rel', - 'type_id', 'attribute_id', - string='Default Attributes', - help='Attributes pre-suggested (pre-checked) when this type is chosen.', - ) - - @api.model - def _load_pos_data_domain(self, data, config): - return [ - ('pos_available', '=', True), - ('active', '=', True), - '|', - ('company_id', '=', False), - ('company_id', 'in', config.company_id.ids), - ] - - @api.model - def _load_pos_data_fields(self, config): - return [ - 'id', 'name', 'code', 'sequence', - 'priority', 'is_delivery', - 'requires_address', 'requires_scheduled_time', - 'color', 'description', - 'extra_price', - 'attribute_ids', - ] diff --git a/addons/laundry_management/models/laundry_payment_method.py b/addons/laundry_management/models/laundry_payment_method.py deleted file mode 100644 index f199212..0000000 --- a/addons/laundry_management/models/laundry_payment_method.py +++ /dev/null @@ -1,101 +0,0 @@ -from odoo import models, fields, api -from odoo.exceptions import UserError - - -class LaundryPaymentMethod(models.Model): - """Configurable payment method linked to an accounting journal. - - One record per payment option (e.g. Cash, Visa, Bank Transfer, Credit). - Each method is typed as cash / bank / credit so that session cash-control - and accounting routing work exactly like Odoo POS payment methods. - - cash → counted in the session cash drawer - bank → posted to a bank/card journal, not counted in cash - credit → deferred / no-posting (customer owes) - """ - _name = 'laundry.payment.method' - _description = 'Laundry Payment Method' - _order = 'sequence, id' - - # ── Identity ────────────────────────────────────────────────────── - name = fields.Char( - string='Method Name', - required=True, - translate=True, - ) - sequence = fields.Integer(default=10) - active = fields.Boolean(default=True) - company_id = fields.Many2one( - 'res.company', - default=lambda self: self.env.company, - ) - - # ── Type ────────────────────────────────────────────────────────── - payment_type = fields.Selection([ - ('cash', 'Cash / نقد'), - ('bank', 'Bank / Card / بنك'), - ('credit', 'Credit / Deferred / آجل'), - ], string='Type', required=True, default='cash', - help=( - 'cash → counts in session cash drawer\n' - 'bank → posted to bank/card journal\n' - 'credit → deferred, no immediate accounting entry' - ), - ) - - # ── Journal ─────────────────────────────────────────────────────── - journal_id = fields.Many2one( - 'account.journal', string='Accounting Journal', - domain="[('type', 'in', ['cash', 'bank']), ('company_id', '=', company_id)]", - help='Leave blank only for Credit/Deferred methods.', - ) - - # ── UI ──────────────────────────────────────────────────────────── - is_default = fields.Boolean( - string='Default', - help='Pre-selected when staff opens the Register Payment wizard.', - ) - - # ── Constraints ─────────────────────────────────────────────────── - @api.constrains('is_default', 'company_id') - def _check_single_default(self): - for rec in self.filtered('is_default'): - duplicate = self.search([ - ('is_default', '=', True), - ('company_id', '=', rec.company_id.id), - ('id', '!=', rec.id), - ], limit=1) - if duplicate: - raise UserError( - f'Only one default payment method is allowed per company.\n' - f'"{duplicate.name}" is already the default.\n' - 'Unset it first before marking this one as default.' - ) - - @api.constrains('payment_type', 'journal_id') - def _check_journal_required(self): - for rec in self: - if rec.payment_type in ('cash', 'bank') and not rec.journal_id: - raise UserError( - f'Payment method "{rec.name}" is of type "{rec.payment_type}" ' - 'and requires an accounting journal. ' - 'Please select a journal or change the type to Credit/Deferred.' - ) - - # ── Helpers ─────────────────────────────────────────────────────── - @api.model - def get_default_method(self): - """Return the default payment method for the current company.""" - company = self.env.company - return ( - self.search([ - ('is_default', '=', True), - ('company_id', '=', company.id), - ('active', '=', True), - ], limit=1) - or self.search([ - ('payment_type', '=', 'cash'), - ('company_id', '=', company.id), - ('active', '=', True), - ], limit=1) - ) diff --git a/addons/laundry_management/models/laundry_session.py b/addons/laundry_management/models/laundry_session.py deleted file mode 100644 index 5d9ccc5..0000000 --- a/addons/laundry_management/models/laundry_session.py +++ /dev/null @@ -1,259 +0,0 @@ -from odoo import models, fields, api -from odoo.exceptions import UserError - - -class LaundrySession(models.Model): - """Daily operational session — POS-style cash management. - - A session groups all laundry orders placed in a single working shift. - Cash totals are derived from standard account.payment records linked - to the session's invoices, so figures are always in sync with Accounting. - - Opening balance: - - When opening a new session, the system suggests the previous - session's actual_closing_cash as the opening float (carry-forward). - - Closing: - - Staff enter the actual physical cash count. - - If a difference exists, a journal entry is posted (optional). - """ - _name = 'laundry.session' - _description = 'Laundry Daily Session' - _inherit = ['mail.thread'] - _order = 'opening_datetime desc, id desc' - - # ── Identity ────────────────────────────────────────────────────── - name = fields.Char( - string='Session', - required=True, copy=False, readonly=True, - default='New', tracking=True, - ) - user_id = fields.Many2one( - 'res.users', string='Opened By', - required=True, default=lambda self: self.env.user, - tracking=True, - ) - company_id = fields.Many2one( - 'res.company', - default=lambda self: self.env.company, - ) - - # ── State ───────────────────────────────────────────────────────── - state = fields.Selection([ - ('new', 'New'), - ('opened', 'Open'), - ('closed', 'Closed'), - ], default='new', required=True, tracking=True, copy=False) - - # ── Timing ──────────────────────────────────────────────────────── - opening_datetime = fields.Datetime(string='Opened At', readonly=True) - closing_datetime = fields.Datetime(string='Closed At', readonly=True) - - # ── Cash Control ────────────────────────────────────────────────── - opening_cash = fields.Float( - string='Opening Float', - digits=(10, 2), - help='Cash in the drawer at the start of the session.', - ) - actual_closing_cash = fields.Float( - string='Actual Cash Count', - digits=(10, 2), - help='Physical cash counted at session close.', - ) - expected_closing_cash = fields.Float( - string='Expected Cash', - compute='_compute_cash_control', store=True, digits=(10, 2), - ) - cash_difference = fields.Float( - string='Difference', - compute='_compute_cash_control', store=True, digits=(10, 2), - help='Actual − Expected. Negative = short.', - ) - - # ── Orders (sale.order with session_id = self) ──────────────────── - order_ids = fields.One2many( - 'sale.order', 'session_id', string='Orders', - ) - order_count = fields.Integer( - compute='_compute_session_totals', store=True, - ) - total_sales = fields.Float( - string='Total Sales', - compute='_compute_session_totals', store=True, digits=(10, 2), - ) - - # ── Payment totals (from account.payment via invoices) ──────────── - total_cash = fields.Float( - string='Cash', - compute='_compute_session_totals', store=True, digits=(10, 2), - ) - total_bank = fields.Float( - string='Bank / Card', - compute='_compute_session_totals', store=True, digits=(10, 2), - ) - total_paid = fields.Float( - string='Total Collected', - compute='_compute_session_totals', store=True, digits=(10, 2), - ) - total_credit = fields.Float( - string='Outstanding / Deferred', - compute='_compute_session_totals', store=True, digits=(10, 2), - help='Total not yet paid (total_sales − total_paid).', - ) - outstanding_amount = fields.Float( - string='Outstanding', - compute='_compute_session_totals', store=True, digits=(10, 2), - ) - - notes = fields.Text(string='Notes') - - # ── Difference account ──────────────────────────────────────────── - difference_account_id = fields.Many2one( - 'account.account', - string='Cash Difference Account', - help='Account used to post cash variances at session close.', - ) - - # ── Constraints ─────────────────────────────────────────────────── - _session_name_uniq = models.Constraint( - 'UNIQUE(name)', - 'Session name must be unique.', - ) - - @api.constrains('state', 'company_id') - def _check_single_open_session(self): - for session in self: - if session.state == 'opened': - duplicate = self.search([ - ('state', '=', 'opened'), - ('company_id', '=', session.company_id.id), - ('id', '!=', session.id), - ], limit=1) - if duplicate: - raise UserError( - f'Session "{duplicate.name}" is already open.\n' - 'Close it before opening a new session.' - ) - - # ── Computed ────────────────────────────────────────────────────── - @api.depends( - 'order_ids.amount_total', - 'order_ids.state', - 'order_ids.invoice_ids.state', - 'order_ids.invoice_ids.payment_state', - 'order_ids.invoice_ids.amount_residual', - 'order_ids.is_laundry_order', - ) - def _compute_session_totals(self): - Payment = self.env['account.payment'] - for session in self: - active = session.order_ids.filtered( - lambda o: o.is_laundry_order and o.state not in ('cancel', 'draft') - ) - session.order_count = len(active) - session.total_sales = sum(active.mapped('amount_total')) - - # Get posted customer invoices for active orders - invoices = active.mapped('invoice_ids').filtered( - lambda inv: inv.state == 'posted' and inv.move_type == 'out_invoice' - ) - - if not invoices: - session.total_cash = 0.0 - session.total_bank = 0.0 - session.total_paid = 0.0 - session.outstanding_amount = session.total_sales - session.total_credit = session.total_sales - continue - - # Payments reconciled against those invoices - # account.payment.reconciled_invoice_ids is a computed M2M in Odoo 16+ - payments = Payment.search([ - ('reconciled_invoice_ids', 'in', invoices.ids), - ('state', '=', 'posted'), - ('payment_type', '=', 'inbound'), - ]) - - cash_pmts = payments.filtered(lambda p: p.journal_id.type == 'cash') - bank_pmts = payments.filtered(lambda p: p.journal_id.type == 'bank') - - session.total_cash = sum(cash_pmts.mapped('amount')) - session.total_bank = sum(bank_pmts.mapped('amount')) - session.total_paid = sum(payments.mapped('amount')) - outstanding = max(session.total_sales - session.total_paid, 0.0) - session.outstanding_amount = outstanding - session.total_credit = outstanding - - @api.depends('opening_cash', 'total_cash', 'actual_closing_cash', 'state') - def _compute_cash_control(self): - for session in self: - session.expected_closing_cash = ( - session.opening_cash + session.total_cash - ) - session.cash_difference = ( - session.actual_closing_cash - session.expected_closing_cash - if session.state == 'closed' else 0.0 - ) - - # ── ORM ─────────────────────────────────────────────────────────── - @api.model_create_multi - def create(self, vals_list): - for vals in vals_list: - if vals.get('name', 'New') == 'New': - vals['name'] = ( - self.env['ir.sequence'].next_by_code('laundry.session') - or 'New' - ) - return super().create(vals_list) - - # ── Workflow ────────────────────────────────────────────────────── - def action_open_session(self): - for session in self: - if session.state != 'new': - raise UserError('This session is already open or closed.') - # Carry forward: suggest previous session's actual closing cash - if not session.opening_cash: - prev = self.search([ - ('state', '=', 'closed'), - ('company_id', '=', session.company_id.id), - ], order='closing_datetime desc', limit=1) - if prev and prev.actual_closing_cash: - session.opening_cash = prev.actual_closing_cash - session.write({ - 'state': 'opened', - 'opening_datetime': fields.Datetime.now(), - }) - session.message_post( - body=f'Session opened by {self.env.user.name}. ' - f'Opening float: {session.opening_cash:.2f}' - ) - - def action_close_session(self): - self.ensure_one() - if self.state != 'opened': - raise UserError('Only open sessions can be closed.') - return { - 'name': 'Close Session', - 'type': 'ir.actions.act_window', - 'res_model': 'laundry.session.close.wizard', - 'view_mode': 'form', - 'target': 'new', - 'context': {'default_session_id': self.id}, - } - - def action_view_orders(self): - self.ensure_one() - return { - 'name': f'Orders — {self.name}', - 'type': 'ir.actions.act_window', - 'res_model': 'sale.order', - 'view_mode': 'list,form', - 'domain': [('session_id', '=', self.id), ('is_laundry_order', '=', True)], - 'context': {'default_session_id': self.id, 'default_is_laundry_order': True}, - } - - def action_print_session_report(self): - self.ensure_one() - return self.env.ref( - 'laundry_management.action_report_laundry_session' - ).report_action(self) diff --git a/addons/laundry_management/models/laundry_settings.py b/addons/laundry_management/models/laundry_settings.py deleted file mode 100644 index 0b092bd..0000000 --- a/addons/laundry_management/models/laundry_settings.py +++ /dev/null @@ -1,137 +0,0 @@ -from odoo import models, fields, api - - -class LaundrySettings(models.TransientModel): - """Laundry Management settings panel (PART 9). - - Extends res.config.settings to add laundry-specific configuration: - - WhatsApp Business API credentials and behaviour - - Commission tracking rates, rules, and default account - - Session enforcement and default cash journal - - Print format defaults - - Cash control: default difference account - """ - _inherit = 'res.config.settings' - - # ── WhatsApp ────────────────────────────────────────────────────── - laundry_wa_token = fields.Char( - string='WhatsApp Cloud API Token', - config_parameter='laundry.whatsapp_api_token', - ) - laundry_wa_phone_id = fields.Char( - string='Phone Number ID', - config_parameter='laundry.whatsapp_phone_id', - ) - laundry_wa_store_number = fields.Char( - string='Store WhatsApp Number', - config_parameter='laundry.whatsapp_store_number', - help='E.164 format without + (e.g. 966501234567). Used as fallback number.', - ) - laundry_wa_auto_send = fields.Boolean( - string='Auto-send WhatsApp on Order Confirm', - ) - - # ── Commission ──────────────────────────────────────────────────── - laundry_commission_enabled = fields.Boolean( - string='Enable Staff Commission Tracking', - ) - laundry_commission_type = fields.Selection([ - ('percentage', 'Percentage of Order Total (%)'), - ('fixed', 'Fixed Amount per Order'), - ], string='Commission Type') - laundry_commission_reception_rate = fields.Float( - string='Reception Commission', digits=(10, 2), - ) - laundry_commission_processing_rate = fields.Float( - string='Processing Commission', digits=(10, 2), - ) - laundry_commission_delivery_rate = fields.Float( - string='Delivery Commission', digits=(10, 2), - ) - laundry_commission_account_id = fields.Many2one( - 'account.account', - string='Commission Expense Account', - domain="[('account_type', 'in', ['expense', 'expense_direct_cost']), ('company_ids', 'in', [company_id])]", - help='Account for booking commission expenses (for future accounting integration).', - ) - - # ── Session / Cash Control ──────────────────────────────────────── - laundry_require_session = fields.Boolean( - string='Require Open Session to Confirm Orders', - ) - laundry_cash_journal_id = fields.Many2one( - 'account.journal', - string='Default Cash Journal', - domain="[('type', '=', 'cash'), ('company_id', '=', company_id)]", - help='Default cash journal used in the payment wizard.', - ) - laundry_difference_account_id = fields.Many2one( - 'account.account', - string='Cash Difference Account', - domain="[('company_ids', 'in', [company_id])]", - help='Default account for posting session cash count variances.', - ) - - # ── Print ───────────────────────────────────────────────────────── - laundry_default_paper = fields.Selection([ - ('a4', 'A4 Full Receipt'), - ('thermal', 'Thermal / 80mm Roll'), - ], string='Default Print Format') - - # company_id helper for domain filtering - company_id = fields.Many2one( - 'res.company', string='Company', - default=lambda self: self.env.company, - readonly=True, - ) - - # ── Load & Save ─────────────────────────────────────────────────── - def get_values(self): - res = super().get_values() - ICP = self.env['ir.config_parameter'].sudo() - - def _bool(key, default='False'): - return ICP.get_param(key, default) == 'True' - - def _float(key, default='0.0'): - try: - return float(ICP.get_param(key, default)) - except (ValueError, TypeError): - return 0.0 - - def _int_record(key): - val = ICP.get_param(key, '') - try: - return int(val) if val else False - except (ValueError, TypeError): - return False - - res.update({ - 'laundry_wa_auto_send': _bool('laundry_management.wa_auto_send'), - 'laundry_commission_enabled': _bool('laundry_management.commission_enabled'), - 'laundry_commission_type': ICP.get_param('laundry_management.commission_type', 'percentage'), - 'laundry_commission_reception_rate': _float('laundry_management.commission_reception_rate'), - 'laundry_commission_processing_rate': _float('laundry_management.commission_processing_rate'), - 'laundry_commission_delivery_rate': _float('laundry_management.commission_delivery_rate'), - 'laundry_commission_account_id': _int_record('laundry_management.commission_account_id'), - 'laundry_require_session': _bool('laundry_management.require_session'), - 'laundry_cash_journal_id': _int_record('laundry_management.cash_journal_id'), - 'laundry_difference_account_id': _int_record('laundry_management.difference_account_id'), - 'laundry_default_paper': ICP.get_param('laundry_management.default_paper', 'a4'), - }) - return res - - def set_values(self): - super().set_values() - ICP = self.env['ir.config_parameter'].sudo() - ICP.set_param('laundry_management.wa_auto_send', str(self.laundry_wa_auto_send)) - ICP.set_param('laundry_management.commission_enabled', str(self.laundry_commission_enabled)) - ICP.set_param('laundry_management.commission_type', self.laundry_commission_type or 'percentage') - ICP.set_param('laundry_management.commission_reception_rate', str(self.laundry_commission_reception_rate)) - ICP.set_param('laundry_management.commission_processing_rate', str(self.laundry_commission_processing_rate)) - ICP.set_param('laundry_management.commission_delivery_rate', str(self.laundry_commission_delivery_rate)) - ICP.set_param('laundry_management.commission_account_id', str(self.laundry_commission_account_id.id or '')) - ICP.set_param('laundry_management.require_session', str(self.laundry_require_session)) - ICP.set_param('laundry_management.cash_journal_id', str(self.laundry_cash_journal_id.id or '')) - ICP.set_param('laundry_management.difference_account_id', str(self.laundry_difference_account_id.id or '')) - ICP.set_param('laundry_management.default_paper', self.laundry_default_paper or 'a4') diff --git a/addons/laundry_management/models/pos_config_ext.py b/addons/laundry_management/models/pos_config_ext.py deleted file mode 100644 index 63efd6a..0000000 --- a/addons/laundry_management/models/pos_config_ext.py +++ /dev/null @@ -1,54 +0,0 @@ -from odoo import models, fields - - -class PosConfigLaundryExt(models.Model): - """Laundry POS settings — dedicated section in POS configuration. - - All defaults are intentionally conservative: feature OFF until the - operator opts in. When `enable_laundry_order_type` is False the - popups are skipped entirely — existing POS flow is unchanged. - """ - _inherit = 'pos.config' - - # ── Order-type / attribute / delivery flow ──────────────────────── - enable_laundry_order_type = fields.Boolean( - string='Ask Laundry Order Type in POS', - default=False, - ) - require_laundry_order_type = fields.Boolean( - string='Order Type Required', - default=False, - ) - ask_laundry_order_type_on_first_line = fields.Boolean( - string='Ask on First Laundry Line', - default=True, - ) - allow_change_laundry_order_type_before_payment = fields.Boolean( - string='Allow Change Before Payment', - default=True, - ) - default_laundry_order_type_id = fields.Many2one( - 'laundry.order.type', - string='Default Order Type', - ) - enable_laundry_attributes = fields.Boolean( - string='Enable Order Attributes', - default=True, - ) - require_delivery_details_if_needed = fields.Boolean( - string='Require Delivery Details', - default=True, - help='Prompt for delivery address / scheduled time when the selected ' - 'type or attributes flag this as a delivery order.', - ) - require_delivery_time = fields.Boolean( - string='Require Delivery Time', - default=False, - help='When enabled, the delivery details popup enforces a scheduled ' - 'time before it can be confirmed. When disabled, cashiers may ' - 'skip the time field and the order is saved without one.', - ) - show_order_type_icons = fields.Boolean( - string='Show Icons in POS', - default=True, - ) diff --git a/addons/laundry_management/models/pos_order.py b/addons/laundry_management/models/pos_order.py deleted file mode 100644 index c6a9594..0000000 --- a/addons/laundry_management/models/pos_order.py +++ /dev/null @@ -1,411 +0,0 @@ -import logging -from odoo import models, fields, api - -_logger = logging.getLogger(__name__) - - -class PosOrderLaundryExt(models.Model): - """Extend pos.order to: - 1. Auto-create laundry.order for sales containing laundry products. - 2. Split POS payments into real cash vs deferred (Customer Account). - - Settlement is NO LONGER a POS order. It is a pure account.payment - invoked via res.partner.settle_laundry_dues_rpc — see res_partner.py. - - The `is_laundry_settlement` field and the old settlement-product path - are preserved in schema form for historical compatibility with DB rows - created before this refactor, but the sync hook no longer creates or - processes settlement POS orders. - """ - _inherit = 'pos.order' - - laundry_order_id = fields.Many2one( - 'laundry.order', string='Laundry Order', - readonly=True, copy=False, index=True, - ) - is_laundry_settlement = fields.Boolean( - string='Laundry Settlement (legacy)', - readonly=True, copy=False, default=False, - help='Legacy flag from the old settlement-product flow. ' - 'New settlements go through account.payment directly and do ' - 'not create POS orders.', - ) - - # -- Order type / attributes / delivery (set in POS UI) -- - laundry_order_type_id = fields.Many2one( - 'laundry.order.type', string='Laundry Order Type', - index=True, copy=False, - ) - laundry_order_attribute_ids = fields.Many2many( - 'laundry.order.attribute', - 'pos_order_laundry_attribute_rel', - 'pos_order_id', 'attribute_id', - string='Laundry Attributes', - copy=False, - ) - laundry_is_delivery = fields.Boolean( - string='Laundry Delivery', copy=False, - ) - laundry_delivery_address = fields.Text( - string='Laundry Delivery Address', copy=False, - ) - laundry_delivery_scheduled_at = fields.Datetime( - string='Laundry Delivery Scheduled At', copy=False, - ) - - # NOTE: pos.order._load_pos_data_fields is intentionally NOT overridden. - # Bisection proved that adding ANY custom field to this loader breaks - # POS order construction (`lines is undefined` in _computeAllPrices). - # Custom values are sent by serializeForORM (see pos_order_patch.js) - # and written directly by core's _process_order via create(**order), - # since the columns already exist on this model. - - def action_open_laundry_order(self): - self.ensure_one() - if not self.laundry_order_id: - return - return { - 'type': 'ir.actions.act_window', - 'res_model': 'laundry.order', - 'res_id': self.laundry_order_id.id, - 'view_mode': 'form', - 'target': 'current', - } - - # ───────────────────────────────────────────────────────────────────── - # Sync hook - # ───────────────────────────────────────────────────────────────────── - @api.model - def _extract_pos_order_ids(self, sync_result): - """Defensive extraction of pos.order ids from the value returned - by `super().sync_from_ui(...)`. Across Odoo 19 patch levels and - edge paths this value can be: - - - a dict {'pos.order': [{'id': N, ...}, ...], ...} - - a list of integer ids - - a recordset of pos.order (if a downstream module already - normalized it) - - an empty container of any of the above - - Returns a list of integer pos.order ids. Logs unknown shapes - instead of silently dropping them. - """ - if not sync_result: - return [] - # Recordset → use .ids - if isinstance(sync_result, models.BaseModel): - return list(sync_result.ids) - # Dict payload (canonical in mainline Odoo 19) - if isinstance(sync_result, dict): - payload = sync_result.get('pos.order', []) - ids = [] - for entry in payload or []: - if isinstance(entry, dict) and entry.get('id'): - ids.append(entry['id']) - elif isinstance(entry, int): - ids.append(entry) - return ids - # Plain list of ids (some patch levels / community forks) - if isinstance(sync_result, (list, tuple)): - ids = [] - for entry in sync_result: - if isinstance(entry, int): - ids.append(entry) - elif isinstance(entry, dict) and entry.get('id'): - ids.append(entry['id']) - return ids - _logger.warning( - "[laundry] unknown sync_from_ui return shape: %s %r", - type(sync_result).__name__, sync_result, - ) - return [] - - @api.model - def sync_from_ui(self, orders): - # CRITICAL: super() runs the entire POS payment commit. The - # laundry hand-off MUST be additive and side-effect-free if it - # fails — savepoint per pos.order so a SQL-level error cannot - # poison the parent transaction. - result = super().sync_from_ui(orders) - - order_ids = self._extract_pos_order_ids(result) - _logger.info( - "[laundry] sync_from_ui post-process: %d pos.order id(s) extracted", - len(order_ids), - ) - - if not order_ids: - return result - - # `.exists()` filters out any id the caller provided that isn't - # actually in the DB anymore (deleted in a concurrent flow). - # `sudo()` is bounded to this internal hand-off — POS users - # may not hold full create/write rights on laundry.order, but - # the hand-off itself is a server-controlled, validated path. - for order in self.browse(order_ids).exists(): - try: - with self.env.cr.savepoint(): - order.sudo()._maybe_create_laundry_order() - except Exception: - _logger.exception( - "[laundry] Failed to create/sync laundry.order from " - "POS %s (POS sale committed; rolled back laundry-side " - "savepoint only)", order.id, - ) - - return result - - def write(self, vals): - res = super().write(vals) - # Same isolation as above. The amount-sync to laundry is a - # SECONDARY effect of a POS payment write; it must never be the - # reason a payment fails. - if 'amount_paid' in vals or 'payment_ids' in vals or 'amount_total' in vals: - for order in self: - if not order.laundry_order_id: - continue - try: - with self.env.cr.savepoint(): - order._sync_laundry_amounts() - except Exception: - _logger.exception( - 'Failed to sync amounts to laundry order %s ' - 'from POS %s (POS write succeeded; rolled back ' - 'laundry-side savepoint only)', - order.laundry_order_id.id, order.id, - ) - return res - - # ───────────────────────────────────────────────────────────────────── - # Payment classification - # ───────────────────────────────────────────────────────────────────── - def _classify_pos_payments(self): - """Return (cash_total, deferred_total) by inspecting pos.payment rows. - - cash_total = Σ amount where method.split_transactions is False - deferred_total = Σ amount where method.split_transactions is True - """ - self.ensure_one() - cash_total = 0.0 - deferred_total = 0.0 - for pmt in self.payment_ids: - method = pmt.payment_method_id - if method and method.split_transactions: - deferred_total += pmt.amount - else: - cash_total += pmt.amount - return cash_total, deferred_total - - def _sync_laundry_amounts(self): - """Push current financial split to the linked laundry.order. - - amount_total / amount_paid_cash / amount_deferred are all in - LOCKED_HEADER_FIELDS — without the POS-sync context bypass the - lock guard would block this write on every payment edit. - """ - self.ensure_one() - if not self.laundry_order_id: - return - cash, deferred = self._classify_pos_payments() - self.laundry_order_id.sudo().with_context( - laundry_pos_sync=True - ).write({ - 'amount_total': self.amount_total, - 'amount_paid_cash': cash, - 'amount_deferred': deferred, - }) - - # ───────────────────────────────────────────────────────────────────── - # Laundry order creation - # ───────────────────────────────────────────────────────────────────── - def _maybe_create_laundry_order(self): - """Create laundry.order if this POS order contains laundry products.""" - self.ensure_one() - _logger.warning( - "[laundry-trace] _maybe_create_laundry_order POS %s " - "(ref=%s, lines=%d, existing_link=%s)", - self.id, self.pos_reference or '-', - len(self.lines), self.laundry_order_id.id or False, - ) - - if self.laundry_order_id: - _logger.warning( - "[laundry-trace] POS %s already linked to laundry.order %s " - "→ syncing amounts only", self.id, self.laundry_order_id.id, - ) - self._sync_laundry_amounts() - return - - if not self.lines: - _logger.warning("[laundry-trace] POS %s has no lines → skip", self.id) - return - - partner = self.partner_id or self.env.company.partner_id - - laundry_lines = [] - skipped = [] - for line in self.lines: - tmpl = line.product_id.product_tmpl_id if line.product_id else None - if not tmpl: - skipped.append((line.id, 'no template')) - continue - if not tmpl.is_laundry_service: - skipped.append((line.id, f'not laundry ({tmpl.name})')) - continue - laundry_lines.append((0, 0, { - 'product_id': line.product_id.id, - 'description': line.full_product_name or line.product_id.name, - 'qty': line.qty, - 'price_unit': line.price_unit, - 'customer_note': line.customer_note or '', - })) - - _logger.warning( - "[laundry-trace] POS %s line scan: %d laundry, %d skipped %s", - self.id, len(laundry_lines), len(skipped), skipped[:5], - ) - - if not laundry_lines: - _logger.warning( - "[laundry-trace] POS %s has no laundry-service lines → skip " - "(this is the most common cause of 'no laundry order created')", - self.id, - ) - return - - LaundryOrder = self.env['laundry.order'] - existing = LaundryOrder.search( - [('pos_order_id', '=', self.id)], limit=1, - ) - if existing: - self.laundry_order_id = existing.id - self._sync_laundry_amounts() - return - - cash, deferred = self._classify_pos_payments() - create_vals = { - 'pos_order_id': self.id, - 'pos_reference': self.pos_reference or '', - 'partner_id': partner.id, - 'company_id': self.company_id.id, - # Phase 3: every laundry.order born from POS is hard-locked - # by `source_type`. Header lock + line lock both kick in - # immediately. The POS-sync context bypass below lets the - # initial create + line writes go through. - 'source_type': 'pos', - 'amount_total': self.amount_total, - 'amount_paid_cash': cash, - 'amount_deferred': deferred, - 'notes': self.general_customer_note or '', - 'line_ids': laundry_lines, - } - # Propagate order type / attributes / delivery — inference in - # laundry.order.create fills priority_level and is_delivery from - # these when not explicitly provided. - if self.laundry_order_type_id: - create_vals['order_type_id'] = self.laundry_order_type_id.id - if self.laundry_order_attribute_ids: - create_vals['attribute_ids'] = [(6, 0, self.laundry_order_attribute_ids.ids)] - if self.laundry_is_delivery: - create_vals['is_delivery'] = True - if self.laundry_delivery_address: - create_vals['delivery_address'] = self.laundry_delivery_address - if self.laundry_delivery_scheduled_at: - create_vals['delivery_scheduled_at'] = self.laundry_delivery_scheduled_at - - laundry_order = LaundryOrder.with_context( - laundry_pos_sync=True - ).create(create_vals) - self.laundry_order_id = laundry_order.id - - _logger.info( - 'Created laundry order %s from POS %s: total=%.2f, cash=%.2f, deferred=%.2f', - laundry_order.name, self.pos_reference or self.id, - self.amount_total, cash, deferred, - ) - - # ───────────────────────────────────────────────────────────────────── - # Integrity audit — Step 4 of the stabilization brief - # ───────────────────────────────────────────────────────────────────── - @api.model - def audit_laundry_links(self, heal=False): - """Report (and optionally heal) POS orders that should have a - linked laundry.order but don't. - - Returns a dict: - { - 'checked': int, - 'missing': [{pos_order_id, pos_reference, partner_name, - amount_total}, ...], - 'duplicates': [pos_order_id, ...], # pos.order pointing to - # a laundry.order that no - # longer exists - 'healed': int, # populated only if heal=True - } - - A POS order is "should-link" when it has at least one line whose - product_template is flagged is_laundry_service AND is NOT the - settlement product. This is exactly the same condition - _maybe_create_laundry_order applies, so the audit and the live - hand-off agree. - - Heal mode re-runs `_maybe_create_laundry_order` for missing - rows, each in its own savepoint — same isolation guarantee as - the live hand-off. Safe to call repeatedly. - """ - Order = self.env['pos.order'] - all_orders = Order.search([]) - missing = [] - duplicates = [] - for o in all_orders: - has_laundry_line = any( - line.product_id.product_tmpl_id.is_laundry_service - and not line.product_id.product_tmpl_id.is_laundry_settlement - for line in o.lines - if line.product_id and line.product_id.product_tmpl_id - ) - if not has_laundry_line: - continue - if o.laundry_order_id and not o.laundry_order_id.exists(): - # Stored fk to a deleted laundry.order — orphan link - duplicates.append(o.id) - continue - if not o.laundry_order_id: - missing.append({ - 'pos_order_id': o.id, - 'pos_reference': o.pos_reference or '', - 'partner_name': o.partner_id.name if o.partner_id else '', - 'amount_total': o.amount_total, - }) - - healed = 0 - if heal: - for entry in missing: - pos_order = Order.browse(entry['pos_order_id']) - try: - with self.env.cr.savepoint(): - pos_order._maybe_create_laundry_order() - if pos_order.laundry_order_id: - healed += 1 - except Exception: - _logger.exception( - 'audit_laundry_links: heal failed for POS %s', - pos_order.id, - ) - # Clear orphan fk pointers (the linked laundry.order was deleted) - for pos_id in duplicates: - try: - with self.env.cr.savepoint(): - Order.browse(pos_id).laundry_order_id = False - except Exception: - _logger.exception( - 'audit_laundry_links: orphan-clear failed for POS %s', - pos_id, - ) - - return { - 'checked': len(all_orders), - 'missing': missing, - 'duplicates': duplicates, - 'healed': healed, - } diff --git a/addons/laundry_management/models/pos_session_ext.py b/addons/laundry_management/models/pos_session_ext.py deleted file mode 100644 index d4360f6..0000000 --- a/addons/laundry_management/models/pos_session_ext.py +++ /dev/null @@ -1,22 +0,0 @@ -"""POS session extension for laundry. - -Cash settlements use account.bank.statement.line tagged with -pos_session_id, so POS's native _compute_cash_balance picks them up -automatically — no display/math override is required. - -We only override _load_pos_data_models to ship our two new configuration -models (laundry.order.type, laundry.order.attribute) to the POS client. -""" -import logging -from odoo import models - -_logger = logging.getLogger(__name__) - - -class PosSessionLaundryExt(models.Model): - _inherit = 'pos.session' - - def _load_pos_data_models(self, config): - models_to_load = super()._load_pos_data_models(config) - models_to_load += ['laundry.order.type', 'laundry.order.attribute'] - return models_to_load diff --git a/addons/laundry_management/models/product_template_ext.py b/addons/laundry_management/models/product_template_ext.py deleted file mode 100644 index becd530..0000000 --- a/addons/laundry_management/models/product_template_ext.py +++ /dev/null @@ -1,33 +0,0 @@ -from odoo import models, fields, api - - -class ProductTemplateExt(models.Model): - """Extend product.template with the two flags the laundry workflow needs. - - is_laundry_service: marks products as laundry services. POS sales of - these products auto-create / link a laundry.order. - is_laundry_settlement: internal flag for the dedicated settlement - product used to collect outstanding laundry dues without - generating revenue lines. - """ - _inherit = 'product.template' - - is_laundry_service = fields.Boolean( - string='Laundry Service', - default=False, - help='Tick to include this product in the Laundry Service Catalog ' - 'and allow selection on laundry order lines.', - ) - is_laundry_settlement = fields.Boolean( - string='Laundry Settlement', - default=False, - help='Internal flag for the settlement product. ' - 'Do not enable on regular products.', - ) - - @api.model - def _load_pos_data_fields(self, config_id): - fields = super()._load_pos_data_fields(config_id) - fields.append('is_laundry_service') - fields.append('is_laundry_settlement') - return fields diff --git a/addons/laundry_management/models/res_partner.py b/addons/laundry_management/models/res_partner.py deleted file mode 100644 index 4ca74c5..0000000 --- a/addons/laundry_management/models/res_partner.py +++ /dev/null @@ -1,513 +0,0 @@ -import logging -from odoo import models, fields, api, _ -from odoo.exceptions import UserError, ValidationError - -_logger = logging.getLogger(__name__) - - -class ResPartnerLaundry(models.Model): - _inherit = 'res.partner' - - laundry_unpaid_count = fields.Integer( - string='Unpaid Laundry Orders', - compute='_compute_laundry_unpaid_count', - ) - - # Customer defaults — seed values only; cashier can override in POS. - default_laundry_order_type_id = fields.Many2one( - 'laundry.order.type', string='Default Laundry Order Type', - ) - default_laundry_attribute_ids = fields.Many2many( - 'laundry.order.attribute', - 'res_partner_laundry_default_attr_rel', - 'partner_id', 'attribute_id', - string='Default Laundry Attributes', - ) - - def _compute_laundry_unpaid_count(self): - if not self.ids: - self.laundry_unpaid_count = 0 - return - self.env.cr.execute(""" - SELECT partner_id, COUNT(*) - FROM laundry_order - WHERE partner_id IN %s - AND amount_due > 0 - AND state != 'delivered' - GROUP BY partner_id - """, [tuple(self.ids)]) - counts = dict(self.env.cr.fetchall()) - for partner in self: - partner.laundry_unpaid_count = counts.get(partner.id, 0) - - @api.model - def _load_pos_data_fields(self, config): - fields = super()._load_pos_data_fields(config) - fields.append('laundry_unpaid_count') - fields.append('default_laundry_order_type_id') - fields.append('default_laundry_attribute_ids') - return fields - - @api.model - def laundry_find_by_phone(self, phone): - """Search for existing partner by phone. Returns dict or False.""" - phone_clean = phone.strip().replace(' ', '') - if not phone_clean: - return False - domain = [('phone', 'ilike', phone_clean)] - if 'mobile' in self._fields: - domain = ['|', ('phone', 'ilike', phone_clean), ('mobile', 'ilike', phone_clean)] - partner = self.search(domain, limit=1) - if partner: - phone_val = partner.phone or '' - if 'mobile' in self._fields and partner.mobile: - phone_val = phone_val or partner.mobile - return {'id': partner.id, 'name': partner.name, 'phone': phone_val} - return False - - @api.model - def laundry_quick_create(self, vals): - """Create partner from POS quick-create popup. Returns partner id. - - UX contract: - - phone is required (the JS popup enforces this; we also guard). - - name is optional. If empty, the partner name defaults to the - phone number — keeps lists scannable when a cashier doesn't - have time to type a name. - - mobile (when the field exists in this Odoo install) is mirrored - from phone so duplicate-search by phone OR mobile keeps working. - """ - phone = (vals.get('phone') or '').strip() - if not phone: - raise UserError(_('Phone is required to create a customer.')) - name = (vals.get('name') or '').strip() or phone - street = (vals.get('street') or '').strip() or False - create_vals = {'name': name, 'phone': phone, 'street': street} - # `mobile` is provided by the optional phone add-on. Mirror only - # when the field exists, so this stays portable across Odoo - # distributions without bringing in extra deps. - if 'mobile' in self._fields: - create_vals['mobile'] = phone - partner = self.create(create_vals) - return partner.id - - @api.model - def get_laundry_dues(self, partner_id): - """Return outstanding laundry dues for a partner (read-only). - - `amount_due` is now `amount_deferred - amount_settled` — the real - money still owed — not `total - paid`. - """ - orders = self.env['laundry.order'].search([ - ('partner_id', '=', partner_id), - ('amount_due', '>', 0), - ('state', '!=', 'delivered'), - ], order='create_date asc, id asc') - order_details = [] - total_due = 0.0 - for o in orders: - total_due += o.amount_due - services = o.line_ids.mapped('product_id.name') - service_summary = ', '.join(dict.fromkeys(services))[:80] - order_details.append({ - 'id': o.id, - 'name': o.name, - 'amount_due': o.amount_due, - 'amount_total': o.amount_total, - 'amount_paid_cash': o.amount_paid_cash, - 'amount_deferred': o.amount_deferred, - 'amount_settled': o.amount_settled, - 'state': o.state, - 'intake_date': fields.Datetime.to_string(o.create_date) if o.create_date else False, - 'item_count': o.item_count, - 'service_summary': service_summary or '', - }) - return { - 'total_due': total_due, - 'order_count': len(orders), - 'orders': order_details, - } - - # ───────────────────────────────────────────────────────────────────── - # Settlement engine - # CASH → account.bank.statement.line (Dr Cash / Cr Receivable) - # + immediate reconcile vs open AR debits (FIFO) - # → POS expected cash auto-includes it via statement_line_ids - # NON-CASH → account.payment (Dr Bank / Cr Receivable) + reconcile - # → drawer untouched, no duplication risk - # ───────────────────────────────────────────────────────────────────── - @api.model - def settle_laundry_dues_rpc(self, partner_id, payment_lines, pos_session_id=None): - """Collect a laundry receivable via one or more journal entries. - - Per-line routing by journal type — see module docstring above. - - Args: - partner_id: res.partner id. - payment_lines: list of dicts, each with: - - pos_payment_method_id: pos.payment.method id - - amount: float > 0 - pos_session_id: optional pos.session id. For CASH it tags the - statement line so POS includes the amount in expected cash. - For NON-CASH it's stamped on account.payment for audit. - - Returns: - dict with: settled_total, remaining_due, payments[], settled_orders[]. - """ - partner = self.browse(partner_id) - if not partner.exists(): - raise UserError(_('Customer not found.')) - - if not payment_lines or not isinstance(payment_lines, list): - raise UserError(_('At least one payment line is required.')) - - normalized = [] - total = 0.0 - for idx, line in enumerate(payment_lines, start=1): - if not isinstance(line, dict): - raise UserError(_('Payment line %s is malformed.', idx)) - method_id = line.get('pos_payment_method_id') - if not method_id: - raise UserError(_('Payment line %s has no payment method.', idx)) - try: - amt = float(line.get('amount') or 0.0) - except (TypeError, ValueError): - raise UserError(_('Payment line %s has an invalid amount.', idx)) - if amt <= 0: - raise UserError(_('Payment line %s must have a positive amount.', idx)) - journal = self._resolve_settlement_journal(method_id) - normalized.append({ - 'method_id': int(method_id), - 'amount': amt, - 'journal': journal, - }) - total += amt - - session = None - if pos_session_id: - session = self.env['pos.session'].browse(int(pos_session_id)) - if not session.exists(): - session = None - - receipt_entries = [] - for line in normalized: - journal = line['journal'] - method = self.env['pos.payment.method'].browse(line['method_id']) - if journal.type == 'cash': - stmt_line = self._create_cash_settlement_statement_line( - partner, journal, line['amount'], session, - ) - self._reconcile_statement_line_to_receivable(stmt_line, partner) - receipt_entries.append({ - 'id': stmt_line.id, - 'name': stmt_line.move_id.name or stmt_line.payment_ref, - 'state': stmt_line.move_id.state, - 'amount': stmt_line.amount, - 'journal_name': journal.name, - 'journal_type': 'cash', - 'method_name': method.name or journal.name, - }) - else: - pmt = self._create_settlement_account_payment( - partner, journal, line['amount'], line['method_id'], session, - ) - receipt_entries.append({ - 'id': pmt.id, - 'name': pmt.name, - 'state': pmt.state, - 'amount': pmt.amount, - 'journal_name': pmt.journal_id.name, - 'journal_type': pmt.journal_id.type, - 'method_name': pmt.settlement_pos_pm_id.name or pmt.journal_id.name, - }) - - settled_orders, remaining_due = self._distribute_settlement_fifo(partner, total) - - _logger.info( - 'Settlement RPC: partner=%s total=%.2f lines=%d remaining=%.2f', - partner.name, total, len(receipt_entries), remaining_due, - ) - - return { - 'settled_total': total, - 'remaining_due': remaining_due, - 'payments': receipt_entries, - 'settled_orders': settled_orders, - } - - @api.model - def _create_cash_settlement_statement_line(self, partner, journal, amount, session): - """Create a POS-tagged cash-in statement line that posts directly - Dr Cash / Cr Receivable. - - `counterpart_account_id` is consumed by account.bank.statement.line.create - to override the journal's default suspense account — so the credit - leg lands on the partner's receivable account, ready for reconcile. - The statement line auto-posts its move (Odoo handles action_post). - - Tagging `pos_session_id` makes the amount flow into POS's native - expected-cash formula (statement_line_ids sum) → no closing diff. - """ - receivable = partner.property_account_receivable_id - if not receivable: - raise UserError(_( - 'Customer "%s" has no receivable account configured.', - partner.name, - )) - vals = { - 'journal_id': journal.id, - 'amount': amount, - 'partner_id': partner.id, - 'payment_ref': _('Customer Settlement (POS) — %s', partner.name), - 'counterpart_account_id': receivable.id, - 'date': fields.Date.context_today(self), - } - if session: - vals['pos_session_id'] = session.id - try: - return self.env['account.bank.statement.line'].sudo().create(vals) - except Exception as e: - _logger.exception( - 'Failed to create cash settlement statement line for partner %s', - partner.id, - ) - raise UserError(_('Failed to record cash settlement: %s', e)) - - @api.model - def _reconcile_statement_line_to_receivable(self, stmt_line, partner): - """Reconcile the statement line's receivable credit against the - partner's open receivable debits (FIFO). - - Both sides live on the partner's receivable account, so a direct - AML.reconcile() fully settles the customer due in one step. - """ - receivable = partner.property_account_receivable_id - if not receivable: - return - counterpart_credits = stmt_line.move_id.line_ids.filtered( - lambda l: l.account_id == receivable - and l.credit > 0 - and not l.reconciled - ) - if not counterpart_credits: - return - AML = self.env['account.move.line'] - open_debits = AML.search([ - ('partner_id', '=', partner.id), - ('account_id', '=', receivable.id), - ('reconciled', '=', False), - ('parent_state', '=', 'posted'), - ('debit', '>', 0), - ], order='date asc, id asc') - if not open_debits: - return - try: - (counterpart_credits | open_debits).reconcile() - except Exception: - _logger.exception( - 'Reconciliation failed for cash settlement statement line %s', - stmt_line.id, - ) - - @api.model - def _create_settlement_account_payment( - self, partner, journal, amount, method_id, session, - ): - """Non-cash settlement: standard account.payment + reconcile.""" - Payment = self.env['account.payment'] - pmt_vals = { - 'payment_type': 'inbound', - 'partner_type': 'customer', - 'partner_id': partner.id, - 'amount': amount, - 'journal_id': journal.id, - 'currency_id': (journal.currency_id or self.env.company.currency_id).id, - 'date': fields.Date.context_today(self), - 'memo': _('Laundry dues settlement — %s', partner.name), - 'settlement_pos_pm_id': method_id, - } - if session: - pmt_vals['pos_session_id'] = session.id - pmt = Payment.sudo().create(pmt_vals) - try: - pmt.action_post() - except Exception as e: - _logger.exception( - 'Failed to post settlement payment for partner %s', partner.id, - ) - raise UserError(_('Failed to post payment: %s', e)) - self._reconcile_settlement_payment(pmt, partner) - return pmt - - @api.model - def _resolve_settlement_journal(self, pos_payment_method_id): - """Pick the account.journal for a settlement payment. - - 1. If a pos.payment.method id is given, enforce split_transactions=False - and return its journal. - 2. Else return the company's default cash journal, then bank journal. - Raises UserError if no valid journal can be found. - """ - if pos_payment_method_id: - method = self.env['pos.payment.method'].browse(int(pos_payment_method_id)) - if not method.exists(): - raise UserError(_('Selected payment method not found.')) - if method.split_transactions: - raise ValidationError(_( - 'Customer Account / pay-later methods cannot be used to ' - 'settle laundry dues — they would create new receivable ' - 'instead of reducing it.' - )) - if not method.journal_id: - raise UserError(_( - 'Payment method "%s" has no journal configured.', - method.name, - )) - return method.journal_id - - Journal = self.env['account.journal'] - j = Journal.search([ - ('company_id', '=', self.env.company.id), - ('type', '=', 'cash'), - ], limit=1) - if not j: - j = Journal.search([ - ('company_id', '=', self.env.company.id), - ('type', '=', 'bank'), - ], limit=1) - if not j: - raise UserError(_('No cash or bank journal available for settlement.')) - return j - - @api.model - def _reconcile_settlement_payment(self, payment, partner): - """Reconcile the payment's partner-receivable line against open - receivable AMLs from prior customer-account POS sales. - - Journals without an outstanding-receipts account post directly to the - partner receivable (Dr Bank / Cr Receivable, state=paid). Journals - configured with an outstanding-receipts account post to it first - (Dr Bank / Cr Outstanding Receipts, state=in_process) — no partner - receivable line yet, so there's nothing to reconcile here until the - bank statement clears. This method safely no-ops in that case. - """ - receivable_account = partner.property_account_receivable_id - if not receivable_account: - _logger.warning('Partner %s has no receivable account', partner.id) - return - - AML = self.env['account.move.line'] - payment_lines = payment.move_id.line_ids.filtered( - lambda l: l.account_id == receivable_account and not l.reconciled - ) - if not payment_lines: - return # state=in_process — reconciliation happens at bank clearing - - open_debits = AML.search([ - ('partner_id', '=', partner.id), - ('account_id', '=', receivable_account.id), - ('reconciled', '=', False), - ('parent_state', '=', 'posted'), - ('debit', '>', 0), - ], order='date asc, id asc') - if not open_debits: - return - - try: - (payment_lines | open_debits).reconcile() - except Exception: - _logger.exception( - 'Reconciliation failed for settlement payment %s', payment.name, - ) - - @api.model - def _distribute_settlement_fifo(self, partner, amount): - """Spread an already-collected amount across the partner's open - laundry orders, oldest first. Returns (settled_orders, remaining_due). - """ - LaundryOrder = self.env['laundry.order'] - open_orders = LaundryOrder.search([ - ('partner_id', '=', partner.id), - ('amount_due', '>', 0), - ('state', '!=', 'delivered'), - ], order='create_date asc, id asc') - - remaining = amount - settled_orders = [] - for lo in open_orders: - if remaining <= 0: - break - apply_amount = min(remaining, lo.amount_due) - # Defense-in-depth: `amount_settled` is not in - # LOCKED_HEADER_FIELDS so this write already sails through, - # but we pass laundry_pos_sync=True to guarantee the bypass - # even if the locked-field whitelist changes later. - lo.sudo().with_context(laundry_pos_sync=True).write({ - 'amount_settled': (lo.amount_settled or 0.0) + apply_amount, - }) - remaining -= apply_amount - settled_orders.append({ - 'id': lo.id, - 'name': lo.name, - 'applied': apply_amount, - 'remaining_on_order': lo.amount_due, - }) - - total_due = sum( - LaundryOrder.search([ - ('partner_id', '=', partner.id), - ('amount_due', '>', 0), - ('state', '!=', 'delivered'), - ]).mapped('amount_due') - ) - return settled_orders, total_due - - @api.model - def get_session_settlements(self, pos_session_id): - """Return laundry settlement payments stamped on a POS session. - - Used by the closing-screen extension to show a read-only summary - of settlement collections made during this session. Does NOT inject - into POS cash-control totals. - """ - Payment = self.env['account.payment'] - payments = Payment.sudo().search([ - ('pos_session_id', '=', int(pos_session_id)), - ('payment_type', '=', 'inbound'), - ('partner_type', '=', 'customer'), - ], order='date asc, id asc') - - by_journal = {} - for p in payments: - jname = p.journal_id.name - jtype = p.journal_id.type # 'cash' or 'bank' - key = jname - if key not in by_journal: - by_journal[key] = {'name': jname, 'type': jtype, 'total': 0.0} - by_journal[key]['total'] += p.amount - - return { - 'total': sum(p.amount for p in payments), - 'count': len(payments), - 'by_journal': list(by_journal.values()), - 'payments': [{ - 'id': p.id, - 'name': p.name, - 'partner_name': p.partner_id.name, - 'amount': p.amount, - 'journal_name': p.journal_id.name, - 'date': fields.Date.to_string(p.date), - } for p in payments], - } - - @api.model - def get_laundry_orders_for_pos(self, partner_id, limit=20): - """Legacy shim — delegates to the canonical method on laundry.order. - Kept so older POS bundles that still call this RPC keep working. - New code should call `laundry.order.pos_search_customer_orders` - directly; it supports a `search_query` parameter and returns a - richer payload (payment_state, allowed_actions, …). - """ - return self.env['laundry.order'].pos_search_customer_orders( - partner_id=partner_id, limit=limit, - ) diff --git a/addons/laundry_management/models/sale_advance_payment.py b/addons/laundry_management/models/sale_advance_payment.py deleted file mode 100644 index b6baff9..0000000 --- a/addons/laundry_management/models/sale_advance_payment.py +++ /dev/null @@ -1,139 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################### -# -# Cybrosys Technologies Pvt. Ltd. -# -# Copyright (C) 2026-TODAY Cybrosys Technologies() -# Author: Anaswara S (odoo@cybrosys.com) -# -# You can modify it under the terms of the GNU AFFERO -# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. -# -# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE -# (AGPL v3) along with this program. -# If not, see . -# -############################################################################### -import time -from odoo import models, _ -from odoo.exceptions import UserError - - -class SaleAdvancePaymentInv(models.TransientModel): - """Inheriting the model of sale.advance.payment.inv to generate advance - payment of invoice""" - _inherit = 'sale.advance.payment.inv' - - def create_invoices(self): - """Function for creating invoices for the advance payment.""" - laundry_sale_id = self._context.get('laundry_sale_id') - sale_order = self.env['sale.order'] - if laundry_sale_id: - sale_orders = sale_order.browse(laundry_sale_id) - else: - sale_orders = sale_order.browse( - self._context.get('active_ids', [])) - if self.advance_payment_method == 'delivered': - sale_orders._create_invoices() - elif self.advance_payment_method == 'all': - sale_orders._create_invoices()(final=True) - else: - # Create deposit product if necessary - if not self.product_id: - vals = self._prepare_deposit_product() - self.product_id = self.env['product.product'].create(vals) - self.env['ir.config_parameter'].sudo().set_param( - 'sale.default_deposit_product_id', self.product_id.id) - for order in sale_orders: - if self.advance_payment_method == 'percentage': - amount = order.amount_untaxed * self.amount / 100 - else: - amount = self.amount - if self.product_id.invoice_policy != 'order': - raise UserError(_( - 'The product used to invoice a down payment should have' - ' an invoice policy set to "Ordered' - ' quantities". Please update your deposit product to be' - ' able to create a deposit invoice.')) - if self.product_id.type != 'service': - raise UserError(_( - "The product used to invoice a down payment should be" - " of type 'Service'. Please use another " - "product or update this product.")) - taxes = self.product_id.taxes_id.filtered( - lambda - r: not order.company_id or r.company_id == - order.company_id) - if order.fiscal_position_id and taxes: - tax_ids = order.fiscal_position_id.map_tax(taxes).ids - else: - tax_ids = taxes.ids - so_line = self.env['sale.order.line'].create({ - 'name': _('Advance: %s') % (time.strftime('%m %Y'),), - 'price_unit': amount, - 'product_uom_qty': 0.0, - 'order_id': order.id, - 'discount': 0.0, - 'product_uom': self.product_id.uom_id.id, - 'product_id': self.product_id.id, - 'tax_id': [(6, 0, tax_ids)], - }) - self._create_invoice(order, so_line, amount) - if self._context.get('open_invoices', False): - return sale_orders.action_view_invoice() - return {'type': 'ir.actions.act_window_close'} - - def _create_invoice(self, order, so_line): - """Function for creating invoice""" - if (self.advance_payment_method == 'percentage' and - self.amount <= 0.00) or (self.advance_payment_method == 'fixed' and - self.fixed_amount <= 0.00): - raise UserError( - _('The value of the down payment amount must be positive.')) - if self.advance_payment_method == 'percentage': - amount = order.amount_untaxed * self.amount / 100 - name = _("Down payment of %s%%") % (self.amount,) - else: - amount = self.fixed_amount - name = _('Down Payment') - - invoice_vals = { - 'move_type': 'out_invoice', - 'invoice_origin': order.name, - 'invoice_user_id': order.user_id.id, - 'narration': order.note, - 'partner_id': order.partner_invoice_id.id, - 'fiscal_position_id': order.fiscal_position_id.id or order. - partner_id.property_account_position_id.id, - 'partner_shipping_id': order.partner_shipping_id.id, - 'currency_id': order.pricelist_id.currency_id.id, - 'ref': order.client_order_ref, - 'invoice_payment_term_id': order.payment_term_id.id, - 'team_id': order.team_id.id, - 'campaign_id': order.campaign_id.id, - 'medium_id': order.medium_id.id, - 'source_id': order.source_id.id, - 'invoice_line_ids': [(0, 0, { - 'name': name, - 'price_unit': amount, - 'quantity': 1.0, - 'product_id': self.product_id.id, - 'product_uom_id': so_line.product_uom.id, - 'sale_line_ids': [(6, 0, [so_line.id])], - 'analytic_tag_ids': [(6, 0, so_line.analytic_tag_ids.ids)], - 'analytic_account_id': order.analytic_account_id.id or False, - })], - } - if order.fiscal_position_id: - invoice_vals['fiscal_position_id'] = order.fiscal_position_id.id - invoice = self.env['account.move'].create(invoice_vals) - invoice.message_post_with_view('mail.message_origin_link', - values={'self': invoice, - 'origin': order}, - subtype_id=self.env.ref( - 'mail.mt_note').id) diff --git a/addons/laundry_management/models/washing_type.py b/addons/laundry_management/models/washing_type.py deleted file mode 100644 index 8a12894..0000000 --- a/addons/laundry_management/models/washing_type.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################### -# -# Cybrosys Technologies Pvt. Ltd. -# -# Copyright (C) 2026-TODAY Cybrosys Technologies() -# Author: Anaswara S (odoo@cybrosys.com) -# -# You can modify it under the terms of the GNU AFFERO -# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. -# -# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE -# (AGPL v3) along with this program. -# If not, see . -# -############################################################################### -from odoo import fields, models - - -class WashingType(models.Model): - """Washing types generating model""" - _name = 'washing.type' - _description = "Washing TYpe" - - name = fields.Char(string='Name', required=True, - help='Name of Washing type.') - assigned_person_id = fields.Many2one('res.users', - string='Assigned Person', - required=True, - help="Name of assigned person") - amount = fields.Float(string='Service Charge', required=True, - help='Service charge of this type') diff --git a/addons/laundry_management/models/washing_washing.py b/addons/laundry_management/models/washing_washing.py deleted file mode 100644 index 9ee294e..0000000 --- a/addons/laundry_management/models/washing_washing.py +++ /dev/null @@ -1,141 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################### -# -# Cybrosys Technologies Pvt. Ltd. -# -# Copyright (C) 2026-TODAY Cybrosys Technologies() -# Author: Anaswara S (odoo@cybrosys.com) -# -# You can modify it under the terms of the GNU AFFERO -# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. -# -# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE -# (AGPL v3) along with this program. -# If not, see . -# -############################################################################### -from datetime import datetime -from odoo import api, fields, models - - -class WashingWashing(models.Model): - """Washing activity generating model""" - _name = 'washing.washing' - _description = 'Washing Washing' - - name = fields.Char(string='Work', help='Mention the work') - laundry_works = fields.Boolean(default=False, help='For set conditions') - user_id = fields.Many2one('res.users', - string='Assigned Person', - help="Name of assigned person") - washing_date = fields.Datetime(string='Date', help="Date of washing") - description = fields.Text(string='Description', - help='Add the description') - state = fields.Selection([ - ('draft', 'Draft'), - ('process', 'Process'), - ('done', 'Done'), - ('cancel', 'Cancelled'), - ], string='Status', readonly=True, copy=False, index=True, default='draft', - help='State of wash') - laundry_id = fields.Many2one('laundry.order.line') - product_line_ids = fields.One2many('wash.order.line', 'wash_id', - string='Products', ondelete='cascade', - help='Related Products for wash.') - total_amount = fields.Float(compute='_compute_total_amount', - string='Grand Total') - - def start_wash(self): - """Function for initiating the activity of washing.""" - if not self.laundry_works: - self.laundry_id.state = 'wash' - self.laundry_id.laundry_id.state = 'process' - for wash in self: - for line in wash.product_line_ids: - self.env['sale.order.line'].create( - {'product_id': line.product_id.id, - 'name': line.name, - 'price_unit': line.price_unit, - 'order_id': wash.laundry_id.laundry_id.sale_id.id, - 'product_uom_qty': line.quantity, - 'product_uom_id': line.uom_id.id, - }) - self.state = 'process' - - def action_set_to_done(self): - """Function for set to done.""" - self.state = 'done' - f = 0 - if not self.laundry_works: - if self.laundry_id.extra_work_ids: - for each in self.laundry_id.extra_work_ids: - self.create({'name': each.name, - 'user_id': each.assigned_person_id.id, - 'description': self.laundry_id.description, - 'laundry_id': self.laundry_id.id, - 'state': 'draft', - 'laundry_works': True, - 'washing_date': datetime.now().strftime( - '%Y-%m-%d %H:%M:%S')}) - self.laundry_id.state = 'extra_work' - laundry_id = self.search([('laundry_id.laundry_id', '=', - self.laundry_id.laundry_id.id)]) - for each in laundry_id: - if each.state != 'done' or each.state == 'cancel': - f = 1 - break - if f == 0: - self.laundry_id.laundry_id.state = 'done' - laundry = self.search([('laundry_id', '=', self.laundry_id.id)]) - f1 = 0 - for each in laundry: - if each.state != 'done' or each.state == 'cancel': - f1 = 1 - break - if f1 == 0: - self.laundry_id.state = 'done' - - @api.depends('product_line_ids') - def _compute_total_amount(self): - """Total of the line""" - total = 0 - for obj in self: - for each in obj.product_line_ids: - total += each.subtotal - obj.total_amount = total - - -class WashOrderLine(models.Model): - """For creating order lines in washing.""" - _name = 'wash.order.line' - _description = 'Wash Order Line' - - wash_id = fields.Many2one('washing.washing', string='Order Reference', - help='Order reference from washing', - ondelete='cascade') - name = fields.Text(string='Description', required=True, - help='Add description') - uom_id = fields.Many2one('uom.uom', 'Unit of Measure ', required=True) - quantity = fields.Integer(string='Quantity', - help='Add the required quantity') - product_id = fields.Many2one('product.product', string='Product', - help='Order line Product') - price_unit = fields.Float('Unit Price', default=0.0, - related='product_id.list_price', - help='Unit price of Product') - subtotal = fields.Float(compute='_compute_subtotal', string='Subtotal', - readonly=True, store=True, - help='Subtotal of the order line') - - @api.depends('price_unit', 'quantity') - def _compute_subtotal(self): - """Computing the subtotal""" - total = 0 - for wash in self: - total += wash.price_unit * wash.quantity - wash.subtotal = total diff --git a/addons/laundry_management/models/washing_work.py b/addons/laundry_management/models/washing_work.py deleted file mode 100644 index 557dd54..0000000 --- a/addons/laundry_management/models/washing_work.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################### -# -# Cybrosys Technologies Pvt. Ltd. -# -# Copyright (C) 2026-TODAY Cybrosys Technologies() -# Author: Anaswara S (odoo@cybrosys.com) -# -# You can modify it under the terms of the GNU AFFERO -# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. -# -# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE -# (AGPL v3) along with this program. -# If not, see . -# -############################################################################### -from odoo import fields, models - - -class WashingWork(models.Model): - """Model for creating extra work for washing.""" - _name = 'washing.work' - _description = 'Washing Work' - - name = fields.Char(string='Name', required=True) - assigned_person_id = fields.Many2one('res.users', - string='Assigned Person', - required=True, - help="Name of assigned person") - amount = fields.Float(string='Service Charge', required=True, - help='Service charge for the extra work') diff --git a/addons/laundry_management/report/laundry_order_report.xml b/addons/laundry_management/report/laundry_order_report.xml deleted file mode 100644 index 3564a2f..0000000 --- a/addons/laundry_management/report/laundry_order_report.xml +++ /dev/null @@ -1,333 +0,0 @@ - - - - - - Laundry Receipt / إيصال المغسلة - sale.order - qweb-pdf - laundry_management.report_laundry_order_receipt - laundry_management.report_laundry_order_receipt - 'Receipt-%s' % (object.name) - - report - - - - - - - diff --git a/addons/laundry_management/report/laundry_session_report.xml b/addons/laundry_management/report/laundry_session_report.xml deleted file mode 100644 index 47bb337..0000000 --- a/addons/laundry_management/report/laundry_session_report.xml +++ /dev/null @@ -1,291 +0,0 @@ - - - - - - Session Summary / تقرير الجلسة - laundry.session - qweb-pdf - laundry_management.report_laundry_session - laundry_management.report_laundry_session - 'Session-%s' % (object.name) - - report - - - - - - - diff --git a/addons/laundry_management/report/laundry_thermal_report.xml b/addons/laundry_management/report/laundry_thermal_report.xml deleted file mode 100644 index cf556c7..0000000 --- a/addons/laundry_management/report/laundry_thermal_report.xml +++ /dev/null @@ -1,296 +0,0 @@ - - - - - - Thermal 80mm Receipt - False - custom - 0 - 80 - Portrait - 3 - 3 - 3 - 3 - False - 3 - 96 - - - - - Thermal Receipt (80mm) - sale.order - qweb-pdf - laundry_management.report_laundry_thermal_receipt - laundry_management.report_laundry_thermal_receipt - 'Thermal-%s' % (object.name) - - - - - - Item Tracking Slips - sale.order - qweb-pdf - laundry_management.report_laundry_tracking_slip - laundry_management.report_laundry_tracking_slip - 'Tracking-%s' % (object.name) - - - - - - - - - - diff --git a/addons/laundry_management/report/laundry_work_order_report.xml b/addons/laundry_management/report/laundry_work_order_report.xml deleted file mode 100644 index 099eded..0000000 --- a/addons/laundry_management/report/laundry_work_order_report.xml +++ /dev/null @@ -1,228 +0,0 @@ - - - - - - Laundry Work Order - laundry.order - qweb-pdf - laundry_management.report_laundry_work_order - laundry_management.report_laundry_work_order - 'WO-%s' % (object.name or '').replace('/', '-') - - report - - - - - - diff --git a/addons/laundry_management/reports/__init__.py b/addons/laundry_management/reports/__init__.py deleted file mode 100644 index d1eca3e..0000000 --- a/addons/laundry_management/reports/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################### -# -# Cybrosys Technologies Pvt. Ltd. -# -# Copyright (C) 2026-TODAY Cybrosys Technologies() -# Author: Anaswara S (odoo@cybrosys.com) -# -# You can modify it under the terms of the GNU AFFERO -# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. -# -# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE -# (AGPL v3) along with this program. -# If not, see . -# -############################################################################### -from . import report_laundry_order diff --git a/addons/laundry_management/reports/report_laundry_order.py b/addons/laundry_management/reports/report_laundry_order.py deleted file mode 100644 index 502cf17..0000000 --- a/addons/laundry_management/reports/report_laundry_order.py +++ /dev/null @@ -1,105 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################### -# -# Cybrosys Technologies Pvt. Ltd. -# -# Copyright (C) 2026-TODAY Cybrosys Technologies() -# Author: Anaswara S (odoo@cybrosys.com) -# -# You can modify it under the terms of the GNU AFFERO -# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. -# -# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE -# (AGPL v3) along with this program. -# If not, see . -# -############################################################################### -from odoo import fields, models, tools - - -class ReportLaundryOrder(models.Model): - """Model for checking the history of all laundry orders.""" - _name = "report.laundry.order" - _description = "Report Laundry Order" - _order = 'name desc' - _auto = False - - name = fields.Char(string="Label") - invoice_status = fields.Selection([ - ('upselling', 'Upselling Opportunity'), - ('invoiced', 'Fully Invoiced'), - ('to invoice', 'To Invoice'), - ('no', 'Nothing to Invoice') - ], string='Invoice Status', store=True, help="status of invoice") - partner_id = fields.Many2one('res.partner', string='Customer', - help="Name of the customer") - partner_invoice_id = fields.Many2one('res.partner', - string='Invoice Address', - help="Invoice address of Customer") - partner_shipping_id = fields.Many2one('res.partner', - string='Delivery Address', - help="Delivery address of customer") - order_date = fields.Datetime(string="Date", help="Date of order") - laundry_person_id = fields.Many2one('res.users', - string='Laundry Person', - help="Name of laundry person") - total_amount = fields.Float(string='Total') - currency_id = fields.Many2one("res.currency", - string="Currency", help="Name of currency") - state = fields.Selection([ - ('draft', 'Draft'), - ('order', 'Laundry Order'), - ('process', 'Processing'), - ('done', 'Done'), - ('return', 'Returned'), - ('cancel', 'Cancelled'), - ], string='Status') - - def _select(self): - select_str = """ - SELECT - (select 1 ) AS nbr, - t.id as id, - t.name as name, - t.invoice_status as invoice_status, - t.partner_id as partner_id, - t.partner_invoice_id as partner_invoice_id, - t.partner_shipping_id as partner_shipping_id, - t.order_date as order_date, - t.laundry_person_id as laundry_person_id, - t.total_amount as total_amount, - t.currency_id as currency_id, - t.state as state - """ - return select_str - - def _group_by(self): - group_by_str = """ - GROUP BY - t.id, - name, - invoice_status, - partner_id, - partner_invoice_id, - partner_shipping_id, - order_date, - laundry_person_id, - total_amount, - currency_id, - state - """ - return group_by_str - - def init(self): - tools.sql.drop_view_if_exists(self._cr, 'report_laundry_order') - self._cr.execute(""" - CREATE view report_laundry_order as - %s - FROM laundry_order t - %s - """ % (self._select(), self._group_by())) diff --git a/addons/laundry_management/security/ir.model.access.csv b/addons/laundry_management/security/ir.model.access.csv deleted file mode 100644 index 94f9ac4..0000000 --- a/addons/laundry_management/security/ir.model.access.csv +++ /dev/null @@ -1,22 +0,0 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_laundry_order_operator,laundry.order operator,model_laundry_order,laundry_management.group_laundry_operator,1,0,0,0 -access_laundry_order_cashier,laundry.order cashier,model_laundry_order,laundry_management.group_laundry_cashier,1,1,1,0 -access_laundry_order_manager,laundry.order manager,model_laundry_order,laundry_management.group_laundry_manager,1,1,1,1 -access_laundry_order_line_operator,laundry.order.line operator,model_laundry_order_line,laundry_management.group_laundry_operator,1,0,0,0 -access_laundry_order_line_cashier,laundry.order.line cashier,model_laundry_order_line,laundry_management.group_laundry_cashier,1,1,1,0 -access_laundry_order_line_manager,laundry.order.line manager,model_laundry_order_line,laundry_management.group_laundry_manager,1,1,1,1 -access_laundry_commission_operator,laundry.commission operator,model_laundry_commission,laundry_management.group_laundry_operator,1,0,0,0 -access_laundry_commission_cashier,laundry.commission cashier,model_laundry_commission,laundry_management.group_laundry_cashier,1,0,0,0 -access_laundry_commission_manager,laundry.commission manager,model_laundry_commission,laundry_management.group_laundry_manager,1,1,1,1 -access_laundry_payment_method_user,laundry.payment.method user,model_laundry_payment_method,laundry_management.group_laundry_cashier,1,0,0,0 -access_laundry_payment_method_manager,laundry.payment.method manager,model_laundry_payment_method,laundry_management.group_laundry_manager,1,1,1,1 -access_laundry_print_wizard_user,laundry.print.wizard user,model_laundry_print_wizard,laundry_management.group_laundry_operator,1,1,1,1 -access_laundry_order_line_addon_user,laundry.order.line.addon user,model_laundry_order_line_addon,laundry_management.group_laundry_operator,1,1,1,0 -access_laundry_order_line_addon_manager,laundry.order.line.addon manager,model_laundry_order_line_addon,laundry_management.group_laundry_manager,1,1,1,1 -access_laundry_order_type_operator,laundry.order.type operator,model_laundry_order_type,laundry_management.group_laundry_operator,1,0,0,0 -access_laundry_order_type_cashier,laundry.order.type cashier,model_laundry_order_type,laundry_management.group_laundry_cashier,1,0,0,0 -access_laundry_order_type_manager,laundry.order.type manager,model_laundry_order_type,laundry_management.group_laundry_manager,1,1,1,1 -access_laundry_order_attribute_operator,laundry.order.attribute operator,model_laundry_order_attribute,laundry_management.group_laundry_operator,1,0,0,0 -access_laundry_order_attribute_cashier,laundry.order.attribute cashier,model_laundry_order_attribute,laundry_management.group_laundry_cashier,1,0,0,0 -access_laundry_order_attribute_manager,laundry.order.attribute manager,model_laundry_order_attribute,laundry_management.group_laundry_manager,1,1,1,1 -access_laundry_order_unlock_wizard_override,laundry.order.unlock.wizard manager,model_laundry_order_unlock_wizard,laundry_management.group_laundry_manager_override,1,1,1,1 diff --git a/addons/laundry_management/security/ir_rule.xml b/addons/laundry_management/security/ir_rule.xml deleted file mode 100644 index 1eddeaf..0000000 --- a/addons/laundry_management/security/ir_rule.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - Laundry Order: Operator read own company - - [('company_id', 'in', company_ids)] - - - - - - - - - - Laundry Order: Cashier read/write/create own company - - [('company_id', 'in', company_ids)] - - - - - - - - - - Laundry Order: Manager full access own company - - [('company_id', 'in', company_ids)] - - - - - - - - diff --git a/addons/laundry_management/security/laundry_management_security.xml b/addons/laundry_management/security/laundry_management_security.xml deleted file mode 100644 index 94d7e3f..0000000 --- a/addons/laundry_management/security/laundry_management_security.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - Laundry - 18 - - - Laundry - - 7 - - - - Laundry User - - - - - - Laundry Manager - - - - - - Laundry Admin - - - - - - - Laundry Manager: Full access - - [(1,'=',1)] - - - - User: own document only - - [('laundry_person_id','=',user.id)] - - - - diff --git a/addons/laundry_management/security/res_groups.xml b/addons/laundry_management/security/res_groups.xml deleted file mode 100644 index c719f9c..0000000 --- a/addons/laundry_management/security/res_groups.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - Laundry - Laundry operations and POS settlement permissions. - 30 - - - - Laundry - 30 - - - - - Operator - 10 - - - - - - Cashier - 20 - - - - - - - Laundry / User - - - - - Manager - 30 - - - - - - - Manager Override (Unlock Locked Orders) - 40 - - - - - diff --git a/addons/laundry_management/static/description/assets/cybro-icon.png b/addons/laundry_management/static/description/assets/cybro-icon.png deleted file mode 100644 index 06e73e1..0000000 Binary files a/addons/laundry_management/static/description/assets/cybro-icon.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/cybro-odoo.png b/addons/laundry_management/static/description/assets/cybro-odoo.png deleted file mode 100644 index ed02e07..0000000 Binary files a/addons/laundry_management/static/description/assets/cybro-odoo.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/h2.png b/addons/laundry_management/static/description/assets/h2.png deleted file mode 100644 index 0bfc470..0000000 Binary files a/addons/laundry_management/static/description/assets/h2.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/arrows-repeat.svg b/addons/laundry_management/static/description/assets/icons/arrows-repeat.svg deleted file mode 100644 index 1d7efab..0000000 --- a/addons/laundry_management/static/description/assets/icons/arrows-repeat.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/banner-1.png b/addons/laundry_management/static/description/assets/icons/banner-1.png deleted file mode 100644 index c180db1..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/banner-1.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/banner-2.svg b/addons/laundry_management/static/description/assets/icons/banner-2.svg deleted file mode 100644 index e606d97..0000000 --- a/addons/laundry_management/static/description/assets/icons/banner-2.svg +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/banner-bg.png b/addons/laundry_management/static/description/assets/icons/banner-bg.png deleted file mode 100644 index a8238d3..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/banner-bg.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/banner-bg.svg b/addons/laundry_management/static/description/assets/icons/banner-bg.svg deleted file mode 100644 index b137810..0000000 --- a/addons/laundry_management/static/description/assets/icons/banner-bg.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/banner-call.svg b/addons/laundry_management/static/description/assets/icons/banner-call.svg deleted file mode 100644 index 96c687e..0000000 --- a/addons/laundry_management/static/description/assets/icons/banner-call.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/banner-mail.svg b/addons/laundry_management/static/description/assets/icons/banner-mail.svg deleted file mode 100644 index cbf0d15..0000000 --- a/addons/laundry_management/static/description/assets/icons/banner-mail.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/banner-pattern.svg b/addons/laundry_management/static/description/assets/icons/banner-pattern.svg deleted file mode 100644 index 9c1c7e1..0000000 --- a/addons/laundry_management/static/description/assets/icons/banner-pattern.svg +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/banner-promo.svg b/addons/laundry_management/static/description/assets/icons/banner-promo.svg deleted file mode 100644 index d52791b..0000000 --- a/addons/laundry_management/static/description/assets/icons/banner-promo.svg +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/brand-pair.svg b/addons/laundry_management/static/description/assets/icons/brand-pair.svg deleted file mode 100644 index d8db7fc..0000000 --- a/addons/laundry_management/static/description/assets/icons/brand-pair.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/chart.png b/addons/laundry_management/static/description/assets/icons/chart.png deleted file mode 100644 index 476838c..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/chart.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/chat.png b/addons/laundry_management/static/description/assets/icons/chat.png deleted file mode 100644 index fd7f0dd..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/chat.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/check.png b/addons/laundry_management/static/description/assets/icons/check.png deleted file mode 100644 index c8e85f5..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/check.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/chevron.png b/addons/laundry_management/static/description/assets/icons/chevron.png deleted file mode 100644 index 2089293..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/chevron.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/close-icon.svg b/addons/laundry_management/static/description/assets/icons/close-icon.svg deleted file mode 100644 index df8cce3..0000000 --- a/addons/laundry_management/static/description/assets/icons/close-icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/close.png b/addons/laundry_management/static/description/assets/icons/close.png deleted file mode 100644 index aa186f9..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/close.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/cogs.png b/addons/laundry_management/static/description/assets/icons/cogs.png deleted file mode 100644 index 95d0bad..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/cogs.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/collabarate-icon.svg b/addons/laundry_management/static/description/assets/icons/collabarate-icon.svg deleted file mode 100644 index dd4e105..0000000 --- a/addons/laundry_management/static/description/assets/icons/collabarate-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/addons/laundry_management/static/description/assets/icons/consultation.png b/addons/laundry_management/static/description/assets/icons/consultation.png deleted file mode 100644 index 8319d4b..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/consultation.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/cybro-logo.png b/addons/laundry_management/static/description/assets/icons/cybro-logo.png deleted file mode 100644 index ff4b782..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/cybro-logo.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/down.svg b/addons/laundry_management/static/description/assets/icons/down.svg deleted file mode 100644 index f21c362..0000000 --- a/addons/laundry_management/static/description/assets/icons/down.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/addons/laundry_management/static/description/assets/icons/ecom-black.png b/addons/laundry_management/static/description/assets/icons/ecom-black.png deleted file mode 100644 index a9385ff..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/ecom-black.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/education-black.png b/addons/laundry_management/static/description/assets/icons/education-black.png deleted file mode 100644 index 3eb09b2..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/education-black.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/faq.png b/addons/laundry_management/static/description/assets/icons/faq.png deleted file mode 100644 index 4250b5b..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/faq.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/feature-icon.svg b/addons/laundry_management/static/description/assets/icons/feature-icon.svg deleted file mode 100644 index fa0ea68..0000000 --- a/addons/laundry_management/static/description/assets/icons/feature-icon.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/feature.png b/addons/laundry_management/static/description/assets/icons/feature.png deleted file mode 100644 index ac7a785..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/feature.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/gear.svg b/addons/laundry_management/static/description/assets/icons/gear.svg deleted file mode 100644 index 0cc66b6..0000000 --- a/addons/laundry_management/static/description/assets/icons/gear.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/hero.gif b/addons/laundry_management/static/description/assets/icons/hero.gif deleted file mode 100644 index 380654d..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/hero.gif and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/hire-odoo.svg b/addons/laundry_management/static/description/assets/icons/hire-odoo.svg deleted file mode 100644 index e1ac089..0000000 --- a/addons/laundry_management/static/description/assets/icons/hire-odoo.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/hotel-black.png b/addons/laundry_management/static/description/assets/icons/hotel-black.png deleted file mode 100644 index 130f613..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/hotel-black.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/idea.png b/addons/laundry_management/static/description/assets/icons/idea.png deleted file mode 100644 index 801ce54..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/idea.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/imple.png b/addons/laundry_management/static/description/assets/icons/imple.png deleted file mode 100644 index 5f1b94d..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/imple.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/license.png b/addons/laundry_management/static/description/assets/icons/license.png deleted file mode 100644 index a586979..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/license.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/life-ring-icon.svg b/addons/laundry_management/static/description/assets/icons/life-ring-icon.svg deleted file mode 100644 index 3ae6e1d..0000000 --- a/addons/laundry_management/static/description/assets/icons/life-ring-icon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/lifebuoy.png b/addons/laundry_management/static/description/assets/icons/lifebuoy.png deleted file mode 100644 index 658d56c..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/lifebuoy.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/mail.png b/addons/laundry_management/static/description/assets/icons/mail.png deleted file mode 100644 index 0b04623..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/mail.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/mail.svg b/addons/laundry_management/static/description/assets/icons/mail.svg deleted file mode 100644 index 1eedde6..0000000 --- a/addons/laundry_management/static/description/assets/icons/mail.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/addons/laundry_management/static/description/assets/icons/manufacturing-black.png b/addons/laundry_management/static/description/assets/icons/manufacturing-black.png deleted file mode 100644 index 697eb0e..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/manufacturing-black.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/notes.png b/addons/laundry_management/static/description/assets/icons/notes.png deleted file mode 100644 index ee5e954..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/notes.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/notification icon.svg b/addons/laundry_management/static/description/assets/icons/notification icon.svg deleted file mode 100644 index 0531899..0000000 --- a/addons/laundry_management/static/description/assets/icons/notification icon.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/odoo-consultancy.svg b/addons/laundry_management/static/description/assets/icons/odoo-consultancy.svg deleted file mode 100644 index e05f65b..0000000 --- a/addons/laundry_management/static/description/assets/icons/odoo-consultancy.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/addons/laundry_management/static/description/assets/icons/odoo-licencing.svg b/addons/laundry_management/static/description/assets/icons/odoo-licencing.svg deleted file mode 100644 index 2606c88..0000000 --- a/addons/laundry_management/static/description/assets/icons/odoo-licencing.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/addons/laundry_management/static/description/assets/icons/odoo-logo.png b/addons/laundry_management/static/description/assets/icons/odoo-logo.png deleted file mode 100644 index 0e4d0eb..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/odoo-logo.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/patter.svg b/addons/laundry_management/static/description/assets/icons/patter.svg deleted file mode 100644 index 25c9c0a..0000000 --- a/addons/laundry_management/static/description/assets/icons/patter.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/pattern1.png b/addons/laundry_management/static/description/assets/icons/pattern1.png deleted file mode 100644 index 09ab0fb..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/pattern1.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/people.png b/addons/laundry_management/static/description/assets/icons/people.png deleted file mode 100644 index 069cd65..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/people.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/pos-black.png b/addons/laundry_management/static/description/assets/icons/pos-black.png deleted file mode 100644 index 97c0f90..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/pos-black.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/puzzle-piece-icon.svg b/addons/laundry_management/static/description/assets/icons/puzzle-piece-icon.svg deleted file mode 100644 index 3e9ad93..0000000 --- a/addons/laundry_management/static/description/assets/icons/puzzle-piece-icon.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/puzzle.png b/addons/laundry_management/static/description/assets/icons/puzzle.png deleted file mode 100644 index 65cf854..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/puzzle.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/replace-icon.svg b/addons/laundry_management/static/description/assets/icons/replace-icon.svg deleted file mode 100644 index d0e3a7a..0000000 --- a/addons/laundry_management/static/description/assets/icons/replace-icon.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/restaurant-black.png b/addons/laundry_management/static/description/assets/icons/restaurant-black.png deleted file mode 100644 index 4a35eb9..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/restaurant-black.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/screenshot-main.png b/addons/laundry_management/static/description/assets/icons/screenshot-main.png deleted file mode 100644 index 575f8e6..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/screenshot-main.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/screenshot.png b/addons/laundry_management/static/description/assets/icons/screenshot.png deleted file mode 100644 index cef2725..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/screenshot.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/service-black.png b/addons/laundry_management/static/description/assets/icons/service-black.png deleted file mode 100644 index 301ab51..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/service-black.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/setting.png b/addons/laundry_management/static/description/assets/icons/setting.png deleted file mode 100644 index 9783660..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/setting.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/skype-fill.svg b/addons/laundry_management/static/description/assets/icons/skype-fill.svg deleted file mode 100644 index c174236..0000000 --- a/addons/laundry_management/static/description/assets/icons/skype-fill.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/skype.png b/addons/laundry_management/static/description/assets/icons/skype.png deleted file mode 100644 index 51b409f..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/skype.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/skype.svg b/addons/laundry_management/static/description/assets/icons/skype.svg deleted file mode 100644 index df3dad3..0000000 --- a/addons/laundry_management/static/description/assets/icons/skype.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/addons/laundry_management/static/description/assets/icons/star-1.svg b/addons/laundry_management/static/description/assets/icons/star-1.svg deleted file mode 100644 index 7e55ab1..0000000 --- a/addons/laundry_management/static/description/assets/icons/star-1.svg +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/star-2.svg b/addons/laundry_management/static/description/assets/icons/star-2.svg deleted file mode 100644 index 5ae9f50..0000000 --- a/addons/laundry_management/static/description/assets/icons/star-2.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/support.png b/addons/laundry_management/static/description/assets/icons/support.png deleted file mode 100644 index af2f02f..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/support.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/test-1 - Copy.png b/addons/laundry_management/static/description/assets/icons/test-1 - Copy.png deleted file mode 100644 index f6a9026..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/test-1 - Copy.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/test-1.png b/addons/laundry_management/static/description/assets/icons/test-1.png deleted file mode 100644 index 0908add..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/test-1.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/test-2.png b/addons/laundry_management/static/description/assets/icons/test-2.png deleted file mode 100644 index 4671fe9..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/test-2.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/tick.png b/addons/laundry_management/static/description/assets/icons/tick.png deleted file mode 100644 index 5720179..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/tick.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/tms.png b/addons/laundry_management/static/description/assets/icons/tms.png deleted file mode 100644 index f87428e..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/tms.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/trading-black.png b/addons/laundry_management/static/description/assets/icons/trading-black.png deleted file mode 100644 index 9398ba2..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/trading-black.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/training.png b/addons/laundry_management/static/description/assets/icons/training.png deleted file mode 100644 index 884ca02..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/training.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/transfer.png b/addons/laundry_management/static/description/assets/icons/transfer.png deleted file mode 100644 index da60e07..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/transfer.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/translate.svg b/addons/laundry_management/static/description/assets/icons/translate.svg deleted file mode 100644 index af9c8a1..0000000 --- a/addons/laundry_management/static/description/assets/icons/translate.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/update.png b/addons/laundry_management/static/description/assets/icons/update.png deleted file mode 100644 index ecbc5a0..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/update.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/user.png b/addons/laundry_management/static/description/assets/icons/user.png deleted file mode 100644 index 6ffb23d..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/user.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/video.png b/addons/laundry_management/static/description/assets/icons/video.png deleted file mode 100644 index 576705b..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/video.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/whatsapp.png b/addons/laundry_management/static/description/assets/icons/whatsapp.png deleted file mode 100644 index 989a8fd..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/whatsapp.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/icons/wrench-icon.svg b/addons/laundry_management/static/description/assets/icons/wrench-icon.svg deleted file mode 100644 index 174b5a4..0000000 --- a/addons/laundry_management/static/description/assets/icons/wrench-icon.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/addons/laundry_management/static/description/assets/icons/wrench.png b/addons/laundry_management/static/description/assets/icons/wrench.png deleted file mode 100644 index 6c04dea..0000000 Binary files a/addons/laundry_management/static/description/assets/icons/wrench.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/modules/1.gif b/addons/laundry_management/static/description/assets/modules/1.gif deleted file mode 100644 index ae3a880..0000000 Binary files a/addons/laundry_management/static/description/assets/modules/1.gif and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/modules/2.gif b/addons/laundry_management/static/description/assets/modules/2.gif deleted file mode 100644 index d19e2b3..0000000 Binary files a/addons/laundry_management/static/description/assets/modules/2.gif and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/modules/3.png b/addons/laundry_management/static/description/assets/modules/3.png deleted file mode 100644 index 8513873..0000000 Binary files a/addons/laundry_management/static/description/assets/modules/3.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/modules/4.png b/addons/laundry_management/static/description/assets/modules/4.png deleted file mode 100644 index 3bedf79..0000000 Binary files a/addons/laundry_management/static/description/assets/modules/4.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/modules/5.png b/addons/laundry_management/static/description/assets/modules/5.png deleted file mode 100644 index 0e311ca..0000000 Binary files a/addons/laundry_management/static/description/assets/modules/5.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/modules/6.jpg b/addons/laundry_management/static/description/assets/modules/6.jpg deleted file mode 100644 index 67c7f70..0000000 Binary files a/addons/laundry_management/static/description/assets/modules/6.jpg and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/modules/b1.gif b/addons/laundry_management/static/description/assets/modules/b1.gif deleted file mode 100644 index f61b9aa..0000000 Binary files a/addons/laundry_management/static/description/assets/modules/b1.gif and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/modules/b2.jpg b/addons/laundry_management/static/description/assets/modules/b2.jpg deleted file mode 100644 index 845917d..0000000 Binary files a/addons/laundry_management/static/description/assets/modules/b2.jpg and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/modules/b3.jpg b/addons/laundry_management/static/description/assets/modules/b3.jpg deleted file mode 100644 index a2c0047..0000000 Binary files a/addons/laundry_management/static/description/assets/modules/b3.jpg and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/modules/b4.jpg b/addons/laundry_management/static/description/assets/modules/b4.jpg deleted file mode 100644 index 81cd21f..0000000 Binary files a/addons/laundry_management/static/description/assets/modules/b4.jpg and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/modules/b5.jpg b/addons/laundry_management/static/description/assets/modules/b5.jpg deleted file mode 100644 index c65b1f1..0000000 Binary files a/addons/laundry_management/static/description/assets/modules/b5.jpg and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/modules/b6.jpg b/addons/laundry_management/static/description/assets/modules/b6.jpg deleted file mode 100644 index 6c2d578..0000000 Binary files a/addons/laundry_management/static/description/assets/modules/b6.jpg and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/screenshots/1.png b/addons/laundry_management/static/description/assets/screenshots/1.png deleted file mode 100644 index 49ae882..0000000 Binary files a/addons/laundry_management/static/description/assets/screenshots/1.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/screenshots/10.png b/addons/laundry_management/static/description/assets/screenshots/10.png deleted file mode 100644 index 309a0cc..0000000 Binary files a/addons/laundry_management/static/description/assets/screenshots/10.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/screenshots/2.png b/addons/laundry_management/static/description/assets/screenshots/2.png deleted file mode 100644 index 77e36e0..0000000 Binary files a/addons/laundry_management/static/description/assets/screenshots/2.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/screenshots/3.png b/addons/laundry_management/static/description/assets/screenshots/3.png deleted file mode 100644 index 2da3ab4..0000000 Binary files a/addons/laundry_management/static/description/assets/screenshots/3.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/screenshots/4.png b/addons/laundry_management/static/description/assets/screenshots/4.png deleted file mode 100644 index 67143ae..0000000 Binary files a/addons/laundry_management/static/description/assets/screenshots/4.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/screenshots/5.png b/addons/laundry_management/static/description/assets/screenshots/5.png deleted file mode 100644 index 10a04c5..0000000 Binary files a/addons/laundry_management/static/description/assets/screenshots/5.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/screenshots/6.png b/addons/laundry_management/static/description/assets/screenshots/6.png deleted file mode 100644 index 5b05ef6..0000000 Binary files a/addons/laundry_management/static/description/assets/screenshots/6.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/screenshots/7.png b/addons/laundry_management/static/description/assets/screenshots/7.png deleted file mode 100644 index 2411517..0000000 Binary files a/addons/laundry_management/static/description/assets/screenshots/7.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/screenshots/8.png b/addons/laundry_management/static/description/assets/screenshots/8.png deleted file mode 100644 index 9ca3bd0..0000000 Binary files a/addons/laundry_management/static/description/assets/screenshots/8.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/screenshots/9.png b/addons/laundry_management/static/description/assets/screenshots/9.png deleted file mode 100644 index 975f770..0000000 Binary files a/addons/laundry_management/static/description/assets/screenshots/9.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/screenshots/hero.gif b/addons/laundry_management/static/description/assets/screenshots/hero.gif deleted file mode 100644 index 8a569d9..0000000 Binary files a/addons/laundry_management/static/description/assets/screenshots/hero.gif and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/screenshots/report.png b/addons/laundry_management/static/description/assets/screenshots/report.png deleted file mode 100644 index 0a3facd..0000000 Binary files a/addons/laundry_management/static/description/assets/screenshots/report.png and /dev/null differ diff --git a/addons/laundry_management/static/description/assets/y18.jpg b/addons/laundry_management/static/description/assets/y18.jpg deleted file mode 100644 index eea1714..0000000 Binary files a/addons/laundry_management/static/description/assets/y18.jpg and /dev/null differ diff --git a/addons/laundry_management/static/description/banner.jpg b/addons/laundry_management/static/description/banner.jpg deleted file mode 100644 index 7c0b8e5..0000000 Binary files a/addons/laundry_management/static/description/banner.jpg and /dev/null differ diff --git a/addons/laundry_management/static/description/icon.png b/addons/laundry_management/static/description/icon.png deleted file mode 100644 index dcd875a..0000000 Binary files a/addons/laundry_management/static/description/icon.png and /dev/null differ diff --git a/addons/laundry_management/static/description/index.html b/addons/laundry_management/static/description/index.html deleted file mode 100644 index e8b5433..0000000 --- a/addons/laundry_management/static/description/index.html +++ /dev/null @@ -1,994 +0,0 @@ - - - - - - - Laundry Management - - - - - - - - - - - - -
-
-
-
-
- -
-
-
-
-
- Supports: -
- Community -
-
- Enterprise -
- -
-
-
-
- Availability: -
- - - Odoo Online -
-
- - - Odoo.sh -
-
- - - On Premise -
-
-
-
-
-
-
- - -
-
-
-

Laundry Management

-

Helps You To Manage Laundry Service.

- - - -
-
-
- - - -
-
- -

Key - Highlights -

- -
-
-
- -
- -
-

Manages Laundry Process. -

-

industrial specific module by Cybrosys - Technologies for Laundry Management. It manages - the laundry process with assigning works to - workers.

- -
-
-
-
- -
- -
-

- Laundry Order Analysis Report. -

-

Can create - laundry order analysis report.

-
-
-
-
- -
- -
-

Using Washing Type and Additional Works.

-

Laundry order - can be created by using different types of - washing and an additional works can be included - in the laundry order.

- -
-
-
-
-
- - -
-
- - - -
-
-
- - -

- Recording Laundry Order

-
- Laundry Management -> Laundry - Management -> Laundry Order - When you install this module, an - extra menu Laundry Management - will created in main menu. Youcan - see the different sub menus under - the main menu. Here you can create - Laundry Order via clicking the - 'Create' button. There you can - specify the customer, laundry - person, - order lines with washing type and - Extra works for your orders. -
- -
- -
- - -

Confirm the - Laundry Order.

-
- When you confirm the Laundry Order - the corresponding works will created - under the assigned person. - There you can add extra products - also. It will also add in Billing.
- -
- -
- - -

Laundry Label. -

-
-You can print label for each order - from the print menu.
- -
- -
- -

Laundry Works. -

-
You can access it through the smart button inside order or Laundry Management -> Laundry - Management -> Laundry Works. - This is a Separate Laundry Works - Form. Here you can see the work - status of Laundry Works. -
-
- -
- -

Extra Works. -

-
- If there is any extra works , it - will created as work When you finish - the main work. Then we - can see the status of that order - line is become 'Make Over'. -
-
- -
- -

Invoice. -

-
- You can create Invoice via the - button 'Create Invoice' when the - order reaches to 'Done' state. -
-
- -
- -

Invoices Smart Button. -

-
- You can see all the Invoice through - the smart button "Invoices" from the - Laundry Order form. -
-
- -
- -

Washing Types. -

-
- You can configure washing types from - the menu Laundry Management -> - Configuration -> - Washing Type by specifying the name, - assigned person and service charge. -
-
- -
- -

Additional Works. -

-
- You can configure additional works - from the menu Laundry Management -> - Configuration -> - Additional Works by specifying the - name, assigned person and service - charge. -
-
- -
-
-
-
-
- -

Our - Features -

-
- -
-
-
01 -
-

Recording Laundry Order.

-
-
-
-
-
02 -
-

Detailed Laundry Work Analysis Report.

-
-
-
-
-
03 -
-

Billing Facility for Extra Works.

-
-
-
-
-
-
-
- -

- Frequently Asked Questions

- -
-
-
-

- -

-
-
-

- You can easily track customer orders by navigating to the "Orders" section of the app. Here, you can view all current and past orders, including details like customer information, items submitted, and their status. You can also set reminders for due dates to ensure timely service. -

-
-
-
- -
-

- -

-
-
-

- Yes, the app allows you to customize pricing based on different services offered. You can set up various service categories, such as washing, ironing, and special treatments, and specify prices for each service type, ensuring accurate billing for your customers. -

-
-
- -
-

- -

-
-
-

- Absolutely! The app includes reporting features that allow you to generate detailed reports on various metrics, such as the number of orders processed, revenue generated, and customer feedback. These insights help you assess performance and identify areas for improvement. -

-
-
- -
-
-
-
-
-
-

- Latest Releases

-
- -
-
-
-
Latest Release 19.0.1.0.0 -
-
Add -
-
- - 26th January, 2026 -
-
-
-
- -
Initial Commit -
-
-
-
-
- - -
-
-
-
- -
- - -
- - - - -
-
- -

Our - Services -

- -
- -
-
- -
- -
-

- Odoo Customization

- - -
-
- -
-
- -
- -
-

- Odoo Implementation

- - -
-
- -
-
- -
- -
-

- Odoo Support

- - -
-
- -
-
- -
- -
-

- Odoo Migration

- - -
-
- -
-
- -
- -
-

- Odoo integration

-
-
- -
-
- -
- -
-

- Odoo Consultancy

- - -
-
- -
-
- -
- -
-

- Odoo Licensing

- - -
-
- -
-
- -
- -
-

- Hire Odoo Developer

- - -
-
- -
-
-
- - - - - - - - - \ No newline at end of file diff --git a/addons/laundry_management/static/src/js/closing_popup_patch.js b/addons/laundry_management/static/src/js/closing_popup_patch.js deleted file mode 100644 index 77833a6..0000000 --- a/addons/laundry_management/static/src/js/closing_popup_patch.js +++ /dev/null @@ -1,52 +0,0 @@ -/** @odoo-module - * - * ClosePosPopup patch — surface a READ-ONLY Laundry Settlements panel - * in the POS closing popup so cashiers and managers can see how much - * was collected via the laundry settle-dues flow during this session, - * grouped per journal. - * - * NON-CASH settlements create account.payment records (not pos.payment), - * so they don't naturally appear in the closing popup's per-method - * breakdown. CASH settlements DO appear there (via statement_line_ids - * in expected cash). This panel makes both visible side-by-side so the - * cashier can reconcile drawer vs. settlement collections. - * - * IMPORTANT: this is purely informational. It does NOT inject into the - * cash-counted/expected math, does NOT create accounting entries, and - * does NOT modify settlement amounts. - */ -import { patch } from "@web/core/utils/patch"; -import { onWillStart, useState } from "@odoo/owl"; -import { ClosePosPopup } from "@point_of_sale/app/components/popups/closing_popup/closing_popup"; - -patch(ClosePosPopup.prototype, { - setup() { - super.setup(); - this.laundrySettlements = useState({ - loaded: false, - total: 0.0, - count: 0, - by_journal: [], - }); - onWillStart(async () => { - const sessionId = this.pos?.session?.id; - if (!sessionId) { - this.laundrySettlements.loaded = true; - return; - } - try { - const data = await this.pos.data.call( - "res.partner", - "get_session_settlements", - [sessionId] - ); - if (data) { - Object.assign(this.laundrySettlements, data, { loaded: true }); - } - } catch { - // Read-only panel — silent failure is fine. - this.laundrySettlements.loaded = true; - } - }); - }, -}); diff --git a/addons/laundry_management/static/src/js/control_buttons_patch.js b/addons/laundry_management/static/src/js/control_buttons_patch.js deleted file mode 100644 index 1c44e29..0000000 --- a/addons/laundry_management/static/src/js/control_buttons_patch.js +++ /dev/null @@ -1,35 +0,0 @@ -/** @odoo-module - * - * ControlButtons patch — wires the two custom POS quick-action buttons - * declared in static/src/xml/control_buttons.xml to the PosStore methods - * that already exist in pos_store_patch.js: - * - * onClickSettleDues → pos.settleLaundryDues() - * onClickViewLaundryOrders → pos.viewLaundryOrders() - * - * Without this bridge, OWL throws - * "Invalid handler (expected a function, received: 'undefined')" - * at click time because the template references methods on the - * ControlButtons component that don't otherwise exist. - * - * Each handler closes the parent ControlButtons modal (props.close) - * after dispatching, mirroring core's behavior for its own buttons. - */ -import { patch } from "@web/core/utils/patch"; -import { ControlButtons } from "@point_of_sale/app/screens/product_screen/control_buttons/control_buttons"; - -patch(ControlButtons.prototype, { - onClickSettleDues() { - this.pos.settleLaundryDues(); - if (this.props.close) { - this.props.close(); - } - }, - - onClickViewLaundryOrders() { - this.pos.viewLaundryOrders(); - if (this.props.close) { - this.props.close(); - } - }, -}); diff --git a/addons/laundry_management/static/src/js/control_buttons_patch.js.bak b/addons/laundry_management/static/src/js/control_buttons_patch.js.bak deleted file mode 100644 index 2040609..0000000 --- a/addons/laundry_management/static/src/js/control_buttons_patch.js.bak +++ /dev/null @@ -1,18 +0,0 @@ -/** @odoo-module */ -import { patch } from "@web/core/utils/patch"; -import { ControlButtons } from "@point_of_sale/app/screens/product_screen/control_buttons/control_buttons"; - -patch(ControlButtons.prototype, { - onClickSettleDues() { - this.pos.settleLaundryDues(); - if (this.props.close) { - this.props.close(); - } - }, - onClickViewLaundryOrders() { - this.pos.viewLaundryOrders(); - if (this.props.close) { - this.props.close(); - } - }, -}); diff --git a/addons/laundry_management/static/src/js/laundry_context_store.js b/addons/laundry_management/static/src/js/laundry_context_store.js deleted file mode 100644 index 47305b5..0000000 --- a/addons/laundry_management/static/src/js/laundry_context_store.js +++ /dev/null @@ -1,72 +0,0 @@ -/** @odoo-module - * - * LaundryContextStore — small reactive store, owned by PosStore, that - * holds the per-order laundry context (type / attributes / delivery) - * keyed by order uuid. - * - * Why a separate store? - * The POS related-models engine wraps records in a field-aware proxy. - * Writing custom properties (not declared in _load_pos_data_fields) - * directly on the order does NOT trigger OWL reactivity, so the - * context panel never re-renders after the popup confirms. - * - * Exposing the fields via _load_pos_data_fields is proven to crash - * POS at boot (`lines is undefined` in _computeAllPrices), so the - * only safe path is to keep these values OUTSIDE the engine, in a - * plain OWL reactive object that components can subscribe to. - * - * Contract: - * pos.laundryContext.get(uuid) → reactive object with keys: - * { type_id, attribute_ids, - * is_delivery, delivery_address, - * delivery_scheduled_at } - * pos.laundryContext.set(uuid, patch) - * pos.laundryContext.clear(uuid) - * - * Persistence is independent: the same values are mirrored on the - * order as primitives (set in pos_store_patch.js) and shipped to the - * backend via PosOrder.serializeForORM (see pos_order_patch.js). - */ -import { reactive } from "@odoo/owl"; - -function emptyContext() { - return { - type_id: false, - attribute_ids: [], - is_delivery: false, - delivery_address: false, - delivery_scheduled_at: false, - }; -} - -export class LaundryContextStore { - constructor() { - this.byUuid = reactive({}); - } - - get(uuid) { - if (!uuid) { - return emptyContext(); - } - if (!this.byUuid[uuid]) { - this.byUuid[uuid] = emptyContext(); - } - return this.byUuid[uuid]; - } - - set(uuid, patch) { - if (!uuid || !patch) { - return; - } - const cur = this.get(uuid); - for (const key of Object.keys(patch)) { - cur[key] = patch[key]; - } - } - - clear(uuid) { - if (uuid && this.byUuid[uuid]) { - delete this.byUuid[uuid]; - } - } -} diff --git a/addons/laundry_management/static/src/js/laundry_order_context_panel.js b/addons/laundry_management/static/src/js/laundry_order_context_panel.js deleted file mode 100644 index 882ed0f..0000000 --- a/addons/laundry_management/static/src/js/laundry_order_context_panel.js +++ /dev/null @@ -1,117 +0,0 @@ -/** @odoo-module - * - * LaundryOrderContextPanel — premium card showing the order's - * legacy laundry context (type / attributes / delivery) with inline edit. - * - * Visible only when `pos.config.enable_laundry_order_type` is True. - * Reactive source: pos.laundryContext.get(order.uuid) - * (see laundry_context_store.js). Falls back to order primitives when - * the store has no entry yet (e.g. on order recall). - */ -import { Component } from "@odoo/owl"; -import { usePos } from "@point_of_sale/app/hooks/pos_hook"; - -export class LaundryOrderContextPanel extends Component { - static template = "laundry_management.LaundryOrderContextPanel"; - static props = {}; - - setup() { - this.pos = usePos(); - } - - get order() { - return this.pos.getOrder(); - } - - get isFeatureEnabled() { - return !!this.pos.config?.enable_laundry_order_type; - } - - /** Reactive context for the current order, with order-primitive fallback. */ - get ctx() { - const order = this.order; - if (!order) return null; - const live = this.pos.laundryContext?.get(order.uuid); - if (live && (live.type_id || live.attribute_ids?.length || live.is_delivery)) { - return live; - } - return { - type_id: order.laundry_order_type_id || false, - attribute_ids: order.laundry_order_attribute_ids || [], - is_delivery: !!order.laundry_is_delivery, - delivery_address: order.laundry_delivery_address || "", - delivery_scheduled_at: order.laundry_delivery_scheduled_at || "", - }; - } - - get orderType() { - const id = this.ctx?.type_id; - if (!id) return null; - return this.pos.models["laundry.order.type"]?.get(id) || null; - } - - get attributes() { - const ids = this.ctx?.attribute_ids || []; - const model = this.pos.models["laundry.order.attribute"]; - if (!model) return []; - return ids.map((id) => model.get(id)).filter(Boolean); - } - - get isDelivery() { - return !!this.ctx?.is_delivery; - } - - get deliveryAddress() { - const a = this.ctx?.delivery_address || ""; - if (!a) return ""; - return a.length > 64 ? a.slice(0, 61) + "…" : a; - } - - get deliveryScheduledAt() { - const v = this.ctx?.delivery_scheduled_at; - if (!v) return ""; - try { - const d = new Date(v.replace(" ", "T")); - if (!isNaN(d.getTime())) { - return d.toLocaleString(undefined, { - weekday: "short", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }); - } - } catch (_e) { - // ignore — fall back to raw value - } - return String(v); - } - - get hasContent() { - return !!(this.orderType || this.attributes.length || this.isDelivery); - } - - get typeStyle() { - const c = this.orderType?.color; - return c ? `background-color:${c};color:#fff;` : ""; - } - - attrStyle(attr) { - const c = attr?.color; - return c ? `background-color:${c};color:#fff;` : ""; - } - - typeIcon(type) { - if (!type) return "fa-tag"; - if (type.priority === "urgent") return "fa-bolt"; - if (type.is_delivery) return "fa-truck"; - return "fa-tag"; - } - - onClickEdit() { - if (!this.order) return; - // Bypasses the allow_change guard because the cashier explicitly - // clicked the panel's edit affordance. - this.pos.runLaundryOrderTypeFlow(this.order); - } -} diff --git a/addons/laundry_management/static/src/js/laundry_pricing_hook.js b/addons/laundry_management/static/src/js/laundry_pricing_hook.js deleted file mode 100644 index 1a93b7a..0000000 --- a/addons/laundry_management/static/src/js/laundry_pricing_hook.js +++ /dev/null @@ -1,47 +0,0 @@ -/** @odoo-module - * - * Pricing hook (PREPARE-ONLY). - * - * Reads the per-type and per-attribute extra_price values for the - * current order. Returned values are NEVER applied to the order. - * This module exists so future pricing logic can plug into a single - * stable surface without touching order.lines / pricing / taxes today. - * - * Usage example (NOT wired anywhere): - * import { computeLaundryExtras } from "@laundry_management/js/laundry_pricing_hook"; - * const { typeExtra, attributeExtras, total } = computeLaundryExtras(pos, order); - */ - -export function computeLaundryExtras(pos, order) { - const empty = { typeExtra: 0, attributeExtras: [], total: 0 }; - if (!pos || !order) return empty; - - const typeId = order.laundry_order_type_id; - const attrIds = Array.isArray(order.laundry_order_attribute_ids) - ? order.laundry_order_attribute_ids - : []; - - let typeExtra = 0; - if (typeId) { - const type = pos.models["laundry.order.type"]?.get(typeId); - typeExtra = Number(type?.extra_price) || 0; - } - - const attrModel = pos.models["laundry.order.attribute"]; - const attributeExtras = []; - let attrTotal = 0; - for (const id of attrIds) { - const rec = attrModel?.get(id); - const price = Number(rec?.extra_price) || 0; - if (price) { - attributeExtras.push({ id, name: rec?.name || "", price }); - attrTotal += price; - } - } - - return { - typeExtra, - attributeExtras, - total: typeExtra + attrTotal, - }; -} diff --git a/addons/laundry_management/static/src/js/laundry_receipt_details.js b/addons/laundry_management/static/src/js/laundry_receipt_details.js deleted file mode 100644 index ae79fa7..0000000 --- a/addons/laundry_management/static/src/js/laundry_receipt_details.js +++ /dev/null @@ -1,83 +0,0 @@ -/** @odoo-module - * - * LaundryReceiptDetails — read-only receipt block. - * Reads from pos.laundryContext (live) for fresh prints, falls back - * to the order's persisted primitives for re-prints / backend prints. - */ -import { Component } from "@odoo/owl"; -import { usePos } from "@point_of_sale/app/hooks/pos_hook"; - -export class LaundryReceiptDetails extends Component { - static template = "laundry_management.LaundryReceiptDetails"; - static props = { - order: { type: Object, optional: true }, - }; - - setup() { - this.pos = usePos(); - } - - get order() { - return this.props.order; - } - - /** Live context first, persisted primitives as fallback. */ - get ctx() { - const order = this.order; - if (!order) return null; - const live = this.pos?.laundryContext?.get(order.uuid); - if (live && (live.type_id || live.attribute_ids?.length || live.is_delivery)) { - return live; - } - return { - type_id: order.laundry_order_type_id || false, - attribute_ids: order.laundry_order_attribute_ids || [], - is_delivery: !!order.laundry_is_delivery, - delivery_address: order.laundry_delivery_address || "", - delivery_scheduled_at: order.laundry_delivery_scheduled_at || "", - }; - } - - get orderType() { - const id = this.ctx?.type_id; - if (!id) return null; - return this.pos?.models?.["laundry.order.type"]?.get(id) || null; - } - - get attributes() { - const ids = this.ctx?.attribute_ids || []; - const model = this.pos?.models?.["laundry.order.attribute"]; - if (!model) return []; - return ids.map((id) => model.get(id)).filter(Boolean); - } - - get isDelivery() { - return !!this.ctx?.is_delivery; - } - - get deliveryAddress() { - return this.ctx?.delivery_address || ""; - } - - get deliveryScheduledAt() { - const v = this.ctx?.delivery_scheduled_at; - if (!v) return ""; - try { - const d = new Date(v.replace(" ", "T")); - if (!isNaN(d.getTime())) { - return d.toLocaleString(); - } - } catch (_e) { - // ignore - } - return String(v); - } - - get isUrgent() { - return this.orderType?.priority === "urgent"; - } - - get hasContent() { - return !!(this.orderType || this.attributes.length || this.isDelivery); - } -} diff --git a/addons/laundry_management/static/src/js/laundry_settle_banner.js b/addons/laundry_management/static/src/js/laundry_settle_banner.js deleted file mode 100644 index 64d65b6..0000000 --- a/addons/laundry_management/static/src/js/laundry_settle_banner.js +++ /dev/null @@ -1,58 +0,0 @@ -/** @odoo-module - * - * LaundrySettleBanner — prominent visual indicator that POS is currently - * locked into settle-due mode. Provides the explicit "Exit" affordance - * and toggles a document.body class so global CSS can dim/disable - * irrelevant POS chrome (non-active order tabs, "+" button) for the - * duration of settle mode. - * - * Reactive source: order.uiState.is_laundry_settle_due (seeded in - * pos_order_patch.js initState so OWL tracks future writes). - */ -import { Component, useEffect } from "@odoo/owl"; -import { usePos } from "@point_of_sale/app/hooks/pos_hook"; - -const BODY_CLASS = "pos-laundry-settle-active"; - -export class LaundrySettleBanner extends Component { - static template = "laundry_management.LaundrySettleBanner"; - static props = {}; - - setup() { - this.pos = usePos(); - useEffect( - (active) => { - if (active) { - document.body.classList.add(BODY_CLASS); - return () => document.body.classList.remove(BODY_CLASS); - } - document.body.classList.remove(BODY_CLASS); - return () => {}; - }, - () => [this.isActive] - ); - } - - get order() { - return this.pos.getOrder(); - } - - get isActive() { - return this.pos.isSettleDueOrder(this.order); - } - - get partnerName() { - return this.order?.getPartner()?.name || ""; - } - - get amountLabel() { - const order = this.order; - if (!order) return ""; - const total = order.priceIncl ?? 0; - return this.pos.env.utils.formatCurrency(total); - } - - onClickExit() { - this.pos.exitSettleDueMode(); - } -} diff --git a/addons/laundry_management/static/src/js/laundry_thermal_receipt.js b/addons/laundry_management/static/src/js/laundry_thermal_receipt.js deleted file mode 100644 index 4a4df66..0000000 --- a/addons/laundry_management/static/src/js/laundry_thermal_receipt.js +++ /dev/null @@ -1,47 +0,0 @@ -/** @odoo-module - * - * LaundryWorkOrderThermal — receipt-style component for the POS printer. - * - * Rendered by `pos.printer.print(LaundryWorkOrderThermal, {data}, {webPrintFallback: true})`. - * The data payload comes from `laundry.order.pos_get_thermal_data` — - * a self-contained dict so this component never reads from the POS env. - * - * Styling lives in laundry_pos.scss under `.laundry-thermal`. The - * `webPrintFallback` option means this component also works when no - * hardware printer is configured: POS opens a print preview in a new - * tab using the same DOM. - */ -import { Component } from "@odoo/owl"; - -export class LaundryWorkOrderThermal extends Component { - static template = "laundry_management.LaundryWorkOrderThermal"; - static props = { - data: { type: Object }, - }; - - fmt(amount) { - return parseFloat(amount || 0).toFixed(2); - } - - fmtDate(s) { - if (!s) return ""; - try { - const d = new Date(s.replace(" ", "T") + "Z"); - if (!isNaN(d.getTime())) return d.toLocaleString(); - } catch (_e) { /* fall through */ } - return s; - } - - get currency() { - const sym = this.props.data?.currency_symbol || ""; - const pos = this.props.data?.currency_position || "after"; - return { sym, pos }; - } - - money(amount) { - const v = this.fmt(amount); - const { sym, pos } = this.currency; - if (!sym) return v; - return pos === "before" ? `${sym} ${v}` : `${v} ${sym}`; - } -} diff --git a/addons/laundry_management/static/src/js/navbar_patch.js b/addons/laundry_management/static/src/js/navbar_patch.js deleted file mode 100644 index cdbdbd7..0000000 --- a/addons/laundry_management/static/src/js/navbar_patch.js +++ /dev/null @@ -1,21 +0,0 @@ -/** @odoo-module - * - * Navbar patch — gate the POS-logo "register" click so cashiers can't - * silently abandon a settle-due order by clicking back to the home/ - * register screen. - * - * The original onClickRegister is sync; we make ours async and the - * existing OWL bindings still work (they fire-and-forget). - */ -import { patch } from "@web/core/utils/patch"; -import { Navbar } from "@point_of_sale/app/components/navbar/navbar"; - -patch(Navbar.prototype, { - async onClickRegister() { - const allowed = await this.pos.confirmExitSettleIfNeeded(null); - if (!allowed) { - return; - } - return super.onClickRegister(); - }, -}); diff --git a/addons/laundry_management/static/src/js/order_payment_validation.js b/addons/laundry_management/static/src/js/order_payment_validation.js deleted file mode 100644 index 8bea61e..0000000 --- a/addons/laundry_management/static/src/js/order_payment_validation.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @odoo-module - * - * ISOLATION STUB — original at order_payment_validation.js.bak - * Disabled to bisect _computeAllPrices crash. - */ diff --git a/addons/laundry_management/static/src/js/order_payment_validation.js.bak b/addons/laundry_management/static/src/js/order_payment_validation.js.bak deleted file mode 100644 index 829a48a..0000000 --- a/addons/laundry_management/static/src/js/order_payment_validation.js.bak +++ /dev/null @@ -1,31 +0,0 @@ -/** @odoo-module */ -import { patch } from "@web/core/utils/patch"; -import { _t } from "@web/core/l10n/translation"; -import OrderPaymentValidation from "@point_of_sale/app/utils/order_payment_validation"; -import { ask } from "@point_of_sale/app/utils/make_awaitable_dialog"; - -patch(OrderPaymentValidation.prototype, { - async askBeforeValidation() { - const result = await super.askBeforeValidation(); - if (result === false) { - return false; - } - // Only enforce customer for orders with laundry products - const hasLaundry = this.order.lines.some( - (line) => line.product_id?.product_tmpl_id?.is_laundry_service - ); - if (hasLaundry && !this.order.getPartner()) { - const confirmed = await ask(this.pos.dialog, { - title: _t("Customer Required"), - body: _t( - "This order contains laundry items. Please select a customer before validating." - ), - }); - if (confirmed) { - await this.pos.selectPartner(); - } - return false; - } - return true; - }, -}); diff --git a/addons/laundry_management/static/src/js/order_receipt_patch.js b/addons/laundry_management/static/src/js/order_receipt_patch.js deleted file mode 100644 index bd1e7d9..0000000 --- a/addons/laundry_management/static/src/js/order_receipt_patch.js +++ /dev/null @@ -1,14 +0,0 @@ -/** @odoo-module - * - * Register LaundryReceiptDetails as a child component of OrderReceipt - * so the receipt template can render . - * - * No prototype changes — additive component wiring only. - */ -import { OrderReceipt } from "@point_of_sale/app/screens/receipt_screen/receipt/order_receipt"; -import { LaundryReceiptDetails } from "@laundry_management/js/laundry_receipt_details"; - -OrderReceipt.components = { - ...(OrderReceipt.components || {}), - LaundryReceiptDetails, -}; diff --git a/addons/laundry_management/static/src/js/order_summary_patch.js b/addons/laundry_management/static/src/js/order_summary_patch.js deleted file mode 100644 index 8f6246d..0000000 --- a/addons/laundry_management/static/src/js/order_summary_patch.js +++ /dev/null @@ -1,34 +0,0 @@ -/** @odoo-module - * - * OrderSummary integration: - * 1. Register the LaundryOrderContextPanel as a child component. - * 2. Lock numpad-driven mutations (qty / discount / price / remove) - * while the active order is in settle-due mode. The settlement - * amount is fixed and must not be touched from the order summary. - */ -import { patch } from "@web/core/utils/patch"; -import { _t } from "@web/core/l10n/translation"; -import { OrderSummary } from "@point_of_sale/app/screens/product_screen/order_summary/order_summary"; -import { LaundryOrderContextPanel } from "@laundry_management/js/laundry_order_context_panel"; -import { LaundrySettleBanner } from "@laundry_management/js/laundry_settle_banner"; - -OrderSummary.components = { - ...(OrderSummary.components || {}), - LaundryOrderContextPanel, - LaundrySettleBanner, -}; - -patch(OrderSummary.prototype, { - async updateSelectedOrderline(args) { - const order = this.pos.getOrder(); - if (this.pos.isSettleDueOrder(order)) { - this.numberBuffer.reset(); - this.pos.notification.add( - _t("Settlement amount is fixed. Cannot modify from the cart."), - { type: "warning" } - ); - return; - } - return super.updateSelectedOrderline(args); - }, -}); diff --git a/addons/laundry_management/static/src/js/order_summary_patch.js.bak b/addons/laundry_management/static/src/js/order_summary_patch.js.bak deleted file mode 100644 index afc419f..0000000 --- a/addons/laundry_management/static/src/js/order_summary_patch.js.bak +++ /dev/null @@ -1,33 +0,0 @@ -/** @odoo-module */ -import { patch } from "@web/core/utils/patch"; -import { OrderSummary } from "@point_of_sale/app/screens/product_screen/order_summary/order_summary"; - -patch(OrderSummary.prototype, { - get laundryTypeSummary() { - const order = this.currentOrder; - if (!order) { - return null; - } - const type = order.laundry_order_type_id; - const attrs = order.laundry_order_attribute_ids || []; - if (!type && (!attrs || attrs.length === 0)) { - return null; - } - return { - typeName: type?.name || "", - typeColor: type?.color || "", - attributes: attrs.map((a) => ({ - id: a.id, - name: a.name, - color: a.color || "", - })), - isDelivery: !!order.laundry_is_delivery, - scheduledAt: order.laundry_delivery_scheduled_at || "", - canEdit: !!this.pos.config?.allow_change_laundry_order_type_before_payment, - }; - }, - - onClickEditLaundryType() { - this.pos.editLaundryOrderType(); - }, -}); diff --git a/addons/laundry_management/static/src/js/order_tabs_patch.js b/addons/laundry_management/static/src/js/order_tabs_patch.js deleted file mode 100644 index 2246ff0..0000000 --- a/addons/laundry_management/static/src/js/order_tabs_patch.js +++ /dev/null @@ -1,28 +0,0 @@ -/** @odoo-module - * - * OrderTabs patch — gate every tab-driven order switch and the "+" new - * order button through pos.confirmExitSettleIfNeeded so the cashier - * cannot silently leave a live settle-due order behind. - * - * Both methods stay async to align with their existing signatures. - */ -import { patch } from "@web/core/utils/patch"; -import { OrderTabs } from "@point_of_sale/app/components/order_tabs/order_tabs"; - -patch(OrderTabs.prototype, { - async selectFloatingOrder(order) { - const allowed = await this.pos.confirmExitSettleIfNeeded(order); - if (!allowed) { - return; - } - return super.selectFloatingOrder(order); - }, - - async newFloatingOrder() { - const allowed = await this.pos.confirmExitSettleIfNeeded(null); - if (!allowed) { - return; - } - return super.newFloatingOrder(); - }, -}); diff --git a/addons/laundry_management/static/src/js/payment_screen_patch.js b/addons/laundry_management/static/src/js/payment_screen_patch.js deleted file mode 100644 index 02f1600..0000000 --- a/addons/laundry_management/static/src/js/payment_screen_patch.js +++ /dev/null @@ -1,166 +0,0 @@ -/** @odoo-module - * - * PaymentScreen patch — minimal laundry behaviors: - * 1. Settlement orders: hide pay-later methods, custom validateOrder - * that calls settle_laundry_dues_rpc and bypasses normal sync. - * 2. Legacy laundry orders: trigger order-type popup at payment time - * (NOT on add-line). Block payment if type is required and missing. - * - * STRICT CONTRACT: - * - Does NOT touch order.lines, pricing, taxes - * - Does NOT override PosOrder.setup - * - Selections stored as primitives only (handled in pos_store_patch.js) - * - For non-laundry orders, super.validateOrder runs unchanged. - */ -import { patch } from "@web/core/utils/patch"; -import { _t } from "@web/core/l10n/translation"; -import { PaymentScreen } from "@point_of_sale/app/screens/payment_screen/payment_screen"; -import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; -import { LaundrySettlementReceipt } from "@laundry_management/js/settlement_receipt"; - -function isSettlementOrder(order) { - return ( - order && - order.lines && - order.lines.some( - (line) => line.product_id?.product_tmpl_id?.is_laundry_settlement - ) - ); -} - -patch(PaymentScreen.prototype, { - setup() { - super.setup(...arguments); - // For settlement orders, hide Customer Account / pay-later methods. - if (isSettlementOrder(this.currentOrder)) { - this.payment_methods_from_config = - this.payment_methods_from_config.filter( - (pm) => !pm.split_transactions - ); - } - }, - - /** - * Pre-payment gate for the legacy laundry order-type flow. - * - true → continue with payment - * - false → block payment - * - * Skipped for: feature disabled, settlement orders, non-laundry orders, - * and orders that already have a type. If required + missing, opens the - * popup; blocks if the user closes/cancels. - */ - async beforePaymentFlow(order) { - const pos = this.pos; - const config = pos.config; - if (!order || !config?.enable_laundry_order_type) { - return true; - } - if (isSettlementOrder(order)) { - return true; - } - if (!pos.orderHasLaundryServiceLine(order)) { - return true; - } - if (order.laundry_order_type_id) { - return true; - } - await pos.runLaundryOrderTypeFlow(order); - if (config.require_laundry_order_type && !order.laundry_order_type_id) { - pos.notification.add( - _t("Order type is required to proceed to payment."), - { type: "warning" } - ); - return false; - } - return true; - }, - - async validateOrder(isForceValidate) { - const order = this.currentOrder; - - // ── Settlement validation — bypass normal POS sync ── - if (isSettlementOrder(order)) { - return this._validateSettlementOrder(order); - } - - // ── Legacy laundry order-type gate ── - const allowed = await this.beforePaymentFlow(order); - if (!allowed) { - return false; - } - - return super.validateOrder(isForceValidate); - }, - - async _validateSettlementOrder(order) { - const paymentLines = order.payment_ids - .filter((pl) => pl.getAmount() > 0) - .map((pl) => ({ - pos_payment_method_id: pl.payment_method_id.id, - amount: pl.getAmount(), - })); - - if (paymentLines.length === 0) { - this.notification.add( - _t("Please add at least one payment."), - { type: "warning" } - ); - return false; - } - - const partnerId = order.partner_id?.id; - if (!partnerId) { - this.notification.add(_t("No customer selected."), { - type: "danger", - }); - return false; - } - - const sessionId = this.pos.session?.id || null; - - let response; - try { - this.ui.block(); - response = await this.pos.data.call( - "res.partner", - "settle_laundry_dues_rpc", - [partnerId, paymentLines, sessionId] - ); - } catch (err) { - this.dialog.add(AlertDialog, { - title: _t("Settlement Failed"), - body: - err?.data?.message || - err?.message || - _t("Unknown error."), - }); - return false; - } finally { - this.ui.unblock(); - } - - if ( - order.payment_ids.some( - (pl) => pl.payment_method_id.is_cash_count && pl.getAmount() > 0 - ) - ) { - this.pos.hardwareProxy.openCashbox(); - } - - this.dialog.add(LaundrySettlementReceipt, { - partnerName: order.partner_id.name, - settledTotal: response.settled_total || 0, - remainingDue: response.remaining_due || 0, - payments: response.payments || [], - settledOrders: response.settled_orders || [], - }); - - this.pos._lastSettlementMethodId = - paymentLines[0]?.pos_payment_method_id || null; - - this.pos.removeOrder(order, false); - this.pos.addNewOrder(); - this.pos.navigate("ProductScreen"); - return true; - }, -}); diff --git a/addons/laundry_management/static/src/js/payment_screen_patch.js.bak b/addons/laundry_management/static/src/js/payment_screen_patch.js.bak deleted file mode 100644 index 5dbf9ce..0000000 --- a/addons/laundry_management/static/src/js/payment_screen_patch.js.bak +++ /dev/null @@ -1,115 +0,0 @@ -/** @odoo-module */ -import { patch } from "@web/core/utils/patch"; -import { _t } from "@web/core/l10n/translation"; -import { PaymentScreen } from "@point_of_sale/app/screens/payment_screen/payment_screen"; -import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; -import { LaundrySettlementReceipt } from "@laundry_management/js/settlement_receipt"; - -/** - * Check whether a POS order is a settlement order (contains - * the special settlement product, NOT a laundry service). - */ -function isSettlementOrder(order) { - return ( - order && - order.lines && - order.lines.some( - (line) => line.product_id?.product_tmpl_id?.is_laundry_settlement - ) - ); -} - -patch(PaymentScreen.prototype, { - setup() { - super.setup(...arguments); - // For settlement orders, hide Customer Account / pay-later methods - if (isSettlementOrder(this.currentOrder)) { - this.payment_methods_from_config = - this.payment_methods_from_config.filter( - (pm) => !pm.split_transactions - ); - } - }, - - async validateOrder(isForceValidate) { - const order = this.currentOrder; - if (!isSettlementOrder(order)) { - return super.validateOrder(isForceValidate); - } - - // ── Settlement validation — bypass normal POS sync ── - const paymentLines = order.payment_ids - .filter((pl) => pl.getAmount() > 0) - .map((pl) => ({ - pos_payment_method_id: pl.payment_method_id.id, - amount: pl.getAmount(), - })); - - if (paymentLines.length === 0) { - this.notification.add( - _t("Please add at least one payment."), - { type: "warning" } - ); - return false; - } - - const partnerId = order.partner_id?.id; - if (!partnerId) { - this.notification.add(_t("No customer selected."), { - type: "danger", - }); - return false; - } - - const sessionId = this.pos.session?.id || null; - - let response; - try { - this.ui.block(); - response = await this.pos.data.call( - "res.partner", - "settle_laundry_dues_rpc", - [partnerId, paymentLines, sessionId] - ); - } catch (err) { - this.dialog.add(AlertDialog, { - title: _t("Settlement Failed"), - body: - err?.data?.message || - err?.message || - _t("Unknown error."), - }); - return false; - } finally { - this.ui.unblock(); - } - - // Open cash drawer if cash was used - if ( - order.payment_ids.some( - (pl) => pl.payment_method_id.is_cash_count && pl.getAmount() > 0 - ) - ) { - this.pos.hardwareProxy.openCashbox(); - } - - // Show settlement receipt - this.dialog.add(LaundrySettlementReceipt, { - partnerName: order.partner_id.name, - settledTotal: response.settled_total || 0, - remainingDue: response.remaining_due || 0, - payments: response.payments || [], - settledOrders: response.settled_orders || [], - }); - - // Remember last-used method for next settlement - this.pos._lastSettlementMethodId = - paymentLines[0]?.pos_payment_method_id || null; - - // Clean up: remove settlement order (never synced to backend) - this.pos.removeOrder(order, false); - this.pos.addNewOrder(); - this.pos.navigate("ProductScreen"); - return true; - }, -}); diff --git a/addons/laundry_management/static/src/js/popups/laundry_delivery_details_popup.js b/addons/laundry_management/static/src/js/popups/laundry_delivery_details_popup.js deleted file mode 100644 index 07ca5b3..0000000 --- a/addons/laundry_management/static/src/js/popups/laundry_delivery_details_popup.js +++ /dev/null @@ -1,59 +0,0 @@ -/** @odoo-module */ -import { Component, useState, useRef, onMounted } from "@odoo/owl"; -import { Dialog } from "@web/core/dialog/dialog"; -import { _t } from "@web/core/l10n/translation"; - -export class LaundryDeliveryDetailsPopup extends Component { - static components = { Dialog }; - static template = "laundry_management.LaundryDeliveryDetailsPopup"; - static props = { - defaultAddress: { type: String, optional: true }, - defaultScheduledAt: { type: String, optional: true }, - requireAddress: { type: Boolean, optional: true }, - requireScheduledTime: { type: Boolean, optional: true }, - title: { type: String, optional: true }, - getPayload: { type: Function }, - close: { type: Function }, - }; - static defaultProps = { - defaultAddress: "", - defaultScheduledAt: "", - requireAddress: true, - requireScheduledTime: false, - title: _t("Delivery Details"), - }; - - setup() { - this.state = useState({ - address: this.props.defaultAddress || "", - scheduledAt: this.props.defaultScheduledAt || "", - addressError: false, - timeError: false, - }); - this.addressRef = useRef("addressInput"); - onMounted(() => { - if (this.addressRef.el) { - this.addressRef.el.focus(); - } - }); - } - - confirm() { - this.state.addressError = - this.props.requireAddress && !this.state.address.trim(); - this.state.timeError = - this.props.requireScheduledTime && !this.state.scheduledAt; - if (this.state.addressError || this.state.timeError) { - return; - } - this.props.getPayload({ - address: this.state.address.trim(), - scheduledAt: this.state.scheduledAt || false, - }); - this.props.close(); - } - - cancel() { - this.props.close(); - } -} diff --git a/addons/laundry_management/static/src/js/popups/laundry_order_attribute_popup.js b/addons/laundry_management/static/src/js/popups/laundry_order_attribute_popup.js deleted file mode 100644 index c9abbce..0000000 --- a/addons/laundry_management/static/src/js/popups/laundry_order_attribute_popup.js +++ /dev/null @@ -1,51 +0,0 @@ -/** @odoo-module */ -import { Component, useState } from "@odoo/owl"; -import { Dialog } from "@web/core/dialog/dialog"; -import { _t } from "@web/core/l10n/translation"; - -export class LaundryOrderAttributePopup extends Component { - static components = { Dialog }; - static template = "laundry_management.LaundryOrderAttributePopup"; - static props = { - attributes: { type: Array }, - preselectedIds: { type: Array, optional: true }, - title: { type: String, optional: true }, - getPayload: { type: Function }, - close: { type: Function }, - }; - static defaultProps = { - preselectedIds: [], - title: _t("Select Attributes"), - }; - - setup() { - const initial = new Set(this.props.preselectedIds); - this.state = useState({ - selected: initial, - }); - } - - toggle(attrId) { - if (this.state.selected.has(attrId)) { - this.state.selected.delete(attrId); - } else { - this.state.selected.add(attrId); - } - } - - isSelected(attrId) { - return this.state.selected.has(attrId); - } - - confirm() { - const ids = [...this.state.selected]; - const chosen = this.props.attributes.filter((a) => ids.includes(a.id)); - this.props.getPayload(chosen); - this.props.close(); - } - - skip() { - this.props.getPayload([]); - this.props.close(); - } -} diff --git a/addons/laundry_management/static/src/js/popups/laundry_order_type_popup.js b/addons/laundry_management/static/src/js/popups/laundry_order_type_popup.js deleted file mode 100644 index 151ee81..0000000 --- a/addons/laundry_management/static/src/js/popups/laundry_order_type_popup.js +++ /dev/null @@ -1,51 +0,0 @@ -/** @odoo-module */ -import { Component, useState } from "@odoo/owl"; -import { Dialog } from "@web/core/dialog/dialog"; -import { _t } from "@web/core/l10n/translation"; - -export class LaundryOrderTypePopup extends Component { - static components = { Dialog }; - static template = "laundry_management.LaundryOrderTypePopup"; - static props = { - types: { type: Array }, - defaultTypeId: { type: [Number, Boolean], optional: true }, - showIcons: { type: Boolean, optional: true }, - allowSkip: { type: Boolean, optional: true }, - title: { type: String, optional: true }, - getPayload: { type: Function }, - close: { type: Function }, - }; - static defaultProps = { - showIcons: true, - allowSkip: true, - title: _t("Select Order Type"), - }; - - setup() { - this.state = useState({ - selectedId: this.props.defaultTypeId || false, - }); - } - - select(typeId) { - this.state.selectedId = typeId; - } - - confirm() { - if (!this.state.selectedId && !this.props.allowSkip) { - return; - } - const chosen = this.props.types.find((t) => t.id === this.state.selectedId); - this.props.getPayload(chosen || null); - this.props.close(); - } - - skip() { - this.props.getPayload(null); - this.props.close(); - } - - cancel() { - this.props.close(); - } -} diff --git a/addons/laundry_management/static/src/js/pos_order_patch.js b/addons/laundry_management/static/src/js/pos_order_patch.js deleted file mode 100644 index a61a5bc..0000000 --- a/addons/laundry_management/static/src/js/pos_order_patch.js +++ /dev/null @@ -1,133 +0,0 @@ -/** @odoo-module - * - * PosOrder patch — laundry context fields. - * - * Two responsibilities: - * 1. setup(): seed primitive defaults on the reactive proxy so that - * OWL components (panel, receipt) which read these slots track - * future writes and re-render. Without seeding, assigning a fresh - * property after first render does NOT trigger reactivity. - * No fields are pulled from the loader — pure in-memory defaults. - * - * 2. serializeForORM(): inject the laundry slots into the sync_from_ui - * payload so the backend pos.order columns are populated, exactly - * like `general_customer_note`. - * - laundry_order_type_id → integer id (Many2one) - * - laundry_order_attribute_ids → [[6, 0, ids]] (Many2many "set") - * - laundry_is_delivery → boolean - * - laundry_delivery_address → string | false - * - laundry_delivery_scheduled_at → string | false - * - * NOTE: this patch does NOT touch order.lines, pricing, taxes, or any - * relational field exposed by _load_pos_data_fields. - */ -import { patch } from "@web/core/utils/patch"; -import { PosOrder } from "@point_of_sale/app/models/pos_order"; - -function pickId(v) { - if (!v) return false; - if (typeof v === "number") return v; - if (typeof v === "object" && Number.isInteger(v.id)) return v.id; - return false; -} - -function pickIds(arr) { - if (!Array.isArray(arr)) return []; - const out = []; - const seen = new Set(); - for (const v of arr) { - const id = pickId(v); - if (id && !seen.has(id)) { - seen.add(id); - out.push(id); - } - } - return out; -} - -function normalizeDateTime(value) { - if (!value || typeof value !== "string") return false; - - let v = value; - - // Remove timezone like +03:00 or -03:00 - const tzMatch = v.match(/([+-]\d{2}:\d{2})$/); - if (tzMatch) { - v = v.replace(tzMatch[0], ""); - } - - // Remove Z - if (v.endsWith("Z")) { - v = v.slice(0, -1); - } - - // Remove milliseconds - if (v.includes(".")) { - v = v.split(".")[0]; - } - - // Replace T with space - v = v.replace("T", " "); - - // Ensure seconds exist - if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(v)) { - v += ":00"; - } - - return v; -} - -patch(PosOrder.prototype, { - setup(...args) { - super.setup(...args); - // Seed defaults on the reactive proxy. Assigning after first - // render only triggers OWL reactivity if the key already exists - // on the proxy at observation time. - if (this.laundry_order_type_id === undefined) { - this.laundry_order_type_id = false; - } - if (this.laundry_order_attribute_ids === undefined) { - this.laundry_order_attribute_ids = []; - } - if (this.laundry_is_delivery === undefined) { - this.laundry_is_delivery = false; - } - if (this.laundry_delivery_address === undefined) { - this.laundry_delivery_address = false; - } - if (this.laundry_delivery_scheduled_at === undefined) { - this.laundry_delivery_scheduled_at = false; - } - }, - - initState(...args) { - super.initState(...args); - // Reactive flag for POS Mode system: when true, the order is a - // dedicated settlement context — products cannot be added, - // quantities cannot be edited, line cannot be deleted. - // Seeded here so OWL reactivity tracks future writes. - if (this.uiState && !("is_laundry_settle_due" in this.uiState)) { - this.uiState.is_laundry_settle_due = false; - } - }, - - serializeForORM(opts = {}) { - const data = super.serializeForORM(opts); - - const typeId = pickId(this.laundry_order_type_id); - data.laundry_order_type_id = typeId || false; - - const attrIds = pickIds(this.laundry_order_attribute_ids); - data.laundry_order_attribute_ids = [[6, 0, attrIds]]; - - data.laundry_is_delivery = !!this.laundry_is_delivery; - data.laundry_delivery_address = this.laundry_delivery_address || false; - // ABSOLUTE GUARANTEE: nothing with a "T", ".000", or timezone reaches - // the backend. Run the full normalizer even if upstream already did; - // normalize(normalize(x)) === normalize(x), so this is a zero-cost net. - data.laundry_delivery_scheduled_at = - normalizeDateTime(this.laundry_delivery_scheduled_at) || false; - - return data; - }, -}); diff --git a/addons/laundry_management/static/src/js/pos_order_patch.js.bak b/addons/laundry_management/static/src/js/pos_order_patch.js.bak deleted file mode 100644 index a592a82..0000000 --- a/addons/laundry_management/static/src/js/pos_order_patch.js.bak +++ /dev/null @@ -1,83 +0,0 @@ -/** @odoo-module - * - * PosOrder JS-side patch for the laundry order-type / attributes feature. - * - * Why this patch exists: - * The two relational laundry fields on `pos.order` - * - laundry_order_type_id (Many2one → laundry.order.type) - * - laundry_order_attribute_ids (Many2many → laundry.order.attribute) - * are NOT exposed via _load_pos_data_fields anymore. Including them was - * breaking the POS relational engine during order initialization - * (TypeError: lines is undefined → _computeAllPrices crash). - * - * So instead of round-tripping them through the relational engine, we: - * 1. Store them as plain JS attributes on the order instance - * (assigned by runLaundryOrderTypeFlow in pos_store_patch.js). - * 2. Inject them into the sync_from_ui payload via serializeForORM - * as standard ORM commands (integer id for M2O, [(6,0,ids)] for M2M). - * 3. The backend pos.order columns still exist; sync writes the values - * normally and _maybe_create_laundry_order propagates them onward. - * - * The 3 SCALAR laundry fields (laundry_is_delivery, ..._address, - * ..._scheduled_at) remain in _load_pos_data_fields and round-trip - * normally through the relational engine. We only set safe defaults - * for them here so consumers never hit `undefined`. - * - * Setup runs with (...args) so super gets the exact arguments the - * framework passed, and all writes happen AFTER super.setup so we never - * interfere with base initialization. - */ -import { patch } from "@web/core/utils/patch"; -import { PosOrder } from "@point_of_sale/app/models/pos_order"; - -patch(PosOrder.prototype, { - setup(...args) { - super.setup(...args); - - // Scalar field defaults (these ARE in _load_pos_data_fields). - if (this.laundry_is_delivery === undefined) { - this.laundry_is_delivery = false; - } - if (this.laundry_delivery_address === undefined) { - this.laundry_delivery_address = false; - } - if (this.laundry_delivery_scheduled_at === undefined) { - this.laundry_delivery_scheduled_at = false; - } - - // Relational field shadows (NOT in _load_pos_data_fields — pure JS). - // Plain attributes only, never relational records, so the engine - // does not see them as relational writes. - if (this.laundry_order_type_id === undefined) { - this.laundry_order_type_id = false; - } - if (this.laundry_order_attribute_ids === undefined) { - this.laundry_order_attribute_ids = []; - } - }, - - serializeForORM(opts = {}) { - const data = super.serializeForORM(opts); - - // Many2one → just the integer id (or false to clear). - const typeRec = this.laundry_order_type_id; - if (typeRec && typeof typeRec === "object" && typeRec.id) { - data.laundry_order_type_id = typeRec.id; - } else if (typeof typeRec === "number") { - data.laundry_order_type_id = typeRec; - } else { - data.laundry_order_type_id = false; - } - - // Many2many → standard ORM "set" command [(6, 0, [ids])]. - const attrs = Array.isArray(this.laundry_order_attribute_ids) - ? this.laundry_order_attribute_ids - : []; - const attrIds = attrs - .map((a) => (a && typeof a === "object" ? a.id : a)) - .filter((id) => Number.isInteger(id)); - data.laundry_order_attribute_ids = [[6, 0, attrIds]]; - - return data; - }, -}); diff --git a/addons/laundry_management/static/src/js/pos_store_patch.js b/addons/laundry_management/static/src/js/pos_store_patch.js deleted file mode 100644 index 5a75e03..0000000 --- a/addons/laundry_management/static/src/js/pos_store_patch.js +++ /dev/null @@ -1,807 +0,0 @@ -/** @odoo-module - * - * PosStore patch — laundry order type / attributes / delivery popup chain. - * - * Two storage paths kept in sync: - * 1. pos.laundryContext (OWL reactive) — drives panel + receipt re-render. - * 2. order.laundry_* primitives — drives serializeForORM payload. - * - * STRICT CONTRACT: - * - Does NOT override PosOrder.setup beyond seeding scalar defaults - * (see pos_order_patch.js) - * - Does NOT touch order.lines, pricing, or taxes - * - Does NOT push laundry fields through _load_pos_data_fields - */ -import { patch } from "@web/core/utils/patch"; -import { _t } from "@web/core/l10n/translation"; -import { PosStore } from "@point_of_sale/app/services/pos_store"; -import { ask, makeAwaitable } from "@point_of_sale/app/utils/make_awaitable_dialog"; -import { LaundryContextStore } from "@laundry_management/js/laundry_context_store"; -import { LaundryQuickCreatePartner } from "@laundry_management/js/quick_create_partner"; -import { LaundryOrdersViewPopup } from "@laundry_management/js/view_laundry_orders"; -import { LaundryOrderTypePopup } from "@laundry_management/js/popups/laundry_order_type_popup"; -import { LaundryOrderAttributePopup } from "@laundry_management/js/popups/laundry_order_attribute_popup"; -import { LaundryDeliveryDetailsPopup } from "@laundry_management/js/popups/laundry_delivery_details_popup"; - -function hasLaundryProduct(order) { - return order.lines.some( - (line) => line.product_id?.product_tmpl_id?.is_laundry_service - ); -} - -function isLaundryServiceLine(line) { - const tmpl = line.product_id?.product_tmpl_id; - return tmpl?.is_laundry_service && !tmpl?.is_laundry_settlement; -} - -function toId(v) { - if (!v) return false; - if (typeof v === "number") return v; - if (typeof v === "object" && Number.isInteger(v.id)) return v.id; - return false; -} - -function toIds(arr) { - if (!Array.isArray(arr)) return []; - const seen = new Set(); - for (const v of arr) { - const id = toId(v); - if (id) seen.add(id); - } - return [...seen]; -} - -function normalizeDateTime(value) { - if (!value || typeof value !== "string") return false; - - let v = value; - - // Remove timezone like +03:00 or -03:00 - const tzMatch = v.match(/([+-]\d{2}:\d{2})$/); - if (tzMatch) { - v = v.replace(tzMatch[0], ""); - } - - // Remove Z - if (v.endsWith("Z")) { - v = v.slice(0, -1); - } - - // Remove milliseconds - if (v.includes(".")) { - v = v.split(".")[0]; - } - - // Replace T with space - v = v.replace("T", " "); - - // Ensure seconds exist - if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(v)) { - v += ":00"; - } - - return v; -} - -patch(PosStore.prototype, { - setup(...args) { - const result = super.setup(...args); - if (!this.laundryContext) { - this.laundryContext = new LaundryContextStore(); - } - if (this._settleDuesInFlight === undefined) { - this._settleDuesInFlight = false; - } - return result; - }, - - /** - * Defensive override for the core configurator helper. - * - * In Odoo 19 core, `doHaveConflictWith` accesses `value.id` and - * `selectedValues.map(v => v.id)` with no null-check. When the - * loaded POS data has even ONE incomplete PTAV reference (most - * commonly after a partial product re-seed, attribute rename, or - * orphan ir_model_data row), the configurator render throws: - * - * TypeError: can't access property "id", value is undefined - * at doHaveConflictWith - * - * The exception leaves the POS in an unrecoverable state mid-popup - * and the cashier can't add the laundry product at all. - * - * This guard filters out null/undefined entries and short-circuits - * to "no conflict" when `value` itself is malformed. It changes - * NO business behavior — exclusions still apply for valid PTAVs. - */ - doHaveConflictWith(value, selectedValues) { - if (!value || value.id === undefined) { - return false; - } - const safeSelected = Array.isArray(selectedValues) - ? selectedValues.filter((v) => v && v.id !== undefined) - : []; - return super.doHaveConflictWith(value, safeSelected); - }, - - // ─── POS Mode (settle_due isolation) ────────────────────────────────── - isSettleDueOrder(order) { - return !!order?.uiState?.is_laundry_settle_due; - }, - - /** Reactive accessor for templates: "sale" | "settle_due". */ - get currentLaundryMode() { - return this.isSettleDueOrder(this.getOrder()) ? "settle_due" : "sale"; - }, - - _markSettleDue(order) { - if (order?.uiState) { - order.uiState.is_laundry_settle_due = true; - } - }, - - _clearSettleDue(order) { - if (order?.uiState) { - order.uiState.is_laundry_settle_due = false; - } - }, - - /** - * Cashier-initiated exit from settle mode. - * ALWAYS drops the settlement order AND creates a fresh clean sale - * order — never leaves the POS in a zero-order state. - * - * Confirmation tone differs based on what the cashier has staged: - * • No payment lines yet → light confirm ("Cancel settlement?"). - * • Amount already entered → stronger confirm warning that the - * payment lines on this settle order will be discarded. - * Cleanup is the same in both cases — the settle order is removed - * locally so any payment lines on it go with it. - */ - async exitSettleDueMode() { - const order = this.getOrder(); - if (!this.isSettleDueOrder(order)) { - return; - } - const hasPaymentLines = (order.payment_ids || []).some( - (p) => (p.amount || 0) > 0 - ); - const dialogProps = hasPaymentLines - ? { - title: _t("Discard settlement payment?"), - body: _t( - "Settlement has payment lines entered. Leaving now " - + "discards them. Continue?" - ), - confirmLabel: _t("Discard & Exit"), - cancelLabel: _t("Stay"), - } - : { - title: _t("Cancel settlement and return to POS?"), - body: _t( - "No payment entered yet. The settlement will be " - + "cancelled and you will return to the product screen." - ), - confirmLabel: _t("Exit"), - cancelLabel: _t("Stay"), - }; - const confirmed = await ask(this.env.services.dialog, dialogProps); - if (!confirmed) { - return; - } - this._clearSettleDue(order); - this.removeOrder(order, false); - // Always hand the cashier a clean new sale order — no stranded state. - this.addNewOrder(); - this.navigate("ProductScreen"); - }, - - /** - * Nuke every open order in the POS except an optional keeper. Used - * when entering settle-due mode to enforce single-order isolation. - * - * DATA-LOSS PROTECTION: if any of the to-be-removed orders still has - * lines, prompt ONCE before discarding. Returns true if the removal - * completed, false if the cashier cancelled. - */ - async _removeAllOpenOrders({ except = null } = {}) { - const keepUuid = except?.uuid || null; - const snapshot = this.getOpenOrders().slice(); - const doomed = snapshot.filter((o) => o.uuid !== keepUuid); - if (!doomed.length) { - return true; - } - const hasLines = doomed.some((o) => (o.lines?.length || 0) > 0); - if (hasLines) { - const confirmed = await ask(this.env.services.dialog, { - title: _t("Discard Open Orders?"), - body: _t( - "Starting a settlement will discard %s open order(s) that still contain items. Continue?", - doomed.length - ), - confirmLabel: _t("Continue"), - cancelLabel: _t("Cancel"), - }); - if (!confirmed) { - return false; - } - } - for (const o of doomed) { - this._clearSettleDue(o); - this.removeOrder(o, false); - } - return true; - }, - - /** - * Navigation gate — every cashier action that would switch the active - * order context (tab click, "+" new order, navbar register click) - * must funnel through this helper FIRST. - * - * targetOrder = null → "create new order" intent - * targetOrder = some open order → "switch to that order" intent - * - * Returns true if the navigation should proceed, false if blocked. - * If the user confirms the exit, the active settle order is dropped - * here so callers can proceed without further cleanup. - */ - async confirmExitSettleIfNeeded(targetOrder) { - const current = this.getOrder(); - if (!this.isSettleDueOrder(current)) { - return true; - } - // Switching to the same settle order itself is a no-op for safety. - if (targetOrder && targetOrder.uuid === current.uuid) { - return true; - } - const confirmed = await ask(this.env.services.dialog, { - title: _t("Exit Settle Dues Mode?"), - body: _t( - "You are settling dues for %s. Leaving will cancel the current settlement.", - current.getPartner()?.name || _t("a customer") - ), - confirmLabel: _t("Exit"), - cancelLabel: _t("Cancel"), - }); - if (!confirmed) { - return false; - } - this._clearSettleDue(current); - this.removeOrder(current, false); - return true; - }, - - /** - * Hard guard: while a settle-due order is active, the only line that - * can land on it is the settlement product itself. Any other product - * click is silently rejected with a notification. - */ - async addLineToCurrentOrder(vals, opts = {}, configure = true) { - const order = this.getOrder(); - if (this.isSettleDueOrder(order)) { - const tmplVal = vals?.product_tmpl_id; - const tmpl = typeof tmplVal === "number" - ? this.data.models["product.template"]?.get(tmplVal) - : tmplVal; - if (!tmpl?.is_laundry_settlement) { - this.notification.add( - _t("Cannot add products while settling dues. Exit settle mode first."), - { type: "warning" } - ); - return null; - } - } - // Laundry configuration now runs through the CORE Odoo configurator - // (ProductConfiguratorPopup). We don't intercept here — core's - // handleConfigurableProduct / openConfigurator fires naturally for - // any template whose attributes expose >1 value. We only enhance - // the popup's presentation via XML inheritance + scoped SCSS - // (see xml/product_configurator_popup.xml). - return super.addLineToCurrentOrder(vals, opts, configure); - }, - - /** - * HARD BLOCK: once a settle-due order is active, no new orders can - * be created. No confirm — just stop. Any path that tries to create - * a new order during settle mode is either a mistake or an escape - * route we haven't caught upstream; either way, block it. - */ - addNewOrder(data = {}) { - if (this.isSettleDueOrder(this.getOrder())) { - this.notification.add( - _t("Finish settlement first."), - { type: "warning" } - ); - return null; - } - return super.addNewOrder(data); - }, - - /** - * HARD CONTROL on order switching. If the active order is settle-due - * and the target is a different order: - * - ask confirmation - * - cancel → block the switch - * - confirm → drop the settle order cleanly, then proceed - * - * The function becomes async. Native sync callers (e.g. TicketScreen) - * do not await, but the user-facing UI gates (OrderTabs.selectFloating- - * Order, Navbar.onClickRegister) run confirmExitSettleIfNeeded first - * and clear settle mode before super.setOrder runs, so this patch is - * defense-in-depth for any escape route that bypasses those gates. - */ - async setOrder(order) { - const current = this.getOrder(); - if ( - current && - this.isSettleDueOrder(current) && - order && - order.uuid !== current.uuid - ) { - const confirmed = await ask(this.env.services.dialog, { - title: _t("Exit Settle Dues Mode?"), - body: _t("The current settlement will be cancelled."), - confirmLabel: _t("Exit"), - cancelLabel: _t("Cancel"), - }); - if (!confirmed) { - return; - } - this._clearSettleDue(current); - this.removeOrder(current, false); - } - return super.setOrder(order); - }, - - /** Always release settle-state when an order is dropped (success OR exit). */ - removeOrder(order, removeFromServer = true) { - if (order?.uiState) { - order.uiState.is_laundry_settle_due = false; - } - return super.removeOrder(order, removeFromServer); - }, - - /** - * Fail-safe navigation: while a settle-due order is active, the ONLY - * legitimate screen is PaymentScreen. Any other navigation target is - * rewritten to PaymentScreen so the cashier never lands on a screen - * that would corrupt the settlement context (no blank screen, no - * stranded products on the wrong order). - * - * BUT: when the cashier presses Back (PaymentScreen → ProductScreen), - * silently snapping them back used to feel like a trap. We now also - * SCHEDULE the existing exit-confirmation dialog on the next - * microtask. The cashier sees PaymentScreen for one frame, then a - * clear "Cancel settlement?" dialog. Confirming → settle order - * removed, fresh sale order, ProductScreen. Cancelling → still on - * PaymentScreen, can keep settling. No new buttons, no XML changes. - * - * Legit exit paths (exitSettleDueMode itself, removeOrder during - * payment success) clear the settle flag BEFORE calling navigate, - * so they pass through unaffected and never trigger the prompt. - */ - navigate(routeName, routeParams = {}) { - const current = this.getOrder(); - if ( - this.isSettleDueOrder(current) && - routeName !== "PaymentScreen" - ) { - // Schedule exit-confirmation but don't await — keep navigate - // sync so callers that don't await still work cleanly. - Promise.resolve().then(() => this.exitSettleDueMode()); - return super.navigate("PaymentScreen", { orderUuid: current.uuid }); - } - return super.navigate(routeName, routeParams); - }, - - /** Mirror a patch into BOTH the reactive store and the order primitives. */ - _writeLaundryContext(order, patchObj) { - if (!order) { - return; - } - // Reactive store — drives panel + receipt re-render. - this.laundryContext.set(order.uuid, patchObj); - // Order primitives — drive serializeForORM payload. - if ("type_id" in patchObj) { - order.laundry_order_type_id = patchObj.type_id || false; - } - if ("attribute_ids" in patchObj) { - order.laundry_order_attribute_ids = Array.isArray(patchObj.attribute_ids) - ? patchObj.attribute_ids.slice() - : []; - } - if ("is_delivery" in patchObj) { - order.laundry_is_delivery = !!patchObj.is_delivery; - } - if ("delivery_address" in patchObj) { - order.laundry_delivery_address = patchObj.delivery_address || false; - } - if ("delivery_scheduled_at" in patchObj) { - order.laundry_delivery_scheduled_at = - patchObj.delivery_scheduled_at || false; - } - }, - - async postSyncAllOrders(orders) { - await super.postSyncAllOrders(orders); - for (const order of orders) { - if (order.laundry_order_id) { - this.notification.add(_t("Laundry Order Created"), { - type: "success", - }); - } - } - }, - - async pay() { - const currentOrder = this.getOrder(); - if (!currentOrder) { - return; - } - // Settle-due orders already have a partner (gated in settleLaundryDues). - if ( - !this.isSettleDueOrder(currentOrder) && - hasLaundryProduct(currentOrder) && - !currentOrder.getPartner() - ) { - const confirmed = await ask(this.env.services.dialog, { - title: _t("Customer Required"), - body: _t( - "This order contains laundry items. Please select or create a customer." - ), - }); - if (confirmed) { - const partner = await this.selectPartner(); - if (!partner) { - return; - } - } else { - return; - } - } - return super.pay(); - }, - - async editPartner(partner) { - if (partner) { - return super.editPartner(partner); - } - const result = await makeAwaitable(this.dialog, LaundryQuickCreatePartner, {}); - if (!result) { - return false; - } - const existing = await this.data.call( - "res.partner", - "laundry_find_by_phone", - [result.phone] - ); - if (existing) { - const useExisting = await ask(this.env.services.dialog, { - title: _t("Customer Already Exists"), - body: _t( - 'A customer "%s" already exists with this phone number. Use existing customer?', - existing.name - ), - }); - if (useExisting) { - const partners = await this.data.read("res.partner", [existing.id]); - return partners[0] || false; - } - return false; - } - const partnerId = await this.data.call( - "res.partner", - "laundry_quick_create", - [{ name: result.name, phone: result.phone, street: result.street || "" }] - ); - const partners = await this.data.read("res.partner", [partnerId]); - return partners[0] || false; - }, - - async settleLaundryDues() { - // Re-entrancy guard — defends against double-clicks while we - // hit the server for dues / before the order is materialized. - if (this._settleDuesInFlight) { - return; - } - this._settleDuesInFlight = true; - try { - const currentOrder = this.getOrder(); - const partner = currentOrder?.getPartner(); - if (!partner) { - this.notification.add(_t("Please select a customer first."), { - type: "warning", - }); - const selectedPartner = await this.selectPartner(); - if (!selectedPartner) { - return; - } - this._settleDuesInFlight = false; - return this.settleLaundryDues(); - } - - // Reuse an existing settle-due order for the same partner - // instead of creating a duplicate one. Whether we reuse or - // create fresh, SINGLE-ORDER MODE must hold: every other - // open order is removed before we navigate. - const existing = this.getOpenOrders().find( - (o) => - this.isSettleDueOrder(o) && - o.getPartner()?.id === partner.id - ); - if (existing) { - const ok = await this._removeAllOpenOrders({ except: existing }); - if (!ok) { - return; - } - // Our patched setOrder short-circuits when target === current - // settle order; safe to call here. - this.setOrder(existing); - this.navigate("PaymentScreen", { orderUuid: existing.uuid }); - return; - } - - const dues = await this.data.call( - "res.partner", - "get_laundry_dues", - [partner.id] - ); - - if (!dues || dues.total_due <= 0) { - this.notification.add( - _t('"%s" has no outstanding laundry dues.', partner.name), - { type: "info" } - ); - return; - } - - const settlementProduct = this.models["product.product"] - .getAll() - .find((p) => p.product_tmpl_id?.is_laundry_settlement); - - if (!settlementProduct) { - this.notification.add( - _t("Settlement product not configured. Please check laundry settings."), - { type: "danger" } - ); - return; - } - - // ─── SINGLE-ORDER MODE ───────────────────────────────────── - // Nuke every open order BEFORE creating the settle order. - // Data-loss protection lives inside _removeAllOpenOrders — - // it prompts once if any order has lines and returns false - // if the cashier cancels. - const ok = await this._removeAllOpenOrders(); - if (!ok) { - return; - } - - const order = this.addNewOrder(); - - // Mark BEFORE adding the settlement line so any concurrent - // attempt to add other products to this order is blocked. - // The settlement product itself is whitelisted in - // addLineToCurrentOrder. - this._markSettleDue(order); - order.setPartner(partner); - - await this.addLineToCurrentOrder( - { - product_id: settlementProduct, - product_tmpl_id: settlementProduct.product_tmpl_id, - price_unit: dues.total_due, - qty: 1, - }, - {}, - false - ); - - this.navigate("PaymentScreen", { - orderUuid: order.uuid, - }); - } finally { - this._settleDuesInFlight = false; - } - }, - - /** True iff order has at least one laundry-service line (excludes settlement). */ - orderHasLaundryServiceLine(order) { - return !!order?.lines?.some(isLaundryServiceLine); - }, - - async runLaundryOrderTypeFlow(order) { - if (!order) { - return; - } - const config = this.config; - - // Step 1 — Main type - const types = (this.models["laundry.order.type"]?.getAll() || []) - .slice() - .sort((a, b) => (a.sequence || 0) - (b.sequence || 0)); - if (!types.length) { - this.notification.add( - _t("No laundry order types configured. Ask a manager to add some."), - { type: "warning" } - ); - if (config.require_laundry_order_type) { - return; - } - } - - const partner = order.getPartner(); - const ctx = this.laundryContext.get(order.uuid); - const defaultTypeId = - toId(ctx.type_id) || - toId(order.laundry_order_type_id) || - toId(partner?.default_laundry_order_type_id) || - toId(config.default_laundry_order_type_id) || - false; - - const chosenType = await makeAwaitable(this.dialog, LaundryOrderTypePopup, { - types: types, - defaultTypeId: defaultTypeId, - showIcons: !!config.show_order_type_icons, - allowSkip: !config.require_laundry_order_type, - title: _t("Select Order Type"), - }); - if (chosenType === undefined) { - return; // user closed without confirming - } - - this._writeLaundryContext(order, { - type_id: chosenType ? chosenType.id : false, - }); - - // Step 2 — Attributes - let chosenAttributes = []; - if (config.enable_laundry_attributes) { - const allAttrs = (this.models["laundry.order.attribute"]?.getAll() || []) - .slice() - .sort((a, b) => (a.sequence || 0) - (b.sequence || 0)); - if (allAttrs.length) { - const suggestedIds = toIds(chosenType?.attribute_ids); - const partnerDefaultIds = toIds(partner?.default_laundry_attribute_ids); - const preselected = [ - ...new Set([...suggestedIds, ...partnerDefaultIds]), - ]; - const result = await makeAwaitable( - this.dialog, - LaundryOrderAttributePopup, - { - attributes: allAttrs, - preselectedIds: preselected, - title: _t("Select Attributes (optional)"), - } - ); - if (result !== undefined) { - chosenAttributes = result; - } - } - } - this._writeLaundryContext(order, { - attribute_ids: chosenAttributes.map((a) => a.id), - }); - - // Step 3 — Delivery details - const typeIsDelivery = !!chosenType?.is_delivery; - const attrIsDelivery = chosenAttributes.some((a) => a.is_delivery_related); - const isDelivery = typeIsDelivery || attrIsDelivery; - this._writeLaundryContext(order, { is_delivery: isDelivery }); - - if (isDelivery && config.require_delivery_details_if_needed) { - const requireAddress = - !!chosenType?.requires_address || attrIsDelivery; - const requireTime = !!chosenType?.requires_scheduled_time; - const result = await makeAwaitable( - this.dialog, - LaundryDeliveryDetailsPopup, - { - defaultAddress: ctx.delivery_address || "", - defaultScheduledAt: ctx.delivery_scheduled_at || "", - requireAddress: requireAddress, - requireScheduledTime: requireTime, - } - ); - if (result) { - this._writeLaundryContext(order, { - delivery_address: result.address || false, - delivery_scheduled_at: normalizeDateTime(result.scheduledAt) || false, - }); - } - } else if (!isDelivery) { - this._writeLaundryContext(order, { - delivery_address: false, - delivery_scheduled_at: false, - }); - } - }, - - async editLaundryOrderType() { - const order = this.getOrder(); - if (!order) { - return; - } - if (!this.config?.allow_change_laundry_order_type_before_payment) { - this.notification.add(_t("Changing order type is disabled."), { - type: "warning", - }); - return; - } - await this.runLaundryOrderTypeFlow(order); - }, - - /** - * Entry point invoked by the laundry-orders popup's "Collect Payment" - * button. Reuses the existing settleLaundryDues flow end-to-end so - * the well-tested settle-due isolation, navigation guards, and - * accounting RPC are inherited verbatim — no parallel collection - * pipeline, no new payment logic, no accounting changes. - * - * Steps: - * 1. Resolve the partner from POS data. - * 2. Make sure the active POS order has that partner set so - * settleLaundryDues doesn't pop the partner-selection dialog - * again. - * 3. Hand off to settleLaundryDues — which: - * • drops every other open order (with a confirm if any - * carry lines) so the cashier is in a single-order state, - * • spins up the settle-due context order, - * • hides Customer Account / pay-later methods, - * • navigates to PaymentScreen, - * • on Validate calls settle_laundry_dues_rpc → cash bank - * statement OR account.payment + reconcile + FIFO - * distribution across the customer's open laundry orders - * (this order included). - * - * Idempotent: if a settle-due is already active for the same - * partner, settleLaundryDues reuses it instead of creating a - * duplicate. - */ - async collectLaundryOrderPayment(partnerId) { - if (!partnerId) { - this.notification.add( - _t("Cannot collect payment: customer not specified."), - { type: "warning" } - ); - return; - } - const partner = this.models["res.partner"].get(partnerId); - if (!partner) { - this.notification.add( - _t("Customer not loaded in POS data. Refresh POS and retry."), - { type: "danger" } - ); - return; - } - let currentOrder = this.getOrder(); - if (!currentOrder) { - currentOrder = this.addNewOrder(); - } - const cur = currentOrder?.getPartner(); - if (!cur || cur.id !== partner.id) { - currentOrder?.setPartner(partner); - } - await this.settleLaundryDues(); - }, - - async viewLaundryOrders() { - const currentOrder = this.getOrder(); - let partner = currentOrder?.getPartner(); - if (!partner) { - partner = await this.selectPartner(); - if (!partner) { - return; - } - } - // Popup owns its own data lifecycle — fetches on mount + search, - // refetches the affected order after each workflow action. - this.dialog.add(LaundryOrdersViewPopup, { - partnerId: partner.id, - partnerName: partner.name, - // `mobile` is optional in some Odoo distributions — guard. - partnerPhone: partner.phone || partner.mobile || "", - }); - }, -}); diff --git a/addons/laundry_management/static/src/js/pos_store_patch.js.bak b/addons/laundry_management/static/src/js/pos_store_patch.js.bak deleted file mode 100644 index ca81390..0000000 --- a/addons/laundry_management/static/src/js/pos_store_patch.js.bak +++ /dev/null @@ -1,363 +0,0 @@ -/** @odoo-module */ -import { patch } from "@web/core/utils/patch"; -import { _t } from "@web/core/l10n/translation"; -import { PosStore } from "@point_of_sale/app/services/pos_store"; -import { ask, makeAwaitable } from "@point_of_sale/app/utils/make_awaitable_dialog"; -import { LaundryQuickCreatePartner } from "@laundry_management/js/quick_create_partner"; -import { LaundryOrdersViewPopup } from "@laundry_management/js/view_laundry_orders"; -import { LaundryOrderTypePopup } from "@laundry_management/js/popups/laundry_order_type_popup"; -import { LaundryOrderAttributePopup } from "@laundry_management/js/popups/laundry_order_attribute_popup"; -import { LaundryDeliveryDetailsPopup } from "@laundry_management/js/popups/laundry_delivery_details_popup"; - -/** - * Check whether a POS order contains at least one laundry-service product. - */ -function hasLaundryProduct(order) { - return order.lines.some( - (line) => line.product_id?.product_tmpl_id?.is_laundry_service - ); -} - -/** A line is a laundry SERVICE line — excludes settlement product. */ -function isLaundryServiceLine(line) { - const tmpl = line.product_id?.product_tmpl_id; - return tmpl?.is_laundry_service && !tmpl?.is_laundry_settlement; -} - -patch(PosStore.prototype, { - // -- Laundry notification after sync -- - async postSyncAllOrders(orders) { - await super.postSyncAllOrders(orders); - for (const order of orders) { - if (order.laundry_order_id) { - this.notification.add(_t("Laundry Order Created"), { - type: "success", - }); - } - } - }, - - // -- Force customer before payment screen (laundry orders only) -- - async pay() { - const currentOrder = this.getOrder(); - if (!currentOrder) { - return; - } - if (hasLaundryProduct(currentOrder) && !currentOrder.getPartner()) { - const confirmed = await ask(this.env.services.dialog, { - title: _t("Customer Required"), - body: _t( - "This order contains laundry items. Please select or create a customer." - ), - }); - if (confirmed) { - const partner = await this.selectPartner(); - if (!partner) { - return; - } - } else { - return; - } - } - return super.pay(); - }, - - // -- Quick create partner (name + phone + street, with duplicate check) -- - async editPartner(partner) { - if (partner) { - return super.editPartner(partner); - } - // New partner: show lightweight popup - const result = await makeAwaitable(this.dialog, LaundryQuickCreatePartner, {}); - if (!result) { - return false; - } - // Check for existing partner by phone - const existing = await this.data.call( - "res.partner", - "laundry_find_by_phone", - [result.phone] - ); - if (existing) { - const useExisting = await ask(this.env.services.dialog, { - title: _t("Customer Already Exists"), - body: _t( - 'A customer "%s" already exists with this phone number. Use existing customer?', - existing.name - ), - }); - if (useExisting) { - const partners = await this.data.read("res.partner", [existing.id]); - return partners[0] || false; - } - return false; - } - // Create new partner - const partnerId = await this.data.call( - "res.partner", - "laundry_quick_create", - [{ name: result.name, phone: result.phone, street: result.street || "" }] - ); - const partners = await this.data.read("res.partner", [partnerId]); - return partners[0] || false; - }, - - // -- Settle laundry dues via native POS PaymentScreen -- - async settleLaundryDues() { - const currentOrder = this.getOrder(); - const partner = currentOrder?.getPartner(); - if (!partner) { - this.notification.add(_t("Please select a customer first."), { - type: "warning", - }); - const selectedPartner = await this.selectPartner(); - if (!selectedPartner) { - return; - } - return this.settleLaundryDues(); - } - - // Fetch outstanding dues - const dues = await this.data.call( - "res.partner", - "get_laundry_dues", - [partner.id] - ); - - if (!dues || dues.total_due <= 0) { - this.notification.add( - _t('"%s" has no outstanding laundry dues.', partner.name), - { type: "info" } - ); - return; - } - - // Find the settlement product (is_laundry_settlement = true) - const settlementProduct = this.models["product.product"] - .getAll() - .find((p) => p.product_tmpl_id?.is_laundry_settlement); - - if (!settlementProduct) { - this.notification.add( - _t("Settlement product not configured. Please check laundry settings."), - { type: "danger" } - ); - return; - } - - // Create a dedicated settlement order - const order = this.addNewOrder(); - order.setPartner(partner); - - // Add settlement product with total_due as price - await this.addLineToCurrentOrder( - { - product_id: settlementProduct, - product_tmpl_id: settlementProduct.product_tmpl_id, - price_unit: dues.total_due, - qty: 1, - }, - {}, - false - ); - - // Navigate to native POS PaymentScreen - this.navigate("PaymentScreen", { - orderUuid: order.uuid, - }); - }, - - // ───────────────────────────────────────────────────────────────── - // Laundry order type popup chain - // ───────────────────────────────────────────────────────────────── - async addLineToCurrentOrder(vals, opts, configure) { - const result = await super.addLineToCurrentOrder(vals, opts, configure); - try { - await this.maybeAskLaundryOrderType(this.getOrder()); - } catch (e) { - console.error("Laundry order-type prompt failed:", e); - } - return result; - }, - - /** - * Trigger the type/attributes/delivery popup chain when the FIRST - * laundry-service line is added to an order — and only when: - * - feature is enabled in pos.config - * - order type not already chosen - * - the order has at least one laundry-service line (excludes settlement) - * - ask-on-first-line is enabled - */ - async maybeAskLaundryOrderType(order) { - if (!order || !order.lines.length) { - return; - } - const config = this.config; - if (!config?.enable_laundry_order_type) { - return; - } - if (order.laundry_order_type_id) { - return; - } - if (config.ask_laundry_order_type_on_first_line === false) { - return; - } - const laundryLines = order.lines.filter(isLaundryServiceLine); - if (!laundryLines.length) { - return; - } - // Only trigger when this is the FIRST laundry line. - if (laundryLines.length > 1) { - return; - } - await this.runLaundryOrderTypeFlow(order); - }, - - /** - * The popup chain itself — also reusable from the inline edit button - * in the order summary. - */ - async runLaundryOrderTypeFlow(order) { - if (!order) { - return; - } - const config = this.config; - - // Step 1 — Main type - const types = (this.models["laundry.order.type"]?.getAll() || []) - .slice() - .sort((a, b) => (a.sequence || 0) - (b.sequence || 0)); - if (!types.length) { - this.notification.add( - _t("No laundry order types configured. Ask a manager to add some."), - { type: "warning" } - ); - if (config.require_laundry_order_type) { - return; - } - } - - const partner = order.getPartner(); - const partnerDefaultType = partner?.default_laundry_order_type_id; - const defaultTypeId = - order.laundry_order_type_id?.id || - (partnerDefaultType?.id ?? partnerDefaultType) || - (config.default_laundry_order_type_id?.id ?? - config.default_laundry_order_type_id) || - false; - - const chosenType = await makeAwaitable(this.dialog, LaundryOrderTypePopup, { - types: types, - defaultTypeId: defaultTypeId, - showIcons: !!config.show_order_type_icons, - allowSkip: !config.require_laundry_order_type, - title: _t("Select Order Type"), - }); - if (chosenType === undefined) { - return; // user closed without confirming - } - order.laundry_order_type_id = chosenType - ? this.models["laundry.order.type"].get(chosenType.id) - : false; - - // Step 2 — Attributes (skip if disabled or nothing to show) - let chosenAttributes = []; - if (config.enable_laundry_attributes) { - const allAttrs = (this.models["laundry.order.attribute"]?.getAll() || []) - .slice() - .sort((a, b) => (a.sequence || 0) - (b.sequence || 0)); - if (allAttrs.length) { - const suggestedIds = chosenType?.attribute_ids - ? chosenType.attribute_ids - .map((a) => (typeof a === "object" ? a.id : a)) - : []; - const partnerDefaultIds = (partner?.default_laundry_attribute_ids || []) - .map((a) => (typeof a === "object" ? a.id : a)); - const preselected = [ - ...new Set([...suggestedIds, ...partnerDefaultIds]), - ]; - const result = await makeAwaitable( - this.dialog, - LaundryOrderAttributePopup, - { - attributes: allAttrs, - preselectedIds: preselected, - title: _t("Select Attributes (optional)"), - } - ); - if (result !== undefined) { - chosenAttributes = result; - } - } - } - order.laundry_order_attribute_ids = chosenAttributes.map((a) => - this.models["laundry.order.attribute"].get(a.id) - ); - - // Step 3 — Delivery details (only when needed) - const typeIsDelivery = !!chosenType?.is_delivery; - const attrIsDelivery = chosenAttributes.some((a) => a.is_delivery_related); - const isDelivery = typeIsDelivery || attrIsDelivery; - order.laundry_is_delivery = isDelivery; - - if (isDelivery && config.require_delivery_details_if_needed) { - const requireAddress = - !!chosenType?.requires_address || - attrIsDelivery; // delivery attribute implies need for address - const requireTime = !!chosenType?.requires_scheduled_time; - const result = await makeAwaitable( - this.dialog, - LaundryDeliveryDetailsPopup, - { - defaultAddress: order.laundry_delivery_address || "", - defaultScheduledAt: order.laundry_delivery_scheduled_at || "", - requireAddress: requireAddress, - requireScheduledTime: requireTime, - } - ); - if (result) { - order.laundry_delivery_address = result.address || false; - order.laundry_delivery_scheduled_at = result.scheduledAt || false; - } - } else if (!isDelivery) { - order.laundry_delivery_address = false; - order.laundry_delivery_scheduled_at = false; - } - }, - - /** Inline edit handler from the order-summary header. */ - async editLaundryOrderType() { - const order = this.getOrder(); - if (!order) { - return; - } - if (!this.config?.allow_change_laundry_order_type_before_payment) { - this.notification.add(_t("Changing order type is disabled."), { - type: "warning", - }); - return; - } - await this.runLaundryOrderTypeFlow(order); - }, - - // -- View laundry orders for current customer -- - async viewLaundryOrders() { - const currentOrder = this.getOrder(); - let partner = currentOrder?.getPartner(); - if (!partner) { - partner = await this.selectPartner(); - if (!partner) { - return; - } - } - const orders = await this.data.call( - "res.partner", - "get_laundry_orders_for_pos", - [partner.id, 30] - ); - this.dialog.add(LaundryOrdersViewPopup, { - partnerName: partner.name, - orders: orders || [], - }); - }, -}); diff --git a/addons/laundry_management/static/src/js/quick_create_partner.js b/addons/laundry_management/static/src/js/quick_create_partner.js deleted file mode 100644 index 49c20c3..0000000 --- a/addons/laundry_management/static/src/js/quick_create_partner.js +++ /dev/null @@ -1,72 +0,0 @@ -/** @odoo-module - * - * LaundryQuickCreatePartner — POS-only quick create. - * - * UX rules: - * - Phone is REQUIRED and the FIRST input (autofocused on mount). - * - Name is OPTIONAL. If empty on confirm, the backend - * `res.partner.laundry_quick_create` falls back to using the phone - * value as the partner name (single source of truth — JS does not - * duplicate that fallback). - * - Street is fully optional. - * - * All user-facing strings come from `_t()` getters so the active - * language switches the popup text cleanly. No mixed-language labels. - */ -import { Component, useState, useRef, onMounted } from "@odoo/owl"; -import { Dialog } from "@web/core/dialog/dialog"; -import { _t } from "@web/core/l10n/translation"; - -export class LaundryQuickCreatePartner extends Component { - static components = { Dialog }; - static template = "laundry_management.QuickCreatePartner"; - static props = { - getPayload: { type: Function }, - close: { type: Function }, - }; - - setup() { - this.state = useState({ - phone: "", - name: "", - street: "", - phoneError: false, - }); - this.phoneRef = useRef("phoneInput"); - onMounted(() => { - if (this.phoneRef.el) this.phoneRef.el.focus(); - }); - } - - // ── Translatable labels (computed each render so language change - // propagates without a remount) ───────────────────────────────── - get title() { return _t("Quick Create Customer"); } - get labelPhone() { return _t("Phone / Mobile"); } - get labelName() { return _t("Name"); } - get labelStreet() { return _t("Street"); } - get labelCreate() { return _t("Create"); } - get labelCancel() { return _t("Cancel"); } - get nameOptionalHint() { return _t("(optional — phone is used if empty)"); } - get placeholderPhone() { return _t("e.g. +966 50 123 4567"); } - get placeholderName() { return _t("Customer name"); } - get placeholderStreet() { return _t("Building, street, district…"); } - get errorPhoneRequired() { return _t("Phone is required."); } - - confirm() { - const phone = (this.state.phone || "").trim(); - if (!phone) { - this.state.phoneError = true; - return; - } - this.props.getPayload({ - phone, - name: (this.state.name || "").trim(), // empty → backend falls back to phone - street: (this.state.street || "").trim(), - }); - this.props.close(); - } - - cancel() { - this.props.close(); - } -} diff --git a/addons/laundry_management/static/src/js/settle_dues.js b/addons/laundry_management/static/src/js/settle_dues.js deleted file mode 100644 index f17181e..0000000 --- a/addons/laundry_management/static/src/js/settle_dues.js +++ /dev/null @@ -1,3 +0,0 @@ -/** @odoo-module */ -// Settlement popup replaced by native POS PaymentScreen flow. -// See payment_screen_patch.js for the settlement validation logic. diff --git a/addons/laundry_management/static/src/js/settlement_receipt.js b/addons/laundry_management/static/src/js/settlement_receipt.js deleted file mode 100644 index ec9bf0f..0000000 --- a/addons/laundry_management/static/src/js/settlement_receipt.js +++ /dev/null @@ -1,94 +0,0 @@ -/** @odoo-module */ -import { Component } from "@odoo/owl"; -import { Dialog } from "@web/core/dialog/dialog"; - -// Sort priority: cash first, then bank, then others -const JOURNAL_ORDER = { cash: 0, bank: 1 }; - -export class LaundrySettlementReceipt extends Component { - static components = { Dialog }; - static template = "laundry_management.SettlementReceipt"; - static props = { - partnerName: { type: String }, - settledTotal: { type: Number }, - remainingDue: { type: Number }, - payments: { type: Array }, - settledOrders: { type: Array }, - close: { type: Function }, - }; - - get dateTime() { - return new Date().toLocaleString(); - } - - fmt(value) { - return parseFloat(value || 0).toFixed(2); - } - - /** Group payments by method name, merge duplicates, sort cash → bank → other. */ - get groupedPayments() { - const map = {}; - for (const p of this.props.payments) { - // Use method_name (from settlement_pos_pm_id) when available, - // fall back to journal_name for legacy payments without it. - const key = p.method_name || p.journal_name; - const jtype = p.journal_type || "other"; - if (!map[key]) { - map[key] = { name: key, amount: 0, _type: jtype }; - } - map[key].amount += p.amount; - } - return Object.values(map).sort( - (a, b) => (JOURNAL_ORDER[a._type] ?? 2) - (JOURNAL_ORDER[b._type] ?? 2) - ); - } - - /** Group settled orders by name, merge duplicates. */ - get groupedOrders() { - const map = {}; - for (const o of this.props.settledOrders) { - const key = o.name; - if (!map[key]) { - map[key] = { name: key, applied: 0, remaining_on_order: 0 }; - } - map[key].applied += o.applied; - map[key].remaining_on_order = o.remaining_on_order; - } - return Object.values(map); - } - - printReceipt() { - const receiptEl = document.querySelector(".settlement-receipt-content"); - if (!receiptEl) return; - const printWindow = window.open("", "_blank", "width=300,height=600"); - if (!printWindow) return; - printWindow.document.write(` - Settlement Receipt - - ${receiptEl.innerHTML} - - `); - printWindow.document.close(); - printWindow.focus(); - printWindow.print(); - printWindow.close(); - } -} diff --git a/addons/laundry_management/static/src/js/ticket_screen_patch.js b/addons/laundry_management/static/src/js/ticket_screen_patch.js deleted file mode 100644 index 0a81de6..0000000 --- a/addons/laundry_management/static/src/js/ticket_screen_patch.js +++ /dev/null @@ -1,26 +0,0 @@ -/** @odoo-module - * - * TicketScreen patch — gate double-click-to-resume-order through the - * settle-due exit confirmation so cashiers can't silently abandon a - * live settle-due order from the ticket list. - * - * Single-click (onClickOrder) only updates the ticket-list selection - * and does NOT change the active POS order, so it's safe to leave - * untouched. Double-click calls pos.setOrder — our patched setOrder - * also enforces the gate, but doing it here too avoids the flicker - * of partially-switching before the async confirm resolves. - */ -import { patch } from "@web/core/utils/patch"; -import { TicketScreen } from "@point_of_sale/app/screens/ticket_screen/ticket_screen"; - -patch(TicketScreen.prototype, { - async onDblClickOrder(order) { - if (!order?.finalized) { - const allowed = await this.pos.confirmExitSettleIfNeeded(order); - if (!allowed) { - return; - } - } - return super.onDblClickOrder(order); - }, -}); diff --git a/addons/laundry_management/static/src/js/view_laundry_orders.js b/addons/laundry_management/static/src/js/view_laundry_orders.js deleted file mode 100644 index 63bc582..0000000 --- a/addons/laundry_management/static/src/js/view_laundry_orders.js +++ /dev/null @@ -1,273 +0,0 @@ -/** @odoo-module - * - * LaundryOrdersViewPopup — in-POS customer laundry orders manager. - * - * Owns its own data lifecycle: fetches on mount, refetches on search, - * refetches the single affected order after each workflow action. - * - * All writes go through the whitelisted `pos_action_*` RPCs on - * `laundry.order` — never touches line/price/customer fields, so - * Phase 3 locking remains authoritative and the popup cannot open a - * side channel around it. - */ -import { Component, useState, onWillStart, useRef, onMounted } from "@odoo/owl"; -import { Dialog } from "@web/core/dialog/dialog"; -import { _t } from "@web/core/l10n/translation"; -import { usePos } from "@point_of_sale/app/hooks/pos_hook"; -import { useService } from "@web/core/utils/hooks"; -import { ask } from "@point_of_sale/app/utils/make_awaitable_dialog"; -// Thermal receipt is intentionally NOT imported — the component is -// excluded from the asset bundle until individually re-validated. -// Print falls back to the standard PDF report (always available). - -const WORKFLOW_BADGE = { - intake: { label: "Intake", klass: "badge-state-intake" }, - processing: { label: "Processing", klass: "badge-state-processing"}, - ready: { label: "Ready", klass: "badge-state-ready" }, - delivered: { label: "Delivered", klass: "badge-state-delivered" }, - cancelled: { label: "Cancelled", klass: "badge-state-cancelled" }, -}; - -const PAYMENT_BADGE = { - paid: { label: "Paid", klass: "badge-payment-paid" }, - deferred: { label: "Deferred", klass: "badge-payment-deferred" }, - settled: { label: "Settled", klass: "badge-payment-settled" }, - due: { label: "Due", klass: "badge-payment-due" }, -}; - -export class LaundryOrdersViewPopup extends Component { - static components = { Dialog }; - static template = "laundry_management.LaundryOrdersViewPopup"; - static props = { - partnerId: { type: Number }, - partnerName: { type: String }, - partnerPhone: { type: String, optional: true }, - close: { type: Function }, - }; - static defaultProps = { - partnerPhone: "", - }; - - setup() { - this.pos = usePos(); - this.notification = useService("notification"); - this.dialog = useService("dialog"); - this.orm = this.pos.data; // shared ORM adapter - this.searchRef = useRef("searchInput"); - - this.state = useState({ - orders: [], - loading: true, - error: null, - searchQuery: "", - busyOrderIds: {}, // { [id]: 'action_key' } while RPC in flight - }); - - onWillStart(async () => { - await this._fetch(); - }); - onMounted(() => { - if (this.searchRef.el) this.searchRef.el.focus(); - }); - } - - // ─── Data ───────────────────────────────────────────────────────── - async _fetch() { - this.state.loading = true; - this.state.error = null; - try { - const rows = await this.orm.call( - "laundry.order", - "pos_search_customer_orders", - [], - { - partner_id: this.props.partnerId, - search_query: this.state.searchQuery || false, - limit: 20, - } - ); - this.state.orders = rows || []; - } catch (err) { - this.state.error = this._humanizeError(err); - } finally { - this.state.loading = false; - } - } - - _replaceOrder(updated) { - if (!updated?.id) return; - const idx = this.state.orders.findIndex((o) => o.id === updated.id); - if (idx >= 0) { - this.state.orders.splice(idx, 1, updated); - } - } - - _humanizeError(err) { - return ( - err?.data?.message || - err?.message || - _t("Could not contact the server. Please retry.") - ); - } - - // ─── Search ─────────────────────────────────────────────────────── - onSearchInput(ev) { - this.state.searchQuery = ev.target.value || ""; - } - - async onSearchSubmit(ev) { - ev?.preventDefault?.(); - await this._fetch(); - } - - async onClearSearch() { - this.state.searchQuery = ""; - await this._fetch(); - } - - // ─── Workflow actions ───────────────────────────────────────────── - async _runAction(order, methodName, actionKey) { - if (this.state.busyOrderIds[order.id]) return; - this.state.busyOrderIds[order.id] = actionKey; - try { - const updated = await this.orm.call( - "laundry.order", - methodName, - [[order.id]] - ); - this._replaceOrder(updated); - this.notification.add( - _t('Order %s updated.', updated?.name || order.name), - { type: "success" } - ); - } catch (err) { - this.notification.add(this._humanizeError(err), { type: "danger" }); - } finally { - delete this.state.busyOrderIds[order.id]; - } - } - - onClickStartProcessing(order) { - return this._runAction(order, "pos_action_start_processing", "start_processing"); - } - - onClickMarkReady(order) { - return this._runAction(order, "pos_action_mark_ready", "mark_ready"); - } - - async onClickDeliver(order) { - const confirmed = await ask(this.dialog, { - title: _t("Deliver Order?"), - body: _t( - "Confirm delivery of %(name)s (%(items)s items, %(total)s).", - { - name: order.name, - items: order.item_count, - total: this._fmt(order.amount_total), - } - ), - confirmLabel: _t("Deliver"), - cancelLabel: _t("Cancel"), - }); - if (!confirmed) return; - return this._runAction(order, "pos_action_deliver", "deliver"); - } - - /** - * Hand off to the proven settle-due flow on the store. We close the - * popup BEFORE the navigation kicks off — keeping the modal mounted - * while routes change is the known cause of the white-screen race - * the user reported. Toast first so the cashier always sees feedback, - * even if the screen change is instantaneous. - */ - async onClickCollectPayment(order) { - this.notification.add( - _t("Redirecting to payment for %(name)s — %(amount)s due.", { - name: order.name, - amount: this._fmt(order.amount_due), - }), - { type: "info" } - ); - this.props.close(); - try { - await this.pos.collectLaundryOrderPayment(this.props.partnerId); - } catch (err) { - // Re-surface server errors as a notification so the cashier - // never gets stuck with no feedback. - this.pos.notification.add(this._humanizeError(err), { type: "danger" }); - } - } - - /** - * Standard PDF Work Order via the existing report action. - * The thermal-printer path is currently disabled (component - * excluded from the bundle until re-validated). PDF is the - * proven fallback — works without hardware-printer configuration. - */ - async onClickPrintWorkOrder(order) { - try { - await this.pos.env.services.action.doAction( - "laundry_management.action_report_laundry_work_order", - { additionalContext: { active_ids: [order.id] } } - ); - } catch (err) { - this.notification.add(this._humanizeError(err), { type: "danger" }); - } - } - - // ─── UI helpers ────────────────────────────────────────────────── - stateBadge(order) { - return WORKFLOW_BADGE[order.state] || WORKFLOW_BADGE.intake; - } - - paymentBadge(order) { - return PAYMENT_BADGE[order.payment_state] || PAYMENT_BADGE.paid; - } - - formatDate(order) { - if (!order.create_date) return ""; - const d = new Date(order.create_date.replace(" ", "T") + "Z"); - if (isNaN(d.getTime())) return order.create_date; - return d.toLocaleString(undefined, { - month: "short", day: "numeric", - hour: "2-digit", minute: "2-digit", - }); - } - - _fmt(amount) { - try { - return this.pos.env.utils.formatCurrency(amount || 0); - } catch { - const sym = this.pos.currency?.symbol || ""; - return `${(amount || 0).toFixed(2)} ${sym}`.trim(); - } - } - - fmt(amount) { - return this._fmt(amount); - } - - isAllowed(order, key) { - return (order.allowed_actions || []).includes(key); - } - - isBusy(order, key) { - return this.state.busyOrderIds[order.id] === key; - } - - isOrderBusy(order) { - return !!this.state.busyOrderIds[order.id]; - } - - get hasResults() { - return !this.state.loading && !this.state.error && this.state.orders.length > 0; - } - - get isEmpty() { - return !this.state.loading && !this.state.error && this.state.orders.length === 0; - } - - close() { - this.props.close(); - } -} diff --git a/addons/laundry_management/static/src/scss/laundry_pos.scss b/addons/laundry_management/static/src/scss/laundry_pos.scss deleted file mode 100644 index 3d06e60..0000000 --- a/addons/laundry_management/static/src/scss/laundry_pos.scss +++ /dev/null @@ -1,994 +0,0 @@ -// ============================================================================ -// Laundry Management — POS / Backend Design System -// 8px grid, soft shadows, premium pill components. -// ============================================================================ - -// ── Tokens ───────────────────────────────────────────────────────────────── -$lm-space-1: 4px; -$lm-space-2: 8px; -$lm-space-3: 12px; -$lm-space-4: 16px; -$lm-space-5: 24px; - -$lm-radius-sm: 6px; -$lm-radius-md: 10px; -$lm-radius-lg: 14px; - -$lm-shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06), 0 1px 1px rgba(15, 23, 42, 0.04); -$lm-shadow-md: 0 4px 12px rgba(15, 23, 42, 0.08), 0 2px 4px rgba(15, 23, 42, 0.05); -$lm-shadow-lg: 0 10px 30px rgba(15, 23, 42, 0.12), 0 4px 8px rgba(15, 23, 42, 0.06); - -$lm-color-urgent: #EF4444; -$lm-color-delivery: #10B981; -$lm-color-vip: #8B5CF6; -$lm-color-normal: #6B7280; -$lm-color-type: #4F46E5; - -$lm-font-xs: 11px; -$lm-font-sm: 13px; -$lm-font-md: 14px; -$lm-font-lg: 16px; -$lm-font-xl: 18px; - -$lm-bg-card: #FFFFFF; -$lm-bg-soft: #F8FAFC; -$lm-border: #E2E8F0; -$lm-text: #0F172A; -$lm-text-muted: #64748B; - -// ── Pill component (shared across panel / kanban / receipt) ─────────────── -.laundry-pill { - display: inline-flex; - align-items: center; - padding: $lm-space-1 $lm-space-3; - border-radius: 9999px; - font-size: $lm-font-sm; - font-weight: 600; - line-height: 1.2; - color: #fff; - background-color: $lm-color-normal; - box-shadow: $lm-shadow-sm; - white-space: nowrap; - transition: transform 0.12s ease, box-shadow 0.12s ease; - - &:hover { transform: translateY(-1px); box-shadow: $lm-shadow-md; } - - .fa { font-size: 0.95em; } - - &--type { background-color: $lm-color-type; } - &--attr { background-color: $lm-color-normal; } - &--urgent { background-color: $lm-color-urgent; } - &--delivery { background-color: $lm-color-delivery; } - &--vip { background-color: $lm-color-vip; } - &--deferred { background-color: #F59E0B; } - - // Semantic data overrides — win over default --attr - &[data-priority="urgent"] { background-color: $lm-color-urgent; } - &[data-delivery="1"] { background-color: $lm-color-delivery; } -} - -// ── Settle Due Banner (POS Mode indicator) ──────────────────────────────── -.laundry-settle-banner { - margin: $lm-space-2 $lm-space-3 0 $lm-space-3; - padding: $lm-space-3 $lm-space-4; - background: linear-gradient(135deg, #F59E0B 0%, #F97316 100%); - color: #fff; - border-radius: $lm-radius-md; - box-shadow: $lm-shadow-md; - display: flex; - align-items: center; - justify-content: space-between; - gap: $lm-space-3; - animation: laundry-settle-banner-pulse 2.4s ease-in-out infinite; - - &__lead { - display: flex; - align-items: center; - gap: $lm-space-3; - min-width: 0; - } - - &__icon { - font-size: $lm-font-xl; - flex-shrink: 0; - } - - &__text { - display: flex; - flex-direction: column; - min-width: 0; - } - - &__title { - font-size: $lm-font-md; - font-weight: 800; - letter-spacing: 0.06em; - line-height: 1.15; - text-transform: uppercase; - } - - &__meta { - display: flex; - align-items: baseline; - gap: $lm-space-3; - margin-top: 3px; - min-width: 0; - } - - &__partner { - font-size: $lm-font-sm; - font-weight: 600; - opacity: 0.95; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 40ch; - } - - &__amount { - font-size: $lm-font-md; - font-weight: 800; - letter-spacing: 0.01em; - padding: 1px 8px; - background: rgba(255, 255, 255, 0.2); - border-radius: $lm-radius-sm; - white-space: nowrap; - } - - &__hint { - font-size: $lm-font-xs; - opacity: 0.85; - margin-top: 2px; - font-style: italic; - } - - &__exit { - background: rgba(255, 255, 255, 0.18); - color: #fff; - border: 1px solid rgba(255, 255, 255, 0.4); - padding: $lm-space-2 $lm-space-3; - border-radius: $lm-radius-sm; - font-size: $lm-font-sm; - font-weight: 600; - cursor: pointer; - flex-shrink: 0; - transition: background 0.15s ease, transform 0.12s ease; - - &:hover { background: rgba(255, 255, 255, 0.3); transform: translateY(-1px); } - &:active { transform: translateY(0); } - &:focus-visible { - outline: 2px solid #fff; - outline-offset: 2px; - } - } -} - -@keyframes laundry-settle-banner-pulse { - 0%, 100% { box-shadow: 0 4px 12px rgba(245, 158, 11, 0.35); } - 50% { box-shadow: 0 4px 20px rgba(245, 158, 11, 0.65); } -} - -// ── Global lock of POS chrome while settle-due mode is active ───────────── -// The banner component toggles `pos-laundry-settle-active` on . -// CSS is a clarity layer, not the safety layer — JS (pos_store_patch / -// order_tabs_patch / navbar_patch) is the real gate. But here we also -// disable pointer-events on the peripherals because SINGLE-ORDER MODE -// guarantees the only tab is the settle order itself; everything else -// is visual chrome that shouldn't respond to clicks. -body.pos-laundry-settle-active { - // Every non-active tab (should be zero under single-order mode — - // but defense in depth): hatched, dim, unclickable. - .floating-order-container .btn:not(.active) { - pointer-events: none; - opacity: 0.35; - filter: saturate(0.3); - position: relative; - - &::after { - content: ""; - position: absolute; - inset: 0; - border-radius: inherit; - background: repeating-linear-gradient( - 135deg, - rgba(245, 158, 11, 0.0) 0 6px, - rgba(245, 158, 11, 0.15) 6px 12px - ); - pointer-events: none; - } - } - - // Active settle tab — keep visible & highlighted in warning orange. - .floating-order-container .btn.active { - outline: 2px solid #F59E0B; - outline-offset: 2px; - pointer-events: none; // the tab IS the current order; no switch needed - } - - // "+" new-order button inside ListContainer — hard-disabled visually. - // JS addNewOrder patch also blocks it. - .list-container-add, - .o_list_container_add, - button[title*="Add a new order" i], - button[aria-label*="Add a new order" i] { - pointer-events: none; - opacity: 0.35; - filter: saturate(0.3); - } -} - -// ── Order Context Panel (POS right-side card) ───────────────────────────── -.laundry-context-panel { - margin: $lm-space-2 $lm-space-3; - padding: $lm-space-3 $lm-space-4; - background: $lm-bg-card; - border: 1px solid $lm-border; - border-radius: $lm-radius-md; - box-shadow: $lm-shadow-sm; - display: flex; - flex-direction: column; - gap: $lm-space-2; - transition: box-shadow 0.18s ease, transform 0.18s ease; - - &:hover { box-shadow: $lm-shadow-md; } - - &__header { - display: flex; - align-items: center; - justify-content: space-between; - } - - &__title { - font-size: $lm-font-xs; - font-weight: 700; - letter-spacing: 0.06em; - text-transform: uppercase; - color: $lm-text-muted; - } - - &__edit { - background: transparent; - border: 0; - color: $lm-text-muted; - padding: $lm-space-1 $lm-space-2; - border-radius: $lm-radius-sm; - cursor: pointer; - transition: background 0.12s ease, color 0.12s ease; - - &:hover { background: $lm-bg-soft; color: $lm-text; } - &:focus-visible { - outline: 2px solid rgba(99, 102, 241, 0.45); - outline-offset: 2px; - } - } - - &__row { - display: flex; - flex-wrap: wrap; - gap: $lm-space-2; - } - - &__attrs .laundry-pill { font-size: $lm-font-xs; padding: 2px $lm-space-2; } - - &__empty { - font-size: $lm-font-sm; - color: $lm-text-muted; - background: $lm-bg-soft; - padding: $lm-space-2 $lm-space-3; - border-radius: $lm-radius-sm; - text-align: center; - } - - &__cta { - display: flex; - align-items: center; - justify-content: center; - gap: $lm-space-2; - width: 100%; - padding: $lm-space-3 $lm-space-4; - background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%); - color: #fff; - font-size: $lm-font-md; - font-weight: 600; - border: 0; - border-radius: $lm-radius-md; - box-shadow: $lm-shadow-sm; - cursor: pointer; - transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.12s ease; - - .fa { font-size: 1.05em; } - - &:hover { - transform: translateY(-1px); - box-shadow: $lm-shadow-md; - filter: brightness(1.05); - } - &:active { transform: translateY(0); filter: brightness(0.95); } - &:focus-visible { - outline: 2px solid rgba(99, 102, 241, 0.55); - outline-offset: 2px; - } - } - - &__edit-label { - margin-inline-start: $lm-space-1; - font-size: $lm-font-xs; - font-weight: 600; - } - - &__delivery { - display: flex; - flex-direction: column; - gap: $lm-space-1; - padding-top: $lm-space-2; - border-top: 1px dashed $lm-border; - } - - &__delivery-row { - display: flex; - align-items: center; - gap: $lm-space-2; - font-size: $lm-font-sm; - color: $lm-text; - - .fa { color: $lm-color-delivery; width: 14px; text-align: center; } - } - - &[data-empty="1"] { background: $lm-bg-soft; } - &[data-delivery="1"] { border-left: 3px solid $lm-color-delivery; } -} - -// ── Popup polish (shared by all 3 laundry popups) ───────────────────────── -.modal .modal-dialog { - .btn.btn-outline-primary, - .btn.btn-primary { - border-radius: $lm-radius-md; - transition: transform 0.12s ease, box-shadow 0.12s ease, - background-color 0.12s ease, border-color 0.12s ease; - } - - .btn.btn-primary { - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.18); - } - - .btn.btn-outline-primary:hover { - transform: translateY(-1px); - box-shadow: $lm-shadow-md; - } - - .modal-footer .btn-primary, - .modal-footer .btn-secondary { - position: sticky; - bottom: 0; - } -} - -// ── Receipt details (printer-friendly) ──────────────────────────────────── -.laundry-receipt-details { - margin-top: $lm-space-2; - text-align: center; - - &__sep { - border-top: 1px dashed #000; - margin: $lm-space-2 0; - } - - &__title { - font-weight: 700; - font-size: $lm-font-md; - margin-bottom: $lm-space-1; - letter-spacing: 0.04em; - } - - &__row { - display: flex; - justify-content: space-between; - gap: $lm-space-3; - font-size: $lm-font-sm; - margin: 2px 0; - } - - &__label { - font-weight: 600; - color: #000; - min-width: 80px; - text-align: start; - } - - &__value { - text-align: end; - flex: 1; - } - - &__chip { - display: inline-block; - padding: 0 $lm-space-2; - margin: 0 2px; - border: 1px solid #000; - border-radius: $lm-radius-sm; - font-size: $lm-font-xs; - } -} - -// RTL safety -[dir="rtl"] .laundry-receipt-details__label { text-align: end; } -[dir="rtl"] .laundry-receipt-details__value { text-align: start; } - -// ── Operational Control Board (laundry.order kanban) ────────────────────── -.laundry-board { - .laundry-board__card { - position: relative; - background: $lm-bg-card; - border: 1px solid $lm-border; - border-radius: $lm-radius-md; - box-shadow: $lm-shadow-sm; - padding: $lm-space-3 $lm-space-4 $lm-space-3 ($lm-space-4 + 4px); - display: flex; - flex-direction: column; - gap: $lm-space-2; - transition: box-shadow 0.18s ease, transform 0.18s ease; - - &:hover { box-shadow: $lm-shadow-md; transform: translateY(-1px); } - } - - .laundry-board__strip { - position: absolute; - left: 0; top: 0; bottom: 0; - width: 4px; - border-top-left-radius: $lm-radius-md; - border-bottom-left-radius: $lm-radius-md; - background: $lm-color-normal; - } - - .laundry-board__card[data-state="intake"] .laundry-board__strip { background: #3B82F6; } - .laundry-board__card[data-state="processing"] .laundry-board__strip { background: #F59E0B; } - .laundry-board__card[data-state="ready"] .laundry-board__strip { background: $lm-color-delivery; } - .laundry-board__card[data-state="delivered"] .laundry-board__strip { background: $lm-color-normal; } - .laundry-board__card[data-priority="urgent"] .laundry-board__strip { background: $lm-color-urgent; } - .laundry-board__card[data-priority="urgent"] { border-color: rgba(239, 68, 68, 0.35); } - - .laundry-board__head { - display: flex; - flex-direction: column; - gap: 2px; - } - - .laundry-board__title-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: $lm-space-2; - } - - .laundry-board__name { - font-size: $lm-font-md; - color: $lm-text; - } - - .laundry-board__total { - font-size: $lm-font-md; - font-weight: 700; - color: $lm-text; - } - - .laundry-board__customer { - font-size: $lm-font-sm; - color: $lm-text-muted; - display: flex; - align-items: center; - } - - .laundry-board__badges { - display: flex; - flex-wrap: wrap; - gap: $lm-space-1; - } - - .laundry-board__meta { - display: flex; - flex-wrap: wrap; - gap: $lm-space-3; - font-size: $lm-font-xs; - color: $lm-text-muted; - } - - .laundry-board__due { - color: $lm-color-urgent; - font-weight: 700; - } - - .laundry-board__actions { margin-top: $lm-space-1; } -} - - -// ── ProductConfiguratorPopup — laundry enhancement ───────────────────── -// Scoped exclusively under `.popup-product-configurator.laundry-enhanced`, -// applied to the modal-content via Dialog's `contentClass` prop from -// xml/product_configurator_popup.xml. Non-laundry configurator popups are -// unaffected. -.popup-product-configurator.laundry-enhanced { - - // Attribute group — stronger title, better spacing. - .modal-body .attribute { - margin-bottom: $lm-space-4 !important; - padding-bottom: $lm-space-3; - border-bottom: 1px dashed $lm-border; - - &:last-child { - margin-bottom: 0 !important; - padding-bottom: 0; - border-bottom: 0; - } - - .attribute_name { - font-size: $lm-font-sm; - font-weight: 800 !important; - text-transform: uppercase; - letter-spacing: 0.09em; - color: $lm-text-muted; - margin-bottom: $lm-space-3 !important; - } - } - - // Touch-first tiles — applies to both Radio and Pills renderers, - // which share the same `.configurator_radio > .attribute-name-cell` - // structure in core. - .configurator_radio { - > .d-flex { - flex-wrap: wrap !important; - gap: $lm-space-3 !important; - } - - .attribute-name-cell { - flex: 1 1 calc(33% - #{$lm-space-3}); - min-width: 140px; - margin: 0; - padding: 0; - - // Hide the native radio — the label IS the tile. - .form-check-input, - .radio-check { - position: absolute !important; - opacity: 0 !important; - pointer-events: none !important; - width: 0 !important; - height: 0 !important; - } - - > label { - display: flex !important; - align-items: center; - justify-content: center; - flex-wrap: wrap; - gap: $lm-space-2; - width: 100%; - min-height: 64px; - padding: 12px 16px !important; - border: 2px solid $lm-border !important; - border-radius: 12px !important; - background: #fff !important; - color: $lm-text !important; - font-size: $lm-font-md; - font-weight: 700; - line-height: 1.25; - cursor: pointer; - position: relative; - box-shadow: none !important; - transition: transform 0.14s ease, - border-color 0.14s ease, - box-shadow 0.16s ease, - background 0.14s ease, - color 0.14s ease; - - &:hover { - transform: translateY(-1px); - box-shadow: $lm-shadow-sm !important; - } - &:active { transform: translateY(0); } - &:focus-visible { - outline: 2px solid rgba(79, 70, 229, 0.45); - outline-offset: 2px; - } - - // Wrap text span so conflict styling still readable. - > span:first-child { word-break: break-word; } - } - } - - // Selected state: - // - Pills renderer: core toggles `.active` on the