Merge branch 'version-13-beta-pre-release' into version-13-beta

This commit is contained in:
Saurabh 2021-02-24 15:01:19 +05:30
commit d713ac4505
101 changed files with 1921 additions and 528 deletions

View File

@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
from frappe.utils import getdate from frappe.utils import getdate
__version__ = '13.0.0-beta.11' __version__ = '13.0.0-beta.12'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@ -33,11 +33,11 @@ class AccountingDimension(Document):
if frappe.flags.in_test: if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self) make_dimension_in_accounting_doctypes(doc=self)
else: else:
frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self) frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue='long')
def on_trash(self): def on_trash(self):
if frappe.flags.in_test: if frappe.flags.in_test:
delete_accounting_dimension(doc=self) delete_accounting_dimension(doc=self, queue='long')
else: else:
frappe.enqueue(delete_accounting_dimension, doc=self) frappe.enqueue(delete_accounting_dimension, doc=self)
@ -48,6 +48,9 @@ class AccountingDimension(Document):
if not self.fieldname: if not self.fieldname:
self.fieldname = scrub(self.label) self.fieldname = scrub(self.label)
def on_update(self):
frappe.flags.accounting_dimensions = None
def make_dimension_in_accounting_doctypes(doc): def make_dimension_in_accounting_doctypes(doc):
doclist = get_doctypes_with_dimensions() doclist = get_doctypes_with_dimensions()
doc_count = len(get_accounting_dimensions()) doc_count = len(get_accounting_dimensions())
@ -165,9 +168,9 @@ def toggle_disabling(doc):
frappe.clear_cache(doctype=doctype) frappe.clear_cache(doctype=doctype)
def get_doctypes_with_dimensions(): def get_doctypes_with_dimensions():
doclist = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset", doclist = ["GL Entry", "Sales Invoice", "POS Invoice", "Purchase Invoice", "Payment Entry", "Asset",
"Expense Claim", "Expense Claim Detail", "Expense Taxes and Charges", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", "Expense Claim", "Expense Claim Detail", "Expense Taxes and Charges", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note",
"Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", "Sales Invoice Item", "POS Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item",
"Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule", "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
"Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation", "Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
"Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription", "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
@ -176,12 +179,14 @@ def get_doctypes_with_dimensions():
return doclist return doclist
def get_accounting_dimensions(as_list=True): def get_accounting_dimensions(as_list=True):
accounting_dimensions = frappe.get_all("Accounting Dimension", fields=["label", "fieldname", "disabled", "document_type"]) if frappe.flags.accounting_dimensions is None:
frappe.flags.accounting_dimensions = frappe.get_all("Accounting Dimension",
fields=["label", "fieldname", "disabled", "document_type"])
if as_list: if as_list:
return [d.fieldname for d in accounting_dimensions] return [d.fieldname for d in frappe.flags.accounting_dimensions]
else: else:
return accounting_dimensions return frappe.flags.accounting_dimensions
def get_checks_for_pl_and_bs_accounts(): def get_checks_for_pl_and_bs_accounts():
dimensions = frappe.db.sql("""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs dimensions = frappe.db.sql("""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs

View File

@ -108,7 +108,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-12-16 15:27:23.659285", "modified": "2021-02-03 12:04:58.678402",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting Dimension Filter", "name": "Accounting Dimension Filter",
@ -125,6 +125,30 @@
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,

View File

@ -27,30 +27,30 @@ class GLEntry(Document):
def validate(self): def validate(self):
self.flags.ignore_submit_comment = True self.flags.ignore_submit_comment = True
self.check_mandatory()
self.validate_and_set_fiscal_year() self.validate_and_set_fiscal_year()
self.pl_must_have_cost_center() self.pl_must_have_cost_center()
self.validate_cost_center()
if not self.flags.from_repost: if not self.flags.from_repost:
self.check_mandatory()
self.validate_cost_center()
self.check_pl_account() self.check_pl_account()
self.validate_party() self.validate_party()
self.validate_currency() self.validate_currency()
def on_update_with_args(self, adv_adj, update_outstanding = 'Yes', from_repost=False): def on_update(self):
if not from_repost: adv_adj = self.flags.adv_adj
if not self.flags.from_repost:
self.validate_account_details(adv_adj) self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs() self.validate_dimensions_for_pl_and_bs()
self.validate_allowed_dimensions() self.validate_allowed_dimensions()
validate_balance_type(self.account, adv_adj)
validate_frozen_account(self.account, adv_adj)
validate_frozen_account(self.account, adv_adj) # Update outstanding amt on against voucher
validate_balance_type(self.account, adv_adj) if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees']
and self.against_voucher and self.flags.update_outstanding == 'Yes'):
# Update outstanding amt on against voucher update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
if self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] \ self.against_voucher)
and self.against_voucher and update_outstanding == 'Yes' and not from_repost:
update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
self.against_voucher)
def check_mandatory(self): def check_mandatory(self):
mandatory = ['account','voucher_type','voucher_no','company'] mandatory = ['account','voucher_type','voucher_no','company']
@ -58,7 +58,7 @@ class GLEntry(Document):
if not self.get(k): if not self.get(k):
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k)))) frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
account_type = frappe.db.get_value("Account", self.account, "account_type") account_type = frappe.get_cached_value("Account", self.account, "account_type")
if not (self.party_type and self.party): if not (self.party_type and self.party):
if account_type == "Receivable": if account_type == "Receivable":
frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}") frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}")
@ -73,7 +73,7 @@ class GLEntry(Document):
.format(self.voucher_type, self.voucher_no, self.account)) .format(self.voucher_type, self.voucher_no, self.account))
def pl_must_have_cost_center(self): def pl_must_have_cost_center(self):
if frappe.db.get_value("Account", self.account, "report_type") == "Profit and Loss": if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
if not self.cost_center and self.voucher_type != 'Period Closing Voucher': if not self.cost_center and self.voucher_type != 'Period Closing Voucher':
frappe.throw(_("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}. Please set up a default Cost Center for the Company.") frappe.throw(_("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}. Please set up a default Cost Center for the Company.")
.format(self.voucher_type, self.voucher_no, self.account)) .format(self.voucher_type, self.voucher_no, self.account))
@ -140,25 +140,16 @@ class GLEntry(Document):
.format(self.voucher_type, self.voucher_no, self.account, self.company)) .format(self.voucher_type, self.voucher_no, self.account, self.company))
def validate_cost_center(self): def validate_cost_center(self):
if not hasattr(self, "cost_center_company"): if not self.cost_center: return
self.cost_center_company = {}
def _get_cost_center_company(): is_group, company = frappe.get_cached_value('Cost Center',
if not self.cost_center_company.get(self.cost_center): self.cost_center, ['is_group', 'company'])
self.cost_center_company[self.cost_center] = frappe.db.get_value(
"Cost Center", self.cost_center, "company")
return self.cost_center_company[self.cost_center] if company != self.company:
def _check_is_group():
return cint(frappe.get_cached_value('Cost Center', self.cost_center, 'is_group'))
if self.cost_center and _get_cost_center_company() != self.company:
frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}") frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}")
.format(self.voucher_type, self.voucher_no, self.cost_center, self.company)) .format(self.voucher_type, self.voucher_no, self.cost_center, self.company))
if not self.flags.from_repost and not self.voucher_type == 'Period Closing Voucher' \ if (self.voucher_type != 'Period Closing Voucher' and is_group):
and self.cost_center and _check_is_group():
frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""").format( frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""").format(
self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
@ -184,7 +175,6 @@ class GLEntry(Document):
if not self.fiscal_year: if not self.fiscal_year:
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0] self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
def validate_balance_type(account, adv_adj=False): def validate_balance_type(account, adv_adj=False):
if not adv_adj and account: if not adv_adj and account:
balance_must_be = frappe.db.get_value("Account", account, "balance_must_be") balance_must_be = frappe.db.get_value("Account", account, "balance_must_be")
@ -250,7 +240,7 @@ def update_outstanding_amt(account, party_type, party, against_voucher_type, aga
def validate_frozen_account(account, adv_adj=None): def validate_frozen_account(account, adv_adj=None):
frozen_account = frappe.db.get_value("Account", account, "freeze_account") frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
if frozen_account == 'Yes' and not adv_adj: if frozen_account == 'Yes' and not adv_adj:
frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None, frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,
'frozen_accounts_modifier') 'frozen_accounts_modifier')

View File

@ -3,6 +3,7 @@
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
@ -82,18 +83,37 @@ class PaymentRequest(Document):
self.make_communication_entry() self.make_communication_entry()
elif self.payment_channel == "Phone": elif self.payment_channel == "Phone":
controller = get_payment_gateway_controller(self.payment_gateway) self.request_phone_payment()
payment_record = dict(
reference_doctype="Payment Request", def request_phone_payment(self):
reference_docname=self.name, controller = get_payment_gateway_controller(self.payment_gateway)
payment_reference=self.reference_name, request_amount = self.get_request_amount()
grand_total=self.grand_total,
sender=self.email_to, payment_record = dict(
currency=self.currency, reference_doctype="Payment Request",
payment_gateway=self.payment_gateway reference_docname=self.name,
) payment_reference=self.reference_name,
controller.validate_transaction_currency(self.currency) request_amount=request_amount,
controller.request_for_payment(**payment_record) sender=self.email_to,
currency=self.currency,
payment_gateway=self.payment_gateway
)
controller.validate_transaction_currency(self.currency)
controller.request_for_payment(**payment_record)
def get_request_amount(self):
data_of_completed_requests = frappe.get_all("Integration Request", filters={
'reference_doctype': self.doctype,
'reference_docname': self.name,
'status': 'Completed'
}, pluck="data")
if not data_of_completed_requests:
return self.grand_total
request_amounts = sum([json.loads(d).get('request_amount') for d in data_of_completed_requests])
return request_amounts
def on_cancel(self): def on_cancel(self):
self.check_if_payment_entry_exists() self.check_if_payment_entry_exists()
@ -351,8 +371,8 @@ def make_payment_request(**args):
if args.order_type == "Shopping Cart" or args.mute_email: if args.order_type == "Shopping Cart" or args.mute_email:
pr.flags.mute_email = True pr.flags.mute_email = True
pr.insert(ignore_permissions=True)
if args.submit_doc: if args.submit_doc:
pr.insert(ignore_permissions=True)
pr.submit() pr.submit()
if args.order_type == "Shopping Cart": if args.order_type == "Shopping Cart":
@ -412,8 +432,8 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
def get_gateway_details(args): def get_gateway_details(args):
"""return gateway and payment account of default payment gateway""" """return gateway and payment account of default payment gateway"""
if args.get("payment_gateway"): if args.get("payment_gateway_account"):
return get_payment_gateway_account(args.get("payment_gateway")) return get_payment_gateway_account(args.get("payment_gateway_account"))
if args.order_type == "Shopping Cart": if args.order_type == "Shopping Cart":
payment_gateway_account = frappe.get_doc("Shopping Cart Settings").payment_gateway_account payment_gateway_account = frappe.get_doc("Shopping Cart Settings").payment_gateway_account

View File

@ -45,7 +45,8 @@ class TestPaymentRequest(unittest.TestCase):
def test_payment_request_linkings(self): def test_payment_request_linkings(self):
so_inr = make_sales_order(currency="INR") so_inr = make_sales_order(currency="INR")
pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com") pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com",
payment_gateway_account="_Test Gateway - INR")
self.assertEqual(pr.reference_doctype, "Sales Order") self.assertEqual(pr.reference_doctype, "Sales Order")
self.assertEqual(pr.reference_name, so_inr.name) self.assertEqual(pr.reference_name, so_inr.name)
@ -54,7 +55,8 @@ class TestPaymentRequest(unittest.TestCase):
conversion_rate = get_exchange_rate("USD", "INR") conversion_rate = get_exchange_rate("USD", "INR")
si_usd = create_sales_invoice(currency="USD", conversion_rate=conversion_rate) si_usd = create_sales_invoice(currency="USD", conversion_rate=conversion_rate)
pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com") pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
payment_gateway_account="_Test Gateway - USD")
self.assertEqual(pr.reference_doctype, "Sales Invoice") self.assertEqual(pr.reference_doctype, "Sales Invoice")
self.assertEqual(pr.reference_name, si_usd.name) self.assertEqual(pr.reference_name, si_usd.name)
@ -68,7 +70,7 @@ class TestPaymentRequest(unittest.TestCase):
so_inr = make_sales_order(currency="INR") so_inr = make_sales_order(currency="INR")
pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com", pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com",
mute_email=1, submit_doc=1, return_doc=1) mute_email=1, payment_gateway_account="_Test Gateway - INR", submit_doc=1, return_doc=1)
pe = pr.set_as_paid() pe = pr.set_as_paid()
so_inr = frappe.get_doc("Sales Order", so_inr.name) so_inr = frappe.get_doc("Sales Order", so_inr.name)
@ -79,7 +81,7 @@ class TestPaymentRequest(unittest.TestCase):
currency="USD", conversion_rate=50) currency="USD", conversion_rate=50)
pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
mute_email=1, payment_gateway="_Test Gateway - USD", submit_doc=1, return_doc=1) mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1)
pe = pr.set_as_paid() pe = pr.set_as_paid()
@ -106,7 +108,7 @@ class TestPaymentRequest(unittest.TestCase):
currency="USD", conversion_rate=50) currency="USD", conversion_rate=50)
pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
mute_email=1, payment_gateway="_Test Gateway - USD", submit_doc=1, return_doc=1) mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1)
pe = pr.create_payment_entry() pe = pr.create_payment_entry()
pr.load_from_db() pr.load_from_db()

View File

@ -3,6 +3,7 @@
frappe.ui.form.on('POS Closing Entry', { frappe.ui.form.on('POS Closing Entry', {
onload: function(frm) { onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log'];
frm.set_query("pos_profile", function(doc) { frm.set_query("pos_profile", function(doc) {
return { return {
filters: { 'user': doc.user } filters: { 'user': doc.user }
@ -20,7 +21,7 @@ frappe.ui.form.on('POS Closing Entry', {
return { filters: { 'status': 'Open', 'docstatus': 1 } }; 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 === 0 && !frm.doc.amended_from) frm.set_value("period_end_date", frappe.datetime.now_datetime());
if (frm.doc.docstatus === 1) set_html_data(frm); if (frm.doc.docstatus === 1) set_html_data(frm);
}, },

View File

@ -11,6 +11,7 @@
"column_break_3", "column_break_3",
"posting_date", "posting_date",
"pos_opening_entry", "pos_opening_entry",
"status",
"section_break_5", "section_break_5",
"company", "company",
"column_break_7", "column_break_7",
@ -184,11 +185,27 @@
"label": "POS Opening Entry", "label": "POS Opening Entry",
"options": "POS Opening Entry", "options": "POS Opening Entry",
"reqd": 1 "reqd": 1
},
{
"allow_on_submit": 1,
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
"label": "Status",
"options": "Draft\nSubmitted\nQueued\nCancelled",
"print_hide": 1,
"read_only": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [
"modified": "2020-05-29 15:03:22.226113", {
"link_doctype": "POS Invoice Merge Log",
"link_fieldname": "pos_closing_entry"
}
],
"modified": "2021-01-12 12:21:05.388650",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Closing Entry", "name": "POS Closing Entry",

View File

@ -6,13 +6,12 @@ from __future__ import unicode_literals
import frappe import frappe
import json import json
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.utils import get_datetime, flt
from frappe.utils import getdate, get_datetime, flt from erpnext.controllers.status_updater import StatusUpdater
from collections import defaultdict
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data 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 from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices, unconsolidate_pos_invoices
class POSClosingEntry(Document): class POSClosingEntry(StatusUpdater):
def validate(self): def validate(self):
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open": 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")) frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
@ -64,17 +63,22 @@ class POSClosingEntry(Document):
frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True) frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
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): def get_payment_reconciliation_details(self):
currency = frappe.get_cached_value('Company', self.company, "default_currency") currency = frappe.get_cached_value('Company', self.company, "default_currency")
return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html", return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html",
{"data": self, "currency": currency}) {"data": self, "currency": currency})
def on_submit(self):
consolidate_pos_invoices(closing_entry=self)
def on_cancel(self):
unconsolidate_pos_invoices(closing_entry=self)
def update_opening_entry(self, for_cancel=False):
opening_entry = frappe.get_doc("POS Opening Entry", self.pos_opening_entry)
opening_entry.pos_closing_entry = self.name if not for_cancel else None
opening_entry.set_status()
opening_entry.save()
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs

View File

@ -0,0 +1,16 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
// render
frappe.listview_settings['POS Closing Entry'] = {
get_indicator: function(doc) {
var status_color = {
"Draft": "red",
"Submitted": "blue",
"Queued": "orange",
"Cancelled": "red"
};
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
}
};

View File

@ -13,7 +13,6 @@ from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profi
class TestPOSClosingEntry(unittest.TestCase): class TestPOSClosingEntry(unittest.TestCase):
def test_pos_closing_entry(self): def test_pos_closing_entry(self):
test_user, pos_profile = init_user_and_profile() test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name) opening_entry = create_opening_entry(pos_profile, test_user.name)
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1) pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
@ -45,6 +44,49 @@ class TestPOSClosingEntry(unittest.TestCase):
frappe.set_user("Administrator") frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Profile`")
def test_cancelling_of_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()
pos_inv1.load_from_db()
self.assertRaises(frappe.ValidationError, pos_inv1.cancel)
si_doc = frappe.get_doc("Sales Invoice", pos_inv1.consolidated_invoice)
self.assertRaises(frappe.ValidationError, si_doc.cancel)
pcv_doc.load_from_db()
pcv_doc.cancel()
si_doc.load_from_db()
pos_inv1.load_from_db()
self.assertEqual(si_doc.docstatus, 2)
self.assertEqual(pos_inv1.status, 'Paid')
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
def init_user_and_profile(**args): def init_user_and_profile(**args):
user = 'test@example.com' user = 'test@example.com'
test_user = frappe.get_doc('User', user) test_user = frappe.get_doc('User', user)

View File

@ -2,6 +2,7 @@
// For license information, please see license.txt // For license information, please see license.txt
{% include 'erpnext/selling/sales_common.js' %}; {% include 'erpnext/selling/sales_common.js' %};
frappe.provide("erpnext.accounts");
erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend({ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend({
setup(doc) { setup(doc) {
@ -9,12 +10,19 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend(
this._super(doc); this._super(doc);
}, },
company: function() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
},
onload(doc) { onload(doc) {
this._super(); this._super();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log'];
if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') { if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') {
this.frm.script_manager.trigger("is_pos"); this.frm.script_manager.trigger("is_pos");
this.frm.refresh_fields(); this.frm.refresh_fields();
} }
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}, },
refresh(doc) { refresh(doc) {
@ -187,18 +195,43 @@ frappe.ui.form.on('POS Invoice', {
}, },
request_for_payment: function (frm) { request_for_payment: function (frm) {
if (!frm.doc.contact_mobile) {
frappe.throw(__('Please enter mobile number first.'));
}
frm.dirty();
frm.save().then(() => { frm.save().then(() => {
frappe.dom.freeze(); frappe.dom.freeze(__('Waiting for payment...'));
frappe.call({ frappe
method: 'create_payment_request', .call({
doc: frm.doc, method: 'create_payment_request',
}) doc: frm.doc
})
.fail(() => { .fail(() => {
frappe.dom.unfreeze(); frappe.dom.unfreeze();
frappe.msgprint('Payment request failed'); frappe.msgprint(__('Payment request failed'));
}) })
.then(() => { .then(({ message }) => {
frappe.msgprint('Payment request sent successfully'); const payment_request_name = message.name;
setTimeout(() => {
frappe.db.get_value('Payment Request', payment_request_name, ['status', 'grand_total']).then(({ message }) => {
if (message.status != 'Paid') {
frappe.dom.unfreeze();
frappe.msgprint({
message: __('Payment Request took too long to respond. Please try requesting for payment again.'),
title: __('Request Timeout')
});
} else if (frappe.dom.freeze_count != 0) {
frappe.dom.unfreeze();
cur_frm.reload_doc();
cur_pos.payment.events.submit_invoice();
frappe.show_alert({
message: __("Payment of {0} received successfully.", [format_currency(message.grand_total, frm.doc.currency, 0)]),
indicator: 'green'
});
}
});
}, 60000);
}); });
}); });
} }

View File

@ -6,10 +6,9 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document 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.utils import get_account_currency
from erpnext.accounts.party import get_party_account, get_due_date from erpnext.accounts.party import get_party_account, get_due_date
from frappe.utils import cint, flt, getdate, nowdate, get_link_to_form
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
@ -58,6 +57,22 @@ class POSInvoice(SalesInvoice):
self.apply_loyalty_points() self.apply_loyalty_points()
self.check_phone_payments() self.check_phone_payments()
self.set_status(update=True) self.set_status(update=True)
def before_cancel(self):
if self.consolidated_invoice and frappe.db.get_value('Sales Invoice', self.consolidated_invoice, 'docstatus') == 1:
pos_closing_entry = frappe.get_all(
"POS Invoice Reference",
ignore_permissions=True,
filters={ 'pos_invoice': self.name },
pluck="parent",
limit=1
)
frappe.throw(
_('You need to cancel POS Closing Entry {} to be able to cancel this document.').format(
get_link_to_form("POS Closing Entry", pos_closing_entry[0])
),
title=_('Not Allowed')
)
def on_cancel(self): def on_cancel(self):
# run on cancel method of selling controller # run on cancel method of selling controller
@ -78,7 +93,7 @@ class POSInvoice(SalesInvoice):
mode_of_payment=pay.mode_of_payment, status="Paid"), mode_of_payment=pay.mode_of_payment, status="Paid"),
fieldname="grand_total") fieldname="grand_total")
if pay.amount != paid_amt: if paid_amt and pay.amount != paid_amt:
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment)) return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
def validate_stock_availablility(self): def validate_stock_availablility(self):
@ -296,14 +311,21 @@ class POSInvoice(SalesInvoice):
self.set(fieldname, profile.get(fieldname)) self.set(fieldname, profile.get(fieldname))
if self.customer: if self.customer:
customer_price_list, customer_group = frappe.db.get_value("Customer", self.customer, ['default_price_list', 'customer_group']) customer_price_list, customer_group, customer_currency = frappe.db.get_value(
"Customer", self.customer, ['default_price_list', 'customer_group', 'default_currency']
)
customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list') 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 profile.get('selling_price_list') selling_price_list = customer_price_list or customer_group_price_list or profile.get('selling_price_list')
if customer_currency != profile.get('currency'):
self.set('currency', customer_currency)
else: else:
selling_price_list = profile.get('selling_price_list') selling_price_list = profile.get('selling_price_list')
if selling_price_list: if selling_price_list:
self.set('selling_price_list', selling_price_list) self.set('selling_price_list', selling_price_list)
if customer_currency != profile.get('currency'):
self.set('currency', customer_currency)
# set pos values in items # set pos values in items
for item in self.get("items"): for item in self.get("items"):
@ -363,22 +385,48 @@ class POSInvoice(SalesInvoice):
if not self.contact_mobile: if not self.contact_mobile:
frappe.throw(_("Please enter the phone number first")) frappe.throw(_("Please enter the phone number first"))
payment_gateway = frappe.db.get_value("Payment Gateway Account", { pay_req = self.get_existing_payment_request(pay)
"payment_account": pay.account, if not pay_req:
}) pay_req = self.get_new_payment_request(pay)
record = { pay_req.submit()
"payment_gateway": payment_gateway, else:
"dt": "POS Invoice", pay_req.request_phone_payment()
"dn": self.name,
"payment_request_type": "Inward",
"party_type": "Customer",
"party": self.customer,
"mode_of_payment": pay.mode_of_payment,
"recipient_id": self.contact_mobile,
"submit_doc": True
}
return make_payment_request(**record) return pay_req
def get_new_payment_request(self, mop):
payment_gateway_account = frappe.db.get_value("Payment Gateway Account", {
"payment_account": mop.account,
}, ["name"])
args = {
"dt": "POS Invoice",
"dn": self.name,
"recipient_id": self.contact_mobile,
"mode_of_payment": mop.mode_of_payment,
"payment_gateway_account": payment_gateway_account,
"payment_request_type": "Inward",
"party_type": "Customer",
"party": self.customer,
"return_doc": True
}
return make_payment_request(**args)
def get_existing_payment_request(self, pay):
payment_gateway_account = frappe.db.get_value("Payment Gateway Account", {
"payment_account": pay.account,
}, ["name"])
args = {
'doctype': 'Payment Request',
'reference_doctype': 'POS Invoice',
'reference_name': self.name,
'payment_gateway_account': payment_gateway_account,
'email_to': self.contact_mobile
}
pr = frappe.db.exists(args)
if pr:
return frappe.get_doc('Payment Request', pr[0][0])
def add_return_modes(doc, pos_profile): def add_return_modes(doc, pos_profile):
def append_payment(payment_mode): def append_payment(payment_mode):

View File

@ -290,7 +290,7 @@ class TestPOSInvoice(unittest.TestCase):
def test_merging_into_sales_invoice_with_discount(self): def test_merging_into_sales_invoice_with_discount(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
frappe.db.sql("delete from `tabPOS Invoice`") frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile() test_user, pos_profile = init_user_and_profile()
@ -306,7 +306,7 @@ class TestPOSInvoice(unittest.TestCase):
}) })
pos_inv2.submit() pos_inv2.submit()
merge_pos_invoices() consolidate_pos_invoices()
pos_inv.load_from_db() pos_inv.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
@ -315,7 +315,7 @@ class TestPOSInvoice(unittest.TestCase):
def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self): def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
frappe.db.sql("delete from `tabPOS Invoice`") frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile() test_user, pos_profile = init_user_and_profile()
@ -348,7 +348,7 @@ class TestPOSInvoice(unittest.TestCase):
}) })
pos_inv2.submit() pos_inv2.submit()
merge_pos_invoices() consolidate_pos_invoices()
pos_inv.load_from_db() pos_inv.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
@ -357,7 +357,7 @@ class TestPOSInvoice(unittest.TestCase):
def test_merging_with_validate_selling_price(self): def test_merging_with_validate_selling_price(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"): if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 1) frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 1)
@ -393,7 +393,7 @@ class TestPOSInvoice(unittest.TestCase):
}) })
pos_inv2.submit() pos_inv2.submit()
merge_pos_invoices() consolidate_pos_invoices()
pos_inv2.load_from_db() pos_inv2.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total") rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")

View File

@ -7,6 +7,8 @@
"field_order": [ "field_order": [
"posting_date", "posting_date",
"customer", "customer",
"column_break_3",
"pos_closing_entry",
"section_break_3", "section_break_3",
"pos_invoices", "pos_invoices",
"references_section", "references_section",
@ -76,11 +78,22 @@
"label": "Consolidated Credit Note", "label": "Consolidated Credit Note",
"options": "Sales Invoice", "options": "Sales Invoice",
"read_only": 1 "read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "pos_closing_entry",
"fieldtype": "Link",
"label": "POS Closing Entry",
"options": "POS Closing Entry"
} }
], ],
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-05-29 15:08:41.317100", "modified": "2020-12-01 11:53:57.267579",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice Merge Log", "name": "POS Invoice Merge Log",

View File

@ -7,8 +7,11 @@ import frappe
from frappe import _ from frappe import _
from frappe.model import default_fields from frappe.model import default_fields
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt, getdate, nowdate
from frappe.utils.background_jobs import enqueue
from frappe.model.mapper import map_doc, map_child_doc from frappe.model.mapper import map_doc, map_child_doc
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate from frappe.utils.scheduler import is_scheduler_inactive
from frappe.core.page.background_jobs.background_jobs import get_info
from six import iteritems from six import iteritems
@ -61,7 +64,13 @@ class POSInvoiceMergeLog(Document):
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
self.update_pos_invoices(sales_invoice, credit_note) self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
def on_cancel(self):
pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
self.update_pos_invoices(pos_invoice_docs)
self.cancel_linked_invoices()
def process_merging_into_sales_invoice(self, data): def process_merging_into_sales_invoice(self, data):
sales_invoice = self.get_new_sales_invoice() sales_invoice = self.get_new_sales_invoice()
@ -111,6 +120,7 @@ class POSInvoiceMergeLog(Document):
i.qty = i.qty + item.qty i.qty = i.qty + item.qty
if not found: if not found:
item.rate = item.net_rate item.rate = item.net_rate
item.price_list_rate = 0
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"}) si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
items.append(si_item) items.append(si_item)
@ -148,6 +158,8 @@ class POSInvoiceMergeLog(Document):
invoice.set('taxes', taxes) invoice.set('taxes', taxes)
invoice.additional_discount_percentage = 0 invoice.additional_discount_percentage = 0
invoice.discount_amount = 0.0 invoice.discount_amount = 0.0
invoice.taxes_and_charges = None
invoice.ignore_pricing_rule = 1
return invoice return invoice
@ -160,17 +172,21 @@ class POSInvoiceMergeLog(Document):
return sales_invoice return sales_invoice
def update_pos_invoices(self, sales_invoice, credit_note): def update_pos_invoices(self, invoice_docs, sales_invoice='', credit_note=''):
for d in self.pos_invoices: for doc in invoice_docs:
doc = frappe.get_doc('POS Invoice', d.pos_invoice) doc.load_from_db()
if not doc.is_return: doc.update({ 'consolidated_invoice': None if self.docstatus==2 else (credit_note if doc.is_return else sales_invoice) })
doc.update({'consolidated_invoice': sales_invoice})
else:
doc.update({'consolidated_invoice': credit_note})
doc.set_status(update=True) doc.set_status(update=True)
doc.save() doc.save()
def get_all_invoices(): def cancel_linked_invoices(self):
for si_name in [self.consolidated_invoice, self.consolidated_credit_note]:
if not si_name: continue
si = frappe.get_doc('Sales Invoice', si_name)
si.flags.ignore_validate = True
si.cancel()
def get_all_unconsolidated_invoices():
filters = { filters = {
'consolidated_invoice': [ 'in', [ '', None ]], 'consolidated_invoice': [ 'in', [ '', None ]],
'status': ['not in', ['Consolidated']], 'status': ['not in', ['Consolidated']],
@ -181,7 +197,7 @@ def get_all_invoices():
return pos_invoices return pos_invoices
def get_invoices_customer_map(pos_invoices): def get_invoice_customer_map(pos_invoices):
# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Custoemr 2' : [{}] } # pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Custoemr 2' : [{}] }
pos_invoice_customer_map = {} pos_invoice_customer_map = {}
for invoice in pos_invoices: for invoice in pos_invoices:
@ -191,20 +207,82 @@ def get_invoices_customer_map(pos_invoices):
return pos_invoice_customer_map return pos_invoice_customer_map
def merge_pos_invoices(pos_invoices=[]): def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
if not pos_invoices: invoices = pos_invoices or closing_entry.get('pos_transactions') or get_all_unconsolidated_invoices()
pos_invoices = get_all_invoices() invoice_by_customer = get_invoice_customer_map(invoices)
pos_invoice_map = get_invoices_customer_map(pos_invoices)
create_merge_logs(pos_invoice_map)
def create_merge_logs(pos_invoice_customer_map): if len(invoices) >= 5 and closing_entry:
for customer, invoices in iteritems(pos_invoice_customer_map): enqueue_job(create_merge_logs, invoice_by_customer, closing_entry)
closing_entry.set_status(update=True, status='Queued')
else:
create_merge_logs(invoice_by_customer, closing_entry)
def unconsolidate_pos_invoices(closing_entry):
merge_logs = frappe.get_all(
'POS Invoice Merge Log',
filters={ 'pos_closing_entry': closing_entry.name },
pluck='name'
)
if len(merge_logs) >= 5:
enqueue_job(cancel_merge_logs, merge_logs, closing_entry)
closing_entry.set_status(update=True, status='Queued')
else:
cancel_merge_logs(merge_logs, closing_entry)
def create_merge_logs(invoice_by_customer, closing_entry={}):
for customer, invoices in iteritems(invoice_by_customer):
merge_log = frappe.new_doc('POS Invoice Merge Log') merge_log = frappe.new_doc('POS Invoice Merge Log')
merge_log.posting_date = getdate(nowdate()) merge_log.posting_date = getdate(nowdate())
merge_log.customer = customer merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get('name', None)
merge_log.set('pos_invoices', invoices) merge_log.set('pos_invoices', invoices)
merge_log.save(ignore_permissions=True) merge_log.save(ignore_permissions=True)
merge_log.submit() merge_log.submit()
if closing_entry:
closing_entry.set_status(update=True, status='Submitted')
closing_entry.update_opening_entry()
def cancel_merge_logs(merge_logs, closing_entry={}):
for log in merge_logs:
merge_log = frappe.get_doc('POS Invoice Merge Log', log)
merge_log.flags.ignore_permissions = True
merge_log.cancel()
if closing_entry:
closing_entry.set_status(update=True, status='Cancelled')
closing_entry.update_opening_entry(for_cancel=True)
def enqueue_job(job, invoice_by_customer, closing_entry):
check_scheduler_status()
job_name = closing_entry.get("name")
if not job_already_enqueued(job_name):
enqueue(
job,
queue="long",
timeout=10000,
event="processing_merge_logs",
job_name=job_name,
closing_entry=closing_entry,
invoice_by_customer=invoice_by_customer,
now=frappe.conf.developer_mode or frappe.flags.in_test
)
if job == create_merge_logs:
msg = _('POS Invoices will be consolidated in a background process')
else:
msg = _('POS Invoices will be unconsolidated in a background process')
frappe.msgprint(msg, alert=1)
def check_scheduler_status():
if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
def job_already_enqueued(job_name):
enqueued_jobs = [d.get("job_name") for d in get_info()]
if job_name in enqueued_jobs:
return True

View File

@ -7,7 +7,7 @@ import frappe
import unittest import unittest
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice 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.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_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
class TestPOSInvoiceMergeLog(unittest.TestCase): class TestPOSInvoiceMergeLog(unittest.TestCase):
@ -34,7 +34,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
}) })
pos_inv3.submit() pos_inv3.submit()
merge_pos_invoices() consolidate_pos_invoices()
pos_inv.load_from_db() pos_inv.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
@ -79,7 +79,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
pos_inv_cn.paid_amount = -300 pos_inv_cn.paid_amount = -300
pos_inv_cn.submit() pos_inv_cn.submit()
merge_pos_invoices() consolidate_pos_invoices()
pos_inv.load_from_db() pos_inv.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))

View File

@ -6,7 +6,6 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint, get_link_to_form from frappe.utils import cint, get_link_to_form
from frappe.model.document import Document
from erpnext.controllers.status_updater import StatusUpdater from erpnext.controllers.status_updater import StatusUpdater
class POSOpeningEntry(StatusUpdater): class POSOpeningEntry(StatusUpdater):

View File

@ -5,7 +5,7 @@
frappe.listview_settings['POS Opening Entry'] = { frappe.listview_settings['POS Opening Entry'] = {
get_indicator: function(doc) { get_indicator: function(doc) {
var status_color = { var status_color = {
"Draft": "grey", "Draft": "red",
"Open": "orange", "Open": "orange",
"Closed": "green", "Closed": "green",
"Cancelled": "red" "Cancelled": "red"

View File

@ -12,8 +12,6 @@
"company", "company",
"country", "country",
"column_break_9", "column_break_9",
"update_stock",
"ignore_pricing_rule",
"warehouse", "warehouse",
"campaign", "campaign",
"company_address", "company_address",
@ -25,8 +23,14 @@
"hide_images", "hide_images",
"hide_unavailable_items", "hide_unavailable_items",
"auto_add_item_to_cart", "auto_add_item_to_cart",
"item_groups",
"column_break_16", "column_break_16",
"update_stock",
"ignore_pricing_rule",
"allow_rate_change",
"allow_discount_change",
"section_break_23",
"item_groups",
"column_break_25",
"customer_groups", "customer_groups",
"section_break_16", "section_break_16",
"print_format", "print_format",
@ -309,6 +313,7 @@
"default": "1", "default": "1",
"fieldname": "update_stock", "fieldname": "update_stock",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1,
"label": "Update Stock", "label": "Update Stock",
"read_only": 1 "read_only": 1
}, },
@ -329,13 +334,34 @@
"fieldname": "auto_add_item_to_cart", "fieldname": "auto_add_item_to_cart",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Automatically Add Filtered Item To Cart" "label": "Automatically Add Filtered Item To Cart"
},
{
"default": "0",
"fieldname": "allow_rate_change",
"fieldtype": "Check",
"label": "Allow User to Edit Rate"
},
{
"default": "0",
"fieldname": "allow_discount_change",
"fieldtype": "Check",
"label": "Allow User to Edit Discount"
},
{
"collapsible": 1,
"fieldname": "section_break_23",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_25",
"fieldtype": "Column Break"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-12-20 13:59:28.877572", "modified": "2021-01-06 14:42:41.713864",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Profile", "name": "POS Profile",

View File

@ -40,6 +40,7 @@
"base_rate", "base_rate",
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"stock_uom_rate",
"is_free_item", "is_free_item",
"section_break_22", "section_break_22",
"net_rate", "net_rate",
@ -783,6 +784,14 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"depends_on": "eval: doc.uom != doc.stock_uom",
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"options": "currency",
"read_only": 1
},
{ {
"fieldname": "sales_invoice_item", "fieldname": "sales_invoice_item",
"fieldtype": "Data", "fieldtype": "Data",
@ -795,7 +804,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-26 17:20:36.415791", "modified": "2021-01-30 21:43:21.488258",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@ -20,6 +20,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
var me = this; var me = this;
this._super(); this._super();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice'];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format // show debit_to in print format
this.frm.set_df_property("debit_to", "print_hide", 0); this.frm.set_df_property("debit_to", "print_hide", 0);

View File

@ -1987,8 +1987,15 @@
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 181, "idx": 181,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [
"modified": "2020-12-25 22:57:32.555067", {
"custom": 1,
"group": "Reference",
"link_doctype": "POS Invoice",
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2021-01-12 12:16:15.192520",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -236,7 +236,25 @@ class SalesInvoice(SellingController):
if len(self.payments) == 0 and self.is_pos: if len(self.payments) == 0 and self.is_pos:
frappe.throw(_("At least one mode of payment is required for POS invoice.")) frappe.throw(_("At least one mode of payment is required for POS invoice."))
def check_if_consolidated_invoice(self):
# since POS Invoice extends Sales Invoice, we explicitly check if doctype is Sales Invoice
if self.doctype == "Sales Invoice" and self.is_consolidated:
invoice_or_credit_note = "consolidated_credit_note" if self.is_return else "consolidated_invoice"
pos_closing_entry = frappe.get_all(
"POS Invoice Merge Log",
filters={ invoice_or_credit_note: self.name },
pluck="pos_closing_entry"
)
if pos_closing_entry:
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}. ").format(
frappe.bold("Consolidated Sales Invoice"),
get_link_to_form("POS Closing Entry", pos_closing_entry[0])
)
frappe.throw(msg, title=_("Not Allowed"))
def before_cancel(self): def before_cancel(self):
self.check_if_consolidated_invoice()
super(SalesInvoice, self).before_cancel() super(SalesInvoice, self).before_cancel()
self.update_time_sheet(None) self.update_time_sheet(None)
@ -438,7 +456,9 @@ class SalesInvoice(SellingController):
if not for_validate and not self.customer: if not for_validate and not self.customer:
self.customer = pos.customer self.customer = pos.customer
self.ignore_pricing_rule = pos.ignore_pricing_rule if not for_validate:
self.ignore_pricing_rule = pos.ignore_pricing_rule
if pos.get('account_for_change_amount'): if pos.get('account_for_change_amount'):
self.account_for_change_amount = pos.get('account_for_change_amount') self.account_for_change_amount = pos.get('account_for_change_amount')

View File

@ -45,6 +45,7 @@
"base_rate", "base_rate",
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"stock_uom_rate",
"is_free_item", "is_free_item",
"section_break_21", "section_break_21",
"net_rate", "net_rate",
@ -811,12 +812,20 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"depends_on": "eval: doc.uom != doc.stock_uom",
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"options": "currency",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-26 17:25:04.090630", "modified": "2021-01-30 21:42:37.796771",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@ -44,9 +44,9 @@ def validate_accounting_period(gl_map):
frappe.throw(_("You cannot create or cancel any accounting entries with in the closed Accounting Period {0}") frappe.throw(_("You cannot create or cancel any accounting entries with in the closed Accounting Period {0}")
.format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod) .format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod)
def process_gl_map(gl_map, merge_entries=True): def process_gl_map(gl_map, merge_entries=True, precision=None):
if merge_entries: if merge_entries:
gl_map = merge_similar_entries(gl_map) gl_map = merge_similar_entries(gl_map, precision)
for entry in gl_map: for entry in gl_map:
# toggle debit, credit if negative entry # toggle debit, credit if negative entry
if flt(entry.debit) < 0: if flt(entry.debit) < 0:
@ -69,7 +69,7 @@ def process_gl_map(gl_map, merge_entries=True):
return gl_map return gl_map
def merge_similar_entries(gl_map): def merge_similar_entries(gl_map, precision=None):
merged_gl_map = [] merged_gl_map = []
accounting_dimensions = get_accounting_dimensions() accounting_dimensions = get_accounting_dimensions()
for entry in gl_map: for entry in gl_map:
@ -88,7 +88,9 @@ def merge_similar_entries(gl_map):
company = gl_map[0].company if gl_map else erpnext.get_default_company() company = gl_map[0].company if gl_map else erpnext.get_default_company()
company_currency = erpnext.get_company_currency(company) company_currency = erpnext.get_company_currency(company)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
if not precision:
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
# filter zero debit and credit entries # filter zero debit and credit entries
merged_gl_map = filter(lambda x: flt(x.debit, precision)!=0 or flt(x.credit, precision)!=0, merged_gl_map) merged_gl_map = filter(lambda x: flt(x.debit, precision)!=0 or flt(x.credit, precision)!=0, merged_gl_map)
@ -132,8 +134,8 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle.update(args) gle.update(args)
gle.flags.ignore_permissions = 1 gle.flags.ignore_permissions = 1
gle.flags.from_repost = from_repost gle.flags.from_repost = from_repost
gle.insert() gle.flags.adv_adj = adv_adj
gle.run_method("on_update_with_args", adv_adj, update_outstanding, from_repost) gle.flags.update_outstanding = update_outstanding or 'Yes'
gle.submit() gle.submit()
if not from_repost: if not from_repost:

View File

@ -222,7 +222,7 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
set_gl_entries_by_account(start_date, set_gl_entries_by_account(start_date,
end_date, root.lft, root.rgt, filters, end_date, root.lft, root.rgt, filters,
gl_entries_by_account, accounts_by_name, ignore_closing_entries=False) gl_entries_by_account, accounts_by_name, accounts, ignore_closing_entries=False)
calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters) calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters)
accumulate_values_into_parents(accounts, accounts_by_name, companies) accumulate_values_into_parents(accounts, accounts_by_name, companies)
@ -339,7 +339,7 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
return data return data
def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, gl_entries_by_account, def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, gl_entries_by_account,
accounts_by_name, ignore_closing_entries=False): accounts_by_name, accounts, ignore_closing_entries=False):
"""Returns a dict like { "account": [gl entries], ... }""" """Returns a dict like { "account": [gl entries], ... }"""
company_lft, company_rgt = frappe.get_cached_value('Company', company_lft, company_rgt = frappe.get_cached_value('Company',
@ -382,15 +382,31 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
for entry in gl_entries: for entry in gl_entries:
key = entry.account_number or entry.account_name key = entry.account_number or entry.account_name
validate_entries(key, entry, accounts_by_name) validate_entries(key, entry, accounts_by_name, accounts)
gl_entries_by_account.setdefault(key, []).append(entry) gl_entries_by_account.setdefault(key, []).append(entry)
return gl_entries_by_account return gl_entries_by_account
def validate_entries(key, entry, accounts_by_name): def get_account_details(account):
return frappe.get_cached_value('Account', account, ['name', 'report_type', 'root_type', 'company',
'is_group', 'account_name', 'account_number', 'parent_account', 'lft', 'rgt'], as_dict=1)
def validate_entries(key, entry, accounts_by_name, accounts):
if key not in accounts_by_name: if key not in accounts_by_name:
field = "Account number" if entry.account_number else "Account name" args = get_account_details(entry.account)
frappe.throw(_("{0} {1} is not present in the parent company").format(field, key))
if args.parent_account:
parent_args = get_account_details(args.parent_account)
args.update({
'lft': parent_args.lft + 1,
'rgt': parent_args.rgt - 1,
'root_type': parent_args.root_type,
'report_type': parent_args.report_type
})
accounts_by_name.setdefault(key, args)
accounts.append(args)
def get_additional_conditions(from_date, ignore_closing_entries, filters): def get_additional_conditions(from_date, ignore_closing_entries, filters):
additional_conditions = [] additional_conditions = []

View File

@ -82,7 +82,7 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb
error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(label, formatdate(transaction_date)) error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(label, formatdate(transaction_date))
if company: if company:
error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company)) error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))
if verbose==1: frappe.msgprint(error_msg) if verbose==1: frappe.msgprint(error_msg)
raise FiscalYearError(error_msg) raise FiscalYearError(error_msg)
@ -888,6 +888,11 @@ def get_coa(doctype, parent, is_root, chart=None):
def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None, def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None,
warehouse_account=None, company=None): warehouse_account=None, company=None):
stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items, company)
repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company, warehouse_account)
def repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company=None, warehouse_account=None):
def _delete_gl_entries(voucher_type, voucher_no): def _delete_gl_entries(voucher_type, voucher_no):
frappe.db.sql("""delete from `tabGL Entry` frappe.db.sql("""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no)) where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no))
@ -895,21 +900,21 @@ def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for
if not warehouse_account: if not warehouse_account:
warehouse_account = get_warehouse_account_map(company) warehouse_account = get_warehouse_account_map(company)
future_stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items) precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
gle = get_voucherwise_gl_entries(future_stock_vouchers, posting_date)
for voucher_type, voucher_no in future_stock_vouchers: gle = get_voucherwise_gl_entries(stock_vouchers, posting_date)
for voucher_type, voucher_no in stock_vouchers:
existing_gle = gle.get((voucher_type, voucher_no), []) existing_gle = gle.get((voucher_type, voucher_no), [])
voucher_obj = frappe.get_doc(voucher_type, voucher_no) voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no)
expected_gle = voucher_obj.get_gl_entries(warehouse_account) expected_gle = voucher_obj.get_gl_entries(warehouse_account)
if expected_gle: if expected_gle:
if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle): if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
_delete_gl_entries(voucher_type, voucher_no) _delete_gl_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True) voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else: else:
_delete_gl_entries(voucher_type, voucher_no) _delete_gl_entries(voucher_type, voucher_no)
def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None): def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None, company=None):
future_stock_vouchers = [] future_stock_vouchers = []
values = [] values = []
@ -922,6 +927,10 @@ def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, f
condition += " and warehouse in ({})".format(", ".join(["%s"] * len(for_warehouses))) condition += " and warehouse in ({})".format(", ".join(["%s"] * len(for_warehouses)))
values += for_warehouses values += for_warehouses
if company:
condition += " and company = %s"
values.append(company)
for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
from `tabStock Ledger Entry` sle from `tabStock Ledger Entry` sle
where where
@ -945,16 +954,17 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
return gl_entries return gl_entries
def compare_existing_and_expected_gle(existing_gle, expected_gle): def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
matched = True matched = True
for entry in expected_gle: for entry in expected_gle:
account_existed = False account_existed = False
for e in existing_gle: for e in existing_gle:
if entry.account == e.account: if entry.account == e.account:
account_existed = True account_existed = True
if entry.account == e.account and entry.against_account == e.against_account \ if (entry.account == e.account and entry.against_account == e.against_account
and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \ and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center)
and (entry.debit != e.debit or entry.credit != e.credit): and ( flt(entry.debit, precision) != flt(e.debit, precision) or
flt(entry.credit, precision) != flt(e.credit, precision))):
matched = False matched = False
break break
if not account_existed: if not account_existed:
@ -982,7 +992,7 @@ def check_if_stock_and_account_balance_synced(posting_date, company, voucher_typ
error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses as on {3}.").format( error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses as on {3}.").format(
stock_bal, account_bal, frappe.bold(account), posting_date) stock_bal, account_bal, frappe.bold(account), posting_date)
error_resolution = _("Please create an adjustment Journal Entry for amount {0} on {1}")\ error_resolution = _("Please create an adjustment Journal Entry for amount {0} on {1}")\
.format(frappe.bold(diff), frappe.bold(posting_date)) .format(frappe.bold(diff), frappe.bold(posting_date))
frappe.msgprint( frappe.msgprint(
msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution), msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution),

View File

@ -40,6 +40,7 @@
"base_rate", "base_rate",
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"stock_uom_rate",
"is_free_item", "is_free_item",
"section_break_29", "section_break_29",
"net_rate", "net_rate",
@ -726,13 +727,21 @@
"fieldname": "more_info_section_break", "fieldname": "more_info_section_break",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "More Information" "label": "More Information"
},
{
"depends_on": "eval: doc.uom != doc.stock_uom",
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"options": "currency",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-07 11:59:47.670951", "modified": "2021-01-30 21:44:41.816974",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",

View File

@ -0,0 +1,14 @@
### Version 13.0.0 Beta 12 Release Notes
#### Features
- Department wise Appointment Type charges ([#24572](https://github.com/frappe/erpnext/pull/24572))
- Capture Rate of stock UOM in purchase ([#24315](https://github.com/frappe/erpnext/pull/24315))
#### Fixes
- Fixed stock and account balance syncing ([#24644](https://github.com/frappe/erpnext/pull/24644))
- Fixed incorrect stock ledger qty in the stock ledger report and bin ([#24649](https://github.com/frappe/erpnext/pull/24649))
- Added patch to fix incorrect stock ledger and stock account value ([#24702](https://github.com/frappe/erpnext/pull/24702))
- Skip e-invoice generation for non-taxable invoices ([#24568](https://github.com/frappe/erpnext/pull/24568))
- Cannot cancel old invoices if eligible for e-invoicing ([#24608](https://github.com/frappe/erpnext/pull/24608))
- Mpesa fixes and enhancement ([#24306](https://github.com/frappe/erpnext/pull/24306))
- Fixed Consolidated Financial Statement report ([#24580](https://github.com/frappe/erpnext/pull/24580))

View File

@ -302,6 +302,7 @@ class AccountsController(TransactionBase):
args["doctype"] = self.doctype args["doctype"] = self.doctype
args["name"] = self.name args["name"] = self.name
args["child_docname"] = item.name args["child_docname"] = item.name
args["ignore_pricing_rule"] = self.ignore_pricing_rule if hasattr(self, 'ignore_pricing_rule') else 0
if not args.get("transaction_date"): if not args.get("transaction_date"):
args["transaction_date"] = args.get("posting_date") args["transaction_date"] = args.get("posting_date")
@ -1503,6 +1504,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.flags.ignore_validate_update_after_submit = True parent.flags.ignore_validate_update_after_submit = True
parent.set_qty_as_per_stock_uom() parent.set_qty_as_per_stock_uom()
parent.calculate_taxes_and_totals() parent.calculate_taxes_and_totals()
parent.set_total_in_words()
if parent_doctype == "Sales Order": if parent_doctype == "Sales Order":
make_packing_list(parent) make_packing_list(parent)
parent.set_gross_profit() parent.set_gross_profit()

View File

@ -456,9 +456,13 @@ class SellingController(StockController):
check_list, chk_dupl_itm = [], [] check_list, chk_dupl_itm = [], []
if cint(frappe.db.get_single_value("Selling Settings", "allow_multiple_items")): if cint(frappe.db.get_single_value("Selling Settings", "allow_multiple_items")):
return return
if self.doctype == "Sales Invoice" and self.is_consolidated:
return
if self.doctype == "POS Invoice":
return
for d in self.get('items'): for d in self.get('items'):
if self.doctype in ["POS Invoice","Sales Invoice"]: if self.doctype == "Sales Invoice":
stock_items = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or ''] stock_items = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or '']
non_stock_items = [d.item_code, d.description, d.sales_order or d.delivery_note] non_stock_items = [d.item_code, d.description, d.sales_order or d.delivery_note]
elif self.doctype == "Delivery Note": elif self.doctype == "Delivery Note":
@ -469,13 +473,19 @@ class SellingController(StockController):
non_stock_items = [d.item_code, d.description] non_stock_items = [d.item_code, d.description]
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1: if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1:
duplicate_items_msg = _("Item {0} entered multiple times.").format(frappe.bold(d.item_code))
duplicate_items_msg += "<br><br>"
duplicate_items_msg += _("Please enable {} in {} to allow same item in multiple rows").format(
frappe.bold("Allow Item to Be Added Multiple Times in a Transaction"),
get_link_to_form("Selling Settings", "Selling Settings")
)
if stock_items in check_list: if stock_items in check_list:
frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code)) frappe.throw(duplicate_items_msg)
else: else:
check_list.append(stock_items) check_list.append(stock_items)
else: else:
if non_stock_items in chk_dupl_itm: if non_stock_items in chk_dupl_itm:
frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code)) frappe.throw(duplicate_items_msg)
else: else:
chk_dupl_itm.append(non_stock_items) chk_dupl_itm.append(non_stock_items)

View File

@ -93,6 +93,12 @@ status_map = {
["Open", "eval:self.docstatus == 1 and not self.pos_closing_entry"], ["Open", "eval:self.docstatus == 1 and not self.pos_closing_entry"],
["Closed", "eval:self.docstatus == 1 and self.pos_closing_entry"], ["Closed", "eval:self.docstatus == 1 and self.pos_closing_entry"],
["Cancelled", "eval:self.docstatus == 2"], ["Cancelled", "eval:self.docstatus == 2"],
],
"POS Closing Entry": [
["Draft", None],
["Submitted", "eval:self.docstatus == 1"],
["Queued", "eval:self.status == 'Queued'"],
["Cancelled", "eval:self.docstatus == 2"],
] ]
} }

View File

@ -24,6 +24,7 @@ class StockController(AccountsController):
self.validate_inspection() self.validate_inspection()
self.validate_serialized_batch() self.validate_serialized_batch()
self.validate_customer_provided_item() self.validate_customer_provided_item()
self.set_rate_of_stock_uom()
self.validate_internal_transfer() self.validate_internal_transfer()
self.validate_putaway_capacity() self.validate_putaway_capacity()
@ -73,7 +74,7 @@ class StockController(AccountsController):
gl_list = [] gl_list = []
warehouse_with_no_account = [] warehouse_with_no_account = []
precision = frappe.get_precision("GL Entry", "debit_in_account_currency") precision = self.get_debit_field_precision()
for item_row in voucher_details: for item_row in voucher_details:
sle_list = sle_map.get(item_row.name) sle_list = sle_map.get(item_row.name)
@ -130,7 +131,13 @@ class StockController(AccountsController):
if frappe.db.get_value("Warehouse", wh, "company"): if frappe.db.get_value("Warehouse", wh, "company"):
frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company)) frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company))
return process_gl_map(gl_list) return process_gl_map(gl_list, precision=precision)
def get_debit_field_precision(self):
if not frappe.flags.debit_field_precision:
frappe.flags.debit_field_precision = frappe.get_precision("GL Entry", "debit_in_account_currency")
return frappe.flags.debit_field_precision
def update_stock_ledger_entries(self, sle): def update_stock_ledger_entries(self, sle):
sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
@ -243,7 +250,7 @@ class StockController(AccountsController):
.format(item.idx, frappe.bold(item.item_code), msg), title=_("Expense Account Missing")) .format(item.idx, frappe.bold(item.item_code), msg), title=_("Expense Account Missing"))
else: else:
is_expense_account = frappe.db.get_value("Account", is_expense_account = frappe.get_cached_value("Account",
item.get("expense_account"), "report_type")=="Profit and Loss" item.get("expense_account"), "report_type")=="Profit and Loss"
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Stock Reconciliation", "Stock Entry") and not is_expense_account: if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Stock Reconciliation", "Stock Entry") and not is_expense_account:
frappe.throw(_("Expense / Difference account ({0}) must be a 'Profit or Loss' account") frappe.throw(_("Expense / Difference account ({0}) must be a 'Profit or Loss' account")
@ -395,6 +402,11 @@ class StockController(AccountsController):
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'): if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
d.allow_zero_valuation_rate = 1 d.allow_zero_valuation_rate = 1
def set_rate_of_stock_uom(self):
if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]:
for d in self.get("items"):
d.stock_uom_rate = d.rate / d.conversion_factor
def validate_internal_transfer(self): def validate_internal_transfer(self):
if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \ if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \
and self.is_internal_transfer(): and self.is_internal_transfer():
@ -481,7 +493,6 @@ class StockController(AccountsController):
"voucher_no": self.name, "voucher_no": self.name,
"company": self.company "company": self.company
}) })
if check_if_future_sle_exists(args): if check_if_future_sle_exists(args):
create_repost_item_valuation_entry(args) create_repost_item_valuation_entry(args)
elif not is_reposting_pending(): elif not is_reposting_pending():

View File

@ -107,7 +107,7 @@ class calculate_taxes_and_totals(object):
elif item.discount_amount and item.pricing_rules: elif item.discount_amount and item.pricing_rules:
item.rate = item.price_list_rate - item.discount_amount item.rate = item.price_list_rate - item.discount_amount
if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item']: if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item']:
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item) item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
if flt(item.rate_with_margin) > 0: if flt(item.rate_with_margin) > 0:
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))

View File

@ -176,7 +176,7 @@ class Lead(SellingController):
"phone": self.mobile_no "phone": self.mobile_no
}) })
contact.insert() contact.insert(ignore_permissions=True)
return contact return contact

View File

@ -5,7 +5,7 @@ import datetime
class MpesaConnector(): class MpesaConnector():
def __init__(self, env="sandbox", app_key=None, app_secret=None, sandbox_url="https://sandbox.safaricom.co.ke", def __init__(self, env="sandbox", app_key=None, app_secret=None, sandbox_url="https://sandbox.safaricom.co.ke",
live_url="https://safaricom.co.ke"): live_url="https://api.safaricom.co.ke"):
"""Setup configuration for Mpesa connector and generate new access token.""" """Setup configuration for Mpesa connector and generate new access token."""
self.env = env self.env = env
self.app_key = app_key self.app_key = app_key
@ -102,14 +102,14 @@ class MpesaConnector():
"BusinessShortCode": business_shortcode, "BusinessShortCode": business_shortcode,
"Password": encoded.decode("utf-8"), "Password": encoded.decode("utf-8"),
"Timestamp": time, "Timestamp": time,
"TransactionType": "CustomerPayBillOnline",
"Amount": amount, "Amount": amount,
"PartyA": int(phone_number), "PartyA": int(phone_number),
"PartyB": business_shortcode, "PartyB": reference_code,
"PhoneNumber": int(phone_number), "PhoneNumber": int(phone_number),
"CallBackURL": callback_url, "CallBackURL": callback_url,
"AccountReference": reference_code, "AccountReference": reference_code,
"TransactionDesc": description "TransactionDesc": description,
"TransactionType": "CustomerPayBillOnline" if self.env == "sandbox" else "CustomerBuyGoodsOnline"
} }
headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"} headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"}

View File

@ -11,8 +11,10 @@
"consumer_secret", "consumer_secret",
"initiator_name", "initiator_name",
"till_number", "till_number",
"transaction_limit",
"sandbox", "sandbox",
"column_break_4", "column_break_4",
"business_shortcode",
"online_passkey", "online_passkey",
"security_credential", "security_credential",
"get_account_balance", "get_account_balance",
@ -84,10 +86,24 @@
"fieldname": "get_account_balance", "fieldname": "get_account_balance",
"fieldtype": "Button", "fieldtype": "Button",
"label": "Get Account Balance" "label": "Get Account Balance"
},
{
"depends_on": "eval:(doc.sandbox==0)",
"fieldname": "business_shortcode",
"fieldtype": "Data",
"label": "Business Shortcode",
"mandatory_depends_on": "eval:(doc.sandbox==0)"
},
{
"default": "150000",
"fieldname": "transaction_limit",
"fieldtype": "Float",
"label": "Transaction Limit",
"non_negative": 1
} }
], ],
"links": [], "links": [],
"modified": "2020-09-25 20:21:38.215494", "modified": "2021-01-29 12:02:16.106942",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "Mpesa Settings", "name": "Mpesa Settings",

View File

@ -33,13 +33,34 @@ class MpesaSettings(Document):
create_mode_of_payment('Mpesa-' + self.payment_gateway_name, payment_type="Phone") create_mode_of_payment('Mpesa-' + self.payment_gateway_name, payment_type="Phone")
def request_for_payment(self, **kwargs): def request_for_payment(self, **kwargs):
if frappe.flags.in_test: args = frappe._dict(kwargs)
from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import get_payment_request_response_payload request_amounts = self.split_request_amount_according_to_transaction_limit(args)
response = frappe._dict(get_payment_request_response_payload())
else:
response = frappe._dict(generate_stk_push(**kwargs))
self.handle_api_response("CheckoutRequestID", kwargs, response) for i, amount in enumerate(request_amounts):
args.request_amount = amount
if frappe.flags.in_test:
from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import get_payment_request_response_payload
response = frappe._dict(get_payment_request_response_payload(amount))
else:
response = frappe._dict(generate_stk_push(**args))
self.handle_api_response("CheckoutRequestID", args, response)
def split_request_amount_according_to_transaction_limit(self, args):
request_amount = args.request_amount
if request_amount > self.transaction_limit:
# make multiple requests
request_amounts = []
requests_to_be_made = frappe.utils.ceil(request_amount / self.transaction_limit) # 480/150 = ceil(3.2) = 4
for i in range(requests_to_be_made):
amount = self.transaction_limit
if i == requests_to_be_made - 1:
amount = request_amount - (self.transaction_limit * i) # for 4th request, 480 - (150 * 3) = 30
request_amounts.append(amount)
else:
request_amounts = [request_amount]
return request_amounts
def get_account_balance_info(self): def get_account_balance_info(self):
payload = dict( payload = dict(
@ -67,7 +88,8 @@ class MpesaSettings(Document):
req_name = getattr(response, global_id) req_name = getattr(response, global_id)
error = None error = None
create_request_log(request_dict, "Host", "Mpesa", req_name, error) if not frappe.db.exists('Integration Request', req_name):
create_request_log(request_dict, "Host", "Mpesa", req_name, error)
if error: if error:
frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error")) frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error"))
@ -80,6 +102,8 @@ def generate_stk_push(**kwargs):
mpesa_settings = frappe.get_doc("Mpesa Settings", args.payment_gateway[6:]) mpesa_settings = frappe.get_doc("Mpesa Settings", args.payment_gateway[6:])
env = "production" if not mpesa_settings.sandbox else "sandbox" env = "production" if not mpesa_settings.sandbox else "sandbox"
# for sandbox, business shortcode is same as till number
business_shortcode = mpesa_settings.business_shortcode if env == "production" else mpesa_settings.till_number
connector = MpesaConnector(env=env, connector = MpesaConnector(env=env,
app_key=mpesa_settings.consumer_key, app_key=mpesa_settings.consumer_key,
@ -87,10 +111,12 @@ def generate_stk_push(**kwargs):
mobile_number = sanitize_mobile_number(args.sender) mobile_number = sanitize_mobile_number(args.sender)
response = connector.stk_push(business_shortcode=mpesa_settings.till_number, response = connector.stk_push(
passcode=mpesa_settings.get_password("online_passkey"), amount=args.grand_total, business_shortcode=business_shortcode, amount=args.request_amount,
passcode=mpesa_settings.get_password("online_passkey"),
callback_url=callback_url, reference_code=mpesa_settings.till_number, callback_url=callback_url, reference_code=mpesa_settings.till_number,
phone_number=mobile_number, description="POS Payment") phone_number=mobile_number, description="POS Payment"
)
return response return response
@ -108,29 +134,72 @@ def verify_transaction(**kwargs):
transaction_response = frappe._dict(kwargs["Body"]["stkCallback"]) transaction_response = frappe._dict(kwargs["Body"]["stkCallback"])
checkout_id = getattr(transaction_response, "CheckoutRequestID", "") checkout_id = getattr(transaction_response, "CheckoutRequestID", "")
request = frappe.get_doc("Integration Request", checkout_id) integration_request = frappe.get_doc("Integration Request", checkout_id)
transaction_data = frappe._dict(loads(request.data)) transaction_data = frappe._dict(loads(integration_request.data))
total_paid = 0 # for multiple integration request made against a pos invoice
success = False # for reporting successfull callback to point of sale ui
if transaction_response['ResultCode'] == 0: if transaction_response['ResultCode'] == 0:
if request.reference_doctype and request.reference_docname: if integration_request.reference_doctype and integration_request.reference_docname:
try: try:
doc = frappe.get_doc(request.reference_doctype,
request.reference_docname)
doc.run_method("on_payment_authorized", 'Completed')
item_response = transaction_response["CallbackMetadata"]["Item"] item_response = transaction_response["CallbackMetadata"]["Item"]
amount = fetch_param_value(item_response, "Amount", "Name")
mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name") mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
frappe.db.set_value("POS Invoice", doc.reference_name, "mpesa_receipt_number", mpesa_receipt) pr = frappe.get_doc(integration_request.reference_doctype, integration_request.reference_docname)
request.handle_success(transaction_response)
mpesa_receipts, completed_payments = get_completed_integration_requests_info(
integration_request.reference_doctype,
integration_request.reference_docname,
checkout_id
)
total_paid = amount + sum(completed_payments)
mpesa_receipts = ', '.join(mpesa_receipts + [mpesa_receipt])
if total_paid >= pr.grand_total:
pr.run_method("on_payment_authorized", 'Completed')
success = True
frappe.db.set_value("POS Invoice", pr.reference_name, "mpesa_receipt_number", mpesa_receipts)
integration_request.handle_success(transaction_response)
except Exception: except Exception:
request.handle_failure(transaction_response) integration_request.handle_failure(transaction_response)
frappe.log_error(frappe.get_traceback()) frappe.log_error(frappe.get_traceback())
else: else:
request.handle_failure(transaction_response) integration_request.handle_failure(transaction_response)
frappe.publish_realtime('process_phone_payment', doctype="POS Invoice", frappe.publish_realtime(
docname=transaction_data.payment_reference, user=request.owner, message=transaction_response) event='process_phone_payment',
doctype="POS Invoice",
docname=transaction_data.payment_reference,
user=integration_request.owner,
message={
'amount': total_paid,
'success': success,
'failure_message': transaction_response["ResultDesc"] if transaction_response['ResultCode'] != 0 else ''
},
)
def get_completed_integration_requests_info(reference_doctype, reference_docname, checkout_id):
output_of_other_completed_requests = frappe.get_all("Integration Request", filters={
'name': ['!=', checkout_id],
'reference_doctype': reference_doctype,
'reference_docname': reference_docname,
'status': 'Completed'
}, pluck="output")
mpesa_receipts, completed_payments = [], []
for out in output_of_other_completed_requests:
out = frappe._dict(loads(out))
item_response = out["CallbackMetadata"]["Item"]
completed_amount = fetch_param_value(item_response, "Amount", "Name")
completed_mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
completed_payments.append(completed_amount)
mpesa_receipts.append(completed_mpesa_receipt)
return mpesa_receipts, completed_payments
def get_account_balance(request_payload): def get_account_balance(request_payload):
"""Call account balance API to send the request to the Mpesa Servers.""" """Call account balance API to send the request to the Mpesa Servers."""

View File

@ -9,6 +9,10 @@ from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import p
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
class TestMpesaSettings(unittest.TestCase): class TestMpesaSettings(unittest.TestCase):
def tearDown(self):
frappe.db.sql('delete from `tabMpesa Settings`')
frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"')
def test_creation_of_payment_gateway(self): def test_creation_of_payment_gateway(self):
create_mpesa_settings(payment_gateway_name="_Test") create_mpesa_settings(payment_gateway_name="_Test")
@ -40,10 +44,13 @@ class TestMpesaSettings(unittest.TestCase):
} }
})) }))
integration_request.delete()
def test_processing_of_callback_payload(self): def test_processing_of_callback_payload(self):
create_mpesa_settings(payment_gateway_name="Payment") create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
pos_invoice = create_pos_invoice(do_not_submit=1) pos_invoice = create_pos_invoice(do_not_submit=1)
pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 500}) pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 500})
@ -55,10 +62,16 @@ class TestMpesaSettings(unittest.TestCase):
# test payment request creation # test payment request creation
self.assertEquals(pr.payment_gateway, "Mpesa-Payment") self.assertEquals(pr.payment_gateway, "Mpesa-Payment")
callback_response = get_payment_callback_payload() # submitting payment request creates integration requests with random id
integration_req_ids = frappe.get_all("Integration Request", filters={
'reference_doctype': pr.doctype,
'reference_docname': pr.name,
}, pluck="name")
callback_response = get_payment_callback_payload(Amount=500, CheckoutRequestID=integration_req_ids[0])
verify_transaction(**callback_response) verify_transaction(**callback_response)
# test creation of integration request # test creation of integration request
integration_request = frappe.get_doc("Integration Request", "ws_CO_061020201133231972") integration_request = frappe.get_doc("Integration Request", integration_req_ids[0])
# test integration request creation and successful update of the status on receiving callback response # test integration request creation and successful update of the status on receiving callback response
self.assertTrue(integration_request) self.assertTrue(integration_request)
@ -68,6 +81,122 @@ class TestMpesaSettings(unittest.TestCase):
integration_request.reload() integration_request.reload()
self.assertEquals(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R") self.assertEquals(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R")
self.assertEquals(integration_request.status, "Completed") self.assertEquals(integration_request.status, "Completed")
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
integration_request.delete()
pr.reload()
pr.cancel()
pr.delete()
pos_invoice.delete()
def test_processing_of_multiple_callback_payload(self):
create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
pos_invoice = create_pos_invoice(do_not_submit=1)
pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 1000})
pos_invoice.contact_mobile = "093456543894"
pos_invoice.currency = "KES"
pos_invoice.save()
pr = pos_invoice.create_payment_request()
# test payment request creation
self.assertEquals(pr.payment_gateway, "Mpesa-Payment")
# submitting payment request creates integration requests with random id
integration_req_ids = frappe.get_all("Integration Request", filters={
'reference_doctype': pr.doctype,
'reference_docname': pr.name,
}, pluck="name")
# create random receipt nos and send it as response to callback handler
mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids]
integration_requests = []
for i in range(len(integration_req_ids)):
callback_response = get_payment_callback_payload(
Amount=500,
CheckoutRequestID=integration_req_ids[i],
MpesaReceiptNumber=mpesa_receipt_numbers[i]
)
# handle response manually
verify_transaction(**callback_response)
# test completion of integration request
integration_request = frappe.get_doc("Integration Request", integration_req_ids[i])
self.assertEquals(integration_request.status, "Completed")
integration_requests.append(integration_request)
# check receipt number once all the integration requests are completed
pos_invoice.reload()
self.assertEquals(pos_invoice.mpesa_receipt_number, ', '.join(mpesa_receipt_numbers))
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
[d.delete() for d in integration_requests]
pr.reload()
pr.cancel()
pr.delete()
pos_invoice.delete()
def test_processing_of_only_one_succes_callback_payload(self):
create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
pos_invoice = create_pos_invoice(do_not_submit=1)
pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 1000})
pos_invoice.contact_mobile = "093456543894"
pos_invoice.currency = "KES"
pos_invoice.save()
pr = pos_invoice.create_payment_request()
# test payment request creation
self.assertEquals(pr.payment_gateway, "Mpesa-Payment")
# submitting payment request creates integration requests with random id
integration_req_ids = frappe.get_all("Integration Request", filters={
'reference_doctype': pr.doctype,
'reference_docname': pr.name,
}, pluck="name")
# create random receipt nos and send it as response to callback handler
mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids]
callback_response = get_payment_callback_payload(
Amount=500,
CheckoutRequestID=integration_req_ids[0],
MpesaReceiptNumber=mpesa_receipt_numbers[0]
)
# handle response manually
verify_transaction(**callback_response)
# test completion of integration request
integration_request = frappe.get_doc("Integration Request", integration_req_ids[0])
self.assertEquals(integration_request.status, "Completed")
# now one request is completed
# second integration request fails
# now retrying payment request should make only one integration request again
pr = pos_invoice.create_payment_request()
new_integration_req_ids = frappe.get_all("Integration Request", filters={
'reference_doctype': pr.doctype,
'reference_docname': pr.name,
'name': ['not in', integration_req_ids]
}, pluck="name")
self.assertEquals(len(new_integration_req_ids), 1)
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
frappe.db.sql("delete from `tabIntegration Request` where integration_request_service = 'Mpesa'")
pr.reload()
pr.cancel()
pr.delete()
pos_invoice.delete()
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
def create_mpesa_settings(payment_gateway_name="Express"): def create_mpesa_settings(payment_gateway_name="Express"):
if frappe.db.exists("Mpesa Settings", payment_gateway_name): if frappe.db.exists("Mpesa Settings", payment_gateway_name):
@ -157,16 +286,19 @@ def get_test_account_balance_response():
} }
} }
def get_payment_request_response_payload(): def get_payment_request_response_payload(Amount=500):
"""Response received after successfully calling the stk push process request API.""" """Response received after successfully calling the stk push process request API."""
CheckoutRequestID = frappe.utils.random_string(10)
return { return {
"MerchantRequestID": "8071-27184008-1", "MerchantRequestID": "8071-27184008-1",
"CheckoutRequestID": "ws_CO_061020201133231972", "CheckoutRequestID": CheckoutRequestID,
"ResultCode": 0, "ResultCode": 0,
"ResultDesc": "The service request is processed successfully.", "ResultDesc": "The service request is processed successfully.",
"CallbackMetadata": { "CallbackMetadata": {
"Item": [ "Item": [
{ "Name": "Amount", "Value": 500.0 }, { "Name": "Amount", "Value": Amount },
{ "Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R" }, { "Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R" },
{ "Name": "TransactionDate", "Value": 20201006113336 }, { "Name": "TransactionDate", "Value": 20201006113336 },
{ "Name": "PhoneNumber", "Value": 254723575670 } { "Name": "PhoneNumber", "Value": 254723575670 }
@ -174,41 +306,26 @@ def get_payment_request_response_payload():
} }
} }
def get_payment_callback_payload(Amount=500, CheckoutRequestID="ws_CO_061020201133231972", MpesaReceiptNumber="LGR7OWQX0R"):
def get_payment_callback_payload():
"""Response received from the server as callback after calling the stkpush process request API.""" """Response received from the server as callback after calling the stkpush process request API."""
return { return {
"Body":{ "Body":{
"stkCallback":{ "stkCallback":{
"MerchantRequestID":"19465-780693-1", "MerchantRequestID":"19465-780693-1",
"CheckoutRequestID":"ws_CO_061020201133231972", "CheckoutRequestID":CheckoutRequestID,
"ResultCode":0, "ResultCode":0,
"ResultDesc":"The service request is processed successfully.", "ResultDesc":"The service request is processed successfully.",
"CallbackMetadata":{ "CallbackMetadata":{
"Item":[ "Item":[
{ { "Name":"Amount", "Value":Amount },
"Name":"Amount", { "Name":"MpesaReceiptNumber", "Value":MpesaReceiptNumber },
"Value":500 { "Name":"Balance" },
}, { "Name":"TransactionDate", "Value":20170727154800 },
{ { "Name":"PhoneNumber", "Value":254721566839 }
"Name":"MpesaReceiptNumber", ]
"Value":"LGR7OWQX0R"
},
{
"Name":"Balance"
},
{
"Name":"TransactionDate",
"Value":20170727154800
},
{
"Name":"PhoneNumber",
"Value":254721566839
} }
]
} }
} }
}
} }
def get_account_balance_callback_payload(): def get_account_balance_callback_payload():

View File

@ -2,4 +2,82 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Appointment Type', { frappe.ui.form.on('Appointment Type', {
refresh: function(frm) {
frm.set_query('price_list', function() {
return {
filters: {'selling': 1}
};
});
frm.set_query('medical_department', 'items', function(doc) {
let item_list = doc.items.map(({medical_department}) => medical_department);
return {
filters: [
['Medical Department', 'name', 'not in', item_list]
]
};
});
frm.set_query('op_consulting_charge_item', 'items', function() {
return {
filters: {
is_stock_item: 0
}
};
});
frm.set_query('inpatient_visit_charge_item', 'items', function() {
return {
filters: {
is_stock_item: 0
}
};
});
}
}); });
frappe.ui.form.on('Appointment Type Service Item', {
op_consulting_charge_item: function(frm, cdt, cdn) {
let d = locals[cdt][cdn];
if (frm.doc.price_list && d.op_consulting_charge_item) {
frappe.call({
'method': 'frappe.client.get_value',
args: {
'doctype': 'Item Price',
'filters': {
'item_code': d.op_consulting_charge_item,
'price_list': frm.doc.price_list
},
'fieldname': ['price_list_rate']
},
callback: function(data) {
if (data.message.price_list_rate) {
frappe.model.set_value(cdt, cdn, 'op_consulting_charge', data.message.price_list_rate);
}
}
});
}
},
inpatient_visit_charge_item: function(frm, cdt, cdn) {
let d = locals[cdt][cdn];
if (frm.doc.price_list && d.inpatient_visit_charge_item) {
frappe.call({
'method': 'frappe.client.get_value',
args: {
'doctype': 'Item Price',
'filters': {
'item_code': d.inpatient_visit_charge_item,
'price_list': frm.doc.price_list
},
'fieldname': ['price_list_rate']
},
callback: function (data) {
if (data.message.price_list_rate) {
frappe.model.set_value(cdt, cdn, 'inpatient_visit_charge', data.message.price_list_rate);
}
}
});
}
}
});

View File

@ -12,7 +12,10 @@
"appointment_type", "appointment_type",
"ip", "ip",
"default_duration", "default_duration",
"color" "color",
"billing_section",
"price_list",
"items"
], ],
"fields": [ "fields": [
{ {
@ -52,10 +55,27 @@
"label": "Color", "label": "Color",
"no_copy": 1, "no_copy": 1,
"report_hide": 1 "report_hide": 1
},
{
"fieldname": "billing_section",
"fieldtype": "Section Break",
"label": "Billing"
},
{
"fieldname": "price_list",
"fieldtype": "Link",
"label": "Price List",
"options": "Price List"
},
{
"fieldname": "items",
"fieldtype": "Table",
"label": "Appointment Type Service Items",
"options": "Appointment Type Service Item"
} }
], ],
"links": [], "links": [],
"modified": "2020-02-03 21:06:05.833050", "modified": "2021-01-22 09:41:05.010524",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Appointment Type", "name": "Appointment Type",

View File

@ -4,6 +4,53 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from frappe.model.document import Document from frappe.model.document import Document
import frappe
class AppointmentType(Document): class AppointmentType(Document):
pass def validate(self):
if self.items and self.price_list:
for item in self.items:
existing_op_item_price = frappe.db.exists('Item Price', {
'item_code': item.op_consulting_charge_item,
'price_list': self.price_list
})
if not existing_op_item_price and item.op_consulting_charge_item and item.op_consulting_charge:
make_item_price(self.price_list, item.op_consulting_charge_item, item.op_consulting_charge)
existing_ip_item_price = frappe.db.exists('Item Price', {
'item_code': item.inpatient_visit_charge_item,
'price_list': self.price_list
})
if not existing_ip_item_price and item.inpatient_visit_charge_item and item.inpatient_visit_charge:
make_item_price(self.price_list, item.inpatient_visit_charge_item, item.inpatient_visit_charge)
@frappe.whitelist()
def get_service_item_based_on_department(appointment_type, department):
item_list = frappe.db.get_value('Appointment Type Service Item',
filters = {'medical_department': department, 'parent': appointment_type},
fieldname = ['op_consulting_charge_item',
'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'],
as_dict = 1
)
# if department wise items are not set up
# use the generic items
if not item_list:
item_list = frappe.db.get_value('Appointment Type Service Item',
filters = {'parent': appointment_type},
fieldname = ['op_consulting_charge_item',
'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'],
as_dict = 1
)
return item_list
def make_item_price(price_list, item, item_price):
frappe.get_doc({
'doctype': 'Item Price',
'price_list': price_list,
'item_code': item,
'price_list_rate': item_price
}).insert(ignore_permissions=True, ignore_mandatory=True)

View File

@ -0,0 +1,67 @@
{
"actions": [],
"creation": "2021-01-22 09:34:53.373105",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"medical_department",
"op_consulting_charge_item",
"op_consulting_charge",
"column_break_4",
"inpatient_visit_charge_item",
"inpatient_visit_charge"
],
"fields": [
{
"fieldname": "medical_department",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Medical Department",
"options": "Medical Department"
},
{
"fieldname": "op_consulting_charge_item",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Out Patient Consulting Charge Item",
"options": "Item"
},
{
"fieldname": "op_consulting_charge",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Out Patient Consulting Charge"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "inpatient_visit_charge_item",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Inpatient Visit Charge Item",
"options": "Item"
},
{
"fieldname": "inpatient_visit_charge",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Inpatient Visit Charge Item"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-01-22 09:35:26.503443",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Appointment Type Service Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, 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 AppointmentTypeServiceItem(Document):
pass

View File

@ -121,6 +121,7 @@ class ClinicalProcedure(Document):
stock_entry.stock_entry_type = 'Material Receipt' stock_entry.stock_entry_type = 'Material Receipt'
stock_entry.to_warehouse = self.warehouse stock_entry.to_warehouse = self.warehouse
stock_entry.company = self.company
expense_account = get_account(None, 'expense_account', 'Healthcare Settings', self.company) expense_account = get_account(None, 'expense_account', 'Healthcare Settings', self.company)
for item in self.items: for item in self.items:
if item.qty > item.actual_qty: if item.qty > item.actual_qty:

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2017, ESS LLP and Contributors # Copyright (c) 2017, ESS LLP and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
@ -60,6 +60,7 @@ def create_procedure(procedure_template, patient, practitioner):
procedure.practitioner = practitioner procedure.practitioner = practitioner
procedure.consume_stock = procedure_template.allow_stock_consumption procedure.consume_stock = procedure_template.allow_stock_consumption
procedure.items = procedure_template.items procedure.items = procedure_template.items
procedure.warehouse = frappe.db.get_single_value('Stock Settings', 'default_warehouse') procedure.company = "_Test Company"
procedure.warehouse = "_Test Warehouse - _TC"
procedure.submit() procedure.submit()
return procedure return procedure

View File

@ -159,6 +159,7 @@
"fieldname": "op_consulting_charge", "fieldname": "op_consulting_charge",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Out Patient Consulting Charge", "label": "Out Patient Consulting Charge",
"mandatory_depends_on": "op_consulting_charge_item",
"options": "Currency" "options": "Currency"
}, },
{ {
@ -174,7 +175,8 @@
{ {
"fieldname": "inpatient_visit_charge", "fieldname": "inpatient_visit_charge",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Inpatient Visit Charge" "label": "Inpatient Visit Charge",
"mandatory_depends_on": "inpatient_visit_charge_item"
}, },
{ {
"depends_on": "eval: !doc.__islocal", "depends_on": "eval: !doc.__islocal",
@ -280,7 +282,7 @@
], ],
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-04-06 13:44:24.759623", "modified": "2021-01-22 10:14:43.187675",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Healthcare Practitioner", "name": "Healthcare Practitioner",

View File

@ -139,6 +139,7 @@ def create_inpatient(patient):
inpatient_record.phone = patient_obj.phone inpatient_record.phone = patient_obj.phone
inpatient_record.inpatient = "Scheduled" inpatient_record.inpatient = "Scheduled"
inpatient_record.scheduled_date = today() inpatient_record.scheduled_date = today()
inpatient_record.company = "_Test Company"
return inpatient_record return inpatient_record

View File

@ -24,11 +24,13 @@ frappe.ui.form.on('Patient Appointment', {
}); });
frm.set_query('practitioner', function() { frm.set_query('practitioner', function() {
return { if (frm.doc.department) {
filters: { return {
'department': frm.doc.department filters: {
} 'department': frm.doc.department
}; }
};
}
}); });
frm.set_query('service_unit', function() { frm.set_query('service_unit', function() {
@ -140,6 +142,20 @@ frappe.ui.form.on('Patient Appointment', {
patient: function(frm) { patient: function(frm) {
if (frm.doc.patient) { if (frm.doc.patient) {
frm.trigger('toggle_payment_fields'); frm.trigger('toggle_payment_fields');
frappe.call({
method: 'frappe.client.get',
args: {
doctype: 'Patient',
name: frm.doc.patient
},
callback: function (data) {
let age = null;
if (data.message.dob) {
age = calculate_age(data.message.dob);
}
frappe.model.set_value(frm.doctype, frm.docname, 'patient_age', age);
}
});
} else { } else {
frm.set_value('patient_name', ''); frm.set_value('patient_name', '');
frm.set_value('patient_sex', ''); frm.set_value('patient_sex', '');
@ -148,6 +164,37 @@ frappe.ui.form.on('Patient Appointment', {
} }
}, },
practitioner: function(frm) {
if (frm.doc.practitioner ) {
frm.events.set_payment_details(frm);
}
},
appointment_type: function(frm) {
if (frm.doc.appointment_type) {
frm.events.set_payment_details(frm);
}
},
set_payment_details: function(frm) {
frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing').then(val => {
if (val) {
frappe.call({
method: 'erpnext.healthcare.utils.get_service_item_and_practitioner_charge',
args: {
doc: frm.doc
},
callback: function(data) {
if (data.message) {
frappe.model.set_value(frm.doctype, frm.docname, 'paid_amount', data.message.practitioner_charge);
frappe.model.set_value(frm.doctype, frm.docname, 'billing_item', data.message.service_item);
}
}
});
}
});
},
therapy_plan: function(frm) { therapy_plan: function(frm) {
frm.trigger('set_therapy_type_filter'); frm.trigger('set_therapy_type_filter');
}, },
@ -190,14 +237,18 @@ frappe.ui.form.on('Patient Appointment', {
// show payment fields as non-mandatory // show payment fields as non-mandatory
frm.toggle_display('mode_of_payment', 0); frm.toggle_display('mode_of_payment', 0);
frm.toggle_display('paid_amount', 0); frm.toggle_display('paid_amount', 0);
frm.toggle_display('billing_item', 0);
frm.toggle_reqd('mode_of_payment', 0); frm.toggle_reqd('mode_of_payment', 0);
frm.toggle_reqd('paid_amount', 0); frm.toggle_reqd('paid_amount', 0);
frm.toggle_reqd('billing_item', 0);
} else { } else {
// if automated appointment invoicing is disabled, hide fields // if automated appointment invoicing is disabled, hide fields
frm.toggle_display('mode_of_payment', data.message ? 1 : 0); frm.toggle_display('mode_of_payment', data.message ? 1 : 0);
frm.toggle_display('paid_amount', data.message ? 1 : 0); frm.toggle_display('paid_amount', data.message ? 1 : 0);
frm.toggle_display('billing_item', data.message ? 1 : 0);
frm.toggle_reqd('mode_of_payment', data.message ? 1 : 0); frm.toggle_reqd('mode_of_payment', data.message ? 1 : 0);
frm.toggle_reqd('paid_amount', data.message ? 1 :0); frm.toggle_reqd('paid_amount', data.message ? 1 :0);
frm.toggle_reqd('billing_item', data.message ? 1 : 0);
} }
} }
}); });
@ -540,57 +591,6 @@ let update_status = function(frm, status){
); );
}; };
frappe.ui.form.on('Patient Appointment', 'practitioner', function(frm) {
if (frm.doc.practitioner) {
frappe.call({
method: 'frappe.client.get',
args: {
doctype: 'Healthcare Practitioner',
name: frm.doc.practitioner
},
callback: function (data) {
frappe.model.set_value(frm.doctype, frm.docname, 'department', data.message.department);
frappe.model.set_value(frm.doctype, frm.docname, 'paid_amount', data.message.op_consulting_charge);
frappe.model.set_value(frm.doctype, frm.docname, 'billing_item', data.message.op_consulting_charge_item);
}
});
}
});
frappe.ui.form.on('Patient Appointment', 'patient', function(frm) {
if (frm.doc.patient) {
frappe.call({
method: 'frappe.client.get',
args: {
doctype: 'Patient',
name: frm.doc.patient
},
callback: function (data) {
let age = null;
if (data.message.dob) {
age = calculate_age(data.message.dob);
}
frappe.model.set_value(frm.doctype,frm.docname, 'patient_age', age);
}
});
}
});
frappe.ui.form.on('Patient Appointment', 'appointment_type', function(frm) {
if (frm.doc.appointment_type) {
frappe.call({
method: 'frappe.client.get',
args: {
doctype: 'Appointment Type',
name: frm.doc.appointment_type
},
callback: function(data) {
frappe.model.set_value(frm.doctype,frm.docname, 'duration',data.message.default_duration);
}
});
}
});
let calculate_age = function(birth) { let calculate_age = function(birth) {
let ageMS = Date.parse(Date()) - Date.parse(birth); let ageMS = Date.parse(Date()) - Date.parse(birth);
let age = new Date(); let age = new Date();

View File

@ -19,19 +19,19 @@
"inpatient_record", "inpatient_record",
"column_break_1", "column_break_1",
"company", "company",
"practitioner",
"practitioner_name",
"department",
"service_unit", "service_unit",
"section_break_12",
"appointment_type",
"duration",
"procedure_template", "procedure_template",
"get_procedure_from_encounter", "get_procedure_from_encounter",
"procedure_prescription", "procedure_prescription",
"therapy_plan", "therapy_plan",
"therapy_type", "therapy_type",
"get_prescribed_therapies", "get_prescribed_therapies",
"practitioner",
"practitioner_name",
"department",
"section_break_12",
"appointment_type",
"duration",
"column_break_17", "column_break_17",
"appointment_date", "appointment_date",
"appointment_time", "appointment_time",
@ -79,6 +79,7 @@
"set_only_once": 1 "set_only_once": 1
}, },
{ {
"fetch_from": "appointment_type.default_duration",
"fieldname": "duration", "fieldname": "duration",
"fieldtype": "Int", "fieldtype": "Int",
"in_filter": 1, "in_filter": 1,
@ -144,7 +145,6 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Healthcare Practitioner", "label": "Healthcare Practitioner",
"options": "Healthcare Practitioner", "options": "Healthcare Practitioner",
"read_only": 1,
"reqd": 1, "reqd": 1,
"search_index": 1, "search_index": 1,
"set_only_once": 1 "set_only_once": 1
@ -158,7 +158,6 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Department", "label": "Department",
"options": "Medical Department", "options": "Medical Department",
"read_only": 1,
"search_index": 1, "search_index": 1,
"set_only_once": 1 "set_only_once": 1
}, },
@ -227,12 +226,14 @@
"fieldname": "mode_of_payment", "fieldname": "mode_of_payment",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Mode of Payment", "label": "Mode of Payment",
"options": "Mode of Payment" "options": "Mode of Payment",
"read_only_depends_on": "invoiced"
}, },
{ {
"fieldname": "paid_amount", "fieldname": "paid_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Paid Amount" "label": "Paid Amount",
"read_only_depends_on": "invoiced"
}, },
{ {
"fieldname": "column_break_2", "fieldname": "column_break_2",
@ -302,7 +303,8 @@
"fieldname": "therapy_plan", "fieldname": "therapy_plan",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Therapy Plan", "label": "Therapy Plan",
"options": "Therapy Plan" "options": "Therapy Plan",
"set_only_once": 1
}, },
{ {
"fieldname": "ref_sales_invoice", "fieldname": "ref_sales_invoice",
@ -347,7 +349,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2020-12-16 13:16:58.578503", "modified": "2021-02-08 13:13:15.116833",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Patient Appointment", "name": "Patient Appointment",

View File

@ -26,6 +26,7 @@ class PatientAppointment(Document):
def after_insert(self): def after_insert(self):
self.update_prescription_details() self.update_prescription_details()
self.set_payment_details()
invoice_appointment(self) invoice_appointment(self)
self.update_fee_validity() self.update_fee_validity()
send_confirmation_msg(self) send_confirmation_msg(self)
@ -85,6 +86,13 @@ class PatientAppointment(Document):
def set_appointment_datetime(self): def set_appointment_datetime(self):
self.appointment_datetime = "%s %s" % (self.appointment_date, self.appointment_time or "00:00:00") self.appointment_datetime = "%s %s" % (self.appointment_date, self.appointment_time or "00:00:00")
def set_payment_details(self):
if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'):
details = get_service_item_and_practitioner_charge(self)
self.db_set('billing_item', details.get('service_item'))
if not self.paid_amount:
self.db_set('paid_amount', details.get('practitioner_charge'))
def validate_customer_created(self): def validate_customer_created(self):
if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'): if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'):
if not frappe.db.get_value('Patient', self.patient, 'customer'): if not frappe.db.get_value('Patient', self.patient, 'customer'):
@ -148,31 +156,37 @@ def invoice_appointment(appointment_doc):
fee_validity = None fee_validity = None
if automate_invoicing and not appointment_invoiced and not fee_validity: if automate_invoicing and not appointment_invoiced and not fee_validity:
sales_invoice = frappe.new_doc('Sales Invoice') create_sales_invoice(appointment_doc)
sales_invoice.patient = appointment_doc.patient
sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer')
sales_invoice.appointment = appointment_doc.name
sales_invoice.due_date = getdate()
sales_invoice.company = appointment_doc.company
sales_invoice.debit_to = get_receivable_account(appointment_doc.company)
item = sales_invoice.append('items', {})
item = get_appointment_item(appointment_doc, item)
# Add payments if payment details are supplied else proceed to create invoice as Unpaid def create_sales_invoice(appointment_doc):
if appointment_doc.mode_of_payment and appointment_doc.paid_amount: sales_invoice = frappe.new_doc('Sales Invoice')
sales_invoice.is_pos = 1 sales_invoice.patient = appointment_doc.patient
payment = sales_invoice.append('payments', {}) sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer')
payment.mode_of_payment = appointment_doc.mode_of_payment sales_invoice.appointment = appointment_doc.name
payment.amount = appointment_doc.paid_amount sales_invoice.due_date = getdate()
sales_invoice.company = appointment_doc.company
sales_invoice.debit_to = get_receivable_account(appointment_doc.company)
sales_invoice.set_missing_values(for_validate=True) item = sales_invoice.append('items', {})
sales_invoice.flags.ignore_mandatory = True item = get_appointment_item(appointment_doc, item)
sales_invoice.save(ignore_permissions=True)
sales_invoice.submit() # Add payments if payment details are supplied else proceed to create invoice as Unpaid
frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True) if appointment_doc.mode_of_payment and appointment_doc.paid_amount:
frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1) sales_invoice.is_pos = 1
frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name) payment = sales_invoice.append('payments', {})
payment.mode_of_payment = appointment_doc.mode_of_payment
payment.amount = appointment_doc.paid_amount
sales_invoice.set_missing_values(for_validate=True)
sales_invoice.flags.ignore_mandatory = True
sales_invoice.save(ignore_permissions=True)
sales_invoice.submit()
frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True)
frappe.db.set_value('Patient Appointment', appointment_doc.name, {
'invoiced': 1,
'ref_sales_invoice': sales_invoice.name
})
def check_is_new_patient(patient, name=None): def check_is_new_patient(patient, name=None):
@ -187,13 +201,14 @@ def check_is_new_patient(patient, name=None):
def get_appointment_item(appointment_doc, item): def get_appointment_item(appointment_doc, item):
service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment_doc) details = get_service_item_and_practitioner_charge(appointment_doc)
item.item_code = service_item charge = appointment_doc.paid_amount or details.get('practitioner_charge')
item.item_code = details.get('service_item')
item.description = _('Consulting Charges: {0}').format(appointment_doc.practitioner) item.description = _('Consulting Charges: {0}').format(appointment_doc.practitioner)
item.income_account = get_income_account(appointment_doc.practitioner, appointment_doc.company) item.income_account = get_income_account(appointment_doc.practitioner, appointment_doc.company)
item.cost_center = frappe.get_cached_value('Company', appointment_doc.company, 'cost_center') item.cost_center = frappe.get_cached_value('Company', appointment_doc.company, 'cost_center')
item.rate = practitioner_charge item.rate = charge
item.amount = practitioner_charge item.amount = charge
item.qty = 1 item.qty = 1
item.reference_dt = 'Patient Appointment' item.reference_dt = 'Patient Appointment'
item.reference_dn = appointment_doc.name item.reference_dn = appointment_doc.name

View File

@ -32,7 +32,8 @@ class TestPatientAppointment(unittest.TestCase):
patient, medical_department, practitioner = create_healthcare_docs() patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice = 1) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice = 1)
self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'), 1) appointment.reload()
self.assertEqual(appointment.invoiced, 1)
encounter = make_encounter(appointment.name) encounter = make_encounter(appointment.name)
self.assertTrue(encounter) self.assertTrue(encounter)
self.assertEqual(encounter.company, appointment.company) self.assertEqual(encounter.company, appointment.company)
@ -41,7 +42,7 @@ class TestPatientAppointment(unittest.TestCase):
# invoiced flag mapped from appointment # invoiced flag mapped from appointment
self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced')) self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'))
def test_invoicing(self): def test_auto_invoicing(self):
patient, medical_department, practitioner = create_healthcare_docs() patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0)
@ -57,6 +58,50 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'patient'), appointment.patient) self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'patient'), appointment.patient)
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount) self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
def test_auto_invoicing_based_on_department(self):
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment_type = create_appointment_type()
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2),
invoice=1, appointment_type=appointment_type.name, department='_Test Medical Department')
appointment.reload()
self.assertEqual(appointment.invoiced, 1)
self.assertEqual(appointment.billing_item, 'HLC-SI-001')
self.assertEqual(appointment.paid_amount, 200)
sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
self.assertTrue(sales_invoice_name)
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
def test_auto_invoicing_according_to_appointment_type_charge(self):
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
item = create_healthcare_service_items()
items = [{
'op_consulting_charge_item': item,
'op_consulting_charge': 300
}]
appointment_type = create_appointment_type(args={
'name': 'Generic Appointment Type charge',
'items': items
})
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2),
invoice=1, appointment_type=appointment_type.name)
appointment.reload()
self.assertEqual(appointment.invoiced, 1)
self.assertEqual(appointment.billing_item, item)
self.assertEqual(appointment.paid_amount, 300)
sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
self.assertTrue(sales_invoice_name)
def test_appointment_cancel(self): def test_appointment_cancel(self):
patient, medical_department, practitioner = create_healthcare_docs() patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1) frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1)
@ -178,14 +223,15 @@ def create_encounter(appointment):
encounter.submit() encounter.submit()
return encounter return encounter
def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0, service_unit=None, save=1): def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0,
service_unit=None, appointment_type=None, save=1, department=None):
item = create_healthcare_service_items() item = create_healthcare_service_items()
frappe.db.set_value('Healthcare Settings', None, 'inpatient_visit_charge_item', item) frappe.db.set_value('Healthcare Settings', None, 'inpatient_visit_charge_item', item)
frappe.db.set_value('Healthcare Settings', None, 'op_consulting_charge_item', item) frappe.db.set_value('Healthcare Settings', None, 'op_consulting_charge_item', item)
appointment = frappe.new_doc('Patient Appointment') appointment = frappe.new_doc('Patient Appointment')
appointment.patient = patient appointment.patient = patient
appointment.practitioner = practitioner appointment.practitioner = practitioner
appointment.department = '_Test Medical Department' appointment.department = department or '_Test Medical Department'
appointment.appointment_date = appointment_date appointment.appointment_date = appointment_date
appointment.company = '_Test Company' appointment.company = '_Test Company'
appointment.duration = 15 appointment.duration = 15
@ -193,7 +239,8 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce
appointment.service_unit = service_unit appointment.service_unit = service_unit
if invoice: if invoice:
appointment.mode_of_payment = 'Cash' appointment.mode_of_payment = 'Cash'
appointment.paid_amount = 500 if appointment_type:
appointment.appointment_type = appointment_type
if procedure_template: if procedure_template:
appointment.procedure_template = create_clinical_procedure_template().get('name') appointment.procedure_template = create_clinical_procedure_template().get('name')
if save: if save:
@ -223,4 +270,29 @@ def create_clinical_procedure_template():
template.description = 'Knee Surgery and Rehab' template.description = 'Knee Surgery and Rehab'
template.rate = 50000 template.rate = 50000
template.save() template.save()
return template return template
def create_appointment_type(args=None):
if not args:
args = frappe.local.form_dict
name = args.get('name') or 'Test Appointment Type wise Charge'
if frappe.db.exists('Appointment Type', name):
return frappe.get_doc('Appointment Type', name)
else:
item = create_healthcare_service_items()
items = [{
'medical_department': '_Test Medical Department',
'op_consulting_charge_item': item,
'op_consulting_charge': 200
}]
return frappe.get_doc({
'doctype': 'Appointment Type',
'appointment_type': args.get('name') or 'Test Appointment Type wise Charge',
'default_duration': args.get('default_duration') or 20,
'color': args.get('color') or '#7575ff',
'price_list': args.get('price_list') or frappe.db.get_value("Price List", {"selling": 1}),
'items': args.get('items') or items
}).insert()

View File

@ -5,9 +5,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import math import math
import frappe import frappe
import json
from frappe import _ from frappe import _
from frappe.utils.formatters import format_value from frappe.utils.formatters import format_value
from frappe.utils import time_diff_in_hours, rounded from frappe.utils import time_diff_in_hours, rounded
from six import string_types
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account
from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity
from erpnext.healthcare.doctype.lab_test.lab_test import create_multiple from erpnext.healthcare.doctype.lab_test.lab_test import create_multiple
@ -64,7 +66,9 @@ def get_appointments_to_invoice(patient, company):
income_account = None income_account = None
service_item = None service_item = None
if appointment.practitioner: if appointment.practitioner:
service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment) details = get_service_item_and_practitioner_charge(appointment)
service_item = details.get('service_item')
practitioner_charge = details.get('practitioner_charge')
income_account = get_income_account(appointment.practitioner, appointment.company) income_account = get_income_account(appointment.practitioner, appointment.company)
appointments_to_invoice.append({ appointments_to_invoice.append({
'reference_type': 'Patient Appointment', 'reference_type': 'Patient Appointment',
@ -97,7 +101,9 @@ def get_encounters_to_invoice(patient, company):
frappe.db.get_single_value('Healthcare Settings', 'do_not_bill_inpatient_encounters'): frappe.db.get_single_value('Healthcare Settings', 'do_not_bill_inpatient_encounters'):
continue continue
service_item, practitioner_charge = get_service_item_and_practitioner_charge(encounter) details = get_service_item_and_practitioner_charge(encounter)
service_item = details.get('service_item')
practitioner_charge = details.get('practitioner_charge')
income_account = get_income_account(encounter.practitioner, encounter.company) income_account = get_income_account(encounter.practitioner, encounter.company)
encounters_to_invoice.append({ encounters_to_invoice.append({
@ -173,7 +179,7 @@ def get_clinical_procedures_to_invoice(patient, company):
if procedure.invoice_separately_as_consumables and procedure.consume_stock \ if procedure.invoice_separately_as_consumables and procedure.consume_stock \
and procedure.status == 'Completed' and not procedure.consumption_invoiced: and procedure.status == 'Completed' and not procedure.consumption_invoiced:
service_item = get_healthcare_service_item('clinical_procedure_consumable_item') service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item')
if not service_item: if not service_item:
msg = _('Please Configure Clinical Procedure Consumable Item in ') msg = _('Please Configure Clinical Procedure Consumable Item in ')
msg += '''<b><a href='#Form/Healthcare Settings'>Healthcare Settings</a></b>''' msg += '''<b><a href='#Form/Healthcare Settings'>Healthcare Settings</a></b>'''
@ -304,24 +310,50 @@ def get_therapy_sessions_to_invoice(patient, company):
return therapy_sessions_to_invoice return therapy_sessions_to_invoice
@frappe.whitelist()
def get_service_item_and_practitioner_charge(doc): def get_service_item_and_practitioner_charge(doc):
if isinstance(doc, string_types):
doc = json.loads(doc)
doc = frappe.get_doc(doc)
service_item = None
practitioner_charge = None
department = doc.medical_department if doc.doctype == 'Patient Encounter' else doc.department
is_inpatient = doc.inpatient_record is_inpatient = doc.inpatient_record
if is_inpatient:
service_item = get_practitioner_service_item(doc.practitioner, 'inpatient_visit_charge_item') if doc.get('appointment_type'):
service_item, practitioner_charge = get_appointment_type_service_item(doc.appointment_type, department, is_inpatient)
if not service_item and not practitioner_charge:
service_item, practitioner_charge = get_practitioner_service_item(doc.practitioner, is_inpatient)
if not service_item: if not service_item:
service_item = get_healthcare_service_item('inpatient_visit_charge_item') service_item = get_healthcare_service_item(is_inpatient)
else:
service_item = get_practitioner_service_item(doc.practitioner, 'op_consulting_charge_item')
if not service_item:
service_item = get_healthcare_service_item('op_consulting_charge_item')
if not service_item: if not service_item:
throw_config_service_item(is_inpatient) throw_config_service_item(is_inpatient)
practitioner_charge = get_practitioner_charge(doc.practitioner, is_inpatient)
if not practitioner_charge: if not practitioner_charge:
throw_config_practitioner_charge(is_inpatient, doc.practitioner) throw_config_practitioner_charge(is_inpatient, doc.practitioner)
return {'service_item': service_item, 'practitioner_charge': practitioner_charge}
def get_appointment_type_service_item(appointment_type, department, is_inpatient):
from erpnext.healthcare.doctype.appointment_type.appointment_type import get_service_item_based_on_department
item_list = get_service_item_based_on_department(appointment_type, department)
service_item = None
practitioner_charge = None
if item_list:
if is_inpatient:
service_item = item_list.get('inpatient_visit_charge_item')
practitioner_charge = item_list.get('inpatient_visit_charge')
else:
service_item = item_list.get('op_consulting_charge_item')
practitioner_charge = item_list.get('op_consulting_charge')
return service_item, practitioner_charge return service_item, practitioner_charge
@ -345,12 +377,27 @@ def throw_config_practitioner_charge(is_inpatient, practitioner):
frappe.throw(msg, title=_('Missing Configuration')) frappe.throw(msg, title=_('Missing Configuration'))
def get_practitioner_service_item(practitioner, service_item_field): def get_practitioner_service_item(practitioner, is_inpatient):
return frappe.db.get_value('Healthcare Practitioner', practitioner, service_item_field) service_item = None
practitioner_charge = None
if is_inpatient:
service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['inpatient_visit_charge_item', 'inpatient_visit_charge'])
else:
service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['op_consulting_charge_item', 'op_consulting_charge'])
return service_item, practitioner_charge
def get_healthcare_service_item(service_item_field): def get_healthcare_service_item(is_inpatient):
return frappe.db.get_single_value('Healthcare Settings', service_item_field) service_item = None
if is_inpatient:
service_item = frappe.db.get_single_value('Healthcare Settings', 'inpatient_visit_charge_item')
else:
service_item = frappe.db.get_single_value('Healthcare Settings', 'op_consulting_charge_item')
return service_item
def get_practitioner_charge(practitioner, is_inpatient): def get_practitioner_charge(practitioner, is_inpatient):
@ -381,7 +428,8 @@ def set_invoiced(item, method, ref_invoice=None):
invoiced = True invoiced = True
if item.reference_dt == 'Clinical Procedure': if item.reference_dt == 'Clinical Procedure':
if get_healthcare_service_item('clinical_procedure_consumable_item') == item.item_code: service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item')
if service_item == item.item_code:
frappe.db.set_value(item.reference_dt, item.reference_dn, 'consumption_invoiced', invoiced) frappe.db.set_value(item.reference_dt, item.reference_dn, 'consumption_invoiced', invoiced)
else: else:
frappe.db.set_value(item.reference_dt, item.reference_dn, 'invoiced', invoiced) frappe.db.set_value(item.reference_dt, item.reference_dn, 'invoiced', invoiced)
@ -403,7 +451,8 @@ def set_invoiced(item, method, ref_invoice=None):
def validate_invoiced_on_submit(item): def validate_invoiced_on_submit(item):
if item.reference_dt == 'Clinical Procedure' and get_healthcare_service_item('clinical_procedure_consumable_item') == item.item_code: if item.reference_dt == 'Clinical Procedure' and \
frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item') == item.item_code:
is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'consumption_invoiced') is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'consumption_invoiced')
else: else:
is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'invoiced') is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'invoiced')

View File

@ -16,6 +16,7 @@ class TestEmployee(unittest.TestCase):
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:] employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:]
employee.company_email = "test@example.com" employee.company_email = "test@example.com"
employee.company = "_Test Company"
employee.save() employee.save()
from erpnext.hr.doctype.employee.employee import get_employees_who_are_born_today, send_birthday_reminders from erpnext.hr.doctype.employee.employee import get_employees_who_are_born_today, send_birthday_reminders

View File

@ -94,11 +94,11 @@ class TestWorkOrder(unittest.TestCase):
wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2, wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2,
source_warehouse=warehouse, skip_transfer=1) source_warehouse=warehouse, skip_transfer=1)
bin1_on_submit = get_bin(item, warehouse) reserved_qty_on_submission = cint(get_bin(item, warehouse).reserved_qty_for_production)
# reserved qty for production is updated # reserved qty for production is updated
self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2, self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2, reserved_qty_on_submission)
cint(bin1_on_submit.reserved_qty_for_production))
test_stock_entry.make_stock_entry(item_code="_Test Item", test_stock_entry.make_stock_entry(item_code="_Test Item",
target=warehouse, qty=100, basic_rate=100) target=warehouse, qty=100, basic_rate=100)
@ -109,9 +109,9 @@ class TestWorkOrder(unittest.TestCase):
s.submit() s.submit()
bin1_at_completion = get_bin(item, warehouse) bin1_at_completion = get_bin(item, warehouse)
self.assertEqual(cint(bin1_at_completion.reserved_qty_for_production), self.assertEqual(cint(bin1_at_completion.reserved_qty_for_production),
cint(bin1_on_submit.reserved_qty_for_production) - 1) reserved_qty_on_submission - 1)
def test_production_item(self): def test_production_item(self):
wo_order = make_wo_order_test_record(item="_Test FG Item", qty=1, do_not_save=True) wo_order = make_wo_order_test_record(item="_Test FG Item", qty=1, do_not_save=True)

View File

@ -677,7 +677,7 @@ 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.fix_quotation_expired_status
erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry
erpnext.patches.v12_0.rename_pos_closing_doctype erpnext.patches.v12_0.rename_pos_closing_doctype
erpnext.patches.v13_0.replace_pos_payment_mode_table erpnext.patches.v13_0.replace_pos_payment_mode_table #2020-12-29
erpnext.patches.v12_0.remove_duplicate_leave_ledger_entries #2020-05-22 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 erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive
execute:frappe.reload_doc("HR", "doctype", "Employee Advance") execute:frappe.reload_doc("HR", "doctype", "Employee Advance")
@ -741,6 +741,7 @@ erpnext.patches.v13_0.update_member_email_address
erpnext.patches.v13_0.update_custom_fields_for_shopify erpnext.patches.v13_0.update_custom_fields_for_shopify
erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
erpnext.patches.v13_0.update_pos_closing_entry_in_merge_log
erpnext.patches.v13_0.add_po_to_global_search erpnext.patches.v13_0.add_po_to_global_search
erpnext.patches.v13_0.update_returned_qty_in_pr_dn erpnext.patches.v13_0.update_returned_qty_in_pr_dn
erpnext.patches.v13_0.create_uae_pos_invoice_fields erpnext.patches.v13_0.create_uae_pos_invoice_fields
@ -749,3 +750,6 @@ erpnext.patches.v13_0.set_company_in_leave_ledger_entry
erpnext.patches.v13_0.convert_qi_parameter_to_link_field erpnext.patches.v13_0.convert_qi_parameter_to_link_field
erpnext.patches.v13_0.setup_patient_history_settings_for_standard_doctypes erpnext.patches.v13_0.setup_patient_history_settings_for_standard_doctypes
erpnext.patches.v13_0.add_naming_series_to_old_projects # 1-02-2021 erpnext.patches.v13_0.add_naming_series_to_old_projects # 1-02-2021
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v12_0.add_state_code_for_ladakh
erpnext.patches.v13_0.update_vehicle_no_reqd_condition

View File

@ -0,0 +1,16 @@
import frappe
from erpnext.regional.india import states
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
custom_fields = ['Address-gst_state', 'Tax Category-gst_state']
# Update options in gst_state custom fields
for field in custom_fields:
gst_state_field = frappe.get_doc('Custom Field', field)
gst_state_field.options = '\n'.join(states)
gst_state_field.save()

View File

@ -0,0 +1,50 @@
import frappe
from frappe import _
from frappe.utils import getdate, get_time
from erpnext.stock.stock_ledger import update_entries_after
from erpnext.accounts.utils import update_gl_entries_after
def execute():
frappe.reload_doc('stock', 'doctype', 'repost_item_valuation')
reposting_project_deployed_on = frappe.db.get_value("DocType", "Repost Item Valuation", "creation")
data = frappe.db.sql('''
SELECT
name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time
FROM
`tabStock Ledger Entry`
WHERE
creation > %s
and is_cancelled = 0
ORDER BY timestamp(posting_date, posting_time) asc, creation asc
''', reposting_project_deployed_on, as_dict=1)
frappe.db.auto_commit_on_many_writes = 1
print("Reposting Stock Ledger Entries...")
total_sle = len(data)
i = 0
for d in data:
update_entries_after({
"item_code": d.item_code,
"warehouse": d.warehouse,
"posting_date": d.posting_date,
"posting_time": d.posting_time,
"voucher_type": d.voucher_type,
"voucher_no": d.voucher_no,
"sle_id": d.name
}, allow_negative_stock=True)
i += 1
if i%100 == 0:
print(i, "/", total_sle)
print("Reposting General Ledger Entries...")
posting_date = getdate(reposting_project_deployed_on)
posting_time = get_time(reposting_project_deployed_on)
for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}):
update_gl_entries_after(posting_date, posting_time, company=row.name)
frappe.db.auto_commit_on_many_writes = 0

View File

@ -6,12 +6,10 @@ from __future__ import unicode_literals
import frappe import frappe
def execute(): def execute():
frappe.reload_doc("accounts", "doctype", "POS Payment Method") frappe.reload_doc("accounts", "doctype", "pos_payment_method")
pos_profiles = frappe.get_all("POS Profile") pos_profiles = frappe.get_all("POS Profile")
for pos_profile in pos_profiles: for pos_profile in pos_profiles:
if not pos_profile.get("payments"): return
payments = frappe.db.sql(""" payments = frappe.db.sql("""
select idx, parentfield, parenttype, parent, mode_of_payment, `default` from `tabSales Invoice Payment` where parent=%s select idx, parentfield, parenttype, parent, mode_of_payment, `default` from `tabSales Invoice Payment` where parent=%s
""", pos_profile.name, as_dict=1) """, pos_profile.name, as_dict=1)

View File

@ -0,0 +1,25 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("accounts", "doctype", "POS Invoice Merge Log")
frappe.reload_doc("accounts", "doctype", "POS Closing Entry")
if frappe.db.count('POS Invoice Merge Log'):
frappe.db.sql('''
UPDATE
`tabPOS Invoice Merge Log` log, `tabPOS Invoice Reference` log_ref
SET
log.pos_closing_entry = (
SELECT clo_ref.parent FROM `tabPOS Invoice Reference` clo_ref
WHERE clo_ref.pos_invoice = log_ref.pos_invoice
AND clo_ref.parenttype = 'POS Closing Entry'
)
WHERE
log_ref.parent = log.name
''')
frappe.db.sql('''UPDATE `tabPOS Closing Entry` SET status = 'Submitted' where docstatus = 1''')
frappe.db.sql('''UPDATE `tabPOS Closing Entry` SET status = 'Cancelled' where docstatus = 2''')

View File

@ -0,0 +1,9 @@
import frappe
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
if frappe.db.exists('Custom Field', { 'fieldname': 'vehicle_no' }):
frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'mandatory_depends_on', '')

View File

@ -342,3 +342,11 @@ let render_employee_attendance = function (frm, data) {
}) })
); );
}; };
frappe.ui.form.on('Payroll Employee Detail', {
employee: function(frm) {
if (!frm.doc.payroll_frequency) {
frappe.throw(__("Please set a Payroll Frequency"));
}
}
});

View File

@ -41,40 +41,6 @@ class TestPayrollEntry(unittest.TestCase):
make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, payable_account=company_doc.default_payroll_payable_account, make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, payable_account=company_doc.default_payroll_payable_account,
currency=company_doc.default_currency) currency=company_doc.default_currency)
def test_multi_currency_payroll_entry(self): # pylint: disable=no-self-use
company = erpnext.get_default_company()
employee = make_employee("test_muti_currency_employee@payroll.com", company=company)
for data in frappe.get_all('Salary Component', fields = ["name"]):
if not frappe.db.get_value('Salary Component Account',
{'parent': data.name, 'company': company}, 'name'):
get_salary_component_account(data.name)
company_doc = frappe.get_doc('Company', company)
salary_structure = make_salary_structure("_Test Multi Currency Salary Structure", "Monthly", company=company, currency='USD')
create_salary_structure_assignment(employee, salary_structure.name, company=company)
frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""",(frappe.db.get_value("Employee", {"user_id": "test_muti_currency_employee@payroll.com"})))
salary_slip = get_salary_slip("test_muti_currency_employee@payroll.com", "Monthly", "_Test Multi Currency Salary Structure")
dates = get_start_end_dates('Monthly', nowdate())
payroll_entry = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date,
payable_account=company_doc.default_payroll_payable_account, currency='USD', exchange_rate=70)
payroll_entry.make_payment_entry()
salary_slip.load_from_db()
payroll_je = salary_slip.journal_entry
payroll_je_doc = frappe.get_doc('Journal Entry', payroll_je)
self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_debit)
self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_credit)
payment_entry = frappe.db.sql('''
Select ifnull(sum(je.total_debit),0) as total_debit, ifnull(sum(je.total_credit),0) as total_credit from `tabJournal Entry` je, `tabJournal Entry Account` jea
Where je.name = jea.parent
And jea.reference_name = %s
''', (payroll_entry.name), as_dict=1)
self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_debit)
self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_credit)
def test_payroll_entry_with_employee_cost_center(self): # pylint: disable=no-self-use def test_payroll_entry_with_employee_cost_center(self): # pylint: disable=no-self-use
for data in frappe.get_all('Salary Component', fields = ["name"]): for data in frappe.get_all('Salary Component', fields = ["name"]):

View File

@ -1103,10 +1103,10 @@ class SalarySlip(TransactionBase):
self.calculate_total_for_salary_slip_based_on_timesheet() self.calculate_total_for_salary_slip_based_on_timesheet()
else: else:
self.total_deduction = 0.0 self.total_deduction = 0.0
if self.earnings: if hasattr(self, "earnings"):
for earning in self.earnings: for earning in self.earnings:
self.gross_pay += flt(earning.amount, earning.precision("amount")) self.gross_pay += flt(earning.amount, earning.precision("amount"))
if self.deductions: if hasattr(self, "deductions"):
for deduction in self.deductions: for deduction in self.deductions:
self.total_deduction += flt(deduction.amount, deduction.precision("amount")) self.total_deduction += flt(deduction.amount, deduction.precision("amount"))
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment) self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment)

View File

@ -191,7 +191,6 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
item.rejected_qty = flt(item.received_qty - item.qty, precision("rejected_qty", item)); item.rejected_qty = flt(item.received_qty - item.qty, precision("rejected_qty", item));
item.received_stock_qty = flt(item.conversion_factor, precision("conversion_factor", item)) * flt(item.received_qty); item.received_stock_qty = flt(item.conversion_factor, precision("conversion_factor", item)) * flt(item.received_qty);
} }
this._super(doc, cdt, cdn); this._super(doc, cdt, cdn);
}, },

View File

@ -40,7 +40,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
cur_frm.cscript.set_gross_profit(item); cur_frm.cscript.set_gross_profit(item);
cur_frm.cscript.calculate_taxes_and_totals(); cur_frm.cscript.calculate_taxes_and_totals();
cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn);
}); });
@ -1121,6 +1121,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
} }
}); });
} }
me.calculate_stock_uom_rate(doc, cdt, cdn);
}, },
conversion_factor: function(doc, cdt, cdn, dont_fetch_price_list_rate) { conversion_factor: function(doc, cdt, cdn, dont_fetch_price_list_rate) {
@ -1141,6 +1142,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
frappe.meta.has_field(doc.doctype, "price_list_currency")) { frappe.meta.has_field(doc.doctype, "price_list_currency")) {
this.apply_price_list(item, true); this.apply_price_list(item, true);
} }
this.calculate_stock_uom_rate(doc, cdt, cdn);
} }
}, },
@ -1161,9 +1163,15 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
qty: function(doc, cdt, cdn) { qty: function(doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn); let item = frappe.get_doc(cdt, cdn);
this.conversion_factor(doc, cdt, cdn, true); this.conversion_factor(doc, cdt, cdn, true);
this.calculate_stock_uom_rate(doc, cdt, cdn);
this.apply_pricing_rule(item, true); this.apply_pricing_rule(item, true);
}, },
calculate_stock_uom_rate: function(doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn);
item.stock_uom_rate = flt(item.rate)/flt(item.conversion_factor);
refresh_field("stock_uom_rate", item.name, item.parentfield);
},
service_stop_date: function(frm, cdt, cdn) { service_stop_date: function(frm, cdt, cdn) {
var child = locals[cdt][cdn]; var child = locals[cdt][cdn];
@ -1274,7 +1282,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
this.frm.set_currency_labels(["base_rate", "base_net_rate", "base_price_list_rate", "base_amount", "base_net_amount"], this.frm.set_currency_labels(["base_rate", "base_net_rate", "base_price_list_rate", "base_amount", "base_net_amount"],
company_currency, "items"); company_currency, "items");
this.frm.set_currency_labels(["rate", "net_rate", "price_list_rate", "amount", "net_amount"], this.frm.set_currency_labels(["rate", "net_rate", "price_list_rate", "amount", "net_amount", "stock_uom_rate"],
this.frm.doc.currency, "items"); this.frm.doc.currency, "items");
if(this.frm.fields_dict["operations"]) { if(this.frm.fields_dict["operations"]) {

View File

@ -20,6 +20,7 @@ states = [
'Jharkhand', 'Jharkhand',
'Karnataka', 'Karnataka',
'Kerala', 'Kerala',
'Ladakh',
'Lakshadweep Islands', 'Lakshadweep Islands',
'Madhya Pradesh', 'Madhya Pradesh',
'Maharashtra', 'Maharashtra',
@ -59,6 +60,7 @@ state_numbers = {
"Jharkhand": "20", "Jharkhand": "20",
"Karnataka": "29", "Karnataka": "29",
"Kerala": "32", "Kerala": "32",
"Ladakh": "38",
"Lakshadweep Islands": "31", "Lakshadweep Islands": "31",
"Madhya Pradesh": "23", "Madhya Pradesh": "23",
"Maharashtra": "27", "Maharashtra": "27",
@ -80,4 +82,4 @@ state_numbers = {
"West Bengal": "19", "West Bengal": "19",
} }
number_state_mapping = {v: k for k, v in iteritems(state_numbers)} number_state_mapping = {v: k for k, v in iteritems(state_numbers)}

View File

@ -188,7 +188,6 @@ const get_ewaybill_fields = (frm) => {
'fieldname': 'vehicle_no', 'fieldname': 'vehicle_no',
'label': 'Vehicle No', 'label': 'Vehicle No',
'fieldtype': 'Data', 'fieldtype': 'Data',
'depends_on': 'eval:(doc.mode_of_transport === "Road")',
'default': frm.doc.vehicle_no 'default': frm.doc.vehicle_no
}, },
{ {

View File

@ -20,11 +20,13 @@ from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds
def validate_einvoice_fields(doc): def validate_einvoice_fields(doc):
einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable')) einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable'))
invalid_doctype = doc.doctype not in ['Sales Invoice'] invalid_doctype = doc.doctype != 'Sales Invoice'
invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
no_taxes_applied = not doc.get('taxes')
if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction: return
if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction or no_taxes_applied:
return
if doc.docstatus == 0 and doc._action == 'save': if doc.docstatus == 0 and doc._action == 'save':
if doc.irn: if doc.irn:
@ -35,7 +37,7 @@ def validate_einvoice_fields(doc):
elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn:
frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN')) frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN'))
elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: elif doc.irn and doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled:
frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed')) frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed'))
def raise_document_name_too_long_error(): def raise_document_name_too_long_error():
@ -158,10 +160,10 @@ def get_item_list(invoice):
item.update(d.as_dict()) item.update(d.as_dict())
item.sr_no = d.idx item.sr_no = d.idx
item.description = d.item_name.replace('"', '\\"') item.description = json.dumps(d.item_name)[1:-1]
item.qty = abs(item.qty) item.qty = abs(item.qty)
item.discount_amount = abs(item.discount_amount * item.qty) item.discount_amount = 0
item.unit_rate = abs(item.base_net_amount / item.qty) item.unit_rate = abs(item.base_net_amount / item.qty)
item.gross_amount = abs(item.base_net_amount) item.gross_amount = abs(item.base_net_amount)
item.taxable_value = abs(item.base_net_amount) item.taxable_value = abs(item.base_net_amount)
@ -221,11 +223,12 @@ def get_invoice_value_details(invoice):
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
invoice_value_details.base_total = abs(invoice.base_total) invoice_value_details.base_total = abs(invoice.base_total)
invoice_value_details.invoice_discount_amt = invoice.base_discount_amount
else: else:
invoice_value_details.base_total = abs(invoice.base_net_total) invoice_value_details.base_total = abs(invoice.base_net_total)
# since tax already considers discount amount
invoice_value_details.invoice_discount_amt = 0
# since tax already considers discount amount
invoice_value_details.invoice_discount_amt = 0 # invoice.base_discount_amount
invoice_value_details.round_off = invoice.base_rounding_adjustment invoice_value_details.round_off = invoice.base_rounding_adjustment
invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total) invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total)
invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total) invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total)
@ -302,7 +305,7 @@ def validate_mandatory_fields(invoice):
_('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'), _('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
title=_('Missing Fields') title=_('Missing Fields')
) )
if not frappe.db.get_value('Address', invoice.customer_address, 'gstin'): if invoice.gst_category != 'Overseas' and not frappe.db.get_value('Address', invoice.customer_address, 'gstin'):
frappe.throw( frappe.throw(
_('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'), _('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'),
title=_('Missing Fields') title=_('Missing Fields')
@ -443,6 +446,8 @@ class GSPConnector():
def get_credentials(self): def get_credentials(self):
if self.invoice: if self.invoice:
gstin = self.get_seller_gstin() gstin = self.get_seller_gstin()
if not self.e_invoice_settings.enable:
frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings")))
credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin) credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin)
else: else:
credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
@ -813,4 +818,4 @@ def generate_eway_bill(doctype, docname, **kwargs):
@frappe.whitelist() @frappe.whitelist()
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark): def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
gsp_connector = GSPConnector(doctype, docname) gsp_connector = GSPConnector(doctype, docname)
gsp_connector.cancel_eway_bill(eway_bill, reason, remark) gsp_connector.cancel_eway_bill(eway_bill, reason, remark)

View File

@ -168,5 +168,10 @@
"state_number": "37", "state_number": "37",
"state_code": "AD", "state_code": "AD",
"state_name": "Andhra Pradesh (New)" "state_name": "Andhra Pradesh (New)"
},
{
"state_number": "38",
"state_code": "LA",
"state_name": "Ladakh"
} }
] ]

View File

@ -236,6 +236,7 @@ class Gstr1Report(object):
self.cgst_sgst_invoices = [] self.cgst_sgst_invoices = []
unidentified_gst_accounts = [] unidentified_gst_accounts = []
unidentified_gst_accounts_invoice = []
for parent, account, item_wise_tax_detail, tax_amount in self.tax_details: for parent, account, item_wise_tax_detail, tax_amount in self.tax_details:
if account in self.gst_accounts.cess_account: if account in self.gst_accounts.cess_account:
self.invoice_cess.setdefault(parent, tax_amount) self.invoice_cess.setdefault(parent, tax_amount)
@ -251,6 +252,7 @@ class Gstr1Report(object):
if not (cgst_or_sgst or account in self.gst_accounts.igst_account): if not (cgst_or_sgst or account in self.gst_accounts.igst_account):
if "gst" in account.lower() and account not in unidentified_gst_accounts: if "gst" in account.lower() and account not in unidentified_gst_accounts:
unidentified_gst_accounts.append(account) unidentified_gst_accounts.append(account)
unidentified_gst_accounts_invoice.append(parent)
continue continue
for item_code, tax_amounts in item_wise_tax_detail.items(): for item_code, tax_amounts in item_wise_tax_detail.items():
@ -273,7 +275,7 @@ class Gstr1Report(object):
# Build itemised tax for export invoices where tax table is blank # Build itemised tax for export invoices where tax table is blank
for invoice, items in iteritems(self.invoice_items): for invoice, items in iteritems(self.invoice_items):
if invoice not in self.items_based_on_tax_rate \ if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \
and frappe.db.get_value(self.doctype, invoice, "export_type") == "Without Payment of Tax": and frappe.db.get_value(self.doctype, invoice, "export_type") == "Without Payment of Tax":
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())

View File

@ -110,9 +110,11 @@ def make_custom_fields():
'Purchase Order': purchase_invoice_fields + invoice_fields, 'Purchase Order': purchase_invoice_fields + invoice_fields,
'Purchase Receipt': purchase_invoice_fields + invoice_fields, 'Purchase Receipt': purchase_invoice_fields + invoice_fields,
'Sales Invoice': sales_invoice_fields + invoice_fields, 'Sales Invoice': sales_invoice_fields + invoice_fields,
'POS Invoice': sales_invoice_fields + invoice_fields,
'Sales Order': sales_invoice_fields + invoice_fields, 'Sales Order': sales_invoice_fields + invoice_fields,
'Delivery Note': sales_invoice_fields + invoice_fields, 'Delivery Note': sales_invoice_fields + invoice_fields,
'Sales Invoice Item': invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt], 'Sales Invoice Item': invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt],
'POS Invoice Item': invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt],
'Purchase Invoice Item': invoice_item_fields, 'Purchase Invoice Item': invoice_item_fields,
'Sales Order Item': invoice_item_fields, 'Sales Order Item': invoice_item_fields,
'Delivery Note Item': invoice_item_fields, 'Delivery Note Item': invoice_item_fields,

View File

@ -47,6 +47,7 @@
"base_amount", "base_amount",
"base_net_amount", "base_net_amount",
"pricing_rules", "pricing_rules",
"stock_uom_rate",
"is_free_item", "is_free_item",
"section_break_43", "section_break_43",
"valuation_rate", "valuation_rate",
@ -634,12 +635,20 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1,
"report_hide": 1 "report_hide": 1
},
{
"depends_on": "eval: doc.uom != doc.stock_uom",
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"options": "currency",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-05-19 20:48:43.222229", "modified": "2021-01-30 21:39:40.174551",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Quotation Item", "name": "Quotation Item",

View File

@ -180,6 +180,7 @@ class SalesOrder(SellingController):
update_coupon_code_count(self.coupon_code,'used') update_coupon_code_count(self.coupon_code,'used')
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
super(SalesOrder, self).on_cancel() super(SalesOrder, self).on_cancel()
# Cannot cancel closed SO # Cannot cancel closed SO

View File

@ -17,6 +17,18 @@ from erpnext.selling.doctype.product_bundle.test_product_bundle import make_prod
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
class TestSalesOrder(unittest.TestCase): class TestSalesOrder(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings",
"unlink_advance_payment_on_cancelation_of_order"))
@classmethod
def tearDownClass(cls) -> None:
# reset config to previous state
frappe.db.set_value("Accounts Settings", "Accounts Settings",
"unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting)
def tearDown(self): def tearDown(self):
frappe.set_user("Administrator") frappe.set_user("Administrator")
@ -325,6 +337,9 @@ class TestSalesOrder(unittest.TestCase):
create_dn_against_so(so.name, 4) create_dn_against_so(so.name, 4)
make_sales_invoice(so.name) make_sales_invoice(so.name)
prev_total = so.get("base_total")
prev_total_in_words = so.get("base_in_words")
first_item_of_so = so.get("items")[0] first_item_of_so = so.get("items")[0]
trans_item = json.dumps([ trans_item = json.dumps([
{'item_code' : first_item_of_so.item_code, 'rate' : first_item_of_so.rate, \ {'item_code' : first_item_of_so.item_code, 'rate' : first_item_of_so.rate, \
@ -340,6 +355,12 @@ class TestSalesOrder(unittest.TestCase):
self.assertEqual(so.get("items")[-1].amount, 1400) self.assertEqual(so.get("items")[-1].amount, 1400)
self.assertEqual(so.status, 'To Deliver and Bill') self.assertEqual(so.status, 'To Deliver and Bill')
updated_total = so.get("base_total")
updated_total_in_words = so.get("base_in_words")
self.assertEqual(updated_total, prev_total+1400)
self.assertNotEqual(updated_total_in_words, prev_total_in_words)
def test_update_child_removing_item(self): def test_update_child_removing_item(self):
so = make_sales_order(**{ so = make_sales_order(**{
"item_list": [{ "item_list": [{
@ -1040,6 +1061,38 @@ class TestSalesOrder(unittest.TestCase):
self.assertRaises(frappe.LinkExistsError, so_doc.cancel) self.assertRaises(frappe.LinkExistsError, so_doc.cancel)
def test_cancel_sales_order_after_cancel_payment_entry(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
# make a sales order
so = make_sales_order()
# disable unlinking of payment entry
frappe.db.set_value("Accounts Settings", "Accounts Settings",
"unlink_advance_payment_on_cancelation_of_order", 0)
# create a payment entry against sales order
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Bank - _TC")
pe.reference_no = "1"
pe.reference_date = nowdate()
pe.paid_from_account_currency = so.currency
pe.paid_to_account_currency = so.currency
pe.source_exchange_rate = 1
pe.target_exchange_rate = 1
pe.paid_amount = so.grand_total
pe.save(ignore_permissions=True)
pe.submit()
# Cancel payment entry
po_doc = frappe.get_doc("Payment Entry", pe.name)
po_doc.cancel()
# Cancel sales order
try:
so_doc = frappe.get_doc('Sales Order', so.name)
so_doc.cancel()
except Exception:
self.fail("Can not cancel sales order with linked cancelled payment entry")
def test_request_for_raw_materials(self): def test_request_for_raw_materials(self):
item = make_item("_Test Finished Item", {"is_stock_item": 1, item = make_item("_Test Finished Item", {"is_stock_item": 1,
"maintain_stock": 1, "maintain_stock": 1,
@ -1198,4 +1251,4 @@ def make_sales_order_workflow():
)) ))
workflow.insert(ignore_permissions=True) workflow.insert(ignore_permissions=True)
return workflow return workflow

View File

@ -46,6 +46,7 @@
"base_rate", "base_rate",
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"stock_uom_rate",
"is_free_item", "is_free_item",
"section_break_24", "section_break_24",
"net_rate", "net_rate",
@ -214,7 +215,6 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "UOM", "label": "UOM",
"options": "UOM", "options": "UOM",
"print_hide": 0,
"reqd": 1 "reqd": 1
}, },
{ {
@ -780,12 +780,20 @@
"fieldname": "manufacturing_section_section", "fieldname": "manufacturing_section_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Manufacturing Section" "label": "Manufacturing Section"
},
{
"depends_on": "eval: doc.uom != doc.stock_uom",
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"options": "currency",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-012-07 20:54:32.309460", "modified": "2021-01-30 21:35:07.617320",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Item", "name": "Sales Order Item",

View File

@ -69,6 +69,10 @@ erpnext.PointOfSale.Controller = class {
dialog.fields_dict.balance_details.grid.refresh(); dialog.fields_dict.balance_details.grid.refresh();
}); });
} }
const pos_profile_query = {
query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query',
filters: { company: frappe.defaults.get_default('company') }
}
const dialog = new frappe.ui.Dialog({ const dialog = new frappe.ui.Dialog({
title: __('Create POS Opening Entry'), title: __('Create POS Opening Entry'),
static: true, static: true,
@ -80,6 +84,7 @@ erpnext.PointOfSale.Controller = class {
{ {
fieldtype: 'Link', label: __('POS Profile'), fieldtype: 'Link', label: __('POS Profile'),
options: 'POS Profile', fieldname: 'pos_profile', reqd: 1, options: 'POS Profile', fieldname: 'pos_profile', reqd: 1,
get_query: () => pos_profile_query,
onchange: () => fetch_pos_payment_methods() onchange: () => fetch_pos_payment_methods()
}, },
{ {
@ -124,9 +129,8 @@ erpnext.PointOfSale.Controller = class {
}); });
frappe.db.get_doc("POS Profile", this.pos_profile).then((profile) => { frappe.db.get_doc("POS Profile", this.pos_profile).then((profile) => {
Object.assign(this.settings, profile);
this.settings.customer_groups = profile.customer_groups.map(group => group.customer_group); this.settings.customer_groups = profile.customer_groups.map(group => group.customer_group);
this.settings.hide_images = profile.hide_images;
this.settings.auto_add_item_to_cart = profile.auto_add_item_to_cart;
this.make_app(); this.make_app();
}); });
} }
@ -255,11 +259,9 @@ erpnext.PointOfSale.Controller = class {
get_frm: () => this.frm, get_frm: () => this.frm,
cart_item_clicked: (item_code, batch_no, uom) => { cart_item_clicked: (item_code, batch_no, uom) => {
const item_row = this.frm.doc.items.find( const search_field = batch_no ? 'batch_no' : 'item_code';
i => i.item_code === item_code const search_value = batch_no || item_code;
&& i.uom === uom const item_row = this.frm.doc.items.find(i => i[search_field] === search_value && i.uom === uom);
&& (!batch_no || (batch_no && i.batch_no === batch_no))
);
this.item_details.toggle_item_details_section(item_row); this.item_details.toggle_item_details_section(item_row);
}, },
@ -281,6 +283,7 @@ erpnext.PointOfSale.Controller = class {
init_item_details() { init_item_details() {
this.item_details = new erpnext.PointOfSale.ItemDetails({ this.item_details = new erpnext.PointOfSale.ItemDetails({
wrapper: this.$components_wrapper, wrapper: this.$components_wrapper,
settings: this.settings,
events: { events: {
get_frm: () => this.frm, get_frm: () => this.frm,
@ -415,6 +418,11 @@ erpnext.PointOfSale.Controller = class {
() => this.item_selector.toggle_component(true) () => this.item_selector.toggle_component(true)
]); ]);
}, },
delete_order: (name) => {
frappe.model.delete_doc(this.frm.doc.doctype, name, () => {
this.recent_order_list.refresh_list();
});
},
new_order: () => { new_order: () => {
frappe.run_serially([ frappe.run_serially([
() => frappe.dom.freeze(), () => frappe.dom.freeze(),
@ -696,14 +704,14 @@ erpnext.PointOfSale.Controller = class {
frappe.dom.freeze(); frappe.dom.freeze();
const { doctype, name, current_item } = this.item_details; const { doctype, name, current_item } = this.item_details;
frappe.model.set_value(doctype, name, 'qty', 0); frappe.model.set_value(doctype, name, 'qty', 0)
.then(() => {
this.frm.script_manager.trigger('qty', doctype, name).then(() => { frappe.model.clear_doc(doctype, name);
frappe.model.clear_doc(doctype, name); this.update_cart_html(current_item, true);
this.update_cart_html(current_item, true); this.item_details.toggle_item_details_section(undefined);
this.item_details.toggle_item_details_section(undefined); frappe.dom.unfreeze();
frappe.dom.unfreeze(); })
}) .catch(e => console.log(e));
} }
} }

View File

@ -5,6 +5,8 @@ erpnext.PointOfSale.ItemCart = class {
this.customer_info = undefined; this.customer_info = undefined;
this.hide_images = settings.hide_images; this.hide_images = settings.hide_images;
this.allowed_customer_groups = settings.customer_groups; this.allowed_customer_groups = settings.customer_groups;
this.allow_rate_change = settings.allow_rate_change;
this.allow_discount_change = settings.allow_discount_change;
this.init_component(); this.init_component();
} }
@ -201,7 +203,7 @@ erpnext.PointOfSale.ItemCart = class {
me.events.checkout(); me.events.checkout();
me.toggle_checkout_btn(false); me.toggle_checkout_btn(false);
me.$add_discount_elem.removeClass("d-none"); me.allow_discount_change && me.$add_discount_elem.removeClass("d-none");
}); });
this.$totals_section.on('click', '.edit-cart-btn', () => { this.$totals_section.on('click', '.edit-cart-btn', () => {
@ -479,11 +481,15 @@ erpnext.PointOfSale.ItemCart = class {
update_totals_section(frm) { update_totals_section(frm) {
if (!frm) frm = this.events.get_frm(); if (!frm) frm = this.events.get_frm();
this.render_net_total(frm.doc.base_net_total); this.render_net_total(frm.doc.net_total);
this.render_grand_total(frm.doc.base_grand_total); this.render_grand_total(frm.doc.grand_total);
const taxes = frm.doc.taxes.map(t => { return { description: t.description, rate: t.rate }}) const taxes = frm.doc.taxes.map(t => {
this.render_taxes(frm.doc.base_total_taxes_and_charges, taxes); return {
description: t.description, rate: t.rate
};
});
this.render_taxes(frm.doc.total_taxes_and_charges, taxes);
} }
render_net_total(value) { render_net_total(value) {
@ -545,7 +551,7 @@ erpnext.PointOfSale.ItemCart = class {
get_cart_item({ item_code, batch_no, uom }) { get_cart_item({ item_code, batch_no, uom }) {
const batch_attr = `[data-batch-no="${escape(batch_no)}"]`; const batch_attr = `[data-batch-no="${escape(batch_no)}"]`;
const item_code_attr = `[data-item-code="${escape(item_code)}"]`; const item_code_attr = `[data-item-code="${escape(item_code)}"]`;
const uom_attr = `[data-uom=${escape(uom)}]`; const uom_attr = `[data-uom="${escape(uom)}"]`;
const item_selector = batch_no ? const item_selector = batch_no ?
`.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`; `.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`;
@ -667,7 +673,7 @@ erpnext.PointOfSale.ItemCart = class {
update_selector_value_in_cart_item(selector, value, item) { update_selector_value_in_cart_item(selector, value, item) {
const $item_to_update = this.get_cart_item(item); const $item_to_update = this.get_cart_item(item);
$item_to_update.attr(`data-${selector}`, value); $item_to_update.attr(`data-${selector}`, escape(value));
} }
toggle_checkout_btn(show_checkout) { toggle_checkout_btn(show_checkout) {
@ -702,14 +708,26 @@ erpnext.PointOfSale.ItemCart = class {
on_numpad_event($btn) { on_numpad_event($btn) {
const current_action = $btn.attr('data-button-value'); const current_action = $btn.attr('data-button-value');
const action_is_field_edit = ['qty', 'discount_percentage', 'rate'].includes(current_action); const action_is_field_edit = ['qty', 'discount_percentage', 'rate'].includes(current_action);
const action_is_allowed = action_is_field_edit ? (
this.highlight_numpad_btn($btn, current_action); (current_action == 'rate' && this.allow_rate_change) ||
(current_action == 'discount_percentage' && this.allow_discount_change) ||
(current_action == 'qty')) : true;
const action_is_pressed_twice = this.prev_action === current_action; const action_is_pressed_twice = this.prev_action === current_action;
const first_click_event = !this.prev_action; const first_click_event = !this.prev_action;
const field_to_edit_changed = this.prev_action && this.prev_action != current_action; const field_to_edit_changed = this.prev_action && this.prev_action != current_action;
if (action_is_field_edit) { if (action_is_field_edit) {
if (!action_is_allowed) {
const label = current_action == 'rate' ? 'Rate'.bold() : 'Discount'.bold();
const message = __('Editing {0} is not allowed as per POS Profile settings', [label]);
frappe.show_alert({
indicator: 'red',
message: message
});
frappe.utils.play_sound("error");
return;
}
if (first_click_event || field_to_edit_changed) { if (first_click_event || field_to_edit_changed) {
this.prev_action = current_action; this.prev_action = current_action;
@ -753,6 +771,7 @@ erpnext.PointOfSale.ItemCart = class {
this.numpad_value = current_action; this.numpad_value = current_action;
} }
this.highlight_numpad_btn($btn, current_action);
this.events.numpad_event(this.numpad_value, this.prev_action); this.events.numpad_event(this.numpad_value, this.prev_action);
} }

View File

@ -1,7 +1,9 @@
erpnext.PointOfSale.ItemDetails = class { erpnext.PointOfSale.ItemDetails = class {
constructor({ wrapper, events }) { constructor({ wrapper, events, settings }) {
this.wrapper = wrapper; this.wrapper = wrapper;
this.events = events; this.events = events;
this.allow_rate_change = settings.allow_rate_change;
this.allow_discount_change = settings.allow_discount_change;
this.current_item = {}; this.current_item = {};
this.init_component(); this.init_component();
@ -207,17 +209,27 @@ erpnext.PointOfSale.ItemDetails = class {
bind_custom_control_change_event() { bind_custom_control_change_event() {
const me = this; const me = this;
if (this.rate_control) { if (this.rate_control) {
this.rate_control.df.onchange = function() { if (this.allow_rate_change) {
if (this.value || flt(this.value) === 0) { this.rate_control.df.onchange = function() {
me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => { if (this.value || flt(this.value) === 0) {
const item_row = frappe.get_doc(me.doctype, me.name); me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => {
const doc = me.events.get_frm().doc; 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); me.$item_price.html(format_currency(item_row.rate, doc.currency));
}); me.render_discount_dom(item_row);
} });
}
};
} else {
this.rate_control.df.read_only = 1;
} }
this.rate_control.refresh();
}
if (this.discount_percentage_control && !this.allow_discount_change) {
this.discount_percentage_control.df.read_only = 1;
this.discount_percentage_control.refresh();
} }
if (this.warehouse_control) { if (this.warehouse_control) {
@ -294,8 +306,16 @@ erpnext.PointOfSale.ItemDetails = class {
} }
frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => { frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => {
const { item_code, batch_no, uom } = this.current_item;
const item_code_is_same = item_code === item_row.item_code;
const batch_is_same = batch_no == item_row.batch_no;
const uom_is_same = uom === item_row.uom;
// check if current_item is same as item_row
const item_is_same = item_code_is_same && batch_is_same && uom_is_same ? true : false;
const field_control = me[`${fieldname}_control`]; const field_control = me[`${fieldname}_control`];
if (field_control) {
if (item_is_same && field_control && field_control.get_value() !== value) {
field_control.set_value(value); field_control.set_value(value);
cur_pos.update_cart_html(item_row); cur_pos.update_cart_html(item_row);
} }

View File

@ -265,6 +265,14 @@ erpnext.PointOfSale.PastOrderSummary = class {
this.$summary_wrapper.addClass('d-none'); this.$summary_wrapper.addClass('d-none');
}); });
this.$summary_container.on('click', '.delete-btn', () => {
this.events.delete_order(this.doc.name);
this.show_summary_placeholder();
// 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.$summary_container.on('click', '.new-btn', () => {
this.events.new_order(); this.events.new_order();
this.toggle_component(false); this.toggle_component(false);
@ -401,7 +409,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
return [{ condition: true, visible_btns: ['Print Receipt', 'Email Receipt', 'New Order'] }]; return [{ condition: true, visible_btns: ['Print Receipt', 'Email Receipt', 'New Order'] }];
return [ return [
{ condition: this.doc.docstatus === 0, visible_btns: ['Edit Order'] }, { condition: this.doc.docstatus === 0, visible_btns: ['Edit Order', 'Delete 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', 'Return']},
{ condition: this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt']} { condition: this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt']}
]; ];

View File

@ -168,30 +168,22 @@ erpnext.PointOfSale.Payment = class {
me.toggle_numpad(true); me.toggle_numpad(true);
me.selected_mode = me[`${mode}_control`]; me.selected_mode = me[`${mode}_control`];
const doc = me.events.get_frm().doc; me.selected_mode && me.selected_mode.$input.get(0).focus();
me.selected_mode?.$input?.get(0).focus(); me.auto_set_remaining_amount();
const current_value = me.selected_mode?.get_value()
!current_value && doc.grand_total > doc.paid_amount ? me.selected_mode?.set_value(doc.grand_total - doc.paid_amount) : '';
} }
}) })
frappe.realtime.on("process_phone_payment", function(data) { frappe.ui.form.on('POS Invoice', 'contact_mobile', (frm) => {
frappe.dom.unfreeze(); const contact = frm.doc.contact_mobile;
cur_frm.reload_doc(); const request_button = $(this.request_for_payment_field.$input[0]);
let message = data["ResultDesc"]; if (contact) {
let title = __("Payment Failed"); request_button.removeClass('btn-default').addClass('btn-primary');
} else {
request_button.removeClass('btn-primary').addClass('btn-default');
}
});
if (data["ResultCode"] == 0) { this.setup_listener_for_payments();
title = __("Payment Received");
$('.btn.btn-xs.btn-default[data-fieldname=request_for_payment]').html(`Payment Received`)
me.events.submit_invoice();
}
frappe.msgprint({
"message": message,
"title": title
});
});
this.$payment_modes.on('click', '.shortcut', function(e) { this.$payment_modes.on('click', '.shortcut', function(e) {
const value = $(this).attr('data-value'); const value = $(this).attr('data-value');
@ -250,6 +242,41 @@ erpnext.PointOfSale.Payment = class {
}) })
} }
setup_listener_for_payments() {
frappe.realtime.on("process_phone_payment", (data) => {
const doc = this.events.get_frm().doc;
const { response, amount, success, failure_message } = data;
let message, title;
if (success) {
title = __("Payment Received");
if (amount >= doc.grand_total) {
frappe.dom.unfreeze();
message = __("Payment of {0} received successfully.", [format_currency(amount, doc.currency, 0)]);
this.events.submit_invoice();
cur_frm.reload_doc();
} else {
message = __("Payment of {0} received successfully. Waiting for other requests to complete...", [format_currency(amount, doc.currency, 0)]);
}
} else if (failure_message) {
message = failure_message;
title = __("Payment Failed");
}
frappe.msgprint({ "message": message, "title": title });
});
}
auto_set_remaining_amount() {
const doc = this.events.get_frm().doc;
const remaining_amount = doc.grand_total - doc.paid_amount;
const current_value = this.selected_mode ? this.selected_mode.get_value() : undefined;
if (!current_value && remaining_amount > 0 && this.selected_mode) {
this.selected_mode.set_value(remaining_amount);
}
}
attach_shortcuts() { attach_shortcuts() {
const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl'; const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl';
this.$component.find('.submit-order').attr("title", `${ctrl_label}+Enter`); this.$component.find('.submit-order').attr("title", `${ctrl_label}+Enter`);
@ -370,9 +397,11 @@ erpnext.PointOfSale.Payment = class {
fieldtype: 'Currency', fieldtype: 'Currency',
placeholder: __('Enter {0} amount.', [p.mode_of_payment]), placeholder: __('Enter {0} amount.', [p.mode_of_payment]),
onchange: function() { onchange: function() {
if (this.value || this.value == 0) { const current_value = frappe.model.get_value(p.doctype, p.name, 'amount');
frappe.model.set_value(p.doctype, p.name, 'amount', flt(this.value)) if (current_value != this.value) {
.then(() => me.update_totals_section()); 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); const formatted_currency = format_currency(this.value, currency);
me.$payment_modes.find(`.${mode}-amount`).html(formatted_currency); me.$payment_modes.find(`.${mode}-amount`).html(formatted_currency);

View File

@ -139,7 +139,7 @@
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2020-03-18 18:10:13.048492", "modified": "2021-02-08 17:01:52.162202",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Customer Group", "name": "Customer Group",
@ -189,6 +189,15 @@
"permlevel": 1, "permlevel": 1,
"read": 1, "read": 1,
"role": "Sales Manager" "role": "Sales Manager"
},
{
"email": 1,
"export": 1,
"print": 1,
"report": 1,
"role": "Customer",
"select": 1,
"share": 1
} }
], ],
"search_fields": "parent_customer_group", "search_fields": "parent_customer_group",

View File

@ -245,6 +245,15 @@
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Accounts User" "role": "Accounts User"
},
{
"email": 1,
"export": 1,
"print": 1,
"report": 1,
"role": "Customer",
"select": 1,
"share": 1
} }
], ],
"search_fields": "parent_item_group", "search_fields": "parent_item_group",

View File

@ -123,7 +123,7 @@
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2020-03-18 18:11:36.623555", "modified": "2021-02-08 17:10:03.767426",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Territory", "name": "Territory",
@ -166,6 +166,15 @@
{ {
"read": 1, "read": 1,
"role": "Maintenance User" "role": "Maintenance User"
},
{
"email": 1,
"export": 1,
"print": 1,
"report": 1,
"role": "Customer",
"select": 1,
"share": 1
} }
], ],
"search_fields": "parent_territory,territory_manager", "search_fields": "parent_territory,territory_manager",

View File

@ -64,10 +64,10 @@ def get_warehouse_account(warehouse, warehouse_account=None):
if not account and warehouse.company: if not account and warehouse.company:
account = get_company_default_inventory_account(warehouse.company) account = get_company_default_inventory_account(warehouse.company)
if not account and warehouse.company: if not account and warehouse.company and not warehouse.is_group:
frappe.throw(_("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}") frappe.throw(_("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}")
.format(warehouse.name, warehouse.company)) .format(warehouse.name, warehouse.company))
return account return account
def get_company_default_inventory_account(company): def get_company_default_inventory_account(company):
return frappe.get_cached_value('Company', company, 'default_inventory_account') return frappe.get_cached_value('Company', company, 'default_inventory_account')

View File

@ -298,9 +298,9 @@ class TestBatch(unittest.TestCase):
self.assertEqual(details.get('price_list_rate'), 400) self.assertEqual(details.get('price_list_rate'), 400)
def create_batch(item_code, rate, create_item_price_for_batch): def create_batch(item_code, rate, create_item_price_for_batch):
pi = make_purchase_invoice(company="_Test Company with perpetual inventory", pi = make_purchase_invoice(company="_Test Company",
warehouse= "Stores - TCP1", cost_center = "Main - TCP1", update_stock=1, warehouse= "Stores - _TC", cost_center = "Main - _TC", update_stock=1,
expense_account ="_Test Account Cost for Goods Sold - TCP1", item_code=item_code) expense_account ="_Test Account Cost for Goods Sold - _TC", item_code=item_code)
batch = frappe.db.get_value('Batch', {'item': item_code, 'reference_name': pi.name}) batch = frappe.db.get_value('Batch', {'item': item_code, 'reference_name': pi.name})

View File

@ -16,8 +16,9 @@ class Bin(Document):
def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False): def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False):
'''Called from erpnext.stock.utils.update_bin''' '''Called from erpnext.stock.utils.update_bin'''
self.update_qty(args) self.update_qty(args)
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
from erpnext.stock.stock_ledger import update_entries_after, validate_negative_qty_in_future_sle from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
if not args.get("posting_date"): if not args.get("posting_date"):
args["posting_date"] = nowdate() args["posting_date"] = nowdate()
@ -34,11 +35,13 @@ class Bin(Document):
"posting_time": args.get("posting_time"), "posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"), "voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"), "voucher_no": args.get("voucher_no"),
"sle_id": args.name "sle_id": args.name,
"creation": args.creation
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
# Validate negative qty in future transactions # update qty in future ale and Validate negative qty
validate_negative_qty_in_future_sle(args) update_qty_in_future_sle(args, allow_negative_stock)
def update_qty(self, args): def update_qty(self, args):
# update the stock values (for current quantities) # update the stock values (for current quantities)
@ -51,7 +54,7 @@ class Bin(Document):
self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty")) self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty"))
self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty")) self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty"))
self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty")) self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty"))
self.set_projected_qty() self.set_projected_qty()
self.db_update() self.db_update()

View File

@ -489,7 +489,10 @@ class TestDeliveryNote(unittest.TestCase):
def test_closed_delivery_note(self): def test_closed_delivery_note(self):
from erpnext.stock.doctype.delivery_note.delivery_note import update_delivery_note_status from erpnext.stock.doctype.delivery_note.delivery_note import update_delivery_note_status
dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True) make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100)
dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True)
dn.submit() dn.submit()

View File

@ -47,6 +47,7 @@
"base_rate", "base_rate",
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"stock_uom_rate",
"is_free_item", "is_free_item",
"section_break_25", "section_break_25",
"net_rate", "net_rate",
@ -743,13 +744,21 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"depends_on": "eval: doc.uom != doc.stock_uom",
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"options": "currency",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-26 17:31:27.029803", "modified": "2021-01-30 21:42:03.767968",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note Item", "name": "Delivery Note Item",

View File

@ -672,13 +672,14 @@ class Item(WebsiteGenerator):
if not records: return if not records: return
document = _("Stock Reconciliation") if len(records) == 1 else _("Stock Reconciliations") document = _("Stock Reconciliation") if len(records) == 1 else _("Stock Reconciliations")
msg = _("The items {0} and {1} are present in the following {2} : <br>" msg = _("The items {0} and {1} are present in the following {2} : ").format(
.format(frappe.bold(old_name), frappe.bold(new_name), document)) frappe.bold(old_name), frappe.bold(new_name), document)
msg += '<br>'
msg += ', '.join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "<br><br>" msg += ', '.join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "<br><br>"
msg += _("Note: To merge the items, create a separate Stock Reconciliation for the old item {0}" msg += _("Note: To merge the items, create a separate Stock Reconciliation for the old item {0}").format(
.format(frappe.bold(old_name))) frappe.bold(old_name))
frappe.throw(_(msg), title=_("Merge not allowed")) frappe.throw(_(msg), title=_("Merge not allowed"))
@ -971,7 +972,7 @@ class Item(WebsiteGenerator):
frappe.throw(_("As there are existing transactions against item {0}, you can not change the value of {1}").format(self.name, frappe.bold(self.meta.get_label(field)))) frappe.throw(_("As there are existing transactions against item {0}, you can not change the value of {1}").format(self.name, frappe.bold(self.meta.get_label(field))))
def check_if_linked_document_exists(self, field): def check_if_linked_document_exists(self, field):
linked_doctypes = ["Delivery Note Item", "Sales Invoice Item", "Purchase Receipt Item", linked_doctypes = ["Delivery Note Item", "Sales Invoice Item", "POS Invoice Item", "Purchase Receipt Item",
"Purchase Invoice Item", "Stock Entry Detail", "Stock Reconciliation Item"] "Purchase Invoice Item", "Stock Entry Detail", "Stock Reconciliation Item"]
# For "Is Stock Item", following doctypes is important # For "Is Stock Item", following doctypes is important

View File

@ -148,7 +148,6 @@ class TestLandedCostVoucher(unittest.TestCase):
def test_landed_cost_voucher_for_odd_numbers (self): def test_landed_cost_voucher_for_odd_numbers (self):
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True) pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True)
pr.items[0].cost_center = "Main - TCP1" pr.items[0].cost_center = "Main - TCP1"
for x in range(2): for x in range(2):
@ -208,6 +207,10 @@ class TestLandedCostVoucher(unittest.TestCase):
self.assertEqual(pr.items[1].landed_cost_voucher_amount, 100) self.assertEqual(pr.items[1].landed_cost_voucher_amount, 100)
def test_multi_currency_lcv(self): def test_multi_currency_lcv(self):
from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records, save_new_records
save_new_records(test_records)
## Create USD Shipping charges_account ## Create USD Shipping charges_account
usd_shipping = create_account(account_name="Shipping Charges USD", usd_shipping = create_account(account_name="Shipping Charges USD",
parent_account="Duties and Taxes - TCP1", company="_Test Company with perpetual inventory", parent_account="Duties and Taxes - TCP1", company="_Test Company with perpetual inventory",

View File

@ -295,7 +295,8 @@ class PurchaseReceipt(BuyingController):
"against": warehouse_account[d.warehouse]["account"], "against": warehouse_account[d.warehouse]["account"],
"cost_center": d.cost_center, "cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(amount["base_amount"]), "credit": (flt(amount["base_amount"]) if (amount["base_amount"] or
account_currency!=self.company_currency) else flt(amount["amount"])),
"credit_in_account_currency": flt(amount["amount"]), "credit_in_account_currency": flt(amount["amount"]),
"project": d.project "project": d.project
}, item=d)) }, item=d))

View File

@ -94,10 +94,15 @@ class TestPurchaseReceipt(unittest.TestCase):
frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete() frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete()
def test_purchase_receipt_no_gl_entry(self): def test_purchase_receipt_no_gl_entry(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item", existing_bin_qty, existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
"warehouse": "_Test Warehouse - _TC"}, "stock_value") "warehouse": "_Test Warehouse - _TC"}, ["actual_qty", "stock_value"])
if existing_bin_qty < 0:
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=abs(existing_bin_qty))
pr = make_purchase_receipt() pr = make_purchase_receipt()

View File

@ -48,6 +48,7 @@
"base_rate", "base_rate",
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"stock_uom_rate",
"is_free_item", "is_free_item",
"section_break_29", "section_break_29",
"net_rate", "net_rate",
@ -874,6 +875,14 @@
"label": "Received Qty in Stock UOM", "label": "Received Qty in Stock UOM",
"print_hide": 1 "print_hide": 1
}, },
{
"depends_on": "eval: doc.uom != doc.stock_uom",
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"options": "currency",
"read_only": 1
},
{ {
"fieldname": "delivery_note_item", "fieldname": "delivery_note_item",
"fieldtype": "Data", "fieldtype": "Data",
@ -886,7 +895,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-26 16:50:56.479347", "modified": "2021-01-30 21:44:06.918515",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt Item", "name": "Purchase Receipt Item",

View File

@ -46,6 +46,9 @@ class RepostItemValuation(Document):
def repost(doc): def repost(doc):
try: try:
if not frappe.db.exists("Repost Item Valuation", doc.name):
return
doc.set_status('In Progress') doc.set_status('In Progress')
frappe.db.commit() frappe.db.commit()
@ -64,7 +67,7 @@ def repost(doc):
message += "<br>" + "Traceback: <br>" + traceback message += "<br>" + "Traceback: <br>" + traceback
frappe.db.set_value(doc.doctype, doc.name, 'error_log', message) frappe.db.set_value(doc.doctype, doc.name, 'error_log', message)
notify_error_to_stock_managers(doc) notify_error_to_stock_managers(doc, message)
doc.set_status('Failed') doc.set_status('Failed')
raise raise
finally: finally:

View File

@ -190,6 +190,7 @@ def create_shipment_company(company_name, abbr):
company.abbr = abbr company.abbr = abbr
company.default_currency = 'EUR' company.default_currency = 'EUR'
company.country = 'Germany' company.country = 'Germany'
company.enable_perpetual_inventory = 0
company.insert() company.insert()
return company return company

View File

@ -37,6 +37,7 @@ class StockLedgerEntry(Document):
self.block_transactions_against_group_warehouse() self.block_transactions_against_group_warehouse()
self.validate_with_last_transaction_posting_time() self.validate_with_last_transaction_posting_time()
def on_submit(self): def on_submit(self):
self.check_stock_frozen_date() self.check_stock_frozen_date()
self.actual_amt_check() self.actual_amt_check()

View File

@ -19,7 +19,7 @@ from erpnext.stock.doctype.item_manufacturer.item_manufacturer import get_item_m
from six import string_types, iteritems from six import string_types, iteritems
sales_doctypes = ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice'] sales_doctypes = ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice', 'POS Invoice']
purchase_doctypes = ['Material Request', 'Supplier Quotation', 'Purchase Order', 'Purchase Receipt', 'Purchase Invoice'] purchase_doctypes = ['Material Request', 'Supplier Quotation', 'Purchase Order', 'Purchase Receipt', 'Purchase Invoice']
@frappe.whitelist() @frappe.whitelist()

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