From 7ac2e15aa74676ba5741887231814cdf9e675755 Mon Sep 17 00:00:00 2001 From: Tyler Matteson Date: Mon, 13 May 2024 16:45:12 -0400 Subject: [PATCH 01/64] feat: add inventory tools settings to boot (#81) --- inventory_tools/hooks.py | 10 +++++----- inventory_tools/inventory_tools/boot.py | 8 ++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 inventory_tools/inventory_tools/boot.py diff --git a/inventory_tools/hooks.py b/inventory_tools/hooks.py index a794150..03f91bb 100644 --- a/inventory_tools/hooks.py +++ b/inventory_tools/hooks.py @@ -31,12 +31,12 @@ # include js in doctype views doctype_js = { - "Work Order": "public/js/work_order_custom.js", - "Purchase Order": "public/js/purchase_order_custom.js", - "Purchase Invoice": "public/js/purchase_invoice_custom.js", - "Stock Entry": "public/js/stock_entry_custom.js", "Job Card": "public/js/job_card_custom.js", + "Purchase Invoice": "public/js/purchase_invoice_custom.js", + "Purchase Order": "public/js/purchase_order_custom.js", "Operation": "public/js/operation_custom.js", + "Stock Entry": "public/js/stock_entry_custom.js", + "Work Order": "public/js/work_order_custom.js", } # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} @@ -83,7 +83,7 @@ # Boot # ------------ -# extend_bootinfo = "inventory_tools.inventory_tools.boot.boot_session" +extend_bootinfo = "inventory_tools.inventory_tools.boot.boot_session" # Desk Notifications diff --git a/inventory_tools/inventory_tools/boot.py b/inventory_tools/inventory_tools/boot.py new file mode 100644 index 0000000..1a20d32 --- /dev/null +++ b/inventory_tools/inventory_tools/boot.py @@ -0,0 +1,8 @@ +import frappe + + +def boot_session(bootinfo): + bootinfo.inventory_tools_settings = {} + for company in frappe.get_all("Inventory Tools Settings", pluck="company"): + settings = frappe.get_doc("Inventory Tools Settings", company) + bootinfo.inventory_tools_settings[company] = settings From 11503144e215a15ae70e3d37a142e245c045c5c7 Mon Sep 17 00:00:00 2001 From: AgriTheory Date: Mon, 13 May 2024 20:46:07 +0000 Subject: [PATCH 02/64] 14.6.0 Automatically generated by python-semantic-release --- CHANGELOG.md | 11 +++++++++++ inventory_tools/__init__.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63b4b8a..abe4c8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ +## v14.6.0 (2024-05-13) + +### Chore + +* chore: update quotation demand docs (#77) ([`f168079`](https://github.com/agritheory/inventory_tools/commit/f168079e29d99056d4c5eb2321d84957cff84f3b)) + +### Feature + +* feat: add inventory tools settings to boot (#81) ([`7ac2e15`](https://github.com/agritheory/inventory_tools/commit/7ac2e15aa74676ba5741887231814cdf9e675755)) + + ## v14.5.0 (2024-04-29) ### Feature diff --git a/inventory_tools/__init__.py b/inventory_tools/__init__.py index d83f37b..845946e 100644 --- a/inventory_tools/__init__.py +++ b/inventory_tools/__init__.py @@ -1,4 +1,4 @@ -__version__ = "14.5.0" +__version__ = "14.6.0" """ From d0469ea9781c1f8f5a0fb42f792699e03731c5cc Mon Sep 17 00:00:00 2001 From: Heather Kusmierz Date: Fri, 24 May 2024 07:58:49 -0400 Subject: [PATCH 03/64] ci: track overrides for purchase cycle (PO/PI/PR) (#89) --- .github/workflows/overrides.yaml | 20 +++++++++++++++++++ .../overrides/purchase_invoice.py | 6 ++++++ .../overrides/purchase_order.py | 18 +++++++++++++++++ .../overrides/purchase_receipt.py | 6 ++++++ 4 files changed, 50 insertions(+) create mode 100644 .github/workflows/overrides.yaml diff --git a/.github/workflows/overrides.yaml b/.github/workflows/overrides.yaml new file mode 100644 index 0000000..472e415 --- /dev/null +++ b/.github/workflows/overrides.yaml @@ -0,0 +1,20 @@ +name: Track Overrides + +on: + pull_request: + branches: + - version-14 + - version-15 + +jobs: + track_overrides: + runs-on: ubuntu-latest + name: Track Overrides + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Track Overrides + uses: diamorafaela/track-overrides@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/inventory_tools/inventory_tools/overrides/purchase_invoice.py b/inventory_tools/inventory_tools/overrides/purchase_invoice.py index 5c64525..87b8c15 100644 --- a/inventory_tools/inventory_tools/overrides/purchase_invoice.py +++ b/inventory_tools/inventory_tools/overrides/purchase_invoice.py @@ -12,6 +12,12 @@ class InventoryToolsPurchaseInvoice(PurchaseInvoice): def validate_with_previous_doc(self): + """ + HASH: 82d206b709ada758c277bc5a266dbc6ec10d00c8 + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py + METHOD: validate_with_previous_doc + """ config = { "Purchase Order": { "ref_dn_field": "purchase_order", diff --git a/inventory_tools/inventory_tools/overrides/purchase_order.py b/inventory_tools/inventory_tools/overrides/purchase_order.py index 3a86972..b8ec96a 100644 --- a/inventory_tools/inventory_tools/overrides/purchase_order.py +++ b/inventory_tools/inventory_tools/overrides/purchase_order.py @@ -25,6 +25,12 @@ def _bypass(*args, **kwargs): class InventoryToolsPurchaseOrder(PurchaseOrder): def validate_with_previous_doc(self): + """ + HASH: 4d34b1ead73baf4c5430a2ecbe44b9e8468d7626 + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/buying/doctype/purchase_order/purchase_order.py + METHOD: validate_with_previous_doc + """ config = { "Supplier Quotation": { "ref_dn_field": "supplier_quotation", @@ -218,6 +224,12 @@ def make_sales_invoices(docname: str, rows: Union[list, str]) -> None: @frappe.whitelist() def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=True): + """ + HASH: 0bee921d402060abe6c06568dd3f600d1445719f + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/stock/get_item_details.py + METHOD: get_item_details + """ import erpnext.stock.get_item_details erpnext.stock.get_item_details.validate_item_details = validate_item_details @@ -229,6 +241,12 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru @frappe.whitelist() def validate_item_details(args, item): + """ + HASH: 0bee921d402060abe6c06568dd3f600d1445719f + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/stock/get_item_details.py + METHOD: validate_item_details + """ if not args.company: throw(_("Please specify Company")) diff --git a/inventory_tools/inventory_tools/overrides/purchase_receipt.py b/inventory_tools/inventory_tools/overrides/purchase_receipt.py index b6a2d2c..d922a67 100644 --- a/inventory_tools/inventory_tools/overrides/purchase_receipt.py +++ b/inventory_tools/inventory_tools/overrides/purchase_receipt.py @@ -10,6 +10,12 @@ class InventoryToolsPurchaseReceipt(PurchaseReceipt): def validate_with_previous_doc(self): + """ + HASH: 9d0c1dc46f94e921eb108fcbfa24fbd776a710d3 + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py + METHOD: validate_with_previous_doc + """ config = { "Purchase Order": { "ref_dn_field": "purchase_order", From a187f29707f7be4e2534356c76d0ca642e56bb7d Mon Sep 17 00:00:00 2001 From: Rohan Date: Fri, 24 May 2024 22:23:47 +0530 Subject: [PATCH 04/64] ci: track overrides for stock and manufacturing cycles (#90) Co-authored-by: Rohan Bansal --- inventory_tools/hooks.py | 8 +- .../inventory_tools/overrides/job_card.py | 7 + .../overrides/production_plan.py | 100 ++++++----- .../inventory_tools/overrides/stock_entry.py | 15 ++ .../inventory_tools/overrides/warehouse.py | 161 +++++++++--------- .../inventory_tools/overrides/work_order.py | 34 +++- 6 files changed, 198 insertions(+), 127 deletions(-) diff --git a/inventory_tools/hooks.py b/inventory_tools/hooks.py index 03f91bb..1838e3f 100644 --- a/inventory_tools/hooks.py +++ b/inventory_tools/hooks.py @@ -109,14 +109,14 @@ # Override standard doctype classes override_doctype_class = { - "Work Order": "inventory_tools.inventory_tools.overrides.work_order.InventoryToolsWorkOrder", + "Job Card": "inventory_tools.inventory_tools.overrides.job_card.InventoryToolsJobCard", + "Production Plan": "inventory_tools.inventory_tools.overrides.production_plan.InventoryToolsProductionPlan", "Purchase Invoice": "inventory_tools.inventory_tools.overrides.purchase_invoice.InventoryToolsPurchaseInvoice", "Purchase Order": "inventory_tools.inventory_tools.overrides.purchase_order.InventoryToolsPurchaseOrder", "Purchase Receipt": "inventory_tools.inventory_tools.overrides.purchase_receipt.InventoryToolsPurchaseReceipt", - "Production Plan": "inventory_tools.inventory_tools.overrides.production_plan.InventoryToolsProductionPlan", - "Stock Entry": "inventory_tools.inventory_tools.overrides.stock_entry.InventoryToolsStockEntry", - "Job Card": "inventory_tools.inventory_tools.overrides.job_card.InventoryToolsJobCard", "Sales Order": "inventory_tools.inventory_tools.overrides.sales_order.InventoryToolsSalesOrder", + "Stock Entry": "inventory_tools.inventory_tools.overrides.stock_entry.InventoryToolsStockEntry", + "Work Order": "inventory_tools.inventory_tools.overrides.work_order.InventoryToolsWorkOrder", } diff --git a/inventory_tools/inventory_tools/overrides/job_card.py b/inventory_tools/inventory_tools/overrides/job_card.py index f86de01..263336c 100644 --- a/inventory_tools/inventory_tools/overrides/job_card.py +++ b/inventory_tools/inventory_tools/overrides/job_card.py @@ -8,6 +8,13 @@ class InventoryToolsJobCard(JobCard): def validate_job_card(self): + """ + HASH: 4d34b1ead73baf4c5430a2ecbe44b9e8468d7626 + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/manufacturing/doctype/job_card/job_card.py + METHOD: validate_job_card + """ + if ( self.work_order and frappe.get_cached_value("Work Order", self.work_order, "status") == "Stopped" diff --git a/inventory_tools/inventory_tools/overrides/production_plan.py b/inventory_tools/inventory_tools/overrides/production_plan.py index ce6d41a..98e11e9 100644 --- a/inventory_tools/inventory_tools/overrides/production_plan.py +++ b/inventory_tools/inventory_tools/overrides/production_plan.py @@ -1,43 +1,57 @@ -import json - -import frappe -from erpnext.manufacturing.doctype.production_plan.production_plan import ProductionPlan -from erpnext.manufacturing.doctype.work_order.work_order import get_default_warehouse - - -class InventoryToolsProductionPlan(ProductionPlan): - @frappe.whitelist() - def make_work_order(self): - wo_list, po_list = [], [] - subcontracted_po = {} - default_warehouses = get_default_warehouse() - - self.make_work_order_for_finished_goods(wo_list, default_warehouses) - self.make_work_order_for_subassembly_items(wo_list, subcontracted_po, default_warehouses) - if frappe.get_value("Inventory Tools Settings", self.company, "create_purchase_orders"): - self.make_subcontracted_purchase_order(subcontracted_po, po_list) - self.show_list_created_message("Work Order", wo_list) - self.show_list_created_message("Purchase Order", po_list) - - def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po, default_warehouses): - for row in self.sub_assembly_items: - if row.type_of_manufacturing == "Subcontract": - subcontracted_po.setdefault(row.supplier, []).append(row) - if not frappe.get_value( - "Inventory Tools Settings", self.company, "enable_work_order_subcontracting" - ): - continue - - if row.type_of_manufacturing == "Material Request": - continue - - work_order_data = { - "wip_warehouse": default_warehouses.get("wip_warehouse"), - "fg_warehouse": default_warehouses.get("fg_warehouse"), - "company": self.get("company"), - } - - self.prepare_data_for_sub_assembly_items(row, work_order_data) - work_order = self.create_work_order(work_order_data) - if work_order: - wo_list.append(work_order) +import json + +import frappe +from erpnext.manufacturing.doctype.production_plan.production_plan import ProductionPlan +from erpnext.manufacturing.doctype.work_order.work_order import get_default_warehouse + + +class InventoryToolsProductionPlan(ProductionPlan): + @frappe.whitelist() + def make_work_order(self): + """ + HASH: b087fb3d549462ea8c9d1e65e8622e952d4039f6 + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/manufacturing/doctype/production_plan/production_plan.py + METHOD: make_work_order + """ + + wo_list, po_list = [], [] + subcontracted_po = {} + default_warehouses = get_default_warehouse() + + self.make_work_order_for_finished_goods(wo_list, default_warehouses) + self.make_work_order_for_subassembly_items(wo_list, subcontracted_po, default_warehouses) + if frappe.get_value("Inventory Tools Settings", self.company, "create_purchase_orders"): + self.make_subcontracted_purchase_order(subcontracted_po, po_list) + self.show_list_created_message("Work Order", wo_list) + self.show_list_created_message("Purchase Order", po_list) + + def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po, default_warehouses): + """ + HASH: b087fb3d549462ea8c9d1e65e8622e952d4039f6 + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/manufacturing/doctype/production_plan/production_plan.py + METHOD: make_work_order_for_subassembly_items + """ + + for row in self.sub_assembly_items: + if row.type_of_manufacturing == "Subcontract": + subcontracted_po.setdefault(row.supplier, []).append(row) + if not frappe.get_value( + "Inventory Tools Settings", self.company, "enable_work_order_subcontracting" + ): + continue + + if row.type_of_manufacturing == "Material Request": + continue + + work_order_data = { + "wip_warehouse": default_warehouses.get("wip_warehouse"), + "fg_warehouse": default_warehouses.get("fg_warehouse"), + "company": self.get("company"), + } + + self.prepare_data_for_sub_assembly_items(row, work_order_data) + work_order = self.create_work_order(work_order_data) + if work_order: + wo_list.append(work_order) diff --git a/inventory_tools/inventory_tools/overrides/stock_entry.py b/inventory_tools/inventory_tools/overrides/stock_entry.py index 89f894a..7f5919d 100644 --- a/inventory_tools/inventory_tools/overrides/stock_entry.py +++ b/inventory_tools/inventory_tools/overrides/stock_entry.py @@ -9,6 +9,11 @@ class InventoryToolsStockEntry(StockEntry): def check_if_operations_completed(self): """ + HASH: 153e0ba81b62acc170a951a289363fff5579edc7 + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/stock/doctype/stock_entry/stock_entry.py + METHOD: check_if_operations_completed + Original code checks that the stock entry amount plus what's already produced in the WO is not larger than any operation's completed quantity (plus the overallowance amount). Since customized code rewires so stock entries happen via a Job Card, the function now @@ -45,6 +50,11 @@ def check_if_operations_completed(self): def validate_finished_goods(self): """ + HASH: 153e0ba81b62acc170a951a289363fff5579edc7 + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/stock/doctype/stock_entry/stock_entry.py + METHOD: validate_finished_goods + 1. Check if FG exists (mfg, repack) 2. Check if Multiple FG Items are present (mfg) 3. Check FG Item and Qty against WO if present (mfg) @@ -105,6 +115,11 @@ def validate_finished_goods(self): def get_pending_raw_materials(self, backflush_based_on=None): """ + HASH: 153e0ba81b62acc170a951a289363fff5579edc7 + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/stock/doctype/stock_entry/stock_entry.py + METHOD: get_pending_raw_materials + issue (item quantity) that is pending to issue or desire to transfer, whichever is less """ diff --git a/inventory_tools/inventory_tools/overrides/warehouse.py b/inventory_tools/inventory_tools/overrides/warehouse.py index dc021b5..00ec300 100644 --- a/inventory_tools/inventory_tools/overrides/warehouse.py +++ b/inventory_tools/inventory_tools/overrides/warehouse.py @@ -1,77 +1,84 @@ -# Copyright (c) 2023, AgriTheory and contributors -# For license information, please see license.txt - -import frappe -from frappe.desk.reportview import get_filters_cond, get_match_cond -from frappe.desk.search import search_link - - -@frappe.whitelist() -def update_warehouse_path(doc, method=None) -> None: - if not frappe.db.exists("Inventory Tools Settings", doc.company): - return - warehouse_path = frappe.db.get_value( - "Inventory Tools Settings", doc.company, "update_warehouse_path" - ) - if not warehouse_path: - return - - def get_parents(doc): - parents = [doc.warehouse_name] - parent = doc.parent_warehouse - while parent: - parent_name = frappe.get_value("Warehouse", parent, "warehouse_name") - if parent_name != "All Warehouses": - parents.append(parent_name) - parent = frappe.get_value("Warehouse", parent, "parent_warehouse") - else: - break - return parents - - def _update_warehouse_path(doc): - parents = get_parents(doc) - if parents: - if len(parents) > 1: - if parents[1] in parents[0]: - parents[0] = parents[0].replace(parents[1], "") - parents[0] = parents[0].replace(" - ", "") - return " \u21D2 ".join(parents[::-1]) - else: - return "" - - doc.warehouse_path = _update_warehouse_path(doc) - - -@frappe.whitelist() -def warehouse_query(doctype, txt, searchfield, start, page_len, filters): - company = frappe.defaults.get_defaults().get("company") - if not company: - return search_link(doctype, txt, searchfield, start, page_len, filters) - if not frappe.db.exists("Inventory Tools Settings", company) and frappe.db.get_value( - "Inventory Tools Settings", company, "update_warehouse_path" - ): - return search_link(doctype, txt, searchfield, start, page_len, filters) - else: - doctype = "Warehouse" - conditions = [] - searchfields = frappe.get_meta(doctype).get_search_fields() - searchfields.remove("name") - searchfields = ["name"] + searchfields - - return frappe.db.sql( - f"""SELECT {', '.join(searchfields)} - FROM `tabWarehouse` - WHERE `tabWarehouse`.`{searchfield}` like %(txt)s - {get_filters_cond(doctype, filters, conditions).replace("%", "%%")} - {get_match_cond(doctype).replace("%", "%%")} - ORDER BY - IF(LOCATE(%(_txt)s, name), LOCATE(%(_txt)s, name), 99999), - idx DESC, name - LIMIT %(start)s, %(page_len)s""", - { - "txt": "%" + txt + "%", - "_txt": txt.replace("%", ""), - "start": start or 0, - "page_len": page_len or 20, - }, - ) +# Copyright (c) 2023, AgriTheory and contributors +# For license information, please see license.txt + +import frappe +from frappe.desk.reportview import get_filters_cond, get_match_cond +from frappe.desk.search import search_link + + +@frappe.whitelist() +def update_warehouse_path(doc, method=None) -> None: + if not frappe.db.exists("Inventory Tools Settings", doc.company): + return + warehouse_path = frappe.db.get_value( + "Inventory Tools Settings", doc.company, "update_warehouse_path" + ) + if not warehouse_path: + return + + def get_parents(doc): + parents = [doc.warehouse_name] + parent = doc.parent_warehouse + while parent: + parent_name = frappe.get_value("Warehouse", parent, "warehouse_name") + if parent_name != "All Warehouses": + parents.append(parent_name) + parent = frappe.get_value("Warehouse", parent, "parent_warehouse") + else: + break + return parents + + def _update_warehouse_path(doc): + parents = get_parents(doc) + if parents: + if len(parents) > 1: + if parents[1] in parents[0]: + parents[0] = parents[0].replace(parents[1], "") + parents[0] = parents[0].replace(" - ", "") + return " \u21D2 ".join(parents[::-1]) + else: + return "" + + doc.warehouse_path = _update_warehouse_path(doc) + + +@frappe.whitelist() +def warehouse_query(doctype, txt, searchfield, start, page_len, filters): + """ + HASH: 4d34b1ead73baf4c5430a2ecbe44b9e8468d7626 + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/controllers/queries.py + METHOD: warehouse_query + """ + + company = frappe.defaults.get_defaults().get("company") + if not company: + return search_link(doctype, txt, searchfield, start, page_len, filters) + if not frappe.db.exists("Inventory Tools Settings", company) and frappe.db.get_value( + "Inventory Tools Settings", company, "update_warehouse_path" + ): + return search_link(doctype, txt, searchfield, start, page_len, filters) + else: + doctype = "Warehouse" + conditions = [] + searchfields = frappe.get_meta(doctype).get_search_fields() + searchfields.remove("name") + searchfields = ["name"] + searchfields + + return frappe.db.sql( + f"""SELECT {', '.join(searchfields)} + FROM `tabWarehouse` + WHERE `tabWarehouse`.`{searchfield}` like %(txt)s + {get_filters_cond(doctype, filters, conditions).replace("%", "%%")} + {get_match_cond(doctype).replace("%", "%%")} + ORDER BY + IF(LOCATE(%(_txt)s, name), LOCATE(%(_txt)s, name), 99999), + idx DESC, name + LIMIT %(start)s, %(page_len)s""", + { + "txt": "%" + txt + "%", + "_txt": txt.replace("%", ""), + "start": start or 0, + "page_len": page_len or 20, + }, + ) diff --git a/inventory_tools/inventory_tools/overrides/work_order.py b/inventory_tools/inventory_tools/overrides/work_order.py index c093fa6..309e1f5 100644 --- a/inventory_tools/inventory_tools/overrides/work_order.py +++ b/inventory_tools/inventory_tools/overrides/work_order.py @@ -8,11 +8,18 @@ make_stock_entry as _make_stock_entry, ) from frappe import _ -from frappe.utils import flt, get_link_to_form, getdate, nowdate +from frappe.utils import flt, get_link_to_form, getdate class InventoryToolsWorkOrder(WorkOrder): def onload(self): + """ + HASH: 4d34b1ead73baf4c5430a2ecbe44b9e8468d7626 + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/manufacturing/doctype/work_order/work_order.py + METHOD: onload + """ + ms = frappe.get_doc("Manufacturing Settings") self.set_onload("material_consumption", ms.material_consumption) self.set_onload("backflush_raw_materials_based_on", ms.backflush_raw_materials_based_on) @@ -102,8 +109,16 @@ def create_job_card(self): return super().create_job_card() def update_work_order_qty(self): - """Update **Manufactured Qty** and **Material Transferred for Qty** in Work Order - based on Stock Entry""" + """ + HASH: 4d34b1ead73baf4c5430a2ecbe44b9e8468d7626 + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/manufacturing/doctype/work_order/work_order.py + METHOD: update_work_order_qty + + Update **Manufactured Qty** and **Material Transferred for Qty** in Work Order + based on Stock Entry + """ + allowance_percentage = get_allowance_percentage(self.company, self.bom_no) for purpose, fieldname in ( @@ -140,6 +155,13 @@ def update_work_order_qty(self): self.update_production_plan_status() def update_operation_status(self): + """ + HASH: 4d34b1ead73baf4c5430a2ecbe44b9e8468d7626 + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/manufacturing/doctype/work_order/work_order.py + METHOD: update_operation_status + """ + allowance_percentage = get_allowance_percentage(self.company, self.bom_no) max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage / 100 * flt(self.qty)) @@ -156,6 +178,12 @@ def update_operation_status(self): frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'")) def validate_qty(self): + """ + HASH: 4d34b1ead73baf4c5430a2ecbe44b9e8468d7626 + REPO: https://github.com/frappe/erpnext/ + PATH: erpnext/manufacturing/doctype/work_order/work_order.py + METHOD: validate_qty + """ if not self.qty > 0: frappe.throw(_("Quantity to Manufacture must be greater than 0.")) From e8f300f2d63e47659dcdf65bd8728a0b4c19f6bc Mon Sep 17 00:00:00 2001 From: Tyler Matteson Date: Wed, 29 May 2024 13:42:53 -0400 Subject: [PATCH 05/64] Test alternative workstation (#80) * test: add test for alternative workstation * feat: make alternative workstations configurable * fix: uncomment js code for testing * feat: search alternative workstation names * refactor: pop filters that cause error for Workstation * chore: update override commit hash --------- Co-authored-by: Heather Kusmierz --- .flake8 | 1 + .../inventory_tools/custom/operation.json | 2 +- .../alternative_workstation.json} | 2 +- .../alternative_workstation.py} | 4 +- .../inventory_tools_settings.json | 23 ++++--- .../inventory_tools/overrides/operation.py | 4 +- .../overrides/purchase_invoice.py | 2 +- .../inventory_tools/overrides/stock_entry.py | 1 + .../inventory_tools/overrides/workstation.py | 68 ++++++++++++------- .../quotation_demand/quotation_demand.py | 1 - inventory_tools/patches.txt | 1 + .../patches/rename_alternative_workstation.py | 11 +++ inventory_tools/public/js/job_card_custom.js | 3 +- .../public/js/work_order_custom.js | 1 + inventory_tools/tests/fixtures.py | 1 + inventory_tools/tests/setup.py | 2 +- .../tests/test_alternative_workstation.py | 37 ++++++++++ inventory_tools/tests/test_uom.py | 24 +++++++ 18 files changed, 144 insertions(+), 44 deletions(-) rename inventory_tools/inventory_tools/doctype/{alternative_workstations/alternative_workstations.json => alternative_workstation/alternative_workstation.json} (94%) rename inventory_tools/inventory_tools/doctype/{alternative_workstations/alternative_workstations.py => alternative_workstation/alternative_workstation.py} (72%) create mode 100644 inventory_tools/patches/rename_alternative_workstation.py create mode 100644 inventory_tools/tests/test_alternative_workstation.py diff --git a/.flake8 b/.flake8 index e783fbb..5400295 100644 --- a/.flake8 +++ b/.flake8 @@ -64,6 +64,7 @@ ignore = W391, W503, W504, + W604, E711, E129, F841, diff --git a/inventory_tools/inventory_tools/custom/operation.json b/inventory_tools/inventory_tools/custom/operation.json index f48d696..6c81292 100644 --- a/inventory_tools/inventory_tools/custom/operation.json +++ b/inventory_tools/inventory_tools/custom/operation.json @@ -44,7 +44,7 @@ "name": "Operation-alternative_workstations", "no_copy": 0, "non_negative": 0, - "options": "Alternative Workstations", + "options": "Alternative Workstation", "owner": "Administrator", "permlevel": 0, "precision": "", diff --git a/inventory_tools/inventory_tools/doctype/alternative_workstations/alternative_workstations.json b/inventory_tools/inventory_tools/doctype/alternative_workstation/alternative_workstation.json similarity index 94% rename from inventory_tools/inventory_tools/doctype/alternative_workstations/alternative_workstations.json rename to inventory_tools/inventory_tools/doctype/alternative_workstation/alternative_workstation.json index 470af37..a1ef665 100644 --- a/inventory_tools/inventory_tools/doctype/alternative_workstations/alternative_workstations.json +++ b/inventory_tools/inventory_tools/doctype/alternative_workstation/alternative_workstation.json @@ -23,7 +23,7 @@ "modified": "2024-02-01 07:04:30.059469", "modified_by": "Administrator", "module": "Inventory Tools", - "name": "Alternative Workstations", + "name": "Alternative Workstation", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/inventory_tools/inventory_tools/doctype/alternative_workstations/alternative_workstations.py b/inventory_tools/inventory_tools/doctype/alternative_workstation/alternative_workstation.py similarity index 72% rename from inventory_tools/inventory_tools/doctype/alternative_workstations/alternative_workstations.py rename to inventory_tools/inventory_tools/doctype/alternative_workstation/alternative_workstation.py index c262a29..581d755 100644 --- a/inventory_tools/inventory_tools/doctype/alternative_workstations/alternative_workstations.py +++ b/inventory_tools/inventory_tools/doctype/alternative_workstation/alternative_workstation.py @@ -1,9 +1,9 @@ # Copyright (c) 2024, AgriTheory and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document -class AlternativeWorkstations(Document): +class AlternativeWorkstation(Document): pass diff --git a/inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.json b/inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.json index aa7da3a..9a9a449 100644 --- a/inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.json +++ b/inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.json @@ -25,7 +25,8 @@ "overproduction_percentage_for_work_order", "section_break_0", "update_warehouse_path", - "section_break_gzcbr", + "column_break_ddssn", + "allow_alternative_workstations", "uoms_section", "enforce_uoms" ], @@ -79,7 +80,8 @@ }, { "fieldname": "section_break_0", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Warehouses and Workstations" }, { "default": "0", @@ -87,11 +89,6 @@ "fieldtype": "Check", "label": "Update Warehouse Path" }, - { - "fieldname": "section_break_gzcbr", - "fieldtype": "Section Break", - "label": "Warehouses" - }, { "fieldname": "uoms_section", "fieldtype": "Section Break", @@ -146,11 +143,21 @@ "fieldtype": "Link", "label": "Aggregated Sales Warehouse", "options": "Warehouse" + }, + { + "fieldname": "column_break_ddssn", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "allow_alternative_workstations", + "fieldtype": "Check", + "label": "Allow Alternative Workstations" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-04-12 10:02:32.688310", + "modified": "2024-05-13 13:19:00.665445", "modified_by": "Administrator", "module": "Inventory Tools", "name": "Inventory Tools Settings", diff --git a/inventory_tools/inventory_tools/overrides/operation.py b/inventory_tools/inventory_tools/overrides/operation.py index d649d48..6d5c393 100644 --- a/inventory_tools/inventory_tools/overrides/operation.py +++ b/inventory_tools/inventory_tools/overrides/operation.py @@ -1,8 +1,8 @@ import frappe -def validate_alternative_workstation(self, method): +def validate_alternative_workstation(self, method=None): if self.workstation: for row in self.alternative_workstations: if row.workstation == self.workstation: - frappe.throw("Default Workstation should not be selected as alternative workstation") + frappe.throw(frappe._("Default Workstation should not be selected as alternative workstation")) diff --git a/inventory_tools/inventory_tools/overrides/purchase_invoice.py b/inventory_tools/inventory_tools/overrides/purchase_invoice.py index 87b8c15..200e89f 100644 --- a/inventory_tools/inventory_tools/overrides/purchase_invoice.py +++ b/inventory_tools/inventory_tools/overrides/purchase_invoice.py @@ -13,7 +13,7 @@ class InventoryToolsPurchaseInvoice(PurchaseInvoice): def validate_with_previous_doc(self): """ - HASH: 82d206b709ada758c277bc5a266dbc6ec10d00c8 + HASH: 014486de39dd7da6fe79bf803adcf1b66d890876 REPO: https://github.com/frappe/erpnext/ PATH: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py METHOD: validate_with_previous_doc diff --git a/inventory_tools/inventory_tools/overrides/stock_entry.py b/inventory_tools/inventory_tools/overrides/stock_entry.py index 7f5919d..e08791f 100644 --- a/inventory_tools/inventory_tools/overrides/stock_entry.py +++ b/inventory_tools/inventory_tools/overrides/stock_entry.py @@ -169,6 +169,7 @@ def get_pending_raw_materials(self, backflush_based_on=None): @frappe.whitelist() +@frappe.read_only() def get_production_item_if_work_orders_for_required_item_exists(stock_entry_name: str) -> str: stock_entry = frappe.get_doc("Stock Entry", stock_entry_name) diff --git a/inventory_tools/inventory_tools/overrides/workstation.py b/inventory_tools/inventory_tools/overrides/workstation.py index 2d3689f..1bce0f4 100644 --- a/inventory_tools/inventory_tools/overrides/workstation.py +++ b/inventory_tools/inventory_tools/overrides/workstation.py @@ -1,6 +1,6 @@ -import json - import frappe +from frappe.desk.reportview import execute +from frappe.desk.search import search_link """ This function fetch workstation of the document operation. @@ -25,38 +25,54 @@ @frappe.whitelist() +@frappe.read_only() @frappe.validate_and_sanitize_search_inputs def get_alternative_workstations(doctype, txt, searchfield, start, page_len, filters): + company = filters.get("company") or frappe.defaults.get_defaults().get("company") + if not frappe.get_cached_value( + "Inventory Tools Settings", company, "allow_alternative_workstations" + ): + filters.pop("operation") if "operation" in filters else True + filters.pop("company") if "company" in filters else True + return execute( + "Workstation", + filters=filters, + fields=[searchfield], + limit_start=start, + limit_page_length=page_len, + as_list=True, + ) + operation = filters.get("operation") if not operation: frappe.throw("Please select a Operation first.") - if txt: - searchfields = frappe.get_meta(doctype).get_search_fields() - searchfields = " or ".join(["ws." + field + f" LIKE '%{txt}%'" for field in searchfields]) - - conditions = "" - if txt and searchfields: - conditions = f"and ({searchfields})" + searchfields = list(reversed(frappe.get_meta(doctype).get_search_fields())) + select = ",\n".join([f"`tabWorkstation`.{field}" for field in searchfields]) + search_text = "AND `tabAlternative Workstation`.workstation LIKE %(txt)s" if txt else "" workstation = frappe.db.sql( - """ - Select aw.workstation, ws.workstation_type, ws.description - From `tabOperation` as op - Left Join `tabAlternative Workstations` as aw ON aw.parent = op.name - Left Join `tabWorkstation` as ws ON ws.name = aw.workstation - Where op.name = '{operation}' {conditions} - """.format( - conditions=conditions, operation=operation - ) + f""" + SELECT DISTINCT {select} + FROM `tabOperation`, `tabWorkstation`, `tabAlternative Workstation` + WHERE `tabWorkstation`.name = `tabAlternative Workstation`.workstation + AND `tabAlternative Workstation`.parent = %(operation)s + {search_text} + """, + {"operation": operation, "txt": f"%{txt}%"}, + as_list=True, ) - default_workstation = frappe.db.get_value("Operation", operation, "workstation") - flag = True - for row in workstation: - if row[0] == None: - workstation = ((default_workstation,),) - flag = False - if flag: - workstation += ((default_workstation,),) + default_workstation_name = frappe.db.get_value("Operation", operation, "workstation") + default_workstation_fields = frappe.db.get_values( + "Workstation", default_workstation_name, searchfields, as_dict=True + ) + if default_workstation_name not in [row[0] for row in workstation]: + _default = tuple( + [ + default_workstation_fields[0].name, + f"{frappe.bold('Default')} - {','.join([v for k, v in default_workstation_fields[0].items() if k != 'name'])}", + ] + ) + workstation.insert(0, _default) return workstation diff --git a/inventory_tools/inventory_tools/report/quotation_demand/quotation_demand.py b/inventory_tools/inventory_tools/report/quotation_demand/quotation_demand.py index 5b4bd74..325523d 100644 --- a/inventory_tools/inventory_tools/report/quotation_demand/quotation_demand.py +++ b/inventory_tools/inventory_tools/report/quotation_demand/quotation_demand.py @@ -162,7 +162,6 @@ def create(company, filters, rows): rows = [frappe._dict(r) for r in json.loads(rows)] if isinstance(rows, str) else rows if not rows: return - print(rows) counter = 0 settings = frappe.get_doc("Inventory Tools Settings", company) requesting_companies = list({row.company for row in rows}) diff --git a/inventory_tools/patches.txt b/inventory_tools/patches.txt index e69de29..97b629c 100644 --- a/inventory_tools/patches.txt +++ b/inventory_tools/patches.txt @@ -0,0 +1 @@ +inventory_tools.patches.rename_alternative_workstation # Tyler Matteson 5/13/24 \ No newline at end of file diff --git a/inventory_tools/patches/rename_alternative_workstation.py b/inventory_tools/patches/rename_alternative_workstation.py new file mode 100644 index 0000000..7ca7c6c --- /dev/null +++ b/inventory_tools/patches/rename_alternative_workstation.py @@ -0,0 +1,11 @@ +import frappe +from frappe.model.rename_doc import rename_doc + + +def execute(): + if frappe.db.exists("DocType", "Alternative Workstations"): + rename_doc( + "DocType", "Alternative Workstations", "Alternative Workstation", ignore_if_exists=True + ) + + frappe.reload_doc("inventory_tools", "doctype", "alternative_workstation", force=True) diff --git a/inventory_tools/public/js/job_card_custom.js b/inventory_tools/public/js/job_card_custom.js index b46df09..02749df 100644 --- a/inventory_tools/public/js/job_card_custom.js +++ b/inventory_tools/public/js/job_card_custom.js @@ -14,7 +14,8 @@ function set_workstation_query(frm) { return { query: 'inventory_tools.inventory_tools.overrides.workstation.get_alternative_workstations', filters: { - operation: doc.operation, + operation: frm.doc.operation, + company: frm.doc.company, }, } }) diff --git a/inventory_tools/public/js/work_order_custom.js b/inventory_tools/public/js/work_order_custom.js index 7a87ae6..0a26a8a 100644 --- a/inventory_tools/public/js/work_order_custom.js +++ b/inventory_tools/public/js/work_order_custom.js @@ -24,6 +24,7 @@ function get_workstations(frm) { query: 'inventory_tools.inventory_tools.overrides.workstation.get_alternative_workstations', filters: { operation: d.operation, + company: frm.doc.company, }, } }) diff --git a/inventory_tools/tests/fixtures.py b/inventory_tools/tests/fixtures.py index aead89c..927a115 100644 --- a/inventory_tools/tests/fixtures.py +++ b/inventory_tools/tests/fixtures.py @@ -488,6 +488,7 @@ "item_price": 0.02, "default_warehouse": "Storeroom - APC", "supplier": ["Freedom Provisions", "Unity Bakery Supply"], + "uom_conversion_detail": {"Box": 100}, }, { "item_code": "Salt", diff --git a/inventory_tools/tests/setup.py b/inventory_tools/tests/setup.py index f7dc0f2..09a6d15 100644 --- a/inventory_tools/tests/setup.py +++ b/inventory_tools/tests/setup.py @@ -622,7 +622,7 @@ def create_production_plan(settings, prod_plan_from_doc): job_card = frappe.get_doc("Job Card", job_card) job_card.time_logs[0].completed_qty = wo.qty job_card.save() - job_card.submit() + job_card.submit() # don't submit to test alternative workstations def create_fruit_material_request(settings): diff --git a/inventory_tools/tests/test_alternative_workstation.py b/inventory_tools/tests/test_alternative_workstation.py new file mode 100644 index 0000000..2b43bf9 --- /dev/null +++ b/inventory_tools/tests/test_alternative_workstation.py @@ -0,0 +1,37 @@ +import frappe +import pytest + + +@pytest.mark.order(45) +def test_alternative_workstation_query(): + # test default settings + frappe.call( + "frappe.desk.search.search_link", + **{ + "doctype": "Workstation", + "txt": "", + "reference_doctype": "Job Card", + }, + ) + assert len(frappe.response.results) == 16 # all workstations + + # test with inventory tools settings + inventory_tools_settings = frappe.get_doc( + "Inventory Tools Settings", frappe.defaults.get_defaults().get("company") + ) + inventory_tools_settings.allow_alternative_workstations = True + inventory_tools_settings.save() + frappe.call( + "frappe.desk.search.search_link", + **{ + "doctype": "Workstation", + "txt": "", + "query": "inventory_tools.inventory_tools.overrides.workstation.get_alternative_workstations", + "filters": {"operation": "Gather Pie Filling Ingredients"}, + "reference_doctype": "Job Card", + }, + ) + assert len(frappe.response.results) == 2 + assert frappe.response.results[0].get("value") == "Food Prep Table 1" # default returns first + assert "Default" in frappe.response.results[0].get("description") + assert frappe.response.results[1].get("value") == "Food Prep Table 2" diff --git a/inventory_tools/tests/test_uom.py b/inventory_tools/tests/test_uom.py index 32a9d0c..9c7e384 100644 --- a/inventory_tools/tests/test_uom.py +++ b/inventory_tools/tests/test_uom.py @@ -17,3 +17,27 @@ def test_uom_enforcement_validation(): so.save() assert "Invalid UOM" in exc_info.value.args[0] + + +@pytest.mark.order(41) +def test_uom_enforcement_query(): + inventory_tools_settings = frappe.get_doc( + "Inventory Tools Settings", frappe.defaults.get_defaults().get("company") + ) + inventory_tools_settings.enforce_uoms = True + inventory_tools_settings.save() + frappe.call( + "frappe.desk.search.search_link", + **{ + "doctype": "UOM", + "txt": "", + "query": "inventory_tools.inventory_tools.overrides.uom.uom_restricted_query", + "filters": {"parent": "Parchment Paper"}, + "reference_doctype": "Purchase Order Item", + }, + ) + assert len(frappe.response.results) == 2 + assert frappe.response.results[0].get("value") == "Nos" + assert frappe.response.results[0].get("description") == "1.0" + assert frappe.response.results[1].get("value") == "Box" + assert frappe.response.results[1].get("description") == "100.0" From 06a8e53c705ba75293c4991dc1669efd224d0a3a Mon Sep 17 00:00:00 2001 From: Tyler Matteson Date: Wed, 15 Nov 2023 08:44:20 -0500 Subject: [PATCH 06/64] wip: doctypes and vue setup --- inventory_tools/hooks.py | 4 +- .../doctype/specification/__init__.py | 0 .../doctype/specification/specification.js | 22 +++++ .../doctype/specification/specification.json | 83 +++++++++++++++++ .../doctype/specification/specification.py | 21 +++++ .../specification/test_specification.py | 9 ++ .../specification_attribute/__init__.py | 0 .../specification_attribute.json | 76 +++++++++++++++ .../specification_attribute.py | 9 ++ .../doctype/specification_value/__init__.py | 0 .../specification_value.js | 7 ++ .../specification_value.json | 50 ++++++++++ .../specification_value.py | 9 ++ .../test_specification_value.py | 9 ++ .../{ => custom}/purchase_invoice_custom.js | 0 .../js/{ => custom}/work_order_custom.js | 0 .../public/js/faceted_search.bundle.js | 1 + .../js/faceted_search/AttributeFilter.vue | 2 + .../js/faceted_search/FacetedSearch.vue | 12 +++ .../js/faceted_search/NestedSetFilter.vue | 4 + .../public/js/faceted_search/api.js | 0 .../js/faceted_search/faceted_search.js | 50 ++++++++++ .../public/js/purchase_order_custom.js | 93 ------------------- inventory_tools/tests/fixtures.py | 34 +++++++ inventory_tools/tests/setup.py | 14 +++ package.json | 21 ----- yarn.lock | 8 -- 27 files changed, 414 insertions(+), 124 deletions(-) create mode 100644 inventory_tools/inventory_tools/doctype/specification/__init__.py create mode 100644 inventory_tools/inventory_tools/doctype/specification/specification.js create mode 100644 inventory_tools/inventory_tools/doctype/specification/specification.json create mode 100644 inventory_tools/inventory_tools/doctype/specification/specification.py create mode 100644 inventory_tools/inventory_tools/doctype/specification/test_specification.py create mode 100644 inventory_tools/inventory_tools/doctype/specification_attribute/__init__.py create mode 100644 inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.json create mode 100644 inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.py create mode 100644 inventory_tools/inventory_tools/doctype/specification_value/__init__.py create mode 100644 inventory_tools/inventory_tools/doctype/specification_value/specification_value.js create mode 100644 inventory_tools/inventory_tools/doctype/specification_value/specification_value.json create mode 100644 inventory_tools/inventory_tools/doctype/specification_value/specification_value.py create mode 100644 inventory_tools/inventory_tools/doctype/specification_value/test_specification_value.py rename inventory_tools/public/js/{ => custom}/purchase_invoice_custom.js (100%) rename inventory_tools/public/js/{ => custom}/work_order_custom.js (100%) create mode 100644 inventory_tools/public/js/faceted_search.bundle.js create mode 100644 inventory_tools/public/js/faceted_search/AttributeFilter.vue create mode 100644 inventory_tools/public/js/faceted_search/FacetedSearch.vue create mode 100644 inventory_tools/public/js/faceted_search/NestedSetFilter.vue create mode 100644 inventory_tools/public/js/faceted_search/api.js create mode 100644 inventory_tools/public/js/faceted_search/faceted_search.js delete mode 100644 inventory_tools/public/js/purchase_order_custom.js delete mode 100644 package.json delete mode 100644 yarn.lock diff --git a/inventory_tools/hooks.py b/inventory_tools/hooks.py index 1838e3f..7d0de64 100644 --- a/inventory_tools/hooks.py +++ b/inventory_tools/hooks.py @@ -13,11 +13,11 @@ # include js, css files in header of desk.html # app_include_css = "/assets/inventory_tools/css/inventory_tools.css" -app_include_js = ["inventory_tools.bundle.js"] +app_include_js = ["inventory_tools.bundle.js", "faceted_search.bundle.js"] # include js, css files in header of web template # web_include_css = "/assets/inventory_tools/css/inventory_tools.css" -# web_include_js = "/assets/inventory_tools/js/inventory_tools.js" +web_include_js = ["faceted_search.bundle.js"] # include custom scss in every website theme (without file extension ".scss") # website_theme_scss = "inventory_tools/public/scss/website" diff --git a/inventory_tools/inventory_tools/doctype/specification/__init__.py b/inventory_tools/inventory_tools/doctype/specification/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/inventory_tools/inventory_tools/doctype/specification/specification.js b/inventory_tools/inventory_tools/doctype/specification/specification.js new file mode 100644 index 0000000..41d68b9 --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification/specification.js @@ -0,0 +1,22 @@ +// Copyright (c) 2023, AgriTheory and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Specification Attribute', { + applied_on: (frm, cdt, cdn) => { + let row = locals[cdt][cdn] + if (!row.applied_on) { + return + } + frappe + .xcall('inventory_tools.inventory_tools.doctype.specification.specification.get_data_fieldnames', { + doctype: row.applied_on, + }) + .then(r => { + if (!r) { + return + } + frm.set_df_property('attributes', 'options', r, frm.doc.name, 'field', row.name) + frm.refresh_field('attributes') + }) + }, +}) diff --git a/inventory_tools/inventory_tools/doctype/specification/specification.json b/inventory_tools/inventory_tools/doctype/specification/specification.json new file mode 100644 index 0000000..4890a79 --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification/specification.json @@ -0,0 +1,83 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-11-13 16:45:20.279292", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "dt", + "enabled", + "column_break_conqh", + "apply_on", + "section_break_5", + "attributes" + ], + "fields": [ + { + "fieldname": "dt", + "fieldtype": "Link", + "label": "DocType", + "options": "DocType" + }, + { + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "fieldname": "apply_on", + "fieldtype": "Dynamic Link", + "label": "Apply On", + "options": "dt" + }, + { + "fieldname": "column_break_conqh", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Attributes" + }, + { + "fieldname": "attributes", + "fieldtype": "Table", + "options": "Specification Attribute" + }, + { + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-11-14 16:44:58.895071", + "modified_by": "Administrator", + "module": "Inventory Tools", + "name": "Specification", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "title" +} \ No newline at end of file diff --git a/inventory_tools/inventory_tools/doctype/specification/specification.py b/inventory_tools/inventory_tools/doctype/specification/specification.py new file mode 100644 index 0000000..5e32c95 --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification/specification.py @@ -0,0 +1,21 @@ +# Copyright (c) 2023, AgriTheory and contributors +# For license information, please see license.txt + +import frappe +from frappe.core.doctype.doctype.doctype import no_value_fields, table_fields +from frappe.model.document import Document + + +class Specification(Document): + def validate(self): + self.title = f"{self.dt}" + if self.apply_on: + self.title += f" - {self.apply_on}" + + +@frappe.whitelist() +def get_data_fieldnames(doctype): + meta = frappe.get_meta(doctype) + return sorted( + f.fieldname for f in meta.fields if f.fieldtype not in no_value_fields + table_fields + ) diff --git a/inventory_tools/inventory_tools/doctype/specification/test_specification.py b/inventory_tools/inventory_tools/doctype/specification/test_specification.py new file mode 100644 index 0000000..1ffbea0 --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification/test_specification.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, AgriTheory and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestSpecification(FrappeTestCase): + pass diff --git a/inventory_tools/inventory_tools/doctype/specification_attribute/__init__.py b/inventory_tools/inventory_tools/doctype/specification_attribute/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.json b/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.json new file mode 100644 index 0000000..4122da6 --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.json @@ -0,0 +1,76 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:attribute_name", + "creation": "2023-11-13 16:49:36.888483", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "attribute_name", + "numeric_values", + "required", + "column_break_k0wzm", + "applied_on", + "field" + ], + "fields": [ + { + "columns": 1, + "fieldname": "attribute_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Attribute Name", + "reqd": 1, + "unique": 1 + }, + { + "columns": 1, + "default": "0", + "fieldname": "numeric_values", + "fieldtype": "Check", + "label": "Numeric Values" + }, + { + "fieldname": "column_break_k0wzm", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "required", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Required" + }, + { + "columns": 2, + "fieldname": "field", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Field" + }, + { + "columns": 2, + "fieldname": "applied_on", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Applied On", + "options": "DocType" + } + ], + "icon": "fa fa-edit", + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-11-14 18:28:38.869757", + "modified_by": "Administrator", + "module": "Inventory Tools", + "name": "Specification Attribute", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.py b/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.py new file mode 100644 index 0000000..1b5c767 --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, AgriTheory and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class SpecificationAttribute(Document): + pass diff --git a/inventory_tools/inventory_tools/doctype/specification_value/__init__.py b/inventory_tools/inventory_tools/doctype/specification_value/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/inventory_tools/inventory_tools/doctype/specification_value/specification_value.js b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.js new file mode 100644 index 0000000..0244537 --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.js @@ -0,0 +1,7 @@ +// Copyright (c) 2023, AgriTheory and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Specification Value', { + // refresh: function(frm) { + // } +}) diff --git a/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json new file mode 100644 index 0000000..c360fb3 --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json @@ -0,0 +1,50 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-11-14 16:01:49.101184", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "attribute", + "value" + ], + "fields": [ + { + "fieldname": "attribute", + "fieldtype": "Link", + "label": "Attribute", + "options": "Specification Attribute" + }, + { + "fieldname": "value", + "fieldtype": "Data", + "label": "Value" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-11-14 16:01:49.101184", + "modified_by": "Administrator", + "module": "Inventory Tools", + "name": "Specification Value", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/inventory_tools/inventory_tools/doctype/specification_value/specification_value.py b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.py new file mode 100644 index 0000000..18cd589 --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, AgriTheory and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class SpecificationValue(Document): + pass diff --git a/inventory_tools/inventory_tools/doctype/specification_value/test_specification_value.py b/inventory_tools/inventory_tools/doctype/specification_value/test_specification_value.py new file mode 100644 index 0000000..d0d9199 --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification_value/test_specification_value.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, AgriTheory and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestSpecificationValue(FrappeTestCase): + pass diff --git a/inventory_tools/public/js/purchase_invoice_custom.js b/inventory_tools/public/js/custom/purchase_invoice_custom.js similarity index 100% rename from inventory_tools/public/js/purchase_invoice_custom.js rename to inventory_tools/public/js/custom/purchase_invoice_custom.js diff --git a/inventory_tools/public/js/work_order_custom.js b/inventory_tools/public/js/custom/work_order_custom.js similarity index 100% rename from inventory_tools/public/js/work_order_custom.js rename to inventory_tools/public/js/custom/work_order_custom.js diff --git a/inventory_tools/public/js/faceted_search.bundle.js b/inventory_tools/public/js/faceted_search.bundle.js new file mode 100644 index 0000000..78fc2f7 --- /dev/null +++ b/inventory_tools/public/js/faceted_search.bundle.js @@ -0,0 +1 @@ +import './faceted_search/faceted_search.js' diff --git a/inventory_tools/public/js/faceted_search/AttributeFilter.vue b/inventory_tools/public/js/faceted_search/AttributeFilter.vue new file mode 100644 index 0000000..b291bef --- /dev/null +++ b/inventory_tools/public/js/faceted_search/AttributeFilter.vue @@ -0,0 +1,2 @@ + + diff --git a/inventory_tools/public/js/faceted_search/FacetedSearch.vue b/inventory_tools/public/js/faceted_search/FacetedSearch.vue new file mode 100644 index 0000000..a03e7a2 --- /dev/null +++ b/inventory_tools/public/js/faceted_search/FacetedSearch.vue @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/inventory_tools/public/js/faceted_search/NestedSetFilter.vue b/inventory_tools/public/js/faceted_search/NestedSetFilter.vue new file mode 100644 index 0000000..17d5d20 --- /dev/null +++ b/inventory_tools/public/js/faceted_search/NestedSetFilter.vue @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/inventory_tools/public/js/faceted_search/api.js b/inventory_tools/public/js/faceted_search/api.js new file mode 100644 index 0000000..e69de29 diff --git a/inventory_tools/public/js/faceted_search/faceted_search.js b/inventory_tools/public/js/faceted_search/faceted_search.js new file mode 100644 index 0000000..f0b01e6 --- /dev/null +++ b/inventory_tools/public/js/faceted_search/faceted_search.js @@ -0,0 +1,50 @@ +import FacetedSearch from './FacetedSearch.vue' + +frappe.provide('faceted_search') + +faceted_search.mount = () => { + if (cur_list && ['Item', 'Website Item', 'BOM'].includes(cur_list.doctype)) { + if (faceted_search.$search == undefined && !$('#faceted-search').length) { + $('.filter-section').prepend('') + waitForElement('#faceted-search').then(() => { + faceted_search.$search = new window.Vue({ + el: '#faceted-search', + render: h => h(FacetedSearch, { props: {} }), + }) + }) + } + } +} + +function waitForElement(selector) { + return new Promise(resolve => { + if (document.querySelector(selector)) { + return resolve(document.querySelector(selector)) + } + const observer = new MutationObserver(mutations => { + if (document.querySelector(selector)) { + resolve(document.querySelector(selector)) + observer.disconnect() + } + }) + observer.observe(document.body, { + childList: true, + subtree: true, + }) + }) +} + +waitForElement('[data-route]').then(element => { + let observer = new MutationObserver(() => { + faceted_search.mount() + }) + const config = { attributes: true, childList: false, characterData: true } + observer.observe(element, config) +}) + +if (document.querySelector('#product-filters')) { + faceted_search.$search = new window.Vue({ + el: '#product-filters', + render: h => h(FacetedSearch, { props: {} }), + }) +} diff --git a/inventory_tools/public/js/purchase_order_custom.js b/inventory_tools/public/js/purchase_order_custom.js deleted file mode 100644 index 3142a70..0000000 --- a/inventory_tools/public/js/purchase_order_custom.js +++ /dev/null @@ -1,93 +0,0 @@ -frappe.ui.form.on('Purchase Order', { - refresh: frm => { - show_subcontracting_fields(frm) - setup_item_queries(frm) - fetch_supplier_warehouse(frm) - }, - is_subcontracted: frm => { - if (frm.doc.is_subcontracted) { - show_subcontracting_fields(frm) - } - }, - company: frm => { - setup_item_queries(frm) - fetch_supplier_warehouse(frm) - }, - supplier: frm => { - fetch_supplier_warehouse(frm) - }, -}) - -// TODO: override when a qty changes in item table, it changes the fg_item_qty (assumes same UOM) -// TODO: subcontracting table: autofill fields when a row is manually added and user selects the WO -// TODO: when subcontracting table row removed, adjust Item row fg_item_qty - -function show_subcontracting_fields(frm) { - if (!frm.doc.company || !frm.doc.is_subcontracted) { - hide_field('subcontracting') - return - } - frappe.db - .get_value('Inventory Tools Settings', { company: frm.doc.company }, 'enable_work_order_subcontracting') - .then(r => { - if (r && r.message && r.message.enable_work_order_subcontracting) { - unhide_field('subcontracting') - setTimeout(() => { - frm.remove_custom_button('Purchase Receipt', 'Create') - frm.remove_custom_button('Subcontracting Order', 'Create') - }, 1000) - } else { - hide_field('subcontracting') - } - }) -} - -function setup_item_queries(frm) { - frm.set_query('item_code', 'items', () => { - if (me.frm.doc.is_subcontracted) { - var filters = { supplier: me.frm.doc.supplier } - if (me.frm.doc.is_old_subcontracting_flow) { - filters['is_sub_contracted_item'] = 1 - } else { - frappe.db.get_value('Inventory Tools Settings', frm.doc.company, 'enable_work_order_subcontracting').then(r => { - if (!r.message.enable_work_order_subcontracting) { - filters['is_stock_item'] = 0 - } - }) - } - return { - query: 'erpnext.controllers.queries.item_query', - filters: filters, - } - } else { - return { - query: 'erpnext.controllers.queries.item_query', - filters: { supplier: me.frm.doc.supplier, is_purchase_item: 1, has_variants: 0 }, - } - } - }) -} - -function setup_supplier_warehouse_query(frm) { - frm.set_query('supplier_warehouse', () => { - return { - filters: { is_group: 0 }, - } - }) -} - -function fetch_supplier_warehouse(frm) { - if (!frm.doc.company || !frm.doc.supplier) { - return - } - frappe - .xcall('inventory_tools.inventory_tools.overrides.purchase_invoice.fetch_supplier_warehouse', { - company: frm.doc.company, - supplier: frm.doc.supplier, - }) - .then(r => { - if (r && r.message) { - frm.set_value('supplier_warehouse', r.message.supplier_warehouse) - } - }) -} diff --git a/inventory_tools/tests/fixtures.py b/inventory_tools/tests/fixtures.py index 927a115..8304fcf 100644 --- a/inventory_tools/tests/fixtures.py +++ b/inventory_tools/tests/fixtures.py @@ -1007,3 +1007,37 @@ "TransAmerica Bank Cafeteria", "Whole Harvest Grocery Group", ] + +specifications = [ + { + "dt": "Item Group", + "apply_on": "Item", + "enabled": 1, + "attributes": [ + { + "attribute_name": "Shelf Life", + "applied_on": "Item", + "field": "shelf_life_in_days", + "numeric_values": 1, + }, + { + "attribute_name": "Weight", + "applied_on": "Item", + "field": "weight_per_unit", + "numeric_values": 1, + }, + {"attribute_name": "Fruits", "applied_on": "Item"}, + {"attribute_name": "Brand", "applied_on": "Item", "field": "brand"}, + ], + } +] + + +attributes = { + {"Ambrosia Pie": []}, + {"Double Plum Pie": []}, + { + "Gooseberry Pie": [], + }, + {"Kaduka Key Lime Pie": []}, +} diff --git a/inventory_tools/tests/setup.py b/inventory_tools/tests/setup.py index 09a6d15..fdf49c3 100644 --- a/inventory_tools/tests/setup.py +++ b/inventory_tools/tests/setup.py @@ -4,6 +4,7 @@ import frappe from erpnext.accounts.doctype.account.account import update_account_number +from erpnext.e_commerce.doctype.website_item.website_item import make_website_item from erpnext.manufacturing.doctype.production_plan.production_plan import ( get_items_for_material_requests, ) @@ -240,6 +241,11 @@ def create_item_groups(settings): ig.parent_item_group = "All Item Groups" ig.save() + if not frappe.db.exists("Brand", "Ambrosia Pie Co"): + brand = frappe.new_doc("Brand") + brand.brand = "Ambrosia Pie Co" + brand.save() + def create_price_lists(settings): if not frappe.db.exists("Price List", "Bakery Buying"): @@ -287,6 +293,7 @@ def create_items(settings): i.valuation_rate = item.get("valuation_rate") or 0 i.is_sub_contracted_item = item.get("is_sub_contracted_item") or 0 i.default_warehouse = settings.get("warehouse") + i.weight_uom = "Pound" if i.is_stock_item else None i.default_material_request_type = ( "Purchase" if item.get("item_group") in ("Bakery Supplies", "Ingredients") @@ -304,6 +311,8 @@ def create_items(settings): else 0 ) i.is_sales_item = 1 if item.get("item_group") == "Baked Goods" else 0 + i.sales_uom = "Each" if i.is_sales_item else None + i.brand = "Ambrosia Pie Co" if i.is_sales_item else None i.append( "item_defaults", { @@ -348,6 +357,11 @@ def create_items(settings): ) se.save() se.submit() + if i.is_sales_item: + website_item = make_website_item(i, True) + website_item = frappe.get_doc("Website Item", website_item[0]) + website_item.route = f"products/{frappe.scrub(i.name)}" + website_item.save() def create_warehouses(settings): diff --git a/package.json b/package.json deleted file mode 100644 index ef1c815..0000000 --- a/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "inventory_tools", - "scripts": {}, - "dependencies": { - "onscan.js": "^1.5.2" - }, - "devDependencies": {}, - "repository": { - "type": "git", - "url": "https://github.com/agritheory/invnetory_tools.git" - }, - "publishConfig": { - "access": "restricted" - }, - "private": true, - "release": { - "branches": [ - "version-14" - ] - } -} diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index fa1b1d6..0000000 --- a/yarn.lock +++ /dev/null @@ -1,8 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -onscan.js@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/onscan.js/-/onscan.js-1.5.2.tgz#14ed636e5f4c3f0a78bacbf9a505dad3140ee341" - integrity sha512-9oGYy2gXYRjvXO9GYqqVca0VuCTAmWhbmX3egBSBP13rXiMNb+dKPJzKFEeECGqPBpf0m40Zoo+GUQ7eCackdw== From 59b0063a073eb2cd054d64ab37c00bb5fa330bcb Mon Sep 17 00:00:00 2001 From: Tyler Matteson Date: Fri, 24 Nov 2023 16:12:24 -0500 Subject: [PATCH 07/64] wip: specification upload initerface --- .../report/specification/__init__.py | 0 .../report/specification/specification.js | 225 ++++++++++++++++++ .../report/specification/specification.json | 29 +++ .../report/specification/specification.py | 109 +++++++++ inventory_tools/tests/fixtures.py | 4 +- inventory_tools/tests/setup.py | 2 +- 6 files changed, 366 insertions(+), 3 deletions(-) create mode 100644 inventory_tools/inventory_tools/report/specification/__init__.py create mode 100644 inventory_tools/inventory_tools/report/specification/specification.js create mode 100644 inventory_tools/inventory_tools/report/specification/specification.json create mode 100644 inventory_tools/inventory_tools/report/specification/specification.py diff --git a/inventory_tools/inventory_tools/report/specification/__init__.py b/inventory_tools/inventory_tools/report/specification/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/inventory_tools/inventory_tools/report/specification/specification.js b/inventory_tools/inventory_tools/report/specification/specification.js new file mode 100644 index 0000000..e0aed5e --- /dev/null +++ b/inventory_tools/inventory_tools/report/specification/specification.js @@ -0,0 +1,225 @@ +// Copyright (c) 2023, AgriTheory and contributors +// For license information, please see license.txt + +frappe.query_reports['Specification'] = { + filters: [ + { + fieldname: 'specification', + label: __('Specification'), + options: 'Specification', + fieldtype: 'Link', + reqd: 1, + }, + ], + get_datatable_options(options) { + return Object.assign(options, { + treeView: true, + checkedRowStatus: false, + checkboxColumn: true, + events: { + onCheckRow: row => { + update_selection(row) + }, + }, + }) + }, + onload: reportview => { + manage_buttons(reportview) + }, + refresh: reportview => { + manage_buttons(reportview) + }, +} + +function manage_buttons(reportview) { + reportview.page.add_inner_button( + 'Create PO(s)', + function () { + create('po') + }, + 'Create' + ) + + reportview.page.add_inner_button( + 'Create RFQ(s)', + function () { + create('rfq') + }, + 'Create' + ) + + reportview.page.add_inner_button( + 'Create based on Item', + function () { + create('item_based') + }, + 'Create' + ) + + // these don't seem to be working + $(".btn-default:contains('Create Card')").addClass('hidden') + $(".btn-default:contains('Set Chart')").addClass('hidden') +} + +async function create(type) { + let values = frappe.query_report.get_filter_values() + let company = undefined + let email_template = undefined + if (type != 'po') { + values = await select_company_and_email_template(values.company) + company = values['company'] + email_template = values['email_template'] + } else { + if (!values.company) { + company = await select_company() + } else { + company = values.company + } + } + + let selected_rows = frappe.query_report.datatable.rowmanager.getCheckedRows() + let selected_items = frappe.query_report.datatable.datamanager.data.filter((row, index) => { + return selected_rows.includes(String(index)) ? row : false + }) + if (!selected_items.length) { + frappe.show_alert({ message: 'Please select one or more rows.', seconds: 5, indicator: 'red' }) + } else { + await frappe + .xcall('inventory_tools.inventory_tools.report.material_demand.material_demand.create', { + company: company, + email_template: email_template || '', + filters: values, + creation_type: type, + rows: selected_items, + }) + .then(r => {}) + } +} + +function update_selection(row) { + if (row !== undefined && !row[5].content) { + const toggle = frappe.query_report.datatable.rowmanager.checkMap[row[0].rowIndex] + select_all_supplier_items(row, toggle).then(() => { + update_selected_qty() + }) + } else { + update_selected_qty() + } +} + +function update_selected_qty() { + // iterate all rows for selected items + let item_map = {} + frappe.query_report.datatable.datamanager.data.forEach((supplier_row, index) => { + if (frappe.query_report.datatable.rowmanager.checkMap[index]) { + if (supplier_row.item_code && !item_map[supplier_row.item_code]) { + item_map[supplier_row.item_code] = supplier_row.qty + } else if (supplier_row.item_code && item_map[supplier_row.item_code]) { + item_map[supplier_row.item_code] += supplier_row.qty + } + } + }) + frappe.query_report.datatable.datamanager.data.forEach((supplier_row, index) => { + if (supplier_row.item_code in item_map) { + let supplier_price = Number(String(supplier_row.supplier_price).replace(/[^0-9\.-]+/g, '')) + let total_selected = item_map[supplier_row.item_code] + let selected_price = item_map[supplier_row.item_code] * (supplier_price || 0) + selected_price = format_currency(selected_price, supplier_row.currency, 2) + if (item_map[supplier_row.item_code] > supplier_row.total_demand) { + total_selected = `${total_selected}` + selected_price = `${selected_price}` + } + frappe.query_report.datatable.cellmanager.updateCell(9, index, total_selected, true) + frappe.query_report.datatable.cellmanager.updateCell(12, index, selected_price, true) + } else { + frappe.query_report.datatable.cellmanager.updateCell(9, index, '', true) + frappe.query_report.datatable.cellmanager.updateCell(12, index, '', true) + } + }) +} + +async function select_all_supplier_items(row, toggle) { + return new Promise(resolve => { + if (frappe.query_report.datatable.datamanager._filteredRows) { + frappe.query_report.datatable.datamanager._filteredRows.forEach(f => { + if (f[2].content === row[1].content) { + frappe.query_report.datatable.rowmanager.checkMap.splice(row[0].rowIndex, 0, toggle ? 1 : 0) + $(row[0].content).find('input').check = toggle + } else { + frappe.query_report.datatable.rowmanager.checkMap.splice(f[0].rowIndex, 0, 0) + } + }) + } else { + frappe.query_report.datatable.datamanager.rows.forEach(f => { + if (f[2].content === row[2].content) { + frappe.query_report.datatable.rowmanager.checkMap.splice(row[0].rowIndex, 0, toggle ? 1 : 0) + let input = $(frappe.query_report.datatable.rowmanager.getRow$(f[0].rowIndex)).find('input') + if (input[0]) { + input[0].checked = toggle + } + } else { + frappe.query_report.datatable.rowmanager.checkMap.splice(f[0].rowIndex, 0, 0) + } + }) + } + resolve() + }) +} + +async function select_company() { + return new Promise(resolve => { + let dialog = new frappe.ui.Dialog({ + title: __('Select a Company'), + fields: [ + { + fieldtype: 'Link', + fieldname: 'company', + label: 'Company', + options: 'Company', + reqd: 1, + }, + ], + primary_action: () => { + let values = dialog.get_values() + dialog.hide() + return resolve(values.company) + }, + primary_action_label: __('Select'), + }) + dialog.show() + dialog.get_close_btn() + }) +} + +async function select_company_and_email_template(company) { + return new Promise(resolve => { + let dialog = new frappe.ui.Dialog({ + title: __('Select Company and Email Template'), + fields: [ + { + fieldtype: 'Link', + fieldname: 'company', + label: 'Company', + options: 'Company', + reqd: 1, + default: company, + }, + { + fieldtype: 'Link', + fieldname: 'email_template', + label: 'Email Template', + options: 'Email Template', + reqd: 1, + }, + ], + primary_action: () => { + let values = dialog.get_values() + dialog.hide() + return resolve(values) + }, + primary_action_label: __('Select'), + }) + dialog.show() + dialog.get_close_btn() + }) +} diff --git a/inventory_tools/inventory_tools/report/specification/specification.json b/inventory_tools/inventory_tools/report/specification/specification.json new file mode 100644 index 0000000..6706ccb --- /dev/null +++ b/inventory_tools/inventory_tools/report/specification/specification.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2023-11-24 15:51:03.630333", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2023-11-24 15:51:03.630333", + "modified_by": "Administrator", + "module": "Inventory Tools", + "name": "Specification", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Specification", + "report_name": "Specification", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Item Manager" + } + ] +} \ No newline at end of file diff --git a/inventory_tools/inventory_tools/report/specification/specification.py b/inventory_tools/inventory_tools/report/specification/specification.py new file mode 100644 index 0000000..56b1771 --- /dev/null +++ b/inventory_tools/inventory_tools/report/specification/specification.py @@ -0,0 +1,109 @@ +# Copyright (c) 2023, AgriTheory and contributors +# For license information, please see license.txt + +import frappe + + +def execute(filters=None): + columns, data = [], [] + return get_columns(filters), data + + +def get_columns(filters): + file_export = True if frappe.form_dict.cmd == "frappe.desk.query_report.export_query" else False + doc = frappe.get_doc("Specification", filters.specification) + + # name w/ indent 0 + # attributes w/ indent 1 + + return [ + { + "label": "Supplier", + "fieldname": "supplier", + "fieldtype": "Link", + "options": "Supplier", + "width": "250px", + }, + { + "fieldname": "material_request", + "fieldtype": "Link", + "options": "Material Request", + "label": "Material Request", + "width": "200px", + }, + { + "fieldname": "schedule_date", + "label": "Required By", + "fieldtype": "Date", + "width": "120px", + }, + { + "fieldname": "material_request_item", + "fieldtype": "Data", + "hidden": 1, + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "hidden": 1, + }, + { + "fieldname": "item_code", + "label": "Item", + "fieldtype": "Link", + "options": "Item", + "width": "250px", + }, + {"fieldname": "item_name", "fieldtype": "Data", "hidden": 1}, # unset for export + { + "label": "MR Qty", + "fieldname": "qty", + "fieldtype": "Data", + "width": "90px", + "align": "right", + }, + { + "label": "Total Demand", + "fieldname": "total_demand", + "fieldtype": "Data", + "width": "90px", + "align": "right", + }, + { + "label": "Draft POs", + "fieldname": "draft_po", + "fieldtype": "Data", + "width": "90px", + "align": "right", + }, + { + "label": "Total Selected", + "fieldname": "total_selected", + "fieldtype": "Data", + "width": "90px", + "align": "right", + }, + { + "label": "UOM", + "fieldname": "uom", + "fieldtype": "Link", + "options": "UOM", + "width": "90px", + }, + { + "label": "Price", + "fieldname": "supplier_price", + "fieldtype": "Data", + "width": "90px", + "align": "right", + }, + { + "label": "Selected Amount", + "fieldname": "amount", + "fieldtype": "Data", + "width": "120px", + "align": "right", + }, + {"fieldname": "currency", "fieldtype": "Link", "options": "Currency", "hidden": 1}, + ] diff --git a/inventory_tools/tests/fixtures.py b/inventory_tools/tests/fixtures.py index 8304fcf..96a2f2f 100644 --- a/inventory_tools/tests/fixtures.py +++ b/inventory_tools/tests/fixtures.py @@ -1033,11 +1033,11 @@ ] -attributes = { +attributes = [ {"Ambrosia Pie": []}, {"Double Plum Pie": []}, { "Gooseberry Pie": [], }, {"Kaduka Key Lime Pie": []}, -} +] diff --git a/inventory_tools/tests/setup.py b/inventory_tools/tests/setup.py index fdf49c3..f05dcb1 100644 --- a/inventory_tools/tests/setup.py +++ b/inventory_tools/tests/setup.py @@ -311,7 +311,7 @@ def create_items(settings): else 0 ) i.is_sales_item = 1 if item.get("item_group") == "Baked Goods" else 0 - i.sales_uom = "Each" if i.is_sales_item else None + i.sales_uom = "Nos" if i.is_sales_item else None i.brand = "Ambrosia Pie Co" if i.is_sales_item else None i.append( "item_defaults", From dd3cd17aed21a19924e539573ac6bfa4e13e942c Mon Sep 17 00:00:00 2001 From: Tyler Matteson Date: Thu, 16 Nov 2023 13:51:07 -0500 Subject: [PATCH 08/64] wip: basic filters rendering + test setup --- .../doctype/specification/specification.js | 37 +++++++----- .../doctype/specification/specification.json | 2 +- .../doctype/specification/specification.py | 60 +++++++++++++++++++ .../specification_attribute.json | 15 ++++- .../specification_value.json | 21 +++++-- .../inventory_tools/faceted_search.py | 30 ++++++++++ .../js/faceted_search/AttributeFilter.vue | 28 ++++++++- .../js/faceted_search/FacetedSearch.vue | 28 +++++++-- .../FacetedSearchNumericRange.vue | 56 +++++++++++++++++ .../public/js/faceted_search/api.js | 12 ++++ .../js/faceted_search/faceted_search.js | 9 +++ inventory_tools/tests/fixtures.py | 47 +++++++++++---- inventory_tools/tests/setup.py | 25 ++++++++ 13 files changed, 329 insertions(+), 41 deletions(-) create mode 100644 inventory_tools/inventory_tools/faceted_search.py create mode 100644 inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue diff --git a/inventory_tools/inventory_tools/doctype/specification/specification.js b/inventory_tools/inventory_tools/doctype/specification/specification.js index 41d68b9..8b062ca 100644 --- a/inventory_tools/inventory_tools/doctype/specification/specification.js +++ b/inventory_tools/inventory_tools/doctype/specification/specification.js @@ -3,20 +3,27 @@ frappe.ui.form.on('Specification Attribute', { applied_on: (frm, cdt, cdn) => { - let row = locals[cdt][cdn] - if (!row.applied_on) { - return - } - frappe - .xcall('inventory_tools.inventory_tools.doctype.specification.specification.get_data_fieldnames', { - doctype: row.applied_on, - }) - .then(r => { - if (!r) { - return - } - frm.set_df_property('attributes', 'options', r, frm.doc.name, 'field', row.name) - frm.refresh_field('attributes') - }) + get_data_fieldnames(frm, cdt, cdn) + }, + form_render: (frm, cdt, cdn) => { + get_data_fieldnames(frm, cdt, cdn) }, }) + +function get_data_fieldnames(frm, cdt, cdn) { + let row = locals[cdt][cdn] + if (!row.applied_on) { + return + } + frappe + .xcall('inventory_tools.inventory_tools.doctype.specification.specification.get_data_fieldnames', { + doctype: row.applied_on, + }) + .then(r => { + if (!r) { + return + } + frm.set_df_property('attributes', 'options', r, frm.doc.name, 'field', row.name) + frm.refresh_field('attributes') + }) +} diff --git a/inventory_tools/inventory_tools/doctype/specification/specification.json b/inventory_tools/inventory_tools/doctype/specification/specification.json index 4890a79..0ef2ded 100644 --- a/inventory_tools/inventory_tools/doctype/specification/specification.json +++ b/inventory_tools/inventory_tools/doctype/specification/specification.json @@ -57,7 +57,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-11-14 16:44:58.895071", + "modified": "2023-11-16 10:49:19.728893", "modified_by": "Administrator", "module": "Inventory Tools", "name": "Specification", diff --git a/inventory_tools/inventory_tools/doctype/specification/specification.py b/inventory_tools/inventory_tools/doctype/specification/specification.py index 5e32c95..0c755ed 100644 --- a/inventory_tools/inventory_tools/doctype/specification/specification.py +++ b/inventory_tools/inventory_tools/doctype/specification/specification.py @@ -12,6 +12,66 @@ def validate(self): if self.apply_on: self.title += f" - {self.apply_on}" + def create_linked_values(self, doc, extra_attributes=None): + for at in self.attributes: + if at.field: + existing_attribute_value = frappe.db.get_value( + "Specification Value", + { + "reference_doctype": at.applied_on, + "reference_name": doc.name, + "attribute": at.attribute_name, + }, + ) + if existing_attribute_value: + av = frappe.get_doc("Specification Value", existing_attribute_value) + else: + av = frappe.new_doc("Specification Value") + av.reference_doctype = at.applied_on + av.reference_name = doc.name + av.attribute = at.attribute_name + av.value = frappe.get_value(av.reference_doctype, av.reference_name, at.field) + av.save() + if at.attribute_name in extra_attributes: + if isinstance(extra_attributes[at.attribute_name], (str, int, float)): + existing_attribute_value = frappe.db.get_value( + "Specification Value", + { + "reference_doctype": at.applied_on, + "reference_name": doc.name, + "attribute": at.attribute_name, + }, + ) + if existing_attribute_value: + av = frappe.get_doc("Specification Value", existing_attribute_value) + else: + av = frappe.new_doc("Specification Value") + av.reference_doctype = at.applied_on + av.reference_name = doc.name + av.attribute = at.attribute_name + av.value = extra_attributes[at.attribute_name] + av.save() + continue + + for value in extra_attributes[at.attribute_name]: # list, tuple or set / not dict + existing_attribute_value = frappe.db.get_value( + "Specification Value", + { + "reference_doctype": at.applied_on, + "reference_name": doc.name, + "attribute": at.attribute_name, + }, + ) + if existing_attribute_value: + av = frappe.get_doc("Specification Value", existing_attribute_value) + else: + av = frappe.new_doc("Specification Value") + av.reference_doctype = at.applied_on + av.reference_name = doc.name + av.attribute = at.attribute_name + av.value = value + av.save() + @frappe.whitelist() def get_data_fieldnames(doctype): diff --git a/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.json b/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.json index 4122da6..a83053d 100644 --- a/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.json +++ b/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.json @@ -12,7 +12,9 @@ "required", "column_break_k0wzm", "applied_on", - "field" + "field", + "section_break_lbzwl", + "component" ], "fields": [ { @@ -56,13 +58,22 @@ "in_list_view": 1, "label": "Applied On", "options": "DocType" + }, + { + "fieldname": "section_break_lbzwl", + "fieldtype": "Section Break" + }, + { + "fieldname": "component", + "fieldtype": "Data", + "label": "Component" } ], "icon": "fa fa-edit", "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:28:38.869757", + "modified": "2023-11-16 11:11:03.969402", "modified_by": "Administrator", "module": "Inventory Tools", "name": "Specification Attribute", diff --git a/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json index c360fb3..c425cf7 100644 --- a/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json +++ b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json @@ -7,25 +7,38 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "reference_doctype", + "reference_name", "attribute", "value" ], "fields": [ { "fieldname": "attribute", - "fieldtype": "Link", - "label": "Attribute", - "options": "Specification Attribute" + "fieldtype": "Data", + "label": "Attribute" }, { "fieldname": "value", "fieldtype": "Data", "label": "Value" + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "label": "Reference Doctype", + "options": "DocType" + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "label": "Reference Name", + "options": "reference_doctype" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-11-14 16:01:49.101184", + "modified": "2023-11-16 09:53:33.634050", "modified_by": "Administrator", "module": "Inventory Tools", "name": "Specification Value", diff --git a/inventory_tools/inventory_tools/faceted_search.py b/inventory_tools/inventory_tools/faceted_search.py new file mode 100644 index 0000000..310f5bf --- /dev/null +++ b/inventory_tools/inventory_tools/faceted_search.py @@ -0,0 +1,30 @@ +import frappe +from frappe.utils.data import flt + + +@frappe.whitelist() +def show_faceted_search_components(doctype="Item"): + attributes = frappe.get_all( + "Specification Attribute", + {"applied_on": doctype}, + ["component", "attribute_name", "numeric_values"], + order_by="idx ASC", + ) + for attribute in attributes: + values = list( + set( + frappe.get_all( + "Specification Value", + {"attribute": attribute.attribute_name, "reference_doctype": doctype}, + pluck="value", + ) + ) + ) + if attribute.numeric_values and values: + _values = [flt(v) for v in values] + _min, _max = min(_values), max(_values) + attribute.values = [_min, _max] + else: + attribute.values = values + + return attributes diff --git a/inventory_tools/public/js/faceted_search/AttributeFilter.vue b/inventory_tools/public/js/faceted_search/AttributeFilter.vue index b291bef..e69363d 100644 --- a/inventory_tools/public/js/faceted_search/AttributeFilter.vue +++ b/inventory_tools/public/js/faceted_search/AttributeFilter.vue @@ -1,2 +1,26 @@ - - + + diff --git a/inventory_tools/public/js/faceted_search/FacetedSearch.vue b/inventory_tools/public/js/faceted_search/FacetedSearch.vue index a03e7a2..7e71a0b 100644 --- a/inventory_tools/public/js/faceted_search/FacetedSearch.vue +++ b/inventory_tools/public/js/faceted_search/FacetedSearch.vue @@ -1,12 +1,30 @@ - \ No newline at end of file diff --git a/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue b/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue new file mode 100644 index 0000000..17cf325 --- /dev/null +++ b/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue @@ -0,0 +1,56 @@ + + + + diff --git a/inventory_tools/public/js/faceted_search/api.js b/inventory_tools/public/js/faceted_search/api.js index e69de29..2893a6c 100644 --- a/inventory_tools/public/js/faceted_search/api.js +++ b/inventory_tools/public/js/faceted_search/api.js @@ -0,0 +1,12 @@ +function getSearchComponents(doctype) { + frappe + .xcall('inventory_tools.inventory_tools.faceted_search.show_faceted_search_components', { doctype: 'Item' }) + .then(r => { + console.log(r) + return r + }) +} + +module.exports = { + getSearchComponents, +} diff --git a/inventory_tools/public/js/faceted_search/faceted_search.js b/inventory_tools/public/js/faceted_search/faceted_search.js index f0b01e6..093725d 100644 --- a/inventory_tools/public/js/faceted_search/faceted_search.js +++ b/inventory_tools/public/js/faceted_search/faceted_search.js @@ -1,16 +1,25 @@ import FacetedSearch from './FacetedSearch.vue' +import AttributeFilter from './AttributeFilter.vue' +import FacetedSearchNumericRange from './FacetedSearchNumericRange.vue' frappe.provide('faceted_search') faceted_search.mount = () => { if (cur_list && ['Item', 'Website Item', 'BOM'].includes(cur_list.doctype)) { + // refactor to handle config object + if (faceted_search.$search) { + return + } if (faceted_search.$search == undefined && !$('#faceted-search').length) { $('.filter-section').prepend('') waitForElement('#faceted-search').then(() => { faceted_search.$search = new window.Vue({ el: '#faceted-search', render: h => h(FacetedSearch, { props: {} }), + props: { doctype: 'Item' }, }) + window.Vue.component('AttributeFilter', AttributeFilter) + window.Vue.component('FacetedSearchNumericRange', FacetedSearchNumericRange) }) } } diff --git a/inventory_tools/tests/fixtures.py b/inventory_tools/tests/fixtures.py index 96a2f2f..41ec5f4 100644 --- a/inventory_tools/tests/fixtures.py +++ b/inventory_tools/tests/fixtures.py @@ -1011,33 +1011,56 @@ specifications = [ { "dt": "Item Group", - "apply_on": "Item", + "apply_on": "Baked Goods", "enabled": 1, "attributes": [ { - "attribute_name": "Shelf Life", + "attribute_name": "Price", "applied_on": "Item", - "field": "shelf_life_in_days", "numeric_values": 1, + "component": "FacetedSearchNumericRange", }, + {"attribute_name": "Fruits", "applied_on": "Item", "component": "AttributeFilter"}, { "attribute_name": "Weight", "applied_on": "Item", "field": "weight_per_unit", "numeric_values": 1, + "component": "FacetedSearchNumericRange", + }, + { + "attribute_name": "Brand", + "applied_on": "Item", + "field": "brand", + "component": "AttributeFilter", + }, + { + "attribute_name": "Shelf Life", + "applied_on": "Item", + "field": "shelf_life_in_days", + "numeric_values": 1, + "component": "FacetedSearchNumericRange", }, - {"attribute_name": "Fruits", "applied_on": "Item"}, - {"attribute_name": "Brand", "applied_on": "Item", "field": "brand"}, ], } ] -attributes = [ - {"Ambrosia Pie": []}, - {"Double Plum Pie": []}, - { - "Gooseberry Pie": [], +attributes = { + "Ambrosia Pie": { + "Fruits": ["Hairless Rambutan", "Cloudberry", "Tayberry"], + "Weight": 120, + "Price": 11.00, }, - {"Kaduka Key Lime Pie": []}, -] + "Double Plum Pie": {"Fruits": ["Cocoplum", "Damson Plum"], "Weight": 124, "Price": 10.50}, + "Gooseberry Pie": { + "Fruits": "Gooseberry", + "Weight": 128, + "Price": 12.00, + }, + "Kaduka Key Lime Pie": { + "Fruits": ["Kaduka Lime", "Limequat"], + "Weight": 132, + "Price": 11.50, + }, +} diff --git a/inventory_tools/tests/setup.py b/inventory_tools/tests/setup.py index f05dcb1..d28245b 100644 --- a/inventory_tools/tests/setup.py +++ b/inventory_tools/tests/setup.py @@ -15,10 +15,12 @@ from frappe.utils.data import flt, getdate from inventory_tools.tests.fixtures import ( + attributes, boms, customers, items, operations, + specifications, suppliers, workstations, ) @@ -97,6 +99,7 @@ def create_test_data(): create_suppliers(settings) create_customers(settings) create_items(settings) + create_specifications(settings) create_boms(settings) prod_plan_from_doc = "Sales Order" if prod_plan_from_doc == "Sales Order": @@ -312,7 +315,10 @@ def create_items(settings): ) i.is_sales_item = 1 if item.get("item_group") == "Baked Goods" else 0 i.sales_uom = "Nos" if i.is_sales_item else None + i.shelf_life_in_days = 7 if i.is_sales_item else None i.brand = "Ambrosia Pie Co" if i.is_sales_item else None + i.weight_per_unit = 32 * 4 if i.is_sales_item else None + i.weight_uom = "Ounce" if i.is_sales_item else None i.append( "item_defaults", { @@ -768,3 +774,22 @@ def create_quotations(settings): quotation.update(values) quotation.save() quotation.submit() + + +def create_specifications(settings=None): + for spec in specifications: + if frappe.db.exists("Specification", {"title": f"{spec.get('dt')} - {spec.get('apply_on')}"}): + s = frappe.get_doc("Specification", {"title": f"{spec.get('dt')} - {spec.get('apply_on')}"}) + else: + s = frappe.new_doc("Specification") + s.dt = spec.get("dt") + s.apply_on = spec.get("apply_on") + s.enabled = spec.get("enabled") + for at in spec.get("attributes"): + s.append("attributes", at) + s.save() + + spec_items = frappe.get_all("Item", {"item_group": "Baked Goods"}) + for spec_item in spec_items: + spec_item = frappe.get_doc("Item", spec_item) + s.create_linked_values(spec_item, attributes[spec_item.name]) From 71a50429098915d5d5e5b2f6a4f1450b0e677a66 Mon Sep 17 00:00:00 2001 From: Tyler Matteson Date: Thu, 30 Nov 2023 13:50:41 -0500 Subject: [PATCH 09/64] wip: faceted search - specification editing report --- .../specification_value.json | 18 +- .../inventory_tools/faceted_search.py | 52 +++- .../report/specification/specification.js | 256 ++++-------------- .../report/specification/specification.py | 133 ++++----- .../js/faceted_search/AttributeFilter.vue | 17 +- .../js/faceted_search/FacetedSearch.vue | 40 ++- .../FacetedSearchNumericRange.vue | 5 +- .../js/faceted_search/faceted_search.js | 82 ++++-- 8 files changed, 276 insertions(+), 327 deletions(-) diff --git a/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json index c425cf7..fd828be 100644 --- a/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json +++ b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json @@ -9,6 +9,7 @@ "field_order": [ "reference_doctype", "reference_name", + "field", "attribute", "value" ], @@ -16,7 +17,8 @@ { "fieldname": "attribute", "fieldtype": "Data", - "label": "Attribute" + "label": "Attribute", + "read_only": 1 }, { "fieldname": "value", @@ -27,18 +29,26 @@ "fieldname": "reference_doctype", "fieldtype": "Link", "label": "Reference Doctype", - "options": "DocType" + "options": "DocType", + "read_only": 1 }, { "fieldname": "reference_name", "fieldtype": "Dynamic Link", "label": "Reference Name", - "options": "reference_doctype" + "options": "reference_doctype", + "read_only": 1 + }, + { + "fieldname": "field", + "fieldtype": "Data", + "label": "Field", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-11-16 09:53:33.634050", + "modified": "2023-11-30 13:08:00.287837", "modified_by": "Administrator", "module": "Inventory Tools", "name": "Specification Value", diff --git a/inventory_tools/inventory_tools/faceted_search.py b/inventory_tools/inventory_tools/faceted_search.py index 310f5bf..bb122e1 100644 --- a/inventory_tools/inventory_tools/faceted_search.py +++ b/inventory_tools/inventory_tools/faceted_search.py @@ -1,15 +1,18 @@ +import json + import frappe from frappe.utils.data import flt -@frappe.whitelist() -def show_faceted_search_components(doctype="Item"): +@frappe.whitelist(allow_guest=True) +def show_faceted_search_components(doctype="Item", filters=None): attributes = frappe.get_all( "Specification Attribute", {"applied_on": doctype}, ["component", "attribute_name", "numeric_values"], order_by="idx ASC", ) + for attribute in attributes: values = list( set( @@ -28,3 +31,48 @@ def show_faceted_search_components(doctype="Item"): attribute.values = values return attributes + + +# @frappe.whitelist() +# def update_or_create_search_attributes(doc, method=None) --> None: +# pass + + +# @frappe.whitelist(allow_guest=True) +# def get_product_list(search=None, start=0, limit=12, filters=None): +# data = get_product_data(search, start, limit) + +# for item in data: +# set_product_info_for_website(item) + +# return [get_item_for_list_in_html(r) for r in data] + + +# def get_product_data(search=None, start=0, limit=12): +# # limit = 12 because we show 12 items in the grid view +# # base query +# query = """ +# SELECT +# web_item_name, item_name, item_code, brand, route, +# website_image, thumbnail, item_group, +# description, web_long_description as website_description, +# website_warehouse, ranking +# FROM `tabWebsite Item` +# WHERE published = 1 +# """ + +# # search term condition +# if search: +# query += """ and (item_name like %(search)s +# or web_item_name like %(search)s +# or brand like %(search)s +# or web_long_description like %(search)s)""" +# search = "%" + cstr(search) + "%" + +# # order by +# query += """ ORDER BY ranking desc, modified desc limit {} offset {}""".format( +# cint(limit), +# cint(start), +# ) + +# return frappe.db.sql(query, {"search": search}, as_dict=1) # nosemgrep diff --git a/inventory_tools/inventory_tools/report/specification/specification.js b/inventory_tools/inventory_tools/report/specification/specification.js index e0aed5e..70bd9c4 100644 --- a/inventory_tools/inventory_tools/report/specification/specification.js +++ b/inventory_tools/inventory_tools/report/specification/specification.js @@ -11,215 +11,69 @@ frappe.query_reports['Specification'] = { reqd: 1, }, ], + refresh: reportview => { + this.get_datatable_options(frappe.query_report.datatable.options) + }, get_datatable_options(options) { + options.columns[3].editable = true return Object.assign(options, { - treeView: true, - checkedRowStatus: false, - checkboxColumn: true, - events: { - onCheckRow: row => { - update_selection(row) - }, - }, + getEditor: this.get_editing_object.bind(this), }) }, - onload: reportview => { - manage_buttons(reportview) - }, - refresh: reportview => { - manage_buttons(reportview) - }, -} - -function manage_buttons(reportview) { - reportview.page.add_inner_button( - 'Create PO(s)', - function () { - create('po') - }, - 'Create' - ) - - reportview.page.add_inner_button( - 'Create RFQ(s)', - function () { - create('rfq') - }, - 'Create' - ) - - reportview.page.add_inner_button( - 'Create based on Item', - function () { - create('item_based') - }, - 'Create' - ) - - // these don't seem to be working - $(".btn-default:contains('Create Card')").addClass('hidden') - $(".btn-default:contains('Set Chart')").addClass('hidden') -} - -async function create(type) { - let values = frappe.query_report.get_filter_values() - let company = undefined - let email_template = undefined - if (type != 'po') { - values = await select_company_and_email_template(values.company) - company = values['company'] - email_template = values['email_template'] - } else { - if (!values.company) { - company = await select_company() - } else { - company = values.company + get_editing_object(colIndex, rowIndex, value, parent) { + if (frappe.query_report.datatable.datamanager.data[rowIndex].field != undefined) { + frappe.show_alert(__('This value cannot be edited in this report')) + return false } - } - - let selected_rows = frappe.query_report.datatable.rowmanager.getCheckedRows() - let selected_items = frappe.query_report.datatable.datamanager.data.filter((row, index) => { - return selected_rows.includes(String(index)) ? row : false - }) - if (!selected_items.length) { - frappe.show_alert({ message: 'Please select one or more rows.', seconds: 5, indicator: 'red' }) - } else { - await frappe - .xcall('inventory_tools.inventory_tools.report.material_demand.material_demand.create', { - company: company, - email_template: email_template || '', - filters: values, - creation_type: type, - rows: selected_items, - }) - .then(r => {}) - } -} - -function update_selection(row) { - if (row !== undefined && !row[5].content) { - const toggle = frappe.query_report.datatable.rowmanager.checkMap[row[0].rowIndex] - select_all_supplier_items(row, toggle).then(() => { - update_selected_qty() - }) - } else { - update_selected_qty() - } -} - -function update_selected_qty() { - // iterate all rows for selected items - let item_map = {} - frappe.query_report.datatable.datamanager.data.forEach((supplier_row, index) => { - if (frappe.query_report.datatable.rowmanager.checkMap[index]) { - if (supplier_row.item_code && !item_map[supplier_row.item_code]) { - item_map[supplier_row.item_code] = supplier_row.qty - } else if (supplier_row.item_code && item_map[supplier_row.item_code]) { - item_map[supplier_row.item_code] += supplier_row.qty - } - } - }) - frappe.query_report.datatable.datamanager.data.forEach((supplier_row, index) => { - if (supplier_row.item_code in item_map) { - let supplier_price = Number(String(supplier_row.supplier_price).replace(/[^0-9\.-]+/g, '')) - let total_selected = item_map[supplier_row.item_code] - let selected_price = item_map[supplier_row.item_code] * (supplier_price || 0) - selected_price = format_currency(selected_price, supplier_row.currency, 2) - if (item_map[supplier_row.item_code] > supplier_row.total_demand) { - total_selected = `${total_selected}` - selected_price = `${selected_price}` - } - frappe.query_report.datatable.cellmanager.updateCell(9, index, total_selected, true) - frappe.query_report.datatable.cellmanager.updateCell(12, index, selected_price, true) - } else { - frappe.query_report.datatable.cellmanager.updateCell(9, index, '', true) - frappe.query_report.datatable.cellmanager.updateCell(12, index, '', true) - } - }) -} - -async function select_all_supplier_items(row, toggle) { - return new Promise(resolve => { - if (frappe.query_report.datatable.datamanager._filteredRows) { - frappe.query_report.datatable.datamanager._filteredRows.forEach(f => { - if (f[2].content === row[1].content) { - frappe.query_report.datatable.rowmanager.checkMap.splice(row[0].rowIndex, 0, toggle ? 1 : 0) - $(row[0].content).find('input').check = toggle - } else { - frappe.query_report.datatable.rowmanager.checkMap.splice(f[0].rowIndex, 0, 0) - } - }) - } else { - frappe.query_report.datatable.datamanager.rows.forEach(f => { - if (f[2].content === row[2].content) { - frappe.query_report.datatable.rowmanager.checkMap.splice(row[0].rowIndex, 0, toggle ? 1 : 0) - let input = $(frappe.query_report.datatable.rowmanager.getRow$(f[0].rowIndex)).find('input') - if (input[0]) { - input[0].checked = toggle - } - } else { - frappe.query_report.datatable.rowmanager.checkMap.splice(f[0].rowIndex, 0, 0) - } - }) - } - resolve() - }) -} - -async function select_company() { - return new Promise(resolve => { - let dialog = new frappe.ui.Dialog({ - title: __('Select a Company'), - fields: [ - { - fieldtype: 'Link', - fieldname: 'company', - label: 'Company', - options: 'Company', - reqd: 1, + const control = this.render_editing_input(colIndex, value, parent) + if (!control) return false + control.df.change = () => control.set_focus() + try { + return { + initValue: async value => { + return control.set_value(value) }, - ], - primary_action: () => { - let values = dialog.get_values() - dialog.hide() - return resolve(values.company) - }, - primary_action_label: __('Select'), - }) - dialog.show() - dialog.get_close_btn() - }) -} + setValue: value => { + let row = frappe.query_report.datatable.datamanager.data[rowIndex] + let docname = row.name + if (!value) { + return control.get_value() + } + if (row.indent === 0) { + return control.get_value() + } + if (row.billed_date) { + return control.get_value() + } -async function select_company_and_email_template(company) { - return new Promise(resolve => { - let dialog = new frappe.ui.Dialog({ - title: __('Select Company and Email Template'), - fields: [ - { - fieldtype: 'Link', - fieldname: 'company', - label: 'Company', - options: 'Company', - reqd: 1, - default: company, + return frappe + .xcall('inventory_tools.inventory_tools.report.specification.specification.set_value', { + docname: docname, + value: value, + }) + .catch(e => { + return control.get_value(value) + }) }, - { - fieldtype: 'Link', - fieldname: 'email_template', - label: 'Email Template', - options: 'Email Template', - reqd: 1, + getValue: async () => { + return control.get_value() }, - ], - primary_action: () => { - let values = dialog.get_values() - dialog.hide() - return resolve(values) - }, - primary_action_label: __('Select'), + } + } catch (error) { + console.log(error) + } + }, + render_editing_input(colIndex, value, parent) { + const col = frappe.query_report.datatable.getColumn(colIndex) + let control = null + control = frappe.ui.form.make_control({ + df: col, + parent: parent, + render_input: true, }) - dialog.show() - dialog.get_close_btn() - }) + control.set_value(value || '') + control.toggle_label(false) + control.toggle_description(false) + return control + }, } diff --git a/inventory_tools/inventory_tools/report/specification/specification.py b/inventory_tools/inventory_tools/report/specification/specification.py index 56b1771..83d7c69 100644 --- a/inventory_tools/inventory_tools/report/specification/specification.py +++ b/inventory_tools/inventory_tools/report/specification/specification.py @@ -1,109 +1,78 @@ # Copyright (c) 2023, AgriTheory and contributors # For license information, please see license.txt +from itertools import groupby + import frappe def execute(filters=None): - columns, data = [], [] - return get_columns(filters), data + specification = frappe.get_doc("Specification", filters.specification) + return get_columns(filters, specification), get_data(filters, specification) -def get_columns(filters): - file_export = True if frappe.form_dict.cmd == "frappe.desk.query_report.export_query" else False - doc = frappe.get_doc("Specification", filters.specification) +def get_data(filters, specification): + doctypes = [row.applied_on for row in specification.attributes] + attributes = [row.attribute_name for row in specification.attributes] + fields = {row.attribute_name: row.field for row in specification.attributes if row.field} + _data = frappe.get_all( + "Specification Value", + {"reference_doctype": ["in", doctypes], "attribute": ["in", attributes]}, + ["reference_doctype", "reference_name", "attribute", "value", "name"], + order_by="reference_name", + ) + data = [] + for ref, d in groupby(_data, key=lambda x: x.get("reference_name")): + _d = sorted(sorted(list(d), key=lambda x: x.get("value")), key=lambda x: x.get("attribute")) + data.append( + { + "reference_name": frappe.bold(ref), + "indent": 0, + } + ) + for __d in _d: + if __d.attribute in fields: + __d.field = fields[__d.attribute] + __d.indent = 1 + data.append(__d) + return data - # name w/ indent 0 - # attributes w/ indent 1 +def get_columns(filters, specification): return [ { - "label": "Supplier", - "fieldname": "supplier", + "label": f"{specification.dt} - {specification.apply_on}" + if specification.apply_on + else specification.dt, + "fieldname": "reference_name", "fieldtype": "Link", - "options": "Supplier", + "options": "Doctype", "width": "250px", }, { - "fieldname": "material_request", - "fieldtype": "Link", - "options": "Material Request", - "label": "Material Request", - "width": "200px", - }, - { - "fieldname": "schedule_date", - "label": "Required By", - "fieldtype": "Date", - "width": "120px", - }, - { - "fieldname": "material_request_item", + "label": "DocType", + "fieldname": "reference_doctype", "fieldtype": "Data", - "hidden": 1, - }, - { - "fieldname": "warehouse", - "fieldtype": "Link", - "options": "Warehouse", - "hidden": 1, - }, - { - "fieldname": "item_code", - "label": "Item", - "fieldtype": "Link", - "options": "Item", "width": "250px", }, - {"fieldname": "item_name", "fieldtype": "Data", "hidden": 1}, # unset for export - { - "label": "MR Qty", - "fieldname": "qty", - "fieldtype": "Data", - "width": "90px", - "align": "right", - }, { - "label": "Total Demand", - "fieldname": "total_demand", + "fieldname": "attribute", "fieldtype": "Data", - "width": "90px", - "align": "right", - }, - { - "label": "Draft POs", - "fieldname": "draft_po", - "fieldtype": "Data", - "width": "90px", - "align": "right", - }, - { - "label": "Total Selected", - "fieldname": "total_selected", - "fieldtype": "Data", - "width": "90px", - "align": "right", - }, - { - "label": "UOM", - "fieldname": "uom", - "fieldtype": "Link", - "options": "UOM", - "width": "90px", - }, - { - "label": "Price", - "fieldname": "supplier_price", - "fieldtype": "Data", - "width": "90px", - "align": "right", + "label": "Attribute", + "width": "200px", }, { - "label": "Selected Amount", - "fieldname": "amount", + "fieldname": "value", + "label": "Value", "fieldtype": "Data", - "width": "120px", - "align": "right", + "width": "250px", }, - {"fieldname": "currency", "fieldtype": "Link", "options": "Currency", "hidden": 1}, + {"fieldname": "field", "fieldtype": "Data", "hidden": 1}, + {"fieldname": "name", "fieldtype": "Data", "hidden": 1}, ] + + +@frappe.whitelist() +def set_value(docname, value): + frappe.set_value("Specification Value", docname, "value", value) + frappe.msgprint("Updated", alert=True) diff --git a/inventory_tools/public/js/faceted_search/AttributeFilter.vue b/inventory_tools/public/js/faceted_search/AttributeFilter.vue index e69363d..250df8e 100644 --- a/inventory_tools/public/js/faceted_search/AttributeFilter.vue +++ b/inventory_tools/public/js/faceted_search/AttributeFilter.vue @@ -2,7 +2,9 @@
{{ attribute_name }}
- + {{ attr.attribute }}
@@ -14,9 +16,16 @@ export default { data() { return { selectedValues: [] } }, - watch: { - 'selectedValues': function (oldValue, newValue) { - console.log(oldValue, newValue) + methods: { + change() { + this.$emit( + 'update_filters', + { + 'attribute_name': this.attribute_name, + 'values': this.selectedValues + .map(r => {return r.isChecked ? r.attribute : null}) + .filter(r => {return r != null}) + }) } }, mounted() { diff --git a/inventory_tools/public/js/faceted_search/FacetedSearch.vue b/inventory_tools/public/js/faceted_search/FacetedSearch.vue index 7e71a0b..f3622ee 100644 --- a/inventory_tools/public/js/faceted_search/FacetedSearch.vue +++ b/inventory_tools/public/js/faceted_search/FacetedSearch.vue @@ -1,25 +1,53 @@ diff --git a/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue b/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue index 17cf325..a4a2148 100644 --- a/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue +++ b/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue @@ -3,8 +3,8 @@
{{ attribute_name }}
- - + +
@@ -32,6 +32,7 @@ export default { white-space:nowrap; } .min-max-inputs input{ + display: inline; max-width: 10ch; text-align: right; } diff --git a/inventory_tools/public/js/faceted_search/faceted_search.js b/inventory_tools/public/js/faceted_search/faceted_search.js index 093725d..aba06c6 100644 --- a/inventory_tools/public/js/faceted_search/faceted_search.js +++ b/inventory_tools/public/js/faceted_search/faceted_search.js @@ -4,26 +4,46 @@ import FacetedSearchNumericRange from './FacetedSearchNumericRange.vue' frappe.provide('faceted_search') -faceted_search.mount = () => { - if (cur_list && ['Item', 'Website Item', 'BOM'].includes(cur_list.doctype)) { - // refactor to handle config object - if (faceted_search.$search) { - return - } - if (faceted_search.$search == undefined && !$('#faceted-search').length) { - $('.filter-section').prepend('') - waitForElement('#faceted-search').then(() => { - faceted_search.$search = new window.Vue({ - el: '#faceted-search', - render: h => h(FacetedSearch, { props: {} }), - props: { doctype: 'Item' }, - }) - window.Vue.component('AttributeFilter', AttributeFilter) - window.Vue.component('FacetedSearchNumericRange', FacetedSearchNumericRange) - }) - } - } +faceted_search.mount = el => { + // if (faceted_search.$search && faceted_search.$search._isVue) { + // return + // } + console.log(el) + faceted_search.$search = new window.Vue({ + el: el, + render: h => h(FacetedSearch, { props: { doctype: 'Item' } }), + props: { doctype: 'Item' }, + }) + window.Vue.component('AttributeFilter', AttributeFilter) + window.Vue.component('FacetedSearchNumericRange', FacetedSearchNumericRange) } +// if (faceted_search.$search == undefined) { +// await waitForElement('#product-filters').then(el => { +// console.log(el) +// faceted_search.$search = new window.Vue({ +// el: el, +// render: h => h(FacetedSearch, { props: { doctype: 'Item' }}), +// }) +// window.Vue.component('AttributeFilter', AttributeFilter) +// window.Vue.component('FacetedSearchNumericRange', FacetedSearchNumericRange) +// }) +// } +// if (faceted_search.$search == undefined && window.cur_list && ['Item', 'Website Item', 'BOM'].includes(cur_list.doctype)) { +// // refactor to handle config object +// if (faceted_search.$search == undefined && !$('#faceted-search').length) { +// $('.filter-section').prepend('') +// waitForElement('#faceted-search').then(el => { +// faceted_search.$search = new window.Vue({ +// el: el, +// render: h => h(FacetedSearch, { props: { doctype: 'Item' } }), +// props: { doctype: 'Item' }, +// }) +// window.Vue.component('AttributeFilter', AttributeFilter) +// window.Vue.component('FacetedSearchNumericRange', FacetedSearchNumericRange) +// }) +// } +// } +// } function waitForElement(selector) { return new Promise(resolve => { @@ -43,17 +63,27 @@ function waitForElement(selector) { }) } +function mount_list_view() { + if (faceted_search.$search == undefined && !$('#faceted-search').length) { + $('.filter-section').prepend('') + waitForElement('#faceted-search').then(el => { + faceted_search.mount(el) + }) + } +} + +function mount_all_products_view(el) { + faceted_search.mount(el) +} + waitForElement('[data-route]').then(element => { let observer = new MutationObserver(() => { - faceted_search.mount() + mount_list_view() }) const config = { attributes: true, childList: false, characterData: true } observer.observe(element, config) }) -if (document.querySelector('#product-filters')) { - faceted_search.$search = new window.Vue({ - el: '#product-filters', - render: h => h(FacetedSearch, { props: {} }), - }) -} +waitForElement('#product-filters').then(element => { + mount_all_products_view(element) +}) From 8209072c74a5f093276cb5b201a246e99f90c76e Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Fri, 8 Dec 2023 13:01:59 +0530 Subject: [PATCH 10/64] feat: add loader for specification and listview filters --- .pre-commit-config.yaml | 12 +- .../doctype/specification/specification.py | 5 + .../inventory_tools/faceted_search.py | 156 +++++++++--------- .../js/faceted_search/AttributeFilter.vue | 73 ++++---- .../js/faceted_search/FacetedSearch.vue | 143 +++++++++------- .../FacetedSearchNumericRange.vue | 123 +++++++------- .../js/faceted_search/faceted_search.js | 2 +- 7 files changed, 279 insertions(+), 235 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 62aaa52..440bbc8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: rev: v4.3.0 hooks: - id: trailing-whitespace - files: 'beam.*' + files: 'inventory_tools.*' exclude: '.*json$|.*txt$|.*csv|.*md|.*svg' - id: check-yaml - id: no-commit-to-branch @@ -38,12 +38,12 @@ repos: # Ignore any files that might contain jinja / bundles exclude: | (?x)^( - beam/public/dist/.*| - .*node_modules.*| .*boilerplate.*| - beam/www/website_script.js| - beam/templates/includes/.*| - beam/public/js/lib/.* + .*node_modules.*| + inventory_tools/public/dist/.*| + inventory_tools/public/js/lib/.*| + inventory_tools/templates/includes/.*| + inventory_tools/www/website_script.js )$ - repo: https://github.com/PyCQA/isort diff --git a/inventory_tools/inventory_tools/doctype/specification/specification.py b/inventory_tools/inventory_tools/doctype/specification/specification.py index 0c755ed..af3055f 100644 --- a/inventory_tools/inventory_tools/doctype/specification/specification.py +++ b/inventory_tools/inventory_tools/doctype/specification/specification.py @@ -12,7 +12,12 @@ def validate(self): if self.apply_on: self.title += f" - {self.apply_on}" + self.create_linked_values(frappe.get_doc(self.dt, self.apply_on)) + def create_linked_values(self, doc, extra_attributes=None): + if not extra_attributes: + extra_attributes = {} + for at in self.attributes: if at.field: existing_attribute_value = frappe.db.get_value( diff --git a/inventory_tools/inventory_tools/faceted_search.py b/inventory_tools/inventory_tools/faceted_search.py index bb122e1..c2ce2c8 100644 --- a/inventory_tools/inventory_tools/faceted_search.py +++ b/inventory_tools/inventory_tools/faceted_search.py @@ -1,78 +1,78 @@ -import json - -import frappe -from frappe.utils.data import flt - - -@frappe.whitelist(allow_guest=True) -def show_faceted_search_components(doctype="Item", filters=None): - attributes = frappe.get_all( - "Specification Attribute", - {"applied_on": doctype}, - ["component", "attribute_name", "numeric_values"], - order_by="idx ASC", - ) - - for attribute in attributes: - values = list( - set( - frappe.get_all( - "Specification Value", - {"attribute": attribute.attribute_name, "reference_doctype": doctype}, - pluck="value", - ) - ) - ) - if attribute.numeric_values and values: - _values = [flt(v) for v in values] - _min, _max = min(_values), max(_values) - attribute.values = [_min, _max] - else: - attribute.values = values - - return attributes - - -# @frappe.whitelist() -# def update_or_create_search_attributes(doc, method=None) --> None: -# pass - - -# @frappe.whitelist(allow_guest=True) -# def get_product_list(search=None, start=0, limit=12, filters=None): -# data = get_product_data(search, start, limit) - -# for item in data: -# set_product_info_for_website(item) - -# return [get_item_for_list_in_html(r) for r in data] - - -# def get_product_data(search=None, start=0, limit=12): -# # limit = 12 because we show 12 items in the grid view -# # base query -# query = """ -# SELECT -# web_item_name, item_name, item_code, brand, route, -# website_image, thumbnail, item_group, -# description, web_long_description as website_description, -# website_warehouse, ranking -# FROM `tabWebsite Item` -# WHERE published = 1 -# """ - -# # search term condition -# if search: -# query += """ and (item_name like %(search)s -# or web_item_name like %(search)s -# or brand like %(search)s -# or web_long_description like %(search)s)""" -# search = "%" + cstr(search) + "%" - -# # order by -# query += """ ORDER BY ranking desc, modified desc limit {} offset {}""".format( -# cint(limit), -# cint(start), -# ) - -# return frappe.db.sql(query, {"search": search}, as_dict=1) # nosemgrep +import json + +import frappe +from frappe.utils.data import flt + + +@frappe.whitelist(allow_guest=True) +def show_faceted_search_components(doctype="Item", filters=None): + attributes = frappe.get_all( + "Specification Attribute", + {"applied_on": doctype}, + ["component", "attribute_name", "numeric_values", "field"], + order_by="idx ASC", + ) + + for attribute in attributes: + values = list( + set( + frappe.get_all( + "Specification Value", + {"attribute": attribute.attribute_name, "reference_doctype": doctype}, + pluck="value", + ) + ) + ) + if attribute.numeric_values and values: + _values = [flt(v) for v in values] + _min, _max = min(_values), max(_values) + attribute.values = [_min, _max] + else: + attribute.values = values + + return attributes + + +# @frappe.whitelist() +# def update_or_create_search_attributes(doc, method=None) --> None: +# pass + + +# @frappe.whitelist(allow_guest=True) +# def get_product_list(search=None, start=0, limit=12, filters=None): +# data = get_product_data(search, start, limit) + +# for item in data: +# set_product_info_for_website(item) + +# return [get_item_for_list_in_html(r) for r in data] + + +# def get_product_data(search=None, start=0, limit=12): +# # limit = 12 because we show 12 items in the grid view +# # base query +# query = """ +# SELECT +# web_item_name, item_name, item_code, brand, route, +# website_image, thumbnail, item_group, +# description, web_long_description as website_description, +# website_warehouse, ranking +# FROM `tabWebsite Item` +# WHERE published = 1 +# """ + +# # search term condition +# if search: +# query += """ and (item_name like %(search)s +# or web_item_name like %(search)s +# or brand like %(search)s +# or web_long_description like %(search)s)""" +# search = "%" + cstr(search) + "%" + +# # order by +# query += """ ORDER BY ranking desc, modified desc limit {} offset {}""".format( +# cint(limit), +# cint(start), +# ) + +# return frappe.db.sql(query, {"search": search}, as_dict=1) # nosemgrep diff --git a/inventory_tools/public/js/faceted_search/AttributeFilter.vue b/inventory_tools/public/js/faceted_search/AttributeFilter.vue index 250df8e..9a25d65 100644 --- a/inventory_tools/public/js/faceted_search/AttributeFilter.vue +++ b/inventory_tools/public/js/faceted_search/AttributeFilter.vue @@ -1,35 +1,38 @@ - - + + + diff --git a/inventory_tools/public/js/faceted_search/FacetedSearch.vue b/inventory_tools/public/js/faceted_search/FacetedSearch.vue index f3622ee..5ed95f6 100644 --- a/inventory_tools/public/js/faceted_search/FacetedSearch.vue +++ b/inventory_tools/public/js/faceted_search/FacetedSearch.vue @@ -1,58 +1,85 @@ - - - \ No newline at end of file + + + + + diff --git a/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue b/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue index a4a2148..33e9e7d 100644 --- a/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue +++ b/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue @@ -1,57 +1,66 @@ - - - - + + + + + diff --git a/inventory_tools/public/js/faceted_search/faceted_search.js b/inventory_tools/public/js/faceted_search/faceted_search.js index aba06c6..f2767dc 100644 --- a/inventory_tools/public/js/faceted_search/faceted_search.js +++ b/inventory_tools/public/js/faceted_search/faceted_search.js @@ -77,7 +77,7 @@ function mount_all_products_view(el) { } waitForElement('[data-route]').then(element => { - let observer = new MutationObserver(() => { + const observer = new MutationObserver(() => { mount_list_view() }) const config = { attributes: true, childList: false, characterData: true } From 824973a9227b721578f8c317b1bc621e4f70ec60 Mon Sep 17 00:00:00 2001 From: Tyler Matteson Date: Mon, 11 Dec 2023 11:54:07 -0500 Subject: [PATCH 11/64] wip: all-products + "Fruits" filter working --- inventory_tools/hooks.py | 1 + .../doctype/specification/specification.py | 20 +- .../inventory_tools/faceted_search.py | 190 +++++++++++------- .../js/faceted_search/FacetedSearch.vue | 89 ++++---- .../js/faceted_search/faceted_search.js | 1 - 5 files changed, 173 insertions(+), 128 deletions(-) diff --git a/inventory_tools/hooks.py b/inventory_tools/hooks.py index 7d0de64..c9f2003 100644 --- a/inventory_tools/hooks.py +++ b/inventory_tools/hooks.py @@ -181,6 +181,7 @@ override_whitelisted_methods = { "erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry": "inventory_tools.inventory_tools.overrides.work_order.make_stock_entry", "erpnext.stock.get_item_details.get_item_details": "inventory_tools.inventory_tools.overrides.purchase_order.get_item_details", + "erpnext.e_commerce.api.get_product_filter_data": "inventory_tools.inventory_tools.faceted_search.get_product_filter_data", } diff --git a/inventory_tools/inventory_tools/doctype/specification/specification.py b/inventory_tools/inventory_tools/doctype/specification/specification.py index af3055f..8f05b68 100644 --- a/inventory_tools/inventory_tools/doctype/specification/specification.py +++ b/inventory_tools/inventory_tools/doctype/specification/specification.py @@ -35,9 +35,9 @@ def create_linked_values(self, doc, extra_attributes=None): av.reference_doctype = at.applied_on av.reference_name = doc.name av.attribute = at.attribute_name - av.value = frappe.get_value(av.reference_doctype, av.reference_name, at.field) + av.value = doc.get(at.field) av.save() - if at.attribute_name in extra_attributes: + if extra_attributes and at.attribute_name in extra_attributes: if isinstance(extra_attributes[at.attribute_name], (str, int, float)): existing_attribute_value = frappe.db.get_value( "Specification Value", @@ -58,6 +58,9 @@ def create_linked_values(self, doc, extra_attributes=None): av.save() continue + if not extra_attributes: + continue + for value in extra_attributes[at.attribute_name]: # list, tuple or set / not dict existing_attribute_value = frappe.db.get_value( "Specification Value", @@ -77,6 +80,19 @@ def create_linked_values(self, doc, extra_attributes=None): av.value = value av.save() + @property + def applied_on_doctypes(self): + return [r.applied_on for r in self.attributes] + + def applies_to(self, doc): + if doc.doctype not in self.applied_on_doctypes: + return + for field in doc.meta.fields: + if field.options == self.dt and not self.apply_on: + return True + if field.options == self.dt and doc.get(field.fieldname) == self.apply_on: + return True + @frappe.whitelist() def get_data_fieldnames(doctype): diff --git a/inventory_tools/inventory_tools/faceted_search.py b/inventory_tools/inventory_tools/faceted_search.py index c2ce2c8..5d18edc 100644 --- a/inventory_tools/inventory_tools/faceted_search.py +++ b/inventory_tools/inventory_tools/faceted_search.py @@ -1,78 +1,112 @@ -import json - -import frappe -from frappe.utils.data import flt - - -@frappe.whitelist(allow_guest=True) -def show_faceted_search_components(doctype="Item", filters=None): - attributes = frappe.get_all( - "Specification Attribute", - {"applied_on": doctype}, - ["component", "attribute_name", "numeric_values", "field"], - order_by="idx ASC", - ) - - for attribute in attributes: - values = list( - set( - frappe.get_all( - "Specification Value", - {"attribute": attribute.attribute_name, "reference_doctype": doctype}, - pluck="value", - ) - ) - ) - if attribute.numeric_values and values: - _values = [flt(v) for v in values] - _min, _max = min(_values), max(_values) - attribute.values = [_min, _max] - else: - attribute.values = values - - return attributes - - -# @frappe.whitelist() -# def update_or_create_search_attributes(doc, method=None) --> None: -# pass - - -# @frappe.whitelist(allow_guest=True) -# def get_product_list(search=None, start=0, limit=12, filters=None): -# data = get_product_data(search, start, limit) - -# for item in data: -# set_product_info_for_website(item) - -# return [get_item_for_list_in_html(r) for r in data] - - -# def get_product_data(search=None, start=0, limit=12): -# # limit = 12 because we show 12 items in the grid view -# # base query -# query = """ -# SELECT -# web_item_name, item_name, item_code, brand, route, -# website_image, thumbnail, item_group, -# description, web_long_description as website_description, -# website_warehouse, ranking -# FROM `tabWebsite Item` -# WHERE published = 1 -# """ - -# # search term condition -# if search: -# query += """ and (item_name like %(search)s -# or web_item_name like %(search)s -# or brand like %(search)s -# or web_long_description like %(search)s)""" -# search = "%" + cstr(search) + "%" - -# # order by -# query += """ ORDER BY ranking desc, modified desc limit {} offset {}""".format( -# cint(limit), -# cint(start), -# ) - -# return frappe.db.sql(query, {"search": search}, as_dict=1) # nosemgrep +import json + +import frappe +from erpnext.e_commerce.api import * +from frappe.utils.data import flt + + +@frappe.whitelist(allow_guest=True) +def show_faceted_search_components(doctype="Item", filters=None): + attributes = frappe.get_all( + "Specification Attribute", + {"applied_on": doctype}, + ["component", "attribute_name", "numeric_values"], + order_by="idx ASC", + ) + + for attribute in attributes: + values = list( + set( + frappe.get_all( + "Specification Value", + {"attribute": attribute.attribute_name, "reference_doctype": doctype}, + pluck="value", + ) + ) + ) + if attribute.numeric_values and values: + _values = [flt(v) for v in values] + _min, _max = min(_values), max(_values) + attribute.values = [_min, _max] + else: + attribute.values = values + + return attributes + + +class FacetedSearchQuery(ProductQuery): + def query_items_with_attributes(self, attributes, start=0): + print(attributes) + item_codes = [] + + attributes_in_use = {k: v for (k, v) in attributes.items() if v} + for attribute, values in attributes_in_use.items(): + if not isinstance(values, list): + values = [values] + + item_code_list = frappe.get_all( + "Specification Value", + fields=["reference_name"], + filters=[ + ["attribute", "=", attribute], + ["value", "in", values], + ], + ) + item_codes.append({x.reference_name for x in item_code_list}) + + if item_codes: + item_codes = list(set.intersection(*item_codes)) + self.filters.append(["item_code", "in", item_codes]) + + return self.query_items(start=start) + + +@frappe.whitelist(allow_guest=True) +def get_product_filter_data(query_args=None): + if isinstance(query_args, str): + query_args = json.loads(query_args) + + query_args = frappe._dict(query_args) + if query_args: + search = query_args.get("search") + field_filters = query_args.get("field_filters", {}) + attribute_filters = query_args.get("attributes", {}) + start = cint(query_args.start) if query_args.get("start") else 0 + item_group = query_args.get("item_group") + from_filters = query_args.get("from_filters") + else: + search, attribute_filters, item_group, from_filters = None, None, None, None + field_filters = {} + start = 0 + + if from_filters: + start = 0 + + sub_categories = [] + if item_group: + sub_categories = get_child_groups_for_website(item_group, immediate=True) + + engine = FacetedSearchQuery() + + try: + result = engine.query( + attribute_filters, field_filters, search_term=search, start=start, item_group=item_group + ) + except Exception: + frappe.log_error("Product query with filter failed") + return {"exc": "Something went wrong!"} + + filters = {} + discounts = result["discounts"] + + if discounts: + filter_engine = ProductFiltersBuilder() + filters["discount_filters"] = filter_engine.get_discount_filters(discounts) + + return { + "items": result["items"] or [], + "filters": filters, + "settings": engine.settings, + "sub_categories": sub_categories, + "items_count": result["items_count"], + } diff --git a/inventory_tools/public/js/faceted_search/FacetedSearch.vue b/inventory_tools/public/js/faceted_search/FacetedSearch.vue index 5ed95f6..5a6b2d5 100644 --- a/inventory_tools/public/js/faceted_search/FacetedSearch.vue +++ b/inventory_tools/public/js/faceted_search/FacetedSearch.vue @@ -5,11 +5,11 @@ :is="comp.component" :values="comp.values" :attribute_name="comp.attribute_name" - @update_filters="update_filters($event)"> - - + @update_filters="update_filters($event)" + > + + - - + + + + From eb821989f3dafc38ad445e80a00abd1d018d169d Mon Sep 17 00:00:00 2001 From: Tyler Matteson Date: Mon, 11 Dec 2023 12:04:40 -0500 Subject: [PATCH 13/64] chore: remove commented import --- inventory_tools/public/js/faceted_search/FacetedSearch.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/inventory_tools/public/js/faceted_search/FacetedSearch.vue b/inventory_tools/public/js/faceted_search/FacetedSearch.vue index d05319e..07cb36c 100644 --- a/inventory_tools/public/js/faceted_search/FacetedSearch.vue +++ b/inventory_tools/public/js/faceted_search/FacetedSearch.vue @@ -17,7 +17,6 @@ frappe.provide('erpnext') export default { name: 'FacetedSearch', props: ['doctype'], - // components: { AttributeFilter, FacetedSearchNumericRange }, data(){ return { searchComponents: [], filterValues: {} } }, From 93bca5ab92b6add95d95f8eb0b388880cb61f444 Mon Sep 17 00:00:00 2001 From: Tyler Matteson Date: Thu, 14 Dec 2023 13:36:34 -0500 Subject: [PATCH 14/64] wip: serialize dates as epoch time --- inventory_tools/hooks.py | 5 +- .../doctype/specification/specification.py | 31 ++++++-- .../specification_attribute.json | 14 ++-- .../inventory_tools/faceted_search.py | 65 ++++++++++++++--- .../js/faceted_search/AttributeFilter.vue | 11 +-- .../js/faceted_search/FacetedSearch.vue | 14 ++-- .../faceted_search/FacetedSearchDateRange.vue | 70 +++++++++++++++++++ .../FacetedSearchNumericRange.vue | 9 ++- .../js/faceted_search/faceted_search.js | 2 + 9 files changed, 185 insertions(+), 36 deletions(-) create mode 100644 inventory_tools/public/js/faceted_search/FacetedSearchDateRange.vue diff --git a/inventory_tools/hooks.py b/inventory_tools/hooks.py index c9f2003..bb0482f 100644 --- a/inventory_tools/hooks.py +++ b/inventory_tools/hooks.py @@ -137,7 +137,10 @@ ], }, "Item": { - "validate": ["inventory_tools.inventory_tools.overrides.uom.duplicate_weight_to_uom_conversion"], + "validate": [ + "inventory_tools.inventory_tools.overrides.uom.duplicate_weight_to_uom_conversion", + "inventory_tools.inventory_tools.faceted_search.update_specification_attribute_values", + ], }, "Warehouse": { "validate": ["inventory_tools.inventory_tools.overrides.warehouse.update_warehouse_path"] diff --git a/inventory_tools/inventory_tools/doctype/specification/specification.py b/inventory_tools/inventory_tools/doctype/specification/specification.py index 8f05b68..a8ca977 100644 --- a/inventory_tools/inventory_tools/doctype/specification/specification.py +++ b/inventory_tools/inventory_tools/doctype/specification/specification.py @@ -1,9 +1,15 @@ # Copyright (c) 2023, AgriTheory and contributors # For license information, please see license.txt +import datetime +import time + import frappe +from dateutil.relativedelta import relativedelta from frappe.core.doctype.doctype.doctype import no_value_fields, table_fields from frappe.model.document import Document +from frappe.utils.data import flt, get_datetime, getdate +from pytz import timezone class Specification(Document): @@ -12,12 +18,7 @@ def validate(self): if self.apply_on: self.title += f" - {self.apply_on}" - self.create_linked_values(frappe.get_doc(self.dt, self.apply_on)) - def create_linked_values(self, doc, extra_attributes=None): - if not extra_attributes: - extra_attributes = {} - for at in self.attributes: if at.field: existing_attribute_value = frappe.db.get_value( @@ -30,12 +31,14 @@ def create_linked_values(self, doc, extra_attributes=None): ) if existing_attribute_value: av = frappe.get_doc("Specification Value", existing_attribute_value) + av.value = frappe.get_value(av.reference_doctype, av.reference_name, at.field) else: av = frappe.new_doc("Specification Value") av.reference_doctype = at.applied_on av.reference_name = doc.name av.attribute = at.attribute_name - av.value = doc.get(at.field) + if at.date_values: + av.value = convert_to_epoch(av.value) av.save() if extra_attributes and at.attribute_name in extra_attributes: if isinstance(extra_attributes[at.attribute_name], (str, int, float)): @@ -55,6 +58,8 @@ def create_linked_values(self, doc, extra_attributes=None): av.reference_name = doc.name av.attribute = at.attribute_name av.value = extra_attributes[at.attribute_name] + if at.date_values: + av.value = convert_to_epoch(av.value) av.save() continue @@ -78,6 +83,8 @@ def create_linked_values(self, doc, extra_attributes=None): av.reference_name = doc.name av.attribute = at.attribute_name av.value = value + if at.date_values: + av.value = convert_to_epoch(av.value) av.save() @property @@ -94,6 +101,18 @@ def applies_to(self, doc): return True +def convert_to_epoch(date): + system_settings = frappe.get_cached_doc("System Settings", "System Settings") + d = datetime.datetime.now( + timezone(time.tzname if isinstance(time.tzname, (int, str)) else time.tzname[0]) + ) # or some other local date ) + utc_offset = d.utcoffset().total_seconds() + return ( + (get_datetime(date) - datetime.timedelta(hours=12, seconds=int(utc_offset))) + - get_datetime("1970-1-1") + ).total_seconds() + + @frappe.whitelist() def get_data_fieldnames(doctype): meta = frappe.get_meta(doctype) diff --git a/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.json b/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.json index a83053d..e05b87d 100644 --- a/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.json +++ b/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.json @@ -1,7 +1,5 @@ { "actions": [], - "allow_rename": 1, - "autoname": "field:attribute_name", "creation": "2023-11-13 16:49:36.888483", "doctype": "DocType", "document_type": "Setup", @@ -9,6 +7,7 @@ "field_order": [ "attribute_name", "numeric_values", + "date_values", "required", "column_break_k0wzm", "applied_on", @@ -41,7 +40,7 @@ "default": "0", "fieldname": "required", "fieldtype": "Check", - "in_list_view": 1, + "hidden": 1, "label": "Required" }, { @@ -67,17 +66,22 @@ "fieldname": "component", "fieldtype": "Data", "label": "Component" + }, + { + "default": "0", + "fieldname": "date_values", + "fieldtype": "Check", + "label": "Date Values" } ], "icon": "fa fa-edit", "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-16 11:11:03.969402", + "modified": "2023-12-13 14:10:35.306962", "modified_by": "Administrator", "module": "Inventory Tools", "name": "Specification Attribute", - "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/inventory_tools/inventory_tools/faceted_search.py b/inventory_tools/inventory_tools/faceted_search.py index f427073..ce73cc1 100644 --- a/inventory_tools/inventory_tools/faceted_search.py +++ b/inventory_tools/inventory_tools/faceted_search.py @@ -1,8 +1,9 @@ import json +from time import localtime import frappe from erpnext.e_commerce.api import * -from frappe.utils.data import flt +from frappe.utils.data import flt, getdate @frappe.whitelist(allow_guest=True) @@ -10,7 +11,7 @@ def show_faceted_search_components(doctype="Item", filters=None): attributes = frappe.get_all( "Specification Attribute", {"applied_on": doctype}, - ["component", "attribute_name", "numeric_values"], + ["component", "attribute_name", "numeric_values", "date_values", "name AS attribute_id"], order_by="idx ASC", ) @@ -28,6 +29,10 @@ def show_faceted_search_components(doctype="Item", filters=None): _values = [flt(v) for v in values] _min, _max = min(_values), max(_values) attribute.values = [_min, _max] + elif attribute.date_values and values: + # TODO: convert from epoch + _values = [localtime(int(v)) for v in values] + _min, _max = min(_values), max(_values) else: attribute.values = values @@ -39,21 +44,38 @@ def query_items_with_attributes(self, attributes, start=0): item_codes = [] attributes_in_use = {k: v for (k, v) in attributes.items() if v} - for attribute, values in attributes_in_use.items(): + for attribute, spec_and_values in attributes_in_use.items(): + spec = spec_and_values.get("attribute_id") + values = spec_and_values.get("values") if not isinstance(values, list): values = [values] + filters = None - item_code_list = frappe.get_all( - "Specification Value", - fields=["reference_name"], - filters=[ + date_or_numeric = frappe.get_value( + "Specification Attribute", spec, ["numeric_values", "date_values"] + ) + if date_or_numeric[0] == 1: + filters = [ ["attribute", "=", attribute], - ["value", "in", values], - ], + ["value", ">=", flt(values[0])], + ["value", "<=", flt(values[-1])], + ] + elif date_or_numeric[1] == 1: + filters = [ + ["attribute", "=", attribute], + ["value", ">=", getdate(values[0]) if values[0] else "1900-1-1"], + ["value", "<=", getdate(values[-1]) if values[-1] else "2100-12-31"], + ] + else: + filters = { + "attribute": attribute, + "value": ["in", values], + } + print(values, filters) + item_code_list = frappe.get_all( + "Specification Value", fields=["reference_name"], filters=filters, debug=True ) item_codes.append({x.reference_name for x in item_code_list}) - print(attribute, item_codes) - if item_codes: item_codes = list(set.intersection(*item_codes)) self.filters.append(["item_code", "in", item_codes]) @@ -67,6 +89,7 @@ def get_product_filter_data(query_args=None): query_args = json.loads(query_args) query_args = frappe._dict(query_args) + if query_args: search = query_args.get("search") field_filters = query_args.get("field_filters", {}) @@ -110,3 +133,23 @@ def get_product_filter_data(query_args=None): "sub_categories": sub_categories, "items_count": result["items_count"], } + + +@frappe.whitelist() +def update_specification_attribute_values(doc, method=None): + specifications = list( + set( + frappe.get_all( + "Specification Attribute", + fields=["parent"], + filters={"applied_on": doc.doctype}, + pluck="parent", + ) + ) + ) + if not specifications: + return + for spec in specifications: + spec = frappe.get_doc("Specification", spec) + if spec.applies_to(doc): + spec.create_linked_values(doc) diff --git a/inventory_tools/public/js/faceted_search/AttributeFilter.vue b/inventory_tools/public/js/faceted_search/AttributeFilter.vue index 9a25d65..da155db 100644 --- a/inventory_tools/public/js/faceted_search/AttributeFilter.vue +++ b/inventory_tools/public/js/faceted_search/AttributeFilter.vue @@ -11,7 +11,7 @@ diff --git a/inventory_tools/public/js/faceted_search/FacetedSearch.vue b/inventory_tools/public/js/faceted_search/FacetedSearch.vue index 07cb36c..d2088a3 100644 --- a/inventory_tools/public/js/faceted_search/FacetedSearch.vue +++ b/inventory_tools/public/js/faceted_search/FacetedSearch.vue @@ -4,6 +4,7 @@ @@ -22,16 +23,15 @@ export default { }, mounted(){ this.loadAttributeFilters() - this.search = erpnext.ProductSearch + // this.search = erpnext.ProductSearch }, methods: { update_filters(values){ console.log('update_filters', values) - this.filterValues[values['attribute_name']] = values['values'] - console.log(this.filterValues) + this.filterValues[values.attribute_name] = { attribute_id: values.attribute_id, values: values.values} // need to debounce here instead of timeout setTimeout(() => { - this.setFilterValues() + this.setFilterValues() }, 300) }, loadAttributeFilters(){ @@ -53,7 +53,9 @@ export default { query_args: { attributes: this.filterValues } }).then(r => { let view_type = localStorage.getItem("product_view") || "List View"; - if(view_type == 'List View'){ + if(!r.items){ + return + } else if(view_type == 'List View'){ new erpnext.ProductList({ items: r.items, products_section: $("#products-list-area"), @@ -70,7 +72,7 @@ export default { } }) } else { - // this is where the filters should be set in the list view + // this is where the filters should be set in the list view } } } diff --git a/inventory_tools/public/js/faceted_search/FacetedSearchDateRange.vue b/inventory_tools/public/js/faceted_search/FacetedSearchDateRange.vue new file mode 100644 index 0000000..41f8150 --- /dev/null +++ b/inventory_tools/public/js/faceted_search/FacetedSearchDateRange.vue @@ -0,0 +1,70 @@ + + + + diff --git a/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue b/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue index 4b260a4..a70129b 100644 --- a/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue +++ b/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue @@ -16,7 +16,7 @@ diff --git a/inventory_tools/public/js/faceted_search/faceted_search.js b/inventory_tools/public/js/faceted_search/faceted_search.js index 1c0b2cd..6748c01 100644 --- a/inventory_tools/public/js/faceted_search/faceted_search.js +++ b/inventory_tools/public/js/faceted_search/faceted_search.js @@ -1,6 +1,7 @@ import FacetedSearch from './FacetedSearch.vue' import AttributeFilter from './AttributeFilter.vue' import FacetedSearchNumericRange from './FacetedSearchNumericRange.vue' +import FacetedSearchDateRange from './FacetedSearchDateRange.vue' frappe.provide('faceted_search') @@ -15,6 +16,7 @@ faceted_search.mount = el => { }) window.Vue.component('AttributeFilter', AttributeFilter) window.Vue.component('FacetedSearchNumericRange', FacetedSearchNumericRange) + window.Vue.component('FacetedSearchDateRange', FacetedSearchDateRange) } // if (faceted_search.$search == undefined) { // await waitForElement('#product-filters').then(el => { From d619397c904784a6f39720e886a4690972f30b1f Mon Sep 17 00:00:00 2001 From: Tyler Matteson Date: Thu, 4 Jan 2024 13:32:31 -0500 Subject: [PATCH 15/64] wip: sort order --- inventory_tools/hooks.py | 13 +- .../doctype/specification/specification.py | 62 +++++++ .../specification_value.json | 9 +- .../inventory_tools/faceted_search.py | 152 ++++++++++++++++-- .../public/js/custom/item_custom.js | 117 ++++++++++++++ .../public/js/{ => custom}/job_card_custom.js | 0 .../js/{ => custom}/operation_custom.js | 0 .../js/{ => custom}/stock_entry_custom.js | 0 .../js/faceted_search/FacetedSearch.vue | 16 +- .../faceted_search/FacetedSearchDateRange.vue | 4 +- .../js/faceted_search/faceted_search.js | 50 +++--- inventory_tools/tests/fixtures.py | 7 + 12 files changed, 369 insertions(+), 61 deletions(-) create mode 100644 inventory_tools/public/js/custom/item_custom.js rename inventory_tools/public/js/{ => custom}/job_card_custom.js (100%) rename inventory_tools/public/js/{ => custom}/operation_custom.js (100%) rename inventory_tools/public/js/{ => custom}/stock_entry_custom.js (100%) diff --git a/inventory_tools/hooks.py b/inventory_tools/hooks.py index bb0482f..bcfb9f3 100644 --- a/inventory_tools/hooks.py +++ b/inventory_tools/hooks.py @@ -31,12 +31,13 @@ # include js in doctype views doctype_js = { - "Job Card": "public/js/job_card_custom.js", - "Purchase Invoice": "public/js/purchase_invoice_custom.js", - "Purchase Order": "public/js/purchase_order_custom.js", - "Operation": "public/js/operation_custom.js", - "Stock Entry": "public/js/stock_entry_custom.js", - "Work Order": "public/js/work_order_custom.js", + "Item": "public/js/custom/item_custom.js", + "Job Card": "public/js/custom/job_card_custom.js", + "Purchase Invoice": "public/js/custom/purchase_invoice_custom.js", + "Purchase Order": "public/js/custom/purchase_order_custom.js", + "Operation": "public/js/custom/operation_custom.js", + "Stock Entry": "public/js/custom/stock_entry_custom.js", + "Work Order": "public/js/custom/work_order_custom.js", } # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} diff --git a/inventory_tools/inventory_tools/doctype/specification/specification.py b/inventory_tools/inventory_tools/doctype/specification/specification.py index a8ca977..185bf1e 100644 --- a/inventory_tools/inventory_tools/doctype/specification/specification.py +++ b/inventory_tools/inventory_tools/doctype/specification/specification.py @@ -2,6 +2,7 @@ # For license information, please see license.txt import datetime +import json import time import frappe @@ -27,6 +28,7 @@ def create_linked_values(self, doc, extra_attributes=None): "reference_doctype": at.applied_on, "reference_name": doc.name, "attribute": at.attribute_name, + "specification": self.name, }, ) if existing_attribute_value: @@ -36,7 +38,9 @@ def create_linked_values(self, doc, extra_attributes=None): av = frappe.new_doc("Specification Value") av.reference_doctype = at.applied_on av.reference_name = doc.name + av.specification = self.name av.attribute = at.attribute_name + av.field = at.field if at.date_values: av.value = convert_to_epoch(av.value) av.save() @@ -113,9 +117,67 @@ def convert_to_epoch(date): ).total_seconds() +def convert_from_epoch(date): + system_settings = frappe.get_cached_doc("System Settings", "System Settings") + d = datetime.datetime.utcfromtimestamp(int(date)) + utc_offset = d.utcoffset().total_seconds() if d.utcoffset() else 0 + return (d + datetime.timedelta(hours=12, seconds=int(utc_offset))).date() + + @frappe.whitelist() def get_data_fieldnames(doctype): meta = frappe.get_meta(doctype) return sorted( f.fieldname for f in meta.fields if f.fieldtype not in no_value_fields + table_fields ) + + +@frappe.whitelist() +def get_specification_values(reference_doctype, reference_name): + r = frappe.get_all( + "Specification Value", + filters={"reference_doctype": reference_doctype, "reference_name": reference_name}, + fields=["name AS row_name", "attribute", "value", "field", "specification"], + order_by="attribute ASC, value ASC", + ) + for row in r: + date_values = frappe.get_value( + "Specification Attribute", + {"parent": row.specification, "attribute_name": row.attribute}, + ["date_values"], + ) + if date_values: + row.value = convert_from_epoch(row.value) + return r + + +@frappe.whitelist() +def update_specification_values(reference_doctype, reference_name, spec, specifications): + if isinstance(specifications, str): + specifications = json.loads(specifications) + specifications = [frappe._dict(**s) for s in specifications] + # convert dates to epoch + existing_values = get_specification_values(reference_doctype, reference_name) + for s in specifications: + for row in existing_values: + if row.row_name and row.row_name == s.row_name and row.value != s.value: + date_values = frappe.get_value( + "Specification Attribute", {"parent": spec, "attribute_name": s.attribute}, ["date_values"] + ) + if date_values: + s.value = convert_to_epoch(s.value) + frappe.set_value("Specification Value", s.row_name, "value", s.value) + if not s.row_name: + av = frappe.new_doc("Specification Value") + av.reference_doctype = reference_doctype + av.reference_name = reference_name + av.attribute = s.attribute + av.specification = spec + av.value = s.value + # TODO: + date_values = frappe.get_value( + "Specification Attribute", {"parent": spec, "attribute_name": s.attribute}, ["date_values"] + ) + if date_values: + av.value = convert_to_epoch(av.value) + av.save() diff --git a/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json index fd828be..91d2862 100644 --- a/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json +++ b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json @@ -9,6 +9,7 @@ "field_order": [ "reference_doctype", "reference_name", + "specification", "field", "attribute", "value" @@ -44,11 +45,17 @@ "fieldtype": "Data", "label": "Field", "read_only": 1 + }, + { + "fieldname": "specification", + "fieldtype": "Link", + "label": "Specification", + "options": "Specification" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-11-30 13:08:00.287837", + "modified": "2023-12-21 13:48:04.067288", "modified_by": "Administrator", "module": "Inventory Tools", "name": "Specification Value", diff --git a/inventory_tools/inventory_tools/faceted_search.py b/inventory_tools/inventory_tools/faceted_search.py index ce73cc1..ed9a636 100644 --- a/inventory_tools/inventory_tools/faceted_search.py +++ b/inventory_tools/inventory_tools/faceted_search.py @@ -5,6 +5,8 @@ from erpnext.e_commerce.api import * from frappe.utils.data import flt, getdate +from inventory_tools.inventory_tools.doctype.specification.specification import convert_to_epoch + @frappe.whitelist(allow_guest=True) def show_faceted_search_components(doctype="Item", filters=None): @@ -30,7 +32,6 @@ def show_faceted_search_components(doctype="Item", filters=None): _min, _max = min(_values), max(_values) attribute.values = [_min, _max] elif attribute.date_values and values: - # TODO: convert from epoch _values = [localtime(int(v)) for v in values] _min, _max = min(_values), max(_values) else: @@ -39,14 +40,102 @@ def show_faceted_search_components(doctype="Item", filters=None): return attributes +sort_order_lookup = { + "Title A-Z": "item_name ASC, ranking DESC", + "Title Z-A": "item_name DESC, ranking DESC", + "Item Code A-Z": "item_code ASC, ranking DESC", + "Item Code Z-A": "item_code DESC, ranking DESC", +} + + class FacetedSearchQuery(ProductQuery): - def query_items_with_attributes(self, attributes, start=0): + def query( + self, attributes=None, fields=None, search_term=None, start=0, item_group=None, sort_order="" + ): + print(attributes, fields, search_term, start, item_group, sort_order) + # track if discounts included in field filters + self.filter_with_discount = bool(fields.get("discount")) + result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0 + + if fields: + self.build_fields_filters(fields) + if item_group: + self.build_item_group_filters(item_group) + if search_term: + self.build_search_filters(search_term) + if self.settings.hide_variants: + self.filters.append(["variant_of", "is", "not set"]) + + print("query", sort_order) + sort_order = sort_order_lookup.get(sort_order) if sort_order else "item_name ASC" + + # query results + if attributes: + result, count = self.query_items_with_attributes(attributes, start, sort_order=sort_order) + else: + result, count = self.query_items(start=start, sort_order=sort_order) + + # sort combined results by ranking + result = sorted(result, key=lambda x: x.get("ranking"), reverse=True) + + if self.settings.enabled: + cart_items = self.get_cart_items() + + result, discount_list = self.add_display_details(result, discount_list, cart_items) + + discounts = [] + if discount_list: + discounts = [min(discount_list), max(discount_list)] + + result = self.filter_results_by_discount(fields, result) + + return {"items": result, "items_count": count, "discounts": discounts} + + def query_items(self, start=0, sort_order=""): + print("query_items", sort_order) + """Build a query to fetch Website Items based on field filters.""" + # MySQL does not support offset without limit, + # frappe does not accept two parameters for limit + # https://dev.mysql.com/doc/refman/8.0/en/select.html#id4651989 + count_items = frappe.db.get_all( + "Website Item", + filters=self.filters, + or_filters=self.or_filters, + limit_page_length=184467440737095516, + limit_start=start, # get all items from this offset for total count ahead + order_by=sort_order, + # debug=True + ) + count = len(count_items) + + # If discounts included, return all rows. + # Slice after filtering rows with discount (See `filter_results_by_discount`). + # Slicing before hand will miss discounted items on the 3rd or 4th page. + # Discounts are fetched on computing Pricing Rules so we cannot query them directly. + page_length = 184467440737095516 if self.filter_with_discount else self.page_length + + items = frappe.db.get_all( + "Website Item", + fields=self.fields, + filters=self.filters, + or_filters=self.or_filters, + limit_page_length=page_length, + limit_start=start, + order_by=sort_order, + # debug=True + ) + + return items, count + + def query_items_with_attributes(self, attributes, start=0, sort_order=""): item_codes = [] attributes_in_use = {k: v for (k, v) in attributes.items() if v} for attribute, spec_and_values in attributes_in_use.items(): spec = spec_and_values.get("attribute_id") values = spec_and_values.get("values") + if not values: + continue if not isinstance(values, list): values = [values] filters = None @@ -55,32 +144,54 @@ def query_items_with_attributes(self, attributes, start=0): "Specification Attribute", spec, ["numeric_values", "date_values"] ) if date_or_numeric[0] == 1: + + if values[0] > values[-1]: + values[0], values[-1] = values[-1], values[0] filters = [ ["attribute", "=", attribute], - ["value", ">=", flt(values[0])], - ["value", "<=", flt(values[-1])], ] + if values[0]: + filters.append( + ["value", ">=", flt(values[0])], + ) + if values[-1]: + filters.append( + ["value", "<=", flt(values[-1])], + ) + elif date_or_numeric[1] == 1: filters = [ ["attribute", "=", attribute], - ["value", ">=", getdate(values[0]) if values[0] else "1900-1-1"], - ["value", "<=", getdate(values[-1]) if values[-1] else "2100-12-31"], + [ + "value", + ">=", + convert_to_epoch(getdate(values[0])) if values[0] else convert_to_epoch(getdate("1900-1-1")), + ], + [ + "value", + "<=", + convert_to_epoch(getdate(values[-1])) + if values[-1] + else convert_to_epoch(getdate("2100-12-31")), + ], ] else: filters = { "attribute": attribute, "value": ["in", values], } - print(values, filters) item_code_list = frappe.get_all( - "Specification Value", fields=["reference_name"], filters=filters, debug=True + "Specification Value", + fields=["reference_name"], + filters=filters, # debug=True ) item_codes.append({x.reference_name for x in item_code_list}) + if item_codes: item_codes = list(set.intersection(*item_codes)) self.filters.append(["item_code", "in", item_codes]) - return self.query_items(start=start) + return self.query_items(start=start, sort_order=sort_order) @frappe.whitelist(allow_guest=True) @@ -94,6 +205,9 @@ def get_product_filter_data(query_args=None): search = query_args.get("search") field_filters = query_args.get("field_filters", {}) attribute_filters = query_args.get("attributes", {}) + sort_order = ( + query_args.get("sort_order").get("sort_order") if query_args.get("sort_order") else "" + ) start = cint(query_args.start) if query_args.get("start") else 0 item_group = query_args.get("item_group") from_filters = query_args.get("from_filters") @@ -101,6 +215,7 @@ def get_product_filter_data(query_args=None): search, attribute_filters, item_group, from_filters = None, None, None, None field_filters = {} start = 0 + sort_order = "" if from_filters: start = 0 @@ -111,13 +226,18 @@ def get_product_filter_data(query_args=None): engine = FacetedSearchQuery() - try: - result = engine.query( - attribute_filters, field_filters, search_term=search, start=start, item_group=item_group - ) - except Exception: - frappe.log_error("Product query with filter failed") - return {"exc": "Something went wrong!"} + # try: + result = engine.query( + attribute_filters, + field_filters, + search_term=search, + start=start, + item_group=item_group, + sort_order=sort_order, + ) + # except Exception: + # frappe.log_error("Product query with filter failed") + # return {"exc": "Something went wrong!"} filters = {} discounts = result["discounts"] diff --git a/inventory_tools/public/js/custom/item_custom.js b/inventory_tools/public/js/custom/item_custom.js new file mode 100644 index 0000000..1e10f86 --- /dev/null +++ b/inventory_tools/public/js/custom/item_custom.js @@ -0,0 +1,117 @@ +frappe.ui.form.on('Item', { + refresh: frm => { + add_specification_dialog(frm) + }, +}) + +function add_specification_dialog(frm) { + // save before continuing + frm.add_custom_button( + 'Edit Specification', + () => { + specification_dialog(frm) + }, + 'Actions' + ) +} + +async function specification_dialog(frm) { + const data = await get_specifications(frm) + let current_spec = data[0].specification + let attributes = data.map(r => r.attribute) + let fields = [ + { + fieldtype: 'Data', + fieldname: 'row_name', + in_list_view: 0, + read_only: 1, + disabled: 0, + hidden: 1, + }, + { + fieldtype: 'Select', + fieldname: 'attribute', + in_list_view: 1, + read_only: 0, + disabled: 0, + label: __('Attribute'), + options: attributes, + }, + { + fieldtype: 'Data', + fieldname: 'field', + label: __('Field'), + in_list_view: 1, + read_only: 1, + }, + { + fieldtype: 'Data', + fieldname: 'value', + label: __('Value'), + in_list_view: 1, + read_only: 0, + }, + ] + return new Promise(resolve => { + let d = new frappe.ui.Dialog({ + title: __('Edit Specifications'), + fields: [ + { + label: __('Specification'), + fieldname: 'specification', + fieldtype: 'Link', + options: 'Specification', + default: current_spec, + }, + { + fieldtype: 'Column Break', + fieldname: 'col_break_1', + }, + { + fieldtype: 'Section Break', + fieldname: 'section_break_1', + }, + { + fieldname: 'specs', + fieldtype: 'Table', + in_place_edit: true, + editable_grid: true, + reqd: 1, + data: data, + get_data: () => get_specifications(frm), + fields: fields, + }, + ], + primary_action: () => { + let values = d.get_values() + if (!values) { + return + } + frappe.xcall( + 'inventory_tools.inventory_tools.doctype.specification.specification.update_specification_values', + { + reference_doctype: frm.doc.doctype, + reference_name: frm.doc.name, + spec: values.specification, + specifications: values.specs, + } + ) + resolve(d.hide()) + }, + primary_action_label: __('Save'), + size: 'large', + }) + d.show() + }) +} + +async function get_specifications(frm) { + let r = await frappe.xcall( + 'inventory_tools.inventory_tools.doctype.specification.specification.get_specification_values', + { + reference_doctype: frm.doc.doctype, + reference_name: frm.doc.name, + } + ) + return r +} diff --git a/inventory_tools/public/js/job_card_custom.js b/inventory_tools/public/js/custom/job_card_custom.js similarity index 100% rename from inventory_tools/public/js/job_card_custom.js rename to inventory_tools/public/js/custom/job_card_custom.js diff --git a/inventory_tools/public/js/operation_custom.js b/inventory_tools/public/js/custom/operation_custom.js similarity index 100% rename from inventory_tools/public/js/operation_custom.js rename to inventory_tools/public/js/custom/operation_custom.js diff --git a/inventory_tools/public/js/stock_entry_custom.js b/inventory_tools/public/js/custom/stock_entry_custom.js similarity index 100% rename from inventory_tools/public/js/stock_entry_custom.js rename to inventory_tools/public/js/custom/stock_entry_custom.js diff --git a/inventory_tools/public/js/faceted_search/FacetedSearch.vue b/inventory_tools/public/js/faceted_search/FacetedSearch.vue index d2088a3..63c8c6d 100644 --- a/inventory_tools/public/js/faceted_search/FacetedSearch.vue +++ b/inventory_tools/public/js/faceted_search/FacetedSearch.vue @@ -6,7 +6,7 @@ :values="comp.values" :attribute_id="comp.attribute_id" :attribute_name="comp.attribute_name" - @update_filters="update_filters($event)" + @update_filters="updateFilters($event)" > @@ -19,16 +19,18 @@ export default { name: 'FacetedSearch', props: ['doctype'], data(){ - return { searchComponents: [], filterValues: {} } + return { searchComponents: [], filterValues: {}, sortOrder: '' } }, mounted(){ this.loadAttributeFilters() - // this.search = erpnext.ProductSearch }, methods: { - update_filters(values){ - console.log('update_filters', values) - this.filterValues[values.attribute_name] = { attribute_id: values.attribute_id, values: values.values} + updateFilters(values){ + if('sort_order' in values){ + this.sortOrder = values + } else { + this.filterValues[values.attribute_name] = { attribute_id: values.attribute_id, values: values.values} + } // need to debounce here instead of timeout setTimeout(() => { this.setFilterValues() @@ -50,7 +52,7 @@ export default { setFilterValues(){ if(true){ // TODO: change this to test for portal view or != list view frappe.xcall('erpnext.e_commerce.api.get_product_filter_data', { - query_args: { attributes: this.filterValues } + query_args: { attributes: this.filterValues, sort_order: this.sortOrder } }).then(r => { let view_type = localStorage.getItem("product_view") || "List View"; if(!r.items){ diff --git a/inventory_tools/public/js/faceted_search/FacetedSearchDateRange.vue b/inventory_tools/public/js/faceted_search/FacetedSearchDateRange.vue index 41f8150..ea46b64 100644 --- a/inventory_tools/public/js/faceted_search/FacetedSearchDateRange.vue +++ b/inventory_tools/public/js/faceted_search/FacetedSearchDateRange.vue @@ -32,8 +32,8 @@ export default { }, mounted() { if(this.values){ - this.minFilterValue = this.values[0] - this.maxFilterValue = this.values[1] + this.minFilterValue = moment(this.values[0] * 1000).format('YYYY-MM-DD') + this.maxFilterValue = moment(this.values[1] * 1000).format('YYYY-MM-DD') } } } diff --git a/inventory_tools/public/js/faceted_search/faceted_search.js b/inventory_tools/public/js/faceted_search/faceted_search.js index 6748c01..20390da 100644 --- a/inventory_tools/public/js/faceted_search/faceted_search.js +++ b/inventory_tools/public/js/faceted_search/faceted_search.js @@ -17,34 +17,10 @@ faceted_search.mount = el => { window.Vue.component('AttributeFilter', AttributeFilter) window.Vue.component('FacetedSearchNumericRange', FacetedSearchNumericRange) window.Vue.component('FacetedSearchDateRange', FacetedSearchDateRange) + faceted_search.$search.updateSortOrder = sortOrder => { + console.log('updated sort order') + } } -// if (faceted_search.$search == undefined) { -// await waitForElement('#product-filters').then(el => { -// console.log(el) -// faceted_search.$search = new window.Vue({ -// el: el, -// render: h => h(FacetedSearch, { props: { doctype: 'Item' }}), -// }) -// window.Vue.component('AttributeFilter', AttributeFilter) -// window.Vue.component('FacetedSearchNumericRange', FacetedSearchNumericRange) -// }) -// } -// if (faceted_search.$search == undefined && window.cur_list && ['Item', 'Website Item', 'BOM'].includes(cur_list.doctype)) { -// // refactor to handle config object -// if (faceted_search.$search == undefined && !$('#faceted-search').length) { -// $('.filter-section').prepend('') -// waitForElement('#faceted-search').then(el => { -// faceted_search.$search = new window.Vue({ -// el: el, -// render: h => h(FacetedSearch, { props: { doctype: 'Item' } }), -// props: { doctype: 'Item' }, -// }) -// window.Vue.component('AttributeFilter', AttributeFilter) -// window.Vue.component('FacetedSearchNumericRange', FacetedSearchNumericRange) -// }) -// } -// } -// } function waitForElement(selector) { return new Promise(resolve => { @@ -73,7 +49,7 @@ function mount_list_view() { } } -function mount_all_products_view(el) { +function mount_ecommerce_view(el) { faceted_search.mount(el) } @@ -86,5 +62,21 @@ waitForElement('[data-route]').then(element => { }) waitForElement('#product-filters').then(element => { - mount_all_products_view(element) + mount_ecommerce_view(element) + waitForElement('.toggle-container').then(element => { + let el = $(element) + el.prepend( + `` + ) + el.on('change', e => { + faceted_search.$search.$children[0].updateFilters({ sort_order: e.target.value }) + }) + }) }) diff --git a/inventory_tools/tests/fixtures.py b/inventory_tools/tests/fixtures.py index 41ec5f4..a63ea77 100644 --- a/inventory_tools/tests/fixtures.py +++ b/inventory_tools/tests/fixtures.py @@ -1041,6 +1041,13 @@ "numeric_values": 1, "component": "FacetedSearchNumericRange", }, + { + "attribute_name": "EOL", + "applied_on": "Item", + "field": "end_of_life", + "date_values": 1, + "component": "FacetedSearchDateRange", + }, ], } ] From f52d7a739aa86af73497b4227fd21e559bc37dcd Mon Sep 17 00:00:00 2001 From: Tyler Matteson Date: Tue, 30 Jan 2024 16:48:43 -0500 Subject: [PATCH 16/64] feat: facted search in listview also --- .../inventory_tools/faceted_search.py | 133 ++++++++++-------- .../js/faceted_search/FacetedSearch.vue | 66 ++++++++- 2 files changed, 134 insertions(+), 65 deletions(-) diff --git a/inventory_tools/inventory_tools/faceted_search.py b/inventory_tools/inventory_tools/faceted_search.py index ed9a636..da605cb 100644 --- a/inventory_tools/inventory_tools/faceted_search.py +++ b/inventory_tools/inventory_tools/faceted_search.py @@ -32,7 +32,7 @@ def show_faceted_search_components(doctype="Item", filters=None): _min, _max = min(_values), max(_values) attribute.values = [_min, _max] elif attribute.date_values and values: - _values = [localtime(int(v)) for v in values] + _values = [localtime(int(flt(v))) for v in values] _min, _max = min(_values), max(_values) else: attribute.values = values @@ -52,7 +52,6 @@ class FacetedSearchQuery(ProductQuery): def query( self, attributes=None, fields=None, search_term=None, start=0, item_group=None, sort_order="" ): - print(attributes, fields, search_term, start, item_group, sort_order) # track if discounts included in field filters self.filter_with_discount = bool(fields.get("discount")) result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0 @@ -66,7 +65,6 @@ def query( if self.settings.hide_variants: self.filters.append(["variant_of", "is", "not set"]) - print("query", sort_order) sort_order = sort_order_lookup.get(sort_order) if sort_order else "item_name ASC" # query results @@ -92,7 +90,6 @@ def query( return {"items": result, "items_count": count, "discounts": discounts} def query_items(self, start=0, sort_order=""): - print("query_items", sort_order) """Build a query to fetch Website Items based on field filters.""" # MySQL does not support offset without limit, # frappe does not accept two parameters for limit @@ -128,64 +125,7 @@ def query_items(self, start=0, sort_order=""): return items, count def query_items_with_attributes(self, attributes, start=0, sort_order=""): - item_codes = [] - - attributes_in_use = {k: v for (k, v) in attributes.items() if v} - for attribute, spec_and_values in attributes_in_use.items(): - spec = spec_and_values.get("attribute_id") - values = spec_and_values.get("values") - if not values: - continue - if not isinstance(values, list): - values = [values] - filters = None - - date_or_numeric = frappe.get_value( - "Specification Attribute", spec, ["numeric_values", "date_values"] - ) - if date_or_numeric[0] == 1: - - if values[0] > values[-1]: - values[0], values[-1] = values[-1], values[0] - filters = [ - ["attribute", "=", attribute], - ] - if values[0]: - filters.append( - ["value", ">=", flt(values[0])], - ) - if values[-1]: - filters.append( - ["value", "<=", flt(values[-1])], - ) - - elif date_or_numeric[1] == 1: - filters = [ - ["attribute", "=", attribute], - [ - "value", - ">=", - convert_to_epoch(getdate(values[0])) if values[0] else convert_to_epoch(getdate("1900-1-1")), - ], - [ - "value", - "<=", - convert_to_epoch(getdate(values[-1])) - if values[-1] - else convert_to_epoch(getdate("2100-12-31")), - ], - ] - else: - filters = { - "attribute": attribute, - "value": ["in", values], - } - item_code_list = frappe.get_all( - "Specification Value", - fields=["reference_name"], - filters=filters, # debug=True - ) - item_codes.append({x.reference_name for x in item_code_list}) + item_codes = get_specification_items(attributes) if item_codes: item_codes = list(set.intersection(*item_codes)) @@ -273,3 +213,72 @@ def update_specification_attribute_values(doc, method=None): spec = frappe.get_doc("Specification", spec) if spec.applies_to(doc): spec.create_linked_values(doc) + + +@frappe.whitelist() +def get_specification_items(attributes, start=0, sort_order=""): + attributes = json.loads(attributes) if isinstance(attributes, str) else attributes + item_codes = [] + + attributes_in_use = {k: v for (k, v) in attributes.items() if v} + for attribute, spec_and_values in attributes_in_use.items(): + spec = spec_and_values.get("attribute_id") + values = spec_and_values.get("values") + if not values: + continue + if not isinstance(values, list): + values = [values] + filters = None + + date_or_numeric = frappe.get_value( + "Specification Attribute", spec, ["numeric_values", "date_values"] + ) + if date_or_numeric[0] == 1: + values[0], values[-1] = ( + flt(values[0]) if values[0] else None, + flt(values[-1]) if values[-1] else None, + ) + if values[0] and values[-1] and values[0] > values[-1]: + values[0], values[-1] = values[-1], values[0] + filters = [ + ["attribute", "=", attribute], + ] + if values[0]: + filters.append( + ["value", ">=", flt(values[0])], + ) + if values[-1]: + filters.append( + ["value", "<=", flt(values[-1])], + ) + + elif date_or_numeric[1] == 1: + filters = [ + ["attribute", "=", attribute], + [ + "value", + ">=", + convert_to_epoch(getdate(values[0])) if values[0] else convert_to_epoch(getdate("1900-1-1")), + ], + [ + "value", + "<=", + convert_to_epoch(getdate(values[-1])) + if values[-1] + else convert_to_epoch(getdate("2100-12-31")), + ], + ] + else: + filters = { + "attribute": attribute, + "value": ["in", values], + } + item_code_list = frappe.get_all( + "Specification Value", + fields=["reference_name"], + filters=filters, # debug=True + ) + item_codes.append({x.reference_name for x in item_code_list}) + + if item_codes: + return list(item_codes) diff --git a/inventory_tools/public/js/faceted_search/FacetedSearch.vue b/inventory_tools/public/js/faceted_search/FacetedSearch.vue index 63c8c6d..c9a7bec 100644 --- a/inventory_tools/public/js/faceted_search/FacetedSearch.vue +++ b/inventory_tools/public/js/faceted_search/FacetedSearch.vue @@ -50,7 +50,7 @@ export default { }) }, setFilterValues(){ - if(true){ // TODO: change this to test for portal view or != list view + if(erpnext.e_commerce){ frappe.xcall('erpnext.e_commerce.api.get_product_filter_data', { query_args: { attributes: this.filterValues, sort_order: this.sortOrder } }).then(r => { @@ -74,9 +74,69 @@ export default { } }) } else { - // this is where the filters should be set in the list view + const listview = frappe.get_list_view(this.doctype) + let filters = listview.filter_area.get() + + for (const [key, value] of Object.entries(this.filterValues)) { + const values = value.values + const attribute = this.searchComponents.find(comp => comp.attribute_name === key) + + if (attribute.field) { + if (Array.isArray(values)) { + if (values.length > 0) { + if (!values[0] && !values[1]) { + // TODO: handle case where numeric range is unset + } else { + filters.push([this.doctype, attribute.field, 'in', values]) + } + } else { + filters = filters.filter(filter => filter[1] !== attribute.field) + } + + this.refreshFilters(filters) + } else { + // TODO: handle edge-case? + } + } else { + if (Array.isArray(values)) { + if (!values[0] && !values[1]) { + // TODO: handle case where numeric range is unset + } else { + frappe + .xcall('inventory_tools.inventory_tools.faceted_search.get_specification_items', { + doctype: this.doctype, + attributes: this.filterValues, + }) + .then(items => { + const existing_name_filter = filters.filter(filter => filter[1] === 'name') + if (existing_name_filter.length > 0) { + const existing_name_filter_value = existing_name_filter[3] + filters = filters.filter(filter => filter[1] !== 'name') + if (Array.isArray(existing_name_filter_value)) { + filters.push([this.doctype, 'name', 'in', [...existing_name_filter_value, ...items]]) + } else { + filters.push([this.doctype, 'name', 'in', [existing_name_filter_value, ...items]]) + } + } else { + filters.push([this.doctype, 'name', 'in', items]) + } + + this.refreshFilters(filters) + }) + } + } else { + // TODO: handle edge-case? + } + } + } } - } + }, + refreshFilters(filters) { + const listview = frappe.get_list_view(this.doctype) + listview.filter_area.clear(false) + listview.filter_area.set(filters) + listview.refresh() + }, } } From 188835faffcc24f2e38e89dd2f83e51137eb592b Mon Sep 17 00:00:00 2001 From: Tyler Matteson Date: Thu, 22 Feb 2024 15:50:05 -0500 Subject: [PATCH 17/64] feat: color picker also test fixtures fixes and csrf token fix (frappe.ready) --- .../inventory_tools/custom/color.json | 70 ++++++++++++ .../doctype/specification/specification.py | 1 + .../inventory_tools/faceted_search.py | 9 ++ .../js/faceted_search/FacetedSearch.vue | 4 +- .../FacetedSearchColorPicker.vue | 100 ++++++++++++++++++ .../public/js/faceted_search/api.js | 14 ++- .../js/faceted_search/faceted_search.js | 2 + inventory_tools/tests/fixtures.py | 19 +++- inventory_tools/tests/setup.py | 15 ++- 9 files changed, 226 insertions(+), 8 deletions(-) create mode 100644 inventory_tools/inventory_tools/custom/color.json create mode 100644 inventory_tools/public/js/faceted_search/FacetedSearchColorPicker.vue diff --git a/inventory_tools/inventory_tools/custom/color.json b/inventory_tools/inventory_tools/custom/color.json new file mode 100644 index 0000000..b963e86 --- /dev/null +++ b/inventory_tools/inventory_tools/custom/color.json @@ -0,0 +1,70 @@ +{ + "custom_fields": [ + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "creation": "2024-02-22 10:20:21.468031", + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "dt": "Color", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "image", + "fieldtype": "Image", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "idx": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "color", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Image", + "length": 0, + "mandatory_depends_on": null, + "modified": "2024-02-22 10:20:21.468031", + "modified_by": "Administrator", + "module": null, + "name": "Color-image", + "no_copy": 0, + "non_negative": 0, + "options": null, + "owner": "Administrator", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0, + "width": null + } + ], + "custom_perms": [], + "doctype": "Color", + "links": [], + "property_setters": [], + "sync_on_migrate": 1 +} \ No newline at end of file diff --git a/inventory_tools/inventory_tools/doctype/specification/specification.py b/inventory_tools/inventory_tools/doctype/specification/specification.py index 185bf1e..58c4f76 100644 --- a/inventory_tools/inventory_tools/doctype/specification/specification.py +++ b/inventory_tools/inventory_tools/doctype/specification/specification.py @@ -77,6 +77,7 @@ def create_linked_values(self, doc, extra_attributes=None): "reference_doctype": at.applied_on, "reference_name": doc.name, "attribute": at.attribute_name, + "value": value, # this make this an add-only API }, ) if existing_attribute_value: diff --git a/inventory_tools/inventory_tools/faceted_search.py b/inventory_tools/inventory_tools/faceted_search.py index da605cb..40669f8 100644 --- a/inventory_tools/inventory_tools/faceted_search.py +++ b/inventory_tools/inventory_tools/faceted_search.py @@ -34,6 +34,15 @@ def show_faceted_search_components(doctype="Item", filters=None): elif attribute.date_values and values: _values = [localtime(int(flt(v))) for v in values] _min, _max = min(_values), max(_values) + elif attribute.component == "FacetedSearchColorPicker": + attribute.values = frappe.get_all( + "Color", + [ + "name", + "color", + ], + order_by="name", + ) else: attribute.values = values diff --git a/inventory_tools/public/js/faceted_search/FacetedSearch.vue b/inventory_tools/public/js/faceted_search/FacetedSearch.vue index c9a7bec..e46dab0 100644 --- a/inventory_tools/public/js/faceted_search/FacetedSearch.vue +++ b/inventory_tools/public/js/faceted_search/FacetedSearch.vue @@ -22,7 +22,9 @@ export default { return { searchComponents: [], filterValues: {}, sortOrder: '' } }, mounted(){ - this.loadAttributeFilters() + frappe.ready(() => { + this.loadAttributeFilters() + }) }, methods: { updateFilters(values){ diff --git a/inventory_tools/public/js/faceted_search/FacetedSearchColorPicker.vue b/inventory_tools/public/js/faceted_search/FacetedSearchColorPicker.vue new file mode 100644 index 0000000..65b8876 --- /dev/null +++ b/inventory_tools/public/js/faceted_search/FacetedSearchColorPicker.vue @@ -0,0 +1,100 @@ + + + + \ No newline at end of file diff --git a/inventory_tools/public/js/faceted_search/api.js b/inventory_tools/public/js/faceted_search/api.js index 2893a6c..c5834ec 100644 --- a/inventory_tools/public/js/faceted_search/api.js +++ b/inventory_tools/public/js/faceted_search/api.js @@ -1,10 +1,14 @@ function getSearchComponents(doctype) { - frappe - .xcall('inventory_tools.inventory_tools.faceted_search.show_faceted_search_components', { doctype: 'Item' }) - .then(r => { - console.log(r) - return r + frappe.ready(() => { + frappe.call({ + method: 'inventory_tools.inventory_tools.faceted_search.show_faceted_search_components', + args: { doctype: 'Item' }, + headers: { 'X-Frappe-CSRF-Token': frappe.csrf_token }, + callback: r => { + return r + }, }) + }) } module.exports = { diff --git a/inventory_tools/public/js/faceted_search/faceted_search.js b/inventory_tools/public/js/faceted_search/faceted_search.js index 20390da..8447760 100644 --- a/inventory_tools/public/js/faceted_search/faceted_search.js +++ b/inventory_tools/public/js/faceted_search/faceted_search.js @@ -2,6 +2,7 @@ import FacetedSearch from './FacetedSearch.vue' import AttributeFilter from './AttributeFilter.vue' import FacetedSearchNumericRange from './FacetedSearchNumericRange.vue' import FacetedSearchDateRange from './FacetedSearchDateRange.vue' +import FacetedSearchColorPicker from './FacetedSearchColorPicker.vue' frappe.provide('faceted_search') @@ -17,6 +18,7 @@ faceted_search.mount = el => { window.Vue.component('AttributeFilter', AttributeFilter) window.Vue.component('FacetedSearchNumericRange', FacetedSearchNumericRange) window.Vue.component('FacetedSearchDateRange', FacetedSearchDateRange) + window.Vue.component('FacetedSearchColorPicker', FacetedSearchColorPicker) faceted_search.$search.updateSortOrder = sortOrder => { console.log('updated sort order') } diff --git a/inventory_tools/tests/fixtures.py b/inventory_tools/tests/fixtures.py index a63ea77..1390d12 100644 --- a/inventory_tools/tests/fixtures.py +++ b/inventory_tools/tests/fixtures.py @@ -1048,6 +1048,11 @@ "date_values": 1, "component": "FacetedSearchDateRange", }, + { + "attribute_name": "Color", + "applied_on": "Item", + "component": "FacetedSearchColorPicker", + }, ], } ] @@ -1058,16 +1063,28 @@ "Fruits": ["Hairless Rambutan", "Cloudberry", "Tayberry"], "Weight": 120, "Price": 11.00, + "Color": ["Blue", "Red"], + "Brand": "Chelsea Fruit Co", + }, + "Double Plum Pie": { + "Fruits": ["Cocoplum", "Damson Plum"], + "Weight": 124, + "Price": 10.50, + "Color": ["Purple"], + "Brand": "Chelsea Fruit Co", }, - "Double Plum Pie": {"Fruits": ["Cocoplum", "Damson Plum"], "Weight": 124, "Price": 10.50}, "Gooseberry Pie": { "Fruits": "Gooseberry", "Weight": 128, "Price": 12.00, + "Color": ["Yellow"], + "Brand": "Chelsea Fruit Co", }, "Kaduka Key Lime Pie": { "Fruits": ["Kaduka Lime", "Limequat"], "Weight": 132, "Price": 11.50, + "Color": ["Green", "Yellow"], + "Brand": "Chelsea Fruit Co", }, } diff --git a/inventory_tools/tests/setup.py b/inventory_tools/tests/setup.py index d28245b..9cc16ad 100644 --- a/inventory_tools/tests/setup.py +++ b/inventory_tools/tests/setup.py @@ -99,7 +99,7 @@ def create_test_data(): create_suppliers(settings) create_customers(settings) create_items(settings) - create_specifications(settings) + # create_specifications(settings) create_boms(settings) prod_plan_from_doc = "Sales Order" if prod_plan_from_doc == "Sales Order": @@ -777,6 +777,19 @@ def create_quotations(settings): def create_specifications(settings=None): + for c in ( + ("Red", "#E24C4C"), + ("Blue", "#2490EF"), + ("Purple", "#8684FF"), + ("Green", "#8CCF54"), + ("Yellow", "#FFFF00"), + ): + if not frappe.db.exists("Color", c[0]): + color = frappe.new_doc("Color") + color.name = c[0] + color.color = c[1] + color.save() + for spec in specifications: if frappe.db.exists("Specification", {"title": f"{spec.get('dt')} - {spec.get('apply_on')}"}): s = frappe.get_doc("Specification", {"title": f"{spec.get('dt')} - {spec.get('apply_on')}"}) From a4095841c5bd14d6d5ca445dd7e07eeb38f8aee0 Mon Sep 17 00:00:00 2001 From: Tyler Matteson Date: Mon, 26 Feb 2024 13:15:53 -0500 Subject: [PATCH 18/64] feat: color picker background, date bugfix --- .../inventory_tools/custom/color.json | 2 +- .../doctype/specification/specification.py | 2 +- .../FacetedSearchColorPicker.vue | 31 +++++++++++++------ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/inventory_tools/inventory_tools/custom/color.json b/inventory_tools/inventory_tools/custom/color.json index b963e86..2f6d99c 100644 --- a/inventory_tools/inventory_tools/custom/color.json +++ b/inventory_tools/inventory_tools/custom/color.json @@ -20,7 +20,7 @@ "fetch_from": null, "fetch_if_empty": 0, "fieldname": "image", - "fieldtype": "Image", + "fieldtype": "Attach", "hidden": 0, "hide_border": 0, "hide_days": 0, diff --git a/inventory_tools/inventory_tools/doctype/specification/specification.py b/inventory_tools/inventory_tools/doctype/specification/specification.py index 58c4f76..07b133f 100644 --- a/inventory_tools/inventory_tools/doctype/specification/specification.py +++ b/inventory_tools/inventory_tools/doctype/specification/specification.py @@ -120,7 +120,7 @@ def convert_to_epoch(date): def convert_from_epoch(date): system_settings = frappe.get_cached_doc("System Settings", "System Settings") - d = datetime.datetime.utcfromtimestamp(int(date)) + d = datetime.datetime.utcfromtimestamp(int(flt(date))) utc_offset = d.utcoffset().total_seconds() if d.utcoffset() else 0 return (d + datetime.timedelta(hours=12, seconds=int(utc_offset))).date() diff --git a/inventory_tools/public/js/faceted_search/FacetedSearchColorPicker.vue b/inventory_tools/public/js/faceted_search/FacetedSearchColorPicker.vue index 65b8876..0c673e8 100644 --- a/inventory_tools/public/js/faceted_search/FacetedSearchColorPicker.vue +++ b/inventory_tools/public/js/faceted_search/FacetedSearchColorPicker.vue @@ -2,14 +2,12 @@
{{ attribute_name }}
-
-
-

✓

-
- {{ attr.attribute.name }} +
+
+

✓

+ {{ attr.attribute.name }} +
@@ -35,10 +33,17 @@ export default { }), }) }, - selectColor(attr, idx){ + selectColor(attr, idx) { attr.isChecked = !attr.isChecked this.change() }, + getBackground(attr) { + if (attr.attribute.image != undefined) { + return { 'background-image': `url("${attr.attribute.image}")` } + } else { + return { 'background-color': attr.attribute.color } + } + }, contrast(color) { if ( (['E', 'F'].includes(color.substring(1, 2).toUpperCase()) && ['E', 'F'].includes(color.substring(3, 4).toUpperCase())) || @@ -64,16 +69,19 @@ export default { display: flex; flex-wrap: wrap; } + .color-card { display: flex; - flex-direction:column; - flex:1; + flex-direction: column; + flex: 1; min-width: 4rem; max-width: 4rem; } + input { display: none; } + .color-display { display: flex; min-height: .75rem; @@ -81,7 +89,9 @@ input { flex-direction: column; justify-content: center; text-align: center; + background-size: contain; } + .colorpicker span { font-size: 90%; user-select: none; @@ -90,6 +100,7 @@ input { text-align: center; align-self: center; } + .color-display p { user-select: none; display: relative; From b42f9fcb76cf71335b48da67fb4f7d2f2679b097 Mon Sep 17 00:00:00 2001 From: Devarsh Bhatt Date: Thu, 11 Apr 2024 01:03:04 +0530 Subject: [PATCH 19/64] refactor: filters UX under all-product page --- .../js/faceted_search/FacetedSearch.vue | 32 ++++++++++++++++++- .../faceted_search/FacetedSearchDateRange.vue | 12 +++---- inventory_tools/tests/setup.py | 2 +- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/inventory_tools/public/js/faceted_search/FacetedSearch.vue b/inventory_tools/public/js/faceted_search/FacetedSearch.vue index e46dab0..453ee99 100644 --- a/inventory_tools/public/js/faceted_search/FacetedSearch.vue +++ b/inventory_tools/public/js/faceted_search/FacetedSearch.vue @@ -1,11 +1,13 @@ @@ -40,13 +41,12 @@ export default { diff --git a/inventory_tools/public/js/faceted_search/NestedSetFilter.vue b/inventory_tools/public/js/faceted_search/NestedSetFilter.vue deleted file mode 100644 index 17d5d20..0000000 --- a/inventory_tools/public/js/faceted_search/NestedSetFilter.vue +++ /dev/null @@ -1,4 +0,0 @@ - - \ No newline at end of file From 94d33798a07e437a81412ad6b1b7666b53b20b37 Mon Sep 17 00:00:00 2001 From: Devarsh Bhatt Date: Fri, 19 Apr 2024 19:00:52 +0530 Subject: [PATCH 22/64] docs: faceted search functionality --- .../docs/assets/edit_specifications.PNG | Bin 0 -> 95476 bytes .../docs/assets/publish_in_website.PNG | Bin 0 -> 108908 bytes inventory_tools/docs/assets/specification.PNG | Bin 0 -> 45166 bytes .../docs/assets/specification_attribute.PNG | Bin 0 -> 42562 bytes inventory_tools/docs/faceted_search.md | 49 ++++++++++++++++++ 5 files changed, 49 insertions(+) create mode 100644 inventory_tools/docs/assets/edit_specifications.PNG create mode 100644 inventory_tools/docs/assets/publish_in_website.PNG create mode 100644 inventory_tools/docs/assets/specification.PNG create mode 100644 inventory_tools/docs/assets/specification_attribute.PNG create mode 100644 inventory_tools/docs/faceted_search.md diff --git a/inventory_tools/docs/assets/edit_specifications.PNG b/inventory_tools/docs/assets/edit_specifications.PNG new file mode 100644 index 0000000000000000000000000000000000000000..5557b474824a353c4d5195fea1ce79fd6421111f GIT binary patch literal 95476 zcmeFZXH=6}8#d~UVgV5=z1T&1??pwzDAJT79Z^Cr5{e`Q6ayHVr(4#$jc5-f9ziRx*W`2v2z(0^x7nPwo2@8fTaxm!Di~i%3`^~j+ z-?1J0ULQMt_4JuDhMb+q)GClO=tK_BjmPK6N|SqPEB?@NlTsK~dZ`!t=hHv0ojr)f z(1!_)Ce~H3+{V1dqN2F~A{5bo%71XBbiyQKqH(*#5$C)A*zb>Y&++K9zd!bK7r3>r z|Ms-?>qXLz@17 zbdZP77KVIFX6J_QtHAtHdefdg8!F#F?VMKTJF%LEJ6822cKqt7!=MKN-`G`6UrDB_ ztLjmDPt4Wix?&K7WeoN7MZI5VZety9V4^Tr`u%AF$Byw?q&52rGSW!L2s6FogUP)U z<6xM9!PSEng%$=BS5*x4+{LTEeYU^H2m0*n*^6VJ)%2Ajoz2kd12h7cpJ`#Oj^yoI z+5aly)L#Zh*V}yZX$^~Naf$l}S_xckrn>H=I@>l-{4W((+@iawka~OpCFZI@P7-pV zkU5?#j8xT#i{^^p`?r7nvd-Py6;AEzw-Q!(-^&f7UE>tr71xZEm8m_8IseN(_DRcb zz@~3_QU~GFu(r3ur|t;P^>4$cuDPB3*CJj0useX{CrTCr&RGfUFCVApFqIgL>$6#a*TRiPTj z`)s>7)+yXXQs4HGxo(Ytt@+WPj`RBMe?KFr3iF4xeylfX2%OT6z7v8!Y}qvli7J3^ zbcX!&y~Oiy#r|W*q=z?p8F$n7A?bDa@V=PN7pNhpjGO**u*JWWn#~s<)9#p znjhO$j8@#|I%{9N5u(nYjA0(Var?>4C5?4u+OTB0$ABxzyXK8uHKtJS3)?w9y?BCI zP@B?u?w)c?U!~ygiMizi35Rwit-!B@ogDD`ilBmtAs2`aMT4+}t8ML-Impp!zxk!! zC@tI1W^AJX2oW4^tJ3Z^Hufb=s1CDKX5q0o<}AS@J0WI;X;;;JzOi6({wjM4Y@^|v znQdl6%mg_lGIlf`*nCdvN(@GPqK{pQWuQ(U#x0Ez( z>lSXSKoqlSAnG&3g%U;^q4O2ywa`$14f~RQ`nNYpw0YlBPt2Xc*C+CD(kDvO-FIxI zaCdN=5|#pirT&~^W~GH3gL}J!X||w#HE+32YC7FYU!39 z_{bV^3;yf8`tzBl^K*@!_?be-{67s@ka%W-gp>)+NSrI(Ow{#Qbg9*#TQ9#~g)`fn zfsYNXmb$uKvK`Y)8y=|Yu#zE*H0qHy&o`WNid(z(;pTYXFc>rBQ@{?w-BlvGEi9}^ zm=%bWEy7O|E&ui|l${TR@y;cwdLNQ^~ z5NB+POl#^x>;>{z6exwBV%bD7X&vBG7=;%d#;og$E6#ta$Sr|6HTQk;(^p+Q3vpQ5 z9)qC+H;o}sudKWs%EgzK9+_L(o?<4WWOsLRF&k*4vcYa8@*|;#=jynidQjx-P(znQ z-e*&;$||yCh2~$+JPzz$MSPy5PkviY5pmeicxQvG%aZLFSKmLEVjJs{VJq*DyWZmL zI2z0)W||? z-{wE_mV8)>3Jo&Wza&r{@A|rRKP@1Clgx1KZFG8LjT0}jNk|^7b_&2ML>6phx*y3Y z-S`t*t{XzbbLp#gr1CT=#=|%J&B>clIw`meZ?LU$D8;$1_)lZHmu-dLeXPQ(!o_Xv zbChd%Z2AT0)VEm${t}%Iju8CDa7_`abw=OQ2CN|qsc-O^@?kD!BiJXVL`0Ph?XoO6Sy!UD(pIugnGLZ&W;}{2$y`AZu?{7*w$!gv zY@o7anO0t#QoOO2A$y05{6TA}LMbUVwCEbYUOhCHU8kJHM6hSH6+CZlOQe;gMYWg#LAON5wVK9Q_c2UA9=*)CHw< zwd+-?i^-Ra;TqULHR&m2Hq9Jp#T@U@U}HZQYiEq5MNh$WwAAYmMkeD+uB$){YmShS zlaH99HM5W76+L~t`aB;x7(X+okrCcW?_r}rV-c0}hVF0dj7b9)81*%N3g%UcAUjCD z?A#iEks{Jkr)+#V#!Y8Y!gG)L^HD#alzX}tN7A!JM{5%$yU}gY=IW!3ZaIZb7{-&k zo`*oP#gf&<5tz!u+Ki|?m#6SK7U=M)6HQq*q%hC8tyg1XBOc+_Y9oQG-v}X5LlA1h z;s{xM-GRVF6nlEKH+#AdJk5C$(G(@o+*M2U!R>{Ve zBSv3C{0pL@jLh^G6t9XOV z&=N{J4G1a=xo>bW4R*#E_?f0dSd9+-RB+ue&nA3Ka;mk2f9 z98vcdkoI-1l!vd&YN_95pi!p96|GNQc(#fLlNS(4jb2GSviBn0Q*61br*c+v0!Gqn zi+pQ@s0PddH7@QRCkv;#+0HE2Rq>b0Oye>%4~;Vk0dfs<42QVQ+}G`S!Bc*<2_gig1)WTURS zV?%w&ieN9Z?388{Fmf1qj9FOO`)I*)VfQwS7O!+2_Tl#x&zpL?P2z0|*7GO$q>eix zKhaZB`-vMW`WE21;&rg)42`1U@0wD#f6(Uy)qBIg716Sh+x&#-=E4KVRL9PJd=*b& zMZ~B$fbTTFxILvZH9xt!A>UMdr6>!gj=8=1UbeT2IXJD|b*e~lqe+{= zGZ2H}2Z6l9vv@ zO6qI9lmV6yP@PB@8dKoI*QS5Anmcq_^r7}BCrzkIvKq(fi{srY%hl~^FI8Avj?Z7- zm^fgMzVNGRlQC|+M>5nZP11|%PQdXUvwBnwoz`W!?D9cmtzToH-0KMfnmiXpOPxID zE}TF4LgKK*VR^MO-nSlnBiol9*<}-K<%d0VV>Ttz0@Be3DQwSJLyf{BPn4w(c&f(V z?fIGmdddbmRe8DGIL@-*>la>u(oJX_v_)6Q6nh?;6)5*)S4Ow6pL3T2{EE|KaQ&=NLLG)B0-?^DIbmbHJ1 zj$m+0)9MBv`2*g6H@yOn^cPDeN1|g!Fm{ajPK%tIG4_h%ukkpx{d~uV!b0ZWtEkm` znDx!(#%G65m^hW*^M%u1KL9L_q=e(?J+uOt?bN=4cwT?0!ed8IdUy&WGEWrSdurEFMyYOhM1;J<0|J%@aKlSmlPt`X|KoQaQ9*f@(yok@gvI40BB~Fbh zjt)ZViWPQXWR_LZOl5rG@Aj6N+fk>gK!yvk>adr=@C;0)u@<8!FDo3*>asH5XOOmX zfiJMvy1u{KFLh>MCVGE@{#NB3Y$5Y({77y{%ST!A$l%5`)A~HY*QF8p{gTJ!--Z#{ zJ)4uW@~bR5MERpp&X_D8Yvg%;B?{bH`eCyAgv*0^yMBR71xNK_UN6bX>-WDl{-Y>G z;iPYO1aIsS&S;R^3_R5 z%T3RolP*k0&RylT&btj?+*>j6qqTG5F~R10j^{^NZc>;lCAQ+@2`^hTNSofqf92kD z#VPGvW3uGhWK23<-v}{cOclK&=t1VXOj_T$)Zc|?%|*$pZF(5jZ4F;Nr-S}W{+m|K zH6us)ShE6bJ`^@LG{YU1SEN(Idy%)1B!7M7U63NlKNU_0FN|$9Y{7en7zylU=Tg0V za#e+^?5?Y%#Y$JNcL-kA-Ei2nJT;t$SHb&M5Zsohze=ttw!d2RqTV8#$T`~ETF6KM z*Kigjw>#N!z^h&)6q+eA@6#0)YG^Ap2YuFSUfn&mT|`O@jhIk?*#@@;KH7Y=@M#y0 z+I>1Fha!ZM6KsOsH+^r%e6|RT5~#Gq3D@E-rw=j?3S_(w!3RW{CCWxvzcP!<#j77s zwC&1(u6n7?Lmr{e^P&%Y`XItRfEFV)*%M{nLWsZ*>K5b7Pg||!bI}jm*Wv1*adoX2 z!#ta}`o8w--+}}29v$K*j(~hgzFXuL9rM}klH7>JfDIocUOTSHW)#*bA6Qx>`KY)} zTb$2rvF{LtoI+0L+4L>~$Vf30idX7y;{REsQYSLVO zhr1X$j8)bztJ=YUvF-86x{lO46#E2!GSJ0G;*W-gZODcYZ*ZxRWtS)fw>9S7%&P!nRt6}?Sn z1WO$_!e7nh`BV9Oq1&7oij{+B zGV6MslnR4=i(PplZ`v;1eSY5*r;w2vls*`o@i0AUqN%&l=E85uNybHvduuZl8xAM{Xa#Ts(@Y zty6X~bmF#V&*Kw>`U{&<9>w!UyWHBwLUY?sMIVtr8NZhW$@<4Yqj$>y92Zxkb=e?s zj#|>-4Yw`p^bR#5_0@X7-3j8;wbahcGj6SvH}(}<+A*s~txvsa1wN^5Ew#}|A~{S9 z)k*pf@4~9P&%n1HxMx!HHZ>zO9Eq~^T3;&w&$kunF=Ho!va(7iFX?C82gRn@1*cMT zMKcLiH|W9{OUpO<{ezK47r3di{LypUuLXyF>8a~1T{A(23t_W6Kmr&ge#N&?oG_pr z0=#%>#UABlyhA*9GJfDudp@5Gqv`$Eo{?B$h#<0Pq&E4^besgYy}$5sw9zg=wp(OJ z7rv8i(pUb&NxF5k2r;m6Di$-dqSiITl{HqLGmr{hEe}jI7fZb(T2CV$d$SzV!v_o6T((^b`hlEzsHmeP{P+=rx@M_vfg&X! zYJ@IHqC|W%?R>PCg5*5Mb5C5YzlPWC(3@i@<7wLCQnk>Gx$ivek~O#FxJ@K+IYon9 zVtJ0lbVycuM&ICR_5(;L+_zl7dj2Ua;3oYAal4h!f;b*sc!n3a9X!g5U%K=lOXhyq z8FhxHlg)UeqJP0Z%CGrcviCqYnnTU_Xza$Hx9P0v>s`|$!N{!1@E&n;KLnJRtKCH- zoJ16I6$BkE-HAvr$i~36`^Io6uwZD4vO_JXF)*E|uEu&!~fx>>e>)`@ofJ~5$NE%KtgHFd;styKv5D!)b zGQo>Lunzb=qRSt`I>~Ek6iBMEz;#VaYi!@JdTt2T!e2JbM;`J^Mi6{h($00n);-B( zd$kgMlYXV&Z+-*IwbU|IL{(^gk`dA(N#;$q06obHrwnja5h1ppCYKB-n;i+g2QfpU zp6`sAkJdx*K&v~_9tz8B$erC{Z+uh@k7J#X)1%S5L(NY^x;NG1AQP`z%B|VO!k(JpteyCrT=KJ3y&I4=C+uGeUP0NuPsAjXJ!F3 zNJ3CnV@-8M-O&f)8UZ(*1wgj>+HvnmyZ-olbMxz~U$hCvVE14E%~`F%-3&Dnczq1n zK78fwRMTvprXmpeW&NN3vfG25?NIV`-In(|J7dP-c7rqrjMs=V89=tEbS8qD2F6yJ zkarU?TDw(U%QtQsfDA^N2PYp(7i=s%lq*D)nJjFnk5?5F)v@;N0@0lS;F$8G9z7s} zC#+@@*hZ#OO>s)IA7jSC?(m zH)NWwaUgIkH}zRy8}KeW7q5O_1_I5}ip@%TODih{ZSB1tznWB#ocFcR_V)XM`_&d%`94^u zHRUu`(T;r?d~}7?G6Dc`Do8YM4xf{#`orlFPuTMWYvtkIL5TV#oSRG)rx=whTI6wm z^R$heO7ndD9q`DN5qDAE=$rAAEqF6>SEdXuwfVeI?gqaY_W%kAJ(_}d<1Njm`VBRN z_*D^&YI}bMy2lpY^Q{~`*ig@uw8oXaTTfbzfcr)rJ4U`^t29u^G~4*@?>aM_eH*379^^!fN6$GFuie_ zH^REn54QKiZEFIiC-7eHd-AI(g486;t#UU;e=tZnTx zGc14!&=)9rqM_4^*>B~kAt!DUMzAP*13&zh$S7D=r8m@541yI791-lcPX6PkBCe<_ zd40}ope<;Q3fXgHO|W07GgiSkVPgAPCRPSx)|o9+1zU&p)g`Ot71VMxF|HvVj@o@*q%JW1c|^D;1G$xptB>>_@8%YwX7SLN^2 zI!o039UlM56MXc~G?e3yp4j_^TrD&mfJ>&!E&eTf0ko1v%F{IP>%j^fTd!2+w2C7EY7%gn%p^Ezr zUDE$(>ypsE!EZQ?ou=W5hN`zVNX#DTl8pub^B_6`@Z-W%cC>wkB@CJZF?!|_LTy{-0Tca6DjDonZ|}LslU(mEYv@izXmIe2C>n{Y43Kqrw$ODD2(HL z4|)R-PeEXWW>ay_NFG4eB)aLcg8b*MMcSSG`RUUhKJF7ES$5UIWkw-J{E_>R-+!c~ zOJ8qKeUCpO?E$E0K3Z>k>7PS*3kaX|#pYAQZ%Xab!@zS?6A0UMUSF&eqSc55=MXap z!LPYhpYi3n#>Hn6f{!lByp2|JRW2^AErB16Jex~+5`v$RLJ6n>jOgFjz`lJyk~)4k zj}Pxp+ADp-Z*k-+`(;W==cK5R{kb1}pH}5tJGnv2H@^-ZWF^(djBFDVIcLQqoolQ7>lC|-5hEDS=4U{a01(DUJ_1e9>Vn{}4HGM{PL^EQ z{P|&bzlBv2?aHmFxt?KwvE_67!JZoZWKWgO>mCA)|Gw8H{^(V8N4XTB8;d+1X^-ef ztJ0UoCJ0~IRS(>uIK$mL>fmkXe`-#KKl0|U9osJwu=$``fHnps0bA2o=SONrS2z1* zuhlWGYxQ9qBU~^Dq`z?P4WFx^Hb}#A@UgjF$Z@t(^W{mRz ze9)~|yQ;rifr)w>gmvx#d3;+4> z`F~FuzRL(#{ZO1~@#5L%|Cm0r;>@o7if``nz^5pUf1TytojKUE=dqYK!N60xYtXG5 z&C4%7S~V@&U`7x7?=qKNu`@bC@COrc270V$UZ(Hi+S=E+9w3k;?mfVfe7_@hD$hA! zYxH2#&Nh?W`n3a6_R~8$r#k_GnK(gEXQfoWxtabIw zWLH^}i^tkR%x=8`CT?CJi;~t50n4@qqn|5`cxII5Nx|=m0oqD(ZCrb2(tkqQc8kou zTC=S#2sypp*BUIFmnJ*bZByQc;QDdm%;P@lW;d1Q5cuYYS9f(~{)1x|CHlYY{|j&& zVf}PzMWN31{y=d>Zq^;5f@cjy_^%U-O|;mgW}lq2RT2q}*FPcg0=?bb>^d=4C#mRb zSG_ep2=57xnp{>^^2j=Ua?H_P8adcVq%R-IUBoEp>0!$pWSU}qbrL*ZOKvZ83@Id&;MuyY>1 z6*KlT9zO28%o(e2Q&{BSuDcffa#vnyZgKfc{%7{s{5Wq0Y=&CbGXNo{NCvH2(HOyM zuTr3YhWSlf%XQkQHE*}4G!(=b$j9v%I=`*_A?uZm=i$hhlaZ|d_4T)J4q9}J|E?! zB*J&$6%Je01idydcZ*NcFqDF}`g_Wm=_TFM zl6Kd@5&IRk*JtkgVd7dNNYZ*yh*9bcPh!qtDkVfI{)WRHo*JQe^HDs8ZqxF|C;_LI z!ZA4pgLF}LSe;sht$hD*S-U$!?+%FS!N5JI5Mo9-w0j{y3b z3C=d6GgrnXefuJ7QB82X5wjojBb%LGe+u;fvN~Tw{u_7-j)fYjf4z;X>ZjySp1##g z7LW~!WXA<<&_!KKH7F4IT_9BUN@Fr*vxFTUf*MgO@khHvF6$bdU!Pz zVK!_G%91IXhjKtAp$0ANEc|fKk!=poje@j^PZMbz4kV~+a}vJV>D~P-I{%d50b8k! zccH-*t%pQ~1A$O7oy*G%*1J=`)=$6?z*S|%)i9vpB9}1+D&3li2zYgoSGY8nKE4O3$t?- z1E4l65FYu8wb>82OBgn;J2rpNQ+@*7F1VCU9z4tKEBp-xA!(@IDh_w>MgN6tEkd2v z*IIHBo)Cs|SQt#5ka*TlB4Z8+iY&ziV+J)wZ+CoTm!z<99mpR0dXya`st#7Mf$qeq zZ@+C|ro;3W(Q=`nwF~mcP&*=CD4Vh zLj{GXJx)ik z=1ev;B8V2gm0I2z zYXrOI;;A2peucHm%0I^+d!*K_q_3u-6mNaH(3Rn|#)Fo(L9@1_bs=d%Q~fTB1_>gdvtgP#x;n9 z{sWNg{JJMJQvoOmF8Ix63)|;Or28DpiMniUJS9nfbSp95rFe+r(S7jm6z^W*JM ze3e9fo7(qpcNz-bKg=ph{~kc0HcK4rs$LYDSRrI<&AO08w3OzJhv|YX*zv47k^(?i zR}kd)m0VMfXi=it(ioWS@U+}0AN9SmP_dKTiV#}2>ewSBxwjVW_@F`aF`B3b z+w~fO@mm2~F_>ogA8NQ#gWol+Z;6K%{))fAVkreaxcRSj{jKiAQIqku;AkPN6pQU~Y1V8f-yq+9mT2khmoabQmjQn>dH8G7h zcNkQGN>29mu;ebFtr>JmDBGlfpM?)|Qbs#2thS`ja54+Yi=6?wGXBKXusS4x zI)z!R*cq%|TB>9XJhfTqmZs{TLLvp!&7#RgiH8A<1s>d$(LX8G3ja$k4L9ob;Ul;es zZ0BxAu$xg?^xj8@`^_C&qm2e7ah%VA-WK_AyF6zQP=Wo<_FnK5ioB^HXSHx8KVc?g zp#gU*Q9-mh_6{@nTSW)kwO+5Id6k#>eY-?wn${gP7Zv%-`-wh4-b}{+YfEm#z|1?_vd&=i_ z)fz{m|NQC4IsdcpvA=(@M^54YwdBve_QbI7Ho;(%-CCgdE=l`1R`};{uH4u9Z+~+A zSMxaZf4BMnp|qKiKugO!05)~7P(g)K=pL}~g^=5Ph=mzyUL&!rt@p~f;tV1kcKFon zg7)=Q6Yp`eM`a3~cd%gUh0JMd_c*sdg|z`uW6 zeHqAd0vi50p0>EscMB8a3Qa@DtQNb{8q27TxO9q$DaD^Yui$R2%KY8;LNNm1reG6psS4j8q((M@K58aY_+X6?`NSW8*sSxDoxas zJvo0L@*h9c-QK=RhV6bno|mS&$r z&#EI}fk$00{5X6uudLB8#D_PfKGdjydRZ8c{*yZBPs(~{$0J_kTWF2AozqR1)^#wo zOAHsx4lQ8hGc1BGH7xKFGvP88^y2pAjkuETpTtoFOrzSVKrj9Za2P%E*%m+|i z#cs3mqILA-QX%^lUno_ur!pt9t*-~RuW**Ugu%8t3I*}Go)4Q?%%7p9d^%`d#tKPkxPEb$Sm({w33cM1ts}C zU7hkRzdvDJA6U|5sT%h zKeJoaPoly=lo2UUz0>@eId^TOMyB(^CG(jta`q;X z?Tmw5){a-nSH~3ZN6J9OP%ytlIvY6U`H7V7S1LjgL+Rpk#ZrJP{&?CRUEmf-e$&t= zddWZ%n5c_F12lPZ-0vFfsRw4oyWb|!igIhGM{#x_EiA;*Ln5&KD=`MVYI3gIQLF1i z71)F3A$jDrK(xM$XbTMn?IGs8UdHvz)82;2M|+NAi>Drz#^)gZqYHLf z#X7&k!#4pYSCZR(Zbf3SqTr*|(Ifd9kOuRiiD2a-=y!2bCYLv z>rU~6*Y${{Dh=H7=^b&hKz6xV2iXUGUXfQ_rZr)~hT-I#s-6!t=4$Xl&Yhll`E7=& zi_{3d65pM!Fk$ruW{_vAG;zz_!Zo>z7vuXvO3JbN)+K}C)C@BL*Lrj)U-&3WiM>U{`nV4Cg*OKjOcaa^+JJI zKyf0JE9h&!si#sm$n0S8I7o?D4d9XG=cnftsw~RnOVEQXqiBht3He;-fU@A>{nax- zNPN8yIdl}k8{J7xYGp+TG|XJjCmTP8;-?M72soUrORnc!OflD?4-$6`m8=S_WDD$6 zC%sqM6wGRpa-;Mm63LsuCEZ}(tL7wC=;bx0Oum!lz^YD&w(2vj*EiB-+uLZ#nFwj^ zvaVDU^5*0K6$S8z0>kyS{+-VqX#0a(bL}z&TysHC2zm2Xxrgq9W*gT4G4I+EnfnHs zm#c5J-6@sQvph$6lMg7Qw#zJO2eNjyEU%k-{%_2A&tKdZSdbLQ6v+(f(PCYrAM7~b zPa=5~XY(GfYEuWn`A074i0?go-4=q=7UnKV@T|#9iLv|1{s)+c=I%o%fIhn9a|0Yn zL6&t300(RL?-d5T9;hTq`2t;O5W8DC|r_t0d3qw#`&!hc6()v60nxSf?`c zhXYA0w5{|95>6r)_{F6UkBvRh=a5tcn_#->hd?@sQ4%WZE2ut$n4|~o>GK(m>Qx`* zA{dk!M90~3)gg@JP>hB53(I`P55={>ivdwziHW*-oGN|}iofDai|}P`>msJ&>+Zrg z`;Y^NwH->jtfIWCIUW2uOw>X2;0z#=2U0k|C2?Qe}@x$t-GdDDt1dmaBc=qa$cqv8C+GPnm9P_Y>JD18qb0?m*0!lFlZ?L zSUglX{kAsg6hR_J?VN97lHlA0B}1u^3j4sT4F(NvPt4Q!uGJ&mtCBR-`uQGaPKMDC zoIuwwjZk>wQ4{wy3H+_t1We!P0Z$f-c3) z%u~Ng<*8^$PlBYav&AxYMzG3ZogXs0j$N^vc>k3v$(Pmj?4+SGQDyVC()2>JF)*}K zBT;4CP{b2!TGcI*()lgc^|FhSHT#KTU-;;XdE8ekcU^HHfbtJngDapBe$-h_&t3^x z`;QV3urM$X^+g^0aJnWJZbo|1SduoFcsUBTLB@^WC$bQd6}2NikGpd&*7sI>YT}tI zv@S;Clr?;GW&uqs9z~6&T9)ev{NtNFk-jN~Ur;?I6VwFz0PKu{>{fJN0`Q)m7i7x^ z;_alG!AhT_R1J#poH$abuXtjxF~H==%1IO`yvtfWr+jdFR<`bx_0DxERI6Ciw>BS= zUt^pfFtk9=V)X}*Cvul4H~XrQ{a0aaUg1yt)V%a?m5Yh$A(BYf|w@14wpQ< z-TV&v>6;$dT!Wh`MYtsS>monnu};~+8DzMEa26h%;+F1YeSQxO=1#gJ5QEr2b)FWE z2>s^%z%Q>8lAVi)DGq!idI{_xMxx1AoJ_{rW*B%m+is&!9!#E^PBsW8AD8uq?L5vm zcI64W{1n0f7oLjjv_L2qciNVCRVO=`zANyaO)QMgnIqEN36AJkh`ji2y!4VPGgfDt z`?5R`s&=QJyw^os?%O?W(_6ZU_nb7JbgXDzS6XK@A0%TCp*Z$SO$5GV31UaudXfPav5?5 zuZCSh%mTL3MBe-S83dChigHsKidH7|9zNTd6h@U_$F;2K=d+H<&t0>F>)!RSb+fEH zXgIc)s#J{Mp64&Rvvtc4crMRMF?^d^pk$C;`%xl#7qb1~aD9bfq zKS-Q;Ma_VfykFM3P%CFL-gP4cpF=|=?(D47e({L*pg}TC;c3JtstiPOvsfA4PJ9ya zX%T-f~FlP9A_sTbp&pi*-3=auSDYV7&4FouEBQ}OEHmP#Ai`Umekap20h`JZv> z%^6H2+Mxqy8m?;k)dTOkdE4{wHSKpmf5I$KvUu6AYL+oeMtbK?5IQUVPJB)<=mzw$ zX|^X&N0~~ER1u@u#rcj*L`L|Yu{<~V&4dp^bK##ck6J{|Y~Ys>*=6%A2(JA>bMv#{ zn(%W++|Sk8$yIuwL-F3R+SOt?N}nA`NW~BQSGXco=p%JjPa5p1o4dMAdM53BDtM|C zR-*CpuH83U$ti4H5#gW4Kk5C`WgnGADVRy!3&od85GqTCZz=Cok%Mg-9=@~l=_-cw zTxu_!<$CN_mT0H({e2SH`T?|ncy{v@YwgLFk1pzY1oI*pt}38G5HO3^23`_l_E$>q zi#v{6@Hp+)+V&g2V z8)dgebZ_)7^G_LQER_X@D)0GB+upC;*VTBM{uJI-I<9C-E-+fv{}eaH3+faghmF;7+{GPIw;sw=0w*4{GA7 zMuY1Q=je+L-0qrZV;B9ds|&O0OlNL)hmm(QntxI9r@=n% zLG318-O1vr-deK@GF@f-YTVX}v*&5gMF(M8ud?n^mqZIgN?an4Kmw}*M$d{(lYG@o zE&}fYl}7I^01AgAVDZ>0PZFS@_dgZncY`F=(8I^Hv8~i={kFfXZb1RbiJE@7Al;M9 z$qHhoUmoEK*LPy;6bd5GEMx3^);s%_P0D|-DitK_TT+i(ES$Jxpjf1(zGlH?ZBw3B z1+F<&ESciSE_xUIEnA==;N@enO7~&Z6^=B`itZ~nuzVz@ps#o-qL#|u1%vA8B;9iw z3c5OV`p6UyQVU2QDHRC{NjuWsr+yVw-2h5+!+(tjNd@~I(weQ66eK7<@Tljqu=Cf6 za9$KoHI1z~*qkl$tq`na5hr6N3ry^aRK@0W%DZmctE!bUn{$~yT{E@}=ipFy## z%YXzeC&W3&^6!pWZnV+o>_UDd21xJCc0Re3Q^U^0GILjin3D{$SF^#6ht8>f%!OiQFM6}u_g-J-A9Oz6g(mXln1Q&H4y8fL1F22&yn$i+*jCwp0- z8EfB?$b}|Z%Qd@&_92|)JXOUovkvNApWybOQDzyVpI+JE6{P9P6K%>+x~RfUeg1SL zZ{KKd@Wh7Okxpj8CMqxGIUn0J2e>)2l9L>QuL8SC6A;=~gVy%IMZOs0DzkHc&Hfi! zl8CQapV2OXV`pL|s$4eyq7Cl|90H{{CQ1e^i^&F=Bsfz>+(Yr{1%u6)G}-;34Z~6Sr;)0bY`Ey;T%c+y{x_d2LBN@{+HAn5Byu zqR(E}2zB%r53yUhzW6C!eK0w9)3B)_z!IWv5W1D7EStzVzvx02W9MkF{O9It%Q}-c z3zgg#>=##cD}wWt5`k9#lE=zws$2SrVWOkHw4=4k$7r-qqVnbAAJXI@mXoNL6K}WF z)>lFVAMah|-nvC9$7HBa@k2Iv9FuY3WOHD`vnMP4&EEY`>dSYG*Ox{aau2^4%RcPB z@RA|lt@+gJNzGwU5sd$YK=l4XF52_YvP(1ovv`OliLZ^u@CMzSZ2I7seG+{YT+_pX zw7KMYIY4;)6lE~w9+%7;iNDc`v6+!gV|Ck=9lR{4*{(OWenRZok(@?YojHttkfuCd zrF1`ixEPo&zQMRC>)v*mOjw8UUP`IzD3!B3i~g&tB7M?KiC;jt8)&Bx=J)br-Q~56 zfCN-jyjMV%CCj2+(NoGbAl233hYdniAPEg9^o~YjW0~|F&hUg$cTWEZ#GyGy2u*$~ z;!}LXy+;GBE40a3q77uK)>Y5HKIp)XZ5@{2T5~;0P^eAihEybYHaP}NY?;#XdymrF zEqBeaGmgE|NX`|Zvx=SudFC>!w{}ox2BIqDd9P82oJvJz26%3x4a~%)ro(gDxo5L=>#UddVcXEIXhr4e@asg``#ib{|Uk9xrX|pyu+f0ufT)0Zxc?g z-+Ge`bYpo9Sw2&|d2Ap{{}hvw#_mppl@zL`LyR=?pX}|qIHzD~soGOpigAqgV=UC3 zdH>u_PVwnN>3FWI8EJ=xeKt>JufkiGu75&s4kjHolUYup~+TMPouI+YT~$G z&76{x)W*7O&n#E0;++(QL$bwM&_!@tMe34 z=OZXKA68X?Fw0tMi%Z>5oMX}=11pXEp80{pR&`Hdq6cg+ocp{&ObU}ySvanPRK_+kD}CVTemP;G-K*#`M9 z9usQkGxImfdF@}6^LpF9QPk^ypw>N|KTsR7AE=F1xb2$;^8Y}D$D}7c0C*Nb2jIe= z*Yg)VYh)hDa^ruIS=A=&z2pIy1QbAccS$(=7V6CN@8-Xq3kmR8v34ZZo50_s;bMO! zhYP=qt;D$ZTXtFr5p2i=uzx^nt6b4<=!EQFAOjN+4?SulBk;Rwibb7u6wtTXS7 z&fedTbIv|He<4qvyRCbzYhCMF9W50&axROzq<=K$z(Sm<59mn9I4u_%wedI_wZ<&`P^+j4}*w>l_Z~p!cZ&w{pQM@ zJ-&0vnEGweQ7VgXs#a(NK~Jd#2T(^w%;n$ptd92&rGK&LF*k&H@QZ~07_xwU?J(5d z%*>&GmGj>9;CszTCYQ;8;Fy9rz96q6dQNDJ`a{;ns{B%=0`$CtDB=!Dc@#6M9PRZb zb+GsR6X+Wa!%cN^6TG{5kPFeqjYb9z32r@87KN1DmErd(YQuCQOhlFKy>hZ~b)$rl z5QnbHLVgBR;U$xhzjeHsV#96K)hwh$YQ5nm%t%%t_rtI?T2#*i>;AgPxH`eWR{nsI zq%W*LhBvmy;9Yi81qGL$}>(if-X9r!BrKC>|A_H3psgpugUb{AF%pGKRE zPJPu}-%@#2h@%oIh?CT(!Sy~@E{ zs&)Bej2X*|SJf+nJETeP4wh(Tx6|6C4MZ-OSKC#R%d^QAy|U6Qv2 zRE=_ZcsXr2gxl)b%qyJRlF`)M_+c}mPZ!i_SM*T#h}9d3FHH1P$2*4+eaS2*0J({I zBWsq+LME9RcDgxqj~;w(E61Ht?D6hBr(g1FTEvXZ9%JwV68c4vQ!2Bl%!G)>BF@HAf1s3?XJ>t5o zQZ>|JMIehtM1-Oyi#KX%dB)Y0WIa7kA;(si*EVqnR?G!RW1I>U+^6m!bLY4PPD4N$ zBWj}!=0M8a6VwxCw_>3b579SXb}g5?X7FyCxp7Kk3ZIGnccegRKx>DSDWUSLi&klO zUpJJ%5xSYAsM6mimddISn#yugl=YNHJ<)G_Il1gumVL$aX=BSVv1DA&^aJ1R7QSg> z`LoeD2bk}>o?{gkg?kI`VlQ61p!Jz54Xu?(g+6DLJ0T@16EQg#=vlZ+IUs(awOhC$ zQA4Z&$-ew5G~xwgSYB+}pitCN=M1ptRhD{ma_5Gzr5^C5=9%N1F%@g2lrD?rV8b=z zrY$l0pbjA?~b{q}=&%yn1EDzf81zggK>e_QLFo4cq47tt=J3rvS!JDb5#o%fyCM6zjL;IJJ6XrzbH-Sk zfW>ek++{A~p0(#w=q;Z<1zW!e(IUeI!=^K@&xtGWcrAGPN9vLkC~I@%CRjo!T-tgrwX!yk1$rIqouB z7M`ZbqFF1_oFE&RMIOu4)miJ5R;jI8Bv>l-GNKW~!@YAo4uV%k2Q~`%eK<%Q*H1}7 zT;W2u0&XHSJL0~*Ok87blJ^ZFCcoMSwb&^wR|MGT`0Z6TU!lF$@Vv&Q(B|G2?pvER z!|JJ2hH-9B!|V}3HI2m*3o+zUzIuO&OPbqS^eE9gca~iJJCc|eja@NLbt5`8&spxc zxK7j*E4dro!mhg9Y`#QwixcYgKLdNWm1A_QJe7IY+g&1$q+$&frVSk`}kfJ1DU_@y@1;Vs0nmBK1k)y_X@2#hE*E5ourZs zZ!EktSbKJ@zte3#FDYGDXV2L6oDl+pA(+;2powNT97ecq4LH>DjEt54 z0%xTFw<~t}UCuBNeIYlO67o70YP3~l#X8*pQ6E{FO5Z}FMeY8^uxRa}ZR}||&4vS6 zq1!wB1%lnBL_N>WZtL)y^n2c9{GRAX?sU~Lj6U@>pYfQ9&Oi8Gr}0>mS(9^rA0TYS zhaVDhTb-#NG1F0=+Y4Xu!MgPYR!dI7_Y5Rckk1<|iuP7Fc=ea zmY8A8)zc))nGR6*JEf%Mx(mEWTju8%!A#9P!gABU%- zCZ_&FWlj;Ga)*(+l0i8>mvjSbiA&oU{G7Es z|4ikt#G;PCw{yzv2W+2FZLg3T)p;DjUCUq&Y zeQqCkn@fDf?CU!t{EOegB5WZZGSRN_;842vtSQm!E@zo?*?VX5XbHDf@rUO%J0VEl z60oB`Y^7pGvBFd&eIs{kk~dkP$4xCdCn~&sIzLTTs30(sm7I~FcrATIWv?xI-SQ75 zxO^;Q5uS5t4ML?dYixpw@Mc=2mbAyBX64{^ z9(T+Pb3C-Ym)G1C+TCYUQdB|=&o^B!y}Dq&jzhR-LLYtRqxUjCM(%Oy_rg>*v5%iH zP*+bT_hOKfZi-Ag_$K3{>lB z1CVAN#=AI0*|{~3zFjeGpoy?{w7fFTrEf}g@k8r4vwXW;vus5k-m5Xs#mAy{+!RpY z!}6&~%6@rj>ioJ%VikV*1tSmLkC`_68P2ioxCrhfCVc1~MCc<{_3Z|(lhS#(s{BKj z^cho+aNnYZBw)NNTt?N`6?0wBeq9XuR_!oNGVqjaLAh+vDmV_NK3`E`$93Cbe5_YY z^<(&L4v2pqi-ymH;^jRb=@on6760;mwca!0+q9ONWY71isXr!-07N2IckVicqj>U=eLG?Ft%mTwYO!`*&%O1ozq| z?c;g1QJ?d=HyiM7A5DMx-@Ev#;x@Z8XzB$akMiv@Q z0*W{Fp_q|XrpBm1k8p)6qvYixHBf$3T|RhD8t6w!+YOt~P{*sGmcCH88iZbPe*fyg zxi&wtapOguqtVH$FV09N7~Ul@6df=w(&ew>cQl<7Lr#iI)V~8Buyh zp>)jyl}~SRajL!WzyL&nwP#z2x3tH^m6_2&9S&EA-GDc)7PvH3k_rmR>jr^6ZlJ8X z$4bdcojds zOPi(9CBM4GE$L~C^=zclnj>BRZ<$J2Ymh{v?n{uBT+vfxP%)mNRGfF>4dWT(ZZA+l zuM1`3kOjUcbhZx@@>5Gq6`CB(!sFZf;_lSCE!hx#qg5)?FmiGL^5i zn96tlJV1R8e{!DbVF_8fTV1x&xn3a}6k5Mr)A7$z=K-NU#WCAa_-F~R&Hi>041!F zJx9rj1L<_((_wZ1@&StoYzQzZFA+I2Hzs8XGio3( z4#VQ+rrVimpvVluWAr{#Fed6K?MlrokVI?Qe3#ioR(ujDbmf;i7P%sXfShW9@==Q` z3&U8X{lGK)FtL9vMWV8ALPl32)^-CN!LuGzGfG0Q?h@oZuPnl;5~_Bb@jfEbq0E(Z zFzccqG9WYI5OqyINKXPi5*X?io?SOq)IGBl4aJ8xdS{wp&OcSKA&A3C-P{i~>O12l za+7EZnW8T93TbJAQBOB9qTMF_>disnW&$}$dHC7oozN(w&T-&1GL_z=m!N&VvgpQ- zpJXYF+x1EwsPmOE9%tVhla_xj?Zu)Pb4D}+m|qy(R2iW6G9W)D-r;XhKF-K4A;C}nBQB$e2lXsyED_0 zohyR0@L05Su~DKV0v3<5B8iz@z(7LM2B!2eCpl4}rN-|*xsNM>s&&qnq=l?p0j0(q zko7O+-)qZ-0b&=iA7AE-Jm8F62X3#bK{o+g9-#)b&w4;1z+t-b!4gVnl-+;(Wp}dt zJgOr4O7QEG@;uN+cPT|slsG(O%o27Bf2WjmaG|q$t!;Av^J>7A86B3GyYXDCjGU1H zLwt}f!8rZ3z=@j~4H4-Y3w?3CAI{@Ajofrf`@nV@mq7Gy3Vf@>m+6|82_5UsSVIge zELA)h+<`j8#H?9cvD>??gyz3J9F%rf&(#VMpB9=UiMKx2!m|~#0#EBOn^*o_YmPyn zv5+G=JN29LjSHx(Uv)3Er(%;>hdG_#t3Z#|0b*}HZkpaVQ0f8CF)%5XGSt-ucL+J9MDrMc_6F4-!V6CGzAo{Q! zSTcs8(&{xecz4-NMlh$`+17eM&gNFn(qaP5H_6nPxP_wRT#(`!PUZ3<$pb~@#)$^ zU9X0fAggZ#nMzeLssd`Is=(2L?fZve$nj3?wq5u>jO>iG)u!YnAa~5_xRN87I`BwG zWqzu^A^N^Umz|1NYgT%0PAC1O&#^Faihy$#bKk&0F0Oi+D#S>fHUZ^zk~nY>XJ}kr z&dG!*;>f;OE2mX|$_nF#kKlAc%!n1x?&1hkU^Xg?S*?~RO1&bFtjVP195cf0^Rnah z{^ZElr8yUN<3@mvlEanme|l~H$=$Yg-(EXJ^l97n{7sBLVf_lWu|OTstZ%`lxMAFF zBFBt!lB~64uwZzu|A@X?FI!23_|TOHsgY_2m(l1{EuX}%tot(xm%B2Oeg9)CGK zi_d+@C!*7Zg2Fzu*s`?^JeyLdMF$GdhA)O8HT0C|(e6N2jE zI##`>tBzm1PLHM!BmZnwg7hLji*);LFF1f|JT0Y-N&2Jbe*97 zS>@%XyFm#xp!n0|e1G_)l74?Iu=t>MUWI-q|Lts@as~=9Z(2YUR!S2RFWW$nGF(ihU_6F+D8a{w3|O|1cZt9ddM{pu_IOKK*N?|&UI`3 zcN_4YV8ks*Ur%iH4soh$F_^B$ASm6NZi>W>Egf1utCvO508d~DBe4GCdpm8>)F`9> zsO|Z*47$5S2R2@o{oOV`bw)~bR=gO%A(I$A`l{GdSL&oSz}TKM(M>d{=~y?>f11|$#UT+SZgm0hrU6MSQ6EXV97#4+OW;jb+9t19$4 zIP`aO@C~PGW%;L<*z%3Dg!sq7?fLpsCnf1~75mke|G38A64+z(`o}>KYyRC4rq4J) zS1@#+=5L4f6_aFm%uV0NXZ~g%gA@MwF;{0#06YhXDc!ofF6+Jr_(d7{uzh`x4xji+ z^}A#FzmxO7lk@#<_#e1){$K@;0Pwr$pNBHz*J$*DTM+a7&8dj`^~w32RTD|E@Sf0p zZ{p}zy0O`^tpk6t7bL|&dlo4Pa&LNCZuocpgy%nLFx1oC=k+`M-03fu$k((9gFd}h z_`5|+dhq4)MgT2^r`)ZOCv0kCpDU3;!EuG0gNOwPApX}7{9ye5e`Y07;)H^WvE76Y zyAoFl|FhgACCpN0G}J=5&%^LT3GGmu%_i|RY;*pvZ2#L-X2y&rlS}&>6RiXsB%};R z4jqAW<;#F1TERe?TQ%PO)neo7k!{BX!qfNVG!V3ju8tE#NmTP!SM{?N8N+{EgO^&e znb129;7qOU4_&bDaO<|D3dR+0(X2fehj}Azf~j%Mw<$~T{N|zlgB8-@%Jq`s^oO z2=NZYdR#L3^_M{x3*L>!ZD%gOOAL{lihy0*?a*LlUsCMjS8tdNB=JHXVm-JF>X%A( z8r5A|sIYf4d6xa_IS10*V>7Yw9ZgAnUbnh|HA&uAVZC4~4W0ZDKm_u=&Y;v1QEGGx z{9j1TDzcvalAlk$z_3qWD}K}FH0EmwyUs)Y#nR%pX`*4yPLM+GT%M6dkM@jWKNh9GvUf3y^}*PJ%7|9O< zb~s{!T83l1rR``h0+w3fn#g*Ge=&%`Et*5|+Qf#B-79`JK<3n)WOl$AJSmrK(Kisd z6;K9&Zr?ZGbX}$5`^^(_)I=uF+SAWCDsIPwaD^4M!H`|MUr}$IGlC>PdPRNCVEp2P z-+sL&x5ap(!;Nz(rB>x0EB2IWy>Z@%rg5Q&CiABaMPAe^oo{dbdM;n@ipV3je?WSV z1pOY|2u|*+$Q^29zeoIsqT-KE@iWK78pi?$pP0qO7Vs62CV%)A#XvtgeM5zhe|^{?F32-ArD)SVxF!rr@2G)hm1ZeZnSTG!ni`y%O7kT{qg{p zyu#_G&r(}FS3)k-qL2D4{nu0Z8#;ufEV-p4d19AJC-2$LsS2!DVdk&D=d?}JLDJMF zCcoG4S$&600=Rixb#(h@)N7kes*X6y=d?^n8gzU^RsQ)nOnZY_n2On#^Y3QQ#tqD#ui>HoX&Yb|vbDx&d4g^V&z}5x0GFB2yfnEAiW8hWfqv|U zYCD8^k`#UJf{s%;V^0TSs`*Vie!UCigImZovCo0sZb7=l4=;3VH7m6C?AR!c?EAeE zi8`S8^a~Qf8{7h944?BW`oFC5nt~Qmp6~ghIndQ!oRz2NmCMWYmoW8G3wU~C?kW;z zQq@}N-wtHDPuEPdi zA&Wv1p%^tl&!Yd?bTwIfPTn(Gp(*)xY%HwsP4HO&Iw+sUzu*J=c<}kKtr{sP_rVG+H;eEv=t(_VK<9=-lmgW?Iu@0K62?otd!B4sQwW1qr`cQ!GZKZFFhb>kj(lH z{OLjL0+W8}BFB^mUJEl^+R)_1@HR-1N5wlCzcH zv6xFOz{%iM@d1@A8fq;E;au^cA7Gys)Ayk5uUPa~ z1fP=Pt^Rh49kZJ%-}HTx-lH=((t+6WS2*^WV{TB3cCJ9W(_2yL@{^K!BQH3|U8D5- zlHOf`vJWx0*wW^Jy+;=o(7RCSQx(H9ZjsyW#C)vh#d77AQ0prWHxcnwR% zw{83Ly3yC40Qdr5(B}i;ps|`u7D_hTA(Cw$NA&k)-yV_9#~q4Y9X1)?N%<{S0NV$*v z;@fYh4i%$1Bz=0HJpaX;f`YI0fJMR_)*<7Tnxx(<1lDw zR*=7eVRLFLytm+@P&_dZ9=0$HxDgpm#}&D63ax$|vA1kS^)4;)(Yy|hE~il=u}bbr zfD)_%tAF&2;Zn$H(!{viq;x;_Q+g^im_1TJx^$J~GpsYGawp-4R9c4~LuQk##=-wM z-Z{X5D^CI8-LvDPblV=93zglfR{MLz>u-TX{5g(Gf=b>ED$+%*g30T>kAFk2WW64l zL@@~-LGM2c;WMkOg0KrAKO8J9hlcqsO(~ThL~OXS+}g9msVQS89*w9=8`BEXCK z8=RFskJ!0BKcJFbED+SPx{V6Y=;P*9(N|XFyrb#o*(*8^75e=LiI8|Eq1DQTPdJ$DeS1ln(v?LvnES>*Zh)z2u}lID z4^me?Y0aVXWBdbJeT$0yVojQwt3{07IF_6|TdUAlI9{0YgJVvKdlTUcaW zsMwDRp!qHh9#|1uB#jtJ!ALdHTNx- zw57&TaSu$!OmSK4`n)Ht&E|spLhy8-B2?<~zx}i=d?L*=t?G0p~FF>J0q( zt*R^*XuY1xoyp9aam|HVMhE(P=3 zrhcL;U}aKK2eNHOv5UDB0!mLGvW@C0$*MCx6>|Rt1k-Nk)uz9J1n3WrR!W9>F4dp< z7i=VvL@`;=8wMCp$?V}x$BytgOPMz5Rwzb|hVfx%LOn-NQwwi>EIsC5@LVmDe&Ai; zf_6M8Me?ct-gRzk& zR!Sz{jR8T!iceRDipm)=M^YhN0@$&%L*qeVqpX_ThnAOPs|)gx#2LozKq4H+i?3+u z%VD^4l(Z`XJ3lQ(>)Ej~p!r~G;nbenC`rKN(DPIY4?WD_9Lj~_VnI9PVlD3U?4!eEQ z{^I=W6v;%6&u^?pr8TPvh(^6jo8HQ}leFP?HWBkor3+Ho30W0cz-bBbh0KeOg*sfR zW@zqem*zo^BnjKU3WctOlG_WnQI~anCU2lRIr^?AUlS=h=ZBL8)=eRlaLBuMgjBaV zJ8EDgxe_4&W7<+I0F22&-Uw0k+iY|B-eH=!=4U}An=Wzw#kc&P%l!>K_L31Di2S&< z&~2@zr+pf@2Fcd#I{T89=EPgvDdAG2v=qK@9n~fORqEqzmPY!q+_cHZpc#Tmv~H)J zXH8!ja$9>kj$P;oC``MUVN#u2TLu4k!=fqVQ9_9i(bD``v0o|D(t18L6Lty2u`iaF zY5iRz5l(*7m8w)2h;EB4c7z7Z%90Iv|2~M>f0V(CS;s0H@dI;-J-acun^* z?+C&+&^f25pW-YeZc*v`+`2vF(fXCO$mbS+00p!p^Nr6Y-DlTyQaX127x!YoR1Fj0 zGtI?^tu0*Xu1Svpiu6{*Jv?METa}&tJr4E~rcRTfShVNC33Ex--a8x>8?sPIh=AC1 zmwPDRHn#K+%ol|8XjbA{aPr zRT5hnQ|;e+mhbr}KM-d%LGzMri#cT~UUtIf^K$RowEG#T#(Pi=sOQL6r$%?QDDuL@ z%Dh(n`-8oAfu0TEfNK2ofc9Qh^qzRA;PLT>FDY&W`X)>8Q7~g-eEv{TRY%(*;oNLG zPx~A^WA|(=`_i10gdE0j*2joFVruHXO8b)YQ{2^IG4-g_BiVCf)=LamU;mLux!@DP zJQpJrp^tN7a7q{SI`5_JWkAhsUp!d!guoGe9GJM@erDoQ`E-~S`UBUH@cAj5nGf~( z1e?p$hUUu2l{0#>R`pK4xf#^rBo7$Ej#0&j;SQsG=Y~I6NaDwy(WB;n@`Efn>`B~n z%n^?1`DCIxN@I8LNM5nMjf$;rrvh^}% zWzXTeQ)GS~z5Z~mi~K4q{NdGp8P5J_au}j(ej3gENITjf8jKV80ap!W>xcR4n z;*V||W9GJhy?)HPyZ-eobNmV(_-{?{-69a0e*T;){44(PokarNna`is7nT1434I?s z;I=U9y8Z7ipz|sJzqJeg1>p1JG3$O08X}hj%(iF+lVU=y4g3^t`tgfnuR~Jus~2Jx zh-jVoN{U}sj~B)2R`!DFx%$dG9LxU&d-dT&ivud~?a^giT_bjdi9CL-*&j5+DHa@X zy!@|s;J-jbpxacA2K4YNcw86adGY9{UVX*ZnSn4Na7ExZ75!d>>QD_>mWOy9HC!zB z8y%>Q)A65%`vTHs8EDP&W_b*?WXqajlm|Lu$8Ta1TdHC>rkFKr4gzFKLUE%RxjgOe ze77X5a7cfFq4kX};gvH*dAe(m-V1QOWmJj5{<8^2&6-)~d(Zn_q<6RM9zo%>vs4Ll zpd}JlQGKEsGH`9Bm#+~pMD0+40~$+K%rCHy<;NFgT_MY&x2})!5x*$J9Sj-Vsb6r?3>J7 zN7O2QcIlquaK_SmdFA=7-AmBl1-z>)8FOuEHIVj$yoRlIOEz?rW7x>2?qd&dJ;VoTVVfC-5mrFKj8tr-fScy+giCo3nn zFVkC9H4Jw|2JyKvu?swdbjrvi#N0QfuXyS=lQKNdRSr}_2zIFxs_xC^$_@bme0qX`AKpH~%+DDj2Rxmw{vaE3>b5scEnUoO9YpQ)bcwAx%peinI zsblu>Zpy4784HuIAHk$7LCd0eYJynKr?owR^UF#Oi52=YwpC^zARHSl$Wqk(id%BC8Y+k*i)yOeci_j))g4o}*q2 zu9?ECGNvL{Y7+vSttwGb*pkp7jUBAq#jvuBkD!^B3%%DZiiL0{&DHHGeZD}ujcVtd za&in`L`7$CaUsf=rXcOI`uhxsdxq0FwCXfA6o|zx0~HyfTsorUgvXzbC3*wkUjBfs zZqanX=?3Xh<+tSU!72tuV4EuvTsBcX+(-AvpD^l8gpc=vCQoqqQkNGfMwj31S*U$2 z7-h_05O|$v+;27JMx?^ z6}T#+e=5n#>s?61CcP)C2Mvt`II?}Dch41hZakb0*by9Q3A@K)@v$lqgjf{nSP^AHiq`4ZUi0xXg4QqP>l$B_M%snTdsa z&niF$8Fnjy6J_MSLlc8(XE{5PXgea#_>lDJ@xXpgmCVkujB0mhmGwdU=0IHKDA*+w zlB{d}zGTxKP0_5vY`8New(X*f>6&5b3n(k=c(%<&yl`9A9xDqowa{7(sv_RKiuZ&u zl%GoTq!v9O-@HaRsUl-hzp|1XHC+EkZ{cx`d89(B9e2xMYGCaMFGlc5lF_*+z35Y_ zJU3H?!qn?o);SYD=5~H?;yX-k#l1rqj<(rFc%gar+Hg2xGkP&4kq{KPRwrD3Fl6+? zXyQEmc0aNZYf?brnF6UZ`BZH&z`ukB8PV#b)(0XF4}CSRz2jtkR@^i#%Xfws z#m`IEd-#?){7Wgz*mdh7+O~Z>Mp2<7R##g3`0p=PqpZ>rOe(UBCeqOemxu(J-lO0m zC6BAQRYQS-Og-;uV@TWVQ3L9WE|ALcjxWjJLsg_enpr{aR1lnG$?|1UIKAf=JgUiL z98%gf69XIOR&vP7$c!;~o`#FOcACA?0!*Jz)>nD76nobqb-#ECJNwn8f&3AS>{+HmFSr_Eokp} zb}F+GAC&w$w*;9v@Z1VXmTl|kbqQk&IP`FTd#J%^j?irp8N7mys)s?;^6232>s?F# z5g0gUE`^vwvK#X|J1ghvNRb>wYDPh;(ejPdZQ{MUaLBV;y`Js2^C^Av!ZSi^UZEB` zxPir{xxfqhl$(s6NiCh3pC=O9d{sDKC&29^XwV3di>?wJFbak1SRo z&^EdBUP#l*(&6}O$E%q`v%*ZB_Ie3worZZ@4)Q#lEsNQ5P2}oTD3(>RYyB8y%J6V9 zi^%rm6FVgXXO#FJRD3oa(GOHZWIVOV*qrx%k1)%%%ty6S)CXjru*0|%OSM3G8sefn?{_1<&}J?|;Rj3=-i`#tmg(qmJqCX};Sb~E$FTF2^5 z#g{h|EsF^!(^3cg>lHK+DvnupVnba&=zXitDAfOLsfk2YQ495=t5HjT?*%?ALp*Lekh*SUvAg1H5Z)$q#g2 zUBR#k1=QuC`EJ`3VTX}dmdjS=#(<>a8WY1*jStA#@SK)X^nGnk^xiU))8A(k&m~a` z4Rz;xF+kvV&e>9|6v?mb9IKb0rMk*2i5_#tsS{;h1+`ow$S^q31zNr^7{0IDO_&Vk z&yds2(960DBj=8%!y_hNriB z?W#$6UiYCKuXS9*$8A|}b$>cc+}W|pEs(-ATm%pGa|_zYAU_|;c#;dxrYYpNe2N~D zEAdU{r`r4#lM36%|@61mr&WBI6^)XyYqbd;2Aee@O!Q^O?GfC`+#!T)LAN9$kOT3wqn2QW|{-(E^V1fskB=l?dho$-{0D2(VX5=F}PnF z!hTe~h$eAN_=ms@)Pb|CiwCTSDu&bLaL9^ho4eGkYiFcDWdi`rl9q34fOgdoky^-Z z3#rAI_jTfK8sjD*9QmO&aEPFn0bH|f?+l0^>l+yvMA+zO$X28VbmK)&_bmy@Tzswb zw$$u2t7d75*HBmoCoImbZz`$hfl9ljUZn;(cGO!&TiJ--MH{rt6^cD5&UljLZ?`u< zLFk!Y4@5)--}M&D9lV4KwQc1I4*+xp*-3AX+J~G|ULt$~Iv2GxUN49u8X~*l%UpqN zo1NlTYbggyT>uA>1mRz^u5q009)ZPGQpzFV9Nac9@Wd-t!aS80jR9XI@m##h0SZ89 zyN8O79s}CC-%^#|Ox9O!2TgHd{|MPt}kWC z{AF53t#JZE&Qgn@nRi@5Ygp)r7up)MXI3^|0SYyBJ#)|17)j+Bye6A=v;M7#p)-A= z>F&E;ZENq^do^&Po1b2$cY%-9qqO#RB6It7D|q!MNeVMFY6GEe$DEZL!`aeuq&iq# z=p8$Tp3_fprWe>SrsY%m6RNuM&_~uXgmgVKb<8oUn+y+ty z-$QLG#>@>;dq@zPyprV*njHojPy6SWdji!Xyj$NL3XqvFtnFF>K>kGk`_W1YVX>>q zs}gff^bB*pcDgHM$+ts|R-QX)Akf2LN#` zeW|i8#yCM@`oVoXn2*yr4a#%M*2DwO8JU>Y);_3K%Z<(G_;g=#jaNtvrzCdv*~*58 zv<-K-en?q@;`0=``hiSRnf2`Cl3<>Ed;2=$0;j9uqljWw%6}*?vBsZ2AY8|BwC>F4 z>jA6PMF`(R=Qy3f0Yg7&56~&MeBWaKRl_&g?uPXhQP{mH7CQ>(MW>@$~a(FdB_ zVbuZ*+ZeGIH{8>}ke;A?Rdk8_&dSfWD>~0*t{8}tva;p0=j$#qIQ@Xg{b0skNUg-L zeq&nU6n)&j9SB&_vP(#S71~=sB$Q ztLXtjc=ws_K_5N!@goBpXk9td6NFy^w1X<-;0rz0 zu$)ITy`Zx*&?+w$UG}VSqSG&qD*r5``t}+tB0Ue76*=z(0H6#6h1@rOzs2^?{OWJM zMTz^hkz*&EzRwsaX84gO4rDsSWq##le|_4|LW2Lv4-zMJl$@h@s-G=iS3vm*RX~IW zN3`vQzf%x?GGcj@07$l-&E6&P#l(u0neCUYfyR<4K>a@V=6q~Imy`S|8By^3uJPH1 zuoR%=_BQNs{G=Jy93sI3E%hNhC{C}Hu2%GpPcLBhu8T|e?q7arp(-IlywKJ@95+%_ zKO52(z+@tHSp;~*h@S@%RV*?2B`FCTTc$iQG;PWL^U_PL=1+b4I0`+(+~P=GCB9j4 z<*o@%GY#Rr`avyFv4)qs+Tf9pTF(Z}NOo;A$MZgj4z(wtznq6*j5DbrRas3I`9_Td zOK@mXa=5+=;&;BkIwxpzteZkPnpouZPH=7cdE{izkzN$Ji>f?Mn-J}pY(l*l>RC=g z*&#z?#zsFilujNI7_d9NRgqHQ898J`=WuwgX)+h6r2<;laM#I1AVPmi(r)4`cRTW!tVv zz)2xIT&N;NPkj{OsH=PSfER)>dLUd=WIUI^Wp1PdSR1`n@rienn-tdsI#tx?`Z8v|@A)Kb{;+YuNuUBSHy!EMOw#EKk=p@z&dZsaRM)RQ!i|I`L zEaQ{G#^n+$Diz9SA7?XSe(80-@{ZFq7EM37goE6h+YTk#azWWU#=GrGo+R2fv6PTO z13(40#JKXTHqVKP!+bFj5QpE|Uf8+U-+qXxDH6HU+YRH=9uwf0fiv}j9Ozfdox(EU}4Q1B7;jtePF5Y=k z=tZd51)ubwdeU#`68;f&RATi}F<+l*jcTz|PPjG?uPSVS`( zY6u{4HLl}5uJ+FIz4hD)5(9RXkcT%(-Mc21BEsw9_1cExG>FW#2CO&z3dm!=cNNC( zUAFH@d}Z&IHzVF@_QbAJi&fzb zzOm+99*+e!$_t%@J%m~5m{Usb!!3$KTN|T=5!)<$m$bFSTh?_~eirk)xaBdSS_AR)QO(2!=~uKw{_59MuEy?XT@l;T!;*WuTY8~Dbf5^ghn&y>vyWm9 z^RGOs^>i}5L|3vERc!D7hx`QO0Sq?UXER^}^(&!LO(m3gyl@J)*=&eVQ^nxTX?&It z$b-pU(kx*`xx=~@$Qsy3lic2%1|G5xZ8*qo#~C?e9MHyi_s@{6ttD$*j>An&Kd;cR zR$$)Fx?I6rBZI@hT(T1>=m6_m7K6`tv@LF|yKFz4evS_X>z2W>CHMgYUj9_36vG1c zi3D=PczU!!gUNA)+6^wC?U!+siK0oHX8c`vY&@qJcfu#Mq_nW+!N=mNJjL~lLy9gh zYzs@Yha6?a>?7nd7>UWx-sYByiG-D?wnmthG`fyW$hdH8AE^z^;&$~4YMG4Ww0{tZ z>+(eoITArD;#q;(*3u}R!@s=Yb2jSD9c{t$mClYI&ffW8Rn_|g{`^Wms%S59%%M)7 zC?-R2Y3+V?7PrdplrA%pPA!UG*eO}4eAYtBukYn6do>?NOw(<9!YFo+_fplHh==7J zMc0EmS8E)tLI0-1K2^JHl9?2nBCPPR5^-L$GV?6(7vk_M`-WF}+pg6fk{&#-9=d$H zD!EM)2fX$31tzFCJ$NKsjbxc~XVSSBSW6-aco=5P|A)&$= zdjFV}QFPR1yadappMdUph%nKQ?bX47pKrcAlVI$f@v zJ$J!|;6J|G9{&io^Z%l*Z8B&m2kNHBn6zmuMsjTz`u0WQu&;gl)Cj^_4utfgE0lZKz%I@0lP5He&_1G11j4(6D+)BCxkGBdPb>ruU-Hp*pNmI39j6;}3gSU$bO@qBn;WNUc}(1Y>a_Ef zAml20lTn{LstQ`-Jt=S88t=kexq?czY(cSQH|vrnS=VEb*g)aLF3&HA0&P9;#kw(_8hp$0OZX+Q?qD((iopMHlG8k>fJ`0@pDd6JGy zOgr+7Kf()|ObDfOZQaHR)>^<0|0&9xh1~24IoG-+5BVX3#dq^Ue3s;%TY1zVua7JO z?F)iUMyE|WkBs}QennZv18+=6=U+*Ts;dxyT;4Hx7GpKngAanSpp{6GlK}Nf0!n%`b5VCU)~DQ+(!~@RPcUXHVaPe&Frw7)a)$caPW)pb1yJVbQdg zglg+cIVYE#Ez2z{-^2mRtU#RqMPq^aDv<>zEsFEq)5B}6n-Hk>cLrOG`)TEi>ZcaQ zTV3Q_ssnmeroUai)%ZC*SW#av2D+H=cw}=%vbmdV_B;RCFlz6Yt7Ofl4EMHSV{Jum z!it?Kk?Qwl55q*=W`jgb1D7V)nC+1PCX5;efPa5po82^XH@EhfcL$_THZ!)O)OT^ z%k=$P$K~fky*hlj8{}KX!a6A}R>69NHtgg16x~*?&jy-YErCjea} z>=}j`Wq9iL!jx8i%Og%Pc-|iSd3DXZ4;sgsPF%>$9m%N(11*sl;WzU^OVhB3Qg`|se)yc z3ObW9eE5KY#k%Y>=JQjNE}%xj%(i&d(Jqv(V?8~RZsc7BT@j19dY=?xbEoR#Wm$Aa zC#wJ5C)C8lfkT2uT$)hF&PIpb43%S`F|7@5&7Z$(P2AS;h+YIGvkIUtQVeX5TWmqe z?&2;@4dD_>KLswkDTXz((`;l{Z|ppndp+n8c_nc8b6LJtuPU9HyZvV70ImmJs|YD6 zv$iw5QtJ>FzSH=+G{Ux+m&b51Pq}&&j+%!EYU85HCz)xcB~{Cvx5+TY+m4mOHdo1& zL;2-Rmk#dW)0|i1PniRq1JyR)G0~%(%%R;2X1IP7;M44;=?b5PZHsCon+!@>mJ=#_ z;)LhUg}dSlGpr0cT}p2!Gdhf2V5ui?tp}Q1-f4*uOjGpFkgp1pz`s^r+Bv|a zbYuI=a6~HuDkey5dZp9u6)Y>mQ83_ELjKd!Y)cy1n6Z5Go{juH2G+9$(PEQ5me zx}TB`>>A*kZ!|6_;S5?arJHQo|9t$g;CvmnJGFKFOQWuA^IZ3tMB<{et%NyIt!#9F z(Rcvec8rQYaAC~s5#Jj!wt6(j(%W+D{Zxpf-%`pq3-NwQwr;uJooB+PVS9P(DAS{< z@Ce5~A6XpUtEP^wgYQwZx2&=E?UtE#-SzTN2bZR@(@KYf*QBpN--`#)lnNPhr)c8= zD>*oi=FS=IB3sxSY<|gL|1ipF>8!A-^}}R!=itXe5JGIR7eQleBwt&RI`iOk*(W3K zPyE8BkoKeI?M|YBfn=dXMTg+WV(JH8sGqdocNzHzpp^-!TO$$^1LL4dQDD-5iXp|2m*bqU>N+-%haoWV>!J3Vj_R8 zx52PnAFQNbmJlODximFkBwdno17pt^Ip9@DlUw*qYKT)Ssy8iPPiP!f(k7=^MH`{w z(y?CyK3A4)|EhTOs0*<<)uz)ZF8n;Li9K~?k*jHs(LRhsx->v#_i6&SS?P@kjn=hQ zDvIoaT19?(Xp`Mh|gq_5h|O9&JBZaORbwMqZ0Zu%sV#Q0@T*YJ+1cU64a*9v1Zf-!Z{9?$@XW}I z_kQqL$X*tXxtZAAdp)5#qZ4>+IU({*Zct)uUpbczx(R#M&*VQT^n9b8EMM#FYVBA~ z&{c0gYElY%pN-gh>Dea|g4?AZO)R6wQ9gcU#t52q;9I_`%cC5L7LdMt=Q+%fGxVBk zS+{vfmXyHHCq2|jswgz-2=w&zBMqN0Ok z4QR)!ps3KB+Gw}S$))MxxFUSHxTo#9WGBTvW<#L3WpM-9hI#}1l0u=Bear=oaT>D! zO^ya5Pz?UhLVLvP(Y=pBWF$t*ox(qdVQFl}2ntl=Nx4AM8B`L7t5pujos+lqJMw zVaHk&6z78vE4o0{!$UlquArcbAJSbcZr&j07ju*l-&lE&5?pn!;o{J?&1nvIm7U*R z(qL5E_VEh`G>2V#(dV>ThO4n(A5Kxj`jnb=GfMk+xkO!|mM$iIB<7mRl43i((6NWL z?arHvByB{zK(Q)@1Ql2T+sF>k*VInd`m{b@h^+d;w2qVeRjBR9dRdhlU_+0_ba~F^ zmk$Ke8CsOWLrlSGfO+5c1aA%#7+#ESavV4n+-?ZP;sMs&c=h5IZ&@7HZk?_a2mYwh zi}Dgn3^esvX>?A@Y&-c%<^gC&sU`Ti+B>Kh%E?RX z*WAwBjhL3v&dgX`KF~y1%$vQ)cy0HBX-yr9q4ZbWel+_jBW2u3W=?atqDM}1C%U2b zdcd@Hr*DJAT-@Lm%?~nA?)-fT6X>RfzPZG1Q+fUf+G0dIuzF%Pv@M@vj}tJ%-;l+X zNnQJ}pGRD|?`YMMiD@b7q|#Y(%9V0k%JD=6JGAgXH%}}0-%PkA2v3lw`;K0!tzNQC zFj^dhW#H%PrSj5>>3j{{Ebjth1-)1+>FzEE*EON?9S zOb0!XmBC4SjYCh$1o?G`-gT*-b@si;HQztCdiCqwSDUa;xfQ!w78*(>BQ{EzcoXWH zmUc#Ds~Mi(CVtV_@)VbPle`#wuHjTjJJWv`Td|ES#U}KXV^F{M>IOp1<+M9`mC3cW zXQUzw&S&|Gi9Zaa(3JaUHHJWKJmKj;XBB5L_4*2Ew-kHn;1HQ(H`4}F6cjj+OvS0s zTTn{pQO@`>tC}}0wo=A;;Z^^likrl`Rx;v4WSK=l_N5y87tY#P|2s@s&?!mV)Y~Yy z-62Pr06O<^AlzDEJsrqT&ZSI~MELkw$bMMuQEj5*?3{mlQ$evU6mo5db_GhT+_U}2 zL3Tvk(E%$c)|(zXH49NT&Am;zm5D^n50O5HOCDtcLE5`2QIG`p$ZsGgGoK)XNQCUP z-~c|uZfUrv*j~YxGGTIp-3n;oi{3pa7+))RCWWfDM<_}c-_{U~nCydzJ{5b_tVn$} z^A>p_&CF6`+(HgRimeMPARzoi%?p%mG~S~Ml5G(UGVdDNkPrQW>DRCU5`@SwsFR0n z{A(*DQs#z$Gq?8U{(QUFUNN4n*OGnRCwAvzm|-+<{`5FStyPVdw?5xbXm71HmBrOE zTpVy#&_tgG2_u<5Hup7|cd?EW0h)?0EtW4 zwd8#kS6}4DKHAsMI0G5;Kpd?QlY#WFvwFK_Bxr&&oVU#NTajHVne;kH*)BI=w47M5 zWkghC9*20fH0FUC#XhD_i?Q!I8XM$3Oyoa-8 z6ZW{Eq2GiZeO2d8pneqAZ*t|jjm-5sDT2V|t6h;DQDWjZq{3KjNxW$c@pvBGZkk9z zFFk_Ig=*}M@WM65gF{$?{whVEJL-GAiQ7O z>zgPEkIkF<-#MWli-}K!tnbOYG_h=zDgcSosJCKjyC1^atB!UR>>G)-#V8)KfI_{W z$2pYurRyYt(--KraRFohl6K(Tsx-<693#yIB+K{tM0rNTaHLqbQ@)7`d6f;68wIJ* zom7ohchP{pz0fJJ(HohbkX~30@nqN~q3t3$QoFTbq-1=t+1yG&;rP&@-np<2a-?+A zL`PFWVa)K#xrwOn@8XHy%&)z=v+uPz*i$h*19w|h(Zm*7Ve ztExd2E7bxPj&i_!pra{#X*VL#Xs#WJ|v|5f5yQs~TxDYdt=wNn0z)zx88dya*3p#8UfDcx{quWScd zN&KP2=Ll1Zz{N0|WVTv%%8oluPmN@=q4dZW0xRaDl*Hzfqw^P|CwaEuTELpUTi6I& zK8muku$!r{nCRV^E2r4+R6nESmz4S~g`i5k;X?CXo!>nn^q%=Kdq7K_c4z_boj3x_ z{#JSKU-%wh+q}wtZW7#}chxj}v&K%J?02|rm+3tFET4`Sr$ejwbXk`<_j-Qc*G<5E zx@0Upuj*vM;D*&168Y#i1HQJZ^;epWPsXgNl7k0by-!TCz1*aWU3YVH%XM)1j(18Q1uohw34~w}rNf@6U!Ak(xxwA}n;lAD@u%vxtz~PP*MRJl6Sr%4 z#nqf#Di&jfcNw>C2>>dIYWAXJ#>^T&un|ERWDVOXbIvEyC+ya}V;QE60K>G)rono- znFGmHA2NhAH}ISta!fXf61RyPK-}NGrk#%fJUc@t)$0eI%`2v2&!AJOdcWUBimv?b zNM$yO^VJJqEEvoecouZVS|LAj?7VWR zjjcoOlp)svL;YeSRjp!GSR6Os?R!E=i6Er!#@yy)J9h`o`V)DG^9B!^&;QxjY3JF&%wWb8fn(IU=53w%t-6uAqqPY zsh7S6?zK_k7zMqxPI%@&;IK$Ilw_x0FK>ok(Je5BTqwa zrMIToZH~)3pM(`J4xoh7K2S}+2JZVhC6x5&zl_&AvrqEHo?SNu36EsTJL2UFsvRW= zBUVAnRR<;^dwB~mWuUE$uBeDNo9@O;GTS@$(Icl&ERL-Lyn^60G}#{l8q+?8J7f<= z8yx1TGuep1@lp78{`Pg#J2T^Nz>MWP-|#u1mW7z71H_y)+)s~iikc>v5?Wthboi}j zuOK091=tIRq#E40zpw6#*=OKxAlv`zr=@qqSQd7ApB5%FP$&C2hmCZwp29n%I!>lK z>H#C_eyyFW7>l9i{2`j057gt@8@aV<9;_?x z-~V!{X@4bXaCFeh<2Rn&p-C48?OZ^g-LBW3_1%GB(TJ4&pFW;_(_m2IeixZFnoC>d z9V~(j_g!Kj>>~94=PNimwn^wD_uhHAEPbK-z$U#|9Nv%#y~DbNEdc?i!Sk^C^$Q@u z5%WkgUi7qWQSlps37ku52?`zwn21`8a|iC9ICBIm0OsL_UoTycY;?;OtdLor;Xi(V zrHXa?zx_|QuO>(ssOsdgRPJr~Y!#vhvt$&iW^Mw?qSlk)CGeJ8efjsP#QmG$_}EE= zg>EFKX=@v;raWDG?988+> zU}vXmnKonVdG`A!YPNLmWQ{<`FBk7eH+3S|{N8nCPz;muJHlHBY~m6rJzsPtY&WgOzlc%D4T# zE@Z>OjmTFo$j%YybbP9XPraD$gaa#!@O^(&+aG%K*H8O=tkiCCALPczsoL7osLB94 z8+HW)M?X?tHbqNPJswHCf1)Cc#1-7WJgCtQJQOojSNboZsP8!^Jzea;n{PlOWx z)TcjL#rOCLYGkbCI<_rh%nx8AU~Tw6?uQn8segBzcmPro9ev>&2 zYv^wfcz<83o9b!V1RY>GFgu_o;6~edzd_Fzm{V|~o%AwkrHrA@pm%aDDmjTe(k~Zb znM`pC7`l8PKz9?gBWfcz{zKinBZ4vD7vPI?HwwRQ>$9brqSlFSsJ5?dR;zxjZ3HF!&pY1j%{ux%(?N-jZiKSCv|H#U{|NZ zM3=vWJmi+sVNad6@);%2Ct|9Zc{6l0y^2wGeG^+q@M3)d{d zO0C}n#e1MQfI-7YF&P%ZASzy&8?6?6N;wU1e{5H*tU$S}JXb&u zX*jFZ9J0tmU+JlHv)l2E9AJSpA=O@S!nTsRUK%UCvX*V`~RdC#qxFB=C9-mufzQegB z)BlAlFmccbZCCPFK9ASkU7RMbO`e;D<9Xlf{yb?iIbyW;i|`1YIVu8kER3}{^Te@h zN#3kr^SKbp)`#Fs)HJ^VW6x~UPTg_nN^h-Ss-#X9FR1pYb1}fQfu5%)Glcz#E6gaT ziz}ZSx0YW}-7CMcB#brcc_a3!yn4oy&Jgt`z;Yyceteha*Z@jBF*1ZF{GA9?rWqTYSwG3;)Y_cS6OWtELqagkMX#pKdRZEsc z4_HDI$*#%=6MJ~=V+*i;fCvq~dD zQI3?QkB`r@vGJ`9hQes>8O>sPH9bT;E@LjD>im2$p8A$2F>&JK(|yi35c~IOLtKz9 zfurTl=K9s)O_|yVRPzQtCa%(`Tr({jm|b^;T-|3rwwTVmKJNHILU-j;>!deKFV9%j zWT(l}=sV*b5!t}#nB_lgS};(8LfB*w)y*1Z@EsSO$}PRQ~yxETkzox4f_p>8RO}%S=WOE+g*(o53={Dop0!xKXb2n4*Ua+np~#;ONZ~ z8g(=6yw#ZB#G9_7eHLS?dkx)3%8xsSk8L)bPV-oOOTP`-8;aYh8q@CH{05#mxk$p6 z^SlAGkBTqGO6P2G&++>Fifn9@a#GS?`4eZd?OZLKQZga#&D6Q^g zw7U2=&EGU^?e?3jY$e$>7qPY@sU5tUq44Nz3WCiDVQsH|@Jikx!O(52Hhn*2frCd(1&w)1m=kXH1Drt3k5)FRnWmeGzO^FlEUHgt)XRu}bJ@c7U~Npcj3gPFo;p1Y z_5>pw*U%x$!Sy&1bvh zg51(_Mso7pW*elRjy}saM?fF1GRIq`$nEa9)S&4%ZVx6g_NB=P+sQJz9mcZ}wh<0 zj=g3nI_@_P|Eap{)fqifshGCuC*h@@G`DkC%q|<|mKG>WO>?1>6Bpr%U6vLBgiugf zf-?q{B|pTW%K2Rql7B|MAQ=BS>IH=yKlqy`Sx;u`9_;V}u9bd6%_aSBOX0W1z?p*d zLkzMYgF_SVFC$S<((#>5vE;v%q85M)O(ZS)jl$4~inumDOrGS`%+)!*w%Qj|QiwG@b) zukO97to@}{^CLC6YqPiy?cf~WFN2XE-~Vqa%8z@{yF*%2GyhSRJ9{=_tx|Vg_OBx; zz>}=ysQ*oR`Ty_*J*{BngxCJAyPsGqDFn?=S;FG?hbZ>10@;s8_HW|Ik16?Yy3da{ z;NL_l7VPmI`~Mfc?|-=AH{>rL*U+2a z^|-a1l)!%U$YUL9YBOrAL!a8I=jra$-E!}^@Ca?|6%_7SQ^@d$LPI*XC6^uXW+fR) z-afL2xA8kn|7&gT;kNlB?I~yU#x**#Q%<5`S>5bSJu)WOmdX@+7*|z)!+z%KQSaX6 zH2F4`S3RhtxYz~79;f|Q&%MXwlAjh`>`ihx!DBSPx8iNSR(7K&HAaX_)7<6rl-j-i z>n0Y}`S^F{ z{MU+{P5o6aqMp$n6 z7?_`!G42k-yvtHVo89WFcZZo@dYc-Jjulpw?p;0qGX5=J^sT;`HZ_9*vI3;{+4v5r zVa>-mQ4>%H@iSrY4ljlZDZO`R&QL0+jm+ADYcn;Tk*cJ=_$^v(-Zt$&QWrl3rToci zQ(MAca-SQj9XY5?j!PDXwJhY@J&H|HFmY+Z*$i_iJ$L?Kk|Pemv?Lax_|JHG^`g(2 zczcM#Fz^o?RF{rt%{{N(V2Lmc(kehe1{vn&Gak~{-Wg}2@kl7F8Rg&d%?fuju_y~_ zuJ&A{*?aKt9fM4CR_**ISVh`EX!@W<;hTULPR9McW~6lqZ~4skOx?_} z-0^&~&>L;xUAPkz>D zW>5wzb>=fia*`SuPxznxQsmT|*!3_QVz18z(l5r{vgnc{9+;ae&BHI>I%I8T93=K` zlQyOR6dz;r1)jIBKL=SGYq|4?-zRYxjnoA=%FB}%HAZSc-7Ui#=Z9Ppy5F~NWLbukN{TZmL6UQN3Kp3s-&uZlgN~>!p z`uQ=AmebI-USqX<1mw^Mo!-G(@oD;oJK2M40g)l&=GQKGIkTad2F}%QqtuzDW|Bd5 zL^}J9qn=bh>X436m59$&2wy)EgY0C3|D5%~7#8S=yOJ{2*H0~<4u5qQ25G&mDrna~ z;rA#OW?~&3GCq{=GAXJ4-b()1=LyB<&b|`3=|MYR4o0yRCzx6 zrf|?-S;^^=pBk(`mfJ8z{Z1)$Bcl?Ht2s(!--K{?BnlaWyuSp2? zq8jJ9Ma!*jd|gf#zBD+HWV(zrGF5ZGIKDf6@V)$7Sf_rX?3J`{8_S(MPj2cbSm)#) zj*A;2!9Fbso4wdg-P4B*w6^s<37;`D?ZzntWrpbW!NePMipok1us4u5MsJzQdfGUc z8|R^7XXauX>Yu_Qgdoi$xm042)wbldgMaE+Sljf_2k7<)N7ZRz9bJZSlo(jX`PUFP^b1U$x1iDqHzTwL`;pmH~tPr|V+rH(EB!k)}?v`X>i(vDbNzni9HFo}Rg zaZal3s#nW3e1HE)kNcuw5#`}-_lb%2QI+lU!9upJ=TDuLy=SX9n&oRN#Fsvn#PoE;uYo1r?i;|8td`v+$3 z;T@`^Gg{A5&rwbWa5%bT1$ccLHZE(u!pInY+54AgNZOeVR!pN4*(rz)og5{8(TL>o zygXCB&Sx;k%=!!}2^mX!c??^7+?<1KsMDCtMHWGKFRKJO1pwOVJ+LH39hu&AzS_}}Wc&IUSmF{0m z4+ydONf1MRp(&{3^PLN5)jNCS&YyQ5Qa0_=5O3$%Nwk`Ubc!uEYfKg&@~>^asx1o# zTCIsYzU3m{7PdPBYx0-iLo0c)Ee-AV{G{Dko|}r*Ywo7H;O)_es;(6KIhWoxKk#h} z!r~2(QOm{Bxp%8d=1d*0FjwU>jJi8zKLF&fOJ)Ua%L_$BI_OtIb|BFqRRswHsbB`g zcD_?HquqTjl(FT`D&ZF+DN7no&FO(Vy*(?AqR|u`MFI`M4eC*~QEZuMiv`9`9ZD zRtLr%@TsZ9{3~S}5jA}p(dJcSjnI6jt79;~P;uKFL0ElXFoB4=s6k&EPW3sC%Qs}p zMnzb<_?N#bp&yV}XWA<81o#07%m7~3KrhaqEGWhiEmwUvNrNQ%g`Xzwxr`cJ@p~6l zEe$j#9O3%DXFnJI`4}{A59#eET;6d3((2(3dx^+t)kV2JS)pI(Wsfvz^R`pTkA z|I&PtF3%!04l8%Dwo6iw0;{@Dh3ep^VMnX0nX_9>K&jfZp9Jh5v8BBl3$q2PXt(Lm zdrbcDvV3*^!egb%NTO@!TdzU8>*Iab(U{%hXaTlUYg$aWA+-fk zff~B1QXaKa)K#C_Jmb5W&c_G7>z-!+q^LKyTPKwD&a}LGrlcbN` z&?)u0hugs|!>RmyvV@ApgIcS6mCXitJz=d%{raYY<$9jm48T*mQ}iiO`FhTMXhNQ4 zU5V6^qxZamWgXq{^hD`}tAw~ldFr4R^9wD;ia(p>)bc~({Sl%6o!Df_mQk!_G-lLC z8Xfqid|QUFXQ27Ca4_1TG|w|UfHvVy(7u!=4603u`o58|J@iMGY9EP$Y}?n)WrCL( zbtOEnalAq?Us(j@{?bP!T1-k(Aq^BtLB_-ojzu2*@I-vRCDpr%5x`g&iX!;K=vj8m zn{ihX_4=y8>UWzW)auHPAT`6W1a%KaW`Le+?vv;Latp00tM4evZL^P427Z<#m$hq{ zuV$vJ2LXs^Ua*DGzkzLxYi&9YD%o`h>MHM5Q3;aPJ)im8yNvm4RRksSo~&LfChKZO zV*y+C&yx6`}zLB`+o^Xe_YS*WVwzMN(Ygo>!Jhm!50Bqk$mf|qmh@>}ed0^ywtvTot{EydQIc9cyorCLSX;44csL=?|fWW^~U8fC0JG*)Y4eFx}PVWKV zV7I&&-Q+62W~uov8rnJeO#3dpb{a2_P#vkO4B>u$RDY7`%Si?80sfU+W6>3&JnY%T zXvu?TCsLKdJ&ukiHFm2{eW_$QZT?!V+4afo0K&o<@5iOIwQ!YW)^T#UwV=*j2FgKY zwXOWM=7l`ZvGa4}%XfY60BNUmA!hqt-pk3ClrlV=heuLNNe@o}f$r|FXWY3$GYpdb zu}J6%`I}pzh?WD%SCvYZePexiZ+-fr`i&)muKoQ7ZpPxpvRzT~B?pbp-@lg3`nko~ zBhwMXSP%GA^Zi}V;OTakA0Hs~^S`wi}1w=>D2eX}1My7rQGt;-so_oFly~u`Jt=t1T&vI}VZIzr+O{*)j zinjvY?c)_t2P(uw3N8HmVp?ANhIF$dFlFatfnVwSzoj28Hw}^$zE|Y1E>){JxIOO@ z>~p+b-|p0MoO6YObs%1-+~27(&;F6NOGQgVL}g1)ct-bUUQ_?}>qaHWebyG@`Kd6w zq{VZ>LAElOu{=}QWZfgmjN$Nnx$P}HZVK*N2P`Vteo3FD-x^J+V+0k0lM4%Ym52?A z1j&E$XkdHG3j>$9pur>M9_P;RHOCGcd5;`Gl^#$GIA2=_WC1P)9sscM&#XMyV#$De z{in%(x%2g3zdB(f!ViU;kV&$QD`2{v{LS1pFKw||iUj-oLXXxbr|yy}?r!}id~C@V zLq#xaTTaZXJ9YJbp*LZ#6)G|$9m*wfTSv2Mk3cbsQ^V2>*%@QM7_iK!SHxFRfwVd-z>#YBc7Rf~1%Rq! zY`?}9(=X6h9YHxOi)CfKiy?fP)lLwLMPJ55jBum%%|z)F)FaBP=o$ADl^b?nr1zM@ zhO{ri&Yjq}GLwgL>bS0W%i4W-{^6(85}C_%yma}vQjV}P-g%;ptS<|1IM)PgO3{sf zym?|Xrs@beZ!{eba>8MRo6RwHnY3*Y&L&m}bm&XWsDkve@1SnEYv03*n}leq{)3Oe z%hPGsddbW<=zt(-+cEX`-LX@CoN1Z&9Y&XaIQ;TP8+ZXA_+h)iDk+gI{ceO4%0m@z zPzTJMWm^R+73}?sor=r2Pue+2>U4Tl+ZRFV8#Ry61@rD@+4|w)-a1z=w^dt{VdoC$ zQd9*A@21Wu-@r8!*0)7=8{>CdUi%WZgIDK6n%*8e{e?bL*=$(_2lJH}ux`oc{5O;_ z;UHl(r})n`Y>T2G%sfBMRR(#%xV5;isPRg9ubMcYG^b#Mm(z7yxq}w=sjL0hG0R00 zMXTOP$?B4zsPIsKuF4JMKBY<`#@>=lRTVT>Mj)x5Xxli3o{%-#z*@6*G0^`b4C#>I zv0Bjc2Tj~|tkeoAUqP{3b*V5O4pk(9i74~A4`=B08z{OpR2=`@ieD^pMm?6c%)jh? z8o{TDH|krElZjAsM<$MQAjozB11&GA_85q1J==)b@Yc+dRq-kQuxe>ZoiaXZ@h*T< z&9%T!V;1O6#Ug|Dt*>wQCo2Wufb&KTaU)yU8dvuRKKZwc%e3lZkFZ4c375CJt6ort z;Cq(r{CM{ZYA1}O3XnqDWgz(ALax^1gu+j|KASi~HChF=rNCg{KIibUe`;2HgE9`6 zTRfAbfBDjn*M1h5zx;#f+%4uEm0>Q^5C_=gtcU^t@`}*F#oF*2U zaV6QU_G>n$y@LHo>7b=K2e`yR*%wxGK=m~*^##iG8nwv~Ux?98GI603R!@}~9G5H* zj;2P zElWW2E*#M!pt;zx0<^)Bj$>nE1tfo)8ZWFoCVPvVKG%%Y`6gR3-w{qwj<8m-T!ad& z=^>L zD*RF7bD%&Lej2$VXqzfcP4$!M?CFiYCZjJB$fe0?RR)+LK#*MUocVTv{<+Q3&)@MR zpf9&Zc-hHf#wHdY5t=cwEcnOt-SR;emP_v=XROy<>C!&8l3yKa@nKaA{izq*llwH5Pgk}K36&C5@Prl+kE5BTydnm=fF~8C|zH-fA zL=Rapl0#X?rmFGJqk=S1eirtWu*1M>`mOAX9`rtYpLvCeT$vIn>w)vrJyxs10TVQ_ zV+sOCO$V#mljL3AZjaa=af?0Fk`e6lH){Yahpeqws zs)X}f;;`>{N}N132oG2!=;#297J5to_Xqmf=uh~(JRDM8csn{By(on8Zl9N;Sh`eF zMmDg`vH!SR%}v`JUG9bG1?;(G*{^Re)WDh9s=^qZc`m`n9N7c9hM1mf)v`lwzH{@R zUKAj$tSJE)dn4T^zRL^K;Gh9=687GXq7iCyN}c-D)F4cN-ZwxeH&-%23{L^RC92!=*R#!Wu&%M*sufscWV3mUKkNn^ zXpnb-s-sq>>h!GT@;j9SjjHs4QQU7!HnE!5I@~XHOU2MQPw@jX^FL2OpxIj;?h-f+ zChXj#8J>ax?Y-AoX>5$KAZ2ZUb|q`1(Z1ZMe)^d z88l{90KlSvvsAEYR8?2~J|VWXzIXE4=v8Cr_mZo>oZPS9Wr;$UWl5>2lw^JN&dKuC zyN$B$=)+lQ9z&2lwV2EiY11;?{I~Np#J3f>fwAS=#w&sLjfK_{rvNbeos9;q;Fr$C zge{5NN44)~0-BHB-@btpfw;xSzxyACCbH*c4FI-s^NtGgw1A?E;P$7yn6Wmw7gq8P z5(L>59?gRCz?gFD_tgV~{K(P||8pD8tkJuVnA)4Af2QtkmivX#=Rf^!w3lqQ)z#J=0W8jS zGQ*ct@d45YPu*9*x`B7>9qU!D7QNMMkR=?wnp0XtT4*hDsE~YNEl|DdavP&1s zDLGd*Ge?bAS?)GXnUtEiqJJ8Bi80qbE%}58f5eb(^HC}WtbKDca7s8tu`;mENa=hd z9N_}|x)KXU!&ZB~#00({UaVxo19deDGmOOu`;BDA_5tsAsSRvx!apX(cX^Gm zS`AF50x+b{t^j0f$+0%)u?LqjIfVnW4XYyxoF>?czTT z+QcTwL|C43()W-zNNv1-FF2oxjxSivJ^gU;?lsolpd*`b83&}-Ry?(|Q&rBORkB-W ze(OB=$K$oOQ%92(5i=^CzF2c2<)k$mn<|F&1ds7!>?>5$Xbr|yUVd*>-N3RckblIq zxIt}cY#&4$_LREv<%8l<@N|rA*RAO$wo_dHFpYRaA=0Po-J4L9N&4%DT-z8KEqhpT z`6f*+&7wPenyJ1_TfFy6^k5^%*&Y(qzO^54)O)go17K5J7V?a6eR6$ivA^B-$duj~Fp#b~-i z$4a%yk7~Fk>+mxo;yzALhmwY7ywjugDPM}uJbvBCT{uSS2VNTP=SCx1k{i{hDpq#L zU_nlHQR8&FWbkKqHa3sm3ujLooM~YdB@EVvQyxQz;MhQlPvfHo02uFg3@|U;3aR`j znE0a{?=Oqe&9Y}%_drjy_1#mqty^Oc;@>*tsLB?SBGLRjG7opWBI^ng1!a+gOd1{IZ|vk zxZo-;@?@_KUuc&<6d5|Jdk3QN~V<(TD0m z+gtN;O#`ErR9Nvb$KMr~znodgQYr5Szi@6tG_=Omxl8hKP_?O`l=hzr&3+Ev>Yr7u zKT}twtrR|Ub4m=@mHrbHnUK5@ac#@*E?W|(y8j^_bkiD*+d=&it^-5r2v(aIg}+ZS zWPce$>cJn8FH64#A!eq~-vlcgAzot_y$gSM)v>NE>~E!SCnmfm3(ftWQCE0EzXy2$ z1_T+3myUn0J*&-vKjgKOko?1jND?Ti|8O(mX6(f-}!d%vFg|2sIk zay}5IhYo?NLW{QK+qJ~|#8a8ksvsNbMVcF*F<|?sbUlYW zXPZ(o5comN?`ssTL7jL+ha#ShHK-2FvL&PkD7$^z0=O(4qbszTn0+(SxGuCkdp5Mu?P+nU7_8ImcM{zGV_r9Vo*Ve z_A{48#}i$fdM>iT0h0x*Cvha6C>~IBa&{{i%FK-|12u|GS6|r^3tk-iP&HGGZ@T)K zIp2c^MYSsBxq(KymdhCd%hMl*PqIDn3?l_ORRm^pSXw%mr>w3}Y86S#k0!ED&Qc(J zs`@Djmwm+N)ZKqaz+%vaJ)ZbSGL<$689^8I)q0txtBz3vovfH~b(_S&|GfK%Qc#Y( zJ4MmulUt(y@MRWe>$Rdy-aESlyA;w~=H}+wu8z@Uj}%kyCdIj+Vq09}cPbu!0deq! zdh~7k7CYP-ypqa-ZDaajIkz~eh0Yeam&2v2FH!@-q+R%Qs+)V7v2!O;ju{pu_-77q zbC*jd8pB?m0G|aA*gNt_mXix3%8L3OqDk@klvx# z{H4~GUrLhA(EhMYl6-fi(fMou<4Ot6cSgrj_1IH@o#5Q->nMBekUZyW&`Tp%{vY<< z1RBcz|Nqyj2#F$VE2S(Ih8QX#QOFv`Qpvt=8HOTDW$C^vLdG((49306|fhUDx}1U+?AhdcGb{w}a!}vk(B} zG8qYM?#Z$wg(ZIEG|m;3M2s3`iu)In>;i*tzg(6!c2<+1qT4&JAv^NDpNW>b7!Mas z*Z|an%#rop`1LX*l$N;sA?VVXbV=i#B=SId04o?2TBTd*)Mb~HHOcXXi9GMH>${}M zlrD8;R#!+s;=a?O-JMI@at)eb);X&~;R5*4z+hy6b`@0cIgtX=@|s^2{ZY(&YihN~ z#ZC;5S+*}JcHk`tLHh+RNSi8>^fm9}*;oAkBA&=rCw4?D=yGLTz2+^7=bbTco37Vt zcj)#K9aUtkf?mP<5StY0c1}NvngsmFguD;7`v*&S2X42xQ!&Fs{;J&GpTxHRLgQ%8 z_@uKb9e!VA7wOe&a5wi|VtDAM_-VB^fws1Hf=xd2Jn8@4hp ztXNc^6FV8~+Ll`$7uPc}mWP=oyt)@y3`&tatM9A#IadG(R#Knabdv}qyB0#7E+slb z_)|~zZ`qX zqlG4N8Vw+;ltyr(DV`wcr=(x9fbEOio;c}X*EdM;#QA6IC_STi72aiI<#&bNDvDfG zJlJk5%@;=S5ALt;6u;s=t<+;!Zt)~8C+S;H0Pz6`2sM7N5}PXlZvkvd%r&o#(yIn$6C#R}8x5oy&1$Ht(OB$8>(mRa-pgGQ$)no56Vqd%&ed-Wbm= z)?>r+NF90=5u-g6KyqSxHbaap`6Lv1;#o{tlHI~vqE%-n7Bk!)=iz{z_+v4B3RO*v`aDSYUTyKE_)oogGYfA7q;>-J%0jOWAS%iM&cJ}auhrh>)uziNbQvWL&D=GBmzVI%ZgHtXZq~7IBQmV zPN)8%qT*@*^OCOrj&@~WC?2hp^Lm3h%|>e zlGpt|82>(k#WS^iu}Fje@!@NsZ9sFx#q{N;pg;H#~C{g*4?gb7>SWN)02d2dv%1SUUU{n0odY5=|elvn+YCZ3P&DDK)fR)z#p{Otpog>D56+Q;BCe8!bfYi`e}V$tf#wlayL9P>G8 zJ1qtyuBUE9yP~7f(LN8cNK~u6ntH|ypUSa$^YSFiC&6`-0K~Qv0IiIAQr>3yi-SPC z@e5As_O5To=4CEoydC zfGRG?y};zP(;|`NycI?5T7Ml04(W?3gSYpfl{ia-w#c#s`Sg)8O`OMw6@tt2!oy`p zbydzIgSSTFRT1rGE?+!SqKKm0{PulOoMGrXt5S+z%jlo?6+%GxVVZpWBW-1XhnsH7 z>h@Sc2*}LeW7bKa&dyF16=yc#{dcD2R2>eJx8~aN3*E3qJk*PWq8y5pqqhy$CZ+8X=PF&~$%b(931QSdJHKTH zHzoW)c%fw9}7KDDm6^@$jRC}pdInM3q~$Fl?}co!ie z(Imsl=9YEet9y(nT5-*%Ig(qT&7eSj=$19dqcyx{+P=DYM=jzAiH1*rXe7ba7iq!w4|IqpAm4@{R1#b!pm}A2m*G~RE%9u-CC@$sr3S(yfq`W* zj671+@7k(d;kRcH+k}@Q92dl>O1{nv?rr}zv-qxRDr4m##{i=!-dN&nCsx$7q^?A` zFuamrljL#IxD4CE(Z*qBx?0tqNm8tu5#=HfX5~T7QlzJSKnZ$|;!7eV_@uSO=pJ!Xgw!&G_WbLMsN>Xu-a&e~BLo`19f__lSj8()#_8a7W9<_rZ@7(#6fY@@^UTbpIFzEK#(b~Z-rK%+yZ>rv)$y$fhJc#P% z=JAxTV1*8~ut9m(hiJ+{0o@L)<0Jg!%;bZWw$>WOrB=3k#O6=6e%`Cz^54e#zxB2Z zC{JR)dXmDfLA^kLbwy!t(0gf7uI62!=c!+7WUrF8vru-oPcvN@vg9m1+Cc2`G)}fM zHO+66elzmy!dn30RfAsg5x899BVja^=WS!}f0kB1G1iqfoa8P%kbck!QMPHXKV+-@v6RP#aL{Y9ZCsL^*#yG=%ZTm>GZP zO_(eh01!*Hu8;!ZBE=J9g-?VvJu?&T6JnhE(ngI;{o3uprcAROd(GoffFjAUw`%oV z3GE$sg8uZ1udICO;h<)u1u3-Bz!p;lwB>oPByf>Q3Z_Qe&&fJDbPXc?(FnZN{QMLD zLhOTRj~9?o z>6IuNh2h$woNSAuQX7hw7q9CZK&n+VnYRT}nN+-b(6ZG;P~heLgvTkq*rtK61hWFL zme-AkGFY+6^R%g*z+ zG^1+UwZZ%;8&@-XxoKbfXb*LN`g;p28|#7h`u<9R+5O#nLALzMV|u~T12z>w?=1H- zWwa6?5Kd0*9=d9gj>Jp*ecL-F>vXNDzML8%cLFjnwD$0HeVxZ17rFP^CP0^ZKfo(z z`=-lzKOgdItx|;a>gm_dBbYNCmKGF3ch7u1H&;Yk=>yU$XEOC$^*|@61IlhY$kf+A zP}0;82?h{TiVc6H0{K4dtb>XrKOt{{mYAAd8%Yu#Uwggr7F1QulP~%cY$7**j4*b} zoTzWvo7tCJ*&S@QLX-6(Zm-OO!i3vBph&L=L$;}TYSX7s{*TMaeI^4-I`^!6k*OJs z86WN^ND)Sl`>GpZ%HumqeIvr%l9IiW!{cx@JaQL?0T6h5u~cW68jQ#TMFeG69_&En zN1N+PXPa4bTv$2-UzUvXQBSW+w+E|~w5wT9GvdmyOP=t8f;>rkCzw{{nX@_PYl)uB z8tOv9@p6!!%~j&G!jJwBp!Lna<5}00vXqAQBkKYE+`4!L9g@tSOzMn*@X59tP^Ici z{Sbz+xatm%vlKl>K3RC7yyfR(FIsc6x4=T!mV6Vrqy=`dK25=1NxIQed~3e`2vvFM zd7&6C?JJiRXRoape9^2gp=p|?58o$dJnko?{iy2jF1EjI)0;Q<3^?mRRcdi6!>tXD?i=SVq_7?yPmw!~!1m}0RAtGit}*drW3_!+F#3;JN&dX3jfe({fcrZa zlTtfI^TB>7ZwqJz0Cch$(xoJ zSUU z)@Gx(Xy`t>rv6FB7J!s2(l=}B>B|L-uknX)9qtX3O=uudk3|;xUF55#y~O`ol|PXR z_Z8`Ux(H(dIM6KNCmrkq+Ydzm`%IeKw?NiE#j zCnA~E1UM?HLmwXqN(YPG?j&!M7!? z^5op_ha{T#9ke&RxM+&o{pRh(3GZScz+B@+;{B|DL8rz#`EoB#EiFxIidWnKNc2Kz z1%)=~fuyS7pBGO5HiKYvr2d(PjM;AY%>{)u?C*~QG@76;Z}$*v3kmYp~}St{qT zSjaEydcTT`q3!7(x_BCI(b||#v?{=Ma82Fc5eeV+kw3Sh&v9|L^^e%77+gF`V zR$5X5_{xBjYpLpsNh5O8lWqsC%CHYAVCDD7M6Ti{;gAB;QKzkFuF= zgJij>LO_bQ|Er=oZ-!QtJM$zb7oZ>OsTI>Jj3)1p_Zwf*7Ty?pvr)lw%|AhoKx3b; ziyG0VY_LbYto`R+G=CPRQEX4iaP6eUQ7?@P<^O_foD)$?j0HK9eo!UJU#6DcW&&F>jqVAOJ_NxpvO-R_fgx#^*5(dtb-v-`wdW=%(~ zFypPzX>C5*Z>z?VTxZ~yNY_h!OT~1(1;c{zyen1dBpS)m-lJB9tKu!O<49pe@nA_B zw$?c|IWX1wq1+58py1Mggfu2i&(v8B$3MdLh3doH#%8|HPau^x@{JA@D*6-D@(*i)< zEM=us@DjAGSXV782u&#-Voj*_z69up?lWUKx(M7+@_1lNh{4t4@Z@O|6-W@ui!{7?Orn z%_RB(&3SZ9`bTs!Av!)~-iG3d%}3w`@EkPKLCK0R@+3cmurqm_=h`$aQx;J^<=MI^ zQD#>vh8eGhZjCxwb$m|>ZCxvBh^-+hZ~#f5Rk?e2*kyz}Z8N+4NW9GZP=7s@wv8b< z&yv36>)B1LF#73rVcMn@SKeznOeI@l1a#xmu^t1JR7ZX^zEJc^g7;*yfX}NSZGJRX z*8@#%&M9$GFF-h_6KAiTYoj@xk<~e1``deX4!-giLYFvUy-|~6%lW)gi8%txE7aG4 zLZHA}WgbyJ>!avgJ8Zk!D1fL$01uNrt7ckFqmi~v*SF1GsMC`9G5hy)HnLI4@g|l>=d9+yaxO$B+)Bp^8T3 zgb+NG%&c*{@^)yMfNdy^X=)GDn9(oMqmaRk#qi| z!iwbs4Ffb0njou+o@xf>A^s85r6EArOggJ|YT6*p~N zw4iTjnm9-&KtRnmyn*p3y0$Gs3&y?Dhg!U5#;vV+Ey-ivTylAsJ+kHeg%av@myc`l;M)#dszt$Up zFmiVWUhP@Sxw(VZEfp(hl_;`m6ZgwK7x@m-s(seUnj@t;^BHju2`)(jdV{9&Oq&#z zzdAb$Ok#S+I@}dMqTssTQp>2Svh33d~ zIC7S1>gO2SLl!ryE!PV(DM`Ud>20~M2nTunGzn$nMBPdJx#FJIC4jp`*A^in%^IU0 zI2q(*jLz0-ZAbat+DT#})oT0rQ9^JSu8eN!E?4Z7-;bN5{Ao6J5PMgp{>8zXAb2zgT)IPBgSwl6MehYI2t+G;*HQnYD}#;MCb#h*PpO9NBF)x3J)#GgLG98r~-OrEVn4wmij(QB%mtVSrxh0UXKr{5C?@Q#B;WMWVh0ANbWpza z%Gc^KV1NwsNY5B9l&`iv9SEIr4}{Y~lW^0q?Idq5*fsAd7vj!@#6!?hnt0G+N~`;ME5RQKgZe9>CxL>RK%pktEhpg*`*6`n4S6Mtcvt*xglRh>;$;qgWRQ~-KF=S%^`gQ zn(3-f;UeSv{pPlBzPobz8pu!D#1$8HYWjigmj6?@PQUxC3eByo9N(U7u0LpHvig#f zDlq-*`iaGE>2qd(m$Up&{R#Zj8N+MDS@4u-12eCKY4E$`z0Tj61e#_z&xh|WDNfNg zijJYGEa7*qQ%^$fqk_$Jof#NDnW?zKD759N)#C`j6!KX~BFM6w$o%AfOj}D<7nJUs z|L2|+6rvxv_6)eFI9|!XrxzQKi}H+7L|v-bUGC^X*}-aEaajWWKVT)~JNJ%Hrw$(T z;4s5IW?C5-jw&j$EnN_w6 zz@)>ozTjn&d4`?8juAt*T6%5CW? z-p#$Wc(Mb48%B=mnL{&ZFk(9>h#9IMd-$Bap0pWs+*gBd5C6`KrqA^a2dip3_cxsC zUVAj-I0!}9ITD@sMmcdWJk+Q!|9a&4^am#gPo+_P_yZA6?W0_nv0J`oyIpRjFCLI! zt3Iar_WocnZ{$xFJ)WPWCY%xPL5!BY6_^)+8g6Dv+_ldc2VL5?=pGyo z1|ky_93F!W<`<&{M%FhrdF7R-ncO#JoMenwnAbSX7pyNL>D1y0 z+SW)R#KY7I_){VlIVisSYP7kKbZE&C$PybU#4$amKFHGGFb>9 z{n-FhUr?lkpDx*spTR~&c30Z59l8wzg$0XyE}k{>Jn$X2EX%9{(62gjp34`Hbk=rf zH0dXtqW|hGMpzSj*rDXjEwloMC6;I33ml|?Yt?2scql7wF>I$Q{g(0rF~gvpn!C#C zg3s`+`@{j9vn-R!`0=y8go5pBzk1g`kthS&Wez=R-GZx10%k~kt5vgv*NYY z{H!>6H~Nho;2q-u_$u~VR{R;&?`0FVR|A@d=JxS>l47>3{pPE8s(2t03+47Kg@^xm zgur}Q-q?M<-HMfdKZgZjwcvhkyl#a4S^(0ZCc(ox;XJ>UeuaNTVg_hW-0??I6b{|x0c>9Htav^P z&{R{YjC-Qhb?9xuy)d>I4qA5TCp^JgIM!z1y%{^Pxj7_Y zfb``|1!>xb$qmPH8lku&C1&LsoX3g3r}sA+&}ZBNTi+I8@M7WgXr0e8_Ln6}l~y zAj`w5(!OQ(Lkzy#%#7-j@&bPAYB}67p?b_p!5`C89AK0oq2Ibfj2m#l>FZrp8Wdp& zr<_Dka0rm5x{OEZVZ;APk$2e4`dS5p_T#yObjPv^h(hQeq#fa ze*Xb#8}aM%SNQfr^cN6Jy;K!2Rk{2GnF-)!RoQzN&1f_4;ZV~ohHuc3IE(K%`q2%D zaiho)f0pkVkqZodexlXJA-JzWaP{eMST$X%%fG?fnVz^Eo(BTQI=(P7h$* zm)aDus`@ai7&~6K=XmSEk|#$9cp&X`RvS0<{Nfznw5!x}Bn$s;k*@oOI^I`4PnG>Vg5b_}=SsislBtRw{p>#T zM+Uur1%Garqyozwysud?&+o^teZ6-i2!XGiV~m1P(CsLGyKKMSt#VF{Mm=8ueglxr znQUtBSymtP|U5Cil;O3x8 zy0}xd=aUVZ_Ozc-_HaozO(x6$f1{p&N0&&4^e!#TG77j`*vN3}bwO7b%rN-zgg}zu z%2+6F{+pS7*M}mE6#>Ls0L@crLSdyYt

-{^~-woJbF_V=B(%l{TssHHLjAMz{Ou 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 literal 0 HcmV?d00001 diff --git a/inventory_tools/docs/assets/publish_in_website.PNG b/inventory_tools/docs/assets/publish_in_website.PNG new file mode 100644 index 0000000000000000000000000000000000000000..eaab12102d8a4710dbda27c8d1d7fed3b2f96ebc GIT binary patch literal 108908 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⋘oYpeMl<3VUkw7`zSw88uCt{Uj@N$G~cp3HK@F$ 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 UJK|rTwt3|6@$akue*WkG0J9OdasU7T literal 0 HcmV?d00001 diff --git a/inventory_tools/docs/assets/specification.PNG b/inventory_tools/docs/assets/specification.PNG new file mode 100644 index 0000000000000000000000000000000000000000..595fa2d20db1750dcdf7abb7b29b0de8d6a4507e GIT binary patch literal 45166 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ÐcLamvQf=vm`K?|EqE1@J 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

Geg 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 S!HpL^J@hu;tyP=DPy7S5PgFGk literal 0 HcmV?d00001 diff --git a/inventory_tools/docs/assets/specification_attribute.PNG b/inventory_tools/docs/assets/specification_attribute.PNG new file mode 100644 index 0000000000000000000000000000000000000000..ada1e0d8ba23cdcf732f9b363ec9e2cbe1c657cf GIT binary patch literal 42562 zcmeFZcT^K^+aRncHV{-4Y=8)YKmY}$H|ZS&l_0%|fYbn?BPt-Ebm^f;lioW75$TZ7 zks6fV5+Fc;kYp#|@7d>l&-uRHKlVMld(O@|%FJZuzOVc0?VeyQ4aJKWZeBQX;>1N| zB{}UAC(clvI6?8~+!^2wYu=4Vz@L*4ZAF-r3H(Uo?WN7fIm?z&X_ySxJcwTzvCygq43D>i>VSUA1=u#?FK?5WX`d$)whsFQ1xt(0#5qL zfdAW`(cz-M1S|qr&)@&pu;wd^LIGwc|Lxv~OHo-5cd$uOBc>m-;k7)2gV4DNho)-) z4UY~-=#sDd6mv0tUY)QbifaI;K?5mH{_`s?1)VB%?IPN`bY^$EqFn5s`zG)uqNf9i zAi+(qb^o86cOdRmXK$|F1J~Ebi#D%rjd;@$7_w)$;L~?*-jWgaGOLzw|$ohszWd_)XSz?b=Vb)-*SF| z2jyy4Q@wTrFw!+Gi_B7;OsqW7;W}KHMWeqc>aeL}^@U^DJ~A}=1z)9h;gGE|Qoq@@ zgyfG5W0l0MT}1gi1|g+hM#ScD09b;&qTG^ z-w-5vgYg`6?{JnQ(mazVW;SLCpFsN;*?OwT^Cm0t$e|v=Ab%#zuc2jWdf!*Kx)YHS zw||RWLT~K1Bj2A!C0oW>{c{p&5wyKh!vkV8v^&+?iPuU8!JP)Qw>e-8Nne~Wu? zk?WiS#sOr?SS(82pK1s4M*lwdcRjDffc1!JY)&J+(~p^@<7GC@zM3W|8*wH|BDi%Z z*22o{VdcHCIIx;sOX67kxRodWhWu zN|c>JA(=8bkttF@Fv69$C%8R(y#s}rj#)NU5{MgxwkdT%pP@f|EC_eB*ra2 zbBu?2n=vwVZb3N=!TQ*_S?@L~GkN^Q>*6^5`})&Kj+7 zb{UPQ$*q&kMnuep%{L-Xobd2`*s2RYa7 zBPO=gHLLkN$9CfKKCfmbg4Gx=vUy!0qXq$>MzxHuG=4nrarwhaG`nu{7mV(= zz#@H;cyNW2oH_I_gZBF-wqhztNtyd~t%S*fTO+SnTU=Sz-_^1D>rLlQO|OOm1hL!8gax_N>oZeA_8m zlNe%`4P0Ch0AOd2N`VeLb$!Pr*c?oYeUrp_S~CksNP!B2hVVYsqZ^Yy$lL%?Mh1jx z6Zr72)YaFY&ep+y9!{)%=|3-vf7C{TPTo7q#1qq+ltjOclK_}t&xy$HCaALbCbjj8 zU~Yfv1jYVD!slF{Y!14opb}9C)qJsTovwdU)Fh0u&CGuQWx!fY$WsVQrKM({TX!T` z29@{CdImjOlKG);=PWT;)Q3akp)U@?Cs;!>kjvs*iQZ)q=m(Cd3EYscSZKxXK&!R+ zRaC60Re~E~kag=2DvlAla-;#jdp~q-nxo0tEkFiJ`0(x^cjH& z*^-imU+@jCCRQc9CUgH!^;MdkB#ZY$hWz3}C=nkml^2XLZiA3%Fvhg@O~ICn(hK{D ztbGsdibiz>_0^szIO{F8oQf9*)W}}4bg8oyP7GuEsl8K;r*22I^|opcN2rzNsM@?( zMvVq&Pl?;R`%15<3}aXD53scX(SYRW^ajW3Y^6G5qJLzO!VA3L`1C9ZB8hK8oo+>5 zCkp0w>+1U@QHG-<=m>$X16N6?(gTg>u`2t;Vo^lg9bfHzcy=r>%xwF9(#L%vbE~D- z2{z@9M7LMwHmS>1%`{t`){#t zPJ=f(i<3>$y)o6YyTX`<9MG&cBI9h2ZKv9gl-ApQvz1SpY(lGkm^UmQo`!>hbn2k& zu)vzmIWzxC2eHiI?PQ_SY1vUzy=Z;VV?+OWCz(VQ=V8Yn+>+H8sDKX~H^6 z;@<8SGwCqCu|QT++>U`NR(x}O4RwmFfp9T^hV5UfKOHp|m_7PXB2Mj?HA@7LjfsAl zsyD0Ra!I_gQ8`d0*__{9gQ`5vX$?d6Z9eL0Fg|=TX--lF8c4exBh4JAPV@bSZ>ypKqL$oN){Tr@M zmyp5~;^MrkG6iWZTmA+WYZ(A%jlb9rl2{gd$#M@ltQu~m_Z2WKf{~{~DSa+}*>ysP z`xdQs(W=;PtYCsom&I4Sjkx9~R$`r$%iNc%W6+jM z21HE9i(Knn?<#Y zNGyJ4%2Q&X+9nA}INV!6Wp^n>Kg&gW=(LoSgW9eg_=|nIiyGu+)YiR05Tha9dOVAu z7_6e(^FBzVRUIDO#aXsNS*CH1Go<`-I-b@(ADJtPO|+LyawFiLaVBxCH8G+f%}txI z>832BRfqJ$j-K?;aoo4vi6K;8JqX=&t<&-!>v-o2!a#zTfK*L(T5Z(h^pb3NJ#BAs z=yeUccN0@~Be=+GA+CSkOR|E@qj~H5G-KrhuybS4n8RmbXLFv!Tav09g#MuH8)l>i}&PVG1NsRz&ULnX7EKlYxmp~XWeZiBavE++urm3k zV}~8}+zaIwNvrnrjMbN_wk-n2XlgJaS?)-~3WQJ|_!MiW5?;FYgy-SL4EXAOpWPSP zpT<2X8sUz5r^ub9;A0etQQS0Lb_(U7Yt*iW_YGzn?H2h+JIAg^j3{xW8FuLD7;xP~ zGRB;7HPEjQ9Clj$;x-KT{35C`v4;ExQE2zOLuaGEf-McwE)8N$^!QS(1G+zTXTG(3 zUDpg1IkLV|a~fl^L^IhL^&Q7P1~>yrYA!)dyq9|D2Sp)V^xrD%by9A570x!fr21X2Ev3&z zAVZm&J`f9)cmbzG^HI}YobFuHFmYrrnUhqW^*jYXCUrAal|A93*^>*fT7rWo`m~nb z)<+2i64%r+tV#QY53}<*@!x9o%sf?NhJ|>-hY;XN@Xs0C;S@;)6-b8u>CM>Reh zgN$YEcFggLX{6q$B~dO-#+{yQ@orB6mm%SHzbpY|Clkrgj?&M(s4d08p59}_qhUP+h*|E-eoW?(hhm5GDwHmrPbBt-uj{GvyT*VoIZUL#7i}$$mXyA2`GkP4F}g> zP4E#I_41g`UJ%XTfbLtBYoh9MSnEzkX^K!hUfZV#LcgZ z4>st}3_JyWoJcRG+k<{5rW{a3i%JyZrEp`_c7Boqa6c>BuDa)9b02nU!&Fh7FLP~O zG2SI~wW0UQdMZ#CA$BZt(3z@%>JHbeMa%UwX{&zJuRVu^0QxjgPnaFpLz_K1mS^&d^#qjH4n$ct<#F zUI4B1@!pn)1Z#U^<=Zd`gztKc0OBs77y#=j&He!J`4UY^XU37$U8CwG)-5op+Gs&? zgkGWaPM>uqe@6MzlnQ5uF;9J>m*vn z;8g+ZjwgocQXJXFcns(Pv`kSZSEb&OB+jE(%!dzG`;JFw(b0%y%e*x|8Ld-~ASwf1?%sW1z# zH@FT-eblW%?;E=?+WvHClq%q^2P;7L+LJP*8BR|Ie{SqvCEv;4Pw*n!od0&73WwfB^yD_mv))#B}~pF*E9_kA1o6p^vfE zVIwzvPqOf@iBcv*zIj4;Ydt1%pMI@j?@jPwz%}XX=;aZ$jt4$ zPyJ2(@7ki-togGLO$RKWHM0GnX@?(ITJ<1=Qb26!eJjOO{~+A&{o@zaQ%{ZA>lC-i&HGHIOv(uwQoY7o%yN zY#;pYXdT7W^4oAp3`*nJ3A!Fi3D?kKY_W1lf0`Gv<9)@3ATK}CaE9Y+F}6(VY2cDOl71v#Jo?l5tF(utk=DIr7mCy`t{ndQ zcU-9+p{KwIH?{WmkYxdlJ7fzYSbmLq0FZuiAW^_)ZSpNa+GyM0C9yN#vxeELu`G}+ zFtxV3l>*}&x4a*NlcLAKk+I(oPfcr(t`)n9sS+CSH<#ydx7#}mDI)U;Ldw`2G&a1$ zHg$!gQJ!wcyWxBiV~Zk;q9xEAj^rxSbi!z*{hL>E9$I=Qv0v5D`A-LpUUVvly#3kr zzCR%(w& z-|vAMwcpe<#w4M=A??FB2af(DOltGe*W9=jo3iUEvf&arH>azVG5l20gfQ~iqgsh= znUnrkOFC$0>ZzH$X4ratXv=c^Bk!O>Tw~n`nynJf8d$Hn%{G#M3U4vhY{GEvmBD#C z9|skJtY{X?QEctOLoeKtBp;L%yjgpe$XFs-JOkbneR0-jJ0AHG6hDsO+-?OdrtCsgFfcvPr94Ed^4u@ zkf%0NVl>J7XYXRKf!iM+9O{hpDWvRQ_%nUcQ7;izf43?|c=a?o)-jH>LpQuzRxSD! z@?cNvDDBAaPmWDK)Pf&)X6btJo^GSY(JcN6Lo(Nf40Zl`yDcAB9NaIm#NGz)=>RcC zjqLrg=dcHq<{EWH(8Yv@zEy*fOSd$B-te>--MxT}U)a^Ul~jd!K~c?o)4v$a_Dq%uO!>(XK(+ ztqMH4>PkIZT)$c*HjoKtZFYS{v-+oKa1pT@DRbmFLOAJ{(nGiFrUeD~yHT%@Nw+fZ zXj_z-Uz?$jmDi(82e3^maBXR+no2PbR;nYl2MgJRO8jDfEJmxEpHhCOZR+gE+z7mh zZV>0n;!SKOxCNfi%llII{X=`RAF1%quTLINC`i+uvDe->L@DfGOg7v(cK?zpZ{IheHn!dwA*NgXV%uNu@u>am%6Ft8O**`vpoAF|kghDkD zw&}2XZC5<6(g#UkJ@K-a-h$yC_Ax56JihXmz(@ti@8DS{1(v zkgRWW%~$beT{0&5z<#uV?-^c)Sy<5iY^5i!utj;$XHi7D^e2v?@@&nW9rXUu150tr)&qEY_00j5*jW)czW~Ptxc? zjrNcZFOrBa>7c8AEr$@_(?IwckwTCHMk@j6VnmBQBOgSKA5DD2^85yyz6qH#-@MEH;Ch3`Q48C^Tru=qMF z#(lJ(@e84Uaq3rq|M%ez#NK?an{VxrZ+j)>uq)Vtq?1jI}(c3T{n@XT<(tVZ@*5>+PKDXq?4b8QaXj6wpa5@3Ju9@*ebC~ zq0(gc%zC(m2|2jsi!O0t!jY2Zd^N@NpQ@V>ohR0#&}+iT z@G9nQ@6jFNkm_nuU{myUfbhga_4c1B-^AQ2NuT^kuG^>@Y)WA~F}C0rRIlf-x>b47 zk2|XSjoSL+O~VI$Kw2k^oWdH3dmN0I)HH^OetPb5)_sOYBW2qq!V1tu1ltvt5BYQX zKu&YVeVf^*t$|e9Q-_&0PhM<#Z&Idip%*c+1oaG~a3gUy$-6ipIRr3F)Ed(;ajvLPtm>O<-6p-}!Ak4Nm zbV2d9ZOPn$r*xm=`5)R>KXk2Pzf$Sjli#hQ&V_$8ihI}&2)P#@e_nX2NpcwS^3r%W9TsD2gisHq0C+!NYNuM_{g7F6i6IwqTM7G zMeo;#Z~F8OA*C2^G=81dZ)nPr;~(|oPX1TX0Sv3{=Wgv4xodUz)S&S$b#XKYPNGUN zkuR)cr}T1dUxT9WpIk7z0ceB%&Q@w<;)0;a&hPn-;azJUda&-A7uWm!ds~Odb=Vi2>iiXkgdvC@!RPfh@=f9e4 z{>{kM8H_=HNI;^h3ws@LGSuh~o%>z|B9{#!l@mtcJOo79ujATC!gnAa|N33)JNN#w zBCs@C570mN#*RZreUQ?e#z_f9|FGp+uNzp#2fvj7SKYK1;0FN-ARPA}Oc3{?fHlUU zV)>1q-bus@-i{7b;_!LbN+Ge0WJQbGf7Kn#sRpM_J}bt>=a^hlPCs-v4oc=<_XBN) zP`+sQ`4mQ4?j9&H!rfEk%ZcI(TC8f zrPxF!VHy|(676lrHOc@T)Gu8|)(cTB3ijq1>n$FX@Xl|Q0hF24lS zRP4Y4K**z}$$r79^p3i{23U5{Kyy?d=28{67qDTy00Zw$yE$vF;Os1?r^U&&8IU=U zwqUO_Neew$cjd)y|69 zs~AEL12CjZt#VaQyw+GMkTes}08x{Nq3Yb3;=3U>FYwsY&wT$S0U_ur{68A!vBX`g zFm%kKUyXLGFy%WuF;6bV(>MH)_iIg;<3Xq9@Eo6ETM0KCn@T6)5_W6b zjMxPF#?;Lv)OfbW*)qorsX_O>$#OC8vp*OmNqfq3*R84Mh3_5sE}Z&X_P%sZ4Te$yGF&tXJp+vnpoK5wRTr3|mXvI-_Yklo)#eOyLvlM6?S_y5nA#Mi`za3-|l?} z;Pg)Befrmn#$T909!kEOR*)RxP~|QD$Lr#bYEv~Vbmxao@HRfbOR3s5xA(1lW|CWb zqZ~Syhjrvt!+)vx4b6PhsWRrX5Lo`*R_UhiZbS^@EoUaH6o;3%?$=Xk*mJ-PR8w+T zsXXjrdsY!r&r#Q#t$tXA~7;!J8;u^3Kjr80SA!i_{fI_IP*jYV6>axYeTy@`o2GWxEzWyZ96-t2 zfY;B$96UNig-Ff6d9B8#W_alDw-|0U@7L~*a5$pHB{HW|52i{FD0A^M?+hB)f8KbTLI;Y-&o2l`lq*LT`ad60Wjm>W4dLFl^4qf<>R7 zQ)G_1b5X_^6n~KR5>}GWd>8Rt4IYi}R~S362=<0{%5F^!hj*K6!dVtGRj0kToc5D~ zn4|Ez%a(i{=U4Bt;oF}Us*wKlwOg89g&F4M1QsZ$P!d(SXs<4Oq)H-ow?TJG`;;S=LnL!N#S2Hx;NhV zK4J254|uug#Q9>Pi4<%&AUe@%2(|C(jcwsSFdpd|<{)Xb`HtQ*hUf#G9N*swf{krZ z>ta*)iW!RIJs9kMEDku0p9Z1>ku`Yu+Szd)R|HANhnLaTrxKA0&Bcx`dEm^?q9TQ? zc<6s}va2=nHG#{tjifN*RnzUa1{^n>_KS#9>B#lrS{xiwo;*P=EJH4BnKj3Fm*~TO zk%*Ah$j6d-I2)!=B1>e)1G(L@cf-XUXQ#0^llCUx!xGP4AG;OBi1IfxQ0?6fm7SXi zWcZ9|Vl~+0;o4{=-IgY8+};B7^~3iEkrh2i( z|AmXom=&j0%ctEamG&RT2vgMEC#Fr18faH2{ ziH3ubzX8cc<9rym&!?)k+X~p0nmp5JD3J-V(npB~2_{4HvDo<5x66V%iRHbmGs#{g z=wiqXf@Ew#M48mDrsoySo|UO10U7|Rqz?k>!H%;Z4hN$KUbk1dKGxhgEm>>UEcMun zn^L7@7A3ljbBPh#4o`mvLxiW#h`yv~eW%*w=eFdwkVaPLjLSK2}KK&n%2`k ziSnU&0kVCka3Mb5Z*Cdh^7rDOz!h)(in6iZNttXDjU%eh7>F7rjR6Hw*GByB5=15K zxZSp&V)c9pIxv2Iatu0ccSYD?1j8!4Tvq-Wyx0LO?YqH;I`D(doK)rJaozc3CemP6 z#BJ1W#Nmw4bmveMH~roOXLFbzVXxEgiz-4k4lW}#z*`d*D_uG>DG7be=C>W8e4g~W zU&{GK{;pr&`b7-HMhU^_CF|xeacRuWqT`-r8_%RuDCI78*HLBqm+bXk;gpk5c88Dy zDPVe#;|nWdmBNir$w_`}#l=2KT*SHM8=BjH7vf0>(mjJvf4==d8gX#Y;Pu`WzGf+k z*-_d?P(TUsP3K>63j?E^$fa^B{VsWf4{Y|)fxog|3-n-8^vy;8IfB3Wq3NY2uvP<} zaewB0)eRAx{LH}pxaG5osHQHugRoHecYK4JOqH)+fcWl@Th52?!{-JzE+*XEC{DF0 zP5V=b`;7PL6#Gr?ATArM5S6Onm3e5PYQ9}GEQidRtqj%$f$LRa7=*YGVJ4bo)tv4LFIXTjl%UWFh|nLO=A1D!D?>>u(ae>X9{OsJbb3{w8YeA}~U_}#Fj_HO2) z&kEs!QI_S+!27vo{sIniQ{35XX8syERzP6&HLuns=_t_ymw7AMfWIU?nQUW-0>GVrpn zN}`)K1S}<-J&fNEx;8Rw5Xme)B3zbdXAu^JoZqA3_Gt~D3#ANs+`M_lgOCjQGQ53p zdEX~FH~~-r5%<((Hh9fvQ1lT|qeUR9a-i#0pBW*9gl<%2Xk__noRXputK z>GcF}Fs#Y9r1%*ePnhv=cnb;hA<@;(xuYV~)YLnNHYlwnYM!8V@zoeQWGCNIoG@wr z8$>T09p2hNS%ZJR@4smCH5XFcMYr-3CG>Qh0n;J1?4-(CeSzaCNGlu(^M1kvaj5j& za~dO9k>J#Vim9Vg>@TJEVF#vwQxK1{UHKzcwsXEd8r6jp>PSjG{R)arTt6G+8Ey&B z&#+fQ>fb7;Jv~X7h63%X2S7?ZCf~~F%f%6|QV5BTufwVOK3VUrxfVX<+7@xG#tMH= zv|8&Fnu|j3K@nMAlE1D=#1VaSNd}^kg5^LgtX|?QSb4{p-QJDTl7)+NL#cT&|6E&2?d`9C8@_)(cbtrO*8F2!u!u z>`F)}V|RdHC>|(V;M)YN?K^&cu3ei1UHOH1raq;GmzXF~2HrWx=Wy1u05Tvq&c+pq zb=^6X3(1*+=rI64DG437C)1x99Nv8rTmdZR#Dl*98P)OMnWFz~L5{FJDkZjfSQ6FX zstT`DK59+x`0+^+sEk$~9J;amX-Gb5Snsi-KiJX_SwHPS~&9)yhROV^g(^*8VQo_$8Gav4--TexEAM+-mqX{qlR4n zPz6ro$bI0m^mi2V?R^LKf8Yjvuvpo55e=vUe~-iKn1D$M@Mf<+2HE$LDm>>-Ycm^IwUwxI5^3Q7nCXM3IOHHoAn}=;L!WwW!a4Qq7?V^YG zb@hce^`gR#VG<#uPU)UTsmWC2Nz_*Q0%Ms051&V4ceOWM=ID8re3(}7Vf&U^V>S*7`^D_pAUBs6r~*e1Ge-4Zc%KC8^y zFM8V@qay6n=ZXHl?6fRiC)-u%W?8oSI_^`Q-Q*`1<@fc4mHJslkmSE;-8=?=FND(0 zSsF&mz@xp`92!ypnaw+c6v6*|Tfyvv%)hvZLL7|j<01>o5D@#qQM)PQ-=O@6#KD%7 zog*SEU5^6zAT#PZ(5@G~3!}89*-?nyVH`{IZMfQ<_(w*sL5+0ZDTI>2#?l;4ysX3Z z+!OKM?wa;jW(MY1Cg}93)u3=s*ucfIni`orsQs7EBaVszWt-J0qfFM(hTSfvGbiu?wyA6DJY^HvUomLNdnU+eQx<2(8~bi@SbK22NbzW0 zKO&C!LAdqZ{A#rVhj0<-OY6R>SG~WL9Ov*%Dmw(JI zu6w#1!Jh*z@zz(})_51mo;x5Pu(LC;2wD}X7=KxOkcpL5T71_S{#QaiCIJkO%!~r! zx1)}tWaBqqRc%5V8SuY=dHJC34W)moEeJ z=o>UVzsV;zF;4kbI~NN|iVSwTar1V>uJJ@bfMFrRIR~ueGcdTj@OEff?93KawH{kz z+UVKfl8XJ+Ta)}P6D)nS;;&@kR^vKdtsc5(87)v;)A9ut!78ORZtZqDY+3v|Y+Eu#LSN9jnU%`_anRmX84 zT`A$OQZIU1-vOO8&w>8GR&CdB>YitUET7RDUIo#x>43o3c(iJ)rxxt~k}%~MyFhIP z^|>CcPU=e&ko_xmKQ)fWrl)GlDwTou=_<`$^Gc`%_py8JFj79}NwKt_un46*XpczQ zcCQ?A@fiP{%Pe}pd-ksm`$<+wR@%znz%vFz*f)&=lUk2t<2=`?pann!^|55E9SQNz zN6n=w!v|T~X|W8?1m)S;1R1CTvo=L`PORI=4?t6J_*}vqKtnCDHuLH?{Q5|W0_dFgD-*o;*)$p_xmW$)#;Y7S7y^&H}*0k2Bd>Aws=8*(a` zp6Xek26K#Sb8AGW<~WsK@RI2D}DDUveK*i)GCZ*db-$yZZOSW zJT?;#+5T^_HdJ;%D;p2??6J*P5>!Yfy}FC&K3YbHj8b-rW{wWvOB?4C1pY^c*%7~6 zu_^o4_TI`s`UzbA@Ah2>nKQ^)(Mq9?$yk2>kA41%^b)x7_Zhb6=w-lwUs*A02KG+z zD{vj#+m$7(q0e+ur!e2~O9_bqj*>9mbYA+`2k%GlHV1c)jMSyFhzulzp^Ns`U)Yth zEm>fiIkHQkl)MrPF)5kvxD$`~xkD!G$SI<7ReS!=I=x?|r)nZ8|I4Uu!fYdr@EfKx(Fx@|a6}S^ln3@}`A^o2 zP|7Z?>f{5LpB@nnnnpfy&Rt1!b=Y64(LO7+3@_OfIGcmW1rq6xQ*|sl*;M4*@`+6O zr4aH`ot*~~E8Kav4?M+!>2I%zQKdxYIO}%VrixM-0_}ctKYL95wzZ^3GYnUvM>#CC zN#KF*c8Ya}ymn8$0w#ByM@zdvZmj-9qsUzbN%$M+pQK&IBug<#AH_M2?E13>^)wge zvvi1z!+4W%5@4a6sT;N>DN)s4pC)my(yG7od66MIfO*MZh!wX`N_a$_zvz%0QMj^6 zI4e~)!7;N^%Fp?WA*XStuDC;Fkp%$#(yMe3y*FWJ)Ea0!T4#F>+Otdo>i7z1FN<*R zKkI!oSE)Yf_N@x8qUZ_w$`*1rM;1F(UEAmOn+{SA_aa+u2(4S*ven*OD>wDFbb(E9 zONaakU!Ksmd3WdFB{UX=cQDWXTHLAXmAv?FEGDsy2;L+?vI=0ge2 zx{X*f@wB+Ms|J?Vt*sx$fh0No@L1ErEW;0Nf`=~f3U!8ZQ_V{@l1AJZx^2Z>TRkJc<$@40`*9X#QVj(52KRiFtJa}8eyQ;eJ6D-WILSaF)GF&KifiL+jm z-EGMXr8HMByXTj+Cc-#XOsun-gJW{d*Bg{b`nHDlJM91y*<2HD9PA0?iR_uB9`PH(J>CciYD`f`^i;nH83P`@O=~w1;(K zrwK`KqT{xPZGG0n1ruJ_IcHG%13m}AA-2P3Vw?hc-XLb2nZAq3N?^qMLQ>MJT5j^j zVW{Dx2A;)wZ%npqV$dWHe{pPs5Hwex&m7nOqA=nehNQN_JMEbA_$rmXVMHBI-c$?s zd?|mTw0FGtbD+y;}B92tG`LxPI{^$ScIQvjhtI* zF#XvxBrSFvL%yBbSU92e9j8dOL)IjTwki4v&H$s3v}RPeD1TgveGSm!(Wse!M`p5T zx)$0iU7j7IS2~lU4JlLCfL&PlDj=@)wUq)XeoDFI!s&Z;(rGBK3P%$_vn75BD4Pq0 z4fIX2r*1C?hfEZhMlNWBz?JLqzX^Hk#=8wCB~EU&(ufd8G{&^cz|KrgA>{01jZlHv z6GvzBFG^NTO<>1m9FSY7M=|8^&g>biq!_T^B}*2Ns z-hDe*JLeVeH4!QhLUPFW7n}aOVskDTwg=Qt=V(rb`XxMOZeED}{y@$~sKL0~8iUu( z=2PjVJtZp+D6JWESqNp|`7j3&B;c(s?nP#a^Xa8~}k6Mrka{ zaO=qijrPJ(r{`UOC+_nAZ8E6mC0x3+)pR`c9XZ9FA(LOWw6T`8l7cLRoZSvAw=SX( zoCc<&liE#8z5$^(z{HLNLM^i%Hf|V&n5I)-qC=aXTfEyXmtZ zAiypbyIS{lCgGF2rwn3EU!ePe9K!ol_%qdVIF;#IS@OX!`+h0($1AIMvpjwc*E!Eo z3+d+1uEX2UTTd5ElaTs%Wogxo#J`5kb|uJ@H{wr9DAULSLf%@Bilmi%ofHYw&vCZ^ z>^@V?C%pj5DZn#t6#*xV$4@faQPD5LNV7N0b*}m4j16e@V<<@XZK!W%=nn?@QX`+IRAG zNkR%WNOZ#}yL#<&J!FuzGZlWTUmtk|vJ9{zn|SzEM9vooa-lr5ZA8V3*Czs1e1jYI z6pJFXJ3G?yV$jJf3eeG*UGLDVHqt(4roZFqH%z-c>9wLa4$H7 zrw?+FmR%x#_2VyjT2;F;n`^b(q?${WkiV~Rf(aqsGl*dE4Y6Esr40Hg0eAuo*!K9u z#3=t}7Lmby>j4;{t`R_9aWzCY^k~z?bvAvFRyHr3RX-J+J_2EvB@)kgVCkx_JSYd> z8I*PYL(V^H;PI(i#W__^6^?StB_E2w8|&4mE+LdRtFh0{m%}WaK>n;Tit|$Ops`f( z#PY7EL?e7UY?_HoI1lI5K()zpG)j*`IVC)tudD;S{Wjv9WvMjuP*Wq1kQ5(*JbQ-h z=G;E*Iq(PEk6d;83#IJQe83CrOurY3do_bqLrVdLQDb#nyE~s-!N$ zUf!dr-^CeXB-h_FyXzAVGg6_Ur>~MfQ*v~Su?@9S@Kv9Cp#RUE4uxZ-K)(K8{Mm+% z>(vgL-J>hjKYR77=kk+|L#@e)C9IZ@WFAtrCGe;l<*f&loWFE9H%KdZocdZ|Jve-p zniKffjmN5xPRwc?r{to7`xkRfIj;j*Pcr3R)DqX{zZA`6y^p=IU+qVF@M;@d2eZe}AAKdG=Q9)4$oBhBf3tQf*B2fD zBn38>Z2OMzS&2%0iR^b;-F$QOO7Uoh2{6-%9$1PvV%z+mcS0l`dffn5YblJF&Zk%q zMsk;bcW257a6mQP49U;?_B=mMY0%4Td)QV{83GA_i$IWNXRE~GlOxTw?deHaUS9ry z;T6Vqi+6Uq>fHBhECc$I;0lR+8p1qL0wRJe(U4lKWu3ke-YBKPonJwIZE(~=9<4dQ z7msxpWJbp%?>KaqU@rmFDa9-YUH?~hlA+728DMbp-X{?&u#-m&t)boe&W^{~=gkil zQoEh=;S4*VI?`hp8`mmSo&~LS_C`xEi_+?BcjkvHX?R2%K$*OJ$}}Z#GNEiPkc&sb zRc6OF>p$yPJ6aKRTT?^hWi&l9z{nWb-|+4|+20Jp-nZFmft7JVKMxtX(AQ*hum(_~ z7*CIE(R*+TX&Tv#AkWre%?qstpmM%ubza}=R@1bMMCVae@TBH(w~}|4?$fIo2{r7h zw|Iuw+fHVy7U!gfkCBl|m9VV%A6GeQ><5cA^Pff-gi_v(c3ue(*W0wI#)5hFYZOur zG*UnwnR31jTVBq_WUV*-kuV2*gJG}bOBvg!2QldJRxsg{UnAMa1-7}`Adw@TzIryQ zTXXwTX98t__{6jK$Y0GLHPE|{BYg3dAdH93=b~H`F`^sSs1!0y2|e{p+jzf~Z&;>^ zq$zFePqWH!?seR^s{KQKhKz}1IH*@&WysfCv+-4M!GT8l;ohQMTm2niEhUZ#z8{W~ zxR;Le)!H#|z$|R^TYZJCM>B4XvvON@GiS)ssizdg;w7&*m5hz6X}VXD(+L?Nnz%W98F#X7FyHs_OggQw&+VK>=kPZQU>1$bO}rCg5u91G{dR#lqFI zpWni)a_EC<+~s`N8X?W5jX}m^VqSzj_Rf9N&jufbVy*xfp?@?O{@_?09A)00gba9E zJpnc&QP^#jC5yad<&z*~K0b{!Ud(r$y-KycyKegKuz&R+kd*Mmcs>;f`MTlJ&2?W% zL5i6u&$;-N2&!Jixk?6K8#~A_beq_d4o;g8YoXfz8l(X@2R?dXQq{lgac?SzZm&m} z+H(OQbckj%n;t8zqoTmOt69b12b?)o*A$ArRR_`y%wpq4J2rI~olRfMu;c%r^wO%E zY$hIxxaQd@NY;vb@zp=SbHAUG!i#vi`ak-61y#d29IW=(CSR`Bcch$`H>vTSd9!60 z^v6!Py|pB;I{Q9|&^Pm$vBv-)Aa4K=<*fRryJ&Gxp)NJjQZ6uSr_Vmuno&^vIJGzs ziIilHXp?$;D^=ABC}04v7J?n&3cgYG+D7;xj{>oQ_qw|r{M7yWwro}qRg>#?BRl0J zDQuV0ujCR+l&kiP6mJX^GhXMv?{E6?5Grswc4t|HHZN18#z9Dek6(f_8ZIQj*_%Y5 zdjj+XnEVzXoKxj06l3Fn9(}7K?(&U@x&d)LsKxh<+efM=!PX_+WZpdM1BGPEaI$=BQ&Hebhk$DLgxB_%=tKhncb9ofNA zoJa7-|9OLhVJrtj2j5oXxm|xw)Y?I9O5om zc6;@oPX<`MWCM`Kd)NMER?|OMYn8wY{uD7#ml^&i-_51;&M2KNhzPjwzZ)rxlVUV! zBso>@uB%ue`xC#zDD#GciaRw6YEKC&mk-uUc@XV9oGeYZAH%VkLoRH;YKfb+Wio~3 z^LstJgm%}bEn~0HW*+e0>w0LwJ$F;>r3=kiN7dc$oi_bgk;IT?3Pjvim8BCBt|hWX z^>o07ve<>O6d>Z2e(``z5Bt)YA8#w*2JH9pL$WOF0&c(Fg(7f|pQ&^%C@e4AM;U77 z3+B_P)Z-7o&{+S{V_Ds<%Ll%Qfbd-2rm=N7NSpE{f80T=cypV^dg*p}l@laao+c*l zrQOIE=9m$p(O--}IqlQa2pNzUzfG`IQcd}k@82+tn>CMx*OT)_dWp9iQ;eh%%$^Kn za}TW#U(eIeoBkE>( zYzUrX^}8oKIVC|7bkn21?%Yb{`C`*(4~ZUnb~V>BKP!8}Is=maLNITXXUrRXbI14g z7|UwNuxI|?!eOSEy^}PePQ@ z@6E%ZZr{K0ySn8rq>V^ODcM8Fp0tpnWEs0G2_fs)#)KmKPO>Fi82i4A<(54rdkjO_ z4Kwz^7=G6bb>E-w_xC)YtBmq~nM z1D8^5SQ0m2MS6?u`=Ph6x5+QhRT(#wk9qQrd8`I|toLiJ#9~m05I8^%H7f7}-yGR5 z<>n3!i$By%wok>VBSLn1;_klo2O0UDk@~qCCOMFTDowW?E`K+IVq`3a%Y|O@L-;P zWMQFDrQ5pYzVDKZvN9i&$Ne>U?mk4VP;%u_;+l04uefTq>TswD1C4J;jFQT(<&aa2 z^7g%vsf{Wn2AXZGSt@k)lj6=@8p<70^eU1v#u0BXwg-yP-LWbsWgAS5#a)oXT+u-p zBZ`$`XSn@db1_!JSFVpHoeU@WFMZ)*+?3oIg+Px1`=(+Dr~yoaEc|Yz=v>Gu`{Vs+ zb(^ngHSogzkMQ~Hwf$G%zGwK^+}tQ%foh}cZYfIHu~SO&W=~Cven6VMK??sB^C3+O zNBl#kg|H9i>;=A@4}dvgFi{Tj(%_{F)~sc*+Gw7bPw59SpCXPSfqz5(v{>>a*-5Pk z_9<_DlEw-gk-^_;o7R~NqUF8OkwA3@*;7&ZJud&2SvYRN_JJqUCEkf1*Uz_OrlrvB zf-%{?jMMw0sifIjSUsd|sL8R&p18`t7mLhq`!PI5j7jGu$H~EZymlpTMEk4FW-nfW zzz{8}spFlFYOa)|$*0M)KNs%nl-;Gki6B7L!gx1KpYs&pW#>(YgnaFmtagYe{l^m;8nI;=k`_&hzC)ugYntB>&y^fh8c^P37~?#YmC5TW$U~ zc-Yq$S;qQr%x z>{{~U|1WDelDqK#r)_{q_@C@F?;m2WBzE92S7wVi98J`UZWj#E5A(N}aov108s^XP zw2LC!1s-xs>@TN6qxliumG6zblm0%zJtD6ZuIACTr4MMCH4@k(e!|#~>Hiebm(jdR zCFg!WPcoEzq>(=~hAx3rz4|NL^L}Xy^a@>vLj^J6MSfcC*wHSN!L_J|dcAkwvIsNO ztll!TgLiDH<^*TJ%a4PlWvn^D!!1c; zwqf2y@aUu=gJ1nD`1l5-GvF91_kL(iCfHS^aL;Yw^IPND@6a3rtUw6#f(LYcV&!`_ zgW3{_Zsj?FZbo}1fT-ok)l!a#s&oCDZD6)q7ZaYOY7os;Tc^h6Zk#?+X9^g#EGSkQ zOZMZQ8&X@ha^3d`%9pHp<^?x#mhT9|8_t+8U!TwpsFs8^o2TAJ;%dp0qw$6XxU|i9 zF1A?XQY10?`<2|(u1H}{IyrYm=e-}SS)04Zxjluq*^VZfIO6!B=y{U#R>e!}5CkPz z3``uP9uVS~b{dazf8X*nR-mK9O3^(qB`~YneJs`STw0QA_qXaY(dUDxU3EILVNz*# z+jDtIK~t!H@ny1Pv9f2uo!kkVd(aiRn%ivXLB_<(&&Htx`gGMO3HO+KD_yj9(e3u# zrOM)4$U3)Dxu*OFp{tOtI-Vx_Siqoy_li&Yz^){5yj?JsZ-P&%ynj5Wf1tIo5Np;_OR{sADsQ*5tGi#rBXPkeQG;E10lU zQYbYxI%Cb=hPz_cbA3U&L~=`}>BxPQt>(Ss%>FfH`i?GlyM{tvR<(XKz*aad??sj1 zJQR~YhccewBgY&rStpQ4_}yx%HM^=kqqUpA1!G??G$vAYO$^PRSf%r=r0ZhUJH>qy zv|0O80VJ(*?P&-|(Ca^i=Y=@7a;*#xID4~hsub3H^1B-%o&_bXHM3d1DAzD6K!4w3 zW#G%+cSnQqy&~;Yn6R646G=*DdUES^K@-Npaz^p316-P|q`mRElh6Qax5GcUVO)Px z7{+lUb=wSfGaM?FkdI|Vaotam7gaqz60+_X$l1!NOqK$HqqA1t6Lc%v66Pw0p;Apa z=hGl;rohR7j~MvDA{^ezL{2Cme_jWjn>Y3nv)g6eYI@3>X4IzO%x1uCR%a2J)9}Op z==t1B-e_F4sz!&uV8TpPR3Q0;aFhLhB~)giW|yFBMo1<_(zduSz6qde?-$8$_r*)) zr`nSl!OMqc+Wrir5|-g?F@$gva@OImDmt{unN#Esq!rK--o3BlI5YUukVno*p|pBR z`aQ2L*WOx`8;NCO^oURzwzRh>adz|W{xAM`O%-C?;2}KvE4cqhz0AErBiY}J^oQvm z`zVq8{r`ja$Aw-fu2stt-qPGMH9va*!k8IgN!ci^byr?;YiYW(fJLLK(tb`sYf|u> zz&_2i=;9}o;kqm3T1`{_447KS`FleJ_dQs^?Yp)A9|hn*Fgy{u>G?vR?Sh(IS+m*C zX04UqH?*nxnrpVKVgF1nW)%W6pa?2EFn|C6A;eo5MiLhD9Nqlwst%Hc_s15e zUdH>JZy-YaJr8sH5cKrEH~TFUj19}rbls1FTPy$4FRYh}6M~*S`0oXnEH()}zp6(b zmZHYz04gy0JDv~c3Jo5~RqXrvv=x`To_$0?$xpb*kix_0k-pJF;z;N66Cr)SM8OrN z9Cwokt7a;;J;>zhKh#cT8#jD+S@S#Vz_8!Y%XhN*PAI$eEdlcX(=MqudFP~>gtMz< zukv;&t48JJ4j4lT-m-iQ$mv3qRV7RVzU<3W7 zJ-$!oHp=@&5H70UkI2;P{h|_23QQHFsMD5JJDX>En18q1kRWoAYXcvHtXm9wGO=pW z8htfONB!EBI-GqBoidhV{R4hu8{K)IO!HMK70kF`8Of2OAA7;2{Dz*1sCr}6$CjCW-Qwjc`V39IaCN(eo7sjYjGt*P6km;Y9nts0_({W{H=s}jw z2d?<@zUF0xlD*=Jqv`c41FR8|)e1Vuj?*gJfmCkp*pM^>5S`y?2~L4r&zz7aZ`qq;(*+7={2_W0 zWBbaWoLq!M+&byI?hdJI+Srn|L+hLMbI&Z~)H$$yaI_Y&x?|M(ejrt9VyRlGrfoH( zy?ju4CVCc&(nE!m8se;Upt5H>dUm9_UA|#L@?r4!K5N0^m{>$^*pU`@QX~Hrb-uqa zmE(epUu4-NCwd%qKE4d7Y0xiPz5M1!8Y+xg)#H7M z?pqn3AlG9{aAAj`n;cd*FYU4ZnR;$`@=G_XrE8%&yLI@PPJKBhHT;&f> z+{oFJU39KTC(c?0D5pLJ58%Ssfih&N&3d*p_mF0OS?{Gp9~CrBX^~Lf;|F5wG53(k_X%xP zcU{%e0?*<$PV+Ht;`NsBS_>0G0=eiwPV*;O3#B23k$o=(G+XpoXJ~ePLGkz-OcX z`Q6#0>Q}1{6T@y5);}l^F*JvqPv%`achs@!M8^j}UY<}q_{dSJ?qSSyv>)o+tkB+elqu;HGQSu0qAp4h?O@dtIU<+tZ5g_7&jC2ZExI!u|CEi6*sL zlk$bpxwInnm}!Anq`8ds(`$@>N)dohDTsBF3yDrtD6Ho`Jo!a2Ew|TZTGl;Mr|_dw z^;YR(g*6?KLc=^=^v@fn)?RQ!nkDh%_xMkDU5j?6LSCS&A$;G?Tp*$)-oEZH?HZ+X zxW>3P*wH-FQ-AW7?+9Qd5xZ_rjGoy&@F4wba!%*?cvLP}CvPPWoVEMLzr)?ai*F$gXYMqArJp4G2l=DspJytJy4Ear3NW8v zb1X_83m0OZWI7l?2hL6AYZAX(2|5^FuDgAHv@Nh#gYP

D*u7z2+s%{dTepjjTkOpy*lyJ=z#fsq4&{9M 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 z `Item` > `Add Item` and fillup all necessary details > `Save` + +3. Add Item to Website Item: + On Item page click `Actions` (Top Right) > Click `Publish in Website` + ![Screen shot of ](./assets/publish_in_website.png) + +5. Create Specifications for the Item: + 1. Typee `Specification` in Awesomebar > `Add Specification`. + 2. Select `DocType` on which you would like to customize the specification. + 3. Select `Apply On` based on the doctype. + 4. Mark it `Enabled`. + 5. Under `Attributes` table click `Add Row` > `Edit`. + ![Screen shot of ](./assets/specification.png) + + - Write unique `Attribute Name`. + - Choose if the value is expected to be a `Date Value` or `Numeric Value`. + - Select `Apply On` (In this case, we can choose `Item`). + - Select the `Field` on which this should be applied (Can be left empty). + - Choose `Component` based one following explainations: + + 1. `FacetedSearchColorPicker` If the Attribute is related to the colors. + 2. `FacetedSearchDateRange` If the Attribute is related to the Date values. + 3. `FacetedSearchNumericRange` If the Attribute is related to the Numeric values. + 4. `AttributeFilter` If any other the mentioned above. + ![Screen shot of ](./assets/specification_attribute.png) + + - `Save` Specifications. +7. Create `Specification Values`: + - Route through `Stock` > `Item` > Choose Item. + - On Item page click `Actions` (Top Right) > Click `Edit Specification` (This will prepopulate the specifications based on your selection, If not please select from the dropdown) + - `Save` + ![Screen shot of ](./assets/edit_specifications.png) + +This is how you can create and manage your specification. you can go to `/all-products`, you will see listed Item and Filter(s) on left. + +### Note: +`Specification` and `Specification Values` are reusable as far as the grouping of Items are done correctly, you may want to create new specification for different type goods. \ No newline at end of file From 2259fff054b7068649da3c1e88d54dabee421301 Mon Sep 17 00:00:00 2001 From: Devarsh Bhatt Date: Fri, 19 Apr 2024 19:04:39 +0530 Subject: [PATCH 23/64] docs: fix images --- inventory_tools/docs/faceted_search.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/inventory_tools/docs/faceted_search.md b/inventory_tools/docs/faceted_search.md index 761f4ec..3692d2b 100644 --- a/inventory_tools/docs/faceted_search.md +++ b/inventory_tools/docs/faceted_search.md @@ -14,7 +14,7 @@ Follow the steps to create your first listed product which allows multiple speci 3. Add Item to Website Item: On Item page click `Actions` (Top Right) > Click `Publish in Website` - ![Screen shot of ](./assets/publish_in_website.png) +![Screen shot of ](./assets/publish_in_website.PNG) 5. Create Specifications for the Item: 1. Typee `Specification` in Awesomebar > `Add Specification`. @@ -22,7 +22,7 @@ Follow the steps to create your first listed product which allows multiple speci 3. Select `Apply On` based on the doctype. 4. Mark it `Enabled`. 5. Under `Attributes` table click `Add Row` > `Edit`. - ![Screen shot of ](./assets/specification.png) +![Screen shot of ](./assets/specification.PNG) - Write unique `Attribute Name`. - Choose if the value is expected to be a `Date Value` or `Numeric Value`. @@ -34,14 +34,14 @@ Follow the steps to create your first listed product which allows multiple speci 2. `FacetedSearchDateRange` If the Attribute is related to the Date values. 3. `FacetedSearchNumericRange` If the Attribute is related to the Numeric values. 4. `AttributeFilter` If any other the mentioned above. - ![Screen shot of ](./assets/specification_attribute.png) +![Screen shot of ](./assets/specification_attribute.PNG) - `Save` Specifications. 7. Create `Specification Values`: - Route through `Stock` > `Item` > Choose Item. - On Item page click `Actions` (Top Right) > Click `Edit Specification` (This will prepopulate the specifications based on your selection, If not please select from the dropdown) - `Save` - ![Screen shot of ](./assets/edit_specifications.png) +![Screen shot of ](./assets/edit_specifications.PNG) This is how you can create and manage your specification. you can go to `/all-products`, you will see listed Item and Filter(s) on left. From 391680b2107449ccd45bdd892c7d093d6b7cf82d Mon Sep 17 00:00:00 2001 From: Devarsh Bhatt Date: Sun, 21 Apr 2024 15:30:25 +0530 Subject: [PATCH 24/64] refactor: documentation, filtering ux --- inventory_tools/docs/faceted_search.md | 5 +++-- .../public/js/faceted_search/AttributeFilter.vue | 1 - .../public/js/faceted_search/FacetedSearch.vue | 8 +++++++- .../public/js/faceted_search/FacetedSearchColorPicker.vue | 1 - .../public/js/faceted_search/FacetedSearchDateRange.vue | 1 - .../js/faceted_search/FacetedSearchNumericRange.vue | 1 - 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/inventory_tools/docs/faceted_search.md b/inventory_tools/docs/faceted_search.md index 3692d2b..4bbc1fe 100644 --- a/inventory_tools/docs/faceted_search.md +++ b/inventory_tools/docs/faceted_search.md @@ -17,13 +17,14 @@ Follow the steps to create your first listed product which allows multiple speci ![Screen shot of ](./assets/publish_in_website.PNG) 5. Create Specifications for the Item: - 1. Typee `Specification` in Awesomebar > `Add Specification`. + 1. Type `Specification` in Awesomebar > `Add Specification`. 2. Select `DocType` on which you would like to customize the specification. 3. Select `Apply On` based on the doctype. 4. Mark it `Enabled`. - 5. Under `Attributes` table click `Add Row` > `Edit`. ![Screen shot of ](./assets/specification.PNG) + 5. Under `Attributes` table click `Add Row` > `Edit`. + - Write unique `Attribute Name`. - Choose if the value is expected to be a `Date Value` or `Numeric Value`. - Select `Apply On` (In this case, we can choose `Item`). diff --git a/inventory_tools/public/js/faceted_search/AttributeFilter.vue b/inventory_tools/public/js/faceted_search/AttributeFilter.vue index da155db..d91b7bb 100644 --- a/inventory_tools/public/js/faceted_search/AttributeFilter.vue +++ b/inventory_tools/public/js/faceted_search/AttributeFilter.vue @@ -1,6 +1,5 @@