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:
parent
8712ac6d39
commit
a6f98d48bc
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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>
|
149
erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
Normal file
149
erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
Normal 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);
|
||||
}
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
127
erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
Normal file
127
erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
Normal 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
|
@ -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'}
|
||||
]),
|
@ -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
|
@ -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
|
||||
}
|
@ -5,5 +5,5 @@
|
||||
from __future__ import unicode_literals
|
||||
from frappe.model.document import Document
|
||||
|
||||
class POSClosingVoucherTaxes(Document):
|
||||
class POSClosingEntryDetail(Document):
|
||||
pass
|
@ -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
|
||||
}
|
@ -5,5 +5,5 @@
|
||||
from __future__ import unicode_literals
|
||||
from frappe.model.document import Document
|
||||
|
||||
class POSClosingVoucherDetails(Document):
|
||||
class POSClosingEntryTaxes(Document):
|
||||
pass
|
205
erpnext/accounts/doctype/pos_invoice/pos_invoice.js
Normal file
205
erpnext/accounts/doctype/pos_invoice/pos_invoice.js
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
1637
erpnext/accounts/doctype/pos_invoice/pos_invoice.json
Normal file
1637
erpnext/accounts/doctype/pos_invoice/pos_invoice.json
Normal file
File diff suppressed because it is too large
Load Diff
374
erpnext/accounts/doctype/pos_invoice/pos_invoice.py
Normal file
374
erpnext/accounts/doctype/pos_invoice/pos_invoice.py
Normal 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()
|
42
erpnext/accounts/doctype/pos_invoice/pos_invoice_list.js
Normal file
42
erpnext/accounts/doctype/pos_invoice/pos_invoice_list.js
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
324
erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
Normal file
324
erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
Normal 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
|
805
erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
Normal file
805
erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
Normal 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"
|
||||
}
|
@ -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
|
@ -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': ''
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -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
|
||||
}
|
@ -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()
|
||||
|
@ -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`")
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
@ -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
|
||||
}
|
@ -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)
|
@ -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];
|
||||
}
|
||||
};
|
@ -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()
|
@ -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
|
||||
}
|
@ -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
|
@ -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"
|
||||
}
|
@ -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
|
@ -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'));
|
||||
|
@ -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",
|
||||
|
@ -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):
|
||||
|
@ -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']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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`
|
||||
|
@ -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
@ -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"
|
||||
}
|
@ -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()
|
||||
]);
|
||||
});
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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"],
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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': '',
|
||||
|
25
erpnext/patches/v12_0/rename_pos_closing_doctype.py
Normal file
25
erpnext/patches/v12_0/rename_pos_closing_doctype.py
Normal 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")
|
@ -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` = '')""")
|
29
erpnext/patches/v13_0/replace_pos_payment_mode_table.py
Normal file
29
erpnext/patches/v13_0/replace_pos_payment_mode_table.py
Normal 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)
|
@ -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()
|
@ -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; }
|
@ -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; });
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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')},
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@ -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)
|
@ -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
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
1
erpnext/selling/page/point_of_sale/onscan.js
Normal file
1
erpnext/selling/page/point_of_sale/onscan.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
@ -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()
|
714
erpnext/selling/page/point_of_sale/pos_controller.js
Normal file
714
erpnext/selling/page/point_of_sale/pos_controller.js
Normal 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();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
951
erpnext/selling/page/point_of_sale/pos_item_cart.js
Normal file
951
erpnext/selling/page/point_of_sale/pos_item_cart.js
Normal 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');
|
||||
}
|
||||
|
||||
}
|
394
erpnext/selling/page/point_of_sale/pos_item_details.js
Normal file
394
erpnext/selling/page/point_of_sale/pos_item_details.js
Normal 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');
|
||||
}
|
||||
}
|
265
erpnext/selling/page/point_of_sale/pos_item_selector.js
Normal file
265
erpnext/selling/page/point_of_sale/pos_item_selector.js
Normal 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');
|
||||
}
|
||||
}
|
49
erpnext/selling/page/point_of_sale/pos_number_pad.js
Normal file
49
erpnext/selling/page/point_of_sale/pos_number_pad.js
Normal 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);
|
||||
})
|
||||
}
|
||||
}
|
130
erpnext/selling/page/point_of_sale/pos_past_order_list.js
Normal file
130
erpnext/selling/page/point_of_sale/pos_past_order_list.js
Normal 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>`
|
||||
)
|
||||
}
|
||||
}
|
452
erpnext/selling/page/point_of_sale/pos_past_order_summary.js
Normal file
452
erpnext/selling/page/point_of_sale/pos_past_order_summary.js
Normal 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);
|
||||
}
|
||||
|
||||
}
|
503
erpnext/selling/page/point_of_sale/pos_payment.js
Normal file
503
erpnext/selling/page/point_of_sale/pos_payment.js
Normal 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');
|
||||
}
|
||||
}
|
@ -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
Loading…
x
Reference in New Issue
Block a user