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>
This commit is contained in:
Saqib 2020-07-23 18:51:26 +05:30 committed by GitHub
parent 8712ac6d39
commit a6f98d48bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 9489 additions and 7949 deletions

View File

@ -147,10 +147,15 @@
"link_to": "Trial Balance",
"type": "Report"
},
{
"label": "Point of Sale",
"link_to": "point-of-sale",
"type": "Page"
},
{
"label": "Dashboard",
"link_to": "Accounts",
"type": "Dashboard"
}
]
}
}

View File

@ -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
}

View File

@ -18,7 +18,7 @@ def get_loyalty_point_entries(customer, loyalty_program, company, expiry_date=No
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

View File

@ -36,7 +36,8 @@ def get_loyalty_details(customer, loyalty_program, expiry_date=None, company=Non
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 @@ def get_loyalty_program_details(customer, loyalty_program=None, expiry_date=None
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

View File

@ -27,7 +27,7 @@ class TestLoyaltyProgram(unittest.TestCase):
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 @@ class TestLoyaltyProgram(unittest.TestCase):
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 @@ class TestLoyaltyProgram(unittest.TestCase):
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 @@ class TestLoyaltyProgram(unittest.TestCase):
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 @@ class TestLoyaltyProgram(unittest.TestCase):
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 @@ class TestLoyaltyProgram(unittest.TestCase):
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 @@ class TestLoyaltyProgram(unittest.TestCase):
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))

View File

@ -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>

View File

@ -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);
}
})
}

View File

@ -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
}

View File

@ -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

View File

@ -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'}
]),

View File

@ -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

View File

@ -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
}

View File

@ -5,5 +5,5 @@
from __future__ import unicode_literals
from frappe.model.document import Document
class POSClosingVoucherTaxes(Document):
class POSClosingEntryDetail(Document):
pass

View File

@ -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
}

View File

@ -5,5 +5,5 @@
from __future__ import unicode_literals
from frappe.model.document import Document
class POSClosingVoucherDetails(Document):
class POSClosingEntryTaxes(Document):
pass

View File

@ -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);
}
}
});

File diff suppressed because it is too large Load Diff

View File

@ -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()

View File

@ -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);
}
}
});
});
},
};

View File

@ -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

View File

@ -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"
}

View File

@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# 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 POSClosingVoucherInvoices(Document):
class POSInvoiceItem(Document):
pass

View File

@ -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': ''
}
}
});
}
});

View File

@ -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
}

View File

@ -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()

View File

@ -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`")

View File

@ -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
}

View File

@ -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

View File

@ -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");
}
});
}
}
});

View File

@ -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
}

View File

@ -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)

View File

@ -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];
}
};

View File

@ -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()

View File

@ -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
}

View File

@ -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

View File

@ -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"
}

View File

@ -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

View File

@ -28,7 +28,7 @@ frappe.ui.form.on("POS Profile", "onload", function(frm) {
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 @@ frappe.ui.form.on('POS Profile', {
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'));

View File

@ -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",
@ -104,15 +95,6 @@
"fieldtype": "Read Only",
"label": "Country"
},
{
"depends_on": "update_stock",
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"oldfieldname": "warehouse",
"oldfieldtype": "Link",
"options": "Warehouse"
},
{
"fieldname": "campaign",
"fieldtype": "Link",
@ -129,48 +111,6 @@
"fieldname": "column_break_9",
"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",
@ -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",
@ -220,12 +160,6 @@
"fieldtype": "Section Break",
"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",
@ -258,39 +192,6 @@
"oldfieldtype": "Select",
"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",
@ -380,20 +281,49 @@
"fieldtype": "Section Break",
"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",

View File

@ -5,8 +5,6 @@ from __future__ import unicode_literals
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 @@ class POSProfile(Document):
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 @@ class POSProfile(Document):
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 @@ def get_item_groups(pos_profile):
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):

View File

@ -8,7 +8,7 @@ def get_data():
'fieldname': 'pos_profile',
'transactions': [
{
'items': ['Sales Invoice', 'POS Closing Voucher']
'items': ['Sales Invoice', 'POS Closing Entry', 'POS Opening Entry']
}
]
}

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
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 @@ class TestPOSProfile(unittest.TestCase):
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 @@ def make_pos_profile(**args):
"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()

View File

@ -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",

View File

@ -6,27 +6,19 @@ frappe.ui.form.on('POS Settings', {
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);
});
}
});

View File

@ -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",

View File

@ -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

View File

@ -282,7 +282,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
"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);
}
}

View File

@ -13,6 +13,7 @@
"customer_name",
"tax_id",
"is_pos",
"is_consolidated",
"pos_profile",
"offline_pos_name",
"is_return",
@ -1921,6 +1922,13 @@
"hide_days": 1,
"hide_seconds": 1
},
{
"default": "0",
"fieldname": "is_consolidated",
"fieldtype": "Check",
"label": "Is Consolidated",
"read_only": 1
},
{
"default": "0",
"fetch_from": "customer.is_internal_customer",

View File

@ -8,8 +8,6 @@ from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_d
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 @@ class SalesInvoice(SellingController):
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 @@ class SalesInvoice(SellingController):
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 @@ class SalesInvoice(SellingController):
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 @@ class SalesInvoice(SellingController):
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 @@ class SalesInvoice(SellingController):
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 @@ class SalesInvoice(SellingController):
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 @@ class SalesInvoice(SellingController):
"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 @@ class SalesInvoice(SellingController):
# 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 @@ class SalesInvoice(SellingController):
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 @@ class SalesInvoice(SellingController):
"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 @@ def get_loyalty_programs(customer):
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 @@ def create_invoice_discounting(source_name, target_doc=None):
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 @@ def create_dunning(source_name, target_doc=None):
"doctype": "Dunning",
}
}, target_doc, set_missing_values)
return doclist
return doclist

View File

@ -706,37 +706,15 @@ class TestSalesInvoice(unittest.TestCase):
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 @@ class TestSalesInvoice(unittest.TestCase):
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 @@ class TestSalesInvoice(unittest.TestCase):
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`

View File

@ -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"
}

File diff suppressed because it is too large Load Diff

View File

@ -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"
}

View File

@ -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()
]);
});

View File

@ -184,7 +184,7 @@ def set_price_list(party_details, party, party_type, given_price_list, pos=None)
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

View File

@ -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",

View File

@ -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",

View File

@ -213,7 +213,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
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 @@ def make_return_doc(doctype, source_name, target_doc=None):
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 @@ def make_return_doc(doctype, source_name, target_doc=None):
'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 @@ def make_return_doc(doctype, source_name, target_doc=None):
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

View File

@ -85,6 +85,12 @@ status_map = {
"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"],
]
}

View File

@ -370,7 +370,7 @@ class calculate_taxes_and_totals(object):
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 @@ class calculate_taxes_and_totals(object):
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:

View File

@ -14,6 +14,7 @@ erpnext.patches.v4_0.apply_user_permissions
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_5.remove_project_type_property_setter
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.v12_0.update_end_date_and_status_in_email_campaign
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 @@ erpnext.patches.v12_0.update_bom_in_so_mr
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 @@ execute:frappe.delete_doc_if_exists("DocType", "Bank Reconciliation")
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

View File

@ -54,7 +54,7 @@ doctype_series_map = {
'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': '',

View File

@ -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")

View File

@ -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` = '')""")

View File

@ -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)

View File

@ -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()

View File

@ -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; }

View File

@ -34,12 +34,12 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
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 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
? 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 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
// 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 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
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 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
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 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
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 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
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 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
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; });

View File

@ -651,7 +651,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
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;
}
});
}

View File

@ -43,6 +43,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
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 @@ erpnext.SerialNoBatchSelector = Class.extend({
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 @@ erpnext.SerialNoBatchSelector = Class.extend({
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 @@ erpnext.SerialNoBatchSelector = Class.extend({
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')},
{

View File

@ -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);
}
}
});
}
};

View File

@ -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)

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -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"
}

View File

@ -6,6 +6,7 @@ import frappe, json
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 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
SELECT
name AS item_code,
item_name,
description,
stock_uom,
image AS item_image,
idx AS idx,
@ -53,10 +55,11 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
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 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
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 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
'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 @@ def item_group_query(doctype, txt, searchfield, start, page_len, filters):
{'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()

View File

@ -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();
})
}
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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);
})
}
}

View File

@ -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>`
)
}
}

View File

@ -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);
}
}

View File

@ -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');
}
}

View File

@ -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()
]);
});

Some files were not shown because too many files have changed in this diff Show More