Merge branch 'version-13-beta-pre-release' into version-13-beta
This commit is contained in:
commit
d713ac4505
@ -5,7 +5,7 @@ import frappe
|
||||
from erpnext.hooks import regional_overrides
|
||||
from frappe.utils import getdate
|
||||
|
||||
__version__ = '13.0.0-beta.11'
|
||||
__version__ = '13.0.0-beta.12'
|
||||
|
||||
def get_default_company(user=None):
|
||||
'''Get default company for user'''
|
||||
|
@ -33,11 +33,11 @@ class AccountingDimension(Document):
|
||||
if frappe.flags.in_test:
|
||||
make_dimension_in_accounting_doctypes(doc=self)
|
||||
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):
|
||||
if frappe.flags.in_test:
|
||||
delete_accounting_dimension(doc=self)
|
||||
delete_accounting_dimension(doc=self, queue='long')
|
||||
else:
|
||||
frappe.enqueue(delete_accounting_dimension, doc=self)
|
||||
|
||||
@ -48,6 +48,9 @@ class AccountingDimension(Document):
|
||||
if not self.fieldname:
|
||||
self.fieldname = scrub(self.label)
|
||||
|
||||
def on_update(self):
|
||||
frappe.flags.accounting_dimensions = None
|
||||
|
||||
def make_dimension_in_accounting_doctypes(doc):
|
||||
doclist = get_doctypes_with_dimensions()
|
||||
doc_count = len(get_accounting_dimensions())
|
||||
@ -165,9 +168,9 @@ def toggle_disabling(doc):
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
|
||||
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",
|
||||
"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",
|
||||
"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",
|
||||
@ -176,12 +179,14 @@ def get_doctypes_with_dimensions():
|
||||
return doclist
|
||||
|
||||
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:
|
||||
return [d.fieldname for d in accounting_dimensions]
|
||||
return [d.fieldname for d in frappe.flags.accounting_dimensions]
|
||||
else:
|
||||
return accounting_dimensions
|
||||
return frappe.flags.accounting_dimensions
|
||||
|
||||
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
|
||||
|
@ -108,7 +108,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-16 15:27:23.659285",
|
||||
"modified": "2021-02-03 12:04:58.678402",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Dimension Filter",
|
||||
@ -125,6 +125,30 @@
|
||||
"role": "System Manager",
|
||||
"share": 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,
|
||||
|
@ -27,30 +27,30 @@ class GLEntry(Document):
|
||||
|
||||
def validate(self):
|
||||
self.flags.ignore_submit_comment = True
|
||||
self.check_mandatory()
|
||||
self.validate_and_set_fiscal_year()
|
||||
self.pl_must_have_cost_center()
|
||||
self.validate_cost_center()
|
||||
|
||||
if not self.flags.from_repost:
|
||||
self.check_mandatory()
|
||||
self.validate_cost_center()
|
||||
self.check_pl_account()
|
||||
self.validate_party()
|
||||
self.validate_currency()
|
||||
|
||||
def on_update_with_args(self, adv_adj, update_outstanding = 'Yes', from_repost=False):
|
||||
if not from_repost:
|
||||
def on_update(self):
|
||||
adv_adj = self.flags.adv_adj
|
||||
if not self.flags.from_repost:
|
||||
self.validate_account_details(adv_adj)
|
||||
self.validate_dimensions_for_pl_and_bs()
|
||||
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)
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
|
||||
# Update outstanding amt on against voucher
|
||||
if self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] \
|
||||
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)
|
||||
# Update outstanding amt on against voucher
|
||||
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(self.account, self.party_type, self.party, self.against_voucher_type,
|
||||
self.against_voucher)
|
||||
|
||||
def check_mandatory(self):
|
||||
mandatory = ['account','voucher_type','voucher_no','company']
|
||||
@ -58,7 +58,7 @@ class GLEntry(Document):
|
||||
if not self.get(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 account_type == "Receivable":
|
||||
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))
|
||||
|
||||
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':
|
||||
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))
|
||||
@ -140,25 +140,16 @@ class GLEntry(Document):
|
||||
.format(self.voucher_type, self.voucher_no, self.account, self.company))
|
||||
|
||||
def validate_cost_center(self):
|
||||
if not hasattr(self, "cost_center_company"):
|
||||
self.cost_center_company = {}
|
||||
if not self.cost_center: return
|
||||
|
||||
def _get_cost_center_company():
|
||||
if not self.cost_center_company.get(self.cost_center):
|
||||
self.cost_center_company[self.cost_center] = frappe.db.get_value(
|
||||
"Cost Center", self.cost_center, "company")
|
||||
is_group, company = frappe.get_cached_value('Cost Center',
|
||||
self.cost_center, ['is_group', 'company'])
|
||||
|
||||
return self.cost_center_company[self.cost_center]
|
||||
|
||||
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:
|
||||
if company != self.company:
|
||||
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))
|
||||
|
||||
if not self.flags.from_repost and not self.voucher_type == 'Period Closing Voucher' \
|
||||
and self.cost_center and _check_is_group():
|
||||
if (self.voucher_type != 'Period Closing Voucher' and is_group):
|
||||
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)))
|
||||
|
||||
@ -184,7 +175,6 @@ class GLEntry(Document):
|
||||
if not self.fiscal_year:
|
||||
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
|
||||
|
||||
|
||||
def validate_balance_type(account, adv_adj=False):
|
||||
if not adv_adj and account:
|
||||
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):
|
||||
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:
|
||||
frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,
|
||||
'frozen_accounts_modifier')
|
||||
|
@ -3,6 +3,7 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
@ -82,18 +83,37 @@ class PaymentRequest(Document):
|
||||
self.make_communication_entry()
|
||||
|
||||
elif self.payment_channel == "Phone":
|
||||
controller = get_payment_gateway_controller(self.payment_gateway)
|
||||
payment_record = dict(
|
||||
reference_doctype="Payment Request",
|
||||
reference_docname=self.name,
|
||||
payment_reference=self.reference_name,
|
||||
grand_total=self.grand_total,
|
||||
sender=self.email_to,
|
||||
currency=self.currency,
|
||||
payment_gateway=self.payment_gateway
|
||||
)
|
||||
controller.validate_transaction_currency(self.currency)
|
||||
controller.request_for_payment(**payment_record)
|
||||
self.request_phone_payment()
|
||||
|
||||
def request_phone_payment(self):
|
||||
controller = get_payment_gateway_controller(self.payment_gateway)
|
||||
request_amount = self.get_request_amount()
|
||||
|
||||
payment_record = dict(
|
||||
reference_doctype="Payment Request",
|
||||
reference_docname=self.name,
|
||||
payment_reference=self.reference_name,
|
||||
request_amount=request_amount,
|
||||
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):
|
||||
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:
|
||||
pr.flags.mute_email = True
|
||||
|
||||
pr.insert(ignore_permissions=True)
|
||||
if args.submit_doc:
|
||||
pr.insert(ignore_permissions=True)
|
||||
pr.submit()
|
||||
|
||||
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):
|
||||
"""return gateway and payment account of default payment gateway"""
|
||||
if args.get("payment_gateway"):
|
||||
return get_payment_gateway_account(args.get("payment_gateway"))
|
||||
if args.get("payment_gateway_account"):
|
||||
return get_payment_gateway_account(args.get("payment_gateway_account"))
|
||||
|
||||
if args.order_type == "Shopping Cart":
|
||||
payment_gateway_account = frappe.get_doc("Shopping Cart Settings").payment_gateway_account
|
||||
|
@ -45,7 +45,8 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
|
||||
def test_payment_request_linkings(self):
|
||||
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_name, so_inr.name)
|
||||
@ -54,7 +55,8 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
conversion_rate = get_exchange_rate("USD", "INR")
|
||||
|
||||
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_name, si_usd.name)
|
||||
@ -68,7 +70,7 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
|
||||
so_inr = make_sales_order(currency="INR")
|
||||
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()
|
||||
|
||||
so_inr = frappe.get_doc("Sales Order", so_inr.name)
|
||||
@ -79,7 +81,7 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
currency="USD", conversion_rate=50)
|
||||
|
||||
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()
|
||||
|
||||
@ -106,7 +108,7 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
currency="USD", conversion_rate=50)
|
||||
|
||||
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()
|
||||
pr.load_from_db()
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
frappe.ui.form.on('POS Closing Entry', {
|
||||
onload: function(frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log'];
|
||||
frm.set_query("pos_profile", function(doc) {
|
||||
return {
|
||||
filters: { 'user': doc.user }
|
||||
@ -20,7 +21,7 @@ frappe.ui.form.on('POS Closing Entry', {
|
||||
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);
|
||||
},
|
||||
|
||||
|
@ -11,6 +11,7 @@
|
||||
"column_break_3",
|
||||
"posting_date",
|
||||
"pos_opening_entry",
|
||||
"status",
|
||||
"section_break_5",
|
||||
"company",
|
||||
"column_break_7",
|
||||
@ -184,11 +185,27 @@
|
||||
"label": "POS Opening Entry",
|
||||
"options": "POS Opening Entry",
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2020-05-29 15:03:22.226113",
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "POS Invoice Merge Log",
|
||||
"link_fieldname": "pos_closing_entry"
|
||||
}
|
||||
],
|
||||
"modified": "2021-01-12 12:21:05.388650",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Closing Entry",
|
||||
|
@ -6,13 +6,12 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
import json
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import getdate, get_datetime, flt
|
||||
from collections import defaultdict
|
||||
from frappe.utils import get_datetime, flt
|
||||
from erpnext.controllers.status_updater import StatusUpdater
|
||||
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):
|
||||
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"))
|
||||
@ -64,17 +63,22 @@ class POSClosingEntry(Document):
|
||||
|
||||
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):
|
||||
currency = frappe.get_cached_value('Company', self.company, "default_currency")
|
||||
return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html",
|
||||
{"data": self, "currency": currency})
|
||||
|
||||
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.validate_and_sanitize_search_inputs
|
||||
|
@ -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];
|
||||
}
|
||||
};
|
@ -13,7 +13,6 @@ from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profi
|
||||
class TestPOSClosingEntry(unittest.TestCase):
|
||||
def test_pos_closing_entry(self):
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
|
||||
@ -45,6 +44,49 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
frappe.set_user("Administrator")
|
||||
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):
|
||||
user = 'test@example.com'
|
||||
test_user = frappe.get_doc('User', user)
|
||||
|
@ -2,6 +2,7 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
{% include 'erpnext/selling/sales_common.js' %};
|
||||
frappe.provide("erpnext.accounts");
|
||||
|
||||
erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend({
|
||||
setup(doc) {
|
||||
@ -9,12 +10,19 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend(
|
||||
this._super(doc);
|
||||
},
|
||||
|
||||
company: function() {
|
||||
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
||||
},
|
||||
|
||||
onload(doc) {
|
||||
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') {
|
||||
this.frm.script_manager.trigger("is_pos");
|
||||
this.frm.refresh_fields();
|
||||
}
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
||||
},
|
||||
|
||||
refresh(doc) {
|
||||
@ -187,18 +195,43 @@ frappe.ui.form.on('POS Invoice', {
|
||||
},
|
||||
|
||||
request_for_payment: function (frm) {
|
||||
if (!frm.doc.contact_mobile) {
|
||||
frappe.throw(__('Please enter mobile number first.'));
|
||||
}
|
||||
frm.dirty();
|
||||
frm.save().then(() => {
|
||||
frappe.dom.freeze();
|
||||
frappe.call({
|
||||
method: 'create_payment_request',
|
||||
doc: frm.doc,
|
||||
})
|
||||
frappe.dom.freeze(__('Waiting for payment...'));
|
||||
frappe
|
||||
.call({
|
||||
method: 'create_payment_request',
|
||||
doc: frm.doc
|
||||
})
|
||||
.fail(() => {
|
||||
frappe.dom.unfreeze();
|
||||
frappe.msgprint('Payment request failed');
|
||||
frappe.msgprint(__('Payment request failed'));
|
||||
})
|
||||
.then(() => {
|
||||
frappe.msgprint('Payment request sent successfully');
|
||||
.then(({ message }) => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -6,10 +6,9 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.accounts.party import get_party_account, get_due_date
|
||||
from 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.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
|
||||
@ -58,6 +57,22 @@ class POSInvoice(SalesInvoice):
|
||||
self.apply_loyalty_points()
|
||||
self.check_phone_payments()
|
||||
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):
|
||||
# run on cancel method of selling controller
|
||||
@ -78,7 +93,7 @@ class POSInvoice(SalesInvoice):
|
||||
mode_of_payment=pay.mode_of_payment, status="Paid"),
|
||||
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))
|
||||
|
||||
def validate_stock_availablility(self):
|
||||
@ -296,14 +311,21 @@ class POSInvoice(SalesInvoice):
|
||||
self.set(fieldname, profile.get(fieldname))
|
||||
|
||||
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')
|
||||
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:
|
||||
selling_price_list = profile.get('selling_price_list')
|
||||
|
||||
if 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
|
||||
for item in self.get("items"):
|
||||
@ -363,22 +385,48 @@ class POSInvoice(SalesInvoice):
|
||||
if not self.contact_mobile:
|
||||
frappe.throw(_("Please enter the phone number first"))
|
||||
|
||||
payment_gateway = frappe.db.get_value("Payment Gateway Account", {
|
||||
"payment_account": pay.account,
|
||||
})
|
||||
record = {
|
||||
"payment_gateway": payment_gateway,
|
||||
"dt": "POS Invoice",
|
||||
"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
|
||||
}
|
||||
pay_req = self.get_existing_payment_request(pay)
|
||||
if not pay_req:
|
||||
pay_req = self.get_new_payment_request(pay)
|
||||
pay_req.submit()
|
||||
else:
|
||||
pay_req.request_phone_payment()
|
||||
|
||||
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 append_payment(payment_mode):
|
||||
|
@ -290,7 +290,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
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_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`")
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
@ -306,7 +306,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
})
|
||||
pos_inv2.submit()
|
||||
|
||||
merge_pos_invoices()
|
||||
consolidate_pos_invoices()
|
||||
|
||||
pos_inv.load_from_db()
|
||||
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):
|
||||
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`")
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
@ -348,7 +348,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
})
|
||||
pos_inv2.submit()
|
||||
|
||||
merge_pos_invoices()
|
||||
consolidate_pos_invoices()
|
||||
|
||||
pos_inv.load_from_db()
|
||||
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):
|
||||
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"):
|
||||
frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 1)
|
||||
@ -393,7 +393,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
})
|
||||
pos_inv2.submit()
|
||||
|
||||
merge_pos_invoices()
|
||||
consolidate_pos_invoices()
|
||||
|
||||
pos_inv2.load_from_db()
|
||||
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
|
||||
|
@ -7,6 +7,8 @@
|
||||
"field_order": [
|
||||
"posting_date",
|
||||
"customer",
|
||||
"column_break_3",
|
||||
"pos_closing_entry",
|
||||
"section_break_3",
|
||||
"pos_invoices",
|
||||
"references_section",
|
||||
@ -76,11 +78,22 @@
|
||||
"label": "Consolidated Credit Note",
|
||||
"options": "Sales Invoice",
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2020-05-29 15:08:41.317100",
|
||||
"modified": "2020-12-01 11:53:57.267579",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Merge Log",
|
||||
|
@ -7,8 +7,11 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model import default_fields
|
||||
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.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
|
||||
|
||||
@ -61,7 +64,13 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
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):
|
||||
sales_invoice = self.get_new_sales_invoice()
|
||||
@ -111,6 +120,7 @@ class POSInvoiceMergeLog(Document):
|
||||
i.qty = i.qty + item.qty
|
||||
if not found:
|
||||
item.rate = item.net_rate
|
||||
item.price_list_rate = 0
|
||||
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
|
||||
items.append(si_item)
|
||||
|
||||
@ -148,6 +158,8 @@ class POSInvoiceMergeLog(Document):
|
||||
invoice.set('taxes', taxes)
|
||||
invoice.additional_discount_percentage = 0
|
||||
invoice.discount_amount = 0.0
|
||||
invoice.taxes_and_charges = None
|
||||
invoice.ignore_pricing_rule = 1
|
||||
|
||||
return invoice
|
||||
|
||||
@ -160,17 +172,21 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
return sales_invoice
|
||||
|
||||
def update_pos_invoices(self, sales_invoice, credit_note):
|
||||
for d in self.pos_invoices:
|
||||
doc = frappe.get_doc('POS Invoice', d.pos_invoice)
|
||||
if not doc.is_return:
|
||||
doc.update({'consolidated_invoice': sales_invoice})
|
||||
else:
|
||||
doc.update({'consolidated_invoice': credit_note})
|
||||
def update_pos_invoices(self, invoice_docs, sales_invoice='', credit_note=''):
|
||||
for doc in invoice_docs:
|
||||
doc.load_from_db()
|
||||
doc.update({ 'consolidated_invoice': None if self.docstatus==2 else (credit_note if doc.is_return else sales_invoice) })
|
||||
doc.set_status(update=True)
|
||||
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 = {
|
||||
'consolidated_invoice': [ 'in', [ '', None ]],
|
||||
'status': ['not in', ['Consolidated']],
|
||||
@ -181,7 +197,7 @@ def get_all_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 = {}
|
||||
for invoice in pos_invoices:
|
||||
@ -191,20 +207,82 @@ def get_invoices_customer_map(pos_invoices):
|
||||
|
||||
return pos_invoice_customer_map
|
||||
|
||||
def merge_pos_invoices(pos_invoices=[]):
|
||||
if not pos_invoices:
|
||||
pos_invoices = get_all_invoices()
|
||||
|
||||
pos_invoice_map = get_invoices_customer_map(pos_invoices)
|
||||
create_merge_logs(pos_invoice_map)
|
||||
def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
|
||||
invoices = pos_invoices or closing_entry.get('pos_transactions') or get_all_unconsolidated_invoices()
|
||||
invoice_by_customer = get_invoice_customer_map(invoices)
|
||||
|
||||
def create_merge_logs(pos_invoice_customer_map):
|
||||
for customer, invoices in iteritems(pos_invoice_customer_map):
|
||||
if len(invoices) >= 5 and closing_entry:
|
||||
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.posting_date = getdate(nowdate())
|
||||
merge_log.customer = customer
|
||||
merge_log.pos_closing_entry = closing_entry.get('name', None)
|
||||
|
||||
merge_log.set('pos_invoices', invoices)
|
||||
merge_log.save(ignore_permissions=True)
|
||||
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
|
@ -7,7 +7,7 @@ import frappe
|
||||
import unittest
|
||||
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
|
||||
from erpnext.accounts.doctype.pos_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
|
||||
|
||||
class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
@ -34,7 +34,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
})
|
||||
pos_inv3.submit()
|
||||
|
||||
merge_pos_invoices()
|
||||
consolidate_pos_invoices()
|
||||
|
||||
pos_inv.load_from_db()
|
||||
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.submit()
|
||||
|
||||
merge_pos_invoices()
|
||||
consolidate_pos_invoices()
|
||||
|
||||
pos_inv.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
|
||||
|
@ -6,7 +6,6 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, get_link_to_form
|
||||
from frappe.model.document import Document
|
||||
from erpnext.controllers.status_updater import StatusUpdater
|
||||
|
||||
class POSOpeningEntry(StatusUpdater):
|
||||
|
@ -5,7 +5,7 @@
|
||||
frappe.listview_settings['POS Opening Entry'] = {
|
||||
get_indicator: function(doc) {
|
||||
var status_color = {
|
||||
"Draft": "grey",
|
||||
"Draft": "red",
|
||||
"Open": "orange",
|
||||
"Closed": "green",
|
||||
"Cancelled": "red"
|
||||
|
@ -12,8 +12,6 @@
|
||||
"company",
|
||||
"country",
|
||||
"column_break_9",
|
||||
"update_stock",
|
||||
"ignore_pricing_rule",
|
||||
"warehouse",
|
||||
"campaign",
|
||||
"company_address",
|
||||
@ -25,8 +23,14 @@
|
||||
"hide_images",
|
||||
"hide_unavailable_items",
|
||||
"auto_add_item_to_cart",
|
||||
"item_groups",
|
||||
"column_break_16",
|
||||
"update_stock",
|
||||
"ignore_pricing_rule",
|
||||
"allow_rate_change",
|
||||
"allow_discount_change",
|
||||
"section_break_23",
|
||||
"item_groups",
|
||||
"column_break_25",
|
||||
"customer_groups",
|
||||
"section_break_16",
|
||||
"print_format",
|
||||
@ -309,6 +313,7 @@
|
||||
"default": "1",
|
||||
"fieldname": "update_stock",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Update Stock",
|
||||
"read_only": 1
|
||||
},
|
||||
@ -329,13 +334,34 @@
|
||||
"fieldname": "auto_add_item_to_cart",
|
||||
"fieldtype": "Check",
|
||||
"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",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-20 13:59:28.877572",
|
||||
"modified": "2021-01-06 14:42:41.713864",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
@ -40,6 +40,7 @@
|
||||
"base_rate",
|
||||
"base_amount",
|
||||
"pricing_rules",
|
||||
"stock_uom_rate",
|
||||
"is_free_item",
|
||||
"section_break_22",
|
||||
"net_rate",
|
||||
@ -783,6 +784,14 @@
|
||||
"print_hide": 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",
|
||||
"fieldtype": "Data",
|
||||
@ -795,7 +804,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-26 17:20:36.415791",
|
||||
"modified": "2021-01-30 21:43:21.488258",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
@ -20,6 +20,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
||||
var me = this;
|
||||
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) {
|
||||
// show debit_to in print format
|
||||
this.frm.set_df_property("debit_to", "print_hide", 0);
|
||||
|
@ -1987,8 +1987,15 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 181,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-25 22:57:32.555067",
|
||||
"links": [
|
||||
{
|
||||
"custom": 1,
|
||||
"group": "Reference",
|
||||
"link_doctype": "POS Invoice",
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2021-01-12 12:16:15.192520",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
@ -236,7 +236,25 @@ class SalesInvoice(SellingController):
|
||||
if len(self.payments) == 0 and self.is_pos:
|
||||
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):
|
||||
self.check_if_consolidated_invoice()
|
||||
|
||||
super(SalesInvoice, self).before_cancel()
|
||||
self.update_time_sheet(None)
|
||||
|
||||
@ -438,7 +456,9 @@ class SalesInvoice(SellingController):
|
||||
if not for_validate and not self.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'):
|
||||
self.account_for_change_amount = pos.get('account_for_change_amount')
|
||||
|
||||
|
@ -45,6 +45,7 @@
|
||||
"base_rate",
|
||||
"base_amount",
|
||||
"pricing_rules",
|
||||
"stock_uom_rate",
|
||||
"is_free_item",
|
||||
"section_break_21",
|
||||
"net_rate",
|
||||
@ -811,12 +812,20 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 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,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-26 17:25:04.090630",
|
||||
"modified": "2021-01-30 21:42:37.796771",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
@ -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}")
|
||||
.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:
|
||||
gl_map = merge_similar_entries(gl_map)
|
||||
gl_map = merge_similar_entries(gl_map, precision)
|
||||
for entry in gl_map:
|
||||
# toggle debit, credit if negative entry
|
||||
if flt(entry.debit) < 0:
|
||||
@ -69,7 +69,7 @@ def process_gl_map(gl_map, merge_entries=True):
|
||||
|
||||
return gl_map
|
||||
|
||||
def merge_similar_entries(gl_map):
|
||||
def merge_similar_entries(gl_map, precision=None):
|
||||
merged_gl_map = []
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
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_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
|
||||
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.flags.ignore_permissions = 1
|
||||
gle.flags.from_repost = from_repost
|
||||
gle.insert()
|
||||
gle.run_method("on_update_with_args", adv_adj, update_outstanding, from_repost)
|
||||
gle.flags.adv_adj = adv_adj
|
||||
gle.flags.update_outstanding = update_outstanding or 'Yes'
|
||||
gle.submit()
|
||||
|
||||
if not from_repost:
|
||||
|
@ -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,
|
||||
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)
|
||||
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
|
||||
|
||||
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], ... }"""
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
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:
|
||||
field = "Account number" if entry.account_number else "Account name"
|
||||
frappe.throw(_("{0} {1} is not present in the parent company").format(field, key))
|
||||
args = get_account_details(entry.account)
|
||||
|
||||
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):
|
||||
additional_conditions = []
|
||||
|
@ -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))
|
||||
if company:
|
||||
error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))
|
||||
|
||||
|
||||
if verbose==1: frappe.msgprint(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,
|
||||
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):
|
||||
frappe.db.sql("""delete from `tabGL Entry`
|
||||
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:
|
||||
warehouse_account = get_warehouse_account_map(company)
|
||||
|
||||
future_stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items)
|
||||
gle = get_voucherwise_gl_entries(future_stock_vouchers, posting_date)
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
|
||||
|
||||
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), [])
|
||||
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)
|
||||
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)
|
||||
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
|
||||
else:
|
||||
_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 = []
|
||||
|
||||
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)))
|
||||
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
|
||||
from `tabStock Ledger Entry` sle
|
||||
where
|
||||
@ -945,16 +954,17 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
|
||||
|
||||
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
|
||||
for entry in expected_gle:
|
||||
account_existed = False
|
||||
for e in existing_gle:
|
||||
if entry.account == e.account:
|
||||
account_existed = True
|
||||
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 (entry.debit != e.debit or entry.credit != e.credit):
|
||||
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 ( flt(entry.debit, precision) != flt(e.debit, precision) or
|
||||
flt(entry.credit, precision) != flt(e.credit, precision))):
|
||||
matched = False
|
||||
break
|
||||
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(
|
||||
stock_bal, account_bal, frappe.bold(account), posting_date)
|
||||
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(
|
||||
msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution),
|
||||
|
@ -40,6 +40,7 @@
|
||||
"base_rate",
|
||||
"base_amount",
|
||||
"pricing_rules",
|
||||
"stock_uom_rate",
|
||||
"is_free_item",
|
||||
"section_break_29",
|
||||
"net_rate",
|
||||
@ -726,13 +727,21 @@
|
||||
"fieldname": "more_info_section_break",
|
||||
"fieldtype": "Section Break",
|
||||
"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,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-07 11:59:47.670951",
|
||||
"modified": "2021-01-30 21:44:41.816974",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
14
erpnext/change_log/v13/v13_0_0-beta_12.md
Normal file
14
erpnext/change_log/v13/v13_0_0-beta_12.md
Normal 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))
|
@ -302,6 +302,7 @@ class AccountsController(TransactionBase):
|
||||
args["doctype"] = self.doctype
|
||||
args["name"] = self.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"):
|
||||
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.set_qty_as_per_stock_uom()
|
||||
parent.calculate_taxes_and_totals()
|
||||
parent.set_total_in_words()
|
||||
if parent_doctype == "Sales Order":
|
||||
make_packing_list(parent)
|
||||
parent.set_gross_profit()
|
||||
|
@ -456,9 +456,13 @@ class SellingController(StockController):
|
||||
check_list, chk_dupl_itm = [], []
|
||||
if cint(frappe.db.get_single_value("Selling Settings", "allow_multiple_items")):
|
||||
return
|
||||
if self.doctype == "Sales Invoice" and self.is_consolidated:
|
||||
return
|
||||
if self.doctype == "POS Invoice":
|
||||
return
|
||||
|
||||
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 '']
|
||||
non_stock_items = [d.item_code, d.description, d.sales_order or d.delivery_note]
|
||||
elif self.doctype == "Delivery Note":
|
||||
@ -469,13 +473,19 @@ class SellingController(StockController):
|
||||
non_stock_items = [d.item_code, d.description]
|
||||
|
||||
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:
|
||||
frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code))
|
||||
frappe.throw(duplicate_items_msg)
|
||||
else:
|
||||
check_list.append(stock_items)
|
||||
else:
|
||||
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:
|
||||
chk_dupl_itm.append(non_stock_items)
|
||||
|
||||
|
@ -93,6 +93,12 @@ status_map = {
|
||||
["Open", "eval:self.docstatus == 1 and not self.pos_closing_entry"],
|
||||
["Closed", "eval:self.docstatus == 1 and self.pos_closing_entry"],
|
||||
["Cancelled", "eval:self.docstatus == 2"],
|
||||
],
|
||||
"POS Closing Entry": [
|
||||
["Draft", None],
|
||||
["Submitted", "eval:self.docstatus == 1"],
|
||||
["Queued", "eval:self.status == 'Queued'"],
|
||||
["Cancelled", "eval:self.docstatus == 2"],
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ class StockController(AccountsController):
|
||||
self.validate_inspection()
|
||||
self.validate_serialized_batch()
|
||||
self.validate_customer_provided_item()
|
||||
self.set_rate_of_stock_uom()
|
||||
self.validate_internal_transfer()
|
||||
self.validate_putaway_capacity()
|
||||
|
||||
@ -73,7 +74,7 @@ class StockController(AccountsController):
|
||||
|
||||
gl_list = []
|
||||
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:
|
||||
|
||||
sle_list = sle_map.get(item_row.name)
|
||||
@ -130,7 +131,13 @@ class StockController(AccountsController):
|
||||
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))
|
||||
|
||||
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):
|
||||
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"))
|
||||
|
||||
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"
|
||||
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")
|
||||
@ -395,6 +402,11 @@ class StockController(AccountsController):
|
||||
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
|
||||
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):
|
||||
if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \
|
||||
and self.is_internal_transfer():
|
||||
@ -481,7 +493,6 @@ class StockController(AccountsController):
|
||||
"voucher_no": self.name,
|
||||
"company": self.company
|
||||
})
|
||||
|
||||
if check_if_future_sle_exists(args):
|
||||
create_repost_item_valuation_entry(args)
|
||||
elif not is_reposting_pending():
|
||||
|
@ -107,7 +107,7 @@ class calculate_taxes_and_totals(object):
|
||||
elif item.discount_amount and item.pricing_rules:
|
||||
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)
|
||||
if flt(item.rate_with_margin) > 0:
|
||||
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
|
||||
|
@ -176,7 +176,7 @@ class Lead(SellingController):
|
||||
"phone": self.mobile_no
|
||||
})
|
||||
|
||||
contact.insert()
|
||||
contact.insert(ignore_permissions=True)
|
||||
|
||||
return contact
|
||||
|
||||
|
@ -5,7 +5,7 @@ import datetime
|
||||
|
||||
class MpesaConnector():
|
||||
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."""
|
||||
self.env = env
|
||||
self.app_key = app_key
|
||||
@ -102,14 +102,14 @@ class MpesaConnector():
|
||||
"BusinessShortCode": business_shortcode,
|
||||
"Password": encoded.decode("utf-8"),
|
||||
"Timestamp": time,
|
||||
"TransactionType": "CustomerPayBillOnline",
|
||||
"Amount": amount,
|
||||
"PartyA": int(phone_number),
|
||||
"PartyB": business_shortcode,
|
||||
"PartyB": reference_code,
|
||||
"PhoneNumber": int(phone_number),
|
||||
"CallBackURL": callback_url,
|
||||
"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"}
|
||||
|
||||
|
@ -11,8 +11,10 @@
|
||||
"consumer_secret",
|
||||
"initiator_name",
|
||||
"till_number",
|
||||
"transaction_limit",
|
||||
"sandbox",
|
||||
"column_break_4",
|
||||
"business_shortcode",
|
||||
"online_passkey",
|
||||
"security_credential",
|
||||
"get_account_balance",
|
||||
@ -84,10 +86,24 @@
|
||||
"fieldname": "get_account_balance",
|
||||
"fieldtype": "Button",
|
||||
"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": [],
|
||||
"modified": "2020-09-25 20:21:38.215494",
|
||||
"modified": "2021-01-29 12:02:16.106942",
|
||||
"modified_by": "Administrator",
|
||||
"module": "ERPNext Integrations",
|
||||
"name": "Mpesa Settings",
|
||||
|
@ -33,13 +33,34 @@ class MpesaSettings(Document):
|
||||
create_mode_of_payment('Mpesa-' + self.payment_gateway_name, payment_type="Phone")
|
||||
|
||||
def request_for_payment(self, **kwargs):
|
||||
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())
|
||||
else:
|
||||
response = frappe._dict(generate_stk_push(**kwargs))
|
||||
args = frappe._dict(kwargs)
|
||||
request_amounts = self.split_request_amount_according_to_transaction_limit(args)
|
||||
|
||||
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):
|
||||
payload = dict(
|
||||
@ -67,7 +88,8 @@ class MpesaSettings(Document):
|
||||
req_name = getattr(response, global_id)
|
||||
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:
|
||||
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:])
|
||||
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,
|
||||
app_key=mpesa_settings.consumer_key,
|
||||
@ -87,10 +111,12 @@ def generate_stk_push(**kwargs):
|
||||
|
||||
mobile_number = sanitize_mobile_number(args.sender)
|
||||
|
||||
response = connector.stk_push(business_shortcode=mpesa_settings.till_number,
|
||||
passcode=mpesa_settings.get_password("online_passkey"), amount=args.grand_total,
|
||||
response = connector.stk_push(
|
||||
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,
|
||||
phone_number=mobile_number, description="POS Payment")
|
||||
phone_number=mobile_number, description="POS Payment"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
@ -108,29 +134,72 @@ def verify_transaction(**kwargs):
|
||||
transaction_response = frappe._dict(kwargs["Body"]["stkCallback"])
|
||||
|
||||
checkout_id = getattr(transaction_response, "CheckoutRequestID", "")
|
||||
request = frappe.get_doc("Integration Request", checkout_id)
|
||||
transaction_data = frappe._dict(loads(request.data))
|
||||
integration_request = frappe.get_doc("Integration Request", checkout_id)
|
||||
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 request.reference_doctype and request.reference_docname:
|
||||
if integration_request.reference_doctype and integration_request.reference_docname:
|
||||
try:
|
||||
doc = frappe.get_doc(request.reference_doctype,
|
||||
request.reference_docname)
|
||||
doc.run_method("on_payment_authorized", 'Completed')
|
||||
|
||||
item_response = transaction_response["CallbackMetadata"]["Item"]
|
||||
amount = fetch_param_value(item_response, "Amount", "Name")
|
||||
mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
|
||||
frappe.db.set_value("POS Invoice", doc.reference_name, "mpesa_receipt_number", mpesa_receipt)
|
||||
request.handle_success(transaction_response)
|
||||
pr = frappe.get_doc(integration_request.reference_doctype, integration_request.reference_docname)
|
||||
|
||||
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:
|
||||
request.handle_failure(transaction_response)
|
||||
integration_request.handle_failure(transaction_response)
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
|
||||
else:
|
||||
request.handle_failure(transaction_response)
|
||||
integration_request.handle_failure(transaction_response)
|
||||
|
||||
frappe.publish_realtime('process_phone_payment', doctype="POS Invoice",
|
||||
docname=transaction_data.payment_reference, user=request.owner, message=transaction_response)
|
||||
frappe.publish_realtime(
|
||||
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):
|
||||
"""Call account balance API to send the request to the Mpesa Servers."""
|
||||
|
@ -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
|
||||
|
||||
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):
|
||||
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):
|
||||
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("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': 500})
|
||||
@ -55,10 +62,16 @@ class TestMpesaSettings(unittest.TestCase):
|
||||
# test payment request creation
|
||||
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)
|
||||
# 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
|
||||
self.assertTrue(integration_request)
|
||||
@ -68,6 +81,122 @@ class TestMpesaSettings(unittest.TestCase):
|
||||
integration_request.reload()
|
||||
self.assertEquals(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R")
|
||||
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"):
|
||||
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."""
|
||||
|
||||
CheckoutRequestID = frappe.utils.random_string(10)
|
||||
|
||||
return {
|
||||
"MerchantRequestID": "8071-27184008-1",
|
||||
"CheckoutRequestID": "ws_CO_061020201133231972",
|
||||
"CheckoutRequestID": CheckoutRequestID,
|
||||
"ResultCode": 0,
|
||||
"ResultDesc": "The service request is processed successfully.",
|
||||
"CallbackMetadata": {
|
||||
"Item": [
|
||||
{ "Name": "Amount", "Value": 500.0 },
|
||||
{ "Name": "Amount", "Value": Amount },
|
||||
{ "Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R" },
|
||||
{ "Name": "TransactionDate", "Value": 20201006113336 },
|
||||
{ "Name": "PhoneNumber", "Value": 254723575670 }
|
||||
@ -174,41 +306,26 @@ def get_payment_request_response_payload():
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_payment_callback_payload():
|
||||
def get_payment_callback_payload(Amount=500, CheckoutRequestID="ws_CO_061020201133231972", MpesaReceiptNumber="LGR7OWQX0R"):
|
||||
"""Response received from the server as callback after calling the stkpush process request API."""
|
||||
return {
|
||||
"Body":{
|
||||
"stkCallback":{
|
||||
"MerchantRequestID":"19465-780693-1",
|
||||
"CheckoutRequestID":"ws_CO_061020201133231972",
|
||||
"ResultCode":0,
|
||||
"ResultDesc":"The service request is processed successfully.",
|
||||
"CallbackMetadata":{
|
||||
"Item":[
|
||||
{
|
||||
"Name":"Amount",
|
||||
"Value":500
|
||||
},
|
||||
{
|
||||
"Name":"MpesaReceiptNumber",
|
||||
"Value":"LGR7OWQX0R"
|
||||
},
|
||||
{
|
||||
"Name":"Balance"
|
||||
},
|
||||
{
|
||||
"Name":"TransactionDate",
|
||||
"Value":20170727154800
|
||||
},
|
||||
{
|
||||
"Name":"PhoneNumber",
|
||||
"Value":254721566839
|
||||
"stkCallback":{
|
||||
"MerchantRequestID":"19465-780693-1",
|
||||
"CheckoutRequestID":CheckoutRequestID,
|
||||
"ResultCode":0,
|
||||
"ResultDesc":"The service request is processed successfully.",
|
||||
"CallbackMetadata":{
|
||||
"Item":[
|
||||
{ "Name":"Amount", "Value":Amount },
|
||||
{ "Name":"MpesaReceiptNumber", "Value":MpesaReceiptNumber },
|
||||
{ "Name":"Balance" },
|
||||
{ "Name":"TransactionDate", "Value":20170727154800 },
|
||||
{ "Name":"PhoneNumber", "Value":254721566839 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def get_account_balance_callback_payload():
|
||||
|
@ -2,4 +2,82 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
@ -12,7 +12,10 @@
|
||||
"appointment_type",
|
||||
"ip",
|
||||
"default_duration",
|
||||
"color"
|
||||
"color",
|
||||
"billing_section",
|
||||
"price_list",
|
||||
"items"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -52,10 +55,27 @@
|
||||
"label": "Color",
|
||||
"no_copy": 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": [],
|
||||
"modified": "2020-02-03 21:06:05.833050",
|
||||
"modified": "2021-01-22 09:41:05.010524",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Healthcare",
|
||||
"name": "Appointment Type",
|
||||
|
@ -4,6 +4,53 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from frappe.model.document import Document
|
||||
import frappe
|
||||
|
||||
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)
|
||||
|
@ -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
|
||||
}
|
@ -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
|
@ -121,6 +121,7 @@ class ClinicalProcedure(Document):
|
||||
|
||||
stock_entry.stock_entry_type = 'Material Receipt'
|
||||
stock_entry.to_warehouse = self.warehouse
|
||||
stock_entry.company = self.company
|
||||
expense_account = get_account(None, 'expense_account', 'Healthcare Settings', self.company)
|
||||
for item in self.items:
|
||||
if item.qty > item.actual_qty:
|
||||
|
@ -1,4 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017, ESS LLP and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
@ -60,6 +60,7 @@ def create_procedure(procedure_template, patient, practitioner):
|
||||
procedure.practitioner = practitioner
|
||||
procedure.consume_stock = procedure_template.allow_stock_consumption
|
||||
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()
|
||||
return procedure
|
@ -159,6 +159,7 @@
|
||||
"fieldname": "op_consulting_charge",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Out Patient Consulting Charge",
|
||||
"mandatory_depends_on": "op_consulting_charge_item",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
@ -174,7 +175,8 @@
|
||||
{
|
||||
"fieldname": "inpatient_visit_charge",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Inpatient Visit Charge"
|
||||
"label": "Inpatient Visit Charge",
|
||||
"mandatory_depends_on": "inpatient_visit_charge_item"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: !doc.__islocal",
|
||||
@ -280,7 +282,7 @@
|
||||
],
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2020-04-06 13:44:24.759623",
|
||||
"modified": "2021-01-22 10:14:43.187675",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Healthcare",
|
||||
"name": "Healthcare Practitioner",
|
||||
|
@ -139,6 +139,7 @@ def create_inpatient(patient):
|
||||
inpatient_record.phone = patient_obj.phone
|
||||
inpatient_record.inpatient = "Scheduled"
|
||||
inpatient_record.scheduled_date = today()
|
||||
inpatient_record.company = "_Test Company"
|
||||
return inpatient_record
|
||||
|
||||
|
||||
|
@ -24,11 +24,13 @@ frappe.ui.form.on('Patient Appointment', {
|
||||
});
|
||||
|
||||
frm.set_query('practitioner', function() {
|
||||
return {
|
||||
filters: {
|
||||
'department': frm.doc.department
|
||||
}
|
||||
};
|
||||
if (frm.doc.department) {
|
||||
return {
|
||||
filters: {
|
||||
'department': frm.doc.department
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
frm.set_query('service_unit', function() {
|
||||
@ -140,6 +142,20 @@ frappe.ui.form.on('Patient Appointment', {
|
||||
patient: function(frm) {
|
||||
if (frm.doc.patient) {
|
||||
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 {
|
||||
frm.set_value('patient_name', '');
|
||||
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) {
|
||||
frm.trigger('set_therapy_type_filter');
|
||||
},
|
||||
@ -190,14 +237,18 @@ frappe.ui.form.on('Patient Appointment', {
|
||||
// show payment fields as non-mandatory
|
||||
frm.toggle_display('mode_of_payment', 0);
|
||||
frm.toggle_display('paid_amount', 0);
|
||||
frm.toggle_display('billing_item', 0);
|
||||
frm.toggle_reqd('mode_of_payment', 0);
|
||||
frm.toggle_reqd('paid_amount', 0);
|
||||
frm.toggle_reqd('billing_item', 0);
|
||||
} else {
|
||||
// if automated appointment invoicing is disabled, hide fields
|
||||
frm.toggle_display('mode_of_payment', 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('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 ageMS = Date.parse(Date()) - Date.parse(birth);
|
||||
let age = new Date();
|
||||
|
@ -19,19 +19,19 @@
|
||||
"inpatient_record",
|
||||
"column_break_1",
|
||||
"company",
|
||||
"practitioner",
|
||||
"practitioner_name",
|
||||
"department",
|
||||
"service_unit",
|
||||
"section_break_12",
|
||||
"appointment_type",
|
||||
"duration",
|
||||
"procedure_template",
|
||||
"get_procedure_from_encounter",
|
||||
"procedure_prescription",
|
||||
"therapy_plan",
|
||||
"therapy_type",
|
||||
"get_prescribed_therapies",
|
||||
"practitioner",
|
||||
"practitioner_name",
|
||||
"department",
|
||||
"section_break_12",
|
||||
"appointment_type",
|
||||
"duration",
|
||||
"column_break_17",
|
||||
"appointment_date",
|
||||
"appointment_time",
|
||||
@ -79,6 +79,7 @@
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "appointment_type.default_duration",
|
||||
"fieldname": "duration",
|
||||
"fieldtype": "Int",
|
||||
"in_filter": 1,
|
||||
@ -144,7 +145,6 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Healthcare Practitioner",
|
||||
"options": "Healthcare Practitioner",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"search_index": 1,
|
||||
"set_only_once": 1
|
||||
@ -158,7 +158,6 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Department",
|
||||
"options": "Medical Department",
|
||||
"read_only": 1,
|
||||
"search_index": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
@ -227,12 +226,14 @@
|
||||
"fieldname": "mode_of_payment",
|
||||
"fieldtype": "Link",
|
||||
"label": "Mode of Payment",
|
||||
"options": "Mode of Payment"
|
||||
"options": "Mode of Payment",
|
||||
"read_only_depends_on": "invoiced"
|
||||
},
|
||||
{
|
||||
"fieldname": "paid_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Paid Amount"
|
||||
"label": "Paid Amount",
|
||||
"read_only_depends_on": "invoiced"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
@ -302,7 +303,8 @@
|
||||
"fieldname": "therapy_plan",
|
||||
"fieldtype": "Link",
|
||||
"label": "Therapy Plan",
|
||||
"options": "Therapy Plan"
|
||||
"options": "Therapy Plan",
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "ref_sales_invoice",
|
||||
@ -347,7 +349,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-12-16 13:16:58.578503",
|
||||
"modified": "2021-02-08 13:13:15.116833",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Healthcare",
|
||||
"name": "Patient Appointment",
|
||||
|
@ -26,6 +26,7 @@ class PatientAppointment(Document):
|
||||
|
||||
def after_insert(self):
|
||||
self.update_prescription_details()
|
||||
self.set_payment_details()
|
||||
invoice_appointment(self)
|
||||
self.update_fee_validity()
|
||||
send_confirmation_msg(self)
|
||||
@ -85,6 +86,13 @@ class PatientAppointment(Document):
|
||||
def set_appointment_datetime(self):
|
||||
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):
|
||||
if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'):
|
||||
if not frappe.db.get_value('Patient', self.patient, 'customer'):
|
||||
@ -148,31 +156,37 @@ def invoice_appointment(appointment_doc):
|
||||
fee_validity = None
|
||||
|
||||
if automate_invoicing and not appointment_invoiced and not fee_validity:
|
||||
sales_invoice = frappe.new_doc('Sales Invoice')
|
||||
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)
|
||||
create_sales_invoice(appointment_doc)
|
||||
|
||||
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
|
||||
if appointment_doc.mode_of_payment and appointment_doc.paid_amount:
|
||||
sales_invoice.is_pos = 1
|
||||
payment = sales_invoice.append('payments', {})
|
||||
payment.mode_of_payment = appointment_doc.mode_of_payment
|
||||
payment.amount = appointment_doc.paid_amount
|
||||
def create_sales_invoice(appointment_doc):
|
||||
sales_invoice = frappe.new_doc('Sales Invoice')
|
||||
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)
|
||||
|
||||
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)
|
||||
frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name)
|
||||
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
|
||||
if appointment_doc.mode_of_payment and appointment_doc.paid_amount:
|
||||
sales_invoice.is_pos = 1
|
||||
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):
|
||||
@ -187,13 +201,14 @@ def check_is_new_patient(patient, name=None):
|
||||
|
||||
|
||||
def get_appointment_item(appointment_doc, item):
|
||||
service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment_doc)
|
||||
item.item_code = service_item
|
||||
details = get_service_item_and_practitioner_charge(appointment_doc)
|
||||
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.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.rate = practitioner_charge
|
||||
item.amount = practitioner_charge
|
||||
item.rate = charge
|
||||
item.amount = charge
|
||||
item.qty = 1
|
||||
item.reference_dt = 'Patient Appointment'
|
||||
item.reference_dn = appointment_doc.name
|
||||
|
@ -32,7 +32,8 @@ class TestPatientAppointment(unittest.TestCase):
|
||||
patient, medical_department, practitioner = create_healthcare_docs()
|
||||
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 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)
|
||||
self.assertTrue(encounter)
|
||||
self.assertEqual(encounter.company, appointment.company)
|
||||
@ -41,7 +42,7 @@ class TestPatientAppointment(unittest.TestCase):
|
||||
# invoiced flag mapped from appointment
|
||||
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()
|
||||
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 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, '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):
|
||||
patient, medical_department, practitioner = create_healthcare_docs()
|
||||
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1)
|
||||
@ -178,14 +223,15 @@ def create_encounter(appointment):
|
||||
encounter.submit()
|
||||
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()
|
||||
frappe.db.set_value('Healthcare Settings', None, 'inpatient_visit_charge_item', item)
|
||||
frappe.db.set_value('Healthcare Settings', None, 'op_consulting_charge_item', item)
|
||||
appointment = frappe.new_doc('Patient Appointment')
|
||||
appointment.patient = patient
|
||||
appointment.practitioner = practitioner
|
||||
appointment.department = '_Test Medical Department'
|
||||
appointment.department = department or '_Test Medical Department'
|
||||
appointment.appointment_date = appointment_date
|
||||
appointment.company = '_Test Company'
|
||||
appointment.duration = 15
|
||||
@ -193,7 +239,8 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce
|
||||
appointment.service_unit = service_unit
|
||||
if invoice:
|
||||
appointment.mode_of_payment = 'Cash'
|
||||
appointment.paid_amount = 500
|
||||
if appointment_type:
|
||||
appointment.appointment_type = appointment_type
|
||||
if procedure_template:
|
||||
appointment.procedure_template = create_clinical_procedure_template().get('name')
|
||||
if save:
|
||||
@ -223,4 +270,29 @@ def create_clinical_procedure_template():
|
||||
template.description = 'Knee Surgery and Rehab'
|
||||
template.rate = 50000
|
||||
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()
|
@ -5,9 +5,11 @@
|
||||
from __future__ import unicode_literals
|
||||
import math
|
||||
import frappe
|
||||
import json
|
||||
from frappe import _
|
||||
from frappe.utils.formatters import format_value
|
||||
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.fee_validity.fee_validity import create_fee_validity
|
||||
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
|
||||
service_item = None
|
||||
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)
|
||||
appointments_to_invoice.append({
|
||||
'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'):
|
||||
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)
|
||||
|
||||
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 \
|
||||
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:
|
||||
msg = _('Please Configure Clinical Procedure Consumable Item in ')
|
||||
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
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
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
|
||||
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:
|
||||
service_item = get_healthcare_service_item('inpatient_visit_charge_item')
|
||||
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')
|
||||
service_item = get_healthcare_service_item(is_inpatient)
|
||||
|
||||
if not service_item:
|
||||
throw_config_service_item(is_inpatient)
|
||||
|
||||
practitioner_charge = get_practitioner_charge(doc.practitioner, is_inpatient)
|
||||
if not practitioner_charge:
|
||||
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
|
||||
|
||||
|
||||
@ -345,12 +377,27 @@ def throw_config_practitioner_charge(is_inpatient, practitioner):
|
||||
frappe.throw(msg, title=_('Missing Configuration'))
|
||||
|
||||
|
||||
def get_practitioner_service_item(practitioner, service_item_field):
|
||||
return frappe.db.get_value('Healthcare Practitioner', practitioner, service_item_field)
|
||||
def get_practitioner_service_item(practitioner, is_inpatient):
|
||||
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):
|
||||
return frappe.db.get_single_value('Healthcare Settings', service_item_field)
|
||||
def get_healthcare_service_item(is_inpatient):
|
||||
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):
|
||||
@ -381,7 +428,8 @@ def set_invoiced(item, method, ref_invoice=None):
|
||||
invoiced = True
|
||||
|
||||
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)
|
||||
else:
|
||||
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):
|
||||
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')
|
||||
else:
|
||||
is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'invoiced')
|
||||
|
@ -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.date_of_birth = "1992" + frappe.utils.nowdate()[4:]
|
||||
employee.company_email = "test@example.com"
|
||||
employee.company = "_Test Company"
|
||||
employee.save()
|
||||
|
||||
from erpnext.hr.doctype.employee.employee import get_employees_who_are_born_today, send_birthday_reminders
|
||||
|
@ -94,11 +94,11 @@ class TestWorkOrder(unittest.TestCase):
|
||||
wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2,
|
||||
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
|
||||
self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2,
|
||||
cint(bin1_on_submit.reserved_qty_for_production))
|
||||
self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2, reserved_qty_on_submission)
|
||||
|
||||
|
||||
test_stock_entry.make_stock_entry(item_code="_Test Item",
|
||||
target=warehouse, qty=100, basic_rate=100)
|
||||
@ -109,9 +109,9 @@ class TestWorkOrder(unittest.TestCase):
|
||||
s.submit()
|
||||
|
||||
bin1_at_completion = get_bin(item, warehouse)
|
||||
|
||||
|
||||
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):
|
||||
wo_order = make_wo_order_test_record(item="_Test FG Item", qty=1, do_not_save=True)
|
||||
|
@ -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.update_appointment_reminder_scheduler_entry
|
||||
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.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive
|
||||
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.updates_for_multi_currency_payroll
|
||||
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.update_returned_qty_in_pr_dn
|
||||
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.setup_patient_history_settings_for_standard_doctypes
|
||||
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
|
||||
|
16
erpnext/patches/v12_0/add_state_code_for_ladakh.py
Normal file
16
erpnext/patches/v12_0/add_state_code_for_ladakh.py
Normal 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()
|
@ -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
|
@ -6,12 +6,10 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
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")
|
||||
|
||||
for pos_profile in pos_profiles:
|
||||
if not pos_profile.get("payments"): return
|
||||
|
||||
payments = frappe.db.sql("""
|
||||
select idx, parentfield, parenttype, parent, mode_of_payment, `default` from `tabSales Invoice Payment` where parent=%s
|
||||
""", pos_profile.name, as_dict=1)
|
||||
|
@ -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''')
|
@ -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', '')
|
@ -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"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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,
|
||||
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
|
||||
for data in frappe.get_all('Salary Component', fields = ["name"]):
|
||||
|
@ -1103,10 +1103,10 @@ class SalarySlip(TransactionBase):
|
||||
self.calculate_total_for_salary_slip_based_on_timesheet()
|
||||
else:
|
||||
self.total_deduction = 0.0
|
||||
if self.earnings:
|
||||
if hasattr(self, "earnings"):
|
||||
for earning in self.earnings:
|
||||
self.gross_pay += flt(earning.amount, earning.precision("amount"))
|
||||
if self.deductions:
|
||||
if hasattr(self, "deductions"):
|
||||
for deduction in self.deductions:
|
||||
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)
|
||||
|
@ -191,7 +191,6 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
|
||||
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);
|
||||
}
|
||||
|
||||
this._super(doc, cdt, cdn);
|
||||
},
|
||||
|
||||
|
@ -40,7 +40,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
|
||||
cur_frm.cscript.set_gross_profit(item);
|
||||
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) {
|
||||
@ -1141,6 +1142,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
frappe.meta.has_field(doc.doctype, "price_list_currency")) {
|
||||
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) {
|
||||
let item = frappe.get_doc(cdt, cdn);
|
||||
this.conversion_factor(doc, cdt, cdn, true);
|
||||
this.calculate_stock_uom_rate(doc, cdt, cdn);
|
||||
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) {
|
||||
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"],
|
||||
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");
|
||||
|
||||
if(this.frm.fields_dict["operations"]) {
|
||||
|
@ -20,6 +20,7 @@ states = [
|
||||
'Jharkhand',
|
||||
'Karnataka',
|
||||
'Kerala',
|
||||
'Ladakh',
|
||||
'Lakshadweep Islands',
|
||||
'Madhya Pradesh',
|
||||
'Maharashtra',
|
||||
@ -59,6 +60,7 @@ state_numbers = {
|
||||
"Jharkhand": "20",
|
||||
"Karnataka": "29",
|
||||
"Kerala": "32",
|
||||
"Ladakh": "38",
|
||||
"Lakshadweep Islands": "31",
|
||||
"Madhya Pradesh": "23",
|
||||
"Maharashtra": "27",
|
||||
@ -80,4 +82,4 @@ state_numbers = {
|
||||
"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)}
|
||||
|
@ -188,7 +188,6 @@ const get_ewaybill_fields = (frm) => {
|
||||
'fieldname': 'vehicle_no',
|
||||
'label': 'Vehicle No',
|
||||
'fieldtype': 'Data',
|
||||
'depends_on': 'eval:(doc.mode_of_transport === "Road")',
|
||||
'default': frm.doc.vehicle_no
|
||||
},
|
||||
{
|
||||
|
@ -20,11 +20,13 @@ from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds
|
||||
|
||||
def validate_einvoice_fields(doc):
|
||||
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']
|
||||
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
|
||||
|
||||
if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction: return
|
||||
no_taxes_applied = not doc.get('taxes')
|
||||
|
||||
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.irn:
|
||||
@ -35,7 +37,7 @@ def validate_einvoice_fields(doc):
|
||||
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'))
|
||||
|
||||
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'))
|
||||
|
||||
def raise_document_name_too_long_error():
|
||||
@ -158,10 +160,10 @@ def get_item_list(invoice):
|
||||
item.update(d.as_dict())
|
||||
|
||||
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.discount_amount = abs(item.discount_amount * item.qty)
|
||||
item.discount_amount = 0
|
||||
item.unit_rate = abs(item.base_net_amount / item.qty)
|
||||
item.gross_amount = 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:
|
||||
invoice_value_details.base_total = abs(invoice.base_total)
|
||||
invoice_value_details.invoice_discount_amt = invoice.base_discount_amount
|
||||
else:
|
||||
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.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)
|
||||
@ -302,7 +305,7 @@ def validate_mandatory_fields(invoice):
|
||||
_('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
|
||||
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(
|
||||
_('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'),
|
||||
title=_('Missing Fields')
|
||||
@ -443,6 +446,8 @@ class GSPConnector():
|
||||
def get_credentials(self):
|
||||
if self.invoice:
|
||||
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)
|
||||
else:
|
||||
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()
|
||||
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
|
||||
gsp_connector = GSPConnector(doctype, docname)
|
||||
gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
||||
gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
||||
|
@ -168,5 +168,10 @@
|
||||
"state_number": "37",
|
||||
"state_code": "AD",
|
||||
"state_name": "Andhra Pradesh (New)"
|
||||
},
|
||||
{
|
||||
"state_number": "38",
|
||||
"state_code": "LA",
|
||||
"state_name": "Ladakh"
|
||||
}
|
||||
]
|
||||
|
@ -236,6 +236,7 @@ class Gstr1Report(object):
|
||||
self.cgst_sgst_invoices = []
|
||||
|
||||
unidentified_gst_accounts = []
|
||||
unidentified_gst_accounts_invoice = []
|
||||
for parent, account, item_wise_tax_detail, tax_amount in self.tax_details:
|
||||
if account in self.gst_accounts.cess_account:
|
||||
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 "gst" in account.lower() and account not in unidentified_gst_accounts:
|
||||
unidentified_gst_accounts.append(account)
|
||||
unidentified_gst_accounts_invoice.append(parent)
|
||||
continue
|
||||
|
||||
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
|
||||
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":
|
||||
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
|
||||
|
||||
|
@ -110,9 +110,11 @@ def make_custom_fields():
|
||||
'Purchase Order': purchase_invoice_fields + invoice_fields,
|
||||
'Purchase Receipt': purchase_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,
|
||||
'Delivery Note': sales_invoice_fields + invoice_fields,
|
||||
'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,
|
||||
'Sales Order Item': invoice_item_fields,
|
||||
'Delivery Note Item': invoice_item_fields,
|
||||
|
@ -47,6 +47,7 @@
|
||||
"base_amount",
|
||||
"base_net_amount",
|
||||
"pricing_rules",
|
||||
"stock_uom_rate",
|
||||
"is_free_item",
|
||||
"section_break_43",
|
||||
"valuation_rate",
|
||||
@ -634,12 +635,20 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 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,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-19 20:48:43.222229",
|
||||
"modified": "2021-01-30 21:39:40.174551",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation Item",
|
||||
|
@ -180,6 +180,7 @@ class SalesOrder(SellingController):
|
||||
update_coupon_code_count(self.coupon_code,'used')
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
|
||||
super(SalesOrder, self).on_cancel()
|
||||
|
||||
# Cannot cancel closed SO
|
||||
|
@ -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
|
||||
|
||||
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):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@ -325,6 +337,9 @@ class TestSalesOrder(unittest.TestCase):
|
||||
create_dn_against_so(so.name, 4)
|
||||
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]
|
||||
trans_item = json.dumps([
|
||||
{'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.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):
|
||||
so = make_sales_order(**{
|
||||
"item_list": [{
|
||||
@ -1040,6 +1061,38 @@ class TestSalesOrder(unittest.TestCase):
|
||||
|
||||
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):
|
||||
item = make_item("_Test Finished Item", {"is_stock_item": 1,
|
||||
"maintain_stock": 1,
|
||||
@ -1198,4 +1251,4 @@ def make_sales_order_workflow():
|
||||
))
|
||||
workflow.insert(ignore_permissions=True)
|
||||
|
||||
return workflow
|
||||
return workflow
|
||||
|
@ -46,6 +46,7 @@
|
||||
"base_rate",
|
||||
"base_amount",
|
||||
"pricing_rules",
|
||||
"stock_uom_rate",
|
||||
"is_free_item",
|
||||
"section_break_24",
|
||||
"net_rate",
|
||||
@ -214,7 +215,6 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "UOM",
|
||||
"options": "UOM",
|
||||
"print_hide": 0,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@ -780,12 +780,20 @@
|
||||
"fieldname": "manufacturing_section_section",
|
||||
"fieldtype": "Section Break",
|
||||
"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,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-012-07 20:54:32.309460",
|
||||
"modified": "2021-01-30 21:35:07.617320",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Item",
|
||||
|
@ -69,6 +69,10 @@ erpnext.PointOfSale.Controller = class {
|
||||
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({
|
||||
title: __('Create POS Opening Entry'),
|
||||
static: true,
|
||||
@ -80,6 +84,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
{
|
||||
fieldtype: 'Link', label: __('POS Profile'),
|
||||
options: 'POS Profile', fieldname: 'pos_profile', reqd: 1,
|
||||
get_query: () => pos_profile_query,
|
||||
onchange: () => fetch_pos_payment_methods()
|
||||
},
|
||||
{
|
||||
@ -124,9 +129,8 @@ erpnext.PointOfSale.Controller = class {
|
||||
});
|
||||
|
||||
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.hide_images = profile.hide_images;
|
||||
this.settings.auto_add_item_to_cart = profile.auto_add_item_to_cart;
|
||||
this.make_app();
|
||||
});
|
||||
}
|
||||
@ -255,11 +259,9 @@ erpnext.PointOfSale.Controller = class {
|
||||
get_frm: () => this.frm,
|
||||
|
||||
cart_item_clicked: (item_code, batch_no, uom) => {
|
||||
const item_row = this.frm.doc.items.find(
|
||||
i => i.item_code === item_code
|
||||
&& i.uom === uom
|
||||
&& (!batch_no || (batch_no && i.batch_no === batch_no))
|
||||
);
|
||||
const search_field = batch_no ? 'batch_no' : 'item_code';
|
||||
const search_value = batch_no || item_code;
|
||||
const item_row = this.frm.doc.items.find(i => i[search_field] === search_value && i.uom === uom);
|
||||
this.item_details.toggle_item_details_section(item_row);
|
||||
},
|
||||
|
||||
@ -281,6 +283,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
init_item_details() {
|
||||
this.item_details = new erpnext.PointOfSale.ItemDetails({
|
||||
wrapper: this.$components_wrapper,
|
||||
settings: this.settings,
|
||||
events: {
|
||||
get_frm: () => this.frm,
|
||||
|
||||
@ -415,6 +418,11 @@ erpnext.PointOfSale.Controller = class {
|
||||
() => 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: () => {
|
||||
frappe.run_serially([
|
||||
() => frappe.dom.freeze(),
|
||||
@ -696,14 +704,14 @@ erpnext.PointOfSale.Controller = class {
|
||||
frappe.dom.freeze();
|
||||
const { doctype, name, current_item } = this.item_details;
|
||||
|
||||
frappe.model.set_value(doctype, name, 'qty', 0);
|
||||
|
||||
this.frm.script_manager.trigger('qty', doctype, name).then(() => {
|
||||
frappe.model.clear_doc(doctype, name);
|
||||
this.update_cart_html(current_item, true);
|
||||
this.item_details.toggle_item_details_section(undefined);
|
||||
frappe.dom.unfreeze();
|
||||
})
|
||||
frappe.model.set_value(doctype, name, 'qty', 0)
|
||||
.then(() => {
|
||||
frappe.model.clear_doc(doctype, name);
|
||||
this.update_cart_html(current_item, true);
|
||||
this.item_details.toggle_item_details_section(undefined);
|
||||
frappe.dom.unfreeze();
|
||||
})
|
||||
.catch(e => console.log(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,8 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
this.customer_info = undefined;
|
||||
this.hide_images = settings.hide_images;
|
||||
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();
|
||||
}
|
||||
@ -201,7 +203,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
me.events.checkout();
|
||||
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', () => {
|
||||
@ -479,11 +481,15 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
update_totals_section(frm) {
|
||||
if (!frm) frm = this.events.get_frm();
|
||||
|
||||
this.render_net_total(frm.doc.base_net_total);
|
||||
this.render_grand_total(frm.doc.base_grand_total);
|
||||
this.render_net_total(frm.doc.net_total);
|
||||
this.render_grand_total(frm.doc.grand_total);
|
||||
|
||||
const taxes = frm.doc.taxes.map(t => { return { description: t.description, rate: t.rate }})
|
||||
this.render_taxes(frm.doc.base_total_taxes_and_charges, taxes);
|
||||
const taxes = frm.doc.taxes.map(t => {
|
||||
return {
|
||||
description: t.description, rate: t.rate
|
||||
};
|
||||
});
|
||||
this.render_taxes(frm.doc.total_taxes_and_charges, taxes);
|
||||
}
|
||||
|
||||
render_net_total(value) {
|
||||
@ -545,7 +551,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
get_cart_item({ item_code, batch_no, uom }) {
|
||||
const batch_attr = `[data-batch-no="${escape(batch_no)}"]`;
|
||||
const item_code_attr = `[data-item-code="${escape(item_code)}"]`;
|
||||
const uom_attr = `[data-uom=${escape(uom)}]`;
|
||||
const uom_attr = `[data-uom="${escape(uom)}"]`;
|
||||
|
||||
const item_selector = batch_no ?
|
||||
`.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`;
|
||||
@ -667,7 +673,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
|
||||
update_selector_value_in_cart_item(selector, value, item) {
|
||||
const $item_to_update = this.get_cart_item(item);
|
||||
$item_to_update.attr(`data-${selector}`, value);
|
||||
$item_to_update.attr(`data-${selector}`, escape(value));
|
||||
}
|
||||
|
||||
toggle_checkout_btn(show_checkout) {
|
||||
@ -702,14 +708,26 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
on_numpad_event($btn) {
|
||||
const current_action = $btn.attr('data-button-value');
|
||||
const action_is_field_edit = ['qty', 'discount_percentage', 'rate'].includes(current_action);
|
||||
|
||||
this.highlight_numpad_btn($btn, current_action);
|
||||
const action_is_allowed = action_is_field_edit ? (
|
||||
(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 first_click_event = !this.prev_action;
|
||||
const field_to_edit_changed = this.prev_action && this.prev_action != current_action;
|
||||
|
||||
if (action_is_field_edit) {
|
||||
if (!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) {
|
||||
this.prev_action = current_action;
|
||||
@ -753,6 +771,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
this.numpad_value = current_action;
|
||||
}
|
||||
|
||||
this.highlight_numpad_btn($btn, current_action);
|
||||
this.events.numpad_event(this.numpad_value, this.prev_action);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
erpnext.PointOfSale.ItemDetails = class {
|
||||
constructor({ wrapper, events }) {
|
||||
constructor({ wrapper, events, settings }) {
|
||||
this.wrapper = wrapper;
|
||||
this.events = events;
|
||||
this.allow_rate_change = settings.allow_rate_change;
|
||||
this.allow_discount_change = settings.allow_discount_change;
|
||||
this.current_item = {};
|
||||
|
||||
this.init_component();
|
||||
@ -207,17 +209,27 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
bind_custom_control_change_event() {
|
||||
const me = this;
|
||||
if (this.rate_control) {
|
||||
this.rate_control.df.onchange = function() {
|
||||
if (this.value || flt(this.value) === 0) {
|
||||
me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => {
|
||||
const item_row = frappe.get_doc(me.doctype, me.name);
|
||||
const doc = me.events.get_frm().doc;
|
||||
|
||||
me.$item_price.html(format_currency(item_row.rate, doc.currency));
|
||||
me.render_discount_dom(item_row);
|
||||
});
|
||||
}
|
||||
if (this.allow_rate_change) {
|
||||
this.rate_control.df.onchange = function() {
|
||||
if (this.value || flt(this.value) === 0) {
|
||||
me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => {
|
||||
const item_row = frappe.get_doc(me.doctype, me.name);
|
||||
const doc = me.events.get_frm().doc;
|
||||
|
||||
me.$item_price.html(format_currency(item_row.rate, doc.currency));
|
||||
me.render_discount_dom(item_row);
|
||||
});
|
||||
}
|
||||
};
|
||||
} 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) {
|
||||
@ -294,8 +306,16 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
}
|
||||
|
||||
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`];
|
||||
if (field_control) {
|
||||
|
||||
if (item_is_same && field_control && field_control.get_value() !== value) {
|
||||
field_control.set_value(value);
|
||||
cur_pos.update_cart_html(item_row);
|
||||
}
|
||||
|
@ -265,6 +265,14 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
||||
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.events.new_order();
|
||||
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: 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']}
|
||||
];
|
||||
|
@ -168,30 +168,22 @@ erpnext.PointOfSale.Payment = class {
|
||||
me.toggle_numpad(true);
|
||||
|
||||
me.selected_mode = me[`${mode}_control`];
|
||||
const doc = me.events.get_frm().doc;
|
||||
me.selected_mode?.$input?.get(0).focus();
|
||||
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) : '';
|
||||
me.selected_mode && me.selected_mode.$input.get(0).focus();
|
||||
me.auto_set_remaining_amount();
|
||||
}
|
||||
})
|
||||
|
||||
frappe.realtime.on("process_phone_payment", function(data) {
|
||||
frappe.dom.unfreeze();
|
||||
cur_frm.reload_doc();
|
||||
let message = data["ResultDesc"];
|
||||
let title = __("Payment Failed");
|
||||
frappe.ui.form.on('POS Invoice', 'contact_mobile', (frm) => {
|
||||
const contact = frm.doc.contact_mobile;
|
||||
const request_button = $(this.request_for_payment_field.$input[0]);
|
||||
if (contact) {
|
||||
request_button.removeClass('btn-default').addClass('btn-primary');
|
||||
} else {
|
||||
request_button.removeClass('btn-primary').addClass('btn-default');
|
||||
}
|
||||
});
|
||||
|
||||
if (data["ResultCode"] == 0) {
|
||||
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.setup_listener_for_payments();
|
||||
|
||||
this.$payment_modes.on('click', '.shortcut', function(e) {
|
||||
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() {
|
||||
const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl';
|
||||
this.$component.find('.submit-order').attr("title", `${ctrl_label}+Enter`);
|
||||
@ -370,9 +397,11 @@ erpnext.PointOfSale.Payment = class {
|
||||
fieldtype: 'Currency',
|
||||
placeholder: __('Enter {0} amount.', [p.mode_of_payment]),
|
||||
onchange: function() {
|
||||
if (this.value || this.value == 0) {
|
||||
frappe.model.set_value(p.doctype, p.name, 'amount', flt(this.value))
|
||||
.then(() => me.update_totals_section());
|
||||
const current_value = frappe.model.get_value(p.doctype, p.name, 'amount');
|
||||
if (current_value != this.value) {
|
||||
frappe.model
|
||||
.set_value(p.doctype, p.name, 'amount', flt(this.value))
|
||||
.then(() => me.update_totals_section())
|
||||
|
||||
const formatted_currency = format_currency(this.value, currency);
|
||||
me.$payment_modes.find(`.${mode}-amount`).html(formatted_currency);
|
||||
|
@ -139,7 +139,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2020-03-18 18:10:13.048492",
|
||||
"modified": "2021-02-08 17:01:52.162202",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Customer Group",
|
||||
@ -189,6 +189,15 @@
|
||||
"permlevel": 1,
|
||||
"read": 1,
|
||||
"role": "Sales Manager"
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"report": 1,
|
||||
"role": "Customer",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "parent_customer_group",
|
||||
|
@ -245,6 +245,15 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"report": 1,
|
||||
"role": "Customer",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "parent_item_group",
|
||||
|
@ -123,7 +123,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2020-03-18 18:11:36.623555",
|
||||
"modified": "2021-02-08 17:10:03.767426",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Territory",
|
||||
@ -166,6 +166,15 @@
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Maintenance User"
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"report": 1,
|
||||
"role": "Customer",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "parent_territory,territory_manager",
|
||||
|
@ -64,10 +64,10 @@ def get_warehouse_account(warehouse, warehouse_account=None):
|
||||
if not account and 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}")
|
||||
.format(warehouse.name, warehouse.company))
|
||||
return account
|
||||
|
||||
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')
|
||||
|
@ -298,9 +298,9 @@ class TestBatch(unittest.TestCase):
|
||||
self.assertEqual(details.get('price_list_rate'), 400)
|
||||
|
||||
def create_batch(item_code, rate, create_item_price_for_batch):
|
||||
pi = make_purchase_invoice(company="_Test Company with perpetual inventory",
|
||||
warehouse= "Stores - TCP1", cost_center = "Main - TCP1", update_stock=1,
|
||||
expense_account ="_Test Account Cost for Goods Sold - TCP1", item_code=item_code)
|
||||
pi = make_purchase_invoice(company="_Test Company",
|
||||
warehouse= "Stores - _TC", cost_center = "Main - _TC", update_stock=1,
|
||||
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})
|
||||
|
||||
|
@ -16,8 +16,9 @@ class Bin(Document):
|
||||
def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||
'''Called from erpnext.stock.utils.update_bin'''
|
||||
self.update_qty(args)
|
||||
|
||||
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"):
|
||||
args["posting_date"] = nowdate()
|
||||
@ -34,11 +35,13 @@ class Bin(Document):
|
||||
"posting_time": args.get("posting_time"),
|
||||
"voucher_type": args.get("voucher_type"),
|
||||
"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)
|
||||
|
||||
# Validate negative qty in future transactions
|
||||
validate_negative_qty_in_future_sle(args)
|
||||
# update qty in future ale and Validate negative qty
|
||||
update_qty_in_future_sle(args, allow_negative_stock)
|
||||
|
||||
|
||||
def update_qty(self, args):
|
||||
# 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.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty"))
|
||||
self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty"))
|
||||
|
||||
|
||||
self.set_projected_qty()
|
||||
self.db_update()
|
||||
|
||||
|
@ -489,7 +489,10 @@ class TestDeliveryNote(unittest.TestCase):
|
||||
def test_closed_delivery_note(self):
|
||||
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()
|
||||
|
||||
|
@ -47,6 +47,7 @@
|
||||
"base_rate",
|
||||
"base_amount",
|
||||
"pricing_rules",
|
||||
"stock_uom_rate",
|
||||
"is_free_item",
|
||||
"section_break_25",
|
||||
"net_rate",
|
||||
@ -743,13 +744,21 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 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,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-26 17:31:27.029803",
|
||||
"modified": "2021-01-30 21:42:03.767968",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note Item",
|
||||
|
@ -672,13 +672,14 @@ class Item(WebsiteGenerator):
|
||||
if not records: return
|
||||
document = _("Stock Reconciliation") if len(records) == 1 else _("Stock Reconciliations")
|
||||
|
||||
msg = _("The items {0} and {1} are present in the following {2} : <br>"
|
||||
.format(frappe.bold(old_name), frappe.bold(new_name), document))
|
||||
msg = _("The items {0} and {1} are present in the following {2} : ").format(
|
||||
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 += _("Note: To merge the items, create a separate Stock Reconciliation for the old item {0}"
|
||||
.format(frappe.bold(old_name)))
|
||||
msg += _("Note: To merge the items, create a separate Stock Reconciliation for the old item {0}").format(
|
||||
frappe.bold(old_name))
|
||||
|
||||
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))))
|
||||
|
||||
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"]
|
||||
|
||||
# For "Is Stock Item", following doctypes is important
|
||||
|
@ -148,7 +148,6 @@ class TestLandedCostVoucher(unittest.TestCase):
|
||||
|
||||
|
||||
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.items[0].cost_center = "Main - TCP1"
|
||||
for x in range(2):
|
||||
@ -208,6 +207,10 @@ class TestLandedCostVoucher(unittest.TestCase):
|
||||
self.assertEqual(pr.items[1].landed_cost_voucher_amount, 100)
|
||||
|
||||
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
|
||||
usd_shipping = create_account(account_name="Shipping Charges USD",
|
||||
parent_account="Duties and Taxes - TCP1", company="_Test Company with perpetual inventory",
|
||||
|
@ -295,7 +295,8 @@ class PurchaseReceipt(BuyingController):
|
||||
"against": warehouse_account[d.warehouse]["account"],
|
||||
"cost_center": d.cost_center,
|
||||
"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"]),
|
||||
"project": d.project
|
||||
}, item=d))
|
||||
|
@ -94,10 +94,15 @@ class TestPurchaseReceipt(unittest.TestCase):
|
||||
frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete()
|
||||
|
||||
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')
|
||||
|
||||
existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
|
||||
"warehouse": "_Test Warehouse - _TC"}, "stock_value")
|
||||
existing_bin_qty, existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
|
||||
"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()
|
||||
|
||||
|
@ -48,6 +48,7 @@
|
||||
"base_rate",
|
||||
"base_amount",
|
||||
"pricing_rules",
|
||||
"stock_uom_rate",
|
||||
"is_free_item",
|
||||
"section_break_29",
|
||||
"net_rate",
|
||||
@ -874,6 +875,14 @@
|
||||
"label": "Received Qty in Stock UOM",
|
||||
"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",
|
||||
"fieldtype": "Data",
|
||||
@ -886,7 +895,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-26 16:50:56.479347",
|
||||
"modified": "2021-01-30 21:44:06.918515",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Item",
|
||||
|
@ -46,6 +46,9 @@ class RepostItemValuation(Document):
|
||||
|
||||
def repost(doc):
|
||||
try:
|
||||
if not frappe.db.exists("Repost Item Valuation", doc.name):
|
||||
return
|
||||
|
||||
doc.set_status('In Progress')
|
||||
frappe.db.commit()
|
||||
|
||||
@ -64,7 +67,7 @@ def repost(doc):
|
||||
message += "<br>" + "Traceback: <br>" + traceback
|
||||
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')
|
||||
raise
|
||||
finally:
|
||||
|
@ -190,6 +190,7 @@ def create_shipment_company(company_name, abbr):
|
||||
company.abbr = abbr
|
||||
company.default_currency = 'EUR'
|
||||
company.country = 'Germany'
|
||||
company.enable_perpetual_inventory = 0
|
||||
company.insert()
|
||||
return company
|
||||
|
||||
|
@ -37,6 +37,7 @@ class StockLedgerEntry(Document):
|
||||
self.block_transactions_against_group_warehouse()
|
||||
self.validate_with_last_transaction_posting_time()
|
||||
|
||||
|
||||
def on_submit(self):
|
||||
self.check_stock_frozen_date()
|
||||
self.actual_amt_check()
|
||||
|
@ -19,7 +19,7 @@ from erpnext.stock.doctype.item_manufacturer.item_manufacturer import get_item_m
|
||||
|
||||
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']
|
||||
|
||||
@frappe.whitelist()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user