refactor: POS workflow (#20789)
* refactor: add pos invoice doctype replacing sales invoice in POS
* refactor: move pos.py to pos invoice
* feat: add pos invoice merge log doctype
* feat: ability to merge pos invoices into a sales invoice
* feat: [wip] new ui for point of sale
* fix: pos.py moved to pos_invoice
* feat: loyalty points for POS Invoice
* fix: loyalty points on merging
* feat: return against pos invoices
* Merge 'fork/serial-no-selector' into refactor-pos-invoice
* chore: status fix and set warehouse from pos profile
* fix: naming series
* feat: merge pos returns into credit notes
* feat: add pos list action for merging into sales invoices
* feat[UX]: add shortcuts & focus on search after customer selection
* feat: stock validation from previous pos transactions
* Merge 'fork/serial-no-selector' into refactor-pos-invoice
* chore: fix df not found for base_amount precision
* feat: serial no validation from previous pos transactions
* chore: move pos.py into pos page
* feat: pos opening voucher
* feat: link pos closing voucher with opening voucher
* chore: use map_doc instead of get_mapped_doc for better perf
* feat: enforce opening voucher on pos page
* feat: [ui] [wip] point of sale beta ui refactor
* fix: auto fetching serial nos with batch no
* feat: [ui] item details section for new pos ui
* feat: remove item from cart
* refactor: [ui] [wip] split point_of_sale into components
* new payment component
* new numberpad
* fix pos opening status
* move from flex to grids
* fix: search from item selector
* feat: loyalty points as payment method
* feat: pos invoice status
* fix a bug with invalid JSON
* fix: loyalty program ui fixes
* feat: past order list and past order summary
* feat: (minor) setting discount from item details
* fix: adding item before customer selection
* feat: post order submission summary
* save and open draft orders
* fix: item group filter
* fix: item_det not defined while submitting sle
* fix: minor bugs
* fix: minor ux fixes
* feat: show opening time in pos ui
* feat: item and customer images
* feat: emailing and printing an invoice
* fix: item details field edit shows empty alert
* fix: (minor) ux fixes
* chore: rename pos opening voucher to pos opening entry
* chore: (minor) rename pos closing voucher and sub doctypes
* chore: add patch for renaming pos closing doctypes
* fix: negative stock not allowed in pos invoices* default is_pos in pos invoices* fix: transalation
* fix: invoices not getting fetched on pos closing
* fix: indentation
* feat: view / edit customer info
* fix: minor bugs
* fix: minor bug
* fix: patch
* fix: minor ux issues
* fix: remove uppercase status
* refactor: pos closing payment reconciliation
* fix: move pos invoice print formats to pos invoice doctype
* fix: ui issues
* feat: new child doctype to store pos payment mode details
* fix: add to patches.txt
* feat: search by serial no
* chore: [wip] code cleanup
* fix: item not selectable from cart
* chore: [wip] code cleanup
* fix: minor issues
* loyalty points transactions
* default payment mode
* fix: minor fixes
* set correct mop amount with loaylty points
* editing draft invoices from UI
* chore: pos invoice merge log tests
* fix: batch / serial validation in pos ui and on submission
* feat: use onscan js for barcode scan events
* fix: cart header with amount column
* fix: validate batch no and qty in pos transactions
* chore: do not fetch closing balances as opening balance
* feat: show available qty in item selector
* feat: shortcuts
* fix: onscan.js not found
* fix: onscan.js not found
* fix: cannot return partial items
* fix: neagtive stock indicator
* feat: invoice discount
* fix: change available stock on warehouse change
* chore: cleanup code
* fix: pos profile payment method table
* feat: adding same item with different uom
* fix: loyalty points deleted after consolidation
* fix: enter loyalty amount instead of loyalty points
* chore: return print format
* feat: custom fields in pos view
* chore: pos invoice test
* chore: remove offline pos
* fix: cyclic dependency
* fix: cyclic dependency
* patch: remove pos page and order fixes
* chore: little fixes
* fix: patch perf and plural naming
* chore: tidy up pos invoice validation
* chore: move pos closing to accounts
* fix: move pos doctypes to accounts
* fix: move pos doctypes to accounts
* fix: item description in cart
* fix: item description in cart
* chore: loyalty tests
* minor fixes
* chore: rename point of sale beta to point of sale
* chore: reset past order summary on filter change
* chore: add point of sale to accounting desk
* fix: payment reconciliation table in pos closing
* fix: travis
* Update accounting.json
* fix: test cases
* fix: tests
* patch loyalty point entries
* fix: remove test
* default mode of payment is mandatory for pos transaction
* chore: remove unused checks from pos profile
* fix: loyalty point entry patch
* fix: numpad reset and patches
* fix: minor bugs
* fix: travis
* fix: travis
* fix: travis
* fix: travis
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json
index 31315e4..a249783 100644
--- a/erpnext/accounts/desk_page/accounting/accounting.json
+++ b/erpnext/accounts/desk_page/accounting/accounting.json
@@ -148,9 +148,14 @@
"type": "Report"
},
{
+ "label": "Point of Sale",
+ "link_to": "point-of-sale",
+ "type": "Page"
+ },
+ {
"label": "Dashboard",
"link_to": "Accounts",
"type": "Dashboard"
}
]
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.json b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.json
index 5975198..4c1be65 100644
--- a/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.json
+++ b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.json
@@ -1,426 +1,123 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "",
- "beta": 0,
- "creation": "2018-01-23 05:40:18.117583",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "creation": "2018-01-23 05:40:18.117583",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "loyalty_program",
+ "loyalty_program_tier",
+ "customer",
+ "invoice_type",
+ "invoice",
+ "redeem_against",
+ "loyalty_points",
+ "purchase_amount",
+ "expiry_date",
+ "posting_date",
+ "company"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "loyalty_program",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Loyalty Program",
- "length": 0,
- "no_copy": 0,
- "options": "Loyalty Program",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "loyalty_program",
+ "fieldtype": "Link",
+ "label": "Loyalty Program",
+ "options": "Loyalty Program"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "loyalty_program_tier",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Loyalty Program Tier",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "loyalty_program_tier",
+ "fieldtype": "Data",
+ "label": "Loyalty Program Tier"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "customer",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Customer",
- "length": 0,
- "no_copy": 0,
- "options": "Customer",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Customer",
+ "options": "Customer"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "sales_invoice",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Sales Invoice",
- "length": 0,
- "no_copy": 0,
- "options": "Sales Invoice",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "redeem_against",
+ "fieldtype": "Link",
+ "label": "Redeem Against",
+ "options": "Loyalty Point Entry"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "redeem_against",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Redeem Against",
- "length": 0,
- "no_copy": 0,
- "options": "Loyalty Point Entry",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "loyalty_points",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Loyalty Points"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "loyalty_points",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Loyalty Points",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "purchase_amount",
+ "fieldtype": "Currency",
+ "label": "Purchase Amount"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "purchase_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Purchase Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "expiry_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Expiry Date"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "expiry_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Expiry Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "label": "Posting Date"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "posting_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Posting Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "company",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Company",
- "length": 0,
- "no_copy": 0,
- "options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "invoice_type",
+ "fieldtype": "Link",
+ "label": "Invoice Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "invoice",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Invoice",
+ "options": "invoice_type"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 1,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-08-29 16:05:22.810347",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Loyalty Point Entry",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "in_create": 1,
+ "modified": "2020-01-30 17:27:55.964242",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Loyalty Point Entry",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Auditor",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
- },
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Auditor"
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
- },
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager"
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User"
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "customer",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "customer",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py
index d65a7d8..3579a1a 100644
--- a/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py
+++ b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py
@@ -18,7 +18,7 @@
date = today()
return frappe.db.sql('''
- select name, loyalty_points, expiry_date, loyalty_program_tier, sales_invoice
+ select name, loyalty_points, expiry_date, loyalty_program_tier, invoice_type, invoice
from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s
and expiry_date>=%s and loyalty_points>0 and company=%s
diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
index 563165b..cb753a3 100644
--- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
+++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
@@ -36,7 +36,8 @@
return {"loyalty_points": 0, "total_spent": 0}
@frappe.whitelist()
-def get_loyalty_program_details_with_points(customer, loyalty_program=None, expiry_date=None, company=None, silent=False, include_expired_entry=False, current_transaction_amount=0):
+def get_loyalty_program_details_with_points(customer, loyalty_program=None, expiry_date=None, company=None, \
+ silent=False, include_expired_entry=False, current_transaction_amount=0):
lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent)
loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program)
lp_details.update(get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry))
@@ -59,10 +60,10 @@
if not loyalty_program:
loyalty_program = frappe.db.get_value("Customer", customer, "loyalty_program")
- if not (loyalty_program or silent):
+ if not loyalty_program and not silent:
frappe.throw(_("Customer isn't enrolled in any Loyalty Program"))
elif silent and not loyalty_program:
- return frappe._dict({"loyalty_program": None})
+ return frappe._dict({"loyalty_programs": None})
if not company:
company = frappe.db.get_default("company") or frappe.get_all("Company")[0].name
diff --git a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
index 341884c..ee73cca 100644
--- a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
+++ b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
@@ -27,7 +27,7 @@
customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"})
earned_points = get_points_earned(si_original)
- lpe = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_original.name, 'customer': si_original.customer})
+ lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer})
self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program)
self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier)
@@ -42,8 +42,8 @@
earned_after_redemption = get_points_earned(si_redeem)
- lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_redeem.name, 'redeem_against': lpe.name})
- lpe_earn = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]})
+ lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'redeem_against': lpe.name})
+ lpe_earn = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]})
self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption)
self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points))
@@ -66,7 +66,7 @@
earned_points = get_points_earned(si_original)
- lpe = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_original.name, 'customer': si_original.customer})
+ lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer})
self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program)
self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier)
@@ -82,8 +82,8 @@
customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"})
earned_after_redemption = get_points_earned(si_redeem)
- lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_redeem.name, 'redeem_against': lpe.name})
- lpe_earn = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]})
+ lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'redeem_against': lpe.name})
+ lpe_earn = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]})
self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption)
self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points))
@@ -101,7 +101,7 @@
si.insert()
si.submit()
- lpe = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si.name, 'customer': si.customer})
+ lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si.name, 'customer': si.customer})
self.assertEqual(True, not (lpe is None))
# cancelling sales invoice
@@ -118,7 +118,7 @@
si_original.submit()
earned_points = get_points_earned(si_original)
- lpe_original = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_original.name, 'customer': si_original.customer})
+ lpe_original = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer})
self.assertEqual(lpe_original.loyalty_points, earned_points)
# create sales invoice return
@@ -130,10 +130,10 @@
si_return.submit()
# fetch original invoice again as its status would have been updated
- si_original = frappe.get_doc('Sales Invoice', lpe_original.sales_invoice)
+ si_original = frappe.get_doc('Sales Invoice', lpe_original.invoice)
earned_points = get_points_earned(si_original)
- lpe_after_return = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_original.name, 'customer': si_original.customer})
+ lpe_after_return = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer})
self.assertEqual(lpe_after_return.loyalty_points, earned_points)
self.assertEqual(True, (lpe_original.loyalty_points > lpe_after_return.loyalty_points))
diff --git a/erpnext/accounts/page/pos/__init__.py b/erpnext/accounts/doctype/pos_closing_entry/__init__.py
similarity index 100%
copy from erpnext/accounts/page/pos/__init__.py
copy to erpnext/accounts/doctype/pos_closing_entry/__init__.py
diff --git a/erpnext/selling/doctype/pos_closing_voucher/closing_voucher_details.html b/erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html
similarity index 71%
rename from erpnext/selling/doctype/pos_closing_voucher/closing_voucher_details.html
rename to erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html
index 2412b07..983f495 100644
--- a/erpnext/selling/doctype/pos_closing_voucher/closing_voucher_details.html
+++ b/erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html
@@ -12,15 +12,15 @@
</thead>
<tbody>
<tr>
- <td class="text-left">{{ _('Grand Total') }}</td>
- <td class='text-right'>{{ data.grand_total or '' }} {{ currency.symbol }}</td>
+ <td class="text-left font-bold">{{ _('Grand Total') }}</td>
+ <td class='text-right'> {{ frappe.utils.fmt_money(data.grand_total or '', currency=currency) }}</td>
</tr>
<tr>
- <td class="text-left">{{ _('Net Total') }}</td>
- <td class='text-right'>{{ data.net_total or '' }} {{ currency.symbol }}</td>
+ <td class="text-left font-bold">{{ _('Net Total') }}</td>
+ <td class='text-right'> {{ frappe.utils.fmt_money(data.net_total or '', currency=currency) }}</td>
</tr>
<tr>
- <td class="text-left">{{ _('Total Quantity') }}</td>
+ <td class="text-left font-bold">{{ _('Total Quantity') }}</td>
<td class='text-right'>{{ data.total_quantity or '' }}</td>
</tr>
@@ -45,7 +45,7 @@
{% for d in data.payment_reconciliation %}
<tr>
<td class="text-left">{{ d.mode_of_payment }}</td>
- <td class='text-right'>{{ d.expected_amount }} {{ currency.symbol }}</td>
+ <td class='text-right'> {{ frappe.utils.fmt_money(d.expected_amount - d.opening_amount, currency=currency) }}</td>
</tr>
{% endfor %}
</tbody>
@@ -55,12 +55,14 @@
<!-- Section end -->
<!-- Taxes section -->
+ {% if data.taxes %}
<div>
<h6 class="text-center uppercase" style="color: #8D99A6">{{ _("Taxes") }}</h6>
<div class="tax-break-up" style="overflow-x: auto;">
<table class="table table-bordered table-hover">
<thead>
<tr>
+ <th class="text-left">{{ _("Account") }}</th>
<th class="text-left">{{ _("Rate") }}</th>
<th class="text-right">{{ _("Amount") }}</th>
</tr>
@@ -68,14 +70,16 @@
<tbody>
{% for d in data.taxes %}
<tr>
+ <td class="text-left">{{ d.account_head }}</td>
<td class="text-left">{{ d.rate }} %</td>
- <td class='text-right'>{{ d.amount }} {{ currency.symbol }}</td>
+ <td class='text-right'> {{ frappe.utils.fmt_money(d.amount, currency=currency) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
+ {% endif %}
<!-- Section end -->
</div>
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
new file mode 100644
index 0000000..8dcd2e4
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
@@ -0,0 +1,149 @@
+// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('POS Closing Entry', {
+ onload: function(frm) {
+ frm.set_query("pos_profile", function(doc) {
+ return {
+ filters: { 'user': doc.user }
+ };
+ });
+
+ frm.set_query("user", function(doc) {
+ return {
+ query: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_cashiers",
+ filters: { 'parent': doc.pos_profile }
+ };
+ });
+
+ frm.set_query("pos_opening_entry", function(doc) {
+ return { filters: { 'status': 'Open', 'docstatus': 1 } };
+ });
+
+ if (frm.doc.docstatus === 0) frm.set_value("period_end_date", frappe.datetime.now_datetime());
+ if (frm.doc.docstatus === 1) set_html_data(frm);
+ },
+
+ pos_opening_entry(frm) {
+ if (frm.doc.pos_opening_entry && frm.doc.period_start_date && frm.doc.period_end_date && frm.doc.user) {
+ reset_values(frm);
+ frm.trigger("set_opening_amounts");
+ frm.trigger("get_pos_invoices");
+ }
+ },
+
+ set_opening_amounts(frm) {
+ frappe.db.get_doc("POS Opening Entry", frm.doc.pos_opening_entry)
+ .then(({ balance_details }) => {
+ balance_details.forEach(detail => {
+ frm.add_child("payment_reconciliation", {
+ mode_of_payment: detail.mode_of_payment,
+ opening_amount: detail.opening_amount,
+ expected_amount: detail.opening_amount
+ });
+ })
+ });
+ },
+
+ get_pos_invoices(frm) {
+ frappe.call({
+ method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices',
+ args: {
+ start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
+ end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
+ user: frm.doc.user
+ },
+ callback: (r) => {
+ let pos_docs = r.message;
+ set_form_data(pos_docs, frm)
+ refresh_fields(frm)
+ set_html_data(frm)
+ }
+ })
+ }
+});
+
+frappe.ui.form.on('POS Closing Entry Detail', {
+ closing_amount: (frm, cdt, cdn) => {
+ const row = locals[cdt][cdn];
+ frappe.model.set_value(cdt, cdn, "difference", flt(row.expected_amount - row.closing_amount))
+ }
+})
+
+function set_form_data(data, frm) {
+ data.forEach(d => {
+ add_to_pos_transaction(d, frm);
+ frm.doc.grand_total += flt(d.grand_total);
+ frm.doc.net_total += flt(d.net_total);
+ frm.doc.total_quantity += flt(d.total_qty);
+ add_to_payments(d, frm);
+ add_to_taxes(d, frm);
+ });
+}
+
+function add_to_pos_transaction(d, frm) {
+ frm.add_child("pos_transactions", {
+ pos_invoice: d.name,
+ posting_date: d.posting_date,
+ grand_total: d.grand_total,
+ customer: d.customer
+ })
+}
+
+function add_to_payments(d, frm) {
+ d.payments.forEach(p => {
+ const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment);
+ if (payment) {
+ payment.expected_amount += flt(p.amount);
+ } else {
+ frm.add_child("payment_reconciliation", {
+ mode_of_payment: p.mode_of_payment,
+ opening_amount: 0,
+ expected_amount: p.amount
+ })
+ }
+ })
+}
+
+function add_to_taxes(d, frm) {
+ d.taxes.forEach(t => {
+ const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate);
+ if (tax) {
+ tax.amount += flt(t.tax_amount);
+ } else {
+ frm.add_child("taxes", {
+ account_head: t.account_head,
+ rate: t.rate,
+ amount: t.tax_amount
+ })
+ }
+ })
+}
+
+function reset_values(frm) {
+ frm.set_value("pos_transactions", []);
+ frm.set_value("payment_reconciliation", []);
+ frm.set_value("taxes", []);
+ frm.set_value("grand_total", 0);
+ frm.set_value("net_total", 0);
+ frm.set_value("total_quantity", 0);
+}
+
+function refresh_fields(frm) {
+ frm.refresh_field("pos_transactions");
+ frm.refresh_field("payment_reconciliation");
+ frm.refresh_field("taxes");
+ frm.refresh_field("grand_total");
+ frm.refresh_field("net_total");
+ frm.refresh_field("total_quantity");
+}
+
+function set_html_data(frm) {
+ frappe.call({
+ method: "get_payment_reconciliation_details",
+ doc: frm.doc,
+ callback: (r) => {
+ frm.get_field("payment_reconciliation_details").$wrapper.html(r.message);
+ }
+ })
+}
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
new file mode 100644
index 0000000..32bca3b
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
@@ -0,0 +1,242 @@
+{
+ "actions": [],
+ "autoname": "POS-CLO-.YYYY.-.#####",
+ "creation": "2018-05-28 19:06:40.830043",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "period_start_date",
+ "period_end_date",
+ "column_break_3",
+ "posting_date",
+ "pos_opening_entry",
+ "section_break_5",
+ "company",
+ "column_break_7",
+ "pos_profile",
+ "user",
+ "section_break_12",
+ "pos_transactions",
+ "section_break_9",
+ "payment_reconciliation_details",
+ "section_break_11",
+ "payment_reconciliation",
+ "section_break_13",
+ "grand_total",
+ "net_total",
+ "total_quantity",
+ "column_break_16",
+ "taxes",
+ "section_break_14",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fetch_from": "pos_opening_entry.period_start_date",
+ "fieldname": "period_start_date",
+ "fieldtype": "Datetime",
+ "in_list_view": 1,
+ "label": "Period Start Date",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "default": "Today",
+ "fieldname": "period_end_date",
+ "fieldtype": "Datetime",
+ "in_list_view": 1,
+ "label": "Period End Date",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "Today",
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Posting Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "pos_opening_entry.pos_profile",
+ "fieldname": "pos_profile",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "POS Profile",
+ "options": "POS Profile",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "pos_opening_entry.user",
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "label": "Cashier",
+ "options": "User",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.docstatus==1",
+ "fieldname": "payment_reconciliation_details",
+ "fieldtype": "HTML"
+ },
+ {
+ "fieldname": "section_break_11",
+ "fieldtype": "Section Break",
+ "label": "Modes of Payment"
+ },
+ {
+ "fieldname": "payment_reconciliation",
+ "fieldtype": "Table",
+ "label": "Payment Reconciliation",
+ "options": "POS Closing Entry Detail"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "eval:doc.docstatus==0",
+ "fieldname": "section_break_13",
+ "fieldtype": "Section Break",
+ "label": "Details"
+ },
+ {
+ "default": "0",
+ "fieldname": "grand_total",
+ "fieldtype": "Currency",
+ "label": "Grand Total",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "net_total",
+ "fieldtype": "Currency",
+ "label": "Net Total",
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_quantity",
+ "fieldtype": "Float",
+ "label": "Total Quantity",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_16",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "taxes",
+ "fieldtype": "Table",
+ "label": "Taxes",
+ "options": "POS Closing Entry Taxes",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_12",
+ "fieldtype": "Section Break",
+ "label": "Linked Invoices"
+ },
+ {
+ "fieldname": "section_break_14",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "POS Closing Entry",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "pos_transactions",
+ "fieldtype": "Table",
+ "label": "POS Transactions",
+ "options": "POS Invoice Reference",
+ "reqd": 1
+ },
+ {
+ "fieldname": "pos_opening_entry",
+ "fieldtype": "Link",
+ "label": "POS Opening Entry",
+ "options": "POS Opening Entry",
+ "reqd": 1
+ }
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-05-29 15:03:22.226113",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "POS Closing Entry",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Administrator",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
new file mode 100644
index 0000000..8eb0a22
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
@@ -0,0 +1,127 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import json
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import getdate, get_datetime, flt
+from collections import defaultdict
+from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
+from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
+
+class POSClosingEntry(Document):
+ def validate(self):
+ user = frappe.get_all('POS Closing Entry',
+ filters = { 'user': self.user, 'docstatus': 1 },
+ or_filters = {
+ 'period_start_date': ('between', [self.period_start_date, self.period_end_date]),
+ 'period_end_date': ('between', [self.period_start_date, self.period_end_date])
+ })
+
+ if user:
+ frappe.throw(_("POS Closing Entry {} against {} between selected period"
+ .format(frappe.bold("already exists"), frappe.bold(self.user))), title=_("Invalid Period"))
+
+ if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
+ frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
+
+ def on_submit(self):
+ merge_pos_invoices(self.pos_transactions)
+ opening_entry = frappe.get_doc("POS Opening Entry", self.pos_opening_entry)
+ opening_entry.pos_closing_entry = self.name
+ opening_entry.set_status()
+ opening_entry.save()
+
+ def get_payment_reconciliation_details(self):
+ currency = frappe.get_cached_value('Company', self.company, "default_currency")
+ return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html",
+ {"data": self, "currency": currency})
+
+@frappe.whitelist()
+def get_cashiers(doctype, txt, searchfield, start, page_len, filters):
+ cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=['user'])
+ return [c['user'] for c in cashiers_list]
+
+@frappe.whitelist()
+def get_pos_invoices(start, end, user):
+ data = frappe.db.sql("""
+ select
+ name, timestamp(posting_date, posting_time) as "timestamp"
+ from
+ `tabPOS Invoice`
+ where
+ owner = %s and docstatus = 1 and
+ (consolidated_invoice is NULL or consolidated_invoice = '')
+ """, (user), as_dict=1)
+
+ data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data))
+ # need to get taxes and payments so can't avoid get_doc
+ data = [frappe.get_doc("POS Invoice", d.name).as_dict() for d in data]
+
+ return data
+
+def make_closing_entry_from_opening(opening_entry):
+ closing_entry = frappe.new_doc("POS Closing Entry")
+ closing_entry.pos_opening_entry = opening_entry.name
+ closing_entry.period_start_date = opening_entry.period_start_date
+ closing_entry.period_end_date = frappe.utils.get_datetime()
+ closing_entry.pos_profile = opening_entry.pos_profile
+ closing_entry.user = opening_entry.user
+ closing_entry.company = opening_entry.company
+ closing_entry.grand_total = 0
+ closing_entry.net_total = 0
+ closing_entry.total_quantity = 0
+
+ invoices = get_pos_invoices(closing_entry.period_start_date, closing_entry.period_end_date, closing_entry.user)
+
+ pos_transactions = []
+ taxes = []
+ payments = []
+ for detail in opening_entry.balance_details:
+ payments.append(frappe._dict({
+ 'mode_of_payment': detail.mode_of_payment,
+ 'opening_amount': detail.opening_amount,
+ 'expected_amount': detail.opening_amount
+ }))
+
+ for d in invoices:
+ pos_transactions.append(frappe._dict({
+ 'pos_invoice': d.name,
+ 'posting_date': d.posting_date,
+ 'grand_total': d.grand_total,
+ 'customer': d.customer
+ }))
+ closing_entry.grand_total += flt(d.grand_total)
+ closing_entry.net_total += flt(d.net_total)
+ closing_entry.total_quantity += flt(d.total_qty)
+
+ for t in d.taxes:
+ existing_tax = [tx for tx in taxes if tx.account_head == t.account_head and tx.rate == t.rate]
+ if existing_tax:
+ existing_tax[0].amount += flt(t.tax_amount);
+ else:
+ taxes.append(frappe._dict({
+ 'account_head': t.account_head,
+ 'rate': t.rate,
+ 'amount': t.tax_amount
+ }))
+
+ for p in d.payments:
+ existing_pay = [pay for pay in payments if pay.mode_of_payment == p.mode_of_payment]
+ if existing_pay:
+ existing_pay[0].expected_amount += flt(p.amount);
+ else:
+ payments.append(frappe._dict({
+ 'mode_of_payment': p.mode_of_payment,
+ 'opening_amount': 0,
+ 'expected_amount': p.amount
+ }))
+
+ closing_entry.set("pos_transactions", pos_transactions)
+ closing_entry.set("payment_reconciliation", payments)
+ closing_entry.set("taxes", taxes)
+
+ return closing_entry
diff --git a/erpnext/selling/doctype/pos_closing_voucher/test_pos_closing_voucher.js b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.js
similarity index 68%
rename from erpnext/selling/doctype/pos_closing_voucher/test_pos_closing_voucher.js
rename to erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.js
index 7633815..48109b1 100644
--- a/erpnext/selling/doctype/pos_closing_voucher/test_pos_closing_voucher.js
+++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.js
@@ -2,15 +2,15 @@
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
-QUnit.test("test: POS Closing Voucher", function (assert) {
+QUnit.test("test: POS Closing Entry", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
- // insert a new POS Closing Voucher
- () => frappe.tests.make('POS Closing Voucher', [
+ // insert a new POS Closing Entry
+ () => frappe.tests.make('POS Closing Entry', [
// values to be set
{key: 'value'}
]),
diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
new file mode 100644
index 0000000..aa6a388
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+import frappe
+import unittest
+from frappe.utils import nowdate
+from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
+from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import make_closing_entry_from_opening
+from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
+from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
+
+class TestPOSClosingEntry(unittest.TestCase):
+ def test_pos_closing_entry(self):
+ test_user, pos_profile = init_user_and_profile()
+
+ opening_entry = create_opening_entry(pos_profile, test_user.name)
+
+ pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
+ pos_inv1.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3500
+ })
+ pos_inv1.submit()
+
+ pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
+ pos_inv2.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
+ })
+ pos_inv2.submit()
+
+ pcv_doc = make_closing_entry_from_opening(opening_entry)
+ payment = pcv_doc.payment_reconciliation[0]
+
+ self.assertEqual(payment.mode_of_payment, 'Cash')
+
+ for d in pcv_doc.payment_reconciliation:
+ if d.mode_of_payment == 'Cash':
+ d.closing_amount = 6700
+
+ pcv_doc.submit()
+
+ self.assertEqual(pcv_doc.total_quantity, 2)
+ self.assertEqual(pcv_doc.net_total, 6700)
+
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabPOS Profile`")
+
+def init_user_and_profile():
+ user = 'test@example.com'
+ test_user = frappe.get_doc('User', user)
+
+ roles = ("Accounts Manager", "Accounts User", "Sales Manager")
+ test_user.add_roles(*roles)
+ frappe.set_user(user)
+
+ pos_profile = make_pos_profile()
+ pos_profile.append('applicable_for_users', {
+ 'default': 1,
+ 'user': user
+ })
+
+ pos_profile.save()
+
+ return test_user, pos_profile
\ No newline at end of file
diff --git a/erpnext/accounts/page/pos/__init__.py b/erpnext/accounts/doctype/pos_closing_entry_detail/__init__.py
similarity index 100%
copy from erpnext/accounts/page/pos/__init__.py
copy to erpnext/accounts/doctype/pos_closing_entry_detail/__init__.py
diff --git a/erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.json b/erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.json
new file mode 100644
index 0000000..798637a
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.json
@@ -0,0 +1,70 @@
+{
+ "actions": [],
+ "creation": "2018-05-28 19:10:47.580174",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "mode_of_payment",
+ "opening_amount",
+ "closing_amount",
+ "expected_amount",
+ "difference"
+ ],
+ "fields": [
+ {
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Mode of Payment",
+ "options": "Mode of Payment",
+ "reqd": 1
+ },
+ {
+ "fieldname": "expected_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Expected Amount",
+ "options": "company:company_currency",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "difference",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Difference",
+ "options": "company:company_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "opening_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Opening Amount",
+ "options": "company:company_currency",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "closing_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Closing Amount",
+ "options": "company:company_currency",
+ "reqd": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-05-29 15:03:34.533607",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "POS Closing Entry Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/pos_closing_voucher_taxes/pos_closing_voucher_taxes.py b/erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.py
similarity index 84%
rename from erpnext/selling/doctype/pos_closing_voucher_taxes/pos_closing_voucher_taxes.py
rename to erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.py
index 87ce842..46b6c77 100644
--- a/erpnext/selling/doctype/pos_closing_voucher_taxes/pos_closing_voucher_taxes.py
+++ b/erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.py
@@ -5,5 +5,5 @@
from __future__ import unicode_literals
from frappe.model.document import Document
-class POSClosingVoucherTaxes(Document):
+class POSClosingEntryDetail(Document):
pass
diff --git a/erpnext/accounts/page/pos/__init__.py b/erpnext/accounts/doctype/pos_closing_entry_taxes/__init__.py
similarity index 100%
copy from erpnext/accounts/page/pos/__init__.py
copy to erpnext/accounts/doctype/pos_closing_entry_taxes/__init__.py
diff --git a/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.json b/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.json
new file mode 100644
index 0000000..42e7d0e
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.json
@@ -0,0 +1,48 @@
+{
+ "actions": [],
+ "creation": "2018-05-30 09:11:22.535470",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "account_head",
+ "rate",
+ "amount"
+ ],
+ "fields": [
+ {
+ "fieldname": "rate",
+ "fieldtype": "Percent",
+ "in_list_view": 1,
+ "label": "Rate",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Amount",
+ "read_only": 1
+ },
+ {
+ "fieldname": "account_head",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Account Head",
+ "options": "Account",
+ "read_only": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-05-29 15:03:39.872884",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "POS Closing Entry Taxes",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/pos_closing_voucher_taxes/pos_closing_voucher_taxes.py b/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.py
similarity index 84%
copy from erpnext/selling/doctype/pos_closing_voucher_taxes/pos_closing_voucher_taxes.py
copy to erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.py
index 87ce842..f72d9a6 100644
--- a/erpnext/selling/doctype/pos_closing_voucher_taxes/pos_closing_voucher_taxes.py
+++ b/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.py
@@ -5,5 +5,5 @@
from __future__ import unicode_literals
from frappe.model.document import Document
-class POSClosingVoucherTaxes(Document):
+class POSClosingEntryTaxes(Document):
pass
diff --git a/erpnext/accounts/page/pos/__init__.py b/erpnext/accounts/doctype/pos_invoice/__init__.py
similarity index 100%
rename from erpnext/accounts/page/pos/__init__.py
rename to erpnext/accounts/doctype/pos_invoice/__init__.py
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
new file mode 100644
index 0000000..3be4304
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
@@ -0,0 +1,205 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+{% include 'erpnext/selling/sales_common.js' %};
+
+erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend({
+ setup(doc) {
+ this.setup_posting_date_time_check();
+ this._super(doc);
+ },
+
+ onload() {
+ this._super();
+ if(this.frm.doc.__islocal && this.frm.doc.is_pos) {
+ //Load pos profile data on the invoice if the default value of Is POS is 1
+
+ me.frm.script_manager.trigger("is_pos");
+ me.frm.refresh_fields();
+ }
+ },
+
+ refresh(doc) {
+ this._super();
+ if (doc.docstatus == 1 && !doc.is_return) {
+ if(doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) {
+ cur_frm.add_custom_button(__('Return'),
+ this.make_sales_return, __('Create'));
+ cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
+ }
+ }
+
+ if (this.frm.doc.is_return) {
+ this.frm.return_print_format = "Sales Invoice Return";
+ cur_frm.set_value('consolidated_invoice', '');
+ }
+ },
+
+ is_pos: function(frm){
+ this.set_pos_data();
+ },
+
+ set_pos_data: function() {
+ if(this.frm.doc.is_pos) {
+ this.frm.set_value("allocate_advances_automatically", 0);
+ if(!this.frm.doc.company) {
+ this.frm.set_value("is_pos", 0);
+ frappe.msgprint(__("Please specify Company to proceed"));
+ } else {
+ var me = this;
+ return this.frm.call({
+ doc: me.frm.doc,
+ method: "set_missing_values",
+ callback: function(r) {
+ if(!r.exc) {
+ if(r.message) {
+ me.frm.pos_print_format = r.message.print_format || "";
+ me.frm.meta.default_print_format = r.message.print_format || "";
+ me.frm.allow_edit_rate = r.message.allow_edit_rate;
+ me.frm.allow_edit_discount = r.message.allow_edit_discount;
+ me.frm.doc.campaign = r.message.campaign;
+ me.frm.allow_print_before_pay = r.message.allow_print_before_pay;
+ }
+ me.frm.script_manager.trigger("update_stock");
+ me.calculate_taxes_and_totals();
+ if(me.frm.doc.taxes_and_charges) {
+ me.frm.script_manager.trigger("taxes_and_charges");
+ }
+ frappe.model.set_default_values(me.frm.doc);
+ me.set_dynamic_labels();
+
+ }
+ }
+ });
+ }
+ }
+ else this.frm.trigger("refresh");
+ },
+
+ customer() {
+ if (!this.frm.doc.customer) return
+
+ if (this.frm.doc.is_pos){
+ var pos_profile = this.frm.doc.pos_profile;
+ }
+ var me = this;
+ if(this.frm.updating_party_details) return;
+ erpnext.utils.get_party_details(this.frm,
+ "erpnext.accounts.party.get_party_details", {
+ posting_date: this.frm.doc.posting_date,
+ party: this.frm.doc.customer,
+ party_type: "Customer",
+ account: this.frm.doc.debit_to,
+ price_list: this.frm.doc.selling_price_list,
+ pos_profile: pos_profile
+ }, function() {
+ me.apply_pricing_rule();
+ });
+ },
+
+ amount: function(){
+ this.write_off_outstanding_amount_automatically()
+ },
+
+ change_amount: function(){
+ if(this.frm.doc.paid_amount > this.frm.doc.grand_total){
+ this.calculate_write_off_amount();
+ }else {
+ this.frm.set_value("change_amount", 0.0);
+ this.frm.set_value("base_change_amount", 0.0);
+ }
+
+ this.frm.refresh_fields();
+ },
+
+ loyalty_amount: function(){
+ this.calculate_outstanding_amount();
+ this.frm.refresh_field("outstanding_amount");
+ this.frm.refresh_field("paid_amount");
+ this.frm.refresh_field("base_paid_amount");
+ },
+
+ write_off_outstanding_amount_automatically: function() {
+ if(cint(this.frm.doc.write_off_outstanding_amount_automatically)) {
+ frappe.model.round_floats_in(this.frm.doc, ["grand_total", "paid_amount"]);
+ // this will make outstanding amount 0
+ this.frm.set_value("write_off_amount",
+ flt(this.frm.doc.grand_total - this.frm.doc.paid_amount - this.frm.doc.total_advance, precision("write_off_amount"))
+ );
+ this.frm.toggle_enable("write_off_amount", false);
+
+ } else {
+ this.frm.toggle_enable("write_off_amount", true);
+ }
+
+ this.calculate_outstanding_amount(false);
+ this.frm.refresh_fields();
+ },
+
+ make_sales_return: function() {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return",
+ frm: cur_frm
+ })
+ },
+})
+
+$.extend(cur_frm.cscript, new erpnext.selling.POSInvoiceController({ frm: cur_frm }))
+
+frappe.ui.form.on('POS Invoice', {
+ redeem_loyalty_points: function(frm) {
+ frm.events.get_loyalty_details(frm);
+ },
+
+ loyalty_points: function(frm) {
+ if (frm.redemption_conversion_factor) {
+ frm.events.set_loyalty_points(frm);
+ } else {
+ frappe.call({
+ method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_redeemption_factor",
+ args: {
+ "loyalty_program": frm.doc.loyalty_program
+ },
+ callback: function(r) {
+ if (r) {
+ frm.redemption_conversion_factor = r.message;
+ frm.events.set_loyalty_points(frm);
+ }
+ }
+ });
+ }
+ },
+
+ get_loyalty_details: function(frm) {
+ if (frm.doc.customer && frm.doc.redeem_loyalty_points) {
+ frappe.call({
+ method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_program_details",
+ args: {
+ "customer": frm.doc.customer,
+ "loyalty_program": frm.doc.loyalty_program,
+ "expiry_date": frm.doc.posting_date,
+ "company": frm.doc.company
+ },
+ callback: function(r) {
+ if (r) {
+ frm.set_value("loyalty_redemption_account", r.message.expense_account);
+ frm.set_value("loyalty_redemption_cost_center", r.message.cost_center);
+ frm.redemption_conversion_factor = r.message.conversion_factor;
+ }
+ }
+ });
+ }
+ },
+
+ set_loyalty_points: function(frm) {
+ if (frm.redemption_conversion_factor) {
+ let loyalty_amount = flt(frm.redemption_conversion_factor*flt(frm.doc.loyalty_points), precision("loyalty_amount"));
+ var remaining_amount = flt(frm.doc.grand_total) - flt(frm.doc.total_advance) - flt(frm.doc.write_off_amount);
+ if (frm.doc.grand_total && (remaining_amount < loyalty_amount)) {
+ let redeemable_points = parseInt(remaining_amount/frm.redemption_conversion_factor);
+ frappe.throw(__("You can only redeem max {0} points in this order.",[redeemable_points]));
+ }
+ frm.set_value("loyalty_amount", loyalty_amount);
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
new file mode 100644
index 0000000..2a2e3df
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
@@ -0,0 +1,1637 @@
+{
+ "actions": [],
+ "allow_import": 1,
+ "autoname": "naming_series:",
+ "creation": "2020-01-24 15:29:29.933693",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "customer_section",
+ "title",
+ "naming_series",
+ "customer",
+ "customer_name",
+ "tax_id",
+ "is_pos",
+ "pos_profile",
+ "offline_pos_name",
+ "is_return",
+ "consolidated_invoice",
+ "column_break1",
+ "company",
+ "posting_date",
+ "posting_time",
+ "set_posting_time",
+ "due_date",
+ "amended_from",
+ "returns",
+ "return_against",
+ "column_break_21",
+ "update_billed_amount_in_sales_order",
+ "accounting_dimensions_section",
+ "project",
+ "dimension_col_break",
+ "cost_center",
+ "customer_po_details",
+ "po_no",
+ "column_break_23",
+ "po_date",
+ "address_and_contact",
+ "customer_address",
+ "address_display",
+ "contact_person",
+ "contact_display",
+ "contact_mobile",
+ "contact_email",
+ "territory",
+ "col_break4",
+ "shipping_address_name",
+ "shipping_address",
+ "company_address",
+ "company_address_display",
+ "currency_and_price_list",
+ "currency",
+ "conversion_rate",
+ "column_break2",
+ "selling_price_list",
+ "price_list_currency",
+ "plc_conversion_rate",
+ "ignore_pricing_rule",
+ "sec_warehouse",
+ "set_warehouse",
+ "items_section",
+ "update_stock",
+ "scan_barcode",
+ "items",
+ "pricing_rule_details",
+ "pricing_rules",
+ "packing_list",
+ "packed_items",
+ "product_bundle_help",
+ "time_sheet_list",
+ "timesheets",
+ "total_billing_amount",
+ "section_break_30",
+ "total_qty",
+ "base_total",
+ "base_net_total",
+ "column_break_32",
+ "total",
+ "net_total",
+ "total_net_weight",
+ "taxes_section",
+ "taxes_and_charges",
+ "column_break_38",
+ "shipping_rule",
+ "tax_category",
+ "section_break_40",
+ "taxes",
+ "sec_tax_breakup",
+ "other_charges_calculation",
+ "section_break_43",
+ "base_total_taxes_and_charges",
+ "column_break_47",
+ "total_taxes_and_charges",
+ "loyalty_points_redemption",
+ "loyalty_points",
+ "loyalty_amount",
+ "redeem_loyalty_points",
+ "column_break_77",
+ "loyalty_program",
+ "loyalty_redemption_account",
+ "loyalty_redemption_cost_center",
+ "section_break_49",
+ "apply_discount_on",
+ "base_discount_amount",
+ "column_break_51",
+ "additional_discount_percentage",
+ "discount_amount",
+ "totals",
+ "base_grand_total",
+ "base_rounding_adjustment",
+ "base_rounded_total",
+ "base_in_words",
+ "column_break5",
+ "grand_total",
+ "rounding_adjustment",
+ "rounded_total",
+ "in_words",
+ "total_advance",
+ "outstanding_amount",
+ "advances_section",
+ "allocate_advances_automatically",
+ "get_advances",
+ "advances",
+ "payment_schedule_section",
+ "payment_terms_template",
+ "payment_schedule",
+ "payments_section",
+ "cash_bank_account",
+ "payments",
+ "section_break_84",
+ "base_paid_amount",
+ "column_break_86",
+ "paid_amount",
+ "section_break_88",
+ "base_change_amount",
+ "column_break_90",
+ "change_amount",
+ "account_for_change_amount",
+ "column_break4",
+ "write_off_amount",
+ "base_write_off_amount",
+ "write_off_outstanding_amount_automatically",
+ "column_break_74",
+ "write_off_account",
+ "write_off_cost_center",
+ "terms_section_break",
+ "tc_name",
+ "terms",
+ "edit_printing_settings",
+ "letter_head",
+ "group_same_items",
+ "language",
+ "column_break_84",
+ "select_print_heading",
+ "more_information",
+ "inter_company_invoice_reference",
+ "customer_group",
+ "campaign",
+ "is_discounted",
+ "col_break23",
+ "status",
+ "source",
+ "more_info",
+ "debit_to",
+ "party_account_currency",
+ "is_opening",
+ "c_form_applicable",
+ "c_form_no",
+ "column_break8",
+ "remarks",
+ "sales_team_section_break",
+ "sales_partner",
+ "column_break10",
+ "commission_rate",
+ "total_commission",
+ "section_break2",
+ "sales_team",
+ "subscription_section",
+ "from_date",
+ "to_date",
+ "column_break_140",
+ "auto_repeat",
+ "update_auto_repeat_reference",
+ "against_income_account",
+ "pos_total_qty"
+ ],
+ "fields": [
+ {
+ "fieldname": "customer_section",
+ "fieldtype": "Section Break",
+ "options": "fa fa-user"
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "{customer_name}",
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Title",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "bold": 1,
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Series",
+ "no_copy": 1,
+ "oldfieldname": "naming_series",
+ "oldfieldtype": "Select",
+ "options": "ACC-PSINV-.YYYY.-",
+ "print_hide": 1,
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "bold": 1,
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "in_standard_filter": 1,
+ "label": "Customer",
+ "oldfieldname": "customer",
+ "oldfieldtype": "Link",
+ "options": "Customer",
+ "print_hide": 1,
+ "search_index": 1
+ },
+ {
+ "bold": 1,
+ "depends_on": "customer",
+ "fetch_from": "customer.customer_name",
+ "fieldname": "customer_name",
+ "fieldtype": "Data",
+ "in_global_search": 1,
+ "label": "Customer Name",
+ "oldfieldname": "customer_name",
+ "oldfieldtype": "Data",
+ "read_only": 1
+ },
+ {
+ "fieldname": "tax_id",
+ "fieldtype": "Data",
+ "label": "Tax Id",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "is_pos",
+ "fieldtype": "Check",
+ "label": "Include Payment (POS)",
+ "oldfieldname": "is_pos",
+ "oldfieldtype": "Check",
+ "print_hide": 1,
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "depends_on": "is_pos",
+ "fieldname": "pos_profile",
+ "fieldtype": "Link",
+ "label": "POS Profile",
+ "options": "POS Profile",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "offline_pos_name",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Offline POS Name",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "0",
+ "fieldname": "is_return",
+ "fieldtype": "Check",
+ "label": "Is Return (Credit Note)",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "fieldname": "column_break1",
+ "fieldtype": "Column Break",
+ "oldfieldtype": "Column Break"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_standard_filter": 1,
+ "label": "Company",
+ "oldfieldname": "company",
+ "oldfieldtype": "Link",
+ "options": "Company",
+ "print_hide": 1,
+ "remember_last_selected_value": 1,
+ "reqd": 1
+ },
+ {
+ "bold": 1,
+ "default": "Today",
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "label": "Date",
+ "no_copy": 1,
+ "oldfieldname": "posting_date",
+ "oldfieldtype": "Date",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "fieldname": "posting_time",
+ "fieldtype": "Time",
+ "label": "Posting Time",
+ "no_copy": 1,
+ "oldfieldname": "posting_time",
+ "oldfieldtype": "Time",
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.docstatus==0",
+ "fieldname": "set_posting_time",
+ "fieldtype": "Check",
+ "label": "Edit Posting Date and Time",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "due_date",
+ "fieldtype": "Date",
+ "label": "Payment Due Date",
+ "no_copy": 1,
+ "oldfieldname": "due_date",
+ "oldfieldtype": "Date"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "label": "Amended From",
+ "no_copy": 1,
+ "oldfieldname": "amended_from",
+ "oldfieldtype": "Link",
+ "options": "POS Invoice",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "depends_on": "return_against",
+ "fieldname": "returns",
+ "fieldtype": "Section Break",
+ "label": "Returns"
+ },
+ {
+ "depends_on": "return_against",
+ "fieldname": "return_against",
+ "fieldtype": "Link",
+ "label": "Return Against POS Invoice",
+ "no_copy": 1,
+ "options": "POS Invoice",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_21",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.is_return && doc.return_against",
+ "fieldname": "update_billed_amount_in_sales_order",
+ "fieldtype": "Check",
+ "label": "Update Billed Amount in Sales Order"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "accounting_dimensions_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Dimensions"
+ },
+ {
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "in_global_search": 1,
+ "label": "Project",
+ "oldfieldname": "project_name",
+ "oldfieldtype": "Link",
+ "options": "Project",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "dimension_col_break",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "po_no",
+ "fieldname": "customer_po_details",
+ "fieldtype": "Section Break",
+ "label": "Customer PO Details"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "po_no",
+ "fieldtype": "Data",
+ "label": "Customer's Purchase Order",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "fieldname": "column_break_23",
+ "fieldtype": "Column Break"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "po_date",
+ "fieldtype": "Date",
+ "label": "Customer's Purchase Order Date"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "address_and_contact",
+ "fieldtype": "Section Break",
+ "label": "Address and Contact"
+ },
+ {
+ "fieldname": "customer_address",
+ "fieldtype": "Link",
+ "label": "Customer Address",
+ "options": "Address",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "address_display",
+ "fieldtype": "Small Text",
+ "label": "Address",
+ "read_only": 1
+ },
+ {
+ "fieldname": "contact_person",
+ "fieldtype": "Link",
+ "in_global_search": 1,
+ "label": "Contact Person",
+ "options": "Contact",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "contact_display",
+ "fieldtype": "Small Text",
+ "label": "Contact",
+ "read_only": 1
+ },
+ {
+ "fieldname": "contact_mobile",
+ "fieldtype": "Small Text",
+ "hidden": 1,
+ "label": "Mobile No",
+ "read_only": 1
+ },
+ {
+ "fieldname": "contact_email",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Contact Email",
+ "options": "Email",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "territory",
+ "fieldtype": "Link",
+ "label": "Territory",
+ "options": "Territory",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "col_break4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "shipping_address_name",
+ "fieldtype": "Link",
+ "label": "Shipping Address Name",
+ "options": "Address",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "shipping_address",
+ "fieldtype": "Small Text",
+ "label": "Shipping Address",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "company_address",
+ "fieldtype": "Link",
+ "label": "Company Address Name",
+ "options": "Address",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "company_address_display",
+ "fieldtype": "Small Text",
+ "hidden": 1,
+ "label": "Company Address",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "depends_on": "customer",
+ "fieldname": "currency_and_price_list",
+ "fieldtype": "Section Break",
+ "label": "Currency and Price List"
+ },
+ {
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "label": "Currency",
+ "oldfieldname": "currency",
+ "oldfieldtype": "Select",
+ "options": "Currency",
+ "print_hide": 1,
+ "reqd": 1
+ },
+ {
+ "description": "Rate at which Customer Currency is converted to customer's base currency",
+ "fieldname": "conversion_rate",
+ "fieldtype": "Float",
+ "label": "Exchange Rate",
+ "oldfieldname": "conversion_rate",
+ "oldfieldtype": "Currency",
+ "precision": "9",
+ "print_hide": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break2",
+ "fieldtype": "Column Break",
+ "width": "50%"
+ },
+ {
+ "fieldname": "selling_price_list",
+ "fieldtype": "Link",
+ "label": "Price List",
+ "oldfieldname": "price_list_name",
+ "oldfieldtype": "Select",
+ "options": "Price List",
+ "print_hide": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "price_list_currency",
+ "fieldtype": "Link",
+ "label": "Price List Currency",
+ "options": "Currency",
+ "print_hide": 1,
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "description": "Rate at which Price list currency is converted to customer's base currency",
+ "fieldname": "plc_conversion_rate",
+ "fieldtype": "Float",
+ "label": "Price List Exchange Rate",
+ "precision": "9",
+ "print_hide": 1,
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "ignore_pricing_rule",
+ "fieldtype": "Check",
+ "label": "Ignore Pricing Rule",
+ "no_copy": 1,
+ "permlevel": 1,
+ "print_hide": 1
+ },
+ {
+ "fieldname": "sec_warehouse",
+ "fieldtype": "Section Break"
+ },
+ {
+ "depends_on": "update_stock",
+ "fieldname": "set_warehouse",
+ "fieldtype": "Link",
+ "label": "Set Source Warehouse",
+ "options": "Warehouse",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "items_section",
+ "fieldtype": "Section Break",
+ "oldfieldtype": "Section Break",
+ "options": "fa fa-shopping-cart"
+ },
+ {
+ "default": "0",
+ "fieldname": "update_stock",
+ "fieldtype": "Check",
+ "label": "Update Stock",
+ "oldfieldname": "update_stock",
+ "oldfieldtype": "Check",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "scan_barcode",
+ "fieldtype": "Data",
+ "label": "Scan Barcode"
+ },
+ {
+ "allow_bulk_edit": 1,
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "label": "Items",
+ "oldfieldname": "entries",
+ "oldfieldtype": "Table",
+ "options": "POS Invoice Item",
+ "reqd": 1
+ },
+ {
+ "fieldname": "pricing_rule_details",
+ "fieldtype": "Section Break",
+ "label": "Pricing Rules"
+ },
+ {
+ "fieldname": "pricing_rules",
+ "fieldtype": "Table",
+ "label": "Pricing Rule Detail",
+ "options": "Pricing Rule Detail",
+ "read_only": 1
+ },
+ {
+ "fieldname": "packing_list",
+ "fieldtype": "Section Break",
+ "label": "Packing List",
+ "options": "fa fa-suitcase",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "packed_items",
+ "fieldtype": "Table",
+ "label": "Packed Items",
+ "options": "Packed Item",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "product_bundle_help",
+ "fieldtype": "HTML",
+ "label": "Product Bundle Help",
+ "print_hide": 1
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "eval:doc.total_billing_amount > 0",
+ "fieldname": "time_sheet_list",
+ "fieldtype": "Section Break",
+ "label": "Time Sheet List"
+ },
+ {
+ "fieldname": "timesheets",
+ "fieldtype": "Table",
+ "label": "Time Sheets",
+ "options": "Sales Invoice Timesheet",
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "total_billing_amount",
+ "fieldtype": "Currency",
+ "label": "Total Billing Amount",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_30",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "total_qty",
+ "fieldtype": "Float",
+ "label": "Total Quantity",
+ "read_only": 1
+ },
+ {
+ "fieldname": "base_total",
+ "fieldtype": "Currency",
+ "label": "Total (Company Currency)",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "base_net_total",
+ "fieldtype": "Currency",
+ "label": "Net Total (Company Currency)",
+ "oldfieldname": "net_total",
+ "oldfieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_32",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "total",
+ "fieldtype": "Currency",
+ "label": "Total",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "net_total",
+ "fieldtype": "Currency",
+ "label": "Net Total",
+ "options": "currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_net_weight",
+ "fieldtype": "Float",
+ "label": "Total Net Weight",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "taxes_section",
+ "fieldtype": "Section Break",
+ "oldfieldtype": "Section Break",
+ "options": "fa fa-money"
+ },
+ {
+ "fieldname": "taxes_and_charges",
+ "fieldtype": "Link",
+ "label": "Sales Taxes and Charges Template",
+ "oldfieldname": "charge",
+ "oldfieldtype": "Link",
+ "options": "Sales Taxes and Charges Template",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "column_break_38",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "shipping_rule",
+ "fieldtype": "Link",
+ "label": "Shipping Rule",
+ "oldfieldtype": "Button",
+ "options": "Shipping Rule",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "tax_category",
+ "fieldtype": "Link",
+ "label": "Tax Category",
+ "options": "Tax Category",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "section_break_40",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "taxes",
+ "fieldtype": "Table",
+ "label": "Sales Taxes and Charges",
+ "oldfieldname": "other_charges",
+ "oldfieldtype": "Table",
+ "options": "Sales Taxes and Charges"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "sec_tax_breakup",
+ "fieldtype": "Section Break",
+ "label": "Tax Breakup"
+ },
+ {
+ "fieldname": "other_charges_calculation",
+ "fieldtype": "Long Text",
+ "label": "Taxes and Charges Calculation",
+ "no_copy": 1,
+ "oldfieldtype": "HTML",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_43",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "base_total_taxes_and_charges",
+ "fieldtype": "Currency",
+ "label": "Total Taxes and Charges (Company Currency)",
+ "oldfieldname": "other_charges_total",
+ "oldfieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_47",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "total_taxes_and_charges",
+ "fieldtype": "Currency",
+ "label": "Total Taxes and Charges",
+ "options": "currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "loyalty_points_redemption",
+ "fieldtype": "Section Break",
+ "label": "Loyalty Points Redemption"
+ },
+ {
+ "depends_on": "redeem_loyalty_points",
+ "fieldname": "loyalty_points",
+ "fieldtype": "Int",
+ "label": "Loyalty Points",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "depends_on": "redeem_loyalty_points",
+ "fieldname": "loyalty_amount",
+ "fieldtype": "Currency",
+ "label": "Loyalty Amount",
+ "no_copy": 1,
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "redeem_loyalty_points",
+ "fieldtype": "Check",
+ "label": "Redeem Loyalty Points",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "fieldname": "column_break_77",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "customer.loyalty_program",
+ "fieldname": "loyalty_program",
+ "fieldtype": "Link",
+ "label": "Loyalty Program",
+ "no_copy": 1,
+ "options": "Loyalty Program",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "depends_on": "redeem_loyalty_points",
+ "fieldname": "loyalty_redemption_account",
+ "fieldtype": "Link",
+ "label": "Redemption Account",
+ "no_copy": 1,
+ "options": "Account"
+ },
+ {
+ "depends_on": "redeem_loyalty_points",
+ "fieldname": "loyalty_redemption_cost_center",
+ "fieldtype": "Link",
+ "label": "Redemption Cost Center",
+ "no_copy": 1,
+ "options": "Cost Center"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "discount_amount",
+ "fieldname": "section_break_49",
+ "fieldtype": "Section Break",
+ "label": "Additional Discount"
+ },
+ {
+ "default": "Grand Total",
+ "fieldname": "apply_discount_on",
+ "fieldtype": "Select",
+ "label": "Apply Additional Discount On",
+ "options": "\nGrand Total\nNet Total",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "base_discount_amount",
+ "fieldtype": "Currency",
+ "label": "Additional Discount Amount (Company Currency)",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_51",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "additional_discount_percentage",
+ "fieldtype": "Float",
+ "label": "Additional Discount Percentage",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "discount_amount",
+ "fieldtype": "Currency",
+ "label": "Additional Discount Amount",
+ "options": "currency",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "totals",
+ "fieldtype": "Section Break",
+ "oldfieldtype": "Section Break",
+ "options": "fa fa-money",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "base_grand_total",
+ "fieldtype": "Currency",
+ "label": "Grand Total (Company Currency)",
+ "oldfieldname": "grand_total",
+ "oldfieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "base_rounding_adjustment",
+ "fieldtype": "Currency",
+ "label": "Rounding Adjustment (Company Currency)",
+ "no_copy": 1,
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "base_rounded_total",
+ "fieldtype": "Currency",
+ "label": "Rounded Total (Company Currency)",
+ "oldfieldname": "rounded_total",
+ "oldfieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "description": "In Words will be visible once you save the Sales Invoice.",
+ "fieldname": "base_in_words",
+ "fieldtype": "Data",
+ "label": "In Words (Company Currency)",
+ "oldfieldname": "in_words",
+ "oldfieldtype": "Data",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break5",
+ "fieldtype": "Column Break",
+ "oldfieldtype": "Column Break",
+ "print_hide": 1,
+ "width": "50%"
+ },
+ {
+ "bold": 1,
+ "fieldname": "grand_total",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Grand Total",
+ "oldfieldname": "grand_total_export",
+ "oldfieldtype": "Currency",
+ "options": "currency",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "rounding_adjustment",
+ "fieldtype": "Currency",
+ "label": "Rounding Adjustment",
+ "no_copy": 1,
+ "options": "currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "bold": 1,
+ "fieldname": "rounded_total",
+ "fieldtype": "Currency",
+ "label": "Rounded Total",
+ "oldfieldname": "rounded_total_export",
+ "oldfieldtype": "Currency",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "in_words",
+ "fieldtype": "Data",
+ "label": "In Words",
+ "oldfieldname": "in_words_export",
+ "oldfieldtype": "Data",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_advance",
+ "fieldtype": "Currency",
+ "label": "Total Advance",
+ "oldfieldname": "total_advance",
+ "oldfieldtype": "Currency",
+ "options": "party_account_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "outstanding_amount",
+ "fieldtype": "Currency",
+ "label": "Outstanding Amount",
+ "no_copy": 1,
+ "oldfieldname": "outstanding_amount",
+ "oldfieldtype": "Currency",
+ "options": "party_account_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "advances",
+ "fieldname": "advances_section",
+ "fieldtype": "Section Break",
+ "label": "Advance Payments",
+ "oldfieldtype": "Section Break",
+ "options": "fa fa-money",
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "allocate_advances_automatically",
+ "fieldtype": "Check",
+ "label": "Allocate Advances Automatically (FIFO)"
+ },
+ {
+ "depends_on": "eval:!doc.allocate_advances_automatically",
+ "fieldname": "get_advances",
+ "fieldtype": "Button",
+ "label": "Get Advances Received",
+ "options": "set_advances"
+ },
+ {
+ "fieldname": "advances",
+ "fieldtype": "Table",
+ "label": "Advances",
+ "oldfieldname": "advance_adjustment_details",
+ "oldfieldtype": "Table",
+ "options": "Sales Invoice Advance",
+ "print_hide": 1
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "eval:(!doc.is_pos && !doc.is_return)",
+ "fieldname": "payment_schedule_section",
+ "fieldtype": "Section Break",
+ "label": "Payment Terms"
+ },
+ {
+ "depends_on": "eval:(!doc.is_pos && !doc.is_return)",
+ "fieldname": "payment_terms_template",
+ "fieldtype": "Link",
+ "label": "Payment Terms Template",
+ "no_copy": 1,
+ "options": "Payment Terms Template",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:(!doc.is_pos && !doc.is_return)",
+ "fieldname": "payment_schedule",
+ "fieldtype": "Table",
+ "label": "Payment Schedule",
+ "no_copy": 1,
+ "options": "Payment Schedule",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.is_pos===1||(doc.advances && doc.advances.length>0)",
+ "fieldname": "payments_section",
+ "fieldtype": "Section Break",
+ "label": "Payments",
+ "options": "fa fa-money"
+ },
+ {
+ "depends_on": "is_pos",
+ "fieldname": "cash_bank_account",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Cash/Bank Account",
+ "oldfieldname": "cash_bank_account",
+ "oldfieldtype": "Link",
+ "options": "Account",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.is_pos===1",
+ "fieldname": "payments",
+ "fieldtype": "Table",
+ "label": "Sales Invoice Payment",
+ "options": "Sales Invoice Payment",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "section_break_84",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "base_paid_amount",
+ "fieldtype": "Currency",
+ "label": "Paid Amount (Company Currency)",
+ "no_copy": 1,
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_86",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval: doc.is_pos || doc.redeem_loyalty_points",
+ "fieldname": "paid_amount",
+ "fieldtype": "Currency",
+ "label": "Paid Amount",
+ "no_copy": 1,
+ "oldfieldname": "paid_amount",
+ "oldfieldtype": "Currency",
+ "options": "currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_88",
+ "fieldtype": "Section Break"
+ },
+ {
+ "depends_on": "is_pos",
+ "fieldname": "base_change_amount",
+ "fieldtype": "Currency",
+ "label": "Base Change Amount (Company Currency)",
+ "no_copy": 1,
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_90",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "is_pos",
+ "fieldname": "change_amount",
+ "fieldtype": "Currency",
+ "label": "Change Amount",
+ "no_copy": 1,
+ "options": "currency",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "is_pos",
+ "fieldname": "account_for_change_amount",
+ "fieldtype": "Link",
+ "label": "Account for Change Amount",
+ "options": "Account",
+ "print_hide": 1
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "write_off_amount",
+ "depends_on": "grand_total",
+ "fieldname": "column_break4",
+ "fieldtype": "Section Break",
+ "label": "Write Off",
+ "width": "50%"
+ },
+ {
+ "fieldname": "write_off_amount",
+ "fieldtype": "Currency",
+ "label": "Write Off Amount",
+ "no_copy": 1,
+ "options": "currency",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "base_write_off_amount",
+ "fieldtype": "Currency",
+ "label": "Write Off Amount (Company Currency)",
+ "no_copy": 1,
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "is_pos",
+ "fieldname": "write_off_outstanding_amount_automatically",
+ "fieldtype": "Check",
+ "label": "Write Off Outstanding Amount",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "column_break_74",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "write_off_account",
+ "fieldtype": "Link",
+ "label": "Write Off Account",
+ "options": "Account",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "write_off_cost_center",
+ "fieldtype": "Link",
+ "label": "Write Off Cost Center",
+ "options": "Cost Center",
+ "print_hide": 1
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "terms",
+ "fieldname": "terms_section_break",
+ "fieldtype": "Section Break",
+ "label": "Terms and Conditions",
+ "oldfieldtype": "Section Break"
+ },
+ {
+ "fieldname": "tc_name",
+ "fieldtype": "Link",
+ "label": "Terms",
+ "oldfieldname": "tc_name",
+ "oldfieldtype": "Link",
+ "options": "Terms and Conditions",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "terms",
+ "fieldtype": "Text Editor",
+ "label": "Terms and Conditions Details",
+ "oldfieldname": "terms",
+ "oldfieldtype": "Text Editor"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "edit_printing_settings",
+ "fieldtype": "Section Break",
+ "label": "Printing Settings"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "letter_head",
+ "fieldtype": "Link",
+ "label": "Letter Head",
+ "oldfieldname": "letter_head",
+ "oldfieldtype": "Select",
+ "options": "Letter Head",
+ "print_hide": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "0",
+ "fieldname": "group_same_items",
+ "fieldtype": "Check",
+ "label": "Group same items",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "language",
+ "fieldtype": "Data",
+ "label": "Print Language",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_84",
+ "fieldtype": "Column Break"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "select_print_heading",
+ "fieldtype": "Link",
+ "label": "Print Heading",
+ "no_copy": 1,
+ "oldfieldname": "select_print_heading",
+ "oldfieldtype": "Link",
+ "options": "Print Heading",
+ "print_hide": 1,
+ "report_hide": 1
+ },
+ {
+ "collapsible": 1,
+ "depends_on": "customer",
+ "fieldname": "more_information",
+ "fieldtype": "Section Break",
+ "label": "More Information"
+ },
+ {
+ "fieldname": "inter_company_invoice_reference",
+ "fieldtype": "Link",
+ "label": "Inter Company Invoice Reference",
+ "options": "Purchase Invoice",
+ "read_only": 1
+ },
+ {
+ "fieldname": "customer_group",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Customer Group",
+ "options": "Customer Group",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "campaign",
+ "fieldtype": "Link",
+ "label": "Campaign",
+ "oldfieldname": "campaign",
+ "oldfieldtype": "Link",
+ "options": "Campaign",
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_discounted",
+ "fieldtype": "Check",
+ "label": "Is Discounted",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "col_break23",
+ "fieldtype": "Column Break",
+ "width": "50%"
+ },
+ {
+ "default": "Draft",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_standard_filter": 1,
+ "label": "Status",
+ "no_copy": 1,
+ "options": "\nDraft\nReturn\nCredit Note Issued\nConsolidated\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "source",
+ "fieldtype": "Link",
+ "label": "Source",
+ "oldfieldname": "source",
+ "oldfieldtype": "Select",
+ "options": "Lead Source",
+ "print_hide": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "more_info",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details",
+ "oldfieldtype": "Section Break",
+ "options": "fa fa-file-text",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "debit_to",
+ "fieldtype": "Link",
+ "label": "Debit To",
+ "oldfieldname": "debit_to",
+ "oldfieldtype": "Link",
+ "options": "Account",
+ "print_hide": 1,
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "fieldname": "party_account_currency",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Party Account Currency",
+ "no_copy": 1,
+ "options": "Currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "default": "No",
+ "fieldname": "is_opening",
+ "fieldtype": "Select",
+ "label": "Is Opening Entry",
+ "oldfieldname": "is_opening",
+ "oldfieldtype": "Select",
+ "options": "No\nYes",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "c_form_applicable",
+ "fieldtype": "Select",
+ "label": "C-Form Applicable",
+ "no_copy": 1,
+ "options": "No\nYes",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "c_form_no",
+ "fieldtype": "Link",
+ "label": "C-Form No",
+ "no_copy": 1,
+ "options": "C-Form",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break8",
+ "fieldtype": "Column Break",
+ "oldfieldtype": "Column Break",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "remarks",
+ "fieldtype": "Small Text",
+ "label": "Remarks",
+ "no_copy": 1,
+ "oldfieldname": "remarks",
+ "oldfieldtype": "Text",
+ "print_hide": 1
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "sales_partner",
+ "fieldname": "sales_team_section_break",
+ "fieldtype": "Section Break",
+ "label": "Commission",
+ "oldfieldtype": "Section Break",
+ "options": "fa fa-group",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "sales_partner",
+ "fieldtype": "Link",
+ "label": "Sales Partner",
+ "oldfieldname": "sales_partner",
+ "oldfieldtype": "Link",
+ "options": "Sales Partner",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "column_break10",
+ "fieldtype": "Column Break",
+ "oldfieldtype": "Column Break",
+ "print_hide": 1,
+ "width": "50%"
+ },
+ {
+ "fieldname": "commission_rate",
+ "fieldtype": "Float",
+ "label": "Commission Rate (%)",
+ "oldfieldname": "commission_rate",
+ "oldfieldtype": "Currency",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "total_commission",
+ "fieldtype": "Currency",
+ "label": "Total Commission",
+ "oldfieldname": "total_commission",
+ "oldfieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "print_hide": 1
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "sales_team",
+ "fieldname": "section_break2",
+ "fieldtype": "Section Break",
+ "label": "Sales Team",
+ "print_hide": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "sales_team",
+ "fieldtype": "Table",
+ "label": "Sales Team1",
+ "oldfieldname": "sales_team",
+ "oldfieldtype": "Table",
+ "options": "Sales Team",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "subscription_section",
+ "fieldtype": "Section Break",
+ "label": "Subscription Section"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "from_date",
+ "fieldtype": "Date",
+ "label": "From Date",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "to_date",
+ "fieldtype": "Date",
+ "label": "To Date",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "fieldname": "column_break_140",
+ "fieldtype": "Column Break"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "auto_repeat",
+ "fieldtype": "Link",
+ "label": "Auto Repeat",
+ "no_copy": 1,
+ "options": "Auto Repeat",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "depends_on": "eval: doc.auto_repeat",
+ "fieldname": "update_auto_repeat_reference",
+ "fieldtype": "Button",
+ "label": "Update Auto Repeat Reference"
+ },
+ {
+ "fieldname": "against_income_account",
+ "fieldtype": "Small Text",
+ "hidden": 1,
+ "label": "Against Income Account",
+ "no_copy": 1,
+ "oldfieldname": "against_income_account",
+ "oldfieldtype": "Small Text",
+ "print_hide": 1,
+ "report_hide": 1
+ },
+ {
+ "fieldname": "pos_total_qty",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "label": "Total Qty",
+ "print_hide": 1,
+ "print_hide_if_no_value": 1,
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "consolidated_invoice",
+ "fieldtype": "Link",
+ "label": "Consolidated Sales Invoice",
+ "options": "Sales Invoice",
+ "read_only": 1
+ }
+ ],
+ "icon": "fa fa-file-text",
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-05-29 15:08:39.337385",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "POS Invoice",
+ "name_case": "Title Case",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "amend": 1,
+ "create": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "permlevel": 1,
+ "read": 1,
+ "role": "Accounts Manager",
+ "write": 1
+ },
+ {
+ "permlevel": 1,
+ "read": 1,
+ "role": "All"
+ }
+ ],
+ "quick_entry": 1,
+ "search_fields": "posting_date, due_date, customer, base_grand_total, outstanding_amount",
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "timeline_field": "customer",
+ "title_field": "title",
+ "track_changes": 1,
+ "track_seen": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
new file mode 100644
index 0000000..8680b71
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -0,0 +1,374 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from erpnext.controllers.selling_controller import SellingController
+from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate
+from erpnext.accounts.utils import get_account_currency
+from erpnext.accounts.party import get_party_account, get_due_date
+from erpnext.accounts.doctype.loyalty_program.loyalty_program import \
+ get_loyalty_program_details_with_points, validate_loyalty_points
+
+from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice, get_bank_cash_account, update_multi_mode_option
+from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos
+
+from six import iteritems
+
+class POSInvoice(SalesInvoice):
+ def __init__(self, *args, **kwargs):
+ super(POSInvoice, self).__init__(*args, **kwargs)
+
+ def validate(self):
+ if not cint(self.is_pos):
+ frappe.throw(_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment")))
+
+ # run on validate method of selling controller
+ super(SalesInvoice, self).validate()
+ self.validate_auto_set_posting_time()
+ self.validate_pos_paid_amount()
+ self.validate_pos_return()
+ self.validate_uom_is_integer("stock_uom", "stock_qty")
+ self.validate_uom_is_integer("uom", "qty")
+ self.validate_debit_to_acc()
+ self.validate_write_off_account()
+ self.validate_change_amount()
+ self.validate_change_account()
+ self.validate_item_cost_centers()
+ self.validate_serialised_or_batched_item()
+ self.validate_stock_availablility()
+ self.validate_return_items()
+ self.set_status()
+ self.set_account_for_mode_of_payment()
+ self.validate_pos()
+ self.verify_payment_amount()
+ self.validate_loyalty_transaction()
+
+ def on_submit(self):
+ # create the loyalty point ledger entry if the customer is enrolled in any loyalty program
+ if self.loyalty_program:
+ self.make_loyalty_point_entry()
+ elif self.is_return and self.return_against and self.loyalty_program:
+ against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
+ against_psi_doc.delete_loyalty_point_entry()
+ against_psi_doc.make_loyalty_point_entry()
+ if self.redeem_loyalty_points and self.loyalty_points:
+ self.apply_loyalty_points()
+ self.set_status(update=True)
+
+ def on_cancel(self):
+ # run on cancel method of selling controller
+ super(SalesInvoice, self).on_cancel()
+ if self.loyalty_program:
+ self.delete_loyalty_point_entry()
+ elif self.is_return and self.return_against and self.loyalty_program:
+ against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
+ against_psi_doc.delete_loyalty_point_entry()
+ against_psi_doc.make_loyalty_point_entry()
+
+ def validate_stock_availablility(self):
+ allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
+
+ for d in self.get('items'):
+ if d.serial_no:
+ filters = {
+ "item_code": d.item_code,
+ "warehouse": d.warehouse,
+ "delivery_document_no": "",
+ "sales_invoice": ""
+ }
+ if d.batch_no:
+ filters["batch_no"] = d.batch_no
+ reserved_serial_nos, unreserved_serial_nos = get_pos_reserved_serial_nos(filters)
+ serial_nos = d.serial_no.split("\n")
+ serial_nos = ' '.join(serial_nos).split() # remove whitespaces
+ invalid_serial_nos = []
+ for s in serial_nos:
+ if s in reserved_serial_nos:
+ invalid_serial_nos.append(s)
+
+ if len(invalid_serial_nos):
+ multiple_nos = 's' if len(invalid_serial_nos) > 1 else ''
+ frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. \
+ Please select valid serial no.".format(d.idx, multiple_nos,
+ frappe.bold(', '.join(invalid_serial_nos)))), title=_("Not Available"))
+ else:
+ if allow_negative_stock:
+ return
+
+ available_stock = get_stock_availability(d.item_code, d.warehouse)
+ if not (flt(available_stock) > 0):
+ frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.'
+ .format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse))), title=_("Not Available"))
+ elif flt(available_stock) < flt(d.qty):
+ frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. \
+ Available quantity {}.'.format(d.idx, frappe.bold(d.item_code),
+ frappe.bold(d.warehouse), frappe.bold(d.qty))), title=_("Not Available"))
+
+ def validate_serialised_or_batched_item(self):
+ for d in self.get("items"):
+ serialized = d.get("has_serial_no")
+ batched = d.get("has_batch_no")
+ no_serial_selected = not d.get("serial_no")
+ no_batch_selected = not d.get("batch_no")
+
+
+ if serialized and batched and (no_batch_selected or no_serial_selected):
+ frappe.throw(_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.'
+ .format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item"))
+ if serialized and no_serial_selected:
+ frappe.throw(_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.'
+ .format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item"))
+ if batched and no_batch_selected:
+ frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.'
+ .format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item"))
+
+ def validate_return_items(self):
+ if not self.get("is_return"): return
+
+ for d in self.get("items"):
+ if d.get("qty") > 0:
+ frappe.throw(_("Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return.")
+ .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
+
+ def validate_pos_paid_amount(self):
+ if len(self.payments) == 0 and self.is_pos:
+ frappe.throw(_("At least one mode of payment is required for POS invoice."))
+
+ def validate_change_account(self):
+ if frappe.db.get_value("Account", self.account_for_change_amount, "company") != self.company:
+ frappe.throw(_("The selected change account {} doesn't belongs to Company {}.").format(self.account_for_change_amount, self.company))
+
+ def validate_change_amount(self):
+ grand_total = flt(self.rounded_total) or flt(self.grand_total)
+ base_grand_total = flt(self.base_rounded_total) or flt(self.base_grand_total)
+ if not flt(self.change_amount) and grand_total < flt(self.paid_amount):
+ self.change_amount = flt(self.paid_amount - grand_total + flt(self.write_off_amount))
+ self.base_change_amount = flt(self.base_paid_amount - base_grand_total + flt(self.base_write_off_amount))
+
+ if flt(self.change_amount) and not self.account_for_change_amount:
+ msgprint(_("Please enter Account for Change Amount"), raise_exception=1)
+
+ def verify_payment_amount(self):
+ for entry in self.payments:
+ if not self.is_return and entry.amount < 0:
+ frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx))
+ if self.is_return and entry.amount > 0:
+ frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
+
+ def validate_pos_return(self):
+ if self.is_pos and self.is_return:
+ total_amount_in_payments = 0
+ for payment in self.payments:
+ total_amount_in_payments += payment.amount
+ invoice_total = self.rounded_total or self.grand_total
+ if total_amount_in_payments < invoice_total:
+ frappe.throw(_("Total payments amount can't be greater than {}".format(-invoice_total)))
+
+ def validate_loyalty_transaction(self):
+ if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center):
+ expense_account, cost_center = frappe.db.get_value('Loyalty Program', self.loyalty_program, ["expense_account", "cost_center"])
+ if not self.loyalty_redemption_account:
+ self.loyalty_redemption_account = expense_account
+ if not self.loyalty_redemption_cost_center:
+ self.loyalty_redemption_cost_center = cost_center
+
+ if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points:
+ validate_loyalty_points(self, self.loyalty_points)
+
+ def set_status(self, update=False, status=None, update_modified=True):
+ if self.is_new():
+ if self.get('amended_from'):
+ self.status = 'Draft'
+ return
+
+ if not status:
+ if self.docstatus == 2:
+ status = "Cancelled"
+ elif self.docstatus == 1:
+ if self.consolidated_invoice:
+ self.status = "Consolidated"
+ elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) < getdate(nowdate()) and self.is_discounted and self.get_discounting_status()=='Disbursed':
+ self.status = "Overdue and Discounted"
+ elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) < getdate(nowdate()):
+ self.status = "Overdue"
+ elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) >= getdate(nowdate()) and self.is_discounted and self.get_discounting_status()=='Disbursed':
+ self.status = "Unpaid and Discounted"
+ elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) >= getdate(nowdate()):
+ self.status = "Unpaid"
+ elif flt(self.outstanding_amount) <= 0 and self.is_return == 0 and frappe.db.get_value('POS Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
+ self.status = "Credit Note Issued"
+ elif self.is_return == 1:
+ self.status = "Return"
+ elif flt(self.outstanding_amount)<=0:
+ self.status = "Paid"
+ else:
+ self.status = "Submitted"
+ else:
+ self.status = "Draft"
+
+ if update:
+ self.db_set('status', self.status, update_modified = update_modified)
+
+ def set_pos_fields(self, for_validate=False):
+ """Set retail related fields from POS Profiles"""
+ from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile
+ if not self.pos_profile:
+ pos_profile = get_pos_profile(self.company) or {}
+ self.pos_profile = pos_profile.get('name')
+
+ pos = {}
+ if self.pos_profile:
+ pos = frappe.get_doc('POS Profile', self.pos_profile)
+
+ if not self.get('payments') and not for_validate:
+ update_multi_mode_option(self, pos)
+
+ if not self.account_for_change_amount:
+ self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
+
+ if pos:
+ if not for_validate:
+ self.tax_category = pos.get("tax_category")
+
+ if not for_validate and not self.customer:
+ self.customer = pos.customer
+
+ self.ignore_pricing_rule = pos.ignore_pricing_rule
+ if pos.get('account_for_change_amount'):
+ self.account_for_change_amount = pos.get('account_for_change_amount')
+ if pos.get('warehouse'):
+ self.set_warehouse = pos.get('warehouse')
+
+ for fieldname in ('naming_series', 'currency', 'letter_head', 'tc_name',
+ 'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges',
+ 'write_off_cost_center', 'apply_discount_on', 'cost_center'):
+ if (not for_validate) or (for_validate and not self.get(fieldname)):
+ self.set(fieldname, pos.get(fieldname))
+
+ if pos.get("company_address"):
+ self.company_address = pos.get("company_address")
+
+ if self.customer:
+ customer_price_list, customer_group = frappe.db.get_value("Customer", self.customer, ['default_price_list', 'customer_group'])
+ customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list')
+ selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list')
+ else:
+ selling_price_list = pos.get('selling_price_list')
+
+ if selling_price_list:
+ self.set('selling_price_list', selling_price_list)
+
+ if not for_validate:
+ self.update_stock = cint(pos.get("update_stock"))
+
+ # set pos values in items
+ for item in self.get("items"):
+ if item.get('item_code'):
+ profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos)
+ for fname, val in iteritems(profile_details):
+ if (not for_validate) or (for_validate and not item.get(fname)):
+ item.set(fname, val)
+
+ # fetch terms
+ if self.tc_name and not self.terms:
+ self.terms = frappe.db.get_value("Terms and Conditions", self.tc_name, "terms")
+
+ # fetch charges
+ if self.taxes_and_charges and not len(self.get("taxes")):
+ self.set_taxes()
+
+ return pos
+
+ def set_missing_values(self, for_validate=False):
+ pos = self.set_pos_fields(for_validate)
+
+ if not self.debit_to:
+ self.debit_to = get_party_account("Customer", self.customer, self.company)
+ self.party_account_currency = frappe.db.get_value("Account", self.debit_to, "account_currency", cache=True)
+ if not self.due_date and self.customer:
+ self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
+
+ super(SalesInvoice, self).set_missing_values(for_validate)
+
+ print_format = pos.get("print_format") if pos else None
+ if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')):
+ print_format = 'POS Invoice'
+
+ if pos:
+ return {
+ "print_format": print_format,
+ "allow_edit_rate": pos.get("allow_user_to_edit_rate"),
+ "allow_edit_discount": pos.get("allow_user_to_edit_discount"),
+ "campaign": pos.get("campaign"),
+ "allow_print_before_pay": pos.get("allow_print_before_pay")
+ }
+
+ def set_account_for_mode_of_payment(self):
+ self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default]
+ for pay in self.payments:
+ if not pay.account:
+ pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
+
+@frappe.whitelist()
+def get_stock_availability(item_code, warehouse):
+ latest_sle = frappe.db.sql("""select qty_after_transaction
+ from `tabStock Ledger Entry`
+ where item_code = %s and warehouse = %s
+ order by posting_date desc, posting_time desc
+ limit 1""", (item_code, warehouse), as_dict=1)
+
+ pos_sales_qty = frappe.db.sql("""select sum(p_item.qty) as qty
+ from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
+ where p.name = p_item.parent
+ and p.consolidated_invoice is NULL
+ and p.docstatus = 1
+ and p_item.docstatus = 1
+ and p_item.item_code = %s
+ and p_item.warehouse = %s
+ """, (item_code, warehouse), as_dict=1)
+
+ sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0
+ pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0
+
+ if sle_qty and pos_sales_qty and sle_qty > pos_sales_qty:
+ return sle_qty - pos_sales_qty
+ else:
+ # when sle_qty is 0
+ # when sle_qty > 0 and pos_sales_qty is 0
+ return sle_qty
+
+@frappe.whitelist()
+def make_sales_return(source_name, target_doc=None):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+ return make_return_doc("POS Invoice", source_name, target_doc)
+
+@frappe.whitelist()
+def make_merge_log(invoices):
+ import json
+ from six import string_types
+
+ if isinstance(invoices, string_types):
+ invoices = json.loads(invoices)
+
+ if len(invoices) == 0:
+ frappe.throw(_('Atleast one invoice has to be selected.'))
+
+ merge_log = frappe.new_doc("POS Invoice Merge Log")
+ merge_log.posting_date = getdate(nowdate())
+ for inv in invoices:
+ inv_data = frappe.db.get_values("POS Invoice", inv.get('name'),
+ ["customer", "posting_date", "grand_total"], as_dict=1)[0]
+ merge_log.customer = inv_data.customer
+ merge_log.append("pos_invoices", {
+ 'pos_invoice': inv.get('name'),
+ 'customer': inv_data.customer,
+ 'posting_date': inv_data.posting_date,
+ 'grand_total': inv_data.grand_total
+ })
+
+ if merge_log.get('pos_invoices'):
+ return merge_log.as_dict()
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice_list.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice_list.js
new file mode 100644
index 0000000..2dbf2a4
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice_list.js
@@ -0,0 +1,42 @@
+// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+// License: GNU General Public License v3. See license.txt
+
+// render
+frappe.listview_settings['POS Invoice'] = {
+ add_fields: ["customer", "customer_name", "base_grand_total", "outstanding_amount", "due_date", "company",
+ "currency", "is_return"],
+ get_indicator: function(doc) {
+ var status_color = {
+ "Draft": "red",
+ "Unpaid": "orange",
+ "Paid": "green",
+ "Submitted": "blue",
+ "Consolidated": "green",
+ "Return": "darkgrey",
+ "Unpaid and Discounted": "orange",
+ "Overdue and Discounted": "red",
+ "Overdue": "red"
+
+ };
+ return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
+ },
+ right_column: "grand_total",
+ onload: function(me) {
+ me.page.add_action_item('Make Merge Log', function() {
+ const invoices = me.get_checked_items();
+ frappe.call({
+ method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_merge_log",
+ freeze: true,
+ args:{
+ "invoices": invoices
+ },
+ callback: function (r) {
+ if (r.message) {
+ var doc = frappe.model.sync(r.message)[0];
+ frappe.set_route("Form", doc.doctype, doc.name);
+ }
+ }
+ });
+ });
+ },
+};
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
new file mode 100644
index 0000000..f295725
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -0,0 +1,324 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest, copy, time
+from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
+from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
+
+class TestPOSInvoice(unittest.TestCase):
+ def test_timestamp_change(self):
+ w = create_pos_invoice(do_not_save=1)
+ w.docstatus = 0
+ w.insert()
+
+ w2 = frappe.get_doc(w.doctype, w.name)
+
+ import time
+ time.sleep(1)
+ w.save()
+
+ import time
+ time.sleep(1)
+ self.assertRaises(frappe.TimestampMismatchError, w2.save)
+
+ def test_change_naming_series(self):
+ inv = create_pos_invoice(do_not_submit=1)
+ inv.naming_series = 'TEST-'
+
+ self.assertRaises(frappe.CannotChangeConstantError, inv.save)
+
+ def test_discount_and_inclusive_tax(self):
+ inv = create_pos_invoice(qty=100, rate=50, do_not_save=1)
+ inv.append("taxes", {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 14,
+ 'included_in_print_rate': 1
+ })
+ inv.insert()
+
+ self.assertEqual(inv.net_total, 4385.96)
+ self.assertEqual(inv.grand_total, 5000)
+
+ inv.reload()
+
+ inv.discount_amount = 100
+ inv.apply_discount_on = 'Net Total'
+ inv.payment_schedule = []
+
+ inv.save()
+
+ self.assertEqual(inv.net_total, 4285.96)
+ self.assertEqual(inv.grand_total, 4885.99)
+
+ inv.reload()
+
+ inv.discount_amount = 100
+ inv.apply_discount_on = 'Grand Total'
+ inv.payment_schedule = []
+
+ inv.save()
+
+ self.assertEqual(inv.net_total, 4298.25)
+ self.assertEqual(inv.grand_total, 4900.00)
+
+ def test_tax_calculation_with_multiple_items(self):
+ inv = create_pos_invoice(qty=84, rate=4.6, do_not_save=True)
+ item_row = inv.get("items")[0]
+ for qty in (54, 288, 144, 430):
+ item_row_copy = copy.deepcopy(item_row)
+ item_row_copy.qty = qty
+ inv.append("items", item_row_copy)
+
+ inv.append("taxes", {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 19
+ })
+ inv.insert()
+
+ self.assertEqual(inv.net_total, 4600)
+
+ self.assertEqual(inv.get("taxes")[0].tax_amount, 874.0)
+ self.assertEqual(inv.get("taxes")[0].total, 5474.0)
+
+ self.assertEqual(inv.grand_total, 5474.0)
+
+ def test_tax_calculation_with_item_tax_template(self):
+ inv = create_pos_invoice(qty=84, rate=4.6, do_not_save=1)
+ item_row = inv.get("items")[0]
+
+ add_items = [
+ (54, '_Test Account Excise Duty @ 12'),
+ (288, '_Test Account Excise Duty @ 15'),
+ (144, '_Test Account Excise Duty @ 20'),
+ (430, '_Test Item Tax Template 1')
+ ]
+ for qty, item_tax_template in add_items:
+ item_row_copy = copy.deepcopy(item_row)
+ item_row_copy.qty = qty
+ item_row_copy.item_tax_template = item_tax_template
+ inv.append("items", item_row_copy)
+
+ inv.append("taxes", {
+ "account_head": "_Test Account Excise Duty - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Excise Duty",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 11
+ })
+ inv.append("taxes", {
+ "account_head": "_Test Account Education Cess - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Education Cess",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 0
+ })
+ inv.append("taxes", {
+ "account_head": "_Test Account S&H Education Cess - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "S&H Education Cess",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 3
+ })
+ inv.insert()
+
+ self.assertEqual(inv.net_total, 4600)
+
+ self.assertEqual(inv.get("taxes")[0].tax_amount, 502.41)
+ self.assertEqual(inv.get("taxes")[0].total, 5102.41)
+
+ self.assertEqual(inv.get("taxes")[1].tax_amount, 197.80)
+ self.assertEqual(inv.get("taxes")[1].total, 5300.21)
+
+ self.assertEqual(inv.get("taxes")[2].tax_amount, 375.36)
+ self.assertEqual(inv.get("taxes")[2].total, 5675.57)
+
+ self.assertEqual(inv.grand_total, 5675.57)
+ self.assertEqual(inv.rounding_adjustment, 0.43)
+ self.assertEqual(inv.rounded_total, 5676.0)
+
+ def test_tax_calculation_with_multiple_items_and_discount(self):
+ inv = create_pos_invoice(qty=1, rate=75, do_not_save=True)
+ item_row = inv.get("items")[0]
+ for rate in (500, 200, 100, 50, 50):
+ item_row_copy = copy.deepcopy(item_row)
+ item_row_copy.price_list_rate = rate
+ item_row_copy.rate = rate
+ inv.append("items", item_row_copy)
+
+ inv.apply_discount_on = "Net Total"
+ inv.discount_amount = 75.0
+
+ inv.append("taxes", {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 24
+ })
+ inv.insert()
+
+ self.assertEqual(inv.total, 975)
+ self.assertEqual(inv.net_total, 900)
+
+ self.assertEqual(inv.get("taxes")[0].tax_amount, 216.0)
+ self.assertEqual(inv.get("taxes")[0].total, 1116.0)
+
+ self.assertEqual(inv.grand_total, 1116.0)
+
+ def test_pos_returns_with_repayment(self):
+ pos = create_pos_invoice(qty = 10, do_not_save=True)
+
+ pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500})
+ pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500})
+ pos.insert()
+ pos.submit()
+
+ pos_return = make_sales_return(pos.name)
+
+ pos_return.insert()
+ pos_return.submit()
+
+ self.assertEqual(pos_return.get('payments')[0].amount, -500)
+ self.assertEqual(pos_return.get('payments')[1].amount, -500)
+
+ def test_pos_change_amount(self):
+ pos = create_pos_invoice(company= "_Test Company", debit_to="Debtors - _TC",
+ income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105,
+ cost_center = "Main - _TC", do_not_save=True)
+
+ pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 50})
+ pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60})
+
+ pos.insert()
+ pos.submit()
+
+ self.assertEqual(pos.grand_total, 105.0)
+ self.assertEqual(pos.change_amount, 5.0)
+
+ def test_without_payment(self):
+ inv = create_pos_invoice(do_not_save=1)
+ # Check that the invoice cannot be submitted without payments
+ inv.payments = []
+ self.assertRaises(frappe.ValidationError, inv.insert)
+
+ def test_serialized_item_transaction(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+ se = make_serialized_item(target_warehouse="_Test Warehouse - _TC")
+ serial_nos = get_serial_nos(se.get("items")[0].serial_no)
+
+ pos = create_pos_invoice(item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
+ pos.get("items")[0].serial_no = serial_nos[0]
+ pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
+
+ pos.insert()
+ pos.submit()
+
+ pos2 = create_pos_invoice(item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
+ pos2.get("items")[0].serial_no = serial_nos[0]
+ pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
+
+ self.assertRaises(frappe.ValidationError, pos2.insert)
+
+ def test_loyalty_points(self):
+ from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records
+ from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points
+
+ create_records()
+ frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty")
+ before_lp_details = get_loyalty_program_details_with_points("Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty")
+
+ inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000)
+
+ lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'POS Invoice', 'invoice': inv.name, 'customer': inv.customer})
+ after_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program)
+
+ self.assertEqual(inv.get('loyalty_program'), "Test Single Loyalty")
+ self.assertEqual(lpe.loyalty_points, 10)
+ self.assertEqual(after_lp_details.loyalty_points, before_lp_details.loyalty_points + 10)
+
+ inv.cancel()
+ after_cancel_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program)
+ self.assertEqual(after_cancel_lp_details.loyalty_points, before_lp_details.loyalty_points)
+
+ def test_loyalty_points_redeemption(self):
+ from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points
+ # add 10 loyalty points
+ create_pos_invoice(customer="Test Loyalty Customer", rate=10000)
+
+ before_lp_details = get_loyalty_program_details_with_points("Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty")
+
+ inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
+ inv.redeem_loyalty_points = 1
+ inv.loyalty_points = before_lp_details.loyalty_points
+ inv.loyalty_amount = inv.loyalty_points * before_lp_details.conversion_factor
+ inv.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 10000 - inv.loyalty_amount})
+ inv.paid_amount = 10000
+ inv.submit()
+
+ after_redeem_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program)
+ self.assertEqual(after_redeem_lp_details.loyalty_points, 9)
+
+def create_pos_invoice(**args):
+ args = frappe._dict(args)
+ pos_profile = None
+ if not args.pos_profile:
+ pos_profile = make_pos_profile()
+ pos_profile.save()
+
+ pos_inv = frappe.new_doc("POS Invoice")
+ pos_inv.update_stock = 1
+ pos_inv.is_pos = 1
+ pos_inv.pos_profile = args.pos_profile or pos_profile.name
+
+ pos_inv.set_missing_values()
+
+ if args.posting_date:
+ pos_inv.set_posting_time = 1
+ pos_inv.posting_date = args.posting_date or frappe.utils.nowdate()
+
+ pos_inv.company = args.company or "_Test Company"
+ pos_inv.customer = args.customer or "_Test Customer"
+ pos_inv.debit_to = args.debit_to or "Debtors - _TC"
+ pos_inv.is_return = args.is_return
+ pos_inv.return_against = args.return_against
+ pos_inv.currency=args.currency or "INR"
+ pos_inv.conversion_rate = args.conversion_rate or 1
+ pos_inv.account_for_change_amount = "Cash - _TC"
+
+ pos_inv.append("items", {
+ "item_code": args.item or args.item_code or "_Test Item",
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "qty": args.qty or 1,
+ "rate": args.rate if args.get("rate") is not None else 100,
+ "income_account": args.income_account or "Sales - _TC",
+ "expense_account": args.expense_account or "Cost of Goods Sold - _TC",
+ "cost_center": args.cost_center or "_Test Cost Center - _TC",
+ "serial_no": args.serial_no
+ })
+
+ if not args.do_not_save:
+ pos_inv.insert()
+ if not args.do_not_submit:
+ pos_inv.submit()
+ else:
+ pos_inv.payment_schedule = []
+ else:
+ pos_inv.payment_schedule = []
+
+ return pos_inv
\ No newline at end of file
diff --git a/erpnext/accounts/page/pos/__init__.py b/erpnext/accounts/doctype/pos_invoice_item/__init__.py
similarity index 100%
copy from erpnext/accounts/page/pos/__init__.py
copy to erpnext/accounts/doctype/pos_invoice_item/__init__.py
diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
new file mode 100644
index 0000000..2b6e7de
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
@@ -0,0 +1,805 @@
+{
+ "actions": [],
+ "autoname": "hash",
+ "creation": "2020-01-27 13:04:55.229516",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "barcode",
+ "item_code",
+ "col_break1",
+ "item_name",
+ "customer_item_code",
+ "description_section",
+ "description",
+ "item_group",
+ "brand",
+ "image_section",
+ "image",
+ "image_view",
+ "quantity_and_rate",
+ "qty",
+ "stock_uom",
+ "col_break2",
+ "uom",
+ "conversion_factor",
+ "stock_qty",
+ "section_break_17",
+ "price_list_rate",
+ "base_price_list_rate",
+ "discount_and_margin",
+ "margin_type",
+ "margin_rate_or_amount",
+ "rate_with_margin",
+ "column_break_19",
+ "discount_percentage",
+ "discount_amount",
+ "base_rate_with_margin",
+ "section_break1",
+ "rate",
+ "amount",
+ "item_tax_template",
+ "col_break3",
+ "base_rate",
+ "base_amount",
+ "pricing_rules",
+ "is_free_item",
+ "section_break_21",
+ "net_rate",
+ "net_amount",
+ "column_break_24",
+ "base_net_rate",
+ "base_net_amount",
+ "drop_ship",
+ "delivered_by_supplier",
+ "accounting",
+ "income_account",
+ "is_fixed_asset",
+ "asset",
+ "finance_book",
+ "col_break4",
+ "expense_account",
+ "deferred_revenue",
+ "deferred_revenue_account",
+ "service_stop_date",
+ "enable_deferred_revenue",
+ "column_break_50",
+ "service_start_date",
+ "service_end_date",
+ "section_break_18",
+ "weight_per_unit",
+ "total_weight",
+ "column_break_21",
+ "weight_uom",
+ "warehouse_and_reference",
+ "warehouse",
+ "target_warehouse",
+ "quality_inspection",
+ "batch_no",
+ "col_break5",
+ "allow_zero_valuation_rate",
+ "serial_no",
+ "item_tax_rate",
+ "actual_batch_qty",
+ "actual_qty",
+ "edit_references",
+ "sales_order",
+ "so_detail",
+ "column_break_74",
+ "delivery_note",
+ "dn_detail",
+ "delivered_qty",
+ "accounting_dimensions_section",
+ "cost_center",
+ "dimension_col_break",
+ "project",
+ "section_break_54",
+ "page_break"
+ ],
+ "fields": [
+ {
+ "fieldname": "barcode",
+ "fieldtype": "Data",
+ "label": "Barcode",
+ "print_hide": 1
+ },
+ {
+ "bold": 1,
+ "columns": 4,
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item",
+ "oldfieldname": "item_code",
+ "oldfieldtype": "Link",
+ "options": "Item",
+ "search_index": 1
+ },
+ {
+ "fieldname": "col_break1",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "in_global_search": 1,
+ "label": "Item Name",
+ "oldfieldname": "item_name",
+ "oldfieldtype": "Data",
+ "print_hide": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "customer_item_code",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Customer's Item Code",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "description_section",
+ "fieldtype": "Section Break",
+ "label": "Description"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Text Editor",
+ "label": "Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Text",
+ "print_width": "200px",
+ "reqd": 1,
+ "width": "200px"
+ },
+ {
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Item Group",
+ "oldfieldname": "item_group",
+ "oldfieldtype": "Link",
+ "options": "Item Group",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "brand",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Brand Name",
+ "oldfieldname": "brand",
+ "oldfieldtype": "Data",
+ "print_hide": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "image_section",
+ "fieldtype": "Section Break",
+ "label": "Image"
+ },
+ {
+ "fieldname": "image",
+ "fieldtype": "Attach",
+ "hidden": 1,
+ "label": "Image"
+ },
+ {
+ "fieldname": "image_view",
+ "fieldtype": "Image",
+ "label": "Image View",
+ "options": "image",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "quantity_and_rate",
+ "fieldtype": "Section Break"
+ },
+ {
+ "bold": 1,
+ "columns": 2,
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Quantity",
+ "oldfieldname": "qty",
+ "oldfieldtype": "Currency"
+ },
+ {
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "label": "Stock UOM",
+ "options": "UOM",
+ "read_only": 1
+ },
+ {
+ "fieldname": "col_break2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "uom",
+ "fieldtype": "Link",
+ "label": "UOM",
+ "options": "UOM",
+ "reqd": 1
+ },
+ {
+ "fieldname": "conversion_factor",
+ "fieldtype": "Float",
+ "label": "UOM Conversion Factor",
+ "print_hide": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "stock_qty",
+ "fieldtype": "Float",
+ "label": "Qty as per Stock UOM",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_17",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "price_list_rate",
+ "fieldtype": "Currency",
+ "label": "Price List Rate",
+ "oldfieldname": "ref_rate",
+ "oldfieldtype": "Currency",
+ "options": "currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "base_price_list_rate",
+ "fieldtype": "Currency",
+ "label": "Price List Rate (Company Currency)",
+ "oldfieldname": "base_ref_rate",
+ "oldfieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "discount_and_margin",
+ "fieldtype": "Section Break",
+ "label": "Discount and Margin"
+ },
+ {
+ "depends_on": "price_list_rate",
+ "fieldname": "margin_type",
+ "fieldtype": "Select",
+ "label": "Margin Type",
+ "options": "\nPercentage\nAmount",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate",
+ "fieldname": "margin_rate_or_amount",
+ "fieldtype": "Float",
+ "label": "Margin Rate or Amount",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
+ "fieldname": "rate_with_margin",
+ "fieldtype": "Currency",
+ "label": "Rate With Margin",
+ "options": "currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_19",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "price_list_rate",
+ "fieldname": "discount_percentage",
+ "fieldtype": "Percent",
+ "label": "Discount (%) on Price List Rate with Margin",
+ "oldfieldname": "adj_rate",
+ "oldfieldtype": "Float",
+ "precision": "2",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "price_list_rate",
+ "fieldname": "discount_amount",
+ "fieldtype": "Currency",
+ "label": "Discount Amount",
+ "options": "currency"
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
+ "fieldname": "base_rate_with_margin",
+ "fieldtype": "Currency",
+ "label": "Rate With Margin (Company Currency)",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break1",
+ "fieldtype": "Section Break"
+ },
+ {
+ "bold": 1,
+ "columns": 2,
+ "fieldname": "rate",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Rate",
+ "oldfieldname": "export_rate",
+ "oldfieldtype": "Currency",
+ "options": "currency",
+ "reqd": 1
+ },
+ {
+ "columns": 2,
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Amount",
+ "oldfieldname": "export_amount",
+ "oldfieldtype": "Currency",
+ "options": "currency",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "item_tax_template",
+ "fieldtype": "Link",
+ "label": "Item Tax Template",
+ "options": "Item Tax Template",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "col_break3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "base_rate",
+ "fieldtype": "Currency",
+ "label": "Rate (Company Currency)",
+ "oldfieldname": "basic_rate",
+ "oldfieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "base_amount",
+ "fieldtype": "Currency",
+ "label": "Amount (Company Currency)",
+ "oldfieldname": "amount",
+ "oldfieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "pricing_rules",
+ "fieldtype": "Small Text",
+ "hidden": 1,
+ "label": "Pricing Rules",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_free_item",
+ "fieldtype": "Check",
+ "label": "Is Free Item",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_21",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "net_rate",
+ "fieldtype": "Currency",
+ "label": "Net Rate",
+ "options": "currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "net_amount",
+ "fieldtype": "Currency",
+ "label": "Net Amount",
+ "options": "currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_24",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "base_net_rate",
+ "fieldtype": "Currency",
+ "label": "Net Rate (Company Currency)",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "base_net_amount",
+ "fieldtype": "Currency",
+ "label": "Net Amount (Company Currency)",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "eval:doc.delivered_by_supplier==1",
+ "fieldname": "drop_ship",
+ "fieldtype": "Section Break",
+ "label": "Drop Ship"
+ },
+ {
+ "default": "0",
+ "fieldname": "delivered_by_supplier",
+ "fieldtype": "Check",
+ "label": "Delivered By Supplier",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "accounting",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fieldname": "income_account",
+ "fieldtype": "Link",
+ "label": "Income Account",
+ "oldfieldname": "income_account",
+ "oldfieldtype": "Link",
+ "options": "Account",
+ "print_hide": 1,
+ "print_width": "120px",
+ "reqd": 1,
+ "width": "120px"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_fixed_asset",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Is Fixed Asset",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "asset",
+ "fieldtype": "Link",
+ "label": "Asset",
+ "no_copy": 1,
+ "options": "Asset"
+ },
+ {
+ "depends_on": "asset",
+ "fieldname": "finance_book",
+ "fieldtype": "Link",
+ "label": "Finance Book",
+ "options": "Finance Book"
+ },
+ {
+ "fieldname": "col_break4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "expense_account",
+ "fieldtype": "Link",
+ "label": "Expense Account",
+ "options": "Account",
+ "print_hide": 1,
+ "width": "120px"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "deferred_revenue",
+ "fieldtype": "Section Break",
+ "label": "Deferred Revenue"
+ },
+ {
+ "depends_on": "enable_deferred_revenue",
+ "fieldname": "deferred_revenue_account",
+ "fieldtype": "Link",
+ "label": "Deferred Revenue Account",
+ "options": "Account"
+ },
+ {
+ "allow_on_submit": 1,
+ "depends_on": "enable_deferred_revenue",
+ "fieldname": "service_stop_date",
+ "fieldtype": "Date",
+ "label": "Service Stop Date",
+ "no_copy": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_deferred_revenue",
+ "fieldtype": "Check",
+ "label": "Enable Deferred Revenue"
+ },
+ {
+ "fieldname": "column_break_50",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "enable_deferred_revenue",
+ "fieldname": "service_start_date",
+ "fieldtype": "Date",
+ "label": "Service Start Date",
+ "no_copy": 1
+ },
+ {
+ "depends_on": "enable_deferred_revenue",
+ "fieldname": "service_end_date",
+ "fieldtype": "Date",
+ "label": "Service End Date",
+ "no_copy": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_18",
+ "fieldtype": "Section Break",
+ "label": "Item Weight Details"
+ },
+ {
+ "fieldname": "weight_per_unit",
+ "fieldtype": "Float",
+ "label": "Weight Per Unit",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_weight",
+ "fieldtype": "Float",
+ "label": "Total Weight",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_21",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "weight_uom",
+ "fieldtype": "Link",
+ "label": "Weight UOM",
+ "options": "UOM",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "eval:doc.serial_no || doc.batch_no",
+ "fieldname": "warehouse_and_reference",
+ "fieldtype": "Section Break",
+ "label": "Stock Details"
+ },
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Warehouse",
+ "oldfieldname": "warehouse",
+ "oldfieldtype": "Link",
+ "options": "Warehouse",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "target_warehouse",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "ignore_user_permissions": 1,
+ "label": "Customer Warehouse (Optional)",
+ "no_copy": 1,
+ "options": "Warehouse",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "quality_inspection",
+ "fieldtype": "Link",
+ "label": "Quality Inspection",
+ "options": "Quality Inspection"
+ },
+ {
+ "fieldname": "batch_no",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Batch No",
+ "options": "Batch",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "col_break5",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_zero_valuation_rate",
+ "fieldtype": "Check",
+ "label": "Allow Zero Valuation Rate",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "fieldname": "serial_no",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Serial No",
+ "oldfieldname": "serial_no",
+ "oldfieldtype": "Small Text"
+ },
+ {
+ "fieldname": "item_tax_rate",
+ "fieldtype": "Small Text",
+ "hidden": 1,
+ "label": "Item Tax Rate",
+ "oldfieldname": "item_tax_rate",
+ "oldfieldtype": "Small Text",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "actual_batch_qty",
+ "fieldtype": "Float",
+ "label": "Available Batch Qty at Warehouse",
+ "no_copy": 1,
+ "print_hide": 1,
+ "print_width": "150px",
+ "read_only": 1,
+ "width": "150px"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "actual_qty",
+ "fieldtype": "Float",
+ "label": "Available Qty at Warehouse",
+ "oldfieldname": "actual_qty",
+ "oldfieldtype": "Currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "edit_references",
+ "fieldtype": "Section Break",
+ "label": "References"
+ },
+ {
+ "fieldname": "sales_order",
+ "fieldtype": "Link",
+ "label": "Sales Order",
+ "no_copy": 1,
+ "oldfieldname": "sales_order",
+ "oldfieldtype": "Link",
+ "options": "Sales Order",
+ "print_hide": 1,
+ "read_only": 1,
+ "search_index": 1
+ },
+ {
+ "fieldname": "so_detail",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Sales Order Item",
+ "no_copy": 1,
+ "oldfieldname": "so_detail",
+ "oldfieldtype": "Data",
+ "print_hide": 1,
+ "read_only": 1,
+ "search_index": 1
+ },
+ {
+ "fieldname": "column_break_74",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "delivery_note",
+ "fieldtype": "Link",
+ "label": "Delivery Note",
+ "no_copy": 1,
+ "oldfieldname": "delivery_note",
+ "oldfieldtype": "Link",
+ "options": "Delivery Note",
+ "print_hide": 1,
+ "read_only": 1,
+ "search_index": 1
+ },
+ {
+ "fieldname": "dn_detail",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Delivery Note Item",
+ "no_copy": 1,
+ "oldfieldname": "dn_detail",
+ "oldfieldtype": "Data",
+ "print_hide": 1,
+ "read_only": 1,
+ "search_index": 1
+ },
+ {
+ "fieldname": "delivered_qty",
+ "fieldtype": "Float",
+ "label": "Delivered Qty",
+ "oldfieldname": "delivered_qty",
+ "oldfieldtype": "Currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "accounting_dimensions_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Dimensions"
+ },
+ {
+ "default": ":Company",
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "oldfieldname": "cost_center",
+ "oldfieldtype": "Link",
+ "options": "Cost Center",
+ "print_hide": 1,
+ "print_width": "120px",
+ "reqd": 1,
+ "width": "120px"
+ },
+ {
+ "fieldname": "dimension_col_break",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_54",
+ "fieldtype": "Section Break"
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "0",
+ "fieldname": "page_break",
+ "fieldtype": "Check",
+ "label": "Page Break",
+ "no_copy": 1,
+ "print_hide": 1,
+ "report_hide": 1
+ },
+ {
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "label": "Project",
+ "options": "Project"
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-07-22 13:40:34.418346",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "POS Invoice Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py
new file mode 100644
index 0000000..92ce61b
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class POSInvoiceItem(Document):
+ pass
diff --git a/erpnext/accounts/page/pos/__init__.py b/erpnext/accounts/doctype/pos_invoice_merge_log/__init__.py
similarity index 100%
copy from erpnext/accounts/page/pos/__init__.py
copy to erpnext/accounts/doctype/pos_invoice_merge_log/__init__.py
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.js b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.js
new file mode 100644
index 0000000..cd08efc
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('POS Invoice Merge Log', {
+ setup: function(frm) {
+ frm.set_query("pos_invoice", "pos_invoices", doc => {
+ return{
+ filters: {
+ 'docstatus': 1,
+ 'customer': doc.customer,
+ 'consolidated_invoice': ''
+ }
+ }
+ });
+ }
+});
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json
new file mode 100644
index 0000000..8f97639
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json
@@ -0,0 +1,147 @@
+{
+ "actions": [],
+ "creation": "2020-01-28 11:56:33.945372",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "posting_date",
+ "customer",
+ "section_break_3",
+ "pos_invoices",
+ "references_section",
+ "consolidated_invoice",
+ "column_break_7",
+ "consolidated_credit_note",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Posting Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Customer",
+ "options": "Customer",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_3",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "pos_invoices",
+ "fieldtype": "Table",
+ "label": "POS Invoices",
+ "options": "POS Invoice Reference",
+ "reqd": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "references_section",
+ "fieldtype": "Section Break",
+ "label": "References"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "POS Invoice Merge Log",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "consolidated_invoice",
+ "fieldtype": "Link",
+ "label": "Consolidated Sales Invoice",
+ "options": "Sales Invoice",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "consolidated_credit_note",
+ "fieldtype": "Link",
+ "label": "Consolidated Credit Note",
+ "options": "Sales Invoice",
+ "read_only": 1
+ }
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-05-29 15:08:41.317100",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "POS Invoice Merge Log",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Administrator",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
new file mode 100644
index 0000000..00dbad5
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -0,0 +1,180 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate
+from frappe.model.document import Document
+from frappe.model.mapper import map_doc
+from frappe.model import default_fields
+
+from six import iteritems
+
+class POSInvoiceMergeLog(Document):
+ def validate(self):
+ self.validate_customer()
+ self.validate_pos_invoice_status()
+
+ def validate_customer(self):
+ for d in self.pos_invoices:
+ if d.customer != self.customer:
+ frappe.throw(_("Row #{}: POS Invoice {} is not against customer {}").format(d.idx, d.pos_invoice, self.customer))
+
+ def validate_pos_invoice_status(self):
+ for d in self.pos_invoices:
+ status, docstatus = frappe.db.get_value('POS Invoice', d.pos_invoice, ['status', 'docstatus'])
+ if docstatus != 1:
+ frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, d.pos_invoice))
+ if status in ['Consolidated']:
+ frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, d.pos_invoice, status))
+
+ def on_submit(self):
+ pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
+
+ returns = [d for d in pos_invoice_docs if d.get('is_return') == 1]
+ sales = [d for d in pos_invoice_docs if d.get('is_return') == 0]
+
+ sales_invoice = self.process_merging_into_sales_invoice(sales)
+
+ if len(returns):
+ credit_note = self.process_merging_into_credit_note(returns)
+ else:
+ credit_note = ""
+
+ self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
+
+ self.update_pos_invoices(sales_invoice, credit_note)
+
+ def process_merging_into_sales_invoice(self, data):
+ sales_invoice = self.get_new_sales_invoice()
+
+ sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
+
+ sales_invoice.is_consolidated = 1
+ sales_invoice.save()
+ sales_invoice.submit()
+ self.consolidated_invoice = sales_invoice.name
+
+ return sales_invoice.name
+
+ def process_merging_into_credit_note(self, data):
+ credit_note = self.get_new_sales_invoice()
+ credit_note.is_return = 1
+
+ credit_note = self.merge_pos_invoice_into(credit_note, data)
+
+ credit_note.is_consolidated = 1
+ # TODO: return could be against multiple sales invoice which could also have been consolidated?
+ credit_note.return_against = self.consolidated_invoice
+ credit_note.save()
+ credit_note.submit()
+ self.consolidated_credit_note = credit_note.name
+
+ return credit_note.name
+
+ def merge_pos_invoice_into(self, invoice, data):
+ items, payments, taxes = [], [], []
+ loyalty_amount_sum, loyalty_points_sum = 0, 0
+ for doc in data:
+ map_doc(doc, invoice, table_map={ "doctype": invoice.doctype })
+
+ if doc.redeem_loyalty_points:
+ invoice.loyalty_redemption_account = doc.loyalty_redemption_account
+ invoice.loyalty_redemption_cost_center = doc.loyalty_redemption_cost_center
+ loyalty_points_sum += doc.loyalty_points
+ loyalty_amount_sum += doc.loyalty_amount
+
+ for item in doc.get('items'):
+ items.append(item)
+
+ for tax in doc.get('taxes'):
+ found = False
+ for t in taxes:
+ if t.account_head == tax.account_head and t.cost_center == tax.cost_center and t.rate == tax.rate:
+ t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount)
+ t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount)
+ found = True
+ if not found:
+ tax.charge_type = 'Actual'
+ taxes.append(tax)
+
+ for payment in doc.get('payments'):
+ found = False
+ for pay in payments:
+ if pay.account == payment.account and pay.mode_of_payment == payment.mode_of_payment:
+ pay.amount = flt(pay.amount) + flt(payment.amount)
+ pay.base_amount = flt(pay.base_amount) + flt(payment.base_amount)
+ found = True
+ if not found:
+ payments.append(payment)
+
+ if loyalty_points_sum:
+ invoice.redeem_loyalty_points = 1
+ invoice.loyalty_points = loyalty_points_sum
+ invoice.loyalty_amount = loyalty_amount_sum
+
+ invoice.set('items', items)
+ invoice.set('payments', payments)
+ invoice.set('taxes', taxes)
+
+ return invoice
+
+ def get_new_sales_invoice(self):
+ sales_invoice = frappe.new_doc('Sales Invoice')
+ sales_invoice.customer = self.customer
+ sales_invoice.is_pos = 1
+ # date can be pos closing date?
+ sales_invoice.posting_date = getdate(nowdate())
+
+ return sales_invoice
+
+ def update_pos_invoices(self, sales_invoice, credit_note):
+ for d in self.pos_invoices:
+ doc = frappe.get_doc('POS Invoice', d.pos_invoice)
+ if not doc.is_return:
+ doc.update({'consolidated_invoice': sales_invoice})
+ else:
+ doc.update({'consolidated_invoice': credit_note})
+ doc.set_status(update=True)
+ doc.save()
+
+def get_all_invoices():
+ filters = {
+ 'consolidated_invoice': [ 'in', [ '', None ]],
+ 'status': ['not in', ['Consolidated']],
+ 'docstatus': 1
+ }
+ pos_invoices = frappe.db.get_all('POS Invoice', filters=filters,
+ fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer'])
+
+ return pos_invoices
+
+def get_invoices_customer_map(pos_invoices):
+ # pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Custoemr 2' : [{}] }
+ pos_invoice_customer_map = {}
+ for invoice in pos_invoices:
+ customer = invoice.get('customer')
+ pos_invoice_customer_map.setdefault(customer, [])
+ pos_invoice_customer_map[customer].append(invoice)
+
+ return pos_invoice_customer_map
+
+def merge_pos_invoices(pos_invoices=[]):
+ if not pos_invoices:
+ pos_invoices = get_all_invoices()
+
+ pos_invoice_map = get_invoices_customer_map(pos_invoices)
+ create_merge_logs(pos_invoice_map)
+
+def create_merge_logs(pos_invoice_customer_map):
+ for customer, invoices in iteritems(pos_invoice_customer_map):
+ merge_log = frappe.new_doc('POS Invoice Merge Log')
+ merge_log.posting_date = getdate(nowdate())
+ merge_log.customer = customer
+
+ merge_log.set('pos_invoices', invoices)
+ merge_log.save(ignore_permissions=True)
+ merge_log.submit()
+
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
new file mode 100644
index 0000000..0f34272
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
+from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
+from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
+from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
+
+class TestPOSInvoiceMergeLog(unittest.TestCase):
+ def test_consolidated_invoice_creation(self):
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
+ test_user, pos_profile = init_user_and_profile()
+
+ pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
+ pos_inv.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
+ })
+ pos_inv.submit()
+
+ pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
+ pos_inv2.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
+ })
+ pos_inv2.submit()
+
+ pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
+ pos_inv3.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300
+ })
+ pos_inv3.submit()
+
+ merge_pos_invoices()
+
+ pos_inv.load_from_db()
+ self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
+
+ pos_inv3.load_from_db()
+ self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
+
+ self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice)
+
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabPOS Profile`")
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
+ def test_consolidated_credit_note_creation(self):
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
+ test_user, pos_profile = init_user_and_profile()
+
+ pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
+ pos_inv.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
+ })
+ pos_inv.submit()
+
+ pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
+ pos_inv2.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
+ })
+ pos_inv2.submit()
+
+ pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
+ pos_inv3.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300
+ })
+ pos_inv3.submit()
+
+ pos_inv_cn = make_sales_return(pos_inv.name)
+ pos_inv_cn.set("payments", [])
+ pos_inv_cn.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300
+ })
+ pos_inv_cn.paid_amount = -300
+ pos_inv_cn.submit()
+
+ merge_pos_invoices()
+
+ pos_inv.load_from_db()
+ self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
+
+ pos_inv3.load_from_db()
+ self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
+
+ pos_inv_cn.load_from_db()
+ self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice))
+ self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return"))
+
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabPOS Profile`")
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
+
diff --git a/erpnext/accounts/page/pos/__init__.py b/erpnext/accounts/doctype/pos_invoice_reference/__init__.py
similarity index 100%
copy from erpnext/accounts/page/pos/__init__.py
copy to erpnext/accounts/doctype/pos_invoice_reference/__init__.py
diff --git a/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json b/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json
new file mode 100644
index 0000000..205c4ed
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json
@@ -0,0 +1,65 @@
+{
+ "actions": [],
+ "creation": "2020-01-28 11:54:47.149392",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "pos_invoice",
+ "posting_date",
+ "column_break_3",
+ "customer",
+ "grand_total"
+ ],
+ "fields": [
+ {
+ "fieldname": "pos_invoice",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "POS Invoice",
+ "options": "POS Invoice",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "pos_invoice.customer",
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "label": "Customer",
+ "options": "Customer",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fetch_from": "pos_invoice.posting_date",
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Date",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "pos_invoice.grand_total",
+ "fieldname": "grand_total",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Amount",
+ "reqd": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-05-29 15:08:42.194979",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "POS Invoice Reference",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.py b/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.py
new file mode 100644
index 0000000..4c45265
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class POSInvoiceReference(Document):
+ pass
diff --git a/erpnext/accounts/page/pos/__init__.py b/erpnext/accounts/doctype/pos_opening_entry/__init__.py
similarity index 100%
copy from erpnext/accounts/page/pos/__init__.py
copy to erpnext/accounts/doctype/pos_opening_entry/__init__.py
diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.js b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.js
new file mode 100644
index 0000000..372e756
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.js
@@ -0,0 +1,56 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('POS Opening Entry', {
+ setup(frm) {
+ if (frm.doc.docstatus == 0) {
+ frm.trigger('set_posting_date_read_only');
+ frm.set_value('period_start_date', frappe.datetime.now_datetime());
+ frm.set_value('user', frappe.session.user);
+ }
+
+ frm.set_query("user", function(doc) {
+ return {
+ query: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_cashiers",
+ filters: { 'parent': doc.pos_profile }
+ };
+ });
+ },
+
+ refresh(frm) {
+ // set default posting date / time
+ if(frm.doc.docstatus == 0) {
+ if(!frm.doc.posting_date) {
+ frm.set_value('posting_date', frappe.datetime.nowdate());
+ }
+ frm.trigger('set_posting_date_read_only');
+ }
+ },
+
+ set_posting_date_read_only(frm) {
+ if(frm.doc.docstatus == 0 && frm.doc.set_posting_date) {
+ frm.set_df_property('posting_date', 'read_only', 0);
+ } else {
+ frm.set_df_property('posting_date', 'read_only', 1);
+ }
+ },
+
+ set_posting_date(frm) {
+ frm.trigger('set_posting_date_read_only');
+ },
+
+ pos_profile: (frm) => {
+ if (frm.doc.pos_profile) {
+ frappe.db.get_doc("POS Profile", frm.doc.pos_profile)
+ .then(({ payments }) => {
+ if (payments.length) {
+ frm.doc.balance_details = [];
+ payments.forEach(({ mode_of_payment }) => {
+ frm.add_child("balance_details", { mode_of_payment });
+ })
+ frm.refresh_field("balance_details");
+ }
+ });
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.json b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.json
new file mode 100644
index 0000000..de729ce
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.json
@@ -0,0 +1,185 @@
+{
+ "actions": [],
+ "autoname": "POS-OPE-.YYYY.-.#####",
+ "creation": "2020-03-05 16:58:53.083708",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "period_start_date",
+ "period_end_date",
+ "status",
+ "column_break_3",
+ "posting_date",
+ "set_posting_date",
+ "section_break_5",
+ "company",
+ "pos_profile",
+ "pos_closing_entry",
+ "column_break_7",
+ "user",
+ "opening_balance_details_section",
+ "balance_details",
+ "section_break_9",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "period_start_date",
+ "fieldtype": "Datetime",
+ "in_list_view": 1,
+ "label": "Period Start Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "period_end_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Period End Date",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "Today",
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Posting Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "pos_profile",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "POS Profile",
+ "options": "POS Profile",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "label": "Cashier",
+ "options": "User",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "POS Opening Entry",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "set_posting_date",
+ "fieldtype": "Check",
+ "label": "Set Posting Date"
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "Draft",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "hidden": 1,
+ "label": "Status",
+ "options": "Draft\nOpen\nClosed\nCancelled",
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "pos_closing_entry",
+ "fieldtype": "Data",
+ "label": "POS Closing Entry",
+ "read_only": 1
+ },
+ {
+ "fieldname": "opening_balance_details_section",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "balance_details",
+ "fieldtype": "Table",
+ "label": "Opening Balance Details",
+ "options": "POS Opening Entry Detail",
+ "reqd": 1
+ }
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-05-29 15:08:40.955310",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "POS Opening Entry",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Administrator",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py
new file mode 100644
index 0000000..15f23b6
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+from frappe.utils import cint
+from frappe.model.document import Document
+from erpnext.controllers.status_updater import StatusUpdater
+
+class POSOpeningEntry(StatusUpdater):
+ def validate(self):
+ self.validate_pos_profile_and_cashier()
+ self.set_status()
+
+ def validate_pos_profile_and_cashier(self):
+ if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
+ frappe.throw(_("POS Profile {} does not belongs to company {}".format(self.pos_profile, self.company)))
+
+ if not cint(frappe.db.get_value("User", self.user, "enabled")):
+ frappe.throw(_("User {} has been disabled. Please select valid user/cashier".format(self.user)))
+
+ def on_submit(self):
+ self.set_status(update=True)
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry_list.js b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry_list.js
new file mode 100644
index 0000000..6c26ded
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry_list.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+// License: GNU General Public License v3. See license.txt
+
+// render
+frappe.listview_settings['POS Opening Entry'] = {
+ get_indicator: function(doc) {
+ var status_color = {
+ "Draft": "grey",
+ "Open": "orange",
+ "Closed": "green",
+ "Cancelled": "red"
+
+ };
+ return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
+ }
+};
diff --git a/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py
new file mode 100644
index 0000000..2e36391
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+
+class TestPOSOpeningEntry(unittest.TestCase):
+ pass
+
+def create_opening_entry(pos_profile, user):
+ entry = frappe.new_doc("POS Opening Entry")
+ entry.pos_profile = pos_profile.name
+ entry.user = user
+ entry.company = pos_profile.company
+ entry.period_start_date = frappe.utils.get_datetime()
+
+ balance_details = [];
+ for d in pos_profile.payments:
+ balance_details.append(frappe._dict({
+ 'mode_of_payment': d.mode_of_payment
+ }))
+
+ entry.set("balance_details", balance_details)
+ entry.submit()
+
+ return entry.as_dict()
diff --git a/erpnext/accounts/page/pos/__init__.py b/erpnext/accounts/doctype/pos_opening_entry_detail/__init__.py
similarity index 100%
copy from erpnext/accounts/page/pos/__init__.py
copy to erpnext/accounts/doctype/pos_opening_entry_detail/__init__.py
diff --git a/erpnext/accounts/doctype/pos_opening_entry_detail/pos_opening_entry_detail.json b/erpnext/accounts/doctype/pos_opening_entry_detail/pos_opening_entry_detail.json
new file mode 100644
index 0000000..c23e3df
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_opening_entry_detail/pos_opening_entry_detail.json
@@ -0,0 +1,42 @@
+{
+ "actions": [],
+ "creation": "2020-04-28 16:44:32.440794",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "mode_of_payment",
+ "opening_amount"
+ ],
+ "fields": [
+ {
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Mode of Payment",
+ "options": "Mode of Payment",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "opening_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Opening Amount",
+ "options": "company:company_currency",
+ "reqd": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-05-29 15:08:41.949378",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "POS Opening Entry Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_opening_entry_detail/pos_opening_entry_detail.py b/erpnext/accounts/doctype/pos_opening_entry_detail/pos_opening_entry_detail.py
new file mode 100644
index 0000000..5557062
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_opening_entry_detail/pos_opening_entry_detail.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class POSOpeningEntryDetail(Document):
+ pass
diff --git a/erpnext/accounts/page/pos/__init__.py b/erpnext/accounts/doctype/pos_payment_method/__init__.py
similarity index 100%
copy from erpnext/accounts/page/pos/__init__.py
copy to erpnext/accounts/doctype/pos_payment_method/__init__.py
diff --git a/erpnext/accounts/doctype/pos_payment_method/pos_payment_method.json b/erpnext/accounts/doctype/pos_payment_method/pos_payment_method.json
new file mode 100644
index 0000000..4d5e1eb
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_payment_method/pos_payment_method.json
@@ -0,0 +1,40 @@
+{
+ "actions": [],
+ "creation": "2020-04-30 14:37:08.148707",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "default",
+ "mode_of_payment"
+ ],
+ "fields": [
+ {
+ "default": "0",
+ "depends_on": "eval:parent.doctype == 'POS Profile'",
+ "fieldname": "default",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Default"
+ },
+ {
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Mode of Payment",
+ "options": "Mode of Payment",
+ "reqd": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-05-29 15:08:41.704844",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "POS Payment Method",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_payment_method/pos_payment_method.py b/erpnext/accounts/doctype/pos_payment_method/pos_payment_method.py
new file mode 100644
index 0000000..8a46d84
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_payment_method/pos_payment_method.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class POSPaymentMethod(Document):
+ pass
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js
index 5e94118..ef431d7 100755
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.js
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js
@@ -28,7 +28,7 @@
frappe.ui.form.on('POS Profile', {
setup: function(frm) {
- frm.set_query("print_format_for_online", function() {
+ frm.set_query("print_format", function() {
return {
filters: [
['Print Format', 'doc_type', '=', 'Sales Invoice'],
@@ -49,12 +49,6 @@
return { filters: { doc_type: "Sales Invoice", print_format_type: "JS"} };
});
- frappe.db.get_value('POS Settings', 'POS Settings', 'use_pos_in_offline_mode', (r) => {
- const is_offline = r && cint(r.use_pos_in_offline_mode)
- frm.toggle_display('offline_pos_section', is_offline);
- frm.toggle_display('print_format_for_online', !is_offline);
- });
-
frm.set_query('company_address', function(doc) {
if(!doc.company) {
frappe.throw(__('Please set Company'));
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json
index fba1bed..454c598 100644
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.json
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2013-05-24 12:15:51",
@@ -11,17 +12,12 @@
"customer",
"company",
"country",
- "warehouse",
- "campaign",
- "company_address",
"column_break_9",
"update_stock",
"ignore_pricing_rule",
- "allow_delete",
- "allow_user_to_edit_rate",
- "allow_user_to_edit_discount",
- "allow_print_before_pay",
- "display_items_in_stock",
+ "warehouse",
+ "campaign",
+ "company_address",
"section_break_15",
"applicable_for_users",
"section_break_11",
@@ -31,16 +27,11 @@
"column_break_16",
"customer_groups",
"section_break_16",
- "print_format_for_online",
+ "print_format",
"letter_head",
"column_break0",
"tc_name",
"select_print_heading",
- "offline_pos_section",
- "territory",
- "column_break_31",
- "print_format",
- "customer_group",
"section_break_19",
"selling_price_list",
"currency",
@@ -105,15 +96,6 @@
"label": "Country"
},
{
- "depends_on": "update_stock",
- "fieldname": "warehouse",
- "fieldtype": "Link",
- "label": "Warehouse",
- "oldfieldname": "warehouse",
- "oldfieldtype": "Link",
- "options": "Warehouse"
- },
- {
"fieldname": "campaign",
"fieldtype": "Link",
"label": "Campaign",
@@ -130,48 +112,6 @@
"fieldtype": "Column Break"
},
{
- "default": "1",
- "fieldname": "update_stock",
- "fieldtype": "Check",
- "label": "Update Stock"
- },
- {
- "default": "0",
- "fieldname": "ignore_pricing_rule",
- "fieldtype": "Check",
- "label": "Ignore Pricing Rule"
- },
- {
- "default": "0",
- "fieldname": "allow_delete",
- "fieldtype": "Check",
- "label": "Allow Delete"
- },
- {
- "default": "0",
- "fieldname": "allow_user_to_edit_rate",
- "fieldtype": "Check",
- "label": "Allow user to edit Rate"
- },
- {
- "default": "0",
- "fieldname": "allow_user_to_edit_discount",
- "fieldtype": "Check",
- "label": "Allow user to edit Discount"
- },
- {
- "default": "0",
- "fieldname": "allow_print_before_pay",
- "fieldtype": "Check",
- "label": "Allow Print Before Pay"
- },
- {
- "default": "0",
- "fieldname": "display_items_in_stock",
- "fieldtype": "Check",
- "label": "Display Items In Stock"
- },
- {
"fieldname": "section_break_15",
"fieldtype": "Section Break",
"label": "Applicable for Users"
@@ -185,13 +125,13 @@
{
"fieldname": "section_break_11",
"fieldtype": "Section Break",
- "label": "Mode of Payment"
+ "label": "Payment Methods"
},
{
"fieldname": "payments",
"fieldtype": "Table",
- "label": "Sales Invoice Payment",
- "options": "Sales Invoice Payment"
+ "options": "POS Payment Method",
+ "reqd": 1
},
{
"fieldname": "section_break_14",
@@ -221,12 +161,6 @@
"label": "Print Settings"
},
{
- "fieldname": "print_format_for_online",
- "fieldtype": "Link",
- "label": "Print Format for Online",
- "options": "Print Format"
- },
- {
"allow_on_submit": 1,
"fieldname": "letter_head",
"fieldtype": "Link",
@@ -259,39 +193,6 @@
"options": "Print Heading"
},
{
- "fieldname": "offline_pos_section",
- "fieldtype": "Section Break",
- "label": "Offline POS Settings"
- },
- {
- "fieldname": "territory",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Territory",
- "oldfieldname": "territory",
- "oldfieldtype": "Link",
- "options": "Territory",
- "reqd": 1
- },
- {
- "fieldname": "column_break_31",
- "fieldtype": "Column Break"
- },
- {
- "default": "Point of Sale",
- "fieldname": "print_format",
- "fieldtype": "Link",
- "label": "Print Format",
- "options": "Print Format"
- },
- {
- "fieldname": "customer_group",
- "fieldtype": "Link",
- "label": "Customer Group",
- "options": "Customer Group",
- "reqd": 1
- },
- {
"fieldname": "section_break_19",
"fieldtype": "Section Break",
"label": "Accounting"
@@ -381,19 +282,48 @@
"label": "Accounting Dimensions"
},
{
- "fieldname": "dimension_col_break",
- "fieldtype": "Column Break"
- },
- {
"fieldname": "tax_category",
"fieldtype": "Link",
"label": "Tax Category",
"options": "Tax Category"
+ },
+ {
+ "fieldname": "dimension_col_break",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "print_format",
+ "fieldtype": "Link",
+ "label": "Print Format",
+ "options": "Print Format"
+ },
+ {
+ "depends_on": "update_stock",
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Warehouse",
+ "oldfieldname": "warehouse",
+ "oldfieldtype": "Link",
+ "options": "Warehouse",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "update_stock",
+ "fieldtype": "Check",
+ "label": "Update Stock"
+ },
+ {
+ "default": "0",
+ "fieldname": "ignore_pricing_rule",
+ "fieldtype": "Check",
+ "label": "Ignore Pricing Rule"
}
],
"icon": "icon-cog",
"idx": 1,
- "modified": "2020-01-24 15:52:03.797701",
+ "links": [],
+ "modified": "2020-06-29 12:20:30.977272",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py
index f186967..8655b4b 100644
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.py
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py
@@ -5,8 +5,6 @@
import frappe
from frappe import msgprint, _
from frappe.utils import cint, now
-from erpnext.accounts.doctype.sales_invoice.pos import get_child_nodes
-from erpnext.accounts.doctype.sales_invoice.sales_invoice import set_account_for_mode_of_payment
from six import iteritems
from frappe.model.document import Document
@@ -16,7 +14,6 @@
self.validate_all_link_fields()
self.validate_duplicate_groups()
self.check_default_payment()
- self.validate_customer_territory_group()
def validate_default_profile(self):
for row in self.applicable_for_users:
@@ -64,19 +61,6 @@
if len(default_mode_of_payment) > 1:
frappe.throw(_("Multiple default mode of payment is not allowed"))
- def validate_customer_territory_group(self):
- if not frappe.db.get_single_value('POS Settings', 'use_pos_in_offline_mode'):
- return
-
- if not self.territory:
- frappe.throw(_("Territory is Required in POS Profile"), title="Mandatory Field")
-
- if not self.customer_group:
- frappe.throw(_("Customer Group is Required in POS Profile"), title="Mandatory Field")
-
- def before_save(self):
- set_account_for_mode_of_payment(self)
-
def on_update(self):
self.set_defaults()
@@ -111,9 +95,14 @@
return list(set(item_groups))
+def get_child_nodes(group_type, root):
+ lft, rgt = frappe.db.get_value(group_type, root, ["lft", "rgt"])
+ return frappe.db.sql(""" Select name, lft, rgt from `tab{tab}` where
+ lft >= {lft} and rgt <= {rgt} order by lft""".format(tab=group_type, lft=lft, rgt=rgt), as_dict=1)
+
@frappe.whitelist()
def get_series():
- return frappe.get_meta("Sales Invoice").get_field("naming_series").options or ""
+ return frappe.get_meta("POS Invoice").get_field("naming_series").options or "s"
@frappe.whitelist()
def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile_dashboard.py b/erpnext/accounts/doctype/pos_profile/pos_profile_dashboard.py
index e28bf73..2e4632a 100644
--- a/erpnext/accounts/doctype/pos_profile/pos_profile_dashboard.py
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile_dashboard.py
@@ -8,7 +8,7 @@
'fieldname': 'pos_profile',
'transactions': [
{
- 'items': ['Sales Invoice', 'POS Closing Voucher']
+ 'items': ['Sales Invoice', 'POS Closing Entry', 'POS Opening Entry']
}
]
}
diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
index 64d347d..8a4050c 100644
--- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
+++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
@@ -6,7 +6,7 @@
import frappe
import unittest
from erpnext.stock.get_item_details import get_pos_profile
-from erpnext.accounts.doctype.sales_invoice.pos import get_items_list, get_customers_list
+from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes
class TestPOSProfile(unittest.TestCase):
def test_pos_profile(self):
@@ -29,6 +29,44 @@
frappe.db.sql("delete from `tabPOS Profile`")
+def get_customers_list(pos_profile={}):
+ cond = "1=1"
+ customer_groups = []
+ if pos_profile.get('customer_groups'):
+ # Get customers based on the customer groups defined in the POS profile
+ for d in pos_profile.get('customer_groups'):
+ customer_groups.extend([d.get('name') for d in get_child_nodes('Customer Group', d.get('customer_group'))])
+ cond = "customer_group in (%s)" % (', '.join(['%s'] * len(customer_groups)))
+
+ return frappe.db.sql(""" select name, customer_name, customer_group,
+ territory, customer_pos_id from tabCustomer where disabled = 0
+ and {cond}""".format(cond=cond), tuple(customer_groups), as_dict=1) or {}
+
+def get_items_list(pos_profile, company):
+ cond = ""
+ args_list = []
+ if pos_profile.get('item_groups'):
+ # Get items based on the item groups defined in the POS profile
+ for d in pos_profile.get('item_groups'):
+ args_list.extend([d.name for d in get_child_nodes('Item Group', d.item_group)])
+ if args_list:
+ cond = "and i.item_group in (%s)" % (', '.join(['%s'] * len(args_list)))
+
+ return frappe.db.sql("""
+ select
+ i.name, i.item_code, i.item_name, i.description, i.item_group, i.has_batch_no,
+ i.has_serial_no, i.is_stock_item, i.brand, i.stock_uom, i.image,
+ id.expense_account, id.selling_cost_center, id.default_warehouse,
+ i.sales_uom, c.conversion_factor
+ from
+ `tabItem` i
+ left join `tabItem Default` id on id.parent = i.name and id.company = %s
+ left join `tabUOM Conversion Detail` c on i.name = c.parent and i.sales_uom = c.uom
+ where
+ i.disabled = 0 and i.has_variants = 0 and i.is_sales_item = 1 and i.is_fixed_asset = 0
+ {cond}
+ """.format(cond=cond), tuple([company] + args_list), as_dict=1)
+
def make_pos_profile(**args):
frappe.db.sql("delete from `tabPOS Profile`")
@@ -50,6 +88,12 @@
"write_off_account": args.write_off_account or "_Test Write Off - _TC",
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC"
})
+
+ payments = [{
+ 'mode_of_payment': 'Cash',
+ 'default': 1
+ }]
+ pos_profile.set("payments", payments)
if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"):
pos_profile.insert()
diff --git a/erpnext/accounts/doctype/pos_profile_user/pos_profile_user.json b/erpnext/accounts/doctype/pos_profile_user/pos_profile_user.json
index 59a673e..c8f3f5e 100644
--- a/erpnext/accounts/doctype/pos_profile_user/pos_profile_user.json
+++ b/erpnext/accounts/doctype/pos_profile_user/pos_profile_user.json
@@ -26,7 +26,7 @@
],
"istable": 1,
"links": [],
- "modified": "2020-05-01 09:46:47.599173",
+ "modified": "2020-05-13 23:57:33.627305",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile User",
diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.js b/erpnext/accounts/doctype/pos_settings/pos_settings.js
index f5b681b..504941d 100644
--- a/erpnext/accounts/doctype/pos_settings/pos_settings.js
+++ b/erpnext/accounts/doctype/pos_settings/pos_settings.js
@@ -6,27 +6,19 @@
frm.trigger("get_invoice_fields");
},
- use_pos_in_offline_mode: function(frm) {
- frm.trigger("get_invoice_fields");
- },
-
get_invoice_fields: function(frm) {
- if (!frm.doc.use_pos_in_offline_mode) {
- frappe.model.with_doctype("Sales Invoice", () => {
- var fields = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) {
- if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 ||
- d.fieldtype === 'Table') {
- return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname };
- } else {
- return null;
- }
- });
-
- frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""].concat(fields);
+ frappe.model.with_doctype("Sales Invoice", () => {
+ var fields = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) {
+ if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 ||
+ d.fieldtype === 'Table') {
+ return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname };
+ } else {
+ return null;
+ }
});
- } else {
- frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""];
- }
+
+ frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""].concat(fields);
+ });
}
});
diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.json b/erpnext/accounts/doctype/pos_settings/pos_settings.json
index 1d55880..3539588 100644
--- a/erpnext/accounts/doctype/pos_settings/pos_settings.json
+++ b/erpnext/accounts/doctype/pos_settings/pos_settings.json
@@ -5,24 +5,11 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "use_pos_in_offline_mode",
- "section_break_2",
- "fields"
+ "invoice_fields"
],
"fields": [
{
- "default": "0",
- "fieldname": "use_pos_in_offline_mode",
- "fieldtype": "Check",
- "label": "Use POS in Offline Mode"
- },
- {
- "fieldname": "section_break_2",
- "fieldtype": "Section Break"
- },
- {
- "depends_on": "eval:!doc.use_pos_in_offline_mode",
- "fieldname": "fields",
+ "fieldname": "invoice_fields",
"fieldtype": "Table",
"label": "POS Field",
"options": "POS Field"
@@ -30,7 +17,7 @@
],
"issingle": 1,
"links": [],
- "modified": "2019-12-26 11:50:47.122997",
+ "modified": "2020-06-01 15:46:41.478928",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Settings",
diff --git a/erpnext/accounts/doctype/sales_invoice/pos.py b/erpnext/accounts/doctype/sales_invoice/pos.py
deleted file mode 100755
index c49ac29..0000000
--- a/erpnext/accounts/doctype/sales_invoice/pos.py
+++ /dev/null
@@ -1,626 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-
-import json
-
-import frappe
-from erpnext.accounts.party import get_party_account_currency
-from erpnext.controllers.accounts_controller import get_taxes_and_charges
-from erpnext.setup.utils import get_exchange_rate
-from erpnext.stock.get_item_details import get_pos_profile
-from frappe import _
-from frappe.core.doctype.communication.email import make
-from frappe.utils import nowdate, cint
-
-from six import string_types, iteritems
-
-
-@frappe.whitelist()
-def get_pos_data():
- doc = frappe.new_doc('Sales Invoice')
- doc.is_pos = 1
- pos_profile = get_pos_profile(doc.company) or {}
- if not pos_profile:
- frappe.throw(_("POS Profile is required to use Point-of-Sale"))
-
- if not doc.company:
- doc.company = pos_profile.get('company')
-
- doc.update_stock = pos_profile.get('update_stock')
-
- if pos_profile.get('name'):
- pos_profile = frappe.get_doc('POS Profile', pos_profile.get('name'))
- pos_profile.validate()
-
- company_data = get_company_data(doc.company)
- update_pos_profile_data(doc, pos_profile, company_data)
- update_multi_mode_option(doc, pos_profile)
- default_print_format = pos_profile.get('print_format') or "Point of Sale"
- print_template = frappe.db.get_value('Print Format', default_print_format, 'html')
- items_list = get_items_list(pos_profile, doc.company)
- customers = get_customers_list(pos_profile)
-
- doc.plc_conversion_rate = update_plc_conversion_rate(doc, pos_profile)
-
- return {
- 'doc': doc,
- 'default_customer': pos_profile.get('customer'),
- 'items': items_list,
- 'item_groups': get_item_groups(pos_profile),
- 'customers': customers,
- 'address': get_customers_address(customers),
- 'contacts': get_contacts(customers),
- 'serial_no_data': get_serial_no_data(pos_profile, doc.company),
- 'batch_no_data': get_batch_no_data(),
- 'barcode_data': get_barcode_data(items_list),
- 'tax_data': get_item_tax_data(),
- 'price_list_data': get_price_list_data(doc.selling_price_list, doc.plc_conversion_rate),
- 'customer_wise_price_list': get_customer_wise_price_list(),
- 'bin_data': get_bin_data(pos_profile),
- 'pricing_rules': get_pricing_rule_data(doc),
- 'print_template': print_template,
- 'pos_profile': pos_profile,
- 'meta': get_meta()
- }
-
-def update_plc_conversion_rate(doc, pos_profile):
- conversion_rate = 1.0
-
- price_list_currency = frappe.get_cached_value("Price List", doc.selling_price_list, "currency")
- if pos_profile.get("currency") != price_list_currency:
- conversion_rate = get_exchange_rate(price_list_currency,
- pos_profile.get("currency"), nowdate(), args="for_selling") or 1.0
-
- return conversion_rate
-
-def get_meta():
- doctype_meta = {
- 'customer': frappe.get_meta('Customer'),
- 'invoice': frappe.get_meta('Sales Invoice')
- }
-
- for row in frappe.get_all('DocField', fields=['fieldname', 'options'],
- filters={'parent': 'Sales Invoice', 'fieldtype': 'Table'}):
- doctype_meta[row.fieldname] = frappe.get_meta(row.options)
-
- return doctype_meta
-
-
-def get_company_data(company):
- return frappe.get_all('Company', fields=["*"], filters={'name': company})[0]
-
-
-def update_pos_profile_data(doc, pos_profile, company_data):
- doc.campaign = pos_profile.get('campaign')
- if pos_profile and not pos_profile.get('country'):
- pos_profile.country = company_data.country
-
- doc.write_off_account = pos_profile.get('write_off_account') or \
- company_data.write_off_account
- doc.change_amount_account = pos_profile.get('change_amount_account') or \
- company_data.default_cash_account
- doc.taxes_and_charges = pos_profile.get('taxes_and_charges')
- if doc.taxes_and_charges:
- update_tax_table(doc)
-
- doc.currency = pos_profile.get('currency') or company_data.default_currency
- doc.conversion_rate = 1.0
-
- if doc.currency != company_data.default_currency:
- doc.conversion_rate = get_exchange_rate(doc.currency, company_data.default_currency, doc.posting_date, args="for_selling")
-
- doc.selling_price_list = pos_profile.get('selling_price_list') or \
- frappe.db.get_value('Selling Settings', None, 'selling_price_list')
- doc.naming_series = pos_profile.get('naming_series') or 'SINV-'
- doc.letter_head = pos_profile.get('letter_head') or company_data.default_letter_head
- doc.ignore_pricing_rule = pos_profile.get('ignore_pricing_rule') or 0
- doc.apply_discount_on = pos_profile.get('apply_discount_on') or 'Grand Total'
- doc.customer_group = pos_profile.get('customer_group') or get_root('Customer Group')
- doc.territory = pos_profile.get('territory') or get_root('Territory')
- doc.terms = frappe.db.get_value('Terms and Conditions', pos_profile.get('tc_name'), 'terms') or doc.terms or ''
- doc.offline_pos_name = ''
-
-
-def get_root(table):
- root = frappe.db.sql(""" select name from `tab%(table)s` having
- min(lft)""" % {'table': table}, as_dict=1)
-
- return root[0].name
-
-
-def update_multi_mode_option(doc, pos_profile):
- from frappe.model import default_fields
-
- if not pos_profile or not pos_profile.get('payments'):
- for payment in get_mode_of_payment(doc):
- payments = doc.append('payments', {})
- payments.mode_of_payment = payment.parent
- payments.account = payment.default_account
- payments.type = payment.type
-
- return
-
- for payment_mode in pos_profile.payments:
- payment_mode = payment_mode.as_dict()
-
- for fieldname in default_fields:
- if fieldname in payment_mode:
- del payment_mode[fieldname]
-
- doc.append('payments', payment_mode)
-
-
-def get_mode_of_payment(doc):
- return frappe.db.sql("""
- select mpa.default_account, mpa.parent, mp.type as type
- from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
- where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""",
- {'company': doc.company}, as_dict=1)
-
-
-def update_tax_table(doc):
- taxes = get_taxes_and_charges('Sales Taxes and Charges Template', doc.taxes_and_charges)
- for tax in taxes:
- doc.append('taxes', tax)
-
-
-def get_items_list(pos_profile, company):
- cond = ""
- args_list = []
- if pos_profile.get('item_groups'):
- # Get items based on the item groups defined in the POS profile
- for d in pos_profile.get('item_groups'):
- args_list.extend([d.name for d in get_child_nodes('Item Group', d.item_group)])
- if args_list:
- cond = "and i.item_group in (%s)" % (', '.join(['%s'] * len(args_list)))
-
- return frappe.db.sql("""
- select
- i.name, i.item_code, i.item_name, i.description, i.item_group, i.has_batch_no,
- i.has_serial_no, i.is_stock_item, i.brand, i.stock_uom, i.image,
- id.expense_account, id.selling_cost_center, id.default_warehouse,
- i.sales_uom, c.conversion_factor
- from
- `tabItem` i
- left join `tabItem Default` id on id.parent = i.name and id.company = %s
- left join `tabUOM Conversion Detail` c on i.name = c.parent and i.sales_uom = c.uom
- where
- i.disabled = 0 and i.has_variants = 0 and i.is_sales_item = 1
- {cond}
- """.format(cond=cond), tuple([company] + args_list), as_dict=1)
-
-
-def get_item_groups(pos_profile):
- item_group_dict = {}
- item_groups = frappe.db.sql("""Select name,
- lft, rgt from `tabItem Group` order by lft""", as_dict=1)
-
- for data in item_groups:
- item_group_dict[data.name] = [data.lft, data.rgt]
- return item_group_dict
-
-
-def get_customers_list(pos_profile={}):
- cond = "1=1"
- customer_groups = []
- if pos_profile.get('customer_groups'):
- # Get customers based on the customer groups defined in the POS profile
- for d in pos_profile.get('customer_groups'):
- customer_groups.extend([d.get('name') for d in get_child_nodes('Customer Group', d.get('customer_group'))])
- cond = "customer_group in (%s)" % (', '.join(['%s'] * len(customer_groups)))
-
- return frappe.db.sql(""" select name, customer_name, customer_group,
- territory, customer_pos_id from tabCustomer where disabled = 0
- and {cond}""".format(cond=cond), tuple(customer_groups), as_dict=1) or {}
-
-
-def get_customers_address(customers):
- customer_address = {}
- if isinstance(customers, string_types):
- customers = [frappe._dict({'name': customers})]
-
- for data in customers:
- address = frappe.db.sql(""" select name, address_line1, address_line2, city, state,
- email_id, phone, fax, pincode from `tabAddress` where is_primary_address =1 and name in
- (select parent from `tabDynamic Link` where link_doctype = 'Customer' and link_name = %s
- and parenttype = 'Address')""", data.name, as_dict=1)
- address_data = {}
- if address:
- address_data = address[0]
-
- address_data.update({'full_name': data.customer_name, 'customer_pos_id': data.customer_pos_id})
- customer_address[data.name] = address_data
-
- return customer_address
-
-
-def get_contacts(customers):
- customer_contact = {}
- if isinstance(customers, string_types):
- customers = [frappe._dict({'name': customers})]
-
- for data in customers:
- contact = frappe.db.sql(""" select email_id, phone, mobile_no from `tabContact`
- where is_primary_contact=1 and name in
- (select parent from `tabDynamic Link` where link_doctype = 'Customer' and link_name = %s
- and parenttype = 'Contact')""", data.name, as_dict=1)
- if contact:
- customer_contact[data.name] = contact[0]
-
- return customer_contact
-
-
-def get_child_nodes(group_type, root):
- lft, rgt = frappe.db.get_value(group_type, root, ["lft", "rgt"])
- return frappe.db.sql(""" Select name, lft, rgt from `tab{tab}` where
- lft >= {lft} and rgt <= {rgt} order by lft""".format(tab=group_type, lft=lft, rgt=rgt), as_dict=1)
-
-
-def get_serial_no_data(pos_profile, company):
- # get itemwise serial no data
- # example {'Nokia Lumia 1020': {'SN0001': 'Pune'}}
- # where Nokia Lumia 1020 is item code, SN0001 is serial no and Pune is warehouse
-
- cond = "1=1"
- if pos_profile.get('update_stock') and pos_profile.get('warehouse'):
- cond = "warehouse = %(warehouse)s"
-
- serial_nos = frappe.db.sql("""select name, warehouse, item_code
- from `tabSerial No` where {0} and company = %(company)s """.format(cond),{
- 'company': company, 'warehouse': frappe.db.escape(pos_profile.get('warehouse'))
- }, as_dict=1)
-
- itemwise_serial_no = {}
- for sn in serial_nos:
- if sn.item_code not in itemwise_serial_no:
- itemwise_serial_no.setdefault(sn.item_code, {})
- itemwise_serial_no[sn.item_code][sn.name] = sn.warehouse
-
- return itemwise_serial_no
-
-
-def get_batch_no_data():
- # get itemwise batch no data
- # exmaple: {'LED-GRE': [Batch001, Batch002]}
- # where LED-GRE is item code, SN0001 is serial no and Pune is warehouse
-
- itemwise_batch = {}
- batches = frappe.db.sql("""select name, item from `tabBatch`
- where ifnull(expiry_date, '4000-10-10') >= curdate()""", as_dict=1)
-
- for batch in batches:
- if batch.item not in itemwise_batch:
- itemwise_batch.setdefault(batch.item, [])
- itemwise_batch[batch.item].append(batch.name)
-
- return itemwise_batch
-
-
-def get_barcode_data(items_list):
- # get itemwise batch no data
- # exmaple: {'LED-GRE': [Batch001, Batch002]}
- # where LED-GRE is item code, SN0001 is serial no and Pune is warehouse
-
- itemwise_barcode = {}
- for item in items_list:
- barcodes = frappe.db.sql("""
- select barcode from `tabItem Barcode` where parent = %s
- """, item.item_code, as_dict=1)
-
- for barcode in barcodes:
- if item.item_code not in itemwise_barcode:
- itemwise_barcode.setdefault(item.item_code, [])
- itemwise_barcode[item.item_code].append(barcode.get("barcode"))
-
- return itemwise_barcode
-
-
-def get_item_tax_data():
- # get default tax of an item
- # example: {'Consulting Services': {'Excise 12 - TS': '12.000'}}
-
- itemwise_tax = {}
- taxes = frappe.db.sql(""" select parent, tax_type, tax_rate from `tabItem Tax Template Detail`""", as_dict=1)
-
- for tax in taxes:
- if tax.parent not in itemwise_tax:
- itemwise_tax.setdefault(tax.parent, {})
- itemwise_tax[tax.parent][tax.tax_type] = tax.tax_rate
-
- return itemwise_tax
-
-
-def get_price_list_data(selling_price_list, conversion_rate):
- itemwise_price_list = {}
- price_lists = frappe.db.sql("""Select ifnull(price_list_rate, 0) as price_list_rate,
- item_code from `tabItem Price` ip where price_list = %(price_list)s""",
- {'price_list': selling_price_list}, as_dict=1)
-
- for item in price_lists:
- itemwise_price_list[item.item_code] = item.price_list_rate * conversion_rate
-
- return itemwise_price_list
-
-def get_customer_wise_price_list():
- customer_wise_price = {}
- customer_price_list_mapping = frappe._dict(frappe.get_all('Customer',fields = ['default_price_list', 'name'], as_list=1))
-
- price_lists = frappe.db.sql(""" Select ifnull(price_list_rate, 0) as price_list_rate,
- item_code, price_list from `tabItem Price` """, as_dict=1)
-
- for item in price_lists:
- if item.price_list and customer_price_list_mapping.get(item.price_list):
-
- customer_wise_price.setdefault(customer_price_list_mapping.get(item.price_list),{}).setdefault(
- item.item_code, item.price_list_rate
- )
-
- return customer_wise_price
-
-def get_bin_data(pos_profile):
- itemwise_bin_data = {}
- filters = { 'actual_qty': ['>', 0] }
- if pos_profile.get('warehouse'):
- filters.update({ 'warehouse': pos_profile.get('warehouse') })
-
- bin_data = frappe.db.get_all('Bin', fields = ['item_code', 'warehouse', 'actual_qty'], filters=filters)
-
- for bins in bin_data:
- if bins.item_code not in itemwise_bin_data:
- itemwise_bin_data.setdefault(bins.item_code, {})
- itemwise_bin_data[bins.item_code][bins.warehouse] = bins.actual_qty
-
- return itemwise_bin_data
-
-
-def get_pricing_rule_data(doc):
- pricing_rules = ""
- if doc.ignore_pricing_rule == 0:
- pricing_rules = frappe.db.sql(""" Select * from `tabPricing Rule` where docstatus < 2
- and ifnull(for_price_list, '') in (%(price_list)s, '') and selling = 1
- and ifnull(company, '') in (%(company)s, '') and disable = 0 and %(date)s
- between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')
- order by priority desc, name desc""",
- {'company': doc.company, 'price_list': doc.selling_price_list, 'date': nowdate()}, as_dict=1)
- return pricing_rules
-
-
-@frappe.whitelist()
-def make_invoice(pos_profile, doc_list={}, email_queue_list={}, customers_list={}):
- import json
-
- if isinstance(doc_list, string_types):
- doc_list = json.loads(doc_list)
-
- if isinstance(email_queue_list, string_types):
- email_queue_list = json.loads(email_queue_list)
-
- if isinstance(customers_list, string_types):
- customers_list = json.loads(customers_list)
-
- customers_list = make_customer_and_address(customers_list)
- name_list = []
- for docs in doc_list:
- for name, doc in iteritems(docs):
- if not frappe.db.exists('Sales Invoice', {'offline_pos_name': name}):
- if isinstance(doc, dict):
- validate_records(doc)
- si_doc = frappe.new_doc('Sales Invoice')
- si_doc.offline_pos_name = name
- si_doc.update(doc)
- si_doc.set_posting_time = 1
- si_doc.customer = get_customer_id(doc)
- si_doc.due_date = doc.get('posting_date')
- name_list = submit_invoice(si_doc, name, doc, name_list)
- else:
- doc.due_date = doc.get('posting_date')
- doc.customer = get_customer_id(doc)
- doc.set_posting_time = 1
- doc.offline_pos_name = name
- name_list = submit_invoice(doc, name, doc, name_list)
- else:
- name_list.append(name)
-
- email_queue = make_email_queue(email_queue_list)
-
- if isinstance(pos_profile, string_types):
- pos_profile = json.loads(pos_profile)
-
- customers = get_customers_list(pos_profile)
- return {
- 'invoice': name_list,
- 'email_queue': email_queue,
- 'customers': customers_list,
- 'synced_customers_list': customers,
- 'synced_address': get_customers_address(customers),
- 'synced_contacts': get_contacts(customers)
- }
-
-
-def validate_records(doc):
- validate_item(doc)
-
-
-def get_customer_id(doc, customer=None):
- cust_id = None
- if doc.get('customer_pos_id'):
- cust_id = frappe.db.get_value('Customer',{'customer_pos_id': doc.get('customer_pos_id')}, 'name')
-
- if not cust_id:
- customer = customer or doc.get('customer')
- if frappe.db.exists('Customer', customer):
- cust_id = customer
- else:
- cust_id = add_customer(doc)
-
- return cust_id
-
-def make_customer_and_address(customers):
- customers_list = []
- for customer, data in iteritems(customers):
- data = json.loads(data)
- cust_id = get_customer_id(data, customer)
- if not cust_id:
- cust_id = add_customer(data)
- else:
- frappe.db.set_value("Customer", cust_id, "customer_name", data.get('full_name'))
-
- make_contact(data, cust_id)
- make_address(data, cust_id)
- customers_list.append(customer)
- frappe.db.commit()
- return customers_list
-
-def add_customer(data):
- customer = data.get('full_name') or data.get('customer')
- if frappe.db.exists("Customer", customer.strip()):
- return customer.strip()
-
- customer_doc = frappe.new_doc('Customer')
- customer_doc.customer_name = data.get('full_name') or data.get('customer')
- customer_doc.customer_pos_id = data.get('customer_pos_id')
- customer_doc.customer_type = 'Company'
- customer_doc.customer_group = get_customer_group(data)
- customer_doc.territory = get_territory(data)
- customer_doc.flags.ignore_mandatory = True
- customer_doc.save(ignore_permissions=True)
- frappe.db.commit()
- return customer_doc.name
-
-def get_territory(data):
- if data.get('territory'):
- return data.get('territory')
-
- return frappe.db.get_single_value('Selling Settings','territory') or _('All Territories')
-
-def get_customer_group(data):
- if data.get('customer_group'):
- return data.get('customer_group')
-
- return frappe.db.get_single_value('Selling Settings', 'customer_group') or frappe.db.get_value('Customer Group', {'is_group': 0}, 'name')
-
-def make_contact(args, customer):
- if args.get('email_id') or args.get('phone'):
- name = frappe.db.get_value('Dynamic Link',
- {'link_doctype': 'Customer', 'link_name': customer, 'parenttype': 'Contact'}, 'parent')
-
- args = {
- 'first_name': args.get('full_name'),
- 'email_id': args.get('email_id'),
- 'phone': args.get('phone')
- }
-
- doc = frappe.new_doc('Contact')
- if name:
- doc = frappe.get_doc('Contact', name)
-
- doc.update(args)
- doc.is_primary_contact = 1
- if not name:
- doc.append('links', {
- 'link_doctype': 'Customer',
- 'link_name': customer
- })
- doc.flags.ignore_mandatory = True
- doc.save(ignore_permissions=True)
-
-def make_address(args, customer):
- if not args.get('address_line1'):
- return
-
- name = args.get('name')
-
- if not name:
- data = get_customers_address(customer)
- name = data[customer].get('name') if data else None
-
- if name:
- address = frappe.get_doc('Address', name)
- else:
- address = frappe.new_doc('Address')
- if args.get('company'):
- address.country = frappe.get_cached_value('Company',
- args.get('company'), 'country')
-
- address.append('links', {
- 'link_doctype': 'Customer',
- 'link_name': customer
- })
-
- address.is_primary_address = 1
- address.is_shipping_address = 1
- address.update(args)
- address.flags.ignore_mandatory = True
- address.save(ignore_permissions=True)
-
-def make_email_queue(email_queue):
- name_list = []
-
- for key, data in iteritems(email_queue):
- name = frappe.db.get_value('Sales Invoice', {'offline_pos_name': key}, 'name')
- if not name: continue
-
- data = json.loads(data)
- sender = frappe.session.user
- print_format = "POS Invoice" if not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')) else None
-
- attachments = [frappe.attach_print('Sales Invoice', name, print_format=print_format)]
-
- make(subject=data.get('subject'), content=data.get('content'), recipients=data.get('recipients'),
- sender=sender, attachments=attachments, send_email=True,
- doctype='Sales Invoice', name=name)
- name_list.append(key)
-
- return name_list
-
-def validate_item(doc):
- for item in doc.get('items'):
- if not frappe.db.exists('Item', item.get('item_code')):
- item_doc = frappe.new_doc('Item')
- item_doc.name = item.get('item_code')
- item_doc.item_code = item.get('item_code')
- item_doc.item_name = item.get('item_name')
- item_doc.description = item.get('description')
- item_doc.stock_uom = item.get('stock_uom')
- item_doc.uom = item.get('uom')
- item_doc.item_group = item.get('item_group')
- item_doc.append('item_defaults', {
- "company": doc.get("company"),
- "default_warehouse": item.get('warehouse')
- })
- item_doc.save(ignore_permissions=True)
- frappe.db.commit()
-
-def submit_invoice(si_doc, name, doc, name_list):
- try:
- si_doc.insert()
- si_doc.submit()
- frappe.db.commit()
- name_list.append(name)
- except Exception as e:
- if frappe.message_log:
- frappe.message_log.pop()
- frappe.db.rollback()
- frappe.log_error(frappe.get_traceback())
- name_list = save_invoice(doc, name, name_list)
-
- return name_list
-
-def save_invoice(doc, name, name_list):
- try:
- if not frappe.db.exists('Sales Invoice', {'offline_pos_name': name}):
- si = frappe.new_doc('Sales Invoice')
- si.update(doc)
- si.set_posting_time = 1
- si.customer = get_customer_id(doc)
- si.due_date = doc.get('posting_date')
- si.flags.ignore_mandatory = True
- si.insert(ignore_permissions=True)
- frappe.db.commit()
- name_list.append(name)
- except Exception:
- frappe.db.rollback()
- frappe.log_error(frappe.get_traceback())
-
- return name_list
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 061ce1c..9af584e 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -282,7 +282,7 @@
"customer": this.frm.doc.customer
},
callback: function(r) {
- if(r.message && r.message.length) {
+ if(r.message && r.message.length > 1) {
select_loyalty_program(me.frm, r.message);
}
}
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 02b4206..4c1d407 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -13,6 +13,7 @@
"customer_name",
"tax_id",
"is_pos",
+ "is_consolidated",
"pos_profile",
"offline_pos_name",
"is_return",
@@ -1923,6 +1924,13 @@
},
{
"default": "0",
+ "fieldname": "is_consolidated",
+ "fieldtype": "Check",
+ "label": "Is Consolidated",
+ "read_only": 1
+ },
+ {
+ "default": "0",
"fetch_from": "customer.is_internal_customer",
"fieldname": "is_internal_customer",
"fieldtype": "Check",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 8984348..3dab054 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -8,8 +8,6 @@
from frappe import _, msgprint, throw
from erpnext.accounts.party import get_party_account, get_due_date
from frappe.model.mapper import get_mapped_doc
-from erpnext.accounts.doctype.sales_invoice.pos import update_multi_mode_option
-
from erpnext.controllers.selling_controller import SellingController
from erpnext.accounts.utils import get_account_currency
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
@@ -133,7 +131,7 @@
if self.is_pos and self.is_return:
self.verify_payment_amount_is_negative()
- if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points:
+ if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points and not self.is_consolidated:
validate_loyalty_points(self, self.loyalty_points)
def validate_fixed_asset(self):
@@ -200,13 +198,13 @@
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program
- if not self.is_return and self.loyalty_program:
+ if not self.is_return and not self.is_consolidated and self.loyalty_program:
self.make_loyalty_point_entry()
- elif self.is_return and self.return_against and self.loyalty_program:
+ elif self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program:
against_si_doc = frappe.get_doc("Sales Invoice", self.return_against)
against_si_doc.delete_loyalty_point_entry()
against_si_doc.make_loyalty_point_entry()
- if self.redeem_loyalty_points and self.loyalty_points:
+ if self.redeem_loyalty_points and not self.is_consolidated and self.loyalty_points:
self.apply_loyalty_points()
# Healthcare Service Invoice.
@@ -265,9 +263,9 @@
if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction":
update_company_current_month_sales(self.company)
self.update_project()
- if not self.is_return and self.loyalty_program:
+ if not self.is_return and not self.is_consolidated and self.loyalty_program:
self.delete_loyalty_point_entry()
- elif self.is_return and self.return_against and self.loyalty_program:
+ elif self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program:
against_si_doc = frappe.get_doc("Sales Invoice", self.return_against)
against_si_doc.delete_loyalty_point_entry()
against_si_doc.make_loyalty_point_entry()
@@ -347,7 +345,7 @@
super(SalesInvoice, self).set_missing_values(for_validate)
- print_format = pos.get("print_format_for_online") if pos else None
+ print_format = pos.get("print_format") if pos else None
if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')):
print_format = 'POS Invoice'
@@ -420,8 +418,6 @@
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
if pos:
- self.allow_print_before_pay = pos.allow_print_before_pay
-
if not for_validate:
self.tax_category = pos.get("tax_category")
@@ -432,8 +428,8 @@
if pos.get('account_for_change_amount'):
self.account_for_change_amount = pos.get('account_for_change_amount')
- for fieldname in ('territory', 'naming_series', 'currency', 'letter_head', 'tc_name',
- 'company', 'select_print_heading', 'cash_bank_account', 'write_off_account', 'taxes_and_charges',
+ for fieldname in ('naming_series', 'currency', 'letter_head', 'tc_name',
+ 'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges',
'write_off_cost_center', 'apply_discount_on', 'cost_center'):
if (not for_validate) or (for_validate and not self.get(fieldname)):
self.set(fieldname, pos.get(fieldname))
@@ -1123,7 +1119,8 @@
"loyalty_program": lp_details.loyalty_program,
"loyalty_program_tier": lp_details.tier_name,
"customer": self.customer,
- "sales_invoice": self.name,
+ "invoice_type": self.doctype,
+ "invoice": self.name,
"loyalty_points": points_earned,
"purchase_amount": eligible_amount,
"expiry_date": add_days(self.posting_date, lp_details.expiry_duration),
@@ -1135,18 +1132,18 @@
# valdite the redemption and then delete the loyalty points earned on cancel of the invoice
def delete_loyalty_point_entry(self):
- lp_entry = frappe.db.sql("select name from `tabLoyalty Point Entry` where sales_invoice=%s",
+ lp_entry = frappe.db.sql("select name from `tabLoyalty Point Entry` where invoice=%s",
(self.name), as_dict=1)
if not lp_entry: return
- against_lp_entry = frappe.db.sql('''select name, sales_invoice from `tabLoyalty Point Entry`
+ against_lp_entry = frappe.db.sql('''select name, invoice from `tabLoyalty Point Entry`
where redeem_against=%s''', (lp_entry[0].name), as_dict=1)
if against_lp_entry:
- invoice_list = ", ".join([d.sales_invoice for d in against_lp_entry])
- frappe.throw(_('''Sales Invoice can't be cancelled since the Loyalty Points earned has been redeemed.
- First cancel the Sales Invoice No {0}''').format(invoice_list))
+ invoice_list = ", ".join([d.invoice for d in against_lp_entry])
+ frappe.throw(_('''{} can't be cancelled since the Loyalty Points earned has been redeemed.
+ First cancel the {} No {}''').format(self.doctype, self.doctype, invoice_list))
else:
- frappe.db.sql('''delete from `tabLoyalty Point Entry` where sales_invoice=%s''', (self.name))
+ frappe.db.sql('''delete from `tabLoyalty Point Entry` where invoice=%s''', (self.name))
# Set loyalty program
self.set_loyalty_program_tier()
@@ -1172,7 +1169,9 @@
points_to_redeem = self.loyalty_points
for lp_entry in loyalty_point_entries:
- if lp_entry.sales_invoice == self.name:
+ if lp_entry.invoice_type != self.doctype or lp_entry.invoice == self.name:
+ # redeemption should be done against same doctype
+ # also it shouldn't be against itself
continue
available_points = lp_entry.loyalty_points - flt(redemption_details.get(lp_entry.name))
if available_points > points_to_redeem:
@@ -1185,7 +1184,8 @@
"loyalty_program": self.loyalty_program,
"loyalty_program_tier": lp_entry.loyalty_program_tier,
"customer": self.customer,
- "sales_invoice": self.name,
+ "invoice_type": self.doctype,
+ "invoice": self.name,
"redeem_against": lp_entry.name,
"loyalty_points": -1*redeemed_points,
"purchase_amount": self.grand_total,
@@ -1576,13 +1576,13 @@
from erpnext.selling.doctype.customer.customer import get_loyalty_programs
customer = frappe.get_doc('Customer', customer)
- if customer.loyalty_program: return
+ if customer.loyalty_program: return [customer.loyalty_program]
lp_details = get_loyalty_programs(customer)
if len(lp_details) == 1:
frappe.db.set(customer, 'loyalty_program', lp_details[0])
- return []
+ return lp_details
else:
return lp_details
@@ -1603,7 +1603,41 @@
return invoice_discounting
-@frappe.whitelist()
+def update_multi_mode_option(doc, pos_profile):
+ def append_payment(payment_mode):
+ payment = doc.append('payments', {})
+ payment.default = payment_mode.default
+ payment.mode_of_payment = payment_mode.parent
+ payment.account = payment_mode.default_account
+ payment.type = payment_mode.type
+
+ doc.set('payments', [])
+ if not pos_profile or not pos_profile.get('payments'):
+ for payment_mode in get_all_mode_of_payments(doc):
+ append_payment(payment_mode)
+ return
+
+ for pos_payment_method in pos_profile.get('payments'):
+ pos_payment_method = pos_payment_method.as_dict()
+
+ payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company)
+ payment_mode[0].default = pos_payment_method.default
+ append_payment(payment_mode[0])
+
+def get_all_mode_of_payments(doc):
+ return frappe.db.sql("""
+ select mpa.default_account, mpa.parent, mp.type as type
+ from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
+ where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""",
+ {'company': doc.company}, as_dict=1)
+
+def get_mode_of_payment_info(mode_of_payment, company):
+ return frappe.db.sql("""
+ select mpa.default_account, mpa.parent, mp.type as type
+ from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
+ where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""",
+ (company, mode_of_payment), as_dict=1)
+
def create_dunning(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text, calculate_interest_and_amount
@@ -1635,4 +1669,4 @@
"doctype": "Dunning",
}
}, target_doc, set_missing_values)
- return doclist
\ No newline at end of file
+ return doclist
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index ff4d613..964566a 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -706,37 +706,15 @@
self.pos_gl_entry(si, pos, 50)
- def test_pos_returns_without_repayment(self):
- pos_profile = make_pos_profile()
-
- pos = create_sales_invoice(qty = 10, do_not_save=True)
- pos.is_pos = 1
- pos.pos_profile = pos_profile.name
-
- pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500})
- pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500})
- pos.insert()
- pos.submit()
-
- pos_return = create_sales_invoice(is_return=1,
- return_against=pos.name, qty=-5, do_not_save=True)
-
- pos_return.is_pos = 1
- pos_return.pos_profile = pos_profile.name
-
- pos_return.insert()
- pos_return.submit()
-
- self.assertFalse(pos_return.is_pos)
- self.assertFalse(pos_return.get('payments'))
-
def test_pos_returns_with_repayment(self):
+ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
+
pos_profile = make_pos_profile()
+ pos_profile.payments = []
pos_profile.append('payments', {
'default': 1,
- 'mode_of_payment': 'Cash',
- 'amount': 0.0
+ 'mode_of_payment': 'Cash'
})
pos_profile.save()
@@ -751,18 +729,12 @@
pos.insert()
pos.submit()
- pos_return = create_sales_invoice(is_return=1,
- return_against=pos.name, qty=-5, do_not_save=True)
+ pos_return = make_sales_return(pos.name)
- pos_return.is_pos = 1
- pos_return.pos_profile = pos_profile.name
pos_return.insert()
pos_return.submit()
- self.assertEqual(pos_return.get('payments')[0].amount, -500)
- pos_profile.payments = []
- pos_profile.save()
-
+ self.assertEqual(pos_return.get('payments')[0].amount, -1000)
def test_pos_change_amount(self):
make_pos_profile()
@@ -788,82 +760,6 @@
self.assertEqual(pos.grand_total, 100.0)
self.assertEqual(pos.write_off_amount, -5)
- def test_make_pos_invoice(self):
- from erpnext.accounts.doctype.sales_invoice.pos import make_invoice
-
- pos_profile = make_pos_profile()
-
- pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",
- item_code= "_Test FG Item",
- warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
-
- pos = create_sales_invoice(company= "_Test Company with perpetual inventory",
- debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1",
- income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1",
- cost_center = "Main - TCP1", do_not_save=True)
-
- pos.is_pos = 1
- pos.update_stock = 1
-
- pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
- pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 50})
-
- taxes = get_taxes_and_charges()
- pos.taxes = []
- for tax in taxes:
- pos.append("taxes", tax)
-
- invoice_data = [{'09052016142': pos}]
- si = make_invoice(pos_profile, invoice_data).get('invoice')
- self.assertEqual(si[0], '09052016142')
-
- sales_invoice = frappe.get_all('Sales Invoice', fields =["*"], filters = {'offline_pos_name': '09052016142', 'docstatus': 1})
- si = frappe.get_doc('Sales Invoice', sales_invoice[0].name)
-
- self.assertEqual(si.grand_total, 100)
-
- self.pos_gl_entry(si, pos, 50)
-
- def test_make_pos_invoice_in_draft(self):
- from erpnext.accounts.doctype.sales_invoice.pos import make_invoice
- from erpnext.stock.doctype.item.test_item import make_item
-
- allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
- if allow_negative_stock:
- frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0)
-
- pos_profile = make_pos_profile()
- timestamp = cint(time.time())
-
- item = make_item("_Test POS Item")
- pos = copy.deepcopy(test_records[1])
- pos['items'][0]['item_code'] = item.name
- pos['items'][0]['warehouse'] = "_Test Warehouse - _TC"
- pos["is_pos"] = 1
- pos["offline_pos_name"] = timestamp
- pos["update_stock"] = 1
- pos["payments"] = [{'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 300},
- {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 330}]
-
- invoice_data = [{timestamp: pos}]
- si = make_invoice(pos_profile, invoice_data).get('invoice')
- self.assertEqual(si[0], timestamp)
-
- sales_invoice = frappe.get_all('Sales Invoice', fields =["*"], filters = {'offline_pos_name': timestamp})
- self.assertEqual(sales_invoice[0].docstatus, 0)
-
- timestamp = cint(time.time())
- pos["offline_pos_name"] = timestamp
- invoice_data = [{timestamp: pos}]
- si1 = make_invoice(pos_profile, invoice_data).get('invoice')
- self.assertEqual(si1[0], timestamp)
-
- sales_invoice1 = frappe.get_all('Sales Invoice', fields =["*"], filters = {'offline_pos_name': timestamp})
- self.assertEqual(sales_invoice1[0].docstatus, 0)
-
- if allow_negative_stock:
- frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1)
-
def pos_gl_entry(self, si, pos, cash_amount):
# check stock ledger entries
sle = frappe.db.sql("""select * from `tabStock Ledger Entry`
diff --git a/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json
index 52cf810..2f9d381 100644
--- a/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json
+++ b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json
@@ -1,314 +1,90 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2016-05-08 23:49:38.842621",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
+ "actions": [],
+ "creation": "2016-05-08 23:49:38.842621",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "default",
+ "mode_of_payment",
+ "amount",
+ "column_break_3",
+ "account",
+ "type",
+ "base_amount",
+ "clearance_date"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:parent.doctype == 'POS Profile'",
- "fetch_if_empty": 0,
- "fieldname": "default",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Default",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Mode of Payment",
+ "options": "Mode of Payment",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "mode_of_payment",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Mode of Payment",
- "length": 0,
- "no_copy": 0,
- "options": "Mode of Payment",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "depends_on": "eval:parent.doctype == 'Sales Invoice'",
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Amount",
+ "options": "currency",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "0",
- "depends_on": "eval:parent.doctype == 'Sales Invoice'",
- "fetch_if_empty": 0,
- "fieldname": "amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Amount",
- "length": 0,
- "no_copy": 0,
- "options": "currency",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "account",
+ "fieldtype": "Link",
+ "label": "Account",
+ "options": "Account",
+ "print_hide": 1,
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "account",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fetch_from": "mode_of_payment.type",
+ "fieldname": "type",
+ "fieldtype": "Read Only",
+ "label": "Type"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "mode_of_payment.type",
- "fetch_if_empty": 0,
- "fieldname": "type",
- "fieldtype": "Read Only",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Type",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "base_amount",
+ "fieldtype": "Currency",
+ "label": "Base Amount (Company Currency)",
+ "no_copy": 1,
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "base_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Base Amount (Company Currency)",
- "length": 0,
- "no_copy": 1,
- "options": "Company:company:default_currency",
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "clearance_date",
+ "fieldtype": "Date",
+ "label": "Clearance Date",
+ "print_hide": 1,
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "clearance_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Clearance Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "default": "0",
+ "fieldname": "default",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Default",
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2019-03-19 14:54:56.524556",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Sales Invoice Payment",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-05-05 16:51:20.091441",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Sales Invoice Payment",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js
deleted file mode 100755
index 24fcb41..0000000
--- a/erpnext/accounts/page/pos/pos.js
+++ /dev/null
@@ -1,2105 +0,0 @@
-frappe.provide("erpnext.pos");
-{% include "erpnext/public/js/controllers/taxes_and_totals.js" %}
-
-frappe.pages['pos'].on_page_load = function (wrapper) {
- var page = frappe.ui.make_app_page({
- parent: wrapper,
- title: __('Point of Sale'),
- single_column: true
- });
-
- frappe.db.get_value('POS Settings', {name: 'POS Settings'}, 'is_online', (r) => {
- if (r && r.use_pos_in_offline_mode && cint(r.use_pos_in_offline_mode)) {
- // offline
- wrapper.pos = new erpnext.pos.PointOfSale(wrapper);
- cur_pos = wrapper.pos;
- } else {
- // online
- frappe.flags.is_online = true
- frappe.set_route('point-of-sale');
- }
- });
-}
-
-frappe.pages['pos'].refresh = function (wrapper) {
- window.onbeforeunload = function () {
- return wrapper.pos.beforeunload()
- }
-
- if (frappe.flags.is_online) {
- frappe.set_route('point-of-sale');
- }
-}
-
-erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
- init: function (wrapper) {
- this.page_len = 20;
- this.freeze = false;
- this.page = wrapper.page;
- this.wrapper = $(wrapper).find('.page-content');
- this.set_indicator();
- this.onload();
- this.make_menu_list();
- this.bind_events();
- this.bind_items_event();
- this.si_docs = this.get_doc_from_localstorage();
- },
-
- beforeunload: function (e) {
- if (this.connection_status == false && frappe.get_route()[0] == "pos") {
- e = e || window.event;
-
- // For IE and Firefox prior to version 4
- if (e) {
- e.returnValue = __("You are in offline mode. You will not be able to reload until you have network.");
- return
- }
-
- // For Safari
- return __("You are in offline mode. You will not be able to reload until you have network.");
- }
- },
-
- check_internet_connection: function () {
- var me = this;
- //Check Internet connection after every 30 seconds
- setInterval(function () {
- me.set_indicator();
- }, 5000)
- },
-
- set_indicator: function () {
- var me = this;
- // navigator.onLine
- this.connection_status = false;
- this.page.set_indicator(__("Offline"), "grey")
- frappe.call({
- method: "frappe.handler.ping",
- callback: function (r) {
- if (r.message) {
- me.connection_status = true;
- me.page.set_indicator(__("Online"), "green")
- }
- }
- })
- },
-
- onload: function () {
- var me = this;
- this.get_data_from_server(function () {
- me.make_control();
- me.create_new();
- me.make();
- });
- },
-
- make_menu_list: function () {
- var me = this;
- this.page.clear_menu();
-
- // for mobile
- this.page.add_menu_item(__("Pay"), function () {
- me.validate();
- me.update_paid_amount_status(true);
- me.create_invoice();
- me.make_payment();
- }).addClass('visible-xs');
-
- this.page.add_menu_item(__("New Sales Invoice"), function () {
- me.save_previous_entry();
- me.create_new();
- })
-
- this.page.add_menu_item(__("Sync Master Data"), function () {
- me.get_data_from_server(function () {
- me.load_data(false);
- me.make_item_list();
- me.set_missing_values();
- })
- });
-
- this.page.add_menu_item(__("Sync Offline Invoices"), function () {
- me.freeze_screen = true;
- me.sync_sales_invoice()
- });
-
- this.page.add_menu_item(__("Cashier Closing"), function () {
- frappe.set_route('List', 'Cashier Closing');
- });
-
- this.page.add_menu_item(__("POS Profile"), function () {
- frappe.set_route('List', 'POS Profile');
- });
- },
-
- email_prompt: function() {
- var me = this;
- var fields = [{label:__("To"), fieldtype:"Data", reqd: 0, fieldname:"recipients",length:524288},
- {fieldtype: "Section Break", collapsible: 1, label: "CC & Email Template"},
- {fieldtype: "Section Break"},
- {label:__("Subject"), fieldtype:"Data", reqd: 1,
- fieldname:"subject",length:524288},
- {fieldtype: "Section Break"},
- {label:__("Message"), fieldtype:"Text Editor", reqd: 1,
- fieldname:"content"},
- {fieldtype: "Section Break"},
- {fieldtype: "Column Break"}];
-
- this.email_dialog = new frappe.ui.Dialog({
- title: "Email",
- fields: fields,
- primary_action_label: __("Send"),
- primary_action: function() {
- me.send_action();
- }
- });
-
- this.email_dialog.show()
- },
-
- send_action: function() {
- this.email_queue = this.get_email_queue()
- this.email_queue[this.frm.doc.offline_pos_name] = JSON.stringify(this.email_dialog.get_values())
- this.update_email_queue()
- this.email_dialog.hide()
- },
-
- update_email_queue: function () {
- try {
- localStorage.setItem('email_queue', JSON.stringify(this.email_queue));
- } catch (e) {
- frappe.throw(__("LocalStorage is full, did not save"))
- }
- },
-
- get_email_queue: function () {
- try {
- return JSON.parse(localStorage.getItem('email_queue')) || {};
- } catch (e) {
- return {}
- }
- },
-
- get_customers_details: function () {
- try {
- return JSON.parse(localStorage.getItem('customer_details')) || {};
- } catch (e) {
- return {}
- }
- },
-
- edit_record: function () {
- var me = this;
-
- doc_data = this.get_invoice_doc(this.si_docs);
- if (doc_data) {
- this.frm.doc = doc_data[0][this.frm.doc.offline_pos_name];
- this.set_missing_values();
- this.refresh(false);
- this.toggle_input_field();
- this.list_dialog && this.list_dialog.hide();
- }
- },
-
- delete_records: function () {
- var me = this;
- this.validate_list()
- this.remove_doc_from_localstorage()
- this.update_localstorage();
- this.toggle_delete_button();
- },
-
- validate_list: function() {
- var me = this;
- this.si_docs = this.get_submitted_invoice()
- $.each(this.removed_items, function(index, pos_name){
- $.each(me.si_docs, function(key, data){
- if(me.si_docs[key][pos_name] && me.si_docs[key][pos_name].offline_pos_name == pos_name ){
- frappe.throw(__("Submitted orders can not be deleted"))
- }
- })
- })
- },
-
- toggle_delete_button: function () {
- var me = this;
- if(this.pos_profile_data["allow_delete"]) {
- if (this.removed_items && this.removed_items.length > 0) {
- $(this.page.wrapper).find('.btn-danger').show();
- } else {
- $(this.page.wrapper).find('.btn-danger').hide();
- }
- }
- },
-
- get_doctype_status: function (doc) {
- if (doc.docstatus == 0) {
- return { status: "Draft", indicator: "red" }
- } else if (doc.outstanding_amount == 0) {
- return { status: "Paid", indicator: "green" }
- } else {
- return { status: "Submitted", indicator: "blue" }
- }
- },
-
- set_missing_values: function () {
- var me = this;
- doc = JSON.parse(localStorage.getItem('doc'))
- if (this.frm.doc.payments.length == 0) {
- this.frm.doc.payments = doc.payments;
- this.calculate_outstanding_amount();
- }
-
- this.set_customer_value_in_party_field();
-
- if (!this.frm.doc.write_off_account) {
- this.frm.doc.write_off_account = doc.write_off_account
- }
-
- if (!this.frm.doc.account_for_change_amount) {
- this.frm.doc.account_for_change_amount = doc.account_for_change_amount
- }
- },
-
- set_customer_value_in_party_field: function() {
- if (this.frm.doc.customer) {
- this.party_field.$input.val(this.frm.doc.customer);
- }
- },
-
- get_invoice_doc: function (si_docs) {
- var me = this;
- this.si_docs = this.get_doc_from_localstorage();
-
- return $.grep(this.si_docs, function (data) {
- for (key in data) {
- return key == me.frm.doc.offline_pos_name;
- }
- })
- },
-
- get_data_from_server: function (callback) {
- var me = this;
- frappe.call({
- method: "erpnext.accounts.doctype.sales_invoice.pos.get_pos_data",
- freeze: true,
- freeze_message: __("Master data syncing, it might take some time"),
- callback: function (r) {
- localStorage.setItem('doc', JSON.stringify(r.message.doc));
- me.init_master_data(r)
- me.set_interval_for_si_sync();
- me.check_internet_connection();
- if (callback) {
- callback();
- }
- },
- error: () => {
- setTimeout(() => frappe.set_route('List', 'POS Profile'), 2000);
- }
- })
- },
-
- init_master_data: function (r) {
- var me = this;
- this.doc = JSON.parse(localStorage.getItem('doc'));
- this.meta = r.message.meta;
- this.item_data = r.message.items;
- this.item_groups = r.message.item_groups;
- this.customers = r.message.customers;
- this.serial_no_data = r.message.serial_no_data;
- this.batch_no_data = r.message.batch_no_data;
- this.barcode_data = r.message.barcode_data;
- this.tax_data = r.message.tax_data;
- this.contacts = r.message.contacts;
- this.address = r.message.address || {};
- this.price_list_data = r.message.price_list_data;
- this.customer_wise_price_list = r.message.customer_wise_price_list
- this.bin_data = r.message.bin_data;
- this.pricing_rules = r.message.pricing_rules;
- this.print_template = r.message.print_template;
- this.pos_profile_data = r.message.pos_profile;
- this.default_customer = r.message.default_customer || null;
- this.print_settings = locals[":Print Settings"]["Print Settings"];
- this.letter_head = (this.pos_profile_data.length > 0) ? frappe.boot.letter_heads[this.pos_profile_data[letter_head]] : {};
- },
-
- save_previous_entry: function () {
- if (this.frm.doc.docstatus < 1 && this.frm.doc.items.length > 0) {
- this.create_invoice();
- }
- },
-
- create_new: function () {
- var me = this;
- this.frm = {}
- this.load_data(true);
- this.frm.doc.offline_pos_name = '';
- this.setup();
- this.set_default_customer()
- },
-
- load_data: function (load_doc) {
- var me = this;
-
- this.items = this.item_data;
- this.actual_qty_dict = {};
-
- if (load_doc) {
- this.frm.doc = JSON.parse(localStorage.getItem('doc'));
- }
-
- $.each(this.meta, function (i, data) {
- frappe.meta.sync(data)
- locals["DocType"][data.name] = data;
- })
-
- this.print_template_data = frappe.render_template("print_template", {
- content: this.print_template,
- title: "POS",
- base_url: frappe.urllib.get_base_url(),
- print_css: frappe.boot.print_css,
- print_settings: this.print_settings,
- header: this.letter_head.header,
- footer: this.letter_head.footer,
- landscape: false,
- columns: []
- })
- },
-
- setup: function () {
- this.set_primary_action();
- this.party_field.$input.attr('disabled', false);
- if(this.selected_row) {
- this.selected_row.hide()
- }
- },
-
- set_default_customer: function() {
- if (this.default_customer && !this.frm.doc.customer) {
- this.party_field.$input.val(this.default_customer);
- this.frm.doc.customer = this.default_customer;
- this.numeric_keypad.show();
- this.toggle_list_customer(false)
- this.toggle_item_cart(true)
- }
- },
-
- set_transaction_defaults: function (party) {
- var me = this;
- this.party = party;
- this.price_list = (party == "Customer" ?
- this.frm.doc.selling_price_list : this.frm.doc.buying_price_list);
- this.price_list_field = (party == "Customer" ? "selling_price_list" : "buying_price_list");
- this.sales_or_purchase = (party == "Customer" ? "Sales" : "Purchase");
- },
-
- make: function () {
- this.make_item_list();
- this.make_discount_field()
- },
-
- make_control: function() {
- this.frm = {}
- this.frm.doc = this.doc
- this.set_transaction_defaults("Customer");
- this.frm.doc["allow_user_to_edit_rate"] = this.pos_profile_data["allow_user_to_edit_rate"] ? true : false;
- this.frm.doc["allow_user_to_edit_discount"] = this.pos_profile_data["allow_user_to_edit_discount"] ? true : false;
- this.wrapper.html(frappe.render_template("pos", this.frm.doc));
- this.make_search();
- this.make_customer();
- this.make_list_customers();
- this.bind_numeric_keypad();
- },
-
- make_search: function () {
- var me = this;
- this.search_item = frappe.ui.form.make_control({
- df: {
- "fieldtype": "Data",
- "label": __("Item"),
- "fieldname": "pos_item",
- "placeholder": __("Search Item")
- },
- parent: this.wrapper.find(".search-item"),
- only_input: true,
- });
-
- this.search_item.make_input();
-
- this.search_item.$input.on("keypress", function (event) {
-
- clearTimeout(me.last_search_timeout);
- me.last_search_timeout = setTimeout(() => {
- if((me.search_item.$input.val() != "") || (event.which == 13)) {
- me.items = me.get_items();
- me.make_item_list();
- }
- }, 400);
- });
-
- this.search_item_group = this.wrapper.find('.search-item-group');
- sorted_item_groups = this.get_sorted_item_groups()
- var dropdown_html = sorted_item_groups.map(function(item_group) {
- return "<li><a class='option' data-value='"+item_group+"'>"+item_group+"</a></li>";
- }).join("");
-
- this.search_item_group.find('.dropdown-menu').html(dropdown_html);
-
- this.search_item_group.on('click', '.dropdown-menu a', function() {
- me.selected_item_group = $(this).attr('data-value');
- me.search_item_group.find('.dropdown-text').text(me.selected_item_group);
-
- me.page_len = 20;
- me.items = me.get_items();
- me.make_item_list();
- })
-
- me.toggle_more_btn();
-
- this.wrapper.on("click", ".btn-more", function() {
- me.page_len += 20;
- me.items = me.get_items();
- me.make_item_list();
- me.toggle_more_btn();
- });
-
- this.page.wrapper.on("click", ".edit-customer-btn", function() {
- me.update_customer()
- })
- },
-
- get_sorted_item_groups: function() {
- list = {}
- $.each(this.item_groups, function(i, data) {
- list[i] = data[0]
- })
-
- return Object.keys(list).sort(function(a,b){return list[a]-list[b]})
- },
-
- toggle_more_btn: function() {
- if(!this.items || this.items.length <= this.page_len) {
- this.wrapper.find(".btn-more").hide();
- } else {
- this.wrapper.find(".btn-more").show();
- }
- },
-
- toggle_totals_area: function(show) {
-
- if(show === undefined) {
- show = this.is_totals_area_collapsed;
- }
-
- var totals_area = this.wrapper.find('.totals-area');
- totals_area.find('.net-total-area, .tax-area, .discount-amount-area')
- .toggle(show);
-
- if(show) {
- totals_area.find('.collapse-btn i')
- .removeClass('octicon-chevron-down')
- .addClass('octicon-chevron-up');
- } else {
- totals_area.find('.collapse-btn i')
- .removeClass('octicon-chevron-up')
- .addClass('octicon-chevron-down');
- }
-
- this.is_totals_area_collapsed = !show;
- },
-
- make_list_customers: function () {
- var me = this;
- this.list_customers_btn = this.page.wrapper.find('.list-customers-btn');
- this.add_customer_btn = this.wrapper.find('.add-customer-btn');
- this.pos_bill = this.wrapper.find('.pos-bill-wrapper').hide();
- this.list_customers = this.wrapper.find('.list-customers');
- this.numeric_keypad = this.wrapper.find('.numeric_keypad');
- this.list_customers_btn.addClass("view_customer")
-
- me.render_list_customers();
- me.toggle_totals_area(false);
-
- this.page.wrapper.on('click', '.list-customers-btn', function() {
- $(this).toggleClass("view_customer");
- if($(this).hasClass("view_customer")) {
- me.render_list_customers();
- me.list_customers.show();
- me.pos_bill.hide();
- me.numeric_keypad.hide();
- me.toggle_delete_button()
- } else {
- if(me.frm.doc.docstatus == 0) {
- me.party_field.$input.attr('disabled', false);
- }
- me.pos_bill.show();
- me.toggle_totals_area(false);
- me.toggle_delete_button()
- me.list_customers.hide();
- me.numeric_keypad.show();
- }
- });
- this.add_customer_btn.on('click', function() {
- me.save_previous_entry();
- me.create_new();
- me.refresh();
- me.set_focus();
- });
- this.pos_bill.on('click', '.collapse-btn', function() {
- me.toggle_totals_area();
- });
- },
-
- bind_numeric_keypad: function() {
- var me = this;
- $(this.numeric_keypad).find('.pos-operation').on('click', function(){
- me.numeric_val = '';
- })
-
- $(this.numeric_keypad).find('.numeric-keypad').on('click', function(){
- me.numeric_id = $(this).attr("id") || me.numeric_id;
- me.val = $(this).attr("val")
- if(me.numeric_id) {
- me.selected_field = $(me.wrapper).find('.selected-item').find('.' + me.numeric_id)
- }
-
- if(me.val && me.numeric_id) {
- me.numeric_val += me.val;
- me.selected_field.val(flt(me.numeric_val))
- me.selected_field.trigger("change")
- // me.render_selected_item()
- }
-
- if(me.numeric_id && $(this).hasClass('pos-operation')) {
- me.numeric_keypad.find('button.pos-operation').removeClass('active');
- $(this).addClass('active');
-
- me.selected_row.find('.pos-list-row').removeClass('active');
- me.selected_field.closest('.pos-list-row').addClass('active');
- }
- })
-
- $(this.numeric_keypad).find('.numeric-del').click(function(){
- if(me.numeric_id) {
- me.selected_field = $(me.wrapper).find('.selected-item').find('.' + me.numeric_id)
- me.numeric_val = cstr(flt(me.selected_field.val())).slice(0, -1);
- me.selected_field.val(me.numeric_val);
- me.selected_field.trigger("change")
- } else {
- //Remove an item from the cart, if focus is at selected item
- me.remove_selected_item()
- }
- })
-
- $(this.numeric_keypad).find('.pos-pay').click(function(){
- me.validate();
- me.update_paid_amount_status(true);
- me.create_invoice();
- me.make_payment();
- })
- },
-
- remove_selected_item: function() {
- this.remove_item = []
- idx = $(this.wrapper).find(".pos-selected-item-action").attr("data-idx")
- this.remove_item.push(idx)
- this.remove_zero_qty_items_from_cart()
- this.update_paid_amount_status(false)
- },
-
- render_list_customers: function () {
- var me = this;
-
- this.removed_items = [];
- // this.list_customers.empty();
- this.si_docs = this.get_doc_from_localstorage();
- if (!this.si_docs.length) {
- this.list_customers.find('.list-customers-table').html("");
- return;
- }
-
- var html = "";
- if(this.si_docs.length) {
- this.si_docs.forEach(function (data, i) {
- for (var key in data) {
- html += frappe.render_template("pos_invoice_list", {
- sr: i + 1,
- name: key,
- customer: data[key].customer,
- paid_amount: format_currency(data[key].paid_amount, me.frm.doc.currency),
- grand_total: format_currency(data[key].grand_total, me.frm.doc.currency),
- data: me.get_doctype_status(data[key])
- });
- }
- });
- }
- this.list_customers.find('.list-customers-table').html(html);
-
- this.list_customers.on('click', '.customer-row', function () {
- me.list_customers.hide();
- me.numeric_keypad.show();
- me.list_customers_btn.toggleClass("view_customer");
- me.pos_bill.show();
- me.list_customers_btn.show();
- me.frm.doc.offline_pos_name = $(this).parents().attr('invoice-name');
- me.edit_record();
- })
-
- //actions
- $(this.wrapper).find('.list-select-all').click(function () {
- me.list_customers.find('.list-delete').prop("checked", $(this).is(":checked"))
- me.removed_items = [];
- if ($(this).is(":checked")) {
- $.each(me.si_docs, function (index, data) {
- for (key in data) {
- me.removed_items.push(key)
- }
- });
- }
-
- me.toggle_delete_button();
- });
-
- $(this.wrapper).find('.list-delete').click(function () {
- me.frm.doc.offline_pos_name = $(this).parent().parent().attr('invoice-name');
- if ($(this).is(":checked")) {
- me.removed_items.push(me.frm.doc.offline_pos_name);
- } else {
- me.removed_items.pop(me.frm.doc.offline_pos_name)
- }
-
- me.toggle_delete_button();
- });
- },
-
- bind_delete_event: function() {
- var me = this;
-
- $(this.page.wrapper).on('click', '.btn-danger', function(){
- frappe.confirm(__("Delete permanently?"), function () {
- me.delete_records();
- me.list_customers.find('.list-customers-table').html("");
- me.render_list_customers();
- })
- })
- },
-
- set_focus: function () {
- if (this.default_customer || this.frm.doc.customer) {
- this.set_customer_value_in_party_field();
- this.search_item.$input.focus();
- } else {
- this.party_field.$input.focus();
- }
- },
-
- make_customer: function () {
- var me = this;
-
- if(!this.party_field) {
- if(this.page.wrapper.find('.pos-bill-toolbar').length === 0) {
- $(frappe.render_template('customer_toolbar', {
- allow_delete: this.pos_profile_data["allow_delete"]
- })).insertAfter(this.page.$title_area.hide());
- }
-
- this.party_field = frappe.ui.form.make_control({
- df: {
- "fieldtype": "Data",
- "options": this.party,
- "label": this.party,
- "fieldname": this.party.toLowerCase(),
- "placeholder": __("Select or add new customer")
- },
- parent: this.page.wrapper.find(".party-area"),
- only_input: true,
- });
-
- this.party_field.make_input();
- setTimeout(this.set_focus.bind(this), 500);
- me.toggle_delete_button();
- }
-
- this.party_field.awesomeplete =
- new Awesomplete(this.party_field.$input.get(0), {
- minChars: 0,
- maxItems: 99,
- autoFirst: true,
- list: [],
- filter: function (item, input) {
- if (item.value.includes('is_action')) {
- return true;
- }
-
- input = input.toLowerCase();
- item = this.get_item(item.value);
- result = item ? item.searchtext.includes(input) : '';
- if(!result) {
- me.prepare_customer_mapper(input);
- } else {
- return result;
- }
- },
- item: function (item, input) {
- var d = this.get_item(item.value);
- var html = "<span>" + __(d.label || d.value) + "</span>";
- if(d.customer_name) {
- html += '<br><span class="text-muted ellipsis">' + __(d.customer_name) + '</span>';
- }
-
- return $('<li></li>')
- .data('item.autocomplete', d)
- .html('<a><p>' + html + '</p></a>')
- .get(0);
- }
- });
-
- this.prepare_customer_mapper()
- this.autocomplete_customers();
-
- this.party_field.$input
- .on('input', function (e) {
- if(me.customers_mapper.length <= 1) {
- me.prepare_customer_mapper(e.target.value);
- }
- me.party_field.awesomeplete.list = me.customers_mapper;
- })
- .on('awesomplete-select', function (e) {
- var customer = me.party_field.awesomeplete
- .get_item(e.originalEvent.text.value);
- if (!customer) return;
- // create customer link
- if (customer.action) {
- customer.action.apply(me);
- return;
- }
- me.toggle_list_customer(false);
- me.toggle_edit_button(true);
- me.update_customer_data(customer);
- me.refresh();
- me.set_focus();
- me.list_customers_btn.removeClass("view_customer");
- })
- .on('focus', function (e) {
- $(e.target).val('').trigger('input');
- me.toggle_edit_button(false);
-
- if(me.frm.doc.items.length) {
- me.toggle_list_customer(false)
- me.toggle_item_cart(true)
- } else {
- me.toggle_list_customer(true)
- me.toggle_item_cart(false)
- }
- })
- .on("awesomplete-selectcomplete", function (e) {
- var item = me.party_field.awesomeplete
- .get_item(e.originalEvent.text.value);
- // clear text input if item is action
- if (item.action) {
- $(this).val("");
- }
- me.make_item_list(item.customer_name);
- });
- },
-
- prepare_customer_mapper: function(key) {
- var me = this;
- var customer_data = '';
-
- if (key) {
- key = key.toLowerCase().trim();
- var re = new RegExp('%', 'g');
- var reg = new RegExp(key.replace(re, '\\w*\\s*[a-zA-Z0-9]*'));
-
- customer_data = $.grep(this.customers, function(data) {
- contact = me.contacts[data.name];
- if(reg.test(data.name.toLowerCase())
- || reg.test(data.customer_name.toLowerCase())
- || (contact && reg.test(contact["phone"]))
- || (contact && reg.test(contact["mobile_no"]))
- || (data.customer_group && reg.test(data.customer_group.toLowerCase()))){
- return data;
- }
- })
- } else {
- customer_data = this.customers;
- }
-
- this.customers_mapper = [];
-
- customer_data.forEach(function (c, index) {
- if(index < 30) {
- contact = me.contacts[c.name];
- if(contact && !c['phone']) {
- c["phone"] = contact["phone"];
- c["email_id"] = contact["email_id"];
- c["mobile_no"] = contact["mobile_no"];
- }
-
- me.customers_mapper.push({
- label: c.name,
- value: c.name,
- customer_name: c.customer_name,
- customer_group: c.customer_group,
- territory: c.territory,
- phone: contact ? contact["phone"] : '',
- mobile_no: contact ? contact["mobile_no"] : '',
- email_id: contact ? contact["email_id"] : '',
- searchtext: ['customer_name', 'customer_group', 'name', 'value',
- 'label', 'email_id', 'phone', 'mobile_no']
- .map(key => c[key]).join(' ')
- .toLowerCase()
- });
- } else {
- return;
- }
- });
-
- this.customers_mapper.push({
- label: "<span class='text-primary link-option'>"
- + "<i class='fa fa-plus' style='margin-right: 5px;'></i> "
- + __("Create a new Customer")
- + "</span>",
- value: 'is_action',
- action: me.add_customer
- });
- },
-
- autocomplete_customers: function() {
- this.party_field.awesomeplete.list = this.customers_mapper;
- },
-
- toggle_edit_button: function(flag) {
- this.page.wrapper.find('.edit-customer-btn').toggle(flag);
- },
-
- toggle_list_customer: function(flag) {
- this.list_customers.toggle(flag);
- },
-
- toggle_item_cart: function(flag) {
- this.wrapper.find('.pos-bill-wrapper').toggle(flag);
- },
-
- add_customer: function() {
- this.frm.doc.customer = "";
- this.update_customer(true);
- this.numeric_keypad.show();
- },
-
- update_customer: function (new_customer) {
- var me = this;
-
- this.customer_doc = new frappe.ui.Dialog({
- 'title': 'Customer',
- fields: [
- {
- "label": __("Full Name"),
- "fieldname": "full_name",
- "fieldtype": "Data",
- "reqd": 1
- },
- {
- "fieldtype": "Section Break"
- },
- {
- "label": __("Email Id"),
- "fieldname": "email_id",
- "fieldtype": "Data"
- },
- {
- "fieldtype": "Column Break"
- },
- {
- "label": __("Contact Number"),
- "fieldname": "phone",
- "fieldtype": "Data"
- },
- {
- "fieldtype": "Section Break"
- },
- {
- "label": __("Address Name"),
- "read_only": 1,
- "fieldname": "name",
- "fieldtype": "Data"
- },
- {
- "label": __("Address Line 1"),
- "fieldname": "address_line1",
- "fieldtype": "Data"
- },
- {
- "label": __("Address Line 2"),
- "fieldname": "address_line2",
- "fieldtype": "Data"
- },
- {
- "fieldtype": "Column Break"
- },
- {
- "label": __("City"),
- "fieldname": "city",
- "fieldtype": "Data"
- },
- {
- "label": __("State"),
- "fieldname": "state",
- "fieldtype": "Data"
- },
- {
- "label": __("ZIP Code"),
- "fieldname": "pincode",
- "fieldtype": "Data"
- },
- {
- "label": __("Customer POS Id"),
- "fieldname": "customer_pos_id",
- "fieldtype": "Data",
- "hidden": 1
- }
- ]
- })
- this.customer_doc.show()
- this.render_address_data()
-
- this.customer_doc.set_primary_action(__("Save"), function () {
- me.make_offline_customer(new_customer);
- me.pos_bill.show();
- me.list_customers.hide();
- });
- },
-
- render_address_data: function() {
- var me = this;
- this.address_data = this.address[this.frm.doc.customer] || {};
- if(!this.address_data.email_id || !this.address_data.phone) {
- this.address_data = this.contacts[this.frm.doc.customer];
- }
-
- this.customer_doc.set_values(this.address_data)
- if(!this.customer_doc.fields_dict.full_name.$input.val()) {
- this.customer_doc.set_value("full_name", this.frm.doc.customer)
- }
-
- if(!this.customer_doc.fields_dict.customer_pos_id.value) {
- this.customer_doc.set_value("customer_pos_id", frappe.datetime.now_datetime())
- }
- },
-
- get_address_from_localstorage: function() {
- this.address_details = this.get_customers_details()
- return this.address_details[this.frm.doc.customer]
- },
-
- make_offline_customer: function(new_customer) {
- this.frm.doc.customer = this.frm.doc.customer || this.customer_doc.get_values().full_name;
- this.frm.doc.customer_pos_id = this.customer_doc.fields_dict.customer_pos_id.value;
- this.customer_details = this.get_customers_details();
- this.customer_details[this.frm.doc.customer] = this.get_prompt_details();
- this.party_field.$input.val(this.frm.doc.customer);
- this.update_address_and_customer_list(new_customer)
- this.autocomplete_customers();
- this.update_customer_in_localstorage()
- this.update_customer_in_localstorage()
- this.customer_doc.hide()
- },
-
- update_address_and_customer_list: function(new_customer) {
- var me = this;
- if(new_customer) {
- this.customers_mapper.push({
- label: this.frm.doc.customer,
- value: this.frm.doc.customer,
- customer_group: "",
- territory: ""
- });
- }
-
- this.address[this.frm.doc.customer] = JSON.parse(this.get_prompt_details())
- },
-
- get_prompt_details: function() {
- this.prompt_details = this.customer_doc.get_values();
- this.prompt_details['country'] = this.pos_profile_data.country;
- this.prompt_details['territory'] = this.pos_profile_data["territory"];
- this.prompt_details['customer_group'] = this.pos_profile_data["customer_group"];
- this.prompt_details['customer_pos_id'] = this.customer_doc.fields_dict.customer_pos_id.value;
- return JSON.stringify(this.prompt_details)
- },
-
- update_customer_data: function (doc) {
- var me = this;
- this.frm.doc.customer = doc.label || doc.name;
- this.frm.doc.customer_name = doc.customer_name;
- this.frm.doc.customer_group = doc.customer_group;
- this.frm.doc.territory = doc.territory;
- this.pos_bill.show();
- this.numeric_keypad.show();
- },
-
- make_item_list: function (customer) {
- var me = this;
- if (!this.price_list) {
- frappe.msgprint(__("Price List not found or disabled"));
- return;
- }
-
- me.item_timeout = null;
-
- var $wrap = me.wrapper.find(".item-list");
- me.wrapper.find(".item-list").empty();
-
- if (this.items.length > 0) {
- $.each(this.items, function(index, obj) {
- let customer_price_list = me.customer_wise_price_list[customer];
- let item_price
- if (customer && customer_price_list && customer_price_list[obj.name]) {
- item_price = format_currency(customer_price_list[obj.name], me.frm.doc.currency);
- } else {
- item_price = format_currency(me.price_list_data[obj.name], me.frm.doc.currency);
- }
- if(index < me.page_len) {
- $(frappe.render_template("pos_item", {
- item_code: obj.name,
- item_price: item_price,
- item_name: obj.name === obj.item_name ? "" : obj.item_name,
- item_image: obj.image,
- item_stock: __('Stock Qty') + ": " + me.get_actual_qty(obj),
- item_uom: obj.stock_uom,
- color: frappe.get_palette(obj.item_name),
- abbr: frappe.get_abbr(obj.item_name)
- })).tooltip().appendTo($wrap);
- }
- });
-
- $wrap.append(`
- <div class="image-view-item btn-more text-muted text-center">
- <div class="image-view-body">
- <i class="mega-octicon octicon-package"></i>
- <div>Load more items</div>
- </div>
- </div>
- `);
-
- me.toggle_more_btn();
- } else {
- $("<p class='text-muted small' style='padding-left: 10px'>"
- +__("Not items found")+"</p>").appendTo($wrap)
- }
-
- if (this.items.length == 1
- && this.search_item.$input.val()) {
- this.search_item.$input.val("");
- this.add_to_cart();
- }
- },
-
- get_items: function (item_code) {
- // To search item as per the key enter
-
- var me = this;
- this.item_serial_no = {};
- this.item_batch_no = {};
-
- if (item_code) {
- return $.grep(this.item_data, function (item) {
- if (item.item_code == item_code) {
- return true
- }
- })
- }
-
- this.items_list = this.apply_category();
-
- key = this.search_item.$input.val().toLowerCase().replace(/[&\/\\#,+()\[\]$~.'":*?<>{}]/g, '\\$&');
- var re = new RegExp('%', 'g');
- var reg = new RegExp(key.replace(re, '[\\w*\\s*[a-zA-Z0-9]*]*'))
- search_status = true
-
- if (key) {
- return $.grep(this.items_list, function (item) {
- if (search_status) {
- if (me.batch_no_data[item.item_code] &&
- in_list(me.batch_no_data[item.item_code], me.search_item.$input.val())) {
- search_status = false;
- return me.item_batch_no[item.item_code] = me.search_item.$input.val()
- } else if (me.serial_no_data[item.item_code]
- && in_list(Object.keys(me.serial_no_data[item.item_code]), me.search_item.$input.val())) {
- search_status = false;
- me.item_serial_no[item.item_code] = [me.search_item.$input.val(), me.serial_no_data[item.item_code][me.search_item.$input.val()]]
- return true
- } else if (me.barcode_data[item.item_code] &&
- in_list(me.barcode_data[item.item_code], me.search_item.$input.val())) {
- search_status = false;
- return true;
- } else if (reg.test(item.item_code.toLowerCase()) || (item.description && reg.test(item.description.toLowerCase())) ||
- reg.test(item.item_name.toLowerCase()) || reg.test(item.item_group.toLowerCase())) {
- return true
- }
- }
- })
- } else {
- return this.items_list;
- }
- },
-
- apply_category: function() {
- var me = this;
- category = this.selected_item_group || "All Item Groups";
- if(category == 'All Item Groups') {
- return this.item_data
- } else {
- return this.item_data.filter(function(element, index, array){
- return element.item_group == category;
- });
- }
- },
-
- bind_items_event: function() {
- var me = this;
- $(this.wrapper).on('click', '.pos-bill-item', function() {
- $(me.wrapper).find('.pos-bill-item').removeClass('active');
- $(this).addClass('active');
- me.numeric_val = "";
- me.numeric_id = ""
- me.item_code = $(this).attr("data-item-code");
- me.render_selected_item()
- me.bind_qty_event()
- me.update_rate()
- $(me.wrapper).find(".selected-item").scrollTop(1000);
- })
- },
-
- bind_qty_event: function () {
- var me = this;
-
- $(this.wrapper).on("change", ".pos-item-qty", function () {
- var item_code = $(this).parents(".pos-selected-item-action").attr("data-item-code");
- var qty = $(this).val();
- me.update_qty(item_code, qty);
- me.update_value();
- })
-
- $(this.wrapper).on("focusout", ".pos-item-qty", function () {
- var item_code = $(this).parents(".pos-selected-item-action").attr("data-item-code");
- var qty = $(this).val();
- me.update_qty(item_code, qty, true);
- me.update_value();
- })
-
- $(this.wrapper).find("[data-action='increase-qty']").on("click", function () {
- var item_code = $(this).parents(".pos-bill-item").attr("data-item-code");
- var qty = flt($(this).parents(".pos-bill-item").find('.pos-item-qty').val()) + 1;
- me.update_qty(item_code, qty);
- })
-
- $(this.wrapper).find("[data-action='decrease-qty']").on("click", function () {
- var item_code = $(this).parents(".pos-bill-item").attr("data-item-code");
- var qty = flt($(this).parents(".pos-bill-item").find('.pos-item-qty').val()) - 1;
- me.update_qty(item_code, qty);
- })
-
- $(this.wrapper).on("change", ".pos-item-disc", function () {
- var item_code = $(this).parents(".pos-selected-item-action").attr("data-item-code");
- var discount = $(this).val();
- if(discount > 100){
- discount = $(this).val('');
- frappe.show_alert({
- indicator: 'red',
- message: __('Discount amount cannot be greater than 100%')
- });
- me.update_discount(item_code, discount);
- }else{
- me.update_discount(item_code, discount);
- me.update_value();
- }
- })
- },
-
- bind_events: function() {
- var me = this;
- // if form is local then allow this function
- // $(me.wrapper).find(".pos-item-wrapper").on("click", function () {
- $(this.wrapper).on("click", ".pos-item-wrapper", function () {
- me.item_code = '';
- me.customer_validate();
- if($(me.pos_bill).is(":hidden")) return;
-
- if (me.frm.doc.docstatus == 0) {
- me.items = me.get_items($(this).attr("data-item-code"))
- me.add_to_cart();
- me.clear_selected_row();
- }
- });
-
- me.bind_delete_event()
- },
-
- update_qty: function (item_code, qty, remove_zero_qty_items) {
- var me = this;
- this.items = this.get_items(item_code);
- this.validate_serial_no()
- this.set_item_details(item_code, "qty", qty, remove_zero_qty_items);
- },
-
- update_discount: function(item_code, discount) {
- var me = this;
- this.items = this.get_items(item_code);
- this.set_item_details(item_code, "discount_percentage", discount);
- },
-
- update_rate: function () {
- var me = this;
- $(this.wrapper).on("change", ".pos-item-price", function () {
- var item_code = $(this).parents(".pos-selected-item-action").attr("data-item-code");
- me.set_item_details(item_code, "rate", $(this).val());
- me.update_value()
- })
- },
-
- update_value: function() {
- var me = this;
- var fields = {qty: ".pos-item-qty", "discount_percentage": ".pos-item-disc",
- "rate": ".pos-item-price", "amount": ".pos-amount"}
- this.child_doc = this.get_child_item(this.item_code);
-
- if(me.child_doc.length) {
- $.each(fields, function(key, field) {
- $(me.selected_row).find(field).val(me.child_doc[0][key])
- })
- } else {
- this.clear_selected_row();
- }
- },
-
- clear_selected_row: function() {
- $(this.wrapper).find('.selected-item').empty();
- },
-
- render_selected_item: function() {
- this.child_doc = this.get_child_item(this.item_code);
- $(this.wrapper).find('.selected-item').empty();
- if(this.child_doc.length) {
- this.child_doc[0]["allow_user_to_edit_rate"] = this.pos_profile_data["allow_user_to_edit_rate"] ? true : false,
- this.child_doc[0]["allow_user_to_edit_discount"] = this.pos_profile_data["allow_user_to_edit_discount"] ? true : false;
- this.selected_row = $(frappe.render_template("pos_selected_item", this.child_doc[0]))
- $(this.wrapper).find('.selected-item').html(this.selected_row)
- }
-
- $(this.selected_row).find('.form-control').click(function(){
- $(this).select();
- })
- },
-
- get_child_item: function(item_code) {
- var me = this;
- return $.map(me.frm.doc.items, function(doc){
- if(doc.item_code == item_code) {
- return doc
- }
- })
- },
-
- set_item_details: function (item_code, field, value, remove_zero_qty_items) {
- var me = this;
- if (value < 0) {
- frappe.throw(__("Enter value must be positive"));
- }
-
- this.remove_item = []
- $.each(this.frm.doc["items"] || [], function (i, d) {
- if (d.item_code == item_code) {
- if (d.serial_no && field == 'qty') {
- me.validate_serial_no_qty(d, item_code, field, value)
- }
-
- d[field] = flt(value);
- d.amount = flt(d.rate) * flt(d.qty);
- if (d.qty == 0 && remove_zero_qty_items) {
- me.remove_item.push(d.idx)
- }
-
- if(field=="discount_percentage" && value == 0) {
- d.rate = d.price_list_rate;
- }
- }
- });
-
- if (field == 'qty') {
- this.remove_zero_qty_items_from_cart();
- }
-
- this.update_paid_amount_status(false)
- },
-
- remove_zero_qty_items_from_cart: function () {
- var me = this;
- var idx = 0;
- this.items = []
- $.each(this.frm.doc["items"] || [], function (i, d) {
- if (!in_list(me.remove_item, d.idx)) {
- d.idx = idx;
- me.items.push(d);
- idx++;
- }
- });
-
- this.frm.doc["items"] = this.items;
- },
-
- make_discount_field: function () {
- var me = this;
-
- this.wrapper.find('input.discount-percentage').on("change", function () {
- me.frm.doc.additional_discount_percentage = flt($(this).val(), precision("additional_discount_percentage"));
-
- if(me.frm.doc.additional_discount_percentage && me.frm.doc.discount_amount) {
- // Reset discount amount
- me.frm.doc.discount_amount = 0;
- }
-
- var total = me.frm.doc.grand_total
-
- if (me.frm.doc.apply_discount_on == 'Net Total') {
- total = me.frm.doc.net_total
- }
-
- me.frm.doc.discount_amount = flt(total * flt(me.frm.doc.additional_discount_percentage) / 100, precision("discount_amount"));
- me.refresh();
- me.wrapper.find('input.discount-amount').val(me.frm.doc.discount_amount)
- });
-
- this.wrapper.find('input.discount-amount').on("change", function () {
- me.frm.doc.discount_amount = flt($(this).val(), precision("discount_amount"));
- me.frm.doc.additional_discount_percentage = 0.0;
- me.refresh();
- me.wrapper.find('input.discount-percentage').val(0);
- });
- },
-
- customer_validate: function () {
- var me = this;
- if (!this.frm.doc.customer || this.party_field.get_value() == "") {
- frappe.throw(__("Please select customer"))
- }
- },
-
- add_to_cart: function () {
- var me = this;
- var caught = false;
- var no_of_items = me.wrapper.find(".pos-bill-item").length;
-
- this.customer_validate();
- this.mandatory_batch_no();
- this.validate_serial_no();
- this.validate_warehouse();
-
- if (no_of_items != 0) {
- $.each(this.frm.doc["items"] || [], function (i, d) {
- if (d.item_code == me.items[0].item_code) {
- caught = true;
- d.qty += 1;
- d.amount = flt(d.rate) * flt(d.qty);
- if (me.item_serial_no[d.item_code]) {
- d.serial_no += '\n' + me.item_serial_no[d.item_code][0]
- d.warehouse = me.item_serial_no[d.item_code][1]
- }
-
- if (me.item_batch_no.length) {
- d.batch_no = me.item_batch_no[d.item_code]
- }
- }
- });
- }
-
- // if item not found then add new item
- if (!caught)
- this.add_new_item_to_grid();
-
- this.update_paid_amount_status(false)
- this.wrapper.find(".item-cart-items").scrollTop(1000);
- },
-
- add_new_item_to_grid: function () {
- var me = this;
- this.child = frappe.model.add_child(this.frm.doc, this.frm.doc.doctype + " Item", "items");
- this.child.item_code = this.items[0].item_code;
- this.child.item_name = this.items[0].item_name;
- this.child.stock_uom = this.items[0].stock_uom;
- this.child.uom = this.items[0].sales_uom || this.items[0].stock_uom;
- this.child.conversion_factor = this.items[0].conversion_factor || 1;
- this.child.brand = this.items[0].brand;
- this.child.description = this.items[0].description || this.items[0].item_name;
- this.child.discount_percentage = 0.0;
- this.child.qty = 1;
- this.child.item_group = this.items[0].item_group;
- this.child.cost_center = this.pos_profile_data['cost_center'] || this.items[0].cost_center;
- this.child.income_account = this.pos_profile_data['income_account'] || this.items[0].income_account;
- this.child.warehouse = (this.item_serial_no[this.child.item_code]
- ? this.item_serial_no[this.child.item_code][1] : (this.pos_profile_data['warehouse'] || this.items[0].default_warehouse));
-
- customer = this.frm.doc.customer;
- let rate;
-
- customer_price_list = this.customer_wise_price_list[customer]
- if (customer_price_list && customer_price_list[this.child.item_code]){
- rate = flt(this.customer_wise_price_list[customer][this.child.item_code] * this.child.conversion_factor, 9) / flt(this.frm.doc.conversion_rate, 9);
- }
- else{
- rate = flt(this.price_list_data[this.child.item_code] * this.child.conversion_factor, 9) / flt(this.frm.doc.conversion_rate, 9);
- }
-
- this.child.price_list_rate = rate;
- this.child.rate = rate;
- this.child.actual_qty = me.get_actual_qty(this.items[0]);
- this.child.amount = flt(this.child.qty) * flt(this.child.rate);
- this.child.batch_no = this.item_batch_no[this.child.item_code];
- this.child.serial_no = (this.item_serial_no[this.child.item_code]
- ? this.item_serial_no[this.child.item_code][0] : '');
- this.child.item_tax_rate = JSON.stringify(this.tax_data[this.child.item_code]);
- },
-
- update_paid_amount_status: function (update_paid_amount) {
- if (this.frm.doc.offline_pos_name) {
- update_paid_amount = update_paid_amount ? false : true;
- }
-
- this.refresh(update_paid_amount);
- },
-
- refresh: function (update_paid_amount) {
- var me = this;
- this.refresh_fields(update_paid_amount);
- this.set_primary_action();
- },
-
- refresh_fields: function (update_paid_amount) {
- this.apply_pricing_rule();
- this.discount_amount_applied = false;
- this._calculate_taxes_and_totals();
- this.calculate_discount_amount();
- this.show_items_in_item_cart();
- this.set_taxes();
- this.calculate_outstanding_amount(update_paid_amount);
- this.set_totals();
- this.update_total_qty();
- },
-
- get_company_currency: function () {
- return erpnext.get_currency(this.frm.doc.company);
- },
-
- show_items_in_item_cart: function () {
- var me = this;
- var $items = this.wrapper.find(".items").empty();
- var $no_items_message = this.wrapper.find(".no-items-message");
- $no_items_message.toggle(this.frm.doc.items.length === 0);
-
- var $totals_area = this.wrapper.find('.totals-area');
- $totals_area.toggle(this.frm.doc.items.length > 0);
-
- $.each(this.frm.doc.items || [], function (i, d) {
- $(frappe.render_template("pos_bill_item_new", {
- item_code: d.item_code,
- item_name: (d.item_name === d.item_code || !d.item_name) ? "" : ("<br>" + d.item_name),
- qty: d.qty,
- discount_percentage: d.discount_percentage || 0.0,
- actual_qty: me.actual_qty_dict[d.item_code] || 0.0,
- projected_qty: d.projected_qty,
- rate: format_currency(d.rate, me.frm.doc.currency),
- amount: format_currency(d.amount, me.frm.doc.currency),
- selected_class: (me.item_code == d.item_code) ? "active" : ""
- })).appendTo($items);
- });
-
- this.wrapper.find("input.pos-item-qty").on("focus", function () {
- $(this).select();
- });
-
- this.wrapper.find("input.pos-item-disc").on("focus", function () {
- $(this).select();
- });
-
- this.wrapper.find("input.pos-item-price").on("focus", function () {
- $(this).select();
- });
- },
-
- set_taxes: function () {
- var me = this;
- me.frm.doc.total_taxes_and_charges = 0.0
-
- var taxes = this.frm.doc.taxes || [];
- $(this.wrapper)
- .find(".tax-area").toggleClass("hide", (taxes && taxes.length) ? false : true)
- .find(".tax-table").empty();
-
- $.each(taxes, function (i, d) {
- if (d.tax_amount && cint(d.included_in_print_rate) == 0) {
- $(frappe.render_template("pos_tax_row", {
- description: d.description,
- tax_amount: format_currency(flt(d.tax_amount_after_discount_amount),
- me.frm.doc.currency)
- })).appendTo(me.wrapper.find(".tax-table"));
- }
- });
- },
-
- set_totals: function () {
- var me = this;
- this.wrapper.find(".net-total").text(format_currency(me.frm.doc.total, me.frm.doc.currency));
- this.wrapper.find(".grand-total").text(format_currency(me.frm.doc.grand_total, me.frm.doc.currency));
- this.wrapper.find('input.discount-percentage').val(this.frm.doc.additional_discount_percentage);
- this.wrapper.find('input.discount-amount').val(this.frm.doc.discount_amount);
- },
-
- update_total_qty: function() {
- var me = this;
- var qty_total = 0;
- $.each(this.frm.doc["items"] || [], function (i, d) {
- if (d.item_code) {
- qty_total += d.qty;
- }
- });
- this.frm.doc.qty_total = qty_total;
- this.wrapper.find('.qty-total').text(this.frm.doc.qty_total);
- },
-
- set_primary_action: function () {
- var me = this;
- this.page.set_primary_action(__("New Cart"), function () {
- me.make_new_cart()
- me.make_menu_list()
- }, "fa fa-plus")
-
- if (this.frm.doc.docstatus == 1 || this.pos_profile_data["allow_print_before_pay"]) {
- this.page.set_secondary_action(__("Print"), function () {
- me.create_invoice();
- var html = frappe.render(me.print_template_data, me.frm.doc)
- me.print_document(html)
- })
- }
-
- if (this.frm.doc.docstatus == 1) {
- this.page.add_menu_item(__("Email"), function () {
- me.email_prompt()
- })
- }
- },
-
- make_new_cart: function (){
- this.item_code = '';
- this.page.clear_secondary_action();
- this.save_previous_entry();
- this.create_new();
- this.refresh();
- this.toggle_input_field();
- this.render_list_customers();
- this.set_focus();
- },
-
- print_dialog: function () {
- var me = this;
-
- this.msgprint = frappe.msgprint(
- `<a class="btn btn-primary print_doc"
- style="margin-right: 5px;">${__('Print')}</a>
- <a class="btn btn-default new_doc">${__('New')}</a>`);
-
- this.msgprint.msg_area.find('.print_doc').on('click', function() {
- var html = frappe.render(me.print_template_data, me.frm.doc);
- me.print_document(html);
- })
-
- this.msgprint.msg_area.find('.new_doc').on('click', function() {
- me.msgprint.hide();
- me.make_new_cart();
- })
-
- },
-
- print_document: function (html) {
- var w = window.open();
- w.document.write(html);
- w.document.close();
- setTimeout(function () {
- w.print();
- w.close();
- }, 1000);
- },
-
- submit_invoice: function () {
- var me = this;
- this.change_status();
- this.update_serial_no()
- if (this.frm.doc.docstatus == 1) {
- this.print_dialog()
- }
- },
-
- update_serial_no: function() {
- var me = this;
-
- //Remove the sold serial no from the cache
- $.each(this.frm.doc.items, function(index, data) {
- var sn = data.serial_no.split('\n')
- if(sn.length) {
- var serial_no_list = me.serial_no_data[data.item_code]
- if(serial_no_list) {
- $.each(sn, function(i, serial_no) {
- if(in_list(Object.keys(serial_no_list), serial_no)) {
- delete serial_no_list[serial_no]
- }
- })
- me.serial_no_data[data.item_code] = serial_no_list;
- }
- }
- })
- },
-
- change_status: function () {
- if (this.frm.doc.docstatus == 0) {
- this.frm.doc.docstatus = 1;
- this.update_invoice();
- this.toggle_input_field();
- }
- },
-
- toggle_input_field: function () {
- var pointer_events = 'inherit'
- var disabled = this.frm.doc.docstatus == 1 ? true: false;
- $(this.wrapper).find('input').attr("disabled", disabled);
- $(this.wrapper).find('select').attr("disabled", disabled);
- $(this.wrapper).find('input').attr("disabled", disabled);
- $(this.wrapper).find('select').attr("disabled", disabled);
- $(this.wrapper).find('button').attr("disabled", disabled);
- this.party_field.$input.attr('disabled', disabled);
-
- if (this.frm.doc.docstatus == 1) {
- pointer_events = 'none';
- }
-
- $(this.wrapper).find('.pos-bill').css('pointer-events', pointer_events);
- $(this.wrapper).find('.pos-items-section').css('pointer-events', pointer_events);
- this.set_primary_action();
-
- $(this.wrapper).find('#pos-item-disc').prop('disabled',
- this.pos_profile_data.allow_user_to_edit_discount ? false : true);
-
- $(this.wrapper).find('#pos-item-price').prop('disabled',
- this.pos_profile_data.allow_user_to_edit_rate ? false : true);
- },
-
- create_invoice: function () {
- var me = this;
- var existing_pos_list = [];
- var invoice_data = {};
- this.si_docs = this.get_doc_from_localstorage();
-
- if(this.si_docs) {
- this.si_docs.forEach((row) => {
- existing_pos_list.push(Object.keys(row)[0]);
- });
- }
-
- if (this.frm.doc.offline_pos_name
- && in_list(existing_pos_list, cstr(this.frm.doc.offline_pos_name))) {
- this.update_invoice()
- } else if(!this.frm.doc.offline_pos_name) {
- this.frm.doc.offline_pos_name = frappe.datetime.now_datetime();
- this.frm.doc.posting_date = frappe.datetime.get_today();
- this.frm.doc.posting_time = frappe.datetime.now_time();
- this.frm.doc.pos_total_qty = this.frm.doc.qty_total;
- this.frm.doc.pos_profile = this.pos_profile_data['name'];
- invoice_data[this.frm.doc.offline_pos_name] = this.frm.doc;
- this.si_docs.push(invoice_data);
- this.update_localstorage();
- this.set_primary_action();
- }
- return invoice_data;
- },
-
- update_invoice: function () {
- var me = this;
- this.si_docs = this.get_doc_from_localstorage();
- $.each(this.si_docs, function (index, data) {
- for (var key in data) {
- if (key == me.frm.doc.offline_pos_name) {
- me.si_docs[index][key] = me.frm.doc;
- me.update_localstorage();
- }
- }
- });
- },
-
- update_localstorage: function () {
- try {
- localStorage.setItem('sales_invoice_doc', JSON.stringify(this.si_docs));
- } catch (e) {
- frappe.throw(__("LocalStorage is full , did not save"))
- }
- },
-
- get_doc_from_localstorage: function () {
- try {
- return JSON.parse(localStorage.getItem('sales_invoice_doc')) || [];
- } catch (e) {
- return []
- }
- },
-
- set_interval_for_si_sync: function () {
- var me = this;
- setInterval(function () {
- me.freeze_screen = false;
- me.sync_sales_invoice()
- }, 180000)
- },
-
- sync_sales_invoice: function () {
- var me = this;
- this.si_docs = this.get_submitted_invoice() || [];
- this.email_queue_list = this.get_email_queue() || {};
- this.customers_list = this.get_customers_details() || {};
-
- if (this.si_docs.length || this.email_queue_list || this.customers_list) {
- frappe.call({
- method: "erpnext.accounts.doctype.sales_invoice.pos.make_invoice",
- freeze: true,
- args: {
- pos_profile: me.pos_profile_data,
- doc_list: me.si_docs,
- email_queue_list: me.email_queue_list,
- customers_list: me.customers_list
- },
- callback: function (r) {
- if (r.message) {
- me.freeze = false;
- me.customers = r.message.synced_customers_list;
- me.address = r.message.synced_address;
- me.contacts = r.message.synced_contacts;
- me.removed_items = r.message.invoice;
- me.removed_email = r.message.email_queue;
- me.removed_customers = r.message.customers;
- me.remove_doc_from_localstorage();
- me.remove_email_queue_from_localstorage();
- me.remove_customer_from_localstorage();
- me.prepare_customer_mapper();
- me.autocomplete_customers();
- me.render_list_customers();
- }
- }
- })
- }
- },
-
- get_submitted_invoice: function () {
- var invoices = [];
- var index = 1;
- var docs = this.get_doc_from_localstorage();
- if (docs) {
- invoices = $.map(docs, function (data) {
- for (var key in data) {
- if (data[key].docstatus == 1 && index < 50) {
- index++
- data[key].docstatus = 0;
- return data
- }
- }
- });
- }
-
- return invoices
- },
-
- remove_doc_from_localstorage: function () {
- var me = this;
- this.si_docs = this.get_doc_from_localstorage();
- this.new_si_docs = [];
- if (this.removed_items) {
- $.each(this.si_docs, function (index, data) {
- for (var key in data) {
- if (!in_list(me.removed_items, key)) {
- me.new_si_docs.push(data);
- }
- }
- })
- this.removed_items = [];
- this.si_docs = this.new_si_docs;
- this.update_localstorage();
- }
- },
-
- remove_email_queue_from_localstorage: function() {
- var me = this;
- this.email_queue = this.get_email_queue()
- if (this.removed_email) {
- $.each(this.email_queue_list, function (index, data) {
- if (in_list(me.removed_email, index)) {
- delete me.email_queue[index]
- }
- })
- this.update_email_queue();
- }
- },
-
- remove_customer_from_localstorage: function() {
- var me = this;
- this.customer_details = this.get_customers_details()
- if (this.removed_customers) {
- $.each(this.customers_list, function (index, data) {
- if (in_list(me.removed_customers, index)) {
- delete me.customer_details[index]
- }
- })
- this.update_customer_in_localstorage();
- }
- },
-
- validate: function () {
- var me = this;
- this.customer_validate();
- this.validate_zero_qty_items();
- this.item_validate();
- this.validate_mode_of_payments();
- },
-
- validate_zero_qty_items: function() {
- this.remove_item = [];
-
- this.frm.doc.items.forEach(d => {
- if (d.qty == 0) {
- this.remove_item.push(d.idx);
- }
- });
-
- if(this.remove_item) {
- this.remove_zero_qty_items_from_cart();
- }
- },
-
- item_validate: function () {
- if (this.frm.doc.items.length == 0) {
- frappe.throw(__("Select items to save the invoice"))
- }
- },
-
- validate_mode_of_payments: function () {
- if (this.frm.doc.payments.length === 0) {
- frappe.throw(__("Payment Mode is not configured. Please check, whether account has been set on Mode of Payments or on POS Profile."))
- }
- },
-
- validate_serial_no: function () {
- var me = this;
- var item_code = ''
- var serial_no = '';
- for (var key in this.item_serial_no) {
- item_code = key;
- serial_no = me.item_serial_no[key][0];
- }
-
- if (this.items && this.items[0].has_serial_no && serial_no == "") {
- this.refresh();
- frappe.throw(__(repl("Error: Serial no is mandatory for item %(item)s", {
- 'item': this.items[0].item_code
- })))
- }
-
- if (item_code && serial_no) {
- $.each(this.frm.doc.items, function (index, data) {
- if (data.item_code == item_code) {
- if (in_list(data.serial_no.split('\n'), serial_no)) {
- frappe.throw(__(repl("Serial no %(serial_no)s is already taken", {
- 'serial_no': serial_no
- })))
- }
- }
- })
- }
- },
-
- validate_serial_no_qty: function (args, item_code, field, value) {
- var me = this;
- if (args.item_code == item_code && args.serial_no
- && field == 'qty' && cint(value) != value) {
- args.qty = 0.0;
- this.refresh();
- frappe.throw(__("Serial no item cannot be a fraction"))
- }
-
- if (args.item_code == item_code && args.serial_no && args.serial_no.split('\n').length != cint(value)) {
- args.qty = 0.0;
- args.serial_no = ''
- this.refresh();
- frappe.throw(__(repl("Total nos of serial no is not equal to quantity for item %(item)s.", {
- 'item': item_code
- })))
- }
- },
-
- mandatory_batch_no: function () {
- var me = this;
- if (this.items[0].has_batch_no && !this.item_batch_no[this.items[0].item_code]) {
- frappe.prompt([{
- 'fieldname': 'batch',
- 'fieldtype': 'Select',
- 'label': __('Batch No'),
- 'reqd': 1,
- 'options': this.batch_no_data[this.items[0].item_code]
- }],
- function(values){
- me.item_batch_no[me.items[0].item_code] = values.batch;
- const item = me.frm.doc.items.find(
- ({ item_code }) => item_code === me.items[0].item_code
- );
- if (item) {
- item.batch_no = values.batch;
- }
- },
- __('Select Batch No'))
- }
- },
-
- apply_pricing_rule: function () {
- var me = this;
- $.each(this.frm.doc["items"], function (n, item) {
- var pricing_rule = me.get_pricing_rule(item)
- me.validate_pricing_rule(pricing_rule)
- if (pricing_rule.length) {
- item.pricing_rule = pricing_rule[0].name;
- item.margin_type = pricing_rule[0].margin_type;
- item.price_list_rate = pricing_rule[0].price || item.price_list_rate;
- item.margin_rate_or_amount = pricing_rule[0].margin_rate_or_amount;
- item.discount_percentage = pricing_rule[0].discount_percentage || 0.0;
- me.apply_pricing_rule_on_item(item)
- } else if (item.pricing_rule) {
- item.price_list_rate = me.price_list_data[item.item_code]
- item.margin_rate_or_amount = 0.0;
- item.discount_percentage = 0.0;
- item.pricing_rule = null;
- me.apply_pricing_rule_on_item(item)
- }
-
- if(item.discount_percentage > 0) {
- me.apply_pricing_rule_on_item(item)
- }
- })
- },
-
- get_pricing_rule: function (item) {
- var me = this;
- return $.grep(this.pricing_rules, function (data) {
- if (item.qty >= data.min_qty && (item.qty <= (data.max_qty ? data.max_qty : item.qty))) {
- if (me.validate_item_condition(data, item)) {
- if (in_list(['Customer', 'Customer Group', 'Territory', 'Campaign'], data.applicable_for)) {
- return me.validate_condition(data)
- } else {
- return true
- }
- }
- }
- })
- },
-
- validate_item_condition: function (data, item) {
- var apply_on = frappe.model.scrub(data.apply_on);
-
- return (data.apply_on == 'Item Group')
- ? this.validate_item_group(data.item_group, item.item_group) : (data[apply_on] == item[apply_on]);
- },
-
- validate_item_group: function (pr_item_group, cart_item_group) {
- //pr_item_group = pricing rule's item group
- //cart_item_group = cart item's item group
- //this.item_groups has information about item group's lft and rgt
- //for example: {'Foods': [12, 19]}
-
- pr_item_group = this.item_groups[pr_item_group]
- cart_item_group = this.item_groups[cart_item_group]
-
- return (cart_item_group[0] >= pr_item_group[0] &&
- cart_item_group[1] <= pr_item_group[1])
- },
-
- validate_condition: function (data) {
- //This method check condition based on applicable for
- var condition = this.get_mapper_for_pricing_rule(data)[data.applicable_for]
- if (in_list(condition[1], condition[0])) {
- return true
- }
- },
-
- get_mapper_for_pricing_rule: function (data) {
- return {
- 'Customer': [data.customer, [this.frm.doc.customer]],
- 'Customer Group': [data.customer_group, [this.frm.doc.customer_group, 'All Customer Groups']],
- 'Territory': [data.territory, [this.frm.doc.territory, 'All Territories']],
- 'Campaign': [data.campaign, [this.frm.doc.campaign]],
- }
- },
-
- validate_pricing_rule: function (pricing_rule) {
- //This method validate duplicate pricing rule
- var pricing_rule_name = '';
- var priority = 0;
- var pricing_rule_list = [];
- var priority_list = []
-
- if (pricing_rule.length > 1) {
-
- $.each(pricing_rule, function (index, data) {
- pricing_rule_name += data.name + ','
- priority_list.push(data.priority)
- if (priority <= data.priority) {
- priority = data.priority
- pricing_rule_list.push(data)
- }
- })
-
- var count = 0
- $.each(priority_list, function (index, value) {
- if (value == priority) {
- count++
- }
- })
-
- if (priority == 0 || count > 1) {
- frappe.throw(__(repl("Multiple Price Rules exists with same criteria, please resolve conflict by assigning priority. Price Rules: %(pricing_rule)s", {
- 'pricing_rule': pricing_rule_name
- })))
- }
-
- return pricing_rule_list
- }
- },
-
- validate_warehouse: function () {
- if (this.items[0].is_stock_item && !this.items[0].default_warehouse && !this.pos_profile_data['warehouse']) {
- frappe.throw(__("Default warehouse is required for selected item"))
- }
- },
-
- get_actual_qty: function (item) {
- this.actual_qty = 0.0;
-
- var warehouse = this.pos_profile_data['warehouse'] || item.default_warehouse;
- if (warehouse && this.bin_data[item.item_code]) {
- this.actual_qty = this.bin_data[item.item_code][warehouse] || 0;
- this.actual_qty_dict[item.item_code] = this.actual_qty
- }
-
- return this.actual_qty
- },
-
- update_customer_in_localstorage: function() {
- var me = this;
- try {
- localStorage.setItem('customer_details', JSON.stringify(this.customer_details));
- } catch (e) {
- frappe.throw(__("LocalStorage is full , did not save"))
- }
- }
-})
\ No newline at end of file
diff --git a/erpnext/accounts/page/pos/pos.json b/erpnext/accounts/page/pos/pos.json
deleted file mode 100644
index abd918a..0000000
--- a/erpnext/accounts/page/pos/pos.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "content": null,
- "creation": "2014-08-08 02:45:55.931022",
- "docstatus": 0,
- "doctype": "Page",
- "icon": "fa fa-th",
- "modified": "2014-08-08 05:59:33.045012",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "pos",
- "owner": "Administrator",
- "page_name": "pos",
- "roles": [
- {
- "role": "Sales User"
- },
- {
- "role": "Purchase User"
- },
- {
- "role": "Accounts User"
- }
- ],
- "script": null,
- "standard": "Yes",
- "style": null,
- "title": "POS"
-}
\ No newline at end of file
diff --git a/erpnext/accounts/page/pos/test_pos.js b/erpnext/accounts/page/pos/test_pos.js
deleted file mode 100644
index e5524a2..0000000
--- a/erpnext/accounts/page/pos/test_pos.js
+++ /dev/null
@@ -1,52 +0,0 @@
-QUnit.test("test:Sales Invoice", function(assert) {
- assert.expect(3);
- let done = assert.async();
-
- frappe.run_serially([
- () => {
- return frappe.tests.make("POS Profile", [
- {naming_series: "SINV"},
- {pos_profile_name: "_Test POS Profile"},
- {country: "India"},
- {currency: "INR"},
- {write_off_account: "Write Off - FT"},
- {write_off_cost_center: "Main - FT"},
- {payments: [
- [
- {"default": 1},
- {"mode_of_payment": "Cash"}
- ]]
- }
- ]);
- },
- () => cur_frm.save(),
- () => frappe.timeout(2),
- () => {
- assert.equal(cur_frm.doc.payments[0].default, 1, "Default mode of payment tested");
- },
- () => frappe.timeout(1),
- () => {
- return frappe.tests.make("Sales Invoice", [
- {customer: "Test Customer 2"},
- {is_pos: 1},
- {posting_date: frappe.datetime.get_today()},
- {due_date: frappe.datetime.get_today()},
- {items: [
- [
- {"item_code": "Test Product 1"},
- {"qty": 5},
- {"warehouse":'Stores - FT'}
- ]]
- }
- ]);
- },
- () => frappe.timeout(2),
- () => cur_frm.save(),
- () => frappe.timeout(2),
- () => {
- assert.equal(cur_frm.doc.payments[0].default, 1, "Default mode of payment tested");
- assert.equal(cur_frm.doc.payments[0].mode_of_payment, "Cash", "Default mode of payment tested");
- },
- () => done()
- ]);
-});
\ No newline at end of file
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index b764eff..28a6519 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -184,7 +184,7 @@
def set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype):
- if doctype not in ["Sales Invoice", "Purchase Invoice"]:
+ if doctype not in ["POS Invoice", "Sales Invoice", "Purchase Invoice"]:
# not an invoice
return {
party_type.lower(): party
diff --git a/erpnext/accounts/print_format/gst_pos_invoice/gst_pos_invoice.json b/erpnext/accounts/print_format/gst_pos_invoice/gst_pos_invoice.json
index 1c5a195..1aa1c02 100644
--- a/erpnext/accounts/print_format/gst_pos_invoice/gst_pos_invoice.json
+++ b/erpnext/accounts/print_format/gst_pos_invoice/gst_pos_invoice.json
@@ -7,10 +7,10 @@
"docstatus": 0,
"doctype": "Print Format",
"font": "Default",
- "html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Monospace;\n\t\tline-height: 200%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n<p class=\"text-center\">\n\t{{ doc.company }}<br>\n\t{% if doc.company_address_display %}\n\t\t{% set company_address = doc.company_address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t{% if \"GSTIN\" not in company_address %}\n\t\t\t{{ company_address }}\n\t\t\t<b>{{ _(\"GSTIN\") }}:</b>{{ doc.company_gstin }}\n\t\t{% else %}\n\t\t\t{{ company_address.replace(\"GSTIN\", \"<br>GSTIN\") }}\n\t\t{% endif %}\n\t{% endif %}\n\t<br>\n\t{% if doc.docstatus == 0 %}\n\t\t<b>{{ doc.status + \" \"+ (doc.select_print_heading or _(\"Invoice\")) }}</b><br>\n\t{% else %}\n\t\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t{% endif %}\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t{% if doc.grand_total > 50000 %}\n\t\t{% set customer_address = doc.address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t<b>{{ _(\"Customer\") }}:</b><br>\n\t\t{{ doc.customer_name }}<br>\n\t\t{{ customer_address }}\n\t{% endif %}\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"40%\">{{ _(\"Item\") }}</b></th>\n\t\t\t<th width=\"30%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"30%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.gst_hsn_code -%}\n\t\t\t\t\t<br><b>{{ _(\"HSN/SAC\") }}:</b> {{ item.gst_hsn_code }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"Serial No\") }}:</b> {{ item.serial_no }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.rate }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if (not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) and row.tax_amount != 0 -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ row.description }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t{%- if doc.change_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t{%- endif -%}\n\t</tbody>\n</table>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
+ "html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Tahoma, sans-serif;\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n<p class=\"text-center\">\n\t{{ doc.company }}<br>\n\t{% if doc.company_address_display %}\n\t\t{% set company_address = doc.company_address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t{% if \"GSTIN\" not in company_address %}\n\t\t\t{{ company_address }}\n\t\t\t<b>{{ _(\"GSTIN\") }}:</b>{{ doc.company_gstin }}\n\t\t{% else %}\n\t\t\t{{ company_address.replace(\"GSTIN\", \"<br>GSTIN\") }}\n\t\t{% endif %}\n\t{% endif %}\n\t<br>\n\t{% if doc.docstatus == 0 %}\n\t\t<b>{{ doc.status + \" \"+ (doc.select_print_heading or _(\"Invoice\")) }}</b><br>\n\t{% else %}\n\t\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t{% endif %}\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t{% if doc.grand_total > 50000 %}\n\t\t{% set customer_address = doc.address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t<b>{{ _(\"Customer\") }}:</b><br>\n\t\t{{ doc.customer_name }}<br>\n\t\t{{ customer_address }}\n\t{% endif %}\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</b></th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.gst_hsn_code -%}\n\t\t\t\t\t<br><b>{{ _(\"HSN/SAC\") }}:</b> {{ item.gst_hsn_code }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"Serial No\") }}:</b> {{ item.serial_no }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.rate }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if (not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) and row.tax_amount != 0 -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ row.description }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t{%- if doc.change_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t{%- endif -%}\n\t</tbody>\n</table>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
"idx": 0,
"line_breaks": 0,
- "modified": "2019-12-09 17:39:23.356573",
+ "modified": "2020-04-29 16:39:12.936215",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GST POS Invoice",
diff --git a/erpnext/accounts/print_format/pos_invoice/pos_invoice.json b/erpnext/accounts/print_format/pos_invoice/pos_invoice.json
index be69922..13a973d 100644
--- a/erpnext/accounts/print_format/pos_invoice/pos_invoice.json
+++ b/erpnext/accounts/print_format/pos_invoice/pos_invoice.json
@@ -6,10 +6,10 @@
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype": "Print Format",
- "html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Monospace;\n\t\tline-height: 200%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\">\n\t{{ doc.company }}<br>\n\t{{ doc.select_print_heading or _(\"Invoice\") }}<br>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</b></th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.get_formatted(\"rate\") }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ row.description }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
+ "html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Tahoma, sans-serif;\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t{{ doc.select_print_heading or _(\"Invoice\") }}<br>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</b></th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.get_formatted(\"rate\") }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ row.description }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
"idx": 1,
"line_breaks": 0,
- "modified": "2019-12-09 17:40:53.183574",
+ "modified": "2020-04-29 16:35:07.043058",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 90c67f1..3f127a2 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -213,7 +213,7 @@
doc.return_against = source.name
doc.ignore_pricing_rule = 1
doc.set_warehouse = ""
- if doctype == "Sales Invoice":
+ if doctype == "Sales Invoice" or doctype == "POS Invoice":
doc.is_pos = source.is_pos
# look for Print Heading "Credit Note"
@@ -229,7 +229,7 @@
tax.tax_amount = -1 * tax.tax_amount
if doc.get("is_return"):
- if doc.doctype == 'Sales Invoice':
+ if doc.doctype == 'Sales Invoice' or doc.doctype == 'POS Invoice':
doc.set('payments', [])
for data in source.payments:
paid_amount = 0.00
@@ -241,8 +241,11 @@
'mode_of_payment': data.mode_of_payment,
'type': data.type,
'amount': -1 * paid_amount,
- 'base_amount': -1 * base_paid_amount
+ 'base_amount': -1 * base_paid_amount,
+ 'account': data.account
})
+ if doc.is_pos:
+ doc.paid_amount = -1 * source.paid_amount
elif doc.doctype == 'Purchase Invoice':
doc.paid_amount = -1 * source.paid_amount
doc.base_paid_amount = -1 * source.base_paid_amount
@@ -287,7 +290,7 @@
target_doc.dn_detail = source_doc.name
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
- elif doctype == "Sales Invoice":
+ elif doctype == "Sales Invoice" or doctype == "POS Invoice":
target_doc.sales_order = source_doc.sales_order
target_doc.delivery_note = source_doc.delivery_note
target_doc.so_detail = source_doc.so_detail
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index b465a10..0dc9878 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -85,6 +85,12 @@
"Bank Transaction": [
["Unreconciled", "eval:self.docstatus == 1 and self.unallocated_amount>0"],
["Reconciled", "eval:self.docstatus == 1 and self.unallocated_amount<=0"]
+ ],
+ "POS Opening Entry": [
+ ["Draft", None],
+ ["Open", "eval:self.docstatus == 1 and not self.pos_closing_entry"],
+ ["Closed", "eval:self.docstatus == 1 and self.pos_closing_entry"],
+ ["Cancelled", "eval:self.docstatus == 2"],
]
}
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 6449c71..572e1ca 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -370,7 +370,7 @@
self._set_in_company_currency(self.doc, ["total_taxes_and_charges", "rounding_adjustment"])
- if self.doc.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]:
+ if self.doc.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"]:
self.doc.base_grand_total = flt(self.doc.grand_total * self.doc.conversion_rate, self.doc.precision("base_grand_total")) \
if self.doc.total_taxes_and_charges else self.doc.base_net_total
else:
@@ -619,17 +619,14 @@
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
def update_paid_amount_for_return(self, total_amount_to_pay):
- default_mode_of_payment = frappe.db.get_value('Sales Invoice Payment',
- {'parent': self.doc.pos_profile, 'default': 1},
- ['mode_of_payment', 'type', 'account'], as_dict=1)
+ default_mode_of_payment = frappe.db.get_value('POS Payment Method',
+ {'parent': self.doc.pos_profile, 'default': 1}, ['mode_of_payment'], as_dict=1)
self.doc.payments = []
if default_mode_of_payment:
self.doc.append('payments', {
'mode_of_payment': default_mode_of_payment.mode_of_payment,
- 'type': default_mode_of_payment.type,
- 'account': default_mode_of_payment.account,
'amount': total_amount_to_pay
})
else:
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index ea2611f..2fb9d7f 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -14,6 +14,7 @@
erpnext.patches.v4_0.move_warehouse_user_to_restrictions
erpnext.patches.v4_0.global_defaults_to_system_settings
erpnext.patches.v4_0.update_incharge_name_to_sales_person_in_maintenance_schedule
+execute:frappe.reload_doc("accounts", "doctype", "POS Payment Method") #2020-05-28
execute:frappe.reload_doc("HR", "doctype", "HR Settings") #2020-01-16
execute:frappe.reload_doc('stock', 'doctype', 'warehouse') # 2017-04-24
execute:frappe.reload_doc('accounts', 'doctype', 'sales_invoice') # 2016-08-31
@@ -437,7 +438,6 @@
erpnext.patches.v8_7.sync_india_custom_fields
erpnext.patches.v8_7.fix_purchase_receipt_status
erpnext.patches.v8_6.rename_bom_update_tool
-erpnext.patches.v8_7.set_offline_in_pos_settings #11-09-17
erpnext.patches.v8_9.add_setup_progress_actions #08-09-2017 #26-09-2017 #22-11-2017 #15-12-2017
erpnext.patches.v8_9.rename_company_sales_target_field
erpnext.patches.v8_8.set_bom_rate_as_per_uom
@@ -677,6 +677,8 @@
erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123
erpnext.patches.v12_0.fix_quotation_expired_status
erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry
+erpnext.patches.v12_0.rename_pos_closing_doctype
+erpnext.patches.v13_0.replace_pos_payment_mode_table
erpnext.patches.v12_0.retain_permission_rules_for_video_doctype
erpnext.patches.v12_0.remove_duplicate_leave_ledger_entries #2020-05-22
erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive
@@ -695,6 +697,7 @@
execute:frappe.delete_doc("Report", "Department Analytics")
execute:frappe.rename_doc("Desk Page", "Loan Management", "Loan", force=True)
erpnext.patches.v12_0.update_uom_conversion_factor
+execute:frappe.delete_doc_if_exists("Page", "pos") #29-05-2020
erpnext.patches.v13_0.delete_old_purchase_reports
erpnext.patches.v12_0.set_italian_import_supplier_invoice_permissions
erpnext.patches.v13_0.update_subscription
@@ -708,6 +711,7 @@
erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020
erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020
erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020
+erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020
erpnext.patches.v12_0.add_taxjar_integration_field
erpnext.patches.v13_0.delete_report_requested_items_to_order
erpnext.patches.v12_0.update_item_tax_template_company
diff --git a/erpnext/patches/v11_0/refactor_autoname_naming.py b/erpnext/patches/v11_0/refactor_autoname_naming.py
index d67c723..5dc5d3b 100644
--- a/erpnext/patches/v11_0/refactor_autoname_naming.py
+++ b/erpnext/patches/v11_0/refactor_autoname_naming.py
@@ -54,7 +54,7 @@
'Payroll Entry': 'HR-PRUN-.YYYY.-.#####',
'Period Closing Voucher': 'ACC-PCV-.YYYY.-.#####',
'Plant Analysis': 'AG-PLA-.YYYY.-.#####',
- 'POS Closing Voucher': 'POS-CLO-.YYYY.-.#####',
+ 'POS Closing Entry': 'POS-CLO-.YYYY.-.#####',
'Prepared Report': 'SYS-PREP-.YYYY.-.#####',
'Program Enrollment': 'EDU-ENR-.YYYY.-.#####',
'Quotation Item': '',
diff --git a/erpnext/patches/v12_0/rename_pos_closing_doctype.py b/erpnext/patches/v12_0/rename_pos_closing_doctype.py
new file mode 100644
index 0000000..8ca92ef
--- /dev/null
+++ b/erpnext/patches/v12_0/rename_pos_closing_doctype.py
@@ -0,0 +1,25 @@
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ if frappe.db.table_exists("POS Closing Voucher"):
+ if not frappe.db.exists("DocType", "POS Closing Entry"):
+ frappe.rename_doc('DocType', 'POS Closing Voucher', 'POS Closing Entry', force=True)
+
+ if not frappe.db.exists('DocType', 'POS Closing Entry Taxes'):
+ frappe.rename_doc('DocType', 'POS Closing Voucher Taxes', 'POS Closing Entry Taxes', force=True)
+
+ if not frappe.db.exists('DocType', 'POS Closing Voucher Details'):
+ frappe.rename_doc('DocType', 'POS Closing Voucher Details', 'POS Closing Entry Details', force=True)
+
+ frappe.reload_doc('Accounts', 'doctype', 'POS Closing Entry')
+ frappe.reload_doc('Accounts', 'doctype', 'POS Closing Entry Taxes')
+ frappe.reload_doc('Accounts', 'doctype', 'POS Closing Entry Details')
+
+ if frappe.db.exists("DocType", "POS Closing Voucher"):
+ frappe.delete_doc("DocType", "POS Closing Voucher")
+ frappe.delete_doc("DocType", "POS Closing Voucher Taxes")
+ frappe.delete_doc("DocType", "POS Closing Voucher Details")
+ frappe.delete_doc("DocType", "POS Closing Voucher Invoices")
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/loyalty_points_entry_for_pos_invoice.py b/erpnext/patches/v13_0/loyalty_points_entry_for_pos_invoice.py
new file mode 100644
index 0000000..ee77340
--- /dev/null
+++ b/erpnext/patches/v13_0/loyalty_points_entry_for_pos_invoice.py
@@ -0,0 +1,20 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+
+def execute():
+ '''`sales_invoice` field from loyalty point entry is splitted into `invoice_type` & `invoice` fields'''
+
+ frappe.reload_doc("Accounts", "doctype", "loyalty_point_entry")
+
+ if not frappe.db.has_column('Loyalty Point Entry', 'sales_invoice'):
+ return
+
+ frappe.db.sql(
+ """UPDATE `tabLoyalty Point Entry` lpe
+ SET lpe.`invoice_type` = 'Sales Invoice', lpe.`invoice` = lpe.`sales_invoice`
+ WHERE lpe.`sales_invoice` IS NOT NULL
+ AND (lpe.`invoice` IS NULL OR lpe.`invoice` = '')""")
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/replace_pos_payment_mode_table.py b/erpnext/patches/v13_0/replace_pos_payment_mode_table.py
new file mode 100644
index 0000000..4a621b6
--- /dev/null
+++ b/erpnext/patches/v13_0/replace_pos_payment_mode_table.py
@@ -0,0 +1,29 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+
+def execute():
+ frappe.reload_doc("Selling", "doctype", "POS Payment Method")
+ pos_profiles = frappe.get_all("POS Profile")
+
+ for pos_profile in pos_profiles:
+ if not pos_profile.get("payments"): return
+
+ payments = frappe.db.sql("""
+ select idx, parentfield, parenttype, parent, mode_of_payment, `default` from `tabSales Invoice Payment` where parent=%s
+ """, pos_profile.name, as_dict=1)
+ if payments:
+ for payment_mode in payments:
+ pos_payment_method = frappe.new_doc("POS Payment Method")
+ pos_payment_method.idx = payment_mode.idx
+ pos_payment_method.default = payment_mode.default
+ pos_payment_method.mode_of_payment = payment_mode.mode_of_payment
+ pos_payment_method.parent = payment_mode.parent
+ pos_payment_method.parentfield = payment_mode.parentfield
+ pos_payment_method.parenttype = payment_mode.parenttype
+ pos_payment_method.db_insert()
+
+ frappe.db.sql("""delete from `tabSales Invoice Payment` where parent=%s""", pos_profile.name)
diff --git a/erpnext/patches/v8_7/set_offline_in_pos_settings.py b/erpnext/patches/v8_7/set_offline_in_pos_settings.py
deleted file mode 100644
index 7d2882e..0000000
--- a/erpnext/patches/v8_7/set_offline_in_pos_settings.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright (c) 2017, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc('accounts', 'doctype', 'pos_field')
- frappe.reload_doc('accounts', 'doctype', 'pos_settings')
-
- doc = frappe.get_doc('POS Settings')
- doc.use_pos_in_offline_mode = 1
- doc.save()
\ No newline at end of file
diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css
index 613a5ff..e80e3ed 100644
--- a/erpnext/public/css/pos.css
+++ b/erpnext/public/css/pos.css
@@ -1,179 +1,216 @@
-[data-route="point-of-sale"] .layout-main-section-wrapper {
- margin-bottom: 0;
-}
-[data-route="point-of-sale"] .pos-items-wrapper {
- max-height: calc(100vh - 210px);
-}
-.pos {
- padding: 15px;
-}
-.list-item {
- min-height: 40px;
- height: auto;
-}
-.cart-container {
- padding: 0 15px;
- display: inline-block;
- width: 39%;
- vertical-align: top;
-}
-.item-container {
- padding: 0 15px;
- display: inline-block;
- width: 60%;
- vertical-align: top;
-}
-.search-field {
- width: 60%;
-}
-.search-field input::placeholder {
- font-size: 12px;
-}
-.item-group-field {
- width: 40%;
- margin-left: 15px;
-}
-.cart-wrapper {
- margin-bottom: 12px;
-}
-.cart-wrapper .list-item__content:not(:first-child) {
- justify-content: flex-end;
-}
-.cart-wrapper .list-item--head .list-item__content:nth-child(2) {
- flex: 1.5;
-}
-.cart-items {
- height: 150px;
- overflow: auto;
-}
-.cart-items .list-item.current-item {
- background-color: #fffce7;
-}
-.cart-items .list-item.current-item.qty input {
- border: 1px solid #5E64FF;
- font-weight: bold;
-}
-.cart-items .list-item.current-item.disc .discount {
- font-weight: bold;
-}
-.cart-items .list-item.current-item.rate .rate {
- font-weight: bold;
-}
-.cart-items .list-item .quantity {
- flex: 1.5;
-}
-.cart-items input {
- text-align: right;
- height: 22px;
- font-size: 12px;
-}
-.fields {
- display: flex;
-}
-.pos-items-wrapper {
- max-height: 480px;
- overflow-y: auto;
-}
-.pos-items {
- overflow: hidden;
-}
-.pos-item-wrapper {
- display: flex;
- flex-direction: column;
- position: relative;
- width: 25%;
-}
-.image-view-container {
- display: block;
-}
-.image-view-container .image-field {
- height: auto;
-}
-.empty-state {
- height: 100%;
- position: relative;
-}
-.empty-state span {
- position: absolute;
- color: #8D99A6;
- font-size: 12px;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
-}
-@keyframes yellow-fade {
- 0% {
- background-color: #fffce7;
- }
- 100% {
- background-color: transparent;
- }
-}
-.highlight {
- animation: yellow-fade 1s ease-in 1;
-}
-input[type=number]::-webkit-inner-spin-button,
-input[type=number]::-webkit-outer-spin-button {
- -webkit-appearance: none;
- margin: 0;
-}
-.number-pad {
- border-collapse: collapse;
- cursor: pointer;
- display: table;
-}
-.num-row {
- display: table-row;
-}
-.num-col {
- display: table-cell;
- border: 1px solid #d1d8dd;
-}
-.num-col > div {
- width: 50px;
- height: 50px;
- text-align: center;
- line-height: 50px;
-}
-.num-col.active {
- background-color: #fffce7;
-}
-.num-col.brand-primary {
- background-color: #5E64FF;
- color: #ffffff;
-}
-.discount-amount .discount-inputs {
- display: flex;
- flex-direction: column;
- padding: 15px 0;
-}
-.discount-amount input:first-child {
- margin-bottom: 10px;
-}
-.taxes-and-totals {
- border-top: 1px solid #d1d8dd;
-}
-.taxes-and-totals .taxes {
- display: flex;
- flex-direction: column;
- padding: 15px 0;
- align-items: flex-end;
-}
-.taxes-and-totals .taxes > div:first-child {
- margin-bottom: 10px;
-}
-.grand-total {
- border-top: 1px solid #d1d8dd;
-}
-.grand-total .list-item {
- height: 60px;
-}
-.grand-total .grand-total-value {
- font-size: 18px;
-}
-.rounded-total-value {
- font-size: 18px;
-}
-.quantity-total {
- font-size: 18px;
-}
+[data-route="point-of-sale"] .layout-main-section { border: none; font-size: 12px; }
+[data-route="point-of-sale"] .layout-main-section-wrapper { margin-bottom: 0; }
+[data-route="point-of-sale"] .pos-items-wrapper { max-height: calc(100vh - 210px); }
+:root { --border-color: #d1d8dd; --text-color: #8d99a6; --primary: #5e64ff; }
+[data-route="point-of-sale"] .flex { display: flex; }
+[data-route="point-of-sale"] .grid { display: grid; }
+[data-route="point-of-sale"] .absolute { position: absolute; }
+[data-route="point-of-sale"] .relative { position: relative; }
+[data-route="point-of-sale"] .abs-center { top: 50%; left: 50%; transform: translate(-50%, -50%); }
+[data-route="point-of-sale"] .inline { display: inline; }
+[data-route="point-of-sale"] .float-right { float: right; }
+[data-route="point-of-sale"] .grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
+[data-route="point-of-sale"] .grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+[data-route="point-of-sale"] .grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+[data-route="point-of-sale"] .grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
+[data-route="point-of-sale"] .grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
+[data-route="point-of-sale"] .grid-cols-10 { grid-template-columns: repeat(10, minmax(0, 1fr)); }
+[data-route="point-of-sale"] .gap-2 { grid-gap: 0.5rem; gap: 0.5rem; }
+[data-route="point-of-sale"] .gap-4 { grid-gap: 1rem; gap: 1rem; }
+[data-route="point-of-sale"] .gap-6 { grid-gap: 1.25rem; gap: 1.25rem; }
+[data-route="point-of-sale"] .gap-8 { grid-gap: 1.5rem; gap: 1.5rem; }
+[data-route="point-of-sale"] .row-gap-2 { grid-row-gap: 0.5rem; row-gap: 0.5rem; }
+[data-route="point-of-sale"] .col-gap-4 { grid-column-gap: 1rem; column-gap: 1rem; }
+[data-route="point-of-sale"] .col-span-2 { grid-column: span 2 / span 2; }
+[data-route="point-of-sale"] .col-span-3 { grid-column: span 3 / span 3; }
+[data-route="point-of-sale"] .col-span-4 { grid-column: span 4 / span 4; }
+[data-route="point-of-sale"] .col-span-6 { grid-column: span 6 / span 6; }
+[data-route="point-of-sale"] .col-span-10 { grid-column: span 10 / span 10; }
+[data-route="point-of-sale"] .row-span-2 { grid-row: span 2 / span 2; }
+[data-route="point-of-sale"] .grid-auto-row { grid-auto-rows: 5.5rem; }
+[data-route="point-of-sale"] .d-none { display: none; }
+[data-route="point-of-sale"] .flex-wrap { flex-wrap: wrap; }
+[data-route="point-of-sale"] .flex-row { flex-direction: row; }
+[data-route="point-of-sale"] .flex-col { flex-direction: column; }
+[data-route="point-of-sale"] .flex-row-rev { flex-direction: row-reverse; }
+[data-route="point-of-sale"] .flex-col-rev { flex-direction: column-reverse; }
+[data-route="point-of-sale"] .flex-1 { flex: 1 1 0%; }
+[data-route="point-of-sale"] .items-center { align-items: center; }
+[data-route="point-of-sale"] .items-end { align-items: flex-end; }
+[data-route="point-of-sale"] .f-grow-1 { flex-grow: 1; }
+[data-route="point-of-sale"] .f-grow-2 { flex-grow: 2; }
+[data-route="point-of-sale"] .f-grow-3 { flex-grow: 3; }
+[data-route="point-of-sale"] .f-grow-4 { flex-grow: 4; }
+[data-route="point-of-sale"] .f-shrink-0 { flex-shrink: 0; }
+[data-route="point-of-sale"] .f-shrink-1 { flex-shrink: 1; }
+[data-route="point-of-sale"] .f-shrink-2 { flex-shrink: 2; }
+[data-route="point-of-sale"] .f-shrink-3 { flex-shrink: 3; }
+[data-route="point-of-sale"] .shadow { box-shadow: 0 0px 3px 0 rgba(0, 0, 0, 0.2), 0 1px 2px 0 rgba(0, 0, 0, 0.06); }
+[data-route="point-of-sale"] .shadow-sm { box-shadow: 0 0.5px 3px 0 rgba(0, 0, 0, 0.125); }
+[data-route="point-of-sale"] .shadow-inner { box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.1); }
+[data-route="point-of-sale"] .rounded { border-radius: 0.3rem; }
+[data-route="point-of-sale"] .rounded-b { border-bottom-left-radius: 0.3rem; border-bottom-right-radius: 0.3rem; }
+[data-route="point-of-sale"] .p-8 { padding: 2rem; }
+[data-route="point-of-sale"] .p-16 { padding: 4rem; }
+[data-route="point-of-sale"] .p-32 { padding: 8rem; }
+[data-route="point-of-sale"] .p-6 { padding: 1.5rem; }
+[data-route="point-of-sale"] .p-4 { padding: 1rem; }
+[data-route="point-of-sale"] .p-3 { padding: 0.75rem; }
+[data-route="point-of-sale"] .p-2 { padding: 0.5rem; }
+[data-route="point-of-sale"] .m-8 { margin: 2rem; }
+[data-route="point-of-sale"] .p-1 { padding: 0.25rem; }
+[data-route="point-of-sale"] .pr-0 { padding-right: 0rem; }
+[data-route="point-of-sale"] .pl-0 { padding-left: 0rem; }
+[data-route="point-of-sale"] .pt-0 { padding-top: 0rem; }
+[data-route="point-of-sale"] .pb-0 { padding-bottom: 0rem; }
+[data-route="point-of-sale"] .mr-0 { margin-right: 0rem; }
+[data-route="point-of-sale"] .ml-0 { margin-left: 0rem; }
+[data-route="point-of-sale"] .mt-0 { margin-top: 0rem; }
+[data-route="point-of-sale"] .mb-0 { margin-bottom: 0rem; }
+[data-route="point-of-sale"] .pr-2 { padding-right: 0.5rem; }
+[data-route="point-of-sale"] .pl-2 { padding-left: 0.5rem; }
+[data-route="point-of-sale"] .pt-2 { padding-top: 0.5rem; }
+[data-route="point-of-sale"] .pb-2 { padding-bottom: 0.5rem; }
+[data-route="point-of-sale"] .pr-3 { padding-right: 0.75rem; }
+[data-route="point-of-sale"] .pl-3 { padding-left: 0.75rem; }
+[data-route="point-of-sale"] .pt-3 { padding-top: 0.75rem; }
+[data-route="point-of-sale"] .pb-3 { padding-bottom: 0.75rem; }
+[data-route="point-of-sale"] .pr-4 { padding-right: 1rem; }
+[data-route="point-of-sale"] .pl-4 { padding-left: 1rem; }
+[data-route="point-of-sale"] .pt-4 { padding-top: 1rem; }
+[data-route="point-of-sale"] .pb-4 { padding-bottom: 1rem; }
+[data-route="point-of-sale"] .mr-4 { margin-right: 1rem; }
+[data-route="point-of-sale"] .ml-4 { margin-left: 1rem; }
+[data-route="point-of-sale"] .mt-4 { margin-top: 1rem; }
+[data-route="point-of-sale"] .mb-4 { margin-bottom: 1rem; }
+[data-route="point-of-sale"] .mr-2 { margin-right: 0.5rem; }
+[data-route="point-of-sale"] .ml-2 { margin-left: 0.5rem; }
+[data-route="point-of-sale"] .mt-2 { margin-top: 0.5rem; }
+[data-route="point-of-sale"] .mb-2 { margin-bottom: 0.5rem; }
+[data-route="point-of-sale"] .mr-1 { margin-right: 0.25rem; }
+[data-route="point-of-sale"] .ml-1 { margin-left: 0.25rem; }
+[data-route="point-of-sale"] .mt-1 { margin-top: 0.25rem; }
+[data-route="point-of-sale"] .mb-1 { margin-bottom: 0.25rem; }
+[data-route="point-of-sale"] .mr-auto { margin-right: auto; }
+[data-route="point-of-sale"] .ml-auto { margin-left: auto; }
+[data-route="point-of-sale"] .mt-auto { margin-top: auto; }
+[data-route="point-of-sale"] .mb-auto { margin-bottom: auto; }
+[data-route="point-of-sale"] .pr-6 { padding-right: 1.5rem; }
+[data-route="point-of-sale"] .pl-6 { padding-left: 1.5rem; }
+[data-route="point-of-sale"] .pt-6 { padding-top: 1.5rem; }
+[data-route="point-of-sale"] .pb-6 { padding-bottom: 1.5rem; }
+[data-route="point-of-sale"] .mr-6 { margin-right: 1.5rem; }
+[data-route="point-of-sale"] .ml-6 { margin-left: 1.5rem; }
+[data-route="point-of-sale"] .mt-6 { margin-top: 1.5rem; }
+[data-route="point-of-sale"] .mb-6 { margin-bottom: 1.5rem; }
+[data-route="point-of-sale"] .mr-8 { margin-right: 2rem; }
+[data-route="point-of-sale"] .ml-8 { margin-left: 2rem; }
+[data-route="point-of-sale"] .mt-8 { margin-top: 2rem; }
+[data-route="point-of-sale"] .mb-8 { margin-bottom: 2rem; }
+[data-route="point-of-sale"] .pr-8 { padding-right: 2rem; }
+[data-route="point-of-sale"] .pl-8 { padding-left: 2rem; }
+[data-route="point-of-sale"] .pt-8 { padding-top: 2rem; }
+[data-route="point-of-sale"] .pb-8 { padding-bottom: 2rem; }
+[data-route="point-of-sale"] .pr-16 { padding-right: 4rem; }
+[data-route="point-of-sale"] .pl-16 { padding-left: 4rem; }
+[data-route="point-of-sale"] .pt-16 { padding-top: 4rem; }
+[data-route="point-of-sale"] .pb-16 { padding-bottom: 4rem; }
+[data-route="point-of-sale"] .w-full { width: 100%; }
+[data-route="point-of-sale"] .h-full { height: 100%; }
+[data-route="point-of-sale"] .w-quarter { width: 25%; }
+[data-route="point-of-sale"] .w-half { width: 50%; }
+[data-route="point-of-sale"] .w-66 { width: 66.66%; }
+[data-route="point-of-sale"] .w-33 { width: 33.33%; }
+[data-route="point-of-sale"] .w-60 { width: 60%; }
+[data-route="point-of-sale"] .w-40 { width: 40%; }
+[data-route="point-of-sale"] .w-fit { width: fit-content; }
+[data-route="point-of-sale"] .w-6 { width: 2rem; }
+[data-route="point-of-sale"] .h-6 { min-height: 2rem; height: 2rem; }
+[data-route="point-of-sale"] .w-8 { width: 2.5rem; }
+[data-route="point-of-sale"] .h-8 { min-height: 2.5rem; height: 2.5rem; }
+[data-route="point-of-sale"] .w-10 { width: 3rem; }
+[data-route="point-of-sale"] .h-10 { min-height:3rem; height: 3rem; }
+[data-route="point-of-sale"] .h-12 { min-height: 3.3rem; height: 3.3rem; }
+[data-route="point-of-sale"] .w-12 { width: 3.3rem; }
+[data-route="point-of-sale"] .h-14 { min-height: 4.2rem; height: 4.2rem; }
+[data-route="point-of-sale"] .h-16 { min-height: 4.6rem; height: 4.6rem; }
+[data-route="point-of-sale"] .h-18 { min-height: 5rem; height: 5rem; }
+[data-route="point-of-sale"] .w-18 { width: 5.4rem; }
+[data-route="point-of-sale"] .w-24 { width: 7.2rem; }
+[data-route="point-of-sale"] .w-26 { width: 8.4rem; }
+[data-route="point-of-sale"] .h-24 { min-height: 7.2rem; height: 7.2rem; }
+[data-route="point-of-sale"] .h-32 { min-height: 9.6rem; height: 9.6rem; }
+[data-route="point-of-sale"] .w-46 { width: 15rem; }
+[data-route="point-of-sale"] .h-46 { min-height:15rem; height: 15rem; }
+[data-route="point-of-sale"] .h-100 { height: 100vh; }
+[data-route="point-of-sale"] .mx-h-70 { max-height: 67rem; }
+[data-route="point-of-sale"] .border-grey-300 { border-color: #e2e8f0; }
+[data-route="point-of-sale"] .border-grey { border: 1px solid #d1d8dd; }
+[data-route="point-of-sale"] .border-white { border: 1px solid #fff; }
+[data-route="point-of-sale"] .border-b-grey { border-bottom: 1px solid #d1d8dd; }
+[data-route="point-of-sale"] .border-t-grey { border-top: 1px solid #d1d8dd; }
+[data-route="point-of-sale"] .border-r-grey { border-right: 1px solid #d1d8dd; }
+[data-route="point-of-sale"] .text-dark-grey { color: #5f5f5f; }
+[data-route="point-of-sale"] .text-grey { color: #8d99a6; }
+[data-route="point-of-sale"] .text-grey-100 { color: #d1d8dd; }
+[data-route="point-of-sale"] .text-grey-200 { color: #a0aec0; }
+[data-route="point-of-sale"] .bg-green-200 { background-color: #c6f6d5; }
+[data-route="point-of-sale"] .text-bold { font-weight: bold; }
+[data-route="point-of-sale"] .italic { font-style: italic; }
+[data-route="point-of-sale"] .font-weight-450 { font-weight: 450; }
+[data-route="point-of-sale"] .justify-around { justify-content: space-around; }
+[data-route="point-of-sale"] .justify-between { justify-content: space-between; }
+[data-route="point-of-sale"] .justify-center { justify-content: center; }
+[data-route="point-of-sale"] .justify-end { justify-content: flex-end; }
+[data-route="point-of-sale"] .bg-white { background-color: white; }
+[data-route="point-of-sale"] .bg-light-grey { background-color: #f0f4f7; }
+[data-route="point-of-sale"] .bg-grey-100 { background-color: #f7fafc; }
+[data-route="point-of-sale"] .bg-grey-200 { background-color: #edf2f7; }
+[data-route="point-of-sale"] .bg-grey { background-color: #f4f5f6; }
+[data-route="point-of-sale"] .text-center { text-align: center; }
+[data-route="point-of-sale"] .text-right { text-align: right; }
+[data-route="point-of-sale"] .text-sm { font-size: 1rem; }
+[data-route="point-of-sale"] .text-md-0 { font-size: 1.25rem; }
+[data-route="point-of-sale"] .text-md { font-size: 1.4rem; }
+[data-route="point-of-sale"] .text-lg { font-size: 1.6rem; }
+[data-route="point-of-sale"] .text-xl { font-size: 2.2rem; }
+[data-route="point-of-sale"] .text-2xl { font-size: 2.8rem; }
+[data-route="point-of-sale"] .text-2-5xl { font-size: 3rem; }
+[data-route="point-of-sale"] .text-3xl { font-size: 3.8rem; }
+[data-route="point-of-sale"] .text-6xl { font-size: 4.8rem; }
+[data-route="point-of-sale"] .line-through { text-decoration: line-through; }
+[data-route="point-of-sale"] .text-primary { color: #5e64ff; }
+[data-route="point-of-sale"] .text-white { color: #fff; }
+[data-route="point-of-sale"] .text-green-500 { color: #48bb78; }
+[data-route="point-of-sale"] .bg-primary { background-color: #5e64ff; }
+[data-route="point-of-sale"] .border-primary { border-color: #5e64ff; }
+[data-route="point-of-sale"] .text-danger { color: #e53e3e; }
+[data-route="point-of-sale"] .scroll-x { overflow-x: scroll;overflow-y: hidden; }
+[data-route="point-of-sale"] .scroll-y { overflow-y: scroll;overflow-x: hidden; }
+[data-route="point-of-sale"] .overflow-hidden { overflow: hidden; }
+[data-route="point-of-sale"] .whitespace-nowrap { white-space: nowrap; }
+[data-route="point-of-sale"] .sticky { position: sticky; top: -1px; }
+[data-route="point-of-sale"] .bg-white { background-color: #fff; }
+[data-route="point-of-sale"] .bg-selected { background-color: #fffdf4; }
+[data-route="point-of-sale"] .border-dashed { border-width:1px; border-style: dashed; }
+[data-route="point-of-sale"] .z-100 { z-index: 100; }
+
+[data-route="point-of-sale"] .frappe-control { margin: 0 !important; width: 100%; }
+[data-route="point-of-sale"] .form-control { font-size: 12px; }
+[data-route="point-of-sale"] .form-group { margin: 0 !important; }
+[data-route="point-of-sale"] .pointer { cursor: pointer; }
+[data-route="point-of-sale"] .no-select { user-select: none; }
+[data-route="point-of-sale"] .item-wrapper:hover { transform: scale(1.02, 1.02); }
+[data-route="point-of-sale"] .hover-underline:hover { text-decoration: underline; }
+[data-route="point-of-sale"] .item-wrapper { transition: scale 0.2s ease-in-out; }
+[data-route="point-of-sale"] .cart-items-section .cart-item-wrapper:not(:first-child) { border-top: none; }
+[data-route="point-of-sale"] .customer-transactions .invoice-wrapper:not(:first-child) { border-top: none; }
+
+[data-route="point-of-sale"] .payment-summary-wrapper:last-child { border-bottom: none; }
+[data-route="point-of-sale"] .item-summary-wrapper:last-child { border-bottom: none; }
+[data-route="point-of-sale"] .total-summary-wrapper:last-child { border-bottom: none; }
+[data-route="point-of-sale"] .invoices-container .invoice-wrapper:last-child { border-bottom: none; }
+[data-route="point-of-sale"] .summary-btns:last-child { margin-right: 0px; }
+[data-route="point-of-sale"] ::-webkit-scrollbar { width: 1px }
+
+[data-route="point-of-sale"] .indicator.grey::before { background-color: #8d99a6; }
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index b72ceb2..405a33c 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -34,12 +34,12 @@
this.calculate_discount_amount();
// Advance calculation applicable to Sales /Purchase Invoice
- if(in_list(["Sales Invoice", "Purchase Invoice"], this.frm.doc.doctype)
+ if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)
&& this.frm.doc.docstatus < 2 && !this.frm.doc.is_return) {
this.calculate_total_advance(update_paid_amount);
}
- if (this.frm.doc.doctype == "Sales Invoice" && this.frm.doc.is_pos &&
+ if (in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype) && this.frm.doc.is_pos &&
this.frm.doc.is_return) {
this.update_paid_amount_for_return();
}
@@ -425,7 +425,7 @@
? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.rounding_adjustment)
: this.frm.doc.net_total);
- if(in_list(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"], this.frm.doc.doctype)) {
+ if(in_list(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"], this.frm.doc.doctype)) {
this.frm.doc.base_grand_total = (this.frm.doc.total_taxes_and_charges) ?
flt(this.frm.doc.grand_total * this.frm.doc.conversion_rate) : this.frm.doc.base_net_total;
} else {
@@ -604,7 +604,7 @@
// NOTE:
// paid_amount and write_off_amount is only for POS/Loyalty Point Redemption Invoice
// total_advance is only for non POS Invoice
- if(this.frm.doc.doctype == "Sales Invoice" && this.frm.doc.is_return){
+ if(in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype) && this.frm.doc.is_return){
this.calculate_paid_amount();
}
@@ -612,7 +612,7 @@
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "total_advance", "write_off_amount"]);
- if(in_list(["Sales Invoice", "Purchase Invoice"], this.frm.doc.doctype)) {
+ if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)) {
var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
if(this.frm.doc.party_account_currency == this.frm.doc.currency) {
@@ -634,7 +634,7 @@
this.frm.refresh_field("base_paid_amount");
}
- if(this.frm.doc.doctype == "Sales Invoice") {
+ if(in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype)) {
let total_amount_for_payment = (this.frm.doc.redeem_loyalty_points && this.frm.doc.loyalty_amount)
? flt(total_amount_to_pay - this.frm.doc.loyalty_amount, precision("base_grand_total"))
: total_amount_to_pay;
@@ -691,11 +691,13 @@
if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) {
$.each(this.frm.doc['payments'] || [], function(index, data) {
if(data.default && payment_status && total_amount_to_pay > 0) {
- data.base_amount = flt(total_amount_to_pay, precision("base_amount"));
- data.amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount"));
+ let base_amount = flt(total_amount_to_pay, precision("base_amount", data));
+ frappe.model.set_value(data.doctype, data.name, "base_amount", base_amount);
+ let amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount", data));
+ frappe.model.set_value(data.doctype, data.name, "amount", amount);
payment_status = false;
} else if(me.frm.doc.paid_amount) {
- data.amount = 0.0;
+ frappe.model.set_value(data.doctype, data.name, "amount", 0.0);
}
});
}
@@ -707,7 +709,7 @@
var base_paid_amount = 0.0;
if(this.frm.doc.is_pos) {
$.each(this.frm.doc['payments'] || [], function(index, data){
- data.base_amount = flt(data.amount * me.frm.doc.conversion_rate, precision("base_amount"));
+ data.base_amount = flt(data.amount * me.frm.doc.conversion_rate, precision("base_amount", data));
paid_amount += data.amount;
base_paid_amount += data.base_amount;
});
@@ -719,14 +721,14 @@
paid_amount += flt(this.frm.doc.loyalty_amount / me.frm.doc.conversion_rate, precision("paid_amount"));
}
- this.frm.doc.paid_amount = flt(paid_amount, precision("paid_amount"));
- this.frm.doc.base_paid_amount = flt(base_paid_amount, precision("base_paid_amount"));
+ this.frm.set_value('paid_amount', flt(paid_amount, precision("paid_amount")));
+ this.frm.set_value('base_paid_amount', flt(base_paid_amount, precision("base_paid_amount")));
},
calculate_change_amount: function(){
this.frm.doc.change_amount = 0.0;
this.frm.doc.base_change_amount = 0.0;
- if(this.frm.doc.doctype == "Sales Invoice"
+ if(in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype)
&& this.frm.doc.paid_amount > this.frm.doc.grand_total && !this.frm.doc.is_return) {
var payment_types = $.map(this.frm.doc.payments, function(d) { return d.type; });
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 3c56a63..4e50f3d 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -651,7 +651,7 @@
let child = frappe.model.add_child(me.frm.doc, "taxes");
child.charge_type = "On Net Total";
child.account_head = tax;
- child.rate = 0;
+ child.rate = rate;
}
});
}
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index d75633e..42f9cabc 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -43,6 +43,7 @@
label: __(me.warehouse_details.type),
default: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '',
onchange: function(e) {
+ me.warehouse_details.name = this.get_value();
if(me.has_batch && !me.has_serial_no) {
fields = fields.concat(me.get_batch_fields());
@@ -50,7 +51,6 @@
fields = fields.concat(me.get_serial_no_fields());
}
- me.warehouse_details.name = this.get_value();
var batches = this.layout.fields_dict.batches;
if(batches) {
batches.grid.df.data = [];
@@ -98,8 +98,13 @@
numbers.then((data) => {
let auto_fetched_serial_numbers = data.message;
let records_length = auto_fetched_serial_numbers.length;
+ if (!records_length) {
+ const warehouse = me.dialog.fields_dict.warehouse.get_value().bold();
+ frappe.msgprint(__(`Serial numbers unavailable for Item ${me.item.item_code.bold()}
+ under warehouse ${warehouse}. Please try changing warehouse.`));
+ }
if (records_length < qty) {
- frappe.msgprint(`Fetched only ${records_length} serial numbers.`);
+ frappe.msgprint(__(`Fetched only ${records_length} available serial numbers.`));
}
let serial_no_list_field = this.dialog.fields_dict.serial_no;
numbers = auto_fetched_serial_numbers.join('\n');
@@ -445,6 +450,28 @@
serial_no_filters['warehouse'] = me.warehouse_details.name;
}
+ if (me.frm.doc.doctype === 'POS Invoice' && !this.showing_reserved_serial_nos_error) {
+ frappe.call({
+ method: "erpnext.stock.doctype.serial_no.serial_no.get_pos_reserved_serial_nos",
+ args: {
+ item_code: me.item_code,
+ warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : ''
+ }
+ }).then((data) => {
+ if (!data.message[1].length) {
+ this.showing_reserved_serial_nos_error = true;
+ const warehouse = me.dialog.fields_dict.warehouse.get_value().bold();
+ const d = frappe.msgprint(__(`Serial numbers unavailable for Item ${me.item.item_code.bold()}
+ under warehouse ${warehouse}. Please try changing warehouse.`));
+ d.get_close_btn().on('click', () => {
+ this.showing_reserved_serial_nos_error = false;
+ d.hide();
+ });
+ }
+ serial_no_filters['name'] = ["not in", data.message[0]]
+ })
+ }
+
return [
{fieldtype: 'Section Break', label: __('Serial Numbers')},
{
diff --git a/erpnext/selling/doctype/pos_closing_voucher/pos_closing_voucher.js b/erpnext/selling/doctype/pos_closing_voucher/pos_closing_voucher.js
deleted file mode 100644
index f24caf7..0000000
--- a/erpnext/selling/doctype/pos_closing_voucher/pos_closing_voucher.js
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('POS Closing Voucher', {
- onload: function(frm) {
- frm.set_query("pos_profile", function(doc) {
- return {
- filters: {
- 'user': doc.user
- }
- };
- });
-
- frm.set_query("user", function(doc) {
- return {
- query: "erpnext.selling.doctype.pos_closing_voucher.pos_closing_voucher.get_cashiers",
- filters: {
- 'parent': doc.pos_profile
- }
- };
- });
- },
-
- total_amount: function(frm) {
- get_difference_amount(frm);
- },
- custody_amount: function(frm){
- get_difference_amount(frm);
- },
- expense_amount: function(frm){
- get_difference_amount(frm);
- },
- refresh: function(frm) {
- get_closing_voucher_details(frm);
- },
- period_start_date: function(frm) {
- get_closing_voucher_details(frm);
- },
- period_end_date: function(frm) {
- get_closing_voucher_details(frm);
- },
- company: function(frm) {
- get_closing_voucher_details(frm);
- },
- pos_profile: function(frm) {
- get_closing_voucher_details(frm);
- },
- user: function(frm) {
- get_closing_voucher_details(frm);
- },
-});
-
-frappe.ui.form.on('POS Closing Voucher Details', {
- collected_amount: function(doc, cdt, cdn) {
- var row = locals[cdt][cdn];
- frappe.model.set_value(cdt, cdn, "difference", row.collected_amount - row.expected_amount);
- }
-});
-
-var get_difference_amount = function(frm){
- frm.doc.difference = frm.doc.total_amount - frm.doc.custody_amount - frm.doc.expense_amount;
- refresh_field("difference");
-};
-
-var get_closing_voucher_details = function(frm) {
- if (frm.doc.period_end_date && frm.doc.period_start_date && frm.doc.company && frm.doc.pos_profile && frm.doc.user) {
- frappe.call({
- method: "get_closing_voucher_details",
- doc: frm.doc,
- callback: function(r) {
- if (r.message) {
- refresh_field("payment_reconciliation");
- refresh_field("sales_invoices_summary");
- refresh_field("taxes");
-
- refresh_field("grand_total");
- refresh_field("net_total");
- refresh_field("total_quantity");
- refresh_field("total_amount");
-
- frm.get_field("payment_reconciliation_details").$wrapper.html(r.message);
- }
- }
- });
- }
-
-};
diff --git a/erpnext/selling/doctype/pos_closing_voucher/pos_closing_voucher.json b/erpnext/selling/doctype/pos_closing_voucher/pos_closing_voucher.json
deleted file mode 100644
index 2ac5779..0000000
--- a/erpnext/selling/doctype/pos_closing_voucher/pos_closing_voucher.json
+++ /dev/null
@@ -1,1016 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "POS-CLO-.YYYY.-.#####",
- "beta": 0,
- "creation": "2018-05-28 19:06:40.830043",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Today",
- "fieldname": "period_start_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Period Start Date",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Today",
- "fieldname": "period_end_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Period End Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Today",
- "fieldname": "posting_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Posting Date",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_5",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "company",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Company",
- "length": 0,
- "no_copy": 0,
- "options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_7",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "pos_profile",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "POS Profile",
- "length": 0,
- "no_copy": 0,
- "options": "POS Profile",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "user",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Cashier",
- "length": 0,
- "no_copy": 0,
- "options": "User",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "expense_details_section",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Expense Details",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "expense_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Expense Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "custody_amount",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Amount in Custody",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_13",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "total_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Total Collected Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "difference",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Difference",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_9",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "payment_reconciliation_details",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_11",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Modes of Payment",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "payment_reconciliation",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Payment Reconciliation",
- "length": 0,
- "no_copy": 0,
- "options": "POS Closing Voucher Details",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 1,
- "columns": 0,
- "fieldname": "section_break_13",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Details",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "grand_total",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Grand Total",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "net_total",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Net Total",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "total_quantity",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Total Quantity",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_16",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Taxes",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "taxes",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Taxes",
- "length": 0,
- "no_copy": 0,
- "options": "POS Closing Voucher Taxes",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 1,
- "columns": 0,
- "fieldname": "section_break_12",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Linked Invoices",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "sales_invoices_summary",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Sales Invoices Summary",
- "length": 0,
- "no_copy": 0,
- "options": "POS Closing Voucher Invoices",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_14",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "amended_from",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Amended From",
- "length": 0,
- "no_copy": 1,
- "options": "POS Closing Voucher",
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-01-28 12:33:45.217813",
- "modified_by": "Administrator",
- "module": "Selling",
- "name": "POS Closing Voucher",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Sales Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/pos_closing_voucher/pos_closing_voucher.py b/erpnext/selling/doctype/pos_closing_voucher/pos_closing_voucher.py
deleted file mode 100644
index bb5f83e..0000000
--- a/erpnext/selling/doctype/pos_closing_voucher/pos_closing_voucher.py
+++ /dev/null
@@ -1,188 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe import _
-from frappe.model.document import Document
-from collections import defaultdict
-from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
-import json
-
-class POSClosingVoucher(Document):
- def get_closing_voucher_details(self):
- filters = {
- 'doc': self.name,
- 'from_date': self.period_start_date,
- 'to_date': self.period_end_date,
- 'company': self.company,
- 'pos_profile': self.pos_profile,
- 'user': self.user,
- 'is_pos': 1
- }
-
- invoice_list = get_invoices(filters)
- self.set_invoice_list(invoice_list)
-
- sales_summary = get_sales_summary(invoice_list)
- self.set_sales_summary_values(sales_summary)
- self.total_amount = sales_summary['grand_total']
-
- if not self.get('payment_reconciliation'):
- mop = get_mode_of_payment_details(invoice_list)
- self.set_mode_of_payments(mop)
-
- taxes = get_tax_details(invoice_list)
- self.set_taxes(taxes)
-
- return self.get_payment_reconciliation_details()
-
- def validate(self):
- user = frappe.get_all('POS Closing Voucher',
- filters = {
- 'user': self.user,
- 'docstatus': 1
- },
- or_filters = {
- 'period_start_date': ('between', [self.period_start_date, self.period_end_date]),
- 'period_end_date': ('between', [self.period_start_date, self.period_end_date])
- })
-
- if user:
- frappe.throw(_("POS Closing Voucher alreday exists for {0} between date {1} and {2}")
- .format(self.user, self.period_start_date, self.period_end_date))
-
- def set_invoice_list(self, invoice_list):
- self.sales_invoices_summary = []
- for invoice in invoice_list:
- self.append('sales_invoices_summary', {
- 'invoice': invoice['name'],
- 'qty_of_items': invoice['pos_total_qty'],
- 'grand_total': invoice['grand_total']
- })
-
- def set_sales_summary_values(self, sales_summary):
- self.grand_total = sales_summary['grand_total']
- self.net_total = sales_summary['net_total']
- self.total_quantity = sales_summary['total_qty']
-
- def set_mode_of_payments(self, mop):
- self.payment_reconciliation = []
- for m in mop:
- self.append('payment_reconciliation', {
- 'mode_of_payment': m['name'],
- 'expected_amount': m['amount']
- })
-
- def set_taxes(self, taxes):
- self.taxes = []
- for tax in taxes:
- self.append('taxes', {
- 'rate': tax['rate'],
- 'amount': tax['amount']
- })
-
- def get_payment_reconciliation_details(self):
- currency = get_company_currency(self)
- return frappe.render_template("erpnext/selling/doctype/pos_closing_voucher/closing_voucher_details.html",
- {"data": self, "currency": currency})
-
-@frappe.whitelist()
-def get_cashiers(doctype, txt, searchfield, start, page_len, filters):
- cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=['user'])
- cashiers = [cashier for cashier in set(c['user'] for c in cashiers_list)]
- return [[c] for c in cashiers]
-
-def get_mode_of_payment_details(invoice_list):
- mode_of_payment_details = []
- invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list])
- if invoice_list:
- inv_mop_detail = frappe.db.sql("""select a.owner, a.posting_date,
- ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_amount) as paid_amount
- from `tabSales Invoice` a, `tabSales Invoice Payment` b
- where a.name = b.parent
- and a.name in ({invoice_list_names})
- group by a.owner, a.posting_date, mode_of_payment
- union
- select a.owner,a.posting_date,
- ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_paid_amount) as paid_amount
- from `tabSales Invoice` a, `tabPayment Entry` b,`tabPayment Entry Reference` c
- where a.name = c.reference_name
- and b.name = c.parent
- and a.name in ({invoice_list_names})
- group by a.owner, a.posting_date, mode_of_payment
- union
- select a.owner, a.posting_date,
- ifnull(a.voucher_type,'') as mode_of_payment, sum(b.credit)
- from `tabJournal Entry` a, `tabJournal Entry Account` b
- where a.name = b.parent
- and a.docstatus = 1
- and b.reference_type = "Sales Invoice"
- and b.reference_name in ({invoice_list_names})
- group by a.owner, a.posting_date, mode_of_payment
- """.format(invoice_list_names=invoice_list_names), as_dict=1)
-
- inv_change_amount = frappe.db.sql("""select a.owner, a.posting_date,
- ifnull(b.mode_of_payment, '') as mode_of_payment, sum(a.base_change_amount) as change_amount
- from `tabSales Invoice` a, `tabSales Invoice Payment` b
- where a.name = b.parent
- and a.name in ({invoice_list_names})
- and b.mode_of_payment = 'Cash'
- and a.base_change_amount > 0
- group by a.owner, a.posting_date, mode_of_payment""".format(invoice_list_names=invoice_list_names), as_dict=1)
-
- for d in inv_change_amount:
- for det in inv_mop_detail:
- if det["owner"] == d["owner"] and det["posting_date"] == d["posting_date"] and det["mode_of_payment"] == d["mode_of_payment"]:
- paid_amount = det["paid_amount"] - d["change_amount"]
- det["paid_amount"] = paid_amount
-
- payment_details = defaultdict(int)
- for d in inv_mop_detail:
- payment_details[d.mode_of_payment] += d.paid_amount
-
- for m in payment_details:
- mode_of_payment_details.append({'name': m, 'amount': payment_details[m]})
-
- return mode_of_payment_details
-
-def get_tax_details(invoice_list):
- tax_breakup = []
- tax_details = defaultdict(int)
- for invoice in invoice_list:
- doc = frappe.get_doc("Sales Invoice", invoice.name)
- itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(doc)
-
- if itemised_tax:
- for a in itemised_tax:
- for b in itemised_tax[a]:
- for c in itemised_tax[a][b]:
- if c == 'tax_rate':
- tax_details[itemised_tax[a][b][c]] += itemised_tax[a][b]['tax_amount']
-
- for t in tax_details:
- tax_breakup.append({'rate': t, 'amount': tax_details[t]})
-
- return tax_breakup
-
-def get_sales_summary(invoice_list):
- net_total = sum(item['net_total'] for item in invoice_list)
- grand_total = sum(item['grand_total'] for item in invoice_list)
- total_qty = sum(item['pos_total_qty'] for item in invoice_list)
-
- return {'net_total': net_total, 'grand_total': grand_total, 'total_qty': total_qty}
-
-def get_company_currency(doc):
- currency = frappe.get_cached_value('Company', doc.company, "default_currency")
- return frappe.get_doc('Currency', currency)
-
-def get_invoices(filters):
- return frappe.db.sql("""select a.name, a.base_grand_total as grand_total,
- a.base_net_total as net_total, a.pos_total_qty
- from `tabSales Invoice` a
- where a.docstatus = 1 and a.posting_date >= %(from_date)s
- and a.posting_date <= %(to_date)s and a.company=%(company)s
- and a.pos_profile = %(pos_profile)s and a.is_pos = %(is_pos)s
- and a.owner = %(user)s""",
- filters, as_dict=1)
diff --git a/erpnext/selling/doctype/pos_closing_voucher/test_pos_closing_voucher.py b/erpnext/selling/doctype/pos_closing_voucher/test_pos_closing_voucher.py
deleted file mode 100644
index 8899aaf..0000000
--- a/erpnext/selling/doctype/pos_closing_voucher/test_pos_closing_voucher.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-from __future__ import unicode_literals
-import frappe
-import unittest
-from frappe.utils import nowdate
-from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
-from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
-
-class TestPOSClosingVoucher(unittest.TestCase):
- def test_pos_closing_voucher(self):
- old_user = frappe.session.user
- user = 'test@example.com'
- test_user = frappe.get_doc('User', user)
-
- roles = ("Accounts Manager", "Accounts User", "Sales Manager")
- test_user.add_roles(*roles)
- frappe.set_user(user)
-
- pos_profile = make_pos_profile()
- pos_profile.append('applicable_for_users', {
- 'default': 1,
- 'user': user
- })
-
- pos_profile.save()
-
- si1 = create_sales_invoice(is_pos=1, rate=3500, do_not_submit=1)
- si1.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3500
- })
- si1.submit()
-
- si2 = create_sales_invoice(is_pos=1, rate=3200, do_not_submit=1)
- si2.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
- })
- si2.submit()
-
- pcv_doc = create_pos_closing_voucher(user=user,
- pos_profile=pos_profile.name, collected_amount=6700)
-
- pcv_doc.get_closing_voucher_details()
-
- self.assertEqual(pcv_doc.total_quantity, 2)
- self.assertEqual(pcv_doc.net_total, 6700)
-
- payment = pcv_doc.payment_reconciliation[0]
- self.assertEqual(payment.mode_of_payment, 'Cash')
-
- si1.load_from_db()
- si1.cancel()
-
- si2.load_from_db()
- si2.cancel()
-
- test_user.load_from_db()
- test_user.remove_roles(*roles)
-
- frappe.set_user(old_user)
- frappe.db.sql("delete from `tabPOS Profile`")
-
-def create_pos_closing_voucher(**args):
- args = frappe._dict(args)
-
- doc = frappe.get_doc({
- 'doctype': 'POS Closing Voucher',
- 'period_start_date': args.period_start_date or nowdate(),
- 'period_end_date': args.period_end_date or nowdate(),
- 'posting_date': args.posting_date or nowdate(),
- 'company': args.company or "_Test Company",
- 'pos_profile': args.pos_profile,
- 'user': args.user or "Administrator",
- })
-
- doc.get_closing_voucher_details()
- if doc.get('payment_reconciliation'):
- doc.payment_reconciliation[0].collected_amount = (args.collected_amount or
- doc.payment_reconciliation[0].expected_amount)
-
- doc.save()
- return doc
\ No newline at end of file
diff --git a/erpnext/selling/doctype/pos_closing_voucher_details/__init__.py b/erpnext/selling/doctype/pos_closing_voucher_details/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/selling/doctype/pos_closing_voucher_details/__init__.py
+++ /dev/null
diff --git a/erpnext/selling/doctype/pos_closing_voucher_details/pos_closing_voucher_details.json b/erpnext/selling/doctype/pos_closing_voucher_details/pos_closing_voucher_details.json
deleted file mode 100644
index a526884..0000000
--- a/erpnext/selling/doctype/pos_closing_voucher_details/pos_closing_voucher_details.json
+++ /dev/null
@@ -1,172 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2018-05-28 19:10:47.580174",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mode_of_payment",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Mode of Payment",
- "length": 0,
- "no_copy": 0,
- "options": "Mode of Payment",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "0.0",
- "fieldname": "collected_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Collected Amount",
- "length": 0,
- "no_copy": 0,
- "options": "currency",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "expected_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Expected Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "difference",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Difference",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-05-29 17:47:16.311557",
- "modified_by": "Administrator",
- "module": "Selling",
- "name": "POS Closing Voucher Details",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/pos_closing_voucher_details/pos_closing_voucher_details.py b/erpnext/selling/doctype/pos_closing_voucher_details/pos_closing_voucher_details.py
deleted file mode 100644
index 6bc323f..0000000
--- a/erpnext/selling/doctype/pos_closing_voucher_details/pos_closing_voucher_details.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-from frappe.model.document import Document
-
-class POSClosingVoucherDetails(Document):
- pass
diff --git a/erpnext/selling/doctype/pos_closing_voucher_invoices/__init__.py b/erpnext/selling/doctype/pos_closing_voucher_invoices/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/selling/doctype/pos_closing_voucher_invoices/__init__.py
+++ /dev/null
diff --git a/erpnext/selling/doctype/pos_closing_voucher_invoices/pos_closing_voucher_invoices.json b/erpnext/selling/doctype/pos_closing_voucher_invoices/pos_closing_voucher_invoices.json
deleted file mode 100644
index 7304550..0000000
--- a/erpnext/selling/doctype/pos_closing_voucher_invoices/pos_closing_voucher_invoices.json
+++ /dev/null
@@ -1,138 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2018-05-29 14:50:08.687453",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "invoice",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Invoices",
- "length": 0,
- "no_copy": 0,
- "options": "Sales Invoice",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "qty_of_items",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Quantity of Items",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "grand_total",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Grand Total",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-05-29 17:46:46.539993",
- "modified_by": "Administrator",
- "module": "Selling",
- "name": "POS Closing Voucher Invoices",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/pos_closing_voucher_invoices/pos_closing_voucher_invoices.py b/erpnext/selling/doctype/pos_closing_voucher_invoices/pos_closing_voucher_invoices.py
deleted file mode 100644
index a2d488b..0000000
--- a/erpnext/selling/doctype/pos_closing_voucher_invoices/pos_closing_voucher_invoices.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-from frappe.model.document import Document
-
-class POSClosingVoucherInvoices(Document):
- pass
diff --git a/erpnext/selling/doctype/pos_closing_voucher_taxes/__init__.py b/erpnext/selling/doctype/pos_closing_voucher_taxes/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/selling/doctype/pos_closing_voucher_taxes/__init__.py
+++ /dev/null
diff --git a/erpnext/selling/doctype/pos_closing_voucher_taxes/pos_closing_voucher_taxes.json b/erpnext/selling/doctype/pos_closing_voucher_taxes/pos_closing_voucher_taxes.json
deleted file mode 100644
index 3089e06..0000000
--- a/erpnext/selling/doctype/pos_closing_voucher_taxes/pos_closing_voucher_taxes.json
+++ /dev/null
@@ -1,106 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2018-05-30 09:11:22.535470",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "rate",
- "fieldtype": "Percent",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Rate",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-05-30 09:11:22.535470",
- "modified_by": "Administrator",
- "module": "Selling",
- "name": "POS Closing Voucher Taxes",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/onscan.js b/erpnext/selling/page/point_of_sale/onscan.js
new file mode 100644
index 0000000..428dc75
--- /dev/null
+++ b/erpnext/selling/page/point_of_sale/onscan.js
@@ -0,0 +1 @@
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t()):e.onScan=t()}(this,function(){var d={attachTo:function(e,t){if(void 0!==e.scannerDetectionData)throw new Error("onScan.js is already initialized for DOM element "+e);var n={onScan:function(e,t){},onScanError:function(e){},onKeyProcess:function(e,t){},onKeyDetect:function(e,t){},onPaste:function(e,t){},keyCodeMapper:function(e){return d.decodeKeyEvent(e)},onScanButtonLongPress:function(){},scanButtonKeyCode:!1,scanButtonLongPressTime:500,timeBeforeScanTest:100,avgTimeByChar:30,minLength:6,suffixKeyCodes:[9,13],prefixKeyCodes:[],ignoreIfFocusOn:!1,stopPropagation:!1,preventDefault:!1,captureEvents:!1,reactToKeydown:!0,reactToPaste:!1,singleScanQty:1};return t=this._mergeOptions(n,t),e.scannerDetectionData={options:t,vars:{firstCharTime:0,lastCharTime:0,accumulatedString:"",testTimer:!1,longPressTimeStart:0,longPressed:!1}},!0===t.reactToPaste&&e.addEventListener("paste",this._handlePaste,t.captureEvents),!1!==t.scanButtonKeyCode&&e.addEventListener("keyup",this._handleKeyUp,t.captureEvents),!0!==t.reactToKeydown&&!1===t.scanButtonKeyCode||e.addEventListener("keydown",this._handleKeyDown,t.captureEvents),this},detachFrom:function(e){e.scannerDetectionData.options.reactToPaste&&e.removeEventListener("paste",this._handlePaste),!1!==e.scannerDetectionData.options.scanButtonKeyCode&&e.removeEventListener("keyup",this._handleKeyUp),e.removeEventListener("keydown",this._handleKeyDown),e.scannerDetectionData=void 0},getOptions:function(e){return e.scannerDetectionData.options},setOptions:function(e,t){switch(e.scannerDetectionData.options.reactToPaste){case!0:!1===t.reactToPaste&&e.removeEventListener("paste",this._handlePaste);break;case!1:!0===t.reactToPaste&&e.addEventListener("paste",this._handlePaste)}switch(e.scannerDetectionData.options.scanButtonKeyCode){case!1:!1!==t.scanButtonKeyCode&&e.addEventListener("keyup",this._handleKeyUp);break;default:!1===t.scanButtonKeyCode&&e.removeEventListener("keyup",this._handleKeyUp)}return e.scannerDetectionData.options=this._mergeOptions(e.scannerDetectionData.options,t),this._reinitialize(e),this},decodeKeyEvent:function(e){var t=this._getNormalizedKeyNum(e);switch(!0){case 48<=t&&t<=90:case 106<=t&&t<=111:if(void 0!==e.key&&""!==e.key)return e.key;var n=String.fromCharCode(t);switch(e.shiftKey){case!1:n=n.toLowerCase();break;case!0:n=n.toUpperCase()}return n;case 96<=t&&t<=105:return t-96}return""},simulate:function(e,t){return this._reinitialize(e),Array.isArray(t)?t.forEach(function(e){var t={};"object"!=typeof e&&"function"!=typeof e||null===e?t.keyCode=parseInt(e):t=e;var n=new KeyboardEvent("keydown",t);document.dispatchEvent(n)}):this._validateScanCode(e,t),this},_reinitialize:function(e){var t=e.scannerDetectionData.vars;t.firstCharTime=0,t.lastCharTime=0,t.accumulatedString=""},_isFocusOnIgnoredElement:function(e){var t=e.scannerDetectionData.options.ignoreIfFocusOn;if(!t)return!1;var n=document.activeElement;if(Array.isArray(t)){for(var a=0;a<t.length;a++)if(!0===n.matches(t[a]))return!0}else if(n.matches(t))return!0;return!1},_validateScanCode:function(e,t){var n,a=e.scannerDetectionData,i=a.options,o=a.options.singleScanQty,r=a.vars.firstCharTime,s=a.vars.lastCharTime,c={};switch(!0){case t.length<i.minLength:c={message:"Receieved code is shorter then minimal length"};break;case s-r>t.length*i.avgTimeByChar:c={message:"Receieved code was not entered in time"};break;default:return i.onScan.call(e,t,o),n=new CustomEvent("scan",{detail:{scanCode:t,qty:o}}),e.dispatchEvent(n),d._reinitialize(e),!0}return c.scanCode=t,c.scanDuration=s-r,c.avgTimeByChar=i.avgTimeByChar,c.minLength=i.minLength,i.onScanError.call(e,c),n=new CustomEvent("scanError",{detail:c}),e.dispatchEvent(n),d._reinitialize(e),!1},_mergeOptions:function(e,t){var n,a={};for(n in e)Object.prototype.hasOwnProperty.call(e,n)&&(a[n]=e[n]);for(n in t)Object.prototype.hasOwnProperty.call(t,n)&&(a[n]=t[n]);return a},_getNormalizedKeyNum:function(e){return e.which||e.keyCode},_handleKeyDown:function(e){var t=d._getNormalizedKeyNum(e),n=this.scannerDetectionData.options,a=this.scannerDetectionData.vars,i=!1;if(!1!==n.onKeyDetect.call(this,t,e)&&!d._isFocusOnIgnoredElement(this))if(!1===n.scanButtonKeyCode||t!=n.scanButtonKeyCode){switch(!0){case a.firstCharTime&&-1!==n.suffixKeyCodes.indexOf(t):e.preventDefault(),e.stopImmediatePropagation(),i=!0;break;case!a.firstCharTime&&-1!==n.prefixKeyCodes.indexOf(t):e.preventDefault(),e.stopImmediatePropagation(),i=!1;break;default:var o=n.keyCodeMapper.call(this,e);if(null===o)return;a.accumulatedString+=o,n.preventDefault&&e.preventDefault(),n.stopPropagation&&e.stopImmediatePropagation(),i=!1}a.firstCharTime||(a.firstCharTime=Date.now()),a.lastCharTime=Date.now(),a.testTimer&&clearTimeout(a.testTimer),i?(d._validateScanCode(this,a.accumulatedString),a.testTimer=!1):a.testTimer=setTimeout(d._validateScanCode,n.timeBeforeScanTest,this,a.accumulatedString),n.onKeyProcess.call(this,o,e)}else a.longPressed||(a.longPressTimer=setTimeout(n.onScanButtonLongPress,n.scanButtonLongPressTime,this),a.longPressed=!0)},_handlePaste:function(e){if(!d._isFocusOnIgnoredElement(this)){e.preventDefault(),oOptions.stopPropagation&&e.stopImmediatePropagation();var t=(event.clipboardData||window.clipboardData).getData("text");this.scannerDetectionData.options.onPaste.call(this,t,event);var n=this.scannerDetectionData.vars;n.firstCharTime=0,n.lastCharTime=0,d._validateScanCode(this,t)}},_handleKeyUp:function(e){d._isFocusOnIgnoredElement(this)||d._getNormalizedKeyNum(e)==this.scannerDetectionData.options.scanButtonKeyCode&&(clearTimeout(this.scannerDetectionData.vars.longPressTimer),this.scannerDetectionData.vars.longPressed=!1)},isScanInProgressFor:function(e){return 0<e.scannerDetectionData.vars.firstCharTime}};return d});
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js
index 7011cf9..2ce0b27 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.js
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.js
@@ -1,5 +1,6 @@
/* global Clusterize */
-frappe.provide('erpnext.pos');
+frappe.provide('erpnext.PointOfSale');
+{% include "erpnext/selling/page/point_of_sale/pos_controller.js" %}
frappe.provide('erpnext.queries');
frappe.pages['point-of-sale'].on_page_load = function(wrapper) {
@@ -8,1988 +9,7 @@
title: __('Point of Sale'),
single_column: true
});
-
- frappe.db.get_value('POS Settings', {name: 'POS Settings'}, 'is_online', (r) => {
- if (r && !cint(r.use_pos_in_offline_mode)) {
- // online
- wrapper.pos = new erpnext.pos.PointOfSale(wrapper);
- window.cur_pos = wrapper.pos;
- } else {
- // offline
- frappe.flags.is_offline = true;
- frappe.set_route('pos');
- }
- });
-};
-
-frappe.pages['point-of-sale'].refresh = function(wrapper) {
- if (wrapper.pos) {
- wrapper.pos.make_new_invoice();
- }
-
- if (frappe.flags.is_offline) {
- frappe.set_route('pos');
- }
-}
-
-erpnext.pos.PointOfSale = class PointOfSale {
- constructor(wrapper) {
- this.wrapper = $(wrapper).find('.layout-main-section');
- this.page = wrapper.page;
-
- const assets = [
- 'assets/erpnext/js/pos/clusterize.js',
- 'assets/erpnext/css/pos.css'
- ];
-
- frappe.require(assets, () => {
- this.make();
- });
- }
-
- make() {
- return frappe.run_serially([
- () => frappe.dom.freeze(),
- () => {
- this.prepare_dom();
- this.prepare_menu();
- this.set_online_status();
- },
- () => this.make_new_invoice(),
- () => {
- if(!this.frm.doc.company) {
- this.setup_company()
- .then((company) => {
- this.frm.doc.company = company;
- this.get_pos_profile();
- });
- }
- },
- () => {
- frappe.dom.unfreeze();
- },
- () => this.page.set_title(__('Point of Sale'))
- ]);
- }
-
- get_pos_profile() {
- return frappe.xcall("erpnext.stock.get_item_details.get_pos_profile",
- {'company': this.frm.doc.company})
- .then((r) => {
- if(r) {
- this.frm.doc.pos_profile = r.name;
- this.set_pos_profile_data()
- .then(() => {
- this.on_change_pos_profile();
- });
- } else {
- this.raise_exception_for_pos_profile();
- }
- });
- }
-
- set_online_status() {
- this.connection_status = false;
- this.page.set_indicator(__("Offline"), "grey");
- frappe.call({
- method: "frappe.handler.ping",
- callback: r => {
- if (r.message) {
- this.connection_status = true;
- this.page.set_indicator(__("Online"), "green");
- }
- }
- });
- }
-
- raise_exception_for_pos_profile() {
- setTimeout(() => frappe.set_route('List', 'POS Profile'), 2000);
- frappe.throw(__("POS Profile is required to use Point-of-Sale"));
- }
-
- prepare_dom() {
- this.wrapper.append(`
- <div class="pos">
- <section class="cart-container">
-
- </section>
- <section class="item-container">
-
- </section>
- </div>
- `);
- }
-
- make_cart() {
- this.cart = new POSCart({
- frm: this.frm,
- wrapper: this.wrapper.find('.cart-container'),
- events: {
- on_customer_change: (customer) => {
- this.frm.set_value('customer', customer);
- },
- on_field_change: (item_code, field, value, batch_no) => {
- this.update_item_in_cart(item_code, field, value, batch_no);
- },
- on_numpad: (value) => {
- if (value == __('Pay')) {
- if (!this.payment) {
- this.make_payment_modal();
- } else {
- this.frm.doc.payments.map(p => {
- this.payment.dialog.set_value(p.mode_of_payment, p.amount);
- });
-
- this.payment.set_title();
- }
- this.payment.open_modal();
- }
- },
- on_select_change: () => {
- this.cart.numpad.set_inactive();
- this.set_form_action();
- },
- get_item_details: (item_code) => {
- return this.items.get(item_code);
- },
- get_loyalty_details: () => {
- var me = this;
- if (this.frm.doc.customer) {
- frappe.call({
- method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_program_details",
- args: {
- "customer": me.frm.doc.customer,
- "expiry_date": me.frm.doc.posting_date,
- "company": me.frm.doc.company,
- "silent": true
- },
- callback: function(r) {
- if (r.message.loyalty_program && r.message.loyalty_points) {
- me.cart.events.set_loyalty_details(r.message, true);
- }
- if (!r.message.loyalty_program) {
- var loyalty_details = {
- loyalty_points: 0,
- loyalty_program: '',
- expense_account: '',
- cost_center: ''
- }
- me.cart.events.set_loyalty_details(loyalty_details, false);
- }
- }
- });
- }
- },
- set_loyalty_details: (details, view_status) => {
- if (view_status) {
- this.cart.available_loyalty_points.$wrapper.removeClass("hide");
- } else {
- this.cart.available_loyalty_points.$wrapper.addClass("hide");
- }
- this.cart.available_loyalty_points.set_value(details.loyalty_points);
- this.cart.available_loyalty_points.refresh_input();
- this.frm.set_value("loyalty_program", details.loyalty_program);
- this.frm.set_value("loyalty_redemption_account", details.expense_account);
- this.frm.set_value("loyalty_redemption_cost_center", details.cost_center);
- }
- }
- });
-
- frappe.ui.form.on('Sales Invoice', 'selling_price_list', (frm) => {
- if(this.items && frm.doc.pos_profile) {
- this.items.reset_items();
- }
- })
- }
-
- toggle_editing(flag) {
- let disabled;
- if (flag !== undefined) {
- disabled = !flag;
- } else {
- disabled = this.frm.doc.docstatus == 1 ? true: false;
- }
- const pointer_events = disabled ? 'none' : 'inherit';
-
- this.wrapper.find('input, button, select').prop("disabled", disabled);
- this.wrapper.find('.number-pad-container').toggleClass("hide", disabled);
-
- this.wrapper.find('.cart-container').css('pointer-events', pointer_events);
- this.wrapper.find('.item-container').css('pointer-events', pointer_events);
-
- this.page.clear_actions();
- }
-
- make_items() {
- this.items = new POSItems({
- wrapper: this.wrapper.find('.item-container'),
- frm: this.frm,
- events: {
- update_cart: (item, field, value) => {
- if(!this.frm.doc.customer) {
- frappe.throw(__('Please select a customer'));
- }
- this.update_item_in_cart(item, field, value);
- this.cart && this.cart.unselect_all();
- }
- }
- });
- }
-
- update_item_in_cart(item_code, field='qty', value=1, batch_no) {
- frappe.dom.freeze();
- if(this.cart.exists(item_code, batch_no)) {
- const search_field = batch_no ? 'batch_no' : 'item_code';
- const search_value = batch_no || item_code;
- const item = this.frm.doc.items.find(i => i[search_field] === search_value);
- frappe.flags.hide_serial_batch_dialog = false;
-
- if (typeof value === 'string' && !in_list(['serial_no', 'batch_no'], field)) {
- // value can be of type '+1' or '-1'
- value = item[field] + flt(value);
- }
-
- if(field === 'serial_no') {
- value = item.serial_no + '\n'+ value;
- }
-
- // if actual_batch_qty and actual_qty if there is only one batch. In such
- // a case, no point showing the dialog
- const show_dialog = item.has_serial_no || item.has_batch_no;
-
- if (show_dialog && field == 'qty' && ((!item.batch_no && item.has_batch_no) ||
- (item.has_serial_no) || (item.actual_batch_qty != item.actual_qty)) ) {
- this.select_batch_and_serial_no(item);
- } else {
- this.update_item_in_frm(item, field, value)
- .then(() => {
- frappe.dom.unfreeze();
- frappe.run_serially([
- () => {
- let items = this.frm.doc.items.map(item => item.name);
- if (items && items.length > 0 && items.includes(item.name)) {
- this.frm.doc.items.forEach(item_row => {
- // update cart
- this.on_qty_change(item_row);
- });
- } else {
- this.on_qty_change(item);
- }
- },
- () => this.post_qty_change(item)
- ]);
- });
- }
- return;
- }
-
- let args = { item_code: item_code };
- if (in_list(['serial_no', 'batch_no'], field)) {
- args[field] = value;
- }
-
- // add to cur_frm
- const item = this.frm.add_child('items', args);
- frappe.flags.hide_serial_batch_dialog = true;
-
- frappe.run_serially([
- () => {
- return this.frm.script_manager.trigger('item_code', item.doctype, item.name)
- .then(() => {
- this.frm.script_manager.trigger('qty', item.doctype, item.name)
- .then(() => {
- frappe.run_serially([
- () => {
- let items = this.frm.doc.items.map(i => i.name);
- if (items && items.length > 0 && items.includes(item.name)) {
- this.frm.doc.items.forEach(item_row => {
- // update cart
- this.on_qty_change(item_row);
- });
- } else {
- this.on_qty_change(item);
- }
- },
- () => this.post_qty_change(item)
- ]);
- });
- });
- },
- () => {
- const show_dialog = item.has_serial_no || item.has_batch_no;
-
- // if actual_batch_qty and actual_qty if then there is only one batch. In such
- // a case, no point showing the dialog
- if (show_dialog && field == 'qty' && ((!item.batch_no && item.has_batch_no) ||
- (item.has_serial_no) || (item.actual_batch_qty != item.actual_qty)) ) {
- // check has serial no/batch no and update cart
- this.select_batch_and_serial_no(item);
- }
- }
- ]);
- }
-
- on_qty_change(item) {
- frappe.run_serially([
- () => this.update_cart_data(item),
- ]);
- }
-
- post_qty_change(item) {
- this.cart.update_taxes_and_totals();
- this.cart.update_grand_total();
- this.cart.update_qty_total();
- this.cart.scroll_to_item(item.item_code);
- this.set_form_action();
- }
-
- select_batch_and_serial_no(row) {
- frappe.dom.unfreeze();
-
- erpnext.show_serial_batch_selector(this.frm, row, () => {
- this.frm.doc.items.forEach(item => {
- this.update_item_in_frm(item, 'qty', item.qty)
- .then(() => {
- // update cart
- frappe.run_serially([
- () => {
- if (item.qty === 0) {
- frappe.model.clear_doc(item.doctype, item.name);
- }
- },
- () => this.update_cart_data(item),
- () => this.post_qty_change(item)
- ]);
- });
- })
- }, () => {
- this.on_close(row);
- }, true);
- }
-
- on_close(item) {
- if (!this.cart.exists(item.item_code, item.batch_no) && item.qty) {
- frappe.model.clear_doc(item.doctype, item.name);
- }
- }
-
- update_cart_data(item) {
- this.cart.add_item(item);
- frappe.dom.unfreeze();
- }
-
- update_item_in_frm(item, field, value) {
- if (field == 'qty' && value < 0) {
- frappe.msgprint(__("Quantity must be positive"));
- value = item.qty;
- } else {
- if (in_list(["qty", "serial_no", "batch"], field)) {
- item[field] = value;
- if (field == "serial_no" && value) {
- let serial_nos = value.split("\n");
- item["qty"] = serial_nos.filter(d => {
- return d!=="";
- }).length;
- }
- } else {
- return frappe.model.set_value(item.doctype, item.name, field, value);
- }
- }
-
- return this.frm.script_manager.trigger('qty', item.doctype, item.name)
- .then(() => {
- if (field === 'qty' && item.qty === 0) {
- frappe.model.clear_doc(item.doctype, item.name);
- }
- })
-
- return Promise.resolve();
- }
-
- make_payment_modal() {
- this.payment = new Payment({
- frm: this.frm,
- events: {
- submit_form: () => {
- this.submit_sales_invoice();
- }
- }
- });
- }
-
- submit_sales_invoice() {
- this.frm.savesubmit()
- .then((r) => {
- if (r && r.doc) {
- this.frm.doc.docstatus = r.doc.docstatus;
- frappe.show_alert({
- indicator: 'green',
- message: __(`Sales invoice ${r.doc.name} created succesfully`)
- });
-
- this.toggle_editing();
- this.set_form_action();
- this.set_primary_action_in_modal();
- }
- });
- }
-
- set_primary_action_in_modal() {
- if (!this.frm.msgbox) {
- this.frm.msgbox = frappe.msgprint(
- `<a class="btn btn-primary" onclick="cur_frm.print_preview.printit(true)" style="margin-right: 5px;">
- ${__('Print')}</a>
- <a class="btn btn-default">
- ${__('New')}</a>`
- );
-
- $(this.frm.msgbox.body).find('.btn-default').on('click', () => {
- this.frm.msgbox.hide();
- this.make_new_invoice();
- })
- }
- }
-
- change_pos_profile() {
- return new Promise((resolve) => {
- const on_submit = ({ company, pos_profile, set_as_default }) => {
- if (pos_profile) {
- this.pos_profile = pos_profile;
- }
-
- if (set_as_default) {
- frappe.call({
- method: "erpnext.accounts.doctype.pos_profile.pos_profile.set_default_profile",
- args: {
- 'pos_profile': pos_profile,
- 'company': company
- }
- }).then(() => {
- this.on_change_pos_profile();
- });
- } else {
- this.on_change_pos_profile();
- }
- }
-
-
- let me = this;
-
- var dialog = frappe.prompt([{
- fieldtype: 'Link',
- label: __('Company'),
- options: 'Company',
- fieldname: 'company',
- default: me.frm.doc.company,
- reqd: 1,
- onchange: function(e) {
- me.get_default_pos_profile(this.value).then((r) => {
- dialog.set_value('pos_profile', (r && r.name)? r.name : '');
- });
- }
- },
- {
- fieldtype: 'Link',
- label: __('POS Profile'),
- options: 'POS Profile',
- fieldname: 'pos_profile',
- default: me.frm.doc.pos_profile,
- reqd: 1,
- get_query: () => {
- return {
- query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query',
- filters: {
- company: dialog.get_value('company')
- }
- };
- }
- }, {
- fieldtype: 'Check',
- label: __('Set as default'),
- fieldname: 'set_as_default'
- }],
- on_submit,
- __('Select POS Profile')
- );
- });
- }
-
- on_change_pos_profile() {
- return frappe.run_serially([
- () => this.make_sales_invoice_frm(),
- () => {
- this.frm.doc.pos_profile = this.pos_profile;
- this.set_pos_profile_data()
- .then(() => {
- this.reset_cart();
- if (this.items) {
- this.items.reset_items();
- }
- });
- }
- ]);
- }
-
- get_default_pos_profile(company) {
- return frappe.xcall("erpnext.stock.get_item_details.get_pos_profile",
- {'company': company})
- }
-
- setup_company() {
- return new Promise(resolve => {
- if(!this.frm.doc.company) {
- frappe.prompt({fieldname:"company", options: "Company", fieldtype:"Link",
- label: __("Select Company"), reqd: 1}, (data) => {
- this.company = data.company;
- resolve(this.company);
- }, __("Select Company"));
- } else {
- resolve();
- }
- })
- }
-
- make_new_invoice() {
- return frappe.run_serially([
- () => this.make_sales_invoice_frm(),
- () => this.set_pos_profile_data(),
- () => {
- if (this.cart) {
- this.cart.frm = this.frm;
- this.cart.reset();
- this.cart.reset_pos_field_value();
- } else {
- this.make_items();
- this.make_cart();
- }
- this.toggle_editing(true);
- },
- ]);
- }
-
- reset_cart() {
- this.cart.frm = this.frm;
- this.cart.reset();
- this.items.reset_search_field();
- }
-
- make_sales_invoice_frm() {
- const doctype = 'Sales Invoice';
- return new Promise(resolve => {
- if (this.frm) {
- this.frm = get_frm(this.frm);
- if(this.company) {
- this.frm.doc.company = this.company;
- }
-
- resolve();
- } else {
- frappe.model.with_doctype(doctype, () => {
- this.frm = get_frm();
- resolve();
- });
- }
- });
-
- function get_frm(_frm) {
- const page = $('<div>');
- const frm = _frm || new frappe.ui.form.Form(doctype, page, false);
- const name = frappe.model.make_new_doc_and_get_name(doctype, true);
- frm.refresh(name);
- frm.doc.items = [];
- frm.doc.is_pos = 1;
-
- return frm;
- }
- }
-
- set_pos_profile_data() {
- if (this.company) {
- this.frm.doc.company = this.company;
- }
-
- if (!this.frm.doc.company) {
- return;
- }
-
- return new Promise(resolve => {
- return this.frm.call({
- doc: this.frm.doc,
- method: "set_missing_values",
- }).then((r) => {
- if(!r.exc) {
- if (!this.frm.doc.pos_profile) {
- frappe.dom.unfreeze();
- this.raise_exception_for_pos_profile();
- }
- this.frm.script_manager.trigger("update_stock");
- frappe.model.set_default_values(this.frm.doc);
- this.frm.cscript.calculate_taxes_and_totals();
-
- if (r.message) {
- this.frm.meta.default_print_format = r.message.print_format || "";
- this.frm.allow_edit_rate = r.message.allow_edit_rate;
- this.frm.allow_edit_discount = r.message.allow_edit_discount;
- this.frm.doc.campaign = r.message.campaign;
- this.frm.allow_print_before_pay = r.message.allow_print_before_pay;
- }
- }
-
- resolve();
- });
- });
- }
-
- prepare_menu() {
- var me = this;
- this.page.clear_menu();
-
- this.page.add_menu_item(__("Form View"), function () {
- frappe.model.sync(me.frm.doc);
- frappe.set_route("Form", me.frm.doc.doctype, me.frm.doc.name);
- });
-
- this.page.add_menu_item(__("POS Profile"), function () {
- frappe.set_route('List', 'POS Profile');
- });
-
- this.page.add_menu_item(__('POS Settings'), function() {
- frappe.set_route('Form', 'POS Settings');
- });
-
- this.page.add_menu_item(__('Change POS Profile'), function() {
- me.change_pos_profile();
- });
- this.page.add_menu_item(__('Close the POS'), function() {
- var voucher = frappe.model.get_new_doc('POS Closing Voucher');
- voucher.pos_profile = me.frm.doc.pos_profile;
- voucher.user = frappe.session.user;
- voucher.company = me.frm.doc.company;
- voucher.period_start_date = me.frm.doc.posting_date;
- voucher.period_end_date = me.frm.doc.posting_date;
- voucher.posting_date = me.frm.doc.posting_date;
- frappe.set_route('Form', 'POS Closing Voucher', voucher.name);
- });
- }
-
- set_form_action() {
- if(this.frm.doc.docstatus == 1 || (this.frm.allow_print_before_pay == 1 && this.frm.doc.items.length > 0)){
- this.page.set_secondary_action(__("Print"), async() => {
- if(this.frm.doc.docstatus != 1 ){
- await this.frm.save();
- }
- this.frm.print_preview.printit(true);
- });
- }
- if(this.frm.doc.items.length == 0){
- this.page.clear_secondary_action();
- }
-
- if (this.frm.doc.docstatus == 1) {
- this.page.set_primary_action(__("New"), () => {
- this.make_new_invoice();
- });
- this.page.add_menu_item(__("Email"), () => {
- this.frm.email_doc();
- });
- }
- }
-};
-
-const [Qty,Disc,Rate,Del,Pay] = [__("Qty"), __('Disc'), __('Rate'), __('Del'), __('Pay')];
-
-class POSCart {
- constructor({frm, wrapper, events}) {
- this.frm = frm;
- this.item_data = {};
- this.wrapper = wrapper;
- this.events = events;
- this.make();
- this.bind_events();
- }
-
- make() {
- this.make_dom();
- this.make_customer_field();
- this.make_pos_fields();
- this.make_loyalty_points();
- this.make_numpad();
- }
-
- make_dom() {
- this.wrapper.append(`
- <div class="pos-cart">
- <div class="customer-field">
- </div>
- <div class="pos-field-section" style="margin-bottom:12px; display:none">
- <a class="h6 uppercase more-fields-section" disabled> ${__("More Information")} </a>
- <i class="octicon octicon-chevron-down pos-fields-octicon collapse-indicator"
- style="color:#cacaca; cursor: pointer"></i>
- <div class="pos-fields" style ="margin-top:12px">
- </div>
- </div>
- <div class="cart-wrapper">
- <div class="list-item-table">
- <div class="list-item list-item--head">
- <div class="list-item__content list-item__content--flex-1.5 text-muted">${__('Item Name')}</div>
- <div class="list-item__content text-muted text-right">${__('Quantity')}</div>
- <div class="list-item__content text-muted text-right">${__('Discount')}</div>
- <div class="list-item__content text-muted text-right">${__('Rate')}</div>
- </div>
- <div class="cart-items">
- <div class="empty-state">
- <span>${__('No Items added to cart')}</span>
- </div>
- </div>
- <div class="taxes-and-totals">
- ${this.get_taxes_and_totals()}
- </div>
- <div class="discount-amount">`+
- (!this.frm.allow_edit_discount ? `` : `${this.get_discount_amount()}`)+
- `</div>
- <div class="grand-total">
- ${this.get_grand_total()}
- </div>
- <div class="quantity-total">
- ${this.get_item_qty_total()}
- </div>
- </div>
- </div>
- <div class="row">
- <div class="number-pad-container col-sm-6"></div>
- <div class="col-sm-6 loyalty-program-section">
- <div class="loyalty-program-field"> </div>
- </div>
- </div>
- </div>
- `);
-
-
- this.$cart_items = this.wrapper.find('.cart-items');
- this.$empty_state = this.wrapper.find('.cart-items .empty-state');
- this.$taxes_and_totals = this.wrapper.find('.taxes-and-totals');
- this.$discount_amount = this.wrapper.find('.discount-amount');
- this.$grand_total = this.wrapper.find('.grand-total');
- this.$qty_total = this.wrapper.find('.quantity-total');
- // this.$loyalty_button = this.wrapper.find('.loyalty-button');
-
- // this.$loyalty_button.on('click', () => {
- // this.loyalty_button.show();
- // })
-
- this.toggle_taxes_and_totals(false);
- this.$grand_total.on('click', () => {
- this.toggle_taxes_and_totals();
- });
- }
-
- reset() {
- this.$cart_items.find('.list-item').remove();
- this.$empty_state.show();
- this.$taxes_and_totals.html(this.get_taxes_and_totals());
- this.numpad && this.numpad.reset_value();
- this.customer_field.set_value("");
- this.frm.msgbox = "";
-
- let total_item_qty = 0.0;
- this.frm.set_value("pos_total_qty",total_item_qty);
-
- this.$discount_amount.find('input:text').val('');
- this.wrapper.find('.grand-total-value').text(
- format_currency(this.frm.doc.grand_total, this.frm.currency));
- this.wrapper.find('.rounded-total-value').text(
- format_currency(this.frm.doc.rounded_total, this.frm.currency));
- this.$qty_total.find(".quantity-total").text(total_item_qty);
-
- const customer = this.frm.doc.customer;
- this.customer_field.set_value(customer);
-
- if (this.numpad) {
- const disable_btns = this.disable_numpad_control()
- const enable_btns = [__('Rate'), __('Disc')]
-
- if (disable_btns) {
- enable_btns.filter(btn => !disable_btns.includes(btn))
- }
-
- this.numpad.enable_buttons(enable_btns);
- }
- }
-
- reset_pos_field_value() {
- let value = '';
- if (this.custom_pos_fields) {
- this.custom_pos_fields.forEach(r => {
- value = this.frm.doc[r.fieldname] || r.default_value || '';
-
- if (this.fields) {
- this.fields[r.fieldname].set_value(value);
- }
- })
- }
-
- this.wrapper.find('.pos-fields').toggle(false);
- this.wrapper.find('.pos-fields-octicon').toggle(true);
- }
-
- get_grand_total() {
- let total = this.get_total_template('Grand Total', 'grand-total-value');
-
- if (!cint(frappe.sys_defaults.disable_rounded_total)) {
- total += this.get_total_template('Rounded Total', 'rounded-total-value');
- }
-
- return total;
- }
-
- get_item_qty_total() {
- let total = this.get_total_template('Total Qty', 'quantity-total');
- return total;
- }
-
- get_total_template(label, class_name) {
- return `
- <div class="list-item">
- <div class="list-item__content text-muted">${__(label)}</div>
- <div class="list-item__content list-item__content--flex-2 ${class_name}">0.00</div>
- </div>
- `;
- }
-
- get_discount_amount() {
- const get_currency_symbol = window.get_currency_symbol;
-
- return `
- <div class="list-item">
- <div class="list-item__content list-item__content--flex-2 text-muted">${__('Discount')}</div>
- <div class="list-item__content discount-inputs">
- <input type="text"
- class="form-control additional_discount_percentage text-right"
- placeholder="% 0.00"
- >
- <input type="text"
- class="form-control discount_amount text-right"
- placeholder="${get_currency_symbol(this.frm.doc.currency)} 0.00"
- >
- </div>
- </div>
- `;
- }
-
- get_taxes_and_totals() {
- return `
- <div class="list-item">
- <div class="list-item__content list-item__content--flex-2 text-muted">${__('Net Total')}</div>
- <div class="list-item__content net-total">0.00</div>
- </div>
- <div class="list-item">
- <div class="list-item__content list-item__content--flex-2 text-muted">${__('Taxes')}</div>
- <div class="list-item__content taxes">0.00</div>
- </div>
- `;
- }
-
- toggle_taxes_and_totals(flag) {
- if (flag !== undefined) {
- this.tax_area_is_shown = flag;
- } else {
- this.tax_area_is_shown = !this.tax_area_is_shown;
- }
-
- this.$taxes_and_totals.toggle(this.tax_area_is_shown);
- this.$discount_amount.toggle(this.tax_area_is_shown);
- }
-
- update_taxes_and_totals() {
- if (!this.frm.doc.taxes) { return; }
-
- const currency = this.frm.doc.currency;
- this.frm.refresh_field('taxes');
-
- // Update totals
- this.$taxes_and_totals.find('.net-total')
- .html(format_currency(this.frm.doc.total, currency));
-
- // Update taxes
- const taxes_html = this.frm.doc.taxes.map(tax => {
- return `
- <div>
- <span>${tax.description}</span>
- <span class="text-right bold">
- ${format_currency(tax.tax_amount, currency)}
- </span>
- </div>
- `;
- }).join("");
- this.$taxes_and_totals.find('.taxes').html(taxes_html);
- }
-
- update_grand_total() {
- this.$grand_total.find('.grand-total-value').text(
- format_currency(this.frm.doc.grand_total, this.frm.currency)
- );
-
- this.$grand_total.find('.rounded-total-value').text(
- format_currency(this.frm.doc.rounded_total, this.frm.currency)
- );
- }
-
- update_qty_total() {
- var total_item_qty = 0;
- $.each(this.frm.doc["items"] || [], function (i, d) {
- if (d.qty > 0) {
- total_item_qty += d.qty;
- }
- });
- this.$qty_total.find('.quantity-total').text(total_item_qty);
- this.frm.set_value("pos_total_qty",total_item_qty);
- }
-
- make_customer_field() {
- this.customer_field = frappe.ui.form.make_control({
- df: {
- fieldtype: 'Link',
- label: 'Customer',
- fieldname: 'customer',
- options: 'Customer',
- reqd: 1,
- get_query: function() {
- return {
- query: 'erpnext.controllers.queries.customer_query'
- }
- },
- onchange: () => {
- this.events.on_customer_change(this.customer_field.get_value());
- this.events.get_loyalty_details();
- }
- },
- parent: this.wrapper.find('.customer-field'),
- render_input: true
- });
-
- this.customer_field.set_value(this.frm.doc.customer);
- }
-
- make_pos_fields() {
- const me = this;
-
- this.fields = {};
- this.wrapper.find('.pos-fields-octicon, .more-fields-section').click(() => {
- this.wrapper.find('.pos-fields').toggle();
- this.wrapper.find('.pos-fields-octicon').toggleClass('octicon-chevron-down').toggleClass('octicon-chevron-up');
- });
- this.wrapper.find('.pos-fields').toggle(false);
-
- return new Promise(res => {
- frappe.call({
- method: "erpnext.selling.page.point_of_sale.point_of_sale.get_pos_fields",
- freeze: true,
- }).then(r => {
- if(r.message.length) {
- this.wrapper.find('.pos-field-section').css('display','block');
- this.custom_pos_fields = r.message;
- if (r.message.length < 3) {
- this.wrapper.find('.pos-fields').toggle(true);
- this.wrapper.find('.pos-fields-octicon').toggleClass('octicon-chevron-down').toggleClass('octicon-chevron-up');
- }
-
- r.message.forEach(field => {
- this.fields[field.fieldname] = frappe.ui.form.make_control({
- df: {
- fieldtype: field.fieldtype,
- label: field.label,
- fieldname: field.fieldname,
- options: field.options,
- reqd: field.reqd || 0,
- read_only: field.read_only || 0,
- default: field.default_value,
- onchange: function() {
- if (this.value) {
- me.frm.set_value(this.df.fieldname, this.value);
- }
- },
- get_query: () => {
- return this.get_query_for_pos_fields(field.fieldname)
- },
- },
- parent: this.wrapper.find('.pos-fields'),
- render_input: true
- });
-
- if (this.frm.doc[field.fieldname]) {
- this.fields[field.fieldname].set_value(this.frm.doc[field.fieldname]);
- }
- });
- }
- });
- });
- }
-
- get_query_for_pos_fields(field) {
- if (this.frm.fields_dict && this.frm.fields_dict[field]
- && this.frm.fields_dict[field].get_query) {
- return this.frm.fields_dict[field].get_query(this.frm.doc);
- }
- }
-
- make_loyalty_points() {
- this.available_loyalty_points = frappe.ui.form.make_control({
- df: {
- fieldtype: 'Int',
- label: 'Available Loyalty Points',
- read_only: 1,
- fieldname: 'available_loyalty_points'
- },
- parent: this.wrapper.find('.loyalty-program-field')
- });
- this.available_loyalty_points.set_value(this.frm.doc.loyalty_points);
- }
-
-
- disable_numpad_control() {
- let disabled_btns = [];
- if(!this.frm.allow_edit_rate) {
- disabled_btns.push(__('Rate'));
- }
- if(!this.frm.allow_edit_discount) {
- disabled_btns.push(__('Disc'));
- }
- return disabled_btns;
- }
-
-
- make_numpad() {
-
- var pay_class = {}
- pay_class[__('Pay')]='brand-primary'
- this.numpad = new NumberPad({
- button_array: [
- [1, 2, 3, Qty],
- [4, 5, 6, Disc],
- [7, 8, 9, Rate],
- [Del, 0, '.', Pay]
- ],
- add_class: pay_class,
- disable_highlight: [Qty, Disc, Rate, Pay],
- reset_btns: [Qty, Disc, Rate, Pay],
- del_btn: Del,
- disable_btns: this.disable_numpad_control(),
- wrapper: this.wrapper.find('.number-pad-container'),
- onclick: (btn_value) => {
- // on click
-
- if (!this.selected_item && btn_value !== Pay) {
- frappe.show_alert({
- indicator: 'red',
- message: __('Please select an item in the cart')
- });
- return;
- }
- if ([Qty, Disc, Rate].includes(btn_value)) {
- this.set_input_active(btn_value);
- } else if (btn_value !== Pay) {
- if (!this.selected_item.active_field) {
- frappe.show_alert({
- indicator: 'red',
- message: __('Please select a field to edit from numpad')
- });
- return;
- }
-
- if (this.selected_item.active_field == 'discount_percentage' && this.numpad.get_value() > cint(100)) {
- frappe.show_alert({
- indicator: 'red',
- message: __('Discount amount cannot be greater than 100%')
- });
- this.numpad.reset_value();
- } else {
- const item_code = unescape(this.selected_item.attr('data-item-code'));
- const batch_no = this.selected_item.attr('data-batch-no');
- const field = this.selected_item.active_field;
- const value = this.numpad.get_value();
-
- this.events.on_field_change(item_code, field, value, batch_no);
- }
- }
-
- this.events.on_numpad(btn_value);
- }
- });
- }
-
- set_input_active(btn_value) {
- this.selected_item.removeClass('qty disc rate');
-
- this.numpad.set_active(btn_value);
- if (btn_value === Qty) {
- this.selected_item.addClass('qty');
- this.selected_item.active_field = 'qty';
- } else if (btn_value == Disc) {
- this.selected_item.addClass('disc');
- this.selected_item.active_field = 'discount_percentage';
- } else if (btn_value == Rate) {
- this.selected_item.addClass('rate');
- this.selected_item.active_field = 'rate';
- }
- }
-
- add_item(item) {
- this.$empty_state.hide();
-
- if (this.exists(item.item_code, item.batch_no)) {
- // update quantity
- this.update_item(item);
- } else if (flt(item.qty) > 0.0) {
- // add to cart
- const $item = $(this.get_item_html(item));
- $item.appendTo(this.$cart_items);
- }
- this.highlight_item(item.item_code);
- }
-
- update_item(item) {
- const item_selector = item.batch_no ?
- `[data-batch-no="${item.batch_no}"]` : `[data-item-code="${escape(item.item_code)}"]`;
-
- const $item = this.$cart_items.find(item_selector);
-
- if(item.qty > 0) {
- const is_stock_item = this.get_item_details(item.item_code).is_stock_item;
- const indicator_class = (!is_stock_item || item.actual_qty >= item.qty) ? 'green' : 'red';
- const remove_class = indicator_class == 'green' ? 'red' : 'green';
-
- $item.find('.quantity input').val(item.qty);
- $item.find('.discount').text(item.discount_percentage + '%');
- $item.find('.rate').text(format_currency(item.rate, this.frm.doc.currency));
- $item.addClass(indicator_class);
- $item.removeClass(remove_class);
- } else {
- $item.remove();
- }
- }
-
- get_item_html(item) {
- const is_stock_item = this.get_item_details(item.item_code).is_stock_item;
- const rate = format_currency(item.rate, this.frm.doc.currency);
- const indicator_class = (!is_stock_item || item.actual_qty >= item.qty) ? 'green' : 'red';
- const batch_no = item.batch_no || '';
-
- return `
- <div class="list-item indicator ${indicator_class}" data-item-code="${escape(item.item_code)}"
- data-batch-no="${batch_no}" title="Item: ${item.item_name} Available Qty: ${item.actual_qty} ${item.stock_uom}">
- <div class="item-name list-item__content list-item__content--flex-1.5 ellipsis">
- ${item.item_name}
- </div>
- <div class="quantity list-item__content text-right">
- ${get_quantity_html(item.qty)}
- </div>
- <div class="discount list-item__content text-right">
- ${item.discount_percentage}%
- </div>
- <div class="rate list-item__content text-right">
- ${rate}
- </div>
- </div>
- `;
-
- function get_quantity_html(value) {
- return `
- <div class="input-group input-group-xs">
- <span class="input-group-btn">
- <button class="btn btn-default btn-xs" data-action="increment">+</button>
- </span>
-
- <input class="form-control" type="number" value="${value}">
-
- <span class="input-group-btn">
- <button class="btn btn-default btn-xs" data-action="decrement">-</button>
- </span>
- </div>
- `;
- }
- }
-
- get_item_details(item_code) {
- if (!this.item_data[item_code]) {
- this.item_data[item_code] = this.events.get_item_details(item_code);
- }
-
- return this.item_data[item_code];
- }
-
- exists(item_code, batch_no) {
- const is_exists = batch_no ?
- `[data-batch-no="${batch_no}"]` : `[data-item-code="${escape(item_code)}"]`;
-
- let $item = this.$cart_items.find(is_exists);
-
- return $item.length > 0;
- }
-
- highlight_item(item_code) {
- const $item = this.$cart_items.find(`[data-item-code="${escape(item_code)}"]`);
- $item.addClass('highlight');
- setTimeout(() => $item.removeClass('highlight'), 1000);
- }
-
- scroll_to_item(item_code) {
- const $item = this.$cart_items.find(`[data-item-code="${escape(item_code)}"]`);
- if ($item.length === 0) return;
- const scrollTop = $item.offset().top - this.$cart_items.offset().top + this.$cart_items.scrollTop();
- this.$cart_items.animate({ scrollTop });
- }
-
- bind_events() {
- const me = this;
- const events = this.events;
-
- // quantity change
- this.$cart_items.on('click',
- '[data-action="increment"], [data-action="decrement"]', function() {
- const $btn = $(this);
- const $item = $btn.closest('.list-item[data-item-code]');
- const item_code = unescape($item.attr('data-item-code'));
- const action = $btn.attr('data-action');
-
- if(action === 'increment') {
- events.on_field_change(item_code, 'qty', '+1');
- } else if(action === 'decrement') {
- events.on_field_change(item_code, 'qty', '-1');
- }
- });
-
- this.$cart_items.on('change', '.quantity input', function() {
- const $input = $(this);
- const $item = $input.closest('.list-item[data-item-code]');
- const item_code = unescape($item.attr('data-item-code'));
- events.on_field_change(item_code, 'qty', flt($input.val()));
- });
-
- // current item
- this.$cart_items.on('click', '.list-item', function() {
- me.set_selected_item($(this));
- });
-
- this.wrapper.find('.additional_discount_percentage').on('change', (e) => {
- const discount_percentage = flt(e.target.value,
- precision("additional_discount_percentage"));
-
- frappe.model.set_value(this.frm.doctype, this.frm.docname,
- 'additional_discount_percentage', discount_percentage)
- .then(() => {
- let discount_wrapper = this.wrapper.find('.discount_amount');
- discount_wrapper.val(flt(this.frm.doc.discount_amount,
- precision('discount_amount')));
- discount_wrapper.trigger('change');
- });
- });
-
- this.wrapper.find('.discount_amount').on('change', (e) => {
- const discount_amount = flt(e.target.value, precision('discount_amount'));
- frappe.model.set_value(this.frm.doctype, this.frm.docname,
- 'discount_amount', discount_amount);
- this.frm.trigger('discount_amount')
- .then(() => {
- this.update_discount_fields();
- this.update_taxes_and_totals();
- this.update_grand_total();
- });
- });
- }
-
- update_discount_fields() {
- let discount_wrapper = this.wrapper.find('.additional_discount_percentage');
- let discount_amt_wrapper = this.wrapper.find('.discount_amount');
- discount_wrapper.val(flt(this.frm.doc.additional_discount_percentage,
- precision('additional_discount_percentage')));
- discount_amt_wrapper.val(flt(this.frm.doc.discount_amount,
- precision('discount_amount')));
- }
-
- set_selected_item($item) {
- this.selected_item = $item;
- this.$cart_items.find('.list-item').removeClass('current-item qty disc rate');
- this.selected_item.addClass('current-item');
- this.events.on_select_change();
- }
-
- unselect_all() {
- this.$cart_items.find('.list-item').removeClass('current-item qty disc rate');
- this.selected_item = null;
- this.events.on_select_change();
- }
-}
-
-class POSItems {
- constructor({wrapper, frm, events}) {
- this.wrapper = wrapper;
- this.frm = frm;
- this.items = {};
- this.events = events;
- this.currency = this.frm.doc.currency;
-
- frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name", (r) => {
- this.parent_item_group = r.name;
- this.make_dom();
- this.make_fields();
-
- this.init_clusterize();
- this.bind_events();
- this.load_items_data();
- })
- }
-
- load_items_data() {
- // bootstrap with 20 items
- this.get_items()
- .then(({ items }) => {
- this.all_items = items;
- this.items = items;
- this.render_items(items);
- });
- }
-
- reset_items() {
- this.wrapper.find('.pos-items').empty();
- this.init_clusterize();
- this.load_items_data();
- }
-
- make_dom() {
- this.wrapper.html(`
- <div class="fields">
- <div class="search-field">
- </div>
- <div class="item-group-field">
- </div>
- </div>
- <div class="items-wrapper">
- </div>
- `);
-
- this.items_wrapper = this.wrapper.find('.items-wrapper');
- this.items_wrapper.append(`
- <div class="list-item-table pos-items-wrapper">
- <div class="pos-items image-view-container">
- </div>
- </div>
- `);
- }
-
- make_fields() {
- // Search field
- const me = this;
- this.search_field = frappe.ui.form.make_control({
- df: {
- fieldtype: 'Data',
- label: __('Search Item (Ctrl + i)'),
- placeholder: __('Search by item code, serial number, batch no or barcode')
- },
- parent: this.wrapper.find('.search-field'),
- render_input: true,
- });
-
- frappe.ui.keys.on('ctrl+i', () => {
- this.search_field.set_focus();
- });
-
- this.search_field.$input.on('input', (e) => {
- clearTimeout(this.last_search);
- this.last_search = setTimeout(() => {
- const search_term = e.target.value;
- const item_group = this.item_group_field ?
- this.item_group_field.get_value() : '';
-
- this.filter_items({ search_term:search_term, item_group: item_group});
- }, 300);
- });
-
- this.item_group_field = frappe.ui.form.make_control({
- df: {
- fieldtype: 'Link',
- label: 'Item Group',
- options: 'Item Group',
- default: me.parent_item_group,
- onchange: () => {
- const item_group = this.item_group_field.get_value();
- if (item_group) {
- this.filter_items({ item_group: item_group });
- }
- },
- get_query: () => {
- return {
- query: 'erpnext.selling.page.point_of_sale.point_of_sale.item_group_query',
- filters: {
- pos_profile: this.frm.doc.pos_profile
- }
- };
- }
- },
- parent: this.wrapper.find('.item-group-field'),
- render_input: true
- });
- }
-
- init_clusterize() {
- this.clusterize = new Clusterize({
- scrollElem: this.wrapper.find('.pos-items-wrapper')[0],
- contentElem: this.wrapper.find('.pos-items')[0],
- rows_in_block: 6
- });
- }
-
- render_items(items) {
- let _items = items || this.items;
-
- const all_items = Object.values(_items).map(item => this.get_item_html(item));
- let row_items = [];
-
- const row_container = '<div class="image-view-row">';
- let curr_row = row_container;
-
- for (let i=0; i < all_items.length; i++) {
- // wrap 4 items in a div to emulate
- // a row for clusterize
- if(i % 4 === 0 && i !== 0) {
- curr_row += '</div>';
- row_items.push(curr_row);
- curr_row = row_container;
- }
- curr_row += all_items[i];
-
- if(i == all_items.length - 1) {
- row_items.push(curr_row);
- }
- }
-
- this.clusterize.update(row_items);
- }
-
- filter_items({ search_term='', item_group=this.parent_item_group }={}) {
- if (search_term) {
- search_term = search_term.toLowerCase();
-
- // memoize
- this.search_index = this.search_index || {};
- if (this.search_index[search_term]) {
- const items = this.search_index[search_term];
- this.items = items;
- this.render_items(items);
- this.set_item_in_the_cart(items);
- return;
- }
- } else if (item_group == this.parent_item_group) {
- this.items = this.all_items;
- return this.render_items(this.all_items);
- }
-
- this.get_items({search_value: search_term, item_group })
- .then(({ items, serial_no, batch_no, barcode }) => {
- if (search_term && !barcode) {
- this.search_index[search_term] = items;
- }
-
- this.items = items;
- this.render_items(items);
- this.set_item_in_the_cart(items, serial_no, batch_no, barcode);
- });
- }
-
- set_item_in_the_cart(items, serial_no, batch_no, barcode) {
- if (serial_no) {
- this.events.update_cart(items[0].item_code,
- 'serial_no', serial_no);
- this.reset_search_field();
- return;
- }
-
- if (batch_no) {
- this.events.update_cart(items[0].item_code,
- 'batch_no', batch_no);
- this.reset_search_field();
- return;
- }
-
- if (items.length === 1 && (serial_no || batch_no || barcode)) {
- this.events.update_cart(items[0].item_code,
- 'qty', '+1');
- this.reset_search_field();
- }
- }
-
- reset_search_field() {
- this.search_field.set_value('');
- this.search_field.$input.trigger("input");
- }
-
- bind_events() {
- var me = this;
- this.wrapper.on('click', '.pos-item-wrapper', function() {
- const $item = $(this);
- const item_code = unescape($item.attr('data-item-code'));
- me.events.update_cart(item_code, 'qty', '+1');
- });
- }
-
- get(item_code) {
- let item = {};
- this.items.map(data => {
- if (data.item_code === item_code) {
- item = data;
- }
- })
-
- return item
- }
-
- get_all() {
- return this.items;
- }
-
- get_item_html(item) {
- const price_list_rate = format_currency(item.price_list_rate, this.currency);
- const { item_code, item_name, item_image} = item;
- const item_title = item_name || item_code;
-
- const template = `
- <div class="pos-item-wrapper image-view-item" data-item-code="${escape(item_code)}">
- <div class="image-view-header">
- <div>
- <a class="grey list-id" data-name="${item_code}" title="${item_title}">
- ${item_title}
- </a>
- </div>
- </div>
- <div class="image-view-body">
- <a data-item-code="${item_code}"
- title="${item_title}"
- >
- <div class="image-field"
- style="${!item_image ? 'background-color: #fafbfc;' : ''} border: 0px;"
- >
- ${!item_image ? `<span class="placeholder-text">
- ${frappe.get_abbr(item_title)}
- </span>` : '' }
- ${item_image ? `<img src="${item_image}" alt="${item_title}">` : '' }
- </div>
- <span class="price-info">
- ${price_list_rate}
- </span>
- </a>
- </div>
- </div>
- `;
-
- return template;
- }
-
- get_items({start = 0, page_length = 40, search_value='', item_group=this.parent_item_group}={}) {
- const price_list = this.frm.doc.selling_price_list;
- return new Promise(res => {
- frappe.call({
- method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items",
- freeze: true,
- args: {
- start,
- page_length,
- price_list,
- item_group,
- search_value,
- pos_profile: this.frm.doc.pos_profile
- }
- }).then(r => {
- // const { items, serial_no, batch_no } = r.message;
-
- // this.serial_no = serial_no || "";
- res(r.message);
- });
- });
- }
-}
-
-class NumberPad {
- constructor({
- wrapper, onclick, button_array,
- add_class={}, disable_highlight=[],
- reset_btns=[], del_btn='', disable_btns
- }) {
- this.wrapper = wrapper;
- this.onclick = onclick;
- this.button_array = button_array;
- this.add_class = add_class;
- this.disable_highlight = disable_highlight;
- this.reset_btns = reset_btns;
- this.del_btn = del_btn;
- this.disable_btns = disable_btns || [];
- this.make_dom();
- this.bind_events();
- this.value = '';
- }
-
- make_dom() {
- if (!this.button_array) {
- this.button_array = [
- [1, 2, 3],
- [4, 5, 6],
- [7, 8, 9],
- ['', 0, '']
- ];
- }
-
- this.wrapper.html(`
- <div class="number-pad">
- ${this.button_array.map(get_row).join("")}
- </div>
- `);
-
- function get_row(row) {
- return '<div class="num-row">' + row.map(get_col).join("") + '</div>';
- }
-
- function get_col(col) {
- return `<div class="num-col" data-value="${col}"><div>${col}</div></div>`;
- }
-
- this.set_class();
-
- if(this.disable_btns) {
- this.disable_btns.forEach((btn) => {
- const $btn = this.get_btn(btn);
- $btn.prop("disabled", true)
- $btn.hover(() => {
- $btn.css('cursor','not-allowed');
- })
- })
- }
- }
-
- enable_buttons(btns) {
- btns.forEach((btn) => {
- const $btn = this.get_btn(btn);
- $btn.prop("disabled", false)
- $btn.hover(() => {
- $btn.css('cursor','pointer');
- })
- })
- }
-
- set_class() {
- for (const btn in this.add_class) {
- const class_name = this.add_class[btn];
- this.get_btn(btn).addClass(class_name);
- }
- }
-
- bind_events() {
- // bind click event
- const me = this;
- this.wrapper.on('click', '.num-col', function() {
- const $btn = $(this);
- const btn_value = $btn.attr('data-value');
- if (!me.disable_highlight.includes(btn_value)) {
- me.highlight_button($btn);
- }
- if (me.reset_btns.includes(btn_value)) {
- me.reset_value();
- } else {
- if (btn_value === me.del_btn) {
- me.value = me.value.substr(0, me.value.length - 1);
- } else {
- me.value += btn_value;
- }
- }
- me.onclick(btn_value);
- });
- }
-
- reset_value() {
- this.value = '';
- }
-
- get_value() {
- return flt(this.value);
- }
-
- get_btn(btn_value) {
- return this.wrapper.find(`.num-col[data-value="${btn_value}"]`);
- }
-
- highlight_button($btn) {
- $btn.addClass('highlight');
- setTimeout(() => $btn.removeClass('highlight'), 1000);
- }
-
- set_active(btn_value) {
- const $btn = this.get_btn(btn_value);
- this.wrapper.find('.num-col').removeClass('active');
- $btn.addClass('active');
- }
-
- set_inactive() {
- this.wrapper.find('.num-col').removeClass('active');
- }
-}
-
-class Payment {
- constructor({frm, events}) {
- this.frm = frm;
- this.events = events;
- this.make();
- this.bind_events();
- this.set_primary_action();
- }
-
- open_modal() {
- this.dialog.show();
- }
-
- make() {
- this.set_flag();
- this.dialog = new frappe.ui.Dialog({
- fields: this.get_fields(),
- width: 800,
- invoice_frm: this.frm
- });
-
- this.set_title();
-
- this.$body = this.dialog.body;
-
- this.numpad = new NumberPad({
- wrapper: $(this.$body).find('[data-fieldname="numpad"]'),
- button_array: [
- [1, 2, 3],
- [4, 5, 6],
- [7, 8, 9],
- [__('Del'), 0, '.'],
- ],
- onclick: () => {
- if(this.fieldname) {
- this.dialog.set_value(this.fieldname, this.numpad.get_value());
- }
- }
- });
- }
-
- set_title() {
- let title = __('Total Amount {0}',
- [format_currency(this.frm.doc.rounded_total || this.frm.doc.grand_total,
- this.frm.doc.currency)]);
-
- this.dialog.set_title(title);
- }
-
- bind_events() {
- var me = this;
- $(this.dialog.body).find('.input-with-feedback').focusin(function() {
- me.numpad.reset_value();
- me.fieldname = $(this).prop('dataset').fieldname;
- if (me.frm.doc.outstanding_amount > 0 &&
- !in_list(['write_off_amount', 'change_amount'], me.fieldname)) {
- me.frm.doc.payments.forEach((data) => {
- if (data.mode_of_payment == me.fieldname && !data.amount) {
- me.dialog.set_value(me.fieldname,
- me.frm.doc.outstanding_amount / me.frm.doc.conversion_rate);
- return;
- }
- })
- }
- });
- }
-
- set_primary_action() {
- var me = this;
-
- this.dialog.set_primary_action(__("Submit"), function() {
- me.dialog.hide();
- me.events.submit_form();
- });
- }
-
- get_fields() {
- const me = this;
-
- let fields = this.frm.doc.payments.map(p => {
- return {
- fieldtype: 'Currency',
- label: __(p.mode_of_payment),
- options: me.frm.doc.currency,
- fieldname: p.mode_of_payment,
- default: p.amount,
- onchange: () => {
- const value = this.dialog.get_value(this.fieldname) || 0;
- me.update_payment_value(this.fieldname, value);
- }
- };
- });
-
- fields = fields.concat([
- {
- fieldtype: 'Column Break',
- },
- {
- fieldtype: 'HTML',
- fieldname: 'numpad'
- },
- {
- fieldtype: 'Section Break',
- depends_on: 'eval: this.invoice_frm.doc.loyalty_program'
- },
- {
- fieldtype: 'Check',
- label: 'Redeem Loyalty Points',
- fieldname: 'redeem_loyalty_points',
- onchange: () => {
- me.update_cur_frm_value("redeem_loyalty_points", () => {
- frappe.flags.redeem_loyalty_points = false;
- me.update_loyalty_points();
- });
- }
- },
- {
- fieldtype: 'Column Break',
- },
- {
- fieldtype: 'Int',
- fieldname: "loyalty_points",
- label: __("Loyalty Points"),
- depends_on: "redeem_loyalty_points",
- onchange: () => {
- me.update_cur_frm_value("loyalty_points", () => {
- frappe.flags.loyalty_points = false;
- me.update_loyalty_points();
- });
- }
- },
- {
- fieldtype: 'Currency',
- label: __("Loyalty Amount"),
- fieldname: "loyalty_amount",
- options: me.frm.doc.currency,
- read_only: 1,
- depends_on: "redeem_loyalty_points"
- },
- {
- fieldtype: 'Section Break',
- },
- {
- fieldtype: 'Currency',
- label: __("Write off Amount"),
- options: me.frm.doc.currency,
- fieldname: "write_off_amount",
- default: me.frm.doc.write_off_amount,
- onchange: () => {
- me.update_cur_frm_value('write_off_amount', () => {
- frappe.flags.change_amount = false;
- me.update_change_amount();
- });
- }
- },
- {
- fieldtype: 'Column Break',
- },
- {
- fieldtype: 'Currency',
- label: __("Change Amount"),
- options: me.frm.doc.currency,
- fieldname: "change_amount",
- default: me.frm.doc.change_amount,
- onchange: () => {
- me.update_cur_frm_value('change_amount', () => {
- frappe.flags.write_off_amount = false;
- me.update_write_off_amount();
- });
- }
- },
- {
- fieldtype: 'Section Break',
- },
- {
- fieldtype: 'Currency',
- label: __("Paid Amount"),
- options: me.frm.doc.currency,
- fieldname: "paid_amount",
- default: me.frm.doc.paid_amount,
- read_only: 1
- },
- {
- fieldtype: 'Column Break',
- },
- {
- fieldtype: 'Currency',
- label: __("Outstanding Amount"),
- options: me.frm.doc.currency,
- fieldname: "outstanding_amount",
- default: me.frm.doc.outstanding_amount,
- read_only: 1
- },
- ]);
-
- return fields;
- }
-
- set_flag() {
- frappe.flags.write_off_amount = true;
- frappe.flags.change_amount = true;
- frappe.flags.loyalty_points = true;
- frappe.flags.redeem_loyalty_points = true;
- frappe.flags.payment_method = true;
- }
-
- update_cur_frm_value(fieldname, callback) {
- if (frappe.flags[fieldname]) {
- const value = this.dialog.get_value(fieldname);
- this.frm.set_value(fieldname, value)
- .then(() => {
- callback();
- });
- }
-
- frappe.flags[fieldname] = true;
- }
-
- update_payment_value(fieldname, value) {
- var me = this;
- $.each(this.frm.doc.payments, function(i, data) {
- if (__(data.mode_of_payment) == __(fieldname)) {
- frappe.model.set_value('Sales Invoice Payment', data.name, 'amount', value)
- .then(() => {
- me.update_change_amount();
- me.update_write_off_amount();
- });
- }
- });
- }
-
- update_change_amount() {
- this.dialog.set_value("change_amount", this.frm.doc.change_amount);
- this.show_paid_amount();
- }
-
- update_write_off_amount() {
- this.dialog.set_value("write_off_amount", this.frm.doc.write_off_amount);
- }
-
- show_paid_amount() {
- this.dialog.set_value("paid_amount", this.frm.doc.paid_amount);
- this.dialog.set_value("outstanding_amount", this.frm.doc.outstanding_amount);
- }
-
- update_payment_amount() {
- var me = this;
- $.each(this.frm.doc.payments, function(i, data) {
- console.log("setting the ", data.mode_of_payment, " for the value", data.amount);
- me.dialog.set_value(data.mode_of_payment, data.amount);
- });
- }
-
- update_loyalty_points() {
- if (this.dialog.get_value("redeem_loyalty_points")) {
- this.dialog.set_value("loyalty_points", this.frm.doc.loyalty_points);
- this.dialog.set_value("loyalty_amount", this.frm.doc.loyalty_amount);
- this.update_payment_amount();
- this.show_paid_amount();
- }
- }
-
-}
+ // online
+ wrapper.pos = new erpnext.PointOfSale.Controller(wrapper);
+ window.cur_pos = wrapper.pos;
+};
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.json b/erpnext/selling/page/point_of_sale/point_of_sale.json
index 6d2f5f2..99b86e4 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.json
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.json
@@ -1,33 +1,33 @@
{
- "content": null,
- "creation": "2017-08-07 17:08:56.737947",
- "docstatus": 0,
- "doctype": "Page",
- "idx": 0,
- "modified": "2017-09-11 13:49:05.415211",
- "modified_by": "Administrator",
- "module": "Selling",
- "name": "point-of-sale",
- "owner": "Administrator",
- "page_name": "Point of Sale",
- "restrict_to_domain": "Retail",
+ "content": null,
+ "creation": "2020-01-28 22:05:44.819140",
+ "docstatus": 0,
+ "doctype": "Page",
+ "idx": 0,
+ "modified": "2020-06-01 15:41:06.348380",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "point-of-sale",
+ "owner": "Administrator",
+ "page_name": "Point of Sale",
+ "restrict_to_domain": "Retail",
"roles": [
{
"role": "Accounts User"
- },
+ },
{
"role": "Accounts Manager"
- },
+ },
{
"role": "Sales User"
- },
+ },
{
"role": "Sales Manager"
}
- ],
- "script": null,
- "standard": "Yes",
- "style": null,
- "system_page": 0,
- "title": "Point of Sale"
+ ],
+ "script": null,
+ "standard": "Yes",
+ "style": null,
+ "system_page": 0,
+ "title": "Point Of Sale"
}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py
index 1ae1fde..f7b7ed8 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.py
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.py
@@ -6,6 +6,7 @@
from frappe.utils.nestedset import get_root_of
from frappe.utils import cint
from erpnext.accounts.doctype.pos_profile.pos_profile import get_item_groups
+from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability
from six import string_types
@@ -43,6 +44,7 @@
SELECT
name AS item_code,
item_name,
+ description,
stock_uom,
image AS item_image,
idx AS idx,
@@ -53,10 +55,11 @@
disabled = 0
AND has_variants = 0
AND is_sales_item = 1
+ AND is_fixed_asset = 0
AND item_group in (SELECT name FROM `tabItem Group` WHERE lft >= {lft} AND rgt <= {rgt})
AND {condition}
ORDER BY
- idx desc
+ name asc
LIMIT
{start}, {page_length}"""
.format(
@@ -73,32 +76,14 @@
fields = ["item_code", "price_list_rate", "currency"],
filters = {'price_list': price_list, 'item_code': ['in', items]})
- item_prices, bin_data = {}, {}
+ item_prices = {}
for d in item_prices_data:
item_prices[d.item_code] = d
- # prepare filter for bin query
- bin_filters = {'item_code': ['in', items]}
- if warehouse:
- bin_filters['warehouse'] = warehouse
- if display_items_in_stock:
- bin_filters['actual_qty'] = [">", 0]
-
- # query item bin
- bin_data = frappe.get_all(
- 'Bin', fields=['item_code', 'sum(actual_qty) as actual_qty'],
- filters=bin_filters, group_by='item_code'
- )
-
- # convert list of dict into dict as {item_code: actual_qty}
- bin_dict = {}
- for b in bin_data:
- bin_dict[b.get('item_code')] = b.get('actual_qty')
-
for item in items_data:
item_code = item.item_code
item_price = item_prices.get(item_code) or {}
- item_stock_qty = bin_dict.get(item_code)
+ item_stock_qty = get_stock_availability(item_code, warehouse)
if display_items_in_stock and not item_stock_qty:
pass
@@ -116,6 +101,13 @@
'items': result
}
+ if len(res['items']) == 1:
+ res['items'][0].setdefault('serial_no', serial_no)
+ res['items'][0].setdefault('batch_no', batch_no)
+ res['items'][0].setdefault('barcode', barcode)
+
+ return res
+
if serial_no:
res.update({
'serial_no': serial_no
@@ -186,6 +178,73 @@
{'txt': '%%%s%%' % txt})
@frappe.whitelist()
-def get_pos_fields():
- return frappe.get_all("POS Field", fields=["label", "fieldname",
- "fieldtype", "default_value", "reqd", "read_only", "options"])
+def check_opening_entry(user):
+ open_vouchers = frappe.db.get_all("POS Opening Entry",
+ filters = {
+ "user": user,
+ "pos_closing_entry": ["in", ["", None]],
+ "docstatus": 1
+ },
+ fields = ["name", "company", "pos_profile", "period_start_date"],
+ order_by = "period_start_date desc"
+ )
+
+ return open_vouchers
+
+@frappe.whitelist()
+def create_opening_voucher(pos_profile, company, balance_details):
+ import json
+ balance_details = json.loads(balance_details)
+
+ new_pos_opening = frappe.get_doc({
+ 'doctype': 'POS Opening Entry',
+ "period_start_date": frappe.utils.get_datetime(),
+ "posting_date": frappe.utils.getdate(),
+ "user": frappe.session.user,
+ "pos_profile": pos_profile,
+ "company": company,
+ })
+ new_pos_opening.set("balance_details", balance_details)
+ new_pos_opening.submit()
+
+ return new_pos_opening.as_dict()
+
+@frappe.whitelist()
+def get_past_order_list(search_term, status, limit=20):
+ fields = ['name', 'grand_total', 'currency', 'customer', 'posting_time', 'posting_date']
+ invoice_list = []
+
+ if search_term and status:
+ invoices_by_customer = frappe.db.get_all('POS Invoice', filters={
+ 'customer': ['like', '%{}%'.format(search_term)],
+ 'status': status
+ }, fields=fields)
+ invoices_by_name = frappe.db.get_all('POS Invoice', filters={
+ 'name': ['like', '%{}%'.format(search_term)],
+ 'status': status
+ }, fields=fields)
+
+ invoice_list = invoices_by_customer + invoices_by_name
+ elif status:
+ invoice_list = frappe.db.get_all('POS Invoice', filters={
+ 'status': status
+ }, fields=fields)
+
+ return invoice_list
+
+@frappe.whitelist()
+def set_customer_info(fieldname, customer, value=""):
+ if fieldname == 'loyalty_program':
+ frappe.db.set_value('Customer', customer, 'loyalty_program', value)
+
+ contact = frappe.get_cached_value('Customer', customer, 'customer_primary_contact')
+
+ if contact:
+ contact_doc = frappe.get_doc('Contact', contact)
+ if fieldname == 'email_id':
+ contact_doc.set('email_ids', [{ 'email_id': value, 'is_primary': 1}])
+ frappe.db.set_value('Customer', customer, 'email_id', value)
+ elif fieldname == 'mobile_no':
+ contact_doc.set('phone_nos', [{ 'phone': value, 'is_primary_mobile_no': 1}])
+ frappe.db.set_value('Customer', customer, 'mobile_no', value)
+ contact_doc.save()
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
new file mode 100644
index 0000000..483ef78
--- /dev/null
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -0,0 +1,714 @@
+{% include "erpnext/selling/page/point_of_sale/onscan.js" %}
+{% include "erpnext/selling/page/point_of_sale/pos_item_selector.js" %}
+{% include "erpnext/selling/page/point_of_sale/pos_item_cart.js" %}
+{% include "erpnext/selling/page/point_of_sale/pos_item_details.js" %}
+{% include "erpnext/selling/page/point_of_sale/pos_payment.js" %}
+{% include "erpnext/selling/page/point_of_sale/pos_number_pad.js" %}
+{% include "erpnext/selling/page/point_of_sale/pos_past_order_list.js" %}
+{% include "erpnext/selling/page/point_of_sale/pos_past_order_summary.js" %}
+
+erpnext.PointOfSale.Controller = class {
+ constructor(wrapper) {
+ this.wrapper = $(wrapper).find('.layout-main-section');
+ this.page = wrapper.page;
+
+ this.load_assets();
+ }
+
+ load_assets() {
+ // after loading assets first check if opening entry has been made
+ frappe.require(['assets/erpnext/css/pos.css'], this.check_opening_entry.bind(this));
+ }
+
+ check_opening_entry() {
+ return frappe.call("erpnext.selling.page.point_of_sale.point_of_sale.check_opening_entry", { "user": frappe.session.user })
+ .then((r) => {
+ if (r.message.length) {
+ // assuming only one opening voucher is available for the current user
+ this.prepare_app_defaults(r.message[0]);
+ } else {
+ this.create_opening_voucher();
+ }
+ });
+ }
+
+ create_opening_voucher() {
+ const table_fields = [
+ { fieldname: "mode_of_payment", fieldtype: "Link", in_list_view: 1, label: "Mode of Payment", options: "Mode of Payment", reqd: 1 },
+ { fieldname: "opening_amount", fieldtype: "Currency", in_list_view: 1, label: "Opening Amount", options: "company:company_currency", reqd: 1 }
+ ];
+
+ const dialog = new frappe.ui.Dialog({
+ title: __('Create POS Opening Entry'),
+ fields: [
+ {
+ fieldtype: 'Link', label: __('Company'), default: frappe.defaults.get_default('company'),
+ options: 'Company', fieldname: 'company', reqd: 1
+ },
+ {
+ fieldtype: 'Link', label: __('POS Profile'),
+ options: 'POS Profile', fieldname: 'pos_profile', reqd: 1,
+ onchange: () => {
+ const pos_profile = dialog.fields_dict.pos_profile.get_value();
+ const company = dialog.fields_dict.company.get_value();
+ const user = frappe.session.user
+
+ if (!pos_profile || !company || !user) return;
+
+ // auto fetch last closing entry's balance details
+ frappe.db.get_list("POS Closing Entry", {
+ filters: { company, pos_profile, user },
+ limit: 1,
+ order_by: 'period_end_date desc'
+ }).then((res) => {
+ if (!res.length) return;
+ const pos_closing_entry = res[0];
+ frappe.db.get_doc("POS Closing Entry", pos_closing_entry.name).then(({ payment_reconciliation }) => {
+ dialog.fields_dict.balance_details.df.data = [];
+ payment_reconciliation.forEach(pay => {
+ const { mode_of_payment, closing_amount } = pay;
+ dialog.fields_dict.balance_details.df.data.push({
+ mode_of_payment: mode_of_payment
+ });
+ });
+ dialog.fields_dict.balance_details.grid.refresh();
+ });
+ });
+ }
+ },
+ {
+ fieldname: "balance_details",
+ fieldtype: "Table",
+ label: "Opening Balance Details",
+ cannot_add_rows: false,
+ in_place_edit: true,
+ reqd: 1,
+ data: [],
+ fields: table_fields
+ }
+ ],
+ primary_action: ({ company, pos_profile, balance_details }) => {
+ if (!balance_details.length) {
+ frappe.show_alert({
+ message: __("Please add Mode of payments and opening balance details."),
+ indicator: 'red'
+ })
+ frappe.utils.play_sound("error");
+ return;
+ }
+ frappe.dom.freeze();
+ return frappe.call("erpnext.selling.page.point_of_sale.point_of_sale.create_opening_voucher",
+ { pos_profile, company, balance_details })
+ .then((r) => {
+ frappe.dom.unfreeze();
+ dialog.hide();
+ if (r.message) {
+ this.prepare_app_defaults(r.message);
+ }
+ })
+ },
+ primary_action_label: __('Submit')
+ });
+ dialog.show();
+ }
+
+ prepare_app_defaults(data) {
+ this.pos_opening = data.name;
+ this.company = data.company;
+ this.pos_profile = data.pos_profile;
+ this.pos_opening_time = data.period_start_date;
+
+ frappe.db.get_value('Stock Settings', undefined, 'allow_negative_stock').then(({ message }) => {
+ this.allow_negative_stock = flt(message.allow_negative_stock) || false;
+ });
+
+ frappe.db.get_doc("POS Profile", this.pos_profile).then((profile) => {
+ this.customer_groups = profile.customer_groups.map(group => group.customer_group);
+ this.cart.make_customer_selector();
+ });
+
+ this.item_stock_map = {};
+
+ this.make_app();
+ }
+
+ set_opening_entry_status() {
+ this.page.set_title_sub(
+ `<span class="indicator orange">
+ <a class="text-muted" href="#Form/POS%20Opening%20Entry/${this.pos_opening}">
+ Opened at ${moment(this.pos_opening_time).format("Do MMMM, h:mma")}
+ </a>
+ </span>`);
+ }
+
+ make_app() {
+ return frappe.run_serially([
+ () => frappe.dom.freeze(),
+ () => {
+ this.set_opening_entry_status();
+ this.prepare_dom();
+ this.prepare_components();
+ this.prepare_menu();
+ },
+ () => this.make_new_invoice(),
+ () => frappe.dom.unfreeze(),
+ () => this.page.set_title(__('Point of Sale Beta')),
+ ]);
+ }
+
+ prepare_dom() {
+ this.wrapper.append(`
+ <div class="app grid grid-cols-10 pt-8 gap-6"></div>`
+ );
+
+ this.$components_wrapper = this.wrapper.find('.app');
+ }
+
+ prepare_components() {
+ this.init_item_selector();
+ this.init_item_details();
+ this.init_item_cart();
+ this.init_payments();
+ this.init_recent_order_list();
+ this.init_order_summary();
+ }
+
+ prepare_menu() {
+ var me = this;
+ this.page.clear_menu();
+
+ this.page.add_menu_item(__("Form View"), function () {
+ frappe.model.sync(me.frm.doc);
+ frappe.set_route("Form", me.frm.doc.doctype, me.frm.doc.name);
+ });
+
+ this.page.add_menu_item(__("Toggle Recent Orders"), () => {
+ const show = this.recent_order_list.$component.hasClass('d-none');
+ this.toggle_recent_order_list(show);
+ });
+
+ this.page.add_menu_item(__("Save as Draft"), this.save_draft_invoice.bind(this));
+
+ frappe.ui.keys.on("ctrl+s", this.save_draft_invoice.bind(this));
+
+ this.page.add_menu_item(__('Close the POS'), this.close_pos.bind(this));
+
+ frappe.ui.keys.on("shift+ctrl+s", this.close_pos.bind(this));
+ }
+
+ save_draft_invoice() {
+ if (!this.$components_wrapper.is(":visible")) return;
+
+ if (this.frm.doc.items.length == 0) {
+ frappe.show_alert({
+ message:__("You must add atleast one item to save it as draft."),
+ indicator:'red'
+ });
+ frappe.utils.play_sound("error");
+ return;
+ }
+
+ this.frm.save(undefined, undefined, undefined, () => {
+ frappe.show_alert({
+ message:__("There was an error saving the document."),
+ indicator:'red'
+ });
+ frappe.utils.play_sound("error");
+ }).then(() => {
+ frappe.run_serially([
+ () => frappe.dom.freeze(),
+ () => this.make_new_invoice(),
+ () => frappe.dom.unfreeze(),
+ ]);
+ })
+ }
+
+ close_pos() {
+ if (!this.$components_wrapper.is(":visible")) return;
+
+ let voucher = frappe.model.get_new_doc('POS Closing Entry');
+ voucher.pos_profile = this.frm.doc.pos_profile;
+ voucher.user = frappe.session.user;
+ voucher.company = this.frm.doc.company;
+ voucher.pos_opening_entry = this.pos_opening;
+ voucher.period_end_date = frappe.datetime.now_datetime();
+ voucher.posting_date = frappe.datetime.now_date();
+ frappe.set_route('Form', 'POS Closing Entry', voucher.name);
+ }
+
+ init_item_selector() {
+ this.item_selector = new erpnext.PointOfSale.ItemSelector({
+ wrapper: this.$components_wrapper,
+ pos_profile: this.pos_profile,
+ events: {
+ item_selected: args => this.on_cart_update(args),
+
+ get_frm: () => this.frm || {},
+
+ get_allowed_item_group: () => this.item_groups
+ }
+ })
+ }
+
+ init_item_cart() {
+ this.cart = new erpnext.PointOfSale.ItemCart({
+ wrapper: this.$components_wrapper,
+ events: {
+ get_frm: () => this.frm,
+
+ cart_item_clicked: (item_code, batch_no, uom) => {
+ const item_row = this.frm.doc.items.find(
+ i => i.item_code === item_code
+ && i.uom === uom
+ && (!batch_no || (batch_no && i.batch_no === batch_no))
+ );
+ this.item_details.toggle_item_details_section(item_row);
+ },
+
+ numpad_event: (value, action) => this.update_item_field(value, action),
+
+ checkout: () => this.payment.checkout(),
+
+ edit_cart: () => this.payment.edit_cart(),
+
+ customer_details_updated: (details) => {
+ this.customer_details = details;
+ // will add/remove LP payment method
+ this.payment.render_loyalty_points_payment_mode();
+ },
+
+ get_allowed_customer_group: () => this.customer_groups
+ }
+ })
+ }
+
+ init_item_details() {
+ this.item_details = new erpnext.PointOfSale.ItemDetails({
+ wrapper: this.$components_wrapper,
+ events: {
+ get_frm: () => this.frm,
+
+ toggle_item_selector: (minimize) => {
+ this.item_selector.resize_selector(minimize);
+ this.cart.toggle_numpad(minimize);
+ },
+
+ form_updated: async (cdt, cdn, fieldname, value) => {
+ const item_row = frappe.model.get_doc(cdt, cdn);
+ if (item_row && item_row[fieldname] != value) {
+
+ if (fieldname === 'qty' && flt(value) == 0) {
+ this.remove_item_from_cart();
+ return;
+ }
+
+ const { item_code, batch_no, uom } = this.item_details.current_item;
+ const event = {
+ field: fieldname,
+ value,
+ item: { item_code, batch_no, uom }
+ }
+ return this.on_cart_update(event)
+ }
+ },
+
+ item_field_focused: (fieldname) => {
+ this.cart.toggle_numpad_field_edit(fieldname);
+ },
+ set_value_in_current_cart_item: (selector, value) => {
+ this.cart.update_selector_value_in_cart_item(selector, value, this.item_details.current_item);
+ },
+ clone_new_batch_item_in_frm: (batch_serial_map, current_item) => {
+ // called if serial nos are 'auto_selected' and if those serial nos belongs to multiple batches
+ // for each unique batch new item row is added in the form & cart
+ Object.keys(batch_serial_map).forEach(batch => {
+ const { item_code, batch_no } = current_item;
+ const item_to_clone = this.frm.doc.items.find(i => i.item_code === item_code && i.batch_no === batch_no);
+ const new_row = this.frm.add_child("items", { ...item_to_clone });
+ // update new serialno and batch
+ new_row.batch_no = batch;
+ new_row.serial_no = batch_serial_map[batch].join(`\n`);
+ new_row.qty = batch_serial_map[batch].length;
+ this.frm.doc.items.forEach(row => {
+ if (item_code === row.item_code) {
+ this.update_cart_html(row);
+ }
+ });
+ })
+ },
+ remove_item_from_cart: () => this.remove_item_from_cart(),
+ get_item_stock_map: () => this.item_stock_map,
+ close_item_details: () => {
+ this.item_details.toggle_item_details_section(undefined);
+ this.cart.prev_action = undefined;
+ this.cart.toggle_item_highlight();
+ },
+ get_available_stock: (item_code, warehouse) => this.get_available_stock(item_code, warehouse)
+ }
+ });
+ }
+
+ init_payments() {
+ this.payment = new erpnext.PointOfSale.Payment({
+ wrapper: this.$components_wrapper,
+ events: {
+ get_frm: () => this.frm || {},
+
+ get_customer_details: () => this.customer_details || {},
+
+ toggle_other_sections: (show) => {
+ if (show) {
+ this.item_details.$component.hasClass('d-none') ? '' : this.item_details.$component.addClass('d-none');
+ this.item_selector.$component.addClass('d-none');
+ } else {
+ this.item_selector.$component.removeClass('d-none');
+ }
+ },
+
+ submit_invoice: () => {
+ this.frm.savesubmit()
+ .then((r) => {
+ // this.set_invoice_status();
+ this.toggle_components(false);
+ this.order_summary.toggle_component(true);
+ this.order_summary.load_summary_of(this.frm.doc, true);
+ frappe.show_alert({
+ indicator: 'green',
+ message: __(`POS invoice ${r.doc.name} created succesfully`)
+ });
+ });
+ }
+ }
+ });
+ }
+
+ init_recent_order_list() {
+ this.recent_order_list = new erpnext.PointOfSale.PastOrderList({
+ wrapper: this.$components_wrapper,
+ events: {
+ open_invoice_data: (name) => {
+ frappe.db.get_doc('POS Invoice', name).then((doc) => {
+ this.order_summary.load_summary_of(doc);
+ });
+ },
+ reset_summary: () => this.order_summary.show_summary_placeholder()
+ }
+ })
+ }
+
+ init_order_summary() {
+ this.order_summary = new erpnext.PointOfSale.PastOrderSummary({
+ wrapper: this.$components_wrapper,
+ events: {
+ get_frm: () => this.frm,
+
+ process_return: (name) => {
+ this.recent_order_list.toggle_component(false);
+ frappe.db.get_doc('POS Invoice', name).then((doc) => {
+ frappe.run_serially([
+ () => this.make_return_invoice(doc),
+ () => this.cart.load_invoice(),
+ () => this.item_selector.toggle_component(true)
+ ]);
+ });
+ },
+ edit_order: (name) => {
+ this.recent_order_list.toggle_component(false);
+ frappe.run_serially([
+ () => this.frm.refresh(name),
+ () => this.cart.load_invoice(),
+ () => this.item_selector.toggle_component(true)
+ ]);
+ },
+ new_order: () => {
+ frappe.run_serially([
+ () => frappe.dom.freeze(),
+ () => this.make_new_invoice(),
+ () => this.item_selector.toggle_component(true),
+ () => frappe.dom.unfreeze(),
+ ]);
+ }
+ }
+ })
+ }
+
+
+
+ toggle_recent_order_list(show) {
+ this.toggle_components(!show);
+ this.recent_order_list.toggle_component(show);
+ this.order_summary.toggle_component(show);
+ }
+
+ toggle_components(show) {
+ this.cart.toggle_component(show);
+ this.item_selector.toggle_component(show);
+
+ // do not show item details or payment if recent order is toggled off
+ !show ? (this.item_details.toggle_component(false) || this.payment.toggle_component(false)) : '';
+ }
+
+ make_new_invoice() {
+ return frappe.run_serially([
+ () => this.make_sales_invoice_frm(),
+ () => this.set_pos_profile_data(),
+ () => this.set_pos_profile_status(),
+ () => this.cart.load_invoice(),
+ ]);
+ }
+
+ make_sales_invoice_frm() {
+ const doctype = 'POS Invoice';
+ return new Promise(resolve => {
+ if (this.frm) {
+ this.frm = this.get_new_frm(this.frm);
+ this.frm.doc.items = [];
+ this.frm.doc.is_pos = 1
+ resolve();
+ } else {
+ frappe.model.with_doctype(doctype, () => {
+ this.frm = this.get_new_frm();
+ this.frm.doc.items = [];
+ this.frm.doc.is_pos = 1
+ resolve();
+ });
+ }
+ });
+ }
+
+ get_new_frm(_frm) {
+ const doctype = 'POS Invoice';
+ const page = $('<div>');
+ const frm = _frm || new frappe.ui.form.Form(doctype, page, false);
+ const name = frappe.model.make_new_doc_and_get_name(doctype, true);
+ frm.refresh(name);
+
+ return frm;
+ }
+
+ async make_return_invoice(doc) {
+ frappe.dom.freeze();
+ this.frm = this.get_new_frm(this.frm);
+ this.frm.doc.items = [];
+ const res = await frappe.call({
+ method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return",
+ args: {
+ 'source_name': doc.name,
+ 'target_doc': this.frm.doc
+ }
+ });
+ frappe.model.sync(res.message);
+ await this.set_pos_profile_data();
+ frappe.dom.unfreeze();
+ }
+
+ set_pos_profile_data() {
+ if (this.company && !this.frm.doc.company) this.frm.doc.company = this.company;
+ if (this.pos_profile && !this.frm.doc.pos_profile) this.frm.doc.pos_profile = this.pos_profile;
+ if (!this.frm.doc.company) return;
+
+ return new Promise(resolve => {
+ return this.frm.call({
+ doc: this.frm.doc,
+ method: "set_missing_values",
+ }).then((r) => {
+ if(!r.exc) {
+ if (!this.frm.doc.pos_profile) {
+ frappe.dom.unfreeze();
+ this.raise_exception_for_pos_profile();
+ }
+ this.frm.trigger("update_stock");
+ this.frm.trigger('calculate_taxes_and_totals');
+ if(this.frm.doc.taxes_and_charges) this.frm.script_manager.trigger("taxes_and_charges");
+ frappe.model.set_default_values(this.frm.doc);
+ if (r.message) {
+ this.frm.pos_print_format = r.message.print_format || "";
+ this.frm.meta.default_print_format = r.message.print_format || "";
+ this.frm.allow_edit_rate = r.message.allow_edit_rate;
+ this.frm.allow_edit_discount = r.message.allow_edit_discount;
+ this.frm.doc.campaign = r.message.campaign;
+ }
+ }
+ resolve();
+ });
+ });
+ }
+
+ raise_exception_for_pos_profile() {
+ setTimeout(() => frappe.set_route('List', 'POS Profile'), 2000);
+ frappe.throw(__("POS Profile is required to use Point-of-Sale"));
+ }
+
+ set_invoice_status() {
+ const [status, indicator] = frappe.listview_settings["POS Invoice"].get_indicator(this.frm.doc);
+ this.page.set_indicator(__(`${status}`), indicator);
+ }
+
+ set_pos_profile_status() {
+ this.page.set_indicator(__(`${this.pos_profile}`), "blue");
+ }
+
+ async on_cart_update(args) {
+ frappe.dom.freeze();
+ try {
+ let { field, value, item } = args;
+ const { item_code, batch_no, serial_no, uom } = item;
+ let item_row = this.get_item_from_frm(item_code, batch_no, uom);
+
+ const item_selected_from_selector = field === 'qty' && value === "+1"
+
+ if (item_row) {
+ item_selected_from_selector && (value = item_row.qty + flt(value))
+
+ field === 'qty' && (value = flt(value));
+
+ if (field === 'qty' && value > 0 && !this.allow_negative_stock)
+ await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse);
+
+ if (this.is_current_item_being_edited(item_row) || item_selected_from_selector) {
+ await frappe.model.set_value(item_row.doctype, item_row.name, field, value);
+ this.update_cart_html(item_row);
+ }
+
+ } else {
+ if (!this.frm.doc.customer) {
+ frappe.dom.unfreeze();
+ frappe.show_alert({
+ message: __('You must select a customer before adding an item.'),
+ indicator: 'orange'
+ });
+ frappe.utils.play_sound("error");
+ return;
+ }
+ item_selected_from_selector && (value = flt(value))
+
+ const args = { item_code, batch_no, [field]: value };
+
+ if (serial_no) args['serial_no'] = serial_no;
+
+ if (field === 'serial_no') args['qty'] = value.split(`\n`).length || 0;
+
+ item_row = this.frm.add_child('items', args);
+
+ if (field === 'qty' && value !== 0 && !this.allow_negative_stock)
+ await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse);
+
+ await this.trigger_new_item_events(item_row);
+
+ this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row);
+ this.update_cart_html(item_row);
+ }
+ } catch (error) {
+ console.log(error);
+ } finally {
+ frappe.dom.unfreeze();
+ }
+ }
+
+ get_item_from_frm(item_code, batch_no, uom) {
+ const has_batch_no = batch_no;
+ return this.frm.doc.items.find(
+ i => i.item_code === item_code
+ && (!has_batch_no || (has_batch_no && i.batch_no === batch_no))
+ && (i.uom === uom)
+ );
+ }
+
+ edit_item_details_of(item_row) {
+ this.item_details.toggle_item_details_section(item_row);
+ }
+
+ is_current_item_being_edited(item_row) {
+ const { item_code, batch_no } = this.item_details.current_item;
+
+ return item_code !== item_row.item_code || batch_no != item_row.batch_no ? false : true;
+ }
+
+ update_cart_html(item_row, remove_item) {
+ this.cart.update_item_html(item_row, remove_item);
+ this.cart.update_totals_section(this.frm);
+ }
+
+ check_serial_batch_selection_needed(item_row) {
+ // right now item details is shown for every type of item.
+ // if item details is not shown for every item then this fn will be needed
+ const serialized = item_row.has_serial_no;
+ const batched = item_row.has_batch_no;
+ const no_serial_selected = !item_row.serial_no;
+ const no_batch_selected = !item_row.batch_no;
+
+ if ((serialized && no_serial_selected) || (batched && no_batch_selected) ||
+ (serialized && batched && (no_batch_selected || no_serial_selected))) {
+ return true;
+ }
+ return false;
+ }
+
+ async trigger_new_item_events(item_row) {
+ await this.frm.script_manager.trigger('item_code', item_row.doctype, item_row.name)
+ await this.frm.script_manager.trigger('qty', item_row.doctype, item_row.name)
+ }
+
+ async check_stock_availability(item_row, qty_needed, warehouse) {
+ const available_qty = (await this.get_available_stock(item_row.item_code, warehouse)).message;
+
+ frappe.dom.unfreeze();
+ if (!(available_qty > 0)) {
+ frappe.model.clear_doc(item_row.doctype, item_row.name);
+ frappe.throw(__(`Item Code: ${item_row.item_code.bold()} is not available under warehouse ${warehouse.bold()}.`))
+ } else if (available_qty < qty_needed) {
+ frappe.show_alert({
+ message: __(`Stock quantity not enough for Item Code: ${item_row.item_code.bold()} under warehouse ${warehouse.bold()}.
+ Available quantity ${available_qty.toString().bold()}.`),
+ indicator: 'orange'
+ });
+ frappe.utils.play_sound("error");
+ this.item_details.qty_control.set_value(flt(available_qty));
+ }
+ frappe.dom.freeze();
+ }
+
+ get_available_stock(item_code, warehouse) {
+ const me = this;
+ return frappe.call({
+ method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.get_stock_availability",
+ args: {
+ 'item_code': item_code,
+ 'warehouse': warehouse,
+ },
+ callback(res) {
+ if (!me.item_stock_map[item_code])
+ me.item_stock_map[item_code] = {}
+ me.item_stock_map[item_code][warehouse] = res.message;
+ }
+ });
+ }
+
+ update_item_field(value, field_or_action) {
+ if (field_or_action === 'checkout') {
+ this.item_details.toggle_item_details_section(undefined);
+ } else if (field_or_action === 'remove') {
+ this.remove_item_from_cart();
+ } else {
+ const field_control = this.item_details[`${field_or_action}_control`];
+ if (!field_control) return;
+ field_control.set_focus();
+ value != "" && field_control.set_value(value);
+ }
+ }
+
+ remove_item_from_cart() {
+ frappe.dom.freeze();
+ const { doctype, name, current_item } = this.item_details;
+
+ frappe.model.set_value(doctype, name, 'qty', 0);
+
+ this.frm.script_manager.trigger('qty', doctype, name).then(() => {
+ frappe.model.clear_doc(doctype, name);
+ this.update_cart_html(current_item, true);
+ this.item_details.toggle_item_details_section(undefined);
+ frappe.dom.unfreeze();
+ })
+ }
+}
+
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
new file mode 100644
index 0000000..c23a6ad
--- /dev/null
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -0,0 +1,951 @@
+erpnext.PointOfSale.ItemCart = class {
+ constructor({ wrapper, events }) {
+ this.wrapper = wrapper;
+ this.events = events;
+ this.customer_info = undefined;
+
+ this.init_component();
+ }
+
+ init_component() {
+ this.prepare_dom();
+ this.init_child_components();
+ this.bind_events();
+ this.attach_shortcuts();
+ }
+
+ prepare_dom() {
+ this.wrapper.append(
+ `<section class="col-span-4 flex flex-col shadow rounded item-cart bg-white mx-h-70 h-100"></section>`
+ )
+
+ this.$component = this.wrapper.find('.item-cart');
+ }
+
+ init_child_components() {
+ this.init_customer_selector();
+ this.init_cart_components();
+ }
+
+ init_customer_selector() {
+ this.$component.append(
+ `<div class="customer-section rounded flex flex-col m-8 mb-0"></div>`
+ )
+ this.$customer_section = this.$component.find('.customer-section');
+ }
+
+ reset_customer_selector() {
+ const frm = this.events.get_frm();
+ frm.set_value('customer', '');
+ this.$customer_section.removeClass('border pr-4 pl-4');
+ this.make_customer_selector();
+ this.customer_field.set_focus();
+ }
+
+ init_cart_components() {
+ this.$component.append(
+ `<div class="cart-container flex flex-col items-center rounded flex-1 relative">
+ <div class="absolute flex flex-col p-8 pt-0 w-full h-full">
+ <div class="flex text-grey cart-header pt-2 pb-2 p-4 mt-2 mb-2 w-full f-shrink-0">
+ <div class="flex-1">Item</div>
+ <div class="mr-4">Qty</div>
+ <div class="rate-list-header mr-1 text-right">Amount</div>
+ </div>
+ <div class="cart-items-section flex flex-col flex-1 scroll-y rounded w-full"></div>
+ <div class="cart-totals-section flex flex-col w-full mt-4 f-shrink-0"></div>
+ <div class="numpad-section flex flex-col mt-4 d-none w-full p-8 pt-0 pb-0 f-shrink-0"></div>
+ </div>
+ </div>`
+ );
+ this.$cart_container = this.$component.find('.cart-container');
+
+ this.make_cart_totals_section();
+ this.make_cart_items_section();
+ this.make_cart_numpad();
+ }
+
+ make_cart_items_section() {
+ this.$cart_header = this.$component.find('.cart-header');
+ this.$cart_items_wrapper = this.$component.find('.cart-items-section');
+
+ this.make_no_items_placeholder();
+ }
+
+ make_no_items_placeholder() {
+ this.$cart_header.addClass('d-none');
+ this.$cart_items_wrapper.html(
+ `<div class="no-item-wrapper flex items-center h-18">
+ <div class="flex-1 text-center text-grey">No items in cart</div>
+ </div>`
+ )
+ this.$cart_items_wrapper.addClass('mt-4 border-grey border-dashed');
+ }
+
+ make_cart_totals_section() {
+ this.$totals_section = this.$component.find('.cart-totals-section');
+
+ this.$totals_section.append(
+ `<div class="add-discount flex items-center pt-4 pb-4 pr-4 pl-4 text-grey pointer no-select d-none">
+ + Add Discount
+ </div>
+ <div class="border border-grey rounded">
+ <div class="net-total flex justify-between items-center h-16 pr-8 pl-8 border-b-grey">
+ <div class="flex flex-col">
+ <div class="text-md text-dark-grey text-bold">Net Total</div>
+ </div>
+ <div class="flex flex-col text-right">
+ <div class="text-md text-dark-grey text-bold">0.00</div>
+ </div>
+ </div>
+ <div class="taxes"></div>
+ <div class="grand-total flex justify-between items-center h-16 pr-8 pl-8 border-b-grey">
+ <div class="flex flex-col">
+ <div class="text-md text-dark-grey text-bold">Grand Total</div>
+ </div>
+ <div class="flex flex-col text-right">
+ <div class="text-md text-dark-grey text-bold">0.00</div>
+ </div>
+ </div>
+ <div class="checkout-btn flex items-center justify-center h-16 pr-8 pl-8 text-center text-grey no-select pointer rounded-b text-md text-bold">
+ Checkout
+ </div>
+ <div class="edit-cart-btn flex items-center justify-center h-16 pr-8 pl-8 text-center text-grey no-select pointer d-none text-md text-bold">
+ Edit Cart
+ </div>
+ </div>`
+ )
+
+ this.$add_discount_elem = this.$component.find(".add-discount");
+ }
+
+ make_cart_numpad() {
+ this.$numpad_section = this.$component.find('.numpad-section');
+
+ this.number_pad = new erpnext.PointOfSale.NumberPad({
+ wrapper: this.$numpad_section,
+ events: {
+ numpad_event: this.on_numpad_event.bind(this)
+ },
+ cols: 5,
+ keys: [
+ [ 1, 2, 3, 'Quantity' ],
+ [ 4, 5, 6, 'Discount' ],
+ [ 7, 8, 9, 'Rate' ],
+ [ '.', 0, 'Delete', 'Remove' ]
+ ],
+ css_classes: [
+ [ '', '', '', 'col-span-2' ],
+ [ '', '', '', 'col-span-2' ],
+ [ '', '', '', 'col-span-2' ],
+ [ '', '', '', 'col-span-2 text-bold text-danger' ]
+ ],
+ fieldnames_map: { 'Quantity': 'qty', 'Discount': 'discount_percentage' }
+ })
+
+ this.$numpad_section.prepend(
+ `<div class="flex mb-2 justify-between">
+ <span class="numpad-net-total"></span>
+ <span class="numpad-grand-total"></span>
+ </div>`
+ )
+
+ this.$numpad_section.append(
+ `<div class="numpad-btn checkout-btn flex items-center justify-center h-16 pr-8 pl-8 bg-primary
+ text-center text-white no-select pointer rounded text-md text-bold mt-4" data-button-value="checkout">
+ Checkout
+ </div>`
+ )
+ }
+
+ bind_events() {
+ const me = this;
+ this.$customer_section.on('click', '.add-remove-customer', function (e) {
+ const customer_info_is_visible = me.$cart_container.hasClass('d-none');
+ customer_info_is_visible ?
+ me.toggle_customer_info(false) : me.reset_customer_selector();
+ });
+
+ this.$customer_section.on('click', '.customer-header', function(e) {
+ // don't triggger the event if .add-remove-customer btn is clicked which is under .customer-header
+ if ($(e.target).closest('.add-remove-customer').length) return;
+
+ const show = !me.$cart_container.hasClass('d-none');
+ me.toggle_customer_info(show);
+ });
+
+ this.$cart_items_wrapper.on('click', '.cart-item-wrapper', function() {
+ const $cart_item = $(this);
+
+ me.toggle_item_highlight(this);
+
+ const payment_section_hidden = me.$totals_section.find('.edit-cart-btn').hasClass('d-none');
+ if (!payment_section_hidden) {
+ // payment section is visible
+ // edit cart first and then open item details section
+ me.$totals_section.find(".edit-cart-btn").click();
+ }
+
+ const item_code = unescape($cart_item.attr('data-item-code'));
+ const batch_no = unescape($cart_item.attr('data-batch-no'));
+ const uom = unescape($cart_item.attr('data-uom'));
+ me.events.cart_item_clicked(item_code, batch_no, uom);
+ this.numpad_value = '';
+ });
+
+ this.$component.on('click', '.checkout-btn', function() {
+ if (!$(this).hasClass('bg-primary')) return;
+
+ me.events.checkout();
+ me.toggle_checkout_btn(false);
+
+ me.$add_discount_elem.removeClass("d-none");
+ });
+
+ this.$totals_section.on('click', '.edit-cart-btn', () => {
+ this.events.edit_cart();
+ this.toggle_checkout_btn(true);
+
+ this.$add_discount_elem.addClass("d-none");
+ });
+
+ this.$component.on('click', '.add-discount', () => {
+ const can_edit_discount = this.$add_discount_elem.find('.edit-discount').length;
+
+ if(!this.discount_field || can_edit_discount) this.show_discount_control();
+ });
+
+ frappe.ui.form.on("POS Invoice", "paid_amount", frm => {
+ // called when discount is applied
+ this.update_totals_section(frm);
+ });
+ }
+
+ attach_shortcuts() {
+ for (let row of this.number_pad.keys) {
+ for (let btn of row) {
+ let shortcut_key = `ctrl+${frappe.scrub(String(btn))[0]}`;
+ if (btn === 'Delete') shortcut_key = 'ctrl+backspace';
+ if (btn === 'Remove') shortcut_key = 'shift+ctrl+backspace'
+ if (btn === '.') shortcut_key = 'ctrl+>';
+
+ // to account for fieldname map
+ const fieldname = this.number_pad.fieldnames[btn] ? this.number_pad.fieldnames[btn] :
+ typeof btn === 'string' ? frappe.scrub(btn) : btn;
+
+ frappe.ui.keys.on(`${shortcut_key}`, () => {
+ const cart_is_visible = this.$component.is(":visible");
+ if (cart_is_visible && this.item_is_selected && this.$numpad_section.is(":visible")) {
+ this.$numpad_section.find(`.numpad-btn[data-button-value="${fieldname}"]`).click();
+ }
+ })
+ }
+ }
+
+ frappe.ui.keys.on("ctrl+enter", () => {
+ const cart_is_visible = this.$component.is(":visible");
+ const payment_section_hidden = this.$totals_section.find('.edit-cart-btn').hasClass('d-none');
+ if (cart_is_visible && payment_section_hidden) {
+ this.$component.find(".checkout-btn").click();
+ }
+ });
+ }
+
+ toggle_item_highlight(item) {
+ const $cart_item = $(item);
+ const item_is_highlighted = $cart_item.hasClass("shadow");
+
+ if (!item || item_is_highlighted) {
+ this.item_is_selected = false;
+ this.$cart_container.find('.cart-item-wrapper').removeClass("shadow").css("opacity", "1");
+ } else {
+ $cart_item.addClass("shadow");
+ this.item_is_selected = true;
+ this.$cart_container.find('.cart-item-wrapper').css("opacity", "1");
+ this.$cart_container.find('.cart-item-wrapper').not(item).removeClass("shadow").css("opacity", "0.65");
+ }
+ // highlight with inner shadow
+ // $cart_item.addClass("shadow-inner bg-selected");
+ // me.$cart_container.find('.cart-item-wrapper').not(this).removeClass("shadow-inner bg-selected");
+ }
+
+ make_customer_selector() {
+ this.$customer_section.html(`<div class="customer-search-field flex flex-1 items-center"></div>`);
+ const me = this;
+ const query = { query: 'erpnext.controllers.queries.customer_query' };
+ const allowed_customer_group = this.events.get_allowed_customer_group() || [];
+ if (allowed_customer_group.length) {
+ query.filters = {
+ customer_group: ['in', allowed_customer_group]
+ }
+ }
+ this.customer_field = frappe.ui.form.make_control({
+ df: {
+ label: __('Customer'),
+ fieldtype: 'Link',
+ options: 'Customer',
+ placeholder: __('Search by customer name, phone, email.'),
+ get_query: () => query,
+ onchange: function() {
+ if (this.value) {
+ const frm = me.events.get_frm();
+ frappe.dom.freeze();
+ frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'customer', this.value);
+ frm.script_manager.trigger('customer', frm.doc.doctype, frm.doc.name).then(() => {
+ frappe.run_serially([
+ () => me.fetch_customer_details(this.value),
+ () => me.events.customer_details_updated(me.customer_info),
+ () => me.update_customer_section(),
+ () => me.update_totals_section(),
+ () => frappe.dom.unfreeze()
+ ]);
+ })
+ }
+ },
+ },
+ parent: this.$customer_section.find('.customer-search-field'),
+ render_input: true,
+ });
+ this.customer_field.toggle_label(false);
+ }
+
+ fetch_customer_details(customer) {
+ if (customer) {
+ return new Promise((resolve) => {
+ frappe.db.get_value('Customer', customer, ["email_id", "mobile_no", "image", "loyalty_program"]).then(({ message }) => {
+ const { loyalty_program } = message;
+ // if loyalty program then fetch loyalty points too
+ if (loyalty_program) {
+ frappe.call({
+ method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_program_details_with_points",
+ args: { customer, loyalty_program, "silent": true },
+ callback: (r) => {
+ const { loyalty_points, conversion_factor } = r.message;
+ if (!r.exc) {
+ this.customer_info = { ...message, customer, loyalty_points, conversion_factor };
+ resolve();
+ }
+ }
+ });
+ } else {
+ this.customer_info = { ...message, customer };
+ resolve();
+ }
+ });
+ });
+ } else {
+ return new Promise((resolve) => {
+ this.customer_info = {}
+ resolve();
+ });
+ }
+ }
+
+ show_discount_control() {
+ this.$add_discount_elem.removeClass("pr-4 pl-4");
+ this.$add_discount_elem.html(
+ `<div class="add-dicount-field flex flex-1 items-center"></div>
+ <div class="submit-field flex items-center"></div>`
+ );
+ const me = this;
+
+ this.discount_field = frappe.ui.form.make_control({
+ df: {
+ label: __('Discount'),
+ fieldtype: 'Data',
+ placeholder: __('Enter discount percentage.'),
+ onchange: function() {
+ if (this.value || this.value == 0) {
+ const frm = me.events.get_frm();
+ frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', this.value);
+ me.hide_discount_control(this.value);
+ }
+ },
+ },
+ parent: this.$add_discount_elem.find('.add-dicount-field'),
+ render_input: true,
+ });
+ this.discount_field.toggle_label(false);
+ this.discount_field.set_focus();
+ }
+
+ hide_discount_control(discount) {
+ this.$add_discount_elem.addClass('pr-4 pl-4');
+ this.$add_discount_elem.html(
+ `<svg class="mr-2" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"
+ stroke-linecap="round" stroke-linejoin="round">
+ <path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
+ </svg>
+ <div class="edit-discount p-1 pr-3 pl-3 text-dark-grey rounded w-fit bg-green-200 mb-2">
+ ${String(discount).bold()}% off
+ </div>
+ `
+ );
+ }
+
+ update_customer_section() {
+ const { customer, email_id='', mobile_no='', image } = this.customer_info || {};
+
+ if (customer) {
+ this.$customer_section.addClass('border pr-4 pl-4').html(
+ `<div class="customer-details flex flex-col">
+ <div class="customer-header flex items-center rounded h-18 pointer">
+ ${get_customer_image()}
+ <div class="customer-name flex flex-col flex-1 f-shrink-1 overflow-hidden whitespace-nowrap">
+ <div class="text-md text-dark-grey text-bold">${customer}</div>
+ ${get_customer_description()}
+ </div>
+ <div class="f-shrink-0 add-remove-customer flex items-center pointer" data-customer="${escape(customer)}">
+ <svg width="32" height="32" viewBox="0 0 14 14" fill="none">
+ <path d="M4.93764 4.93759L7.00003 6.99998M9.06243 9.06238L7.00003 6.99998M7.00003 6.99998L4.93764 9.06238L9.06243 4.93759" stroke="#8D99A6"/>
+ </svg>
+ </div>
+ </div>
+ </div>`
+ );
+ } else {
+ // reset customer selector
+ this.reset_customer_selector();
+ }
+
+ function get_customer_description() {
+ if (!email_id && !mobile_no) {
+ return `<div class="text-grey-200 italic">Click to add email / phone</div>`
+ } else if (email_id && !mobile_no) {
+ return `<div class="text-grey">${email_id}</div>`
+ } else if (mobile_no && !email_id) {
+ return `<div class="text-grey">${mobile_no}</div>`
+ } else {
+ return `<div class="text-grey">${email_id} | ${mobile_no}</div>`
+ }
+ }
+
+ function get_customer_image() {
+ if (image) {
+ return `<div class="icon flex items-center justify-center w-12 h-12 rounded bg-light-grey mr-4 text-grey-200">
+ <img class="h-full" src="${image}" alt="${image}" style="object-fit: cover;">
+ </div>`
+ } else {
+ return `<div class="icon flex items-center justify-center w-12 h-12 rounded bg-light-grey mr-4 text-grey-200 text-md">
+ ${frappe.get_abbr(customer)}
+ </div>`
+ }
+ }
+ }
+
+ update_totals_section(frm) {
+ if (!frm) frm = this.events.get_frm();
+
+ this.render_net_total(frm.doc.base_net_total);
+ this.render_grand_total(frm.doc.base_grand_total);
+
+ const taxes = frm.doc.taxes.map(t => { return { description: t.description, rate: t.rate }})
+ this.render_taxes(frm.doc.base_total_taxes_and_charges, taxes);
+ }
+
+ render_net_total(value) {
+ const currency = this.events.get_frm().doc.currency;
+ this.$totals_section.find('.net-total').html(
+ `<div class="flex flex-col">
+ <div class="text-md text-dark-grey text-bold">Net Total</div>
+ </div>
+ <div class="flex flex-col text-right">
+ <div class="text-md text-dark-grey text-bold">${format_currency(value, currency)}</div>
+ </div>`
+ )
+
+ this.$numpad_section.find('.numpad-net-total').html(`Net Total: <span class="text-bold">${format_currency(value, currency)}</span>`)
+ }
+
+ render_grand_total(value) {
+ const currency = this.events.get_frm().doc.currency;
+ this.$totals_section.find('.grand-total').html(
+ `<div class="flex flex-col">
+ <div class="text-md text-dark-grey text-bold">Grand Total</div>
+ </div>
+ <div class="flex flex-col text-right">
+ <div class="text-md text-dark-grey text-bold">${format_currency(value, currency)}</div>
+ </div>`
+ )
+
+ this.$numpad_section.find('.numpad-grand-total').html(`Grand Total: <span class="text-bold">${format_currency(value, currency)}</span>`)
+ }
+
+ render_taxes(value, taxes) {
+ if (taxes.length) {
+ const currency = this.events.get_frm().doc.currency;
+ this.$totals_section.find('.taxes').html(
+ `<div class="flex items-center justify-between h-16 pr-8 pl-8 border-b-grey">
+ <div class="flex">
+ <div class="text-md text-dark-grey text-bold w-fit">Tax Charges</div>
+ <div class="flex ml-6 text-dark-grey">
+ ${
+ taxes.map((t, i) => {
+ let margin_left = '';
+ if (i !== 0) margin_left = 'ml-2';
+ return `<span class="border-grey p-1 pl-2 pr-2 rounded ${margin_left}">${t.description}</span>`
+ }).join('')
+ }
+ </div>
+ </div>
+ <div class="flex flex-col text-right">
+ <div class="text-md text-dark-grey text-bold">${format_currency(value, currency)}</div>
+ </div>
+ </div>`
+ )
+ } else {
+ this.$totals_section.find('.taxes').html('')
+ }
+ }
+
+ get_cart_item({ item_code, batch_no, uom }) {
+ const batch_attr = `[data-batch-no="${escape(batch_no)}"]`;
+ const item_code_attr = `[data-item-code="${escape(item_code)}"]`;
+ const uom_attr = `[data-uom=${escape(uom)}]`;
+
+ const item_selector = batch_no ?
+ `.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`;
+
+ return this.$cart_items_wrapper.find(item_selector);
+ }
+
+ update_item_html(item, remove_item) {
+ const $item = this.get_cart_item(item);
+
+ if (remove_item) {
+ $item && $item.remove();
+ } else {
+ const { item_code, batch_no, uom } = item;
+ const search_field = batch_no ? 'batch_no' : 'item_code';
+ const search_value = batch_no || item_code;
+ const item_row = this.events.get_frm().doc.items.find(i => i[search_field] === search_value && i.uom === uom);
+
+ this.render_cart_item(item_row, $item);
+ }
+
+ const no_of_cart_items = this.$cart_items_wrapper.children().length;
+ no_of_cart_items > 0 && this.highlight_checkout_btn(no_of_cart_items > 0);
+
+ this.update_empty_cart_section(no_of_cart_items);
+ }
+
+ render_cart_item(item_data, $item_to_update) {
+ const currency = this.events.get_frm().doc.currency;
+ const me = this;
+
+ if (!$item_to_update.length) {
+ this.$cart_items_wrapper.append(
+ `<div class="cart-item-wrapper flex items-center h-18 pr-4 pl-4 rounded border-grey pointer no-select"
+ data-item-code="${escape(item_data.item_code)}" data-uom="${escape(item_data.uom)}"
+ data-batch-no="${escape(item_data.batch_no || '')}">
+ </div>`
+ )
+ $item_to_update = this.get_cart_item(item_data);
+ }
+
+ $item_to_update.html(
+ `<div class="flex flex-col flex-1 f-shrink-1 overflow-hidden whitespace-nowrap">
+ <div class="text-md text-dark-grey text-bold">
+ ${item_data.item_name}
+ </div>
+ ${get_description_html()}
+ </div>
+ ${get_rate_discount_html()}
+ </div>`
+ )
+
+ set_dynamic_rate_header_width();
+ this.scroll_to_item($item_to_update);
+
+ function set_dynamic_rate_header_width() {
+ const rate_cols = Array.from(me.$cart_items_wrapper.find(".rate-col"));
+ me.$cart_header.find(".rate-list-header").css("width", "");
+ me.$cart_items_wrapper.find(".rate-col").css("width", "");
+ let max_width = rate_cols.reduce((max_width, elm) => {
+ if ($(elm).width() > max_width)
+ max_width = $(elm).width();
+ return max_width;
+ }, 0);
+
+ max_width += 1;
+ if (max_width == 1) max_width = "";
+
+ me.$cart_header.find(".rate-list-header").css("width", max_width);
+ me.$cart_items_wrapper.find(".rate-col").css("width", max_width);
+ }
+
+ function get_rate_discount_html() {
+ if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) {
+ return `
+ <div class="flex f-shrink-0 ml-4 items-center">
+ <div class="flex w-8 h-8 rounded bg-light-grey mr-4 items-center justify-center font-bold f-shrink-0">
+ <span>${item_data.qty || 0}</span>
+ </div>
+ <div class="rate-col flex flex-col f-shrink-0 text-right">
+ <div class="text-md text-dark-grey text-bold">${format_currency(item_data.amount, currency)}</div>
+ <div class="text-md-0 text-dark-grey">${format_currency(item_data.rate, currency)}</div>
+ </div>
+ </div>`
+ } else {
+ return `
+ <div class="flex f-shrink-0 ml-4 text-right">
+ <div class="flex w-8 h-8 rounded bg-light-grey mr-4 items-center justify-center font-bold f-shrink-0">
+ <span>${item_data.qty || 0}</span>
+ </div>
+ <div class="rate-col flex flex-col f-shrink-0 text-right">
+ <div class="text-md text-dark-grey text-bold">${format_currency(item_data.rate, currency)}</div>
+ </div>
+ </div>`
+ }
+ }
+
+ function get_description_html() {
+ if (item_data.description) {
+ if (item_data.description.indexOf('<div>') != -1) {
+ try {
+ item_data.description = $(item_data.description).text();
+ } catch (error) {
+ item_data.description = item_data.description.replace(/<div>/g, ' ').replace(/<\/div>/g, ' ').replace(/ +/g, ' ');
+ }
+ }
+ item_data.description = frappe.ellipsis(item_data.description, 45);
+ return `<div class="text-grey">${item_data.description}</div>`
+ }
+ return ``;
+ }
+ }
+
+ scroll_to_item($item) {
+ if ($item.length === 0) return;
+ const scrollTop = $item.offset().top - this.$cart_items_wrapper.offset().top + this.$cart_items_wrapper.scrollTop();
+ this.$cart_items_wrapper.animate({ scrollTop });
+ }
+
+ update_selector_value_in_cart_item(selector, value, item) {
+ const $item_to_update = this.get_cart_item(item);
+ $item_to_update.attr(`data-${selector}`, value);
+ }
+
+ toggle_checkout_btn(show_checkout) {
+ if (show_checkout) {
+ this.$totals_section.find('.checkout-btn').removeClass('d-none');
+ this.$totals_section.find('.edit-cart-btn').addClass('d-none');
+ } else {
+ this.$totals_section.find('.checkout-btn').addClass('d-none');
+ this.$totals_section.find('.edit-cart-btn').removeClass('d-none');
+ }
+ }
+
+ highlight_checkout_btn(toggle) {
+ const has_primary_class = this.$totals_section.find('.checkout-btn').hasClass('bg-primary');
+ if (toggle && !has_primary_class) {
+ this.$totals_section.find('.checkout-btn').addClass('bg-primary text-white text-lg');
+ } else if (!toggle && has_primary_class) {
+ this.$totals_section.find('.checkout-btn').removeClass('bg-primary text-white text-lg');
+ }
+ }
+
+ update_empty_cart_section(no_of_cart_items) {
+ const $no_item_element = this.$cart_items_wrapper.find('.no-item-wrapper');
+
+ // if cart has items and no item is present
+ no_of_cart_items > 0 && $no_item_element && $no_item_element.remove()
+ && this.$cart_items_wrapper.removeClass('mt-4 border-grey border-dashed') && this.$cart_header.removeClass('d-none');
+
+ no_of_cart_items === 0 && !$no_item_element.length && this.make_no_items_placeholder();
+ }
+
+ on_numpad_event($btn) {
+ const current_action = $btn.attr('data-button-value');
+ const action_is_field_edit = ['qty', 'discount_percentage', 'rate'].includes(current_action);
+
+ this.highlight_numpad_btn($btn, current_action);
+
+ const action_is_pressed_twice = this.prev_action === current_action;
+ const first_click_event = !this.prev_action;
+ const field_to_edit_changed = this.prev_action && this.prev_action != current_action;
+
+ if (action_is_field_edit) {
+
+ if (first_click_event || field_to_edit_changed) {
+ this.prev_action = current_action;
+ } else if (action_is_pressed_twice) {
+ this.prev_action = undefined;
+ }
+ this.numpad_value = '';
+
+ } else if (current_action === 'checkout') {
+ this.prev_action = undefined;
+ this.toggle_item_highlight();
+ this.events.numpad_event(undefined, current_action);
+ return;
+ } else if (current_action === 'remove') {
+ this.prev_action = undefined;
+ this.toggle_item_highlight();
+ this.events.numpad_event(undefined, current_action);
+ return;
+ } else {
+ this.numpad_value = current_action === 'delete' ? this.numpad_value.slice(0, -1) : this.numpad_value + current_action;
+ this.numpad_value = this.numpad_value || 0;
+ }
+
+ const first_click_event_is_not_field_edit = !action_is_field_edit && first_click_event;
+
+ if (first_click_event_is_not_field_edit) {
+ frappe.show_alert({
+ indicator: 'red',
+ message: __('Please select a field to edit from numpad')
+ });
+ frappe.utils.play_sound("error");
+ return;
+ }
+
+ if (flt(this.numpad_value) > 100 && this.prev_action === 'discount_percentage') {
+ frappe.show_alert({
+ message: __('Discount cannot be greater than 100%'),
+ indicator: 'orange'
+ });
+ frappe.utils.play_sound("error");
+ this.numpad_value = current_action;
+ }
+
+ this.events.numpad_event(this.numpad_value, this.prev_action);
+ }
+
+ highlight_numpad_btn($btn, curr_action) {
+ const curr_action_is_highlighted = $btn.hasClass('shadow-inner');
+ const curr_action_is_action = ['qty', 'discount_percentage', 'rate', 'done'].includes(curr_action);
+
+ if (!curr_action_is_highlighted) {
+ $btn.addClass('shadow-inner bg-selected');
+ }
+ if (this.prev_action === curr_action && curr_action_is_highlighted) {
+ // if Qty is pressed twice
+ $btn.removeClass('shadow-inner bg-selected');
+ }
+ if (this.prev_action && this.prev_action !== curr_action && curr_action_is_action) {
+ // Order: Qty -> Rate then remove Qty highlight
+ const prev_btn = $(`[data-button-value='${this.prev_action}']`);
+ prev_btn.removeClass('shadow-inner bg-selected');
+ }
+ if (!curr_action_is_action || curr_action === 'done') {
+ // if numbers are clicked
+ setTimeout(() => {
+ $btn.removeClass('shadow-inner bg-selected');
+ }, 100);
+ }
+ }
+
+ toggle_numpad(show) {
+ if (show) {
+ this.$totals_section.addClass('d-none');
+ this.$numpad_section.removeClass('d-none');
+ } else {
+ this.$totals_section.removeClass('d-none');
+ this.$numpad_section.addClass('d-none');
+ }
+ this.reset_numpad();
+ }
+
+ reset_numpad() {
+ this.numpad_value = '';
+ this.prev_action = undefined;
+ this.$numpad_section.find('.shadow-inner').removeClass('shadow-inner bg-selected');
+ }
+
+ toggle_numpad_field_edit(fieldname) {
+ if (['qty', 'discount_percentage', 'rate'].includes(fieldname)) {
+ this.$numpad_section.find(`[data-button-value="${fieldname}"]`).click();
+ }
+ }
+
+ toggle_customer_info(show) {
+ if (show) {
+ this.$cart_container.addClass('d-none')
+ this.$customer_section.addClass('flex-1 scroll-y').removeClass('mb-0 border pr-4 pl-4')
+ this.$customer_section.find('.icon').addClass('w-24 h-24 text-2xl').removeClass('w-12 h-12 text-md')
+ this.$customer_section.find('.customer-header').removeClass('h-18');
+ this.$customer_section.find('.customer-details').addClass('sticky z-100 bg-white');
+
+ this.$customer_section.find('.customer-name').html(
+ `<div class="text-md text-dark-grey text-bold">${this.customer_info.customer}</div>
+ <div class="last-transacted-on text-grey-200"></div>`
+ )
+
+ this.$customer_section.find('.customer-details').append(
+ `<div class="customer-form">
+ <div class="text-grey mt-4 mb-6">CONTACT DETAILS</div>
+ <div class="grid grid-cols-2 gap-4">
+ <div class="email_id-field"></div>
+ <div class="mobile_no-field"></div>
+ <div class="loyalty_program-field"></div>
+ <div class="loyalty_points-field"></div>
+ </div>
+ <div class="text-grey mt-4 mb-6">RECENT TRANSACTIONS</div>
+ </div>`
+ )
+ // transactions need to be in diff div from sticky elem for scrolling
+ this.$customer_section.append(`<div class="customer-transactions flex-1 rounded"></div>`)
+
+ this.render_customer_info_form();
+ this.fetch_customer_transactions();
+
+ } else {
+ this.$cart_container.removeClass('d-none');
+ this.$customer_section.removeClass('flex-1 scroll-y').addClass('mb-0 border pr-4 pl-4');
+ this.$customer_section.find('.icon').addClass('w-12 h-12 text-md').removeClass('w-24 h-24 text-2xl');
+ this.$customer_section.find('.customer-header').addClass('h-18')
+ this.$customer_section.find('.customer-details').removeClass('sticky z-100 bg-white');
+
+ this.update_customer_section();
+ }
+ }
+
+ render_customer_info_form() {
+ const $customer_form = this.$customer_section.find('.customer-form');
+
+ const dfs = [{
+ fieldname: 'email_id',
+ label: __('Email'),
+ fieldtype: 'Data',
+ options: 'email',
+ placeholder: __("Enter customer's email")
+ },{
+ fieldname: 'mobile_no',
+ label: __('Phone Number'),
+ fieldtype: 'Data',
+ placeholder: __("Enter customer's phone number")
+ },{
+ fieldname: 'loyalty_program',
+ label: __('Loyalty Program'),
+ fieldtype: 'Link',
+ options: 'Loyalty Program',
+ placeholder: __("Select Loyalty Program")
+ },{
+ fieldname: 'loyalty_points',
+ label: __('Loyalty Points'),
+ fieldtype: 'Int',
+ read_only: 1
+ }];
+
+ const me = this;
+ dfs.forEach(df => {
+ this[`customer_${df.fieldname}_field`] = frappe.ui.form.make_control({
+ df: { ...df,
+ onchange: handle_customer_field_change,
+ },
+ parent: $customer_form.find(`.${df.fieldname}-field`),
+ render_input: true,
+ });
+ this[`customer_${df.fieldname}_field`].set_value(this.customer_info[df.fieldname]);
+ })
+
+ function handle_customer_field_change() {
+ const current_value = me.customer_info[this.df.fieldname];
+ const current_customer = me.customer_info.customer;
+
+ if (this.value && current_value != this.value && this.df.fieldname != 'loyalty_points') {
+ frappe.call({
+ method: 'erpnext.selling.page.point_of_sale.point_of_sale.set_customer_info',
+ args: {
+ fieldname: this.df.fieldname,
+ customer: current_customer,
+ value: this.value
+ },
+ callback: (r) => {
+ if(!r.exc) {
+ me.customer_info[this.df.fieldname] = this.value;
+ frappe.show_alert({
+ message: __("Customer contact updated successfully."),
+ indicator: 'green'
+ });
+ frappe.utils.play_sound("submit");
+ }
+ }
+ });
+ }
+ }
+ }
+
+ fetch_customer_transactions() {
+ frappe.db.get_list('POS Invoice', {
+ filters: { customer: this.customer_info.customer, docstatus: 1 },
+ fields: ['name', 'grand_total', 'status', 'posting_date', 'posting_time', 'currency'],
+ limit: 20
+ }).then((res) => {
+ const transaction_container = this.$customer_section.find('.customer-transactions');
+
+ if (!res.length) {
+ transaction_container.removeClass('flex-1 border rounded').html(
+ `<div class="text-grey text-center">No recent transactions found</div>`
+ )
+ return;
+ };
+
+ const elapsed_time = moment(res[0].posting_date+" "+res[0].posting_time).fromNow();
+ this.$customer_section.find('.last-transacted-on').html(`Last transacted ${elapsed_time}`);
+
+ res.forEach(invoice => {
+ const posting_datetime = moment(invoice.posting_date+" "+invoice.posting_time).format("Do MMMM, h:mma");
+ let indicator_color = '';
+
+ if (in_list(['Paid', 'Consolidated'], invoice.status)) (indicator_color = 'green');
+ if (invoice.status === 'Draft') (indicator_color = 'red');
+ if (invoice.status === 'Return') (indicator_color = 'grey');
+
+ transaction_container.append(
+ `<div class="invoice-wrapper flex p-3 justify-between border-grey rounded pointer no-select" data-invoice-name="${escape(invoice.name)}">
+ <div class="flex flex-col justify-end">
+ <div class="text-dark-grey text-bold overflow-hidden whitespace-nowrap mb-2">${invoice.name}</div>
+ <div class="flex items-center f-shrink-1 text-dark-grey overflow-hidden whitespace-nowrap">
+ ${posting_datetime}
+ </div>
+ </div>
+ <div class="flex flex-col text-right">
+ <div class="f-shrink-0 text-md text-dark-grey text-bold ml-4">
+ ${format_currency(invoice.grand_total, invoice.currency, 0) || 0}
+ </div>
+ <div class="f-shrink-0 text-grey ml-4 text-bold indicator ${indicator_color}">${invoice.status}</div>
+ </div>
+ </div>`
+ )
+ });
+ })
+ }
+
+ load_invoice() {
+ const frm = this.events.get_frm();
+ this.fetch_customer_details(frm.doc.customer).then(() => {
+ this.events.customer_details_updated(this.customer_info);
+ this.update_customer_section();
+ })
+
+ this.$cart_items_wrapper.html('');
+ if (frm.doc.items.length) {
+ frm.doc.items.forEach(item => {
+ this.update_item_html(item);
+ });
+ } else {
+ this.make_no_items_placeholder();
+ this.highlight_checkout_btn(false);
+ }
+
+ this.update_totals_section(frm);
+
+ if(frm.doc.docstatus === 1) {
+ this.$totals_section.find('.checkout-btn').addClass('d-none');
+ this.$totals_section.find('.edit-cart-btn').addClass('d-none');
+ this.$totals_section.find('.grand-total').removeClass('border-b-grey');
+ } else {
+ this.$totals_section.find('.checkout-btn').removeClass('d-none');
+ this.$totals_section.find('.edit-cart-btn').addClass('d-none');
+ this.$totals_section.find('.grand-total').addClass('border-b-grey');
+ }
+
+ this.toggle_component(true);
+ }
+
+ toggle_component(show) {
+ show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
+ }
+
+}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js
new file mode 100644
index 0000000..86a1be9
--- /dev/null
+++ b/erpnext/selling/page/point_of_sale/pos_item_details.js
@@ -0,0 +1,394 @@
+erpnext.PointOfSale.ItemDetails = class {
+ constructor({ wrapper, events }) {
+ this.wrapper = wrapper;
+ this.events = events;
+ this.current_item = {};
+
+ this.init_component();
+ }
+
+ init_component() {
+ this.prepare_dom();
+ this.init_child_components();
+ this.bind_events();
+ this.attach_shortcuts();
+ }
+
+ prepare_dom() {
+ this.wrapper.append(
+ `<section class="col-span-4 flex shadow rounded item-details bg-white mx-h-70 h-100 d-none"></section>`
+ )
+
+ this.$component = this.wrapper.find('.item-details');
+ }
+
+ init_child_components() {
+ this.$component.html(
+ `<div class="details-container flex flex-col p-8 rounded w-full">
+ <div class="flex justify-between mb-2">
+ <div class="text-grey">ITEM DETAILS</div>
+ <div class="close-btn text-grey hover-underline pointer no-select">Close</div>
+ </div>
+ <div class="item-defaults flex">
+ <div class="flex-1 flex flex-col justify-end mr-4 mb-2">
+ <div class="item-name text-xl font-weight-450"></div>
+ <div class="item-description text-md-0 text-grey-200"></div>
+ <div class="item-price text-xl font-bold"></div>
+ </div>
+ <div class="item-image flex items-center justify-center w-46 h-46 bg-light-grey rounded ml-4 text-6xl text-grey-100"></div>
+ </div>
+ <div class="discount-section flex items-center"></div>
+ <div class="text-grey mt-4 mb-6">STOCK DETAILS</div>
+ <div class="form-container grid grid-cols-2 row-gap-2 col-gap-4 grid-auto-row"></div>
+ </div>`
+ )
+
+ this.$item_name = this.$component.find('.item-name');
+ this.$item_description = this.$component.find('.item-description');
+ this.$item_price = this.$component.find('.item-price');
+ this.$item_image = this.$component.find('.item-image');
+ this.$form_container = this.$component.find('.form-container');
+ this.$dicount_section = this.$component.find('.discount-section');
+ }
+
+ toggle_item_details_section(item) {
+ const { item_code, batch_no, uom } = this.current_item;
+ const item_code_is_same = item && item_code === item.item_code;
+ const batch_is_same = item && batch_no == item.batch_no;
+ const uom_is_same = item && uom === item.uom;
+
+ this.item_has_changed = !item ? false : item_code_is_same && batch_is_same && uom_is_same ? false : true;
+
+ this.events.toggle_item_selector(this.item_has_changed);
+ this.toggle_component(this.item_has_changed);
+
+ if (this.item_has_changed) {
+ this.doctype = item.doctype;
+ this.item_meta = frappe.get_meta(this.doctype);
+ this.name = item.name;
+ this.item_row = item;
+ this.currency = this.events.get_frm().doc.currency;
+
+ this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom };
+
+ this.render_dom(item);
+ this.render_discount_dom(item);
+ this.render_form(item);
+ } else {
+ this.validate_serial_batch_item();
+ this.current_item = {};
+ }
+ }
+
+ validate_serial_batch_item() {
+ const doc = this.events.get_frm().doc;
+ const item_row = doc.items.find(item => item.name === this.name);
+
+ if (!item_row) return;
+
+ const serialized = item_row.has_serial_no;
+ const batched = item_row.has_batch_no;
+ const no_serial_selected = !item_row.serial_no;
+ const no_batch_selected = !item_row.batch_no;
+
+ if ((serialized && no_serial_selected) || (batched && no_batch_selected) ||
+ (serialized && batched && (no_batch_selected || no_serial_selected))) {
+
+ frappe.show_alert({
+ message: __("Item will be removed since no serial / batch no selected."),
+ indicator: 'orange'
+ });
+ frappe.utils.play_sound("cancel");
+ this.events.remove_item_from_cart();
+ }
+ }
+
+ render_dom(item) {
+ let { item_code ,item_name, description, image, price_list_rate } = item;
+
+ function get_description_html() {
+ if (description) {
+ description = description.indexOf('...') === -1 && description.length > 75 ? description.substr(0, 73) + '...' : description;
+ return description;
+ }
+ return ``;
+ }
+
+ this.$item_name.html(item_name);
+ this.$item_description.html(get_description_html());
+ this.$item_price.html(format_currency(price_list_rate, this.currency));
+ if (image) {
+ this.$item_image.html(
+ `<img class="h-full" src="${image}" alt="${image}" style="object-fit: cover;">`
+ );
+ } else {
+ this.$item_image.html(frappe.get_abbr(item_code));
+ }
+
+ }
+
+ render_discount_dom(item) {
+ if (item.discount_percentage) {
+ this.$dicount_section.html(
+ `<div class="text-grey line-through mr-4 text-md mb-2">
+ ${format_currency(item.price_list_rate, this.currency)}
+ </div>
+ <div class="p-1 pr-3 pl-3 rounded w-fit text-bold bg-green-200 mb-2">
+ ${item.discount_percentage}% off
+ </div>`
+ )
+ this.$item_price.html(format_currency(item.rate, this.currency));
+ } else {
+ this.$dicount_section.html(``)
+ }
+ }
+
+ render_form(item) {
+ const fields_to_display = this.get_form_fields(item);
+ this.$form_container.html('');
+
+ fields_to_display.forEach((fieldname, idx) => {
+ this.$form_container.append(
+ `<div class="">
+ <div class="item_detail_field ${fieldname}-control" data-fieldname="${fieldname}"></div>
+ </div>`
+ )
+
+ const field_meta = this.item_meta.fields.find(df => df.fieldname === fieldname);
+ fieldname === 'discount_percentage' ? (field_meta.label = __('Discount (%)')) : '';
+ const me = this;
+
+ this[`${fieldname}_control`] = frappe.ui.form.make_control({
+ df: {
+ ...field_meta,
+ onchange: function() {
+ me.events.form_updated(me.doctype, me.name, fieldname, this.value);
+ }
+ },
+ parent: this.$form_container.find(`.${fieldname}-control`),
+ render_input: true,
+ })
+ this[`${fieldname}_control`].set_value(item[fieldname]);
+ });
+
+ this.make_auto_serial_selection_btn(item);
+
+ this.bind_custom_control_change_event();
+ }
+
+ get_form_fields(item) {
+ const fields = ['qty', 'uom', 'rate', 'price_list_rate', 'discount_percentage', 'warehouse', 'actual_qty'];
+ if (item.has_serial_no) fields.push('serial_no');
+ if (item.has_batch_no) fields.push('batch_no');
+ return fields;
+ }
+
+ make_auto_serial_selection_btn(item) {
+ if (item.has_serial_no) {
+ this.$form_container.append(
+ `<div class="grid-filler no-select"></div>`
+ )
+ if (!item.has_batch_no) {
+ this.$form_container.append(
+ `<div class="grid-filler no-select"></div>`
+ )
+ }
+ this.$form_container.append(
+ `<div class="auto-fetch-btn bg-grey-100 border border-grey text-bold rounded pt-3 pb-3 pl-6 pr-8 text-grey pointer no-select mt-2"
+ style="height: 3.3rem">
+ Auto Fetch Serial Numbers
+ </div>`
+ )
+ this.$form_container.find('.serial_no-control').find('textarea').css('height', '9rem');
+ this.$form_container.find('.serial_no-control').parent().addClass('row-span-2');
+ }
+ }
+
+ bind_custom_control_change_event() {
+ const me = this;
+ if (this.rate_control) {
+ this.rate_control.df.onchange = function() {
+ if (this.value) {
+ me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => {
+ const item_row = frappe.get_doc(me.doctype, me.name);
+ const doc = me.events.get_frm().doc;
+
+ me.$item_price.html(format_currency(item_row.rate, doc.currency));
+ me.render_discount_dom(item_row);
+ });
+ }
+ }
+ }
+
+ if (this.warehouse_control) {
+ this.warehouse_control.df.reqd = 1;
+ this.warehouse_control.df.onchange = function() {
+ if (this.value) {
+ me.events.form_updated(me.doctype, me.name, 'warehouse', this.value).then(() => {
+ me.item_stock_map = me.events.get_item_stock_map();
+ const available_qty = me.item_stock_map[me.item_row.item_code][this.value];
+ if (available_qty === undefined) {
+ me.events.get_available_stock(me.item_row.item_code, this.value).then(() => {
+ // item stock map is updated now reset warehouse
+ me.warehouse_control.set_value(this.value);
+ })
+ } else if (available_qty === 0) {
+ me.warehouse_control.set_value('');
+ frappe.throw(__(`Item Code: ${me.item_row.item_code.bold()} is not available under warehouse ${this.value.bold()}.`));
+ }
+ me.actual_qty_control.set_value(available_qty);
+ });
+ }
+ }
+ this.warehouse_control.refresh();
+ }
+
+ if (this.discount_percentage_control) {
+ this.discount_percentage_control.df.onchange = function() {
+ if (this.value) {
+ me.events.form_updated(me.doctype, me.name, 'discount_percentage', this.value).then(() => {
+ const item_row = frappe.get_doc(me.doctype, me.name);
+ me.rate_control.set_value(item_row.rate);
+ });
+ }
+ }
+ }
+
+ if (this.serial_no_control) {
+ this.serial_no_control.df.reqd = 1;
+ this.serial_no_control.df.onchange = async function() {
+ !me.current_item.batch_no && await me.auto_update_batch_no();
+ me.events.form_updated(me.doctype, me.name, 'serial_no', this.value);
+ }
+ this.serial_no_control.refresh();
+ }
+
+ if (this.batch_no_control) {
+ this.batch_no_control.df.reqd = 1;
+ this.batch_no_control.df.get_query = () => {
+ return {
+ query: 'erpnext.controllers.queries.get_batch_no',
+ filters: {
+ item_code: me.item_row.item_code,
+ warehouse: me.item_row.warehouse
+ }
+ }
+ };
+ this.batch_no_control.df.onchange = function() {
+ me.events.set_value_in_current_cart_item('batch-no', this.value);
+ me.events.form_updated(me.doctype, me.name, 'batch_no', this.value);
+ me.current_item.batch_no = this.value;
+ }
+ this.batch_no_control.refresh();
+ }
+
+ if (this.uom_control) {
+ this.uom_control.df.onchange = function() {
+ me.events.set_value_in_current_cart_item('uom', this.value);
+ me.events.form_updated(me.doctype, me.name, 'uom', this.value);
+ me.current_item.uom = this.value;
+ }
+ }
+ }
+
+ async auto_update_batch_no() {
+ if (this.serial_no_control && this.batch_no_control) {
+ const selected_serial_nos = this.serial_no_control.get_value().split(`\n`).filter(s => s);
+ if (!selected_serial_nos.length) return;
+
+ // find batch nos of the selected serial no
+ const serials_with_batch_no = await frappe.db.get_list("Serial No", {
+ filters: { 'name': ["in", selected_serial_nos]},
+ fields: ["batch_no", "name"]
+ });
+ const batch_serial_map = serials_with_batch_no.reduce((acc, r) => {
+ acc[r.batch_no] || (acc[r.batch_no] = []);
+ acc[r.batch_no] = [...acc[r.batch_no], r.name];
+ return acc;
+ }, {});
+ // set current item's batch no and serial no
+ const batch_no = Object.keys(batch_serial_map)[0];
+ const batch_serial_nos = batch_serial_map[batch_no].join(`\n`);
+ // eg. 10 selected serial no. -> 5 belongs to first batch other 5 belongs to second batch
+ const serial_nos_belongs_to_other_batch = selected_serial_nos.length !== batch_serial_map[batch_no].length;
+
+ const current_batch_no = this.batch_no_control.get_value();
+ current_batch_no != batch_no && await this.batch_no_control.set_value(batch_no);
+
+ if (serial_nos_belongs_to_other_batch) {
+ this.serial_no_control.set_value(batch_serial_nos);
+ this.qty_control.set_value(batch_serial_map[batch_no].length);
+ }
+
+ delete batch_serial_map[batch_no];
+
+ if (serial_nos_belongs_to_other_batch)
+ this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item);
+ }
+ }
+
+ bind_events() {
+ this.bind_auto_serial_fetch_event();
+ this.bind_fields_to_numpad_fields();
+
+ this.$component.on('click', '.close-btn', () => {
+ this.events.close_item_details();
+ });
+ }
+
+ attach_shortcuts() {
+ frappe.ui.keys.on("escape", () => {
+ const item_details_visible = this.$component.is(":visible");
+ if (item_details_visible) {
+ this.events.close_item_details();
+ }
+ });
+ }
+
+ bind_fields_to_numpad_fields() {
+ const me = this;
+ this.$form_container.on('click', '.input-with-feedback', function() {
+ const fieldname = $(this).attr('data-fieldname');
+ if (this.last_field_focused != fieldname) {
+ me.events.item_field_focused(fieldname);
+ this.last_field_focused = fieldname;
+ }
+ });
+ }
+
+ bind_auto_serial_fetch_event() {
+ this.$form_container.on('click', '.auto-fetch-btn', () => {
+ this.batch_no_control.set_value('');
+ let qty = this.qty_control.get_value();
+ let numbers = frappe.call({
+ method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number",
+ args: {
+ qty,
+ item_code: this.current_item.item_code,
+ warehouse: this.warehouse_control.get_value() || '',
+ batch_nos: this.current_item.batch_no || '',
+ for_doctype: 'POS Invoice'
+ }
+ });
+
+ numbers.then((data) => {
+ let auto_fetched_serial_numbers = data.message;
+ let records_length = auto_fetched_serial_numbers.length;
+ if (!records_length) {
+ const warehouse = this.warehouse_control.get_value().bold();
+ frappe.msgprint(__(`Serial numbers unavailable for Item ${this.current_item.item_code.bold()}
+ under warehouse ${warehouse}. Please try changing warehouse.`));
+ } else if (records_length < qty) {
+ frappe.msgprint(`Fetched only ${records_length} available serial numbers.`);
+ this.qty_control.set_value(records_length);
+ }
+ numbers = auto_fetched_serial_numbers.join(`\n`);
+ this.serial_no_control.set_value(numbers);
+ });
+ })
+ }
+
+ toggle_component(show) {
+ show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
+ }
+}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js
new file mode 100644
index 0000000..ee0c06d
--- /dev/null
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -0,0 +1,265 @@
+erpnext.PointOfSale.ItemSelector = class {
+ constructor({ frm, wrapper, events, pos_profile }) {
+ this.wrapper = wrapper;
+ this.events = events;
+ this.pos_profile = pos_profile;
+
+ this.inti_component();
+ }
+
+ inti_component() {
+ this.prepare_dom();
+ this.make_search_bar();
+ this.load_items_data();
+ this.bind_events();
+ this.attach_shortcuts();
+ }
+
+ prepare_dom() {
+ this.wrapper.append(
+ `<section class="col-span-6 flex shadow rounded items-selector bg-white mx-h-70 h-100">
+ <div class="flex flex-col rounded w-full scroll-y">
+ <div class="filter-section flex p-8 pb-2 bg-white sticky z-100">
+ <div class="search-field flex f-grow-3 mr-8 items-center text-grey"></div>
+ <div class="item-group-field flex f-grow-1 items-center text-grey text-bold"></div>
+ </div>
+ <div class="flex flex-1 flex-col p-8 pt-2">
+ <div class="text-grey mb-6">ALL ITEMS</div>
+ <div class="items-container grid grid-cols-4 gap-8">
+ </div>
+ </div>
+ </div>
+ </section>`
+ );
+
+ this.$component = this.wrapper.find('.items-selector');
+ }
+
+ async load_items_data() {
+ if (!this.item_group) {
+ const res = await frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name");
+ this.parent_item_group = res.message.name;
+ };
+ if (!this.price_list) {
+ const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list");
+ this.price_list = res.message.selling_price_list;
+ }
+
+ this.get_items({}).then(({message}) => {
+ this.render_item_list(message.items);
+ });
+ }
+
+ get_items({start = 0, page_length = 40, search_value=''}) {
+ const price_list = this.events.get_frm().doc?.selling_price_list || this.price_list;
+ let { item_group, pos_profile } = this;
+
+ !item_group && (item_group = this.parent_item_group);
+
+ return frappe.call({
+ method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items",
+ freeze: true,
+ args: { start, page_length, price_list, item_group, search_value, pos_profile },
+ });
+ }
+
+
+ render_item_list(items) {
+ this.$items_container = this.$component.find('.items-container');
+ this.$items_container.html('');
+
+ items.forEach(item => {
+ const item_html = this.get_item_html(item);
+ this.$items_container.append(item_html);
+ })
+ }
+
+ get_item_html(item) {
+ const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item;
+ const indicator_color = actual_qty > 10 ? "green" : actual_qty !== 0 ? "orange" : "red";
+
+ function get_item_image_html() {
+ if (item_image) {
+ return `<div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
+ <img class="h-full" src="${item_image}" alt="${item_image}" style="object-fit: cover;">
+ </div>`
+ } else {
+ return `<div class="flex items-center justify-center h-32 bg-light-grey text-6xl text-grey-100">
+ ${frappe.get_abbr(item.item_name)}
+ </div>`
+ }
+ }
+
+ return (
+ `<div class="item-wrapper rounded shadow pointer no-select" data-item-code="${escape(item.item_code)}"
+ data-serial-no="${escape(serial_no)}" data-batch-no="${escape(batch_no)}" data-uom="${escape(stock_uom)}"
+ title="Avaiable Qty: ${actual_qty}">
+ ${get_item_image_html()}
+ <div class="flex items-center pr-4 pl-4 h-10 justify-between">
+ <div class="flex items-center f-shrink-1 text-dark-grey overflow-hidden whitespace-nowrap">
+ <span class="indicator ${indicator_color}"></span>
+ ${frappe.ellipsis(item.item_name, 18)}
+ </div>
+ <div class="f-shrink-0 text-dark-grey text-bold ml-4">${format_currency(item.price_list_rate, item.currency, 0) || 0}</div>
+ </div>
+ </div>`
+ )
+ }
+
+ make_search_bar() {
+ const me = this;
+ this.$component.find('.search-field').html('');
+ this.$component.find('.item-group-field').html('');
+
+ this.search_field = frappe.ui.form.make_control({
+ df: {
+ label: __('Search'),
+ fieldtype: 'Data',
+ placeholder: __('Search by item code, serial number, batch no or barcode')
+ },
+ parent: this.$component.find('.search-field'),
+ render_input: true,
+ });
+ this.item_group_field = frappe.ui.form.make_control({
+ df: {
+ label: __('Item Group'),
+ fieldtype: 'Link',
+ options: 'Item Group',
+ placeholder: __('Select item group'),
+ onchange: function() {
+ me.item_group = this.value;
+ !me.item_group && (me.item_group = me.parent_item_group);
+ me.filter_items();
+ },
+ get_query: function () {
+ return {
+ query: 'erpnext.selling.page.point_of_sale.point_of_sale.item_group_query',
+ filters: {
+ pos_profile: me.events.get_frm().doc?.pos_profile
+ }
+ }
+ },
+ },
+ parent: this.$component.find('.item-group-field'),
+ render_input: true,
+ });
+ this.search_field.toggle_label(false);
+ this.item_group_field.toggle_label(false);
+ }
+
+ bind_events() {
+ const me = this;
+ onScan.attachTo(document, {
+ onScan: (sScancode) => {
+ if (this.search_field && this.$component.is(':visible')) {
+ this.search_field.set_focus();
+ $(this.search_field.$input[0]).val(sScancode).trigger("input");
+ this.barcode_scanned = true;
+ }
+ }
+ });
+
+ this.$component.on('click', '.item-wrapper', function() {
+ const $item = $(this);
+ const item_code = unescape($item.attr('data-item-code'));
+ let batch_no = unescape($item.attr('data-batch-no'));
+ let serial_no = unescape($item.attr('data-serial-no'));
+ let uom = unescape($item.attr('data-uom'));
+
+ // escape(undefined) returns "undefined" then unescape returns "undefined"
+ batch_no = batch_no === "undefined" ? undefined : batch_no;
+ serial_no = serial_no === "undefined" ? undefined : serial_no;
+ uom = uom === "undefined" ? undefined : uom;
+
+ me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }});
+ })
+
+ this.search_field.$input.on('input', (e) => {
+ clearTimeout(this.last_search);
+ this.last_search = setTimeout(() => {
+ const search_term = e.target.value;
+ this.filter_items({ search_term });
+ }, 300);
+ });
+ }
+
+ attach_shortcuts() {
+ frappe.ui.keys.on("ctrl+i", () => {
+ const selector_is_visible = this.$component.is(':visible');
+ if (!selector_is_visible) return;
+ this.search_field.set_focus();
+ });
+ frappe.ui.keys.on("ctrl+g", () => {
+ const selector_is_visible = this.$component.is(':visible');
+ if (!selector_is_visible) return;
+ this.item_group_field.set_focus();
+ });
+ // for selecting the last filtered item on search
+ frappe.ui.keys.on("enter", () => {
+ const selector_is_visible = this.$component.is(':visible');
+ if (!selector_is_visible || this.search_field.get_value() === "") return;
+
+ if (this.items.length == 1) {
+ this.$items_container.find(".item-wrapper").click();
+ frappe.utils.play_sound("submit");
+ $(this.search_field.$input[0]).val("").trigger("input");
+ } else if (this.items.length == 0 && this.barcode_scanned) {
+ // only show alert of barcode is scanned and enter is pressed
+ frappe.show_alert({
+ message: __("No items found. Scan barcode again."),
+ indicator: 'orange'
+ });
+ frappe.utils.play_sound("error");
+ this.barcode_scanned = false;
+ $(this.search_field.$input[0]).val("").trigger("input");
+ }
+ });
+ }
+
+ filter_items({ search_term='' }={}) {
+ if (search_term) {
+ search_term = search_term.toLowerCase();
+
+ // memoize
+ this.search_index = this.search_index || {};
+ if (this.search_index[search_term]) {
+ const items = this.search_index[search_term];
+ this.items = items;
+ this.render_item_list(items);
+ return;
+ }
+ }
+
+ this.get_items({ search_value: search_term })
+ .then(({ message }) => {
+ const { items, serial_no, batch_no, barcode } = message;
+ if (search_term && !barcode) {
+ this.search_index[search_term] = items;
+ }
+ this.items = items;
+ this.render_item_list(items);
+ });
+ }
+
+ resize_selector(minimize) {
+ minimize ?
+ this.$component.find('.search-field').removeClass('mr-8') :
+ this.$component.find('.search-field').addClass('mr-8');
+
+ minimize ?
+ this.$component.find('.filter-section').addClass('flex-col') :
+ this.$component.find('.filter-section').removeClass('flex-col');
+
+ minimize ?
+ this.$component.removeClass('col-span-6').addClass('col-span-2') :
+ this.$component.removeClass('col-span-2').addClass('col-span-6')
+
+ minimize ?
+ this.$items_container.removeClass('grid-cols-4').addClass('grid-cols-1') :
+ this.$items_container.removeClass('grid-cols-1').addClass('grid-cols-4')
+ }
+
+ toggle_component(show) {
+ show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
+ }
+}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_number_pad.js b/erpnext/selling/page/point_of_sale/pos_number_pad.js
new file mode 100644
index 0000000..2ffc2c0
--- /dev/null
+++ b/erpnext/selling/page/point_of_sale/pos_number_pad.js
@@ -0,0 +1,49 @@
+erpnext.PointOfSale.NumberPad = class {
+ constructor({ wrapper, events, cols, keys, css_classes, fieldnames_map }) {
+ this.wrapper = wrapper;
+ this.events = events;
+ this.cols = cols;
+ this.keys = keys;
+ this.css_classes = css_classes || [];
+ this.fieldnames = fieldnames_map || {};
+
+ this.init_component();
+ }
+
+ init_component() {
+ this.prepare_dom();
+ this.bind_events();
+ }
+
+ prepare_dom() {
+ const { cols, keys, css_classes, fieldnames } = this;
+
+ function get_keys() {
+ return keys.reduce((a, row, i) => {
+ return a + row.reduce((a2, number, j) => {
+ const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : '';
+ const fieldname = fieldnames && fieldnames[number] ?
+ fieldnames[number] :
+ typeof number === 'string' ? frappe.scrub(number) : number;
+
+ return a2 + `<div class="numpad-btn pointer no-select rounded ${class_to_append}
+ flex items-center justify-center h-16 text-md border-grey border" data-button-value="${fieldname}">${number}</div>`
+ }, '')
+ }, '');
+ }
+
+ this.wrapper.html(
+ `<div class="grid grid-cols-${cols} gap-4">
+ ${get_keys()}
+ </div>`
+ )
+ }
+
+ bind_events() {
+ const me = this;
+ this.wrapper.on('click', '.numpad-btn', function() {
+ const $btn = $(this);
+ me.events.numpad_event($btn);
+ })
+ }
+}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_list.js b/erpnext/selling/page/point_of_sale/pos_past_order_list.js
new file mode 100644
index 0000000..9181ee8
--- /dev/null
+++ b/erpnext/selling/page/point_of_sale/pos_past_order_list.js
@@ -0,0 +1,130 @@
+erpnext.PointOfSale.PastOrderList = class {
+ constructor({ wrapper, events }) {
+ this.wrapper = wrapper;
+ this.events = events;
+
+ this.init_component();
+ }
+
+ init_component() {
+ this.prepare_dom();
+ this.make_filter_section();
+ this.bind_events();
+ }
+
+ prepare_dom() {
+ this.wrapper.append(
+ `<section class="col-span-4 flex flex-col shadow rounded past-order-list bg-white mx-h-70 h-100 d-none">
+ <div class="flex flex-col rounded w-full scroll-y">
+ <div class="filter-section flex flex-col p-8 pb-2 bg-white sticky z-100">
+ <div class="search-field flex items-center text-grey"></div>
+ <div class="status-field flex items-center text-grey text-bold"></div>
+ </div>
+ <div class="flex flex-1 flex-col p-8 pt-2">
+ <div class="text-grey mb-6">RECENT ORDERS</div>
+ <div class="invoices-container rounded border grid grid-cols-1"></div>
+ </div>
+ </div>
+ </section>`
+ )
+
+ this.$component = this.wrapper.find('.past-order-list');
+ this.$invoices_container = this.$component.find('.invoices-container');
+ }
+
+ bind_events() {
+ this.search_field.$input.on('input', (e) => {
+ clearTimeout(this.last_search);
+ this.last_search = setTimeout(() => {
+ const search_term = e.target.value;
+ this.refresh_list(search_term, this.status_field.get_value());
+ }, 300);
+ });
+ const me = this;
+ this.$invoices_container.on('click', '.invoice-wrapper', function() {
+ const invoice_name = unescape($(this).attr('data-invoice-name'));
+
+ me.events.open_invoice_data(invoice_name);
+ })
+ }
+
+ make_filter_section() {
+ const me = this;
+ this.search_field = frappe.ui.form.make_control({
+ df: {
+ label: __('Search'),
+ fieldtype: 'Data',
+ placeholder: __('Search by invoice id or customer name')
+ },
+ parent: this.$component.find('.search-field'),
+ render_input: true,
+ });
+ this.status_field = frappe.ui.form.make_control({
+ df: {
+ label: __('Invoice Status'),
+ fieldtype: 'Select',
+ options: `Draft\nPaid\nConsolidated\nReturn`,
+ placeholder: __('Filter by invoice status'),
+ onchange: function() {
+ me.refresh_list(me.search_field.get_value(), this.value);
+ }
+ },
+ parent: this.$component.find('.status-field'),
+ render_input: true,
+ });
+ this.search_field.toggle_label(false);
+ this.status_field.toggle_label(false);
+ this.status_field.set_value('Paid');
+ }
+
+ toggle_component(show) {
+ show ?
+ this.$component.removeClass('d-none') && this.refresh_list() :
+ this.$component.addClass('d-none');
+ }
+
+ refresh_list() {
+ frappe.dom.freeze();
+ this.events.reset_summary();
+ const search_term = this.search_field.get_value();
+ const status = this.status_field.get_value();
+
+ this.$invoices_container.html('');
+
+ return frappe.call({
+ method: "erpnext.selling.page.point_of_sale.point_of_sale.get_past_order_list",
+ freeze: true,
+ args: { search_term, status },
+ callback: (response) => {
+ frappe.dom.unfreeze();
+ response.message.forEach(invoice => {
+ const invoice_html = this.get_invoice_html(invoice);
+ this.$invoices_container.append(invoice_html);
+ });
+ }
+ });
+ }
+
+ get_invoice_html(invoice) {
+ const posting_datetime = moment(invoice.posting_date+" "+invoice.posting_time).format("Do MMMM, h:mma");
+ return (
+ `<div class="invoice-wrapper flex p-4 justify-between border-b-grey pointer no-select" data-invoice-name="${escape(invoice.name)}">
+ <div class="flex flex-col justify-end">
+ <div class="text-dark-grey text-bold overflow-hidden whitespace-nowrap mb-2">${invoice.name}</div>
+ <div class="flex items-center">
+ <div class="flex items-center f-shrink-1 text-dark-grey overflow-hidden whitespace-nowrap">
+ <svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
+ </svg>
+ ${invoice.customer}
+ </div>
+ </div>
+ </div>
+ <div class="flex flex-col text-right">
+ <div class="f-shrink-0 text-lg text-dark-grey text-bold ml-4">${format_currency(invoice.grand_total, invoice.currency, 0) || 0}</div>
+ <div class="f-shrink-0 text-grey ml-4">${posting_datetime}</div>
+ </div>
+ </div>`
+ )
+ }
+}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
new file mode 100644
index 0000000..24326b2
--- /dev/null
+++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
@@ -0,0 +1,452 @@
+erpnext.PointOfSale.PastOrderSummary = class {
+ constructor({ wrapper, events }) {
+ this.wrapper = wrapper;
+ this.events = events;
+
+ this.init_component();
+ }
+
+ init_component() {
+ this.prepare_dom();
+ this.init_child_components();
+ this.bind_events();
+ this.attach_shortcuts();
+ }
+
+ prepare_dom() {
+ this.wrapper.append(
+ `<section class="col-span-6 flex flex-col items-center shadow rounded past-order-summary bg-white mx-h-70 h-100 d-none">
+ <div class="no-summary-placeholder flex flex-1 items-center justify-center p-16">
+ <div class="no-item-wrapper flex items-center h-18 pr-4 pl-4">
+ <div class="flex-1 text-center text-grey">Select an invoice to load summary data</div>
+ </div>
+ </div>
+ <div class="summary-wrapper d-none flex-1 w-66 text-dark-grey relative">
+ <div class="summary-container absolute flex flex-col pt-16 pb-16 pr-8 pl-8 w-full h-full"></div>
+ </div>
+ </section>`
+ )
+
+ this.$component = this.wrapper.find('.past-order-summary');
+ this.$summary_wrapper = this.$component.find('.summary-wrapper');
+ this.$summary_container = this.$component.find('.summary-container');
+ }
+
+ init_child_components() {
+ this.init_upper_section();
+ this.init_items_summary();
+ this.init_totals_summary();
+ this.init_payments_summary();
+ this.init_summary_buttons();
+ this.init_email_print_dialog();
+ }
+
+ init_upper_section() {
+ this.$summary_container.append(
+ `<div class="flex upper-section justify-between w-full h-24"></div>`
+ );
+
+ this.$upper_section = this.$summary_container.find('.upper-section');
+ }
+
+ init_items_summary() {
+ this.$summary_container.append(
+ `<div class="flex flex-col flex-1 mt-6 w-full scroll-y">
+ <div class="text-grey mb-4 sticky bg-white">ITEMS</div>
+ <div class="items-summary-container border rounded flex flex-col w-full"></div>
+ </div>`
+ )
+
+ this.$items_summary_container = this.$summary_container.find('.items-summary-container');
+ }
+
+ init_totals_summary() {
+ this.$summary_container.append(
+ `<div class="flex flex-col mt-6 w-full f-shrink-0">
+ <div class="text-grey mb-4">TOTALS</div>
+ <div class="summary-totals-container border rounded flex flex-col w-full"></div>
+ </div>`
+ )
+
+ this.$totals_summary_container = this.$summary_container.find('.summary-totals-container');
+ }
+
+ init_payments_summary() {
+ this.$summary_container.append(
+ `<div class="flex flex-col mt-6 w-full f-shrink-0">
+ <div class="text-grey mb-4">PAYMENTS</div>
+ <div class="payments-summary-container border rounded flex flex-col w-full mb-4"></div>
+ </div>`
+ )
+
+ this.$payment_summary_container = this.$summary_container.find('.payments-summary-container');
+ }
+
+ init_summary_buttons() {
+ this.$summary_container.append(
+ `<div class="summary-btns flex summary-btns justify-between w-full f-shrink-0"></div>`
+ )
+
+ this.$summary_btns = this.$summary_container.find('.summary-btns');
+ }
+
+ init_email_print_dialog() {
+ const email_dialog = new frappe.ui.Dialog({
+ title: 'Email Receipt',
+ fields: [
+ {fieldname:'email_id', fieldtype:'Data', options: 'Email', label:'Email ID'},
+ // {fieldname:'remarks', fieldtype:'Text', label:'Remarks (if any)'}
+ ],
+ primary_action: () => {
+ this.send_email();
+ },
+ primary_action_label: __('Send'),
+ });
+ this.email_dialog = email_dialog;
+
+ const print_dialog = new frappe.ui.Dialog({
+ title: 'Print Receipt',
+ fields: [
+ {fieldname:'print', fieldtype:'Data', label:'Print Preview'}
+ ],
+ primary_action: () => {
+ this.events.get_frm().print_preview.printit(true);
+ },
+ primary_action_label: __('Print'),
+ });
+ this.print_dialog = print_dialog;
+ }
+
+ get_upper_section_html(doc) {
+ const { status } = doc; let indicator_color = '';
+
+ in_list(['Paid', 'Consolidated'], status) && (indicator_color = 'green');
+ status === 'Draft' && (indicator_color = 'red');
+ status === 'Return' && (indicator_color = 'grey');
+
+ return `<div class="flex flex-col items-start justify-end pr-4">
+ <div class="text-lg text-bold pt-2">${doc.customer}</div>
+ <div class="text-grey">${this.customer_email}</div>
+ <div class="text-grey mt-auto">Sold by: ${doc.owner}</div>
+ </div>
+ <div class="flex flex-col flex-1 items-end justify-between">
+ <div class="text-2-5xl text-bold">${format_currency(doc.paid_amount, doc.currency)}</div>
+ <div class="flex justify-between">
+ <div class="text-grey mr-4">${doc.name}</div>
+ <div class="text-grey text-bold indicator ${indicator_color}">${doc.status}</div>
+ </div>
+ </div>`
+ }
+
+ get_discount_html(doc) {
+ if (doc.discount_amount) {
+ return `<div class="total-summary-wrapper flex items-center h-12 pr-4 pl-4 pointer border-b-grey no-select">
+ <div class="flex f-shrink-1 items-center">
+ <div class="text-md-0 text-dark-grey text-bold overflow-hidden whitespace-nowrap mr-2">
+ Discount
+ </div>
+ <span class="text-grey">(${doc.additional_discount_percentage} %)</span>
+ </div>
+ <div class="flex flex-col f-shrink-0 ml-auto text-right">
+ <div class="text-md-0 text-dark-grey text-bold">${format_currency(doc.discount_amount, doc.currency)}</div>
+ </div>
+ </div>`;
+ } else {
+ return ``;
+ }
+ }
+
+ get_net_total_html(doc) {
+ return `<div class="total-summary-wrapper flex items-center h-12 pr-4 pl-4 pointer border-b-grey no-select">
+ <div class="flex f-shrink-1 items-center">
+ <div class="text-md-0 text-dark-grey text-bold overflow-hidden whitespace-nowrap">
+ Net Total
+ </div>
+ </div>
+ <div class="flex flex-col f-shrink-0 ml-auto text-right">
+ <div class="text-md-0 text-dark-grey text-bold">${format_currency(doc.net_total, doc.currency)}</div>
+ </div>
+ </div>`
+ }
+
+ get_taxes_html(doc) {
+ return `<div class="total-summary-wrapper flex items-center justify-between h-12 pr-4 pl-4 border-b-grey">
+ <div class="flex">
+ <div class="text-md-0 text-dark-grey text-bold w-fit">Tax Charges</div>
+ <div class="flex ml-6 text-dark-grey">
+ ${
+ doc.taxes.map((t, i) => {
+ let margin_left = '';
+ if (i !== 0) margin_left = 'ml-2';
+ return `<span class="pl-2 pr-2 ${margin_left}">${t.description} @${t.rate}%</span>`
+ }).join('')
+ }
+ </div>
+ </div>
+ <div class="flex flex-col text-right">
+ <div class="text-md-0 text-dark-grey text-bold">${format_currency(doc.base_total_taxes_and_charges, doc.currency)}</div>
+ </div>
+ </div>`
+ }
+
+ get_grand_total_html(doc) {
+ return `<div class="total-summary-wrapper flex items-center h-12 pr-4 pl-4 pointer border-b-grey no-select">
+ <div class="flex f-shrink-1 items-center">
+ <div class="text-md-0 text-dark-grey text-bold overflow-hidden whitespace-nowrap">
+ Grand Total
+ </div>
+ </div>
+ <div class="flex flex-col f-shrink-0 ml-auto text-right">
+ <div class="text-md-0 text-dark-grey text-bold">${format_currency(doc.grand_total, doc.currency)}</div>
+ </div>
+ </div>`
+ }
+
+ get_item_html(doc, item_data) {
+ return `<div class="item-summary-wrapper flex items-center h-12 pr-4 pl-4 border-b-grey pointer no-select">
+ <div class="flex w-6 h-6 rounded bg-light-grey mr-4 items-center justify-center font-bold f-shrink-0">
+ <span>${item_data.qty || 0}</span>
+ </div>
+ <div class="flex flex-col f-shrink-1">
+ <div class="text-md text-dark-grey text-bold overflow-hidden whitespace-nowrap">
+ ${item_data.item_name}
+ </div>
+ </div>
+ <div class="flex f-shrink-0 ml-auto text-right">
+ ${get_rate_discount_html()}
+ </div>
+ </div>`
+
+ function get_rate_discount_html() {
+ if (item_data.rate && item_data.price_list_rate && item_data.rate !== item_data.price_list_rate) {
+ return `<span class="text-grey mr-2">(${item_data.discount_percentage}% off)</span>
+ <div class="text-md-0 text-dark-grey text-bold">${format_currency(item_data.rate, doc.currency)}</div>`
+ } else {
+ return `<div class="text-md-0 text-dark-grey text-bold">${format_currency(item_data.price_list_rate || item_data.rate, doc.currency)}</div>`
+ }
+ }
+ }
+
+ get_payment_html(doc, payment) {
+ return `<div class="payment-summary-wrapper flex items-center h-12 pr-4 pl-4 pointer border-b-grey no-select">
+ <div class="flex f-shrink-1 items-center">
+ <div class="text-md-0 text-dark-grey text-bold overflow-hidden whitespace-nowrap">
+ ${payment.mode_of_payment}
+ </div>
+ </div>
+ <div class="flex flex-col f-shrink-0 ml-auto text-right">
+ <div class="text-md-0 text-dark-grey text-bold">${format_currency(payment.amount, doc.currency)}</div>
+ </div>
+ </div>`
+ }
+
+ bind_events() {
+ this.$summary_container.on('click', '.return-btn', () => {
+ this.events.process_return(this.doc.name);
+ this.toggle_component(false);
+ this.$component.find('.no-summary-placeholder').removeClass('d-none');
+ this.$summary_wrapper.addClass('d-none');
+ });
+
+ this.$summary_container.on('click', '.edit-btn', () => {
+ this.events.edit_order(this.doc.name);
+ this.toggle_component(false);
+ this.$component.find('.no-summary-placeholder').removeClass('d-none');
+ this.$summary_wrapper.addClass('d-none');
+ });
+
+ this.$summary_container.on('click', '.new-btn', () => {
+ this.events.new_order();
+ this.toggle_component(false);
+ this.$component.find('.no-summary-placeholder').removeClass('d-none');
+ this.$summary_wrapper.addClass('d-none');
+ });
+
+ this.$summary_container.on('click', '.email-btn', () => {
+ this.email_dialog.fields_dict.email_id.set_value(this.customer_email);
+ this.email_dialog.show();
+ });
+
+ this.$summary_container.on('click', '.print-btn', () => {
+ // this.print_dialog.show();
+ const frm = this.events.get_frm();
+ frm.doc = this.doc;
+ frm.print_preview.printit(true);
+ });
+ }
+
+ attach_shortcuts() {
+ frappe.ui.keys.on("ctrl+p", () => {
+ const print_btn_visible = this.$summary_container.find('.print-btn').is(":visible");
+ const summary_visible = this.$component.is(":visible");
+ if (!summary_visible || !print_btn_visible) return;
+
+ this.$summary_container.find('.print-btn').click();
+ });
+ }
+
+ toggle_component(show) {
+ show ?
+ this.$component.removeClass('d-none') :
+ this.$component.addClass('d-none');
+ }
+
+ send_email() {
+ const frm = this.events.get_frm();
+ const recipients = this.email_dialog.get_values().recipients;
+ const doc = this.doc || frm.doc;
+ const print_format = frm.pos_print_format;
+
+ frappe.call({
+ method:"frappe.core.doctype.communication.email.make",
+ args: {
+ recipients: recipients,
+ subject: __(frm.meta.name) + ': ' + doc.name,
+ doctype: doc.doctype,
+ name: doc.name,
+ send_email: 1,
+ print_format,
+ sender_full_name: frappe.user.full_name(),
+ _lang : doc.language
+ },
+ callback: r => {
+ if(!r.exc) {
+ frappe.utils.play_sound("email");
+ if(r.message["emails_not_sent_to"]) {
+ frappe.msgprint(__("Email not sent to {0} (unsubscribed / disabled)",
+ [ frappe.utils.escape_html(r.message["emails_not_sent_to"]) ]) );
+ } else {
+ frappe.show_alert({
+ message: __('Email sent successfully.'),
+ indicator: 'green'
+ });
+ }
+ this.email_dialog.hide();
+ } else {
+ frappe.msgprint(__("There were errors while sending email. Please try again."));
+ }
+ }
+ });
+ }
+
+ add_summary_btns(map) {
+ this.$summary_btns.html('');
+ map.forEach(m => {
+ if (m.condition) {
+ m.visible_btns.forEach(b => {
+ const class_name = b.split(' ')[0].toLowerCase();
+ this.$summary_btns.append(
+ `<div class="${class_name}-btn border rounded h-14 flex flex-1 items-center mr-4 justify-center text-md text-bold no-select pointer">
+ ${b}
+ </div>`
+ )
+ });
+ }
+ });
+ this.$summary_btns.children().last().removeClass('mr-4');
+ }
+
+ show_summary_placeholder() {
+ this.$summary_wrapper.addClass("d-none");
+ this.$component.find('.no-summary-placeholder').removeClass('d-none');
+ }
+
+ switch_to_post_submit_summary() {
+ // switch to full width view
+ this.$component.removeClass('col-span-6').addClass('col-span-10');
+ this.$summary_wrapper.removeClass('w-66').addClass('w-40');
+
+ // switch place holder with summary container
+ this.$component.find('.no-summary-placeholder').addClass('d-none');
+ this.$summary_wrapper.removeClass('d-none');
+ }
+
+ switch_to_recent_invoice_summary() {
+ // switch full width view with 60% view
+ this.$component.removeClass('col-span-10').addClass('col-span-6');
+ this.$summary_wrapper.removeClass('w-40').addClass('w-66');
+
+ // switch place holder with summary container
+ this.$component.find('.no-summary-placeholder').addClass('d-none');
+ this.$summary_wrapper.removeClass('d-none');
+ }
+
+ get_condition_btn_map(after_submission) {
+ if (after_submission)
+ return [{ condition: true, visible_btns: ['Print Receipt', 'Email Receipt', 'New Order'] }];
+
+ return [
+ { condition: this.doc.docstatus === 0, visible_btns: ['Edit Order'] },
+ { condition: !this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt', 'Return']},
+ { condition: this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt']}
+ ];
+ }
+
+ load_summary_of(doc, after_submission=false) {
+ this.$summary_wrapper.removeClass("d-none");
+
+ after_submission ?
+ this.switch_to_post_submit_summary() : this.switch_to_recent_invoice_summary();
+
+ this.doc = doc;
+
+ this.attach_basic_info(doc);
+
+ this.attach_items_info(doc);
+
+ this.attach_totals_info(doc);
+
+ this.attach_payments_info(doc);
+
+ const condition_btns_map = this.get_condition_btn_map(after_submission);
+
+ this.add_summary_btns(condition_btns_map);
+ }
+
+ attach_basic_info(doc) {
+ frappe.db.get_value('Customer', this.doc.customer, 'email_id').then(({ message }) => {
+ this.customer_email = message.email_id || '';
+ const upper_section_dom = this.get_upper_section_html(doc);
+ this.$upper_section.html(upper_section_dom);
+ });
+ }
+
+ attach_items_info(doc) {
+ this.$items_summary_container.html('');
+ doc.items.forEach(item => {
+ const item_dom = this.get_item_html(doc, item);
+ this.$items_summary_container.append(item_dom);
+ });
+ }
+
+ attach_payments_info(doc) {
+ this.$payment_summary_container.html('');
+ doc.payments.forEach(p => {
+ if (p.amount) {
+ const payment_dom = this.get_payment_html(doc, p);
+ this.$payment_summary_container.append(payment_dom);
+ }
+ });
+ if (doc.redeem_loyalty_points && doc.loyalty_amount) {
+ const payment_dom = this.get_payment_html(doc, {
+ mode_of_payment: 'Loyalty Points',
+ amount: doc.loyalty_amount,
+ });
+ this.$payment_summary_container.append(payment_dom);
+ }
+ }
+
+ attach_totals_info(doc) {
+ this.$totals_summary_container.html('');
+
+ const discount_dom = this.get_discount_html(doc);
+ const net_total_dom = this.get_net_total_html(doc);
+ const taxes_dom = this.get_taxes_html(doc);
+ const grand_total_dom = this.get_grand_total_html(doc);
+ this.$totals_summary_container.append(discount_dom);
+ this.$totals_summary_container.append(net_total_dom);
+ this.$totals_summary_container.append(taxes_dom);
+ this.$totals_summary_container.append(grand_total_dom);
+ }
+
+}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
new file mode 100644
index 0000000..e1c54f6
--- /dev/null
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -0,0 +1,503 @@
+{% include "erpnext/selling/page/point_of_sale/pos_number_pad.js" %}
+
+erpnext.PointOfSale.Payment = class {
+ constructor({ events, wrapper }) {
+ this.wrapper = wrapper;
+ this.events = events;
+
+ this.init_component();
+ }
+
+ init_component() {
+ this.prepare_dom();
+ this.initialize_numpad();
+ this.bind_events();
+ this.attach_shortcuts();
+
+ }
+
+ prepare_dom() {
+ this.wrapper.append(
+ `<section class="col-span-6 flex shadow rounded payment-section bg-white mx-h-70 h-100 d-none">
+ <div class="flex flex-col p-16 pt-8 pb-8 w-full">
+ <div class="text-grey mb-6 payment-section no-select pointer">
+ PAYMENT METHOD<span class="octicon octicon-chevron-down collapse-indicator"></span>
+ </div>
+ <div class="payment-modes flex flex-wrap"></div>
+ <div class="invoice-details-section"></div>
+ <div class="flex mt-auto justify-center w-full">
+ <div class="flex flex-col justify-center flex-1 ml-4">
+ <div class="flex w-full">
+ <div class="totals-remarks items-end justify-end flex flex-1">
+ <div class="remarks text-md-0 text-grey mr-auto"></div>
+ <div class="totals flex justify-end pt-4"></div>
+ </div>
+ <div class="number-pad w-40 mb-4 ml-8 d-none"></div>
+ </div>
+ <div class="flex items-center justify-center mt-4 submit-order h-16 w-full rounded bg-primary text-md text-white no-select pointer text-bold">
+ Complete Order
+ </div>
+ <div class="order-time flex items-center justify-end mt-2 pt-2 pb-2 w-full text-md-0 text-grey no-select pointer d-none"></div>
+ </div>
+ </div>
+ </div>
+ </section>`
+ )
+ this.$component = this.wrapper.find('.payment-section');
+ this.$payment_modes = this.$component.find('.payment-modes');
+ this.$totals_remarks = this.$component.find('.totals-remarks');
+ this.$totals = this.$component.find('.totals');
+ this.$remarks = this.$component.find('.remarks');
+ this.$numpad = this.$component.find('.number-pad');
+ this.$invoice_details_section = this.$component.find('.invoice-details-section');
+ }
+
+ make_invoice_fields_control() {
+ frappe.db.get_doc("POS Settings", undefined).then((doc) => {
+ const fields = doc.invoice_fields;
+ if (!fields.length) return;
+
+ this.$invoice_details_section.html(
+ `<div class="text-grey pb-6 mt-2 pointer no-select">
+ ADDITIONAL INFORMATION<span class="octicon octicon-chevron-down collapse-indicator"></span>
+ </div>
+ <div class="invoice-fields grid grid-cols-2 gap-4 mb-6 d-none"></div>`
+ );
+ this.$invoice_fields = this.$invoice_details_section.find('.invoice-fields');
+ const frm = this.events.get_frm();
+
+ fields.forEach(df => {
+ this.$invoice_fields.append(
+ `<div class="invoice_detail_field ${df.fieldname}-field" data-fieldname="${df.fieldname}"></div>`
+ );
+
+ this[`${df.fieldname}_field`] = frappe.ui.form.make_control({
+ df: {
+ ...df,
+ onchange: function() {
+ frm.set_value(this.df.fieldname, this.value);
+ }
+ },
+ parent: this.$invoice_fields.find(`.${df.fieldname}-field`),
+ render_input: true,
+ });
+ this[`${df.fieldname}_field`].set_value(frm.doc[df.fieldname]);
+ })
+ });
+ }
+
+ initialize_numpad() {
+ const me = this;
+ this.number_pad = new erpnext.PointOfSale.NumberPad({
+ wrapper: this.$numpad,
+ events: {
+ numpad_event: function($btn) {
+ me.on_numpad_clicked($btn);
+ }
+ },
+ cols: 3,
+ keys: [
+ [ 1, 2, 3 ],
+ [ 4, 5, 6 ],
+ [ 7, 8, 9 ],
+ [ '.', 0, 'Delete' ]
+ ],
+ })
+
+ this.numpad_value = '';
+ }
+
+ on_numpad_clicked($btn) {
+ const me = this;
+ const button_value = $btn.attr('data-button-value');
+
+ highlight_numpad_btn($btn);
+ this.numpad_value = button_value === 'delete' ? this.numpad_value.slice(0, -1) : this.numpad_value + button_value;
+ this.selected_mode.$input.get(0).focus();
+ this.selected_mode.set_value(this.numpad_value);
+
+ function highlight_numpad_btn($btn) {
+ $btn.addClass('shadow-inner bg-selected');
+ setTimeout(() => {
+ $btn.removeClass('shadow-inner bg-selected');
+ }, 100);
+ }
+ }
+
+ bind_events() {
+ const me = this;
+
+ this.$payment_modes.on('click', '.mode-of-payment', function(e) {
+ const mode_clicked = $(this);
+ // if clicked element doesn't have .mode-of-payment class then return
+ if (!$(e.target).is(mode_clicked)) return;
+
+ const mode = mode_clicked.attr('data-mode');
+
+ // hide all control fields and shortcuts
+ $(`.mode-of-payment-control`).addClass('d-none');
+ $(`.cash-shortcuts`).addClass('d-none');
+ me.$payment_modes.find(`.pay-amount`).removeClass('d-none');
+ me.$payment_modes.find(`.loyalty-amount-name`).addClass('d-none');
+
+ // remove highlight from all mode-of-payments
+ $('.mode-of-payment').removeClass('border-primary');
+
+ if (mode_clicked.hasClass('border-primary')) {
+ // clicked one is selected then unselect it
+ mode_clicked.removeClass('border-primary');
+ me.selected_mode = '';
+ me.toggle_numpad(false);
+ } else {
+ // clicked one is not selected then select it
+ mode_clicked.addClass('border-primary');
+ mode_clicked.find('.mode-of-payment-control').removeClass('d-none');
+ mode_clicked.find('.cash-shortcuts').removeClass('d-none');
+ me.$payment_modes.find(`.${mode}-amount`).addClass('d-none');
+ me.$payment_modes.find(`.${mode}-name`).removeClass('d-none');
+ me.toggle_numpad(true);
+
+ me.selected_mode = me[`${mode}_control`];
+ const doc = me.events.get_frm().doc;
+ me.selected_mode?.$input?.get(0).focus();
+ !me.selected_mode?.get_value() ? me.selected_mode?.set_value(doc.grand_total - doc.paid_amount) : '';
+ }
+ })
+
+ this.$payment_modes.on('click', '.shortcut', function(e) {
+ const value = $(this).attr('data-value');
+ me.selected_mode.set_value(value);
+ })
+
+ // this.$totals_remarks.on('click', '.remarks', () => {
+ // this.toggle_remarks_control();
+ // })
+
+ this.$component.on('click', '.submit-order', () => {
+ const doc = this.events.get_frm().doc;
+ const paid_amount = doc.paid_amount;
+ const items = doc.items;
+
+ if (paid_amount == 0 || !items.length) {
+ const message = items.length ? __("You cannot submit the order without payment.") : __("You cannot submit empty order.")
+ frappe.show_alert({ message, indicator: "orange" });
+ frappe.utils.play_sound("error");
+ return;
+ }
+
+ this.events.submit_invoice();
+ })
+
+ frappe.ui.form.on('POS Invoice', 'paid_amount', (frm) => {
+ this.update_totals_section(frm.doc);
+
+ // need to re calculate cash shortcuts after discount is applied
+ const is_cash_shortcuts_invisible = this.$payment_modes.find('.cash-shortcuts').hasClass('d-none');
+ this.attach_cash_shortcuts(frm.doc);
+ !is_cash_shortcuts_invisible && this.$payment_modes.find('.cash-shortcuts').removeClass('d-none');
+ })
+
+ frappe.ui.form.on('POS Invoice', 'loyalty_amount', (frm) => {
+ const formatted_currency = format_currency(frm.doc.loyalty_amount, frm.doc.currency);
+ this.$payment_modes.find(`.loyalty-amount-amount`).html(formatted_currency);
+ });
+
+ frappe.ui.form.on("Sales Invoice Payment", "amount", (frm, cdt, cdn) => {
+ // for setting correct amount after loyalty points are redeemed
+ const default_mop = locals[cdt][cdn];
+ const mode = default_mop.mode_of_payment.replace(' ', '_').toLowerCase();
+ if (this[`${mode}_control`] && this[`${mode}_control`].get_value() != default_mop.amount) {
+ this[`${mode}_control`].set_value(default_mop.amount);
+ }
+ });
+
+ this.$component.on('click', '.invoice-details-section', function(e) {
+ if ($(e.target).closest('.invoice-fields').length) return;
+
+ me.$payment_modes.addClass('d-none');
+ me.$invoice_fields.toggleClass("d-none");
+ me.toggle_numpad(false);
+ });
+ this.$component.on('click', '.payment-section', () => {
+ this.$invoice_fields.addClass("d-none");
+ this.$payment_modes.toggleClass('d-none');
+ this.toggle_numpad(true);
+ })
+ }
+
+ attach_shortcuts() {
+ frappe.ui.keys.on("ctrl+enter", () => {
+ const payment_is_visible = this.$component.is(":visible");
+ const active_mode = this.$payment_modes.find(".border-primary");
+ if (payment_is_visible && active_mode.length) {
+ this.$component.find('.submit-order').click();
+ }
+ });
+
+ frappe.ui.keys.on("tab", () => {
+ const payment_is_visible = this.$component.is(":visible");
+ const mode_of_payments = Array.from(this.$payment_modes.find(".mode-of-payment")).map(m => $(m).attr("data-mode"));
+ let active_mode = this.$payment_modes.find(".border-primary");
+ active_mode = active_mode.length ? active_mode.attr("data-mode") : undefined;
+
+ if (!active_mode) return;
+
+ const mode_index = mode_of_payments.indexOf(active_mode);
+ const next_mode_index = (mode_index + 1) % mode_of_payments.length;
+ const next_mode_to_be_clicked = this.$payment_modes.find(`.mode-of-payment[data-mode="${mode_of_payments[next_mode_index]}"]`);
+
+ if (payment_is_visible && mode_index != next_mode_index) {
+ next_mode_to_be_clicked.click();
+ }
+ });
+ }
+
+ toggle_numpad(show) {
+ if (show) {
+ this.$numpad.removeClass('d-none');
+ this.$remarks.addClass('d-none');
+ this.$totals_remarks.addClass('w-60 justify-center').removeClass('justify-end w-full');
+ } else {
+ this.$numpad.addClass('d-none');
+ this.$remarks.removeClass('d-none');
+ this.$totals_remarks.removeClass('w-60 justify-center').addClass('justify-end w-full');
+ }
+ }
+
+ render_payment_section() {
+ this.render_payment_mode_dom();
+ this.make_invoice_fields_control();
+ this.update_totals_section();
+ }
+
+ edit_cart() {
+ this.events.toggle_other_sections(false);
+ this.toggle_component(false);
+ }
+
+ checkout() {
+ this.events.toggle_other_sections(true);
+ this.toggle_component(true);
+
+ this.render_payment_section();
+ }
+
+ toggle_remarks_control() {
+ if (this.$remarks.find('.frappe-control').length) {
+ this.$remarks.html('+ Add Remark');
+ } else {
+ this.$remarks.html('');
+ this[`remark_control`] = frappe.ui.form.make_control({
+ df: {
+ label: __('Remark'),
+ fieldtype: 'Data',
+ onchange: function() {}
+ },
+ parent: this.$totals_remarks.find(`.remarks`),
+ render_input: true,
+ });
+ this[`remark_control`].set_value('');
+ }
+ }
+
+ render_payment_mode_dom() {
+ const doc = this.events.get_frm().doc;
+ const payments = doc.payments;
+ const currency = doc.currency;
+
+ this.$payment_modes.html(
+ `${
+ payments.map((p, i) => {
+ const mode = p.mode_of_payment.replace(' ', '_').toLowerCase();
+ const payment_type = p.type;
+ const margin = i % 2 === 0 ? 'pr-2' : 'pl-2';
+ const amount = p.amount > 0 ? format_currency(p.amount, currency) : '';
+
+ return (
+ `<div class="w-half ${margin} bg-white">
+ <div class="mode-of-payment rounded border border-grey text-grey text-md
+ mb-4 p-8 pt-4 pb-4 no-select pointer" data-mode="${mode}" data-payment-type="${payment_type}">
+ ${p.mode_of_payment}
+ <div class="${mode}-amount pay-amount inline float-right text-bold">${amount}</div>
+ <div class="${mode} mode-of-payment-control mt-4 flex flex-1 items-center d-none"></div>
+ </div>
+ </div>`
+ )
+ }).join('')
+ }`
+ )
+
+ payments.forEach(p => {
+ const mode = p.mode_of_payment.replace(' ', '_').toLowerCase();
+ const me = this;
+ this[`${mode}_control`] = frappe.ui.form.make_control({
+ df: {
+ label: __(`${p.mode_of_payment}`),
+ fieldtype: 'Currency',
+ placeholder: __(`Enter ${p.mode_of_payment} amount.`),
+ onchange: function() {
+ if (this.value || this.value == 0) {
+ frappe.model.set_value(p.doctype, p.name, 'amount', flt(this.value))
+ .then(() => me.update_totals_section());
+
+ const formatted_currency = format_currency(this.value, currency);
+ me.$payment_modes.find(`.${mode}-amount`).html(formatted_currency);
+ }
+ }
+ },
+ parent: this.$payment_modes.find(`.${mode}.mode-of-payment-control`),
+ render_input: true,
+ });
+ this[`${mode}_control`].toggle_label(false);
+ this[`${mode}_control`].set_value(p.amount);
+
+ if (p.default) {
+ setTimeout(() => {
+ this.$payment_modes.find(`.${mode}.mode-of-payment-control`).parent().click();
+ }, 500);
+ }
+ })
+
+ this.render_loyalty_points_payment_mode();
+
+ this.attach_cash_shortcuts(doc);
+ }
+
+ attach_cash_shortcuts(doc) {
+ const grand_total = doc.grand_total;
+ const currency = doc.currency;
+
+ const shortcuts = this.get_cash_shortcuts(flt(grand_total));
+
+ this.$payment_modes.find('.cash-shortcuts').remove();
+ this.$payment_modes.find('[data-payment-type="Cash"]').find('.mode-of-payment-control').after(
+ `<div class="cash-shortcuts grid grid-cols-3 gap-2 flex-1 text-center text-md-0 mb-2 d-none">
+ ${
+ shortcuts.map(s => {
+ return `<div class="shortcut rounded bg-light-grey text-dark-grey pt-2 pb-2 no-select pointer" data-value="${s}">
+ ${format_currency(s, currency)}
+ </div>`
+ }).join('')
+ }
+ </div>`
+ )
+ }
+
+ get_cash_shortcuts(grand_total) {
+ let steps = [1, 5, 10];
+ const digits = String(Math.round(grand_total)).length;
+
+ steps = steps.map(x => x * (10 ** (digits - 2)));
+
+ const get_nearest = (amount, x) => {
+ let nearest_x = Math.ceil((amount / x)) * x;
+ return nearest_x === amount ? nearest_x + x : nearest_x;
+ }
+
+ return steps.reduce((finalArr, x) => {
+ let nearest_x = get_nearest(grand_total, x);
+ nearest_x = finalArr.indexOf(nearest_x) != -1 ? nearest_x + x : nearest_x;
+ return [...finalArr, nearest_x];
+ }, []);
+ }
+
+ render_loyalty_points_payment_mode() {
+ const me = this;
+ const doc = this.events.get_frm().doc;
+ const { loyalty_program, loyalty_points, conversion_factor } = this.events.get_customer_details();
+
+ this.$payment_modes.find(`.mode-of-payment[data-mode="loyalty-amount"]`).parent().remove();
+
+ if (!loyalty_program) return;
+
+ let description, read_only, max_redeemable_amount;
+ if (!loyalty_points) {
+ description = __(`You don't have enough points to redeem.`);
+ read_only = true;
+ } else {
+ max_redeemable_amount = flt(flt(loyalty_points) * flt(conversion_factor), precision("loyalty_amount", doc))
+ description = __(`You can redeem upto ${format_currency(max_redeemable_amount)}.`);
+ read_only = false;
+ }
+
+ const margin = this.$payment_modes.children().length % 2 === 0 ? 'pr-2' : 'pl-2';
+ const amount = doc.loyalty_amount > 0 ? format_currency(doc.loyalty_amount, doc.currency) : '';
+ this.$payment_modes.append(
+ `<div class="w-half ${margin} bg-white">
+ <div class="mode-of-payment rounded border border-grey text-grey text-md
+ mb-4 p-8 pt-4 pb-4 no-select pointer" data-mode="loyalty-amount" data-payment-type="loyalty-amount">
+ Redeem Loyalty Points
+ <div class="loyalty-amount-amount pay-amount inline float-right text-bold">${amount}</div>
+ <div class="loyalty-amount-name inline float-right text-bold text-md-0 d-none">${loyalty_program}</div>
+ <div class="loyalty-amount mode-of-payment-control mt-4 flex flex-1 items-center d-none"></div>
+ </div>
+ </div>`
+ )
+
+ this['loyalty-amount_control'] = frappe.ui.form.make_control({
+ df: {
+ label: __('Redeem Loyalty Points'),
+ fieldtype: 'Currency',
+ placeholder: __(`Enter amount to be redeemed.`),
+ options: 'company:currency',
+ read_only,
+ onchange: async function() {
+ if (!loyalty_points) return;
+
+ if (this.value > max_redeemable_amount) {
+ frappe.show_alert({
+ message: __(`You cannot redeem more than ${format_currency(max_redeemable_amount)}.`),
+ indicator: "red"
+ });
+ frappe.utils.play_sound("submit");
+ me['loyalty-amount_control'].set_value(0);
+ return;
+ }
+ const redeem_loyalty_points = this.value > 0 ? 1 : 0;
+ await frappe.model.set_value(doc.doctype, doc.name, 'redeem_loyalty_points', redeem_loyalty_points);
+ frappe.model.set_value(doc.doctype, doc.name, 'loyalty_points', parseInt(this.value / conversion_factor));
+ },
+ description
+ },
+ parent: this.$payment_modes.find(`.loyalty-amount.mode-of-payment-control`),
+ render_input: true,
+ });
+ this['loyalty-amount_control'].toggle_label(false);
+
+ // this.render_add_payment_method_dom();
+ }
+
+ render_add_payment_method_dom() {
+ const docstatus = this.events.get_frm().doc.docstatus;
+ if (docstatus === 0)
+ this.$payment_modes.append(
+ `<div class="w-full pr-2">
+ <div class="add-mode-of-payment w-half text-grey mb-4 no-select pointer">+ Add Payment Method</div>
+ </div>`
+ )
+ }
+
+ update_totals_section(doc) {
+ if (!doc) doc = this.events.get_frm().doc;
+ const paid_amount = doc.paid_amount;
+ const remaining = doc.grand_total - doc.paid_amount;
+ const change = doc.change_amount || remaining <= 0 ? -1 * remaining : undefined;
+ const currency = doc.currency
+ const label = change ? __('Change') : __('To Be Paid');
+
+ this.$totals.html(
+ `<div>
+ <div class="pr-8 border-r-grey">Paid Amount</div>
+ <div class="pr-8 border-r-grey text-bold text-2xl">${format_currency(paid_amount, currency)}</div>
+ </div>
+ <div>
+ <div class="pl-8">${label}</div>
+ <div class="pl-8 text-green-400 text-bold text-2xl">${format_currency(change || remaining, currency)}</div>
+ </div>`
+ )
+ }
+
+ toggle_component(show) {
+ show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
+ }
+ }
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/tests/test_point_of_sale.js b/erpnext/selling/page/point_of_sale/tests/test_point_of_sale.js
deleted file mode 100644
index 79d1700..0000000
--- a/erpnext/selling/page/point_of_sale/tests/test_point_of_sale.js
+++ /dev/null
@@ -1,38 +0,0 @@
-QUnit.test("test:Point of Sales", function(assert) {
- assert.expect(1);
- let done = assert.async();
-
- frappe.run_serially([
- () => frappe.set_route('point-of-sale'),
- () => frappe.timeout(3),
- () => frappe.set_control('customer', 'Test Customer 1'),
- () => frappe.timeout(0.2),
- () => cur_frm.set_value('customer', 'Test Customer 1'),
- () => frappe.timeout(2),
- () => frappe.click_link('Test Product 2'),
- () => frappe.timeout(0.2),
- () => frappe.click_element(`.cart-items [data-item-code="Test Product 2"]`),
- () => frappe.timeout(0.2),
- () => frappe.click_element(`.number-pad [data-value="Rate"]`),
- () => frappe.timeout(0.2),
- () => frappe.click_element(`.number-pad [data-value="2"]`),
- () => frappe.timeout(0.2),
- () => frappe.click_element(`.number-pad [data-value="5"]`),
- () => frappe.timeout(0.2),
- () => frappe.click_element(`.number-pad [data-value="0"]`),
- () => frappe.timeout(0.2),
- () => frappe.click_element(`.number-pad [data-value="Pay"]`),
- () => frappe.timeout(0.2),
- () => frappe.click_element(`.frappe-control [data-value="4"]`),
- () => frappe.timeout(0.2),
- () => frappe.click_element(`.frappe-control [data-value="5"]`),
- () => frappe.timeout(0.2),
- () => frappe.click_element(`.frappe-control [data-value="0"]`),
- () => frappe.timeout(0.2),
- () => frappe.click_button('Submit'),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(3),
- () => assert.ok(cur_frm.doc.docstatus==1, "Sales invoice created successfully"),
- () => done()
- ]);
-});
\ No newline at end of file
diff --git a/erpnext/selling/doctype/pos_closing_voucher/__init__.py b/erpnext/selling/print_format/__init__.py
similarity index 100%
rename from erpnext/selling/doctype/pos_closing_voucher/__init__.py
rename to erpnext/selling/print_format/__init__.py
diff --git a/erpnext/selling/doctype/pos_closing_voucher/__init__.py b/erpnext/selling/print_format/gst_pos_invoice/__init__.py
similarity index 100%
copy from erpnext/selling/doctype/pos_closing_voucher/__init__.py
copy to erpnext/selling/print_format/gst_pos_invoice/__init__.py
diff --git a/erpnext/selling/print_format/gst_pos_invoice/gst_pos_invoice.json b/erpnext/selling/print_format/gst_pos_invoice/gst_pos_invoice.json
new file mode 100644
index 0000000..9094a07b
--- /dev/null
+++ b/erpnext/selling/print_format/gst_pos_invoice/gst_pos_invoice.json
@@ -0,0 +1,23 @@
+{
+ "align_labels_right": 0,
+ "creation": "2017-08-08 12:33:04.773099",
+ "custom_format": 1,
+ "disabled": 0,
+ "doc_type": "POS Invoice",
+ "docstatus": 0,
+ "doctype": "Print Format",
+ "font": "Default",
+ "html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Tahoma, sans-serif;\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n<p class=\"text-center\">\n\t{{ doc.company }}<br>\n\t{% if doc.company_address_display %}\n\t\t{% set company_address = doc.company_address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t{% if \"GSTIN\" not in company_address %}\n\t\t\t{{ company_address }}\n\t\t\t<b>{{ _(\"GSTIN\") }}:</b>{{ doc.company_gstin }}\n\t\t{% else %}\n\t\t\t{{ company_address.replace(\"GSTIN\", \"<br>GSTIN\") }}\n\t\t{% endif %}\n\t{% endif %}\n\t<br>\n\t{% if doc.docstatus == 0 %}\n\t\t<b>{{ doc.status + \" \"+ (doc.select_print_heading or _(\"Invoice\")) }}</b><br>\n\t{% else %}\n\t\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t{% endif %}\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t{% if doc.grand_total > 50000 %}\n\t\t{% set customer_address = doc.address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t<b>{{ _(\"Customer\") }}:</b><br>\n\t\t{{ doc.customer_name }}<br>\n\t\t{{ customer_address }}\n\t{% endif %}\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</b></th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.gst_hsn_code -%}\n\t\t\t\t\t<br><b>{{ _(\"HSN/SAC\") }}:</b> {{ item.gst_hsn_code }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.rate }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if (not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) and row.tax_amount != 0 -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t{%- if doc.change_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t{%- endif -%}\n\t</tbody>\n</table>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
+ "idx": 0,
+ "line_breaks": 0,
+ "modified": "2020-04-29 16:47:02.743246",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "GST POS Invoice",
+ "owner": "Administrator",
+ "print_format_builder": 0,
+ "print_format_type": "Jinja",
+ "raw_printing": 0,
+ "show_section_headings": 0,
+ "standard": "Yes"
+}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/pos_closing_voucher/__init__.py b/erpnext/selling/print_format/pos_invoice/__init__.py
similarity index 100%
copy from erpnext/selling/doctype/pos_closing_voucher/__init__.py
copy to erpnext/selling/print_format/pos_invoice/__init__.py
diff --git a/erpnext/selling/print_format/pos_invoice/pos_invoice.json b/erpnext/selling/print_format/pos_invoice/pos_invoice.json
new file mode 100644
index 0000000..99094ed
--- /dev/null
+++ b/erpnext/selling/print_format/pos_invoice/pos_invoice.json
@@ -0,0 +1,22 @@
+{
+ "align_labels_right": 0,
+ "creation": "2011-12-21 11:08:55",
+ "custom_format": 1,
+ "disabled": 0,
+ "doc_type": "POS Invoice",
+ "docstatus": 0,
+ "doctype": "Print Format",
+ "html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Tahoma, sans-serif;\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t{{ doc.select_print_heading or _(\"Invoice\") }}<br>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.get_formatted(\"rate\") }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
+ "idx": 1,
+ "line_breaks": 0,
+ "modified": "2020-04-29 16:45:58.942375",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "POS Invoice",
+ "owner": "Administrator",
+ "print_format_builder": 0,
+ "print_format_type": "Jinja",
+ "raw_printing": 0,
+ "show_section_headings": 0,
+ "standard": "Yes"
+}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/pos_closing_voucher/__init__.py b/erpnext/selling/print_format/return_pos_invoice/__init__.py
similarity index 100%
copy from erpnext/selling/doctype/pos_closing_voucher/__init__.py
copy to erpnext/selling/print_format/return_pos_invoice/__init__.py
diff --git a/erpnext/selling/print_format/return_pos_invoice/return_pos_invoice.json b/erpnext/selling/print_format/return_pos_invoice/return_pos_invoice.json
new file mode 100644
index 0000000..d7f3350
--- /dev/null
+++ b/erpnext/selling/print_format/return_pos_invoice/return_pos_invoice.json
@@ -0,0 +1,24 @@
+{
+ "align_labels_right": 0,
+ "creation": "2020-05-14 17:02:44.207166",
+ "custom_format": 1,
+ "default_print_language": "en",
+ "disabled": 0,
+ "doc_type": "POS Invoice",
+ "docstatus": 0,
+ "doctype": "Print Format",
+ "font": "Default",
+ "html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Tahoma, sans-serif;\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t{{ doc.select_print_heading or _(\"Return Invoice\") }}<br>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Original Invoice\") }}:</b> {{ doc.return_against }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.get_formatted(\"rate\") }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc)}}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\")}}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
+ "idx": 0,
+ "line_breaks": 0,
+ "modified": "2020-05-14 17:13:29.354015",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Return POS Invoice",
+ "owner": "Administrator",
+ "print_format_builder": 0,
+ "print_format_type": "Jinja",
+ "raw_printing": 0,
+ "show_section_headings": 0,
+ "standard": "Yes"
+}
\ No newline at end of file
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 4a7dd5a..333a563 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -142,7 +142,7 @@
frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]);
// check if child doctype is Sales Order Item/Qutation Item and calculate the rate
- if(in_list(["Quotation Item", "Sales Order Item", "Delivery Note Item", "Sales Invoice Item"]), cdt)
+ if(in_list(["Quotation Item", "Sales Order Item", "Delivery Note Item", "Sales Invoice Item", "POS Invoice Item"]), cdt)
this.apply_pricing_rule_on_item(item);
else
item.rate = flt(item.price_list_rate * (1 - item.discount_percentage / 100.0),
@@ -312,6 +312,11 @@
batch_no: function(doc, cdt, cdn) {
var me = this;
var item = frappe.get_doc(cdt, cdn);
+
+ if (item.serial_no) {
+ return;
+ }
+
item.serial_no = null;
var has_serial_no;
frappe.db.get_value('Item', {'item_code': item.item_code}, 'has_serial_no', (r) => {
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 153ce2f..f7ff916 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -3,6 +3,7 @@
from __future__ import unicode_literals
import frappe
+import json
from frappe.model.naming import make_autoname
from frappe.utils import cint, cstr, flt, add_days, nowdate, getdate
@@ -537,15 +538,54 @@
return serial_nos
@frappe.whitelist()
-def auto_fetch_serial_number(qty, item_code, warehouse, batch_nos=None):
- import json
+def auto_fetch_serial_number(qty, item_code, warehouse, batch_nos=None, for_doctype=None):
filters = {
"item_code": item_code,
"warehouse": warehouse,
"delivery_document_no": "",
"sales_invoice": ""
}
- if batch_nos: filters["batch_no"] = ["in", json.loads(batch_nos)]
+
+ if batch_nos:
+ try:
+ filters["batch_no"] = ["in", json.loads(batch_nos)]
+ except:
+ filters["batch_no"] = ["in", [batch_nos]]
+
+ if for_doctype == 'POS Invoice':
+ reserved_serial_nos, unreserved_serial_nos = get_pos_reserved_serial_nos(filters, qty)
+ return unreserved_serial_nos
serial_numbers = frappe.get_list("Serial No", filters=filters, limit=qty, order_by="creation")
return [item['name'] for item in serial_numbers]
+
+@frappe.whitelist()
+def get_pos_reserved_serial_nos(filters, qty=None):
+ batch_no_cond = ""
+ if filters.get("batch_no"):
+ batch_no_cond = "and item.batch_no = {}".format(frappe.db.escape(filters.get('batch_no')))
+
+ reserved_serial_nos_str = [d.serial_no for d in frappe.db.sql("""select item.serial_no as serial_no
+ from `tabPOS Invoice` p, `tabPOS Invoice Item` item
+ where p.name = item.parent
+ and p.consolidated_invoice is NULL
+ and p.docstatus = 1
+ and item.docstatus = 1
+ and item.item_code = %s
+ and item.warehouse = %s
+ {}
+ """.format(batch_no_cond), [filters.get('item_code'), filters.get('warehouse')], as_dict=1)]
+
+ reserved_serial_nos = []
+ for s in reserved_serial_nos_str:
+ if not s: continue
+
+ serial_nos = s.split("\n")
+ serial_nos = ' '.join(serial_nos).split() # remove whitespaces
+ if len(serial_nos): reserved_serial_nos += serial_nos
+
+ filters["name"] = ["not in", reserved_serial_nos]
+ serial_numbers = frappe.get_list("Serial No", filters=filters, limit=qty, order_by="creation")
+ unreserved_serial_nos = [item['name'] for item in serial_numbers]
+
+ return reserved_serial_nos, unreserved_serial_nos
\ No newline at end of file
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index b8554c8..1a7c15e 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -401,13 +401,30 @@
return warehouse
def update_barcode_value(out):
- from erpnext.accounts.doctype.sales_invoice.pos import get_barcode_data
barcode_data = get_barcode_data([out])
# If item has one barcode then update the value of the barcode field
if barcode_data and len(barcode_data.get(out.item_code)) == 1:
out['barcode'] = barcode_data.get(out.item_code)[0]
+def get_barcode_data(items_list):
+ # get itemwise batch no data
+ # exmaple: {'LED-GRE': [Batch001, Batch002]}
+ # where LED-GRE is item code, SN0001 is serial no and Pune is warehouse
+
+ itemwise_barcode = {}
+ for item in items_list:
+ barcodes = frappe.db.sql("""
+ select barcode from `tabItem Barcode` where parent = %s
+ """, item.item_code, as_dict=1)
+
+ for barcode in barcodes:
+ if item.item_code not in itemwise_barcode:
+ itemwise_barcode.setdefault(item.item_code, [])
+ itemwise_barcode[item.item_code].append(barcode.get("barcode"))
+
+ return itemwise_barcode
+
@frappe.whitelist()
def get_item_tax_info(company, tax_category, item_codes):
out = {}