Merge branch 'develop' of https://github.com/frappe/erpnext into rebrand-ui

This commit is contained in:
Suraj Shetty 2021-01-29 08:56:22 +05:30
commit 0a8939e1c1
186 changed files with 5197 additions and 1366 deletions

View File

@ -132,16 +132,10 @@ def allow_regional(fn):
return caller
def get_last_membership():
def get_last_membership(member):
'''Returns last membership if exists'''
last_membership = frappe.get_all('Membership', 'name,to_date,membership_type',
dict(member=frappe.session.user, paid=1), order_by='to_date desc', limit=1)
dict(member=member, paid=1), order_by='to_date desc', limit=1)
return last_membership and last_membership[0]
def is_member():
'''Returns true if the user is still a member'''
last_membership = get_last_membership()
if last_membership and getdate(last_membership.to_date) > getdate():
return True
return False
if last_membership:
return last_membership[0]

View File

@ -254,7 +254,8 @@ def create_account(**kwargs):
account_name = kwargs.get('account_name'),
account_type = kwargs.get('account_type'),
parent_account = kwargs.get('parent_account'),
company = kwargs.get('company')
company = kwargs.get('company'),
account_currency = kwargs.get('account_currency')
))
account.save()

View File

@ -165,9 +165,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",

View File

@ -21,6 +21,7 @@
"book_asset_depreciation_entry_automatically",
"add_taxes_from_item_tax_template",
"automatically_fetch_payment_terms",
"delete_linked_ledger_entries",
"deferred_accounting_settings_section",
"automatically_process_deferred_accounting_entry",
"book_deferred_entries_based_on",
@ -219,6 +220,12 @@
"fieldtype": "Select",
"label": "Book Deferred Entries Based On",
"options": "Days\nMonths"
},
{
"default": "0",
"fieldname": "delete_linked_ledger_entries",
"fieldtype": "Check",
"label": "Delete Accounting and Stock Ledger Entries on deletion of Transaction"
}
],
"icon": "icon-cog",
@ -226,7 +233,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-10-13 11:32:52.268826",
"modified": "2021-01-05 13:04:00.118892",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
@ -254,4 +261,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}
}

View File

@ -159,8 +159,8 @@ class GLEntry(Document):
if not self.flags.from_repost and not self.voucher_type == 'Period Closing Voucher' \
and self.cost_center and _check_is_group():
frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot
be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
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)))
def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party)
@ -170,7 +170,7 @@ class GLEntry(Document):
account_currency = get_account_currency(self.account)
if not self.account_currency:
self.account_currency = company_currency
self.account_currency = account_currency or company_currency
if account_currency != self.account_currency:
frappe.throw(_("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}")

View File

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

View File

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

View File

@ -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"))
@ -57,20 +56,29 @@ class POSClosingEntry(Document):
if not invalid_rows:
return
error_list = [_("Row #{}: {}").format(row.get('idx'), row.get('msg')) for row in invalid_rows]
frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
error_list = []
for row in invalid_rows:
for msg in row.get('msg'):
error_list.append(_("Row #{}: {}").format(row.get('idx'), msg))
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()
frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
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

View File

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

View File

@ -13,7 +13,6 @@ from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profi
class TestPOSClosingEntry(unittest.TestCase):
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)

View File

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

View File

@ -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):
@ -297,7 +312,9 @@ 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')
else:
@ -305,6 +322,8 @@ class POSInvoice(SalesInvoice):
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"):

View File

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

View File

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

View File

@ -5,10 +5,13 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate
from frappe.model.document import Document
from frappe.model.mapper import map_doc
from frappe.model import default_fields
from 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.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()
@ -83,7 +92,7 @@ class POSInvoiceMergeLog(Document):
credit_note.is_consolidated = 1
# TODO: return could be against multiple sales invoice which could also have been consolidated?
credit_note.return_against = self.consolidated_invoice
# credit_note.return_against = self.consolidated_invoice
credit_note.save()
credit_note.submit()
self.consolidated_credit_note = credit_note.name
@ -111,7 +120,9 @@ class POSInvoiceMergeLog(Document):
i.qty = i.qty + item.qty
if not found:
item.rate = item.net_rate
items.append(item)
item.price_list_rate = 0
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
items.append(si_item)
for tax in doc.get('taxes'):
found = False
@ -147,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
@ -159,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']],
@ -180,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:
@ -190,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

View File

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

View File

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

View File

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

View File

@ -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-10 13:59:28.877572",
"modified": "2021-01-06 14:42:41.713864",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",

View File

@ -275,8 +275,11 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
supplier: function() {
var me = this;
if(this.frm.updating_party_details)
// Do not update if inter company reference is there as the details will already be updated
if(this.frm.updating_party_details || this.frm.doc.inter_company_invoice_reference)
return;
erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details",
{
posting_date: this.frm.doc.posting_date,

View File

@ -57,8 +57,8 @@
"set_warehouse",
"rejected_warehouse",
"col_break_warehouse",
"set_from_warehouse",
"is_subcontracted",
"supplier_warehouse",
"items_section",
"update_stock",
"scan_barcode",
@ -515,6 +515,7 @@
},
{
"depends_on": "update_stock",
"description": "Sets 'Accepted Warehouse' in each row of the items table.",
"fieldname": "set_warehouse",
"fieldtype": "Link",
"label": "Set Accepted Warehouse",
@ -543,17 +544,6 @@
"options": "No\nYes",
"print_hide": 1
},
{
"depends_on": "eval:doc.is_subcontracted==\"Yes\"",
"fieldname": "supplier_warehouse",
"fieldtype": "Link",
"label": "Supplier Warehouse",
"no_copy": 1,
"options": "Warehouse",
"print_hide": 1,
"print_width": "50px",
"width": "50px"
},
{
"fieldname": "items_section",
"fieldtype": "Section Break",
@ -1232,7 +1222,9 @@
"fieldname": "inter_company_invoice_reference",
"fieldtype": "Link",
"label": "Inter Company Invoice Reference",
"no_copy": 1,
"options": "Sales Invoice",
"print_hide": 1,
"read_only": 1
},
{
@ -1356,13 +1348,25 @@
"fieldtype": "Link",
"label": "Represents Company",
"options": "Company"
},
{
"depends_on": "eval:doc.update_stock && (doc.is_subcontracted==\"Yes\" || doc.is_internal_supplier)",
"description": "Sets 'From Warehouse' in each row of the items table.",
"fieldname": "set_from_warehouse",
"fieldtype": "Link",
"label": "Set From Warehouse",
"no_copy": 1,
"options": "Warehouse",
"print_hide": 1,
"print_width": "50px",
"width": "50px"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2020-12-11 12:46:12.796378",
"modified": "2020-12-26 20:49:03.305063",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@ -443,7 +443,7 @@ class PurchaseInvoice(BuyingController):
else:
self.stock_received_but_not_billed = None
self.expenses_included_in_valuation = None
self.negative_expense_to_be_booked = 0.0
gl_entries = []
@ -457,7 +457,7 @@ class PurchaseInvoice(BuyingController):
self.make_internal_transfer_gl_entries(gl_entries)
gl_entries = make_regional_gl_entries(gl_entries, self)
gl_entries = merge_similar_entries(gl_entries)
self.make_payment_gl_entries(gl_entries)
@ -480,7 +480,7 @@ class PurchaseInvoice(BuyingController):
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
if grand_total and not self.is_internal_transfer():
# Didnot use base_grand_total to book rounding loss gle
# Did not use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
gl_entries.append(
@ -511,8 +511,8 @@ class PurchaseInvoice(BuyingController):
voucher_wise_stock_value = {}
if self.update_stock:
for d in frappe.get_all('Stock Ledger Entry',
fields = ["voucher_detail_no", "stock_value_difference"], filters={'voucher_no': self.name}):
voucher_wise_stock_value.setdefault(d.voucher_detail_no, d.stock_value_difference)
fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], filters={'voucher_no': self.name}):
voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference)
valuation_tax_accounts = [d.account_head for d in self.get("taxes")
if d.category in ('Valuation', 'Total and Valuation')
@ -563,16 +563,17 @@ class PurchaseInvoice(BuyingController):
)
else:
gl_entries.append(
self.get_gl_dict({
"account": item.expense_account,
"against": self.supplier,
"debit": warehouse_debit_amount,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"cost_center": item.cost_center,
"project": item.project or self.project
}, account_currency, item=item)
)
if not self.is_internal_transfer():
gl_entries.append(
self.get_gl_dict({
"account": item.expense_account,
"against": self.supplier,
"debit": warehouse_debit_amount,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"cost_center": item.cost_center,
"project": item.project or self.project
}, account_currency, item=item)
)
# Amount added through landed-cost-voucher
if landed_cost_entries:
@ -582,7 +583,8 @@ class PurchaseInvoice(BuyingController):
"against": item.expense_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(amount),
"credit": flt(amount["base_amount"]),
"credit_in_account_currency": flt(amount["amount"]),
"project": item.project or self.project
}, item=item))
@ -624,13 +626,14 @@ class PurchaseInvoice(BuyingController):
if expense_booked_in_pr:
expense_account = service_received_but_not_billed_account
gl_entries.append(self.get_gl_dict({
"account": expense_account,
"against": self.supplier,
"debit": amount,
"cost_center": item.cost_center,
"project": item.project or self.project
}, account_currency, item=item))
if not self.is_internal_transfer():
gl_entries.append(self.get_gl_dict({
"account": expense_account,
"against": self.supplier,
"debit": amount,
"cost_center": item.cost_center,
"project": item.project or self.project
}, account_currency, item=item))
# If asset is bought through this document and not linked to PR
if self.update_stock and item.landed_cost_voucher_amount:
@ -795,10 +798,10 @@ class PurchaseInvoice(BuyingController):
# Stock ledger value is not matching with the warehouse amount
if (self.update_stock and voucher_wise_stock_value.get(item.name) and
warehouse_debit_amount != flt(voucher_wise_stock_value.get(item.name), net_amt_precision)):
warehouse_debit_amount != flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)):
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
stock_amount = flt(voucher_wise_stock_value.get(item.name), net_amt_precision)
stock_amount = flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
stock_adjustment_amt = warehouse_debit_amount - stock_amount
gl_entries.append(
@ -999,10 +1002,10 @@ class PurchaseInvoice(BuyingController):
self.delete_auto_created_batches()
self.make_gl_entries_on_cancel()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
self.update_project()
frappe.db.set(self, 'status', 'Cancelled')

View File

@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "hash",
"creation": "2013-05-22 12:43:10",
"doctype": "DocType",
@ -87,6 +88,7 @@
"po_detail",
"purchase_receipt",
"pr_detail",
"sales_invoice_item",
"item_weight_details",
"weight_per_unit",
"total_weight",
@ -553,8 +555,8 @@
"fieldtype": "Link",
"hidden": 1,
"label": "Brand",
"print_hide": 1,
"options": "Brand"
"options": "Brand",
"print_hide": 1
},
{
"fetch_from": "item_code.item_group",
@ -562,9 +564,9 @@
"fieldname": "item_group",
"fieldtype": "Link",
"label": "Item Group",
"options": "Item Group",
"print_hide": 1,
"read_only": 1,
"options": "Item Group"
"read_only": 1
},
{
"description": "Tax detail table fetched from item master as a string and stored in this field.\nUsed for Taxes and Charges",
@ -759,10 +761,11 @@
"read_only": 1
},
{
"depends_on": "eval:parent.is_internal_supplier && parent.update_stock",
"fieldname": "from_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Supplier Warehouse",
"label": "From Warehouse",
"options": "Warehouse"
},
{
@ -779,11 +782,20 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "sales_invoice_item",
"fieldtype": "Data",
"label": "Sales Invoice Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"modified": "2020-08-20 11:48:01.398356",
"links": [],
"modified": "2020-12-26 17:20:36.415791",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
@ -791,4 +803,4 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
}
}

View File

@ -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);
@ -130,16 +131,15 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
this.set_default_print_format();
if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) {
frappe.model.with_doc("Customer", me.frm.doc.customer, function() {
var customer = frappe.model.get_doc("Customer", me.frm.doc.customer);
var internal = customer.is_internal_customer;
var disabled = customer.disabled;
if (internal == 1 && disabled == 0) {
me.frm.add_custom_button("Inter Company Invoice", function() {
me.make_inter_company_invoice();
}, __('Create'));
}
});
let internal = me.frm.doc.is_internal_customer;
if (internal) {
let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Invoice" :
"Inter Company Purchase Invoice";
me.frm.add_custom_button(button_label, function() {
me.make_inter_company_invoice();
}, __('Create'));
}
}
},

View File

@ -60,6 +60,8 @@
"ignore_pricing_rule",
"sec_warehouse",
"set_warehouse",
"column_break_55",
"set_target_warehouse",
"items_section",
"update_stock",
"scan_barcode",
@ -1969,13 +1971,31 @@
"label": "Represents Company",
"options": "Company",
"read_only": 1
},
{
"fieldname": "column_break_55",
"fieldtype": "Column Break"
},
{
"depends_on": "eval: doc.is_internal_customer && doc.update_stock",
"fieldname": "set_target_warehouse",
"fieldtype": "Link",
"label": "Set Target Warehouse",
"options": "Warehouse"
}
],
"icon": "fa fa-file-text",
"idx": 181,
"is_submittable": 1,
"links": [],
"modified": "2020-12-11 12:48:31.769958",
"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",

View File

@ -6,7 +6,7 @@ import frappe, erpnext
import frappe.defaults
from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate
from frappe import _, msgprint, throw
from erpnext.accounts.party import get_party_account, get_due_date
from erpnext.accounts.party import get_party_account, get_due_date, get_party_details
from frappe.model.mapper import get_mapped_doc
from erpnext.controllers.selling_controller import SellingController
from erpnext.accounts.utils import get_account_currency
@ -21,6 +21,8 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente
from erpnext.accounts.doctype.loyalty_program.loyalty_program import \
get_loyalty_program_details_with_points, get_loyalty_details, validate_loyalty_points
from erpnext.accounts.deferred_revenue import validate_service_stop_date
from frappe.model.utils import get_fetch_values
from frappe.contacts.doctype.address.address import get_address_display
from erpnext.healthcare.utils import manage_invoice_submit_cancel
@ -231,7 +233,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)
@ -433,7 +453,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')
@ -1534,7 +1556,7 @@ def validate_inter_company_transaction(doc, doctype):
details = get_inter_company_details(doc, doctype)
price_list = doc.selling_price_list if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"] else doc.buying_price_list
valid_price_list = frappe.db.get_value("Price List", {"name": price_list, "buying": 1, "selling": 1})
if not valid_price_list:
if not valid_price_list and not doc.is_internal_transfer():
frappe.throw(_("Selected Price List should have buying and selling fields checked."))
party = details.get("party")
@ -1557,6 +1579,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
if doctype in ["Sales Invoice", "Sales Order"]:
source_doc = frappe.get_doc(doctype, source_name)
target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order"
target_detail_field = "sales_invoice_item" if doctype == "Sales Invoice" else "sales_order_item"
source_document_warehouse_field = 'target_warehouse'
target_document_warehouse_field = 'from_warehouse'
else:
@ -1570,6 +1593,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
def set_missing_values(source, target):
target.run_method("set_missing_values")
set_purchase_references(target)
def update_details(source_doc, target_doc, source_parent):
target_doc.inter_company_invoice_reference = source_doc.name
@ -1577,19 +1601,38 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
currency = frappe.db.get_value('Supplier', details.get('party'), 'default_currency')
target_doc.company = details.get("company")
target_doc.supplier = details.get("party")
target_doc.is_internal_supplier = 1
target_doc.ignore_pricing_rule = 1
target_doc.buying_price_list = source_doc.selling_price_list
# Invert Addresses
update_address(target_doc, 'supplier_address', 'address_display', source_doc.company_address)
update_address(target_doc, 'shipping_address', 'shipping_address_display', source_doc.customer_address)
if currency:
target_doc.currency = currency
update_taxes(target_doc, party=target_doc.supplier, party_type='Supplier', company=target_doc.company,
doctype=target_doc.doctype, party_address=target_doc.supplier_address,
company_address=target_doc.shipping_address)
else:
currency = frappe.db.get_value('Customer', details.get('party'), 'default_currency')
target_doc.company = details.get("company")
target_doc.customer = details.get("party")
target_doc.selling_price_list = source_doc.buying_price_list
update_address(target_doc, 'company_address', 'company_address_display', source_doc.supplier_address)
update_address(target_doc, 'shipping_address_name', 'shipping_address', source_doc.shipping_address)
update_address(target_doc, 'customer_address', 'address_display', source_doc.shipping_address)
if currency:
target_doc.currency = currency
update_taxes(target_doc, party=target_doc.customer, party_type='Customer', company=target_doc.company,
doctype=target_doc.doctype, party_address=target_doc.customer_address,
company_address=target_doc.company_address, shipping_address_name=target_doc.shipping_address_name)
item_field_map = {
"doctype": target_doctype + " Item",
"field_no_map": [
@ -1597,25 +1640,33 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"expense_account",
"cost_center",
"warehouse"
]
],
"field_map": {
'rate': 'rate',
}
}
if source_doc.get('update_stock'):
item_field_map.update({
'field_map': {
source_document_warehouse_field: target_document_warehouse_field,
'batch_no': 'batch_no',
'serial_no': 'serial_no'
}
if doctype in ["Sales Invoice", "Sales Order"]:
item_field_map["field_map"].update({
"name": target_detail_field,
})
if source_doc.get('update_stock'):
item_field_map["field_map"].update({
source_document_warehouse_field: target_document_warehouse_field,
'batch_no': 'batch_no',
'serial_no': 'serial_no'
})
doclist = get_mapped_doc(doctype, source_name, {
doctype: {
"doctype": target_doctype,
"postprocess": update_details,
"set_target_warehouse": "set_from_warehouse",
"field_no_map": [
"taxes_and_charges"
"taxes_and_charges",
"set_warehouse",
"shipping_address"
]
},
doctype +" Item": item_field_map
@ -1624,6 +1675,110 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
return doclist
def set_purchase_references(doc):
# add internal PO or PR links if any
if doc.is_internal_transfer():
if doc.doctype == 'Purchase Receipt':
so_item_map = get_delivery_note_details(doc.inter_company_invoice_reference)
if so_item_map:
pd_item_map, parent_child_map, warehouse_map = \
get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item')
update_pr_items(doc, so_item_map, pd_item_map, parent_child_map, warehouse_map)
elif doc.doctype == 'Purchase Invoice':
dn_item_map, so_item_map = get_sales_invoice_details(doc.inter_company_invoice_reference)
# First check for Purchase receipt
if list(dn_item_map.values()):
pd_item_map, parent_child_map, warehouse_map = \
get_pd_details('Purchase Receipt Item', dn_item_map, 'delivery_note_item')
update_pi_items(doc, 'pr_detail', 'purchase_receipt',
dn_item_map, pd_item_map, parent_child_map, warehouse_map)
if list(so_item_map.values()):
pd_item_map, parent_child_map, warehouse_map = \
get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item')
update_pi_items(doc, 'po_detail', 'purchase_order',
so_item_map, pd_item_map, parent_child_map, warehouse_map)
def update_pi_items(doc, detail_field, parent_field, sales_item_map,
purchase_item_map, parent_child_map, warehouse_map):
for item in doc.get('items'):
item.set(detail_field, purchase_item_map.get(sales_item_map.get(item.sales_invoice_item)))
item.set(parent_field, parent_child_map.get(sales_item_map.get(item.sales_invoice_item)))
if doc.update_stock:
item.warehouse = warehouse_map.get(sales_item_map.get(item.sales_invoice_item))
def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, warehouse_map):
for item in doc.get('items'):
item.purchase_order_item = purchase_item_map.get(sales_item_map.get(item.delivery_note_item))
item.warehouse = warehouse_map.get(sales_item_map.get(item.delivery_note_item))
item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item))
def get_delivery_note_details(internal_reference):
so_item_map = {}
si_item_details = frappe.get_all('Delivery Note Item', fields=['name', 'so_detail'],
filters={'parent': internal_reference})
for d in si_item_details:
so_item_map.setdefault(d.name, d.so_detail)
return so_item_map
def get_sales_invoice_details(internal_reference):
dn_item_map = {}
so_item_map = {}
si_item_details = frappe.get_all('Sales Invoice Item', fields=['name', 'so_detail',
'dn_detail'], filters={'parent': internal_reference})
for d in si_item_details:
if d.dn_detail:
dn_item_map.setdefault(d.name, d.dn_detail)
if d.so_detail:
so_item_map.setdefault(d.name, d.so_detail)
return dn_item_map, so_item_map
def get_pd_details(doctype, sd_detail_map, sd_detail_field):
pd_item_map = {}
accepted_warehouse_map = {}
parent_child_map = {}
pd_item_details = frappe.get_all(doctype,
fields=[sd_detail_field, 'name', 'warehouse', 'parent'], filters={sd_detail_field: ('in', list(sd_detail_map.values()))})
for d in pd_item_details:
pd_item_map.setdefault(d.get(sd_detail_field), d.name)
parent_child_map.setdefault(d.get(sd_detail_field), d.parent)
accepted_warehouse_map.setdefault(d.get(sd_detail_field), d.warehouse)
return pd_item_map, parent_child_map, accepted_warehouse_map
def update_taxes(doc, party=None, party_type=None, company=None, doctype=None, party_address=None,
company_address=None, shipping_address_name=None, master_doctype=None):
# Update Party Details
party_details = get_party_details(party=party, party_type=party_type, company=company,
doctype=doctype, party_address=party_address, company_address=company_address,
shipping_address=shipping_address_name)
# Update taxes and charges if any
doc.taxes_and_charges = party_details.get('taxes_and_charges')
doc.set('taxes', party_details.get('taxes'))
def update_address(doc, address_field, address_display_field, address_name):
doc.set(address_field, address_name)
fetch_values = get_fetch_values(doc.doctype, address_field, address_name)
for key, value in fetch_values.items():
doc.set(key, value)
doc.set(address_display_field, get_address_display(doc.get(address_field)))
@frappe.whitelist()
def get_loyalty_programs(customer):
''' sets applicable loyalty program to the customer or returns a list of applicable programs '''

View File

@ -22,6 +22,7 @@ from erpnext.regional.india.utils import get_ewb_data
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
from erpnext.stock.utils import get_incoming_rate
class TestSalesInvoice(unittest.TestCase):
def make(self):
@ -688,7 +689,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertTrue(gle)
def test_pos_gl_entry_with_perpetual_inventory(self):
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1")
@ -745,7 +746,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(pos_return.get('payments')[0].amount, -1000)
def test_pos_change_amount(self):
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",
@ -1801,6 +1802,24 @@ class TestSalesInvoice(unittest.TestCase):
si.items[0].target_warehouse = 'Work In Progress - TCP1'
add_taxes(si)
si.save()
rate = 0.0
for d in si.get('items'):
rate = get_incoming_rate({
"item_code": d.item_code,
"warehouse": d.warehouse,
"posting_date": si.posting_date,
"posting_time": si.posting_time,
"qty": -1 * flt(d.get('stock_qty')),
"serial_no": d.serial_no,
"company": si.company,
"voucher_type": 'Sales Invoice',
"voucher_no": si.name,
"allow_zero_valuation": d.get("allow_zero_valuation")
}, raise_error_if_no_rate=False)
rate = flt(rate, 2)
si.submit()
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
@ -1810,18 +1829,23 @@ class TestSalesInvoice(unittest.TestCase):
target_doc.save()
target_doc.submit()
tax_amount = flt(rate * (12/100), 2)
si_gl_entries = [
["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()],
["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()]
["_Test Account Excise Duty - TCP1", 0.0, tax_amount, nowdate()],
["Unrealized Profit - TCP1", tax_amount, 0.0, nowdate()]
]
check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1))
pi_gl_entries = [
["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()],
["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()]
["_Test Account Excise Duty - TCP1", tax_amount , 0.0, nowdate()],
["Unrealized Profit - TCP1", 0.0, tax_amount, nowdate()]
]
# Sale and Purchase both should be at valuation rate
self.assertEqual(si.items[0].rate, rate)
self.assertEqual(target_doc.items[0].rate, rate)
check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
def test_eway_bill_json(self):
@ -1841,7 +1865,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(data['billLists'][0]['sgstValue'], 5400)
self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234')
self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000)
def test_einvoice_submission_without_irn(self):
# init
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1)
@ -1857,7 +1881,7 @@ class TestSalesInvoice(unittest.TestCase):
# reset
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0)
frappe.flags.country = country
def test_einvoice_json(self):
from erpnext.regional.india.e_invoice.utils import make_einvoice

View File

@ -565,11 +565,12 @@
"print_hide": 1
},
{
"depends_on": "eval: parent.is_internal_customer && parent.update_stock",
"fieldname": "target_warehouse",
"fieldtype": "Link",
"hidden": 1,
"ignore_user_permissions": 1,
"label": "Customer Warehouse (Optional)",
"label": "Target Warehouse",
"no_copy": 1,
"options": "Warehouse",
"print_hide": 1
@ -815,7 +816,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2020-09-23 19:59:04.879322",
"modified": "2020-12-26 17:25:04.090630",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@ -34,6 +34,9 @@ def valdiate_taxes_and_charges_template(doc):
validate_disabled(doc)
# Validate with existing taxes and charges template for unique tax category
validate_for_tax_category(doc)
for tax in doc.get("taxes"):
validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, doc)
@ -41,3 +44,7 @@ def valdiate_taxes_and_charges_template(doc):
def validate_disabled(doc):
if doc.is_default and doc.disabled:
frappe.throw(_("Disabled template must not be default template"))
def validate_for_tax_category(doc):
if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0}):
frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category)))

View File

@ -152,7 +152,7 @@
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td>
</tr>

View File

@ -49,12 +49,13 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
elif d.po_detail:
purchase_receipt = ", ".join(po_pr_map.get(d.po_detail, []))
expense_account = d.expense_account or aii_account_map.get(d.company)
expense_account = d.unrealized_profit_loss_account or d.expense_account \
or aii_account_map.get(d.company)
row = {
'item_code': d.item_code,
'item_name': item_record.item_name,
'item_group': item_record.item_group,
'item_name': item_record.item_name if item_record else d.item_name,
'item_group': item_record.item_group if item_record else d.item_group,
'description': d.description,
'invoice': d.parent,
'posting_date': d.posting_date,
@ -315,7 +316,9 @@ def get_items(filters, additional_query_columns):
`tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`,
`tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company,
`tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total,
`tabPurchase Invoice`.unrealized_profit_loss_account,
`tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description,
`tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`,
`tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,
`tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`,
`tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`,

View File

@ -76,7 +76,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
'company': d.company,
'sales_order': d.sales_order,
'delivery_note': d.delivery_note,
'income_account': d.income_account,
'income_account': d.unrealized_profit_loss_account or d.income_account,
'cost_center': d.cost_center,
'stock_qty': d.stock_qty,
'stock_uom': d.stock_uom
@ -379,6 +379,7 @@ def get_items(filters, additional_query_columns):
select
`tabSales Invoice Item`.name, `tabSales Invoice Item`.parent,
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
`tabSales Invoice`.unrealized_profit_loss_account,
`tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,

View File

@ -14,13 +14,15 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
if not filters: filters = {}
invoice_list = get_invoices(filters, additional_query_columns)
columns, expense_accounts, tax_accounts = get_columns(invoice_list, additional_table_columns)
columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts \
= get_columns(invoice_list, additional_table_columns)
if not invoice_list:
msgprint(_("No record found"))
return columns, invoice_list
invoice_expense_map = get_invoice_expense_map(invoice_list)
internal_invoice_map = get_internal_invoice_map(invoice_list)
invoice_expense_map, invoice_tax_map = get_invoice_tax_map(invoice_list,
invoice_expense_map, expense_accounts)
invoice_po_pr_map = get_invoice_po_pr_map(invoice_list)
@ -52,10 +54,17 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
# map expense values
base_net_total = 0
for expense_acc in expense_accounts:
expense_amount = flt(invoice_expense_map.get(inv.name, {}).get(expense_acc))
if inv.is_internal_supplier and inv.company == inv.represents_company:
expense_amount = 0
else:
expense_amount = flt(invoice_expense_map.get(inv.name, {}).get(expense_acc))
base_net_total += expense_amount
row.append(expense_amount)
# Add amount in unrealized account
for account in unrealized_profit_loss_accounts:
row.append(flt(internal_invoice_map.get((inv.name, account))))
# net total
row.append(base_net_total or inv.base_net_total)
@ -96,7 +105,8 @@ def get_columns(invoice_list, additional_table_columns):
"width": 80
}
]
expense_accounts = tax_accounts = expense_columns = tax_columns = []
expense_accounts = tax_accounts = expense_columns = tax_columns = unrealized_profit_loss_accounts = \
unrealized_profit_loss_account_columns = []
if invoice_list:
expense_accounts = frappe.db.sql_list("""select distinct expense_account
@ -112,17 +122,25 @@ def get_columns(invoice_list, additional_table_columns):
and parent in (%s) order by account_head""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
from `tabPurchase Invoice` where docstatus = 1 and name in (%s)
and ifnull(unrealized_profit_loss_account, '') != ''
order by unrealized_profit_loss_account""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
expense_columns = [(account + ":Currency/currency:120") for account in expense_accounts]
unrealized_profit_loss_account_columns = [(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts]
for account in tax_accounts:
if account not in expense_accounts:
tax_columns.append(account + ":Currency/currency:120")
columns = columns + expense_columns + [_("Net Total") + ":Currency/currency:120"] + tax_columns + \
columns = columns + expense_columns + unrealized_profit_loss_account_columns + \
[_("Net Total") + ":Currency/currency:120"] + tax_columns + \
[_("Total Tax") + ":Currency/currency:120", _("Grand Total") + ":Currency/currency:120",
_("Rounded Total") + ":Currency/currency:120", _("Outstanding Amount") + ":Currency/currency:120"]
return columns, expense_accounts, tax_accounts
return columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts
def get_conditions(filters):
conditions = ""
@ -199,6 +217,19 @@ def get_invoice_expense_map(invoice_list):
return invoice_expense_map
def get_internal_invoice_map(invoice_list):
unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account,
base_net_total as amount from `tabPurchase Invoice` where name in (%s)
and is_internal_supplier = 1 and company = represents_company""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
internal_invoice_map = {}
for d in unrealized_amount_details:
if d.unrealized_profit_loss_account:
internal_invoice_map.setdefault((d.name, d.unrealized_profit_loss_account), d.amount)
return internal_invoice_map
def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts):
tax_details = frappe.db.sql("""
select parent, account_head, case add_deduct_tax when "Add" then sum(base_tax_amount_after_discount_amount)

View File

@ -15,13 +15,14 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No
if not filters: filters = frappe._dict({})
invoice_list = get_invoices(filters, additional_query_columns)
columns, income_accounts, tax_accounts = get_columns(invoice_list, additional_table_columns)
columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts = get_columns(invoice_list, additional_table_columns)
if not invoice_list:
msgprint(_("No record found"))
return columns, invoice_list
invoice_income_map = get_invoice_income_map(invoice_list)
internal_invoice_map = get_internal_invoice_map(invoice_list)
invoice_income_map, invoice_tax_map = get_invoice_tax_map(invoice_list,
invoice_income_map, income_accounts)
#Cost Center & Warehouse Map
@ -70,12 +71,22 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No
# map income values
base_net_total = 0
for income_acc in income_accounts:
income_amount = flt(invoice_income_map.get(inv.name, {}).get(income_acc))
if inv.is_internal_customer and inv.company == inv.represents_company:
income_amount = 0
else:
income_amount = flt(invoice_income_map.get(inv.name, {}).get(income_acc))
base_net_total += income_amount
row.update({
frappe.scrub(income_acc): income_amount
})
# Add amount in unrealized account
for account in unrealized_profit_loss_accounts:
row.update({
frappe.scrub(account): flt(internal_invoice_map.get((inv.name, account)))
})
# net total
row.update({'net_total': base_net_total or inv.base_net_total})
@ -230,6 +241,8 @@ def get_columns(invoice_list, additional_table_columns):
tax_accounts = []
income_columns = []
tax_columns = []
unrealized_profit_loss_accounts = []
unrealized_profit_loss_account_columns = []
if invoice_list:
income_accounts = frappe.db.sql_list("""select distinct income_account
@ -243,12 +256,18 @@ def get_columns(invoice_list, additional_table_columns):
and parent in (%s) order by account_head""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
from `tabSales Invoice` where docstatus = 1 and name in (%s)
and ifnull(unrealized_profit_loss_account, '') != ''
order by unrealized_profit_loss_account""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
for account in income_accounts:
income_columns.append({
"label": account,
"fieldname": frappe.scrub(account),
"fieldtype": "Currency",
"options": 'currency',
"options": "currency",
"width": 120
})
@ -258,15 +277,24 @@ def get_columns(invoice_list, additional_table_columns):
"label": account,
"fieldname": frappe.scrub(account),
"fieldtype": "Currency",
"options": 'currency',
"options": "currency",
"width": 120
})
for account in unrealized_profit_loss_accounts:
unrealized_profit_loss_account_columns.append({
"label": account,
"fieldname": frappe.scrub(account),
"fieldtype": "Currency",
"options": "currency",
"width": 120
})
net_total_column = [{
"label": _("Net Total"),
"fieldname": "net_total",
"fieldtype": "Currency",
"options": 'currency',
"options": "currency",
"width": 120
}]
@ -301,9 +329,10 @@ def get_columns(invoice_list, additional_table_columns):
}
]
columns = columns + income_columns + net_total_column + tax_columns + total_columns
columns = columns + income_columns + unrealized_profit_loss_account_columns + \
net_total_column + tax_columns + total_columns
return columns, income_accounts, tax_accounts
return columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts
def get_conditions(filters):
conditions = ""
@ -368,7 +397,8 @@ def get_invoices(filters, additional_query_columns):
return frappe.db.sql("""
select name, posting_date, debit_to, project, customer,
customer_name, owner, remarks, territory, tax_id, customer_group,
base_net_total, base_grand_total, base_rounded_total, outstanding_amount {0}
base_net_total, base_grand_total, base_rounded_total, outstanding_amount,
is_internal_customer, represents_company, company {0}
from `tabSales Invoice`
where docstatus = 1 %s order by posting_date desc, name desc""".format(additional_query_columns or '') %
conditions, filters, as_dict=1)
@ -385,6 +415,19 @@ def get_invoice_income_map(invoice_list):
return invoice_income_map
def get_internal_invoice_map(invoice_list):
unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account,
base_net_total as amount from `tabSales Invoice` where name in (%s)
and is_internal_customer = 1 and company = represents_company""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
internal_invoice_map = {}
for d in unrealized_amount_details:
if d.unrealized_profit_loss_account:
internal_invoice_map.setdefault((d.name, d.unrealized_profit_loss_account), d.amount)
return internal_invoice_map
def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts):
tax_details = frappe.db.sql("""select parent, account_head,
sum(base_tax_amount_after_discount_amount) as tax_amount

View File

@ -164,16 +164,16 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
if (doc.docstatus === 1 && !doc.inter_company_order_reference) {
let me = this;
frappe.model.with_doc("Supplier", me.frm.doc.supplier, () => {
let supplier = frappe.model.get_doc("Supplier", me.frm.doc.supplier);
let internal = supplier.is_internal_supplier;
let disabled = supplier.disabled;
if (internal === 1 && disabled === 0) {
me.frm.add_custom_button("Inter Company Order", function() {
me.make_inter_company_order(me.frm);
}, __('Create'));
}
});
let internal = me.frm.doc.is_internal_supplier;
if (internal) {
let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Sales Order" :
"Inter Company Sales Order";
me.frm.add_custom_button(button_label, function() {
me.make_inter_company_order(me.frm);
}, __('Create'));
}
}
}
@ -353,7 +353,8 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
make_purchase_receipt: function() {
frappe.model.open_mapped_doc({
method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt",
frm: cur_frm
frm: cur_frm,
freeze_message: __("Creating Purchase Receipt ...")
})
},
@ -380,7 +381,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
material_request_type: "Purchase",
docstatus: 1,
status: ["!=", "Stopped"],
per_ordered: ["<", 99.99],
per_ordered: ["<", 100],
company: me.frm.doc.company
}
})

View File

@ -134,6 +134,8 @@
"ref_sq",
"column_break_74",
"party_account_currency",
"is_internal_supplier",
"represents_company",
"inter_company_order_reference"
],
"fields": [
@ -1101,13 +1103,28 @@
{
"fieldname": "items_col_break",
"fieldtype": "Column Break"
},
{
"default": "0",
"fetch_from": "supplier.is_internal_supplier",
"fieldname": "is_internal_supplier",
"fieldtype": "Check",
"label": "Is Internal Supplier"
},
{
"fetch_from": "supplier.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
"label": "Represents Company",
"options": "Company",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2020-12-03 16:46:44.229351",
"modified": "2021-01-20 22:07:23.487138",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@ -123,8 +123,8 @@ class PurchaseOrder(BuyingController):
if self.is_subcontracted == "Yes":
for item in self.items:
if not item.bom:
frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}"\
.format(item.item_code, item.idx)))
frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}")
.format(item.item_code, item.idx))
def get_schedule_dates(self):
for d in self.get('items'):

View File

@ -224,7 +224,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
material_request_type: "Purchase",
docstatus: 1,
status: ["!=", "Stopped"],
per_ordered: ["<", 99.99],
per_ordered: ["<", 100],
company: me.frm.doc.company
}
})
@ -280,7 +280,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
material_request_type: "Purchase",
docstatus: 1,
status: ["!=", "Stopped"],
per_ordered: ["<", 99.99]
per_ordered: ["<", 100]
}
});
dialog.hide();

View File

@ -52,7 +52,10 @@ class Supplier(TransactionBase):
self.validate_internal_supplier()
def validate_internal_supplier(self):
if self.is_internal_supplier and frappe.db.get_value("Supplier", {"represents_company": self.represents_company}, "name"):
internal_supplier = frappe.db.get_value("Supplier",
{"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name")
if internal_supplier:
frappe.throw(_("Internal Supplier for company {0} already exists").format(
frappe.bold(self.represents_company)))

View File

@ -44,7 +44,7 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext
material_request_type: "Purchase",
docstatus: 1,
status: ["!=", "Stopped"],
per_ordered: ["<", 99.99],
per_ordered: ["<", 100],
company: me.frm.doc.company
}
})

View File

@ -89,6 +89,9 @@ class AccountsController(TransactionBase):
self.ensure_supplier_is_not_blocked()
self.validate_date_with_fiscal_year()
self.validate_inter_company_reference()
self.set_incoming_rate()
if self.meta.get_field("currency"):
self.calculate_taxes_and_totals()
@ -124,14 +127,20 @@ class AccountsController(TransactionBase):
self.set_inter_company_account()
validate_regional(self)
validate_einvoice_fields(self)
if self.doctype != 'Material Request':
apply_pricing_rule_on_transaction(self)
def before_cancel(self):
validate_einvoice_fields(self)
def on_trash(self):
# delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value('Accounts Settings', 'delete_linked_ledger_entries'):
frappe.db.sql("delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name))
frappe.db.sql("delete from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name))
def validate_deferred_start_and_end_date(self):
for d in self.items:
@ -223,6 +232,17 @@ class AccountsController(TransactionBase):
validate_fiscal_year(self.get(date_field), self.fiscal_year, self.company,
self.meta.get_label(date_field), self)
def validate_inter_company_reference(self):
if self.doctype not in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'):
return
if self.is_internal_transfer():
if not (self.get('inter_company_reference') or self.get('inter_company_invoice_reference')
or self.get('inter_company_order_reference')):
msg = _("Internal Sale or Delivery Reference missing. ")
msg += _("Please create purchase from internal sale or delivery document itself")
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
def validate_due_date(self):
if self.get('is_pos'): return
@ -299,6 +319,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")
@ -465,8 +486,10 @@ class AccountsController(TransactionBase):
account_currency = get_account_currency(gl_dict.account)
if gl_dict.account and self.doctype not in ["Journal Entry",
"Period Closing Voucher", "Payment Entry"]:
"Period Closing Voucher", "Payment Entry", "Purchase Receipt", "Purchase Invoice", "Stock Entry"]:
self.validate_account_currency(gl_dict.account, account_currency)
if gl_dict.account and self.doctype not in ["Journal Entry", "Period Closing Voucher", "Payment Entry"]:
set_balance_in_account_currency(gl_dict, account_currency, self.get("conversion_rate"),
self.company_currency)
@ -979,9 +1002,9 @@ class AccountsController(TransactionBase):
It will an internal transfer if its an internal customer and representation
company is same as billing company
"""
if self.doctype == 'Sales Invoice':
if self.doctype in ('Sales Invoice', 'Delivery Note', 'Sales Order'):
internal_party_field = 'is_internal_customer'
else:
elif self.doctype in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'):
internal_party_field = 'is_internal_supplier'
if self.get(internal_party_field) and (self.represents_company == self.company):

View File

@ -34,7 +34,6 @@ class BuyingController(StockController):
self.validate_items()
self.set_qty_as_per_stock_uom()
self.validate_stock_or_nonstock_items()
self.update_tax_category_for_internal_transfer()
self.validate_warehouse()
self.validate_from_warehouse()
self.set_supplier_address()
@ -90,11 +89,6 @@ class BuyingController(StockController):
msg = _('Tax Category has been changed to "Total" because all the Items are non-stock items')
self.update_tax_category(msg)
def update_tax_category_for_internal_transfer(self):
if self.doctype == 'Purchase Invoice' and self.is_internal_transfer():
msg = _('Tax Category has been changed to "Total" as its an internal purchase.')
self.update_tax_category(msg)
def update_tax_category(self, msg):
tax_for_valuation = [d for d in self.get("taxes")
if d.category in ["Valuation", "Valuation and Total"]]
@ -214,6 +208,48 @@ class BuyingController(StockController):
else:
item.valuation_rate = 0.0
def set_incoming_rate(self):
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"):
return
ref_doctype_map = {
"Purchase Order": "Sales Order Item",
"Purchase Receipt": "Delivery Note Item",
"Purchase Invoice": "Sales Invoice Item",
}
ref_doctype = ref_doctype_map.get(self.doctype)
items = self.get("items")
for d in items:
if not cint(self.get("is_return")):
# Get outgoing rate based on original item cost based on valuation method
if not d.get(frappe.scrub(ref_doctype)):
outgoing_rate = get_incoming_rate({
"item_code": d.item_code,
"warehouse": d.get('from_warehouse'),
"posting_date": self.get('posting_date') or self.get('transation_date'),
"posting_time": self.get('posting_time'),
"qty": -1 * flt(d.get('stock_qty')),
"serial_no": d.get('serial_no'),
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
"allow_zero_valuation": d.get("allow_zero_valuation")
}, raise_error_if_no_rate=False)
rate = flt(outgoing_rate * d.conversion_factor, d.precision('rate'))
else:
rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), 'rate')
if self.is_internal_transfer():
if rate != d.rate:
d.rate = rate
d.discount_percentage = 0
d.discount_amount = 0
frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer")
.format(d.idx), alert=1)
def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
supplied_items_cost = 0.0
for d in self.get("supplied_items"):
@ -233,7 +269,7 @@ class BuyingController(StockController):
d.amount = flt(flt(d.consumed_qty) * flt(d.rate), d.precision("amount"))
supplied_items_cost += flt(d.amount)
return supplied_items_cost
def validate_for_subcontracting(self):
@ -549,6 +585,8 @@ class BuyingController(StockController):
from_warehouse_sle = self.get_sl_entries(d, {
"actual_qty": -1 * pr_qty,
"warehouse": d.from_warehouse,
"outgoing_rate": d.rate,
"recalculate_rate": 1,
"dependant_sle_voucher_detail_no": d.name
})

View File

@ -655,6 +655,34 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(query, filters)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_healthcare_service_units(doctype, txt, searchfield, start, page_len, filters):
query = """
select name
from `tabHealthcare Service Unit`
where
is_group = 0
and company = {company}
and name like {txt}""".format(
company = frappe.db.escape(filters.get('company')), txt = frappe.db.escape('%{0}%'.format(txt)))
if filters and filters.get('inpatient_record'):
from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_current_healthcare_service_unit
service_unit = get_current_healthcare_service_unit(filters.get('inpatient_record'))
# if the patient is admitted, then appointments should be allowed against the admission service unit,
# inspite of it being an Inpatient Occupancy service unit
if service_unit:
query += " and (allow_appointments = 1 or name = {service_unit})".format(service_unit = frappe.db.escape(service_unit))
else:
query += " and allow_appointments = 1"
else:
query += " and allow_appointments = 1"
return frappe.db.sql(query, filters)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_tax_template(doctype, txt, searchfield, start, page_len, filters):

View File

@ -262,6 +262,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
if doc.get("is_return"):
if doc.doctype == 'Sales Invoice' or doc.doctype == 'POS Invoice':
doc.consolidated_invoice = ""
doc.set('payments', [])
for data in source.payments:
paid_amount = 0.00

View File

@ -3,7 +3,7 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cint, flt, cstr, comma_or, get_link_to_form
from frappe.utils import cint, flt, cstr, get_link_to_form, nowtime
from frappe import _, throw
from erpnext.stock.get_item_details import get_bin_details
from erpnext.stock.utils import get_incoming_rate
@ -39,7 +39,6 @@ class SellingController(StockController):
self.set_customer_address()
self.validate_for_duplicate_items()
self.validate_target_warehouse()
self.set_incoming_rate()
def set_missing_values(self, for_validate=False):
@ -302,7 +301,7 @@ class SellingController(StockController):
sales_order.update_reserved_qty(so_item_rows)
def set_incoming_rate(self):
if self.doctype not in ("Delivery Note", "Sales Invoice"):
if self.doctype not in ("Delivery Note", "Sales Invoice", "Sales Order"):
return
items = self.get("items") + (self.get("packed_items") or [])
@ -312,15 +311,26 @@ class SellingController(StockController):
d.incoming_rate = get_incoming_rate({
"item_code": d.item_code,
"warehouse": d.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1*flt(d.qty),
"serial_no": d.serial_no,
"posting_date": self.get('posting_date') or self.get('transaction_date'),
"posting_time": self.get('posting_time') or nowtime(),
"qty": -1 * flt(d.get('stock_qty') or d.get('actual_qty')),
"serial_no": d.get('serial_no'),
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
"allow_zero_valuation": d.get("allow_zero_valuation")
}, raise_error_if_no_rate=False)
# For internal transfers use incoming rate as the valuation rate
if self.is_internal_transfer():
rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate'))
if d.rate != rate:
d.rate = rate
d.discount_percentage = 0
d.discount_amount = 0
frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer")
.format(d.idx), alert=1)
elif self.get("return_against"):
# Get incoming rate of return entry from reference document
# based on original item cost as per valuation method
@ -381,7 +391,7 @@ class SellingController(StockController):
})
if item_row.warehouse:
sle.dependant_sle_voucher_detail_no = item_row.name
return sle
def set_po_nos(self, for_validate=False):
@ -449,13 +459,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)

View File

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

View File

@ -6,6 +6,7 @@ import frappe, erpnext
from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate
from frappe import _
import frappe.defaults
from collections import defaultdict
from erpnext.accounts.utils import get_fiscal_year, check_if_stock_and_account_balance_synced
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
from erpnext.controllers.accounts_controller import AccountsController
@ -23,6 +24,8 @@ class StockController(AccountsController):
self.validate_inspection()
self.validate_serialized_batch()
self.validate_customer_provided_item()
self.validate_internal_transfer()
self.validate_putaway_capacity()
def make_gl_entries(self, gl_entries=None, from_repost=False):
if self.docstatus == 2:
@ -72,6 +75,7 @@ class StockController(AccountsController):
warehouse_with_no_account = []
precision = frappe.get_precision("GL Entry", "debit_in_account_currency")
for item_row in voucher_details:
sle_list = sle_map.get(item_row.name)
if sle_list:
for sle in sle_list:
@ -216,7 +220,7 @@ class StockController(AccountsController):
""", (self.doctype, self.name), as_dict=True)
for sle in stock_ledger_entries:
stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle)
stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle)
return stock_ledger
def make_batches(self, warehouse_field):
@ -391,6 +395,84 @@ class StockController(AccountsController):
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
d.allow_zero_valuation_rate = 1
def validate_internal_transfer(self):
if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \
and self.is_internal_transfer():
self.validate_in_transit_warehouses()
self.validate_multi_currency()
self.validate_packed_items()
def validate_in_transit_warehouses(self):
if (self.doctype == 'Sales Invoice' and self.get('update_stock')) or self.doctype == 'Delivery Note':
for item in self.get('items'):
if not item.target_warehouse:
frappe.throw(_("Row {0}: Target Warehouse is mandatory for internal transfers").format(item.idx))
if (self.doctype == 'Purchase Invoice' and self.get('update_stock')) or self.doctype == 'Purchase Receipt':
for item in self.get('items'):
if not item.from_warehouse:
frappe.throw(_("Row {0}: From Warehouse is mandatory for internal transfers").format(item.idx))
def validate_multi_currency(self):
if self.currency != self.company_currency:
frappe.throw(_("Internal transfers can only be done in company's default currency"))
def validate_packed_items(self):
if self.doctype in ('Sales Invoice', 'Delivery Note Item') and self.get('packed_items'):
frappe.throw(_("Packed Items cannot be transferred internally"))
def validate_putaway_capacity(self):
# if over receipt is attempted while 'apply putaway rule' is disabled
# and if rule was applied on the transaction, validate it.
from erpnext.stock.doctype.putaway_rule.putaway_rule import get_available_putaway_capacity
valid_doctype = self.doctype in ("Purchase Receipt", "Stock Entry", "Purchase Invoice",
"Stock Reconciliation")
if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0:
valid_doctype = False
if valid_doctype:
rule_map = defaultdict(dict)
for item in self.get("items"):
warehouse_field = "t_warehouse" if self.doctype == "Stock Entry" else "warehouse"
rule = frappe.db.get_value("Putaway Rule",
{
"item_code": item.get("item_code"),
"warehouse": item.get(warehouse_field)
},
["name", "disable"], as_dict=True)
if rule:
if rule.get("disabled"): continue # dont validate for disabled rule
if self.doctype == "Stock Reconciliation":
stock_qty = flt(item.qty)
else:
stock_qty = flt(item.transfer_qty) if self.doctype == "Stock Entry" else flt(item.stock_qty)
rule_name = rule.get("name")
if not rule_map[rule_name]:
rule_map[rule_name]["warehouse"] = item.get(warehouse_field)
rule_map[rule_name]["item"] = item.get("item_code")
rule_map[rule_name]["qty_put"] = 0
rule_map[rule_name]["capacity"] = get_available_putaway_capacity(rule_name)
rule_map[rule_name]["qty_put"] += flt(stock_qty)
for rule, values in rule_map.items():
if flt(values["qty_put"]) > flt(values["capacity"]):
message = self.prepare_over_receipt_message(rule, values)
frappe.throw(msg=message, title=_("Over Receipt"))
def prepare_over_receipt_message(self, rule, values):
message = _("{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}.") \
.format(
frappe.bold(values["qty_put"]), frappe.bold(values["item"]),
frappe.bold(values["warehouse"]), frappe.bold(values["capacity"])
)
message += "<br><br>"
rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule)
message += _(" Please adjust the qty or edit {0} to proceed.").format(rule_link)
return message
def repost_future_sle_and_gle(self):
args = frappe._dict({
"posting_date": self.posting_date,

View File

@ -10,6 +10,7 @@ from erpnext.controllers.accounts_controller import validate_conversion_rate, \
validate_taxes_and_charges, validate_inclusive_tax
from erpnext.stock.get_item_details import _get_item_tax_template
from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules
from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate
class calculate_taxes_and_totals(object):
def __init__(self, doc):
@ -106,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"))
@ -758,3 +759,35 @@ def get_rounded_tax_amount(itemised_tax, precision):
for taxes in itemised_tax.values():
for tax_account in taxes:
taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision)
class init_landed_taxes_and_totals(object):
def __init__(self, doc):
self.doc = doc
self.tax_field = 'taxes' if self.doc.doctype == 'Landed Cost Voucher' else 'additional_costs'
self.set_account_currency()
self.set_exchange_rate()
self.set_amounts_in_company_currency()
def set_account_currency(self):
company_currency = erpnext.get_company_currency(self.doc.company)
for d in self.doc.get(self.tax_field):
if not d.account_currency:
account_currency = frappe.db.get_value('Account', d.expense_account, 'account_currency')
d.account_currency = account_currency or company_currency
def set_exchange_rate(self):
company_currency = erpnext.get_company_currency(self.doc.company)
for d in self.doc.get(self.tax_field):
if d.account_currency == company_currency:
d.exchange_rate = 1
elif not d.exchange_rate or d.exchange_rate == 1 or self.doc.posting_date:
d.exchange_rate = get_exchange_rate(self.doc.posting_date, account=d.expense_account,
account_currency=d.account_currency, company=self.doc.company)
if not d.exchange_rate:
frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx))
def set_amounts_in_company_currency(self):
for d in self.doc.get(self.tax_field):
d.amount = flt(d.amount, d.precision("amount"))
d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount"))

View File

@ -8,12 +8,12 @@
"is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-05-14 17:38:27.496696",
"modified": "2021-01-21 15:28:52.483839",
"modified_by": "Administrator",
"name": "Create Opportunity",
"owner": "Administrator",
"reference_document": "Opportunity",
"show_full_form": 0,
"show_full_form": 1,
"title": "Create Opportunity",
"validate_action": 1
}

View File

@ -44,6 +44,7 @@ class TestMpesaSettings(unittest.TestCase):
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})
@ -69,6 +70,8 @@ class TestMpesaSettings(unittest.TestCase):
self.assertEquals(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R")
self.assertEquals(integration_request.status, "Completed")
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):
return frappe.get_doc("Mpesa Settings", payment_gateway_name)

View File

@ -100,7 +100,6 @@ class ClinicalProcedure(Document):
allow_start = self.set_actual_qty()
if allow_start:
self.db_set('status', 'In Progress')
insert_clinical_procedure_to_medical_record(self)
return 'success'
return 'insufficient stock'
@ -247,21 +246,3 @@ def make_procedure(source_name, target_doc=None):
}, target_doc, set_missing_values)
return doc
def insert_clinical_procedure_to_medical_record(doc):
subject = frappe.bold(_("Clinical Procedure conducted: ")) + cstr(doc.procedure_template) + "<br>"
if doc.practitioner:
subject += frappe.bold(_('Healthcare Practitioner: ')) + doc.practitioner
if subject and doc.notes:
subject += '<br/>' + doc.notes
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.start_date
medical_record.reference_doctype = 'Clinical Procedure'
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.save(ignore_permissions=True)

View File

@ -264,7 +264,7 @@ def get_filters(entry):
def get_current_healthcare_service_unit(inpatient_record):
ip_record = frappe.get_doc('Inpatient Record', inpatient_record)
if ip_record.inpatient_occupancies:
if ip_record.status in ['Admitted', 'Discharge Scheduled'] and ip_record.inpatient_occupancies:
return ip_record.inpatient_occupancies[-1].service_unit
return

View File

@ -142,11 +142,15 @@ def create_inpatient(patient):
return inpatient_record
def get_healthcare_service_unit():
service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1})
def get_healthcare_service_unit(unit_name=None):
if not unit_name:
service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1})
else:
service_unit = frappe.db.exists("Healthcare Service Unit", {"healthcare_service_unit_name": unit_name})
if not service_unit:
service_unit = frappe.new_doc("Healthcare Service Unit")
service_unit.healthcare_service_unit_name = "Test Service Unit Ip Occupancy"
service_unit.healthcare_service_unit_name = unit_name or "Test Service Unit Ip Occupancy"
service_unit.company = "_Test Company"
service_unit.service_unit_type = get_service_unit_type()
service_unit.inpatient_occupancy = 1

View File

@ -359,6 +359,7 @@
{
"fieldname": "normal_test_items",
"fieldtype": "Table",
"label": "Normal Test Result",
"options": "Normal Test Result",
"print_hide": 1
},
@ -380,6 +381,7 @@
{
"fieldname": "sensitivity_test_items",
"fieldtype": "Table",
"label": "Sensitivity Test Result",
"options": "Sensitivity Test Result",
"print_hide": 1,
"report_hide": 1
@ -529,6 +531,7 @@
{
"fieldname": "descriptive_test_items",
"fieldtype": "Table",
"label": "Descriptive Test Result",
"options": "Descriptive Test Result",
"print_hide": 1,
"report_hide": 1
@ -549,13 +552,14 @@
{
"fieldname": "organism_test_items",
"fieldtype": "Table",
"label": "Organism Test Result",
"options": "Organism Test Result",
"print_hide": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2020-07-30 18:18:38.516215",
"modified": "2020-11-30 11:04:17.195848",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Lab Test",

View File

@ -17,11 +17,9 @@ class LabTest(Document):
self.validate_result_values()
self.db_set('submitted_date', getdate())
self.db_set('status', 'Completed')
insert_lab_test_to_medical_record(self)
def on_cancel(self):
self.db_set('status', 'Cancelled')
delete_lab_test_from_medical_record(self)
self.reload()
def on_update(self):
@ -330,60 +328,6 @@ def get_employee_by_user_id(user_id):
return frappe.get_doc('Employee', emp_id)
return None
def insert_lab_test_to_medical_record(doc):
table_row = False
subject = cstr(doc.lab_test_name)
if doc.practitioner:
subject += frappe.bold(_('Healthcare Practitioner: '))+ doc.practitioner + '<br>'
if doc.normal_test_items:
item = doc.normal_test_items[0]
comment = ''
if item.lab_test_comment:
comment = str(item.lab_test_comment)
table_row = frappe.bold(_('Lab Test Conducted: ')) + item.lab_test_name
if item.lab_test_event:
table_row += frappe.bold(_('Lab Test Event: ')) + item.lab_test_event
if item.result_value:
table_row += ' ' + frappe.bold(_('Lab Test Result: ')) + item.result_value
if item.normal_range:
table_row += ' ' + _('Normal Range: ') + item.normal_range
table_row += ' ' + comment
elif doc.descriptive_test_items:
item = doc.descriptive_test_items[0]
if item.lab_test_particulars and item.result_value:
table_row = item.lab_test_particulars + ' ' + item.result_value
elif doc.sensitivity_test_items:
item = doc.sensitivity_test_items[0]
if item.antibiotic and item.antibiotic_sensitivity:
table_row = item.antibiotic + ' ' + item.antibiotic_sensitivity
if table_row:
subject += '<br>' + table_row
if doc.lab_test_comment:
subject += '<br>' + cstr(doc.lab_test_comment)
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.result_date
medical_record.reference_doctype = 'Lab Test'
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.save(ignore_permissions = True)
def delete_lab_test_from_medical_record(self):
medical_record_id = frappe.db.sql('select name from `tabPatient Medical Record` where reference_name=%s', (self.name))
if medical_record_id and medical_record_id[0][0]:
frappe.delete_doc('Patient Medical Record', medical_record_id[0][0])
@frappe.whitelist()
def get_lab_test_prescribed(patient):

View File

@ -31,12 +31,12 @@ frappe.ui.form.on('Patient Appointment', {
};
});
frm.set_query('service_unit', function(){
frm.set_query('service_unit', function() {
return {
query: 'erpnext.controllers.queries.get_healthcare_service_units',
filters: {
'is_group': false,
'allow_appointments': true,
'company': frm.doc.company
company: frm.doc.company,
inpatient_record: frm.doc.inpatient_record
}
};
});

View File

@ -18,6 +18,7 @@ from erpnext.healthcare.utils import check_fee_validity, get_service_item_and_pr
class PatientAppointment(Document):
def validate(self):
self.validate_overlaps()
self.validate_service_unit()
self.set_appointment_datetime()
self.validate_customer_created()
self.set_status()
@ -68,6 +69,19 @@ class PatientAppointment(Document):
overlaps[0][1], overlaps[0][2], overlaps[0][3], overlaps[0][4])
frappe.throw(overlapping_details, title=_('Appointments Overlapping'))
def validate_service_unit(self):
if self.inpatient_record and self.service_unit:
from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_current_healthcare_service_unit
is_inpatient_occupancy_unit = frappe.db.get_value('Healthcare Service Unit', self.service_unit,
'inpatient_occupancy')
service_unit = get_current_healthcare_service_unit(self.inpatient_record)
if is_inpatient_occupancy_unit and service_unit != self.service_unit:
msg = _('Patient {0} is not admitted in the service unit {1}').format(frappe.bold(self.patient), frappe.bold(self.service_unit)) + '<br>'
msg += _('Appointment for service units with Inpatient Occupancy can only be created against the unit where patient has been admitted.')
frappe.throw(msg, title=_('Invalid Healthcare Service Unit'))
def set_appointment_datetime(self):
self.appointment_datetime = "%s %s" % (self.appointment_date, self.appointment_time or "00:00:00")

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import unittest
import frappe
from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter
from frappe.utils import nowdate, add_days
from frappe.utils import nowdate, add_days, now_datetime
from frappe.utils.make_random import get_random
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
@ -78,6 +78,59 @@ class TestPatientAppointment(unittest.TestCase):
sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'status'), 'Cancelled')
def test_appointment_booking_for_admission_service_unit(self):
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import \
create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
frappe.db.sql("""delete from `tabInpatient Record`""")
patient, medical_department, practitioner = create_healthcare_docs()
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
ip_record.expected_length_of_stay = 0
ip_record.save(ignore_permissions = True)
# Admit
service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
admit_patient(ip_record, service_unit, now_datetime())
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit)
self.assertEqual(appointment.service_unit, service_unit)
# Discharge
schedule_discharge(frappe.as_json({'patient': patient}))
ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name)
mark_invoiced_inpatient_occupancy(ip_record1)
discharge_patient(ip_record1)
def test_invalid_healthcare_service_unit_validation(self):
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import \
create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
frappe.db.sql("""delete from `tabInpatient Record`""")
patient, medical_department, practitioner = create_healthcare_docs()
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
ip_record.expected_length_of_stay = 0
ip_record.save(ignore_permissions = True)
# Admit
service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
admit_patient(ip_record, service_unit, now_datetime())
appointment_service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy for Appointment')
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=appointment_service_unit, save=0)
self.assertRaises(frappe.exceptions.ValidationError, appointment.save)
# Discharge
schedule_discharge(frappe.as_json({'patient': patient}))
ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name)
mark_invoiced_inpatient_occupancy(ip_record1)
discharge_patient(ip_record1)
def create_healthcare_docs():
patient = create_patient()
@ -125,7 +178,7 @@ def create_encounter(appointment):
encounter.submit()
return encounter
def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0):
def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0, service_unit=None, save=1):
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)
@ -136,12 +189,15 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce
appointment.appointment_date = appointment_date
appointment.company = '_Test Company'
appointment.duration = 15
if service_unit:
appointment.service_unit = service_unit
if invoice:
appointment.mode_of_payment = 'Cash'
appointment.paid_amount = 500
if procedure_template:
appointment.procedure_template = create_clinical_procedure_template().get('name')
appointment.save(ignore_permissions=True)
if save:
appointment.save(ignore_permissions=True)
return appointment
def create_healthcare_service_items():
@ -152,6 +208,7 @@ def create_healthcare_service_items():
item.item_name = 'Consulting Charges'
item.item_group = 'Services'
item.is_stock_item = 0
item.stock_uom = 'Nos'
item.save()
return item.name

View File

@ -210,7 +210,7 @@
{
"fieldname": "drug_prescription",
"fieldtype": "Table",
"label": "Items",
"label": "Drug Prescription",
"options": "Drug Prescription"
},
{
@ -328,7 +328,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2020-05-16 21:00:08.644531",
"modified": "2020-11-30 10:39:00.783119",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient Encounter",

View File

@ -17,10 +17,6 @@ class PatientEncounter(Document):
def on_update(self):
if self.appointment:
frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed')
update_encounter_medical_record(self)
def after_insert(self):
insert_encounter_to_medical_record(self)
def on_submit(self):
if self.therapies:
@ -33,8 +29,6 @@ class PatientEncounter(Document):
if self.inpatient_record and self.drug_prescription:
delete_ip_medication_order(self)
delete_medical_record(self)
def set_title(self):
self.title = _('{0} with {1}').format(self.patient_name or self.patient,
self.practitioner_name or self.practitioner)[:100]
@ -102,61 +96,7 @@ def create_therapy_plan(encounter):
frappe.msgprint(_('Therapy Plan {0} created successfully.').format(frappe.bold(doc.name)), alert=True)
def insert_encounter_to_medical_record(doc):
subject = set_subject_field(doc)
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.encounter_date
medical_record.reference_doctype = 'Patient Encounter'
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.save(ignore_permissions=True)
def update_encounter_medical_record(encounter):
medical_record_id = frappe.db.exists('Patient Medical Record', {'reference_name': encounter.name})
if medical_record_id and medical_record_id[0][0]:
subject = set_subject_field(encounter)
frappe.db.set_value('Patient Medical Record', medical_record_id[0][0], 'subject', subject)
else:
insert_encounter_to_medical_record(encounter)
def delete_medical_record(encounter):
record = frappe.db.exists('Patient Medical Record', {'reference_name', encounter.name})
if record:
frappe.delete_doc('Patient Medical Record', record, force=1)
def delete_ip_medication_order(encounter):
record = frappe.db.exists('Inpatient Medication Order', {'patient_encounter': encounter.name})
if record:
frappe.delete_doc('Inpatient Medication Order', record, force=1)
def set_subject_field(encounter):
subject = frappe.bold(_('Healthcare Practitioner: ')) + encounter.practitioner + '<br>'
if encounter.symptoms:
subject += frappe.bold(_('Symptoms: ')) + '<br>'
for entry in encounter.symptoms:
subject += cstr(entry.complaint) + '<br>'
else:
subject += frappe.bold(_('No Symptoms')) + '<br>'
if encounter.diagnosis:
subject += frappe.bold(_('Diagnosis: ')) + '<br>'
for entry in encounter.diagnosis:
subject += cstr(entry.diagnosis) + '<br>'
else:
subject += frappe.bold(_('No Diagnosis')) + '<br>'
if encounter.drug_prescription:
subject += '<br>' + _('Drug(s) Prescribed.')
if encounter.lab_test_prescription:
subject += '<br>' + _('Test(s) Prescribed.')
if encounter.procedure_prescription:
subject += '<br>' + _('Procedure(s) Prescribed.')
return subject
frappe.delete_doc('Inpatient Medication Order', record, force=1)

View File

@ -0,0 +1,55 @@
{
"actions": [],
"creation": "2020-11-25 13:40:23.054469",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"date_fieldname",
"add_edit_fields",
"selected_fields"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "selected_fields",
"fieldtype": "Code",
"label": "Selected Fields",
"read_only": 1
},
{
"fieldname": "add_edit_fields",
"fieldtype": "Button",
"in_list_view": 1,
"label": "Add / Edit Fields"
},
{
"fieldname": "date_fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Date Fieldname",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-30 13:54:37.474671",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient History Custom Document Type",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class PatientHistoryCustomDocumentType(Document):
pass

View File

@ -0,0 +1,133 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Patient History Settings', {
refresh: function(frm) {
frm.set_query('document_type', 'custom_doctypes', () => {
return {
filters: {
custom: 1,
is_submittable: 1,
module: 'Healthcare',
}
};
});
},
field_selector: function(frm, doc, standard=1) {
let document_fields = [];
if (doc.selected_fields)
document_fields = (JSON.parse(doc.selected_fields)).map(f => f.fieldname);
frm.call({
method: 'get_doctype_fields',
doc: frm.doc,
args: {
document_type: doc.document_type,
fields: document_fields
},
freeze: true,
callback: function(r) {
if (r.message) {
let doctype = 'Patient History Custom Document Type';
if (standard)
doctype = 'Patient History Standard Document Type';
frm.events.show_field_selector_dialog(frm, doc, doctype, r.message);
}
}
});
},
show_field_selector_dialog: function(frm, doc, doctype, doc_fields) {
let d = new frappe.ui.Dialog({
title: __('{0} Fields', [__(doc.document_type)]),
fields: [
{
label: __('Select Fields'),
fieldtype: 'MultiCheck',
fieldname: 'fields',
options: doc_fields,
columns: 2
}
]
});
d.$body.prepend(`
<div class="columns-search">
<input type="text" placeholder="${__('Search')}" data-element="search" class="form-control input-xs">
</div>`
);
frappe.utils.setup_search(d.$body, '.unit-checkbox', '.label-area');
d.set_primary_action(__('Save'), () => {
let values = d.get_values().fields;
let selected_fields = [];
frappe.model.with_doctype(doc.document_type, function() {
for (let idx in values) {
let value = values[idx];
let field = frappe.get_meta(doc.document_type).fields.filter((df) => df.fieldname == value)[0];
if (field) {
selected_fields.push({
label: field.label,
fieldname: field.fieldname,
fieldtype: field.fieldtype
});
}
}
d.refresh();
frappe.model.set_value(doctype, doc.name, 'selected_fields', JSON.stringify(selected_fields));
});
d.hide();
});
d.show();
},
get_date_field_for_dt: function(frm, row) {
frm.call({
method: 'get_date_field_for_dt',
doc: frm.doc,
args: {
document_type: row.document_type
},
callback: function(data) {
if (data.message) {
frappe.model.set_value('Patient History Custom Document Type',
row.name, 'date_fieldname', data.message);
}
}
});
}
});
frappe.ui.form.on('Patient History Custom Document Type', {
document_type: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.document_type) {
frm.events.get_date_field_for_dt(frm, row);
}
},
add_edit_fields: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.document_type) {
frm.events.field_selector(frm, row, 0);
}
}
});
frappe.ui.form.on('Patient History Standard Document Type', {
add_edit_fields: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.document_type) {
frm.events.field_selector(frm, row);
}
}
});

View File

@ -0,0 +1,55 @@
{
"actions": [],
"creation": "2020-11-25 13:41:37.675518",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"standard_doctypes",
"section_break_2",
"custom_doctypes"
],
"fields": [
{
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"fieldname": "custom_doctypes",
"fieldtype": "Table",
"label": "Custom Document Types",
"options": "Patient History Custom Document Type"
},
{
"fieldname": "standard_doctypes",
"fieldtype": "Table",
"label": "Standard Document Types",
"options": "Patient History Standard Document Type",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-11-25 13:43:38.511771",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient History Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,188 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import json
from frappe import _
from frappe.utils import cstr, cint
from frappe.model.document import Document
from erpnext.healthcare.page.patient_history.patient_history import get_patient_history_doctypes
class PatientHistorySettings(Document):
def validate(self):
self.validate_submittable_doctypes()
self.validate_date_fieldnames()
def validate_submittable_doctypes(self):
for entry in self.custom_doctypes:
if not cint(frappe.db.get_value('DocType', entry.document_type, 'is_submittable')):
msg = _('Row #{0}: Document Type {1} is not submittable. ').format(
entry.idx, frappe.bold(entry.document_type))
msg += _('Patient Medical Record can only be created for submittable document types.')
frappe.throw(msg)
def validate_date_fieldnames(self):
for entry in self.custom_doctypes:
field = frappe.get_meta(entry.document_type).get_field(entry.date_fieldname)
if not field:
frappe.throw(_('Row #{0}: No such Field named {1} found in the Document Type {2}.').format(
entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type)))
if field.fieldtype not in ['Date', 'Datetime']:
frappe.throw(_('Row #{0}: Field {1} in Document Type {2} is not a Date / Datetime field.').format(
entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type)))
def get_doctype_fields(self, document_type, fields):
multicheck_fields = []
doc_fields = frappe.get_meta(document_type).fields
for field in doc_fields:
if field.fieldtype not in frappe.model.no_value_fields or \
field.fieldtype in frappe.model.table_fields and not field.hidden:
multicheck_fields.append({
'label': field.label,
'value': field.fieldname,
'checked': 1 if field.fieldname in fields else 0
})
return multicheck_fields
def get_date_field_for_dt(self, document_type):
meta = frappe.get_meta(document_type)
date_fields = meta.get('fields', {
'fieldtype': ['in', ['Date', 'Datetime']]
})
if date_fields:
return date_fields[0].get('fieldname')
def create_medical_record(doc, method=None):
medical_record_required = validate_medical_record_required(doc)
if not medical_record_required:
return
if frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name }):
return
subject = set_subject_field(doc)
date_field = get_date_field(doc.doctype)
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.get(date_field)
medical_record.reference_doctype = doc.doctype
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.save(ignore_permissions=True)
def update_medical_record(doc, method=None):
medical_record_required = validate_medical_record_required(doc)
if not medical_record_required:
return
medical_record_id = frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name })
if medical_record_id:
subject = set_subject_field(doc)
frappe.db.set_value('Patient Medical Record', medical_record_id[0][0], 'subject', subject)
else:
create_medical_record(doc)
def delete_medical_record(doc, method=None):
medical_record_required = validate_medical_record_required(doc)
if not medical_record_required:
return
record = frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name })
if record:
frappe.delete_doc('Patient Medical Record', record, force=1)
def set_subject_field(doc):
from frappe.utils.formatters import format_value
meta = frappe.get_meta(doc.doctype)
subject = ''
patient_history_fields = get_patient_history_fields(doc)
for entry in patient_history_fields:
fieldname = entry.get('fieldname')
if entry.get('fieldtype') == 'Table' and doc.get(fieldname):
formatted_value = get_formatted_value_for_table_field(doc.get(fieldname), meta.get_field(fieldname))
subject += frappe.bold(_(entry.get('label')) + ': ') + '<br>' + cstr(formatted_value) + '<br>'
else:
if doc.get(fieldname):
formatted_value = format_value(doc.get(fieldname), meta.get_field(fieldname), doc)
subject += frappe.bold(_(entry.get('label')) + ': ') + cstr(formatted_value) + '<br>'
return subject
def get_date_field(doctype):
dt = get_patient_history_config_dt(doctype)
return frappe.db.get_value(dt, { 'document_type': doctype }, 'date_fieldname')
def get_patient_history_fields(doc):
dt = get_patient_history_config_dt(doc.doctype)
patient_history_fields = frappe.db.get_value(dt, { 'document_type': doc.doctype }, 'selected_fields')
if patient_history_fields:
return json.loads(patient_history_fields)
def get_formatted_value_for_table_field(items, df):
child_meta = frappe.get_meta(df.options)
table_head = ''
table_row = ''
html = ''
create_head = True
for item in items:
table_row += '<tr>'
for cdf in child_meta.fields:
if cdf.in_list_view:
if create_head:
table_head += '<td>' + cdf.label + '</td>'
if item.get(cdf.fieldname):
table_row += '<td>' + str(item.get(cdf.fieldname)) + '</td>'
else:
table_row += '<td></td>'
create_head = False
table_row += '</tr>'
html += "<table class='table table-condensed table-bordered'>" + table_head + table_row + "</table>"
return html
def get_patient_history_config_dt(doctype):
if frappe.db.get_value('DocType', doctype, 'custom'):
return 'Patient History Custom Document Type'
else:
return 'Patient History Standard Document Type'
def validate_medical_record_required(doc):
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard \
or get_module(doc) != 'Healthcare':
return False
if doc.doctype not in get_patient_history_doctypes():
return False
return True
def get_module(doc):
module = doc.meta.module
if not module:
module = frappe.db.get_value('DocType', doc.doctype, 'module')
return module

View File

@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
import json
from frappe.utils import getdate
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient
class TestPatientHistorySettings(unittest.TestCase):
def setUp(self):
dt = create_custom_doctype()
settings = frappe.get_single("Patient History Settings")
settings.append("custom_doctypes", {
"document_type": dt.name,
"date_fieldname": "date",
"selected_fields": json.dumps([{
"label": "Date",
"fieldname": "date",
"fieldtype": "Date"
},
{
"label": "Rating",
"fieldname": "rating",
"fieldtype": "Rating"
},
{
"label": "Feedback",
"fieldname": "feedback",
"fieldtype": "Small Text"
}])
})
settings.save()
def test_custom_doctype_medical_record(self):
# tests for medical record creation of standard doctypes in test_patient_medical_record.py
patient = create_patient()
doc = create_doc(patient)
# check for medical record
medical_rec = frappe.db.exists("Patient Medical Record", {"status": "Open", "reference_name": doc.name})
self.assertTrue(medical_rec)
medical_rec = frappe.get_doc("Patient Medical Record", medical_rec)
expected_subject = "<b>Date: </b>{0}<br><b>Rating: </b>3<br><b>Feedback: </b>Test Patient History Settings<br>".format(
frappe.utils.format_date(getdate()))
self.assertEqual(medical_rec.subject, expected_subject)
self.assertEqual(medical_rec.patient, patient)
self.assertEqual(medical_rec.communication_date, getdate())
def create_custom_doctype():
if not frappe.db.exists("DocType", "Test Patient Feedback"):
doc = frappe.get_doc({
"doctype": "DocType",
"module": "Healthcare",
"custom": 1,
"is_submittable": 1,
"fields": [{
"label": "Date",
"fieldname": "date",
"fieldtype": "Date"
},
{
"label": "Patient",
"fieldname": "patient",
"fieldtype": "Link",
"options": "Patient"
},
{
"label": "Rating",
"fieldname": "rating",
"fieldtype": "Rating"
},
{
"label": "Feedback",
"fieldname": "feedback",
"fieldtype": "Small Text"
}],
"permissions": [{
"role": "System Manager",
"read": 1
}],
"name": "Test Patient Feedback",
})
doc.insert()
return doc
else:
return frappe.get_doc("DocType", "Test Patient Feedback")
def create_doc(patient):
doc = frappe.get_doc({
"doctype": "Test Patient Feedback",
"patient": patient,
"date": getdate(),
"rating": 3,
"feedback": "Test Patient History Settings"
}).insert()
doc.submit()
return doc

View File

@ -0,0 +1,57 @@
{
"actions": [],
"creation": "2020-11-25 13:39:36.014814",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"date_fieldname",
"add_edit_fields",
"selected_fields"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "selected_fields",
"fieldtype": "Code",
"label": "Selected Fields",
"read_only": 1
},
{
"fieldname": "add_edit_fields",
"fieldtype": "Button",
"in_list_view": 1,
"label": "Add / Edit Fields"
},
{
"fieldname": "date_fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Date Fieldname",
"read_only": 1,
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-30 13:54:56.773325",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient History Standard Document Type",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class PatientHistoryStandardDocumentType(Document):
pass

View File

@ -18,6 +18,7 @@ class TestPatientMedicalRecord(unittest.TestCase):
patient, medical_department, practitioner = create_healthcare_docs()
appointment = create_appointment(patient, practitioner, nowdate(), invoice=1)
encounter = create_encounter(appointment)
# check for encounter
medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': encounter.name})
self.assertTrue(medical_rec)

View File

@ -41,7 +41,6 @@ class TherapySession(Document):
def on_submit(self):
self.update_sessions_count_in_therapy_plan()
insert_session_medical_record(self)
def on_update(self):
if self.appointment:
@ -142,23 +141,3 @@ def get_therapy_item(therapy, item):
item.reference_dt = 'Therapy Session'
item.reference_dn = therapy.name
return item
def insert_session_medical_record(doc):
subject = frappe.bold(_('Therapy: ')) + cstr(doc.therapy_type) + '<br>'
if doc.therapy_plan:
subject += frappe.bold(_('Therapy Plan: ')) + cstr(doc.therapy_plan) + '<br>'
if doc.practitioner:
subject += frappe.bold(_('Healthcare Practitioner: ')) + doc.practitioner
subject += frappe.bold(_('Total Counts Targeted: ')) + cstr(doc.total_counts_targeted) + '<br>'
subject += frappe.bold(_('Total Counts Completed: ')) + cstr(doc.total_counts_completed) + '<br>'
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.start_date
medical_record.reference_doctype = 'Therapy Session'
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.save(ignore_permissions=True)

View File

@ -12,47 +12,7 @@ class VitalSigns(Document):
def validate(self):
self.set_title()
def on_submit(self):
insert_vital_signs_to_medical_record(self)
def on_cancel(self):
delete_vital_signs_from_medical_record(self)
def set_title(self):
self.title = _('{0} on {1}').format(self.patient_name or self.patient,
frappe.utils.format_date(self.signs_date))[:100]
def insert_vital_signs_to_medical_record(doc):
subject = set_subject_field(doc)
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.signs_date
medical_record.reference_doctype = 'Vital Signs'
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.flags.ignore_mandatory = True
medical_record.save(ignore_permissions=True)
def delete_vital_signs_from_medical_record(doc):
medical_record = frappe.db.get_value('Patient Medical Record', {'reference_name': doc.name})
if medical_record:
frappe.delete_doc('Patient Medical Record', medical_record)
def set_subject_field(doc):
subject = ''
if doc.temperature:
subject += frappe.bold(_('Temperature: ')) + cstr(doc.temperature) + '<br>'
if doc.pulse:
subject += frappe.bold(_('Pulse: ')) + cstr(doc.pulse) + '<br>'
if doc.respiratory_rate:
subject += frappe.bold(_('Respiratory Rate: ')) + cstr(doc.respiratory_rate) + '<br>'
if doc.bp:
subject += frappe.bold(_('BP: ')) + cstr(doc.bp) + '<br>'
if doc.bmi:
subject += frappe.bold(_('BMI: ')) + cstr(doc.bmi) + '<br>'
if doc.nutrition_note:
subject += frappe.bold(_('Note: ')) + cstr(doc.nutrition_note) + '<br>'
return subject

View File

@ -109,6 +109,11 @@
padding-right: 0px;
}
.patient-history-filter {
margin-left: 35px;
width: 25%;
}
#page-medical_record .plot-wrapper {
padding: 20px 15px;
border-bottom: 1px solid #d1d8dd;

View File

@ -1,6 +1,5 @@
<div class="col-sm-12">
<div class="col-sm-3">
<p class="text-center">{%= __("Select Patient") %}</p>
<p class="patient" style="margin: auto; max-width: 300px; margin-bottom: 20px;"></p>
<div class="patient_details" style="z-index=0"></div>
</div>
@ -11,6 +10,13 @@
<div id="chart" class="col-sm-12 patient_vital_charts">
</div>
</div>
<div class="header-separator col-sm-12 d-flex border-bottom py-3" style="display:none"></div>
<div class="row">
<div class="col-sm-12 d-flex">
<div class="patient-history-filter doctype-filter"></div>
<div class="patient-history-filter date-filter"></div>
</div>
</div>
<div class="col-sm-12 patient_documents_list">
</div>
<div class="col-sm-12 text-center py-3">

View File

@ -1,141 +1,225 @@
frappe.provide("frappe.patient_history");
frappe.provide('frappe.patient_history');
frappe.pages['patient_history'].on_page_load = function(wrapper) {
var me = this;
var page = frappe.ui.make_app_page({
let me = this;
let page = frappe.ui.make_app_page({
parent: wrapper,
title: 'Patient History',
single_column: true
});
frappe.breadcrumbs.add("Healthcare");
frappe.breadcrumbs.add('Healthcare');
let pid = '';
page.main.html(frappe.render_template("patient_history", {}));
var patient = frappe.ui.form.make_control({
parent: page.main.find(".patient"),
page.main.html(frappe.render_template('patient_history', {}));
page.main.find('.header-separator').hide();
let patient = frappe.ui.form.make_control({
parent: page.main.find('.patient'),
df: {
fieldtype: "Link",
options: "Patient",
fieldname: "patient",
change: function(){
if(pid != patient.get_value() && patient.get_value()){
fieldtype: 'Link',
options: 'Patient',
fieldname: 'patient',
placeholder: __('Select Patient'),
only_select: true,
change: function() {
let patient_id = patient.get_value();
if (pid != patient_id && patient_id) {
me.start = 0;
me.page.main.find(".patient_documents_list").html("");
get_documents(patient.get_value(), me);
show_patient_info(patient.get_value(), me);
show_patient_vital_charts(patient.get_value(), me, "bp", "mmHg", "Blood Pressure");
me.page.main.find('.patient_documents_list').html('');
setup_filters(patient_id, me);
get_documents(patient_id, me);
show_patient_info(patient_id, me);
show_patient_vital_charts(patient_id, me, 'bp', 'mmHg', 'Blood Pressure');
}
pid = patient.get_value();
pid = patient_id;
}
},
only_input: true,
});
patient.refresh();
if (frappe.route_options){
if (frappe.route_options) {
patient.set_value(frappe.route_options.patient);
}
this.page.main.on("click", ".btn-show-chart", function() {
var btn_show_id = $(this).attr("data-show-chart-id"), pts = $(this).attr("data-pts");
var title = $(this).attr("data-title");
this.page.main.on('click', '.btn-show-chart', function() {
let btn_show_id = $(this).attr('data-show-chart-id'), pts = $(this).attr('data-pts');
let title = $(this).attr('data-title');
show_patient_vital_charts(patient.get_value(), me, btn_show_id, pts, title);
});
this.page.main.on("click", ".btn-more", function() {
var doctype = $(this).attr("data-doctype"), docname = $(this).attr("data-docname");
if(me.page.main.find("."+docname).parent().find('.document-html').attr('data-fetched') == "1"){
me.page.main.find("."+docname).hide();
me.page.main.find("."+docname).parent().find('.document-html').show();
}else{
if(doctype && docname){
let exclude = ["patient", "patient_name", 'patient_sex', "encounter_date"];
this.page.main.on('click', '.btn-more', function() {
let doctype = $(this).attr('data-doctype'), docname = $(this).attr('data-docname');
if (me.page.main.find('.'+docname).parent().find('.document-html').attr('data-fetched') == '1') {
me.page.main.find('.'+docname).hide();
me.page.main.find('.'+docname).parent().find('.document-html').show();
} else {
if (doctype && docname) {
let exclude = ['patient', 'patient_name', 'patient_sex', 'encounter_date'];
frappe.call({
method: "erpnext.healthcare.utils.render_doc_as_html",
method: 'erpnext.healthcare.utils.render_doc_as_html',
args:{
doctype: doctype,
docname: docname,
exclude_fields: exclude
},
freeze: true,
callback: function(r) {
if (r.message){
me.page.main.find("."+docname).hide();
me.page.main.find("."+docname).parent().find('.document-html').html(r.message.html+"\
<div align='center'><a class='btn octicon octicon-chevron-up btn-default btn-xs\
btn-less' data-doctype='"+doctype+"' data-docname='"+docname+"'></a></div>");
me.page.main.find("."+docname).parent().find('.document-html').show();
me.page.main.find("."+docname).parent().find('.document-html').attr('data-fetched', "1");
if (r.message) {
me.page.main.find('.' + docname).hide();
me.page.main.find('.' + docname).parent().find('.document-html').html(
`${r.message.html}
<div align='center'>
<a class='btn octicon octicon-chevron-up btn-default btn-xs btn-less'
data-doctype='${doctype}'
data-docname='${docname}'>
</a>
</div>
`);
me.page.main.find('.' + docname).parent().find('.document-html').show();
me.page.main.find('.' + docname).parent().find('.document-html').attr('data-fetched', '1');
}
},
freeze: true
}
});
}
}
});
this.page.main.on("click", ".btn-less", function() {
var docname = $(this).attr("data-docname");
me.page.main.find("."+docname).parent().find('.document-id').show();
me.page.main.find("."+docname).parent().find('.document-html').hide();
this.page.main.on('click', '.btn-less', function() {
let docname = $(this).attr('data-docname');
me.page.main.find('.' + docname).parent().find('.document-id').show();
me.page.main.find('.' + docname).parent().find('.document-html').hide();
});
me.start = 0;
me.page.main.on("click", ".btn-get-records", function(){
me.page.main.on('click', '.btn-get-records', function() {
get_documents(patient.get_value(), me);
});
};
var get_documents = function(patient, me){
let setup_filters = function(patient, me) {
$('.doctype-filter').empty();
frappe.xcall(
'erpnext.healthcare.page.patient_history.patient_history.get_patient_history_doctypes'
).then(document_types => {
let doctype_filter = frappe.ui.form.make_control({
parent: $('.doctype-filter'),
df: {
fieldtype: 'MultiSelectList',
fieldname: 'document_type',
placeholder: __('Select Document Type'),
input_class: 'input-xs',
change: () => {
me.start = 0;
me.page.main.find('.patient_documents_list').html('');
get_documents(patient, me, doctype_filter.get_value(), date_range_field.get_value());
},
get_data: () => {
return document_types.map(document_type => {
return {
description: document_type,
value: document_type
};
});
},
}
});
doctype_filter.refresh();
$('.date-filter').empty();
let date_range_field = frappe.ui.form.make_control({
df: {
fieldtype: 'DateRange',
fieldname: 'date_range',
placeholder: __('Date Range'),
input_class: 'input-xs',
change: () => {
let selected_date_range = date_range_field.get_value();
if (selected_date_range && selected_date_range.length === 2) {
me.start = 0;
me.page.main.find('.patient_documents_list').html('');
get_documents(patient, me, doctype_filter.get_value(), selected_date_range);
}
}
},
parent: $('.date-filter')
});
date_range_field.refresh();
});
};
let get_documents = function(patient, me, document_types="", selected_date_range="") {
let filters = {
name: patient,
start: me.start,
page_length: 20
};
if (document_types)
filters['document_types'] = document_types;
if (selected_date_range)
filters['date_range'] = selected_date_range;
frappe.call({
"method": "erpnext.healthcare.page.patient_history.patient_history.get_feed",
args: {
name: patient,
start: me.start,
page_length: 20
},
callback: function (r) {
var data = r.message;
if(data.length){
'method': 'erpnext.healthcare.page.patient_history.patient_history.get_feed',
args: filters,
callback: function(r) {
let data = r.message;
if (data.length) {
add_to_records(me, data);
}else{
me.page.main.find(".patient_documents_list").append("<div class='text-muted' align='center'><br><br>No more records..<br><br></div>");
me.page.main.find(".btn-get-records").hide();
} else {
me.page.main.find('.patient_documents_list').append(`
<div class='text-muted' align='center'>
<br><br>${__('No more records..')}<br><br>
</div>`);
me.page.main.find('.btn-get-records').hide();
}
}
});
};
var add_to_records = function(me, data){
var details = "<ul class='nav nav-pills nav-stacked'>";
var i;
for(i=0; i<data.length; i++){
if(data[i].reference_doctype){
let add_to_records = function(me, data) {
let details = "<ul class='nav nav-pills nav-stacked'>";
let i;
for (i=0; i<data.length; i++) {
if (data[i].reference_doctype) {
let label = '';
if(data[i].subject){
label += "<br/>"+data[i].subject;
if (data[i].subject) {
label += "<br/>" + data[i].subject;
}
data[i] = add_date_separator(data[i]);
if(frappe.user_info(data[i].owner).image){
if (frappe.user_info(data[i].owner).image) {
data[i].imgsrc = frappe.utils.get_file_link(frappe.user_info(data[i].owner).image);
}
else{
} else {
data[i].imgsrc = false;
}
var time_line_heading = data[i].practitioner ? `${data[i].practitioner} ` : ``;
time_line_heading += data[i].reference_doctype + " - "+ data[i].reference_name;
details += `<li data-toggle='pill' class='patient_doc_menu'
data-doctype='${data[i].reference_doctype}' data-docname='${data[i].reference_name}'>
<div class='col-sm-12 d-flex border-bottom py-3'>`;
if (data[i].imgsrc){
details += `<span class='mr-3'>
<img class='avtar' src='${data[i].imgsrc}' width='32' height='32'>
</img>
</span>`;
}else{
details += `<span class='mr-3 avatar avatar-small' style='width:32px; height:32px;'><div align='center' class='standard-image'
style='background-color: #fafbfc;'>${data[i].practitioner ? data[i].practitioner.charAt(0) : "U"}</div></span>`;
let time_line_heading = data[i].practitioner ? `${data[i].practitioner} ` : ``;
time_line_heading += data[i].reference_doctype + " - " +
`<a onclick="frappe.set_route('Form', '${data[i].reference_doctype}', '${data[i].reference_name}');">
${data[i].reference_name}
</a>`;
details += `
<li data-toggle='pill' class='patient_doc_menu'
data-doctype='${data[i].reference_doctype}' data-docname='${data[i].reference_name}'>
<div class='col-sm-12 d-flex border-bottom py-3'>`;
if (data[i].imgsrc) {
details += `
<span class='mr-3'>
<img class='avtar' src='${data[i].imgsrc}' width='32' height='32'></img>
</span>`;
} else {
details += `<span class='mr-3 avatar avatar-small' style='width:32px; height:32px;'>
<div align='center' class='standard-image' style='background-color: #fafbfc;'>
${data[i].practitioner ? data[i].practitioner.charAt(0) : 'U'}
</div>
</span>`;
}
details += `<div class='d-flex flex-column width-full'>
<div>
`+time_line_heading+` on
`+time_line_heading+`
<span>
${data[i].date_sep}
</span>
@ -156,133 +240,150 @@ var add_to_records = function(me, data){
</li>`;
}
}
details += "</ul>";
me.page.main.find(".patient_documents_list").append(details);
details += '</ul>';
me.page.main.find('.patient_documents_list').append(details);
me.start += data.length;
if(data.length===20){
if (data.length === 20) {
me.page.main.find(".btn-get-records").show();
}else{
} else {
me.page.main.find(".btn-get-records").hide();
me.page.main.find(".patient_documents_list").append("<div class='text-muted' align='center'><br><br>No more records..<br><br></div>");
me.page.main.find(".patient_documents_list").append(`
<div class='text-muted' align='center'>
<br><br>${__('No more records..')}<br><br>
</div>`);
}
};
var add_date_separator = function(data) {
var date = frappe.datetime.str_to_obj(data.creation);
let add_date_separator = function(data) {
let date = frappe.datetime.str_to_obj(data.communication_date);
let pdate = '';
let diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date));
var diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date));
if(diff < 1) {
var pdate = 'Today';
} else if(diff < 2) {
pdate = 'Yesterday';
if (diff < 1) {
pdate = __('Today');
} else if (diff < 2) {
pdate = __('Yesterday');
} else {
pdate = frappe.datetime.global_date_format(date);
pdate = __('on ') + frappe.datetime.global_date_format(date);
}
data.date_sep = pdate;
return data;
};
var show_patient_info = function(patient, me){
let show_patient_info = function(patient, me) {
frappe.call({
"method": "erpnext.healthcare.doctype.patient.patient.get_patient_detail",
'method': 'erpnext.healthcare.doctype.patient.patient.get_patient_detail',
args: {
patient: patient
},
callback: function (r) {
var data = r.message;
var details = "";
if(data.image){
details += "<div><img class='thumbnail' width=75% src='"+data.image+"'></div>";
callback: function(r) {
let data = r.message;
let details = '';
if (data.image) {
details += `<div><img class='thumbnail' width=75% src='${data.image}'></div>`;
}
details += "<b>" + data.patient_name +"</b><br>" + data.sex;
if(data.email) details += "<br>" + data.email;
if(data.mobile) details += "<br>" + data.mobile;
if(data.occupation) details += "<br><br><b>Occupation :</b> " + data.occupation;
if(data.blood_group) details += "<br><b>Blood group : </b> " + data.blood_group;
if(data.allergies) details += "<br><br><b>Allergies : </b> "+ data.allergies.replace("\n", "<br>");
if(data.medication) details += "<br><b>Medication : </b> "+ data.medication.replace("\n", "<br>");
if(data.alcohol_current_use) details += "<br><br><b>Alcohol use : </b> "+ data.alcohol_current_use;
if(data.alcohol_past_use) details += "<br><b>Alcohol past use : </b> "+ data.alcohol_past_use;
if(data.tobacco_current_use) details += "<br><b>Tobacco use : </b> "+ data.tobacco_current_use;
if(data.tobacco_past_use) details += "<br><b>Tobacco past use : </b> "+ data.tobacco_past_use;
if(data.medical_history) details += "<br><br><b>Medical history : </b> "+ data.medical_history.replace("\n", "<br>");
if(data.surgical_history) details += "<br><b>Surgical history : </b> "+ data.surgical_history.replace("\n", "<br>");
if(data.surrounding_factors) details += "<br><br><b>Occupational hazards : </b> "+ data.surrounding_factors.replace("\n", "<br>");
if(data.other_risk_factors) details += "<br><b>Other risk factors : </b> " + data.other_risk_factors.replace("\n", "<br>");
if(data.patient_details) details += "<br><br><b>More info : </b> " + data.patient_details.replace("\n", "<br>");
if(details){
details = "<div style='padding-left:10px; font-size:13px;' align='center'>" + details + "</div>";
details += `<b> ${data.patient_name} </b><br> ${data.sex}`;
if (data.email) details += `<br> ${data.email}`;
if (data.mobile) details += `<br> ${data.mobile}`;
if (data.occupation) details += `<br><br><b> ${__('Occupation')} : </b> ${data.occupation}`;
if (data.blood_group) details += `<br><b> ${__('Blood Group')} : </b> ${data.blood_group}`;
if (data.allergies) details += `<br><br><b> ${__('Allerigies')} : </b> ${data.allergies.replace("\n", ", ")}`;
if (data.medication) details += `<br><b> ${__('Medication')} : </b> ${data.medication.replace("\n", ", ")}`;
if (data.alcohol_current_use) details += `<br><br><b> ${__('Alcohol use')} : </b> ${data.alcohol_current_use}`;
if (data.alcohol_past_use) details += `<br><b> ${__('Alcohol past use')} : </b> ${data.alcohol_past_use}`;
if (data.tobacco_current_use) details += `<br><b> ${__('Tobacco use')} : </b> ${data.tobacco_current_use}`;
if (data.tobacco_past_use) details += `<br><b> ${__('Tobacco past use')} : </b> ${data.tobacco_past_use}`;
if (data.medical_history) details += `<br><br><b> ${__('Medical history')} : </b> ${data.medical_history.replace("\n", ", ")}`;
if (data.surgical_history) details += `<br><b> ${__('Surgical history')} : </b> ${data.surgical_history.replace("\n", ", ")}`;
if (data.surrounding_factors) details += `<br><br><b> ${__('Occupational hazards')} : </b> ${data.surrounding_factors.replace("\n", ", ")}`;
if (data.other_risk_factors) details += `<br><b> ${__('Other risk factors')} : </b> ${data.other_risk_factors.replace("\n", ", ")}`;
if (data.patient_details) details += `<br><br><b> ${__('More info')} : </b> ${data.patient_details.replace("\n", ", ")}`;
if (details) {
details = `<div style='padding-left:10px; font-size:13px;' align='left'>` + details + `</div>`;
}
me.page.main.find(".patient_details").html(details);
me.page.main.find('.patient_details').html(details);
}
});
};
var show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) {
let show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) {
frappe.call({
method: "erpnext.healthcare.utils.get_patient_vitals",
method: 'erpnext.healthcare.utils.get_patient_vitals',
args:{
patient: patient
},
callback: function(r) {
if (r.message){
var show_chart_btns_html = "<div style='padding-top:5px;'><a class='btn btn-default btn-xs btn-show-chart' \
data-show-chart-id='bp' data-pts='mmHg' data-title='Blood Pressure'>Blood Pressure</a>\
<a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='pulse_rate' \
data-pts='per Minutes' data-title='Respiratory/Pulse Rate'>Respiratory/Pulse Rate</a>\
<a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='temperature' \
data-pts='°C or °F' data-title='Temperature'>Temperature</a>\
<a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='bmi' \
data-pts='' data-title='BMI'>BMI</a></div>";
me.page.main.find(".show_chart_btns").html(show_chart_btns_html);
var data = r.message;
if (r.message) {
let show_chart_btns_html = `
<div style='padding-top:10px;'>
<a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='bp' data-pts='mmHg' data-title='Blood Pressure'>
${__('Blood Pressure')}
</a>
<a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='pulse_rate' data-pts='per Minutes' data-title='Respiratory/Pulse Rate'>
${__('Respiratory/Pulse Rate')}
</a>
<a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='temperature' data-pts='°C or °F' data-title='Temperature'>
${__('Temperature')}
</a>
<a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='bmi' data-pts='' data-title='BMI'>
${__('BMI')}
</a>
</div>`;
me.page.main.find('.show_chart_btns').html(show_chart_btns_html);
let data = r.message;
let labels = [], datasets = [];
let bp_systolic = [], bp_diastolic = [], temperature = [];
let pulse = [], respiratory_rate = [], bmi = [], height = [], weight = [];
for(var i=0; i<data.length; i++){
labels.push(data[i].signs_date+"||"+data[i].signs_time);
if(btn_show_id=="bp"){
for (let i=0; i<data.length; i++) {
labels.push(data[i].signs_date+'||'+data[i].signs_time);
if (btn_show_id === 'bp') {
bp_systolic.push(data[i].bp_systolic);
bp_diastolic.push(data[i].bp_diastolic);
}
if(btn_show_id=="temperature"){
if (btn_show_id === 'temperature') {
temperature.push(data[i].temperature);
}
if(btn_show_id=="pulse_rate"){
if (btn_show_id === 'pulse_rate') {
pulse.push(data[i].pulse);
respiratory_rate.push(data[i].respiratory_rate);
}
if(btn_show_id=="bmi"){
if (btn_show_id === 'bmi') {
bmi.push(data[i].bmi);
height.push(data[i].height);
weight.push(data[i].weight);
}
}
if(btn_show_id=="temperature"){
datasets.push({name: "Temperature", values: temperature, chartType:'line'});
if (btn_show_id === 'temperature') {
datasets.push({name: 'Temperature', values: temperature, chartType: 'line'});
}
if(btn_show_id=="bmi"){
datasets.push({name: "BMI", values: bmi, chartType:'line'});
datasets.push({name: "Height", values: height, chartType:'line'});
datasets.push({name: "Weight", values: weight, chartType:'line'});
if (btn_show_id === 'bmi') {
datasets.push({name: 'BMI', values: bmi, chartType: 'line'});
datasets.push({name: 'Height', values: height, chartType: 'line'});
datasets.push({name: 'Weight', values: weight, chartType: 'line'});
}
if(btn_show_id=="bp"){
datasets.push({name: "BP Systolic", values: bp_systolic, chartType:'line'});
datasets.push({name: "BP Diastolic", values: bp_diastolic, chartType:'line'});
if (btn_show_id === 'bp') {
datasets.push({name: 'BP Systolic', values: bp_systolic, chartType: 'line'});
datasets.push({name: 'BP Diastolic', values: bp_diastolic, chartType: 'line'});
}
if(btn_show_id=="pulse_rate"){
datasets.push({name: "Heart Rate / Pulse", values: pulse, chartType:'line'});
datasets.push({name: "Respiratory Rate", values: respiratory_rate, chartType:'line'});
if (btn_show_id === 'pulse_rate') {
datasets.push({name: 'Heart Rate / Pulse', values: pulse, chartType: 'line'});
datasets.push({name: 'Respiratory Rate', values: respiratory_rate, chartType: 'line'});
}
new frappe.Chart( ".patient_vital_charts", {
new frappe.Chart('.patient_vital_charts', {
data: {
labels: labels,
datasets: datasets
},
title: title,
type: 'axis-mixed', // 'axis-mixed', 'bar', 'line', 'pie', 'percentage'
type: 'axis-mixed',
height: 200,
colors: ['purple', '#ffa3ef', 'light-blue'],
@ -291,9 +392,11 @@ var show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) {
formatTooltipY: d => d + ' ' + pts,
}
});
}else{
me.page.main.find(".patient_vital_charts").html("");
me.page.main.find(".show_chart_btns").html("");
me.page.main.find('.header-separator').show();
} else {
me.page.main.find('.patient_vital_charts').html('');
me.page.main.find('.show_chart_btns').html('');
me.page.main.find('.header-separator').hide();
}
}
});

View File

@ -4,36 +4,70 @@
from __future__ import unicode_literals
import frappe
import json
from frappe.utils import cint
from erpnext.healthcare.utils import render_docs_as_html
@frappe.whitelist()
def get_feed(name, start=0, page_length=20):
def get_feed(name, document_types=None, date_range=None, start=0, page_length=20):
"""get feed"""
result = frappe.db.sql("""select name, owner, creation,
reference_doctype, reference_name, subject
from `tabPatient Medical Record`
where patient=%(patient)s
order by creation desc
limit %(start)s, %(page_length)s""",
{
"patient": name,
"start": cint(start),
"page_length": cint(page_length)
}, as_dict=True)
filters = get_filters(name, document_types, date_range)
result = frappe.db.get_all('Patient Medical Record',
fields=['name', 'owner', 'communication_date',
'reference_doctype', 'reference_name', 'subject'],
filters=filters,
order_by='communication_date DESC',
limit=cint(page_length),
start=cint(start)
)
return result
def get_filters(name, document_types=None, date_range=None):
filters = {'patient': name}
if document_types:
document_types = json.loads(document_types)
if len(document_types):
filters['reference_doctype'] = ['IN', document_types]
if date_range:
try:
date_range = json.loads(date_range)
if date_range:
filters['communication_date'] = ['between', [date_range[0], date_range[1]]]
except json.decoder.JSONDecodeError:
pass
return filters
@frappe.whitelist()
def get_feed_for_dt(doctype, docname):
"""get feed"""
result = frappe.db.sql("""select name, owner, modified, creation,
reference_doctype, reference_name, subject
from `tabPatient Medical Record`
where reference_name=%(docname)s and reference_doctype=%(doctype)s
order by creation desc""",
{
"docname": docname,
"doctype": doctype
}, as_dict=True)
result = frappe.db.get_all('Patient Medical Record',
fields=['name', 'owner', 'communication_date',
'reference_doctype', 'reference_name', 'subject'],
filters={
'reference_doctype': doctype,
'reference_name': docname
},
order_by='communication_date DESC'
)
return result
@frappe.whitelist()
def get_patient_history_doctypes():
document_types = []
settings = frappe.get_single("Patient History Settings")
for entry in settings.standard_doctypes:
document_types.append(entry.document_type)
for entry in settings.custom_doctypes:
document_types.append(entry.document_type)
return document_types

View File

@ -16,6 +16,7 @@ def setup_healthcare():
create_healthcare_item_groups()
create_sensitivity()
add_healthcare_service_unit_tree_root()
setup_patient_history_settings()
def create_medical_departments():
departments = [
@ -213,3 +214,82 @@ def get_company():
if company:
return company[0].name
return None
def setup_patient_history_settings():
import json
settings = frappe.get_single('Patient History Settings')
configuration = get_patient_history_config()
for dt, config in configuration.items():
settings.append("standard_doctypes", {
"document_type": dt,
"date_fieldname": config[0],
"selected_fields": json.dumps(config[1])
})
settings.save()
def get_patient_history_config():
return {
"Patient Encounter": ("encounter_date", [
{"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
{"label": "Symptoms", "fieldname": "symptoms", "fieldtype": "Table Multiselect"},
{"label": "Diagnosis", "fieldname": "diagnosis", "fieldtype": "Table Multiselect"},
{"label": "Drug Prescription", "fieldname": "drug_prescription", "fieldtype": "Table"},
{"label": "Lab Tests", "fieldname": "lab_test_prescription", "fieldtype": "Table"},
{"label": "Clinical Procedures", "fieldname": "procedure_prescription", "fieldtype": "Table"},
{"label": "Therapies", "fieldname": "therapies", "fieldtype": "Table"},
{"label": "Review Details", "fieldname": "encounter_comment", "fieldtype": "Small Text"}
]),
"Clinical Procedure": ("start_date", [
{"label": "Procedure Template", "fieldname": "procedure_template", "fieldtype": "Link"},
{"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
{"label": "Notes", "fieldname": "notes", "fieldtype": "Small Text"},
{"label": "Service Unit", "fieldname": "service_unit", "fieldtype": "Healthcare Service Unit"},
{"label": "Start Time", "fieldname": "start_time", "fieldtype": "Time"},
{"label": "Sample", "fieldname": "sample", "fieldtype": "Link"}
]),
"Lab Test": ("result_date", [
{"label": "Test Template", "fieldname": "template", "fieldtype": "Link"},
{"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
{"label": "Test Name", "fieldname": "lab_test_name", "fieldtype": "Data"},
{"label": "Lab Technician Name", "fieldname": "employee_name", "fieldtype": "Data"},
{"label": "Sample ID", "fieldname": "sample", "fieldtype": "Link"},
{"label": "Normal Test Result", "fieldname": "normal_test_items", "fieldtype": "Table"},
{"label": "Descriptive Test Result", "fieldname": "descriptive_test_items", "fieldtype": "Table"},
{"label": "Organism Test Result", "fieldname": "organism_test_items", "fieldtype": "Table"},
{"label": "Sensitivity Test Result", "fieldname": "sensitivity_test_items", "fieldtype": "Table"},
{"label": "Comments", "fieldname": "lab_test_comment", "fieldtype": "Table"}
]),
"Therapy Session": ("start_date", [
{"label": "Therapy Type", "fieldname": "therapy_type", "fieldtype": "Link"},
{"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
{"label": "Therapy Plan", "fieldname": "therapy_plan", "fieldtype": "Link"},
{"label": "Duration", "fieldname": "duration", "fieldtype": "Int"},
{"label": "Location", "fieldname": "location", "fieldtype": "Link"},
{"label": "Healthcare Service Unit", "fieldname": "service_unit", "fieldtype": "Link"},
{"label": "Start Time", "fieldname": "start_time", "fieldtype": "Time"},
{"label": "Exercises", "fieldname": "exercises", "fieldtype": "Table"},
{"label": "Total Counts Targeted", "fieldname": "total_counts_targeted", "fieldtype": "Int"},
{"label": "Total Counts Completed", "fieldname": "total_counts_completed", "fieldtype": "Int"}
]),
"Vital Signs": ("signs_date", [
{"label": "Body Temperature", "fieldname": "temperature", "fieldtype": "Data"},
{"label": "Heart Rate / Pulse", "fieldname": "pulse", "fieldtype": "Data"},
{"label": "Respiratory rate", "fieldname": "respiratory_rate", "fieldtype": "Data"},
{"label": "Tongue", "fieldname": "tongue", "fieldtype": "Select"},
{"label": "Abdomen", "fieldname": "abdomen", "fieldtype": "Select"},
{"label": "Reflexes", "fieldname": "reflexes", "fieldtype": "Select"},
{"label": "Blood Pressure", "fieldname": "bp", "fieldtype": "Data"},
{"label": "Notes", "fieldname": "vital_signs_note", "fieldtype": "Small Text"},
{"label": "Height (In Meter)", "fieldname": "height", "fieldtype": "Float"},
{"label": "Weight (In Kilogram)", "fieldname": "weight", "fieldtype": "Float"},
{"label": "BMI", "fieldname": "bmi", "fieldtype": "Float"}
]),
"Inpatient Medication Order": ("start_date", [
{"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
{"label": "Start Date", "fieldname": "start_date", "fieldtype": "Date"},
{"label": "End Date", "fieldname": "end_date", "fieldtype": "Date"},
{"label": "Medication Orders", "fieldname": "medication_orders", "fieldtype": "Table"},
{"label": "Total Orders", "fieldname": "total_orders", "fieldtype": "Float"}
])
}

View File

@ -6,6 +6,7 @@ from __future__ import unicode_literals
import math
import frappe
from frappe import _
from frappe.utils.formatters import format_value
from frappe.utils import time_diff_in_hours, rounded
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account
from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity
@ -648,11 +649,15 @@ def render_doc_as_html(doctype, docname, exclude_fields = []):
html += "<table class='table table-condensed table-bordered'>" \
+ table_head + table_row + "</table>"
continue
#on other field types add label and value to html
if not df.hidden and not df.print_hide and doc.get(df.fieldname) and df.fieldname not in exclude_fields:
html += '<br>{0} : {1}'.format(df.label or df.fieldname, \
doc.get(df.fieldname))
if doc.get(df.fieldname):
formatted_value = format_value(doc.get(df.fieldname), meta.get_field(df.fieldname), doc)
html += '<br>{0} : {1}'.format(df.label or df.fieldname, formatted_value)
if not has_data : has_data = True
if sec_on and col_on and has_data:
doc_html += section_html + html + '</div></div>'
elif sec_on and not col_on and has_data:

View File

@ -222,6 +222,11 @@ standard_queries = {
}
doc_events = {
"*": {
"on_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.create_medical_record",
"on_update_after_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.update_medical_record",
"on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.delete_medical_record"
},
"Stock Entry": {
"on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty",
"on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty"
@ -339,7 +344,8 @@ scheduler_events = {
"erpnext.selling.doctype.quotation.quotation.set_expired_status",
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_status",
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email"
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
"erpnext.non_profit.doctype.membership.membership.set_expired_status"
],
"daily_long": [
"erpnext.setup.doctype.email_digest.email_digest.send",
@ -408,9 +414,6 @@ regional_overrides = {
'Italy': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.italy.utils.update_itemised_tax_data',
'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.italy.utils.sales_invoice_validate',
},
'Germany': {
'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.germany.accounts_controller.validate_regional',
}
}
user_privacy_documents = [

View File

@ -88,7 +88,7 @@ def get_events(start, end, filters=None):
def add_assignments(events, start, end, conditions=None):
query = """select name, start_date, end_date, employee_name,
employee, docstatus
employee, docstatus, shift_type
from `tabShift Assignment` where
start_date >= %(start_date)s
or end_date <= %(end_date)s
@ -97,18 +97,40 @@ def add_assignments(events, start, end, conditions=None):
if conditions:
query += conditions
for d in frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True):
e = {
"name": d.name,
"doctype": "Shift Assignment",
"start_date": d.start_date,
"end_date": d.end_date if d.end_date else nowdate(),
"title": cstr(d.employee_name) + ": "+ \
cstr(d.shift_type),
"docstatus": d.docstatus
}
if e not in events:
events.append(e)
records = frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True)
shift_timing_map = get_shift_type_timing([d.shift_type for d in records])
for d in records:
daily_event_start = d.start_date
daily_event_end = d.end_date if d.end_date else getdate()
delta = timedelta(days=1)
while daily_event_start <= daily_event_end:
start_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['start_time']
end_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['end_time']
daily_event_start += delta
e = {
"name": d.name,
"doctype": "Shift Assignment",
"start_date": start_timing,
"end_date": end_timing,
"title": cstr(d.employee_name) + ": "+ \
cstr(d.shift_type),
"docstatus": d.docstatus,
"allDay": 0
}
if e not in events:
events.append(e)
return events
def get_shift_type_timing(shift_types):
shift_timing_map = {}
data = frappe.get_all("Shift Type", filters = {"name": ("IN", shift_types)}, fields = ['name', 'start_time', 'end_time'])
for d in data:
shift_timing_map[d.name] = d
return shift_timing_map
def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=False, next_shift_direction=None):

View File

@ -6,14 +6,8 @@ frappe.views.calendar["Shift Assignment"] = {
"start": "start_date",
"end": "end_date",
"id": "name",
"docstatus": 1
},
options: {
header: {
left: 'prev,next today',
center: 'title',
right: 'month'
}
"docstatus": 1,
"allDay": "allDay",
},
get_events_method: "erpnext.hr.doctype.shift_assignment.shift_assignment.get_events"
}

View File

@ -26,6 +26,7 @@ def get_columns(filters):
{"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80},
{"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100},
{"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100},
{"label": _("Price Valid Upto"), "fieldname": "price_valid_upto", "fieldtype": "Datetime", "width": 100},
{"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100},
{"label": _("% Of Applicant Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100},
{"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100},
@ -42,38 +43,53 @@ def get_data(filters):
currency = erpnext.get_company_currency(filters.get('company'))
for key, qty in iteritems(pledge_values):
row = {}
current_value = flt(qty * loan_security_details.get(key[1])['latest_price'])
row.update(loan_security_details.get(key[1]))
row.update({
'applicant_type': applicant_type_map.get(key[0]),
'applicant_name': key[0],
'total_qty': qty,
'current_value': current_value,
'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2),
'currency': currency
})
if qty:
row = {}
current_value = flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0))
valid_upto = loan_security_details.get(key[1], {}).get('valid_upto')
data.append(row)
row.update(loan_security_details.get(key[1]))
row.update({
'applicant_type': applicant_type_map.get(key[0]),
'applicant_name': key[0],
'total_qty': qty,
'current_value': current_value,
'price_valid_upto': valid_upto,
'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2) if total_value_map.get(key[0]) \
else 0.0,
'currency': currency
})
data.append(row)
return data
def get_loan_security_details(filters):
security_detail_map = {}
loan_security_price_map = {}
lsp_validity_map = {}
loan_security_price_map = frappe._dict(frappe.db.sql("""
SELECT loan_security, loan_security_price
loan_security_prices = frappe.db.sql("""
SELECT loan_security, loan_security_price, valid_upto
FROM `tabLoan Security Price` t1
WHERE valid_from >= (SELECT MAX(valid_from) FROM `tabLoan Security Price` t2
WHERE t1.loan_security = t2.loan_security)
""", as_list=1))
""", as_dict=1)
for security in loan_security_prices:
loan_security_price_map.setdefault(security.loan_security, security.loan_security_price)
lsp_validity_map.setdefault(security.loan_security, security.valid_upto)
loan_security_details = frappe.get_all('Loan Security', fields=['name as loan_security',
'loan_security_code', 'loan_security_name', 'haircut', 'loan_security_type',
'disabled'])
for security in loan_security_details:
security.update({'latest_price': flt(loan_security_price_map.get(security.loan_security))})
security.update({
'latest_price': flt(loan_security_price_map.get(security.loan_security)),
'valid_upto': lsp_validity_map.get(security.loan_security)
})
security_detail_map.setdefault(security.loan_security, security)
return security_detail_map
@ -118,6 +134,6 @@ def get_applicant_wise_total_loan_security_qty(filters, loan_security_details):
applicant_wise_unpledges.get((security.applicant, security.loan_security), 0.0)
total_value_map[security.applicant] += current_pledges.get((security.applicant, security.loan_security)) \
* loan_security_details.get(security.loan_security)['latest_price']
* loan_security_details.get(security.loan_security, {}).get('latest_price', 0)
return current_pledges, total_value_map, applicant_type_map

View File

@ -6,6 +6,8 @@ import frappe
import erpnext
from frappe import _
from frappe.utils import flt, getdate, add_days
from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applicant_wise_loan_security_exposure \
import get_loan_security_details
def execute(filters=None):
@ -31,6 +33,7 @@ def get_columns(filters):
{"label": _("Undue Booked Interest"), "fieldname": "undue_interest", "fieldtype": "Currency", "options": "currency", "width": 120},
{"label": _("Interest %"), "fieldname": "rate_of_interest", "fieldtype": "Percent", "width": 100},
{"label": _("Penalty Interest %"), "fieldname": "penalty_interest", "fieldtype": "Percent", "width": 100},
{"label": _("Loan To Value Ratio"), "fieldname": "loan_to_value", "fieldtype": "Percent", "width": 100},
{"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100},
]
@ -50,6 +53,9 @@ def get_active_loan_details(filters):
loan_list = [d.loan for d in loan_details]
current_pledges = get_loan_wise_pledges(filters)
loan_wise_security_value = get_loan_wise_security_value(filters, current_pledges)
sanctioned_amount_map = get_sanctioned_amount_map()
penal_interest_rate_map = get_penal_interest_rate_map()
payments = get_payments(loan_list)
@ -67,12 +73,16 @@ def get_active_loan_details(filters):
"penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")),
"penalty_interest": penal_interest_rate_map.get(loan.loan_type),
"undue_interest": flt(accrual_map.get(loan.loan, {}).get("undue_interest")),
"loan_to_value": 0.0,
"currency": currency
})
loan['total_outstanding'] = loan['principal_outstanding'] + loan['interest_outstanding'] \
+ loan['penalty']
if loan_wise_security_value.get(loan.loan):
loan['loan_to_value'] = flt((loan['principal_outstanding'] * 100) / loan_wise_security_value.get(loan.loan))
return loan_details
def get_sanctioned_amount_map():
@ -121,4 +131,53 @@ def get_interest_accruals(loans):
return accrual_map
def get_penal_interest_rate_map():
return frappe._dict(frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1))
return frappe._dict(frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1))
def get_loan_wise_pledges(filters):
loan_wise_unpledges = {}
current_pledges = {}
conditions = ""
if filters.get('company'):
conditions = "AND company = %(company)s"
unpledges = frappe.db.sql("""
SELECT up.loan, u.loan_security, sum(u.qty) as qty
FROM `tabLoan Security Unpledge` up, `tabUnpledge` u
WHERE u.parent = up.name
AND up.status = 'Approved'
{conditions}
GROUP BY up.loan, u.loan_security
""".format(conditions=conditions), filters, as_dict=1)
for unpledge in unpledges:
loan_wise_unpledges.setdefault((unpledge.loan, unpledge.loan_security), unpledge.qty)
pledges = frappe.db.sql("""
SELECT lp.loan, p.loan_security, sum(p.qty) as qty
FROM `tabLoan Security Pledge` lp, `tabPledge`p
WHERE p.parent = lp.name
AND lp.status = 'Pledged'
{conditions}
GROUP BY lp.loan, p.loan_security
""".format(conditions=conditions), filters, as_dict=1)
for security in pledges:
current_pledges.setdefault((security.loan, security.loan_security), security.qty)
current_pledges[(security.loan, security.loan_security)] -= \
loan_wise_unpledges.get((security.loan, security.loan_security), 0.0)
return current_pledges
def get_loan_wise_security_value(filters, current_pledges):
loan_security_details = get_loan_security_details(filters)
loan_wise_security_value = {}
for key in current_pledges:
qty = current_pledges.get(key)
loan_wise_security_value.setdefault(key[0], 0.0)
loan_wise_security_value[key[0]] += \
flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0))
return loan_wise_security_value

View File

@ -24,6 +24,7 @@ def get_columns(filters):
{"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80},
{"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100},
{"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100},
{"label": _("Price Valid Upto"), "fieldname": "price_valid_upto", "fieldtype": "Datetime", "width": 100},
{"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100},
{"label": _("% Of Total Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100},
{"label": _("Pledged Applicant Count"), "fieldname": "pledged_applicant_count", "fieldtype": "Percentage", "width": 100},
@ -39,18 +40,22 @@ def get_data(filters):
currency = erpnext.get_company_currency(filters.get('company'))
for security, value in iteritems(current_pledges):
row = {}
current_value = flt(value['qty'] * loan_security_details.get(security)['latest_price'])
row.update(loan_security_details.get(security))
row.update({
'total_qty': value['qty'],
'current_value': current_value,
'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2),
'pledged_applicant_count': value['applicant_count'],
'currency': currency
})
if value.get('qty'):
row = {}
current_value = flt(value.get('qty', 0) * loan_security_details.get(security, {}).get('latest_price', 0))
valid_upto = loan_security_details.get(security, {}).get('valid_upto')
data.append(row)
row.update(loan_security_details.get(security))
row.update({
'total_qty': value.get('qty'),
'current_value': current_value,
'price_valid_upto': valid_upto,
'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2),
'pledged_applicant_count': value.get('applicant_count'),
'currency': currency
})
data.append(row)
return data

View File

@ -12,7 +12,6 @@
"membership_expiry_date",
"column_break_5",
"membership_type",
"email",
"email_id",
"image",
"customer_section",
@ -64,13 +63,6 @@
"options": "Membership Type",
"reqd": 1
},
{
"fieldname": "email",
"fieldtype": "Link",
"in_list_view": 1,
"label": "User",
"options": "User"
},
{
"fieldname": "image",
"fieldtype": "Attach Image",
@ -178,7 +170,7 @@
],
"image_field": "image",
"links": [],
"modified": "2020-09-16 23:44:13.596948",
"modified": "2020-11-09 12:12:10.174647",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Member",

View File

@ -18,8 +18,6 @@ class Member(Document):
def validate(self):
if self.email:
self.validate_email_type(self.email)
if self.email_id:
self.validate_email_type(self.email_id)
@ -57,14 +55,16 @@ class Member(Document):
def make_customer_and_link(self):
if self.customer:
frappe.msgprint(_("A customer is already linked to this Member"))
cust = create_customer(frappe._dict({
customer = create_customer(frappe._dict({
'fullname': self.member_name,
'email': self.email_id or self.email,
'email': self.email_id,
'phone': None
}))
self.customer = cust
self.customer = customer
self.save()
frappe.msgprint(_("Customer {0} has been created succesfully.").format(self.customer))
def get_or_create_member(user_details):

View File

@ -4,16 +4,25 @@
frappe.ui.form.on('Membership', {
setup: function(frm) {
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
if (val) frm.set_df_property('razorpay_details_section', 'hidden', false);
if (val) frm.set_df_property("razorpay_details_section", "hidden", false);
})
},
refresh: function(frm) {
if (frm.doc.__islocal)
return;
!frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => {
frm.call("generate_invoice", {
save: true
}).then(() => {
frm.reload_doc();
frm.call({
doc: frm.doc,
method: "generate_invoice",
args: {save: true},
freeze: true,
freeze_message: __("Creating Membership Invoice"),
callback: function(r) {
if (r.invoice)
frm.reload_doc();
}
});
});
@ -27,6 +36,6 @@ frappe.ui.form.on('Membership', {
},
onload: function(frm) {
frm.add_fetch('membership_type', 'amount', 'amount');
frm.add_fetch("membership_type", "amount", "amount");
}
});

View File

@ -7,6 +7,7 @@
"engine": "InnoDB",
"field_order": [
"member",
"member_name",
"membership_type",
"column_break_3",
"membership_status",
@ -46,6 +47,8 @@
{
"fieldname": "membership_status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Membership Status",
"options": "New\nCurrent\nExpired\nPending\nCancelled"
},
@ -122,11 +125,18 @@
"fieldtype": "Link",
"label": "Invoice",
"options": "Sales Invoice"
},
{
"fetch_from": "member.member_name",
"fieldname": "member_name",
"fieldtype": "Data",
"label": "Member Name",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-09-19 14:28:11.532696",
"modified": "2021-01-21 16:31:20.032656",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership",
@ -158,7 +168,9 @@
}
],
"restrict_to_domain": "Non Profit",
"search_fields": "member, member_name",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "member_name",
"track_changes": 1
}
}

View File

@ -14,33 +14,43 @@ from erpnext.non_profit.doctype.member.member import create_member
from frappe import _
import erpnext
class Membership(Document):
def validate(self):
if not self.member or not frappe.db.exists("Member", self.member):
member_name = frappe.get_value('Member', dict(email=frappe.session.user))
# for web forms
user_type = frappe.db.get_value("User", frappe.session.user, "user_type")
if user_type == "Website User":
self.create_member_from_website_user()
else:
frappe.throw(_("Please select a Member"))
if not member_name:
user = frappe.get_doc('User', frappe.session.user)
member = frappe.get_doc(dict(
doctype='Member',
email=frappe.session.user,
membership_type=self.membership_type,
member_name=user.get_fullname()
)).insert(ignore_permissions=True)
member_name = member.name
self.validate_membership_period()
if self.get("__islocal"):
self.member = member_name
def create_member_from_website_user(self):
member_name = frappe.get_value("Member", dict(email_id=frappe.session.user))
if not member_name:
user = frappe.get_doc("User", frappe.session.user)
member = frappe.get_doc(dict(
doctype="Member",
email_id=frappe.session.user,
membership_type=self.membership_type,
member_name=user.get_fullname()
)).insert(ignore_permissions=True)
member_name = member.name
if self.get("__islocal"):
self.member = member_name
def validate_membership_period(self):
# get last membership (if active)
last_membership = erpnext.get_last_membership()
last_membership = erpnext.get_last_membership(self.member)
# if person applied for offline membership
if last_membership and not frappe.session.user == "Administrator":
# if last membership does not expire in 30 days, then do not allow to renew
if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) :
frappe.throw(_('You can only renew if your membership expires within 30 days'))
frappe.throw(_("You can only renew if your membership expires within 30 days"))
self.from_date = add_days(last_membership.to_date, 1)
elif frappe.session.user == "Administrator":
@ -54,11 +64,16 @@ class Membership(Document):
self.to_date = add_months(self.from_date, 1)
def on_payment_authorized(self, status_changed_to=None):
if status_changed_to in ("Completed", "Authorized"):
self.load_from_db()
self.db_set('paid', 1)
if status_changed_to not in ("Completed", "Authorized"):
return
self.load_from_db()
self.db_set("paid", 1)
settings = frappe.get_doc("Membership Settings")
if settings.enable_invoicing and settings.create_for_web_forms:
self.generate_invoice(with_payment_entry=settings.make_payment_entry, save=True)
def generate_invoice(self, save=True):
def generate_invoice(self, save=True, with_payment_entry=False):
if not (self.paid or self.currency or self.amount):
frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details"))
@ -66,34 +81,64 @@ class Membership(Document):
frappe.throw(_("An invoice is already linked to this document"))
member = frappe.get_doc("Member", self.member)
plan = frappe.get_doc("Membership Type", self.membership_type)
settings = frappe.get_doc("Membership Settings")
if not member.customer:
frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member)))
if not settings.debit_account:
frappe.throw(_("You need to set <b>Debit Account</b> in Membership Settings"))
if not settings.company:
frappe.throw(_("You need to set <b>Default Company</b> for invoicing in Membership Settings"))
plan = frappe.get_doc("Membership Type", self.membership_type)
settings = frappe.get_doc("Membership Settings")
self.validate_membership_type_and_settings(plan, settings)
invoice = make_invoice(self, member, plan, settings)
self.invoice = invoice.name
if with_payment_entry:
self.make_payment_entry(settings, invoice)
if save:
self.save()
return invoice
def validate_membership_type_and_settings(self, plan, settings):
settings_link = get_link_to_form("Membership Type", self.membership_type)
if not settings.debit_account:
frappe.throw(_("You need to set <b>Debit Account</b> in {0}").format(settings_link))
if not settings.company:
frappe.throw(_("You need to set <b>Default Company</b> for invoicing in {0}").format(settings_link))
if not plan.linked_item:
frappe.throw(_("Please set a Linked Item for the Membership Type {0}").format(
get_link_to_form("Membership Type", self.membership_type)))
def make_payment_entry(self, settings, invoice):
if not settings.payment_account:
frappe.throw(_("You need to set <b>Payment Account</b> in {0}").format(
get_link_to_form("Membership Type", self.membership_type)))
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
frappe.flags.ignore_account_permission = True
pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total)
frappe.flags.ignore_account_permission=False
pe.paid_to = settings.payment_account
pe.reference_no = self.name
pe.reference_date = getdate()
pe.save(ignore_permissions=True)
pe.submit()
def send_acknowlement(self):
settings = frappe.get_doc("Membership Settings")
if not settings.send_email:
frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> in Membership Settings"))
frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> in {0}").format(
get_link_to_form("Membership Settings", "Membership Settings")))
member = frappe.get_doc("Member", self.member)
if not member.email_id:
frappe.throw(_("Email address of member {0} is missing").format(frappe.utils.get_link_to_form("Member", self.member)))
plan = frappe.get_doc("Membership Type", self.membership_type)
email = member.email_id if member.email_id else member.email
email = member.email_id
attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)]
if self.invoice and settings.send_invoice:
@ -112,48 +157,56 @@ class Membership(Document):
}
if not frappe.flags.in_test:
frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args)
frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
else:
frappe.sendmail(**email_args)
def generate_and_send_invoice(self):
invoice = self.generate_invoice(False)
self.generate_invoice(save=False)
self.send_acknowlement()
def make_invoice(membership, member, plan, settings):
invoice = frappe.get_doc({
'doctype': 'Sales Invoice',
'customer': member.customer,
'debit_to': settings.debit_account,
'currency': membership.currency,
'is_pos': 0,
'items': [
"doctype": "Sales Invoice",
"customer": member.customer,
"debit_to": settings.debit_account,
"currency": membership.currency,
"company": settings.company,
"is_pos": 0,
"items": [
{
'item_code': plan.linked_item,
'rate': membership.amount,
'qty': 1
"item_code": plan.linked_item,
"rate": membership.amount,
"qty": 1
}
]
})
invoice.set_missing_values()
invoice.insert(ignore_permissions=True)
invoice.submit()
frappe.msgprint(_("Sales Invoice created successfully"))
return invoice
def get_member_based_on_subscription(subscription_id, email):
members = frappe.get_all("Member", filters={
'subscription_id': subscription_id,
'email_id': email
"subscription_id": subscription_id,
"email_id": email
}, order_by="creation desc")
try:
return frappe.get_doc("Member", members[0]['name'])
return frappe.get_doc("Member", members[0]["name"])
except:
return None
def verify_signature(data):
signature = frappe.request.headers.get('X-Razorpay-Signature')
if frappe.flags.in_test:
return True
signature = frappe.request.headers.get("X-Razorpay-Signature")
settings = frappe.get_doc("Membership Settings")
key = settings.get_webhook_secret()
@ -162,6 +215,7 @@ def verify_signature(data):
controller.verify_signature(data, signature, key)
@frappe.whitelist(allow_guest=True)
def trigger_razorpay_subscription(*args, **kwargs):
data = frappe.request.get_data(as_text=True)
@ -170,16 +224,16 @@ def trigger_razorpay_subscription(*args, **kwargs):
except Exception as e:
log = frappe.log_error(e, "Webhook Verification Error")
notify_failure(log)
return { 'status': 'Failed', 'reason': e}
return { "status": "Failed", "reason": e}
if isinstance(data, six.string_types):
data = json.loads(data)
data = frappe._dict(data)
subscription = data.payload.get("subscription", {}).get('entity', {})
subscription = data.payload.get("subscription", {}).get("entity", {})
subscription = frappe._dict(subscription)
payment = data.payload.get("payment", {}).get('entity', {})
payment = data.payload.get("payment", {}).get("entity", {})
payment = frappe._dict(payment)
try:
@ -189,15 +243,15 @@ def trigger_razorpay_subscription(*args, **kwargs):
member = get_member_based_on_subscription(subscription.id, payment.email)
if not member:
member = create_member(frappe._dict({
'fullname': payment.email,
'email': payment.email,
'plan_id': get_plan_from_razorpay_id(subscription.plan_id)
"fullname": payment.email,
"email": payment.email,
"plan_id": get_plan_from_razorpay_id(subscription.plan_id)
}))
member.subscription_id = subscription.id
member.customer_id = payment.customer_id
if subscription.notes and type(subscription.notes) == dict:
notes = '\n'.join("{}: {}".format(k, v) for k, v in subscription.notes.items())
notes = "\n".join("{}: {}".format(k, v) for k, v in subscription.notes.items())
member.add_comment("Comment", notes)
elif subscription.notes and type(subscription.notes) == str:
member.add_comment("Comment", subscription.notes)
@ -227,28 +281,39 @@ def trigger_razorpay_subscription(*args, **kwargs):
message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id)
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
notify_failure(log)
return { 'status': 'Failed', 'reason': e}
return { "status": "Failed", "reason": e}
return { 'status': 'Success' }
return { "status": "Success" }
def notify_failure(log):
try:
content = """Dear System Manager,
Razorpay webhook for creating renewing membership subscription failed due to some reason. Please check the following error log linked below
content = """
Dear System Manager,
Razorpay webhook for creating renewing membership subscription failed due to some reason.
Please check the following error log linked below
Error Log: {0}
Regards, Administrator
""".format(get_link_to_form("Error Log", log.name))
Error Log: {0}
Regards,
Administrator""".format(get_link_to_form("Error Log", log.name))
sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content)
except:
pass
def get_plan_from_razorpay_id(plan_id):
plan = frappe.get_all("Membership Type", filters={'razorpay_plan_id': plan_id}, order_by="creation desc")
plan = frappe.get_all("Membership Type", filters={"razorpay_plan_id": plan_id}, order_by="creation desc")
try:
return plan[0]['name']
return plan[0]["name"]
except:
return None
def set_expired_status():
frappe.db.sql("""
UPDATE
`tabMembership` SET `status` = 'Expired'
WHERE
`status` not in ('Cancelled') AND `to_date` < %s
""", (nowdate()))

View File

@ -0,0 +1,15 @@
frappe.listview_settings['Membership'] = {
get_indicator: function(doc) {
if (doc.membership_status == 'New') {
return [__('New'), 'blue', 'membership_status,=,New'];
} else if (doc.membership_status === 'Current') {
return [__('Current'), 'green', 'membership_status,=,Current'];
} else if (doc.membership_status === 'Pending') {
return [__('Pending'), 'yellow', 'membership_status,=,Pending'];
} else if (doc.membership_status === 'Expired') {
return [__('Expired'), 'grey', 'membership_status,=,Expired'];
} else {
return [__('Cancelled'), 'red', 'membership_status,=,Cancelled'];
}
}
};

View File

@ -2,8 +2,110 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
import frappe
import erpnext
from erpnext.non_profit.doctype.member.member import create_member
from frappe.utils import nowdate, add_months
class TestMembership(unittest.TestCase):
pass
def setUp(self):
# Get default company
company = frappe.get_doc("Company", erpnext.get_default_company())
# update membership settings
settings = frappe.get_doc("Membership Settings")
# Enable razorpay
settings.enable_razorpay = 1
settings.billing_cycle = "Monthly"
settings.billing_frequency = 24
# Enable invoicing
settings.enable_invoicing = 1
settings.make_payment_entry = 1
settings.company = company.name
settings.payment_account = company.default_cash_account
settings.debit_account = company.default_receivable_account
settings.save()
# make test plan
if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"):
plan = frappe.new_doc("Membership Type")
plan.membership_type = "_rzpy_test_milythm"
plan.amount = 100
plan.razorpay_plan_id = "_rzpy_test_milythm"
plan.linked_item = create_item("_Test Item for Non Profit Membership").name
plan.insert()
else:
plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
# make test member
self.member_doc = create_member(frappe._dict({
'fullname': "_Test_Member",
'email': "_test_member_erpnext@example.com",
'plan_id': plan.name
}))
self.member_doc.make_customer_and_link()
self.member = self.member_doc.name
def test_auto_generate_invoice_and_payment_entry(self):
entry = make_membership(self.member)
# Naive test to see if at all invoice was generated and attached to member
# In any case if details were missing, the invoicing would throw an error
invoice = entry.generate_invoice(save=True)
self.assertEqual(invoice.name, entry.invoice)
def test_renew_within_30_days(self):
# create a membership for two months
# Should work fine
make_membership(self.member, { "from_date": nowdate() })
make_membership(self.member, { "from_date": add_months(nowdate(), 1) })
from frappe.utils.user import add_role
add_role("test@example.com", "Non Profit Manager")
frappe.set_user("test@example.com")
# create next membership with expiry not within 30 days
self.assertRaises(frappe.ValidationError, make_membership, self.member, {
"from_date": add_months(nowdate(), 2),
})
frappe.set_user("Administrator")
# create the same membership but as administrator
make_membership(self.member, {
"from_date": add_months(nowdate(), 2),
"to_date": add_months(nowdate(), 3),
})
def set_config(key, value):
frappe.db.set_value("Membership Settings", None, key, value)
def make_membership(member, payload={}):
data = {
"doctype": "Membership",
"member": member,
"membership_status": "Current",
"membership_type": "_rzpy_test_milythm",
"currency": "INR",
"paid": 1,
"from_date": nowdate(),
"amount": 100
}
data.update(payload)
membership = frappe.get_doc(data)
membership.insert(ignore_permissions=True, ignore_if_duplicate=True)
return membership
def create_item(item_code):
if not frappe.db.exists("Item", item_code):
item = frappe.new_doc("Item")
item.item_code = item_code
item.item_name = item_code
item.stock_uom = "Nos"
item.description = item_code
item.item_group = "All Item Groups"
item.is_stock_item = 0
item.save()
else:
item = frappe.get_doc("Item", item_code)
return item

View File

@ -11,7 +11,7 @@ frappe.ui.form.on("Membership Settings", {
});
}
frm.set_query('inv_print_format', function(doc) {
frm.set_query("inv_print_format", function() {
return {
filters: {
"doc_type": "Sales Invoice"
@ -19,7 +19,7 @@ frappe.ui.form.on("Membership Settings", {
};
});
frm.set_query('membership_print_format', function(doc) {
frm.set_query("membership_print_format", function() {
return {
filters: {
"doc_type": "Membership"
@ -27,12 +27,23 @@ frappe.ui.form.on("Membership Settings", {
};
});
frm.set_query('debit_account', function(doc) {
frm.set_query("debit_account", function() {
return {
filters: {
'account_type': 'Receivable',
'is_group': 0,
'company': frm.doc.company
"account_type": "Receivable",
"is_group": 0,
"company": frm.doc.company
}
};
});
frm.set_query("payment_account", function () {
var account_types = ["Bank", "Cash"];
return {
filters: {
"account_type": ["in", account_types],
"is_group": 0,
"company": frm.doc.company
}
};
});

View File

@ -11,9 +11,12 @@
"billing_frequency",
"webhook_secret",
"column_break_6",
"enable_auto_invoicing",
"enable_invoicing",
"create_for_web_forms",
"make_payment_entry",
"company",
"debit_account",
"payment_account",
"column_break_9",
"send_email",
"send_invoice",
@ -58,14 +61,7 @@
"label": "Invoicing"
},
{
"default": "0",
"fieldname": "enable_auto_invoicing",
"fieldtype": "Check",
"label": "Enable Auto Invoicing",
"mandatory_depends_on": "eval:doc.send_invoice"
},
{
"depends_on": "eval:doc.enable_auto_invoicing",
"depends_on": "eval:doc.enable_invoicing",
"fieldname": "debit_account",
"fieldtype": "Link",
"label": "Debit Account",
@ -77,7 +73,7 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.enable_auto_invoicing",
"depends_on": "eval:doc.enable_invoicing",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
@ -86,7 +82,7 @@
},
{
"default": "0",
"depends_on": "eval:doc.enable_auto_invoicing && doc.send_email",
"depends_on": "eval:doc.enable_invoicing && doc.send_email",
"fieldname": "send_invoice",
"fieldtype": "Check",
"label": "Send Invoice with Email"
@ -119,11 +115,43 @@
"label": "Email Template",
"mandatory_depends_on": "eval:doc.send_email",
"options": "Email Template"
},
{
"default": "0",
"fieldname": "enable_invoicing",
"fieldtype": "Check",
"label": "Enable Invoicing",
"mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry"
},
{
"default": "0",
"depends_on": "eval:doc.enable_invoicing",
"description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.",
"fieldname": "make_payment_entry",
"fieldtype": "Check",
"label": "Make Payment Entry"
},
{
"depends_on": "eval:doc.make_payment_entry",
"fieldname": "payment_account",
"fieldtype": "Link",
"label": "Payment To",
"mandatory_depends_on": "eval:doc.make_payment_entry",
"options": "Account"
},
{
"default": "0",
"depends_on": "eval:doc.enable_invoicing",
"description": "Automatically create an invoice when payment is authorized from a web form entry",
"fieldname": "create_for_web_forms",
"fieldtype": "Check",
"label": "Auto Create Invoice for Web Forms"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-08-05 17:26:37.287395",
"modified": "2021-01-21 19:57:53.213286",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership Settings",

View File

@ -2,13 +2,21 @@
// For license information, please see license.txt
frappe.ui.form.on('Membership Type', {
refresh: function(frm) {
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
refresh: function (frm) {
frappe.db.get_single_value('Membership Settings', 'enable_razorpay').then(val => {
if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false);
});
frappe.db.get_single_value("Membership Settings", "enable_auto_invoicing").then(val => {
frappe.db.get_single_value('Membership Settings', 'enable_invoicing').then(val => {
if (val) frm.set_df_property('linked_item', 'hidden', false);
});
frm.set_query('linked_item', () => {
return {
filters: {
is_stock_item: 0
}
};
});
}
});

View File

@ -5,9 +5,14 @@
from __future__ import unicode_literals
from frappe.model.document import Document
import frappe
from frappe import _
class MembershipType(Document):
pass
def validate(self):
if self.linked_item:
is_stock_item = frappe.db.get_value("Item", self.linked_item, "is_stock_item")
if is_stock_item:
frappe.throw(_("The Linked Item should be a service item"))
def get_membership_type(razorpay_id):
return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id})

View File

@ -677,7 +677,7 @@ erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123
erpnext.patches.v12_0.fix_quotation_expired_status
erpnext.patches.v12_0.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")
@ -737,13 +737,18 @@ erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail
erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02
erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.update_reason_for_resignation_in_employee
erpnext.patches.v13_0.update_custom_fields_for_shopify
execute:frappe.delete_doc("Report", "Quoted Item Comparison")
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
execute:frappe.rename_doc("Workspace", "Loan", "Loan Management", ignore_if_exists=True, force=True)
erpnext.patches.v13_0.create_uae_pos_invoice_fields
erpnext.patches.v13_0.update_project_template_tasks
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

View File

@ -0,0 +1,26 @@
from __future__ import unicode_literals
import frappe
from frappe.custom.doctype.property_setter.property_setter import make_property_setter, delete_property_setter
def execute():
frappe.reload_doc("projects", "doctype", "project")
projects = frappe.db.get_all("Project",
fields=["name", "naming_series", "modified"],
filters={
"naming_series": ["is", "not set"]
},
order_by="timestamp(modified) asc")
# disable set only once as the old docs must be saved
# (to bypass 'Cant change naming series' validation on save)
make_property_setter("Project", "naming_series", "set_only_once", 0, "Check")
for entry in projects:
# need to save the doc so that users can edit old projects
doc = frappe.get_doc("Project", entry.name)
if not doc.naming_series:
doc.naming_series = "PROJ-.####"
doc.save()
delete_property_setter("Project", "set_only_once", "naming_series")
frappe.db.commit()

View File

@ -0,0 +1,14 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from erpnext.regional.united_arab_emirates.setup import make_custom_fields
def execute():
company = frappe.get_all('Company', filters = {'country': ['in', ['Saudi Arabia', 'United Arab Emirates']]})
if not company:
return
make_custom_fields()

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